mcp-as-code 0.1.0__py3-none-any.whl

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.
maco/runner.py ADDED
@@ -0,0 +1,104 @@
1
+ """Run Python scripts with generated MCP wrappers on PYTHONPATH."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+
12
+
13
+ class RunnerError(RuntimeError):
14
+ """Raised when maco cannot prepare a code execution run."""
15
+
16
+
17
+ def run_code(
18
+ code_path: str | os.PathLike[str],
19
+ script_args: list[str] | None = None,
20
+ *,
21
+ workspace: str | os.PathLike[str] | None = None,
22
+ cwd: str | os.PathLike[str] | None = None,
23
+ python: str | None = None,
24
+ ) -> int:
25
+ """Run a Python script with the generated maco workspace available."""
26
+
27
+ script = Path(code_path).expanduser().resolve()
28
+ if not script.exists():
29
+ raise RunnerError(f"code file not found: {script}")
30
+ workspace_path = find_workspace(script, workspace)
31
+ gateway_file = workspace_path / "gateway.json"
32
+
33
+ env = os.environ.copy()
34
+ env["MACO_WORKSPACE"] = str(workspace_path)
35
+ env["MACO_GATEWAY_FILE"] = str(gateway_file)
36
+ gateway = _read_gateway(gateway_file)
37
+ if gateway.get("url") and not env.get("MACO_GATEWAY_URL"):
38
+ env["MACO_GATEWAY_URL"] = str(gateway["url"])
39
+ if gateway.get("token") and not env.get("MACO_GATEWAY_TOKEN"):
40
+ env["MACO_GATEWAY_TOKEN"] = str(gateway["token"])
41
+ env["PYTHONPATH"] = _prepend_path(str(workspace_path), env.get("PYTHONPATH"))
42
+
43
+ uv = shutil.which("uv")
44
+ if uv is None:
45
+ raise RunnerError("uv is required to run code; install uv or run the script manually with PYTHONPATH set")
46
+
47
+ command = [uv, "run"]
48
+ if python:
49
+ command.extend(["--python", python])
50
+ command.extend([str(script), *(script_args or [])])
51
+ completed = subprocess.run(command, env=env, cwd=str(Path(cwd).resolve()) if cwd else None)
52
+ return completed.returncode
53
+
54
+
55
+ def find_workspace(
56
+ script: Path,
57
+ explicit_workspace: str | os.PathLike[str] | None = None,
58
+ ) -> Path:
59
+ """Find the generated workspace for a script."""
60
+
61
+ candidates: list[Path] = []
62
+ if explicit_workspace:
63
+ candidates.append(Path(explicit_workspace).expanduser())
64
+ if os.environ.get("MACO_WORKSPACE"):
65
+ candidates.append(Path(os.environ["MACO_WORKSPACE"]).expanduser())
66
+ candidates.extend(parent / ".maco" for parent in [script.parent, *script.parents])
67
+ cwd = Path.cwd().resolve()
68
+ candidates.extend(parent / ".maco" for parent in [cwd, *cwd.parents])
69
+
70
+ seen: set[Path] = set()
71
+ for candidate in candidates:
72
+ resolved = candidate.resolve()
73
+ if resolved in seen:
74
+ continue
75
+ seen.add(resolved)
76
+ if (resolved / "maco_generated" / "client.py").exists():
77
+ return resolved
78
+ raise RunnerError(
79
+ "could not find a generated maco workspace. Run `maco gen`, pass --workspace, "
80
+ "or set MACO_WORKSPACE."
81
+ )
82
+
83
+
84
+ def _read_gateway(path: Path) -> dict[str, object]:
85
+ try:
86
+ data = json.loads(path.read_text(encoding="utf-8"))
87
+ except FileNotFoundError:
88
+ return {}
89
+ except Exception as exc:
90
+ raise RunnerError(f"failed to read gateway file {path}: {exc}") from exc
91
+ if not isinstance(data, dict):
92
+ raise RunnerError(f"gateway file {path} must contain a JSON object")
93
+ return data
94
+
95
+
96
+ def _prepend_path(prefix: str, existing: str | None) -> str:
97
+ if not existing:
98
+ return prefix
99
+ return os.pathsep.join([prefix, existing])
100
+
101
+
102
+ def exit_with_error(exc: BaseException) -> None:
103
+ print(f"maco: {exc}", file=sys.stderr)
104
+ raise SystemExit(1)
@@ -0,0 +1,43 @@
1
+ """Sandbox providers for maco serve-mcp execution."""
2
+
3
+ from .core import (
4
+ DEFAULT_MATCHLOCK_GATEWAY_IP,
5
+ DEFAULT_SANDBOX_IMAGE,
6
+ GatewayInfo,
7
+ SANDBOX_SDK_ROOT,
8
+ SANDBOX_TOOLS_ROOT,
9
+ SANDBOX_USER,
10
+ SandboxContext,
11
+ SandboxError,
12
+ SandboxExec,
13
+ SandboxProvider,
14
+ SandboxRunResult,
15
+ guest_path_for,
16
+ provider_from_name,
17
+ translate_loopback_url,
18
+ write_code_file,
19
+ )
20
+ from .providers.docker import DockerSandboxProvider
21
+ from .providers.local import LocalSandboxProvider
22
+ from .providers.matchlock import MatchlockSandboxProvider
23
+
24
+ __all__ = [
25
+ "DEFAULT_SANDBOX_IMAGE",
26
+ "DEFAULT_MATCHLOCK_GATEWAY_IP",
27
+ "DockerSandboxProvider",
28
+ "GatewayInfo",
29
+ "LocalSandboxProvider",
30
+ "MatchlockSandboxProvider",
31
+ "SANDBOX_SDK_ROOT",
32
+ "SANDBOX_TOOLS_ROOT",
33
+ "SANDBOX_USER",
34
+ "SandboxContext",
35
+ "SandboxError",
36
+ "SandboxExec",
37
+ "SandboxProvider",
38
+ "SandboxRunResult",
39
+ "guest_path_for",
40
+ "provider_from_name",
41
+ "translate_loopback_url",
42
+ "write_code_file",
43
+ ]
maco/sandbox/core.py ADDED
@@ -0,0 +1,216 @@
1
+ """Core sandbox types and provider factory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from importlib.metadata import PackageNotFoundError, version
7
+ import json
8
+ import os
9
+ from pathlib import Path
10
+ from typing import Mapping, Protocol
11
+ from urllib.parse import urlsplit, urlunsplit
12
+
13
+
14
+ class SandboxError(RuntimeError):
15
+ """Raised when a sandbox provider cannot run a command."""
16
+
17
+
18
+ def _maco_version() -> str:
19
+ try:
20
+ return version("maco")
21
+ except PackageNotFoundError:
22
+ for parent in Path(__file__).resolve().parents:
23
+ version_file = parent / "VERSION.txt"
24
+ if version_file.exists():
25
+ return version_file.read_text(encoding="utf-8").strip()
26
+ return "0.0.0"
27
+
28
+
29
+ DEFAULT_SANDBOX_IMAGE = f"ghcr.io/jingkaihe/maco:{_maco_version()}-alpine"
30
+ DEFAULT_MATCHLOCK_GATEWAY_IP = "192.168.100.1"
31
+ SANDBOX_UID = 1000
32
+ SANDBOX_GID = 1000
33
+ SANDBOX_USER = f"{SANDBOX_UID}:{SANDBOX_GID}"
34
+ SANDBOX_SDK_ROOT = "/workspace/macosdk"
35
+ SANDBOX_TOOLS_ROOT = f"{SANDBOX_SDK_ROOT}/tools"
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class GatewayInfo:
40
+ """Host-side maco gateway coordinates."""
41
+
42
+ url: str
43
+ token: str | None = None
44
+
45
+ @classmethod
46
+ def from_file(cls, path: str | os.PathLike[str]) -> GatewayInfo:
47
+ gateway_path = Path(path).expanduser()
48
+ try:
49
+ payload = json.loads(gateway_path.read_text(encoding="utf-8"))
50
+ except FileNotFoundError as exc:
51
+ raise SandboxError(f"gateway file not found: {gateway_path}") from exc
52
+ except Exception as exc:
53
+ raise SandboxError(f"failed to read gateway file {gateway_path}: {exc}") from exc
54
+ if not isinstance(payload, dict):
55
+ raise SandboxError(f"gateway file {gateway_path} must contain a JSON object")
56
+ url = payload.get("url")
57
+ if not isinstance(url, str) or not url:
58
+ raise SandboxError(f"gateway file {gateway_path} must contain a non-empty url")
59
+ token = payload.get("token")
60
+ return cls(url=url, token=token if isinstance(token, str) and token else None)
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class SandboxRunResult:
65
+ """Result from a sandbox command."""
66
+
67
+ exit_code: int
68
+ stdout: str
69
+ stderr: str
70
+
71
+ @property
72
+ def ok(self) -> bool:
73
+ return self.exit_code == 0
74
+
75
+
76
+ @dataclass(frozen=True)
77
+ class SandboxContext:
78
+ """Provider-independent paths and gateway details."""
79
+
80
+ workspace: Path
81
+ scratch: Path
82
+ gateway: GatewayInfo
83
+ timeout: int = 60
84
+ python_command: str | None = None
85
+ debug: bool = False
86
+
87
+
88
+ @dataclass(frozen=True)
89
+ class SandboxExec:
90
+ """Command request for a sandbox provider."""
91
+
92
+ command: str
93
+ timeout: int | None = None
94
+ env: Mapping[str, str] = field(default_factory=dict)
95
+
96
+
97
+ class SandboxProvider(Protocol):
98
+ """Common execution surface for local, Docker, and Matchlock sandboxes."""
99
+
100
+ guest_workspace: str
101
+ guest_scratch: str
102
+
103
+ def start(self) -> None:
104
+ """Start or bootstrap provider resources."""
105
+
106
+ def stop(self) -> None:
107
+ """Release provider resources."""
108
+
109
+ def run(self, request: SandboxExec) -> SandboxRunResult:
110
+ """Run a non-interactive command in the sandbox."""
111
+
112
+ def write_file(self, relative_path: str, content: str) -> str:
113
+ """Write a file inside sandbox scratch and return its guest path."""
114
+
115
+ def python_script_command(self, guest_script_path: str, args: list[str]) -> str:
116
+ """Build the shell command used by code_execute for a guest script."""
117
+
118
+
119
+ def provider_from_name(
120
+ name: str,
121
+ context: SandboxContext,
122
+ *,
123
+ image: str | None = None,
124
+ docker_binary: str = "docker",
125
+ docker_network: str | None = None,
126
+ docker_gateway_host: str = "host.docker.internal",
127
+ docker_gateway_ip: str | None = None,
128
+ matchlock_binary: str = "matchlock",
129
+ matchlock_gateway_host: str = "maco-gateway.internal",
130
+ matchlock_gateway_ip: str | None = None,
131
+ matchlock_extra_allow_hosts: list[str] | None = None,
132
+ ) -> SandboxProvider:
133
+ """Construct a sandbox provider from a CLI-friendly name."""
134
+
135
+ # Imports are local to avoid importing provider subprocess dependencies when
136
+ # callers only need the data types/helpers.
137
+ from .providers.docker import DockerSandboxProvider
138
+ from .providers.local import LocalSandboxProvider
139
+ from .providers.matchlock import MatchlockSandboxProvider
140
+
141
+ normalized = name.replace("_", "-").lower()
142
+ if normalized == "local":
143
+ return LocalSandboxProvider(context)
144
+ if normalized == "docker":
145
+ return DockerSandboxProvider(
146
+ context,
147
+ image=image or DEFAULT_SANDBOX_IMAGE,
148
+ docker_binary=docker_binary,
149
+ network=docker_network,
150
+ gateway_host=docker_gateway_host,
151
+ gateway_ip=docker_gateway_ip,
152
+ )
153
+ if normalized == "matchlock":
154
+ return MatchlockSandboxProvider(
155
+ context,
156
+ image=image or DEFAULT_SANDBOX_IMAGE,
157
+ matchlock_binary=matchlock_binary,
158
+ gateway_host=matchlock_gateway_host,
159
+ gateway_ip=matchlock_gateway_ip,
160
+ extra_allow_hosts=matchlock_extra_allow_hosts or [],
161
+ )
162
+ raise SandboxError(f"unknown sandbox provider {name!r}; expected local, docker, or matchlock")
163
+
164
+
165
+ def write_code_file(scratch: Path, filename: str, code: str) -> Path:
166
+ """Write code into the scratch directory after constraining the path."""
167
+
168
+ if not filename.strip():
169
+ raise SandboxError("filename must be non-empty")
170
+ relative = Path(filename)
171
+ if relative.is_absolute() or ".." in relative.parts:
172
+ raise SandboxError("filename must be a relative path inside the sandbox scratch directory")
173
+ path = scratch / relative
174
+ path.parent.mkdir(parents=True, exist_ok=True)
175
+ path.write_text(code, encoding="utf-8")
176
+ return path
177
+
178
+
179
+ def guest_path_for(local_path: Path, scratch: Path, guest_scratch: str) -> str:
180
+ """Translate a local scratch path to the corresponding guest path."""
181
+
182
+ try:
183
+ relative = local_path.resolve().relative_to(scratch.resolve())
184
+ except ValueError as exc:
185
+ raise SandboxError(f"path {local_path} is not inside scratch directory {scratch}") from exc
186
+ return posix_join(guest_scratch, relative.as_posix())
187
+
188
+
189
+ def translate_loopback_url(url: str, host: str) -> str:
190
+ """Replace localhost in a gateway URL with a guest-reachable host alias."""
191
+
192
+ parts = urlsplit(url)
193
+ if parts.hostname not in {"127.0.0.1", "localhost", "::1"}:
194
+ return url
195
+ netloc = host
196
+ if parts.port is not None:
197
+ netloc = f"{host}:{parts.port}"
198
+ return urlunsplit((parts.scheme, netloc, parts.path, parts.query, parts.fragment))
199
+
200
+
201
+ def normalize_context(context: SandboxContext) -> SandboxContext:
202
+ workspace = context.workspace.expanduser().resolve()
203
+ scratch = context.scratch.expanduser().resolve()
204
+ scratch.mkdir(parents=True, exist_ok=True)
205
+ return SandboxContext(
206
+ workspace=workspace,
207
+ scratch=scratch,
208
+ gateway=context.gateway,
209
+ timeout=context.timeout,
210
+ python_command=context.python_command,
211
+ debug=context.debug,
212
+ )
213
+
214
+
215
+ def posix_join(root: str, child: str) -> str:
216
+ return f"{root.rstrip('/')}/{child.lstrip('/')}"
@@ -0,0 +1,7 @@
1
+ """Provider implementations for maco-sandbox."""
2
+
3
+ from .docker import DockerSandboxProvider
4
+ from .local import LocalSandboxProvider
5
+ from .matchlock import MatchlockSandboxProvider
6
+
7
+ __all__ = ["DockerSandboxProvider", "LocalSandboxProvider", "MatchlockSandboxProvider"]
@@ -0,0 +1,69 @@
1
+ """Shared helpers for concrete sandbox providers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ import shlex
7
+ from typing import Mapping
8
+
9
+ from ..core import SANDBOX_SDK_ROOT, SandboxContext, SandboxError, SandboxExec, normalize_context, posix_join
10
+
11
+
12
+ class BaseSandboxProvider:
13
+ guest_workspace: str
14
+ guest_scratch: str
15
+ default_python_command = "python"
16
+
17
+ def __init__(self, context: SandboxContext) -> None:
18
+ self.context = normalize_context(context)
19
+
20
+ def start(self) -> None:
21
+ """Start provider resources. Local/one-shot providers can no-op."""
22
+
23
+ def stop(self) -> None:
24
+ """Release provider resources. Local/one-shot providers can no-op."""
25
+
26
+ def write_file(self, relative_path: str, content: str) -> str:
27
+ path = self._local_scratch_path(relative_path)
28
+ path.parent.mkdir(parents=True, exist_ok=True)
29
+ path.write_text(content, encoding="utf-8")
30
+ return self._guest_scratch_path(relative_path)
31
+
32
+ def python_script_command(self, guest_script_path: str, args: list[str]) -> str:
33
+ command = self.context.python_command or self.default_python_command
34
+ return " ".join([command, shlex.quote(guest_script_path), *[shlex.quote(arg) for arg in args]])
35
+
36
+ def _timeout(self, request: SandboxExec) -> int:
37
+ return request.timeout or self.context.timeout
38
+
39
+ def _guest_env(self, request_env: Mapping[str, str], *, gateway_url: str) -> dict[str, str]:
40
+ env = {
41
+ "MACO_WORKSPACE": self.guest_workspace,
42
+ "MACO_GATEWAY_FILE": posix_join(self.guest_workspace, "gateway.json"),
43
+ "MACO_GATEWAY_URL": gateway_url,
44
+ "PYTHONPATH": self.guest_workspace,
45
+ }
46
+ if self.context.gateway.token:
47
+ env["MACO_GATEWAY_TOKEN"] = self.context.gateway.token
48
+ env.update(request_env)
49
+ return env
50
+
51
+ def _local_scratch_path(self, relative_path: str) -> Path:
52
+ relative = self._relative_scratch_path(relative_path)
53
+ return self.context.scratch / relative
54
+
55
+ def _guest_scratch_path(self, relative_path: str) -> str:
56
+ return posix_join(self.guest_scratch, self._relative_scratch_path(relative_path).as_posix())
57
+
58
+ def _relative_scratch_path(self, relative_path: str) -> Path:
59
+ if not relative_path.strip():
60
+ raise SandboxError("path must be non-empty")
61
+ relative = Path(relative_path)
62
+ if relative.is_absolute() or ".." in relative.parts:
63
+ raise SandboxError("path must be relative and inside the sandbox scratch directory")
64
+ return relative
65
+
66
+
67
+ class RemoteSandboxProvider(BaseSandboxProvider):
68
+ guest_workspace = SANDBOX_SDK_ROOT
69
+ guest_scratch = "/workspace"
@@ -0,0 +1,228 @@
1
+ """Docker sandbox provider."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shlex
7
+ import subprocess
8
+ import sys
9
+ import uuid
10
+ from urllib.parse import urlsplit, urlunsplit
11
+
12
+ from ..core import (
13
+ SANDBOX_SDK_ROOT,
14
+ SANDBOX_USER,
15
+ SandboxContext,
16
+ SandboxError,
17
+ SandboxExec,
18
+ SandboxRunResult,
19
+ translate_loopback_url,
20
+ )
21
+ from .base import RemoteSandboxProvider
22
+
23
+
24
+ class DockerSandboxProvider(RemoteSandboxProvider):
25
+ """Run commands inside one long-lived Docker container."""
26
+
27
+ def __init__(
28
+ self,
29
+ context: SandboxContext,
30
+ *,
31
+ image: str,
32
+ docker_binary: str = "docker",
33
+ network: str | None = None,
34
+ gateway_host: str = "host.docker.internal",
35
+ gateway_ip: str | None = None,
36
+ ) -> None:
37
+ super().__init__(context)
38
+ self.image = image
39
+ self.docker_binary = docker_binary
40
+ self.network = network
41
+ self.gateway_host = gateway_host
42
+ self.gateway_ip = gateway_ip
43
+ self.container_name = self._container_name()
44
+ self.container_id: str | None = None
45
+ self._start_attempted = False
46
+
47
+ def start(self) -> None:
48
+ if self.container_id is not None:
49
+ return
50
+ gateway_url = _docker_gateway_url(
51
+ self.context.gateway.url,
52
+ gateway_host=self.gateway_host,
53
+ gateway_ip=self.gateway_ip,
54
+ )
55
+ env = self._guest_env({}, gateway_url=gateway_url)
56
+ host_env = os.environ.copy()
57
+ command = [
58
+ self.docker_binary,
59
+ "run",
60
+ "-d",
61
+ "--rm",
62
+ "--user",
63
+ SANDBOX_USER,
64
+ "--name",
65
+ self.container_name,
66
+ "--label",
67
+ "org.opencontainers.image.title=maco-sandbox",
68
+ "--label",
69
+ "maco.managed=true",
70
+ ]
71
+ if self.network:
72
+ command.extend(["--network", self.network])
73
+ if self.gateway_ip:
74
+ command.extend(["--add-host", f"{self.gateway_host}:{self.gateway_ip}"])
75
+ for key, value in sorted(env.items()):
76
+ if key == "MACO_GATEWAY_TOKEN" and self.context.gateway.token:
77
+ host_env[key] = self.context.gateway.token
78
+ command.extend(["-e", key])
79
+ else:
80
+ command.extend(["-e", f"{key}={value}"])
81
+ command.extend(["-w", self.guest_scratch, self.image])
82
+ self._start_attempted = True
83
+ try:
84
+ completed = subprocess.run(
85
+ command,
86
+ env=host_env,
87
+ text=True,
88
+ stdout=subprocess.PIPE,
89
+ stderr=subprocess.PIPE,
90
+ timeout=self.context.timeout,
91
+ check=False,
92
+ )
93
+ if completed.returncode != 0:
94
+ raise SandboxError(f"failed to start Docker sandbox: {completed.stderr.strip()}")
95
+ self.container_id = completed.stdout.strip()
96
+ self._bootstrap_sdk()
97
+ except BaseException:
98
+ self.stop()
99
+ raise
100
+
101
+ def stop(self) -> None:
102
+ if self.container_id is None and not self._start_attempted:
103
+ return
104
+ target = self.container_id or self.container_name
105
+ try:
106
+ subprocess.run(
107
+ [self.docker_binary, "rm", "-f", target],
108
+ stdout=subprocess.DEVNULL,
109
+ stderr=subprocess.DEVNULL,
110
+ timeout=10,
111
+ check=False,
112
+ )
113
+ except Exception:
114
+ pass
115
+ finally:
116
+ self.container_id = None
117
+ self._start_attempted = False
118
+
119
+ def run(self, request: SandboxExec) -> SandboxRunResult:
120
+ self.start()
121
+ assert self.container_id is not None
122
+ command = [
123
+ self.docker_binary,
124
+ "exec",
125
+ "--user",
126
+ SANDBOX_USER,
127
+ "-w",
128
+ self.guest_scratch,
129
+ self.container_id,
130
+ "sh",
131
+ "-lc",
132
+ request.command,
133
+ ]
134
+ completed = subprocess.run(
135
+ command,
136
+ text=True,
137
+ stdout=subprocess.PIPE,
138
+ stderr=subprocess.PIPE,
139
+ timeout=self._timeout(request),
140
+ check=False,
141
+ )
142
+ if self.context.debug:
143
+ print(f"maco docker command: {self._command_summary(command)!r}", file=sys.stderr)
144
+ return SandboxRunResult(completed.returncode, completed.stdout, completed.stderr)
145
+
146
+ def write_file(self, relative_path: str, content: str) -> str:
147
+ self.start()
148
+ assert self.container_id is not None
149
+ guest_path = self._guest_scratch_path(relative_path)
150
+ parent = guest_path.rsplit("/", 1)[0]
151
+ command = [
152
+ self.docker_binary,
153
+ "exec",
154
+ "-i",
155
+ "--user",
156
+ SANDBOX_USER,
157
+ self.container_id,
158
+ "sh",
159
+ "-lc",
160
+ f"mkdir -p {shlex.quote(parent)} && cat > {shlex.quote(guest_path)}",
161
+ ]
162
+ completed = subprocess.run(
163
+ command,
164
+ input=content,
165
+ text=True,
166
+ stdout=subprocess.PIPE,
167
+ stderr=subprocess.PIPE,
168
+ timeout=self.context.timeout,
169
+ check=False,
170
+ )
171
+ if completed.returncode != 0:
172
+ raise SandboxError(f"failed to write Docker sandbox file {guest_path}: {completed.stderr.strip()}")
173
+ return guest_path
174
+
175
+ def _bootstrap_sdk(self) -> None:
176
+ assert self.container_id is not None
177
+ command = [
178
+ self.docker_binary,
179
+ "exec",
180
+ "--user",
181
+ SANDBOX_USER,
182
+ self.container_id,
183
+ "maco",
184
+ "sandbox-bootstrap",
185
+ "--workspace",
186
+ SANDBOX_SDK_ROOT,
187
+ ]
188
+ completed = subprocess.run(
189
+ command,
190
+ text=True,
191
+ stdout=subprocess.PIPE,
192
+ stderr=subprocess.PIPE,
193
+ timeout=self.context.timeout,
194
+ check=False,
195
+ )
196
+ if completed.returncode != 0:
197
+ raise SandboxError(f"failed to bootstrap Docker sandbox SDK: {completed.stderr.strip()}")
198
+
199
+ def _command_summary(self, command: list[str]) -> list[str]:
200
+ redacted: list[str] = []
201
+ for part in command:
202
+ if self.context.gateway.token and part == self.context.gateway.token:
203
+ redacted.append("<redacted>")
204
+ else:
205
+ redacted.append(part)
206
+ return redacted
207
+
208
+ def _container_name(self) -> str:
209
+ return f"maco-sandbox-{uuid.uuid4().hex[:12]}"
210
+
211
+
212
+ def _docker_gateway_url(url: str, *, gateway_host: str, gateway_ip: str | None) -> str:
213
+ translated = translate_loopback_url(url, gateway_host)
214
+ if gateway_ip and _url_host(translated) in {gateway_ip, "0.0.0.0"}:
215
+ return _replace_url_host(translated, gateway_host)
216
+ return translated
217
+
218
+
219
+ def _url_host(url: str) -> str | None:
220
+ return urlsplit(url).hostname
221
+
222
+
223
+ def _replace_url_host(url: str, host: str) -> str:
224
+ parts = urlsplit(url)
225
+ netloc = host
226
+ if parts.port is not None:
227
+ netloc = f"{host}:{parts.port}"
228
+ return urlunsplit((parts.scheme, netloc, parts.path, parts.query, parts.fragment))