mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 07:04:48 +00:00
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:
parent
0bbe92b344
commit
cbd36eeca8
12 changed files with 228 additions and 194 deletions
76
docs/troubleshooting.mdx
Normal file
76
docs/troubleshooting.mdx
Normal 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.
|
||||
|
|
@ -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 });
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
});
|
||||
|
|
@ -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();
|
||||
|
|
@ -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
12
pnpm-lock.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -77,8 +77,10 @@ async function buildSandboxAgent(): Promise<string> {
|
|||
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<string> {
|
|||
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<void> {
|
||||
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<void> {
|
|||
|
||||
const checks = [
|
||||
{ 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: "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) {
|
||||
|
|
|
|||
|
|
@ -84,7 +84,9 @@ impl Platform {
|
|||
pub fn detect() -> Result<Self, AgentError> {
|
||||
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)]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue