@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/dist/client.js CHANGED
@@ -3,6 +3,7 @@ import { HttpClient, SDK_VERSION, SERVER_WAIT_MAX_S, defaultFetch, defaultSleep,
3
3
  import { Workspace } from "./workspace.js";
4
4
  import { AuditApi } from "./audit.js";
5
5
  import { EgressPolicyApi } from "./egress.js";
6
+ import { WorkspacePorts } from "./ports.js";
6
7
  import { SecretsApi } from "./secrets.js";
7
8
  import { TemplatesApi, saveAsTemplateBody } from "./templates.js";
8
9
  import { UsageApi } from "./usage.js";
@@ -46,6 +47,70 @@ function alreadyRunning(answer) {
46
47
  },
47
48
  }, 'api');
48
49
  }
50
+ /** The `PATCH .../caps` body of a resize: the given caps only, snake_case. Throws TypeError when none is given. */
51
+ function resizeBody(p) {
52
+ const body = {};
53
+ if (p.memoryMib !== undefined)
54
+ body.memory_mib = p.memoryMib;
55
+ if (p.memoryMibHeld !== undefined)
56
+ body.memory_mib_held = p.memoryMibHeld;
57
+ if (p.allocationMode !== undefined)
58
+ body.allocation_mode = p.allocationMode;
59
+ if (p.cpuMillis !== undefined)
60
+ body.cpu_millis = p.cpuMillis;
61
+ if (p.diskGib !== undefined)
62
+ body.disk_gib = p.diskGib;
63
+ if (Object.keys(body).length === 0)
64
+ throw new TypeError('resize() needs at least one of memoryMib, memoryMibHeld, allocationMode, cpuMillis, diskGib');
65
+ return body;
66
+ }
67
+ const RESIZE_KEYS = {
68
+ memory: ['applies_at', 'allocation_mode', 'requested_mib', 'target_mib', 'previous_mib', 'applied_mib', 'held_mib', 'converged', 'limit_reason', 'reason'],
69
+ cpu: ['applies_at', 'requested_millis', 'target_millis', 'previous_millis', 'applied_millis', 'limit_reason', 'reason'],
70
+ disk: ['applies_at', 'requested_gib', 'target_gib', 'previous_gib', 'applied_gib', 'limit_reason', 'reason'],
71
+ };
72
+ const record = (v) => (v !== null && typeof v === 'object' && !Array.isArray(v) ? v : {});
73
+ /**
74
+ * A finished resize operation's `result` in the held answer's shape (as the API builds its 200): each resource the
75
+ * cell reported with its documented keys, a key the cell left out taken from the operation's input (target, requested)
76
+ * or null; the cell's other keys (timings, the previous values of a retried attempt) are left out.
77
+ */
78
+ function resizeResultBodyOf(op) {
79
+ const result = record(op.result);
80
+ const input = record(op.input);
81
+ const target = record(input.target);
82
+ const requested = record(input.requested);
83
+ const fallback = {
84
+ memory: { allocation_mode: target.allocation_mode, requested_mib: requested.memory_mib, target_mib: target.memory_mib, held_mib: target.memory_mib_held },
85
+ cpu: { requested_millis: requested.cpu_millis, target_millis: target.cpu_millis },
86
+ disk: { requested_gib: requested.disk_gib, target_gib: target.disk_gib },
87
+ };
88
+ const out = { workspace_id: result.workspace_id ?? op.workspace_id, operation_id: op.id, state: result.state };
89
+ for (const [name, keys] of Object.entries(RESIZE_KEYS)) {
90
+ if (result[name] === null || typeof result[name] !== 'object')
91
+ continue;
92
+ const r = record(result[name]);
93
+ out[name] = Object.fromEntries(keys.map((k) => [k, r[k] ?? fallback[name]?.[k] ?? null]));
94
+ }
95
+ return out;
96
+ }
97
+ /** A resize result body (the held 200, or a finished operation's `result`) as ResizeResult. */
98
+ function resizeResultOf(body, status, operation, workspaceId) {
99
+ if (!body || typeof body !== 'object' || !['memory', 'cpu', 'disk'].some((k) => body[k] && typeof body[k] === 'object')) {
100
+ throw new ShardfluxProtocolError('resize: the result names no resource (memory, cpu or disk)', status, 'api');
101
+ }
102
+ const part = (k) => (body[k] && typeof body[k] === 'object' ? body[k] : null);
103
+ return {
104
+ workspaceId: typeof body.workspace_id === 'string' ? body.workspace_id : workspaceId,
105
+ operationId: typeof body.operation_id === 'string' ? body.operation_id : (operation?.id ?? null),
106
+ state: typeof body.state === 'string' ? body.state : 'unknown',
107
+ memory: part('memory'),
108
+ cpu: part('cpu'),
109
+ disk: part('disk'),
110
+ caps: body.caps && typeof body.caps === 'object' ? body.caps : null,
111
+ operation,
112
+ };
113
+ }
49
114
  /** Races the injected sleep against the signal (the injected sleep itself may not be abortable). */
50
115
  function abortableSleep(sleep, ms, signal) {
51
116
  if (signal.aborted)
@@ -316,6 +381,10 @@ export class WorkspacesApi {
316
381
  async get(workspaceId, opts = {}) {
317
382
  return this.#wrap(await this.#getView(workspaceId), opts);
318
383
  }
384
+ /** The exposed ports of a workspace by id (0.14.0; the same as `workspace.ports` without reading the workspace first). */
385
+ ports(workspaceId) {
386
+ return new WorkspacePorts(this.#ctx(), workspaceId);
387
+ }
319
388
  /** Replace labels. An empty map clears them. */
320
389
  async setLabels(workspaceId, labels) {
321
390
  return this.#wrap(await this.#http.json('PUT', `/v1/workspaces/${encodeURIComponent(workspaceId)}/labels`, { json: { labels } }, this.#auth));
@@ -514,6 +583,85 @@ export class WorkspacesApi {
514
583
  async cancelSuspendWhenIdle(workspaceId) {
515
584
  return this.#wrap(await this.#http.json('DELETE', `/v1/workspaces/${encodeURIComponent(workspaceId)}/suspend-when-idle`, {}, this.#auth));
516
585
  }
586
+ /**
587
+ * Resizes a workspace (0.14.0; `PATCH /v1/workspaces/{id}/caps`): any of memory, the held floor, the allocation mode,
588
+ * CPU and disk, of a running, suspended or stopped workspace, fixed or elastic, without a restart or a fork. The new
589
+ * caps are stored, so every later start uses them. Resolves once the resize has finished, with what applied per
590
+ * resource: `now` (live), `resume` (a suspended workspace gets it when it resumes, before its first call) or
591
+ * `next_start` (with the `reason`). The request is held by the server while the resize runs; a resize still running
592
+ * after the hold is waited for like any operation.
593
+ *
594
+ * A resize that meets another lifecycle operation (409 `operation_in_progress`: a suspend, a resume, another resize)
595
+ * waits for it and is sent again, within `timeoutMs`.
596
+ *
597
+ * Errors: ShardfluxApiError 409 `conflict` (reason `resize_not_available`, `workspace_deleted`), 422
598
+ * `validation_failed` (reason `shrink_not_supported` with `details.current_disk_gib`, `requires_elastic`,
599
+ * `exceeds_memory_mib`, `allocation_mode_not_available`, `not_supported_for_mode`). A resize that fails is its
600
+ * error: ShardfluxApiError with `operationId` when the server held the request until then, OperationFailedError when
601
+ * the SDK waited for the operation. OperationTimeoutError after `timeoutMs`. Params without a resource throw
602
+ * TypeError before any request.
603
+ *
604
+ * const r = await cloud.workspaces.resize(id, { memoryMib: 6144 });
605
+ * r.memory?.applies_at; // 'now'
606
+ */
607
+ async resize(workspaceId, params) {
608
+ const body = resizeBody(params);
609
+ const trace = new Trace('resize', combineListeners(this.#ctx().onProgress, params.onProgress), { workspaceId });
610
+ return traced(trace, async () => {
611
+ const timeoutMs = params.timeoutMs ?? 300_000;
612
+ const started = Date.now();
613
+ const left = () => Math.max(1, timeoutMs - (Date.now() - started));
614
+ const idempotencyKey = params.idempotencyKey ?? randomId('op-');
615
+ const path = `/v1/workspaces/${encodeURIComponent(workspaceId)}/caps`;
616
+ let res;
617
+ for (let attempt = 0;; attempt += 1) {
618
+ const waitS = Math.min(SERVER_WAIT_MAX_S, Math.floor((attempt === 0 ? timeoutMs : left()) / 1000));
619
+ const init = { json: body, idempotencyKey, onRetry: trace.onRetry, ...(params.signal ? { signal: params.signal } : {}) };
620
+ if (waitS >= 1) {
621
+ // Held: the server answers once the resize has finished (200) or the wait elapsed (202).
622
+ init.headers = { prefer: `wait=${waitS}` };
623
+ init.timeoutMs = Math.max(this.#http.opts.timeoutMs, waitS * 1000 + 10_000);
624
+ }
625
+ trace.phase('request', waitS >= 1 ? 'held' : attempt === 0 ? null : 'retry');
626
+ try {
627
+ res = await this.#http.jsonWithStatus('PATCH', path, init, this.#auth);
628
+ break;
629
+ }
630
+ catch (e) {
631
+ // Another lifecycle operation (a suspend, a resume, another resize) holds the workspace: the refused request
632
+ // changed nothing, so wait for that operation and send it again (bounded, within timeoutMs).
633
+ const active = e instanceof ShardfluxApiError && e.code === 'conflict' && e.reason === 'operation_in_progress' ? (e.operationId ?? e.details?.['active_operation_id']) : undefined;
634
+ if (typeof active !== 'string' || attempt >= 3 || Date.now() - started >= timeoutMs)
635
+ throw e;
636
+ trace.retry({ request: `PATCH ${path}`, attempt: attempt + 1, cause: `conflict operation_in_progress (waits for ${active})`, delayMs: 0 });
637
+ try {
638
+ await this.waitForOperation(active, { timeoutMs: left(), ...(params.signal ? { signal: params.signal } : {}), [TRACE]: trace });
639
+ }
640
+ catch (w) {
641
+ if (!(w instanceof OperationFailedError))
642
+ throw w;
643
+ }
644
+ }
645
+ }
646
+ if (res.status !== 202)
647
+ return resizeResultOf(res.body, res.status, null, workspaceId);
648
+ const operation = res.body?.operation;
649
+ if (!operation || typeof operation !== 'object')
650
+ throw new ShardfluxProtocolError('resize: 202 response has no operation', res.status, 'api');
651
+ trace.observe(operation);
652
+ let final = operation;
653
+ if (operation.state !== 'succeeded') {
654
+ if (TERMINAL.has(operation.state))
655
+ throw new OperationFailedError(operation);
656
+ const wait = { timeoutMs: Math.max(1, timeoutMs - (Date.now() - started)), ...(params.signal ? { signal: params.signal } : {}), [TRACE]: trace };
657
+ final = await this.waitForOperation(operation.id, wait);
658
+ }
659
+ // The finished operation's result is the cell's: shaped like the held answer, whose caps come from the view.
660
+ const result = resizeResultBodyOf(final);
661
+ const view = await trace.span('view', () => this.#getView(workspaceId, trace.onRetry));
662
+ return resizeResultOf({ ...result, state: result.state ?? view.observed_state, caps: view.caps }, res.status, final, workspaceId);
663
+ });
664
+ }
517
665
  async close(workspaceId, opts = {}) {
518
666
  return (await this.closeWithView(workspaceId, opts)).operation;
519
667
  }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Codex identity proof (0.14.0+, Node): the OpenAI ID token of the Codex CLI signed in on this machine, for
3
+ * `ShardfluxAccount.signup({ codexIdToken })` and `auth.stepUp({ codexIdToken })`.
4
+ *
5
+ * Codex keeps the token in `$CODEX_HOME/auth.json` (default `~/.codex/auth.json`) and refreshes it only every few
6
+ * days, while the API accepts one issued in the last 15 minutes. So this first asks Codex to refresh its own login
7
+ * (`codex app-server`, JSON-RPC `account/read {"refreshToken": true}`: Codex runs its normal refresh flow and writes
8
+ * its own file), then reads the token. Only the ID token is returned: Codex's access and refresh tokens are never read
9
+ * into memory beyond parsing the file, and never leave the machine. The ID token proves the ChatGPT account's verified
10
+ * email; it is not a credential to OpenAI.
11
+ *
12
+ * const proof = await codexIdentityProof();
13
+ * if (proof.ok) {
14
+ * const { account, result } = await ShardfluxAccount.signup({ codexIdToken: proof.idToken });
15
+ * }
16
+ */
17
+ export type CodexProofUnavailable =
18
+ /** No `codex` executable on PATH (or CODEX_BIN), and no usable auth.json. */
19
+ 'codex_not_found'
20
+ /** Codex is not signed in (no auth.json, or no ID token in it: signed in with an API key, or credentials in the OS keyring). */
21
+ | 'not_signed_in'
22
+ /** Codex is signed in with an OpenAI API key, which carries no identity. */
23
+ | 'api_key_login'
24
+ /** The token on disk is too old and Codex could not refresh it. */
25
+ | 'stale';
26
+ export type CodexIdentityProof = {
27
+ ok: true;
28
+ idToken: string;
29
+ email: string | null;
30
+ refreshed: boolean;
31
+ } | {
32
+ ok: false;
33
+ reason: CodexProofUnavailable;
34
+ message: string;
35
+ };
36
+ export interface CodexProofOptions {
37
+ /** Environment to read CODEX_HOME, CODEX_BIN and HOME from. Default `process.env`. */
38
+ env?: Record<string, string | undefined>;
39
+ /** Longest wait for `codex app-server` (ms). Default 15 000. */
40
+ timeoutMs?: number;
41
+ /** Skip the refresh and only read the file (tests, or when Codex must not be started). */
42
+ refresh?: boolean;
43
+ /** Accept a token issued at most this long ago (s), e.g. when Codex cannot refresh. Default 600 (the API allows 900). */
44
+ maxAgeSeconds?: number;
45
+ /** clientInfo sent to the app server. */
46
+ clientName?: string;
47
+ clientVersion?: string;
48
+ }
49
+ /**
50
+ * The ID token of this machine's Codex login, refreshed by Codex when needed. Never throws: `{ ok: false, reason }`
51
+ * says why there is none (sign up with an email address instead).
52
+ */
53
+ export declare function codexIdentityProof(opts?: CodexProofOptions): Promise<CodexIdentityProof>;
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Codex identity proof (0.14.0+, Node): the OpenAI ID token of the Codex CLI signed in on this machine, for
3
+ * `ShardfluxAccount.signup({ codexIdToken })` and `auth.stepUp({ codexIdToken })`.
4
+ *
5
+ * Codex keeps the token in `$CODEX_HOME/auth.json` (default `~/.codex/auth.json`) and refreshes it only every few
6
+ * days, while the API accepts one issued in the last 15 minutes. So this first asks Codex to refresh its own login
7
+ * (`codex app-server`, JSON-RPC `account/read {"refreshToken": true}`: Codex runs its normal refresh flow and writes
8
+ * its own file), then reads the token. Only the ID token is returned: Codex's access and refresh tokens are never read
9
+ * into memory beyond parsing the file, and never leave the machine. The ID token proves the ChatGPT account's verified
10
+ * email; it is not a credential to OpenAI.
11
+ *
12
+ * const proof = await codexIdentityProof();
13
+ * if (proof.ok) {
14
+ * const { account, result } = await ShardfluxAccount.signup({ codexIdToken: proof.idToken });
15
+ * }
16
+ */
17
+ function claimsOf(token) {
18
+ const part = token.split('.')[1];
19
+ if (!part)
20
+ return null;
21
+ try {
22
+ return JSON.parse(Buffer.from(part, 'base64url').toString('utf8'));
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ function fresh(token, maxAgeSeconds) {
29
+ const c = claimsOf(token);
30
+ const now = Date.now() / 1000;
31
+ return c !== null && typeof c.iat === 'number' && typeof c.exp === 'number' && now - c.iat <= maxAgeSeconds && c.exp - now > 60;
32
+ }
33
+ async function codexHome(env) {
34
+ const path = await import('node:path');
35
+ const os = await import('node:os');
36
+ const set = env.CODEX_HOME?.trim();
37
+ return set ? path.resolve(set) : path.join(env.HOME?.trim() || os.homedir(), '.codex');
38
+ }
39
+ /** `tokens.id_token` of auth.json, or null (missing file, other shape, API-key login). */
40
+ async function readIdToken(home) {
41
+ const fs = await import('node:fs/promises');
42
+ const path = await import('node:path');
43
+ let raw;
44
+ try {
45
+ raw = await fs.readFile(path.join(home, 'auth.json'), 'utf8');
46
+ }
47
+ catch {
48
+ return { token: null, apiKeyOnly: false, exists: false };
49
+ }
50
+ try {
51
+ const doc = JSON.parse(raw);
52
+ const token = typeof doc.tokens?.id_token === 'string' && doc.tokens.id_token.length > 0 ? doc.tokens.id_token : null;
53
+ const apiKeyOnly = token === null && (doc.auth_mode === 'apikey' || (typeof doc.OPENAI_API_KEY === 'string' && doc.OPENAI_API_KEY.length > 0));
54
+ return { token, apiKeyOnly, exists: true };
55
+ }
56
+ catch {
57
+ return { token: null, apiKeyOnly: false, exists: true };
58
+ }
59
+ }
60
+ /**
61
+ * The `codex` executables to try, in PATH order (CODEX_BIN alone when set). Every match is kept, not only the first:
62
+ * npx puts ancestor node_modules/.bin directories first on PATH, where an unrelated npm package named `codex` can
63
+ * shadow the Codex CLI. Distinct by real path.
64
+ */
65
+ async function codexCandidates(env) {
66
+ const set = env.CODEX_BIN?.trim();
67
+ if (set)
68
+ return [set];
69
+ const fs = await import('node:fs/promises');
70
+ const path = await import('node:path');
71
+ const names = process.platform === 'win32' ? ['codex.exe', 'codex.cmd', 'codex'] : ['codex'];
72
+ const out = [];
73
+ const seen = new Set();
74
+ for (const dir of (env.PATH ?? '').split(path.delimiter)) {
75
+ if (!dir)
76
+ continue;
77
+ for (const name of names) {
78
+ const file = path.join(dir, name);
79
+ try {
80
+ await fs.access(file, fs.constants.X_OK);
81
+ const real = await fs.realpath(file);
82
+ if (seen.has(real))
83
+ continue;
84
+ seen.add(real);
85
+ out.push(file);
86
+ }
87
+ catch {
88
+ // not there, or not executable
89
+ }
90
+ }
91
+ }
92
+ return out;
93
+ }
94
+ /** Asks Codex to refresh its own login through the app server of the first candidate that speaks its protocol. */
95
+ async function refreshViaAppServer(env, opts) {
96
+ const candidates = await codexCandidates(env);
97
+ if (candidates.length === 0)
98
+ return 'not_found';
99
+ let outcome = 'not_found';
100
+ for (const bin of candidates.slice(0, 4)) {
101
+ outcome = await appServerRefresh(bin, env, opts);
102
+ if (outcome !== 'failed' && outcome !== 'not_found')
103
+ return outcome;
104
+ }
105
+ return outcome;
106
+ }
107
+ async function appServerRefresh(bin, env, opts) {
108
+ const { spawn } = await import('node:child_process');
109
+ return new Promise((resolve) => {
110
+ let settled = false;
111
+ let buffer = '';
112
+ const child = spawn(bin, ['app-server'], { stdio: ['pipe', 'pipe', 'ignore'], env, windowsHide: true });
113
+ const done = (o) => {
114
+ if (settled)
115
+ return;
116
+ settled = true;
117
+ clearTimeout(timer);
118
+ child.stdin.end();
119
+ child.kill();
120
+ resolve(o);
121
+ };
122
+ const timer = setTimeout(() => done('failed'), opts.timeoutMs);
123
+ const send = (msg) => child.stdin.write(`${JSON.stringify(msg)}\n`);
124
+ child.on('error', (err) => done(err.code === 'ENOENT' ? 'not_found' : 'failed'));
125
+ child.on('exit', () => done('failed'));
126
+ child.stdin.on('error', () => done('failed'));
127
+ child.stdout.setEncoding('utf8');
128
+ child.stdout.on('data', (chunk) => {
129
+ buffer += chunk;
130
+ let nl;
131
+ while ((nl = buffer.indexOf('\n')) >= 0) {
132
+ const line = buffer.slice(0, nl).trim();
133
+ buffer = buffer.slice(nl + 1);
134
+ if (line.length === 0)
135
+ continue;
136
+ let msg;
137
+ try {
138
+ msg = JSON.parse(line);
139
+ }
140
+ catch {
141
+ continue;
142
+ }
143
+ if (msg.id === 1) {
144
+ if (msg.error !== undefined)
145
+ return done('failed');
146
+ send({ jsonrpc: '2.0', method: 'initialized' });
147
+ send({ jsonrpc: '2.0', id: 2, method: 'account/read', params: { refreshToken: true } });
148
+ }
149
+ else if (msg.id === 2) {
150
+ if (msg.error !== undefined)
151
+ return done('failed');
152
+ const type = msg.result?.account?.type;
153
+ return done(type === 'chatgpt' ? 'refreshed' : type === 'apiKey' ? 'api_key_login' : type === undefined || type === null ? 'not_signed_in' : 'failed');
154
+ }
155
+ }
156
+ });
157
+ send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { clientInfo: { name: opts.clientName, version: opts.clientVersion } } });
158
+ });
159
+ }
160
+ /**
161
+ * The ID token of this machine's Codex login, refreshed by Codex when needed. Never throws: `{ ok: false, reason }`
162
+ * says why there is none (sign up with an email address instead).
163
+ */
164
+ export async function codexIdentityProof(opts = {}) {
165
+ const env = opts.env ?? process.env;
166
+ const maxAge = opts.maxAgeSeconds ?? 600;
167
+ const home = await codexHome(env);
168
+ let outcome = 'skipped';
169
+ // Always refresh first: each refresh mints a token with a new jti, and the API accepts each jti once, so a token
170
+ // still fresh on disk may already have been used. The file alone is the fallback when Codex cannot refresh.
171
+ let file = await readIdToken(home);
172
+ let renewed = false;
173
+ if (opts.refresh !== false) {
174
+ const appServer = { timeoutMs: opts.timeoutMs ?? 15_000, clientName: opts.clientName ?? 'shardflux', clientVersion: opts.clientVersion ?? '0' };
175
+ const before = file.token;
176
+ // A refresh counts only when the token on disk changed. One retry: a refresh can fail on a transient network
177
+ // error, answer without renewing, or race another Codex process refreshing the same login.
178
+ for (let attempt = 0; attempt < 2 && !renewed; attempt++) {
179
+ if (attempt > 0)
180
+ await new Promise((r) => setTimeout(r, 500));
181
+ outcome = await refreshViaAppServer(env, appServer);
182
+ file = await readIdToken(home);
183
+ renewed = file.token !== null && file.token !== before && fresh(file.token, maxAge);
184
+ if (outcome === 'api_key_login' || outcome === 'not_signed_in' || outcome === 'not_found')
185
+ break;
186
+ }
187
+ }
188
+ if (file.token !== null && fresh(file.token, maxAge)) {
189
+ const email = claimsOf(file.token)?.email;
190
+ return { ok: true, idToken: file.token, email: typeof email === 'string' ? email : null, refreshed: renewed };
191
+ }
192
+ if (outcome === 'api_key_login' || file.apiKeyOnly)
193
+ return { ok: false, reason: 'api_key_login', message: 'Codex is signed in with an API key, which carries no identity. Sign in with ChatGPT (codex login), or sign up with an email address.' };
194
+ if (outcome === 'not_signed_in' || (file.token === null && outcome !== 'not_found')) {
195
+ return { ok: false, reason: 'not_signed_in', message: 'Codex is not signed in with ChatGPT on this machine (codex login), or keeps its login in the OS keyring. Sign up with an email address instead.' };
196
+ }
197
+ if (outcome === 'not_found' && !file.exists)
198
+ return { ok: false, reason: 'codex_not_found', message: 'The Codex CLI is not installed (or not on PATH). Sign up with an email address instead.' };
199
+ return { ok: false, reason: 'stale', message: 'The Codex login on this machine could not be refreshed. Run any codex command (or codex login), then try again.' };
200
+ }
package/dist/errors.d.ts CHANGED
@@ -82,7 +82,7 @@ export type ErrorCode = AppErrorCode | CellErrorCode;
82
82
  * `retained_state` (details.limit_value and details.current in GiB) when opening a new key or forking with the plan's
83
83
  * Retained state used up. `details.limit` is not a reason: these are not in this union.
84
84
  */
85
- export type KnownErrorReason = 'invalid_recipe' | 'base_not_layered' | 'language_unavailable' | 'language_conflict' | 'invalid_package' | 'too_many_files' | 'platform_owned_path' | 'upload_required' | 'upload_missing' | 'upload_digest_mismatch' | 'upload_too_large' | 'extra_hosts_without_auto' | 'invalid_settings' | 'services_unsupported' | 'input_required' | 'input_unknown' | 'input_invalid' | 'egress_widening' | 'reserved_session_id' | 'env_collision' | 'reserved_template_slug' | 'package_index_unavailable' | 'package_not_found' | 'startup_failed' | 'service_not_ready' | 'secrets_unavailable' | 'workspace_not_running' | 'operation_in_progress' | 'workspace_deleted' | 'secret_not_available' | 'legacy_disk_layout' | 'not_session' | 'session_lifetime' | 'lifetime_mismatch' | 'not_resettable' | 'template_not_layered' | 'draft_exists' | 'draft_stale' | 'build_in_progress' | 'file_list_unavailable' | 'file_list_indexing' | 'guest_feature_unavailable' | 'confirm_destructive_required' | 'reserved_key_prefix' | 'invalid_defaults' | 'invalid_path' | 'too_many_acknowledged_findings' | 'template_dev_mode_role' | 'draft_not_found' | 'version_not_found' | 'path_not_found' | 'revision_mismatch' | 'edit_not_found' | 'edit_ambiguous' | 'edit_not_text' | 'patch_invalid' | 'host_capacity' | 'wake_failed' | 'workspace_fenced' | 'offline_unavailable' | 'offline_budget' | 'offline_changed' | 'host_feature_unavailable' | 'not_supported_for_mode' | 'mode_mismatch' | 'mode_not_available' | 'layout_unsupported' | 'tree_revision_mismatch' | 'outside_tree_root' | 'execution_in_progress' | 'execution_id_reused' | 'operation_id_reused' | 'no_execution_host' | 'lease_expired' | 'host_unreachable' | 'host_restarted' | 'tree_moved' | 'blob_missing' | 'blob_corrupt' | 'exec_failed_to_start' | 'invalid_cwd' | 'allowance_used' | 'overage_paused' | 'spend_cap_reached' | 'overage_unavailable' | 'spend_cap_required' | 'spend_cap_below_minimum' | 'spend_cap_above_plan_price' | 'spend_cap_below_charges' | 'version_mismatch' | 'allocation_mode_not_available' | 'requires_elastic' | 'exceeds_memory_mib' | 'burst_mode_not_supported' | 'burst_not_supported' | 'burst_size_exceeds_plan' | 'not_available' | 'shared_volumes' | 'fence_not_drained' | 'apply_pending' | 'park_failed' | 'workspace_resumed' | 'interrupted' | 'burst_lost' | 'disk_full' | 'apply_failed' | 'reverted' | 'revert_failed' | 'immutable_path_removed' | 'immutable_paths_unsupported_base' | 'read_only_path' | 'host_lost';
85
+ export type KnownErrorReason = 'invalid_recipe' | 'base_not_layered' | 'language_unavailable' | 'language_conflict' | 'invalid_package' | 'too_many_files' | 'platform_owned_path' | 'upload_required' | 'upload_missing' | 'upload_digest_mismatch' | 'upload_too_large' | 'extra_hosts_without_auto' | 'invalid_settings' | 'services_unsupported' | 'input_required' | 'input_unknown' | 'input_invalid' | 'egress_widening' | 'reserved_session_id' | 'env_collision' | 'reserved_template_slug' | 'package_index_unavailable' | 'package_not_found' | 'startup_failed' | 'service_not_ready' | 'secrets_unavailable' | 'workspace_not_running' | 'operation_in_progress' | 'workspace_deleted' | 'secret_not_available' | 'legacy_disk_layout' | 'not_session' | 'session_lifetime' | 'lifetime_mismatch' | 'not_resettable' | 'template_not_layered' | 'draft_exists' | 'draft_stale' | 'build_in_progress' | 'file_list_unavailable' | 'file_list_indexing' | 'guest_feature_unavailable' | 'confirm_destructive_required' | 'reserved_key_prefix' | 'invalid_defaults' | 'invalid_path' | 'too_many_acknowledged_findings' | 'template_dev_mode_role' | 'draft_not_found' | 'version_not_found' | 'path_not_found' | 'revision_mismatch' | 'edit_not_found' | 'edit_ambiguous' | 'edit_not_text' | 'patch_invalid' | 'host_capacity' | 'wake_failed' | 'workspace_fenced' | 'offline_unavailable' | 'offline_budget' | 'offline_changed' | 'host_feature_unavailable' | 'not_supported_for_mode' | 'mode_mismatch' | 'mode_not_available' | 'layout_unsupported' | 'tree_revision_mismatch' | 'outside_tree_root' | 'execution_in_progress' | 'execution_id_reused' | 'operation_id_reused' | 'no_execution_host' | 'lease_expired' | 'host_unreachable' | 'host_restarted' | 'tree_moved' | 'blob_missing' | 'blob_corrupt' | 'exec_failed_to_start' | 'invalid_cwd' | 'allowance_used' | 'overage_paused' | 'spend_cap_reached' | 'overage_unavailable' | 'spend_cap_required' | 'spend_cap_below_minimum' | 'spend_cap_above_plan_price' | 'spend_cap_below_charges' | 'version_mismatch' | 'allocation_mode_not_available' | 'requires_elastic' | 'exceeds_memory_mib' | 'burst_mode_not_supported' | 'burst_not_supported' | 'burst_size_exceeds_plan' | 'not_available' | 'shared_volumes' | 'fence_not_drained' | 'apply_pending' | 'park_failed' | 'workspace_resumed' | 'interrupted' | 'burst_lost' | 'disk_full' | 'apply_failed' | 'reverted' | 'revert_failed' | 'immutable_path_removed' | 'immutable_paths_unsupported_base' | 'read_only_path' | 'resize_not_available' | 'shrink_not_supported' | 'resize_failed' | 'host_lost' | 'inbound_ports_not_available' | 'port_not_exposed' | 'port_limit';
86
86
  /** A known reason, or any other string the server sends (reasons are open-ended). */
87
87
  export type ErrorReason = KnownErrorReason | (string & {});
88
88
  export interface ErrorBodyLike {