Skip to content

Bundles, disclosure & verification

An evidence bundle is everything a relying party needs to check a set of records offline: the records and their Signed Statements, an RFC 6962 inclusion proof for each against the newest included tree head, the tree heads with any witness receipts, and consistency proofs between consecutive heads so the heads are shown to be one append-only history.

From the console (Compliance → Evidence Ledger) or the admin API, which delegates to the core:

Terminal window
curl -X POST http://localhost:5050/v1/admin/evidence/bundles \
-H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
-d '{
"selector": { "ai_system_id": "rg-claims-agent", "from": "2026-09-01T00:00:00Z", "to": "2026-09-19T00:00:00Z" },
"audience": "Example Audit LLP",
"closure_depth": 1,
"framework_id": "eu-ai-act"
}'
Selector Selection Meaning
{"record_ids": [...]} producer-selected a hand-picked list — the verifier is told so
{"root_task_id": "..."} contiguous every record of one run
{"ai_system_id": "...", "from": "...", "to": "..."} contiguous every record of a system in a window

closure_depth (0–8, default 1) follows chain parents that many hops. The core forces a tree head first if the newest selected record is not yet covered, so every proof is against a signed head. Limits: 10 000 records including closure, 50 MB. Bodies never enter records, so payloads: "all" is refused (409 payloads_unavailable) rather than silently degraded.

ai.brutor/evidence-bundle/v1:

{ "manifest": {
"kind": "ai.brutor/evidence-bundle", "version": 1,
"bundle_id", "root_record_id"?, "selector", "produced_at", "issuer", "log_id",
"kids": [...], "framework_id"?, "catalog_version"?,
"completeness": {
"closure_depth", "records_mode": "complete" | "declared_incomplete",
"selection": "contiguous" | "producer-selected",
"payloads_mode": "none", "suppressed_fields": [], "missing": [record_id...] },
"verification": { "self_report": { "records": n, "proofs": n, "heads": n } } },
"records": [ { "record": {...}, "statement_b64": "...", "seq": n } ],
"proofs": [ { "record_id", "leaf_index", "tree_size", "path": [hex...] } ], // against tree_heads[-1]
"tree_heads": [ { "statement_b64", "transparent_statement_b64"?,
"receipts": [ { "kind", "receipt_b64", "grade" } ] } ],
"consistency":[ { "from_size", "to_size", "path": [hex...] } ], // between consecutive heads
"prunes": [...], // declared prunes overlapping the window
"disclosures":{}, // revealed preimages (none in v1)
"extensions": { "ai.brutor/evidence-report/v1": {...} }, // digest-covered, uninterpreted by others
"countersignatures": [] } // excluded from the digest
Member Purpose
manifest.selector, selection what was asked for, and whether it is a contiguous range or a producer’s choice
manifest.completeness.missing chain parents neither included nor followed. Missing is listed, never dropped; a non-empty list forces records_mode: "declared_incomplete"
records[] each record with its Signed Statement and log sequence
proofs[] one inclusion proof per record against the newest head
tree_heads[] the first head covering each record plus the newest, with receipts and the Transparent Statement (receipts at label 394) when witnessed
consistency[] RFC 6962 consistency proofs between consecutive included heads
prunes[] declared prunes in the window — gaps with their reason
extensions e.g. an Evidence Report’s rows
countersignatures[] later signatures (an auditor’s) that must not change the identity the producer sealed

Bundle digest = hex(SHA-256(JCS(bundle minus "countersignatures"))). The core seals it as an ai.brutor.bundle record, so the digest a verifier compares against is itself in the log.

Three completeness claims, reported separately

Section titled “Three completeness claims, reported separately”
  1. Graph closure — every chain parent is included or listed in missing (records_mode, checked by closure_parent_dropped / records_mode_mismatch).
  2. Interval coverage — selection: "contiguous" states that every record of the run or window was selected; producer-selected states that it was not, and the verifier is told so.
  3. Per-record membership — each record is in the tenant log at the newest head (inclusion proof).

Each head’s receipts carry their own grade, so a bundle is only as witnessed as its heads; until a receipt verifies, the claims above are the producer’s own. None of them proves there was no action the gateway never saw: integrity is not completeness. Scope is a human-signed census.

Producing a bundle for a named audience seals a disclosure: an ai.brutor.disclosure record (the audience enters only as a digest) and a log entry of kind disclosure. completeness is contiguous for a range and always producer-selected for an explicit list. Viewing is not disclosing — only a bundle built with an audience creates one.

An answer to an auditor or regulator is exactly one of three things:

Answer What the platform does today
record a bundle; with an audience, the disclosure is sealed into the tenant log
refusal a request that cannot be met honestly is refused, not degraded — e.g. payloads: "all" → 409 payloads_unavailable
absence no record matches the selector → 404 no_records; an absence is an answer, not silence

Only the record answer is sealed into the log today; record refusals and absences you give to an authority in your own correspondence.

List disclosures with GET /v1/admin/evidence/disclosures (Evidence Ledger → Disclosures).

Integrity can be checked by anyone. Record semantics are checked by Brutor’s published rules, which a third party can reimplement from this page and the vector corpus.

Everything cryptographic is a published standard:

  1. Parse statement_b64 as a COSE_Sign1 (RFC 9052). Check the Ed25519 signature under a key from the public key document that you pinned out-of-band.
  2. Check the payload equals SHA-256 of the RFC 8785 bytes of the record without record_id (RFC 9995 hash envelope, label 258 = −16).
  3. Compute the leaf SHA-256(0x00 ‖ statement bytes) and check its RFC 6962 inclusion proof against the newest head’s root.
  4. Verify each head’s signature and payload, and the consistency proofs between heads.
  5. Verify each scitt receipt as an RFC 9942 COSE Receipt over the head statement, under the witness’s pinned key; RFC 3161 tokens with openssl ts -verify.

The corpus ships vectors/interop/verify_with_scitt_cose.py, which does exactly this with the upstream scitt-cose Python package — nothing from Brutor — and optionally the Go verifier.

Fixed order, structured result, never throws, no clock, no network, no model. Only error findings gate ok; warning and info never do.

# Rule Finding codes (severity)
1 Structural — object, schema, required members (schema action_id kind operator system timestamp verdict), types, digest hygiene (no floats, safe integers), only admitted members, no never-enters member at any depth not_an_object, unsupported_schema, missing_required_field, field_not_string, field_not_bool, field_not_array, block_not_object, record_id_malformed, unadmitted_field, never_enters_member, kind_invalid, approver_invalid, float_in_digest_field, unsafe_integer_in_digest_field, not_canonicalizable, reference_malformed (error)
2 Identity — recompute record_id record_id_mismatch, record_id_uncomputable (error)
3 Confirmed-effect binding — confirmed needs a response_digest; planned carries none confirmed_without_response, planned_with_digests (error)
4 Verdict / effect orthogonality — a never-dispatching verdict must derive not_applicable verdict_effect_conflict (error)
5 Attestation presence — a dispatched effect carries attestation attestation_missing (error), attestation_on_planned (info)
6 Chain — parent present in the store, well-formed, relation set, not duplicated as a reference; concurrent supersedes chain_parent_missing, chain_parent_malformed, reference_duplicates_chain_parent (error); chain_check_store_level, concurrent_supersedes (info)
7 Claims never above derived — reserved; v1 records carry no claimed modes —
8 Unknown vocabulary — a verdict, decision, effect status or relation this verifier does not know unknown_vocabulary_value (info — never grades up)
+ Defensive honesty — human_disposed: true with a non-human approver (the builder already refuses to sign such a record) dishonest_human_disposed (warning)
Code Severity Meaning
bundle_not_object error not a JSON object
bundle_digest_mismatch error computed digest ≠ the expected (sealed) digest
digest_uncomputable error the bundle cannot be canonicalised
manifest_invalid error missing manifest, or wrong kind / version
records_mode_mismatch error records_mode contradicts missing
payloads_mode_unsupported error v1 bundles carry no payloads
kid_not_pinned warning no trusted keys supplied
structure_invalid error a member has the wrong shape
untrusted_kid error a statement’s key is not in the trust set
head_invalid, head_log_mismatch error a head fails its signature or payload checks, or names another log
transparent_statement_mismatch error the Transparent Statement is not the head statement plus receipts
heads_not_ascending error heads out of order
consistency_missing, consistency_invalid error consecutive heads not proven to be one history
duplicate_record error a record appears twice
record_semantic_error error a record fails a semantic rule above
record_statement_invalid, statement_payload_mismatch error bad signature, or the statement does not commit to this record
proof_for_unknown_record, proof_wrong_head, leaf_index_mismatch, proof_invalid, proof_missing error inclusion-proof failures
closure_parent_dropped error a chain parent is neither included nor listed as missing
missing_listed_but_present warning listed as missing but included
record_not_in_selector error an explicit-id bundle contains a record neither selected nor within the closure
self_report_mismatch warning the manifest’s counts disagree with the contents

brutor-verify adds: input_invalid, kid_not_pinned, untrusted_kid, kid_retired (info), kid_invalid, statement_malformed, signature_invalid, statement_payload_mismatch, head_malformed, head_signature_invalid, head_consistency_invalid, previous_head_invalid, head_log_mismatch, head_not_consistent, consistency_undetermined, receipt_invalid, receipt_unpinned (warning — not evidence of forgery), receipt_not_checked (info), receipt_kind_unknown.