build(agent): semicolon#54de0b iteration
This commit is contained in:
parent
248a465cd5
commit
c5f7e1b606
14
README.md
14
README.md
|
|
@ -1,18 +1,18 @@
|
||||||
# PortaLedger: Verifiable Privacy-Preserving Provenance
|
# PortaLedger: Verifiable Privacy-Preserving Provenance
|
||||||
|
|
||||||
Overview
|
Overview
|
||||||
- A production-grade prototype that records provenance for portfolio experiments in a privacy-preserving, verifiable ledger. Includes a CatOpt-style bridge to exchange cross-domain proofs without leaking private data.
|
- A compact, testable Python package for recording portfolio experiment provenance with cryptographic attestation, SQLite-backed audit storage, and a CatOpt-style bridge for canonical Graph-of-Contracts payloads.
|
||||||
|
|
||||||
Architecture
|
Architecture
|
||||||
- Core: src/portaledger/core.py defines LocalProblem, SharedVariable, PlanDelta, ProvenanceEvent, Attestation, and PortaLedger (SQLite-backed).
|
- Core: src/portaledger/core.py defines LocalProblem, SharedVariable, PlanDelta, ProvenanceEvent, Attestation, ProvenanceRecord, and PortaLedger (SQLite-backed).
|
||||||
- Bridge: src/portaledger/bridge.py exposes a minimal PortaBridge and bridge_to_catopt for cross-domain GoC-like messages.
|
- Bridge: src/portaledger/bridge.py exposes PortaBridge, bridge_to_catopt, PrivacyBudget, and PolicyBlock for GoC-like messages.
|
||||||
- Governance: src/portaledger/governance.py provides a simple governance log anchored in SQLite.
|
- Governance: src/portaledger/governance.py provides a versioned policy registry and tamper-evident governance log anchored in SQLite.
|
||||||
- Tests: tests/test_portaledger.py validates signing, logging, and bridge mapping.
|
- Tests: tests/test_portaledger.py validates signing, verification, querying, bridge mapping, and governance versioning.
|
||||||
- Packaging: pyproject.toml, README.md, and test.sh to build and test.
|
- Packaging: pyproject.toml, README.md, and test.sh to build and test.
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
- Run tests: ./test.sh
|
- Run tests: ./test.sh
|
||||||
- The Python package is published as portaledger (0.1.0) with a production-like API surface for core data types and ledger hooks.
|
- The Python package is published as portaledger (0.1.0) with a production-like API surface for core data types, bridge helpers, and governance hooks.
|
||||||
|
|
||||||
Note
|
Note
|
||||||
- This is a solid foundation; additional complexity (DID identities, secure aggregation, and full CatOpt SDK bindings) can be layered in subsequent sprint phases.
|
- This implementation already supports attested deltas, queryable provenance history, structured bridge payloads, and versioned governance records.
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ name = "portaledger"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Verifiable, privacy-preserving portfolio experiment provenance with PortaLedger and CatOpt bridge"
|
description = "Verifiable, privacy-preserving portfolio experiment provenance with PortaLedger and CatOpt bridge"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = { text = "MIT" }
|
license = "MIT"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cryptography>=40.0.0",
|
"cryptography>=40.0.0",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from .core import LocalProblem, SharedVariable, PlanDelta, ProvenanceEvent, Attestation
|
from .core import LocalProblem, SharedVariable, PlanDelta, ProvenanceEvent, Attestation
|
||||||
from .core import PortaLedger
|
from .core import PortaLedger, ProvenanceRecord
|
||||||
from .bridge import PortaBridge, bridge_to_catopt
|
from .bridge import PortaBridge, bridge_to_catopt, PrivacyBudget, PolicyBlock
|
||||||
from .governance import GovernanceLedger
|
from .governance import GovernanceLedger
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|
@ -9,8 +9,11 @@ __all__ = [
|
||||||
"PlanDelta",
|
"PlanDelta",
|
||||||
"ProvenanceEvent",
|
"ProvenanceEvent",
|
||||||
"Attestation",
|
"Attestation",
|
||||||
|
"ProvenanceRecord",
|
||||||
"PortaLedger",
|
"PortaLedger",
|
||||||
"PortaBridge",
|
"PortaBridge",
|
||||||
|
"PrivacyBudget",
|
||||||
|
"PolicyBlock",
|
||||||
"bridge_to_catopt",
|
"bridge_to_catopt",
|
||||||
"GovernanceLedger",
|
"GovernanceLedger",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,57 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict, dataclass
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from .core import LocalProblem, SharedVariable, PlanDelta, ProvenanceEvent, Attestation
|
from .core import LocalProblem, SharedVariable, PlanDelta, ProvenanceEvent, Attestation
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PrivacyBudget:
|
||||||
|
message_budget: int
|
||||||
|
disclosure_level: str = "selective"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PolicyBlock:
|
||||||
|
policy_id: str
|
||||||
|
version: int
|
||||||
|
flags: Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
class PortaBridge:
|
class PortaBridge:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def portaledger_to_catopt(self, event: ProvenanceEvent, problem: LocalProblem, vars: Dict[str, SharedVariable]) -> Dict[str, Any]:
|
def portaledger_to_catopt(self, event: ProvenanceEvent, problem: LocalProblem, vars: Dict[str, SharedVariable]) -> Dict[str, Any]:
|
||||||
# Map PortaLedger event to a CatOpt-like GoC primitive set (simplified)
|
# Map PortaLedger event to a CatOpt-like GoC primitive set.
|
||||||
|
plan_delta = PlanDelta(delta_id=event.contract_id, changes={"delta_hash": event.delta_hash}, timestamp=event.timestamp, delta_hash=event.delta_hash, attestation=event.attestation)
|
||||||
|
privacy_budget = PrivacyBudget(message_budget=max(0, len(vars)), disclosure_level="aggregate")
|
||||||
|
policy_block = PolicyBlock(policy_id=event.contract_id, version=1, flags={"policy_flags": event.policy_flags})
|
||||||
coc = {
|
coc = {
|
||||||
|
"graph_type": "GoC",
|
||||||
"LocalProblem": asdict(problem),
|
"LocalProblem": asdict(problem),
|
||||||
"SharedVariables": [asdict(v) for v in vars.values()],
|
"SharedVariables": [asdict(v) for v in vars.values()],
|
||||||
"PlanDelta": {
|
"PlanDelta": plan_delta.to_dict(),
|
||||||
"delta_id": event.contract_id,
|
"PolicyBlock": asdict(policy_block),
|
||||||
"delta_hash": event.delta_hash,
|
"PrivacyBudget": asdict(privacy_budget),
|
||||||
"timestamp": event.timestamp,
|
|
||||||
},
|
|
||||||
"AuditLog": {
|
"AuditLog": {
|
||||||
"event_id": event.event_id,
|
"event_id": event.event_id,
|
||||||
"actor": event.actor,
|
"actor": event.actor,
|
||||||
"policy_flags": event.policy_flags,
|
"policy_flags": event.policy_flags,
|
||||||
|
"attestation": event.attestation.to_dict(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return coc
|
return coc
|
||||||
|
|
||||||
|
def aggregate_shared_signal(self, variables: Dict[str, SharedVariable]) -> Dict[str, Any]:
|
||||||
|
numeric_values = [v.value for v in variables.values() if isinstance(v.value, (int, float))]
|
||||||
|
return {
|
||||||
|
"count": len(variables),
|
||||||
|
"numeric_count": len(numeric_values),
|
||||||
|
"numeric_sum": sum(numeric_values) if numeric_values else 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def bridge_to_catopt(event: ProvenanceEvent, problem: LocalProblem, vars: Dict[str, SharedVariable]) -> Dict[str, Any]:
|
def bridge_to_catopt(event: ProvenanceEvent, problem: LocalProblem, vars: Dict[str, SharedVariable]) -> Dict[str, Any]:
|
||||||
bridge = PortaBridge()
|
bridge = PortaBridge()
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,10 @@ import json
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass, asdict
|
from dataclasses import dataclass, asdict
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional, cast
|
||||||
|
|
||||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||||
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
||||||
from cryptography.hazmat.primitives import serialization
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -17,6 +18,10 @@ def _hash(data: Any) -> str:
|
||||||
return h.hexdigest()
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical_payload(data: Any) -> bytes:
|
||||||
|
return json.dumps(data, sort_keys=True, default=str, separators=(",", ":")).encode()
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LocalProblem:
|
class LocalProblem:
|
||||||
problem_id: str
|
problem_id: str
|
||||||
|
|
@ -31,19 +36,6 @@ class SharedVariable:
|
||||||
version: int = 0
|
version: int = 0
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PlanDelta:
|
|
||||||
delta_id: str
|
|
||||||
changes: Dict[str, Any]
|
|
||||||
timestamp: float
|
|
||||||
delta_hash: str = ""
|
|
||||||
attestation: Optional[bytes] = None
|
|
||||||
|
|
||||||
def compute_hash(self) -> str:
|
|
||||||
self.delta_hash = _hash({"delta_id": self.delta_id, "changes": self.changes, "timestamp": self.timestamp})
|
|
||||||
return self.delta_hash
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Attestation:
|
class Attestation:
|
||||||
signer_pubkey_pem: bytes
|
signer_pubkey_pem: bytes
|
||||||
|
|
@ -55,6 +47,33 @@ class Attestation:
|
||||||
"signature": self.signature.hex(),
|
"signature": self.signature.hex(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def verify(self, message: bytes) -> bool:
|
||||||
|
public_key = serialization.load_pem_public_key(self.signer_pubkey_pem)
|
||||||
|
try:
|
||||||
|
cast(Any, public_key).verify(self.signature, message)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlanDelta:
|
||||||
|
delta_id: str
|
||||||
|
changes: Dict[str, Any]
|
||||||
|
timestamp: float
|
||||||
|
delta_hash: str = ""
|
||||||
|
attestation: Optional[Attestation] = None
|
||||||
|
|
||||||
|
def compute_hash(self) -> str:
|
||||||
|
self.delta_hash = _hash({"delta_id": self.delta_id, "changes": self.changes, "timestamp": self.timestamp})
|
||||||
|
return self.delta_hash
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
payload = asdict(self)
|
||||||
|
if self.attestation is not None:
|
||||||
|
payload["attestation"] = self.attestation.to_dict()
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ProvenanceEvent:
|
class ProvenanceEvent:
|
||||||
|
|
@ -73,6 +92,12 @@ class ProvenanceEvent:
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ProvenanceRecord:
|
||||||
|
event: ProvenanceEvent
|
||||||
|
verified: bool
|
||||||
|
|
||||||
|
|
||||||
class PortaLedger:
|
class PortaLedger:
|
||||||
def __init__(self, db_path: str = ":memory:"):
|
def __init__(self, db_path: str = ":memory:"):
|
||||||
self.conn = sqlite3.connect(db_path)
|
self.conn = sqlite3.connect(db_path)
|
||||||
|
|
@ -90,11 +115,24 @@ class PortaLedger:
|
||||||
timestamp REAL,
|
timestamp REAL,
|
||||||
contract_id TEXT,
|
contract_id TEXT,
|
||||||
delta_hash TEXT,
|
delta_hash TEXT,
|
||||||
|
signer_pubkey_pem BLOB,
|
||||||
attestation BLOB,
|
attestation BLOB,
|
||||||
policy_flags TEXT
|
policy_flags TEXT
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_provenance_experiment
|
||||||
|
ON provenance_events (experiment_id, timestamp)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_provenance_contract
|
||||||
|
ON provenance_events (contract_id, timestamp)
|
||||||
|
"""
|
||||||
|
)
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
||||||
def sign_delta(self, delta: PlanDelta) -> PlanDelta:
|
def sign_delta(self, delta: PlanDelta) -> PlanDelta:
|
||||||
|
|
@ -109,10 +147,17 @@ class PortaLedger:
|
||||||
delta.attestation = Attestation(signer_pubkey_pem=pub, signature=signature)
|
delta.attestation = Attestation(signer_pubkey_pem=pub, signature=signature)
|
||||||
return delta
|
return delta
|
||||||
|
|
||||||
|
def verify_delta(self, delta: PlanDelta) -> bool:
|
||||||
|
if not delta.delta_hash:
|
||||||
|
delta.compute_hash()
|
||||||
|
if not delta.attestation:
|
||||||
|
return False
|
||||||
|
return delta.attestation.verify(delta.delta_hash.encode())
|
||||||
|
|
||||||
def log_provenance(self, event: ProvenanceEvent) -> None:
|
def log_provenance(self, event: ProvenanceEvent) -> None:
|
||||||
cur = self.conn.cursor()
|
cur = self.conn.cursor()
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"INSERT OR REPLACE INTO provenance_events (event_id, experiment_id, actor, timestamp, contract_id, delta_hash, attestation, policy_flags) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
"INSERT OR REPLACE INTO provenance_events (event_id, experiment_id, actor, timestamp, contract_id, delta_hash, signer_pubkey_pem, attestation, policy_flags) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
(
|
(
|
||||||
event.event_id,
|
event.event_id,
|
||||||
event.experiment_id,
|
event.experiment_id,
|
||||||
|
|
@ -120,20 +165,86 @@ class PortaLedger:
|
||||||
event.timestamp,
|
event.timestamp,
|
||||||
event.contract_id,
|
event.contract_id,
|
||||||
event.delta_hash,
|
event.delta_hash,
|
||||||
|
event.attestation.signer_pubkey_pem if event.attestation else None,
|
||||||
event.attestation.signature if event.attestation else None,
|
event.attestation.signature if event.attestation else None,
|
||||||
json.dumps(event.policy_flags),
|
json.dumps(event.policy_flags),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
||||||
|
def record_event(self, experiment_id: str, actor: str, contract_id: str, delta: PlanDelta, policy_flags: Optional[List[str]] = None) -> ProvenanceEvent:
|
||||||
|
signed = self.sign_delta(delta)
|
||||||
|
attestation = signed.attestation
|
||||||
|
assert attestation is not None
|
||||||
|
event = ProvenanceEvent(
|
||||||
|
event_id=_hash({"experiment_id": experiment_id, "contract_id": contract_id, "delta_hash": signed.delta_hash, "ts": time.time()}),
|
||||||
|
experiment_id=experiment_id,
|
||||||
|
actor=actor,
|
||||||
|
timestamp=time.time(),
|
||||||
|
contract_id=contract_id,
|
||||||
|
delta_hash=signed.delta_hash,
|
||||||
|
attestation=attestation,
|
||||||
|
policy_flags=policy_flags or [],
|
||||||
|
)
|
||||||
|
self.log_provenance(event)
|
||||||
|
return event
|
||||||
|
|
||||||
def fetch_event(self, event_id: str) -> Optional[ProvenanceEvent]:
|
def fetch_event(self, event_id: str) -> Optional[ProvenanceEvent]:
|
||||||
cur = self.conn.cursor()
|
cur = self.conn.cursor()
|
||||||
cur.execute("SELECT event_id, experiment_id, actor, timestamp, contract_id, delta_hash, attestation, policy_flags FROM provenance_events WHERE event_id = ?", (event_id,))
|
cur.execute("SELECT event_id, experiment_id, actor, timestamp, contract_id, delta_hash, signer_pubkey_pem, attestation, policy_flags FROM provenance_events WHERE event_id = ?", (event_id,))
|
||||||
row = cur.fetchone()
|
row = cur.fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
return None
|
return None
|
||||||
# Minimal reconstruction; attestation storage is simplified
|
attestation = Attestation(signer_pubkey_pem=row[6] or b"", signature=row[7] or b"")
|
||||||
delta_hash = row[5]
|
event = ProvenanceEvent(event_id=row[0], experiment_id=row[1], actor=row[2], timestamp=row[3], contract_id=row[4], delta_hash=row[5], attestation=attestation, policy_flags=json.loads(row[8]))
|
||||||
attestation = Attestation(signer_pubkey_pem=b"", signature=row[6] or b"")
|
|
||||||
event = ProvenanceEvent(event_id=row[0], experiment_id=row[1], actor=row[2], timestamp=row[3], contract_id=row[4], delta_hash=delta_hash, attestation=attestation, policy_flags=json.loads(row[7]))
|
|
||||||
return event
|
return event
|
||||||
|
|
||||||
|
def query_events(self, experiment_id: Optional[str] = None, actor: Optional[str] = None, contract_id: Optional[str] = None) -> List[ProvenanceEvent]:
|
||||||
|
clauses = []
|
||||||
|
params: List[Any] = []
|
||||||
|
if experiment_id is not None:
|
||||||
|
clauses.append("experiment_id = ?")
|
||||||
|
params.append(experiment_id)
|
||||||
|
if actor is not None:
|
||||||
|
clauses.append("actor = ?")
|
||||||
|
params.append(actor)
|
||||||
|
if contract_id is not None:
|
||||||
|
clauses.append("contract_id = ?")
|
||||||
|
params.append(contract_id)
|
||||||
|
query = "SELECT event_id, experiment_id, actor, timestamp, contract_id, delta_hash, signer_pubkey_pem, attestation, policy_flags FROM provenance_events"
|
||||||
|
if clauses:
|
||||||
|
query += " WHERE " + " AND ".join(clauses)
|
||||||
|
query += " ORDER BY timestamp ASC"
|
||||||
|
cur = self.conn.cursor()
|
||||||
|
cur.execute(query, tuple(params))
|
||||||
|
rows = cur.fetchall()
|
||||||
|
events: List[ProvenanceEvent] = []
|
||||||
|
for row in rows:
|
||||||
|
events.append(
|
||||||
|
ProvenanceEvent(
|
||||||
|
event_id=row[0],
|
||||||
|
experiment_id=row[1],
|
||||||
|
actor=row[2],
|
||||||
|
timestamp=row[3],
|
||||||
|
contract_id=row[4],
|
||||||
|
delta_hash=row[5],
|
||||||
|
attestation=Attestation(signer_pubkey_pem=row[6] or b"", signature=row[7] or b""),
|
||||||
|
policy_flags=json.loads(row[8]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return events
|
||||||
|
|
||||||
|
def verify_event(self, event_id: str) -> bool:
|
||||||
|
event = self.fetch_event(event_id)
|
||||||
|
if event is None:
|
||||||
|
return False
|
||||||
|
if not event.attestation.signer_pubkey_pem or not event.attestation.signature:
|
||||||
|
return False
|
||||||
|
return event.attestation.verify(event.delta_hash.encode())
|
||||||
|
|
||||||
|
def aggregate_privacy_budget(self, events: List[ProvenanceEvent]) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"event_count": len(events),
|
||||||
|
"unique_actors": sorted({event.actor for event in events}),
|
||||||
|
"delta_hashes": [event.delta_hash for event in events],
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,13 @@ from __future__ import annotations
|
||||||
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import json
|
import json
|
||||||
from typing import List
|
import time
|
||||||
|
import hashlib
|
||||||
|
from typing import Any, List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
def _policy_hash(payload: Any) -> str:
|
||||||
|
return hashlib.sha256(json.dumps(payload, sort_keys=True, default=str).encode()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
class GovernanceLedger:
|
class GovernanceLedger:
|
||||||
|
|
@ -19,26 +25,71 @@ class GovernanceLedger:
|
||||||
timestamp REAL,
|
timestamp REAL,
|
||||||
actor TEXT,
|
actor TEXT,
|
||||||
policy_id TEXT,
|
policy_id TEXT,
|
||||||
|
policy_version INTEGER,
|
||||||
decision TEXT,
|
decision TEXT,
|
||||||
details TEXT
|
details TEXT,
|
||||||
|
entry_hash TEXT
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS policy_registry (
|
||||||
|
policy_id TEXT,
|
||||||
|
version INTEGER,
|
||||||
|
actor TEXT,
|
||||||
|
payload TEXT,
|
||||||
|
payload_hash TEXT,
|
||||||
|
timestamp REAL,
|
||||||
|
PRIMARY KEY (policy_id, version)
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
||||||
def record_decision(self, actor: str, policy_id: str, decision: str, details: dict) -> None:
|
def register_policy(self, actor: str, policy_id: str, payload: dict) -> int:
|
||||||
cur = self.conn.cursor()
|
cur = self.conn.cursor()
|
||||||
|
cur.execute("SELECT COALESCE(MAX(version), 0) FROM policy_registry WHERE policy_id = ?", (policy_id,))
|
||||||
|
version = int(cur.fetchone()[0]) + 1
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"INSERT INTO governance_logs (timestamp, actor, policy_id, decision, details) VALUES (?, ?, ?, ?, ?)",
|
"INSERT INTO policy_registry (policy_id, version, actor, payload, payload_hash, timestamp) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
(time.time(), actor, policy_id, decision, json.dumps(details)),
|
(policy_id, version, actor, json.dumps(payload, sort_keys=True), _policy_hash(payload), time.time()),
|
||||||
|
)
|
||||||
|
self.conn.commit()
|
||||||
|
return version
|
||||||
|
|
||||||
|
def record_decision(self, actor: str, policy_id: str, decision: str, details: dict, policy_version: Optional[int] = None) -> None:
|
||||||
|
cur = self.conn.cursor()
|
||||||
|
entry_hash = _policy_hash({"actor": actor, "policy_id": policy_id, "decision": decision, "details": details, "policy_version": policy_version, "timestamp": time.time()})
|
||||||
|
cur.execute(
|
||||||
|
"INSERT INTO governance_logs (timestamp, actor, policy_id, policy_version, decision, details, entry_hash) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
(time.time(), actor, policy_id, policy_version, decision, json.dumps(details, sort_keys=True), entry_hash),
|
||||||
)
|
)
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
||||||
def get_logs(self) -> List[dict]:
|
def get_logs(self) -> List[dict]:
|
||||||
cur = self.conn.cursor()
|
cur = self.conn.cursor()
|
||||||
cur.execute("SELECT id, timestamp, actor, policy_id, decision, details FROM governance_logs ORDER BY timestamp DESC")
|
cur.execute("SELECT id, timestamp, actor, policy_id, policy_version, decision, details, entry_hash FROM governance_logs ORDER BY timestamp DESC")
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
return [
|
return [
|
||||||
{"id": r[0], "timestamp": r[1], "actor": r[2], "policy_id": r[3], "decision": r[4], "details": json.loads(r[5])}
|
{"id": r[0], "timestamp": r[1], "actor": r[2], "policy_id": r[3], "policy_version": r[4], "decision": r[5], "details": json.loads(r[6]), "entry_hash": r[7]}
|
||||||
for r in rows
|
for r in rows
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def latest_policy(self, policy_id: str) -> Optional[dict]:
|
||||||
|
cur = self.conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"SELECT policy_id, version, actor, payload, payload_hash, timestamp FROM policy_registry WHERE policy_id = ? ORDER BY version DESC LIMIT 1",
|
||||||
|
(policy_id,),
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
if row is None:
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"policy_id": row[0],
|
||||||
|
"version": row[1],
|
||||||
|
"actor": row[2],
|
||||||
|
"payload": json.loads(row[3]),
|
||||||
|
"payload_hash": row[4],
|
||||||
|
"timestamp": row[5],
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,16 @@ import time
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
|
||||||
from portaledger import LocalProblem, PortaLedger, PlanDelta, ProvenanceEvent, Attestation, PortaBridge, bridge_to_catopt
|
from portaledger import (
|
||||||
from portaledger.governance import GovernanceLedger
|
LocalProblem,
|
||||||
|
PortaLedger,
|
||||||
|
PlanDelta,
|
||||||
|
ProvenanceEvent,
|
||||||
|
Attestation,
|
||||||
|
PortaBridge,
|
||||||
|
bridge_to_catopt,
|
||||||
|
GovernanceLedger,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_sign_and_log_provenance_roundtrip():
|
def test_sign_and_log_provenance_roundtrip():
|
||||||
|
|
@ -12,6 +20,8 @@ def test_sign_and_log_provenance_roundtrip():
|
||||||
lp = LocalProblem(problem_id="p1", assets=["AAPL"], objectives=["maximize return"])
|
lp = LocalProblem(problem_id="p1", assets=["AAPL"], objectives=["maximize return"])
|
||||||
delta = PlanDelta(delta_id="d1", changes={"AAPL": {"hold": 10}}, timestamp=time.time())
|
delta = PlanDelta(delta_id="d1", changes={"AAPL": {"hold": 10}}, timestamp=time.time())
|
||||||
signed = ledger.sign_delta(delta)
|
signed = ledger.sign_delta(delta)
|
||||||
|
assert ledger.verify_delta(signed)
|
||||||
|
assert signed.attestation is not None
|
||||||
event = ProvenanceEvent(
|
event = ProvenanceEvent(
|
||||||
event_id="ev1",
|
event_id="ev1",
|
||||||
experiment_id="exp1",
|
experiment_id="exp1",
|
||||||
|
|
@ -26,6 +36,8 @@ def test_sign_and_log_provenance_roundtrip():
|
||||||
fetched = ledger.fetch_event("ev1")
|
fetched = ledger.fetch_event("ev1")
|
||||||
assert fetched is not None
|
assert fetched is not None
|
||||||
assert fetched.event_id == "ev1"
|
assert fetched.event_id == "ev1"
|
||||||
|
assert ledger.verify_event("ev1")
|
||||||
|
assert ledger.query_events(experiment_id="exp1")[0].event_id == "ev1"
|
||||||
|
|
||||||
|
|
||||||
def test_bridge_conversion():
|
def test_bridge_conversion():
|
||||||
|
|
@ -33,6 +45,7 @@ def test_bridge_conversion():
|
||||||
lp = LocalProblem(problem_id="p1", assets=["BTC"], objectives=["minimize risk"])
|
lp = LocalProblem(problem_id="p1", assets=["BTC"], objectives=["minimize risk"])
|
||||||
delta = PlanDelta(delta_id="d1", changes={"BTC": {"alloc": 5}}, timestamp=time.time())
|
delta = PlanDelta(delta_id="d1", changes={"BTC": {"alloc": 5}}, timestamp=time.time())
|
||||||
signed = ledger.sign_delta(delta)
|
signed = ledger.sign_delta(delta)
|
||||||
|
assert signed.attestation is not None
|
||||||
event = ProvenanceEvent(
|
event = ProvenanceEvent(
|
||||||
event_id="ev2",
|
event_id="ev2",
|
||||||
experiment_id="exp2",
|
experiment_id="exp2",
|
||||||
|
|
@ -47,3 +60,17 @@ def test_bridge_conversion():
|
||||||
composite = bridge_to_catopt(event, lp, {})
|
composite = bridge_to_catopt(event, lp, {})
|
||||||
assert isinstance(composite, dict)
|
assert isinstance(composite, dict)
|
||||||
assert "LocalProblem" in composite
|
assert "LocalProblem" in composite
|
||||||
|
assert composite["graph_type"] == "GoC"
|
||||||
|
assert "PrivacyBudget" in composite
|
||||||
|
assert "PolicyBlock" in composite
|
||||||
|
|
||||||
|
|
||||||
|
def test_governance_versioning():
|
||||||
|
gov = GovernanceLedger()
|
||||||
|
version = gov.register_policy("tester", "policy-1", {"allow": True})
|
||||||
|
gov.record_decision("tester", "policy-1", "approve", {"reason": "ok"}, policy_version=version)
|
||||||
|
logs = gov.get_logs()
|
||||||
|
assert logs[0]["policy_version"] == version
|
||||||
|
latest = gov.latest_policy("policy-1")
|
||||||
|
assert latest is not None
|
||||||
|
assert latest["version"] == version
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue