@shardflux/sdk 0.14.0 → 0.16.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +85 -23
  2. package/README.md +226 -7
  3. package/dist/account.d.ts +2 -0
  4. package/dist/account.js +6 -0
  5. package/dist/cell.d.ts +27 -0
  6. package/dist/cell.js +71 -0
  7. package/dist/client.d.ts +54 -5
  8. package/dist/client.js +100 -7
  9. package/dist/computer.d.ts +153 -0
  10. package/dist/computer.js +229 -0
  11. package/dist/errors.d.ts +5 -1
  12. package/dist/errors.js +9 -0
  13. package/dist/executions.d.ts +2 -6
  14. package/dist/executions.js +9 -0
  15. package/dist/exit-code.d.ts +7 -0
  16. package/dist/exit-code.js +12 -0
  17. package/dist/generated/app-api.d.ts +1055 -119
  18. package/dist/generated/cell-api.d.ts +334 -0
  19. package/dist/http.d.ts +7 -1
  20. package/dist/http.js +34 -13
  21. package/dist/index.d.ts +13 -6
  22. package/dist/index.js +7 -2
  23. package/dist/ports.d.ts +7 -0
  24. package/dist/ports.js +1 -1
  25. package/dist/progress.d.ts +2 -2
  26. package/dist/progress.js +1 -1
  27. package/dist/templates.d.ts +12 -0
  28. package/dist/templates.js +9 -0
  29. package/dist/testing/index.d.ts +62 -0
  30. package/dist/testing/index.js +585 -0
  31. package/dist/testing/seed.d.ts +433 -0
  32. package/dist/testing/seed.js +449 -0
  33. package/dist/tools.d.ts +21 -3
  34. package/dist/tools.js +113 -22
  35. package/dist/tunnel-assets/linux-amd64.gz +0 -0
  36. package/dist/tunnel-assets/linux-arm64.gz +0 -0
  37. package/dist/tunnel-assets.d.ts +10 -0
  38. package/dist/tunnel-assets.js +11 -0
  39. package/dist/tunnel-packet.d.ts +3 -0
  40. package/dist/tunnel-packet.js +43 -0
  41. package/dist/tunnel-pty.d.ts +86 -0
  42. package/dist/tunnel-pty.js +243 -0
  43. package/dist/tunnels.d.ts +47 -0
  44. package/dist/tunnels.js +454 -0
  45. package/dist/workspace-ref.d.ts +87 -0
  46. package/dist/workspace-ref.js +173 -0
  47. package/dist/workspace.d.ts +40 -1
  48. package/dist/workspace.js +111 -2
  49. package/package.json +7 -2
package/CHANGELOG.md CHANGED
@@ -1,13 +1,77 @@
1
1
  # Changelog
2
2
 
3
- ## 0.14.0 — 2026-10-04 (not yet published)
4
-
5
- - Opt-in `workspaceTools(workspace, { prewake: true })` input-start hooks prepare VM tools while their input streams. Default tools, offline reads and file-first workspaces install no hook.
6
-
7
3
  Every API the README shows is available from the version named here. Breaking changes ship in minor releases and are
8
4
  marked **Breaking**.
9
5
 
