fix: recognize content ownership within prefixed error chains

This commit is contained in:
2026-10-10 02:56:43 +08:00
parent 902eaa87e4
commit bfe763021e
2 changed files with 37 additions and 5 deletions
+12 -5
View File
@@ -115,6 +115,7 @@ func redactForm(body string) (string, bool) {
}
var textFieldLine = regexp.MustCompile(`^\s*([A-Za-z_][A-Za-z0-9_.-]*)\s*[:=]`)
var textFieldLabel = regexp.MustCompile(`\b([A-Za-z_][A-Za-z0-9_.-]*)\s*[:=]`)
func redactConversationLines(body string) string {
lines := strings.SplitAfter(body, "\n")
@@ -133,14 +134,20 @@ func redactConversationLines(body string) string {
}
hiding = false
}
if len(field) > 0 && sensitive(field[1]) {
lines[i] = "[REDACTED_FIELD]"
// Labels establish ownership wherever they occur in an error chain, not
// only at line start. Preserve the preceding actual failure description.
for _, span := range textFieldLabel.FindAllStringSubmatchIndex(line, -1) {
key := line[span[2]:span[3]]
if !sensitive(key) {
continue
}
lines[i] = line[:span[0]] + "[REDACTED_FIELD]"
if strings.HasSuffix(line, "\n") {
lines[i] += "\n"
}
// Credentials/header fields own their single line, not unrelated
// following diagnostics. Only conversational fields own continuation.
hiding = conversationField(field[1])
// Credentials own only this line; content owns the following paragraphs.
hiding = conversationField(key)
break
}
}
return strings.Join(lines, "")
+25
View File
@@ -61,6 +61,31 @@ func TestContentFieldClassificationAndEveryLoggingPathAgree(t *testing.T) {
}
}
func TestPrefixedContentLabelsKeepReasonAndOwnFollowingDialogue(t *testing.T) {
for _, prefix := range []string{"upstream failed: ", "wrapped error: upstream failed: ", "upstream failed; ", "上游失败:upstream failed: "} {
for _, field := range []string{"conversation", "history", "messages", "system_message", "prompt", "final_transcript"} {
value := prefix + field + ":\n\nuser: private-user-turn\n\nassistant: private-assistant-turn\nstatus=retry_pending\nattempts=2"
for _, debug := range []bool{false, true} {
out := installTestLogger(t, debug)
slog.Error("request failed", "error", errors.New(value), "detail", value)
slog.Info(value, "stage", "failure_observed")
TextBody(t.Context(), "HTTP text observed", []byte(value), "transport", "http")
text := out.String()
for _, secret := range []string{"private-user-turn", "private-assistant-turn"} {
if strings.Contains(text, secret) {
t.Fatalf("prefix %q field %s debug %v leak: %s", prefix, field, debug, text)
}
}
for _, keep := range []string{"upstream failed", "retry_pending", "attempts=2"} {
if !strings.Contains(text, keep) {
t.Fatal("business reason lost", text)
}
}
}
}
}
}
func TestConversationOwnershipPersistsAcrossBlankLines(t *testing.T) {
for _, gap := range []string{"\n", "\n\n", "\n \n\t\n", "\r\n\r\n"} {
for _, label := range []string{"messages", "prompt", "transcript", "content"} {