kosa8 docs

Guides / JavaScript & TypeScript / Running code

Running code

Inside a sandbox, code runs in containers on the sandbox's own engine. There are three shapes: a container run to completion, commands executed in a container that keeps running, and an interactive container you feed through stdin. This page shows each one, with streaming, exit codes, time limits and errors.

CallWhat it doesReturns
run(id, image, args?, opts?)A fresh container from the image, run to completion. Options: env, onStdout, onStderr, onStatus.ExecResult
startContainer(id, image, args?)A detached container that keeps running.its id
exec(id, container, args, opts?)A command in a running container. Options: onStdout, onStderr.ExecResult
execFrames(id, container, args)The same, as the raw stream of frames.async iterable of Frame
startInteractive(id, image, args?, { env?, tty? })A container with stdin held open.its id
writeStdin(id, container, data)Stream bytes into its stdin; stdin closes when the body ends.{ ok }
attach(id, container, opts?)Follow a container's output until it exits.ExecResult

ExecResult is { exitCode, stdout, stderr, stdoutBytes, stderrBytes }: the text decoded as UTF-8, and the raw bytes as Buffers for binary output. Arguments are always an argv array. No shell is involved unless you run one, so nothing in an argument is interpreted.

The examples share one sandbox, created like this:

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

const kosa8 = new Kosa8Client();
await kosa8.createSandbox({ name: 'runner', egress: 'deny' });
await kosa8.pullIntoSandbox('runner', 'alpine:latest');

run: to completion, streaming as it goes

// run: a fresh container from the image, to completion. Output streams as it is produced.
const t0 = Date.now();
const r = await kosa8.run('runner', 'alpine:latest',
  ['sh', '-c', 'for i in 1 2 3; do echo "tick $i ($GREETING)"; sleep 1; done; echo done >&2; exit 7'],
  {
    env: { GREETING: 'hello' },                 // or ['GREETING=hello']
    onStdout: (b) => process.stdout.write(`[${Date.now() - t0}ms] ${b}`),
    onStderr: (b) => process.stdout.write(`[${Date.now() - t0}ms] stderr: ${b}`),
  });
console.log(r.exitCode, JSON.stringify(r.stdout), JSON.stringify(r.stderr), r.stdoutBytes.length);
$ node run1.mjs
[162ms] tick 1 (hello)
[1153ms] tick 2 (hello)
[2155ms] tick 3 (hello)
[3157ms] stderr: done
7 "tick 1 (hello)\ntick 2 (hello)\ntick 3 (hello)\n" "done\n" 45

The callbacks fire as output is produced, a second apart here, and the full text is collected in the result as well. The exit code 7 is in the result; the promise resolves. A command's failure is information you act on, not an exception.

onStatus receives progress lines the daemon emits while it prepares the run. The image must already be in the sandbox: run does not pull (see the errors below).

exec: commands in a container that stays up

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

const kosa8 = new Kosa8Client();
const box = kosa8.sandbox('runner');

// A long-lived container, then several commands in it. State carries over between execs.
const cid = await box.startContainer('alpine:latest', ['sh', '-c', 'while :; do sleep 3600; done']);
console.log('container', cid);
await box.exec(cid, ['sh', '-c', 'mkdir -p /app && echo 41 > /app/n']);
const r = await box.exec(cid, ['sh', '-c', 'echo $(( $(cat /app/n) + 1 ))']);
console.log('exec ->', r.exitCode, r.stdout.trim());

// Arguments are passed as an argv array: no shell unless you ask for one.
console.log((await box.exec(cid, ['echo', '$HOME', '; rm -rf /'])).stdout.trim());

// execFrames: the raw stream, one frame at a time, for output you want as it arrives.
for await (const f of kosa8.execFrames('runner', cid, ['sh', '-c', 'echo a; echo b >&2; exit 4'])) {
  console.log({ ...f, data: f.data?.toString() });
}
$ node run2.mjs
container k2-99f6f9b7
exec -> 0 42
$HOME ; rm -rf /
{ stream: 'stdout', data: 'a\n' }
{ stream: 'stderr', data: 'b\n' }
{ done: true, ok: true, exit: 4, data: undefined }

Interactive containers and stdin

exec has no stdin. For a process that reads input, start an interactive container, attach to its output, then write to it:

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

const kosa8 = new Kosa8Client();

