@shardflux/sdk 0.13.0 → 0.14.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
@@ -1,9 +1,180 @@
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
+
3
7
  Every API the README shows is available from the version named here. Breaking changes ship in minor releases and are
4
8
  marked **Breaking**.
5
9
 
6
- ## 0.13.0 (not yet published)
10
+ ## 0.14.0 (not yet published)
11
+
12
+ ### Sign up from an agent
13
+
14
+ Additive (API contracts §44).
15
+
16
+ - `ShardfluxAccount.signup({ email?, displayName?, codexIdToken? })` (POST /v1/auth/signup): a working account and
17
+ its session in one call, with no browser and no email round-trip. Returns `{ account, result }`; `result.access`
18
+ is `verified`, `provisional` (works at once on the Free plan; the trial starts when the email is verified) or
19
+ `verification_required`, and `result.created` is false when a Codex proof signed in to an existing account.
20
+ - `codexIdentityProof(opts?)` (Node): the OpenAI ID token of the Codex CLI signed in on this machine, refreshed by
21
+ Codex itself (`codex app-server`, `account/read {refreshToken: true}`) and read from `$CODEX_HOME/auth.json`. Only
22
+ the ID token is returned. Never throws: `{ ok: false, reason: 'codex_not_found' | 'not_signed_in' |
23
+ 'api_key_login' | 'stale', message }`.
24
+ - `account.auth.requestEmailCode()` and `account.auth.verifyEmailCode(code)` (POST /v1/auth/verify-email/code):
25
+ verify with a 6-digit emailed code; nothing is signed out.
26
+ - `account.auth.stepUp({ codexIdToken })`: a step-up with the linked Codex login instead of the password.
27
+ - New types `SignupResult`, `SignupAccess`, `EmailCodeResult`, `CodexIdentityProof`, `CodexProofOptions`,
28
+ `CodexProofUnavailable`.
29
+
30
+ ### Workspaces working at once
31
+
32
+ Additive. The plan's workspace number caps workspaces working at the same moment; stored workspaces are bounded by
33
+ Retained state.
34
+
35
+ - A tool call that finds every workspace the plan allows to work at once busy is refused with 429 `quota_exceeded`
36
+ (retryable, `Retry-After`; `details.limit` `concurrent_workspaces`, `limit_value`, `current`,
37
+ `retry_after_seconds`). The call did not run, so the SDK sends the same request again after `Retry-After` (at most
38
+ 5 s) within `maxRetries`, for every call: exec starts, stdin, PTY create, keepalive and process signals included.
39
+ A refusal that outlasts the retries is a `ShardfluxApiError` with `retryAfterSeconds`. `executions.run()` keeps
40
+ these retries and does not add its own on top; the wake hint is not retried (it never waits).
41
+ - `pty.read()` rejects with the refusal (`ShardfluxApiError`, e.g. 429 `quota_exceeded`, retryable) when an accepted
42
+ attach is closed with an error body and a 4xxx close code, instead of returning empty output.
43
+ - The API's 403 `quota_exceeded` is not retried, as before. New `details.limit`: `retained_state` (`limit_value` and
44
+ `current` in GiB): opening a new key and forking are refused while the plan's Retained state is used up; existing
45
+ workspaces keep running, waking, suspending and resuming. Usage summaries report that allowance with `enforcement`
46
+ `storage_block` and `cap_state` `storage_blocked`.
47
+ - Regenerated contract types: `ErrorCode` includes `quota_exceeded` for the workspace gateway; allowance
48
+ `enforcement` adds `storage_block`, `cap_state` adds `storage_blocked`.
49
+
50
+ ### Inbound ports
51
+
52
+ Additive. A TCP port of a processful workspace can be served at its own private HTTPS URL,
53
+ `https://<port>-<handle>.<domain>`; a request wakes a parked or suspended workspace and is answered once it runs.
54
+
55
+ - `workspace.ports` and `cloud.workspaces.ports(id)` (class `WorkspacePorts`): `expose(port)` →
56
+ `{port, url, createdAt, callback, created}` (`ExposePortResult`; idempotent, `created` false when it already was),
57
+ `list()` → `ExposedPort[]`, `close(port)`, `token(port, {ttlSeconds?})` → `{token, expiresAt, url}` (`PortToken`),
58
+ `link(port, {ttlSeconds?, path?})` → `{url, expiresAt}` (`PortLink`), `createCallbackUrl(port)` →
59
+ `{url, createdAt}` (`PortCallbackUrl`) and `revokeCallbackUrl(port)`. `expose`, `close` and `revokeCallbackUrl` are
60
+ retried on transient failures; minting calls are not.
61
+ - Types `ExposedPort`, `ExposePortResult`, `PortToken`, `PortLink`, `PortCallbackUrl`, `PortTokenOptions`,
62
+ `PortLinkOptions` and the raw API shapes `PortView`, `PortTokenResponse`, `PortLinkResponse`, `PortCallbackResponse`
63
+ (regenerated contract types).
64
+ - `KnownErrorReason` adds `inbound_ports_not_available`, `port_not_exposed` and `port_limit`.
65
+
66
+ ### Memory across platform upgrades
67
+
68
+ Additive. A resume keeps the workspace's memory and running processes across host kernel, CPU generation and
69
+ Firecracker upgrades of the platform.
70
+
71
+ - `ColdBootReason` gains `runtime_retired`, and `COLD_BOOT_REASONS` lists every reason this version knows
72
+ (`runtime_changed`, `host_lost`, `runtime_retired`). `runtime_retired`: the runtime the workspace was suspended on
73
+ was retired after an announced window; the resume booted the saved disk (`resumePath` `cold_boot`,
74
+ `memoryRestored` false). A reason this version does not know is still passed through as a string.
75
+
76
+ ### Resize a workspace
77
+
78
+ Additive. Change the memory, CPU and disk of any workspace, running or suspended, fixed or elastic, without a restart
79
+ or a fork.
80
+
81
+ - `workspace.resize({ memoryMib, memoryMibHeld, allocationMode, cpuMillis, diskGib })` and
82
+ `cloud.workspaces.resize(id, params)` send `PATCH /v1/workspaces/{id}/caps` with the given caps (snake_case, at
83
+ least one; none throws `TypeError` before any request), an `Idempotency-Key` (`idempotencyKey`, default a fresh key
84
+ per call, so a retried request replays) and `Prefer: wait` (up to 20 s, within `timeoutMs`). They resolve once the
85
+ resize has finished: a resize the server answers with `202` is waited for like any operation (`timeoutMs`, default
86
+ 300 000 ms; `signal`; `onProgress`, action `resize`). A resize that meets another lifecycle operation (409
87
+ `operation_in_progress`) waits for it and is sent again with the same `Idempotency-Key`, within `timeoutMs`. A failed
88
+ resize is its error: `ShardfluxApiError` 409 with `operationId` (reason `resize_failed`, ...) when the server held
89
+ the request until then, `OperationFailedError` when the SDK waited for the operation.
90
+ - `ResizeResult`: `workspaceId`, `operationId`, `state`, `caps` (the stored caps every later start uses),
91
+ `operation` (the finished operation after a `202`, else null) and, for each resource the request named,
92
+ `memory` / `cpu` / `disk` (`ResizeMemory`, `ResizeCpu`, `ResizeDisk`): `applies_at` (`now`, `resume` or
93
+ `next_start`), `requested_*`, `target_*`, `previous_*`, `applied_*`, `limit_reason` (`plan`, `template`,
94
+ `host_capacity`, `shrink_stalled`) and `reason` (`suspended`, `stopped`, `boot_vcpus`, `region`, `below_base`,
95
+ `legacy_layout`); memory adds `allocation_mode`, `held_mib` and `converged`. Types `ResizeParams`,
96
+ `ResizeAppliesAt`, `ResizeLimitReason` and `ResizeDeferReason` are exported.
97
+ - The handle refreshes its view after a resize. A file-first workspace is refused locally with
98
+ `NotSupportedForModeError`.
99
+ - A finished operation's `result` (the cell's) is returned in the held answer's shape: each named resource with its
100
+ documented keys, a key left out taken from the operation's input or null, other keys dropped.
101
+ - `KnownErrorReason` adds `resize_not_available` (409 `conflict`), `resize_failed` (409 `conflict`, a failed resize)
102
+ and `shrink_not_supported` (422 `validation_failed`, `details.current_disk_gib`). `LifecycleAction` adds `resize`.
103
+
104
+ ### Memory hint for commands
105
+
106
+ Additive. `RunOptions.resourceHint` (`'auto' | 'light' | 'heavy'`, type `ResourceHint`) on `exec.run()`, and
107
+ `resource_hint` on `exec.start()` requests: `heavy` gives an elastic workspace its memory before the command starts,
108
+ `light` starts it at once, `auto` (the default) decides from the command. Sent only when set, so other requests are
109
+ unchanged. The processful `exec` agent tool takes `resource_hint` with the same values.
110
+
111
+ ### Elastic by default
112
+
113
+ With elastic-by-default enabled for the organization, an open of a new processful workspace without
114
+ `caps.allocation_mode` is elastic: it holds the memory its commands use and grows up to `memory_mib` (when not given,
115
+ the plan's largest workspace, bounded by the template); billing counts the memory it holds. `allocation_mode: 'fixed'`
116
+ gives a static allocation of `memory_mib`. Reopening an existing workspace with caps that omit `allocation_mode` then
117
+ keeps its stored mode; a fork without caps inherits its source's. The SDK's requests do not change;
118
+ `workspace.memory` and `workspace.allocationMode` report the mode the workspace got.
119
+
120
+ ## 0.13.1 (not yet published)
121
+
122
+ Patch, additive: the transport, a documented value, a fix for tool calls across a move and self-healing workspaces.
123
+ `@shardflux/cli` 0.8.1, `@shardflux/mcp` 0.7.1 and the `shardflux` bundle 0.10.1 follow (`^0.13.1`).
124
+
125
+ ### Workspaces recover by themselves
126
+
127
+ Additive. When the machine a workspace runs on fails, the workspace is suspended, and its next use (a tool call's wake,
128
+ `resume()`, `open()`) restores it: from its own disk, files kept, or from its newest checkpoint. Nothing changes for
129
+ code that already handles a cold resume.
130
+
131
+ - `cold_boot_reason` / `ServerTiming.coldBootReason` can be `host_lost`: the resume booted the workspace's disk
132
+ (`resumePath` `cold_boot`, `memoryRestored` false, processes restarted). New type `ColdBootReason`
133
+ (`'runtime_changed' | 'host_lost'`, open to other strings; the field still accepts any string).
134
+ - `hostLostOf(op)` and `ServerTiming.hostLost`: `result.host_lost` camelCased (`HostLost`: `detectedAt`,
135
+ `restoredFrom` `'disk' | 'checkpoint'`, `restoredCheckpointId`, `stateAsOf`). A resume from the checkpoint has its
136
+ state as of `stateAsOf`; a suspend that found the machine failed succeeds with `durable: true` and only
137
+ `detectedAt` (`restoredFrom` null).
138
+ - `formatTiming()` prints `processes restarted (host_lost)` for the disk, `restored checkpoint <id> (host_lost; state as
139
+ of <time>)` for the checkpoint, and `host_lost (detected <time>)` for the suspend.
140
+ - Documented operation error codes: `resume_required` (a fork or snapshot of such a workspace before its resume; resume
141
+ it first; not retryable) and `workspace_storage_unavailable` with `details.reason` `host_lost` (not retryable).
142
+ `KnownErrorReason` gains `host_lost`.
143
+
144
+ ### Connection reuse
145
+
146
+ Additive. The call after an agent's pause between tool calls reuses its connection instead of opening a new one.
147
+
148
+ - On Node 26 the pooled transport keeps an idle connection reusable for 5 minutes (was 4 s), so a request seconds or
149
+ minutes after the last one skips the TCP and TLS handshake. Pooled sockets send TCP keepalive probes after 60 s
150
+ idle, so NATs and firewalls along the way keep the connection open. Idle connections never keep a process alive. A
151
+ supplied `fetch`, other runtimes and `SHARDFLUX_HTTP_KEEPALIVE` work as before.
152
+ - A request the SDK retries (GET, a request with an `Idempotency-Key`, a read-only POST such as files search) whose
153
+ connection closes before the response arrives is sent again at once on a new connection. That one resend has no
154
+ backoff and does not count against `maxRetries`; timing records and progress `retry` events list it with
155
+ `delayMs: 0`. A further failure follows `maxRetries` and backoff as before, and a POST without an
156
+ `Idempotency-Key` is never sent twice.
157
+
158
+ ### Faster suspend and resume
159
+
160
+ Additive. `resumePath` / `resume_path` can be `thaw`: a resume that arrives while its suspend is still being
161
+ written continues the same VM in place. Nothing else changes for your code.
162
+
163
+ ### Tool calls across a move
164
+
165
+ Fix. A tool call made while its workspace finishes a resume or a move now runs once the workspace runs, instead of
166
+ failing with `409 conflict` (`workspace_not_running`).
167
+
168
+ - When the call's tool token was refused as not running and the wake then found the workspace already running (the
169
+ resume or move committed in between; `wake` resolves `false`), the call is retried with a current token: the held
170
+ resume's, or a new one. A refused call never ran, so the retry cannot run it twice. Before, the refusal surfaced.
171
+ - A refusal that comes back while the API keeps reporting the workspace running is retried after 500 ms, within the
172
+ same 3 wakes and `transitionTimeoutMs`, then surfaces as before.
173
+ - `onProgress` sees the retry as a `tool` event of type `retry`, cause `conflict workspace_not_running (the workspace
174
+ runs; new token)`.
175
+ - The CLI, the MCP server and the `shardflux` bundle get it through their `^0.13.0` dependency.
176
+
177
+ ## 0.13.0
7
178
 
