kosa8 docs

Guides / JavaScript & TypeScript / Connecting

Connecting

By default the client talks to the daemon's unix socket in your home folder, and needs no configuration. This page covers the other places it can connect, the environment variables it reads the same way the CLI does, and the two headers that tell the audit log who is calling.

Where requests go

resolveEndpoint is what the client uses to decide. It sends nothing, so you can call it to check a configuration:

import { resolveEndpoint, defaultSocketPath } from '@kosa8/sdk';

// Where a client would connect, for each way of saying it. No request is sent.
console.log('default socket:', defaultSocketPath());
const cases = [
  {},
  { socketPath: '/var/run/kosa8-ci.sock' },
  { host: 'unix:///srv/kosa8/kosa8.sock' },
  { host: 'https://worker.example.com', token: 't0ken' },
  { host: 'tcp://127.0.0.1:7440' },
  { host: 'localhost:7440' },
  { env: { KOSA8_HOST: 'https://worker.example.com/', KOSA8_TOKEN: 'from-env' } },
  { host: 'http://10.0.0.5:7440', insecure: true },
  { host: 'http://10.0.0.5:7440' },
  { host: 'ssh://worker' },
];
for (const opts of cases) {
  try {
    const e = resolveEndpoint({ env: {}, ...opts });
    console.log(JSON.stringify(opts), '->', e.kind === 'unix' ? `unix ${e.socketPath}` : `${e.url.href} token=${e.token ?? '-'}`);
  } catch (err) {
    console.log(JSON.stringify(opts), '->', err.constructor.name + ':', err.message);
  }
}
$ node conn1.mjs
default socket: /tmp/k8d/.kosa8/run/kosa8.sock
{} -> unix /tmp/k8d/.kosa8/run/kosa8.sock
{"socketPath":"/var/run/kosa8-ci.sock"} -> unix /var/run/kosa8-ci.sock
{"host":"unix:///srv/kosa8/kosa8.sock"} -> unix /srv/kosa8/kosa8.sock
{"host":"https://worker.example.com","token":"t0ken"} -> https://worker.example.com/ token=t0ken
{"host":"tcp://127.0.0.1:7440"} -> http://127.0.0.1:7440/ token=-
{"host":"localhost:7440"} -> http://localhost:7440/ token=-
{"env":{"KOSA8_HOST":"https://worker.example.com/","KOSA8_TOKEN":"from-env"}} -> https://worker.example.com/ token=from-env
{"host":"http://10.0.0.5:7440","insecure":true} -> http://10.0.0.5:7440/ token=-
{"host":"http://10.0.0.5:7440"} -> Kosa8Error: refusing to talk to 10.0.0.5:7440 over plain http: the token and every request would cross the network unencrypted. Use https://10.0.0.5:7440 (a worker behind a TLS-terminating tunnel or proxy), or set insecure: true / KOSA8_INSECURE=1 if this link is already private and trusted
{"host":"ssh://worker"} -> Kosa8Error: KOSA8_HOST scheme "ssh" is not supported; use http://, https://, tcp:// or unix://

The rules

SettingOptionEnvironmentNotes
Socket pathsocketPath(none)Default $HOME/.kosa8/run/kosa8.sock. Ignored when a host is set.
HosthostKOSA8_HOSThttps://h, http://h:p, tcp://h:p (same as http://), bare h:p (also http://), or unix:///path.sock. Trailing slashes are dropped; a path prefix (https://h/kosa8) is kept.
TokentokenKOSA8_TOKENSent as Authorization: Bearer … to a TCP host. The variable is read only when the host also came from KOSA8_HOST; with a host option, pass token too.
Plain httpinsecureKOSA8_INSECURE=1Plain http:// to anything but loopback (localhost, 127.x, ::1) is refused unless set, as in the CLI.
Actoractor(none)'tool' (default), 'agent' or 'human'. Below.
Agent tokenagentTokenKOSA8_AGENT_TOKENAn agent identity. Below. agentToken: '' ignores the variable.
Extra headersheaders(none)Added to every request.
Environmentenv(none)Where KOSA8_* are read from. Default process.env. Useful in tests.

Who is calling: actor and agent token

Every state-changing request is written to kosa8's hash-chained audit log, with who made it. Two headers set that.

X-Kosa8-ActorA claim: human, agent or tool. The SDK sends tool unless told otherwise, which is the honest value for a program. Set actor: 'agent' when a model is choosing the calls, and 'human' when a person clicked a button in your app.
X-Kosa8-Agent-TokenProof, not a claim. A token from kosa8 agent token create NAME. The daemon verifies it and names the agent in each audit entry. A wrong, expired or revoked token gets 401, never an anonymous pass. Org policies can require one, and can scope what each agent may do.
$ kosa8 agent token create ci-reviewer --ttl 1h
# prints the agent's id and its token, once (shown here from --json, token cut short)
{'agent': {'id': 'agt_371b55e04e83', 'name': 'ci-reviewer', 'created': '2026-10-16T00:37:43.504918Z', 'expires': '2026-10-16T01:37:43.504918Z'}, 'token': 'k8a_371b55e0…'}
import { Kosa8Client, Kosa8AuthError } from '@kosa8/sdk';

// KOSA8_AGENT_TOKEN is read from the environment; agentToken: '...' sets it explicitly.
const agent = new Kosa8Client({ actor: 'agent' });
await agent.createSandbox({ name: 'agent-box', egress: 'deny' });
await agent.deleteSandbox('agent-box');

