feat(web): planning reads the plan's GitHub repository in an eve sandbox, and the Mac command path is deleted

Merged
DA
11d ago
+1348-1071

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.ts declares a VercelSandbox.environment with a prepare that holds the one thing the checkout needs of eve's base image: git. eve build prewarms its snapshot on Vercel.
  • apps/web/eve/subagents/worker/sandbox.ts is defineParentSandbox(), so the worker's reads see the same checkout.
  • defaultTools: false stays: the planning model gets no new general tools. The network is eve's default open internet.
  • Neither file runs an Effect, so effect-edges.json and 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's Basic header at the sandbox firewall on github.com under the repository's own .git/ endpoint alone, clones the default branch one commit deep, and sets the policy back to allow-all in 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 -i with 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-run with 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: migration 0064_drop_plan_command.sql, the Drizzle module, and the schema test's table list. drizzle-schema.test.ts compares 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 regenerated api-rewrites.json and vercel.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.ts and its wiring in compose-planning.ts. The folder store, the folder chip, the planning acts' folder entries, and show_code are left to B4.

Tests

  • tests/repository-shell.test.ts over 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.ts drives 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.ts holds 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.sh exits 0.
  • test:store and test:agent ran 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