8
179
  ### Elastic memory
9
180
 
@@ -446,7 +617,6 @@ behavior is the automatic version check (below), which makes one background requ
446
617
  - New exports: `FeedbackCategory`, `FeedbackContext`, `FeedbackReceipt`, `SendFeedbackParams`, `AccountFeedbackParams`,
447
618
  `FEEDBACK_CATEGORIES`, `FEEDBACK_MESSAGE_MAX_LENGTH`.
448
619
 
449
-
450
620
  ## 0.8.0 (2026-09-28)
451
621
 
452
622
  Types only; nothing changes at run time and the API is unchanged.
package/README.md CHANGED
@@ -296,10 +296,10 @@ await cell.exec.run(['make', 'test']); // resumes the wo
296
296
  ```
297
297
 
298
298
  **Detecting a cold resume (0.11.0+).** A resume restores memory and running processes from the checkpoint. After a
299
- platform runtime update, a resume can boot from the saved disk instead of restoring memory (a cold boot), also when a
300
- tool call wakes the workspace; check `memoryRestored`. The workspace runs with its files as of the suspend, and its
301
- processes start fresh, as after a reboot: start dev servers, databases and background jobs again. The resume's timing
302
- says which it was:
299
+ platform runtime update, or after the machine the workspace ran on failed (0.13.1+), a resume can boot from the saved
300
+ disk instead of restoring memory (a cold boot), also when a tool call wakes the workspace; check `memoryRestored`. The
301
+ workspace runs with its files kept, and its processes start fresh, as after a reboot: start dev servers, databases and
302
+ background jobs again. The resume's timing says which it was:
303
303
 
304
304
  ```ts
