opensandbox-cli 1.1.0__tar.gz → 1.1.2.dev20261020__tar.gz

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.
Files changed (33) hide show
  1. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/PKG-INFO +102 -27
  2. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/README.md +99 -25
  3. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/pyproject.toml +2 -1
  4. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/src/opensandbox_cli/client.py +0 -5
  5. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/src/opensandbox_cli/commands/command.py +47 -61
  6. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/src/opensandbox_cli/commands/config_cmd.py +46 -57
  7. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/src/opensandbox_cli/commands/devops.py +0 -9
  8. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/src/opensandbox_cli/commands/diagnostics.py +19 -10
  9. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/src/opensandbox_cli/commands/file.py +0 -24
  10. opensandbox_cli-1.1.2.dev20261020/src/opensandbox_cli/commands/sandbox.py +1057 -0
  11. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/src/opensandbox_cli/commands/skills.py +6 -6
  12. opensandbox_cli-1.1.2.dev20261020/src/opensandbox_cli/commands/snapshot.py +170 -0
  13. opensandbox_cli-1.1.2.dev20261020/src/opensandbox_cli/commands/template.py +324 -0
  14. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/src/opensandbox_cli/config.py +48 -15
  15. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/src/opensandbox_cli/main.py +42 -5
  16. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/src/opensandbox_cli/output.py +49 -87
  17. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/src/opensandbox_cli/skill_registry.py +6 -4
  18. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/src/opensandbox_cli/skills/opensandbox-command-execution.md +5 -7
  19. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/src/opensandbox_cli/skills/opensandbox-credential-vault.md +8 -19
  20. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/src/opensandbox_cli/skills/opensandbox-file-operations.md +4 -8
  21. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/src/opensandbox_cli/skills/opensandbox-network-egress.md +2 -4
  22. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/src/opensandbox_cli/skills/opensandbox-sandbox-lifecycle.md +61 -2
  23. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/src/opensandbox_cli/skills/opensandbox-sandbox-troubleshooting.md +6 -0
  24. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/src/opensandbox_cli/utils.py +37 -40
  25. opensandbox_cli-1.1.0/src/opensandbox_cli/commands/sandbox.py +0 -594
  26. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/.gitignore +0 -0
  27. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/LICENSE +0 -0
  28. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/src/opensandbox_cli/__init__.py +0 -0
  29. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/src/opensandbox_cli/__main__.py +0 -0
  30. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/src/opensandbox_cli/commands/__init__.py +0 -0
  31. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/src/opensandbox_cli/commands/credential_vault.py +0 -0
  32. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/src/opensandbox_cli/commands/egress.py +0 -0
  33. {opensandbox_cli-1.1.0 → opensandbox_cli-1.1.2.dev20261020}/src/opensandbox_cli/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: opensandbox-cli
3
- Version: 1.1.0
3
+ Version: 1.1.2.dev20261020
4
4
  Summary: OpenSandbox CLI - Command-line interface for managing sandboxes
5
5
  Project-URL: Homepage, https://open-sandbox.ai
6
6
  Project-URL: Repository, https://github.com/opensandbox-group/OpenSandbox
@@ -224,10 +224,11 @@ Classifier: Topic :: Software Development :: Libraries
224
224
  Classifier: Typing :: Typed
225
225
  Requires-Python: >=3.10
226
226
  Requires-Dist: click<9.0,>=8.1.0
227
- Requires-Dist: opensandbox<2.0.0,>=1.1.0
227
+ Requires-Dist: opensandbox<2.0.0,>=1.1.1
228
228
  Requires-Dist: pyyaml<7.0,>=6.0
229
229
  Requires-Dist: rich<14.0,>=13.0.0
230
230
  Requires-Dist: tomli>=2.0.0; python_version < '3.11'
231
+ Requires-Dist: tomlkit<1.0,>=0.13.0
231
232
  Description-Content-Type: text/markdown
232
233
 
233
234
  # OpenSandbox CLI
@@ -235,6 +236,7 @@ Description-Content-Type: text/markdown
235
236
  `osb` is the command-line interface for OpenSandbox. It is built for the common day-to-day flows:
236
237
 
237
238
  - create and manage sandboxes
239
+ - build golden-image templates and create sandboxes from templates or snapshots
238
240
  - run commands inside a sandbox
239
241
  - read and modify sandbox files
240
242
  - inspect runtime egress policy
@@ -387,6 +389,84 @@ Create with Credential Vault proxy enabled:
387
389
  osb sandbox create --image python:3.12 --network-policy-file network-policy.json --credential-proxy -o json
388
390
  ```
389
391
 
392
+ Create from a template (golden image):
393
+
394
+ ```bash
395
+ osb sandbox create --template <template-id> --timeout 30m -o json
396
+ ```
397
+
398
+ Template mode fixes the workload shape on the server: `--image`, `--env`, `--resource`, `--entrypoint`, `--volumes-file`, and `--credential-proxy` cannot be combined with `--template`. Only `--timeout` (required), `--metadata`, `--extension`, and `--network-policy-file` may accompany it. Only templates in the `Succeeded` phase can be used. See [Manage templates](#manage-templates).
399
+
400
+ Create from a snapshot:
401
+
402
+ ```bash
403
+ osb sandbox create --snapshot-id <snapshot-id> --timeout 30m -o json
404
+ ```
405
+
406
+ `--image`, `--template`, and `--snapshot-id` are mutually exclusive.
407
+
408
+ Create from a request file (`-f`, like `kubectl apply -f`):
409
+
410
+ ```bash
411
+ osb sandbox create -f sandbox-request.json -o json
412
+ ```
413
+
414
+ `sandbox-request.json` uses the public `CreateSandboxRequest` JSON format (camelCase keys):
415
+
416
+ ```json
417
+ {
418
+ "image": "python:3.12",
419
+ "timeout": 1800,
420
+ "resourceLimits": { "cpu": "1", "memory": "2Gi" },
421
+ "entrypoint": ["python", "-m", "http.server"],
422
+ "networkPolicy": {
423
+ "defaultAction": "deny",
424
+ "egress": [{ "action": "allow", "target": "pypi.org" }]
425
+ }
426
+ }
427
+ ```
428
+
429
+ - Exactly one of `image` (string or `{ "uri": ..., "auth": { "username": ..., "password": ... } }`), `templateId`, or `snapshotId` is required; `timeout` is integer seconds, or `null` for manual cleanup.
430
+ - Supported fields: `image`, `templateId`, `snapshotId`, `platform`, `timeout`, `resourceLimits`, `resourceRequests`, `env`, `metadata`, `extensions`, `entrypoint`, `networkPolicy`, `credentialProxy`, `secureAccess`, `volumes`, `lifecycle`.
431
+ - `-f` cannot be combined with request-building flags; `--skip-health-check`, `--ready-timeout`, and `-o` still apply. In template mode the file may only set `timeout`, `metadata`, `extensions`, and `networkPolicy` alongside `templateId` (a finite `timeout` is required).
432
+
433
+ ### Manage templates
434
+
435
+ Templates are golden-image builds; the build runs asynchronously, so poll `template get` until the status phase is `Succeeded`. Template management requires a Kubernetes-backed runtime.
436
+
437
+ ```bash
438
+ osb template create --image python:3.12 --publish s3://bucket/publish -o json
439
+ osb template get <template-id> -o json
440
+ osb template list -o json
441
+ osb template delete <template-id> -o json
442
+ ```
443
+
444
+ `template create` also accepts `--resource cpu=1 memory=512Mi disk=2Gi`, `--entrypoint` (repeat per argv item), `--env KEY=VALUE`, `--metadata KEY=VALUE`, `--format native|overlaybd`, `--readiness-probe`, and `--warmup-seconds`. Or pass a request file in the public `CreateTemplateRequest` JSON format:
445
+
446
+ ```bash
447
+ osb template create -f template-request.json -o json
448
+ ```
449
+
450
+ ```json
451
+ {
452
+ "image": "python:3.12",
453
+ "publish": "s3://bucket/publish",
454
+ "resourceLimits": { "cpu": "1", "memory": "512Mi" },
455
+ "readiness": { "probe": "tcp://127.0.0.1:44772", "warmupSeconds": 60 }
456
+ }
457
+ ```
458
+
459
+ ### Manage snapshots
460
+
461
+ Snapshots capture a sandbox's state and can back new sandboxes.
462
+
463
+ ```bash
464
+ osb snapshot create <sandbox-id> --name golden -o json
465
+ osb snapshot get <snapshot-id> -o json
466
+ osb snapshot list --sandbox-id <sandbox-id> -o json
467
+ osb snapshot delete <snapshot-id> -o json
468
+ ```
469
+
390
470
  ### List and inspect sandboxes
391
471
 
392
472
  ```bash
@@ -400,9 +480,7 @@ osb sandbox metrics <sandbox-id> --watch -o raw
400
480
 
401
481
  ### Pause a sandbox
402
482
 
403
- Pause is asynchronous. `Pause request accepted` confirms that the server accepted
404
- the request, not that the sandbox has reached the `Paused` state. Poll the sandbox
405
- until the transition finishes:
483
+ Pause is asynchronous. `Pause request accepted` confirms that the server accepted the request, not that the sandbox has reached the `Paused` state. Poll the sandbox until the transition finishes:
406
484
 
407
485
  ```bash
408
486
  osb sandbox pause <sandbox-id>
@@ -431,12 +509,9 @@ osb command status <sandbox-id> <execution-id> -o json
431
509
  osb command logs <sandbox-id> <execution-id> -o json
432
510
  ```
433
511
 
