@kosa8/sdk 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kosa8
4
+
5
+ This license covers the source code in this repository (the kosa8
6
+ TypeScript SDK packages, @kosa8/sdk and @kosa8/ai-sdk-sandbox) only. It does
7
+ not cover kosa8 itself (the kosa8 CLI, the kosa8d daemon or any other kosa8
8
+ software), which is licensed separately.
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,261 @@
1
- # Temporary Holding Version
1
+ # @kosa8/sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Typed TypeScript client for the [kosa8](https://kosa8.com) native API. kosa8 gives every AI agent its own sealed machine: a real microVM with its own kernel, with the network closed except for the hosts on an allow-list you write. This package talks to the kosa8 daemon over its unix socket. It covers sandboxes, running and exec'ing commands, the sandbox toolbox (code, files, git, shell sessions, terminals, previews, lifecycle, templates), checkpoint and rewind, fork, explore, and snapshots.
4
+
5
+ - No runtime dependencies. It uses Node's own `http` with `socketPath`.
6
+ - ESM and CommonJS builds with `.d.ts` types. Node 18 or later.
7
+ - Every endpoint, field and response shape comes from the daemon's source.
8
+ - Errors are typed and keep the daemon's own words: what went wrong, and what to do about it.
9
+
10
+ ## Requirements
11
+
12
+ - kosa8 installed, with its daemon running (`kosa8 daemon start`). Install it with:
13
+
14
+ ```sh
15
+ curl -fsSL https://kosa8.com/install.sh | sh
16
+ ```
17
+
18
+ - macOS on Apple silicon, or x86-64 Linux.
19
+ - Node.js 18 or later.
20
+
21
+ ## Install
22
+
23
+ ```sh
24
+ npm install @kosa8/sdk
25
+ ```
26
+
27
+ ## Quickstart
28
+
29
+ ```ts
30
+ import { Kosa8Client } from '@kosa8/sdk';
31
+
32
+ const kosa8 = new Kosa8Client(); // $HOME/.kosa8/run/kosa8.sock
33
+
34
+ console.log(await kosa8.version());
35
+
36
+ // A microVM that can reach only api.anthropic.com.
37
+ const { sandbox } = await kosa8.createSandbox({
38
+ name: 'demo',
39
+ egressAllow: ['api.anthropic.com'],
40
+ });
41
+
42
+ // The sandbox has its own container engine. Images are fetched on the host.
43
+ await kosa8.pullIntoSandbox('demo', 'alpine:latest');
44
+
45
+ // Run a container to completion.
46
+ const r = await kosa8.run('demo', 'alpine:latest', ['sh', '-c', 'echo hi; exit 3']);
47
+ console.log(r.exitCode, r.stdout); // 3 "hi\n"
48
+
49
+ // Or keep a container running and exec into it.
50
+ const cid = await kosa8.startContainer('demo', 'alpine:latest', ['sh', '-c', 'while :; do sleep 3600; done']);
51
+ await kosa8.exec('demo', cid, ['sh', '-c', 'echo v1 > /note']);
52
+
53
+ // Undo button: checkpoint, break something, rewind. Processes come back too.
54
+ await kosa8.checkpoint('demo', { label: 'before the risky bit' });
55
+ await kosa8.exec('demo', cid, ['sh', '-c', 'rm /note']);
56
+ await kosa8.rewind('demo');
57
+ console.log((await kosa8.exec('demo', cid, ['cat', '/note'])).stdout); // "v1\n"
58
+
59
+ // Try approaches in parallel, then keep the one that works.
60
+ const best = await kosa8.explore('demo', { variants: ['npm test', 'make test'] });
61
+ console.log(best.winner);
62
+
63
+ await kosa8.deleteSandbox('demo');
64
+ ```
65
+
66
+ `kosa8.sandbox('demo')` returns a handle with the same methods, already bound to that sandbox (`run`, `exec`, `checkpoint`, `rewind`, `fork` and the rest).
67
+
68
+ ## How commands run
69
+
70
+ A kosa8 sandbox is a microVM that runs its own container engine. `run` starts a container from an image and runs it to completion. `startContainer` starts a detached one, and `exec` runs a command in it. A sandbox's engine does not pull images by itself, so pull first with `pullIntoSandbox`. The image is fetched on the host and checked against your org's registry policy, so pulling works even when the sandbox has no network.
71
+
72
+ ## API reference
73
+
74
+ All methods return promises. Every method takes `{ approval?, signal? }` as its last argument: `approval` is a signed-approval id (see Errors), and `signal` is an `AbortSignal`.
75
+
76
+ ### Client
77
+
78
+ `new Kosa8Client(options?)`
79
+
80
+ | option | default | |
81
+ | --- | --- | --- |
82
+ | `socketPath` | `$HOME/.kosa8/run/kosa8.sock` | daemon socket |
83
+ | `host` | `KOSA8_HOST` | remote worker: `https://…`, `tcp://host:port`, `host:port`, or `unix:///path.sock` |
84
+ | `token` | `KOSA8_TOKEN` (with `KOSA8_HOST`) | bearer token for a remote worker |
85
+ | `insecure` | `KOSA8_INSECURE=1` | allow plain http to a non-loopback host (refused by default, as in the CLI) |
86
+ | `workerFile` | `$HOME/.kosa8/worker.json` | the CLI's saved worker connection, used when no `host`, `KOSA8_HOST` or `socketPath` is given; `null` reads none |
87
+ | `actor` | `'tool'` | `X-Kosa8-Actor`: how the audit log files these requests (`human`, `agent`, `tool`) |
88
+ | `agentToken` | `KOSA8_AGENT_TOKEN` | agent identity token, for policies that require one |
89
+ | `checkpointStore` | `FileCheckpointStore` (`~/.kosa8/checkpoints.json`) | where checkpoint stacks live; `MemoryCheckpointStore` keeps them in memory |
90
+
91
+ Where requests go, in order: `host` or `KOSA8_HOST`; else the connection the
92
+ CLI saved (`kosa8 wsl setup` on Windows, where the engine runs in WSL2, or
93
+ `kosa8 worker join` anywhere), with its token; else the local daemon's socket.
94
+ So on Windows, once `kosa8 wsl setup` has run, `new Kosa8Client()` reaches the
95
+ WSL2 engine with no configuration.
96
+
97
+ ### Status
98
+
99
+ | method | endpoint |
100
+ | --- | --- |
101
+ | `health()` → `{ status, version, commit, go, uptime_seconds, phase }` | `GET /kosa8/v1/health` |
102
+ | `version()` → `string` | `GET /kosa8/v1/health` |
103
+ | `pull(ref)` | `POST /kosa8/v1/pull` (host engine) |
104
+
105
+ ### Sandboxes
106
+
107
+ | method | endpoint |
108
+ | --- | --- |
109
+ | `createSandbox({ name?, cpus?, memMb?, diskGb?, mounts?, egress?, egressAllow?, capture?, labels?, broker?, autoStopMin?, ttlMin?, autoDeleteMin?, ephemeral?, template? })` → `{ sandbox, boot_ms, broker? }` | `POST /kosa8/v1/sandboxes` |
110
+ | `listSandboxes()` → `Sandbox[]` | `GET /kosa8/v1/sandboxes` |
111
+ | `getSandbox(idOrName)` → `Sandbox \| undefined` | (from the list) |
112
+ | `startSandbox(id)` | `POST /kosa8/v1/sandboxes/{id}/up` |
113
+ | `stopSandbox(id)` | `POST /kosa8/v1/sandboxes/{id}/down` |
114
+ | `deleteSandbox(id, { wipe?, keepCheckpoints? })` | `DELETE /kosa8/v1/sandboxes/{id}` (drops its checkpoints too, as `kosa8 sandbox rm` does) |
115
+ | `pullIntoSandbox(id, ref)` | `POST /kosa8/v1/sandboxes/{id}/pull` |
116
+ | `sandboxImages(id)`, `sandboxContainers(id)` | `GET …/images`, `GET …/containers` |
117
+ | `sandboxEgress(id)` → the policy enforced, and pass/deny counters | `GET …/egress` |
118
+
119
+ `egress` takes `'deny'`, `'any'`, `'fakenet'`, `'allow:a.com,*.b.com,10.0.0.0/8'` or `{ allow: [...] }`. `egressAllow: [...]` is shorthand for the allow-list. If you set neither, the daemon applies the org policy's default egress, or `any` when there is none. `broker` puts the sandbox behind kosa8's egress broker: real credentials stay on the host and the sandbox holds only placeholders (returned in `broker.placeholders`). `autoStopMin` stops the sandbox after that many idle minutes, `ttlMin` that long after it was created, `autoDeleteMin` deletes it once it has been stopped that long, and `ephemeral` deletes it whenever it stops. `template` starts it with a template's image (see Toolbox).
120
+
121
+ ### Run and exec
122
+
123
+ | method | endpoint |
124
+ | --- | --- |
125
+ | `run(id, image, args?, { env?, onStdout?, onStderr?, onStatus? })` → `ExecResult` | `POST …/run` |
126
+ | `startContainer(id, image, args?)` → container id | `POST …/start` |
127
+ | `exec(id, container, args, { onStdout?, onStderr? })` → `ExecResult` | `POST …/exec` |
128
+ | `execFrames(id, container, args)` → async iterable of frames | `POST …/exec` |
129
+ | `startInteractive(id, image, args?, { env?, tty? })` → container id | `POST …/start-interactive` |
130
+ | `writeStdin(id, container, data \| asyncIterable)` | `POST …/containers/{cid}/stdin` |
131
+ | `attach(id, container)` → `ExecResult` | `POST …/containers/{cid}/attach` |
132
+
133
+ `ExecResult` is `{ exitCode, stdout, stderr, stdoutBytes, stderrBytes }`. A non-zero exit code is a result, not an error.
134
+
135
+ ### Toolbox
136
+
137
+ The toolbox is for agents that want to work in a sandbox the way they work on a laptop: run a snippet, read and write files, use git, keep a shell. None of it needs an image pulled first; the daemon picks one (or the sandbox template's).
138
+
139
+ ```ts
140
+ const sb = kosa8.sandbox('demo');
141
+
142
+ const r = await sb.runCode('print(6 * 7)'); // python by default
143
+ console.log(r.ok, r.stdout, r.artifacts); // matplotlib charts come back as PNG bytes
144
+
145
+ await sb.writeFile('/workspace/app.py', 'print("hi")\n');
146
+ console.log(await sb.readText('/workspace/app.py'));
147
+
148
+ await sb.gitClone('https://github.com/octocat/Hello-World.git', { path: 'hw', depth: 1 });
149
+ await sb.gitCommit('hw', 'my change', { authorName: 'Agent', authorEmail: 'agent@example.com' });
150
+
151
+ await sb.shell('cd /tmp && export FOO=bar'); // the session "main" is made on first use
152
+ console.log((await sb.shell('echo $FOO; pwd')).stdout); // "bar\n/tmp\n"
153
+ ```
154
+
155
+ | method | endpoint |
156
+ | --- | --- |
157
+ | `runCode(id, code, { language?, image?, env?, timeoutS? })` → `CodeResult` | `POST …/code` |
158
+ | `listFiles(id, path?)`, `statFile(id, path)` → `FileInfo` | `GET …/files`, `GET …/files/stat` |
159
+ | `readFile(id, path)` → `Buffer`, `readText(id, path)` → `string` | `GET …/files/content` |
160
+ | `writeFile(id, path, data, { mode? })` | `PUT …/files/content` (bytes, a string or an async iterable) |
161
+ | `deleteFile(id, path, { recursive? })`, `makeDir(id, path)`, `moveFile(id, from, to)` | `DELETE …/files`, `POST …/files/mkdir`, `POST …/files/move` |
162
+ | `git(id, op, fields)` → `GitResult` | `POST …/git` (`clone`, `status`, `add`, `commit`, `push`, `pull`, `checkout`, `branches`, `log`, `diff`) |
163
+ | `gitClone(id, url, { path?, depth?, … })`, `gitStatus(id, path)`, `gitCommit(id, path, message, { addAll? })` | `POST …/git` (`gitCommit` stages everything first) |
164
+ | `createSession(id, name?, { image?, env? })`, `listSessions(id)`, `deleteSession(id, name)` | `POST/GET …/sessions`, `DELETE …/sessions/{name}` |
165
+ | `sessionExec(id, session, command, { timeoutS? })` → `ShellResult` | `POST …/sessions/{name}/exec` |
166
+ | `shell(id, command, { session?, timeoutS? })` → `ShellResult` | as `sessionExec`, making the session (default `main`) on first use |
167
+ | `startPty(id, { image?, command?, cols?, rows?, env? })` → container id, `resizePty(id, cid, cols, rows)` | `POST …/pty`, `POST …/containers/{cid}/resize` |
168
+ | `createPreview(id, port, { session?, container?, expiresInS? })` → `{ url, token, expires_at, … }` | `POST …/previews` |
169
+ | `revokePreviews()` | `POST /kosa8/v1/previews/rotate` (ends every preview link) |
170
+ | `setLifecycle(id, { autoStopMin?, ttlMin?, autoDeleteMin?, ephemeral? })` → `Sandbox` | `POST …/lifecycle` (left out keeps its value, 0 turns it off) |
171
+ | `listTemplates()`, `deleteTemplate(name)` | `GET /kosa8/v1/templates`, `DELETE /kosa8/v1/templates/{name}` |
172
+ | `buildTemplate(name, contextDir?, { dockerfile?, buildArgs?, egress?, replace?, onOutput? })` → final frame | `POST /kosa8/v1/templates` |
173
+
174
+ `CodeResult` is `{ ok, exitCode, stdout, stderr, language, image, artifacts, durationMs, timedOut, truncated }` and `ShellResult` is `{ ok, exitCode, stdout, stderr, cwd, durationMs, timedOut, truncated }`. As with `run`, a non-zero exit code is a result, and so is a git failure (`ok: false`). Only a refusal throws. `buildTemplate` packs the context directory as a tar, leaving out what `.dockerignore` names (paths and their prefixes), builds it in a throwaway sandbox, and keeps the image, so that `createSandbox({ template })` starts from it. A failed build throws `Kosa8StreamError`. The sandbox handle has every per-sandbox toolbox method, with the sandbox id already bound (`sb.runCode(code)`, `sb.sessions()`, …).
175
+
176
+ ### Snapshots, fork, explore
177
+
178
+ | method | endpoint |
179
+ | --- | --- |
180
+ | `snapshot(id, { id?, encrypt? })` | `POST …/snapshot` |
181
+ | `listSnapshots()` | `GET /kosa8/v1/snapshots` |
182
+ | `restoreSnapshot(snapshotId, { name?, label?, allowTampered? })` | `POST /kosa8/v1/snapshots/{id}/restore` |
183
+ | `deleteSnapshot(snapshotId, { wipe?, force? })` | `DELETE /kosa8/v1/snapshots/{id}` |
184
+ | `fork(id, { n?, prefix?, label? })` → `{ forks, snapshot, fork_ms, count }` | `POST …/fork` |
185
+ | `explore(id, { variants, image?, keepAll?, keep?, timeoutMs?, prefix?, pull? })` → `{ branches, winner, removed }` | pull, fork, then a run in each branch, as `kosa8 explore` |
186
+
187
+ `explore` forks the sandbox once per variant and runs `sh -c VARIANT` in `image` (default `alpine:latest`) in every branch at the same time. It keeps the fastest successful branch and deletes the others. It pulls `image` into the parent first, so every branch already has it (turn this off with `pull: false`).
188
+
189
+ ### Checkpoints
190
+
191
+ | method | |
192
+ | --- | --- |
193
+ | `checkpoint(id, { label? })` → `{ checkpoint, evicted, snapshot }` | snapshot `<id>-ckpt-<n>`, recorded on the sandbox's stack (at most 10, oldest evicted) |
194
+ | `listCheckpoints(id)` | oldest first |
195
+ | `rewind(id, { to? })` → `{ sandbox, checkpoint, dropped, restore_ms }` | back to the latest checkpoint, or to one named by id or label, with processes resumed |
196
+
197
+ The daemon has no checkpoint endpoint. Checkpoints are bookkeeping kept on the client side over snapshots, exactly as in the kosa8 CLI and MCP server. By default they go in the CLI's own store, so `kosa8 rewind` sees checkpoints you took with the SDK, and the SDK sees the CLI's. A rewind deletes the sandbox and restores the checkpoint under the same name, and checkpoints taken after the target are discarded.
198
+
199
+ ## kosa8 Cloud
200
+
201
+ The same client shape, against kosa8 Cloud instead of the local daemon. Make
202
+ an API key in the cloud dashboard and set `KOSA8_API_KEY`.
203
+
204
+ ```ts
205
+ import { Kosa8Cloud, Kosa8PaymentRequiredError } from '@kosa8/sdk';
206
+
207
+ const cloud = new Kosa8Cloud(); // or new Kosa8Cloud({ apiKey: 'k8c_live_…' })
208
+
209
+ const box = await cloud.sandboxes.create({ template: 'python', sleepAfterS: 600 });
210
+ const r = await box.exec(['python3', '-c', 'print(6 * 7)']);
211
+ console.log(r.exitCode, r.stdout); // 0 "42\n"
212
+ await box.sleep(); // costs only snapshot storage while asleep
213
+ await box.exec('ls /workspace'); // wakes it, files and all
214
+ await box.delete();
215
+
216
+ await cloud.functions.deploy('hello', {
217
+ runtime: 'python3.12',
218
+ code: 'def handler(event):\n return {"hello": event.get("name", "world")}\n',
219
+ });
220
+ const out = await cloud.functions.invoke('hello', { name: 'kosa8' });
221
+ console.log(out.ok, out.result); // true { hello: 'kosa8' }
222
+ ```
223
+
224
+ A new organization runs nothing until a payment method is on file; until
225
+ then creating a sandbox throws `Kosa8PaymentRequiredError` (`code`
226
+ `payment_required`), whose message says what to do. Every error from the
227
+ cloud carries its stable `code`.
228
+
229
+ ## Errors
230
+
231
+ Every error extends `Kosa8Error`. An error from the daemon carries `status`, `daemonMessage` (the daemon's `error`, verbatim) and `remediation` (what to do), and its `message` is `"<error> (<remediation>)"`.
232
+
233
+ | class | when |
234
+ | --- | --- |
235
+ | `Kosa8ConnectionError` | the daemon cannot be reached (is it running?) |
236
+ | `Kosa8BadRequestError` | 400: for example an egress policy the daemon refuses |
237
+ | `Kosa8AuthError` | 401: remote worker token or agent token |
238
+ | `Kosa8PolicyError` | 403: refused by the org policy, the registry allow-list or labels |
239
+ | `Kosa8ApprovalRequiredError` | 403: the policy wants signed approval from named people. `error.approval` is the daemon's refusal (`why`, `signers`, `request_command`, `requirement`, …) |
240
+ | `Kosa8NotFoundError` | 404 |
241
+ | `Kosa8ConflictError` | 409: for example the sandbox exists but is stopped, or a snapshot id is already taken |
242
+ | `Kosa8UnsupportedError` | 501: no microVM backend on this platform |
243
+ | `Kosa8StreamError` | a run, exec or template build ended with an error frame (output so far in `stdout`/`stderr`), or a file read was cut short |
244
+ | `Kosa8CheckpointError` | no checkpoint to rewind to, or an invalid name |
245
+
246
+ When an action needs approval, a human opens a request (`error.approval.request_command`), the approvers sign it, and you repeat the call with the id:
247
+
248
+ ```ts
249
+ try {
250
+ await kosa8.fork('web', { n: 8 });
251
+ } catch (err) {
252
+ if (err instanceof Kosa8ApprovalRequiredError) {
253
+ console.log(err.approval.why, err.approval.signers, err.approval.request_command);
254
+ // later: await kosa8.fork('web', { n: 8, approval: 'APPROVAL_ID' });
255
+ }
256
+ }
257
+ ```
258
+
259
+ ## License
260
+
261
+ MIT. The license covers this SDK package only, not kosa8 itself.