Configure lefthook formatter checks (#231)

* Add lefthook formatter checks

* Fix SDK mode hydration

* Stabilize SDK mode integration test
This commit is contained in:
Nathan Flurry 2026-03-10 23:03:11 -07:00 committed by GitHub
parent 0471214d65
commit d2346bafb3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
282 changed files with 5840 additions and 8399 deletions

View file

@ -11,17 +11,14 @@ setupImage();
console.log("Creating BoxLite sandbox...");
const box = new SimpleBox({
rootfsPath: OCI_DIR,
env,
ports: [{ hostPort: 3000, guestPort: 3000 }],
diskSizeGb: 4,
rootfsPath: OCI_DIR,
env,
ports: [{ hostPort: 3000, guestPort: 3000 }],
diskSizeGb: 4,
});
console.log("Starting server...");
const result = await box.exec(
"sh", "-c",
"nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &",
);
const result = await box.exec("sh", "-c", "nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &");
if (result.exitCode !== 0) throw new Error(`Failed to start server: ${result.stderr}`);
const baseUrl = "http://localhost:3000";
@ -36,9 +33,9 @@ console.log(" Press Ctrl+C to stop.");
const keepAlive = setInterval(() => {}, 60_000);
const cleanup = async () => {
clearInterval(keepAlive);
await box.stop();
process.exit(0);
clearInterval(keepAlive);
await box.stop();
process.exit(0);
};
process.once("SIGINT", cleanup);
process.once("SIGTERM", cleanup);

View file

@ -5,12 +5,12 @@ export const DOCKER_IMAGE = "sandbox-agent-boxlite";
export const OCI_DIR = new URL("../oci-image", import.meta.url).pathname;
export function setupImage() {
console.log(`Building image "${DOCKER_IMAGE}" (cached after first run)...`);
execSync(`docker build -t ${DOCKER_IMAGE} ${new URL("..", import.meta.url).pathname}`, { stdio: "inherit" });
console.log(`Building image "${DOCKER_IMAGE}" (cached after first run)...`);
execSync(`docker build -t ${DOCKER_IMAGE} ${new URL("..", import.meta.url).pathname}`, { stdio: "inherit" });
if (!existsSync(`${OCI_DIR}/oci-layout`)) {
console.log("Exporting to OCI layout...");
mkdirSync(OCI_DIR, { recursive: true });
execSync(`docker save ${DOCKER_IMAGE} | tar -xf - -C ${OCI_DIR}`, { stdio: "inherit" });
}
if (!existsSync(`${OCI_DIR}/oci-layout`)) {
console.log("Exporting to OCI layout...");
mkdirSync(OCI_DIR, { recursive: true });
execSync(`docker save ${DOCKER_IMAGE} | tar -xf - -C ${OCI_DIR}`, { stdio: "inherit" });
}
}

View file

@ -128,7 +128,7 @@ export function App() {
console.error("Event stream error:", err);
}
},
[log]
[log],
);
const send = useCallback(async () => {
@ -162,12 +162,7 @@ export function App() {
<div style={styles.connectForm}>
<label style={styles.label}>
Sandbox name:
<input
style={styles.input}
value={sandboxName}
onChange={(e) => setSandboxName(e.target.value)}
placeholder="demo"
/>
<input style={styles.input} value={sandboxName} onChange={(e) => setSandboxName(e.target.value)} placeholder="demo" />
</label>
<button style={styles.button} onClick={connect}>
Connect

View file

@ -5,5 +5,5 @@ import { App } from "./App";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>
</StrictMode>,
);

View file

@ -2,65 +2,61 @@ import type { Sandbox } from "@cloudflare/sandbox";
import { SandboxAgent } from "sandbox-agent";
export type PromptRequest = {
agent?: string;
prompt?: string;
agent?: string;
prompt?: string;
};
export async function runPromptEndpointStream(
sandbox: Sandbox,
request: PromptRequest,
port: number,
emit: (event: { type: string; [key: string]: unknown }) => Promise<void> | void,
sandbox: Sandbox,
request: PromptRequest,
port: number,
emit: (event: { type: string; [key: string]: unknown }) => Promise<void> | void,
): Promise<void> {
const client = await SandboxAgent.connect({
fetch: (req, init) =>
sandbox.containerFetch(
req,
{
...(init ?? {}),
// Cloudflare containerFetch may drop long-lived update streams when
// a forwarded AbortSignal is cancelled; clear it for this path.
signal: undefined,
},
port,
),
});
const client = await SandboxAgent.connect({
fetch: (req, init) =>
sandbox.containerFetch(
req,
{
...(init ?? {}),
// Cloudflare containerFetch may drop long-lived update streams when
// a forwarded AbortSignal is cancelled; clear it for this path.
signal: undefined,
},
port,
),
});
let unsubscribe: (() => void) | undefined;
try {
const session = await client.createSession({
agent: request.agent ?? "codex",
});
let unsubscribe: (() => void) | undefined;
try {
const session = await client.createSession({
agent: request.agent ?? "codex",
});
const promptText =
request.prompt?.trim() || "Reply with a short confirmation.";
await emit({
type: "session.created",
sessionId: session.id,
agent: session.agent,
prompt: promptText,
});
const promptText = request.prompt?.trim() || "Reply with a short confirmation.";
await emit({
type: "session.created",
sessionId: session.id,
agent: session.agent,
prompt: promptText,
});
let pendingWrites: Promise<void> = Promise.resolve();
unsubscribe = session.onEvent((event) => {
pendingWrites = pendingWrites
.then(async () => {
await emit({ type: "session.event", event });
})
.catch(() => {});
});
let pendingWrites: Promise<void> = Promise.resolve();
unsubscribe = session.onEvent((event) => {
pendingWrites = pendingWrites
.then(async () => {
await emit({ type: "session.event", event });
})
.catch(() => {});
});
const response = await session.prompt([{ type: "text", text: promptText }]);
await pendingWrites;
await emit({ type: "prompt.response", response });
await emit({ type: "prompt.completed" });
} finally {
if (unsubscribe) {
unsubscribe();
}
await Promise.race([
client.dispose(),
new Promise((resolve) => setTimeout(resolve, 250)),
]);
}
const response = await session.prompt([{ type: "text", text: promptText }]);
await pendingWrites;
await emit({ type: "prompt.response", response });
await emit({ type: "prompt.completed" });
} finally {
if (unsubscribe) {
unsubscribe();
}
await Promise.race([client.dispose(), new Promise((resolve) => setTimeout(resolve, 250))]);
}
}

View file

@ -15,8 +15,7 @@ import { fileURLToPath } from "node:url";
import { resolve } from "node:path";
const PORT = 3000;
const REQUEST_TIMEOUT_MS =
Number.parseInt(process.env.COMPUTESDK_TIMEOUT_MS || "", 10) || 120_000;
const REQUEST_TIMEOUT_MS = Number.parseInt(process.env.COMPUTESDK_TIMEOUT_MS || "", 10) || 120_000;
/**
* Detects and validates the provider to use.
@ -24,28 +23,22 @@ const REQUEST_TIMEOUT_MS =
*/
function resolveProvider(): ProviderName {
const providerOverride = process.env.COMPUTESDK_PROVIDER;
if (providerOverride) {
if (!isValidProvider(providerOverride)) {
throw new Error(
`Unsupported ComputeSDK provider "${providerOverride}". Supported providers: ${PROVIDER_NAMES.join(", ")}`
);
throw new Error(`Unsupported ComputeSDK provider "${providerOverride}". Supported providers: ${PROVIDER_NAMES.join(", ")}`);
}
if (!isProviderAuthComplete(providerOverride)) {
const missing = getMissingEnvVars(providerOverride);
throw new Error(
`Missing credentials for provider "${providerOverride}". Set: ${missing.join(", ")}`
);
throw new Error(`Missing credentials for provider "${providerOverride}". Set: ${missing.join(", ")}`);
}
console.log(`Using ComputeSDK provider: ${providerOverride} (explicit)`);
return providerOverride as ProviderName;
}
const detected = detectProvider();
if (!detected) {
throw new Error(
`No provider credentials found. Set one of: ${PROVIDER_NAMES.map((p) => getMissingEnvVars(p).join(", ")).join(" | ")}`
);
throw new Error(`No provider credentials found. Set one of: ${PROVIDER_NAMES.map((p) => getMissingEnvVars(p).join(", ")).join(" | ")}`);
}
console.log(`Using ComputeSDK provider: ${detected} (auto-detected)`);
return detected as ProviderName;
@ -53,20 +46,19 @@ function resolveProvider(): ProviderName {
function configureComputeSDK(): void {
const provider = resolveProvider();
const config: ExplicitComputeConfig = {
provider,
computesdkApiKey: process.env.COMPUTESDK_API_KEY,
requestTimeoutMs: REQUEST_TIMEOUT_MS,
};
const providerConfig = getProviderConfigFromEnv(provider);
if (Object.keys(providerConfig).length > 0) {
const configWithProvider =
config as ExplicitComputeConfig & Record<ProviderName, Record<string, string>>;
const configWithProvider = config as ExplicitComputeConfig & Record<ProviderName, Record<string, string>>;
configWithProvider[provider] = providerConfig;
}
compute.setConfig(config);
}
@ -149,9 +141,7 @@ export async function runComputeSdkExample(): Promise<void> {
await new Promise(() => {});
}
const isDirectRun = Boolean(
process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)
);
const isDirectRun = Boolean(process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url));
if (isDirectRun) {
runComputeSdkExample().catch((error) => {

View file

@ -5,12 +5,7 @@ import { setupComputeSdkSandboxAgent } from "../src/computesdk.ts";
const hasModal = Boolean(process.env.MODAL_TOKEN_ID && process.env.MODAL_TOKEN_SECRET);
const hasVercel = Boolean(process.env.VERCEL_TOKEN || process.env.VERCEL_OIDC_TOKEN);
const hasProviderKey = Boolean(
process.env.BLAXEL_API_KEY ||
process.env.CSB_API_KEY ||
process.env.DAYTONA_API_KEY ||
process.env.E2B_API_KEY ||
hasModal ||
hasVercel
process.env.BLAXEL_API_KEY || process.env.CSB_API_KEY || process.env.DAYTONA_API_KEY || process.env.E2B_API_KEY || hasModal || hasVercel,
);
const shouldRun = Boolean(process.env.COMPUTESDK_API_KEY) && hasProviderKey;
@ -34,6 +29,6 @@ describe("computesdk example", () => {
await cleanup();
}
},
timeoutMs
timeoutMs,
);
});

View file

@ -5,23 +5,19 @@ import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared";
const daytona = new Daytona();
const envVars: Record<string, string> = {};
if (process.env.ANTHROPIC_API_KEY)
envVars.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
if (process.env.OPENAI_API_KEY)
envVars.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
if (process.env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
if (process.env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
// Build a custom image with sandbox-agent pre-installed (slower first run, faster subsequent runs)
const image = Image.base("ubuntu:22.04").runCommands(
"apt-get update && apt-get install -y curl ca-certificates",
"curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh",
"apt-get update && apt-get install -y curl ca-certificates",
"curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh",
);
console.log("Creating Daytona sandbox (first run builds the base image and may take a few minutes, subsequent runs are fast)...");
const sandbox = await daytona.create({ envVars, image, autoStopInterval: 0 }, { timeout: 180 });
await sandbox.process.executeCommand(
"nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &",
);
await sandbox.process.executeCommand("nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &");
const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url;
@ -35,9 +31,9 @@ console.log(" Press Ctrl+C to stop.");
const keepAlive = setInterval(() => {}, 60_000);
const cleanup = async () => {
clearInterval(keepAlive);
await sandbox.delete(60);
process.exit(0);
clearInterval(keepAlive);
await sandbox.delete(60);
process.exit(0);
};
process.once("SIGINT", cleanup);
process.once("SIGTERM", cleanup);

View file

@ -5,10 +5,8 @@ import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared";
const daytona = new Daytona();
const envVars: Record<string, string> = {};
if (process.env.ANTHROPIC_API_KEY)
envVars.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
if (process.env.OPENAI_API_KEY)
envVars.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
if (process.env.ANTHROPIC_API_KEY) envVars.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
if (process.env.OPENAI_API_KEY) envVars.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
// Use default image and install sandbox-agent at runtime (faster startup, no snapshot build)
console.log("Creating Daytona sandbox...");
@ -16,17 +14,13 @@ const sandbox = await daytona.create({ envVars, autoStopInterval: 0 });
// Install sandbox-agent and start server
console.log("Installing sandbox-agent...");
await sandbox.process.executeCommand(
"curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh",
);
await sandbox.process.executeCommand("curl -fsSL https://releases.rivet.dev/sandbox-agent/0.3.x/install.sh | sh");
console.log("Installing agents...");
await sandbox.process.executeCommand("sandbox-agent install-agent claude");
await sandbox.process.executeCommand("sandbox-agent install-agent codex");
await sandbox.process.executeCommand(
"nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &",
);
await sandbox.process.executeCommand("nohup sandbox-agent server --no-token --host 0.0.0.0 --port 3000 >/tmp/sandbox-agent.log 2>&1 &");
const baseUrl = (await sandbox.getSignedPreviewUrl(3000, 4 * 60 * 60)).url;
@ -40,9 +34,9 @@ console.log(" Press Ctrl+C to stop.");
const keepAlive = setInterval(() => {}, 60_000);
const cleanup = async () => {
clearInterval(keepAlive);
await sandbox.delete(60);
process.exit(0);
clearInterval(keepAlive);
await sandbox.delete(60);
process.exit(0);
};
process.once("SIGINT", cleanup);
process.once("SIGTERM", cleanup);

View file

@ -23,6 +23,6 @@ describe("daytona example", () => {
await cleanup();
}
},
timeoutMs
timeoutMs,
);
});

View file

@ -8,9 +8,7 @@ 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 bindMounts = codexAuthPath && fs.existsSync(codexAuthPath) ? [`${codexAuthPath}:/root/.codex/auth.json:ro`] : [];
const docker = new Docker({ socketPath: "/var/run/docker.sock" });
@ -22,7 +20,7 @@ try {
await new Promise<void>((resolve, reject) => {
docker.pull(IMAGE, (err: Error | null, stream: NodeJS.ReadableStream) => {
if (err) return reject(err);
docker.modem.followProgress(stream, (err: Error | null) => err ? reject(err) : resolve());
docker.modem.followProgress(stream, (err: Error | null) => (err ? reject(err) : resolve()));
});
});
}
@ -30,13 +28,17 @@ try {
console.log("Starting container...");
const container = await docker.createContainer({
Image: IMAGE,
Cmd: ["sh", "-c", [
"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.3.x/install.sh | sh",
`sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`,
].join(" && ")],
Cmd: [
"sh",
"-c",
[
"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.3.x/install.sh | sh",
`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}` : "",
@ -63,8 +65,12 @@ console.log(" Press Ctrl+C to stop.");
const keepAlive = setInterval(() => {}, 60_000);
const cleanup = async () => {
clearInterval(keepAlive);
try { await container.stop({ t: 5 }); } catch {}
try { await container.remove({ force: true }); } catch {}
try {
await container.stop({ t: 5 });
} catch {}
try {
await container.remove({ force: true });
} catch {}
process.exit(0);
};
process.once("SIGINT", cleanup);

View file

@ -23,6 +23,6 @@ describe("docker example", () => {
await cleanup();
}
},
timeoutMs
timeoutMs,
);
});

View file

@ -23,6 +23,6 @@ describe("e2b example", () => {
await cleanup();
}
},
timeoutMs
timeoutMs,
);
});

