@shardflux/sdk 0.13.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +134 -0
- package/README.md +233 -13
- package/dist/account.d.ts +41 -2
- package/dist/account.js +36 -2
- package/dist/cell.d.ts +39 -0
- package/dist/cell.js +71 -0
- package/dist/client.d.ts +115 -6
- package/dist/client.js +205 -5
- package/dist/codex-proof.d.ts +53 -0
- package/dist/codex-proof.js +200 -0
- package/dist/computer.d.ts +143 -0
- package/dist/computer.js +146 -0
- package/dist/errors.d.ts +1 -1
- package/dist/generated/app-api.d.ts +2649 -540
- package/dist/generated/cell-api.d.ts +338 -0
- package/dist/http.d.ts +1 -1
- package/dist/http.js +1 -1
- package/dist/index.d.ts +14 -6
- package/dist/index.js +7 -2
- package/dist/ports.d.ts +117 -0
- package/dist/ports.js +62 -0
- package/dist/progress.d.ts +11 -7
- package/dist/progress.js +2 -0
- package/dist/templates.d.ts +12 -0
- package/dist/templates.js +9 -0
- package/dist/tools.d.ts +24 -3
- package/dist/tools.js +131 -22
- package/dist/workspace-ref.d.ts +83 -0
- package/dist/workspace-ref.js +149 -0
- package/dist/workspace.d.ts +48 -1
- package/dist/workspace.js +71 -0
- package/package.json +1 -1
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
|
/**
|
|
@@ -141,6 +147,16 @@ export type GitCommitRequest = S['GitCommitRequest'];
|
|
|
141
147
|
export type GitResult = S['GitResult'];
|
|
142
148
|
export type GitStatus = S['GitStatus'];
|
|
143
149
|
export type BrowserScreenshotRequest = S['BrowserScreenshotRequest'];
|
|
150
|
+
/** Contracts §45 (0.15.0+): the workspace desktop. */
|
|
151
|
+
export type ComputerAction = S['ComputerAction'];
|
|
152
|
+
export type ComputerActionName = ComputerAction['action'];
|
|
153
|
+
export type ComputerActionsRequest = S['ComputerActionsRequest'];
|
|
154
|
+
export type ComputerActionsResult = S['ComputerActionsResult'];
|
|
155
|
+
export type ComputerActionResult = S['ComputerActionResult'];
|
|
156
|
+
export type ComputerImage = S['ComputerImage'];
|
|
157
|
+
export type ComputerStatus = S['ComputerStatus'];
|
|
158
|
+
export type ComputerStartRequest = S['ComputerStartRequest'];
|
|
159
|
+
export type ComputerStreamInfo = S['ComputerStream'];
|
|
144
160
|
export type BrowserContentRequest = S['BrowserContentRequest'];
|
|
145
161
|
export type BrowserContent = S['BrowserContent'];
|
|
146
162
|
export type Signal = S['SignalValue'];
|
|
@@ -293,6 +309,14 @@ export interface RunOptions {
|
|
|
293
309
|
burstVcpus?: number;
|
|
294
310
|
/** Burst VM memory in MiB (512-65536; default the host's, 8192, or the plan's ceiling when lower). */
|
|
295
311
|
burstMemoryMib?: number;
|
|
312
|
+
/**
|
|
313
|
+
* How much memory the command needs at its start (0.14.0; sent as `resource_hint`, omitted when unset): `'heavy'`
|
|
314
|
+
* grows an elastic workspace's memory before the command starts, so a build, test suite or install sizes its heap and
|
|
315
|
+
* workers from the memory it gets; `'light'` starts it at once and the workspace grows while it runs; `'auto'` (the
|
|
316
|
+
* default) decides from the command (package installers, test runners, compilers, type checkers and bundlers are
|
|
317
|
+
* heavy). A value outside these is ShardfluxApiError 422 `validation_failed` (details.field `resource_hint`).
|
|
318
|
+
*/
|
|
319
|
+
resourceHint?: ResourceHint;
|
|
296
320
|
}
|
|
297
321
|
/** The API error of a burst's recorded failure (`burst.error` of its session). Internal (the agent tools use it too). */
|
|
298
322
|
export declare function burstFailure(e: BurstError): ShardfluxApiError;
|
|
@@ -516,6 +540,21 @@ export declare class CellClient {
|
|
|
516
540
|
status: (path: string) => Promise<GitStatus>;
|
|
517
541
|
commit: (req: GitCommitRequest) => Promise<GitResult>;
|
|
518
542
|
};
|
|
543
|
+
readonly computer: {
|
|
544
|
+
status: () => Promise<ComputerStatus>;
|
|
545
|
+
start: (req?: ComputerStartRequest) => Promise<ComputerStatus>;
|
|
546
|
+
stop: () => Promise<void>;
|
|
547
|
+
/**
|
|
548
|
+
* Runs a batch of actions in order (the first failure stops it; the rest are reported `skipped`), then a screenshot
|
|
549
|
+
* when `screenshot` is set. Waits and key holds may take up to 300 s per batch.
|
|
550
|
+
*/
|
|
551
|
+
act: (req: ComputerActionsRequest, signal?: AbortSignal) => Promise<ComputerActionsResult>;
|
|
552
|
+
/** Starts the viewer in the guest; expose the returned port and open a link to `path` (Workspace.computer.stream does both). */
|
|
553
|
+
streamStart: (req?: {
|
|
554
|
+
interactive?: boolean;
|
|
555
|
+
}) => Promise<ComputerStreamInfo>;
|
|
556
|
+
streamStop: () => Promise<void>;
|
|
557
|
+
};
|
|
519
558
|
readonly browser: {
|
|
520
559
|
screenshot: (req: BrowserScreenshotRequest) => Promise<Uint8Array>;
|
|
521
560
|
content: (req: BrowserContentRequest) => Promise<BrowserContent>;
|
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)
|
|
@@ -1063,6 +1065,75 @@ export class CellClient {
|
|
|
1063
1065
|
return this.#json('POST', this.#p('/v1/workspaces/{workspace_id}/git/commit'), { json: req });
|
|
1064
1066
|
},
|
|
1065
1067
|
};
|
|
1068
|
+
// ---- computer (contracts §45, 0.15.0+) ---------------------------------------------------------
|
|
1069
|
+
/**
|
|
1070
|
+
* The workspace desktop. The platform starts it on the first call that needs it (actions, start, stream); status
|
|
1071
|
+
* never starts it. Needs the `computer` tool, which tool tokens carry while the workspace's computer use is on.
|
|
1072
|
+
*/
|
|
1073
|
+
/**
|
|
1074
|
+
* A computer call with one retry on a fresh token when the cached one lacks the computer tool: computer use may have
|
|
1075
|
+
* been switched on after the token was issued (tokens live up to 15 minutes).
|
|
1076
|
+
*/
|
|
1077
|
+
async #computer(call) {
|
|
1078
|
+
try {
|
|
1079
|
+
return await call();
|
|
1080
|
+
}
|
|
1081
|
+
catch (err) {
|
|
1082
|
+
if (!(err instanceof ShardfluxApiError && err.status === 403 && err.details?.tool === 'computer'))
|
|
1083
|
+
throw err;
|
|
1084
|
+
this.tokens.invalidate();
|
|
1085
|
+
return call();
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
computer = {
|
|
1089
|
+
status: () => {
|
|
1090
|
+
const refusal = this.#needsVm('computer.status');
|
|
1091
|
+
if (refusal)
|
|
1092
|
+
return Promise.reject(refusal);
|
|
1093
|
+
return this.#computer(() => this.#json('GET', this.#p('/v1/workspaces/{workspace_id}/computer')));
|
|
1094
|
+
},
|
|
1095
|
+
start: (req = {}) => {
|
|
1096
|
+
const refusal = this.#needsVm('computer.start');
|
|
1097
|
+
if (refusal)
|
|
1098
|
+
return Promise.reject(refusal);
|
|
1099
|
+
return this.#computer(() => this.#json('POST', this.#p('/v1/workspaces/{workspace_id}/computer'), { json: req, timeoutMs: 90_000 }));
|
|
1100
|
+
},
|
|
1101
|
+
stop: async () => {
|
|
1102
|
+
const refusal = this.#needsVm('computer.stop');
|
|
1103
|
+
if (refusal)
|
|
1104
|
+
throw refusal;
|
|
1105
|
+
await this.#computer(() => this.#json('DELETE', this.#p('/v1/workspaces/{workspace_id}/computer')));
|
|
1106
|
+
},
|
|
1107
|
+
/**
|
|
1108
|
+
* Runs a batch of actions in order (the first failure stops it; the rest are reported `skipped`), then a screenshot
|
|
1109
|
+
* when `screenshot` is set. Waits and key holds may take up to 300 s per batch.
|
|
1110
|
+
*/
|
|
1111
|
+
act: (req, signal) => {
|
|
1112
|
+
const refusal = this.#needsVm('computer.actions');
|
|
1113
|
+
if (refusal)
|
|
1114
|
+
return Promise.reject(refusal);
|
|
1115
|
+
const waits = req.actions.reduce((sum, a) => sum + (a.duration ?? 0), 0);
|
|
1116
|
+
const typing = req.actions.reduce((sum, a) => sum + (a.action === 'type' ? (a.text?.length ?? 0) : 0), 0);
|
|
1117
|
+
return this.#computer(() => this.#json('POST', this.#p('/v1/workspaces/{workspace_id}/computer/actions'), {
|
|
1118
|
+
json: req,
|
|
1119
|
+
timeoutMs: 120_000 + waits * 1000 + typing * 25,
|
|
1120
|
+
...(signal ? { signal } : {}),
|
|
1121
|
+
}));
|
|
1122
|
+
},
|
|
1123
|
+
/** Starts the viewer in the guest; expose the returned port and open a link to `path` (Workspace.computer.stream does both). */
|
|
1124
|
+
streamStart: (req = {}) => {
|
|
1125
|
+
const refusal = this.#needsVm('computer.stream');
|
|
1126
|
+
if (refusal)
|
|
1127
|
+
return Promise.reject(refusal);
|
|
1128
|
+
return this.#computer(() => this.#json('POST', this.#p('/v1/workspaces/{workspace_id}/computer/stream'), { json: req, timeoutMs: 90_000 }));
|
|
1129
|
+
},
|
|
1130
|
+
streamStop: async () => {
|
|
1131
|
+
const refusal = this.#needsVm('computer.stream_stop');
|
|
1132
|
+
if (refusal)
|
|
1133
|
+
throw refusal;
|
|
1134
|
+
await this.#computer(() => this.#json('DELETE', this.#p('/v1/workspaces/{workspace_id}/computer/stream')));
|
|
1135
|
+
},
|
|
1136
|
+
};
|
|
1066
1137
|
// ---- browser ---------------------------------------------------------------------------
|
|
1067
1138
|
browser = {
|
|
1068
1139
|
screenshot: (req) => {
|
package/dist/client.d.ts
CHANGED
|
@@ -14,8 +14,11 @@ import { HttpClient } from './http.js';
|
|
|
14
14
|
import type { RequestOptions } from './http.js';
|
|
15
15
|
import type { ToolName, ToolToken } from './tokens.js';
|
|
16
16
|
import { Workspace } from './workspace.js';
|
|
17
|
+
import { WorkspaceRef } from './workspace-ref.js';
|
|
18
|
+
import type { WorkspaceRefParams } from './workspace-ref.js';
|
|
17
19
|
import { AuditApi } from './audit.js';
|
|
18
20
|
import { EgressPolicyApi } from './egress.js';
|
|
21
|
+
import { WorkspacePorts } from './ports.js';
|
|
19
22
|
import { SecretsApi } from './secrets.js';
|
|
20
23
|
import { TemplatesApi } from './templates.js';
|
|
21
24
|
import type { SaveAsTemplateParams, SaveAsTemplateResponse } from './templates.js';
|
|
@@ -27,6 +30,8 @@ import type { ProgressListener } from './progress.js';
|
|
|
27
30
|
import { CaptureRegistry } from './capture.js';
|
|
28
31
|
import type { FeedbackReceipt, SendFeedbackParams } from './feedback.js';
|
|
29
32
|
export type WorkspaceView = components['schemas']['Workspace'];
|
|
33
|
+
/** Contracts §45.1 (0.15.0+): the workspace's computer use switch. */
|
|
34
|
+
export type ComputerUse = components['schemas']['ComputerUse'];
|
|
30
35
|
export type Operation = components['schemas']['Operation'];
|
|
31
36
|
/** persistent (kept until deleted) or session (discarded when the session ends: close(), idle timeout). */
|
|
32
37
|
export type WorkspaceLifetime = components['schemas']['WorkspaceLifetime'];
|
|
@@ -115,9 +120,9 @@ export interface SuspendWhenIdleResult {
|
|
|
115
120
|
workspace: Workspace;
|
|
116
121
|
}
|
|
117
122
|
export interface ShardfluxOptions {
|
|
118
|
-
/** Project API key: sfk_<key_id>_<secret>. */
|
|
119
|
-
apiKey
|
|
120
|
-
/** Default https://api.shardflux.dev
|
|
123
|
+
/** Project API key: sfk_<key_id>_<secret>. Default (0.15.0+): the `SHARDFLUX_API_KEY` environment variable. */
|
|
124
|
+
apiKey?: string;
|
|
125
|
+
/** Default (0.15.0+): `SHARDFLUX_API_URL`, else https://api.shardflux.dev. */
|
|
121
126
|
baseUrl?: string;
|
|
122
127
|
/** Default: pooled HTTP/1.1 on Node 26+, native fetch on other runtimes (see defaultFetch in http.ts). */
|
|
123
128
|
fetch?: typeof fetch;
|
|
@@ -158,8 +163,9 @@ export interface Caps {
|
|
|
158
163
|
memory_mib?: number;
|
|
159
164
|
disk_gib?: number;
|
|
160
165
|
/**
|
|
161
|
-
* `fixed`
|
|
162
|
-
* the
|
|
166
|
+
* `fixed` or `elastic` (0.13.0). Omitted: fixed, or elastic where that is the organization's default (a reopen then
|
|
167
|
+
* keeps the stored mode); the SDK never fills it in. Given caps replace the stored ones; omitted caps keep the stored
|
|
168
|
+
* layout. `workspace.resize()` (0.14.0+) changes it on a running or suspended workspace. Elastic needs the organization's entitlement, else
|
|
163
169
|
* ShardfluxApiError 422 `validation_failed` reason `allocation_mode_not_available` (nothing is created or changed);
|
|
164
170
|
* a file-first workspace gets `not_supported_for_mode`. A promise that does not exceed the held floor by at least
|
|
165
171
|
* 512 MiB is fixed at the promise (the returned `caps.allocation_mode` says which). Takes effect at the next VM start.
|
|
@@ -176,6 +182,64 @@ export interface ForkTarget {
|
|
|
176
182
|
caps?: Caps;
|
|
177
183
|
lifetime?: WorkspaceLifetime;
|
|
178
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* `workspace.resize()` / `workspaces.resize()` (0.14.0): the caps to change. Give at least one; the others keep their
|
|
187
|
+
* stored values. Each value is bounded by the plan's per-workspace maximum and the template's limit, as caps at open.
|
|
188
|
+
*/
|
|
189
|
+
export interface ResizeParams {
|
|
190
|
+
/** Memory in MiB: the size of a fixed workspace, the maximum an elastic one may grow to. */
|
|
191
|
+
memoryMib?: number;
|
|
192
|
+
/** Elastic only: the memory the workspace holds while idle, in MiB (at most the resulting `memoryMib`). */
|
|
193
|
+
memoryMibHeld?: number;
|
|
194
|
+
/** `fixed` or `elastic`: switches the mode, live on a running workspace. */
|
|
195
|
+
allocationMode?: AllocationMode;
|
|
196
|
+
/** CPU in millicores (1000 = one vCPU). */
|
|
197
|
+
cpuMillis?: number;
|
|
198
|
+
/** Disk in GiB; disks grow only. */
|
|
199
|
+
diskGib?: number;
|
|
200
|
+
/** Replays the stored response for a repeated request (default: a fresh key per call, so transport retries replay). */
|
|
201
|
+
idempotencyKey?: string;
|
|
202
|
+
/** Give up waiting for the resize after this long (default 300 000 ms); it continues server side. */
|
|
203
|
+
timeoutMs?: number;
|
|
204
|
+
signal?: AbortSignal;
|
|
205
|
+
/** Progress of the resize (the held request, the operation's states, retries, `done` with the timing). */
|
|
206
|
+
onProgress?: ProgressListener;
|
|
207
|
+
}
|
|
208
|
+
/** The held answer of `PATCH /v1/workspaces/{id}/caps` (and a finished resize operation's `result`). */
|
|
209
|
+
export type ResizeResponse = components['schemas']['ResizeResult'];
|
|
210
|
+
/** 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. */
|
|
211
|
+
export type ResizeMemory = NonNullable<ResizeResponse['memory']>;
|
|
212
|
+
/** The CPU part of a resize result (0.14.0), in millicores. */
|
|
213
|
+
export type ResizeCpu = NonNullable<ResizeResponse['cpu']>;
|
|
214
|
+
/** The disk part of a resize result (0.14.0), in GiB. */
|
|
215
|
+
export type ResizeDisk = NonNullable<ResizeResponse['disk']>;
|
|
216
|
+
/** When a resized resource takes effect (0.14.0): `now`, when the suspended workspace resumes, or at its next start. */
|
|
217
|
+
export type ResizeAppliesAt = ResizeMemory['applies_at'];
|
|
218
|
+
/** Why less than requested applied: clamped to the plan or template bound, the host had no room, or the guest kept memory it uses. */
|
|
219
|
+
export type ResizeLimitReason = NonNullable<ResizeMemory['limit_reason']>;
|
|
220
|
+
/**
|
|
221
|
+
* Why a resource applies later than now: `suspended` (at resume), `stopped` (at the next start), `boot_vcpus` (more CPU
|
|
222
|
+
* than the vCPUs the workspace booted with), `region` / `below_base` / `legacy_layout` (a memory size its running VM was
|
|
223
|
+
* not booted for).
|
|
224
|
+
*/
|
|
225
|
+
export type ResizeDeferReason = NonNullable<ResizeMemory['reason']>;
|
|
226
|
+
/**
|
|
227
|
+
* What a resize did (0.14.0), per resource the request named (the others are null): when it applies (`applies_at`),
|
|
228
|
+
* what was asked, the bounded target, the size before, what is live now, and why less or later.
|
|
229
|
+
*/
|
|
230
|
+
export interface ResizeResult {
|
|
231
|
+
workspaceId: string;
|
|
232
|
+
operationId: string | null;
|
|
233
|
+
/** The workspace's state the result describes: `running`, `suspended` or `stopped`. */
|
|
234
|
+
state: string;
|
|
235
|
+
memory: ResizeMemory | null;
|
|
236
|
+
cpu: ResizeCpu | null;
|
|
237
|
+
disk: ResizeDisk | null;
|
|
238
|
+
/** The workspace's stored caps after the change: every later start uses them. */
|
|
239
|
+
caps: ResizeResponse['caps'] | null;
|
|
240
|
+
/** The finished `resize` operation when the API answered before it was done (202), else null. */
|
|
241
|
+
operation: Operation | null;
|
|
242
|
+
}
|
|
179
243
|
export interface WaitOptions {
|
|
180
244
|
/**
|
|
181
245
|
* Give up waiting after this long (default 300 000 ms); the operation continues server side. A queued start
|
|
@@ -200,6 +264,12 @@ export interface OpenParams {
|
|
|
200
264
|
/** Searchable metadata; supplied labels replace the existing map. */
|
|
201
265
|
labels?: Record<string, string>;
|
|
202
266
|
idlePolicy?: IdlePolicy;
|
|
267
|
+
/**
|
|
268
|
+
* Computer use (0.15.0+, contracts §45.1): true or false sets the workspace's own switch, null follows the template;
|
|
269
|
+
* omitted leaves it unchanged. While it is on, tool tokens carry the `computer` tool and `workspace.computer` drives
|
|
270
|
+
* the workspace desktop. 409 `computer_use_unavailable` when the template version cannot run a desktop.
|
|
271
|
+
*/
|
|
272
|
+
computerUse?: boolean | null;
|
|
203
273
|
key: string;
|
|
204
274
|
template: string;
|
|
205
275
|
caps?: Caps;
|
|
@@ -338,9 +408,13 @@ export declare class WorkspacesApi {
|
|
|
338
408
|
agentLabel?: string;
|
|
339
409
|
tools?: ToolName[];
|
|
340
410
|
}): Promise<Workspace>;
|
|
411
|
+
/** The exposed ports of a workspace by id (0.14.0; the same as `workspace.ports` without reading the workspace first). */
|
|
412
|
+
ports(workspaceId: string): WorkspacePorts;
|
|
341
413
|
/** Replace labels. An empty map clears them. */
|
|
342
414
|
setLabels(workspaceId: string, labels: Record<string, string>): Promise<Workspace>;
|
|
343
415
|
/** null clears the override, restoring the template or platform policy. */
|
|
416
|
+
/** Computer use (0.15.0+): the workspace's own switch; null follows the template. Returns the switch. */
|
|
417
|
+
setComputerUse(workspaceId: string, enabled: boolean | null): Promise<ComputerUse>;
|
|
344
418
|
setIdlePolicy(workspaceId: string, idlePolicy: IdlePolicy | null): Promise<Workspace>;
|
|
345
419
|
list(params?: ListParams): Promise<Page<Workspace>>;
|
|
346
420
|
/** Iterates every page. */
|
|
@@ -412,6 +486,28 @@ export declare class WorkspacesApi {
|
|
|
412
486
|
* is not undone: it is the workspace's `activeOperation` (resume or open the workspace instead).
|
|
413
487
|
*/
|
|
414
488
|
cancelSuspendWhenIdle(workspaceId: string): Promise<Workspace>;
|
|
489
|
+
/**
|
|
490
|
+
* Resizes a workspace (0.14.0; `PATCH /v1/workspaces/{id}/caps`): any of memory, the held floor, the allocation mode,
|
|
491
|
+
* CPU and disk, of a running, suspended or stopped workspace, fixed or elastic, without a restart or a fork. The new
|
|
492
|
+
* caps are stored, so every later start uses them. Resolves once the resize has finished, with what applied per
|
|
493
|
+
* resource: `now` (live), `resume` (a suspended workspace gets it when it resumes, before its first call) or
|
|
494
|
+
* `next_start` (with the `reason`). The request is held by the server while the resize runs; a resize still running
|
|
495
|
+
* after the hold is waited for like any operation.
|
|
496
|
+
*
|
|
497
|
+
* A resize that meets another lifecycle operation (409 `operation_in_progress`: a suspend, a resume, another resize)
|
|
498
|
+
* waits for it and is sent again, within `timeoutMs`.
|
|
499
|
+
*
|
|
500
|
+
* Errors: ShardfluxApiError 409 `conflict` (reason `resize_not_available`, `workspace_deleted`), 422
|
|
501
|
+
* `validation_failed` (reason `shrink_not_supported` with `details.current_disk_gib`, `requires_elastic`,
|
|
502
|
+
* `exceeds_memory_mib`, `allocation_mode_not_available`, `not_supported_for_mode`). A resize that fails is its
|
|
503
|
+
* error: ShardfluxApiError with `operationId` when the server held the request until then, OperationFailedError when
|
|
504
|
+
* the SDK waited for the operation. OperationTimeoutError after `timeoutMs`. Params without a resource throw
|
|
505
|
+
* TypeError before any request.
|
|
506
|
+
*
|
|
507
|
+
* const r = await cloud.workspaces.resize(id, { memoryMib: 6144 });
|
|
508
|
+
* r.memory?.applies_at; // 'now'
|
|
509
|
+
*/
|
|
510
|
+
resize(workspaceId: string, params: ResizeParams): Promise<ResizeResult>;
|
|
415
511
|
/**
|
|
416
512
|
* Ends a session workspace now: the workspace is deleted exactly like delete() (ended_reason
|
|
417
513
|
* closed) and returns the `delete` operation (input.reason session_closed); the key then opens a NEW workspace.
|
|
@@ -501,7 +597,14 @@ export declare class Shardflux {
|
|
|
501
597
|
readonly audit: AuditApi;
|
|
502
598
|
/** Shared volumes: persistent storage attached to workspaces at a mount path. */
|
|
503
599
|
readonly volumes: VolumesApi;
|
|
504
|
-
|
|
600
|
+
/** Reads `SHARDFLUX_API_KEY` and `SHARDFLUX_API_URL` (0.15.0+) for an `apiKey` or `baseUrl` that is not passed. */
|
|
601
|
+
constructor(opts?: ShardfluxOptions);
|
|
602
|
+
/**
|
|
603
|
+
* A workspace named by its key (0.15.0+, contracts §46): no request until its first call, which opens the key
|
|
604
|
+
* (created on first use with `params.template`, resumed afterwards). `workspace(key, { template: 'default' })
|
|
605
|
+
* .exec('...')` is a whole integration; see `WorkspaceRef`.
|
|
606
|
+
*/
|
|
607
|
+
workspace(key: string, params: WorkspaceRefParams): WorkspaceRef;
|
|
505
608
|
/** The authenticated principal (the API key, its organization and project). */
|
|
506
609
|
me(): Promise<Me>;
|
|
507
610
|
entitlements(organizationId: string): Promise<Entitlements>;
|
|
@@ -517,4 +620,10 @@ export declare class Shardflux {
|
|
|
517
620
|
/** Raw access to any /v1 endpoint with the SDK's authentication and error handling. */
|
|
518
621
|
request<T>(method: string, path: string, init?: Parameters<HttpClient['json']>[2]): Promise<T>;
|
|
519
622
|
}
|
|
623
|
+
/**
|
|
624
|
+
* A workspace named by its key, on a client from the environment (0.15.0+): `new Shardflux()` reads
|
|
625
|
+
* `SHARDFLUX_API_KEY` (and `SHARDFLUX_API_URL`) on the first call. `workspace(key, { template: 'default' }).exec('...')`
|
|
626
|
+
* creates the workspace on first use and resumes it afterwards. Use `cloud.workspace()` for a client of your own.
|
|
627
|
+
*/
|
|
628
|
+
export declare function workspace(key: string, params: WorkspaceRefParams): WorkspaceRef;
|
|
520
629
|
export { ShardfluxApiError };
|
package/dist/client.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { DurabilityLostError, OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError } from "./errors.js";
|
|
2
2
|
import { HttpClient, SDK_VERSION, SERVER_WAIT_MAX_S, defaultFetch, defaultSleep, pollWithWait, randomId } from "./http.js";
|
|
3
3
|
import { Workspace } from "./workspace.js";
|
|
4
|
+
import { WorkspaceRef } from "./workspace-ref.js";
|
|
4
5
|
import { AuditApi } from "./audit.js";
|
|
5
6
|
import { EgressPolicyApi } from "./egress.js";
|
|
7
|
+
import { WorkspacePorts } from "./ports.js";
|
|
6
8
|
import { SecretsApi } from "./secrets.js";
|
|
7
9
|
import { TemplatesApi, saveAsTemplateBody } from "./templates.js";
|
|
8
10
|
import { UsageApi } from "./usage.js";
|
|
@@ -46,6 +48,70 @@ function alreadyRunning(answer) {
|
|
|
46
48
|
},
|
|
47
49
|
}, 'api');
|
|
48
50
|
}
|
|
51
|
+
/** The `PATCH .../caps` body of a resize: the given caps only, snake_case. Throws TypeError when none is given. */
|
|
52
|
+
function resizeBody(p) {
|
|
53
|
+
const body = {};
|
|
54
|
+
if (p.memoryMib !== undefined)
|
|
55
|
+
body.memory_mib = p.memoryMib;
|
|
56
|
+
if (p.memoryMibHeld !== undefined)
|
|
57
|
+
body.memory_mib_held = p.memoryMibHeld;
|
|
58
|
+
if (p.allocationMode !== undefined)
|
|
59
|
+
body.allocation_mode = p.allocationMode;
|
|
60
|
+
if (p.cpuMillis !== undefined)
|
|
61
|
+
body.cpu_millis = p.cpuMillis;
|
|
62
|
+
if (p.diskGib !== undefined)
|
|
63
|
+
body.disk_gib = p.diskGib;
|
|
64
|
+
if (Object.keys(body).length === 0)
|
|
65
|
+
throw new TypeError('resize() needs at least one of memoryMib, memoryMibHeld, allocationMode, cpuMillis, diskGib');
|
|
66
|
+
return body;
|
|
67
|
+
}
|
|
68
|
+
const RESIZE_KEYS = {
|
|
69
|
+
memory: ['applies_at', 'allocation_mode', 'requested_mib', 'target_mib', 'previous_mib', 'applied_mib', 'held_mib', 'converged', 'limit_reason', 'reason'],
|
|
70
|
+
cpu: ['applies_at', 'requested_millis', 'target_millis', 'previous_millis', 'applied_millis', 'limit_reason', 'reason'],
|
|
71
|
+
disk: ['applies_at', 'requested_gib', 'target_gib', 'previous_gib', 'applied_gib', 'limit_reason', 'reason'],
|
|
72
|
+
};
|
|
73
|
+
const record = (v) => (v !== null && typeof v === 'object' && !Array.isArray(v) ? v : {});
|
|
74
|
+
/**
|
|
75
|
+
* A finished resize operation's `result` in the held answer's shape (as the API builds its 200): each resource the
|
|
76
|
+
* cell reported with its documented keys, a key the cell left out taken from the operation's input (target, requested)
|
|
77
|
+
* or null; the cell's other keys (timings, the previous values of a retried attempt) are left out.
|
|
78
|
+
*/
|
|
79
|
+
function resizeResultBodyOf(op) {
|
|
80
|
+
const result = record(op.result);
|
|
81
|
+
const input = record(op.input);
|
|
82
|
+
const target = record(input.target);
|
|
83
|
+
const requested = record(input.requested);
|
|
84
|
+
const fallback = {
|
|
85
|
+
memory: { allocation_mode: target.allocation_mode, requested_mib: requested.memory_mib, target_mib: target.memory_mib, held_mib: target.memory_mib_held },
|
|
86
|
+
cpu: { requested_millis: requested.cpu_millis, target_millis: target.cpu_millis },
|
|
87
|
+
disk: { requested_gib: requested.disk_gib, target_gib: target.disk_gib },
|
|
88
|
+
};
|
|
89
|
+
const out = { workspace_id: result.workspace_id ?? op.workspace_id, operation_id: op.id, state: result.state };
|
|
90
|
+
for (const [name, keys] of Object.entries(RESIZE_KEYS)) {
|
|
91
|
+
if (result[name] === null || typeof result[name] !== 'object')
|
|
92
|
+
continue;
|
|
93
|
+
const r = record(result[name]);
|
|
94
|
+
out[name] = Object.fromEntries(keys.map((k) => [k, r[k] ?? fallback[name]?.[k] ?? null]));
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
/** A resize result body (the held 200, or a finished operation's `result`) as ResizeResult. */
|
|
99
|
+
function resizeResultOf(body, status, operation, workspaceId) {
|
|
100
|
+
if (!body || typeof body !== 'object' || !['memory', 'cpu', 'disk'].some((k) => body[k] && typeof body[k] === 'object')) {
|
|
101
|
+
throw new ShardfluxProtocolError('resize: the result names no resource (memory, cpu or disk)', status, 'api');
|
|
102
|
+
}
|
|
103
|
+
const part = (k) => (body[k] && typeof body[k] === 'object' ? body[k] : null);
|
|
104
|
+
return {
|
|
105
|
+
workspaceId: typeof body.workspace_id === 'string' ? body.workspace_id : workspaceId,
|
|
106
|
+
operationId: typeof body.operation_id === 'string' ? body.operation_id : (operation?.id ?? null),
|
|
107
|
+
state: typeof body.state === 'string' ? body.state : 'unknown',
|
|
108
|
+
memory: part('memory'),
|
|
109
|
+
cpu: part('cpu'),
|
|
110
|
+
disk: part('disk'),
|
|
111
|
+
caps: body.caps && typeof body.caps === 'object' ? body.caps : null,
|
|
112
|
+
operation,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
49
115
|
/** Races the injected sleep against the signal (the injected sleep itself may not be abortable). */
|
|
50
116
|
function abortableSleep(sleep, ms, signal) {
|
|
51
117
|
if (signal.aborted)
|
|
@@ -100,6 +166,8 @@ export class WorkspacesApi {
|
|
|
100
166
|
body.labels = params.labels;
|
|
101
167
|
if (params.idlePolicy !== undefined)
|
|
102
168
|
body.idle_policy = params.idlePolicy;
|
|
169
|
+
if (params.computerUse !== undefined)
|
|
170
|
+
body.computer_use = params.computerUse;
|
|
103
171
|
if (params.lifetime !== undefined)
|
|
104
172
|
body.lifetime = params.lifetime;
|
|
105
173
|
if (params.mode !== undefined)
|
|
@@ -128,7 +196,9 @@ export class WorkspacesApi {
|
|
|
128
196
|
if (res.body.operation)
|
|
129
197
|
trace.observe(res.body.operation);
|
|
130
198
|
this.#noteToken(res.body.tool_token);
|
|
131
|
-
|
|
199
|
+
// An API before contracts §46.2 does not say whether the open created the workspace.
|
|
200
|
+
const created = res.body.created ?? null;
|
|
201
|
+
const wrapOpts = { agentLabel: params.agentLabel, tools: params.tools, token: res.body.tool_token, trace, created };
|
|
132
202
|
if (res.status === 200 || params.wait === false || res.body.operation === null)
|
|
133
203
|
return this.#wrap(res.body.workspace, wrapOpts);
|
|
134
204
|
if (TERMINAL.has(res.body.operation.state)) {
|
|
@@ -316,11 +386,19 @@ export class WorkspacesApi {
|
|
|
316
386
|
async get(workspaceId, opts = {}) {
|
|
317
387
|
return this.#wrap(await this.#getView(workspaceId), opts);
|
|
318
388
|
}
|
|
389
|
+
/** The exposed ports of a workspace by id (0.14.0; the same as `workspace.ports` without reading the workspace first). */
|
|
390
|
+
ports(workspaceId) {
|
|
391
|
+
return new WorkspacePorts(this.#ctx(), workspaceId);
|
|
392
|
+
}
|
|
319
393
|
/** Replace labels. An empty map clears them. */
|
|
320
394
|
async setLabels(workspaceId, labels) {
|
|
321
395
|
return this.#wrap(await this.#http.json('PUT', `/v1/workspaces/${encodeURIComponent(workspaceId)}/labels`, { json: { labels } }, this.#auth));
|
|
322
396
|
}
|
|
323
397
|
/** null clears the override, restoring the template or platform policy. */
|
|
398
|
+
/** Computer use (0.15.0+): the workspace's own switch; null follows the template. Returns the switch. */
|
|
399
|
+
async setComputerUse(workspaceId, enabled) {
|
|
400
|
+
return this.#http.json('PUT', `/v1/workspaces/${encodeURIComponent(workspaceId)}/computer-use`, { json: { enabled } }, this.#auth);
|
|
401
|
+
}
|
|
324
402
|
async setIdlePolicy(workspaceId, idlePolicy) {
|
|
325
403
|
return this.#wrap(await this.#http.json('PUT', `/v1/workspaces/${encodeURIComponent(workspaceId)}/idle-policy`, { json: { idle_policy: idlePolicy } }, this.#auth));
|
|
326
404
|
}
|
|
@@ -514,6 +592,85 @@ export class WorkspacesApi {
|
|
|
514
592
|
async cancelSuspendWhenIdle(workspaceId) {
|
|
515
593
|
return this.#wrap(await this.#http.json('DELETE', `/v1/workspaces/${encodeURIComponent(workspaceId)}/suspend-when-idle`, {}, this.#auth));
|
|
516
594
|
}
|
|
595
|
+
/**
|
|
596
|
+
* Resizes a workspace (0.14.0; `PATCH /v1/workspaces/{id}/caps`): any of memory, the held floor, the allocation mode,
|
|
597
|
+
* CPU and disk, of a running, suspended or stopped workspace, fixed or elastic, without a restart or a fork. The new
|
|
598
|
+
* caps are stored, so every later start uses them. Resolves once the resize has finished, with what applied per
|
|
599
|
+
* resource: `now` (live), `resume` (a suspended workspace gets it when it resumes, before its first call) or
|
|
600
|
+
* `next_start` (with the `reason`). The request is held by the server while the resize runs; a resize still running
|
|
601
|
+
* after the hold is waited for like any operation.
|
|
602
|
+
*
|
|
603
|
+
* A resize that meets another lifecycle operation (409 `operation_in_progress`: a suspend, a resume, another resize)
|
|
604
|
+
* waits for it and is sent again, within `timeoutMs`.
|
|
605
|
+
*
|
|
606
|
+
* Errors: ShardfluxApiError 409 `conflict` (reason `resize_not_available`, `workspace_deleted`), 422
|
|
607
|
+
* `validation_failed` (reason `shrink_not_supported` with `details.current_disk_gib`, `requires_elastic`,
|
|
608
|
+
* `exceeds_memory_mib`, `allocation_mode_not_available`, `not_supported_for_mode`). A resize that fails is its
|
|
609
|
+
* error: ShardfluxApiError with `operationId` when the server held the request until then, OperationFailedError when
|
|
610
|
+
* the SDK waited for the operation. OperationTimeoutError after `timeoutMs`. Params without a resource throw
|
|
611
|
+
* TypeError before any request.
|
|
612
|
+
*
|
|
613
|
+
* const r = await cloud.workspaces.resize(id, { memoryMib: 6144 });
|
|
614
|
+
* r.memory?.applies_at; // 'now'
|
|
615
|
+
*/
|
|
616
|
+
async resize(workspaceId, params) {
|
|
617
|
+
const body = resizeBody(params);
|
|
618
|
+
const trace = new Trace('resize', combineListeners(this.#ctx().onProgress, params.onProgress), { workspaceId });
|
|
619
|
+
return traced(trace, async () => {
|
|
620
|
+
const timeoutMs = params.timeoutMs ?? 300_000;
|
|
621
|
+
const started = Date.now();
|
|
622
|
+
const left = () => Math.max(1, timeoutMs - (Date.now() - started));
|
|
623
|
+
const idempotencyKey = params.idempotencyKey ?? randomId('op-');
|
|
624
|
+
const path = `/v1/workspaces/${encodeURIComponent(workspaceId)}/caps`;
|
|
625
|
+
let res;
|
|
626
|
+
for (let attempt = 0;; attempt += 1) {
|
|
627
|
+
const waitS = Math.min(SERVER_WAIT_MAX_S, Math.floor((attempt === 0 ? timeoutMs : left()) / 1000));
|
|
628
|
+
const init = { json: body, idempotencyKey, onRetry: trace.onRetry, ...(params.signal ? { signal: params.signal } : {}) };
|
|
629
|
+
if (waitS >= 1) {
|
|
630
|
+
// Held: the server answers once the resize has finished (200) or the wait elapsed (202).
|
|
631
|
+
init.headers = { prefer: `wait=${waitS}` };
|
|
632
|
+
init.timeoutMs = Math.max(this.#http.opts.timeoutMs, waitS * 1000 + 10_000);
|
|
633
|
+
}
|
|
634
|
+
trace.phase('request', waitS >= 1 ? 'held' : attempt === 0 ? null : 'retry');
|
|
635
|
+
try {
|
|
636
|
+
res = await this.#http.jsonWithStatus('PATCH', path, init, this.#auth);
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
catch (e) {
|
|
640
|
+
// Another lifecycle operation (a suspend, a resume, another resize) holds the workspace: the refused request
|
|
641
|
+
// changed nothing, so wait for that operation and send it again (bounded, within timeoutMs).
|
|
642
|
+
const active = e instanceof ShardfluxApiError && e.code === 'conflict' && e.reason === 'operation_in_progress' ? (e.operationId ?? e.details?.['active_operation_id']) : undefined;
|
|
643
|
+
if (typeof active !== 'string' || attempt >= 3 || Date.now() - started >= timeoutMs)
|
|
644
|
+
throw e;
|
|
645
|
+
trace.retry({ request: `PATCH ${path}`, attempt: attempt + 1, cause: `conflict operation_in_progress (waits for ${active})`, delayMs: 0 });
|
|
646
|
+
try {
|
|
647
|
+
await this.waitForOperation(active, { timeoutMs: left(), ...(params.signal ? { signal: params.signal } : {}), [TRACE]: trace });
|
|
648
|
+
}
|
|
649
|
+
catch (w) {
|
|
650
|
+
if (!(w instanceof OperationFailedError))
|
|
651
|
+
throw w;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (res.status !== 202)
|
|
656
|
+
return resizeResultOf(res.body, res.status, null, workspaceId);
|
|
657
|
+
const operation = res.body?.operation;
|
|
658
|
+
if (!operation || typeof operation !== 'object')
|
|
659
|
+
throw new ShardfluxProtocolError('resize: 202 response has no operation', res.status, 'api');
|
|
660
|
+
trace.observe(operation);
|
|
661
|
+
let final = operation;
|
|
662
|
+
if (operation.state !== 'succeeded') {
|
|
663
|
+
if (TERMINAL.has(operation.state))
|
|
664
|
+
throw new OperationFailedError(operation);
|
|
665
|
+
const wait = { timeoutMs: Math.max(1, timeoutMs - (Date.now() - started)), ...(params.signal ? { signal: params.signal } : {}), [TRACE]: trace };
|
|
666
|
+
final = await this.waitForOperation(operation.id, wait);
|
|
667
|
+
}
|
|
668
|
+
// The finished operation's result is the cell's: shaped like the held answer, whose caps come from the view.
|
|
669
|
+
const result = resizeResultBodyOf(final);
|
|
670
|
+
const view = await trace.span('view', () => this.#getView(workspaceId, trace.onRetry));
|
|
671
|
+
return resizeResultOf({ ...result, state: result.state ?? view.observed_state, caps: view.caps }, res.status, final, workspaceId);
|
|
672
|
+
});
|
|
673
|
+
}
|
|
517
674
|
async close(workspaceId, opts = {}) {
|
|
518
675
|
return (await this.closeWithView(workspaceId, opts)).operation;
|
|
519
676
|
}
|
|
@@ -645,8 +802,13 @@ export class Shardflux {
|
|
|
645
802
|
/** Shared volumes: persistent storage attached to workspaces at a mount path. */
|
|
646
803
|
volumes;
|
|
647
804
|
#ctx;
|
|
648
|
-
|
|
649
|
-
|
|
805
|
+
#toolGrants = new GrantsCache(() => this.me());
|
|
806
|
+
/** Reads `SHARDFLUX_API_KEY` and `SHARDFLUX_API_URL` (0.15.0+) for an `apiKey` or `baseUrl` that is not passed. */
|
|
807
|
+
constructor(opts = {}) {
|
|
808
|
+
const apiKey = opts.apiKey ?? envVar('SHARDFLUX_API_KEY');
|
|
809
|
+
if (apiKey === undefined)
|
|
810
|
+
throw new Error('Missing API key: pass apiKey or set SHARDFLUX_API_KEY (a project key, sfk_<key_id>_<secret>)');
|
|
811
|
+
if (!/^sfk_[a-z2-7]{16}_[A-Za-z0-9]+$/.test(apiKey))
|
|
650
812
|
throw new Error('apiKey must be a Shardflux project key (sfk_<key_id>_<secret>)');
|
|
651
813
|
const f = opts.fetch ?? defaultFetch();
|
|
652
814
|
const userAgent = opts.userAgent ?? `shardflux-sdk-ts/${SDK_VERSION}`;
|
|
@@ -659,10 +821,10 @@ export class Shardflux {
|
|
|
659
821
|
this.egress = new EgressPolicyApi(() => this.#ctx);
|
|
660
822
|
this.audit = new AuditApi(() => this.#ctx);
|
|
661
823
|
this.volumes = new VolumesApi(() => this.#ctx);
|
|
662
|
-
const baseUrl = opts.baseUrl ?? 'https://api.shardflux.dev';
|
|
824
|
+
const baseUrl = opts.baseUrl ?? envVar('SHARDFLUX_API_URL') ?? 'https://api.shardflux.dev';
|
|
663
825
|
this.#ctx = {
|
|
664
826
|
http: new HttpClient({ baseUrl, fetch: f, userAgent, timeoutMs: opts.timeoutMs ?? 30_000, maxRetries: opts.maxRetries ?? 2, source: 'api', sleep, onSuccess: versionCheckHook(opts.versionCheck, baseUrl, f, userAgent) }),
|
|
665
|
-
authorization: `Bearer ${
|
|
827
|
+
authorization: `Bearer ${apiKey}`,
|
|
666
828
|
fetch: f,
|
|
667
829
|
userAgent,
|
|
668
830
|
sleep,
|
|
@@ -671,6 +833,14 @@ export class Shardflux {
|
|
|
671
833
|
captures: new CaptureRegistry(),
|
|
672
834
|
};
|
|
673
835
|
}
|
|
836
|
+
/**
|
|
837
|
+
* A workspace named by its key (0.15.0+, contracts §46): no request until its first call, which opens the key
|
|
838
|
+
* (created on first use with `params.template`, resumed afterwards). `workspace(key, { template: 'default' })
|
|
839
|
+
* .exec('...')` is a whole integration; see `WorkspaceRef`.
|
|
840
|
+
*/
|
|
841
|
+
workspace(key, params) {
|
|
842
|
+
return new WorkspaceRef(this.workspaces, key, params, () => this.#toolGrants.get());
|
|
843
|
+
}
|
|
674
844
|
/** The authenticated principal (the API key, its organization and project). */
|
|
675
845
|
me() {
|
|
676
846
|
return this.#ctx.http.json('GET', '/v1/me', {}, this.#ctx.authorization);
|
|
@@ -694,4 +864,34 @@ export class Shardflux {
|
|
|
694
864
|
return this.#ctx.http.json(method, path, init, this.#ctx.authorization);
|
|
695
865
|
}
|
|
696
866
|
}
|
|
867
|
+
/** The client of the module-level `workspace()`: created on first use from the environment. */
|
|
868
|
+
let defaultClient = null;
|
|
869
|
+
/**
|
|
870
|
+
* A workspace named by its key, on a client from the environment (0.15.0+): `new Shardflux()` reads
|
|
871
|
+
* `SHARDFLUX_API_KEY` (and `SHARDFLUX_API_URL`) on the first call. `workspace(key, { template: 'default' }).exec('...')`
|
|
872
|
+
* creates the workspace on first use and resumes it afterwards. Use `cloud.workspace()` for a client of your own.
|
|
873
|
+
*/
|
|
874
|
+
export function workspace(key, params) {
|
|
875
|
+
defaultClient ??= new Shardflux();
|
|
876
|
+
return defaultClient.workspace(key, params);
|
|
877
|
+
}
|
|
878
|
+
/** An environment variable, trimmed; undefined when unset, empty or outside Node-like runtimes. */
|
|
879
|
+
function envVar(name) {
|
|
880
|
+
return globalThis.process?.env?.[name]?.trim() || undefined;
|
|
881
|
+
}
|
|
882
|
+
/** The API key's tool permissions (GET /v1/me), read once; a failed read is not kept. Null for a non-key principal. */
|
|
883
|
+
class GrantsCache {
|
|
884
|
+
#load;
|
|
885
|
+
#value = null;
|
|
886
|
+
constructor(load) {
|
|
887
|
+
this.#load = load;
|
|
888
|
+
}
|
|
889
|
+
get() {
|
|
890
|
+
this.#value ??= this.#load().then((me) => (me.api_key ? [...me.api_key.tool_permissions] : null), (err) => {
|
|
891
|
+
this.#value = null;
|
|
892
|
+
throw err;
|
|
893
|
+
});
|
|
894
|
+
return this.#value;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
697
897
|
export { ShardfluxApiError };
|