@shardflux/sdk 0.13.1 → 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,10 +1,32 @@
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
10
  ## 0.14.0 (not yet published)
7
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
+
8
30
  ### Workspaces working at once
9
31
 
10
32
  Additive. The plan's workspace number caps workspaces working at the same moment; stored workspaces are bounded by
@@ -25,6 +47,76 @@ Retained state.
25
47
  - Regenerated contract types: `ErrorCode` includes `quota_exceeded` for the workspace gateway; allowance
26
48
  `enforcement` adds `storage_block`, `cap_state` adds `storage_blocked`.
27
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
+
28
120
  ## 0.13.1 (not yet published)
29
121
 
30
122
  Patch, additive: the transport, a documented value, a fix for tool calls across a move and self-healing workspaces.
package/README.md CHANGED
@@ -360,10 +360,11 @@ lookups by key.
360
360
 
361
361
  ### Elastic memory (0.13.0+)
362
362
 
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.
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.
367
368
 
368
369
  ```ts
369
370
  const ws = await cloud.workspaces.open({
@@ -375,10 +376,32 @@ ws.memory; // { allocation_mode: 'elastic', promised_mib: 8192, held_m
375
376
 
376
377
  const r = await ws.cell().exec.run(['npm', 'test']);
377
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
381
+ ```
382
+
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
378
399
  ```
379
400
 
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.
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.
382
405
 
383
406
  ### Burst execution (0.13.0+)
384
407
 
@@ -398,6 +421,36 @@ workspace's own processes resume where they were. A burst is not signalled or ca
398
421
  fails (`burst_apply_failed`), retry `exec.run` with the same `sessionId` to finish the apply. The agent tools offer
399
422
  bursts when asked: `workspaceTools(ws, { burst: true })`.
400
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
+
401
454
  ### Timing and progress (0.6.0+)
402
455
 
403
456
  Every open, wake and waited lifecycle call is traced. `workspace.lastTiming` (and `err.timing` when the call fails)
@@ -849,7 +902,7 @@ await workspace.cell().exec.run(['python3', 'agent.py']); // sees $OPEN
849
902
 
850
903
  `ShardfluxAccount` does what a person does in the web app, with a user session instead of an API key: sign up, sign
851
904
  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.
905
+ deletion. Paying in Stripe Checkout is the step a person does.
853
906
 
854
907
  ```ts
855
908
  import { Shardflux, ShardfluxAccount } from '@shardflux/sdk';
@@ -887,6 +940,37 @@ const cloud = new Shardflux({ apiKey: secret });
887
940
  - `parseEmailToken(input)` returns the `token` of a link's `#token=` fragment or `?token=` query (else the trimmed
888
941
  input), and throws for a link without one.
889
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
+
890
974
  Upgrading a plan: a person pays at the Checkout `url`; the code waits for the subscription.
891
975
 
