Repository navigation
feat(pr-verify): add the key proxy for the driven capture session - #13662
Conversation
A stdlib Node HTTP server on a unix socket that holds the API key (read from stdin) and is the only path from the driven Claude Code to the Messages API. An ordered check pipeline (path, size, parse, model, max_tokens, server tools, cap reservation) refuses with statuses Claude Code does not retry; allowed requests go upstream with the credential replaced, the body and anthropic-* headers unchanged, and the response relayed unbuffered. One budget tracks committed and in-flight reserved tokens; usage follows the cumulative message_delta counts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An upstream socket that closed after the response headers left the client open until Claude Code's stall watchdog fired, with the reservation still held. The proxy now drops the client and commits the reservation, as for a client disconnect. Also tests the socketMode option and drops wording from the server-tool refusal that implied a retry path a 403 does not take. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A content block whose source.type is "url" makes the API fetch a URL the client chose, an egress path out of the no-network sandbox. The proxy now refuses such a block anywhere in messages or system, at any depth and tool_result content included, with the same 403 as server tools. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Forward the re-serialized checked object (fatal UTF-8 decode), so a duplicate key cannot smuggle a value past the checks. - Reserve the upstream count_tokens preflight plus max_tokens; add maxInFlight and a per-job client count_tokens limit, refuse count_tokens once the cap is spent, and commit the reservation when a stream ends without message_delta. - Allowlist request headers (unlisted betas refused), top-level body keys, content block and source types, the beta=true query and relayed response headers. - Destroy the request on an oversized body. - Cite a pointer for each upstream behavior the code relies on, and test with hand-computed values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A preflight 429, 5xx or network error now reaches the client with its status, retry headers and body, so Claude Code's own retry covers an upstream blip; the in-flight slot is freed and nothing is reserved. Any other preflight 4xx stays a non-retry refusal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y proxy - An upstream failure before any response byte (no answer, a dropped connection, a stall) is answered 502 with x-should-retry true, on the main request and the count_tokens preflight alike; policy refusals keep x-should-retry false. - New required config key upstreamTimeoutMs: an upstream socket idle that long is aborted. Before any byte is relayed the reservation is released; after, the client is dropped and the reservation committed, as for a client disconnect. The in-flight slot is freed either way. - Every upstream request now sends accept-encoding: identity. Before, none was sent, which by RFC 9110 12.5.3 accepts any coding, so a compressed body could hide usage. The header-allowlist test now asserts identity upstream instead of an absent header, a stronger check. - A tools field that is not a list, or a tools entry that is not an object, is refused. - A beta refusal names the refused betas (up to ten) in the response body and the log line. - Comments giving upstream behavior as a reason carry a pointer, as-of date and recheck trigger, or are stated as our decision. Drop the unused MESSAGES constant. - Tests for each, plus a 429 and a dropped-connection preflight. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sources Give the test file's count_tokens, streaming and spend-limit comments a pointer, as-of date and recheck trigger, and bump the two proxy pointers re-read on 2026-11-06. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the body is sent A non-streaming response sends nothing until generation ends, and Claude Code resends a retryable failure, so a timeout or upstream error before response headers could leave billed generations uncounted. Once the body has been fully written upstream, such a failure now commits the full reservation; it is released only when the connection failed before the body was written. Both still get the retryable 502. The two tests that expected a release after the upstream had read the body now expect the commit (150 and 650, hand-computed from the counted input plus max_tokens); a new test shows a connect failure releases it. Document how to size upstreamTimeoutMs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review Summary: ❌ FailedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
claude-security-review reviewed Security review (incremental since b1fcd70): no security issues foundcritical-findings: 0 Scope. This review covers only the push since What changed. The request What I checked (no exploitable path found):
The new test sends a deeply nested No hold: nothing here can cause a real charge, and there is no CRITICAL finding. |
|
claude-review reviewed Code review (incremental, blocking only):
|
Answering at once closed the socket while the client was still uploading, so it saw a reset, which Claude Code retries, instead of the non-retried 413. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… of exiting A body that passed the checks but overflowed the stack on re-serialization rejected the async end handler, and the unhandled rejection ended the proxy for the whole capture job. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Post-merge mutation testingKilled 5, survived 2, timeout 0, no coverage 0, invalid 0, not run 0.
Surviving mutants
The run's summary has the full report. |
Refs: #12232
Summary
PR C Phase 2 of #12232. The pr-verify UI capture will run the driven Claude Code inside a sibling sandbox holding only a dummy key. This adds the process that holds the real key outside that sandbox: a stdlib Node HTTP proxy on a unix socket that swaps the dummy credential for the key and lets through only the traffic a capture session sends, within a per-job token cap. Nothing runs it yet; Phase 5 wires it into
pr-verify-capture.yml.Fix
.github/scripts/pr-verify/key-proxy.mjs(new) withkey-proxy.test.mjs:/v1/messagesand/v1/messages/count_tokens, with?beta=trueonly when the client sent exactly that; everything else is refused.anthropic-betavalues (a refusal names the betas to add), top-level body keys, content block types and source types (url sources and server tools are refused, since they make the API reach a PR-chosen host). Bodies are re-serialized from the checked object, so duplicate keys cannot smuggle a value past the check.count_tokensand reserves its input tokens plusmax_tokens;maxInFlightand a per-jobcount_tokensrequest limit bound concurrency; usage is summed from the stream without double counting; past the cap the answer is a 429 withx-should-retry: false. A request whose body was fully sent upstream commits its reservation even if it then fails or stalls before any response (it may still be billed, and Claude Code retries it); it is released only when the failure came before the body was sent.upstreamTimeoutMs(required) aborts a stalled upstream with a retryable answer and settles its slot; upstream always getsaccept-encoding: identity; only a fixed set of response headers is relayed; an oversized body gets 413 and the connection is closed.No changelog fragment: no plugin changed.
Verification
node --test .github/scripts/pr-verify/key-proxy.test.mjs: 35 tests, 35 pass; expected values are hand-computed literals.scripts/check-affected.sh: 9 passed on the pushed head.Related
docs/conventions/pr-pipeline.yaml.Follow-ups
🤖 Generated with Claude Code