@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.
Files changed (49) hide show
  1. package/CHANGELOG.md +85 -23
  2. package/README.md +226 -7
  3. package/dist/account.d.ts +2 -0
  4. package/dist/account.js +6 -0
  5. package/dist/cell.d.ts +27 -0
  6. package/dist/cell.js +71 -0
  7. package/dist/client.d.ts +54 -5
  8. package/dist/client.js +100 -7
  9. package/dist/computer.d.ts +153 -0
  10. package/dist/computer.js +229 -0
  11. package/dist/errors.d.ts +5 -1
  12. package/dist/errors.js +9 -0
  13. package/dist/executions.d.ts +2 -6
  14. package/dist/executions.js +9 -0
  15. package/dist/exit-code.d.ts +7 -0
  16. package/dist/exit-code.js +12 -0
  17. package/dist/generated/app-api.d.ts +1055 -119
  18. package/dist/generated/cell-api.d.ts +334 -0
  19. package/dist/http.d.ts +7 -1
  20. package/dist/http.js +34 -13
  21. package/dist/index.d.ts +13 -6
  22. package/dist/index.js +7 -2
  23. package/dist/ports.d.ts +7 -0
  24. package/dist/ports.js +1 -1
  25. package/dist/progress.d.ts +2 -2
  26. package/dist/progress.js +1 -1
  27. package/dist/templates.d.ts +12 -0
  28. package/dist/templates.js +9 -0
  29. package/dist/testing/index.d.ts +62 -0
  30. package/dist/testing/index.js +585 -0
  31. package/dist/testing/seed.d.ts +433 -0
  32. package/dist/testing/seed.js +449 -0
  33. package/dist/tools.d.ts +21 -3
  34. package/dist/tools.js +113 -22
  35. package/dist/tunnel-assets/linux-amd64.gz +0 -0
  36. package/dist/tunnel-assets/linux-arm64.gz +0 -0
  37. package/dist/tunnel-assets.d.ts +10 -0
  38. package/dist/tunnel-assets.js +11 -0
  39. package/dist/tunnel-packet.d.ts +3 -0
  40. package/dist/tunnel-packet.js +43 -0
  41. package/dist/tunnel-pty.d.ts +86 -0
  42. package/dist/tunnel-pty.js +243 -0
  43. package/dist/tunnels.d.ts +47 -0
  44. package/dist/tunnels.js +454 -0
  45. package/dist/workspace-ref.d.ts +87 -0
  46. package/dist/workspace-ref.js +173 -0
  47. package/dist/workspace.d.ts +40 -1
  48. package/dist/workspace.js +111 -2
  49. 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: string;
121
- /** Default https://api.shardflux.dev (override with `baseUrl`). */
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; for an elastic workspace the promise (what it may grow to). */
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
- constructor(opts: ShardfluxOptions);
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
- const wrapOpts = { agentLabel: params.agentLabel, tools: params.tools, token: res.body.tool_token, trace };
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
- constructor(opts) {
797
- if (!/^sfk_[a-z2-7]{16}_[A-Za-z0-9]+$/.test(opts.apiKey))
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 ${opts.apiKey}`,
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 {};