305
305
  const t = workspace.lastTiming; // after resume({ wait: true }), wake(), or a tool call that woke the workspace
@@ -310,11 +310,20 @@ if (t?.server?.memoryRestored === false) {
310
310
 
311
311
  - `ServerTiming.memoryRestored`: `true` when memory and processes came back, `false` when the workspace booted
312
312
  (`resumePath` `cold_boot`, or `reset_blank_layer` after a reset), `null` when the API did not say (older APIs).
313
- - `ServerTiming.coldBootReason`: why it booted (`runtime_changed`), else null. `formatTiming()` prints
314
- `resume from cold_boot: processes restarted (runtime_changed)`.
313
+ - `ServerTiming.coldBootReason` (type `ColdBootReason`): why it booted, `runtime_changed` or `host_lost`
314
+ **(0.13.1+)**, else null. `formatTiming()` prints `resume from cold_boot: processes restarted (runtime_changed)`.
315
315
  - The finished resume operation carries the same in `result` (`memory_restored`, `cold_boot_reason`, and
316
316
  `cold_boot` with details such as `files_as_of`; it is informational and not part of the stable contract).
317
317
 
318
+ **Workspaces recover by themselves (0.13.1+).** When the machine a workspace runs on fails, the workspace is suspended,
319
+ and its next use (a tool call, `resume()`, `open()`) restores it; there is nothing extra to call.
320
+ `hostLostOf(op)` and `lastTiming.server.hostLost` (`HostLost`) say how:
321
+
322
+ - `restoredFrom: 'disk'`: it booted from its own disk, files kept (`coldBootReason` `host_lost`, processes restarted).
323
+ - `restoredFrom: 'checkpoint'`: it resumed its newest checkpoint (`restoredCheckpointId`); its state is as of
324
+ `stateAsOf`. `formatTiming()` adds `restored checkpoint <id> (host_lost; state as of <time>)`.
325
+ - A `suspend()` that finds the machine failed succeeds (`result.durable` true, `hostLost.detectedAt`).
326
+
318
327
  `suspend`, `resume`, `fork`, `snapshot` and `delete` return the lifecycle operation.
319
328
  `cloud.workspaces.waitForOperation(id)` waits for it (default timeout 5 minutes): each poll asks the API to hold
320
329
  the response until the operation changes (`Prefer: wait`, at most 20 s per request), so completion arrives within
@@ -324,7 +333,15 @@ operation keeps running server side (a queued start until its `deadlineAt`); wai
324
333
  same call. `templates.builds.waitForBuild()` waits the same way. A waited `open()` issues the first tool token
325
334
  together with the final workspace read, so the first tool call starts at once.
326
335
 
327
- From 0.12.0, Node 26 reuses TLS connections through a private HTTP/1.1 pool. Other runtimes keep native fetch. A supplied `fetch` stays unchanged. `SHARDFLUX_HTTP_KEEPALIVE=0` forces connection closure for diagnosis; `=1` opts into native pooling. No global dispatcher is installed.
336
+ From 0.12.0, Node 26 reuses TLS connections through a private HTTP/1.1 pool. From 0.13.0, an idle connection stays
337
+ reusable for 5 minutes, so the call after an agent's pause between tool calls skips the TCP and TLS handshake, and
338
+ pooled sockets send TCP keepalive probes after 60 s idle. Idle connections never keep a process alive. Other runtimes
339
+ keep native fetch. A supplied `fetch` stays unchanged. `SHARDFLUX_HTTP_KEEPALIVE=0` forces connection closure for
340
+ diagnosis; `=1` opts into native pooling. No global dispatcher is installed.
341
+
342
+ A request the SDK retries (safe methods, requests with an `Idempotency-Key`, read-only POSTs) whose connection closes
343
+ before the response arrives is sent again at once on a new connection **(0.13.0+)**, without backoff and outside
344
+ `maxRetries`; timing records list it with `delayMs: 0`.
328
345
 
329
346
  List and look up workspaces:
330
347
 
@@ -343,10 +360,11 @@ lookups by key.
343
360
 
344
361
  ### Elastic memory (0.13.0+)
345
362
 
346
- Promise a workspace a lot of memory while it holds only what it uses. An elastic workspace idles at its held floor
347
- (`memory_mib_held`, default 1024) and grows before each command starts, so compilers, test runners and V8 size
348
- themselves from the memory they will actually get. After 30 s without work it shrinks back. The layout takes effect at
349
- the workspace's next start.
363
+ A workspace can hold only the memory its commands use and grow up to `memory_mib` when they need more; billing counts
364
+ the memory it holds. An elastic workspace idles at its held floor (`memory_mib_held`, default 1024) and shrinks back
365
+ after 30 s without work. Package installs, test runners, compilers, type checkers and bundlers get their memory before
366
+ they start, so they size their heaps and workers from what they will actually get; everything else starts at once and
367
+ the workspace grows while it runs.
350
368
 
351
369
  ```ts
352
370
  const ws = await cloud.workspaces.open({
@@ -358,10 +376,32 @@ ws.memory; // { allocation_mode: 'elastic', promised_mib: 8192, held_m
358
376
 
359
377
  const r = await ws.cell().exec.run(['npm', 'test']);
360
378
  r.memoryGrow; // { outcome: 'delivered', from_mib: 1024, want_mib: 4096, got_mib: 4096, deliver_ms: 175 }
379
+
380
+ await ws.cell().exec.run(['python3', 'train.py'], { resourceHint: 'heavy' }); // 0.14.0+: memory first, then the command
361
381
  ```
362
382
 
363
- Given `caps` replace the stored ones, so send `allocation_mode` with every `caps` you pass; an open without `caps`
364
- keeps the stored layout. The exec agent tool adds `memory_grow` to its result when the command's start grew the VM.
383
+ `resourceHint` (0.14.0+) is `'auto'` (the default: decided from the command), `'heavy'` or `'light'`; the exec agent
384
+ tool takes `resource_hint`. `allocation_mode: 'fixed'` gives a static allocation of `memory_mib`. Send
385
+ `allocation_mode` with every `caps` you pass to keep the mode you chose; an open without `caps` keeps the stored
386
+ layout, and a fork without `caps` inherits its source's. The exec agent tool adds `memory_grow` to its result when the
387
+ command's start grew the workspace.
388
+
389
+ ### Resize a workspace (0.14.0+)
390
+
391
+ Change the memory, CPU and disk of any workspace, running or suspended, fixed or elastic, without a restart:
392
+
393
+ ```ts
394
+ const r = await ws.resize({ memoryMib: 6144, diskGib: 20 });
395
+ r.memory; // { applies_at: 'now', previous_mib: 2048, applied_mib: 6144, converged: true, ... }
396
+ r.disk; // { applies_at: 'now', previous_gib: 10, applied_gib: 20, ... }
397
+
398
+ await ws.resize({ allocationMode: 'elastic', memoryMibHeld: 1024 }); // switch modes live
399
+ ```
400
+
401
+ Memory changes live, and disks grow online. A suspended workspace gets its new size when it resumes, before its first
402
+ call. CPU changes live within the vCPUs the workspace booted with; a larger CPU size applies at the next start. Each
403
+ resource says when it applies (`applies_at`: `now`, `resume` or `next_start`), and the new caps are stored, so every
404
+ later start uses them.
365
405
 
366
406
  ### Burst execution (0.13.0+)
367
407
 
@@ -381,6 +421,36 @@ workspace's own processes resume where they were. A burst is not signalled or ca
381
421
  fails (`burst_apply_failed`), retry `exec.run` with the same `sessionId` to finish the apply. The agent tools offer
382
422
  bursts when asked: `workspaceTools(ws, { burst: true })`.
383
423
 
424
+ ### Inbound ports (0.14.0+)
425
+
426
+ Serve a port of the workspace at its own HTTPS URL: a preview of the app your agent is building, a webhook receiver,
427
+ an OAuth redirect, a live view. A request wakes a parked or suspended workspace and is answered once it runs, so the
428
+ workspace can sleep between visits. Every port is private: a request carries a port token, a signed link's browser
429
+ session, or arrives on the port's callback URL.
430
+
431
+ ```ts
432
+ await ws.cell().exec.start({ argv: ['python3', '-m', 'http.server', '3000', '--bind', '0.0.0.0'] });
433
+ const { url } = await ws.ports.expose(3000); // https://3000-<handle>.shardflux.app
434
+
435
+ const { token } = await ws.ports.token(3000); // { token: 'sfp_…', expiresAt, url }, 1 h by default
436
+ await fetch(url, { headers: { authorization: `Bearer ${token}` } });
437
+
438
+ const link = await ws.ports.link(3000, { path: '/dashboard' }); // open link.url in a browser (24 h by default)
439
+ const hook = await ws.ports.createCallbackUrl(3000); // register `${hook.url}github` as a webhook URL
440
+ ```
441
+
442
+ - The server inside the workspace listens on `0.0.0.0` (all interfaces), not only `127.0.0.1`.
443
+ - `expose(port)` is idempotent: `created` is true when this call exposed the port; an exposed port keeps its URL, tokens,
444
+ links and callback URL. `list()` returns `{ port, url, createdAt, callback }` for each exposed port.
445
+ - `token(port, { ttlSeconds })` (60-86400): send it as `Authorization: Bearer <token>`, or as `X-Shardflux-Token`
446
+ when the app reads `Authorization` itself. `link(port, { ttlSeconds, path })` (60-604800 s): anyone with the link can
447
+ open the port until it expires, so share it like a password.
448
+ - `createCallbackUrl(port)`: a URL with a secret in its path for GitHub, Slack, Stripe or an OAuth provider; a request
449
+ to `<url><rest>` reaches `/<rest>` without a token header. Its secret is shown only once; calling it again replaces
450
+ it, `revokeCallbackUrl(port)` revokes it.
451
+ - `close(port)` stops the port's tokens, links and callback URL at once (also when the port is exposed again).
452
+ - By id, without reading the workspace: `cloud.workspaces.ports(id)`.
453
+
384
454
  ### Timing and progress (0.6.0+)
385
455
 
386
456
  Every open, wake and waited lifecycle call is traced. `workspace.lastTiming` (and `err.timing` when the call fails)
@@ -412,7 +482,7 @@ This resume brought the workspace back from its host's local cache, memory and r
412
482
  running, including any time in `capacity_pending`; `ran` is the cell's work (placement, boot or restore, guest
413
483
  readiness). `start` / `resume from` and `boot to ready` / `host …` are what the cell reported. A resume that booted
