-
Notifications
You must be signed in to change notification settings - Fork 784
fix(redact,responses): close the credential and lifecycle-identity variant gaps #1038
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,12 +13,22 @@ const SECRET_VALUE_PATTERNS: Array<[RegExp, string]> = [ | |
| [/\btid=[A-Za-z0-9-]+(?:;[A-Za-z0-9_.-]+=[^;\s"']*)+(?::[A-Za-z0-9+/=_-]+)?/g, REDACTED_SECRET], | ||
| [/\b((?:api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|client[_-]?secret|refreshToken|accessToken|clientSecret|apiKey)=)([^&\s"',;]+)/gi, `$1${REDACTED_SECRET}`], | ||
| // Colon-labelled credentials. Upstream error bodies quote the offending header | ||
| // or field back at us ("x-api-key: abc…"), and the `=` rule above never fires | ||
| // for that shape, so the credential survived into client-visible error text. | ||
| // Header-style names are included because that is exactly what a provider | ||
| // echoes when it rejects a request. A `Bearer <token>` value is left to the | ||
| // dedicated rule above so its scheme prefix stays readable in diagnostics. | ||
| [/\b((?:x-api-key|x-goog-api-key|x-amz-security-token|api[_-]?key|apiKey|access[_-]?token|accessToken|refresh[_-]?token|refreshToken|id[_-]?token|client[_-]?secret|clientSecret|authorization|proxy-authorization|cookie|password|secret|token)\s*:\s*)(?!\s)(?!Bearer\b)([^\s"',;]+)/gi, `$1${REDACTED_SECRET}`], | ||
| // or field back at us ("x-api-key: abc…"), and the `=` rules never fire for | ||
| // that shape, so the credential survived into client-visible error text. | ||
| // | ||
| // The value class deliberately runs to end-of-line rather than stopping at a | ||
| // quote, space, or semicolon. A first attempt tokenized on those characters | ||
| // and leaked every delimiter-bearing variant: `x-api-key: "quoted…"` kept the | ||
| // whole quoted secret, `Authorization: Basic dXNlcjpwYXNz` kept the payload | ||
| // after the scheme, and `Cookie: a=1; b=2` kept everything after the first | ||
| // `;`. A credential header's value IS the rest of the line, so that is what | ||
| // gets masked. | ||
| // | ||
| // `Bearer` is the one readable exception: an auth scheme is diagnostically | ||
| // useful and the dedicated rule above already masks its token, so the scheme | ||
| // word is preserved and only what follows is consumed here. Other schemes | ||
| // (Basic, Digest, …) are masked whole, since their payload is the credential. | ||
| [/\b((?:x-api-key|x-goog-api-key|x-amz-security-token|api[_-]?key|apiKey|access[_-]?token|accessToken|refresh[_-]?token|refreshToken|id[_-]?token|client[_-]?secret|clientSecret|authorization|proxy-authorization|cookie|set-cookie|password|secret|token)\s*:\s*(?:Bearer\s+)?)(?!\s*$)[^\r\n]+/gi, `$1${REDACTED_SECRET}`], | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This replacement adds AGENTS.md reference: AGENTS.md:L218-L223 Useful? React with 👍 / 👎. |
||
| [/((?:"(?:api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|client[_-]?secret|refreshToken|accessToken|clientSecret|apiKey)"\s*:\s*"))([^"]+)(")/gi, `$1${REDACTED_SECRET}$3`], | ||
| // Raw JSON "token" field values (Copilot token exchange bodies echo the credential here). | ||
| [/(("token"\s*:\s*"))([^"]+)(")/gi, `$1${REDACTED_SECRET}$4`], | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -368,6 +368,18 @@ export function createResponsesSnapshotBlockRewrite( | |
| && isPlainObject(event.part)) { | ||
| if (outputIndex !== undefined) { | ||
| const open = openItems.get(outputIndex); | ||
| // A PRESENT-but-mismatched item_id is contradictory lifecycle evidence, | ||
| // exactly like output_item.done and output_text.done: the stream is | ||
| // telling us our identity model for this index is wrong. Merely | ||
| // ignoring it left the item open and let the terminal fabricate a full | ||
| // closure sequence (content_part.added → output_text.done → | ||
| // content_part.done → output_item.done) on top of a stream we do not | ||
| // understand. Go fail-closed instead. An OMITTED item_id stays | ||
| // legitimate and is still correlated by output_index. | ||
| if (open && itemId !== undefined && itemId !== open.itemId) { | ||
| taintAndRelease(); | ||
| return [changed ? jsonBlock(nextEvent) : block]; | ||
|
Comment on lines
+379
to
+381
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a sparse/malformed provider sends Useful? React with 👍 / 👎. |
||
| } | ||
| // Correlate by item_id when present: a mismatched event must not | ||
| // mutate (or suppress injections for) the tracked item (#893 review). | ||
| if (open && (itemId === undefined || itemId === open.itemId)) { | ||
|
|
@@ -398,6 +410,14 @@ export function createResponsesSnapshotBlockRewrite( | |
| } | ||
| if (type === "response.output_text.delta" && typeof event.delta === "string" && outputIndex !== undefined) { | ||
| const open = openItems.get(outputIndex); | ||
| // Same identity contract as the *.done terminals: a present-but-foreign | ||
| // item_id on a tracked index means our model of this index is wrong, and | ||
| // reconstructing from the text we DID accept would ship a message the | ||
| // upstream never assembled that way. | ||
| if (open && itemId !== undefined && itemId !== open.itemId) { | ||
| taintAndRelease(); | ||
| return [changed ? jsonBlock(nextEvent) : block]; | ||
|
Comment on lines
+417
to
+419
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This foreign- Useful? React with 👍 / 👎. |
||
| } | ||
| if (open && (itemId === undefined || itemId === open.itemId)) { | ||
| const deltaBytes = Buffer.byteLength(event.delta, "utf8"); | ||
| open.text += event.delta; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
redactSecretStringis given raw JSON/error text containing a colon-labelled credential inside a string, this end-of-line match consumes the closing quote and the rest of the JSON line; for example{"error":{"message":"x-api-key: secret"}}becomes an unterminated{"error":{"message":"x-api-key: [REDACTED]. Several callers redact raw upstream error text before returning diagnostics, so this can mangle otherwise parseable provider payloads; keep the whole-header behavior only for actual header lines or preserve trailing JSON punctuation.Useful? React with 👍 / 👎.