build(agent): new-agents-3#dd492b iteration

This commit is contained in:
agent-dd492b85242a98c5 2026-04-21 10:38:15 +02:00
parent 0b20970c5c
commit 1fed725255
12 changed files with 262 additions and 27 deletions

View File

@ -17,5 +17,11 @@ Privacy controls
- Server.aggregate also supports an optional clip_norm parameter to clip all incoming updates prior to averaging, providing an additional privacy safeguard.
- You can combine clipping with Gaussian noise (noise_scale) for stronger privacy guarantees.
Enhancements added (Planning Lens MVP)
- PlanDelta provenance fields: The PlanDelta data model now includes optional provenance metadata (timestamp, author, contract_id, signature) to support auditing and deterministic replay in offline/offline-first scenarios.
- Adapters enhanced to carry provenance in contract payloads: Canonical, CatOptBridge, and EnergiBridge serialize/deserialize new PlanDelta fields when present.
- MVP-oriented interoperability surface improved: The bridge adapters now carry additional fields that enable governance and traceability without breaking existing usage patterns.
- This work aligns with the MVP roadmap to enable a Graph-of-Contracts (GoC) registry-based ecosystem with signed deltas, versioned adapters, and offline-first resilience.
How to run tests:
- This repository provides a test script via test.sh (see below).

View File

