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,252 @@
1
+ """
2
+ devtorch_core.gateway.metrics_webhook
3
+ =======================================
4
+ Push aggregate gateway metrics to an external webhook endpoint.
5
+
6
+ PRIVACY GUARANTEE:
7
+ The payload NEVER includes prompt text, code, reasoning blocks, or any
8
+ developer-authored content. Only counts and aggregate scores are sent.
9
+
10
+ Payload schema:
11
+ {
12
+ "ts": ISO-8601 timestamp (UTC),
13
+ "gateway_version": str,
14
+ "token_counts": {
15
+ "total": int, # sum across all sessions
16
+ "by_session": {user_id: int} # per-user totals (user IDs only)
17
+ },
18
+ "mcs_score": float | null,
19
+ "dhs_score": float | null,
20
+ "collision_count": int,
21
+ "session_count": int
22
+ }
23
+
24
+ Design decisions:
25
+ - Uses stdlib urllib.request so no new dependencies are introduced.
26
+ - All errors are caught silently and push() returns False — the gateway
27
+ must never crash because a webhook is unreachable.
28
+ - enabled=False is the safe default so metrics are opt-in.
29
+ """
30
+ from __future__ import annotations
31
+
32
+ import json
33
+ import logging
34
+ import os
35
+ import urllib.request
36
+ from dataclasses import dataclass
37
+ from datetime import datetime, timezone
38
+ from typing import Optional
39
+
40
+ logger = logging.getLogger("devtorch.gateway.metrics_webhook")
41
+
42
+ GATEWAY_VERSION = "0.7.0"
43
+
44
+ DEFAULT_METRICS_INTERVAL_SECONDS = 300
45
+
46
+
47
+ @dataclass
48
+ class MetricsWebhookConfig:
49
+ """
50
+ Environment-driven configuration for the metrics webhook.
51
+
52
+ All fields are optional and default to safe/opt-in-off values.
53
+ """
54
+
55
+ url: str = ""
56
+ enabled: bool = False
57
+ api_key: str = ""
58
+ interval_seconds: int = DEFAULT_METRICS_INTERVAL_SECONDS
59
+
60
+
61
+ # Keys that must NEVER appear in the webhook payload (privacy guard)
62
+ _FORBIDDEN_KEYS = frozenset(
63
+ {"prompt", "prompts", "content", "messages", "reasoning", "text", "code"}
64
+ )
65
+
66
+
67
+ def load_metrics_webhook_config() -> MetricsWebhookConfig:
68
+ """
69
+ Load metrics webhook configuration from environment variables.
70
+
71
+ Variables:
72
+ - DEVTORCH_METRICS_WEBHOOK_URL
73
+ - DEVTORCH_METRICS_WEBHOOK_ENABLED
74
+ - DEVTORCH_METRICS_WEBHOOK_API_KEY
75
+ - DEVTORCH_METRICS_WEBHOOK_INTERVAL_SECONDS
76
+
77
+ Returns
78
+ -------
79
+ MetricsWebhookConfig with safe defaults (disabled by default).
80
+ """
81
+ url = os.environ.get("DEVTORCH_METRICS_WEBHOOK_URL", "").strip()
82
+ enabled = _parse_bool_env(os.environ.get("DEVTORCH_METRICS_WEBHOOK_ENABLED", ""))
83
+ api_key = os.environ.get("DEVTORCH_METRICS_WEBHOOK_API_KEY", "").strip()
84
+
85
+ interval = DEFAULT_METRICS_INTERVAL_SECONDS
86
+ interval_raw = os.environ.get("DEVTORCH_METRICS_WEBHOOK_INTERVAL_SECONDS", "")
87
+ if interval_raw:
88
+ try:
89
+ interval = max(1, int(interval_raw))
90
+ except ValueError:
91
+ interval = DEFAULT_METRICS_INTERVAL_SECONDS
92
+
93
+ return MetricsWebhookConfig(
94
+ url=url,
95
+ enabled=enabled,
96
+ api_key=api_key,
97
+ interval_seconds=interval,
98
+ )
99
+
100
+
101
+ def _parse_bool_env(value: str) -> bool:
102
+ """Return True if *value* looks like an affirmative env string."""
103
+ if not value:
104
+ return False
105
+ return value.strip().lower() in {"1", "true", "yes", "on"}
106
+
107
+
108
+ class MetricsWebhook:
109
+ """
110
+ Push aggregate metrics to an external HTTP webhook.
111
+
112
+ Parameters
113
+ ----------
114
+ webhook_url:
115
+ Full URL to POST metrics to (e.g. "https://analytics.example.com/devtorch").
116
+ enabled:
117
+ Set True to activate. Defaults to False so metrics are opt-in.
118
+ api_key:
119
+ Optional bearer token sent as `Authorization: Bearer <api_key>`.
120
+ interval_seconds:
121
+ How often (seconds) metrics are pushed when scheduled by the gateway.
122
+ """
123
+
124
+ def __init__(
125
+ self,
126
+ webhook_url: str = "",
127
+ enabled: bool = False,
128
+ api_key: str = "",
129
+ interval_seconds: int = DEFAULT_METRICS_INTERVAL_SECONDS,
130
+ ) -> None:
131
+ self._url = webhook_url.strip()
132
+ self._enabled = enabled
133
+ self._api_key = api_key.strip()
134
+ self._interval_seconds = max(1, interval_seconds)
135
+
136
+ # ------------------------------------------------------------------
137
+ # Public API
138
+ # ------------------------------------------------------------------
139
+
140
+ def push(self, payload: dict) -> bool:
141
+ """
142
+ POST *payload* as JSON to the configured webhook URL.
143
+
144
+ Returns True on HTTP 200, False on any error or if disabled.
145
+ NEVER raises — all exceptions are caught silently.
146
+ """
147
+ if not self._enabled or not self._url:
148
+ return False
149
+
150
+ # Privacy guard: strip any forbidden keys recursively.
151
+ safe_payload = _strip_forbidden(payload)
152
+
153
+ try:
154
+ body = json.dumps(safe_payload, default=str).encode("utf-8")
155
+ headers = {
156
+ "Content-Type": "application/json",
157
+ "User-Agent": f"DevTorch-Gateway/{GATEWAY_VERSION}",
158
+ }
159
+ if self._api_key:
160
+ headers["Authorization"] = f"Bearer {self._api_key}"
161
+
162
+ req = urllib.request.Request(
163
+ self._url,
164
+ data=body,
165
+ method="POST",
166
+ headers=headers,
167
+ )
168
+ with urllib.request.urlopen(req, timeout=5) as resp:
169
+ return resp.status == 200
170
+ except Exception as exc:
171
+ logger.debug("MetricsWebhook.push failed: %s", exc)
172
+ return False
173
+
174
+ def build_payload(self, sessions: list[dict]) -> dict:
175
+ """
176
+ Build a privacy-safe webhook payload from session data.
177
+
178
+ Parameters
179
+ ----------
180
+ sessions:
181
+ List of session dicts. Each dict may contain:
182
+ - user_id: str
183
+ - tokens_used: int
184
+ - mcs_score: float | None
185
+ - dhs_score: float | None
186
+ - collision_count: int
187
+
188
+ Returns a dict matching the payload schema. No prompt/content fields
189
+ will ever appear.
190
+ """
191
+ total_tokens = 0
192
+ by_session: dict[str, int] = {}
193
+ mcs_scores: list[float] = []
194
+ dhs_scores: list[float] = []
195
+ total_collisions = 0
196
+
197
+ for sess in sessions:
198
+ uid = str(sess.get("user_id", "unknown"))
199
+ tokens = int(sess.get("tokens_used", 0))
200
+ total_tokens += tokens
201
+ by_session[uid] = by_session.get(uid, 0) + tokens
202
+
203
+ mcs = sess.get("mcs_score")
204
+ if mcs is not None:
205
+ try:
206
+ mcs_scores.append(float(mcs))
207
+ except (TypeError, ValueError):
208
+ pass
209
+
210
+ dhs = sess.get("dhs_score")
211
+ if dhs is not None:
212
+ try:
213
+ dhs_scores.append(float(dhs))
214
+ except (TypeError, ValueError):
215
+ pass
216
+
217
+ total_collisions += int(sess.get("collision_count", 0))
218
+
219
+ return {
220
+ "ts": datetime.now(tz=timezone.utc).isoformat(),
221
+ "gateway_version": GATEWAY_VERSION,
222
+ "token_counts": {
223
+ "total": total_tokens,
224
+ "by_session": by_session,
225
+ },
226
+ "mcs_score": (sum(mcs_scores) / len(mcs_scores)) if mcs_scores else None,
227
+ "dhs_score": (sum(dhs_scores) / len(dhs_scores)) if dhs_scores else None,
228
+ "collision_count": total_collisions,
229
+ "session_count": len(sessions),
230
+ }
231
+
232
+
233
+ # ---------------------------------------------------------------------------
234
+ # Internal helpers
235
+ # ---------------------------------------------------------------------------
236
+
237
+ def _strip_forbidden(obj: object) -> object:
238
+ """
239
+ Recursively remove any dict keys in _FORBIDDEN_KEYS.
240
+
241
+ This is the privacy firewall between internal session data and the
242
+ outgoing webhook payload.
243
+ """
244
+ if isinstance(obj, dict):
245
+ return {
246
+ k: _strip_forbidden(v)
247
+ for k, v in obj.items()
248
+ if k.lower() not in _FORBIDDEN_KEYS
249
+ }
250
+ if isinstance(obj, list):
251
+ return [_strip_forbidden(item) for item in obj]
252
+ return obj
@@ -0,0 +1,262 @@
1
+ """
2
+ devtorch_core.gateway.policy
3
+ =============================
4
+ Governance policy dataclass and enforcement engine for the enterprise gateway.
5
+
6
+ GovernancePolicy defines what is allowed for an organisation.
7
+ PolicyEngine enforces it on every request and response.
8
+
9
+ Design decisions:
10
+ - check_request returns (False, reason) to let the caller decide HTTP status.
11
+ - check_response only *warns* on missing RACP blocks — it does not block
12
+ responses so that developer flow is never interrupted by compliance issues.
13
+ - load_from_file accepts a JSON file whose top-level keys map 1:1 to the
14
+ dataclass fields, making policy files human-editable without a schema editor.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import logging
20
+ import threading
21
+ from dataclasses import dataclass, field
22
+ from typing import ClassVar
23
+
24
+ logger = logging.getLogger("devtorch.gateway.policy")
25
+
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Dataclass
29
+ # ---------------------------------------------------------------------------
30
+
31
+ @dataclass
32
+ class GovernancePolicy:
33
+ """
34
+ Org-level governance policy.
35
+
36
+ All fields have permissive defaults so that a gateway started with
37
+ ``GovernancePolicy.default()`` never blocks any legitimate request.
38
+ """
39
+
40
+ # Which LLM models are permitted. Empty list = all permitted.
41
+ allowed_models: list[str] = field(default_factory=list)
42
+
43
+ # Maximum cumulative tokens consumed by one (user, date) session.
44
+ max_tokens_per_session: int = 1_000_000
45
+
46
+ # Whether RACP compliance is mandatory. If True, a missing RACP block
47
+ # triggers a WARNING log (but does NOT block the response).
48
+ racp_compliance_required: bool = False
49
+
50
+ # Minimum ratio (0.0–1.0) of responses that must contain RACP blocks.
51
+ # Informational for now; used by the metrics webhook payload.
52
+ racp_compliance_threshold: float = 0.0
53
+
54
+ # If True, a warning is emitted when the agent skips devtorch_commit.
55
+ require_devtorch_commit: bool = False
56
+
57
+ # Concepts that trigger an alert when found in a response (informational).
58
+ blocked_concepts: list[str] = field(default_factory=list)
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # Class methods
62
+ # ---------------------------------------------------------------------------
63
+
64
+ @classmethod
65
+ def default(cls) -> "GovernancePolicy":
66
+ """Return a maximally permissive policy suitable for development."""
67
+ return cls(
68
+ allowed_models=[], # empty = all models allowed
69
+ max_tokens_per_session=1_000_000,
70
+ racp_compliance_required=False,
71
+ racp_compliance_threshold=0.0,
72
+ require_devtorch_commit=False,
73
+ blocked_concepts=[],
74
+ )
75
+
76
+ @classmethod
77
+ def load_from_file(cls, path: str) -> "GovernancePolicy":
78
+ """
79
+ Load a GovernancePolicy from a JSON file.
80
+
81
+ The JSON must have top-level keys matching the dataclass field names.
82
+ Unknown keys are ignored. Missing keys fall back to the default.
83
+ """
84
+ with open(path, "r", encoding="utf-8") as fh:
85
+ raw = json.load(fh)
86
+
87
+ defaults = cls.default()
88
+ return cls(
89
+ allowed_models=raw.get("allowed_models", defaults.allowed_models),
90
+ max_tokens_per_session=int(
91
+ raw.get("max_tokens_per_session", defaults.max_tokens_per_session)
92
+ ),
93
+ racp_compliance_required=bool(
94
+ raw.get("racp_compliance_required", defaults.racp_compliance_required)
95
+ ),
96
+ racp_compliance_threshold=float(
97
+ raw.get("racp_compliance_threshold", defaults.racp_compliance_threshold)
98
+ ),
99
+ require_devtorch_commit=bool(
100
+ raw.get("require_devtorch_commit", defaults.require_devtorch_commit)
101
+ ),
102
+ blocked_concepts=raw.get("blocked_concepts", defaults.blocked_concepts),
103
+ )
104
+
105
+ def to_summary(self) -> dict:
106
+ """Return a JSON-safe summary for the /health endpoint (no secrets)."""
107
+ return {
108
+ "allowed_models": self.allowed_models or ["*"],
109
+ "max_tokens_per_session": self.max_tokens_per_session,
110
+ "racp_compliance_required": self.racp_compliance_required,
111
+ "racp_compliance_threshold": self.racp_compliance_threshold,
112
+ "require_devtorch_commit": self.require_devtorch_commit,
113
+ "blocked_concepts_count": len(self.blocked_concepts),
114
+ }
115
+
116
+
117
+ # ---------------------------------------------------------------------------
118
+ # Engine
119
+ # ---------------------------------------------------------------------------
120
+
121
+ class PolicyEngine:
122
+ """
123
+ Enforce a GovernancePolicy on individual requests and responses.
124
+
125
+ Thread-safe: the mutable RACP compliance window is protected by a lock.
126
+ """
127
+
128
+ # Default size of the rolling RACP compliance window.
129
+ _DEFAULT_RACP_WINDOW_SIZE: ClassVar[int] = 20
130
+
131
+ def __init__(
132
+ self,
133
+ policy: GovernancePolicy,
134
+ racp_window_size: int = _DEFAULT_RACP_WINDOW_SIZE,
135
+ ) -> None:
136
+ self._policy = policy
137
+ self._racp_window_size = max(1, racp_window_size)
138
+ self._racp_window: list[bool] = []
139
+ self._lock = threading.Lock()
140
+
141
+ # ------------------------------------------------------------------
142
+ # Public API
143
+ # ------------------------------------------------------------------
144
+
145
+ def check_request(
146
+ self,
147
+ model: str,
148
+ messages: list,
149
+ session_tokens_used: int,
150
+ ) -> tuple[bool, str]:
151
+ """
152
+ Validate an outgoing request against the current policy.
153
+
154
+ Returns:
155
+ (True, "") — request is permitted
156
+ (False, reason_str) — request should be blocked with 403
157
+ """
158
+ p = self._policy
159
+
160
+ # 1. Model allowlist (empty list = all models permitted)
161
+ if p.allowed_models and model not in p.allowed_models:
162
+ reason = (
163
+ f"Model '{model}' is not in the org allowed_models list. "
164
+ f"Permitted: {p.allowed_models}"
165
+ )
166
+ logger.warning("Gateway BLOCKED request: %s", reason)
167
+ return False, reason
168
+
169
+ # 2. Token quota
170
+ if session_tokens_used >= p.max_tokens_per_session:
171
+ reason = (
172
+ f"Session token limit reached: {session_tokens_used} >= "
173
+ f"{p.max_tokens_per_session}. Start a new session."
174
+ )
175
+ logger.warning("Gateway BLOCKED request: %s", reason)
176
+ return False, reason
177
+
178
+ return True, ""
179
+
180
+ def check_response(
181
+ self,
182
+ response_body: dict,
183
+ has_racp_blocks: bool,
184
+ ) -> tuple[bool, str]:
185
+ """
186
+ Validate a response from the upstream LLM.
187
+
188
+ This method only *warns* — it never blocks a response — so the return
189
+ value is always (True, warning_or_empty).
190
+
191
+ Returns:
192
+ (True, "") — response is clean
193
+ (True, warning_str) — response has compliance issue but is allowed
194
+ """
195
+ p = self._policy
196
+
197
+ # Track RACP presence in a rolling window for threshold enforcement.
198
+ self._record_racp_presence(has_racp_blocks)
199
+
200
+ if p.racp_compliance_required and not has_racp_blocks:
201
+ warning = (
202
+ "RACP compliance required but this response contains no RACP blocks. "
203
+ "Ensure the agent is using devtorch_commit / RACP annotations."
204
+ )
205
+ logger.warning("Gateway compliance warning: %s", warning)
206
+ return True, warning
207
+
208
+ # Enforce the rolling RACP compliance threshold (informational only).
209
+ threshold_warning = self._check_racp_threshold()
210
+ if threshold_warning:
211
+ logger.warning("Gateway compliance warning: %s", threshold_warning)
212
+ return True, threshold_warning
213
+
214
+ return True, ""
215
+
216
+ @property
217
+ def policy(self) -> GovernancePolicy:
218
+ return self._policy
219
+
220
+ @property
221
+ def racp_ratio(self) -> float:
222
+ """Return the current ratio of responses in the window that contain RACP blocks."""
223
+ with self._lock:
224
+ if not self._racp_window:
225
+ return 0.0
226
+ return sum(self._racp_window) / len(self._racp_window)
227
+
228
+ @property
229
+ def racp_window_count(self) -> int:
230
+ """Return the number of responses currently in the RACP compliance window."""
231
+ with self._lock:
232
+ return len(self._racp_window)
233
+
234
+ # ------------------------------------------------------------------
235
+ # Internal helpers
236
+ # ------------------------------------------------------------------
237
+
238
+ def _record_racp_presence(self, has_racp_blocks: bool) -> None:
239
+ """Append *has_racp_blocks* to the rolling window, trimming to size."""
240
+ with self._lock:
241
+ self._racp_window.append(bool(has_racp_blocks))
242
+ if len(self._racp_window) > self._racp_window_size:
243
+ self._racp_window.pop(0)
244
+
245
+ def _check_racp_threshold(self) -> str:
246
+ """
247
+ Return a warning string if the rolling RACP ratio is below the policy
248
+ threshold. Returns an empty string when the threshold is disabled or
249
+ when the ratio is satisfactory.
250
+ """
251
+ p = self._policy
252
+ if p.racp_compliance_threshold <= 0:
253
+ return ""
254
+
255
+ ratio = self.racp_ratio
256
+ if ratio < p.racp_compliance_threshold:
257
+ return (
258
+ f"Rolling RACP compliance ratio {ratio:.2f} is below the org "
259
+ f"threshold {p.racp_compliance_threshold:.2f}. "
260
+ f"Ensure agents are using devtorch_commit / RACP annotations."
261
+ )
262
+ return ""