@giselles-ai/sandkit
v0.1.2
Published
Workspace state, session management, and durable command execution for Vercel Sandbox.
Readme
Sandkit
Sandkit makes Vercel Sandbox stateful.
Sandkit adds workspace state, session management, durable command execution, and resumable sandbox workflows to Vercel Sandbox.
It keeps two paths explicit:
workspace.sandbox.runCommand(...)for durable, one-command-at-a-time workopenSession()/attachSession()for a live leased sandbox when you need an interactive process
An active session is an exclusive workspace lease. While a live session is open, runCommand() is unavailable until you attach to that session or commit it.
Durable lock enforcement is currently in-process (packages/sandkit only). Concurrent durable commands for the same workspace are excluded while one is in flight in the same process.
Provider-specific behavior still matters, but the public API stays centered on workspaces, policies, and durable state.
Problem
Vercel Sandbox is ephemeral by design. It does not give you durable workspaces, session lifecycle, or a clear boundary between one-shot commands and live attached execution.
- No built-in workspace identity or durable workspace state
- No session management abstraction for live attach / resume
- No durable command boundary for one-command-at-a-time work
- Teams end up rebuilding the same sandbox state and lifecycle layer around jobs, agents, and recovery flows
Solution
Sandkit adds a workspace state layer on top of Vercel Sandbox:
- Persistent workspaces for sandbox state management
- Live session lifecycle with explicit attach / commit semantics
- Durable command execution through
runCommand(...)for committed work - Policy controls that stay part of workspace state
- Resumable sandbox workflows for long-running apps and control planes
Positioning
| Tool | Responsibility | | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | Vercel Sandbox | Ephemeral execution environment | | Sandkit | State and lifecycle layer for sandboxed execution: workspaces, session lifecycle, durable command boundaries, and resume | | Workflow engines / app control planes | Decide when and why work runs |
Primary Use Case: Persistent Workspaces for AI Coding Agents
Sandkit is especially useful when an agent or long-running sandbox app needs to keep a workspace alive across runs, attach to a live process, expose a public URL, and commit progress durably.
Install
npm install @giselles-ai/sandkitWith Drizzle:
npm install @giselles-ai/sandkit drizzle-ormQuick Start
Migration note:
sandkit(...)was renamed tocreateSandkit(...)and this package is not yet aliased. Callers must update imports and call sites fromsandkittocreateSandkit.
import { Database } from "bun:sqlite";
import { createSandkit } from "@giselles-ai/sandkit";
import { createBunSqliteAdapter } from "@giselles-ai/sandkit/adapters/sqlite-bun";
import { vercelSandbox } from "@giselles-ai/sandkit/integrations/vercel";
const database = new Database("./sandkit.sqlite");
const workspaceAdapter = createBunSqliteAdapter(database);
const sandkit = createSandkit({
database: workspaceAdapter,
sandbox: vercelSandbox({
defaultTimeout: 60_000,
}),
});
const workspace = await sandkit.createWorkspace({
name: "hello-sandkit",
});
await workspace.sandbox.runCommand({
command: "sh",
args: ["-lc", "echo 'hello world' > ./hello.txt"],
});
const result = await workspace.sandbox.runCommand({
command: "cat",
args: ["./hello.txt"],
});
console.log(result.stdout.trim());runCommand(...) (without detached) resolves to CommandResult only after the full unit-of-work is complete: process exit, snapshot/commit, and persist.
For detached execution, pass detached: true and you get a Command object. Observe logs and then await durable completion:
const command = await workspace.sandbox.runCommand({
command: "sh",
args: ["-lc", "echo 'start'; sleep 1; echo 'done'"],
detached: true,
});
for await (const chunk of command.logs?.() ?? []) {
console.log(`${chunk.stream}: ${chunk.chunk}`);
}
const commandResult = await command.wait();Set VERCEL_OIDC_TOKEN for local runs or VERCEL_ACCESS_TOKEN in CI before creating a Vercel-backed sandbox.
Declare exposedPorts on createWorkspace({ sandbox: ... }) only when you need a live session URL. defaultTimeout is the provider-level lease default; override a specific live session with openSession({ timeoutMs }).
Setup bootstrap
setup is the shared bootstrap definition, not the materialized artifact.
It is optional.
When setup is provided, it is the shared bootstrap command, args, and required durable policy.
This produces adapter-scoped shared bootstrap state keyed by adapter.id + setup definition fingerprint.
Multiple Sandkit instances using the same adapter and setup definition can reuse the same shared bootstrap state.
sandkit.bootstrap() is an optional eager materialization step:
- it creates shared bootstrap state if missing,
- it leaves existing shared bootstrap state untouched,
- it does not run a restore path to prove an existing shared bootstrap state is still usable.
Without bootstrap(), shared setup is still materialized lazily on first workspace use (first runCommand(...) or openSession(...) that needs it).
Stale or unusable shared bootstrap artifacts are detected and rebuilt in those workspace flows, not by bootstrap() alone.
setup durability is adapter-backed. With a persistent adapter such as Bun SQLite or Drizzle, the shared bootstrap survives process restarts. With the default in-memory adapter, it does not.
import { createSandkit, allowAll } from "@giselles-ai/sandkit";
import { vercelSandbox } from "@giselles-ai/sandkit/integrations/vercel";
const sandkit = createSandkit({
sandbox: vercelSandbox(),
setup: {
command: "sh",
args: ["-lc", "npm ci"],
policy: allowAll(),
},
});
await sandkit.bootstrap();
// Optional: omit bootstrap() and let setup materialize on first workspace use.
const workspace = await sandkit.createWorkspace({
name: "bootstrapped-workspace",
});Configuration
Provide a sandbox provider explicitly (for example vercelSandbox(...)).
If you do not pass database, Sandkit defaults to the in-memory adapter. That default is useful for local tests and internal development, but the primary published usage is an explicit Vercel provider plus a persistent adapter.
Policies
npm()allows the public npm registry hostregistry.npmjs.orgbun()allows Bun install/distribution hostsbun.shandbun.comcodex()readsCODEX_API_KEYgemini()readsGEMINI_API_KEYgithub()readsGITHUB_TOKENand maps it through Vercel Sandbox firewall transforms:Authorization: Basic <base64(x-access-token:<token>)>on requests togithub.com, intended for Git-over-HTTPS operationsAuthorization: Bearer <token>on requests toapi.github.com- no Authorization header for
*.githubusercontent.com
aiGateway()readsAI_GATEWAY_API_KEYfrom host env and allows the hostname (plus wildcard) fromAI_GATEWAY_BASE_URL.AI_GATEWAY_BASE_URLports are ignored for allow-listing; only host/domain matches are used.
For JavaScript package bootstrap, prefer explicit service presets over allowAll():
import { allowServices, bun, npm } from "@giselles-ai/sandkit";
const policy = allowServices([bun(), npm()]);Durable default policy belongs to the workspace: use createWorkspace({ policy: ... }) when you create it, or workspace.setPolicy(...) later. Pass policy to runCommand(...) for one-off overrides.
Schema Generation
npx @giselles-ai/sandkit generate --adapter drizzle --provider sqliteIf the project already has a Drizzle setup, provider discovery can infer the dialect:
npx @giselles-ai/sandkit generateExamples
examples/workflow-hello-gitexamples/sandbox-openclawsmoke/drizzle-sample
Repository: github.com/giselles-ai/sandkit
