Skip to content

Audit h5 - #9

Merged
ojassug merged 3 commits into
mainfrom
audit-h5
Aug 10, 2026
Merged

Audit h5#9
ojassug merged 3 commits into
mainfrom
audit-h5

Conversation

@ojassug

@ojassug ojassug commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

ojassug and others added 3 commits August 9, 2026 22:51
… actually does

Closes audit C3 and H1, and the partial L2. Two findings, one decision, because
they are the same question: what is Gateway mode for?

C3 — `exec` returned 401 to its own child. `runExecCommand` generated a per-run
token and injected it as `TOKENDAMPER_GATEWAY_TOKEN`, a variable nothing in
`src/` read and that no third-party client has heard of. The child is `aider`,
`claude`, `codex` or `curl`; it sends `authorization` or `x-api-key` and nothing
else. Reproduced by spawning a real child through `runExecCommand`: every request
came back 401, and `exec` exited 0. The suite passed throughout, because its
gateway test presented the header no real client sends — which is why the new
test spawns a child instead.

Loopback peers are now trusted. The server binds to 127.0.0.1, so a loopback peer
was already the only peer that could connect, and the token was protecting one
local process from another on the same machine — a real but narrow boundary that
was being paid for with a mode nobody could use. Determined from
`req.socket.remoteAddress`, never from a header, since `X-Forwarded-For` is
attacker-supplied; includes `::1` and the IPv4-mapped `::ffff:127.0.0.1` form
Node reports on a dual-stack listener. The token is still enforced on any
non-loopback bind, where the boundary is not narrow.

`HTTP_PROXY` and `HTTPS_PROXY` are no longer set. `GatewayServer` implements
neither HTTP proxy semantics (absolute-form request URIs) nor the `connect` event
`CONNECT` requires, so any child honouring them would have failed to reach the
provider at all — a second failure, independent of the 401 and masked by it.
Setting a proxy variable for a server that is not a proxy is worse than setting
nothing. Base-URL interception is now the only supported mechanism.

Partial L2: `?token=` query authentication is removed — a credential in a query
string lands in access logs, shell history and any error that echoes the URL —
and the header comparison is constant-time.

H1 — the Gateway saves nothing across turns, and that is correct. Measured over
real sockets on realistic two-turn conversations, where a resent history contains
each block exactly once: 0 bytes saved, fallback on every turn, for code, prose
and JSON tool results alike. (An earlier attempt at this measurement duplicated
the block inside turn 2 and appeared to save; that is the within-payload case,
not the cross-turn one.)

Not a bug. `cleanup:session-dedup` marks an elision recoverable only when an
intact copy survives elsewhere in the same payload (§16); a sole copy seen only
in a previous turn is scored in full and refused, correctly, because the consumer
is a stateless provider API with no rehydration mechanism and such a marker is
deletion rather than reference. No cross-turn transform is available without
provider-side resolvability, which does not exist.

So the mode is documented as experimental and the saving claim is withdrawn. What
it does deliver — transparent interception, the full validation pipeline,
byte-faithful forwarding, metrics, within-payload dedup — is a coherent product;
"Cross-turn Session Deduplication" was not. README, ARCHITECTURE.md and CLAUDE.md
invariant 8 updated.

The measurement is pinned by `test/integration/gateway-dedup-reality.test.ts`
rather than left as prose. If a cross-turn saving ever appears, that is the signal
to read: either resolvability was implemented, in which case update the test
deliberately, or the drift gate was relaxed and the Gateway is deleting content
the model cannot recover, in which case do not.

Also resolves the rest of the audit's M4 list rather than deferring it: knapsack
planning marked implemented-but-unreachable (H5), token hashing qualified as
irreversible on the CLI by design, `TOKENDAMPER_RISK_TOLERANCE` marked as having
no effect on optimization (H4, still open), `TOKENDAMPER_GATEWAY_TOKEN` described
accurately.

