fix: headroom inner-sandbox routing was silently a no-op#97
Merged
Conversation
Dogfooding issue #84 (measure headroom's real token savings on a live run) surfaced that the routing PR #90 added never actually worked: a full sandbox run completed real commits while headroom's own /stats endpoint stayed at api_requests: 0 the entire time. Root cause, confirmed by reading @ai-hero/sandcastle 0.10.0's own source: an AgentProvider's `.env` (what claudeCode(model, { env }) sets) is never applied on the createSandbox() + sandbox.run() path this runner uses. createSandbox() hardcodes `agentProviderEnv: {}` when starting the container (index.js's createSandbox, "not the agent yet — only the sandbox provider is known here"), and invokeAgent()'s sandbox.exec() call for each iteration never passes an env override either (chunk with invokeAgent's execEffect: `sandbox.exec(printCmd.command, { onLine, cwd, stdin })` — no env key). So ANTHROPIC_BASE_URL was set on an object nobody ever reads. What IS threaded through for real: docker()'s own `env` option becomes literal `-e KEY=VALUE` flags at `docker run` time (startContainer in sandcastle's docker.ts), which the container keeps for its whole lifetime — inherited automatically by every subsequent `docker exec`, no per-call env needed. Fix: move the ANTHROPIC_BASE_URL override from claudeCode()'s env to a new `sandboxEnv` field on AgentInput, applied to the docker() sandbox provider in SandboxRunner.runIssue(). Live-verified end-to-end with a throwaway createSandbox() (no agent invocation, no API cost): the container's real env now carries ANTHROPIC_BASE_URL correctly.
3 tasks
lsfera
added a commit
that referenced
this pull request
Jul 3, 2026
…scription (#98) Both PR #92's and PR #96's review runs failed with "Credit balance is too low" (the exit-causing error; an unrelated "workspace has not been trusted" permissions warning prints alongside it and had been masking the real cause). Root cause, confirmed live: reviewer-adapter.ts runs via noSandbox() + sandcastle's top-level run(), which inherits the devcontainer's own process env — ANTHROPIC_API_KEY (ADR-0018 cockpit passthrough; also needed by headroom's own compress() calls, ADR-0023) sits alongside CLAUDE_CODE_OAUTH_TOKEN (resolved separately from .sandcastle/.env by sandcastle for agent auth). Claude Code prefers an explicit API key over the subscription token whenever both are present, so every review silently authenticated against the API key — and failed outright once that key's balance ran out. Verified precisely: `ANTHROPIC_API_KEY="" CLAUDE_CODE_OAUTH_TOKEN=<real> claude --print ...` succeeds where the unmodified env fails. Confirmed this specific fix mechanism (claudeCode(model, { env }) on sandcastle's top-level run()) actually threads through — unlike SandboxRunner's createSandbox() + .run() path (see PR #97), where AgentProvider.env is silently discarded. Fix: force ANTHROPIC_API_KEY to an empty string (not omitted — Claude Code only falls back to OAuth when the var is unset/empty) on all 4 claudeCode() calls in reviewer-adapter.ts, via a shared FORCE_OAUTH_ENV constant.
lsfera
added a commit
that referenced
this pull request
Jul 3, 2026
…g comment The proxy-injection tests still asserted against input.agent.env, which PR #97 (merged after this branch was cut) replaced with input.sandboxEnv — the only field sandcastle's docker() provider actually forwards into the running container. Also fixes the test comment on the Promise-shape assertion, which incorrectly claimed headroom-ai isn't installed in the test environment (it is, per package.json) as the reason the assertion holds.
lsfera
added a commit
that referenced
this pull request
Jul 3, 2026
…s on a live run (#96) * feat: add context-compressor unit tests and wire into test suite (#84) - Refactor context-compressor.ts to read HEADROOM_MODE/HEADROOM_MODEL at call time (not module load) so tests can mutate env vars after import - Add token savings logging (char counts + %) to the compression callback - Add context-compressor.test.ts: 10 tests covering getHeadroomMode, isCompressionActive, and getCompressionCallback structural shape - Add sandbox-runner.test.ts: 3 tests confirming ANTHROPIC_BASE_URL proxy injection fires on conservative/aggressive and is skipped on local tier - Wire context-compressor.test.ts into the npm test script (219 tests, all pass) * fix: rebase PR #96 tests onto sandboxEnv mechanism, correct misleading comment The proxy-injection tests still asserted against input.agent.env, which PR #97 (merged after this branch was cut) replaced with input.sandboxEnv — the only field sandcastle's docker() provider actually forwards into the running container. Also fixes the test comment on the Promise-shape assertion, which incorrectly claimed headroom-ai isn't installed in the test environment (it is, per package.json) as the reason the assertion holds.
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
Dogfooding issue #84 (measuring headroom's real token savings on a live run) surfaced that the inner-sandbox routing PR #90 added never actually worked: a full
/afksandbox run completed real commits whilecurl localhost:8787/statsstayed atapi_requests: 0for the entire run.Root cause (confirmed by reading
@ai-hero/sandcastle0.10.0's own source, not guessed): anAgentProvider's.env— whatclaudeCode(model, { env })sets — is never applied on thecreateSandbox()+sandbox.run()pathSandboxRunneruses:createSandbox()hardcodesagentProviderEnv: {}when starting the container (the agent isn't even known yet at that point — only the sandbox provider is).invokeAgent()'s per-iterationsandbox.exec(printCmd.command, { onLine, cwd, stdin })call never passes an env override either.So
ANTHROPIC_BASE_URLwas set on an object nobody ever reads — completely silent, no error, no warning.What does work:
docker()'s ownenvoption becomes literal-e KEY=VALUEflags atdocker runtime (in sandcastle'sstartContainer), which the container keeps for its entire lifetime and every subsequentdocker execinherits automatically.Fix: moved the
ANTHROPIC_BASE_URLoverride fromclaudeCode()'senvto a newsandboxEnvfield onAgentInput, applied to thedocker()sandbox provider inSandboxRunner.runIssue().Test plan
npm test— 206/206 passingtsc --noEmit— cleancreateSandbox()call (no agent invocation, no API cost): confirmed viadocker exec <container> envthatANTHROPIC_BASE_URLnow actually lands inside the container, where it previously didn'tANTHROPIC_BASE_URLin the container env at all