opensandbox-cli 0.1.1__tar.gz → 1.1.0__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 (30) hide show
  1. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/.gitignore +18 -0
  2. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/PKG-INFO +69 -14
  3. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/README.md +64 -9
  4. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/pyproject.toml +8 -6
  5. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/src/opensandbox_cli/__init__.py +1 -1
  6. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/src/opensandbox_cli/__main__.py +1 -1
  7. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/src/opensandbox_cli/client.py +1 -1
  8. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/src/opensandbox_cli/commands/__init__.py +1 -1
  9. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/src/opensandbox_cli/commands/command.py +31 -5
  10. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/src/opensandbox_cli/commands/config_cmd.py +1 -1
  11. opensandbox_cli-1.1.0/src/opensandbox_cli/commands/credential_vault.py +306 -0
  12. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/src/opensandbox_cli/commands/devops.py +1 -1
  13. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/src/opensandbox_cli/commands/diagnostics.py +12 -6
  14. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/src/opensandbox_cli/commands/egress.py +1 -1
  15. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/src/opensandbox_cli/commands/file.py +55 -7
  16. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/src/opensandbox_cli/commands/sandbox.py +16 -2
  17. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/src/opensandbox_cli/commands/skills.py +1 -1
  18. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/src/opensandbox_cli/config.py +1 -1
  19. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/src/opensandbox_cli/main.py +3 -1
  20. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/src/opensandbox_cli/output.py +1 -1
  21. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/src/opensandbox_cli/skill_registry.py +29 -0
  22. opensandbox_cli-1.1.0/src/opensandbox_cli/skills/opensandbox-credential-vault.md +136 -0
  23. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/src/opensandbox_cli/skills/opensandbox-file-operations.md +4 -1
  24. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/src/opensandbox_cli/skills/opensandbox-sandbox-lifecycle.md +4 -0
  25. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/src/opensandbox_cli/skills/opensandbox-sandbox-troubleshooting.md +8 -11
  26. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/src/opensandbox_cli/utils.py +1 -1
  27. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/LICENSE +0 -0
  28. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/src/opensandbox_cli/py.typed +0 -0
  29. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/src/opensandbox_cli/skills/opensandbox-command-execution.md +0 -0
  30. {opensandbox_cli-0.1.1 → opensandbox_cli-1.1.0}/src/opensandbox_cli/skills/opensandbox-network-egress.md +0 -0
@@ -269,6 +269,19 @@ nbdist/
269
269
  generated/
