kosa8 docs

Guides / Writing a kit

Writing a kit

A Sandbox Kit is a short file that describes the environment an AI agent runs in. It names the image, what gets installed, the one folder the agent can see, the hosts it can reach and the credentials it can use. kosa8 turns each line into something the sandbox enforces, so the limits hold whatever the agent decides to do.

What a kit decides

An agent told "only touch this project" can ignore the instruction. An agent whose virtual machine was only given that project's folder cannot. A kit is the list of things the machine is given.

imageThe container image the agent runs in, for example node:22-alpine.
packages, setupWhat is installed before the agent starts. packages are pinned to an exact version and a digest; setup is free-form shell for whatever a package list cannot express.
workspaceThe one folder on your computer the agent sees, and where it appears inside the sandbox. Nothing else on your computer is visible.
egress, egress_allowWhich hosts the sandbox can reach. Everything else is refused on your side of the boundary.
credentials, secretsWhich API keys the agent can use. credentials stay on your computer and the sandbox holds a placeholder; secrets are copied in as they are.

What kosa8 agent run does

The steps of kosa8 agent run Seven steps in order. Load the kit and check its signature. Refuse if the kit is marked unavailable or its setup installs something unpinned. Start the sandbox and its egress broker. Pull the image into the sandbox. Install the pinned packages, checking each digest. Run the command in the workspace folder. Remove the sandbox, however the run ended. load the kit check its signature refuse if unavailable, or setup installs unpinned start sandbox and its egress broker pull image into the sandbox install packages check each sha512 digest first run the command in the workspace folder remove sandbox however the run ended
Nothing the kit says is acted on until the kit itself has been checked.
  1. The kit is loaded: a built-in, a file in ~/.kosa8/kits, or a path. A file is checked against its signature.
  2. The run stops here if the kit is marked unavailable, or if setup installs or downloads something that is not pinned.
  3. kosa8 creates a fresh sandbox with the workspace mounted and, when the kit has credentials, an egress broker holding the real keys.
  4. The image is pulled into that sandbox. The service fetches it on your side, so the sandbox needs no network for this.
  5. Each pinned package is downloaded, its digest is compared with the one in the kit, and only then is it installed.
  6. The kit's command runs in the workspace folder, and its output is streamed back to your terminal.
  7. When the command exits, fails, or you press Ctrl-C, the sandbox is removed, and its broker and credentials go with it.

This is what it prints, on the error stream, for the built-in claude kit:

$ kosa8 agent run -w ~/code/api claude
kosa8: kit claude (node:22-alpine)
kosa8: workspace /Users/you/code/api -> /work (nothing else on the host is visible)
kosa8: network closed except through the egress broker: registry.npmjs.org, statsig.anthropic.com
kosa8: brokering ANTHROPIC_API_KEY -> api.anthropic.com (the sandbox holds a placeholder, never the key)
kosa8: sandbox claude-44059 ready in 653ms
kosa8: pulling node:22-alpine
kosa8: running kit setup, then: claude
...
kosa8: sandbox claude-44059 removed

Start from a built-in

kosa8 ships kits for common agents. List them:

$ kosa8 agent kits
KIT       IMAGE           NETWORK                             KEYS                  DESCRIPTION
azmx      node:22-alpine  api.azmx.ai,registry.npmjs.org      brokered + passed in  UNAVAILABLE — AZMX AI agent CLI in an isolated sandbox
claude    node:22-alpine  registry.npmjs.org,statsig.anthr..  brokered              Claude Code in an isolated sandbox
codex     node:22-alpine  registry.npmjs.org                  brokered              OpenAI Codex CLI in an isolated sandbox
copilot   node:22-alpine  api.githubcopilot.com,api.github..  passed in             GitHub Copilot CLI in an isolated sandbox
cursor    node:22-alpine  api2.cursor.sh,api.cursor.sh        passed in             UNAVAILABLE — Cursor CLI (cursor-agent) in an isolated sandbox
gemini    node:22-alpine  registry.npmjs.org                  brokered              Gemini CLI in an isolated sandbox
opencode  node:22-alpine  registry.npmjs.org                  brokered              OpenCode in an isolated sandbox
shell     alpine:latest   none                                none                  A plain shell with the workspace mounted and no network

brokered: the key stays on this machine and the sandbox holds a placeholder
passed in: the real key is in the sandbox's environment
kits in ~/.kosa8/kits override built-ins of the same name

Copy a built-in to a file

$ kosa8 agent init claude
wrote /Users/you/.kosa8/kits/claude.yaml

The file is YAML, readable only by you, and starts with a comment reminding you that everything in it is enforced by the sandbox.

Edit it

Open ~/.kosa8/kits/claude.yaml in any editor. Give it a different name and save it under that name if you want to keep the built-in as well. The fields are listed below.

See what it would do

$ kosa8 agent run --dry-run -w ~/code/api ./team-claude.yaml
kosa8: warning: kit file ./team-claude.yaml is unsigned: nothing vouches for its contents (sign it with `kosa8 agent sign`, and pin the key with `kosa8 policy trust`)
kosa8: kit team-claude (node:22-alpine)
kosa8: workspace /Users/you/code/api -> /work (nothing else on the host is visible)
kosa8: network closed except through the egress broker: registry.npmjs.org, statsig.anthropic.com
kosa8: brokering ANTHROPIC_API_KEY -> api.anthropic.com (the sandbox holds a placeholder, never the key)
kosa8: passing CI into the sandbox as plain environment variables — the agent can read them
kosa8: --dry-run, stopping before creating the sandbox

--dry-run prints the plan and creates nothing. Read it before the first real run of any kit you did not write.

Run it, by name or by path

$ kosa8 agent run team-claude            # ~/.kosa8/kits/team-claude.yaml
$ kosa8 agent run ./kits/team-claude.yaml  # any path

An argument that ends in .yaml or .yml, or contains a /, is read as a path. Anything else is a name: first a file in ~/.kosa8/kits, then a built-in.

The kit file, field by field

Only image is required. A kit file that leaves out name takes the file's name without its extension.

FieldTypeDefaultWhat it does
namestringthe file nameWhat the kit is called. The sandbox is named NAME- and a number unless you pass --name.
imagestringrequiredThe image the agent runs in. A kit with no image does not load: a kit must specify an image.
setuplist of stringsnoneShell lines run once, in order, after the pinned packages and before the command. A line that installs or downloads is refused unless you pass --allow-unpinned.
commandlist of stringsnoneWhat the kit exists to run. Extra arguments after the kit name on the command line are added to the end.
workspacestringcurrent directoryThe folder on your computer the agent sees. -w overrides it. It must be an existing directory.
mount_atstring/workWhere the workspace appears inside the sandbox. The command starts there.
egressstringemptydeny for no network at all. Left empty, egress_allow decides.
egress_allowlist of stringsnoneThe only hosts the sandbox may reach. With neither this nor egress: deny the network is open, and every run says so: network UNRESTRICTED — this kit sets no egress_allow.
secretslist of stringsnoneNames of environment variables on your computer whose real values are copied into the sandbox. The agent can read them. Prefer credentials.
envmap of stringsnoneFixed environment variables for the sandbox. Not for secrets: the kit file is not one.
cpuswhole number2Virtual CPUs for the sandbox.
mem_mbwhole number1024Memory for the sandbox, in MiB.
descriptionstringnoneOne line saying what the kit is for. kosa8 agent kits shows it for built-ins.
packageslist of packagesnonenpm packages installed globally, each pinned to one version and the digest of that version's tarball.
credentialslist of credentialsnoneAPI keys the agent uses without holding. Listing any puts the sandbox behind an egress broker.
unavailablestringemptyWhen set, the reason this kit cannot be run. kosa8 agent run refuses with it. kosa8 agent init leaves it out of the copy it writes.
The command is run by a shell

Setup and the command run as one shell line: sh -lc "…setup… && cd /work && COMMAND ARGS". The words of command are the kit author's shell text and are joined with spaces as written, so a word containing spaces does not stay one word; keep command to a program and simple flags, and put anything longer in a script in the workspace. Arguments you type after the kit name are different: each is quoted, so one with a space or a semicolon arrives as that one argument and never as more shell.

A package

FieldTypeWhat it must be
namestringAn npm package name, scoped or not: lower-case letters, digits, ., _ and -.
versionstringOne exact version, such as 2.1.296 or 1.0.0-beta.2. A range or a tag is refused.
integritystringsha512- followed by the base64 digest of the tarball, exactly as the registry publishes it.

A credential

FieldTypeDefaultWhat it does
namestringrequiredThe route's name and the first part of the address the sandbox calls. 1–32 characters of a-z, 0-9 and -; unique within the kit.
upstreamstringrequiredThe only place the key is ever sent: https://host, with no path.
secretstringrequiredThe environment variable on your computer that holds the real key. Read by kosa8 agent run.
headerstringrequiredThe header that carries the key to the upstream, for example x-api-key or Authorization.
schemestringemptyText put before the key in the header, for example "Bearer " with its trailing space.
methodslist of stringsanyThe HTTP methods the key may be used with.
pathslist of stringsanyPath prefixes on the upstream the key may be used for. Each must start with /.
envmap of stringsrequiredWhat the sandbox is given instead. {{url}} becomes the route's address at the broker and {{placeholder}} the stand-in for the key. Some variable must contain {{placeholder}}.

A kit with credentials reaches every host through the broker, by name. Its egress_allow may therefore list host names or single addresses, but not a network such as 10.0.0.0/8; that is refused when the kit is loaded. How the broker uses these fields, and what a refused request looks like, is in Keep API keys out of the sandbox.

Two complete kits

A shell tool with no network

name: lint
description: A shell with the project mounted and no network
image: alpine:latest
command: [sh]
egress: deny
cpus: 2
mem_mb: 1024
$ kosa8 agent run --dry-run -w ~/code/api ./lint.yaml
kosa8: warning: kit file ./lint.yaml is unsigned: nothing vouches for its contents (sign it with `kosa8 agent sign`, and pin the key with `kosa8 policy trust`)
kosa8: kit lint (alpine:latest)
kosa8: workspace /Users/you/code/api -> /work (nothing else on the host is visible)
kosa8: network denied
kosa8: --dry-run, stopping before creating the sandbox

An agent with a pinned package, a brokered key and an allow-list

name: team-claude
description: Claude Code for the API team
image: node:22-alpine
packages:
  - name: "@anthropic-ai/claude-code"
    version: 2.1.296
    integrity: sha512-OX/k/rpcthMqnFKOWcpNcbiLsMKtAjLOpQNJHlYiDCFUznWCIwdRTyl/p9SdHVrsrzZuPMCNqZjnpakIni6upw==
command: [claude]
mount_at: /work
egress_allow:
  - registry.npmjs.org
  - statsig.anthropic.com
credentials:
  - name: anthropic
    upstream: https://api.anthropic.com
    secret: ANTHROPIC_API_KEY          # read on your computer; never enters the sandbox
    header: x-api-key
    methods: [POST]                    # optional
    paths: [/v1/messages]              # optional
    env:                               # what the sandbox gets instead
      ANTHROPIC_BASE_URL: "{{url}}"
      ANTHROPIC_API_KEY: "{{placeholder}}"
env:
  CI: "1"
cpus: 4
mem_mb: 4096

The package, version and digest here are the ones the built-in claude kit pins. If ANTHROPIC_API_KEY is not set where you run the command, kosa8 warns and leaves that route out, so the agent fails to sign in rather than reaching the upstream with nothing:

kosa8: warning: ANTHROPIC_API_KEY is not set on the host; the agent may fail to authenticate

Flags for kosa8 agent run

FlagWhat it does
-w, --workspace DIRThe folder the agent may see. Default: the kit's workspace, then the current directory.
--name NAMEName the sandbox yourself.
--keepKeep the sandbox after the agent exits.
--no-networkDeny all network access, overriding the kit. No broker is started, so brokered keys are unavailable too.
--dry-runShow what would run without creating the sandbox.
--allow-unpinnedRun a kit whose setup installs software that is not pinned to a version and digest.
Flags go before the kit name