View file

@ -24,10 +24,7 @@ console.log("Uploading files via batch tar...");
const client = await SandboxAgent.connect({ baseUrl });
const tarPath = path.join(tmpDir, "upload.tar");
await tar.create(
{ file: tarPath, cwd: tmpDir },
["my-project"],
);
await tar.create({ file: tarPath, cwd: tmpDir }, ["my-project"]);
const tarBuffer = await fs.promises.readFile(tarPath);
const uploadResult = await client.uploadFsBatch(tarBuffer, { path: "/opt" });
console.log(` Uploaded ${uploadResult.paths.length} files: ${uploadResult.paths.join(", ")}`);
@ -54,4 +51,7 @@ console.log(' Try: "read the README in /opt/my-project"');
console.log(" Press Ctrl+C to stop.");
const keepAlive = setInterval(() => {}, 60_000);
process.on("SIGINT", () => { clearInterval(keepAlive); cleanup().then(() => process.exit(0)); });
process.on("SIGINT", () => {
clearInterval(keepAlive);
cleanup().then(() => process.exit(0));
});

View file

@ -23,10 +23,7 @@ console.log("Uploading MCP server bundle...");
const client = await SandboxAgent.connect({ baseUrl });
const bundle = await fs.promises.readFile(serverFile);
const written = await client.writeFsFile(
{ path: "/opt/mcp/custom-tools/mcp-server.cjs" },
bundle,
);
const written = await client.writeFsFile({ path: "/opt/mcp/custom-tools/mcp-server.cjs" }, bundle);
console.log(` Written: ${written.path} (${written.bytesWritten} bytes)`);
// Create a session with the uploaded MCP server as a local command.
@ -35,12 +32,14 @@ const session = await client.createSession({
agent: detectAgent(),
sessionInit: {
cwd: "/root",
mcpServers: [{
name: "customTools",
command: "node",
args: ["/opt/mcp/custom-tools/mcp-server.cjs"],
env: [],
}],
mcpServers: [
{
name: "customTools",
command: "node",
args: ["/opt/mcp/custom-tools/mcp-server.cjs"],
env: [],
},
],
},
});
const sessionId = session.id;
@ -49,4 +48,7 @@ console.log(' Try: "generate a random number between 1 and 100"');
console.log(" Press Ctrl+C to stop.");
const keepAlive = setInterval(() => {}, 60_000);
process.on("SIGINT", () => { clearInterval(keepAlive); cleanup().then(() => process.exit(0)); });
process.on("SIGINT", () => {
clearInterval(keepAlive);
cleanup().then(() => process.exit(0));
});

