kosa8 docs

Guides / JavaScript & TypeScript / Sandboxes

Sandboxes

A sandbox is a microVM with its own kernel, its own container engine, its own disk and its own network. This page covers every option createSandbox takes and every other call in a sandbox's life, each with the output it produced.

Create one

import { Kosa8Client } from '@kosa8/sdk';

const kosa8 = new Kosa8Client();

const { sandbox, boot_ms } = await kosa8.createSandbox({
  name: 'web',
  cpus: 2,
  memMb: 1024,      // MiB of guest memory
  diskGb: 20,       // the data disk: images, layers, everything written inside
  egress: 'deny',
  labels: { team: 'payments', ticket: 'PAY-142' },
});
console.log(`booted ${sandbox.id} in ${boot_ms}ms`);
console.log(sandbox);

console.log('list:', (await kosa8.listSandboxes()).map((s) => `${s.id}=${s.state}`));
console.log('get web:', (await kosa8.getSandbox('web'))?.state);
console.log('get nope:', await kosa8.getSandbox('nope'));
$ node sandboxes1.mjs
booted web in 1164ms
{
  id: 'web',
  name: 'web',
  created: '2026-10-15T19:57:29.810605-04:00',
  state: 'running',
  cpus: 2,
  mem_mb: 1024,
  disk_gb: 20,
  egress: 'deny',
  ip: '192.168.127.2',
  labels: { team: 'payments', ticket: 'PAY-142' }
}
list: [ 'web=running' ]
get web: running
get nope: undefined

createSandbox resolves when the machine has booted. The result is { sandbox, boot_ms, broker? }. The sandbox record uses the daemon's own field names (mem_mb, disk_gb), so it matches what kosa8 sandbox ls --json prints. There is no single-sandbox endpoint: getSandbox reads the list and returns undefined for a name it does not find.

Every create option

OptionTypeWhat it does
namestringThe sandbox's name, which is also its id. The daemon picks one (sbx1, sbx2, …) when you leave it out. A name already in use is refused.
cpusnumberVirtual CPUs. The daemon default is 2.
memMbnumberGuest memory in MiB. The daemon default is 1024. A checkpoint saves this memory to disk, so smaller is cheaper to save.
diskGbnumberSize of the data disk, which holds images, layers and everything written inside. The daemon default is 32. The file is sparse: it takes real space only as it fills. Your licence sets the ceiling.
mountsMount[]Host folders to share: { host_path, guest_path, read_only }. Every container started in the sandbox sees them at guest_path. Below.
egressEgressPolicyThe network rule: 'deny', 'any', 'fakenet', 'allow:a.com,*.b.com,10.0.0.0/8', or { allow: [...] }. Fixed for the sandbox's life. Left out, the org policy's default_egress applies, or any when there is none. See Network and secrets.
egressAllowstring[]Shorthand for egress: { allow: [...] }. Setting it together with an egress other than 'deny' throws before anything is sent.
captureCaptureSpecRecord the sandbox's traffic to a pcapng file on the host from the first frame: { path, max_bytes?, max_files? }. The path must be absolute.
labelsRecord<string,string>Free-form key–value labels, kept on the sandbox record and shown by kosa8 sandbox ls --json.
brokerBrokerSpecPut the sandbox behind an egress broker that adds credentials the sandbox never holds. See the broker.
approval, signalstring, AbortSignalAccepted by every call. approval is a signed-approval id (Approvals); signal cancels the HTTP request.

Share a host folder

import { Kosa8Client } from '@kosa8/sdk';
import { readFileSync } from 'node:fs';

const kosa8 = new Kosa8Client();
const host = '/tmp/k8d/projects/api';

await kosa8.createSandbox({
  name: 'review',
  egress: 'deny',
  mounts: [
    { host_path: host, guest_path: '/work', read_only: false },
  ],
});
await kosa8.pullIntoSandbox('review', 'alpine:latest');

// Every container started in the sandbox sees the mount at its guest path.
const r = await kosa8.run('review', 'alpine:latest', ['sh', '-c',
  'ls -l /work; echo "// reviewed" >> /work/index.js']);
console.log(r.stdout);
console.log('on the host now:\n' + readFileSync(`${host}/index.js`, 'utf8'));
console.log('sandbox record:', (await kosa8.getSandbox('review'))?.mounts);
$ node mounts.mjs
total 4
-rw-r--r--    1 root     root            36 Oct 15 23:58 index.js

on the host now:
console.log("hello from the host");
// reviewed

sandbox record: [
  {
    host_path: '/tmp/k8d/projects/api',
    guest_path: '/work',
    read_only: false
  }
]

The sandbox sees that one folder and nothing else of your computer. A write inside lands on your disk at once. Make it read-only when the work inside should only look:

import { Kosa8Client, Kosa8ConflictError, Kosa8NotFoundError } from '@kosa8/sdk';

const kosa8 = new Kosa8Client();

// A read-only mount: writes fail inside, with an ordinary exit code.
await kosa8.createSandbox({
  name: 'ro', egress: 'deny',
  mounts: [{ host_path: '/tmp/k8d/projects/api', guest_path: '/src', read_only: true }],
});
await kosa8.pullIntoSandbox('ro', 'alpine:latest');
const w = await kosa8.run('ro', 'alpine:latest', ['sh', '-c', 'touch /src/new.txt']);
console.log('write to a read-only mount:', w.exitCode, w.stderr.trim());
write to a read-only mount: 1 touch: /src/new.txt: Read-only file system
Mounts here, and --allow-mount elsewhere

