dir-core 0.3.0__tar.gz → 0.4.2__tar.gz

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 (79) hide show
  1. {dir_core-0.3.0 → dir_core-0.4.2}/PKG-INFO +15 -7
  2. {dir_core-0.3.0 → dir_core-0.4.2}/README.md +8 -6
  3. {dir_core-0.3.0 → dir_core-0.4.2}/pyproject.toml +3 -2
  4. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/__init__.py +17 -1
  5. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/agent_registry.py +91 -0
  6. dir_core-0.4.2/src/dir_core/amendments.py +137 -0
  7. dir_core-0.4.2/src/dir_core/contract_projection.py +238 -0
  8. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/data_types.py +10 -0
  9. dir_core-0.4.2/src/dir_core/dim.py +163 -0
  10. dir_core-0.4.2/src/dir_core/invariant_compile.py +194 -0
  11. dir_core-0.4.2/src/dir_core/invariants.py +216 -0
  12. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/models.py +78 -2
  13. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/runtime.py +73 -4
  14. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/utils/__init__.py +12 -3
  15. dir_core-0.4.2/src/dir_core/utils/logging_utils.py +78 -0
  16. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core.egg-info/PKG-INFO +15 -7
  17. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core.egg-info/SOURCES.txt +17 -0
  18. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core.egg-info/requires.txt +7 -0
  19. {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_agent_registry.py +25 -0
  20. dir_core-0.4.2/tests/test_contract_env.py +111 -0
  21. dir_core-0.4.2/tests/test_contract_studio.py +466 -0
  22. dir_core-0.4.2/tests/test_contract_tool.py +509 -0
  23. {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_dim_ttl.py +49 -3
  24. dir_core-0.4.2/tests/test_governance_context.py +49 -0
  25. dir_core-0.4.2/tests/test_governance_validation.py +154 -0
  26. dir_core-0.4.2/tests/test_insurance_agent.py +33 -0
  27. dir_core-0.4.2/tests/test_insurance_invariants.py +127 -0
  28. dir_core-0.4.2/tests/test_insurance_report.py +157 -0
  29. dir_core-0.4.2/tests/test_invariants.py +251 -0
  30. dir_core-0.4.2/tests/test_llm_json_parse.py +31 -0
  31. dir_core-0.4.2/tests/test_logging_utils.py +49 -0
  32. dir_core-0.4.2/tests/test_responsibility_adapter.py +69 -0
  33. dir_core-0.4.2/tests/test_runtime.py +226 -0
  34. dir_core-0.4.2/tests/test_samples_isolation.py +39 -0
  35. dir_core-0.3.0/src/dir_core/dim.py +0 -112
  36. dir_core-0.3.0/src/dir_core/utils/logging_utils.py +0 -27
  37. dir_core-0.3.0/tests/test_runtime.py +0 -85
  38. {dir_core-0.3.0 → dir_core-0.4.2}/LICENSE +0 -0
  39. {dir_core-0.3.0 → dir_core-0.4.2}/setup.cfg +0 -0
  40. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/arbitration.py +0 -0
  41. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/context_store.py +0 -0
  42. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/dfid.py +0 -0
  43. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/escalation.py +0 -0
  44. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/event_bus.py +0 -0
  45. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/idempotency.py +0 -0
  46. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/intent_retry.py +0 -0
  47. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/jit.py +0 -0
  48. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/ledger.py +0 -0
  49. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/lifecycle.py +0 -0
  50. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/pci.py +0 -0
  51. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/resource_lock.py +0 -0
  52. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/saga.py +0 -0
  53. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/storage/__init__.py +0 -0
  54. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/storage/base.py +0 -0
  55. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/storage/json_util.py +0 -0
  56. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/storage/memory.py +0 -0
  57. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/storage/schema.sql +0 -0
  58. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/storage/sqlite.py +0 -0
  59. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/utils/llm_client.py +0 -0
  60. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/wakeup.py +0 -0
  61. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core.egg-info/dependency_links.txt +0 -0
  62. {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core.egg-info/top_level.txt +0 -0
  63. {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_arbitration.py +0 -0
  64. {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_context_store.py +0 -0
  65. {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_decision_ledger_storage.py +0 -0
  66. {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_dfid.py +0 -0
  67. {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_escalation.py +0 -0
  68. {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_event_bus.py +0 -0
  69. {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_idempotency.py +0 -0
  70. {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_intent_retry.py +0 -0
  71. {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_jit.py +0 -0
  72. {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_ledger.py +0 -0
  73. {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_lifecycle.py +0 -0
  74. {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_ollama.py +0 -0
  75. {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_pci.py +0 -0
  76. {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_pg_repo.py +0 -0
  77. {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_resource_lock.py +0 -0
  78. {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_saga.py +0 -0
  79. {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_wakeup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dir-core
3
- Version: 0.3.0
3
+ Version: 0.4.2
4
4
  Summary: Decision Intelligence Runtime - deterministic Kernel Space for agentic AI
5
5
  Author-email: Artur Huk <artur.huk.it@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -33,6 +33,12 @@ Provides-Extra: samples
33
33
  Requires-Dist: markdown>=3.5; extra == "samples"
34
34
  Requires-Dist: httpx; extra == "samples"
35
35
  Requires-Dist: plotly; extra == "samples"
36
+ Requires-Dist: google-genai>=1.0.0; extra == "samples"
37
+ Provides-Extra: studio
38
+ Requires-Dist: fastapi>=0.110; extra == "studio"
39
+ Requires-Dist: uvicorn[standard]>=0.27; extra == "studio"
40
+ Requires-Dist: z3-solver>=4.12; extra == "studio"
41
+ Requires-Dist: python-dotenv>=1.0; extra == "studio"
36
42
  Provides-Extra: dev
37
43
  Requires-Dist: pytest>=7; extra == "dev"
38
44
  Requires-Dist: pytest-cov>=4; extra == "dev"
@@ -86,19 +92,21 @@ DIR proposes that many of these discussions can be viewed through a common archi
86
92
 
87
93
  DIR is built on the formal premise that an AI agent cannot be trusted to maintain system integrity probabilistically. Instead, the runtime ensures that the system never enters an **Illegal Decision State**.
88
94
 
89
- A decision is considered execution-safe—and allowed to pass through the runtime—only when five independent dimensions are simultaneously satisfied. We call this the **Legal Decision State (LDS)**:
95
+ <p align="center">
96
+ <img src="./assets/images/lds-octagon.jpg" alt="Legal Decision State Invariants" width="60%">
97
+ </p>
90
98
 
91
- $$LDS = C \wedge A \wedge I \wedge E \wedge T$$
99
+ A decision is considered execution-safe—and allowed to pass through the runtime—only when five independent dimensions are simultaneously satisfied. We call this the **Legal Decision State (LDS)**:
92
100
 
93
- ![Legal Decision State Invariants](./assets/images/lds-octagon.png)
101
+ $$LDS = A \wedge C \wedge T \wedge I \wedge E$$
94
102
 
95
- If any of these components is false ($\neg C \vee \neg A \vee \neg I \vee \neg E \vee \neg T$), the decision is structurally illegal and must be aborted. Every component in the DIR ecosystem exists exclusively to protect one of these invariants:
103
+ If any of these components is false ($\neg A \vee \neg C \vee \neg T \vee \neg I \vee \neg E$), the decision is structurally illegal and must be aborted. Every component in the DIR ecosystem exists exclusively to protect one of these invariants:
96
104
 
97
- - **Context (C):** Protected by the **Context Store** & **Context-as-Code (CaC)**. Ensures the agent is not acting on stale, hallucinated, or mission-drifted data.
98
105
  - **Authority (A):** Protected by **Responsibility-Oriented Agents (ROA)**. Ensures the agent's proposed action does not exceed its explicitly registered contractual limits.
106
+ - **Context (C):** Protected by the **Context Store** & **Context-as-Code (CaC)**. Ensures the agent is not acting on stale, hallucinated, or mission-drifted data.
107
+ - **Time (T):** Protected by **JIT State Verification** & **Drift Envelopes**. Prevents Time-of-Check to Time-of-Use (TOCTOU) races by ensuring reality hasn't shifted between LLM reasoning and API execution.
99
108
  - **Intent (I):** Protected by **SDS Grammars** & **DIM Schema Validation**. Ensures the action is syntactically valid and structurally executable.
100
109
  - **Evidence (E):** Protected by the **Evidence Governance Layer**. Detects "Compliant Lies" by ensuring the action is backed by deterministically reproducible reasoning.
101
- - **Time (T):** Protected by **JIT State Verification** & **Drift Envelopes**. Prevents Time-of-Check to Time-of-Use (TOCTOU) races by ensuring reality hasn't shifted between LLM reasoning and API execution.
102
110
 
103
111
  *The Decision Integrity Module (DIM) is the deterministic gatekeeper that mathematically enforces this equation before any side effect occurs.*
104
112
 
@@ -44,19 +44,21 @@ DIR proposes that many of these discussions can be viewed through a common archi
44
44
 
45
45
  DIR is built on the formal premise that an AI agent cannot be trusted to maintain system integrity probabilistically. Instead, the runtime ensures that the system never enters an **Illegal Decision State**.
46
46
 
47
- A decision is considered execution-safe—and allowed to pass through the runtime—only when five independent dimensions are simultaneously satisfied. We call this the **Legal Decision State (LDS)**:
47
+ <p align="center">
48
+ <img src="./assets/images/lds-octagon.jpg" alt="Legal Decision State Invariants" width="60%">
49
+ </p>
48
50
 
49
- $$LDS = C \wedge A \wedge I \wedge E \wedge T$$
51
+ A decision is considered execution-safe—and allowed to pass through the runtime—only when five independent dimensions are simultaneously satisfied. We call this the **Legal Decision State (LDS)**:
50
52
 
51
- ![Legal Decision State Invariants](./assets/images/lds-octagon.png)
53
+ $$LDS = A \wedge C \wedge T \wedge I \wedge E$$
52
54
 
53
- If any of these components is false ($\neg C \vee \neg A \vee \neg I \vee \neg E \vee \neg T$), the decision is structurally illegal and must be aborted. Every component in the DIR ecosystem exists exclusively to protect one of these invariants:
55
+ If any of these components is false ($\neg A \vee \neg C \vee \neg T \vee \neg I \vee \neg E$), the decision is structurally illegal and must be aborted. Every component in the DIR ecosystem exists exclusively to protect one of these invariants:
54
56
 
55
- - **Context (C):** Protected by the **Context Store** & **Context-as-Code (CaC)**. Ensures the agent is not acting on stale, hallucinated, or mission-drifted data.
56
57
  - **Authority (A):** Protected by **Responsibility-Oriented Agents (ROA)**. Ensures the agent's proposed action does not exceed its explicitly registered contractual limits.
58
+ - **Context (C):** Protected by the **Context Store** & **Context-as-Code (CaC)**. Ensures the agent is not acting on stale, hallucinated, or mission-drifted data.
59
+ - **Time (T):** Protected by **JIT State Verification** & **Drift Envelopes**. Prevents Time-of-Check to Time-of-Use (TOCTOU) races by ensuring reality hasn't shifted between LLM reasoning and API execution.
57
60
  - **Intent (I):** Protected by **SDS Grammars** & **DIM Schema Validation**. Ensures the action is syntactically valid and structurally executable.
58
61
  - **Evidence (E):** Protected by the **Evidence Governance Layer**. Detects "Compliant Lies" by ensuring the action is backed by deterministically reproducible reasoning.
59
- - **Time (T):** Protected by **JIT State Verification** & **Drift Envelopes**. Prevents Time-of-Check to Time-of-Use (TOCTOU) races by ensuring reality hasn't shifted between LLM reasoning and API execution.
60
62
 
61
63
  *The Decision Integrity Module (DIM) is the deterministic gatekeeper that mathematically enforces this equation before any side effect occurs.*
62
64
 
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  # dir-core keeps pip install name and import path aligned (frictionless adoption).
7
7
  name = "dir-core"
8
- version = "0.3.0"
8
+ version = "0.4.2"
9
9
  description = "Decision Intelligence Runtime - deterministic Kernel Space for agentic AI"
10
10
  readme = "README.md"
11
11
  license = "Apache-2.0"
@@ -35,7 +35,8 @@ postgres = ["psycopg[binary]>=3.1"]
35
35
  asyncpg = ["asyncpg>=0.29"]
36
36
  # Optional: Boxed Intelligence pattern (wrappers)
37
37
  frameworks = ["langchain-core", "crewai"]
38
- samples = ["markdown>=3.5", "httpx", "plotly"]
38
+ samples = ["markdown>=3.5", "httpx", "plotly", "google-genai>=1.0.0"]
39
+ studio = ["fastapi>=0.110", "uvicorn[standard]>=0.27", "z3-solver>=4.12", "python-dotenv>=1.0"]
39
40
  dev = [
40
41
  "pytest>=7",
41
42
  "pytest-cov>=4",
@@ -21,7 +21,7 @@ Custom storage backend example::
21
21
  registry = AgentRegistry(storage=MyPostgresStorage())
22
22
  """
23
23
 
24
- from .agent_registry import AgentRegistry, HandshakeResult
24
+ from .agent_registry import AgentRegistry, AmendmentResult, HandshakeResult
25
25
  from .arbitration import DEFAULT_PRIORITY_MATRIX, select_winner
26
26
  from .dim import validate_proposal
27
27
  from .data_types import (
@@ -80,6 +80,10 @@ from .models import (
80
80
  PolicyProposal,
81
81
  ProofCarryingIntent,
82
82
  CompensationAction,
83
+ ContractReleaseRef,
84
+ RuntimeContractProjection,
85
+ InvariantSpec,
86
+ ContractParameterAmendment,
83
87
  ResponsibilityContract,
84
88
  SelfCheckResult,
85
89
  )
@@ -90,6 +94,9 @@ from .pci import (
90
94
  hash_content,
91
95
  proposal_params_for_hash,
92
96
  )
97
+ from .contract_projection import project_contract, responsibility_contract_from_projection
98
+ from .invariants import evaluate_invariants
99
+ from .amendments import validate_amendment_patch
93
100
  from .wakeup import (
94
101
  WakeupPredicate,
95
102
  is_relevant_instrument,
@@ -214,6 +221,15 @@ __all__ = [
214
221
  "verify_drift",
215
222
  # Core models
216
223
  "ResponsibilityContract",
224
+ "ContractReleaseRef",
225
+ "RuntimeContractProjection",
226
+ "InvariantSpec",
227
+ "ContractParameterAmendment",
228
+ "AmendmentResult",
229
+ "project_contract",
230
+ "responsibility_contract_from_projection",
231
+ "evaluate_invariants",
232
+ "validate_amendment_patch",
217
233
  "PolicyProposal",
218
234
  "ExecutionIntent",
219
235
  # ROA Lifecycle models
@@ -14,6 +14,9 @@ from dataclasses import dataclass
14
14
  from typing import Any, Dict, List, Optional
15
15
 
16
16
  from .data_types import AgentRegistryStatus, HandshakeRejectionReason
17
+ from .amendments import validate_amendment_patch, resolve_expires_at
18
+ from .contract_projection import project_contract
19
+ from .models import ContractParameterAmendment, RuntimeContractProjection
17
20
  from .storage.base import AgentRegistryStorage
18
21
  from .storage.sqlite import SqliteAgentRegistryStorage
19
22
 
@@ -31,6 +34,15 @@ class HandshakeResult:
31
34
  reason: Optional[str] = None
32
35
 
33
36
 
37
+ @dataclass
38
+ class AmendmentResult:
39
+ """Result of applying a human-gated parameter amendment."""
40
+
41
+ accepted: bool
42
+ reason: Optional[str] = None
43
+ projection: Optional[RuntimeContractProjection] = None
44
+
45
+
34
46
  def _parse_version(v: str) -> Optional[tuple]:
35
47
  """Parse semver string to (major, minor, patch)."""
36
48
  m = SEMVER_RE.match(v.strip())
@@ -121,6 +133,22 @@ class AgentRegistry:
121
133
  logger.info("Handshake: agent_id=%s ver=%s accepted", agent_id, agent_version)
122
134
  return HandshakeResult(accepted=True, session_token=token)
123
135
 
136
+ def register_projection(
137
+ self,
138
+ projection: RuntimeContractProjection,
139
+ agent_version: str,
140
+ priority: int = 0,
141
+ ) -> HandshakeResult:
142
+ """Register an approved RuntimeContractProjection explicitly."""
143
+ if projection.agent_id == "":
144
+ return HandshakeResult(accepted=False, reason="MISSING_AGENT_ID")
145
+ return self.handshake(
146
+ projection.agent_id,
147
+ projection.model_dump(mode="json"),
148
+ agent_version,
149
+ priority=priority,
150
+ )
151
+
124
152
  def get_schema(
125
153
  self, agent_id: str, schema_kind: Optional[str] = None
126
154
  ) -> Optional[dict]:
@@ -164,6 +192,69 @@ class AgentRegistry:
164
192
  rec = self._storage.get_agent(agent_id)
165
193
  return rec["contract"] if rec else None
166
194
 
195
+ def get_agent_projection(
196
+ self, agent_id: str
197
+ ) -> Optional[RuntimeContractProjection]:
198
+ """Retrieve the registered contract as an execution projection."""
199
+ contract = self.get_agent_contract(agent_id)
200
+ return project_contract(contract) if contract is not None else None
201
+
202
+ def apply_parameter_amendment(
203
+ self,
204
+ amendment: ContractParameterAmendment,
205
+ ) -> AmendmentResult:
206
+ """Apply a human-gated scalar patch to a runtime-amendable invariant."""
207
+ projection = self.get_agent_projection(amendment.agent_id)
208
+ if projection is None:
209
+ return AmendmentResult(accepted=False, reason="AGENT_NOT_FOUND")
210
+
211
+ target = next(
212
+ (spec for spec in projection.invariants if spec.id == amendment.invariant_id),
213
+ None,
214
+ )
215
+ if target is None:
216
+ return AmendmentResult(accepted=False, reason="INVARIANT_NOT_FOUND")
217
+
218
+ reject_reason = validate_amendment_patch(target, amendment.patch)
219
+ if reject_reason is not None:
220
+ return AmendmentResult(accepted=False, reason=reject_reason)
221
+
222
+ stored = dict(self.get_agent_contract(amendment.agent_id) or {})
223
+ if not stored.get("invariants"):
224
+ stored = projection.model_dump(mode="json")
225
+
226
+ amendments = list(stored.get("parameter_amendments") or [])
227
+ payload = amendment.model_dump(mode="json")
228
+ payload["expires_at"] = (
229
+ resolve_expires_at(amendment, target).isoformat()
230
+ if resolve_expires_at(amendment, target) is not None
231
+ else None
232
+ )
233
+ amendments.append(payload)
234
+ stored["parameter_amendments"] = amendments
235
+
236
+ rec = self._storage.get_agent(amendment.agent_id)
237
+ agent_version = rec.get("agent_version") if rec else "1.0.0"
238
+ if not agent_version:
239
+ agent_version = "1.0.0"
240
+
241
+ self._storage.upsert_agent(
242
+ agent_id=amendment.agent_id,
243
+ contract_json=json.dumps(stored),
244
+ priority=rec["priority"] if rec else 0,
245
+ status=AgentRegistryStatus.ACTIVE,
246
+ agent_version=agent_version,
247
+ session_token=rec.get("session_token") if rec else None,
248
+ )
249
+ updated = self.get_agent_projection(amendment.agent_id)
250
+ logger.info(
251
+ "Parameter amendment: agent_id=%s invariant_id=%s actor=%s",
252
+ amendment.agent_id,
253
+ amendment.invariant_id,
254
+ amendment.actor_id,
255
+ )
256
+ return AmendmentResult(accepted=True, projection=updated)
257
+
167
258
  def get_agent_manifest(self, agent_id: str) -> Optional[Dict[str, Any]]:
168
259
  """Retrieve agent capability contract. Deprecated: use get_agent_contract."""
169
260
  return self.get_agent_contract(agent_id)
@@ -0,0 +1,137 @@
1
+ """Apply human-gated parameter amendments to runtime contract projections."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timedelta, timezone
6
+ from typing import Any
7
+
8
+ from .models import (
9
+ ContractParameterAmendment,
10
+ InvariantSpec,
11
+ RuntimeContractProjection,
12
+ )
13
+
14
+ ALLOWED_PATCH_KEYS = frozenset({"min", "max", "allowed", "drift_envelope_pct"})
15
+
16
+
17
+ def _utcnow() -> datetime:
18
+ return datetime.now(timezone.utc)
19
+
20
+
21
+ def apply_amendment_patch(
22
+ invariant: InvariantSpec,
23
+ patch: dict[str, Any],
24
+ ) -> InvariantSpec:
25
+ """Return a copy of invariant with scalar patch applied."""
26
+ data = invariant.model_dump()
27
+ for key, value in patch.items():
28
+ data[key] = value
29
+ return InvariantSpec.model_validate(data)
30
+
31
+
32
+ def validate_amendment_patch(
33
+ invariant: InvariantSpec,
34
+ patch: dict[str, Any],
35
+ *,
36
+ now: datetime | None = None,
37
+ ) -> str | None:
38
+ """Validate amendment patch against runtime_amendable rules and envelope."""
39
+ now = now or _utcnow()
40
+ if not invariant.runtime_amendable:
41
+ return "INVARIANT_NOT_AMENDABLE"
42
+
43
+ if not patch:
44
+ return "EMPTY_PATCH"
45
+
46
+ invalid_keys = set(patch) - ALLOWED_PATCH_KEYS
47
+ if invalid_keys:
48
+ return f"DISALLOWED_PATCH_KEYS: {sorted(invalid_keys)}"
49
+
50
+ if "max" in patch and patch["max"] is not None:
51
+ new_max = float(patch["max"])
52
+ if invariant.max_ceiling is not None and new_max > float(invariant.max_ceiling):
53
+ return "PATCH_EXCEEDS_MAX_CEILING"
54
+ if invariant.min_floor is not None and new_max < float(invariant.min_floor):
55
+ return "PATCH_BELOW_MIN_FLOOR"
56
+
57
+ if "min" in patch and patch["min"] is not None:
58
+ new_min = float(patch["min"])
59
+ if invariant.min_floor is not None and new_min < float(invariant.min_floor):
60
+ return "PATCH_BELOW_MIN_FLOOR"
61
+ if invariant.max_ceiling is not None and new_min > float(invariant.max_ceiling):
62
+ return "PATCH_EXCEEDS_MAX_CEILING"
63
+
64
+ if "allowed" in patch:
65
+ new_allowed = list(patch["allowed"])
66
+ if not isinstance(patch["allowed"], list):
67
+ return "ALLOWED_PATCH_MUST_BE_LIST"
68
+ signed_allowed = set(invariant.allowed)
69
+ if signed_allowed and not set(new_allowed).issubset(signed_allowed):
70
+ return "ALLOWED_PATCH_EXPANSION_FORBIDDEN"
71
+ if not signed_allowed and invariant.type == "set":
72
+ return "ALLOWED_PATCH_REQUIRES_SIGNED_BASE"
73
+
74
+ if "drift_envelope_pct" in patch and patch["drift_envelope_pct"] is not None:
75
+ new_drift = float(patch["drift_envelope_pct"])
76
+ if new_drift < 0:
77
+ return "DRIFT_ENVELOPE_NEGATIVE"
78
+
79
+ if invariant.amend_ttl_seconds is not None and invariant.amend_ttl_seconds <= 0:
80
+ return "INVALID_AMEND_TTL"
81
+
82
+ return None
83
+
84
+
85
+ def resolve_expires_at(
86
+ amendment: ContractParameterAmendment,
87
+ invariant: InvariantSpec,
88
+ ) -> datetime | None:
89
+ if amendment.expires_at is not None:
90
+ return amendment.expires_at
91
+ if invariant.amend_ttl_seconds:
92
+ return amendment.created_at + timedelta(seconds=int(invariant.amend_ttl_seconds))
93
+ return None
94
+
95
+
96
+ def effective_invariants(
97
+ projection: RuntimeContractProjection,
98
+ *,
99
+ now: datetime | None = None,
100
+ ) -> list[InvariantSpec]:
101
+ """Merge base invariants with active parameter amendments."""
102
+ now = now or _utcnow()
103
+ by_id = {spec.id: spec for spec in projection.invariants}
104
+
105
+ for amendment in projection.parameter_amendments:
106
+ expires_at = resolve_expires_at(amendment, by_id.get(amendment.invariant_id, InvariantSpec(
107
+ id=amendment.invariant_id,
108
+ type="range",
109
+ field="policy_kind",
110
+ )))
111
+ if expires_at is not None and now >= expires_at:
112
+ continue
113
+ base = by_id.get(amendment.invariant_id)
114
+ if base is None:
115
+ continue
116
+ by_id[amendment.invariant_id] = apply_amendment_patch(base, amendment.patch)
117
+
118
+ return list(by_id.values())
119
+
120
+
121
+ def projection_with_effective_invariants(
122
+ projection: RuntimeContractProjection,
123
+ *,
124
+ now: datetime | None = None,
125
+ ) -> RuntimeContractProjection:
126
+ """Return projection copy whose invariants reflect active amendments."""
127
+ effective = effective_invariants(projection, now=now)
128
+ data = projection.model_dump(mode="python")
129
+ data["invariants"] = effective
130
+ data["transaction_limits"] = _limits_from_invariants(effective)
131
+ return RuntimeContractProjection.model_validate(data)
132
+
133
+
134
+ def _limits_from_invariants(invariants: list[InvariantSpec]) -> dict[str, dict[str, Any]]:
135
+ from .invariant_compile import invariants_to_transaction_limits
136
+
137
+ return invariants_to_transaction_limits(invariants)
@@ -0,0 +1,238 @@
1
+ """Normalize canonical and legacy contracts into a Runtime projection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+
8
+ from .invariant_compile import compile_authority_invariants, invariants_to_transaction_limits
9
+ from .models import (
10
+ ContractParameterAmendment,
11
+ ContractReleaseRef,
12
+ InvariantSpec,
13
+ ResponsibilityContract,
14
+ RuntimeContractProjection,
15
+ )
16
+
17
+
18
+ def _as_limit(value: Any) -> dict[str, Any] | None:
19
+ """Convert a legacy numeric limit to the projection's typed shape."""
20
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
21
+ return {"value": float(value)}
22
+ if isinstance(value, Mapping) and "value" in value:
23
+ return dict(value)
24
+ return None
25
+
26
+
27
+ def _is_projection_dump(data: Mapping[str, Any]) -> bool:
28
+ """Detect serialized RuntimeContractProjection payloads."""
29
+ if "release" in data and ("invariants" in data or "transaction_limits" in data):
30
+ return True
31
+ if "agent_id" in data and "invariants" in data and "allowed_policy_types" in data:
32
+ return True
33
+ return False
34
+
35
+
36
+ def _build_projection_from_contract_mapping(data: Mapping[str, Any]) -> RuntimeContractProjection:
37
+ metadata = dict(data.get("metadata") or {})
38
+ subject = dict(data.get("subject") or {})
39
+ authority = dict(data.get("authority") or {})
40
+ responsibility = dict(data.get("responsibility") or {})
41
+ governance = dict(data.get("governance") or {})
42
+
43
+ legacy_permissions = dict(data.get("permissions") or {})
44
+ if not authority and legacy_permissions:
45
+ authority = legacy_permissions
46
+ if not authority and (
47
+ "allowed_policy_types" in data or "authorized_instruments" in data
48
+ ):
49
+ authority = {
50
+ key: value
51
+ for key, value in data.items()
52
+ if key in {"allowed_policy_types", "authorized_instruments"}
53
+ or key.startswith("max_")
54
+ }
55
+
56
+ release = ContractReleaseRef(
57
+ contract_id=str(metadata.get("contract_id", data.get("contract_id", "legacy"))),
58
+ contract_version=str(metadata.get("version", data.get("version", "0.0.0"))),
59
+ api_version=str(metadata.get("api_version", data.get("api_version", "legacy"))),
60
+ contract_hash=str(metadata.get("contract_hash", data.get("contract_hash", ""))),
61
+ )
62
+
63
+ invariants = compile_authority_invariants(data)
64
+ transaction_limits = invariants_to_transaction_limits(invariants)
65
+
66
+ legacy_limits: dict[str, dict[str, Any]] = {}
67
+ source_limits = dict(authority.get("limits") or {})
68
+ if not source_limits:
69
+ source_limits = {
70
+ key: value
71
+ for key, value in authority.items()
72
+ if key
73
+ not in {
74
+ "allowed_policy_types",
75
+ "authorized_instruments",
76
+ "resource_scope",
77
+ "invariants",
78
+ }
79
+ }
80
+ for key, value in source_limits.items():
81
+ typed_limit = _as_limit(value)
82
+ if typed_limit is not None:
83
+ legacy_limits[str(key)] = typed_limit
84
+ for key, value in legacy_limits.items():
85
+ transaction_limits.setdefault(key, value)
86
+
87
+ evidence = dict(responsibility.get("evidence") or {})
88
+ if not evidence and "evidence_level" in responsibility:
89
+ evidence["level"] = responsibility["evidence_level"]
90
+
91
+ escalation = dict(responsibility.get("escalation") or {})
92
+ if not escalation:
93
+ if "escalation" in responsibility:
94
+ escalation["mode"] = responsibility["escalation"]
95
+ if "escalate_on_uncertainty" in responsibility:
96
+ escalation["confidence_below"] = responsibility["escalate_on_uncertainty"]
97
+
98
+ mission_value = data.get("mission", "")
99
+ if isinstance(mission_value, Mapping):
100
+ mission_value = mission_value.get("statement", "")
101
+
102
+ aggregate_raw = governance.get("aggregate_policies") or []
103
+ aggregate_policies = list(aggregate_raw) if isinstance(aggregate_raw, list) else []
104
+
105
+ return RuntimeContractProjection(
106
+ release=release,
107
+ agent_id=str(subject.get("agent_id", data.get("agent_id", ""))),
108
+ role=subject.get("role", data.get("role", "EXECUTOR")),
109
+ parent_agent_id=subject.get("parent_agent_id", data.get("parent_agent_id")),
110
+ mission=str(mission_value),
111
+ allowed_policy_types=list(
112
+ authority.get("allowed_policy_types")
113
+ or legacy_permissions.get("allowed_policy_types")
114
+ or []
115
+ ),
116
+ resource_scope=dict(authority.get("resource_scope") or {}),
117
+ transaction_limits=transaction_limits,
118
+ execution_conditions=dict(data.get("execution_conditions") or {}),
119
+ evidence_requirements=evidence,
120
+ escalation_policy=escalation,
121
+ aggregate_policies=aggregate_policies,
122
+ invariants=invariants,
123
+ parameter_amendments=[],
124
+ )
125
+
126
+
127
+ def ensure_projection_invariants(projection: RuntimeContractProjection) -> RuntimeContractProjection:
128
+ """Backfill invariant IR when only legacy projection fields were stored."""
129
+ if projection.invariants:
130
+ return projection
131
+ if (
132
+ not projection.allowed_policy_types
133
+ and not projection.transaction_limits
134
+ and not projection.resource_scope.get("instruments")
135
+ and not projection.execution_conditions
136
+ ):
137
+ return projection
138
+
139
+ rebuilt = _build_projection_from_contract_mapping(
140
+ {
141
+ "metadata": projection.release.model_dump(mode="json"),
142
+ "subject": {"agent_id": projection.agent_id, "role": projection.role},
143
+ "mission": {"statement": projection.mission},
144
+ "authority": {
145
+ "allowed_policy_types": projection.allowed_policy_types,
146
+ "resource_scope": projection.resource_scope,
147
+ "limits": projection.transaction_limits,
148
+ },
149
+ "execution_conditions": projection.execution_conditions,
150
+ "responsibility": {"escalation": projection.escalation_policy},
151
+ "governance": {"aggregate_policies": projection.aggregate_policies},
152
+ }
153
+ )
154
+ if not rebuilt.invariants:
155
+ return projection
156
+ data = projection.model_dump(mode="python")
157
+ data["invariants"] = rebuilt.invariants
158
+ data["transaction_limits"] = rebuilt.transaction_limits
159
+ return RuntimeContractProjection.model_validate(data)
160
+
161
+
162
+ def _parse_projection_dump(data: Mapping[str, Any]) -> RuntimeContractProjection:
163
+ payload = dict(data)
164
+ invariants_raw = payload.get("invariants") or []
165
+ payload["invariants"] = [
166
+ spec if isinstance(spec, InvariantSpec) else InvariantSpec.model_validate(spec)
167
+ for spec in invariants_raw
168
+ ]
169
+ amendments_raw = payload.get("parameter_amendments") or []
170
+ payload["parameter_amendments"] = [
171
+ amendment
172
+ if isinstance(amendment, ContractParameterAmendment)
173
+ else ContractParameterAmendment.model_validate(amendment)
174
+ for amendment in amendments_raw
175
+ ]
176
+ if not payload.get("transaction_limits") and payload.get("invariants"):
177
+ payload["transaction_limits"] = invariants_to_transaction_limits(payload["invariants"])
178
+ aggregate_raw = payload.get("aggregate_policies")
179
+ if aggregate_raw is None or isinstance(aggregate_raw, dict):
180
+ payload["aggregate_policies"] = []
181
+ projection = RuntimeContractProjection.model_validate(payload)
182
+ return ensure_projection_invariants(projection)
183
+
184
+
185
+ def project_contract(contract: Mapping[str, Any]) -> RuntimeContractProjection:
186
+ """Create the execution-facing projection from a contract mapping."""
187
+ data = dict(contract)
188
+ if _is_projection_dump(data):
189
+ return _parse_projection_dump(data)
190
+ return ensure_projection_invariants(_build_projection_from_contract_mapping(data))
191
+
192
+
193
+ def _limit_value(projection: RuntimeContractProjection, key: str) -> float | None:
194
+ raw = projection.transaction_limits.get(key)
195
+ if not isinstance(raw, Mapping):
196
+ return None
197
+ value = raw.get("value")
198
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
199
+ return None
200
+ return float(value)
201
+
202
+
203
+ def responsibility_contract_from_projection(
204
+ projection: RuntimeContractProjection,
205
+ ) -> ResponsibilityContract:
206
+ """Derive the flat agent-facing ``ResponsibilityContract`` from a projection.
207
+
208
+ ``max_drawdown_limit`` is a fraction (0.04 = 4%); ``max_drawdown_limit_pct`` is in percent points.
209
+ """
210
+ fields: dict[str, Any] = {
211
+ "agent_id": projection.agent_id,
212
+ "role": projection.role,
213
+ "mission": projection.mission,
214
+ "authorized_instruments": list(
215
+ projection.resource_scope.get("instruments")
216
+ or projection.resource_scope.get("categories")
217
+ or []
218
+ ),
219
+ "allowed_policy_types": list(projection.allowed_policy_types),
220
+ "parent_agent_id": projection.parent_agent_id,
221
+ }
222
+
223
+ confidence_below = projection.escalation_policy.get("confidence_below")
224
+ if confidence_below is not None:
225
+ fields["escalate_on_uncertainty"] = float(confidence_below)
226
+
227
+ wake_up = projection.execution_conditions.get("wake_up_threshold_pct")
228
+ if wake_up is not None:
229
+ fields["wake_up_threshold_pct"] = float(wake_up)
230
+
231
+ drawdown = _limit_value(projection, "max_drawdown_limit")
232
+ if drawdown is None:
233
+ drawdown_pct = _limit_value(projection, "max_drawdown_limit_pct")
234
+ drawdown = drawdown_pct / 100.0 if drawdown_pct is not None else None
235
+ if drawdown is not None:
236
+ fields["max_drawdown_limit"] = drawdown
237
+
238
+ return ResponsibilityContract(**fields)
@@ -17,6 +17,12 @@ class DimReasonCode(StrEnum):
17
17
 
18
18
  REASONING_EXHAUSTION = "REASONING_EXHAUSTION"
19
19
  TTL_EXPIRED = "TTL_EXPIRED"
20
+ AGENT_SUSPENDED = "AGENT_SUSPENDED"
21
+ AGENT_RETIRED = "AGENT_RETIRED"
22
+ AGENT_DEGRADED = "AGENT_DEGRADED"
23
+ CONTRACT_LIMIT_EXCEEDED = "CONTRACT_LIMIT_EXCEEDED"
24
+ CONTRACT_PARAMETER_MISSING = "CONTRACT_PARAMETER_MISSING"
25
+ INVARIANT_VIOLATION = "INVARIANT_VIOLATION"
20
26
  VALIDATION_PASSED = "VALIDATION_PASSED"
21
27
 
22
28
 
@@ -88,6 +94,10 @@ class AgentRegistryStatus(StrEnum):
88
94
  """Agent row status in registry storage."""
89
95
 
90
96
  ACTIVE = "ACTIVE"
97
+ SUSPENDED = "SUSPENDED"
98
+ DEGRADED = "DEGRADED"
99
+ ESCALATION_ONLY = "ESCALATION_ONLY"
100
+ RETIRED = "RETIRED"
91
101
 
92
102
 
93
103
  class HandshakeRejectionReason(StrEnum):