devtorch-core 3.0.1__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 (193) hide show
  1. devtorch_core/__init__.py +158 -0
  2. devtorch_core/aggphi_textual.py +275 -0
  3. devtorch_core/alerts/__init__.py +23 -0
  4. devtorch_core/alerts/base.py +46 -0
  5. devtorch_core/alerts/config.py +60 -0
  6. devtorch_core/alerts/dispatcher.py +110 -0
  7. devtorch_core/alerts/jira.py +96 -0
  8. devtorch_core/alerts/linear.py +72 -0
  9. devtorch_core/alerts/pagerduty.py +66 -0
  10. devtorch_core/alerts/slack.py +81 -0
  11. devtorch_core/alerts/teams.py +70 -0
  12. devtorch_core/audit/__init__.py +43 -0
  13. devtorch_core/audit/exporter.py +297 -0
  14. devtorch_core/audit/privacy.py +101 -0
  15. devtorch_core/audit/scrubber.py +149 -0
  16. devtorch_core/audit/service.py +67 -0
  17. devtorch_core/audit/signing.py +127 -0
  18. devtorch_core/broadcast/__init__.py +4 -0
  19. devtorch_core/broadcast/broadcaster.py +100 -0
  20. devtorch_core/broadcast/watcher.py +71 -0
  21. devtorch_core/capability.py +639 -0
  22. devtorch_core/cloud/__init__.py +1 -0
  23. devtorch_core/cloud/client_config.py +472 -0
  24. devtorch_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
  25. devtorch_core/cloud/client_configs/.claude-stdio.json +13 -0
  26. devtorch_core/cloud/client_configs/.cursor-mcp.json +13 -0
  27. devtorch_core/cloud/client_configs/.opencode-bridge.json +13 -0
  28. devtorch_core/cloud/client_configs/.opencode.json +15 -0
  29. devtorch_core/cloud/client_configs/.vscode-mcp.json +13 -0
  30. devtorch_core/cloud/devtorch-mcp-bridge.js +357 -0
  31. devtorch_core/cloud/mcp_client.py +229 -0
  32. devtorch_core/cloud/setup.py +144 -0
  33. devtorch_core/cloud/sync.py +143 -0
  34. devtorch_core/cloud/sync_bundle.py +603 -0
  35. devtorch_core/cloud/sync_conflicts.py +159 -0
  36. devtorch_core/cloud/sync_state.py +159 -0
  37. devtorch_core/cloud/team_sync.py +283 -0
  38. devtorch_core/codex/__init__.py +9 -0
  39. devtorch_core/codex/__main__.py +97 -0
  40. devtorch_core/codex/capture.py +208 -0
  41. devtorch_core/codex/proxy.py +412 -0
  42. devtorch_core/concept_catalog.py +209 -0
  43. devtorch_core/consolidation/__init__.py +3 -0
  44. devtorch_core/consolidation/synthesizer.py +87 -0
  45. devtorch_core/consolidation/workflow.py +175 -0
  46. devtorch_core/daemon/__init__.py +27 -0
  47. devtorch_core/daemon/supervisor.py +293 -0
  48. devtorch_core/daemon/watcher.py +244 -0
  49. devtorch_core/dashboard_api.py +2012 -0
  50. devtorch_core/deltaf.py +97 -0
  51. devtorch_core/disclosure.py +50 -0
  52. devtorch_core/divergence/__init__.py +3 -0
  53. devtorch_core/divergence/detector.py +166 -0
  54. devtorch_core/gateway/__init__.py +32 -0
  55. devtorch_core/gateway/key_manager.py +124 -0
  56. devtorch_core/gateway/metrics_webhook.py +252 -0
  57. devtorch_core/gateway/policy.py +262 -0
  58. devtorch_core/gateway/server.py +727 -0
  59. devtorch_core/gateway/sso.py +233 -0
  60. devtorch_core/gcc.py +1246 -0
  61. devtorch_core/github/__init__.py +35 -0
  62. devtorch_core/github/app.py +240 -0
  63. devtorch_core/github/comment_builder.py +113 -0
  64. devtorch_core/github/pat.py +76 -0
  65. devtorch_core/github/pr_parser.py +82 -0
  66. devtorch_core/github/pr_reporter.py +555 -0
  67. devtorch_core/gitlab/__init__.py +177 -0
  68. devtorch_core/hitl/__init__.py +4 -0
  69. devtorch_core/hitl/channels.py +129 -0
  70. devtorch_core/hitl/orchestrator.py +95 -0
  71. devtorch_core/hooks/__init__.py +17 -0
  72. devtorch_core/hooks/claude_code.py +228 -0
  73. devtorch_core/hooks/git_capture.py +341 -0
  74. devtorch_core/hooks/git_commit.py +182 -0
  75. devtorch_core/hooks/installer.py +733 -0
  76. devtorch_core/hooks/pre_commit.py +157 -0
  77. devtorch_core/hooks/runner.py +344 -0
  78. devtorch_core/identity/__init__.py +4 -0
  79. devtorch_core/identity/agent.py +86 -0
  80. devtorch_core/identity/providers.py +85 -0
  81. devtorch_core/invariants.py +182 -0
  82. devtorch_core/mcp/__init__.py +10 -0
  83. devtorch_core/mcp/auth.py +177 -0
  84. devtorch_core/mcp/server.py +1049 -0
  85. devtorch_core/metrics/__init__.py +35 -0
  86. devtorch_core/metrics/aggregate.py +215 -0
  87. devtorch_core/metrics/calibrate.py +198 -0
  88. devtorch_core/metrics/calibration.py +125 -0
  89. devtorch_core/metrics/credibility.py +288 -0
  90. devtorch_core/metrics/delivery_time.py +70 -0
  91. devtorch_core/metrics/dhs.py +126 -0
  92. devtorch_core/metrics/mcs.py +96 -0
  93. devtorch_core/metrics/roi.py +88 -0
  94. devtorch_core/metrics/session_writer.py +81 -0
  95. devtorch_core/metrics/shadow_ai.py +117 -0
  96. devtorch_core/metrics/sprint_writer.py +243 -0
  97. devtorch_core/observability/__init__.py +78 -0
  98. devtorch_core/observability/datadog.py +157 -0
  99. devtorch_core/observability/formatter.py +119 -0
  100. devtorch_core/observability/report.py +264 -0
  101. devtorch_core/observability/servicenow.py +147 -0
  102. devtorch_core/observability/splunk.py +218 -0
  103. devtorch_core/observability/webhook.py +227 -0
  104. devtorch_core/parser/__init__.py +30 -0
  105. devtorch_core/parser/blocks.py +216 -0
  106. devtorch_core/parser/inference.py +159 -0
  107. devtorch_core/parser/thinking.py +112 -0
  108. devtorch_core/projects.py +169 -0
  109. devtorch_core/prompt_artifact.py +76 -0
  110. devtorch_core/proxy/__init__.py +9 -0
  111. devtorch_core/proxy/routes/__init__.py +1 -0
  112. devtorch_core/proxy/routes/anthropic.py +264 -0
  113. devtorch_core/proxy/routes/azure_openai.py +336 -0
  114. devtorch_core/proxy/routes/gemini.py +331 -0
  115. devtorch_core/proxy/routes/groq.py +284 -0
  116. devtorch_core/proxy/routes/ollama.py +279 -0
  117. devtorch_core/proxy/routes/openai.py +287 -0
  118. devtorch_core/proxy/server.py +356 -0
  119. devtorch_core/query/__init__.py +15 -0
  120. devtorch_core/query/grep.py +181 -0
  121. devtorch_core/query/hybrid.py +86 -0
  122. devtorch_core/query/semantic.py +157 -0
  123. devtorch_core/rdp.py +105 -0
  124. devtorch_core/reasoning/__init__.py +4 -0
  125. devtorch_core/reasoning/entry.py +31 -0
  126. devtorch_core/reasoning/store.py +122 -0
  127. devtorch_core/reasoning_plus/__init__.py +70 -0
  128. devtorch_core/reasoning_plus/augmenter.py +326 -0
  129. devtorch_core/reasoning_plus/capture.py +51 -0
  130. devtorch_core/reasoning_plus/config.py +256 -0
  131. devtorch_core/reasoning_plus/context.py +262 -0
  132. devtorch_core/reasoning_plus/learning/__init__.py +72 -0
  133. devtorch_core/reasoning_plus/learning/analytics.py +141 -0
  134. devtorch_core/reasoning_plus/learning/api.py +313 -0
  135. devtorch_core/reasoning_plus/learning/chain.py +285 -0
  136. devtorch_core/reasoning_plus/learning/composer.py +74 -0
  137. devtorch_core/reasoning_plus/learning/cross_project.py +234 -0
  138. devtorch_core/reasoning_plus/learning/embeddings.py +209 -0
  139. devtorch_core/reasoning_plus/learning/extractor.py +207 -0
  140. devtorch_core/reasoning_plus/learning/models.py +116 -0
  141. devtorch_core/reasoning_plus/learning/provenance.py +126 -0
  142. devtorch_core/reasoning_plus/learning/recorder.py +81 -0
  143. devtorch_core/reasoning_plus/learning/relevance.py +122 -0
  144. devtorch_core/reasoning_plus/learning/state.py +86 -0
  145. devtorch_core/reasoning_plus/learning/store.py +160 -0
  146. devtorch_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
  147. devtorch_core/reasoning_plus/prompt.py +90 -0
  148. devtorch_core/rep.py +134 -0
  149. devtorch_core/rep_network/__init__.py +25 -0
  150. devtorch_core/rep_network/merge.py +70 -0
  151. devtorch_core/rep_network/node.py +137 -0
  152. devtorch_core/rep_network/server.py +140 -0
  153. devtorch_core/rep_network/sync.py +207 -0
  154. devtorch_core/sensitivity.py +182 -0
  155. devtorch_core/serve.py +258 -0
  156. devtorch_core/session/__init__.py +39 -0
  157. devtorch_core/session/disagreement.py +188 -0
  158. devtorch_core/session/models.py +114 -0
  159. devtorch_core/session/orchestrator.py +182 -0
  160. devtorch_core/session/planner.py +169 -0
  161. devtorch_core/session/simulator.py +132 -0
  162. devtorch_core/signing.py +290 -0
  163. devtorch_core/sis.py +197 -0
  164. devtorch_core/storage.py +308 -0
  165. devtorch_core/templates/__init__.py +6 -0
  166. devtorch_core/templates/engine.py +122 -0
  167. devtorch_core/templates/go.py +18 -0
  168. devtorch_core/templates/infra.py +19 -0
  169. devtorch_core/templates/library/__init__.py +18 -0
  170. devtorch_core/templates/library/api_design.md +27 -0
  171. devtorch_core/templates/library/bug_fix.md +27 -0
  172. devtorch_core/templates/library/decision_record.md +27 -0
  173. devtorch_core/templates/library/engine.py +228 -0
  174. devtorch_core/templates/library/security_review.md +30 -0
  175. devtorch_core/templates/python.py +19 -0
  176. devtorch_core/templates/react.py +18 -0
  177. devtorch_core/templates/typescript.py +18 -0
  178. devtorch_core/theta.py +221 -0
  179. devtorch_core/theta_synthesis.py +268 -0
  180. devtorch_core/topics.py +320 -0
  181. devtorch_core/variance.py +219 -0
  182. devtorch_core/wrapper/__init__.py +52 -0
  183. devtorch_core/wrapper/anthropic.py +487 -0
  184. devtorch_core/wrapper/base.py +562 -0
  185. devtorch_core/wrapper/bedrock.py +342 -0
  186. devtorch_core/wrapper/gemini.py +422 -0
  187. devtorch_core/wrapper/ollama.py +527 -0
  188. devtorch_core/wrapper/openai.py +461 -0
  189. devtorch_core-3.0.1.dist-info/METADATA +867 -0
  190. devtorch_core-3.0.1.dist-info/RECORD +193 -0
  191. devtorch_core-3.0.1.dist-info/WHEEL +5 -0
  192. devtorch_core-3.0.1.dist-info/entry_points.txt +2 -0
  193. devtorch_core-3.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,297 @@
1
+ """Audit report export engine.
2
+
3
+ Reads ``.GCC/events.log.jsonl``, applies date/project/workspace/user/event-type
4
+ filters, applies privacy policy + scrubbing, and emits JSON or PDF.
5
+
6
+ PDF generation uses ``reportlab`` (optional ``devtorch-core[audit]`` dependency).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import dataclasses
12
+ import datetime
13
+ import json
14
+ from pathlib import Path
15
+ from typing import Any, Dict, List, Optional
16
+
17
+ from .privacy import PrivacyConfig, apply_privacy_policy
18
+ from .scrubber import RegexScrubber
19
+
20
+
21
+ def _parse_iso(value: str | datetime.datetime | None) -> Optional[datetime.datetime]:
22
+ """Parse an ISO timestamp or pass through a datetime object."""
23
+ if value is None or isinstance(value, datetime.datetime):
24
+ return value
25
+ # Try common formats.
26
+ for fmt in ("%Y-%m-%dT%H:%M:%S%z", "%Y-%m-%dT%H:%M:%S.%f%z", "%Y-%m-%d"):
27
+ try:
28
+ return datetime.datetime.strptime(value, fmt)
29
+ except ValueError:
30
+ continue
31
+ # Python 3.10+ isoformat with timezone.
32
+ try:
33
+ return datetime.datetime.fromisoformat(value)
34
+ except ValueError:
35
+ return None
36
+
37
+
38
+ @dataclasses.dataclass
39
+ class AuditFilters:
40
+ """Filters for audit export queries."""
41
+
42
+ since: Optional[datetime.datetime] = None
43
+ until: Optional[datetime.datetime] = None
44
+ project: Optional[str] = None
45
+ workspace: Optional[str] = None
46
+ user: Optional[str] = None
47
+ event_type: Optional[str] = None
48
+ actor: Optional[str] = None
49
+ limit: Optional[int] = None
50
+
51
+ def matches(self, event: Dict[str, Any]) -> bool:
52
+ ts = event.get("timestamp")
53
+ if ts:
54
+ parsed = _parse_iso(ts)
55
+ if parsed:
56
+ if self.since is not None and parsed < self.since:
57
+ return False
58
+ if self.until is not None and parsed > self.until:
59
+ return False
60
+
61
+ if self.event_type and event.get("event_type") != self.event_type:
62
+ return False
63
+ if self.actor and event.get("actor") != self.actor:
64
+ return False
65
+
66
+ payload = event.get("payload", {}) or {}
67
+ if isinstance(payload, dict):
68
+ if self.project and payload.get("project") != self.project:
69
+ return False
70
+ if self.workspace and payload.get("workspace") != self.workspace:
71
+ return False
72
+ if self.user and payload.get("user") != self.user:
73
+ return False
74
+
75
+ return True
76
+
77
+
78
+ class AuditExporter:
79
+ """Export filtered, scrubbed audit events."""
80
+
81
+ def __init__(
82
+ self,
83
+ gcc_dir: Path,
84
+ privacy_config: PrivacyConfig | None = None,
85
+ scrubber: RegexScrubber | None = None,
86
+ ):
87
+ self.gcc_dir = gcc_dir
88
+ self.privacy_config = privacy_config or PrivacyConfig()
89
+ self.scrubber = scrubber or RegexScrubber()
90
+ self.event_log = gcc_dir / "events.log.jsonl"
91
+
92
+ def load_events(self, filters: AuditFilters | None = None) -> List[Dict[str, Any]]:
93
+ """Read and filter events from the event log."""
94
+ filters = filters or AuditFilters()
95
+ events: List[Dict[str, Any]] = []
96
+ if not self.event_log.exists():
97
+ return events
98
+
99
+ with self.event_log.open("r", encoding="utf-8") as f:
100
+ for line in f:
101
+ line = line.strip()
102
+ if not line:
103
+ continue
104
+ try:
105
+ event = json.loads(line)
106
+ except json.JSONDecodeError:
107
+ continue
108
+
109
+ if not filters.matches(event):
110
+ continue
111
+
112
+ event = apply_privacy_policy(event, self.privacy_config, self.scrubber)
113
+ events.append(event)
114
+
115
+ if filters.limit is not None and len(events) >= filters.limit:
116
+ break
117
+
118
+ return events
119
+
120
+ def export_json(
121
+ self,
122
+ events: List[Dict[str, Any]],
123
+ output_path: Path,
124
+ filters: AuditFilters | None = None,
125
+ repo_name: str = "",
126
+ ) -> None:
127
+ """Write events to a JSON file."""
128
+ report = self._build_report(events, filters, repo_name)
129
+ output_path.write_text(json.dumps(report, indent=2), encoding="utf-8")
130
+
131
+ def export_pdf(
132
+ self,
133
+ events: List[Dict[str, Any]],
134
+ output_path: Path,
135
+ filters: AuditFilters | None = None,
136
+ repo_name: str = "",
137
+ ) -> None:
138
+ """Write events to a PDF file using reportlab."""
139
+ try:
140
+ from reportlab.lib import colors
141
+ from reportlab.lib.pagesizes import letter
142
+ from reportlab.platypus import (
143
+ SimpleDocTemplate,
144
+ Paragraph,
145
+ Spacer,
146
+ Table,
147
+ TableStyle,
148
+ )
149
+ from reportlab.lib.styles import getSampleStyleSheet
150
+ from reportlab.lib.units import inch
151
+ except ImportError as exc:
152
+ raise RuntimeError("reportlab is required for PDF export; install devtorch-core[audit]") from exc
153
+
154
+ report = self._build_report(events, filters, repo_name)
155
+ doc = SimpleDocTemplate(str(output_path), pagesize=letter)
156
+ styles = getSampleStyleSheet()
157
+ story: List[Any] = []
158
+
159
+ story.append(Paragraph("DevTorch Audit Report", styles["Title"]))
160
+ story.append(Paragraph(f"Repository: {report['repo_name']}", styles["Normal"]))
161
+ story.append(Paragraph(f"Generated: {report['generated_at']}", styles["Normal"]))
162
+ story.append(Paragraph(f"Event count: {report['event_count']}", styles["Normal"]))
163
+ story.append(Spacer(1, 0.2 * inch))
164
+
165
+ filters = report["filters"]
166
+ filter_text = (
167
+ f"Filters — since: {filters.get('since')}, until: {filters.get('until')}, "
168
+ f"project: {filters.get('project')}, workspace: {filters.get('workspace')}, "
169
+ f"user: {filters.get('user')}, event_type: {filters.get('event_type')}, "
170
+ f"actor: {filters.get('actor')}, limit: {filters.get('limit')}"
171
+ )
172
+ story.append(Paragraph(filter_text, styles["Normal"]))
173
+ story.append(Spacer(1, 0.2 * inch))
174
+
175
+ if events:
176
+ headers = ["Timestamp", "Actor", "Type", "Hash", "Payload"]
177
+ data = [headers]
178
+ for event in events:
179
+ payload = event.get("payload", "")
180
+ if isinstance(payload, dict):
181
+ payload_text = json.dumps(payload, indent=2)
182
+ else:
183
+ payload_text = str(payload)
184
+ data.append(
185
+ [
186
+ Paragraph(str(event.get("timestamp", "")), styles["Normal"]),
187
+ Paragraph(str(event.get("actor", "")), styles["Normal"]),
188
+ Paragraph(str(event.get("event_type", "")), styles["Normal"]),
189
+ Paragraph(str(event.get("hash", "")[:16]), styles["Normal"]),
190
+ Paragraph(payload_text, styles["Normal"]),
191
+ ]
192
+ )
193
+
194
+ table = Table(data, colWidths=[1.1 * inch, 0.9 * inch, 0.9 * inch, 1.0 * inch, 3.2 * inch])
195
+ table.setStyle(
196
+ TableStyle(
197
+ [
198
+ ("BACKGROUND", (0, 0), (-1, 0), colors.grey),
199
+ ("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke),
200
+ ("ALIGN", (0, 0), (-1, -1), "LEFT"),
201
+ ("VALIGN", (0, 0), (-1, -1), "TOP"),
202
+ ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
203
+ ("FONTSIZE", (0, 0), (-1, 0), 10),
204
+ ("BOTTOMPADDING", (0, 0), (-1, 0), 12),
205
+ ("BACKGROUND", (0, 1), (-1, -1), colors.beige),
206
+ ("GRID", (0, 0), (-1, -1), 1, colors.black),
207
+ ]
208
+ )
209
+ )
210
+ story.append(table)
211
+ else:
212
+ story.append(Paragraph("No events matched the selected filters.", styles["Normal"]))
213
+
214
+ doc.build(story, onFirstPage=self._footer, onLaterPages=self._footer)
215
+
216
+ def _build_report(
217
+ self,
218
+ events: List[Dict[str, Any]],
219
+ filters: AuditFilters | None,
220
+ repo_name: str,
221
+ ) -> Dict[str, Any]:
222
+ return {
223
+ "repo_name": repo_name or str(self.gcc_dir.parent),
224
+ "generated_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
225
+ "event_count": len(events),
226
+ "filters": self._filters_dict(filters),
227
+ "events": events,
228
+ }
229
+
230
+ def _filters_dict(self, filters: AuditFilters | None) -> Dict[str, Any]:
231
+ if filters is None:
232
+ return {
233
+ "since": None,
234
+ "until": None,
235
+ "project": None,
236
+ "workspace": None,
237
+ "user": None,
238
+ "event_type": None,
239
+ "actor": None,
240
+ "limit": None,
241
+ }
242
+
243
+ def fmt(value):
244
+ if value is None:
245
+ return None
246
+ if isinstance(value, datetime.datetime):
247
+ return value.isoformat()
248
+ return str(value)
249
+
250
+ return {
251
+ "since": fmt(filters.since),
252
+ "until": fmt(filters.until),
253
+ "project": filters.project,
254
+ "workspace": filters.workspace,
255
+ "user": filters.user,
256
+ "event_type": filters.event_type,
257
+ "actor": filters.actor,
258
+ "limit": filters.limit,
259
+ }
260
+
261
+ def _footer(self, canvas, doc) -> None:
262
+ """Draw page number and timestamp in the footer."""
263
+ from reportlab.lib.units import inch
264
+ canvas.saveState()
265
+ canvas.setFont("Helvetica", 8)
266
+ footer_text = (
267
+ f"Page {doc.page} • DevTorch Audit Report • "
268
+ f"{datetime.datetime.now(datetime.timezone.utc).isoformat()}"
269
+ )
270
+ canvas.drawString(0.75 * inch, 0.5 * inch, footer_text)
271
+ canvas.restoreState()
272
+
273
+
274
+ # Default convenience wrapper.
275
+
276
+ def export_audit(
277
+ gcc_dir: Path,
278
+ output_path: Path,
279
+ fmt: str = "json",
280
+ filters: AuditFilters | None = None,
281
+ privacy_config: PrivacyConfig | None = None,
282
+ repo_name: str = "",
283
+ ) -> Dict[str, Any]:
284
+ """One-shot export helper used by the CLI."""
285
+ exporter = AuditExporter(gcc_dir, privacy_config=privacy_config)
286
+ events = exporter.load_events(filters)
287
+ if fmt == "json":
288
+ exporter.export_json(events, output_path, filters=filters, repo_name=repo_name)
289
+ elif fmt == "pdf":
290
+ exporter.export_pdf(events, output_path, filters=filters, repo_name=repo_name)
291
+ else:
292
+ raise ValueError(f"Unsupported export format: {fmt}")
293
+ return {
294
+ "format": fmt,
295
+ "output_path": str(output_path),
296
+ "event_count": len(events),
297
+ }
@@ -0,0 +1,101 @@
1
+ """Privacy policy configuration and enforcement.
2
+
3
+ The configuration lives in ``.GCC/privacy.json`` and is versioned with the
4
+ repository. It controls whether audit logs, prompts, and telemetry may leave the
5
+ local environment, and whether redaction is applied before export.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import dataclasses
11
+ import json
12
+ from pathlib import Path
13
+ from typing import Any, Dict
14
+
15
+ from .scrubber import RegexScrubber, Scrubber
16
+
17
+
18
+ PRIVACY_FILE_NAME = "privacy.json"
19
+
20
+ PROMPT_EVENT_TYPES = {"PROMPT", "THINKING", "REASONING", "CHAT", "LLM_REQUEST", "PROMPT_ARTIFACT"}
21
+
22
+
23
+ @dataclasses.dataclass
24
+ class PrivacyConfig:
25
+ """Privacy policy knobs for a DevTorch repository."""
26
+
27
+ telemetry_enabled: bool = True
28
+ prompt_telemetry_enabled: bool = True
29
+ redaction_enabled: bool = True
30
+ allow_remote_endpoints: bool = False
31
+ retention_days: int = 365
32
+
33
+ def to_dict(self) -> Dict[str, Any]:
34
+ return dataclasses.asdict(self)
35
+
36
+ @classmethod
37
+ def from_dict(cls, data: Dict[str, Any]) -> "PrivacyConfig":
38
+ return cls(
39
+ telemetry_enabled=bool(data.get("telemetry_enabled", True)),
40
+ prompt_telemetry_enabled=bool(data.get("prompt_telemetry_enabled", True)),
41
+ redaction_enabled=bool(data.get("redaction_enabled", True)),
42
+ allow_remote_endpoints=bool(data.get("allow_remote_endpoints", False)),
43
+ retention_days=int(data.get("retention_days", 365)),
44
+ )
45
+
46
+
47
+ def load_privacy_config(gcc_dir: Path) -> PrivacyConfig:
48
+ """Load privacy config from ``.GCC/privacy.json`` or return defaults."""
49
+ path = gcc_dir / PRIVACY_FILE_NAME
50
+ if not path.exists():
51
+ return PrivacyConfig()
52
+ try:
53
+ data = json.loads(path.read_text(encoding="utf-8"))
54
+ except (json.JSONDecodeError, OSError):
55
+ return PrivacyConfig()
56
+ return PrivacyConfig.from_dict(data)
57
+
58
+
59
+ def save_privacy_config(gcc_dir: Path, config: PrivacyConfig) -> None:
60
+ """Persist privacy config to ``.GCC/privacy.json``."""
61
+ path = gcc_dir / PRIVACY_FILE_NAME
62
+ path.write_text(json.dumps(config.to_dict(), indent=2), encoding="utf-8")
63
+
64
+
65
+ def _is_prompt_event(event: Dict[str, Any]) -> bool:
66
+ """Heuristic: event likely contains prompt text."""
67
+ event_type = event.get("event_type", "")
68
+ if isinstance(event_type, str) and event_type.upper() in PROMPT_EVENT_TYPES:
69
+ return True
70
+ payload = event.get("payload", {})
71
+ if isinstance(payload, dict):
72
+ keys = {k.lower() for k in payload.keys()}
73
+ if keys & {"prompt", "system_prompt", "messages", "prompt_text"}:
74
+ return True
75
+ return False
76
+
77
+
78
+ def apply_privacy_policy(
79
+ event: Dict[str, Any],
80
+ config: PrivacyConfig,
81
+ scrubber: Scrubber | None = None,
82
+ ) -> Dict[str, Any]:
83
+ """Apply privacy policy to a single event.
84
+
85
+ * Redaction is applied when ``redaction_enabled`` is true.
86
+ * Prompt telemetry is suppressed when ``prompt_telemetry_enabled`` is false.
87
+ * Non-prompt telemetry is suppressed when ``telemetry_enabled`` is false.
88
+ * Suppressed events are returned with a ``_telemetry_excluded`` flag and the
89
+ payload replaced by ``{ "excluded": true }``.
90
+ """
91
+ scrubber = scrubber or RegexScrubber()
92
+ if not config.telemetry_enabled:
93
+ return {**event, "_telemetry_excluded": True, "payload": {"excluded": True}}
94
+
95
+ if not config.prompt_telemetry_enabled and _is_prompt_event(event):
96
+ return {**event, "_telemetry_excluded": True, "payload": {"excluded": True}}
97
+
98
+ if config.redaction_enabled:
99
+ return scrubber.scrub(event)
100
+
101
+ return event
@@ -0,0 +1,149 @@
1
+ """PII and secret scrubbing for audit exports.
2
+
3
+ Regex-based scrubber is the default. ``presidio-analyzer`` is supported as an
4
+ opt-in backend because pulling a full spaCy model into the OSS core is
5
+ prohibitively heavy.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from typing import Any, Dict, List, Pattern, Tuple
12
+
13
+
14
+ DEFAULT_REDACTION = "[REDACTED]"
15
+
16
+
17
+ # Patterns are tuples of (label, compiled_regex, replacement_template).
18
+ # The replacement template may reference groups if the regex contains groups.
19
+ DEFAULT_PATTERNS: List[Tuple[str, Pattern[str], str]] = [
20
+ (
21
+ "email",
22
+ re.compile(r"(?i)\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b"),
23
+ "[REDACTED_EMAIL]",
24
+ ),
25
+ (
26
+ "phone",
27
+ re.compile(
28
+ r"\b(?:\+\d{1,2}\s?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b"
29
+ ),
30
+ "[REDACTED_PHONE]",
31
+ ),
32
+ (
33
+ "ssn",
34
+ re.compile(r"\b\d{3}-\d{2}-\d{4}\b"),
35
+ "[REDACTED_SSN]",
36
+ ),
37
+ (
38
+ "credit_card",
39
+ re.compile(r"\b(?:\d[ -]*?){13,16}\b"),
40
+ "[REDACTED_CC]",
41
+ ),
42
+ (
43
+ "openai_key",
44
+ re.compile(r"\b(sk-[a-zA-Z0-9]{20,})\b"),
45
+ "[REDACTED_API_KEY]",
46
+ ),
47
+ (
48
+ "aws_key",
49
+ re.compile(r"\bAKIA[0-9A-Z]{16}\b"),
50
+ "[REDACTED_AWS_KEY]",
51
+ ),
52
+ (
53
+ "github_token",
54
+ re.compile(
55
+ r"\b(ghp_[a-zA-Z0-9]{36}|github_pat_[a-zA-Z0-9_]{22,}|gho_[a-zA-Z0-9]{36})\b"
56
+ ),
57
+ "[REDACTED_GITHUB_TOKEN]",
58
+ ),
59
+ (
60
+ "bearer_token",
61
+ re.compile(r"\bBearer\s+[a-zA-Z0-9_\-\.]+\b", flags=re.IGNORECASE),
62
+ "[REDACTED_BEARER_TOKEN]",
63
+ ),
64
+ (
65
+ "private_key",
66
+ re.compile(
67
+ r"-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----",
68
+ flags=re.DOTALL | re.IGNORECASE,
69
+ ),
70
+ "[REDACTED_PRIVATE_KEY]",
71
+ ),
72
+ (
73
+ "generic_secret",
74
+ re.compile(
75
+ r"(?i)(api[_-]?key|secret|password|token)\s*[:=]\s*['\"]?([a-zA-Z0-9_\-/+=]{8,})['\"]?"
76
+ ),
77
+ r"\1=[REDACTED_SECRET]",
78
+ ),
79
+ ]
80
+
81
+
82
+ class Scrubber:
83
+ """Abstract scrubber interface."""
84
+
85
+ def scrub(self, obj: Any) -> Any:
86
+ raise NotImplementedError
87
+
88
+
89
+ class RegexScrubber(Scrubber):
90
+ """Redact PII and secrets using regex patterns."""
91
+
92
+ def __init__(self, patterns: List[Tuple[str, Pattern[str], str]] | None = None):
93
+ self.patterns = patterns or DEFAULT_PATTERNS
94
+
95
+ def scrub(self, obj: Any) -> Any:
96
+ if isinstance(obj, str):
97
+ return self._scrub_str(obj)
98
+ if isinstance(obj, dict):
99
+ return {k: self.scrub(v) for k, v in obj.items()}
100
+ if isinstance(obj, list):
101
+ return [self.scrub(item) for item in obj]
102
+ return obj
103
+
104
+ def _scrub_str(self, text: str) -> str:
105
+ for _label, pattern, replacement in self.patterns:
106
+ text = pattern.sub(replacement, text)
107
+ return text
108
+
109
+
110
+ class PresidioScrubber(Scrubber):
111
+ """Lightweight wrapper around Microsoft Presidio analyzer.
112
+
113
+ Falls back to ``RegexScrubber`` for any entity type that Presidio does not
114
+ support (e.g., API keys, bearer tokens).
115
+ """
116
+
117
+ def __init__(self, additional_patterns: List[Tuple[str, Pattern[str], str]] | None = None):
118
+ from presidio_analyzer import AnalyzerEngine
119
+ self._analyzer = AnalyzerEngine()
120
+ self._fallback = RegexScrubber(additional_patterns or DEFAULT_PATTERNS)
121
+
122
+ def scrub(self, obj: Any) -> Any:
123
+ if not isinstance(obj, str):
124
+ return self._fallback.scrub(obj)
125
+ return self._scrub_str(obj)
126
+
127
+ def _scrub_str(self, text: str) -> str:
128
+ results = self._analyzer.analyze(text=text, language="en")
129
+ # Redact from end to start so indices stay stable.
130
+ redacted = text
131
+ for result in sorted(results, key=lambda r: r.start, reverse=True):
132
+ redacted = redacted[: result.start] + "[REDACTED]" + redacted[result.end :]
133
+ return self._fallback._scrub_str(redacted)
134
+
135
+
136
+ def make_scrubber(use_presidio: bool = False) -> Scrubber:
137
+ """Factory: returns a regex scrubber unless ``use_presidio=True``."""
138
+ if use_presidio:
139
+ try:
140
+ return PresidioScrubber()
141
+ except Exception:
142
+ return RegexScrubber()
143
+ return RegexScrubber()
144
+
145
+
146
+ def scrub_dict(obj: Any, scrubber: Scrubber | None = None) -> Any:
147
+ """Convenience helper for one-off scrubbing."""
148
+ scrubber = scrubber or RegexScrubber()
149
+ return scrubber.scrub(obj)
@@ -0,0 +1,67 @@
1
+ """High-level audit service used by the CLI and MCP server.
2
+
3
+ Keeps the orchestration logic in one place so both command-line users and
4
+ programmatic callers get the same behaviour.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from typing import Any, Dict, List
11
+
12
+ from .exporter import AuditExporter, AuditFilters, export_audit
13
+ from .privacy import PrivacyConfig, apply_privacy_policy, load_privacy_config
14
+ from .scrubber import RegexScrubber
15
+ from .signing import ExportSigner
16
+
17
+
18
+ class AuditService:
19
+ """Facade for audit export, scrubbing, and signing operations."""
20
+
21
+ def __init__(self, gcc_dir: Path):
22
+ self.gcc_dir = gcc_dir
23
+ self.exporter = AuditExporter(gcc_dir)
24
+ self.signer = ExportSigner()
25
+
26
+ def get_privacy_config(self) -> PrivacyConfig:
27
+ return load_privacy_config(self.gcc_dir)
28
+
29
+ def query_events(self, filters: AuditFilters | None = None) -> List[Dict[str, Any]]:
30
+ """Return filtered, policy-applied events."""
31
+ privacy = self.get_privacy_config()
32
+ return self.exporter.load_events(filters)
33
+
34
+ def export(
35
+ self,
36
+ output_path: Path,
37
+ fmt: str = "json",
38
+ filters: AuditFilters | None = None,
39
+ repo_name: str = "",
40
+ privacy_config: PrivacyConfig | None = None,
41
+ ) -> Dict[str, Any]:
42
+ """Export events to JSON or PDF."""
43
+ return export_audit(
44
+ self.gcc_dir,
45
+ output_path,
46
+ fmt=fmt,
47
+ filters=filters,
48
+ repo_name=repo_name,
49
+ privacy_config=privacy_config,
50
+ )
51
+
52
+ def sign_export(self, input_path: Path, output_path: Path, private_key_path: Path) -> None:
53
+ """Sign an exported JSON file and write the signed envelope."""
54
+ self.signer.sign_file(input_path, output_path, private_key_path)
55
+
56
+ def verify_export(self, envelope_path: Path, public_key_path: Path) -> bool:
57
+ """Verify a signed export envelope."""
58
+ return self.signer.verify_file(envelope_path, public_key_path)
59
+
60
+ def scrub(self, obj: Any) -> Any:
61
+ """Run the default regex scrubber on an arbitrary object."""
62
+ return RegexScrubber().scrub(obj)
63
+
64
+ def apply_privacy(self, event: Dict[str, Any], config: PrivacyConfig | None = None) -> Dict[str, Any]:
65
+ """Apply the repository privacy policy (or a supplied config) to an event."""
66
+ config = config or self.get_privacy_config()
67
+ return apply_privacy_policy(event, config, self.exporter.scrubber)