One existing test asserted the old 401 and was updated with its finding preserved:
it passed only because it sent a header real clients do not.

New tests verified to fail 3/6 against the unfixed gateway.

See DECISIONS §41.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes audit H6, the largest single cause of real code not being optimized —
`CONSTRAINT_DIRECTIVE_LOST` accounted for 24 of 40 fallbacks on the audit's
corpus.

The nine-keyword scan (`must`, `must not`, `never`, `always`, `only if`,
`do not`, `required`, `except when`, `make sure to`, `critical`) is written for
natural-language system prompts, and was applied to raw content of every kind. In
source, `required` and `critical` are ordinary identifiers.

Measured over a frozen 293-file corpus at `targetReductionRatio: 0.3`,
classifying every directive a run reported as dropped by where it came from:

  Python       16 from comments/docstrings, 38 from code — nearly all
               `logger.critical(...)`
  TypeScript   38 from comments/docstrings, 13 from code — `readonly required?`,
               error-message literals

That rules out both extremes. Trusting the check everywhere keeps 51 false
positives. The audit's proposed remedy — scope extraction by content type, not
`code` — discards 54 genuine constraints, and specifically the Python docstring
case that `docs/phase-1d-semantic-gate-disposition.md` measured to be the only
thing this check actually catches. What separates the two populations is not the
content type but the region: an instruction to a reader lives in a comment or a
docstring, never in an expression.

So extraction is scoped to prose regions — line comments, block comment bodies
and Python docstrings including their interior lines, plus whole content for
prose content types. Deliberately line-oriented and syntax-approximate rather
than lexed: this is a filter on what may *raise* a constraint, so over-inclusion
costs a false positive (the pre-existing behaviour) and under-inclusion costs a
missed constraint. Requiring the comment leader at the start of a trimmed line is
what excludes `logger.critical(exc)` while keeping `# never call this twice`.

Retention is now checked per item. The check collected every item's directives
into one list and tested each against the joined content of every item, so a
directive from item A was satisfied if the string happened to appear anywhere in
item B — the check could pass for content that was in fact destroyed — and a loss
anywhere failed the whole run with no way to say where. Matching by item id fixes
both and the message names the item. An item absent from `after` is skipped, on
the same reasoning `DriftTracker.findUnwitnessedItems` records: selection is not
elision, and failing here would make any prunable item carrying an imperative
unprunable.

Measured:

  python     file    14.98% -> 23.14%   (+8.16pp)
  python     stdin   14.88% -> 22.66%   (+7.78pp)
  typescript file    23.38% -> 27.33%   (+3.95pp)

20 rows changed of 586, and none regressed — no file went from reducing to
falling back. Every other bucket is byte-identical.

TypeScript now has zero remaining code-sourced directives; every surviving
`CONSTRAINT_DIRECTIVE_LOST` is a genuine imperative in a comment or docstring
that an elision would drop. Those files still fall back, and should. That is why
the category does not go to zero.

Ordering note: this had to land after §37 (C1a). The audit observed that this
check was "currently the only thing preventing markdown documents from being
deleted" — a document survived if its author happened to use one of nine words.
Narrowing it first would have widened that data loss. With §37 in place the drift
measurement gate covers markdown on its own merits, which the corpus confirms:
the prose bucket is unchanged at 28/28 fallbacks, now attributed to drift rather
than to a coincidence of vocabulary.

New tests verified to fail 9/11 against the unfixed code; the 2 that pass are the
ones pinning behaviour H6 must preserve.

See DECISIONS §42.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes the ingestion half of audit H5. `optimize` now accepts multiple paths and
directories.

The architectural centerpiece could not run. `createContextBundle` produces
exactly one item, `applyCacheAwarePrefixLocking` pins everything inside the first
1,024 tokens, and `solve01Knapsack` places pinned items outside the candidate set
and always selects them — so item 0 was always pinned, `itemsPruned` was always
0, and the knapsack solver, cache-aware prefix locking, topology scoring, the
dependency graph and the git inspector could not affect any output the product
was able to produce.

