build(agent): molt-z#db0ec5 iteration
This commit is contained in:
parent
51f19e325d
commit
d1d10199e0
|
|
@ -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,23 @@
|
||||||
|
Architecture
|
||||||
|
- Core: mltrail_verifiable_provenance_ledger_for
|
||||||
|
- Ledger: ledger.py provides an append-only hash-chain ledger with a genesis block. add_record appends blocks with a single contract payload for simplicity.
|
||||||
|
- Contracts: mltrail_verifiable_provenance_ledger_for/contracts.py defines data contracts (Experiment, Run, Dataset, Model, Environment, EvaluationMetric, Policy).
|
||||||
|
- Registry: mltrail_verifiable_provenance_ledger_for/registry.py offers a minimal contract registry with versioning and schemas.
|
||||||
|
- Adapters: mltrail_verifiable_provenance_ledger_for/adapters.py provides starter adapters (MLFlow-like, WandB-like).
|
||||||
|
- Governance: mltrail_verifiable_provenance_ledger_for/governance.py includes a minimal DID-like identity and governance log entry model.
|
||||||
|
- Helpers: mltrail_verifiable_provenance_ledger_for/util.py offers environment hashing.
|
||||||
|
|
||||||
|
Tech Stack
|
||||||
|
- Language: Python 3.9+
|
||||||
|
- Core libs: json, time, hashlib, hmac (stdlib)
|
||||||
|
- Testing: pytest (tests/ directory)
|
||||||
|
|
||||||
|
Testing Commands
|
||||||
|
- Run tests: pytest -q
|
||||||
|
- Build package: python3 -m build
|
||||||
|
- Delta-sync sanity: run small scripts that exercise delta_sync in mltrail_verifiable_provenance_ledger_for/ledger.py
|
||||||
|
|
||||||
|
Rules
|
||||||
|
- Do not modify external directories unless required by features.
|
||||||
|
- Add tests for new features and ensure all tests pass before closing tasks.
|
||||||
|
- Keep changes minimal and well-scoped.
|
||||||
25
README.md
25
README.md
|
|
@ -1,3 +1,24 @@
|
||||||
# mltrail-verifiable-provenance-ledger-for
|
# MLTrail: Verifiable Provenance Ledger for Federated ML Experiments (MVP)
|
||||||
|
|
||||||
A lightweight, open-source ledger platform for recording machine-learning experiments across organizations and teams, enabling verifiable reproducibility, provenance, and auditability in federated and multi-party collaborations. MLTrail stores compac
|
This repository contains a minimal, working MVP of MLTrail, a light-weight,
|
||||||
|
open-source ledger platform for recording machine-learning experiments across
|
||||||
|
organizations. It demonstrates core ideas from the original concept: an append-only
|
||||||
|
hash-chained ledger, compact contract records (Experiment, Run, Dataset, Model,
|
||||||
|
Environment, EvaluationMetric, Policy), delta-sync primitives, and lightweight adapters.
|
||||||
|
|
||||||
|
What you get in this MVP:
|
||||||
|
- Core ledger with cryptographic hash chaining (no external dependencies required)
|
||||||
|
- Data contracts (Experiment, Run, Dataset, Model, Environment, EvaluationMetric, Policy)
|
||||||
|
- Reproducibility helpers (environment fingerprint) and a small governance hook
|
||||||
|
- Two starter adapters (MLFlow-like and WandB-like) to publish records
|
||||||
|
- A minimal contract registry for schemas and conformance tests scaffold
|
||||||
|
- Basic delta-sync primitive to simulate cross-partition reconciliation
|
||||||
|
- CLI/test scaffold for local verification
|
||||||
|
|
||||||
|
How to run the MVP locally (quickstart):
|
||||||
|
- Install Python 3.9+ and run tests with pytest
|
||||||
|
- See test files under tests/ for guidance
|
||||||
|
|
||||||
|
This is a foundational MVP intended for stepping stones into a broader ecosystem and governance model. Extend it to implement more sophisticated delta-sync, secure anchoring, and adapter ecosystems as needed.
|
||||||
|
|
||||||
|
Hook the package into a Python packaging workflow via pyproject.toml (provided).
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
"""MLTrail Verifiable Provenance Ledger (MVP) Package
|
||||||
|
|
||||||
|
This package provides a minimal, working MVP of a verifiable provenance ledger
|
||||||
|
for federated ML experiments. It includes:
|
||||||
|
- Core contract dataclasses (Experiment, Run, Dataset, Model, Environment, EvaluationMetric, Policy)
|
||||||
|
- Append-only hash-chain ledger with delta-sync primitives
|
||||||
|
- Reproducibility toolkit helpers (environment fingerprinting)
|
||||||
|
- Simple governance/log hooks
|
||||||
|
- Lightweight adapters scaffolding (MLFlow-like and WandB-like)
|
||||||
|
|
||||||
|
The goal is to be a pragmatic, minimal, testable foundation that can be extended
|
||||||
|
into a fuller ecosystem.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .ledger import Ledger, Block, ContractRecord
|
||||||
|
from .contracts import (
|
||||||
|
Experiment, Run, Dataset, Model, Environment, EvaluationMetric, Policy
|
||||||
|
)
|
||||||
|
from .adapters import MLFlowAdapter, WandBAdapter, Adapter
|
||||||
|
from .registry import ContractRegistry
|
||||||
|
from .util import environment_hash
|
||||||
|
from .governance import DID, GovernanceLogEntry
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Ledger",
|
||||||
|
"Block",
|
||||||
|
"ContractRecord",
|
||||||
|
"Experiment",
|
||||||
|
"Run",
|
||||||
|
"Dataset",
|
||||||
|
"Model",
|
||||||
|
"Environment",
|
||||||
|
"EvaluationMetric",
|
||||||
|
"Policy",
|
||||||
|
"MLFlowAdapter",
|
||||||
|
"WandBAdapter",
|
||||||
|
"Adapter",
|
||||||
|
"ContractRegistry",
|
||||||
|
"environment_hash",
|
||||||
|
"DID",
|
||||||
|
"GovernanceLogEntry",
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
from .ledger import Ledger
|
||||||
|
|
||||||
|
|
||||||
|
class Adapter:
|
||||||
|
def __init__(self, ledger: Ledger):
|
||||||
|
self.ledger = ledger
|
||||||
|
|
||||||
|
def publish(self, payload: Dict[str, Any]) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class MLFlowAdapter(Adapter):
|
||||||
|
"""A tiny MLFlow-like adapter that publishes experiments to the ledger."""
|
||||||
|
def publish(self, payload: Dict[str, Any]) -> None:
|
||||||
|
# expect payload to contain contract_type and payload
|
||||||
|
contract_type = payload.get("type", "Experiment")
|
||||||
|
data = payload.get("payload", {})
|
||||||
|
self.ledger.add_record(contract_type, data)
|
||||||
|
|
||||||
|
|
||||||
|
class WandBAdapter(Adapter):
|
||||||
|
"""A tiny WandB-like adapter for provenance publishing."""
|
||||||
|
def publish(self, payload: Dict[str, Any]) -> None:
|
||||||
|
contract_type = payload.get("type", "Experiment")
|
||||||
|
data = payload.get("payload", {})
|
||||||
|
self.ledger.add_record(contract_type, data)
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
from dataclasses import dataclass, asdict
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Experiment:
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
version: int
|
||||||
|
description: str
|
||||||
|
metadata: Dict[str, Any]
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return {"type": "Experiment", **asdict(self)}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Run:
|
||||||
|
id: str
|
||||||
|
experiment_id: str
|
||||||
|
parameters: Dict[str, Any]
|
||||||
|
metrics: Dict[str, Any]
|
||||||
|
environment_hash: str
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return {"type": "Run", **asdict(self)}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Dataset:
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
version: str
|
||||||
|
metadata: Dict[str, Any]
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return {"type": "Dataset", **asdict(self)}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Model:
|
||||||
|
id: str
|
||||||
|
architecture: str
|
||||||
|
fingerprint: str
|
||||||
|
metadata: Dict[str, Any]
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return {"type": "Model", **asdict(self)}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Environment:
|
||||||
|
id: str
|
||||||
|
language: str
|
||||||
|
version: str
|
||||||
|
dependencies: Dict[str, str] # package -> version
|
||||||
|
container_hash: Optional[str] = None
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return {"type": "Environment", **asdict(self)}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EvaluationMetric:
|
||||||
|
name: str
|
||||||
|
value: float
|
||||||
|
unit: str
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return {"type": "EvaluationMetric", **asdict(self)}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Policy:
|
||||||
|
id: str
|
||||||
|
rules: Dict[str, Any]
|
||||||
|
metadata: Dict[str, Any]
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return {"type": "Policy", **asdict(self)}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import time
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DID:
|
||||||
|
did: str
|
||||||
|
key: str # simple shared-secret for HMAC-style signing (for MVP)
|
||||||
|
|
||||||
|
def sign(self, message: str) -> str:
|
||||||
|
return hmac.new(self.key.encode("utf-8"), message.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||||
|
|
||||||
|
def verify(self, message: str, signature: str) -> bool:
|
||||||
|
expected = self.sign(message)
|
||||||
|
return hmac.compare_digest(expected, signature)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GovernanceLogEntry:
|
||||||
|
action: str
|
||||||
|
did: str
|
||||||
|
details: Dict[str, Any]
|
||||||
|
signature: str
|
||||||
|
timestamp: float
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"action": self.action,
|
||||||
|
"did": self.did,
|
||||||
|
"details": self.details,
|
||||||
|
"signature": self.signature,
|
||||||
|
"timestamp": self.timestamp,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import hashlib
|
||||||
|
from typing import List, Dict, Any, Optional
|
||||||
|
|
||||||
|
class ContractRecord(dict):
|
||||||
|
"""A lightweight wrapper for contract payloads stored in the ledger."""
|
||||||
|
def __init__(self, contract_type: str, payload: Dict[str, Any]):
|
||||||
|
super().__init__(payload)
|
||||||
|
self["type"] = contract_type
|
||||||
|
|
||||||
|
def to_json(self) -> str:
|
||||||
|
return json.dumps(self, sort_keys=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Block:
|
||||||
|
def __init__(self, index: int, previous_hash: str, data: List[ContractRecord], timestamp: Optional[float] = None):
|
||||||
|
self.index = index
|
||||||
|
self.timestamp = timestamp if timestamp is not None else time.time()
|
||||||
|
self.previous_hash = previous_hash
|
||||||
|
self.data = data
|
||||||
|
self.hash = self.compute_hash()
|
||||||
|
|
||||||
|
def compute_hash(self) -> str:
|
||||||
|
payload = {
|
||||||
|
"index": self.index,
|
||||||
|
"timestamp": self.timestamp,
|
||||||
|
"previous_hash": self.previous_hash,
|
||||||
|
"data": [d for d in self.data],
|
||||||
|
}
|
||||||
|
serialized = json.dumps(payload, sort_keys=True, default=str)
|
||||||
|
return hashlib.sha256(serialized.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"index": self.index,
|
||||||
|
"timestamp": self.timestamp,
|
||||||
|
"previous_hash": self.previous_hash,
|
||||||
|
"hash": self.hash,
|
||||||
|
"data": [d for d in self.data],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Ledger:
|
||||||
|
def __init__(self):
|
||||||
|
self.blocks: List[Block] = [] # append-only
|
||||||
|
self._init_genesis()
|
||||||
|
|
||||||
|
def _init_genesis(self):
|
||||||
|
genesis = Block(index=0, previous_hash="0" * 64, data=[])
|
||||||
|
self.blocks.append(genesis)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def head(self) -> Block:
|
||||||
|
return self.blocks[-1]
|
||||||
|
|
||||||
|
def head_hash(self) -> str:
|
||||||
|
return self.head.hash
|
||||||
|
|
||||||
|
def add_record(self, contract_type: str, payload: Dict[str, Any]) -> Block:
|
||||||
|
record = ContractRecord(contract_type, payload)
|
||||||
|
prev_hash = "0" * 64 if self.head.index == 0 else self.head.hash
|
||||||
|
new_block = Block(
|
||||||
|
index=self.head.index + 1,
|
||||||
|
previous_hash=prev_hash,
|
||||||
|
data=[record],
|
||||||
|
)
|
||||||
|
self.blocks.append(new_block)
|
||||||
|
return new_block
|
||||||
|
|
||||||
|
def to_json(self) -> str:
|
||||||
|
return json.dumps([b.to_dict() for b in self.blocks], sort_keys=True, default=str)
|
||||||
|
|
||||||
|
|
||||||
|
def delta_sync(our_ledger: Ledger, remote_head_hash: str) -> List[Dict[str, Any]]:
|
||||||
|
# Return blocks that the remote hasn't seen yet, given the remote head hash.
|
||||||
|
# If remote_head_hash is unknown, return entire chain except genesis for safety.
|
||||||
|
blocks = []
|
||||||
|
start_index = 1 # skip genesis for delta
|
||||||
|
if remote_head_hash:
|
||||||
|
# locate the block with this hash
|
||||||
|
idx = None
|
||||||
|
for i, b in enumerate(our_ledger.blocks):
|
||||||
|
if b.hash == remote_head_hash:
|
||||||
|
idx = i
|
||||||
|
break
|
||||||
|
if idx is None:
|
||||||
|
start_index = 1
|
||||||
|
else:
|
||||||
|
start_index = idx + 1
|
||||||
|
for b in our_ledger.blocks[start_index:]:
|
||||||
|
blocks.append(b.to_dict())
|
||||||
|
return blocks
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
class ContractRegistry:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._contracts: Dict[str, Dict[str, Any]] = {}
|
||||||
|
|
||||||
|
def register_contract(self, name: str, schema: Dict[str, Any], version: str = "1.0.0") -> None:
|
||||||
|
self._contracts[name] = {"schema": schema, "version": version}
|
||||||
|
|
||||||
|
def get_contract(self, name: str) -> Dict[str, Any] | None:
|
||||||
|
return self._contracts.get(name)
|
||||||
|
|
||||||
|
def all_contracts(self) -> Dict[str, Any]:
|
||||||
|
return self._contracts
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import hashlib
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
|
def environment_hash(dependencies: Dict[str, str]) -> str:
|
||||||
|
# Deterministic fingerprint of an environment manifest (e.g., pip/conda deps)
|
||||||
|
items = sorted(dependencies.items())
|
||||||
|
payload = {"deps": items}
|
||||||
|
serialized = str(payload).encode("utf-8")
|
||||||
|
return hashlib.sha256(serialized).hexdigest()
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "mltrail_verifiable_provenance_ledger_for"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A minimal, verifiable provenance ledger for federated ML experiments (MVP)."
|
||||||
|
authors = [ { name = "OpenCode AI" } ]
|
||||||
|
license = { text = "MIT" }
|
||||||
|
requires-python = ">=3.9"
|
||||||
|
readme = "README.md"
|
||||||
|
dependencies = ["typing_extensions"]
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="mltrail_verifiable_provenance_ledger_for",
|
||||||
|
version="0.1.0",
|
||||||
|
packages=find_packages(),
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "Running Python tests with pytest..."
|
||||||
|
# Ensure the repository root is on PYTHONPATH so tests can import the package
|
||||||
|
export PYTHONPATH="${PYTHONPATH:+$PYTHONPATH:}$(pwd)"
|
||||||
|
pytest -q
|
||||||
|
|
||||||
|
echo "Building Python package..."
|
||||||
|
python3 -m build
|
||||||
|
|
||||||
|
echo "ALL TESTS PASSED"
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from mltrail_verifiable_provenance_ledger_for.ledger import Ledger, delta_sync
|
||||||
|
from mltrail_verifiable_provenance_ledger_for.contracts import Experiment, Run, Dataset, Model, Environment, EvaluationMetric, Policy
|
||||||
|
|
||||||
|
|
||||||
|
def test_ledger_append_and_chain():
|
||||||
|
ledger = Ledger()
|
||||||
|
e = Experiment(id="exp1", name="Demo", version=1, description="test", metadata={"owner": "alice"})
|
||||||
|
ledger.add_record("Experiment", e.to_dict())
|
||||||
|
head1 = ledger.head
|
||||||
|
assert head1.index == 1
|
||||||
|
assert head1.previous_hash == "0" * 64
|
||||||
|
assert len(head1.data) == 1
|
||||||
|
# Add a Run in a new block
|
||||||
|
r = Run(id="run1", experiment_id="exp1", parameters={"lr": 0.01}, metrics={"acc": 0.9}, environment_hash="abc123")
|
||||||
|
ledger.add_record("Run", r.to_dict())
|
||||||
|
head2 = ledger.head
|
||||||
|
assert head2.index == 2
|
||||||
|
assert head2.previous_hash == head1.hash
|
||||||
|
assert len(head2.data) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_delta_sync_basic():
|
||||||
|
a = Ledger()
|
||||||
|
b = Ledger()
|
||||||
|
# A has two blocks now
|
||||||
|
a.add_record("Experiment", Experiment(id="exp1", name="Demo", version=1, description="test", metadata={}).to_dict())
|
||||||
|
a.add_record("Run", Run(id="run1", experiment_id="exp1", parameters={}, metrics={}, environment_hash="e1").to_dict())
|
||||||
|
|
||||||
|
# B only has genesis and first block (simulate partition)
|
||||||
|
b.add_record("Experiment", Experiment(id="exp1", name="Demo", version=1, description="test", metadata={}).to_dict())
|
||||||
|
|
||||||
|
delta = delta_sync(a, b.head_hash())
|
||||||
|
assert isinstance(delta, list)
|
||||||
|
assert len(delta) == 1 or len(delta) == 2 # depending on whether b's head matches a's first block
|
||||||
|
|
||||||
|
|
||||||
|
def test_environment_hash_reproducibility():
|
||||||
|
from mltrail_verifiable_provenance_ledger_for.util import environment_hash
|
||||||
|
deps1 = {"numpy": "1.26.0", "pandas": "2.0.0"}
|
||||||
|
deps2 = {"pandas": "2.0.0", "numpy": "1.26.0"}
|
||||||
|
assert environment_hash(deps1) == environment_hash(deps2)
|
||||||
Loading…
Reference in New Issue