mosaic-sandbox 0.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.
- mosaic_sandbox-0.1.0/PKG-INFO +67 -0
- mosaic_sandbox-0.1.0/README.md +45 -0
- mosaic_sandbox-0.1.0/mosaic_sandbox/__init__.py +6 -0
- mosaic_sandbox-0.1.0/mosaic_sandbox/cli.py +372 -0
- mosaic_sandbox-0.1.0/mosaic_sandbox/client.py +505 -0
- mosaic_sandbox-0.1.0/mosaic_sandbox/mcp_server.py +246 -0
- mosaic_sandbox-0.1.0/mosaic_sandbox/migration.py +100 -0
- mosaic_sandbox-0.1.0/mosaic_sandbox.egg-info/PKG-INFO +67 -0
- mosaic_sandbox-0.1.0/mosaic_sandbox.egg-info/SOURCES.txt +17 -0
- mosaic_sandbox-0.1.0/mosaic_sandbox.egg-info/dependency_links.txt +1 -0
- mosaic_sandbox-0.1.0/mosaic_sandbox.egg-info/entry_points.txt +2 -0
- mosaic_sandbox-0.1.0/mosaic_sandbox.egg-info/top_level.txt +1 -0
- mosaic_sandbox-0.1.0/pyproject.toml +37 -0
- mosaic_sandbox-0.1.0/setup.cfg +4 -0
- mosaic_sandbox-0.1.0/tests/test_benchmark_matrix.py +62 -0
- mosaic_sandbox-0.1.0/tests/test_cli.py +71 -0
- mosaic_sandbox-0.1.0/tests/test_client.py +108 -0
- mosaic_sandbox-0.1.0/tests/test_guest_ssh_image.py +22 -0
- mosaic_sandbox-0.1.0/tests/test_migration.py +45 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mosaic-sandbox
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for Mosaic Sandbox (MAR)
|
|
5
|
+
Author: Mosaic
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://sandbox.mosaicos.com
|
|
8
|
+
Project-URL: Documentation, https://sandbox.mosaicos.com/docs/
|
|
9
|
+
Project-URL: Repository, https://github.com/nitinrao/Mosaic-Sandbox
|
|
10
|
+
Project-URL: Issues, https://github.com/nitinrao/Mosaic-Sandbox/issues
|
|
11
|
+
Keywords: sandbox,firecracker,microvm,agent,cli,mcp,ssh
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# Mosaic Sandbox Python SDK
|
|
24
|
+
|
|
25
|
+
`mosaic-sandbox` is the Python SDK and CLI for Mosaic Sandbox, a Firecracker-based
|
|
26
|
+
runtime for coding agents.
|
|
27
|
+
|
|
28
|
+
Install:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
python -m pip install mosaic-sandbox
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Set your endpoint and API token:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
export MAR_ENDPOINT=https://sandbox.mosaicos.com
|
|
38
|
+
export MAR_API_TOKEN=msk_live_...
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Run the CLI smoke test:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
mar doctor
|
|
45
|
+
mar run --template node-20 node -v
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Python example:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from mosaic_sandbox import Sandbox
|
|
52
|
+
|
|
53
|
+
sbx = Sandbox.create(
|
|
54
|
+
template="node-20",
|
|
55
|
+
endpoint="https://sandbox.mosaicos.com",
|
|
56
|
+
api_token="msk_live_..."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
result = sbx.run_command("node -v")
|
|
61
|
+
print(result.stdout)
|
|
62
|
+
print(result.tti_ms)
|
|
63
|
+
finally:
|
|
64
|
+
sbx.destroy()
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Docs: <https://sandbox.mosaicos.com/docs/>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Mosaic Sandbox Python SDK
|
|
2
|
+
|
|
3
|
+
`mosaic-sandbox` is the Python SDK and CLI for Mosaic Sandbox, a Firecracker-based
|
|
4
|
+
runtime for coding agents.
|
|
5
|
+
|
|
6
|
+
Install:
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
python -m pip install mosaic-sandbox
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Set your endpoint and API token:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
export MAR_ENDPOINT=https://sandbox.mosaicos.com
|
|
16
|
+
export MAR_API_TOKEN=msk_live_...
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Run the CLI smoke test:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
mar doctor
|
|
23
|
+
mar run --template node-20 node -v
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Python example:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from mosaic_sandbox import Sandbox
|
|
30
|
+
|
|
31
|
+
sbx = Sandbox.create(
|
|
32
|
+
template="node-20",
|
|
33
|
+
endpoint="https://sandbox.mosaicos.com",
|
|
34
|
+
api_token="msk_live_..."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
result = sbx.run_command("node -v")
|
|
39
|
+
print(result.stdout)
|
|
40
|
+
print(result.tti_ms)
|
|
41
|
+
finally:
|
|
42
|
+
sbx.destroy()
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Docs: <https://sandbox.mosaicos.com/docs/>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Low-overhead client for the Mosaic Sandbox daemon."""
|
|
2
|
+
__version__ = "0.1.0"
|
|
3
|
+
|
|
4
|
+
from .client import AsyncProcessHandle, AsyncSandbox, ExecutionResult, FileInfo, Preview, ProcessHandle, ProcessInfo, Sandbox, Snapshot
|
|
5
|
+
|
|
6
|
+
__all__ = ["AsyncProcessHandle", "AsyncSandbox", "ExecutionResult", "FileInfo", "Preview", "ProcessHandle", "ProcessInfo", "Sandbox", "Snapshot", "__version__"]
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import shlex
|
|
7
|
+
import sys
|
|
8
|
+
from dataclasses import asdict
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from . import __version__
|
|
13
|
+
from .client import ProcessHandle, ProcessInfo, Sandbox
|
|
14
|
+
from .migration import PROVIDER_INDEX, assess as assess_migration
|
|
15
|
+
from .migration import list_providers, plan as plan_migration
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
DEFAULT_ENDPOINT = os.environ.get("MAR_ENDPOINT", "http://127.0.0.1:8080")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _request(args: argparse.Namespace, path: str, payload: dict[str, Any] | None = None, method: str = "GET") -> dict[str, Any]:
|
|
22
|
+
return Sandbox._request(
|
|
23
|
+
args.endpoint,
|
|
24
|
+
path,
|
|
25
|
+
payload or {},
|
|
26
|
+
method=method,
|
|
27
|
+
api_token=args.token,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _emit(value: Any, *, raw: bool = False) -> None:
|
|
32
|
+
if raw and isinstance(value, str):
|
|
33
|
+
sys.stdout.write(value)
|
|
34
|
+
if value and not value.endswith("\n"):
|
|
35
|
+
sys.stdout.write("\n")
|
|
36
|
+
return
|
|
37
|
+
print(json.dumps(value, indent=2, sort_keys=True))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _sandbox(args: argparse.Namespace) -> Sandbox:
|
|
41
|
+
return Sandbox(args.id, args.endpoint, args.token)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _command(parts: list[str]) -> str:
|
|
45
|
+
if parts[:1] == ["--"]:
|
|
46
|
+
parts = parts[1:]
|
|
47
|
+
if not parts:
|
|
48
|
+
raise ValueError("a command is required")
|
|
49
|
+
return shlex.join(parts)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _load_manifest(path: str) -> dict[str, Any]:
|
|
53
|
+
raw = sys.stdin.read() if path == "-" else Path(path).read_text()
|
|
54
|
+
value = json.loads(raw)
|
|
55
|
+
if not isinstance(value, dict):
|
|
56
|
+
raise ValueError("migration manifest must be a JSON object")
|
|
57
|
+
return value
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _ssh_key(args: argparse.Namespace) -> str | None:
|
|
61
|
+
path = getattr(args, "ssh_public_key_file", None)
|
|
62
|
+
return Path(path).expanduser().read_text().strip() if path else None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _sandbox_created(sandbox: Sandbox) -> dict[str, Any]:
|
|
66
|
+
return {
|
|
67
|
+
"id": sandbox.id,
|
|
68
|
+
"state": "running",
|
|
69
|
+
"tti_ms": sandbox._create_tti_ms,
|
|
70
|
+
"ssh_connection_string": sandbox.ssh_connection_string,
|
|
71
|
+
"ssh_host": sandbox.ssh_host,
|
|
72
|
+
"ssh_port": sandbox.ssh_port,
|
|
73
|
+
"ssh_private_key_path": sandbox.ssh_private_key_path,
|
|
74
|
+
"ssh_known_hosts_path": sandbox.ssh_known_hosts_path,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _agent_config(client: str, endpoint: str) -> str:
|
|
79
|
+
if client == "codex":
|
|
80
|
+
return (
|
|
81
|
+
"[mcp_servers.mosaic_sandbox]\n"
|
|
82
|
+
'command = "mar"\n'
|
|
83
|
+
'args = ["mcp"]\n\n'
|
|
84
|
+
"[mcp_servers.mosaic_sandbox.env]\n"
|
|
85
|
+
f'MAR_ENDPOINT = "{endpoint}"\n'
|
|
86
|
+
)
|
|
87
|
+
config = {
|
|
88
|
+
"mcpServers": {
|
|
89
|
+
"mosaic-sandbox": {
|
|
90
|
+
"command": "mar",
|
|
91
|
+
"args": ["mcp"],
|
|
92
|
+
"env": {"MAR_ENDPOINT": endpoint},
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if client == "cursor":
|
|
97
|
+
return json.dumps(config, indent=2) + "\n"
|
|
98
|
+
payload = json.dumps(
|
|
99
|
+
{
|
|
100
|
+
"type": "stdio",
|
|
101
|
+
"command": "mar",
|
|
102
|
+
"args": ["mcp"],
|
|
103
|
+
"env": {"MAR_ENDPOINT": endpoint},
|
|
104
|
+
},
|
|
105
|
+
separators=(",", ":"),
|
|
106
|
+
)
|
|
107
|
+
return (
|
|
108
|
+
"claude mcp add-json mosaic-sandbox "
|
|
109
|
+
+ shlex.quote(payload)
|
|
110
|
+
+ " --scope project\n"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
115
|
+
parser = argparse.ArgumentParser(
|
|
116
|
+
prog="mar",
|
|
117
|
+
description="Mosaic Sandbox CLI and MCP server for coding agents.",
|
|
118
|
+
)
|
|
119
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
120
|
+
parser.add_argument("--endpoint", default=DEFAULT_ENDPOINT, help="MAR REST endpoint")
|
|
121
|
+
parser.add_argument("--token", default=os.environ.get("MAR_API_TOKEN"), help="Bearer token; prefer MAR_API_TOKEN")
|
|
122
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
123
|
+
|
|
124
|
+
create = sub.add_parser("create", help="Create a sandbox")
|
|
125
|
+
create.add_argument("--template", default="node-20")
|
|
126
|
+
create.add_argument("--memory-mb", type=int, default=4096)
|
|
127
|
+
create.add_argument("--vcpu", type=int, default=2)
|
|
128
|
+
create.add_argument("--no-ssh", action="store_true", help="Disable the default SSH endpoint")
|
|
129
|
+
create.add_argument("--ssh-public-key-file", help="Public key to install instead of generating a temporary keypair")
|
|
130
|
+
create.add_argument("--ssh-private-key", help="Local private key matching --ssh-public-key-file")
|
|
131
|
+
|
|
132
|
+
execute = sub.add_parser("exec", help="Execute a command in an existing sandbox")
|
|
133
|
+
execute.add_argument("--cwd")
|
|
134
|
+
execute.add_argument("--env", action="append", default=[], metavar="NAME=VALUE")
|
|
135
|
+
execute.add_argument("--stdin")
|
|
136
|
+
execute.add_argument("id")
|
|
137
|
+
execute.add_argument("cmd", nargs=argparse.REMAINDER)
|
|
138
|
+
execute.add_argument("--timeout-ms", type=int)
|
|
139
|
+
|
|
140
|
+
run = sub.add_parser("run", help="Create, execute, and destroy a sandbox")
|
|
141
|
+
run.add_argument("--template", default="node-20")
|
|
142
|
+
run.add_argument("--memory-mb", type=int, default=4096)
|
|
143
|
+
run.add_argument("--vcpu", type=int, default=2)
|
|
144
|
+
run.add_argument("--timeout-ms", type=int)
|
|
145
|
+
run.add_argument("--cwd")
|
|
146
|
+
run.add_argument("--env", action="append", default=[], metavar="NAME=VALUE")
|
|
147
|
+
run.add_argument("--stdin")
|
|
148
|
+
run.add_argument("--keep", action="store_true", help="Keep the sandbox after execution")
|
|
149
|
+
run.add_argument("cmd", nargs=argparse.REMAINDER)
|
|
150
|
+
|
|
151
|
+
for name in ("info", "pause", "resume", "destroy"):
|
|
152
|
+
lifecycle = sub.add_parser(name, help=f"{name.capitalize()} a sandbox")
|
|
153
|
+
lifecycle.add_argument("id")
|
|
154
|
+
|
|
155
|
+
sub.add_parser("health", help="Check daemon health")
|
|
156
|
+
sub.add_parser("metrics", help="Read runtime metrics")
|
|
157
|
+
sub.add_parser("mcp", help="Run the MCP stdio server")
|
|
158
|
+
|
|
159
|
+
files = sub.add_parser("files", help="Read and modify /workspace")
|
|
160
|
+
file_sub = files.add_subparsers(dest="file_command", required=True)
|
|
161
|
+
file_list = file_sub.add_parser("list", help="List a directory")
|
|
162
|
+
file_list.add_argument("id"); file_list.add_argument("path", nargs="?", default="/workspace")
|
|
163
|
+
file_read = file_sub.add_parser("read", help="Read a file")
|
|
164
|
+
file_read.add_argument("id"); file_read.add_argument("path"); file_read.add_argument("--raw", action="store_true")
|
|
165
|
+
file_write = file_sub.add_parser("write", help="Write a local file or stdin")
|
|
166
|
+
file_write.add_argument("id"); file_write.add_argument("path"); file_write.add_argument("--from", dest="source", default="-")
|
|
167
|
+
file_mkdir = file_sub.add_parser("mkdir", help="Create a directory")
|
|
168
|
+
file_mkdir.add_argument("id"); file_mkdir.add_argument("path"); file_mkdir.add_argument("--no-recursive", action="store_true")
|
|
169
|
+
file_delete = file_sub.add_parser("delete", help="Delete a file or directory")
|
|
170
|
+
file_delete.add_argument("id"); file_delete.add_argument("path"); file_delete.add_argument("--recursive", action="store_true")
|
|
171
|
+
file_move = file_sub.add_parser("move", help="Move a file or directory")
|
|
172
|
+
file_move.add_argument("id"); file_move.add_argument("source"); file_move.add_argument("destination"); file_move.add_argument("--overwrite", action="store_true")
|
|
173
|
+
|
|
174
|
+
process = sub.add_parser("process", help="Manage durable processes")
|
|
175
|
+
process_sub = process.add_subparsers(dest="process_command", required=True)
|
|
176
|
+
process_start = process_sub.add_parser("start"); process_start.add_argument("id"); process_start.add_argument("cmd", nargs=argparse.REMAINDER); process_start.add_argument("--cwd")
|
|
177
|
+
for name in ("status", "logs", "kill"):
|
|
178
|
+
operation = process_sub.add_parser(name); operation.add_argument("id"); operation.add_argument("process_id")
|
|
179
|
+
process_input = process_sub.add_parser("input"); process_input.add_argument("id"); process_input.add_argument("process_id"); process_input.add_argument("data")
|
|
180
|
+
|
|
181
|
+
snapshot = sub.add_parser("snapshot", help="Manage checkpoints and forks")
|
|
182
|
+
snapshot_sub = snapshot.add_subparsers(dest="snapshot_command", required=True)
|
|
183
|
+
snapshot_create = snapshot_sub.add_parser("create"); snapshot_create.add_argument("id"); snapshot_create.add_argument("--name"); snapshot_create.add_argument("--retention-seconds", type=int)
|
|
184
|
+
snapshot_sub.add_parser("list")
|
|
185
|
+
for name in ("info", "delete"):
|
|
186
|
+
operation = snapshot_sub.add_parser(name); operation.add_argument("snapshot_id")
|
|
187
|
+
snapshot_fork = snapshot_sub.add_parser("fork"); snapshot_fork.add_argument("snapshot_id")
|
|
188
|
+
|
|
189
|
+
preview = sub.add_parser("preview", help="Manage expiring preview URLs")
|
|
190
|
+
preview_sub = preview.add_subparsers(dest="preview_command", required=True)
|
|
191
|
+
preview_create = preview_sub.add_parser("create"); preview_create.add_argument("id"); preview_create.add_argument("port", type=int); preview_create.add_argument("--expires", type=int, default=3600)
|
|
192
|
+
preview_revoke = preview_sub.add_parser("revoke"); preview_revoke.add_argument("id"); preview_revoke.add_argument("preview_id")
|
|
193
|
+
preview_ready = preview_sub.add_parser("ready"); preview_ready.add_argument("id"); preview_ready.add_argument("preview_id")
|
|
194
|
+
|
|
195
|
+
config = sub.add_parser("agent-config", help="Print MCP setup for a coding agent")
|
|
196
|
+
config.add_argument("client", choices=("codex", "claude", "cursor"))
|
|
197
|
+
|
|
198
|
+
migrate = sub.add_parser("migrate", help="Assess and plan migrations to Mosaic")
|
|
199
|
+
migrate_sub = migrate.add_subparsers(dest="migration_command", required=True)
|
|
200
|
+
migrate_sub.add_parser("providers", help="List supported source providers")
|
|
201
|
+
for name in ("assess", "plan"):
|
|
202
|
+
operation = migrate_sub.add_parser(name, help=f"{name.capitalize()} a migration")
|
|
203
|
+
operation.add_argument("--provider", required=True, choices=tuple(PROVIDER_INDEX))
|
|
204
|
+
operation.add_argument("--manifest", required=True, help="JSON manifest path, or - for stdin")
|
|
205
|
+
|
|
206
|
+
sub.add_parser("doctor", help="Check CLI configuration and daemon connectivity")
|
|
207
|
+
return parser
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _parse_env(values: list[str]) -> dict[str, str]:
|
|
211
|
+
result = {}
|
|
212
|
+
for value in values:
|
|
213
|
+
if "=" not in value or value.startswith("="):
|
|
214
|
+
raise ValueError(f"invalid environment assignment: {value}")
|
|
215
|
+
key, item = value.split("=", 1)
|
|
216
|
+
result[key] = item
|
|
217
|
+
return result
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def main(argv: list[str] | None = None) -> int:
|
|
221
|
+
args = build_parser().parse_args(argv)
|
|
222
|
+
try:
|
|
223
|
+
if args.command == "mcp":
|
|
224
|
+
os.environ["MAR_ENDPOINT"] = args.endpoint
|
|
225
|
+
if args.token:
|
|
226
|
+
os.environ["MAR_API_TOKEN"] = args.token
|
|
227
|
+
from .mcp_server import main as mcp_main
|
|
228
|
+
|
|
229
|
+
mcp_main()
|
|
230
|
+
return 0
|
|
231
|
+
if args.command == "agent-config":
|
|
232
|
+
_emit(_agent_config(args.client, args.endpoint), raw=True)
|
|
233
|
+
return 0
|
|
234
|
+
if args.command == "migrate":
|
|
235
|
+
if args.migration_command == "providers":
|
|
236
|
+
_emit({"providers": list_providers()})
|
|
237
|
+
else:
|
|
238
|
+
manifest = _load_manifest(args.manifest)
|
|
239
|
+
result = (
|
|
240
|
+
assess_migration(args.provider, manifest)
|
|
241
|
+
if args.migration_command == "assess"
|
|
242
|
+
else plan_migration(args.provider, manifest)
|
|
243
|
+
)
|
|
244
|
+
_emit(result)
|
|
245
|
+
return 0
|
|
246
|
+
if args.command == "files":
|
|
247
|
+
sandbox = Sandbox(args.id, args.endpoint, args.token)
|
|
248
|
+
if args.file_command == "list":
|
|
249
|
+
_emit({"path": args.path, "entries": [asdict(entry) for entry in sandbox.files.list(args.path)]})
|
|
250
|
+
elif args.file_command == "read":
|
|
251
|
+
content = sandbox.files.read_bytes(args.path)
|
|
252
|
+
if args.raw:
|
|
253
|
+
sys.stdout.buffer.write(content)
|
|
254
|
+
else:
|
|
255
|
+
_emit({"path": args.path, "content": content.decode("utf-8"), "size": len(content)})
|
|
256
|
+
elif args.file_command == "write":
|
|
257
|
+
content = sys.stdin.buffer.read() if args.source == "-" else Path(args.source).read_bytes()
|
|
258
|
+
_emit({"path": args.path, "size": sandbox.files.write(args.path, content)})
|
|
259
|
+
elif args.file_command == "mkdir":
|
|
260
|
+
sandbox.files.mkdir(args.path, recursive=not args.no_recursive); _emit({"path": args.path, "created": True})
|
|
261
|
+
elif args.file_command == "delete":
|
|
262
|
+
sandbox.files.remove(args.path, recursive=args.recursive); _emit({"path": args.path, "deleted": True})
|
|
263
|
+
elif args.file_command == "move":
|
|
264
|
+
sandbox.files.move(args.source, args.destination, overwrite=args.overwrite); _emit({"source": args.source, "destination": args.destination, "moved": True})
|
|
265
|
+
return 0
|
|
266
|
+
if args.command == "process":
|
|
267
|
+
sandbox = Sandbox(args.id, args.endpoint, args.token)
|
|
268
|
+
if args.process_command == "start": _emit(asdict(sandbox.process.start(args.cmd[1:] if args.cmd[:1] == ["--"] else args.cmd, cwd=args.cwd).initial))
|
|
269
|
+
elif args.process_command == "status": _emit(asdict(ProcessHandle(sandbox, ProcessInfo(**sandbox._call("GET", f"/processes/{args.process_id}"))).info()))
|
|
270
|
+
elif args.process_command == "logs": _emit(sandbox._call("GET", f"/processes/{args.process_id}/logs"))
|
|
271
|
+
elif args.process_command == "input": sandbox._call("POST", f"/processes/{args.process_id}/input", {"data": args.data}); _emit({"sent": True})
|
|
272
|
+
elif args.process_command == "kill": sandbox._call("DELETE", f"/processes/{args.process_id}"); _emit({"killed": True})
|
|
273
|
+
return 0
|
|
274
|
+
if args.command == "snapshot":
|
|
275
|
+
if args.snapshot_command == "list": _emit(_request(args, "/v1/snapshots"))
|
|
276
|
+
elif args.snapshot_command == "info": _emit(_request(args, f"/v1/snapshots/{args.snapshot_id}"))
|
|
277
|
+
elif args.snapshot_command == "delete": _emit(_request(args, f"/v1/snapshots/{args.snapshot_id}", method="DELETE"))
|
|
278
|
+
elif args.snapshot_command == "fork":
|
|
279
|
+
sandbox = Sandbox.create(endpoint=args.endpoint, api_token=args.token, snapshot_id=args.snapshot_id); _emit(_sandbox_created(sandbox))
|
|
280
|
+
else: _emit(asdict(Sandbox(args.id, args.endpoint, args.token).snapshot(args.name, args.retention_seconds)))
|
|
281
|
+
return 0
|
|
282
|
+
if args.command == "preview":
|
|
283
|
+
sandbox = Sandbox(args.id, args.endpoint, args.token)
|
|
284
|
+
if args.preview_command == "create": _emit(asdict(sandbox.preview(args.port, args.expires)))
|
|
285
|
+
elif args.preview_command == "ready": _emit({"preview_id": args.preview_id, "ready": sandbox.preview_ready(args.preview_id)})
|
|
286
|
+
else: sandbox.revoke_preview(args.preview_id); _emit({"revoked": True})
|
|
287
|
+
return 0
|
|
288
|
+
if args.command == "health":
|
|
289
|
+
_emit(_request(args, "/healthz"))
|
|
290
|
+
return 0
|
|
291
|
+
if args.command == "metrics":
|
|
292
|
+
_emit(_request(args, "/metrics"))
|
|
293
|
+
return 0
|
|
294
|
+
if args.command == "doctor":
|
|
295
|
+
health = _request(args, "/healthz")
|
|
296
|
+
_emit(
|
|
297
|
+
{
|
|
298
|
+
"cli": "ok",
|
|
299
|
+
"endpoint": args.endpoint,
|
|
300
|
+
"authentication": "configured" if args.token else "not configured",
|
|
301
|
+
"daemon": health,
|
|
302
|
+
}
|
|
303
|
+
)
|
|
304
|
+
return 0
|
|
305
|
+
if args.command == "create":
|
|
306
|
+
sandbox = Sandbox.create(
|
|
307
|
+
template=args.template,
|
|
308
|
+
memory_mb=args.memory_mb,
|
|
309
|
+
vcpu=args.vcpu,
|
|
310
|
+
endpoint=args.endpoint,
|
|
311
|
+
api_token=args.token,
|
|
312
|
+
enable_ssh=not args.no_ssh,
|
|
313
|
+
ssh_public_key=_ssh_key(args),
|
|
314
|
+
ssh_private_key_path=args.ssh_private_key,
|
|
315
|
+
)
|
|
316
|
+
_emit(_sandbox_created(sandbox))
|
|
317
|
+
return 0
|
|
318
|
+
if args.command == "exec":
|
|
319
|
+
if not args.cmd:
|
|
320
|
+
raise ValueError("exec requires a command after the sandbox ID")
|
|
321
|
+
result = _sandbox(args).run_command(
|
|
322
|
+
_command(args.cmd),
|
|
323
|
+
args.timeout_ms,
|
|
324
|
+
cwd=args.cwd,
|
|
325
|
+
env=_parse_env(args.env),
|
|
326
|
+
stdin=args.stdin,
|
|
327
|
+
)
|
|
328
|
+
_emit(asdict(result))
|
|
329
|
+
return result.exit_code
|
|
330
|
+
if args.command == "run":
|
|
331
|
+
if not args.cmd:
|
|
332
|
+
raise ValueError("run requires a command")
|
|
333
|
+
sandbox = Sandbox.create(
|
|
334
|
+
template=args.template,
|
|
335
|
+
memory_mb=args.memory_mb,
|
|
336
|
+
vcpu=args.vcpu,
|
|
337
|
+
endpoint=args.endpoint,
|
|
338
|
+
api_token=args.token,
|
|
339
|
+
enable_ssh=False,
|
|
340
|
+
)
|
|
341
|
+
try:
|
|
342
|
+
result = sandbox.run_command(
|
|
343
|
+
_command(args.cmd),
|
|
344
|
+
args.timeout_ms,
|
|
345
|
+
cwd=args.cwd,
|
|
346
|
+
env=_parse_env(args.env),
|
|
347
|
+
stdin=args.stdin,
|
|
348
|
+
)
|
|
349
|
+
_emit({"sandbox_id": sandbox.id, **asdict(result)})
|
|
350
|
+
return result.exit_code
|
|
351
|
+
finally:
|
|
352
|
+
if not args.keep:
|
|
353
|
+
sandbox.destroy()
|
|
354
|
+
if args.command == "info":
|
|
355
|
+
_emit(_request(args, f"/v1/sandboxes/{args.id}"))
|
|
356
|
+
return 0
|
|
357
|
+
sandbox = _sandbox(args)
|
|
358
|
+
if args.command == "pause":
|
|
359
|
+
sandbox.pause()
|
|
360
|
+
elif args.command == "resume":
|
|
361
|
+
sandbox.resume()
|
|
362
|
+
elif args.command == "destroy":
|
|
363
|
+
sandbox.destroy()
|
|
364
|
+
_emit({"id": args.id, "state": args.command})
|
|
365
|
+
return 0
|
|
366
|
+
except (RuntimeError, ValueError) as error:
|
|
367
|
+
print(f"mar: {error}", file=sys.stderr)
|
|
368
|
+
return 1
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
if __name__ == "__main__":
|
|
372
|
+
raise SystemExit(main())
|