Measured on `src/core` at `maxInputTokens: 4000`: 31 items, 15 pruned, 20,540
tokens saved by the planner.

`test/unit/fallback-render.test.ts` pinned the success path's `items.join('\n')`
as a latent defect and said whoever made an `emittedOutput` consumer multi-item
"should stop and read it". This is that change, so it is fixed rather than
inherited. One item renders as its content and nothing else — CLI, MCP and bench
stay byte-identical. More than one renders with a `==> path <==` header, which is
`head`/`tail`'s convention rather than a format invented here. It is not
collision-proof and nothing escapes it: the consumer is a model being given
context, legibility beats round-trip parsing, and anything needing to
machine-parse should read `finalBundle` from the trace. Fail-open is per file —
each file's original bytes, never a re-encoding — so §35 holds per item; the
stream as a whole is not byte-identical, because the headers are TokenDamper's.

Three defects this exposed, fixed here:

1. Pruning was scored as drift. `findUnwitnessedItems` had always exempted an
   item absent from `after` — selection is not elision — but the ratios compared
   whole bundles, so a pruned item's symbols vanished and `R_AST` read the planner
   doing its job as semantic loss. The ratios now score retained items only,
   guarded on ids actually corresponding: `id` is content-derived at construction
   and preserved by the transforms, so a caller rebuilding its `after` bundle
   independently would otherwise leave nothing to compare and report `S_k = 0` for
   a gutted bundle. With no correspondence the whole bundle is compared, as
   before — failing open to more measurement rather than less.

2. Whole-item elision of a symbol-bearing item is no longer attempted. Since §40,
   `S_k = 1 - R_AST` for code, so destroying every symbol scores 1.0 against a
   gate firing above 0.40; no threshold or flag lets it through. On a one-item
   bundle that was invisible — the run fell back and emitted the input, which is
   what skipping produces anyway. On a multi-item bundle two pure-`types.ts` files
   were taking a 16-file batch down with them. Symbol-free items are unaffected.

3. `TD_PRESERVE:` matched its own implementation. `drift-tracker.ts` and
   `cli/html-reporter.ts` each acquired a content marker they do not semantically
   have; because `R_struct` is a bundle-scoped set, one phantom marker being
   elided drove it to 0 and took a 16-file batch to `S_k = 0.4053` on a run whose
   real symbol retention was 99.1%. Scoped to prose regions for `code` only — not
   the prose types generally, because `TD_PRESERVE:` is an unambiguous token, and
   the only way it appears without being a directive is as a literal inside an
   expression. This also retires the `html-reporter.ts` regression §40 recorded as
   left in deliberately.

Also: envelope headers were counted on the output side only, so a multi-item
fallback reported 72,973 -> 73,667 tokens — a negative reduction, the same shape
as the phantom -1.39% already diagnosed once in the Python bench harness.

Corpus, 586 rows: 1 row changed, 0 regressions. TypeScript 27.33% -> 29.55%.
Fallback counts drop sharply (prose 28 -> 9, TS stdin 57 -> 0) because doomed
elisions are no longer transformed-then-reverted; same bytes, less wasted work.

Not done, and now the binding constraint: multi-file runs still fall back on real
corpora. On the 45-file Python corpus drift is 0.0359 and AST is clean, but 26
constraint failures across 14 items revert all 45 — validation is bundle-scoped
and fallback is all-or-nothing (audit §3.1, Phase 1c, unstarted). This delivers
the mechanism; §3.1 stands between it and the outcome. Phase 1c's missing
prerequisite — attribution — now exists for constraint (§42), unwitnessed (§37)
and AST (`itemId`) failures.

Four pinned tests asserted behaviour this changes and were updated with their
findings preserved.

See DECISIONS §43.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ojassug
ojassug merged commit dd540fe into main Aug 10, 2026
3 checks passed
@ojassug
ojassug deleted the audit-h5 branch August 10, 2026 11:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant