Repository navigation
Mask secrets inside carried compaction summaries; fall back when automatic cache breakpoints are refused - #308
Merged
Conversation
…matic cache breakpoints are refused
Compaction summaries at rest. The summary we hand Codex as a compaction item
is `tw1.c.` plus base64url text, opaque to rule-based redaction and to
shape-based masking. Codex sends it back every turn, so a secret mentioned in
the summary could reach the stored request body. Before a body is written,
each decodable carried summary is now decoded, masked with the same rules,
ledger placeholders and shape masking, re-encoded and put back in place. The
rest of the body is masked as before, still reusing the hits found on the
request path. Damaged or truncated values are masked as ordinary text.
Automatic cache breakpoints.
- Bedrock: automatic `cachePoint`s now go only to Claude models AWS lists
for prompt caching: every Claude from 4.5 on (unlisted newer ids included),
plus 3.7 Sonnet and 3.5 Sonnet v2. Matched by family and version in the id,
so cross-region inference profile ids and ARNs pointing at them are
recognized. Sonnet 4, Opus 4/4.1, 3.5 Haiku and Claude 3 get none.
- Safety net for Anthropic-format and Bedrock upstreams: if the request
carries only automatic breakpoints and the upstream answers 400 mentioning
`cache_control` / `cachePoint` / prompt caching, the hop is sent once more
without them. That upstream and model is remembered (in memory, bounded) so
later requests leave them out. Breakpoints the client set itself are never
removed.
- Speed probes no longer carry automatic breakpoints.
New `tw_dialect::cache::{may_have_marks, strip_marks}`; docs updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes for two problems the 0.67.0 release review found in #305 / #306. Meant for core 0.67.1.
1. Secrets inside carried compaction summaries at rest
The summary returned to Codex as a compaction item (#305) is
tw1.c.+ base64url text. Before storage, both masking passes look at the body: the rule-based replacement (Redaction::apply_found) and the shape-basedtw_secret::mask_body. Neither can see inside that base64. Codex sends the item back on every request, so a key the summary mentions could be written to the stored request body.Today the shape masker usually swallows the whole long token, since it looks like an opaque credential. That leaves the summary unreadable rather than masked, and nothing guarantees it: a short token, or a long one with fewer than two digits, isn't treated as a credential.
Fix (
tw-gateway/src/bodies.rs): before a body is written, every decodable carried summary in it is decoded, masked by the sameRedaction, re-encoded and put back in place. That covers rule hits, intercept-mode ledger placeholders, and shape masking. It applies to request, after-plugins and response bodies, streamed or whole.BodyRecord::found) are still reused for everything outside the summaries; the summaries are scanned fresh.mask_bodyuses, so masking segment by segment equals masking the whole text.2. Automatic cache breakpoints on Bedrock, and a fallback when they're refused
Allowlist
#306 marked every
anthropic.claude*id. AutomaticcachePoints now go only to the Claude models AWS lists under Supported models, Regions, and explicit caching limits (https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html, read 2026-10-09).That table lists:
20241022, Preview)Not listed: Claude 3 Haiku / Sonnet / Opus, 3.5 Sonnet v1, 3.5 Haiku, Sonnet 4, Opus 4 / 4.1.
Rule (
bedrock::request::caches_automatically): parse family and version from the id, in both naming schemes (claude-sonnet-4-5-20250929-v1:0,claude-3-7-sonnet-…).20241022.us.,eu.,apac.,global.…), dates and-v1:0tails don't matter. An inference-profile ARN that names the model is recognized; an application inference profile ARN (which names nothing) gets none.Safety net (Anthropic-format and Bedrock upstreams)
When a converted request carries only automatic breakpoints (the client set none) and the upstream answers 400 with a message that mentions
cache_control,cachePointor prompt caching:tw_dialect::cache::strip_marks). Bedrock re-signs the new body.AppState::cache_marks), at most 1024 entries with the oldest dropped first, kept across config reloads. Later requests leave the marks out up front.tracing::info.l3) no longer carry automatic breakpoints. They're too short to cache, and a refusal would count as a failed probe.Docs (
docs/config.md,docs/config.zh-CN.md) describe the Bedrock rule and the fallback. There is no config key; nomsg!sentence changed.API
Additive only: new
tw_dialect::cache::{may_have_marks, strip_marks}. The Bedrock allowlist is private to the encoder. The gateway changes are intw-gateway, which the enterprise edition doesn't use.Tests
bodies.rs:sk-ant-…key and anAKIA…id is masked inside, in a request body, a streamed response body and a whole response body. The JSON stays valid and the rest of the summary stays readable.tw1.c.a, non-base64, cut UTF-8, standard-alphabet base64, a bare prefix) give exactly the plain shape-masked result and never panic.conversion.rs).cache_control: one resend without it, then no marks on the next request (conversion.rs).cachePoint: one re-signed resend, then remembered; an Anthropic client's owncache_controlrefused by Bedrock passes the 400 through unchanged (bedrock.rs).strip_markson both formats, including a body that only mentions the word.Checks:
cargo fmt --all --check,cargo clippy --workspace --all-targets -- -D warnings,cargo test --workspace --no-fail-fastwith proxies unset (3159 passed, 0 failed, 8 ignored),scripts/smoke.sh(79 passed, 0 failed).🤖 Generated with Claude Code