opensandbox-cli 1.2.0__tar.gz → 1.2.1__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.
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/.gitignore +4 -0
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/PKG-INFO +10 -5
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/README.md +8 -3
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/pyproject.toml +1 -1
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/commands/command.py +6 -3
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/commands/config_cmd.py +4 -3
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/commands/egress.py +1 -1
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/commands/sandbox.py +12 -4
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/commands/template.py +4 -1
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/commands/terminal.py +14 -3
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/config.py +62 -9
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/main.py +14 -3
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/output.py +9 -7
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/skills/opensandbox-sandbox-lifecycle.md +5 -2
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/utils.py +4 -7
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/LICENSE +0 -0
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/__init__.py +0 -0
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/__main__.py +0 -0
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/client.py +0 -0
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/commands/__init__.py +0 -0
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/commands/credential_vault.py +0 -0
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/commands/devops.py +0 -0
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/commands/diagnostics.py +0 -0
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/commands/file.py +0 -0
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/commands/skills.py +0 -0
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/commands/snapshot.py +0 -0
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/py.typed +0 -0
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/skill_registry.py +0 -0
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/skills/opensandbox-command-execution.md +0 -0
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/skills/opensandbox-credential-vault.md +0 -0
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/skills/opensandbox-file-operations.md +0 -0
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/skills/opensandbox-network-egress.md +0 -0
- {opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/skills/opensandbox-sandbox-troubleshooting.md +0 -0
|
@@ -294,3 +294,7 @@ obj/
|
|
|
294
294
|
# fast-sandbox pinned source checkout (materialized by
|
|
295
295
|
# manifests/release/build-fast-sandbox.sh from manifests/third-party/fast-sandbox.commit)
|
|
296
296
|
.fast-sandbox/
|
|
297
|
+
|
|
298
|
+
# Test-only localhost certificate pair for the egress OSEP-0025 resolver stub
|
|
299
|
+
# (fixtures/README.md documents regeneration; these certify nothing).
|
|
300
|
+
!components/egress/tests/resolver/fixtures/*.pem
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: opensandbox-cli
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.1
|
|
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,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<2.0.0,>=1.2.
|
|
227
|
+
Requires-Dist: opensandbox<2.0.0,>=1.2.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'
|
|
@@ -436,7 +436,8 @@ osb sandbox create -f sandbox-request.json -o json
|
|
|
436
436
|
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.
|
|
437
437
|
|
|
438
438
|
```bash
|
|
439
|
-
osb template create --image python:3.12 --publish s3://bucket/publish -o json
|
|
439
|
+
osb template create --image python:3.12 --publish s3://bucket/publish --format native -o json
|
|
440
|
+
osb template create --image python:3.12 --publish oci://registry.example.com/sandbox/templates --format overlaybd -o json
|
|
440
441
|
osb template get <template-id> -o json
|
|
441
442
|
osb template list -o json
|
|
442
443
|
osb template delete <template-id> -o json
|
|
@@ -452,11 +453,14 @@ osb template create -f template-request.json -o json
|
|
|
452
453
|
{
|
|
453
454
|
"image": "python:3.12",
|
|
454
455
|
"publish": "s3://bucket/publish",
|
|
456
|
+
"format": "native",
|
|
455
457
|
"resourceLimits": { "cpu": "1", "memory": "512Mi" },
|
|
456
458
|
"readiness": { "probe": "tcp://127.0.0.1:44772", "warmupSeconds": 60 }
|
|
457
459
|
}
|
|
458
460
|
```
|
|
459
461
|
|
|
462
|
+
OCI targets must name a repository without a tag or digest. Use `overlaybd` (the default) for OCI and explicitly select `native` for S3. The builder returns an immutable OCI reference in `status.manifestRef`; `sandbox create --template` uses that reference automatically. See [OCI template setup](../docs/guides/oci-templates.md) for Registry Secrets and runtime prerequisites.
|
|
463
|
+
|
|
460
464
|
### Manage snapshots
|
|
461
465
|
|
|
462
466
|
Snapshots capture a sandbox's state and can back new sandboxes.
|
|
@@ -512,7 +516,7 @@ osb command logs <sandbox-id> <execution-id> -o json
|
|
|
512
516
|
|
|
513
517
|
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.
|
|
514
518
|
|
|
515
|
-
By default
|
|
519
|
+
By default each payload word after `--` is quoted as a single shell token and the tokens are joined into one shell command line that the sandbox's shell runs. Shell syntax such as pipelines or redirection must therefore ride inside an explicit word (`-- sh -c 'echo hi > /tmp/x'`); an unquoted `$VAR` is expanded by your own terminal before the CLI sees it, while a quoted one reaches the sandbox shell literally. 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:
|
|
516
520
|
|
|
517
521
|
```bash
|
|
518
522
|
osb command run <sandbox-id> -o raw --argv -- python3 -c "import sys; print(sys.argv[1:])" "a b" '$HOME' "x'y" ""
|
|
@@ -734,7 +738,8 @@ api_key = "your-api-key"
|
|
|
734
738
|
domain = "localhost:8080"
|
|
735
739
|
protocol = "http"
|
|
736
740
|
request_timeout = 30
|
|
737
|
-
use_server_proxy
|
|
741
|
+
# use_server_proxy is deprecated (maintenance mode); prefer the ingress gateway for data-plane traffic.
|
|
742
|
+
# use_server_proxy = false
|
|
738
743
|
|
|
739
744
|
[output]
|
|
740
745
|
color = true
|
|
@@ -202,7 +202,8 @@ osb sandbox create -f sandbox-request.json -o json
|
|
|
202
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
203
|
|
|
204
204
|
```bash
|
|
205
|
-
osb template create --image python:3.12 --publish s3://bucket/publish -o json
|
|
205
|
+
osb template create --image python:3.12 --publish s3://bucket/publish --format native -o json
|
|
206
|
+
osb template create --image python:3.12 --publish oci://registry.example.com/sandbox/templates --format overlaybd -o json
|
|
206
207
|
osb template get <template-id> -o json
|
|
207
208
|
osb template list -o json
|
|
208
209
|
osb template delete <template-id> -o json
|
|
@@ -218,11 +219,14 @@ osb template create -f template-request.json -o json
|
|
|
218
219
|
{
|
|
219
220
|
"image": "python:3.12",
|
|
220
221
|
"publish": "s3://bucket/publish",
|
|
222
|
+
"format": "native",
|
|
221
223
|
"resourceLimits": { "cpu": "1", "memory": "512Mi" },
|
|
222
224
|
"readiness": { "probe": "tcp://127.0.0.1:44772", "warmupSeconds": 60 }
|
|
223
225
|
}
|
|
224
226
|
```
|
|
225
227
|
|
|
228
|
+
OCI targets must name a repository without a tag or digest. Use `overlaybd` (the default) for OCI and explicitly select `native` for S3. The builder returns an immutable OCI reference in `status.manifestRef`; `sandbox create --template` uses that reference automatically. See [OCI template setup](../docs/guides/oci-templates.md) for Registry Secrets and runtime prerequisites.
|
|
229
|
+
|
|
226
230
|
### Manage snapshots
|
|
227
231
|
|
|
228
232
|
Snapshots capture a sandbox's state and can back new sandboxes.
|
|
@@ -278,7 +282,7 @@ osb command logs <sandbox-id> <execution-id> -o json
|
|
|
278
282
|
|
|
279
283
|
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
284
|
|
|
281
|
-
By default
|
|
285
|
+
By default each payload word after `--` is quoted as a single shell token and the tokens are joined into one shell command line that the sandbox's shell runs. Shell syntax such as pipelines or redirection must therefore ride inside an explicit word (`-- sh -c 'echo hi > /tmp/x'`); an unquoted `$VAR` is expanded by your own terminal before the CLI sees it, while a quoted one reaches the sandbox shell literally. 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:
|
|
282
286
|
|
|
283
287
|
```bash
|
|
284
288
|
osb command run <sandbox-id> -o raw --argv -- python3 -c "import sys; print(sys.argv[1:])" "a b" '$HOME' "x'y" ""
|
|
@@ -500,7 +504,8 @@ api_key = "your-api-key"
|
|
|
500
504
|
domain = "localhost:8080"
|
|
501
505
|
protocol = "http"
|
|
502
506
|
request_timeout = 30
|
|
503
|
-
use_server_proxy
|
|
507
|
+
# use_server_proxy is deprecated (maintenance mode); prefer the ingress gateway for data-plane traffic.
|
|
508
|
+
# use_server_proxy = false
|
|
504
509
|
|
|
505
510
|
[output]
|
|
506
511
|
color = true
|
|
@@ -44,7 +44,7 @@ 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.2.
|
|
47
|
+
"opensandbox>=1.2.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",
|
|
@@ -147,9 +147,12 @@ def _handle_execution_error(
|
|
|
147
147
|
help="Run a command in a sandbox. Use `--` before the sandbox command payload.",
|
|
148
148
|
epilog=(
|
|
149
149
|
"Separator rule: use `--` before the sandbox command payload.\n\n"
|
|
150
|
-
"By default
|
|
151
|
-
"
|
|
152
|
-
"
|
|
150
|
+
"By default each payload word is quoted as a single shell token and "
|
|
151
|
+
"the tokens are joined into one shell command line, so shell syntax "
|
|
152
|
+
"such as redirects or pipes must ride inside an explicit word "
|
|
153
|
+
"(for example: -- sh -c 'echo hi > /tmp/x'). Add `--argv` to pass "
|
|
154
|
+
"the arguments to the executable as-is (no shell), so literal "
|
|
155
|
+
"`$HOME`, quotes, and empty strings are preserved; this needs a "
|
|
153
156
|
"sandbox image whose execd accepts argv requests."
|
|
154
157
|
),
|
|
155
158
|
)
|
|
@@ -26,7 +26,7 @@ from tomlkit.exceptions import ParseError
|
|
|
26
26
|
from tomlkit.items import InlineTable, Table
|
|
27
27
|
|
|
28
28
|
from opensandbox_cli.client import ClientContext
|
|
29
|
-
from opensandbox_cli.config import init_config_file, resolve_config
|
|
29
|
+
from opensandbox_cli.config import STRING_KEYS, init_config_file, resolve_config
|
|
30
30
|
from opensandbox_cli.utils import handle_errors, output_option, prepare_output
|
|
31
31
|
|
|
32
32
|
|
|
@@ -53,7 +53,8 @@ def config_init(obj: ClientContext, force: bool, output_format: str | None) -> N
|
|
|
53
53
|
path = init_config_file(obj.config_path, force=force)
|
|
54
54
|
output.success(f"Config file created: {path}")
|
|
55
55
|
except FileExistsError as exc:
|
|
56
|
-
output.
|
|
56
|
+
output.error(str(exc))
|
|
57
|
+
raise click.exceptions.Exit(1) from None
|
|
57
58
|
|
|
58
59
|
|
|
59
60
|
_SENSITIVE_CONFIG_KEYS = {
|
|
@@ -166,7 +167,7 @@ def config_set(
|
|
|
166
167
|
if not isinstance(table, (Table, InlineTable, OutOfOrderTableProxy)):
|
|
167
168
|
raise click.ClickException(f"Cannot set {key}: '{section}' is not a TOML table.")
|
|
168
169
|
|
|
169
|
-
table[field] = _parse_toml_value(value)
|
|
170
|
+
table[field] = value if key in STRING_KEYS else _parse_toml_value(value)
|
|
170
171
|
path.write_text(tomlkit.dumps(document), encoding="utf-8")
|
|
171
172
|
|
|
172
173
|
obj.output.success(f"Set {key} = {value}")
|
|
@@ -26,7 +26,7 @@ from opensandbox_cli.utils import handle_errors, output_option, prepare_output
|
|
|
26
26
|
def _parse_rule(value: str) -> NetworkRule:
|
|
27
27
|
"""Parse ACTION=TARGET into a NetworkRule."""
|
|
28
28
|
action, sep, target = value.partition("=")
|
|
29
|
-
if not sep:
|
|
29
|
+
if not sep or not target.strip():
|
|
30
30
|
raise click.BadParameter(
|
|
31
31
|
f"Invalid rule '{value}'. Use ACTION=TARGET, for example allow=pypi.org."
|
|
32
32
|
)
|
|
@@ -1015,12 +1015,20 @@ def _parse_metric_stream_line(line: str) -> tuple[SandboxMetrics | None, str | N
|
|
|
1015
1015
|
if not payload:
|
|
1016
1016
|
return None, None
|
|
1017
1017
|
|
|
1018
|
-
|
|
1018
|
+
try:
|
|
1019
|
+
decoded: Any = json.loads(payload)
|
|
1020
|
+
except json.JSONDecodeError:
|
|
1021
|
+
return None, "Metrics stream error: invalid JSON"
|
|
1022
|
+
|
|
1019
1023
|
if isinstance(decoded, dict) and decoded.get("error"):
|
|
1020
1024
|
return None, f"Metrics stream error: {decoded['error']}"
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1025
|
+
|
|
1026
|
+
try:
|
|
1027
|
+
if isinstance(decoded, dict) and "cpu_used_pct" in decoded:
|
|
1028
|
+
return MetricsModelConverter.to_sandbox_metrics(Metrics.from_dict(decoded)), None
|
|
1029
|
+
return SandboxMetrics.model_validate(decoded), None
|
|
1030
|
+
except (KeyError, TypeError, ValueError, ValidationError) as exc:
|
|
1031
|
+
return None, f"Metrics stream error: {exc}"
|
|
1024
1032
|
|
|
1025
1033
|
|
|
1026
1034
|
def _describe_create_timeout(
|
|
@@ -55,7 +55,7 @@ def template_group(ctx: click.Context) -> None:
|
|
|
55
55
|
"--publish",
|
|
56
56
|
"-p",
|
|
57
57
|
default=None,
|
|
58
|
-
help="
|
|
58
|
+
help="Publish target: s3://bucket/publish (native) or oci://registry/repository (overlaybd, no tag or digest). Required unless --file is used.",
|
|
59
59
|
)
|
|
60
60
|
@click.option(
|
|
61
61
|
"--file",
|
|
@@ -233,6 +233,9 @@ def template_get(obj, template_id: str, output_format: str | None) -> None:
|
|
|
233
233
|
status_val = d.get("status")
|
|
234
234
|
if isinstance(status_val, dict):
|
|
235
235
|
d["status"] = status_val.get("phase", str(status_val))
|
|
236
|
+
# model_dump uses the field name; accept the wire alias too.
|
|
237
|
+
manifest_ref = status_val.get("manifest_ref", status_val.get("manifestRef"))
|
|
238
|
+
d["manifest_ref"] = manifest_ref
|
|
236
239
|
if status_val.get("message"):
|
|
237
240
|
d["status_message"] = status_val["message"]
|
|
238
241
|
readiness_val = d.get("readiness")
|
|
@@ -259,9 +259,12 @@ async def _connect_and_stream(
|
|
|
259
259
|
stdout_write: WriteFn,
|
|
260
260
|
get_size: SizeFn,
|
|
261
261
|
error_write: WriteFn | None = None,
|
|
262
|
+
on_connected: Callable[[], None] | None = None,
|
|
262
263
|
) -> tuple[int | None, list[str]]:
|
|
263
264
|
"""Connect to the PTY WebSocket, drive the relay loop, handle resize."""
|
|
264
265
|
async with websockets.connect(ws_url, additional_headers=headers or None) as ws:
|
|
266
|
+
if on_connected is not None:
|
|
267
|
+
on_connected()
|
|
265
268
|
# websockets forbids concurrent sends: stdin frames and SIGWINCH
|
|
266
269
|
# resize frames share one lock, and fire-and-forget resize tasks get
|
|
267
270
|
# their results consumed so failures never surface as task warnings.
|
|
@@ -364,6 +367,12 @@ def terminal(
|
|
|
364
367
|
exit_code: int | None = None
|
|
365
368
|
errors: list[str] = []
|
|
366
369
|
teardown = False
|
|
370
|
+
connected = False
|
|
371
|
+
|
|
372
|
+
def mark_connected() -> None:
|
|
373
|
+
nonlocal connected
|
|
374
|
+
connected = True
|
|
375
|
+
|
|
367
376
|
try:
|
|
368
377
|
with _raw_mode(stdin_fd):
|
|
369
378
|
exit_code, errors = asyncio.run(
|
|
@@ -374,6 +383,7 @@ def terminal(
|
|
|
374
383
|
stdout_write=stdout_write,
|
|
375
384
|
get_size=get_size,
|
|
376
385
|
error_write=report_error,
|
|
386
|
+
on_connected=mark_connected,
|
|
377
387
|
)
|
|
378
388
|
)
|
|
379
389
|
teardown = exit_code is not None or bool(errors)
|
|
@@ -385,9 +395,10 @@ def terminal(
|
|
|
385
395
|
raise click.ClickException(f"Terminal connection failed: {exc}") from exc
|
|
386
396
|
finally:
|
|
387
397
|
# Clean exit tears the session down, as does a reported failure (e.g.
|
|
388
|
-
# START_FAILED: the shell never ran, so there is nothing to preserve)
|
|
389
|
-
#
|
|
390
|
-
|
|
398
|
+
# START_FAILED: the shell never ran, so there is nothing to preserve)
|
|
399
|
+
# or a WebSocket that never opened (the shell starts on the first
|
|
400
|
+
# one). A lost connection on a live shell is the one case worth keeping.
|
|
401
|
+
if teardown or not connected:
|
|
391
402
|
_delete_pty_session(http_base, headers, session_id, timeout=timeout)
|
|
392
403
|
|
|
393
404
|
if exit_code is None:
|
|
@@ -28,6 +28,8 @@ import sys
|
|
|
28
28
|
from pathlib import Path
|
|
29
29
|
from typing import Any
|
|
30
30
|
|
|
31
|
+
import click
|
|
32
|
+
|
|
31
33
|
if sys.version_info >= (3, 11):
|
|
32
34
|
import tomllib
|
|
33
35
|
else:
|
|
@@ -49,6 +51,7 @@ DEFAULT_CONFIG_TEMPLATE = """\
|
|
|
49
51
|
# domain = "localhost:8080"
|
|
50
52
|
# protocol = "http"
|
|
51
53
|
# request_timeout = 30
|
|
54
|
+
# use_server_proxy is deprecated (maintenance mode); prefer the ingress gateway for data-plane traffic.
|
|
52
55
|
# use_server_proxy = false
|
|
53
56
|
|
|
54
57
|
[output]
|
|
@@ -64,14 +67,38 @@ def load_config_file(config_path: Path | None = None) -> dict[str, Any]:
|
|
|
64
67
|
"""Load and parse the TOML config file.
|
|
65
68
|
|
|
66
69
|
Returns an empty dict if the file doesn't exist or tomllib is unavailable.
|
|
70
|
+
Raises ``click.ClickException`` if the file cannot be read or parsed.
|
|
67
71
|
"""
|
|
68
72
|
path = config_path or DEFAULT_CONFIG_PATH
|
|
69
|
-
if not path.exists():
|
|
70
|
-
return {}
|
|
71
73
|
if tomllib is None:
|
|
72
74
|
return {}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
# Open directly instead of checking path.exists() first: exists() raises
|
|
76
|
+
# PermissionError for a path under a directory that cannot be searched.
|
|
77
|
+
try:
|
|
78
|
+
with open(path, "rb") as f:
|
|
79
|
+
return tomllib.load(f)
|
|
80
|
+
except (FileNotFoundError, NotADirectoryError):
|
|
81
|
+
return {}
|
|
82
|
+
except OSError as exc:
|
|
83
|
+
raise click.ClickException(f"Cannot read config file {path}: {exc}") from exc
|
|
84
|
+
except (tomllib.TOMLDecodeError, UnicodeDecodeError) as exc:
|
|
85
|
+
raise click.ClickException(
|
|
86
|
+
f"Config file is not valid TOML ({path}): {exc}"
|
|
87
|
+
) from exc
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# Config-file keys resolve_config reads as text. `config set` stores their
|
|
91
|
+
# values verbatim: inferring a TOML number would rewrite an API key such as
|
|
92
|
+
# "007123" as 7123.
|
|
93
|
+
STRING_KEYS = frozenset(
|
|
94
|
+
{
|
|
95
|
+
"connection.api_key",
|
|
96
|
+
"connection.domain",
|
|
97
|
+
"connection.protocol",
|
|
98
|
+
"defaults.image",
|
|
99
|
+
"defaults.timeout",
|
|
100
|
+
}
|
|
101
|
+
)
|
|
75
102
|
|
|
76
103
|
|
|
77
104
|
def resolve_config(
|
|
@@ -92,10 +119,15 @@ def resolve_config(
|
|
|
92
119
|
declared type.
|
|
93
120
|
|
|
94
121
|
Keys returned:
|
|
95
|
-
- api_key, domain, protocol, request_timeout (int seconds), use_server_proxy (bool)
|
|
122
|
+
- api_key, domain, protocol, request_timeout (int seconds), use_server_proxy (bool; deprecated)
|
|
96
123
|
- default_image, default_timeout (str like "10m")
|
|
97
124
|
"""
|
|
98
125
|
file_cfg = load_config_file(config_path)
|
|
126
|
+
for section in ("connection", "output", "defaults"):
|
|
127
|
+
if section in file_cfg and not isinstance(file_cfg[section], dict):
|
|
128
|
+
raise click.ClickException(
|
|
129
|
+
f"Config section '{section}' must be a TOML table (use [{section}])."
|
|
130
|
+
)
|
|
99
131
|
conn = file_cfg.get("connection", {})
|
|
100
132
|
output_cfg = file_cfg.get("output", {})
|
|
101
133
|
defaults = file_cfg.get("defaults", {})
|
|
@@ -111,10 +143,11 @@ def resolve_config(
|
|
|
111
143
|
or os.getenv("OPEN_SANDBOX_PROTOCOL")
|
|
112
144
|
or _str_or_none(conn.get("protocol"))
|
|
113
145
|
or "http",
|
|
114
|
-
"request_timeout":
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
146
|
+
"request_timeout": _request_timeout_seconds(
|
|
147
|
+
cli_timeout,
|
|
148
|
+
_int_or_none(os.getenv("OPEN_SANDBOX_REQUEST_TIMEOUT")),
|
|
149
|
+
_int_or_none(conn.get("request_timeout")),
|
|
150
|
+
),
|
|
118
151
|
"use_server_proxy": _coalesce(
|
|
119
152
|
cli_use_server_proxy,
|
|
120
153
|
_bool_or_none(os.getenv("OPEN_SANDBOX_USE_SERVER_PROXY")),
|
|
@@ -139,6 +172,26 @@ def init_config_file(config_path: Path | None = None, *, force: bool = False) ->
|
|
|
139
172
|
return path
|
|
140
173
|
|
|
141
174
|
|
|
175
|
+
def _request_timeout_seconds(*candidates: int | None) -> int:
|
|
176
|
+
"""Return the first explicit request timeout, in seconds.
|
|
177
|
+
|
|
178
|
+
``None`` means that source did not set a timeout. Zero and negative values
|
|
179
|
+
are explicit and invalid: they must not fall through to a later source or
|
|
180
|
+
the 30 second default. ``--request-timeout 0`` used to be ignored because
|
|
181
|
+
``0`` is falsy.
|
|
182
|
+
"""
|
|
183
|
+
for value in candidates:
|
|
184
|
+
if value is None:
|
|
185
|
+
continue
|
|
186
|
+
if value <= 0:
|
|
187
|
+
raise click.ClickException(
|
|
188
|
+
"Request timeout must be a positive number of seconds, "
|
|
189
|
+
f"got {value}."
|
|
190
|
+
)
|
|
191
|
+
return value
|
|
192
|
+
return 30
|
|
193
|
+
|
|
194
|
+
|
|
142
195
|
def _int_or_none(value: Any) -> int | None:
|
|
143
196
|
"""Coerce a configured value to ``int``, or ``None`` when it is not numeric."""
|
|
144
197
|
if value is None or isinstance(value, bool):
|
|
@@ -36,7 +36,7 @@ from opensandbox_cli.commands.skills import skills_group
|
|
|
36
36
|
from opensandbox_cli.commands.snapshot import snapshot_group
|
|
37
37
|
from opensandbox_cli.commands.template import template_group
|
|
38
38
|
from opensandbox_cli.commands.terminal import terminal
|
|
39
|
-
from opensandbox_cli.config import resolve_config
|
|
39
|
+
from opensandbox_cli.config import _request_timeout_seconds, resolve_config
|
|
40
40
|
|
|
41
41
|
BANNER = r"""[bold cyan]
|
|
42
42
|
____ _____ _ _
|
|
@@ -104,7 +104,10 @@ class BannerGroup(click.Group):
|
|
|
104
104
|
@click.option(
|
|
105
105
|
"--use-server-proxy/--no-use-server-proxy",
|
|
106
106
|
default=None,
|
|
107
|
-
help=
|
|
107
|
+
help=(
|
|
108
|
+
"DEPRECATED: the server proxy is in maintenance mode (security/bugfix only); "
|
|
109
|
+
"route data-plane traffic through the ingress gateway instead."
|
|
110
|
+
),
|
|
108
111
|
)
|
|
109
112
|
@click.option("--config", "config_path", type=click.Path(exists=False, path_type=Path), default=None, help="Config file path.")
|
|
110
113
|
@click.option("-v", "--verbose", is_flag=True, default=False, help="Enable verbose/debug output.")
|
|
@@ -141,7 +144,7 @@ def cli(
|
|
|
141
144
|
"api_key": api_key,
|
|
142
145
|
"domain": domain,
|
|
143
146
|
"protocol": protocol or "http",
|
|
144
|
-
"request_timeout": request_timeout
|
|
147
|
+
"request_timeout": _request_timeout_seconds(request_timeout),
|
|
145
148
|
"use_server_proxy": use_server_proxy if use_server_proxy is not None else False,
|
|
146
149
|
"color": True,
|
|
147
150
|
"default_image": None,
|
|
@@ -158,6 +161,14 @@ def cli(
|
|
|
158
161
|
)
|
|
159
162
|
resolved["color"] = not no_color and resolved.get("color", True)
|
|
160
163
|
|
|
164
|
+
if ctx.invoked_subcommand != "config" and resolved.get("use_server_proxy"):
|
|
165
|
+
click.echo(
|
|
166
|
+
"Warning: connection.use_server_proxy is deprecated: the server proxy "
|
|
167
|
+
"data path is in maintenance mode (security and bug fixes only); route "
|
|
168
|
+
"data-plane traffic through the ingress gateway instead.",
|
|
169
|
+
err=True,
|
|
170
|
+
)
|
|
171
|
+
|
|
161
172
|
effective_config_path = config_path or Path.home() / ".opensandbox" / "config.toml"
|
|
162
173
|
ctx.obj = ClientContext(
|
|
163
174
|
resolved_config=resolved,
|
|
@@ -32,6 +32,7 @@ except ImportError: # pragma: no cover
|
|
|
32
32
|
from pydantic import BaseModel
|
|
33
33
|
from rich import box
|
|
34
34
|
from rich.console import Console
|
|
35
|
+
from rich.markup import escape
|
|
35
36
|
from rich.panel import Panel
|
|
36
37
|
from rich.status import Status
|
|
37
38
|
from rich.table import Table
|
|
@@ -116,7 +117,7 @@ class OutputFormatter:
|
|
|
116
117
|
return
|
|
117
118
|
if self.color:
|
|
118
119
|
console = self._err_console if stderr else self.console
|
|
119
|
-
console.print(f" [bold {style}]{icon} {msg}[/]")
|
|
120
|
+
console.print(f" [bold {style}]{icon} {escape(msg)}[/]")
|
|
120
121
|
else:
|
|
121
122
|
click.echo(f"{plain}: {msg}", err=stderr)
|
|
122
123
|
|
|
@@ -143,7 +144,7 @@ class OutputFormatter:
|
|
|
143
144
|
self._err_console.print(f" [bold red]{title}[/]")
|
|
144
145
|
self._err_console.print(f" [dim]{'─' * (len(title) + 2)}[/]")
|
|
145
146
|
for line in msg.splitlines():
|
|
146
|
-
self._err_console.print(f" {line}")
|
|
147
|
+
self._err_console.print(f" {escape(line)}")
|
|
147
148
|
self._err_console.print()
|
|
148
149
|
else:
|
|
149
150
|
click.echo(f"ERROR [{title}]: {msg}", err=True)
|
|
@@ -152,7 +153,7 @@ class OutputFormatter:
|
|
|
152
153
|
def spinner(self, msg: str) -> Generator[Status, None, None]:
|
|
153
154
|
"""Context manager that shows a spinner while work is in progress."""
|
|
154
155
|
if self.color and self.fmt == "table":
|
|
155
|
-
with self._err_console.status(f"[bold cyan]⏳ {msg}[/]", spinner="dots") as status:
|
|
156
|
+
with self._err_console.status(f"[bold cyan]⏳ {escape(msg)}[/]", spinner="dots") as status:
|
|
156
157
|
yield status
|
|
157
158
|
else:
|
|
158
159
|
yield None # type: ignore[arg-type]
|
|
@@ -161,7 +162,7 @@ class OutputFormatter:
|
|
|
161
162
|
"""Print content inside a styled panel."""
|
|
162
163
|
if self.color:
|
|
163
164
|
self.console.print(Panel(
|
|
164
|
-
content,
|
|
165
|
+
Text(content),
|
|
165
166
|
title=title,
|
|
166
167
|
title_align="left",
|
|
167
168
|
border_style=style,
|
|
@@ -187,7 +188,7 @@ class OutputFormatter:
|
|
|
187
188
|
self.console.print(f" [bold green]✓ {title}[/]")
|
|
188
189
|
self.console.print(f" [dim]{'─' * (len(title) + 2)}[/]")
|
|
189
190
|
for k, v in data.items():
|
|
190
|
-
self.console.print(f" [bold]{k}:[/] [cyan]{v}[/]")
|
|
191
|
+
self.console.print(f" [bold]{escape(str(k))}:[/] [cyan]{escape(str(v))}[/]")
|
|
191
192
|
self.console.print()
|
|
192
193
|
else:
|
|
193
194
|
click.echo(f"--- {title} ---")
|
|
@@ -316,10 +317,11 @@ class OutputFormatter:
|
|
|
316
317
|
for row in rows:
|
|
317
318
|
cells: list[Text | str] = []
|
|
318
319
|
for col in columns:
|
|
319
|
-
|
|
320
|
+
raw = row.get(col)
|
|
321
|
+
val = "-" if raw is None else str(raw)
|
|
320
322
|
if col in _STATUS_COLUMNS:
|
|
321
323
|
cells.append(_style_value(col, val))
|
|
322
324
|
else:
|
|
323
|
-
cells.append(val)
|
|
325
|
+
cells.append(Text(val))
|
|
324
326
|
table.add_row(*cells)
|
|
325
327
|
self.console.print(table)
|
|
@@ -140,7 +140,8 @@ If the user does not specify an image, recommend one that matches the runtime th
|
|
|
140
140
|
Templates are golden-image builds. The build is asynchronous: `template create` returns a template in `Pending`; poll `template get` until the phase is `Succeeded` (or `Failed`).
|
|
141
141
|
|
|
142
142
|
```bash
|
|
143
|
-
osb template create --image python:3.12 --publish s3://bucket/publish -o json
|
|
143
|
+
osb template create --image python:3.12 --publish s3://bucket/publish --format native -o json
|
|
144
|
+
osb template create --image python:3.12 --publish oci://registry.example.com/sandbox/templates --format overlaybd -o json
|
|
144
145
|
osb template create -f template-request.json -o json
|
|
145
146
|
osb template get <template-id> -o json
|
|
146
147
|
osb template list -o json
|
|
@@ -149,6 +150,8 @@ osb template delete <template-id> -o json
|
|
|
149
150
|
|
|
150
151
|
Useful `template create` options: `--resource cpu=1 memory=512Mi disk=2Gi`, `--entrypoint` (repeat per argv item), `--env KEY=VALUE`, `--metadata KEY=VALUE`, `--format native|overlaybd`, `--readiness-probe tcp://127.0.0.1:44772`, `--warmup-seconds 60`. `-f <request-file>` reads the full request as public `CreateTemplateRequest` JSON (camelCase: `image`, `publish`, `resourceLimits`, `entrypoint`, `env`, `metadata`, `format`, `readiness.probe`, `readiness.warmupSeconds`), mutually exclusive with those flags.
|
|
151
152
|
|
|
153
|
+
OCI publication requires a repository-only target without a tag or digest, `overlaybd` format, and a server-configured `.dockerconfigjson` Secret (`kubernetes.template_oci_publish_secret`). Sandbox creation also requires an OCI-enabled Fast Sandbox pool and the OverlayBD node daemon. S3 publication uses `native`. Wait for `Succeeded` before creating a sandbox; the server uses the immutable OCI manifest reference for OCI templates.
|
|
154
|
+
|
|
152
155
|
Create a sandbox from a succeeded template:
|
|
153
156
|
|
|
154
157
|
```bash
|
|
@@ -320,7 +323,7 @@ osb sandbox health <sandbox-id> -o json
|
|
|
320
323
|
Build a template, wait for it, and create from it:
|
|
321
324
|
|
|
322
325
|
```bash
|
|
323
|
-
osb template create --image python:3.12 --publish s3://bucket/publish -o json
|
|
326
|
+
osb template create --image python:3.12 --publish s3://bucket/publish --format native -o json
|
|
324
327
|
osb template get <template-id> -o json
|
|
325
328
|
osb sandbox create --template <template-id> --timeout 30m -o json
|
|
326
329
|
osb sandbox health <sandbox-id> -o json
|
|
@@ -86,12 +86,9 @@ class DurationType(click.ParamType):
|
|
|
86
86
|
return value
|
|
87
87
|
try:
|
|
88
88
|
return parse_duration(value)
|
|
89
|
-
except click.BadParameter:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
param,
|
|
93
|
-
ctx,
|
|
94
|
-
)
|
|
89
|
+
except click.BadParameter as exc:
|
|
90
|
+
# Keep the specific reason (empty, malformed, out of range).
|
|
91
|
+
self.fail(exc.format_message(), param, ctx)
|
|
95
92
|
|
|
96
93
|
|
|
97
94
|
DURATION = DurationType()
|
|
@@ -205,7 +202,7 @@ def handle_errors(fn): # type: ignore[no-untyped-def]
|
|
|
205
202
|
output.error_panel(str(exc), title="Sandbox Error")
|
|
206
203
|
else:
|
|
207
204
|
output.error_panel(
|
|
208
|
-
f"{
|
|
205
|
+
f"{exc}\n\nType: {type(exc).__qualname__}",
|
|
209
206
|
title=type(exc).__name__,
|
|
210
207
|
)
|
|
211
208
|
else:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{opensandbox_cli-1.2.0 → opensandbox_cli-1.2.1}/src/opensandbox_cli/commands/credential_vault.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|