fix: detect musl/glibc at runtime for correct Claude binary download

Previously used cfg!(target_env = "musl") which checks compile-time,
causing musl-compiled sandbox-agent to always download musl binaries
even on glibc systems like Debian/E2B.

Now checks for /lib/ld-musl-*.so.1 at runtime to detect the actual
system libc and download the correct Claude binary variant.
This commit is contained in:
Nathan Flurry 2026-01-28 04:19:35 -08:00
parent 0bbe92b344
commit cbd36eeca8
12 changed files with 228 additions and 194 deletions

76
docs/troubleshooting.mdx Normal file
View file

@ -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.

View file

@ -35,8 +35,13 @@ if (!process.env.DAYTONA_API_KEY || (!anthropicKey && !openaiKey)) {
); );
} }
const SNAPSHOT = "sandbox-agent-ready"; console.log(
const AGENT_BIN_DIR = "/root/.local/share/sandbox-agent/bin"; "\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(); const daytona = new Daytona();
@ -52,18 +57,11 @@ if (!hasSnapshot) {
image: Image.base("ubuntu:22.04").runCommands( image: Image.base("ubuntu:22.04").runCommands(
// Install dependencies // Install dependencies
"apt-get update && apt-get install -y curl ca-certificates", "apt-get update && apt-get install -y curl ca-certificates",
// Install sandbox-agent // Install sandbox-agent (0.1.0-rc.1 has install-agent command)
"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",
// Create agent bin directory // Install agents
`mkdir -p ${AGENT_BIN_DIR}`, "sandbox-agent install-agent claude",
// Install Claude: get latest version, download binary "sandbox-agent install-agent codex",
`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`,
), ),
}, },
{ onLogs: (log) => console.log(` ${log}`) }, { onLogs: (log) => console.log(` ${log}`) },
@ -76,6 +74,10 @@ const envVars: Record<string, string> = {};
if (anthropicKey) envVars.ANTHROPIC_API_KEY = anthropicKey; if (anthropicKey) envVars.ANTHROPIC_API_KEY = anthropicKey;
if (openaiKey) envVars.OPENAI_API_KEY = openaiKey; 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({ const sandbox = await daytona.create({
snapshot: SNAPSHOT, snapshot: SNAPSHOT,
envVars, envVars,
@ -96,10 +98,25 @@ const envCheck = await sandbox.process.executeCommand(
console.log("Sandbox env:", envCheck.result.output || "(none)"); console.log("Sandbox env:", envCheck.result.output || "(none)");
const binCheck = await sandbox.process.executeCommand( 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); 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; const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url;
logInspectorUrl({ baseUrl }); logInspectorUrl({ baseUrl });

View file

@ -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"); 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 PORT = 3000;
const docker = new Docker({ socketPath: "/var/run/docker.sock" }); const docker = new Docker({ socketPath: "/var/run/docker.sock" });
@ -26,13 +27,18 @@ try {
console.log("Starting container..."); console.log("Starting container...");
const container = await docker.createContainer({ const container = await docker.createContainer({
Image: IMAGE, Image: IMAGE,
Cmd: ["bash", "-lc", [ Cmd: ["sh", "-c", [
"apt-get update && apt-get install -y curl ca-certificates", // Install dependencies (Alpine uses apk, not apt-get)
"curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh", "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 claude",
"sandbox-agent install-agent codex", "sandbox-agent install-agent codex",
`sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`, `sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`,
].join(" && ")], ].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`]: {} }, ExposedPorts: { [`${PORT}/tcp`]: {} },
HostConfig: { HostConfig: {
AutoRemove: true, AutoRemove: true,

View file

@ -1,5 +1,4 @@
import { Sandbox } from "@e2b/code-interpreter"; import { Sandbox } from "@e2b/code-interpreter";
import { SandboxAgent } from "sandbox-agent";
import { logInspectorUrl, runPrompt } from "@sandbox-agent/example-shared"; import { logInspectorUrl, runPrompt } from "@sandbox-agent/example-shared";
if (!process.env.E2B_API_KEY || (!process.env.OPENAI_API_KEY && !process.env.ANTHROPIC_API_KEY)) { 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; if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
const sandbox = await Sandbox.create({ allowInternetAccess: true, envs }); const sandbox = await Sandbox.create({ allowInternetAccess: true, envs });
const run = async (cmd: string) => {
const run = (cmd: string) => sandbox.commands.run(cmd); 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..."); 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..."); console.log("Starting server...");
await sandbox.commands.run("sandbox-agent server --no-token --host 0.0.0.0 --port 3000", { background: true }); 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 // Wait for server to be ready
console.log("Waiting for server..."); console.log("Waiting for server...");
const client = await SandboxAgent.connect({ baseUrl });
for (let i = 0; i < 30; i++) { for (let i = 0; i < 30; i++) {
try { try {
await client.getHealth(); const res = await fetch(`${baseUrl}/v1/health`);
break; if (res.ok) break;
} catch { } catch {
await new Promise((r) => setTimeout(r, 1000)); await new Promise((r) => setTimeout(r, 1000));
} }
} }
console.log("Installing agents...");
await client.installAgent("claude");
await client.installAgent("codex");
const cleanup = async () => { const cleanup = async () => {
console.log("Cleaning up..."); console.log("Cleaning up...");
await sandbox.kill(); await sandbox.kill();

View file

@ -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"
}
}

View file

@ -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
);
});

View file

@ -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();

View file

@ -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"]
}

12
pnpm-lock.yaml generated
View file

@ -179,7 +179,7 @@ importers:
dependencies: dependencies:
'@anthropic-ai/claude-code': '@anthropic-ai/claude-code':
specifier: latest specifier: latest
version: 2.1.20 version: 2.1.22
'@openai/codex': '@openai/codex':
specifier: latest specifier: latest
version: 0.92.0 version: 0.92.0
@ -258,6 +258,9 @@ importers:
'@daytonaio/sdk': '@daytonaio/sdk':
specifier: latest specifier: latest
version: 0.135.0(ws@8.19.0) version: 0.135.0(ws@8.19.0)
'@e2b/code-interpreter':
specifier: latest
version: 2.3.3
devDependencies: devDependencies:
'@types/node': '@types/node':
specifier: latest specifier: latest
@ -307,8 +310,8 @@ packages:
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'} engines: {node: '>=10'}
'@anthropic-ai/claude-code@2.1.20': '@anthropic-ai/claude-code@2.1.22':
resolution: {integrity: sha512-5r9OEF5TTmkhOKWtJ9RYqdn/vchwQWABO3dvgZVXftqlBZV/IiKjHVISu0dKtqWzByLBolchwePrhY68ul0QrA==} resolution: {integrity: sha512-WMwUUC/Ux87LqBDBC4KI/uYE0L/jcro3XcBzyd4a/YCkGVRyruyhypeeyHqAW7bUxm72xxWaPoy0keBkxpgIpQ==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
hasBin: true hasBin: true
@ -3606,6 +3609,7 @@ packages:
tar@7.5.6: tar@7.5.6:
resolution: {integrity: sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==} resolution: {integrity: sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==}
engines: {node: '>=18'} 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: text-decoder@1.2.3:
resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==}
@ -4143,7 +4147,7 @@ snapshots:
'@alloc/quick-lru@5.2.0': {} '@alloc/quick-lru@5.2.0': {}
'@anthropic-ai/claude-code@2.1.20': '@anthropic-ai/claude-code@2.1.22':
optionalDependencies: optionalDependencies:
'@img/sharp-darwin-arm64': 0.33.5 '@img/sharp-darwin-arm64': 0.33.5
'@img/sharp-darwin-x64': 0.33.5 '@img/sharp-darwin-x64': 0.33.5

View file

@ -10,7 +10,8 @@
"test:verbose": "tsx test-sandbox.ts docker --verbose" "test:verbose": "tsx test-sandbox.ts docker --verbose"
}, },
"dependencies": { "dependencies": {
"@daytonaio/sdk": "latest" "@daytonaio/sdk": "latest",
"@e2b/code-interpreter": "latest"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "latest", "@types/node": "latest",

View file

@ -77,8 +77,10 @@ async function buildSandboxAgent(): Promise<string> {
return "RELEASE"; return "RELEASE";
} }
// Binary is in workspace root target dir, not server target dir
const binaryPath = join(ROOT_DIR, "target/release/sandbox-agent");
if (skipBuild) { if (skipBuild) {
const binaryPath = join(SERVER_DIR, "target/release/sandbox-agent");
if (!existsSync(binaryPath)) { if (!existsSync(binaryPath)) {
throw new Error(`Binary not found at ${binaryPath}. Run without --skip-build.`); throw new Error(`Binary not found at ${binaryPath}. Run without --skip-build.`);
} }
@ -89,10 +91,9 @@ async function buildSandboxAgent(): Promise<string> {
log.info("Running cargo build --release..."); log.info("Running cargo build --release...");
try { try {
execSync("cargo build --release -p sandbox-agent", { execSync("cargo build --release -p sandbox-agent", {
cwd: SERVER_DIR, cwd: ROOT_DIR,
stdio: verbose ? "inherit" : "pipe", stdio: verbose ? "inherit" : "pipe",
}); });
const binaryPath = join(SERVER_DIR, "target/release/sandbox-agent");
log.success(`Built: ${binaryPath}`); log.success(`Built: ${binaryPath}`);
return binaryPath; return binaryPath;
} catch (err) { } catch (err) {
@ -116,6 +117,7 @@ interface Sandbox {
} }
// Docker provider // Docker provider
// Uses Alpine because Claude Code binary is built for musl libc
const dockerProvider: SandboxProvider = { const dockerProvider: SandboxProvider = {
name: "docker", name: "docker",
requiredEnv: [], requiredEnv: [],
@ -127,12 +129,12 @@ const dockerProvider: SandboxProvider = {
log.info(`Creating Docker container: ${id}`); log.info(`Creating Docker container: ${id}`);
execSync( 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" }, { stdio: verbose ? "inherit" : "pipe" },
); );
// Install curl // Install dependencies (Alpine uses apk; musl C++ libs needed for Claude Code)
execSync(`docker exec ${id} bash -c "apt-get update && apt-get install -y curl ca-certificates"`, { execSync(`docker exec ${id} sh -c "apk add --no-cache curl ca-certificates libstdc++ libgcc bash"`, {
stdio: verbose ? "inherit" : "pipe", stdio: verbose ? "inherit" : "pipe",
}); });
@ -140,7 +142,7 @@ const dockerProvider: SandboxProvider = {
id, id,
async exec(cmd) { async exec(cmd) {
try { 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", encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"], stdio: ["pipe", "pipe", "pipe"],
}); });
@ -174,6 +176,8 @@ const daytonaProvider: SandboxProvider = {
const daytona = new Daytona(); const daytona = new Daytona();
log.info("Creating Daytona sandbox..."); 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({ const sandbox = await daytona.create({
image: "ubuntu:22.04", image: "ubuntu:22.04",
envVars, 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 // Get provider
function getProvider(name: string): SandboxProvider { function getProvider(name: string): SandboxProvider {
switch (name) { switch (name) {
@ -217,8 +273,10 @@ function getProvider(name: string): SandboxProvider {
return dockerProvider; return dockerProvider;
case "daytona": case "daytona":
return daytonaProvider; return daytonaProvider;
case "e2b":
return e2bProvider;
default: 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") { if (binaryPath === "RELEASE") {
log.info("Installing from releases.rivet.dev..."); log.info("Installing from releases.rivet.dev...");
// Use 0.1.0-rc.1 which has install-agent command
const result = await sandbox.exec( 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}`); log.debug(`Install output: ${result.stdout}`);
if (result.exitCode !== 0) { if (result.exitCode !== 0) {
@ -249,47 +308,18 @@ async function installSandboxAgent(sandbox: Sandbox, binaryPath: string): Promis
async function installAgents(sandbox: Sandbox, agents: string[]): Promise<void> { async function installAgents(sandbox: Sandbox, agents: string[]): Promise<void> {
log.section("Installing agents"); 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) { for (const agent of agents) {
log.info(`Installing ${agent}...`); log.info(`Installing ${agent}...`);
if (agent === "claude") { if (agent === "claude" || agent === "codex") {
// First get the version const result = await sandbox.exec(`sandbox-agent install-agent ${agent}`);
const versionResult = await sandbox.exec( if (result.exitCode !== 0) throw new Error(`Failed to install ${agent}: ${result.stderr}`);
"curl -fsSL https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/latest", log.success(`Installed ${agent}`);
);
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}`);
} else if (agent === "mock") { } else if (agent === "mock") {
// Mock agent is built into sandbox-agent, no install needed // Mock agent is built into sandbox-agent, no install needed
log.info("Mock agent is built-in, skipping install"); 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 // Start server and check health
@ -422,10 +452,13 @@ async function checkEnvironment(sandbox: Sandbox): Promise<void> {
const checks = [ const checks = [
{ name: "Environment variables", cmd: "env | grep -E 'ANTHROPIC|OPENAI|CLAUDE|CODEX' | sed 's/=.*/=<set>/'" }, { name: "Environment variables", cmd: "env | grep -E 'ANTHROPIC|OPENAI|CLAUDE|CODEX' | sed 's/=.*/=<set>/'" },
{ 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: "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: "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) { for (const { name, cmd } of checks) {

View file

@ -84,7 +84,9 @@ impl Platform {
pub fn detect() -> Result<Self, AgentError> { pub fn detect() -> Result<Self, AgentError> {
let os = std::env::consts::OS; let os = std::env::consts::OS;
let arch = std::env::consts::ARCH; 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) { match (os, arch, is_musl) {
("linux", "x86_64", true) => Ok(Self::LinuxX64Musl), ("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)] #[derive(Debug, Clone)]