kosa8 docs

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
Signing on macOS

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

PathWhat it is for
cmd/kosa8The command-line program. A few lines that call internal/cli.
cmd/kosa8dThe 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-guestdThe helper inside each virtual machine. It is the guest's init, built for Linux only.
cmd/kosa8-docsDumps the command tree as JSON, so the command reference on this site is generated from the binary and cannot drift from it.
internal/cliThe command tree. Every command supports --json, and errors carry a hint about what to do next.
pkg/apidServes 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/gatePuts the organisation policy's signed-approval rules in front of every route, and holds the table that classifies each one.
pkg/apid/nativeThe handlers for /kosa8/v1: sandboxes, snapshots, agents, audit, labels.
pkg/apid/compatThe Docker Engine API tree, for the docker CLI, Compose, Testcontainers and Dev Containers.
pkg/apid/opsEngine operations both trees share, such as pulling an image, with the policy checks that belong to them.
pkg/sandboxManages per-session virtual machines and their snapshots: create, run, snapshot, fork, restore. Starts each sandbox's network and broker.
pkg/vmmThe machine layer: booting a microVM, saving its memory and processes, bringing them back. One contract, one backend per operating system.
pkg/enginevm, pkg/engineThe shared engine VM, and the protocol the host speaks to the engine inside any VM.
pkg/vnetGives each sandbox its own userspace network, and the guard that checks every frame against the sandbox's network rule.
pkg/brokerThe 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/kitSandbox Kits: the description of an agent's environment, pinned packages, signatures.
pkg/mcpdkosa8 as an MCP server: its own tools, the gateway for third-party tool servers, pinned tool definitions, held calls.
pkg/govmcpThe signed MCP policy: which servers and tools each scope may use.
pkg/schemaThe typed layer under it: checks arguments and results against JSON Schema, and decides calls by argument values.
pkg/scorerAn optional local classifier that decides whether a call a rule marked "ask" is routine enough to proceed.
pkg/governThe signed organisation policy: its fields, signing, verification and fail-closed loading.
pkg/agentidGives an AI agent an identity the service can check: tokens, scopes, revocation.
pkg/identityWho signed something: a named person and the key that speaks for them.
pkg/approveThe gate between "the assistant decided to do this" and "it happened": classifying actions, and signed approvals.
pkg/policydThe hash-chained audit log: entries, references, verification.
pkg/aiauditPuts 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/auditfwdShips the audit log off the machine and applies retention.
pkg/evidencePackages what was recorded about a sandbox, a snapshot or a stretch of time into one signed artifact.
pkg/model, pkg/inferThe local model runtime with OpenAI- and Anthropic-compatible endpoints, and kosa8's own inference engine.
pkg/billingLicensing and usage metering. Checked in the clients; the service holds no licence.
pkg/registryA minimal OCI registry client for pulling images.
pkg/crypt, pkg/seal, pkg/labelSnapshot encryption, signed snapshot records, and classification labels.
pkg/pathsEvery 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 request's path through kosa8's packages The command line and the MCP server both send requests over a socket to the service. Inside the service, apid identifies the caller and wraps the request for the audit log, apid/gate checks whether the policy wants a signed approval, the native handler does the work through the sandbox manager, and the sandbox manager drives vmm, vnet and broker. The helper inside the virtual machine is reached over a control channel. kosa8d kosa8 internal/cli mcp serve pkg/mcpd apid identify the caller; audit entry when it returns apid/gate does policy want a signed approval for this call? apid/native the /kosa8/v1 handler; notes what it did sandbox the Manager: create, run, snapshot, fork vmm boots the machine vnet its guarded network broker holds the API keys kosa8-guestd inside the VM control channel
Every way of using kosa8 enters at the same place, so every way is identified, gated and audited alike.
  1. A command (internal/cli) or an AI client's tool call (pkg/mcpd) becomes an HTTP request on the service's socket.
  2. pkg/apid works 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.
  3. pkg/apid/gate wraps every route. If the organisation policy asks for signed approvals of this action, the call needs one, and it is spent here.
  4. The handler in pkg/apid/native checks the policy fields that apply, adds notes and references to the audit entry, and calls the manager.
  5. pkg/sandbox owns the sandbox's whole life.
  6. It boots the machine through pkg/vmm, gives it a network through pkg/vnet, and starts a pkg/broker when credentials were supplied.
  7. 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:

@kosa8/sdkA typed client for the native API (Node 18+, no runtime dependencies): sandboxes, run and exec, egress and the broker, checkpoint and rewind, fork, explore, snapshots and signed approvals, with typed errors that keep the daemon's words. Start with the Quickstart.
@kosa8/ai-sdk-sandboxA kosa8 sandbox for the Vercel AI SDK 7 (Node 22+), for agents whose tools read, write and run in a microVM. See Vercel AI SDK adapter.

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.

Docker-compatibleAt the root and under /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.
NativeUnder /kosa8/v1. Sandboxes, snapshots, agents, audit. JSON in, JSON out. Errors are {"error": "…", "remediation": "…"} with an ordinary HTTP status.

Headers

HeaderMeaning
X-Kosa8-ActorWho 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-TokenAn 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-ApprovalThe 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