434
- By default the payload after `--` is joined into one shell command string, so
435
- pipelines, redirection, and `$VAR` expansion work as in a terminal. Add `--argv`
436
- to pass the arguments to the executable as a literal argv list (no shell) when
437
- values such as `$HOME`, quotes, embedded spaces, or empty strings must reach the
438
- process unchanged. `--argv` needs a sandbox image whose execd accepts argv
439
- requests:
512
+ Background start errors or a stream ending before startup confirmation make the CLI exit non-zero without success metadata. After a successful start, use `command status` and `command logs` to check the process's eventual result.
513
+
514
+ By default the payload after `--` is joined into one shell command string, so pipelines, redirection, and `$VAR` expansion work as in a terminal. Add `--argv` to pass the arguments to the executable as a literal argv list (no shell) when values such as `$HOME`, quotes, embedded spaces, or empty strings must reach the process unchanged. `--argv` needs a sandbox image whose execd accepts argv requests:
440
515
 
441
516
  ```bash
442
517
  osb command run <sandbox-id> -o raw --argv -- python3 -c "import sys; print(sys.argv[1:])" "a b" '$HOME' "x'y" ""
@@ -463,10 +538,7 @@ osb file replace <sandbox-id> /workspace/app.py --old old --new new -o json
463
538
  osb file chmod <sandbox-id> /workspace/script.sh --mode 755 -o json
464
539
  ```
465
540
 
