@shardflux/sdk 0.5.0 → 0.6.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 +63 -0
- package/README.md +228 -24
- package/dist/cell.d.ts +67 -2
- package/dist/cell.js +98 -8
- package/dist/client.d.ts +148 -22
- package/dist/client.js +232 -29
- package/dist/errors.d.ts +20 -0
- package/dist/errors.js +10 -0
- package/dist/generated/app-api.d.ts +10223 -5857
- package/dist/generated/cell-api.d.ts +140 -8
- package/dist/http.d.ts +30 -1
- package/dist/http.js +57 -3
- package/dist/index.d.ts +13 -9
- package/dist/index.js +5 -4
- package/dist/lifecycle.d.ts +48 -0
- package/dist/lifecycle.js +33 -0
- package/dist/progress.d.ts +166 -0
- package/dist/progress.js +238 -0
- package/dist/secrets.d.ts +83 -6
- package/dist/secrets.js +53 -1
- package/dist/templates.d.ts +279 -7
- package/dist/templates.js +216 -4
- package/dist/tokens.d.ts +8 -3
- package/dist/tokens.js +25 -13
- package/dist/tools.d.ts +17 -0
- package/dist/tools.js +5 -1
- package/dist/workspace.d.ts +112 -20
- package/dist/workspace.js +176 -10
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Every API the README shows is available from the version named here. Below 1.0, a minor release may break
|
|
4
|
+
compatibility; breaking changes are marked **Breaking**.
|
|
5
|
+
|
|
6
|
+
## 0.6.0 (not yet published; npm `latest` is 0.5.0)
|
|
7
|
+
|
|
8
|
+
### Lifecycle calls: requested or finished
|
|
9
|
+
|
|
10
|
+
- `suspend`, `resume`, `snapshot`, `fork`, `delete`, `reset` and `close` take `wait`. Without it they resolve when the
|
|
11
|
+
change is **requested** (the operation is usually still `queued`), as before. With `{ wait: true }` (or
|
|
12
|
+
`WaitOptions`) they resolve when it has **finished**: they return the succeeded operation (typed
|
|
13
|
+
`FinishedOperation`) and, on a workspace handle, refresh it, so `workspace.state` is `suspended` after
|
|
14
|
+
`await workspace.suspend({ wait: true })`. A failed operation throws `OperationFailedError`; running out of time
|
|
15
|
+
throws `OperationTimeoutError` (the operation continues).
|
|
16
|
+
|
|
17
|
+
### Lifecycle timing and progress
|
|
18
|
+
|
|
19
|
+
- `onProgress` on `new Shardflux()`, `workspaces.open()`, `waitForOperation()`, `wake()`, every lifecycle call and
|
|
20
|
+
`workspace.cell()`. Events: `phase` (the request, each observed operation state with the server's reason such as
|
|
21
|
+
`capacity_pending` / `no_ready_host`, the view read, the first tool token), `retry` (a transient failure retried,
|
|
22
|
+
with its cause and backoff), and `done` with the call's `LifecycleTiming`.
|
|
23
|
+
- `LifecycleTiming` separates the client's phases from the operation's own server timing (`queuedMs`, `runMs`,
|
|
24
|
+
start or resume path, boot to ready, host restore steps) and reports `outsideServerMs`: time spent outside the
|
|
25
|
+
operation (network, TLS, polling, view and token).
|
|
26
|
+
- `workspace.lastTiming` (open, wake, waited lifecycle calls); `timing` on `OperationTimeoutError`,
|
|
27
|
+
`OperationFailedError` and `ShardfluxApiError` when a traced call fails; `formatTiming()` prints a timing.
|
|
28
|
+
- Tool token fetches are traced (action `token`, reason `initial`, `expiring` or `invalidated`), and tool calls report
|
|
29
|
+
`workspace_busy` waits, replaced stale tokens and retries (action `tool`).
|
|
30
|
+
- Needs an API that reports `started_at` on operations for `queuedMs` / `runMs`; with an older API they are null.
|
|
31
|
+
|
|
32
|
+
### Faster opens and waits
|
|
33
|
+
|
|
34
|
+
- `open()` holds the request on the server until the workspace is ready (`Prefer: wait`, up to 20 s per request) and
|
|
35
|
+
receives the first tool token in the same response; it pre-connects to the workspace's cell meanwhile.
|
|
36
|
+
- `waitForOperation()` and `templates.builds.waitForBuild()` use server-held polls (at most 20 s per request) and
|
|
37
|
+
fall back to backoff (250 ms doubling to 5 s, ±20 % jitter) against a server without them.
|
|
38
|
+
- On Node 26 the default `fetch` sends `Connection: close` (its bundled undici 8 can stall a request on a reused
|
|
39
|
+
keep-alive connection for tens of seconds). `SHARDFLUX_HTTP_KEEPALIVE=1` or your own `fetch` changes that.
|
|
40
|
+
|
|
41
|
+
### Suspended workspaces wake on use
|
|
42
|
+
|
|
43
|
+
- A tool call on a suspended workspace resumes it (or joins the resume or open already running) and then runs,
|
|
44
|
+
bounded by `transitionTimeoutMs` (default 120 000 ms); calls during a suspend or resume wait for it.
|
|
45
|
+
`workspace.wake()` does the same on demand; `workspace.cell({ wake: null })` opts out.
|
|
46
|
+
|
|
47
|
+
### Workspaces
|
|
48
|
+
|
|
49
|
+
- Secret bindings: `open({ secrets })`, `workspace.secrets.get()/set()`; `cloud.secrets` gained
|
|
50
|
+
`createOrganization`, `listOrganization` and `accessEvents`.
|
|
51
|
+
- Sessions: `open({ lifetime: 'session' })`, `workspace.close()`, `idleTimeoutSeconds`, `endedReason`.
|
|
52
|
+
- Layered workspaces: `workspace.reset()`, `saveAsTemplate()`, `changes()`, `diskLayout`, `origin`.
|
|
53
|
+
- `workspaces.findByKey()` (live workspace first, then tombstones), `list({ lifetime, purpose })`, `pickByKey()`.
|
|
54
|
+
- Templates: `templates.files()`, `fileEntry()`, `diff()`, `diffAll()`, and dev mode (`templates.draft(slug)`:
|
|
55
|
+
create, capture state, test instances, publish, discard).
|
|
56
|
+
- `ShardfluxApiError.reason` (`details.reason`), with `KnownErrorReason`.
|
|
57
|
+
|
|
58
|
+
## 0.5.0 (2026-09-26)
|
|
59
|
+
|
|
60
|
+
First public release: `Shardflux` client, `workspaces.open/get/list/listAll`, lifecycle calls returning operations,
|
|
61
|
+
`waitForOperation()`, the cell client (`exec`, `files`, `pty`, `processes`, `git`, `browser`), `workspaceTools()` with
|
|
62
|
+
`toOpenAITools()` / `toAnthropicTools()` / `executeToolCall()`, secrets, egress, usage, billing, volumes, templates and
|
|
63
|
+
custom template builds.
|
package/README.md
CHANGED
|
@@ -9,6 +9,10 @@ tools (exec, files, processes, PTY, git, browser) that plug into any model provi
|
|
|
9
9
|
> **Early access.** Shardflux is in early access. The API is versioned (`/v1`), but this SDK is
|
|
10
10
|
> below 1.0: a minor release may contain breaking changes (see [Compatibility](#compatibility)).
|
|
11
11
|
|
|
12
|
+
> **Versions.** This README describes 0.6.0. Anything marked **(0.6.0+)** is not in 0.5.0;
|
|
13
|
+
> [CHANGELOG.md](./CHANGELOG.md) lists what each version added. Check yours with
|
|
14
|
+
> `npm ls @shardflux/sdk` or the exported `SDK_VERSION`.
|
|
15
|
+
|
|
12
16
|
- ESM only, no runtime dependencies, Node.js 24 or later.
|
|
13
17
|
- Typed from the published OpenAPI documents.
|
|
14
18
|
- Retries, idempotency keys, operation polling and tool-token refresh are handled for you.
|
|
@@ -25,23 +29,33 @@ Create a project API key in the Shardflux console (`sfk_<key id>_<secret>`) and
|
|
|
25
29
|
server. API keys are server credentials: never put one in a browser bundle.
|
|
26
30
|
|
|
27
31
|
```ts
|
|
28
|
-
import { Shardflux } from '@shardflux/sdk';
|
|
32
|
+
import { Shardflux, formatTiming } from '@shardflux/sdk';
|
|
29
33
|
|
|
30
34
|
const cloud = new Shardflux({ apiKey: process.env.SHARDFLUX_API_KEY! });
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
console.log(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
const open = () => cloud.workspaces.open({ key: 'customer-42/main', template: 'python-node-browser' });
|
|
36
|
+
|
|
37
|
+
// 1. Open the workspace (created on first use) and run a command. Check that it worked.
|
|
38
|
+
const workspace = await open();
|
|
39
|
+
const run = await workspace.cell().exec.run(['python3', '-c', 'print(40 + 2)']);
|
|
40
|
+
if (run.exitCode !== 0) throw new Error(`python3 exited ${run.exitCode}: ${run.stderr}`);
|
|
41
|
+
console.log(run.stdout.trim()); // 42
|
|
42
|
+
|
|
43
|
+
// 2. Write a file, then suspend the workspace and wait until the suspend has finished.
|
|
44
|
+
await workspace.cell().files.write('/home/user/notes.txt', 'hello from the SDK\n');
|
|
45
|
+
await workspace.suspend({ wait: true }); // (0.6.0+) resolves once suspended
|
|
46
|
+
|
|
47
|
+
// 3. Open the same key again: the workspace resumes, and the file is still there.
|
|
48
|
+
const again = await open();
|
|
49
|
+
console.log(await again.cell().files.readText('/home/user/notes.txt')); // hello from the SDK
|
|
50
|
+
console.log(formatTiming(again.lastTiming!)); // (0.6.0+) where the resume's time went
|
|
41
51
|
```
|
|
42
52
|
|
|
43
53
|
`open()` waits until the workspace is running. Opening the same key again never resets it: files,
|
|
44
|
-
installed packages and running processes are still there.
|
|
54
|
+
installed packages and running processes are still there. The same program is in
|
|
55
|
+
[`examples/quickstart.ts`](./examples/quickstart.ts).
|
|
56
|
+
|
|
57
|
+
With 0.5.0, wait for the suspend by its operation instead:
|
|
58
|
+
`await cloud.workspaces.waitForOperation((await workspace.suspend()).id)`.
|
|
45
59
|
|
|
46
60
|
## Configuration
|
|
47
61
|
|
|
@@ -90,19 +104,56 @@ The same client has `exec.start/get/output/signal/cancel`, `pty`, `processes`, `
|
|
|
90
104
|
## Lifecycle
|
|
91
105
|
|
|
92
106
|
```ts
|
|
93
|
-
await workspace.suspend();
|
|
94
|
-
await workspace.resume();
|
|
107
|
+
await workspace.suspend({ wait: true }); // memory and processes are checkpointed; resolves once suspended
|
|
108
|
+
await workspace.resume({ wait: true }); // or simply open() the key again
|
|
95
109
|
|
|
96
|
-
const {
|
|
97
|
-
await cloud.workspaces.waitForOperation(operation.id);
|
|
110
|
+
const { workspace: copy } = await workspace.fork({ key: 'customer-42/experiment' }, { wait: true });
|
|
98
111
|
|
|
99
112
|
await copy.delete(); // tool access ends immediately; keys are never reused
|
|
100
113
|
```
|
|
101
114
|
|
|
115
|
+
### Requested or finished
|
|
116
|
+
|
|
117
|
+
`suspend`, `resume`, `snapshot`, `fork`, `delete`, `reset` and `close` start a lifecycle operation. What the promise
|
|
118
|
+
means depends on `wait`:
|
|
119
|
+
|
|
120
|
+
| Call | Resolves when | Returns |
|
|
121
|
+
| --- | --- | --- |
|
|
122
|
+
| `await workspace.suspend()` | the suspend is **requested** (usually `queued`; the workspace is still running) | the operation (`Operation`) |
|
|
123
|
+
| `await workspace.suspend({ wait: true })` **(0.6.0+)** | the suspend has **finished** (`workspace.state` is then `suspended`) | the succeeded operation (`FinishedOperation`) |
|
|
124
|
+
|
|
125
|
+
`wait` also takes `WaitOptions` (`timeoutMs`, default 5 minutes; `signal`; `onProgress`). A failed operation throws
|
|
126
|
+
`OperationFailedError`. Running out of time throws `OperationTimeoutError`, and the operation continues server side:
|
|
127
|
+
wait again with `cloud.workspaces.waitForOperation(err.operationId)`. Without `wait`, the returned operation is the
|
|
128
|
+
handle for the work in progress: pass its `id` to `waitForOperation()` when you need it finished.
|
|
129
|
+
|
|
130
|
+
**Suspended workspaces wake on use.** A tool call on a suspended workspace resumes it (or joins the resume or open
|
|
131
|
+
already running), then runs. A call made during a suspend or resume waits for the transition to finish. The call
|
|
132
|
+
never runs twice: the cell executes nothing it refused.
|
|
133
|
+
|
|
134
|
+
- The wait is bounded per call by `transitionTimeoutMs` (default 120 000 ms), shared by the transition waits and at
|
|
135
|
+
most 3 wakes. A resume or open still pending at the end throws `OperationTimeoutError`, naming the operation, its
|
|
136
|
+
state and reason. A failed resume or open throws `OperationFailedError` at once.
|
|
137
|
+
- Following an exec's output never wakes a workspace, so an explicit `suspend()` is respected.
|
|
138
|
+
- `workspace.wake({ timeoutMs })` does the same on demand.
|
|
139
|
+
- `workspace.cell({ wake: null })` returns `workspace_not_running` instead.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
const cell = workspace.cell({ transitionTimeoutMs: 30_000 }); // give up waking after 30 s
|
|
143
|
+
await cell.exec.run(['make', 'test']); // resumes the workspace first if it is suspended
|
|
144
|
+
```
|
|
145
|
+
|
|
102
146
|
`suspend`, `resume`, `fork`, `snapshot` and `delete` return the lifecycle operation.
|
|
103
|
-
`cloud.workspaces.waitForOperation(id)`
|
|
104
|
-
|
|
105
|
-
|
|
147
|
+
`cloud.workspaces.waitForOperation(id)` waits for it (default timeout 5 minutes): each poll asks the API to hold
|
|
148
|
+
the response until the operation changes (`Prefer: wait`, at most 20 s per request), so completion arrives within
|
|
149
|
+
one round trip of the commit. Against an API without bounded waits (or with `serverWait: false`) it polls with
|
|
150
|
+
backoff (250 ms doubling to 5 s, ±20 % jitter). If the timeout passes, it throws `OperationTimeoutError` and the
|
|
151
|
+
operation keeps running server side; wait for it again with the same call. `templates.builds.waitForBuild()` waits
|
|
152
|
+
the same way. A waited `open()` issues the first tool token together with the final workspace read, so the first
|
|
153
|
+
tool call starts at once.
|
|
154
|
+
|
|
155
|
+
On Node 26 the default fetch sends `Connection: close`: its bundled undici 8 can stall a request on a reused
|
|
156
|
+
keep-alive connection for tens of seconds. Pass your own `fetch`, or set `SHARDFLUX_HTTP_KEEPALIVE=1`, to change that.
|
|
106
157
|
|
|
107
158
|
List and look up workspaces:
|
|
108
159
|
|
|
@@ -110,8 +161,128 @@ List and look up workspaces:
|
|
|
110
161
|
const page = await cloud.workspaces.list({ keyPrefix: 'customer-42/' });
|
|
111
162
|
for await (const ws of cloud.workspaces.listAll()) console.log(ws.key, ws.state);
|
|
112
163
|
const same = await cloud.workspaces.get(workspace.id);
|
|
164
|
+
const byKey = await cloud.workspaces.findByKey('customer-42/main'); // null when no workspace has the key
|
|
113
165
|
```
|
|
114
166
|
|
|
167
|
+
`list()` shows persistent standard workspaces by default. Pass `lifetime`
|
|
168
|
+
(`persistent` | `session` | `any`) and `purpose` (`standard` | `template_draft` | `template_test` | `any`) to see
|
|
169
|
+
sessions, template drafts and test instances. `findByKey()` searches every lifetime and purpose. It returns the live
|
|
170
|
+
workspace, and a tombstone only when no live workspace has the key. Use it rather than `listAll({ keyPrefix })` for
|
|
171
|
+
lookups by key.
|
|
172
|
+
|
|
173
|
+
### Timing and progress (0.6.0+)
|
|
174
|
+
|
|
175
|
+
Every open, wake and waited lifecycle call is traced. `workspace.lastTiming` (and `err.timing` when the call fails)
|
|
176
|
+
says where the time went, and `formatTiming()` prints it:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
const ws = await cloud.workspaces.open({ key: 'customer-42/main', template: 'python-node-browser' });
|
|
180
|
+
console.log(formatTiming(ws.lastTiming!));
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
A slow open (34 s instead of the usual second) then reads, for example:
|
|
184
|
+
|
|
185
|
+
```text
|
|
186
|
+
open 34.18 s, succeeded (workspace 01a0e5a8-3edd-74ba-b489-d62b8925e342, operation 01a0e5a8-3ef0-7ecb-975e-dff2d5ca6e33)
|
|
187
|
+
client: request 20.01 s (held) → capacity_pending 13.52 s (no_ready_host) → running 0.59 s → view 42 ms ∥ token 61 ms
|
|
188
|
+
server: queued 33.40 s, ran 0.62 s, total 34.02 s; start warm, boot to ready 79 ms
|
|
189
|
+
outside the server: 0.16 s
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Here the time went to waiting for a host with capacity; the network and the VM start were fast.
|
|
193
|
+
|
|
194
|
+
- **client** phases are on your monotonic clock: the request (a held open waits on the server, `held`), then each
|
|
195
|
+
operation state the SDK observed while waiting, with the server's reason (`capacity_pending` / `no_ready_host`:
|
|
196
|
+
waiting for a host; `running` / `template_downloading`: fetching the template), then reading the workspace and
|
|
197
|
+
issuing the first tool token (together).
|
|
198
|
+
- **server** timing comes from the operation itself (one database clock): `queued` is creation until it began
|
|
199
|
+
running, including any wait for capacity; `ran` is the cell's work (placement, boot or restore, guest readiness).
|
|
200
|
+
`start` / `resume from` and `boot to ready` / `host …` are what the cell reported.
|
|
201
|
+
- **outside the server** is your total minus the operation's: network, TLS, polling latency, view and token. A large
|
|
202
|
+
value with a small server total points at the connection between you and the API, not at the workspace.
|
|
203
|
+
- **retries** lists transient failures the SDK retried (cause and backoff).
|
|
204
|
+
|
|
205
|
+
Watch it live with `onProgress` (on the client for every call, or per call):
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
const cloud = new Shardflux({
|
|
209
|
+
apiKey: process.env.SHARDFLUX_API_KEY!,
|
|
210
|
+
onProgress: (e) => {
|
|
211
|
+
if (e.type === 'phase') console.error(`${e.action}: ${e.phase}${e.reason ? ` (${e.reason})` : ''} at ${e.atMs} ms`);
|
|
212
|
+
if (e.type === 'retry') console.error(`${e.action}: retry ${e.retry.request}: ${e.retry.cause}`);
|
|
213
|
+
if (e.type === 'done') console.error(formatTiming(e.timing));
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Events are `phase` (a phase began), `retry` and `done` (with the `LifecycleTiming`). Tool calls add `token` traces
|
|
219
|
+
(a tool token fetched: `initial`, `expiring` or `invalidated`) and `tool` events (a `busy` wait, a stale token
|
|
220
|
+
replaced, a retry). `queued`/`ran` need an API that reports the operation's `started_at`; otherwise they are null.
|
|
221
|
+
|
|
222
|
+
### Sessions
|
|
223
|
+
|
|
224
|
+
A session workspace is discarded when its session ends. The session ends on `close()`, or after it has been idle for
|
|
225
|
+
the idle timeout (10 minutes unless the template sets one). The key then opens a new, empty workspace with a new id.
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
const ws = await cloud.workspaces.open({ key: `job-${jobId}`, template: 'python-node-browser', lifetime: 'session' });
|
|
229
|
+
try {
|
|
230
|
+
await ws.cell().exec.run(['python3', 'job.py']);
|
|
231
|
+
} finally {
|
|
232
|
+
await ws.close(); // sessions: ends it (returns the delete operation); persistent: no request, returns null
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
- `close()` is safe in `finally` for any workspace. It always aborts the handle's local streams (exec output, PTY
|
|
237
|
+
reads and in-flight cell requests); commands keep running. Only for a session does it call
|
|
238
|
+
`POST /v1/workspaces/{id}/close`.
|
|
239
|
+
- Disconnecting, a closed WebSocket or an expired token never ends a session.
|
|
240
|
+
- A session cannot be suspended (409, `details.reason` `session_lifetime`). Fork it to keep its state. `fork()`
|
|
241
|
+
takes its own `lifetime` (default `persistent`).
|
|
242
|
+
- Reopening a live key with a different `lifetime` is 409 `lifetime_mismatch`.
|
|
243
|
+
|
|
244
|
+
### Reset, save as template, changes (layered workspaces)
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
await ws.reset(); // wipes every change; restarts on the template (7-day recovery point)
|
|
248
|
+
const { operation, build } = await ws.saveAsTemplate({ templateSlug: 'acme-dev', description: 'deps installed' });
|
|
249
|
+
await cloud.templates.builds.waitForBuild(build.organization_id, build.id);
|
|
250
|
+
const page = await ws.changes({ pathPrefix: '/home/user', summary: true }); // added | modified | metadata | deleted | replaced
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
`ws.diskLayout` says whether a workspace is `layered`. A legacy workspace is refused with 409
|
|
254
|
+
`legacy_disk_layout`. `changes()` is served by the cell from the running workspace and needs the `files` tool.
|
|
255
|
+
`cell().changesAll()` follows the pages.
|
|
256
|
+
|
|
257
|
+
## Templates: file tree, diff and dev mode
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
const dir = await cloud.templates.files('python-node-browser', 5, { path: '/usr/local/bin' });
|
|
261
|
+
const entry = await cloud.templates.fileEntry('acme-dev', 3, '/home/user/.bashrc');
|
|
262
|
+
const diff = await cloud.templates.diff('acme-dev', { from: 2, to: 3 }); // diff.summary on the first page; from: 'base' too
|
|
263
|
+
for await (const d of cloud.templates.diffAll('acme-dev', { from: 2, to: 3 })) console.log(d.change, d.path);
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Versions published before file lists answer 409 `file_list_unavailable`. A version that is still being indexed
|
|
267
|
+
answers `file_list_indexing` (retryable).
|
|
268
|
+
|
|
269
|
+
Develop an organization template in a draft: a layered workspace you edit live.
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
const draft = cloud.templates.draft('acme-dev');
|
|
273
|
+
const { workspace } = await draft.create({ base: 'python-node-browser@5' }); // one live draft per template
|
|
274
|
+
await workspace.cell().exec.run(['bash', '-lc', 'npm ci']);
|
|
275
|
+
await draft.captureState({ label: 'deps installed' });
|
|
276
|
+
const test = await draft.openTestInstance(); // a session on a copy of the state; the draft never sees its writes
|
|
277
|
+
await test.close();
|
|
278
|
+
const { build } = await draft.publish({ description: 'npm ci' }); // 409 draft_stale if the template moved on
|
|
279
|
+
await draft.discard(); // deletes the draft and ends its test instances
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
`draft.get()` throws 404 `draft_not_found` when there is no draft. `draft.find()` returns null instead.
|
|
283
|
+
`states()`, `statesAll()` and `testInstances({ includeEnded })` list the draft's states and test instances. Only
|
|
284
|
+
owners, admins and API keys with a tool permission may change drafts; others get 403 `template_dev_mode_role`.
|
|
285
|
+
|
|
115
286
|
## Agent tools
|
|
116
287
|
|
|
117
288
|
`workspaceTools(workspace)` returns tools with a name, a description, a JSON Schema for the
|
|
@@ -131,21 +302,52 @@ const output = await executeToolCall(tools, { name: call.name, input: call.input
|
|
|
131
302
|
Your agent loop and model calls stay in your application; the workspace is the computer the tools
|
|
132
303
|
act on.
|
|
133
304
|
|
|
305
|
+
## Secrets
|
|
306
|
+
|
|
307
|
+
Store credentials once and give them to a workspace's processes as environment variables. Values
|
|
308
|
+
are write-only: no API returns them. Every exec and terminal in the workspace receives the secrets
|
|
309
|
+
bound to it, plus any the call names in `secretRefs`.
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
const project = (await cloud.me()).api_key!.project_id;
|
|
313
|
+
await cloud.secrets.create(project, { name: 'OPENAI_API_KEY', value: process.env.OPENAI_API_KEY! });
|
|
314
|
+
|
|
315
|
+
// Bind by name when opening (a new key gets the binding; an existing key has it replaced).
|
|
316
|
+
const workspace = await cloud.workspaces.open({ key: 'customer-42/main', template: 'python-node-browser', secrets: ['OPENAI_API_KEY'] });
|
|
317
|
+
|
|
318
|
+
await workspace.secrets.get(); // { names, secrets: [{ name, status, ... }] }
|
|
319
|
+
await workspace.secrets.set(['OPENAI_API_KEY', 'DATABASE_URL']); // replace; [] clears
|
|
320
|
+
await workspace.cell().exec.run(['python3', 'agent.py']); // sees $OPENAI_API_KEY and $DATABASE_URL
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
- A name that is unknown, or a secret this workspace may not use, is refused with
|
|
324
|
+
`ShardfluxApiError` 422 (`details.reason` `secret_not_available`, `details.names`); nothing
|
|
325
|
+
changes. A bound secret must allow the `exec` and `pty` tools (the default).
|
|
326
|
+
- `status` per bound name: `available`, `not_allowed` (its permissions no longer cover this
|
|
327
|
+
workspace; starts are refused with 403 `secret_not_available` until fixed) or `deleted`.
|
|
328
|
+
- Deleting a secret removes it from every binding. Forks keep the binding, but secrets limited to
|
|
329
|
+
specific workspaces are checked against the fork's own id.
|
|
330
|
+
- A bound name that is also passed in `env` is refused (422).
|
|
331
|
+
- `cloud.secrets` also has `list`, `get`, `update`, `rotate`, `versions`, `delete`,
|
|
332
|
+
`accessEvents`, `createOrganization` and `listOrganization`. Organization-wide secrets and access
|
|
333
|
+
logs belong to organization owners and admins, so a project API key gets 403 for those.
|
|
334
|
+
|
|
134
335
|
## Errors
|
|
135
336
|
|
|
136
337
|
- `ShardfluxApiError`: the API or the workspace refused the request. Fields: `status`, `code`,
|
|
137
|
-
`message`, `requestId`, `retryable`, `details`, `operationId`, `retryAfterSeconds
|
|
338
|
+
`message`, `requestId`, `retryable`, `details`, `operationId`, `retryAfterSeconds`, and `reason`
|
|
339
|
+
(`details.reason`, e.g. `not_session`, `draft_not_found`, `legacy_disk_layout`; see `KnownErrorReason`).
|
|
138
340
|
- `OperationFailedError`: an awaited operation ended `failed` or `canceled` (`errorCode`,
|
|
139
|
-
`operation`).
|
|
140
|
-
- `OperationTimeoutError`: waiting gave up; the operation continues (`operationId`).
|
|
341
|
+
`operation`, `timing`).
|
|
342
|
+
- `OperationTimeoutError`: waiting gave up; the operation continues (`operationId`, `timing`).
|
|
141
343
|
- `ShardfluxProtocolError`: a response was not the documented shape.
|
|
142
344
|
|
|
143
|
-
Treat unknown error codes as generic errors: show `message`, and use `retryable`.
|
|
345
|
+
Treat unknown error codes and reasons as generic errors: show `message`, and use `retryable`.
|
|
144
346
|
|
|
145
347
|
## More of the API
|
|
146
348
|
|
|
147
349
|
The `Shardflux` object also has `templates` (including custom template builds), `volumes`
|
|
148
|
-
(shared persistent storage attached to workspaces), `secrets
|
|
350
|
+
(shared persistent storage attached to workspaces), `secrets` (see above), `egress` (outbound allowlists),
|
|
149
351
|
`usage`, `billing`, `me()`, `entitlements(orgId)` and `request(method, path)` for any `/v1`
|
|
150
352
|
route. The package exports the OpenAPI-generated types as well (`paths`, `components`,
|
|
151
353
|
`WorkspaceView`, `Operation` and more).
|
|
@@ -156,6 +358,8 @@ route. The package exports the OpenAPI-generated types as well (`paths`, `compon
|
|
|
156
358
|
any release; ignore unknown fields.
|
|
157
359
|
- While below 1.0, a breaking change bumps the minor version (0.5 to 0.6).
|
|
158
360
|
- `SDK_VERSION` is exported; requests send `User-Agent: shardflux-sdk-ts/<version>`.
|
|
361
|
+
- Examples in this README, in `examples/` and on shardflux.dev name the version they need. The examples on the
|
|
362
|
+
website and in the console are checked against the version published on npm before they ship.
|
|
159
363
|
|
|
160
364
|
## License
|
|
161
365
|
|
package/dist/cell.d.ts
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* (token expired or revoked early) invalidates the token and retries once with
|
|
8
8
|
* a fresh one; the retried request is safe because the gateway rejected the
|
|
9
9
|
* first before any effect (and exec/PTY starts are idempotent by session_id).
|
|
10
|
+
* The same holds for lifecycle refusals (contracts §20.4): `workspace_busy` is
|
|
11
|
+
* waited out and `workspace_not_running` wakes the workspace, then the call is
|
|
12
|
+
* retried, all within one bounded transition budget per call.
|
|
10
13
|
*
|
|
11
14
|
* Exec output is retained in the guest and addressed by byte offsets, so
|
|
12
15
|
* `exec.run()` survives gateway restarts/disconnects by reconnecting with the
|
|
@@ -14,6 +17,7 @@
|
|
|
14
17
|
*/
|
|
15
18
|
import type { components, paths } from './generated/cell-api.js';
|
|
16
19
|
import type { RequestOptions } from './http.js';
|
|
20
|
+
import type { ProgressListener } from './progress.js';
|
|
17
21
|
import type { ToolTokenManager } from './tokens.js';
|
|
18
22
|
type S = components['schemas'];
|
|
19
23
|
export type ExecStartRequest = S['ExecStartRequest'];
|
|
@@ -34,6 +38,24 @@ export type BrowserScreenshotRequest = S['BrowserScreenshotRequest'];
|
|
|
34
38
|
export type BrowserContentRequest = S['BrowserContentRequest'];
|
|
35
39
|
export type BrowserContent = S['BrowserContent'];
|
|
36
40
|
export type Signal = S['SignalValue'];
|
|
41
|
+
/** One entry of a workspace's changes against its template (contracts §19.10). */
|
|
42
|
+
export type WorkspaceChange = S['WorkspaceChange'];
|
|
43
|
+
export type WorkspaceChangeKind = WorkspaceChange['change'];
|
|
44
|
+
export type WorkspaceChangesSummary = S['WorkspaceChangesSummary'];
|
|
45
|
+
/** One page of changes in raw path-byte order; `summary` only when requested. */
|
|
46
|
+
export type WorkspaceChangesPage = S['WorkspaceChangesPage'];
|
|
47
|
+
export interface WorkspaceChangesParams {
|
|
48
|
+
/** Absolute path; only entries at or below it (default `/`). */
|
|
49
|
+
pathPrefix?: string;
|
|
50
|
+
/** 1..1000 (gateway default 1000). */
|
|
51
|
+
limit?: number;
|
|
52
|
+
/** `next_cursor` of the previous page. */
|
|
53
|
+
cursor?: string;
|
|
54
|
+
/** Hash regular files of 16 MiB or less (reports `metadata` when content equals the template's). */
|
|
55
|
+
hash?: boolean;
|
|
56
|
+
/** Also return totals over everything under pathPrefix. */
|
|
57
|
+
summary?: boolean;
|
|
58
|
+
}
|
|
37
59
|
type CellPath = keyof paths;
|
|
38
60
|
/** Fills a path template that must exist in cell-api.yaml. */
|
|
39
61
|
export declare function cellPath<P extends CellPath>(template: P, params: Record<string, string | number>): string;
|
|
@@ -44,7 +66,33 @@ export interface CellClientOptions {
|
|
|
44
66
|
/** Retries for idempotent calls on transient failures (default 2). */
|
|
45
67
|
maxRetries?: number;
|
|
46
68
|
sleep?: (ms: number) => Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Wakes a suspended workspace (resume, or join the active resume/open) within `timeoutMs` and resolves once it runs;
|
|
71
|
+
* resolves `false` when there was nothing to wake (the API already reports it running), and the refusal then
|
|
72
|
+
* surfaces. It throws when the wake fails (OperationFailedError) or outlasts `timeoutMs` (OperationTimeoutError).
|
|
73
|
+
* Workspace.cell() supplies `workspace.wake()`. A call refused with `workspace_not_running` wakes the workspace and is
|
|
74
|
+
* retried; a refused call was never executed (contracts §20.4), so the retry cannot duplicate it. `null` surfaces
|
|
75
|
+
* the refusal instead.
|
|
76
|
+
*/
|
|
77
|
+
wake?: ((timeoutMs: number, signal?: AbortSignal) => Promise<boolean | void>) | null;
|
|
78
|
+
/**
|
|
79
|
+
* Total time one call spends waiting for lifecycle transitions: `workspace_busy` waits plus wakes (default
|
|
80
|
+
* 120 000 ms). The wake hook gets what is left of it.
|
|
81
|
+
*/
|
|
82
|
+
transitionTimeoutMs?: number;
|
|
83
|
+
/**
|
|
84
|
+
* Progress of this client's tool calls: tool token fetches (action `token`, with their timing), and `tool` events for
|
|
85
|
+
* `workspace_busy` waits (phase `busy`), stale or rejected tokens and transient failures retried (type `retry`).
|
|
86
|
+
* Through Workspace.cell() a wake reports here too (action `wake`).
|
|
87
|
+
*/
|
|
88
|
+
onProgress?: ProgressListener;
|
|
89
|
+
}
|
|
90
|
+
/** Per-request transition handling (internal to CellClient). */
|
|
91
|
+
interface TransitionOptions {
|
|
92
|
+
/** Wake a suspended workspace for this call (default true; exec output follow reconnects pass false). */
|
|
93
|
+
wake?: boolean;
|
|
47
94
|
}
|
|
95
|
+
export declare const DEFAULT_TRANSITION_TIMEOUT_MS = 120000;
|
|
48
96
|
export interface RunResult {
|
|
49
97
|
sessionId: string;
|
|
50
98
|
exitCode: number | null;
|
|
@@ -94,8 +142,21 @@ export declare class CellClient {
|
|
|
94
142
|
readonly workspaceId: string;
|
|
95
143
|
readonly tokens: ToolTokenManager;
|
|
96
144
|
constructor(workspaceId: string, tokens: ToolTokenManager, opts?: CellClientOptions);
|
|
97
|
-
/**
|
|
98
|
-
|
|
145
|
+
/** True after close(): every request (and stream) of this client is aborted. */
|
|
146
|
+
get closed(): boolean;
|
|
147
|
+
/**
|
|
148
|
+
* Aborts every in-flight and future request of this client, including exec output streams and PTY reads (commands
|
|
149
|
+
* keep running in the workspace: nothing is canceled there). Workspace.close() calls it.
|
|
150
|
+
*/
|
|
151
|
+
close(): void;
|
|
152
|
+
/**
|
|
153
|
+
* One authorized request. Refreshes the token once on stale_epoch / 401. Lifecycle transitions (contracts §20.4),
|
|
154
|
+
* bounded in total by `transitionTimeoutMs`: a call refused with `workspace_busy` is retried after `Retry-After`; one
|
|
155
|
+
* refused with `workspace_not_running` (or whose token cannot be minted because the workspace is not running) wakes
|
|
156
|
+
* the workspace through `wake`, given the time left, and is retried with a fresh token, at most 3 wakes per call.
|
|
157
|
+
* Refused calls were never executed, so retrying is safe. When the budget is spent the refusal surfaces.
|
|
158
|
+
*/
|
|
159
|
+
request(method: string, path: string, init?: RequestOptions & TransitionOptions): Promise<Response>;
|
|
99
160
|
readonly exec: {
|
|
100
161
|
/** Starts argv (no shell). Idempotent by session_id: an existing session is returned, never re-run. */
|
|
101
162
|
start: (req: ExecStartRequest, signal?: AbortSignal) => Promise<ExecSession>;
|
|
@@ -182,6 +243,10 @@ export declare class CellClient {
|
|
|
182
243
|
overwrite?: boolean;
|
|
183
244
|
}) => Promise<FileInfo>;
|
|
184
245
|
};
|
|
246
|
+
/** One page of the workspace's changes against its template (needs the `files` tool). */
|
|
247
|
+
changes(params?: WorkspaceChangesParams): Promise<WorkspaceChangesPage>;
|
|
248
|
+
/** Every change under `pathPrefix`, following next_cursor. */
|
|
249
|
+
changesAll(params?: Omit<WorkspaceChangesParams, 'cursor' | 'summary'>): AsyncGenerator<WorkspaceChange>;
|
|
185
250
|
readonly git: {
|
|
186
251
|
clone: (req: GitCloneRequest) => Promise<GitResult>;
|
|
187
252
|
status: (path: string) => Promise<GitStatus>;
|