892
976
  ```ts
@@ -966,6 +1050,11 @@ of that base), or `invalid_path` (`details.field` `recipe.immutable[<i>]`). A bu
966
1050
  `failure.code` `immutable_path_missing` (`failure.details.path` is not a directory in the built filesystem) or
967
1051
  `immutable_image_too_large`.
968
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
+
969
1058
  ## Usage and overage
970
1059
 
971
1060
  `cloud.usage` reads the organization's usage (API keys see organization totals and their own project's workspaces):
@@ -1110,3 +1199,13 @@ import { isWorkspaceGone, ShardfluxProtocolError } from '@shardflux/sdk';
1110
1199
  ### Repositories with a minimum release age
1111
1200
 
1112
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
  /**
@@ -293,6 +299,14 @@ export interface RunOptions {
293
299
  burstVcpus?: number;
294
300
  /** Burst VM memory in MiB (512-65536; default the host's, 8192, or the plan's ceiling when lower). */
295
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;
296
310
  }
297
311
  /** The API error of a burst's recorded failure (`burst.error` of its session). Internal (the agent tools use it too). */
298
312
  export declare function burstFailure(e: BurstError): ShardfluxApiError;
package/dist/cell.js CHANGED
@@ -444,6 +444,8 @@ export class CellClient {
444
444
  req.kill_grace_ms = opts.killGraceMs;
445
445
  if (opts.secretRefs !== undefined)
446
446
  req.secret_refs = opts.secretRefs;
447
+ if (opts.resourceHint !== undefined)
448
+ req.resource_hint = opts.resourceHint;
447
449
  if (opts.burst !== undefined)
448
450
  req.burst = opts.burst;
449
451
  if (opts.burstVcpus !== undefined)
package/dist/client.d.ts CHANGED
@@ -16,6 +16,7 @@ import type { ToolName, ToolToken } from './tokens.js';
16
16
  import { Workspace } from './workspace.js';
17
17
  import { AuditApi } from './audit.js';
18
18
  import { EgressPolicyApi } from './egress.js';
19
+ import { WorkspacePorts } from './ports.js';
19
20
  import { SecretsApi } from './secrets.js';
20
21
  import { TemplatesApi } from './templates.js';
21
22
  import type { SaveAsTemplateParams, SaveAsTemplateResponse } from './templates.js';
@@ -158,8 +159,9 @@ export interface Caps {
158
159
  memory_mib?: number;
159
160
  disk_gib?: number;
160
161
  /**
161
- * `fixed` (default) or `elastic` (0.13.0). Given caps replace the stored ones: caps without it make
162
- * the workspace fixed again; omitted caps keep the stored layout. Elastic needs the organization's entitlement, else
162
+ * `fixed` or `elastic` (0.13.0). Omitted: fixed, or elastic where that is the organization's default (a reopen then
163
+ * keeps the stored mode); the SDK never fills it in. Given caps replace the stored ones; omitted caps keep the stored
164
+ * layout. `workspace.resize()` (0.14.0+) changes it on a running or suspended workspace. Elastic needs the organization's entitlement, else
163
165
  * ShardfluxApiError 422 `validation_failed` reason `allocation_mode_not_available` (nothing is created or changed);
164
166
  * a file-first workspace gets `not_supported_for_mode`. A promise that does not exceed the held floor by at least
165
167
  * 512 MiB is fixed at the promise (the returned `caps.allocation_mode` says which). Takes effect at the next VM start.
@@ -176,6 +178,64 @@ export interface ForkTarget {
176
178
  caps?: Caps;
177
179
  lifetime?: WorkspaceLifetime;
178
180
  }
181
+ /**
182
+ * `workspace.resize()` / `workspaces.resize()` (0.14.0): the caps to change. Give at least one; the others keep their
183
+ * stored values. Each value is bounded by the plan's per-workspace maximum and the template's limit, as caps at open.
184
+ */
185
+ export interface ResizeParams {
186
+ /** Memory in MiB: the size of a fixed workspace, the maximum an elastic one may grow to. */
187
+ memoryMib?: number;
188
+ /** Elastic only: the memory the workspace holds while idle, in MiB (at most the resulting `memoryMib`). */
189
+ memoryMibHeld?: number;
190
+ /** `fixed` or `elastic`: switches the mode, live on a running workspace. */
191
+ allocationMode?: AllocationMode;
192
+ /** CPU in millicores (1000 = one vCPU). */
193
+ cpuMillis?: number;
194
+ /** Disk in GiB; disks grow only. */
195
+ diskGib?: number;
196
+ /** Replays the stored response for a repeated request (default: a fresh key per call, so transport retries replay). */
197
+ idempotencyKey?: string;
198
+ /** Give up waiting for the resize after this long (default 300 000 ms); it continues server side. */
199
+ timeoutMs?: number;
200
+ signal?: AbortSignal;
201
+ /** Progress of the resize (the held request, the operation's states, retries, `done` with the timing). */
202
+ onProgress?: ProgressListener;
203
+ }
204
+ /** The held answer of `PATCH /v1/workspaces/{id}/caps` (and a finished resize operation's `result`). */
205
+ export type ResizeResponse = components['schemas']['ResizeResult'];
206
+ /** The memory part of a resize result (0.14.0). `*_mib` are MiB; `applied_mib` is what is live after the call (null when no VM runs), `held_mib` the elastic floor, `converged` whether a live change reached its target. */
207
+ export type ResizeMemory = NonNullable<ResizeResponse['memory']>;
208
+ /** The CPU part of a resize result (0.14.0), in millicores. */
209
+ export type ResizeCpu = NonNullable<ResizeResponse['cpu']>;
210
+ /** The disk part of a resize result (0.14.0), in GiB. */
211
+ export type ResizeDisk = NonNullable<ResizeResponse['disk']>;
212
+ /** When a resized resource takes effect (0.14.0): `now`, when the suspended workspace resumes, or at its next start. */
213
+ export type ResizeAppliesAt = ResizeMemory['applies_at'];
214
+ /** Why less than requested applied: clamped to the plan or template bound, the host had no room, or the guest kept memory it uses. */
215
+ export type ResizeLimitReason = NonNullable<ResizeMemory['limit_reason']>;
216
+ /**
217
+ * Why a resource applies later than now: `suspended` (at resume), `stopped` (at the next start), `boot_vcpus` (more CPU
218
+ * than the vCPUs the workspace booted with), `region` / `below_base` / `legacy_layout` (a memory size its running VM was
219
+ * not booted for).
220
+ */
221
+ export type ResizeDeferReason = NonNullable<ResizeMemory['reason']>;
222
+ /**
223
+ * What a resize did (0.14.0), per resource the request named (the others are null): when it applies (`applies_at`),
224
+ * what was asked, the bounded target, the size before, what is live now, and why less or later.
225
+ */
226
+ export interface ResizeResult {
227
+ workspaceId: string;
228
+ operationId: string | null;
229
+ /** The workspace's state the result describes: `running`, `suspended` or `stopped`. */
230
+ state: string;
231
+ memory: ResizeMemory | null;
232
+ cpu: ResizeCpu | null;
233
+ disk: ResizeDisk | null;
234
+ /** The workspace's stored caps after the change: every later start uses them. */
235
+ caps: ResizeResponse['caps'] | null;
236
+ /** The finished `resize` operation when the API answered before it was done (202), else null. */
237
+ operation: Operation | null;
238
+ }
179
239
  export interface WaitOptions {
180
240
  /**
181
241
  * Give up waiting after this long (default 300 000 ms); the operation continues server side. A queued start
@@ -338,6 +398,8 @@ export declare class WorkspacesApi {
338
398
  agentLabel?: string;
339
399
  tools?: ToolName[];
340
400
  }): Promise<Workspace>;
401
+ /** The exposed ports of a workspace by id (0.14.0; the same as `workspace.ports` without reading the workspace first). */
402
+ ports(workspaceId: string): WorkspacePorts;
341
403
  /** Replace labels. An empty map clears them. */
342
404
  setLabels(workspaceId: string, labels: Record<string, string>): Promise<Workspace>;
343
405
  /** null clears the override, restoring the template or platform policy. */
@@ -412,6 +474,28 @@ export declare class WorkspacesApi {
412
474
  * is not undone: it is the workspace's `activeOperation` (resume or open the workspace instead).
413
475
  */
414
476
  cancelSuspendWhenIdle(workspaceId: string): Promise<Workspace>;
477
+ /**
478
+ * Resizes a workspace (0.14.0; `PATCH /v1/workspaces/{id}/caps`): any of memory, the held floor, the allocation mode,
479
+ * CPU and disk, of a running, suspended or stopped workspace, fixed or elastic, without a restart or a fork. The new
480
+ * caps are stored, so every later start uses them. Resolves once the resize has finished, with what applied per
481
+ * resource: `now` (live), `resume` (a suspended workspace gets it when it resumes, before its first call) or
482
+ * `next_start` (with the `reason`). The request is held by the server while the resize runs; a resize still running
483
+ * after the hold is waited for like any operation.
484
+ *
485
+ * A resize that meets another lifecycle operation (409 `operation_in_progress`: a suspend, a resume, another resize)
486
+ * waits for it and is sent again, within `timeoutMs`.
487
+ *
488
+ * Errors: ShardfluxApiError 409 `conflict` (reason `resize_not_available`, `workspace_deleted`), 422
489
+ * `validation_failed` (reason `shrink_not_supported` with `details.current_disk_gib`, `requires_elastic`,
490
+ * `exceeds_memory_mib`, `allocation_mode_not_available`, `not_supported_for_mode`). A resize that fails is its
491
+ * error: ShardfluxApiError with `operationId` when the server held the request until then, OperationFailedError when
492
+ * the SDK waited for the operation. OperationTimeoutError after `timeoutMs`. Params without a resource throw
493
+ * TypeError before any request.
494
+ *
495
+ * const r = await cloud.workspaces.resize(id, { memoryMib: 6144 });
496
+ * r.memory?.applies_at; // 'now'
497
+ */
498
+ resize(workspaceId: string, params: ResizeParams): Promise<ResizeResult>;
415
499
  /**
416
500
  * Ends a session workspace now: the workspace is deleted exactly like delete() (ended_reason
417
501
  * closed) and returns the `delete` operation (input.reason session_closed); the key then opens a NEW workspace.