MethodPathPurpose
GET/kosa8/v1/healthIs the service up; its version, commit and uptime.
GET/kosa8/v1/sandboxesList sandboxes.
POST/kosa8/v1/sandboxesCreate 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, /upStop a sandbox keeping its disk; start it again.
POST/kosa8/v1/sandboxes/{id}/pullPull an image into the sandbox. Body {"ref": "alpine"}.
POST/kosa8/v1/sandboxes/{id}/runRun a container to completion. Body {"image", "args", "env"}. The response is a stream of JSON lines.
POST/kosa8/v1/sandboxes/{id}/execRun a command in a running container. Body {"container", "args"}.
GET/kosa8/v1/sandboxes/{id}/egressThe network rule in force and what it has passed and refused.
POST/kosa8/v1/sandboxes/{id}/snapshotSave the running machine. Body {"id", "encrypt"}.
POST/kosa8/v1/sandboxes/{id}/forkCopy a running sandbox into live copies. Body {"n", "prefix"}.
GET/kosa8/v1/snapshotsList snapshots.
POST/kosa8/v1/snapshots/{id}/restoreBoot a sandbox from a snapshot. Body {"name"}.
DELETE/kosa8/v1/snapshots/{id}Delete a snapshot.
GET/kosa8/v1/agentsList agent identities.
POST/kosa8/v1/agentsMint 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/whoamiWhich identity the presented token is.
DELETE/kosa8/v1/agents/{id}Revoke an identity at once. Never held for approval.
GET/kosa8/v1/auditRecent audit entries. ?limit=N, with 0 for all.
GET/kosa8/v1/audit/verifyVerify the audit chain.
POST/kosa8/v1/ai/recordsHow the MCP gateway and model gateway put their records into the audit log.
POST/kosa8/v1/pullPull an image into the shared engine. Body {"ref"}.
POST/kosa8/v1/runRun a container on the shared engine. Body {"image", "args"}. Refused when the policy sets require_sandbox.
GET/kosa8/v1/containers, /imagesList 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"
    }
  }
}
FieldMeaning
egressdeny, 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.urlThe broker as the sandbox sees it. A route's base address is this plus / and the route's name.
top-level broker.placeholdersRoute 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.

Where the secret goes

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

Fail closedA policy that is present and does not verify refuses; it never falls back to "no policy". The pattern 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.
One writer for the audit chainOnly the service appends to the log. Other processes that see auditable things, the MCP gateway and the model gateway, send their records to it (pkg/aiaudit). Two writers would mean two ideas of what the last hash is.
Signed structs only gain optional fieldsA policy is signed over its JSON. Every field added to 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.
Unknown is escalatingAn action kind the classifier in pkg/approve does not recognise is treated as escalating, the most cautious class. kosa8 cannot bound the damage of something it does not know.
Defaults are refused, not assumedAn unpriced tool and an unclassified route stop the program at start-up. Shipping the quiet default is how a control ends up enforced from one client only.
Secrets stay in memoryBrokered credentials and agent tokens are never written as themselves: the first live in the service's memory, the second are stored as digests.

The test layers

LayerHow to run itWhat it needs
Unit tests, beside each packagemake test, make test-raceGo.
Frame-level network tests in pkg/vnetgo test ./pkg/vnetGo. They drive real frames through the network stack: TestBrokerPinholeReachesTheBrokerAndNothingElse proves the hole to the broker is one port wide.
Sandbox lifecycle in a real VMmake test-integration, which runs KOSA8_INTEGRATION=1 sh test/integration/sandbox-lifecycle.shGuest assets and a signed kosa8d. It skips itself, with the reason, where the host cannot run it.
Egress broker in a real VMKOSA8_INTEGRATION=1 sh test/integration/egress-broker.shA running kosa8d and the network; it calls httpbin.org as the stand-in upstream.
Docker CLI compatibilitysh test/compat/docker-cli.sh (make test-compat)The docker CLI and a running kosa8d.
MCP protocolpython3 test/compat/mcp.py (make test-mcp)A running kosa8d.
MCP gatewaypython3 test/compat/mcp-gateway.pyA running kosa8d. It registers its own throwaway server and removes it.
Governed MCP gatewayHOME=/scratch KOSA8_COMPAT_ISOLATED=1 python3 test/compat/mcp-governance.pyA scratch HOME with guest assets under .kosa8/guest and a kosa8d started there.
Never run the governance suite against your own state

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.

FileSubject
agent-security.mdThe broker, pinned tools, argument rules, agent identity, the scorer and kit supply chain.
identity-and-approvals.mdPersonal signing keys and signed approvals.
audit-forwarding.mdShipping the audit log to a SIEM, and retention.
evidence.mdSigned evidence packages.
host-egress.mdEnforcing network rules on the host.
encryption-at-rest.md, sealed-snapshots.md, edge-sync.mdSnapshot encryption, sealing and transfer.
labels-and-cui.mdClassification labels.
ai-assurance.mdApproved models and recording what a model was asked.
malware-lab.md, cyber-range.md, gxp-validation.mdThe sector packs built on the same pieces.

Where to go next