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

View file

@ -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<string, string> = {};
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 });

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");
}
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,

View file

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

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