Trust Surfaces

Four read-only commands that turn a capsule's trust evidence into something a human can read at a glance: the Merkle proof tree, the trust-attestation radar, the redaction/secret-scan X-Ray, and the portable agent passport. All four are pure projections — they read verification output NovaFabric already produces, mutate nothing, change no schema, and add no dependency.

Status: experimental (v0.61, shipped 2026-07-16).

Updated 2026-07-31 (v0.98.0): two of the three interactive dashboard views the ADRs describe now exist — the SVG radar glyph (ADR-0173) and the redaction heat-overlay (ADR-0174), both in the dashboard's Seal tab (Provenance & Trust group), reading the already-shipped GET /api/runs/{run_id}/trust-radar and …/redaction-xray endpoints. The proof-tree explorer (ADR-0172, nova merkle-tree) remains future design: it has no serve endpoint yet, so there is nothing for a view to read.

The visual half preserves the same honesty contract as the CLI: a guarantee the capsule cannot evidence is drawn as a hollow, dashed tick and is excluded from the filled claim polygon — an unsealed capsule can never be made to look like a failed one, and the shaded area only ever claims what was actually verified. The X-Ray renders paths and states only; per ADR-0174 §1 the payload type carries no field value to render.

nova trust-radar and nova redaction-xray also accept --capsule <dir> and derive their input from the capsule directly. nova merkle-tree and nova passport take a small JSON document you assemble from existing outputs (seal metadata, log-entry.json).

Command Shows ADR
nova merkle-tree Which sealed fields roll up to which root — the proof structure ADR-0172
nova trust-radar The seven verification guarantees as one shape + verdict ADR-0173
nova redaction-xray Per-field protection state + a conservative coverage meter ADR-0174
nova passport A portable green/amber/red identity summary for an agent ADR-0149

All four honor the redaction invariant (ADR-0009) at the type level: they carry field paths, hash prefixes, and refs — never field values, finding bodies, or component contents.


nova merkle-tree (ADR-0172)

Renders the Evidence Provenance Merkle proof tree from a sealed capsule's leaf hashes — leaf → intermediate → seal-root → tsr. The layers are enumerated by the NovaSeal log's own tree code (trust/novaseal/merkle.py — not the Evidence Bundle root in evidence/merkle.py), so the projected tree matches what was signed provided you pass the right root_version. A supplied sealed_root is compared against the recomputed root: match ⇒ verified, mismatch ⇒ flagged.

Input: a JSON object with leaf_hashes (required) plus optional leaf_labels (field paths, never values), sealed_root, tsr_hash, root_version (1 = legacy pairwise-pad, 2 = RFC 6962) and tree_size. ⚠ An absent root_version means 1. Every NovaSeal log created since ADR-0313 writes v2 roots, so copy root_version from log-entry.json — otherwise a genuine v2 root reports a mismatch, which reads like tampering.

$ nova merkle-tree tree.json --capsule-id run-2041
Merkle proof tree: sealed   capsule: run-2041
  ✓ leaf         d2dbf0..6c8188 (verified)  metadata.command
  ✓ leaf         4140bf..00e855 (verified)  env_lock.python
  ✓ leaf         649837..fe0f9a (verified)  events[0].digest
  ✓ leaf         9fde56..502454 (verified)  outputs.stdout_sha256
    ✓ intermediate c76c13..8d9831 (verified)
    ✓ intermediate 6d001d..2254bf (verified)
      ✓ seal-root    1313c9..66e0be (verified)

--json emits the ProofTree model instead. Exit codes: 0 — rendered (sealed+verified, or unsealed); 1 — seal-root mismatch (tamper); 2 — missing/malformed input. Without a sealed_root the tree renders unsealed with every node unverified. A tsr_hash node renders as unverified in v0 — RFC 3161 verification is out of scope for this projection.

nova trust-radar (ADR-0173)

Projects a capsule's seven verification guarantees onto a fixed-axis radar — signature · timestamp · log_integrity · redaction_coverage · secret_scan · policy · eval_gate — in a fixed order so shapes are comparable across capsules. Booleans plot as 0/1; redaction_coverage is a clamped ratio; an absent/null guarantee becomes an n/a axis, kept distinct from a fail (an unsealed capsule has no signature_ok; that is not a failure).

Input: a JSON object of the guarantees (signature_ok, timestamp_ok, log_integrity_ok, redaction_coverage, secret_scan_clean, policy_pass, eval_gate_pass), or --capsule <dir>.

⚠ Given a JSON object, the radar is a projection of those seven keys only — it is not verification. Whoever wrote the JSON chose the values. With --capsule <dir> (and in the dashboard Seal tab) the seal axes come from nova verify's own checks, below. For a release gate, use nova verify's exit code; attested describes the axes, not --strict anchoring.

$ nova trust-radar verify.json --capsule-id run-2041
Trust radar: PARTIAL   capsule: run-2041
  ● Signature            1.00  (ok)
  ● Timestamp            1.00  (ok)
  ● Log integrity        1.00  (ok)
  ◐ Redaction coverage   0.97  (warn)
  ● Secret scan          1.00  (ok)
  ● Policy               1.00  (ok)
  · Eval gate             n/a  (na)

Verdicts: attested (sealed, every applicable axis ok), partial (sealed, some axis short of 1), critical (a seal-integrity anchor — signature or log-integrity — failed), unsealed (no signature guarantee; an unsealed capsule can never be attested, however clean its policy and eval axes). Exit codes: 0 for attested/partial/unsealed (informational), 1 only on critical, 2 on bad input.

--capsule <dir> runs nova verify's own checks (works today). The radar with --capsule, GET /api/runs/{id}/trust-radar, the dashboard Seal tab and POST /api/runs/{id}/verify all call the same function that nova verify <capsule-dir> reports from. A capsule that nova verify rejects is therefore never attested. The three seal axes mean:

Axis ok only when
Signature the DSSE signature verifies, and binds this directory (ADR-0251): the manifest the capsule was sealed with (capsule.yaml) exists and equals the signed payload, no other manifest-named file (capsule.json) disagrees with it, every evidence_digests file still hashes to its signed digest, no file a signed *_ref names was added after sealing, and log-entry.json names this payload. A signer chain configured in novaseal.yaml (ca_bundle) must also validate. A capsule edited after sealing, whose capsule.yaml was renamed or replaced by a capsule.json, carrying another capsule's .seal/, or whose .seal/manifest.dsse is malformed, fails this axis and the verdict is critical.
Timestamp an RFC 3161 token is present, intact, and its TSA chain reaches an anchor you supplied (tsa_ca_certs in novaseal.yaml): timestamp_trust is anchored. An intact but unanchored token plots 0.5 (warn), no token is n/a, a failed token is 0 (fail). See Timestamp trust.
Log integrity Merkle inclusion was established by the carried proof or the sealer's log. An entry that neither could check (log_inclusion: not-checked, which nova verify accepts) plots 0.5 (warn, verdict partial); an entry that failed the check is 0 (fail, verdict critical).

Like nova verify, the radar verifies from the capsule alone when this machine has no novaseal.yaml; the profile adds the sealer's log and the operator anchors. A novaseal.yaml that fails to load leaves the seal axes n/a (verification could not run), never ok. POST /api/runs/{id}/verify reports the binding as capsule_binding_ok, and its valid is the verdict nova verify exits on.

Timestamp trust: anchored, unanchored, absent, invalid

Works today. A timestamp has two separate questions: is the RFC 3161 token intact (timestamp_ok), and is its time trusted (timestamp_trust)? Anyone can mint a TSA certificate and sign any genTime, so an intact token alone proves nothing about when the capsule was sealed. timestamp_trust takes one of four values:

timestamp_trust Meaning nova verify prints Radar axis
anchored The token verified and its TSA chain reached a TSA anchor you supplied (--tsa-ca-bundle, or tsa_ca_certs in novaseal.yaml). Trusted time. Timestamp (RFC 3161): OK 1.00 (ok)
unanchored The token is intact and bound to this envelope, but no TSA anchor was consulted. Its genTime is an unverified claim. UNANCHORED, with the claimed genTime and TSA subject 0.50 (warn)
absent No token. Timestamping is opt-in (ADR-0292), so no time is claimed. NOT PRESENT n/a
invalid A token is present and failed integrity, or failed the anchored chain check. Timestamp (RFC 3161): FAIL 0.00 (fail)

unanchored never counts as ok:

With TSA anchors configured, a missing or empty token fails, because the token is not covered by the DSSE signature. Until 2026-10-22, nova verify printed an intact but unanchored token as OK.

Before 2026-10-22 the radar ran only the signature, timestamp and log checks, so a capsule tampered after sealing could show attested. Until 2026-10-23 a sealed capsule whose capsule.yaml was renamed to capsule.json and edited still read attested, while nova query and the runs list served the edited status: the missing capsule.yaml counted as "not present" and capsule.json as an extra file. Both now fail the Signature axis, and every reader of a sealed capsule's status takes it from the signed payload.

nova redaction-xray (ADR-0174)

Projects a capsule's field-protection state — which fields are clear, redacted, secret_scrubbed, never_captured, or unknown — with per-state counts and a coverage meter. No field value is ever printed; the report model has no value field at all, so a value handed in alongside a record cannot reach the output.

Input: a JSON object with either fields ([{path, state}]) or raw findings (MaskingPipeline finding records, adapted automatically).

$ nova redaction-xray xray.json --capsule-id run-2041
Redaction X-Ray: coverage 67% of sensitive surface   capsule: run-2041
  counts: clear=1  redacted=1  secret_scrubbed=1  never_captured=1  unknown=1
  ■ request.headers.authorization  (secret_scrubbed)
  ▨ request.body.messages[0].content  (redacted)
  · response.body.usage.total_tokens  (clear)
  ░ env.OPENAI_API_KEY  (never_captured)
  ? response.body.choices[0].text  (unknown)

Coverage is deliberately conservative: (redacted + secret_scrubbed) / (redacted + secret_scrubbed + unknown) — a field with absent scan metadata (unknown) lowers coverage instead of being asserted clear. Exit codes: 0 rendered, 2 bad input.

nova passport (ADR-0149)

Projects the identity, lineage, AIBOM, eval-card, package, and delegation references NovaFabric already produces for an agent into one portable passport document, verifiable offline:

The passport never claims ancestry NovaFabric cannot attest — an opaque ancestor is amber, never dressed up as green. Components carry a ref/digest only, never the component body.

Input: {"agent_ref": ..., "present": {component: ref}, "opaque": [...]}.

$ nova passport issue refs.json
agent: agent:support-triage@1.4.0
status: (amber)
  identity    present  sha256:2f4a1c
  lineage     opaque   —
  aibom       present  sha256:77aa0b
  card        present  sha256:e01d9f
  package     present  sha256:5b2c44
  delegation  present  sha256:0d3e21

$ nova passport issue refs.json --json > agent-passport.json
$ nova passport verify agent-passport.json
...
verified — status (amber) matches

verify re-derives the verdict offline and confirms it matches the document. Exit codes: issue — 0 rendered, 2 malformed input; verify — 0 match, 3 status mismatch, 2 malformed input.

Honest limits: this slice emits an unsigned projection (signed: false). Sealing the passport through the shipped seal path, loading component refs directly from a sealed capsule (--asset), and the broader ADR-0149 interop facets (NF-171..178, NF-180) are future design.


See also