View file

@ -5,9 +5,7 @@ import { startDockerSandbox } from "@sandbox-agent/example-shared/docker";
console.log("Starting sandbox...");
const { baseUrl, cleanup } = await startDockerSandbox({
port: 3002,
setupCommands: [
"npm install -g --silent @modelcontextprotocol/server-everything@2026.1.26",
],
setupCommands: ["npm install -g --silent @modelcontextprotocol/server-everything@2026.1.26"],
});
console.log("Creating session with everything MCP server...");
@ -16,12 +14,14 @@ const session = await client.createSession({
agent: detectAgent(),
sessionInit: {
cwd: "/root",
mcpServers: [{
name: "everything",
command: "mcp-server-everything",
args: [],
env: [],
}],
mcpServers: [
{
name: "everything",
command: "mcp-server-everything",
args: [],
env: [],
},
],
},
});
const sessionId = session.id;
@ -30,4 +30,7 @@ console.log(' Try: "generate a random number between 1 and 100"');
console.log(" Press Ctrl+C to stop.");
const keepAlive = setInterval(() => {}, 60_000);
process.on("SIGINT", () => { clearInterval(keepAlive); cleanup().then(() => process.exit(0)); });
process.on("SIGINT", () => {
clearInterval(keepAlive);
cleanup().then(() => process.exit(0));
});

