fix(sanitization): port robust scanner + two-step fail-closed gate - #20
Merged
Conversation
The gate could report a repo clean while the semantic layer never ran, and the
verdict parser threw away good verdicts.
Scanner (ported from swarm-map, kept config-driven so this repo keeps its own
sanitize.config.json and domain instructions):
* Verdict extraction walks every `{` and lets the JSON decoder consume exactly
one value, taking the first object that is actually a verdict. The previous
first-`{`-to-last-`}` span failed with "Extra data" whenever the model added a
sentence containing a brace, and the last-`{` retry landed inside the prose --
a correct verdict became a parse error.
* Any semantic-layer failure (missing, malformed, revoked or rate-limited key,
unreachable API, unparseable reply) is now "the layer did not run" rather than
an unhandled traceback, and --require-semantic decides whether that is fatal.
* --deterministic-only runs layer 1 alone, so the always-on CI step needs no key.
* The summary line names the layers that actually ran, and `ok <file>` is printed
only for files the semantic layer really reviewed.
* Deterministic layer gains the shapes swarm-map detects and this repo missed:
Stripe, Twilio, SendGrid, Google OAuth, GitHub fine-grained PAT, Slack webhook,
JWT, bearer/basic auth, credentials in connection strings, quoted and
entropy-gated unquoted credential assignments, SSN, Luhn-valid card numbers.
Wholly low-entropy credential bodies (documentation stubs) are not reported;
PII patterns are never placeholder-gated.
* allow_substrings is matched against the detected value instead of the whole
line, so exempting one documentation address cannot quietly exempt a real one
beside it.
* Scanned content is fenced with a per-call random nonce and the model is told to
treat it as untrusted data, so content cannot pose as an instruction.
Workflow: the gate is two steps. Deterministic runs on every PR with no key.
Semantic runs with --require-semantic and FAILS CLOSED. A red step now names its
own cause instead of degrading to a warning and exiting 0. Fork PRs get step 1
plus a loud annotation, since GitHub withholds secrets from them by design.
Adds an on-demand full-tree audit lane (cron intentionally left commented until
a first manual run is triaged) and docs/sanitization-gate.md.
Verified: self-tests pass; the full offline suite passes; the whole tree is clean
under the deterministic layer; step 2 exits non-zero with no key.
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.
The finding
This repo's sanitization gate could report clean while doing half its job, and
its verdict parser threw away good verdicts. Both were reproduced locally against
the pre-change code before anything was edited.
Reproduced (Matilde): with the key unset or set to an empty string, the gate
printed a warning, printed
ok <file>for a file the semantic layer never lookedat, and exited 0 reporting
sanitization: clean. In CI, an empty or revokedANTHROPIC_API_KEYrepo secret therefore produces a green check that meansstrictly less than it appears to.
Reproduced (osint-engine): worse — the repo had no deterministic layer at
all.
AKIA…committed toengine/exited 0 withsanitization: no sensitive files changed — skipping. Any credential outside thefour sensitive prefixes walked straight through.
The parser:
text[first "{" : last "}"]breaks on the most common real replyshape — a correct verdict followed by a sentence containing a brace. The span then
holds two objects,
json.loadsraisesExtra data, and the last-{retry landsinside the prose. A perfectly good verdict became a hard parse error. This is
the failure that hit agentic-startup-generalist once its key was fixed.
What changed
Scanner ported from
swarm-map, kept config-driven so this repo'ssanitize.config.json— itsdomain_noun,flag_examples,do_not_flag_examples, prefixes and allowlist — remains the single place domainknowledge lives. The scanner carries detection machinery only.
{→ last}{,raw_decodeone value, take the first real verdict--require-semanticexits 2ok <file>cleanDeterministic layer gains the shapes swarm-map detects and this repo missed:
Stripe, Twilio, SendGrid, Google OAuth client secret, GitHub fine-grained PAT,
Slack webhook, JWT, bearer/basic auth, credentials embedded in connection
strings, quoted and entropy-gated unquoted credential assignments, SSN, and
Luhn-valid card numbers. Wholly low-entropy credential bodies (documentation
stubs) are not reported; PII patterns are never placeholder-gated.
allow_substringsis now matched against the detected value rather than thewhole line. Line scoping silently exempts neighbours — one documentation address
would clear a real address sitting beside it and the log would show nothing.
Workflow: two steps, on purpose
including forks. Red here means a leak is in the diff.
--require-semantic, fails closed. Red heremeans particulars were found or the layer could not run, and the log says
which.
Collapsing these into one step is what made the gate dishonest: a broken key
degraded to deterministic-only, warned, and exited 0. Fork PRs get step 1 plus a
loud annotation, because GitHub withholds repository secrets from them by design.
Also adds an on-demand full-tree audit lane. Its cron is deliberately left
commented out: the deterministic layer is verified clean tree-wide, but the
semantic layer has never run over the whole tree here, so enabling a nightly run
before anyone triages the first result would just manufacture a standing red.
Instructions to enable are in the file.
Verification
Everything below was actually run, not assumed:
testsCI runs): unchanged and green.attempt. See "what the new layer found", below.
no key exits non-zero with
Failing closed rather than reporting a half-checked diff.The semantic layer runs for real on this PR.
docs/sanitization-gate.mdiscontent-bearing, so step 2 exercises the key end-to-end here rather than passing
vacuously on a diff with nothing to review. If it comes back red, read the
reasons — that is the gate working, and the fix is the content, not the gate.
What the new deterministic layer found
Running it tree-wide surfaced real hits that had never been scanned before. None
were waved through silently:
id, a credential-bearing DSN, a NANP number, a card number, a dotted quad)
tripped its own detectors. Fixed by describing the shapes instead of writing
them out — this module is scanned, so a literal sample is a self-inflicted hit.
A house rule now says so in the file.
and Twilio SID. They are supposed to look real — that is how a detector is
proven. Rather than clicking "allow the secret", each provider prefix is split
across two adjacent string literals: Python joins them at compile time so the
scanner sees the whole value, while the file text holds no credential-shaped
token.
What was deliberately NOT ported
swarm-map suppresses NANP
555numbers and RFC 5737 documentation IP rangesoutright. Those relaxations are not here: this repo's own tests assert that a
203.0.113.xaddress is flagged, and quietly loosening a detector to reducenoise is how a gate stops meaning anything. Verified documentation placeholders
are handled through the config allowlist instead, where a reviewer can see exactly
what is exempted and why.