Everything after the kit name is handed to the agent's command, flags included. kosa8 agent run -w ~/code/api claude sets the workspace. kosa8 agent run claude -w ~/code/api runs claude -w ~/code/api in the current directory.

The full flag list is on the agent run reference page.

Pinning what a kit installs

npm install -g some-cli installs whatever the registry serves at that minute, next to your API key. A pinned package names one version and the SHA-512 digest of that version's tarball, so every run installs the same bytes or stops.

Ask the registry for the digest of the version you have reviewed, and copy both into the kit:

$ npm view @anthropic-ai/[email protected] dist.integrity
sha512-OX/k/rpcthMqnFKOWcpNcbiLsMKtAjLOpQNJHlYiDCFUznWCIwdRTyl/p9SdHVrsrzZuPMCNqZjnpakIni6upw==

For each package kosa8 generates one install step. This is it, with the name, version and digest filled in from the kit (the digest is converted to the hexadecimal form sha512sum prints):

t=$(cd /tmp && npm pack --silent NAME@VERSION) \
  && echo "SHA512_HEX  /tmp/$t" | sha512sum -c - >/dev/null \
  && npm install -g --silent "/tmp/$t" \
  && rm -f "/tmp/$t"
  1. npm pack downloads the exact tarball to /tmp.
  2. sha512sum -c compares it with the digest in the kit.
  3. npm install -g installs that file, never the name, which npm would look up again.

The steps are joined with &&, together with setup and the command, so one failing stops everything after it.

The digest check stopping a swapped package The kit file holds a version and a sha512 digest. npm pack downloads the tarball the registry serves for that version. sha512sum compares the tarball with the kit's digest. If they match, that file is installed. If the registry served different bytes, the check fails, the run ends and the agent never starts. registry the tarball for name@version npm pack downloads it to /tmp packages: version + integrity (sha512) sha512sum -c against the kit's digest digest matches npm install -g the file digest differs run ends; agent never starts
The kit decides what the right bytes are. The registry only delivers them.
  1. The kit carries the digest of the tarball you reviewed.
  2. At run time the tarball is downloaded again, from the registry, inside the sandbox.
  3. Before anything is installed, the download is hashed and compared with the kit.
  4. The same bytes: that file is installed and the run goes on.
  5. Different bytes, because the version was replaced or the registry was tampered with: the step fails and nothing after it runs.

A failed check ends the run before the agent starts, with this from sha512sum:

sha512sum: WARNING: 1 of 1 computed checksums did NOT match

A package that is not written exactly is refused when the kit is loaded:

$ kosa8 agent run --dry-run ./bad.yaml
kosa8: ./bad.yaml: package cowsay: version "latest" is not exact (a range or a tag such as "latest" is whatever the registry says today)

What counts as unpinned

kosa8 reads each setup line and refuses the kit if any of them fetches code to run. The check is on the words of the line, in upper or lower case:

ToolRefused when the line contains
JavaScriptnpm, pnpm, yarn or bun followed by install, i, add, exec or dlx; or npx
Pythonpip, pip3, pipx or uv followed by install, tool or run
System packagesapk, apt, apt-get, dnf or yum followed by add or install
Other languagesgo install, cargo install, gem install
Downloadscurl or wget, anywhere in the line
$ kosa8 agent run --dry-run ./unpinned.yaml
kosa8: kit quick installs software that is not pinned to a version and digest:
  npm install -g cowsay
list it under `packages:` with a version and integrity, or pass --allow-unpinned to run whatever the registry serves today

--allow-unpinned accepts those lines for one run. Use it while you are writing a kit, not for one a team depends on.

Signing kits

A kit decides what an agent can reach, so whoever can write a kit file decides that too. Signing lets an organisation say which kit files are its own, and lets each computer refuse the rest. It uses the same key that signs company rules, so one key vouches for policy and kits alike.

Make a signing key, once