414
484
  the saved disk instead of restoring memory reads `resume from cold_boot: processes restarted (runtime_changed)`
415
- (0.11.0+).
485
+ (0.11.0+), or `(host_lost)` (0.13.1+) after the machine the workspace ran on failed.
416
486
  - **outside the server** is your total minus the operation's: network, TLS, polling latency, view and token. A large
417
487
  value with a small server total points at the connection between you and the API, not at the workspace.
418
488
  - **retries** lists transient failures the SDK retried (cause and backoff).
@@ -832,7 +902,7 @@ await workspace.cell().exec.run(['python3', 'agent.py']); // sees $OPEN
832
902
 
833
903
  `ShardfluxAccount` does what a person does in the web app, with a user session instead of an API key: sign up, sign
834
904
  in (with MFA), organizations, projects, API keys, members, invitations, billing, audit, data export and account
835
- deletion. Two steps stay human: opening the verification email, and paying in Stripe Checkout.
905
+ deletion. Paying in Stripe Checkout is the step a person does.
836
906
 
837
907
  ```ts
838
908
  import { Shardflux, ShardfluxAccount } from '@shardflux/sdk';
@@ -870,6 +940,37 @@ const cloud = new Shardflux({ apiKey: secret });
870
940
  - `parseEmailToken(input)` returns the `token` of a link's `#token=` fragment or `?token=` query (else the trimmed
871
941
  input), and throws for a link without one.
872
942
 
943
+ ### Sign up from an agent (0.14.0+)
944
+
945
+ One call gives a working account, with no browser and no email round-trip. With the Codex CLI signed in with ChatGPT
946
+ on the machine, the account is verified by that login and starts with its trial:
947
+
948
+ ```ts
949
+ import { ShardfluxAccount, codexIdentityProof } from '@shardflux/sdk';
950
+
951
+ const proof = await codexIdentityProof(); // asks Codex to refresh its login, returns the ID token
952
+ const { account, result } = proof.ok
953
+ ? await ShardfluxAccount.signup({ codexIdToken: proof.idToken, onSessionToken: (s) => save(s.token) })
954
+ : await ShardfluxAccount.signup({ email: 'ada@example.com', onSessionToken: (s) => save(s.token) });
955
+ // result.access: 'verified' | 'provisional' | 'verification_required'; result.created: false when it signed in
956
+
957
+ if (result.access !== 'verified') {
958
+ await account.auth.requestEmailCode(); // a 6-digit code to the inbox (15 minutes)
959
+ await account.auth.verifyEmailCode('123456'); // verified; the session and keys keep working
960
+ }
961
+ ```
962
+
963
+ - `codexIdentityProof()` (Node) reads only the ID token from `$CODEX_HOME/auth.json` (default `~/.codex`), after
964
+ `codex app-server` refreshes it; it never throws: `{ ok: false, reason }` is `codex_not_found`, `not_signed_in`,
965
+ `api_key_login` or `stale`. The token proves the ChatGPT account's verified email and is not a credential to
966
+ OpenAI.
967
+ - With a Codex proof, `signup` signs in to the account of that ChatGPT identity or email when there is one, else
968
+ creates it. `result.status` `mfa_required`: `account.auth.completeMfa({ code })`.
969
+ - With an email only, a `provisional` account works at once on the Free plan; verifying the email starts the trial.
970
+ An address that has an account is 409 `conflict` (`details.reason` `email_registered`).
971
+ - `account.auth.stepUp({ codexIdToken })` confirms a sensitive action with the linked Codex login instead of a
972
+ password.
973
+
873
974
  Upgrading a plan: a person pays at the Checkout `url`; the code waits for the subscription.
