build(agent): new-agents-3#dd492b iteration
This commit is contained in:
parent
0b20970c5c
commit
1fed725255
|
|
@ -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.
|
- 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.
|
- 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:
|
How to run tests:
|
||||||
- This repository provides a test script via test.sh (see below).
|
- This repository provides a test script via test.sh (see below).
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,18 @@
|
||||||
"""Interplanetary Edge Orchestrator: Privacy-Preserving Federated Optimization
|
"""
|
||||||
This package provides a lightweight simulation of a federated learning
|
Public package for Interplanetary Edge Orchestrator (Privacy MVP).
|
||||||
orchestrator suitable for offline-first operation in space habitats.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from .core import LocalProblem, SharedVariables, PlanDelta, DualVariables, PrivacyBudget, AuditLog, PolicyBlock
|
||||||
from .federated import Client, Server
|
from .federated import Client, Server
|
||||||
from .dsl import PrivacyBudget, AuditLog # export governance primitives for adapters
|
|
||||||
try:
|
__all__ = [
|
||||||
# Optional: Expose registry helpers for MVP interoperability without
|
"LocalProblem",
|
||||||
# forcing a hard dependency in tests that only exercise federation.
|
"SharedVariables",
|
||||||
from .go_registry import register_contract, get_contract_schema, list_contracts # type: ignore
|
"PlanDelta",
|
||||||
__all__ = [
|
"DualVariables",
|
||||||
"Client",
|
|
||||||
"Server",
|
|
||||||
"PrivacyBudget",
|
"PrivacyBudget",
|
||||||
"AuditLog",
|
"AuditLog",
|
||||||
"register_contract",
|
"PolicyBlock",
|
||||||
"get_contract_schema",
|
"Client",
|
||||||
"list_contracts",
|
"Server",
|
||||||
]
|
]
|
||||||
except Exception:
|
|
||||||
# If registry is not available for some environments, still expose the core APIs.
|
|
||||||
__all__ = ["Client", "Server", "PrivacyBudget", "AuditLog"]
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ class CanonicalAdapter:
|
||||||
return LocalProblem(problem_id=problem_id, features=features, objective="minimize_cost")
|
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:
|
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)
|
return PlanDelta(version=version, delta=delta, insight=insight)
|
||||||
|
|
||||||
def map_privacy_budget(self, budget: PrivacyBudget) -> Dict[str, Any]:
|
def map_privacy_budget(self, budget: PrivacyBudget) -> Dict[str, Any]:
|
||||||
|
|
|
||||||
|
|
@ -51,13 +51,18 @@ class CatOptBridge:
|
||||||
|
|
||||||
def map_plan_delta(self, delta: PlanDelta) -> Dict[str, Any]:
|
def map_plan_delta(self, delta: PlanDelta) -> Dict[str, Any]:
|
||||||
"""Serialize PlanDelta into the contract surface."""
|
"""Serialize PlanDelta into the contract surface."""
|
||||||
return {
|
payload = {
|
||||||
"contract_version": self.CONTRACT_VERSION,
|
"contract_version": self.CONTRACT_VERSION,
|
||||||
"type": "PlanDelta",
|
"type": "PlanDelta",
|
||||||
"version": delta.version,
|
"version": delta.version,
|
||||||
"delta": delta.delta,
|
"delta": delta.delta,
|
||||||
"insight": delta.insight,
|
"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]:
|
def to_contract(self, problem: LocalProblem, shared: SharedVariables, delta: Optional[PlanDelta] = None) -> Dict[str, Any]:
|
||||||
"""Combine DSL primitives into a single contract payload."""
|
"""Combine DSL primitives into a single contract payload."""
|
||||||
|
|
@ -96,6 +101,10 @@ class CatOptBridge:
|
||||||
version=delta_payload.get("version", 0),
|
version=delta_payload.get("version", 0),
|
||||||
delta=delta_payload.get("delta", {}),
|
delta=delta_payload.get("delta", {}),
|
||||||
insight=delta_payload.get("insight"),
|
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 lp, sv, delta
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,10 @@ class EnergiBridge:
|
||||||
"version": delta.version,
|
"version": delta.version,
|
||||||
"delta": delta.delta,
|
"delta": delta.delta,
|
||||||
"insight": delta.insight,
|
"insight": delta.insight,
|
||||||
|
"timestamp": delta.timestamp,
|
||||||
|
"author": delta.author,
|
||||||
|
"contract_id": delta.contract_id,
|
||||||
|
"signature": delta.signature,
|
||||||
}
|
}
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
@ -77,5 +81,9 @@ class EnergiBridge:
|
||||||
version=delta_payload.get("version", 0),
|
version=delta_payload.get("version", 0),
|
||||||
delta=delta_payload.get("delta", {}),
|
delta=delta_payload.get("delta", {}),
|
||||||
insight=delta_payload.get("insight"),
|
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
|
||||||
|
|
|
||||||
|
|
@ -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"],
|
||||||
|
}
|
||||||
|
|
@ -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"],
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -35,10 +35,22 @@ class SharedVariables:
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PlanDelta:
|
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
|
version: int
|
||||||
delta: Dict[str, Any] = field(default_factory=dict)
|
delta: Dict[str, Any] = field(default_factory=dict)
|
||||||
insight: Optional[str] = None
|
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
|
@dataclass
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
4
test.sh
4
test.sh
|
|
@ -1,8 +1,4 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Run unit tests
|
|
||||||
pytest -q
|
pytest -q
|
||||||
|
|
||||||
# Build the package to verify packaging metadata and directory structure
|
|
||||||
python3 -m build
|
python3 -m build
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
Loading…
Reference in New Issue