View file

@ -1,18 +1,12 @@
import { createInterface } from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
import { Command } from "commander";
import {
SandboxAgent,
type PermissionReply,
type SessionPermissionRequest,
} from "sandbox-agent";
import { SandboxAgent, type PermissionReply, type SessionPermissionRequest } from "sandbox-agent";
const options = parseOptions();
const agent = options.agent.trim().toLowerCase();
const autoReply = parsePermissionReply(options.reply);
const promptText =
options.prompt?.trim() ||
`Create ./permission-example.txt with the text 'hello from the ${agent} permissions example'.`;
const promptText = options.prompt?.trim() || `Create ./permission-example.txt with the text 'hello from the ${agent} permissions example'.`;
const sdk = await SandboxAgent.start({
spawn: {
@ -31,11 +25,7 @@ try {
: [];
const modeOption = configOptions.find((option) => option.category === "mode");
const availableModes = extractOptionValues(modeOption);
const mode =
options.mode?.trim() ||
(typeof modeOption?.currentValue === "string" ? modeOption.currentValue : "") ||
availableModes[0] ||
"";
const mode = options.mode?.trim() || (typeof modeOption?.currentValue === "string" ? modeOption.currentValue : "") || availableModes[0] || "";
console.log(`Agent: ${agent}`);
console.log(`Mode: ${mode || "(default)"}`);
@ -91,10 +81,7 @@ async function handlePermissionRequest(
await session.respondPermission(request.id, reply);
}
async function promptForReply(
request: SessionPermissionRequest,
rl: ReturnType<typeof createInterface> | null,
): Promise<PermissionReply> {
async function promptForReply(request: SessionPermissionRequest, rl: ReturnType<typeof createInterface> | null): Promise<PermissionReply> {
if (!rl) {
return "reject";
}
@ -136,8 +123,7 @@ function extractOptionValues(option: { options?: unknown[] } | undefined): strin
if (!nested || typeof nested !== "object") {
continue;
}
const nestedValue =
"value" in nested && typeof nested.value === "string" ? nested.value : null;
const nestedValue = "value" in nested && typeof nested.value === "string" ? nested.value : null;
if (nestedValue) {
values.push(nestedValue);
}

View file

@ -7,10 +7,7 @@ const persist = new InMemorySessionPersistDriver();
console.log("Starting sandbox...");
const sandbox = await startDockerSandbox({
port: 3000,
setupCommands: [
"sandbox-agent install-agent claude",
"sandbox-agent install-agent codex",
],
setupCommands: ["sandbox-agent install-agent claude", "sandbox-agent install-agent codex"],
});
const sdk = await SandboxAgent.connect({ baseUrl: sandbox.baseUrl, persist });

View file

@ -16,21 +16,47 @@ if (process.env.DATABASE_URL) {
connectionString = process.env.DATABASE_URL;
} else {
const name = `persist-example-${randomUUID().slice(0, 8)}`;
containerId = execFileSync("docker", [
"run", "-d", "--rm", "--name", name,
"-e", "POSTGRES_USER=postgres", "-e", "POSTGRES_PASSWORD=postgres", "-e", "POSTGRES_DB=sandbox",
"-p", "127.0.0.1::5432", "postgres:16-alpine",
], { encoding: "utf8" }).trim();
containerId = execFileSync(
"docker",
[
"run",
"-d",
"--rm",
"--name",
name,
"-e",
"POSTGRES_USER=postgres",
"-e",
"POSTGRES_PASSWORD=postgres",
"-e",
"POSTGRES_DB=sandbox",
"-p",
"127.0.0.1::5432",
"postgres:16-alpine",
],
{ encoding: "utf8" },
).trim();
const port = execFileSync("docker", ["port", containerId, "5432/tcp"], { encoding: "utf8" })
.trim().split("\n")[0]?.match(/:(\d+)$/)?.[1];
.trim()
.split("\n")[0]
?.match(/:(\d+)$/)?.[1];
connectionString = `postgres://postgres:postgres@127.0.0.1:${port}/sandbox`;
console.log(`Postgres on port ${port}`);
const deadline = Date.now() + 30_000;
while (Date.now() < deadline) {
const c = new Client({ connectionString });
try { await c.connect(); await c.query("SELECT 1"); await c.end(); break; }
catch { try { await c.end(); } catch {} await delay(250); }
try {
await c.connect();
await c.query("SELECT 1");
await c.end();
break;
} catch {
try {
await c.end();
} catch {}
await delay(250);
}
}
}
@ -40,10 +66,7 @@ try {
console.log("Starting sandbox...");
const sandbox = await startDockerSandbox({
port: 3000,
setupCommands: [
"sandbox-agent install-agent claude",
"sandbox-agent install-agent codex",
],
setupCommands: ["sandbox-agent install-agent claude", "sandbox-agent install-agent codex"],
});
const sdk = await SandboxAgent.connect({ baseUrl: sandbox.baseUrl, persist });
@ -71,6 +94,8 @@ try {
await sandbox.cleanup();
} finally {
if (containerId) {
try { execFileSync("docker", ["rm", "-f", containerId], { stdio: "ignore" }); } catch {}
try {
execFileSync("docker", ["rm", "-f", containerId], { stdio: "ignore" });
} catch {}
}
}

View file

@ -8,10 +8,7 @@ const persist = new SQLiteSessionPersistDriver({ filename: "./sessions.db" });
console.log("Starting sandbox...");
const sandbox = await startDockerSandbox({
port: 3000,
setupCommands: [
"sandbox-agent install-agent claude",
"sandbox-agent install-agent codex",
],
setupCommands: ["sandbox-agent install-agent claude", "sandbox-agent install-agent codex"],
});
const sdk = await SandboxAgent.connect({ baseUrl: sandbox.baseUrl, persist });

View file

@ -40,7 +40,7 @@ const DIRECT_CREDENTIAL_KEYS = [
function stripShellQuotes(value: string): string {
const trimmed = value.trim();
if (trimmed.length >= 2 && trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
return trimmed.slice(1, -1);
}
if (trimmed.length >= 2 && trimmed.startsWith("'") && trimmed.endsWith("'")) {
@ -107,11 +107,7 @@ function collectCredentialEnv(): Record<string, string> {
const merged: Record<string, string> = {};
let extracted: Record<string, string> = {};
try {
const output = execFileSync(
"sandbox-agent",
["credentials", "extract-env"],
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
);
const output = execFileSync("sandbox-agent", ["credentials", "extract-env"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
extracted = parseExtractedCredentials(output);
} catch {
// Fall back to direct env vars if extraction is unavailable.
@ -132,10 +128,7 @@ function shellSingleQuotedLiteral(value: string): string {
}
function stripAnsi(value: string): string {
return value.replace(
/[\u001B\u009B][[\]()#;?]*(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007|(?:\d{1,4}(?:;\d{0,4})*)?[0-9A-ORZcf-nqry=><])/g,
"",
);
return value.replace(/[\u001B\u009B][[\]()#;?]*(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007|(?:\d{1,4}(?:;\d{0,4})*)?[0-9A-ORZcf-nqry=><])/g, "");
}
async function ensureExampleImage(_docker: Docker): Promise<string> {
@ -145,11 +138,7 @@ async function ensureExampleImage(_docker: Docker): Promise<string> {
if (dev) {
console.log(" Building sandbox image from source (may take a while, only runs once)...");
try {
execFileSync("docker", [
"build", "-t", imageName,
"-f", path.join(DOCKERFILE_DIR, "Dockerfile.dev"),
REPO_ROOT,
], {
execFileSync("docker", ["build", "-t", imageName, "-f", path.join(DOCKERFILE_DIR, "Dockerfile.dev"), REPO_ROOT], {
stdio: ["ignore", "ignore", "pipe"],
});
} catch (err: unknown) {
@ -224,19 +213,13 @@ export async function startDockerSandbox(opts: DockerSandboxOptions): Promise<Do
image = await ensureExampleImage(docker);
}
const bootCommands = [
...setupCommands,
`sandbox-agent server --no-token --host 0.0.0.0 --port ${port}`,
];
const bootCommands = [...setupCommands, `sandbox-agent server --no-token --host 0.0.0.0 --port ${port}`];
const container = await docker.createContainer({
Image: image,
WorkingDir: "/root",
Cmd: ["sh", "-c", bootCommands.join(" && ")],
Env: [
...Object.entries(credentialEnv).map(([key, value]) => `${key}=${value}`),
...Object.entries(bootstrapEnv).map(([key, value]) => `${key}=${value}`),
],
Env: [...Object.entries(credentialEnv).map(([key, value]) => `${key}=${value}`), ...Object.entries(bootstrapEnv).map(([key, value]) => `${key}=${value}`)],
ExposedPorts: { [`${port}/tcp`]: {} },
HostConfig: {
AutoRemove: true,
@ -246,12 +229,12 @@ export async function startDockerSandbox(opts: DockerSandboxOptions): Promise<Do
await container.start();
const logChunks: string[] = [];
const startupLogs = await container.logs({
const startupLogs = (await container.logs({
follow: true,
stdout: true,
stderr: true,
since: 0,
}) as NodeJS.ReadableStream;
})) as NodeJS.ReadableStream;
const stdoutStream = new PassThrough();
const stderrStream = new PassThrough();
stdoutStream.on("data", (chunk) => {
@ -263,7 +246,9 @@ export async function startDockerSandbox(opts: DockerSandboxOptions): Promise<Do
docker.modem.demuxStream(startupLogs, stdoutStream, stderrStream);
const stopStartupLogs = () => {
const stream = startupLogs as NodeJS.ReadableStream & { destroy?: () => void };
try { stream.destroy?.(); } catch {}
try {
stream.destroy?.();
} catch {}
};
const inspect = await container.inspect();
@ -279,8 +264,12 @@ export async function startDockerSandbox(opts: DockerSandboxOptions): Promise<Do
const cleanup = async () => {
stopStartupLogs();
try { await container.stop({ t: 5 }); } catch {}
try { await container.remove({ force: true }); } catch {}
try {
await container.stop({ t: 5 });
} catch {}
try {
await container.remove({ force: true });
} catch {}
process.exit(0);
};
process.once("SIGINT", cleanup);

View file

@ -41,15 +41,7 @@ export function buildInspectorUrl({
return `${normalized}/ui/${sessionPath}${queryString ? `?${queryString}` : ""}`;
}
export function logInspectorUrl({
baseUrl,
token,
headers,
}: {
baseUrl: string;
token?: string;
headers?: Record<string, string>;
}): void {
export function logInspectorUrl({ baseUrl, token, headers }: { baseUrl: string; token?: string; headers?: Record<string, string> }): void {
console.log(`Inspector: ${buildInspectorUrl({ baseUrl, token, headers })}`);
}
@ -84,10 +76,7 @@ export function generateSessionId(): string {
export function detectAgent(): string {
if (process.env.SANDBOX_AGENT) return process.env.SANDBOX_AGENT;
const hasClaude = Boolean(
process.env.ANTHROPIC_API_KEY ||
process.env.CLAUDE_API_KEY ||
process.env.CLAUDE_CODE_OAUTH_TOKEN ||
process.env.ANTHROPIC_AUTH_TOKEN,
process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_API_KEY || process.env.CLAUDE_CODE_OAUTH_TOKEN || process.env.ANTHROPIC_AUTH_TOKEN,
);
const openAiLikeKey = process.env.OPENAI_API_KEY || process.env.CODEX_API_KEY || "";
const hasCodexApiKey = openAiLikeKey.startsWith("sk-");

View file

@ -23,25 +23,16 @@ console.log("Uploading script and skill file...");
const client = await SandboxAgent.connect({ baseUrl });
const script = await fs.promises.readFile(scriptFile);
const scriptResult = await client.writeFsFile(
{ path: "/opt/skills/random-number/random-number.cjs" },
script,
);
const scriptResult = await client.writeFsFile({ path: "/opt/skills/random-number/random-number.cjs" }, script);
console.log(` Script: ${scriptResult.path} (${scriptResult.bytesWritten} bytes)`);
const skillMd = await fs.promises.readFile(path.resolve(__dirname, "../SKILL.md"));
const skillResult = await client.writeFsFile(
{ path: "/opt/skills/random-number/SKILL.md" },
skillMd,
);
const skillResult = await client.writeFsFile({ path: "/opt/skills/random-number/SKILL.md" }, skillMd);
console.log(` Skill: ${skillResult.path} (${skillResult.bytesWritten} bytes)`);
// Configure the uploaded skill.
console.log("Configuring custom skill...");
await client.setSkillsConfig(
{ directory: "/", skillName: "random-number" },
{ sources: [{ type: "local", source: "/opt/skills/random-number" }] },
);
await client.setSkillsConfig({ directory: "/", skillName: "random-number" }, { sources: [{ type: "local", source: "/opt/skills/random-number" }] });
// Create a session.
console.log("Creating session with custom skill...");
@ -52,4 +43,7 @@ console.log(' Try: "generate a random number between 1 and 100"');
console.log(" Press Ctrl+C to stop.");
const keepAlive = setInterval(() => {}, 60_000);
process.on("SIGINT", () => { clearInterval(keepAlive); cleanup().then(() => process.exit(0)); });
process.on("SIGINT", () => {
clearInterval(keepAlive);
cleanup().then(() => process.exit(0));
});

View file

@ -22,4 +22,7 @@ console.log(' Try: "How do I start sandbox-agent?"');
console.log(" Press Ctrl+C to stop.");
const keepAlive = setInterval(() => {}, 60_000);
process.on("SIGINT", () => { clearInterval(keepAlive); cleanup().then(() => process.exit(0)); });
process.on("SIGINT", () => {
clearInterval(keepAlive);
cleanup().then(() => process.exit(0));
});

View file

@ -23,6 +23,6 @@ describe("vercel example", () => {
await cleanup();
}
},
timeoutMs
timeoutMs,
);
});

View file

@ -1,10 +1,7 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": [
"ES2022",
"DOM"
],
"lib": ["ES2022", "DOM"],
"module": "ESNext",
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
@ -14,11 +11,6 @@
"skipLibCheck": true,
"resolveJsonModule": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"**/*.test.ts"
]
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.test.ts"]
}