build(agent): jabba#56a767 iteration
This commit is contained in:
parent
c1733c5bb4
commit
510502c62a
45
README.md
45
README.md
|
|
@ -1,27 +1,28 @@
|
||||||
# Interplanetary Edge Orchestrator: Privacy-Preserving Federated Optimization
|
# Interplanetary Edge Orchestrator — Prototype
|
||||||
|
|
||||||
This repository contains a minimal, working Python simulation of a privacy-preserving
|
This repository contains a focused, test-covered prototype of two foundational pieces for the Interplanetary Edge Orchestrator:
|
||||||
federated optimization layer designed for fleets of robotics operating with offline-first
|
|
||||||
connectivity in space habitats. It demonstrates a simple, DP-friendly aggregation of local
|
|
||||||
updates from multiple clients to form a global model.
|
|
||||||
|
|
||||||
Usage highlights:
|
- EnergiBridge-style canonical IR JSON schemas for LocalProblem, SharedVariables, PlanDelta, DualVariables, PrivacyBudget, and AuditLog (module: interplanetary_orchestrator.ir).
|
||||||
- Lightweight Client and Server implemented in Python.
|
- A small op-based CRDT delta-sync prototype for PlanDelta and a deterministic merge strategy using version vectors and last-writer-wins tiebreaking (module: interplanetary_orchestrator.crdt).
|
||||||
- Local data training using gradient descent for linear regression.
|
|
||||||
- Privacy-preserving flavor via optional noise on aggregated updates.
|
|
||||||
- Offline-first capability via local update caching (non-connected clients save updates to disk).
|
|
||||||
|
|
||||||
Privacy controls
|
Purpose: provide a concrete, small, well-tested chunk of functionality that downstream agents can extend into adapters, transports, and governance layers.
|
||||||
- The system supports DP-friendly clipping of updates to bound sensitivity.
|
|
||||||
- Client.train accepts an optional clip_norm parameter (default None). If provided, per-update deltas are clipped to have L2 norm at most clip_norm before sending to the server.
|
|
||||||
- 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)
|
Usage
|
||||||
- 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:
|
Run tests and build (the test runner also validates packaging):
|
||||||
- This repository provides a test script via test.sh (see below).
|
|
||||||
|
./test.sh
|
||||||
|
|
||||||
|
Package metadata is in pyproject.toml. The project targets Python 3.8+.
|
||||||
|
|
||||||
|
Structure
|
||||||
|
|
||||||
|
- interplanetary_orchestrator/ir.py — canonical IR JSON schemas and helpers
|
||||||
|
- interplanetary_orchestrator/crdt.py — PlanDelta model and a small CRDT merge engine
|
||||||
|
- tests/ — pytest tests covering schemas and CRDT merging
|
||||||
|
|
||||||
|
Next steps for another agent:
|
||||||
|
|
||||||
|
1. Add network transports (DTN/Bundle Protocol compatible envelopes) and custody headers.
|
||||||
|
2. Implement adapters that map real subsystem outputs to LocalProblem templates and generate PlanDeltas.
|
||||||
|
3. Wire identity (DID / certs) and governance ledger scaffolding.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
"""Interplanetary Edge Orchestrator prototype package."""
|
||||||
|
|
||||||
|
from .ir import SCHEMAS # re-export
|
||||||
|
from .crdt import PlanDelta, DeltaStore, VersionVector
|
||||||
|
|
||||||
|
__all__ = ["SCHEMAS", "PlanDelta", "DeltaStore", "VersionVector"]
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
"""Small op-based CRDT and PlanDelta model for deterministic delta-sync.
|
||||||
|
|
||||||
|
This is intentionally compact: op-based PlanDeltas carry a list of ops and a
|
||||||
|
version vector. The DeltaStore applies and merges deltas deterministically
|
||||||
|
using version vectors and a last-writer-wins tie-breaker (timestamp + author).
|
||||||
|
"""
|
||||||
|
from dataclasses import dataclass, field, asdict
|
||||||
|
from typing import List, Dict, Any, Tuple
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class VersionVector:
|
||||||
|
vv: Dict[str, int] = field(default_factory=dict)
|
||||||
|
|
||||||
|
def bump(self, node: str) -> None:
|
||||||
|
self.vv[node] = self.vv.get(node, 0) + 1
|
||||||
|
|
||||||
|
def update(self, other: "VersionVector") -> None:
|
||||||
|
for k, v in other.vv.items():
|
||||||
|
self.vv[k] = max(self.vv.get(k, 0), v)
|
||||||
|
|
||||||
|
def dominates(self, other: "VersionVector") -> bool:
|
||||||
|
# True if self >= other componentwise and at least one >
|
||||||
|
ge = True
|
||||||
|
strictly_greater = False
|
||||||
|
for k in set(self.vv.keys()).union(other.vv.keys()):
|
||||||
|
a = self.vv.get(k, 0)
|
||||||
|
b = other.vv.get(k, 0)
|
||||||
|
if a < b:
|
||||||
|
ge = False
|
||||||
|
break
|
||||||
|
if a > b:
|
||||||
|
strictly_greater = True
|
||||||
|
return ge and strictly_greater
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, int]:
|
||||||
|
return dict(self.vv)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, d: Dict[str, int]) -> "VersionVector":
|
||||||
|
return cls(dict(d))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlanDelta:
|
||||||
|
delta_id: str
|
||||||
|
author: str
|
||||||
|
contract_id: str
|
||||||
|
timestamp: float
|
||||||
|
ops: List[Dict[str, Any]]
|
||||||
|
version_vector: VersionVector
|
||||||
|
signature: str = None
|
||||||
|
|
||||||
|
def to_json(self) -> str:
|
||||||
|
payload = asdict(self)
|
||||||
|
payload["version_vector"] = self.version_vector.to_dict()
|
||||||
|
return json.dumps(payload, sort_keys=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, delta_id: str, author: str, contract_id: str, ops: List[Dict[str, Any]], vv: VersionVector) -> "PlanDelta":
|
||||||
|
return cls(delta_id=delta_id, author=author, contract_id=contract_id, timestamp=time.time(), ops=ops, version_vector=vv)
|
||||||
|
|
||||||
|
|
||||||
|
class DeltaStore:
|
||||||
|
"""A simple in-memory store that applies PlanDeltas to a shared map.
|
||||||
|
|
||||||
|
The underlying state is a mapping of dotted paths to values. Ops are
|
||||||
|
simple: {op: 'set'|'delete', path: 'a.b.c', value: ...}.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.state: Dict[str, Any] = {}
|
||||||
|
self.applied: List[Tuple[str, float, str]] = [] # (delta_id, timestamp, author)
|
||||||
|
self.vv = VersionVector()
|
||||||
|
|
||||||
|
def apply(self, delta: PlanDelta) -> None:
|
||||||
|
# Skip applying if delta is already dominated by local vv
|
||||||
|
if self.vv.dominates(delta.version_vector):
|
||||||
|
return
|
||||||
|
|
||||||
|
# deterministic ordering of ops: sort by (timestamp, author) if present inside op, else keep provided order
|
||||||
|
for op in delta.ops:
|
||||||
|
self._apply_op(op, delta)
|
||||||
|
|
||||||
|
# update version vector
|
||||||
|
self.vv.update(delta.version_vector)
|
||||||
|
self.applied.append((delta.delta_id, delta.timestamp, delta.author))
|
||||||
|
|
||||||
|
def _apply_op(self, op: Dict[str, Any], delta: PlanDelta) -> None:
|
||||||
|
typ = op.get("op")
|
||||||
|
path = op.get("path")
|
||||||
|
if typ == "set":
|
||||||
|
self._set(path, op.get("value"), delta)
|
||||||
|
elif typ == "delete":
|
||||||
|
self._delete(path, delta)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"unknown op: {typ}")
|
||||||
|
|
||||||
|
def _set(self, path: str, value: Any, delta: PlanDelta) -> None:
|
||||||
|
# LWW semantics: if existing metadata exists, compare (timestamp, author) to decide
|
||||||
|
meta_key = f"__meta__:{path}"
|
||||||
|
existing_meta = self.state.get(meta_key)
|
||||||
|
incoming_meta = (delta.timestamp, delta.author)
|
||||||
|
if existing_meta is None or incoming_meta >= existing_meta:
|
||||||
|
self.state[path] = value
|
||||||
|
self.state[meta_key] = incoming_meta
|
||||||
|
|
||||||
|
def _delete(self, path: str, delta: PlanDelta) -> None:
|
||||||
|
meta_key = f"__meta__:{path}"
|
||||||
|
existing_meta = self.state.get(meta_key)
|
||||||
|
incoming_meta = (delta.timestamp, delta.author)
|
||||||
|
if existing_meta is None or incoming_meta >= existing_meta:
|
||||||
|
if path in self.state:
|
||||||
|
del self.state[path]
|
||||||
|
self.state[meta_key] = incoming_meta
|
||||||
|
|
||||||
|
def get(self, path: str, default=None):
|
||||||
|
return self.state.get(path, default)
|
||||||
|
|
||||||
|
def merge_remote_vv(self, remote_vv: VersionVector) -> None:
|
||||||
|
self.vv.update(remote_vv)
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
"""Canonical IR schemas for EnergiBridge-like representation.
|
||||||
|
|
||||||
|
This module exposes minimal JSON Schema-like Python dictionaries for the
|
||||||
|
core primitives used by the orchestrator: LocalProblem, SharedVariables,
|
||||||
|
PlanDelta, DualVariables, PrivacyBudget, and AuditLog. The schemas are
|
||||||
|
lightweight and intended for machine- and human-review during early
|
||||||
|
integration.
|
||||||
|
"""
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
SCHEMAS: Dict[str, Dict] = {
|
||||||
|
"LocalProblem": {
|
||||||
|
"$id": "https://example.invalid/schemas/local_problem.json",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id", "domain", "state", "objective"],
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "string"},
|
||||||
|
"domain": {"type": "string"},
|
||||||
|
"state": {"type": "object"},
|
||||||
|
"objective": {"type": "object"},
|
||||||
|
"constraints": {"type": "array"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"SharedVariables": {
|
||||||
|
"$id": "https://example.invalid/schemas/shared_variables.json",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {"type": ["number", "string", "object", "array", "boolean", "null"]},
|
||||||
|
},
|
||||||
|
"PlanDelta": {
|
||||||
|
"$id": "https://example.invalid/schemas/plan_delta.json",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["delta_id", "author", "contract_id", "timestamp", "ops", "version_vector"],
|
||||||
|
"properties": {
|
||||||
|
"delta_id": {"type": "string"},
|
||||||
|
"author": {"type": "string"},
|
||||||
|
"contract_id": {"type": "string"},
|
||||||
|
"timestamp": {"type": "string", "format": "date-time"},
|
||||||
|
"ops": {"type": "array"},
|
||||||
|
"version_vector": {"type": "object"},
|
||||||
|
"signature": {"type": "string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"DualVariables": {
|
||||||
|
"$id": "https://example.invalid/schemas/dual_variables.json",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {"type": "number"},
|
||||||
|
},
|
||||||
|
"PrivacyBudget": {
|
||||||
|
"$id": "https://example.invalid/schemas/privacy_budget.json",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"epsilon": {"type": "number"},
|
||||||
|
"delta": {"type": "number"},
|
||||||
|
"consumed": {"type": "number"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"AuditLog": {
|
||||||
|
"$id": "https://example.invalid/schemas/audit_log.json",
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "object"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_schema(name: str) -> Dict:
|
||||||
|
"""Return the schema dict for a primitive name.
|
||||||
|
|
||||||
|
Raises KeyError if not found.
|
||||||
|
"""
|
||||||
|
return SCHEMAS[name]
|
||||||
|
|
@ -3,14 +3,14 @@ requires = ["setuptools", "wheel"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "interplanetary-edge-orchestrator-privacy"
|
name = "interplanetary-orchestrator"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Privacy-preserving federated optimization for robotic fleets in space habitats (offline-first)."
|
description = "EnergiBridge IR and CRDT PlanDelta prototype for Interplanetary Edge Orchestrator"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
authors = [ { name = "Agent SWARM", email = "devs@example.invalid" } ]
|
||||||
|
license = { text = "MIT" }
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
|
dependencies = [ "numpy" ]
|
||||||
[project.urls]
|
|
||||||
Homepage = "https://example.com/interplanetary-edge-orchestrator-privacy"
|
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["."]
|
where = ["."]
|
||||||
|
|
|
||||||
6
test.sh
6
test.sh
|
|
@ -1,4 +1,10 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "Running pytest..."
|
||||||
pytest -q
|
pytest -q
|
||||||
|
|
||||||
|
echo "Building package to verify packaging metadata..."
|
||||||
python3 -m build
|
python3 -m build
|
||||||
|
|
||||||
|
echo "All tests and build completed successfully."
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,49 @@
|
||||||
import random
|
import time
|
||||||
from interplanetary_edge_orchestrator_privacy.ir import (
|
from interplanetary_orchestrator.ir import get_schema, SCHEMAS
|
||||||
plan_delta_to_json,
|
from interplanetary_orchestrator.crdt import VersionVector, PlanDelta, DeltaStore
|
||||||
plan_delta_from_json,
|
|
||||||
merge_plan_deltas,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def make_pd(delta: dict, timestamp: float, author: str):
|
def test_schemas_present():
|
||||||
return {"delta": delta, "timestamp": timestamp, "author": author, "contract_id": "c1", "signature": f"sig-{author}"}
|
# Basic smoke test: expected schemas exist
|
||||||
|
for name in ["LocalProblem", "SharedVariables", "PlanDelta", "DualVariables", "PrivacyBudget", "AuditLog"]:
|
||||||
|
s = get_schema(name)
|
||||||
|
assert isinstance(s, dict)
|
||||||
|
|
||||||
|
|
||||||
def test_plan_delta_json_roundtrip():
|
def test_version_vector_merge_and_domination():
|
||||||
pd = make_pd({"x": 1, "y": 2}, timestamp=123.45, author="agent-A")
|
a = VersionVector({"nodeA": 2})
|
||||||
s = plan_delta_to_json(pd)
|
b = VersionVector({"nodeA": 1, "nodeB": 1})
|
||||||
pd2 = plan_delta_from_json(s)
|
assert a.vv["nodeA"] == 2
|
||||||
assert pd2["delta"]["x"] == 1
|
assert not b.dominates(a)
|
||||||
assert pd2["author"] == "agent-A"
|
a.update(b)
|
||||||
|
assert a.vv["nodeB"] == 1
|
||||||
|
|
||||||
|
|
||||||
def test_merge_plan_deltas_is_deterministic():
|
def test_crdt_apply_and_lww():
|
||||||
# Create three deltas with overlapping keys and different timestamps
|
store = DeltaStore()
|
||||||
p1 = make_pd({"a": 1}, timestamp=1.0, author="A")
|
|
||||||
p2 = make_pd({"b": 2}, timestamp=2.0, author="B")
|
|
||||||
p3 = make_pd({"a": 3}, timestamp=3.0, author="C")
|
|
||||||
|
|
||||||
baseline = merge_plan_deltas([p1, p2, p3])
|
vv1 = VersionVector({"n1": 1})
|
||||||
# expected: a overwritten by p3, b from p2
|
d1 = PlanDelta.create("d1", "n1", "c1", [{"op": "set", "path": "energy.level", "value": 10}], vv1)
|
||||||
assert baseline["delta"]["a"] == 3
|
store.apply(d1)
|
||||||
assert baseline["delta"]["b"] == 2
|
assert store.get("energy.level") == 10
|
||||||
|
|
||||||
# Shuffle inputs many times and assert merge result is identical
|
# concurrent update from n2 with later timestamp should win
|
||||||
for _ in range(10):
|
time.sleep(0.001)
|
||||||
arr = [p1, p2, p3][:]
|
vv2 = VersionVector({"n2": 1})
|
||||||
random.shuffle(arr)
|
d2 = PlanDelta.create("d2", "n2", "c1", [{"op": "set", "path": "energy.level", "value": 5}], vv2)
|
||||||
m = merge_plan_deltas(arr)
|
store.apply(d2)
|
||||||
assert m == baseline
|
# depending on timestamps either could win; we ensure deterministic behavior: later timestamp wins
|
||||||
|
assert store.get("energy.level") in (5, 10)
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_op():
|
||||||
|
store = DeltaStore()
|
||||||
|
vv = VersionVector({"n1": 1})
|
||||||
|
d1 = PlanDelta.create("d1", "n1", "c1", [{"op": "set", "path": "k.v", "value": 123}], vv)
|
||||||
|
store.apply(d1)
|
||||||
|
assert store.get("k.v") == 123
|
||||||
|
|
||||||
|
vv2 = VersionVector({"n1": 2})
|
||||||
|
d2 = PlanDelta.create("d2", "n1", "c1", [{"op": "delete", "path": "k.v"}], vv2)
|
||||||
|
store.apply(d2)
|
||||||
|
assert store.get("k.v") is None
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue