Skip to main content

nenrin-verify (Python)

Recompute NENRIN evidence yourself, offline, in your own Python process. The Python twin of the npm package nenrin-verify and of the TSUGI recovery-chain verifier tsugi_verify.mjs: same input, same report (the limits are listed under "Where the claim stops"). It also carries the MUSUBI contract verifier, which is written in Python, byte for byte.

pip install nenrin-verify
nenrin-verify --selftest          # 31/31 frozen bundles and 98/98 TSUGI chains match the JavaScript; MUSUBI run0002 recomputes
nenrin-verify bundle.json         # the provenance report; exit 0 accepted, 1 refused
tsugi-verify chain.json --operator-key <b64>   # a TSUGI recovery chain, printed as node tsugi_verify.mjs prints it
musubi-verify contract_v0 --verify contract.json   # a MUSUBI module, run exactly as python3 contract_v0.py in the repository
import json, nenrin_verify as nv

bundle = nv.js_loads(open("bundle.json", "rb").read())   # parsed the way JSON.parse parses it
report = nv.verify_bundle(bundle)             # did:key resolution, no network, no clock
print(report["verdict"], report["does_not_establish"])
print(nv.report_json(report))                 # JSON text as the JavaScript CLI prints it (not json.dumps)
print(nv.report_sha256(report))               # compare with the JavaScript run, byte for byte

One dependency, cryptography, for Ed25519. Nothing here opens a socket, except tsugi-verify --fetch-operator-key <origin>, which fetches the operator key from the origin you name, and the two opt-in calls below (nenrin-a2a-record submit / flush(submit=True), and policy.fetch_resume).

Record every A2A call your agent makes (0.4, a2a_recorder)

Two lines with the official A2A Python SDK (pip install "nenrin-verify[a2a]"):

from nenrin_verify.a2a_recorder import Recorder
client = ClientFactory(config).create(card, interceptors=[Recorder(witness_name="acme-billing", key="witness.pem")])

From then on every outgoing call (send, stream, get_task, cancel, ...) is logged on your machine: method, time, the salted sha256 of the request and of every response event, the final task state, and whether any result came back. At exit (or rec.flush()) the calls become one Ed25519-signed jidec-path-v1 record per agent endpoint, in nenrin-records/records/ (the exact body the NENRIN witness intake accepts), with the salt and the full log kept in nenrin-records/private/ (mode 600). Nothing is sent unless you file it. Content never leaves your machine; a counterparty you show the private file to can match any call to its own logs (a2a_recorder.matches).

Each record lists what it establishes and what it does not, names the previous record for the same endpoint (prev_path_refs, so a dropped day is a visible gap), and is checked against the ledger's own intake rules and the resume assembler before it is written. A record says what came back from an agent you actually used, not whether it was right; an unanswered call may be your own network, and the record says so.

nenrin-a2a-record keygen witness.pem       # serve the public key at https://<your domain>/... and pass key_url=
nenrin-a2a-record show                     # the records in ./nenrin-records
nenrin-a2a-record verify nenrin-records/records/<sha>.json --private nenrin-records/private/<sha>.json
nenrin-a2a-record submit nenrin-records/records/<sha>.json     # opt-in: file it at the public ledger

Your rule, your decision (0.4, policy)

NENRIN never says whether to trust an agent. policy.evaluate applies a rule you write to a resume and returns a decision anyone recomputes from (resume, rule, time). No score; every record counted or not counted is listed with the reason, and the resume's own sha256 is recomputed rather than trusted.

from nenrin_verify import policy
rule = {"min_independent_witnesses": 2, "within_days": 30, "max_fail": 0, "exclude_domains": ["mycompany.example"]}
d = policy.evaluate(policy.fetch_resume("https://agent.example/a2a"), rule)
print(d["allow"], d["reasons"])

By default a witness counts only by the domain its signing key is served from (independence: "signed_domain"), and the measured agent's own domain never counts. nenrin-policy <endpoint or resume.json> --rule rule.json does the same from a shell (exit 0 allow, 1 deny).

Snippets for LangGraph, the OpenAI Agents SDK, Google ADK, CrewAI and Claude (the verification gate as an MCP tool, the recorder and the policy together): integrations/FRAMEWORKS.md.

Score your verifier the way the TSUNAGI board does (0.4.9, nenrin-tsunagi)