10
- ## 0.14.0 (not yet published)
6
+ ## 0.16.0 (not yet published)
7
+
8
+ - Add `@shardflux/sdk/testing`: `createFakeShardflux` supplies an isolated in-memory transport beneath real SDK handles, refs and errors, with scripted commands, a virtual clock, fault controls and captured response shapes. A shared scenario manifest runs against the fake and live API.
9
+
10
+ - Add opt-in persistent workspace idle deletion retention, workspace/project setters and projected deletion times.
11
+
12
+ - Accelerate reverse tunnels with a bounded, acknowledged binary WebSocket stream, 64 KiB frames and batched credits. Select `auto`, `pty` or `exec` transport; reconnect preserves byte offsets. Opt-in binary PTY output accepts larger blocks for file transfers.
13
+
14
+ - Add opt-in resize controls to grow named resources only and return the held server answer without polling a pending operation.
15
+ - Expose typed stored workspace caps and preserve nullable grants. Fixed memory without an explicit size uses the template default.
16
+
17
+ - `WorkspaceRef.turn(fn, { afterSeconds: 0 })` and `Workspace.turn()` hint without waiting and request idle suspension when the body settles, including errors.
18
+ - Computer `act()` avoids an extra screenshot after `screenshot`/`zoom`. Viewer links are reused by kind with more than 60 seconds left; `fresh: true` remints, and `stopStream()` clears them.
19
+ - `computerToolset(ws, { beforeAction })` and `workspaceTools(ws, { computer: { beforeAction } })` check each action immediately before execution; refusal answers that action with an error and skips the rest.
20
+ - The default pooled transport honors environment HTTP(S) proxies and NO_PROXY. `SHARDFLUX_BASE_URL` aliases `SHARDFLUX_API_URL`, which takes precedence.
21
+ - Exec and execution results expose `exitCodePosix`. Missing cell file paths with `path_not_found` raise `FileNotFoundError`, distinct from workspace deletion and protocol errors.
22
+ - Embed live desktop viewers and port previews in a web app with an explicit HTTPS origin allowlist and partitioned browser sessions.
23
+
24
+ - Add `workspace.tunnels.reverse({ remotePort, target, bindAddress?, signal? })`: a private workspace TCP listener forwarded to this machine, with binary multiplexing, bounded backpressure, half-close, transport reconnection, suspend/resume, statistics and deterministic close.
25
+
26
+ - Add `ws.upgrade({ at })`, `upgradeAvailable` and `upgradePending`. An opt-in cold start preserves the workspace disk; `next_resume` schedules it. Lifecycle cold boot reasons include `upgraded`.
27
+
28
+ ### Integration testing
29
+
30
+ ## 0.15.0 — 2026-10-04
31
+
32
+ ### Workspaces by key
33
+
34
+ Additive (contracts §46). A workspace named by its key, created on its first use, with no lifecycle code:
35
+
36
+ ```ts
37
+ import { workspace } from '@shardflux/sdk';
38
+ const run = await workspace('acme/thread-42', { template: 'default' }).exec('python3 -c "print(40 + 2)"');
39
+ ```
40
+
41
+ - `cloud.workspace(key, params)` and module-level `workspace(key, params)` return a `WorkspaceRef` without a request.
42
+ Its first call opens the key (one held request); concurrent first calls share it, a failed open is retried by the
43
+ next call, and a workspace deleted under the ref is opened again by the call after the one that met the deletion.
44
+ - `WorkspaceRef`: `exec(command)` (a string runs through `bash -lc`, an argv array without a shell), `files`,
45
+ `executions`, `tools()` (definitions from the key's grants before any VM; the first tool call opens), `open()` (the
46
+ `Workspace`), `cell()`, `hint()` (starts the open), `key`, `created`, `mode`. `create: false` never creates (404
47
+ `not_found` for an unknown key).
48
+ - `new Shardflux()` reads `SHARDFLUX_API_KEY` and `SHARDFLUX_API_URL` when `apiKey` / `baseUrl` are not passed.
49
+ - `workspace.created`: whether the `open()` that returned it created the workspace (the API's new `created` field).
50
+ - `template: 'default'` opens the platform default template (`python-node-browser`) on any client version.
51
+ - `workspaceTools()` takes a `ToolTarget`: a `Workspace` or a `WorkspaceRef`.
52
+
53
+ ### Computer use
54
+
55
+ Additive. A desktop in the workspace that agents drive with screenshots, clicks and keystrokes (contracts §45).
56
+
57
+ - `workspace.computer`: `act(actions, { screenshot, settleMs, format, quality })` runs a batch of Claude's computer
58
+ toolset actions in order (the first failure stops it; the rest come back `skipped`), `screenshot()`, `status()`,
59
+ `start({ width, height })`, `stop()`, `stream({ interactive, ttlSeconds })` (a signed link to the viewer; it exposes
60
+ port 61002, or 61003 when interactive) and `stopStream()`.
61
+ - `computerToolset(workspace)` / `COMPUTER_TOOLSET`: answers Claude's `computer_toolset_20260801` calls of a model turn
62
+ with one batch; every `tool_result` echoes `toolset_name: "computer"`.
63
+ - `workspaceTools()` adds, when the token grants `computer`, the `computer` tool (one action per call) and
64
+ `computer_batch` (up to 50 actions in one call, with each action's result, a zoom's image and one screenshot after
65
+ them). Both take `screenshot: false`, `settle_ms`, and `format: "jpeg"` with `quality`.
66
+ - `open({ computerUse })`, `workspace.setComputerUse(true | false | null)`, `workspace.computerUse`,
67
+ `cloud.workspaces.setComputerUse(id, enabled)`. Tool tokens carry `computer` while the switch is on; a computer call
68
+ whose cached token predates the switch is sent again with a fresh token.
69
+ - `cloud.templates.setComputerUse(slug, enabled, { organizationId })` switches it for every workspace of one of your
70
+ templates. `ToolName` includes `computer`; template views carry `computer_use`.
71
+
72
+ ## 0.14.0 — 2026-10-04
73
+
74
+ - Opt-in `workspaceTools(workspace, { prewake: true })` input-start hooks prepare VM tools while their input streams. Default tools, offline reads and file-first workspaces install no hook.
11
75
 
12
76
  ### Sign up from an agent
13
77
 
@@ -117,7 +181,7 @@ gives a static allocation of `memory_mib`. Reopening an existing workspace with
117
181
  keeps its stored mode; a fork without caps inherits its source's. The SDK's requests do not change;
118
182
  `workspace.memory` and `workspace.allocationMode` report the mode the workspace got.
119
183
 
120
- ## 0.13.1 (not yet published)
184
+ ## 0.13.1 — 2026-10-03
121
185
 
122
186
  Patch, additive: the transport, a documented value, a fix for tool calls across a move and self-healing workspaces.
123
187
  `@shardflux/cli` 0.8.1, `@shardflux/mcp` 0.7.1 and the `shardflux` bundle 0.10.1 follow (`^0.13.1`).
@@ -174,7 +238,7 @@ failing with `409 conflict` (`workspace_not_running`).
174
238
  runs; new token)`.
175
239
  - The CLI, the MCP server and the `shardflux` bundle get it through their `^0.13.0` dependency.
176
240
 
177
- ## 0.13.0
241
+ ## 0.13.0 — 2026-10-02
178
242
 
179
243
  ### Elastic memory
180
244
 
@@ -284,14 +348,12 @@ template's newest published version, picked up at its next cold boot or resume.
284
348
  - The cancel that `exec.run` sends when its `signal` aborts passes `killGraceMs` capped at 60000, so a larger
285
349
  `killGraceMs` still cancels the command.
286
350
 
287
- ## 0.12.0 (release candidate)
351
+ ## 0.12.0 — 2026-09-30
288
352
 
289
353
  Completed exec output streams are drained before releasing their HTTP connections, with a bounded cleanup if a peer does not close.
290
354
 
291
355
  Pooled HTTP/1.1 on Node 26 with pinned undici 8.10.2; custom fetch stays unchanged. Labels on open/list and setLabels, typed idle(), keepalive() and setIdlePolicy(). Protocol errors carry source; isWorkspaceGone recognizes only an explicit API workspace_deleted refusal. Exec sessions accept stdin_open and offset-addressed exec.input. Failed workspaces recover through resume/auto-wake with the same ID. Completed deletions free keys for new IDs.
292
356
 
293
- Requires the QM integration backend release for labels, failed recovery, key reuse and pipe stdin. No production deployment has occurred from this branch.
294
-
295
357
  ### Instant suspend: durable storage in the result
296
358
 
297
359
  Additive. A suspend returns as soon as the workspace is sealed on its host; the copy lands in durable storage right
@@ -316,7 +378,7 @@ after. This release reads that from the result and can wait for it.
316
378
  Requires the instant-suspend cell release for `durable: false` results; against other servers every succeeded suspend
317
379
  is already durable and `durable: true` resolves at once.
318
380
 
319
- ## 0.11.1
381
+ ## 0.11.1 — 2026-09-30
320
382
 
321
383
  Wording: messages and JSDoc say what to do, without internals (no behaviour change).
322
384
 
@@ -330,7 +392,7 @@ Wording: messages and JSDoc say what to do, without internals (no behaviour chan
330
392
  `SHARDFLUX_HTTP_KEEPALIVE=1`.
331
393
  - The `formatTiming()` example is a production resume (413 ms).
332
394
 
333
- ## 0.11.0
395
+ ## 0.11.0 — 2026-09-29
334
396
 
335
397
  ### A resume that restarted processes says so (cold boot)
336
398
 
@@ -353,17 +415,17 @@ the resume's result through; this release reads it.
353
415
  - The new `ServerTiming` fields are optional in the type (always set by the SDK), so timings built by hand still
354
416
  type-check. No return type or behaviour changes: a wake that cold-booted still resolves `true`.
355
417
 
356
- ## 0.10.2
418
+ ## 0.10.2 — 2026-09-29
357
419
 
358
420
  Fix: the SDK reports 0.10.2. 0.10.1 was published reporting 0.10.0, its previous version; the build now
359
421
  fails when `SDK_VERSION` differs from package.json.
360
422
 
361
- ## 0.10.1
423
+ ## 0.10.1 — 2026-09-29
362
424
 
363
425
  Public support: package metadata and the README now link to the [shared bug tracker](https://github.com/shardfluxdev/community/issues),
364
426
  feature requests, and private support/security reporting.
365
427
 
366
- ## 0.10.0
428
+ ## 0.10.0 — 2026-09-29
367
429
 
368
430
  ### A command that could not start rejects exec.run() (ExecStartError)
369
431
 
@@ -411,7 +473,7 @@ from the API's OpenAPI); the account client's `setSpendPolicy()` gains the overa
411
473
 
412
474
  - `workspace.suspendWhenIdle({ afterSeconds, idempotencyKey? })` and `cloud.workspaces.suspendWhenIdle(id, {
413
475
  afterSeconds })` (POST /v1/workspaces/{id}/suspend-when-idle): the workspace is suspended once it has been idle for
414
- `afterSeconds` (30..3600), counted from the later of its last work and the request. Meant for the end of an agent
476
+ `afterSeconds` (0..3600 since the 2026-09-30 API release, commit `2b2f5e658`; originally 30..3600), counted from the later of its last work and the request. Meant for the end of an agent
415
477
  turn. A running command, an attached stream or a keepalive postpones it; the next tool call or a resume cancels it.
416
478
  Resolves with `{ suspendRequest, operation, workspace }`: `operation` is the suspend already in progress, if any
417
479
  (then nothing is recorded).
@@ -421,7 +483,7 @@ from the API's OpenAPI); the account client's `setSpendPolicy()` gains the overa
421
483
  count as the next turn and cancel the request.
422
484
  - Types `SuspendRequest`, `SuspendWhenIdleOptions`, `SuspendWhenIdleResult`, `SuspendWhenIdleResponse`.
423
485
 
424
- ## 0.9.0
486
+ ## 0.9.0 — 2026-09-29
425
487
 
426
488
  Elastic compute (decision 0007): file tools and wake hints for parked workspaces. Additive; older APIs and cell
427
489
  gateways keep working (the new calls answer 404 there).
@@ -617,7 +679,7 @@ behavior is the automatic version check (below), which makes one background requ
617
679
  - New exports: `FeedbackCategory`, `FeedbackContext`, `FeedbackReceipt`, `SendFeedbackParams`, `AccountFeedbackParams`,
618
680
  `FEEDBACK_CATEGORIES`, `FEEDBACK_MESSAGE_MAX_LENGTH`.
619
681
 
620
- ## 0.8.0 (2026-09-28)
682
+ ## 0.8.0 — 2026-09-28
621
683
 
622
684
  Types only; nothing changes at run time and the API is unchanged.
623
685
 
@@ -637,7 +699,7 @@ Types only; nothing changes at run time and the API is unchanged.
637
699
  - Checked at compile time against `@anthropic-ai/sdk` 0.128.0 and `openai` 7.23.0 (devDependencies only; the SDK still
638
700
  has no runtime dependencies).
639
701
 
640
- ## 0.7.0 (2026-09-28)
702
+ ## 0.7.0 — 2026-09-28
641
703
 
642
704
  Needs an API with the template editor; every new field is additive and older fields are unchanged.
643
705
 
@@ -725,7 +787,7 @@ docs/decisions/0006-tool-call-capture.md (shared with the Python SDK 0.3.0).
725
787
  - **Breaking (types only):** `LifecyclePhase` gains `capture_flush`; an exhaustive `switch` over phases without a
726
788
  `default` case needs the new member.
727
789
 
728
- ## 0.6.2 (2026-09-28)
790
+ ## 0.6.2 — 2026-09-28
729
791
 
730
792
  ### Starts that wait for capacity end
731
793
 
@@ -740,7 +802,7 @@ boot (and bill) long after every wait had given up.
740
802
  - `onProgress`: a `capacity_pending` `phase` event carries `deadlineAt` when the API reports it.
741
803
  - Docs: waits no longer say a pending start continues indefinitely.
742
804
 
743
- ## 0.6.1 (2026-09-28)
805
+ ## 0.6.1 — 2026-09-28
744
806
 
745
807
  - `formatTiming()` joined two phases that followed each other with `∥` (ran together) when their 0.1 ms times summed
746
808
  with a floating-point error (1000.2 + 300.1 > 1300.3); it now prints `→`.
@@ -750,7 +812,7 @@ boot (and bill) long after every wait had given up.
750
812
  belonged to no phase (a 1 s wait could show `queued 0 ms`).
751
813
  - README: the timing example is the formatter's real output (durations under a second print in ms).
752
814
 
753
- ## 0.6.0 (2026-09-28)
815
+ ## 0.6.0 — 2026-09-28
754
816
 
755
817
  ### Lifecycle calls: requested or finished
756
818
 
@@ -801,7 +863,7 @@ boot (and bill) long after every wait had given up.
801
863
  create, capture state, test instances, publish, discard).
802
864
  - `ShardfluxApiError.reason` (`details.reason`), with `KnownErrorReason`.
803
865
 
804
- ## 0.5.0 (2026-09-26)
866
+ ## 0.5.0 — 2026-09-26
805
867
 
806
868
  First public release: `Shardflux` client, `workspaces.open/get/list/listAll`, lifecycle calls returning operations,
807
869
  `waitForOperation()`, the cell client (`exec`, `files`, `pty`, `processes`, `git`, `browser`), `workspaceTools()` with
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @shardflux/sdk
2
2
 
3
- TypeScript SDK for [Shardflux](https://shardflux.dev): cloud computers for AI agents.
3
+ TypeScript SDK for [Shardflux](https://shardflux.dev): serverless VMs for AI agents.
4
4
 
5
5
  Open a persistent workspace by key, run commands and move files in it, suspend it when idle, resume
6
6
  it later with its disk and memory intact, and fork it. Hand your agent framework-neutral workspace
@@ -10,8 +10,9 @@ tool calls into the workspace ([tool-call capture](#tool-call-capture-070)).
10
10
  > **Compatibility.** The API is versioned (`/v1`). Breaking changes ship only in minor releases and are marked
11
11
  > **Breaking** in the changelog (see [Compatibility](#compatibility)).
12
12
 
13
- > **Versions.** This README describes 0.11.0. Anything marked **(0.11.0+)** is not in 0.10.x, **(0.10.0+)** not in 0.9.0, **(0.9.0+)** not in 0.8.x, **(0.8.0+)** not in 0.7.x,
14
- > **(0.7.0+)** not in 0.6.x and **(0.6.0+)** not in 0.5.0; [CHANGELOG.md](./CHANGELOG.md) lists what each version added. Check yours with
13
+ > **Versions.** This README describes 0.15.0. Anything marked **(0.15.0+)** is not in 0.14.x, **(0.14.0+)** not in 0.13.x,
14
+ > **(0.13.0+)** not in 0.12.x, **(0.12.0+)** not in 0.11.x, **(0.11.0+)** not in 0.10.x, **(0.10.0+)** not in 0.9.0, **(0.9.0+)** not in 0.8.x,
15
+ > **(0.8.0+)** not in 0.7.x, **(0.7.0+)** not in 0.6.x and **(0.6.0+)** not in 0.5.0; [CHANGELOG.md](./CHANGELOG.md) lists what each version added. Check yours with
15
16
  > `npm ls @shardflux/sdk` or the exported `SDK_VERSION`.
16
17
 
17
18
  - ESM only, Node.js 24 or later. Reading a YAML template file uses the optional peer
@@ -19,6 +20,34 @@ tool calls into the workspace ([tool-call capture](#tool-call-capture-070)).
19
20
  - Typed from the published OpenAPI documents.
20
21
  - Retries, idempotency keys, operation polling and tool-token refresh are handled for you.
21
22
 
23
+ ## Test your integration (0.16.0+)
24
+
25
+ Use the same client calls in fast, isolated tests with `@shardflux/sdk/testing`:
26
+
27
+ ```ts
28
+ import { createFakeShardflux } from '@shardflux/sdk/testing';
29
+
30
+ const cloud = createFakeShardflux({
31
+ onExec: (request) => ({ stdout: `ran ${request.argv.join(' ')}\n`, exitCode: 0 }),
32
+ });
33
+ const ref = cloud.workspace('acme/test', { template: 'python-node-browser' });
34
+ const result = await ref.exec(['python3', '-V'], { sessionId: 'version-check' });
35
+ await ref.files.write('/home/user/result.txt', result.stdout);
36
+ ```
37
+
38
+ The fake returns real SDK workspace handles, refs and errors. Workspace lifecycle calls, labels, paginated lists,
39
+ files, command output, desktop actions and port lists use an isolated in-memory store. Command results come from
40
+ `onExec`; returning `null` leaves a command running for `cloud.testing.completeCommand(id, sessionId, result)`.
41
+ Unscripted commands and unimplemented routes raise `FakeUnsupportedError`.
42
+
43
+ Control state with `cloud.testing.setFaults(id, { busy: true, failed: true, legacyLayout: true })`,
44
+ `computerUnavailable`, `deleted`, `resizeRefusal` (`shrink_not_supported` or `resize_not_available`), and
45
+ `memoryLimitMib` (a plan clamp). Set `cloud.testing.quota` to `retained_state` or `concurrent_workspaces`.
46
+ `cloud.testing.advance(milliseconds)` drives idle deadlines; `latencyMs` and `sleep` inject request latency.
47
+ `cloud.testing.inspect(id)` returns a copy of the raw workspace view. Each fake has its own state.
48
+ Import the testing subpath when building tests; the main SDK entry keeps its existing runtime dependencies.
49
+
50
+
22
51
  ## Support and bug reports
23
52
 
24
53
  [Report a bug](https://github.com/shardfluxdev/community/issues/new?template=bug_report.yml) or
@@ -30,6 +59,29 @@ For usage, account, billing, or private support, email [shardflux@heliosone.fi](
30
59
  Report vulnerabilities through [private security reporting](https://github.com/shardfluxdev/community/security/advisories/new).
31
60
  See the [support guide](https://github.com/shardfluxdev/community/blob/main/SUPPORT.md) for all reporting options.
32
61
 
62
+ ## Reverse tunnels (0.16.0+)
63
+
64
+ Forward a workspace TCP port to a service on the machine running your SDK:
65
+
66
+ ```ts
67
+ const tunnel = await workspace.tunnels.reverse({ remotePort: 8081, target: 'localhost:8081' });
68
+ try {
69
+ // Commands in the workspace can call http://127.0.0.1:8081.
70
+ console.log(tunnel.stats);
71
+ } finally {
72
+ await tunnel.close();
73
+ }
74
+ ```
75
+
76
+ The guest listener binds to `127.0.0.1` by default; `bindAddress: '0.0.0.0'` selects all guest interfaces.
77
+ The target is dialed from this machine. TCP binary data, concurrent connections and half-close are preserved;
78
+ network reconnection resumes the tunnel while the handle is open. `closed` reports completion or failure,
79
+ `sessionId` identifies the forwarder, and `stats` counts connections, bytes in each direction and reconnects.
80
+ An open tunnel keeps the workspace awake; close it when callbacks are finished. For persistent workspaces, explicit suspend/resume preserves
81
+ an open tunnel. The caller needs `exec` and `files` access.
82
+
83
+ The tunnel selects a streaming transport automatically when `pty` permission is granted. Choose `transport: 'pty'` or `transport: 'exec'` in the SDK, or `--transport pty|exec` in the CLI, to select it explicitly. The handle reports `transport`.
84
+
33
85
  ## Install
34
86
 
35
87
  ```sh