@ -1,23 +1,18 @@
"""Interplanetary Edge Orchestrator: Privacy-Preserving Federated Optimization
This package provides a lightweight simulation of a federated learning
orchestrator suitable for offline-first operation in space habitats.
"""
Public package for Interplanetary Edge Orchestrator (Privacy MVP).
"""
from .core import LocalProblem, SharedVariables, PlanDelta, DualVariables, PrivacyBudget, AuditLog, PolicyBlock
from .federated import Client, Server
from .dsl import PrivacyBudget, AuditLog # export governance primitives for adapters
try:
# Optional: Expose registry helpers for MVP interoperability without
# forcing a hard dependency in tests that only exercise federation.
from .go_registry import register_contract, get_contract_schema, list_contracts # type: ignore
__all__ = [
"Client",
"Server",
"PrivacyBudget",
"AuditLog",
"register_contract",
"get_contract_schema",
"list_contracts",
]
except Exception:
# If registry is not available for some environments, still expose the core APIs.
__all__ = ["Client", "Server", "PrivacyBudget", "AuditLog"]
__all__ = [
"LocalProblem",
"SharedVariables",
"PlanDelta",
"DualVariables",
"PrivacyBudget",
"AuditLog",
"PolicyBlock",
"Client",
"Server",
]

View File

@ -23,6 +23,7 @@ class CanonicalAdapter:
return LocalProblem(problem_id=problem_id, features=features, objective="minimize_cost")
def map_plan_delta(self, version: int, delta: Dict[str, Any], insight: str | None = None) -> PlanDelta:
# Build PlanDelta with optional provenance fields left as None by default.
return PlanDelta(version=version, delta=delta, insight=insight)
def map_privacy_budget(self, budget: PrivacyBudget) -> Dict[str, Any]:

View File

@ -51,13 +51,18 @@ class CatOptBridge:
def map_plan_delta(self, delta: PlanDelta) -> Dict[str, Any]:
"""Serialize PlanDelta into the contract surface."""
return {
payload = {
"contract_version": self.CONTRACT_VERSION,
"type": "PlanDelta",
"version": delta.version,
"delta": delta.delta,
"insight": delta.insight,
"timestamp": delta.timestamp,
"author": delta.author,
"contract_id": delta.contract_id,
"signature": delta.signature,
}
return payload
def to_contract(self, problem: LocalProblem, shared: SharedVariables, delta: Optional[PlanDelta] = None) -> Dict[str, Any]:
"""Combine DSL primitives into a single contract payload."""
@ -96,6 +101,10 @@ class CatOptBridge:
version=delta_payload.get("version", 0),
delta=delta_payload.get("delta", {}),
insight=delta_payload.get("insight"),
timestamp=delta_payload.get("timestamp"),
author=delta_payload.get("author"),
contract_id=delta_payload.get("contract_id"),
signature=delta_payload.get("signature"),
)
return lp, sv, delta

View File

@ -53,6 +53,10 @@ class EnergiBridge:
"version": delta.version,
"delta": delta.delta,
"insight": delta.insight,
"timestamp": delta.timestamp,
"author": delta.author,
"contract_id": delta.contract_id,
"signature": delta.signature,
}
return payload
@ -77,5 +81,9 @@ class EnergiBridge:
version=delta_payload.get("version", 0),
delta=delta_payload.get("delta", {}),
insight=delta_payload.get("insight"),
timestamp=delta_payload.get("timestamp"),
author=delta_payload.get("author"),
contract_id=delta_payload.get("contract_id"),
signature=delta_payload.get("signature"),
)
return _lp, _sv, _delta
return None

View File

@ -0,0 +1,14 @@
from __future__ import annotations
from typing import Dict, Any
class HabitatModuleAdapter:
"""
Toy adapter for habitat scheduling.
"""
name = "habitat_module"
def schedule(self, problem: Dict[str, Any]) -> Dict[str, Any]:
return {
"schedule_id": f"habitat-{problem.get('id', 'unknown')}",
"tasks": ["maintenance_check", "air_filter_replace"],
}

View File

@ -0,0 +1,15 @@
from __future__ import annotations
from typing import Dict, Any
class RoverPlannerAdapter:
"""
Toy adapter for rover route planning.
"""
name = "rover_planner"
def plan(self, problem: Dict[str, Any]) -> Dict[str, Any]:
# Very naive planner: simply echoes the problem and returns a dummy plan
return {
"plan_id": f"rover-{problem.get('id', 'unknown')}",
"actions": ["move_forward", "scan_area", "return_to_base"],
}

View File

@ -0,0 +1,122 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Dict, Optional, List
import time
import json
# Canonical IR primitives (minimal)
@dataclass
class LocalProblem:
id: str
domain: str
assets: List[str]
objective: str
constraints: Optional[Dict[str, Any]] = field(default_factory=dict)
solver_hint: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
return {
"id": self.id,
"domain": self.domain,
"assets": self.assets,
"objective": self.objective,
"constraints": self.constraints or {},
"solver_hint": self.solver_hint,
}
@dataclass
class SharedVariables:
# Lightweight, versioned signals
versions: Dict[str, int] = field(default_factory=dict)
payloads: Dict[str, Any] = field(default_factory=dict)
encryption_schema: Optional[str] = None
def bump_version(self, key: str) -> int:
self.versions[key] = self.versions.get(key, 0) + 1
return self.versions[key]
def to_dict(self) -> Dict[str, Any]:
return {
"versions": self.versions,
"payloads": self.payloads,
"encryption_schema": self.encryption_schema,
}
@dataclass
class PlanDelta:
delta: Dict[str, Any]
timestamp: float = field(default_factory=lambda: time.time())
author: Optional[str] = None
contract_id: Optional[str] = None
signature: Optional[str] = None
def sign(self, signer: str) -> None:
# Simple deterministic "signature" for demo purposes
payload = json.dumps({"delta": self.delta, "timestamp": self.timestamp, "author": signer}, sort_keys=True)
self.signature = f"sig-{abs(hash(payload))}"
self.author = signer
def to_dict(self) -> Dict[str, Any]:
return {
"delta": self.delta,
"timestamp": self.timestamp,
"author": self.author,
"contract_id": self.contract_id,
"signature": self.signature,
}
@dataclass
class DualVariables:
multipliers: Dict[str, float] = field(default_factory=dict)
def set(self, name: str, value: float) -> None:
self.multipliers[name] = value
def to_dict(self) -> Dict[str, Any]:
return {"multipliers": self.multipliers}
@dataclass
class PrivacyBudget:
signal: str
budget: float
expiry: float # epoch
def is_expired(self) -> bool:
return time.time() > self.expiry
def to_dict(self) -> Dict[str, Any]:
return {"signal": self.signal, "budget": self.budget, "expiry": self.expiry}
@dataclass
class AuditLog:
entry: str
signer: str
timestamp: float
contract_id: Optional[str] = None
version: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
return {
"entry": self.entry,
"signer": self.signer,
"timestamp": self.timestamp,
"contract_id": self.contract_id,
"version": self.version,
}
@dataclass
class PolicyBlock:
safety: Optional[str] = None
exposure_controls: Optional[Dict[str, Any]] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
return {
"safety": self.safety,
"exposure_controls": self.exposure_controls or {},
}
# Tiny helper for serialization (to aid tests)
def serialize(obj: Any) -> str:
if hasattr(obj, "to_dict"):
return json.dumps(obj.to_dict(), sort_keys=True)
return json.dumps(obj, default=lambda o: o.__dict__, sort_keys=True)

View File

@ -35,10 +35,22 @@ class SharedVariables:
@dataclass
class PlanDelta:
"""Represents an incremental plan change derived from optimization."""
"""Represents an incremental plan change derived from optimization.
Extended with optional provenance fields to support auditing and offline replay:
- timestamp: when the delta was created
- author: identifier of the delta creator
- contract_id: contract/session identifier
- signature: cryptographic signature proving integrity
"""
version: int
delta: Dict[str, Any] = field(default_factory=dict)
insight: Optional[str] = None
# Provenance / governance fields (optional)
timestamp: Optional[float] = None
author: Optional[str] = None
contract_id: Optional[str] = None
signature: Optional[str] = None
@dataclass

View File

@ -0,0 +1,18 @@
from __future__ import annotations
from typing import Dict, Any, List
from ..core import PlanDelta
def merge_plan_deltas(base: PlanDelta, incoming: PlanDelta) -> PlanDelta:
"""
Minimal deterministic merge: if incoming timestamp is newer, replace.
In real MVP this would be a CRDT-like merge with tombstones.
"""
if incoming.timestamp > base.timestamp:
return incoming
return base
def deterministic_replay(plan: List[PlanDelta]) -> List[PlanDelta]:
"""
Return deltas in ascending timestamp order to simulate replay after islanding.
"""
return sorted(plan, key=lambda d: d.timestamp)

View File

@ -1,8 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
# Run unit tests
pytest -q
# Build the package to verify packaging metadata and directory structure
python3 -m build

39
tests/test_core.py Normal file
View File

@ -0,0 +1,39 @@
import time
from interplanetary_edge_orchestrator_privacy.core import LocalProblem, SharedVariables, PlanDelta, DualVariables, PrivacyBudget, AuditLog
def test_local_problem_basic():
lp = LocalProblem(
id="lp1",
domain="rover",
assets=["rover1"],
objective="minimize_energy"
)
assert lp.id == "lp1"
assert lp.domain == "rover"
assert "rover1" in lp.assets
def test_shared_variables_versioning():
sv = SharedVariables()
v1 = sv.bump_version("forecasts")
v2 = sv.bump_version("forecasts")
assert v1 == 1 and v2 == 2
sv.payloads["forecasts"] = {"temp": 20}
d = sv.to_dict()
assert "forecasts" in d["payloads"]
def test_plan_delta_signature():
pd = PlanDelta(delta={"delta": "update"})
pd.sign("agent-A")
assert pd.author == "agent-A"
assert pd.signature is not None
def test_dual_and_privacy_and_audit():
dv = DualVariables()
dv.set("lambda", 1.23)
assert dv.multipliers["lambda"] == 1.23
pb = PrivacyBudget(signal="mesh-energy", budget=0.5, expiry=time.time() + 60)
assert not pb.is_expired()
al = AuditLog(entry="init", signer="agent-A", timestamp=time.time())
assert al.entry == "init"