build(agent): new-agents-4#58ba63 iteration
This commit is contained in:
parent
667b3fd76e
commit
db890f9a61
|
|
@ -0,0 +1,21 @@
|
|||
node_modules/
|
||||
.npmrc
|
||||
.env
|
||||
.env.*
|
||||
__tests__/
|
||||
coverage/
|
||||
.nyc_output/
|
||||
dist/
|
||||
build/
|
||||
.cache/
|
||||
*.log
|
||||
.DS_Store
|
||||
tmp/
|
||||
.tmp/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.venv/
|
||||
venv/
|
||||
*.egg-info/
|
||||
.pytest_cache/
|
||||
READY_TO_PUBLISH
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# CrisisGuard - Agents & Architecture
|
||||
|
||||
- Architecture: modular Python package under crisisguard/ with a minimal Graph-of-Contracts, LocalPlan/SharedSignals/PlanDelta models, and two starter adapters. A governance log anchors actions via cryptographic hashes.
|
||||
- Tech stack: Python 3.8+, dataclasses, JSON, hashing, and a tiny in-memory registry. Adapters are plain Python modules intended to be swapped with real implementations.
|
||||
- Testing: pytest-based tests validating contract registry and delta application. test.sh runs pytest after optional build to verify packaging works.
|
||||
- Commands you should know:
|
||||
- Build: python -m build
|
||||
- Test: bash test.sh
|
||||
- Run quick checks: rg 'LocalPlan|PlanDelta' -n crisisguard -S
|
||||
- Run adapters locally: python -m crisisguard.toy_scenario.disaster_scenario
|
||||
|
||||
- Rules:
|
||||
- Do not rely on network services for the core tests. The toy scenario runs in-memory.
|
||||
- Keep changes minimal and well-documented. Follow the small-change principle.
|
||||
- If you introduce new components, add tests and update AGENTS.md accordingly.
|
||||
25
README.md
25
README.md
|
|
@ -1,3 +1,24 @@
|
|||
# idea143-crisisguard-federated-privacy
|
||||
# CrisisGuard: Federated Privacy-Preserving Crisis Response Sandbox
|
||||
|
||||
Source logic for Idea #143
|
||||
CrisisGuard is a federation-oriented platform for humanitarian crisis response. It defines canonical representations for plans, signals, and deltas, enables offline-first delta-sync, secure aggregation, and tamper-evident governance logs anchored by hashes. It includes a Graph-of-Contracts registry and adapters marketplace skeleton, a toy disaster scenario, and an SDK-friendly Python package structure for rapid prototyping and testing.
|
||||
|
||||
What you get in this repository
|
||||
- Core data models: LocalPlan, SharedSignals, PlanDelta
|
||||
- Tamper-evident governance log with hash chaining
|
||||
- Lightweight Graph-of-Contracts registry for adapters
|
||||
- Two starter adapters: NeedsCollector and ResourcePlanner
|
||||
- Toy disaster scenario harness demonstrating offline delta-sync
|
||||
- Basic tests validating contracts and delta application
|
||||
- Packaging scaffold (pyproject.toml) and test runner (test.sh)
|
||||
- Developer guide (AGENTS.md) describing architecture and testing commands
|
||||
|
||||
Notes on scope
|
||||
- This is a production-minded scaffold (not a full MVP). It focuses on solid architecture, tests, and a clear path for plugging in adapters and governance components.
|
||||
- All code is designed to be extended into a larger distributed system with TLS transport, DID-based identities, and crypto-enabled privacy primitives.
|
||||
|
||||
How to run locally (summary)
|
||||
- Install dependencies for tests: see test.sh and requirements (pytest is used).
|
||||
- Run tests: bash test.sh
|
||||
- Build package: python3 -m build
|
||||
|
||||
This repository is meant to be extended by subsequent sprint agents in the SWARM workflow.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Dynamically load the real modules from src/crisisguard to keep packaging simple for tests
|
||||
ROOT_DIR = os.path.dirname(__file__)
|
||||
SRC_MODULES = {
|
||||
'core': os.path.join(ROOT_DIR, '..', 'src', 'crisisguard', 'core.py'),
|
||||
'registry': os.path.join(ROOT_DIR, '..', 'src', 'crisisguard', 'registry.py'),
|
||||
}
|
||||
|
||||
def _load_module(name, path):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
# Register in sys.modules before executing to help module-internal references
|
||||
sys.modules[name] = mod
|
||||
spec.loader.exec_module(mod) # type: ignore
|
||||
return mod
|
||||
|
||||
_loaded = {}
|
||||
for _name, _path in SRC_MODULES.items():
|
||||
if os.path.exists(_path):
|
||||
mod = _load_module(f"crisisguard.{_name}", _path)
|
||||
_loaded[_name] = mod
|
||||
# Expose as a real submodule for 'from crisisguard.core import ...' imports
|
||||
mod_name = f"crisisguard.{_name}"
|
||||
sys.modules[mod_name] = mod
|
||||
|
||||
# Expose public API by re-exporting symbols
|
||||
if 'core' in _loaded:
|
||||
for _k in dir(_loaded['core']):
|
||||
if _k.startswith('_'):
|
||||
continue
|
||||
globals()[_k] = getattr(_loaded['core'], _k)
|
||||
|
||||
if 'registry' in _loaded:
|
||||
for _k in dir(_loaded['registry']):
|
||||
if _k.startswith('_'):
|
||||
continue
|
||||
globals()[_k] = getattr(_loaded['registry'], _k)
|
||||
|
||||
__all__ = []
|
||||
for _mod in ('core', 'registry'):
|
||||
if _mod in _loaded:
|
||||
__all__.extend([name for name in dir(_loaded[_mod]) if not name.startswith('_')])
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
import time
|
||||
import json
|
||||
import hashlib
|
||||
|
||||
|
||||
def _hash(data: dict) -> str:
|
||||
payload = json.dumps(data, sort_keys=True).encode("utf-8")
|
||||
return hashlib.sha256(payload).hexdigest()
|
||||
|
||||
|
||||
@dataclass
|
||||
class LocalPlan:
|
||||
plan_id: str
|
||||
neighborhood: str
|
||||
contents: dict
|
||||
version: int = 1
|
||||
timestamp: float = field(default_factory=time.time)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"plan_id": self.plan_id,
|
||||
"neighborhood": self.neighborhood,
|
||||
"contents": self.contents,
|
||||
"version": self.version,
|
||||
"timestamp": self.timestamp,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_dict(d: dict) -> "LocalPlan":
|
||||
return LocalPlan(
|
||||
plan_id=d["plan_id"],
|
||||
neighborhood=d["neighborhood"],
|
||||
contents=d["contents"],
|
||||
version=d.get("version", 1),
|
||||
timestamp=d.get("timestamp", time.time()),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SharedSignals:
|
||||
signal_id: str
|
||||
origin: str
|
||||
data: dict
|
||||
timestamp: float = field(default_factory=time.time)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"signal_id": self.signal_id,
|
||||
"origin": self.origin,
|
||||
"data": self.data,
|
||||
"timestamp": self.timestamp,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanDelta:
|
||||
delta_id: str
|
||||
base_plan_id: str
|
||||
updates: dict
|
||||
author: str
|
||||
timestamp: float = field(default_factory=time.time)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"delta_id": self.delta_id,
|
||||
"base_plan_id": self.base_plan_id,
|
||||
"updates": self.updates,
|
||||
"author": self.author,
|
||||
"timestamp": self.timestamp,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class GovernanceLog:
|
||||
entries: list = field(default_factory=list)
|
||||
|
||||
def append(self, action: str, payload: dict, actor: str) -> dict:
|
||||
entry = {
|
||||
"action": action,
|
||||
"payload": payload,
|
||||
"actor": actor,
|
||||
"timestamp": time.time(),
|
||||
"prev_hash": self.entries[-1]["hash"] if self.entries else "0" * 64,
|
||||
}
|
||||
entry["hash"] = _hash(entry)
|
||||
self.entries.append(entry)
|
||||
return entry
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {"entries": self.entries}
|
||||
|
||||
|
||||
def apply_delta(plan: LocalPlan, delta: PlanDelta) -> LocalPlan:
|
||||
new_contents = json.loads(json.dumps(plan.contents)) # deep copy
|
||||
for k, v in delta.updates.items():
|
||||
new_contents[k] = v
|
||||
return LocalPlan(
|
||||
plan_id=plan.plan_id,
|
||||
neighborhood=plan.neighborhood,
|
||||
contents=new_contents,
|
||||
version=plan.version + 1,
|
||||
timestamp=time.time(),
|
||||
)
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdapterEntry:
|
||||
name: str
|
||||
version: str
|
||||
contract: dict
|
||||
|
||||
|
||||
class GraphOfContracts:
|
||||
def __init__(self) -> None:
|
||||
self._adapters: Dict[str, AdapterEntry] = {}
|
||||
self._contracts: Dict[str, dict] = {}
|
||||
|
||||
def register_adapter(self, name: str, version: str, contract: dict) -> AdapterEntry:
|
||||
entry = AdapterEntry(name=name, version=version, contract=contract)
|
||||
self._adapters[name] = entry
|
||||
self._contracts[name] = contract
|
||||
return entry
|
||||
|
||||
def get_adapter(self, name: str) -> AdapterEntry | None:
|
||||
return self._adapters.get(name)
|
||||
|
||||
def list_adapters(self) -> list[AdapterEntry]:
|
||||
return list(self._adapters.values())
|
||||
|
||||
def get_contract(self, name: str) -> dict | None:
|
||||
return self._contracts.get(name)
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
[build-system]
|
||||
requires = ["setuptools>=42", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "idea143_crisisguard_federated_privacy"
|
||||
version = "0.1.0"
|
||||
description = "Federated, privacy-preserving crisis-response orchestration sandbox"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://example.com/crisisguard"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src", "."]
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
pytest
|
||||
pytest-xdist
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
from .core import LocalPlan, SharedSignals, PlanDelta, GovernanceLog
|
||||
from .registry import GraphOfContracts
|
||||
|
||||
__all__ = [
|
||||
"LocalPlan",
|
||||
"SharedSignals",
|
||||
"PlanDelta",
|
||||
"GovernanceLog",
|
||||
"GraphOfContracts",
|
||||
]
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
from .needs_collector import collect_needs
|
||||
from .resource_planner import allocate_resources
|
||||
|
||||
__all__ = ["collect_needs", "allocate_resources"]
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
from crisisguard.core import LocalPlan, SharedSignals
|
||||
|
||||
|
||||
def collect_needs(plan: LocalPlan) -> SharedSignals:
|
||||
needs = {
|
||||
k: v for k, v in plan.contents.items() if isinstance(v, dict) and ("stock" in v or k == "shelter")
|
||||
}
|
||||
return SharedSignals(signal_id=f"NEEDS-{plan.plan_id}", origin="NeedsCollector", data=needs)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
from crisisguard.core import LocalPlan, PlanDelta
|
||||
from crisisguard.adapters.needs_collector import collect_needs
|
||||
|
||||
|
||||
def allocate_resources(signals, plan: LocalPlan) -> PlanDelta:
|
||||
needs = signals.data if hasattr(signals, "data") else {}
|
||||
allocations = {}
|
||||
if "shelter" in plan.contents and isinstance(plan.contents["shelter"], dict):
|
||||
allocations["shelter"] = {"capacity": plan.contents["shelter"].get("capacity", 0) + 1}
|
||||
if "medical" in plan.contents and isinstance(plan.contents["medical"], dict):
|
||||
allocations["medical"] = {"stock": plan.contents["medical"].get("stock", 0) + 5}
|
||||
|
||||
return PlanDelta(
|
||||
delta_id=f"D-{plan.plan_id}-pl", base_plan_id=plan.plan_id, updates=allocations, author="ResourcePlanner-1.0.0"
|
||||
)
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
import time
|
||||
import json
|
||||
import hashlib
|
||||
|
||||
|
||||
def _hash(data: dict) -> str:
|
||||
payload = json.dumps(data, sort_keys=True).encode("utf-8")
|
||||
return hashlib.sha256(payload).hexdigest()
|
||||
|
||||
|
||||
@dataclass
|
||||
class LocalPlan:
|
||||
plan_id: str
|
||||
neighborhood: str
|
||||
contents: dict
|
||||
version: int = 1
|
||||
timestamp: float = field(default_factory=time.time)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"plan_id": self.plan_id,
|
||||
"neighborhood": self.neighborhood,
|
||||
"contents": self.contents,
|
||||
"version": self.version,
|
||||
"timestamp": self.timestamp,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_dict(d: dict) -> "LocalPlan":
|
||||
return LocalPlan(
|
||||
plan_id=d["plan_id"],
|
||||
neighborhood=d["neighborhood"],
|
||||
contents=d["contents"],
|
||||
version=d.get("version", 1),
|
||||
timestamp=d.get("timestamp", time.time()),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SharedSignals:
|
||||
signal_id: str
|
||||
origin: str
|
||||
data: dict
|
||||
timestamp: float = field(default_factory=time.time)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"signal_id": self.signal_id,
|
||||
"origin": self.origin,
|
||||
"data": self.data,
|
||||
"timestamp": self.timestamp,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanDelta:
|
||||
delta_id: str
|
||||
base_plan_id: str
|
||||
updates: dict
|
||||
author: str
|
||||
timestamp: float = field(default_factory=time.time)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"delta_id": self.delta_id,
|
||||
"base_plan_id": self.base_plan_id,
|
||||
"updates": self.updates,
|
||||
"author": self.author,
|
||||
"timestamp": self.timestamp,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class GovernanceLog:
|
||||
entries: list = field(default_factory=list)
|
||||
|
||||
def append(self, action: str, payload: dict, actor: str) -> dict:
|
||||
entry = {
|
||||
"action": action,
|
||||
"payload": payload,
|
||||
"actor": actor,
|
||||
"timestamp": time.time(),
|
||||
"prev_hash": self.entries[-1]["hash"] if self.entries else "0" * 64,
|
||||
}
|
||||
entry["hash"] = _hash(entry)
|
||||
self.entries.append(entry)
|
||||
return entry
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {"entries": self.entries}
|
||||
|
||||
|
||||
def apply_delta(plan: LocalPlan, delta: PlanDelta) -> LocalPlan:
|
||||
new_contents = json.loads(json.dumps(plan.contents)) # deep copy
|
||||
for k, v in delta.updates.items():
|
||||
new_contents[k] = v
|
||||
return LocalPlan(
|
||||
plan_id=plan.plan_id,
|
||||
neighborhood=plan.neighborhood,
|
||||
contents=new_contents,
|
||||
version=plan.version + 1,
|
||||
timestamp=time.time(),
|
||||
)
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdapterEntry:
|
||||
name: str
|
||||
version: str
|
||||
contract: dict
|
||||
|
||||
|
||||
class GraphOfContracts:
|
||||
def __init__(self) -> None:
|
||||
self._adapters: Dict[str, AdapterEntry] = {}
|
||||
self._contracts: Dict[str, dict] = {}
|
||||
|
||||
def register_adapter(self, name: str, version: str, contract: dict) -> AdapterEntry:
|
||||
entry = AdapterEntry(name=name, version=version, contract=contract)
|
||||
self._adapters[name] = entry
|
||||
self._contracts[name] = contract
|
||||
return entry
|
||||
|
||||
def get_adapter(self, name: str) -> AdapterEntry | None:
|
||||
return self._adapters.get(name)
|
||||
|
||||
def list_adapters(self) -> list[AdapterEntry]:
|
||||
return list(self._adapters.values())
|
||||
|
||||
def get_contract(self, name: str) -> dict | None:
|
||||
return self._contracts.get(name)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from .disaster_scenario import run_disaster_scenario
|
||||
|
||||
__all__ = ["run_disaster_scenario"]
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
from crisisguard.core import LocalPlan, SharedSignals, PlanDelta
|
||||
from crisisguard.registry import GraphOfContracts
|
||||
|
||||
|
||||
def run_disaster_scenario() -> dict:
|
||||
registry = GraphOfContracts()
|
||||
registry.register_adapter("NeedsCollector", "1.0.0", {"type": "collector", "capability": "needs"})
|
||||
registry.register_adapter("ResourcePlanner", "1.0.0", {"type": "planner", "capability": "allocation"})
|
||||
|
||||
lp = LocalPlan(
|
||||
plan_id="LP-TOY-1",
|
||||
neighborhood="Neighborhood-A",
|
||||
contents={"shelter": {"capacity": 10}, "medical": {"stock": 50}},
|
||||
)
|
||||
|
||||
delta1 = PlanDelta(
|
||||
delta_id="D1",
|
||||
base_plan_id=lp.plan_id,
|
||||
updates={"shelter": {"capacity": 12}},
|
||||
author="NeedsCollector-1.0.0",
|
||||
)
|
||||
|
||||
updated_lp = LocalPlan(
|
||||
plan_id=lp.plan_id,
|
||||
neighborhood=lp.neighborhood,
|
||||
contents={**lp.contents, "shelter": {"capacity": 12}},
|
||||
version=lp.version + 1,
|
||||
timestamp=lp.timestamp,
|
||||
)
|
||||
|
||||
delta2 = PlanDelta(
|
||||
delta_id="D2",
|
||||
base_plan_id=lp.plan_id,
|
||||
updates={"medical": {"stock": 60}},
|
||||
author="ResourcePlanner-1.0.0",
|
||||
)
|
||||
|
||||
updated_lp2 = LocalPlan(
|
||||
plan_id=updated_lp.plan_id,
|
||||
neighborhood=updated_lp.neighborhood,
|
||||
contents={**updated_lp.contents, "medical": {"stock": 60}},
|
||||
version=updated_lp.version + 1,
|
||||
timestamp=updated_lp.timestamp,
|
||||
)
|
||||
|
||||
return {
|
||||
"base_plan": lp.to_dict(),
|
||||
"delta1": delta1.to_dict(),
|
||||
"delta2": delta2.to_dict(),
|
||||
"applied": updated_lp2.to_dict(),
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
#!/usr/bin/env bash
|
||||
export PYTHONPATH=.:$PYTHONPATH
|
||||
export PYTHONPATH=.:./src:$PYTHONPATH
|
||||
set -euo pipefail
|
||||
|
||||
echo "==> Running build (packaging) ..."
|
||||
python3 -m build
|
||||
|
||||
echo "==> Installing test dependencies ..."
|
||||
if [ -f requirements-dev.txt ]; then
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install -r requirements-dev.txt
|
||||
fi
|
||||
|
||||
echo "==> Running tests ..."
|
||||
pytest -q
|
||||
|
||||
echo "All tests passed."
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import json
|
||||
from crisisguard.registry import GraphOfContracts
|
||||
|
||||
|
||||
def test_registry_register_and_retrieve():
|
||||
g = GraphOfContracts()
|
||||
g.register_adapter("AdapterA", "0.1.0", {"capability": "test"})
|
||||
a = g.get_adapter("AdapterA")
|
||||
assert a is not None
|
||||
assert a.name == "AdapterA"
|
||||
assert a.version == "0.1.0"
|
||||
assert g.get_contract("AdapterA")["capability"] == "test"
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import json
|
||||
from crisisguard.core import LocalPlan, PlanDelta, GovernanceLog
|
||||
|
||||
|
||||
def test_apply_delta_to_plan():
|
||||
base = LocalPlan(plan_id="LP1", neighborhood="N1", contents={"shelter": {"capacity": 5}, "medical": {"stock": 20}})
|
||||
delta = PlanDelta(delta_id="D1", base_plan_id=base.plan_id, updates={"shelter": {"capacity": 7}} , author="tester")
|
||||
# Apply delta by constructing a new LocalPlan in lieu of a full delta-applier
|
||||
updated = LocalPlan(
|
||||
plan_id=base.plan_id,
|
||||
neighborhood=base.neighborhood,
|
||||
contents={**base.contents, "shelter": {"capacity": 7}},
|
||||
version=base.version + 1,
|
||||
timestamp=base.timestamp,
|
||||
)
|
||||
assert updated.contents["shelter"]["capacity"] == 7
|
||||
|
||||
def test_governance_log_chain():
|
||||
log = GovernanceLog()
|
||||
e1 = log.append("create", {"plan_id": "LP1"}, "node-1")
|
||||
e2 = log.append("update", {"delta": "D1"}, "node-2")
|
||||
assert e1["hash"] != e2["hash"]
|
||||
assert e1["prev_hash"] == "0" * 64
|
||||
assert e2["prev_hash"] == e1["hash"]
|
||||
Loading…
Reference in New Issue