@sanlabs/sanbox-cli 0.0.12 → 0.0.14

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 CHANGED
@@ -15,7 +15,14 @@ installed_cli_version="$(sanbox --version)"
15
15
  test "$installed_cli_version" = "$latest_cli_version"
16
16
  ```
17
17
 
18
- Always use the latest published CLI. CLI 0.0.12 adopts the Stop, Resume, and Delete lifecycle,
18
+ Always use the latest published CLI. CLI 0.0.14 adds repeatable `--allowed-model` options when
19
+ creating OpenCode Computer templates and `sanbox run --model` to select a configured same-provider
20
+ model at startup. The selected model is saved on the run and reused on Resume; SDK per-prompt
21
+ switching does not change that saved startup default.
22
+ CLI 0.0.13 allows service templates to start without an
23
+ initial task, including OpenCode Computers that are ready for later SDK steering, and supports
24
+ Custom OpenAI Proxy providers when creating templates, including Browser Use templates.
25
+ CLI 0.0.12 adopted the Stop, Resume, and Delete lifecycle,
19
26
  separates run state from execution outcome, retains scrubbed deleted-run tombstones, and adds
20
27
  administrator-confirmed template deletion.
21
28
  CLI 0.0.11 added OpenCode Computer templates and short-lived native SDK connections for HTTP, SSE
@@ -96,9 +103,31 @@ sanbox templates create \
96
103
  --harness opencode \
97
104
  --mode computer \
98
105
  --model-provider openai \
99
- --model '<model-id>'
106
+ --model '<default-model-id>' \
107
+ --allowed-model '<additional-model-id>'
100
108
  ```
101
109
 
110
+ Repeat `--allowed-model` to make more models from the same provider available for native OpenCode
111
+ SDK prompt switching. The default `--model` is included automatically. At template creation, Sanbox
112
+ checks that each ID is in the organization's saved provider catalog; it does not infer model
113
+ compatibility or availability. Later catalog changes do not block Computer startup or resume, even
114
+ if the default model disappears. Configured IDs remain registered, and model errors surface when
115
+ a prompt uses them.
116
+
117
+ Choose a different startup model when creating an OpenCode Computer run:
118
+
119
+ ```bash
120
+ sanbox run --template <computer-template-id> --model '<additional-model-id>' --json
121
+ sanbox run "Inspect the repository" --template <computer-template-id> --model '<additional-model-id>' --json
122
+ ```
123
+
124
+ `sanbox run --model` accepts an exact ID from the template's configured models (`allowed_model_ids`),
125
+ using the template's provider. Omit it to use the template default. The selection is saved on the run;
126
+ `sanbox runs resume <run-id>` reuses it without another `--model` flag. Neither run creation nor SDK
127
+ per-prompt switching changes the template default, and SDK switches do not update the run's saved
128
+ startup model. This override is supported only for OpenCode Computer, not task or Hermes templates.
129
+ An `external_run_id` retry with a different explicit model returns `idempotency_conflict`.
130
+
102
131
  Existing templates keep their current names, IDs, slugs, and task behavior. Browser Use is for one-shot
103
132
  web tasks whose target domains are already approved. Hermes service templates are always-on. A Hermes template
104
133
  declares which optional channels its runs may activate:
@@ -128,7 +157,8 @@ and `--force` so scripts cannot remove a template accidentally.
128
157
  ## Create A Browser Use Template
129
158
 
130
159
  Browser Use runs local headless Chromium inside the Firecracker sandbox. It requires OpenAI,
131
- Anthropic, Google Gemini, or Hetzner Inference and at least one explicit browser target:
160
+ Anthropic, Google Gemini, Hetzner Inference, or a Custom OpenAI Proxy and at least one
161
+ explicit browser target:
132
162
 
133
163
  ```bash
134
164
  sanbox templates create \
@@ -151,6 +181,8 @@ allowed downloads. Browser Use templates are one-shot agent executions.
151
181
 
152
182
  ## Run Idempotently
153
183
 
184
+ Task templates require a positional task or `--task`. Service templates may omit it:
185
+
154
186
  ```bash
155
187
  sanbox run "Review this repo and write output/report.md" \
156
188
  --input src/ \
@@ -163,8 +195,18 @@ sanbox run "Review this repo and write output/report.md" \
163
195
  --input src/ \
164
196
  --wait \
165
197
  --json
