mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 08:03:46 +00:00
Add Modal and ComputeSDK built-in providers, update examples and docs
- Add `sandbox-agent/modal` provider using Modal SDK with node:22-slim image - Add `sandbox-agent/computesdk` provider using ComputeSDK's unified sandbox API - Update Modal and ComputeSDK examples to use new SDK providers - Update Modal and ComputeSDK deploy docs with provider-based examples - Add Modal to quickstart CodeGroup and docs.json navigation - Add provider test entries for Modal and ComputeSDK - Remove old standalone example files (modal.ts, computesdk.ts) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
441083ea2a
commit
20202c45ee
18 changed files with 377 additions and 646 deletions
|
|
@ -1,160 +1,61 @@
|
|||
---
|
||||
title: "ComputeSDK"
|
||||
description: "Deploy the daemon using ComputeSDK's provider-agnostic sandbox API."
|
||||
description: "Deploy Sandbox Agent using ComputeSDK's provider-agnostic sandbox API."
|
||||
---
|
||||
|
||||
[ComputeSDK](https://computesdk.com) provides a unified interface for managing sandboxes across multiple providers. Write once, deploy anywhere—switch providers by changing environment variables.
|
||||
[ComputeSDK](https://computesdk.com) provides a unified interface for managing sandboxes across multiple providers. Write once, deploy anywhere by changing environment variables.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `COMPUTESDK_API_KEY` from [console.computesdk.com](https://console.computesdk.com)
|
||||
- Provider API key (one of: `E2B_API_KEY`, `DAYTONA_API_KEY`, `VERCEL_TOKEN`, `MODAL_TOKEN_ID` + `MODAL_TOKEN_SECRET`, `BLAXEL_API_KEY`, `CSB_API_KEY`)
|
||||
- `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` for the coding agents
|
||||
- `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`
|
||||
|
||||
## TypeScript Example
|
||||
## TypeScript example
|
||||
|
||||
```bash
|
||||
npm install sandbox-agent@0.3.x computesdk
|
||||
```
|
||||
|
||||
```typescript
|
||||
import {
|
||||
compute,
|
||||
detectProvider,
|
||||
getMissingEnvVars,
|
||||
getProviderConfigFromEnv,
|
||||
isProviderAuthComplete,
|
||||
isValidProvider,
|
||||
PROVIDER_NAMES,
|
||||
type ExplicitComputeConfig,
|
||||
type ProviderName,
|
||||
} from "computesdk";
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { computesdk } from "sandbox-agent/computesdk";
|
||||
|
||||
const PORT = 3000;
|
||||
const REQUEST_TIMEOUT_MS =
|
||||
Number.parseInt(process.env.COMPUTESDK_TIMEOUT_MS || "", 10) || 120_000;
|
||||
|
||||
/**
|
||||
* Detects and validates the provider to use.
|
||||
* Priority: COMPUTESDK_PROVIDER env var > auto-detection from API keys
|
||||
*/
|
||||
function resolveProvider(): ProviderName {
|
||||
const providerOverride = process.env.COMPUTESDK_PROVIDER;
|
||||
|
||||
if (providerOverride) {
|
||||
if (!isValidProvider(providerOverride)) {
|
||||
throw new Error(
|
||||
`Unsupported provider "${providerOverride}". Supported: ${PROVIDER_NAMES.join(", ")}`
|
||||
);
|
||||
}
|
||||
if (!isProviderAuthComplete(providerOverride)) {
|
||||
const missing = getMissingEnvVars(providerOverride);
|
||||
throw new Error(
|
||||
`Missing credentials for "${providerOverride}". Set: ${missing.join(", ")}`
|
||||
);
|
||||
}
|
||||
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(" | ")}`
|
||||
);
|
||||
}
|
||||
return detected as ProviderName;
|
||||
}
|
||||
|
||||
function configureComputeSDK(): void {
|
||||
const provider = resolveProvider();
|
||||
|
||||
const config: ExplicitComputeConfig = {
|
||||
provider,
|
||||
computesdkApiKey: process.env.COMPUTESDK_API_KEY,
|
||||
requestTimeoutMs: REQUEST_TIMEOUT_MS,
|
||||
};
|
||||
|
||||
// Add provider-specific config from environment
|
||||
const providerConfig = getProviderConfigFromEnv(provider);
|
||||
if (Object.keys(providerConfig).length > 0) {
|
||||
(config as any)[provider] = providerConfig;
|
||||
}
|
||||
|
||||
compute.setConfig(config);
|
||||
}
|
||||
|
||||
configureComputeSDK();
|
||||
|
||||
// Build environment variables to pass to sandbox
|
||||
const envs: Record<string, string> = {};
|
||||
if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||
if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
|
||||
// Create sandbox
|
||||
const sandbox = await compute.sandbox.create({
|
||||
envs: Object.keys(envs).length > 0 ? envs : undefined,
|
||||
const sdk = await SandboxAgent.start({
|
||||
sandbox: computesdk({
|
||||
create: { envs },
|
||||
}),
|
||||
});
|
||||
|
||||
// Helper to run commands with error handling
|
||||
const run = async (cmd: string, options?: { background?: boolean }) => {
|
||||
const result = await sandbox.runCommand(cmd, options);
|
||||
if (typeof result?.exitCode === "number" && result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${cmd} (exit ${result.exitCode})\n${result.stderr || ""}`);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// Install sandbox-agent
|
||||
await run("curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh");
|
||||
|
||||
// Install agents conditionally based on available API keys
|
||||
if (envs.ANTHROPIC_API_KEY) {
|
||||
await run("sandbox-agent install-agent claude");
|
||||
try {
|
||||
const session = await sdk.createSession({ agent: "claude" });
|
||||
const response = await session.prompt([
|
||||
{ type: "text", text: "Summarize this repository" },
|
||||
]);
|
||||
console.log(response.stopReason);
|
||||
} finally {
|
||||
await sdk.destroySandbox();
|
||||
}
|
||||
if (envs.OPENAI_API_KEY) {
|
||||
await run("sandbox-agent install-agent codex");
|
||||
}
|
||||
|
||||
// Start the server in the background
|
||||
await run(`sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`, { background: true });
|
||||
|
||||
// Get the public URL for the sandbox
|
||||
const baseUrl = await sandbox.getUrl({ port: PORT });
|
||||
|
||||
// Wait for server to be ready
|
||||
const deadline = Date.now() + REQUEST_TIMEOUT_MS;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/v1/health`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data?.status === "ok") break;
|
||||
}
|
||||
} catch {
|
||||
// Server not ready yet
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
}
|
||||
|
||||
// Connect to the server
|
||||
const client = await SandboxAgent.connect({ baseUrl });
|
||||
|
||||
// Detect which agent to use based on available API keys
|
||||
const agent = envs.ANTHROPIC_API_KEY ? "claude" : "codex";
|
||||
|
||||
// Create a session and start coding
|
||||
await client.createSession("my-session", { agent });
|
||||
|
||||
await client.postMessage("my-session", {
|
||||
message: "Summarize this repository",
|
||||
});
|
||||
|
||||
for await (const event of client.streamEvents("my-session")) {
|
||||
console.log(event.type, event.data);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
await sandbox.destroy();
|
||||
```
|
||||
|
||||
## Supported Providers
|
||||
The `computesdk` provider handles sandbox creation, Sandbox Agent installation, agent setup, and server startup automatically. ComputeSDK routes to your configured provider behind the scenes.
|
||||
|
||||
Before calling `SandboxAgent.start()`, configure ComputeSDK with your provider:
|
||||
|
||||
```typescript
|
||||
import { compute } from "computesdk";
|
||||
|
||||
compute.setConfig({
|
||||
provider: "e2b", // or auto-detect via detectProvider()
|
||||
computesdkApiKey: process.env.COMPUTESDK_API_KEY,
|
||||
});
|
||||
```
|
||||
|
||||
## Supported providers
|
||||
|
||||
ComputeSDK auto-detects your provider from environment variables:
|
||||
|
||||
|
|
@ -169,46 +70,7 @@ ComputeSDK auto-detects your provider from environment variables:
|
|||
|
||||
## Notes
|
||||
|
||||
- **Provider resolution order**: `COMPUTESDK_PROVIDER` env var takes priority, otherwise auto-detection from API keys.
|
||||
- **Conditional agent installation**: Only agents with available API keys are installed, reducing setup time.
|
||||
- **Command error handling**: The example validates exit codes and throws on failures for easier debugging.
|
||||
- **Provider resolution**: Set `COMPUTESDK_PROVIDER` to force a specific provider, or let ComputeSDK auto-detect from API keys.
|
||||
- `sandbox.runCommand(..., { background: true })` keeps the server running while your app continues.
|
||||
- `sandbox.getUrl({ port })` returns a public URL for the sandbox port.
|
||||
- Always destroy the sandbox when you are done to avoid leaking resources.
|
||||
- If sandbox creation times out, set `COMPUTESDK_TIMEOUT_MS` to a higher value (default: 120000ms).
|
||||
|
||||
## Explicit Provider Selection
|
||||
|
||||
To force a specific provider instead of auto-detection, set the `COMPUTESDK_PROVIDER` environment variable:
|
||||
|
||||
```bash
|
||||
export COMPUTESDK_PROVIDER=e2b
|
||||
```
|
||||
|
||||
Or configure programmatically using `getProviderConfigFromEnv()`:
|
||||
|
||||
```typescript
|
||||
import { compute, getProviderConfigFromEnv, type ExplicitComputeConfig } from "computesdk";
|
||||
|
||||
const config: ExplicitComputeConfig = {
|
||||
provider: "e2b",
|
||||
computesdkApiKey: process.env.COMPUTESDK_API_KEY,
|
||||
requestTimeoutMs: 120_000,
|
||||
};
|
||||
|
||||
// Automatically populate provider-specific config from environment
|
||||
const providerConfig = getProviderConfigFromEnv("e2b");
|
||||
if (Object.keys(providerConfig).length > 0) {
|
||||
(config as any).e2b = providerConfig;
|
||||
}
|
||||
|
||||
compute.setConfig(config);
|
||||
```
|
||||
|
||||
## Direct Mode (No ComputeSDK API Key)
|
||||
|
||||
To bypass the ComputeSDK gateway and use provider SDKs directly, see the provider-specific examples:
|
||||
|
||||
- [E2B](/deploy/e2b)
|
||||
- [Daytona](/deploy/daytona)
|
||||
- [Vercel](/deploy/vercel)
|
||||
- Always destroy the sandbox when done to avoid leaking resources.
|
||||
|
|
|
|||
|
|
@ -10,88 +10,43 @@ description: "Deploy Sandbox Agent inside a Modal sandbox."
|
|||
|
||||
## TypeScript example
|
||||
|
||||
```typescript
|
||||
import { ModalClient } from "modal";
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
|
||||
const modal = new ModalClient();
|
||||
const app = await modal.apps.fromName("sandbox-agent", { createIfMissing: true });
|
||||
|
||||
const image = modal.images
|
||||
.fromRegistry("ubuntu:22.04")
|
||||
.dockerfileCommands([
|
||||
"RUN apt-get update && apt-get install -y curl ca-certificates",
|
||||
"RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh",
|
||||
]);
|
||||
|
||||
const envs: Record<string, string> = {};
|
||||
if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||
if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
|
||||
const secrets = Object.keys(envs).length > 0
|
||||
? [await modal.secrets.fromObject(envs)]
|
||||
: [];
|
||||
|
||||
const sb = await modal.sandboxes.create(app, image, {
|
||||
encryptedPorts: [3000],
|
||||
secrets,
|
||||
});
|
||||
|
||||
const exec = async (cmd: string) => {
|
||||
const p = await sb.exec(["bash", "-c", cmd], { stdout: "pipe", stderr: "pipe" });
|
||||
const exitCode = await p.wait();
|
||||
if (exitCode !== 0) {
|
||||
const stderr = await p.stderr.readText();
|
||||
throw new Error(`Command failed (exit ${exitCode}): ${cmd}\n${stderr}`);
|
||||
}
|
||||
};
|
||||
|
||||
await exec("sandbox-agent install-agent claude");
|
||||
await exec("sandbox-agent install-agent codex");
|
||||
|
||||
await sb.exec(
|
||||
["bash", "-c", "sandbox-agent server --no-token --host 0.0.0.0 --port 3000 &"],
|
||||
);
|
||||
|
||||
const tunnels = await sb.tunnels();
|
||||
const baseUrl = tunnels[3000].url;
|
||||
|
||||
const sdk = await SandboxAgent.connect({ baseUrl });
|
||||
|
||||
const session = await sdk.createSession({ agent: "claude" });
|
||||
const off = session.onEvent((event) => {
|
||||
console.log(event.sender, event.payload);
|
||||
});
|
||||
|
||||
await session.prompt([{ type: "text", text: "Summarize this repository" }]);
|
||||
off();
|
||||
|
||||
await sb.terminate();
|
||||
```bash
|
||||
npm install sandbox-agent@0.3.x modal
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { modal } from "sandbox-agent/modal";
|
||||
|
||||
const secrets: Record<string, string> = {};
|
||||
if (process.env.ANTHROPIC_API_KEY) secrets.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||
if (process.env.OPENAI_API_KEY) secrets.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
|
||||
const sdk = await SandboxAgent.start({
|
||||
sandbox: modal({
|
||||
create: { secrets },
|
||||
}),
|
||||
});
|
||||
|
||||
try {
|
||||
const session = await sdk.createSession({ agent: "claude" });
|
||||
const response = await session.prompt([
|
||||
{ type: "text", text: "Summarize this repository" },
|
||||
]);
|
||||
console.log(response.stopReason);
|
||||
} finally {
|
||||
await sdk.destroySandbox();
|
||||
}
|
||||
```
|
||||
|
||||
The `modal` provider handles app creation, image building, sandbox provisioning, agent installation, server startup, and tunnel networking automatically.
|
||||
|
||||
## Faster cold starts
|
||||
|
||||
Modal caches image layers, so the `dockerfileCommands` that install `curl` and `sandbox-agent` only run on the first build. Subsequent sandbox creates reuse the cached image.
|
||||
|
||||
## Running the test
|
||||
|
||||
The example includes a health-check test. First, build the SDK:
|
||||
|
||||
```bash
|
||||
pnpm --filter sandbox-agent build
|
||||
```
|
||||
|
||||
Then run the test with your Modal credentials:
|
||||
|
||||
```bash
|
||||
MODAL_TOKEN_ID=<your-token-id> MODAL_TOKEN_SECRET=<your-token-secret> npx vitest run
|
||||
```
|
||||
|
||||
Run from `examples/modal/`. The test will skip if credentials are not set.
|
||||
Modal caches image layers, so the Dockerfile commands that install `curl` and `sandbox-agent` only run on the first build. Subsequent sandbox creates reuse the cached image.
|
||||
|
||||
## Notes
|
||||
|
||||
- Modal sandboxes use [gVisor](https://gvisor.dev/) for strong isolation.
|
||||
- Ports are exposed via encrypted tunnels (`encryptedPorts`). Use `sb.tunnels()` to get the public HTTPS URL.
|
||||
- Environment variables (API keys) are passed as Modal [Secrets](https://modal.com/docs/guide/secrets) rather than plain env vars for security.
|
||||
- Always call `sb.terminate()` when done to avoid leaking sandbox resources.
|
||||
- Ports are exposed via encrypted tunnels (`encryptedPorts`). The provider uses `sb.tunnels()` to get the public HTTPS URL.
|
||||
- Environment variables (API keys) are passed as Modal [Secrets](https://modal.com/docs/guide/secrets) for security.
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@
|
|||
"deploy/vercel",
|
||||
"deploy/cloudflare",
|
||||
"deploy/docker",
|
||||
"deploy/modal",
|
||||
"deploy/boxlite",
|
||||
"deploy/computesdk"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -86,6 +86,23 @@ icon: "rocket"
|
|||
});
|
||||
```
|
||||
|
||||
```typescript Modal
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { modal } from "sandbox-agent/modal";
|
||||
|
||||
const sdk = await SandboxAgent.start({
|
||||
sandbox: modal({
|
||||
create: {
|
||||
// Pass whichever keys your agent needs
|
||||
secrets: {
|
||||
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
|
||||
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
```typescript Cloudflare
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { cloudflare } from "sandbox-agent/cloudflare";
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "tsx src/computesdk.ts",
|
||||
"start": "tsx src/index.ts",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -1,151 +0,0 @@
|
|||
import {
|
||||
compute,
|
||||
detectProvider,
|
||||
getMissingEnvVars,
|
||||
getProviderConfigFromEnv,
|
||||
isProviderAuthComplete,
|
||||
isValidProvider,
|
||||
PROVIDER_NAMES,
|
||||
type ExplicitComputeConfig,
|
||||
type ProviderName,
|
||||
} from "computesdk";
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { detectAgent, buildInspectorUrl } from "@sandbox-agent/example-shared";
|
||||
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;
|
||||
|
||||
/**
|
||||
* Detects and validates the provider to use.
|
||||
* Priority: COMPUTESDK_PROVIDER env var > auto-detection from API keys
|
||||
*/
|
||||
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(", ")}`);
|
||||
}
|
||||
if (!isProviderAuthComplete(providerOverride)) {
|
||||
const missing = getMissingEnvVars(providerOverride);
|
||||
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(" | ")}`);
|
||||
}
|
||||
console.log(`Using ComputeSDK provider: ${detected} (auto-detected)`);
|
||||
return detected as 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>>;
|
||||
configWithProvider[provider] = providerConfig;
|
||||
}
|
||||
|
||||
compute.setConfig(config);
|
||||
}
|
||||
|
||||
configureComputeSDK();
|
||||
|
||||
const buildEnv = (): Record<string, string> => {
|
||||
const env: Record<string, string> = {};
|
||||
if (process.env.ANTHROPIC_API_KEY) env.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||
if (process.env.OPENAI_API_KEY) env.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
return env;
|
||||
};
|
||||
|
||||
export async function setupComputeSdkSandboxAgent(): Promise<{
|
||||
baseUrl: string;
|
||||
cleanup: () => Promise<void>;
|
||||
}> {
|
||||
const env = buildEnv();
|
||||
|
||||
console.log("Creating ComputeSDK sandbox...");
|
||||
const sandbox = await compute.sandbox.create({
|
||||
envs: Object.keys(env).length > 0 ? env : undefined,
|
||||
});
|
||||
|
||||
const run = async (cmd: string, options?: { background?: boolean }) => {
|
||||
const result = await sandbox.runCommand(cmd, options);
|
||||
if (typeof result?.exitCode === "number" && result.exitCode !== 0) {
|
||||
throw new Error(`Command failed: ${cmd} (exit ${result.exitCode})\n${result.stderr || ""}`);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
console.log("Installing sandbox-agent...");
|
||||
await run("curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/install.sh | sh");
|
||||
|
||||
if (env.ANTHROPIC_API_KEY) {
|
||||
console.log("Installing Claude agent...");
|
||||
await run("sandbox-agent install-agent claude");
|
||||
}
|
||||
|
||||
if (env.OPENAI_API_KEY) {
|
||||
console.log("Installing Codex agent...");
|
||||
await run("sandbox-agent install-agent codex");
|
||||
}
|
||||
|
||||
console.log("Starting server...");
|
||||
await run(`sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT}`, { background: true });
|
||||
|
||||
const baseUrl = await sandbox.getUrl({ port: PORT });
|
||||
|
||||
const cleanup = async () => {
|
||||
try {
|
||||
await sandbox.destroy();
|
||||
} catch (error) {
|
||||
console.warn("Cleanup failed:", error instanceof Error ? error.message : error);
|
||||
}
|
||||
};
|
||||
|
||||
return { baseUrl, cleanup };
|
||||
}
|
||||
|
||||
export async function runComputeSdkExample(): Promise<void> {
|
||||
const { baseUrl, cleanup } = await setupComputeSdkSandboxAgent();
|
||||
|
||||
const handleExit = async () => {
|
||||
await cleanup();
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
process.once("SIGINT", handleExit);
|
||||
process.once("SIGTERM", handleExit);
|
||||
|
||||
const client = await SandboxAgent.connect({ baseUrl });
|
||||
const session = await client.createSession({ agent: detectAgent(), cwd: "/home" });
|
||||
const sessionId = session.id;
|
||||
|
||||
console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
|
||||
console.log(" Press Ctrl+C to stop.");
|
||||
|
||||
// Keep alive until SIGINT/SIGTERM triggers cleanup above
|
||||
await new Promise(() => {});
|
||||
}
|
||||
|
||||
const isDirectRun = Boolean(process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url));
|
||||
|
||||
if (isDirectRun) {
|
||||
runComputeSdkExample().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
30
examples/computesdk/src/index.ts
Normal file
30
examples/computesdk/src/index.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { computesdk } from "sandbox-agent/computesdk";
|
||||
import { detectAgent } 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;
|
||||
if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
|
||||
const client = await SandboxAgent.start({
|
||||
sandbox: computesdk({
|
||||
create: { envs },
|
||||
}),
|
||||
});
|
||||
|
||||
console.log(`UI: ${client.inspectorUrl}`);
|
||||
|
||||
const session = await client.createSession({
|
||||
agent: detectAgent(),
|
||||
});
|
||||
|
||||
session.onEvent((event) => {
|
||||
console.log(`[${event.sender}]`, JSON.stringify(event.payload));
|
||||
});
|
||||
|
||||
session.prompt([{ type: "text", text: "Say hello from ComputeSDK in one sentence." }]);
|
||||
|
||||
process.once("SIGINT", async () => {
|
||||
await client.destroySandbox();
|
||||
process.exit(0);
|
||||
});
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { buildHeaders } from "@sandbox-agent/example-shared";
|
||||
import { setupComputeSdkSandboxAgent } from "../src/computesdk.ts";
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { computesdk } from "sandbox-agent/computesdk";
|
||||
|
||||
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);
|
||||
|
|
@ -13,20 +13,23 @@ const timeoutMs = Number.parseInt(process.env.SANDBOX_TEST_TIMEOUT_MS || "", 10)
|
|||
|
||||
const testFn = shouldRun ? it : it.skip;
|
||||
|
||||
describe("computesdk example", () => {
|
||||
describe("computesdk provider", () => {
|
||||
testFn(
|
||||
"starts sandbox-agent and responds to /v1/health",
|
||||
async () => {
|
||||
const { baseUrl, cleanup } = await setupComputeSdkSandboxAgent();
|
||||
const envs: Record<string, string> = {};
|
||||
if (process.env.ANTHROPIC_API_KEY) envs.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||
if (process.env.OPENAI_API_KEY) envs.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
|
||||
const sdk = await SandboxAgent.start({
|
||||
sandbox: computesdk({ create: { envs } }),
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/v1/health`, {
|
||||
headers: buildHeaders({}),
|
||||
});
|
||||
expect(response.ok).toBe(true);
|
||||
const data = await response.json();
|
||||
expect(data.status).toBe("ok");
|
||||
const health = await sdk.getHealth();
|
||||
expect(health.status).toBe("ok");
|
||||
} finally {
|
||||
await cleanup();
|
||||
await sdk.destroySandbox();
|
||||
}
|
||||
},
|
||||
timeoutMs,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "tsx src/modal.ts",
|
||||
"start": "tsx src/index.ts",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
|||
30
examples/modal/src/index.ts
Normal file
30
examples/modal/src/index.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { modal } from "sandbox-agent/modal";
|
||||
import { detectAgent } from "@sandbox-agent/example-shared";
|
||||
|
||||
const secrets: Record<string, string> = {};
|
||||
if (process.env.ANTHROPIC_API_KEY) secrets.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||
if (process.env.OPENAI_API_KEY) secrets.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
|
||||
const client = await SandboxAgent.start({
|
||||
sandbox: modal({
|
||||
create: { secrets },
|
||||
}),
|
||||
});
|
||||
|
||||
console.log(`UI: ${client.inspectorUrl}`);
|
||||
|
||||
const session = await client.createSession({
|
||||
agent: detectAgent(),
|
||||
});
|
||||
|
||||
session.onEvent((event) => {
|
||||
console.log(`[${event.sender}]`, JSON.stringify(event.payload));
|
||||
});
|
||||
|
||||
session.prompt([{ type: "text", text: "Say hello from Modal in one sentence." }]);
|
||||
|
||||
process.once("SIGINT", async () => {
|
||||
await client.destroySandbox();
|
||||
process.exit(0);
|
||||
});
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
import { ModalClient } from "modal";
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { detectAgent, buildInspectorUrl, waitForHealth } from "@sandbox-agent/example-shared";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolve } from "node:path";
|
||||
import { run } from "node:test";
|
||||
|
||||
const PORT = 3000;
|
||||
const APP_NAME = "sandbox-agent";
|
||||
|
||||
async function buildSecrets(modal: ModalClient) {
|
||||
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 (Object.keys(envVars).length === 0) return [];
|
||||
return [await modal.secrets.fromObject(envVars)];
|
||||
}
|
||||
|
||||
export async function setupModalSandboxAgent(): Promise<{
|
||||
baseUrl: string;
|
||||
cleanup: () => Promise<void>;
|
||||
}> {
|
||||
const modal = new ModalClient();
|
||||
const app = await modal.apps.fromName(APP_NAME, { createIfMissing: true });
|
||||
|
||||
const image = modal.images
|
||||
.fromRegistry("ubuntu:22.04")
|
||||
.dockerfileCommands([
|
||||
"RUN apt-get update && apt-get install -y curl ca-certificates",
|
||||
"RUN curl -fsSL https://releases.rivet.dev/sandbox-agent/0.2.x/install.sh | sh",
|
||||
]);
|
||||
|
||||
const secrets = await buildSecrets(modal);
|
||||
|
||||
console.log("Creating Modal sandbox!");
|
||||
const sb = await modal.sandboxes.create(app, image, {
|
||||
secrets: secrets,
|
||||
encryptedPorts: [PORT],
|
||||
});
|
||||
console.log(`Sandbox created: ${sb.sandboxId}`);
|
||||
|
||||
const exec = async (cmd: string) => {
|
||||
const p = await sb.exec(["bash", "-c", cmd], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
const exitCode = await p.wait();
|
||||
if (exitCode !== 0) {
|
||||
const stderr = await p.stderr.readText();
|
||||
throw new Error(`Command failed (exit ${exitCode}): ${cmd}\n${stderr}`);
|
||||
}
|
||||
};
|
||||
|
||||
if (process.env.ANTHROPIC_API_KEY) {
|
||||
console.log("Installing Claude agent...");
|
||||
await exec("sandbox-agent install-agent claude");
|
||||
}
|
||||
if (process.env.OPENAI_API_KEY) {
|
||||
console.log("Installing Codex agent...");
|
||||
await exec("sandbox-agent install-agent codex");
|
||||
}
|
||||
|
||||
console.log("Starting server...");
|
||||
|
||||
await sb.exec(
|
||||
["bash", "-c", `sandbox-agent server --no-token --host 0.0.0.0 --port ${PORT} &`],
|
||||
);
|
||||
|
||||
const tunnels = await sb.tunnels();
|
||||
const tunnel = tunnels[PORT];
|
||||
if (!tunnel) {
|
||||
throw new Error(`No tunnel found for port ${PORT}`);
|
||||
}
|
||||
const baseUrl = tunnel.url;
|
||||
|
||||
console.log("Waiting for server...");
|
||||
await waitForHealth({ baseUrl });
|
||||
|
||||
const cleanup = async () => {
|
||||
try {
|
||||
await sb.terminate();
|
||||
} catch (error) {
|
||||
console.warn("Cleanup failed:", error instanceof Error ? error.message : error);
|
||||
}
|
||||
};
|
||||
|
||||
return { baseUrl, cleanup };
|
||||
}
|
||||
|
||||
export async function runModalExample(): Promise<void> {
|
||||
const { baseUrl, cleanup } = await setupModalSandboxAgent();
|
||||
|
||||
const handleExit = async () => {
|
||||
await cleanup();
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
process.once("SIGINT", handleExit);
|
||||
process.once("SIGTERM", handleExit);
|
||||
|
||||
const client = await SandboxAgent.connect({ baseUrl });
|
||||
const session = await client.createSession({ agent: detectAgent(), sessionInit: { cwd: "/root", mcpServers: [] } });
|
||||
const sessionId = session.id;
|
||||
|
||||
console.log(` UI: ${buildInspectorUrl({ baseUrl, sessionId })}`);
|
||||
console.log(" Press Ctrl+C to stop.");
|
||||
|
||||
await new Promise(() => {});
|
||||
}
|
||||
|
||||
const isDirectRun = Boolean(
|
||||
process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url),
|
||||
);
|
||||
|
||||
if (isDirectRun) {
|
||||
runModalExample().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,26 +1,29 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { buildHeaders } from "@sandbox-agent/example-shared";
|
||||
import { setupModalSandboxAgent } from "../src/modal.ts";
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { modal } from "sandbox-agent/modal";
|
||||
|
||||
const shouldRun = Boolean(process.env.MODAL_TOKEN_ID && process.env.MODAL_TOKEN_SECRET);
|
||||
const timeoutMs = Number.parseInt(process.env.SANDBOX_TEST_TIMEOUT_MS || "", 10) || 300_000;
|
||||
|
||||
const testFn = shouldRun ? it : it.skip;
|
||||
|
||||
describe("modal example", () => {
|
||||
describe("modal provider", () => {
|
||||
testFn(
|
||||
"starts sandbox-agent and responds to /v1/health",
|
||||
async () => {
|
||||
const { baseUrl, cleanup } = await setupModalSandboxAgent();
|
||||
const secrets: Record<string, string> = {};
|
||||
if (process.env.ANTHROPIC_API_KEY) secrets.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
||||
if (process.env.OPENAI_API_KEY) secrets.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
|
||||
const sdk = await SandboxAgent.start({
|
||||
sandbox: modal({ create: { secrets } }),
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/v1/health`, {
|
||||
headers: buildHeaders({}),
|
||||
});
|
||||
expect(response.ok).toBe(true);
|
||||
const data = await response.json();
|
||||
expect(data.status).toBe("ok");
|
||||
const health = await sdk.getHealth();
|
||||
expect(health.status).toBe("ok");
|
||||
} finally {
|
||||
await cleanup();
|
||||
await sdk.destroySandbox();
|
||||
}
|
||||
},
|
||||
timeoutMs,
|
||||
|
|
|
|||
106
pnpm-lock.yaml
generated
106
pnpm-lock.yaml
generated
|
|
@ -345,9 +345,6 @@ importers:
|
|||
'@sandbox-agent/example-shared':
|
||||
specifier: workspace:*
|
||||
version: link:../shared
|
||||
'@sandbox-agent/persist-postgres':
|
||||
specifier: workspace:*
|
||||
version: link:../../sdks/persist-postgres
|
||||
pg:
|
||||
specifier: latest
|
||||
version: 8.20.0
|
||||
|
|
@ -373,13 +370,16 @@ importers:
|
|||
'@sandbox-agent/example-shared':
|
||||
specifier: workspace:*
|
||||
version: link:../shared
|
||||
'@sandbox-agent/persist-sqlite':
|
||||
specifier: workspace:*
|
||||
version: link:../../sdks/persist-sqlite
|
||||
better-sqlite3:
|
||||
specifier: ^11.0.0
|
||||
version: 11.10.0
|
||||
sandbox-agent:
|
||||
specifier: workspace:*
|
||||
version: link:../../sdks/typescript
|
||||
devDependencies:
|
||||
'@types/better-sqlite3':
|
||||
specifier: ^7.0.0
|
||||
version: 7.6.13
|
||||
'@types/node':
|
||||
specifier: latest
|
||||
version: 25.5.0
|
||||
|
|
@ -640,9 +640,6 @@ importers:
|
|||
|
||||
frontend/packages/inspector:
|
||||
dependencies:
|
||||
'@sandbox-agent/persist-indexeddb':
|
||||
specifier: workspace:*
|
||||
version: link:../../../sdks/persist-indexeddb
|
||||
lucide-react:
|
||||
specifier: ^0.469.0
|
||||
version: 0.469.0(react@18.3.1)
|
||||
|
|
@ -897,57 +894,30 @@ importers:
|
|||
sdks/gigacode/platforms/win32-x64: {}
|
||||
|
||||
sdks/persist-indexeddb:
|
||||
dependencies:
|
||||
sandbox-agent:
|
||||
specifier: workspace:*
|
||||
version: link:../typescript
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22.0.0
|
||||
version: 22.19.7
|
||||
fake-indexeddb:
|
||||
specifier: ^6.2.4
|
||||
version: 6.2.5
|
||||
tsup:
|
||||
specifier: ^8.0.0
|
||||
version: 8.5.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
|
||||
typescript:
|
||||
specifier: ^5.7.0
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^3.0.0
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
sdks/persist-postgres:
|
||||
dependencies:
|
||||
pg:
|
||||
specifier: ^8.16.3
|
||||
version: 8.18.0
|
||||
sandbox-agent:
|
||||
specifier: workspace:*
|
||||
version: link:../typescript
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22.0.0
|
||||
version: 22.19.7
|
||||
'@types/pg':
|
||||
specifier: ^8.15.6
|
||||
version: 8.16.0
|
||||
tsup:
|
||||
specifier: ^8.0.0
|
||||
version: 8.5.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
|
||||
typescript:
|
||||
specifier: ^5.7.0
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^3.0.0
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
sdks/persist-rivet:
|
||||
dependencies:
|
||||
sandbox-agent:
|
||||
specifier: workspace:*
|
||||
version: link:../typescript
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22.0.0
|
||||
|
|
@ -958,22 +928,9 @@ importers:
|
|||
typescript:
|
||||
specifier: ^5.7.0
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^3.0.0
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
sdks/persist-sqlite:
|
||||
dependencies:
|
||||
better-sqlite3:
|
||||
specifier: ^11.0.0
|
||||
version: 11.10.0
|
||||
sandbox-agent:
|
||||
specifier: workspace:*
|
||||
version: link:../typescript
|
||||
devDependencies:
|
||||
'@types/better-sqlite3':
|
||||
specifier: ^7.0.0
|
||||
version: 7.6.13
|
||||
'@types/node':
|
||||
specifier: ^22.0.0
|
||||
version: 22.19.7
|
||||
|
|
@ -983,9 +940,6 @@ importers:
|
|||
typescript:
|
||||
specifier: ^5.7.0
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^3.0.0
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@1.21.7)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
sdks/react:
|
||||
dependencies:
|
||||
|
|
@ -1046,12 +1000,18 @@ importers:
|
|||
'@vercel/sandbox':
|
||||
specifier: '>=0.1.0'
|
||||
version: 1.8.1
|
||||
computesdk:
|
||||
specifier: '>=0.1.0'
|
||||
version: 2.5.0
|
||||
dockerode:
|
||||
specifier: '>=4.0.0'
|
||||
version: 4.0.9
|
||||
get-port:
|
||||
specifier: '>=7.0.0'
|
||||
version: 7.1.0
|
||||
modal:
|
||||
specifier: '>=0.1.0'
|
||||
version: 0.7.3
|
||||
openapi-typescript:
|
||||
specifier: ^6.7.0
|
||||
version: 6.7.6
|
||||
|
|
@ -3628,9 +3588,6 @@ packages:
|
|||
'@types/node@25.5.0':
|
||||
resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==}
|
||||
|
||||
'@types/pg@8.16.0':
|
||||
resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==}
|
||||
|
||||
'@types/pg@8.18.0':
|
||||
resolution: {integrity: sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==}
|
||||
|
||||
|
|
@ -5844,9 +5801,6 @@ packages:
|
|||
pg-cloudflare@1.3.0:
|
||||
resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==}
|
||||
|
||||
pg-connection-string@2.11.0:
|
||||
resolution: {integrity: sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==}
|
||||
|
||||
pg-connection-string@2.12.0:
|
||||
resolution: {integrity: sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==}
|
||||
|
||||
|
|
@ -5854,11 +5808,6 @@ packages:
|
|||
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
|
||||
pg-pool@3.11.0:
|
||||
resolution: {integrity: sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==}
|
||||
peerDependencies:
|
||||
pg: '>=8.0'
|
||||
|
||||
pg-pool@3.13.0:
|
||||
resolution: {integrity: sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==}
|
||||
peerDependencies:
|
||||
|
|
@ -5874,15 +5823,6 @@ packages:
|
|||
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
pg@8.18.0:
|
||||
resolution: {integrity: sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==}
|
||||
engines: {node: '>= 16.0.0'}
|
||||
peerDependencies:
|
||||
pg-native: '>=3.0.1'
|
||||
peerDependenciesMeta:
|
||||
pg-native:
|
||||
optional: true
|
||||
|
||||
pg@8.20.0:
|
||||
resolution: {integrity: sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==}
|
||||
engines: {node: '>= 16.0.0'}
|
||||
|
|
@ -10211,12 +10151,6 @@ snapshots:
|
|||
dependencies:
|
||||
undici-types: 7.18.2
|
||||
|
||||
'@types/pg@8.16.0':
|
||||
dependencies:
|
||||
'@types/node': 24.10.9
|
||||
pg-protocol: 1.11.0
|
||||
pg-types: 2.2.0
|
||||
|
||||
'@types/pg@8.18.0':
|
||||
dependencies:
|
||||
'@types/node': 24.10.9
|
||||
|
|
@ -12804,16 +12738,10 @@ snapshots:
|
|||
pg-cloudflare@1.3.0:
|
||||
optional: true
|
||||
|
||||
pg-connection-string@2.11.0: {}
|
||||
|
||||
pg-connection-string@2.12.0: {}
|
||||
|
||||
pg-int8@1.0.1: {}
|
||||
|
||||
pg-pool@3.11.0(pg@8.18.0):
|
||||
dependencies:
|
||||
pg: 8.18.0
|
||||
|
||||
pg-pool@3.13.0(pg@8.20.0):
|
||||
dependencies:
|
||||
pg: 8.20.0
|
||||
|
|
@ -12830,16 +12758,6 @@ snapshots:
|
|||
postgres-date: 1.0.7
|
||||
postgres-interval: 1.2.0
|
||||
|
||||
pg@8.18.0:
|
||||
dependencies:
|
||||
pg-connection-string: 2.11.0
|
||||
pg-pool: 3.11.0(pg@8.18.0)
|
||||
pg-protocol: 1.11.0
|
||||
pg-types: 2.2.0
|
||||
pgpass: 1.0.5
|
||||
optionalDependencies:
|
||||
pg-cloudflare: 1.3.0
|
||||
|
||||
pg@8.20.0:
|
||||
dependencies:
|
||||
pg-connection-string: 2.12.0
|
||||
|
|
|
|||
|
|
@ -38,6 +38,14 @@
|
|||
"./cloudflare": {
|
||||
"types": "./dist/providers/cloudflare.d.ts",
|
||||
"import": "./dist/providers/cloudflare.js"
|
||||
},
|
||||
"./modal": {
|
||||
"types": "./dist/providers/modal.d.ts",
|
||||
"import": "./dist/providers/modal.js"
|
||||
},
|
||||
"./computesdk": {
|
||||
"types": "./dist/providers/computesdk.d.ts",
|
||||
"import": "./dist/providers/computesdk.js"
|
||||
}
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
|
@ -46,7 +54,9 @@
|
|||
"@e2b/code-interpreter": ">=1.0.0",
|
||||
"@vercel/sandbox": ">=0.1.0",
|
||||
"dockerode": ">=4.0.0",
|
||||
"get-port": ">=7.0.0"
|
||||
"get-port": ">=7.0.0",
|
||||
"modal": ">=0.1.0",
|
||||
"computesdk": ">=0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@cloudflare/sandbox": {
|
||||
|
|
@ -66,6 +76,12 @@
|
|||
},
|
||||
"get-port": {
|
||||
"optional": true
|
||||
},
|
||||
"modal": {
|
||||
"optional": true
|
||||
},
|
||||
"computesdk": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
@ -94,6 +110,8 @@
|
|||
"@vercel/sandbox": ">=0.1.0",
|
||||
"dockerode": ">=4.0.0",
|
||||
"get-port": ">=7.0.0",
|
||||
"modal": ">=0.1.0",
|
||||
"computesdk": ">=0.1.0",
|
||||
"openapi-typescript": "^6.7.0",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.7.0",
|
||||
|
|
|
|||
53
sdks/typescript/src/providers/computesdk.ts
Normal file
53
sdks/typescript/src/providers/computesdk.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { compute } from "computesdk";
|
||||
import type { SandboxProvider } from "./types.ts";
|
||||
import { DEFAULT_AGENTS, SANDBOX_AGENT_INSTALL_SCRIPT } from "./shared.ts";
|
||||
|
||||
const DEFAULT_AGENT_PORT = 3000;
|
||||
|
||||
export interface ComputeSdkProviderOptions {
|
||||
create?: {
|
||||
envs?: Record<string, string>;
|
||||
};
|
||||
agentPort?: number;
|
||||
}
|
||||
|
||||
export function computesdk(options: ComputeSdkProviderOptions = {}): SandboxProvider {
|
||||
const agentPort = options.agentPort ?? DEFAULT_AGENT_PORT;
|
||||
|
||||
return {
|
||||
name: "computesdk",
|
||||
async create(): Promise<string> {
|
||||
const envs = options.create?.envs;
|
||||
const sandbox = await compute.sandbox.create({
|
||||
envs: envs && Object.keys(envs).length > 0 ? envs : undefined,
|
||||
});
|
||||
|
||||
const run = async (cmd: string, runOptions?: { background?: boolean }) => {
|
||||
const result = await sandbox.runCommand(cmd, runOptions);
|
||||
if (typeof result?.exitCode === "number" && result.exitCode !== 0) {
|
||||
throw new Error(`computesdk command failed: ${cmd} (exit ${result.exitCode})\n${result.stderr || ""}`);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
await run(`curl -fsSL ${SANDBOX_AGENT_INSTALL_SCRIPT} | sh`);
|
||||
for (const agent of DEFAULT_AGENTS) {
|
||||
await run(`sandbox-agent install-agent ${agent}`);
|
||||
}
|
||||
await run(`sandbox-agent server --no-token --host 0.0.0.0 --port ${agentPort}`, {
|
||||
background: true,
|
||||
});
|
||||
|
||||
return sandbox.sandboxId;
|
||||
},
|
||||
async destroy(sandboxId: string): Promise<void> {
|
||||
const sandbox = await compute.sandbox.getById(sandboxId);
|
||||
if (sandbox) await sandbox.destroy();
|
||||
},
|
||||
async getUrl(sandboxId: string): Promise<string> {
|
||||
const sandbox = await compute.sandbox.getById(sandboxId);
|
||||
if (!sandbox) throw new Error(`computesdk sandbox not found: ${sandboxId}`);
|
||||
return sandbox.getUrl({ port: agentPort });
|
||||
},
|
||||
};
|
||||
}
|
||||
75
sdks/typescript/src/providers/modal.ts
Normal file
75
sdks/typescript/src/providers/modal.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { ModalClient } from "modal";
|
||||
import type { SandboxProvider } from "./types.ts";
|
||||
import { DEFAULT_AGENTS, SANDBOX_AGENT_INSTALL_SCRIPT, buildServerStartCommand } from "./shared.ts";
|
||||
|
||||
const DEFAULT_AGENT_PORT = 3000;
|
||||
const DEFAULT_APP_NAME = "sandbox-agent";
|
||||
|
||||
export interface ModalProviderOptions {
|
||||
create?: {
|
||||
secrets?: Record<string, string>;
|
||||
appName?: string;
|
||||
};
|
||||
agentPort?: number;
|
||||
}
|
||||
|
||||
export function modal(options: ModalProviderOptions = {}): SandboxProvider {
|
||||
const agentPort = options.agentPort ?? DEFAULT_AGENT_PORT;
|
||||
const appName = options.create?.appName ?? DEFAULT_APP_NAME;
|
||||
const client = new ModalClient();
|
||||
|
||||
return {
|
||||
name: "modal",
|
||||
async create(): Promise<string> {
|
||||
const app = await client.apps.fromName(appName, { createIfMissing: true });
|
||||
|
||||
const image = client.images
|
||||
.fromRegistry("node:22-slim")
|
||||
.dockerfileCommands([
|
||||
"RUN apt-get update && apt-get install -y curl ca-certificates && rm -rf /var/lib/apt/lists/*",
|
||||
`RUN curl -fsSL ${SANDBOX_AGENT_INSTALL_SCRIPT} | sh`,
|
||||
]);
|
||||
|
||||
const envVars = options.create?.secrets ?? {};
|
||||
const secrets = Object.keys(envVars).length > 0 ? [await client.secrets.fromObject(envVars)] : [];
|
||||
|
||||
const sb = await client.sandboxes.create(app, image, {
|
||||
encryptedPorts: [agentPort],
|
||||
secrets,
|
||||
});
|
||||
|
||||
const exec = async (cmd: string) => {
|
||||
const p = await sb.exec(["bash", "-c", cmd], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
const exitCode = await p.wait();
|
||||
if (exitCode !== 0) {
|
||||
const stderr = await p.stderr.readText();
|
||||
throw new Error(`modal command failed (exit ${exitCode}): ${cmd}\n${stderr}`);
|
||||
}
|
||||
};
|
||||
|
||||
for (const agent of DEFAULT_AGENTS) {
|
||||
await exec(`sandbox-agent install-agent ${agent}`);
|
||||
}
|
||||
|
||||
await sb.exec(["bash", "-c", buildServerStartCommand(agentPort)]);
|
||||
|
||||
return sb.sandboxId;
|
||||
},
|
||||
async destroy(sandboxId: string): Promise<void> {
|
||||
const sb = await client.sandboxes.fromId(sandboxId);
|
||||
await sb.terminate();
|
||||
},
|
||||
async getUrl(sandboxId: string): Promise<string> {
|
||||
const sb = await client.sandboxes.fromId(sandboxId);
|
||||
const tunnels = await sb.tunnels();
|
||||
const tunnel = tunnels[agentPort];
|
||||
if (!tunnel) {
|
||||
throw new Error(`modal: no tunnel found for port ${agentPort}`);
|
||||
}
|
||||
return tunnel.url;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -13,6 +13,8 @@ import { docker } from "../src/providers/docker.ts";
|
|||
import { e2b } from "../src/providers/e2b.ts";
|
||||
import { daytona } from "../src/providers/daytona.ts";
|
||||
import { vercel } from "../src/providers/vercel.ts";
|
||||
import { modal } from "../src/providers/modal.ts";
|
||||
import { computesdk } from "../src/providers/computesdk.ts";
|
||||
import { prepareMockAgentDataHome } from "./helpers/mock-agent.ts";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
|
@ -216,6 +218,42 @@ function buildProviders(): ProviderEntry[] {
|
|||
});
|
||||
}
|
||||
|
||||
// --- modal ---
|
||||
// Session tests disabled: see docker comment above (ACP protocol mismatch).
|
||||
{
|
||||
entries.push({
|
||||
name: "modal",
|
||||
skipReasons: [...missingEnvVars("MODAL_TOKEN_ID", "MODAL_TOKEN_SECRET"), ...missingModules("modal")],
|
||||
agent: "claude",
|
||||
startTimeoutMs: 300_000,
|
||||
canVerifyDestroyedSandbox: false,
|
||||
sessionTestsEnabled: false,
|
||||
createProvider() {
|
||||
return modal({
|
||||
create: { secrets: collectApiKeys() },
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// --- computesdk ---
|
||||
// Session tests disabled: see docker comment above (ACP protocol mismatch).
|
||||
{
|
||||
entries.push({
|
||||
name: "computesdk",
|
||||
skipReasons: [...missingEnvVars("COMPUTESDK_API_KEY"), ...missingModules("computesdk")],
|
||||
agent: "claude",
|
||||
startTimeoutMs: 300_000,
|
||||
canVerifyDestroyedSandbox: false,
|
||||
sessionTestsEnabled: false,
|
||||
createProvider() {
|
||||
return computesdk({
|
||||
create: { envs: collectApiKeys() },
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,10 +9,12 @@ export default defineConfig({
|
|||
"src/providers/docker.ts",
|
||||
"src/providers/vercel.ts",
|
||||
"src/providers/cloudflare.ts",
|
||||
"src/providers/modal.ts",
|
||||
"src/providers/computesdk.ts",
|
||||
],
|
||||
format: ["esm"],
|
||||
dts: true,
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
external: ["@cloudflare/sandbox", "@daytonaio/sdk", "@e2b/code-interpreter", "@vercel/sandbox", "dockerode", "get-port"],
|
||||
external: ["@cloudflare/sandbox", "@daytonaio/sdk", "@e2b/code-interpreter", "@vercel/sandbox", "dockerode", "get-port", "modal", "computesdk"],
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue