@sanlabs/sanbox-cli 0.0.4 → 0.0.5
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 +19 -5
- package/dist/api.js +67 -35
- package/dist/args.js +1 -1
- package/dist/cli.js +124 -123
- package/dist/config.js +3 -10
- package/dist/output.js +3 -1
- package/dist/runs.js +2 -3
- package/dist/version.js +1 -1
- package/dist/watch.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,28 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
Run isolated Sanbox agent tasks from a terminal, CI job, or autonomous coding agent.
|
|
4
4
|
|
|
5
|
+
Sanbox packages a task and selected inputs, starts an isolated runner, streams events, and keeps the sandbox and outputs indefinitely.
|
|
6
|
+
|
|
5
7
|
The full machine operating protocol is available at https://console.sanbox.cloud/agent.md and in [`web/public/agent.md`](../web/public/agent.md).
|
|
6
8
|
|
|
7
9
|
## Install
|
|
8
10
|
|
|
9
11
|
```bash
|
|
10
12
|
npm install -g @sanlabs/sanbox-cli@latest
|
|
11
|
-
sanbox
|
|
13
|
+
latest_cli_version="$(npm view @sanlabs/sanbox-cli version)"
|
|
14
|
+
installed_cli_version="$(sanbox --version)"
|
|
15
|
+
test "$installed_cli_version" = "$latest_cli_version"
|
|
12
16
|
```
|
|
13
17
|
|
|
14
|
-
CLI 0.0.
|
|
18
|
+
Always use the latest published CLI. CLI 0.0.5 adds organization discovery from the API key,
|
|
19
|
+
Hermes-aware template metadata, permanent paused sandboxes, and same-sandbox follow-ups.
|
|
15
20
|
|
|
16
21
|
## Configure
|
|
17
22
|
|
|
18
23
|
```bash
|
|
19
24
|
export SANBOX_API_URL=https://console.sanbox.cloud
|
|
20
25
|
export SANBOX_API_KEY=sbx_live_...
|
|
21
|
-
sanbox orgs list --json
|
|
22
|
-
export SANBOX_ORG=<returned-org-slug>
|
|
23
26
|
export SANBOX_TEMPLATE=<template-id-or-slug>
|
|
24
27
|
```
|
|
25
28
|
|
|
26
29
|
`SANBOX_API_URL` is optional for the hosted service. `SANBOX_TEMPLATE` can instead come from `--template` or `.sanbox/config.json` `default_template`.
|
|
30
|
+
The CLI derives the organization automatically from `SANBOX_API_KEY`; use an organization-scoped
|
|
31
|
+
key that has access to exactly one organization.
|
|
27
32
|
|
|
28
33
|
An org admin configures provider credentials and templates in the console. Do not pass provider keys to the CLI or a runner.
|
|
29
34
|
|
|
@@ -37,7 +42,9 @@ sanbox templates validate "$SANBOX_TEMPLATE" --json
|
|
|
37
42
|
sanbox doctor --json
|
|
38
43
|
```
|
|
39
44
|
|
|
40
|
-
Model IDs are provider-scoped. The CLI never guesses or silently substitutes
|
|
45
|
+
Model IDs are provider-scoped. The CLI never guesses or silently substitutes a provider, model, or template.
|
|
46
|
+
For waited task runs, choose a template with `runnable: true`, `template_type: "runner"`, and
|
|
47
|
+
`runner_config.harness: "opencode"`. Hermes service templates are always-on.
|
|
41
48
|
|
|
42
49
|
## Run Idempotently
|
|
43
50
|
|
|
@@ -93,6 +100,13 @@ sanbox runs message <run-id> "Summarize the retained output" --wait --json
|
|
|
93
100
|
|
|
94
101
|
The JSON response includes the user `message`, submitted `chat_job`, matching terminal `followup_event`, and conversation messages. The waiter matches `payload.chat_job_id`, so another concurrent follow-up cannot complete the wrong command.
|
|
95
102
|
|
|
103
|
+
Completed runs pause into a durable Firecracker snapshot. `sanbox runs message <run-id> --message "..."`
|
|
104
|
+
resumes the same writable sandbox and OpenCode session, then pauses it again. There is no retention
|
|
105
|
+
TTL; paused sandboxes and their artifacts remain available until explicitly deleted.
|
|
106
|
+
|
|
107
|
+
Check `sanbox runs get <run-id> --json` before a follow-up and require `sandbox_state: "paused"` with
|
|
108
|
+
a positive `snapshot_generation`. Do not submit concurrent follow-ups to one run.
|
|
109
|
+
|
|
96
110
|
## Batch Work
|
|
97
111
|
|
|
98
112
|
```json
|
package/dist/api.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CliError } from "./errors.js";
|
|
1
2
|
const errorRecord = (body) => {
|
|
2
3
|
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
3
4
|
return null;
|
|
@@ -38,6 +39,9 @@ export class SanboxApiError extends Error {
|
|
|
38
39
|
}
|
|
39
40
|
export class SanboxClient {
|
|
40
41
|
config;
|
|
42
|
+
mePromise = null;
|
|
43
|
+
organizationPromise = null;
|
|
44
|
+
organizationSlug = null;
|
|
41
45
|
constructor(config) {
|
|
42
46
|
this.config = config;
|
|
43
47
|
}
|
|
@@ -80,22 +84,50 @@ export class SanboxClient {
|
|
|
80
84
|
return res;
|
|
81
85
|
}
|
|
82
86
|
me() {
|
|
83
|
-
|
|
87
|
+
this.mePromise ||= this.request("/v1/me");
|
|
88
|
+
return this.mePromise;
|
|
89
|
+
}
|
|
90
|
+
organization() {
|
|
91
|
+
this.organizationPromise ||= this.resolveOrganization();
|
|
92
|
+
return this.organizationPromise;
|
|
93
|
+
}
|
|
94
|
+
resolvedOrganizationSlug() {
|
|
95
|
+
return this.organizationSlug;
|
|
96
|
+
}
|
|
97
|
+
async resolveOrganization() {
|
|
98
|
+
const me = await this.me();
|
|
99
|
+
const organizations = Array.isArray(me.organizations)
|
|
100
|
+
? me.organizations.filter((item) => Boolean(item)
|
|
101
|
+
&& typeof item === "object"
|
|
102
|
+
&& !Array.isArray(item)
|
|
103
|
+
&& typeof item.id === "string"
|
|
104
|
+
&& typeof item.slug === "string"
|
|
105
|
+
&& Boolean(String(item.slug).trim()))
|
|
106
|
+
: [];
|
|
107
|
+
if (organizations.length === 0) {
|
|
108
|
+
throw new CliError("organization_not_found", "The API key is not associated with an organization.");
|
|
109
|
+
}
|
|
110
|
+
if (organizations.length > 1) {
|
|
111
|
+
throw new CliError("organization_ambiguous", "The API key can access multiple organizations. Use an organization-scoped API key.");
|
|
112
|
+
}
|
|
113
|
+
this.organizationSlug = organizations[0].slug;
|
|
114
|
+
return organizations[0];
|
|
84
115
|
}
|
|
85
|
-
|
|
86
|
-
|
|
116
|
+
async orgPath(path) {
|
|
117
|
+
const organization = await this.organization();
|
|
118
|
+
return `/v1/orgs/${encodeURIComponent(organization.slug)}${path}`;
|
|
87
119
|
}
|
|
88
|
-
listRuns(limit = 50) {
|
|
89
|
-
return this.request(
|
|
120
|
+
async listRuns(limit = 50) {
|
|
121
|
+
return this.request(`${await this.orgPath("/runs")}?limit=${limit}`);
|
|
90
122
|
}
|
|
91
|
-
createRun(body) {
|
|
92
|
-
return this.request(
|
|
123
|
+
async createRun(body) {
|
|
124
|
+
return this.request(await this.orgPath("/runs"), {
|
|
93
125
|
method: "POST",
|
|
94
126
|
body: JSON.stringify(body)
|
|
95
127
|
});
|
|
96
128
|
}
|
|
97
|
-
uploadInputCollection(input) {
|
|
98
|
-
return this.request(
|
|
129
|
+
async uploadInputCollection(input) {
|
|
130
|
+
return this.request(await this.orgPath("/input-collections"), {
|
|
99
131
|
method: "POST",
|
|
100
132
|
headers: {
|
|
101
133
|
"Content-Type": "application/vnd.sanbox.input+zip",
|
|
@@ -105,8 +137,8 @@ export class SanboxClient {
|
|
|
105
137
|
body: Uint8Array.from(input.buffer).buffer
|
|
106
138
|
});
|
|
107
139
|
}
|
|
108
|
-
listModelProviders() {
|
|
109
|
-
return this.request(
|
|
140
|
+
async listModelProviders() {
|
|
141
|
+
return this.request(await this.orgPath("/model-providers"));
|
|
110
142
|
}
|
|
111
143
|
async getModelProvider(providerId) {
|
|
112
144
|
const payload = await this.listModelProviders();
|
|
@@ -115,49 +147,49 @@ export class SanboxClient {
|
|
|
115
147
|
throw new SanboxApiError(`Model provider ${providerId} was not found.`, 404, { code: "model_provider_not_found" });
|
|
116
148
|
return { provider };
|
|
117
149
|
}
|
|
118
|
-
listProviderModels(providerId) {
|
|
119
|
-
return this.request(
|
|
150
|
+
async listProviderModels(providerId) {
|
|
151
|
+
return this.request(await this.orgPath(`/model-providers/${encodeURIComponent(providerId)}/models`));
|
|
120
152
|
}
|
|
121
|
-
listTemplates() {
|
|
122
|
-
return this.request(
|
|
153
|
+
async listTemplates() {
|
|
154
|
+
return this.request(await this.orgPath("/templates"));
|
|
123
155
|
}
|
|
124
|
-
getTemplate(templateId) {
|
|
125
|
-
return this.request(
|
|
156
|
+
async getTemplate(templateId) {
|
|
157
|
+
return this.request(await this.orgPath(`/templates/${encodeURIComponent(templateId)}`));
|
|
126
158
|
}
|
|
127
|
-
validateTemplate(templateId) {
|
|
128
|
-
return this.request(
|
|
159
|
+
async validateTemplate(templateId) {
|
|
160
|
+
return this.request(await this.orgPath(`/templates/${encodeURIComponent(templateId)}/validate`));
|
|
129
161
|
}
|
|
130
|
-
createTemplate(body) {
|
|
131
|
-
return this.request(
|
|
162
|
+
async createTemplate(body) {
|
|
163
|
+
return this.request(await this.orgPath("/templates"), {
|
|
132
164
|
method: "POST",
|
|
133
165
|
body: JSON.stringify(body)
|
|
134
166
|
});
|
|
135
167
|
}
|
|
136
|
-
getRun(runId, signal) {
|
|
137
|
-
return this.request(
|
|
168
|
+
async getRun(runId, signal) {
|
|
169
|
+
return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}`), { signal });
|
|
138
170
|
}
|
|
139
|
-
listEvents(runId, afterEventId = 0, limit = 200, signal) {
|
|
140
|
-
return this.request(
|
|
171
|
+
async listEvents(runId, afterEventId = 0, limit = 200, signal) {
|
|
172
|
+
return this.request(`${await this.orgPath(`/runs/${encodeURIComponent(runId)}/events`)}?after_event_id=${afterEventId}&limit=${limit}`, { signal });
|
|
141
173
|
}
|
|
142
|
-
cancelRun(runId) {
|
|
143
|
-
return this.request(
|
|
174
|
+
async cancelRun(runId) {
|
|
175
|
+
return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/cancel`), {
|
|
144
176
|
method: "POST",
|
|
145
177
|
body: "{}"
|
|
146
178
|
});
|
|
147
179
|
}
|
|
148
|
-
sendMessage(runId, message, payload = {}) {
|
|
149
|
-
return this.request(
|
|
180
|
+
async sendMessage(runId, message, payload = {}) {
|
|
181
|
+
return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/messages`), {
|
|
150
182
|
method: "POST",
|
|
151
183
|
body: JSON.stringify({ message, payload })
|
|
152
184
|
});
|
|
153
185
|
}
|
|
154
|
-
listMessages(runId) {
|
|
155
|
-
return this.request(
|
|
186
|
+
async listMessages(runId) {
|
|
187
|
+
return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/messages`));
|
|
156
188
|
}
|
|
157
|
-
listArtifacts(runId) {
|
|
158
|
-
return this.request(
|
|
189
|
+
async listArtifacts(runId) {
|
|
190
|
+
return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/artifacts`));
|
|
159
191
|
}
|
|
160
|
-
downloadArtifact(runId, artifactPath) {
|
|
161
|
-
return this.rawRequest(
|
|
192
|
+
async downloadArtifact(runId, artifactPath) {
|
|
193
|
+
return this.rawRequest(`${await this.orgPath(`/runs/${encodeURIComponent(runId)}/artifacts`)}?path=${encodeURIComponent(artifactPath)}`);
|
|
162
194
|
}
|
|
163
195
|
}
|
package/dist/args.js
CHANGED
package/dist/cli.js
CHANGED
|
@@ -16,14 +16,12 @@ const help = `Sanbox CLI
|
|
|
16
16
|
|
|
17
17
|
Environment:
|
|
18
18
|
SANBOX_API_URL Sanbox API base URL
|
|
19
|
-
SANBOX_ORG Organization slug
|
|
20
19
|
SANBOX_API_KEY Org API key
|
|
21
20
|
SANBOX_TEMPLATE Explicit template id or slug for run and batch
|
|
22
21
|
|
|
23
22
|
Commands:
|
|
24
23
|
sanbox --version
|
|
25
24
|
sanbox auth check [--json]
|
|
26
|
-
sanbox orgs list [--json]
|
|
27
25
|
sanbox context [--json]
|
|
28
26
|
sanbox doctor [--json]
|
|
29
27
|
sanbox model-providers list [--json]
|
|
@@ -32,7 +30,7 @@ Commands:
|
|
|
32
30
|
sanbox templates list [--json]
|
|
33
31
|
sanbox templates get <template-id> [--json]
|
|
34
32
|
sanbox templates validate <template-id> [--json]
|
|
35
|
-
sanbox templates create --name "..." --model-provider <provider-id> --model <model-id> [--llm-budget-usd <amount>] [--
|
|
33
|
+
sanbox templates create --name "..." --model-provider <provider-id> --model <model-id> [--harness opencode|hermes] [--llm-budget-usd <amount>] [--json]
|
|
36
34
|
sanbox run "task" --template <template-id> [--input <path>] [--wait | --watch] [--json | --jsonl]
|
|
37
35
|
sanbox run --task "..." --template <template-id> [--input <path>] [--wait | --watch] [--json | --jsonl]
|
|
38
36
|
sanbox batch --tasks tasks.json --template <template-id> [--input <path>] [--max-parallel 5] [--wait] [--json]
|
|
@@ -58,7 +56,6 @@ Options:
|
|
|
58
56
|
--input <path> File, directory, or glob to upload. Repeatable.
|
|
59
57
|
--template <id> Template id or slug. Required unless SANBOX_TEMPLATE or project config sets it.
|
|
60
58
|
--external-run-id <id> Idempotency key for retries.
|
|
61
|
-
--retention-ttl-seconds <n> Workspace retention TTL. Default: 86400.
|
|
62
59
|
--dry-run Preview included files without creating a run.
|
|
63
60
|
--wait Poll until terminal status.
|
|
64
61
|
--watch Stream activity until terminal status.
|
|
@@ -73,7 +70,7 @@ const doctorHelp = `Sanbox doctor
|
|
|
73
70
|
Usage:
|
|
74
71
|
sanbox doctor [--json]
|
|
75
72
|
|
|
76
|
-
Checks local configuration, API health, auth,
|
|
73
|
+
Checks local configuration, API health, auth, organization discovery, and template access.
|
|
77
74
|
SANBOX_API_KEY is still read only from the environment.
|
|
78
75
|
`;
|
|
79
76
|
const modelProvidersHelp = `Sanbox model providers
|
|
@@ -92,26 +89,34 @@ Usage:
|
|
|
92
89
|
sanbox templates list [--json]
|
|
93
90
|
sanbox templates get <template-id> [--json]
|
|
94
91
|
sanbox templates validate <template-id> [--json]
|
|
95
|
-
sanbox templates create --name "Code review" --model-provider <provider-id> --model <model-id> [--llm-budget-usd <amount>] [--
|
|
92
|
+
sanbox templates create --name "Code review" --model-provider <provider-id> --model <model-id> [--harness opencode|hermes] [--llm-budget-usd <amount>] [--json]
|
|
96
93
|
|
|
97
94
|
Template creation requires an exact provider id and that provider's exact model id.
|
|
98
95
|
LiteLLM budgets are optional USD amounts and apply separately to each run.
|
|
96
|
+
Hermes templates are always-on Telegram gateways. Set SANBOX_TELEGRAM_BOT_TOKEN
|
|
97
|
+
and repeat --telegram-allowed-user <numeric-id> for every permitted user.
|
|
99
98
|
`;
|
|
100
99
|
const agentInstructions = `# Operate Sanbox Autonomously
|
|
101
100
|
|
|
102
101
|
Use the \`sanbox\` CLI for focused, isolated, long-running, risky, or parallel work. The canonical
|
|
103
|
-
protocol is https://console.sanbox.cloud/agent.md.
|
|
102
|
+
protocol is https://console.sanbox.cloud/agent.md.
|
|
103
|
+
|
|
104
|
+
Always use the latest published CLI. Install and verify it at the start of every operating session:
|
|
105
|
+
\`\`\`bash
|
|
106
|
+
npm install -g @sanlabs/sanbox-cli@latest
|
|
107
|
+
latest_cli_version="$(npm view @sanlabs/sanbox-cli version)"
|
|
108
|
+
installed_cli_version="$(sanbox --version)"
|
|
109
|
+
test "$installed_cli_version" = "$latest_cli_version"
|
|
110
|
+
\`\`\`
|
|
104
111
|
|
|
105
112
|
Human bootstrap:
|
|
106
|
-
- A human/admin supplies SANBOX_API_KEY and configures provider credentials plus a runnable template.
|
|
113
|
+
- A human/admin supplies an organization-scoped SANBOX_API_KEY and configures provider credentials plus a runnable template.
|
|
107
114
|
- Never print, persist, prompt with, or upload API keys or provider credentials.
|
|
108
115
|
- If the key or a runnable template is unavailable, stop with the exact human action required.
|
|
109
116
|
|
|
110
117
|
Deterministic startup:
|
|
111
118
|
\`\`\`bash
|
|
112
119
|
sanbox --version
|
|
113
|
-
sanbox orgs list --json
|
|
114
|
-
export SANBOX_ORG=<returned-org-slug>
|
|
115
120
|
sanbox auth check --json
|
|
116
121
|
sanbox context --json
|
|
117
122
|
sanbox templates list --json
|
|
@@ -120,8 +125,11 @@ sanbox templates validate "$SANBOX_TEMPLATE" --json
|
|
|
120
125
|
sanbox doctor --json
|
|
121
126
|
\`\`\`
|
|
122
127
|
|
|
123
|
-
Never guess opaque IDs.
|
|
124
|
-
|
|
128
|
+
Never guess opaque IDs. The CLI derives the organization from the API key and requires the key to
|
|
129
|
+
resolve to exactly one organization. For task execution, select only a template with runnable: true,
|
|
130
|
+
template_type: "runner", and runner_config.harness: "opencode". Never select a Hermes service
|
|
131
|
+
template for a waited task because it is always-on. Select automatically only when exactly one task
|
|
132
|
+
template qualifies; otherwise ask the user. Provider credentials and template administration are
|
|
125
133
|
console-only.
|
|
126
134
|
|
|
127
135
|
Use --json for request/response commands and --jsonl for streams. Parse the versioned envelope:
|
|
@@ -139,7 +147,7 @@ sanbox run "Investigate one focused task and write output/report.md" \\
|
|
|
139
147
|
|
|
140
148
|
Reuse the same --external-run-id after ambiguous failures. Retry network errors, HTTP 429, HTTP 5xx,
|
|
141
149
|
and workspace_busy with bounded backoff. Do not retry other 4xx errors unless next_actions directs
|
|
142
|
-
recovery.
|
|
150
|
+
recovery. Terminal statuses are completed, failed, and canceled.
|
|
143
151
|
|
|
144
152
|
Recover and retrieve results:
|
|
145
153
|
\`\`\`bash
|
|
@@ -154,33 +162,38 @@ sanbox runs download <run-id> --output .sanbox/output/<run-id> --json
|
|
|
154
162
|
Tasks must put durable files under /workspace/output. Downloads preserve relative paths and report
|
|
155
163
|
byte counts plus SHA-256 digests; existing files require explicit --overwrite.
|
|
156
164
|
|
|
157
|
-
Continue through the
|
|
165
|
+
Continue through the same paused writable sandbox:
|
|
158
166
|
\`\`\`bash
|
|
159
167
|
sanbox runs messages <run-id> --json
|
|
168
|
+
sanbox runs get <run-id> --json
|
|
160
169
|
sanbox runs message <run-id> "Summarize the retained output" --wait --json
|
|
161
170
|
\`\`\`
|
|
162
171
|
|
|
163
|
-
|
|
172
|
+
Before a follow-up, require sandbox_state: "paused" and a positive snapshot_generation. The message
|
|
173
|
+
resumes the same sandbox and OpenCode session, then pauses it as a new generation. Follow-up waits
|
|
174
|
+
are correlated to the returned chat_job.id. Never submit concurrent follow-ups to one run. Retry
|
|
175
|
+
sandbox_not_paused only after the run returns to paused; treat sandbox_not_resumable,
|
|
176
|
+
sandbox_snapshot_missing, sandbox_worker_missing, and hermes_uses_channel as blockers.
|
|
177
|
+
|
|
164
178
|
For independent fan-out, use \`sanbox batch\` with a stable external_run_id per task and keep the
|
|
165
179
|
client alive until submission completes.
|
|
166
180
|
|
|
167
181
|
Do not claim completion until the run is completed, required artifacts are downloaded and verified,
|
|
168
|
-
and required follow-ups succeeded. Report run/external/template IDs, status,
|
|
169
|
-
and blockers. The CLI excludes common secrets by default; add
|
|
182
|
+
and required follow-ups succeeded. Report run/external/template IDs, status, sandbox state, snapshot
|
|
183
|
+
generation, artifact paths/digests, and blockers. The CLI excludes common secrets by default; add
|
|
184
|
+
.sanboxignore for project rules.
|
|
170
185
|
`;
|
|
171
186
|
const cwd = () => process.cwd();
|
|
172
187
|
const makeClient = (flags) => new SanboxClient(readConfig(flags));
|
|
173
|
-
const makeAuthClient = (flags) => new SanboxClient(readConfig(flags, { requireOrg: false }));
|
|
174
188
|
const jsonContext = (client) => ({
|
|
175
189
|
api_url: client.config.apiUrl,
|
|
176
|
-
org: client.
|
|
190
|
+
...(client.resolvedOrganizationSlug() ? { org: client.resolvedOrganizationSlug() } : {})
|
|
177
191
|
});
|
|
178
192
|
const localJsonContext = (flags) => {
|
|
179
193
|
try {
|
|
180
194
|
const local = readLocalConfig();
|
|
181
195
|
const apiUrl = String(flags["api-url"] || process.env.SANBOX_API_URL || local.api_url || defaultApiUrl).replace(/\/+$/, "");
|
|
182
|
-
|
|
183
|
-
return { api_url: apiUrl, ...(org ? { org } : {}) };
|
|
196
|
+
return { api_url: apiUrl };
|
|
184
197
|
}
|
|
185
198
|
catch {
|
|
186
199
|
return {};
|
|
@@ -191,7 +204,7 @@ const consoleUrl = (client) => consolePathUrl(client, "/model-providers");
|
|
|
191
204
|
const providerConsoleAction = (client) => consoleAction(consoleUrl(client), "Ask an organization admin to configure or repair the model provider in the Sanbox console.");
|
|
192
205
|
const templateAdminConsoleAction = (client) => consoleAction(consolePathUrl(client, "/templates"), "Ask an organization admin to create a template in the Sanbox console.");
|
|
193
206
|
const templateActions = () => [
|
|
194
|
-
commandAction(["sanbox", "templates", "list", "--json"], "List templates available to the
|
|
207
|
+
commandAction(["sanbox", "templates", "list", "--json"], "List templates available to the API key's organization."),
|
|
195
208
|
commandAction(["sanbox", "templates", "validate", "<template-id>", "--json"], "Check whether a template is runnable.")
|
|
196
209
|
];
|
|
197
210
|
const runReadinessCodes = new Set([
|
|
@@ -273,10 +286,9 @@ const formatBytes = (bytes) => {
|
|
|
273
286
|
};
|
|
274
287
|
const positionalText = (command, startIndex) => command.slice(startIndex).join(" ").trim();
|
|
275
288
|
const runTask = (command, flags) => flagString(flags, "task") || positionalText(command, 1);
|
|
276
|
-
const commonFlags = ["api-url", "
|
|
289
|
+
const commonFlags = ["api-url", "json", "help"];
|
|
277
290
|
const flagSets = {
|
|
278
291
|
"auth.check": commonFlags,
|
|
279
|
-
"orgs.list": commonFlags,
|
|
280
292
|
context: [...commonFlags, "template"],
|
|
281
293
|
doctor: [...commonFlags, "template"],
|
|
282
294
|
"model-providers.list": commonFlags,
|
|
@@ -285,15 +297,24 @@ const flagSets = {
|
|
|
285
297
|
"templates.list": commonFlags,
|
|
286
298
|
"templates.get": commonFlags,
|
|
287
299
|
"templates.validate": commonFlags,
|
|
288
|
-
"templates.create": [
|
|
300
|
+
"templates.create": [
|
|
301
|
+
...commonFlags,
|
|
302
|
+
"name",
|
|
303
|
+
"model-provider",
|
|
304
|
+
"model",
|
|
305
|
+
"harness",
|
|
306
|
+
"llm-budget-usd",
|
|
307
|
+
"web-access",
|
|
308
|
+
"telegram-allowed-user"
|
|
309
|
+
],
|
|
289
310
|
run: [
|
|
290
|
-
...commonFlags, "task", "input", "template", "external-run-id",
|
|
311
|
+
...commonFlags, "task", "input", "template", "external-run-id",
|
|
291
312
|
"dry-run", "wait", "watch", "jsonl", "view", "after-event-id", "cancel-on-interrupt",
|
|
292
313
|
"poll-interval-ms", "event-page-size", "timeout-seconds", "verbose"
|
|
293
314
|
],
|
|
294
315
|
batch: [
|
|
295
316
|
...commonFlags, "tasks", "template", "input", "max-parallel", "wait", "batch-id",
|
|
296
|
-
"
|
|
317
|
+
"poll-interval-ms", "timeout-seconds"
|
|
297
318
|
],
|
|
298
319
|
"runs.list": [...commonFlags, "limit"],
|
|
299
320
|
"runs.get": commonFlags,
|
|
@@ -321,8 +342,6 @@ const commandKey = (command) => {
|
|
|
321
342
|
return "version";
|
|
322
343
|
if (command[0] === "auth")
|
|
323
344
|
return `auth.${command[1] || ""}`;
|
|
324
|
-
if (command[0] === "orgs")
|
|
325
|
-
return `orgs.${command[1] || ""}`;
|
|
326
345
|
if (command[0] === "model-providers" || command[0] === "templates" || command[0] === "runs") {
|
|
327
346
|
return `${command[0]}.${command[1] || ""}`;
|
|
328
347
|
}
|
|
@@ -331,10 +350,11 @@ const commandKey = (command) => {
|
|
|
331
350
|
return command[0];
|
|
332
351
|
};
|
|
333
352
|
const requiredValueFlags = new Set([
|
|
334
|
-
"api-url", "
|
|
335
|
-
"
|
|
353
|
+
"api-url", "template", "task", "input", "include", "external-run-id",
|
|
354
|
+
"tasks", "max-parallel", "batch-id", "poll-interval-ms",
|
|
336
355
|
"event-page-size", "timeout-seconds", "view", "after-event-id", "limit", "name",
|
|
337
|
-
"model-provider", "model", "llm-budget-usd", "
|
|
356
|
+
"model-provider", "model", "harness", "llm-budget-usd", "telegram-allowed-user",
|
|
357
|
+
"message", "output", "artifact"
|
|
338
358
|
]);
|
|
339
359
|
const validateFlagValues = (flags) => {
|
|
340
360
|
for (const flag of requiredValueFlags) {
|
|
@@ -372,7 +392,7 @@ const validateFlags = (command, flags) => {
|
|
|
372
392
|
return;
|
|
373
393
|
}
|
|
374
394
|
const knownRoots = new Set([
|
|
375
|
-
"auth", "
|
|
395
|
+
"auth", "context", "doctor", "model-providers", "templates",
|
|
376
396
|
"run", "batch", "runs", "init", "version"
|
|
377
397
|
]);
|
|
378
398
|
const allowed = flagSets[key] ?? (knownRoots.has(command[0] || "") ? commonFlags : null);
|
|
@@ -388,7 +408,6 @@ const validatePositionals = (command, flags) => {
|
|
|
388
408
|
return;
|
|
389
409
|
const maximums = {
|
|
390
410
|
"auth.check": 2,
|
|
391
|
-
"orgs.list": 2,
|
|
392
411
|
context: 1,
|
|
393
412
|
doctor: 1,
|
|
394
413
|
"model-providers.list": 2,
|
|
@@ -533,8 +552,7 @@ const commandRun = async (command, flags) => {
|
|
|
533
552
|
instruction: task,
|
|
534
553
|
inputs: flagList(flags, "input"),
|
|
535
554
|
externalRunId: flagString(flags, "external-run-id") || undefined,
|
|
536
|
-
templateId: template.id
|
|
537
|
-
retentionTtlSeconds: integerFlag(flags, "retention-ttl-seconds", 86400, 0, Number.MAX_SAFE_INTEGER)
|
|
555
|
+
templateId: template.id
|
|
538
556
|
});
|
|
539
557
|
}
|
|
540
558
|
catch (error) {
|
|
@@ -587,8 +605,7 @@ const commandBatch = async (flags) => {
|
|
|
587
605
|
instruction: task.task,
|
|
588
606
|
inputs: task.input || inputs,
|
|
589
607
|
externalRunId: task.external_run_id || `sanbox-batch-${batchId}-${index + 1}`,
|
|
590
|
-
templateId: template.id
|
|
591
|
-
retentionTtlSeconds: integerFlag(flags, "retention-ttl-seconds", 86400, 0, Number.MAX_SAFE_INTEGER)
|
|
608
|
+
templateId: template.id
|
|
592
609
|
});
|
|
593
610
|
}
|
|
594
611
|
catch (error) {
|
|
@@ -617,50 +634,28 @@ const commandBatch = async (flags) => {
|
|
|
617
634
|
};
|
|
618
635
|
const commandAuthCheck = async (flags) => {
|
|
619
636
|
const client = makeClient(flags);
|
|
620
|
-
const me = await client.me();
|
|
621
|
-
const org =
|
|
622
|
-
const
|
|
623
|
-
? me.organizations
|
|
624
|
-
: [];
|
|
625
|
-
const selected = organizations.find((item) => item.slug === org) || null;
|
|
626
|
-
const output = { ok: Boolean(selected), org, selected, me };
|
|
637
|
+
const [me, selected] = await Promise.all([client.me(), client.organization()]);
|
|
638
|
+
const org = selected.slug;
|
|
639
|
+
const output = { ok: true, org, selected, me };
|
|
627
640
|
if (hasFlag(flags, "json"))
|
|
628
641
|
printSuccess("auth.check", output, jsonContext(client));
|
|
629
642
|
else
|
|
630
|
-
process.stdout.write(
|
|
631
|
-
if (!selected)
|
|
632
|
-
process.exitCode = 2;
|
|
633
|
-
};
|
|
634
|
-
const commandOrganizations = async (command, flags) => {
|
|
635
|
-
if (command[1] !== "list")
|
|
636
|
-
throw new CliError("orgs_action_required", "orgs requires the list action.");
|
|
637
|
-
const client = makeAuthClient(flags);
|
|
638
|
-
const payload = await client.listOrganizations();
|
|
639
|
-
if (hasFlag(flags, "json")) {
|
|
640
|
-
printSuccess("orgs.list", payload, { api_url: client.config.apiUrl });
|
|
641
|
-
return;
|
|
642
|
-
}
|
|
643
|
-
for (const organization of payload.organizations) {
|
|
644
|
-
process.stdout.write(`${organization.slug}${organization.name ? `\t${organization.name}` : ""}${organization.membership_role ? `\t${organization.membership_role}` : ""}\n`);
|
|
645
|
-
}
|
|
643
|
+
process.stdout.write(`ok org=${org}\n`);
|
|
646
644
|
};
|
|
647
645
|
const commandContext = async (flags) => {
|
|
648
646
|
const client = makeClient(flags);
|
|
649
647
|
const selection = readTemplateSelection(flags, { required: false });
|
|
650
|
-
const [me, providerPayload, templatePayload] = await Promise.all([
|
|
648
|
+
const [organization, me, providerPayload, templatePayload] = await Promise.all([
|
|
649
|
+
client.organization(),
|
|
651
650
|
client.me(),
|
|
652
651
|
client.listModelProviders(),
|
|
653
652
|
client.listTemplates()
|
|
654
653
|
]);
|
|
655
|
-
const organizations = Array.isArray(me.organizations)
|
|
656
|
-
? me.organizations
|
|
657
|
-
: [];
|
|
658
|
-
const selectedOrganization = organizations.find((item) => item.slug === client.config.org) || null;
|
|
659
654
|
const selectedTemplate = selection
|
|
660
655
|
? templatePayload.templates.find((item) => item.id === selection.id || item.template_slug === selection.id) || null
|
|
661
656
|
: null;
|
|
662
657
|
const output = {
|
|
663
|
-
organization
|
|
658
|
+
organization,
|
|
664
659
|
authentication: me.auth || null,
|
|
665
660
|
template_selection: selection
|
|
666
661
|
? { id: selection.id, source: selection.source, found: Boolean(selectedTemplate), template: selectedTemplate }
|
|
@@ -669,22 +664,17 @@ const commandContext = async (flags) => {
|
|
|
669
664
|
templates: templatePayload.templates
|
|
670
665
|
};
|
|
671
666
|
const nextActions = [];
|
|
672
|
-
if (!selectedOrganization) {
|
|
673
|
-
nextActions.push(commandAction(["sanbox", "auth", "check", "--json"], "Verify that the API key can access the selected organization."));
|
|
674
|
-
}
|
|
675
667
|
if (!selection) {
|
|
676
668
|
nextActions.push(commandAction(["sanbox", "templates", "list", "--json"], "List available templates."), commandAction(["sanbox", "context", "--json"], "Select a template for this shell and inspect the resolved context.", { SANBOX_TEMPLATE: "<template-id>" }));
|
|
677
669
|
}
|
|
678
670
|
else if (!selectedTemplate) {
|
|
679
671
|
nextActions.push(commandAction(["sanbox", "templates", "list", "--json"], "Choose a template that exists in this organization."));
|
|
680
672
|
}
|
|
681
|
-
if (!selectedOrganization)
|
|
682
|
-
process.exitCode = 2;
|
|
683
673
|
if (hasFlag(flags, "json")) {
|
|
684
674
|
printSuccess("context.get", output, jsonContext(client), nextActions);
|
|
685
675
|
return;
|
|
686
676
|
}
|
|
687
|
-
process.stdout.write(`org=${
|
|
677
|
+
process.stdout.write(`org=${organization.slug}\n`);
|
|
688
678
|
process.stdout.write(`template=${selection ? `${selection.id} source=${selection.source}${selectedTemplate ? "" : " missing"}` : "not selected"}\n`);
|
|
689
679
|
process.stdout.write(`model_providers=${providerPayload.providers.map((item) => providerId(item)).filter(Boolean).join(",") || "none"}\n`);
|
|
690
680
|
process.stdout.write(`templates=${templatePayload.templates.map((item) => item.template_slug || item.id).join(",") || "none"}\n`);
|
|
@@ -767,7 +757,7 @@ const commandTemplates = async (command, flags) => {
|
|
|
767
757
|
const payload = await client.listTemplates();
|
|
768
758
|
const nextActions = payload.templates.length === 0
|
|
769
759
|
? [
|
|
770
|
-
commandAction(["sanbox", "context", "--json"], "Inspect the
|
|
760
|
+
commandAction(["sanbox", "context", "--json"], "Inspect the API key's organization and current authentication role."),
|
|
771
761
|
templateAdminConsoleAction(client)
|
|
772
762
|
]
|
|
773
763
|
: [];
|
|
@@ -791,7 +781,7 @@ const commandTemplates = async (command, flags) => {
|
|
|
791
781
|
throw new CliError("template_not_found", error.message, {
|
|
792
782
|
status: error.status,
|
|
793
783
|
details: { template_id: id },
|
|
794
|
-
nextActions: [commandAction(["sanbox", "templates", "list", "--json"], "List templates available to the
|
|
784
|
+
nextActions: [commandAction(["sanbox", "templates", "list", "--json"], "List templates available to the API key's organization.")]
|
|
795
785
|
});
|
|
796
786
|
}
|
|
797
787
|
throw error;
|
|
@@ -814,7 +804,7 @@ const commandTemplates = async (command, flags) => {
|
|
|
814
804
|
throw new CliError("template_validation_failed", error.message, {
|
|
815
805
|
status: error.status,
|
|
816
806
|
details: { template_id: id },
|
|
817
|
-
nextActions: [commandAction(["sanbox", "templates", "list", "--json"], "List templates available to the
|
|
807
|
+
nextActions: [commandAction(["sanbox", "templates", "list", "--json"], "List templates available to the API key's organization.")]
|
|
818
808
|
});
|
|
819
809
|
}
|
|
820
810
|
throw error;
|
|
@@ -835,6 +825,10 @@ const commandTemplates = async (command, flags) => {
|
|
|
835
825
|
const name = requiredPositional(flagString(flags, "name"), "template_name_required", "templates create requires --name.");
|
|
836
826
|
const modelProvider = requiredPositional(flagString(flags, "model-provider"), "model_provider_required", "templates create requires --model-provider.");
|
|
837
827
|
const model = requiredPositional(flagString(flags, "model"), "model_required", "templates create requires --model.");
|
|
828
|
+
const harness = flagString(flags, "harness", "opencode");
|
|
829
|
+
if (harness !== "opencode" && harness !== "hermes") {
|
|
830
|
+
throw new CliError("invalid_harness", "templates create --harness must be opencode or hermes.");
|
|
831
|
+
}
|
|
838
832
|
const budgetRaw = flagString(flags, "llm-budget-usd");
|
|
839
833
|
const parsedBudgetUsd = budgetRaw ? Number(budgetRaw) : undefined;
|
|
840
834
|
const llmBudgetUsd = parsedBudgetUsd === undefined
|
|
@@ -844,14 +838,29 @@ const commandTemplates = async (command, flags) => {
|
|
|
844
838
|
(!Number.isFinite(parsedBudgetUsd) || llmBudgetUsd <= 0 || llmBudgetUsd > 100_000)) {
|
|
845
839
|
throw new CliError("invalid_llm_budget", "templates create --llm-budget-usd must be at least 0.000001 and no more than 100000.");
|
|
846
840
|
}
|
|
841
|
+
if (harness === "hermes" && llmBudgetUsd !== undefined) {
|
|
842
|
+
throw new CliError("hermes_budget_unsupported", "Always-on Hermes templates do not support --llm-budget-usd yet.");
|
|
843
|
+
}
|
|
844
|
+
const telegramBotToken = process.env.SANBOX_TELEGRAM_BOT_TOKEN?.trim() || "";
|
|
845
|
+
const telegramAllowedUsers = flagList(flags, "telegram-allowed-user").map((value) => value.trim()).filter(Boolean);
|
|
846
|
+
if (harness === "hermes" && !telegramBotToken) {
|
|
847
|
+
throw new CliError("telegram_bot_token_required", "Hermes template creation requires SANBOX_TELEGRAM_BOT_TOKEN.");
|
|
848
|
+
}
|
|
849
|
+
if (harness === "hermes" && telegramAllowedUsers.length === 0) {
|
|
850
|
+
throw new CliError("telegram_allowed_user_required", "Hermes template creation requires at least one --telegram-allowed-user.");
|
|
851
|
+
}
|
|
847
852
|
let payload;
|
|
848
853
|
try {
|
|
849
854
|
payload = await client.createTemplate({
|
|
850
855
|
name,
|
|
851
856
|
provider_id: modelProvider,
|
|
852
857
|
model_id: model,
|
|
853
|
-
|
|
854
|
-
...(
|
|
858
|
+
...(llmBudgetUsd === undefined ? {} : { llm_budget_usd: llmBudgetUsd }),
|
|
859
|
+
...(harness === "hermes" ? {
|
|
860
|
+
harness,
|
|
861
|
+
telegram_bot_token: telegramBotToken,
|
|
862
|
+
telegram_allowed_users: telegramAllowedUsers
|
|
863
|
+
} : {})
|
|
855
864
|
});
|
|
856
865
|
}
|
|
857
866
|
catch (error) {
|
|
@@ -860,7 +869,9 @@ const commandTemplates = async (command, flags) => {
|
|
|
860
869
|
if (hasFlag(flags, "json")) {
|
|
861
870
|
printSuccess("templates.create", payload, jsonContext(client), [
|
|
862
871
|
commandAction(["sanbox", "templates", "validate", payload.template.id, "--json"], "Validate the new template before running it."),
|
|
863
|
-
commandAction(["sanbox", "run", "<task>", "--template", payload.template.id],
|
|
872
|
+
commandAction(["sanbox", "run", harness === "hermes" ? "<agent role and instructions>" : "<task>", "--template", payload.template.id], harness === "hermes"
|
|
873
|
+
? "Start the always-on gateway; interact with it through the configured Telegram bot."
|
|
874
|
+
: "Create a run with the new template.")
|
|
864
875
|
]);
|
|
865
876
|
return;
|
|
866
877
|
}
|
|
@@ -1096,9 +1107,9 @@ const commandRuns = async (command, flags) => {
|
|
|
1096
1107
|
const commandDoctor = async (flags) => {
|
|
1097
1108
|
const localConfig = readLocalConfig();
|
|
1098
1109
|
const apiUrl = String(flags["api-url"] || process.env.SANBOX_API_URL || localConfig.api_url || defaultApiUrl).replace(/\/+$/, "");
|
|
1099
|
-
const org = String(flags.org || process.env.SANBOX_ORG || localConfig.org || "").trim();
|
|
1100
1110
|
const apiKey = process.env.SANBOX_API_KEY || "";
|
|
1101
1111
|
const selection = readTemplateSelection(flags, { required: false });
|
|
1112
|
+
let resolvedOrg = "";
|
|
1102
1113
|
const checks = [];
|
|
1103
1114
|
const nextActions = [];
|
|
1104
1115
|
const add = (name, ok, detail, data) => checks.push({
|
|
@@ -1109,11 +1120,7 @@ const commandDoctor = async (flags) => {
|
|
|
1109
1120
|
});
|
|
1110
1121
|
add("node", Number(process.versions.node.split(".")[0]) >= 20, process.version);
|
|
1111
1122
|
add("api_url", Boolean(apiUrl), apiUrl);
|
|
1112
|
-
add("org", Boolean(org), org || "missing SANBOX_ORG, --org, or .sanbox/config.json org");
|
|
1113
1123
|
add("api_key", Boolean(apiKey), apiKey ? "present in environment" : "missing SANBOX_API_KEY");
|
|
1114
|
-
if (!org) {
|
|
1115
|
-
nextActions.push(commandAction(["sanbox", "doctor", "--json"], "Select an organization and run the checks again.", { SANBOX_ORG: "<org-slug>" }));
|
|
1116
|
-
}
|
|
1117
1124
|
if (!apiKey) {
|
|
1118
1125
|
nextActions.push(commandAction(["sanbox", "doctor", "--json"], "Set a Sanbox control-plane API key and run the checks again.", { SANBOX_API_KEY: "<sanbox-api-key>" }));
|
|
1119
1126
|
}
|
|
@@ -1124,50 +1131,50 @@ const commandDoctor = async (flags) => {
|
|
|
1124
1131
|
catch (error) {
|
|
1125
1132
|
add("api_health", false, error instanceof Error ? error.message : String(error));
|
|
1126
1133
|
}
|
|
1127
|
-
if (
|
|
1128
|
-
const client = new SanboxClient({ apiUrl,
|
|
1134
|
+
if (apiKey) {
|
|
1135
|
+
const client = new SanboxClient({ apiUrl, apiKey });
|
|
1129
1136
|
try {
|
|
1130
|
-
const
|
|
1131
|
-
|
|
1132
|
-
? me.organizations
|
|
1133
|
-
: [];
|
|
1137
|
+
const organization = await client.organization();
|
|
1138
|
+
resolvedOrg = organization.slug;
|
|
1134
1139
|
add("auth", true, "authenticated");
|
|
1135
|
-
add("
|
|
1140
|
+
add("organization", true, organization.slug, organization);
|
|
1136
1141
|
}
|
|
1137
1142
|
catch (error) {
|
|
1138
1143
|
add("auth", false, error instanceof Error ? error.message : String(error));
|
|
1139
1144
|
}
|
|
1140
|
-
|
|
1141
|
-
const providers = await client.listModelProviders();
|
|
1142
|
-
add("model_providers", true, providers.providers.map((item) => `${providerId(item)}:${item.status || (item.configured ? "configured" : "not_configured")}`).join(", ") || "none", providers.providers);
|
|
1143
|
-
}
|
|
1144
|
-
catch (error) {
|
|
1145
|
-
add("model_providers", false, error instanceof Error ? error.message : String(error));
|
|
1146
|
-
}
|
|
1147
|
-
if (!selection) {
|
|
1148
|
-
add("template", false, "not selected; use --template, SANBOX_TEMPLATE, or .sanbox/config.json default_template");
|
|
1149
|
-
nextActions.push(...templateActions());
|
|
1150
|
-
}
|
|
1151
|
-
else {
|
|
1145
|
+
if (resolvedOrg) {
|
|
1152
1146
|
try {
|
|
1153
|
-
const
|
|
1154
|
-
add("
|
|
1147
|
+
const providers = await client.listModelProviders();
|
|
1148
|
+
add("model_providers", true, providers.providers.map((item) => `${providerId(item)}:${item.status || (item.configured ? "configured" : "not_configured")}`).join(", ") || "none", providers.providers);
|
|
1149
|
+
}
|
|
1150
|
+
catch (error) {
|
|
1151
|
+
add("model_providers", false, error instanceof Error ? error.message : String(error));
|
|
1152
|
+
}
|
|
1153
|
+
if (!selection) {
|
|
1154
|
+
add("template", false, "not selected; use --template, SANBOX_TEMPLATE, or .sanbox/config.json default_template");
|
|
1155
|
+
nextActions.push(...templateActions());
|
|
1156
|
+
}
|
|
1157
|
+
else {
|
|
1155
1158
|
try {
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1159
|
+
const template = await client.getTemplate(selection.id);
|
|
1160
|
+
add("template", true, `${template.template.id}; source=${selection.source}`, template.template);
|
|
1161
|
+
try {
|
|
1162
|
+
const validation = await client.validateTemplate(selection.id);
|
|
1163
|
+
const runnable = validationRunnable(validation);
|
|
1164
|
+
add("template_readiness", runnable, runnable ? "ready" : "blocked", validation);
|
|
1165
|
+
if (!runnable) {
|
|
1166
|
+
nextActions.push(commandAction(["sanbox", "model-providers", "list", "--json"], "Inspect model-provider status."), providerConsoleAction(client));
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
catch (error) {
|
|
1170
|
+
add("template_readiness", false, error instanceof Error ? error.message : String(error));
|
|
1161
1171
|
}
|
|
1162
1172
|
}
|
|
1163
1173
|
catch (error) {
|
|
1164
|
-
add("
|
|
1174
|
+
add("template", false, error instanceof Error ? error.message : String(error));
|
|
1175
|
+
nextActions.push(...templateActions());
|
|
1165
1176
|
}
|
|
1166
1177
|
}
|
|
1167
|
-
catch (error) {
|
|
1168
|
-
add("template", false, error instanceof Error ? error.message : String(error));
|
|
1169
|
-
nextActions.push(...templateActions());
|
|
1170
|
-
}
|
|
1171
1178
|
}
|
|
1172
1179
|
}
|
|
1173
1180
|
try {
|
|
@@ -1183,7 +1190,7 @@ const commandDoctor = async (flags) => {
|
|
|
1183
1190
|
checks
|
|
1184
1191
|
};
|
|
1185
1192
|
if (hasFlag(flags, "json")) {
|
|
1186
|
-
printSuccess("doctor", output, { api_url: apiUrl, ...(
|
|
1193
|
+
printSuccess("doctor", output, { api_url: apiUrl, ...(resolvedOrg ? { org: resolvedOrg } : {}) }, nextActions);
|
|
1187
1194
|
}
|
|
1188
1195
|
else {
|
|
1189
1196
|
for (const check of checks) {
|
|
@@ -1220,11 +1227,9 @@ const commandInit = async (command, flags) => {
|
|
|
1220
1227
|
const force = hasFlag(flags, "force");
|
|
1221
1228
|
const localConfig = readLocalConfig();
|
|
1222
1229
|
const apiUrl = String(flags["api-url"] || process.env.SANBOX_API_URL || localConfig.api_url || defaultApiUrl).replace(/\/+$/, "");
|
|
1223
|
-
const org = String(flags.org || process.env.SANBOX_ORG || localConfig.org || "").trim();
|
|
1224
1230
|
const template = readTemplateSelection(flags, { required: false });
|
|
1225
1231
|
const config = {
|
|
1226
1232
|
api_url: apiUrl,
|
|
1227
|
-
org,
|
|
1228
1233
|
...(template ? { default_template: template.id } : {})
|
|
1229
1234
|
};
|
|
1230
1235
|
const sanboxIgnore = [
|
|
@@ -1245,7 +1250,7 @@ const commandInit = async (command, flags) => {
|
|
|
1245
1250
|
printSuccess("init", {
|
|
1246
1251
|
files: results.map(([file, status]) => ({ file, status })),
|
|
1247
1252
|
template_selection: template
|
|
1248
|
-
}, { api_url: apiUrl
|
|
1253
|
+
}, { api_url: apiUrl }, template ? [] : templateActions());
|
|
1249
1254
|
}
|
|
1250
1255
|
else {
|
|
1251
1256
|
for (const [file, status] of results)
|
|
@@ -1268,8 +1273,6 @@ const commandId = (command) => {
|
|
|
1268
1273
|
return "version";
|
|
1269
1274
|
if (command[0] === "auth")
|
|
1270
1275
|
return "auth.check";
|
|
1271
|
-
if (command[0] === "orgs")
|
|
1272
|
-
return `orgs.${command[1] || "unknown"}`;
|
|
1273
1276
|
if (command[0] === "context")
|
|
1274
1277
|
return "context.get";
|
|
1275
1278
|
if (command[0] === "model-providers")
|
|
@@ -1306,8 +1309,6 @@ const main = async (command, flags) => {
|
|
|
1306
1309
|
}
|
|
1307
1310
|
if (command[0] === "auth" && command[1] === "check")
|
|
1308
1311
|
return commandAuthCheck(flags);
|
|
1309
|
-
if (command[0] === "orgs")
|
|
1310
|
-
return commandOrganizations(command, flags);
|
|
1311
1312
|
if (command[0] === "context")
|
|
1312
1313
|
return commandContext(flags);
|
|
1313
1314
|
if (command[0] === "doctor")
|
package/dist/config.js
CHANGED
|
@@ -11,7 +11,6 @@ export const readLocalConfig = (cwd = process.cwd()) => {
|
|
|
11
11
|
const record = parsed;
|
|
12
12
|
return {
|
|
13
13
|
api_url: typeof record.api_url === "string" ? record.api_url : undefined,
|
|
14
|
-
org: typeof record.org === "string" ? record.org : undefined,
|
|
15
14
|
default_template: typeof record.default_template === "string" ? record.default_template : undefined
|
|
16
15
|
};
|
|
17
16
|
}
|
|
@@ -21,22 +20,16 @@ export const readLocalConfig = (cwd = process.cwd()) => {
|
|
|
21
20
|
throw error;
|
|
22
21
|
}
|
|
23
22
|
};
|
|
24
|
-
export const readConfig = (flags = {}
|
|
23
|
+
export const readConfig = (flags = {}) => {
|
|
25
24
|
const localConfig = readLocalConfig();
|
|
26
25
|
const apiUrl = String(flags["api-url"] || process.env.SANBOX_API_URL || localConfig.api_url || defaultApiUrl).replace(/\/+$/, "");
|
|
27
|
-
const org = String(flags.org || process.env.SANBOX_ORG || localConfig.org || "").trim();
|
|
28
26
|
const apiKey = process.env.SANBOX_API_KEY || "";
|
|
29
|
-
if (options.requireOrg !== false && !org) {
|
|
30
|
-
throw new CliError("org_required", "Organization context is required.", {
|
|
31
|
-
nextActions: [commandAction(["sanbox", "context", "--json"], "Select an organization and retry with SANBOX_ORG set.", { SANBOX_ORG: "<org-slug>" })]
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
27
|
if (!apiKey) {
|
|
35
28
|
throw new CliError("api_key_required", "SANBOX_API_KEY is required.", {
|
|
36
29
|
nextActions: [commandAction(["sanbox", "auth", "check", "--json"], "Set a Sanbox control-plane API key and check access.", { SANBOX_API_KEY: "<sanbox-api-key>" })]
|
|
37
30
|
});
|
|
38
31
|
}
|
|
39
|
-
return { apiUrl,
|
|
32
|
+
return { apiUrl, apiKey };
|
|
40
33
|
};
|
|
41
34
|
export function readTemplateSelection(flags = {}, options = {}) {
|
|
42
35
|
const flagValue = flags.template;
|
|
@@ -52,7 +45,7 @@ export function readTemplateSelection(flags = {}, options = {}) {
|
|
|
52
45
|
return null;
|
|
53
46
|
throw new CliError("template_required", "A template must be selected explicitly.", {
|
|
54
47
|
nextActions: [
|
|
55
|
-
commandAction(["sanbox", "templates", "list", "--json"], "List templates available to the
|
|
48
|
+
commandAction(["sanbox", "templates", "list", "--json"], "List templates available to the API key's organization."),
|
|
56
49
|
commandAction(["sanbox", "run", "<task>", "--template", "<template-id>"], "Run with an explicit template."),
|
|
57
50
|
commandAction(["sanbox", "context", "--json"], "Select a template for this shell and inspect the resolved context.", { SANBOX_TEMPLATE: "<template-id>" })
|
|
58
51
|
]
|
package/dist/output.js
CHANGED
|
@@ -77,7 +77,9 @@ export const summarizeRun = (payload) => {
|
|
|
77
77
|
const selection = [
|
|
78
78
|
templateId ? `template=${templateId}` : "",
|
|
79
79
|
run.provider_id ? `provider=${run.provider_id}` : "",
|
|
80
|
-
run.model_id ? `model=${run.model_id}` : ""
|
|
80
|
+
run.model_id ? `model=${run.model_id}` : "",
|
|
81
|
+
run.sandbox_state ? `sandbox=${run.sandbox_state}` : "",
|
|
82
|
+
run.snapshot_generation ? `snapshot=${run.snapshot_generation}` : ""
|
|
81
83
|
].filter(Boolean).join(" ");
|
|
82
84
|
return `${run.id} ${run.status}${selection ? ` ${selection}` : ""}${run.exit_code === null ? "" : ` exit=${run.exit_code}`}${run.error ? ` error=${run.error}` : ""}`;
|
|
83
85
|
};
|
package/dist/runs.js
CHANGED
|
@@ -2,7 +2,7 @@ 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 terminalStatuses = new Set(["completed", "failed", "canceled"
|
|
5
|
+
const terminalStatuses = new Set(["completed", "failed", "canceled"]);
|
|
6
6
|
export const isTerminalRun = (run) => terminalStatuses.has(run.status);
|
|
7
7
|
export const waitForRun = async (client, runId, options = {}) => {
|
|
8
8
|
const pollIntervalMs = options.pollIntervalMs ?? 2000;
|
|
@@ -31,8 +31,7 @@ export const createRun = async (client, options) => {
|
|
|
31
31
|
external_run_id: options.externalRunId,
|
|
32
32
|
workload_id: options.templateId,
|
|
33
33
|
instruction: options.instruction,
|
|
34
|
-
...(inputCollectionId ? { input_collection_id: inputCollectionId } : {})
|
|
35
|
-
retention_ttl_seconds: options.retentionTtlSeconds
|
|
34
|
+
...(inputCollectionId ? { input_collection_id: inputCollectionId } : {})
|
|
36
35
|
});
|
|
37
36
|
};
|
|
38
37
|
export const readTasks = async (tasksPath) => {
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = "0.0.
|
|
1
|
+
export const version = "0.0.5";
|
package/dist/watch.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SanboxApiError } from "./api.js";
|
|
2
|
-
const terminalStatuses = new Set(["completed", "failed", "canceled"
|
|
3
|
-
const terminalEventKinds = new Set(["run.completed", "run.failed", "run.canceled"
|
|
2
|
+
const terminalStatuses = new Set(["completed", "failed", "canceled"]);
|
|
3
|
+
const terminalEventKinds = new Set(["run.completed", "run.failed", "run.canceled"]);
|
|
4
4
|
const retryableNetworkCodes = new Set(["ECONNRESET", "ECONNREFUSED", "EPIPE", "ETIMEDOUT", "EAI_AGAIN", "ENETUNREACH"]);
|
|
5
5
|
export class WatchInterruptedError extends Error {
|
|
6
6
|
constructor() {
|