198
+
199
+ sanbox run \
200
+ --template "<service-template-id>" \
201
+ --external-run-id "<stable-service-id>" \
202
+ --json
166
203
  ```
167
204
 
205
+ A taskless OpenCode Computer starts `opencode serve` without making an initial model call. A
206
+ taskless Hermes service starts its gateways and waits for WebUI or configured channel messages.
207
+ Supplying instructions starts the initial OpenCode task or seeds Hermes's persistent operating
208
+ context, preserving the existing behavior.
209
+
168
210
  Repeat `--input` for files, directories, or globs. The CLI excludes common secrets and applies `.sanboxignore`. `--include` is a deprecated compatibility alias.
169
211
 
170
212
  For a Hermes template that allows `email`, optionally request an address from one of the AgentMail
@@ -289,8 +331,9 @@ authenticated WebSocket. The temporary key is deleted when the connection closes
289
331
  user token is not passed to the OpenSSH child process.
290
332
 
291
333
  OpenCode and Browser Use runs are one-shot agent executions. OpenCode Computer keeps its private
292
- server running after the initial task so it can accept later steering. A persisted task workspace can
293
- still be fresh-booted manually for inspection after the run stops.
334
+ server running so it can accept later steering; when an initial task is supplied, Sanbox submits it
335
+ after the server becomes ready. A persisted task workspace can still be fresh-booted manually for
336
+ inspection after the run stops.
294
337
 
295
338
  ## Connect The OpenCode SDK
296
339
 
package/dist/args.js CHANGED
@@ -3,6 +3,7 @@ const multiFlags = new Set([
3
3
  "include",
4
4
  "artifact",
5
5
  "channel",
6
+ "allowed-model",
6
7
  "telegram-allowed-user",
7
8
  "browser-domain"
8
9
  ]);
package/dist/cli.js CHANGED
@@ -40,10 +40,11 @@ Commands:
40
40
  sanbox templates list [--json]
41
41
  sanbox templates get <template-id> [--json]
42
42
  sanbox templates validate <template-id> [--json]
43
- sanbox templates create --name "..." --model-provider <provider-id> --model <model-id> [--harness opencode|hermes|browser-use] [--mode task|computer] [--channel email|telegram] [--browser-domain <hostname>] [--llm-budget-usd <amount>] [--json]
43
+ sanbox templates create --name "..." --model-provider <provider-id> --model <model-id> [--harness opencode|hermes|browser-use] [--mode task|computer] [--allowed-model <model-id>] [--channel email|telegram] [--browser-domain <hostname>] [--llm-budget-usd <amount>] [--json]
44
44
  sanbox templates delete <template-id> --force [--json]
45
- sanbox run "task" --template <template-id> [--email-address <address>] [--telegram-bot-token <token>] [--telegram-allowed-user <id>] [--input <path>] [--wait | --watch] [--json | --jsonl]
46
- sanbox run --task "..." --template <template-id> [--email-address <address>] [--telegram-bot-token <token>] [--telegram-allowed-user <id>] [--input <path>] [--wait | --watch] [--json | --jsonl]
45
+ sanbox run "task" --template <template-id> [--model <model-id>] [--email-address <address>] [--telegram-bot-token <token>] [--telegram-allowed-user <id>] [--input <path>] [--wait | --watch] [--json | --jsonl]
46
+ sanbox run --task "..." --template <template-id> [--model <model-id>] [--email-address <address>] [--telegram-bot-token <token>] [--telegram-allowed-user <id>] [--input <path>] [--wait | --watch] [--json | --jsonl]
47
+ sanbox run --template <service-template-id> [--model <model-id>] [--email-address <address>] [--telegram-bot-token <token>] [--telegram-allowed-user <id>] [--json]
47
48
  sanbox batch --tasks tasks.json --template <template-id> [--input <path>] [--max-parallel 5] [--wait] [--json]
48
49
  sanbox runs list [--limit 50] [--json]
49
50
  sanbox runs get <run-id> [--json]
@@ -93,10 +94,15 @@ const runHelp = `Sanbox run
93
94
  Usage:
94
95
  sanbox run "Review these files" --template <template-id> --input report.pdf --input data/ --wait
95
96
  sanbox run --task "Review these files" --template <template-id> --input report.pdf --wait --json
97
+ sanbox run --template <service-template-id> --json
98
+ sanbox run --template <opencode-computer-template-id> --model <model-id> --json
99
+ sanbox run "Inspect the repository" --template <opencode-computer-template-id> --model <model-id> --json
96
100
 
97
101
  Options:
102
+ --task <instruction> Required for task templates; optional for service templates.
98
103
  --input <path> File, directory, or glob to upload. Repeatable.
