Metadata-Version: 2.5
Name: runseal
Version: 0.1.1
Summary: Content-addressed provenance for computational research runs: hash it, register it, sign it, prove it did not change.
Project-URL: Homepage, https://github.com/charlieyanhx/runseal
Project-URL: Issues, https://github.com/charlieyanhx/runseal/issues
Author: Charlie Yan
License-Expression: MIT
License-File: LICENSE
Keywords: audit,content-addressing,merkle,provenance,reproducibility,research
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Security :: Cryptography
Requires-Python: >=3.9
Provides-Extra: signing
Requires-Dist: cryptography>=41.0; extra == 'signing'
Provides-Extra: test
Requires-Dist: cryptography>=41.0; extra == 'test'
Requires-Dist: numpy>=1.24; extra == 'test'
Requires-Dist: pytest>=7.0; extra == 'test'
Description-Content-Type: text/markdown

# runseal

**Prove which inputs produced a result — and that nothing changed since.**

[![tests](https://github.com/charlieyanhx/runseal/actions/workflows/tests.yml/badge.svg)](https://github.com/charlieyanhx/runseal/actions/workflows/tests.yml)
[![PyPI](https://img.shields.io/pypi/v/runseal.svg)](https://pypi.org/project/runseal/)
[![Python](https://img.shields.io/pypi/pyversions/runseal.svg)](https://pypi.org/project/runseal/)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)

Content-addressed provenance for computational research. Four primitives, no required
dependencies, no daemon, no server.

![How runseal identifies and seals a run](docs/provenance.svg)

```bash
pip install runseal            # hashing, merkle, registry
pip install runseal[signing]   # adds RSA-PSS artifact signing
```

## Thirty seconds

```python
from runseal import HashGenerator, ArtifactRegistry, Artifact

h = HashGenerator()
training_frame = {"rows": [1, 2, 3]}
config = {"window": 30, "seed": 7}

hashes = dict(
    source_hash=h.hash_data_snapshot(training_frame),
    code_hash=h.hash_code("src/"),
    config_hash=h.hash_config(config),
)

# A run is identified by what went into it, never by when you ran it.
run_id = h.compute_run_id(
    data_hash=hashes["source_hash"],
    config_hash=hashes["config_hash"],
    code_hash=hashes["code_hash"],
    env_hash=h.hash_environment(h.get_environment_manifest()),
)

registry = ArtifactRegistry("./provenance")
registry.register(Artifact(id=f"{run_id}/raw",   type="dataset", layer="ingest", **hashes))
registry.register(Artifact(id=f"{run_id}/model", type="model",   layer="train",
                           parent=f"{run_id}/raw", metrics={"auc": 0.71}, **hashes))

print(run_id[:32])
print(" -> ".join(a.id.split("/")[-1] for a in registry.get_lineage(f"{run_id}/model")))
print(registry.verify_integrity()["integrity_ok"])
```

```
45cd8972f036917e3a89ee3363c7119e
model -> raw
True
```

Re-run the same pipeline on the same inputs and `run_id` is byte-identical. Change one config
value, one byte of data, or one line of code, and it is not.

## What is in it

| | |
|---|---|
| `HashGenerator` | Content-addressed run IDs from data, config, code and environment. Deterministic across processes and machines. |
| `MerkleTree` | Append-only log with root verification and per-leaf inclusion proofs. |
| `ArtifactRegistry` | JSON-backed store with parent lineage, filtered queries and integrity checks. |
| `SignatureManager` | RSA-PSS signing and verification of artifact records. Optional extra. |

`hash_config` is order-insensitive, `hash_data_snapshot` handles dicts, sequences and numpy
arrays by content, and `hash_code` walks a directory's `.py` files in sorted order.

## What this is not

It is easy to pattern-match this to a pipeline tool. It is not one.

| If you want | Use |
|---|---|
| Data and model versioning with remote storage | [DVC](https://dvc.org) |
| Experiment tracking with a UI and a server | [MLflow](https://mlflow.org), [Weights & Biases](https://wandb.ai) |
| Pipeline orchestration | Airflow, Prefect, Dagster |
| A small library that answers "what produced this, and has it changed?" | **runseal** |

No service to run, no storage backend to configure, nothing to log into. It records and verifies;
it does not schedule, execute, or store your data.

## Why this exists

Extracted from a private research program where a wrong number that reconciles is more expensive
than one that crashes. Writing the test suite surfaced two defects, both fixed here with
regression tests named after them.

**Merkle proofs verified only for leaf 0.** `get_proof` returned bare sibling hashes with no
record of which side each sibling sat on, while `verify_proof` always combined
`H(current + sibling)`. Parents are built as `H(left + right)`, so any leaf that was a right child
at any level failed to verify. On a four-leaf tree, three of four *valid* proofs were rejected.
`get_proof` now returns `(sibling_hash, side)` pairs, `verify_proof` honours the side, and the
odd-leaf self-pairing case is explicit. Covered for trees of 1–33 leaves.

**Environment hashes were never stable.** `get_environment_manifest()` embedded `time.time()`, so
hashing the manifest gave a different digest on every call and `compute_run_id` produced a
different RUN_ID for an identical environment — defeating the whole point of content addressing.
The manifest is now deterministic, with wall-clock time left to `Artifact.created_at` where it
belongs. Package enumeration also moved off the deprecated `pkg_resources`.

## Scope and limits

Signing covers artifact identity and the provenance hashes — not mutable `metrics` or `metadata`,
which are expected to be annotated after the fact. A test asserts that boundary so it stays
explicit.

The Merkle implementation duplicates the final node on odd levels, the common convention, which
carries the known second-preimage ambiguity between a tree of N leaves and certain smaller trees.
That is fine for tamper-evidence inside one append-only registry and unsuitable as a
general-purpose commitment scheme.

## Tests

```bash
pip install -e ".[test]"
pytest -q
```

80 tests. The suite runs with or without `cryptography` — signing tests skip cleanly, and CI
exercises both paths across Python 3.9–3.13.

## Licence

MIT. See [CHANGELOG.md](CHANGELOG.md) and [CONTRIBUTING.md](CONTRIBUTING.md).
