Trust, secret-scan and assurance commands

Part of the NovaFabric CLI reference. Both nova and novafabric run the same binary.

Trust layer commands (v0.4)

nova scan-secrets <capsule>

Read-only inspection of a capsule's redaction-proof.json. Reports findings without modifying the capsule. Designed as a CI gate.

nova scan-secrets .novafabric/runs/01HXAY7M5JZ8R7K4P9DPBYK2WX/
nova scan-secrets <capsule> --fail-on critical    # exit 2 on any critical finding
nova scan-secrets <capsule> --fail-on high        # exit 2 on critical or high
nova scan-secrets <capsule> --json                # emit the proof as JSON to stdout

Options:

Exit codes: 0 (clean or below threshold), 1 (missing proof / invalid input), 2 (threshold exceeded).


nova assure <capsule-path> (v0.25, E-10)

Run OWASP Top 10 for LLM (2025) evidence checks against a captured capsule.

nova assure .novafabric/runs/01HXAY7M5JZ8R7K4P9DPBYK2WX/
nova assure .novafabric/runs/01HXAY7M5JZ8R7K4P9DPBYK2WX/ --format json
nova assure .novafabric/runs/01HXAY7M5JZ8R7K4P9DPBYK2WX/ --format json > assurance.json

Performs 10 deterministic checks against capsule artifacts:

Check OWASP Category What is verified
LLM01 Prompt Injection redaction-proof.json captured
LLM02 Sensitive Info Disclosure secrets_found == 0 in redaction proof
LLM03 Supply Chain env.lock + novafabric_version recorded
LLM04 Data/Model Poisoning total input tokens < 100,000
LLM05 Improper Output Handling tool-calls.jsonl present when tool_call_count > 0
LLM06 Excessive Agency tool/model call ratio < 10 (warn) / < 50 (fail)
LLM07 System Prompt Leakage no system_prompt in DEBUG model-call records
LLM08 Vector/Embedding Weakness replay.yaml policy captured
LLM09 Misinformation trace.jsonl has ≥1 span
LLM10 Unbounded Consumption duration_ms < 1,800,000 (warn) / < 3,600,000 (fail)

Options:

Exit codes: 0 = all PASS/WARN/SKIP, 1 = any FAIL, 2 = capsule path not found.


nova redact <capsule>

Re-scan a capsule and update its redaction-proof.json. Two modes:

Mode 1 — metadata-only (no re-scan, edits the existing proof in place):

nova redact <capsule> --mark-unsafe-skip <finding_id> --rationale "test fixture"
nova redact <capsule> --clear-unsafe-skips

Mode 2 — re-scan (default):

nova redact <capsule>
nova redact <capsule> --strategy-override openai-api-key:hash
nova redact <capsule> --strategy-override pii-email:drop

Options:

unsafe_skips block nova export-evidence unless --allow-unsafe-skips is passed.


nova comment add | list (experimental, ADR-0121)

Append-only human annotations on capsule evidence — a reviewer's note, an auditor's rationale, a "this dataset was mislabelled upstream" flag — stored as one JSON line per comment in an optional comments.jsonl inside the capsule, mirroring scores.jsonl (ADR-0099). Comments are asynchronous evidence annotations, not live chat: they are immutable, travel with the capsule, work fully offline, and are covered by the capsule Merkle root at Evidence-Bundle time exactly like any other capsule file. A capsule without comments.jsonl stays valid (additive-only; the Run Capsule schema is untouched). Record contract: schemas/comment.schema.json.

There is no in-place edit and no destructive delete (append-only invariant): an edit is a new comment with --reply-to <comment_id>; a delete is a tombstone comment (--tombstone --reply-to <id>) — the retracted bytes stay in the file and list simply hides them by default.

Scope: single capsule.

nova comment add --subject <capsule-dir> --body "retrieval pulled a stale doc — blocking promotion"
nova comment add --subject <capsule-dir> --body "confirmed fixed" --reply-to 01HXB0K3M7QM4YZ2K7N9DPBYK2
nova comment add --subject sha256:<hex> --capsule <capsule-dir> --kind span --body "this span is the culprit"
nova comment add --subject <capsule-dir> --tombstone --reply-to 01HXB0K3... --body "retracting: false alarm"
nova comment list --subject <capsule-dir>            # write order; tombstoned comments hidden
nova comment list --subject <capsule-dir> --all      # full audit trail, raw records
nova comment list --subject <capsule-dir> --json

--subject accepts a capsule directory (resolved to a stable content-addressed root digest that excludes the annotation stream itself, so every comment on a capsule shares one subject) or a sha256:<hex> digest (then --capsule <dir> locates comments.jsonl). asset://<type>/<name>@<version> subjects annotate registry assets rather than capsules. They are stored in the registry's asset_comments table (assets have no capsule log), but are the same records with the same append-only semantics — threads, tombstones and the secret gate behave identically:

nova comment add --subject asset://model/summarizer@1.2.0 --kind asset \
  --body "eval regression on the finance set — do not promote"
nova comment list --subject asset://model/summarizer@1.2.0

--subject and --kind must agree: an asset:// ref requires --kind asset, and --kind asset requires an asset:// ref (exit 2 otherwise).

nova comment thread <comment-id> --subject <capsule-dir> resolves the reply chain containing a comment, root first, indenting each level (or --json for the raw array):

nova comment thread 01HX... --subject <capsule-dir>
nova comment thread 01HX... --subject <capsule-dir> --json

