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 stdoutOptions:
--fail-on {critical,high,medium,low,info}— exit 2 if any finding has severity at or above the threshold. Tab-completion available vianova --install-completion.--json— emit the full redaction proof as parseable JSON
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.jsonPerforms 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:
--format {rich,json}— Output format:rich(default, colored table) orjson. Tab-completion available vianova --install-completion.
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-skipsMode 2 — re-scan (default):
nova redact <capsule>
nova redact <capsule> --strategy-override openai-api-key:hash
nova redact <capsule> --strategy-override pii-email:dropOptions:
--strategy-override RULE:STRATEGY—mask(default),hash(SHA-256 first-8), ordrop(remove). Repeatable.--mark-unsafe-skip FINDING_ID(with--rationale) — record a user-acknowledged false positive. The capsule's existingunsafe_skipsare preserved across re-scans by default.--clear-unsafe-skips— drop allunsafe_skipsbefore writing.--review— interactive triage (requires a TTY; v0.4 stub: exits 2 if no TTY, full triage UI planned).
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> --jsonResolution 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):
--body TEXT(required) — free text, secret-scanned--author ID— defaults to the local username (self-asserted in local mode)--kind {capsule,span,run,score}— what the subject digest addresses (defaultcapsule)--reply-to COMMENT_ID— thread a reply / supersede a prior comment--tag LABEL— repeatable free labels--tombstone— retract the comment named by--reply-to--redact— mask secret matches instead of refusing--json— print the stored record
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 scorequeue 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:
- Well-formed: all shipped
Scoreinvariants (ULID ids,sha256:digests, value/value-type agreement). - Config-valid (ADR-0117): a value violating the score config governing
--nameis rejected; with no matching config the score is accepted and the--jsonenvelope reportsconfig_bound: false. - Subject anchored:
--subjectmust be a digest that exists in the target capsule (its stable root digest, current Merkle root, manifest digest, or any digest recorded in the capsule's top-level evidence files). - Append-only: a correction is a new record whose
--supersedesnames an existingscore_id; prior lines are never edited or removed. - Idempotent: re-running with the same client-minted
--score-idand body is a no-op that echoes the stored record; the same--score-idwith a different body is refused (idempotency-key collision).
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-skipsOptions:
--key PATH(required) — PEM-encoded ed25519 private key--output, -o PATH(required) — output ZIP path--allow-unsafe-skips— permit export when redaction-proof.json containsunsafe_skips--sigstore— after building the bundle, publish the DSSE envelope to the Rekor transparency log. RequiresNOVA_REKOR_URLenv var; without it, prints a skip warning and exits 0. Network errors are warnings only (fail-open).--with-custody(experimental, ADR-0095) — embed the FRE-902(14) court-admissibility blocks (chain_of_custody+self_authentication, additive optional$defsonevidence-bundle.schema.json) in the bundle manifest, built from the hash-chained audit log + capsule Merkle root. Invariant I3: unwitnessed fields arenull+operator_declared, never fabricated.--custodian ID— custodian identity recorded in the custody block (used with--with-custody).--custodian-provenance {novaseal-identity|oidc|operator_declared}— provenance of the custodian identity; onlynovaseal-identity/oidccan make a bundleself-authenticating(used with--with-custody).--dsse(experimental, NF-029/ADR-0096) — also emit a standard DSSE outer envelope wrapping the final bundle manifest, written to<bundle>.dsse.json(payloadTypeapplication/vnd.novafabric.bundle+json, signed with the same--key). The manifest chains custody over every artifact via their sha256 +manifest_hash, so the envelope transitively covers the whole bundle. Verify it withnova verify-envelopeor stockcosign verify-blob-attestation. Opt-in: without this flag the bundle output is byte-for-byte unchanged. Emitted after any--timestamprewrite, so it wraps the final canonical manifest.
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.pemBundle 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 recipeVerification 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:
--html— select the single-file HTML renderer (required; the only renderer in v0)--output, -o PATH— output HTML file path (default:<capsule_dir>.htmlnext to the capsule dir)--title STR— optional page title override
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:
- Projection only. The summary contains only fields the capsule already exposes — never tool arguments/results or message bodies, and it never adds content the capsule does not carry.
- Redaction is inviolable. The exporter reads the already-redacted capsule;
redaction markers (
[REDACTED:…]) render verbatim and there is no un-redact flag (ADR-0009). - A human view, not a verifier. The page states plainly that real
verification is
nova verify(or the signed Evidence Bundle fromnova export-evidence, which this command complements, never replaces). - Read-only and non-blocking. Unreadable sections are skipped with a warning; the file is still produced.
- Planned (not yet implemented):
--include-verification(rendered seal/signature status panel) and--graph(inlined Sigma.js lineage view) — ADR-0140 P3/P4.
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 --anchorOptions:
--key PATH— PEM-encoded ed25519 private key; when given, output is a DSSE-signed in-toto envelope (predicate typesnovafabric.io/{completeness,criterion-binding,reperformance}/v0).attest-replayalways requires it.--profile ID|PATH— audit profile (eu-ai-act-high-risk,gdpr,iso42001,nist-ai-rmf,scientific-reproducibility,soc2-type2, or a YAML path).--mode {mocked,forensic,semantic,exact}— replay mode forattest-replay(defaultmocked); the verdict (exact/semantic-match/mismatch) is recorded separately from the mode. Exit 2 onmismatch.--certify— (experimental, ADR-0094 B) after the re-performance attestation, also emit a DSSE-signed determinism certificate (ReplayAttestation, predicatenovafabric.io/replay-attestation/v0) toreplay-attestation-<capsule>.intoto.json.determinism_class∈BIT_EXACT/BOUNDED_EQUIVALENT/NON_DETERMINISTICis derived honestly from the capsule's recorded pins (model-calls.jsonlgen_ai.*+env.lock): a missing seed/model digest or a non-exactverdict downgrades toNON_DETERMINISTICwith the reasons recorded — hosted models without a digest and pinned seed never classifyBIT_EXACT. When a ledger checkpoint exists (or--anchoris given) the certificate back-links vialedger_ref.--anchor— (experimental, ADR-0094 A) append the attestation digest to<capsule>/attestations.jsonland seal it via the accountability-ledger path (per-stream sidecar hash chains + a signed checkpoint with a local finalize anchor), making the attestation tamper-evident. Verify withnova ledger verify. Existing.jsonlevidence streams are never modified.--output, -o PATH— output file.
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:
--seal-config PATH— path tonovaseal.yaml(default:~/.novafabric/novaseal.yaml; env:NOVAFABRIC_SEAL_CONFIG)--backend [local|sigstore]— verification backend (default:local). Usesigstoreto verify a Sigstore bundle stored alongside the capsule; requirespip install novafabric[sigstore]--capsule-id TEXT— capsule ID for Sigstore bundle lookup (required when--backend sigstore)--home PATH—NOVAFABRIC_HOMEoverride (used for Sigstore bundle path)--ca-bundle PATH— experimental (ADR-0055). Operator CA bundle (concatenated PEM). Adds aSigner certificate chain (CA bundle)check: the certificate embedded in the DSSE envelope must chain — RFC 5280 path validation viacryptography'sx509.verification, offline, validity checked at the current time — to a certificate in the bundle. Every bundle certificate is a trust anchor; the envelope carries only the leaf, so include the issuing intermediate in the bundle. No OCSP; offline CRL revocation checking is opt-in via--crl-dir(below). Overrides the optionalca_bundlekey innovaseal.yaml; when neither is set, the check is skipped and output is unchanged. Local backend only. Fails closed (exit 1) on an unreadable/malformed bundle, a bare-key envelope, or a chain that does not reach an anchor.--crl-dir DIR— experimental (ADR-0070 §3, ADR-0055 OQ-55-3). Directory of CRLs (DER or PEM) an operator syncs locally; never fetched. Every certificate of the validated signer chain below a self-signed root (leaf, intermediates — including a bundled intermediate, checked against its root's CRL) is checked against the CRL issued by its issuer, authenticated with the issuer's key. Per-certificate lines are printed underRevocation (CRL, offline, soft-fail|strict):GOOD,REVOKED(with reason/date — always exit 1), orWARNING no_crl/stale/invalid_crl(forged or unsigned CRL). Skipped files (non-CRL, oversize > 16 MiB, delta/indirect CRLs) are listed. More than 256 files fails closed. Requires--ca-bundleorca_bundleinnovaseal.yaml(otherwise exit 2); overridescrl_dirinnovaseal.yaml.--crl-strict— with--crl-dir: a missing, stale or invalid CRL also fails (exit 1). Also enabled bycrl_strict: trueinnovaseal.yaml.--tsa-ca-bundle PATH— experimental (ADR-0070 §1). Operator TSA CA bundle (concatenated PEM). When the capsule's.seal/manifest.dsse.tsr(or an Evidence Bundle ZIP'smanifest.dsse.tsr) holds an RFC 3161 token, adds aTSA certificate chain (RFC 3161, TSA CA bundle)check, fully offline and fail-closed: the CMS signature over the signed attributes verifies under the TSA signer certificate,messageDigestmatches theTSTInfo, thesigningCertificate/signingCertificateV2(ESSCertID) hash binds that certificate, the certificate'sextendedKeyUsageis critical and onlyid-kp-timeStamping, the message imprint equals SHA-256 of the DSSE envelope (ZIP: ofattestations/run.intoto.json), and the certificate chains to the bundle at the token's genTime (depth ≤ 10).--crl-dir/--crl-strictalso revocation-check this chain as of genTime, with CRL freshness judged at verification time so a CRL synced today works under--crl-strict;keyCompromise/cACompromiseentries (or aninvalidityDate≤ genTime) revoke even when dated after genTime (and--crl-diris then accepted without--ca-bundle). Overridestsa_ca_certsinnovaseal.yaml; when neither is set the check is skipped and output is unchanged. With TSA anchors set, a missing or empty token fails (exit 1) — the.tsris not covered by the DSSE signature, so a deleted token must not pass. The message imprint's hash algorithm must be SHA-256. RSASSA-PSS and SHA-1 message digests are rejected.
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=TrueExample 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.jsonlCapsules 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 acceptedOptions:
--key PATH— PEM-encoded Ed25519 key (public or private; the public half is derived from a private PEM). Required.
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 recordNotes:
- Deadlines anchor at
--aware-at(fallback--occurred-at): 15 days standard (Art. 73(2)), 10 days death (Art. 73(4)), 2 days widespread infringement / critical infrastructure (Art. 73(3)). Classification keywordsdeath,widespread,critical_infrastructureselect the stricter clocks; an empty classification emits all candidates (fail-informative). - Lifecycle is forward-only
open → reported → closed;exporttransitions an open incident toreported. A wrongly-opened incident is closed, never deleted. attestation_refcan reference an ADR-0087 re-performance attestation (post-incident replay evidence).
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
--capsulehere, whennova trust-radarandnova redaction-xrayboth have one. The proof tree visualises NovaSeal's Merkle tree, which uses a different construction from the RFC 6962 tree behindcapsule_merkle_root(pairwise with odd-duplicate padding, vs.0x00/0x01prefixes 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-42Options:
<document>(positional, required) — JSON withleaf_hashes(+ optionallabels/sealed_root/tsr_hash)--capsule-id TEXT— capsule id to label the tree with. A label only — it selects nothing--json— emit the proof tree as JSON
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-42Options:
<verification>(positional) — path to a JSON object of the 7 verification guarantees. Omit when using--capsule--capsule PATH— capsule directory to derive the guarantees from--capsule-id TEXT— label the radar with this capsule id. A label only — it selects nothing; use--capsuleto read from a capsule--json— emit the radar model as JSON
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.
nova trust-path show --capsule <run_id|dir> [--json]— print the hops unverified (no signature is checked). Exit0whenever the capsule was read, including "no path recorded";2on a missing/unreadable capsule or a malformed path.nova trust-path verify --capsule <run_id|dir> --anchor ORG=PUBKEY.pem [--anchor …] [--max-depth N] [--revoked SUBJECT …] [--strict-depth] [--json]— walk the path offline. Hop 0 must verify under an anchor you pin with--anchor(Ed25519 or ECDSA P-256 PEM public key); the capsule's owntrust_anchorpin is never used. Every later hop must be signed by the key the previous hop vouched for, linkfrom_orgto the previousto_org, carry the sameanchor_digest, and have a recomputablestatement_digest. Passes only when the path isacyclic, hasno_broken_hop,terminates_at_anchor, and touches no--revokedorg id / key digest. A signedmax_path_lengthor--max-depthoverrun is flaggeddelegation_depth_exceeded— not fatal by default; the report separatessigned_depth_flagsfrompolicy_depth_exceeded.--strict-depthmakes a signedmax_path_lengthoverrun fatal (exit1, reasonsigned_depth_exceeded, OpenID Federation semantics);--max-depthstays a flag either way.
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... --jsonReference: 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-42Options:
<document>(positional) — JSON with eitherfields([{path, state}], state one ofclear|redacted|secret_scrubbed|never_captured|unknown) or rawfindings(MaskingPipeline finding records). Omit when using--capsule--capsule PATH— capsule directory to readredaction-proof.jsonfrom--capsule-id TEXT— label the report with this capsule id. A label only — it selects nothing; use--capsuleto read from a capsule. When--capsuleis used, the id comes from the proof's owncapsule_run_id--json— emit the X-Ray report as JSON
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:
case(required) — the argument graph:{case_id, nodes[]}resolvable_digests(optional) — evidence digests that currently resolvecurrency(optional) —{nodes[]}, a currency ledger (needs--as-of)conformance(optional) —{entries[]}, standard/clause mappingsdefeaters(optional) — recorded challenges to nodes
nova assure-case case.json
nova assure-case case.json --as-of 2026-06-01T00:00:00Z
nova assure-case case.json --jsonOptions:
--as-of TEXT— ISO-8601 instant to evaluate currency at (required if the document carries a currency ledger; never inferred from the system clock)--json— emit a machine-readable JSON report
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 --jsonOptions:
--as-of TEXT— ISO-8601 instant to evaluate currency at (required if the document carries a currency ledger; never inferred from the system clock)--json— emit the coverage report as JSON
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.jsonissue options:
<document>(positional, required) — JSON:{agent_ref, present: {component: ref}, opaque: [...]}--json— emit the passport document as JSON
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 --jsonOptions (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 evidenceOptions: --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.
sim2real showprints the four refs andunbound. An absent or unresolvablesim_policy_refis recorded asunbound: true— shown, never an error.sim2real verifychecks thatdeployment_run_idnames this capsule'srun_id(arun:prefix is ignored), notes whensim_policy_refappears in this capsule's NF-202facets.model_provenance.checkpoint_chain, and compares any supplied local artifact. Errors:artifact_mismatch,deployment_mismatch. Warnings (never fatal):unbound,deployment_unchecked(norun_id),artifact_unreferenced.teleop listprints each handoff (ts,direction, pseudonymousoperator_ref,triggercode,latency_ms) in time order. A PII-shaped operator ref (e-mail, whitespace, bare phone-like digit run) is refused (OperatorIdentityError) without echoing it. Two consecutive handoffs in the same direction are adirection_repeatwarning.timing showprints each clock domain'ssource,offset_ms(or "unrecorded") andmax_observed_latency_ms, and warns (undeclared_clock_domain) for each sensor stream stamped against a domain the block does not describe. NovaFabric synchronizes, steers or corrects no clock.timing showis not named in the ADR-0162 spec's CLI list (NF-308 has no CLI there); it is attached to the existingnova embodiedgroup as the read-only counterpart of the othershowcommands.
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 hophandoff 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).
nova consent record | withdraw | show | verify | attest | verify-attestation (experimental, ADR-0150 P3)
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