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.
@@ -0,0 +1,46 @@
1
+ """Local subprocess sandbox provider."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import subprocess
7
+ import sys
8
+
9
+ from ..core import SandboxContext, SandboxExec, SandboxRunResult
10
+ from .base import BaseSandboxProvider
11
+
12
+
13
+ class LocalSandboxProvider(BaseSandboxProvider):
14
+ """Run commands as local subprocesses with maco env injected."""
15
+
16
+ default_python_command = "uv run python"
17
+ guest_workspace: str
18
+ guest_scratch: str
19
+
20
+ def __init__(self, context: SandboxContext) -> None:
21
+ super().__init__(context)
22
+ self.guest_workspace = str(self.context.workspace)
23
+ self.guest_scratch = str(self.context.scratch)
24
+
25
+ def run(self, request: SandboxExec) -> SandboxRunResult:
26
+ self.context.scratch.mkdir(parents=True, exist_ok=True)
27
+ env = os.environ.copy()
28
+ injected = self._guest_env(request.env, gateway_url=self.context.gateway.url)
29
+ existing_pythonpath = env.get("PYTHONPATH")
30
+ if existing_pythonpath:
31
+ injected["PYTHONPATH"] = os.pathsep.join([self.guest_workspace, existing_pythonpath])
32
+ env.update(injected)
33
+ command = ["sh", "-lc", request.command]
34
+ completed = subprocess.run(
35
+ command,
36
+ cwd=str(self.context.scratch),
37
+ env=env,
38
+ text=True,
39
+ stdout=subprocess.PIPE,
40
+ stderr=subprocess.PIPE,
41
+ timeout=self._timeout(request),
42
+ check=False,
43
+ )
44
+ if self.context.debug:
45
+ print(f"maco local command: {command!r}", file=sys.stderr)
46
+ return SandboxRunResult(completed.returncode, completed.stdout, completed.stderr)
@@ -0,0 +1,224 @@
1
+ """Matchlock sandbox provider."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shlex
6
+ import sys
7
+ from typing import Any
8
+ from urllib.parse import urlsplit, urlunsplit
9
+
10
+ from ..core import (
11
+ SANDBOX_SDK_ROOT,
12
+ SANDBOX_USER,
13
+ SandboxContext,
14
+ SandboxError,
15
+ SandboxExec,
16
+ SandboxRunResult,
17
+ translate_loopback_url,
18
+ )
19
+ from .base import RemoteSandboxProvider
20
+
21
+
22
+ class MatchlockSandboxProvider(RemoteSandboxProvider):
23
+ """Run commands inside one long-lived Matchlock micro-VM."""
24
+
25
+ def __init__(
26
+ self,
27
+ context: SandboxContext,
28
+ *,
29
+ image: str,
30
+ matchlock_binary: str = "matchlock",
31
+ gateway_host: str = "maco-gateway.internal",
32
+ gateway_ip: str | None = None,
33
+ extra_allow_hosts: list[str] | None = None,
34
+ ) -> None:
35
+ super().__init__(context)
36
+ self.image = image
37
+ self.matchlock_binary = matchlock_binary
38
+ self.gateway_host = gateway_host
39
+ self.gateway_ip = gateway_ip
40
+ self.extra_allow_hosts = extra_allow_hosts or []
41
+ self.client: Any | None = None
42
+ self.gateway_url = ""
43
+ self.allowed_hosts: list[str] = []
44
+ self.gateway_mapping: tuple[str, str] | None = None
45
+
46
+ def start(self) -> None:
47
+ if self.client is not None:
48
+ return
49
+ Client, Config, Sandbox = _load_matchlock_sdk()
50
+ self.gateway_url = _matchlock_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=self.gateway_url)
56
+ gateway_policy_host = _url_host(self.gateway_url) or self.gateway_host
57
+ self.gateway_mapping = (gateway_policy_host, self.gateway_ip) if self.gateway_ip else None
58
+ if self.gateway_mapping is not None and self.extra_allow_hosts:
59
+ raise SandboxError(
60
+ "matchlock extra allow hosts cannot be combined with a mapped local gateway yet; "
61
+ "Matchlock currently proxies all HTTP traffic when allow-hosts are configured"
62
+ )
63
+
64
+ # Matchlock applies the image-config user from launch to later exec
65
+ # calls when no per-exec user is supplied, so every client.exec below
66
+ # runs as the same unprivileged sandbox user.
67
+ spec = (
68
+ Sandbox(self.image)
69
+ .with_workspace(self.guest_scratch)
70
+ .with_user(SANDBOX_USER)
71
+ .with_env_map(env)
72
+ )
73
+ if self.gateway_mapping is not None:
74
+ spec.add_host(*self.gateway_mapping)
75
+ self.allowed_hosts = [*self.extra_allow_hosts]
76
+ if self.gateway_mapping is None:
77
+ self.allowed_hosts.append(gateway_policy_host)
78
+ for host in sorted(set(self.allowed_hosts)):
79
+ spec.allow_host(host)
80
+ if self.context.gateway.token and self.gateway_mapping is None:
81
+ placeholder = "MACO_GATEWAY_TOKEN_PLACEHOLDER"
82
+ spec.with_env("MACO_GATEWAY_TOKEN", placeholder)
83
+ spec.add_secret_with_placeholder(
84
+ "MACO_GATEWAY_TOKEN",
85
+ self.context.gateway.token,
86
+ placeholder,
87
+ gateway_policy_host,
88
+ )
89
+ spec.mount_memory(self.guest_scratch)
90
+
91
+ config = Config(binary_path=self.matchlock_binary)
92
+ client = Client(config)
93
+ try:
94
+ client.start()
95
+ client.launch(spec)
96
+ self.client = client
97
+ self._bootstrap_sdk()
98
+ except BaseException:
99
+ try:
100
+ client.close()
101
+ finally:
102
+ try:
103
+ client.remove()
104
+ except Exception:
105
+ pass
106
+ self.client = None
107
+ raise
108
+
109
+ def stop(self) -> None:
110
+ if self.client is None:
111
+ return
112
+ client = self.client
113
+ self.client = None
114
+ try:
115
+ client.close()
116
+ finally:
117
+ try:
118
+ client.remove()
119
+ except Exception:
120
+ pass
121
+
122
+ def run(self, request: SandboxExec) -> SandboxRunResult:
123
+ self.start()
124
+ assert self.client is not None
125
+ result = self.client.exec(
126
+ request.command,
127
+ working_dir=self.guest_scratch,
128
+ timeout=self._timeout(request),
129
+ )
130
+ if self.context.debug:
131
+ summary = _sdk_command_summary(
132
+ self.matchlock_binary,
133
+ self.image,
134
+ request.command,
135
+ gateway_url=self.gateway_url,
136
+ allowed_hosts=sorted(set(self.allowed_hosts)),
137
+ gateway_mapping=self.gateway_mapping,
138
+ )
139
+ print(
140
+ f"maco matchlock command: {summary!r}",
141
+ file=sys.stderr,
142
+ )
143
+ return SandboxRunResult(result.exit_code, result.stdout, result.stderr)
144
+
145
+ def write_file(self, relative_path: str, content: str) -> str:
146
+ self.start()
147
+ assert self.client is not None
148
+ guest_path = self._guest_scratch_path(relative_path)
149
+ parent = guest_path.rsplit("/", 1)[0]
150
+ self.client.exec(
151
+ f"mkdir -p {shlex.quote(parent)}",
152
+ working_dir=self.guest_scratch,
153
+ timeout=self.context.timeout,
154
+ )
155
+ self.client.write_file(guest_path, content)
156
+ return guest_path
157
+
158
+ def _bootstrap_sdk(self) -> None:
159
+ assert self.client is not None
160
+ result = self.client.exec(
161
+ f"maco sandbox-bootstrap --workspace {shlex.quote(SANDBOX_SDK_ROOT)}",
162
+ working_dir=self.guest_scratch,
163
+ timeout=self.context.timeout,
164
+ )
165
+ if result.exit_code != 0:
166
+ raise SandboxError(f"failed to bootstrap Matchlock sandbox SDK: {result.stderr.strip()}")
167
+
168
+
169
+ def _sdk_command_summary(
170
+ binary: str,
171
+ image: str,
172
+ command: str,
173
+ *,
174
+ gateway_url: str,
175
+ allowed_hosts: list[str],
176
+ gateway_mapping: tuple[str, str] | None = None,
177
+ ) -> list[str]:
178
+ """Return a non-secret summary for debug logs."""
179
+
180
+ summary: list[str] = [
181
+ binary,
182
+ "rpc",
183
+ "launch",
184
+ image,
185
+ "exec",
186
+ command,
187
+ f"MACO_GATEWAY_URL={gateway_url}",
188
+ ]
189
+ if gateway_mapping is not None:
190
+ host, ip = gateway_mapping
191
+ summary.append(f"hosts={ip}:{host}")
192
+ for host in allowed_hosts:
193
+ summary.append(f"allow_host={host}")
194
+ return summary
195
+
196
+
197
+ def _url_host(url: str) -> str | None:
198
+ return urlsplit(url).hostname
199
+
200
+
201
+ def _matchlock_gateway_url(url: str, *, gateway_host: str, gateway_ip: str | None) -> str:
202
+ translated = translate_loopback_url(url, gateway_host)
203
+ if gateway_ip and _url_host(translated) in {gateway_ip, "0.0.0.0"}:
204
+ return _replace_url_host(translated, gateway_host)
205
+ return translated
206
+
207
+
208
+ def _replace_url_host(url: str, host: str) -> str:
209
+ parts = urlsplit(url)
210
+ netloc = host
211
+ if parts.port is not None:
212
+ netloc = f"{host}:{parts.port}"
213
+ return urlunsplit((parts.scheme, netloc, parts.path, parts.query, parts.fragment))
214
+
215
+
216
+ def _load_matchlock_sdk() -> tuple[type[Any], type[Any], type[Any]]:
217
+ try:
218
+ from matchlock import Client, Config, Sandbox
219
+ except ImportError as exc: # pragma: no cover - depends on optional package availability
220
+ raise SandboxError(
221
+ "matchlock provider requires the Matchlock Python SDK; "
222
+ "install `maco-sandbox[matchlock]` or `matchlock`"
223
+ ) from exc
224
+ return Client, Config, Sandbox