// A container with stdin held open. Here, a shell that reads commands from stdin.
const cid = await kosa8.startInteractive('runner', 'alpine:latest', ['sh']);
console.log('interactive container', cid);

// attach follows the container's output until it exits; start it before writing.
const done = kosa8.attach('runner', cid, { onStdout: (b) => process.stdout.write(`out: ${b}`) });

// writeStdin streams bytes in. When the body ends, the container's stdin is closed.
console.log(await kosa8.writeStdin('runner', cid, 'echo hello from stdin\nuname -m\nexit 5\n'));
const r = await done;
console.log('attach ->', r.exitCode, JSON.stringify(r.stdout));
$ node run3.mjs
interactive container k7-f0b75fe7
out: hello from stdin
out: aarch64
{ ok: true }
attach -> 5 "hello from stdin\naarch64\n"

Each container's stdin takes one writeStdin body, and closes when that body ends. A second call returns { ok: true } but its bytes go nowhere; in our test, a cat saw end-of-file after the first call and exited. To feed input over time, pass an async iterable, which is streamed chunk by chunk:

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

const kosa8 = new Kosa8Client();
const cid = await kosa8.startInteractive('runner', 'alpine:latest',
  ['sh', '-c', 'while read line; do echo "got: $line"; done; echo "stdin closed"']);
const done = kosa8.attach('runner', cid, { onStdout: (b) => process.stdout.write(b) });

// One writeStdin call per container: stdin closes when its body ends.
// To feed input over time, pass an async iterable; it is streamed chunk by chunk.
async function* lines() {
  for (const l of ['one', 'two', 'three']) {
    yield l + '\n';
    await new Promise((r) => setTimeout(r, 500));
  }
}
await kosa8.writeStdin('runner', cid, lines());
console.log('exit', (await done).exitCode);
$ node run3c.mjs
got: one
got: two
got: three
stdin closed
exit 0

tty: true gives the container a terminal, for programs that change behaviour when they see one. It was not exercised for this page.

Time limits, and errors

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

const kosa8 = new Kosa8Client();

// A time limit: the promise rejects with the signal's reason.
try {
  await kosa8.run('runner', 'alpine:latest', ['sleep', '30'], { signal: AbortSignal.timeout(2000) });
} catch (err) {
  console.log('aborted:', err.name, '-', err.message);
}

// An image the sandbox does not have.
try {
  await kosa8.run('runner', 'python:3.13-alpine', ['python3', '-V']);
} catch (err) {
  console.log(err.constructor.name, err.status ?? '', '-', err.message);
}

// A container that does not exist.
try {
  await kosa8.exec('runner', 'k99-nope', ['true']);
} catch (err) {
  console.log(err.constructor.name, err.status ?? '', '-', err.message);
}

// A command that is not in the image.
const r = await kosa8.run('runner', 'alpine:latest', ['python3', '-V']).catch((e) => e);
console.log(r.constructor.name, r.exitCode ?? '', r.message ?? r.stderr);
$ node run4.mjs
aborted: TimeoutError - The operation was aborted due to timeout
Kosa8StreamError  - image "docker.io/library/python:3.13-alpine" not found in engine: image "docker.io/library/python:3.13-alpine": not found
Kosa8StreamError  - no such container: k99-nope
Kosa8StreamError  failed to create shim task: OCI runtime create failed: executable file `python3` not found in $PATH: No such file or directory

Two kinds of failure reach you. An error before the run starts, such as a refusal by policy, is an HTTP error and arrives as the matching class (Kosa8PolicyError, Kosa8NotFoundError, …). An error once the stream has started, such as an image that is not in the sandbox or a missing executable, arrives as Kosa8StreamError, with any output produced so far in err.stdout and err.stderr. Every class is on the Errors reference.

Aborting stops the request, not the process

A signal ends the HTTP request and rejects the promise with the signal's reason. The command inside the sandbox keeps running: right after the 2-second abort above, sandboxContainers still listed the sleep 30 container as running. To bound how long code runs, put the limit inside the sandbox, with timeout, and see the untrusted code recipe for the PID 1 detail that makes it work.

Environment variables

run and startInteractive take env as an object ({ GREETING: 'hello' }) or as Docker-style strings (['GREETING=hello']). startContainer and exec take none: set variables inside the command (['sh', '-c', 'FOO=1 my-tool']), or use the AI SDK adapter, which applies env and a working directory to every command for you.

Next

Files: getting files into and out of a sandbox.