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
| Setting | Option | Environment | Notes |
|---|---|---|---|
| Socket path | socketPath | (none) | Default $HOME/.kosa8/run/kosa8.sock. Ignored when a host is set. |
| Host | host | KOSA8_HOST | https://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. |
| Token | token | KOSA8_TOKEN | Sent 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 http | insecure | KOSA8_INSECURE=1 | Plain http:// to anything but loopback (localhost, 127.x, ::1) is refused unless set, as in the CLI. |
| Actor | actor | (none) | 'tool' (default), 'agent' or 'human'. Below. |
| Agent token | agentToken | KOSA8_AGENT_TOKEN | An agent identity. Below. agentToken: '' ignores the variable. |
| Extra headers | headers | (none) | Added to every request. |
| Environment | env | (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.
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.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.
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.
Sandboxes: create options and the life of a sandbox.