gdmcode 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 (131) hide show
  1. gdmcode-0.1.0.dist-info/METADATA +240 -0
  2. gdmcode-0.1.0.dist-info/RECORD +131 -0
  3. gdmcode-0.1.0.dist-info/WHEEL +4 -0
  4. gdmcode-0.1.0.dist-info/entry_points.txt +2 -0
  5. src/__init__.py +1 -0
  6. src/_internal/__init__.py +0 -0
  7. src/_internal/constants.py +244 -0
  8. src/_internal/domain_skills.py +339 -0
  9. src/agent/__init__.py +0 -0
  10. src/agent/commit_classifier.py +91 -0
  11. src/agent/context_budget.py +391 -0
  12. src/agent/daemon.py +681 -0
  13. src/agent/dag_validator.py +153 -0
  14. src/agent/debug_loop.py +473 -0
  15. src/agent/impact_analyzer.py +149 -0
  16. src/agent/impact_graph.py +117 -0
  17. src/agent/loop.py +1410 -0
  18. src/agent/orchestrator.py +141 -0
  19. src/agent/regression_guard.py +251 -0
  20. src/agent/review_gate.py +648 -0
  21. src/agent/risk_scorer.py +169 -0
  22. src/agent/self_healing.py +145 -0
  23. src/agent/smart_test_selector.py +89 -0
  24. src/agent/system_prompt.py +226 -0
  25. src/agent/task_tracker.py +320 -0
  26. src/agent/test_validator.py +210 -0
  27. src/agent/tool_orchestrator.py +402 -0
  28. src/agent/transcript.py +230 -0
  29. src/agent/verification_loop.py +133 -0
  30. src/agent/work_director.py +136 -0
  31. src/agent/worktree_manager.py +53 -0
  32. src/artifacts/__init__.py +16 -0
  33. src/artifacts/artifact_store.py +456 -0
  34. src/artifacts/verification_graph.py +75 -0
  35. src/auth.py +411 -0
  36. src/cli.py +1290 -0
  37. src/commands.py +1398 -0
  38. src/config.py +762 -0
  39. src/cost_tracker.py +348 -0
  40. src/db/__init__.py +4 -0
  41. src/db/migrations.py +337 -0
  42. src/enterprise/__init__.py +3 -0
  43. src/enterprise/audit_log.py +182 -0
  44. src/enterprise/identity.py +90 -0
  45. src/enterprise/rbac.py +100 -0
  46. src/enterprise/team_config.py +125 -0
  47. src/enterprise/usage_analytics.py +261 -0
  48. src/exceptions.py +207 -0
  49. src/git_workflow.py +651 -0
  50. src/integrations/__init__.py +6 -0
  51. src/integrations/github_actions.py +106 -0
  52. src/integrations/mcp_server.py +333 -0
  53. src/integrations/sentry_integration.py +100 -0
  54. src/integrations/sentry_server.py +82 -0
  55. src/integrations/webhook_security.py +19 -0
  56. src/main.py +27 -0
  57. src/memory/__init__.py +0 -0
  58. src/memory/code_index.py +376 -0
  59. src/memory/compressor.py +378 -0
  60. src/memory/context_memory.py +135 -0
  61. src/memory/continuous_memory.py +234 -0
  62. src/memory/conventions.py +495 -0
  63. src/memory/db.py +1119 -0
  64. src/memory/document_index.py +205 -0
  65. src/memory/file_cache.py +128 -0
  66. src/memory/project_scanner.py +178 -0
  67. src/memory/session_store.py +201 -0
  68. src/models/__init__.py +0 -0
  69. src/models/client.py +715 -0
  70. src/models/definitions.py +459 -0
  71. src/models/router.py +418 -0
  72. src/models/schemas.py +389 -0
  73. src/permissions.py +294 -0
  74. src/remote/__init__.py +5 -0
  75. src/remote/command_filter.py +33 -0
  76. src/remote/models.py +31 -0
  77. src/remote/permission_handler.py +79 -0
  78. src/remote/phone_ui.py +48 -0
  79. src/remote/protocol.py +59 -0
  80. src/remote/qr.py +65 -0
  81. src/remote/server.py +586 -0
  82. src/remote/token_manager.py +61 -0
  83. src/remote/tunnel.py +212 -0
  84. src/repl.py +475 -0
  85. src/runtime/__init__.py +1 -0
  86. src/runtime/branch_farm.py +372 -0
  87. src/runtime/replay.py +351 -0
  88. src/sandbox/__init__.py +2 -0
  89. src/sandbox/hermetic.py +214 -0
  90. src/sandbox/policy.py +44 -0
  91. src/sdk/__init__.py +3 -0
  92. src/sdk/plugin_base.py +39 -0
  93. src/sdk/plugin_host.py +100 -0
  94. src/sdk/plugin_loader.py +101 -0
  95. src/security.py +409 -0
  96. src/server/__init__.py +7 -0
  97. src/server/bridge.py +427 -0
  98. src/server/bridge_cli.py +103 -0
  99. src/server/bridge_client.py +170 -0
  100. src/server/protocol_version.py +103 -0
  101. src/session/__init__.py +10 -0
  102. src/session/event_fanout.py +46 -0
  103. src/session/input_broker.py +38 -0
  104. src/session/permission_bridge.py +100 -0
  105. src/tools/__init__.py +160 -0
  106. src/tools/_atomic.py +72 -0
  107. src/tools/agent_tools.py +423 -0
  108. src/tools/ask_user_tool.py +83 -0
  109. src/tools/bash_tool.py +384 -0
  110. src/tools/browser_tool.py +352 -0
  111. src/tools/browser_tools.py +179 -0
  112. src/tools/dep_tools.py +210 -0
  113. src/tools/document_reader.py +167 -0
  114. src/tools/document_tool.py +240 -0
  115. src/tools/document_writer.py +171 -0
  116. src/tools/impact_tools.py +240 -0
  117. src/tools/playwright_tool.py +172 -0
  118. src/tools/quality_tools.py +366 -0
  119. src/tools/read_tools.py +318 -0
  120. src/tools/result_cache.py +157 -0
  121. src/tools/search_tools.py +310 -0
  122. src/tools/shell_tools.py +311 -0
  123. src/tools/write_tools.py +337 -0
  124. src/voice/__init__.py +25 -0
  125. src/voice/audio_capture.py +92 -0
  126. src/voice/audio_playback.py +68 -0
  127. src/voice/errors.py +14 -0
  128. src/voice/models.py +35 -0
  129. src/voice/providers.py +143 -0
  130. src/voice/vad.py +55 -0
  131. src/voice/voice_loop.py +156 -0
