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,286 @@
1
+ """Credential-like path detection and search exclusions.
2
+
3
+ The list is intentionally narrow: false positives make the agent unusable, so
4
+ this is a last line of defence rather than the only sandbox boundary.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from pathlib import Path
11
+
12
+ SENSITIVE_DIRECTORY_NAMES = frozenset(
13
+ {
14
+ ".ssh",
15
+ ".aws",
16
+ ".azure",
17
+ ".gnupg",
18
+ ".kube",
19
+ ".minimalcode",
20
+ ".docker",
21
+ ".password-store",
22
+ }
23
+ )
24
+ SENSITIVE_FILE_NAMES = frozenset(
25
+ {
26
+ ".env",
27
+ # direnv exports real tokens from `.envrc`, one character away from the
28
+ # `.env` that is already blocked.
29
+ ".envrc",
30
+ ".netrc",
31
+ ".npmrc",
32
+ ".pgpass",
33
+ ".git-credentials",
34
+ ".htpasswd",
35
+ ".pypirc",
36
+ ".vault-token",
37
+ ".dockercfg",
38
+ ".yarnrc.yml",
39
+ "credentials",
40
+ "credentials.json",
41
+ "secrets",
42
+ "secrets.json",
43
+ "id_rsa",
44
+ "id_dsa",
45
+ "id_ecdsa",
46
+ "id_ed25519",
47
+ "id_ed25519_sk",
48
+ }
49
+ )
50
+ # Credential files are just as often YAML/TOML (`secrets.yaml`,
51
+ # `credentials.toml`); matching only the bare and `.json` spellings left the
52
+ # most common compose/Ansible layout readable.
53
+ SENSITIVE_FILE_STEMS = ("credentials", "secrets")
54
+ SENSITIVE_STEM_SUFFIXES = (".yaml", ".yml", ".toml", ".ini", ".conf", ".cfg", ".env")
55
+ SENSITIVE_KEY_PREFIXES = ("id_rsa", "id_dsa", "id_ecdsa", "id_ed25519")
56
+ SENSITIVE_SUFFIXES = (
57
+ ".pem",
58
+ ".key",
59
+ ".p12",
60
+ ".pfx",
61
+ ".keystore",
62
+ ".jks",
63
+ ".ppk",
64
+ ".p8",
65
+ ".kdbx",
66
+ )
67
+ # `.git/config` often carries a remote URL with an embedded token, while the
68
+ # rest of `.git/` (HEAD, refs) is harmless and useful to read.
69
+ SENSITIVE_PATH_SUFFIXES = ((".git", "config"),)
70
+ SAFE_ENV_TEMPLATES = frozenset(
71
+ {".env.example", ".env.sample", ".env.template", ".env.dist"}
72
+ )
73
+
74
+ # ripgrep globs that keep bulk searches from echoing credentials. They are
75
+ # exclusion-only: a positive re-include would widen a `find` pattern. The
76
+ # globs cannot express every policy rule (case-insensitive suffixes, and the
77
+ # difference between `.env.example` and `.env.local`), so result paths are
78
+ # filtered through `SandboxRunner.is_allowed` as a second line of defence.
79
+ SENSITIVE_SEARCH_GLOBS: tuple[str, ...] = (
80
+ "!**/.env",
81
+ "!**/.env-*",
82
+ "!**/.envrc",
83
+ "!**/.netrc",
84
+ "!**/.npmrc",
85
+ "!**/.pgpass",
86
+ "!**/.git-credentials",
87
+ "!**/.htpasswd",
88
+ "!**/.pypirc",
89
+ "!**/.vault-token",
90
+ "!**/.dockercfg",
91
+ "!**/.yarnrc.yml",
92
+ "!**/credentials",
93
+ "!**/credentials.json",
94
+ # Listed individually: `credentials.*` would also hide `credentials.py`,
95
+ # which `is_sensitive_path` allows and `read` can still open.
96
+ "!**/credentials.yaml",
97
+ "!**/credentials.yml",
98
+ "!**/credentials.toml",
99
+ "!**/secrets",
100
+ "!**/secrets.json",
101
+ "!**/secrets.yaml",
102
+ "!**/secrets.yml",
103
+ "!**/secrets.toml",
104
+ "!**/.git/config",
105
+ "!**/id_rsa*",
106
+ "!**/id_dsa*",
107
+ "!**/id_ecdsa*",
108
+ "!**/id_ed25519*",
109
+ "!**/*.pem",
110
+ "!**/*.key",
111
+ "!**/*.p12",
112
+ "!**/*.pfx",
113
+ "!**/*.keystore",
114
+ "!**/*.jks",
115
+ "!**/*.ppk",
116
+ "!**/*.p8",
117
+ "!**/*.kdbx",
118
+ "!**/.ssh/**",
119
+ "!**/.aws/**",
120
+ "!**/.azure/**",
121
+ "!**/.gnupg/**",
122
+ "!**/.kube/**",
123
+ "!**/.minimalcode/**",
124
+ "!**/.docker/**",
125
+ "!**/.password-store/**",
126
+ "!**/.password-store/**",
127
+ )
128
+ # Word separators and quote handling for `sensitive_command_paths`. The shell
129
+ # gate only sees a string, so it needs its own minimal scanner.
130
+ _COMMAND_WORD_BREAK = frozenset(" \t\r\n|;&(){}<>`")
131
+ _COMMAND_QUOTES = frozenset("'\"")
132
+ _COMMAND_WORD_TRIM = "=,:;|&" + "\"'`"
133
+ # A quoted word after one of these is a command, not data: `cmd /c "type .env"`
134
+ # and `pwsh -Command "Get-Content .env"` must be inspected like a bare command.
135
+ _INLINE_FLAGS = frozenset(
136
+ {
137
+ "-c",
138
+ "/c",
139
+ "/k",
140
+ "-command",
141
+ "--command",
142
+ "-commandwithargs",
143
+ "-lc",
144
+ "-ic",
145
+ }
146
+ )
147
+ _MAX_INLINE_DEPTH = 3
148
+
149
+
150
+ def _looks_sensitive_token(token: str) -> bool:
151
+ """Return True when one shell word names a sensitive path."""
152
+ candidate = token.split("=", 1)[-1] if "=" in token else token
153
+ candidate = candidate.strip(_COMMAND_WORD_TRIM)
154
+ if not candidate or candidate.startswith("-"):
155
+ return False
156
+ try:
157
+ return is_sensitive_path(Path(candidate).expanduser())
158
+ except (OSError, RuntimeError, ValueError):
159
+ return False
160
+
161
+
162
+ def _command_words(command: str) -> list[tuple[str, bool]]:
163
+ """Split a command line into ``(word, quoted)`` pairs."""
164
+ words: list[tuple[str, bool]] = []
165
+ word: list[str] = []
166
+ quote = ""
167
+ quoted = False
168
+ for char in command:
169
+ if quote:
170
+ if char == quote:
171
+ quote = ""
172
+ else:
173
+ word.append(char)
174
+ continue
175
+ if char in _COMMAND_QUOTES:
176
+ quote = char
177
+ quoted = True
178
+ continue
179
+ if char in _COMMAND_WORD_BREAK:
180
+ if word:
181
+ words.append(("".join(word), quoted))
182
+ word = []
183
+ quoted = False
184
+ continue
185
+ word.append(char)
186
+ if word:
187
+ words.append(("".join(word), quoted))
188
+ return words
189
+
190
+
191
+ def sensitive_command_paths(command: str, depth: int = 0) -> tuple[str, ...]:
192
+ """Return words in a shell command that name a sensitive path.
193
+
194
+ `read` and `write` refuse `.env` / `~/.ssh/id_rsa`, but the shell only took
195
+ a command string, so `type .env` and `cat ~/.ssh/id_rsa` returned the same
196
+ credentials the tools refuse. A word inside quotes is usually data
197
+ (`git commit -m "document .env"`), except when it is the command handed to
198
+ `sh -c` / `cmd /c` / `pwsh -Command`, which is scanned in turn.
199
+ """
200
+ words = _command_words(command)
201
+ hits: list[str] = []
202
+ previous = ""
203
+ for token, quoted in words:
204
+ if quoted and previous.lower() in _INLINE_FLAGS and depth < _MAX_INLINE_DEPTH:
205
+ for hit in sensitive_command_paths(token, depth + 1):
206
+ if hit not in hits:
207
+ hits.append(hit)
208
+ previous = token
209
+ continue
210
+ if (
211
+ _looks_sensitive_token(token)
212
+ and token not in hits
213
+ and not _directory_option_value(token, previous)
214
+ ):
215
+ hits.append(token)
216
+ previous = token
217
+ return tuple(hits)
218
+
219
+
220
+ def _directory_option_value(token: str, option: str) -> bool:
221
+ """Return True for a directory-valued long option such as `--home`.
222
+
223
+ The agent itself runs `minimalcode --home <dir>`, and a config directory
224
+ named `.MinimalCode` matched the sensitive-directory rule, so every nested
225
+ agent was refused. Naming a directory is not reading a credential: the
226
+ exemption needs a `--long` option *and* a bare directory name, so
227
+ `Get-Content -Path "C:\\x\\.ssh\\id_rsa"` is still denied.
228
+ """
229
+ if not option.startswith("--"):
230
+ return False
231
+ candidate = token.strip(_COMMAND_WORD_TRIM)
232
+ if not candidate:
233
+ return False
234
+ try:
235
+ parts = _normalized_parts(Path(candidate).expanduser())
236
+ except (OSError, RuntimeError, ValueError):
237
+ return False
238
+ if not parts:
239
+ return False
240
+ name = parts[-1]
241
+ return name in SENSITIVE_DIRECTORY_NAMES and name not in SENSITIVE_FILE_NAMES
242
+
243
+
244
+ def _normalized_parts(path: Path) -> list[str]:
245
+ """Lowercase path components, folding Windows name quirks when present."""
246
+ parts = [part.lower() for part in path.parts]
247
+ if os.name == "nt":
248
+ # NTFS ignores trailing dots/spaces on lookup, and `name:stream` opens
249
+ # an alternate data stream of `name`; neither spelling should dodge
250
+ # the name rules.
251
+ normalized: list[str] = []
252
+ for part in parts:
253
+ part = part.rstrip(" .")
254
+ if part and part[-1] != "\\":
255
+ part = part.split(":", 1)[0]
256
+ normalized.append(part)
257
+ parts = normalized
258
+ return parts
259
+
260
+
261
+ def is_sensitive_path(path: Path) -> bool:
262
+ """Return True for credential-like paths that the sandbox blocks by default."""
263
+ parts = _normalized_parts(path)
264
+ for part in parts:
265
+ if part in SENSITIVE_DIRECTORY_NAMES:
266
+ return True
267
+ if not parts:
268
+ return False
269
+ name = parts[-1]
270
+ # Checked before the generic name rules so `.env.example` stays readable.
271
+ if name in SAFE_ENV_TEMPLATES:
272
+ return False
273
+ if name in SENSITIVE_FILE_NAMES:
274
+ return True
275
+ if name.startswith((".env.", ".env-")):
276
+ return True
277
+ if name.startswith(SENSITIVE_KEY_PREFIXES) and not name.endswith(".pub"):
278
+ return True
279
+ for stem in SENSITIVE_FILE_STEMS:
280
+ if name == stem or (
281
+ name.startswith(stem + ".") and name.endswith(SENSITIVE_STEM_SUFFIXES)
282
+ ):
283
+ return True
284
+ if tuple(parts[-2:]) in SENSITIVE_PATH_SUFFIXES:
285
+ return True
286
+ return any(name.endswith(suffix) for suffix in SENSITIVE_SUFFIXES)