@sanlabs/sanbox-cli 0.0.11 → 0.0.12
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/README.md +27 -12
- package/dist/activity.js +18 -1
- package/dist/api.js +10 -8
- package/dist/args.js +2 -1
- package/dist/cli.js +89 -49
- package/dist/output.js +12 -3
- package/dist/runs.js +14 -11
- package/dist/version.js +1 -1
- package/dist/watch.js +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,8 +15,11 @@ installed_cli_version="$(sanbox --version)"
|
|
|
15
15
|
test "$installed_cli_version" = "$latest_cli_version"
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
Always use the latest published CLI. CLI 0.0.
|
|
19
|
-
|
|
18
|
+
Always use the latest published CLI. CLI 0.0.12 adopts the Stop, Resume, and Delete lifecycle,
|
|
19
|
+
separates run state from execution outcome, retains scrubbed deleted-run tombstones, and adds
|
|
20
|
+
administrator-confirmed template deletion.
|
|
21
|
+
CLI 0.0.11 added OpenCode Computer templates and short-lived native SDK connections for HTTP, SSE
|
|
22
|
+
streaming, and interactive steering.
|
|
20
23
|
CLI 0.0.10 added per-run Hermes email and Telegram channels, Supabase user authorization, and
|
|
21
24
|
removed the retired run-chat commands.
|
|
22
25
|
CLI 0.0.9 added user login for private SSH access to supported running sandboxes.
|
|
@@ -113,6 +116,15 @@ sanbox templates create \
|
|
|
113
116
|
Omit both `--channel` flags for a WebUI-only computer. Channel credentials never belong to the
|
|
114
117
|
template.
|
|
115
118
|
|
|
119
|
+
Delete a template when it should no longer be available for new runs:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
sanbox templates delete <template-id-or-slug> --force --json
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Deletion preserves existing runs and retained sandboxes. It requires organization-admin access
|
|
126
|
+
and `--force` so scripts cannot remove a template accidentally.
|
|
127
|
+
|
|
116
128
|
## Create A Browser Use Template
|
|
117
129
|
|
|
118
130
|
Browser Use runs local headless Chromium inside the Firecracker sandbox. It requires OpenAI,
|
|
@@ -200,7 +212,7 @@ callback lands on a Sanbox-hosted completion page, so a CLI demo does not need a
|
|
|
200
212
|
Customer integrations may use `--return-url <https-url>` when its origin matches the return origin
|
|
201
213
|
configured on the organization Supabase connection.
|
|
202
214
|
|
|
203
|
-
Reuse the same external ID when retrying an ambiguous submission. To stream activity, replace `--wait --json` with `--jsonl`. Ctrl-C detaches without
|
|
215
|
+
Reuse the same external ID when retrying an ambiguous submission. To stream activity, replace `--wait --json` with `--jsonl`. Ctrl-C detaches without stopping unless `--stop-on-interrupt` is supplied.
|
|
204
216
|
|
|
205
217
|
## Inspect And Recover
|
|
206
218
|
|
|
@@ -243,20 +255,23 @@ working when the exact sandbox session stops, the link expires, or it is revoked
|
|
|
243
255
|
when created and must be handled as a bearer secret. See
|
|
244
256
|
[Live Filesystem Access](../docs/live-filesystem-access.md) for the HTTP contract and exclusions.
|
|
245
257
|
|
|
246
|
-
## Resume Or
|
|
258
|
+
## Stop, Resume, Or Delete A Run
|
|
247
259
|
|
|
248
260
|
```bash
|
|
249
261
|
sanbox runs get <run-id> --json
|
|
250
262
|
sanbox runs resume <run-id> --wait --json
|
|
251
263
|
sanbox runs share <run-id> --expires 1h --json
|
|
252
|
-
sanbox runs
|
|
264
|
+
sanbox runs stop <run-id> --wait --json
|
|
265
|
+
sanbox runs delete <run-id> --yes --json
|
|
253
266
|
```
|
|
254
267
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
`
|
|
259
|
-
|
|
268
|
+
Stop terminates compute and durably syncs the workspace. It does not retain RAM, process state, or
|
|
269
|
+
network connections. Resume fresh-boots the pinned runtime artifact with the retained workspace; it
|
|
270
|
+
does not silently replay a completed task. `state` is the single run phase; inspect
|
|
271
|
+
`latest_execution.outcome` for the last bounded execution outcome and `workspace.saved_at` for disk
|
|
272
|
+
persistence proof. Stopped runs have no automatic TTL. Delete permanently removes the workspace
|
|
273
|
+
after archiving usage. The control plane retains a scrubbed run tombstone, but normal CLI run listings
|
|
274
|
+
exclude deleted runs.
|
|
260
275
|
|
|
261
276
|
## SSH Into A Running Sandbox
|
|
262
277
|
|
|
@@ -274,8 +289,8 @@ authenticated WebSocket. The temporary key is deleted when the connection closes
|
|
|
274
289
|
user token is not passed to the OpenSSH child process.
|
|
275
290
|
|
|
276
291
|
OpenCode and Browser Use runs are one-shot agent executions. OpenCode Computer keeps its private
|
|
277
|
-
server running after the initial task so it can accept later steering. A persisted task
|
|
278
|
-
still be
|
|
292
|
+
server running after the initial task so it can accept later steering. A persisted task workspace can
|
|
293
|
+
still be fresh-booted manually for inspection after the run stops.
|
|
279
294
|
|
|
280
295
|
## Connect The OpenCode SDK
|
|
281
296
|
|
package/dist/activity.js
CHANGED
|
@@ -35,6 +35,23 @@ export const formatActivityLine = (event, runCreatedAt) => {
|
|
|
35
35
|
return `${elapsed(runCreatedAt, event.created_at).padStart(8)} ${event.kind.padEnd(22)} ${bounded}`.trimEnd();
|
|
36
36
|
};
|
|
37
37
|
const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
38
|
+
const legacyGuestEventKinds = new Set([
|
|
39
|
+
"log.stdout",
|
|
40
|
+
"log.stderr",
|
|
41
|
+
"runner.event_invalid",
|
|
42
|
+
"sandbox.stderr",
|
|
43
|
+
"anthropic.worker.stdout",
|
|
44
|
+
"anthropic.worker.stderr"
|
|
45
|
+
]);
|
|
46
|
+
const legacyEventSource = (event) => {
|
|
47
|
+
if (legacyGuestEventKinds.has(event.kind)) {
|
|
48
|
+
return { plane: "sandbox", trust: "legacy_guest_content" };
|
|
49
|
+
}
|
|
50
|
+
if (event.kind === "sandbox.status") {
|
|
51
|
+
return { plane: "unknown", trust: "legacy_mixed_origin" };
|
|
52
|
+
}
|
|
53
|
+
return { plane: "control_plane", trust: "control_plane" };
|
|
54
|
+
};
|
|
38
55
|
const safeEventData = (value) => {
|
|
39
56
|
if (Array.isArray(value))
|
|
40
57
|
return value.map(safeEventData);
|
|
@@ -61,7 +78,7 @@ export const activityEnvelope = (event) => {
|
|
|
61
78
|
kind: event.kind,
|
|
62
79
|
level: event.level,
|
|
63
80
|
summary: event.message,
|
|
64
|
-
source: normalized ? event.payload.source :
|
|
81
|
+
source: normalized ? event.payload.source : legacyEventSource(event),
|
|
65
82
|
sequence: normalized && typeof event.payload.sequence === "number" ? event.payload.sequence : null,
|
|
66
83
|
correlation: normalized && isRecord(event.payload.correlation) ? event.payload.correlation : {},
|
|
67
84
|
data: safeEventData(normalized ? event.payload.data : event.payload),
|
package/dist/api.js
CHANGED
|
@@ -174,6 +174,9 @@ export class SanboxClient {
|
|
|
174
174
|
async validateTemplate(templateId) {
|
|
175
175
|
return this.request(await this.orgPath(`/templates/${encodeURIComponent(templateId)}/validate`));
|
|
176
176
|
}
|
|
177
|
+
async deleteTemplate(templateId) {
|
|
178
|
+
return this.request(await this.orgPath(`/templates/${encodeURIComponent(templateId)}`), { method: "DELETE" });
|
|
179
|
+
}
|
|
177
180
|
async createTemplate(body) {
|
|
178
181
|
return this.request(await this.orgPath("/templates"), {
|
|
179
182
|
method: "POST",
|
|
@@ -198,20 +201,19 @@ export class SanboxClient {
|
|
|
198
201
|
async listEvents(runId, afterEventId = 0, limit = 200, signal) {
|
|
199
202
|
return this.request(`${await this.orgPath(`/runs/${encodeURIComponent(runId)}/events`)}?after_event_id=${afterEventId}&limit=${limit}`, { signal });
|
|
200
203
|
}
|
|
201
|
-
async
|
|
202
|
-
return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/
|
|
204
|
+
async stopRun(runId) {
|
|
205
|
+
return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/stop`), {
|
|
203
206
|
method: "POST",
|
|
204
207
|
body: "{}"
|
|
205
208
|
});
|
|
206
209
|
}
|
|
207
|
-
async
|
|
208
|
-
return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}
|
|
209
|
-
method: "
|
|
210
|
-
body: "{}"
|
|
210
|
+
async deleteRun(runId) {
|
|
211
|
+
return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}`), {
|
|
212
|
+
method: "DELETE"
|
|
211
213
|
});
|
|
212
214
|
}
|
|
213
|
-
async
|
|
214
|
-
return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/
|
|
215
|
+
async resumeRun(runId) {
|
|
216
|
+
return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/resume`), {
|
|
215
217
|
method: "POST",
|
|
216
218
|
body: "{}"
|
|
217
219
|
});
|
package/dist/args.js
CHANGED
package/dist/cli.js
CHANGED
|
@@ -10,8 +10,8 @@ import { loginWithBrowser, openBrowser, revokeUserSession } from "./deviceLogin.
|
|
|
10
10
|
import { CliError, commandAction, consoleAction } from "./errors.js";
|
|
11
11
|
import { parseFileAccessExpiry } from "./fileAccess.js";
|
|
12
12
|
import { previewInputs } from "./inputs.js";
|
|
13
|
-
import { printError, printJsonlError, printRun, printSuccess, publicRun, publicRunPayload } from "./output.js";
|
|
14
|
-
import { createRun, isTerminalRun, readTasks, runPool, stableBatchId, waitForRun,
|
|
13
|
+
import { isoUtcTimestamp, printError, printJsonlError, printRun, printSuccess, publicRun, publicRunPayload } from "./output.js";
|
|
14
|
+
import { createRun, isTerminalRun, runSucceeded, readTasks, runPool, stableBatchId, waitForRun, waitForRunState } from "./runs.js";
|
|
15
15
|
import { version } from "./version.js";
|
|
16
16
|
import { WatchInterruptedError, watchRun } from "./watch.js";
|
|
17
17
|
import { openSSH, runSSHProxy } from "./ssh.js";
|
|
@@ -41,6 +41,7 @@ Commands:
|
|
|
41
41
|
sanbox templates get <template-id> [--json]
|
|
42
42
|
sanbox templates validate <template-id> [--json]
|
|
43
43
|
sanbox templates create --name "..." --model-provider <provider-id> --model <model-id> [--harness opencode|hermes|browser-use] [--mode task|computer] [--channel email|telegram] [--browser-domain <hostname>] [--llm-budget-usd <amount>] [--json]
|
|
44
|
+
sanbox templates delete <template-id> --force [--json]
|
|
44
45
|
sanbox run "task" --template <template-id> [--email-address <address>] [--telegram-bot-token <token>] [--telegram-allowed-user <id>] [--input <path>] [--wait | --watch] [--json | --jsonl]
|
|
45
46
|
sanbox run --task "..." --template <template-id> [--email-address <address>] [--telegram-bot-token <token>] [--telegram-allowed-user <id>] [--input <path>] [--wait | --watch] [--json | --jsonl]
|
|
46
47
|
sanbox batch --tasks tasks.json --template <template-id> [--input <path>] [--max-parallel 5] [--wait] [--json]
|
|
@@ -52,10 +53,10 @@ Commands:
|
|
|
52
53
|
sanbox runs shares <run-id> [--json]
|
|
53
54
|
sanbox runs unshare <run-id> <access-point-id> [--json]
|
|
54
55
|
sanbox runs download <run-id> --output <directory> [--artifact <path>] [--overwrite] [--json]
|
|
55
|
-
sanbox runs watch <run-id> [--after-event-id 0] [--view activity|logs|compact] [--jsonl]
|
|
56
|
-
sanbox runs
|
|
56
|
+
sanbox runs watch <run-id> [--after-event-id 0] [--view activity|logs|compact] [--stop-on-interrupt] [--jsonl]
|
|
57
|
+
sanbox runs stop <run-id> [--wait] [--json]
|
|
57
58
|
sanbox runs resume <run-id> [--wait] [--json]
|
|
58
|
-
sanbox runs
|
|
59
|
+
sanbox runs delete <run-id> --yes [--json]
|
|
59
60
|
sanbox runs supabase authorize <run-id> [--open] [--return-url <https-url>] [--json]
|
|
60
61
|
sanbox opencode connect <run-id> [--expires 1h] [--json]
|
|
61
62
|
sanbox opencode connections list <run-id> [--json]
|
|
@@ -102,12 +103,12 @@ Options:
|
|
|
102
103
|
--telegram-bot-token <token> Optional Telegram bot token; SANBOX_TELEGRAM_BOT_TOKEN is also supported.
|
|
103
104
|
--telegram-allowed-user <id> Numeric Telegram user allowed to reach this run. Repeatable.
|
|
104
105
|
--dry-run Preview included files without creating a run.
|
|
105
|
-
--wait Poll until terminal
|
|
106
|
-
--watch Stream activity until terminal
|
|
106
|
+
--wait Poll until a terminal run state.
|
|
107
|
+
--watch Stream activity until a terminal run state.
|
|
107
108
|
--jsonl Stream one versioned activity event per line. Implies --watch.
|
|
108
|
-
--view <name> activity,
|
|
109
|
+
--view <name> activity, compact, or historical logs. Default: activity.
|
|
109
110
|
--after-event-id <id> Resume after an event cursor. Default: 0.
|
|
110
|
-
--
|
|
111
|
+
--stop-on-interrupt Request Stop when Ctrl-C is pressed.
|
|
111
112
|
--json Print JSON.
|
|
112
113
|
`;
|
|
113
114
|
const runsSupabaseHelp = `Sanbox Supabase run authorization
|
|
@@ -160,6 +161,7 @@ Usage:
|
|
|
160
161
|
sanbox templates get <template-id> [--json]
|
|
161
162
|
sanbox templates validate <template-id> [--json]
|
|
162
163
|
sanbox templates create --name "Code review" --model-provider <provider-id> --model <model-id> [--harness opencode|hermes|browser-use] [--mode task|computer] [--browser-domain <hostname>] [--llm-budget-usd <amount>] [--json]
|
|
164
|
+
sanbox templates delete <template-id> --force [--json]
|
|
163
165
|
|
|
164
166
|
Template creation requires an exact provider id and that provider's exact model id.
|
|
165
167
|
LiteLLM budgets are optional USD amounts and apply separately to each run.
|
|
@@ -172,6 +174,7 @@ or Anthropic and at least one repeatable --browser-domain hostname or leading wi
|
|
|
172
174
|
Optional controls: --browser-max-steps, --browser-step-timeout-seconds,
|
|
173
175
|
--browser-vision-mode auto|always|never, --browser-viewport WIDTHxHEIGHT,
|
|
174
176
|
--browser-download-policy allow|deny, and --browser-additional-instructions.
|
|
177
|
+
Deleting a template requires --force. Existing runs and their retained sandboxes are preserved.
|
|
175
178
|
`;
|
|
176
179
|
const agentInstructions = `# Operate Sanbox Autonomously
|
|
177
180
|
|
|
@@ -225,7 +228,8 @@ sanbox run "Investigate one focused task and write output/report.md" \\
|
|
|
225
228
|
|
|
226
229
|
Reuse the same --external-run-id after ambiguous failures. Retry network errors, HTTP 429, HTTP 5xx,
|
|
227
230
|
and workspace_busy with bounded backoff. Do not retry other 4xx errors unless next_actions directs
|
|
228
|
-
recovery.
|
|
231
|
+
recovery. A run is inactive when state is stopped; inspect latest_execution.outcome for the most
|
|
232
|
+
recent bounded execution outcome.
|
|
229
233
|
|
|
230
234
|
Recover and retrieve results:
|
|
231
235
|
\`\`\`bash
|
|
@@ -254,20 +258,20 @@ Manage the retained sandbox independently of its agent harness:
|
|
|
254
258
|
sanbox runs get <run-id> --json
|
|
255
259
|
sanbox runs resume <run-id> --wait --json
|
|
256
260
|
sanbox runs share <run-id> --expires 1h --json
|
|
257
|
-
sanbox runs
|
|
261
|
+
sanbox runs stop <run-id> --wait --json
|
|
262
|
+
sanbox runs delete <run-id> --yes --json
|
|
258
263
|
\`\`\`
|
|
259
264
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
positive snapshot_generation.
|
|
265
|
+
Stop terminates compute and durably syncs the workspace without retaining RAM or VM state. Resume
|
|
266
|
+
fresh-boots the pinned runtime artifact with that workspace; it does not restore process state or
|
|
267
|
+
silently replay a finished task. Delete permanently removes the workspace after archiving usage.
|
|
264
268
|
|
|
265
269
|
For independent fan-out, use \`sanbox batch\` with a stable external_run_id per task and keep the
|
|
266
270
|
client alive until submission completes.
|
|
267
271
|
|
|
268
|
-
Do not claim completion until
|
|
269
|
-
Report run/external/template IDs,
|
|
270
|
-
|
|
272
|
+
Do not claim completion until state is stopped, latest_execution.outcome is completed, and required
|
|
273
|
+
artifacts are downloaded and verified. Report run/external/template IDs, run state, execution outcome, workspace save time,
|
|
274
|
+
artifact paths/digests, and blockers. The CLI excludes common secrets by default; add
|
|
271
275
|
.sanboxignore for project rules.
|
|
272
276
|
`;
|
|
273
277
|
const cwd = () => process.cwd();
|
|
@@ -446,6 +450,7 @@ const flagSets = {
|
|
|
446
450
|
"templates.list": commonFlags,
|
|
447
451
|
"templates.get": commonFlags,
|
|
448
452
|
"templates.validate": commonFlags,
|
|
453
|
+
"templates.delete": [...commonFlags, "force"],
|
|
449
454
|
"templates.create": [
|
|
450
455
|
...commonFlags,
|
|
451
456
|
"name",
|
|
@@ -468,7 +473,7 @@ const flagSets = {
|
|
|
468
473
|
...commonFlags, "task", "input", "template", "external-run-id", "email-address",
|
|
469
474
|
"supabase-user-id",
|
|
470
475
|
"telegram-bot-token", "telegram-allowed-user",
|
|
471
|
-
"dry-run", "wait", "watch", "jsonl", "view", "after-event-id", "
|
|
476
|
+
"dry-run", "wait", "watch", "jsonl", "view", "after-event-id", "stop-on-interrupt",
|
|
472
477
|
"poll-interval-ms", "event-page-size", "timeout-seconds", "verbose"
|
|
473
478
|
],
|
|
474
479
|
batch: [
|
|
@@ -484,12 +489,12 @@ const flagSets = {
|
|
|
484
489
|
"runs.unshare": commonFlags,
|
|
485
490
|
"runs.download": [...commonFlags, "output", "artifact", "overwrite"],
|
|
486
491
|
"runs.watch": [
|
|
487
|
-
...commonFlags, "after-event-id", "view", "jsonl", "
|
|
492
|
+
...commonFlags, "after-event-id", "view", "jsonl", "stop-on-interrupt",
|
|
488
493
|
"poll-interval-ms", "event-page-size", "timeout-seconds"
|
|
489
494
|
],
|
|
490
|
-
"runs.
|
|
495
|
+
"runs.stop": [...commonFlags, "wait", "poll-interval-ms", "timeout-seconds"],
|
|
496
|
+
"runs.delete": [...commonFlags, "yes"],
|
|
491
497
|
"runs.resume": [...commonFlags, "wait", "poll-interval-ms", "timeout-seconds"],
|
|
492
|
-
"runs.pause": [...commonFlags, "wait", "poll-interval-ms", "timeout-seconds"],
|
|
493
498
|
"runs.supabase": [...commonFlags, "open", "return-url"],
|
|
494
499
|
"opencode.connect": [...commonFlags, "expires"],
|
|
495
500
|
"opencode.connections": commonFlags,
|
|
@@ -598,6 +603,7 @@ const validatePositionals = (command, flags) => {
|
|
|
598
603
|
"templates.list": 2,
|
|
599
604
|
"templates.get": 3,
|
|
600
605
|
"templates.validate": 3,
|
|
606
|
+
"templates.delete": 3,
|
|
601
607
|
"templates.create": 2,
|
|
602
608
|
batch: 1,
|
|
603
609
|
"runs.list": 2,
|
|
@@ -609,9 +615,9 @@ const validatePositionals = (command, flags) => {
|
|
|
609
615
|
"runs.unshare": 4,
|
|
610
616
|
"runs.download": 3,
|
|
611
617
|
"runs.watch": 3,
|
|
612
|
-
"runs.
|
|
618
|
+
"runs.stop": 3,
|
|
619
|
+
"runs.delete": 3,
|
|
613
620
|
"runs.resume": 3,
|
|
614
|
-
"runs.pause": 3,
|
|
615
621
|
"runs.supabase": 4,
|
|
616
622
|
"opencode.connect": 3,
|
|
617
623
|
"opencode.connections": 5,
|
|
@@ -658,7 +664,7 @@ const watchRunWithOutput = async (client, runId, flags, initialPayload) => {
|
|
|
658
664
|
const onInterrupt = () => controller.abort();
|
|
659
665
|
process.once("SIGINT", onInterrupt);
|
|
660
666
|
if (!jsonl)
|
|
661
|
-
process.stdout.write(`Watching run ${runId}. Ctrl-C detaches without
|
|
667
|
+
process.stdout.write(`Watching run ${runId}. Ctrl-C detaches without stopping.\n`);
|
|
662
668
|
try {
|
|
663
669
|
return await watchRun(client, runId, {
|
|
664
670
|
afterEventId: integerFlag(flags, "after-event-id", 0, 0, Number.MAX_SAFE_INTEGER),
|
|
@@ -680,9 +686,9 @@ const watchRunWithOutput = async (client, runId, flags, initialPayload) => {
|
|
|
680
686
|
catch (error) {
|
|
681
687
|
if (!(error instanceof WatchInterruptedError))
|
|
682
688
|
throw error;
|
|
683
|
-
if (hasFlag(flags, "
|
|
684
|
-
await client.
|
|
685
|
-
process.stderr.write(`
|
|
689
|
+
if (hasFlag(flags, "stop-on-interrupt")) {
|
|
690
|
+
await client.stopRun(runId);
|
|
691
|
+
process.stderr.write(`Stop requested for run ${runId}.\n`);
|
|
686
692
|
}
|
|
687
693
|
else {
|
|
688
694
|
process.stderr.write(`Detached from run ${runId}; the run is still active.\n`);
|
|
@@ -785,7 +791,7 @@ const commandRun = async (command, flags) => {
|
|
|
785
791
|
payload = watched;
|
|
786
792
|
if (!hasFlag(flags, "jsonl"))
|
|
787
793
|
printRun(payload);
|
|
788
|
-
if (isTerminalRun(payload.run) && payload.run
|
|
794
|
+
if (isTerminalRun(payload.run) && !runSucceeded(payload.run))
|
|
789
795
|
process.exitCode = 2;
|
|
790
796
|
return;
|
|
791
797
|
}
|
|
@@ -803,7 +809,7 @@ const commandRun = async (command, flags) => {
|
|
|
803
809
|
}
|
|
804
810
|
else
|
|
805
811
|
printRun(payload);
|
|
806
|
-
if (hasFlag(flags, "wait") && isTerminalRun(payload.run) && payload.run
|
|
812
|
+
if (hasFlag(flags, "wait") && isTerminalRun(payload.run) && !runSucceeded(payload.run))
|
|
807
813
|
process.exitCode = 2;
|
|
808
814
|
};
|
|
809
815
|
const commandBatch = async (flags) => {
|
|
@@ -850,7 +856,7 @@ const commandBatch = async (flags) => {
|
|
|
850
856
|
printSuccess("batch.create", output, jsonContext(client));
|
|
851
857
|
else
|
|
852
858
|
results.forEach(printRun);
|
|
853
|
-
if (wait && results.some((result) => result.run
|
|
859
|
+
if (wait && results.some((result) => !runSucceeded(result.run)))
|
|
854
860
|
process.exitCode = 2;
|
|
855
861
|
};
|
|
856
862
|
const commandAuthCheck = async (flags) => {
|
|
@@ -1013,7 +1019,7 @@ const commandAnthropicEnvironments = async (command, flags) => {
|
|
|
1013
1019
|
for (const environment of payload.environments) {
|
|
1014
1020
|
process.stdout.write(`${environment.environment_id}\t${environment.status}` +
|
|
1015
1021
|
`\tworker=${environment.assigned_worker_id || "unassigned"}` +
|
|
1016
|
-
`\tverified=${environment.last_verified_at
|
|
1022
|
+
`\tverified=${environment.last_verified_at ? isoUtcTimestamp(environment.last_verified_at) : "never"}\n`);
|
|
1017
1023
|
}
|
|
1018
1024
|
return;
|
|
1019
1025
|
}
|
|
@@ -1033,7 +1039,7 @@ const commandAnthropicEnvironments = async (command, flags) => {
|
|
|
1033
1039
|
const environment = payload.environment;
|
|
1034
1040
|
process.stdout.write(`${environment.environment_id} ${environment.status}` +
|
|
1035
1041
|
` worker=${environment.assigned_worker_id || "unassigned"}` +
|
|
1036
|
-
` verified=${environment.last_verified_at
|
|
1042
|
+
` verified=${environment.last_verified_at ? isoUtcTimestamp(environment.last_verified_at) : "never"}\n`);
|
|
1037
1043
|
return;
|
|
1038
1044
|
}
|
|
1039
1045
|
if (action === "connect") {
|
|
@@ -1280,7 +1286,39 @@ const commandTemplates = async (command, flags) => {
|
|
|
1280
1286
|
`${payload.template.llm_budget_usd ? ` budget=$${payload.template.llm_budget_usd}/run` : ""}\n`);
|
|
1281
1287
|
return;
|
|
1282
1288
|
}
|
|
1283
|
-
|
|
1289
|
+
if (action === "delete") {
|
|
1290
|
+
const id = requiredPositional(command[2], "template_id_required", "templates delete requires a template id or slug.");
|
|
1291
|
+
if (!hasFlag(flags, "force")) {
|
|
1292
|
+
throw new CliError("confirmation_required", "templates delete requires --force.", {
|
|
1293
|
+
details: { template_id: id },
|
|
1294
|
+
nextActions: [commandAction(["sanbox", "templates", "delete", id, "--force", "--json"], "Delete the template from this organization while preserving existing runs.")]
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
let payload;
|
|
1298
|
+
try {
|
|
1299
|
+
payload = await client.deleteTemplate(id);
|
|
1300
|
+
}
|
|
1301
|
+
catch (error) {
|
|
1302
|
+
if (error instanceof SanboxApiError) {
|
|
1303
|
+
throw new CliError(error.code, error.message, {
|
|
1304
|
+
status: error.status,
|
|
1305
|
+
details: { template_id: id },
|
|
1306
|
+
nextActions: error.code === "template_not_found"
|
|
1307
|
+
? [commandAction(["sanbox", "templates", "list", "--json"], "List templates available to the API key's organization.")]
|
|
1308
|
+
: []
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
throw error;
|
|
1312
|
+
}
|
|
1313
|
+
const nextActions = [commandAction(["sanbox", "templates", "list", "--json"], "Confirm that the template is no longer available for new runs.")];
|
|
1314
|
+
if (hasFlag(flags, "json")) {
|
|
1315
|
+
printSuccess("templates.delete", payload, jsonContext(client), nextActions);
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
process.stdout.write(`deleted ${payload.template_id}\n`);
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
throw new CliError("templates_action_required", "templates requires an action: list, get, validate, create, or delete.");
|
|
1284
1322
|
};
|
|
1285
1323
|
const commandRuns = async (command, flags) => {
|
|
1286
1324
|
const client = makeClient(flags);
|
|
@@ -1295,7 +1333,7 @@ const commandRuns = async (command, flags) => {
|
|
|
1295
1333
|
return;
|
|
1296
1334
|
}
|
|
1297
1335
|
for (const run of payload.runs) {
|
|
1298
|
-
process.stdout.write(`${run.id}\t${run.
|
|
1336
|
+
process.stdout.write(`${run.id}\t${run.state}\t${isoUtcTimestamp(run.created_at)}\t${run.instruction.replace(/\s+/g, " ").slice(0, 100)}\n`);
|
|
1299
1337
|
}
|
|
1300
1338
|
return;
|
|
1301
1339
|
}
|
|
@@ -1367,7 +1405,7 @@ const commandRuns = async (command, flags) => {
|
|
|
1367
1405
|
return;
|
|
1368
1406
|
}
|
|
1369
1407
|
for (const accessPoint of payload.access_points) {
|
|
1370
|
-
process.stdout.write(`${accessPoint.id}\t${accessPoint.status}\t${accessPoint.expires_at}\t${accessPoint.name}\n`);
|
|
1408
|
+
process.stdout.write(`${accessPoint.id}\t${accessPoint.status}\t${isoUtcTimestamp(accessPoint.expires_at)}\t${accessPoint.name}\n`);
|
|
1371
1409
|
}
|
|
1372
1410
|
return;
|
|
1373
1411
|
}
|
|
@@ -1435,24 +1473,26 @@ const commandRuns = async (command, flags) => {
|
|
|
1435
1473
|
return;
|
|
1436
1474
|
if (!hasFlag(flags, "jsonl"))
|
|
1437
1475
|
printRun(payload);
|
|
1438
|
-
if (isTerminalRun(payload.run) && payload.run
|
|
1476
|
+
if (isTerminalRun(payload.run) && !runSucceeded(payload.run))
|
|
1439
1477
|
process.exitCode = 2;
|
|
1440
1478
|
return;
|
|
1441
1479
|
}
|
|
1442
|
-
if (action === "
|
|
1443
|
-
|
|
1480
|
+
if (action === "delete") {
|
|
1481
|
+
if (!hasFlag(flags, "yes")) {
|
|
1482
|
+
throw new CliError("confirmation_required", "Run deletion permanently removes its workspace. Re-run with --yes.");
|
|
1483
|
+
}
|
|
1484
|
+
const payload = await client.deleteRun(runId);
|
|
1444
1485
|
if (hasFlag(flags, "json"))
|
|
1445
|
-
printSuccess("runs.
|
|
1486
|
+
printSuccess("runs.delete", publicRunPayload(payload), jsonContext(client));
|
|
1446
1487
|
else
|
|
1447
|
-
|
|
1488
|
+
process.stdout.write(`deletion requested for run ${runId}\n`);
|
|
1448
1489
|
return;
|
|
1449
1490
|
}
|
|
1450
|
-
if (action === "
|
|
1451
|
-
const submitted = action === "resume"
|
|
1452
|
-
|
|
1453
|
-
: await client.pauseRun(runId);
|
|
1491
|
+
if (action === "stop" || action === "resume") {
|
|
1492
|
+
const submitted = action === "resume" ? await client.resumeRun(runId) : await client.stopRun(runId);
|
|
1493
|
+
const target = action === "resume" ? "running" : "stopped";
|
|
1454
1494
|
const payload = hasFlag(flags, "wait")
|
|
1455
|
-
? await
|
|
1495
|
+
? await waitForRunState(client, runId, target, {
|
|
1456
1496
|
pollIntervalMs: integerFlag(flags, "poll-interval-ms", 2000, 250, 60_000),
|
|
1457
1497
|
timeoutSeconds: integerFlag(flags, "timeout-seconds", 1800, 1, 604_800)
|
|
1458
1498
|
})
|
|
@@ -1464,7 +1504,7 @@ const commandRuns = async (command, flags) => {
|
|
|
1464
1504
|
printRun(payload);
|
|
1465
1505
|
}
|
|
1466
1506
|
else {
|
|
1467
|
-
process.stdout.write(
|
|
1507
|
+
process.stdout.write(`${action} requested for run ${runId}\n`);
|
|
1468
1508
|
}
|
|
1469
1509
|
return;
|
|
1470
1510
|
}
|
|
@@ -1486,7 +1526,7 @@ const commandOpenCode = async (command, flags) => {
|
|
|
1486
1526
|
]);
|
|
1487
1527
|
return;
|
|
1488
1528
|
}
|
|
1489
|
-
process.stdout.write(`URL ${payload.url}\nTOKEN ${payload.access_token}\nEXPIRES ${payload.connection.expires_at}\n`);
|
|
1529
|
+
process.stdout.write(`URL ${payload.url}\nTOKEN ${payload.access_token}\nEXPIRES ${isoUtcTimestamp(payload.connection.expires_at)}\n`);
|
|
1490
1530
|
return;
|
|
1491
1531
|
}
|
|
1492
1532
|
if (action === "connections") {
|
|
@@ -1502,7 +1542,7 @@ const commandOpenCode = async (command, flags) => {
|
|
|
1502
1542
|
return;
|
|
1503
1543
|
}
|
|
1504
1544
|
for (const connection of payload.connections) {
|
|
1505
|
-
process.stdout.write(`${connection.id}\t${connection.status}\t${connection.expires_at}\t${connection.token_prefix}\n`);
|
|
1545
|
+
process.stdout.write(`${connection.id}\t${connection.status}\t${isoUtcTimestamp(connection.expires_at)}\t${connection.token_prefix}\n`);
|
|
1506
1546
|
}
|
|
1507
1547
|
return;
|
|
1508
1548
|
}
|
package/dist/output.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { CliError } from "./errors.js";
|
|
2
2
|
import { SanboxApiError } from "./api.js";
|
|
3
|
+
// Human-readable CLI output uses one canonical ISO 8601 UTC representation.
|
|
4
|
+
// Structured JSON output preserves the API payload unchanged.
|
|
5
|
+
export const isoUtcTimestamp = (value) => {
|
|
6
|
+
const date = new Date(value);
|
|
7
|
+
return Number.isFinite(date.getTime()) ? date.toISOString() : value;
|
|
8
|
+
};
|
|
3
9
|
export const printJson = (value) => {
|
|
4
10
|
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
5
11
|
};
|
|
@@ -73,18 +79,21 @@ export const publicRunPayload = (payload) => ({
|
|
|
73
79
|
});
|
|
74
80
|
export const summarizeRun = (payload) => {
|
|
75
81
|
const { run } = payload;
|
|
82
|
+
const execution = run.latest_execution;
|
|
76
83
|
const templateId = run.template_id || run.workload_id;
|
|
77
84
|
const selection = [
|
|
78
85
|
templateId ? `template=${templateId}` : "",
|
|
79
86
|
run.provider_id ? `provider=${run.provider_id}` : "",
|
|
80
87
|
run.model_id ? `model=${run.model_id}` : "",
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
`state=${run.state}`,
|
|
89
|
+
execution?.outcome
|
|
90
|
+
? `result=${execution.outcome}`
|
|
91
|
+
: "",
|
|
83
92
|
run.email?.address ? `email=${run.email.address}` : run.email ? `email=${run.email.status}` : "",
|
|
84
93
|
run.channels?.telegram.enabled ? "telegram=enabled" : "",
|
|
85
94
|
run.supabase ? `supabase=${run.supabase.status}` : ""
|
|
86
95
|
].filter(Boolean).join(" ");
|
|
87
|
-
return `${run.id}
|
|
96
|
+
return `${run.id}${selection ? ` ${selection}` : ""}${execution?.exit_code == null ? "" : ` exit=${execution.exit_code}`}${execution?.error ? ` error=${execution.error}` : ""}`;
|
|
88
97
|
};
|
|
89
98
|
export const printRun = (payload) => {
|
|
90
99
|
process.stdout.write(`${summarizeRun(payload)}\n`);
|
package/dist/runs.js
CHANGED
|
@@ -2,8 +2,8 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { buildInputBundle } from "./inputs.js";
|
|
5
|
-
const
|
|
6
|
-
export const
|
|
5
|
+
export const isTerminalRun = (run) => run.state === "stopped" || run.state === "deleting" || run.state === "deleted";
|
|
6
|
+
export const runSucceeded = (run) => run.latest_execution?.outcome === "completed";
|
|
7
7
|
export const waitForRun = async (client, runId, options = {}) => {
|
|
8
8
|
const pollIntervalMs = options.pollIntervalMs ?? 2000;
|
|
9
9
|
const deadline = Date.now() + (options.timeoutSeconds ?? 1800) * 1000;
|
|
@@ -16,20 +16,23 @@ export const waitForRun = async (client, runId, options = {}) => {
|
|
|
16
16
|
}
|
|
17
17
|
return payload;
|
|
18
18
|
};
|
|
19
|
-
export const
|
|
19
|
+
export const waitForRunState = async (client, runId, target, options = {}) => {
|
|
20
20
|
const pollIntervalMs = options.pollIntervalMs ?? 2000;
|
|
21
21
|
const deadline = Date.now() + (options.timeoutSeconds ?? 1800) * 1000;
|
|
22
22
|
let payload = await client.getRun(runId);
|
|
23
|
-
while (payload.run.
|
|
24
|
-
if (payload.run.
|
|
25
|
-
throw new Error(
|
|
23
|
+
while (payload.run.state !== target) {
|
|
24
|
+
if (payload.run.state === "deleting" || payload.run.state === "deleted") {
|
|
25
|
+
throw new Error("Run is being deleted.");
|
|
26
26
|
}
|
|
27
|
-
if (target === "running" && payload.run.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
if (target === "running" && payload.run.state === "stopped") {
|
|
28
|
+
const lifecycleMessage = typeof payload.run.lifecycle_error?.message === "string"
|
|
29
|
+
? payload.run.lifecycle_error.message
|
|
30
|
+
: null;
|
|
31
|
+
const detail = lifecycleMessage || payload.run.latest_execution?.error;
|
|
32
|
+
throw new Error(`Run ${runId} stopped before it became running${detail ? `: ${detail}` : "."}`);
|
|
32
33
|
}
|
|
34
|
+
if (Date.now() > deadline)
|
|
35
|
+
throw new Error(`Timed out waiting for run ${runId} to become ${target}.`);
|
|
33
36
|
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
34
37
|
payload = await client.getRun(runId);
|
|
35
38
|
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = "0.0.
|
|
1
|
+
export const version = "0.0.12";
|
package/dist/watch.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SanboxApiError } from "./api.js";
|
|
2
|
-
const
|
|
3
|
-
const terminalEventKinds = new Set(["run.completed", "run.failed", "run.
|
|
2
|
+
const terminalStates = new Set(["stopped", "deleting", "deleted"]);
|
|
3
|
+
const terminalEventKinds = new Set(["run.completed", "run.failed", "run.stopped"]);
|
|
4
4
|
const retryableNetworkCodes = new Set(["ECONNRESET", "ECONNREFUSED", "EPIPE", "ETIMEDOUT", "EAI_AGAIN", "ENETUNREACH"]);
|
|
5
5
|
export class WatchInterruptedError extends Error {
|
|
6
6
|
constructor() {
|
|
@@ -37,7 +37,7 @@ const assertActive = (signal, deadline, now, runId) => {
|
|
|
37
37
|
if (now() >= deadline)
|
|
38
38
|
throw new Error(`Timed out watching run ${runId}.`);
|
|
39
39
|
};
|
|
40
|
-
export const isTerminalStatus = (
|
|
40
|
+
export const isTerminalStatus = (state) => terminalStates.has(state);
|
|
41
41
|
export const watchEventsUntil = async (client, runId, options) => {
|
|
42
42
|
const pageSize = Math.max(1, Math.min(500, Math.floor(options.pageSize ?? 200)));
|
|
43
43
|
const pollIntervalMs = Math.max(1, Math.floor(options.pollIntervalMs ?? 2000));
|
|
@@ -156,7 +156,7 @@ export const watchRun = async (client, runId, options) => {
|
|
|
156
156
|
fetchAnotherPage = reportedMore && cursor > previousCursor;
|
|
157
157
|
}
|
|
158
158
|
const payload = await request(() => client.getRun(runId, options.signal));
|
|
159
|
-
if (isTerminalStatus(payload.run.
|
|
159
|
+
if (isTerminalStatus(payload.run.state)) {
|
|
160
160
|
emptyTerminalReads = deliveredThisCycle === 0 ? emptyTerminalReads + 1 : 0;
|
|
161
161
|
const requiredEmptyReads = sawTerminalEvent ? 1 : 2;
|
|
162
162
|
if (emptyTerminalReads >= requiredEmptyReads)
|