Guides / For developers
For developers
This page is for two kinds of reader: someone reading or changing kosa8's source, and someone driving kosa8 from their own program. It covers how to build and test, what each package is for, the path a request takes through them, the service's HTTP API, and the rules the code is written to.
If you have not read How kosa8 works, start there. It explains the three programs and the boundary this page assumes.
Build
kosa8 is written in Go; go.mod names the version it needs (Go 1.26). Running
sandboxes needs macOS on Apple silicon, or Linux with KVM, cloud-hypervisor and
virtiofsd. Everything else, including the unit tests, builds anywhere Go does.
$ make build # bin/kosa8 and bin/kosa8d $ make guest # build the guest image and install it to ~/.kosa8/guest $ make test # go test ./... $ make test-race # go test -race ./... $ make lint # go vet ./... $ make cross # build the guest helper and every package for Linux arm64 and amd64 $ make check # fmt, lint, test and cross: everything CI enforces
kosa8d embeds Apple's Virtualization.framework, which only works in a binary that
carries the right entitlement. make build therefore runs
scripts/sign-macos.sh bin/kosa8d on a Mac. With APPLE_SIGN_IDENTITY set
it makes a real Developer ID signature; otherwise it signs ad hoc, which is enough to run
locally. On a Mac, a kosa8d built with plain go build and left unsigned
lacks that entitlement.
make cross matters because the macOS build cannot see two things: the files tagged
for Linux, and the guest helper, which is Linux-only. make fips builds against Go's
FIPS 140-3 module and make test-fips runs the tests under it.
The source layout
| Path | What it is for |
|---|---|
cmd/kosa8 | The command-line program. A few lines that call internal/cli. |
cmd/kosa8d | The background service: a rootless, per-user process serving the Docker-compatible API and the native API on a unix socket. Its start-up checks the organisation policy before anything is opened. |
cmd/kosa8-guestd | The helper inside each virtual machine. It is the guest's init, built for Linux only. |
cmd/kosa8-docs | Dumps the command tree as JSON, so the command reference on this site is generated from the binary and cannot drift from it. |
internal/cli | The command tree. Every command supports --json, and errors carry a hint about what to do next. |
pkg/apid | Serves the service's API: the Docker-compatible tree and the native /kosa8/v1 tree. Identifies the caller and writes the audit entry for each state-changing request. |
pkg/apid/gate | Puts the organisation policy's signed-approval rules in front of every route, and holds the table that classifies each one. |
pkg/apid/native | The handlers for /kosa8/v1: sandboxes, snapshots, agents, audit, labels. |
pkg/apid/compat | The Docker Engine API tree, for the docker CLI, Compose, Testcontainers and Dev Containers. |
pkg/apid/ops | Engine operations both trees share, such as pulling an image, with the policy checks that belong to them. |
pkg/sandbox | Manages per-session virtual machines and their snapshots: create, run, snapshot, fork, restore. Starts each sandbox's network and broker. |
pkg/vmm | The machine layer: booting a microVM, saving its memory and processes, bringing them back. One contract, one backend per operating system. |
pkg/enginevm, pkg/engine | The shared engine VM, and the protocol the host speaks to the engine inside any VM. |
pkg/vnet | Gives each sandbox its own userspace network, and the guard that checks every frame against the sandbox's network rule. |
pkg/broker | The per-sandbox HTTP egress broker: the one door a sandbox's traffic leaves through, and where a real API key is kept so it never enters the sandbox. |
pkg/kit | Sandbox Kits: the description of an agent's environment, pinned packages, signatures. |
pkg/mcpd | kosa8 as an MCP server: its own tools, the gateway for third-party tool servers, pinned tool definitions, held calls. |
pkg/govmcp | The signed MCP policy: which servers and tools each scope may use. |
pkg/schema | The typed layer under it: checks arguments and results against JSON Schema, and decides calls by argument values. |
pkg/scorer | An optional local classifier that decides whether a call a rule marked "ask" is routine enough to proceed. |
pkg/govern | The signed organisation policy: its fields, signing, verification and fail-closed loading. |
pkg/agentid | Gives an AI agent an identity the service can check: tokens, scopes, revocation. |
pkg/identity | Who signed something: a named person and the key that speaks for them. |
pkg/approve | The gate between "the assistant decided to do this" and "it happened": classifying actions, and signed approvals. |
pkg/policyd | The hash-chained audit log: entries, references, verification. |
pkg/aiaudit | Puts what an AI workload did into that log: tool decisions and model exchanges, sent to the service because it is the log's only writer. |
pkg/auditfwd | Ships the audit log off the machine and applies retention. |
pkg/evidence | Packages what was recorded about a sandbox, a snapshot or a stretch of time into one signed artifact. |
pkg/model, pkg/infer | The local model runtime with OpenAI- and Anthropic-compatible endpoints, and kosa8's own inference engine. |
pkg/billing | Licensing and usage metering. Checked in the clients; the service holds no licence. |
pkg/registry | A minimal OCI registry client for pulling images. |
pkg/crypt, pkg/seal, pkg/label | Snapshot encryption, signed snapshot records, and classification labels. |
pkg/paths | Every on-disk location, in one place. |
Each package starts with a comment that says why it exists and what it deliberately does not do. Read that before the code.
The path of a request
- A command (
internal/cli) or an AI client's tool call (pkg/mcpd) becomes an HTTP request on the service's socket. pkg/apidworks out who is asking: it verifies an agent token if one was sent, and refuses a bad one. For a state-changing request it also wraps the handler, so an audit entry is written with the final status however the request ends.pkg/apid/gatewraps every route. If the organisation policy asks for signed approvals of this action, the call needs one, and it is spent here.- The handler in
pkg/apid/nativechecks the policy fields that apply, adds notes and references to the audit entry, and calls the manager. pkg/sandboxowns the sandbox's whole life.- It boots the machine through
pkg/vmm, gives it a network throughpkg/vnet, and starts apkg/brokerwhen credentials were supplied. - Work inside the machine is asked of the guest helper over a control channel that is not a network connection.
From JavaScript and TypeScript
For Node programs there are two npm packages over this API, so you do not have to speak HTTP:
Choosing a path compares them with MCP and the Docker API.
The service's HTTP API
The service listens on a unix socket, ~/.kosa8/run/kosa8.sock. The socket's file
permissions let only your user open it, and that is the whole of its access control: there is no
token on it. It serves two trees.
/vX.Y/
prefixes: /containers/json, /images/create and so on. Point any Docker
client at it with DOCKER_HOST=unix://$HOME/.kosa8/run/kosa8.sock. It reports Docker
Engine 28.3.0, API 1.51. Since v0.9.10 it serves bind mounts below the directories named with
kosa8 daemon start --allow-mount DIR[:rw], docker cp
(/containers/{id}/archive), POST /build, POST /commit, and
exec with stdin, -w, -e and -u. The Docker socket and
~/.kosa8 are never shared into a container, so Testcontainers needs
TESTCONTAINERS_RYUK_DISABLED=true. Tested clients:
dockerode and testcontainers-node, and
more on kosa8.com./kosa8/v1. Sandboxes, snapshots,
agents, audit. JSON in, JSON out. Errors are
{"error": "…", "remediation": "…"} with an ordinary HTTP status.Headers
| Header | Meaning |
|---|---|
X-Kosa8-Actor | Who you say you are: human, agent or tool. It is recorded in the audit log as a claim. Without it you are recorded as tool, which is the honest value for your own program. |
X-Kosa8-Agent-Token | An agent's token from kosa8 agent token create. The service verifies it, treats the request as that agent's, and names the agent in the audit entry. A bad, expired or revoked token gets 401, never an anonymous pass. |
X-Kosa8-Approval | The id of a recorded signed approval, for a call the organisation policy requires one for. It is spent by the one call it covers. |
Native routes an integrator needs
| Method | Path | Purpose |
|---|---|---|
| GET | /kosa8/v1/health | Is the service up; its version, commit and uptime. |
| GET | /kosa8/v1/sandboxes | List sandboxes. |
| POST | /kosa8/v1/sandboxes | Create and boot a sandbox. Body below. |
| DELETE | /kosa8/v1/sandboxes/{id} | Remove a sandbox. ?wipe=1 overwrites its disks first. |
| POST | /kosa8/v1/sandboxes/{id}/down, /up | Stop a sandbox keeping its disk; start it again. |
| POST | /kosa8/v1/sandboxes/{id}/pull | Pull an image into the sandbox. Body {"ref": "alpine"}. |
| POST | /kosa8/v1/sandboxes/{id}/run | Run a container to completion. Body {"image", "args", "env"}. The response is a stream of JSON lines. |
| POST | /kosa8/v1/sandboxes/{id}/exec | Run a command in a running container. Body {"container", "args"}. |
| GET | /kosa8/v1/sandboxes/{id}/egress | The network rule in force and what it has passed and refused. |
| POST | /kosa8/v1/sandboxes/{id}/snapshot | Save the running machine. Body {"id", "encrypt"}. |
| POST | /kosa8/v1/sandboxes/{id}/fork | Copy a running sandbox into live copies. Body {"n", "prefix"}. |
| GET | /kosa8/v1/snapshots | List snapshots. |
| POST | /kosa8/v1/snapshots/{id}/restore | Boot a sandbox from a snapshot. Body {"name"}. |
| DELETE | /kosa8/v1/snapshots/{id} | Delete a snapshot. |
| GET | /kosa8/v1/agents | List agent identities. |
| POST | /kosa8/v1/agents | Mint an identity. Body {"name", "scope", "ttl_seconds"}; the token is in the response, once. A request from an agent is refused. |
| GET | /kosa8/v1/agents/whoami | Which identity the presented token is. |
| DELETE | /kosa8/v1/agents/{id} | Revoke an identity at once. Never held for approval. |
| GET | /kosa8/v1/audit | Recent audit entries. ?limit=N, with 0 for all. |
| GET | /kosa8/v1/audit/verify | Verify the audit chain. |
| POST | /kosa8/v1/ai/records | How the MCP gateway and model gateway put their records into the audit log. |
| POST | /kosa8/v1/pull | Pull an image into the shared engine. Body {"ref"}. |
| POST | /kosa8/v1/run | Run a container on the shared engine. Body {"image", "args"}. Refused when the policy sets require_sandbox. |
| GET | /kosa8/v1/containers, /images | List containers and images on the shared engine. |
The full list is the table in pkg/apid/gate/routes.go, which the service will not
start without.
Worked example: a sandbox behind a broker
This creates a sandbox whose only way out is its egress broker. The real API key goes in the request and stays with the service; the response gives you a placeholder to hand to the sandbox instead.
$ curl --unix-socket ~/.kosa8/run/kosa8.sock \
-X POST http://localhost/kosa8/v1/sandboxes \
-H 'Content-Type: application/json' \
-d @- <<EOF
{
"name": "review-1",
"egress": "deny",
"mounts": [
{"host_path": "/Users/you/code/api", "guest_path": "/work", "read_only": true}
],
"broker": {
"allow": [
{"host": "registry.npmjs.org"}
],
"routes": [
{"name": "anthropic",
"upstream": "https://api.anthropic.com",
"header": "x-api-key",
"methods": ["POST"],
"paths": ["/v1/messages"],
"secret": "$ANTHROPIC_API_KEY"}
]
}
}
EOF
The answer is 201 Created:
{
"sandbox": {
"id": "review-1",
"name": "review-1",
"state": "running",
"egress": "deny",
"broker": {
"url": "http://192.168.127.254:57504",
"hosts": ["registry.npmjs.org"],
"routes": ["anthropic -> api.anthropic.com"]
},
…
},
"boot_ms": 653,
"broker": {
"url": "http://192.168.127.254:57504",
"placeholders": {
"anthropic": "kosa8-placeholder-1803c857986afe6bc9f59600e4d6f6f8"
}
}
}
| Field | Meaning |
|---|---|
egress | deny, or allow:host,host. With a broker it may be left empty, which means deny; any is refused, because beside an open network the broker would be optional. |
mounts[] | host_path, guest_path, read_only. |
broker.allow[] | Hosts the sandbox may reach through the broker. host is an exact name or *.domain; optional ports (default 443 and 80) and methods. |
broker.routes[] | Upstreams the sandbox has a credential for: name, upstream, header, optional scheme, methods, paths, and secret, the real value. |
top-level broker.url | The broker as the sandbox sees it. A route's base address is this plus / and the route's name. |
top-level broker.placeholders | Route name to the stand-in the sandbox should present. Returned here and nowhere else. |
Give the sandbox the stand-ins as environment variables when you run something in it. The proxy variables send everything else through the broker too:
$ curl --unix-socket ~/.kosa8/run/kosa8.sock \ -X POST http://localhost/kosa8/v1/sandboxes/review-1/pull -d '{"ref": "node:22-alpine"}' $ curl --unix-socket ~/.kosa8/run/kosa8.sock -N \ -X POST http://localhost/kosa8/v1/sandboxes/review-1/run -d '{ "image": "node:22-alpine", "args": ["sh", "-lc", "env | grep -c ANTHROPIC"], "env": [ "ANTHROPIC_BASE_URL=http://192.168.127.254:57504/anthropic", "ANTHROPIC_API_KEY=kosa8-placeholder-1803c857986afe6bc9f59600e4d6f6f8", "HTTPS_PROXY=http://192.168.127.254:57504", "HTTP_PROXY=http://192.168.127.254:57504", "NO_PROXY=192.168.127.254" ]}' $ curl --unix-socket ~/.kosa8/run/kosa8.sock \ -X DELETE http://localhost/kosa8/v1/sandboxes/review-1
run answers with one JSON object per line: output frames with stream
and base64 data, then a last frame with done and the exit status.
The secret travels once, over the socket only your user can open. The service keeps it in
memory for as long as the sandbox runs and writes it nowhere: not to the sandbox's record, not to
a snapshot, not to the audit log, which records route names and upstreams only. The consequence
is that a brokered sandbox that is stopped and started, restored or forked comes back with its
network closed and no broker. Create a fresh one each time, as
kosa8 agent run does.
Driving the CLI instead
You do not have to speak HTTP. Every command takes --json and prints one JSON
document with a stable shape in place of the table:
$ kosa8 sandbox ls --json $ kosa8 audit ls --json -n 0 $ kosa8 agent kits --json
A failing command exits non-zero and says what to do next. --approval ID is the
other global flag; it sends the X-Kosa8-Approval header for you.
Writing a new MCP tool
kosa8's own tools are registered in registerTools in
pkg/mcpd/tools.go. Each is a thin, typed wrapper over the same service API the CLI
uses, so an agent and a person see the same behaviour. A new tool is four things, and the build
tells you if you forget the two that matter most.
An input struct
type sandboxRunIn struct {
Sandbox string `json:"sandbox" jsonschema:"sandbox name"`
Image string `json:"image" jsonschema:"image to run inside the sandbox"`
Command []string `json:"command,omitempty" jsonschema:"command and arguments"`
Approval string `json:"approval,omitempty" jsonschema:"id of a recorded signed approval …"`
}
The jsonschema tags are what the model reads to learn each argument. Tools that
change state take an approval argument and begin with
s := s.with(in.Approval), so a signed approval can be presented through the tool.
A new input type also needs a case in the type switch inside add.
A call to add
add("sandbox_run", "Run a container inside a sandbox's private engine, isolated from the host.", true,
func(ctx context.Context, _ *mcp.CallToolRequest, in sandboxRunIn) (*mcp.CallToolResult, any, error) {
s := s.with(in.Approval)
out, exit, err := s.postStream("/kosa8/v1/sandboxes/"+url.PathEscape(in.Sandbox)+"/run",
map[string]any{"image": in.Image, "args": in.Command})
…
return text(out), nil, nil
})
The third argument says whether the tool changes state, which becomes the hint MCP clients
use to ask before calling it. add wraps every handler so that the licence check and
the MCP policy run before it.
A price, in toolFeature mandatory
pkg/mcpd/gate.go lists what each tool costs: an empty string for free, or the
feature it belongs to. A tool with no entry would default to free, so the server refuses to
start instead:
panic: mcpd: tool NAME has no entry in toolFeature
A classification for any new route mandatory
If the tool needs a new route on the service, register it in pkg/apid/native
and add the exact same pattern to Routes in pkg/apid/gate/routes.go.
The entry says what kind of action a call performs, for the signed-approval rules, or why the
route never needs approval. A route nobody classified would default to ungated, so the service
refuses to start:
panic: gate: route "PATTERN" has no entry in gate.Routes — say which approval kind it performs, or why it needs none
Two tests hold the tables to reality: TestEveryToolIsPriced in
pkg/mcpd and TestEveryMutatingRouteIsClassified in
pkg/apid.
The rules the code follows
if present && err != nil { refuse } appears at every enforcement point. The
same holds for values a rule cannot evaluate: an argument of the wrong type counts against the
caller.pkg/aiaudit). Two writers would mean two ideas of what the last hash
is.govern.Profile or the MCP policy after the first
release is omitempty, so a policy that does not use it produces the bytes it always
did and its old signature still verifies.pkg/approve does not recognise is treated as escalating, the most cautious class.
kosa8 cannot bound the damage of something it does not know.The test layers
| Layer | How to run it | What it needs |
|---|---|---|
| Unit tests, beside each package | make test, make test-race | Go. |
Frame-level network tests in pkg/vnet | go test ./pkg/vnet | Go. They drive real frames through the network stack: TestBrokerPinholeReachesTheBrokerAndNothingElse proves the hole to the broker is one port wide. |
| Sandbox lifecycle in a real VM | make test-integration, which runs KOSA8_INTEGRATION=1 sh test/integration/sandbox-lifecycle.sh | Guest assets and a signed kosa8d. It skips itself, with the reason, where the host cannot run it. |
| Egress broker in a real VM | KOSA8_INTEGRATION=1 sh test/integration/egress-broker.sh | A running kosa8d and the network; it calls httpbin.org as the stand-in upstream. |
| Docker CLI compatibility | sh test/compat/docker-cli.sh (make test-compat) | The docker CLI and a running kosa8d. |
| MCP protocol | python3 test/compat/mcp.py (make test-mcp) | A running kosa8d. |
| MCP gateway | python3 test/compat/mcp-gateway.py | A running kosa8d. It registers its own throwaway server and removes it. |
| Governed MCP gateway | HOME=/scratch KOSA8_COMPAT_ISOLATED=1 python3 test/compat/mcp-governance.py | A scratch HOME with guest assets under .kosa8/guest and a kosa8d started there. |
mcp-governance.py pins a signing key and installs signed policies. On a state
directory anyone uses, that would govern their machine with a test policy. It refuses to run
unless KOSA8_COMPAT_ISOLATED=1 is set and no key is pinned yet.
Fuzz targets
$ go test ./pkg/schema -fuzz FuzzEvaluate # argument rules against arbitrary arguments $ go test ./pkg/scorer -fuzz FuzzHeuristic # the deterministic veto in front of the scorer
Both take input an agent or a third-party server controls, which is why they are the ones fuzzed.
Further reading in the repository
The design notes in docs/design/ say why each subsystem is the way it is, and what
it does not do. They are in the source tree, not on this site.
| File | Subject |
|---|---|
agent-security.md | The broker, pinned tools, argument rules, agent identity, the scorer and kit supply chain. |
identity-and-approvals.md | Personal signing keys and signed approvals. |
audit-forwarding.md | Shipping the audit log to a SIEM, and retention. |
evidence.md | Signed evidence packages. |
host-egress.md | Enforcing network rules on the host. |
encryption-at-rest.md, sealed-snapshots.md, edge-sync.md | Snapshot encryption, sealing and transfer. |
labels-and-cui.md | Classification labels. |
ai-assurance.md | Approved models and recording what a model was asked. |
malware-lab.md, cyber-range.md, gxp-validation.md | The sector packs built on the same pieces. |
Where to go next
JavaScript and TypeScript
The npm packages, MCP and the Docker API from Node, with tested recipes.
How kosa8 works
The three programs and the boundary, with the picture.
The audit log
The entry format your integration's requests will produce.
Policy reference
Every policy field and where the code enforces it.
Writing a kit
The file behind kosa8 agent run.