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,97 @@
1
+ """
2
+ Sprint 5 – A4: Δf (sensitivity) estimation runner, report artifact, expiry.
3
+
4
+ Publish Δf in Ωᵢ (capability) or separate store; used for privacy accounting.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ DELTAF_DIR_NAME = "deltaf"
15
+ DELTAF_REPORT_NAME = "deltaf_report.json"
16
+ DELTAF_EXPIRY_DAYS_DEFAULT = 90
17
+
18
+
19
+ @dataclass
20
+ class DeltaFReport:
21
+ """Δf estimation report (global or per-concept)."""
22
+ delta_f: float
23
+ concept: Optional[str] = None
24
+ expiry_date: Optional[str] = None # ISO date
25
+ computed_at: Optional[str] = None # ISO timestamp
26
+
27
+ def to_dict(self) -> dict:
28
+ d = {"delta_f": self.delta_f}
29
+ if self.concept is not None:
30
+ d["concept"] = self.concept
31
+ if self.expiry_date is not None:
32
+ d["expiry_date"] = self.expiry_date
33
+ if self.computed_at is not None:
34
+ d["computed_at"] = self.computed_at
35
+ return d
36
+
37
+ @classmethod
38
+ def from_dict(cls, d: dict) -> "DeltaFReport":
39
+ return cls(
40
+ delta_f=float(d["delta_f"]),
41
+ concept=d.get("concept"),
42
+ expiry_date=d.get("expiry_date"),
43
+ computed_at=d.get("computed_at"),
44
+ )
45
+
46
+
47
+ def run_deltaf_estimation(
48
+ sensitivities: list,
49
+ concept: Optional[str] = None,
50
+ ) -> DeltaFReport:
51
+ """
52
+ Placeholder Δf estimation: derive from sensitivity numerical values or use default.
53
+ In production would use proper sensitivity analysis (e.g. global L2 sensitivity).
54
+ """
55
+ delta_f = 1.0 # placeholder
56
+ for s in sensitivities:
57
+ if isinstance(s, dict) and "numerical_value" in s and s["numerical_value"] is not None:
58
+ delta_f = max(delta_f, abs(float(s["numerical_value"])))
59
+ return DeltaFReport(delta_f=delta_f, concept=concept)
60
+
61
+
62
+ class DeltaFStore:
63
+ """Persist Δf report and optionally publish to Ωᵢ."""
64
+ def __init__(self, gcc_dir: Path) -> None:
65
+ self.deltaf_dir = gcc_dir / DELTAF_DIR_NAME
66
+ self.report_path = self.deltaf_dir / DELTAF_REPORT_NAME
67
+
68
+ def ensure_dir(self) -> None:
69
+ self.deltaf_dir.mkdir(parents=True, exist_ok=True)
70
+
71
+ def save_report(self, report: DeltaFReport, computed_at: str, expiry_days: int = DELTAF_EXPIRY_DAYS_DEFAULT) -> None:
72
+ """Save report with computed_at and expiry_date."""
73
+ import datetime as _dt
74
+ report.computed_at = computed_at
75
+ try:
76
+ d = _dt.datetime.fromisoformat(computed_at.replace("Z", "+00:00"))
77
+ report.expiry_date = (d + _dt.timedelta(days=expiry_days)).date().isoformat()
78
+ except Exception:
79
+ report.expiry_date = None
80
+ self.ensure_dir()
81
+ self.report_path.write_text(json.dumps(report.to_dict(), indent=2) + "\n", encoding="utf-8")
82
+
83
+ def load_report(self) -> Optional[DeltaFReport]:
84
+ if not self.report_path.exists():
85
+ return None
86
+ return DeltaFReport.from_dict(json.loads(self.report_path.read_text(encoding="utf-8")))
87
+
88
+ def is_expired(self) -> bool:
89
+ """True if report exists and expiry_date is in the past."""
90
+ r = self.load_report()
91
+ if r is None or r.expiry_date is None:
92
+ return False
93
+ import datetime as _dt
94
+ try:
95
+ return _dt.date.fromisoformat(r.expiry_date) < _dt.date.today()
96
+ except Exception:
97
+ return False
@@ -0,0 +1,50 @@
1
+ """
2
+ Sprint 5 – A4: Textual disclosure policy, [PRIVATE] suppression, fixed-length padding, receiver mandate.
3
+
4
+ [PRIVATE] marker rules (category/count suppression); fixed-length padding; receiver non-zero constraint.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from dataclasses import dataclass
11
+ from typing import List, Optional
12
+
13
+ PRIVATE_MARKER = "[PRIVATE]"
14
+ DEFAULT_PADDING_LENGTH = 32
15
+ DEFAULT_PADDING_CHAR = "█"
16
+
17
+
18
+ @dataclass
19
+ class DisclosurePolicy:
20
+ """Configuration for disclosure and padding."""
21
+ suppress_private: bool = True
22
+ padding_length: int = DEFAULT_PADDING_LENGTH
23
+ padding_char: str = DEFAULT_PADDING_CHAR
24
+ receiver_mandate_nonzero: bool = True # [PRIVATE] must be treated as non-zero in downstream metadata
25
+
26
+ def redact_private(self, text: str) -> str:
27
+ """Replace [PRIVATE] spans with fixed-length padding."""
28
+ if not self.suppress_private:
29
+ return text
30
+ # Replace each [PRIVATE] ... (until next [PRIVATE] or end) or bare [PRIVATE] with padding
31
+ placeholder = self.padding_char * self.padding_length
32
+ # Match [PRIVATE] optionally followed by content until next [PRIVATE] or end of string
33
+ pattern = r"\[PRIVATE\](?:\s*[^\[]*?)?(?=\[PRIVATE\]|$)"
34
+ return re.sub(pattern, placeholder, text, flags=re.DOTALL)
35
+
36
+ def count_private_markers(self, text: str) -> int:
37
+ """Count occurrences of [PRIVATE] for receiver mandate (non-zero constraint)."""
38
+ return len(re.findall(re.escape(PRIVATE_MARKER), text))
39
+
40
+
41
+ def apply_disclosure_policy(text: str, policy: Optional[DisclosurePolicy] = None) -> str:
42
+ """Apply disclosure policy: suppress [PRIVATE] with padding."""
43
+ if policy is None:
44
+ policy = DisclosurePolicy()
45
+ return policy.redact_private(text)
46
+
47
+
48
+ def receiver_mandate_private_count(text: str) -> int:
49
+ """Return count of [PRIVATE] markers; downstream must treat as non-zero (do not ignore)."""
50
+ return len(re.findall(re.escape(PRIVATE_MARKER), text))
@@ -0,0 +1,3 @@
1
+ from .detector import DivergenceSignal, DivergenceDetector
2
+
3
+ __all__ = ["DivergenceSignal", "DivergenceDetector"]
@@ -0,0 +1,166 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import json
5
+ from pathlib import Path
6
+ from typing import List, Optional
7
+
8
+ _SENSITIVITIES_FILE = "sensitivities/events.jsonl"
9
+ _SEVERITY_ORDER = {"HIGH": 0, "MEDIUM": 1, "LOW": 2}
10
+ _CONFIDENCE_GAP_THRESHOLD = 0.35
11
+
12
+
13
+ @dataclasses.dataclass
14
+ class DivergenceSignal:
15
+ """
16
+ A detected divergence between agents on a specific concept.
17
+
18
+ signal_type:
19
+ A — conflicting decisions (commit messages contradict each other)
20
+ B — confidence gap ≥0.35 between two agents on the same concept
21
+ C — disclosure mismatch (e.g. PUBLIC vs PRIVATE)
22
+ D — missing coverage (agent has no reasoning for a concept another agent flagged)
23
+ E — consensus contradiction (new signal conflicts with a durable consensus commit)
24
+ """
25
+ signal_type: str # "A" | "B" | "C" | "D" | "E"
26
+ severity: str # "HIGH" | "MEDIUM" | "LOW"
27
+ concept: str
28
+ agent_ids: List[str]
29
+ description: str
30
+ evidence: dict
31
+
32
+
33
+ class DivergenceDetector:
34
+ """Detect reasoning divergence across agents for a given concept."""
35
+
36
+ def __init__(self, repo) -> None:
37
+ self._repo = repo
38
+ self._gcc_dir = repo.gcc_dir
39
+
40
+ def detect(self, concept: str) -> List[DivergenceSignal]:
41
+ events = self._load_events(concept)
42
+ if len(events) < 2:
43
+ return []
44
+
45
+ signals: List[DivergenceSignal] = []
46
+ signals.extend(self._check_confidence_gap(concept, events))
47
+ signals.extend(self._check_disclosure_mismatch(concept, events))
48
+ signals.extend(self._check_consensus_contradiction(concept, events))
49
+
50
+ signals.sort(key=lambda s: _SEVERITY_ORDER.get(s.severity, 99))
51
+ return signals
52
+
53
+ def _load_events(self, concept: str) -> List[dict]:
54
+ path = self._gcc_dir / _SENSITIVITIES_FILE
55
+ if not path.exists():
56
+ return []
57
+ results = []
58
+ for line in path.read_text(encoding="utf-8").splitlines():
59
+ line = line.strip()
60
+ if not line:
61
+ continue
62
+ try:
63
+ d = json.loads(line)
64
+ if d.get("target_concept") == concept:
65
+ results.append(d)
66
+ except json.JSONDecodeError:
67
+ continue
68
+ return results
69
+
70
+ def _check_confidence_gap(self, concept: str, events: List[dict]) -> List[DivergenceSignal]:
71
+ """Signal B: two agents differ in confidence by ≥0.35."""
72
+ by_agent: dict[str, List[float]] = {}
73
+ for ev in events:
74
+ agent = ev.get("agent_id") or ev.get("source_node", "unknown")
75
+ by_agent.setdefault(agent, []).append(float(ev.get("confidence", 0.0)))
76
+
77
+ if len(by_agent) < 2:
78
+ return []
79
+
80
+ agent_means = {a: sum(cs) / len(cs) for a, cs in by_agent.items()}
81
+ agents = list(agent_means.keys())
82
+ signals = []
83
+ for i in range(len(agents)):
84
+ for j in range(i + 1, len(agents)):
85
+ gap = abs(agent_means[agents[i]] - agent_means[agents[j]])
86
+ if gap >= _CONFIDENCE_GAP_THRESHOLD:
87
+ signals.append(DivergenceSignal(
88
+ signal_type="B",
89
+ severity="MEDIUM",
90
+ concept=concept,
91
+ agent_ids=[agents[i], agents[j]],
92
+ description=f"Confidence gap {gap:.2f} on '{concept}': "
93
+ f"{agents[i]}={agent_means[agents[i]]:.2f}, "
94
+ f"{agents[j]}={agent_means[agents[j]]:.2f}",
95
+ evidence={
96
+ f"{agents[i]}_mean_confidence": round(agent_means[agents[i]], 4),
97
+ f"{agents[j]}_mean_confidence": round(agent_means[agents[j]], 4),
98
+ "gap": round(gap, 4),
99
+ },
100
+ ))
101
+ return signals
102
+
103
+ def _check_disclosure_mismatch(self, concept: str, events: List[dict]) -> List[DivergenceSignal]:
104
+ """Signal C: agents disagree on disclosure level — always HIGH severity."""
105
+ by_agent: dict[str, set] = {}
106
+ for ev in events:
107
+ agent = ev.get("agent_id") or ev.get("source_node", "unknown")
108
+ by_agent.setdefault(agent, set()).add(ev.get("disclosure_level", "PROTECTED"))
109
+
110
+ # Take the most restrictive disclosure level per agent
111
+ all_levels: dict[str, str] = {
112
+ agent: max(levels, key=lambda l: {"PRIVATE": 2, "PROTECTED": 1, "PUBLIC": 0}.get(l, 0))
113
+ for agent, levels in by_agent.items()
114
+ }
115
+
116
+ if len(all_levels) < 2:
117
+ return []
118
+
119
+ unique_levels = set(all_levels.values())
120
+ if len(unique_levels) <= 1:
121
+ return []
122
+
123
+ return [DivergenceSignal(
124
+ signal_type="C",
125
+ severity="HIGH",
126
+ concept=concept,
127
+ agent_ids=list(all_levels.keys()),
128
+ description=f"Disclosure mismatch on '{concept}': " +
129
+ ", ".join(f"{a}={l}" for a, l in all_levels.items()),
130
+ evidence=dict(all_levels),
131
+ )]
132
+
133
+ def _check_consensus_contradiction(self, concept: str, events: List[dict]) -> List[DivergenceSignal]:
134
+ """Signal E: new sensitivity contradicts an existing consensus commit."""
135
+ consensus_path = self._gcc_dir / "consolidations"
136
+ if not consensus_path.exists():
137
+ return []
138
+
139
+ signals = []
140
+ for f in consensus_path.glob("*.json"):
141
+ try:
142
+ record = json.loads(f.read_text(encoding="utf-8"))
143
+ except (json.JSONDecodeError, OSError):
144
+ continue
145
+ if record.get("concept") != concept:
146
+ continue
147
+ if record.get("state") not in ("resolved", "auto_resolved"):
148
+ continue
149
+ resolved_at = record.get("resolved_at", "")
150
+ post_consensus = [
151
+ ev for ev in events
152
+ if ev.get("created_at", "") > resolved_at
153
+ ]
154
+ if post_consensus:
155
+ signals.append(DivergenceSignal(
156
+ signal_type="E",
157
+ severity="HIGH",
158
+ concept=concept,
159
+ agent_ids=list({
160
+ ev.get("agent_id") or ev.get("source_node", "unknown")
161
+ for ev in post_consensus
162
+ }),
163
+ description=f"{len(post_consensus)} new signals on '{concept}' after consensus at {resolved_at[:19]}",
164
+ evidence={"consensus_record_id": record.get("record_id", ""), "post_consensus_count": len(post_consensus)},
165
+ ))
166
+ return signals
@@ -0,0 +1,32 @@
1
+ """
2
+ devtorch_core.gateway
3
+ =====================
4
+ Enterprise org gateway — multi-tenant proxy with SSO, governance policy
5
+ enforcement, central API key management, and metrics webhooks.
6
+
7
+ This is the enterprise counterpart to the local proxy (devtorch_core/proxy/).
8
+ It runs in the company's own cloud and handles multiple developers simultaneously.
9
+
10
+ Exports:
11
+ GovernancePolicy — policy dataclass
12
+ PolicyEngine — request/response enforcement
13
+ SSOValidator — JWT / SSO token validation
14
+ MetricsWebhook — push aggregate metrics to external webhook
15
+ GatewayServer — multi-tenant org proxy server
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from .key_manager import KeyManager
20
+ from .policy import GovernancePolicy, PolicyEngine
21
+ from .sso import SSOValidator
22
+ from .metrics_webhook import MetricsWebhook
23
+ from .server import GatewayServer
24
+
25
+ __all__ = [
26
+ "GovernancePolicy",
27
+ "PolicyEngine",
28
+ "SSOValidator",
29
+ "MetricsWebhook",
30
+ "KeyManager",
31
+ "GatewayServer",
32
+ ]
@@ -0,0 +1,124 @@
1
+ """
2
+ devtorch_core.gateway.key_manager
3
+ =================================
4
+ Centralised org LLM API key management for the enterprise gateway.
5
+
6
+ The KeyManager keeps provider keys in memory only (never writes to disk), and
7
+ masks them in string representations so they are not accidentally leaked in
8
+ logs or tracebacks.
9
+
10
+ Backward compatibility:
11
+ Keys may be supplied directly via the GatewayServer constructor, loaded
12
+ from environment variables (ANTHROPIC_API_KEY, OPENAI_API_KEY), or
13
+ provided as a dictionary to KeyManager. Constructor-supplied keys take
14
+ precedence over the environment.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import os
19
+ from typing import Optional
20
+
21
+
22
+ # Provider names used by the gateway.
23
+ ANTHROPIC = "anthropic"
24
+ OPENAI = "openai"
25
+
26
+ # Default environment variable names for each provider.
27
+ _ENV_VARS = {
28
+ ANTHROPIC: "ANTHROPIC_API_KEY",
29
+ OPENAI: "OPENAI_API_KEY",
30
+ }
31
+
32
+
33
+ class KeyManager:
34
+ """
35
+ In-memory store and rotator for org LLM API keys.
36
+
37
+ Parameters
38
+ ----------
39
+ keys:
40
+ Optional mapping of provider name -> API key. Missing providers are
41
+ backfilled from environment variables. Use an empty dict to force
42
+ environment-only loading.
43
+ env_var_map:
44
+ Optional mapping of provider name -> environment variable name to use
45
+ instead of the defaults.
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ keys: Optional[dict[str, str]] = None,
51
+ env_var_map: Optional[dict[str, str]] = None,
52
+ ) -> None:
53
+ self._env_var_map = {**_ENV_VARS, **(env_var_map or {})}
54
+ self._keys: dict[str, str] = {}
55
+
56
+ # Seed from explicit keys first (normalise provider names to lowercase).
57
+ if keys:
58
+ for provider, key in keys.items():
59
+ self._keys[provider.lower()] = key
60
+
61
+ # Backfill from environment variables where not already provided.
62
+ for provider, env_var in self._env_var_map.items():
63
+ provider_lower = provider.lower()
64
+ if provider_lower not in self._keys:
65
+ value = os.environ.get(env_var, "")
66
+ if value:
67
+ self._keys[provider_lower] = value
68
+
69
+ # ------------------------------------------------------------------
70
+ # Public API
71
+ # ------------------------------------------------------------------
72
+
73
+ def get_key(self, provider: str) -> str:
74
+ """
75
+ Return the API key for *provider*, or an empty string if not configured.
76
+
77
+ *provider* is normalised to lowercase.
78
+ """
79
+ return self._keys.get(provider.lower(), "")
80
+
81
+ def rotate_key(self, provider: str, new_key: str) -> str:
82
+ """
83
+ Replace the stored key for *provider* with *new_key*.
84
+
85
+ Returns the previous key (or empty string if none existed).
86
+ """
87
+ provider = provider.lower()
88
+ previous = self._keys.get(provider, "")
89
+ self._keys[provider] = new_key
90
+ return previous
91
+
92
+ def set_key(self, provider: str, key: str) -> None:
93
+ """
94
+ Alias for rotate_key that does not return the previous value.
95
+ """
96
+ self.rotate_key(provider, key)
97
+
98
+ def has_key(self, provider: str) -> bool:
99
+ """Return True if a non-empty key is configured for *provider*."""
100
+ return bool(self.get_key(provider))
101
+
102
+ def providers(self) -> list[str]:
103
+ """Return the list of providers that currently have a key configured."""
104
+ return [p for p, k in self._keys.items() if k]
105
+
106
+ # ------------------------------------------------------------------
107
+ # Safety / introspection
108
+ # ------------------------------------------------------------------
109
+
110
+ def _mask(self, key: str) -> str:
111
+ """Return a masked representation of *key* (last 4 chars only)."""
112
+ if not key:
113
+ return ""
114
+ if len(key) <= 4:
115
+ return "****"
116
+ return f"{'*' * (len(key) - 4)}{key[-4:]}"
117
+
118
+ def __repr__(self) -> str:
119
+ masked = {p: self._mask(k) for p, k in self._keys.items()}
120
+ return f"{self.__class__.__name__}({masked!r})"
121
+
122
+ def __str__(self) -> str:
123
+ providers = self.providers()
124
+ return f"{self.__class__.__name__}(providers={providers})"