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.
- {dir_core-0.3.0 → dir_core-0.4.2}/PKG-INFO +15 -7
- {dir_core-0.3.0 → dir_core-0.4.2}/README.md +8 -6
- {dir_core-0.3.0 → dir_core-0.4.2}/pyproject.toml +3 -2
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/__init__.py +17 -1
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/agent_registry.py +91 -0
- dir_core-0.4.2/src/dir_core/amendments.py +137 -0
- dir_core-0.4.2/src/dir_core/contract_projection.py +238 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/data_types.py +10 -0
- dir_core-0.4.2/src/dir_core/dim.py +163 -0
- dir_core-0.4.2/src/dir_core/invariant_compile.py +194 -0
- dir_core-0.4.2/src/dir_core/invariants.py +216 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/models.py +78 -2
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/runtime.py +73 -4
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/utils/__init__.py +12 -3
- dir_core-0.4.2/src/dir_core/utils/logging_utils.py +78 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core.egg-info/PKG-INFO +15 -7
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core.egg-info/SOURCES.txt +17 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core.egg-info/requires.txt +7 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_agent_registry.py +25 -0
- dir_core-0.4.2/tests/test_contract_env.py +111 -0
- dir_core-0.4.2/tests/test_contract_studio.py +466 -0
- dir_core-0.4.2/tests/test_contract_tool.py +509 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_dim_ttl.py +49 -3
- dir_core-0.4.2/tests/test_governance_context.py +49 -0
- dir_core-0.4.2/tests/test_governance_validation.py +154 -0
- dir_core-0.4.2/tests/test_insurance_agent.py +33 -0
- dir_core-0.4.2/tests/test_insurance_invariants.py +127 -0
- dir_core-0.4.2/tests/test_insurance_report.py +157 -0
- dir_core-0.4.2/tests/test_invariants.py +251 -0
- dir_core-0.4.2/tests/test_llm_json_parse.py +31 -0
- dir_core-0.4.2/tests/test_logging_utils.py +49 -0
- dir_core-0.4.2/tests/test_responsibility_adapter.py +69 -0
- dir_core-0.4.2/tests/test_runtime.py +226 -0
- dir_core-0.4.2/tests/test_samples_isolation.py +39 -0
- dir_core-0.3.0/src/dir_core/dim.py +0 -112
- dir_core-0.3.0/src/dir_core/utils/logging_utils.py +0 -27
- dir_core-0.3.0/tests/test_runtime.py +0 -85
- {dir_core-0.3.0 → dir_core-0.4.2}/LICENSE +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/setup.cfg +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/arbitration.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/context_store.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/dfid.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/escalation.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/event_bus.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/idempotency.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/intent_retry.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/jit.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/ledger.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/lifecycle.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/pci.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/resource_lock.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/saga.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/storage/__init__.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/storage/base.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/storage/json_util.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/storage/memory.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/storage/schema.sql +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/storage/sqlite.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/utils/llm_client.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core/wakeup.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core.egg-info/dependency_links.txt +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/src/dir_core.egg-info/top_level.txt +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_arbitration.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_context_store.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_decision_ledger_storage.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_dfid.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_escalation.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_event_bus.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_idempotency.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_intent_retry.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_jit.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_ledger.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_lifecycle.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_ollama.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_pci.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_pg_repo.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_resource_lock.py +0 -0
- {dir_core-0.3.0 → dir_core-0.4.2}/tests/test_saga.py +0 -0
- {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
|
+
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
|
-
|
|
95
|
+
<p align="center">
|
|
96
|
+
<img src="./assets/images/lds-octagon.jpg" alt="Legal Decision State Invariants" width="60%">
|
|
97
|
+
</p>
|
|
90
98
|
|
|
91
|
-
|
|
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
|
-
|
|
101
|
+
$$LDS = A \wedge C \wedge T \wedge I \wedge E$$
|
|
94
102
|
|
|
95
|
-
If any of these components is false ($\neg
|
|
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
|
-
|
|
47
|
+
<p align="center">
|
|
48
|
+
<img src="./assets/images/lds-octagon.jpg" alt="Legal Decision State Invariants" width="60%">
|
|
49
|
+
</p>
|
|
48
50
|
|
|
49
|
-
|
|
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
|
-
|
|
53
|
+
$$LDS = A \wedge C \wedge T \wedge I \wedge E$$
|
|
52
54
|
|
|
53
|
-
If any of these components is false ($\neg
|
|
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.
|
|
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):
|