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.
Files changed (121) hide show
  1. mycode/__init__.py +0 -0
  2. mycode/adapters/__init__.py +21 -0
  3. mycode/adapters/jsonl.py +692 -0
  4. mycode/agent/__init__.py +25 -0
  5. mycode/agent/events.py +111 -0
  6. mycode/agent/outcome.py +103 -0
  7. mycode/agent/progress.py +373 -0
  8. mycode/agent/runner.py +1481 -0
  9. mycode/application/__init__.py +38 -0
  10. mycode/application/agent_session.py +367 -0
  11. mycode/application/events.py +59 -0
  12. mycode/application/runtime.py +211 -0
  13. mycode/application/sessions.py +180 -0
  14. mycode/cli.py +840 -0
  15. mycode/config.py +355 -0
  16. mycode/context/__init__.py +1 -0
  17. mycode/context/artifacts.py +672 -0
  18. mycode/context/budget.py +752 -0
  19. mycode/context/builder.py +112 -0
  20. mycode/context/compact.py +795 -0
  21. mycode/context/tool_result_format.py +199 -0
  22. mycode/context/tool_result_retention.py +261 -0
  23. mycode/conversation.py +78 -0
  24. mycode/error_handling.py +481 -0
  25. mycode/event_format.py +147 -0
  26. mycode/instructions.py +285 -0
  27. mycode/llm.py +771 -0
  28. mycode/mcp/__init__.py +41 -0
  29. mycode/mcp/client.py +44 -0
  30. mycode/mcp/config.py +207 -0
  31. mycode/mcp/errors.py +302 -0
  32. mycode/mcp/manager.py +339 -0
  33. mycode/mcp/models.py +20 -0
  34. mycode/mcp/result_adapter.py +58 -0
  35. mycode/mcp/tool_adapter.py +145 -0
  36. mycode/mcp/trust.py +313 -0
  37. mycode/memory.py +570 -0
  38. mycode/memory_context.py +245 -0
  39. mycode/messages.py +63 -0
  40. mycode/observability.py +28 -0
  41. mycode/permissions.py +262 -0
  42. mycode/persistence/__init__.py +1 -0
  43. mycode/persistence/filesystem.py +291 -0
  44. mycode/persistence/project_storage.py +208 -0
  45. mycode/persistence/session_lock.py +138 -0
  46. mycode/persistence/session_store.py +503 -0
  47. mycode/presentation/__init__.py +1 -0
  48. mycode/presentation/cli/__init__.py +14 -0
  49. mycode/presentation/cli/confirmer.py +116 -0
  50. mycode/presentation/cli/mcp_trust.py +61 -0
  51. mycode/presentation/cli/presenter.py +320 -0
  52. mycode/presentation/cli/session_menu.py +146 -0
  53. mycode/presentation/cli/subagent_observer.py +124 -0
  54. mycode/presentation/command_format.py +90 -0
  55. mycode/presentation/commands.py +95 -0
  56. mycode/presentation/tui/__init__.py +6 -0
  57. mycode/presentation/tui/app.py +1351 -0
  58. mycode/presentation/tui/interactions.py +253 -0
  59. mycode/presentation/tui/presenter.py +266 -0
  60. mycode/presentation/tui/screens.py +305 -0
  61. mycode/presentation/tui/widgets.py +214 -0
  62. mycode/project.py +22 -0
  63. mycode/prompts.py +181 -0
  64. mycode/reasoning.py +40 -0
  65. mycode/session.py +86 -0
  66. mycode/skills/__init__.py +27 -0
  67. mycode/skills/builtin/database-recovery/SKILL.md +138 -0
  68. mycode/skills/builtin/database-recovery/references/sqlite.md +235 -0
  69. mycode/skills/registry.py +295 -0
  70. mycode/skills/state.py +68 -0
  71. mycode/subagents/__init__.py +1 -0
  72. mycode/subagents/audit.py +212 -0
  73. mycode/subagents/concurrency.py +124 -0
  74. mycode/subagents/contracts.py +421 -0
  75. mycode/subagents/delegate.py +80 -0
  76. mycode/subagents/delegation.py +128 -0
  77. mycode/subagents/lifecycle.py +86 -0
  78. mycode/subagents/limits.py +7 -0
  79. mycode/subagents/observability.py +150 -0
  80. mycode/subagents/persistence.py +152 -0
  81. mycode/subagents/profiles.py +184 -0
  82. mycode/subagents/prompts.py +67 -0
  83. mycode/subagents/results.py +178 -0
  84. mycode/subagents/runtime.py +528 -0
  85. mycode/subagents/snapshots.py +211 -0
  86. mycode/subagents/tool_batch.py +260 -0
  87. mycode/tools/__init__.py +81 -0
  88. mycode/tools/base.py +222 -0
  89. mycode/tools/bounds.py +14 -0
  90. mycode/tools/command_executor.py +167 -0
  91. mycode/tools/command_output.py +166 -0
  92. mycode/tools/command_risk.py +596 -0
  93. mycode/tools/defaults.py +59 -0
  94. mycode/tools/edit_file.py +524 -0
  95. mycode/tools/file_mutation.py +30 -0
  96. mycode/tools/glob.py +247 -0
  97. mycode/tools/grep.py +324 -0
  98. mycode/tools/ignore.py +122 -0
  99. mycode/tools/inspect_changes.py +269 -0
  100. mycode/tools/load_skill.py +92 -0
  101. mycode/tools/memory.py +264 -0
  102. mycode/tools/path_permissions.py +78 -0
  103. mycode/tools/patterns.py +48 -0
  104. mycode/tools/permission_metadata.py +27 -0
  105. mycode/tools/process_tree.py +166 -0
  106. mycode/tools/read_file.py +242 -0
  107. mycode/tools/read_skill_resource.py +93 -0
  108. mycode/tools/registry.py +279 -0
  109. mycode/tools/run_command.py +237 -0
  110. mycode/tools/run_skill_script.py +206 -0
  111. mycode/tools/run_validation.py +107 -0
  112. mycode/tools/submit_result.py +93 -0
  113. mycode/tools/text.py +15 -0
  114. mycode/tools/validation_command.py +377 -0
  115. mycode/tools/workspace.py +33 -0
  116. mycode/tools/write_file.py +169 -0
  117. mycode_coding_agent-0.1.0.dist-info/METADATA +244 -0
  118. mycode_coding_agent-0.1.0.dist-info/RECORD +121 -0
  119. mycode_coding_agent-0.1.0.dist-info/WHEEL +4 -0
  120. mycode_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
  121. 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
@@ -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
+ )