// A person at a keyboard driving your tool says so. (agentToken: '' ignores the variable.)
const human = new Kosa8Client({ actor: 'human', agentToken: '' });
await human.stopSandbox('runner');
await human.startSandbox('runner');

// A wrong token is refused, never treated as anonymous.
const forged = new Kosa8Client({ agentToken: 'k8a_not-a-real-token' });
try {
  await forged.listSandboxes();
} catch (err) {
  console.log(err instanceof Kosa8AuthError, err.status, err.message);
}
$ KOSA8_AGENT_TOKEN=k8a_… node conn3.mjs
true 401 agent token is not recognised; mint one with `kosa8 agent token create NAME`
$ kosa8 audit ls -n 5
SEQ  TIME      ACTOR  ACTION              TARGET          STATUS
369  00:38:03  agent  sandboxes.create                    201
370  00:38:03  agent  sandboxes.delete    agent-box       200
371  00:38:03  human  sandboxes.down      runner          200
372  00:38:04  human  sandboxes.up        runner          200
373  00:38:04  agent  agent.auth.refused  /kosa8/v1/sa..  401

With --json, the agent's entries carry its verified identity:

{'actor': 'agent', 'action': 'sandboxes.create', 'target': None, 'status': 201, 'refs': {'agent': {'id': 'agt_371b55e04e83', 'name': 'ci-reviewer'}}}
{'actor': 'agent', 'action': 'sandboxes.delete', 'target': 'agent-box', 'status': 200, 'refs': {'agent': {'id': 'agt_371b55e04e83', 'name': 'ci-reviewer'}}}
{'actor': 'human', 'action': 'sandboxes.down', 'target': 'runner', 'status': 200, 'refs': None}

Reads such as listSandboxes are not audited; only actions that change something are. The refused forged token is recorded too.

Over TCP: --listen and remote workers

kosa8 daemon start --listen HOST:PORT serves the API over TCP behind a generated token in ~/.kosa8/worker-token, so another device can drive the machine. While it does, a v0.9.10 daemon replaces the unix socket: the CLI follows automatically, but the SDK does not, so point it at the TCP address. (Fixed in kosa8 0.9.36: the unix socket is served beside the TCP listener, unchanged and without a token, so the default client keeps working on that machine; only the TCP port needs the token.) On one machine, over loopback:

$ kosa8 daemon start --listen 127.0.0.1:7440
kosa8d started (pid 39066, log /tmp/k8d/.kosa8/log/kosa8d.log)
import { Kosa8Client, Kosa8AuthError } from '@kosa8/sdk';

// KOSA8_HOST and KOSA8_TOKEN, as the CLI reads them.
const kosa8 = new Kosa8Client();
console.log('target:', kosa8.transport.target);
console.log('version over TCP:', await kosa8.version());
console.log('sandboxes:', (await kosa8.listSandboxes()).map((s) => `${s.id}=${s.state}`));

const wrong = new Kosa8Client({ host: 'http://127.0.0.1:7440', token: 'wrong' });
await wrong.listSandboxes().catch((err) => console.log(err instanceof Kosa8AuthError, err.status, err.message));
$ KOSA8_HOST=http://127.0.0.1:7440 KOSA8_TOKEN=$(cat ~/.kosa8/worker-token) node remote.mjs
target: http://127.0.0.1:7440
version over TCP: 0.9.10
sandboxes: [ 'runner=stopped' ]
true 401 missing or invalid bearer token (set KOSA8_TOKEN to the worker's token)

Streaming works the same way over TCP:

import { Kosa8Client } from '@kosa8/sdk';
const kosa8 = new Kosa8Client();
const up = await kosa8.startSandbox('runner');
console.log('started over TCP in', up.boot_ms, 'ms');
const r = await kosa8.run('runner', 'alpine:latest', ['sh', '-c', 'echo streamed over tcp; exit 2']);
console.log(r.exitCode, r.stdout.trim());
started over TCP in 599 ms
2 streamed over tcp

With --listen set, the default client fails with connect ENOENT …/kosa8.sock on v0.9.10, because there is no socket (from 0.9.36 there is). To go back to the socket, run kosa8 daemon stop, kosa8 worker forget, then kosa8 daemon start: --listen is remembered until you forget it.

untested A worker on another machine

The SDK accepts the same KOSA8_HOST and KOSA8_TOKEN as the CLI, so KOSA8_HOST=https://worker.example.com KOSA8_TOKEN=… should reach a worker behind a TLS-terminating proxy or tunnel. That was not tested end to end for this release: only the unix socket and loopback TCP above were. Use https:// across a network; plain http:// to a non-loopback address is refused unless you set KOSA8_INSECURE=1, because the token would cross the network in the clear.

The socket path on macOS

A unix socket path is limited to 104 bytes on macOS (108 on Linux). With a long home folder, for example inside a deep temporary directory, the daemon cannot create its socket and the client cannot reach it:

kosa8d: server stopped: listen unix /private/tmp/claude-501/…/longhome/.kosa8/run/kosa8.sock: bind: invalid argument
Kosa8ConnectionError: kosa8 daemon unreachable at unix:///private/tmp/claude-501/…/longhome/.kosa8/run/kosa8.sock: connect EINVAL …

That path was 131 bytes. Keep HOME short, or point HOME at a short symlink, as these pages' tests did with /tmp/k8d.

Next

Sandboxes: create options and the life of a sandbox.