kosa8 docs

Guides / SDKs / Native HTTP API

Native HTTP API

All five SDKs are thin clients for one HTTP API that the kosa8 daemon serves under /kosa8/v1. This page documents that API directly: how to reach it, the headers it reads, how streaming output is framed, how errors come back, and the request and response of every endpoint the SDKs use. Use it to write a client in another language, to call kosa8 from curl, or to understand exactly what an SDK call does on the wire.

In plain words

The daemon is a small web server that only listens on a file on your own disk, not on the network. You send it JSON, and it answers with JSON. Commands that produce output answer with a stream of small JSON lines, one per piece of output, ending with one that says the command has finished. The responses shown on this page came from a kosa8 0.9.36 daemon on an Apple silicon Mac.

Reaching the daemon

Local: the unix socket

The daemon listens on $HOME/.kosa8/run/kosa8.sock. Any HTTP/1.1 client that can dial a unix socket works. The host name in the URL is ignored.

$ curl -s --unix-socket ~/.kosa8/run/kosa8.sock http://kosa8/kosa8/v1/health
{"commit":"f6124c0","go":"go1.26.9","phase":"0-foundations","status":"ok","uptime_seconds":3396,"version":"0.9.36"}

The socket file is the access control: it is created mode 0600 in a directory only your user can enter, so only processes running as you can connect. No token is needed or checked locally.

Remote: a worker over HTTPS

A daemon can also serve other machines as a remote worker (kosa8 worker). Clients then send every request with Authorization: Bearer <token>; a missing or wrong token gets 401 with WWW-Authenticate: Bearer. The SDKs choose the endpoint the way the CLI does:

KOSA8_HOST (or the SDK's host option)Endpoint
unset or emptythe local socket
https://host[:port][/path]HTTPS to that host; a path is kept as a prefix
http://host:portplain HTTP, refused unless the host is loopback (localhost, 127.x, ::1) or KOSA8_INSECURE=1 / the SDK's insecure option is set
tcp://host:portthe same as http://
host:portthe same as http://
unix:///path/to.sockthat socket file
any other schemean error

KOSA8_TOKEN is only sent to a host that also came from KOSA8_HOST. A host passed in code gets only a token passed in code, so a token in your environment never leaks to a host chosen somewhere else. Plain HTTP is refused because the token, and every command you run, would cross the network in clear text; put the worker behind a TLS-terminating proxy or tunnel instead.

Request headers

HeaderMeaning
Content-Type: application/jsonFor JSON request bodies. Endpoints that take raw bytes (stdin) use application/octet-stream.
X-Kosa8-ActorWho is acting: human, agent or tool. The SDKs send tool unless told otherwise. It is recorded on the audit entry and policies can treat actors differently.
X-Kosa8-Agent-TokenAn agent identity minted by kosa8 agent (Agent identity). The SDKs read it from KOSA8_AGENT_TOKEN. Under a policy with require_agent_identity or require_agent_owner, requests without a valid one are refused.
X-Kosa8-ApprovalA signed approval for one action, when your organisation's policy requires one (Company rules).
Authorization: Bearer …Remote workers only.

Responses and errors

Successful answers are JSON with status 200 (or 201 for a created sandbox). Every error has the same shape:

$ curl -s --unix-socket ~/.kosa8/run/kosa8.sock -X POST \
    -d '{"egress":"bogus"}' http://kosa8/kosa8/v1/sandboxes
{"error":"egress policy \"bogus\": want any, deny, fakenet, or allow:HOST[,HOST...]","remediation":""}

error says what went wrong and remediation, when not empty, says what to do about it. The SDKs join them as error (remediation) and choose an error type by status:

StatusMeaningSDK error
400The request is malformed or a value is invalid.BadRequest
401Missing or wrong bearer token (remote workers).Auth
403Refused by policy. When the body also has an approval object with a why, a signed approval would allow it.Policy, or ApprovalRequired
404No such sandbox, snapshot or container.NotFound
409The request conflicts with the current state: a name or snapshot id is taken, a sandbox is not running, a sealed snapshot fails verification.Conflict
501Not supported on this platform or build.Unsupported
500The operation failed; the remediation names where to look.Api

Streaming output: NDJSON frames

Endpoints that run something (run, exec, attach) answer 200 immediately and then stream newline-delimited JSON, one frame per line, as the output is produced:

{"stream":"status","data":"UHVsbGluZy4uLg=="}
{"stream":"stdout","data":"aGVsbG8K"}
{"stream":"stderr","data":"d2FybmluZwo="}
{"done":true,"ok":true,"exit":0}
FieldMeaning
streamstdout, stderr, or status (progress such as pulls)
dataThe bytes, base64-encoded, so binary output survives
donePresent and true on the last frame only
exitThe process's exit code, on the last frame
okWhether the operation itself worked (a non-zero exit is still ok)
errorOn the last frame, when the operation failed after streaming began

A non-zero exit code is a normal result, not an error. A stream that ends without a done frame means the connection broke; the SDKs report that as a stream error and keep the output received so far. Errors found before streaming starts (no such sandbox, policy refusal) come back as an ordinary JSON error with its status code.

Daemon

GET /kosa8/v1/health

Liveness and version. Returns status, version, commit, go, uptime_seconds.

POST /kosa8/v1/pull

Pulls an image into the host's shared engine. Body {"ref":"alpine:latest"}.

Sandboxes

GET /kosa8/v1/sandboxes

Lists sandboxes as an array of sandbox objects:

FieldMeaning
id, nameIdentifier and the name you gave it; either can be used in paths
staterunning, paused or stopped
createdRFC 3339 time
parentFor a fork, the sandbox it was copied from
cpus, mem_mb, disk_gbResources
mountsHost folders shared in
egressThe network policy string
ipIts address on the private network
labelsClassification labels
capture, brokerPacket capture file and credential broker, when set

POST /kosa8/v1/sandboxes

Creates and boots a sandbox. Every field is optional.

FieldTypeMeaning
namestringName; generated when omitted
cpusintVirtual CPUs
mem_mbintMemory in MiB
disk_gbintPrivate disk size in GiB
mountsarrayHost folders to share, host:guest[:ro]
egressstringdeny, any, fakenet or allow:HOST[,HOST…]. Hosts are names or IPv4 addresses and networks. When omitted, your organisation's default applies if a signed policy sets one, otherwise any.
captureobjectWrite the sandbox's packets to a pcapng file
labelsobjectClassification labels, checked against the org policy
brokerobjectCredential broker: real API keys stay on the host and the sandbox gets placeholders (Keep API keys out)
auto_stop_minintStop after this many minutes without use. 0 or omitted: never.
ttl_minintStop this many minutes after creation, in use or not
auto_delete_minintDelete once it has been stopped this many minutes
ephemeralboolDelete it, disk and all, whenever it stops
templatestringStart with a template's image (see Templates below)

Answers 201 with sandbox (the object above) and boot_ms. With a broker it also returns broker.url, broker.placeholders and broker.decide_token, only in this response.

Lifecycle: auto-stop, TTL, auto-delete, ephemeral

Agents forget to clean up. A sandbox with a lifecycle cleans up after itself instead of holding memory and disk until someone notices. The daemon checks every 30 seconds.

The settings are kept in the sandbox's record, so they survive a daemon restart. A restored or forked copy starts with none. Change them on a live or stopped sandbox with POST /kosa8/v1/sandboxes/{id}/lifecycle. Fields you leave out keep their value, and 0 turns one off. The answer is the sandbox, whose lifecycle field shows the result. From the CLI:

$ kosa8 sandbox create web --auto-stop 30m --auto-delete 24h
$ kosa8 sandbox create scratch --ephemeral --ttl 2h
$ kosa8 sandbox lifecycle web --auto-stop 0

Under an org policy that requires signed approvals, changing a lifecycle needs one, because a lifecycle can delete a sandbox. Over MCP, sandbox_create takes auto_stop_min, ttl_min and ephemeral.

POST /kosa8/v1/sandboxes/{id}/down and /up

down stops a sandbox and keeps its disk; stopping one that is already stopped answers {"ok":true,"already_stopped":true}. up boots it again from its own disk and returns sandbox, boot_ms and any warning.

POST /kosa8/v1/sandboxes/{id}/pause and /resume

Freeze a running sandbox where it is, and continue it. POST /kosa8/v1/sandboxes/pause pauses every running sandbox at once and returns the ones it paused.

DELETE /kosa8/v1/sandboxes/{id}

Deletes a sandbox. With ?wipe=1 its disks are overwritten and verified first, and the answer includes a wipe summary and per-file files. Checkpoints are kept by the client, not the daemon, so the SDKs also delete the sandbox's checkpoint snapshots unless told to keep them, and add checkpoints_dropped to the result.

POST /kosa8/v1/sandboxes/{id}/pull

Pulls an image into the sandbox's own engine; code in a sandbox can only use images pulled into it. Body {"ref":"python:3.12-alpine"}, answer {"ok":true,"image":"…"}.

GET /kosa8/v1/sandboxes/{id}/images, /containers, /egress

The images in the sandbox (array of strings), its containers (array of objects), and its network policy with the decisions the host has made under it.

Running code

POST /kosa8/v1/sandboxes/{id}/run

Runs a container to completion and streams its output as frames. Body: {"image":"alpine:latest","args":["sh","-c","echo hi"],"env":["A=1"]}. args replaces the image's command; env entries are KEY=VALUE.

POST /kosa8/v1/sandboxes/{id}/code

Runs a snippet of Python, JavaScript or TypeScript and answers with one JSON object once it ends. You don't pick an image or quote anything for a shell: send the language and the source.

$ curl -s --unix-socket ~/.kosa8/run/kosa8.sock -X POST \
    http://kosa8/kosa8/v1/sandboxes/demo/code \
    -d '{"language":"python","code":"import sys\nprint(sys.version)"}'
{"language":"python","image":"docker.io/library/python:3.12-slim","exit":0,
 "stdout":"3.12.…\n","stderr":"","artifacts":[],"duration_ms":412}
FieldMeaning
languagepython, javascript or typescript (also py, js, node, ts).
codeThe source, up to 96 KiB. For a bigger program, copy it into the sandbox and use /run.
imageOptional. Defaults to python:3.12-slim or node:22-slim. If the sandbox doesn't have it yet, the daemon pulls it on the host and loads it in, so the sandbox needs no network access for that.
envOptional map of extra environment variables.
timeout_sOptional. Default 300, at most 3600. When the time runs out the snippet is killed and the answer has "timed_out":true and exit 124.

The snippet starts in the sandbox's workspace, /workspace, so files it writes there can be read back with the file API below, and files you upload are there for it to open. Python runs the source as __main__. If the snippet uses matplotlib, the daemon switches it to the non-GUI backend so plt.show() doesn't hang. When the snippet ends, every open figure comes back in artifacts as {"type":"image/png","name":"figure-1.png","data":"<base64>"}. TypeScript runs on Node's built-in type stripping, so there is no compile step and types aren't checked. A snippet that fails still answers 200: read exit and stderr. Output beyond 32 MiB is dropped and "truncated":true is set. If the caller hangs up, the snippet is killed.

Over MCP the same call is the sandbox_code_run tool. Its result is the output as text, followed by each chart as an image the model can look at.

POST /kosa8/v1/sandboxes/{id}/start

Starts a detached container and returns {"ok":true,"id":"…"}. Use it for a long-running process you then exec into.

POST /kosa8/v1/sandboxes/{id}/exec

Runs a command in a running container and streams frames. Body {"container":"<id>","args":["ls","-la"]}.

POST /kosa8/v1/sandboxes/{id}/start-interactive

Starts a container with stdin held open: {"image","args","env","tty"}, returning its id. Feed it with stdin and read it with attach.

POST /kosa8/v1/sandboxes/{id}/containers/{cid}/stdin

The request body is written to the container's stdin as raw bytes. Send it with Transfer-Encoding: chunked to stream input as it is produced. Answers {"ok":true}.

POST /kosa8/v1/sandboxes/{id}/containers/{cid}/attach

Streams the container's output as frames until it exits.

Files and git

Every sandbox has a workspace: a folder on its own data disk that code runs see at /workspace. It survives a stop, and a snapshot or fork takes it along. These routes read and write it directly, without starting a container. A path is written as a container sees it (/workspace/src/app.py) or relative to the workspace (src/app.py). Any other absolute path is refused, and no path can leave the folder: the guest resolves every path, symlinks included, inside it.

CallWhat it does
GET …/files?path=Lists a folder: {"path","files":[{"name","path","type","size","mode","mod_time","link"}]}. type is file, dir, symlink or other. On a file it describes that file.
GET …/files/stat?path=Describes one entry without following a symlink.
GET …/files/content?path=Downloads a file, raw, with its Content-Length.
PUT …/files/content?path=&mode=644Uploads the body as the file, replacing it in one step (readers never see half a file) and making its folders. Up to 4 GiB. Answers with the new entry.
DELETE …/files?path=&recursive=trueRemoves a file, or a folder with recursive. The workspace itself can't be removed.
POST …/files/mkdir{"path"}: makes a folder and its parents.
POST …/files/move{"from","to"}: renames, making the destination's folders.
$ S=~/.kosa8/run/kosa8.sock; B=http://kosa8/kosa8/v1/sandboxes/demo
$ curl -s --unix-socket $S -X PUT --data-binary @data.csv "$B/files/content?path=/workspace/data.csv"
$ curl -s --unix-socket $S -X POST $B/code -d '{"language":"python","code":"print(open(\"data.csv\").read()[:80])"}'

A missing path is 404. A path outside the workspace, a folder where a file was expected, or a non-empty folder removed without recursive is 400. A sandbox whose guest image predates the file API answers 501: upgrade kosa8, then stop and start the sandbox.

POST /kosa8/v1/sandboxes/{id}/git

Runs one git operation on a repository in the workspace. git runs in a container inside the sandbox, so a clone or push goes out under the sandbox's own network policy. A hook in a cloned repository runs sealed in there too. The image is alpine/git, which the daemon pulls on first use. Body: {"op", "path", …}, with the fields each operation reads:

opFieldsParsed result
cloneurl (http or https), path (default: the repository's name), branch, depth
statuspathstatus: {"branch","upstream","ahead","behind","clean","files":[{"path","from","index","worktree"}]}
addfiles (default all)
commitmessage, author_name, author_email
push, pullremote (default origin), branch
checkoutbranch, create
branchesbranches: [{"name","current"}]
logn (default 20)log: [{"hash","author_name","author_email","date","subject"}]
diffstagedthe diff is stdout

Every answer has ok, exit, stdout and stderr. A git failure (a conflict, an unknown branch) is still a 200 with "ok":false, so read stderr. For a private remote, pass token (and username, which defaults to x-access-token, what GitHub, GitLab and Gitea accept). The token reaches git as an HTTP header through its environment. It never appears on a command line, in the audit log or in an approval, and it is scrubbed from the output.

$ curl -s --unix-socket $S -X POST $B/git -d '{"op":"clone","url":"https://github.com/kosa8ai/kosa8-py","depth":1}'
$ curl -s --unix-socket $S -X POST $B/git -d '{"op":"status","path":"kosa8-py"}'

Over MCP these are the sandbox_file_list, sandbox_file_read, sandbox_file_write, sandbox_file_delete and sandbox_git tools.

Sessions and terminals

Sessions: a shell that remembers

An agent runs one command at a time, and expects cd src in one command to still hold in the next, and export too. A session gives it that. Each session is a long-lived container in the sandbox, which starts in /workspace. Each command runs in it with sh, and afterwards the shell's working directory and exported variables are saved for the next command. The daemon keeps no session state of its own: a session is its container, so a daemon restart, a snapshot or a fork carries it along.

CallWhat it does
POST …/sessions{"name","image","env"}, all optional. The image defaults to python:3.12-slim, pulled on first use. Answers {"name","id","image","status"}; 409 if the name is taken.
GET …/sessionsThe sandbox's sessions.
POST …/sessions/{name}/exec{"command","timeout_s"}. Answers {"exit","stdout","stderr","cwd","duration_ms","timed_out","truncated"}. cwd is where the shell is now.
DELETE …/sessions/{name}Ends the session and everything it started.
$ curl -s --unix-socket $S -X POST $B/sessions -d '{"name":"main"}'
$ curl -s --unix-socket $S -X POST $B/sessions/main/exec -d '{"command":"cd kosa8-py && export N=3"}'
$ curl -s --unix-socket $S -X POST $B/sessions/main/exec -d '{"command":"echo $N; pwd"}'
{"exit":0,"stdout":"3\n/workspace/kosa8-py\n","stderr":"","cwd":"/workspace/kosa8-py","duration_ms":31}

Commands in one session run one at a time, in the order they arrive. Only exported variables carry over; shell functions and aliases don't. A command that runs longer than timeout_s (default 300) is killed, and the session keeps the state it had before that command. To leave something running, start it in the background with its output redirected, for example nohup ./server > server.log 2>&1 &. Over MCP this is sandbox_shell, which makes the session (default main) on first use, and sandbox_session_delete.

Terminals (PTY)

For a program that needs a real terminal: a REPL, an editor, a TUI, or a person typing.

CallWhat it does
POST …/pty{"image","command","cols","rows","env"}, all optional. Starts a container with a terminal (by default a login shell in /workspace, with TERM=xterm-256color) and answers {"id"}.
POST …/containers/{cid}/resize{"cols","rows"}: sets the terminal's size.
GET …/containers/{cid}/ttyA WebSocket that is the terminal itself.

On the WebSocket, send keyboard input as binary messages and control messages as text, for example {"type":"resize","cols":120,"rows":40}. The terminal's output arrives as binary messages. When the program ends, a text message {"type":"exit","code":0} arrives and the socket closes. A terminal's input can be opened once. A request that carries an Origin header, which every browser sends, is refused, so a web page can't open a terminal in your sandbox. A guest image from before resize support answers 501 to resize.

Previews: a link to a web server in a sandbox

An agent starts a web app in a sandbox and wants you to see it. A preview is a link that opens it in your browser and stops working when it expires.

$ curl -s --unix-socket $S -X POST $B/sessions/main/exec -d '{"command":"nohup python3 -m http.server 8000 > web.log 2>&1 &"}'
$ curl -s --unix-socket $S -X POST $B/previews -d '{"port":8000,"session":"main","expires_in_s":1800}'
{"url":"http://p-3f9c…a1.localhost:8791/?kosa8_preview=eyJz…","token":"eyJz…",
 "expires_at":"2026-11-03T13:30:00Z","container":"session-main","port":8000,"listen":"127.0.0.1:8791"}
$ kosa8 sandbox preview demo 8000 --session main --expires 30m

Body: port (required); session or container for where the server runs, defaulting to the main session or the only running container; and expires_in_s (default 3600, at most 7 days). The server must listen on 0.0.0.0, not 127.0.0.1, so the sandbox's network can reach it.

How it works

Under an org policy that requires signed approvals, making a preview needs one, because the link opens the sandbox's port to whoever holds it. Revoking does not. Over MCP this is sandbox_preview, which returns the link for the agent to give you.

Templates: what a sandbox starts with

A template is an image built from your Dockerfile and kept on your machine. A new sandbox created with "template": "NAME" starts with it already loaded. Its sessions, terminals and code runs then use that image unless a call names another. So an agent's sandbox_shell or sandbox_code_run finds your libraries, tools and data already there.

$ cat Dockerfile
FROM python:3.12-slim
RUN pip install --no-cache-dir pandas matplotlib scikit-learn
$ kosa8 template build py-data .
$ kosa8 sandbox create analysis --template py-data --auto-stop 30m
$ kosa8 template ls
$ kosa8 template rm py-data

The build runs in a sandbox made for it and deleted after it. That way the Dockerfile's RUN steps (an install script, a curl | sh) run sealed, never on your machine or the shared engine. They run under the egress you give: any by default, because builds fetch packages, or an allow-list such as --egress allow:pypi.org,files.pythonhosted.org. Base images are pulled by the host, under your organisation's registry allowlist when it has one.

CallWhat it does
POST /kosa8/v1/templates?name=&dockerfile=&buildarg=&egress=&replace=1 The body is the build context as a tar, as for /kosa8/v1/build. Streams NDJSON frames (build output, then {"done":true,"ok":true,"image":…}). Names are lower-case. 409 if the template exists, unless replace=1. Optional cpus, mem_mb and disk_gb size the build sandbox.
GET /kosa8/v1/templates[{"name","image","size_bytes","created","dockerfile"}]
DELETE /kosa8/v1/templates/{name}Deletes it. Sandboxes made from it keep their copy.

A sandbox made from a template carries the labels kosa8.io/template and kosa8.io/image, and so do its snapshots, restores and forks. Templates live in ~/.kosa8/templates/ as OCI archives.

Snapshots and forks

POST /kosa8/v1/sandboxes/{id}/snapshot

Captures memory, running processes and disks while the sandbox keeps running. Body {"id":"optional-id","encrypt":false}. Answers snapshot and snapshot_ms; with encryption, also encryption and encrypt_ms. An id already in use is 409. If the org policy requires encryption, it happens anyway and the answer says encrypted_by_policy.

GET /kosa8/v1/snapshots

Lists snapshots: id, sandbox, created, cpus, mem_mb, size_bytes and more.

POST /kosa8/v1/snapshots/{id}/restore

Boots a new sandbox from a snapshot with its processes resumed. Body {"name":"…","label":"…","allow_tampered":false}. Answers sandbox and restore_ms, plus seal for a sealed snapshot. A sealed snapshot whose seal no longer verifies is refused with 409 unless allow_tampered is set, and the override is written to the audit log.

DELETE /kosa8/v1/snapshots/{id}

Deletes a snapshot, shredding its key first if it is encrypted. ?wipe=1 overwrites its files; ?force=1 is required to delete a sealed one.

POST /kosa8/v1/sandboxes/{id}/fork

Makes n live copies of a running sandbox from one snapshot. Body {"n":3,"prefix":"try","label":""}. Answers forks (sandbox objects), snapshot (the id they were made from), fork_ms and count. A failed fork keeps nothing, so it can be retried. Disks are cloned copy-on-write where the filesystem allows (APFS, XFS, btrfs).

Built on the API: checkpoints and explore

Two SDK features are client-side logic over the endpoints above, done the same way as the CLI:

Other endpoints

The daemon serves more than the SDKs use today: networks, volumes, images (build, tag, save, load, push), ranges, labels, the audit log (GET /kosa8/v1/audit and /audit/verify), packet capture, snapshot sealing and encryption, and the malware lab. The command reference documents them through the CLI, which is built on the same API. The daemon also serves the Docker Engine API on the same socket (For developers).