@sanlabs/sanbox-cli 0.0.13 → 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,11 @@ installed_cli_version="$(sanbox --version)"
15
15
  test "$installed_cli_version" = "$latest_cli_version"
16
16
  ```
17
17
 
18
- Always use the latest published CLI. CLI 0.0.13 allows service templates to start without an
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
19
23
  initial task, including OpenCode Computers that are ready for later SDK steering, and supports
20
24
  Custom OpenAI Proxy providers when creating templates, including Browser Use templates.
21
25
  CLI 0.0.12 adopted the Stop, Resume, and Delete lifecycle,
@@ -99,9 +103,31 @@ sanbox templates create \
99
103
  --harness opencode \
100
104
  --mode computer \
101
105
  --model-provider openai \
102
- --model '<model-id>'
106
+ --model '<default-model-id>' \
107
+ --allowed-model '<additional-model-id>'
103
108
  ```
104
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
+
105
131
  Existing templates keep their current names, IDs, slugs, and task behavior. Browser Use is for one-shot
106
132
  web tasks whose target domains are already approved. Hermes service templates are always-on. A Hermes template
107
133
  declares which optional channels its runs may activate:
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,11 +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]
47
- sanbox run --template <service-template-id> [--email-address <address>] [--telegram-bot-token <token>] [--telegram-allowed-user <id>] [--json]
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]
48
48
  sanbox batch --tasks tasks.json --template <template-id> [--input <path>] [--max-parallel 5] [--wait] [--json]
49
49
  sanbox runs list [--limit 50] [--json]
50
50
  sanbox runs get <run-id> [--json]
@@ -95,11 +95,14 @@ Usage:
95
95
  sanbox run "Review these files" --template <template-id> --input report.pdf --input data/ --wait
96
96
  sanbox run --task "Review these files" --template <template-id> --input report.pdf --wait --json
97
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
98
100
 
99
101
  Options:
100
102
  --task <instruction> Required for task templates; optional for service templates.
101
103
  --input <path> File, directory, or glob to upload. Repeatable.
102
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.
103
106
  --external-run-id <id> Idempotency key for retries.
104
107
  --supabase-user-id <id> Supabase Auth user UUID expected during OAuth consent.
105
108
  --email-address <address> Optional for Hermes computers; must use a listed AgentMail domain.
@@ -113,6 +116,10 @@ Options:
113
116
  --after-event-id <id> Resume after an event cursor. Default: 0.
114
117
  --stop-on-interrupt Request Stop when Ctrl-C is pressed.
115
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.
116
123
  `;
117
124
  const runsSupabaseHelp = `Sanbox Supabase run authorization
118
125
 
@@ -163,13 +170,15 @@ Usage:
163
170
  sanbox templates list [--json]
164
171
  sanbox templates get <template-id> [--json]
165
172
  sanbox templates validate <template-id> [--json]
166
- 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]
167
174
  sanbox templates delete <template-id> --force [--json]
168
175
 
169
176
  Template creation requires an exact provider id and that provider's exact model id.
170
177
  LiteLLM budgets are optional USD amounts and apply separately to each run.
171
178
  OpenCode defaults to --mode task. Use --mode computer for a retained private OpenCode server;
172
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.
173
182
  Hermes templates are always-on computers. Use repeatable --channel email|telegram to select which
174
183
  channels runs may activate. Email and Telegram credentials are supplied only when creating a run.
175
184
  Browser Use templates run local headless Chromium inside a one-shot sandbox. They require OpenAI
@@ -216,6 +225,13 @@ a Hermes service template for a waited task because it is always-on. Select auto
216
225
  template qualifies; otherwise ask the user. Provider credentials and template administration are
217
226
  console-only.
218
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
+
219
235
  Use --json for request/response commands and --jsonl for streams. Parse the versioned envelope:
220
236
  schema_version, ok, command, context, data or error, and next_actions. Execute command actions as
221
237
  argv arrays, never shell strings. Exit 0 means command success, 1 local/API failure, 2 readiness or
@@ -316,7 +332,19 @@ const providerReadinessCodes = new Set([
316
332
  "model_not_found_for_provider",
317
333
  "template_provider_missing"
318
334
  ]);
319
- 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
+ }
320
348
  if (!(error instanceof SanboxApiError) || !runReadinessCodes.has(error.code))
321
349
  return error;