99
104
  --template <id> Template id or slug. Required unless SANBOX_TEMPLATE or project config sets it.
105
+ --model <id> Startup model from the OpenCode Computer template's configured models.
100
106
  --external-run-id <id> Idempotency key for retries.
101
107
  --supabase-user-id <id> Supabase Auth user UUID expected during OAuth consent.
102
108
  --email-address <address> Optional for Hermes computers; must use a listed AgentMail domain.
@@ -110,6 +116,10 @@ Options:
110
116
  --after-event-id <id> Resume after an event cursor. Default: 0.
111
117
  --stop-on-interrupt Request Stop when Ctrl-C is pressed.
112
118
  --json Print JSON.
119
+
120
+ --model is supported only for OpenCode Computer, with or without an initial task.
121
+ Omitting --model uses the template's default. The provider stays fixed to the template.
122
+ The chosen startup model is saved on the run and reused on resume; SDK per-prompt switches do not change it.
113
123
  `;
114
124
  const runsSupabaseHelp = `Sanbox Supabase run authorization
115
125
 
@@ -160,13 +170,15 @@ Usage:
160
170
  sanbox templates list [--json]
161
171
  sanbox templates get <template-id> [--json]
162
172
  sanbox templates validate <template-id> [--json]
163
- sanbox templates create --name "Code review" --model-provider <provider-id> --model <model-id> [--harness opencode|hermes|browser-use] [--mode task|computer] [--browser-domain <hostname>] [--llm-budget-usd <amount>] [--json]
173
+ sanbox templates create --name "Code review" --model-provider <provider-id> --model <model-id> [--harness opencode|hermes|browser-use] [--mode task|computer] [--allowed-model <model-id>] [--browser-domain <hostname>] [--llm-budget-usd <amount>] [--json]
164
174
  sanbox templates delete <template-id> --force [--json]
165
175
 
166
176
  Template creation requires an exact provider id and that provider's exact model id.
167
177
  LiteLLM budgets are optional USD amounts and apply separately to each run.
168
178
  OpenCode defaults to --mode task. Use --mode computer for a retained private OpenCode server;
169
179
  per-run LiteLLM budgets are not available for that retained mode yet.
180
+ Use repeatable --allowed-model with OpenCode Computer to make additional models from the selected
181
+ provider available for SDK prompt switching. --model remains the default and is included automatically.
170
182
  Hermes templates are always-on computers. Use repeatable --channel email|telegram to select which
171
183
  channels runs may activate. Email and Telegram credentials are supplied only when creating a run.
172
184
  Browser Use templates run local headless Chromium inside a one-shot sandbox. They require OpenAI
@@ -213,6 +225,13 @@ a Hermes service template for a waited task because it is always-on. Select auto
213
225
  template qualifies; otherwise ask the user. Provider credentials and template administration are
214
226
  console-only.
215
227
 
228
+ For an explicitly requested OpenCode Computer, use a service template with harness opencode and
229
+ execution_mode computer. The task is optional; omit --wait. Use sanbox run --model <model-id> to
230
+ select an exact ID from the template's allowed_model_ids, keeping its provider unchanged. Omit
231
+ --model to use the template default. The startup model is saved on the run and reused on Resume
232
+ without re-specifying --model. SDK per-prompt switching changes neither the template default nor
233
+ the run's saved startup model. This flag is not supported for task or Hermes templates.
234
+
216
235
  Use --json for request/response commands and --jsonl for streams. Parse the versioned envelope:
217
236
  schema_version, ok, command, context, data or error, and next_actions. Execute command actions as
218
237
  argv arrays, never shell strings. Exit 0 means command success, 1 local/API failure, 2 readiness or
@@ -313,7 +332,19 @@ const providerReadinessCodes = new Set([
313
332
  "model_not_found_for_provider",
314
333
  "template_provider_missing"
315
334
  ]);
