Ophamin interop overview¶
One page covering every way to drive, consume, or observe Ophamin from outside Python. Eight interop layers stacked so a consumer picks the one that fits their shape.
At a glance¶
| Consumer shape | Layer | Surface | Read-only? | First shipped |
|---|---|---|---|---|
| Non-Python systems needing cryptographic verification | Wire-format ports | crates/ophamin-proof (Rust) + packages/ophamin-proof-js (JS/TS) — both read AND write |
no (write-side since 0.21.0) | 0.16.0 (read), 0.21.0 (write) |
| AI agents speaking MCP | MCP server | ophamin mcp serve (stdio / SSE / streamable-http) |
one tool writes (run_scenario); rest are read |
0.17.0 |
| HTTP / service-style consumers | HTTP REST API | ophamin http serve (FastAPI; OpenAPI 3 at /openapi.json) |
same as MCP | 0.18.0 |
| Event-stream routing infrastructure | CloudEvents 1.0 envelope | ophamin.cloudevents.wrap / unwrap (Python) |
no — wrap is opt-in; routing is read | 0.19.0 |
| Observability backends (Jaeger / Datadog / etc.) | OpenTelemetry instrumentation | ophamin.observability.setup_otel() + ambient OTel SDK |
n/a (telemetry is one-way) | 0.20.0 |
| Supply-chain attestation (Sigstore / SLSA / Rekor / cosign) | in-toto Attestation Framework v1 (ITE-6) + DSSE envelope | ophamin.interop.to_in_toto_statement / to_dsse_envelope (Python) |
n/a (export only) | 0.35.0 |
| FAIR research-data infrastructure (Zenodo / Galaxy / WorkflowHub) | RO-Crate 1.2 (Research Object Crate, JSON-LD + schema.org) | ophamin.interop.to_ro_crate_metadata (Python) |
n/a (export only) | 0.36.0 |
| Data-pipeline lineage backends (Airflow / dbt / Spark / Marquez) | OpenLineage 2.0 RunEvent | ophamin.interop.to_openlineage_event (Python) |
n/a (export only) | 0.37.0 |
All eight layers wrap the same shared implementations
(src/ophamin/interfaces/_impls.py), so behavioural drift between
them is structurally impossible.
Choosing your layer¶
"I have a record I want to verify, from a non-Python language."¶
Use the wire-format port for your language:
- Rust:
cargo add ophamin-proof@0.21.2→ophamin_proof::parse_proof(text)+ophamin_proof::verify_signature(&record, key). Seecrates/ophamin-proof/README.md. - JS/TS:
npm install @ophamin/proof@0.21.2→parseProof(text)+verifySignature(record, key). Seepackages/ophamin-proof-js/README.md.
Conformance against the cross-language fixtures is locked: a record verifying under any port verifies under every port.
"I want to PRODUCE a record from a non-Python language."¶
Same packages, write-side surface:
- Rust:
- JS/TS:
The Rust CanonicalValue enum and the JS PyInt wrapper both
preserve Python's int / float distinction (load-bearing for
byte-equivalent signatures).
"I'm building an AI agent and want it to drive Ophamin."¶
Use the MCP server. Install with the [mcp] extra:
Wire it into your MCP client. For Claude Code:
// ~/.claude/config.json (or equivalent)
{
"mcpServers": {
"ophamin": { "command": "ophamin", "args": ["mcp", "serve"] }
}
}
Six tools become available to the agent: list_scenarios,
get_scenario_claim, verify_proof, canonicalize_value,
read_proof_index, run_scenario. See
src/ophamin/mcp/README.md for
client recipes (Claude Desktop, Cursor, Cline, generic stdio
clients).
"I'm building a service that talks JSON over HTTP."¶
Use the HTTP REST API:
The eight endpoints mirror the MCP surface plus /health and
/version. OpenAPI 3 spec at /openapi.json; Swagger UI at
/docs. See src/ophamin/http_api/README.md
for curl examples + Docker / Kubernetes / systemd deployment
recipes.
"I want Ophamin records flowing through Kafka / EventBridge / Knative."¶
Wrap them at production:
from ophamin.cloudevents import wrap
import json
envelope = wrap(proof, source="urn:my-deployment:ophamin")
producer.send(topic="ophamin.proofs", value=json.dumps(envelope).encode())
Unwrap at consumption:
from ophamin.cloudevents import unwrap
from ophamin.interfaces._impls import verify_proof_impl
proof = unwrap(envelope_text)
result = verify_proof_impl(json.dumps(proof))
assert result["verified"]
See src/ophamin/cloudevents/README.md
for the full attribute catalogue + Kafka / EventBridge recipes.
"I want Ophamin spans in Jaeger / metrics in Prometheus."¶
Wire OpenTelemetry once at app startup:
Or via the standard env var:
Every run_scenario, verify_proof, and canonicalize_value
call from then on emits spans (ophamin.scenario.run.<name>,
ophamin.proof.verify, ophamin.canonical.encode) and records
metrics (ophamin_scenarios_run_total,
ophamin_scenario_duration_seconds,
ophamin_proofs_verified_total,
ophamin_canonical_bytes_encoded).
See src/ophamin/observability/README.md
for the full attribute catalogue + sidecar wiring with HTTP /
MCP / CloudEvents.
"I want my proof on Sigstore / Rekor / SLSA infrastructure."¶
Wrap the proof as an in-toto Statement v1 (ITE-6), optionally sealed inside a DSSE envelope:
from ophamin.interop import to_in_toto_statement, to_dsse_envelope
# 1. Bare Statement — for in-process inspection or custom signing
stmt = to_in_toto_statement(signed_proof)
# {
# "_type": "https://in-toto.io/Statement/v1",
# "predicateType": "https://github.com/IdirBenSlama/Ophamin/.../SCHEMAS.md#empirical-proof-record-v1",
# "subject": [{"name": "...", "digest": {"sha256": "<proof_id>"}}],
# "predicate": {"body": {...}, "signature": "<hmac>"}
# }
# 2. DSSE-sealed envelope — for upload to Rekor or storage in an
# attestation archive
envelope = to_dsse_envelope(signed_proof, key=b"my-dsse-key", keyid="rsa-2026")
# {
# "payloadType": "application/vnd.in-toto+json",
# "payload": "<base64 of canonical Statement JSON>",
# "signatures": [{"keyid": "rsa-2026", "sig": "<base64 HMAC>"}]
# }
The Statement's subject digest IS the proof's content-addressed
proof_id (SHA-256 over sections 1–8), so the in-toto subject
is structurally tied to Ophamin's own canonical form. Downstream
verifiers can:
- Run
cosign verify-attestation --type customagainst the envelope usingOPHAMIN_PREDICATE_TYPE_V1as the type-filter. - Upload to Rekor:
rekor-cli upload --type intoto --artifact envelope.json. - Plug into a
policy-controlleradmission webhook to gate Kubernetes deployments on the presence of a VALIDATED Ophamin proof matching the cluster's expected subject digest.
The DSSE PAE (Pre-Authentication Encoding) prevents
signature-substitution across payloadTypes, per the
secure-systems-lab DSSE spec. verify_dsse_envelope(env, key)
checks the outer envelope; the inner Ophamin HMAC over
predicate.body is verified separately with the original
Ophamin signing key — two-layer cryptographic trust where the
outer (DSSE) and inner (Ophamin) keys may differ.
See src/ophamin/interop/in_toto.py
for the full API. References:
"I want my proof packaged for Zenodo / Galaxy / WorkflowHub."¶
Wrap the proof as an RO-Crate 1.2 (Research Object Crate). Two APIs exist — the building block returns the metadata dict, and the convenience wrapper writes a complete crate directory:
from ophamin.interop import write_ro_crate
# One call → complete self-describing crate directory on disk
crate_dir = write_ro_crate(
signed_proof,
"./my-empirical-attestation",
extra_root_metadata={
"creator": {"@id": "https://orcid.org/0000-0000-0000-0000"},
"license": {"@id": "https://spdx.org/licenses/Apache-2.0"},
},
)
# crate_dir is an absolute pathlib.Path to the directory containing
# both proof.json and ro-crate-metadata.json — ready to upload to
# Zenodo, submit to WorkflowHub, ingest into Galaxy, or zip via
# shutil.make_archive(str(crate_dir), "zip", crate_dir).
For full control over the metadata-without-write path, use
to_ro_crate_metadata directly (returns a dict). write_ro_crate
refuses to overwrite an existing directory by default —
overwrite=True opts in. Both APIs accept the same proof_filename
and extra_root_metadata kwargs.
The resulting ./my-empirical-attestation/ directory is a
complete RO-Crate. Upload it to Zenodo (mint a DOI), submit it
to WorkflowHub, or ingest it into Galaxy — the JSON-LD
metadata is self-describing and tools that consume RO-Crate
will pick out the principal artifact (the signed proof),
data references, substrate, verdict, and reproduction command
automatically.
The exporter maps Ophamin's nine sections into schema.org vocabulary:
- Root
Dataset(@id: "./") — the crate itself, name + datePublished + identifier (the proof's content-addressedproof_id) File(@id: "proof.json") — the signed proof JSONDataset(one per §4 DatasetRef) — content-addressed, carriesurl(source) +size(n_records)SoftwareApplication(@id: "#substrate-<name>@<commit>") — the substrate under testAssessAction(@id: "#verdict") — the §6 verdict, withresultas aPropertyValuecarrying the observed metricSoftwareSourceCode(@id: "#reproduction") — the §7 reproduction commandSoftwareApplication(@id: "#ophamin") — Ophamin itself, withsoftwareVersion+identifier(git commit)
See src/ophamin/interop/ro_crate.py
for the full API. References:
"I want Ophamin scenarios in my Airflow / dbt / Spark lineage."¶
Emit one OpenLineage 2.0 RunEvent per signed proof, then POST it to a Marquez backend (or any OpenLineage-aware collector):
import requests
from ophamin.interop import to_openlineage_event
event = to_openlineage_event(signed_proof)
requests.post(
"http://marquez:5000/api/v1/lineage",
json=event,
timeout=5,
)
The mapping is shaped for live pipeline integration:
eventType—COMPLETEfor VALIDATED and REFUTED outcomes;FAILfor INCONCLUSIVE. The REFUTED-vs-FAIL distinction matters: REFUTED is a real empirical result and MUST NOT trip "job failure" alerts; INCONCLUSIVE means the run didn't produce a deciding observation.run.runId— deterministic UUIDv5 derived fromproof_idvia the pinned namespaceec1e6b1c-…-000000000001. Same proof → same runId on any machine, so Marquez dedupes re-emits for free.job.namespace— defaults to"ophamin"; override per deployment.job.name— defaults to the first PillarEvidence'spillarfield (e.g."I.cma"), overridable viajob_name=.inputs— one per §4 DatasetRef, withdataSourcefacet (URL) + customophamin_datasetfacet (content_hash, n_records).outputs— exactly one, namespacedophamin.proofs, named with the content-addressedproof_id, with aschemafacet describing the proof's column shape.- Custom facets
ophamin_claim+ophamin_verdictcarry the §2 claim + §6 verdict structured payload, plus the §9 HMAC signature for cross-attribution.
The deterministic runId is the killer feature: a downstream pipeline operator can join lineage on proof_id ↔ runId without maintaining any separate mapping table. Marquez consumers see each Ophamin scenario as a first-class job in their lineage graph.
For long-running Ophamin campaigns, emit the full lifecycle (START at scenario boot, periodic RUNNING heartbeats, terminal COMPLETE or FAIL):
from ophamin.interop import (
new_run_id,
to_openlineage_start_event,
to_openlineage_running_event,
to_openlineage_complete_event,
to_openlineage_fail_event,
)
run_id = new_run_id() # mint once at scenario boot
# 1. START — emit BEFORE the substrate measurement begins
post(to_openlineage_start_event(
run_id=run_id,
scenario_name="immune_siege",
claim=preregistered_claim,
datasets=corpus_dataset_refs,
analysis_plan="cumulative meta-analysis over 1000 cycles",
))
# 2. RUNNING heartbeats — emit periodically during the run
try:
for batch_idx, batch in enumerate(scenario_batches):
process_batch(batch)
post(to_openlineage_running_event(
run_id=run_id,
scenario_name="immune_siege",
progress={
"percent_complete": (batch_idx + 1) / len(scenario_batches),
"cycles_completed": cycles_so_far,
"cycles_total": total_cycles,
"message": f"Batch {batch_idx + 1}/{len(scenario_batches)}",
},
))
# 3a. COMPLETE — emit with the signed proof
signed_proof = build_and_sign_proof(...)
post(to_openlineage_complete_event(
run_id=run_id, proof=signed_proof,
))
except Exception as exc:
# 3b. FAIL — emit if the scenario crashed before producing a proof
post(to_openlineage_fail_event(
run_id=run_id,
scenario_name="immune_siege",
error_message=str(exc),
error_type=type(exc).__name__,
))
raise
Marquez renders the full lifecycle: a job that started at T0,
emitted progress at T0+5m / T0+10m / T0+15m, and completed (or
failed) at T0+20m. The ophamin_progress facet on RUNNING
events surfaces percent-complete bars; the ophamin_error
facet on FAIL events surfaces the exception details.
The single-event terminal :func:to_openlineage_event remains
available for emit-once-when-done callers (no START/RUNNING
preamble); it derives a deterministic runId from proof_id.
For streaming, use the four lifecycle functions above with a
caller-managed run_id from new_run_id().
See src/ophamin/interop/openlineage.py
for the full API. References:
Cross-layer composition¶
The layers are designed to compose. A typical pipeline:
[Rust producer] [Python verifier]
CanonicalValue::Object{...} HTTP POST /verify
+ sign_canonical │
+ CloudEvents wrap (in Rust if needed, ▼
or hand off to Python proxy) {verified: true, verdict: ...}
│
▼ Kafka topic "ophamin.proofs"
[Kafka consumer pod]
+ unwrap envelope
+ emit span (OTel)
+ forward to verifier service over HTTP
The wire-format contract (signed bytes) is the load-bearing primitive that survives every transport unchanged. The other four layers transport it.
Stability contract¶
The interop layers follow Ophamin's API stability contract:
@Stable— wire format (SCHEMAS.mdR1–R11), the six tool function signatures inophamin.interfaces._impls, MCP tool names + arg shapes, HTTP endpoint paths + body shapes, CloudEvents attribute names emitted, OTel span names and attribute names, in-toto Statement / DSSE constants (IN_TOTO_STATEMENT_V1_TYPE,OPHAMIN_PREDICATE_TYPE_V1,DSSE_INTOTO_PAYLOAD_TYPE), RO-Crate constants (RO_CRATE_CONTEXT_V1_2,RO_CRATE_CONFORMS_TO_V1_2,DEFAULT_PROOF_FILENAME,RO_CRATE_METADATA_FILENAME), OpenLineage constants (OPENLINEAGE_SCHEMA_URL,OPENLINEAGE_PRODUCER_URL_BASE,DEFAULT_NAMESPACE,OPHAMIN_RUNID_NAMESPACE).@Provisional— implementation-internal details (Rust module layout undercrates/ophamin-proof/src/, JS module layout underpackages/ophamin-proof-js/src/, OTel metric internals).@Deprecated— none currently. Backward-compat policy matchesSCHEMAS.md: one-minor-version deprecation window before removal.
A drift in any @Stable surface is a major-version bump with a
documented migration path.
Runnable examples¶
Every layer ships at least one runnable consumer-facing example:
| Layer | Run | What it does |
|---|---|---|
| Wire-format (Python) | pytest tests/test_canonical_form_fixtures.py |
Validates Python canonical-form encoder against 5 cross-language fixtures |
| Wire-format (Rust) | cd crates/ophamin-proof && cargo run --example verify_proofcargo run --example sign_value |
Loads a shipped proof + verifies HMAC under default key; builds a CanonicalValue tree + signs |
| Wire-format (JS) | cd packages/ophamin-proof-js && npm run example:verifynpm run example:sign |
Same shape as the Rust examples |
| MCP server | PYTHONPATH=src python examples/walkthrough_mcp_server.py |
Exercises all 6 MCP tools through FastMCP's in-process call_tool |
| HTTP REST API | PYTHONPATH=src python examples/walkthrough_http_api.py |
Drives 7 endpoints + inspects /openapi.json via TestClient |
| CloudEvents | PYTHONPATH=src python examples/walkthrough_cloudevents.py |
Wraps + transit-serializes + unwraps + re-verifies a real shipped proof |
| OpenTelemetry | PYTHONPATH=src python examples/walkthrough_otel.py |
Installs InMemoryExporter, exercises impls, prints captured spans + metrics |
Each script self-asserts its invariants — they exit non-zero on
behavioural drift, so they double as CI smoke pins. See
examples/README.md
for the full catalogue.
See also¶
SCHEMAS.md— the normative wire-format spec.docs/REPRODUCING.md— external-rebuild guide for verifying the framework's claims on your own infrastructure.docs/STABILITY.md— API stability policy.- Per-layer READMEs:
crates/ophamin-proof/README.mdpackages/ophamin-proof-js/README.mdsrc/ophamin/mcp/README.mdsrc/ophamin/http_api/README.mdsrc/ophamin/cloudevents/README.mdsrc/ophamin/observability/README.mdpaper/paper.md— methods paper covering the framework's empirical claims.