$ kosa8 policy keygen -o org.key
public:  kQ+JPoaWWs9JC1Nic/sRli1nJOAgQFAmmm+xtD0oEeo=
private key written to org.key (keep it secret)

The public half is also written to org.key.pub. Skip this step if your organisation already has a policy key.

Sign the kit

$ kosa8 agent sign org.key team-claude.yaml
signed team-claude.yaml -> team-claude.yaml.sig

The signature is a small file beside the kit. It covers the kit's bytes exactly, so any edit, even to a comment, needs a new signature. A kit that would not load is not signed: the command stops with refusing to sign: and the reason. Distribute the two files together.

Pin the key on each computer

$ kosa8 policy trust org.key.pub
trusted policy signer set

From now on this computer runs only kit files signed by that key. Built-in kits are part of kosa8 itself and are not affected.

Making a key, signing and pinning need the Governance subscription or a trial. Checking a signature never does.

What happens to a kit file

On this computerThe kit file isResult
No key pinnedunsignedloads, with a warning kit file PATH is unsigned: nothing vouches for its contents (sign it with `kosa8 agent sign`, and pin the key with `kosa8 policy trust`)
signed, signature matchesloads with no warning, whoever signed it.
signed, signature does not matchrefused kit PATH does not match its signature: it was changed after it was signed, or signed with a different key
A key is pinnedunsignedrefused kit PATH is not signed, and this machine only runs kits signed by its org's key; have it signed with `kosa8 agent sign`
signed by the pinned keyloads
signed by another keyrefused kit PATH is signed by a key this machine does not trust
changed after signingrefused kit PATH does not match its signature: it was changed after it was signed, or signed with a different key

A bad signature is refused even where no key is pinned: somebody signed that kit, and these are not the bytes they signed.

A kit file with a built-in's name

A file in ~/.kosa8/kits takes the place of the built-in with the same name. That is how kosa8 agent init claude lets you customise claude, and it is also how something else writing to that folder could swap the kit you think you are running. So every run says when it has happened:

kosa8: warning: kit file /Users/you/.kosa8/kits/claude.yaml replaces the built-in kit of the same name

Where a key is pinned the replacement must also be signed by it, or it does not load at all.

The two built-ins that do not run

cursor and azmx are listed as UNAVAILABLE. Each used to install an npm package by name, and neither name can be vouched for. kosa8 refuses, with the reason, instead of installing something nobody checked and handing it an API key.

KitWhyWhat you see
cursorThe npm package cursor-agent is published by an account that is not Cursor. Cursor distributes its CLI through an install script, which cannot be pinned to a digest.kosa8: kit cursor cannot be run as shipped: the npm package "cursor-agent" is not published by Cursor, so this kit no longer installs it; write a kit that installs Cursor's CLI from a source you have verified (`kosa8 agent init cursor`)
azmxThe npm name @azmx/cli is not registered, so anyone could publish under it and be installed here.kosa8: kit azmx cannot be run as shipped: the npm package "@azmx/cli" is not published, so there is nothing verified to install; pin the package and its integrity in this kit once it is released

kosa8 agent init cursor writes a copy without the unavailable line. That copy is yours to make runnable: add an install step from a source you have checked yourself.

What this does not do

  • Dependencies of a pinned package are not pinned. The digest covers that package's own tarball. npm resolves its dependencies at install time, and those can change. If you need them fixed, build an image that already contains the tool and use that as the kit's image.
  • Images are named by tag. node:22-alpine is whatever the registry serves under that tag today. kosa8 does not pin or verify the image for you.
  • A signature says the organisation approved these bytes, not that they are safe. A signed kit with an open network and a passed-in key is still an open network and a passed-in key.
  • The unpinned check reads words, not meaning. It catches the usual installers and downloaders in setup. It does not understand a script already in the image or the workspace that downloads something itself.
  • secrets are not protected. The real value is in the sandbox's environment, where the agent and everything it installs can read it and send it to any host the kit allows.
  • Pinning the key is local. The pinned key is a file in ~/.kosa8. Someone who can delete it on their own computer can run unsigned kits there; protect it the way Policy reference describes.

Where to go next