Resolution is bounded and defensive, because an append-only log can legitimately hold malformed links: a reply whose parent is missing is treated as an orphan root, not an error, and a reply cycle is reported (exit 1) rather than looped over. An unknown comment id exits 1.

Secret hygiene is mandatory (ADR-0009): the body passes the same secret-scan rules as every other capsule text before storage. A body that trips a secret pattern is refused (exit 3, and the secret is never echoed back); --redact masks the match in place ([REDACTED:<rule_id>]) and records redaction_applied: true on the comment. A body emptied by redaction is refused.

Options (add):

Options (list): --all (raw audit trail, no tombstone folding), --json.

Exit codes: 0 (ok), 1 (invalid comments.jsonl), 2 (usage / unsupported subject), 3 (body refused by the secret gate).


nova annotate (experimental, ADR-0118)

Human annotation queues: route review subjects (capsules or spans) to human reviewers, track each item through pending → assigned → completed (with an optional maker-checker checker_pending step), and land every completed annotation as a typed, HUMAN-source Score in the subject capsule's append-only scores.jsonl — the exact evidence path every other score uses (ADR-0099). The metrics a reviewer grades are score configs (ADR-0117), referenced by name; a queue cannot be created until each criterion is registered with nova eval score config add. Queue/item workflow state lives in the local registry SQLite DB (local-first, offline); annotation is entirely off the live workload path. Record contracts: schemas/annotation-queue.schema.json and schemas/annotation-queue-item.schema.json.

nova eval score config add --name factuality --value-type boolean --description "Factually correct?"
nova annotate queue create --name hallu-review --criteria factuality [--require-checker] [--policy manual]
nova annotate queue add hallu-review --capsule <capsule-dir>          # enqueue the capsule
nova annotate queue add hallu-review --capsule <dir> --subject sha256:<hex> --kind span
nova annotate queue populate hallu-review [--dry-run] [--json]        # enqueue everything matching the selector
nova annotate queue list [--json]                                     # queues + per-state progress
nova annotate queue show hallu-review [--json]
nova annotate next --queue hallu-review --as reviewer:a               # claim (round-robin)
nova annotate next --item <item_id> --as reviewer:a                   # claim a named item (manual)
nova annotate submit <item_id> --score factuality=true [--as reviewer:a] [--skip-criterion NAME]
nova annotate confirm <item_id> --as reviewer:b                       # checker step (SoD)
nova annotate skip <item_id> --note "out of scope"                    # terminal, writes no score

queue populate enqueues every stored capsule matching the queue's subject_selector (all present keys ANDed). It is idempotent — a subject already queued is skipped, so re-running after new capsules land adds only the new ones, which makes it safe to schedule. A sample fraction is applied deterministically (hash of the subject digest), so repeat runs choose the same subjects: an auditor asking "why was this run reviewed and that one not?" gets a stable answer rather than "chance". A span-scoped queue refuses auto-population, since spans are not enumerable from the capsule store.

submit validates every queue criterion against its score config before any write (all-or-nothing: a bad value, missing criterion, or missing capsule appends nothing and the item stays assigned, retryable), then appends one HUMAN score per criterion with evaluator_id = the assignee and eval_card_digest = the config's content_digest (content-addressed provenance). The submission — and, on --require-checker queues, the checker's confirmation — is Ed25519-signed with the reviewer's keyring key (ADR-0058 paths); fingerprints and signatures are carried in the item record's extensions. The checker's identity and key fingerprint must both differ from the maker's (separation of duties, ADR-0003 pattern). A bare --capsule enqueue uses the capsule's content-addressed root digest excluding the annotation streams (scores.jsonl, comments.jsonl), so successive review rounds share one stable subject.

Planned (not yet implemented): evidence-bundle/NovaSeal sealing of completed scores (--seal is recorded and warns; ADR-0118 P5); server-mode multi-user assignment. (Corrected: automatic queue population by evaluating subject_selector over stored capsules — described above as nova annotate queue populate — shipped in ADR-0118 P2; an earlier version of this note called it not-yet-implemented, which was stale.)

Exit codes: 0 (ok, including an empty-queue next), 1 (validation / state / separation-of-duties refusal), 2 (usage).


nova score submit (experimental, ADR-0119)

Submit one externally-computed evaluation score — from a CI job, a third-party LLM-as-judge, a human tool, a batch scorer — into a target capsule's append-only scores.jsonl. This is the documented ingest surface over the same evidence-grade Score record every other score uses (ADR-0099): NovaFabric records the value, it never runs the evaluator (no model call, no paid API). Fully offline: no server, no internet. The same validation core backs the SDK (novafabric.scores.submit) and the optional server endpoints (POST /api/runs/{run_id}/scores on the dashboard, POST /v0/capsules/{run_id}/scores on nova server). Contracts: schemas/score-submission-request.schema.json and schemas/score-submission-response.schema.json.

nova score submit --capsule <capsule-dir> \
  --name answer_correct --value 0.87 --value-type numeric \
  --evaluator "ci://acme/repo#judge@v3" --source code \
  --subject sha256:<hex> --eval-card sha256:<hex> \
  [--score-id <ulid>] [--supersedes <score_id>] [--run-id <ulid>] [--json]

Validation is fail-closed — on any rejection nothing is written:

On success the appended (or replayed) record is echoed to stdout as JSON (--json wraps it in the full {score, idempotent_replay, config_bound} envelope). On rejection a structured {"error": <code>, "message": …} line goes to stderr. Exit codes: 0 (ok, including an idempotent replay), 1 (rejection), 2 (usage).


nova export-evidence <capsule> --output <bundle.zip>

Build a signed Evidence Bundle ZIP per ADR-0011. Signs with a local ed25519 key; optionally publishes the DSSE envelope to a Rekor transparency log when --sigstore and NOVA_REKOR_URL are both set.

nova export-evidence <capsule> --key ~/.novafabric/keys/ed25519.pem --output evidence.zip
nova export-evidence <capsule> --key key.pem --output e.zip --allow-unsafe-skips

Options:

When the capsule has an energy-receipts.jsonl stream, a signed PREDICATE_ENERGY energy attestation is added to the bundle automatically (experimental, ADR-0093).

Generate a fresh keypair from Python:

from pathlib import Path
from novafabric.evidence.signing import generate_keypair
generate_keypair(Path("./keys"))    # writes ed25519.pem + ed25519.pub.pem

Bundle layout:

evidence.zip
├── manifest.json                       # index + manifest_hash
├── run-capsule/                        # byte-identical capsule copy
├── lineage-subgraph/edges.jsonl        # lineage for this run
├── attestations/                       # in-toto Statement v1, DSSE-enveloped
│   ├── run.intoto.json
│   ├── redaction.intoto.json
│   └── lineage.intoto.json
├── signatures/                         # raw signature + public key per envelope
│   ├── *.sig    (raw ed25519 signature bytes)
│   └── *.cert   (PEM public key)
├── schemas/                            # all 10 JSON schemas vendored
└── README.md                           # human-readable verification recipe

Verification requires only sha256sum and an ed25519 verifier. The vendored schemas make the bundle a time capsule — verification works in 2031 against the same schemas the bundle was built with.


nova export --html <capsule-dir>

experimental — Shareable single-file offline capsule viewer (ADR-0140). Renders one capsule's non-sensitive summary as exactly one self-contained HTML file — inline CSS, no JavaScript, zero external requests — that a recipient with no NovaFabric install and no server opens in any browser from file:// (email it, attach it to a ticket, carry it on a USB stick).

nova export --html .novafabric/runs/01HX.../
nova export --html .novafabric/runs/01HX.../ -o run.html --title "Nightly agent run"

Options:

The page shows the capsule header (run id, capture mode, run window, agent identity and capsule hash when the capsule carries them), model calls (model, tokens, latency, status), tool calls (tool, mutation_class, status, duration), eval scores from scores.jsonl, and lineage references from lineage.jsonl. The underlying CapsuleView summary JSON (schemas/capsule-view.schema.json) is embedded inline in a <script type="application/json" id="capsule-view-data"> block for View-Source inspection.

Guarantees and limits:

Exit codes: 0 (file written), 1 (capsule not found), 2 (--html not passed).


nova evidence (experimental, v0.50.0, ADR-0087)

Audit-grade evidence operations beyond the signed bundle: completeness assertion, criterion→evidence bindings, and re-performance attestation. All three are additive and optional — pre-existing bundles still verify.

# What does this capsule claim to contain? (per-stream counts, drop counters,
# capture level, time window, active hooks)
nova evidence completeness <run-id-or-capsule-path>
nova evidence completeness <capsule> --key ed25519.pem -o completeness.intoto.json

# Bind audit-profile controls to the capsule facts that evidence them
nova evidence bind <capsule> --profile eu-ai-act-high-risk
nova evidence bind <capsule> --profile gdpr --key ed25519.pem

# Replay the run and emit a DSSE-signed re-performance attestation
nova evidence attest-replay <capsule> --key ed25519.pem --mode mocked

# Also emit a signed determinism certificate (ReplayAttestation, ADR-0094 B)
# and anchor the attestation digest into the capsule's accountability ledger
nova evidence attest-replay <capsule> --key ed25519.pem --mode exact --certify --anchor

Options:

Without --certify/--anchor, attest-replay behavior is unchanged (including exit 2 on mismatch).


nova verify <capsule>

Verify a capsule's cryptographic seal — and that the capsule on disk is the one that was sealed. Needs only the capsule: no NovaSeal configuration and no Merkle log are required, so an independent auditor can verify on a fresh machine; a configured novaseal.yaml adds a check against the sealer's own log. experimental — shipped v0.10; see v0.10.0 release notes.

nova verify .novafabric/runs/01HXAY7M5JZ8R7K4P9DPBYK2WX/
nova verify <capsule> --seal-config ~/.novafabric/novaseal.yaml
NOVAFABRIC_SEAL_CONFIG=./novaseal.yaml nova verify <capsule>

Checks (all must pass for exit 0; a check that could not run is printed ⊘ … NOT CHECKED/NOT PRESENT and does not fail):

Check What is verified
Signature DSSE envelope signature — ECDSA P-256 against the certificate embedded in the envelope
Timestamp RFC 3161 token, offline: the token covers SHA-256 of the whole DSSE envelope (signature included — the token is computed over the signature, so it is outside it by design, ADR-0030). Checked positionally: TSTInfo messageImprint equals that hash, messageDigest equals the hash of the TSTInfo, the CMS signature verifies under the embedded TSA certificate bound by ESSCertID. A response with no TSA-signed token fails. Who the TSA is needs --tsa-ca-bundle (below). A token the strict parser cannot read is reported structural check only
Merkle log log-entry.json's entry must hash to its leaf_hash and name the signed payload's capsule id; then inclusion is checked by every means available — the inclusion proof carried in log-entry.json (seals from v0.103) must recompute the recorded root_hash, and when the local (sealer's) log holds the entry it must sit at the recorded index under the log's root. A capsule with neither (sealed by ≤ v0.102.x, verified away from the sealer's log) prints ⊘ Merkle log inclusion: NOT CHECKED — log not available and does not fail. The carried proof's tree head is not independently anchored (not signed or witnessed)
Manifest binding capsule.yaml on disk matches the signed DSSE payload, and capsule_id is recomputed from that payload rather than read from log-entry.json (ADR-0251)
Evidence binding every file listed in the manifest's evidence_digests still hashes to the value that was signed (ADR-0251)

The first three prove a manifest was signed by a key at a time. The last two prove that manifest describes this directory. Without them, editing a recorded token count in model-calls.jsonl — or the status field in capsule.yaml itself — left all three original checks green.

Options:

Exit codes: 0 (all checks pass), 1 (any check fails or .seal/ missing).

Example output (all passing):

NovaSeal verification: 01HXAY7M5JZ8R7K4P9DPBYK2WX
  ✓ Signature (DSSE ECDSA P-256): OK
    Intent: authored
  ✓ Timestamp (RFC 3161): OK
  ✓ Merkle log inclusion: OK
  ✓ Manifest binding (capsule.yaml == signed payload): OK
  ✓ Evidence binding (per-file sha256): OK

signature_ok=True, timestamp_ok=True, log_integrity_ok=True

Example output (a modified capsule):

  ✓ Signature (DSSE ECDSA P-256): OK
  ✓ Timestamp (RFC 3161): OK
  ✓ Merkle log inclusion: OK
  ✓ Manifest binding (capsule.yaml == signed payload): OK
  ✗ Evidence binding (per-file sha256): FAIL
    modified: model-calls.jsonl

Capsules sealed before evidence_digests shipped print ⊘ Evidence binding: NOT PRESENT (sealed before evidence_digests) and still exit 0. They are not invalid, they are unbound — and a check that did not run is never reported as OK.

Files added after sealing — nova export-c2pa writes c2pa-manifest.json, for instance — are listed as not covered by the seal but never fail the capsule. Adding a derived artifact next to a capsule is normal; failing on it would make the check unusable.

Capsules produced without a NovaSeal config have no .seal/ directory; nova verify exits 1 with an informational message (not an error — unsigned capsules are valid).

Batch export manifests: when the argument is an export-manifest.json file (instead of a capsule directory), nova verify runs the offline batch-export verification instead — DSSE ed25519 signature, batch digest, and every member's bytes at the destination (--public-key <pem> required; optional --dest override). See nova export-blob (experimental, ADR-0141).

Dashboard equivalent: SealTab → Capsule Integrity Verify panel (run ID input + DSSE/TSA/Merkle result display).

NovaSeal configuration (~/.novafabric/novaseal.yaml):

profile: local        # "local" | "aws_kms" | "azure_kv" | "gcp_kms"
key_path: ~/.novafabric/seal.key    # ECDSA P-256 PEM private key
cert_path: ~/.novafabric/seal.crt   # X.509 PEM certificate
tsa_url: https://freetsa.org/tsr    # opt-in, no default (ADR-0292); FreeTSA for dev, QTSP for EU regulated
merkle_db: ~/.novafabric/novaseal-merkle.db  # SQLite Merkle log (default)

Key environment variables: NOVAFABRIC_SEAL_CONFIG (path to yaml), NOVAFABRIC_SEAL_DB_PATH (Merkle DB override). Full configuration reference including Docker/SLURM patterns and path resolution order: see docs/novaseal-configuration.md.

Generate a test key pair:

openssl ecparam -name prime256v1 -genkey -noout | \
  openssl pkcs8 -topk8 -nocrypt -out ~/.novafabric/seal.key
openssl req -new -x509 -key ~/.novafabric/seal.key -days 365 \
  -out ~/.novafabric/seal.crt -subj "/CN=NovaSeal-Local"

nova verify-envelope <envelope.json> --key <pem>

Verify the Ed25519 signature on a standard outer envelope — a DSSE envelope produced by NovaFabric's envelopes/ emitters (Evidence Bundle wrap, in-toto capsule Statement, or SLSA provenance). Gives the same verdict a third party gets from stock cosign verify-blob-attestation, with no NovaFabric dependency on the verifier side. experimental — NF-029/030/031, ADR-0096.

nova verify-envelope bundle.dsse.json --key seal.pub.pem      # public-key PEM
nova verify-envelope capsule.intoto.json --key seal.key       # private-key PEM also accepted

Options:

Exit codes: 0 (signature verifies), 1 (signature failure, tampered payload, or malformed envelope), 2 (key is not an Ed25519 key / usage error).

The envelope's inner artifact is carried verbatim as the DSSE payload (wrap, don't replace) — the original bundle/statement bytes are never rewritten. This command checks the outer signature only; use nova verify for a capsule's full NovaSeal seal (signature + RFC 3161 timestamp + Merkle inclusion). nova export-evidence --dsse emits a bundle DSSE envelope today; nova promote direct --slsa-provenance emits a DSSE-signed SLSA v1 provenance statement for the promotion (add --slsa-ml-profile for the SLSA-for-ML profile with dataset hashes, seeds, and the gating eval-verdict digest — experimental).


nova incident (experimental, v0.50.0, ADR-0088)

First-class incident records with an EU AI Act Art. 73 deadline clock and two export regimes (OECD AIM + NIS2). Self-contained: the record lives in $NOVAFABRIC_HOME/incidents.db (override: NOVAFABRIC_INCIDENTS_DB_PATH). Deadline outputs are operational aids, not legal advice.

nova incident open --title "Tool misuse in prod agent" \
  --classification unauthorized_tool_use --severity high \
  --occurred-at 2026-06-10T08:00:00+00:00 --aware-at 2026-06-11T09:30:00+00:00 \
  --run-id 01HX...

nova incident list                       # status + most-pressing deadline
nova incident status inc-0123abcd4567    # full record + Art. 73 deadline table
nova incident export inc-0123abcd4567 --format aim     # OECD AIM JSON
nova incident export inc-0123abcd4567 --format nis2    # NIS2 report from the stored record

Notes:


Trust visualization and assurance commands (experimental, v0.59–v0.61)

Read-only projections over trust-layer evidence. Each command loads a JSON document, renders a terminal report (or --json), and prints only references, states, and short hash prefixes — never evidence bodies, field values, or full hashes.

nova merkle-tree <document> (experimental, ADR-0172)

Render an Evidence Provenance Merkle proof tree from a sealed capsule's hashes: leaf → intermediate → seal-root → tsr. The tree is derived from NovaSeal's canonical layer enumerator, so the recomputed root matches the sealed root byte-for-byte. Full leaf hashes are never printed — only short prefixes (ADR-0009); leaf labels are field paths, never values.

Why there is no --capsule here, when nova trust-radar and nova redaction-xray both have one. The proof tree visualises NovaSeal's Merkle tree, which uses a different construction from the RFC 6962 tree behind capsule_merkle_root (pairwise with odd-duplicate padding, vs. 0x00/0x01 prefixes and a power-of-2 split). An adapter that computed leaves with one and combined them with the other produced a root matching neither — verified on a real capsule and reverted rather than shipped, because two honest-looking roots that disagree are worse than no proof tree. A correct adapter must use NovaSeal leaves and is only meaningful for a sealed capsule. See ADR-0172.

nova merkle-tree tree.json
nova merkle-tree tree.json --json --capsule-id run-42

Options:

Exit codes: 0 (rendered — sealed+verified, or unsealed), 1 (seal-root mismatch — tamper evidence), 2 (missing or malformed input).


nova trust-radar <verification> | --capsule <dir> (experimental, ADR-0173)

Render a Trust Attestation Radar from a capsule's verification output. The input is a JSON object of the seven Trust-Layer guarantees (signature_ok, timestamp_ok, log_integrity_ok, redaction_coverage, secret_scan_clean, policy_pass, eval_gate_pass) — for example the summarized output of nova verify plus the evidence-bundle scan flags. Any absent/null guarantee becomes an n/a axis (e.g. an unsealed capsule has no signature_ok).

With --capsule the guarantees are derived from the capsule itself instead of being supplied by hand (trust/capsule_flags.py).

Absent is not false. A guarantee the capsule cannot evidence renders n/a, never fail:

Guarantee Source When unavailable
signature_ok / timestamp_ok / log_integrity_ok NovaSeal verification over .seal/ Unsealed capsule, or no NovaSeal profile configured → n/a. Unverified is not failed, and "could not verify" is not "verification failed".
redaction_coverage / secret_scan_clean the capsule's redaction-proof.json Captured without the masking pipeline → n/a
policy_pass / eval_gate_pass never derived from a capsule Always n/a. These are registry/promotion facts; a capsule records that a run happened, not whether an asset later cleared a gate. Inferring them would attach a promotion verdict to the wrong artifact.

A capsule with no findings reports redaction_coverage: 1.0 — nothing sensitive to cover — rather than 0.0, which would paint a clean capsule red.

nova trust-radar --capsule .novafabric/runs/01HX.../
nova trust-radar verify.json
nova trust-radar verify.json --json --capsule-id run-42

Options:

Exit codes: 0 (attested / partial / unsealed — informational), 1 (critical: a seal-integrity anchor — signature or log-integrity — failed), 2 (missing or malformed input).


nova trust-path show | verify (experimental, ADR-0168)

experimental (ADR-0168 P2, NF-363). Read / verify the transitive cross-org trust path recorded in a capsule's facets.federation.trust_path: an ordered, anchor-first chain of signed delegation statements (OpenID-Federation trust-chain shape), each hop {from_org, to_org, statement_digest, anchor_digest, signature, subject_public_key[, max_path_length]} — "A trusts C's evidence via B" as a verifiable path, not an assertion.

Exit codes for verify: 0 path verified; 1 walk failed — the first broken hop and a reason code are printed (anchor_not_pinned, anchor_org_mismatch, anchor_mismatch, broken_linkage, statement_digest_mismatch, bad_signature, bad_subject_key, cycle, path_touches_revoked, signed_depth_exceeded (only with --strict-depth), malformed_path, no_trust_path); 2 usage/input error (capsule not found, bad or private-key --anchor file). Paths are capped at 16 hops.

Every output, JSON included, carries the in-mission-boundary line: NovaFabric walks paths other orgs signed; it is never the anchor, CA or trust authority, and a valid path proves the delegation chain composes — not that the foreign evidence is correct. Revocation input is verifier-supplied until the NF-369 record lands (future design); nova federate and nova trust-anchor add remain planned.

nova trust-path show --capsule 01KZ...
nova trust-path verify --capsule 01KZ... --anchor orgA=./orgA-root.pub.pem
nova trust-path verify --capsule ./my-capsule --anchor orgA=./orgA-root.pub.pem \
  --max-depth 3 --revoked sha256:9f2c... --json

Reference: src/novafabric/federation/trust_path.py, src/novafabric/cli/trust_path.py.


nova redaction-xray <document> | --capsule <dir> (experimental, ADR-0174)

Render a Redaction / Secret-scan X-Ray from a capsule's protection metadata: a per-field state overlay, a coverage meter, and per-state counts. No field value is ever printed — the command surfaces field paths and states only (ADR-0174 §1); any value handed in alongside a record is dropped by the projection.

With --capsule the report is built from the capsule's own redaction-proof.json instead of a hand-assembled document. A capsule with zero findings is a genuine result (nothing sensitive was detected), not an error; a capsule captured without the masking pipeline says so explicitly rather than failing opaquely.

nova redaction-xray --capsule .novafabric/runs/01HX.../
nova redaction-xray xray.json
nova redaction-xray xray.json --json --capsule-id run-42

Options:

Exit codes: 0 (report rendered), 2 (missing or malformed input, both a document and --capsule, or neither).


nova assure-case <document> (experimental, ADR-0166)

Inspect an assurance-case document: structural validity, currency/drift, a conformance receipt, and open defeaters. Prints only references and digests, never evidence bodies.

The document is a JSON object with:

nova assure-case case.json
nova assure-case case.json --as-of 2026-06-01T00:00:00Z
nova assure-case case.json --json

Options:

Exit codes: 0 (case structurally valid AND no defeater open), 1 (structurally invalid, or at least one defeater open — argument defeated), 2 (document missing/malformed, or a currency ledger present without --as-of).


nova assure-coverage <document> (experimental, ADR-0166)

Report structural coverage of an assurance case — counts and gaps, never a grade. Takes the same document shape as nova assure-case (minus conformance).

nova assure-coverage case.json
nova assure-coverage case.json --as-of 2026-06-01T00:00:00Z --json

Options:

Exit codes: 0 (coverage report rendered), 2 (missing or malformed input).


nova passport issue | verify (experimental, ADR-0149)

Portable agent-passport projection (ADR-0149 / NF-179): a green/amber/red verdict projected from component refs, re-derivable offline.

nova passport issue refs.json          # human-readable passport
nova passport issue refs.json --json   # passport document (feed to verify)
nova passport verify agent-passport.json

issue options:

verify takes a passport document produced by nova passport issue --json, re-derives the verdict offline, and confirms it matches the document.

Exit codes: issue — 0 (rendered), 2 (malformed input). verify — 0 (recomputed status matches the document), 3 (status mismatch), 2 (malformed input).


nova embodied odd show | trajectory verify (experimental, ADR-0162)

Experimental (ADR-0162 P2, NF-303 / NF-310). Reads facets.embodied from a capsule's capsule.yaml offline — no robot, network, or control-plane contact. Record-only: NovaFabric never controls, drives, flies or actuates anything, and never decides whether an action was safe or in-ODD; every output prints that in-mission-boundary line.

nova embodied odd show --capsule ./capsules/run-01HX            # declared ODD + excursions
nova embodied odd show --capsule 01HXAY7M5JZ8R7K4P9DPBYK2WX --json
nova embodied trajectory verify --capsule ./capsules/run-01HX   # walk perception→…→actuation
nova embodied trajectory verify --capsule 01HXAY7M5JZ8R7K4P9DPBYK2WX --json

Options (both): --capsule (required) — capsule directory or a bare run id resolved under NOVAFABRIC_CAPSULE_DIR; --json — machine-readable output.

odd show prints odd_ref, each excursion (condition, observed, ts, always in_odd: false) in time order, and verdict: null. Zero excursions is reported as "none recorded", never as "stayed in-ODD". A block carrying a ruling — a non-null verdict or an in_odd: true excursion — is refused (AdjudicationRefusedError).

trajectory verify walks the ordered hops and reports acyclic, no_broken_parent, monotonic and complete (an actuation hop whose ancestry reaches a perception root). Each defect is named by hop index: broken_parent, missing_parent, self_reference, forward_reference, duplicate_output, ts_regression, empty_chain (errors) and stage_regression (warning — a new perception after an actuation is a closed loop, not a regression).

Exit codes: 0 — shown / chain intact; 1 — the recorded evidence is defective (broken chain, ruling in the ODD block, malformed hop, raw payload, non-digest ref); 2 — nothing could be checked (capsule not found/unreadable, or no trajectory recorded for trajectory verify — absent is not a pass). odd show with no ODD recorded exits 0 and says so.

The other ADR-0162 surfaces (nova embodied sensors|actuation|verify, nova export-dssad, nova lineage embodied) are planned, not shipped; the P3 subcommands below are shipped experimental.

nova embodied sim2real show | verify · teleop list · timing show (experimental, ADR-0162 P3)

Experimental (ADR-0162 P3, NF-304 / NF-305 / NF-308). Same offline, record-only contract and in-mission-boundary line as odd show above; each subcommand validates only the object it reports on.

nova embodied sim2real show   --capsule ./capsules/run-01HX              # NF-304 sim policy → real deployment
nova embodied sim2real verify --capsule 01HXAY --policy ./policy.ckpt    # re-hash local artifacts vs recorded digests
nova embodied teleop list     --capsule 01HXAY --direction autonomy_to_human --json   # NF-305 handoffs
nova embodied timing show     --capsule ./capsules/run-01HX              # NF-308 per-clock-domain evidence

Options: --capsule (required), --json (all four). sim2real verify also takes --policy, --env, --randomization — local files to stream-hash (regular files only, capped at 64 GiB) and compare with sim_policy_ref / sim_env_ref / randomization_ref. teleop list takes --direction autonomy_to_human|human_to_autonomy.

Exit codes: 0 — shown (including "none recorded") / nothing contradicts the record; 1 — the recorded evidence is defective or contradicted (malformed block, PII-shaped operator, prose trigger, out-of-order ts, duplicate clock domain, non-finite or out-of-range milliseconds, digest or deployment mismatch); 2 — nothing could be checked (capsule not found/unreadable or over the 64 MiB manifest cap, a named artifact unreadable, an unknown --direction, or no sim2real recorded for sim2real verify — absent is not a pass).

nova hitl thread | context | override | rationale (experimental, ADR-0150)

Read-only views over the human-agent accountability records stored in a capsule's facets.conversation block (NF-181 thread, NF-182 decision-context receipt, NF-187 override, NF-188 rationale). Nothing is written; --capsule takes a capsule directory or a run id; --json emits deterministic JSON (sorted keys) with a notice field.

nova hitl thread show     --capsule <dir|run-id> [--json]               # NF-181 turns in order, pseudonymous authors
nova hitl context show    --capsule <dir|run-id> --turn t2 [--json]     # NF-182 what the human saw + offline root re-check
nova hitl context verify  --capsule <dir|run-id> [--json]               # re-perform every receipt (fail-closed)
nova hitl override list   --capsule <dir|run-id> [--json]               # NF-187 human corrections
nova hitl rationale show  --capsule <dir|run-id> --turn t1 [--json]     # NF-188 agent's stated reason (digest)

context verify recomputes each receipt's context_root — an RFC 6962-style SHA-256 Merkle root over the ordered shown_context items, same construction as the capsule Merkle root — and checks that its turn_ref resolves to exactly one turn.

Exit codes: context verify — 0 every receipt re-performs; 1 any receipt (or the capsule) is defective: dangling turn_ref, root mismatch, two receipts for one turn, malformed entry, unreadable capsule; 2 nothing to check. context show / rationale show exit 1 when the turn does not resolve, nothing is recorded for it, or a stored record is defective. override list exits 1 on a dangling turn_ref or a malformed entry, 0 for an empty list. thread show exits 1 on a broken parent_turn_id or a repeated turn id, and 0 (saying so) when there is no conversation facet.

Every output carries the record-only notice: NovaFabric records what was shown, decided, overridden and surfaced; it does not adjudicate the decision, grant or deny any right, or certify that oversight was adequate or lawful. Records hold digests, short codes and pseudonymous human: refs only — never prose or names. Records are written by the library (novafabric.hitl.record_decision_context / record_override / record_rationale, fail-open); there is no CLI writer for these three. P3 adds nova hitl handoff list / nova hitl acted-as and nova consent (below).

nova hitl handoff list | acted-as (experimental, ADR-0150 P3)

Read-only views over the NF-189 accountability handoff receipts and the NF-186 acted-on-behalf bindings stored in facets.conversation (handoff, acted_on_behalf).

nova hitl handoff list --capsule <dir|run-id> [--json]                                # NF-189 responsibility transfers
nova hitl acted-as     --capsule <dir|run-id> --turn t1 [--delegation <nf084.json>] [--json]  # NF-186 turn -> NF-084 hop

handoff list checks each receipt offline. from_party must differ from to_party (compared after NFKC + case-fold, and two fp: refs where one hex is a prefix of the other are the same party; a self-handoff is a defect; an fp: ref needs 16..64 hex). An ed25519: sig is verified over the canonical record with the embedded signer_key; when the signer ref is fingerprint-shaped (human:fp:<hex>) the key is also checked against that fingerprint (key_binding: fingerprint_match), otherwise the key is reported unbound — the key-to-party binding is not established. A sha256: sig is a reference to an external signature envelope and is reported reference_only, never as verified. Exits 1 on a malformed or self-handoff record, a dangling turn_ref, an invalid signature, or a fingerprint mismatch; 0 otherwise (including an empty list).

acted-as looks up each binding's delegation_hop_ref among the grant_ref values of an NF-084 delegation document (schemas/features/delegation-chain-v0.schema.json, given with --delegation, or facets.delegation when a capsule carries one) and surfaces the state recorded in that document: established, broken (with broken_hop verbatim), unverified, absent, ambiguous or malformed, labelled "as recorded by the NF-084 document, not re-verified" (reverified: false in --json). The binding's principal_ref must match the hop's granter or the chain root's granter (NFKC + case-fold; user: in the document equals human:), else the state is principal_mismatch. It never re-verifies the chain. --delegation is capped at 4 MiB. Exits 0 only when the turn resolves, has a binding, and every bound hop is established; 1 otherwise (including principal_mismatch).

NF-183 consent receipts in an ISO/IEC TS 27560 / W3C DPV-shaped structure (consent_id, subject_ref, purpose, action, given_at, expiry, withdrawable, withdrawn_at, receipt_digest), stored in facets.conversation.consent.

nova consent record --capsule <dir|run-id> --subject human:fp:<hex> --purpose dpv:ServiceProvision \
    --scope dpv:Store [--scope …] [--expiry <iso>] [--given-at <iso>] [--consent-id <id>] \
    [--turn <turn_id>] [--not-withdrawable] [--dry-run] [--force-unseal] [--json]
nova consent withdraw --capsule <dir|run-id> --consent-id <id> [--withdrawn-at <iso>] \
    [--dry-run] [--force-unseal] [--json]
nova consent show   --capsule <dir|run-id> [--json]
nova consent verify --capsule <dir|run-id> [--json]
nova consent attest --capsule <dir|run-id> --key <ed25519.pem> [-o <file>] [--json]
nova consent verify-attestation --capsule <dir|run-id> --attestation <file> \
    --public-key <ed25519.pub.pem> [--json]

record builds the receipt, binds receipt_digest (SHA-256 over the canonical receipt, extension fields included, withdrawn_at excluded) and atomically rewrites capsule.yaml. That changes the capsule Merkle root, so any seal or signature issued over the capsule earlier must be re-issued. It exits 1 when the receipt is refused (non-human: or email-shaped subject, prose in a concept field, expiry not after given_at, a --turn that does not resolve, a duplicate consent_id). --dry-run prints without writing. A NovaSeal-sealed capsule (.seal/ present) is refused (exit 1, capsule.yaml untouched, nova verify still passes) unless --force-unseal is passed; the write then proceeds with a loud warning that the existing seal no longer verifies and must be re-issued. A symlinked capsule.yaml or capsule directory is always refused.

withdraw (experimental) records that a stored consent was withdrawn: it sets withdrawn_at (default: now, UTC) on the one receipt with --consent-id and atomically rewrites capsule.yaml under the same sealed-capsule / --force-unseal / symlink rules as record. The receipt is never deleted, and its receipt_digest still verifies because the digest excludes withdrawn_at. It exits 1 when refused: unknown or duplicated consent_id, a stored receipt that is malformed or fails its digest check (withdrawing would launder a tampered record), a receipt recorded withdrawable: false, one already withdrawn (a withdrawal is recorded once and never moved), or a --withdrawn-at that is not ISO-8601, precedes given_at, or lies more than 5 minutes in the future. --json emits {receipt, index, written, notice}.

verify recomputes every receipt_digest, resolves any turn_ref, flags a duplicate consent_id, and prints the capsule.yaml digest the receipts bind through (the binding to the capsule root is transitive; for a signed binding use attest). Exit 0 intact, 1 defective, 2 nothing to check. show exits 1 on a malformed entry.

attest (experimental) signs an in-toto Statement v1 (DSSE, the shared writer in evidence/intoto.py, ed25519 LocalSigner) with predicate type https://novafabric.io/consent-attestation/v0. It has one subject per receipt (consent/<consent_id> with its receipt_digest) and a predicate carrying each receipt's receipt_digest, withdrawn_at and a withdrawal_digest binding the withdrawal event, a consent_set_digest, and the capsule's RFC 6962 Merkle root (the Evidence Bundle construction; the NovaSeal pairwise root is not used). It writes a sidecar file (default ./<run_id>.consent.intoto.json) and never touches the capsule, so a sealed capsule stays sealed. Re-run it after withdraw to bind the withdrawal. It exits 1 when nothing can be attested (no receipts, a malformed or digest-failing receipt, an unusable key).

verify-attestation (experimental) checks, offline, the DSSE signature against the supplied public key (the envelope keyid must name it), the statement framing, the predicate's own digests, and every attested receipt against the capsule. Exit 0 ok; 1 invalid (bad signature or structure, a receipt edited since attestation, a withdrawal removed or moved, a consent dropped); 3 stale (not tampered, but a consent was withdrawn or recorded or the capsule root changed after attestation — re-attest). Not yet included in nova evidence export bundles: store the sidecar next to the bundle. The signing key's trust (who holds it) is outside this check.

Every output carries the notice that NovaFabric records the receipt and does not assert the consent was freely given, informed, specific, lawful, or otherwise legally valid.

Worked example (temporary NOVAFABRIC_HOME; key made with openssl genpkey -algorithm ed25519 -out k.pem, public half with openssl pkey -in k.pem -pubout -out k.pub.pem). The attestation verifies, then a withdrawal makes it stale (exit 3), not invalid:

$ nova consent record --capsule $RID --subject human:fp:aaaaaaaaaaaaaaaa \
      --purpose dpv:ServiceProvision --scope dpv:Store --consent-id c1        # exit 0
$ nova consent attest --capsule $RID --key k.pem -o att.json
Wrote consent attestation att.json
$ nova consent verify-attestation --capsule $RID --attestation att.json --public-key k.pub.pem
  OK signature DSSE signature verifies against the supplied key
  ...
  OK receipts every attested receipt matches the capsule
Status: ok                                                                       # exit 0
$ nova consent withdraw --capsule $RID --consent-id c1
$ nova consent verify-attestation --capsule $RID --attestation att.json --public-key k.pub.pem
  STALE c1: withdrawn after attestation (at 2026-10-02T10:47:26Z)
  STALE capsule Merkle root changed since attestation
Status: stale                                                                    # exit 3