feat(web): planning reads the plan's GitHub repository in an eve sandbox, and the Mac command path is deleted
What
Planning reads the plan's GitHub repository in a Vercel Sandbox owned by the planning conversation's eve session, and the Mac command path (the plan_command table, its routes, and the host package's sandbox-exec runner) is deleted.
Base: coding-agents. The base's merge of main (#1867) passed ./scripts/check.sh unchanged before this work started, so nothing of it needed fixing.
The sandbox
apps/web/eve/sandbox.tsdeclares aVercelSandbox.environmentwith apreparethat holds the one thing the checkout needs of eve's base image: git.eve buildprewarms its snapshot on Vercel.apps/web/eve/subagents/worker/sandbox.tsisdefineParentSandbox(), so the worker's reads see the same checkout.defaultTools: falsestays: the planning model gets no new general tools. The network is eve's default open internet.- Neither file runs an Effect, so
effect-edges.jsonand AGENTS.md's runtime-edge list are unchanged.
run_in_repository
apps/web/server/hosted/repository-shell.ts is rewritten. A call reaches the session's sandbox through eve's ctx.getSandbox() on the tool context the host already receives, so no signature changed on BrainHost.runTool beyond its required services gaining GitHubApp.
- First call: a probe finds no checkout of the plan's repository, so the shell confirms the developer still reaches it through the Luke GitHub App, mints a token for that one repository with
contents: read(GitHubApp.repositoryReadToken, the B1 follow-up:POST /app/installations/{id}/access_tokens), sets the token as GitHub'sBasicheader at the sandbox firewall ongithub.comunder the repository's own.git/endpoint alone, clones the default branch one commit deep, and sets the policy back toallow-allin a finalizer. Later calls reuse the checkout. A plan whose repository changed is checked out again. - Commands run from the checkout root under a 60-second bound and a clean environment (
env -iwith a search path, a home, and git's fixed values, as the Mac runner once gave one), the command text handed to bash as an argument and never spliced into shell text. The result is exit code, stdout, and stderr, each cut to 20,000 characters. - The token never enters the sandbox filesystem or environment, a command, an output, a result, or a stored row; the tests read every run, every clone argument, the result, and the conversation's record back for it, plain and base64.
- Refusals are all
not-runwith a reason the model can act on, each answered ahead of what it would need: no repository yet (before any sandbox opens), sign-in required, not reachable through the App, GitHub unreadable, sandbox unavailable or without a firewall, and checkout failed with git's words. The planning and worker instructions tell the model to say nothing was read. - The tool's description now says it runs one bash command in a checkout of the plan's GitHub repository, meant for reading.
Deleted
plan_command: migration0064_drop_plan_command.sql, the Drizzle module, and the schema test's table list.drizzle-schema.test.tscompares both ways, so a surviving module would fail it.- The claim and settle endpoints in
plans-app.ts, their route files, their segment rewrites, and the regeneratedapi-rewrites.jsonandvercel.json. - The long-poll constants, the wire schemas and goldens for commands, the plan client's claim and settle, and their service paths.
packages/host/src/planning-commands.tsand its wiring incompose-planning.ts. The folder store, the folder chip, the planning acts' folder entries, andshow_codeare left to B4.
Tests
tests/repository-shell.test.tsover a sandbox double at eve's boundary (tests/support/repository-sandbox.ts), a scripted GitHub, and PGlite: first-call checkout then reuse, the null-repository refusal before any sandbox, revoked and unreachable, GitHub down, checkout failure with retry on the next call, a changed repository, a sandbox that cannot open or has no firewall, output truncation, and the token never reaching a run, an output, or a result.tests/hosted-planning.test.tsdrives the scripted model's read through the host and the relay: the listing lands in the record and the token does not, and a plan with no repository gets the refusal in the model's words.tests/github-app.test.tsholds the mint: one repository, contents read, under the App's JWT, none for a repository the user does not reach, and a refused mint as unavailable by status.- The offline eval (
test:agent) gains a repository read on a plan with no repository, refused ahead of any sandbox, so it opens none.
Verified
./scripts/check.shexits 0.test:storeandtest:agentran green against a local Postgres 16 (migrations through 0064).tests/eve-layout.test.ts(eve build --skip-sandbox-prewarm) passes with the sandbox file.
Vercel project settings
The eve service's build now prewarms a Vercel Sandbox snapshot, and a prewarm that fails stops the deployment. This PR's Preview deployment built the eve service with that prewarm under the project's current settings, so nothing needs changing for it; the credentials it uses are the build's own VERCEL_OIDC_TOKEN, or VERCEL_TOKEN with VERCEL_TEAM_ID and VERCEL_PROJECT_ID.
Known limitation
The reach check runs before the checkout only. A developer whose access to the repository is revoked mid-conversation keeps reading the checkout already in the session's sandbox until that sandbox goes: it is a copy they were authorised for minutes earlier, planning sessions are short, and re-checking reach on every command would add two GitHub reads to each. Settled with the orchestrator as the simpler behavior. The show_code path still reads the Mac folder until B4 moves it onto this checkout.
Not covered here
No real Vercel Sandbox or live GitHub checkout has been run from this VM; the shell is held against the double and a scripted GitHub, as docs/PLANNING.md now records. The Preview deployment of this PR did build the eve service with the sandbox prewarm under the current project settings.
š¤ Generated with Claude Code