874
975
 
875
976
  ```ts
@@ -949,6 +1050,11 @@ of that base), or `invalid_path` (`details.field` `recipe.immutable[<i>]`). A bu
949
1050
  `failure.code` `immutable_path_missing` (`failure.details.path` is not a directory in the built filesystem) or
950
1051
  `immutable_image_too_large`.
951
1052
 
1053
+ Inbound ports **(0.14.0+)**: 403 `forbidden` with `reason` `inbound_ports_not_available` (inbound ports are not enabled
1054
+ for the organization), 404 `not_found` with `reason` `port_not_exposed` (a token, link or callback URL of a port that is
1055
+ not exposed: expose it first; `details.port`), 409 `conflict` with `reason` `port_limit` (close a port first;
1056
+ `details.limit`), and 422 `validation_failed` for a port outside 1-65535 or an option out of range.
1057
+
952
1058
  ## Usage and overage
953
1059
 
954
1060
  `cloud.usage` reads the organization's usage (API keys see organization totals and their own project's workspaces):
@@ -1093,3 +1199,13 @@ import { isWorkspaceGone, ShardfluxProtocolError } from '@shardflux/sdk';
1093
1199
  ### Repositories with a minimum release age
1094
1200
 
1095
1201
  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.
1202
+
1203
+ ### Prepare tool input (0.14.0+)
1204
+
1205
+ `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.
1206
+
1207
+ ```ts
1208
+ const tools = workspaceTools(workspace, { prewake: true });
1209
+ // In your model stream's tool-input-start handler:
1210
+ tools.find(tool => tool.name === toolName)?.onInputStart?.();
1211
+ ```
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;
package/dist/cell.d.ts CHANGED
@@ -30,6 +30,12 @@ import type { RequestOptions } from './http.js';
30
30
  import type { ProgressListener } from './progress.js';
