@shardflux/sdk 0.14.0 → 0.16.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 +85 -23
- package/README.md +226 -7
- package/dist/account.d.ts +2 -0
- package/dist/account.js +6 -0
- package/dist/cell.d.ts +27 -0
- package/dist/cell.js +71 -0
- package/dist/client.d.ts +54 -5
- package/dist/client.js +100 -7
- package/dist/computer.d.ts +153 -0
- package/dist/computer.js +229 -0
- package/dist/errors.d.ts +5 -1
- package/dist/errors.js +9 -0
- package/dist/executions.d.ts +2 -6
- package/dist/executions.js +9 -0
- package/dist/exit-code.d.ts +7 -0
- package/dist/exit-code.js +12 -0
- package/dist/generated/app-api.d.ts +1055 -119
- package/dist/generated/cell-api.d.ts +334 -0
- package/dist/http.d.ts +7 -1
- package/dist/http.js +34 -13
- package/dist/index.d.ts +13 -6
- package/dist/index.js +7 -2
- package/dist/ports.d.ts +7 -0
- package/dist/ports.js +1 -1
- package/dist/progress.d.ts +2 -2
- package/dist/progress.js +1 -1
- package/dist/templates.d.ts +12 -0
- package/dist/templates.js +9 -0
- package/dist/testing/index.d.ts +62 -0
- package/dist/testing/index.js +585 -0
- package/dist/testing/seed.d.ts +433 -0
- package/dist/testing/seed.js +449 -0
- package/dist/tools.d.ts +21 -3
- package/dist/tools.js +113 -22
- package/dist/tunnel-assets/linux-amd64.gz +0 -0
- package/dist/tunnel-assets/linux-arm64.gz +0 -0
- package/dist/tunnel-assets.d.ts +10 -0
- package/dist/tunnel-assets.js +11 -0
- package/dist/tunnel-packet.d.ts +3 -0
- package/dist/tunnel-packet.js +43 -0
- package/dist/tunnel-pty.d.ts +86 -0
- package/dist/tunnel-pty.js +243 -0
- package/dist/tunnels.d.ts +47 -0
- package/dist/tunnels.js +454 -0
- package/dist/workspace-ref.d.ts +87 -0
- package/dist/workspace-ref.js +173 -0
- package/dist/workspace.d.ts +40 -1
- package/dist/workspace.js +111 -2
- package/package.json +7 -2
package/dist/cell.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ExecStartError, NotSupportedForModeError, ShardfluxApiError, ShardfluxProtocolError, apiError, isErrorBody, isWorkingQuotaRefusal } from "./errors.js";
|
|
2
|
+
import { exitCodePosix } from "./exit-code.js";
|
|
2
3
|
import { EXECUTION_ID, ExecutionResult, newExecutionId } from "./executions.js";
|
|
3
4
|
import { HttpClient, defaultSleep, randomId, treeRevisionOf } from "./http.js";
|
|
4
5
|
import { describeFailure, emitTo } from "./progress.js";
|
|
@@ -590,6 +591,7 @@ export class CellClient {
|
|
|
590
591
|
return {
|
|
591
592
|
sessionId,
|
|
592
593
|
exitCode: session.exit_code ?? null,
|
|
594
|
+
exitCodePosix: exitCodePosix({ exitCode: session.exit_code ?? null, termSignal: session.term_signal ?? null, timedOut: session.timed_out ?? false, canceled: session.canceled ?? false }),
|
|
593
595
|
termSignal: session.term_signal ?? null,
|
|
594
596
|
timedOut: session.timed_out ?? false,
|
|
595
597
|
canceled: session.canceled ?? false,
|
|
@@ -1065,6 +1067,75 @@ export class CellClient {
|
|
|
1065
1067
|
return this.#json('POST', this.#p('/v1/workspaces/{workspace_id}/git/commit'), { json: req });
|
|
1066
1068
|
},
|
|
1067
1069
|
};
|
|
1070
|
+
// ---- computer (contracts §45, 0.15.0+) ---------------------------------------------------------
|
|
1071
|
+
/**
|
|
1072
|
+
* The workspace desktop. The platform starts it on the first call that needs it (actions, start, stream); status
|
|
1073
|
+
* never starts it. Needs the `computer` tool, which tool tokens carry while the workspace's computer use is on.
|
|
1074
|
+
*/
|
|
1075
|
+
/**
|
|
1076
|
+
* A computer call with one retry on a fresh token when the cached one lacks the computer tool: computer use may have
|
|
1077
|
+
* been switched on after the token was issued (tokens live up to 15 minutes).
|
|
1078
|
+
*/
|
|
1079
|
+
async #computer(call) {
|
|
1080
|
+
try {
|
|
1081
|
+
return await call();
|
|
1082
|
+
}
|
|
1083
|
+
catch (err) {
|
|
1084
|
+
if (!(err instanceof ShardfluxApiError && err.status === 403 && err.details?.tool === 'computer'))
|
|
1085
|
+
throw err;
|
|
1086
|
+
this.tokens.invalidate();
|
|
1087
|
+
return call();
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
computer = {
|
|
1091
|
+
status: () => {
|
|
1092
|
+
const refusal = this.#needsVm('computer.status');
|
|
1093
|
+
if (refusal)
|
|
1094
|
+
return Promise.reject(refusal);
|
|
1095
|
+
return this.#computer(() => this.#json('GET', this.#p('/v1/workspaces/{workspace_id}/computer')));
|
|
1096
|
+
},
|
|
1097
|
+
start: (req = {}) => {
|
|
1098
|
+
const refusal = this.#needsVm('computer.start');
|
|
1099
|
+
if (refusal)
|
|
1100
|
+
return Promise.reject(refusal);
|
|
1101
|
+
return this.#computer(() => this.#json('POST', this.#p('/v1/workspaces/{workspace_id}/computer'), { json: req, timeoutMs: 90_000 }));
|
|
1102
|
+
},
|
|
1103
|
+
stop: async () => {
|
|
1104
|
+
const refusal = this.#needsVm('computer.stop');
|
|
1105
|
+
if (refusal)
|
|
1106
|
+
throw refusal;
|
|
1107
|
+
await this.#computer(() => this.#json('DELETE', this.#p('/v1/workspaces/{workspace_id}/computer')));
|
|
1108
|
+
},
|
|
1109
|
+
/**
|
|
1110
|
+
* Runs a batch of actions in order (the first failure stops it; the rest are reported `skipped`), then a screenshot
|
|
1111
|
+
* when `screenshot` is set. Waits and key holds may take up to 300 s per batch.
|
|
1112
|
+
*/
|
|
1113
|
+
act: (req, signal) => {
|
|
1114
|
+
const refusal = this.#needsVm('computer.actions');
|
|
1115
|
+
if (refusal)
|
|
1116
|
+
return Promise.reject(refusal);
|
|
1117
|
+
const waits = req.actions.reduce((sum, a) => sum + (a.duration ?? 0), 0);
|
|
1118
|
+
const typing = req.actions.reduce((sum, a) => sum + (a.action === 'type' ? (a.text?.length ?? 0) : 0), 0);
|
|
1119
|
+
return this.#computer(() => this.#json('POST', this.#p('/v1/workspaces/{workspace_id}/computer/actions'), {
|
|
1120
|
+
json: req,
|
|
1121
|
+
timeoutMs: 120_000 + waits * 1000 + typing * 25,
|
|
1122
|
+
...(signal ? { signal } : {}),
|
|
1123
|
+
}));
|
|
1124
|
+
},
|
|
1125
|
+
/** Starts the viewer in the guest; expose the returned port and open a link to `path` (Workspace.computer.stream does both). */
|
|
1126
|
+
streamStart: (req = {}) => {
|
|
1127
|
+
const refusal = this.#needsVm('computer.stream');
|
|
1128
|
+
if (refusal)
|
|
1129
|
+
return Promise.reject(refusal);
|
|
1130
|
+
return this.#computer(() => this.#json('POST', this.#p('/v1/workspaces/{workspace_id}/computer/stream'), { json: req, timeoutMs: 90_000 }));
|
|
1131
|
+
},
|
|
1132
|
+
streamStop: async () => {
|
|
1133
|
+
const refusal = this.#needsVm('computer.stream_stop');
|
|
1134
|
+
if (refusal)
|
|
1135
|
+
throw refusal;
|
|
1136
|
+
await this.#computer(() => this.#json('DELETE', this.#p('/v1/workspaces/{workspace_id}/computer/stream')));
|
|
1137
|
+
},
|
|
1138
|
+
};
|
|
1068
1139
|
// ---- browser ---------------------------------------------------------------------------
|
|
1069
1140
|
browser = {
|
|
1070
1141
|
screenshot: (req) => {
|
package/dist/client.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ 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';
|
|
19
21
|
import { WorkspacePorts } from './ports.js';
|
|
@@ -28,6 +30,8 @@ import type { ProgressListener } from './progress.js';
|
|
|
28
30
|
import { CaptureRegistry } from './capture.js';
|
|
29
31
|
import type { FeedbackReceipt, SendFeedbackParams } from './feedback.js';
|
|
30
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'];
|
|
31
35
|
export type Operation = components['schemas']['Operation'];
|
|
32
36
|
/** persistent (kept until deleted) or session (discarded when the session ends: close(), idle timeout). */
|
|
33
37
|
export type WorkspaceLifetime = components['schemas']['WorkspaceLifetime'];
|
|
@@ -116,9 +120,9 @@ export interface SuspendWhenIdleResult {
|
|
|
116
120
|
workspace: Workspace;
|
|
117
121
|
}
|
|
118
122
|
export interface ShardfluxOptions {
|
|
119
|
-
/** Project API key: sfk_<key_id>_<secret>. */
|
|
120
|
-
apiKey
|
|
121
|
-
/** 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. */
|
|
122
126
|
baseUrl?: string;
|
|
123
127
|
/** Default: pooled HTTP/1.1 on Node 26+, native fetch on other runtimes (see defaultFetch in http.ts). */
|
|
124
128
|
fetch?: typeof fetch;
|
|
@@ -155,7 +159,7 @@ export type AllocationMode = 'fixed' | 'elastic';
|
|
|
155
159
|
export type WorkspaceMemory = WorkspaceView['memory'];
|
|
156
160
|
export interface Caps {
|
|
157
161
|
cpu_millis?: number;
|
|
158
|
-
/** Memory in MiB;
|
|
162
|
+
/** Memory in MiB; elastic promise, or fixed size (the template default when omitted). */
|
|
159
163
|
memory_mib?: number;
|
|
160
164
|
disk_gib?: number;
|
|
161
165
|
/**
|
|
@@ -193,6 +197,10 @@ export interface ResizeParams {
|
|
|
193
197
|
cpuMillis?: number;
|
|
194
198
|
/** Disk in GiB; disks grow only. */
|
|
195
199
|
diskGib?: number;
|
|
200
|
+
/** Only grow each named numeric resource; a smaller or equal value leaves it unchanged. */
|
|
201
|
+
atLeast?: boolean;
|
|
202
|
+
/** Wait for completion (default true); false returns the held server answer without operation polling. */
|
|
203
|
+
wait?: boolean;
|
|
196
204
|
/** Replays the stored response for a repeated request (default: a fresh key per call, so transport retries replay). */
|
|
197
205
|
idempotencyKey?: string;
|
|
198
206
|
/** Give up waiting for the resize after this long (default 300 000 ms); it continues server side. */
|
|
@@ -256,10 +264,30 @@ export interface WaitOptions {
|
|
|
256
264
|
onProgress?: ProgressListener;
|
|
257
265
|
}
|
|
258
266
|
export type IdlePolicy = 'adaptive' | 'never' | `fixed:${number}`;
|
|
267
|
+
export interface RetentionPolicy {
|
|
268
|
+
delete_after_idle_days: number;
|
|
269
|
+
}
|
|
270
|
+
export interface ProjectRetentionPolicy extends RetentionPolicy {
|
|
271
|
+
labels?: Record<string, string>;
|
|
272
|
+
}
|
|
273
|
+
export declare class ProjectsRetentionApi {
|
|
274
|
+
#private;
|
|
275
|
+
constructor(ctx: () => ClientContext);
|
|
276
|
+
getRetention(projectId: string): Promise<ProjectRetentionPolicy | null>;
|
|
277
|
+
setRetention(projectId: string, policy: ProjectRetentionPolicy | null): Promise<ProjectRetentionPolicy | null>;
|
|
278
|
+
}
|
|
259
279
|
export interface OpenParams {
|
|
280
|
+
/** Opt-in idle deletion in days (1..3650), persistent workspaces only. Omitted leaves it unchanged. */
|
|
281
|
+
retention?: RetentionPolicy;
|
|
260
282
|
/** Searchable metadata; supplied labels replace the existing map. */
|
|
261
283
|
labels?: Record<string, string>;
|
|
262
284
|
idlePolicy?: IdlePolicy;
|
|
285
|
+
/**
|
|
286
|
+
* Computer use (0.15.0+, contracts §45.1): true or false sets the workspace's own switch, null follows the template;
|
|
287
|
+
* omitted leaves it unchanged. While it is on, tool tokens carry the `computer` tool and `workspace.computer` drives
|
|
288
|
+
* the workspace desktop. 409 `computer_use_unavailable` when the template version cannot run a desktop.
|
|
289
|
+
*/
|
|
290
|
+
computerUse?: boolean | null;
|
|
263
291
|
key: string;
|
|
264
292
|
template: string;
|
|
265
293
|
caps?: Caps;
|
|
@@ -403,6 +431,9 @@ export declare class WorkspacesApi {
|
|
|
403
431
|
/** Replace labels. An empty map clears them. */
|
|
404
432
|
setLabels(workspaceId: string, labels: Record<string, string>): Promise<Workspace>;
|
|
405
433
|
/** null clears the override, restoring the template or platform policy. */
|
|
434
|
+
/** Computer use (0.15.0+): the workspace's own switch; null follows the template. Returns the switch. */
|
|
435
|
+
setComputerUse(workspaceId: string, enabled: boolean | null): Promise<ComputerUse>;
|
|
436
|
+
setRetention(workspaceId: string, policy: RetentionPolicy | null): Promise<Workspace>;
|
|
406
437
|
setIdlePolicy(workspaceId: string, idlePolicy: IdlePolicy | null): Promise<Workspace>;
|
|
407
438
|
list(params?: ListParams): Promise<Page<Workspace>>;
|
|
408
439
|
/** Iterates every page. */
|
|
@@ -516,6 +547,10 @@ export declare class WorkspacesApi {
|
|
|
516
547
|
* boots blank on the next resume. Returns the `reset` operation; its result names the recovery checkpoint (restorable
|
|
517
548
|
* for 7 days). Errors: 409 legacy_disk_layout, not_resettable, operation_in_progress.
|
|
518
549
|
*/
|
|
550
|
+
/** Opt-in cold start preserving the disk; next_resume schedules it without stopping the VM. */
|
|
551
|
+
upgrade(workspaceId: string, opts?: LifecycleOptions & {
|
|
552
|
+
at?: 'now' | 'next_resume';
|
|
553
|
+
}): Promise<Operation>;
|
|
519
554
|
reset(workspaceId: string, opts: WaitedLifecycleOptions): Promise<FinishedOperation>;
|
|
520
555
|
reset(workspaceId: string, opts?: LifecycleOptions): Promise<Operation>;
|
|
521
556
|
/**
|
|
@@ -571,6 +606,7 @@ export declare function fetchBillingCatalog(opts?: {
|
|
|
571
606
|
}): Promise<BillingCatalog>;
|
|
572
607
|
export declare class Shardflux {
|
|
573
608
|
#private;
|
|
609
|
+
readonly projects: ProjectsRetentionApi;
|
|
574
610
|
readonly workspaces: WorkspacesApi;
|
|
575
611
|
readonly billing: BillingApi;
|
|
576
612
|
/** Usage, allowances, estimates, grants/leases and spend (Phase 9). */
|
|
@@ -585,7 +621,14 @@ export declare class Shardflux {
|
|
|
585
621
|
readonly audit: AuditApi;
|
|
586
622
|
/** Shared volumes: persistent storage attached to workspaces at a mount path. */
|
|
587
623
|
readonly volumes: VolumesApi;
|
|
588
|
-
|
|
624
|
+
/** Reads `SHARDFLUX_API_KEY` and `SHARDFLUX_API_URL` (0.15.0+) for an `apiKey` or `baseUrl` that is not passed. */
|
|
625
|
+
constructor(opts?: ShardfluxOptions);
|
|
626
|
+
/**
|
|
627
|
+
* A workspace named by its key (0.15.0+, contracts §46): no request until its first call, which opens the key
|
|
628
|
+
* (created on first use with `params.template`, resumed afterwards). `workspace(key, { template: 'default' })
|
|
629
|
+
* .exec('...')` is a whole integration; see `WorkspaceRef`.
|
|
630
|
+
*/
|
|
631
|
+
workspace(key: string, params: WorkspaceRefParams): WorkspaceRef;
|
|
589
632
|
/** The authenticated principal (the API key, its organization and project). */
|
|
590
633
|
me(): Promise<Me>;
|
|
591
634
|
entitlements(organizationId: string): Promise<Entitlements>;
|
|
@@ -601,4 +644,10 @@ export declare class Shardflux {
|
|
|
601
644
|
/** Raw access to any /v1 endpoint with the SDK's authentication and error handling. */
|
|
602
645
|
request<T>(method: string, path: string, init?: Parameters<HttpClient['json']>[2]): Promise<T>;
|
|
603
646
|
}
|
|
647
|
+
/**
|
|
648
|
+
* A workspace named by its key, on a client from the environment (0.15.0+): `new Shardflux()` reads
|
|
649
|
+
* `SHARDFLUX_API_KEY` (and `SHARDFLUX_API_URL`) on the first call. `workspace(key, { template: 'default' }).exec('...')`
|
|
650
|
+
* creates the workspace on first use and resumes it afterwards. Use `cloud.workspace()` for a client of your own.
|
|
651
|
+
*/
|
|
652
|
+
export declare function workspace(key: string, params: WorkspaceRefParams): WorkspaceRef;
|
|
604
653
|
export { ShardfluxApiError };
|
package/dist/client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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";
|
|
6
7
|
import { WorkspacePorts } from "./ports.js";
|
|
@@ -13,6 +14,18 @@ import { AFTER_WAIT, HELD_RESUME, TRACE, runLifecycle, waitOptionsOf } from "./l
|
|
|
13
14
|
import { Trace, combineListeners, durabilityOf, isDurable, traced } from "./progress.js";
|
|
14
15
|
import { CaptureRegistry } from "./capture.js";
|
|
15
16
|
import { sendFeedback } from "./feedback.js";
|
|
17
|
+
export class ProjectsRetentionApi {
|
|
18
|
+
#ctx;
|
|
19
|
+
constructor(ctx) { this.#ctx = ctx; }
|
|
20
|
+
getRetention(projectId) {
|
|
21
|
+
const c = this.#ctx();
|
|
22
|
+
return c.http.json('GET', `/v1/projects/${encodeURIComponent(projectId)}/retention`, {}, c.authorization);
|
|
23
|
+
}
|
|
24
|
+
setRetention(projectId, policy) {
|
|
25
|
+
const c = this.#ctx();
|
|
26
|
+
return c.http.json('PUT', `/v1/projects/${encodeURIComponent(projectId)}/retention`, { json: policy }, c.authorization);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
16
29
|
/**
|
|
17
30
|
* The workspace a key names: the live row (deleted_at null) when there is one, since at most one live
|
|
18
31
|
* workspace holds a key; otherwise the newest tombstone (ended sessions leave tombstones with the same key, and a
|
|
@@ -62,6 +75,8 @@ function resizeBody(p) {
|
|
|
62
75
|
body.disk_gib = p.diskGib;
|
|
63
76
|
if (Object.keys(body).length === 0)
|
|
64
77
|
throw new TypeError('resize() needs at least one of memoryMib, memoryMibHeld, allocationMode, cpuMillis, diskGib');
|
|
78
|
+
if (p.atLeast !== undefined)
|
|
79
|
+
body.at_least = p.atLeast;
|
|
65
80
|
return body;
|
|
66
81
|
}
|
|
67
82
|
const RESIZE_KEYS = {
|
|
@@ -163,8 +178,12 @@ export class WorkspacesApi {
|
|
|
163
178
|
body.inputs = params.inputs;
|
|
164
179
|
if (params.labels !== undefined)
|
|
165
180
|
body.labels = params.labels;
|
|
181
|
+
if (params.retention !== undefined)
|
|
182
|
+
body.retention = params.retention;
|
|
166
183
|
if (params.idlePolicy !== undefined)
|
|
167
184
|
body.idle_policy = params.idlePolicy;
|
|
185
|
+
if (params.computerUse !== undefined)
|
|
186
|
+
body.computer_use = params.computerUse;
|
|
168
187
|
if (params.lifetime !== undefined)
|
|
169
188
|
body.lifetime = params.lifetime;
|
|
170
189
|
if (params.mode !== undefined)
|
|
@@ -193,7 +212,9 @@ export class WorkspacesApi {
|
|
|
193
212
|
if (res.body.operation)
|
|
194
213
|
trace.observe(res.body.operation);
|
|
195
214
|
this.#noteToken(res.body.tool_token);
|
|
196
|
-
|
|
215
|
+
// An API before contracts §46.2 does not say whether the open created the workspace.
|
|
216
|
+
const created = res.body.created ?? null;
|
|
217
|
+
const wrapOpts = { agentLabel: params.agentLabel, tools: params.tools, token: res.body.tool_token, trace, created };
|
|
197
218
|
if (res.status === 200 || params.wait === false || res.body.operation === null)
|
|
198
219
|
return this.#wrap(res.body.workspace, wrapOpts);
|
|
199
220
|
if (TERMINAL.has(res.body.operation.state)) {
|
|
@@ -390,6 +411,13 @@ export class WorkspacesApi {
|
|
|
390
411
|
return this.#wrap(await this.#http.json('PUT', `/v1/workspaces/${encodeURIComponent(workspaceId)}/labels`, { json: { labels } }, this.#auth));
|
|
391
412
|
}
|
|
392
413
|
/** null clears the override, restoring the template or platform policy. */
|
|
414
|
+
/** Computer use (0.15.0+): the workspace's own switch; null follows the template. Returns the switch. */
|
|
415
|
+
async setComputerUse(workspaceId, enabled) {
|
|
416
|
+
return this.#http.json('PUT', `/v1/workspaces/${encodeURIComponent(workspaceId)}/computer-use`, { json: { enabled } }, this.#auth);
|
|
417
|
+
}
|
|
418
|
+
async setRetention(workspaceId, policy) {
|
|
419
|
+
return this.#wrap(await this.#http.json('PUT', `/v1/workspaces/${encodeURIComponent(workspaceId)}/retention`, { json: policy }, this.#auth));
|
|
420
|
+
}
|
|
393
421
|
async setIdlePolicy(workspaceId, idlePolicy) {
|
|
394
422
|
return this.#wrap(await this.#http.json('PUT', `/v1/workspaces/${encodeURIComponent(workspaceId)}/idle-policy`, { json: { idle_policy: idlePolicy } }, this.#auth));
|
|
395
423
|
}
|
|
@@ -465,7 +493,7 @@ export class WorkspacesApi {
|
|
|
465
493
|
if (kind === 'delete' || kind === 'reset')
|
|
466
494
|
this.#ctx().captures.discard(workspaceId, kind);
|
|
467
495
|
return operation;
|
|
468
|
-
}, opts, { settle: kind === 'suspend' || kind === 'snapshot' });
|
|
496
|
+
}, opts, { settle: kind === 'suspend' || kind === 'snapshot' || kind === 'upgrade' });
|
|
469
497
|
}
|
|
470
498
|
delete(workspaceId, opts = {}) {
|
|
471
499
|
return this.#op('delete', workspaceId, undefined, opts);
|
|
@@ -631,7 +659,7 @@ export class WorkspacesApi {
|
|
|
631
659
|
// Another lifecycle operation (a suspend, a resume, another resize) holds the workspace: the refused request
|
|
632
660
|
// changed nothing, so wait for that operation and send it again (bounded, within timeoutMs).
|
|
633
661
|
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)
|
|
662
|
+
if (params.wait === false || typeof active !== 'string' || attempt >= 3 || Date.now() - started >= timeoutMs)
|
|
635
663
|
throw e;
|
|
636
664
|
trace.retry({ request: `PATCH ${path}`, attempt: attempt + 1, cause: `conflict operation_in_progress (waits for ${active})`, delayMs: 0 });
|
|
637
665
|
try {
|
|
@@ -649,6 +677,15 @@ export class WorkspacesApi {
|
|
|
649
677
|
if (!operation || typeof operation !== 'object')
|
|
650
678
|
throw new ShardfluxProtocolError('resize: 202 response has no operation', res.status, 'api');
|
|
651
679
|
trace.observe(operation);
|
|
680
|
+
if (params.wait === false) {
|
|
681
|
+
if (operation.state === 'failed' || operation.state === 'canceled')
|
|
682
|
+
throw new OperationFailedError(operation);
|
|
683
|
+
const workspace = res.body.workspace;
|
|
684
|
+
const result = resizeResultBodyOf(operation);
|
|
685
|
+
return { workspaceId, operationId: operation.id, state: workspace?.observed_state ?? operation.state,
|
|
686
|
+
memory: result.memory ?? null, cpu: result.cpu ?? null, disk: result.disk ?? null,
|
|
687
|
+
caps: workspace?.caps ?? null, operation };
|
|
688
|
+
}
|
|
652
689
|
let final = operation;
|
|
653
690
|
if (operation.state !== 'succeeded') {
|
|
654
691
|
if (TERMINAL.has(operation.state))
|
|
@@ -676,6 +713,17 @@ export class WorkspacesApi {
|
|
|
676
713
|
}, opts, { settle: true });
|
|
677
714
|
return { operation, workspace: workspace };
|
|
678
715
|
}
|
|
716
|
+
/**
|
|
717
|
+
* Resets a layered workspace to its template: every change in the workspace layer is wiped; key,
|
|
718
|
+
* id, template version, caps, secret bindings and volume attachments stay. Running: restarted on a blank layer
|
|
719
|
+
* (processes are gone; old tool tokens get 409 stale_epoch and the SDK refreshes them). Suspended: stays suspended and
|
|
720
|
+
* boots blank on the next resume. Returns the `reset` operation; its result names the recovery checkpoint (restorable
|
|
721
|
+
* for 7 days). Errors: 409 legacy_disk_layout, not_resettable, operation_in_progress.
|
|
722
|
+
*/
|
|
723
|
+
/** Opt-in cold start preserving the disk; next_resume schedules it without stopping the VM. */
|
|
724
|
+
upgrade(workspaceId, opts = {}) {
|
|
725
|
+
return this.#op('upgrade', workspaceId, { at: opts.at ?? 'now' }, opts);
|
|
726
|
+
}
|
|
679
727
|
reset(workspaceId, opts = {}) {
|
|
680
728
|
const body = { confirm_destructive: true };
|
|
681
729
|
return this.#op('reset', workspaceId, body, opts);
|
|
@@ -778,6 +826,7 @@ export async function fetchBillingCatalog(opts = {}) {
|
|
|
778
826
|
return http.json('GET', '/v1/billing/catalog');
|
|
779
827
|
}
|
|
780
828
|
export class Shardflux {
|
|
829
|
+
projects;
|
|
781
830
|
workspaces;
|
|
782
831
|
billing;
|
|
783
832
|
/** Usage, allowances, estimates, grants/leases and spend (Phase 9). */
|
|
@@ -793,12 +842,18 @@ export class Shardflux {
|
|
|
793
842
|
/** Shared volumes: persistent storage attached to workspaces at a mount path. */
|
|
794
843
|
volumes;
|
|
795
844
|
#ctx;
|
|
796
|
-
|
|
797
|
-
|
|
845
|
+
#toolGrants = new GrantsCache(() => this.me());
|
|
846
|
+
/** Reads `SHARDFLUX_API_KEY` and `SHARDFLUX_API_URL` (0.15.0+) for an `apiKey` or `baseUrl` that is not passed. */
|
|
847
|
+
constructor(opts = {}) {
|
|
848
|
+
const apiKey = opts.apiKey ?? envVar('SHARDFLUX_API_KEY');
|
|
849
|
+
if (apiKey === undefined)
|
|
850
|
+
throw new Error('Missing API key: pass apiKey or set SHARDFLUX_API_KEY (a project key, sfk_<key_id>_<secret>)');
|
|
851
|
+
if (!/^sfk_[a-z2-7]{16}_[A-Za-z0-9]+$/.test(apiKey))
|
|
798
852
|
throw new Error('apiKey must be a Shardflux project key (sfk_<key_id>_<secret>)');
|
|
799
853
|
const f = opts.fetch ?? defaultFetch();
|
|
800
854
|
const userAgent = opts.userAgent ?? `shardflux-sdk-ts/${SDK_VERSION}`;
|
|
801
855
|
const sleep = opts.sleep ?? defaultSleep;
|
|
856
|
+
this.projects = new ProjectsRetentionApi(() => this.#ctx);
|
|
802
857
|
this.workspaces = new WorkspacesApi(() => this.#ctx);
|
|
803
858
|
this.billing = new BillingApi(() => this.#ctx);
|
|
804
859
|
this.usage = new UsageApi(() => this.#ctx);
|
|
@@ -807,10 +862,10 @@ export class Shardflux {
|
|
|
807
862
|
this.egress = new EgressPolicyApi(() => this.#ctx);
|
|
808
863
|
this.audit = new AuditApi(() => this.#ctx);
|
|
809
864
|
this.volumes = new VolumesApi(() => this.#ctx);
|
|
810
|
-
const baseUrl = opts.baseUrl ?? 'https://api.shardflux.dev';
|
|
865
|
+
const baseUrl = opts.baseUrl ?? envVar('SHARDFLUX_API_URL') ?? envVar('SHARDFLUX_BASE_URL') ?? 'https://api.shardflux.dev';
|
|
811
866
|
this.#ctx = {
|
|
812
867
|
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) }),
|
|
813
|
-
authorization: `Bearer ${
|
|
868
|
+
authorization: `Bearer ${apiKey}`,
|
|
814
869
|
fetch: f,
|
|
815
870
|
userAgent,
|
|
816
871
|
sleep,
|
|
@@ -819,6 +874,14 @@ export class Shardflux {
|
|
|
819
874
|
captures: new CaptureRegistry(),
|
|
820
875
|
};
|
|
821
876
|
}
|
|
877
|
+
/**
|
|
878
|
+
* A workspace named by its key (0.15.0+, contracts §46): no request until its first call, which opens the key
|
|
879
|
+
* (created on first use with `params.template`, resumed afterwards). `workspace(key, { template: 'default' })
|
|
880
|
+
* .exec('...')` is a whole integration; see `WorkspaceRef`.
|
|
881
|
+
*/
|
|
882
|
+
workspace(key, params) {
|
|
883
|
+
return new WorkspaceRef(this.workspaces, key, params, () => this.#toolGrants.get());
|
|
884
|
+
}
|
|
822
885
|
/** The authenticated principal (the API key, its organization and project). */
|
|
823
886
|
me() {
|
|
824
887
|
return this.#ctx.http.json('GET', '/v1/me', {}, this.#ctx.authorization);
|
|
@@ -842,4 +905,34 @@ export class Shardflux {
|
|
|
842
905
|
return this.#ctx.http.json(method, path, init, this.#ctx.authorization);
|
|
843
906
|
}
|
|
844
907
|
}
|
|
908
|
+
/** The client of the module-level `workspace()`: created on first use from the environment. */
|
|
909
|
+
let defaultClient = null;
|
|
910
|
+
/**
|
|
911
|
+
* A workspace named by its key, on a client from the environment (0.15.0+): `new Shardflux()` reads
|
|
912
|
+
* `SHARDFLUX_API_KEY` (and `SHARDFLUX_API_URL`) on the first call. `workspace(key, { template: 'default' }).exec('...')`
|
|
913
|
+
* creates the workspace on first use and resumes it afterwards. Use `cloud.workspace()` for a client of your own.
|
|
914
|
+
*/
|
|
915
|
+
export function workspace(key, params) {
|
|
916
|
+
defaultClient ??= new Shardflux();
|
|
917
|
+
return defaultClient.workspace(key, params);
|
|
918
|
+
}
|
|
919
|
+
/** An environment variable, trimmed; undefined when unset, empty or outside Node-like runtimes. */
|
|
920
|
+
function envVar(name) {
|
|
921
|
+
return globalThis.process?.env?.[name]?.trim() || undefined;
|
|
922
|
+
}
|
|
923
|
+
/** The API key's tool permissions (GET /v1/me), read once; a failed read is not kept. Null for a non-key principal. */
|
|
924
|
+
class GrantsCache {
|
|
925
|
+
#load;
|
|
926
|
+
#value = null;
|
|
927
|
+
constructor(load) {
|
|
928
|
+
this.#load = load;
|
|
929
|
+
}
|
|
930
|
+
get() {
|
|
931
|
+
this.#value ??= this.#load().then((me) => (me.api_key ? [...me.api_key.tool_permissions] : null), (err) => {
|
|
932
|
+
this.#value = null;
|
|
933
|
+
throw err;
|
|
934
|
+
});
|
|
935
|
+
return this.#value;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
845
938
|
export { ShardfluxApiError };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Computer use (0.15.0+, contracts §45): the workspace desktop, which the platform starts on the first call that needs
|
|
3
|
+
* it. `workspace.computer` drives it; `computerToolset(workspace)` answers Claude's computer toolset
|
|
4
|
+
* (`computer_toolset_20260801`) with one batch per model turn.
|
|
5
|
+
*
|
|
6
|
+
* await workspace.setComputerUse(true); // or open({ ..., computerUse: true })
|
|
7
|
+
* const shot = await workspace.computer.screenshot(); // { format: 'png', width: 1280, height: 800, data }
|
|
8
|
+
* await workspace.computer.act([{ action: 'left_click', coordinate: [640, 400] }, { action: 'type', text: 'hello' }]);
|
|
9
|
+
* const { url } = await workspace.computer.stream(); // a private link to watch the screen
|
|
10
|
+
*/
|
|
11
|
+
import type { CellClient, ComputerAction, ComputerActionsResult, ComputerImage, ComputerStatus } from './cell.js';
|
|
12
|
+
import type { ComputerUse } from './client.js';
|
|
13
|
+
import type { WorkspacePorts } from './ports.js';
|
|
14
|
+
/** A decoded screen image. */
|
|
15
|
+
export interface ComputerScreenshot {
|
|
16
|
+
format: 'png' | 'jpeg';
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
data: Uint8Array;
|
|
20
|
+
}
|
|
21
|
+
export interface ComputerActOptions {
|
|
22
|
+
/** Append a screenshot after the last action that ran (also after a failure). */
|
|
23
|
+
screenshot?: boolean;
|
|
24
|
+
/** Wait this long before that screenshot when an action changed the screen (default 250). */
|
|
25
|
+
settleMs?: number;
|
|
26
|
+
format?: 'png' | 'jpeg';
|
|
27
|
+
/** JPEG quality 1..100 (default 80). */
|
|
28
|
+
quality?: number;
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
}
|
|
31
|
+
export interface ComputerStreamOptions {
|
|
32
|
+
/** Mint a new link even when a cached link has more than 60 seconds left. */
|
|
33
|
+
fresh?: boolean;
|
|
34
|
+
/** Frame the viewer in these HTTPS origins with a partitioned browser session (§48). */
|
|
35
|
+
embed?: import('./ports.js').PortEmbedOptions;
|
|
36
|
+
/** Let the viewer use the mouse and keyboard (default false: view only, enforced in the workspace). */
|
|
37
|
+
interactive?: boolean;
|
|
38
|
+
/** How long the link works (60..604800 s; default 86400). */
|
|
39
|
+
ttlSeconds?: number;
|
|
40
|
+
}
|
|
41
|
+
/** A private link to the desktop's viewer: open it in a browser or an iframe. */
|
|
42
|
+
export interface ComputerStream {
|
|
43
|
+
url: string;
|
|
44
|
+
expiresAt: string;
|
|
45
|
+
port: number;
|
|
46
|
+
interactive: boolean;
|
|
47
|
+
}
|
|
48
|
+
/** Decodes a result image (base64 in the API) to bytes. */
|
|
49
|
+
export declare function decodeComputerImage(img: ComputerImage): ComputerScreenshot;
|
|
50
|
+
interface ComputerDeps {
|
|
51
|
+
cell: () => CellClient;
|
|
52
|
+
ports: () => WorkspacePorts;
|
|
53
|
+
setEnabled: (enabled: boolean | null) => Promise<ComputerUse>;
|
|
54
|
+
}
|
|
55
|
+
/** The workspace desktop (workspace.computer). */
|
|
56
|
+
export declare class WorkspaceComputer {
|
|
57
|
+
#private;
|
|
58
|
+
constructor(deps: ComputerDeps);
|
|
59
|
+
/**
|
|
60
|
+
* Runs actions in order (the first failure stops the batch; later actions come back `skipped`), optionally ending
|
|
61
|
+
* with a screenshot. The actions are Claude's computer toolset members with their parameter names.
|
|
62
|
+
*/
|
|
63
|
+
act(actions: ComputerAction[], opts?: ComputerActOptions): Promise<ComputerActionsResult>;
|
|
64
|
+
/** The screen now (the pointer drawn in). */
|
|
65
|
+
screenshot(opts?: Pick<ComputerActOptions, 'format' | 'quality' | 'signal'>): Promise<ComputerScreenshot>;
|
|
66
|
+
/** Whether the desktop runs, its size and its viewers. Never starts it. */
|
|
67
|
+
status(): Promise<ComputerStatus>;
|
|
68
|
+
/** Starts the desktop (a no-op while it runs; the size applies to a start only: 640x480..2560x1600, default 1280x800). */
|
|
69
|
+
start(size?: {
|
|
70
|
+
width?: number;
|
|
71
|
+
height?: number;
|
|
72
|
+
}): Promise<ComputerStatus>;
|
|
73
|
+
/** Stops the desktop; its windows close. The next call that needs it starts a fresh one. */
|
|
74
|
+
stop(): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* A private link to watch the desktop (or use it, with `interactive`): starts the viewer in the workspace, exposes
|
|
77
|
+
* its port and signs a link to the viewer page (inbound ports, contracts §39). Anyone with the link can open it until
|
|
78
|
+
* it expires; `stopStream()` ends every view.
|
|
79
|
+
*/
|
|
80
|
+
stream(opts?: ComputerStreamOptions): Promise<ComputerStream>;
|
|
81
|
+
/** Ends every view and closes the viewer ports (their links stop working). */
|
|
82
|
+
stopStream(): Promise<void>;
|
|
83
|
+
/** Switches computer use on or off for this workspace (null follows the template). */
|
|
84
|
+
setEnabled(enabled: boolean | null): Promise<ComputerUse>;
|
|
85
|
+
}
|
|
86
|
+
/** The `tools` entry of Claude's computer toolset (no name, no display size). */
|
|
87
|
+
export declare const COMPUTER_TOOLSET: {
|
|
88
|
+
readonly type: "computer_toolset_20260801";
|
|
89
|
+
};
|
|
90
|
+
/** A `tool_use` content block (structurally the Anthropic SDK's, so its blocks pass as they are). */
|
|
91
|
+
export interface ToolUseBlockLike {
|
|
92
|
+
type: string;
|
|
93
|
+
id: string;
|
|
94
|
+
name: string;
|
|
95
|
+
input: unknown;
|
|
96
|
+
toolset_name?: string | null;
|
|
97
|
+
}
|
|
98
|
+
type ResultContent = Array<{
|
|
99
|
+
type: 'text';
|
|
100
|
+
text: string;
|
|
101
|
+
} | {
|
|
102
|
+
type: 'image';
|
|
103
|
+
source: {
|
|
104
|
+
type: 'base64';
|
|
105
|
+
media_type: 'image/png' | 'image/jpeg';
|
|
106
|
+
data: string;
|
|
107
|
+
};
|
|
108
|
+
}>;
|
|
109
|
+
/** A `tool_result` block answering a toolset call (each echoes `toolset_name: "computer"`, as the API requires). */
|
|
110
|
+
export interface ComputerToolResult {
|
|
111
|
+
type: 'tool_result';
|
|
112
|
+
tool_use_id: string;
|
|
113
|
+
toolset_name: 'computer';
|
|
114
|
+
content: ResultContent;
|
|
115
|
+
is_error?: true;
|
|
116
|
+
}
|
|
117
|
+
export interface ComputerToolsetOptions {
|
|
118
|
+
/** Called just before each action. Return false/a refusal string, or throw, to stop the turn. */
|
|
119
|
+
beforeAction?: BeforeComputerAction;
|
|
120
|
+
/** Screenshot format (default png). */
|
|
121
|
+
format?: 'png' | 'jpeg';
|
|
122
|
+
quality?: number;
|
|
123
|
+
settleMs?: number;
|
|
124
|
+
signal?: AbortSignal;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Answers Claude's computer toolset (`tools: [COMPUTER_TOOLSET]`, Claude Opus 5.5 / Sonnet 5.5 and later on the
|
|
128
|
+
* Claude API) from this workspace's desktop:
|
|
129
|
+
*
|
|
130
|
+
* const computer = computerToolset(workspace);
|
|
131
|
+
* const msg = await anthropic.messages.create({ model, max_tokens, tools: [computer.definition], messages });
|
|
132
|
+
* messages.push({ role: 'assistant', content: msg.content });
|
|
133
|
+
* messages.push({ role: 'user', content: await computer.run(msg.content) });
|
|
134
|
+
*
|
|
135
|
+
* `run` takes the response content, runs every `toolset_name: "computer"` call of the turn as one batch (in order;
|
|
136
|
+
* the first failure stops it and the rest are answered "Not executed"), and returns one `tool_result` per call:
|
|
137
|
+
* images for screenshot and zoom, the position for cursor_position, `OK` otherwise. When the batch does not end with a
|
|
138
|
+
* look, a screenshot is attached to the last result, so the model sees the outcome without another round trip. A
|
|
139
|
+
* batch the desktop refuses as invalid (a coordinate outside the screen) is answered with the reason on every call.
|
|
140
|
+
*/
|
|
141
|
+
export declare function computerToolset(workspace: {
|
|
142
|
+
computer: WorkspaceComputer;
|
|
143
|
+
}, defaults?: ComputerToolsetOptions): {
|
|
144
|
+
definition: {
|
|
145
|
+
readonly type: "computer_toolset_20260801";
|
|
146
|
+
};
|
|
147
|
+
run(content: readonly unknown[], opts?: ComputerToolsetOptions): Promise<ComputerToolResult[]>;
|
|
148
|
+
};
|
|
149
|
+
export type BeforeComputerAction = (action: Readonly<ComputerAction>) => void | boolean | string | Promise<void | boolean | string>;
|
|
150
|
+
type ComputerBatchResult = Partial<ComputerActionsResult> & Pick<ComputerActionsResult, 'results'>;
|
|
151
|
+
/** No hook: one batch as before. With a hook, check immediately before each individual action runs. */
|
|
152
|
+
export declare function runComputerActions(act: (actions: ComputerAction[], opts: ComputerActOptions) => Promise<ComputerActionsResult>, actions: ComputerAction[], opts: ComputerActOptions, beforeAction?: BeforeComputerAction): Promise<ComputerBatchResult>;
|
|
153
|
+
export {};
|