minimalcode 1.5.2__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.
- minimalcode/__init__.py +3 -0
- minimalcode/__main__.py +3 -0
- minimalcode/agent.py +1084 -0
- minimalcode/cli.py +1520 -0
- minimalcode/clipboard.py +212 -0
- minimalcode/config.py +1065 -0
- minimalcode/defaults.json +491 -0
- minimalcode/errors.py +31 -0
- minimalcode/highlight/__init__.py +47 -0
- minimalcode/highlight/code.py +261 -0
- minimalcode/highlight/color.py +58 -0
- minimalcode/highlight/markdown.py +710 -0
- minimalcode/highlight/palette.py +73 -0
- minimalcode/highlight/sanitize.py +175 -0
- minimalcode/highlight/thinking.py +71 -0
- minimalcode/httpclient.py +278 -0
- minimalcode/importer.py +253 -0
- minimalcode/prompt.py +195 -0
- minimalcode/provider/__init__.py +28 -0
- minimalcode/provider/anthropic.py +805 -0
- minimalcode/provider/anthropic_env.py +161 -0
- minimalcode/provider/chat.py +394 -0
- minimalcode/provider/client.py +243 -0
- minimalcode/provider/common.py +176 -0
- minimalcode/provider/deepseek_web.py +1158 -0
- minimalcode/provider/factory.py +63 -0
- minimalcode/provider/responses.py +692 -0
- minimalcode/provider/sha3_wasm_bg.wasm +0 -0
- minimalcode/provider/transport.py +241 -0
- minimalcode/provider/types.py +54 -0
- minimalcode/sandbox/__init__.py +64 -0
- minimalcode/sandbox/audit.py +120 -0
- minimalcode/sandbox/backends.py +343 -0
- minimalcode/sandbox/execution.py +435 -0
- minimalcode/sandbox/network.py +793 -0
- minimalcode/sandbox/policy.py +253 -0
- minimalcode/sandbox/runner.py +347 -0
- minimalcode/sandbox/sensitive.py +286 -0
- minimalcode/session.py +465 -0
- minimalcode/shell.py +333 -0
- minimalcode/subagent.py +120 -0
- minimalcode/tools/__init__.py +46 -0
- minimalcode/tools/files.py +1139 -0
- minimalcode/tools/process.py +164 -0
- minimalcode/tools/remote.py +74 -0
- minimalcode/tools/runtime.py +219 -0
- minimalcode/tools/schemas.py +418 -0
- minimalcode/tools/search.py +521 -0
- minimalcode/tools/search_tools.py +583 -0
- minimalcode/tools/validation.py +99 -0
- minimalcode/vision.py +231 -0
- minimalcode/web.py +238 -0
- minimalcode-1.5.2.dist-info/METADATA +646 -0
- minimalcode-1.5.2.dist-info/RECORD +58 -0
- minimalcode-1.5.2.dist-info/WHEEL +5 -0
- minimalcode-1.5.2.dist-info/entry_points.txt +2 -0
- minimalcode-1.5.2.dist-info/licenses/LICENSE +201 -0
- minimalcode-1.5.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""Pluggable execution backends and backend selection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from ..errors import ConfigError, ToolError
|
|
13
|
+
from .policy import SandboxPolicy
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SandboxUnavailable(ToolError):
|
|
17
|
+
"""Raised when the selected backend cannot execute the requested input."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class CommandRequest:
|
|
22
|
+
"""One prepared invocation handed to a backend's ``wrap`` method."""
|
|
23
|
+
|
|
24
|
+
argv: tuple[str, ...]
|
|
25
|
+
command: str | None
|
|
26
|
+
shell_kind: str
|
|
27
|
+
shell_label: str
|
|
28
|
+
cwd: Path
|
|
29
|
+
temp_dir: Path | None
|
|
30
|
+
policy: SandboxPolicy
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SandboxBackend:
|
|
34
|
+
"""Base backend that runs the command unchanged."""
|
|
35
|
+
|
|
36
|
+
name = "base"
|
|
37
|
+
capabilities: frozenset[str] = frozenset()
|
|
38
|
+
|
|
39
|
+
def available(self) -> bool:
|
|
40
|
+
"""Return True when the backend's external dependency is usable."""
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
def wrap(self, request: CommandRequest) -> list[str]:
|
|
44
|
+
"""Return the final argv passed to the operating system."""
|
|
45
|
+
return list(request.argv)
|
|
46
|
+
|
|
47
|
+
def temp_path(self, request: CommandRequest) -> str | None:
|
|
48
|
+
"""Path the child should use for TMPDIR, or None to disable the override."""
|
|
49
|
+
return str(request.temp_dir) if request.temp_dir is not None else None
|
|
50
|
+
|
|
51
|
+
def cleanup(self, request: CommandRequest) -> None:
|
|
52
|
+
"""Release resources that outlive the wrapped client process, if any."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class LocalSandboxBackend(SandboxBackend):
|
|
56
|
+
"""Cross-platform baseline: policy layer plus subprocess limits."""
|
|
57
|
+
|
|
58
|
+
name = "local"
|
|
59
|
+
capabilities = frozenset({"argv", "shell", "resources"})
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class BwrapBackend(SandboxBackend):
|
|
63
|
+
"""Linux bubblewrap backend with filesystem and network namespaces."""
|
|
64
|
+
|
|
65
|
+
name = "bwrap"
|
|
66
|
+
capabilities = frozenset({"argv", "shell", "filesystem", "network"})
|
|
67
|
+
|
|
68
|
+
def available(self) -> bool:
|
|
69
|
+
if not sys.platform.startswith("linux"):
|
|
70
|
+
return False
|
|
71
|
+
cached = getattr(self, "_available", None)
|
|
72
|
+
if cached is not None:
|
|
73
|
+
return bool(cached)
|
|
74
|
+
executable = shutil.which("bwrap")
|
|
75
|
+
usable = False
|
|
76
|
+
if executable is not None:
|
|
77
|
+
try:
|
|
78
|
+
probe = subprocess.run(
|
|
79
|
+
[
|
|
80
|
+
executable,
|
|
81
|
+
"--unshare-all",
|
|
82
|
+
"--ro-bind",
|
|
83
|
+
"/",
|
|
84
|
+
"/",
|
|
85
|
+
"true",
|
|
86
|
+
],
|
|
87
|
+
stdin=subprocess.DEVNULL,
|
|
88
|
+
stdout=subprocess.DEVNULL,
|
|
89
|
+
stderr=subprocess.DEVNULL,
|
|
90
|
+
timeout=5,
|
|
91
|
+
check=False,
|
|
92
|
+
)
|
|
93
|
+
usable = probe.returncode == 0
|
|
94
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
95
|
+
usable = False
|
|
96
|
+
self._available = usable
|
|
97
|
+
return usable
|
|
98
|
+
|
|
99
|
+
def wrap(self, request: CommandRequest) -> list[str]:
|
|
100
|
+
argv = [
|
|
101
|
+
shutil.which("bwrap") or "bwrap",
|
|
102
|
+
"--die-with-parent",
|
|
103
|
+
"--new-session",
|
|
104
|
+
"--unshare-all",
|
|
105
|
+
"--ro-bind",
|
|
106
|
+
"/",
|
|
107
|
+
"/",
|
|
108
|
+
"--dev",
|
|
109
|
+
"/dev",
|
|
110
|
+
"--proc",
|
|
111
|
+
"/proc",
|
|
112
|
+
]
|
|
113
|
+
if not request.policy.isolates_network:
|
|
114
|
+
argv.append("--share-net")
|
|
115
|
+
for root in request.policy.write_roots(request.cwd):
|
|
116
|
+
argv.extend(["--bind", str(root), str(root)])
|
|
117
|
+
if request.temp_dir is not None:
|
|
118
|
+
argv.extend(["--bind", str(request.temp_dir), str(request.temp_dir)])
|
|
119
|
+
argv.extend(["--chdir", str(request.cwd), "--", *request.argv])
|
|
120
|
+
return argv
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class FirejailBackend(SandboxBackend):
|
|
124
|
+
"""Linux firejail backend for environments where bwrap is unavailable."""
|
|
125
|
+
|
|
126
|
+
name = "firejail"
|
|
127
|
+
capabilities = frozenset({"argv", "shell", "network"})
|
|
128
|
+
|
|
129
|
+
def available(self) -> bool:
|
|
130
|
+
return sys.platform.startswith("linux") and shutil.which("firejail") is not None
|
|
131
|
+
|
|
132
|
+
def wrap(self, request: CommandRequest) -> list[str]:
|
|
133
|
+
argv = [
|
|
134
|
+
shutil.which("firejail") or "firejail",
|
|
135
|
+
"--quiet",
|
|
136
|
+
"--private-tmp",
|
|
137
|
+
"--nosound",
|
|
138
|
+
"--no3d",
|
|
139
|
+
]
|
|
140
|
+
if request.policy.isolates_network:
|
|
141
|
+
argv.append("--net=none")
|
|
142
|
+
argv.extend(["--", *request.argv])
|
|
143
|
+
return argv
|
|
144
|
+
|
|
145
|
+
def temp_path(self, request: CommandRequest) -> str | None:
|
|
146
|
+
# --private-tmp mounts an empty /tmp, so the host temp path is gone.
|
|
147
|
+
return "/tmp" if request.temp_dir is not None else None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class MacSandboxExecBackend(SandboxBackend):
|
|
151
|
+
"""macOS ``sandbox-exec`` backend with an inline network profile."""
|
|
152
|
+
|
|
153
|
+
name = "sandbox-exec"
|
|
154
|
+
capabilities = frozenset({"argv", "shell", "network"})
|
|
155
|
+
|
|
156
|
+
def available(self) -> bool:
|
|
157
|
+
return sys.platform == "darwin" and shutil.which("sandbox-exec") is not None
|
|
158
|
+
|
|
159
|
+
def wrap(self, request: CommandRequest) -> list[str]:
|
|
160
|
+
profile = "(version 1)\n(allow default)\n"
|
|
161
|
+
if request.policy.isolates_network:
|
|
162
|
+
profile += "(deny network*)\n"
|
|
163
|
+
return [
|
|
164
|
+
shutil.which("sandbox-exec") or "sandbox-exec",
|
|
165
|
+
"-p",
|
|
166
|
+
profile,
|
|
167
|
+
*request.argv,
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class DockerBackend(SandboxBackend):
|
|
172
|
+
"""Docker backend for shell commands with a mounted workspace."""
|
|
173
|
+
|
|
174
|
+
name = "docker"
|
|
175
|
+
capabilities = frozenset({"shell", "filesystem", "network"})
|
|
176
|
+
|
|
177
|
+
def available(self) -> bool:
|
|
178
|
+
# Docker Desktop on Windows cannot mount drive-letter paths the way this
|
|
179
|
+
# backend builds them (`C:\ws:C:\ws`), so claiming availability there
|
|
180
|
+
# would produce a confusing failure after the policy said it was usable.
|
|
181
|
+
if os.name == "nt":
|
|
182
|
+
return False
|
|
183
|
+
cached = getattr(self, "_available", None)
|
|
184
|
+
if cached is not None:
|
|
185
|
+
return bool(cached)
|
|
186
|
+
executable = shutil.which("docker")
|
|
187
|
+
usable = False
|
|
188
|
+
if executable is not None:
|
|
189
|
+
try:
|
|
190
|
+
probe = subprocess.run(
|
|
191
|
+
[executable, "info", "--format", "{{.ServerVersion}}"],
|
|
192
|
+
stdin=subprocess.DEVNULL,
|
|
193
|
+
capture_output=True,
|
|
194
|
+
timeout=5,
|
|
195
|
+
check=False,
|
|
196
|
+
)
|
|
197
|
+
usable = probe.returncode == 0
|
|
198
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
199
|
+
usable = False
|
|
200
|
+
self._available = usable
|
|
201
|
+
return usable
|
|
202
|
+
|
|
203
|
+
@staticmethod
|
|
204
|
+
def _container_name(request: CommandRequest) -> str | None:
|
|
205
|
+
if request.temp_dir is None:
|
|
206
|
+
return None
|
|
207
|
+
return f"minimalcode-{request.temp_dir.name}"
|
|
208
|
+
|
|
209
|
+
def wrap(self, request: CommandRequest) -> list[str]:
|
|
210
|
+
if not request.command:
|
|
211
|
+
raise SandboxUnavailable(
|
|
212
|
+
"the docker sandbox backend can only run shell commands"
|
|
213
|
+
)
|
|
214
|
+
if request.shell_kind in {"pwsh7", "windows_powershell"}:
|
|
215
|
+
# The command is handed to `/bin/sh` inside the container. Passing a
|
|
216
|
+
# PowerShell command string to it silently mis-executes the command
|
|
217
|
+
# instead of failing, so refuse the combination.
|
|
218
|
+
raise SandboxUnavailable(
|
|
219
|
+
"the docker sandbox backend runs commands with a POSIX shell, "
|
|
220
|
+
f"which cannot execute {request.shell_label or request.shell_kind} "
|
|
221
|
+
"syntax"
|
|
222
|
+
)
|
|
223
|
+
image = request.policy.docker_image
|
|
224
|
+
if not image:
|
|
225
|
+
raise SandboxUnavailable("sandbox.docker_image must not be empty")
|
|
226
|
+
network = "none" if request.policy.isolates_network else "bridge"
|
|
227
|
+
argv = [
|
|
228
|
+
shutil.which("docker") or "docker",
|
|
229
|
+
"run",
|
|
230
|
+
"--rm",
|
|
231
|
+
"--interactive",
|
|
232
|
+
"--network",
|
|
233
|
+
network,
|
|
234
|
+
]
|
|
235
|
+
name = self._container_name(request)
|
|
236
|
+
if name is not None:
|
|
237
|
+
# `--rm` cleans up after a normal exit, but the container belongs to
|
|
238
|
+
# the daemon; killing the docker client leaves it running. A name is
|
|
239
|
+
# what lets cleanup() stop it after a timeout or Ctrl-C.
|
|
240
|
+
argv.extend(["--name", name])
|
|
241
|
+
user = _docker_user()
|
|
242
|
+
if user is not None:
|
|
243
|
+
# Containers run as root by default, which would leave root-owned
|
|
244
|
+
# files in the mounted workspace.
|
|
245
|
+
argv.extend(["--user", user])
|
|
246
|
+
mounted: set[str] = set()
|
|
247
|
+
for root in request.policy.write_roots(request.cwd):
|
|
248
|
+
text = str(root)
|
|
249
|
+
if text in mounted:
|
|
250
|
+
continue
|
|
251
|
+
mounted.add(text)
|
|
252
|
+
argv.extend(["--volume", f"{text}:{text}"])
|
|
253
|
+
argv.extend(
|
|
254
|
+
[
|
|
255
|
+
"--workdir",
|
|
256
|
+
str(request.cwd),
|
|
257
|
+
image,
|
|
258
|
+
"sh",
|
|
259
|
+
"-lc",
|
|
260
|
+
request.command,
|
|
261
|
+
]
|
|
262
|
+
)
|
|
263
|
+
return argv
|
|
264
|
+
|
|
265
|
+
def temp_path(self, request: CommandRequest) -> str | None:
|
|
266
|
+
return "/tmp" if request.temp_dir is not None else None
|
|
267
|
+
|
|
268
|
+
def cleanup(self, request: CommandRequest) -> None:
|
|
269
|
+
"""Stop and remove a container whose docker client was killed."""
|
|
270
|
+
name = self._container_name(request)
|
|
271
|
+
if name is None:
|
|
272
|
+
return
|
|
273
|
+
executable = shutil.which("docker")
|
|
274
|
+
if executable is None:
|
|
275
|
+
return
|
|
276
|
+
for arguments in (["kill", name], ["rm", "-f", name]):
|
|
277
|
+
try:
|
|
278
|
+
subprocess.run(
|
|
279
|
+
[executable, *arguments],
|
|
280
|
+
stdin=subprocess.DEVNULL,
|
|
281
|
+
stdout=subprocess.DEVNULL,
|
|
282
|
+
stderr=subprocess.DEVNULL,
|
|
283
|
+
timeout=10,
|
|
284
|
+
check=False,
|
|
285
|
+
)
|
|
286
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
287
|
+
continue
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _docker_user() -> str | None:
|
|
291
|
+
"""Return the ``uid:gid`` the container should write files as, if known."""
|
|
292
|
+
getuid = getattr(os, "getuid", None)
|
|
293
|
+
getgid = getattr(os, "getgid", None)
|
|
294
|
+
if getuid is None or getgid is None: # pragma: no cover - Windows
|
|
295
|
+
return None
|
|
296
|
+
try:
|
|
297
|
+
return f"{getuid()}:{getgid()}"
|
|
298
|
+
except OSError: # pragma: no cover - defensive
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _auto_backend() -> SandboxBackend:
|
|
303
|
+
"""Pick the first genuinely available isolating backend, else local."""
|
|
304
|
+
candidates: tuple[SandboxBackend, ...] = (
|
|
305
|
+
BwrapBackend(),
|
|
306
|
+
FirejailBackend(),
|
|
307
|
+
MacSandboxExecBackend(),
|
|
308
|
+
)
|
|
309
|
+
for backend in candidates:
|
|
310
|
+
if backend.available():
|
|
311
|
+
return backend
|
|
312
|
+
return LocalSandboxBackend()
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def backend_for(name: str) -> SandboxBackend:
|
|
316
|
+
"""Instantiate the backend named by configuration."""
|
|
317
|
+
if name == "auto":
|
|
318
|
+
return _auto_backend()
|
|
319
|
+
factories: dict[str, type[SandboxBackend]] = {
|
|
320
|
+
"local": LocalSandboxBackend,
|
|
321
|
+
"bwrap": BwrapBackend,
|
|
322
|
+
"firejail": FirejailBackend,
|
|
323
|
+
"sandbox-exec": MacSandboxExecBackend,
|
|
324
|
+
"docker": DockerBackend,
|
|
325
|
+
}
|
|
326
|
+
factory = factories.get(name)
|
|
327
|
+
if factory is None:
|
|
328
|
+
raise ConfigError(f"unknown sandbox backend: {name}")
|
|
329
|
+
return factory()
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def available_backends() -> dict[str, bool]:
|
|
333
|
+
"""Return a platform capability report for the CLI."""
|
|
334
|
+
return {
|
|
335
|
+
name: factory().available()
|
|
336
|
+
for name, factory in (
|
|
337
|
+
("local", LocalSandboxBackend),
|
|
338
|
+
("bwrap", BwrapBackend),
|
|
339
|
+
("firejail", FirejailBackend),
|
|
340
|
+
("sandbox-exec", MacSandboxExecBackend),
|
|
341
|
+
("docker", DockerBackend),
|
|
342
|
+
)
|
|
343
|
+
}
|