31
31
  import type { ToolTokenManager } from './tokens.js';
32
32
  type S = components['schemas'];
33
+ /**
34
+ * How much memory a command needs at its start (0.14.0; `resource_hint` of an exec start): `heavy` grows an elastic
35
+ * workspace's memory before the command starts (a build, a test suite, a package install), `light` starts it at once,
36
+ * `auto` (the default) decides from the command. A fixed workspace has its memory already.
37
+ */
38
+ export type ResourceHint = NonNullable<S['ExecStartRequest']['resource_hint']>;
33
39
  export type ExecStartRequest = S['ExecStartRequest'];
34
40
  export type ExecSession = S['ExecSession'];
35
41
  /**
@@ -174,8 +180,9 @@ export interface CellClientOptions {
174
180
  sleep?: (ms: number) => Promise<void>;
175
181
  /**
176
182
  * Wakes a suspended workspace (resume, or join the active resume/open) within `timeoutMs` and resolves once it runs;
177
- * resolves `false` when there was nothing to wake (the API already reports it running), and the refusal then
178
- * surfaces. It throws when the wake fails (OperationFailedError) or outlasts `timeoutMs` (OperationTimeoutError).
183
+ * resolves `false` when there was nothing to wake (the API already reports it running): the refused call is then
184
+ * retried with a current token, since the transition that refused it ended meanwhile (0.13.1+). It throws when the
185
+ * wake fails (OperationFailedError) or outlasts `timeoutMs` (OperationTimeoutError).
179
186
  * Workspace.cell() supplies `workspace.wake()`. A call refused with `workspace_not_running` wakes the workspace and is
180
187
  * retried; a refused call was never executed, so the retry cannot duplicate it. `null` surfaces
181
188
  * the refusal instead. A wake that put a new token into this client's manager (the held resume;
@@ -292,6 +299,14 @@ export interface RunOptions {
292
299
  burstVcpus?: number;
293
300
  /** Burst VM memory in MiB (512-65536; default the host's, 8192, or the plan's ceiling when lower). */
