← stitchkitDocumentation

Optional process sandbox

stitchkit/agent-runtime/sandbox is a server-only, evolving API. Existing coding tools and agent configuration retain their current behavior. A host explicitly creates a session and opts into createSandboxCodingTools; importing this entrypoint does not redirect existing coding tools.

import { createBubblewrapSandboxBackend } from 'stitchkit/agent-runtime/sandbox';

const backend = await createBubblewrapSandboxBackend({
  stateDirectory: '/var/lib/my-app/sandboxes',
  maxConcurrentCommands: 8,
  onBrokerError: (cause) => console.error('Internal sandbox gateway error', cause),
});
const template = await backend.prewarm({
  template: 'agent-workspace',
  files: { 'hello.txt': new TextEncoder().encode('hello') },
});
const handle = await backend.create({ template: template.templateKey, network: 'deny-all' });
try {
  const result = await handle.session.run({ executable: '/usr/bin/cat', args: ['hello.txt'] });
  console.log(result.stdout);
  const state = handle.captureState(); // Persist this descriptor in the host's store.
  await handle.stop();
  const resumed = await backend.create({ template: template.templateKey, state, network: 'deny-all' });
  try { console.log(await resumed.session.readTextFile('hello.txt')); }
  finally { await resumed.delete(); }
} finally { await handle.shutdown(); }

prewarm snapshots the supplied byte files. Its content-addressed key includes the backend name, template name and file contents. It is not a VM image or a package installer. stop and shutdown kill live commands and retain workspace files; delete removes session state and workspace, preserving reusable templates. Reconnect requires the same backend name and template. A live owner holds an exclusive lease: concurrent attach and deletion by an older handle fail with SANDBOX_BUSY. After a host crash, an operator must verify that its processes are gone before removing a stale lease; automatic crash recovery is not provided. Storage is process-durable, without a power-loss/fsync guarantee.

Coding tool integration

import { createSandboxCodingTools } from 'stitchkit/agent-runtime/sandbox';
import { mountAgent } from 'stitchkit/tools';

const tools = mountAgent([], {
  runtimeTools: createSandboxCodingTools(handle, {
    authorize: (operation) => applicationPolicy(operation),
    executables: { shell: '/usr/bin/sh' },
    requiredRestrictions: ['network-denied', 'write-contained', 'process-contained', 'secrets-hidden'],
    limits: { shellTimeoutMs: 10_000, maxShellOutputBytes: 65_536 },
  }),
});

The factory uses the existing coding profile, including its authorization, path containment, UTF-8 handling, output artifacts and command limits. File tools access the same workspace through the host’s contained-file implementation; commands run inside the namespace with /workspace as their root. The host-only handle.coding binding never belongs in model input or persisted state. Unsupported backends omit the binding and the factory refuses explicitly. When the adapter’s probe() or prepare() fails, the agent gets SANDBOX_UNAVAILABLE with a fixed reason; the adapter’s own error is the refusal’s cause, delivered to the mount’s hooks.onToolError, which is where an application logs it.

Stop also kills coding commands. Both launch paths share the configured maxConcurrentCommands admission limit (default 8); an occupied slot gives SANDBOX_BUSY before another process starts. This counts direct commands, not guest descendants. A direct launch retains its slot through terminal group cleanup, including descendants that outlive the leader. Required restrictions are checked again during preparation; a network-policy change invalidates an outstanding prepared launch. After reconnect, create the coding profile from the new handle; old tools remain stopped.

Network gateway

deny-all isolates the network namespace, including DNS and loopback access to host services. { allow: [{ origin, headers }] } retains that isolation and allows HTTP through a Unix-socket gateway only. Origins are exact HTTP(S) origins, including scheme and port; no wildcard, redirect, arbitrary TCP or CONNECT tunnel is allowed. For HTTPS upstreams the host establishes TLS. Guests speak HTTP over the socket with the upstream host in Host, for example:

await handle.session.setNetworkPolicy({
  allow: [{ origin: 'https://api.example.com', headers: { authorization: hostCredential } }],
});
await handle.session.run({
  executable: '/usr/bin/curl',
  args: ['--unix-socket', '/run/stitchkit-network.sock', 'http://api.example.com/resource'],
});

The host obtains hostCredential from its credential store. Headers never enter guest environment, process arguments or persisted reconnect state. Authorized upstreams are trusted: an upstream that reflects its request headers can disclose them in its response. This gateway cannot prevent that. Header values are fixed for the policy; replace the policy to rotate credentials. Policy changes require all commands to finish or stop (SANDBOX_BUSY otherwise). allow-all explicitly shares host networking and adds no injected headers; use it only when unrestricted egress is intended. Policies are resupplied on every reconnect and are not stored.

Runtime boundary

The reference backend requires Linux, /usr/bin/bwrap, usable namespaces and a conventional /usr, /lib, /lib64 layout. Failure to establish isolation gives SANDBOX_UNAVAILABLE; there is no host-execution fallback. Runtime directories are read-only, the workspace is writable, environment is cleared, and host home, state storage and other host sockets are not mounted. Each command gets ephemeral /tmp; only /workspace persists. Runtime binaries and the configured state directory belong to the trusted host. See the Bubblewrap security model.

This is namespace isolation over the host kernel, not a VM. It does not impose memory, CPU, disk or process-count quotas. A host running mutually untrusted workloads must supply those bounds outside the namespace (for example, a delegated cgroup and quota-controlled storage); command admission is not a substitute. Commands default to a 30-second deadline and 1 MiB combined output; callers can configure both. File I/O is bounded to 1 MiB. The gateway allows 16 concurrent requests, 1 MiB request/response bodies and a 30-second deadline. Do not treat these bounds as resource containment for arbitrary hostile workloads.

Custom backends implement SandboxDriver byte I/O, spawn and policy enforcement; createSandboxSession supplies text conversion, paths and run once. Paths are workspace-relative or canonical /workspace/... paths. Unsupported policies must be refused explicitly by the backend.