322
350
  const nextActions = [
@@ -348,7 +376,7 @@ const templateCreationError = (error, client, providerId, modelId) => {
348
376
  return new CliError(error.code, error.message, {
349
377
  status: error.status,
350
378
  details: { provider_id: providerId, model_id: modelId },
351
- 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.")]
352
380
  });
353
381
  }
354
382
  if (error.code === "template_name_conflict") {
@@ -461,6 +489,7 @@ const flagSets = {
461
489
  "model",
462
490
  "harness",
463
491
  "mode",
492
+ "allowed-model",
464
493
  "llm-budget-usd",
465
494
  "web-access",
466
495
  "channel",
@@ -473,7 +502,7 @@ const flagSets = {
473
502
  "browser-additional-instructions"
474
503
  ],
475
504
  run: [
476
- ...commonFlags, "task", "input", "template", "external-run-id", "email-address",
505
+ ...commonFlags, "task", "input", "template", "model", "external-run-id", "email-address",
477
506
  "supabase-user-id",
478
507
  "telegram-bot-token", "telegram-allowed-user",
479
508
  "dry-run", "wait", "watch", "jsonl", "view", "after-event-id", "stop-on-interrupt",
@@ -537,7 +566,7 @@ const requiredValueFlags = new Set([
537
566
  "telegram-bot-token", "telegram-allowed-user", "channel",
538
567
  "tasks", "max-parallel", "batch-id", "poll-interval-ms",
539
568
  "event-page-size", "timeout-seconds", "view", "after-event-id", "limit", "name",
540
- "model-provider", "model", "harness", "mode", "llm-budget-usd",
569
+ "model-provider", "model", "harness", "mode", "allowed-model", "llm-budget-usd",
541
570
  "browser-domain", "browser-max-steps", "browser-step-timeout-seconds",
542
571
  "browser-vision-mode", "browser-viewport", "browser-download-policy",
543
572
  "browser-additional-instructions", "expires", "output", "artifact"
@@ -729,6 +758,7 @@ const commandRun = async (command, flags) => {
729
758
  throw new CliError("conflicting_arguments", "Use either --task or a positional task, not both.");
730
759
  }
731
760
  const task = runTask(command, flags);
761
+ const modelId = flags.model === undefined ? undefined : flagString(flags, "model").trim();
732
762
  if (hasFlag(flags, "wait") && wantsWatch(flags)) {
733
763
  throw new Error("--wait cannot be combined with --watch or --jsonl.");
734
764
  }
@@ -773,6 +803,7 @@ const commandRun = async (command, flags) => {
773
803
  payload = await createRun(client, {
774
804
  cwd: cwd(),
775
805
  instruction: task || undefined,
806
+ modelId,
776
807
  inputs: flagList(flags, "input"),
777
808
  externalRunId: flagString(flags, "external-run-id") || undefined,
778
809
  supabaseUserId: supabaseUserId || undefined,
@@ -783,7 +814,7 @@ const commandRun = async (command, flags) => {
783
814
  });
784
815
  }
785
816
  catch (error) {
786
- throw runCreationError(error, client, template);
817
+ throw runCreationError(error, client, template, modelId);
787
818
  }
788
819
  if (wantsWatch(flags)) {
789
820
  const watched = await watchRunWithOutput(client, payload.run.id, flags, payload);
@@ -972,7 +1003,7 @@ const commandModelProviders = async (command, flags) => {
972
1003
  for (const model of payload.models) {
973
1004
  const modelId = String(model.model_id || model.id || "");
974
1005
  const displayName = String(model.display_name || model.name || "");
975
- 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`);
976
1007
  }
977
1008
  return;
978
1009
  }
@@ -1184,6 +1215,13 @@ const commandTemplates = async (command, flags) => {
1184
1215
  if (harness !== "opencode" && modeFlag) {
1185
1216
  throw new CliError("opencode_mode_not_supported", "templates create --mode requires --harness opencode.");
1186
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;
1187
1225
  const budgetRaw = flagString(flags, "llm-budget-usd");
1188
1226
  const parsedBudgetUsd = budgetRaw ? Number(budgetRaw) : undefined;
1189
1227
  const llmBudgetUsd = parsedBudgetUsd === undefined
@@ -1241,6 +1279,7 @@ const commandTemplates = async (command, flags) => {
1241
1279
  name,
1242
1280
  provider_id: modelProvider,
1243
1281
  model_id: model,
1282
+ ...(allowedModelIds ? { allowed_model_ids: allowedModelIds } : {}),
1244
1283
  ...(harness === "opencode" && modeFlag ? { mode: mode } : {}),
1245
1284
  ...(llmBudgetUsd === undefined ? {} : { llm_budget_usd: llmBudgetUsd }),
1246
1285
  ...(harness === "browser-use" ? {
package/dist/runs.js CHANGED
@@ -52,6 +52,7 @@ 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
+ ...(options.modelId !== undefined ? { model_id: options.modelId } : {}),
55
56
  ...(options.instruction ? { instruction: options.instruction } : {}),
56
57
  ...(options.supabaseUserId ? { supabase_user_id: options.supabaseUserId } : {}),
57
58
  ...(options.emailAddress ? { email_address: options.emailAddress } : {}),
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = "0.0.13";
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.13",
3
+ "version": "0.0.14",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",