From 20202c45eeb7c0b53a671a7330dc4aac470f65c3 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Sun, 15 Mar 2026 14:45:52 -0700 Subject: [PATCH] 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 --- docs/deploy/computesdk.mdx | 212 ++++--------------- docs/deploy/modal.mdx | 109 +++------- docs/docs.json | 1 + docs/quickstart.mdx | 17 ++ examples/computesdk/package.json | 2 +- examples/computesdk/src/computesdk.ts | 151 ------------- examples/computesdk/src/index.ts | 30 +++ examples/computesdk/tests/computesdk.test.ts | 25 ++- examples/modal/package.json | 2 +- examples/modal/src/index.ts | 30 +++ examples/modal/src/modal.ts | 123 ----------- examples/modal/tests/modal.test.ts | 25 ++- pnpm-lock.yaml | 106 ++-------- sdks/typescript/package.json | 20 +- sdks/typescript/src/providers/computesdk.ts | 53 +++++ sdks/typescript/src/providers/modal.ts | 75 +++++++ sdks/typescript/tests/providers.test.ts | 38 ++++ sdks/typescript/tsup.config.ts | 4 +- 18 files changed, 377 insertions(+), 646 deletions(-) delete mode 100644 examples/computesdk/src/computesdk.ts create mode 100644 examples/computesdk/src/index.ts create mode 100644 examples/modal/src/index.ts delete mode 100644 examples/modal/src/modal.ts create mode 100644 sdks/typescript/src/providers/computesdk.ts create mode 100644 sdks/typescript/src/providers/modal.ts diff --git a/docs/deploy/computesdk.mdx b/docs/deploy/computesdk.mdx index 5e07da0..1adfffe 100644 --- a/docs/deploy/computesdk.mdx +++ b/docs/deploy/computesdk.mdx @@ -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 = {}; 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. diff --git a/docs/deploy/modal.mdx b/docs/deploy/modal.mdx index cb081b0..02a3828 100644 --- a/docs/deploy/modal.mdx +++ b/docs/deploy/modal.mdx @@ -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 = {}; -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 = {}; +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= MODAL_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. diff --git a/docs/docs.json b/docs/docs.json index 3f2be1a..e1efd78 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -63,6 +63,7 @@ "deploy/vercel", "deploy/cloudflare", "deploy/docker", + "deploy/modal", "deploy/boxlite", "deploy/computesdk" ] diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx index 64e4de4..321d1a3 100644 --- a/docs/quickstart.mdx +++ b/docs/quickstart.mdx @@ -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"; diff --git a/examples/computesdk/package.json b/examples/computesdk/package.json index e22b51b..243b3b1 100644 --- a/examples/computesdk/package.json +++ b/examples/computesdk/package.json @@ -3,7 +3,7 @@ "private": true, "type": "module", "scripts": { - "start": "tsx src/computesdk.ts", + "start": "tsx src/index.ts", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/examples/computesdk/src/computesdk.ts b/examples/computesdk/src/computesdk.ts deleted file mode 100644 index 8e32644..0000000 --- a/examples/computesdk/src/computesdk.ts +++ /dev/null @@ -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>; - configWithProvider[provider] = providerConfig; - } - - compute.setConfig(config); -} - -configureComputeSDK(); - -const buildEnv = (): Record => { - const env: Record = {}; - 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; -}> { - 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 { - 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); - }); -} diff --git a/examples/computesdk/src/index.ts b/examples/computesdk/src/index.ts new file mode 100644 index 0000000..63d4aee --- /dev/null +++ b/examples/computesdk/src/index.ts @@ -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 = {}; +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); +}); diff --git a/examples/computesdk/tests/computesdk.test.ts b/examples/computesdk/tests/computesdk.test.ts index 0bbd24c..61ebb2c 100644 --- a/examples/computesdk/tests/computesdk.test.ts +++ b/examples/computesdk/tests/computesdk.test.ts @@ -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 = {}; + 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, diff --git a/examples/modal/package.json b/examples/modal/package.json index 61debbd..d3e51ec 100644 --- a/examples/modal/package.json +++ b/examples/modal/package.json @@ -3,7 +3,7 @@ "private": true, "type": "module", "scripts": { - "start": "tsx src/modal.ts", + "start": "tsx src/index.ts", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/examples/modal/src/index.ts b/examples/modal/src/index.ts new file mode 100644 index 0000000..35eef8d --- /dev/null +++ b/examples/modal/src/index.ts @@ -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 = {}; +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); +}); diff --git a/examples/modal/src/modal.ts b/examples/modal/src/modal.ts deleted file mode 100644 index d525ad3..0000000 --- a/examples/modal/src/modal.ts +++ /dev/null @@ -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 = {}; - 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; -}> { - 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 { - 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); - }); -} diff --git a/examples/modal/tests/modal.test.ts b/examples/modal/tests/modal.test.ts index 9c27a21..010256a 100644 --- a/examples/modal/tests/modal.test.ts +++ b/examples/modal/tests/modal.test.ts @@ -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 = {}; + 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, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 59a5a6b..e4e7838 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/sdks/typescript/package.json b/sdks/typescript/package.json index 64d42e6..9fd62fe 100644 --- a/sdks/typescript/package.json +++ b/sdks/typescript/package.json @@ -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", diff --git a/sdks/typescript/src/providers/computesdk.ts b/sdks/typescript/src/providers/computesdk.ts new file mode 100644 index 0000000..e1b1a69 --- /dev/null +++ b/sdks/typescript/src/providers/computesdk.ts @@ -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; + }; + agentPort?: number; +} + +export function computesdk(options: ComputeSdkProviderOptions = {}): SandboxProvider { + const agentPort = options.agentPort ?? DEFAULT_AGENT_PORT; + + return { + name: "computesdk", + async create(): Promise { + 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 { + const sandbox = await compute.sandbox.getById(sandboxId); + if (sandbox) await sandbox.destroy(); + }, + async getUrl(sandboxId: string): Promise { + const sandbox = await compute.sandbox.getById(sandboxId); + if (!sandbox) throw new Error(`computesdk sandbox not found: ${sandboxId}`); + return sandbox.getUrl({ port: agentPort }); + }, + }; +} diff --git a/sdks/typescript/src/providers/modal.ts b/sdks/typescript/src/providers/modal.ts new file mode 100644 index 0000000..b018d05 --- /dev/null +++ b/sdks/typescript/src/providers/modal.ts @@ -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; + 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 { + 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 { + const sb = await client.sandboxes.fromId(sandboxId); + await sb.terminate(); + }, + async getUrl(sandboxId: string): Promise { + 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; + }, + }; +} diff --git a/sdks/typescript/tests/providers.test.ts b/sdks/typescript/tests/providers.test.ts index 4b874b0..3376026 100644 --- a/sdks/typescript/tests/providers.test.ts +++ b/sdks/typescript/tests/providers.test.ts @@ -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; } diff --git a/sdks/typescript/tsup.config.ts b/sdks/typescript/tsup.config.ts index fe84102..984eeb3 100644 --- a/sdks/typescript/tsup.config.ts +++ b/sdks/typescript/tsup.config.ts @@ -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"], });