@sanlabs/sanbox-cli 0.0.4 → 0.0.9
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 +129 -8
- package/dist/api.js +112 -35
- package/dist/args.js +9 -2
- package/dist/cli.js +620 -138
- package/dist/config.js +24 -14
- package/dist/deviceLogin.js +106 -0
- package/dist/fileAccess.js +16 -0
- package/dist/output.js +3 -1
- package/dist/runs.js +21 -3
- package/dist/ssh.js +129 -0
- package/dist/userSession.js +124 -0
- package/dist/version.js +1 -1
- package/dist/watch.js +2 -2
- package/package.json +3 -1
package/dist/cli.js
CHANGED
|
@@ -5,34 +5,42 @@ import { formatActivityJsonl, formatActivityLine, parseActivityView, shouldRende
|
|
|
5
5
|
import { ArtifactExistsError, downloadArtifacts } from "./artifacts.js";
|
|
6
6
|
import { booleanFlags, flagList, flagString, hasFlag, parseArgs } from "./args.js";
|
|
7
7
|
import { SanboxApiError, SanboxClient } from "./api.js";
|
|
8
|
-
import { defaultApiUrl, readConfig, readLocalConfig, readTemplateSelection } from "./config.js";
|
|
8
|
+
import { defaultApiUrl, readConfig, readLocalConfig, readTemplateSelection, readUserConfig, resolveApiUrl } from "./config.js";
|
|
9
|
+
import { loginWithBrowser, openBrowser, revokeUserSession } from "./deviceLogin.js";
|
|
9
10
|
import { CliError, commandAction, consoleAction } from "./errors.js";
|
|
11
|
+
import { parseFileAccessExpiry } from "./fileAccess.js";
|
|
10
12
|
import { previewInputs } from "./inputs.js";
|
|
11
13
|
import { printError, printJsonlError, printRun, printSuccess, publicRun, publicRunPayload } from "./output.js";
|
|
12
|
-
import { createRun, isTerminalRun, readTasks, runPool, stableBatchId, waitForRun } from "./runs.js";
|
|
14
|
+
import { createRun, isTerminalRun, readTasks, runPool, stableBatchId, waitForRun, waitForSandboxState } from "./runs.js";
|
|
13
15
|
import { version } from "./version.js";
|
|
14
16
|
import { WatchInterruptedError, watchEventsUntil, watchRun } from "./watch.js";
|
|
17
|
+
import { openSSH, runSSHProxy } from "./ssh.js";
|
|
18
|
+
import { userSessionStore } from "./userSession.js";
|
|
15
19
|
const help = `Sanbox CLI
|
|
16
20
|
|
|
17
21
|
Environment:
|
|
18
22
|
SANBOX_API_URL Sanbox API base URL
|
|
19
|
-
|
|
20
|
-
SANBOX_API_KEY Org API key
|
|
23
|
+
SANBOX_API_KEY Org API key for automation (no interactive access)
|
|
21
24
|
SANBOX_TEMPLATE Explicit template id or slug for run and batch
|
|
22
25
|
|
|
23
26
|
Commands:
|
|
24
27
|
sanbox --version
|
|
28
|
+
sanbox login [--no-browser]
|
|
29
|
+
sanbox logout
|
|
25
30
|
sanbox auth check [--json]
|
|
26
|
-
sanbox orgs list [--json]
|
|
27
31
|
sanbox context [--json]
|
|
28
32
|
sanbox doctor [--json]
|
|
29
33
|
sanbox model-providers list [--json]
|
|
30
34
|
sanbox model-providers get <provider-id> [--json]
|
|
31
35
|
sanbox model-providers models <provider-id> [--json]
|
|
36
|
+
sanbox anthropic-environments list [--json]
|
|
37
|
+
sanbox anthropic-environments get <environment-id> [--json]
|
|
38
|
+
sanbox anthropic-environments connect <environment-id> [--json]
|
|
39
|
+
sanbox anthropic-environments disconnect <environment-id> --force [--json]
|
|
32
40
|
sanbox templates list [--json]
|
|
33
41
|
sanbox templates get <template-id> [--json]
|
|
34
42
|
sanbox templates validate <template-id> [--json]
|
|
35
|
-
sanbox templates create --name "..." --model-provider <provider-id> --model <model-id> [--
|
|
43
|
+
sanbox templates create --name "..." --model-provider <provider-id> --model <model-id> [--harness opencode|hermes|browser-use] [--browser-domain <hostname>] [--llm-budget-usd <amount>] [--json]
|
|
36
44
|
sanbox run "task" --template <template-id> [--input <path>] [--wait | --watch] [--json | --jsonl]
|
|
37
45
|
sanbox run --task "..." --template <template-id> [--input <path>] [--wait | --watch] [--json | --jsonl]
|
|
38
46
|
sanbox batch --tasks tasks.json --template <template-id> [--input <path>] [--max-parallel 5] [--wait] [--json]
|
|
@@ -41,13 +49,30 @@ Commands:
|
|
|
41
49
|
sanbox runs events <run-id> [--after-event-id 0] [--json]
|
|
42
50
|
sanbox runs messages <run-id> [--json]
|
|
43
51
|
sanbox runs artifacts <run-id> [--json]
|
|
52
|
+
sanbox runs share <run-id> [--expires 1h] [--name "Debug access"] [--json]
|
|
53
|
+
sanbox runs shares <run-id> [--json]
|
|
54
|
+
sanbox runs unshare <run-id> <access-point-id> [--json]
|
|
44
55
|
sanbox runs download <run-id> --output <directory> [--artifact <path>] [--overwrite] [--json]
|
|
45
56
|
sanbox runs watch <run-id> [--after-event-id 0] [--view activity|logs|compact] [--jsonl]
|
|
46
57
|
sanbox runs cancel <run-id> [--json]
|
|
58
|
+
sanbox runs resume <run-id> [--wait] [--json]
|
|
59
|
+
sanbox runs pause <run-id> [--wait] [--json]
|
|
47
60
|
sanbox runs message <run-id> "..." [--wait | --watch] [--json | --jsonl]
|
|
61
|
+
sanbox ssh <run-id>
|
|
48
62
|
sanbox init [--force]
|
|
49
63
|
sanbox init agent [--write]
|
|
50
64
|
`;
|
|
65
|
+
const sshHelp = `Sanbox SSH
|
|
66
|
+
|
|
67
|
+
Usage:
|
|
68
|
+
sanbox ssh <run-id>
|
|
69
|
+
|
|
70
|
+
Opens the running Hermes Computer with your local OpenSSH client. Sanbox creates an ephemeral
|
|
71
|
+
Ed25519 key, authorizes it for one connection, verifies the microVM host key, and tunnels SSH over
|
|
72
|
+
the authenticated private management path. Run sanbox login first. The computer owner and
|
|
73
|
+
organization admins can connect; organization API keys cannot open interactive sessions. The guest
|
|
74
|
+
does not expose port 22 publicly.
|
|
75
|
+
`;
|
|
51
76
|
const runHelp = `Sanbox run
|
|
52
77
|
|
|
53
78
|
Usage:
|
|
@@ -58,7 +83,6 @@ Options:
|
|
|
58
83
|
--input <path> File, directory, or glob to upload. Repeatable.
|
|
59
84
|
--template <id> Template id or slug. Required unless SANBOX_TEMPLATE or project config sets it.
|
|
60
85
|
--external-run-id <id> Idempotency key for retries.
|
|
61
|
-
--retention-ttl-seconds <n> Workspace retention TTL. Default: 86400.
|
|
62
86
|
--dry-run Preview included files without creating a run.
|
|
63
87
|
--wait Poll until terminal status.
|
|
64
88
|
--watch Stream activity until terminal status.
|
|
@@ -73,7 +97,7 @@ const doctorHelp = `Sanbox doctor
|
|
|
73
97
|
Usage:
|
|
74
98
|
sanbox doctor [--json]
|
|
75
99
|
|
|
76
|
-
Checks local configuration, API health, auth,
|
|
100
|
+
Checks local configuration, API health, auth, organization discovery, and template access.
|
|
77
101
|
SANBOX_API_KEY is still read only from the environment.
|
|
78
102
|
`;
|
|
79
103
|
const modelProvidersHelp = `Sanbox model providers
|
|
@@ -86,32 +110,59 @@ Usage:
|
|
|
86
110
|
Provider credentials are configured by organization admins in the Sanbox console.
|
|
87
111
|
Model ids are scoped to their provider and are never deduplicated across providers.
|
|
88
112
|
`;
|
|
113
|
+
const anthropicEnvironmentsHelp = `Sanbox Anthropic environments
|
|
114
|
+
|
|
115
|
+
Usage:
|
|
116
|
+
sanbox anthropic-environments list [--json]
|
|
117
|
+
sanbox anthropic-environments get <environment-id> [--json]
|
|
118
|
+
ANTHROPIC_ENVIRONMENT_KEY=<key> sanbox anthropic-environments connect <environment-id> [--json]
|
|
119
|
+
sanbox anthropic-environments disconnect <environment-id> --force [--json]
|
|
120
|
+
|
|
121
|
+
Create the self-hosted environment and generate its environment key in Claude Console first.
|
|
122
|
+
The connect command reads the key only from ANTHROPIC_ENVIRONMENT_KEY so it does not enter argv.
|
|
123
|
+
Connect, key replacement, and disconnect require an organization-admin user session or operator
|
|
124
|
+
credential. Organization-scoped member API keys can list and inspect environments but cannot
|
|
125
|
+
manage their credentials.
|
|
126
|
+
`;
|
|
89
127
|
const templatesHelp = `Sanbox templates
|
|
90
128
|
|
|
91
129
|
Usage:
|
|
92
130
|
sanbox templates list [--json]
|
|
93
131
|
sanbox templates get <template-id> [--json]
|
|
94
132
|
sanbox templates validate <template-id> [--json]
|
|
95
|
-
sanbox templates create --name "Code review" --model-provider <provider-id> --model <model-id> [--
|
|
133
|
+
sanbox templates create --name "Code review" --model-provider <provider-id> --model <model-id> [--harness opencode|hermes|browser-use] [--browser-domain <hostname>] [--llm-budget-usd <amount>] [--json]
|
|
96
134
|
|
|
97
135
|
Template creation requires an exact provider id and that provider's exact model id.
|
|
98
136
|
LiteLLM budgets are optional USD amounts and apply separately to each run.
|
|
137
|
+
Hermes templates are always-on Telegram gateways. Set SANBOX_TELEGRAM_BOT_TOKEN
|
|
138
|
+
and repeat --telegram-allowed-user <numeric-id> for every permitted user.
|
|
139
|
+
Browser Use templates run local headless Chromium inside a one-shot sandbox. They require OpenAI
|
|
140
|
+
or Anthropic and at least one repeatable --browser-domain hostname or leading wildcard.
|
|
141
|
+
Optional controls: --browser-max-steps, --browser-step-timeout-seconds,
|
|
142
|
+
--browser-vision-mode auto|always|never, --browser-viewport WIDTHxHEIGHT,
|
|
143
|
+
--browser-download-policy allow|deny, and --browser-additional-instructions.
|
|
99
144
|
`;
|
|
100
145
|
const agentInstructions = `# Operate Sanbox Autonomously
|
|
101
146
|
|
|
102
147
|
Use the \`sanbox\` CLI for focused, isolated, long-running, risky, or parallel work. The canonical
|
|
103
|
-
protocol is https://console.sanbox.cloud/agent.md.
|
|
148
|
+
protocol is https://console.sanbox.cloud/agent.md.
|
|
149
|
+
|
|
150
|
+
Always use the latest published CLI. Install and verify it at the start of every operating session:
|
|
151
|
+
\`\`\`bash
|
|
152
|
+
npm install -g @sanlabs/sanbox-cli@latest
|
|
153
|
+
latest_cli_version="$(npm view @sanlabs/sanbox-cli version)"
|
|
154
|
+
installed_cli_version="$(sanbox --version)"
|
|
155
|
+
test "$installed_cli_version" = "$latest_cli_version"
|
|
156
|
+
\`\`\`
|
|
104
157
|
|
|
105
158
|
Human bootstrap:
|
|
106
|
-
- A human/admin supplies SANBOX_API_KEY and configures provider credentials plus a runnable template.
|
|
159
|
+
- A human/admin supplies an organization-scoped SANBOX_API_KEY and configures provider credentials plus a runnable template.
|
|
107
160
|
- Never print, persist, prompt with, or upload API keys or provider credentials.
|
|
108
161
|
- If the key or a runnable template is unavailable, stop with the exact human action required.
|
|
109
162
|
|
|
110
163
|
Deterministic startup:
|
|
111
164
|
\`\`\`bash
|
|
112
165
|
sanbox --version
|
|
113
|
-
sanbox orgs list --json
|
|
114
|
-
export SANBOX_ORG=<returned-org-slug>
|
|
115
166
|
sanbox auth check --json
|
|
116
167
|
sanbox context --json
|
|
117
168
|
sanbox templates list --json
|
|
@@ -120,8 +171,12 @@ sanbox templates validate "$SANBOX_TEMPLATE" --json
|
|
|
120
171
|
sanbox doctor --json
|
|
121
172
|
\`\`\`
|
|
122
173
|
|
|
123
|
-
Never guess opaque IDs.
|
|
124
|
-
|
|
174
|
+
Never guess opaque IDs. The CLI derives the organization from the API key and requires the key to
|
|
175
|
+
resolve to exactly one organization. For task execution, select only a template with runnable: true,
|
|
176
|
+
template_type: "runner", and runner_config.harness: "opencode" or "browser-use". Select Browser Use
|
|
177
|
+
only for a one-shot web task whose target domains are already approved on the template. Never select
|
|
178
|
+
a Hermes service template for a waited task because it is always-on. Select automatically only when exactly one task
|
|
179
|
+
template qualifies; otherwise ask the user. Provider credentials and template administration are
|
|
125
180
|
console-only.
|
|
126
181
|
|
|
127
182
|
Use --json for request/response commands and --jsonl for streams. Parse the versioned envelope:
|
|
@@ -139,7 +194,7 @@ sanbox run "Investigate one focused task and write output/report.md" \\
|
|
|
139
194
|
|
|
140
195
|
Reuse the same --external-run-id after ambiguous failures. Retry network errors, HTTP 429, HTTP 5xx,
|
|
141
196
|
and workspace_busy with bounded backoff. Do not retry other 4xx errors unless next_actions directs
|
|
142
|
-
recovery.
|
|
197
|
+
recovery. Terminal statuses are completed, failed, and canceled.
|
|
143
198
|
|
|
144
199
|
Recover and retrieve results:
|
|
145
200
|
\`\`\`bash
|
|
@@ -154,33 +209,53 @@ sanbox runs download <run-id> --output .sanbox/output/<run-id> --json
|
|
|
154
209
|
Tasks must put durable files under /workspace/output. Downloads preserve relative paths and report
|
|
155
210
|
byte counts plus SHA-256 digests; existing files require explicit --overwrite.
|
|
156
211
|
|
|
157
|
-
|
|
212
|
+
While a sandbox is running, create one read-only capability URL for its entire root filesystem:
|
|
158
213
|
\`\`\`bash
|
|
159
|
-
sanbox runs
|
|
160
|
-
sanbox runs
|
|
214
|
+
sanbox runs share <run-id> --expires 1h --json
|
|
215
|
+
sanbox runs shares <run-id> --json
|
|
216
|
+
sanbox runs unshare <run-id> <access-point-id> --json
|
|
161
217
|
\`\`\`
|
|
218
|
+
The URL starts at /, supports directory JSON with ?format=json, and stops working when the sandbox
|
|
219
|
+
stops, the link expires, or it is revoked. Treat the URL as a bearer secret.
|
|
220
|
+
|
|
221
|
+
Manage the retained sandbox independently of its agent harness:
|
|
222
|
+
\`\`\`bash
|
|
223
|
+
sanbox runs get <run-id> --json
|
|
224
|
+
sanbox runs resume <run-id> --wait --json
|
|
225
|
+
sanbox runs share <run-id> --expires 1h --json
|
|
226
|
+
sanbox runs pause <run-id> --wait --json
|
|
227
|
+
\`\`\`
|
|
228
|
+
|
|
229
|
+
Resume restores the latest Firecracker snapshot without starting an agent command. It creates an
|
|
230
|
+
exclusive manual lease and leaves the sandbox running until pause snapshots it again. Do not submit
|
|
231
|
+
agent work while the manual lease is active. Before resume, require sandbox_state: "paused" and a
|
|
232
|
+
positive snapshot_generation.
|
|
233
|
+
|
|
234
|
+
For an OpenCode follow-up, use \`sanbox runs message <run-id> "..." --wait --json\`. Follow-up waits
|
|
235
|
+
are correlated to the returned chat_job.id. Never submit concurrent follow-ups to one run. Retry
|
|
236
|
+
sandbox_not_paused only after the run returns to paused; treat sandbox_not_resumable,
|
|
237
|
+
sandbox_snapshot_missing, sandbox_worker_missing, hermes_uses_channel, and
|
|
238
|
+
browser_use_followup_unsupported as blockers.
|
|
162
239
|
|
|
163
|
-
Follow-up waits are correlated to the returned chat_job.id. Keep dependent follow-ups sequential.
|
|
164
240
|
For independent fan-out, use \`sanbox batch\` with a stable external_run_id per task and keep the
|
|
165
241
|
client alive until submission completes.
|
|
166
242
|
|
|
167
243
|
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
|
|
244
|
+
and required follow-ups succeeded. Report run/external/template IDs, status, sandbox state, snapshot
|
|
245
|
+
generation, artifact paths/digests, and blockers. The CLI excludes common secrets by default; add
|
|
246
|
+
.sanboxignore for project rules.
|
|
170
247
|
`;
|
|
171
248
|
const cwd = () => process.cwd();
|
|
172
249
|
const makeClient = (flags) => new SanboxClient(readConfig(flags));
|
|
173
|
-
const makeAuthClient = (flags) => new SanboxClient(readConfig(flags, { requireOrg: false }));
|
|
174
250
|
const jsonContext = (client) => ({
|
|
175
251
|
api_url: client.config.apiUrl,
|
|
176
|
-
org: client.
|
|
252
|
+
...(client.resolvedOrganizationSlug() ? { org: client.resolvedOrganizationSlug() } : {})
|
|
177
253
|
});
|
|
178
254
|
const localJsonContext = (flags) => {
|
|
179
255
|
try {
|
|
180
256
|
const local = readLocalConfig();
|
|
181
257
|
const apiUrl = String(flags["api-url"] || process.env.SANBOX_API_URL || local.api_url || defaultApiUrl).replace(/\/+$/, "");
|
|
182
|
-
|
|
183
|
-
return { api_url: apiUrl, ...(org ? { org } : {}) };
|
|
258
|
+
return { api_url: apiUrl };
|
|
184
259
|
}
|
|
185
260
|
catch {
|
|
186
261
|
return {};
|
|
@@ -188,10 +263,12 @@ const localJsonContext = (flags) => {
|
|
|
188
263
|
};
|
|
189
264
|
const consolePathUrl = (client, pathname) => new URL(pathname, `${client.config.apiUrl}/`).toString();
|
|
190
265
|
const consoleUrl = (client) => consolePathUrl(client, "/model-providers");
|
|
266
|
+
const anthropicEnvironmentsConsoleUrl = (client) => consolePathUrl(client, "/anthropic-environments");
|
|
191
267
|
const providerConsoleAction = (client) => consoleAction(consoleUrl(client), "Ask an organization admin to configure or repair the model provider in the Sanbox console.");
|
|
192
268
|
const templateAdminConsoleAction = (client) => consoleAction(consolePathUrl(client, "/templates"), "Ask an organization admin to create a template in the Sanbox console.");
|
|
269
|
+
const anthropicEnvironmentAdminConsoleAction = (client) => consoleAction(anthropicEnvironmentsConsoleUrl(client), "Ask an organization admin to manage the Anthropic environment in the Sanbox console.");
|
|
193
270
|
const templateActions = () => [
|
|
194
|
-
commandAction(["sanbox", "templates", "list", "--json"], "List templates available to the
|
|
271
|
+
commandAction(["sanbox", "templates", "list", "--json"], "List templates available to the API key's organization."),
|
|
195
272
|
commandAction(["sanbox", "templates", "validate", "<template-id>", "--json"], "Check whether a template is runnable.")
|
|
196
273
|
];
|
|
197
274
|
const runReadinessCodes = new Set([
|
|
@@ -256,6 +333,62 @@ const requiredPositional = (value, code, message) => {
|
|
|
256
333
|
throw new CliError(code, message);
|
|
257
334
|
return normalized;
|
|
258
335
|
};
|
|
336
|
+
const browserDomainPattern = /^(?:\*\.)?[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/;
|
|
337
|
+
const parseBrowserDomains = (values) => {
|
|
338
|
+
const domains = [...new Set(values.map((value) => value.trim().toLowerCase().replace(/\.$/, "")))];
|
|
339
|
+
if (domains.length < 1) {
|
|
340
|
+
throw new CliError("browser_domain_required", "Browser Use template creation requires at least one --browser-domain.");
|
|
341
|
+
}
|
|
342
|
+
if (domains.length > 64 ||
|
|
343
|
+
domains.some((domain) => !browserDomainPattern.test(domain) ||
|
|
344
|
+
/^\d+(?:\.\d+){3}$/.test(domain.startsWith("*.") ? domain.slice(2) : domain))) {
|
|
345
|
+
throw new CliError("invalid_browser_domain", "--browser-domain must be a public hostname or leading wildcard such as example.com or *.example.com.");
|
|
346
|
+
}
|
|
347
|
+
return domains;
|
|
348
|
+
};
|
|
349
|
+
const optionalBrowserInteger = (flags, key, fallback, minimum, maximum) => {
|
|
350
|
+
if (flags[key] === undefined)
|
|
351
|
+
return fallback;
|
|
352
|
+
const value = Number(flagString(flags, key));
|
|
353
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
354
|
+
throw new CliError("invalid_browser_use_config", `--${key} must be an integer between ${minimum} and ${maximum}.`);
|
|
355
|
+
}
|
|
356
|
+
return value;
|
|
357
|
+
};
|
|
358
|
+
const parseBrowserUseConfig = (flags) => {
|
|
359
|
+
const maxSteps = optionalBrowserInteger(flags, "browser-max-steps", 30, 1, 100);
|
|
360
|
+
const stepTimeout = optionalBrowserInteger(flags, "browser-step-timeout-seconds", 120, 30, 300);
|
|
361
|
+
const visionMode = flagString(flags, "browser-vision-mode", "auto");
|
|
362
|
+
if (visionMode !== "auto" && visionMode !== "always" && visionMode !== "never") {
|
|
363
|
+
throw new CliError("invalid_browser_use_config", "--browser-vision-mode must be auto, always, or never.");
|
|
364
|
+
}
|
|
365
|
+
const normalizedVisionMode = visionMode;
|
|
366
|
+
const downloadPolicy = flagString(flags, "browser-download-policy", "allow");
|
|
367
|
+
if (downloadPolicy !== "allow" && downloadPolicy !== "deny") {
|
|
368
|
+
throw new CliError("invalid_browser_use_config", "--browser-download-policy must be allow or deny.");
|
|
369
|
+
}
|
|
370
|
+
const normalizedDownloadPolicy = downloadPolicy;
|
|
371
|
+
const viewportRaw = flagString(flags, "browser-viewport", "1280x720").toLowerCase();
|
|
372
|
+
const viewportMatch = /^(\d{3,4})x(\d{3,4})$/.exec(viewportRaw);
|
|
373
|
+
const width = Number(viewportMatch?.[1]);
|
|
374
|
+
const height = Number(viewportMatch?.[2]);
|
|
375
|
+
if (!viewportMatch || width < 320 || width > 1920 || height < 480 || height > 1080) {
|
|
376
|
+
throw new CliError("invalid_browser_use_config", "--browser-viewport must be WIDTHxHEIGHT from 320x480 through 1920x1080.");
|
|
377
|
+
}
|
|
378
|
+
const additionalInstructions = flagString(flags, "browser-additional-instructions");
|
|
379
|
+
if (additionalInstructions.length > 4_000 ||
|
|
380
|
+
/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(additionalInstructions)) {
|
|
381
|
+
throw new CliError("invalid_browser_use_config", "--browser-additional-instructions must contain at most 4000 printable characters.");
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
max_steps: maxSteps,
|
|
385
|
+
step_timeout_seconds: stepTimeout,
|
|
386
|
+
vision_mode: normalizedVisionMode,
|
|
387
|
+
viewport: { width, height },
|
|
388
|
+
download_policy: normalizedDownloadPolicy,
|
|
389
|
+
additional_instructions: additionalInstructions.trim()
|
|
390
|
+
};
|
|
391
|
+
};
|
|
259
392
|
const providerId = (provider) => String(provider.provider_id || provider.id || "");
|
|
260
393
|
const templateName = (template) => String(template.display_name || template.template_slug || template.id || "");
|
|
261
394
|
const validationRunnable = (validation) => {
|
|
@@ -273,43 +406,71 @@ const formatBytes = (bytes) => {
|
|
|
273
406
|
};
|
|
274
407
|
const positionalText = (command, startIndex) => command.slice(startIndex).join(" ").trim();
|
|
275
408
|
const runTask = (command, flags) => flagString(flags, "task") || positionalText(command, 1);
|
|
276
|
-
const commonFlags = ["api-url", "
|
|
409
|
+
const commonFlags = ["api-url", "json", "help"];
|
|
277
410
|
const flagSets = {
|
|
278
411
|
"auth.check": commonFlags,
|
|
279
|
-
"orgs.list": commonFlags,
|
|
280
412
|
context: [...commonFlags, "template"],
|
|
281
413
|
doctor: [...commonFlags, "template"],
|
|
282
414
|
"model-providers.list": commonFlags,
|
|
283
415
|
"model-providers.get": commonFlags,
|
|
284
416
|
"model-providers.models": commonFlags,
|
|
417
|
+
"anthropic-environments.list": commonFlags,
|
|
418
|
+
"anthropic-environments.get": commonFlags,
|
|
419
|
+
"anthropic-environments.connect": commonFlags,
|
|
420
|
+
"anthropic-environments.disconnect": [...commonFlags, "force"],
|
|
285
421
|
"templates.list": commonFlags,
|
|
286
422
|
"templates.get": commonFlags,
|
|
287
423
|
"templates.validate": commonFlags,
|
|
288
|
-
"templates.create": [
|
|
424
|
+
"templates.create": [
|
|
425
|
+
...commonFlags,
|
|
426
|
+
"name",
|
|
427
|
+
"model-provider",
|
|
428
|
+
"model",
|
|
429
|
+
"harness",
|
|
430
|
+
"llm-budget-usd",
|
|
431
|
+
"web-access",
|
|
432
|
+
"telegram-allowed-user",
|
|
433
|
+
"browser-domain",
|
|
434
|
+
"browser-max-steps",
|
|
435
|
+
"browser-step-timeout-seconds",
|
|
436
|
+
"browser-vision-mode",
|
|
437
|
+
"browser-viewport",
|
|
438
|
+
"browser-download-policy",
|
|
439
|
+
"browser-additional-instructions"
|
|
440
|
+
],
|
|
289
441
|
run: [
|
|
290
|
-
...commonFlags, "task", "input", "template", "external-run-id",
|
|
442
|
+
...commonFlags, "task", "input", "template", "external-run-id",
|
|
291
443
|
"dry-run", "wait", "watch", "jsonl", "view", "after-event-id", "cancel-on-interrupt",
|
|
292
444
|
"poll-interval-ms", "event-page-size", "timeout-seconds", "verbose"
|
|
293
445
|
],
|
|
294
446
|
batch: [
|
|
295
447
|
...commonFlags, "tasks", "template", "input", "max-parallel", "wait", "batch-id",
|
|
296
|
-
"
|
|
448
|
+
"poll-interval-ms", "timeout-seconds"
|
|
297
449
|
],
|
|
298
450
|
"runs.list": [...commonFlags, "limit"],
|
|
299
451
|
"runs.get": commonFlags,
|
|
300
452
|
"runs.events": [...commonFlags, "after-event-id"],
|
|
301
453
|
"runs.messages": commonFlags,
|
|
302
454
|
"runs.artifacts": commonFlags,
|
|
455
|
+
"runs.share": [...commonFlags, "expires", "name"],
|
|
456
|
+
"runs.shares": commonFlags,
|
|
457
|
+
"runs.unshare": commonFlags,
|
|
303
458
|
"runs.download": [...commonFlags, "output", "artifact", "overwrite"],
|
|
304
459
|
"runs.watch": [
|
|
305
460
|
...commonFlags, "after-event-id", "view", "jsonl", "cancel-on-interrupt",
|
|
306
461
|
"poll-interval-ms", "event-page-size", "timeout-seconds"
|
|
307
462
|
],
|
|
308
463
|
"runs.cancel": commonFlags,
|
|
464
|
+
"runs.resume": [...commonFlags, "wait", "poll-interval-ms", "timeout-seconds"],
|
|
465
|
+
"runs.pause": [...commonFlags, "wait", "poll-interval-ms", "timeout-seconds"],
|
|
309
466
|
"runs.message": [
|
|
310
467
|
...commonFlags, "message", "wait", "watch", "jsonl", "view",
|
|
311
468
|
"poll-interval-ms", "event-page-size", "timeout-seconds"
|
|
312
469
|
],
|
|
470
|
+
ssh: ["api-url", "help"],
|
|
471
|
+
"ssh-proxy": [],
|
|
472
|
+
login: ["api-url", "json", "no-browser", "help"],
|
|
473
|
+
logout: ["api-url", "json", "help"],
|
|
313
474
|
init: [...commonFlags, "force", "template"],
|
|
314
475
|
"init.agent": [...commonFlags, "write"],
|
|
315
476
|
version: ["json", "help"]
|
|
@@ -321,9 +482,10 @@ const commandKey = (command) => {
|
|
|
321
482
|
return "version";
|
|
322
483
|
if (command[0] === "auth")
|
|
323
484
|
return `auth.${command[1] || ""}`;
|
|
324
|
-
if (command[0] === "
|
|
325
|
-
|
|
326
|
-
|
|
485
|
+
if (command[0] === "model-providers" ||
|
|
486
|
+
command[0] === "anthropic-environments" ||
|
|
487
|
+
command[0] === "templates" ||
|
|
488
|
+
command[0] === "runs") {
|
|
327
489
|
return `${command[0]}.${command[1] || ""}`;
|
|
328
490
|
}
|
|
329
491
|
if (command[0] === "init" && command[1] === "agent")
|
|
@@ -331,10 +493,14 @@ const commandKey = (command) => {
|
|
|
331
493
|
return command[0];
|
|
332
494
|
};
|
|
333
495
|
const requiredValueFlags = new Set([
|
|
334
|
-
"api-url", "
|
|
335
|
-
"
|
|
496
|
+
"api-url", "template", "task", "input", "include", "external-run-id",
|
|
497
|
+
"tasks", "max-parallel", "batch-id", "poll-interval-ms",
|
|
336
498
|
"event-page-size", "timeout-seconds", "view", "after-event-id", "limit", "name",
|
|
337
|
-
"model-provider", "model", "llm-budget-usd", "
|
|
499
|
+
"model-provider", "model", "harness", "llm-budget-usd", "telegram-allowed-user",
|
|
500
|
+
"browser-domain", "browser-max-steps", "browser-step-timeout-seconds",
|
|
501
|
+
"browser-vision-mode", "browser-viewport", "browser-download-policy",
|
|
502
|
+
"browser-additional-instructions", "expires",
|
|
503
|
+
"message", "output", "artifact"
|
|
338
504
|
]);
|
|
339
505
|
const validateFlagValues = (flags) => {
|
|
340
506
|
for (const flag of requiredValueFlags) {
|
|
@@ -372,8 +538,8 @@ const validateFlags = (command, flags) => {
|
|
|
372
538
|
return;
|
|
373
539
|
}
|
|
374
540
|
const knownRoots = new Set([
|
|
375
|
-
"auth", "
|
|
376
|
-
"run", "batch", "runs", "init", "version"
|
|
541
|
+
"auth", "context", "doctor", "model-providers", "anthropic-environments", "templates",
|
|
542
|
+
"run", "batch", "runs", "ssh", "ssh-proxy", "login", "logout", "init", "version"
|
|
377
543
|
]);
|
|
378
544
|
const allowed = flagSets[key] ?? (knownRoots.has(command[0] || "") ? commonFlags : null);
|
|
379
545
|
if (!allowed)
|
|
@@ -388,12 +554,15 @@ const validatePositionals = (command, flags) => {
|
|
|
388
554
|
return;
|
|
389
555
|
const maximums = {
|
|
390
556
|
"auth.check": 2,
|
|
391
|
-
"orgs.list": 2,
|
|
392
557
|
context: 1,
|
|
393
558
|
doctor: 1,
|
|
394
559
|
"model-providers.list": 2,
|
|
395
560
|
"model-providers.get": 3,
|
|
396
561
|
"model-providers.models": 3,
|
|
562
|
+
"anthropic-environments.list": 2,
|
|
563
|
+
"anthropic-environments.get": 3,
|
|
564
|
+
"anthropic-environments.connect": 3,
|
|
565
|
+
"anthropic-environments.disconnect": 3,
|
|
397
566
|
"templates.list": 2,
|
|
398
567
|
"templates.get": 3,
|
|
399
568
|
"templates.validate": 3,
|
|
@@ -404,9 +573,18 @@ const validatePositionals = (command, flags) => {
|
|
|
404
573
|
"runs.events": 3,
|
|
405
574
|
"runs.messages": 3,
|
|
406
575
|
"runs.artifacts": 3,
|
|
576
|
+
"runs.share": 3,
|
|
577
|
+
"runs.shares": 3,
|
|
578
|
+
"runs.unshare": 4,
|
|
407
579
|
"runs.download": 3,
|
|
408
580
|
"runs.watch": 3,
|
|
409
581
|
"runs.cancel": 3,
|
|
582
|
+
"runs.resume": 3,
|
|
583
|
+
"runs.pause": 3,
|
|
584
|
+
ssh: 2,
|
|
585
|
+
"ssh-proxy": 1,
|
|
586
|
+
login: 1,
|
|
587
|
+
logout: 1,
|
|
410
588
|
init: 1,
|
|
411
589
|
"init.agent": 2,
|
|
412
590
|
version: 1
|
|
@@ -533,8 +711,7 @@ const commandRun = async (command, flags) => {
|
|
|
533
711
|
instruction: task,
|
|
534
712
|
inputs: flagList(flags, "input"),
|
|
535
713
|
externalRunId: flagString(flags, "external-run-id") || undefined,
|
|
536
|
-
templateId: template.id
|
|
537
|
-
retentionTtlSeconds: integerFlag(flags, "retention-ttl-seconds", 86400, 0, Number.MAX_SAFE_INTEGER)
|
|
714
|
+
templateId: template.id
|
|
538
715
|
});
|
|
539
716
|
}
|
|
540
717
|
catch (error) {
|
|
@@ -587,8 +764,7 @@ const commandBatch = async (flags) => {
|
|
|
587
764
|
instruction: task.task,
|
|
588
765
|
inputs: task.input || inputs,
|
|
589
766
|
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)
|
|
767
|
+
templateId: template.id
|
|
592
768
|
});
|
|
593
769
|
}
|
|
594
770
|
catch (error) {
|
|
@@ -617,50 +793,28 @@ const commandBatch = async (flags) => {
|
|
|
617
793
|
};
|
|
618
794
|
const commandAuthCheck = async (flags) => {
|
|
619
795
|
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 };
|
|
796
|
+
const [me, selected] = await Promise.all([client.me(), client.organization()]);
|
|
797
|
+
const org = selected.slug;
|
|
798
|
+
const output = { ok: true, org, selected, me };
|
|
627
799
|
if (hasFlag(flags, "json"))
|
|
628
800
|
printSuccess("auth.check", output, jsonContext(client));
|
|
629
801
|
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
|
-
}
|
|
802
|
+
process.stdout.write(`ok org=${org}\n`);
|
|
646
803
|
};
|
|
647
804
|
const commandContext = async (flags) => {
|
|
648
805
|
const client = makeClient(flags);
|
|
649
806
|
const selection = readTemplateSelection(flags, { required: false });
|
|
650
|
-
const [me, providerPayload, templatePayload] = await Promise.all([
|
|
807
|
+
const [organization, me, providerPayload, templatePayload] = await Promise.all([
|
|
808
|
+
client.organization(),
|
|
651
809
|
client.me(),
|
|
652
810
|
client.listModelProviders(),
|
|
653
811
|
client.listTemplates()
|
|
654
812
|
]);
|
|
655
|
-
const organizations = Array.isArray(me.organizations)
|
|
656
|
-
? me.organizations
|
|
657
|
-
: [];
|
|
658
|
-
const selectedOrganization = organizations.find((item) => item.slug === client.config.org) || null;
|
|
659
813
|
const selectedTemplate = selection
|
|
660
814
|
? templatePayload.templates.find((item) => item.id === selection.id || item.template_slug === selection.id) || null
|
|
661
815
|
: null;
|
|
662
816
|
const output = {
|
|
663
|
-
organization
|
|
817
|
+
organization,
|
|
664
818
|
authentication: me.auth || null,
|
|
665
819
|
template_selection: selection
|
|
666
820
|
? { id: selection.id, source: selection.source, found: Boolean(selectedTemplate), template: selectedTemplate }
|
|
@@ -669,22 +823,17 @@ const commandContext = async (flags) => {
|
|
|
669
823
|
templates: templatePayload.templates
|
|
670
824
|
};
|
|
671
825
|
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
826
|
if (!selection) {
|
|
676
827
|
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
828
|
}
|
|
678
829
|
else if (!selectedTemplate) {
|
|
679
830
|
nextActions.push(commandAction(["sanbox", "templates", "list", "--json"], "Choose a template that exists in this organization."));
|
|
680
831
|
}
|
|
681
|
-
if (!selectedOrganization)
|
|
682
|
-
process.exitCode = 2;
|
|
683
832
|
if (hasFlag(flags, "json")) {
|
|
684
833
|
printSuccess("context.get", output, jsonContext(client), nextActions);
|
|
685
834
|
return;
|
|
686
835
|
}
|
|
687
|
-
process.stdout.write(`org=${
|
|
836
|
+
process.stdout.write(`org=${organization.slug}\n`);
|
|
688
837
|
process.stdout.write(`template=${selection ? `${selection.id} source=${selection.source}${selectedTemplate ? "" : " missing"}` : "not selected"}\n`);
|
|
689
838
|
process.stdout.write(`model_providers=${providerPayload.providers.map((item) => providerId(item)).filter(Boolean).join(",") || "none"}\n`);
|
|
690
839
|
process.stdout.write(`templates=${templatePayload.templates.map((item) => item.template_slug || item.id).join(",") || "none"}\n`);
|
|
@@ -725,7 +874,8 @@ const commandModelProviders = async (command, flags) => {
|
|
|
725
874
|
printSuccess("model_providers.get", payload, jsonContext(client), nextActions);
|
|
726
875
|
return;
|
|
727
876
|
}
|
|
728
|
-
process.stdout.write(`${providerId(payload.provider)}${payload.provider.status ? ` ${payload.provider.status}` : ""}
|
|
877
|
+
process.stdout.write(`${providerId(payload.provider)}${payload.provider.status ? ` ${payload.provider.status}` : ""}` +
|
|
878
|
+
`${payload.provider.base_url ? ` base_url=${payload.provider.base_url}` : ""}\n`);
|
|
729
879
|
return;
|
|
730
880
|
}
|
|
731
881
|
if (action === "models") {
|
|
@@ -760,6 +910,125 @@ const commandModelProviders = async (command, flags) => {
|
|
|
760
910
|
}
|
|
761
911
|
throw new CliError("model_providers_action_required", "model-providers requires an action: list, get, or models.");
|
|
762
912
|
};
|
|
913
|
+
const anthropicEnvironmentIdPattern = /^env_[A-Za-z0-9_-]{1,196}$/;
|
|
914
|
+
const requiredAnthropicEnvironmentId = (value, action) => {
|
|
915
|
+
const environmentId = requiredPositional(value, "anthropic_environment_required", `anthropic-environments ${action} requires an environment id.`);
|
|
916
|
+
if (!anthropicEnvironmentIdPattern.test(environmentId)) {
|
|
917
|
+
throw new CliError("invalid_anthropic_environment", "The environment id must start with env_ and contain only letters, numbers, underscores, or hyphens.");
|
|
918
|
+
}
|
|
919
|
+
return environmentId;
|
|
920
|
+
};
|
|
921
|
+
const anthropicEnvironmentError = (error, client, environmentId) => {
|
|
922
|
+
if (!(error instanceof SanboxApiError))
|
|
923
|
+
return error;
|
|
924
|
+
if (error.status === 403) {
|
|
925
|
+
return new CliError(error.code, error.message, {
|
|
926
|
+
status: error.status,
|
|
927
|
+
details: { environment_id: environmentId },
|
|
928
|
+
nextActions: [anthropicEnvironmentAdminConsoleAction(client)]
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
if (error.status === 404) {
|
|
932
|
+
return new CliError("anthropic_environment_not_found", error.message, {
|
|
933
|
+
status: error.status,
|
|
934
|
+
details: { environment_id: environmentId },
|
|
935
|
+
nextActions: [commandAction(["sanbox", "anthropic-environments", "list", "--json"], "List Anthropic environments connected to this organization.")]
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
return error;
|
|
939
|
+
};
|
|
940
|
+
const commandAnthropicEnvironments = async (command, flags) => {
|
|
941
|
+
const client = makeClient(flags);
|
|
942
|
+
const action = command[1];
|
|
943
|
+
if (action === "list") {
|
|
944
|
+
const payload = await client.listAnthropicEnvironments();
|
|
945
|
+
const nextActions = payload.environments.length === 0
|
|
946
|
+
? [anthropicEnvironmentAdminConsoleAction(client)]
|
|
947
|
+
: [];
|
|
948
|
+
if (hasFlag(flags, "json")) {
|
|
949
|
+
printSuccess("anthropic_environments.list", payload, jsonContext(client), nextActions);
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
for (const environment of payload.environments) {
|
|
953
|
+
process.stdout.write(`${environment.environment_id}\t${environment.status}` +
|
|
954
|
+
`\tworker=${environment.assigned_worker_id || "unassigned"}` +
|
|
955
|
+
`\tverified=${environment.last_verified_at || "never"}\n`);
|
|
956
|
+
}
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
if (action === "get") {
|
|
960
|
+
const environmentId = requiredAnthropicEnvironmentId(command[2], action);
|
|
961
|
+
let payload;
|
|
962
|
+
try {
|
|
963
|
+
payload = await client.getAnthropicEnvironment(environmentId);
|
|
964
|
+
}
|
|
965
|
+
catch (error) {
|
|
966
|
+
throw anthropicEnvironmentError(error, client, environmentId);
|
|
967
|
+
}
|
|
968
|
+
if (hasFlag(flags, "json")) {
|
|
969
|
+
printSuccess("anthropic_environments.get", payload, jsonContext(client));
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
const environment = payload.environment;
|
|
973
|
+
process.stdout.write(`${environment.environment_id} ${environment.status}` +
|
|
974
|
+
` worker=${environment.assigned_worker_id || "unassigned"}` +
|
|
975
|
+
` verified=${environment.last_verified_at || "never"}\n`);
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
if (action === "connect") {
|
|
979
|
+
const environmentId = requiredAnthropicEnvironmentId(command[2], action);
|
|
980
|
+
const environmentKey = process.env.ANTHROPIC_ENVIRONMENT_KEY?.trim() || "";
|
|
981
|
+
if (!environmentKey) {
|
|
982
|
+
throw new CliError("anthropic_environment_key_required", "ANTHROPIC_ENVIRONMENT_KEY is required for anthropic-environments connect.", {
|
|
983
|
+
details: { environment_id: environmentId },
|
|
984
|
+
nextActions: [commandAction(["sanbox", "anthropic-environments", "connect", environmentId, "--json"], "Connect or replace the environment key without placing the secret in argv.", { ANTHROPIC_ENVIRONMENT_KEY: "<environment-key>" })]
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
if (environmentKey.length < 16 ||
|
|
988
|
+
environmentKey.length > 20_000 ||
|
|
989
|
+
/[\r\n\u0000]/.test(environmentKey)) {
|
|
990
|
+
throw new CliError("invalid_anthropic_environment_key", "ANTHROPIC_ENVIRONMENT_KEY must be a single-line Anthropic environment credential.");
|
|
991
|
+
}
|
|
992
|
+
let payload;
|
|
993
|
+
try {
|
|
994
|
+
payload = await client.connectAnthropicEnvironment(environmentId, environmentKey);
|
|
995
|
+
}
|
|
996
|
+
catch (error) {
|
|
997
|
+
throw anthropicEnvironmentError(error, client, environmentId);
|
|
998
|
+
}
|
|
999
|
+
const nextActions = [commandAction(["sanbox", "anthropic-environments", "get", environmentId, "--json"], "Check whether the first authenticated queue poll connected the environment.")];
|
|
1000
|
+
if (hasFlag(flags, "json")) {
|
|
1001
|
+
printSuccess("anthropic_environments.connect", payload, jsonContext(client), nextActions);
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
process.stdout.write(`${payload.environment.environment_id} ${payload.environment.status}` +
|
|
1005
|
+
` key=${payload.environment.key_hint || "stored"}\n`);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
if (action === "disconnect") {
|
|
1009
|
+
const environmentId = requiredAnthropicEnvironmentId(command[2], action);
|
|
1010
|
+
if (!hasFlag(flags, "force")) {
|
|
1011
|
+
throw new CliError("confirmation_required", "anthropic-environments disconnect requires --force.", {
|
|
1012
|
+
details: { environment_id: environmentId },
|
|
1013
|
+
nextActions: [commandAction(["sanbox", "anthropic-environments", "disconnect", environmentId, "--force", "--json"], "Disconnect the environment while preserving its session workspace mappings.")]
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
let payload;
|
|
1017
|
+
try {
|
|
1018
|
+
payload = await client.disconnectAnthropicEnvironment(environmentId);
|
|
1019
|
+
}
|
|
1020
|
+
catch (error) {
|
|
1021
|
+
throw anthropicEnvironmentError(error, client, environmentId);
|
|
1022
|
+
}
|
|
1023
|
+
if (hasFlag(flags, "json")) {
|
|
1024
|
+
printSuccess("anthropic_environments.disconnect", payload, jsonContext(client));
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
process.stdout.write(`${payload.environment_id} ${payload.disconnected ? "disconnected" : "not found"}\n`);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
throw new CliError("anthropic_environments_action_required", "anthropic-environments requires an action: list, get, connect, or disconnect.");
|
|
1031
|
+
};
|
|
763
1032
|
const commandTemplates = async (command, flags) => {
|
|
764
1033
|
const client = makeClient(flags);
|
|
765
1034
|
const action = command[1];
|
|
@@ -767,7 +1036,7 @@ const commandTemplates = async (command, flags) => {
|
|
|
767
1036
|
const payload = await client.listTemplates();
|
|
768
1037
|
const nextActions = payload.templates.length === 0
|
|
769
1038
|
? [
|
|
770
|
-
commandAction(["sanbox", "context", "--json"], "Inspect the
|
|
1039
|
+
commandAction(["sanbox", "context", "--json"], "Inspect the API key's organization and current authentication role."),
|
|
771
1040
|
templateAdminConsoleAction(client)
|
|
772
1041
|
]
|
|
773
1042
|
: [];
|
|
@@ -791,7 +1060,7 @@ const commandTemplates = async (command, flags) => {
|
|
|
791
1060
|
throw new CliError("template_not_found", error.message, {
|
|
792
1061
|
status: error.status,
|
|
793
1062
|
details: { template_id: id },
|
|
794
|
-
nextActions: [commandAction(["sanbox", "templates", "list", "--json"], "List templates available to the
|
|
1063
|
+
nextActions: [commandAction(["sanbox", "templates", "list", "--json"], "List templates available to the API key's organization.")]
|
|
795
1064
|
});
|
|
796
1065
|
}
|
|
797
1066
|
throw error;
|
|
@@ -814,7 +1083,7 @@ const commandTemplates = async (command, flags) => {
|
|
|
814
1083
|
throw new CliError("template_validation_failed", error.message, {
|
|
815
1084
|
status: error.status,
|
|
816
1085
|
details: { template_id: id },
|
|
817
|
-
nextActions: [commandAction(["sanbox", "templates", "list", "--json"], "List templates available to the
|
|
1086
|
+
nextActions: [commandAction(["sanbox", "templates", "list", "--json"], "List templates available to the API key's organization.")]
|
|
818
1087
|
});
|
|
819
1088
|
}
|
|
820
1089
|
throw error;
|
|
@@ -835,6 +1104,10 @@ const commandTemplates = async (command, flags) => {
|
|
|
835
1104
|
const name = requiredPositional(flagString(flags, "name"), "template_name_required", "templates create requires --name.");
|
|
836
1105
|
const modelProvider = requiredPositional(flagString(flags, "model-provider"), "model_provider_required", "templates create requires --model-provider.");
|
|
837
1106
|
const model = requiredPositional(flagString(flags, "model"), "model_required", "templates create requires --model.");
|
|
1107
|
+
const harness = flagString(flags, "harness", "opencode");
|
|
1108
|
+
if (harness !== "opencode" && harness !== "hermes" && harness !== "browser-use") {
|
|
1109
|
+
throw new CliError("invalid_harness", "templates create --harness must be opencode, hermes, or browser-use.");
|
|
1110
|
+
}
|
|
838
1111
|
const budgetRaw = flagString(flags, "llm-budget-usd");
|
|
839
1112
|
const parsedBudgetUsd = budgetRaw ? Number(budgetRaw) : undefined;
|
|
840
1113
|
const llmBudgetUsd = parsedBudgetUsd === undefined
|
|
@@ -844,14 +1117,67 @@ const commandTemplates = async (command, flags) => {
|
|
|
844
1117
|
(!Number.isFinite(parsedBudgetUsd) || llmBudgetUsd <= 0 || llmBudgetUsd > 100_000)) {
|
|
845
1118
|
throw new CliError("invalid_llm_budget", "templates create --llm-budget-usd must be at least 0.000001 and no more than 100000.");
|
|
846
1119
|
}
|
|
1120
|
+
if (harness === "hermes" && llmBudgetUsd !== undefined) {
|
|
1121
|
+
throw new CliError("hermes_budget_unsupported", "Always-on Hermes templates do not support --llm-budget-usd yet.");
|
|
1122
|
+
}
|
|
1123
|
+
const telegramBotToken = process.env.SANBOX_TELEGRAM_BOT_TOKEN?.trim() || "";
|
|
1124
|
+
const telegramAllowedUsers = flagList(flags, "telegram-allowed-user").map((value) => value.trim()).filter(Boolean);
|
|
1125
|
+
const browserFlagNames = [
|
|
1126
|
+
"browser-domain",
|
|
1127
|
+
"browser-max-steps",
|
|
1128
|
+
"browser-step-timeout-seconds",
|
|
1129
|
+
"browser-vision-mode",
|
|
1130
|
+
"browser-viewport",
|
|
1131
|
+
"browser-download-policy",
|
|
1132
|
+
"browser-additional-instructions"
|
|
1133
|
+
];
|
|
1134
|
+
if (harness !== "browser-use" && browserFlagNames.some((name) => flags[name] !== undefined)) {
|
|
1135
|
+
throw new CliError("browser_use_config_requires_browser_use", "Browser settings require --harness browser-use.");
|
|
1136
|
+
}
|
|
1137
|
+
if (harness === "browser-use" &&
|
|
1138
|
+
modelProvider !== "openai" &&
|
|
1139
|
+
modelProvider !== "anthropic" &&
|
|
1140
|
+
modelProvider !== "google") {
|
|
1141
|
+
throw new CliError("browser_use_provider_unsupported", "Browser Use templates require --model-provider openai, anthropic, or google.");
|
|
1142
|
+
}
|
|
1143
|
+
const browserDomains = harness === "browser-use"
|
|
1144
|
+
? parseBrowserDomains(flagList(flags, "browser-domain"))
|
|
1145
|
+
: [];
|
|
1146
|
+
const browserUse = harness === "browser-use"
|
|
1147
|
+
? parseBrowserUseConfig(flags)
|
|
1148
|
+
: undefined;
|
|
1149
|
+
if (harness === "hermes" && !telegramBotToken) {
|
|
1150
|
+
throw new CliError("telegram_bot_token_required", "Hermes template creation requires SANBOX_TELEGRAM_BOT_TOKEN.");
|
|
1151
|
+
}
|
|
1152
|
+
if (harness === "hermes" && telegramAllowedUsers.length === 0) {
|
|
1153
|
+
throw new CliError("telegram_allowed_user_required", "Hermes template creation requires at least one --telegram-allowed-user.");
|
|
1154
|
+
}
|
|
847
1155
|
let payload;
|
|
848
1156
|
try {
|
|
849
1157
|
payload = await client.createTemplate({
|
|
850
1158
|
name,
|
|
851
1159
|
provider_id: modelProvider,
|
|
852
1160
|
model_id: model,
|
|
853
|
-
|
|
854
|
-
...(
|
|
1161
|
+
...(llmBudgetUsd === undefined ? {} : { llm_budget_usd: llmBudgetUsd }),
|
|
1162
|
+
...(harness === "browser-use" ? {
|
|
1163
|
+
harness,
|
|
1164
|
+
browser_use: browserUse,
|
|
1165
|
+
network_policy: {
|
|
1166
|
+
default_action: "deny",
|
|
1167
|
+
rules: browserDomains.map((destination) => ({
|
|
1168
|
+
destination,
|
|
1169
|
+
ports: [
|
|
1170
|
+
{ from: 80, to: 80 },
|
|
1171
|
+
{ from: 443, to: 443 }
|
|
1172
|
+
]
|
|
1173
|
+
}))
|
|
1174
|
+
}
|
|
1175
|
+
} : {}),
|
|
1176
|
+
...(harness === "hermes" ? {
|
|
1177
|
+
harness,
|
|
1178
|
+
telegram_bot_token: telegramBotToken,
|
|
1179
|
+
telegram_allowed_users: telegramAllowedUsers
|
|
1180
|
+
} : {})
|
|
855
1181
|
});
|
|
856
1182
|
}
|
|
857
1183
|
catch (error) {
|
|
@@ -860,7 +1186,9 @@ const commandTemplates = async (command, flags) => {
|
|
|
860
1186
|
if (hasFlag(flags, "json")) {
|
|
861
1187
|
printSuccess("templates.create", payload, jsonContext(client), [
|
|
862
1188
|
commandAction(["sanbox", "templates", "validate", payload.template.id, "--json"], "Validate the new template before running it."),
|
|
863
|
-
commandAction(["sanbox", "run", "<task>", "--template", payload.template.id],
|
|
1189
|
+
commandAction(["sanbox", "run", harness === "hermes" ? "<agent role and instructions>" : "<task>", "--template", payload.template.id], harness === "hermes"
|
|
1190
|
+
? "Start the always-on gateway; interact with it through the configured Telegram bot."
|
|
1191
|
+
: "Create a run with the new template.")
|
|
864
1192
|
]);
|
|
865
1193
|
return;
|
|
866
1194
|
}
|
|
@@ -926,6 +1254,43 @@ const commandRuns = async (command, flags) => {
|
|
|
926
1254
|
}
|
|
927
1255
|
return;
|
|
928
1256
|
}
|
|
1257
|
+
if (action === "share") {
|
|
1258
|
+
const expiresInSeconds = parseFileAccessExpiry(flagString(flags, "expires", "1h"));
|
|
1259
|
+
const name = flagString(flags, "name").trim();
|
|
1260
|
+
const payload = await client.createFileAccessPoint(runId, {
|
|
1261
|
+
expires_in_seconds: expiresInSeconds,
|
|
1262
|
+
...(name ? { name } : {})
|
|
1263
|
+
});
|
|
1264
|
+
if (hasFlag(flags, "json")) {
|
|
1265
|
+
printSuccess("runs.share", payload, jsonContext(client), [
|
|
1266
|
+
commandAction(["sanbox", "runs", "unshare", runId, payload.access_point.id, "--json"], "Revoke this root filesystem link.")
|
|
1267
|
+
]);
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
process.stdout.write(`${payload.url}\n`);
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
if (action === "shares") {
|
|
1274
|
+
const payload = await client.listFileAccessPoints(runId);
|
|
1275
|
+
if (hasFlag(flags, "json")) {
|
|
1276
|
+
printSuccess("runs.shares", payload, jsonContext(client));
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
for (const accessPoint of payload.access_points) {
|
|
1280
|
+
process.stdout.write(`${accessPoint.id}\t${accessPoint.status}\t${accessPoint.expires_at}\t${accessPoint.name}\n`);
|
|
1281
|
+
}
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
if (action === "unshare") {
|
|
1285
|
+
const accessPointId = requiredPositional(command[3], "file_access_point_id_required", "runs unshare requires an access point id.");
|
|
1286
|
+
const payload = await client.revokeFileAccessPoint(runId, accessPointId);
|
|
1287
|
+
if (hasFlag(flags, "json")) {
|
|
1288
|
+
printSuccess("runs.unshare", payload, jsonContext(client));
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
process.stdout.write(`revoked ${payload.access_point.id}\n`);
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
929
1294
|
if (action === "download") {
|
|
930
1295
|
const outputDirectory = requiredPositional(flagString(flags, "output"), "output_directory_required", "runs download requires --output.");
|
|
931
1296
|
const payload = await client.listArtifacts(runId);
|
|
@@ -992,7 +1357,29 @@ const commandRuns = async (command, flags) => {
|
|
|
992
1357
|
printRun(payload);
|
|
993
1358
|
return;
|
|
994
1359
|
}
|
|
1360
|
+
if (action === "resume" || action === "pause") {
|
|
1361
|
+
const submitted = action === "resume"
|
|
1362
|
+
? await client.resumeRun(runId)
|
|
1363
|
+
: await client.pauseRun(runId);
|
|
1364
|
+
const payload = hasFlag(flags, "wait")
|
|
1365
|
+
? await waitForSandboxState(client, runId, action === "resume" ? "running" : "paused", {
|
|
1366
|
+
pollIntervalMs: integerFlag(flags, "poll-interval-ms", 2000, 250, 60_000),
|
|
1367
|
+
timeoutSeconds: integerFlag(flags, "timeout-seconds", 1800, 1, 604_800)
|
|
1368
|
+
})
|
|
1369
|
+
: submitted;
|
|
1370
|
+
if (hasFlag(flags, "json")) {
|
|
1371
|
+
printSuccess(`runs.${action}`, publicRunPayload(payload), jsonContext(client));
|
|
1372
|
+
}
|
|
1373
|
+
else if (hasFlag(flags, "wait")) {
|
|
1374
|
+
printRun(payload);
|
|
1375
|
+
}
|
|
1376
|
+
else {
|
|
1377
|
+
process.stdout.write(`sandbox ${action} requested for run ${runId}\n`);
|
|
1378
|
+
}
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
995
1381
|
if (action === "message") {
|
|
1382
|
+
const commandName = "runs.message";
|
|
996
1383
|
const flaggedMessage = flagString(flags, "message");
|
|
997
1384
|
const positionalMessage = positionalText(command, 3);
|
|
998
1385
|
if (flaggedMessage && positionalMessage) {
|
|
@@ -1000,7 +1387,7 @@ const commandRuns = async (command, flags) => {
|
|
|
1000
1387
|
}
|
|
1001
1388
|
const message = flaggedMessage || positionalMessage;
|
|
1002
1389
|
if (!message)
|
|
1003
|
-
throw new Error(
|
|
1390
|
+
throw new Error(`runs ${action} requires an instruction as positional text or --message.`);
|
|
1004
1391
|
if (wantsWatch(flags) && hasFlag(flags, "json")) {
|
|
1005
1392
|
throw new Error("--json cannot be combined with --watch or --jsonl.");
|
|
1006
1393
|
}
|
|
@@ -1010,14 +1397,14 @@ const commandRuns = async (command, flags) => {
|
|
|
1010
1397
|
const submitted = await client.sendMessage(runId, message);
|
|
1011
1398
|
if (!hasFlag(flags, "wait") && !wantsWatch(flags)) {
|
|
1012
1399
|
if (hasFlag(flags, "json")) {
|
|
1013
|
-
printSuccess(
|
|
1400
|
+
printSuccess(commandName, {
|
|
1014
1401
|
...publicRunPayload(submitted),
|
|
1015
1402
|
message: submitted.message,
|
|
1016
1403
|
chat_job: submitted.chat_job
|
|
1017
1404
|
}, jsonContext(client));
|
|
1018
1405
|
}
|
|
1019
1406
|
else
|
|
1020
|
-
process.stdout.write(`queued follow-up for run ${runId}\n`);
|
|
1407
|
+
process.stdout.write(`queued OpenCode follow-up for run ${runId}\n`);
|
|
1021
1408
|
return;
|
|
1022
1409
|
}
|
|
1023
1410
|
const chatJobId = submitted.chat_job.id;
|
|
@@ -1064,7 +1451,7 @@ const commandRuns = async (command, flags) => {
|
|
|
1064
1451
|
});
|
|
1065
1452
|
const messages = await client.listMessages(runId);
|
|
1066
1453
|
if (hasFlag(flags, "json")) {
|
|
1067
|
-
printSuccess(
|
|
1454
|
+
printSuccess(commandName, {
|
|
1068
1455
|
...publicRunPayload(submitted),
|
|
1069
1456
|
message: submitted.message,
|
|
1070
1457
|
chat_job: submitted.chat_job,
|
|
@@ -1096,9 +1483,23 @@ const commandRuns = async (command, flags) => {
|
|
|
1096
1483
|
const commandDoctor = async (flags) => {
|
|
1097
1484
|
const localConfig = readLocalConfig();
|
|
1098
1485
|
const apiUrl = String(flags["api-url"] || process.env.SANBOX_API_URL || localConfig.api_url || defaultApiUrl).replace(/\/+$/, "");
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1486
|
+
const environmentApiKey = process.env.SANBOX_API_KEY || "";
|
|
1487
|
+
let savedUserSession = null;
|
|
1488
|
+
if (!environmentApiKey) {
|
|
1489
|
+
try {
|
|
1490
|
+
savedUserSession = userSessionStore.read(apiUrl);
|
|
1491
|
+
if (savedUserSession && Date.parse(savedUserSession.expiresAt) <= Date.now()) {
|
|
1492
|
+
userSessionStore.delete(apiUrl);
|
|
1493
|
+
savedUserSession = null;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
catch {
|
|
1497
|
+
savedUserSession = null;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
const apiKey = environmentApiKey || savedUserSession?.accessToken || "";
|
|
1101
1501
|
const selection = readTemplateSelection(flags, { required: false });
|
|
1502
|
+
let resolvedOrg = "";
|
|
1102
1503
|
const checks = [];
|
|
1103
1504
|
const nextActions = [];
|
|
1104
1505
|
const add = (name, ok, detail, data) => checks.push({
|
|
@@ -1109,13 +1510,13 @@ const commandDoctor = async (flags) => {
|
|
|
1109
1510
|
});
|
|
1110
1511
|
add("node", Number(process.versions.node.split(".")[0]) >= 20, process.version);
|
|
1111
1512
|
add("api_url", Boolean(apiUrl), apiUrl);
|
|
1112
|
-
add("
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1513
|
+
add("authentication", Boolean(apiKey), environmentApiKey
|
|
1514
|
+
? "organization API key present in environment"
|
|
1515
|
+
: savedUserSession
|
|
1516
|
+
? `signed in as ${savedUserSession.email}`
|
|
1517
|
+
: "not signed in");
|
|
1117
1518
|
if (!apiKey) {
|
|
1118
|
-
nextActions.push(commandAction(["sanbox", "
|
|
1519
|
+
nextActions.push(commandAction(["sanbox", "login"], "Sign in with your Sanbox user account."));
|
|
1119
1520
|
}
|
|
1120
1521
|
try {
|
|
1121
1522
|
const res = await fetch(`${apiUrl}/health`);
|
|
@@ -1124,50 +1525,50 @@ const commandDoctor = async (flags) => {
|
|
|
1124
1525
|
catch (error) {
|
|
1125
1526
|
add("api_health", false, error instanceof Error ? error.message : String(error));
|
|
1126
1527
|
}
|
|
1127
|
-
if (
|
|
1128
|
-
const client = new SanboxClient({ apiUrl,
|
|
1528
|
+
if (apiKey) {
|
|
1529
|
+
const client = new SanboxClient({ apiUrl, apiKey });
|
|
1129
1530
|
try {
|
|
1130
|
-
const
|
|
1131
|
-
|
|
1132
|
-
? me.organizations
|
|
1133
|
-
: [];
|
|
1531
|
+
const organization = await client.organization();
|
|
1532
|
+
resolvedOrg = organization.slug;
|
|
1134
1533
|
add("auth", true, "authenticated");
|
|
1135
|
-
add("
|
|
1534
|
+
add("organization", true, organization.slug, organization);
|
|
1136
1535
|
}
|
|
1137
1536
|
catch (error) {
|
|
1138
1537
|
add("auth", false, error instanceof Error ? error.message : String(error));
|
|
1139
1538
|
}
|
|
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 {
|
|
1539
|
+
if (resolvedOrg) {
|
|
1152
1540
|
try {
|
|
1153
|
-
const
|
|
1154
|
-
add("
|
|
1541
|
+
const providers = await client.listModelProviders();
|
|
1542
|
+
add("model_providers", true, providers.providers.map((item) => `${providerId(item)}:${item.status || (item.configured ? "configured" : "not_configured")}`).join(", ") || "none", providers.providers);
|
|
1543
|
+
}
|
|
1544
|
+
catch (error) {
|
|
1545
|
+
add("model_providers", false, error instanceof Error ? error.message : String(error));
|
|
1546
|
+
}
|
|
1547
|
+
if (!selection) {
|
|
1548
|
+
add("template", false, "not selected; use --template, SANBOX_TEMPLATE, or .sanbox/config.json default_template");
|
|
1549
|
+
nextActions.push(...templateActions());
|
|
1550
|
+
}
|
|
1551
|
+
else {
|
|
1155
1552
|
try {
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1553
|
+
const template = await client.getTemplate(selection.id);
|
|
1554
|
+
add("template", true, `${template.template.id}; source=${selection.source}`, template.template);
|
|
1555
|
+
try {
|
|
1556
|
+
const validation = await client.validateTemplate(selection.id);
|
|
1557
|
+
const runnable = validationRunnable(validation);
|
|
1558
|
+
add("template_readiness", runnable, runnable ? "ready" : "blocked", validation);
|
|
1559
|
+
if (!runnable) {
|
|
1560
|
+
nextActions.push(commandAction(["sanbox", "model-providers", "list", "--json"], "Inspect model-provider status."), providerConsoleAction(client));
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
catch (error) {
|
|
1564
|
+
add("template_readiness", false, error instanceof Error ? error.message : String(error));
|
|
1161
1565
|
}
|
|
1162
1566
|
}
|
|
1163
1567
|
catch (error) {
|
|
1164
|
-
add("
|
|
1568
|
+
add("template", false, error instanceof Error ? error.message : String(error));
|
|
1569
|
+
nextActions.push(...templateActions());
|
|
1165
1570
|
}
|
|
1166
1571
|
}
|
|
1167
|
-
catch (error) {
|
|
1168
|
-
add("template", false, error instanceof Error ? error.message : String(error));
|
|
1169
|
-
nextActions.push(...templateActions());
|
|
1170
|
-
}
|
|
1171
1572
|
}
|
|
1172
1573
|
}
|
|
1173
1574
|
try {
|
|
@@ -1183,7 +1584,7 @@ const commandDoctor = async (flags) => {
|
|
|
1183
1584
|
checks
|
|
1184
1585
|
};
|
|
1185
1586
|
if (hasFlag(flags, "json")) {
|
|
1186
|
-
printSuccess("doctor", output, { api_url: apiUrl, ...(
|
|
1587
|
+
printSuccess("doctor", output, { api_url: apiUrl, ...(resolvedOrg ? { org: resolvedOrg } : {}) }, nextActions);
|
|
1187
1588
|
}
|
|
1188
1589
|
else {
|
|
1189
1590
|
for (const check of checks) {
|
|
@@ -1193,6 +1594,69 @@ const commandDoctor = async (flags) => {
|
|
|
1193
1594
|
if (!output.ok)
|
|
1194
1595
|
process.exitCode = 2;
|
|
1195
1596
|
};
|
|
1597
|
+
const commandSSH = async (command, flags) => {
|
|
1598
|
+
const runId = requiredPositional(command[1], "run_id_required", "ssh requires a run id.");
|
|
1599
|
+
const client = new SanboxClient(readUserConfig(flags));
|
|
1600
|
+
const exitCode = await openSSH(client, runId);
|
|
1601
|
+
if (exitCode !== 0)
|
|
1602
|
+
process.exitCode = exitCode;
|
|
1603
|
+
};
|
|
1604
|
+
const commandLogin = async (flags) => {
|
|
1605
|
+
const apiUrl = resolveApiUrl(flags);
|
|
1606
|
+
const previousSession = userSessionStore.read(apiUrl);
|
|
1607
|
+
const session = await loginWithBrowser({
|
|
1608
|
+
apiUrl,
|
|
1609
|
+
launchBrowser: hasFlag(flags, "no-browser") ? () => false : openBrowser,
|
|
1610
|
+
onPrompt: (request, browserOpened) => {
|
|
1611
|
+
process.stderr.write(`${browserOpened ? "Complete sign-in in your browser" : "Open this URL in your browser"}:\n` +
|
|
1612
|
+
`${request.verification_url}\n\nConfirm code: ${request.user_code}\n`);
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
userSessionStore.write(session);
|
|
1616
|
+
if (previousSession && previousSession.accessToken !== session.accessToken) {
|
|
1617
|
+
try {
|
|
1618
|
+
await revokeUserSession(apiUrl, previousSession.accessToken);
|
|
1619
|
+
}
|
|
1620
|
+
catch {
|
|
1621
|
+
process.stderr.write("Warning: the previous local CLI session could not be revoked and will expire automatically.\n");
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
if (hasFlag(flags, "json")) {
|
|
1625
|
+
printSuccess("auth.login", {
|
|
1626
|
+
email: session.email,
|
|
1627
|
+
organization: session.organization,
|
|
1628
|
+
expires_at: session.expiresAt
|
|
1629
|
+
}, { api_url: apiUrl, org: session.organization.slug });
|
|
1630
|
+
}
|
|
1631
|
+
else {
|
|
1632
|
+
process.stdout.write(`Signed in as ${session.email} for ${session.organization.name}.\n`);
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1635
|
+
const commandLogout = async (flags) => {
|
|
1636
|
+
const apiUrl = resolveApiUrl(flags);
|
|
1637
|
+
const session = userSessionStore.read(apiUrl);
|
|
1638
|
+
if (session) {
|
|
1639
|
+
let revokeError = null;
|
|
1640
|
+
try {
|
|
1641
|
+
await revokeUserSession(apiUrl, session.accessToken);
|
|
1642
|
+
}
|
|
1643
|
+
catch (error) {
|
|
1644
|
+
revokeError = error;
|
|
1645
|
+
}
|
|
1646
|
+
finally {
|
|
1647
|
+
userSessionStore.delete(apiUrl);
|
|
1648
|
+
}
|
|
1649
|
+
if (revokeError) {
|
|
1650
|
+
throw new CliError("logout_revoke_failed", "The local session was removed, but the server session could not be revoked and will expire automatically.");
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
if (hasFlag(flags, "json")) {
|
|
1654
|
+
printSuccess("auth.logout", { signed_out: Boolean(session) }, { api_url: apiUrl });
|
|
1655
|
+
}
|
|
1656
|
+
else {
|
|
1657
|
+
process.stdout.write(session ? "Signed out.\n" : "No saved Sanbox user session.\n");
|
|
1658
|
+
}
|
|
1659
|
+
};
|
|
1196
1660
|
const commandInit = async (command, flags) => {
|
|
1197
1661
|
const dir = path.join(cwd(), ".sanbox");
|
|
1198
1662
|
if (command[1] === "agent") {
|
|
@@ -1220,11 +1684,9 @@ const commandInit = async (command, flags) => {
|
|
|
1220
1684
|
const force = hasFlag(flags, "force");
|
|
1221
1685
|
const localConfig = readLocalConfig();
|
|
1222
1686
|
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
1687
|
const template = readTemplateSelection(flags, { required: false });
|
|
1225
1688
|
const config = {
|
|
1226
1689
|
api_url: apiUrl,
|
|
1227
|
-
org,
|
|
1228
1690
|
...(template ? { default_template: template.id } : {})
|
|
1229
1691
|
};
|
|
1230
1692
|
const sanboxIgnore = [
|
|
@@ -1245,7 +1707,7 @@ const commandInit = async (command, flags) => {
|
|
|
1245
1707
|
printSuccess("init", {
|
|
1246
1708
|
files: results.map(([file, status]) => ({ file, status })),
|
|
1247
1709
|
template_selection: template
|
|
1248
|
-
}, { api_url: apiUrl
|
|
1710
|
+
}, { api_url: apiUrl }, template ? [] : templateActions());
|
|
1249
1711
|
}
|
|
1250
1712
|
else {
|
|
1251
1713
|
for (const [file, status] of results)
|
|
@@ -1259,8 +1721,12 @@ const helpFor = (command) => {
|
|
|
1259
1721
|
return doctorHelp;
|
|
1260
1722
|
if (command[0] === "model-providers")
|
|
1261
1723
|
return modelProvidersHelp;
|
|
1724
|
+
if (command[0] === "anthropic-environments")
|
|
1725
|
+
return anthropicEnvironmentsHelp;
|
|
1262
1726
|
if (command[0] === "templates")
|
|
1263
1727
|
return templatesHelp;
|
|
1728
|
+
if (command[0] === "ssh")
|
|
1729
|
+
return sshHelp;
|
|
1264
1730
|
return help;
|
|
1265
1731
|
};
|
|
1266
1732
|
const commandId = (command) => {
|
|
@@ -1268,12 +1734,13 @@ const commandId = (command) => {
|
|
|
1268
1734
|
return "version";
|
|
1269
1735
|
if (command[0] === "auth")
|
|
1270
1736
|
return "auth.check";
|
|
1271
|
-
if (command[0] === "orgs")
|
|
1272
|
-
return `orgs.${command[1] || "unknown"}`;
|
|
1273
1737
|
if (command[0] === "context")
|
|
1274
1738
|
return "context.get";
|
|
1275
1739
|
if (command[0] === "model-providers")
|
|
1276
1740
|
return `model_providers.${command[1] || "unknown"}`;
|
|
1741
|
+
if (command[0] === "anthropic-environments") {
|
|
1742
|
+
return `anthropic_environments.${command[1] || "unknown"}`;
|
|
1743
|
+
}
|
|
1277
1744
|
if (command[0] === "templates")
|
|
1278
1745
|
return `templates.${command[1] || "unknown"}`;
|
|
1279
1746
|
if (command[0] === "run")
|
|
@@ -1282,6 +1749,8 @@ const commandId = (command) => {
|
|
|
1282
1749
|
return "batch.create";
|
|
1283
1750
|
if (command[0] === "runs")
|
|
1284
1751
|
return `runs.${command[1] || "unknown"}`;
|
|
1752
|
+
if (command[0] === "ssh" || command[0] === "ssh-proxy")
|
|
1753
|
+
return command[0];
|
|
1285
1754
|
return command[0] || "help";
|
|
1286
1755
|
};
|
|
1287
1756
|
const main = async (command, flags) => {
|
|
@@ -1306,14 +1775,19 @@ const main = async (command, flags) => {
|
|
|
1306
1775
|
}
|
|
1307
1776
|
if (command[0] === "auth" && command[1] === "check")
|
|
1308
1777
|
return commandAuthCheck(flags);
|
|
1309
|
-
if (command[0] === "
|
|
1310
|
-
return
|
|
1778
|
+
if (command[0] === "login")
|
|
1779
|
+
return commandLogin(flags);
|
|
1780
|
+
if (command[0] === "logout")
|
|
1781
|
+
return commandLogout(flags);
|
|
1311
1782
|
if (command[0] === "context")
|
|
1312
1783
|
return commandContext(flags);
|
|
1313
1784
|
if (command[0] === "doctor")
|
|
1314
1785
|
return commandDoctor(flags);
|
|
1315
1786
|
if (command[0] === "model-providers")
|
|
1316
1787
|
return commandModelProviders(command, flags);
|
|
1788
|
+
if (command[0] === "anthropic-environments") {
|
|
1789
|
+
return commandAnthropicEnvironments(command, flags);
|
|
1790
|
+
}
|
|
1317
1791
|
if (command[0] === "templates")
|
|
1318
1792
|
return commandTemplates(command, flags);
|
|
1319
1793
|
if (command[0] === "run")
|
|
@@ -1322,6 +1796,14 @@ const main = async (command, flags) => {
|
|
|
1322
1796
|
return commandBatch(flags);
|
|
1323
1797
|
if (command[0] === "runs")
|
|
1324
1798
|
return commandRuns(command, flags);
|
|
1799
|
+
if (command[0] === "ssh")
|
|
1800
|
+
return commandSSH(command, flags);
|
|
1801
|
+
if (command[0] === "ssh-proxy") {
|
|
1802
|
+
const exitCode = await runSSHProxy();
|
|
1803
|
+
if (exitCode !== 0)
|
|
1804
|
+
process.exitCode = exitCode;
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1325
1807
|
if (command[0] === "init")
|
|
1326
1808
|
return commandInit(command, flags);
|
|
1327
1809
|
throw new CliError("unknown_command", `Unknown command: ${command.join(" ")}`);
|