Through the native API, the mount you pass is the mount you get: the caller owns the daemon's socket, so it is trusted to choose. Two other ways in are limited by an allow-list. kosa8 mcp serve --allow-mount DIR[:rw] limits the folders an AI agent may share into sandboxes it creates, and kosa8 daemon start --allow-mount DIR[:rw] limits Docker API bind mounts. Under a signed org policy whose label lists mount_paths, the policy narrows native mounts too.

Stop, start, and what survives

import { Kosa8Client } from '@kosa8/sdk';

const kosa8 = new Kosa8Client();
const web = kosa8.sandbox('web');            // a handle: the same methods, bound to one sandbox

await web.pull('alpine:latest');
console.log('images:', await web.images());
const cid = await web.startContainer('alpine:latest', ['sh', '-c', 'echo kept > /data.txt; sleep 3600']);
console.log('containers:', await web.containers());

console.log('stop:', await web.stop());
console.log('state now:', (await web.info())?.state);
const up = await web.start();
console.log('start:', up.sandbox.state, `in ${up.boot_ms}ms`, up.warning ?? '');
console.log('images after restart:', await web.images());
console.log('containers after restart:', await web.containers());
$ node sandboxes2.mjs
images: [ 'docker.io/library/alpine:latest' ]
containers: [
  {
    id: 'k1-9f6ad3d7',
    image: 'docker.io/library/alpine:latest',
    status: 'running',
    ip: '10.88.0.2'
  }
]
stop: {
  ok: true,
  sandbox: {
    id: 'web',
    name: 'web',
    created: '2026-10-15T19:57:29.810605-04:00',
    state: 'stopped',
    cpus: 2,
    mem_mb: 1024,
    disk_gb: 20,
    egress: 'deny',
    labels: { team: 'payments', ticket: 'PAY-142' }
  },
  warning: ''
}
state now: stopped
start: running in 581ms
images after restart: [ 'docker.io/library/alpine:latest' ]
containers after restart: []

stopSandbox shuts the machine down and keeps its disk. startSandbox boots it again from that disk: pulled images and written files are still there, but no process is, so the container list is empty. To bring back running processes, take a snapshot or a checkpoint before you stop. A daemon restart also leaves every sandbox stopped, because a sandbox's VM cannot outlive kosa8d.

kosa8.sandbox('web') returns a SandboxHandle: the same methods with the sandbox id filled in (info, start, stop, delete, pull, run, startContainer, exec, containers, images, egress, snapshot, fork, explore, checkpoint, checkpoints, rewind).

Delete, and wipe

// A name that is taken.
try {
  await kosa8.createSandbox({ name: 'ro' });
} catch (err) {
  console.log(err.constructor.name, err.status, '-', err.message);
}

// Delete, overwriting the disks first.
console.log('delete with wipe:', await kosa8.deleteSandbox('ro', { wipe: true }));

// Deleting what is not there.
try {
  await kosa8.deleteSandbox('ro');
} catch (err) {
  console.log(err instanceof Kosa8NotFoundError, err.status, '-', err.message);
}
Kosa8ApiError 500 - sandbox ro already exists (run `kosa8 doctor`; check ~/.kosa8/log/)
delete with wipe: {
  files: [
    {
      path: '/tmp/k8d/.kosa8/sandboxes/ro.img',
      size: 1610612736,
      overwritten: 1610612736,
      verified: true,
      removed: true,
      filesystem: [Object],
      caveats: [Array]
    },
    {
      path: '/tmp/k8d/.kosa8/sandboxes/ro-data.img',
      size: 34359738368,
      overwritten: 11395072,
      verified: true,
      removed: true,
      filesystem: [Object],
      caveats: [Array]
    }
  ],
  ok: true,
  wipe: {
    method: 'NIST SP 800-88 Rev. 1 Clear: one pass of a pseudorandom pattern over every allocated extent, flushed, read back and compared, then truncated and unlinked',
    files: 2,
    overwritten_bytes: 1622007808,
    verified: true,
    caveats: [
      'flash media (SSD, NVMe) may keep old copies in cells the overwrite cannot address; cryptographic erase (FileVault/LUKS, crypto-shred) is the sanitization that reaches them',
      'copy-on-write filesystem: the overwrite was written to new blocks, and blocks shared with a clone or filesystem snapshot keep the old data until nothing references them'
    ]
  },
  checkpoints_dropped: 0
}
true 404 - no such sandbox: ro (list sandboxes with `kosa8 sandbox ls`)

Images and containers

Each sandbox has its own private image store and container engine. Nothing is shared with another sandbox or with your host.

CallReturnsNotes
pullIntoSandbox(id, ref){ ok, image }Fetched on the host and checked against the org's registry allow-list, then loaded into the sandbox. Works under egress: 'deny'.
sandboxImages(id)string[]Full references, such as docker.io/library/alpine:latest.
sandboxContainers(id)Container[]{ id, image, status, exit?, ip? }, including exited ones.
sandboxEgress(id)EgressStatusThe rule enforced on the host and its counters. See Network.
pull(ref){ ok, image }Pulls into the shared engine, the one the Docker API and kosa8 run use, not into a sandbox.
import { Kosa8Client } from '@kosa8/sdk';

// The shared engine (the one the Docker API and `kosa8 run` use), not a sandbox.
const kosa8 = new Kosa8Client();
console.log(await kosa8.pull('alpine:3.20'));
$ node hostpull.mjs
{ image: 'docker.io/library/alpine:3.20', ok: true }

Packet capture

Pass capture at creation to record every frame from the first one. The file is written on the host, where the sandbox cannot reach it, and sandboxEgress reports where it is. The Network page creates a sandbox with capture: { path: '/tmp/k8d/allow1.pcapng' }; after one request the file held 18 KB of pcapng. Open it with Wireshark or tshark -r.

Next

Network and secrets: allow-lists, refusals, and the broker that keeps API keys out.