diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bd5590b --- /dev/null +++ b/.gitignore @@ -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 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ff2fcbf --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,29 @@ +# NebulaForge Architecture and Contribution Guide + +Overview +- NebulaForge is an offline-resilient, federated foundation-model platform designed for space robotics with intermittent connectivity. This repository contains a production-ready MVP scaffold including a device runtime, secure aggregation, data contracts, governance ledger, and a simulator stub. + +Architecture Overview +- Runtime (nebulaforge.runtime): lightweight on-device inference and planning primitives tailored for ARM/RISC-V platforms. +- Federated (nebulaforge.federated): secure aggregation with optional differential privacy budgets. +- Contracts (nebulaforge.contracts): per-message data contracts such as LocalProblem, SharedVariables, PlanDelta, PrivacyBudget, AuditLog. +- Governance (nebulaforge.governance): crypto-signed decisions and tamper-evident provenance ledger. +- Simulator (nebulaforge.simulator): stubbed integration points for Gazebo/ROS-based HIL validation. + +Getting Started +- Install Python dependencies via test.sh which runs packaging checks and tests. +- Run unit tests with pytest. + +Contribution Rules +- Follow PEP 8 where applicable; keep modules small and focused. +- Add tests for new functionality; ensure all tests pass before proposing changes. +- Update AGENTS.md if you introduce new major components or interfaces. + +Testing and Validation +- The test.sh script in the repo automates: building the package (python3 -m build) and running tests (pytest). +- Use pytest to validate behavior of runtime, federated aggregation, and contracts. + +Release Process (high level) +- Bump version in nebulaforge/__init__.py or pyproject.toml when features are merged. +- Run test.sh to ensure green tests and packaging integrity. +- Create READY_TO_PUBLISH file to signal completion for publishing workflow. diff --git a/README.md b/README.md index 868ee2b..e1a5e6b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,18 @@ -# nebulaforge-offline-resilient-federated- +# NebulaForge: Offline-Resilient Federated Foundation Models for Space Robotics -A novel platform to deploy, train, and govern lightweight foundation models on space robotics fleets (rovers, drones, habitat modules) that operate with intermittent communications. It enables on-device inference, privacy-preserving federated fine-tu \ No newline at end of file +NebulaForge is an offline-resilient, federated foundation-model platform designed for space robotics with intermittent connectivity. It enables on-device inference, privacy-preserving federated fine-tuning, and contract-driven safety guarantees across fleets of rovers, drones, and habitat modules. + +Key Components +- Runtime: compact on-device runtime optimized for ARM/RISC-V for real-time planning and perception tasks. +- Federated: secure aggregation with optional differential privacy budgets. +- Contracts: CatOpt-inspired data-contract layer with versioned schemas (LocalProblem, SharedVariables, PlanDelta, PrivacyBudget, AuditLog). +- Governance: crypto-signed decisions and tamper-evident provenance ledger. +- Simulator: Gazebo/ROS-ready stub for rapid HIL validation. + +MVP Plan (8–12 weeks) +- Phase 0: runtime core + two starter adapters (rover planner, habitat supervisor) + delta-sync scaffold. +- Phase 1: secure aggregation, governance ledger, delta reconciliation proofs. +- Phase 2: HIL validation with Gazebo/ROS and two physical rigs; cross-domain demo. +- Phase 3: governance conformance tests and a lightweight adapter marketplace entry. + +This repository contains a production-oriented skeleton to enable collaborative extension by multiple teams. See AGENTS.md for architectural guidelines and testing commands. diff --git a/nebulaforge/__init__.py b/nebulaforge/__init__.py new file mode 100644 index 0000000..7612e0c --- /dev/null +++ b/nebulaforge/__init__.py @@ -0,0 +1,21 @@ +"""NebulaForge package initialization.""" +__version__ = "0.1.0" + +from .runtime import DeviceRuntime +from .federated import SecureAggregator +from .contracts import LocalProblem, SharedVariables, PlanDelta, PrivacyBudget, AuditLog +from .governance import GovernanceLedger +from .simulator import SimulatorStub + +__all__ = [ + "__version__", + "DeviceRuntime", + "SecureAggregator", + "LocalProblem", + "SharedVariables", + "PlanDelta", + "PrivacyBudget", + "AuditLog", + "GovernanceLedger", + "SimulatorStub", +] diff --git a/nebulaforge/contracts.py b/nebulaforge/contracts.py new file mode 100644 index 0000000..498394d --- /dev/null +++ b/nebulaforge/contracts.py @@ -0,0 +1,39 @@ +from __future__ import annotations +from dataclasses import dataclass, asdict +from typing import Any, Dict +import json + +@dataclass +class LocalProblem: + id: str + domain: str + description: str + version: int = 1 + +@dataclass +class SharedVariables: + variables: Dict[str, Any] + version: int = 1 + +@dataclass +class PlanDelta: + delta: Dict[str, Any] + timestamp: float + version: int = 1 + +@dataclass +class PrivacyBudget: + lambda_privacy: float + +@dataclass +class AuditLog: + entry: str + signer: str + timestamp: float + contract_id: str + version: int = 1 + +def to_json(obj: Any) -> str: + if hasattr(obj, "__dict__"): + return json.dumps(asdict(obj), default=str) + return json.dumps(obj, default=str) diff --git a/nebulaforge/federated.py b/nebulaforge/federated.py new file mode 100644 index 0000000..e23de8a --- /dev/null +++ b/nebulaforge/federated.py @@ -0,0 +1,29 @@ +from __future__ import annotations +from typing import Any, List + +class SecureAggregator: + """Very small, deterministic secure-aggregation surface. + + This MVP aggregates a list of numeric vectors and optionally applies a + per-vector differential-privacy budget by clipping/noising if budgets are provided. + """ + + def __init__(self, dp_budget: float | None = None) -> None: + self.dp_budget = dp_budget + + def aggregate(self, updates: List[List[float]]) -> List[float]: + if not updates: + return [] + # Simple elementwise average as a placeholder aggregation + n = len(updates) + m = max(len(u) for u in updates) + sums = [0.0] * m + for u in updates: + for i, v in enumerate(u): + sums[i] += v + agg = [s / n for s in sums] + if self.dp_budget is not None: + # Very naive DP: clip to +/- budget around 0 for each dimension + limit = float(self.dp_budget) + agg = [min(max(x, -limit), limit) for x in agg] + return agg diff --git a/nebulaforge/governance.py b/nebulaforge/governance.py new file mode 100644 index 0000000..a5d1251 --- /dev/null +++ b/nebulaforge/governance.py @@ -0,0 +1,28 @@ +from __future__ import annotations +from typing import List +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey +from cryptography.hazmat.primitives import serialization +import time + +class GovernanceLedger: + """Tamper-evident ledger of governance decisions (signature-based).""" + def __init__(self) -> None: + self.entries: List[dict] = [] + self._private_key = Ed25519PrivateKey.generate() + self.public_key = self._private_key.public_key() + + def sign(self, message: bytes) -> bytes: + return self._private_key.sign(message) + + def verify(self, message: bytes, signature: bytes) -> bool: + try: + self.public_key.verify(signature, message) + return True + except Exception: + return False + + def add_entry(self, decision: str, contract_id: str) -> None: + timestamp = time.time() + payload = f"{decision}:{contract_id}:{timestamp}".encode() + sig = self.sign(payload) + self.entries.append({"decision": decision, "contract_id": contract_id, "timestamp": timestamp, "signature": sig.hex()}) diff --git a/nebulaforge/runtime.py b/nebulaforge/runtime.py new file mode 100644 index 0000000..b0713e9 --- /dev/null +++ b/nebulaforge/runtime.py @@ -0,0 +1,28 @@ +from __future__ import annotations +from typing import Any + +class DeviceRuntime: + """Minimal on-device runtime abstraction. + + This is intentionally lightweight and designed to be extended. + """ + + def __init__(self, device: str = "generic-arm-v8") -> None: + self.device = device + self.initialized = False + + def initialize(self, config: dict[str, Any] | None = None) -> None: + # Initialize runtime with optional device-specific config + self.initialized = True + + def infer(self, inputs: Any) -> Any: + if not self.initialized: + raise RuntimeError("DeviceRuntime not initialized") + # Placeholder: return inputs as mock inference result + return {"result": inputs} + + def plan(self, state: Any) -> Any: + if not self.initialized: + raise RuntimeError("DeviceRuntime not initialized") + # Placeholder planning: echo back state as plan delta + return {"plan_delta": state} diff --git a/nebulaforge/simulator.py b/nebulaforge/simulator.py new file mode 100644 index 0000000..a38f36c --- /dev/null +++ b/nebulaforge/simulator.py @@ -0,0 +1,16 @@ +from __future__ import annotations +from typing import Any + +class SimulatorStub: + """Tiny stub to emulate a Gazebo/ROS-style simulator interface.""" + def __init__(self) -> None: + self.connected = False + + def connect(self) -> None: + self.connected = True + + def run(self, scene: str, params: dict[str, Any] | None = None) -> dict[str, Any]: + if not self.connected: + raise RuntimeError("Simulator not connected") + # Return a minimal synthetic result + return {"scene": scene, "params": params or {}, "status": "ok"} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..01c870a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "nebulaforge" +version = "0.1.0" +description = "Offline-resilient federated foundation models for space robotics" +requires-python = ">=3.9" +readme = "README.md" +license = { file = "LICENSE" } +authors = [ + { name = "NebulaForge Contributors" } +] + +dependencies = [ + "cryptography>=38.0.0", + "pydantic>=1.10.2", + "pytest>=7.4.0", + "numpy>=1.23", + "protobuf>=3.20", + "setuptools>=42", + "wheel" +] diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..d98e406 --- /dev/null +++ b/test.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -euo pipefail + +# MVP sanity: run packaging and tests +export PYTHONPATH="${PWD}${PYTHONPATH:+:${PYTHONPATH}}:/usr/local/lib/python3.11/site-packages" +echo "==> Building package (Python) ..." +python3 -m build +echo "==> Running tests (pytest) ..." +pytest -q + +# If we reach here, tests passed +echo "All tests passed." diff --git a/tests/test_contracts.py b/tests/test_contracts.py new file mode 100644 index 0000000..c68d39d --- /dev/null +++ b/tests/test_contracts.py @@ -0,0 +1,21 @@ +from nebulaforge.contracts import LocalProblem, SharedVariables, PlanDelta, AuditLog +import json + +def test_contract_dataclasses(): + lp = LocalProblem(id="lp1", domain="rover", description="test", version=1) + sv = SharedVariables(variables={"a": 1}, version=1) + pd = PlanDelta(delta={"x": 1}, timestamp=123.456, version=1) + al = AuditLog(entry="update", signer="tester", timestamp=123.456, contract_id="lp1", version=1) + + assert isinstance(lp, LocalProblem) + assert lp.id == "lp1" + assert isinstance(sv.variables, dict) + assert isinstance(pd.delta, dict) + assert isinstance(al.entry, str) + + # JSON serialization sanity check + from nebulaforge.contracts import to_json + json_str = to_json(lp) + assert isinstance(json_str, str) + data = json.loads(json_str) + assert data["id"] == "lp1" diff --git a/tests/test_federated.py b/tests/test_federated.py new file mode 100644 index 0000000..20f5390 --- /dev/null +++ b/tests/test_federated.py @@ -0,0 +1,14 @@ +from nebulaforge.federated import SecureAggregator + +def test_aggregate_basic(): + agg = SecureAggregator() + updates = [[1.0, 2.0], [3.0, 4.0]] + out = agg.aggregate(updates) + assert out == [2.0, 3.0] + +def test_dp_budget_clipping(): + agg = SecureAggregator(dp_budget=1.0) + updates = [[2.0, -2.0]] + out = agg.aggregate(updates) + # clipped to +/-1 + assert all(-1.0 <= v <= 1.0 for v in out) diff --git a/tests/test_runtime.py b/tests/test_runtime.py new file mode 100644 index 0000000..601f30a --- /dev/null +++ b/tests/test_runtime.py @@ -0,0 +1,11 @@ +from nebulaforge.runtime import DeviceRuntime + +def test_runtime_basic(): + rt = DeviceRuntime(device="cpu-test") + rt.initialize() + res = rt.infer({"input": 1}) + assert isinstance(res, dict) + assert res.get("result") == {"input": 1} + pl = rt.plan({"state": 42}) + assert isinstance(pl, dict) + assert "plan_delta" in pl