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
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sprint 3 – Ωᵢ capability declaration (v2.1 A1 fields) + PST runner.
|
|
3
|
+
|
|
4
|
+
OmegaCapability is the per-agent capability record that must be set and valid
|
|
5
|
+
before a node may participate in Textual Mode. It captures the empirical
|
|
6
|
+
Lipschitz bound (L̂), calibration metadata, and PST (Prompt Stability Test)
|
|
7
|
+
results. CapabilityStore handles on-disk persistence, and PSTRunner provides
|
|
8
|
+
the evaluation harness for measuring output stability.
|
|
9
|
+
|
|
10
|
+
A1 Textual Mode gate (RACP spec v2.1):
|
|
11
|
+
Textual Mode is allowed iff ALL of the following hold:
|
|
12
|
+
1. A valid Ωᵢ exists (not None / not stub format).
|
|
13
|
+
2. Calibration date is within expiry_days of today.
|
|
14
|
+
3. L̂ ≤ L_max (configurable; default = 2.0).
|
|
15
|
+
4. pst_status == "passed".
|
|
16
|
+
If any condition fails a CAPABILITY_DEGRADED event is emitted by the
|
|
17
|
+
GCCRepository layer, and the agent MUST fall back to Numerical-only /
|
|
18
|
+
deterministic mode.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import dataclasses
|
|
24
|
+
import datetime as _dt
|
|
25
|
+
import json
|
|
26
|
+
from collections import Counter
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Callable, List, Optional
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Constants
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
EXPIRY_DAYS_DEFAULT: int = 90 # Calibration expires after 90 days
|
|
36
|
+
L_MAX_DEFAULT: float = 2.0 # Default Lipschitz bound threshold
|
|
37
|
+
PST_SCORE_THRESHOLD: float = 0.80 # PST must score ≥ 80 % to pass
|
|
38
|
+
|
|
39
|
+
PST_STATUS_PASSED = "passed"
|
|
40
|
+
PST_STATUS_FAILED = "failed"
|
|
41
|
+
PST_STATUS_NOT_RUN = "not_run"
|
|
42
|
+
|
|
43
|
+
CAPABILITY_DEGRADED_EVENT = "CAPABILITY_DEGRADED"
|
|
44
|
+
OMEGA_SCHEMA_VERSION = "omega-v2.1"
|
|
45
|
+
|
|
46
|
+
ROLE_POLICY_FILENAME = "role_policy.json"
|
|
47
|
+
ROLES_SUBDIR = "roles"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# AgentRole dataclass
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
@dataclasses.dataclass
|
|
55
|
+
class AgentRole:
|
|
56
|
+
"""
|
|
57
|
+
Role-based policy for agent capabilities and concept access.
|
|
58
|
+
|
|
59
|
+
A role constrains what an agent may do based on its OmegaCapability:
|
|
60
|
+
- allowed_concepts: concept names the role may touch (empty means all).
|
|
61
|
+
- disallowed_concepts: concept names the role may never touch.
|
|
62
|
+
- required_capabilities: OmegaCapability field names that must be present.
|
|
63
|
+
- max_temperature: optional sampling-temperature ceiling.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
role_id: str
|
|
67
|
+
name: str
|
|
68
|
+
allowed_concepts: List[str]
|
|
69
|
+
disallowed_concepts: List[str]
|
|
70
|
+
required_capabilities: List[str]
|
|
71
|
+
max_temperature: Optional[float] = None
|
|
72
|
+
created_at: str = dataclasses.field(
|
|
73
|
+
default_factory=lambda: _dt.datetime.now(tz=_dt.timezone.utc).isoformat()
|
|
74
|
+
)
|
|
75
|
+
updated_at: str = dataclasses.field(
|
|
76
|
+
default_factory=lambda: _dt.datetime.now(tz=_dt.timezone.utc).isoformat()
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def __post_init__(self) -> None:
|
|
80
|
+
if not self.role_id.strip():
|
|
81
|
+
raise ValueError("role_id must not be empty.")
|
|
82
|
+
if not self.name.strip():
|
|
83
|
+
raise ValueError("name must not be empty.")
|
|
84
|
+
if self.max_temperature is not None and not (0.0 <= self.max_temperature <= 2.0):
|
|
85
|
+
raise ValueError("max_temperature must be in [0.0, 2.0].")
|
|
86
|
+
|
|
87
|
+
def to_dict(self) -> dict:
|
|
88
|
+
return dataclasses.asdict(self)
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def from_dict(cls, d: dict) -> "AgentRole":
|
|
92
|
+
known = {f.name for f in dataclasses.fields(cls)}
|
|
93
|
+
return cls(**{k: v for k, v in d.items() if k in known})
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# RolePolicy dataclass
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
@dataclasses.dataclass
|
|
101
|
+
class RolePolicy:
|
|
102
|
+
"""Collection of AgentRole objects with a default_role identifier."""
|
|
103
|
+
|
|
104
|
+
roles: List[AgentRole]
|
|
105
|
+
default_role: str
|
|
106
|
+
created_at: str = dataclasses.field(
|
|
107
|
+
default_factory=lambda: _dt.datetime.now(tz=_dt.timezone.utc).isoformat()
|
|
108
|
+
)
|
|
109
|
+
updated_at: str = dataclasses.field(
|
|
110
|
+
default_factory=lambda: _dt.datetime.now(tz=_dt.timezone.utc).isoformat()
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def __post_init__(self) -> None:
|
|
114
|
+
if not self.default_role.strip():
|
|
115
|
+
raise ValueError("default_role must not be empty.")
|
|
116
|
+
role_ids = {r.role_id for r in self.roles}
|
|
117
|
+
if self.default_role not in role_ids:
|
|
118
|
+
raise ValueError(
|
|
119
|
+
f"default_role '{self.default_role}' is not in roles {role_ids}."
|
|
120
|
+
)
|
|
121
|
+
if len(role_ids) != len(self.roles):
|
|
122
|
+
raise ValueError("role_ids must be unique within a RolePolicy.")
|
|
123
|
+
|
|
124
|
+
def to_dict(self) -> dict:
|
|
125
|
+
return {
|
|
126
|
+
"roles": [r.to_dict() for r in self.roles],
|
|
127
|
+
"default_role": self.default_role,
|
|
128
|
+
"created_at": self.created_at,
|
|
129
|
+
"updated_at": self.updated_at,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
def from_dict(cls, d: dict) -> "RolePolicy":
|
|
134
|
+
roles = [AgentRole.from_dict(r) for r in d.get("roles", [])]
|
|
135
|
+
known = {f.name for f in dataclasses.fields(cls)}
|
|
136
|
+
return cls(
|
|
137
|
+
roles=roles,
|
|
138
|
+
default_role=d.get("default_role", ""),
|
|
139
|
+
**{k: v for k, v in d.items() if k in known and k not in ("roles", "default_role")},
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def get_role(self, role_id: str) -> Optional[AgentRole]:
|
|
143
|
+
for role in self.roles:
|
|
144
|
+
if role.role_id == role_id:
|
|
145
|
+
return role
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
# OmegaCapability dataclass
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
@dataclasses.dataclass
|
|
154
|
+
class OmegaCapability:
|
|
155
|
+
"""
|
|
156
|
+
Per-agent capability declaration (Ωᵢ) for RACP v2.1 A1 compliance.
|
|
157
|
+
|
|
158
|
+
Required A1 fields
|
|
159
|
+
------------------
|
|
160
|
+
agent_id : Unique agent identifier.
|
|
161
|
+
lipschitz_bound : Empirical L̂ from calibration (must be ≤ L_max).
|
|
162
|
+
calibration_dataset_hash: SHA-256 hex digest of the calibration corpus.
|
|
163
|
+
embedding_model : Name/version of the embedding model used.
|
|
164
|
+
calibration_date : ISO-8601 date (YYYY-MM-DD) of the calibration run.
|
|
165
|
+
temperature_used : Sampling temperature used during calibration [0, 2].
|
|
166
|
+
|
|
167
|
+
Optional A1 fields
|
|
168
|
+
------------------
|
|
169
|
+
lipschitz_p95 : 95th-percentile Lipschitz estimate.
|
|
170
|
+
|
|
171
|
+
PST fields (populated by PSTRunner)
|
|
172
|
+
------------------------------------
|
|
173
|
+
pst_status : "passed" | "failed" | "not_run".
|
|
174
|
+
pst_score : Mean stability score [0, 1].
|
|
175
|
+
pst_n_prompts : Number of prompts used.
|
|
176
|
+
pst_n_repetitions : Number of repetitions per prompt.
|
|
177
|
+
|
|
178
|
+
Lifecycle
|
|
179
|
+
---------
|
|
180
|
+
expiry_days : Days after calibration_date before the declaration expires.
|
|
181
|
+
schema_version : Fixed to "omega-v2.1".
|
|
182
|
+
created_at / updated_at : ISO-8601 UTC timestamps.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
# Core A1 fields (required)
|
|
186
|
+
agent_id: str
|
|
187
|
+
lipschitz_bound: float
|
|
188
|
+
calibration_dataset_hash: str
|
|
189
|
+
embedding_model: str
|
|
190
|
+
calibration_date: str
|
|
191
|
+
temperature_used: float
|
|
192
|
+
|
|
193
|
+
# Optional A1 field
|
|
194
|
+
lipschitz_p95: Optional[float] = None
|
|
195
|
+
|
|
196
|
+
# PST results
|
|
197
|
+
pst_status: str = PST_STATUS_NOT_RUN
|
|
198
|
+
pst_score: Optional[float] = None
|
|
199
|
+
pst_n_prompts: int = 0
|
|
200
|
+
pst_n_repetitions: int = 0
|
|
201
|
+
|
|
202
|
+
# Lifecycle
|
|
203
|
+
expiry_days: int = EXPIRY_DAYS_DEFAULT
|
|
204
|
+
|
|
205
|
+
# Metadata
|
|
206
|
+
schema_version: str = OMEGA_SCHEMA_VERSION
|
|
207
|
+
created_at: str = dataclasses.field(
|
|
208
|
+
default_factory=lambda: _dt.datetime.now(tz=_dt.timezone.utc).isoformat()
|
|
209
|
+
)
|
|
210
|
+
updated_at: str = dataclasses.field(
|
|
211
|
+
default_factory=lambda: _dt.datetime.now(tz=_dt.timezone.utc).isoformat()
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def __post_init__(self) -> None:
|
|
215
|
+
if self.pst_status not in (PST_STATUS_PASSED, PST_STATUS_FAILED, PST_STATUS_NOT_RUN):
|
|
216
|
+
raise ValueError(
|
|
217
|
+
f"pst_status must be 'passed', 'failed', or 'not_run', "
|
|
218
|
+
f"got '{self.pst_status}'"
|
|
219
|
+
)
|
|
220
|
+
if self.lipschitz_bound < 0:
|
|
221
|
+
raise ValueError("lipschitz_bound must be non-negative.")
|
|
222
|
+
if not (0.0 <= self.temperature_used <= 2.0):
|
|
223
|
+
raise ValueError("temperature_used must be in [0.0, 2.0].")
|
|
224
|
+
if not self.agent_id.strip():
|
|
225
|
+
raise ValueError("agent_id must not be empty.")
|
|
226
|
+
|
|
227
|
+
def to_dict(self) -> dict:
|
|
228
|
+
return dataclasses.asdict(self)
|
|
229
|
+
|
|
230
|
+
@classmethod
|
|
231
|
+
def from_dict(cls, d: dict) -> "OmegaCapability":
|
|
232
|
+
known = {f.name for f in dataclasses.fields(cls)}
|
|
233
|
+
return cls(**{k: v for k, v in d.items() if k in known})
|
|
234
|
+
|
|
235
|
+
# ------------------------------------------------------------------
|
|
236
|
+
# A1 gate logic
|
|
237
|
+
# ------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
def is_expired(self) -> bool:
|
|
240
|
+
"""True if the calibration_date is older than expiry_days from today."""
|
|
241
|
+
try:
|
|
242
|
+
cal = _dt.date.fromisoformat(self.calibration_date[:10])
|
|
243
|
+
except (ValueError, TypeError):
|
|
244
|
+
return True # Unparseable → treat as expired
|
|
245
|
+
return (_dt.date.today() - cal).days > self.expiry_days
|
|
246
|
+
|
|
247
|
+
def textual_mode_allowed(
|
|
248
|
+
self, l_max: float = L_MAX_DEFAULT
|
|
249
|
+
) -> tuple[bool, List[str]]:
|
|
250
|
+
"""
|
|
251
|
+
Return (allowed, [failure_reasons]).
|
|
252
|
+
|
|
253
|
+
All three conditions must hold for Textual Mode to be permitted:
|
|
254
|
+
- Calibration not expired.
|
|
255
|
+
- L̂ ≤ L_max.
|
|
256
|
+
- PST status is "passed".
|
|
257
|
+
"""
|
|
258
|
+
reasons: List[str] = []
|
|
259
|
+
if self.is_expired():
|
|
260
|
+
reasons.append(
|
|
261
|
+
f"Calibration expired "
|
|
262
|
+
f"(calibration_date={self.calibration_date}, expiry_days={self.expiry_days})."
|
|
263
|
+
)
|
|
264
|
+
if self.lipschitz_bound > l_max:
|
|
265
|
+
reasons.append(
|
|
266
|
+
f"L\u0302={self.lipschitz_bound} exceeds L_max={l_max}."
|
|
267
|
+
)
|
|
268
|
+
if self.pst_status != PST_STATUS_PASSED:
|
|
269
|
+
reasons.append(
|
|
270
|
+
f"PST status is '{self.pst_status}' (must be '{PST_STATUS_PASSED}')."
|
|
271
|
+
)
|
|
272
|
+
return len(reasons) == 0, reasons
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# ---------------------------------------------------------------------------
|
|
276
|
+
# CapabilityValidationResult
|
|
277
|
+
# ---------------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
@dataclasses.dataclass
|
|
280
|
+
class CapabilityValidationResult:
|
|
281
|
+
"""Summary returned by CapabilityStore.validate()."""
|
|
282
|
+
|
|
283
|
+
valid: bool
|
|
284
|
+
textual_mode_allowed: bool
|
|
285
|
+
reasons: List[str]
|
|
286
|
+
agent_id: str
|
|
287
|
+
pst_status: str
|
|
288
|
+
lipschitz_bound: float # float("inf") if no capability exists
|
|
289
|
+
is_expired: bool
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# ---------------------------------------------------------------------------
|
|
293
|
+
# CapabilityStore
|
|
294
|
+
# ---------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
class CapabilityStore:
|
|
297
|
+
"""
|
|
298
|
+
Reads and writes OmegaCapability to `.GCC/capabilities/omega.json`.
|
|
299
|
+
|
|
300
|
+
The legacy stub format (`schema == "omega-stub-v0"`) is recognised but
|
|
301
|
+
treated as "no capability set", so callers get a clear signal to run
|
|
302
|
+
`devtorch capability set` before proceeding.
|
|
303
|
+
"""
|
|
304
|
+
|
|
305
|
+
def __init__(self, capabilities_dir: Path) -> None:
|
|
306
|
+
self.dir = capabilities_dir
|
|
307
|
+
self.omega_path = capabilities_dir / "omega.json"
|
|
308
|
+
self.roles_dir = capabilities_dir / ROLES_SUBDIR
|
|
309
|
+
self.policy_path = capabilities_dir / ROLE_POLICY_FILENAME
|
|
310
|
+
|
|
311
|
+
def save(self, cap: OmegaCapability) -> None:
|
|
312
|
+
self.dir.mkdir(parents=True, exist_ok=True)
|
|
313
|
+
self.omega_path.write_text(
|
|
314
|
+
json.dumps(cap.to_dict(), indent=2, sort_keys=True) + "\n",
|
|
315
|
+
encoding="utf-8",
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
def load(self) -> Optional[OmegaCapability]:
|
|
319
|
+
if not self.omega_path.exists():
|
|
320
|
+
return None
|
|
321
|
+
try:
|
|
322
|
+
data = json.loads(self.omega_path.read_text(encoding="utf-8"))
|
|
323
|
+
except (json.JSONDecodeError, OSError):
|
|
324
|
+
return None
|
|
325
|
+
# Detect legacy stub format from Sprint 0
|
|
326
|
+
if data.get("schema") == "omega-stub-v0":
|
|
327
|
+
return None
|
|
328
|
+
try:
|
|
329
|
+
return OmegaCapability.from_dict(data)
|
|
330
|
+
except (TypeError, ValueError):
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
def validate(self, l_max: float = L_MAX_DEFAULT) -> CapabilityValidationResult:
|
|
334
|
+
"""Run A1 gate checks and return a structured result."""
|
|
335
|
+
cap = self.load()
|
|
336
|
+
if cap is None:
|
|
337
|
+
return CapabilityValidationResult(
|
|
338
|
+
valid=False,
|
|
339
|
+
textual_mode_allowed=False,
|
|
340
|
+
reasons=["No valid capability declaration found (Ωᵢ not set)."],
|
|
341
|
+
agent_id="<none>",
|
|
342
|
+
pst_status=PST_STATUS_NOT_RUN,
|
|
343
|
+
lipschitz_bound=float("inf"),
|
|
344
|
+
is_expired=True,
|
|
345
|
+
)
|
|
346
|
+
allowed, reasons = cap.textual_mode_allowed(l_max=l_max)
|
|
347
|
+
return CapabilityValidationResult(
|
|
348
|
+
valid=allowed,
|
|
349
|
+
textual_mode_allowed=allowed,
|
|
350
|
+
reasons=reasons,
|
|
351
|
+
agent_id=cap.agent_id,
|
|
352
|
+
pst_status=cap.pst_status,
|
|
353
|
+
lipschitz_bound=cap.lipschitz_bound,
|
|
354
|
+
is_expired=cap.is_expired(),
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# ------------------------------------------------------------------
|
|
358
|
+
# Role / policy persistence
|
|
359
|
+
# ------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
def save_role(self, role: AgentRole) -> None:
|
|
362
|
+
self.roles_dir.mkdir(parents=True, exist_ok=True)
|
|
363
|
+
path = self.roles_dir / f"{role.role_id}.json"
|
|
364
|
+
path.write_text(
|
|
365
|
+
json.dumps(role.to_dict(), indent=2, sort_keys=True) + "\n",
|
|
366
|
+
encoding="utf-8",
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
def load_role(self, role_id: str) -> Optional[AgentRole]:
|
|
370
|
+
path = self.roles_dir / f"{role_id}.json"
|
|
371
|
+
if not path.exists():
|
|
372
|
+
return None
|
|
373
|
+
try:
|
|
374
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
375
|
+
except (json.JSONDecodeError, OSError):
|
|
376
|
+
return None
|
|
377
|
+
try:
|
|
378
|
+
return AgentRole.from_dict(data)
|
|
379
|
+
except (TypeError, ValueError):
|
|
380
|
+
return None
|
|
381
|
+
|
|
382
|
+
def list_roles(self) -> List[str]:
|
|
383
|
+
if not self.roles_dir.exists():
|
|
384
|
+
return []
|
|
385
|
+
roles = []
|
|
386
|
+
for path in self.roles_dir.iterdir():
|
|
387
|
+
if path.is_file() and path.suffix == ".json":
|
|
388
|
+
roles.append(path.stem)
|
|
389
|
+
return sorted(roles)
|
|
390
|
+
|
|
391
|
+
def save_policy(self, policy: RolePolicy) -> None:
|
|
392
|
+
self.dir.mkdir(parents=True, exist_ok=True)
|
|
393
|
+
self.policy_path.write_text(
|
|
394
|
+
json.dumps(policy.to_dict(), indent=2, sort_keys=True) + "\n",
|
|
395
|
+
encoding="utf-8",
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
def load_policy(self) -> Optional[RolePolicy]:
|
|
399
|
+
if not self.policy_path.exists():
|
|
400
|
+
return None
|
|
401
|
+
try:
|
|
402
|
+
data = json.loads(self.policy_path.read_text(encoding="utf-8"))
|
|
403
|
+
except (json.JSONDecodeError, OSError):
|
|
404
|
+
return None
|
|
405
|
+
try:
|
|
406
|
+
return RolePolicy.from_dict(data)
|
|
407
|
+
except (TypeError, ValueError):
|
|
408
|
+
return None
|
|
409
|
+
|
|
410
|
+
def check_role_policy(
|
|
411
|
+
self,
|
|
412
|
+
cap: OmegaCapability,
|
|
413
|
+
role: AgentRole,
|
|
414
|
+
concepts_used: List[str],
|
|
415
|
+
) -> List[str]:
|
|
416
|
+
"""
|
|
417
|
+
Return a list of human-readable reasons if `cap` does not satisfy the
|
|
418
|
+
constraints of `role` for `concepts_used`. An empty list means allowed.
|
|
419
|
+
"""
|
|
420
|
+
reasons: List[str] = []
|
|
421
|
+
|
|
422
|
+
# Required capability fields
|
|
423
|
+
for field in role.required_capabilities:
|
|
424
|
+
if not hasattr(cap, field):
|
|
425
|
+
reasons.append(f"Missing required capability field '{field}'.")
|
|
426
|
+
continue
|
|
427
|
+
value = getattr(cap, field)
|
|
428
|
+
if value is None:
|
|
429
|
+
reasons.append(f"Required capability field '{field}' is unset.")
|
|
430
|
+
elif isinstance(value, (int, float)) and value == 0:
|
|
431
|
+
# zero numeric fields are considered present
|
|
432
|
+
pass
|
|
433
|
+
elif isinstance(value, str) and not value.strip():
|
|
434
|
+
reasons.append(f"Required capability field '{field}' is empty.")
|
|
435
|
+
|
|
436
|
+
# Temperature ceiling
|
|
437
|
+
if role.max_temperature is not None and cap.temperature_used > role.max_temperature:
|
|
438
|
+
reasons.append(
|
|
439
|
+
f"temperature_used={cap.temperature_used} exceeds "
|
|
440
|
+
f"role max_temperature={role.max_temperature}."
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
# Disallowed concepts
|
|
444
|
+
for concept in concepts_used:
|
|
445
|
+
if concept in role.disallowed_concepts:
|
|
446
|
+
reasons.append(f"Role may not touch disallowed concept '{concept}'.")
|
|
447
|
+
|
|
448
|
+
# Allowed-concept whitelist
|
|
449
|
+
if role.allowed_concepts:
|
|
450
|
+
allowed = set(role.allowed_concepts)
|
|
451
|
+
for concept in concepts_used:
|
|
452
|
+
if concept not in allowed:
|
|
453
|
+
reasons.append(
|
|
454
|
+
f"Role is not allowed to touch concept '{concept}'."
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
return reasons
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def apply_default_policy(store: CapabilityStore) -> RolePolicy:
|
|
461
|
+
"""
|
|
462
|
+
Create a sensible default role policy and persist it through `store`.
|
|
463
|
+
|
|
464
|
+
Returns the policy object.
|
|
465
|
+
"""
|
|
466
|
+
roles = [
|
|
467
|
+
AgentRole(
|
|
468
|
+
role_id="architect",
|
|
469
|
+
name="System Architect",
|
|
470
|
+
allowed_concepts=["design", "architecture", "security", "api"],
|
|
471
|
+
disallowed_concepts=["payments", "secrets", "pii"],
|
|
472
|
+
required_capabilities=["lipschitz_bound", "embedding_model"],
|
|
473
|
+
max_temperature=1.0,
|
|
474
|
+
),
|
|
475
|
+
AgentRole(
|
|
476
|
+
role_id="implementer",
|
|
477
|
+
name="Implementer",
|
|
478
|
+
allowed_concepts=["code", "tests", "api", "schema"],
|
|
479
|
+
disallowed_concepts=["payments", "secrets", "pii"],
|
|
480
|
+
required_capabilities=["lipschitz_bound"],
|
|
481
|
+
max_temperature=1.2,
|
|
482
|
+
),
|
|
483
|
+
AgentRole(
|
|
484
|
+
role_id="reviewer",
|
|
485
|
+
name="Reviewer",
|
|
486
|
+
allowed_concepts=["code", "tests", "security", "api", "schema"],
|
|
487
|
+
disallowed_concepts=["payments", "secrets", "pii"],
|
|
488
|
+
required_capabilities=["lipschitz_bound", "lipschitz_p95", "pst_status"],
|
|
489
|
+
max_temperature=0.5,
|
|
490
|
+
),
|
|
491
|
+
AgentRole(
|
|
492
|
+
role_id="ops",
|
|
493
|
+
name="Operations",
|
|
494
|
+
allowed_concepts=["infrastructure", "deployment", "config"],
|
|
495
|
+
disallowed_concepts=["pii", "payments", "auth"],
|
|
496
|
+
required_capabilities=["lipschitz_bound"],
|
|
497
|
+
max_temperature=0.3,
|
|
498
|
+
),
|
|
499
|
+
]
|
|
500
|
+
policy = RolePolicy(roles=roles, default_role="implementer")
|
|
501
|
+
for role in roles:
|
|
502
|
+
store.save_role(role)
|
|
503
|
+
store.save_policy(policy)
|
|
504
|
+
return policy
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
# ---------------------------------------------------------------------------
|
|
508
|
+
# PST runner
|
|
509
|
+
# ---------------------------------------------------------------------------
|
|
510
|
+
|
|
511
|
+
@dataclasses.dataclass
|
|
512
|
+
class PSTReport:
|
|
513
|
+
"""Structured output from a PSTRunner.run() call."""
|
|
514
|
+
|
|
515
|
+
agent_id: str
|
|
516
|
+
n_prompts: int
|
|
517
|
+
n_repetitions: int
|
|
518
|
+
pst_score: float
|
|
519
|
+
per_prompt_scores: List[float]
|
|
520
|
+
status: str # "passed" | "failed"
|
|
521
|
+
threshold: float
|
|
522
|
+
run_at: str
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
# ---------------------------------------------------------------------------
|
|
526
|
+
# A1 Round Cap (S9)
|
|
527
|
+
# ---------------------------------------------------------------------------
|
|
528
|
+
|
|
529
|
+
T_MAX_MULTIPLIER: float = 2.0 # T_max = T_MAX_MULTIPLIER × T_txt
|
|
530
|
+
CONVERGENCE_TIMEOUT_ROUNDS: int = 20 # Absolute fallback cap
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
@dataclasses.dataclass
|
|
534
|
+
class RoundCapResult:
|
|
535
|
+
allowed: bool # whether next round is allowed
|
|
536
|
+
current_round: int
|
|
537
|
+
t_max: int
|
|
538
|
+
reason: str # human-readable explanation
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def check_round_cap(
|
|
542
|
+
current_round: int,
|
|
543
|
+
t_txt: int,
|
|
544
|
+
t_max_multiplier: float = T_MAX_MULTIPLIER,
|
|
545
|
+
absolute_cap: int = CONVERGENCE_TIMEOUT_ROUNDS,
|
|
546
|
+
) -> RoundCapResult:
|
|
547
|
+
"""
|
|
548
|
+
Check whether a Textual Mode round is permitted under A1 round cap.
|
|
549
|
+
T_max = floor(t_max_multiplier × t_txt), capped at absolute_cap.
|
|
550
|
+
Returns RoundCapResult with allowed=False and CONVERGENCE_TIMEOUT reason
|
|
551
|
+
if current_round > T_max.
|
|
552
|
+
"""
|
|
553
|
+
t_max = min(int(t_max_multiplier * t_txt), absolute_cap)
|
|
554
|
+
if current_round > t_max:
|
|
555
|
+
return RoundCapResult(
|
|
556
|
+
allowed=False,
|
|
557
|
+
current_round=current_round,
|
|
558
|
+
t_max=t_max,
|
|
559
|
+
reason=f"CONVERGENCE_TIMEOUT: round {current_round} exceeds T_max={t_max} "
|
|
560
|
+
f"(= {t_max_multiplier}×T_txt={t_txt}, cap={absolute_cap})",
|
|
561
|
+
)
|
|
562
|
+
return RoundCapResult(
|
|
563
|
+
allowed=True,
|
|
564
|
+
current_round=current_round,
|
|
565
|
+
t_max=t_max,
|
|
566
|
+
reason="ok",
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
class PSTRunner:
|
|
571
|
+
"""
|
|
572
|
+
Prompt Stability Test (PST) harness.
|
|
573
|
+
|
|
574
|
+
Runs `n_prompts` canonical prompts × `n_repetitions` repetitions through
|
|
575
|
+
a caller-supplied `run_fn(prompt: str, rep: int) -> str`. The per-prompt
|
|
576
|
+
stability score is the fraction of repetitions that produced the most
|
|
577
|
+
common output. The overall PST_score is the mean across all prompts.
|
|
578
|
+
|
|
579
|
+
Example (deterministic stub):
|
|
580
|
+
runner = PSTRunner(
|
|
581
|
+
agent_id="agent-1",
|
|
582
|
+
run_fn=lambda p, r: "always the same",
|
|
583
|
+
)
|
|
584
|
+
report = runner.run() # pst_score == 1.0, status == "passed"
|
|
585
|
+
|
|
586
|
+
The OSS-local mode intentionally does *not* call an external LLM; callers
|
|
587
|
+
supply their own run_fn, which can wrap any LLM client or a local stub.
|
|
588
|
+
"""
|
|
589
|
+
|
|
590
|
+
DEFAULT_PROMPTS: List[str] = [
|
|
591
|
+
"Summarise the current reasoning context in one sentence.",
|
|
592
|
+
"What is the primary constraint in the current decision?",
|
|
593
|
+
"List the top three risks in the current plan.",
|
|
594
|
+
"Is the current approach deterministic? Answer yes or no.",
|
|
595
|
+
"What data is marked PRIVATE in the current context?",
|
|
596
|
+
]
|
|
597
|
+
|
|
598
|
+
def __init__(
|
|
599
|
+
self,
|
|
600
|
+
agent_id: str,
|
|
601
|
+
run_fn: Callable[[str, int], str],
|
|
602
|
+
n_prompts: int = 5,
|
|
603
|
+
n_repetitions: int = 3,
|
|
604
|
+
threshold: float = PST_SCORE_THRESHOLD,
|
|
605
|
+
prompts: Optional[List[str]] = None,
|
|
606
|
+
) -> None:
|
|
607
|
+
self.agent_id = agent_id
|
|
608
|
+
self.run_fn = run_fn
|
|
609
|
+
self.n_prompts = n_prompts
|
|
610
|
+
self.n_repetitions = n_repetitions
|
|
611
|
+
self.threshold = threshold
|
|
612
|
+
self.prompts = (prompts if prompts is not None else self.DEFAULT_PROMPTS)[: n_prompts]
|
|
613
|
+
|
|
614
|
+
def run(self) -> PSTReport:
|
|
615
|
+
"""Execute the PST and return a PSTReport."""
|
|
616
|
+
per_prompt_scores: List[float] = []
|
|
617
|
+
for prompt in self.prompts:
|
|
618
|
+
outputs = [self.run_fn(prompt, rep) for rep in range(self.n_repetitions)]
|
|
619
|
+
most_common_count = Counter(outputs).most_common(1)[0][1]
|
|
620
|
+
stability = most_common_count / self.n_repetitions
|
|
621
|
+
per_prompt_scores.append(stability)
|
|
622
|
+
|
|
623
|
+
pst_score = (
|
|
624
|
+
sum(per_prompt_scores) / len(per_prompt_scores)
|
|
625
|
+
if per_prompt_scores
|
|
626
|
+
else 0.0
|
|
627
|
+
)
|
|
628
|
+
status = PST_STATUS_PASSED if pst_score >= self.threshold else PST_STATUS_FAILED
|
|
629
|
+
|
|
630
|
+
return PSTReport(
|
|
631
|
+
agent_id=self.agent_id,
|
|
632
|
+
n_prompts=self.n_prompts,
|
|
633
|
+
n_repetitions=self.n_repetitions,
|
|
634
|
+
pst_score=round(pst_score, 6),
|
|
635
|
+
per_prompt_scores=[round(s, 6) for s in per_prompt_scores],
|
|
636
|
+
status=status,
|
|
637
|
+
threshold=self.threshold,
|
|
638
|
+
run_at=_dt.datetime.now(tz=_dt.timezone.utc).isoformat(),
|
|
639
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Cloud MCP server client configuration and IDE wiring."""
|