@shardflux/sdk 0.14.0 → 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 CHANGED
@@ -1,12 +1,54 @@
1
1
  # Changelog
2
2
 
3
+ Every API the README shows is available from the version named here. Breaking changes ship in minor releases and are
4
+ marked **Breaking**.
5
+
6
+ ## 0.15.0 (not yet published)
7
+
8
+ ### Workspaces by key
9
+
10
+ Additive (contracts §46). A workspace named by its key, created on its first use, with no lifecycle code:
11
+
12
+ ```ts
13
+ import { workspace } from '@shardflux/sdk';
14
+ const run = await workspace('acme/thread-42', { template: 'default' }).exec('python3 -c "print(40 + 2)"');
15
+ ```
16
+
17
+ - `cloud.workspace(key, params)` and module-level `workspace(key, params)` return a `WorkspaceRef` without a request.
18
+ Its first call opens the key (one held request); concurrent first calls share it, a failed open is retried by the
19
+ next call, and a workspace deleted under the ref is opened again by the call after the one that met the deletion.
20
+ - `WorkspaceRef`: `exec(command)` (a string runs through `bash -lc`, an argv array without a shell), `files`,
21
+ `executions`, `tools()` (definitions from the key's grants before any VM; the first tool call opens), `open()` (the
22
+ `Workspace`), `cell()`, `hint()` (starts the open), `key`, `created`, `mode`. `create: false` never creates (404
23
+ `not_found` for an unknown key).
24
+ - `new Shardflux()` reads `SHARDFLUX_API_KEY` and `SHARDFLUX_API_URL` when `apiKey` / `baseUrl` are not passed.
25
+ - `workspace.created`: whether the `open()` that returned it created the workspace (the API's new `created` field).
26
+ - `template: 'default'` opens the platform default template (`python-node-browser`) on any client version.
27
+ - `workspaceTools()` takes a `ToolTarget`: a `Workspace` or a `WorkspaceRef`.
28
+
29
+ ### Computer use
30
+
31
+ Additive. A desktop in the workspace that agents drive with screenshots, clicks and keystrokes (contracts §45).
32
+
33
+ - `workspace.computer`: `act(actions, { screenshot, settleMs, format, quality })` runs a batch of Claude's computer
34
+ toolset actions in order (the first failure stops it; the rest come back `skipped`), `screenshot()`, `status()`,
35
+ `start({ width, height })`, `stop()`, `stream({ interactive, ttlSeconds })` (a signed link to the viewer; it exposes
36
+ port 61002, or 61003 when interactive) and `stopStream()`.
37
+ - `computerToolset(workspace)` / `COMPUTER_TOOLSET`: answers Claude's `computer_toolset_20260801` calls of a model turn
38
+ with one batch; every `tool_result` echoes `toolset_name: "computer"`.
39
+ - `workspaceTools()` adds, when the token grants `computer`, the `computer` tool (one action per call) and
40
+ `computer_batch` (up to 50 actions in one call, with each action's result, a zoom's image and one screenshot after
41
+ them). Both take `screenshot: false`, `settle_ms`, and `format: "jpeg"` with `quality`.
42
+ - `open({ computerUse })`, `workspace.setComputerUse(true | false | null)`, `workspace.computerUse`,
43
+ `cloud.workspaces.setComputerUse(id, enabled)`. Tool tokens carry `computer` while the switch is on; a computer call
44
+ whose cached token predates the switch is sent again with a fresh token.
45
+ - `cloud.templates.setComputerUse(slug, enabled, { organizationId })` switches it for every workspace of one of your
46
+ templates. `ToolName` includes `computer`; template views carry `computer_use`.
47
+
3
48
  ## 0.14.0 — 2026-10-04 (not yet published)
4
49
 
5
50
  - Opt-in `workspaceTools(workspace, { prewake: true })` input-start hooks prepare VM tools while their input streams. Default tools, offline reads and file-first workspaces install no hook.
6
51
 
7
- Every API the README shows is available from the version named here. Breaking changes ship in minor releases and are
8
- marked **Breaking**.
9
-
10
52
  ## 0.14.0 (not yet published)
11
53
 
12
54
  ### Sign up from an agent
package/README.md CHANGED
@@ -10,8 +10,9 @@ tool calls into the workspace ([tool-call capture](#tool-call-capture-070)).
10
10
  > **Compatibility.** The API is versioned (`/v1`). Breaking changes ship only in minor releases and are marked
11
11
  > **Breaking** in the changelog (see [Compatibility](#compatibility)).
12
12
 
13
- > **Versions.** This README describes 0.11.0. Anything marked **(0.11.0+)** is not in 0.10.x, **(0.10.0+)** not in 0.9.0, **(0.9.0+)** not in 0.8.x, **(0.8.0+)** not in 0.7.x,
14
- > **(0.7.0+)** not in 0.6.x and **(0.6.0+)** not in 0.5.0; [CHANGELOG.md](./CHANGELOG.md) lists what each version added. Check yours with
13
+ > **Versions.** This README describes 0.15.0. Anything marked **(0.15.0+)** is not in 0.14.x, **(0.14.0+)** not in 0.13.x,
14
+ > **(0.13.0+)** not in 0.12.x, **(0.12.0+)** not in 0.11.x, **(0.11.0+)** not in 0.10.x, **(0.10.0+)** not in 0.9.0, **(0.9.0+)** not in 0.8.x,
15
+ > **(0.8.0+)** not in 0.7.x, **(0.7.0+)** not in 0.6.x and **(0.6.0+)** not in 0.5.0; [CHANGELOG.md](./CHANGELOG.md) lists what each version added. Check yours with
15
16
  > `npm ls @shardflux/sdk` or the exported `SDK_VERSION`.
16
17
 
17
18
  - ESM only, Node.js 24 or later. Reading a YAML template file uses the optional peer
@@ -41,6 +42,33 @@ npm install @shardflux/sdk
41
42
  Create a project API key in the Shardflux console (`sfk_<key id>_<secret>`) and keep it on the
42
43
  server. API keys are server credentials: never put one in a browser bundle.
43
44
 
45
+ ```ts
46
+ import { workspace } from '@shardflux/sdk';
47
+
48
+ const ws = workspace('customer-42/main', { template: 'default' }); // reads SHARDFLUX_API_KEY; no request yet
49
+
50
+ // 1. Run a command: the first call creates the workspace. Check that it worked.
51
+ const run = await ws.exec('python3 -c "print(40 + 2)"');
52
+ if (run.exitCode !== 0) throw new Error(`python3 exited ${run.exitCode}: ${run.stderr}`);
53
+ console.log(run.stdout.trim()); // 42
54
+
55
+ // 2. Write a file, then suspend the workspace and wait until the suspend has finished.
56
+ await ws.files.write('/home/user/notes.txt', 'hello from the SDK\n');
57
+ await (await ws.open()).suspend({ wait: true }); // resolves once suspended
58
+
59
+ // 3. The same key again: the workspace wakes, and the file is still there.
60
+ const again = workspace('customer-42/main', { template: 'default' });
61
+ console.log(await again.files.readText('/home/user/notes.txt')); // hello from the SDK
62
+ ```
63
+
64
+ The key names the workspace **(0.15.0+)**: the first call creates it, later calls (from any process) reuse it, and it
65
+ suspends when idle and wakes on the next call with its files, packages and processes. There is no create call and no
66
+ lifecycle code to write. See [Workspaces by key](#workspaces-by-key-0150).
67
+
68
+ ### Open, suspend and resume
69
+
70
+ The same workspace with every step explicit:
71
+
44
72
  ```ts
45
73
  import { Shardflux, formatTiming } from '@shardflux/sdk';
46
74
 
@@ -70,6 +98,48 @@ installed packages and running processes are still there. The same program is in
70
98
  With 0.5.0, wait for the suspend by its operation instead:
71
99
  `await cloud.workspaces.waitForOperation((await workspace.suspend()).id)`.
72
100
 
101
+ ## Workspaces by key (0.15.0+)
102
+
103
+ `workspace(key, params)` (or `cloud.workspace(key, params)` on your own client) names a workspace by the identifier your
104
+ application already has: a user, a thread, a repository, a customer. It makes no request. Its first call opens the key:
105
+ the workspace is created from `params.template` on first use and resumed afterwards, in one request that also brings
106
+ back its tool token. Later calls go straight to the workspace.
107
+
108
+ ```ts
109
+ import { workspace } from '@shardflux/sdk';
110
+
111
+ const ws = workspace(`acme/${threadId}`, { template: 'default' });
112
+
113
+ await ws.exec('pip install requests && python3 app.py'); // a string runs through bash -lc
114
+ await ws.exec(['python3', '-c', 'print(42)']); // an argv array runs without a shell
115
+ await ws.files.write('/home/user/notes.txt', 'hello\n');
116
+ console.log(await ws.files.readText('/home/user/notes.txt'));
117
+
118
+ ws.created; // true when this ref's open created the workspace
119
+ const full = await ws.open(); // the Workspace: suspend, fork, ports, computer, capture
120
+ ```
121
+
122
+ - `params` are `open()`'s without the key: `template` is required (`'default'` is the platform default template,
123
+ `python-node-browser`), plus `secrets`, `inputs`, `caps`, `labels`, `idlePolicy`, `lifetime`, `computerUse`,
124
+ `agentLabel`, `tools`.
125
+ - Calls made while the first open runs share it. A failed open is not kept: the next call opens again.
126
+ - `create: false` never creates: the first call finds the key's workspace and fails with 404 `not_found` when it has
127
+ none.
128
+ - A workspace deleted under the ref (`isWorkspaceGone(err)`, 409 `workspace_deleted`) fails the call that meets it;
129
+ the next call opens the key again, which creates a new workspace.
130
+ - `ws.hint()` starts the open early, for example when your model starts a tool call.
131
+
132
+ **In an agent.** The tools exist before the workspace does; the model's first tool call creates it:
133
+
134
+ ```ts
135
+ import { toAnthropicTools, executeToolCall, workspace } from '@shardflux/sdk';
136
+
137
+ const tools = await workspace(`acme/${threadId}`, { template: 'default' }).tools(); // reads the key's grants
138
+ const anthropicTools = toAnthropicTools(tools);
139
+ // for each tool_use block the model returns:
140
+ const output = await executeToolCall(tools, block);
141
+ ```
142
+
73
143
  ## Configuration
74
144
 
75
145
  ```ts
@@ -82,8 +152,9 @@ const cloud = new Shardflux({
82
152
  });
83
153
  ```
84
154
 
85
- The SDK reads nothing from the environment by itself (except the version check's opt-out). `SHARDFLUX_API_KEY` and
86
- `SHARDFLUX_API_URL` are the conventional names (the `shard` CLI reads them); pass them in as shown.
155
+ `new Shardflux()` with no `apiKey` reads `SHARDFLUX_API_KEY`, and with no `baseUrl` reads `SHARDFLUX_API_URL`
156
+ **(0.15.0+)**; options you pass win. A missing key throws when the client is created. The module-level `workspace()`
157
+ uses such a client, created on its first call.
87
158
 
88
159
  ## Commands and files
89
160
 
@@ -451,6 +522,53 @@ const hook = await ws.ports.createCallbackUrl(3000); // register `${h
451
522
  - `close(port)` stops the port's tokens, links and callback URL at once (also when the port is exposed again).
452
523
  - By id, without reading the workspace: `cloud.workspaces.ports(id)`.
453
524
 
525
+ ### Computer use (0.15.0+)
526
+
527
+ A desktop in the workspace for agents that operate GUI software: a screen, a mouse and a keyboard. Switch it on for a
528
+ workspace (or for every workspace of your template); the platform starts the desktop on the first call that needs it.
529
+
530
+ ```ts
531
+ const ws = await cloud.workspaces.open({ key: 'agent/42', template: 'python-node-browser', computerUse: true });
532
+
533
+ const shot = await ws.computer.screenshot(); // { format: 'png', width: 1280, height: 800, data }
534
+ await ws.computer.act([
535
+ { action: 'left_click', coordinate: [640, 400] },
536
+ { action: 'type', text: 'hello' },
537
+ { action: 'key', text: 'Return' },
538
+ ], { screenshot: true }); // one round trip for the whole batch and a look
539
+
540
+ const { url } = await ws.computer.stream(); // a private link to watch the screen live
541
+ ```
542
+
543
+ With Claude, answer the computer toolset directly:
544
+
545
+ ```ts
546
+ import { computerToolset } from '@shardflux/sdk';
547
+
548
+ const computer = computerToolset(ws);
549
+ const msg = await anthropic.messages.create({ model: 'claude-opus-5-5', max_tokens: 16000, tools: [computer.definition], messages });
550
+ messages.push({ role: 'assistant', content: msg.content });
551
+ messages.push({ role: 'user', content: await computer.run(msg.content) });
552
+ ```
553
+
554
+ - The actions are Claude's computer toolset members with their parameters: `screenshot`, `zoom`, `left_click`,
555
+ `right_click`, `middle_click`, `double_click`, `triple_click`, `left_click_drag`, `mouse_move`, `left_mouse_down`,
556
+ `left_mouse_up`, `cursor_position`, `scroll`, `type`, `key`, `hold_key`, `wait`. A batch runs in order and stops at
557
+ the first failure; later actions come back `skipped`.
558
+ - `computer.run(content)` sends every computer call of a model turn as one batch and returns one `tool_result` per call
559
+ (each with `toolset_name: "computer"`), with a screenshot on the last one when the turn did not end with a look.
560
+ - `workspaceTools()` includes, for any model while the workspace's computer use is on, a `computer` tool (one action
561
+ per call, answered with a screenshot) and `computer_batch` (several actions in one call, with each action's result
562
+ and one screenshot after them). Both take `screenshot: false` to skip the image, `settle_ms`, and `format: "jpeg"`
563
+ with `quality` for smaller images.
564
+ - `act(actions, { screenshot, settleMs, format, quality })` takes the same options.
565
+ - Commands the agent runs see the desktop: `exec` of `chromium https://example.com &` opens the browser on it.
566
+ - `stream({ interactive })` returns a signed link (view only by default; `interactive: true` lets the viewer use the
567
+ mouse and keyboard); `stopStream()` ends every view. Streaming uses inbound ports.
568
+ - `ws.setComputerUse(true | false | null)` sets the workspace's own switch (null follows the template);
569
+ `ws.computerUse` reads `{ enabled, workspace, template, available }`. `status()`, `start({ width, height })` and
570
+ `stop()` manage the desktop directly.
571
+
454
572
  ### Timing and progress (0.6.0+)
455
573
 
456
574
  Every open, wake and waited lifecycle call is traced. `workspace.lastTiming` (and `err.timing` when the call fails)
@@ -710,7 +828,9 @@ it: add paths, never remove them. A build reports the list its version declares
710
828
  ## Agent tools
711
829
 
712
830
  `workspaceTools(workspace)` returns tools with a name, a description, a JSON Schema for the
713
- parameters and an `execute` function. Export them for your model provider and dispatch its tool
831
+ parameters and an `execute` function. It takes a `Workspace` or **(0.15.0+)** a `WorkspaceRef`, whose
832
+ `await ref.tools(opts)` builds them from the API key's grants before the workspace exists (see
833
+ [Workspaces by key](#workspaces-by-key-0150)). Export them for your model provider and dispatch its tool
714
834
  calls:
715
835
 
716
836
  ```ts
@@ -735,7 +855,8 @@ Your agent loop and model calls stay in your application; the workspace is the c
735
855
  act on.
736
856
 
737
857
  The tools are `exec`, `read_file`, `write_file`, `list_files`, `search_files` and `edit_file` (0.9.0+), the process,
738
- terminal, git and browser tools, filtered by the tools your key grants. `edit_file` replaces exact text; when the
858
+ terminal, git and browser tools, and `computer` (0.15.0+, while the workspace's computer use is on), filtered by the
859
+ tools your key grants. `edit_file` replaces exact text; when the
739
860
  model passes no `expected_revision` it reads the file's revision first, so a change made in between fails the edit
740
861
  instead of being overwritten. Each call first sends `workspace.hint()` without waiting for it (`hint: false` turns
741
862
  that off, e.g. when you send the hint yourself as the model starts a tool call), except `read_file`, `list_files` and
package/dist/cell.d.ts CHANGED
@@ -147,6 +147,16 @@ export type GitCommitRequest = S['GitCommitRequest'];
147
147
  export type GitResult = S['GitResult'];
148
148
  export type GitStatus = S['GitStatus'];
149
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'];
150
160
  export type BrowserContentRequest = S['BrowserContentRequest'];
151
161
  export type BrowserContent = S['BrowserContent'];
152
162
  export type Signal = S['SignalValue'];
@@ -530,6 +540,21 @@ export declare class CellClient {
530
540
  status: (path: string) => Promise<GitStatus>;
531
541
  commit: (req: GitCommitRequest) => Promise<GitResult>;
532
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
+ };
533
558
  readonly browser: {
534
559
  screenshot: (req: BrowserScreenshotRequest) => Promise<Uint8Array>;
535
560
  content: (req: BrowserContentRequest) => Promise<BrowserContent>;
package/dist/cell.js CHANGED
@@ -1065,6 +1065,75 @@ export class CellClient {
1065
1065
  return this.#json('POST', this.#p('/v1/workspaces/{workspace_id}/git/commit'), { json: req });
1066
1066
  },
1067
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
+ };
1068
1137
  // ---- browser ---------------------------------------------------------------------------
1069
1138
  browser = {
1070
1139
  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;
@@ -260,6 +264,12 @@ export interface OpenParams {
260
264
  /** Searchable metadata; supplied labels replace the existing map. */
261
265
  labels?: Record<string, string>;
262
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;
263
273
  key: string;
264
274
  template: string;
265
275
  caps?: Caps;
@@ -403,6 +413,8 @@ export declare class WorkspacesApi {
403
413
  /** Replace labels. An empty map clears them. */
404
414
  setLabels(workspaceId: string, labels: Record<string, string>): Promise<Workspace>;
405
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>;
406
418
  setIdlePolicy(workspaceId: string, idlePolicy: IdlePolicy | null): Promise<Workspace>;
407
419
  list(params?: ListParams): Promise<Page<Workspace>>;
408
420
  /** Iterates every page. */
@@ -585,7 +597,14 @@ export declare class Shardflux {
585
597
  readonly audit: AuditApi;
586
598
  /** Shared volumes: persistent storage attached to workspaces at a mount path. */
587
599
  readonly volumes: VolumesApi;
588
- constructor(opts: ShardfluxOptions);
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;
589
608
  /** The authenticated principal (the API key, its organization and project). */
590
609
  me(): Promise<Me>;
591
610
  entitlements(organizationId: string): Promise<Entitlements>;
@@ -601,4 +620,10 @@ export declare class Shardflux {
601
620
  /** Raw access to any /v1 endpoint with the SDK's authentication and error handling. */
602
621
  request<T>(method: string, path: string, init?: Parameters<HttpClient['json']>[2]): Promise<T>;
603
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;
604
629
  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";
@@ -165,6 +166,8 @@ export class WorkspacesApi {
165
166
  body.labels = params.labels;
166
167
  if (params.idlePolicy !== undefined)
167
168
  body.idle_policy = params.idlePolicy;
169
+ if (params.computerUse !== undefined)
170
+ body.computer_use = params.computerUse;
168
171
  if (params.lifetime !== undefined)
169
172
  body.lifetime = params.lifetime;
170
173
  if (params.mode !== undefined)
@@ -193,7 +196,9 @@ export class WorkspacesApi {
193
196
  if (res.body.operation)
194
197
  trace.observe(res.body.operation);
195
198
  this.#noteToken(res.body.tool_token);
196
- const wrapOpts = { agentLabel: params.agentLabel, tools: params.tools, token: res.body.tool_token, trace };
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 };
197
202
  if (res.status === 200 || params.wait === false || res.body.operation === null)
198
203
  return this.#wrap(res.body.workspace, wrapOpts);
199
204
  if (TERMINAL.has(res.body.operation.state)) {
@@ -390,6 +395,10 @@ export class WorkspacesApi {
390
395
  return this.#wrap(await this.#http.json('PUT', `/v1/workspaces/${encodeURIComponent(workspaceId)}/labels`, { json: { labels } }, this.#auth));
391
396
  }
392
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
+ }
393
402
  async setIdlePolicy(workspaceId, idlePolicy) {
394
403
  return this.#wrap(await this.#http.json('PUT', `/v1/workspaces/${encodeURIComponent(workspaceId)}/idle-policy`, { json: { idle_policy: idlePolicy } }, this.#auth));
395
404
  }
@@ -793,8 +802,13 @@ export class Shardflux {
793
802
  /** Shared volumes: persistent storage attached to workspaces at a mount path. */
794
803
  volumes;
795
804
  #ctx;
796
- constructor(opts) {
797
- if (!/^sfk_[a-z2-7]{16}_[A-Za-z0-9]+$/.test(opts.apiKey))
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))
798
812
  throw new Error('apiKey must be a Shardflux project key (sfk_<key_id>_<secret>)');
799
813
  const f = opts.fetch ?? defaultFetch();
800
814
  const userAgent = opts.userAgent ?? `shardflux-sdk-ts/${SDK_VERSION}`;
@@ -807,10 +821,10 @@ export class Shardflux {
807
821
  this.egress = new EgressPolicyApi(() => this.#ctx);
808
822
  this.audit = new AuditApi(() => this.#ctx);
809
823
  this.volumes = new VolumesApi(() => this.#ctx);
810
- const baseUrl = opts.baseUrl ?? 'https://api.shardflux.dev';
824
+ const baseUrl = opts.baseUrl ?? envVar('SHARDFLUX_API_URL') ?? 'https://api.shardflux.dev';
811
825
  this.#ctx = {
812
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) }),
813
- authorization: `Bearer ${opts.apiKey}`,
827
+ authorization: `Bearer ${apiKey}`,
814
828
  fetch: f,
815
829
  userAgent,
816
830
  sleep,
@@ -819,6 +833,14 @@ export class Shardflux {
819
833
  captures: new CaptureRegistry(),
820
834
  };
821
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
+ }
822
844
  /** The authenticated principal (the API key, its organization and project). */
823
845
  me() {
824
846
  return this.#ctx.http.json('GET', '/v1/me', {}, this.#ctx.authorization);
@@ -842,4 +864,34 @@ export class Shardflux {
842
864
  return this.#ctx.http.json(method, path, init, this.#ctx.authorization);
843
865
  }
844
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
+ }
845
897
  export { ShardfluxApiError };