Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 7 additions & 12 deletions docs/deploy/docker.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,11 @@ docker run --rm -p 3000:3000 \
-e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
-e OPENAI_API_KEY="$OPENAI_API_KEY" \
alpine:latest sh -c "\
apk add --no-cache curl ca-certificates libstdc++ libgcc bash && \
apk add --no-cache curl ca-certificates libstdc++ libgcc bash nodejs npm && \
curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh && \
sandbox-agent install-agent claude && \
sandbox-agent install-agent codex && \
sandbox-agent server --no-token --host 0.0.0.0 --port 3000"
```

<Note>
Alpine is required for some agent binaries that target musl libc.
</Note>

## TypeScript with dockerode

```typescript
Expand All @@ -37,17 +31,18 @@ const docker = new Docker();
const PORT = 3000;

const container = await docker.createContainer({
Image: "alpine:latest",
Image: "node:22-bookworm-slim",
Cmd: ["sh", "-c", [
"apk add --no-cache curl ca-certificates libstdc++ libgcc bash",
"apt-get update",
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates bash libstdc++6",
"rm -rf /var/lib/apt/lists/*",
"curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/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: [
`ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}`,
`OPENAI_API_KEY=${process.env.OPENAI_API_KEY}`,
`CODEX_API_KEY=${process.env.CODEX_API_KEY}`,
].filter(Boolean),
ExposedPorts: { [`${PORT}/tcp`]: {} },
HostConfig: {
Expand All @@ -61,7 +56,7 @@ await container.start();
const baseUrl = `http://127.0.0.1:${PORT}`;
const sdk = await SandboxAgent.connect({ baseUrl });

const session = await sdk.createSession({ agent: "claude" });
const session = await sdk.createSession({ agent: "codex" });
await session.prompt([{ type: "text", text: "Summarize this repository." }]);
```

Expand Down
17 changes: 17 additions & 0 deletions docs/sdk-overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ const sdk = await SandboxAgent.connect({
});
```

`SandboxAgent.connect(...)` now waits for `/v1/health` by default before other SDK requests proceed. To disable that gate, pass `waitForHealth: false`. To keep the default gate but fail after a bounded wait, pass `waitForHealth: { timeoutMs: 120_000 }`. To cancel the startup wait early, pass `signal: abortController.signal`.

With a custom fetch handler (for example, proxying requests inside Workers):

```ts
Expand All @@ -47,6 +49,19 @@ const sdk = await SandboxAgent.connect({
});
```

With an abort signal for the startup health gate:

```ts
const controller = new AbortController();

const sdk = await SandboxAgent.connect({
baseUrl: "http://127.0.0.1:2468",
signal: controller.signal,
});

controller.abort();
```

With persistence:

```ts
Expand Down Expand Up @@ -170,6 +185,8 @@ Parameters:
- `token` (optional): Bearer token for authenticated servers
- `headers` (optional): Additional request headers
- `fetch` (optional): Custom fetch implementation used by SDK HTTP and ACP calls
- `waitForHealth` (optional, defaults to enabled): waits for `/v1/health` before HTTP helpers and ACP session setup proceed; pass `false` to disable or `{ timeoutMs }` to bound the wait
- `signal` (optional): aborts the startup `/v1/health` wait used by `connect()`

## Types

Expand Down
6 changes: 2 additions & 4 deletions examples/boxlite/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SimpleBox } from "@boxlite-ai/boxlite";
import { SandboxAgent } from "sandbox-agent";
import { detectAgent, buildInspectorUrl, waitForHealth } from "@sandbox-agent/example-shared";
import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared";
import { setupImage, OCI_DIR } from "./setup-image.ts";

const env: Record<string, string> = {};
Expand All @@ -26,9 +26,7 @@ if (result.exitCode !== 0) throw new Error(`Failed to start server: ${result.std

const baseUrl = "http://localhost:3000";

console.log("Waiting for server...");
await waitForHealth({ baseUrl });

console.log("Connecting to server...");
const client = await SandboxAgent.connect({ baseUrl });
const session = await client.createSession({ agent: detectAgent(), sessionInit: { cwd: "/root", mcpServers: [] } });
const sessionId = session.id;
Expand Down
5 changes: 1 addition & 4 deletions examples/computesdk/src/computesdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
type ProviderName,
} from "computesdk";
import { SandboxAgent } from "sandbox-agent";
import { detectAgent, buildInspectorUrl, waitForHealth } from "@sandbox-agent/example-shared";
import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared";
import { fileURLToPath } from "node:url";
import { resolve } from "node:path";

Expand Down Expand Up @@ -116,9 +116,6 @@ export async function setupComputeSdkSandboxAgent(): Promise<{

const baseUrl = await sandbox.getUrl({ port: PORT });

console.log("Waiting for server...");
await waitForHealth({ baseUrl });

const cleanup = async () => {
try {
await sandbox.destroy();
Expand Down
6 changes: 2 additions & 4 deletions examples/daytona/src/daytona-with-snapshot.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Daytona, Image } from "@daytonaio/sdk";
import { SandboxAgent } from "sandbox-agent";
import { detectAgent, buildInspectorUrl, waitForHealth } from "@sandbox-agent/example-shared";
import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared";

const daytona = new Daytona();

Expand All @@ -25,9 +25,7 @@ await sandbox.process.executeCommand(

const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url;

console.log("Waiting for server...");
await waitForHealth({ baseUrl });

console.log("Connecting to server...");
const client = await SandboxAgent.connect({ baseUrl });
const session = await client.createSession({ agent: detectAgent(), sessionInit: { cwd: "/home/daytona", mcpServers: [] } });
const sessionId = session.id;
Expand Down
6 changes: 2 additions & 4 deletions examples/daytona/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Daytona } from "@daytonaio/sdk";
import { SandboxAgent } from "sandbox-agent";
import { detectAgent, buildInspectorUrl, waitForHealth } from "@sandbox-agent/example-shared";
import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared";

const daytona = new Daytona();

Expand Down Expand Up @@ -30,9 +30,7 @@ await sandbox.process.executeCommand(

const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url;

console.log("Waiting for server...");
await waitForHealth({ baseUrl });

console.log("Connecting to server...");
const client = await SandboxAgent.connect({ baseUrl });
const session = await client.createSession({ agent: detectAgent(), sessionInit: { cwd: "/home/daytona", mcpServers: [] } });
const sessionId = session.id;
Expand Down
22 changes: 15 additions & 7 deletions examples/docker/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import Docker from "dockerode";
import fs from "node:fs";
import path from "node:path";
import { SandboxAgent } from "sandbox-agent";
import { detectAgent, buildInspectorUrl, waitForHealth } from "@sandbox-agent/example-shared";
import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared";

const IMAGE = "alpine:latest";
const IMAGE = "node:22-bookworm-slim";
const PORT = 3000;
const agent = detectAgent();
const codexAuthPath = process.env.HOME ? path.join(process.env.HOME, ".codex", "auth.json") : null;
const bindMounts = codexAuthPath && fs.existsSync(codexAuthPath)
? [`${codexAuthPath}:/root/.codex/auth.json:ro`]
: [];

const docker = new Docker({ socketPath: "/var/run/docker.sock" });

Expand All @@ -24,29 +31,30 @@ console.log("Starting container...");
const container = await docker.createContainer({
Image: IMAGE,
Cmd: ["sh", "-c", [
"apk add --no-cache curl ca-certificates libstdc++ libgcc bash",
"apt-get update",
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates bash libstdc++6",
"rm -rf /var/lib/apt/lists/*",
"curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/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}` : "",
process.env.CODEX_API_KEY ? `CODEX_API_KEY=${process.env.CODEX_API_KEY}` : "",
].filter(Boolean),
ExposedPorts: { [`${PORT}/tcp`]: {} },
HostConfig: {
AutoRemove: true,
PortBindings: { [`${PORT}/tcp`]: [{ HostPort: `${PORT}` }] },
Binds: bindMounts,
},
});
await container.start();

const baseUrl = `http://127.0.0.1:${PORT}`;
await waitForHealth({ baseUrl });

const client = await SandboxAgent.connect({ baseUrl });
const session = await client.createSession({ agent: detectAgent(), sessionInit: { cwd: "/root", mcpServers: [] } });
const session = await client.createSession({ agent, sessionInit: { cwd: "/root", mcpServers: [] } });
const sessionId = session.id;

console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
Expand Down
6 changes: 2 additions & 4 deletions examples/e2b/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Sandbox } from "@e2b/code-interpreter";
import { SandboxAgent } from "sandbox-agent";
import { detectAgent, buildInspectorUrl, waitForHealth } from "@sandbox-agent/example-shared";
import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared";

const envs: Record<string, string> = {};
if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
Expand All @@ -27,9 +27,7 @@ await sandbox.commands.run("sandbox-agent server --no-token --host 0.0.0.0 --por

const baseUrl = `https://${sandbox.getHost(3000)}`;

console.log("Waiting for server...");
await waitForHealth({ baseUrl });

console.log("Connecting to server...");
const client = await SandboxAgent.connect({ baseUrl });
const session = await client.createSession({ agent: detectAgent(), sessionInit: { cwd: "/home/user", mcpServers: [] } });
const sessionId = session.id;
Expand Down
15 changes: 2 additions & 13 deletions examples/shared/src/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import fs from "node:fs";
import path from "node:path";
import { PassThrough } from "node:stream";
import { fileURLToPath } from "node:url";
import { waitForHealth } from "./sandbox-agent-client.ts";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const EXAMPLE_IMAGE = "sandbox-agent-examples:latest";
Expand Down Expand Up @@ -173,7 +172,7 @@ async function ensureExampleImage(_docker: Docker): Promise<string> {
}

/**
* Start a Docker container running sandbox-agent and wait for it to be healthy.
* Start a Docker container running sandbox-agent.
* Registers SIGINT/SIGTERM handlers for cleanup.
*/
export async function startDockerSandbox(opts: DockerSandboxOptions): Promise<DockerSandbox> {
Expand Down Expand Up @@ -275,18 +274,8 @@ export async function startDockerSandbox(opts: DockerSandboxOptions): Promise<Do
}
const baseUrl = `http://127.0.0.1:${mappedHostPort}`;

try {
await waitForHealth({ baseUrl });
} catch (err) {
stopStartupLogs();
console.error(" Container logs:");
for (const chunk of logChunks) {
process.stderr.write(` ${chunk}`);
}
throw err;
}
stopStartupLogs();
console.log(` Ready (${baseUrl})`);
console.log(` Started (${baseUrl})`);

const cleanup = async () => {
stopStartupLogs();
Expand Down
38 changes: 0 additions & 38 deletions examples/shared/src/sandbox-agent-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
* Provides minimal helpers for connecting to and interacting with sandbox-agent servers.
*/

import { setTimeout as delay } from "node:timers/promises";

function normalizeBaseUrl(baseUrl: string): string {
return baseUrl.replace(/\/+$/, "");
}
Expand Down Expand Up @@ -74,41 +72,6 @@ export function buildHeaders({
return headers;
}

export async function waitForHealth({
baseUrl,
token,
extraHeaders,
timeoutMs = 120_000,
}: {
baseUrl: string;
token?: string;
extraHeaders?: Record<string, string>;
timeoutMs?: number;
}): Promise<void> {
const normalized = normalizeBaseUrl(baseUrl);
const deadline = Date.now() + timeoutMs;
let lastError: unknown;
while (Date.now() < deadline) {
try {
const headers = buildHeaders({ token, extraHeaders });
const response = await fetch(`${normalized}/v1/health`, { headers });
if (response.ok) {
const data = await response.json();
if (data?.status === "ok") {
return;
}
lastError = new Error(`Unexpected health response: ${JSON.stringify(data)}`);
} else {
lastError = new Error(`Health check failed: ${response.status}`);
}
} catch (error) {
lastError = error;
}
await delay(500);
}
throw (lastError ?? new Error("Timed out waiting for /v1/health")) as Error;
}

export function generateSessionId(): string {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
let id = "session-";
Expand Down Expand Up @@ -144,4 +107,3 @@ export function detectAgent(): string {
}
return "claude";
}

6 changes: 2 additions & 4 deletions examples/vercel/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Sandbox } from "@vercel/sandbox";
import { SandboxAgent } from "sandbox-agent";
import { detectAgent, buildInspectorUrl, waitForHealth } from "@sandbox-agent/example-shared";
import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared";

const envs: Record<string, string> = {};
if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
Expand Down Expand Up @@ -38,9 +38,7 @@ await sandbox.runCommand({

const baseUrl = sandbox.domain(3000);

console.log("Waiting for server...");
await waitForHealth({ baseUrl });

console.log("Connecting to server...");
const client = await SandboxAgent.connect({ baseUrl });
const session = await client.createSession({ agent: detectAgent(), sessionInit: { cwd: "/home/vercel-sandbox", mcpServers: [] } });
const sessionId = session.id;
Expand Down
Loading