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.
|
||||
- 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).
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
||||
__all__ = [
|
||||
"LocalProblem",
|
||||
"SharedVariables",
|
||||
"PlanDelta",
|
||||
"DualVariables",
|
||||
"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"]
|
||||
"PolicyBlock",
|
||||
"Client",
|
||||
"Server",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
set -euo pipefail
|
||||
|
||||
# Run unit tests
|
||||
pytest -q
|
||||
|
||||
# Build the package to verify packaging metadata and directory structure
|
||||
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