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
@@ -0,0 +1,33 @@
1
+ """Remote command filter — allowlist for commands accepted from remote input."""
2
+ from __future__ import annotations
3
+ import logging
4
+ from src.remote.models import InputMessage
5
+
6
+ __all__ = ["RemoteCommandFilter"]
7
+
8
+ log = logging.getLogger(__name__)
9
+
10
+
11
+ class RemoteCommandFilter:
12
+ """Blocks disallowed slash-commands from remote clients.
13
+
14
+ Non-command text (anything not starting with '/') is always allowed.
15
+ Only explicitly whitelisted commands pass through.
16
+ """
17
+
18
+ ALLOWED_COMMANDS: frozenset = frozenset(
19
+ {"/help", "/status", "/cost", "/cancel", "/approve", "/deny"}
20
+ )
21
+
22
+ def is_allowed(self, text: str) -> bool:
23
+ """Return True for non-command text or whitelisted commands."""
24
+ if not text.startswith("/"):
25
+ return True
26
+ return text.strip() in self.ALLOWED_COMMANDS
27
+
28
+ def filter(self, msg: InputMessage) -> InputMessage | None:
29
+ """Return the message if allowed, or None if blocked (logs a warning)."""
30
+ if self.is_allowed(msg.text):
31
+ return msg
32
+ log.warning("RemoteCommandFilter: blocked command %r", msg.text)
33
+ return None
src/remote/models.py ADDED
@@ -0,0 +1,31 @@
1
+ """Data models for the remote server module."""
2
+ from __future__ import annotations
3
+ from dataclasses import dataclass, field
4
+
5
+ __all__ = ["RemoteEvent", "InputMessage", "SessionState", "PermissionRequest"]
6
+
7
+
8
+ @dataclass
9
+ class RemoteEvent:
10
+ type: str
11
+ payload: dict = field(default_factory=dict)
12
+
13
+
14
+ @dataclass
15
+ class InputMessage:
16
+ text: str
17
+ command: bool = False
18
+
19
+
20
+ @dataclass
21
+ class SessionState:
22
+ status: str
23
+ turn: int
24
+ cost_usd: float
25
+
26
+
27
+ @dataclass
28
+ class PermissionRequest:
29
+ id: str
30
+ description: str
31
+ risk_level: str
@@ -0,0 +1,79 @@
1
+ """Per-connection permission prompt handler for remote WebSocket clients."""
2
+ from __future__ import annotations
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ import secrets
7
+
8
+ __all__ = ["RemotePermissionPromptHandler"]
9
+
10
+ log = logging.getLogger(__name__)
11
+
12
+
13
+ class RemotePermissionPromptHandler:
14
+ """Sends a PermissionPrompt over WebSocket and awaits the client's response.
15
+
16
+ Usage::
17
+ handler = RemotePermissionPromptHandler(send_queue)
18
+ approved = await handler("Delete /etc/hosts?", "high")
19
+ # When the client responds:
20
+ handler.resolve(prompt_id, "approve") # called by the WS receiver
21
+ """
22
+
23
+ def __init__(self, send_queue: asyncio.Queue) -> None:
24
+ self._send_queue = send_queue
25
+ self._pending: dict[str, asyncio.Future] = {}
26
+
27
+ async def __call__(self, description: str, risk_level: str) -> bool:
28
+ """Send a permission prompt and block until approved/denied or timeout.
29
+
30
+ Returns True if approved, False on denial, timeout, or disconnect.
31
+ """
32
+ req_id = secrets.token_hex(16)
33
+ loop = asyncio.get_running_loop()
34
+ future: asyncio.Future = loop.create_future()
35
+ self._pending[req_id] = future
36
+
37
+ msg = json.dumps(
38
+ {
39
+ "type": "permission",
40
+ "id": req_id,
41
+ "description": description,
42
+ "risk_level": risk_level,
43
+ }
44
+ )
45
+ await _put_send_queue(self._send_queue, msg)
46
+
47
+ try:
48
+ return await asyncio.wait_for(future, timeout=60.0)
49
+ except asyncio.TimeoutError:
50
+ log.warning("Permission prompt %s timed out → deny", req_id)
51
+ return False
52
+ finally:
53
+ self._pending.pop(req_id, None)
54
+
55
+ def resolve(self, req_id: str, decision: str) -> None:
56
+ """Called by the WS receiver when the client sends a permission_response."""
57
+ future = self._pending.get(req_id)
58
+ if future and not future.done():
59
+ future.set_result(decision == "approve")
60
+
61
+ def cancel_all(self) -> None:
62
+ """Deny all pending prompts (called on disconnect)."""
63
+ for future in list(self._pending.values()):
64
+ if not future.done():
65
+ future.set_result(False)
66
+ self._pending.clear()
67
+
68
+
69
+ async def _put_send_queue(send_queue: asyncio.Queue, item: str) -> None:
70
+ """Put item into send queue, dropping the oldest entry if full."""
71
+ if send_queue.full():
72
+ try:
73
+ send_queue.get_nowait()
74
+ except asyncio.QueueEmpty:
75
+ pass
76
+ try:
77
+ send_queue.put_nowait(item)
78
+ except asyncio.QueueFull:
79
+ pass
src/remote/phone_ui.py ADDED
@@ -0,0 +1,48 @@
1
+ """phone_ui — HTML asset loader, ANSI stripper, audit log for RemoteServer."""
2
+ from __future__ import annotations
3
+
4
+ import datetime
5
+ import pathlib
6
+ import re
7
+ from typing import Any
8
+
9
+ CSP_HEADER = (
10
+ "default-src 'self'; "
11
+ "script-src 'self' 'unsafe-inline'; "
12
+ "style-src 'self' 'unsafe-inline'"
13
+ )
14
+
15
+ _ASSETS_DIR = pathlib.Path(__file__).parent.parent.parent / "assets" / "remote"
16
+
17
+ # ANSI escape sequences: CSI (colors/cursor), OSC (window title), bare ESC
18
+ _ANSI_RE = re.compile(
19
+ r"\x1b\[[0-9;]*[A-Za-z]"
20
+ r"|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)"
21
+ r"|\x1b[^[\]]"
22
+ )
23
+
24
+
25
+ def get_html() -> str:
26
+ """Return the contents of assets/remote/index.html."""
27
+ return (_ASSETS_DIR / "index.html").read_text(encoding="utf-8")
28
+
29
+
30
+ def strip_ansi(text: str) -> str:
31
+ """Remove ANSI escape sequences from *text*."""
32
+ return _ANSI_RE.sub("", text)
33
+
34
+
35
+ class AuditLog:
36
+ """In-memory append-only audit log for remote session events."""
37
+
38
+ def __init__(self) -> None:
39
+ self._entries: list[dict[str, Any]] = []
40
+
41
+ def record(self, event: str, detail: dict[str, Any] | None = None) -> None:
42
+ ts = datetime.datetime.now(datetime.timezone.utc).isoformat(
43
+ timespec="seconds"
44
+ ).replace("+00:00", "Z")
45
+ self._entries.append({"ts": ts, "event": event, **(detail or {})})
46
+
47
+ def entries(self) -> list[dict[str, Any]]:
48
+ return list(self._entries)
src/remote/protocol.py ADDED
@@ -0,0 +1,59 @@
1
+ """Canonical WebSocket message schema — all message types as TypedDicts."""
2
+ from __future__ import annotations
3
+ from typing import Literal
4
+ from typing_extensions import TypedDict
5
+
6
+ __all__ = [
7
+ "PingMessage",
8
+ "PongMessage",
9
+ "TranscriptEvent",
10
+ "InputCmd",
11
+ "PermissionPrompt",
12
+ "PermissionResponse",
13
+ "SessionStateMsg",
14
+ "ErrorMsg",
15
+ ]
16
+
17
+
18
+ class PingMessage(TypedDict):
19
+ type: Literal["ping"]
20
+
21
+
22
+ class PongMessage(TypedDict):
23
+ type: Literal["pong"]
24
+
25
+
26
+ class TranscriptEvent(TypedDict):
27
+ type: Literal["transcript"]
28
+ text: str
29
+ turn: int
30
+
31
+
32
+ class InputCmd(TypedDict):
33
+ type: Literal["input"]
34
+ text: str
35
+
36
+
37
+ class PermissionPrompt(TypedDict):
38
+ type: Literal["permission"]
39
+ id: str
40
+ description: str
41
+ risk_level: str
42
+
43
+
44
+ class PermissionResponse(TypedDict):
45
+ type: Literal["permission_response"]
46
+ id: str
47
+ decision: Literal["approve", "deny"]
48
+
49
+
50
+ class SessionStateMsg(TypedDict):
51
+ type: Literal["state"]
52
+ status: str
53
+ turn: int
54
+ cost_usd: float
55
+
56
+
57
+ class ErrorMsg(TypedDict):
58
+ type: Literal["error"]
59
+ message: str
src/remote/qr.py ADDED
@@ -0,0 +1,65 @@
1
+ """QR code rendering for gdm remote pairing URLs.
2
+
3
+ Security contract
4
+ -----------------
5
+ The pairing *token* MUST live in the URL **fragment** (``#token=...``),
6
+ never in the path or query string. Fragment identifiers are never sent
7
+ to the server in HTTP requests, so the token stays client-side only.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from urllib.parse import urlencode, urlparse, urlunparse
13
+
14
+
15
+ def make_pairing_url(tunnel_url: str, token: str) -> str:
16
+ """Embed *token* in the fragment of *tunnel_url*.
17
+
18
+ >>> url = make_pairing_url("https://example.trycloudflare.com", "abc123")
19
+ >>> url.startswith("https://example.trycloudflare.com")
20
+ True
21
+ >>> "#token=" in url
22
+ True
23
+ >>> "?" not in url # token is NOT in query string
24
+ True
25
+ """
26
+ parsed = urlparse(tunnel_url)
27
+ fragment = urlencode({"token": token})
28
+ return urlunparse(parsed._replace(fragment=fragment))
29
+
30
+
31
+ def render_qr(url: str, border: int = 2) -> str:
32
+ """Render *url* as a Unicode block QR code string.
33
+
34
+ Uses ``qrcode`` (``pip install qrcode``). Each dark module is
35
+ rendered as ``'\\u2588\\u2588'`` (full block × 2) for a square aspect
36
+ ratio in most terminal fonts. Light modules are two spaces.
37
+
38
+ Returns the QR code as a multi-line string.
39
+ """
40
+ try:
41
+ import qrcode # type: ignore[import-untyped]
42
+ import qrcode.constants # type: ignore[import-untyped]
43
+ except ImportError as exc:
44
+ raise ImportError(
45
+ "qrcode is required for QR rendering: pip install qrcode"
46
+ ) from exc
47
+
48
+ qr = qrcode.QRCode(
49
+ error_correction=qrcode.constants.ERROR_CORRECT_M,
50
+ box_size=1,
51
+ border=border,
52
+ )
53
+ qr.add_data(url)
54
+ qr.make(fit=True)
55
+
56
+ lines: list[str] = []
57
+ for row in qr.modules:
58
+ line = "".join("\u2588\u2588" if cell else " " for cell in row)
59
+ lines.append(line)
60
+ return "\n".join(lines)
61
+
62
+
63
+ def print_qr(url: str, border: int = 2) -> None:
64
+ """Print a QR code for *url* to stdout."""
65
+ print(render_qr(url, border=border))