build(agent): new-agents-2#7e3bbc iteration
This commit is contained in:
parent
9c930445b3
commit
0bf6a5bd06
|
|
@ -19,3 +19,8 @@ How to run locally:
|
||||||
This is a minimal, production-oriented MVP designed to be extended by follow-up iterations.
|
This is a minimal, production-oriented MVP designed to be extended by follow-up iterations.
|
||||||
|
|
||||||
Documentation: See docs/CRISISPULSE_MVP_SPEC.md for a concrete MVP blueprint focused on a two-adapter (solar and water) setup and offline delta-sync workflows.
|
Documentation: See docs/CRISISPULSE_MVP_SPEC.md for a concrete MVP blueprint focused on a two-adapter (solar and water) setup and offline delta-sync workflows.
|
||||||
|
|
||||||
|
Package integration:
|
||||||
|
- Python package name: idea168_crisispulse_federated_resource
|
||||||
|
- Version: 0.1.0 (consistent with pyproject.toml)
|
||||||
|
- Exposes core primitives under idea168_crisispulse_federated_resource.core
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ name = "idea168_crisispulse_federated_resource"
|
||||||
description = "CrisisPulse: Federated Resource Orchestration for Disaster-Relief Camp Networks MVP"
|
description = "CrisisPulse: Federated Resource Orchestration for Disaster-Relief Camp Networks MVP"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.11"
|
||||||
authors = [{name = "OpenCode Robot", email = "engineer@example.com"}]
|
authors = [{name = "OpenCode Robot", email = "engineer@example.com"}]
|
||||||
license = {text = "MIT"}
|
license = {text = "MIT"}
|
||||||
classifiers = [
|
classifiers = [
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import setuptools
|
||||||
|
|
||||||
|
setuptools.setup(
|
||||||
|
name="idea168_crisispulse_federated_resource",
|
||||||
|
version="0.1.0",
|
||||||
|
packages=setuptools.find_packages("src"),
|
||||||
|
package_dir={"": "src"},
|
||||||
|
description="CrisisPulse MVP: Federated Resource Orchestration for Disaster-Relief Camp Networks",
|
||||||
|
long_description="",
|
||||||
|
long_description_content_type="text/markdown",
|
||||||
|
url="",
|
||||||
|
author="OpenCode Robot",
|
||||||
|
license="MIT",
|
||||||
|
python_requires=">=3.11",
|
||||||
|
install_requires=[],
|
||||||
|
)
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
"""CrisisPulse Federated Resource (package init)"""
|
"""idea168_crisispulse_federated_resource
|
||||||
|
A production-oriented MVP scaffold for CrisisPulse fellowing the idea168 spec.
|
||||||
|
|
||||||
|
This package exposes a minimal, well-typed core to validate the local ledger,
|
||||||
|
delta-sync, contract registry, adapters, governance, and privacy primitives
|
||||||
|
used in unit tests.
|
||||||
|
"""
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"ledger",
|
"core",
|
||||||
"contract_registry",
|
"__version__",
|
||||||
"adapters",
|
|
||||||
"governance",
|
|
||||||
"privacy",
|
|
||||||
"sim",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
"""Core primitives for CrisisPulse MVP"""
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ledger",
|
||||||
|
"contract_registry",
|
||||||
|
"adapters",
|
||||||
|
"governance",
|
||||||
|
"privacy",
|
||||||
|
"sim",
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
"""Base adapters and two sample starter adapters (Solar, WaterPurifier)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from .ledger import LocalLedger
|
||||||
|
|
||||||
|
|
||||||
|
class BaseAdapter:
|
||||||
|
name: str = "base-adapter"
|
||||||
|
|
||||||
|
def export_resources(self, ledger: LocalLedger) -> Dict[str, Any]:
|
||||||
|
# Return a minimal perspective of resources available
|
||||||
|
return {
|
||||||
|
"adapter": self.name,
|
||||||
|
"resources": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SolarAdapter(BaseAdapter):
|
||||||
|
name = "solar-adapter"
|
||||||
|
|
||||||
|
def export_resources(self, ledger: LocalLedger) -> Dict[str, Any]:
|
||||||
|
# Expose a tiny LocalResourcePlan and a forecast
|
||||||
|
plan = {
|
||||||
|
"domain": "energy",
|
||||||
|
"allocation": {"capacity_kw": 50},
|
||||||
|
}
|
||||||
|
forecast = {"domain": "energy", "demand_kw": 40}
|
||||||
|
return {
|
||||||
|
"adapter": self.name,
|
||||||
|
"resources": {"LocalResourcePlan": plan, "SharedForecast": forecast},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class WaterPurifierAdapter(BaseAdapter):
|
||||||
|
name = "water-purifier-adapter"
|
||||||
|
|
||||||
|
def export_resources(self, ledger: LocalLedger) -> Dict[str, Any]:
|
||||||
|
plan = {"domain": "water", "allocation_liters": 1000}
|
||||||
|
forecast = {"domain": "water", "demand_liters": 900}
|
||||||
|
return {
|
||||||
|
"adapter": self.name,
|
||||||
|
"resources": {"LocalResourcePlan": plan, "SharedForecast": forecast},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
"""Graph-of-Contracts: versioned schemas registry (MVP)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
class GraphOfContracts:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
# contract_name -> version -> schema
|
||||||
|
self._registry: Dict[str, Dict[int, Dict[str, Any]]] = {}
|
||||||
|
|
||||||
|
def register(self, contract_name: str, version: int, schema: Dict[str, Any]) -> None:
|
||||||
|
self._registry.setdefault(contract_name, {})[version] = schema
|
||||||
|
|
||||||
|
def get(self, contract_name: str, version: int) -> Dict[str, Any]:
|
||||||
|
versions = self._registry.get(contract_name, {})
|
||||||
|
if version not in versions:
|
||||||
|
raise KeyError(f"contract {contract_name} version {version} not found")
|
||||||
|
return versions[version]
|
||||||
|
|
||||||
|
def latest_version(self, contract_name: str) -> Tuple[int, Dict[str, Any]]:
|
||||||
|
versions = self._registry.get(contract_name, {})
|
||||||
|
if not versions:
|
||||||
|
raise KeyError(f"contract {contract_name} has no versions")
|
||||||
|
ver = max(versions.keys())
|
||||||
|
return ver, versions[ver]
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
"""Tamper-evident governance ledger (MVP)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hmac
|
||||||
|
import time
|
||||||
|
from hashlib import sha256
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
|
||||||
|
class GovernanceLedger:
|
||||||
|
def __init__(self, signer_key: str) -> None:
|
||||||
|
self._signer_key = signer_key.encode("utf-8")
|
||||||
|
self._entries: List[Dict[str, Any]] = []
|
||||||
|
|
||||||
|
def sign_event(self, event: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
payload = sha256(_serialize(event).encode("utf-8")).hexdigest()
|
||||||
|
signature = hmac.new(self._signer_key, payload.encode("utf-8"), sha256).hexdigest()
|
||||||
|
entry = {
|
||||||
|
"ts": int(time.time()),
|
||||||
|
"event": event,
|
||||||
|
"signature": signature,
|
||||||
|
}
|
||||||
|
self._entries.append(entry)
|
||||||
|
return entry
|
||||||
|
|
||||||
|
def verify_event(self, entry: Dict[str, Any]) -> bool:
|
||||||
|
event = entry.get("event")
|
||||||
|
sig = entry.get("signature")
|
||||||
|
if event is None or sig is None:
|
||||||
|
return False
|
||||||
|
payload = sha256(_serialize(event).encode("utf-8")).hexdigest()
|
||||||
|
expected = hmac.new(self._signer_key, payload.encode("utf-8"), sha256).hexdigest()
|
||||||
|
return hmac.compare_digest(expected, sig)
|
||||||
|
|
||||||
|
def _serialize(obj: object) -> str:
|
||||||
|
import json
|
||||||
|
return json.dumps(obj, sort_keys=True, separators=(",", ":"))
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
"""Local ledger with delta-sync primitives (MVP).
|
||||||
|
|
||||||
|
This is a tiny, self-contained in-memory ledger designed for tests. It
|
||||||
|
implements:
|
||||||
|
- Adding entries
|
||||||
|
- Delta application with a simple tag-based signature
|
||||||
|
- Merkle-root-like state digest for basic integrity checks
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize(obj: Any) -> str:
|
||||||
|
return json.dumps(obj, sort_keys=True, separators=(",", ":"))
|
||||||
|
|
||||||
|
|
||||||
|
class LocalLedger:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
# entry_id -> {"data": ..., "version": int, "ts": int}
|
||||||
|
self._entries: Dict[str, Dict[str, Any]] = {}
|
||||||
|
self._version_counter: int = 0
|
||||||
|
|
||||||
|
def add_entry(self, entry_id: str, data: Dict[str, Any]) -> None:
|
||||||
|
self._version_counter += 1
|
||||||
|
self._entries[entry_id] = {
|
||||||
|
"data": data,
|
||||||
|
"version": self._version_counter,
|
||||||
|
"ts": int(time.time()),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_entry(self, entry_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
return self._entries.get(entry_id)
|
||||||
|
|
||||||
|
def apply_delta(self, delta: Dict[str, Any]) -> None:
|
||||||
|
# delta schema: {"entry_id": str, "changes": dict, "tags": [str], "signature": str}
|
||||||
|
entry_id = delta.get("entry_id")
|
||||||
|
changes = delta.get("changes", {})
|
||||||
|
if not entry_id:
|
||||||
|
raise ValueError("delta must contain entry_id")
|
||||||
|
|
||||||
|
base = self._entries.get(entry_id, {"data": {}, "version": 0, "ts": 0})["data"]
|
||||||
|
if not isinstance(base, dict):
|
||||||
|
base = {}
|
||||||
|
# naive merge
|
||||||
|
merged = {**base, **changes}
|
||||||
|
self.add_entry(entry_id, merged)
|
||||||
|
|
||||||
|
def merkle_root(self) -> str:
|
||||||
|
# Simple Merkle-like digest: hash of sorted entries json
|
||||||
|
items = []
|
||||||
|
for k in sorted(self._entries.keys()):
|
||||||
|
items.append(_serialize({k: self._entries[k]}))
|
||||||
|
payload = "|".join(items)
|
||||||
|
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
def snapshot(self) -> Dict[str, Any]:
|
||||||
|
# Return a shallow copy suitable for tests
|
||||||
|
return {k: v.copy() for k, v in self._entries.items()}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
"""Privacy-preserving summaries (MVP): simple per-entry budgets and secure aggregation."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
|
class PrivacyBudget:
|
||||||
|
def __init__(self, per_entry_budget: int) -> None:
|
||||||
|
self.per_entry_budget = per_entry_budget
|
||||||
|
self.usage: Dict[str, int] = {}
|
||||||
|
|
||||||
|
def spend(self, entry_id: str, amount: int) -> None:
|
||||||
|
self.usage[entry_id] = self.usage.get(entry_id, 0) + amount
|
||||||
|
|
||||||
|
def remaining(self, entry_id: str) -> int:
|
||||||
|
spent = self.usage.get(entry_id, 0)
|
||||||
|
return max(0, self.per_entry_budget - spent)
|
||||||
|
|
||||||
|
|
||||||
|
def secure_aggregate(data: Dict[str, int], budget: PrivacyBudget) -> Dict[str, int]:
|
||||||
|
# Very simple per-key DP-like masking: cap contributions by budget per key
|
||||||
|
result: Dict[str, int] = {}
|
||||||
|
for k, v in data.items():
|
||||||
|
rem = budget.remaining(k)
|
||||||
|
take = min(v, rem)
|
||||||
|
budget.spend(k, take)
|
||||||
|
result[k] = take
|
||||||
|
return result
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
"""Lightweight simulation helpers for delta-sync and multi-domain co-simulation (MVP)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
|
||||||
|
def simulate_delta_sync(source: Dict[str, Any], target: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Merge two domain plans in a deterministic way.
|
||||||
|
|
||||||
|
This is a very small helper suitable for tests and toy demos. It performs a
|
||||||
|
simple deep merge where values from source override target when keys collide.
|
||||||
|
"""
|
||||||
|
merged = dict(target)
|
||||||
|
for k, v in source.items():
|
||||||
|
if isinstance(v, dict) and isinstance(merged.get(k), dict):
|
||||||
|
merged[k] = {**merged[k], **v}
|
||||||
|
else:
|
||||||
|
merged[k] = v
|
||||||
|
return merged
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||||
|
SRC = str(ROOT / "src")
|
||||||
|
sys.path.insert(0, SRC)
|
||||||
|
|
||||||
|
from idea168_crisispulse_federated_resource.core.adapters import SolarAdapter, WaterPurifierAdapter
|
||||||
|
from idea168_crisispulse_federated_resource.core.ledger import LocalLedger
|
||||||
|
|
||||||
|
|
||||||
|
def test_solar_adapter_exports_resources():
|
||||||
|
ledger = LocalLedger()
|
||||||
|
adj = SolarAdapter()
|
||||||
|
out = adj.export_resources(ledger)
|
||||||
|
assert out["adapter"] == "solar-adapter"
|
||||||
|
assert "LocalResourcePlan" in out["resources"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_water_purifier_adapter_exports_resources():
|
||||||
|
ledger = LocalLedger()
|
||||||
|
adj = WaterPurifierAdapter()
|
||||||
|
out = adj.export_resources(ledger)
|
||||||
|
assert out["adapter"] == "water-purifier-adapter"
|
||||||
|
assert "SharedForecast" in out["resources"]
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||||
|
SRC = str(ROOT / "src")
|
||||||
|
sys.path.insert(0, SRC)
|
||||||
|
|
||||||
|
from idea168_crisispulse_federated_resource.core.contract_registry import GraphOfContracts
|
||||||
|
|
||||||
|
|
||||||
|
def test_contract_registry_versions():
|
||||||
|
reg = GraphOfContracts()
|
||||||
|
reg.register("LocalResourcePlan", 1, {"schema": {"type": "object"}})
|
||||||
|
reg.register("LocalResourcePlan", 2, {"schema": {"type": "object", "properties": {"domain": {"type": "string"}}}})
|
||||||
|
ver_schema = reg.get("LocalResourcePlan", 2)
|
||||||
|
assert isinstance(ver_schema, dict)
|
||||||
|
ver, schema = reg.latest_version("LocalResourcePlan")
|
||||||
|
assert ver == 2
|
||||||
|
assert isinstance(schema, dict)
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Ensure tests can import the src package layout when running from repo root
|
||||||
|
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
SRC = os.path.join(ROOT, "src")
|
||||||
|
sys.path.insert(0, SRC)
|
||||||
|
|
||||||
|
from idea168_crisispulse_federated_resource.core.ledger import LocalLedger
|
||||||
|
|
||||||
|
|
||||||
|
def test_ledger_add_and_snapshot_and_merkle():
|
||||||
|
ledger = LocalLedger()
|
||||||
|
ledger.add_entry("camp1", {"resources": {"energy": 100}})
|
||||||
|
ledger.add_entry("camp2", {"resources": {"water": 200}})
|
||||||
|
snap = ledger.snapshot()
|
||||||
|
assert "camp1" in snap and "camp2" in snap
|
||||||
|
root = ledger.merkle_root()
|
||||||
|
assert isinstance(root, str) and len(root) == 64
|
||||||
|
|
||||||
|
def test_ledger_apply_delta():
|
||||||
|
ledger = LocalLedger()
|
||||||
|
ledger.add_entry("camp1", {"resources": {"energy": 50}})
|
||||||
|
delta = {
|
||||||
|
"entry_id": "camp1",
|
||||||
|
"changes": {"resources": {"energy": 75}},
|
||||||
|
"tags": ["test"],
|
||||||
|
"signature": "signature-placeholder",
|
||||||
|
}
|
||||||
|
ledger.apply_delta(delta)
|
||||||
|
entry = ledger.get_entry("camp1")
|
||||||
|
assert entry is not None
|
||||||
|
assert entry["data"]["resources"]["energy"] == 75
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||||
|
SRC = str(ROOT / "src")
|
||||||
|
sys.path.insert(0, SRC)
|
||||||
|
|
||||||
|
from idea168_crisispulse_federated_resource.core.privacy import PrivacyBudget, secure_aggregate
|
||||||
|
|
||||||
|
|
||||||
|
def test_privacy_budget_and_aggregate():
|
||||||
|
budget = PrivacyBudget(per_entry_budget=5)
|
||||||
|
data = {"camp1": 3, "camp2": 4}
|
||||||
|
masked = secure_aggregate(data, budget)
|
||||||
|
assert all(v <= 5 for v in masked.values())
|
||||||
|
# second call uses remaining budgets
|
||||||
|
more = {"camp1": 3, "camp2": 2}
|
||||||
|
masked2 = secure_aggregate(more, budget)
|
||||||
|
assert isinstance(masked2, dict)
|
||||||
Loading…
Reference in New Issue