feat(agents): launch, stop and relaunch the runner in the sandbox
Merged
CH
5d ago+2265+2265+0123456789012345678901234567890123456789-26-26-01234567890123456789
P08 of the rollout in "Bringing Codex or Claude Code Max plans into Luke" (https://tryluke.dev/stage/plan/89f19bfc-88b5-471a-886a-84b00180aa09). Adds the claude_code implementation of AgentSessions. Nothing routes to it yet (that's P11), so nothing reaches users.
What it does
packages/service/src/agents/coder-host/claude-code-sessions.ts, claudeCodeSessions(options):
open- Opens a persistent Vercel Sandbox from the coding agent's image and tags it
sessionId:<Luke-minted id>. The Terminal tab, the stop and the cleanup sweep find it the same way they find eve's. P11 stores this id inruntime_session_id. - Checks the repository out exactly as eve's sandbox does today (
checkOutAgentRepository, same token at the firewall). An agent with no repository gets the empty working folder. - Mints the runner's bearer and records its SHA-256 with
recordCodingAgentRunnerToken. - Uploads P05's bundle as the default user.
- Starts a detached command with
sudo: truethat:- installs the bundle root-owned under
/opt/luke/agent-runnerand checks it against Luke's digest (in argv; not a secret); - marks the checkout
safe.directoryand shares it withagent's group; execssetpriv --reuid=agent … --inh-caps=-all --ambient-caps=-all --bounding-set=-allinto the runner.
- installs the bundle root-owned under
- The plan token (from the key store's
claude_setup_tokenrow) and the bearer are passed only in that command'senv. In test mode, an Anthropic API key (testApiKey) replaces the plan token. Neither appears in argv, the config line, a file, or a tag. - Writes the config line to the runner's stdin:
harness,executable= the image'sclaude, the model, effort,append,firstSequence, the events and MCP URLs,allowedDomains. The first message line follows it. - If anything after the create fails, the sandbox is deleted again.
- Opens a persistent Vercel Sandbox from the coding agent's image and tags it
send- Writes a message line to the running runner.
- If the sandbox stopped or the runner exited, it resumes the sandbox, sets the repository credential at the firewall again, and relaunches with
resume=native_sessionand the nextfirstSequence. Each launch gets a fresh bearer.
cancelwritesstop.resetretires the bearer (recordCodingAgentRunnerToken(…, null)) and deletes the sandbox.streamis empty. The runner posts its events itself.runnerEnded({ target, sessionId, sequence })is for P09's route on END:- It stops the sandbox, taking the firewall credential off first, unless a Terminal lease holds it.
- An END from a launch that a newer one replaced does nothing. It's recognized by its sequence being lower than the launch's first, which is kept in a sandbox tag.
- Under P05's exit rule a waiting runner posts no END, so its sandbox stays up.
The runner's command id, the sandbox session it runs in, and the launch's first sequence are kept as sandbox tags. That's how a later call (a separate function invocation) finds the runner to write to. This adds five tags, which is Vercel's maximum.
Supporting changes
- Runner (
@luke/agent-runner)- A message line may carry
uuid(Luke's line id). The runner uses it as the SDK prompt's uuid and postsluke_runner_accepted { uuid }just before handing the prompt to the SDK. P09 matches the awaiting row on it; the shape was agreed with P09. - A test-mode
ANTHROPIC_API_KEYis scrubbed from events like the plan token. EveMessagegains an optionallineId, which eve never sees.
- A message line may carry
vercel-sandbox-api.ts: the calls from@vercel/sandbox3.8.0 that the 3.3.0 copy lacks. These arecreate(from a snapshot, on v3),updateTags,writeFiles(gzipped tar), detachedstartwithattachStdin,writeStdin(offset-safe, 512 KiB chunks),commandExitandkill, plusenvonrun.- Image seam (
AgentSandboxImage), filled today fromLUKE_AGENT_SANDBOX_SNAPSHOTthrough the environment reader. When it's unset,openanswers failed and opens nothing.- eve exposes its prepared snapshot id to no code of Luke's, and an eve app serves no custom route, so this is option (d), agreed with the orchestrator.
- ⚠️ A hand-set snapshot id goes stale every time
PREPARE_SCRIPTchanges the image. It only carries us to Gate B. A follow-up must replace it, most likely with Luke's own template keyed by theLUKE_SANDBOX_IMAGEdigest, before P11's workflow runs on Preview. There's a TODO at the seam.
- Runner bundle
- The web build now runs
pnpm --filter @luke/agent-runner buildfirst. bundle-functions.tsfails ifagent-runner.mjsis missing. It copies the bundle into every.funcwhose bundle reachesrunner-bundle.ts, whichrunnerBundleFromDirectoryreads beside the entry. No function reaches it yet; P11's will.
- The web build now runs
- Left to the composer, P11: the events URL, MCP URL, instructions and
nextSequenceare options. P09'sreadNativeSequence+ 1 fillsnextSequence. The checkout's promise-shapedRepositorySandboxruns through an edge-handedrun, asWebStoreRundoes.
Choices made here
- Command sandbox allowlist: github.com, *.github.com, *.githubusercontent.com, npm and PyPI.
- Unsupported attachments: an attached file the protocol can't carry (a PDF) is passed as a text note.
- Model: the Anthropic id comes from
providerModelOf. - Concurrency: no lock against two concurrent
sends relaunching at once. The conversation's existing admission is the guard, as for eve.
Checks
- Hermetic tests (
claude-code-sessions.test.ts) run against a stateful fake of the Sandbox REST API over PGlite. They cover:- launch env and config, with no secret in argv, config, tags or the upload;
- the plan token opened from a sealed
claude_setup_tokenrow; - no snapshot or no token → failed, and the sandbox deleted;
- send and cancel;
- relaunch with
resumeand the rightfirstSequenceafter a sandbox stop, with a fresh bearer; - relaunch after the runner exited;
- a late END from a replaced launch stops nothing;
- a Terminal lease keeps the sandbox up;
- reset deletes the sandbox and retires the bearer.
- Runner tests cover ACCEPTED, a malformed uuid, and API-key scrubbing.
- Typecheck (agent-runner, service, web), knip and biome pass locally. The full
pnpm check:affectedran out of memory in my sandbox, so CI gates the rest. - Preview: the developer waived per-PR Preview runs for this rollout. I did not run it live in a Luke-image sandbox, because this sandbox has no Vercel credentials. So "a Terminal can't read the token" is not checked live here. It rests on P00/P03's finding (agent-owned process, caps dropped for Terminals) and is still owed to the Gate B live run.
Made with Luke