270
270
  **/generated/**
271
271
 
272
+ # Fast Sandbox FastPath gRPC stubs are committed intentionally (deterministic protoc
273
+ # output; the server package needs them at install time, so they are checked
274
+ # in rather than regenerated).
275
+ !server/opensandbox_server/services/fast_sandbox/generated/
276
+ !server/opensandbox_server/services/fast_sandbox/generated/**
277
+ server/opensandbox_server/services/fast_sandbox/generated/__pycache__/
278
+
279
+ # execd eBPF kernel BTF dumps (bpf2go build-time only, never committed —
280
+ # see components/execd/pkg/ebpf/prog/audit.bpf.c; regenerate with:
281
+ # bpftool btf dump file /sys/kernel/btf/vmlinux format c > .../vmlinux.h)
282
+ components/execd/pkg/ebpf/prog/vmlinux.h
283
+ components/execd/pkg/ebpf/prog/vmlinux_*.h
284
+
272
285
  # gVisor runtime binaries (downloaded dynamically)
273
286
  kubernetes/test/kind/gvisor/runsc
274
287
  kubernetes/test/kind/gvisor/containerd-shim-runsc-v1
@@ -276,3 +289,8 @@ bin/
276
289
  obj/
277
290
 
278
291
  .qoder/
292
+ /.vs
293
+
294
+ # fast-sandbox pinned source checkout (materialized by
295
+ # manifests/release/build-fast-sandbox.sh from manifests/third-party/fast-sandbox.commit)
296
+ .fast-sandbox/
@@ -1,11 +1,11 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: opensandbox-cli
3
- Version: 0.1.1
3
+ Version: 1.1.0
4
4
  Summary: OpenSandbox CLI - Command-line interface for managing sandboxes
5
5
  Project-URL: Homepage, https://open-sandbox.ai
6
- Project-URL: Repository, https://github.com/alibaba/OpenSandbox
6
+ Project-URL: Repository, https://github.com/opensandbox-group/OpenSandbox
7
7
  Project-URL: Documentation, https://open-sandbox.ai
8
- Project-URL: Issues, https://github.com/alibaba/OpenSandbox/issues
8
+ Project-URL: Issues, https://github.com/opensandbox-group/OpenSandbox/issues
9
9
  Author-email: OpenSandbox Team <ninan.nn@alibaba-inc.com>
10
10
  License: Apache License
11
11
  Version 2.0, January 2004
@@ -224,7 +224,7 @@ 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<0.2.0,>=0.1.9
227
+ Requires-Dist: opensandbox<2.0.0,>=1.1.0
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'
@@ -238,6 +238,7 @@ Description-Content-Type: text/markdown
238
238
  - run commands inside a sandbox
239
239
  - read and modify sandbox files
240
240
  - inspect runtime egress policy
241
+ - manage sandbox-local Credential Vault state
241
242
  - collect low-level diagnostics
242
243
  - install OpenSandbox-specific skills for coding agents
243
244
 
@@ -282,6 +283,7 @@ opensandbox-server
282
283
  osb config init
283
284
  osb config set connection.domain localhost:8080
284
285
  osb config set connection.protocol http
286
+ osb config set connection.api_key <your-api-key>
285
287
  osb config show -o json
286
288
  ```
287
289
 
@@ -379,6 +381,12 @@ osb sandbox create \
379
381
  --volumes-file volumes.json
380
382
  ```
381
383
 
384
+ Create with Credential Vault proxy enabled:
385
+
386
+ ```bash
387
+ osb sandbox create --image python:3.12 --network-policy-file network-policy.json --credential-proxy -o json
388
+ ```
389
+
382
390
  ### List and inspect sandboxes
383
391
 
384
392
  ```bash
@@ -390,6 +398,17 @@ osb sandbox metrics <sandbox-id>
390
398
  osb sandbox metrics <sandbox-id> --watch -o raw
391
399
  ```
392
400
 
401
+ ### Pause a sandbox
402
+
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:
406
+
407
+ ```bash
408
+ osb sandbox pause <sandbox-id>
409
+ osb sandbox get <sandbox-id> -o json
410
+ ```
411
+
393
412
  ### Expose a service
394
413
 
395
414
  ```bash
@@ -412,6 +431,17 @@ osb command status <sandbox-id> <execution-id> -o json
412
431
  osb command logs <sandbox-id> <execution-id> -o json
413
432
  ```
414
433
 
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:
440
+
441
+ ```bash
442
+ osb command run <sandbox-id> -o raw --argv -- python3 -c "import sys; print(sys.argv[1:])" "a b" '$HOME' "x'y" ""
443
+ ```
444
+
415
445
  Persistent shell session:
416
446
 
417
447
  ```bash
@@ -433,6 +463,11 @@ osb file replace <sandbox-id> /workspace/app.py --old old --new new -o json
433
463
  osb file chmod <sandbox-id> /workspace/script.sh --mode 755 -o json
434
464
  ```
435
465
 
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).
470
+
436
471
  ### Manage runtime egress policy
437
472
 
438
473
  Inspect current policy:
@@ -453,26 +488,46 @@ If you are debugging connectivity, verify behavior with an actual command:
453
488
  osb command run <sandbox-id> -o raw -- curl -I https://pypi.org
454
489
  ```
455
490
 
491
+ ### Manage Credential Vault
492
+
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.
496
+
497
+ ```bash
498
+ osb credential-vault create <sandbox-id> --file vault.yaml -o json
499
+ osb credential-vault get <sandbox-id> -o json
500
+ osb credential-vault patch <sandbox-id> --file mutation.yaml -o json
501
+ osb credential-vault credential list <sandbox-id> -o json
502
+ osb credential-vault binding list <sandbox-id> -o json
503
+ osb credential-vault delete <sandbox-id> -o json
504
+ ```
505
+
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.
508
+
456
509
  ### Collect diagnostics
457
510
 
458
511
  Use the stable diagnostics commands for API-backed log and event descriptors.
459
512
 
460
513
  ```bash
461
- osb diagnostics events <sandbox-id> --scope lifecycle -o raw
462
514
  osb diagnostics events <sandbox-id> --scope runtime -o raw
515
+ osb diagnostics events <sandbox-id> --scope all -o raw
463
516
  osb diagnostics logs <sandbox-id> --scope container -o raw
464
- osb diagnostics logs <sandbox-id> --scope lifecycle -o json
517
+ osb diagnostics logs <sandbox-id> --scope all -o json
465
518
  osb diagnostics events <sandbox-id> --scope runtime -o json
466
519
  osb diagnostics logs <sandbox-id> --scope container -o yaml
467
520
  ```
468
521
 
469
- `--scope` is required for stable diagnostics. Common scopes are `lifecycle` and
470
- `container` for logs, and `lifecycle` and `runtime` for events. Raw output
471
- prints inline diagnostic text, or the content URL when diagnostics are
472
- delivered as a temporary URL. Structured CLI output follows the SDK/Python field
473
- style, for example `content_url`, `content_length`, and `expires_at`.
474
- Some server builds may return `DIAGNOSTICS_NOT_IMPLEMENTED` for scoped
475
- diagnostics until the stable backend implementation is enabled.
522
+ `--scope` is required for stable diagnostics. The built-in server supports
523
+ `container` and `all` for logs, and `runtime` and `all` for events. It returns
524
+ `DIAGNOSTICS_SCOPE_UNSUPPORTED` for unavailable scopes, including lifecycle events.
525
+ Best-effort scopes may include a `warnings` field when the backend can only
526
+ provide a subset. Raw output prints inline
527
+ diagnostic text, or the content URL when diagnostics are delivered as a
528
+ temporary URL. Structured CLI output follows the SDK/Python field style, for
529
+ example `content_url`, `content_length`, and `expires_at`. Older server builds
530
+ may still return `DIAGNOSTICS_NOT_IMPLEMENTED` for scoped diagnostics.
476
531
 
477
532
  Legacy DevOps diagnostics remain experimental. Prefer `osb diagnostics logs/events`
478
533
  for stable API-backed log and event collection.
@@ -6,6 +6,7 @@
6
6
  - run commands inside a sandbox
7
7
  - read and modify sandbox files
8
8
  - inspect runtime egress policy
9
+ - manage sandbox-local Credential Vault state
9
10
  - collect low-level diagnostics
10
11
  - install OpenSandbox-specific skills for coding agents
11
12
 
@@ -50,6 +51,7 @@ opensandbox-server
50
51
  osb config init
51
52
  osb config set connection.domain localhost:8080
52
53
  osb config set connection.protocol http
54
+ osb config set connection.api_key <your-api-key>
53
55
  osb config show -o json
54
56
  ```
55
57
 
@@ -147,6 +149,12 @@ osb sandbox create \
147
149
  --volumes-file volumes.json
148
150
  ```
149
151
 
152
+ Create with Credential Vault proxy enabled:
153
+
154
+ ```bash
155
+ osb sandbox create --image python:3.12 --network-policy-file network-policy.json --credential-proxy -o json
156
+ ```
157
+
150
158
  ### List and inspect sandboxes
151
159
 
152
160
  ```bash
@@ -158,6 +166,17 @@ osb sandbox metrics <sandbox-id>
158
166
  osb sandbox metrics <sandbox-id> --watch -o raw
159
167
  ```
160
168
 
169
+ ### Pause a sandbox
170
+
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:
174
+
175
+ ```bash
176
+ osb sandbox pause <sandbox-id>
177
+ osb sandbox get <sandbox-id> -o json
178
+ ```
179
+
161
180
  ### Expose a service
162
181
 
163
182
  ```bash
@@ -180,6 +199,17 @@ osb command status <sandbox-id> <execution-id> -o json
180
199
  osb command logs <sandbox-id> <execution-id> -o json
181
200
  ```
182
201
 
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:
208
+
209
+ ```bash
210
+ osb command run <sandbox-id> -o raw --argv -- python3 -c "import sys; print(sys.argv[1:])" "a b" '$HOME' "x'y" ""
211
+ ```
212
+
183
213
  Persistent shell session:
184
214
 
185
215
  ```bash
@@ -201,6 +231,11 @@ osb file replace <sandbox-id> /workspace/app.py --old old --new new -o json
201
231
  osb file chmod <sandbox-id> /workspace/script.sh --mode 755 -o json
202
232
  ```
203
233
 
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).
238
+
204
239
  ### Manage runtime egress policy
205
240
 
206
241
  Inspect current policy:
@@ -221,26 +256,46 @@ If you are debugging connectivity, verify behavior with an actual command:
221
256
  osb command run <sandbox-id> -o raw -- curl -I https://pypi.org
222
257
  ```
223
258
 
259
+ ### Manage Credential Vault
260
+
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.
264
+
265
+ ```bash
266
+ osb credential-vault create <sandbox-id> --file vault.yaml -o json
267
+ osb credential-vault get <sandbox-id> -o json
268
+ osb credential-vault patch <sandbox-id> --file mutation.yaml -o json
269
+ osb credential-vault credential list <sandbox-id> -o json
270
+ osb credential-vault binding list <sandbox-id> -o json
271
+ osb credential-vault delete <sandbox-id> -o json
272
+ ```
273
+
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.
276
+
224
277
  ### Collect diagnostics
225
278
 
226
279
  Use the stable diagnostics commands for API-backed log and event descriptors.
227
280
 
228
281
  ```bash
229
- osb diagnostics events <sandbox-id> --scope lifecycle -o raw
230
282
  osb diagnostics events <sandbox-id> --scope runtime -o raw
283
+ osb diagnostics events <sandbox-id> --scope all -o raw
231
284
  osb diagnostics logs <sandbox-id> --scope container -o raw
232
- osb diagnostics logs <sandbox-id> --scope lifecycle -o json
285
+ osb diagnostics logs <sandbox-id> --scope all -o json
233
286
  osb diagnostics events <sandbox-id> --scope runtime -o json
234
287
  osb diagnostics logs <sandbox-id> --scope container -o yaml
235
288
  ```
236
289
 
237
- `--scope` is required for stable diagnostics. Common scopes are `lifecycle` and
238
- `container` for logs, and `lifecycle` and `runtime` for events. Raw output
239
- prints inline diagnostic text, or the content URL when diagnostics are
240
- delivered as a temporary URL. Structured CLI output follows the SDK/Python field
241
- style, for example `content_url`, `content_length`, and `expires_at`.
242
- Some server builds may return `DIAGNOSTICS_NOT_IMPLEMENTED` for scoped
243
- diagnostics until the stable backend implementation is enabled.
290
+ `--scope` is required for stable diagnostics. The built-in server supports
291
+ `container` and `all` for logs, and `runtime` and `all` for events. It returns
292
+ `DIAGNOSTICS_SCOPE_UNSUPPORTED` for unavailable scopes, including lifecycle events.
293
+ Best-effort scopes may include a `warnings` field when the backend can only
294
+ provide a subset. Raw output prints inline
295
+ diagnostic text, or the content URL when diagnostics are delivered as a
296
+ temporary URL. Structured CLI output follows the SDK/Python field style, for
297
+ example `content_url`, `content_length`, and `expires_at`. Older server builds
298
+ may still return `DIAGNOSTICS_NOT_IMPLEMENTED` for scoped diagnostics.
244
299
 
245
300
  Legacy DevOps diagnostics remain experimental. Prefer `osb diagnostics logs/events`
246
301
  for stable API-backed log and event collection.
@@ -1,4 +1,4 @@
1
- # Copyright 2026 Alibaba Group Holding Ltd.
1
+ # Copyright 2026 The OpenSandbox Authors.
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -42,7 +42,9 @@ classifiers = [
42
42
  "Typing :: Typed",
43
43
  ]
44
44
  dependencies = [
45
- "opensandbox>=0.1.9,<0.2.0",
45
+ # `command run --argv` passes a list to commands.run; the SDK accepts a
46
+ # list there from 0.1.17. Release the CLI only after that SDK is published.
47
+ "opensandbox>=1.1.0,<2.0.0",
46
48
  "click>=8.1.0,<9.0",
47
49
  "rich>=13.0.0,<14.0",
48
50
  "pyyaml>=6.0,<7.0",
@@ -51,9 +53,9 @@ dependencies = [
51
53
 
52
54
  [project.urls]
53
55
  Homepage = "https://open-sandbox.ai"
54
- Repository = "https://github.com/alibaba/OpenSandbox"
56
+ Repository = "https://github.com/opensandbox-group/OpenSandbox"
55
57
  Documentation = "https://open-sandbox.ai"
56
- Issues = "https://github.com/alibaba/OpenSandbox/issues"
58
+ Issues = "https://github.com/opensandbox-group/OpenSandbox/issues"
57
59
 
58
60
  [project.scripts]
59
61
  opensandbox = "opensandbox_cli.main:cli"
@@ -64,8 +66,8 @@ source = "vcs"
64
66
 
65
67
  [tool.hatch.version.raw-options]
66
68
  root = ".."
67
- tag_regex = "^cli/v(?P<version>\\d+\\.\\d+\\.\\d+(?:[\\.\\w\\+\\-]*)?)$"
68
- git_describe_command = 'git describe --dirty --tags --long --match "cli/v*"'
69
+ tag_regex = "^release-(?P<version>\\d+\\.\\d+\\.\\d+(?:[\\.\\w\\+\\-]*)?)$"
70
+ git_describe_command = 'git describe --dirty --tags --long --match "release-*"'
69
71
  fallback_version = "0.1.1"
70
72
 
71
73
  [tool.hatch.build]
@@ -1,4 +1,4 @@
1
- # Copyright 2026 Alibaba Group Holding Ltd.
1
+ # Copyright 2026 The OpenSandbox Authors.
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -1,4 +1,4 @@
1
- # Copyright 2026 Alibaba Group Holding Ltd.
1
+ # Copyright 2026 The OpenSandbox Authors.
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -1,4 +1,4 @@
1
- # Copyright 2026 Alibaba Group Holding Ltd.
1
+ # Copyright 2026 The OpenSandbox Authors.
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -1,4 +1,4 @@
1
- # Copyright 2026 Alibaba Group Holding Ltd.
1
+ # Copyright 2026 The OpenSandbox Authors.
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -1,4 +1,4 @@
1
- # Copyright 2026 Alibaba Group Holding Ltd.
1
+ # Copyright 2026 The OpenSandbox Authors.
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -51,17 +51,28 @@ def _run_command(
51
51
  workdir: str | None,
52
52
  timeout: timedelta | None,
53
53
  output_format: str | None,
54
+ argv: bool = False,
54
55
  ) -> None:
55
56
  """Shared implementation for ``command run``.
56
57
 
57
58
  Mode contract:
58
59
  - foreground (default): stream output directly, allow only ``-o raw``
59
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
60
67
  """
61
68
  allowed = ("table", "json", "yaml") if background else ("raw",)
62
69
  fallback = "table" if background else "raw"
63
70
  prepare_output(obj, output_format, allowed=allowed, fallback=fallback)
64
- cmd_str = " ".join(shlex.quote(arg) for arg in command)
71
+ payload: str | list[str]
72
+ if argv:
73
+ payload = list(command)
74
+ else:
75
+ payload = " ".join(shlex.quote(arg) for arg in command)
65
76
  sandbox = obj.connect_sandbox(sandbox_id)
66
77
 
67
78
  try:
@@ -72,7 +83,7 @@ def _run_command(
72
83
  )
73
84
 
74
85
  if background:
75
- execution = sandbox.commands.run(cmd_str, opts=opts)
86
+ execution = sandbox.commands.run(payload, opts=opts)
76
87
  obj.output.success_panel(
77
88
  {
78
89
  "execution_id": execution.id,
@@ -99,7 +110,7 @@ def _run_command(
99
110
  sys.stderr.flush()
100
111
 
101
112
  handlers = ExecutionHandlersSync(on_stdout=on_stdout, on_stderr=on_stderr)
102
- execution = sandbox.commands.run(cmd_str, opts=opts, handlers=handlers)
113
+ execution = sandbox.commands.run(payload, opts=opts, handlers=handlers)
103
114
 
104
115
  # Ensure terminal prompt starts on a new line
105
116
  if last_text and not last_text.endswith("\n"):
@@ -124,13 +135,25 @@ def _handle_execution_error(obj: ClientContext, execution) -> None:
124
135
  @command_group.command(
125
136
  "run",
126
137
  help="Run a command in a sandbox. Use `--` before the sandbox command payload.",
127
- epilog="Separator rule: use `--` before the sandbox command payload.",
138
+ epilog=(
139
+ "Separator rule: use `--` before the sandbox command payload.\n\n"
140
+ "By default the payload is joined into one shell command string. Add "
141
+ "`--argv` to pass the arguments to the executable as-is (no shell), so "
142
+ "literal `$HOME`, quotes, and empty strings are preserved; this needs a "
143
+ "sandbox image whose execd accepts argv requests."
144
+ ),
128
145
  )
129
146
  @click.argument("sandbox_id")
130
147
  @click.argument("command", nargs=-1, required=True)
131
148
  @click.option("-d", "--background", is_flag=True, default=False, help="Run in background.")
132
149
  @click.option("-w", "--workdir", default=None, help="Working directory.")
133
150
  @click.option("-t", "--timeout", type=DURATION, default=None, help="Command timeout (e.g. 30s, 5m).")
151
+ @click.option(
152
+ "--argv",
153
+ is_flag=True,
154
+ default=False,
155
+ help="Pass the payload as a literal argv list (no shell). Requires an argv-capable execd.",
156
+ )
134
157
  @output_option("table", "json", "yaml", "raw")
135
158
  @click.pass_obj
136
159
  @handle_errors
@@ -141,12 +164,14 @@ def command_run(
141
164
  background: bool,
142
165
  workdir: str | None,
143
166
  timeout: timedelta | None,
167
+ argv: bool,
144
168
  output_format: str | None,
145
169
  ) -> None:
146
170
  """Run a command in a sandbox.
147
171
 
148
172
  Default mode streams output directly. Add ``--background`` to return a
149
173
  tracked execution object instead. Use ``--`` before the sandbox command payload.
174
+ The payload is sent as a shell command string unless ``--argv`` is given.
150
175
  """
151
176
  _run_command(
152
177
  obj,
@@ -156,6 +181,7 @@ def command_run(
156
181
  workdir,
157
182
  timeout,
158
183
  output_format,
184
+ argv=argv,
159
185
  )
160
186
 
161
187
 
@@ -1,4 +1,4 @@
1
- # Copyright 2026 Alibaba Group Holding Ltd.
1
+ # Copyright 2026 The OpenSandbox Authors.
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.