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,218 @@
1
+ """
2
+ Splunk SIEM integration for DevTorch governance events.
3
+
4
+ Streams `.GCC/events.log.jsonl` to the Splunk HTTP Event Collector (HEC)
5
+ endpoint `/services/collector/event` in batches.
6
+
7
+ NEVER includes: prompts, code, reasoning tokens, commit message bodies,
8
+ sensitivity signal text, or any LLM response content.
9
+
10
+ Events containing forbidden keys are dropped before sending.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import dataclasses
16
+ import json
17
+ import os
18
+ import urllib.request
19
+ import urllib.error
20
+ from pathlib import Path
21
+ from typing import Any, Dict, Iterator, List, Optional
22
+
23
+ from .webhook import WebhookResult
24
+
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # Forbidden content keys (privacy guard)
28
+ # ---------------------------------------------------------------------------
29
+
30
+ _FORBIDDEN_KEYS = frozenset({
31
+ "prompt",
32
+ "content",
33
+ "messages",
34
+ "reasoning",
35
+ "text",
36
+ "code",
37
+ })
38
+
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # Dataclasses
42
+ # ---------------------------------------------------------------------------
43
+
44
+ @dataclasses.dataclass
45
+ class SplunkConfig:
46
+ """Configuration for Splunk HEC ingestion."""
47
+ hec_url: str
48
+ hec_token: str
49
+ source: str = "devtorch"
50
+ sourcetype: str = "_json"
51
+ timeout_seconds: int = 5
52
+ batch_size: int = 100
53
+ enabled: bool = True
54
+
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # Config loader
58
+ # ---------------------------------------------------------------------------
59
+
60
+ def load_splunk_config() -> Optional[SplunkConfig]:
61
+ """
62
+ Load Splunk configuration from environment variables.
63
+
64
+ Environment variables:
65
+ - DEVTORCH_SPLUNK_HEC_URL (required)
66
+ - DEVTORCH_SPLUNK_HEC_TOKEN (required)
67
+ - DEVTORCH_SPLUNK_SOURCE
68
+
69
+ Returns None if required variables are missing.
70
+ Never raises.
71
+ """
72
+ try:
73
+ hec_url = os.environ.get("DEVTORCH_SPLUNK_HEC_URL", "").strip()
74
+ hec_token = os.environ.get("DEVTORCH_SPLUNK_HEC_TOKEN", "").strip()
75
+ if not hec_url or not hec_token:
76
+ return None
77
+ source = os.environ.get("DEVTORCH_SPLUNK_SOURCE", "devtorch").strip() or "devtorch"
78
+ return SplunkConfig(
79
+ hec_url=hec_url,
80
+ hec_token=hec_token,
81
+ source=source,
82
+ )
83
+ except Exception:
84
+ return None
85
+
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # Event streaming
89
+ # ---------------------------------------------------------------------------
90
+
91
+ def stream_events_log(gcc_dir: Path) -> Iterator[Dict[str, Any]]:
92
+ """
93
+ Yield parsed JSON objects from `.GCC/events.log.jsonl`.
94
+
95
+ Skips malformed lines silently. Never raises.
96
+ """
97
+ event_log = gcc_dir / "events.log.jsonl"
98
+ if not event_log.exists():
99
+ return
100
+
101
+ try:
102
+ with event_log.open("r", encoding="utf-8") as f:
103
+ for line in f:
104
+ line = line.strip()
105
+ if not line:
106
+ continue
107
+ try:
108
+ yield json.loads(line)
109
+ except json.JSONDecodeError:
110
+ continue
111
+ except Exception:
112
+ return
113
+
114
+
115
+ # ---------------------------------------------------------------------------
116
+ # Privacy guard
117
+ # ---------------------------------------------------------------------------
118
+
119
+ def _event_contains_forbidden_keys(event: Dict[str, Any]) -> bool:
120
+ """Return True if the event payload contains any forbidden content keys."""
121
+ return not _FORBIDDEN_KEYS.isdisjoint(event.keys())
122
+
123
+
124
+ # ---------------------------------------------------------------------------
125
+ # Sender
126
+ # ---------------------------------------------------------------------------
127
+
128
+ def send_to_splunk(gcc_dir: Path, config: SplunkConfig) -> WebhookResult:
129
+ """
130
+ Send governance events from `.GCC/events.log.jsonl` to Splunk HEC.
131
+
132
+ Events are sent in batches of up to config.batch_size. Events containing
133
+ forbidden keys (`prompt`, `content`, `messages`, `reasoning`, `text`, `code`)
134
+ are skipped.
135
+
136
+ Uses urllib.request — no external dependencies.
137
+ Never raises — returns WebhookResult with success=False on any error.
138
+
139
+ Headers sent:
140
+ Authorization: Splunk {hec_token}
141
+ Content-Type: application/json
142
+ """
143
+ if not config.enabled:
144
+ return WebhookResult(success=False, status_code=0, error="Splunk export is disabled")
145
+
146
+ try:
147
+ url = f"{config.hec_url.rstrip('/')}/services/collector/event"
148
+ headers = {
149
+ "Authorization": f"Splunk {config.hec_token}",
150
+ "Content-Type": "application/json",
151
+ }
152
+
153
+ batch: List[Dict[str, Any]] = []
154
+ total_sent = 0
155
+ last_status = 200
156
+ last_error = ""
157
+
158
+ for event in stream_events_log(gcc_dir):
159
+ if _event_contains_forbidden_keys(event):
160
+ continue
161
+
162
+ # Wrap each event in Splunk HEC envelope.
163
+ wrapped = {
164
+ "source": config.source,
165
+ "sourcetype": config.sourcetype,
166
+ "event": event,
167
+ }
168
+ batch.append(wrapped)
169
+
170
+ if len(batch) >= config.batch_size:
171
+ result = _send_batch(url, headers, batch, config.timeout_seconds)
172
+ if result.success:
173
+ total_sent += len(batch)
174
+ else:
175
+ last_status = result.status_code
176
+ last_error = result.error
177
+ batch = []
178
+
179
+ if batch:
180
+ result = _send_batch(url, headers, batch, config.timeout_seconds)
181
+ if result.success:
182
+ total_sent += len(batch)
183
+ else:
184
+ last_status = result.status_code
185
+ last_error = result.error
186
+
187
+ if last_error:
188
+ return WebhookResult(success=False, status_code=last_status, error=last_error)
189
+ return WebhookResult(success=True, status_code=last_status, error="")
190
+
191
+ except Exception as exc:
192
+ return WebhookResult(success=False, status_code=0, error=str(exc))
193
+
194
+
195
+ def _send_batch(url: str, headers: Dict[str, str], batch: List[Dict[str, Any]], timeout: int) -> WebhookResult:
196
+ """POST a batch of Splunk HEC events."""
197
+ try:
198
+ # Splunk HEC accepts newline-separated JSON payloads.
199
+ body_lines = [json.dumps(item) for item in batch]
200
+ body_bytes = "\n".join(body_lines).encode("utf-8")
201
+
202
+ req = urllib.request.Request(
203
+ url=url,
204
+ data=body_bytes,
205
+ method="POST",
206
+ headers=headers,
207
+ )
208
+
209
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
210
+ status_code = resp.status
211
+ return WebhookResult(success=True, status_code=status_code, error="")
212
+
213
+ except urllib.error.HTTPError as exc:
214
+ return WebhookResult(success=False, status_code=exc.code, error=str(exc))
215
+ except urllib.error.URLError as exc:
216
+ return WebhookResult(success=False, status_code=0, error=str(exc))
217
+ except Exception as exc:
218
+ return WebhookResult(success=False, status_code=0, error=str(exc))
@@ -0,0 +1,227 @@
1
+ """
2
+ Enterprise metrics webhook sender.
3
+
4
+ Sends a METRICS-ONLY payload to a configured endpoint.
5
+ NEVER includes: prompts, code, reasoning tokens, commit message bodies,
6
+ sensitivity signal text, or any LLM response content.
7
+
8
+ Only sends: counts, scores, timestamps, branch names, node state.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import dataclasses
14
+ import json
15
+ import os
16
+ import urllib.request
17
+ import urllib.error
18
+ from typing import Optional
19
+
20
+ from .report import ObservabilityReport
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Approved metric keys sent in the webhook payload (explicit allowlist)
25
+ # ---------------------------------------------------------------------------
26
+
27
+ _PAYLOAD_KEYS = frozenset({
28
+ "org_id",
29
+ "node_state",
30
+ "branch",
31
+ "commit_count",
32
+ "sensitivity_count",
33
+ "high_disclosure_count",
34
+ "dhs_score",
35
+ "mcs_score",
36
+ "rdp_epsilon_used",
37
+ "rdp_exhausted",
38
+ "variance_alert_count",
39
+ "generated_at",
40
+ })
41
+
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # Dataclasses
45
+ # ---------------------------------------------------------------------------
46
+
47
+ @dataclasses.dataclass
48
+ class WebhookConfig:
49
+ """Configuration for the enterprise metrics webhook."""
50
+ endpoint_url: str # DEVTORCH_METRICS_WEBHOOK_URL
51
+ org_id: str # DEVTORCH_ORG_ID
52
+ api_key: str # DEVTORCH_METRICS_API_KEY
53
+ timeout_seconds: int = 10
54
+
55
+
56
+ @dataclasses.dataclass
57
+ class WebhookPayload:
58
+ """
59
+ Metrics-only payload sent to the webhook endpoint.
60
+
61
+ Explicit field list — every field is named here, nothing else.
62
+ Contains: counts, scores, timestamps, branch name, node state only.
63
+ """
64
+ org_id: str
65
+ node_state: str
66
+ branch: str
67
+ commit_count: int
68
+ sensitivity_count: int
69
+ high_disclosure_count: int
70
+ dhs_score: Optional[float]
71
+ mcs_score: Optional[float]
72
+ rdp_epsilon_used: float
73
+ rdp_exhausted: bool
74
+ variance_alert_count: int
75
+ generated_at: str
76
+
77
+
78
+ @dataclasses.dataclass
79
+ class WebhookResult:
80
+ """Result of a webhook POST attempt."""
81
+ success: bool
82
+ status_code: int
83
+ error: str
84
+
85
+
86
+ # ---------------------------------------------------------------------------
87
+ # Config loader
88
+ # ---------------------------------------------------------------------------
89
+
90
+ def load_webhook_config() -> Optional[WebhookConfig]:
91
+ """
92
+ Load webhook configuration from environment variables.
93
+
94
+ Returns None if DEVTORCH_METRICS_WEBHOOK_URL is not set.
95
+ Never raises.
96
+ """
97
+ try:
98
+ endpoint_url = os.environ.get("DEVTORCH_METRICS_WEBHOOK_URL", "").strip()
99
+ if not endpoint_url:
100
+ return None
101
+ org_id = os.environ.get("DEVTORCH_ORG_ID", "").strip()
102
+ api_key = os.environ.get("DEVTORCH_METRICS_API_KEY", "").strip()
103
+ timeout_str = os.environ.get("DEVTORCH_METRICS_TIMEOUT_SECONDS", "10").strip()
104
+ try:
105
+ timeout_seconds = int(timeout_str)
106
+ except (ValueError, TypeError):
107
+ timeout_seconds = 10
108
+ return WebhookConfig(
109
+ endpoint_url=endpoint_url,
110
+ org_id=org_id,
111
+ api_key=api_key,
112
+ timeout_seconds=timeout_seconds,
113
+ )
114
+ except Exception:
115
+ return None
116
+
117
+
118
+ # ---------------------------------------------------------------------------
119
+ # Payload builder
120
+ # ---------------------------------------------------------------------------
121
+
122
+ def build_webhook_payload(report: ObservabilityReport, org_id: str) -> WebhookPayload:
123
+ """
124
+ Map ObservabilityReport fields to a WebhookPayload.
125
+
126
+ Explicit field mapping: every included field is named here, nothing else.
127
+ Intentionally omits: prompts, commit messages, sensitivity signal text,
128
+ theta concept names beyond counts, code, or any LLM output.
129
+ """
130
+ rdp = report.rdp_budget
131
+ rdp_epsilon_used = float(rdp.get("epsilon_used", 0.0))
132
+ rdp_epsilon_max = float(rdp.get("epsilon_max", 1.0))
133
+ rdp_exhausted = bool(rdp.get("read_only", False))
134
+
135
+ variance = report.variance_state
136
+ variance_alert_count = int(variance.get("alert_count", 0))
137
+
138
+ return WebhookPayload(
139
+ org_id=org_id,
140
+ node_state=report.node_state,
141
+ branch=report.branch,
142
+ commit_count=report.commit_count,
143
+ sensitivity_count=report.sensitivity_event_count,
144
+ high_disclosure_count=report.high_disclosure_count,
145
+ dhs_score=None, # not computed here; callers may populate via separate DHS call
146
+ mcs_score=None, # not computed here; callers may populate via separate MCS call
147
+ rdp_epsilon_used=rdp_epsilon_used,
148
+ rdp_exhausted=rdp_exhausted,
149
+ variance_alert_count=variance_alert_count,
150
+ generated_at=report.generated_at,
151
+ )
152
+
153
+
154
+ # ---------------------------------------------------------------------------
155
+ # Webhook sender
156
+ # ---------------------------------------------------------------------------
157
+
158
+ def send_webhook(payload: WebhookPayload, config: WebhookConfig) -> WebhookResult:
159
+ """
160
+ POST the metrics payload to config.endpoint_url.
161
+
162
+ Uses urllib.request — no external dependencies.
163
+ Never raises — returns WebhookResult with success=False on any error.
164
+
165
+ Headers sent:
166
+ Authorization: Bearer {api_key}
167
+ Content-Type: application/json
168
+ X-DevTorch-Org: {org_id}
169
+ """
170
+ try:
171
+ body_dict = {
172
+ "org_id": payload.org_id,
173
+ "node_state": payload.node_state,
174
+ "branch": payload.branch,
175
+ "commit_count": payload.commit_count,
176
+ "sensitivity_count": payload.sensitivity_count,
177
+ "high_disclosure_count": payload.high_disclosure_count,
178
+ "dhs_score": payload.dhs_score,
179
+ "mcs_score": payload.mcs_score,
180
+ "rdp_epsilon_used": payload.rdp_epsilon_used,
181
+ "rdp_exhausted": payload.rdp_exhausted,
182
+ "variance_alert_count": payload.variance_alert_count,
183
+ "generated_at": payload.generated_at,
184
+ }
185
+ body_bytes = json.dumps(body_dict).encode("utf-8")
186
+
187
+ req = urllib.request.Request(
188
+ url=config.endpoint_url,
189
+ data=body_bytes,
190
+ method="POST",
191
+ headers={
192
+ "Authorization": f"Bearer {config.api_key}",
193
+ "Content-Type": "application/json",
194
+ "X-DevTorch-Org": config.org_id,
195
+ },
196
+ )
197
+
198
+ with urllib.request.urlopen(req, timeout=config.timeout_seconds) as resp:
199
+ status_code = resp.status
200
+ return WebhookResult(success=True, status_code=status_code, error="")
201
+
202
+ except urllib.error.HTTPError as exc:
203
+ return WebhookResult(success=False, status_code=exc.code, error=str(exc))
204
+ except urllib.error.URLError as exc:
205
+ return WebhookResult(success=False, status_code=0, error=str(exc))
206
+ except Exception as exc:
207
+ return WebhookResult(success=False, status_code=0, error=str(exc))
208
+
209
+
210
+ # ---------------------------------------------------------------------------
211
+ # Convenience helper
212
+ # ---------------------------------------------------------------------------
213
+
214
+ def send_metrics_if_configured(report: ObservabilityReport) -> Optional[WebhookResult]:
215
+ """
216
+ Load config from environment; if not configured return None.
217
+ Otherwise build payload, send webhook, and return WebhookResult.
218
+ Never raises.
219
+ """
220
+ try:
221
+ config = load_webhook_config()
222
+ if config is None:
223
+ return None
224
+ payload = build_webhook_payload(report, org_id=config.org_id)
225
+ return send_webhook(payload, config)
226
+ except Exception:
227
+ return None
@@ -0,0 +1,30 @@
1
+ """
2
+ devtorch_core.parser
3
+ ====================
4
+ Shared parser foundation for the S7 LLM Wrapper layer.
5
+
6
+ Extracts structured data from LLM responses:
7
+
8
+ * **blocks** — RACP XML blocks (``<devtorch:commit>``, ``<devtorch:sensitivity>``)
9
+ * **thinking** — extended-thinking / reasoning content blocks
10
+ * **inference** — heuristic fallback when no explicit RACP blocks were emitted
11
+ """
12
+ from .blocks import extract_racp_blocks, strip_racp_blocks, CommitBlock, SensitivityBlock
13
+ from .thinking import extract_thinking_blocks, thinking_to_commit_message, ThinkingBlock
14
+ from .inference import infer_sensitivity, extract_decision_summary, InferredSensitivity
15
+
16
+ __all__ = [
17
+ # blocks
18
+ "extract_racp_blocks",
19
+ "strip_racp_blocks",
20
+ "CommitBlock",
21
+ "SensitivityBlock",
22
+ # thinking
23
+ "extract_thinking_blocks",
24
+ "thinking_to_commit_message",
25
+ "ThinkingBlock",
26
+ # inference
27
+ "infer_sensitivity",
28
+ "extract_decision_summary",
29
+ "InferredSensitivity",
30
+ ]
@@ -0,0 +1,216 @@
1
+ """
2
+ devtorch_core.parser.blocks
3
+ ===========================
4
+ Extract and strip RACP-structured XML blocks emitted by LLMs that have been
5
+ instructed with the DevTorch system prompt.
6
+
7
+ LLM output formats handled
8
+ ---------------------------
9
+ Self-closing tags::
10
+
11
+ <devtorch:commit message="migrate schema to v3" concepts="schema,migration"
12
+ confidence="0.9"/>
13
+ <devtorch:sensitivity concept="schema_version"
14
+ signal="migration collapses if format changes"
15
+ confidence="0.91"
16
+ disclosure="PROTECTED"/>
17
+
18
+ Strategy
19
+ --------
20
+ 1. Regex locates the raw tag strings in the text (tolerates attribute-order
21
+ variation and minor whitespace differences).
22
+ 2. ``xml.etree.ElementTree`` parses each located string for attribute values.
23
+ 3. Attribute values are validated at parse time: confidence is clamped to
24
+ [0, 1], disclosure is checked against the controlled vocabulary, and
25
+ required attributes (message, concept, signal) must be present.
26
+ 4. Malformed or invalid tags are logged and skipped — never raised.
27
+ """
28
+ from __future__ import annotations
29
+
30
+ import logging
31
+ import re
32
+ import xml.etree.ElementTree as ET
33
+ from dataclasses import dataclass, field
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+ # Controlled vocabulary for disclosure levels
38
+ VALID_DISCLOSURE_LEVELS = frozenset({"PUBLIC", "PROTECTED", "PRIVATE"})
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # Data classes
42
+ # ---------------------------------------------------------------------------
43
+
44
+ @dataclass
45
+ class CommitBlock:
46
+ message: str
47
+ confidence: float = 0.9
48
+ concepts: list[str] = field(default_factory=list)
49
+
50
+
51
+ @dataclass
52
+ class SensitivityBlock:
53
+ concept: str
54
+ signal: str
55
+ confidence: float = 0.9
56
+ disclosure: str = "PROTECTED"
57
+
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # Internal regex patterns
61
+ # ---------------------------------------------------------------------------
62
+
63
+ # Matches self-closing <devtorch:commit ... /> tags.
64
+ _RE_COMMIT = re.compile(
65
+ r"<devtorch:commit\b([^>]*?)/>",
66
+ re.DOTALL | re.IGNORECASE,
67
+ )
68
+
69
+ # Matches self-closing <devtorch:sensitivity ... /> tags.
70
+ _RE_SENSITIVITY = re.compile(
71
+ r"<devtorch:sensitivity\b([^>]*?)/>",
72
+ re.DOTALL | re.IGNORECASE,
73
+ )
74
+
75
+ # Matches any paired <devtorch:*>...</devtorch:*> tags (for strip).
76
+ _RE_PAIRED = re.compile(
77
+ r"<devtorch:[^>]+>.*?</devtorch:[^>]+>",
78
+ re.DOTALL | re.IGNORECASE,
79
+ )
80
+
81
+ # Matches all self-closing devtorch tags (for strip).
82
+ _RE_SELF_CLOSING = re.compile(
83
+ r"<devtorch:\w[\w:.-]*\b[^>]*?/>",
84
+ re.DOTALL | re.IGNORECASE,
85
+ )
86
+
87
+
88
+ # ---------------------------------------------------------------------------
89
+ # Helpers
90
+ # ---------------------------------------------------------------------------
91
+
92
+ def _parse_tag(raw: str) -> ET.Element | None:
93
+ """
94
+ Parse a raw XML tag string via ElementTree.
95
+
96
+ The tag may use namespace prefixes (``devtorch:commit``) which are not
97
+ valid bare XML without a namespace declaration. We normalise the prefix
98
+ to a plain element name so ElementTree can handle it.
99
+
100
+ Returns the parsed Element, or None on error.
101
+ """
102
+ # Normalise namespace prefix: devtorch:foo -> devtorch_foo
103
+ normalised = re.sub(r"<(/?)\s*devtorch:", r"<\1devtorch_", raw)
104
+ try:
105
+ return ET.fromstring(normalised)
106
+ except ET.ParseError as exc:
107
+ logger.warning("devtorch parser: malformed tag skipped — %s | raw=%r", exc, raw)
108
+ return None
109
+
110
+
111
+ # ---------------------------------------------------------------------------
112
+ # Public API
113
+ # ---------------------------------------------------------------------------
114
+
115
+ def extract_racp_blocks(
116
+ text: str,
117
+ ) -> tuple[list[CommitBlock], list[SensitivityBlock]]:
118
+ """
119
+ Extract all ``<devtorch:commit>`` and ``<devtorch:sensitivity>`` blocks
120
+ from *text*.
121
+
122
+ Uses regex to locate tag boundaries, then ``xml.etree.ElementTree`` to
123
+ parse attribute values. Attribute order variations and minor whitespace
124
+ are handled gracefully. Malformed tags are logged and skipped.
125
+
126
+ Returns
127
+ -------
128
+ (commit_blocks, sensitivity_blocks)
129
+ """
130
+ commits: list[CommitBlock] = []
131
+ sensitivities: list[SensitivityBlock] = []
132
+
133
+ # --- commit blocks ---
134
+ for match in _RE_COMMIT.finditer(text):
135
+ raw_tag = match.group(0)
136
+ elem = _parse_tag(raw_tag)
137
+ if elem is None:
138
+ continue
139
+ message = elem.get("message")
140
+ if not message:
141
+ logger.warning("devtorch parser: <devtorch:commit> missing 'message' attribute — skipped | raw=%r", raw_tag)
142
+ continue
143
+ try:
144
+ confidence = float(elem.get("confidence", 0.9))
145
+ if not 0.0 <= confidence <= 1.0:
146
+ logger.warning(
147
+ "devtorch parser: commit confidence %.2f out of range [0,1] — skipped | raw=%r",
148
+ confidence, raw_tag,
149
+ )
150
+ continue
151
+ except (ValueError, TypeError):
152
+ logger.warning("devtorch parser: invalid confidence value in commit block — defaulting to 0.9")
153
+ confidence = 0.9
154
+ concepts_raw = elem.get("concepts") or elem.get("concepts_used") or ""
155
+ concepts = [c.strip() for c in concepts_raw.split(",") if c.strip()]
156
+ commits.append(CommitBlock(message=message, confidence=confidence, concepts=concepts))
157
+
158
+ # --- sensitivity blocks ---
159
+ for match in _RE_SENSITIVITY.finditer(text):
160
+ raw_tag = match.group(0)
161
+ elem = _parse_tag(raw_tag)
162
+ if elem is None:
163
+ continue
164
+ concept = elem.get("concept")
165
+ signal = elem.get("signal")
166
+ if not concept or not signal:
167
+ logger.warning(
168
+ "devtorch parser: <devtorch:sensitivity> missing required attributes — skipped | raw=%r",
169
+ raw_tag,
170
+ )
171
+ continue
172
+ try:
173
+ confidence = float(elem.get("confidence", 0.9))
174
+ if not 0.0 <= confidence <= 1.0:
175
+ logger.warning(
176
+ "devtorch parser: sensitivity confidence %.2f out of range [0,1] — skipped | raw=%r",
177
+ confidence, raw_tag,
178
+ )
179
+ continue
180
+ except (ValueError, TypeError):
181
+ logger.warning("devtorch parser: invalid confidence value in sensitivity block — defaulting to 0.9")
182
+ confidence = 0.9
183
+ disclosure = (elem.get("disclosure") or "PROTECTED").upper()
184
+ if disclosure not in VALID_DISCLOSURE_LEVELS:
185
+ logger.warning(
186
+ "devtorch parser: invalid disclosure level '%s' in sensitivity block — defaulting to PROTECTED | raw=%r",
187
+ disclosure, raw_tag,
188
+ )
189
+ disclosure = "PROTECTED"
190
+ sensitivities.append(
191
+ SensitivityBlock(
192
+ concept=concept,
193
+ signal=signal,
194
+ confidence=confidence,
195
+ disclosure=disclosure,
196
+ )
197
+ )
198
+
199
+ return commits, sensitivities
200
+
201
+
202
+ def strip_racp_blocks(text: str) -> str:
203
+ """
204
+ Remove all ``<devtorch:commit .../>``, ``<devtorch:sensitivity .../>``,
205
+ and any ``<devtorch:*>...</devtorch:*>`` paired tags from *text*.
206
+
207
+ Returns clean text suitable for presentation to the user/caller.
208
+ Surrounding whitespace is collapsed but the overall structure is preserved.
209
+ """
210
+ # Remove paired tags first (they span more characters).
211
+ cleaned = _RE_PAIRED.sub("", text)
212
+ # Remove self-closing tags.
213
+ cleaned = _RE_SELF_CLOSING.sub("", cleaned)
214
+ # Collapse runs of blank lines that may have been left behind.
215
+ cleaned = re.sub(r"\n{3,}", "\n\n", cleaned)
216
+ return cleaned.strip()