From 1afcf9191304ee60615d293e0306d7d3ee3c0df2 Mon Sep 17 00:00:00 2001 From: agent-db0ec53c058f1326 Date: Thu, 16 Apr 2026 23:00:17 +0200 Subject: [PATCH] build(agent): molt-z#db0ec5 iteration --- nova_plan/contracts.py | 54 +++++++++++++++++++++++++++++++++++++ tests/test_cac_contracts.py | 29 ++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 tests/test_cac_contracts.py diff --git a/nova_plan/contracts.py b/nova_plan/contracts.py index ae35526..97e4a91 100644 --- a/nova_plan/contracts.py +++ b/nova_plan/contracts.py @@ -16,10 +16,30 @@ class PlanDelta: agent_id: str delta: Dict[str, float] timestamp: float + # Optional fields to support CRDT-like, partition-tolerant merges + parent_version: int | None = None + sequence: int | None = None def to_json(self) -> str: return json.dumps(asdict(self)) +# Simple CRDT-style merge helper (shadow plan example, not a full CRDT) +def crdt_merge_deltas(d1: "PlanDelta", d2: "PlanDelta") -> "PlanDelta": + merged_delta = {**d1.delta, **d2.delta} + merged_agent = d2.agent_id if d2.agent_id else d1.agent_id + merged_ts = max(d1.timestamp, d2.timestamp) + merged_parent = None + if d1.parent_version is not None or d2.parent_version is not None: + v1 = d1.parent_version if d1.parent_version is not None else 0 + v2 = d2.parent_version if d2.parent_version is not None else 0 + merged_parent = max(v1, v2) + merged_seq = None + if d1.sequence is not None or d2.sequence is not None: + s1 = d1.sequence if d1.sequence is not None else 0 + s2 = d2.sequence if d2.sequence is not None else 0 + merged_seq = max(s1, s2) + return PlanDelta(agent_id=merged_agent, delta=merged_delta, timestamp=merged_ts, parent_version=merged_parent, sequence=merged_seq) + @dataclass class SharedSchedule: schedule: Dict[str, Any] @@ -176,6 +196,40 @@ AdapterRegistry.register_schema( "types": {"adapter_id": str, "status": dict}, }, ) + +# ---------------- CaC (Contract-as-Code) primitives ----------------- +@dataclass +class CaCContract: + contract_id: str + version: int + content: Dict[str, Any] + signature: str | None = None + + def to_json(self) -> str: + return json.dumps({ + "contract_id": self.contract_id, + "version": self.version, + "content": self.content, + "signature": self.signature, + }) + +def sign_ca_contract(contract: CaCContract, key: str) -> CaCContract: + import hashlib, json + payload = json.dumps(contract.content, sort_keys=True).encode() + contract.signature = hashlib.sha256((key).encode() + payload).hexdigest() + return contract + +class CaCRegistry: + _contracts: Dict[str, CaCContract] = {} + + @classmethod + def register(cls, contract: CaCContract) -> None: + cls._contracts[contract.contract_id] = contract + + @classmethod + def get(cls, contract_id: str) -> CaCContract | None: + return cls._contracts.get(contract_id) + AdapterRegistry.register_schema( name="HabitatAdapter", version=1, diff --git a/tests/test_cac_contracts.py b/tests/test_cac_contracts.py new file mode 100644 index 0000000..7bb2084 --- /dev/null +++ b/tests/test_cac_contracts.py @@ -0,0 +1,29 @@ +import json + +from nova_plan.contracts import CaCContract, CaCRegistry, sign_ca_contract, crdt_merge_deltas, PlanDelta + + +def test_cac_contract_signing_and_serialization(): + c = CaCContract(contract_id="cac-001", version=1, content={"name": "NovaPlan", "purpose": "MVP"}) + signed = sign_ca_contract(c, key="topsecret") + assert signed.signature is not None + s = signed.to_json() + # Ensure JSON can be parsed back without error + parsed = json.loads(s) + assert parsed.get("contract_id") == "cac-001" + + +def test_crdt_merge_deltas_basic(): + d1 = PlanDelta(agent_id="a1", delta={"x": 1.0}, timestamp=1.0) + d2 = PlanDelta(agent_id="a2", delta={"y": 2.0}, timestamp=2.0) + merged = crdt_merge_deltas(d1, d2) + assert isinstance(merged, PlanDelta) + assert "x" in merged.delta and "y" in merged.delta + assert merged.timestamp == 2.0 + + +def test_cac_registry_store_and_retrieve(): + c = CaCContract(contract_id="cac-002", version=1, content={"foo": "bar"}) + CaCRegistry.register(c) + retrieved = CaCRegistry.get("cac-002") + assert retrieved is c