Skip to content

Audit C3/H1: exec reaches its own gateway, and the mode says what it actually does - #5

Merged
ojassug merged 1 commit into
audit-c1bfrom
audit-gateway
Aug 10, 2026
Merged

Audit C3/H1: exec reaches its own gateway, and the mode says what it actually does#5
ojassug merged 1 commit into
audit-c1bfrom
audit-gateway

Conversation

@ojassug

@ojassug ojassug commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #4 (which is stacked on #3). Base is audit-c1b, so this diff shows only the Gateway commit. Merge #3#4 → this; each will retarget automatically.

Closes audit C3, H1, the remainder of M4, and partially L2.

Two findings, one PR, because they are the same question: what is Gateway mode for? C3 is a straightforward bug. H1 is a design conclusion with a consequence, and treating it as a bug would have been the wrong move.

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/ reads 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:

CHILD: base=http://127.0.0.1:62865
CHILD: HTTP_PROXY=http://127.0.0.1:62865
CHILD: status=401
CHILD: body={"error":"Unauthorized: Invalid or missing gateway token"}
exec exit code: 0

The flagship integration was non-functional end to end — and exec reported success.

The existing suite passed throughout, because its gateway test presented the header that no real client sends. That is why the new test spawns an actual child process; the audit's own closing note on C3 said exactly this would have caught it.

Fix: trust loopback, keep the token for other binds

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

Second, independent defect: the proxy vars

HTTP_PROXY and HTTPS_PROXY are no longer set. GatewayServer implements neither HTTP proxy semantics (absolute-form request URIs) nor the connect event that CONNECT tunnelling requires — verified, there is no handler. Any child honouring HTTPS_PROXY (most HTTP clients) would have failed to reach the provider at all, independently 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, and is documented as such.

Partial L2

?token= query authentication 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 now constant-time.

After: child gets 200, HTTP_PROXY unset.

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:

content type sent forwarded saved fallback
code 2,919 B 2,919 B 0 true
prose 1,688 B 1,688 B 0 true
JSON tool result 3,142 B 3,142 B 0 true

A note on getting this measurement right. My first attempt appeared to save bytes. It duplicated the block inside turn 2, which is the within-payload case — the one that already works — not the cross-turn case the feature claims. The shape matters more than the numbers here.

This is not a bug. cleanup:session-dedup marks an elision recoverable: true only when an intact copy survives elsewhere in the same payload (DECISIONS §16). A sole copy seen only in a previous turn is scored in full and refused — correctly, because Phase A established that the consumer is a stateless provider API with no rehydration mechanism, so such a marker is deletion, not reference.

No cross-turn transform is available without provider-side resolvability, which does not exist.

Decision: document it as experimental, withdraw the claim

The mode does deliver transparent interception, the full validation pipeline, byte-faithful forwarding (§38, in #3), metrics, and within-payload deduplication. That is a coherent product. "Cross-turn Session Deduplication" was not. README, ARCHITECTURE.md and CLAUDE.md invariant 8 updated.

The measurement is pinned as a test rather than left as prose (test/integration/gateway-dedup-reality.test.ts). If a cross-turn saving ever appears, that is the signal to read: either resolvability was implemented — 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.

M4, finished rather than deferred

With the Gateway question settled, the remaining overstated README claims are resolved:

  • 0/1 Knapsack Planning → marked implemented, not yet reachable (H5: every shipping entry point builds a one-item bundle)
  • Reversible Token Hashing → qualified as irreversible on the CLI by design, since no TokenHasher is wired in
  • TOKENDAMPER_RISK_TOLERANCE → marked as having no effect on optimization (H4, still open)
  • TOKENDAMPER_GATEWAY_TOKEN → described accurately

Verification

  • npm run typecheck, npm run lint — clean
  • npx vitest run507 passing, 58 files (up from 499)
  • New tests verified to fail 3 of 6 against the unfixed gateway, including the spawn test with the exact 401 → 200 signature
  • 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

Still open

C4 (structured message content flattened to a string) remains open and is still masked by the fallback — worth noting that if H1's gate were ever relaxed, C4 becomes live 400s. M7 (savings measured against a newline-joined render rather than wire bytes) and M8 (two environment branches in the request path) also remain.

🤖 Generated with Claude Code

… 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>
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