build(agent): semicolon#54de0b iteration
This commit is contained in:
parent
f3daff5ee0
commit
97f542d063
|
|
@ -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,37 @@
|
|||
# AGENTS.md
|
||||
|
||||
## Architecture
|
||||
|
||||
This repository implements `KappaBridge` as a Python package under `src/idea156_kappabridge_privacy_safe`.
|
||||
|
||||
Core modules:
|
||||
- `contracts.py`: canonical contract models for `Object`, `SharedSignal`, `PlanDelta`, `DatasetSchema`, `AccessPolicy`, and audit/reproducibility records.
|
||||
- `storage.py`: SQLite persistence with SQLAlchemy, including the contract table and tamper-evident governance ledger.
|
||||
- `registry.py`: graph-of-contracts registry backed by `networkx`.
|
||||
- `attestation.py`: Ed25519-based cryptographic attestations for contract use.
|
||||
- `sync.py`: deterministic delta sync envelopes for offline-first reconnection.
|
||||
- `adapters.py`: lab/data-format adapters for climate, epidemiology, genomics, ecology, and image datasets.
|
||||
- `optimizer.py`: lightweight deterministic federated planner for joint experiment requests.
|
||||
- `harness.py`: conformance harness for adapter validation.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- Python 3.11+
|
||||
- Pydantic for contract validation
|
||||
- SQLAlchemy + SQLite for durable persistence
|
||||
- Cryptography for attestations
|
||||
- NetworkX for the contract graph
|
||||
- Pandas, Pillow, and NumPy for adapter support
|
||||
|
||||
## Testing
|
||||
|
||||
- Run `bash test.sh` to build the package and execute the full test suite.
|
||||
- The script must continue to pass after any change to contracts, ledger hashing, or sync behavior.
|
||||
|
||||
## Rules
|
||||
|
||||
- Keep contract hashing canonical and deterministic.
|
||||
- Do not mutate stored contracts; append new records instead.
|
||||
- Preserve ledger ordering and hash chaining.
|
||||
- When adding adapters, ensure they produce stable, validated manifests.
|
||||
- Prefer the smallest correct change that keeps offline sync and governance auditable.
|
||||
52
README.md
52
README.md
|
|
@ -1,3 +1,51 @@
|
|||
# idea156-kappabridge-privacy-safe
|
||||
# KappaBridge
|
||||
|
||||
Source logic for Idea #156
|
||||
KappaBridge is a privacy-safe, offline-first scientific knowledge federation framework.
|
||||
|
||||
It provides a contract-based exchange layer for federating datasets and models across labs while preserving data sovereignty. The implementation in this repository focuses on the core control plane:
|
||||
|
||||
- Canonical contracts for datasets, access policy, objects, shared signals, plans, audit events, and reproducibility manifests.
|
||||
- A Graph-of-Contracts registry backed by SQLite and SQLAlchemy.
|
||||
- Tamper-evident governance ledger entries with hash chaining.
|
||||
- Ed25519 attestations for contract use.
|
||||
- Deterministic delta sync envelopes for intermittent connectivity.
|
||||
- Adapters for climate tabular data, epidemiology CSVs, genomics FASTA, ecology JSONL, and image dataset manifests.
|
||||
- A lightweight federated planner for joint experiment requests.
|
||||
- A conformance harness and adapter marketplace for extension points.
|
||||
|
||||
## What It Solves
|
||||
|
||||
Scientific collaborations often break on data movement, trust, and connectivity. KappaBridge lets labs publish structured contracts instead of raw data, exchange signed governance records, and replay changes deterministically when connectivity returns.
|
||||
|
||||
## Package
|
||||
|
||||
- Name: `idea156-kappabridge-privacy-safe`
|
||||
- Import path: `idea156_kappabridge_privacy_safe`
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from idea156_kappabridge_privacy_safe import AccessPolicyContract, ContractRegistry, FederationStore
|
||||
|
||||
store = FederationStore("kappa.db")
|
||||
registry = ContractRegistry(store)
|
||||
policy = AccessPolicyContract(name="default-policy", allowed_actions=["read"])
|
||||
registry.register(policy)
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Run the full package checks with:
|
||||
|
||||
```bash
|
||||
bash test.sh
|
||||
```
|
||||
|
||||
That script builds the distribution and runs the test suite.
|
||||
|
||||
## Design Rules
|
||||
|
||||
- Contract hashes must remain canonical and deterministic.
|
||||
- Stored contracts are immutable; changes are appended as new records.
|
||||
- Ledger ordering and hash chaining must be preserved.
|
||||
- Adapter outputs must stay stable and validated.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
[build-system]
|
||||
requires = ["setuptools>=68", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "idea156-kappabridge-privacy-safe"
|
||||
version = "0.1.0"
|
||||
description = "Privacy-safe offline-first scientific knowledge federation with contract-based sync and governance."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"pydantic>=2.0",
|
||||
"sqlalchemy>=2.0",
|
||||
"cryptography>=42.0",
|
||||
"networkx>=3.0",
|
||||
"pandas>=2.0",
|
||||
"pillow>=10.0",
|
||||
"numpy>=1.26",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
test = ["pytest>=8.0", "build>=1.0"]
|
||||
|
||||
[tool.setuptools]
|
||||
package-dir = {"" = "src"}
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
pythonpath = ["src"]
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
"""KappaBridge: privacy-safe offline-first scientific knowledge federation."""
|
||||
|
||||
from .adapters import (
|
||||
AdapterMarketplace,
|
||||
ClimateCSVAdapter,
|
||||
EcologyJSONLAdapter,
|
||||
EpidemiologyCSVAdapter,
|
||||
GenomicsFASTAAdapter,
|
||||
ImageDatasetAdapter,
|
||||
)
|
||||
from .attestation import AttestationAuthority, AttestationRecord
|
||||
from .contracts import (
|
||||
AccessPolicyContract,
|
||||
AuditLogContract,
|
||||
DatasetSchemaContract,
|
||||
ObjectContract,
|
||||
PlanDeltaContract,
|
||||
ReproducibilityManifestContract,
|
||||
SharedSignalContract,
|
||||
)
|
||||
from .optimizer import FederatedPlanner, LabProfile
|
||||
from .registry import ContractRegistry
|
||||
from .storage import FederationStore
|
||||
from .sync import DeltaSyncEngine, SyncEnvelope
|
||||
|
||||
__all__ = [
|
||||
"AccessPolicyContract",
|
||||
"AdapterMarketplace",
|
||||
"AttestationAuthority",
|
||||
"AttestationRecord",
|
||||
"AuditLogContract",
|
||||
"ClimateCSVAdapter",
|
||||
"ContractRegistry",
|
||||
"DatasetSchemaContract",
|
||||
"DeltaSyncEngine",
|
||||
"EcologyJSONLAdapter",
|
||||
"EpidemiologyCSVAdapter",
|
||||
"FederatedPlanner",
|
||||
"FederationStore",
|
||||
"GenomicsFASTAAdapter",
|
||||
"ImageDatasetAdapter",
|
||||
"LabProfile",
|
||||
"ObjectContract",
|
||||
"PlanDeltaContract",
|
||||
"ReproducibilityManifestContract",
|
||||
"SharedSignalContract",
|
||||
"SyncEnvelope",
|
||||
]
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from io import BytesIO, StringIO
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Protocol
|
||||
|
||||
import pandas as pd
|
||||
from PIL import Image
|
||||
|
||||
from .contracts import (
|
||||
AccessPolicyContract,
|
||||
DatasetSchemaContract,
|
||||
ObjectContract,
|
||||
SchemaField,
|
||||
SharedSignalContract,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AdaptedBundle:
|
||||
dataset_schema: DatasetSchemaContract
|
||||
object_contract: ObjectContract
|
||||
shared_signals: list[SharedSignalContract]
|
||||
metadata: dict[str, Any]
|
||||
|
||||
|
||||
class DataAdapter(Protocol):
|
||||
name: str
|
||||
|
||||
def adapt(self, source: Any, *, dataset_name: str, discipline: str) -> AdaptedBundle: ...
|
||||
|
||||
|
||||
class ClimateCSVAdapter:
|
||||
name = "climate-csv"
|
||||
|
||||
def adapt(self, source: str | Path, *, dataset_name: str, discipline: str = "climate") -> AdaptedBundle:
|
||||
frame = pd.read_csv(source)
|
||||
fields = [SchemaField(name=str(column), dtype=str(frame[column].dtype), required=True) for column in frame.columns]
|
||||
payload = frame.to_csv(index=False).encode("utf-8")
|
||||
schema = DatasetSchemaContract(
|
||||
name=f"{dataset_name}-schema",
|
||||
modality="tabular",
|
||||
discipline=discipline,
|
||||
fields=fields,
|
||||
provenance={"rows": int(len(frame)), "columns": list(frame.columns)},
|
||||
)
|
||||
object_contract = ObjectContract(
|
||||
name=f"{dataset_name}-object",
|
||||
media_type="text/csv",
|
||||
content_hash=_sha256(payload),
|
||||
byte_size=len(payload),
|
||||
source_uri=str(source),
|
||||
)
|
||||
signals = [
|
||||
SharedSignalContract(
|
||||
name=f"{dataset_name}-signal",
|
||||
signal_name="tabular_summary",
|
||||
source_object_id=object_contract.contract_id,
|
||||
payload_hash=schema.digest(),
|
||||
privacy_budget_spent=0.0,
|
||||
)
|
||||
]
|
||||
return AdaptedBundle(schema, object_contract, signals, {"adapter": self.name, "rows": len(frame)})
|
||||
|
||||
|
||||
class EpidemiologyCSVAdapter(ClimateCSVAdapter):
|
||||
name = "epidemiology-csv"
|
||||
|
||||
def adapt(self, source: str | Path, *, dataset_name: str, discipline: str = "epidemiology") -> AdaptedBundle:
|
||||
return super().adapt(source, dataset_name=dataset_name, discipline=discipline)
|
||||
|
||||
|
||||
class GenomicsFASTAAdapter:
|
||||
name = "genomics-fasta"
|
||||
|
||||
def adapt(self, source: str | Path, *, dataset_name: str, discipline: str = "genomics") -> AdaptedBundle:
|
||||
text = Path(source).read_text(encoding="utf-8")
|
||||
records = _parse_fasta(text)
|
||||
fields = [SchemaField(name="sequence_id", dtype="string"), SchemaField(name="sequence", dtype="string")]
|
||||
schema = DatasetSchemaContract(
|
||||
name=f"{dataset_name}-schema",
|
||||
modality="sequence",
|
||||
discipline=discipline,
|
||||
fields=fields,
|
||||
provenance={"records": len(records)},
|
||||
)
|
||||
object_contract = ObjectContract(
|
||||
name=f"{dataset_name}-object",
|
||||
media_type="chemical/seq-na",
|
||||
content_hash=_sha256(text.encode("utf-8")),
|
||||
byte_size=len(text.encode("utf-8")),
|
||||
source_uri=str(source),
|
||||
)
|
||||
signals = [
|
||||
SharedSignalContract(
|
||||
name=f"{dataset_name}-signal",
|
||||
signal_name="sequence_inventory",
|
||||
source_object_id=object_contract.contract_id,
|
||||
payload_hash=schema.digest(),
|
||||
)
|
||||
]
|
||||
return AdaptedBundle(schema, object_contract, signals, {"adapter": self.name, "records": len(records)})
|
||||
|
||||
|
||||
class EcologyJSONLAdapter:
|
||||
name = "ecology-jsonl"
|
||||
|
||||
def adapt(self, source: str | Path, *, dataset_name: str, discipline: str = "ecology") -> AdaptedBundle:
|
||||
text = Path(source).read_text(encoding="utf-8")
|
||||
rows = [json.loads(line) for line in text.splitlines() if line.strip()]
|
||||
columns = sorted({key for row in rows for key in row.keys()})
|
||||
fields = [SchemaField(name=column, dtype="string", required=False) for column in columns]
|
||||
schema = DatasetSchemaContract(
|
||||
name=f"{dataset_name}-schema",
|
||||
modality="tabular",
|
||||
discipline=discipline,
|
||||
fields=fields,
|
||||
provenance={"observations": len(rows)},
|
||||
)
|
||||
object_contract = ObjectContract(
|
||||
name=f"{dataset_name}-object",
|
||||
media_type="application/jsonl",
|
||||
content_hash=_sha256(text.encode("utf-8")),
|
||||
byte_size=len(text.encode("utf-8")),
|
||||
source_uri=str(source),
|
||||
)
|
||||
return AdaptedBundle(schema, object_contract, [], {"adapter": self.name, "observations": len(rows)})
|
||||
|
||||
|
||||
class ImageDatasetAdapter:
|
||||
name = "image-dataset"
|
||||
|
||||
def adapt(self, source: str | Path, *, dataset_name: str, discipline: str = "cross-domain") -> AdaptedBundle:
|
||||
path = Path(source)
|
||||
images = sorted([p for p in path.iterdir() if p.is_file()])
|
||||
rows = []
|
||||
for image_path in images:
|
||||
with Image.open(image_path) as img:
|
||||
rows.append({"filename": image_path.name, "width": img.width, "height": img.height, "format": img.format or "unknown"})
|
||||
frame = pd.DataFrame(rows)
|
||||
fields = [SchemaField(name="filename", dtype="string"), SchemaField(name="width", dtype="integer"), SchemaField(name="height", dtype="integer"), SchemaField(name="format", dtype="string")]
|
||||
schema = DatasetSchemaContract(
|
||||
name=f"{dataset_name}-schema",
|
||||
modality="image",
|
||||
discipline=discipline,
|
||||
fields=fields,
|
||||
provenance={"images": len(images)},
|
||||
)
|
||||
manifest_bytes = frame.to_json(orient="records", indent=None).encode("utf-8")
|
||||
object_contract = ObjectContract(
|
||||
name=f"{dataset_name}-object",
|
||||
media_type="application/vnd.kappabridge.image-manifest+json",
|
||||
content_hash=_sha256(manifest_bytes),
|
||||
byte_size=len(manifest_bytes),
|
||||
source_uri=str(source),
|
||||
)
|
||||
return AdaptedBundle(schema, object_contract, [], {"adapter": self.name, "images": len(images)})
|
||||
|
||||
|
||||
class AdapterMarketplace:
|
||||
def __init__(self) -> None:
|
||||
self._adapters: dict[str, DataAdapter] = {}
|
||||
|
||||
def register(self, adapter: DataAdapter) -> None:
|
||||
self._adapters[adapter.name] = adapter
|
||||
|
||||
def get(self, name: str) -> DataAdapter:
|
||||
return self._adapters[name]
|
||||
|
||||
def names(self) -> list[str]:
|
||||
return sorted(self._adapters)
|
||||
|
||||
|
||||
def _parse_fasta(text: str) -> list[tuple[str, str]]:
|
||||
records: list[tuple[str, str]] = []
|
||||
current_id: str | None = None
|
||||
chunks: list[str] = []
|
||||
for line in text.splitlines():
|
||||
if line.startswith(">"):
|
||||
if current_id is not None:
|
||||
records.append((current_id, "".join(chunks)))
|
||||
current_id = line[1:].strip()
|
||||
chunks = []
|
||||
elif line.strip():
|
||||
chunks.append(line.strip())
|
||||
if current_id is not None:
|
||||
records.append((current_id, "".join(chunks)))
|
||||
return records
|
||||
|
||||
|
||||
def _sha256(data: bytes) -> str:
|
||||
import hashlib
|
||||
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from base64 import b64decode, b64encode
|
||||
from datetime import datetime, timezone
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from .contracts import BaseContract
|
||||
|
||||
|
||||
class AttestationRecord(BaseModel):
|
||||
model_config = ConfigDict(frozen=True, extra="forbid")
|
||||
|
||||
attestation_id: str
|
||||
subject_contract_id: str
|
||||
subject_hash: str
|
||||
signer: str
|
||||
signature_b64: str
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
claims: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AttestationAuthority:
|
||||
signer_id: str
|
||||
private_key: Ed25519PrivateKey | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.private_key is None:
|
||||
self.private_key = Ed25519PrivateKey.generate()
|
||||
|
||||
@property
|
||||
def public_key(self) -> Ed25519PublicKey:
|
||||
assert self.private_key is not None
|
||||
return self.private_key.public_key()
|
||||
|
||||
def attest(self, contract: BaseContract, **claims: Any) -> AttestationRecord:
|
||||
payload = f"{contract.contract_id}|{contract.digest()}|{self.signer_id}".encode("utf-8")
|
||||
assert self.private_key is not None
|
||||
signature = self.private_key.sign(payload)
|
||||
return AttestationRecord(
|
||||
attestation_id=f"att-{contract.contract_id}",
|
||||
subject_contract_id=contract.contract_id,
|
||||
subject_hash=contract.digest(),
|
||||
signer=self.signer_id,
|
||||
signature_b64=b64encode(signature).decode("ascii"),
|
||||
claims=claims,
|
||||
)
|
||||
|
||||
def verify(self, attestation: AttestationRecord, contract: BaseContract, public_key: Ed25519PublicKey | None = None) -> bool:
|
||||
key = public_key or self.public_key
|
||||
payload = f"{contract.contract_id}|{contract.digest()}|{attestation.signer}".encode("utf-8")
|
||||
try:
|
||||
key.verify(b64decode(attestation.signature_b64), payload)
|
||||
except InvalidSignature:
|
||||
return False
|
||||
return attestation.subject_contract_id == contract.contract_id and attestation.subject_hash == contract.digest()
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from hashlib import sha256
|
||||
import json
|
||||
from typing import Any, Literal
|
||||
from uuid import uuid4
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
def _utcnow() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
def _canonical_payload(value: BaseModel | dict[str, Any]) -> str:
|
||||
if isinstance(value, BaseModel):
|
||||
payload = value.model_dump(mode="json", exclude={"contract_id", "created_at"})
|
||||
else:
|
||||
payload = value
|
||||
return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
|
||||
|
||||
|
||||
class ContractKind(str, Enum):
|
||||
object = "Object"
|
||||
shared_signal = "SharedSignal"
|
||||
plan_delta = "PlanDelta"
|
||||
dataset_schema = "DatasetSchema"
|
||||
access_policy = "AccessPolicy"
|
||||
audit_log = "AuditLog"
|
||||
reproducibility_manifest = "ReproducibilityManifest"
|
||||
|
||||
|
||||
class BaseContract(BaseModel):
|
||||
model_config = ConfigDict(frozen=True, extra="forbid")
|
||||
|
||||
contract_id: str = Field(default_factory=lambda: uuid4().hex)
|
||||
kind: str
|
||||
name: str
|
||||
version: str = "1.0.0"
|
||||
created_at: datetime = Field(default_factory=_utcnow)
|
||||
parents: list[str] = Field(default_factory=list)
|
||||
|
||||
def digest(self) -> str:
|
||||
return sha256(_canonical_payload(self).encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
class SchemaField(BaseModel):
|
||||
model_config = ConfigDict(frozen=True, extra="forbid")
|
||||
|
||||
name: str
|
||||
dtype: str
|
||||
required: bool = True
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class DatasetSchemaContract(BaseContract):
|
||||
kind: Literal["DatasetSchema"] = ContractKind.dataset_schema.value
|
||||
modality: str
|
||||
discipline: str
|
||||
fields: list[SchemaField]
|
||||
provenance: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class AccessPolicyContract(BaseContract):
|
||||
kind: Literal["AccessPolicy"] = ContractKind.access_policy.value
|
||||
allowed_actions: list[str]
|
||||
privacy_budget_epsilon: float | None = None
|
||||
requires_attestation: bool = True
|
||||
data_residency: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ObjectContract(BaseContract):
|
||||
kind: Literal["Object"] = ContractKind.object.value
|
||||
media_type: str
|
||||
content_hash: str
|
||||
byte_size: int
|
||||
source_uri: str | None = None
|
||||
|
||||
|
||||
class SharedSignalContract(BaseContract):
|
||||
kind: Literal["SharedSignal"] = ContractKind.shared_signal.value
|
||||
signal_name: str
|
||||
source_object_id: str
|
||||
payload_hash: str
|
||||
privacy_budget_spent: float = 0.0
|
||||
|
||||
|
||||
class PlanDeltaContract(BaseContract):
|
||||
kind: Literal["PlanDelta"] = ContractKind.plan_delta.value
|
||||
objective: str
|
||||
requested_contracts: list[str]
|
||||
steps: list[str]
|
||||
deterministic_key: str
|
||||
|
||||
|
||||
class AuditLogContract(BaseContract):
|
||||
kind: Literal["AuditLog"] = ContractKind.audit_log.value
|
||||
actor: str
|
||||
action: str
|
||||
target_contract_id: str
|
||||
details: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class ReproducibilityManifestContract(BaseContract):
|
||||
kind: Literal["ReproducibilityManifest"] = ContractKind.reproducibility_manifest.value
|
||||
execution_id: str
|
||||
code_hash: str
|
||||
data_hashes: list[str]
|
||||
environment: dict[str, Any] = Field(default_factory=dict)
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .adapters import AdaptedBundle, AdapterMarketplace, DataAdapter
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ConformanceResult:
|
||||
adapter_name: str
|
||||
passed: bool
|
||||
checks: list[str]
|
||||
|
||||
|
||||
class ConformanceHarness:
|
||||
def __init__(self, marketplace: AdapterMarketplace) -> None:
|
||||
self.marketplace = marketplace
|
||||
|
||||
def validate(self, adapter_name: str, source: Any, *, dataset_name: str, discipline: str) -> ConformanceResult:
|
||||
adapter: DataAdapter = self.marketplace.get(adapter_name)
|
||||
bundle = adapter.adapt(source, dataset_name=dataset_name, discipline=discipline)
|
||||
checks = [
|
||||
f"schema:{bundle.dataset_schema.digest()}",
|
||||
f"object:{bundle.object_contract.digest()}",
|
||||
f"signals:{len(bundle.shared_signals)}",
|
||||
]
|
||||
return ConformanceResult(adapter_name=adapter_name, passed=bool(bundle.dataset_schema.fields and bundle.object_contract.content_hash), checks=checks)
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from hashlib import sha256
|
||||
|
||||
from .contracts import PlanDeltaContract
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LabProfile:
|
||||
lab_name: str
|
||||
discipline: str
|
||||
modalities: list[str]
|
||||
offline_capable: bool = True
|
||||
privacy_budget_epsilon: float | None = None
|
||||
|
||||
|
||||
class FederatedPlanner:
|
||||
def plan_joint_objective(self, objective: str, labs: list[LabProfile], requested_contracts: list[str]) -> PlanDeltaContract:
|
||||
ordered_labs = sorted(labs, key=lambda lab: (lab.discipline, lab.lab_name))
|
||||
steps = [
|
||||
f"request:{lab.lab_name}:{','.join(sorted(lab.modalities))}"
|
||||
for lab in ordered_labs
|
||||
if lab.offline_capable
|
||||
]
|
||||
steps.append(f"objective:{objective}")
|
||||
deterministic_key = sha256("|".join([objective, *sorted(requested_contracts), *steps]).encode("utf-8")).hexdigest()
|
||||
return PlanDeltaContract(
|
||||
name="joint-plan",
|
||||
objective=objective,
|
||||
requested_contracts=sorted(requested_contracts),
|
||||
steps=steps,
|
||||
deterministic_key=deterministic_key,
|
||||
)
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import networkx as nx
|
||||
|
||||
from .contracts import BaseContract
|
||||
from .storage import FederationStore
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RegistryLookup:
|
||||
contract_id: str
|
||||
ancestors: list[str]
|
||||
descendants: list[str]
|
||||
|
||||
|
||||
class ContractRegistry:
|
||||
def __init__(self, store: FederationStore) -> None:
|
||||
self.store = store
|
||||
self.graph = nx.DiGraph()
|
||||
self._load_from_store()
|
||||
|
||||
def _load_from_store(self) -> None:
|
||||
for record in self.store.contracts():
|
||||
self.graph.add_node(record["contract_id"], **record)
|
||||
for parent in record["parents"]:
|
||||
self.graph.add_edge(parent, record["contract_id"])
|
||||
|
||||
def register(self, contract: BaseContract) -> str:
|
||||
self.store.upsert_contract(contract)
|
||||
self.graph.add_node(contract.contract_id, contract=contract, kind=contract.kind, name=contract.name)
|
||||
for parent in contract.parents:
|
||||
self.graph.add_edge(parent, contract.contract_id)
|
||||
self.store.record_event(
|
||||
"contract_added",
|
||||
{
|
||||
"contract": contract.model_dump(mode="json"),
|
||||
"content_hash": contract.digest(),
|
||||
"active": True,
|
||||
},
|
||||
)
|
||||
return contract.contract_id
|
||||
|
||||
def get(self, contract_id: str) -> dict[str, object] | None:
|
||||
return self.store.get_contract_record(contract_id)
|
||||
|
||||
def ancestry(self, contract_id: str) -> list[str]:
|
||||
return sorted(nx.ancestors(self.graph, contract_id))
|
||||
|
||||
def descendants(self, contract_id: str) -> list[str]:
|
||||
return sorted(nx.descendants(self.graph, contract_id))
|
||||
|
||||
def lookup(self, contract_id: str) -> RegistryLookup:
|
||||
return RegistryLookup(contract_id=contract_id, ancestors=self.ancestry(contract_id), descendants=self.descendants(contract_id))
|
||||
|
||||
def active_contracts(self, kind: str | None = None) -> list[dict[str, object]]:
|
||||
contracts = [record for record in self.store.contracts() if record["active"]]
|
||||
if kind is not None:
|
||||
contracts = [record for record in contracts if record["kind"] == kind]
|
||||
return contracts
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, Integer, String, Text, create_engine, select
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column, sessionmaker
|
||||
|
||||
from .contracts import BaseContract
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class ContractRow(Base):
|
||||
__tablename__ = "contracts"
|
||||
|
||||
contract_id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||
kind: Mapped[str] = mapped_column(String(64), index=True)
|
||||
name: Mapped[str] = mapped_column(String(255), index=True)
|
||||
version: Mapped[str] = mapped_column(String(32))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True)
|
||||
parents_json: Mapped[str] = mapped_column(Text)
|
||||
payload_json: Mapped[str] = mapped_column(Text)
|
||||
content_hash: Mapped[str] = mapped_column(String(64), index=True)
|
||||
active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
|
||||
|
||||
|
||||
class LedgerRow(Base):
|
||||
__tablename__ = "governance_ledger"
|
||||
|
||||
seq: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
event_type: Mapped[str] = mapped_column(String(64), index=True)
|
||||
event_json: Mapped[str] = mapped_column(Text)
|
||||
prev_hash: Mapped[str] = mapped_column(String(64))
|
||||
entry_hash: Mapped[str] = mapped_column(String(64), unique=True, index=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True)
|
||||
|
||||
|
||||
class SyncStateRow(Base):
|
||||
__tablename__ = "sync_state"
|
||||
|
||||
peer_id: Mapped[str] = mapped_column(String(128), primary_key=True)
|
||||
last_seq: Mapped[int] = mapped_column(Integer, default=0)
|
||||
|
||||
|
||||
def _utcnow() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class FederationStore:
|
||||
def __init__(self, path: str | Path = ":memory:") -> None:
|
||||
self.path = str(path)
|
||||
self.engine = create_engine(
|
||||
f"sqlite:///{self.path}" if self.path != ":memory:" else "sqlite+pysqlite:///:memory:",
|
||||
future=True,
|
||||
)
|
||||
Base.metadata.create_all(self.engine)
|
||||
self.session_factory = sessionmaker(bind=self.engine, future=True)
|
||||
|
||||
def session(self) -> Session:
|
||||
return self.session_factory()
|
||||
|
||||
def upsert_contract(self, contract: BaseContract, *, active: bool = True) -> None:
|
||||
with self.session() as session:
|
||||
row = ContractRow(
|
||||
contract_id=contract.contract_id,
|
||||
kind=contract.kind,
|
||||
name=contract.name,
|
||||
version=contract.version,
|
||||
created_at=contract.created_at,
|
||||
parents_json=json.dumps(contract.parents, sort_keys=True),
|
||||
payload_json=json.dumps(contract.model_dump(mode="json"), sort_keys=True, separators=(",", ":"), ensure_ascii=True),
|
||||
content_hash=contract.digest(),
|
||||
active=active,
|
||||
)
|
||||
session.merge(row)
|
||||
session.commit()
|
||||
|
||||
def contracts(self) -> list[dict[str, Any]]:
|
||||
with self.session() as session:
|
||||
rows = session.execute(select(ContractRow).order_by(ContractRow.created_at, ContractRow.contract_id)).scalars().all()
|
||||
return [self._row_to_contract_dict(row) for row in rows]
|
||||
|
||||
def get_contract_record(self, contract_id: str) -> dict[str, Any] | None:
|
||||
with self.session() as session:
|
||||
row = session.get(ContractRow, contract_id)
|
||||
return None if row is None else self._row_to_contract_dict(row)
|
||||
|
||||
def record_event(self, event_type: str, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
with self.session() as session:
|
||||
last = session.execute(select(LedgerRow).order_by(LedgerRow.seq.desc())).scalars().first()
|
||||
prev_hash = last.entry_hash if last else "0" * 64
|
||||
seq = (last.seq if last else 0) + 1
|
||||
event_json = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
|
||||
entry_hash = _hash_ledger(seq, event_type, event_json, prev_hash)
|
||||
row = LedgerRow(
|
||||
seq=seq,
|
||||
event_type=event_type,
|
||||
event_json=event_json,
|
||||
prev_hash=prev_hash,
|
||||
entry_hash=entry_hash,
|
||||
created_at=_utcnow(),
|
||||
)
|
||||
session.add(row)
|
||||
session.commit()
|
||||
return self._ledger_row_to_dict(row)
|
||||
|
||||
def import_ledger_event(self, record: dict[str, Any]) -> dict[str, Any]:
|
||||
with self.session() as session:
|
||||
if session.execute(select(LedgerRow).where(LedgerRow.entry_hash == record["entry_hash"])).scalar_one_or_none():
|
||||
return record
|
||||
row = LedgerRow(
|
||||
seq=record["seq"],
|
||||
event_type=record["event_type"],
|
||||
event_json=record["event_json"],
|
||||
prev_hash=record["prev_hash"],
|
||||
entry_hash=record["entry_hash"],
|
||||
created_at=datetime.fromisoformat(record["created_at"]),
|
||||
)
|
||||
session.add(row)
|
||||
session.commit()
|
||||
payload = json.loads(row.event_json)
|
||||
if row.event_type == "contract_added":
|
||||
self.import_contract_payload(payload)
|
||||
return self._ledger_row_to_dict(row)
|
||||
|
||||
def import_contract_payload(self, payload: dict[str, Any]) -> None:
|
||||
contract = payload["contract"]
|
||||
with self.session() as session:
|
||||
row = ContractRow(
|
||||
contract_id=contract["contract_id"],
|
||||
kind=contract["kind"],
|
||||
name=contract["name"],
|
||||
version=contract["version"],
|
||||
created_at=datetime.fromisoformat(contract["created_at"]),
|
||||
parents_json=json.dumps(contract.get("parents", []), sort_keys=True),
|
||||
payload_json=json.dumps(contract, sort_keys=True, separators=(",", ":"), ensure_ascii=True),
|
||||
content_hash=payload["content_hash"],
|
||||
active=payload.get("active", True),
|
||||
)
|
||||
session.merge(row)
|
||||
session.commit()
|
||||
|
||||
def ledger_since(self, seq: int) -> list[dict[str, Any]]:
|
||||
with self.session() as session:
|
||||
rows = session.execute(select(LedgerRow).where(LedgerRow.seq > seq).order_by(LedgerRow.seq)).scalars().all()
|
||||
return [self._ledger_row_to_dict(row) for row in rows]
|
||||
|
||||
def latest_seq(self) -> int:
|
||||
with self.session() as session:
|
||||
row = session.execute(select(LedgerRow).order_by(LedgerRow.seq.desc())).scalars().first()
|
||||
return 0 if row is None else row.seq
|
||||
|
||||
@staticmethod
|
||||
def _row_to_contract_dict(row: ContractRow) -> dict[str, Any]:
|
||||
return {
|
||||
"contract_id": row.contract_id,
|
||||
"kind": row.kind,
|
||||
"name": row.name,
|
||||
"version": row.version,
|
||||
"created_at": row.created_at.isoformat(),
|
||||
"parents": json.loads(row.parents_json),
|
||||
"payload_json": row.payload_json,
|
||||
"content_hash": row.content_hash,
|
||||
"active": row.active,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _ledger_row_to_dict(row: LedgerRow) -> dict[str, Any]:
|
||||
return {
|
||||
"seq": row.seq,
|
||||
"event_type": row.event_type,
|
||||
"event_json": row.event_json,
|
||||
"prev_hash": row.prev_hash,
|
||||
"entry_hash": row.entry_hash,
|
||||
"created_at": row.created_at.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
def _hash_ledger(seq: int, event_type: str, event_json: str, prev_hash: str) -> str:
|
||||
import hashlib
|
||||
|
||||
data = f"{seq}|{event_type}|{event_json}|{prev_hash}".encode("utf-8")
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .storage import FederationStore
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SyncEnvelope:
|
||||
anchor_seq: int
|
||||
source: str
|
||||
records: list[dict[str, object]]
|
||||
|
||||
|
||||
class DeltaSyncEngine:
|
||||
def __init__(self, store: FederationStore, peer_id: str) -> None:
|
||||
self.store = store
|
||||
self.peer_id = peer_id
|
||||
|
||||
def build_envelope(self, anchor_seq: int = 0) -> SyncEnvelope:
|
||||
return SyncEnvelope(anchor_seq=anchor_seq, source=self.peer_id, records=self.store.ledger_since(anchor_seq))
|
||||
|
||||
def ingest_envelope(self, envelope: SyncEnvelope) -> int:
|
||||
applied = 0
|
||||
for record in envelope.records:
|
||||
self.store.import_ledger_event(record)
|
||||
applied += 1
|
||||
return applied
|
||||
|
||||
def checkpoint(self) -> int:
|
||||
return self.store.latest_seq()
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
python3 -m build
|
||||
pytest -q
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from idea156_kappabridge_privacy_safe import (
|
||||
AccessPolicyContract,
|
||||
AdapterMarketplace,
|
||||
AttestationAuthority,
|
||||
ClimateCSVAdapter,
|
||||
ContractRegistry,
|
||||
DeltaSyncEngine,
|
||||
FederationStore,
|
||||
FederatedPlanner,
|
||||
GenomicsFASTAAdapter,
|
||||
LabProfile,
|
||||
)
|
||||
from idea156_kappabridge_privacy_safe.harness import ConformanceHarness
|
||||
|
||||
|
||||
def test_registry_ledger_and_attestation(tmp_path: Path) -> None:
|
||||
store = FederationStore(tmp_path / "kappa.db")
|
||||
registry = ContractRegistry(store)
|
||||
|
||||
policy = AccessPolicyContract(name="open-policy", allowed_actions=["read", "request"], privacy_budget_epsilon=1.0)
|
||||
registry.register(policy)
|
||||
|
||||
authority = AttestationAuthority("lab-a")
|
||||
attestation = authority.attest(policy, purpose="governance")
|
||||
assert authority.verify(attestation, policy)
|
||||
|
||||
assert registry.active_contracts(kind="AccessPolicy")[0]["name"] == "open-policy"
|
||||
assert store.latest_seq() == 1
|
||||
|
||||
|
||||
def test_delta_sync_replays_ledger(tmp_path: Path) -> None:
|
||||
left = FederationStore(tmp_path / "left.db")
|
||||
right = FederationStore(tmp_path / "right.db")
|
||||
left_registry = ContractRegistry(left)
|
||||
right_registry = ContractRegistry(right)
|
||||
|
||||
policy = AccessPolicyContract(name="sync-policy", allowed_actions=["read"])
|
||||
left_registry.register(policy)
|
||||
|
||||
engine = DeltaSyncEngine(left, peer_id="lab-left")
|
||||
envelope = engine.build_envelope(anchor_seq=0)
|
||||
assert right_registry.store.latest_seq() == 0
|
||||
applied = DeltaSyncEngine(right, peer_id="lab-right").ingest_envelope(envelope)
|
||||
assert applied == 1
|
||||
assert right_registry.store.latest_seq() == 1
|
||||
assert right_registry.get(policy.contract_id)["name"] == "sync-policy"
|
||||
|
||||
|
||||
def test_adapters_and_planner_conformance(tmp_path: Path) -> None:
|
||||
climate_csv = tmp_path / "climate.csv"
|
||||
climate_csv.write_text("station,temp,humidity\nA,12,40\nB,15,55\n", encoding="utf-8")
|
||||
|
||||
fasta = tmp_path / "genome.fasta"
|
||||
fasta.write_text(
|
||||
">seq1\nACGT\n>seq2\nGGTT\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
marketplace = AdapterMarketplace()
|
||||
marketplace.register(ClimateCSVAdapter())
|
||||
marketplace.register(GenomicsFASTAAdapter())
|
||||
|
||||
harness = ConformanceHarness(marketplace)
|
||||
assert harness.validate("climate-csv", climate_csv, dataset_name="climate", discipline="climate").passed
|
||||
assert harness.validate("genomics-fasta", fasta, dataset_name="genome", discipline="genomics").passed
|
||||
|
||||
planner = FederatedPlanner()
|
||||
plan = planner.plan_joint_objective(
|
||||
"joint climate-genomics study",
|
||||
[LabProfile("lab-a", "climate", ["tabular"]), LabProfile("lab-b", "genomics", ["sequence"])],
|
||||
["contract-a", "contract-b"],
|
||||
)
|
||||
assert plan.kind == "PlanDelta"
|
||||
assert plan.steps
|
||||
Loading…
Reference in New Issue