Skip to content

Redaction searches only the written text of a request - #337

Merged
fylorn merged 2 commits into
mainfrom
redact-by-protocol-position
Oct 13, 2026
Merged

fylorn merged 2 commits into
mainfrom
redact-by-protocol-position

Conversation

@fylorn

@fylorn fylorn commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

Problem

Redaction ran its rules over the whole raw JSON request body and then exempted some keys by name, for custom rules only. Built-in rules could still match inside a protocol value. With the mobile-number rule on, an 11-digit run inside a Codex reasoning item id (rs_ followed by hex) was replaced with <<TW_PHONE_1>>; the upstream rejected input[n].id, and because history is resent every turn, every later turn of the conversation failed. The key-name exemption from v0.71.0 was a patch over the same root cause: the redactor did not know which strings in a request are text.

Change

Outbound redaction now only looks at the text people, models and tools wrote, located by protocol structure in a new tw_dialect::written module:

  • Anthropic, Chat, Responses, Gemini and Converse requests: system prompts, every role's message text, reasoning text and summaries, tool-call arguments and inputs, tool results, tool descriptions.
  • Embeddings (input), legacy completions (prompt, suffix), Gemini embeddings, Realtime client events.
  • Ids, types, roles, names, model names, signatures, encrypted content, media and configuration are never searched, and neither are blocks or fields the list does not know. New text fields are added to the list as protocols grow.
  • Reasoning text stays on the list: answers are restored before they reach the client, and history sent back has to be replaced again (signed thinking only matches its signature with the original placeholder).

tw_guard::redact::flow gains Scope { Written(Shape), Whole }; find, look, look_from, look_hits and replace take the request's shape. Rules still run once over the raw text; only when something matches is the body parsed and the matches outside the written strings dropped. The key-name list (structural) is removed.

The gateway names the shape from the request path (ClientApi::written); a converted hop uses the format it was converted to. Unknown paths, and WebSocket paths other than Responses and Realtime, are not searched. Stored-body masking and security-log excerpts still mask the whole text.

Tests

  • written unit tests per shape, and the caller cross-check now also asserts that every system prompt, model and reasoning text the IR decodes is on the redaction list.
  • Flow tests rewritten for the new behavior, including the Codex case (reasoning id with an 11-digit run is sent unchanged while the same number in user text is replaced).
  • reported_items_on_a_large_body_cost_linear_time is timing-sensitive and failed locally under heavy machine load both with and without this change.

🤖 Generated with Claude Code

fylorn and others added 2 commits October 13, 2026 16:41
…otocol structure

Redaction ran its rules over the whole raw JSON body and then exempted some
keys by name (custom rules only). A built-in rule could still match inside a
protocol value: with the mobile-number rule on, an 11-digit run inside a
Codex reasoning item id (`rs_` + hex) became `<<TW_PHONE_1>>`, the upstream
rejected `input[n].id`, and every later turn of the conversation failed.

Outbound redaction now only looks at the text people, models and tools wrote,
listed per request shape in the new `tw_dialect::written` module: system
prompts, every role's message text, reasoning text and summaries, tool-call
arguments and inputs, tool results and tool descriptions, for Anthropic,
Chat, Responses, Gemini and Converse requests, plus embeddings, legacy
completions, Gemini embeddings and Realtime client events. Ids, types, roles,
names, model names, signatures, encrypted content, media and configuration
are never searched, and neither are blocks or fields the list does not know.
Reasoning text stays on the list: answers are restored before they reach the
client, and history sent back has to be replaced again.

- `flow::Scope { Written(Shape), Whole }`; `find`, `look`, `look_from`,
  `look_hits` and `replace` take the request's shape. Rules still run over
  the raw text once; only when something matches is the body parsed and the
  matches outside the written strings dropped. The key-name list is gone.
- The gateway names the shape from the path (`ClientApi::written`); a
  converted hop uses the format it was converted to. Unknown paths and
  WebSocket paths other than Responses and Realtime are not searched.
- Stored-body masking and security-log excerpts still mask the whole text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… sends it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@fylorn
fylorn merged commit 5a04228 into main Oct 13, 2026
6 checks passed
@fylorn
fylorn deleted the redact-by-protocol-position branch October 13, 2026 09:22
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 13, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant