mycode-coding-agent 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.
- mycode/__init__.py +0 -0
- mycode/adapters/__init__.py +21 -0
- mycode/adapters/jsonl.py +692 -0
- mycode/agent/__init__.py +25 -0
- mycode/agent/events.py +111 -0
- mycode/agent/outcome.py +103 -0
- mycode/agent/progress.py +373 -0
- mycode/agent/runner.py +1481 -0
- mycode/application/__init__.py +38 -0
- mycode/application/agent_session.py +367 -0
- mycode/application/events.py +59 -0
- mycode/application/runtime.py +211 -0
- mycode/application/sessions.py +180 -0
- mycode/cli.py +840 -0
- mycode/config.py +355 -0
- mycode/context/__init__.py +1 -0
- mycode/context/artifacts.py +672 -0
- mycode/context/budget.py +752 -0
- mycode/context/builder.py +112 -0
- mycode/context/compact.py +795 -0
- mycode/context/tool_result_format.py +199 -0
- mycode/context/tool_result_retention.py +261 -0
- mycode/conversation.py +78 -0
- mycode/error_handling.py +481 -0
- mycode/event_format.py +147 -0
- mycode/instructions.py +285 -0
- mycode/llm.py +771 -0
- mycode/mcp/__init__.py +41 -0
- mycode/mcp/client.py +44 -0
- mycode/mcp/config.py +207 -0
- mycode/mcp/errors.py +302 -0
- mycode/mcp/manager.py +339 -0
- mycode/mcp/models.py +20 -0
- mycode/mcp/result_adapter.py +58 -0
- mycode/mcp/tool_adapter.py +145 -0
- mycode/mcp/trust.py +313 -0
- mycode/memory.py +570 -0
- mycode/memory_context.py +245 -0
- mycode/messages.py +63 -0
- mycode/observability.py +28 -0
- mycode/permissions.py +262 -0
- mycode/persistence/__init__.py +1 -0
- mycode/persistence/filesystem.py +291 -0
- mycode/persistence/project_storage.py +208 -0
- mycode/persistence/session_lock.py +138 -0
- mycode/persistence/session_store.py +503 -0
- mycode/presentation/__init__.py +1 -0
- mycode/presentation/cli/__init__.py +14 -0
- mycode/presentation/cli/confirmer.py +116 -0
- mycode/presentation/cli/mcp_trust.py +61 -0
- mycode/presentation/cli/presenter.py +320 -0
- mycode/presentation/cli/session_menu.py +146 -0
- mycode/presentation/cli/subagent_observer.py +124 -0
- mycode/presentation/command_format.py +90 -0
- mycode/presentation/commands.py +95 -0
- mycode/presentation/tui/__init__.py +6 -0
- mycode/presentation/tui/app.py +1351 -0
- mycode/presentation/tui/interactions.py +253 -0
- mycode/presentation/tui/presenter.py +266 -0
- mycode/presentation/tui/screens.py +305 -0
- mycode/presentation/tui/widgets.py +214 -0
- mycode/project.py +22 -0
- mycode/prompts.py +181 -0
- mycode/reasoning.py +40 -0
- mycode/session.py +86 -0
- mycode/skills/__init__.py +27 -0
- mycode/skills/builtin/database-recovery/SKILL.md +138 -0
- mycode/skills/builtin/database-recovery/references/sqlite.md +235 -0
- mycode/skills/registry.py +295 -0
- mycode/skills/state.py +68 -0
- mycode/subagents/__init__.py +1 -0
- mycode/subagents/audit.py +212 -0
- mycode/subagents/concurrency.py +124 -0
- mycode/subagents/contracts.py +421 -0
- mycode/subagents/delegate.py +80 -0
- mycode/subagents/delegation.py +128 -0
- mycode/subagents/lifecycle.py +86 -0
- mycode/subagents/limits.py +7 -0
- mycode/subagents/observability.py +150 -0
- mycode/subagents/persistence.py +152 -0
- mycode/subagents/profiles.py +184 -0
- mycode/subagents/prompts.py +67 -0
- mycode/subagents/results.py +178 -0
- mycode/subagents/runtime.py +528 -0
- mycode/subagents/snapshots.py +211 -0
- mycode/subagents/tool_batch.py +260 -0
- mycode/tools/__init__.py +81 -0
- mycode/tools/base.py +222 -0
- mycode/tools/bounds.py +14 -0
- mycode/tools/command_executor.py +167 -0
- mycode/tools/command_output.py +166 -0
- mycode/tools/command_risk.py +596 -0
- mycode/tools/defaults.py +59 -0
- mycode/tools/edit_file.py +524 -0
- mycode/tools/file_mutation.py +30 -0
- mycode/tools/glob.py +247 -0
- mycode/tools/grep.py +324 -0
- mycode/tools/ignore.py +122 -0
- mycode/tools/inspect_changes.py +269 -0
- mycode/tools/load_skill.py +92 -0
- mycode/tools/memory.py +264 -0
- mycode/tools/path_permissions.py +78 -0
- mycode/tools/patterns.py +48 -0
- mycode/tools/permission_metadata.py +27 -0
- mycode/tools/process_tree.py +166 -0
- mycode/tools/read_file.py +242 -0
- mycode/tools/read_skill_resource.py +93 -0
- mycode/tools/registry.py +279 -0
- mycode/tools/run_command.py +237 -0
- mycode/tools/run_skill_script.py +206 -0
- mycode/tools/run_validation.py +107 -0
- mycode/tools/submit_result.py +93 -0
- mycode/tools/text.py +15 -0
- mycode/tools/validation_command.py +377 -0
- mycode/tools/workspace.py +33 -0
- mycode/tools/write_file.py +169 -0
- mycode_coding_agent-0.1.0.dist-info/METADATA +244 -0
- mycode_coding_agent-0.1.0.dist-info/RECORD +121 -0
- mycode_coding_agent-0.1.0.dist-info/WHEEL +4 -0
- mycode_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
- mycode_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
HIGH_RISK_COMMANDS = {
|
|
6
|
+
"dd",
|
|
7
|
+
"del",
|
|
8
|
+
"diskpart",
|
|
9
|
+
"erase",
|
|
10
|
+
"format",
|
|
11
|
+
"mkfs",
|
|
12
|
+
"rd",
|
|
13
|
+
"rm",
|
|
14
|
+
"rmdir",
|
|
15
|
+
"shutdown",
|
|
16
|
+
"stop-computer",
|
|
17
|
+
}
|
|
18
|
+
SYSTEM_COMMANDS = {
|
|
19
|
+
"chmod",
|
|
20
|
+
"chown",
|
|
21
|
+
"kill",
|
|
22
|
+
"killall",
|
|
23
|
+
"mount",
|
|
24
|
+
"net",
|
|
25
|
+
"netsh",
|
|
26
|
+
"reboot",
|
|
27
|
+
"reg",
|
|
28
|
+
"sc",
|
|
29
|
+
"set-executionpolicy",
|
|
30
|
+
"sudo",
|
|
31
|
+
"takeown",
|
|
32
|
+
"taskkill",
|
|
33
|
+
}
|
|
34
|
+
NETWORK_COMMANDS = {
|
|
35
|
+
"curl",
|
|
36
|
+
"wget",
|
|
37
|
+
"invoke-restmethod",
|
|
38
|
+
"invoke-webrequest",
|
|
39
|
+
}
|
|
40
|
+
INSTALL_COMMANDS = {
|
|
41
|
+
"install",
|
|
42
|
+
"add",
|
|
43
|
+
"sync",
|
|
44
|
+
}
|
|
45
|
+
INSPECT_COMMANDS = {
|
|
46
|
+
"cat",
|
|
47
|
+
"diff",
|
|
48
|
+
"dir",
|
|
49
|
+
"file",
|
|
50
|
+
"find",
|
|
51
|
+
"findstr",
|
|
52
|
+
"get-childitem",
|
|
53
|
+
"grep",
|
|
54
|
+
"head",
|
|
55
|
+
"ls",
|
|
56
|
+
"rg",
|
|
57
|
+
"sed",
|
|
58
|
+
"select-string",
|
|
59
|
+
"stat",
|
|
60
|
+
"strings",
|
|
61
|
+
"tail",
|
|
62
|
+
"type",
|
|
63
|
+
"wc",
|
|
64
|
+
"xxd",
|
|
65
|
+
}
|
|
66
|
+
TEST_COMMANDS = {
|
|
67
|
+
"pytest",
|
|
68
|
+
}
|
|
69
|
+
PYTHON_COMMANDS = {"python", "python3", "py"}
|
|
70
|
+
POWERSHELL_COMMANDS = {"powershell", "pwsh"}
|
|
71
|
+
CMD_COMMANDS = {"cmd"}
|
|
72
|
+
POSIX_SHELL_COMMANDS = {"bash", "dash", "ksh", "sh", "zsh"}
|
|
73
|
+
DANGEROUS_TEXT_MARKERS = {
|
|
74
|
+
".unlink",
|
|
75
|
+
" dd ",
|
|
76
|
+
" del ",
|
|
77
|
+
" diskpart ",
|
|
78
|
+
" erase ",
|
|
79
|
+
" git clean ",
|
|
80
|
+
" git checkout ",
|
|
81
|
+
" git push --force",
|
|
82
|
+
" git restore ",
|
|
83
|
+
" git reset ",
|
|
84
|
+
" kill ",
|
|
85
|
+
" killall ",
|
|
86
|
+
" os.remove",
|
|
87
|
+
" os.unlink",
|
|
88
|
+
" rd ",
|
|
89
|
+
" reboot ",
|
|
90
|
+
" remove-item",
|
|
91
|
+
" rm ",
|
|
92
|
+
" rmdir ",
|
|
93
|
+
" shutil.rmtree",
|
|
94
|
+
" stop-process",
|
|
95
|
+
" taskkill ",
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True)
|
|
100
|
+
class CommandRiskAnalysis:
|
|
101
|
+
category: str
|
|
102
|
+
risk: str
|
|
103
|
+
decision: str
|
|
104
|
+
reason: str
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def analyze_command_risk(command: list[str]) -> CommandRiskAnalysis:
|
|
108
|
+
executable = _normalized_executable(command)
|
|
109
|
+
lowered_parts = [part.lower() for part in command]
|
|
110
|
+
|
|
111
|
+
if executable in CMD_COMMANDS:
|
|
112
|
+
return _analyze_cmd_command(lowered_parts)
|
|
113
|
+
|
|
114
|
+
if executable in POWERSHELL_COMMANDS:
|
|
115
|
+
return _analyze_powershell_command(lowered_parts)
|
|
116
|
+
|
|
117
|
+
if executable in POSIX_SHELL_COMMANDS:
|
|
118
|
+
return _analyze_posix_shell_command(lowered_parts)
|
|
119
|
+
|
|
120
|
+
if executable in PYTHON_COMMANDS:
|
|
121
|
+
return _analyze_python_command(lowered_parts)
|
|
122
|
+
|
|
123
|
+
if executable == "uv":
|
|
124
|
+
return _analyze_uv_command(lowered_parts)
|
|
125
|
+
|
|
126
|
+
if executable in HIGH_RISK_COMMANDS:
|
|
127
|
+
return CommandRiskAnalysis(
|
|
128
|
+
category="delete_or_destructive",
|
|
129
|
+
risk="high",
|
|
130
|
+
decision="deny",
|
|
131
|
+
reason=f"Destructive command is blocked: {executable}",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if executable == "git":
|
|
135
|
+
return _analyze_git_command(lowered_parts)
|
|
136
|
+
|
|
137
|
+
if _is_test_command(executable, lowered_parts):
|
|
138
|
+
return CommandRiskAnalysis(
|
|
139
|
+
category="test",
|
|
140
|
+
risk="low",
|
|
141
|
+
decision="ask",
|
|
142
|
+
reason="Command appears to run tests or validation.",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if _is_install_command(executable, lowered_parts):
|
|
146
|
+
return CommandRiskAnalysis(
|
|
147
|
+
category="install_dependency",
|
|
148
|
+
risk="medium",
|
|
149
|
+
decision="ask",
|
|
150
|
+
reason="Command may install, add, or synchronize dependencies.",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if executable in NETWORK_COMMANDS:
|
|
154
|
+
return CommandRiskAnalysis(
|
|
155
|
+
category="network_access",
|
|
156
|
+
risk="medium",
|
|
157
|
+
decision="ask",
|
|
158
|
+
reason="Command may access the network.",
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if executable in SYSTEM_COMMANDS:
|
|
162
|
+
return CommandRiskAnalysis(
|
|
163
|
+
category="system_operation",
|
|
164
|
+
risk="high",
|
|
165
|
+
decision="deny",
|
|
166
|
+
reason=f"System-level command is blocked: {executable}",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if executable in INSPECT_COMMANDS:
|
|
170
|
+
return CommandRiskAnalysis(
|
|
171
|
+
category="inspect",
|
|
172
|
+
risk="low",
|
|
173
|
+
decision="ask",
|
|
174
|
+
reason="Command appears to inspect local state.",
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return CommandRiskAnalysis(
|
|
178
|
+
category="unknown",
|
|
179
|
+
risk="medium",
|
|
180
|
+
decision="ask",
|
|
181
|
+
reason="Command risk is unknown and requires confirmation.",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _analyze_git_command(lowered_parts: list[str]) -> CommandRiskAnalysis:
|
|
186
|
+
action_index = _git_action_index(lowered_parts)
|
|
187
|
+
if action_index is None:
|
|
188
|
+
if any(part in {"--help", "--version"} for part in lowered_parts[1:]):
|
|
189
|
+
return CommandRiskAnalysis(
|
|
190
|
+
category="inspect",
|
|
191
|
+
risk="low",
|
|
192
|
+
decision="ask",
|
|
193
|
+
reason="Git help/version command appears to inspect local state.",
|
|
194
|
+
)
|
|
195
|
+
return CommandRiskAnalysis(
|
|
196
|
+
category="unknown",
|
|
197
|
+
risk="medium",
|
|
198
|
+
decision="ask",
|
|
199
|
+
reason="Git command risk is unknown and requires confirmation.",
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
git_parts = lowered_parts[action_index:]
|
|
203
|
+
git_action = git_parts[0]
|
|
204
|
+
if git_action == "reset":
|
|
205
|
+
return CommandRiskAnalysis(
|
|
206
|
+
category="git_reset",
|
|
207
|
+
risk="high",
|
|
208
|
+
decision="deny",
|
|
209
|
+
reason="Git reset is blocked because it can discard work.",
|
|
210
|
+
)
|
|
211
|
+
if git_action == "clean":
|
|
212
|
+
return CommandRiskAnalysis(
|
|
213
|
+
category="delete_or_destructive",
|
|
214
|
+
risk="high",
|
|
215
|
+
decision="deny",
|
|
216
|
+
reason="Git clean is blocked because it can delete untracked files.",
|
|
217
|
+
)
|
|
218
|
+
if git_action == "restore":
|
|
219
|
+
return CommandRiskAnalysis(
|
|
220
|
+
category="git_restore",
|
|
221
|
+
risk="high",
|
|
222
|
+
decision="deny",
|
|
223
|
+
reason="Git restore is blocked because it can discard work.",
|
|
224
|
+
)
|
|
225
|
+
if git_action == "checkout":
|
|
226
|
+
return CommandRiskAnalysis(
|
|
227
|
+
category="git_checkout",
|
|
228
|
+
risk="high",
|
|
229
|
+
decision="deny",
|
|
230
|
+
reason="Git checkout is blocked because it can discard work.",
|
|
231
|
+
)
|
|
232
|
+
if git_action == "push":
|
|
233
|
+
if _git_push_is_force(git_parts):
|
|
234
|
+
return CommandRiskAnalysis(
|
|
235
|
+
category="git_force_push",
|
|
236
|
+
risk="high",
|
|
237
|
+
decision="deny",
|
|
238
|
+
reason="Git force push is blocked because it can overwrite remote history.",
|
|
239
|
+
)
|
|
240
|
+
return CommandRiskAnalysis(
|
|
241
|
+
category="network_access",
|
|
242
|
+
risk="medium",
|
|
243
|
+
decision="ask",
|
|
244
|
+
reason="Git push may access the network or change repository state.",
|
|
245
|
+
)
|
|
246
|
+
if git_action in {"clone", "fetch", "pull"}:
|
|
247
|
+
return CommandRiskAnalysis(
|
|
248
|
+
category="network_access",
|
|
249
|
+
risk="medium",
|
|
250
|
+
decision="ask",
|
|
251
|
+
reason=f"Git {git_action} may access the network or change repository state.",
|
|
252
|
+
)
|
|
253
|
+
if git_action == "switch":
|
|
254
|
+
return CommandRiskAnalysis(
|
|
255
|
+
category="git_switch",
|
|
256
|
+
risk="medium",
|
|
257
|
+
decision="ask",
|
|
258
|
+
reason="Git switch may change working tree state and requires confirmation.",
|
|
259
|
+
)
|
|
260
|
+
if git_action in {"status", "diff", "log", "show"}:
|
|
261
|
+
return CommandRiskAnalysis(
|
|
262
|
+
category="inspect",
|
|
263
|
+
risk="low",
|
|
264
|
+
decision="ask",
|
|
265
|
+
reason=f"Git {git_action} appears to inspect repository state.",
|
|
266
|
+
)
|
|
267
|
+
if git_action == "branch" and _git_branch_is_inspect(git_parts):
|
|
268
|
+
return CommandRiskAnalysis(
|
|
269
|
+
category="inspect",
|
|
270
|
+
risk="low",
|
|
271
|
+
decision="ask",
|
|
272
|
+
reason="Git branch appears to inspect repository state.",
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
return CommandRiskAnalysis(
|
|
276
|
+
category="git_operation",
|
|
277
|
+
risk="medium",
|
|
278
|
+
decision="ask",
|
|
279
|
+
reason=f"Git {git_action} may change repository state and requires confirmation.",
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _git_action_index(lowered_parts: list[str]) -> int | None:
|
|
284
|
+
value_options = {
|
|
285
|
+
"--config-env",
|
|
286
|
+
"--exec-path",
|
|
287
|
+
"--git-dir",
|
|
288
|
+
"--html-path",
|
|
289
|
+
"--namespace",
|
|
290
|
+
"--super-prefix",
|
|
291
|
+
"--work-tree",
|
|
292
|
+
"-c",
|
|
293
|
+
"-C",
|
|
294
|
+
}
|
|
295
|
+
flag_options = {
|
|
296
|
+
"--bare",
|
|
297
|
+
"--help",
|
|
298
|
+
"--literal-pathspecs",
|
|
299
|
+
"--no-optional-locks",
|
|
300
|
+
"--no-pager",
|
|
301
|
+
"--no-replace-objects",
|
|
302
|
+
"--version",
|
|
303
|
+
"-p",
|
|
304
|
+
}
|
|
305
|
+
value_option_prefixes = tuple(f"{option}=" for option in value_options)
|
|
306
|
+
|
|
307
|
+
index = 1
|
|
308
|
+
while index < len(lowered_parts):
|
|
309
|
+
part = lowered_parts[index]
|
|
310
|
+
if part == "--":
|
|
311
|
+
index += 1
|
|
312
|
+
continue
|
|
313
|
+
if part in value_options:
|
|
314
|
+
index += 2
|
|
315
|
+
continue
|
|
316
|
+
if part.startswith(value_option_prefixes):
|
|
317
|
+
index += 1
|
|
318
|
+
continue
|
|
319
|
+
if part in flag_options:
|
|
320
|
+
index += 1
|
|
321
|
+
continue
|
|
322
|
+
if part.startswith("-"):
|
|
323
|
+
index += 1
|
|
324
|
+
continue
|
|
325
|
+
|
|
326
|
+
return index
|
|
327
|
+
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _git_push_is_force(git_parts: list[str]) -> bool:
|
|
332
|
+
force_options = {
|
|
333
|
+
"--force",
|
|
334
|
+
"--force-if-includes",
|
|
335
|
+
"--force-with-lease",
|
|
336
|
+
"-f",
|
|
337
|
+
}
|
|
338
|
+
return any(
|
|
339
|
+
part in force_options
|
|
340
|
+
or part.startswith("--force-with-lease=")
|
|
341
|
+
or part.startswith("--force-if-includes=")
|
|
342
|
+
for part in git_parts[1:]
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _git_branch_is_inspect(lowered_parts: list[str]) -> bool:
|
|
347
|
+
inspect_options = {
|
|
348
|
+
"--all",
|
|
349
|
+
"--contains",
|
|
350
|
+
"--format",
|
|
351
|
+
"--list",
|
|
352
|
+
"--merged",
|
|
353
|
+
"--no-contains",
|
|
354
|
+
"--no-merged",
|
|
355
|
+
"--points-at",
|
|
356
|
+
"--remotes",
|
|
357
|
+
"--show-current",
|
|
358
|
+
"-a",
|
|
359
|
+
"-r",
|
|
360
|
+
"-v",
|
|
361
|
+
"-vv",
|
|
362
|
+
}
|
|
363
|
+
return len(lowered_parts) == 2 or all(
|
|
364
|
+
part.startswith("--sort=")
|
|
365
|
+
or part.startswith("--format=")
|
|
366
|
+
or part in inspect_options
|
|
367
|
+
for part in lowered_parts[2:]
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _is_test_command(executable: str, lowered_parts: list[str]) -> bool:
|
|
372
|
+
if executable in TEST_COMMANDS:
|
|
373
|
+
return True
|
|
374
|
+
|
|
375
|
+
if executable in PYTHON_COMMANDS and lowered_parts[1:3] == ["-m", "pytest"]:
|
|
376
|
+
return True
|
|
377
|
+
|
|
378
|
+
if executable == "uv" and lowered_parts[1:3] == ["run", "pytest"]:
|
|
379
|
+
return True
|
|
380
|
+
|
|
381
|
+
return False
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _analyze_cmd_command(lowered_parts: list[str]) -> CommandRiskAnalysis:
|
|
385
|
+
inner_parts = _parts_after_first_switch(lowered_parts, {"/c", "/k"})
|
|
386
|
+
if _contains_dangerous_text(inner_parts):
|
|
387
|
+
return _wrapped_dangerous_command("cmd")
|
|
388
|
+
|
|
389
|
+
if inner_parts:
|
|
390
|
+
inner_executable = _normalized_executable(inner_parts)
|
|
391
|
+
if inner_executable in HIGH_RISK_COMMANDS:
|
|
392
|
+
return CommandRiskAnalysis(
|
|
393
|
+
category="delete_or_destructive",
|
|
394
|
+
risk="high",
|
|
395
|
+
decision="deny",
|
|
396
|
+
reason=f"Destructive command is blocked inside cmd: {inner_executable}",
|
|
397
|
+
)
|
|
398
|
+
if inner_executable in SYSTEM_COMMANDS:
|
|
399
|
+
return CommandRiskAnalysis(
|
|
400
|
+
category="system_operation",
|
|
401
|
+
risk="high",
|
|
402
|
+
decision="deny",
|
|
403
|
+
reason=f"System-level command is blocked inside cmd: {inner_executable}",
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
return CommandRiskAnalysis(
|
|
407
|
+
category="shell_wrapper",
|
|
408
|
+
risk="high",
|
|
409
|
+
decision="ask",
|
|
410
|
+
reason="cmd wrapper command requires confirmation.",
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _analyze_powershell_command(lowered_parts: list[str]) -> CommandRiskAnalysis:
|
|
415
|
+
command_parts = _parts_after_first_switch(
|
|
416
|
+
lowered_parts,
|
|
417
|
+
{"-command", "-c", "/c"},
|
|
418
|
+
)
|
|
419
|
+
if _contains_dangerous_text(command_parts or lowered_parts):
|
|
420
|
+
return _wrapped_dangerous_command(lowered_parts[0])
|
|
421
|
+
|
|
422
|
+
return CommandRiskAnalysis(
|
|
423
|
+
category="shell_wrapper",
|
|
424
|
+
risk="high",
|
|
425
|
+
decision="ask",
|
|
426
|
+
reason="PowerShell wrapper command requires confirmation.",
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _analyze_posix_shell_command(lowered_parts: list[str]) -> CommandRiskAnalysis:
|
|
431
|
+
command_parts = _parts_after_first_switch(lowered_parts, {"-c"})
|
|
432
|
+
if _contains_dangerous_text(command_parts or lowered_parts):
|
|
433
|
+
return _wrapped_dangerous_command(lowered_parts[0])
|
|
434
|
+
|
|
435
|
+
return CommandRiskAnalysis(
|
|
436
|
+
category="shell_wrapper",
|
|
437
|
+
risk="high",
|
|
438
|
+
decision="ask",
|
|
439
|
+
reason="POSIX shell wrapper command requires confirmation.",
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _analyze_python_command(lowered_parts: list[str]) -> CommandRiskAnalysis:
|
|
444
|
+
if _is_python_version_command(lowered_parts):
|
|
445
|
+
return CommandRiskAnalysis(
|
|
446
|
+
category="inspect",
|
|
447
|
+
risk="low",
|
|
448
|
+
decision="ask",
|
|
449
|
+
reason="Python version command appears to inspect local state.",
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
if lowered_parts[1:3] == ["-m", "pytest"]:
|
|
453
|
+
return CommandRiskAnalysis(
|
|
454
|
+
category="test",
|
|
455
|
+
risk="low",
|
|
456
|
+
decision="ask",
|
|
457
|
+
reason="Command appears to run tests or validation.",
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
if "-c" in lowered_parts:
|
|
461
|
+
inline_parts = lowered_parts[lowered_parts.index("-c") + 1 :]
|
|
462
|
+
if _contains_dangerous_text(inline_parts):
|
|
463
|
+
return CommandRiskAnalysis(
|
|
464
|
+
category="delete_or_destructive",
|
|
465
|
+
risk="high",
|
|
466
|
+
decision="deny",
|
|
467
|
+
reason="Python inline command is blocked because it may delete or alter files.",
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
return CommandRiskAnalysis(
|
|
471
|
+
category="python_inline",
|
|
472
|
+
risk="medium",
|
|
473
|
+
decision="ask",
|
|
474
|
+
reason="Python inline command requires confirmation.",
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
return CommandRiskAnalysis(
|
|
478
|
+
category="python_execution",
|
|
479
|
+
risk="medium",
|
|
480
|
+
decision="ask",
|
|
481
|
+
reason="Python command requires confirmation.",
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _analyze_uv_command(lowered_parts: list[str]) -> CommandRiskAnalysis:
|
|
486
|
+
if len(lowered_parts) > 1 and lowered_parts[1] in {"--version", "-v"}:
|
|
487
|
+
return CommandRiskAnalysis(
|
|
488
|
+
category="inspect",
|
|
489
|
+
risk="low",
|
|
490
|
+
decision="ask",
|
|
491
|
+
reason="uv version command appears to inspect local state.",
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
if lowered_parts[1:3] == ["run", "pytest"]:
|
|
495
|
+
return CommandRiskAnalysis(
|
|
496
|
+
category="test",
|
|
497
|
+
risk="low",
|
|
498
|
+
decision="ask",
|
|
499
|
+
reason="Command appears to run tests or validation.",
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
if len(lowered_parts) > 2 and lowered_parts[1] == "run":
|
|
503
|
+
run_parts = lowered_parts[2:]
|
|
504
|
+
run_executable = _normalized_executable(run_parts)
|
|
505
|
+
if run_executable in PYTHON_COMMANDS:
|
|
506
|
+
return _analyze_python_command(run_parts)
|
|
507
|
+
if _contains_dangerous_text(run_parts):
|
|
508
|
+
return _wrapped_dangerous_command("uv run")
|
|
509
|
+
|
|
510
|
+
return CommandRiskAnalysis(
|
|
511
|
+
category="uv_run",
|
|
512
|
+
risk="medium",
|
|
513
|
+
decision="ask",
|
|
514
|
+
reason="uv run command requires confirmation.",
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
if _is_install_command("uv", lowered_parts):
|
|
518
|
+
return CommandRiskAnalysis(
|
|
519
|
+
category="install_dependency",
|
|
520
|
+
risk="medium",
|
|
521
|
+
decision="ask",
|
|
522
|
+
reason="Command may install, add, or synchronize dependencies.",
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
return CommandRiskAnalysis(
|
|
526
|
+
category="unknown",
|
|
527
|
+
risk="medium",
|
|
528
|
+
decision="ask",
|
|
529
|
+
reason="uv command risk is unknown and requires confirmation.",
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _is_python_version_command(lowered_parts: list[str]) -> bool:
|
|
534
|
+
return len(lowered_parts) == 2 and lowered_parts[1] in {
|
|
535
|
+
"--version",
|
|
536
|
+
"-v",
|
|
537
|
+
"-vv",
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _parts_after_first_switch(
|
|
542
|
+
lowered_parts: list[str],
|
|
543
|
+
switches: set[str],
|
|
544
|
+
) -> list[str]:
|
|
545
|
+
for index, part in enumerate(lowered_parts):
|
|
546
|
+
if part in switches:
|
|
547
|
+
return lowered_parts[index + 1 :]
|
|
548
|
+
|
|
549
|
+
return []
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _contains_dangerous_text(parts: list[str]) -> bool:
|
|
553
|
+
if not parts:
|
|
554
|
+
return False
|
|
555
|
+
|
|
556
|
+
text = " ".join(parts)
|
|
557
|
+
normalized = f" {text.replace(';', ' ').replace('&', ' ')} "
|
|
558
|
+
return any(marker in normalized for marker in DANGEROUS_TEXT_MARKERS)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _wrapped_dangerous_command(wrapper: str) -> CommandRiskAnalysis:
|
|
562
|
+
return CommandRiskAnalysis(
|
|
563
|
+
category="delete_or_destructive",
|
|
564
|
+
risk="high",
|
|
565
|
+
decision="deny",
|
|
566
|
+
reason=f"Dangerous command is blocked inside {wrapper}.",
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _is_install_command(executable: str, lowered_parts: list[str]) -> bool:
|
|
571
|
+
if executable in {"pip", "pip3"} and len(lowered_parts) > 1:
|
|
572
|
+
return lowered_parts[1] in INSTALL_COMMANDS
|
|
573
|
+
|
|
574
|
+
if executable == "uv" and len(lowered_parts) > 1:
|
|
575
|
+
return lowered_parts[1] in {"add", "sync"} or lowered_parts[1:3] == [
|
|
576
|
+
"pip",
|
|
577
|
+
"install",
|
|
578
|
+
]
|
|
579
|
+
|
|
580
|
+
if executable in {"npm", "pnpm", "yarn"} and len(lowered_parts) > 1:
|
|
581
|
+
return lowered_parts[1] in INSTALL_COMMANDS
|
|
582
|
+
|
|
583
|
+
if executable == "poetry" and len(lowered_parts) > 1:
|
|
584
|
+
return lowered_parts[1] in {"add", "install", "sync"}
|
|
585
|
+
|
|
586
|
+
return False
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _normalized_executable(command: list[str]) -> str:
|
|
590
|
+
executable = Path(command[0]).name.lower()
|
|
591
|
+
if executable.endswith(".exe"):
|
|
592
|
+
executable = executable[:-4]
|
|
593
|
+
if executable.endswith(".cmd") or executable.endswith(".bat"):
|
|
594
|
+
executable = executable[:-4]
|
|
595
|
+
|
|
596
|
+
return executable
|
mycode/tools/defaults.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
|
|
3
|
+
from mycode.permissions import Confirmer
|
|
4
|
+
from mycode.memory import MemoryStore
|
|
5
|
+
from mycode.tools.base import BaseTool
|
|
6
|
+
from mycode.tools.edit_file import EditFileTool
|
|
7
|
+
from mycode.tools.glob import GlobTool
|
|
8
|
+
from mycode.tools.grep import GrepTool
|
|
9
|
+
from mycode.tools.read_file import ReadFileTool
|
|
10
|
+
from mycode.tools.registry import ToolRegistry
|
|
11
|
+
from mycode.tools.run_command import RunCommandTool
|
|
12
|
+
from mycode.tools.memory import DeleteMemoryTool, ListMemoriesTool, SaveMemoryTool
|
|
13
|
+
from mycode.tools.workspace import Workspace
|
|
14
|
+
from mycode.tools.write_file import WriteFileTool
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_read_only_tool_registry(
|
|
18
|
+
workspace: Workspace,
|
|
19
|
+
*,
|
|
20
|
+
confirmer: Confirmer | None = None,
|
|
21
|
+
) -> ToolRegistry:
|
|
22
|
+
return ToolRegistry.from_tools(
|
|
23
|
+
[
|
|
24
|
+
ReadFileTool(workspace),
|
|
25
|
+
GlobTool(workspace),
|
|
26
|
+
GrepTool(workspace),
|
|
27
|
+
],
|
|
28
|
+
confirmer=confirmer,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def create_default_tool_registry(
|
|
33
|
+
workspace: Workspace,
|
|
34
|
+
*,
|
|
35
|
+
confirmer: Confirmer | None = None,
|
|
36
|
+
memory_store: MemoryStore | None = None,
|
|
37
|
+
extra_tools: Iterable[BaseTool] = (),
|
|
38
|
+
) -> ToolRegistry:
|
|
39
|
+
tools = [
|
|
40
|
+
ReadFileTool(workspace),
|
|
41
|
+
GlobTool(workspace),
|
|
42
|
+
GrepTool(workspace),
|
|
43
|
+
WriteFileTool(workspace),
|
|
44
|
+
EditFileTool(workspace),
|
|
45
|
+
RunCommandTool(workspace),
|
|
46
|
+
]
|
|
47
|
+
if memory_store is not None:
|
|
48
|
+
tools.extend(
|
|
49
|
+
[
|
|
50
|
+
ListMemoriesTool(memory_store),
|
|
51
|
+
SaveMemoryTool(memory_store),
|
|
52
|
+
DeleteMemoryTool(memory_store),
|
|
53
|
+
]
|
|
54
|
+
)
|
|
55
|
+
tools.extend(extra_tools)
|
|
56
|
+
return ToolRegistry.from_tools(
|
|
57
|
+
tools,
|
|
58
|
+
confirmer=confirmer,
|
|
59
|
+
)
|