466
- Downloads replace regular files only on success; a failed or interrupted
467
- download preserves any existing regular file. Existing devices and named pipes
468
- receive data directly. Stdout aliases (such as `/dev/stdout`) stream only the file
469
- bytes, without a success message. See the [CLI guide](../docs/cli/index.md#work-with-files).
541
+ Downloads replace regular files only on success; a failed or interrupted download preserves any existing regular file. Existing devices and named pipes receive data directly. Stdout aliases (such as `/dev/stdout`) stream only the file bytes, without a success message. See the [CLI guide](../docs/cli/index.md#work-with-files).
470
542
 
471
543
  ### Manage runtime egress policy
472
544
 
@@ -490,9 +562,7 @@ osb command run <sandbox-id> -o raw -- curl -I https://pypi.org
490
562
 
491
563
  ### Manage Credential Vault
492
564
 
493
- Credential Vault operations call the sandbox egress sidecar through the Python SDK.
494
- Create the sandbox with `--credential-proxy` and an explicit network policy before
495
- writing vault state.
565
+ Credential Vault operations call the sandbox egress sidecar through the Python SDK. Create the sandbox with `--credential-proxy` and an explicit network policy before writing vault state.
496
566
 
497
567
  ```bash
498
568
  osb credential-vault create <sandbox-id> --file vault.yaml -o json
@@ -503,14 +573,14 @@ osb credential-vault binding list <sandbox-id> -o json
503
573
  osb credential-vault delete <sandbox-id> -o json
504
574
  ```
505
575
 
506
- Use `--file -` to read a JSON/YAML payload from stdin. Do not pass plaintext
507
- credential values as command-line flags; keep them in the payload stream or file.
576
+ Use `--file -` to read a JSON/YAML payload from stdin. Do not pass plaintext credential values as command-line flags; keep them in the payload stream or file.
508
577
 
509
578
  ### Collect diagnostics
510
579
 
511
- Use the stable diagnostics commands for API-backed log and event descriptors.
580
+ Use context to identify the runtime backend, then collect log and event descriptors.
512
581
 
513
582
  ```bash
583
+ osb diagnostics context <sandbox-id> -o json
514
584
  osb diagnostics events <sandbox-id> --scope runtime -o raw
515
585
  osb diagnostics events <sandbox-id> --scope all -o raw
516
586
  osb diagnostics logs <sandbox-id> --scope container -o raw
@@ -519,7 +589,9 @@ osb diagnostics events <sandbox-id> --scope runtime -o json
519
589
  osb diagnostics logs <sandbox-id> --scope container -o yaml
520
590
  ```
521
591
 
522
- `--scope` is required for stable diagnostics. The built-in server supports
592
+ `context` returns backend routing information without connecting to execd and
593
+ supports table, JSON, and YAML output. `--scope` is required for logs/events.
594
+ The built-in server supports
523
595
  `container` and `all` for logs, and `runtime` and `all` for events. It returns
524
596
  `DIAGNOSTICS_SCOPE_UNSUPPORTED` for unavailable scopes, including lifecycle events.
525
597
  Best-effort scopes may include a `warnings` field when the backend can only
@@ -529,8 +601,7 @@ temporary URL. Structured CLI output follows the SDK/Python field style, for
529
601
  example `content_url`, `content_length`, and `expires_at`. Older server builds
530
602
  may still return `DIAGNOSTICS_NOT_IMPLEMENTED` for scoped diagnostics.
531
603
 
532
- Legacy DevOps diagnostics remain experimental. Prefer `osb diagnostics logs/events`
533
- for stable API-backed log and event collection.
604
+ Legacy DevOps diagnostics remain experimental. Prefer `osb diagnostics logs/events` for stable API-backed log and event collection.
534
605
 
535
606
  ```bash
536
607
  osb devops inspect <sandbox-id> -o raw
@@ -561,6 +632,8 @@ Not every command supports every format. Use `--help` on the specific command wh
561
632
  The main command groups are:
562
633
 
563
634
  - `osb sandbox`: lifecycle management
635
+ - `osb template`: golden-image template builds
636
+ - `osb snapshot`: snapshot management
564
637
  - `osb command`: command execution and persistent sessions
565
638
  - `osb file`: file and directory operations
566
639
  - `osb egress`: runtime egress policy
@@ -573,6 +646,8 @@ Explore them directly:
573
646
 
574
647
  ```bash
575
648
  osb sandbox --help
649
+ osb template --help
650
+ osb snapshot --help
576
651
  osb command --help
577
652
  osb file --help
578
653
  osb skills --help
@@ -594,12 +669,12 @@ Supported targets:
594
669
 
595
670
  | Target | Install location |
596
671
  | --- | --- |
597
- | `claude` | `./.claude/skills/` or `~/.claude/skills/` |
672
+ | `claude` | `./.claude/skills/<name>/SKILL.md` or `~/.claude/skills/<name>/SKILL.md` |
598
673
  | `cursor` | `./.cursor/rules/` or `~/.cursor/rules/` |
599
674
  | `codex` | `./.codex/skills/<name>/SKILL.md` or `~/.codex/skills/<name>/SKILL.md` |
600
- | `copilot` | `./.github/copilot-instructions.md` or `~/.github/copilot-instructions.md` |
675
+ | `copilot` | `./.github/copilot-instructions.md` or `~/.copilot/copilot-instructions.md` |
601
676
  | `windsurf` | `./.windsurfrules` or `~/.windsurfrules` |
602
- | `cline` | `./.clinerules` or `~/.clinerules` |
677
+ | `cline` | `./.clinerules` or `~/.cline/rules/opensandbox-<name>.md` |
603
678
  | `opencode` | `./.agents/skills/<name>/SKILL.md` or `~/.agents/skills/<name>/SKILL.md` |
604
679
 
605
680
  Common flows:
@@ -3,6 +3,7 @@
3
3
  `osb` is the command-line interface for OpenSandbox. It is built for the common day-to-day flows:
4
4
 
5
5
  - create and manage sandboxes
6
+ - build golden-image templates and create sandboxes from templates or snapshots
6
7
  - run commands inside a sandbox
7
8
  - read and modify sandbox files
8
9
  - inspect runtime egress policy
@@ -155,6 +156,84 @@ Create with Credential Vault proxy enabled:
155
156
  osb sandbox create --image python:3.12 --network-policy-file network-policy.json --credential-proxy -o json
156
157
  ```
157
158
 
159
+ Create from a template (golden image):
160
+
161
+ ```bash
162
+ osb sandbox create --template <template-id> --timeout 30m -o json
163
+ ```
164
+
165
+ Template mode fixes the workload shape on the server: `--image`, `--env`, `--resource`, `--entrypoint`, `--volumes-file`, and `--credential-proxy` cannot be combined with `--template`. Only `--timeout` (required), `--metadata`, `--extension`, and `--network-policy-file` may accompany it. Only templates in the `Succeeded` phase can be used. See [Manage templates](#manage-templates).
166
+
167
+ Create from a snapshot:
168
+
169
+ ```bash
170
+ osb sandbox create --snapshot-id <snapshot-id> --timeout 30m -o json
171
+ ```
172
+
173
+ `--image`, `--template`, and `--snapshot-id` are mutually exclusive.
174
+
175
+ Create from a request file (`-f`, like `kubectl apply -f`):
176
+
177
+ ```bash
178
+ osb sandbox create -f sandbox-request.json -o json
179
+ ```
180
+
181
+ `sandbox-request.json` uses the public `CreateSandboxRequest` JSON format (camelCase keys):
182
+
183
+ ```json
184
+ {
185
+ "image": "python:3.12",
186
+ "timeout": 1800,
187
+ "resourceLimits": { "cpu": "1", "memory": "2Gi" },
188
+ "entrypoint": ["python", "-m", "http.server"],
189
+ "networkPolicy": {
190
+ "defaultAction": "deny",
191
+ "egress": [{ "action": "allow", "target": "pypi.org" }]
192
+ }
193
+ }
194
+ ```
195
+
196
+ - Exactly one of `image` (string or `{ "uri": ..., "auth": { "username": ..., "password": ... } }`), `templateId`, or `snapshotId` is required; `timeout` is integer seconds, or `null` for manual cleanup.
197
+ - Supported fields: `image`, `templateId`, `snapshotId`, `platform`, `timeout`, `resourceLimits`, `resourceRequests`, `env`, `metadata`, `extensions`, `entrypoint`, `networkPolicy`, `credentialProxy`, `secureAccess`, `volumes`, `lifecycle`.
198
+ - `-f` cannot be combined with request-building flags; `--skip-health-check`, `--ready-timeout`, and `-o` still apply. In template mode the file may only set `timeout`, `metadata`, `extensions`, and `networkPolicy` alongside `templateId` (a finite `timeout` is required).
199
+
200
+ ### Manage templates
201
+
202
+ Templates are golden-image builds; the build runs asynchronously, so poll `template get` until the status phase is `Succeeded`. Template management requires a Kubernetes-backed runtime.
203
+
204
+ ```bash
205
+ osb template create --image python:3.12 --publish s3://bucket/publish -o json
206
+ osb template get <template-id> -o json
207
+ osb template list -o json
208
+ osb template delete <template-id> -o json
209
+ ```
210
+
211
+ `template create` also accepts `--resource cpu=1 memory=512Mi disk=2Gi`, `--entrypoint` (repeat per argv item), `--env KEY=VALUE`, `--metadata KEY=VALUE`, `--format native|overlaybd`, `--readiness-probe`, and `--warmup-seconds`. Or pass a request file in the public `CreateTemplateRequest` JSON format:
212
+
213
+ ```bash
214
+ osb template create -f template-request.json -o json
215
+ ```
216
+
217
+ ```json
218
+ {
219
+ "image": "python:3.12",
220
+ "publish": "s3://bucket/publish",
221
+ "resourceLimits": { "cpu": "1", "memory": "512Mi" },
222
+ "readiness": { "probe": "tcp://127.0.0.1:44772", "warmupSeconds": 60 }
223
+ }
224
+ ```
225
+
226
+ ### Manage snapshots
227
+
228
+ Snapshots capture a sandbox's state and can back new sandboxes.
229
+
230
+ ```bash
231
+ osb snapshot create <sandbox-id> --name golden -o json
232
+ osb snapshot get <snapshot-id> -o json
233
+ osb snapshot list --sandbox-id <sandbox-id> -o json
234
+ osb snapshot delete <snapshot-id> -o json
235
+ ```
236
+
158
237
  ### List and inspect sandboxes
159
238
 
160
239
  ```bash
@@ -168,9 +247,7 @@ osb sandbox metrics <sandbox-id> --watch -o raw
168
247
 
169
248
  ### Pause a sandbox
170
249
 
171
- Pause is asynchronous. `Pause request accepted` confirms that the server accepted
172
- the request, not that the sandbox has reached the `Paused` state. Poll the sandbox
173
- until the transition finishes:
250
+ Pause is asynchronous. `Pause request accepted` confirms that the server accepted the request, not that the sandbox has reached the `Paused` state. Poll the sandbox until the transition finishes:
174
251
 
175
252
  ```bash
176
253
  osb sandbox pause <sandbox-id>
@@ -199,12 +276,9 @@ osb command status <sandbox-id> <execution-id> -o json
199
276
  osb command logs <sandbox-id> <execution-id> -o json
200
277
  ```
201
278
 
202
- By default the payload after `--` is joined into one shell command string, so
203
- pipelines, redirection, and `$VAR` expansion work as in a terminal. Add `--argv`
204
- to pass the arguments to the executable as a literal argv list (no shell) when
205
- values such as `$HOME`, quotes, embedded spaces, or empty strings must reach the
206
- process unchanged. `--argv` needs a sandbox image whose execd accepts argv
207
- requests:
279
+ Background start errors or a stream ending before startup confirmation make the CLI exit non-zero without success metadata. After a successful start, use `command status` and `command logs` to check the process's eventual result.
280
+
281
+ By default the payload after `--` is joined into one shell command string, so pipelines, redirection, and `$VAR` expansion work as in a terminal. Add `--argv` to pass the arguments to the executable as a literal argv list (no shell) when values such as `$HOME`, quotes, embedded spaces, or empty strings must reach the process unchanged. `--argv` needs a sandbox image whose execd accepts argv requests:
208
282
 
209
283
  ```bash
210
284
  osb command run <sandbox-id> -o raw --argv -- python3 -c "import sys; print(sys.argv[1:])" "a b" '$HOME' "x'y" ""
@@ -231,10 +305,7 @@ osb file replace <sandbox-id> /workspace/app.py --old old --new new -o json
231
305
  osb file chmod <sandbox-id> /workspace/script.sh --mode 755 -o json
232
306
  ```
233
307
 
234
- Downloads replace regular files only on success; a failed or interrupted
235
- download preserves any existing regular file. Existing devices and named pipes
236
- receive data directly. Stdout aliases (such as `/dev/stdout`) stream only the file
237
- bytes, without a success message. See the [CLI guide](../docs/cli/index.md#work-with-files).
308
+ Downloads replace regular files only on success; a failed or interrupted download preserves any existing regular file. Existing devices and named pipes receive data directly. Stdout aliases (such as `/dev/stdout`) stream only the file bytes, without a success message. See the [CLI guide](../docs/cli/index.md#work-with-files).
238
309
 
239
310
  ### Manage runtime egress policy
240
311
 
@@ -258,9 +329,7 @@ osb command run <sandbox-id> -o raw -- curl -I https://pypi.org
258
329
 
259
330
  ### Manage Credential Vault
260
331
 
261
- Credential Vault operations call the sandbox egress sidecar through the Python SDK.
262
- Create the sandbox with `--credential-proxy` and an explicit network policy before
263
- writing vault state.
332
+ Credential Vault operations call the sandbox egress sidecar through the Python SDK. Create the sandbox with `--credential-proxy` and an explicit network policy before writing vault state.
264
333
 
265
334
  ```bash
266
335
  osb credential-vault create <sandbox-id> --file vault.yaml -o json
@@ -271,14 +340,14 @@ osb credential-vault binding list <sandbox-id> -o json
271
340
  osb credential-vault delete <sandbox-id> -o json
272
341
  ```
273
342
 
274
- Use `--file -` to read a JSON/YAML payload from stdin. Do not pass plaintext
275
- credential values as command-line flags; keep them in the payload stream or file.
343
+ Use `--file -` to read a JSON/YAML payload from stdin. Do not pass plaintext credential values as command-line flags; keep them in the payload stream or file.
276
344
 
277
345
  ### Collect diagnostics
278
346
 
279
- Use the stable diagnostics commands for API-backed log and event descriptors.
347
+ Use context to identify the runtime backend, then collect log and event descriptors.
280
348
 
281
349
  ```bash
350
+ osb diagnostics context <sandbox-id> -o json
282
351
  osb diagnostics events <sandbox-id> --scope runtime -o raw
283
352
  osb diagnostics events <sandbox-id> --scope all -o raw
284
353
  osb diagnostics logs <sandbox-id> --scope container -o raw
@@ -287,7 +356,9 @@ osb diagnostics events <sandbox-id> --scope runtime -o json
287
356
  osb diagnostics logs <sandbox-id> --scope container -o yaml
288
357
  ```
289
358
 
290
- `--scope` is required for stable diagnostics. The built-in server supports
359
+ `context` returns backend routing information without connecting to execd and
360
+ supports table, JSON, and YAML output. `--scope` is required for logs/events.
361
+ The built-in server supports
291
362
  `container` and `all` for logs, and `runtime` and `all` for events. It returns
292
363
  `DIAGNOSTICS_SCOPE_UNSUPPORTED` for unavailable scopes, including lifecycle events.
293
364
  Best-effort scopes may include a `warnings` field when the backend can only
@@ -297,8 +368,7 @@ temporary URL. Structured CLI output follows the SDK/Python field style, for
297
368
  example `content_url`, `content_length`, and `expires_at`. Older server builds
298
369
  may still return `DIAGNOSTICS_NOT_IMPLEMENTED` for scoped diagnostics.
299
370
 
300
- Legacy DevOps diagnostics remain experimental. Prefer `osb diagnostics logs/events`
301
- for stable API-backed log and event collection.
371
+ Legacy DevOps diagnostics remain experimental. Prefer `osb diagnostics logs/events` for stable API-backed log and event collection.
302
372
 
303
373
  ```bash
304
374
  osb devops inspect <sandbox-id> -o raw
@@ -329,6 +399,8 @@ Not every command supports every format. Use `--help` on the specific command wh
329
399
  The main command groups are:
330
400
 
331
401
  - `osb sandbox`: lifecycle management
402
+ - `osb template`: golden-image template builds
403
+ - `osb snapshot`: snapshot management
332
404
  - `osb command`: command execution and persistent sessions
333
405
  - `osb file`: file and directory operations
334
406
  - `osb egress`: runtime egress policy
@@ -341,6 +413,8 @@ Explore them directly:
341
413
 
342
414
  ```bash
343
415
  osb sandbox --help
416
+ osb template --help
417
+ osb snapshot --help
344
418
  osb command --help
345
419
  osb file --help
346
420
  osb skills --help
@@ -362,12 +436,12 @@ Supported targets:
362
436
 
363
437
  | Target | Install location |
364
438
  | --- | --- |
365
- | `claude` | `./.claude/skills/` or `~/.claude/skills/` |
439
+ | `claude` | `./.claude/skills/<name>/SKILL.md` or `~/.claude/skills/<name>/SKILL.md` |
366
440
  | `cursor` | `./.cursor/rules/` or `~/.cursor/rules/` |
367
441
  | `codex` | `./.codex/skills/<name>/SKILL.md` or `~/.codex/skills/<name>/SKILL.md` |
368
- | `copilot` | `./.github/copilot-instructions.md` or `~/.github/copilot-instructions.md` |
442
+ | `copilot` | `./.github/copilot-instructions.md` or `~/.copilot/copilot-instructions.md` |
369
443
  | `windsurf` | `./.windsurfrules` or `~/.windsurfrules` |
370
- | `cline` | `./.clinerules` or `~/.clinerules` |
444
+ | `cline` | `./.clinerules` or `~/.cline/rules/opensandbox-<name>.md` |
371
445
  | `opencode` | `./.agents/skills/<name>/SKILL.md` or `~/.agents/skills/<name>/SKILL.md` |
372
446
 
373
447
  Common flows:
@@ -44,10 +44,11 @@ classifiers = [
44
44
  dependencies = [
45
45
  # `command run --argv` passes a list to commands.run; the SDK accepts a
46
46
  # list there from 0.1.17. Release the CLI only after that SDK is published.
47
- "opensandbox>=1.1.0,<2.0.0",
47
+ "opensandbox>=1.1.1,<2.0.0",
48
48
  "click>=8.1.0,<9.0",
49
49
  "rich>=13.0.0,<14.0",
50
50
  "pyyaml>=6.0,<7.0",
51
+ "tomlkit>=0.13.0,<1.0",
51
52
  "tomli>=2.0.0; python_version < '3.11'",
52
53
  ]
53
54
 
@@ -90,15 +90,10 @@ class ClientContext:
90
90
  self._manager = SandboxManagerSync.create(self.connection_config)
91
91
  return self._manager
92
92
 
93
- def resolve_sandbox_id(self, sandbox_id: str) -> str:
94
- """Return the sandbox ID exactly as provided by the user."""
95
- return sandbox_id
96
-
97
93
  def connect_sandbox(
98
94
  self, sandbox_id: str, *, skip_health_check: bool = True
99
95
  ) -> SandboxSync:
100
96
  """Connect to an existing sandbox by ID."""
101
- sandbox_id = self.resolve_sandbox_id(sandbox_id)
102
97
  return SandboxSync.connect(
103
98
  sandbox_id,
104
99
  connection_config=self.connection_config,
@@ -18,10 +18,11 @@ from __future__ import annotations
18
18
 
19
19
  import shlex
20
20
  import sys
21
+ from collections.abc import Callable
21
22
  from datetime import timedelta
22
23
 
23
24
  import click
24
- from opensandbox.models.execd import OutputMessage, RunCommandOpts
25
+ from opensandbox.models.execd import Execution, OutputMessage, RunCommandOpts
25
26
  from opensandbox.models.execd_sync import ExecutionHandlersSync
26
27
 
27
28
  from opensandbox_cli.client import ClientContext
@@ -41,7 +42,29 @@ def command_group(ctx: click.Context) -> None:
41
42
  click.echo(ctx.get_help())
42
43
 
43
44
 
44
- # ---- run ------------------------------------------------------------------
45
+ def _stream_handlers() -> tuple[ExecutionHandlersSync, Callable[[], None]]:
46
+ """Create stdout/stderr streaming handlers and a trailing-newline flusher."""
47
+ last_text = ""
48
+
49
+ def on_stdout(msg: OutputMessage) -> None:
50
+ nonlocal last_text
51
+ last_text = msg.text
52
+ sys.stdout.write(msg.text)
53
+ sys.stdout.flush()
54
+
55
+ def on_stderr(msg: OutputMessage) -> None:
56
+ nonlocal last_text
57
+ last_text = msg.text
58
+ sys.stderr.write(msg.text)
59
+ sys.stderr.flush()
60
+
61
+ def flush_newline() -> None:
62
+ if last_text and not last_text.endswith("\n"):
63
+ sys.stdout.write("\n")
64
+ sys.stdout.flush()
65
+
66
+ return ExecutionHandlersSync(on_stdout=on_stdout, on_stderr=on_stderr), flush_newline
67
+
45
68
 
46
69
  def _run_command(
47
70
  obj: ClientContext,
@@ -55,15 +78,10 @@ def _run_command(
55
78
  ) -> None:
56
79
  """Shared implementation for ``command run``.
57
80
 
58
- Mode contract:
59
- - foreground (default): stream output directly, allow only ``-o raw``
60
- - background (``--background``): return a tracked execution object, allow structured output
61
-
62
- Payload contract:
63
- - default: join the arguments into one shell command string (works against
64
- every deployed execd)
65
- - ``--argv``: send the arguments as a literal argv list, no shell; needs an
66
- execd that accepts the ``argv`` request field
81
+ Foreground mode streams output and allows only ``-o raw``; background mode
82
+ returns a tracked execution object and allows structured output. The payload
83
+ is joined into one shell command string unless ``argv`` sends it as a
84
+ literal argv list (needs an argv-capable execd).
67
85
  """
68
86
  allowed = ("table", "json", "yaml") if background else ("raw",)
69
87
  fallback = "table" if background else "raw"
@@ -84,6 +102,7 @@ def _run_command(
84
102
 
85
103
  if background:
86
104
  execution = sandbox.commands.run(payload, opts=opts)
105
+ _handle_execution_error(obj, execution, background=True)
87
106
  obj.output.success_panel(
88
107
  {
89
108
  "execution_id": execution.id,
@@ -94,42 +113,33 @@ def _run_command(
94
113
  )
95
114
  return
96
115
 
97
- # Foreground: stream stdout/stderr to terminal
98
- last_text = ""
99
-
100
- def on_stdout(msg: OutputMessage) -> None:
101
- nonlocal last_text
102
- last_text = msg.text
103
- sys.stdout.write(msg.text)
104
- sys.stdout.flush()
105
-
106
- def on_stderr(msg: OutputMessage) -> None:
107
- nonlocal last_text
108
- last_text = msg.text
109
- sys.stderr.write(msg.text)
110
- sys.stderr.flush()
111
-
112
- handlers = ExecutionHandlersSync(on_stdout=on_stdout, on_stderr=on_stderr)
116
+ handlers, flush_newline = _stream_handlers()
113
117
  execution = sandbox.commands.run(payload, opts=opts, handlers=handlers)
114
-
115
- # Ensure terminal prompt starts on a new line
116
- if last_text and not last_text.endswith("\n"):
117
- sys.stdout.write("\n")
118
- sys.stdout.flush()
119
-
118
+ flush_newline()
120
119
  _handle_execution_error(obj, execution)
121
120
  finally:
122
121
  sandbox.close()
123
122
 
124
123
 
125
- def _handle_execution_error(obj: ClientContext, execution) -> None:
126
- """Exit non-zero if the execution finished with an error."""
124
+ def _handle_execution_error(
125
+ obj: ClientContext, execution: Execution, *, background: bool = False
126
+ ) -> None:
127
+ """Exit non-zero on execution errors or missing completion/startup confirmation."""
127
128
  if execution.error:
128
129
  obj.output.error_panel(
129
130
  f"{execution.error.name}: {execution.error.value}",
130
131
  title="Execution Error",
131
132
  )
132
133
  sys.exit(1)
134
+ if execution.complete is None:
135
+ confirmation = (
136
+ "background command startup was confirmed" if background else "the command finished"
137
+ )
138
+ obj.output.error_panel(
139
+ f"The output stream ended before {confirmation}, so its result is unknown.",
140
+ title="Execution Incomplete",
141
+ )
142
+ sys.exit(1)
133
143
 
134
144
 
135
145
  @command_group.command(
@@ -185,8 +195,6 @@ def command_run(
185
195
  )
186
196
 
187
197
 
188
- # ---- status ---------------------------------------------------------------
189
-
190
198
  @command_group.command("status")
191
199
  @click.argument("sandbox_id")
192
200
  @click.argument("execution_id")
@@ -209,8 +217,6 @@ def command_status(
209
217
  sandbox.close()
210
218
 
211
219
 
212
- # ---- logs -----------------------------------------------------------------
213
-
214
220
  @command_group.command("logs")
215
221
  @click.argument("sandbox_id")
216
222
  @click.argument("execution_id")
@@ -242,8 +248,6 @@ def command_logs(
242
248
  sandbox.close()
243
249
 
244
250
 
245
- # ---- interrupt ------------------------------------------------------------
246
-
247
251
  @command_group.command("interrupt")
248
252
  @click.argument("sandbox_id")
249
253
  @click.argument("execution_id")
@@ -330,21 +334,7 @@ def session_run(
330
334
  cmd_str = " ".join(shlex.quote(arg) for arg in command)
331
335
  sandbox = obj.connect_sandbox(sandbox_id)
332
336
  try:
333
- last_text = ""
334
-
335
- def on_stdout(msg: OutputMessage) -> None:
336
- nonlocal last_text
337
- last_text = msg.text
338
- sys.stdout.write(msg.text)
339
- sys.stdout.flush()
340
-
341
- def on_stderr(msg: OutputMessage) -> None:
342
- nonlocal last_text
343
- last_text = msg.text
344
- sys.stderr.write(msg.text)
345
- sys.stderr.flush()
346
-
347
- handlers = ExecutionHandlersSync(on_stdout=on_stdout, on_stderr=on_stderr)
337
+ handlers, flush_newline = _stream_handlers()
348
338
  execution = sandbox.commands.run_in_session(
349
339
  session_id,
350
340
  cmd_str,
@@ -352,11 +342,7 @@ def session_run(
352
342
  timeout=timeout,
353
343
  handlers=handlers,
354
344
  )
355
-
356
- if last_text and not last_text.endswith("\n"):
357
- sys.stdout.write("\n")
358
- sys.stdout.flush()
359
-
345
+ flush_newline()
360
346
  _handle_execution_error(obj, execution)
361
347
  finally:
362
348
  sandbox.close()