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/auth.py ADDED
@@ -0,0 +1,411 @@
1
+ """Credential management for gdm — keychain storage + interactive login.
2
+
3
+ Providers:
4
+ - xAI Grok: API key (no OAuth, just a string)
5
+ - Google Gemini: API key OR OAuth device flow (RFC 8628)
6
+ - OpenAI Codex: API key
7
+
8
+ Storage priority (highest → lowest):
9
+ 1. Environment variables (XAI_API_KEY, GEMINI_API_KEY, OPENAI_API_KEY)
10
+ 2. System keychain (via `keyring` — macOS Keychain, Windows Credential Store, etc.)
11
+ 3. ~/.config/gdm/config.toml [api] section
12
+
13
+ Usage::
14
+
15
+ from src.auth import CredentialStore
16
+ store = CredentialStore()
17
+ store.set("grok", "xai-abc123")
18
+ key = store.get("grok") # "xai-abc123"
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import logging
23
+ import os
24
+ import time
25
+ from dataclasses import dataclass
26
+ from pathlib import Path
27
+ from typing import TYPE_CHECKING
28
+
29
+ from src.exceptions import ConfigError
30
+
31
+ if TYPE_CHECKING:
32
+ pass
33
+
34
+ __all__ = [
35
+ "CredentialStore",
36
+ "GdmCredentials",
37
+ "login_interactive",
38
+ "logout",
39
+ ]
40
+
41
+ log = logging.getLogger(__name__)
42
+
43
+ _KEYRING_SERVICE = "gdm-code"
44
+ _KEYRING_KEYS: dict[str, str] = {
45
+ "grok": "xai_api_key",
46
+ "gemini": "gemini_api_key",
47
+ "gemini_refresh": "gemini_refresh_token",
48
+ "codex": "openai_api_key",
49
+ "proxy": "proxy_token",
50
+ }
51
+
52
+ # Gemini OAuth constants (bring-your-own GCP project recommended for now).
53
+ # Users can set GDM_GOOGLE_CLIENT_ID / GDM_GOOGLE_CLIENT_SECRET env vars.
54
+ _GEMINI_AUTH_URI = "https://accounts.google.com/o/oauth2/device/code"
55
+ _GEMINI_TOKEN_URI = "https://oauth2.googleapis.com/token"
56
+ _GEMINI_SCOPE = "https://www.googleapis.com/auth/generative-language"
57
+ _GEMINI_DEVICE_POLL_INTERVAL = 5 # seconds between polls
58
+
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # GdmCredentials
62
+ # ---------------------------------------------------------------------------
63
+
64
+ @dataclass
65
+ class GdmCredentials:
66
+ """All resolved credentials for a session."""
67
+
68
+ xai_api_key: str | None = None
69
+ gemini_api_key: str | None = None
70
+ openai_api_key: str | None = None
71
+ proxy_token: str | None = None
72
+
73
+ @property
74
+ def has_grok(self) -> bool:
75
+ return bool(self.xai_api_key)
76
+
77
+ @property
78
+ def has_gemini(self) -> bool:
79
+ return bool(self.gemini_api_key)
80
+
81
+ @property
82
+ def has_codex(self) -> bool:
83
+ return bool(self.openai_api_key)
84
+
85
+ @property
86
+ def any_key(self) -> bool:
87
+ return self.has_grok or self.has_gemini or self.has_codex
88
+
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # CredentialStore
92
+ # ---------------------------------------------------------------------------
93
+
94
+ class CredentialStore:
95
+ """Unified credential store with keychain backend.
96
+
97
+ Falls back gracefully if `keyring` is not installed — writes/reads from
98
+ ~/.config/gdm/.credentials (plain text, user-readable only).
99
+ """
100
+
101
+ def __init__(self) -> None:
102
+ self._keyring_available = _check_keyring()
103
+ self._fallback_path = Path.home() / ".config" / "gdm" / ".credentials"
104
+
105
+ # ── Public API ────────────────────────────────────────────────────────
106
+
107
+ def set(self, provider: str, value: str) -> None:
108
+ """Persist a credential for `provider`."""
109
+ key = _KEYRING_KEYS.get(provider, provider)
110
+ if self._keyring_available:
111
+ import keyring # type: ignore[import]
112
+ keyring.set_password(_KEYRING_SERVICE, key, value)
113
+ log.debug("Stored %r in system keychain", key)
114
+ else:
115
+ self._set_fallback(key, value)
116
+ log.debug("Stored %r in fallback credentials file", key)
117
+
118
+ def get(self, provider: str) -> str | None:
119
+ """Retrieve a credential for `provider`, or None if not found."""
120
+ key = _KEYRING_KEYS.get(provider, provider)
121
+ if self._keyring_available:
122
+ import keyring # type: ignore[import]
123
+ try:
124
+ return keyring.get_password(_KEYRING_SERVICE, key) or None
125
+ except Exception as exc: # noqa: BLE001
126
+ log.warning("keyring.get_password failed: %s", exc)
127
+ return self._get_fallback(key)
128
+
129
+ def delete(self, provider: str) -> None:
130
+ """Remove a stored credential."""
131
+ key = _KEYRING_KEYS.get(provider, provider)
132
+ if self._keyring_available:
133
+ import keyring # type: ignore[import]
134
+ try:
135
+ keyring.delete_password(_KEYRING_SERVICE, key)
136
+ except Exception: # noqa: BLE001
137
+ pass
138
+ self._delete_fallback(key)
139
+
140
+ def load_all(self) -> GdmCredentials:
141
+ """Return all credentials, checking env vars first, then keychain."""
142
+ return GdmCredentials(
143
+ xai_api_key=(
144
+ os.environ.get("XAI_API_KEY")
145
+ or self.get("grok")
146
+ or None
147
+ ),
148
+ gemini_api_key=(
149
+ os.environ.get("GEMINI_API_KEY")
150
+ or self.get("gemini")
151
+ or None
152
+ ),
153
+ openai_api_key=(
154
+ os.environ.get("OPENAI_API_KEY")
155
+ or self.get("codex")
156
+ or None
157
+ ),
158
+ proxy_token=(
159
+ os.environ.get("GDM_PROXY_TOKEN")
160
+ or self.get("proxy")
161
+ or None
162
+ ),
163
+ )
164
+
165
+ # ── Fallback file (when keyring not available) ────────────────────────
166
+
167
+ def _set_fallback(self, key: str, value: str) -> None:
168
+ self._fallback_path.parent.mkdir(parents=True, exist_ok=True)
169
+ data = self._read_fallback_file()
170
+ data[key] = value
171
+ lines = "\n".join(f"{k}={v}" for k, v in data.items())
172
+ self._fallback_path.write_text(lines, encoding="utf-8")
173
+ # Restrict permissions to owner-only on POSIX.
174
+ try:
175
+ os.chmod(self._fallback_path, 0o600)
176
+ except OSError as exc:
177
+ log.warning(
178
+ "chmod failed for credential file %s: %s — file may be world-readable",
179
+ self._fallback_path,
180
+ exc,
181
+ )
182
+
183
+ def _get_fallback(self, key: str) -> str | None:
184
+ data = self._read_fallback_file()
185
+ return data.get(key)
186
+
187
+ def _delete_fallback(self, key: str) -> None:
188
+ data = self._read_fallback_file()
189
+ data.pop(key, None)
190
+ lines = "\n".join(f"{k}={v}" for k, v in data.items())
191
+ self._fallback_path.write_text(lines, encoding="utf-8")
192
+
193
+ def _read_fallback_file(self) -> dict[str, str]:
194
+ if not self._fallback_path.exists():
195
+ return {}
196
+ try:
197
+ result: dict[str, str] = {}
198
+ for line in self._fallback_path.read_text(encoding="utf-8").splitlines():
199
+ if "=" in line:
200
+ k, _, v = line.partition("=")
201
+ result[k.strip()] = v.strip()
202
+ return result
203
+ except OSError:
204
+ return {}
205
+
206
+
207
+ # ---------------------------------------------------------------------------
208
+ # Interactive login / logout
209
+ # ---------------------------------------------------------------------------
210
+
211
+ def login_interactive(provider: str, store: CredentialStore | None = None) -> None:
212
+ """Interactively prompt the user to log in for a provider.
213
+
214
+ Supports:
215
+ - "grok" → paste API key
216
+ - "gemini" → paste API key OR OAuth device flow
217
+ - "codex" → paste API key
218
+
219
+ Args:
220
+ provider: one of "grok", "gemini", "codex", "all"
221
+ store: CredentialStore instance (creates a new one if None)
222
+ """
223
+ from rich.console import Console
224
+ from rich.prompt import Prompt
225
+
226
+ console = Console()
227
+ s = store or CredentialStore()
228
+
229
+ providers_to_login = (
230
+ ["grok", "gemini", "codex"] if provider == "all" else [provider.lower()]
231
+ )
232
+
233
+ for prov in providers_to_login:
234
+ match prov:
235
+ case "grok":
236
+ _login_grok(s, console)
237
+ case "gemini":
238
+ _login_gemini(s, console)
239
+ case "codex":
240
+ _login_codex(s, console)
241
+ case _:
242
+ console.print(f"[red]Unknown provider: {prov!r}[/red]")
243
+ console.print("Valid providers: grok, gemini, codex, all")
244
+
245
+
246
+ def logout(provider: str, store: CredentialStore | None = None) -> None:
247
+ """Remove stored credentials for a provider.
248
+
249
+ Args:
250
+ provider: one of "grok", "gemini", "codex", "all"
251
+ store: CredentialStore instance (creates a new one if None)
252
+ """
253
+ from rich.console import Console
254
+ console = Console()
255
+ s = store or CredentialStore()
256
+
257
+ if provider == "all":
258
+ for prov in ("grok", "gemini", "gemini_refresh", "codex"):
259
+ s.delete(prov)
260
+ console.print("[green]✓[/green] Logged out from all providers.")
261
+ else:
262
+ s.delete(provider)
263
+ if provider == "gemini":
264
+ s.delete("gemini_refresh")
265
+ console.print(f"[green]✓[/green] Logged out from [bold]{provider}[/bold].")
266
+
267
+
268
+ # ---------------------------------------------------------------------------
269
+ # Per-provider login helpers
270
+ # ---------------------------------------------------------------------------
271
+
272
+ def _login_grok(store: CredentialStore, console: object) -> None:
273
+ from rich.console import Console
274
+ from rich.prompt import Prompt
275
+ c: Console = console # type: ignore[assignment]
276
+ c.print("\n[bold cyan]xAI Grok login[/bold cyan]")
277
+ c.print("Get your API key at: [link=https://console.x.ai]https://console.x.ai[/link]")
278
+ key = Prompt.ask("Paste your XAI_API_KEY", password=True, console=c)
279
+ if key.strip():
280
+ store.set("grok", key.strip())
281
+ c.print("[green]✓[/green] Grok API key saved to keychain.")
282
+ else:
283
+ c.print("[yellow]No key entered — skipped.[/yellow]")
284
+
285
+
286
+ def _login_codex(store: CredentialStore, console: object) -> None:
287
+ from rich.console import Console
288
+ from rich.prompt import Prompt
289
+ c: Console = console # type: ignore[assignment]
290
+ c.print("\n[bold cyan]OpenAI / Codex login[/bold cyan]")
291
+ c.print("Get your API key at: [link=https://platform.openai.com/api-keys]platform.openai.com/api-keys[/link]")
292
+ key = Prompt.ask("Paste your OPENAI_API_KEY", password=True, console=c)
293
+ if key.strip():
294
+ store.set("codex", key.strip())
295
+ c.print("[green]✓[/green] OpenAI API key saved to keychain.")
296
+ else:
297
+ c.print("[yellow]No key entered — skipped.[/yellow]")
298
+
299
+
300
+ def _login_gemini(store: CredentialStore, console: object) -> None:
301
+ from rich.console import Console
302
+ from rich.prompt import Prompt
303
+ c: Console = console # type: ignore[assignment]
304
+ c.print("\n[bold cyan]Google Gemini login[/bold cyan]")
305
+ c.print("1. API key (quick) — no OAuth needed")
306
+ c.print("2. OAuth device flow — uses Google account, no key required\n")
307
+ choice = Prompt.ask("Choose login method", choices=["1", "2"], default="1", console=c)
308
+
309
+ if choice == "1":
310
+ c.print("Get your API key at: [link=https://aistudio.google.com/app/apikey]aistudio.google.com/app/apikey[/link]")
311
+ key = Prompt.ask("Paste your GEMINI_API_KEY", password=True, console=c)
312
+ if key.strip():
313
+ store.set("gemini", key.strip())
314
+ c.print("[green]✓[/green] Gemini API key saved to keychain.")
315
+ else:
316
+ c.print("[yellow]No key entered — skipped.[/yellow]")
317
+ else:
318
+ _login_gemini_oauth(store, c)
319
+
320
+
321
+ def _login_gemini_oauth(store: CredentialStore, console: object) -> None:
322
+ """Complete Gemini OAuth device authorization flow (RFC 8628)."""
323
+ from rich.console import Console
324
+ c: Console = console # type: ignore[assignment]
325
+
326
+ client_id = os.environ.get("GDM_GOOGLE_CLIENT_ID")
327
+ client_secret = os.environ.get("GDM_GOOGLE_CLIENT_SECRET")
328
+
329
+ if not client_id or not client_secret:
330
+ c.print(
331
+ "[yellow]Warning:[/yellow] Gemini OAuth requires a GCP OAuth app.\n"
332
+ "Set GDM_GOOGLE_CLIENT_ID and GDM_GOOGLE_CLIENT_SECRET, or use method 1 (API key).\n"
333
+ "See: https://console.cloud.google.com/apis/credentials"
334
+ )
335
+ return
336
+
337
+ try:
338
+ import requests
339
+ except ImportError:
340
+ c.print("[red]`requests` not installed. Run: pip install requests[/red]")
341
+ return
342
+
343
+ # Step 1: Request device code.
344
+ resp = requests.post(
345
+ _GEMINI_AUTH_URI,
346
+ data={"client_id": client_id, "scope": _GEMINI_SCOPE},
347
+ timeout=20,
348
+ )
349
+ if resp.status_code != 200:
350
+ c.print(f"[red]Device code request failed: {resp.status_code} {resp.text}[/red]")
351
+ return
352
+
353
+ data = resp.json()
354
+ device_code: str = data["device_code"]
355
+ user_code: str = data["user_code"]
356
+ verification_url: str = data["verification_url"]
357
+ expires_in: int = data.get("expires_in", 1800)
358
+ interval: int = data.get("interval", _GEMINI_DEVICE_POLL_INTERVAL)
359
+
360
+ c.print(f"\n[bold]Visit:[/bold] {verification_url}")
361
+ c.print(f"[bold]Enter code:[/bold] {user_code}\n")
362
+ c.print(f"Waiting for authorization (expires in {expires_in}s)...")
363
+
364
+ # Step 2: Poll for token.
365
+ deadline = time.time() + expires_in
366
+ while time.time() < deadline:
367
+ time.sleep(interval)
368
+ token_resp = requests.post(
369
+ _GEMINI_TOKEN_URI,
370
+ data={
371
+ "client_id": client_id,
372
+ "client_secret": client_secret,
373
+ "device_code": device_code,
374
+ "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
375
+ },
376
+ timeout=20,
377
+ )
378
+ token_data = token_resp.json()
379
+ error = token_data.get("error")
380
+ if error == "authorization_pending":
381
+ continue
382
+ if error == "slow_down":
383
+ interval += 5
384
+ continue
385
+ if "access_token" in token_data:
386
+ access_token: str = token_data["access_token"]
387
+ refresh_token: str | None = token_data.get("refresh_token")
388
+ store.set("gemini", access_token)
389
+ if refresh_token:
390
+ store.set("gemini_refresh", refresh_token)
391
+ c.print("[green]✓[/green] Gemini OAuth complete. Access token saved to keychain.")
392
+ return
393
+ # Unrecoverable error.
394
+ c.print(f"[red]OAuth error: {token_data.get('error_description', error)}[/red]")
395
+ return
396
+
397
+ c.print("[red]Authorization timed out. Please try again.[/red]")
398
+
399
+
400
+ # ---------------------------------------------------------------------------
401
+ # Helpers
402
+ # ---------------------------------------------------------------------------
403
+
404
+ def _check_keyring() -> bool:
405
+ """Return True if keyring is available and functional."""
406
+ try:
407
+ import keyring # noqa: F401
408
+ return True
409
+ except ImportError:
410
+ log.info("keyring not installed — using fallback credential file")
411
+ return False