@@ -41,6 +93,33 @@ npm install @shardflux/sdk
41
93
  Create a project API key in the Shardflux console (`sfk_<key id>_<secret>`) and keep it on the
42
94
  server. API keys are server credentials: never put one in a browser bundle.
43
95
 
96
+ ```ts
97
+ import { workspace } from '@shardflux/sdk';
98
+
99
+ const ws = workspace('customer-42/main', { template: 'default' }); // reads SHARDFLUX_API_KEY; no request yet
100
+
101
+ // 1. Run a command: the first call creates the workspace. Check that it worked.
102
+ const run = await ws.exec('python3 -c "print(40 + 2)"');
103
+ if (run.exitCode !== 0) throw new Error(`python3 exited ${run.exitCode}: ${run.stderr}`);
104
+ console.log(run.stdout.trim()); // 42
105
+
106
+ // 2. Write a file, then suspend the workspace and wait until the suspend has finished.
107
+ await ws.files.write('/home/user/notes.txt', 'hello from the SDK\n');
108
+ await (await ws.open()).suspend({ wait: true }); // resolves once suspended
109
+
110
+ // 3. The same key again: the workspace wakes, and the file is still there.
111
+ const again = workspace('customer-42/main', { template: 'default' });
112
+ console.log(await again.files.readText('/home/user/notes.txt')); // hello from the SDK
113
+ ```
114
+
115
+ The key names the workspace **(0.15.0+)**: the first call creates it, later calls (from any process) reuse it, and it
116
+ suspends when idle and wakes on the next call with its files, packages and processes. There is no create call and no
117
+ lifecycle code to write. See [Workspaces by key](#workspaces-by-key-0150).
118
+
119
+ ### Open, suspend and resume
120
+
121
+ The same workspace with every step explicit:
122
+
44
123
  ```ts
45
124
  import { Shardflux, formatTiming } from '@shardflux/sdk';
46
125
 
@@ -70,6 +149,48 @@ installed packages and running processes are still there. The same program is in
70
149
  With 0.5.0, wait for the suspend by its operation instead:
71
150
  `await cloud.workspaces.waitForOperation((await workspace.suspend()).id)`.
72
151
 
152
+ ## Workspaces by key (0.15.0+)
153
+
154
+ `workspace(key, params)` (or `cloud.workspace(key, params)` on your own client) names a workspace by the identifier your
155
+ application already has: a user, a thread, a repository, a customer. It makes no request. Its first call opens the key:
156
+ the workspace is created from `params.template` on first use and resumed afterwards, in one request that also brings
157
+ back its tool token. Later calls go straight to the workspace.
158
+
159
+ ```ts
160
+ import { workspace } from '@shardflux/sdk';
161
+
162
+ const ws = workspace(`acme/${threadId}`, { template: 'default' });
163
+
164
+ await ws.exec('pip install requests && python3 app.py'); // a string runs through bash -lc
165
+ await ws.exec(['python3', '-c', 'print(42)']); // an argv array runs without a shell
166
+ await ws.files.write('/home/user/notes.txt', 'hello\n');
167
+ console.log(await ws.files.readText('/home/user/notes.txt'));
168
+
169
+ ws.created; // true when this ref's open created the workspace
170
+ const full = await ws.open(); // the Workspace: suspend, fork, ports, computer, capture
171
+ ```
172
+
173
+ - `params` are `open()`'s without the key: `template` is required (`'default'` is the platform default template,
174
+ `python-node-browser`), plus `secrets`, `inputs`, `caps`, `labels`, `idlePolicy`, `lifetime`, `computerUse`,
175
+ `agentLabel`, `tools`.
176
+ - Calls made while the first open runs share it. A failed open is not kept: the next call opens again.
177
+ - `create: false` never creates: the first call finds the key's workspace and fails with 404 `not_found` when it has
178
+ none.
179
+ - A workspace deleted under the ref (`isWorkspaceGone(err)`, 409 `workspace_deleted`) fails the call that meets it;
180
+ the next call opens the key again, which creates a new workspace.
181
+ - `ws.hint()` starts the open early, for example when your model starts a tool call.
182
+
183
+ **In an agent.** The tools exist before the workspace does; the model's first tool call creates it:
184
+
185
+ ```ts
186
+ import { toAnthropicTools, executeToolCall, workspace } from '@shardflux/sdk';
187
+
188
+ const tools = await workspace(`acme/${threadId}`, { template: 'default' }).tools(); // reads the key's grants
189
+ const anthropicTools = toAnthropicTools(tools);
190
+ // for each tool_use block the model returns:
191
+ const output = await executeToolCall(tools, block);
192
+ ```
193
+
73
194
  ## Configuration
74
195
 
75
196
  ```ts
@@ -82,8 +203,9 @@ const cloud = new Shardflux({
82
203
  });
83
204
  ```
84
205
 
85
- The SDK reads nothing from the environment by itself (except the version check's opt-out). `SHARDFLUX_API_KEY` and
86
- `SHARDFLUX_API_URL` are the conventional names (the `shard` CLI reads them); pass them in as shown.
206
+ `new Shardflux()` with no `apiKey` reads `SHARDFLUX_API_KEY`, and with no `baseUrl` reads `SHARDFLUX_API_URL`
207
+ **(0.15.0+)**; options you pass win. A missing key throws when the client is created. The module-level `workspace()`
208
+ uses such a client, created on its first call.
87
209
 
88
210
  ## Commands and files
89
211
 
@@ -451,6 +573,53 @@ const hook = await ws.ports.createCallbackUrl(3000); // register `${h
451
573
  - `close(port)` stops the port's tokens, links and callback URL at once (also when the port is exposed again).
452
574
  - By id, without reading the workspace: `cloud.workspaces.ports(id)`.
453
575
 
576
+ ### Computer use (0.15.0+)
577
+
578
+ A desktop in the workspace for agents that operate GUI software: a screen, a mouse and a keyboard. Switch it on for a
579
+ workspace (or for every workspace of your template); the platform starts the desktop on the first call that needs it.
580
+
581
+ ```ts
582
+ const ws = await cloud.workspaces.open({ key: 'agent/42', template: 'python-node-browser', computerUse: true });
583
+
584
+ const shot = await ws.computer.screenshot(); // { format: 'png', width: 1280, height: 800, data }
585
+ await ws.computer.act([
586
+ { action: 'left_click', coordinate: [640, 400] },
587
+ { action: 'type', text: 'hello' },
588
+ { action: 'key', text: 'Return' },
589
+ ], { screenshot: true }); // one round trip for the whole batch and a look
590
+
591
+ const { url } = await ws.computer.stream(); // a private link to watch the screen live
592
+ ```
593
+
594
+ With Claude, answer the computer toolset directly:
595
+
596
+ ```ts
597
+ import { computerToolset } from '@shardflux/sdk';
598
+
599
+ const computer = computerToolset(ws);
600
+ const msg = await anthropic.messages.create({ model: 'claude-opus-5-5', max_tokens: 16000, tools: [computer.definition], messages });
601
+ messages.push({ role: 'assistant', content: msg.content });
602
+ messages.push({ role: 'user', content: await computer.run(msg.content) });
603
+ ```
604
+
605
+ - The actions are Claude's computer toolset members with their parameters: `screenshot`, `zoom`, `left_click`,
606
+ `right_click`, `middle_click`, `double_click`, `triple_click`, `left_click_drag`, `mouse_move`, `left_mouse_down`,
607
+ `left_mouse_up`, `cursor_position`, `scroll`, `type`, `key`, `hold_key`, `wait`. A batch runs in order and stops at
608
+ the first failure; later actions come back `skipped`.
609
+ - `computer.run(content)` sends every computer call of a model turn as one batch and returns one `tool_result` per call
610
+ (each with `toolset_name: "computer"`), with a screenshot on the last one when the turn did not end with a look.
611
+ - `workspaceTools()` includes, for any model while the workspace's computer use is on, a `computer` tool (one action
612
+ per call, answered with a screenshot) and `computer_batch` (several actions in one call, with each action's result
613
+ and one screenshot after them). Both take `screenshot: false` to skip the image, `settle_ms`, and `format: "jpeg"`
614
+ with `quality` for smaller images.
615
+ - `act(actions, { screenshot, settleMs, format, quality })` takes the same options.
616
+ - Commands the agent runs see the desktop: `exec` of `chromium https://example.com &` opens the browser on it.
617
+ - `stream({ interactive })` returns a signed link (view only by default; `interactive: true` lets the viewer use the
618
+ mouse and keyboard); `stopStream()` ends every view. Streaming uses inbound ports.
619
+ - `ws.setComputerUse(true | false | null)` sets the workspace's own switch (null follows the template);
620
+ `ws.computerUse` reads `{ enabled, workspace, template, available }`. `status()`, `start({ width, height })` and
621
+ `stop()` manage the desktop directly.
622
+
454
623
  ### Timing and progress (0.6.0+)
455
624
 
456
625
  Every open, wake and waited lifecycle call is traced. `workspace.lastTiming` (and `err.timing` when the call fails)
@@ -710,7 +879,9 @@ it: add paths, never remove them. A build reports the list its version declares
710
879
  ## Agent tools
711
880
 
712
881
  `workspaceTools(workspace)` returns tools with a name, a description, a JSON Schema for the
713
- parameters and an `execute` function. Export them for your model provider and dispatch its tool
882
+ parameters and an `execute` function. It takes a `Workspace` or **(0.15.0+)** a `WorkspaceRef`, whose
883
+ `await ref.tools(opts)` builds them from the API key's grants before the workspace exists (see
884
+ [Workspaces by key](#workspaces-by-key-0150)). Export them for your model provider and dispatch its tool
714
885
  calls:
715
886
 
716
887
  ```ts
@@ -735,7 +906,8 @@ Your agent loop and model calls stay in your application; the workspace is the c
735
906
  act on.
736
907
 
737
908
  The tools are `exec`, `read_file`, `write_file`, `list_files`, `search_files` and `edit_file` (0.9.0+), the process,
738
- terminal, git and browser tools, filtered by the tools your key grants. `edit_file` replaces exact text; when the
909
+ terminal, git and browser tools, and `computer` (0.15.0+, while the workspace's computer use is on), filtered by the
910
+ tools your key grants. `edit_file` replaces exact text; when the
739
911
  model passes no `expected_revision` it reads the file's revision first, so a change made in between fails the edit
740
912
  instead of being overwritten. Each call first sends `workspace.hint()` without waiting for it (`hint: false` turns
741
913
  that off, e.g. when you send the hint yourself as the model starts a tool call), except `read_file`, `list_files` and
@@ -1209,3 +1381,50 @@ const tools = workspaceTools(workspace, { prewake: true });
1209
1381
  // In your model stream's tool-input-start handler:
1210
1382
  tools.find(tool => tool.name === toolName)?.onInputStart?.();
1211
1383
  ```
1384
+
1385
+ Resize controls (0.16.0+): `workspace.resize({ memoryMib: 6144, diskGib: 2, atLeast: true, wait: false })` grows
1386
+ only resources that need more capacity and returns the held server answer with its operation. `workspace.caps` exposes
1387
+ typed stored caps for later starts. A pending operation carries per-resource results once the resize has decided
1388
+ them. Fixed memory without `memory_mib` uses the template default, bounded by the plan and template.
1389
+
1390
+ ## New in 0.16.0
1391
+
1392
+ `ref.turn(async (ref) => { ... })` (also `workspace.turn`) sends a wake hint without waiting, returns the body's result, and requests `suspendWhenIdle({ afterSeconds: 0 })` when it settles, including on error. Pass `{ afterSeconds: 60 }` to keep the workspace warm for a billed minute. Use persistent workspaces for resumable turns.
1393
+
1394
+ Computer batches ending in `screenshot` or `zoom` return that action's image without an extra screenshot. `computer.stream()` reuses a link of the same kind with more than 60 seconds left; `{ fresh: true }` mints one, and `stopStream()` clears the cache. Gate each action with `computerToolset(ws, { beforeAction })` or `workspaceTools(ws, { computer: { beforeAction } })`: return `false`, a refusal string, or throw to answer that action with an error and skip the remaining actions. Hooks may be async.
1395
+
1396
+ Exec results expose `exitCodePosix`: timeout 124, canceled 130, nonnegative exit code, 128 + signal, otherwise 1. `FileNotFoundError` is a `ShardfluxApiError` for cell `path_not_found` replies. The default transport honors `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` and `NODE_USE_ENV_PROXY`. `SHARDFLUX_BASE_URL` aliases `SHARDFLUX_API_URL`; `SHARDFLUX_API_URL` wins.
1397
+
1398
+ ## Embed a live viewer (0.16.0+)
1399
+
1400
+ Watch the desktop or a port preview inside your web app. Choose 1–10 distinct HTTPS origins, then use the returned URL as the iframe source.
1401
+
1402
+ ```ts
1403
+ const { url } = await ws.computer.stream({ embed: { origins: ['https://app.example.com'] } });
1404
+ const preview = await ws.ports.link(3000, { embed: { origins: ['https://app.example.com'] } });
1405
+ ```
1406
+
1407
+ ```html
1408
+ <iframe src="RETURNED_LINK_URL" title="Watch the agent" style="width:100%;height:600px;border:0"></iframe>
1409
+ ```
1410
+
1411
+ The browser session is partitioned by the top-level site. Each origin is exactly a scheme and host with an optional port, such as `https://app.example.com:8443`. The viewer permits framing by those origins; interactive viewing uses the same allowlist.
1412
+
1413
+ ## Idle deletion retention (0.16.0+)
1414
+
1415
+ Keep one persistent workspace per conversation and delete it automatically after 14 idle days:
1416
+
1417
+ ```ts
1418
+ const ws = await cloud.workspaces.open({ key: 'chat/42', template: 'python-node-browser', retention: { delete_after_idle_days: 14 } });
1419
+ await ws.setRetention({ delete_after_idle_days: 30 });
1420
+ await cloud.projects.setRetention((await cloud.me()).api_key!.project_id, { delete_after_idle_days: 14, labels: { app: 'chat' } });
1421
+ await ws.setRetention(null); // follow the project default
1422
+ ```
1423
+
1424
+ `cloud.workspace(key, { template, retention })` accepts the same policy. `ws.retention` shows its effective days
1425
+ and projected `delete_at`. All project selector labels must match exactly; a workspace's own policy wins.
1426
+ Calls and ongoing work postpone deletion. Without a policy, your workspace stays until you delete it.
1427
+
1428
+ ## Workspace upgrade (0.16.0+)
1429
+
1430
+ `await ws.upgrade({ at: "now", wait: true })` keeps files, installed package files and home while cold starting on the supported current layout and base. `at: "next_resume"` schedules it for the next resume. Memory and running processes end at the cold start. Read `ws.upgradeAvailable` and `ws.upgradePending` for the target.
package/dist/account.d.ts CHANGED
@@ -277,6 +277,8 @@ export declare class AccountProjectsApi {
277
277
  name: string;
278
278
  slug?: string;
279
279
  }): Promise<Project>;
280
+ getRetention(projectId: string): Promise<import('./client.js').ProjectRetentionPolicy | null>;
281
+ setRetention(projectId: string, policy: import('./client.js').ProjectRetentionPolicy | null): Promise<import('./client.js').ProjectRetentionPolicy | null>;
280
282
  get(projectId: string): Promise<Project>;
281
283
  }
