---
title: "Trust Surfaces"
description: "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…"
canonical: https://novafabric.ai/docs/trust-surfaces/
last_updated: 2026-10-24
source: https://github.com/MSKazemi/novafabric/blob/main/docs/trust-surfaces.md
---

# 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](https://novafabric.ai/docs/decisions/) |
| `nova trust-radar` | The seven verification guarantees as one shape + verdict | [ADR-0173](https://novafabric.ai/docs/decisions/) |
| `nova redaction-xray` | Per-field protection state + a conservative coverage meter | [ADR-0174](https://novafabric.ai/docs/decisions/) |
| `nova passport` | A portable green/amber/red identity summary for an agent | [ADR-0149](https://novafabric.ai/docs/decisions/) |

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.

```bash
$ 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.

```bash
$ 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](#timestamp-trust-anchored-unanchored-absent-invalid). |
| 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:

- On the radar it is a `warn` axis, so a capsule with an unanchored token reads
  `partial`, never `attested`.
- `nova verify` exits 0 on an unanchored token. `nova verify --strict` exits 1,
  because `timestamp-unanchored` is listed in `unanchored_claims`.
- `nova verify --json`, `POST /api/runs/{id}/verify` and the seal-verify route
  return `timestamp_trust` next to `timestamp_ok`. `timestamp_ok: true` with
  `timestamp_trust: unanchored` means "intact, time not trusted".
- `nova evidence bind-custody --timestamp-ok` and
  `nova evidence check-admissibility --timestamp-ok` assert an `anchored`
  timestamp, not merely an intact token.

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).

```bash
$ 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:

- **green** — every component present and resolvable (carried as a ref);
- **amber** — the identity anchor exists but a component is absent or
  **opaque** (present but unattestable, e.g. an opaque lineage ancestor);
- **red** — the identity anchor is absent: no basis for a passport.

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": [...]}`.

```bash
$ 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

- [User guide — trust layer](https://novafabric.ai/docs/user-guide/) — `nova verify`, `nova redact`,
  `nova export-evidence`, the sources these projections consume
- [NovaSeal configuration](https://novafabric.ai/docs/novaseal-configuration/) — sealing, Merkle log,
  timestamps
- [Assurance cases](https://novafabric.ai/docs/assurance-cases/) — binding sealed evidence into a
  machine-checkable argument
- [Human-oversight evidence](https://novafabric.ai/docs/human-oversight/) — what a human reviewer was
  shown and whether they corrected the agent, as sealed capsule evidence
  (experimental)
- [Frontier-safety evidence](https://novafabric.ai/docs/frontier-safety/) — threshold evals, indicators
  and AI-control decisions recorded in `facets.frontier_safety` (experimental)
