@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 +45 -3
- package/README.md +127 -6
- package/dist/cell.d.ts +25 -0
- package/dist/cell.js +69 -0
- package/dist/client.d.ts +29 -4
- package/dist/client.js +57 -5
- package/dist/computer.d.ts +143 -0
- package/dist/computer.js +146 -0
- package/dist/generated/app-api.d.ts +452 -68
- package/dist/generated/cell-api.d.ts +332 -0
- package/dist/http.d.ts +1 -1
- package/dist/http.js +1 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +4 -1
- package/dist/templates.d.ts +12 -0
- package/dist/templates.js +9 -0
- package/dist/tools.d.ts +16 -3
- package/dist/tools.js +111 -22
- package/dist/workspace-ref.d.ts +83 -0
- package/dist/workspace-ref.js +149 -0
- package/dist/workspace.d.ts +23 -1
- package/dist/workspace.js +36 -0
- package/package.json +1 -1
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.
|
|
14
|
-
> **(0.
|
|
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
|
-
|
|
86
|
-
|
|
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.
|
|
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,
|
|
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
|
|
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;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
797
|
-
|
|
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 ${
|
|
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 };
|