282
284
  /** Every tool permission an API key can carry (pass it as `toolPermissions` for a key with every tool). */
package/dist/account.js CHANGED
@@ -280,6 +280,12 @@ export class AccountProjectsApi {
280
280
  create(organizationId, params) {
281
281
  return json(this.#core, 'POST', `/v1/organizations/${enc(organizationId)}/projects`, { json: { name: params.name, ...(params.slug === undefined ? {} : { slug: params.slug }) } });
282
282
  }
283
+ getRetention(projectId) {
284
+ return json(this.#core, 'GET', `/v1/projects/${enc(projectId)}/retention`);
285
+ }
286
+ setRetention(projectId, policy) {
287
+ return json(this.#core, 'PUT', `/v1/projects/${enc(projectId)}/retention`, { json: policy });
288
+ }
283
289
  get(projectId) {
284
290
  return json(this.#core, 'GET', `/v1/projects/${enc(projectId)}`);
285
291
  }
package/dist/cell.d.ts CHANGED
@@ -147,6 +147,16 @@ export type GitCommitRequest = S['GitCommitRequest'];
147
147
  export type GitResult = S['GitResult'];
148
148
  export type GitStatus = S['GitStatus'];
149
149
  export type BrowserScreenshotRequest = S['BrowserScreenshotRequest'];
150
+ /** Contracts §45 (0.15.0+): the workspace desktop. */
151
+ export type ComputerAction = S['ComputerAction'];
152
+ export type ComputerActionName = ComputerAction['action'];
153
+ export type ComputerActionsRequest = S['ComputerActionsRequest'];
154
+ export type ComputerActionsResult = S['ComputerActionsResult'];
155
+ export type ComputerActionResult = S['ComputerActionResult'];
156
+ export type ComputerImage = S['ComputerImage'];
157
+ export type ComputerStatus = S['ComputerStatus'];
158
+ export type ComputerStartRequest = S['ComputerStartRequest'];
159
+ export type ComputerStreamInfo = S['ComputerStream'];
150
160
  export type BrowserContentRequest = S['BrowserContentRequest'];
151
161
  export type BrowserContent = S['BrowserContent'];
152
162
  export type Signal = S['SignalValue'];
@@ -238,6 +248,8 @@ export declare const CAPTURE_BARRIER: unique symbol;
238
248
  export interface RunResult {
239
249
  sessionId: string;
240
250
  exitCode: number | null;
251
+ /** POSIX status: timeout 124, canceled 130, exit code, 128 + signal, otherwise 1. */
252
+ exitCodePosix: number;
241
253
  termSignal: number | null;
242
254
  timedOut: boolean;
243
255
  canceled: boolean;
@@ -530,6 +542,21 @@ export declare class CellClient {
530
542
  status: (path: string) => Promise<GitStatus>;
531
543
  commit: (req: GitCommitRequest) => Promise<GitResult>;
532
544
  };
545
+ readonly computer: {
546
+ status: () => Promise<ComputerStatus>;
547
+ start: (req?: ComputerStartRequest) => Promise<ComputerStatus>;
548
+ stop: () => Promise<void>;
549
+ /**
550
+ * Runs a batch of actions in order (the first failure stops it; the rest are reported `skipped`), then a screenshot
551
+ * when `screenshot` is set. Waits and key holds may take up to 300 s per batch.
552
+ */
553
+ act: (req: ComputerActionsRequest, signal?: AbortSignal) => Promise<ComputerActionsResult>;
554
+ /** Starts the viewer in the guest; expose the returned port and open a link to `path` (Workspace.computer.stream does both). */
555
+ streamStart: (req?: {
556
+ interactive?: boolean;
557
+ }) => Promise<ComputerStreamInfo>;
558
+ streamStop: () => Promise<void>;
559
+ };
533
560
  readonly browser: {
534
561
  screenshot: (req: BrowserScreenshotRequest) => Promise<Uint8Array>;
535
562
  content: (req: BrowserContentRequest) => Promise<BrowserContent>;