From bfe763021ecfec65e9107ff8ce9c5327841e1c71 Mon Sep 17 00:00:00 2001 From: Rogee Date: Sat, 10 Oct 2026 02:56:43 +0800 Subject: [PATCH] fix: recognize content ownership within prefixed error chains --- internal/logging/text.go | 17 ++++++++++++----- internal/logging/text_security_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/internal/logging/text.go b/internal/logging/text.go index d2e5ff9..204135c 100644 --- a/internal/logging/text.go +++ b/internal/logging/text.go @@ -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, "") diff --git a/internal/logging/text_security_test.go b/internal/logging/text_security_test.go index 56b3fee..6cbb174 100644 --- a/internal/logging/text_security_test.go +++ b/internal/logging/text_security_test.go @@ -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"} {