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.
- devtorch_core/__init__.py +158 -0
- devtorch_core/aggphi_textual.py +275 -0
- devtorch_core/alerts/__init__.py +23 -0
- devtorch_core/alerts/base.py +46 -0
- devtorch_core/alerts/config.py +60 -0
- devtorch_core/alerts/dispatcher.py +110 -0
- devtorch_core/alerts/jira.py +96 -0
- devtorch_core/alerts/linear.py +72 -0
- devtorch_core/alerts/pagerduty.py +66 -0
- devtorch_core/alerts/slack.py +81 -0
- devtorch_core/alerts/teams.py +70 -0
- devtorch_core/audit/__init__.py +43 -0
- devtorch_core/audit/exporter.py +297 -0
- devtorch_core/audit/privacy.py +101 -0
- devtorch_core/audit/scrubber.py +149 -0
- devtorch_core/audit/service.py +67 -0
- devtorch_core/audit/signing.py +127 -0
- devtorch_core/broadcast/__init__.py +4 -0
- devtorch_core/broadcast/broadcaster.py +100 -0
- devtorch_core/broadcast/watcher.py +71 -0
- devtorch_core/capability.py +639 -0
- devtorch_core/cloud/__init__.py +1 -0
- devtorch_core/cloud/client_config.py +472 -0
- devtorch_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
- devtorch_core/cloud/client_configs/.claude-stdio.json +13 -0
- devtorch_core/cloud/client_configs/.cursor-mcp.json +13 -0
- devtorch_core/cloud/client_configs/.opencode-bridge.json +13 -0
- devtorch_core/cloud/client_configs/.opencode.json +15 -0
- devtorch_core/cloud/client_configs/.vscode-mcp.json +13 -0
- devtorch_core/cloud/devtorch-mcp-bridge.js +357 -0
- devtorch_core/cloud/mcp_client.py +229 -0
- devtorch_core/cloud/setup.py +144 -0
- devtorch_core/cloud/sync.py +143 -0
- devtorch_core/cloud/sync_bundle.py +603 -0
- devtorch_core/cloud/sync_conflicts.py +159 -0
- devtorch_core/cloud/sync_state.py +159 -0
- devtorch_core/cloud/team_sync.py +283 -0
- devtorch_core/codex/__init__.py +9 -0
- devtorch_core/codex/__main__.py +97 -0
- devtorch_core/codex/capture.py +208 -0
- devtorch_core/codex/proxy.py +412 -0
- devtorch_core/concept_catalog.py +209 -0
- devtorch_core/consolidation/__init__.py +3 -0
- devtorch_core/consolidation/synthesizer.py +87 -0
- devtorch_core/consolidation/workflow.py +175 -0
- devtorch_core/daemon/__init__.py +27 -0
- devtorch_core/daemon/supervisor.py +293 -0
- devtorch_core/daemon/watcher.py +244 -0
- devtorch_core/dashboard_api.py +2012 -0
- devtorch_core/deltaf.py +97 -0
- devtorch_core/disclosure.py +50 -0
- devtorch_core/divergence/__init__.py +3 -0
- devtorch_core/divergence/detector.py +166 -0
- devtorch_core/gateway/__init__.py +32 -0
- devtorch_core/gateway/key_manager.py +124 -0
- devtorch_core/gateway/metrics_webhook.py +252 -0
- devtorch_core/gateway/policy.py +262 -0
- devtorch_core/gateway/server.py +727 -0
- devtorch_core/gateway/sso.py +233 -0
- devtorch_core/gcc.py +1246 -0
- devtorch_core/github/__init__.py +35 -0
- devtorch_core/github/app.py +240 -0
- devtorch_core/github/comment_builder.py +113 -0
- devtorch_core/github/pat.py +76 -0
- devtorch_core/github/pr_parser.py +82 -0
- devtorch_core/github/pr_reporter.py +555 -0
- devtorch_core/gitlab/__init__.py +177 -0
- devtorch_core/hitl/__init__.py +4 -0
- devtorch_core/hitl/channels.py +129 -0
- devtorch_core/hitl/orchestrator.py +95 -0
- devtorch_core/hooks/__init__.py +17 -0
- devtorch_core/hooks/claude_code.py +228 -0
- devtorch_core/hooks/git_capture.py +341 -0
- devtorch_core/hooks/git_commit.py +182 -0
- devtorch_core/hooks/installer.py +733 -0
- devtorch_core/hooks/pre_commit.py +157 -0
- devtorch_core/hooks/runner.py +344 -0
- devtorch_core/identity/__init__.py +4 -0
- devtorch_core/identity/agent.py +86 -0
- devtorch_core/identity/providers.py +85 -0
- devtorch_core/invariants.py +182 -0
- devtorch_core/mcp/__init__.py +10 -0
- devtorch_core/mcp/auth.py +177 -0
- devtorch_core/mcp/server.py +1049 -0
- devtorch_core/metrics/__init__.py +35 -0
- devtorch_core/metrics/aggregate.py +215 -0
- devtorch_core/metrics/calibrate.py +198 -0
- devtorch_core/metrics/calibration.py +125 -0
- devtorch_core/metrics/credibility.py +288 -0
- devtorch_core/metrics/delivery_time.py +70 -0
- devtorch_core/metrics/dhs.py +126 -0
- devtorch_core/metrics/mcs.py +96 -0
- devtorch_core/metrics/roi.py +88 -0
- devtorch_core/metrics/session_writer.py +81 -0
- devtorch_core/metrics/shadow_ai.py +117 -0
- devtorch_core/metrics/sprint_writer.py +243 -0
- devtorch_core/observability/__init__.py +78 -0
- devtorch_core/observability/datadog.py +157 -0
- devtorch_core/observability/formatter.py +119 -0
- devtorch_core/observability/report.py +264 -0
- devtorch_core/observability/servicenow.py +147 -0
- devtorch_core/observability/splunk.py +218 -0
- devtorch_core/observability/webhook.py +227 -0
- devtorch_core/parser/__init__.py +30 -0
- devtorch_core/parser/blocks.py +216 -0
- devtorch_core/parser/inference.py +159 -0
- devtorch_core/parser/thinking.py +112 -0
- devtorch_core/projects.py +169 -0
- devtorch_core/prompt_artifact.py +76 -0
- devtorch_core/proxy/__init__.py +9 -0
- devtorch_core/proxy/routes/__init__.py +1 -0
- devtorch_core/proxy/routes/anthropic.py +264 -0
- devtorch_core/proxy/routes/azure_openai.py +336 -0
- devtorch_core/proxy/routes/gemini.py +331 -0
- devtorch_core/proxy/routes/groq.py +284 -0
- devtorch_core/proxy/routes/ollama.py +279 -0
- devtorch_core/proxy/routes/openai.py +287 -0
- devtorch_core/proxy/server.py +356 -0
- devtorch_core/query/__init__.py +15 -0
- devtorch_core/query/grep.py +181 -0
- devtorch_core/query/hybrid.py +86 -0
- devtorch_core/query/semantic.py +157 -0
- devtorch_core/rdp.py +105 -0
- devtorch_core/reasoning/__init__.py +4 -0
- devtorch_core/reasoning/entry.py +31 -0
- devtorch_core/reasoning/store.py +122 -0
- devtorch_core/reasoning_plus/__init__.py +70 -0
- devtorch_core/reasoning_plus/augmenter.py +326 -0
- devtorch_core/reasoning_plus/capture.py +51 -0
- devtorch_core/reasoning_plus/config.py +256 -0
- devtorch_core/reasoning_plus/context.py +262 -0
- devtorch_core/reasoning_plus/learning/__init__.py +72 -0
- devtorch_core/reasoning_plus/learning/analytics.py +141 -0
- devtorch_core/reasoning_plus/learning/api.py +313 -0
- devtorch_core/reasoning_plus/learning/chain.py +285 -0
- devtorch_core/reasoning_plus/learning/composer.py +74 -0
- devtorch_core/reasoning_plus/learning/cross_project.py +234 -0
- devtorch_core/reasoning_plus/learning/embeddings.py +209 -0
- devtorch_core/reasoning_plus/learning/extractor.py +207 -0
- devtorch_core/reasoning_plus/learning/models.py +116 -0
- devtorch_core/reasoning_plus/learning/provenance.py +126 -0
- devtorch_core/reasoning_plus/learning/recorder.py +81 -0
- devtorch_core/reasoning_plus/learning/relevance.py +122 -0
- devtorch_core/reasoning_plus/learning/state.py +86 -0
- devtorch_core/reasoning_plus/learning/store.py +160 -0
- devtorch_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
- devtorch_core/reasoning_plus/prompt.py +90 -0
- devtorch_core/rep.py +134 -0
- devtorch_core/rep_network/__init__.py +25 -0
- devtorch_core/rep_network/merge.py +70 -0
- devtorch_core/rep_network/node.py +137 -0
- devtorch_core/rep_network/server.py +140 -0
- devtorch_core/rep_network/sync.py +207 -0
- devtorch_core/sensitivity.py +182 -0
- devtorch_core/serve.py +258 -0
- devtorch_core/session/__init__.py +39 -0
- devtorch_core/session/disagreement.py +188 -0
- devtorch_core/session/models.py +114 -0
- devtorch_core/session/orchestrator.py +182 -0
- devtorch_core/session/planner.py +169 -0
- devtorch_core/session/simulator.py +132 -0
- devtorch_core/signing.py +290 -0
- devtorch_core/sis.py +197 -0
- devtorch_core/storage.py +308 -0
- devtorch_core/templates/__init__.py +6 -0
- devtorch_core/templates/engine.py +122 -0
- devtorch_core/templates/go.py +18 -0
- devtorch_core/templates/infra.py +19 -0
- devtorch_core/templates/library/__init__.py +18 -0
- devtorch_core/templates/library/api_design.md +27 -0
- devtorch_core/templates/library/bug_fix.md +27 -0
- devtorch_core/templates/library/decision_record.md +27 -0
- devtorch_core/templates/library/engine.py +228 -0
- devtorch_core/templates/library/security_review.md +30 -0
- devtorch_core/templates/python.py +19 -0
- devtorch_core/templates/react.py +18 -0
- devtorch_core/templates/typescript.py +18 -0
- devtorch_core/theta.py +221 -0
- devtorch_core/theta_synthesis.py +268 -0
- devtorch_core/topics.py +320 -0
- devtorch_core/variance.py +219 -0
- devtorch_core/wrapper/__init__.py +52 -0
- devtorch_core/wrapper/anthropic.py +487 -0
- devtorch_core/wrapper/base.py +562 -0
- devtorch_core/wrapper/bedrock.py +342 -0
- devtorch_core/wrapper/gemini.py +422 -0
- devtorch_core/wrapper/ollama.py +527 -0
- devtorch_core/wrapper/openai.py +461 -0
- devtorch_core-3.0.1.dist-info/METADATA +867 -0
- devtorch_core-3.0.1.dist-info/RECORD +193 -0
- devtorch_core-3.0.1.dist-info/WHEEL +5 -0
- devtorch_core-3.0.1.dist-info/entry_points.txt +2 -0
- devtorch_core-3.0.1.dist-info/top_level.txt +1 -0
devtorch_core/deltaf.py
ADDED
|
@@ -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,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})"
|