src/tools/bash_tool.py ADDED
@@ -0,0 +1,384 @@
1
+ """BashTool and PowerShellTool — full shell execution with security validation.
2
+
3
+ Security layers (mirroring Claude Code's 12 bash submodules):
4
+ 1. Command semantics — classify read / write / network / destructive
5
+ 2. Destructive command warning — explicit warn before rm/drop/delete
6
+ 3. Path validation — no path traversal outside workspace
7
+ 4. Read-only mode enforcement — block writes when session is read-only
8
+ 5. Injection detection — multi-command chains that escape quotes
9
+ 6. Git safety — block force-push / rebase in protected branches
10
+ 7. Sandbox routing — decide whether to use subprocess or sandboxed exec
11
+
12
+ Never raises — all errors returned as ToolResult(error=...).
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ import os
18
+ import platform
19
+ import re
20
+ import shlex
21
+ import signal
22
+ import subprocess
23
+ import sys
24
+ from pathlib import Path
25
+ from typing import Any, ClassVar
26
+
27
+ from src.tools import REGISTRY, ToolBase, ToolResult
28
+
29
+ __all__ = ["BashTool", "PowerShellTool"]
30
+
31
+ log = logging.getLogger(__name__)
32
+
33
+ # Per-session set of filenames already flagged for injection (shared with security.py dedup)
34
+ _bash_flagged: set[str] = set()
35
+ _BASH_INJECT_CHECK_THRESHOLD = 1024 # only scan stdout > 1 KB
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Constants
39
+ # ---------------------------------------------------------------------------
40
+
41
+ _MAX_OUTPUT_BYTES: int = 50_000
42
+ _DEFAULT_TIMEOUT_SECS: int = 60
43
+ _LONG_TIMEOUT_SECS: int = 600 # for explicitly slow commands like npm install
44
+
45
+ # Patterns that identify destructive operations requiring explicit confirmation.
46
+ _DESTRUCTIVE_PATTERNS: tuple[re.Pattern[str], ...] = tuple(
47
+ re.compile(p, re.IGNORECASE)
48
+ for p in [
49
+ r"\brm\s+-rf\b",
50
+ r"\brm\s+--recursive\b",
51
+ r"\bdropdb\b",
52
+ r"\bdrop\s+table\b",
53
+ r"\bdrop\s+database\b",
54
+ r"\btruncate\b",
55
+ r"\bformat\b",
56
+ r"\bmkfs\b",
57
+ r"\bdd\s+if=",
58
+ r">\s*/dev/",
59
+ ]
60
+ )
61
+
62
+ # Patterns that classify a command as "network" (triggers user prompt in restricted mode).
63
+ _NETWORK_PATTERNS: tuple[re.Pattern[str], ...] = tuple(
64
+ re.compile(p, re.IGNORECASE)
65
+ for p in [r"\bcurl\b", r"\bwget\b", r"\bssh\b", r"\brsync\b", r"\bftp\b", r"\bnc\b"]
66
+ )
67
+
68
+ # Git commands blocked on protected branches.
69
+ _DANGEROUS_GIT_PATTERN = re.compile(
70
+ r"git\s+(push\s+--force|push\s+-f|rebase\s+-i|reset\s+--hard\s+HEAD~)", re.IGNORECASE
71
+ )
72
+
73
+ # Commands that clearly don't write to disk (safe in read-only sessions).
74
+ _READ_ONLY_SAFE_PREFIXES: tuple[str, ...] = (
75
+ "cat ", "ls ", "find ", "echo ", "pwd", "whoami", "which ", "type ",
76
+ "head ", "tail ", "wc ", "grep ", "rg ", "fd ", "bat ", "less ",
77
+ "git log", "git diff", "git status", "git show", "git blame",
78
+ "python -m pytest", "python -c ", "python3 -c ",
79
+ )
80
+
81
+
82
+ # ---------------------------------------------------------------------------
83
+ # Validation helpers
84
+ # ---------------------------------------------------------------------------
85
+
86
+ def _classify(cmd: str) -> dict[str, bool]:
87
+ """Return classification dict: read_only, destructive, network, git_dangerous."""
88
+ return {
89
+ "read_only": any(cmd.lstrip().startswith(p) for p in _READ_ONLY_SAFE_PREFIXES),
90
+ "destructive": any(p.search(cmd) for p in _DESTRUCTIVE_PATTERNS),
91
+ "network": any(p.search(cmd) for p in _NETWORK_PATTERNS),
92
+ "git_dangerous": bool(_DANGEROUS_GIT_PATTERN.search(cmd)),
93
+ }
94
+
95
+
96
+ def _path_traversal_risk(cmd: str, workspace: Path | None) -> str | None:
97
+ """Return error string if command references paths outside workspace."""
98
+ if workspace is None:
99
+ return None
100
+ # Look for absolute paths that aren't under the workspace.
101
+ for token in shlex.split(cmd, posix=(sys.platform != "win32")):
102
+ if token.startswith("/") or (len(token) > 2 and token[1] == ":"):
103
+ candidate = Path(token).resolve()
104
+ try:
105
+ candidate.relative_to(workspace)
106
+ except ValueError:
107
+ # Allow common system dirs: /usr, /bin, /etc, C:\Windows, etc.
108
+ # Block only if it looks like a project directory outside workspace.
109
+ if not _is_system_path(candidate):
110
+ return f"Path {token!r} is outside workspace {workspace}"
111
+ return None
112
+
113
+
114
+ def _is_system_path(p: Path) -> bool:
115
+ """True if p is a well-known system path that tools legitimately access."""
116
+ system_roots = (
117
+ "/usr", "/bin", "/sbin", "/lib", "/etc", "/tmp", "/var",
118
+ "/proc", "/dev", "/sys", "/opt",
119
+ )
120
+ if sys.platform == "win32":
121
+ return any(
122
+ str(p).lower().startswith(r.lower())
123
+ for r in (r"C:\Windows", r"C:\Program Files", r"C:\ProgramData")
124
+ )
125
+ return any(str(p).startswith(r) for r in system_roots)
126
+
127
+
128
+ def _validate(cmd: str, *, read_only: bool, workspace: Path | None) -> str | None:
129
+ """Return an error message if the command should be blocked, else None."""
130
+ cls = _classify(cmd)
131
+ if read_only and not cls["read_only"]:
132
+ return f"Session is read-only. Command {cmd!r} may write to disk."
133
+ if cls["git_dangerous"]:
134
+ return (
135
+ f"Blocked: {cmd!r} is a potentially destructive git operation. "
136
+ "Run manually if you are sure."
137
+ )
138
+ if err := _path_traversal_risk(cmd, workspace):
139
+ return err
140
+ return None
141
+
142
+
143
+ # ---------------------------------------------------------------------------
144
+ # Output helpers
145
+ # ---------------------------------------------------------------------------
146
+
147
+ def _truncate(output: str, max_bytes: int = _MAX_OUTPUT_BYTES) -> tuple[str, bool]:
148
+ """Return (possibly_truncated_text, was_truncated)."""
149
+ encoded = output.encode("utf-8", errors="replace")
150
+ if len(encoded) <= max_bytes:
151
+ return output, False
152
+ truncated = encoded[:max_bytes].decode("utf-8", errors="replace")
153
+ return truncated, True
154
+
155
+
156
+ def _run_subprocess(
157
+ cmd: str,
158
+ *,
159
+ shell_exec: str,
160
+ shell_args: list[str],
161
+ cwd: Path | None,
162
+ timeout: int,
163
+ env: dict[str, str] | None,
164
+ ) -> tuple[str, str, int]:
165
+ """Run command; return (stdout, stderr, returncode)."""
166
+ full_cmd = [shell_exec, *shell_args, cmd]
167
+ try:
168
+ result = subprocess.run(
169
+ full_cmd,
170
+ capture_output=True,
171
+ text=True,
172
+ timeout=timeout,
173
+ cwd=cwd,
174
+ env=env,
175
+ # On POSIX, use process group so we can kill the entire tree.
176
+ start_new_session=(sys.platform != "win32"),
177
+ )
178
+ return result.stdout, result.stderr, result.returncode
179
+ except subprocess.TimeoutExpired:
180
+ return "", f"Command timed out after {timeout}s", -1
181
+ except FileNotFoundError as exc:
182
+ return "", str(exc), -1
183
+
184
+
185
+ # ---------------------------------------------------------------------------
186
+ # BashTool
187
+ # ---------------------------------------------------------------------------
188
+
189
+ class BashTool(ToolBase):
190
+ """Execute a bash / sh command and return its stdout+stderr.
191
+
192
+ Runs in the current working directory unless `cwd` is specified.
193
+ On Windows, falls back to `sh` from Git-for-Windows or WSL if available.
194
+ """
195
+
196
+ name: ClassVar[str] = "bash"
197
+ description: ClassVar[str] = (
198
+ "Run a shell command and return its output. "
199
+ "Use for file operations, running tests, git commands, build steps, etc. "
200
+ "Output is truncated at 50 KB."
201
+ )
202
+ input_schema: ClassVar[dict[str, Any]] = {
203
+ "type": "object",
204
+ "required": ["command"],
205
+ "properties": {
206
+ "command": {
207
+ "type": "string",
208
+ "description": "The shell command to execute.",
209
+ },
210
+ "cwd": {
211
+ "type": "string",
212
+ "description": "Working directory. Defaults to project root.",
213
+ },
214
+ "timeout": {
215
+ "type": "integer",
216
+ "description": f"Timeout in seconds (default {_DEFAULT_TIMEOUT_SECS}, max {_LONG_TIMEOUT_SECS}).",
217
+ },
218
+ "read_only": {
219
+ "type": "boolean",
220
+ "description": "If true, block any command classified as a write operation.",
221
+ },
222
+ },
223
+ "additionalProperties": False,
224
+ }
225
+
226
+ def __init__(self, workspace: Path | None = None) -> None:
227
+ self._workspace = workspace
228
+
229
+ def execute(self, params: dict[str, Any]) -> ToolResult: # noqa: D102
230
+ cmd: str = params["command"]
231
+ cwd_str: str | None = params.get("cwd")
232
+ timeout: int = min(int(params.get("timeout", _DEFAULT_TIMEOUT_SECS)), _LONG_TIMEOUT_SECS)
233
+ read_only: bool = bool(params.get("read_only", False))
234
+
235
+ cwd = Path(cwd_str).resolve() if cwd_str else self._workspace
236
+
237
+ if err := _validate(cmd, read_only=read_only, workspace=self._workspace):
238
+ return ToolResult(output="", error=err)
239
+
240
+ cls = _classify(cmd)
241
+ if cls["destructive"]:
242
+ log.warning("BashTool executing destructive command: %s", cmd)
243
+
244
+ shell_exec, shell_args = _resolve_shell()
245
+ stdout, stderr, rc = _run_subprocess(
246
+ cmd,
247
+ shell_exec=shell_exec,
248
+ shell_args=shell_args,
249
+ cwd=cwd,
250
+ timeout=timeout,
251
+ env=None,
252
+ )
253
+
254
+ combined = _merge_output(stdout, stderr)
255
+ output, truncated = _truncate(combined)
256
+
257
+ # Scan large stdout for injection patterns (file content piped into the shell)
258
+ if len(stdout) > _BASH_INJECT_CHECK_THRESHOLD:
259
+ from src.security import check_file_injection # local import avoids circular
260
+ cmd_key = cmd[:80] # stable dedup key per command
261
+ if cmd_key not in _bash_flagged:
262
+ injection = check_file_injection(stdout, filename=f"<bash stdout: {cmd[:60]}>")
263
+ if injection.is_injected and injection.severity == "high":
264
+ _bash_flagged.add(cmd_key)
265
+ output = f"[SECURITY: injection attempt blocked in bash stdout]\n{output[:200]}"
266
+
267
+ return ToolResult(
268
+ output=output,
269
+ error=None if rc == 0 else f"Exit code {rc}",
270
+ exit_code=rc,
271
+ truncated=truncated,
272
+ metadata={"command": cmd, "cwd": str(cwd or ""), "classification": cls},
273
+ )
274
+
275
+
276
+ # ---------------------------------------------------------------------------
277
+ # PowerShellTool
278
+ # ---------------------------------------------------------------------------
279
+
280
+ class PowerShellTool(ToolBase):
281
+ """Execute a PowerShell command. Windows-native; available on all platforms via pwsh."""
282
+
283
+ name: ClassVar[str] = "powershell"
284
+ description: ClassVar[str] = (
285
+ "Run a PowerShell command and return its output. "
286
+ "Use on Windows or when the task requires PowerShell-specific cmdlets. "
287
+ "Output is truncated at 50 KB."
288
+ )
289
+ input_schema: ClassVar[dict[str, Any]] = {
290
+ "type": "object",
291
+ "required": ["command"],
292
+ "properties": {
293
+ "command": {
294
+ "type": "string",
295
+ "description": "The PowerShell command to execute.",
296
+ },
297
+ "cwd": {
298
+ "type": "string",
299
+ "description": "Working directory. Defaults to project root.",
300
+ },
301
+ "timeout": {
302
+ "type": "integer",
303
+ "description": f"Timeout in seconds (default {_DEFAULT_TIMEOUT_SECS}).",
304
+ },
305
+ },
306
+ "additionalProperties": False,
307
+ }
308
+
309
+ def __init__(self, workspace: Path | None = None) -> None:
310
+ self._workspace = workspace
311
+
312
+ def execute(self, params: dict[str, Any]) -> ToolResult: # noqa: D102
313
+ cmd: str = params["command"]
314
+ cwd_str: str | None = params.get("cwd")
315
+ timeout: int = min(int(params.get("timeout", _DEFAULT_TIMEOUT_SECS)), _LONG_TIMEOUT_SECS)
316
+
317
+ cwd = Path(cwd_str).resolve() if cwd_str else self._workspace
318
+
319
+ ps_exec = _resolve_powershell()
320
+ if ps_exec is None:
321
+ return ToolResult(output="", error="PowerShell not found on this system.")
322
+
323
+ stdout, stderr, rc = _run_subprocess(
324
+ cmd,
325
+ shell_exec=ps_exec,
326
+ shell_args=["-NoProfile", "-NonInteractive", "-Command"],
327
+ cwd=cwd,
328
+ timeout=timeout,
329
+ env=None,
330
+ )
331
+
332
+ combined = _merge_output(stdout, stderr)
333
+ output, truncated = _truncate(combined)
334
+
335
+ return ToolResult(
336
+ output=output,
337
+ error=None if rc == 0 else f"Exit code {rc}",
338
+ exit_code=rc,
339
+ truncated=truncated,
340
+ metadata={"command": cmd, "cwd": str(cwd or "")},
341
+ )
342
+
343
+
344
+ # ---------------------------------------------------------------------------
345
+ # Shell resolution helpers
346
+ # ---------------------------------------------------------------------------
347
+
348
+ def _resolve_shell() -> tuple[str, list[str]]:
349
+ """Return (executable, args_before_command) for the best available shell."""
350
+ if sys.platform == "win32":
351
+ # Try Git Bash, then WSL bash, then cmd as last resort.
352
+ for candidate in ("bash", "sh"):
353
+ if _which(candidate):
354
+ return candidate, ["-c"]
355
+ return "cmd.exe", ["/c"]
356
+ return "bash", ["-c"]
357
+
358
+
359
+ def _resolve_powershell() -> str | None:
360
+ """Return path to PowerShell 7 (pwsh) or Windows PowerShell 5.1."""
361
+ for candidate in ("pwsh", "powershell"):
362
+ if _which(candidate):
363
+ return candidate
364
+ return None
365
+
366
+
367
+ def _which(name: str) -> bool:
368
+ """Return True if `name` is found on PATH."""
369
+ import shutil
370
+ return shutil.which(name) is not None
371
+
372
+
373
+ def _merge_output(stdout: str, stderr: str) -> str:
374
+ """Merge stdout and stderr the way a terminal would show them."""
375
+ parts = [p for p in (stdout.rstrip(), stderr.rstrip()) if p]
376
+ return "\n".join(parts)
377
+
378
+
379
+ # ---------------------------------------------------------------------------
380
+ # Auto-register
381
+ # ---------------------------------------------------------------------------
382
+
383
+ REGISTRY.register(BashTool())
384
+ REGISTRY.register(PowerShellTool())