294
301
  burstMemoryMib?: number;
302
+ /**
303
+ * How much memory the command needs at its start (0.14.0; sent as `resource_hint`, omitted when unset): `'heavy'`
304
+ * grows an elastic workspace's memory before the command starts, so a build, test suite or install sizes its heap and
305
+ * workers from the memory it gets; `'light'` starts it at once and the workspace grows while it runs; `'auto'` (the
306
+ * default) decides from the command (package installers, test runners, compilers, type checkers and bundlers are
307
+ * heavy). A value outside these is ShardfluxApiError 422 `validation_failed` (details.field `resource_hint`).
308
+ */
309
+ resourceHint?: ResourceHint;
295
310
  }
296
311
  /** The API error of a burst's recorded failure (`burst.error` of its session). Internal (the agent tools use it too). */
297
312
  export declare function burstFailure(e: BurstError): ShardfluxApiError;
@@ -320,8 +335,10 @@ export declare class CellClient {
320
335
  * One authorized request. Refreshes the token once on stale_epoch / 401. Lifecycle transitions,
321
336
  * bounded in total by `transitionTimeoutMs`: a call refused with `workspace_busy` is retried after `Retry-After`; one
322
337
  * refused with `workspace_not_running` (or whose token cannot be minted because the workspace is not running) wakes
323
- * the workspace through `wake`, given the time left, and is retried with a fresh token, at most 3 wakes per call.
324
- * Refused calls were never executed, so retrying is safe. When the budget is spent the refusal surfaces.
338
+ * the workspace through `wake`, given the time left, and is retried with a fresh token, at most 3 wakes per call. A
339
+ * wake that finds the workspace already running (`false`) is retried the same way: the transition that refused the
340
+ * call ended meanwhile (0.13.1+). Refused calls were never executed, so retrying is safe. When the budget is spent
341
+ * the refusal surfaces.
325
342
  */
326
343
  request(method: string, path: string, init?: RequestOptions & TransitionOptions): Promise<Response>;
327
344
  /**