Repository navigation
Scrub long transient values without a regex and find short base64 - #1684
Merged
Merged
Conversation
scrubTransientCommsText built one global regex alternation of the scope's values. V8 refuses a literal of 32,768 characters once the regex alternates, so a received link that long, registered with the code it carries, made the scope fail with TRANSIENT_NARRATION_SECRET_LIMIT and the analysis fail. scrubValuesAsWritten in secret-scrub.ts is the new interface: it finds every occurrence of each value with eachOccurrence (from #1674), keeps the longest value per start position, and sweeps once from the left, resuming after each replaced value. That is the regex's output without its size limit. The transient scope loses its pattern cache and the try/catch that turned a regex failure into the limit error. The registration limits stay. They bound memory and the known-value scrub's cost per text; the regex limit was not their reason: the per-value limit is 65,536 bytes, twice what the regex accepts, and 8192 values of 128 bytes compile. Checked: the old regex is kept as modelLiteralScrub in the test model; the property that scrubTransientCommsText equals it passed 10,000 cases on seeds 271828 and 99991. Mutations (first value found instead of the longest; merged overlaps) fail it. A 32,768- and a 65,536-character link next to a code are scrubbed (red first: TRANSIENT_NARRATION_SECRET_LIMIT). Not checked: a live run. Part of #1646. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A value of 4 or 5 bytes, such as a 4- or 5-digit code, has an unpadded base64 form under the 8-character minimum, so `abcd` written `YWJjZA` survived both known-value scrubs. MIN_ENCODED_FORM is now 6, and the unpadded standard base64 form is listed explicitly (base64url covered it only for values without `+` or `/`). A 5-byte value is now found at every byte offset; a 4-byte value inside a longer base64 run keeps only 4 or 5 stable characters and stays unfound. The contract says so and a test records it. Measured on the text files of the 0.114.0 and 0.115.0 smoke runs (9 runs, 2.48 M characters, read in place): no 6- or 7-character form of any 4-, 5- or 6-digit code, 1,718 word values or 100,000 random tokens occurs. The only hits are inside observer/index.html's embedded base64, which no scrub reads. None of the 1,452 distinct 6- and 7-character tokens in that text is the unpadded base64 of printable ASCII. RunSecrets uses encodedForms too, so run evidence gets the same forms. Checked: red first on `YWJjZA`, `Pz8/Pw` and `NzQzOTI`; the reference model's minimum moved to 6 and all properties passed 10,000 cases on seeds 271828 and 99991. Restoring 8, or dropping the unpadded standard form, fails the properties. evidence, run, comms, analysis, verify and routes tests: 2989 passed. Part of #1646. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
# Conflicts: # CHANGELOG.md
A Codex review of #1684 measured the linear search at the registration limits (8192 values, 1 MiB) on 64 KiB of text: 729 ms for values that nearly match and 2,065 ms for values that overlap at every position, where the regex took 3.1 and 1.7 ms. The transient scrub runs the literal pass twice per field. scrubValuesAsWritten now builds the alternation regex as before and runs it. V8 compiles a regex at its first use and refuses it there once the regex alternates and a value has 32,768 characters; the scrub catches that and finds each value itself (replaceLongestFirst), with the same output. The transient scope caches the scrub per set of values again, as it cached the regex. Checked: a timing test at the limits times the scrub and a regex built in the test in turn, fastest of five, and bounds the ratio at 10 with a 5 ms floor; red first at ratios 185 and 530. The literal property runs through both paths, the second forced by registering a 32,768-character value no text holds; 10,000 cases on seeds 271828 and 99991 each. Keeping the first value found instead of the longest fails the fallback path's property. Part of #1646. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RunSecrets.scrub had no final check. With the 6-character base64 forms, values ["abcd", "SECRET]x"] on "SECRET]x YWJjZAx" came out as "[REDACTED_SECRET] [REDACTED_SECRET]x": the marker written for `YWJjZA` spells `SECRET]x` again. The shared and transient scrubs catch this with holdsSecretValue; RunSecrets now does the same and returns its marker in place of the whole text. The check is rebuilt after `add` holds a new value, since the scrub reads the values on each call. Routing RunSecrets through scrubSecretValues would remove no copy: that scrub returns decoded text, and RunSecrets keeps every other character as written. Checked: example test with the review's input, red first; a value added later is checked too. run, routes, actors, study, comms, evidence, analysis and verify tests: 5165 passed. Part of #1646. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RunSecrets found the stretches a value's view drops or decodes with one regex. Its operating-system command branch, `\x1b\][^\x07]*(?:\x07|\x1b\\)`, searched to the end of the text and back from every start that had no BEL after it, so "\x1b]".repeat(n / 2) took 361 ms at 16 KiB and 3.4 s at 64 KiB (Codex review of #1684). escapeSequences (src/run/escape-sequences.ts) returns the same matches in time linear in the text: the six shapes start with different characters, a command ends at the first BEL after it, found once per stretch, or else at the last string terminator in the text, which is what the regex's greedy match gives. viewSpans reads it; the regex is deleted from src and kept in the test model. Checked: escapeSequences equals the old regex on generated text of escape pieces, 10,000 cases on seeds 271828 and 99991 and 50,000 on seed 5; ending a command at the first terminator fails it once the generator draws command pieces more often. Scaling tests through RunSecrets.scrub at 8 KiB and 64 KiB: the command shape failed at ratio 61 before, and two other shapes guard the rest. run, terminal route and evidence tests pass. Part of #1646. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Codex review of #1684 built identifiers that hold a short value's base64 middle, such as `aMTIzNAz` with the code `1234` registered; the scrub redacts that part. The contract now states this cost next to the 6-character minimum, with the corpus count that set it, and an example test records the case. CHANGELOG lines cover the three follow-up fixes. Part of #1646. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
prose:check counts BEL as all-caps emphasis and "used to" as history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch was successfully deployed
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.
Closes #1646. #1674 merged the main hardening; this PR does the three items left after it, then the fixes from the Codex security review of this PR. idiom.md in the 10-08 audit ("fast-check for #1674") calls these items property-shaped; each now has a property test, examples, or both.
What changed
Literal scrub (
src/evidence/secret-scrub.ts,src/run/transient-comms-secrets.ts, item 1 and review finding 4)scrubValuesAsWritten(values)(text), the same shape asscrubSecretValues.scrubTransientCommsTextused to build, longest value first, and runs it.replaceLongestFirst, with the same output.replaceLongestFirstuseseachOccurrence(Deepen text readings and test the scrub against an independent model #1674's linear search) to find every occurrence of each value. It keeps the longest value at each start position and resumes after each replacement, so it never searches a marker it wrote.TRANSIENT_NARRATION_SECRET_LIMIT. The analysis then failed withanalysis_validation_failed_unexpected.literal), as it cached the regex. The try/catch that turned a regex failure into the limit error is deleted.Short base64 (
src/evidence/secret-scrub.ts, item 2): implemented.MIN_ENCODED_FORMis 6 (was 8).encodedFormslists unpadded standard base64 itself. base64url supplied it only for a value whose base64 has no+or/.abcdwrittenYWJjZA,????writtenPz8/Pw, and a 5-byte value at every byte offset.aMTIzNAzbecomesa[REDACTED_SECRET]zwhile the code1234(MTIzNA) is registered.docs/contracts/study-analysis.mdstates this next to the minimum, and an example test records it. The minimum stays at 6 on the corpus evidence below.RunSecrets(src/run/secrets.ts) readsencodedForms, so run evidence gets the same forms.Evidence, read in place from the 0.114.0 and 0.115.0 smoke runs:
.json/.ndjson/.mdfiles, 2.48 M characters.observer/index.htmlfiles: 11.40 M characters of the Observer bundle, whose embedded base64 no scrub reads.RunSecrets final check (
src/run/secrets.ts, review finding 1)["abcd","SECRET]x"]onSECRET]x YWJjZAxcame out as[REDACTED_SECRET] [REDACTED_SECRET]x. The marker written forYWJjZAspellsSECRET]xagain.RunSecrets.scrubnow ends with theholdsSecretValuecheck the shared and transient scrubs have. If the result still holds a held value in anyreadingsOf, the whole text becomes the marker.addholds a new value, because the scrub reads the values on each call.scrubSecretValuesinstead would remove no copy. That scrub returns decoded text, and RunSecrets keeps every other character as written.Terminal escape sequences in one pass (
src/run/escape-sequences.ts, review finding 5, which predates this PR)\x1b\][^\x07]*(?:\x07|\x1b\\), searched to the end of the text and back from every start with no bell character after it.escapeSequences(text)returns the same stretches in one pass.viewSpansreads it. The regex is deleted fromsrcand kept in the test model as the reference.src/observer/data.ts:356, outside this PR's files.Values that are part of a marker or hold one (item 3): no change.
CODE,LOBBY,TEXTandPATHare each part of a marker humanish writes.transientCommsKnownValueScrubon this branch:["CODE"],The subject CODE arrived; the lobby showed [REDACTED_LOBBY_CODE].givesThe subject [REDACTED_SECRET] arrived; the lobby showed [REDACTED_LOBBY_[REDACTED_SECRET]].["[REDACTED_SECRET] for you"],Subject: [REDACTED_SECRET] for yougivesSubject: [REDACTED_SECRET].["Re: [REDACTED_SECRET]"],Opened Re: [REDACTED_SECRET] and Re%3A%20%5BREDACTED_SECRET%5DgivesOpened [REDACTED_SECRET] and [REDACTED_SECRET].markerDrawn) and passed at 10,000 cases on two seeds.Tests
tests/helpers/scrub-model.ts:modelLiteralScrub(values)(text), the regex alternation on the platform's RegExp;modelEscapeSequences, the regex RunSecrets used;MIN_BINARY_FORM6.tests/helpers/scrub-arbitraries.ts:literalInputs(): values over few characters, regex syntax, a lone surrogate half, marker pieces, and prefixes and suffixes of one string.escapeTexts(): raw and JSON-escaped sequence pieces, whole and cut, and percent pieces. Command starts and ends are drawn more often.scrubTransientCommsTextequalsmodelLiteralScrub, through V8's regex and through the fallback. The fallback is forced by registering a 32,768-character value that no generated text holds.escapeSequencesequalsmodelEscapeSequences.RunSecrets.scrubon three escape shapes at 8 KiB and 64 KiB: ratio under 16 with a 20 ms floor.YWJjZA,Pz8/Pw,NzQzOTI, and a 5-byte value at offsets 0 to 2;aMTIzNAz;["abcd","SECRET]x"];Checked
Red first:
TRANSIENT_NARRATION_SECRET_LIMIT.[REDACTED_SECRET] [REDACTED_SECRET]x.Properties at 10,000 cases on seeds 271828 and 99991: 0 failures. This covers all scrub properties, both literal paths,
readingsOfandescapeSequences.escapeSequencesalso passed 50,000 cases on seed 5.The equivalence of the linear search with the regex was first shown against the regex still in production. The review then ran 329,500 differential comparisons with no difference.
Mutations:
{"values":["aaaaaa","aaaa","aaaaa"],"text":"aaaaa"}.{"values":["|.|."],"text":"|.|.|.|."}.escapeSequencesproperty ("\u001b]\u001b\\\u001b\\") once command pieces are drawn more often. It passed 400 cases before that.>from the two-byte escapes fails it too.Timings, Node 24.12.0, fastest of several, on this machine:
RunSecrets.scrub, 64 KiB of unterminated commandsRunSecrets.scrub's 4.3 ms on 63 KiB of colored terminal text with 4 values.pnpm release:checkon 601b1f5 (merged with origin/main 3adf086): exit 0. 7687 root tests passed (12 skipped) and 158 TUI tests. Lint at 439, its cap. Public API proof and public-surface scan passed.run, routes, actors, study, comms, evidence, analysis and verify tests passed after the RunSecrets change: 5165.
Out of scope
["abcd","T]]x"]withT]]x [REDACTED_YWJjZA]%252578. The output showsT]]xonly after two more percent decodings, beyond the documented one-extra-decoding check. The marker manufactures the occurrence. The same class as Codex round 2 finding 3 on Deepen text readings and test the scrub against an independent model #1674.Not verified
RunSecrets.spans, which the terminal route uses to replace values across chunks, has no final check. The transcript built from the chunks goes throughscrubafterwards.SECRETmakes[REDACTED_SECRET]read[REDACTED_[REDACTED_[REDACTED_SECRET]]].🤖 Generated with Claude Code