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/sdk/plugin_host.py ADDED
@@ -0,0 +1,100 @@
1
+ """
2
+ Plugin host subprocess entry point.
3
+ Run as: python -m src.sdk.plugin_host <plugin_package>
4
+ Communicates with parent via JSON-RPC 2.0 over stdin/stdout.
5
+ """
6
+ import sys
7
+ import json
8
+ import importlib
9
+ import logging
10
+ from src.sdk.plugin_base import GdmPlugin, PermissionManifest
11
+
12
+ log = logging.getLogger(__name__)
13
+
14
+
15
+ class PermissionError(Exception):
16
+ pass
17
+
18
+
19
+ def _check_permission(manifest: PermissionManifest, tool_meta: dict) -> None:
20
+ required = tool_meta.get("permissions")
21
+ if required is None:
22
+ return
23
+ for perm in ("file_write", "network", "git_write"):
24
+ if getattr(required, perm, False) and not getattr(manifest, perm, False):
25
+ raise PermissionError(f"Tool requires '{perm}' but plugin manifest denies it")
26
+
27
+
28
+ def main():
29
+ if len(sys.argv) < 2:
30
+ print(json.dumps({"error": "Usage: plugin_host <plugin_package>"}), flush=True)
31
+ sys.exit(1)
32
+
33
+ plugin_package = sys.argv[1]
34
+ try:
35
+ module = importlib.import_module(plugin_package)
36
+ except ImportError as e:
37
+ print(json.dumps({"error": f"Cannot import plugin: {e}"}), flush=True)
38
+ sys.exit(1)
39
+
40
+ plugin: GdmPlugin = getattr(module, "plugin", None)
41
+ if plugin is None:
42
+ print(json.dumps({"error": "Plugin module has no 'plugin' instance"}), flush=True)
43
+ sys.exit(1)
44
+
45
+ plugin.on_load(context={})
46
+
47
+ # Discover @tool-decorated methods
48
+ tools = {}
49
+ for attr_name in dir(type(plugin)):
50
+ fn = getattr(type(plugin), attr_name, None)
51
+ if callable(fn) and hasattr(fn, "_gdm_tool"):
52
+ tools[fn._gdm_tool["name"]] = fn
53
+
54
+ try:
55
+ for line in sys.stdin:
56
+ line = line.strip()
57
+ if not line:
58
+ continue
59
+ try:
60
+ request = json.loads(line)
61
+ except json.JSONDecodeError:
62
+ print(json.dumps({"id": None, "error": "Invalid JSON"}), flush=True)
63
+ continue
64
+
65
+ req_id = request.get("id")
66
+ method = request.get("method", "")
67
+
68
+ if method == "list_tools":
69
+ reply = {"id": req_id, "result": [t._gdm_tool for t in tools.values()]}
70
+ elif method == "call_tool":
71
+ params = request.get("params", {})
72
+ name = params.get("name", "")
73
+ args = params.get("args", {})
74
+ if name not in tools:
75
+ reply = {"id": req_id, "error": f"Unknown tool: {name}"}
76
+ else:
77
+ fn = tools[name]
78
+ try:
79
+ _check_permission(plugin.permissions, fn._gdm_tool)
80
+ result = fn(plugin, args) if hasattr(fn, "__self__") else fn(args)
81
+ reply = {"id": req_id, "result": result}
82
+ except PermissionError as e:
83
+ reply = {"id": req_id, "error": f"Permission denied: {e}"}
84
+ except Exception as e:
85
+ reply = {"id": req_id, "error": str(e)}
86
+ elif method == "shutdown":
87
+ plugin.on_unload()
88
+ reply = {"id": req_id, "result": "ok"}
89
+ print(json.dumps(reply), flush=True)
90
+ break
91
+ else:
92
+ reply = {"id": req_id, "error": f"Unknown method: {method}"}
93
+
94
+ print(json.dumps(reply), flush=True)
95
+ finally:
96
+ plugin.on_unload()
97
+
98
+
99
+ if __name__ == "__main__":
100
+ main()
@@ -0,0 +1,101 @@
1
+ import json
2
+ import subprocess
3
+ import sys
4
+ import threading
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ TRUST_STORE = Path.home() / ".config" / "gdm" / "trusted_plugins.json"
9
+ LOCAL_PLUGIN_DIR = Path(".gdm_plugins")
10
+
11
+
12
+ class UntrustedPluginError(Exception):
13
+ pass
14
+
15
+
16
+ class IpcPluginHandle:
17
+ def __init__(self, plugin_name: str, proc: subprocess.Popen):
18
+ self.name = plugin_name
19
+ self._proc = proc
20
+ self._lock = threading.Lock()
21
+ self._req_id = 0
22
+
23
+ def call(self, method: str, params: dict = None) -> dict:
24
+ with self._lock:
25
+ self._req_id += 1
26
+ req = {"id": self._req_id, "method": method, "params": params or {}}
27
+ self._proc.stdin.write(json.dumps(req) + "\n")
28
+ self._proc.stdin.flush()
29
+ line = self._proc.stdout.readline()
30
+ return json.loads(line)
31
+
32
+ def list_tools(self) -> list[dict]:
33
+ resp = self.call("list_tools")
34
+ return resp.get("result", [])
35
+
36
+ def call_tool(self, name: str, args: dict) -> dict:
37
+ return self.call("call_tool", {"name": name, "args": args})
38
+
39
+ def shutdown(self) -> None:
40
+ try:
41
+ self.call("shutdown")
42
+ self._proc.wait(timeout=5)
43
+ except Exception:
44
+ self._proc.kill()
45
+
46
+
47
+ def is_trusted(plugin_name: str, trust_store_path: Path = None) -> bool:
48
+ path = trust_store_path or TRUST_STORE
49
+ if not path.exists():
50
+ return False
51
+ try:
52
+ trusted = json.loads(path.read_text())
53
+ return plugin_name in trusted.get("plugins", [])
54
+ except Exception:
55
+ return False
56
+
57
+
58
+ def trust_plugin(plugin_name: str, trust_store_path: Path = None) -> None:
59
+ path = trust_store_path or TRUST_STORE
60
+ path.parent.mkdir(parents=True, exist_ok=True)
61
+ trusted = {"plugins": []}
62
+ if path.exists():
63
+ try:
64
+ trusted = json.loads(path.read_text())
65
+ except Exception:
66
+ pass
67
+ if plugin_name not in trusted["plugins"]:
68
+ trusted["plugins"].append(plugin_name)
69
+ path.write_text(json.dumps(trusted, indent=2))
70
+
71
+
72
+ def discover_local_plugins() -> list[Path]:
73
+ if not LOCAL_PLUGIN_DIR.exists():
74
+ return []
75
+ return [p for p in LOCAL_PLUGIN_DIR.iterdir()
76
+ if p.is_dir() and (p / "gdm_plugin.py").exists()]
77
+
78
+
79
+ def discover_entry_point_plugins() -> list[str]:
80
+ """Discover plugins registered via [project.entry-points.'gdm.plugins']."""
81
+ try:
82
+ from importlib.metadata import entry_points
83
+ eps = entry_points(group="gdm.plugins")
84
+ return [ep.name for ep in eps]
85
+ except Exception:
86
+ return []
87
+
88
+
89
+ def load_plugin(plugin_name: str, plugin_path: str,
90
+ trust_store_path: Path = None) -> IpcPluginHandle:
91
+ if not is_trusted(plugin_name, trust_store_path):
92
+ raise UntrustedPluginError(
93
+ f"Plugin '{plugin_name}' is not in the trust store. "
94
+ f"Run `gdm plugin trust {plugin_name}` to grant access."
95
+ )
96
+ proc = subprocess.Popen(
97
+ [sys.executable, "-m", "src.sdk.plugin_host", plugin_path],
98
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE,
99
+ text=True, bufsize=1
100
+ )
101
+ return IpcPluginHandle(plugin_name, proc)
src/security.py ADDED
@@ -0,0 +1,409 @@
1
+ """Security module — prompt injection defense, context tagging, audit logging.
2
+
3
+ The single most critical non-functional concern in an agentic coding assistant.
4
+ OWASP #1 for AI agents: prompt injection via file content.
5
+
6
+ Four layers:
7
+ 1. tag_untrusted() — wraps file content so the model knows it is data, not instruction
8
+ 2. check_file_injection() — high-sensitivity structural scan for file content (redact on hit)
9
+ 3. check_user_injection() — low-sensitivity structural-only scan for user-typed messages
10
+ 4. AuditLogger — writes every tool call to gdm.db audit_log (immutable record)
11
+
12
+ Legacy:
13
+ check_injection() — kept for backward compatibility; delegates to check_file_injection()
14
+ and raises InjectionDetectedError on high severity.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import dataclasses
19
+ import logging
20
+ import re
21
+ from typing import Literal
22
+
23
+ from src._internal.constants import (
24
+ _INJECTION_PATTERNS,
25
+ _UNTRUSTED_TAG_PREFIX,
26
+ _UNTRUSTED_TAG_SUFFIX,
27
+ _USER_INSTRUCTIONS_TAG,
28
+ )
29
+ from src.exceptions import InjectionDetectedError
30
+
31
+ __all__ = [
32
+ "tag_untrusted",
33
+ "tag_user_instructions",
34
+ "check_file_injection",
35
+ "check_user_injection",
36
+ "check_injection",
37
+ "InjectionResult",
38
+ "AuditLogger",
39
+ "clear_flagged_files",
40
+ "redact",
41
+ "redact_dict",
42
+ "redact_json",
43
+ "RedactionEngine",
44
+ "SafeForRemote",
45
+ "SafeForExport",
46
+ "InternalOnly",
47
+ ]
48
+
49
+ log = logging.getLogger(__name__)
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # InjectionResult
53
+ # ---------------------------------------------------------------------------
54
+
55
+ @dataclasses.dataclass(frozen=True)
56
+ class InjectionResult:
57
+ """Result of an injection scan.
58
+
59
+ Attributes:
60
+ is_injected: True if an injection pattern was found.
61
+ pattern: The matching pattern string, or None.
62
+ severity: "high" | "medium" | "low"
63
+ """
64
+ is_injected: bool
65
+ pattern: str | None
66
+ severity: Literal["high", "medium", "low"]
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # File-content injection patterns (HIGH sensitivity)
71
+ #
72
+ # These match structural injection: role overrides, token stuffing, embedded
73
+ # system prompt headers. Real attack surface — file content injected into the
74
+ # model prompt. Action on hit: redact + log WARNING.
75
+ # ---------------------------------------------------------------------------
76
+
77
+ _FILE_HIGH_PATTERNS: list[tuple[re.Pattern[str], str]] = [
78
+ # Role injection at line start
79
+ (re.compile(r"^\s*SYSTEM\s*:", re.IGNORECASE | re.MULTILINE), "role-injection:SYSTEM"),
80
+ (re.compile(r"^\s*ASSISTANT\s*:", re.IGNORECASE | re.MULTILINE), "role-injection:ASSISTANT"),
81
+ # Common override phrases (multi-line, anywhere)
82
+ (re.compile(r"IGNORE\s+PREVIOUS\s+INSTRUCTIONS", re.IGNORECASE), "override:ignore-prev"),
83
+ (re.compile(r"IGNORE\s+ALL\s+PREVIOUS", re.IGNORECASE), "override:ignore-all"),
84
+ (re.compile(r"FORGET\s+YOUR\s+INSTRUCTIONS", re.IGNORECASE), "override:forget"),
85
+ (re.compile(r"DISREGARD\s+(ALL\s+)?YOUR\s+(PREVIOUS\s+)?INSTRUCTIONS", re.IGNORECASE), "override:disregard"),
86
+ # HTML/markdown comment injection
87
+ (re.compile(r"<!--\s*IGNORE\s+PREVIOUS", re.IGNORECASE), "html-comment-inject"),
88
+ # ChatML / tokenizer-level injection
89
+ (re.compile(r"<\|im_start\|>\s*system", re.IGNORECASE), "chatml:im_start"),
90
+ (re.compile(r"<\|endoftext\|>", re.IGNORECASE), "chatml:endoftext"),
91
+ (re.compile(r"<\|pad\|>", re.IGNORECASE), "chatml:pad"),
92
+ (re.compile(r"<\|system\|>", re.IGNORECASE), "chatml:system"),
93
+ (re.compile(r"<\|user\|>", re.IGNORECASE), "chatml:user"),
94
+ (re.compile(r"<\|assistant\|>", re.IGNORECASE), "chatml:assistant"),
95
+ # JSON/YAML key injection
96
+ (re.compile(r'"_system"\s*:', re.IGNORECASE), "json-key:_system"),
97
+ (re.compile(r'"_instructions"\s*:', re.IGNORECASE), "json-key:_instructions"),
98
+ (re.compile(r'"_override"\s*:', re.IGNORECASE), "json-key:_override"),
99
+ (re.compile(r'_system\s*:', re.IGNORECASE), "yaml-key:_system"),
100
+ # Docstring hijacking with instruction verbs
101
+ (re.compile(r'"""\s*(IGNORE|OVERRIDE|FORGET|DISREGARD)\s', re.IGNORECASE), "docstring-inject"),
102
+ ]
103
+
104
+ _FILE_MEDIUM_PATTERNS: list[tuple[re.Pattern[str], str]] = [
105
+ # Softer override phrases — higher false-positive risk
106
+ (re.compile(r"\bNEW\s+INSTRUCTIONS\b\s*:", re.IGNORECASE), "soft:new-instructions"),
107
+ (re.compile(r"\bSYSTEM\s+PROMPT\b\s*:", re.IGNORECASE), "soft:system-prompt"),
108
+ (re.compile(r"\bYOU\s+ARE\s+NOW\b", re.IGNORECASE), "soft:you-are-now"),
109
+ (re.compile(r"\bACT\s+AS\s+IF\b", re.IGNORECASE), "soft:act-as-if"),
110
+ (re.compile(r"###\s*INSTRUCTION", re.IGNORECASE), "soft:instruction-heading"),
111
+ ]
112
+
113
+ # ---------------------------------------------------------------------------
114
+ # User-message injection patterns (LOW sensitivity, structural-only)
115
+ #
116
+ # Legitimate developer messages ("ignore the lint errors") must NOT trigger.
117
+ # Only clearly malicious token-level or structural abuse patterns are here.
118
+ # ---------------------------------------------------------------------------
119
+
120
+ _USER_STRUCTURAL_PATTERNS: list[tuple[re.Pattern[str], str]] = [
121
+ (re.compile(r"\[SYSTEM\s+OVERRIDE\]", re.IGNORECASE), "user:system-override"),
122
+ (re.compile(r"<\|im_start\|>\s*system", re.IGNORECASE), "user:chatml-im_start"),
123
+ (re.compile(r"<\|endoftext\|>", re.IGNORECASE), "user:chatml-endoftext"),
124
+ (re.compile(r"<\|system\|>", re.IGNORECASE), "user:chatml-system"),
125
+ (re.compile(r"<<<\s*SYSTEM\s*>>>", re.IGNORECASE), "user:triple-lt-system"),
126
+ (re.compile(r"\[INST\]\s*\[SYS\]", re.IGNORECASE), "user:llama-sys"),
127
+ (re.compile(r"<s>\s*\[INST\]", re.IGNORECASE), "user:llama-bos"),
128
+ ]
129
+
130
+ # ---------------------------------------------------------------------------
131
+ # Per-session dedup — same file flagged at most once to avoid log floods
132
+ # ---------------------------------------------------------------------------
133
+
134
+ _flagged_files: set[str] = set()
135
+
136
+
137
+ def clear_flagged_files() -> None:
138
+ """Reset the per-session dedup set (call at session start/between sessions)."""
139
+ _flagged_files.clear()
140
+
141
+
142
+ # ---------------------------------------------------------------------------
143
+ # Legacy compiled patterns (used only by check_injection() backward compat)
144
+ # ---------------------------------------------------------------------------
145
+ _COMPILED_PATTERNS: list[re.Pattern[str]] = [
146
+ re.compile(p, re.IGNORECASE) for p in _INJECTION_PATTERNS
147
+ ]
148
+
149
+
150
+ # ---------------------------------------------------------------------------
151
+ # Public API
152
+ # ---------------------------------------------------------------------------
153
+
154
+ def check_file_injection(content: str, filename: str) -> InjectionResult:
155
+ """Scan file content for prompt injection patterns.
156
+
157
+ Uses HIGH-sensitivity patterns for direct structural injection, then
158
+ MEDIUM patterns for softer overrides. Only logs each unique filename
159
+ once per session (dedup).
160
+
161
+ Args:
162
+ content: Raw text content of the file.
163
+ filename: Display name of the file (used in logs and redaction prefix).
164
+
165
+ Returns:
166
+ InjectionResult. Caller should inspect `is_injected` and `severity`
167
+ and redact or block if severity == "high".
168
+ """
169
+ for pattern, label in _FILE_HIGH_PATTERNS:
170
+ if pattern.search(content):
171
+ if filename not in _flagged_files:
172
+ _flagged_files.add(filename)
173
+ log.warning(
174
+ "Injection pattern detected in '%s' (high severity): '%s' — content will be redacted",
175
+ filename, label,
176
+ )
177
+ return InjectionResult(is_injected=True, pattern=label, severity="high")
178
+
179
+ for pattern, label in _FILE_MEDIUM_PATTERNS:
180
+ if pattern.search(content):
181
+ if filename not in _flagged_files:
182
+ _flagged_files.add(filename)
183
+ log.warning(
184
+ "Injection pattern detected in '%s' (medium severity): '%s'",
185
+ filename, label,
186
+ )
187
+ return InjectionResult(is_injected=True, pattern=label, severity="medium")
188
+
189
+ return InjectionResult(is_injected=False, pattern=None, severity="low")
190
+
191
+
192
+ def check_user_injection(message: str) -> InjectionResult:
193
+ """Scan a user-typed message for structural injection patterns (LOW sensitivity).
194
+
195
+ Only catches clearly malicious token-level abuse. Legitimate developer
196
+ messages ("ignore the lint errors", "you are now a senior dev") are
197
+ intentionally NOT flagged.
198
+
199
+ Args:
200
+ message: The user's raw message text.
201
+
202
+ Returns:
203
+ InjectionResult. If is_injected is True, the caller should emit a
204
+ WARNING event but must NOT block the message.
205
+ """
206
+ for pattern, label in _USER_STRUCTURAL_PATTERNS:
207
+ if pattern.search(message):
208
+ log.warning("User message contains structural injection pattern: '%s'", label)
209
+ return InjectionResult(is_injected=True, pattern=label, severity="low")
210
+ return InjectionResult(is_injected=False, pattern=None, severity="low")
211
+
212
+
213
+ def check_injection(content: str, filename: str) -> None:
214
+ """Legacy compatibility shim — prefer check_file_injection() for new code.
215
+
216
+ Raises InjectionDetectedError on high-severity file injection patterns.
217
+ """
218
+ result = check_file_injection(content, filename)
219
+ if result.is_injected and result.severity == "high":
220
+ raise InjectionDetectedError(filename=filename, pattern=result.pattern or "unknown")
221
+
222
+
223
+ def tag_untrusted(content: str, filename: str) -> str:
224
+ """Wrap file content in an untrusted tag so the model treats it as data only.
225
+
226
+ The system prompt must explicitly instruct the model to never follow
227
+ instructions found inside [UNTRUSTED: ...] blocks.
228
+
229
+ Example output::
230
+
231
+ [UNTRUSTED: src/auth.py]
232
+ def login(user, password):
233
+ ...
234
+ [/UNTRUSTED: src/auth.py]
235
+ """
236
+ return (
237
+ f"{_UNTRUSTED_TAG_PREFIX}{filename}{_UNTRUSTED_TAG_SUFFIX}\n"
238
+ f"{content}\n"
239
+ f"[/UNTRUSTED: {filename}]"
240
+ )
241
+
242
+
243
+ def tag_user_instructions(content: str) -> str:
244
+ """Wrap .gdm instructions with the USER INSTRUCTIONS tag.
245
+
246
+ Unlike UNTRUSTED, the model IS allowed to follow these instructions.
247
+ The distinction between the two tags must be clear in the system prompt.
248
+ """
249
+ return f"{_USER_INSTRUCTIONS_TAG}\n{content}\n[/USER INSTRUCTIONS]"
250
+
251
+
252
+ class AuditLogger:
253
+ """Writes an immutable audit trail of all tool executions to gdm.db.
254
+
255
+ Instantiate once per session and pass the session_id. Every tool call —
256
+ allowed or denied — must be logged here. This is the accountability record.
257
+ """
258
+
259
+ def __init__(self, db: object, session_id: str) -> None:
260
+ # Typed as object to avoid circular imports; runtime type is GdmDatabase
261
+ self._db = db
262
+ self._session_id = session_id
263
+
264
+ def log_tool(
265
+ self,
266
+ tool: str,
267
+ args: dict, # type: ignore[type-arg]
268
+ model: str | None = None,
269
+ decision: str = "allowed",
270
+ ) -> None:
271
+ """Record a tool call.
272
+
273
+ Args:
274
+ tool: tool name (e.g. "write_file", "bash", "web_search")
275
+ args: tool arguments dict (will be JSON-serialised)
276
+ model: model that requested this tool call, if known
277
+ decision: "allowed" | "denied" | "allowed_session"
278
+ """
279
+ try:
280
+ self._db.audit_log_write( # type: ignore[attr-defined]
281
+ session_id=self._session_id,
282
+ tool=tool,
283
+ args=args,
284
+ model=model,
285
+ decision=decision,
286
+ )
287
+ except Exception as exc:
288
+ # Audit failure must never crash the agent — log it and continue
289
+ log.error("AuditLogger: failed to write entry for tool '%s': %s", tool, exc)
290
+
291
+ def log_denied(
292
+ self,
293
+ tool: str,
294
+ args: dict, # type: ignore[type-arg]
295
+ reason: str,
296
+ model: str | None = None,
297
+ ) -> None:
298
+ """Convenience method for denied tool calls."""
299
+ log.warning("Tool '%s' DENIED — %s", tool, reason)
300
+ self.log_tool(tool=tool, args=args, model=model, decision="denied")
301
+
302
+
303
+ # ---------------------------------------------------------------------------
304
+ # Redaction Engine (security-001)
305
+ # ---------------------------------------------------------------------------
306
+
307
+ import json as _json
308
+ from typing import Any as _Any, Annotated as _Annotated
309
+
310
+ _SECRET_PATTERNS_REDACT: list[re.Pattern[str]] = [
311
+ # Provider API key formats
312
+ re.compile(r'\bsk-[A-Za-z0-9\-_]{20,}'), # OpenAI
313
+ re.compile(r'\bxai-[A-Za-z0-9\-_]{20,}'), # Grok/xAI
314
+ re.compile(r'\bghp_[A-Za-z0-9]{36}\b'), # GitHub PAT
315
+ re.compile(r'\bghs_[A-Za-z0-9]{36}\b'), # GitHub Actions token
316
+ re.compile(r'\bBearer\s+[A-Za-z0-9\-_.~+/]{20,}'), # Bearer tokens
317
+ # Environment variable assignments (KEY=value, KEY: value, KEY = "value")
318
+ re.compile(r'(?i)(?:api[_-]?key|token|secret|password|passwd|credential|auth[_-]?token)\s*[=:]\s*["\']?[\w\-\.+/]{8,}["\']?'),
319
+ # PEM/certificate headers
320
+ re.compile(r'-----BEGIN\s+[A-Z ]+-----[\s\S]+?-----END\s+[A-Z ]+-----'),
321
+ # AWS-style access keys
322
+ re.compile(r'\bAKIA[0-9A-Z]{16}\b'),
323
+ ]
324
+
325
+ _SENSITIVE_FILE_PATTERNS: list[re.Pattern[str]] = [
326
+ re.compile(r'(?i)\b\S+\.(pem|key|p12|pfx|jks|crt|cer)\b'),
327
+ ]
328
+
329
+ REDACTED: str = "[REDACTED]"
330
+
331
+
332
+ class RedactionEngine:
333
+ """Pattern-based secret redactor for text and structured data.
334
+
335
+ Used by event log writers, remote transcript, audit export, and evals
336
+ to ensure no raw secrets are stored or transmitted.
337
+
338
+ Usage::
339
+
340
+ engine = RedactionEngine()
341
+ clean = engine.redact("my key is sk-abc123xyz...")
342
+ clean_dict = engine.redact_dict({"args": {"content": "sk-abc..."}})
343
+ """
344
+
345
+ def redact(self, text: str) -> str:
346
+ """Replace secret patterns in text with REDACTED marker."""
347
+ for pattern in _SECRET_PATTERNS_REDACT + _SENSITIVE_FILE_PATTERNS:
348
+ text = pattern.sub(REDACTED, text)
349
+ return text
350
+
351
+ def redact_dict(self, d: dict[str, _Any]) -> dict[str, _Any]:
352
+ """Recursively redact string values in a dict."""
353
+ result: dict[str, _Any] = {}
354
+ for k, v in d.items():
355
+ if isinstance(v, str):
356
+ result[k] = self.redact(v)
357
+ elif isinstance(v, dict):
358
+ result[k] = self.redact_dict(v)
359
+ elif isinstance(v, list):
360
+ result[k] = [
361
+ self.redact(item) if isinstance(item, str) else item
362
+ for item in v
363
+ ]
364
+ else:
365
+ result[k] = v
366
+ return result
367
+
368
+ def redact_json(self, json_str: str) -> str:
369
+ """Redact a JSON string by parsing, redacting, and re-serialising."""
370
+ try:
371
+ obj = _json.loads(json_str)
372
+ if isinstance(obj, dict):
373
+ return _json.dumps(self.redact_dict(obj))
374
+ elif isinstance(obj, str):
375
+ return _json.dumps(self.redact(obj))
376
+ except (_json.JSONDecodeError, TypeError):
377
+ pass
378
+ # Fallback: redact the raw string
379
+ return self.redact(json_str)
380
+
381
+
382
+ # Module-level singleton — import redact/redact_dict directly
383
+ _redaction_engine = RedactionEngine()
384
+
385
+
386
+ def redact(text: str) -> str:
387
+ """Redact secrets from a plain text string. Module-level convenience wrapper."""
388
+ return _redaction_engine.redact(text)
389
+
390
+
391
+ def redact_dict(d: dict[str, _Any]) -> dict[str, _Any]:
392
+ """Redact secrets from a dict recursively. Module-level convenience wrapper."""
393
+ return _redaction_engine.redact_dict(d)
394
+
395
+
396
+ def redact_json(json_str: str) -> str:
397
+ """Redact secrets from a JSON string. Module-level convenience wrapper."""
398
+ return _redaction_engine.redact_json(json_str)
399
+
400
+
401
+ # ---------------------------------------------------------------------------
402
+ # Field safety classifiers (for type annotation use)
403
+ # ---------------------------------------------------------------------------
404
+
405
+ # Annotate dataclass/TypedDict fields with these to document safety level.
406
+ # Any field with InternalOnly must be stripped before remote/export dispatch.
407
+ SafeForRemote = _Annotated[str, "safe_for_remote"] # can stream to phone UI
408
+ SafeForExport = _Annotated[str, "safe_for_export"] # can write to audit/SIEM export
409
+ InternalOnly = _Annotated[str, "internal_only"] # never leave this process
src/server/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Bridge server for browser extension communication."""
2
+ from __future__ import annotations
3
+
4
+ from src.server.bridge import BridgeServer
5
+ from src.server.bridge_client import BridgeClient
6
+
7
+ __all__ = ["BridgeServer", "BridgeClient"]