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.
| Call | What it does | Returns |
|---|---|---|
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 }
- State carries over between
execcalls in the same container: the file written by the first is read by the second. A container started byrunis gone when it finishes, so state you want to keep belongs in a long-lived container or a mount. - The third call shows the argv rule:
$HOME ; rm -rf /reachedechoas plain text. execFramesyields each frame as it arrives: output frames with astreamand decodeddata, then a last frame withdoneand theexitcode. Breaking out of the loop closes the request.
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.
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.
Files: getting files into and out of a sandbox.