feat(proxy): wire the CCR upstream proxy (C9, env-gated no-op by default) - #666
Merged
Conversation
…fault)
The port ported upstream_proxy.py (init_upstream_proxy + get_upstream_proxy_env,
gated on CLAUDE_CODE_REMOTE) but left it UNWIRED — zero live callers, and
subprocess_env carried a TODO ("when it lands, merge get_upstream_proxy_env").
This wires it, mirroring TS's registerUpstreamProxyEnvFn indirection.
- src/utils/subprocess_env.py: register_upstream_proxy_env_fn(fn) + a module
provider slot. subprocess_env merges the provider's recipe AFTER the scrub
(so an injected HTTPS_PROXY / *_CA_BUNDLE isn't stripped by the
anti-exfiltration pass), best-effort (a proxy-env failure can't break child
spawning). The indirection keeps this module from statically importing the
upstreamproxy package (asyncio/ssl/relay).
- src/entrypoints/agent_server_cli.py: _maybe_init_upstream_proxy() at _serve
start — when CLAUDE_CODE_REMOTE is truthy, register the provider + await
init_upstream_proxy(); FAIL-OPEN (a remote session degrades to direct rather
than the server dying). No-op otherwise.
DEFAULT BEHAVIOUR UNCHANGED: with CLAUDE_CODE_REMOTE unset (every local build,
and the open TS build strips this path entirely), the provider is never
registered and subprocess_env is byte-for-byte the base. Low blast radius —
the value is that a CCR-remote deployment's ported proxy actually engages
instead of being dead code.
Scope: the WS mTLS/CA-context and relay.stop()-cleanup pieces from the plan live
inside the dormant relay itself (no open-build reference, untestable without CCR
infra) — deferred; this lands the reachable, testable wiring.
tests/test_c9_upstream_proxy_wiring.py (6): default no-op (env unchanged /
empty-provider no-op), proxy-var merge, survives-the-scrub, fail-open, and the
entrypoint guard is a no-op without the env. 89 subprocess/proxy tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pre-addressing the c9-critic's highest-risk probe: _maybe_init_upstream_proxy
registered the env provider BEFORE await init_upstream_proxy(), so a failed init
(fail-open) could leave the provider registered. Reordered to init-first,
register-only-on-success — a failed init now leaves ZERO registration, so
subprocess_env stays a pure no-op. (Defense-in-depth: get_upstream_proxy_env
already returns {} when the proxy state is DISABLED with a clean env, verified.)
Tests: a failing init leaves _upstream_proxy_env_fn is None (no half-registered
provider); the disabled state returns an empty recipe. 8 C9 tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…S parity) The c9-critic corrected two real errors (and disproved my #4 premise: I'd grepped typescript/ INSIDE the worktree where it doesn't exist — the open build HAS this wiring at init.ts:148-149, 30 files reference CLAUDE_CODE_REMOTE. So C9 is genuine restored parity, not overreach — ship it). MAJOR-A [subprocess_env.py] — scrub/merge order was INVERTED vs TS. TS (subprocessEnv.ts:91-97) merges the proxy recipe THEN runs the scrub loop, so the SCRUB is authoritative/last — no provider can re-introduce a scrubbed secret. The port scrubbed first then merged, so a provider returning a scrub-set key would SURVIVE into the child (a defense-in-depth hole in the exact anti-exfiltration control), and the comment falsely claimed "TS order". Fixed: merge proxy first, THEN scrub — exact TS order; proxy keys aren't in the scrub set so they still survive. New test proves a provider returning ANTHROPIC_API_KEY is scrubbed while HTTPS_PROXY survives. MAJOR-B [agent_server_cli.py] — my earlier #3 "fix" (init-first) diverged from TS register-first (init.ts:148-149) on a FALSE premise: the critic proved a provider over a disabled/failed init returns {} on a clean env (safe in EITHER order). Reverted to register-first for exact TS parity; dropped the false "injects bad vars" rationale. Test updated to assert the real safe property (a failed init injects nothing because the disabled state returns {}). Also (critic notes): wired _serve_stdio too (TS inits in shared init.ts for ALL sessions, not just HTTP — a CCR-remote stdio container would otherwise miss the proxy); fixed the upstream_proxy docstring (9-var→8-var, dropped the nonexistent lowercase `proxy` claim). 9 C9 tests green (default no-op, proxy-merge, scrub-authoritative, fail-open, disabled-returns-empty, entrypoint-guard). subprocess/proxy suite green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ericleepi314
added a commit
that referenced
this pull request
Jul 7, 2026
…ult) (#666) * feat(proxy): wire the CCR upstream proxy (C9 — env-gated, no-op by default) The port ported upstream_proxy.py (init_upstream_proxy + get_upstream_proxy_env, gated on CLAUDE_CODE_REMOTE) but left it UNWIRED — zero live callers, and subprocess_env carried a TODO ("when it lands, merge get_upstream_proxy_env"). This wires it, mirroring TS's registerUpstreamProxyEnvFn indirection. - src/utils/subprocess_env.py: register_upstream_proxy_env_fn(fn) + a module provider slot. subprocess_env merges the provider's recipe AFTER the scrub (so an injected HTTPS_PROXY / *_CA_BUNDLE isn't stripped by the anti-exfiltration pass), best-effort (a proxy-env failure can't break child spawning). The indirection keeps this module from statically importing the upstreamproxy package (asyncio/ssl/relay). - src/entrypoints/agent_server_cli.py: _maybe_init_upstream_proxy() at _serve start — when CLAUDE_CODE_REMOTE is truthy, register the provider + await init_upstream_proxy(); FAIL-OPEN (a remote session degrades to direct rather than the server dying). No-op otherwise. DEFAULT BEHAVIOUR UNCHANGED: with CLAUDE_CODE_REMOTE unset (every local build, and the open TS build strips this path entirely), the provider is never registered and subprocess_env is byte-for-byte the base. Low blast radius — the value is that a CCR-remote deployment's ported proxy actually engages instead of being dead code. Scope: the WS mTLS/CA-context and relay.stop()-cleanup pieces from the plan live inside the dormant relay itself (no open-build reference, untestable without CCR infra) — deferred; this lands the reachable, testable wiring. tests/test_c9_upstream_proxy_wiring.py (6): default no-op (env unchanged / empty-provider no-op), proxy-var merge, survives-the-scrub, fail-open, and the entrypoint guard is a no-op without the env. 89 subprocess/proxy tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(proxy): C9 #3 — register the proxy provider only AFTER init succeeds Pre-addressing the c9-critic's highest-risk probe: _maybe_init_upstream_proxy registered the env provider BEFORE await init_upstream_proxy(), so a failed init (fail-open) could leave the provider registered. Reordered to init-first, register-only-on-success — a failed init now leaves ZERO registration, so subprocess_env stays a pure no-op. (Defense-in-depth: get_upstream_proxy_env already returns {} when the proxy state is DISABLED with a clean env, verified.) Tests: a failing init leaves _upstream_proxy_env_fn is None (no half-registered provider); the disabled state returns an empty recipe. 8 C9 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(proxy): C9 critic — scrub-authoritative order + register-first (TS parity) The c9-critic corrected two real errors (and disproved my #4 premise: I'd grepped typescript/ INSIDE the worktree where it doesn't exist — the open build HAS this wiring at init.ts:148-149, 30 files reference CLAUDE_CODE_REMOTE. So C9 is genuine restored parity, not overreach — ship it). MAJOR-A [subprocess_env.py] — scrub/merge order was INVERTED vs TS. TS (subprocessEnv.ts:91-97) merges the proxy recipe THEN runs the scrub loop, so the SCRUB is authoritative/last — no provider can re-introduce a scrubbed secret. The port scrubbed first then merged, so a provider returning a scrub-set key would SURVIVE into the child (a defense-in-depth hole in the exact anti-exfiltration control), and the comment falsely claimed "TS order". Fixed: merge proxy first, THEN scrub — exact TS order; proxy keys aren't in the scrub set so they still survive. New test proves a provider returning ANTHROPIC_API_KEY is scrubbed while HTTPS_PROXY survives. MAJOR-B [agent_server_cli.py] — my earlier #3 "fix" (init-first) diverged from TS register-first (init.ts:148-149) on a FALSE premise: the critic proved a provider over a disabled/failed init returns {} on a clean env (safe in EITHER order). Reverted to register-first for exact TS parity; dropped the false "injects bad vars" rationale. Test updated to assert the real safe property (a failed init injects nothing because the disabled state returns {}). Also (critic notes): wired _serve_stdio too (TS inits in shared init.ts for ALL sessions, not just HTTP — a CCR-remote stdio container would otherwise miss the proxy); fixed the upstream_proxy docstring (9-var→8-var, dropped the nonexistent lowercase `proxy` claim). 9 C9 tests green (default no-op, proxy-merge, scrub-authoritative, fail-open, disabled-returns-empty, entrypoint-guard). subprocess/proxy suite green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ericleepi314
added a commit
that referenced
this pull request
Jul 21, 2026
…ult) (#666) * feat(proxy): wire the CCR upstream proxy (C9 — env-gated, no-op by default) The port ported upstream_proxy.py (init_upstream_proxy + get_upstream_proxy_env, gated on CLAUDE_CODE_REMOTE) but left it UNWIRED — zero live callers, and subprocess_env carried a TODO ("when it lands, merge get_upstream_proxy_env"). This wires it, mirroring TS's registerUpstreamProxyEnvFn indirection. - src/utils/subprocess_env.py: register_upstream_proxy_env_fn(fn) + a module provider slot. subprocess_env merges the provider's recipe AFTER the scrub (so an injected HTTPS_PROXY / *_CA_BUNDLE isn't stripped by the anti-exfiltration pass), best-effort (a proxy-env failure can't break child spawning). The indirection keeps this module from statically importing the upstreamproxy package (asyncio/ssl/relay). - src/entrypoints/agent_server_cli.py: _maybe_init_upstream_proxy() at _serve start — when CLAUDE_CODE_REMOTE is truthy, register the provider + await init_upstream_proxy(); FAIL-OPEN (a remote session degrades to direct rather than the server dying). No-op otherwise. DEFAULT BEHAVIOUR UNCHANGED: with CLAUDE_CODE_REMOTE unset (every local build, and the open TS build strips this path entirely), the provider is never registered and subprocess_env is byte-for-byte the base. Low blast radius — the value is that a CCR-remote deployment's ported proxy actually engages instead of being dead code. Scope: the WS mTLS/CA-context and relay.stop()-cleanup pieces from the plan live inside the dormant relay itself (no open-build reference, untestable without CCR infra) — deferred; this lands the reachable, testable wiring. tests/test_c9_upstream_proxy_wiring.py (6): default no-op (env unchanged / empty-provider no-op), proxy-var merge, survives-the-scrub, fail-open, and the entrypoint guard is a no-op without the env. 89 subprocess/proxy tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(proxy): C9 #3 — register the proxy provider only AFTER init succeeds Pre-addressing the c9-critic's highest-risk probe: _maybe_init_upstream_proxy registered the env provider BEFORE await init_upstream_proxy(), so a failed init (fail-open) could leave the provider registered. Reordered to init-first, register-only-on-success — a failed init now leaves ZERO registration, so subprocess_env stays a pure no-op. (Defense-in-depth: get_upstream_proxy_env already returns {} when the proxy state is DISABLED with a clean env, verified.) Tests: a failing init leaves _upstream_proxy_env_fn is None (no half-registered provider); the disabled state returns an empty recipe. 8 C9 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(proxy): C9 critic — scrub-authoritative order + register-first (TS parity) The c9-critic corrected two real errors (and disproved my #4 premise: I'd grepped typescript/ INSIDE the worktree where it doesn't exist — the open build HAS this wiring at init.ts:148-149, 30 files reference CLAUDE_CODE_REMOTE. So C9 is genuine restored parity, not overreach — ship it). MAJOR-A [subprocess_env.py] — scrub/merge order was INVERTED vs TS. TS (subprocessEnv.ts:91-97) merges the proxy recipe THEN runs the scrub loop, so the SCRUB is authoritative/last — no provider can re-introduce a scrubbed secret. The port scrubbed first then merged, so a provider returning a scrub-set key would SURVIVE into the child (a defense-in-depth hole in the exact anti-exfiltration control), and the comment falsely claimed "TS order". Fixed: merge proxy first, THEN scrub — exact TS order; proxy keys aren't in the scrub set so they still survive. New test proves a provider returning ANTHROPIC_API_KEY is scrubbed while HTTPS_PROXY survives. MAJOR-B [agent_server_cli.py] — my earlier #3 "fix" (init-first) diverged from TS register-first (init.ts:148-149) on a FALSE premise: the critic proved a provider over a disabled/failed init returns {} on a clean env (safe in EITHER order). Reverted to register-first for exact TS parity; dropped the false "injects bad vars" rationale. Test updated to assert the real safe property (a failed init injects nothing because the disabled state returns {}). Also (critic notes): wired _serve_stdio too (TS inits in shared init.ts for ALL sessions, not just HTTP — a CCR-remote stdio container would otherwise miss the proxy); fixed the upstream_proxy docstring (9-var→8-var, dropped the nonexistent lowercase `proxy` claim). 9 C9 tests green (default no-op, proxy-merge, scrub-authoritative, fail-open, disabled-returns-empty, entrypoint-guard). subprocess/proxy suite green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.
Summary
C9 — wire the CCR upstream proxy (env-gated, no-op by default). The port ported
src/upstreamproxy/upstream_proxy.py(init_upstream_proxy+get_upstream_proxy_env, gated onCLAUDE_CODE_REMOTE) but left it UNWIRED — zero live callers, andsubprocess_envcarried a documented TODO. This restores the genuine parity the open TS build has (init.ts:140-156+subprocessEnv.ts:79-99;CLAUDE_CODE_REMOTEappears in 30 TS files).src/utils/subprocess_env.py:register_upstream_proxy_env_fn(fn)+ a provider slot (mirrors TSregisterUpstreamProxyEnvFn— no static import of the asyncio/ssl/relay graph).subprocess_envmerges the proxy recipe FIRST, then runs the scrub loop LAST so the anti-exfiltration scrub stays authoritative — exact TSsubprocessEnv.ts:85-98order (a provider can't re-introduce a scrubbed secret).src/entrypoints/agent_server_cli.py:_maybe_init_upstream_proxy()— whenCLAUDE_CODE_REMOTEis truthy,registerUpstreamProxyEnvFnthenawait init_upstream_proxy()(exactinit.ts:148-149order), fail-open. Called from BOTH_serve(HTTP) and_serve_stdio— matching TS's sharedinit.tsfor all sessions.Default behaviour unchanged: with
CLAUDE_CODE_REMOTEunset (every local build), no provider is registered andsubprocess_envis byte-for-byte the base; the entrypoint short-circuits before any import. Zero blast radius — the value is that a CCR-remote deployment's ported proxy actually engages.Critic (2 rounds → APPROVE)
Corrected two real errors of mine: my "open build strips this" premise (a worktree-grep artifact —
typescript/is main-only; the open build HAS the wiring) and my pre-emptive init-first reorder (a parity regression on a false premise — register-first is already safe since a disabled proxy returns{}). Both MAJORs fixed to exact TS parity and verified line-by-line: "Both MAJORs correctly and faithfully resolved… Default no-op airtight. Ship it."Verification
tests/test_c9_upstream_proxy_wiring.py(9): default no-op (env byte-for-byte unchanged / empty-provider no-op), proxy-var merge, scrub-authoritative (a provider returningANTHROPIC_API_KEYis stripped whileHTTPS_PROXYsurvives), fail-open, failed-init-injects-nothing, disabled-returns-empty, entrypoint-guard-noop. 49 subprocess/scrub/upstream_proxy tests green; prior-commit full suite at the 6-failure baseline.🤖 Generated with Claude Code