dir-core 0.4.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.4.0 → dir_core-0.4.2}/PKG-INFO +1 -1
- {dir_core-0.4.0 → dir_core-0.4.2}/pyproject.toml +1 -1
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/__init__.py +2 -1
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/contract_projection.py +50 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/invariants.py +18 -4
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/models.py +1 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core.egg-info/PKG-INFO +1 -1
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core.egg-info/SOURCES.txt +2 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_contract_studio.py +151 -10
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_contract_tool.py +174 -110
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_governance_validation.py +23 -1
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_invariants.py +46 -0
- dir_core-0.4.2/tests/test_responsibility_adapter.py +69 -0
- dir_core-0.4.2/tests/test_samples_isolation.py +39 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/LICENSE +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/README.md +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/setup.cfg +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/agent_registry.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/amendments.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/arbitration.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/context_store.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/data_types.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/dfid.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/dim.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/escalation.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/event_bus.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/idempotency.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/intent_retry.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/invariant_compile.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/jit.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/ledger.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/lifecycle.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/pci.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/resource_lock.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/runtime.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/saga.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/storage/__init__.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/storage/base.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/storage/json_util.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/storage/memory.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/storage/schema.sql +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/storage/sqlite.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/utils/__init__.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/utils/llm_client.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/utils/logging_utils.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core/wakeup.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core.egg-info/dependency_links.txt +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core.egg-info/requires.txt +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/src/dir_core.egg-info/top_level.txt +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_agent_registry.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_arbitration.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_context_store.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_contract_env.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_decision_ledger_storage.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_dfid.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_dim_ttl.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_escalation.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_event_bus.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_governance_context.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_idempotency.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_insurance_agent.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_insurance_invariants.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_insurance_report.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_intent_retry.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_jit.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_ledger.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_lifecycle.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_llm_json_parse.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_logging_utils.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_ollama.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_pci.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_pg_repo.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_resource_lock.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_runtime.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_saga.py +0 -0
- {dir_core-0.4.0 → dir_core-0.4.2}/tests/test_wakeup.py +0 -0
|
@@ -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.4.
|
|
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"
|
|
@@ -94,7 +94,7 @@ from .pci import (
|
|
|
94
94
|
hash_content,
|
|
95
95
|
proposal_params_for_hash,
|
|
96
96
|
)
|
|
97
|
-
from .contract_projection import project_contract
|
|
97
|
+
from .contract_projection import project_contract, responsibility_contract_from_projection
|
|
98
98
|
from .invariants import evaluate_invariants
|
|
99
99
|
from .amendments import validate_amendment_patch
|
|
100
100
|
from .wakeup import (
|
|
@@ -227,6 +227,7 @@ __all__ = [
|
|
|
227
227
|
"ContractParameterAmendment",
|
|
228
228
|
"AmendmentResult",
|
|
229
229
|
"project_contract",
|
|
230
|
+
"responsibility_contract_from_projection",
|
|
230
231
|
"evaluate_invariants",
|
|
231
232
|
"validate_amendment_patch",
|
|
232
233
|
"PolicyProposal",
|
|
@@ -10,6 +10,7 @@ from .models import (
|
|
|
10
10
|
ContractParameterAmendment,
|
|
11
11
|
ContractReleaseRef,
|
|
12
12
|
InvariantSpec,
|
|
13
|
+
ResponsibilityContract,
|
|
13
14
|
RuntimeContractProjection,
|
|
14
15
|
)
|
|
15
16
|
|
|
@@ -105,6 +106,7 @@ def _build_projection_from_contract_mapping(data: Mapping[str, Any]) -> RuntimeC
|
|
|
105
106
|
release=release,
|
|
106
107
|
agent_id=str(subject.get("agent_id", data.get("agent_id", ""))),
|
|
107
108
|
role=subject.get("role", data.get("role", "EXECUTOR")),
|
|
109
|
+
parent_agent_id=subject.get("parent_agent_id", data.get("parent_agent_id")),
|
|
108
110
|
mission=str(mission_value),
|
|
109
111
|
allowed_policy_types=list(
|
|
110
112
|
authority.get("allowed_policy_types")
|
|
@@ -186,3 +188,51 @@ def project_contract(contract: Mapping[str, Any]) -> RuntimeContractProjection:
|
|
|
186
188
|
if _is_projection_dump(data):
|
|
187
189
|
return _parse_projection_dump(data)
|
|
188
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)
|
|
@@ -30,11 +30,24 @@ def get_nested_value(obj: Any, path: str) -> Any:
|
|
|
30
30
|
return current
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
def resolve_field(
|
|
34
|
+
field: str,
|
|
35
|
+
proposal: PolicyProposal,
|
|
36
|
+
context: Optional[dict[str, Any]] = None,
|
|
37
|
+
) -> Any:
|
|
38
|
+
"""Resolve a rule field: ``state.*`` reads authoritative state, anything else the proposal."""
|
|
39
|
+
if field.startswith("state."):
|
|
40
|
+
state = (context or {}).get("state")
|
|
41
|
+
return get_nested_value(state, field.removeprefix("state."))
|
|
42
|
+
return get_nested_value(proposal, field)
|
|
43
|
+
|
|
44
|
+
|
|
33
45
|
def evaluate_range_rule(
|
|
34
46
|
rule: InvariantSpec,
|
|
35
47
|
proposal: PolicyProposal,
|
|
48
|
+
context: Optional[dict[str, Any]] = None,
|
|
36
49
|
) -> Optional[InvariantEvaluationFailure]:
|
|
37
|
-
value =
|
|
50
|
+
value = resolve_field(rule.field, proposal, context)
|
|
38
51
|
if value is None:
|
|
39
52
|
return InvariantEvaluationFailure(
|
|
40
53
|
reason=DimReasonCode.CONTRACT_PARAMETER_MISSING,
|
|
@@ -63,8 +76,9 @@ def evaluate_range_rule(
|
|
|
63
76
|
def evaluate_set_rule(
|
|
64
77
|
rule: InvariantSpec,
|
|
65
78
|
proposal: PolicyProposal,
|
|
79
|
+
context: Optional[dict[str, Any]] = None,
|
|
66
80
|
) -> Optional[InvariantEvaluationFailure]:
|
|
67
|
-
value =
|
|
81
|
+
value = resolve_field(rule.field, proposal, context)
|
|
68
82
|
if value is None:
|
|
69
83
|
return InvariantEvaluationFailure(
|
|
70
84
|
reason=DimReasonCode.CONTRACT_PARAMETER_MISSING,
|
|
@@ -184,9 +198,9 @@ def evaluate_invariants(
|
|
|
184
198
|
):
|
|
185
199
|
continue
|
|
186
200
|
if rule.type == "range":
|
|
187
|
-
failure = evaluate_range_rule(rule, proposal)
|
|
201
|
+
failure = evaluate_range_rule(rule, proposal, context)
|
|
188
202
|
elif rule.type == "set":
|
|
189
|
-
failure = evaluate_set_rule(rule, proposal)
|
|
203
|
+
failure = evaluate_set_rule(rule, proposal, context)
|
|
190
204
|
elif rule.type == "state_match":
|
|
191
205
|
failure = evaluate_state_match_rule(rule, proposal, context)
|
|
192
206
|
else:
|
|
@@ -130,6 +130,7 @@ class RuntimeContractProjection(BaseModel):
|
|
|
130
130
|
release: ContractReleaseRef = Field(default_factory=ContractReleaseRef)
|
|
131
131
|
agent_id: str
|
|
132
132
|
role: ContractRole = ContractRole.EXECUTOR
|
|
133
|
+
parent_agent_id: Optional[str] = None
|
|
133
134
|
mission: str = ""
|
|
134
135
|
allowed_policy_types: List[str] = Field(default_factory=list)
|
|
135
136
|
resource_scope: Dict[str, List[str]] = Field(default_factory=dict)
|
|
@@ -3,19 +3,18 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
-
import os
|
|
7
6
|
import tempfile
|
|
8
7
|
from pathlib import Path
|
|
9
8
|
|
|
10
9
|
import pytest
|
|
11
10
|
|
|
11
|
+
from samples.shared.llm.clients import MockLLMClient
|
|
12
12
|
from tools.contract.db.store import ContractStudioStore
|
|
13
13
|
from tools.contract.llm_interview import (
|
|
14
14
|
empty_draft_contract,
|
|
15
15
|
mock_contract_llm_strategy,
|
|
16
16
|
process_chat_turn,
|
|
17
17
|
)
|
|
18
|
-
from samples.shared.llm.clients import MockLLMClient
|
|
19
18
|
|
|
20
19
|
|
|
21
20
|
@pytest.fixture
|
|
@@ -77,6 +76,42 @@ def test_process_chat_turn_mock() -> None:
|
|
|
77
76
|
assert turn.change_summary
|
|
78
77
|
|
|
79
78
|
|
|
79
|
+
def test_noncanonical_analysis_key_remains_warning_through_integrity() -> None:
|
|
80
|
+
from tools.contract.integrity import verify_contract_yaml
|
|
81
|
+
from tools.contract.schema import IRREVERSIBLE_LIMIT_KEYS
|
|
82
|
+
|
|
83
|
+
def strategy(prompt: str, system: str | None = None) -> str:
|
|
84
|
+
response = json.loads(mock_contract_llm_strategy(prompt, system))
|
|
85
|
+
response["governance_analysis"]["invariant_candidates"][0][
|
|
86
|
+
"linked_limit_key"
|
|
87
|
+
] = "max_payout_usd"
|
|
88
|
+
return json.dumps(response)
|
|
89
|
+
|
|
90
|
+
turn = process_chat_turn(
|
|
91
|
+
MockLLMClient(strategy=strategy),
|
|
92
|
+
current_contract=empty_draft_contract("trading"),
|
|
93
|
+
chat_history=[],
|
|
94
|
+
user_message="Build a crypto trading agent with limits",
|
|
95
|
+
preset="trading",
|
|
96
|
+
)
|
|
97
|
+
assert turn.validation_ok
|
|
98
|
+
assert not turn.blocking_errors
|
|
99
|
+
assert any("INVALID_LIMIT_KEY" in warning for warning in turn.warnings)
|
|
100
|
+
|
|
101
|
+
result = verify_contract_yaml(
|
|
102
|
+
turn.contract_yaml,
|
|
103
|
+
preset="trading",
|
|
104
|
+
governance_analysis=turn.governance_analysis,
|
|
105
|
+
)
|
|
106
|
+
assert result["integrity_ok"]
|
|
107
|
+
assert result["checks"]["contract_validation"]["ok"]
|
|
108
|
+
assert not result["errors"]
|
|
109
|
+
warning = next(w for w in result["validation_warnings"] if "INVALID_LIMIT_KEY" in w)
|
|
110
|
+
assert "max_payout_usd" in warning
|
|
111
|
+
for key in IRREVERSIBLE_LIMIT_KEYS:
|
|
112
|
+
assert key in warning
|
|
113
|
+
|
|
114
|
+
|
|
80
115
|
@pytest.fixture
|
|
81
116
|
def studio_client(monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
|
|
82
117
|
pytest.importorskip("fastapi")
|
|
@@ -118,14 +153,15 @@ def test_api_create_chat_export(studio_client) -> None:
|
|
|
118
153
|
assert vbody["checks"]["bootstrap"]["ok"] is True
|
|
119
154
|
assert vbody["sha256"]
|
|
120
155
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
156
|
+
with tempfile.TemporaryDirectory() as output_dir:
|
|
157
|
+
export = studio_client.post(
|
|
158
|
+
f"/api/sessions/{session_id}/export",
|
|
159
|
+
json={"emit": "registry", "out": output_dir},
|
|
160
|
+
)
|
|
161
|
+
assert export.status_code == 200
|
|
162
|
+
paths = export.json()["paths"]
|
|
163
|
+
assert any(p.endswith(".yaml") for p in paths)
|
|
164
|
+
assert Path(paths[0]).is_file()
|
|
129
165
|
|
|
130
166
|
revisions = studio_client.get(f"/api/sessions/{session_id}/revisions")
|
|
131
167
|
assert revisions.status_code == 200
|
|
@@ -239,6 +275,12 @@ def test_index_serves_versioned_assets(studio_client) -> None:
|
|
|
239
275
|
assert 'id="yamlEditor"' in res.text
|
|
240
276
|
assert "/static/app.js?v=" in res.text
|
|
241
277
|
assert "/static/styles.css?v=" in res.text
|
|
278
|
+
assert '<details id="integrityPanel"' in res.text
|
|
279
|
+
assert '<details id="governancePanel"' in res.text
|
|
280
|
+
assert 'id="integritySummary"' in res.text
|
|
281
|
+
assert 'id="yamlHighlight" aria-hidden="true"' in res.text
|
|
282
|
+
assert 'wrap="off"' in res.text
|
|
283
|
+
assert "<details" in res.text and " open" not in res.text
|
|
242
284
|
|
|
243
285
|
|
|
244
286
|
def test_verify_contract_yaml_rejects_garbage() -> None:
|
|
@@ -249,6 +291,32 @@ def test_verify_contract_yaml_rejects_garbage() -> None:
|
|
|
249
291
|
assert result["checks"]["yaml_parse"]["ok"] is False
|
|
250
292
|
|
|
251
293
|
|
|
294
|
+
def test_integrity_preserves_authoring_export_block(studio_client) -> None:
|
|
295
|
+
session_id = studio_client.post(
|
|
296
|
+
"/api/sessions", json={"title": "Blocked", "preset": "trading"}
|
|
297
|
+
).json()["session"]["id"]
|
|
298
|
+
chat = studio_client.post(
|
|
299
|
+
f"/api/sessions/{session_id}/chat", json={"message": "Build a crypto trading agent"}
|
|
300
|
+
).json()
|
|
301
|
+
edited = chat["contract_yaml"].replace("jane.doe@example.com", "owner@example.com")
|
|
302
|
+
saved = studio_client.put(
|
|
303
|
+
f"/api/sessions/{session_id}/contract", json={"yaml": edited}
|
|
304
|
+
).json()
|
|
305
|
+
assert saved["saved"]
|
|
306
|
+
assert not saved["validation_ok"]
|
|
307
|
+
result = studio_client.post(
|
|
308
|
+
f"/api/sessions/{session_id}/validate", json={}
|
|
309
|
+
).json()
|
|
310
|
+
assert not result["integrity_ok"]
|
|
311
|
+
assert not result["validation_ok"]
|
|
312
|
+
assert not result["checks"]["contract_validation"]["ok"]
|
|
313
|
+
assert any("PLACEHOLDER_IDENTITY" in error for error in result["validation_errors"])
|
|
314
|
+
assert studio_client.get(f"/api/sessions/{session_id}").json()["session"]["status"] == "drafting"
|
|
315
|
+
assert studio_client.post(
|
|
316
|
+
f"/api/sessions/{session_id}/export", json={}
|
|
317
|
+
).status_code == 400
|
|
318
|
+
|
|
319
|
+
|
|
252
320
|
def test_rename_and_delete_session(studio_client) -> None:
|
|
253
321
|
a = studio_client.post("/api/sessions", json={"title": "Alpha", "preset": "generic"})
|
|
254
322
|
b = studio_client.post("/api/sessions", json={"title": "Beta", "preset": "trading"})
|
|
@@ -323,3 +391,76 @@ responsibility:
|
|
|
323
391
|
result = verify_contract_yaml(yaml_text, preset="generic")
|
|
324
392
|
assert result["integrity_ok"] is True
|
|
325
393
|
assert result["checks"]["bootstrap"]["ok"] is True
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def test_normalize_llm_resource_scope_and_invariant_candidate() -> None:
|
|
397
|
+
from tools.contract.invariants import compile_authority_invariants
|
|
398
|
+
from tools.contract.schema import CanonicalContract, normalize_contract_dict
|
|
399
|
+
from tools.contract.validation import validate_contract
|
|
400
|
+
|
|
401
|
+
raw = empty_draft_contract("generic")
|
|
402
|
+
raw["metadata"].update(
|
|
403
|
+
contract_id="storm_claims_agent",
|
|
404
|
+
owner="claims.owner@example.org",
|
|
405
|
+
)
|
|
406
|
+
raw["subject"]["agent_id"] = "storm_claims_agent"
|
|
407
|
+
raw["mission"]["statement"] = "Review residential storm damage claims."
|
|
408
|
+
raw["authority"]["limits"] = {
|
|
409
|
+
"max_transaction_usd": {"value": 5000.0, "unit": "USD"}
|
|
410
|
+
}
|
|
411
|
+
raw["authority"]["resource_scope"] = {
|
|
412
|
+
"coverage": ["homeowner policy endorsement"],
|
|
413
|
+
"category": ["fine arts and rugs"],
|
|
414
|
+
"exclusions": {},
|
|
415
|
+
"match": {
|
|
416
|
+
"substring": ["contents coverage", "fine arts and rugs"]
|
|
417
|
+
},
|
|
418
|
+
}
|
|
419
|
+
raw["authority"]["invariants"] = [
|
|
420
|
+
{
|
|
421
|
+
"invariant_id": "INV-RUG-PAYMENT-RANGE",
|
|
422
|
+
"constraint_class": "transaction_invariant",
|
|
423
|
+
"predicate": {
|
|
424
|
+
"op": "and",
|
|
425
|
+
"left": {
|
|
426
|
+
"op": "ge",
|
|
427
|
+
"variable": "proposed_rug_payment_usd",
|
|
428
|
+
"value": 0,
|
|
429
|
+
},
|
|
430
|
+
"right": {
|
|
431
|
+
"op": "le",
|
|
432
|
+
"variable": "proposed_rug_payment_usd",
|
|
433
|
+
"value": 5000,
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
"code": "POLICY_SUBLIMIT_EXCEEDED",
|
|
437
|
+
"applies_to_actions": ["PROPOSE_SETTLEMENT"],
|
|
438
|
+
"linked_limit_key": "max_transaction_usd",
|
|
439
|
+
}
|
|
440
|
+
]
|
|
441
|
+
|
|
442
|
+
normalized = normalize_contract_dict(raw)
|
|
443
|
+
contract = CanonicalContract.model_validate(normalized)
|
|
444
|
+
validated, validation_ok, errors, _ = validate_contract(
|
|
445
|
+
normalized, preset="generic"
|
|
446
|
+
)
|
|
447
|
+
invariants = compile_authority_invariants(contract)
|
|
448
|
+
rug_limit = next(inv for inv in invariants if inv.id == "INV-RUG-PAYMENT-RANGE")
|
|
449
|
+
|
|
450
|
+
assert validated is not None
|
|
451
|
+
assert not validation_ok
|
|
452
|
+
assert any("RUNTIME_PROJECTION_INVALID" in error for error in errors)
|
|
453
|
+
assert contract.authority.exclusions == {}
|
|
454
|
+
assert contract.authority.resource_scope["match"] == {
|
|
455
|
+
"substring": ["contents coverage", "fine arts and rugs"]
|
|
456
|
+
}
|
|
457
|
+
assert rug_limit.type == "range"
|
|
458
|
+
assert rug_limit.field == "params.proposed_rug_payment_usd"
|
|
459
|
+
assert rug_limit.min == 0
|
|
460
|
+
assert rug_limit.max == 5000
|
|
461
|
+
assert rug_limit.reason_code == "POLICY_SUBLIMIT_EXCEEDED"
|
|
462
|
+
assert rug_limit.applies_to_policy_kinds == ["PROPOSE_SETTLEMENT"]
|
|
463
|
+
|
|
464
|
+
del normalized["authority"]["resource_scope"]["match"]
|
|
465
|
+
_, validation_ok, errors, _ = validate_contract(normalized, preset="generic")
|
|
466
|
+
assert validation_ok, errors
|
|
@@ -1,48 +1,204 @@
|
|
|
1
|
-
"""Tests for
|
|
1
|
+
"""Tests for contract validation, compilation and canonical rendering."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
import tempfile
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
9
|
+
import pytest
|
|
8
10
|
import yaml
|
|
9
11
|
|
|
10
|
-
from dir_core import
|
|
12
|
+
from dir_core import RuntimeContractProjection, project_contract
|
|
13
|
+
from samples.shared.contracts.provider import YamlContractProvider
|
|
11
14
|
from tools.contract.bootstrap_rules import validate_bootstrap
|
|
12
|
-
from tools.contract.
|
|
13
|
-
from tools.contract.from_sample import answers_from_sample
|
|
15
|
+
from tools.contract.cli import main
|
|
14
16
|
from tools.contract.render import render_registry_yaml, render_sample_fragment
|
|
15
|
-
from tools.contract.schema import CanonicalContract
|
|
16
|
-
from
|
|
17
|
+
from tools.contract.schema import CanonicalContract
|
|
18
|
+
from tools.contract.validation import validate_contract
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def contract_path(tmp_path: Path) -> Path:
|
|
23
|
+
contract = CanonicalContract(
|
|
24
|
+
agent_id="payments_01",
|
|
25
|
+
owner="payments.owner@example.org",
|
|
26
|
+
mission="Issue bounded refunds.",
|
|
27
|
+
authority={
|
|
28
|
+
"allowed_policy_types": ["REFUND"],
|
|
29
|
+
"limits": {"max_refund_usd": {"value": 50.0, "unit": "USD"}},
|
|
30
|
+
"invariants": [
|
|
31
|
+
{
|
|
32
|
+
"id": "INV_COUNTRY",
|
|
33
|
+
"type": "set",
|
|
34
|
+
"field": "state.property.country",
|
|
35
|
+
"denied": ["KP", "IR", "SY"],
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
)
|
|
40
|
+
path = tmp_path / "contract.yaml"
|
|
41
|
+
path.write_text(render_registry_yaml(contract), encoding="utf-8")
|
|
42
|
+
return path
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_cli_validate(contract_path: Path, capsys) -> None:
|
|
46
|
+
assert main(["validate", str(contract_path)]) == 0
|
|
47
|
+
captured = capsys.readouterr()
|
|
48
|
+
assert captured.out.startswith("OK:")
|
|
49
|
+
assert "ANALYSIS_MISSING" in captured.err
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_cli_compile_stdout_is_projection_json(contract_path: Path, capsys) -> None:
|
|
53
|
+
assert main(["compile", str(contract_path)]) == 0
|
|
54
|
+
captured = capsys.readouterr()
|
|
55
|
+
result = json.loads(captured.out)
|
|
56
|
+
projection = RuntimeContractProjection.model_validate(result)
|
|
57
|
+
expected = project_contract(
|
|
58
|
+
CanonicalContract.from_raw(yaml.safe_load(contract_path.read_text(encoding="utf-8")))
|
|
59
|
+
.model_dump(mode="json", exclude_none=True)
|
|
60
|
+
)
|
|
61
|
+
assert projection == expected
|
|
62
|
+
assert projection.release.contract_id == "payments_01"
|
|
63
|
+
assert projection.release.contract_version == "1.0.0"
|
|
64
|
+
assert any(spec.field == "state.property.country" for spec in projection.invariants)
|
|
65
|
+
assert any(spec.max == 50 for spec in projection.invariants)
|
|
66
|
+
assert "ANALYSIS_MISSING" in captured.err
|
|
67
|
+
assert main(["compile", str(contract_path)]) == 0
|
|
68
|
+
assert capsys.readouterr().out == captured.out
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_cli_compile_file(contract_path: Path, tmp_path: Path, capsys) -> None:
|
|
72
|
+
output = tmp_path / "projection.json"
|
|
73
|
+
assert main(["compile", str(contract_path), "--out", str(output)]) == 0
|
|
74
|
+
assert capsys.readouterr().out == ""
|
|
75
|
+
projection = RuntimeContractProjection.model_validate_json(output.read_text(encoding="utf-8"))
|
|
76
|
+
assert projection.agent_id == "payments_01"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest.mark.parametrize("command", ["validate", "compile"])
|
|
80
|
+
@pytest.mark.parametrize("content", ["", "- item", "authority: {{{", "null", "42"])
|
|
81
|
+
def test_cli_invalid_yaml(command: str, content: str, tmp_path: Path, capsys) -> None:
|
|
82
|
+
path = tmp_path / "invalid.yaml"
|
|
83
|
+
path.write_text(content, encoding="utf-8")
|
|
84
|
+
assert main([command, str(path)]) == 1
|
|
85
|
+
captured = capsys.readouterr()
|
|
86
|
+
assert captured.out == ""
|
|
87
|
+
assert "failed" in captured.err.lower()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@pytest.mark.parametrize("command", ["validate", "compile"])
|
|
91
|
+
def test_cli_missing_file(command: str, tmp_path: Path, capsys) -> None:
|
|
92
|
+
assert main([command, str(tmp_path / "missing.yaml")]) == 1
|
|
93
|
+
assert capsys.readouterr().err
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_cli_invalid_contract_does_not_overwrite_output(contract_path: Path, tmp_path: Path, capsys) -> None:
|
|
97
|
+
contract_path.write_text(
|
|
98
|
+
contract_path.read_text(encoding="utf-8").replace("payments.owner@example.org", "owner@example.com"),
|
|
99
|
+
encoding="utf-8",
|
|
100
|
+
)
|
|
101
|
+
output = tmp_path / "projection.json"
|
|
102
|
+
output.write_text("unchanged", encoding="utf-8")
|
|
103
|
+
assert main(["compile", str(contract_path), "--out", str(output)]) == 1
|
|
104
|
+
assert output.read_text(encoding="utf-8") == "unchanged"
|
|
105
|
+
assert "PLACEHOLDER_IDENTITY" in capsys.readouterr().err
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_cli_compile_reports_write_failure(contract_path: Path, tmp_path: Path, capsys) -> None:
|
|
109
|
+
assert main(["compile", str(contract_path), "--out", str(tmp_path)]) == 1
|
|
110
|
+
assert "compile failed" in capsys.readouterr().err
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_cli_preset_validation(contract_path: Path, capsys) -> None:
|
|
114
|
+
assert main(["validate", str(contract_path), "--preset", "trading"]) == 1
|
|
115
|
+
assert "max_order_size_usd" in capsys.readouterr().err
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@pytest.mark.parametrize("command", ["init", "from-sample"])
|
|
119
|
+
def test_cli_authoring_commands_removed(command: str, capsys) -> None:
|
|
120
|
+
with pytest.raises(SystemExit) as exc:
|
|
121
|
+
main([command])
|
|
122
|
+
assert exc.value.code == 2
|
|
123
|
+
assert "invalid choice" in capsys.readouterr().err
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_shared_validation_rejects_runtime_incompatible_role(contract_path: Path) -> None:
|
|
127
|
+
data = yaml.safe_load(contract_path.read_text(encoding="utf-8"))
|
|
128
|
+
data["subject"]["role"] = "INTERFACE"
|
|
129
|
+
data["authority"] = {}
|
|
130
|
+
contract, ok, errors, _ = validate_contract(data)
|
|
131
|
+
assert contract is not None
|
|
132
|
+
assert not ok
|
|
133
|
+
assert any("RUNTIME_PROJECTION_INVALID" in error for error in errors)
|
|
134
|
+
assert data["subject"]["role"] == "INTERFACE"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@pytest.mark.parametrize("command", ["validate", "compile"])
|
|
138
|
+
def test_cli_rejects_runtime_incompatible_scope(command: str, contract_path: Path, capsys) -> None:
|
|
139
|
+
data = yaml.safe_load(contract_path.read_text(encoding="utf-8"))
|
|
140
|
+
data["authority"]["resource_scope"] = {"match": {"substring": ["home"]}}
|
|
141
|
+
contract_path.write_text(yaml.safe_dump(data), encoding="utf-8")
|
|
142
|
+
assert main([command, str(contract_path)]) == 1
|
|
143
|
+
captured = capsys.readouterr()
|
|
144
|
+
assert captured.out == ""
|
|
145
|
+
assert "RUNTIME_PROJECTION_INVALID" in captured.err
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_shared_validation_rejects_invalid_invariant(contract_path: Path) -> None:
|
|
149
|
+
data = yaml.safe_load(contract_path.read_text(encoding="utf-8"))
|
|
150
|
+
data["authority"]["invariants"][0]["allowed"] = ["US"]
|
|
151
|
+
_, ok, errors, _ = validate_contract(data)
|
|
152
|
+
assert not ok
|
|
153
|
+
assert any("INVARIANT_SET_MODE" in error for error in errors)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_integrity_revalidates_governance_analysis(contract_path: Path) -> None:
|
|
157
|
+
from tools.contract.governance.models import GoalModel, GovernanceAnalysis, SourceBinding
|
|
158
|
+
from tools.contract.integrity import verify_contract_yaml
|
|
159
|
+
|
|
160
|
+
analysis = GovernanceAnalysis(
|
|
161
|
+
goal=GoalModel(
|
|
162
|
+
objective="Issue bounded refunds",
|
|
163
|
+
source_bindings=[SourceBinding(clause_id="UNKNOWN_CLAUSE")],
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
result = verify_contract_yaml(
|
|
167
|
+
contract_path.read_text(encoding="utf-8"), governance_analysis=analysis
|
|
168
|
+
)
|
|
169
|
+
assert not result["integrity_ok"]
|
|
170
|
+
assert any("INVALID_CLAUSE_REF" in error for error in result["errors"])
|
|
17
171
|
|
|
18
172
|
|
|
19
|
-
def
|
|
20
|
-
|
|
21
|
-
preset="fraud_gate",
|
|
173
|
+
def test_canonical_contract_and_validate() -> None:
|
|
174
|
+
contract = CanonicalContract(
|
|
22
175
|
agent_id="fraud_guard_v1",
|
|
23
|
-
owner="owner@example.
|
|
176
|
+
owner="fraud.owner@example.org",
|
|
24
177
|
role="EXECUTOR",
|
|
25
178
|
mission="Evaluate transactions.",
|
|
26
|
-
|
|
27
|
-
|
|
179
|
+
authority={
|
|
180
|
+
"allowed_policy_types": ["ALLOW", "BLOCK"],
|
|
181
|
+
"limits": {"max_transaction_usd": {"value": 5000.0, "unit": "USD"}},
|
|
182
|
+
},
|
|
28
183
|
)
|
|
29
|
-
contract = answers.to_canonical()
|
|
30
184
|
validate_bootstrap(contract, preset="fraud_gate")
|
|
31
185
|
limit = contract.authority.limits["max_transaction_usd"]
|
|
32
186
|
assert limit.value == 5000.0
|
|
33
187
|
assert limit.unit == "USD"
|
|
34
188
|
assert contract.subject.agent_id == "fraud_guard_v1"
|
|
35
|
-
assert contract.metadata.owner == "owner@example.
|
|
189
|
+
assert contract.metadata.owner == "fraud.owner@example.org"
|
|
36
190
|
|
|
37
191
|
|
|
38
192
|
def test_renderers_emit_only_canonical_contract_shape() -> None:
|
|
39
|
-
contract =
|
|
193
|
+
contract = CanonicalContract(
|
|
40
194
|
agent_id="bot_01",
|
|
41
195
|
owner="owner@example.com",
|
|
42
196
|
mission="Trade safely.",
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
197
|
+
authority={
|
|
198
|
+
"allowed_policy_types": ["BUY"],
|
|
199
|
+
"limits": {"max_order_size_usd": {"value": 1000.0, "unit": "USD"}},
|
|
200
|
+
},
|
|
201
|
+
)
|
|
46
202
|
|
|
47
203
|
for rendered in (render_registry_yaml(contract), render_sample_fragment(contract)):
|
|
48
204
|
data = yaml.safe_load(rendered)
|
|
@@ -172,41 +328,6 @@ def test_validate_authoring_rejects_placeholder_and_dup_limit() -> None:
|
|
|
172
328
|
assert any("AGGREGATE_DUP_LIMIT" in e for e in errors)
|
|
173
329
|
|
|
174
330
|
|
|
175
|
-
def test_flatten_round_trip_dir_core() -> None:
|
|
176
|
-
contract = CanonicalContract(
|
|
177
|
-
agent_id="bot_01",
|
|
178
|
-
owner="o@example.com",
|
|
179
|
-
mission="Trade safely.",
|
|
180
|
-
authority={
|
|
181
|
-
"authorized_instruments": ["ETH-USD"],
|
|
182
|
-
"allowed_policy_types": ["BUY"],
|
|
183
|
-
"max_order_size_usd": 1000.0,
|
|
184
|
-
"max_drawdown_limit_pct": 4.0,
|
|
185
|
-
},
|
|
186
|
-
)
|
|
187
|
-
flat = flatten_canonical(contract)
|
|
188
|
-
rc = ResponsibilityContract(**flat)
|
|
189
|
-
assert rc.agent_id == "bot_01"
|
|
190
|
-
assert rc.max_drawdown_limit == 0.04
|
|
191
|
-
assert flat["permissions"]["max_order_size_usd"] == 1000.0
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
def test_flatten_contract_dict_nested() -> None:
|
|
195
|
-
nested = {
|
|
196
|
-
"agent_id": "a1",
|
|
197
|
-
"role": "EXECUTOR",
|
|
198
|
-
"mission": "m",
|
|
199
|
-
"authority": {
|
|
200
|
-
"allowed_policy_types": ["HOLD"],
|
|
201
|
-
"max_order_size_usd": 500.0,
|
|
202
|
-
},
|
|
203
|
-
"responsibility": {"escalate_on_uncertainty": 0.8},
|
|
204
|
-
}
|
|
205
|
-
flat = flatten_contract_dict(nested)
|
|
206
|
-
assert flat["allowed_policy_types"] == ["HOLD"]
|
|
207
|
-
assert flat["permissions"]["max_order_size_usd"] == 500.0
|
|
208
|
-
|
|
209
|
-
|
|
210
331
|
def test_project_contract_canonical_shape() -> None:
|
|
211
332
|
projection = project_contract(
|
|
212
333
|
{
|
|
@@ -324,31 +445,6 @@ def test_explicit_invariant_conflict_detected() -> None:
|
|
|
324
445
|
assert errors
|
|
325
446
|
|
|
326
447
|
|
|
327
|
-
def test_from_sample_quick_start() -> None:
|
|
328
|
-
repo_root = Path(__file__).resolve().parents[1]
|
|
329
|
-
answers = answers_from_sample(repo_root / "samples" / "00_quick_start")
|
|
330
|
-
assert answers.agent_id == "trading_bot_01"
|
|
331
|
-
assert "max_order_size_usd" in answers.irreversible_limits
|
|
332
|
-
assert "max_drawdown_limit_pct" in answers.irreversible_limits
|
|
333
|
-
contract = answers.to_canonical()
|
|
334
|
-
validate_bootstrap(contract, preset=answers.preset)
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
def test_from_sample_preserves_canonical_invariant_ir() -> None:
|
|
338
|
-
repo_root = Path(__file__).resolve().parents[1]
|
|
339
|
-
answers = answers_from_sample(
|
|
340
|
-
repo_root / "samples" / "33_insurance_underwriting"
|
|
341
|
-
)
|
|
342
|
-
contract = answers.to_canonical()
|
|
343
|
-
limit = next(
|
|
344
|
-
spec
|
|
345
|
-
for spec in contract.authority.invariants
|
|
346
|
-
if spec.id == "INV_LIMIT_PROPOSAL_TIV"
|
|
347
|
-
)
|
|
348
|
-
assert limit.max == 3000000.0
|
|
349
|
-
assert limit.field == "params.total_insured_value"
|
|
350
|
-
|
|
351
|
-
|
|
352
448
|
def test_normalize_preserves_explicit_currency_limit() -> None:
|
|
353
449
|
contract = CanonicalContract.from_raw(
|
|
354
450
|
{
|
|
@@ -371,38 +467,6 @@ def test_normalize_preserves_explicit_currency_limit() -> None:
|
|
|
371
467
|
assert contract.authority.limits["max_refund_eur"].unit == "EUR"
|
|
372
468
|
|
|
373
469
|
|
|
374
|
-
def test_yaml_contract_provider_flattens_nested() -> None:
|
|
375
|
-
from samples.shared.contracts.provider import YamlContractProvider
|
|
376
|
-
|
|
377
|
-
nested = {
|
|
378
|
-
"agents": [
|
|
379
|
-
{
|
|
380
|
-
"agent_id": "nested_agent",
|
|
381
|
-
"contract": {
|
|
382
|
-
"role": "EXECUTOR",
|
|
383
|
-
"mission": "Test nested load.",
|
|
384
|
-
"authority": {
|
|
385
|
-
"allowed_policy_types": ["ACTION"],
|
|
386
|
-
"max_transaction_usd": 100.0,
|
|
387
|
-
},
|
|
388
|
-
"responsibility": {"escalate_on_uncertainty": 0.6},
|
|
389
|
-
},
|
|
390
|
-
}
|
|
391
|
-
]
|
|
392
|
-
}
|
|
393
|
-
with tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False) as handle:
|
|
394
|
-
yaml.dump(nested, handle)
|
|
395
|
-
path = handle.name
|
|
396
|
-
|
|
397
|
-
try:
|
|
398
|
-
provider = YamlContractProvider(path)
|
|
399
|
-
contract = provider.get_contract("nested_agent")
|
|
400
|
-
assert contract.agent_id == "nested_agent"
|
|
401
|
-
assert contract.allowed_policy_types == ["ACTION"]
|
|
402
|
-
finally:
|
|
403
|
-
Path(path).unlink(missing_ok=True)
|
|
404
|
-
|
|
405
|
-
|
|
406
470
|
def test_yaml_contract_provider_loads_canonical_envelope() -> None:
|
|
407
471
|
nested = {
|
|
408
472
|
"agents": [
|
|
@@ -4,13 +4,14 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from tools.contract.governance.models import (
|
|
6
6
|
ActionClass,
|
|
7
|
-
GovernanceAnalysis,
|
|
8
7
|
GoalModel,
|
|
8
|
+
GovernanceAnalysis,
|
|
9
9
|
InvariantCandidate,
|
|
10
10
|
PredicateAST,
|
|
11
11
|
SourceBinding,
|
|
12
12
|
)
|
|
13
13
|
from tools.contract.governance.validation import validate_governance_analysis
|
|
14
|
+
from tools.contract.schema import IRREVERSIBLE_LIMIT_KEYS
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def _valid_analysis() -> GovernanceAnalysis:
|
|
@@ -130,3 +131,24 @@ def test_valid_analysis_passes() -> None:
|
|
|
130
131
|
}
|
|
131
132
|
report = validate_governance_analysis(analysis=analysis, contract_dict=contract)
|
|
132
133
|
assert report.blocking_ok
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_noncanonical_linked_limit_is_warning_with_allowed_keys() -> None:
|
|
137
|
+
analysis = _valid_analysis()
|
|
138
|
+
analysis.invariant_candidates[0].linked_limit_key = "max_payout_usd"
|
|
139
|
+
report = validate_governance_analysis(
|
|
140
|
+
analysis=analysis,
|
|
141
|
+
contract_dict={
|
|
142
|
+
"authority": {
|
|
143
|
+
"allowed_policy_types": ["BUY"],
|
|
144
|
+
"limits": {"max_order_size_usd": {"value": 50000.0, "unit": "USD"}},
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
assert report.blocking_ok
|
|
150
|
+
issue = next(issue for issue in report.warnings if issue.code == "INVALID_LIMIT_KEY")
|
|
151
|
+
assert issue.severity == "warning"
|
|
152
|
+
assert "max_payout_usd" in issue.message
|
|
153
|
+
for key in IRREVERSIBLE_LIMIT_KEYS:
|
|
154
|
+
assert key in issue.message
|
|
@@ -203,3 +203,49 @@ def test_evaluate_invariants_collects_all_failures() -> None:
|
|
|
203
203
|
"PROHIBITED_TERRITORY",
|
|
204
204
|
"AUTHORITY_CEILING",
|
|
205
205
|
]
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def test_set_rule_allowlist_reads_authoritative_state() -> None:
|
|
209
|
+
rule = InvariantSpec(
|
|
210
|
+
id="INV_SUPPORTED_POLICY_TYPES",
|
|
211
|
+
type="set",
|
|
212
|
+
field="state.policy.type",
|
|
213
|
+
allowed=["HOMEOWNERS", "CONDO"],
|
|
214
|
+
reason_code="UNSUPPORTED_POLICY_TYPE",
|
|
215
|
+
)
|
|
216
|
+
proposal = _proposal(params={})
|
|
217
|
+
assert evaluate_set_rule(rule, proposal, {"state": {"policy": {"type": "CONDO"}}}) is None
|
|
218
|
+
failure = evaluate_set_rule(rule, proposal, {"state": {"policy": {"type": "COMMERCIAL"}}})
|
|
219
|
+
assert failure is not None
|
|
220
|
+
assert failure.reason == "UNSUPPORTED_POLICY_TYPE"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_set_rule_on_state_ignores_value_claimed_by_agent() -> None:
|
|
224
|
+
rule = InvariantSpec(
|
|
225
|
+
id="INV_RESTRICTED_COUNTRIES",
|
|
226
|
+
type="set",
|
|
227
|
+
field="state.property.country",
|
|
228
|
+
denied=["KP", "IR", "SY"],
|
|
229
|
+
reason_code="COUNTRY_NOT_PERMITTED",
|
|
230
|
+
)
|
|
231
|
+
lying_proposal = _proposal(params={"property_country": "US"})
|
|
232
|
+
failure = evaluate_set_rule(
|
|
233
|
+
rule,
|
|
234
|
+
lying_proposal,
|
|
235
|
+
{"state": {"property": {"country": "KP"}}},
|
|
236
|
+
)
|
|
237
|
+
assert failure is not None
|
|
238
|
+
assert failure.reason == "COUNTRY_NOT_PERMITTED"
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def test_range_rule_on_state_fails_closed_when_state_missing() -> None:
|
|
242
|
+
rule = InvariantSpec(
|
|
243
|
+
id="INV_STATE_LIMIT",
|
|
244
|
+
type="range",
|
|
245
|
+
field="state.claim.reserve_usd",
|
|
246
|
+
max=1000,
|
|
247
|
+
)
|
|
248
|
+
failure = evaluate_range_rule(rule, _proposal(params={"reserve_usd": 1}), {"state": {}})
|
|
249
|
+
assert failure is not None
|
|
250
|
+
assert failure.reason == DimReasonCode.CONTRACT_PARAMETER_MISSING
|
|
251
|
+
assert evaluate_range_rule(rule, _proposal(params={}), None) is not None
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from dir_core import (
|
|
6
|
+
ContractRole,
|
|
7
|
+
RuntimeContractProjection,
|
|
8
|
+
project_contract,
|
|
9
|
+
responsibility_contract_from_projection,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _canonical(**overrides):
|
|
14
|
+
contract = {
|
|
15
|
+
"api_version": "roa.dir/v1",
|
|
16
|
+
"kind": "ResponsibilityContract",
|
|
17
|
+
"metadata": {"version": "1.0.0"},
|
|
18
|
+
"subject": {
|
|
19
|
+
"agent_id": "pos_01",
|
|
20
|
+
"role": "EXECUTOR",
|
|
21
|
+
"parent_agent_id": "strategist_01",
|
|
22
|
+
},
|
|
23
|
+
"mission": {"statement": "Hold safely."},
|
|
24
|
+
"authority": {
|
|
25
|
+
"allowed_policy_types": ["HOLD", "CLOSE"],
|
|
26
|
+
"resource_scope": {"instruments": ["ETH-USD"]},
|
|
27
|
+
"limits": {"max_order_size_usd": {"value": 1000, "unit": "USD"}},
|
|
28
|
+
},
|
|
29
|
+
"execution_conditions": {"wake_up_threshold_pct": 0.25},
|
|
30
|
+
"responsibility": {"escalation": {"mode": "mandatory", "confidence_below": 0.6}},
|
|
31
|
+
}
|
|
32
|
+
contract.update(overrides)
|
|
33
|
+
return contract
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_projection_keeps_parent_agent_id() -> None:
|
|
37
|
+
projection = project_contract(_canonical())
|
|
38
|
+
assert isinstance(projection, RuntimeContractProjection)
|
|
39
|
+
assert projection.parent_agent_id == "strategist_01"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_adapter_maps_canonical_projection_to_flat_contract() -> None:
|
|
43
|
+
rc = responsibility_contract_from_projection(project_contract(_canonical()))
|
|
44
|
+
assert rc.agent_id == "pos_01"
|
|
45
|
+
assert rc.role == ContractRole.EXECUTOR
|
|
46
|
+
assert rc.mission == "Hold safely."
|
|
47
|
+
assert rc.authorized_instruments == ["ETH-USD"]
|
|
48
|
+
assert rc.allowed_policy_types == ["HOLD", "CLOSE"]
|
|
49
|
+
assert rc.escalate_on_uncertainty == 0.6
|
|
50
|
+
assert rc.wake_up_threshold_pct == 0.25
|
|
51
|
+
assert rc.parent_agent_id == "strategist_01"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_adapter_uses_defaults_when_optional_blocks_missing() -> None:
|
|
55
|
+
contract = _canonical(execution_conditions={}, responsibility={})
|
|
56
|
+
rc = responsibility_contract_from_projection(project_contract(contract))
|
|
57
|
+
assert rc.escalate_on_uncertainty == 0.7
|
|
58
|
+
assert rc.wake_up_threshold_pct == 0.5
|
|
59
|
+
assert rc.max_drawdown_limit == 0.05
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_adapter_drawdown_fraction_and_percent() -> None:
|
|
63
|
+
fraction = _canonical()
|
|
64
|
+
fraction["authority"]["limits"]["max_drawdown_limit"] = {"value": 0.04, "unit": "fraction"}
|
|
65
|
+
assert responsibility_contract_from_projection(project_contract(fraction)).max_drawdown_limit == 0.04
|
|
66
|
+
|
|
67
|
+
percent = _canonical()
|
|
68
|
+
percent["authority"]["limits"]["max_drawdown_limit_pct"] = {"value": 4.0, "unit": "percent"}
|
|
69
|
+
assert responsibility_contract_from_projection(project_contract(percent)).max_drawdown_limit == pytest.approx(0.04)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Samples are Runtime consumers: they may depend on ``dir_core`` but never on Build-Time ``tools``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
SAMPLES_DIR = Path(__file__).resolve().parents[1] / "samples"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _imported_modules(path: Path) -> list[str]:
|
|
12
|
+
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
|
13
|
+
modules: list[str] = []
|
|
14
|
+
for node in ast.walk(tree):
|
|
15
|
+
if isinstance(node, ast.Import):
|
|
16
|
+
modules.extend(alias.name for alias in node.names)
|
|
17
|
+
elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module:
|
|
18
|
+
modules.append(node.module)
|
|
19
|
+
return modules
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_samples_do_not_import_build_time_tools() -> None:
|
|
23
|
+
offenders = [
|
|
24
|
+
f"{path.relative_to(SAMPLES_DIR)}: {module}"
|
|
25
|
+
for path in SAMPLES_DIR.rglob("*.py")
|
|
26
|
+
if "__pycache__" not in path.parts
|
|
27
|
+
for module in _imported_modules(path)
|
|
28
|
+
if module == "tools" or module.startswith("tools.")
|
|
29
|
+
]
|
|
30
|
+
assert not offenders, f"samples must not depend on tools: {offenders}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_samples_do_not_mutate_sys_path_with_repo_root() -> None:
|
|
34
|
+
offenders = [
|
|
35
|
+
str(path.relative_to(SAMPLES_DIR))
|
|
36
|
+
for path in SAMPLES_DIR.rglob("*.py")
|
|
37
|
+
if "__pycache__" not in path.parts and "parents[3]" in path.read_text(encoding="utf-8")
|
|
38
|
+
]
|
|
39
|
+
assert not offenders, f"samples must not add the repo root to sys.path: {offenders}"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|