kosa8 docs

Guides / JavaScript & TypeScript / Files

Files

kosa8's native API has no file endpoint for sandboxes. There are three ways to move files instead, and which one fits depends on whether the files live on your machine or inside the sandbox.

A mountShare a host folder when you create the sandbox. Files are on your disk; the sandbox reads and writes them in place, at full speed, with no copying. Best for a repository an agent edits. See Share a host folder.
Through execWrite with base64 -d and read with cat, inside a running container. Works for any file in the container, and needs sh and base64 in the image. The helpers below are about 20 lines.
The AI SDK adapter readTextFile, writeBinaryFile, streams and the rest, already written, doing the same exec-and-base64 work underneath.

Through exec, with @kosa8/sdk

import { Kosa8Client } from '@kosa8/sdk';
import { createHash, randomBytes } from 'node:crypto';

const kosa8 = new Kosa8Client();
const sha = (b) => createHash('sha256').update(b).digest('hex').slice(0, 16);

// There is no file endpoint: files go in and out through exec. The image needs sh and base64.
async function writeFile(sandbox, container, path, data) {
  const b64 = Buffer.from(data).toString('base64');
  const CHUNK = 96 * 1024;   // one exec argument stays well under Linux's 128 KiB limit
  let r = await kosa8.exec(sandbox, container,
    ['sh', '-c', 'mkdir -p "$(dirname "$1")" && : > "$1.tmp"', 'sh', path]);
  for (let i = 0; i < b64.length && r.exitCode === 0; i += CHUNK) {
    r = await kosa8.exec(sandbox, container,
      ['sh', '-c', 'printf %s "$2" | base64 -d >> "$1.tmp"', 'sh', path, b64.slice(i, i + CHUNK)]);
  }
  if (r.exitCode === 0) r = await kosa8.exec(sandbox, container, ['mv', '-f', `${path}.tmp`, path]);
  if (r.exitCode !== 0) throw new Error(`write ${path}: ${r.stderr.trim()}`);
}

async function readFile(sandbox, container, path) {
  const r = await kosa8.exec(sandbox, container, ['cat', '--', path]);
  if (r.exitCode !== 0) throw new Error(`read ${path}: ${r.stderr.trim()}`);
  return r.stdoutBytes;      // the raw bytes; r.stdout is the same decoded as UTF-8
}

const cid = await kosa8.startContainer('runner', 'alpine:latest', ['sh', '-c', 'while :; do sleep 3600; done']);

await writeFile('runner', cid, '/work/hello.txt', 'hi from the host\n');
console.log((await readFile('runner', cid, '/work/hello.txt')).toString());

const blob = randomBytes(1024 * 1024);               // 1 MiB of binary
const t0 = Date.now();
await writeFile('runner', cid, '/work/blob.bin', blob);
const back = await readFile('runner', cid, '/work/blob.bin');
console.log(`1 MiB round trip in ${Date.now() - t0}ms: ${sha(blob)} -> ${sha(back)}, ${back.length} bytes`);

await readFile('runner', cid, '/work/missing.txt').catch((e) => console.log(e.message));
$ node files1.mjs
hi from the host

1 MiB round trip in 597ms: 3044a160fdc5bac4 -> 3044a160fdc5bac4, 1048576 bytes
read /work/missing.txt: cat: can't open '/work/missing.txt': No such file or directory

Points that matter, all visible in the code:

The image needs sh and base64. Alpine, Debian, Ubuntu and the official Node and Python images have both. A distroless image has neither; use a mount for those.

With the AI SDK adapter

The adapter's session does all of this for you. Paths are relative to its working directory, /workspace by default, and parent directories are created on write.

import { createKosa8NetworkSandboxSession } from '@kosa8/ai-sdk-sandbox';
import { createHash, randomBytes } from 'node:crypto';

const sha = (b) => createHash('sha256').update(b).digest('hex').slice(0, 16);
const session = await createKosa8NetworkSandboxSession({ networkPolicy: { mode: 'deny-all' } });
const sb = session.restricted();
try {
  // Binary in and out.
  const blob = randomBytes(300 * 1024);
  await sb.writeBinaryFile({ path: 'data/blob.bin', content: blob });
  const back = await sb.readBinaryFile({ path: 'data/blob.bin' });
  console.log('binary:', sha(blob), '->', sha(back), back.length, 'bytes');

  // Streams: write from a web ReadableStream, read as one.
  const enc = new TextEncoder();
  const content = new ReadableStream({
    start(c) { for (let i = 1; i <= 3; i++) c.enqueue(enc.encode(`row ${i}\n`)); c.close(); },
  });
  await sb.writeFile({ path: 'data/rows.csv', content });
  let text = '';
  for await (const chunk of await sb.readFile({ path: 'data/rows.csv' })) text += new TextDecoder().decode(chunk);
  console.log('stream:', JSON.stringify(text));

  // Missing files read as null; a directory is an error.
  console.log('missing:', await sb.readFile({ path: 'nope' }), await sb.readBinaryFile({ path: 'nope' }));
  await sb.readTextFile({ path: 'data' }).catch((e) => console.log('directory:', e.message));
} finally {
  await session.destroy();
}
process.exit(0);
$ node adfiles.mjs
binary: dec126367973ae47 -> dec126367973ae47 307200 bytes
stream: "row 1\nrow 2\nrow 3\n"
missing: null null
directory: /workspace/data is a directory
MethodNotes
readTextFile({ path, encoding?, startLine?, endLine? })Text, or null if missing. startLine/endLine select lines (1-based).
readBinaryFile({ path })Uint8Array, or null.
readFile({ path })A web ReadableStream<Uint8Array>, or null. Streams from cat.
writeTextFile({ path, content, encoding? })Creates parent directories; replaces atomically.
writeBinaryFile({ path, content })Same, from a Uint8Array.
writeFile({ path, content })Same, from a ReadableStream<Uint8Array>, chunked as it reads.

Each accepts abortSignal. A path that is a directory is an error, not null.

Which to choose

SituationUse
An agent edits a repository you will review with git diffA writable mount
Inputs the code must not changeA read-only mount
Generated code, test fixtures, one file at a timeThrough exec, or the adapter
Results to collect after a runA mount if they are large; cat through exec if not
Large datasets, gigabytesA mount. Exec is about 600 ms per MiB round trip in our test, fine for source files and slow for bulk data.
Next

Checkpoints and rewind: an undo button for the whole machine.