@kosa8/sdk 0.1.0 → 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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @kosa8/sdk
2
2
 
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, checkpoint and rewind, fork, explore, and snapshots.
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
4
 
5
5
  - No runtime dependencies. It uses Node's own `http` with `socketPath`.
6
6
  - ESM and CommonJS builds with `.d.ts` types. Node 18 or later.
@@ -83,10 +83,17 @@ All methods return promises. Every method takes `{ approval?, signal? }` as its
83
83
  | `host` | `KOSA8_HOST` | remote worker: `https://…`, `tcp://host:port`, `host:port`, or `unix:///path.sock` |
84
84
  | `token` | `KOSA8_TOKEN` (with `KOSA8_HOST`) | bearer token for a remote worker |
85
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 |
86
87
  | `actor` | `'tool'` | `X-Kosa8-Actor`: how the audit log files these requests (`human`, `agent`, `tool`) |
87
88
  | `agentToken` | `KOSA8_AGENT_TOKEN` | agent identity token, for policies that require one |
88
89
  | `checkpointStore` | `FileCheckpointStore` (`~/.kosa8/checkpoints.json`) | where checkpoint stacks live; `MemoryCheckpointStore` keeps them in memory |
89
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
+
90
97
  ### Status
91
98
 
92
99
  | method | endpoint |
@@ -99,7 +106,7 @@ All methods return promises. Every method takes `{ approval?, signal? }` as its
99
106
 
100
107
  | method | endpoint |
101
108
  | --- | --- |
102
- | `createSandbox({ name?, cpus?, memMb?, diskGb?, mounts?, egress?, egressAllow?, capture?, labels?, broker? })` → `{ sandbox, boot_ms, broker? }` | `POST /kosa8/v1/sandboxes` |
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` |
103
110
  | `listSandboxes()` → `Sandbox[]` | `GET /kosa8/v1/sandboxes` |
104
111
  | `getSandbox(idOrName)` → `Sandbox \| undefined` | (from the list) |
105
112
  | `startSandbox(id)` | `POST /kosa8/v1/sandboxes/{id}/up` |
@@ -109,7 +116,7 @@ All methods return promises. Every method takes `{ approval?, signal? }` as its
109
116
  | `sandboxImages(id)`, `sandboxContainers(id)` | `GET …/images`, `GET …/containers` |
110
117
  | `sandboxEgress(id)` → the policy enforced, and pass/deny counters | `GET …/egress` |
111
118
 
112
- `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`).
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).
113
120
 
114
121
  ### Run and exec
115
122
 
@@ -125,6 +132,47 @@ All methods return promises. Every method takes `{ approval?, signal? }` as its
125
132
 
126
133
  `ExecResult` is `{ exitCode, stdout, stderr, stdoutBytes, stderrBytes }`. A non-zero exit code is a result, not an error.
127
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
+
128
176
  ### Snapshots, fork, explore
129
177
 
130
178
  | method | endpoint |
@@ -148,6 +196,36 @@ All methods return promises. Every method takes `{ approval?, signal? }` as its
148
196
 
149
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.
150
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
+
151
229
  ## Errors
152
230
 
153
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>)"`.
@@ -162,7 +240,7 @@ Every error extends `Kosa8Error`. An error from the daemon carries `status`, `da
162
240
  | `Kosa8NotFoundError` | 404 |
163
241
  | `Kosa8ConflictError` | 409: for example the sandbox exists but is stopped, or a snapshot id is already taken |
164
242
  | `Kosa8UnsupportedError` | 501: no microVM backend on this platform |
165
- | `Kosa8StreamError` | a run or exec ended with an error frame (output so far in `stdout`/`stderr`) |
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 |
166
244
  | `Kosa8CheckpointError` | no checkpoint to rewind to, or an invalid name |
167
245
 
168
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: