Skip to content

fix: headroom inner-sandbox routing was silently a no-op#97

Merged
lsfera merged 1 commit into
mainfrom
fix/headroom-inner-sandbox-env
Jul 3, 2026
Merged

fix: headroom inner-sandbox routing was silently a no-op#97
lsfera merged 1 commit into
mainfrom
fix/headroom-inner-sandbox-env

Conversation

@lsfera

@lsfera lsfera commented Jul 3, 2026

Copy link
Copy Markdown
Owner

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 /afk sandbox run completed real commits while curl localhost:8787/stats stayed at api_requests: 0 for the entire run.

Root cause (confirmed by reading @ai-hero/sandcastle 0.10.0's own source, not guessed): an AgentProvider's .env — what claudeCode(model, { env }) sets — is never applied on the createSandbox() + sandbox.run() path SandboxRunner uses:

  • createSandbox() hardcodes agentProviderEnv: {} when starting the container (the agent isn't even known yet at that point — only the sandbox provider is).
  • invokeAgent()'s per-iteration sandbox.exec(printCmd.command, { onLine, cwd, stdin }) call never passes an env override either.

So ANTHROPIC_BASE_URL was set on an object nobody ever reads — completely silent, no error, no warning.

What does work: docker()'s own env option becomes literal -e KEY=VALUE flags at docker run time (in sandcastle's startContainer), which the container keeps for its entire lifetime and every subsequent docker exec inherits automatically.

Fix: moved 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().

Test plan

  • npm test — 206/206 passing
  • tsc --noEmit — clean
  • Live-verified end-to-end with a throwaway createSandbox() call (no agent invocation, no API cost): confirmed via docker exec <container> env that ANTHROPIC_BASE_URL now actually lands inside the container, where it previously didn't
  • Cross-checked against the original (broken) code: same live-verification approach showed no ANTHROPIC_BASE_URL in the container env at all

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.
@lsfera lsfera merged commit fafc433 into main Jul 3, 2026
3 checks passed
@lsfera lsfera deleted the fix/headroom-inner-sandbox-env branch July 3, 2026 07:54
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.
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