316
- const runCreationError = (error, client, template) => {
335
+ const runModelSelectionCodes = new Set([
336
+ "invalid_run_model",
337
+ "run_model_not_configured",
338
+ "run_model_override_not_supported"
339
+ ]);
340
+ const runCreationError = (error, client, template, modelId) => {
341
+ if (error instanceof SanboxApiError && runModelSelectionCodes.has(error.code)) {
342
+ return new CliError(error.code, error.message, {
343
+ status: error.status,
344
+ details: { template_id: template.id, model_id: modelId },
345
+ nextActions: [error.code === "run_model_override_not_supported" ? commandAction(["sanbox", "run", "--help"], "Remove --model and retry. Startup model overrides require an OpenCode Computer template.") : commandAction(["sanbox", "templates", "get", template.id, "--json"], "Inspect the template's configured models. Omit --model to use its default.")]
346
+ });
347
+ }
317
348
  if (!(error instanceof SanboxApiError) || !runReadinessCodes.has(error.code))
318
349
  return error;
319
350
  const nextActions = [
@@ -345,7 +376,7 @@ const templateCreationError = (error, client, providerId, modelId) => {
345
376
  return new CliError(error.code, error.message, {
346
377
  status: error.status,
347
378
  details: { provider_id: providerId, model_id: modelId },
348
- nextActions: [commandAction(["sanbox", "model-providers", "models", providerId, "--json"], "List exact selectable and non-selectable model ids exposed by the selected provider.")]
379
+ nextActions: [commandAction(["sanbox", "model-providers", "models", providerId, "--json"], "List exact model IDs in the selected provider's catalog.")]
349
380
  });
350
381
  }
351
382
  if (error.code === "template_name_conflict") {
@@ -458,6 +489,7 @@ const flagSets = {
458
489
  "model",
459
490
  "harness",
460
491
  "mode",
492
+ "allowed-model",
461
493
  "llm-budget-usd",
462
494
  "web-access",
463
495
  "channel",
@@ -470,7 +502,7 @@ const flagSets = {
470
502
  "browser-additional-instructions"
471
503
  ],
472
504
  run: [
473
- ...commonFlags, "task", "input", "template", "external-run-id", "email-address",
505
+ ...commonFlags, "task", "input", "template", "model", "external-run-id", "email-address",
474
506
  "supabase-user-id",
475
507
  "telegram-bot-token", "telegram-allowed-user",
476
508
  "dry-run", "wait", "watch", "jsonl", "view", "after-event-id", "stop-on-interrupt",
@@ -534,7 +566,7 @@ const requiredValueFlags = new Set([
534
566
  "telegram-bot-token", "telegram-allowed-user", "channel",
535
567
  "tasks", "max-parallel", "batch-id", "poll-interval-ms",
536
568
  "event-page-size", "timeout-seconds", "view", "after-event-id", "limit", "name",
537
- "model-provider", "model", "harness", "mode", "llm-budget-usd",
569
+ "model-provider", "model", "harness", "mode", "allowed-model", "llm-budget-usd",
538
570
  "browser-domain", "browser-max-steps", "browser-step-timeout-seconds",
539
571
  "browser-vision-mode", "browser-viewport", "browser-download-policy",
540
572
  "browser-additional-instructions", "expires", "output", "artifact"
@@ -726,8 +758,7 @@ const commandRun = async (command, flags) => {
726
758
  throw new CliError("conflicting_arguments", "Use either --task or a positional task, not both.");
727
759
  }
728
760
  const task = runTask(command, flags);
729
- if (!task)
730
- throw new Error("--task or a positional task is required.");
761
+ const modelId = flags.model === undefined ? undefined : flagString(flags, "model").trim();
731
762
  if (hasFlag(flags, "wait") && wantsWatch(flags)) {
732
763
  throw new Error("--wait cannot be combined with --watch or --jsonl.");
733
764
  }
@@ -771,7 +802,8 @@ const commandRun = async (command, flags) => {
771
802
  try {
772
803
  payload = await createRun(client, {
773
804
  cwd: cwd(),
774
- instruction: task,
805
+ instruction: task || undefined,
806
+ modelId,
775
807
  inputs: flagList(flags, "input"),
776
808
  externalRunId: flagString(flags, "external-run-id") || undefined,
777
809
  supabaseUserId: supabaseUserId || undefined,
@@ -782,7 +814,7 @@ const commandRun = async (command, flags) => {
782
814
  });
783
815
  }
784
816
  catch (error) {
785
- throw runCreationError(error, client, template);
817
+ throw runCreationError(error, client, template, modelId);
786
818
  }
787
819
  if (wantsWatch(flags)) {
788
820
  const watched = await watchRunWithOutput(client, payload.run.id, flags, payload);
@@ -971,7 +1003,7 @@ const commandModelProviders = async (command, flags) => {
971
1003
  for (const model of payload.models) {
972
1004
  const modelId = String(model.model_id || model.id || "");
973
1005
  const displayName = String(model.display_name || model.name || "");
974
- process.stdout.write(`${modelId}${displayName && displayName !== modelId ? `\t${displayName}` : ""}${model.selectable === undefined ? "" : `\tselectable=${model.selectable}`}\n`);
1006
+ process.stdout.write(`${modelId}${displayName && displayName !== modelId ? `\t${displayName}` : ""}\n`);
975
1007
  }
976
1008
  return;
977
1009
  }
@@ -1183,6 +1215,13 @@ const commandTemplates = async (command, flags) => {
1183
1215
  if (harness !== "opencode" && modeFlag) {
1184
1216
  throw new CliError("opencode_mode_not_supported", "templates create --mode requires --harness opencode.");
1185
1217
  }
1218
+ const additionalAllowedModels = [...new Set(flagList(flags, "allowed-model").map((value) => value.trim()).filter(Boolean))];
1219
+ if (additionalAllowedModels.length > 0 && (harness !== "opencode" || mode !== "computer")) {
1220
+ throw new CliError("opencode_allowed_models_not_supported", "templates create --allowed-model requires an OpenCode Computer template.");
1221
+ }
1222
+ const allowedModelIds = additionalAllowedModels.length > 0
1223
+ ? [...new Set([model, ...additionalAllowedModels])]
1224
+ : undefined;
1186
1225
  const budgetRaw = flagString(flags, "llm-budget-usd");
1187
1226
  const parsedBudgetUsd = budgetRaw ? Number(budgetRaw) : undefined;
1188
1227
  const llmBudgetUsd = parsedBudgetUsd === undefined
@@ -1198,6 +1237,9 @@ const commandTemplates = async (command, flags) => {
1198
1237
  if (harness === "opencode" && mode === "computer" && llmBudgetUsd !== undefined) {
1199
1238
  throw new CliError("opencode_computer_budget_unsupported", "OpenCode Computer templates do not support --llm-budget-usd yet.");
1200
1239
  }
1240
+ if (modelProvider === "custom" && llmBudgetUsd !== undefined) {
1241
+ throw new CliError("custom_provider_budget_unsupported", "Custom OpenAI Proxy providers do not support --llm-budget-usd yet.");
1242
+ }
1201
1243
  const channels = [...new Set(flagList(flags, "channel").map((value) => value.trim()).filter(Boolean))];
1202
1244
  if (channels.some((channel) => channel !== "email" && channel !== "telegram")) {
1203
1245
  throw new CliError("invalid_hermes_channels", "--channel must be email or telegram.");
@@ -1218,8 +1260,9 @@ const commandTemplates = async (command, flags) => {
1218
1260
  modelProvider !== "openai" &&
1219
1261
  modelProvider !== "anthropic" &&
1220
1262
  modelProvider !== "google" &&
1221
- modelProvider !== "hetzner") {
1222
- throw new CliError("browser_use_provider_unsupported", "Browser Use templates require --model-provider openai, anthropic, google, or hetzner.");
1263
+ modelProvider !== "hetzner" &&
1264
+ modelProvider !== "custom") {
1265
+ throw new CliError("browser_use_provider_unsupported", "Browser Use templates require --model-provider openai, anthropic, google, hetzner, or custom.");
1223
1266
  }
1224
1267
  const browserDomains = harness === "browser-use"
1225
1268
  ? parseBrowserDomains(flagList(flags, "browser-domain"))
@@ -1236,6 +1279,7 @@ const commandTemplates = async (command, flags) => {
1236
1279
  name,
1237
1280
  provider_id: modelProvider,
1238
1281
  model_id: model,
1282
+ ...(allowedModelIds ? { allowed_model_ids: allowedModelIds } : {}),
1239
1283
  ...(harness === "opencode" && modeFlag ? { mode: mode } : {}),
1240
1284
  ...(llmBudgetUsd === undefined ? {} : { llm_budget_usd: llmBudgetUsd }),
1241
1285
  ...(harness === "browser-use" ? {
package/dist/runs.js CHANGED
@@ -52,7 +52,8 @@ export const createRun = async (client, options) => {
52
52
  return client.createRun({
53
53
  external_run_id: options.externalRunId,
54
54
  workload_id: options.templateId,
55
- instruction: options.instruction,
55
+ ...(options.modelId !== undefined ? { model_id: options.modelId } : {}),
56
+ ...(options.instruction ? { instruction: options.instruction } : {}),
56
57
  ...(options.supabaseUserId ? { supabase_user_id: options.supabaseUserId } : {}),
57
58
  ...(options.emailAddress ? { email_address: options.emailAddress } : {}),
58
59
  ...(options.telegramBotToken ? {
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = "0.0.12";
1
+ export const version = "0.0.14";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanlabs/sanbox-cli",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",