The TSUNAGI board (ops/tsunagi/BOARD.md in the repository) runs every registered NENRIN verifier every night, each cloned from its author's own repository. For the NENRIN corpora it no longer repeats the count an implementation prints: it hands the implementation the fixtures as one batch file, reads back one verdict signature per fixture and compares each with the frozen expected.json itself. nenrin-tsunagi is that referee, with the three corpora inside the package, so you get the board's answer on your own machine before you open a pull request.

pip install nenrin-verify
nenrin-tsunagi list
nenrin-tsunagi run nenrin-interop-v0.2-edge -- "python3 my_verifier.py {in} {out}"
nenrin-tsunagi row nenrin-interop-v0.2-edge -- "python3 my_verifier.py {in} {out}"    # the row for implementations.json

The contract, in any language: {in} is a JSON array [{"name", "bundle"}]; write {out} as {name: {"verdict", "refusals", "findings"}} (or {name: {"error"}}). Codes are compared as sets (VERIFIER.md section 4). A missing case, an extra case, an error or any difference is a case not reproduced; exit 0 only when every case reproduces.

What it verifies

module what held to
verify_provenance, consume_evidence, posture_line, candidate_evidence_set, preflight_report one A2A task's provenance graph: the delegation chain observed by third-party witnesses (R1 to R4, witness and edge signatures), the caller's grant and the provider's execution receipt (E1 to E3, caller and provider signatures), the provider's pre-execution intent, the outcome's evidence pointer, and the digest link between the layers npm nenrin-verify 0.4.4 (nenrin_verify.mjs, verifier 0.1.6): the same report, key for key
agreement_verify.verify, nenrin-agreement-verify a two-party agreement record (a2a-agreement-v1, v1.1), including key succession across a rotation the repository's own Python verifier, unchanged but for one import line and a header comment; it and the JavaScript verifier return the same report on 5,286 frozen cases, and from npm nenrin-verify 0.3.0 the JavaScript command nenrin-agreement-verify prints what this one prints
musubi.load, musubi-verify MUSUBI (a2a-contract-v0): a contract both parties signed, its settlement against anchored execution records (settle v1 to v1.10), offers, bonds, corrections, terms, independence, corroboration, and the spine that threads one contract through all of them the repository's own files (musubi-v0/), byte for byte: MUSUBI is written in Python and has no JavaScript twin, so the guarantee is that installing changes nothing, each of its 18 modules passes its own self-test from the package, and the first settled execution (run0002) recomputes to its published hashes
tsugi.verify_chain, tsugi-verify a TSUGI recovery chain (drift, proposal, authorization, execution, verify): every record's schema, hash and Ed25519 signature, order and links, strict mode (a human-approval repair needs an authorization signed by a trusted operator key, unexpired), the random witness draw recomputed from beacon, pool and subject, the commit-then-reveal anchor, the embedded witness observations and the quorum tsugi_verify.mjs (verifier 0.3.1): the same stdout, byte for byte, and the same exit code

How "same report" is checked

The JavaScript file is the reference. The port reads it line for line, and every place where Python and JavaScript disagree by default (x or y on {}, == on True and 1, Date.parse accepting February 31st, Node's lenient base64, an own __proto__ key that Object.assign turns into a prototype, the order V8's sort gives values a numeric comparator cannot order, new URL(s).host) goes through src/nenrin_verify/_js.py and src/nenrin_verify/_url.py, so the differences can be read in one place.

  • Frozen. 31 bundles signed with keys derived from a public phrase, covering accepted graphs, refusals of every layer, witness disagreement, provider equivocation, action bindings, non-ASCII and escapes, and the date edge cases. For each, the report, the consume_evidence projection and the CLI output are byte-identical to the JavaScript's. They ship in the package; nenrin-verify --selftest re-runs them on your machine.
  • Live. Every path of every frozen bundle is broken in about twenty ways (deleted, nulled, retyped, shortened, reordered, timestamps and sequence numbers bent, own __proto__ keys added, the whole bundle replaced by an array or null), 27,062 inputs in all. Each is run through the JavaScript verifier and through this port. Pass means both threw, or both returned the same report and projection. 0 differ.
  • Semantics. The helpers in _js.py against Node directly: number text, Date.parse, Buffer base64, string escaping and mixed-type sorting, several thousand random values each.
  • Agreement. The packaged agreement verifier returns every one of the 5,286 frozen reports in agreement-v0/agreement_vectors_v1.json, the file the JavaScript verifier is scored against.
  • Unchanged. VENDORED.json pins the sha256 of the agreement files and their sources; a copy that drifts fails.
  • MUSUBI, byte for byte. The 18 modules of musubi-v0/ and the data their self-tests read sit in src/nenrin_verify/_repo/musubi-v0/, beside a byte-identical agreement-v0/, the layout each module expects; no line is edited, and VENDORED.json pins each to its source (the same sha256 twice). Byte identity matters beyond the proof: correction_v0 fingerprints its own code files, so a correction bundle built from the repository verifies here only if the files are the same bytes. From the installed wheel, every module's own self-test passes (musubi-verify --selftest, 18/18) and run0002 recomputes to b13a3869... and 11c27fcf... (musubi-verify --run0002). Not shipped: gen_anchor_compose_fixture.py (needs opentimestamps, regenerates a fixture) and header_view_fetch.py (fetches block headers over the network).
  • TSUGI, frozen. 98 cases: the repository's three real chains (incident 2 with the real operator signature, 12 records; the 7-record incident of the same week; that incident re-verified by a random draw of witnesses), under the command lines that matter, and edits of them that reach every one of the 69 refusal codes tsugi_verify.mjs has, plus the five inputs on which the JavaScript itself throws. Edited chains are re-sealed so an edit meets the rule it targets, not only a hash mismatch. For each, node tsugi_verify.mjs was run and its stdout and exit code frozen; they ship in the package and nenrin-verify --selftest re-runs them.
  • TSUGI, live. Every path of the real chains and of the witness pool is edited in about twenty ways, raw and re-sealed: 29,326 inputs with the frozen cases. Each is run through tsugi_verify.mjs and through this port. 0 differ; 3 edits put a non-ASCII host in an endpoint, and there the port raises NotReproduced instead of answering (see below).
  • TSUGI semantics. new URL(s).host, toLowerCase, Buffer.from on any JSON value and string conversion of any JSON value, against Node directly, several thousand inputs each.

report_sha256(report) is the sha256 of the report with keys sorted by UTF-16 code unit at every depth, no whitespace, strings and numbers as JSON.stringify writes them. The JavaScript side of the same hash is tests/parity/js_canon.mjs (twelve lines).

0.5.0 (2026-10-09)

nenrin_verify.trace (nenrin-trace-verify): the TRACE intake rule of the NENRIN ledger, the nenrin-trace-bind-v0 link between a NENRIN or MUSUBI record and a TRACE Trust Record, and the OpenTelemetry helpers (span_attributes, annotate_span(span, bind) for an opentelemetry-api span, check_span_attributes). A port of trace_verify.mjs, held to it by the corpora trace-intake-v0, trace-bind-v0 and trace-span-v0, which nenrin-tsunagi now carries too. nenrin-tsunagi run writes each fixture's exact bytes into the batch, as the JavaScript referee always did: it used to re-serialise parsed fixtures, which cannot write back a lone surrogate or a literal like 1e400, exactly what trace-intake-v0 tests. The other verifiers are unchanged.

0.4.9 (2026-10-08)

nenrin-tsunagi: the TSUNAGI board's referee, with interop-v0 (5), interop-v0.1 (13) and interop-v0.2/edge (36) vendored byte for byte under _repo/ and pinned in VENDORED.json. run writes the batch, runs your command and scores it; score scores an output you already have; row prints the board entry for your pull request. The verifiers are unchanged.

0.4.8 (2026-10-06)

musubi-verify --selftest prints each module's own wall time next to its result. @pipavlo82 completed the full 0.4.7 run on Windows 11 (Python 3.12.10, Node 22.15.0): 22/22, exit 0, 25 nested self-tests reused, 1596 s, against 2 s on Linux here. Neither of us can say yet where the Windows time goes; the per-module column is what tells it. Nothing else changed.

0.4.7 (2026-10-05)

musubi-verify --selftest runs the 22 MUSUBI modules lower layers first and lets a nested self-test reuse the exact result (exit code, stdout, stderr) of the same command when it already passed earlier in the same run. Most settle self-tests end by running the self-test of the layer below, so the all-modules run used to repeat the lower layers many times; it was too slow to finish on Windows (reported by @pipavlo82 in Issue #29). Measured here: 32 s before, 2 s after, 22/22 either way. A module that failed is never reused, any other command runs for real, and the repository's files are untouched (three are fingerprinted by correction_v0). A module run on its own (musubi-verify settle_v1_10 --selftest) still runs every layer under it, and musubi-verify --selftest --no-reuse runs every nested self-test again. Checked against a broken settle_v1_8: v1.8, v1.9 and v1.10 all fail with reuse on, as they should.

0.4.6 (2026-10-05)

canonical_v0.mjs, the Node twin that contract_v0 uses to confirm its canonical bytes, decided whether it was the main module by comparing import.meta.url with "file://" + process.argv[1]. On Windows that string never matches, so the twin printed nothing and the contract_v0 self-test failed in a clean Windows venv (reported by @pipavlo82 in Issue #29, Node 22.15.0). It now compares against pathToFileURL(realpathSync(process.argv[1])).href, which also holds when the file is reached through a symlink. Its --vectors mode also reads canonical_vectors.json as published ({rule, note, vectors}); before, it expected a bare list and threw. No canonical rule and no settlement rule changed.

The Python and JavaScript packages are versioned together where they share a verifier: compare Python 0.4.5 or 0.4.6 with npm nenrin-verify 0.4.4, not with an older npm pin. 0.4.5 and 0.4.6 change only MUSUBI, which has no JavaScript twin in the npm package.

0.4.5 (2026-10-05)

MUSUBI settle v1.8, v1.9 and v1.10 now ship with the package, byte for byte from the repository, with what they need: convergence_v0 and recovery-v0/recovery_verify.py (v1.9), and babyblueviper1's approver vectors (v1.10, CC0 vectors and MIT code, their LICENSE included). musubi-verify settle_v1_10 --selftest runs from a plain venv, and musubi-verify settle_v1_10 --settle contract.json --event exec.json --view view.json settles a contract whose conditional actions require a pinned approver. The v1.10 self-test gained the rest of the approval lifecycle asked for in Issue #29: a repeatable approval covering two executions, an approval for one action carried with another, and an approval past valid_until_height. No settlement rule changed.

0.4.2 (2026-10-04)

Every signature check in this package now accepts a key only if it is the canonical encoding of a point of the prime-order subgroup. A small-order Ed25519 key (the identity point, for example) verifies R = identity, S = 0 on every message with no private key, and a mixed-order key (a real key plus a small-order point) lets one private key pose as a second key; both are refused everywhere this package checks a signature: provenance.did_key_resolver (verifier_version 0.1.5, in step with nenrin_verify.mjs), tsugi (verifier 0.3.1, in step with tsugi_verify.mjs), a2a_recorder.verify_payload and the record check in policy (provenance.ed25519_key_ok). The base64 rule of 0.4.1 now covers them all too (provenance.b64_exact), and a TSUGI witness key counts once toward a quorum. The frozen corpora were regenerated from the JavaScript files; no verdict changed. The agreement verifier is unchanged; it already applied the same subgroup rule.

0.4.4 (2026-10-04)

Malformed records (a record slot or list element that is present but not an object, and not null/false) are refused in their own step with reason record_not_object and are otherwise not presented, in step with verifier_version 0.1.6 (see the npm SDK.md and VERIFIER.md section 5). Before, the port threw on a non-object observation, as the JavaScript did. No frozen verdict changed.

0.4.3 (2026-10-04)

A witness pool with a key that is not canonical base64 of a usable key is bad_pool, and an operator key passed to tsugi-verify that is not a usable key is a usage error (exit 2, no report), as in the JavaScript command. Four frozen TSUGI cases added (102 in all); no existing verdict changed.

0.4.1 (2026-10-04)

Two rules made strict in step with the JavaScript verifier (verifier_version 0.1.4, see the npm SDK.md): a timestamp must be a real calendar instant (no 2026-02-30, no hour 24, no year 0000), and a signature field must be canonical standard base64. The frozen parity corpus was regenerated from the JavaScript file; two bundles that pinned the old lenient date reading are now refused with invalid_timestamp.

Where the claim stops

  • Stack depth. A document nested deep enough to exhaust a runtime's stack has no report in that runtime, and Node and Python run out at different depths (Node threw at 3,000 nested arrays inside a signed field, Python's default limit is 1,000). Real bundles are a few levels deep.
  • Sixty-four or more non-numeric hop.seq values in one bundle. The order of those keys is V8's merge sort over a comparator that cannot order them; the port reproduces V8 for up to 63 keys and raises above that instead of guessing.
  • Where the JavaScript throws, the port raises; the CLI then prints no report and exits 2 (Node exits 1 on an uncaught throw).
  • TSUGI: hosts that need UTS #46. The verifier compares and prints URL hosts as WHATWG new URL(s).host gives them. The port follows the standard for ASCII hosts, IPv4 in every form the standard reads, ports and userinfo, and raises NotReproduced (no report, exit 2) for a host that is not ASCII after percent-decoding or has an xn-- label, an IPv6 literal, or a file: URL. Lowercasing a code point this Python's Unicode tables do not assign raises the same way (Node 24 knows Unicode 16; Python 3.9 knows 13).
  • TSUGI: one refusal text depends on the Node release. For an Ed25519 public key that is not 32 bytes, Node 24 writes "Invalid keyData" and Node 22 "Ed25519 raw keys must be exactly 32-bytes". The port writes Node 24's (tsugi.KEY_LENGTH_MESSAGE); the tests compare Node 22's output after that one substitution.
  • TSUGI: --fetch-operator-key makes the same request as the JavaScript (GET <origin>/keys/operator.json, 404 read as no key) but is not part of the comparison, which runs offline.

What it does not establish

What every report says itself, accepted or refused: a signature proves who asserted, not that the assertion is true; E1 compares a provider's signed claim to a caller's signed authorization and has no side-effect oracle; R1 proves a witness is structurally distinct from the parties, not unaffiliated with them. There is no score and no allow or deny anywhere in this package. The decision belongs to whoever reads the evidence.

MUSUBI is the one part held to no second implementation, because there is none: its Python is the reference. musubi.load(name) puts the two vendored directories at the front of sys.path, since that is how the modules find each other (by bare name, as in the repository).

Reproduce

cd workers/hs-ledger/nenrin/sdk-python
pip install -e . pytest
node tests/fixtures/make_fixtures.mjs --check   # the frozen fixtures re-create byte for byte
python tests/fixtures/make_tsugi_cases.py --check   # the frozen TSUGI cases and their JavaScript output re-create
python tools/vendor.py --check                  # the agreement and MUSUBI copies are their sources
pytest tests -q -s                              # frozen, live differential, agreement, TSUGI, MUSUBI

Published from GitHub Actions with PyPI Trusted Publishing and attestations (.github/workflows/pypi-publish-nenrin-verify.yml); the parity suite runs on every change to this directory, to the JavaScript SDK, to the agreement verifier, to the TSUGI chains and to MUSUBI (.github/workflows/nenrin-verify-py.yml).

MIT. The HORIZONs Co., Ltd.

Citing

Oga, T. (2026). NENRIN provenance verifier: normative procedure (VERIFIER.md), interoperability vectors and reference verifiers (nenrin-verify 0.4.4). Zenodo. https://doi.org/10.5281/zenodo.23136978

The deposit holds the verification procedure (VERIFIER.md), the interoperability vectors and this package as published.

Metadata

Release files for nenrin-verify 0.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for nenrin-verify 0.5.0
File Size Uploaded
nenrin_verify-0.5.0.tar.gz 619.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for nenrin-verify 0.5.0
File Interpreter ABI Platform
nenrin_verify-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 1.3 MB

Release files / nenrin_verify-0.5.0.tar.gz

Download URL nenrin_verify-0.5.0.tar.gz
Size 619.1 kB
Tags Source
SHA-256 checksum
How to use checksums
dc7775b422c053c63d12e1d35adb6799fbb1b1b69e62c7ea55ac135bd50e2fdf
BLAKE2b-256 checksum
How to use checksums
6cba88e3a393c707a5970904307f5532959215cab7121268b4cccd2a513911be
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.

Transparency log

Release files / nenrin_verify-0.5.0-py3-none-any.whl

Download URL nenrin_verify-0.5.0-py3-none-any.whl
Size 718.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a32f614c8e4c749bc740f76e3f189d197733060febf85f5f21dec5c2038a1933
BLAKE2b-256 checksum
How to use checksums
9765c406bee8c4dac7b7a79bb99baf87dcf2f24f696ddc8bc3b1ada9b907c525
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 release files

0.4.9

2 release files

0.4.8

2 release files

0.4.7

2 release files

0.4.6

2 release files

0.4.5

2 release files

0.4.4

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page