diff --git a/docs/troubleshooting.mdx b/docs/troubleshooting.mdx new file mode 100644 index 0000000..838cc28 --- /dev/null +++ b/docs/troubleshooting.mdx @@ -0,0 +1,76 @@ +--- +title: "Troubleshooting" +description: "Common issues and solutions when running sandbox-agent" +--- + +## "Agent Process Exited" immediately after sending a message + +This typically means the agent (Claude, Codex) crashed on startup. Common causes: + +### 1. Network restrictions + +The sandbox cannot reach the AI provider's API (`api.anthropic.com` or `api.openai.com`). Test connectivity with: + +```bash +curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 https://api.anthropic.com/v1/messages +``` + +A `000` or timeout means the network is blocked. See [Daytona Network Restrictions](#daytona-network-restrictions) below. + +### 2. Missing API key + +Ensure `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` is set in the sandbox environment, not just locally. + +### 3. Agent binary not found + +Verify the agent is installed: + +```bash +ls -la ~/.local/share/sandbox-agent/bin/ +``` + +### 4. Binary libc mismatch (musl vs glibc) + +Claude Code binaries are available in both musl and glibc variants. If you see errors like: + +``` +cannot execute: required file not found +Error loading shared library libstdc++.so.6: No such file or directory +``` + +This means the wrong binary variant was downloaded. + +**For sandbox-agent 0.2.0+**: Platform detection is automatic. The correct binary (musl or glibc) is downloaded based on the runtime environment. + +**For sandbox-agent 0.1.x**: Use Alpine Linux which has native musl support: + +```dockerfile +FROM alpine:latest +RUN apk add --no-cache curl ca-certificates libstdc++ libgcc bash +``` + +## Daytona Network Restrictions + +Daytona sandboxes have tier-based network access: + +| Tier | Network Access | +|------|----------------| +| Tier 1 & 2 | Restricted. **Cannot be overridden.** AI provider APIs blocked by default. | +| Tier 3 & 4 | Full internet access. Custom allowlists supported. | + +If you're on Tier 1/2 and agents fail immediately, you have two options: + +1. **Upgrade to Tier 3+** for full network access +2. **Contact Daytona support** to whitelist `api.anthropic.com` and `api.openai.com` for your organization + +The `networkAllowList` parameter only works on Tier 3+: + +```typescript +await daytona.create({ + snapshot: "my-snapshot", + envVars: { ANTHROPIC_API_KEY: "..." }, + networkAllowList: "api.anthropic.com,api.openai.com", // Tier 3+ only +}); +``` + +See [Daytona Network Limits documentation](https://www.daytona.io/docs/en/network-limits/) for details. diff --git a/examples/daytona/src/daytona.ts b/examples/daytona/src/daytona.ts index 1f78dbe..a045baf 100644 --- a/examples/daytona/src/daytona.ts +++ b/examples/daytona/src/daytona.ts @@ -35,8 +35,13 @@ if (!process.env.DAYTONA_API_KEY || (!anthropicKey && !openaiKey)) { ); } -const SNAPSHOT = "sandbox-agent-ready"; -const AGENT_BIN_DIR = "/root/.local/share/sandbox-agent/bin"; +console.log( + "\x1b[33m[NOTE]\x1b[0m Daytona Tier 3+ required to access api.anthropic.com and api.openai.com.\n" + + " Tier 1/2 sandboxes have restricted network access that will cause 'Agent Process Exited' errors.\n" + + " See: https://www.daytona.io/docs/en/network-limits/\n", +); + +const SNAPSHOT = "sandbox-agent-ready-v2"; const daytona = new Daytona(); @@ -52,18 +57,11 @@ if (!hasSnapshot) { image: Image.base("ubuntu:22.04").runCommands( // Install dependencies "apt-get update && apt-get install -y curl ca-certificates", - // Install sandbox-agent - "curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh", - // Create agent bin directory - `mkdir -p ${AGENT_BIN_DIR}`, - // Install Claude: get latest version, download binary - `CLAUDE_VERSION=$(curl -fsSL https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/latest) && ` + - `curl -fsSL -o ${AGENT_BIN_DIR}/claude "https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/$CLAUDE_VERSION/linux-x64/claude" && ` + - `chmod +x ${AGENT_BIN_DIR}/claude`, - // Install Codex: download tarball, extract binary - `curl -fsSL -L https://github.com/openai/codex/releases/latest/download/codex-x86_64-unknown-linux-musl.tar.gz | tar -xzf - -C /tmp && ` + - `find /tmp -name 'codex-x86_64-unknown-linux-musl' -exec mv {} ${AGENT_BIN_DIR}/codex \\; && ` + - `chmod +x ${AGENT_BIN_DIR}/codex`, + // Install sandbox-agent (0.1.0-rc.1 has install-agent command) + "curl -fsSL https://releases.rivet.dev/sandbox-agent/0.1.0-rc.1/install.sh | sh", + // Install agents + "sandbox-agent install-agent claude", + "sandbox-agent install-agent codex", ), }, { onLogs: (log) => console.log(` ${log}`) }, @@ -76,6 +74,10 @@ const envVars: Record = {}; if (anthropicKey) envVars.ANTHROPIC_API_KEY = anthropicKey; if (openaiKey) envVars.OPENAI_API_KEY = openaiKey; +// NOTE: Tier 1/2 sandboxes have restricted network access that cannot be overridden +// If you're on Tier 1/2 and see "Agent Process Exited", contact Daytona to whitelist +// api.anthropic.com and api.openai.com for your organization +// See: https://www.daytona.io/docs/en/network-limits/ const sandbox = await daytona.create({ snapshot: SNAPSHOT, envVars, @@ -96,10 +98,25 @@ const envCheck = await sandbox.process.executeCommand( console.log("Sandbox env:", envCheck.result.output || "(none)"); const binCheck = await sandbox.process.executeCommand( - `ls -la ${AGENT_BIN_DIR}/`, + "ls -la /root/.local/share/sandbox-agent/bin/", ); console.log("Agent binaries:", binCheck.result.output); +// Network connectivity test +console.log("Testing network connectivity..."); +const netTest = await sandbox.process.executeCommand( + "curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 https://api.anthropic.com/v1/messages 2>&1 || echo 'FAILED'", +); +const httpCode = netTest.result.output?.trim(); +if (httpCode === "405" || httpCode === "401") { + console.log("api.anthropic.com: reachable"); +} else if (httpCode === "000" || httpCode === "FAILED" || !httpCode) { + console.log("\x1b[31mapi.anthropic.com: UNREACHABLE - Tier 1/2 network restriction detected\x1b[0m"); + console.log("Claude/Codex will fail. Upgrade to Tier 3+ or contact Daytona support."); +} else { + console.log(`api.anthropic.com: ${httpCode}`); +} + const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url; logInspectorUrl({ baseUrl }); diff --git a/examples/docker/src/docker.ts b/examples/docker/src/docker.ts index b79b007..36a850e 100644 --- a/examples/docker/src/docker.ts +++ b/examples/docker/src/docker.ts @@ -5,7 +5,8 @@ if (!process.env.OPENAI_API_KEY && !process.env.ANTHROPIC_API_KEY) { throw new Error("OPENAI_API_KEY or ANTHROPIC_API_KEY required"); } -const IMAGE = "debian:bookworm-slim"; +// Alpine is required because Claude Code binary is built for musl libc +const IMAGE = "alpine:latest"; const PORT = 3000; const docker = new Docker({ socketPath: "/var/run/docker.sock" }); @@ -26,13 +27,18 @@ try { console.log("Starting container..."); const container = await docker.createContainer({ Image: IMAGE, - Cmd: ["bash", "-lc", [ - "apt-get update && apt-get install -y curl ca-certificates", - "curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh", + Cmd: ["sh", "-c", [ + // Install dependencies (Alpine uses apk, not apt-get) + "apk add --no-cache curl ca-certificates libstdc++ libgcc bash", + "curl -fsSL https://releases.rivet.dev/sandbox-agent/0.1.0-rc.1/install.sh | sh", "sandbox-agent install-agent claude", "sandbox-agent install-agent codex", `sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`, ].join(" && ")], + Env: [ + process.env.ANTHROPIC_API_KEY ? `ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}` : "", + process.env.OPENAI_API_KEY ? `OPENAI_API_KEY=${process.env.OPENAI_API_KEY}` : "", + ].filter(Boolean), ExposedPorts: { [`${PORT}/tcp`]: {} }, HostConfig: { AutoRemove: true, diff --git a/examples/e2b/src/e2b.ts b/examples/e2b/src/e2b.ts index 36fc584..eaed086 100644 --- a/examples/e2b/src/e2b.ts +++ b/examples/e2b/src/e2b.ts @@ -1,5 +1,4 @@ import { Sandbox } from "@e2b/code-interpreter"; -import { SandboxAgent } from "sandbox-agent"; import { logInspectorUrl, runPrompt } from "@sandbox-agent/example-shared"; if (!process.env.E2B_API_KEY || (!process.env.OPENAI_API_KEY && !process.env.ANTHROPIC_API_KEY)) { @@ -11,11 +10,18 @@ if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPI if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY; const sandbox = await Sandbox.create({ allowInternetAccess: true, envs }); - -const run = (cmd: string) => sandbox.commands.run(cmd); +const run = async (cmd: string) => { + const result = await sandbox.commands.run(cmd); + if (result.exitCode !== 0) throw new Error(`Command failed: ${cmd}\n${result.stderr}`); + return result; +}; console.log("Installing sandbox-agent..."); -await run("curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh"); +await run("curl -fsSL https://releases.rivet.dev/sandbox-agent/0.1.0-rc.1/install.sh | sh"); + +console.log("Installing agents..."); +await run("sandbox-agent install-agent claude"); +await run("sandbox-agent install-agent codex"); console.log("Starting server..."); await sandbox.commands.run("sandbox-agent server --no-token --host 0.0.0.0 --port 3000", { background: true }); @@ -25,20 +31,15 @@ logInspectorUrl({ baseUrl }); // Wait for server to be ready console.log("Waiting for server..."); -const client = await SandboxAgent.connect({ baseUrl }); for (let i = 0; i < 30; i++) { try { - await client.getHealth(); - break; + const res = await fetch(`${baseUrl}/v1/health`); + if (res.ok) break; } catch { await new Promise((r) => setTimeout(r, 1000)); } } -console.log("Installing agents..."); -await client.installAgent("claude"); -await client.installAgent("codex"); - const cleanup = async () => { console.log("Cleaning up..."); await sandbox.kill(); diff --git a/examples/vercel/package.json b/examples/vercel/package.json deleted file mode 100644 index 924bc4e..0000000 --- a/examples/vercel/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "@sandbox-agent/example-vercel", - "private": true, - "type": "module", - "scripts": { - "start": "tsx src/vercel-sandbox.ts", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "@vercel/sandbox": "latest", - "@sandbox-agent/example-shared": "workspace:*" - }, - "devDependencies": { - "@types/node": "latest", - "tsx": "latest", - "typescript": "latest" - } -} diff --git a/examples/vercel/src/vercel-sandbox.test.ts b/examples/vercel/src/vercel-sandbox.test.ts deleted file mode 100644 index 2962828..0000000 --- a/examples/vercel/src/vercel-sandbox.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { buildHeaders } from "../shared/sandbox-agent-client.ts"; -import { setupVercelSandboxAgent } from "./vercel-sandbox.ts"; - -const hasOidc = Boolean(process.env.VERCEL_OIDC_TOKEN); -const hasAccess = Boolean( - process.env.VERCEL_TOKEN && - process.env.VERCEL_TEAM_ID && - process.env.VERCEL_PROJECT_ID -); -const shouldRun = hasOidc || hasAccess; -const timeoutMs = Number.parseInt(process.env.SANDBOX_TEST_TIMEOUT_MS || "", 10) || 300_000; - -const testFn = shouldRun ? it : it.skip; - -describe("vercel sandbox example", () => { - testFn( - "starts sandbox-agent and responds to /v1/health", - async () => { - const { baseUrl, token, cleanup } = await setupVercelSandboxAgent(); - try { - const response = await fetch(`${baseUrl}/v1/health`, { - headers: buildHeaders({ token }), - }); - expect(response.ok).toBe(true); - const data = await response.json(); - expect(data.status).toBe("ok"); - } finally { - await cleanup(); - } - }, - timeoutMs - ); -}); diff --git a/examples/vercel/src/vercel-sandbox.ts b/examples/vercel/src/vercel-sandbox.ts deleted file mode 100644 index 696dbd5..0000000 --- a/examples/vercel/src/vercel-sandbox.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Sandbox } from "@vercel/sandbox"; -import { logInspectorUrl, runPrompt, waitForHealth } from "@sandbox-agent/example-shared"; - -if (!process.env.OPENAI_API_KEY && !process.env.ANTHROPIC_API_KEY) { - throw new Error("OPENAI_API_KEY or ANTHROPIC_API_KEY required"); -} - -const PORT = 3000; - -const sandbox = await Sandbox.create({ - runtime: process.env.VERCEL_RUNTIME || "node24", - ports: [PORT], - ...(process.env.VERCEL_TOKEN && process.env.VERCEL_TEAM_ID && process.env.VERCEL_PROJECT_ID - ? { token: process.env.VERCEL_TOKEN, teamId: process.env.VERCEL_TEAM_ID, projectId: process.env.VERCEL_PROJECT_ID } - : {}), -}); - -const run = (cmd: string) => sandbox.runCommand({ cmd: "bash", args: ["-lc", cmd], sudo: true }); - -console.log("Installing sandbox-agent..."); -await run("curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh"); -await run("sandbox-agent install-agent claude"); -await run("sandbox-agent install-agent codex"); - -console.log("Starting server..."); -await sandbox.runCommand({ - cmd: "bash", - args: ["-lc", `sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`], - sudo: true, - detached: true, -}); - -const baseUrl = `https://${sandbox.domain(PORT)}`; -await waitForHealth({ baseUrl }); -logInspectorUrl({ baseUrl }); - -const cleanup = async () => { - console.log("Cleaning up..."); - await sandbox.stop(); - process.exit(0); -}; -process.once("SIGINT", cleanup); -process.once("SIGTERM", cleanup); - -await runPrompt({ baseUrl }); -await cleanup(); diff --git a/examples/vercel/tsconfig.json b/examples/vercel/tsconfig.json deleted file mode 100644 index 96ba2fd..0000000 --- a/examples/vercel/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "lib": ["ES2022", "DOM"], - "module": "ESNext", - "moduleResolution": "Bundler", - "allowImportingTsExtensions": true, - "noEmit": true, - "esModuleInterop": true, - "strict": true, - "skipLibCheck": true, - "resolveJsonModule": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "**/*.test.ts"] -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2daa10a..86856ff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -179,7 +179,7 @@ importers: dependencies: '@anthropic-ai/claude-code': specifier: latest - version: 2.1.20 + version: 2.1.22 '@openai/codex': specifier: latest version: 0.92.0 @@ -258,6 +258,9 @@ importers: '@daytonaio/sdk': specifier: latest version: 0.135.0(ws@8.19.0) + '@e2b/code-interpreter': + specifier: latest + version: 2.3.3 devDependencies: '@types/node': specifier: latest @@ -307,8 +310,8 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@anthropic-ai/claude-code@2.1.20': - resolution: {integrity: sha512-5r9OEF5TTmkhOKWtJ9RYqdn/vchwQWABO3dvgZVXftqlBZV/IiKjHVISu0dKtqWzByLBolchwePrhY68ul0QrA==} + '@anthropic-ai/claude-code@2.1.22': + resolution: {integrity: sha512-WMwUUC/Ux87LqBDBC4KI/uYE0L/jcro3XcBzyd4a/YCkGVRyruyhypeeyHqAW7bUxm72xxWaPoy0keBkxpgIpQ==} engines: {node: '>=18.0.0'} hasBin: true @@ -3606,6 +3609,7 @@ packages: tar@7.5.6: resolution: {integrity: sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==} engines: {node: '>=18'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me text-decoder@1.2.3: resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} @@ -4143,7 +4147,7 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@anthropic-ai/claude-code@2.1.20': + '@anthropic-ai/claude-code@2.1.22': optionalDependencies: '@img/sharp-darwin-arm64': 0.33.5 '@img/sharp-darwin-x64': 0.33.5 diff --git a/scripts/sandbox-testing/package.json b/scripts/sandbox-testing/package.json index 022f016..9c458d3 100644 --- a/scripts/sandbox-testing/package.json +++ b/scripts/sandbox-testing/package.json @@ -10,7 +10,8 @@ "test:verbose": "tsx test-sandbox.ts docker --verbose" }, "dependencies": { - "@daytonaio/sdk": "latest" + "@daytonaio/sdk": "latest", + "@e2b/code-interpreter": "latest" }, "devDependencies": { "@types/node": "latest", diff --git a/scripts/sandbox-testing/test-sandbox.ts b/scripts/sandbox-testing/test-sandbox.ts index cf2c686..27ab6bf 100644 --- a/scripts/sandbox-testing/test-sandbox.ts +++ b/scripts/sandbox-testing/test-sandbox.ts @@ -77,8 +77,10 @@ async function buildSandboxAgent(): Promise { return "RELEASE"; } + // Binary is in workspace root target dir, not server target dir + const binaryPath = join(ROOT_DIR, "target/release/sandbox-agent"); + if (skipBuild) { - const binaryPath = join(SERVER_DIR, "target/release/sandbox-agent"); if (!existsSync(binaryPath)) { throw new Error(`Binary not found at ${binaryPath}. Run without --skip-build.`); } @@ -89,10 +91,9 @@ async function buildSandboxAgent(): Promise { log.info("Running cargo build --release..."); try { execSync("cargo build --release -p sandbox-agent", { - cwd: SERVER_DIR, + cwd: ROOT_DIR, stdio: verbose ? "inherit" : "pipe", }); - const binaryPath = join(SERVER_DIR, "target/release/sandbox-agent"); log.success(`Built: ${binaryPath}`); return binaryPath; } catch (err) { @@ -116,6 +117,7 @@ interface Sandbox { } // Docker provider +// Uses Alpine because Claude Code binary is built for musl libc const dockerProvider: SandboxProvider = { name: "docker", requiredEnv: [], @@ -127,12 +129,12 @@ const dockerProvider: SandboxProvider = { log.info(`Creating Docker container: ${id}`); execSync( - `docker run -d --name ${id} ${envArgs} -p 0:3000 ubuntu:22.04 tail -f /dev/null`, + `docker run -d --name ${id} ${envArgs} -p 0:3000 alpine:latest tail -f /dev/null`, { stdio: verbose ? "inherit" : "pipe" }, ); - // Install curl - execSync(`docker exec ${id} bash -c "apt-get update && apt-get install -y curl ca-certificates"`, { + // Install dependencies (Alpine uses apk; musl C++ libs needed for Claude Code) + execSync(`docker exec ${id} sh -c "apk add --no-cache curl ca-certificates libstdc++ libgcc bash"`, { stdio: verbose ? "inherit" : "pipe", }); @@ -140,7 +142,7 @@ const dockerProvider: SandboxProvider = { id, async exec(cmd) { try { - const stdout = execSync(`docker exec ${id} bash -c "${cmd.replace(/"/g, '\\"')}"`, { + const stdout = execSync(`docker exec ${id} sh -c "${cmd.replace(/"/g, '\\"')}"`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], }); @@ -174,6 +176,8 @@ const daytonaProvider: SandboxProvider = { const daytona = new Daytona(); log.info("Creating Daytona sandbox..."); + // NOTE: Tier 1/2 sandboxes have restricted network that cannot be overridden + // networkAllowList requires CIDR notation (IP ranges), not domain names const sandbox = await daytona.create({ image: "ubuntu:22.04", envVars, @@ -210,6 +214,58 @@ const daytonaProvider: SandboxProvider = { }, }; +// E2B provider +const e2bProvider: SandboxProvider = { + name: "e2b", + requiredEnv: ["E2B_API_KEY"], + async create({ envVars }) { + const { Sandbox } = await import("@e2b/code-interpreter"); + + log.info("Creating E2B sandbox..."); + let sandbox; + try { + sandbox = await Sandbox.create({ + allowInternetAccess: true, + envs: envVars, + }); + } catch (err: any) { + log.error(`E2B sandbox creation failed: ${err.message || err}`); + throw err; + } + const id = sandbox.sandboxId; + + // Install curl (E2B uses Debian which has glibc, sandbox-agent will auto-detect) + const installResult = await sandbox.commands.run( + "sudo apt-get update && sudo apt-get install -y curl ca-certificates" + ); + log.debug(`Install output: ${installResult.stdout} ${installResult.stderr}`); + + return { + id, + async exec(cmd) { + const result = await sandbox.commands.run(cmd); + return { + stdout: result.stdout || "", + stderr: result.stderr || "", + exitCode: result.exitCode, + }; + }, + async upload(localPath, remotePath) { + const content = readFileSync(localPath); + await sandbox.files.write(remotePath, content); + await sandbox.commands.run(`chmod +x ${remotePath}`); + }, + async getBaseUrl(port) { + return `https://${sandbox.getHost(port)}`; + }, + async cleanup() { + log.info(`Cleaning up E2B sandbox: ${id}`); + await sandbox.kill(); + }, + }; + }, +}; + // Get provider function getProvider(name: string): SandboxProvider { switch (name) { @@ -217,8 +273,10 @@ function getProvider(name: string): SandboxProvider { return dockerProvider; case "daytona": return daytonaProvider; + case "e2b": + return e2bProvider; default: - throw new Error(`Unknown provider: ${name}. Available: docker, daytona`); + throw new Error(`Unknown provider: ${name}. Available: docker, daytona, e2b`); } } @@ -228,8 +286,9 @@ async function installSandboxAgent(sandbox: Sandbox, binaryPath: string): Promis if (binaryPath === "RELEASE") { log.info("Installing from releases.rivet.dev..."); + // Use 0.1.0-rc.1 which has install-agent command const result = await sandbox.exec( - "curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh", + "curl -fsSL https://releases.rivet.dev/sandbox-agent/0.1.0-rc.1/install.sh | sh", ); log.debug(`Install output: ${result.stdout}`); if (result.exitCode !== 0) { @@ -249,47 +308,18 @@ async function installSandboxAgent(sandbox: Sandbox, binaryPath: string): Promis async function installAgents(sandbox: Sandbox, agents: string[]): Promise { log.section("Installing agents"); - const AGENT_BIN_DIR = "/root/.local/share/sandbox-agent/bin"; - await sandbox.exec(`mkdir -p ${AGENT_BIN_DIR}`); - for (const agent of agents) { log.info(`Installing ${agent}...`); - if (agent === "claude") { - // First get the version - const versionResult = await sandbox.exec( - "curl -fsSL https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/latest", - ); - if (versionResult.exitCode !== 0) throw new Error(`Failed to get Claude version: ${versionResult.stderr}`); - const claudeVersion = versionResult.stdout.trim(); - log.debug(`Claude version: ${claudeVersion}`); - - // Then download the binary - const downloadUrl = `https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/${claudeVersion}/linux-x64/claude`; - log.debug(`Download URL: ${downloadUrl}`); - const result = await sandbox.exec( - `curl -fsSL -o ${AGENT_BIN_DIR}/claude "${downloadUrl}" && chmod +x ${AGENT_BIN_DIR}/claude`, - ); - if (result.exitCode !== 0) throw new Error(`Failed to install claude: ${result.stderr}`); - } else if (agent === "codex") { - const result = await sandbox.exec( - `curl -fsSL -L https://github.com/openai/codex/releases/latest/download/codex-x86_64-unknown-linux-musl.tar.gz | tar -xzf - -C /tmp && ` + - `find /tmp -name 'codex-x86_64-unknown-linux-musl' -exec mv {} ${AGENT_BIN_DIR}/codex \\; && ` + - `chmod +x ${AGENT_BIN_DIR}/codex`, - ); - if (result.exitCode !== 0) throw new Error(`Failed to install codex: ${result.stderr}`); + if (agent === "claude" || agent === "codex") { + const result = await sandbox.exec(`sandbox-agent install-agent ${agent}`); + if (result.exitCode !== 0) throw new Error(`Failed to install ${agent}: ${result.stderr}`); + log.success(`Installed ${agent}`); } else if (agent === "mock") { // Mock agent is built into sandbox-agent, no install needed log.info("Mock agent is built-in, skipping install"); - continue; } - - log.success(`Installed ${agent}`); } - - // List installed agents - const ls = await sandbox.exec(`ls -la ${AGENT_BIN_DIR}/`); - log.debug(`Agent binaries:\n${ls.stdout}`); } // Start server and check health @@ -422,10 +452,13 @@ async function checkEnvironment(sandbox: Sandbox): Promise { const checks = [ { name: "Environment variables", cmd: "env | grep -E 'ANTHROPIC|OPENAI|CLAUDE|CODEX' | sed 's/=.*/=/'" }, - { name: "Agent binaries", cmd: "ls -la /root/.local/share/sandbox-agent/bin/ 2>/dev/null || echo 'No agents installed'" }, + // Check both /root (Alpine) and /home/user (E2B/Debian) paths + { name: "Agent binaries", cmd: "ls -la ~/.local/share/sandbox-agent/bin/ 2>/dev/null || ls -la /root/.local/share/sandbox-agent/bin/ 2>/dev/null || ls -la /home/user/.local/share/sandbox-agent/bin/ 2>/dev/null || echo 'No agents installed'" }, { name: "sandbox-agent version", cmd: "sandbox-agent --version 2>/dev/null || echo 'Not installed'" }, - { name: "Server process", cmd: "pgrep -a sandbox-agent || echo 'Not running'" }, + { name: "Server process", cmd: "pgrep -a sandbox-agent 2>/dev/null || ps aux | grep sandbox-agent | grep -v grep || echo 'Not running'" }, { name: "Server logs (last 20 lines)", cmd: "tail -20 /tmp/sandbox-agent.log 2>/dev/null || echo 'No logs'" }, + { name: "Network: api.anthropic.com", cmd: "curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 https://api.anthropic.com/v1/messages 2>&1 || echo 'UNREACHABLE'" }, + { name: "Network: api.openai.com", cmd: "curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 https://api.openai.com/v1/models 2>&1 || echo 'UNREACHABLE'" }, ]; for (const { name, cmd } of checks) { diff --git a/server/packages/agent-management/src/agents.rs b/server/packages/agent-management/src/agents.rs index c563c27..91aa143 100644 --- a/server/packages/agent-management/src/agents.rs +++ b/server/packages/agent-management/src/agents.rs @@ -84,7 +84,9 @@ impl Platform { pub fn detect() -> Result { let os = std::env::consts::OS; let arch = std::env::consts::ARCH; - let is_musl = cfg!(target_env = "musl"); + // Detect musl at runtime by checking for the musl dynamic linker + // This is more reliable than cfg!(target_env = "musl") which checks compile-time + let is_musl = Self::detect_musl_runtime(); match (os, arch, is_musl) { ("linux", "x86_64", true) => Ok(Self::LinuxX64Musl), @@ -98,6 +100,14 @@ impl Platform { }), } } + + /// Detect if the runtime environment uses musl libc by checking for musl dynamic linker + fn detect_musl_runtime() -> bool { + use std::path::Path; + // Check for musl dynamic linkers (x86_64 and aarch64) + Path::new("/lib/ld-musl-x86_64.so.1").exists() + || Path::new("/lib/ld-musl-aarch64.so.1").exists() + } } #[derive(Debug, Clone)]