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,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)
|