@shardflux/sdk 0.13.1 → 0.15.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/CHANGELOG.md CHANGED
@@ -3,8 +3,72 @@
3
3
  Every API the README shows is available from the version named here. Breaking changes ship in minor releases and are
4
4
  marked **Breaking**.
5
5
 
6
+ ## 0.15.0 (not yet published)
7
+
8
+ ### Workspaces by key
9
+
10
+ Additive (contracts §46). A workspace named by its key, created on its first use, with no lifecycle code:
11
+
12
+ ```ts
13
+ import { workspace } from '@shardflux/sdk';
14
+ const run = await workspace('acme/thread-42', { template: 'default' }).exec('python3 -c "print(40 + 2)"');
15
+ ```
16
+
17
+ - `cloud.workspace(key, params)` and module-level `workspace(key, params)` return a `WorkspaceRef` without a request.
18
+ Its first call opens the key (one held request); concurrent first calls share it, a failed open is retried by the
19
+ next call, and a workspace deleted under the ref is opened again by the call after the one that met the deletion.
20
+ - `WorkspaceRef`: `exec(command)` (a string runs through `bash -lc`, an argv array without a shell), `files`,
21
+ `executions`, `tools()` (definitions from the key's grants before any VM; the first tool call opens), `open()` (the
22
+ `Workspace`), `cell()`, `hint()` (starts the open), `key`, `created`, `mode`. `create: false` never creates (404
23
+ `not_found` for an unknown key).
24
+ - `new Shardflux()` reads `SHARDFLUX_API_KEY` and `SHARDFLUX_API_URL` when `apiKey` / `baseUrl` are not passed.
25
+ - `workspace.created`: whether the `open()` that returned it created the workspace (the API's new `created` field).
26
+ - `template: 'default'` opens the platform default template (`python-node-browser`) on any client version.
27
+ - `workspaceTools()` takes a `ToolTarget`: a `Workspace` or a `WorkspaceRef`.
28
+
29
+ ### Computer use
30
+
31
+ Additive. A desktop in the workspace that agents drive with screenshots, clicks and keystrokes (contracts §45).
32
+
33
+ - `workspace.computer`: `act(actions, { screenshot, settleMs, format, quality })` runs a batch of Claude's computer
34
+ toolset actions in order (the first failure stops it; the rest come back `skipped`), `screenshot()`, `status()`,
35
+ `start({ width, height })`, `stop()`, `stream({ interactive, ttlSeconds })` (a signed link to the viewer; it exposes
36
+ port 61002, or 61003 when interactive) and `stopStream()`.
37
+ - `computerToolset(workspace)` / `COMPUTER_TOOLSET`: answers Claude's `computer_toolset_20260801` calls of a model turn
38
+ with one batch; every `tool_result` echoes `toolset_name: "computer"`.
39
+ - `workspaceTools()` adds, when the token grants `computer`, the `computer` tool (one action per call) and
40
+ `computer_batch` (up to 50 actions in one call, with each action's result, a zoom's image and one screenshot after
41
+ them). Both take `screenshot: false`, `settle_ms`, and `format: "jpeg"` with `quality`.
42
+ - `open({ computerUse })`, `workspace.setComputerUse(true | false | null)`, `workspace.computerUse`,
43
+ `cloud.workspaces.setComputerUse(id, enabled)`. Tool tokens carry `computer` while the switch is on; a computer call
44
+ whose cached token predates the switch is sent again with a fresh token.
45
+ - `cloud.templates.setComputerUse(slug, enabled, { organizationId })` switches it for every workspace of one of your
46
+ templates. `ToolName` includes `computer`; template views carry `computer_use`.
47
+
48
+ ## 0.14.0 — 2026-10-04 (not yet published)
49
+
50
+ - 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.
51
+
6
52
  ## 0.14.0 (not yet published)
7
53
 
54
+ ### Sign up from an agent
55
+
56
+ Additive (API contracts §44).
57
+
58
+ - `ShardfluxAccount.signup({ email?, displayName?, codexIdToken? })` (POST /v1/auth/signup): a working account and
59
+ its session in one call, with no browser and no email round-trip. Returns `{ account, result }`; `result.access`
60
+ is `verified`, `provisional` (works at once on the Free plan; the trial starts when the email is verified) or
61
+ `verification_required`, and `result.created` is false when a Codex proof signed in to an existing account.
62
+ - `codexIdentityProof(opts?)` (Node): the OpenAI ID token of the Codex CLI signed in on this machine, refreshed by
63
+ Codex itself (`codex app-server`, `account/read {refreshToken: true}`) and read from `$CODEX_HOME/auth.json`. Only
64
+ the ID token is returned. Never throws: `{ ok: false, reason: 'codex_not_found' | 'not_signed_in' |
65
+ 'api_key_login' | 'stale', message }`.
66
+ - `account.auth.requestEmailCode()` and `account.auth.verifyEmailCode(code)` (POST /v1/auth/verify-email/code):
67
+ verify with a 6-digit emailed code; nothing is signed out.
68
+ - `account.auth.stepUp({ codexIdToken })`: a step-up with the linked Codex login instead of the password.
69
+ - New types `SignupResult`, `SignupAccess`, `EmailCodeResult`, `CodexIdentityProof`, `CodexProofOptions`,
70
+ `CodexProofUnavailable`.
71
+
8
72
  ### Workspaces working at once
9
73
 
10
74
  Additive. The plan's workspace number caps workspaces working at the same moment; stored workspaces are bounded by
@@ -25,6 +89,76 @@ Retained state.
25
89
  - Regenerated contract types: `ErrorCode` includes `quota_exceeded` for the workspace gateway; allowance
26
90
  `enforcement` adds `storage_block`, `cap_state` adds `storage_blocked`.
27
91
 
92
+ ### Inbound ports
93
+
94
+ Additive. A TCP port of a processful workspace can be served at its own private HTTPS URL,
95
+ `https://<port>-<handle>.<domain>`; a request wakes a parked or suspended workspace and is answered once it runs.
96
+
97
+ - `workspace.ports` and `cloud.workspaces.ports(id)` (class `WorkspacePorts`): `expose(port)` →
98
+ `{port, url, createdAt, callback, created}` (`ExposePortResult`; idempotent, `created` false when it already was),
99
+ `list()` → `ExposedPort[]`, `close(port)`, `token(port, {ttlSeconds?})` → `{token, expiresAt, url}` (`PortToken`),
100
+ `link(port, {ttlSeconds?, path?})` → `{url, expiresAt}` (`PortLink`), `createCallbackUrl(port)` →
101
+ `{url, createdAt}` (`PortCallbackUrl`) and `revokeCallbackUrl(port)`. `expose`, `close` and `revokeCallbackUrl` are
102
+ retried on transient failures; minting calls are not.
103
+ - Types `ExposedPort`, `ExposePortResult`, `PortToken`, `PortLink`, `PortCallbackUrl`, `PortTokenOptions`,
104
+ `PortLinkOptions` and the raw API shapes `PortView`, `PortTokenResponse`, `PortLinkResponse`, `PortCallbackResponse`
105
+ (regenerated contract types).
106
+ - `KnownErrorReason` adds `inbound_ports_not_available`, `port_not_exposed` and `port_limit`.
107
+
108
+ ### Memory across platform upgrades
109
+
110
+ Additive. A resume keeps the workspace's memory and running processes across host kernel, CPU generation and
111
+ Firecracker upgrades of the platform.
112
+
113
+ - `ColdBootReason` gains `runtime_retired`, and `COLD_BOOT_REASONS` lists every reason this version knows
114
+ (`runtime_changed`, `host_lost`, `runtime_retired`). `runtime_retired`: the runtime the workspace was suspended on
115
+ was retired after an announced window; the resume booted the saved disk (`resumePath` `cold_boot`,
116
+ `memoryRestored` false). A reason this version does not know is still passed through as a string.
117
+
118
+ ### Resize a workspace
119
+
120
+ Additive. Change the memory, CPU and disk of any workspace, running or suspended, fixed or elastic, without a restart
121
+ or a fork.
122
+
123
+ - `workspace.resize({ memoryMib, memoryMibHeld, allocationMode, cpuMillis, diskGib })` and
124
+ `cloud.workspaces.resize(id, params)` send `PATCH /v1/workspaces/{id}/caps` with the given caps (snake_case, at
125
+ least one; none throws `TypeError` before any request), an `Idempotency-Key` (`idempotencyKey`, default a fresh key
126
+ per call, so a retried request replays) and `Prefer: wait` (up to 20 s, within `timeoutMs`). They resolve once the
127
+ resize has finished: a resize the server answers with `202` is waited for like any operation (`timeoutMs`, default
128
+ 300 000 ms; `signal`; `onProgress`, action `resize`). A resize that meets another lifecycle operation (409
129
+ `operation_in_progress`) waits for it and is sent again with the same `Idempotency-Key`, within `timeoutMs`. A failed
130
+ resize is its error: `ShardfluxApiError` 409 with `operationId` (reason `resize_failed`, ...) when the server held
131
+ the request until then, `OperationFailedError` when the SDK waited for the operation.
132
+ - `ResizeResult`: `workspaceId`, `operationId`, `state`, `caps` (the stored caps every later start uses),
133
+ `operation` (the finished operation after a `202`, else null) and, for each resource the request named,
134
+ `memory` / `cpu` / `disk` (`ResizeMemory`, `ResizeCpu`, `ResizeDisk`): `applies_at` (`now`, `resume` or
135
+ `next_start`), `requested_*`, `target_*`, `previous_*`, `applied_*`, `limit_reason` (`plan`, `template`,
136
+ `host_capacity`, `shrink_stalled`) and `reason` (`suspended`, `stopped`, `boot_vcpus`, `region`, `below_base`,
137
+ `legacy_layout`); memory adds `allocation_mode`, `held_mib` and `converged`. Types `ResizeParams`,
138
+ `ResizeAppliesAt`, `ResizeLimitReason` and `ResizeDeferReason` are exported.
139
+ - The handle refreshes its view after a resize. A file-first workspace is refused locally with
140
+ `NotSupportedForModeError`.
141
+ - A finished operation's `result` (the cell's) is returned in the held answer's shape: each named resource with its
142
+ documented keys, a key left out taken from the operation's input or null, other keys dropped.
143
+ - `KnownErrorReason` adds `resize_not_available` (409 `conflict`), `resize_failed` (409 `conflict`, a failed resize)
144
+ and `shrink_not_supported` (422 `validation_failed`, `details.current_disk_gib`). `LifecycleAction` adds `resize`.
145
+
146
+ ### Memory hint for commands
147
+
148
+ Additive. `RunOptions.resourceHint` (`'auto' | 'light' | 'heavy'`, type `ResourceHint`) on `exec.run()`, and
149
+ `resource_hint` on `exec.start()` requests: `heavy` gives an elastic workspace its memory before the command starts,
150
+ `light` starts it at once, `auto` (the default) decides from the command. Sent only when set, so other requests are
151
+ unchanged. The processful `exec` agent tool takes `resource_hint` with the same values.
152
+
153
+ ### Elastic by default
154
+
155
+ With elastic-by-default enabled for the organization, an open of a new processful workspace without
156
+ `caps.allocation_mode` is elastic: it holds the memory its commands use and grows up to `memory_mib` (when not given,
157
+ the plan's largest workspace, bounded by the template); billing counts the memory it holds. `allocation_mode: 'fixed'`
158
+ gives a static allocation of `memory_mib`. Reopening an existing workspace with caps that omit `allocation_mode` then
159
+ keeps its stored mode; a fork without caps inherits its source's. The SDK's requests do not change;
160
+ `workspace.memory` and `workspace.allocationMode` report the mode the workspace got.
161
+
28
162
  ## 0.13.1 (not yet published)
29
163
 
30
164
  Patch, additive: the transport, a documented value, a fix for tool calls across a move and self-healing workspaces.
package/README.md CHANGED
@@ -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
@@ -41,6 +42,33 @@ npm install @shardflux/sdk
41
42
  Create a project API key in the Shardflux console (`sfk_<key id>_<secret>`) and keep it on the
42
43
  server. API keys are server credentials: never put one in a browser bundle.
43
44
 
45
+ ```ts
46
+ import { workspace } from '@shardflux/sdk';
47
+
48
+ const ws = workspace('customer-42/main', { template: 'default' }); // reads SHARDFLUX_API_KEY; no request yet
49
+
50
+ // 1. Run a command: the first call creates the workspace. Check that it worked.
51
+ const run = await ws.exec('python3 -c "print(40 + 2)"');
52
+ if (run.exitCode !== 0) throw new Error(`python3 exited ${run.exitCode}: ${run.stderr}`);
53
+ console.log(run.stdout.trim()); // 42
54
+
55
+ // 2. Write a file, then suspend the workspace and wait until the suspend has finished.
56
+ await ws.files.write('/home/user/notes.txt', 'hello from the SDK\n');
57
+ await (await ws.open()).suspend({ wait: true }); // resolves once suspended
58
+
59
+ // 3. The same key again: the workspace wakes, and the file is still there.
60
+ const again = workspace('customer-42/main', { template: 'default' });
61
+ console.log(await again.files.readText('/home/user/notes.txt')); // hello from the SDK
62
+ ```
63
+
64
+ The key names the workspace **(0.15.0+)**: the first call creates it, later calls (from any process) reuse it, and it
65
+ suspends when idle and wakes on the next call with its files, packages and processes. There is no create call and no
66
+ lifecycle code to write. See [Workspaces by key](#workspaces-by-key-0150).
67
+
68
+ ### Open, suspend and resume
69
+
70
+ The same workspace with every step explicit:
71
+
44
72
  ```ts
45
73
  import { Shardflux, formatTiming } from '@shardflux/sdk';
46
74
 
@@ -70,6 +98,48 @@ installed packages and running processes are still there. The same program is in
70
98
  With 0.5.0, wait for the suspend by its operation instead:
71
99
  `await cloud.workspaces.waitForOperation((await workspace.suspend()).id)`.
72
100
 
101
+ ## Workspaces by key (0.15.0+)
102
+
103
+ `workspace(key, params)` (or `cloud.workspace(key, params)` on your own client) names a workspace by the identifier your
104
+ application already has: a user, a thread, a repository, a customer. It makes no request. Its first call opens the key:
105
+ the workspace is created from `params.template` on first use and resumed afterwards, in one request that also brings
106
+ back its tool token. Later calls go straight to the workspace.
107
+
108
+ ```ts
109
+ import { workspace } from '@shardflux/sdk';
110
+
111
+ const ws = workspace(`acme/${threadId}`, { template: 'default' });
112
+
113
+ await ws.exec('pip install requests && python3 app.py'); // a string runs through bash -lc
114
+ await ws.exec(['python3', '-c', 'print(42)']); // an argv array runs without a shell
115
+ await ws.files.write('/home/user/notes.txt', 'hello\n');
116
+ console.log(await ws.files.readText('/home/user/notes.txt'));
117
+
118
+ ws.created; // true when this ref's open created the workspace
119
+ const full = await ws.open(); // the Workspace: suspend, fork, ports, computer, capture
120
+ ```
121
+
122
+ - `params` are `open()`'s without the key: `template` is required (`'default'` is the platform default template,
123
+ `python-node-browser`), plus `secrets`, `inputs`, `caps`, `labels`, `idlePolicy`, `lifetime`, `computerUse`,
124
+ `agentLabel`, `tools`.
125
+ - Calls made while the first open runs share it. A failed open is not kept: the next call opens again.
126
+ - `create: false` never creates: the first call finds the key's workspace and fails with 404 `not_found` when it has
127
+ none.
128
+ - A workspace deleted under the ref (`isWorkspaceGone(err)`, 409 `workspace_deleted`) fails the call that meets it;
129
+ the next call opens the key again, which creates a new workspace.
130
+ - `ws.hint()` starts the open early, for example when your model starts a tool call.
131
+
132
+ **In an agent.** The tools exist before the workspace does; the model's first tool call creates it:
133
+
134
+ ```ts
135
+ import { toAnthropicTools, executeToolCall, workspace } from '@shardflux/sdk';
136
+
137
+ const tools = await workspace(`acme/${threadId}`, { template: 'default' }).tools(); // reads the key's grants
138
+ const anthropicTools = toAnthropicTools(tools);
139
+ // for each tool_use block the model returns:
140
+ const output = await executeToolCall(tools, block);
141
+ ```
142
+
73
143
  ## Configuration
74
144
 
75
145
  ```ts
@@ -82,8 +152,9 @@ const cloud = new Shardflux({
82
152
  });
83
153
  ```
84
154
 
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.
155
+ `new Shardflux()` with no `apiKey` reads `SHARDFLUX_API_KEY`, and with no `baseUrl` reads `SHARDFLUX_API_URL`
156
+ **(0.15.0+)**; options you pass win. A missing key throws when the client is created. The module-level `workspace()`
157
+ uses such a client, created on its first call.
87
158
 
88
159
  ## Commands and files
89
160
 
@@ -360,10 +431,11 @@ lookups by key.
360
431
 
361
432
  ### Elastic memory (0.13.0+)
362
433
 
363
- Promise a workspace a lot of memory while it holds only what it uses. An elastic workspace idles at its held floor
364
- (`memory_mib_held`, default 1024) and grows before each command starts, so compilers, test runners and V8 size
365
- themselves from the memory they will actually get. After 30 s without work it shrinks back. The layout takes effect at
366
- the workspace's next start.
434
+ A workspace can hold only the memory its commands use and grow up to `memory_mib` when they need more; billing counts
435
+ the memory it holds. An elastic workspace idles at its held floor (`memory_mib_held`, default 1024) and shrinks back
436
+ after 30 s without work. Package installs, test runners, compilers, type checkers and bundlers get their memory before
437
+ they start, so they size their heaps and workers from what they will actually get; everything else starts at once and
438
+ the workspace grows while it runs.
367
439
 
368
440
  ```ts
369
441
  const ws = await cloud.workspaces.open({
@@ -375,10 +447,32 @@ ws.memory; // { allocation_mode: 'elastic', promised_mib: 8192, held_m
375
447
 
376
448
  const r = await ws.cell().exec.run(['npm', 'test']);
377
449
  r.memoryGrow; // { outcome: 'delivered', from_mib: 1024, want_mib: 4096, got_mib: 4096, deliver_ms: 175 }
450
+
451
+ await ws.cell().exec.run(['python3', 'train.py'], { resourceHint: 'heavy' }); // 0.14.0+: memory first, then the command
378
452
  ```
379
453
 
380
- Given `caps` replace the stored ones, so send `allocation_mode` with every `caps` you pass; an open without `caps`
381
- keeps the stored layout. The exec agent tool adds `memory_grow` to its result when the command's start grew the VM.
454
+ `resourceHint` (0.14.0+) is `'auto'` (the default: decided from the command), `'heavy'` or `'light'`; the exec agent
455
+ tool takes `resource_hint`. `allocation_mode: 'fixed'` gives a static allocation of `memory_mib`. Send
456
+ `allocation_mode` with every `caps` you pass to keep the mode you chose; an open without `caps` keeps the stored
457
+ layout, and a fork without `caps` inherits its source's. The exec agent tool adds `memory_grow` to its result when the
458
+ command's start grew the workspace.
459
+
460
+ ### Resize a workspace (0.14.0+)
461
+
462
+ Change the memory, CPU and disk of any workspace, running or suspended, fixed or elastic, without a restart:
463
+
464
+ ```ts
465
+ const r = await ws.resize({ memoryMib: 6144, diskGib: 20 });
466
+ r.memory; // { applies_at: 'now', previous_mib: 2048, applied_mib: 6144, converged: true, ... }
467
+ r.disk; // { applies_at: 'now', previous_gib: 10, applied_gib: 20, ... }
468
+
469
+ await ws.resize({ allocationMode: 'elastic', memoryMibHeld: 1024 }); // switch modes live
470
+ ```
471
+
472
+ Memory changes live, and disks grow online. A suspended workspace gets its new size when it resumes, before its first
473
+ call. CPU changes live within the vCPUs the workspace booted with; a larger CPU size applies at the next start. Each
474
+ resource says when it applies (`applies_at`: `now`, `resume` or `next_start`), and the new caps are stored, so every
475
+ later start uses them.
382
476
 
383
477
  ### Burst execution (0.13.0+)
384
478
 
@@ -398,6 +492,83 @@ workspace's own processes resume where they were. A burst is not signalled or ca
398
492
  fails (`burst_apply_failed`), retry `exec.run` with the same `sessionId` to finish the apply. The agent tools offer
399
493
  bursts when asked: `workspaceTools(ws, { burst: true })`.
400
494
 
495
+ ### Inbound ports (0.14.0+)
496
+
497
+ Serve a port of the workspace at its own HTTPS URL: a preview of the app your agent is building, a webhook receiver,
498
+ an OAuth redirect, a live view. A request wakes a parked or suspended workspace and is answered once it runs, so the
499
+ workspace can sleep between visits. Every port is private: a request carries a port token, a signed link's browser
500
+ session, or arrives on the port's callback URL.
501
+
502
+ ```ts
503
+ await ws.cell().exec.start({ argv: ['python3', '-m', 'http.server', '3000', '--bind', '0.0.0.0'] });
504
+ const { url } = await ws.ports.expose(3000); // https://3000-<handle>.shardflux.app
505
+
506
+ const { token } = await ws.ports.token(3000); // { token: 'sfp_…', expiresAt, url }, 1 h by default
507
+ await fetch(url, { headers: { authorization: `Bearer ${token}` } });
508
+
509
+ const link = await ws.ports.link(3000, { path: '/dashboard' }); // open link.url in a browser (24 h by default)
510
+ const hook = await ws.ports.createCallbackUrl(3000); // register `${hook.url}github` as a webhook URL
511
+ ```
512
+
513
+ - The server inside the workspace listens on `0.0.0.0` (all interfaces), not only `127.0.0.1`.
514
+ - `expose(port)` is idempotent: `created` is true when this call exposed the port; an exposed port keeps its URL, tokens,
515
+ links and callback URL. `list()` returns `{ port, url, createdAt, callback }` for each exposed port.
516
+ - `token(port, { ttlSeconds })` (60-86400): send it as `Authorization: Bearer <token>`, or as `X-Shardflux-Token`
517
+ when the app reads `Authorization` itself. `link(port, { ttlSeconds, path })` (60-604800 s): anyone with the link can
518
+ open the port until it expires, so share it like a password.
519
+ - `createCallbackUrl(port)`: a URL with a secret in its path for GitHub, Slack, Stripe or an OAuth provider; a request
520
+ to `<url><rest>` reaches `/<rest>` without a token header. Its secret is shown only once; calling it again replaces
521
+ it, `revokeCallbackUrl(port)` revokes it.
522
+ - `close(port)` stops the port's tokens, links and callback URL at once (also when the port is exposed again).
523
+ - By id, without reading the workspace: `cloud.workspaces.ports(id)`.
524
+
525
+ ### Computer use (0.15.0+)
526
+
527
+ A desktop in the workspace for agents that operate GUI software: a screen, a mouse and a keyboard. Switch it on for a
528
+ workspace (or for every workspace of your template); the platform starts the desktop on the first call that needs it.
529
+
530
+ ```ts
531
+ const ws = await cloud.workspaces.open({ key: 'agent/42', template: 'python-node-browser', computerUse: true });
532
+
533
+ const shot = await ws.computer.screenshot(); // { format: 'png', width: 1280, height: 800, data }
534
+ await ws.computer.act([
535
+ { action: 'left_click', coordinate: [640, 400] },
536
+ { action: 'type', text: 'hello' },
537
+ { action: 'key', text: 'Return' },
538
+ ], { screenshot: true }); // one round trip for the whole batch and a look
539
+
540
+ const { url } = await ws.computer.stream(); // a private link to watch the screen live
541
+ ```
542
+
543
+ With Claude, answer the computer toolset directly:
544
+
545
+ ```ts
546
+ import { computerToolset } from '@shardflux/sdk';
547
+
548
+ const computer = computerToolset(ws);
549
+ const msg = await anthropic.messages.create({ model: 'claude-opus-5-5', max_tokens: 16000, tools: [computer.definition], messages });
550
+ messages.push({ role: 'assistant', content: msg.content });
551
+ messages.push({ role: 'user', content: await computer.run(msg.content) });
552
+ ```
553
+
554
+ - The actions are Claude's computer toolset members with their parameters: `screenshot`, `zoom`, `left_click`,
555
+ `right_click`, `middle_click`, `double_click`, `triple_click`, `left_click_drag`, `mouse_move`, `left_mouse_down`,
556
+ `left_mouse_up`, `cursor_position`, `scroll`, `type`, `key`, `hold_key`, `wait`. A batch runs in order and stops at
557
+ the first failure; later actions come back `skipped`.
558
+ - `computer.run(content)` sends every computer call of a model turn as one batch and returns one `tool_result` per call
559
+ (each with `toolset_name: "computer"`), with a screenshot on the last one when the turn did not end with a look.
560
+ - `workspaceTools()` includes, for any model while the workspace's computer use is on, a `computer` tool (one action
561
+ per call, answered with a screenshot) and `computer_batch` (several actions in one call, with each action's result
562
+ and one screenshot after them). Both take `screenshot: false` to skip the image, `settle_ms`, and `format: "jpeg"`
563
+ with `quality` for smaller images.
564
+ - `act(actions, { screenshot, settleMs, format, quality })` takes the same options.
565
+ - Commands the agent runs see the desktop: `exec` of `chromium https://example.com &` opens the browser on it.
566
+ - `stream({ interactive })` returns a signed link (view only by default; `interactive: true` lets the viewer use the
567
+ mouse and keyboard); `stopStream()` ends every view. Streaming uses inbound ports.
568
+ - `ws.setComputerUse(true | false | null)` sets the workspace's own switch (null follows the template);
569
+ `ws.computerUse` reads `{ enabled, workspace, template, available }`. `status()`, `start({ width, height })` and
570
+ `stop()` manage the desktop directly.
571
+
401
572
  ### Timing and progress (0.6.0+)
402
573
 
403
574
  Every open, wake and waited lifecycle call is traced. `workspace.lastTiming` (and `err.timing` when the call fails)
@@ -657,7 +828,9 @@ it: add paths, never remove them. A build reports the list its version declares
657
828
  ## Agent tools
658
829
 
659
830
  `workspaceTools(workspace)` returns tools with a name, a description, a JSON Schema for the
660
- parameters and an `execute` function. Export them for your model provider and dispatch its tool
831
+ parameters and an `execute` function. It takes a `Workspace` or **(0.15.0+)** a `WorkspaceRef`, whose
832
+ `await ref.tools(opts)` builds them from the API key's grants before the workspace exists (see
833
+ [Workspaces by key](#workspaces-by-key-0150)). Export them for your model provider and dispatch its tool
661
834
  calls:
662
835
 
663
836
  ```ts
@@ -682,7 +855,8 @@ Your agent loop and model calls stay in your application; the workspace is the c
682
855
  act on.
683
856
 
684
857
  The tools are `exec`, `read_file`, `write_file`, `list_files`, `search_files` and `edit_file` (0.9.0+), the process,
685
- terminal, git and browser tools, filtered by the tools your key grants. `edit_file` replaces exact text; when the
858
+ terminal, git and browser tools, and `computer` (0.15.0+, while the workspace's computer use is on), filtered by the
859
+ tools your key grants. `edit_file` replaces exact text; when the
686
860
  model passes no `expected_revision` it reads the file's revision first, so a change made in between fails the edit
687
861
  instead of being overwritten. Each call first sends `workspace.hint()` without waiting for it (`hint: false` turns
688
862
  that off, e.g. when you send the hint yourself as the model starts a tool call), except `read_file`, `list_files` and
@@ -849,7 +1023,7 @@ await workspace.cell().exec.run(['python3', 'agent.py']); // sees $OPEN
849
1023
 
850
1024
  `ShardfluxAccount` does what a person does in the web app, with a user session instead of an API key: sign up, sign
851
1025
  in (with MFA), organizations, projects, API keys, members, invitations, billing, audit, data export and account
852
- deletion. Two steps stay human: opening the verification email, and paying in Stripe Checkout.
1026
+ deletion. Paying in Stripe Checkout is the step a person does.
853
1027
 
854
1028
  ```ts
855
1029
  import { Shardflux, ShardfluxAccount } from '@shardflux/sdk';
@@ -887,6 +1061,37 @@ const cloud = new Shardflux({ apiKey: secret });
887
1061
  - `parseEmailToken(input)` returns the `token` of a link's `#token=` fragment or `?token=` query (else the trimmed
888
1062
  input), and throws for a link without one.
889
1063
 
1064
+ ### Sign up from an agent (0.14.0+)
1065
+
1066
+ One call gives a working account, with no browser and no email round-trip. With the Codex CLI signed in with ChatGPT
1067
+ on the machine, the account is verified by that login and starts with its trial:
1068
+
1069
+ ```ts
1070
+ import { ShardfluxAccount, codexIdentityProof } from '@shardflux/sdk';
1071
+
1072
+ const proof = await codexIdentityProof(); // asks Codex to refresh its login, returns the ID token
1073
+ const { account, result } = proof.ok
1074
+ ? await ShardfluxAccount.signup({ codexIdToken: proof.idToken, onSessionToken: (s) => save(s.token) })
1075
+ : await ShardfluxAccount.signup({ email: 'ada@example.com', onSessionToken: (s) => save(s.token) });
1076
+ // result.access: 'verified' | 'provisional' | 'verification_required'; result.created: false when it signed in
1077
+
1078
+ if (result.access !== 'verified') {
1079
+ await account.auth.requestEmailCode(); // a 6-digit code to the inbox (15 minutes)
1080
+ await account.auth.verifyEmailCode('123456'); // verified; the session and keys keep working
1081
+ }
1082
+ ```
1083
+
1084
+ - `codexIdentityProof()` (Node) reads only the ID token from `$CODEX_HOME/auth.json` (default `~/.codex`), after
1085
+ `codex app-server` refreshes it; it never throws: `{ ok: false, reason }` is `codex_not_found`, `not_signed_in`,
1086
+ `api_key_login` or `stale`. The token proves the ChatGPT account's verified email and is not a credential to
1087
+ OpenAI.
1088
+ - With a Codex proof, `signup` signs in to the account of that ChatGPT identity or email when there is one, else
1089
+ creates it. `result.status` `mfa_required`: `account.auth.completeMfa({ code })`.
1090
+ - With an email only, a `provisional` account works at once on the Free plan; verifying the email starts the trial.
1091
+ An address that has an account is 409 `conflict` (`details.reason` `email_registered`).
1092
+ - `account.auth.stepUp({ codexIdToken })` confirms a sensitive action with the linked Codex login instead of a
1093
+ password.
1094
+
890
1095
  Upgrading a plan: a person pays at the Checkout `url`; the code waits for the subscription.
891
1096
 
892
1097
  ```ts
@@ -966,6 +1171,11 @@ of that base), or `invalid_path` (`details.field` `recipe.immutable[<i>]`). A bu
966
1171
  `failure.code` `immutable_path_missing` (`failure.details.path` is not a directory in the built filesystem) or
967
1172
  `immutable_image_too_large`.
968
1173
 
1174
+ Inbound ports **(0.14.0+)**: 403 `forbidden` with `reason` `inbound_ports_not_available` (inbound ports are not enabled
1175
+ for the organization), 404 `not_found` with `reason` `port_not_exposed` (a token, link or callback URL of a port that is
1176
+ not exposed: expose it first; `details.port`), 409 `conflict` with `reason` `port_limit` (close a port first;
1177
+ `details.limit`), and 422 `validation_failed` for a port outside 1-65535 or an option out of range.
1178
+
969
1179
  ## Usage and overage
970
1180
 
971
1181
  `cloud.usage` reads the organization's usage (API keys see organization totals and their own project's workspaces):
@@ -1110,3 +1320,13 @@ import { isWorkspaceGone, ShardfluxProtocolError } from '@shardflux/sdk';
1110
1320
  ### Repositories with a minimum release age
1111
1321
 
1112
1322
  Keep your repository's age policy. Pin an exact version that has aged enough; do not exempt the entire `@shardflux/*` scope. The first SDK, 0.5.0, was published September 26, 2026 at 21:13:52 UTC and reaches seven days on October 3 at that time. New versions, including 0.12.0, need their own seven days after publication. Before then no registry setting on our side can make them eligible. `npm view @shardflux/sdk time --json` shows publication times. A targeted exception for an inspected exact release is a repository-owner decision, not an installation requirement we bypass. Older versions do not contain this release's fixes.
1323
+
1324
+ ### Prepare tool input (0.14.0+)
1325
+
1326
+ `workspaceTools(workspace, { prewake: true })` adds an optional `onInputStart()` callback to tools that need the VM. Call the matching tool's callback when the model starts streaming its input, then execute the tool normally once its arguments are complete. The callback returns immediately and prepares the workspace in the background. Without `prewake`, tools have no input-start callback. File reads served from disk and file-first tools have no callback.
1327
+
1328
+ ```ts
1329
+ const tools = workspaceTools(workspace, { prewake: true });
1330
+ // In your model stream's tool-input-start handler:
1331
+ tools.find(tool => tool.name === toolName)?.onInputStart?.();
1332
+ ```
package/dist/account.d.ts CHANGED
@@ -2,6 +2,8 @@
2
2
  * The account plane with a user session (0.9.0): what a person does in the web app, from code.
3
3
  *
4
4
  * // Before a session exists (no token needed)
5
+ * const proof = await codexIdentityProof(); // 0.14.0+: this machine's Codex login
6
+ * const { account } = await ShardfluxAccount.signup(proof.ok ? { codexIdToken: proof.idToken } : { email });
5
7
  * await ShardfluxAccount.register({ email, password, displayName });
6
8
  * await ShardfluxAccount.verifyEmail(linkFromTheEmail);
7
9
  * const { account, result } = await ShardfluxAccount.login({ email, password, onSessionToken: (s) => save(s.token) });
@@ -44,6 +46,14 @@ type Ok<Op> = Op extends {
44
46
  [K in keyof R]: K extends 200 | 201 | 202 ? JsonOf<R[K]> : never;
45
47
  }[keyof R] : never;
46
48
  export type RegisterResult = Ok<operations['postV1AuthRegister']>;
49
+ /**
50
+ * Agent signup (0.14.0+): `status`, `created`, `access` (`verified` | `provisional` | `verification_required`), the
51
+ * user, and the new `session_token`.
52
+ */
53
+ export type SignupResult = Ok<operations['postV1AuthSignup']>;
54
+ export type SignupAccess = SignupResult['access'];
55
+ /** POST /v1/auth/verify-email/code: `{ status: 'accepted' }` (code sent) or `{ status: 'verified' }`. */
56
+ export type EmailCodeResult = Ok<operations['postV1AuthVerifyEmailCode']>;
47
57
  export type VerifyEmailResult = Ok<operations['postV1AuthVerifyEmail']>;
48
58
  export type PasswordResetRequestResult = Ok<operations['postV1AuthPasswordResetRequest']>;
49
59
  export type PasswordResetConfirmResult = Ok<operations['postV1AuthPasswordResetConfirm']>;
@@ -178,11 +188,15 @@ export declare class AccountAuthApi {
178
188
  sessions(): Promise<AuthSessionPage>;
179
189
  revokeSession(sessionId: string): Promise<void>;
180
190
  /**
181
- * Confirms the password (and a code when MFA is on) for the calls that need a recent check (403
191
+ * Confirms the password, or a Codex identity proof of the Codex login linked to the account (0.14.0+:
192
+ * `codexIdToken` from codexIdentityProof()), plus a code when MFA is on, for the calls that need a recent check (403
182
193
  * `step_up_required`). Rotates the session token.
183
194
  */
184
- stepUp(params: {
195
+ stepUp(params: ({
185
196
  password: string;
197
+ } | {
198
+ codexIdToken: string;
199
+ }) & {
186
200
  code?: string;
187
201
  recoveryCode?: string;
188
202
  }): Promise<StepUpResult>;
@@ -195,6 +209,13 @@ export declare class AccountAuthApi {
195
209
  changeEmail(newEmail: string): Promise<EmailChangeResult>;
196
210
  /** Sends the verification email again. */
197
211
  resendVerification(): Promise<ResendVerificationResult>;
212
+ /**
213
+ * Emails a 6-digit verification code to the account address (0.14.0+; valid 15 minutes, once). Confirm it with
214
+ * verifyEmailCode: nothing is signed out, and an account from `signup` gets its trial.
215
+ */
216
+ requestEmailCode(): Promise<EmailCodeResult>;
217
+ /** Verifies the email with the 6-digit code (0.14.0+). A wrong code is 400 `token_invalid`; 5 wrong attempts end it. */
218
+ verifyEmailCode(code: string): Promise<EmailCodeResult>;
198
219
  }
199
220
  export declare class OrganizationExportsApi {
200
221
  #private;
@@ -458,6 +479,24 @@ export declare class ShardfluxAccount {
458
479
  sendFeedback(params: AccountFeedbackParams): Promise<FeedbackReceipt>;
459
480
  /** Raw access to any /v1 endpoint with the session's authentication and the SDK's error handling. */
460
481
  request<T>(method: string, path: string, init?: RequestOptions): Promise<T>;
482
+ /**
483
+ * Signs up from an agent with no browser or email round-trip (0.14.0+); `account` holds the new session.
484
+ *
485
+ * - `codexIdToken` (from codexIdentityProof()): the email is verified by the Codex login's ChatGPT account. Signs in
486
+ * to the account of that identity or email when there is one (`result.created` false), else creates it.
487
+ * `result.status` `mfa_required`: complete with `account.auth.completeMfa({ code })`.
488
+ * - `email` only: creates an unverified account. `result.access` `provisional`: it works now on the default plan,
489
+ * and the trial comes with `auth.requestEmailCode()` + `auth.verifyEmailCode(code)`; `verification_required`:
490
+ * verify first. An address that has an account is 409 `conflict` (`details.reason` `email_registered`).
491
+ */
492
+ static signup(params: PreSession<{
493
+ email?: string;
494
+ displayName?: string;
495
+ codexIdToken?: string;
496
+ }>): Promise<{
497
+ account: ShardfluxAccount;
498
+ result: SignupResult;
499
+ }>;
461
500
  /** Creates an account; a verification link is emailed (`{ status: 'accepted' }` also when the email is taken). */
462
501
  static register(params: PreSession<{
463
502
  email: string;
package/dist/account.js CHANGED
@@ -149,11 +149,12 @@ export class AccountAuthApi {
149
149
  return json(this.#core, 'DELETE', `/v1/auth/sessions/${enc(sessionId)}`);
150
150
  }
151
151
  /**
152
- * Confirms the password (and a code when MFA is on) for the calls that need a recent check (403
152
+ * Confirms the password, or a Codex identity proof of the Codex login linked to the account (0.14.0+:
153
+ * `codexIdToken` from codexIdentityProof()), plus a code when MFA is on, for the calls that need a recent check (403
153
154
  * `step_up_required`). Rotates the session token.
154
155
  */
155
156
  async stepUp(params) {
156
- const body = { password: params.password };
157
+ const body = 'password' in params ? { password: params.password } : { codex_id_token: params.codexIdToken };
157
158
  if (params.code !== undefined)
158
159
  body.code = params.code;
159
160
  if (params.recoveryCode !== undefined)
@@ -173,6 +174,17 @@ export class AccountAuthApi {
173
174
  resendVerification() {
174
175
  return json(this.#core, 'POST', '/v1/auth/verify-email/resend');
175
176
  }
177
+ /**
178
+ * Emails a 6-digit verification code to the account address (0.14.0+; valid 15 minutes, once). Confirm it with
179
+ * verifyEmailCode: nothing is signed out, and an account from `signup` gets its trial.
180
+ */
181
+ requestEmailCode() {
182
+ return json(this.#core, 'POST', '/v1/auth/verify-email/code', { json: {} });
183
+ }
184
+ /** Verifies the email with the 6-digit code (0.14.0+). A wrong code is 400 `token_invalid`; 5 wrong attempts end it. */
185
+ verifyEmailCode(code) {
186
+ return json(this.#core, 'POST', '/v1/auth/verify-email/code', { json: { code: code.replace(/\s+/g, '') } });
187
+ }
176
188
  }
177
189
  export class OrganizationExportsApi {
178
190
  #core;
@@ -603,6 +615,28 @@ export class ShardfluxAccount {
603
615
  const account = ShardfluxAccount.#client(opts);
604
616
  return account.#ctx.http.json('POST', path, body === undefined ? {} : { json: body });
605
617
  }
618
+ /**
619
+ * Signs up from an agent with no browser or email round-trip (0.14.0+); `account` holds the new session.
620
+ *
621
+ * - `codexIdToken` (from codexIdentityProof()): the email is verified by the Codex login's ChatGPT account. Signs in
622
+ * to the account of that identity or email when there is one (`result.created` false), else creates it.
623
+ * `result.status` `mfa_required`: complete with `account.auth.completeMfa({ code })`.
624
+ * - `email` only: creates an unverified account. `result.access` `provisional`: it works now on the default plan,
625
+ * and the trial comes with `auth.requestEmailCode()` + `auth.verifyEmailCode(code)`; `verification_required`:
626
+ * verify first. An address that has an account is 409 `conflict` (`details.reason` `email_registered`).
627
+ */
628
+ static async signup(params) {
629
+ const { email, displayName, codexIdToken, ...opts } = params;
630
+ const account = ShardfluxAccount.#client(opts);
631
+ const body = {
632
+ ...(email === undefined ? {} : { email }),
633
+ ...(displayName === undefined ? {} : { display_name: displayName }),
634
+ ...(codexIdToken === undefined ? {} : { codex_id_token: codexIdToken }),
635
+ };
636
+ const result = await account.#ctx.http.json('POST', '/v1/auth/signup', { json: body });
637
+ await account.#adopt(result);
638
+ return { account, result };
639
+ }
606
640
  /** Creates an account; a verification link is emailed (`{ status: 'accepted' }` also when the email is taken). */
607
641
  static register(params) {
608
642
  const { email, password, displayName, ...opts } = params;