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.
Files changed (58) hide show
  1. minimalcode/__init__.py +3 -0
  2. minimalcode/__main__.py +3 -0
  3. minimalcode/agent.py +1084 -0
  4. minimalcode/cli.py +1520 -0
  5. minimalcode/clipboard.py +212 -0
  6. minimalcode/config.py +1065 -0
  7. minimalcode/defaults.json +491 -0
  8. minimalcode/errors.py +31 -0
  9. minimalcode/highlight/__init__.py +47 -0
  10. minimalcode/highlight/code.py +261 -0
  11. minimalcode/highlight/color.py +58 -0
  12. minimalcode/highlight/markdown.py +710 -0
  13. minimalcode/highlight/palette.py +73 -0
  14. minimalcode/highlight/sanitize.py +175 -0
  15. minimalcode/highlight/thinking.py +71 -0
  16. minimalcode/httpclient.py +278 -0
  17. minimalcode/importer.py +253 -0
  18. minimalcode/prompt.py +195 -0
  19. minimalcode/provider/__init__.py +28 -0
  20. minimalcode/provider/anthropic.py +805 -0
  21. minimalcode/provider/anthropic_env.py +161 -0
  22. minimalcode/provider/chat.py +394 -0
  23. minimalcode/provider/client.py +243 -0
  24. minimalcode/provider/common.py +176 -0
  25. minimalcode/provider/deepseek_web.py +1158 -0
  26. minimalcode/provider/factory.py +63 -0
  27. minimalcode/provider/responses.py +692 -0
  28. minimalcode/provider/sha3_wasm_bg.wasm +0 -0
  29. minimalcode/provider/transport.py +241 -0
  30. minimalcode/provider/types.py +54 -0
  31. minimalcode/sandbox/__init__.py +64 -0
  32. minimalcode/sandbox/audit.py +120 -0
  33. minimalcode/sandbox/backends.py +343 -0
  34. minimalcode/sandbox/execution.py +435 -0
  35. minimalcode/sandbox/network.py +793 -0
  36. minimalcode/sandbox/policy.py +253 -0
  37. minimalcode/sandbox/runner.py +347 -0
  38. minimalcode/sandbox/sensitive.py +286 -0
  39. minimalcode/session.py +465 -0
  40. minimalcode/shell.py +333 -0
  41. minimalcode/subagent.py +120 -0
  42. minimalcode/tools/__init__.py +46 -0
  43. minimalcode/tools/files.py +1139 -0
  44. minimalcode/tools/process.py +164 -0
  45. minimalcode/tools/remote.py +74 -0
  46. minimalcode/tools/runtime.py +219 -0
  47. minimalcode/tools/schemas.py +418 -0
  48. minimalcode/tools/search.py +521 -0
  49. minimalcode/tools/search_tools.py +583 -0
  50. minimalcode/tools/validation.py +99 -0
  51. minimalcode/vision.py +231 -0
  52. minimalcode/web.py +238 -0
  53. minimalcode-1.5.2.dist-info/METADATA +646 -0
  54. minimalcode-1.5.2.dist-info/RECORD +58 -0
  55. minimalcode-1.5.2.dist-info/WHEEL +5 -0
  56. minimalcode-1.5.2.dist-info/entry_points.txt +2 -0
  57. minimalcode-1.5.2.dist-info/licenses/LICENSE +201 -0
  58. minimalcode-1.5.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,164 @@
1
+ """Shell and subagent execution tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from typing import Any
7
+
8
+ from ..errors import ToolError
9
+ from ..provider.anthropic_env import ANTHROPIC_ENV_PROVIDER
10
+ from ..shell import MAX_TIMEOUT_SECONDS, powershell_heredoc_delimiter, run_shell
11
+ from ..subagent import PREAMBLE as SUBAGENT_PREAMBLE
12
+ from ..subagent import build_command as build_subagent_command
13
+ from ..subagent import child_env as subagent_env
14
+
15
+ _POWERSHELL_KINDS = frozenset({"pwsh7", "windows_powershell"})
16
+
17
+
18
+ def _clamp_timeout(value: float) -> float:
19
+ """Apply the ceiling `run_shell` enforces, so the reported value is real."""
20
+ return max(1.0, min(value, MAX_TIMEOUT_SECONDS))
21
+
22
+
23
+ class ProcessToolsMixin:
24
+ """Run commands and nested agents inside the active sandbox policy."""
25
+
26
+ def _subagent_sandbox_options(self) -> tuple[list[str], list[tuple[str, str]]]:
27
+ """Mirror the current in-memory sandbox policy onto the child command.
28
+
29
+ CLI flags are applied to ``settings.data`` in memory only; without this
30
+ the child reloads ``settings.json`` and silently drops ``--sandbox``,
31
+ ``--sandbox-network``, and the other per-run overrides.
32
+ """
33
+ policy = self.sandbox.policy
34
+ if not policy.enabled:
35
+ return ["--no-sandbox"], []
36
+ flags = ["--sandbox"]
37
+ options: list[tuple[str, str]] = [
38
+ ("--sandbox-backend", policy.backend),
39
+ ("--sandbox-network", policy.network_policy),
40
+ ("--sandbox-timeout", f"{policy.timeout_seconds:g}"),
41
+ ("--sandbox-max-output", str(policy.max_output_chars)),
42
+ ]
43
+ for root in policy.allowed_roots:
44
+ options.append(("--sandbox-root", str(root)))
45
+ for host in policy.allowed_hosts:
46
+ options.append(("--sandbox-allow-host", host))
47
+ flags.append(
48
+ "--sandbox-allow-sensitive"
49
+ if policy.allow_sensitive_files
50
+ else "--no-sandbox-allow-sensitive"
51
+ )
52
+ return flags, options
53
+
54
+ def shell(
55
+ self, command: str, timeout: float | None = None, cwd: str = "."
56
+ ) -> dict[str, Any]:
57
+ """Run one command in the configured shell and workspace."""
58
+ if not isinstance(command, str):
59
+ raise ToolError("command must be a string")
60
+ if self.shell_spec.kind in _POWERSHELL_KINDS:
61
+ delimiter = powershell_heredoc_delimiter(command)
62
+ if delimiter is not None:
63
+ raise ToolError(
64
+ f"{self.shell_spec.label} does not support bash heredocs "
65
+ f"(<<{delimiter}). Write the payload with write/append_file "
66
+ "and run the file, use a PowerShell here-string (@'...'@), "
67
+ "or pass stdin; the same command will fail again."
68
+ )
69
+ workdir = self._path(cwd, operation="execute")
70
+ if not workdir.is_dir():
71
+ raise ToolError(f"working directory not found: {workdir}")
72
+ if timeout is not None and (
73
+ isinstance(timeout, bool) or not isinstance(timeout, (int, float))
74
+ ):
75
+ raise ToolError("timeout must be a number")
76
+ try:
77
+ requested_timeout = float(
78
+ timeout if timeout is not None else self.shell_timeout
79
+ )
80
+ except (OverflowError, ValueError) as exc:
81
+ raise ToolError("timeout must be a number") from exc
82
+ if math.isnan(requested_timeout):
83
+ raise ToolError("timeout must be a number")
84
+ # The configured value is both the default and the hard ceiling.
85
+ effective_timeout = max(1.0, min(requested_timeout, self.shell_timeout))
86
+ return run_shell(
87
+ command,
88
+ shell=self.shell_spec,
89
+ cwd=workdir,
90
+ timeout=effective_timeout,
91
+ max_output_chars=self.max_output_chars,
92
+ sandbox=self.sandbox,
93
+ )
94
+
95
+ def subagent(
96
+ self,
97
+ task: str,
98
+ cwd: str = ".",
99
+ timeout: float | None = None,
100
+ max_turns: int | None = None,
101
+ ) -> dict[str, Any]:
102
+ """Run a nested ``minimalcode -p`` process and return its answer."""
103
+ if not self.subagent_enabled:
104
+ raise ToolError(
105
+ "subagent is disabled here: set subagent.enabled or raise "
106
+ f"subagent.max_depth (currently {self.subagent_max_depth})"
107
+ )
108
+ if not isinstance(task, str) or not task.strip():
109
+ raise ToolError("task must be a non-empty string")
110
+ workdir = self._path(cwd, operation="execute")
111
+ if not workdir.is_dir():
112
+ raise ToolError(f"working directory not found: {workdir}")
113
+ if timeout is not None and (
114
+ isinstance(timeout, bool) or not isinstance(timeout, (int, float))
115
+ ):
116
+ raise ToolError("timeout must be a number")
117
+ try:
118
+ requested_timeout = float(
119
+ timeout if timeout is not None else self.subagent_timeout
120
+ )
121
+ except (OverflowError, ValueError) as exc:
122
+ raise ToolError("timeout must be a number") from exc
123
+ if math.isnan(requested_timeout):
124
+ raise ToolError("timeout must be a number")
125
+ limits = [requested_timeout, self.subagent_timeout]
126
+ if self.sandbox.policy.enabled:
127
+ # Only an active sandbox caps the child's runtime.
128
+ limits.append(self.sandbox.policy.timeout_seconds)
129
+ effective_timeout = _clamp_timeout(min(limits))
130
+ turns = self.subagent_max_turns if max_turns is None else max_turns
131
+ if isinstance(turns, bool) or not isinstance(turns, int):
132
+ raise ToolError("max_turns must be an integer")
133
+ turns = max(1, min(turns, self.subagent_max_turns))
134
+
135
+ sandbox_flags, sandbox_options = self._subagent_sandbox_options()
136
+ extra_flags = list(sandbox_flags)
137
+ provider = self.provider_name
138
+ if provider == ANTHROPIC_ENV_PROVIDER:
139
+ # The env-derived provider exists only in this process; the child
140
+ # must rebuild it from the inherited ANTHROPIC_* environment.
141
+ extra_flags.append("--anthropic-env")
142
+ provider = None
143
+ command = build_subagent_command(
144
+ self.shell_spec,
145
+ cwd=workdir,
146
+ home=self.settings.home,
147
+ provider=provider,
148
+ model=self.model,
149
+ thinking=self.thinking,
150
+ max_turns=turns,
151
+ extra_flags=extra_flags,
152
+ extra_options=sandbox_options,
153
+ )
154
+ result = run_shell(
155
+ command,
156
+ shell=self.shell_spec,
157
+ cwd=workdir,
158
+ timeout=effective_timeout,
159
+ max_output_chars=self.max_output_chars,
160
+ env=subagent_env(self.subagent_depth + 1),
161
+ stdin_text=SUBAGENT_PREAMBLE + task.strip(),
162
+ sandbox=self.sandbox,
163
+ )
164
+ return {**result, "command": command, "timeout_seconds": effective_timeout}
@@ -0,0 +1,74 @@
1
+ """Optional provider-backed tools: image understanding and web access."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ..errors import ToolError
8
+ from ..vision import SUPPORTED_IMAGE_FORMATS, image_media_type
9
+
10
+
11
+ class RemoteToolsMixin:
12
+ """Tools that delegate to a configured provider client."""
13
+
14
+ def read_image(self, path: str, prompt: str | None = None) -> dict[str, Any]:
15
+ """Describe a workspace image through the provider's vision model."""
16
+ if not self.vision_enabled or self.vision_client is None:
17
+ raise ToolError("read_image is not enabled for the current provider")
18
+ if prompt is not None and not isinstance(prompt, str):
19
+ raise ToolError("prompt must be a string or null")
20
+ target = self._path(path, operation="read")
21
+ if not target.is_file():
22
+ raise ToolError(f"file not found: {target}")
23
+ try:
24
+ # Read one byte past the limit instead of trusting stat(): the file
25
+ # can grow between the check and the read.
26
+ with target.open("rb") as handle:
27
+ data = handle.read(self.vision_client.max_image_bytes + 1)
28
+ except OSError as exc:
29
+ raise ToolError(f"cannot read image: {target}: {exc}") from exc
30
+ if len(data) > self.vision_client.max_image_bytes:
31
+ raise ToolError(
32
+ f"image is too large: more than "
33
+ f"vision.max_image_bytes ({self.vision_client.max_image_bytes})"
34
+ )
35
+ media_type = image_media_type(data)
36
+ if media_type is None:
37
+ raise ToolError(
38
+ f"unsupported image format; expected {SUPPORTED_IMAGE_FORMATS}"
39
+ )
40
+ description = self.vision_client.describe(
41
+ media_type, data, question=prompt or None
42
+ )
43
+ return {
44
+ "status": "success",
45
+ "path": str(target),
46
+ "media_type": media_type,
47
+ "bytes": len(data),
48
+ "description": description,
49
+ }
50
+
51
+ def web_search(
52
+ self,
53
+ query: str,
54
+ count: int | None = None,
55
+ need_content: bool = False,
56
+ ori_question: str | None = None,
57
+ localization: str | None = None,
58
+ ) -> dict[str, Any]:
59
+ """Search the web through the configured provider client."""
60
+ if not self.web_enabled or self.web_client is None:
61
+ raise ToolError("web tools are not enabled for the current provider")
62
+ return self.web_client.search(
63
+ query,
64
+ count=count,
65
+ need_content=need_content,
66
+ ori_question=ori_question,
67
+ localization=localization,
68
+ )
69
+
70
+ def web_fetch(self, url: str) -> dict[str, Any]:
71
+ """Fetch one HTTP(S) page through the configured provider client."""
72
+ if not self.web_enabled or self.web_client is None:
73
+ raise ToolError("web tools are not enabled for the current provider")
74
+ return self.web_client.fetch(url)
@@ -0,0 +1,219 @@
1
+ """The ToolRuntime facade: configuration, path policy, and dispatch."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from ..config import Settings
10
+ from ..errors import ConfigError, ToolError
11
+ from ..sandbox import SandboxRunner
12
+ from ..shell import ShellSpec
13
+ from ..subagent import depth_from_env as subagent_depth_from_env
14
+ from ..vision import ProviderVisionClient
15
+ from ..web import ProviderWebClient
16
+ from .files import FileToolsMixin
17
+ from .process import ProcessToolsMixin
18
+ from .remote import RemoteToolsMixin
19
+ from .schemas import READ_ONLY_TOOLS, TOOL_NAMES
20
+ from .search_tools import SearchToolsMixin
21
+ from .validation import check_argument_types, clip
22
+
23
+
24
+ class ToolRuntime(
25
+ FileToolsMixin,
26
+ SearchToolsMixin,
27
+ ProcessToolsMixin,
28
+ RemoteToolsMixin,
29
+ ):
30
+ """Execute model tool calls with workspace and sandbox enforcement.
31
+
32
+ The methods are grouped into mixins by concern; this class owns the shared
33
+ settings, path resolver, and dispatcher so every tool sees one policy.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ settings: Settings,
39
+ cwd: Path,
40
+ shell: ShellSpec,
41
+ *,
42
+ web_client: ProviderWebClient | None = None,
43
+ vision_client: ProviderVisionClient | None = None,
44
+ provider_name: str | None = None,
45
+ model: str | None = None,
46
+ thinking: str | None = None,
47
+ read_only: bool = False,
48
+ ):
49
+ self.settings = settings
50
+ self.cwd = cwd.resolve()
51
+ self.shell_spec = shell
52
+ self.web_client = web_client
53
+ self.web_enabled = bool(web_client and web_client.enabled)
54
+ self.vision_client = vision_client
55
+ self.vision_enabled = bool(vision_client and vision_client.enabled)
56
+ self.provider_name = provider_name
57
+ self.model = model
58
+ self.thinking = thinking
59
+
60
+ # Subagent depth is inherited from the child environment so a nested
61
+ # process cannot recurse without bound.
62
+ self.subagent_depth = subagent_depth_from_env()
63
+ self.subagent_max_depth = int(settings.get("subagent.max_depth", 2))
64
+ self.subagent_enabled = (
65
+ bool(settings.get("subagent.enabled", True))
66
+ and self.subagent_depth < self.subagent_max_depth
67
+ )
68
+ self.subagent_timeout = float(settings.get("subagent.timeout_seconds", 900))
69
+ self.subagent_max_turns = int(settings.get("subagent.max_turns", 40))
70
+ self.subagent_max_parallel = max(
71
+ 1, int(settings.get("subagent.max_parallel", 4))
72
+ )
73
+
74
+ self.allow_outside = bool(settings.get("tools.allow_outside_workspace", False))
75
+ self.max_file_chars = int(settings.get("tools.max_file_chars", 160_000))
76
+ self.max_file_bytes = int(settings.get("tools.max_file_bytes", 5_000_000))
77
+ self.max_results = int(settings.get("tools.max_results", 200))
78
+ self.max_parallel = max(1, int(settings.get("tools.max_parallel", 4)))
79
+ # ``tools.allow`` and ``--read-only`` narrow the same set, so the two
80
+ # compose instead of the flag silently widening what the file allows.
81
+ self.read_only = bool(read_only)
82
+ self.allowed_tools = self._allowed_tool_names(
83
+ settings, read_only=self.read_only
84
+ )
85
+ self.shell_timeout = float(settings.get("shell.timeout_seconds", 120))
86
+ self.max_output_chars = int(settings.get("shell.max_output_chars", 60_000))
87
+ self.sandbox = SandboxRunner.from_settings(settings, self.cwd)
88
+ if self.sandbox.policy.enabled:
89
+ cap = self.sandbox.policy.timeout_seconds
90
+ if self.subagent_timeout > cap:
91
+ # A subagent is one sandboxed command, so the policy cap always
92
+ # wins. Clamp here so the value the tool reports is the value
93
+ # that actually applies, instead of silently killing the child
94
+ # at the sandbox timeout.
95
+ self.subagent_timeout = cap
96
+ self.sandbox.audit.note(
97
+ "subagent.timeout_seconds capped by sandbox.timeout_seconds "
98
+ f"({cap:g}s)"
99
+ )
100
+
101
+ @staticmethod
102
+ def _allowed_tool_names(settings: Settings, *, read_only: bool) -> frozenset[str]:
103
+ """Resolve ``tools.allow`` and ``--read-only`` into one name set.
104
+
105
+ A typo in ``tools.allow`` would otherwise intersect down to an empty
106
+ set, silently leaving an agent that cannot do anything, so unknown
107
+ names are a configuration error rather than a no-op.
108
+ """
109
+ configured = settings.get("tools.allow")
110
+ allowed = set(TOOL_NAMES)
111
+ if configured is not None:
112
+ if not isinstance(configured, list) or not configured:
113
+ raise ConfigError("tools.allow must be a non-empty list of tool names")
114
+ unknown = sorted(
115
+ str(name)
116
+ for name in configured
117
+ if not isinstance(name, str) or name not in TOOL_NAMES
118
+ )
119
+ if unknown:
120
+ raise ConfigError(
121
+ "tools.allow contains unknown tools: " + ", ".join(unknown)
122
+ )
123
+ allowed &= {name for name in configured if isinstance(name, str)}
124
+ if read_only:
125
+ allowed &= READ_ONLY_TOOLS
126
+ if not allowed:
127
+ raise ConfigError(
128
+ "no tools are enabled; widen tools.allow or drop --read-only"
129
+ )
130
+ return frozenset(allowed)
131
+
132
+ def _path(self, raw: str, *, operation: str = "read") -> Path:
133
+ """Resolve a tool path through the sandbox's shared policy."""
134
+ return self.sandbox.resolve_path(
135
+ raw,
136
+ cwd=self.cwd,
137
+ operation=operation,
138
+ allow_outside=self.allow_outside,
139
+ )
140
+
141
+ def sandbox_summary(self) -> dict[str, Any]:
142
+ """Return the serializable sandbox status used by the CLI."""
143
+ return self.sandbox.summary()
144
+
145
+ def dispatch(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]:
146
+ """Validate and run one named tool call.
147
+
148
+ Lookup is explicit rather than ``getattr`` so a model cannot reach
149
+ helper methods or private attributes by name.
150
+ """
151
+ handlers = {
152
+ "read": self.read,
153
+ "read_range": self.read_range,
154
+ "write": self.write,
155
+ "append_file": self.append_file,
156
+ "edit": self.edit,
157
+ "shell": self.shell,
158
+ "subagent": self.subagent,
159
+ "grep": self.grep,
160
+ "find": self.find,
161
+ "read_image": self.read_image,
162
+ "web_search": self.web_search,
163
+ "web_fetch": self.web_fetch,
164
+ }
165
+ handler = handlers.get(name)
166
+ if handler is None:
167
+ raise ToolError(f"unknown tool: {name}")
168
+ if name not in self.allowed_tools:
169
+ # The model can only ask for what was advertised, but a replayed
170
+ # session or a provider that ignores the schema list must still be
171
+ # stopped here: this is the enforcement point, not the schema.
172
+ reason = (
173
+ "--read-only"
174
+ if self.read_only and name not in READ_ONLY_TOOLS
175
+ else "tools.allow"
176
+ )
177
+ raise ToolError(f"tool {name!r} is disabled by {reason}")
178
+ if not isinstance(arguments, dict):
179
+ raise ToolError("tool arguments must be an object")
180
+ try:
181
+ inspect.signature(handler).bind(**arguments)
182
+ except TypeError as exc:
183
+ raise ToolError(f"invalid arguments for {name}: {exc}") from exc
184
+ check_argument_types(name, handler, arguments)
185
+ return handler(**arguments)
186
+
187
+ @staticmethod
188
+ def activity(name: str, arguments: dict[str, Any]) -> str:
189
+ """Return a short one-line preview for the terminal activity stream."""
190
+ if name == "shell":
191
+ detail = clip(str(arguments.get("command", "")))
192
+ elif name == "subagent":
193
+ detail = clip(str(arguments.get("task", "")))
194
+ elif name in {
195
+ "read",
196
+ "read_range",
197
+ "write",
198
+ "append_file",
199
+ "edit",
200
+ "read_image",
201
+ }:
202
+ detail = clip(str(arguments.get("path", "")))
203
+ elif name == "grep":
204
+ detail = (
205
+ f"{clip(str(arguments.get('pattern', '')))} in "
206
+ f"{clip(str(arguments.get('path', '.')))}"
207
+ )
208
+ elif name == "find":
209
+ detail = (
210
+ f"{clip(str(arguments.get('pattern', '*')))} in "
211
+ f"{clip(str(arguments.get('path', '.')))}"
212
+ )
213
+ elif name == "web_search":
214
+ detail = clip(str(arguments.get("query", "")))
215
+ elif name == "web_fetch":
216
+ detail = clip(str(arguments.get("url", "")))
217
+ else:
218
+ detail = ""
219
+ return f"[{name}] {detail}".rstrip()