build(agent): melter#14fd4b iteration
This commit is contained in:
parent
b9b2d9cc64
commit
e3d7dc42da
|
|
@ -0,0 +1,62 @@
|
||||||
|
import time
|
||||||
|
import hashlib
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
from .reprobundle import ReproBundle
|
||||||
|
|
||||||
|
|
||||||
|
class ReproRunner:
|
||||||
|
"""Lightweight deterministic runner that verifies a ReproBundle and
|
||||||
|
emits a signed run transcript.
|
||||||
|
|
||||||
|
This is intentionally minimal: it does not execute arbitrary code. Instead
|
||||||
|
it validates the bundle's integrity (merkle root) and produces a reproducible
|
||||||
|
run transcript digest that can be recorded in the ledger or used for
|
||||||
|
conformance testing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, signing_key: str = ""):
|
||||||
|
# signing_key is optional and only used for deterministic mock signatures
|
||||||
|
self.signing_key = signing_key
|
||||||
|
|
||||||
|
def verify_bundle(self, bundle: ReproBundle) -> bool:
|
||||||
|
# Recompute merkle root and compare to provided one if present
|
||||||
|
data = bundle.to_dict()
|
||||||
|
provided = data.get("merkle_root")
|
||||||
|
computed = bundle.merkle_root()
|
||||||
|
return provided == computed
|
||||||
|
|
||||||
|
def run(self, bundle: ReproBundle) -> Dict[str, Any]:
|
||||||
|
"""Validate the bundle and emit a signed transcript.
|
||||||
|
|
||||||
|
The transcript contains:
|
||||||
|
- merkle_root: bundle root
|
||||||
|
- timestamp: run time
|
||||||
|
- manifest_digest: sha256 of the run_manifest component
|
||||||
|
- signature: deterministic signature over the transcript fields
|
||||||
|
"""
|
||||||
|
ok = self.verify_bundle(bundle)
|
||||||
|
if not ok:
|
||||||
|
raise ValueError("ReproBundle failed integrity check")
|
||||||
|
|
||||||
|
components = bundle.components
|
||||||
|
manifest = components.get("run_manifest", {})
|
||||||
|
# canonical digest for run manifest
|
||||||
|
manifest_ser = str(manifest).encode("utf-8")
|
||||||
|
manifest_digest = hashlib.sha256(manifest_ser).hexdigest()
|
||||||
|
|
||||||
|
merkle = bundle.merkle_root()
|
||||||
|
ts = int(time.time())
|
||||||
|
|
||||||
|
payload = f"{merkle}|{manifest_digest}|{ts}"
|
||||||
|
# deterministic mock signature using signing_key
|
||||||
|
sig_source = (self.signing_key + payload).encode("utf-8")
|
||||||
|
signature = hashlib.sha256(sig_source).hexdigest()
|
||||||
|
|
||||||
|
transcript = {
|
||||||
|
"merkle_root": merkle,
|
||||||
|
"timestamp": ts,
|
||||||
|
"manifest_digest": manifest_digest,
|
||||||
|
"signature": signature,
|
||||||
|
}
|
||||||
|
return transcript
|
||||||
|
|
@ -1,43 +1,37 @@
|
||||||
from dataclasses import dataclass, asdict
|
|
||||||
from typing import List, Dict, Any, Optional
|
|
||||||
import json
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import json
|
||||||
from .contracts import Environment
|
from typing import Dict, Any, List
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ReproBundle:
|
class ReproBundle:
|
||||||
"""A compact, content-addressed snapshot describing everything needed to
|
"""Compact, content-addressed bundle for reproducible runs.
|
||||||
deterministically replay a run (code commit, environment, dataset refs,
|
|
||||||
model ref, and a minimal run manifest).
|
|
||||||
|
|
||||||
This is intentionally small and deterministic so it can be cheaply
|
The bundle is a mapping of named components (code_commit, environment,
|
||||||
exchanged between peers and used to compute Merkle-like bundle hashes.
|
datasets, model_refs, run_manifest). We compute a deterministic Merkle-like
|
||||||
|
root by hashing each component's canonical JSON (sorted keys) and then
|
||||||
|
hashing the concatenation of component hashes in a stable order.
|
||||||
"""
|
"""
|
||||||
code_commit: str
|
|
||||||
environment: Environment
|
def __init__(self, components: Dict[str, Any]):
|
||||||
dataset_refs: List[Dict[str, Any]]
|
# Normalize components to dict of JSON-serializable structures
|
||||||
model_ref: Dict[str, Any]
|
self.components = components
|
||||||
run_manifest: Dict[str, Any]
|
|
||||||
container_hash: Optional[str] = None
|
def _hash_component(self, name: str, value: Any) -> str:
|
||||||
|
serialized = json.dumps({"name": name, "value": value}, sort_keys=True, default=str)
|
||||||
|
return hashlib.sha256(serialized.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
def merkle_root(self) -> str:
|
||||||
|
# Compute per-component hashes, sort by component name to ensure order-independence
|
||||||
|
items: List[tuple] = []
|
||||||
|
for k, v in self.components.items():
|
||||||
|
h = self._hash_component(k, v)
|
||||||
|
items.append((k, h))
|
||||||
|
items.sort(key=lambda x: x[0])
|
||||||
|
concat = "".join(h for _, h in items).encode("utf-8")
|
||||||
|
return hashlib.sha256(concat).hexdigest()
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
# Convert to a canonical dict suitable for hashing/serialization.
|
return {"components": self.components, "merkle_root": self.merkle_root()}
|
||||||
return {
|
|
||||||
"code_commit": self.code_commit,
|
|
||||||
"environment": self.environment.to_dict(),
|
|
||||||
"container_hash": self.container_hash,
|
|
||||||
"dataset_refs": sorted(self.dataset_refs, key=lambda d: d.get("id") or ""),
|
|
||||||
"model_ref": self.model_ref,
|
|
||||||
"run_manifest": self.run_manifest,
|
|
||||||
}
|
|
||||||
|
|
||||||
def compute_bundle_hash(self) -> str:
|
def to_json(self) -> str:
|
||||||
"""Compute a deterministic SHA-256 digest over the canonical bundle
|
return json.dumps(self.to_dict(), sort_keys=True, default=str)
|
||||||
representation. Uses JSON with sorted keys so identical bundles always
|
|
||||||
produce the same digest.
|
|
||||||
"""
|
|
||||||
payload = self.to_dict()
|
|
||||||
serialized = json.dumps(payload, sort_keys=True, default=str).encode("utf-8")
|
|
||||||
return hashlib.sha256(serialized).hexdigest()
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
from mltrail_verifiable_provenance_ledger_for.reprobundle import ReproBundle
|
||||||
|
from mltrail_verifiable_provenance_ledger_for.repro_runner import ReproRunner
|
||||||
|
|
||||||
|
|
||||||
|
def test_repro_runner_valid_bundle():
|
||||||
|
components = {
|
||||||
|
"code_commit": {"repo": "example", "commit": "abc123"},
|
||||||
|
"environment": {"python": "3.9", "deps": {"numpy": "1.26.0"}},
|
||||||
|
"run_manifest": {"cmd": "python train.py", "seed": 42},
|
||||||
|
}
|
||||||
|
bundle = ReproBundle(components)
|
||||||
|
runner = ReproRunner(signing_key="key1")
|
||||||
|
transcript = runner.run(bundle)
|
||||||
|
|
||||||
|
assert transcript["merkle_root"] == bundle.merkle_root()
|
||||||
|
assert "manifest_digest" in transcript
|
||||||
|
assert "signature" in transcript
|
||||||
|
|
||||||
|
|
||||||
|
def test_repro_runner_deterministic_signature():
|
||||||
|
components = {
|
||||||
|
"code_commit": {"repo": "example", "commit": "abc123"},
|
||||||
|
"environment": {"python": "3.9", "deps": {"numpy": "1.26.0"}},
|
||||||
|
"run_manifest": {"cmd": "python train.py", "seed": 42},
|
||||||
|
}
|
||||||
|
bundle = ReproBundle(components)
|
||||||
|
r1 = ReproRunner(signing_key="keyX")
|
||||||
|
r2 = ReproRunner(signing_key="keyX")
|
||||||
|
|
||||||
|
t1 = r1.run(bundle)
|
||||||
|
t2 = r2.run(bundle)
|
||||||
|
|
||||||
|
# signatures should be deterministic given same key and bundle if run at same timestamp
|
||||||
|
# timestamps may differ; signatures will therefore usually differ. We check manifest_digest and merkle_root stability
|
||||||
|
assert t1["merkle_root"] == t2["merkle_root"]
|
||||||
|
assert t1["manifest_digest"] == t2["manifest_digest"]
|
||||||
|
|
@ -1,54 +1,24 @@
|
||||||
from mltrail_verifiable_provenance_ledger_for.reprobundle import ReproBundle
|
from mltrail_verifiable_provenance_ledger_for.reprobundle import ReproBundle
|
||||||
from mltrail_verifiable_provenance_ledger_for.contracts import Environment
|
|
||||||
|
|
||||||
|
|
||||||
def make_sample_environment():
|
def test_reprobundle_deterministic_root():
|
||||||
return Environment(
|
components = {
|
||||||
id="env1",
|
"code_commit": {"repo": "example", "commit": "abc123"},
|
||||||
language="python",
|
"environment": {"python": "3.9", "deps": {"numpy": "1.26.0"}},
|
||||||
version="3.9",
|
"run_manifest": {"cmd": "python train.py", "seed": 42},
|
||||||
dependencies={"numpy": "1.24.0", "pandas": "1.5.3"},
|
}
|
||||||
container_hash=None,
|
b1 = ReproBundle(components)
|
||||||
)
|
b2 = ReproBundle({k: components[k] for k in reversed(list(components.keys()))})
|
||||||
|
|
||||||
|
# Merkle root must be stable regardless of insertion order
|
||||||
|
assert b1.merkle_root() == b2.merkle_root()
|
||||||
|
|
||||||
|
|
||||||
def test_bundle_hash_deterministic():
|
def test_reprobundle_detects_change():
|
||||||
env = make_sample_environment()
|
base = {
|
||||||
bundle1 = ReproBundle(
|
"code_commit": {"repo": "example", "commit": "abc123"},
|
||||||
code_commit="deadbeef",
|
"environment": {"python": "3.9", "deps": {"numpy": "1.26.0"}},
|
||||||
environment=env,
|
}
|
||||||
dataset_refs=[{"id": "ds1", "fingerprint": "fp1"}],
|
b = ReproBundle(base)
|
||||||
model_ref={"id": "m1", "fingerprint": "mf1"},
|
b_changed = ReproBundle({**base, "run_manifest": {"cmd": "python train.py"}})
|
||||||
run_manifest={"seed": 42, "cmd": "python train.py"},
|
assert b.merkle_root() != b_changed.merkle_root()
|
||||||
)
|
|
||||||
bundle2 = ReproBundle(
|
|
||||||
code_commit="deadbeef",
|
|
||||||
environment=make_sample_environment(),
|
|
||||||
dataset_refs=[{"id": "ds1", "fingerprint": "fp1"}],
|
|
||||||
model_ref={"id": "m1", "fingerprint": "mf1"},
|
|
||||||
run_manifest={"seed": 42, "cmd": "python train.py"},
|
|
||||||
)
|
|
||||||
|
|
||||||
h1 = bundle1.compute_bundle_hash()
|
|
||||||
h2 = bundle2.compute_bundle_hash()
|
|
||||||
assert h1 == h2
|
|
||||||
|
|
||||||
|
|
||||||
def test_bundle_hash_changes_on_difference():
|
|
||||||
env = make_sample_environment()
|
|
||||||
base = ReproBundle(
|
|
||||||
code_commit="deadbeef",
|
|
||||||
environment=env,
|
|
||||||
dataset_refs=[{"id": "ds1", "fingerprint": "fp1"}],
|
|
||||||
model_ref={"id": "m1", "fingerprint": "mf1"},
|
|
||||||
run_manifest={"seed": 42, "cmd": "python train.py"},
|
|
||||||
)
|
|
||||||
modified = ReproBundle(
|
|
||||||
code_commit="cafebabe", # different commit
|
|
||||||
environment=env,
|
|
||||||
dataset_refs=[{"id": "ds1", "fingerprint": "fp1"}],
|
|
||||||
model_ref={"id": "m1", "fingerprint": "mf1"},
|
|
||||||
run_manifest={"seed": 42, "cmd": "python train.py"},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert base.compute_bundle_hash() != modified.compute_bundle_hash()
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue