Skip to content

feat: Phase 2 — Vercel Sandbox adapter + MCP server support - #1

Merged
cdotte merged 1 commit into
masterfrom
feat/phase2-sandbox-adapter-mcp
Mar 21, 2026
Merged

feat: Phase 2 — Vercel Sandbox adapter + MCP server support#1
cdotte merged 1 commit into
masterfrom
feat/phase2-sandbox-adapter-mcp

Conversation

@cdotte

@cdotte cdotte commented Mar 21, 2026

Copy link
Copy Markdown

Summary

  • MCP server support in claude-local: Agents can now configure mcpServers in their adapter config. The adapter writes the config to a temp file and passes it via --mcp-config to the Claude CLI. Includes validation in environment tests.
  • New emisso_sandbox adapter: Runs Claude Code inside ephemeral Vercel Sandbox microVMs (Firecracker). Full lifecycle: create sandbox → clone repos → inject instructions + MCP config → run Claude CLI with stream-json → parse results → stop sandbox. Registered in all 3 registries (server, UI, CLI).
  • Engineering skill + setup guide: skills/emisso-engineering/SKILL.md for agent instructions, doc/guides/engineering-agent-setup.md for configuration reference.

Files changed

Modified (11): constants, claude-local execute/index/build-config/test, adapter-utils types, all 3 registries, server package.json, lockfile

Created (12): 6 server adapter files (types, helpers, cost-calculator, execute, test, index), 4 UI adapter files (parse-stdout, config-fields, build-config, index), engineering skill, setup guide

Test plan

  • pnpm build passes all packages
  • pnpm typecheck clean across monorepo
  • claude-local: agent with mcpServers config → verify --mcp-config appears in CLI invocation
  • emisso_sandbox: testEnvironment() validates Vercel auth + API key
  • emisso_sandbox: config form renders in UI with model selector, vCPUs, MCP servers
  • emisso_sandbox: end-to-end sandbox execution with a test repo
  • No regressions in existing adapter types

🤖 Generated with Claude Code

Add first-class MCP server support to claude-local and introduce the
emisso_sandbox adapter for running Claude Code in ephemeral Vercel
Sandbox microVMs.

MCP support (claude-local):
- Write mcpServers config to temp file, pass via --mcp-config flag
- Validate mcpServers structure in environment test
- Document mcpServers in agentConfigurationDoc
- Pass through mcpServersJson in UI build-config

Emisso Sandbox adapter (new):
- Server: create sandbox → clone repos → inject instructions + MCP →
  run Claude CLI → stream logs → parse result → stop sandbox
- UI: config form (model, vCPUs, timeout, snapshot, MCP servers JSON),
  stream-json transcript parser
- CLI: reuses claude-local's stream event formatter
- Registered in all 3 registries (server, UI, CLI)
- Cost calculator for sandbox compute + API token costs
- Environment test validates API key, Vercel auth, SDK availability

Also adds:
- emisso-engineering skill (SKILL.md)
- Engineering agent setup guide

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@cdotte

cdotte commented Mar 21, 2026

Copy link
Copy Markdown
Author

@claude review edge cases

@github-actions

github-actions Bot commented Mar 21, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

@cdotte

cdotte commented Mar 21, 2026

Copy link
Copy Markdown
Author

@claude review edge cases

@github-actions

github-actions Bot commented Mar 21, 2026

Copy link
Copy Markdown

Claude finished @cdotte's task in 2m 18s —— View job


Edge Case Review

  • Gather context
  • Read emisso-sandbox execute.ts (core lifecycle)
  • Read emisso-sandbox helpers.ts, types.ts, cost-calculator.ts
  • Read emisso-sandbox test.ts
  • Read claude-local execute.ts changes (MCP config)
  • Read UI config-fields.tsx + parse-stdout.ts
  • Analyze and post edge case findings

Findings

🔴 Critical

1. Kill-then-rerun race condition (execute.ts:284–313)

The adapter spawns a Claude CLI process with detached: true, immediately kills it, writes a runner script, and re-runs via Node. This pattern is inherently racy:

  • If cmd.kill("SIGTERM") fails silently (SDK throws, or SIGTERM is ignored), both processes run concurrently, double-billing Anthropic API tokens.
  • The first process's logs are streamed nowhere before it's killed — if there's startup latency, the kill may arrive while the process is already producing output.
  • A cleaner approach: skip the detached spawn entirely and only write + run the runner script. Fix this →

2. instructionsFilePath failure silently skipped (execute.ts:243–257)

if (readResult.exitCode === 0) {
  // write file
}
// no else — missing file is silently ignored

An agent with a misconfigured instructionsFilePath will run without any system prompt, no warning logged, no error returned. Should log a warning or return an error. Fix this →


🟠 High

3. MCP textarea state lost on reload (config-fields.tsx:156–175)

Edit mode stores valid JSON in mcpServers and invalid/intermediate JSON in mcpServersJson. On a page reload, the textarea's initial value is eff("adapterConfig", "mcpServersJson", ...) which returns "" — the valid saved mcpServers object is never serialized back. The field appears empty even when MCP servers are configured.

Fix: derive the display value from mcpServers when mcpServersJson is empty:

eff("adapterConfig", "mcpServersJson",
  config.mcpServers ? JSON.stringify(config.mcpServers, null, 2) : ""
)

(This pattern already appears in the config.mcpServers ? fallback on line 162, but eff() will return the stored mcpServersJson raw value preferentially, which is stale after save.) Fix this →

4. additionalRepos with empty repoUrl gives a confusing failure (execute.ts:211–214)

for (const extra of additionalRepos) {
  const url = gitAuth ? embedGitCredentials(extra.repoUrl, gitAuth) : extra.repoUrl;
  repos.push({ url, dirName: extra.dirName || extractRepoDirName(extra.repoUrl) });
}

If extra.repoUrl is "" or missing, extractRepoDirName("") returns "repo", embedGitCredentials("") returns "", and git clone --depth 1 "" workspace/repo fails with a cryptic git error rather than a clear config validation error. Should be validated early alongside the repoUrl check at line 125.

5. cloneDepth: 0 passes through unclamped (execute.ts:94)

const cloneDepth = asNumber(config.cloneDepth, 1);

vcpus and timeoutSec are clamped, but cloneDepth is not. --depth 0 tells git to do a full clone. For a large monorepo this could exhaust sandbox memory or hit the CLONE_TIMEOUT_MS limit with no clear explanation.

6. Timeout detection may miss sandbox-forced termination (execute.ts:347, 391)

const isTimeout = stderr.includes("ETIMEDOUT") || stderr.includes("timed out");
// ...
const isTimeout = message.includes("AbortError") || message.includes("timed out");

When Vercel terminates the sandbox due to its wall-clock timeout option, the resulting error may be an SDK-specific exception (e.g., SandboxTimeoutError, HTTP 408/504, or similar). Neither ETIMEDOUT nor AbortError may match, leading the caller to see timedOut: false even though it was a timeout. Worth testing with the actual SDK error shape.


🟡 Medium

7. fullStdout unbounded memory growth (execute.ts:331–337)

let fullStdout = "";
for await (const log of runnerCmd.logs()) {
  if (log.stream === "stdout") fullStdout += log.data;
  await onLog(log.stream, log.data);
}

fullStdout accumulates the entire Claude session output (stream-json events, tool results, intermediate assistant messages). A 30-turn session with large tool outputs could easily reach tens of MB. Only the result event is needed for parseStreamJsonOutput. Consider accumulating only when a line starts with { and contains "type":"result" or capping to last N bytes.

8. testEnvironment doesn't validate vercelToken as a blocking error (test.ts:53–73)

The environment test warns when Vercel auth is partial/missing but uses level: "warn". However in execute.ts, if vercelToken is missing and Sandbox.create fails, the error is caught and returned as a generic sandbox_execution_failed. There's no pre-flight error for missing Vercel auth in execute. A clear early return before Sandbox.create (like the one for anthropicApiKey) would surface this faster.

9. parse-stdout.ts drops all but first tool_result per user message (parse-stdout.ts:37–58)

if (event.type === "user" && event.message?.content) {
  for (const block of event.message.content) {
    if (block.type === "tool_result") {
      return [{ ... }]; // returns on first match
    }
  }
}

A user message can contain multiple tool_result blocks (when Claude calls multiple tools in one turn). Only the first is returned to the transcript; the rest are silently discarded.


🔵 Low / Informational

10. revision only applies to primary repo (undocumented)

args.push("--branch", revision) only when index === 0. Additional repos always clone the default branch. This seems intentional but is not documented in types.ts or the setup guide.

11. Cost calculator CPU utilization is a fixed guess (cost-calculator.ts:57)

cpuUtilizationFactor = 0.25 is hardcoded as a default. The actual compute billing from Vercel is wall-clock based (not utilization-adjusted) for microVMs. This will consistently underestimate sandbox costs. Consider using wallClockHours * vcpus directly without the utilization factor, or document that this is a rough estimate.

12. testEnvironment doesn't validate timeoutSec range

The test reports the configured timeoutSec but doesn't warn if it's outside [MIN_TIMEOUT_SEC, MAX_TIMEOUT_SEC]. An agent configured with timeoutSec: 5 would show as valid in env test but get clamped to 10s at runtime.


Summary Table

# Severity File Issue
1 🔴 Critical execute.ts:284 Kill-then-rerun race; could double-bill
2 🔴 Critical execute.ts:249 Missing instructions file silently ignored
3 🟠 High config-fields.tsx:159 MCP textarea empty on reload after save
4 🟠 High execute.ts:211 Empty additionalRepos[].repoUrl → cryptic failure
5 🟠 High execute.ts:94 cloneDepth:0 unclamped → full clone
6 🟠 High execute.ts:347,391 Timeout detection misses SDK-specific errors
7 🟡 Medium execute.ts:331 fullStdout unbounded accumulation
8 🟡 Medium test.ts:53 Missing Vercel token is warn, not blocking error
9 🟡 Medium parse-stdout.ts:37 Only first tool_result per user message captured
10 🔵 Low execute.ts:223 revision scope undocumented
11 🔵 Low cost-calculator.ts:57 CPU utilization factor underestimates Vercel costs
12 🔵 Low test.ts:109 timeoutSec range not validated in env test

@cdotte
cdotte merged commit d084047 into master Mar 21, 2026
cdotte added a commit that referenced this pull request Mar 21, 2026
- Log warning when instructionsFilePath not found (was silently ignored)
- Clamp cloneDepth to [1, 10000] (depth 0 caused full clones)
- Skip additionalRepos with empty repoUrl (was causing cryptic git errors)
- Broaden timeout detection to catch SDK-specific errors (TimeoutError, SandboxTimeoutError, 408, 504)
- Cap fullStdout accumulation at 5 MB to prevent unbounded memory growth
- Upgrade Vercel auth check from warn to error (missing auth blocks execution)
- Collect all tool_result blocks per user message, not just the first

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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