mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 13:03:46 +00:00
SDK: Add ensureServer() for automatic server recovery
Add ensureServer() to SandboxProvider interface to handle cases where the sandbox-agent server stops or goes to sleep. The SDK now calls this method after 3 consecutive health-check failures, allowing providers to restart the server if needed. Most built-in providers (E2B, Daytona, Vercel, Modal, ComputeSDK) implement this. Docker and Cloudflare manage server lifecycle differently, and Local uses managed child processes. Also update docs for quickstart, architecture, multiplayer, and session persistence; mark persist-* packages as deprecated; and add ensureServer implementations to all applicable providers. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d008283c17
commit
35840facdd
38 changed files with 620 additions and 205 deletions
12
CLAUDE.md
12
CLAUDE.md
|
|
@ -46,6 +46,18 @@
|
|||
- `docs/agent-capabilities.mdx` lists models/modes/thought levels per agent. Update it when adding a new agent or changing `fallback_config_options`. If its "Last updated" date is >2 weeks old, re-run `cd scripts/agent-configs && npx tsx dump.ts` and update the doc to match. Source data: `scripts/agent-configs/resources/*.json` and hardcoded entries in `server/packages/sandbox-agent/src/router/support.rs` (`fallback_config_options`).
|
||||
- Some agent models are gated by subscription (e.g. Claude `opus`). The live report only shows models available to the current credentials. The static doc and JSON resource files should list all known models regardless of subscription tier.
|
||||
|
||||
## Persist Packages (Deprecated)
|
||||
|
||||
- The `@sandbox-agent/persist-*` npm packages (`persist-sqlite`, `persist-postgres`, `persist-indexeddb`, `persist-rivet`) are deprecated stubs. They still publish to npm but throw a deprecation error at import time.
|
||||
- Driver implementations now live inline in examples and consuming packages:
|
||||
- SQLite: `examples/persist-sqlite/src/persist.ts`
|
||||
- Postgres: `examples/persist-postgres/src/persist.ts`
|
||||
- IndexedDB: `frontend/packages/inspector/src/persist-indexeddb.ts`
|
||||
- Rivet: inlined in `docs/multiplayer.mdx`
|
||||
- In-memory: built into the main `sandbox-agent` SDK (`InMemorySessionPersistDriver`)
|
||||
- Docs (`docs/session-persistence.mdx`) link to the example implementations on GitHub instead of referencing the packages.
|
||||
- Do not re-add `@sandbox-agent/persist-*` as dependencies anywhere. New persist drivers should be copied into the consuming project directly.
|
||||
|
||||
## Install Version References
|
||||
|
||||
- Channel policy:
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
|||
COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/
|
||||
COPY sdks/cli-shared/package.json ./sdks/cli-shared/
|
||||
COPY sdks/acp-http-client/package.json ./sdks/acp-http-client/
|
||||
COPY sdks/persist-indexeddb/package.json ./sdks/persist-indexeddb/
|
||||
COPY sdks/react/package.json ./sdks/react/
|
||||
COPY sdks/typescript/package.json ./sdks/typescript/
|
||||
|
||||
|
|
@ -21,15 +20,13 @@ RUN pnpm install --filter @sandbox-agent/inspector...
|
|||
COPY docs/openapi.json ./docs/
|
||||
COPY sdks/cli-shared ./sdks/cli-shared
|
||||
COPY sdks/acp-http-client ./sdks/acp-http-client
|
||||
COPY sdks/persist-indexeddb ./sdks/persist-indexeddb
|
||||
COPY sdks/react ./sdks/react
|
||||
COPY sdks/typescript ./sdks/typescript
|
||||
|
||||
# Build cli-shared, acp-http-client, SDK, then persist-indexeddb and react (depends on SDK)
|
||||
# Build cli-shared, acp-http-client, SDK, then react (depends on SDK)
|
||||
RUN cd sdks/cli-shared && pnpm exec tsup
|
||||
RUN cd sdks/acp-http-client && pnpm exec tsup
|
||||
RUN cd sdks/typescript && SKIP_OPENAPI_GEN=1 pnpm exec tsup
|
||||
RUN cd sdks/persist-indexeddb && pnpm exec tsup
|
||||
RUN cd sdks/react && pnpm exec tsup
|
||||
|
||||
# Copy inspector source and build
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
|||
COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/
|
||||
COPY sdks/cli-shared/package.json ./sdks/cli-shared/
|
||||
COPY sdks/acp-http-client/package.json ./sdks/acp-http-client/
|
||||
COPY sdks/persist-indexeddb/package.json ./sdks/persist-indexeddb/
|
||||
COPY sdks/react/package.json ./sdks/react/
|
||||
COPY sdks/typescript/package.json ./sdks/typescript/
|
||||
|
||||
|
|
@ -21,15 +20,13 @@ RUN pnpm install --filter @sandbox-agent/inspector...
|
|||
COPY docs/openapi.json ./docs/
|
||||
COPY sdks/cli-shared ./sdks/cli-shared
|
||||
COPY sdks/acp-http-client ./sdks/acp-http-client
|
||||
COPY sdks/persist-indexeddb ./sdks/persist-indexeddb
|
||||
COPY sdks/react ./sdks/react
|
||||
COPY sdks/typescript ./sdks/typescript
|
||||
|
||||
# Build cli-shared, acp-http-client, SDK, then persist-indexeddb and react (depends on SDK)
|
||||
# Build cli-shared, acp-http-client, SDK, then react (depends on SDK)
|
||||
RUN cd sdks/cli-shared && pnpm exec tsup
|
||||
RUN cd sdks/acp-http-client && pnpm exec tsup
|
||||
RUN cd sdks/typescript && SKIP_OPENAPI_GEN=1 pnpm exec tsup
|
||||
RUN cd sdks/persist-indexeddb && pnpm exec tsup
|
||||
RUN cd sdks/react && pnpm exec tsup
|
||||
|
||||
# Copy inspector source and build
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
|||
COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/
|
||||
COPY sdks/cli-shared/package.json ./sdks/cli-shared/
|
||||
COPY sdks/acp-http-client/package.json ./sdks/acp-http-client/
|
||||
COPY sdks/persist-indexeddb/package.json ./sdks/persist-indexeddb/
|
||||
COPY sdks/react/package.json ./sdks/react/
|
||||
COPY sdks/typescript/package.json ./sdks/typescript/
|
||||
|
||||
|
|
@ -21,15 +20,13 @@ RUN pnpm install --filter @sandbox-agent/inspector...
|
|||
COPY docs/openapi.json ./docs/
|
||||
COPY sdks/cli-shared ./sdks/cli-shared
|
||||
COPY sdks/acp-http-client ./sdks/acp-http-client
|
||||
COPY sdks/persist-indexeddb ./sdks/persist-indexeddb
|
||||
COPY sdks/react ./sdks/react
|
||||
COPY sdks/typescript ./sdks/typescript
|
||||
|
||||
# Build cli-shared, acp-http-client, SDK, then persist-indexeddb and react (depends on SDK)
|
||||
# Build cli-shared, acp-http-client, SDK, then react (depends on SDK)
|
||||
RUN cd sdks/cli-shared && pnpm exec tsup
|
||||
RUN cd sdks/acp-http-client && pnpm exec tsup
|
||||
RUN cd sdks/typescript && SKIP_OPENAPI_GEN=1 pnpm exec tsup
|
||||
RUN cd sdks/persist-indexeddb && pnpm exec tsup
|
||||
RUN cd sdks/react && pnpm exec tsup
|
||||
|
||||
# Copy inspector source and build
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
|||
COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/
|
||||
COPY sdks/cli-shared/package.json ./sdks/cli-shared/
|
||||
COPY sdks/acp-http-client/package.json ./sdks/acp-http-client/
|
||||
COPY sdks/persist-indexeddb/package.json ./sdks/persist-indexeddb/
|
||||
COPY sdks/react/package.json ./sdks/react/
|
||||
COPY sdks/typescript/package.json ./sdks/typescript/
|
||||
|
||||
|
|
@ -21,15 +20,13 @@ RUN pnpm install --filter @sandbox-agent/inspector...
|
|||
COPY docs/openapi.json ./docs/
|
||||
COPY sdks/cli-shared ./sdks/cli-shared
|
||||
COPY sdks/acp-http-client ./sdks/acp-http-client
|
||||
COPY sdks/persist-indexeddb ./sdks/persist-indexeddb
|
||||
COPY sdks/react ./sdks/react
|
||||
COPY sdks/typescript ./sdks/typescript
|
||||
|
||||
# Build cli-shared, acp-http-client, SDK, then persist-indexeddb and react (depends on SDK)
|
||||
# Build cli-shared, acp-http-client, SDK, then react (depends on SDK)
|
||||
RUN cd sdks/cli-shared && pnpm exec tsup
|
||||
RUN cd sdks/acp-http-client && pnpm exec tsup
|
||||
RUN cd sdks/typescript && SKIP_OPENAPI_GEN=1 pnpm exec tsup
|
||||
RUN cd sdks/persist-indexeddb && pnpm exec tsup
|
||||
RUN cd sdks/react && pnpm exec tsup
|
||||
|
||||
# Copy inspector source and build
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
|||
COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/
|
||||
COPY sdks/cli-shared/package.json ./sdks/cli-shared/
|
||||
COPY sdks/acp-http-client/package.json ./sdks/acp-http-client/
|
||||
COPY sdks/persist-indexeddb/package.json ./sdks/persist-indexeddb/
|
||||
COPY sdks/react/package.json ./sdks/react/
|
||||
COPY sdks/typescript/package.json ./sdks/typescript/
|
||||
|
||||
|
|
@ -21,15 +20,13 @@ RUN pnpm install --filter @sandbox-agent/inspector...
|
|||
COPY docs/openapi.json ./docs/
|
||||
COPY sdks/cli-shared ./sdks/cli-shared
|
||||
COPY sdks/acp-http-client ./sdks/acp-http-client
|
||||
COPY sdks/persist-indexeddb ./sdks/persist-indexeddb
|
||||
COPY sdks/react ./sdks/react
|
||||
COPY sdks/typescript ./sdks/typescript
|
||||
|
||||
# Build cli-shared, acp-http-client, SDK, then persist-indexeddb and react (depends on SDK)
|
||||
# Build cli-shared, acp-http-client, SDK, then react (depends on SDK)
|
||||
RUN cd sdks/cli-shared && pnpm exec tsup
|
||||
RUN cd sdks/acp-http-client && pnpm exec tsup
|
||||
RUN cd sdks/typescript && SKIP_OPENAPI_GEN=1 pnpm exec tsup
|
||||
RUN cd sdks/persist-indexeddb && pnpm exec tsup
|
||||
RUN cd sdks/react && pnpm exec tsup
|
||||
|
||||
# Copy inspector source and build
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
|||
COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/
|
||||
COPY sdks/cli-shared/package.json ./sdks/cli-shared/
|
||||
COPY sdks/acp-http-client/package.json ./sdks/acp-http-client/
|
||||
COPY sdks/persist-indexeddb/package.json ./sdks/persist-indexeddb/
|
||||
COPY sdks/react/package.json ./sdks/react/
|
||||
COPY sdks/typescript/package.json ./sdks/typescript/
|
||||
|
||||
|
|
@ -23,7 +22,6 @@ RUN pnpm install --filter @sandbox-agent/inspector...
|
|||
COPY docs/openapi.json ./docs/
|
||||
COPY sdks/cli-shared ./sdks/cli-shared
|
||||
COPY sdks/acp-http-client ./sdks/acp-http-client
|
||||
COPY sdks/persist-indexeddb ./sdks/persist-indexeddb
|
||||
COPY sdks/react ./sdks/react
|
||||
COPY sdks/typescript ./sdks/typescript
|
||||
|
||||
|
|
@ -31,7 +29,6 @@ COPY sdks/typescript ./sdks/typescript
|
|||
RUN cd sdks/cli-shared && pnpm exec tsup
|
||||
RUN cd sdks/acp-http-client && pnpm exec tsup
|
||||
RUN cd sdks/typescript && SKIP_OPENAPI_GEN=1 pnpm exec tsup
|
||||
RUN cd sdks/persist-indexeddb && pnpm exec tsup
|
||||
RUN cd sdks/react && pnpm exec tsup
|
||||
|
||||
# Copy inspector source and build
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
|||
COPY frontend/packages/inspector/package.json ./frontend/packages/inspector/
|
||||
COPY sdks/cli-shared/package.json ./sdks/cli-shared/
|
||||
COPY sdks/acp-http-client/package.json ./sdks/acp-http-client/
|
||||
COPY sdks/persist-indexeddb/package.json ./sdks/persist-indexeddb/
|
||||
COPY sdks/react/package.json ./sdks/react/
|
||||
COPY sdks/typescript/package.json ./sdks/typescript/
|
||||
|
||||
|
|
@ -20,14 +19,12 @@ RUN pnpm install --filter @sandbox-agent/inspector...
|
|||
COPY docs/openapi.json ./docs/
|
||||
COPY sdks/cli-shared ./sdks/cli-shared
|
||||
COPY sdks/acp-http-client ./sdks/acp-http-client
|
||||
COPY sdks/persist-indexeddb ./sdks/persist-indexeddb
|
||||
COPY sdks/react ./sdks/react
|
||||
COPY sdks/typescript ./sdks/typescript
|
||||
|
||||
RUN cd sdks/cli-shared && pnpm exec tsup
|
||||
RUN cd sdks/acp-http-client && pnpm exec tsup
|
||||
RUN cd sdks/typescript && SKIP_OPENAPI_GEN=1 pnpm exec tsup
|
||||
RUN cd sdks/persist-indexeddb && pnpm exec tsup
|
||||
RUN cd sdks/react && pnpm exec tsup
|
||||
|
||||
COPY frontend/packages/inspector ./frontend/packages/inspector
|
||||
|
|
|
|||
|
|
@ -40,7 +40,11 @@ flowchart LR
|
|||
|
||||
For the `local` provider, provisioning is a no-op and the server runs as a local subprocess.
|
||||
|
||||
## Server endpoints
|
||||
### Server recovery
|
||||
|
||||
If the server process stops, the SDK automatically calls the provider's `ensureServer()` after 3 consecutive health-check failures. Most built-in providers implement this. Custom providers can add `ensureServer(sandboxId)` to their `SandboxProvider` object.
|
||||
|
||||
## Server HTTP API
|
||||
|
||||
See the [HTTP API reference](/api-reference) for the full list of server endpoints.
|
||||
|
||||
|
|
@ -54,6 +58,6 @@ sandbox-agent install-agent --all
|
|||
|
||||
The `rivetdev/sandbox-agent:0.3.2-full` Docker image ships with all agents pre-installed.
|
||||
|
||||
## Production topology
|
||||
## Production-ready agent orchestration
|
||||
|
||||
For production deployments, see [Orchestration Architecture](/orchestration-architecture) for recommended topology, backend requirements, and session persistence patterns.
|
||||
|
|
|
|||
|
|
@ -22,16 +22,13 @@ Use [actor keys](https://rivet.dev/docs/actors/keys) to map each workspace to on
|
|||
import { actor, setup } from "rivetkit";
|
||||
import { SandboxAgent, type SessionPersistDriver, type SessionRecord, type SessionEvent, type ListPageRequest, type ListPage, type ListEventsRequest } from "sandbox-agent";
|
||||
|
||||
// Inline Rivet persist driver — copy into your project.
|
||||
// See https://github.com/nichochar/sandbox-agent/tree/main/examples/persist-rivet
|
||||
interface ActorContextLike { state: Record<string, unknown>; }
|
||||
interface RivetPersistData { sessions: Record<string, SessionRecord>; events: Record<string, SessionEvent[]>; }
|
||||
type RivetPersistState = { _sandboxAgentPersist: RivetPersistData };
|
||||
|
||||
class RivetSessionPersistDriver implements SessionPersistDriver {
|
||||
private readonly stateKey: string;
|
||||
private readonly ctx: ActorContextLike;
|
||||
constructor(ctx: ActorContextLike, options: { stateKey?: string } = {}) {
|
||||
private readonly ctx: { state: Record<string, unknown> };
|
||||
constructor(ctx: { state: Record<string, unknown> }, options: { stateKey?: string } = {}) {
|
||||
this.ctx = ctx;
|
||||
this.stateKey = options.stateKey ?? "_sandboxAgentPersist";
|
||||
if (!this.ctx.state[this.stateKey]) {
|
||||
|
|
@ -146,5 +143,5 @@ await conn.prompt({
|
|||
## Notes
|
||||
|
||||
- Keep sandbox calls actor-only. Browser clients should not call Sandbox Agent directly.
|
||||
- Inline the Rivet persist driver (shown above) so session history persists in actor state.
|
||||
- Copy the Rivet persist driver from the example above into your project so session history persists in actor state.
|
||||
- For client connection patterns, see [Rivet JavaScript client](https://rivet.dev/docs/clients/javascript).
|
||||
|
|
|
|||
|
|
@ -23,112 +23,177 @@ icon: "rocket"
|
|||
</Step>
|
||||
|
||||
<Step title="Start the sandbox">
|
||||
`SandboxAgent.start()` provisions a sandbox, starts a lightweight [Sandbox Agent server](/architecture) inside it, and connects your SDK client. Pass your LLM API keys so the agent can reach its provider.
|
||||
`SandboxAgent.start()` provisions a sandbox, starts a lightweight [Sandbox Agent server](/architecture) inside it, and connects your SDK client.
|
||||
|
||||
<CodeGroup>
|
||||
```typescript Local
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { local } from "sandbox-agent/local";
|
||||
<Tabs>
|
||||
<Tab title="Local">
|
||||
```bash
|
||||
npm install sandbox-agent@0.3.x
|
||||
```
|
||||
|
||||
// Runs on your machine. Inherits process.env automatically.
|
||||
const sdk = await SandboxAgent.start({
|
||||
sandbox: local(),
|
||||
});
|
||||
```
|
||||
```typescript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { local } from "sandbox-agent/local";
|
||||
|
||||
```typescript E2B
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { e2b } from "sandbox-agent/e2b";
|
||||
// Runs on your machine. Inherits process.env automatically.
|
||||
const client = await SandboxAgent.start({
|
||||
sandbox: local(),
|
||||
});
|
||||
```
|
||||
|
||||
const sdk = await SandboxAgent.start({
|
||||
sandbox: e2b({
|
||||
create: {
|
||||
// Pass whichever keys your agent needs
|
||||
envs: {
|
||||
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
|
||||
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
See [Local deploy guide](/deploy/local)
|
||||
|
||||
```typescript Daytona
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { daytona } from "sandbox-agent/daytona";
|
||||
<Accordion title="Passing LLM credentials">
|
||||
Local inherits `process.env` automatically, so no extra config is needed if your environment variables are already set.
|
||||
</Accordion>
|
||||
</Tab>
|
||||
|
||||
const sdk = await SandboxAgent.start({
|
||||
sandbox: daytona({
|
||||
create: {
|
||||
envVars: {
|
||||
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
|
||||
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
<Tab title="E2B">
|
||||
```bash
|
||||
npm install sandbox-agent@0.3.x @e2b/code-interpreter
|
||||
```
|
||||
|
||||
```typescript Vercel
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { vercel } from "sandbox-agent/vercel";
|
||||
```typescript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { e2b } from "sandbox-agent/e2b";
|
||||
|
||||
const sdk = await SandboxAgent.start({
|
||||
sandbox: vercel({
|
||||
create: {
|
||||
runtime: "node24",
|
||||
env: {
|
||||
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
|
||||
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
// Provisions a cloud sandbox on E2B, installs the server, and connects.
|
||||
const client = await SandboxAgent.start({
|
||||
sandbox: e2b(),
|
||||
});
|
||||
```
|
||||
|
||||
```typescript Modal
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { modal } from "sandbox-agent/modal";
|
||||
See [E2B deploy guide](/deploy/e2b)
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
<Accordion title="Passing LLM credentials">
|
||||
```typescript
|
||||
e2b({ create: { envs: { ANTHROPIC_API_KEY: "..." } } })
|
||||
```
|
||||
</Accordion>
|
||||
</Tab>
|
||||
|
||||
```typescript Cloudflare
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { cloudflare } from "sandbox-agent/cloudflare";
|
||||
<Tab title="Daytona">
|
||||
```bash
|
||||
npm install sandbox-agent@0.3.x @daytonaio/sdk
|
||||
```
|
||||
|
||||
const sdk = await SandboxAgent.start({
|
||||
sandbox: cloudflare({ sdk: cfSandboxClient }),
|
||||
});
|
||||
```
|
||||
```typescript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { daytona } from "sandbox-agent/daytona";
|
||||
|
||||
```typescript Docker
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { docker } from "sandbox-agent/docker";
|
||||
// Provisions a Daytona workspace with the server pre-installed.
|
||||
const client = await SandboxAgent.start({
|
||||
sandbox: daytona(),
|
||||
});
|
||||
```
|
||||
|
||||
// Good for testing. Not security-hardened like cloud sandboxes.
|
||||
const sdk = await SandboxAgent.start({
|
||||
sandbox: docker({
|
||||
env: [
|
||||
`ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}`,
|
||||
`OPENAI_API_KEY=${process.env.OPENAI_API_KEY}`,
|
||||
],
|
||||
}),
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
See [Daytona deploy guide](/deploy/daytona)
|
||||
|
||||
Each provider handles provisioning, server installation, and networking. Install the provider's peer dependency (e.g. `@e2b/code-interpreter`, `dockerode`) in your project. See the [Deploy](/deploy/local) guides for full setup details. For multi-tenant billing, per-user keys, and gateway options, see [LLM Credentials](/llm-credentials).
|
||||
<Accordion title="Passing LLM credentials">
|
||||
```typescript
|
||||
daytona({ create: { envVars: { ANTHROPIC_API_KEY: "..." } } })
|
||||
```
|
||||
</Accordion>
|
||||
</Tab>
|
||||
|
||||
<Tab title="Vercel">
|
||||
```bash
|
||||
npm install sandbox-agent@0.3.x @vercel/sandbox
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { vercel } from "sandbox-agent/vercel";
|
||||
|
||||
// Provisions a Vercel sandbox with the server installed on boot.
|
||||
const client = await SandboxAgent.start({
|
||||
sandbox: vercel(),
|
||||
});
|
||||
```
|
||||
|
||||
See [Vercel deploy guide](/deploy/vercel)
|
||||
|
||||
<Accordion title="Passing LLM credentials">
|
||||
```typescript
|
||||
vercel({ create: { env: { ANTHROPIC_API_KEY: "..." } } })
|
||||
```
|
||||
</Accordion>
|
||||
</Tab>
|
||||
|
||||
<Tab title="Modal">
|
||||
```bash
|
||||
npm install sandbox-agent@0.3.x modal
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { modal } from "sandbox-agent/modal";
|
||||
|
||||
// Builds a container image with agents pre-installed (cached after first run),
|
||||
// starts a Modal sandbox from that image, and connects.
|
||||
const client = await SandboxAgent.start({
|
||||
sandbox: modal(),
|
||||
});
|
||||
```
|
||||
|
||||
See [Modal deploy guide](/deploy/modal)
|
||||
|
||||
<Accordion title="Passing LLM credentials">
|
||||
```typescript
|
||||
modal({ create: { secrets: { ANTHROPIC_API_KEY: "..." } } })
|
||||
```
|
||||
</Accordion>
|
||||
</Tab>
|
||||
|
||||
<Tab title="Cloudflare">
|
||||
```bash
|
||||
npm install sandbox-agent@0.3.x @cloudflare/sandbox
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { cloudflare } from "sandbox-agent/cloudflare";
|
||||
import { SandboxClient } from "@cloudflare/sandbox";
|
||||
|
||||
// Uses the Cloudflare Sandbox SDK to provision and connect.
|
||||
// The Cloudflare SDK handles server lifecycle internally.
|
||||
const cfSandboxClient = new SandboxClient();
|
||||
const client = await SandboxAgent.start({
|
||||
sandbox: cloudflare({ sdk: cfSandboxClient }),
|
||||
});
|
||||
```
|
||||
|
||||
See [Cloudflare deploy guide](/deploy/cloudflare)
|
||||
|
||||
<Accordion title="Passing LLM credentials">
|
||||
Pass credentials via the Cloudflare SDK's environment configuration. See the [Cloudflare deploy guide](/deploy/cloudflare) for details.
|
||||
</Accordion>
|
||||
</Tab>
|
||||
|
||||
<Tab title="Docker">
|
||||
```bash
|
||||
npm install sandbox-agent@0.3.x dockerode get-port
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { docker } from "sandbox-agent/docker";
|
||||
|
||||
// Runs a Docker container locally. Good for testing.
|
||||
const client = await SandboxAgent.start({
|
||||
sandbox: docker(),
|
||||
});
|
||||
```
|
||||
|
||||
See [Docker deploy guide](/deploy/docker)
|
||||
|
||||
<Accordion title="Passing LLM credentials">
|
||||
```typescript
|
||||
docker({ env: ["ANTHROPIC_API_KEY=..."] })
|
||||
```
|
||||
</Accordion>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Implementing a custom provider">
|
||||
|
|
@ -152,7 +217,7 @@ icon: "rocket"
|
|||
},
|
||||
};
|
||||
|
||||
const sdk = await SandboxAgent.start({
|
||||
const client = await SandboxAgent.start({
|
||||
sandbox: myProvider,
|
||||
});
|
||||
```
|
||||
|
|
@ -162,7 +227,7 @@ icon: "rocket"
|
|||
If you already have a Sandbox Agent server running, connect directly:
|
||||
|
||||
```typescript
|
||||
const sdk = await SandboxAgent.connect({
|
||||
const client = await SandboxAgent.connect({
|
||||
baseUrl: "http://127.0.0.1:2468",
|
||||
});
|
||||
```
|
||||
|
|
@ -196,29 +261,115 @@ icon: "rocket"
|
|||
</Step>
|
||||
|
||||
<Step title="Create a session and send a prompt">
|
||||
```typescript
|
||||
const session = await sdk.createSession({
|
||||
agent: "claude",
|
||||
});
|
||||
<CodeGroup>
|
||||
|
||||
session.onEvent((event) => {
|
||||
console.log(event.sender, event.payload);
|
||||
});
|
||||
```typescript Claude
|
||||
const session = await client.createSession({
|
||||
agent: "claude",
|
||||
});
|
||||
|
||||
const result = await session.prompt([
|
||||
{ type: "text", text: "Summarize the repository and suggest next steps." },
|
||||
]);
|
||||
session.onEvent((event) => {
|
||||
console.log(event.sender, event.payload);
|
||||
});
|
||||
|
||||
console.log(result.stopReason);
|
||||
```
|
||||
const result = await session.prompt([
|
||||
{ type: "text", text: "Summarize the repository and suggest next steps." },
|
||||
]);
|
||||
|
||||
console.log(result.stopReason);
|
||||
```
|
||||
|
||||
```typescript Codex
|
||||
const session = await client.createSession({
|
||||
agent: "codex",
|
||||
});
|
||||
|
||||
session.onEvent((event) => {
|
||||
console.log(event.sender, event.payload);
|
||||
});
|
||||
|
||||
const result = await session.prompt([
|
||||
{ type: "text", text: "Summarize the repository and suggest next steps." },
|
||||
]);
|
||||
|
||||
console.log(result.stopReason);
|
||||
```
|
||||
|
||||
```typescript OpenCode
|
||||
const session = await client.createSession({
|
||||
agent: "opencode",
|
||||
});
|
||||
|
||||
session.onEvent((event) => {
|
||||
console.log(event.sender, event.payload);
|
||||
});
|
||||
|
||||
const result = await session.prompt([
|
||||
{ type: "text", text: "Summarize the repository and suggest next steps." },
|
||||
]);
|
||||
|
||||
console.log(result.stopReason);
|
||||
```
|
||||
|
||||
```typescript Cursor
|
||||
const session = await client.createSession({
|
||||
agent: "cursor",
|
||||
});
|
||||
|
||||
session.onEvent((event) => {
|
||||
console.log(event.sender, event.payload);
|
||||
});
|
||||
|
||||
const result = await session.prompt([
|
||||
{ type: "text", text: "Summarize the repository and suggest next steps." },
|
||||
]);
|
||||
|
||||
console.log(result.stopReason);
|
||||
```
|
||||
|
||||
```typescript Amp
|
||||
const session = await client.createSession({
|
||||
agent: "amp",
|
||||
});
|
||||
|
||||
session.onEvent((event) => {
|
||||
console.log(event.sender, event.payload);
|
||||
});
|
||||
|
||||
const result = await session.prompt([
|
||||
{ type: "text", text: "Summarize the repository and suggest next steps." },
|
||||
]);
|
||||
|
||||
console.log(result.stopReason);
|
||||
```
|
||||
|
||||
```typescript Pi
|
||||
const session = await client.createSession({
|
||||
agent: "pi",
|
||||
});
|
||||
|
||||
session.onEvent((event) => {
|
||||
console.log(event.sender, event.payload);
|
||||
});
|
||||
|
||||
const result = await session.prompt([
|
||||
{ type: "text", text: "Summarize the repository and suggest next steps." },
|
||||
]);
|
||||
|
||||
console.log(result.stopReason);
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
See [Agent Sessions](/agent-sessions) for the full sessions API.
|
||||
</Step>
|
||||
|
||||
<Step title="Clean up">
|
||||
```typescript
|
||||
await sdk.destroySandbox(); // tears down the sandbox and disconnects
|
||||
await client.destroySandbox(); // tears down the sandbox and disconnects
|
||||
```
|
||||
|
||||
Use `sdk.dispose()` instead to disconnect without destroying the sandbox (for reconnecting later).
|
||||
Use `client.dispose()` instead to disconnect without destroying the sandbox (for reconnecting later).
|
||||
</Step>
|
||||
|
||||
<Step title="Inspect with the UI">
|
||||
|
|
@ -236,7 +387,7 @@ icon: "rocket"
|
|||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { e2b } from "sandbox-agent/e2b";
|
||||
|
||||
const sdk = await SandboxAgent.start({
|
||||
const client = await SandboxAgent.start({
|
||||
sandbox: e2b({
|
||||
create: {
|
||||
envs: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY },
|
||||
|
|
@ -245,7 +396,7 @@ const sdk = await SandboxAgent.start({
|
|||
});
|
||||
|
||||
try {
|
||||
const session = await sdk.createSession({ agent: "claude" });
|
||||
const session = await client.createSession({ agent: "claude" });
|
||||
|
||||
session.onEvent((event) => {
|
||||
console.log(`[${event.sender}]`, JSON.stringify(event.payload));
|
||||
|
|
@ -257,7 +408,7 @@ try {
|
|||
|
||||
console.log("Done:", result.stopReason);
|
||||
} finally {
|
||||
await sdk.destroySandbox();
|
||||
await client.destroySandbox();
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -10,11 +10,19 @@ With persistence enabled, sessions can be restored after runtime/session loss. S
|
|||
|
||||
Each driver stores:
|
||||
|
||||
- `SessionRecord` (`id`, `agent`, `agentSessionId`, `lastConnectionId`, `createdAt`, optional `destroyedAt`, optional `sandboxId`, optional `sessionInit`)
|
||||
- `SessionRecord` (`id`, `agent`, `agentSessionId`, `lastConnectionId`, `createdAt`, optional `destroyedAt`, optional `sandboxId`, optional `sessionInit`, optional `configOptions`, optional `modes`)
|
||||
- `SessionEvent` (`id`, `eventIndex`, `sessionId`, `connectionId`, `sender`, `payload`, `createdAt`)
|
||||
|
||||
## Persistence drivers
|
||||
|
||||
### Rivet
|
||||
|
||||
Recommended for sandbox orchestration with actor state. See [Multiplayer](/multiplayer) for a full Rivet actor example with persistence in actor state.
|
||||
|
||||
### IndexedDB (browser)
|
||||
|
||||
Best for browser apps that should survive reloads. See the [Inspector source](https://github.com/rivet-dev/sandbox-agent/tree/main/frontend/packages/inspector/src/persist-indexeddb.ts) for a complete IndexedDB driver you can copy into your project.
|
||||
|
||||
### In-memory (built-in)
|
||||
|
||||
Best for local dev and ephemeral workloads. No extra dependencies required.
|
||||
|
|
@ -55,7 +63,7 @@ const sdk = await SandboxAgent.connect({
|
|||
});
|
||||
```
|
||||
|
||||
See the [full SQLite example](https://github.com/nichochar/sandbox-agent/tree/main/examples/persist-sqlite) for the complete driver implementation you can copy into your project.
|
||||
See the [full SQLite example](https://github.com/rivet-dev/sandbox-agent/tree/main/examples/persist-sqlite) for the complete driver implementation you can copy into your project.
|
||||
|
||||
### Postgres
|
||||
|
||||
|
|
@ -80,15 +88,7 @@ const sdk = await SandboxAgent.connect({
|
|||
});
|
||||
```
|
||||
|
||||
See the [full Postgres example](https://github.com/nichochar/sandbox-agent/tree/main/examples/persist-postgres) for the complete driver implementation you can copy into your project.
|
||||
|
||||
### IndexedDB (browser)
|
||||
|
||||
Best for browser apps that should survive reloads. See the [Inspector source](https://github.com/nichochar/sandbox-agent/tree/main/frontend/packages/inspector/src/persist-indexeddb.ts) for a complete IndexedDB driver you can copy into your project.
|
||||
|
||||
### Rivet
|
||||
|
||||
Recommended for sandbox orchestration with actor state. See [Multiplayer](/multiplayer) for a full Rivet actor example with inline persistence.
|
||||
See the [full Postgres example](https://github.com/rivet-dev/sandbox-agent/tree/main/examples/persist-postgres) for the complete driver implementation you can copy into your project.
|
||||
|
||||
### Custom driver
|
||||
|
||||
|
|
|
|||
154
examples/cloudflare/tests/cloudflare.test.ts
Normal file
154
examples/cloudflare/tests/cloudflare.test.ts
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const PROJECT_DIR = resolve(__dirname, "..");
|
||||
|
||||
/**
|
||||
* Cloudflare Workers integration test.
|
||||
*
|
||||
* Set RUN_CLOUDFLARE_EXAMPLES=1 to enable. Requires wrangler and Docker.
|
||||
*
|
||||
* This starts `wrangler dev` which:
|
||||
* 1. Builds the Dockerfile (cloudflare/sandbox base + sandbox-agent)
|
||||
* 2. Starts a local Workers runtime with Durable Objects and containers
|
||||
* 3. Exposes the app on a local port
|
||||
*
|
||||
* We then test through the proxy endpoint which forwards to sandbox-agent
|
||||
* running inside the container.
|
||||
*/
|
||||
const shouldRun = process.env.RUN_CLOUDFLARE_EXAMPLES === "1";
|
||||
const timeoutMs = Number.parseInt(process.env.SANDBOX_TEST_TIMEOUT_MS || "", 10) || 600_000;
|
||||
|
||||
const testFn = shouldRun ? it : it.skip;
|
||||
|
||||
interface WranglerDev {
|
||||
baseUrl: string;
|
||||
cleanup: () => void;
|
||||
}
|
||||
|
||||
async function startWranglerDev(): Promise<WranglerDev> {
|
||||
// Build frontend assets first (wrangler expects dist/ to exist)
|
||||
execSync("npx vite build", { cwd: PROJECT_DIR, stdio: "pipe" });
|
||||
|
||||
return new Promise<WranglerDev>((resolve, reject) => {
|
||||
const child: ChildProcess = spawn("npx", ["wrangler", "dev", "--port", "0"], {
|
||||
cwd: PROJECT_DIR,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
detached: true,
|
||||
env: {
|
||||
...process.env,
|
||||
// Ensure wrangler picks up API keys to pass to the container
|
||||
NODE_ENV: "development",
|
||||
},
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
let resolved = false;
|
||||
|
||||
const cleanup = () => {
|
||||
if (child.pid) {
|
||||
// Kill process group to ensure wrangler and its children are cleaned up
|
||||
try {
|
||||
process.kill(-child.pid, "SIGTERM");
|
||||
} catch {
|
||||
try {
|
||||
child.kill("SIGTERM");
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
cleanup();
|
||||
reject(new Error(`wrangler dev did not start within 120s.\nstdout: ${stdout}\nstderr: ${stderr}`));
|
||||
}
|
||||
}, 120_000);
|
||||
|
||||
const onData = (chunk: Buffer) => {
|
||||
const text = chunk.toString();
|
||||
stdout += text;
|
||||
|
||||
// wrangler dev prints "Ready on http://localhost:XXXX" when ready
|
||||
const match = stdout.match(/Ready on (https?:\/\/[^\s]+)/i) ?? stdout.match(/(https?:\/\/(?:localhost|127\.0\.0\.1):\d+)/);
|
||||
if (match && !resolved) {
|
||||
resolved = true;
|
||||
clearTimeout(timer);
|
||||
resolve({ baseUrl: match[1], cleanup });
|
||||
}
|
||||
};
|
||||
|
||||
child.stdout?.on("data", onData);
|
||||
child.stderr?.on("data", (chunk: Buffer) => {
|
||||
const text = chunk.toString();
|
||||
stderr += text;
|
||||
// Some wrangler versions print ready message to stderr
|
||||
const match = text.match(/Ready on (https?:\/\/[^\s]+)/i) ?? text.match(/(https?:\/\/(?:localhost|127\.0\.0\.1):\d+)/);
|
||||
if (match && !resolved) {
|
||||
resolved = true;
|
||||
clearTimeout(timer);
|
||||
resolve({ baseUrl: match[1], cleanup });
|
||||
}
|
||||
});
|
||||
|
||||
child.on("error", (err) => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
clearTimeout(timer);
|
||||
reject(new Error(`wrangler dev failed to start: ${err.message}`));
|
||||
}
|
||||
});
|
||||
|
||||
child.on("exit", (code) => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
clearTimeout(timer);
|
||||
reject(new Error(`wrangler dev exited with code ${code}.\nstdout: ${stdout}\nstderr: ${stderr}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe("cloudflare example", () => {
|
||||
testFn(
|
||||
"starts wrangler dev and sandbox-agent responds via proxy",
|
||||
async () => {
|
||||
const { baseUrl, cleanup } = await startWranglerDev();
|
||||
try {
|
||||
// The Cloudflare example proxies requests through /sandbox/:name/proxy/*
|
||||
// Wait for the container inside the Durable Object to start sandbox-agent
|
||||
const healthUrl = `${baseUrl}/sandbox/test/proxy/v1/health`;
|
||||
|
||||
let healthy = false;
|
||||
for (let i = 0; i < 120; i++) {
|
||||
try {
|
||||
const res = await fetch(healthUrl);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
// The proxied health endpoint returns {name: "Sandbox Agent", ...}
|
||||
if (data.status === "ok" || data.name === "Sandbox Agent") {
|
||||
healthy = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
}
|
||||
expect(healthy).toBe(true);
|
||||
|
||||
// Confirm a second request also works
|
||||
const response = await fetch(healthUrl);
|
||||
expect(response.ok).toBe(true);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
},
|
||||
timeoutMs,
|
||||
);
|
||||
});
|
||||
8
examples/cloudflare/vitest.config.ts
Normal file
8
examples/cloudflare/vitest.config.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
root: ".",
|
||||
include: ["tests/**/*.test.ts"],
|
||||
},
|
||||
});
|
||||
|
|
@ -1,8 +1,15 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { buildHeaders } from "@sandbox-agent/example-shared";
|
||||
import { setupDockerSandboxAgent } from "../src/docker.ts";
|
||||
import { startDockerSandbox } from "@sandbox-agent/example-shared/docker";
|
||||
|
||||
const shouldRun = process.env.RUN_DOCKER_EXAMPLES === "1";
|
||||
/**
|
||||
* Docker integration test.
|
||||
*
|
||||
* Set SANDBOX_AGENT_DOCKER_IMAGE to the image tag to test (e.g. a locally-built
|
||||
* full image). The test starts a container from that image, waits for
|
||||
* sandbox-agent to become healthy, and validates the /v1/health endpoint.
|
||||
*/
|
||||
const image = process.env.SANDBOX_AGENT_DOCKER_IMAGE;
|
||||
const shouldRun = Boolean(image);
|
||||
const timeoutMs = Number.parseInt(process.env.SANDBOX_TEST_TIMEOUT_MS || "", 10) || 300_000;
|
||||
|
||||
const testFn = shouldRun ? it : it.skip;
|
||||
|
|
@ -11,11 +18,29 @@ describe("docker example", () => {
|
|||
testFn(
|
||||
"starts sandbox-agent and responds to /v1/health",
|
||||
async () => {
|
||||
const { baseUrl, token, cleanup } = await setupDockerSandboxAgent();
|
||||
const { baseUrl, cleanup } = await startDockerSandbox({
|
||||
port: 2468,
|
||||
image: image!,
|
||||
});
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/v1/health`, {
|
||||
headers: buildHeaders({ token }),
|
||||
});
|
||||
// Wait for health check
|
||||
let healthy = false;
|
||||
for (let i = 0; i < 60; i++) {
|
||||
try {
|
||||
const res = await fetch(`${baseUrl}/v1/health`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data.status === "ok") {
|
||||
healthy = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
}
|
||||
expect(healthy).toBe(true);
|
||||
|
||||
const response = await fetch(`${baseUrl}/v1/health`);
|
||||
expect(response.ok).toBe(true);
|
||||
const data = await response.json();
|
||||
expect(data.status).toBe("ok");
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export class PostgresSessionPersistDriver implements SessionPersistDriver {
|
|||
await this.ready();
|
||||
|
||||
const result = await this.pool.query<SessionRow>(
|
||||
`SELECT id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, sandbox_id, session_init_json
|
||||
`SELECT id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, sandbox_id, session_init_json, config_options_json, modes_json
|
||||
FROM ${this.table("sessions")}
|
||||
WHERE id = $1`,
|
||||
[id],
|
||||
|
|
@ -57,7 +57,7 @@ export class PostgresSessionPersistDriver implements SessionPersistDriver {
|
|||
const limit = normalizeLimit(request.limit);
|
||||
|
||||
const rowsResult = await this.pool.query<SessionRow>(
|
||||
`SELECT id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, sandbox_id, session_init_json
|
||||
`SELECT id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, sandbox_id, session_init_json, config_options_json, modes_json
|
||||
FROM ${this.table("sessions")}
|
||||
ORDER BY created_at ASC, id ASC
|
||||
LIMIT $1 OFFSET $2`,
|
||||
|
|
@ -79,8 +79,8 @@ export class PostgresSessionPersistDriver implements SessionPersistDriver {
|
|||
|
||||
await this.pool.query(
|
||||
`INSERT INTO ${this.table("sessions")} (
|
||||
id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, sandbox_id, session_init_json
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, sandbox_id, session_init_json, config_options_json, modes_json
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
agent = EXCLUDED.agent,
|
||||
agent_session_id = EXCLUDED.agent_session_id,
|
||||
|
|
@ -88,7 +88,9 @@ export class PostgresSessionPersistDriver implements SessionPersistDriver {
|
|||
created_at = EXCLUDED.created_at,
|
||||
destroyed_at = EXCLUDED.destroyed_at,
|
||||
sandbox_id = EXCLUDED.sandbox_id,
|
||||
session_init_json = EXCLUDED.session_init_json`,
|
||||
session_init_json = EXCLUDED.session_init_json,
|
||||
config_options_json = EXCLUDED.config_options_json,
|
||||
modes_json = EXCLUDED.modes_json`,
|
||||
[
|
||||
session.id,
|
||||
session.agent,
|
||||
|
|
@ -97,7 +99,9 @@ export class PostgresSessionPersistDriver implements SessionPersistDriver {
|
|||
session.createdAt,
|
||||
session.destroyedAt ?? null,
|
||||
session.sandboxId ?? null,
|
||||
session.sessionInit ?? null,
|
||||
session.sessionInit ? JSON.stringify(session.sessionInit) : null,
|
||||
session.configOptions ? JSON.stringify(session.configOptions) : null,
|
||||
session.modes !== undefined ? JSON.stringify(session.modes) : null,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
@ -174,7 +178,9 @@ export class PostgresSessionPersistDriver implements SessionPersistDriver {
|
|||
created_at BIGINT NOT NULL,
|
||||
destroyed_at BIGINT,
|
||||
sandbox_id TEXT,
|
||||
session_init_json JSONB
|
||||
session_init_json JSONB,
|
||||
config_options_json JSONB,
|
||||
modes_json JSONB
|
||||
)
|
||||
`);
|
||||
|
||||
|
|
@ -183,6 +189,16 @@ export class PostgresSessionPersistDriver implements SessionPersistDriver {
|
|||
ADD COLUMN IF NOT EXISTS sandbox_id TEXT
|
||||
`);
|
||||
|
||||
await this.pool.query(`
|
||||
ALTER TABLE ${this.table("sessions")}
|
||||
ADD COLUMN IF NOT EXISTS config_options_json JSONB
|
||||
`);
|
||||
|
||||
await this.pool.query(`
|
||||
ALTER TABLE ${this.table("sessions")}
|
||||
ADD COLUMN IF NOT EXISTS modes_json JSONB
|
||||
`);
|
||||
|
||||
await this.pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS ${this.table("events")} (
|
||||
id TEXT PRIMARY KEY,
|
||||
|
|
@ -238,6 +254,8 @@ type SessionRow = {
|
|||
destroyed_at: string | number | null;
|
||||
sandbox_id: string | null;
|
||||
session_init_json: unknown | null;
|
||||
config_options_json: unknown | null;
|
||||
modes_json: unknown | null;
|
||||
};
|
||||
|
||||
type EventRow = {
|
||||
|
|
@ -260,6 +278,8 @@ function decodeSessionRow(row: SessionRow): SessionRecord {
|
|||
destroyedAt: row.destroyed_at === null ? undefined : parseInteger(row.destroyed_at),
|
||||
sandboxId: row.sandbox_id ?? undefined,
|
||||
sessionInit: row.session_init_json ? (row.session_init_json as SessionRecord["sessionInit"]) : undefined,
|
||||
configOptions: row.config_options_json ? (row.config_options_json as SessionRecord["configOptions"]) : undefined,
|
||||
modes: row.modes_json ? (row.modes_json as SessionRecord["modes"]) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export class SQLiteSessionPersistDriver implements SessionPersistDriver {
|
|||
async getSession(id: string): Promise<SessionRecord | undefined> {
|
||||
const row = this.db
|
||||
.prepare(
|
||||
`SELECT id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, sandbox_id, session_init_json
|
||||
`SELECT id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, sandbox_id, session_init_json, config_options_json, modes_json
|
||||
FROM sessions WHERE id = ?`,
|
||||
)
|
||||
.get(id) as SessionRow | undefined;
|
||||
|
|
@ -36,7 +36,7 @@ export class SQLiteSessionPersistDriver implements SessionPersistDriver {
|
|||
|
||||
const rows = this.db
|
||||
.prepare(
|
||||
`SELECT id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, sandbox_id, session_init_json
|
||||
`SELECT id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, sandbox_id, session_init_json, config_options_json, modes_json
|
||||
FROM sessions
|
||||
ORDER BY created_at ASC, id ASC
|
||||
LIMIT ? OFFSET ?`,
|
||||
|
|
@ -56,8 +56,8 @@ export class SQLiteSessionPersistDriver implements SessionPersistDriver {
|
|||
this.db
|
||||
.prepare(
|
||||
`INSERT INTO sessions (
|
||||
id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, sandbox_id, session_init_json
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
id, agent, agent_session_id, last_connection_id, created_at, destroyed_at, sandbox_id, session_init_json, config_options_json, modes_json
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
agent = excluded.agent,
|
||||
agent_session_id = excluded.agent_session_id,
|
||||
|
|
@ -65,7 +65,9 @@ export class SQLiteSessionPersistDriver implements SessionPersistDriver {
|
|||
created_at = excluded.created_at,
|
||||
destroyed_at = excluded.destroyed_at,
|
||||
sandbox_id = excluded.sandbox_id,
|
||||
session_init_json = excluded.session_init_json`,
|
||||
session_init_json = excluded.session_init_json,
|
||||
config_options_json = excluded.config_options_json,
|
||||
modes_json = excluded.modes_json`,
|
||||
)
|
||||
.run(
|
||||
session.id,
|
||||
|
|
@ -76,6 +78,8 @@ export class SQLiteSessionPersistDriver implements SessionPersistDriver {
|
|||
session.destroyedAt ?? null,
|
||||
session.sandboxId ?? null,
|
||||
session.sessionInit ? JSON.stringify(session.sessionInit) : null,
|
||||
session.configOptions ? JSON.stringify(session.configOptions) : null,
|
||||
session.modes !== undefined ? JSON.stringify(session.modes) : null,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -134,7 +138,9 @@ export class SQLiteSessionPersistDriver implements SessionPersistDriver {
|
|||
created_at INTEGER NOT NULL,
|
||||
destroyed_at INTEGER,
|
||||
sandbox_id TEXT,
|
||||
session_init_json TEXT
|
||||
session_init_json TEXT,
|
||||
config_options_json TEXT,
|
||||
modes_json TEXT
|
||||
)
|
||||
`);
|
||||
|
||||
|
|
@ -142,6 +148,12 @@ export class SQLiteSessionPersistDriver implements SessionPersistDriver {
|
|||
if (!sessionColumns.some((column) => column.name === "sandbox_id")) {
|
||||
this.db.exec(`ALTER TABLE sessions ADD COLUMN sandbox_id TEXT`);
|
||||
}
|
||||
if (!sessionColumns.some((column) => column.name === "config_options_json")) {
|
||||
this.db.exec(`ALTER TABLE sessions ADD COLUMN config_options_json TEXT`);
|
||||
}
|
||||
if (!sessionColumns.some((column) => column.name === "modes_json")) {
|
||||
this.db.exec(`ALTER TABLE sessions ADD COLUMN modes_json TEXT`);
|
||||
}
|
||||
|
||||
this.ensureEventsTable();
|
||||
}
|
||||
|
|
@ -233,6 +245,8 @@ type SessionRow = {
|
|||
destroyed_at: number | null;
|
||||
sandbox_id: string | null;
|
||||
session_init_json: string | null;
|
||||
config_options_json: string | null;
|
||||
modes_json: string | null;
|
||||
};
|
||||
|
||||
type EventRow = {
|
||||
|
|
@ -260,6 +274,8 @@ function decodeSessionRow(row: SessionRow): SessionRecord {
|
|||
destroyedAt: row.destroyed_at ?? undefined,
|
||||
sandboxId: row.sandbox_id ?? undefined,
|
||||
sessionInit: row.session_init_json ? (JSON.parse(row.session_init_json) as SessionRecord["sessionInit"]) : undefined,
|
||||
configOptions: row.config_options_json ? (JSON.parse(row.config_options_json) as SessionRecord["configOptions"]) : undefined,
|
||||
modes: row.modes_json ? (JSON.parse(row.modes_json) as SessionRecord["modes"]) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,11 +78,11 @@ function readClaudeCredentialFiles(): ClaudeCredentialFile[] {
|
|||
const candidates: Array<{ hostPath: string; containerPath: string }> = [
|
||||
{
|
||||
hostPath: path.join(homeDir, ".claude", ".credentials.json"),
|
||||
containerPath: "/root/.claude/.credentials.json",
|
||||
containerPath: ".claude/.credentials.json",
|
||||
},
|
||||
{
|
||||
hostPath: path.join(homeDir, ".claude-oauth-credentials.json"),
|
||||
containerPath: "/root/.claude-oauth-credentials.json",
|
||||
containerPath: ".claude-oauth-credentials.json",
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -180,10 +180,9 @@ export async function startDockerSandbox(opts: DockerSandboxOptions): Promise<Do
|
|||
const credentialBootstrapCommands = claudeCredentialFiles.flatMap((file, index) => {
|
||||
const envKey = `SANDBOX_AGENT_CLAUDE_CREDENTIAL_${index}_B64`;
|
||||
bootstrapEnv[envKey] = file.base64Content;
|
||||
return [
|
||||
`mkdir -p ${shellSingleQuotedLiteral(path.posix.dirname(file.containerPath))}`,
|
||||
`printf %s "$${envKey}" | base64 -d > ${shellSingleQuotedLiteral(file.containerPath)}`,
|
||||
];
|
||||
// Use $HOME-relative paths so credentials work regardless of container user
|
||||
const containerDir = path.posix.dirname(file.containerPath);
|
||||
return [`mkdir -p "$HOME/${containerDir}"`, `printf %s "$${envKey}" | base64 -d > "$HOME/${file.containerPath}"`];
|
||||
});
|
||||
setupCommands.unshift(...credentialBootstrapCommands);
|
||||
}
|
||||
|
|
@ -200,8 +199,9 @@ export async function startDockerSandbox(opts: DockerSandboxOptions): Promise<Do
|
|||
|
||||
const container = await docker.createContainer({
|
||||
Image: image,
|
||||
Entrypoint: ["/bin/sh", "-c"],
|
||||
WorkingDir: "/home/sandbox",
|
||||
Cmd: ["sh", "-c", bootCommands.join(" && ")],
|
||||
Cmd: [bootCommands.join(" && ")],
|
||||
Env: [...Object.entries(credentialEnv).map(([key, value]) => `${key}=${value}`), ...Object.entries(bootstrapEnv).map(([key, value]) => `${key}=${value}`)],
|
||||
ExposedPorts: { [`${port}/tcp`]: {} },
|
||||
HostConfig: {
|
||||
|
|
@ -253,10 +253,13 @@ export async function startDockerSandbox(opts: DockerSandboxOptions): Promise<Do
|
|||
try {
|
||||
await container.remove({ force: true });
|
||||
} catch {}
|
||||
};
|
||||
const signalCleanup = async () => {
|
||||
await cleanup();
|
||||
process.exit(0);
|
||||
};
|
||||
process.once("SIGINT", cleanup);
|
||||
process.once("SIGTERM", cleanup);
|
||||
process.once("SIGINT", signalCleanup);
|
||||
process.once("SIGTERM", signalCleanup);
|
||||
|
||||
return { baseUrl, cleanup };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,6 +141,8 @@ type SessionRow = {
|
|||
destroyedAt?: number;
|
||||
sandboxId?: string;
|
||||
sessionInit?: SessionRecord["sessionInit"];
|
||||
configOptions?: SessionRecord["configOptions"];
|
||||
modes?: SessionRecord["modes"];
|
||||
};
|
||||
|
||||
type EventRow = {
|
||||
|
|
@ -163,6 +165,8 @@ function encodeSessionRow(session: SessionRecord): SessionRow {
|
|||
destroyedAt: session.destroyedAt,
|
||||
sandboxId: session.sandboxId,
|
||||
sessionInit: session.sessionInit,
|
||||
configOptions: session.configOptions,
|
||||
modes: session.modes,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -176,6 +180,8 @@ function decodeSessionRow(row: SessionRow): SessionRecord {
|
|||
destroyedAt: row.destroyedAt,
|
||||
sandboxId: row.sandboxId,
|
||||
sessionInit: row.sessionInit,
|
||||
configOptions: row.configOptions,
|
||||
modes: row.modes,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,4 +2,4 @@
|
|||
|
||||
> **Deprecated:** This package has been deprecated and removed.
|
||||
|
||||
Copy the driver source directly into your project. See the [session persistence docs](https://sandboxagent.dev/session-persistence) for guidance.
|
||||
Copy the driver source into your project. See the [reference implementation](https://github.com/rivet-dev/sandbox-agent/tree/main/frontend/packages/inspector/src/persist-indexeddb.ts) and the [session persistence docs](https://sandboxagent.dev/session-persistence) for guidance.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@sandbox-agent/persist-indexeddb",
|
||||
"version": "0.3.2",
|
||||
"description": "IndexedDB persistence driver for the Sandbox Agent TypeScript SDK",
|
||||
"description": "IndexedDB persistence driver for the Sandbox Agent TypeScript SDK (DEPRECATED)",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
throw new Error(
|
||||
"@sandbox-agent/persist-indexeddb has been deprecated and removed. " +
|
||||
"Copy the reference implementation into your project instead. " +
|
||||
"See https://github.com/nichochar/sandbox-agent/tree/main/examples/persist-indexeddb",
|
||||
"Copy the reference implementation from frontend/packages/inspector/src/persist-indexeddb.ts into your project instead. " +
|
||||
"See https://github.com/rivet-dev/sandbox-agent/tree/main/frontend/packages/inspector/src/persist-indexeddb.ts",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# @sandbox-agent/persist-postgres
|
||||
|
||||
> **Deprecated:** This package has been deprecated and removed. The implementation now lives as a copy-paste reference in [`examples/persist-postgres`](../../examples/persist-postgres).
|
||||
> **Deprecated:** This package has been deprecated and removed.
|
||||
|
||||
Install `pg` directly and copy the driver source into your project. See the [full example](https://github.com/nichochar/sandbox-agent/tree/main/examples/persist-postgres).
|
||||
Install `pg` directly and copy the driver source into your project. See the [full example](https://github.com/rivet-dev/sandbox-agent/tree/main/examples/persist-postgres) and the [session persistence docs](https://sandboxagent.dev/session-persistence) for guidance.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
throw new Error(
|
||||
"@sandbox-agent/persist-postgres has been deprecated and removed. " +
|
||||
"Copy the reference implementation from examples/persist-postgres into your project instead. " +
|
||||
"See https://github.com/nichochar/sandbox-agent/tree/main/examples/persist-postgres",
|
||||
"See https://github.com/rivet-dev/sandbox-agent/tree/main/examples/persist-postgres",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,4 +2,4 @@
|
|||
|
||||
> **Deprecated:** This package has been deprecated and removed.
|
||||
|
||||
Copy the driver source directly into your project. See the [session persistence docs](https://sandboxagent.dev/session-persistence) for guidance.
|
||||
Copy the driver source into your project. See the [multiplayer docs](https://github.com/rivet-dev/sandbox-agent/tree/main/docs/multiplayer.mdx) and the [session persistence docs](https://sandboxagent.dev/session-persistence) for guidance.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@sandbox-agent/persist-rivet",
|
||||
"version": "0.3.2",
|
||||
"description": "Rivet Actor persistence driver for the Sandbox Agent TypeScript SDK",
|
||||
"description": "Rivet Actor persistence driver for the Sandbox Agent TypeScript SDK (DEPRECATED)",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
throw new Error(
|
||||
"@sandbox-agent/persist-rivet has been deprecated and removed. " +
|
||||
"Copy the reference implementation into your project instead. " +
|
||||
"See https://github.com/nichochar/sandbox-agent/tree/main/examples/persist-rivet",
|
||||
"Copy the reference implementation from docs/multiplayer.mdx into your project instead. " +
|
||||
"See https://github.com/rivet-dev/sandbox-agent/tree/main/docs/multiplayer.mdx",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# @sandbox-agent/persist-sqlite
|
||||
|
||||
> **Deprecated:** This package has been deprecated and removed. The implementation now lives as a copy-paste reference in [`examples/persist-sqlite`](../../examples/persist-sqlite).
|
||||
> **Deprecated:** This package has been deprecated and removed.
|
||||
|
||||
Install `better-sqlite3` directly and copy the driver source into your project. See the [full example](https://github.com/nichochar/sandbox-agent/tree/main/examples/persist-sqlite).
|
||||
Install `better-sqlite3` directly and copy the driver source into your project. See the [full example](https://github.com/rivet-dev/sandbox-agent/tree/main/examples/persist-sqlite) and the [session persistence docs](https://sandboxagent.dev/session-persistence) for guidance.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@sandbox-agent/persist-sqlite",
|
||||
"version": "0.3.2",
|
||||
"description": "SQLite persistence driver for the Sandbox Agent TypeScript SDK",
|
||||
"description": "SQLite persistence driver for the Sandbox Agent TypeScript SDK (DEPRECATED)",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
throw new Error(
|
||||
"@sandbox-agent/persist-sqlite has been deprecated and removed. " +
|
||||
"Copy the reference implementation from examples/persist-sqlite into your project instead. " +
|
||||
"See https://github.com/nichochar/sandbox-agent/tree/main/examples/persist-sqlite",
|
||||
"See https://github.com/rivet-dev/sandbox-agent/tree/main/examples/persist-sqlite",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ const HEALTH_WAIT_MIN_DELAY_MS = 500;
|
|||
const HEALTH_WAIT_MAX_DELAY_MS = 15_000;
|
||||
const HEALTH_WAIT_LOG_AFTER_MS = 5_000;
|
||||
const HEALTH_WAIT_LOG_EVERY_MS = 10_000;
|
||||
const HEALTH_WAIT_ENSURE_SERVER_AFTER_FAILURES = 3;
|
||||
|
||||
export interface SandboxAgentHealthWaitOptions {
|
||||
timeoutMs?: number;
|
||||
|
|
@ -903,7 +904,7 @@ export class SandboxAgent {
|
|||
const createdSandbox = !existingSandbox;
|
||||
|
||||
if (existingSandbox) {
|
||||
await provider.wake?.(rawSandboxId);
|
||||
await provider.ensureServer?.(rawSandboxId);
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -2118,6 +2119,7 @@ export class SandboxAgent {
|
|||
let delayMs = HEALTH_WAIT_MIN_DELAY_MS;
|
||||
let nextLogAt = startedAt + HEALTH_WAIT_LOG_AFTER_MS;
|
||||
let lastError: unknown;
|
||||
let consecutiveFailures = 0;
|
||||
|
||||
while (!this.disposed && (deadline === undefined || Date.now() < deadline)) {
|
||||
throwIfAborted(signal);
|
||||
|
|
@ -2128,11 +2130,22 @@ export class SandboxAgent {
|
|||
return;
|
||||
}
|
||||
lastError = new Error(`Unexpected health response: ${JSON.stringify(health)}`);
|
||||
consecutiveFailures++;
|
||||
} catch (error) {
|
||||
if (isAbortError(error)) {
|
||||
throw error;
|
||||
}
|
||||
lastError = error;
|
||||
consecutiveFailures++;
|
||||
}
|
||||
|
||||
if (consecutiveFailures >= HEALTH_WAIT_ENSURE_SERVER_AFTER_FAILURES && this.sandboxProvider?.ensureServer && this.sandboxProviderRawId) {
|
||||
try {
|
||||
await this.sandboxProvider.ensureServer(this.sandboxProviderRawId);
|
||||
} catch {
|
||||
// Best-effort; the next health check will determine if it worked.
|
||||
}
|
||||
consecutiveFailures = 0;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
|
|
|||
|
|
@ -49,5 +49,12 @@ export function computesdk(options: ComputeSdkProviderOptions = {}): SandboxProv
|
|||
if (!sandbox) throw new Error(`computesdk sandbox not found: ${sandboxId}`);
|
||||
return sandbox.getUrl({ port: agentPort });
|
||||
},
|
||||
async ensureServer(sandboxId: string): Promise<void> {
|
||||
const sandbox = await compute.sandbox.getById(sandboxId);
|
||||
if (!sandbox) throw new Error(`computesdk sandbox not found: ${sandboxId}`);
|
||||
await sandbox.runCommand(`sandbox-agent server --no-token --host 0.0.0.0 --port ${agentPort}`, {
|
||||
background: true,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export function daytona(options: DaytonaProviderOptions = {}): SandboxProvider {
|
|||
const preview = await sandbox.getSignedPreviewUrl(agentPort, previewTtlSeconds);
|
||||
return typeof preview === "string" ? preview : preview.url;
|
||||
},
|
||||
async wake(sandboxId: string): Promise<void> {
|
||||
async ensureServer(sandboxId: string): Promise<void> {
|
||||
const sandbox = await client.get(sandboxId);
|
||||
if (!sandbox) {
|
||||
throw new Error(`daytona sandbox not found: ${sandboxId}`);
|
||||
|
|
|
|||
|
|
@ -53,5 +53,10 @@ export function e2b(options: E2BProviderOptions = {}): SandboxProvider {
|
|||
const sandbox = await Sandbox.connect(sandboxId, connectOpts as any);
|
||||
return `https://${sandbox.getHost(agentPort)}`;
|
||||
},
|
||||
async ensureServer(sandboxId: string): Promise<void> {
|
||||
const connectOpts = await resolveOptions(options.connect, sandboxId);
|
||||
const sandbox = await Sandbox.connect(sandboxId, connectOpts as any);
|
||||
await sandbox.commands.run(`sandbox-agent server --no-token --host 0.0.0.0 --port ${agentPort}`, { background: true, timeoutMs: 0 });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,5 +66,9 @@ export function modal(options: ModalProviderOptions = {}): SandboxProvider {
|
|||
}
|
||||
return tunnel.url;
|
||||
},
|
||||
async ensureServer(sandboxId: string): Promise<void> {
|
||||
const sb = await client.sandboxes.fromId(sandboxId);
|
||||
sb.exec(["sandbox-agent", "server", "--no-token", "--host", "0.0.0.0", "--port", String(agentPort)]);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,8 +21,11 @@ export interface SandboxProvider {
|
|||
getFetch?(sandboxId: string): Promise<typeof globalThis.fetch>;
|
||||
|
||||
/**
|
||||
* Optional hook invoked before reconnecting to an existing sandbox.
|
||||
* Useful for providers where the sandbox-agent process may need to be restarted.
|
||||
* Ensure the sandbox-agent server process is running inside the sandbox.
|
||||
* Called during health-wait after consecutive failures, and before
|
||||
* reconnecting to an existing sandbox. Implementations should be
|
||||
* idempotent — if the server is already running, this should be a no-op
|
||||
* (e.g. the duplicate process exits on port conflict).
|
||||
*/
|
||||
wake?(sandboxId: string): Promise<void>;
|
||||
ensureServer?(sandboxId: string): Promise<void>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,5 +53,13 @@ export function vercel(options: VercelProviderOptions = {}): SandboxProvider {
|
|||
const sandbox = await Sandbox.get({ sandboxId });
|
||||
return sandbox.domain(agentPort);
|
||||
},
|
||||
async ensureServer(sandboxId: string): Promise<void> {
|
||||
const sandbox = await Sandbox.get({ sandboxId });
|
||||
await sandbox.runCommand({
|
||||
cmd: "sandbox-agent",
|
||||
args: ["server", "--no-token", "--host", "0.0.0.0", "--port", String(agentPort)],
|
||||
detached: true,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -415,7 +415,7 @@ impl AcpProxyRuntime {
|
|||
|
||||
async fn is_ready(&self, agent: AgentId) -> bool {
|
||||
if agent == AgentId::Mock {
|
||||
return self.inner.agent_manager.agent_process_path(agent).exists();
|
||||
return true;
|
||||
}
|
||||
self.inner.agent_manager.is_installed(agent)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue