NovaFabric REST API Reference

Status: works today (v0.98.0). Generated from the live nova serve FastAPI route table (src/novafabric/serve/app.py).

The nova serve dashboard server exposes 208 REST endpoints in the generated tables below, plus the 14 experimental routes in the next section that the generator has not yet been re-run for — 222 in all, every route create_app can mount across its flags (GET / and the /api/topology/* routes are conditional). The authorization table classifies 225, the other three being the TV-5 routes nova serve mounts after the app is built. All require a valid auth token except the seven public routes: the probes GET /api/health, GET /livez and GET /readyz, the interactive docs GET /api/docs, GET /api/openapi.json and GET /docs/oauth2-redirect, and GET / (the placeholder served when no dashboard is mounted). The multi-tenant nova server API is specified separately in api/openapi.yaml.

Authorization (ADR-0228, experimental)

A valid token is necessary but no longer sufficient. Every route carries one of four scopes, enforced by a single declarative table (src/novafabric/serve/authz.py):

Scope Reaches ADR-0027 layer
read every read-only endpoint, including the POST routes that only compute and return — /api/query, /api/policy/check (packaged policies only), /api/mcp/scan, /api/kg/blast-radius, and the whole /api/compliance/export/* family Layer A
operate bounded, reversible mutations — register, promote, score, placing a hold, incident, ingest Layer B
admin irreversible or privilege-affecting — capsule/run deletion, releasing a legal hold, PII erase, seal bypass, signing, key ratchet, role and token management, and the forensic/semantic/exact replay routes (none of the three re-executes the workload; each writes a replay result beside the capsule) Layer C
audit everything read reaches plus GET /api/audit, GET /api/reports/dashboard-audit, GET /api/reports/policy-audit (and their /export), GET /api/policy/recent-decisions, GET /api/policy/explain, GET /v1/kg/audit — and never operate or admin orthogonal

admin ⊇ operate ⊇ read, and admin also reaches audit. Insufficient scope returns 403 with the required and held scope in detail, and appends an authz.denied record to the dashboard audit log. A missing or unrecognised credential on a classified route still returns 401, and writes nothing.

Two rules sit on top of the table:

Whoever asks, every opa that nova serve (or the CLI) starts is sandboxed: it gets no environment from the server except a fixed PATH, and runs with an OPA capabilities file derived from the installed binary minus opa.runtime, http.send and net.lookup_ip_addr, with no network allowed. Rego that calls one of those fails to compile and the decision is a deny whose reason names the undefined function. If the capabilities cannot be derived (an opa too old for opa capabilities --current), nothing is evaluated and the decision is a deny. Request-path evaluations run in a worker thread with a 10 s timeout per opa call.

The default is unchanged: the .serve-token holds admin, as does any token issued before this landed (a tokens.jsonl record with no scope field). Mint a narrower one with POST /api/admin/tokens with a scope field and "confirmed": true (without it the answer is 400 and nothing is minted). The scope must be exactly read, operate, admin or audit; anything else is refused with 422 and nothing is stored. A stored record whose scope is not one of those (for example a hand-edited typo) holds no scope: it is refused with 403 on every non-public route, and GET /api/admin/tokens lists it as none.

The audit fingerprint of the server token (actor_token_fp in route records, session_token_fingerprint in GET /api/admin/tokens, user.uid in the SIEM export) is 12 hex characters of a domain-separated SHA-256 of the token. It is stable, and it is never a prefix of the token.

Paths in requests

Fields that name a server-side file or directory (output_path, key_path, cert_path, db_path, source, output, capsule_path (body, and the ?capsule_path= query of GET /api/assure/{run_id}), capsule_base, bundle_path, target_db, a non-built-in compliance profile, and an /api/ingest-capsule run_id that contains a /) are confined to allowlisted roots. A write may target only $NOVAFABRIC_HOME/exports, $NOVAFABRIC_HOME/evidence or $NOVAFABRIC_EVIDENCE_DIR. A read may also target $NOVAFABRIC_HOME and the server's capsule directory. Each path is resolved, symlinks included, before it is checked. A .. segment, or a path that resolves outside the roots, is refused with 403. An empty path or a NUL byte is refused with 400. Relative paths are taken from $NOVAFABRIC_HOME (reads) or $NOVAFABRIC_HOME/exports (writes). Operators add directories with NOVAFABRIC_SERVE_PATH_ROOTS (absolute paths, os.pathsep-separated). NOVAFABRIC_SERVE_ALLOW_ANY_PATH is no longer honoured.

The same roots bound run ids. An entry of the capsule directory that is a symlink is never listed by /api/runs, /api/runs/search, the reports, the cost report or any other route that scans capsules. Named directly (/api/runs/latest), it is served only when it resolves inside the read roots; otherwise every {run_id} route answers 403. A capsule whose top level holds a symlink or a special file (FIFO, socket, device) is refused with 403, and a symlinked or special file in a sibling capsule is read as absent by every scanning route. See Links inside a capsule are never followed.

A read never reaches the server's own secrets, even inside a root: the token stores (tokens.jsonl, .serve-token, .server-token), .env, the PII key store (dek.db and its SQLite sidecars), the NovaSeal ratchet state, config.yaml and the audit logs are refused with 403, and so is a directory that contains one of them ($NOVAFABRIC_HOME itself, for example). A source read (source, capsule_path, capsule_base, bundle_path, profile) also refuses key material: $NOVAFABRIC_HOME/keys, the NovaSeal keyring, and any *.pem or *.key file. key_path/cert_path may name key material, which the server uses to sign and never returns.

Values a route joins onto a server directory as one name (node_id on /api/seal/ratchet/*, capsule_id on /api/seal/sigstore/* and /api/seal/{capsule_id}/..., run_id on /children and /validate-distributed) are refused with 400 if they contain /, \, .., a NUL or other control character, are absolute, or are longer than 255 bytes (one rule, shared with nova server's /v0 routes and nova import); an evidence-cart cart_id must match [A-Za-z0-9_-]+ (422 otherwise). Ratchet responses never include the chain key.

No route returns the target of a symbolic link planted in a capsule. GET /api/runs/{run_id}/file/{path} answers 404 for a link or a special file; a capsule with a link at its top level is refused with 403 by every /api/runs/{run_id}/... route; POST /api/compliance/export/rocrate answers 422 for a capsule holding any link. See Concepts: links inside a capsule.

Single-actor promotion into a protected stage

POST /api/assets/{name}/{version}/promote and POST /api/assets/{asset_id}/promote apply the same ADR-0312 gate as nova promote direct: production needs an explicit promote_direct policy allow, and force does not bypass it. Without one, the answer is 403. Without OPA there is never an explicit allow. Reach production through nova promote propose + approve. The policy subject is serve:<credential fingerprint>, not the request's actor field.

Registration is not a way in either. POST /api/assets registers every asset as development; a spec that declares any other status is refused with 422 (audited as an error) and POST /api/assets/register-from-yaml answers ok: false. A version that was ever in production cannot be registered again (409). A rollback restores, without a policy decision, only a version whose current registration has production history; see nova rollback.

Confirmation on state-changing routes

Nineteen POST routes refuse to act unless the JSON body carries "confirmed": true; without it they answer 400 and change nothing: /api/admin/tokens, /api/admin/rebuild-metadata-db, /api/admin/reindex-runs, /api/assets, /api/assets/{asset_id}/approve, /api/assets/{asset_id}/eval, /api/assets/{asset_id}/promote, /api/assets/{name}/rollback, /api/assets/{name}/{version}/eval, /api/assets/{name}/{version}/promote, /api/compliance/erasure/request, /api/evidence/{run_id}, /api/evidence/cart/export, /api/runs/{run_id}/redact, /api/seal/ratchet/rotate, and the four /api/runs/{run_id}/replay/{dry-run,exact,forensic,semantic} routes. The flag is a guard against an accidental click, not an authorization step: the scope check runs first.

Aggregates and the honest-degradation rule (ADR-0234, experimental)

An aggregate that cannot be computed faithfully must refuse, and say why. It may not render an approximation, a partial result, or a zero in place of an unavailable one.

Endpoints that return an aggregate carry an additive aggregate block:

{"computable": false,
 "condition": "source_unavailable",
 "reason": "cost aggregation needs the ClickHouse cost store, which is not configured; …",
 "remedy": "set NOVA_CLICKHOUSE_URL … or read per-run cost from the capsule's cost facet instead"}

A refusal carries no value — not zero, not null. A computable verdict carries {"computable": true, "value": …} and may carry non-fatal notes.

condition Meaning
source_unavailable the backing store is not configured or did not answer
truncated_source the underlying set is bounded (ADR-0199); a total over it is not a total
absent_contributor a contributing field is legitimately absent — e.g. an unpriced model call
unpushable_filter a filter could not be pushed into the aggregation
tenant_unsafe_store ADR-0229 declares a required store unable to answer per tenant

GET /api/runs/cost-summary is the first consumer. Each run additionally reports unpriced_calls and cost_is_complete: CostInterceptor._estimate_cost returns 0.0 for a model with no catalog price, so without these a run on an unpriced model reported $0.00 and read as free. A selection in which every call is unpriced now refuses rather than reporting zero.

⚠ Zero is still a legitimate value. "No runs failed in this bucket" is a measured fact. The rule is about unmeasured quantities, never about measured zeroes.

Tenancy (ADR-0229, experimental)

GET /api/doctor includes a tenancy_posture check reporting, for each of the eight stores in the read path, whether it is aware, agnostic, or unsafe, plus a blocking list. Three are unsafe today — the runs index, the knowledge graph, and the lineage store — so multi-tenant mode is refused at startup (NOVAFABRIC_SERVE_TENANCY=multi makes nova serve exit 2 rather than serve cross-tenant reads). Every endpoint below therefore answers for a single tenant. For multi-tenant deployments use nova server (api/openapi.yaml).

A route absent from the table is denied to everyone, including the server token and callers that send no credential at all — a CI guard fails on the first unclassified route, so this should never be reachable in a release.

Generated by design/scripts/gen_api_reference.py (private) — re-run it after route changes rather than editing endpoints by hand.

TV-5 3D topology routes (experimental). When the server is started with nova serve --tv5, an additional router is mounted at /api/tv5/ (src/novafabric/serve/topology/router_tv5.py): GET /api/tv5/live (Arrow delta stream), GET /api/tv5/windows, GET /api/tv5/snapshot/{window_id}, and a WS /api/tv5/ws websocket. These are conditional and eval-gated, so they are not in the generated table above. Since v0.97.0 they are authenticated — the HTTP routes are token-gated like every other /api/* route, and the WebSocket enforces the localhost Host guard (close code 4403) and the token (close code 4401) before accepting the connection — on the WebSocket the token must arrive as a ?token= query parameter, since browsers cannot set headers on a WS connect. Previously this router was mounted with no auth at all; a custom TV-5 client that connected without a token must now supply one.

Dashboards, saved views, filter bar and evidence cart (experimental)

Status: experimental. Hand-maintained until the generator is re-run; the authority is the route table in src/novafabric/serve/authz.py. User-facing behaviour is in dashboard.md.

Method Path Scope Summary
GET /api/filter/parse read Parse a filter-bar string with the nova query DSL parser (ADR-0232).
GET /api/filter/runs read Runs matching a filter, with scope node, root or tree (ADR-0233); reports "N of M" and truncation.
GET /api/filter/suggest read Observed values for one filterable dimension.
GET /api/views read List nova view files, each with its Runs-view form or the reason it has none (ADR-0232 D4).
POST /api/views operate Save the Runs view state as a nova view file; audited.
DELETE /api/views/{view_id} operate Remove one saved view; audited.
GET /api/dashboards read List dashboards and widgets under $NOVAFABRIC_HOME/dashboards; refused files are listed with the reason (ADR-0235).
GET /api/dashboards/{dashboard_id} read One dashboard (422 with the reason if the stored file is refused).
GET /api/dashboards/{dashboard_id}/export read The stored JSON, byte for byte.
GET /api/dashboard-widgets/{widget_id}/data read Run only the query stored in the widget file; the request carries no query text (ADR-0236 ratio() included).
GET /api/dashboard-widgets/{widget_id}/export read The stored widget JSON, byte for byte.
POST /api/dashboards/validate read Dry run: the verdict and diff inputs; writes nothing. Body over 256 KiB is a 413.
POST /api/dashboards/apply operate Validate then store atomically; 409 if the file changed since the preview digest; refuses builtin, symlinked targets and bad ids; audited as dashboard_apply.
POST /api/evidence/cart/export admin Resolve a cart once and write one signed Evidence Bundle (ADR-0239); one export at a time (429).

Health & meta (9)

Method Path Summary
GET / serve no dashboard
GET /api/adapters List registered Nova framework adapters with availability flags.
GET /api/docs swagger ui html
GET /api/doctor Run diagnostic checks on the NovaFabric installation (nova doctor).
GET /api/health health
GET /api/openapi.json openapi
GET /api/stats Aggregate counts for the HomeTab.
POST /api/validate-spec Validate an asset YAML spec without registering — mirrors nova validate <spec>.
GET /docs/oauth2-redirect swagger ui redirect

Runs & capsules (34)

Method Path Summary
POST /api/capsule-migrate Migrate a v0.1.x capsule directory to v1.0.0 format (ADR-0034 §6). source must be under a read root and output under a write root (see Paths in request bodies).
POST /api/otlp/v1/traces Ingest OTLP OTel GenAI spans into a run capsule (NF-034, experimental).
POST /api/otlp/v1/logs Ingest OTLP log records into the sidecar log store, never into a capsule (ADR-0293, experimental). The body is read bounded: over 16 MiB is a 413 before it is buffered.
GET /api/runs list runs
GET /api/runs/cost-summary Return per-run token and cost totals from ClickHouse.
GET /api/runs/search Cursor-based run listing. True keyset paging: the cursor seeks the index directly — O(page), no cursor-to-offset conversion (ADR-0199 B2).
GET /api/runs/stream Server-Sent Events stream of new run summaries.
GET /api/runs/suggest-register Return asset registration suggestions derived from recent capsules.
DELETE /api/runs/{run_id} Delete a capsule directory, subject to legal holds and retention policy.
GET /api/runs/{run_id} get run
GET /api/runs/{run_id}/children Return parent capsule metadata + list of child capsule summaries. A sealed capsule's status and exit_code come from its signed payload (.seal/manifest.dsse), never from an unsealed capsule.yaml/capsule.json beside it.
GET /api/runs/{run_id}/diagnose Attribute a failed run to its most likely responsible step.
GET /api/runs/{run_id}/energy Energy-Anchored Action Receipts + conservation for a run (ADR-0093).
POST /api/runs/{run_id}/export-system-card Generate and SEAL an auto-generated system/audit card.
GET /api/runs/{run_id}/file/{filepath:path} get run file
GET /api/runs/{run_id}/forensics-timeline Reconstruct a deterministic forensics timeline from a run's sealed capsule (ADR-0155 D1; see note below).
GET /api/runs/{run_id}/ledger Adversary-anchored ledger verification status (ADR-0094).
POST /api/runs/{run_id}/redact redact endpoint
GET /api/runs/{run_id}/redaction-proof get redaction proof endpoint
GET /api/runs/{run_id}/redaction-xray Field-protection overlay for one capsule (ADR-0174).
POST /api/runs/{run_id}/replay/dry-run dry run replay endpoint
POST /api/runs/{run_id}/replay/exact exact replay endpoint
POST /api/runs/{run_id}/replay/forensic forensic replay endpoint
POST /api/runs/{run_id}/replay/semantic semantic replay endpoint
GET /api/runs/{run_id}/run-lineage List spool lineage edges for a distributed run — nova run lineage.
GET /api/runs/{run_id}/safety-case Compile an evidence-grounded safety case for a run (ADR-0095).
GET /api/runs/{run_id}/scan-secrets Report secret/PII findings from the redaction log — nova scan-secrets.
POST /api/runs/{run_id}/scores Append one externally-computed score to the run's scores.jsonl.
GET /api/runs/{run_id}/tool-permission-events Return ToolPermissionEvent records for a capsule.
GET /api/runs/{run_id}/tree Show the parent/child capsule tree — nova run show --with-children.
GET /api/runs/{run_id}/trust-radar Trust guarantees for one capsule (ADR-0173).
POST /api/runs/{run_id}/validate Validate a capsule's schema and required files.
POST /api/runs/{run_id}/validate-distributed Validate a distributed parent capsule + its workers (nova run validate-distributed).
POST /api/runs/{run_id}/verify Verify DSSE signature + RFC 3161 timestamp + Merkle log inclusion + the ADR-0251 capsule binding with the same function as nova verify <capsule-dir>. Without a novaseal.yaml on the host it verifies from the capsule alone, as nova verify does, and returns every check with configured: false (up to v0.106.0 it returned only configured: false and verified nothing). An unusable novaseal.yaml is valid: false. Opens configured logs read-only; creates nothing.

Content search on GET /api/runs/search — scope parameter (experimental, ADR-0204)

GET /api/runs/search accepts an additive scope=meta|content|all query parameter (default meta):

If the host SQLite lacks FTS5, content scopes return a 501 error envelope ({"error": {"code": "fts5_unavailable", …}}); scope=meta is unaffected. Postgres (nova server) parity is planned — no target version.

GET /api/runs/{run_id}/forensics-timeline (ADR-0155 D1)

Reconstructs a deterministic forensics timeline from a run's own sealed capsule via the pure forensics/timeline.py::merge_timeline core — the same merge nova forensics timeline uses. Honest scope: it only reconstructs what the capsule already carries (run start/finish, model-call and tool-call events with a usable timestamp); a documented follow-on "incident → session → lineage collector" does not exist yet, so lineage coverage is always reported as a gap, never fabricated. Each source is capped (MAX_EVENTS_PER_SOURCE); anything beyond the cap is a gap, not a silent drop.

Assets & registry (19)

Method Path Summary
GET /api/assets list assets endpoint
POST /api/assets register asset endpoint
POST /api/assets/register-from-yaml Register an asset from a YAML spec string (used by suggest-register panel).
GET /api/assets/{asset_id} Asset detail with its recent eval history (bounded: eval_limit default 50, max 500; adds eval_results_total/eval_results_truncated).
GET /api/assets/{asset_id}/approvals Return approval records for an asset (by UUID).
POST /api/assets/{asset_id}/approve approve asset endpoint
POST /api/assets/{asset_id}/eval Eval by UUID — resolves name+version from the registry then delegates.
GET /api/assets/{asset_id}/eval-history eval history endpoint
POST /api/assets/{asset_id}/promote Promote by UUID — resolves name+version from the registry then delegates (same ADR-0312 gate).
GET /api/assets/{name}/diff asset spec diff endpoint
POST /api/assets/{name}/rollback Roll back to the most recent version the registry's status history shows was in production (ADR-0312 D1); never to a version without production history. 409 when there is none; a gated target without a promote_direct allow is 403.
DELETE /api/assets/{name}/{version} Hard-delete an asset from the registry (nova unregister).
GET /api/assets/{name}/{version} get asset endpoint
POST /api/assets/{name}/{version}/eval eval asset endpoint
POST /api/assets/{name}/{version}/promote Single-actor promotion, gated exactly like nova promote direct (ADR-0312): production needs an explicit promote_direct policy allow (403 otherwise); force does not bypass it.
GET /api/diff diff two runs; returns summary (changed/added/removed), sections (the nova diff --output-format json sections) and has_changes (the --assert-no-regressions property) beside the legacy report
POST /api/eval/compare Compare two EvalResult JSON objects for regression — mirrors nova eval compare.
POST /api/eval/run Run a standard eval suite against a capsule — nova eval run.
GET /api/eval/suites List registered eval suite adapters — nova eval list.

Lineage (8)

Method Path Summary
GET /api/lineage/blast-radius/{ref:path} lineage blast radius
GET /api/lineage/edges Return lineage edges (newest-first). Powers the dashboard's full-DAG view. Keyset cursor/next_cursor over (created_at, edge_id), plus true total and truncated (ADR-0199 B2).
POST /api/lineage/export-prov Export W3C PROV-JSON lineage document for a capsule.
POST /api/lineage/import Import capsule lineage events into the store — mirrors nova lineage import.
GET /api/lineage/provenance/{ref:path} lineage provenance
GET /api/lineage/replay-chain/{run_id} lineage replay chain
GET /api/lineage/time-travel/{ref:path} lineage time travel
GET /api/lineage/{run_id}/emit-openlineage Return OpenLineage events for a capsule as JSON (nova lineage emit-openlineage).

Evidence & audit (8)

Method Path Summary
GET /api/audit list audit
GET /api/evidence List Evidence Bundles (*.zip in the evidence directory), newest first. Only regular files are listed: a symlink or other special file named *.zip is skipped and not counted in total. verified is the verdict of the same verifier as POST …/verify under the operator trust list, cached per archive SHA-256 (a file-identity key reused a stale verdict on filesystems with 1 s timestamps; review r8); run_ids are the runs its verified run statements sign, run_id the signed run when exactly one verifies (else the bundle id), signer_trust as in …/verify, members_signed as in …/verify. timestamp comes from the bytes that were verified. manifest.json is read with a 64 MiB bound. Before 2026-10-23 verified compared a bare hash with the prefixed manifest_hash and was always false, and run_id was the unsigned manifest label.
GET /api/evidence/completeness/{run_id} Compute the completeness assertion for a capsule.
GET /api/evidence/{bundle_id} Bundle detail: manifest, decoded run statement, member list — the bundle's own content, shown as data — plus verified, run_ids, run_id, signer_trust, members_signed and unsigned_members from the shared verifier (as in the list). The archive is copied once into a private snapshot and the verdict and everything shown come from that copy, so a bundle rewritten in place during the request never shows forged content beside verified: true (review r8). signing_key_fingerprint is the sha256: fingerprint (SHA-256 of the SubjectPublicKeyInfo DER, as in …/verify's signer_fingerprints) of the key whose signatures verified, null when none did or several keys signed; signer_fingerprints lists them. Before review r8 it hashed a signatures/run.intoto.json.cert member no bundle has, so it was always null. run_id is never the manifest's unsigned label. The ZIP is opened without following a link; a symlink, FIFO or other non-regular file returns 422, a missing bundle 404, a manifest.json over 64 MiB (or a run envelope over 16 MiB, or an archive over 1 GiB) 413.
GET /api/evidence/{bundle_id}/download Stream the bundle ZIP (Content-Length set). Read from a descriptor opened without following a link; a symlink, FIFO or other non-regular file returns 422 and no bytes, a missing bundle 404.
POST /api/evidence/{bundle_id}/verify Verify an evidence bundle with the same verifier as nova verify <bundle>.zip: every artifact digest, no missing, extra or duplicate member, manifest_hash, every attestation's DSSE signature, and each signed subject recomputed from the bundle's own content, plus the RFC 3161 token and the local NovaSeal log. The verifying key ships inside the bundle, so valid: true proves consistency, not origin. signer_trust is anchored only when every signer is in the operator's trust list (NOVAFABRIC_EVIDENCE_TRUSTED_KEYS, default $NOVAFABRIC_HOME/evidence-trusted-keys.pem); unanchored means no trust list is configured; untrusted (with valid: false) means a signer is not in it. A key in the request is never used. run_ids lists every run a verified run statement signs and run_id is that run when exactly one verified, else null; a manifest subject, label or top-level run_id that no verified statement signs fails the bundle. members_signed is true when the bundle's members statement (attestations/bundle.intoto.json) covers every member and the manifest's fields; then a rewritten, added or removed member fails valid. A bundle exported before that statement existed can be valid with members_signed: false, and unsigned_members names what no signature covers (always README.md, schemas/ and manifest.json) — signed content only, never fully verified (details). The local NovaSeal log is checked only for signed runs. An unusable configured trust list returns 500, a link or non-ZIP 422, a bundle over 1 GiB uncompressed (or a manifest.json over 64 MiB) 413.
POST /api/evidence/{run_id} export evidence endpoint
POST /api/evidence/{run_id}/bind Build criterion-evidence bindings for a capsule against a profile.

Compliance & governance (27)

Method Path Summary
POST /api/aibom/generate Generate CycloneDX AI-BOM(s) for one or all capsules — nova aibom generate.
GET /api/aibom/status Show CRA SBOM compliance status — nova aibom status.
GET /api/compliance/annex-iv Build and return an EU AI Act Annex IV document as JSON-LD.
POST /api/compliance/audit/bundle Export a compliance evidence bundle as base64-encoded ZIP (nova audit bundle).
GET /api/compliance/audit/coverage Per-control coverage analysis for the local capsule store (nova audit coverage).
GET /api/compliance/audit/map List evidence checkers for a compliance profile.
POST /api/compliance/audit/report Run compliance audit against a capsule and return a coverage report.
POST /api/compliance/audit/verify Validate the structure of an audit report JSON-LD (nova audit verify).
POST /api/compliance/erasure/request Experimental — works today (ADR-0210). (Makes PII unreadable only in capsules captured with --pii-subject, ADR-0314.) Execute a GDPR Art.17 erasure request through the real DEK destruction machinery (DEKStore.erase_subject, same code path as nova pii erase). Safe-mutations gated: body must carry confirmed: true (400 otherwise, nothing mutated). Persists the request PENDING in $NOVAFABRIC_HOME/erasure.db, executes synchronously, returns the terminal state (COMPLETED/DEFERRED/FAILED/HELD) with a receipt + receipt_sha256. Legal holds win (GDPR Art.17(3)(e)): while any legal hold is active the DEK is kept and the request ends HELD → 409 {error: "legal_hold_active", erased: false} with an ErasureHeldReceipt naming the holds and held capsules. Gated on crypto-shredding's own prerequisite: when the DEK store cannot be loaded → 409 {error: "crypto_shred_unavailable", reason}. NOVA_CAP003_ENABLED (cap-003, the dual-object split) does not gate it; cap003_enabled is reported for information only.
GET /api/compliance/erasure/status Experimental — works today (ADR-0210). List persisted GDPR erasure requests from erasure.db, newest first (?subject_id= exact filter, ?limit=, default 200). Rows are hash-only at top level (subject_sha256); receipts appear verbatim. Empty queue returns requests: [] honestly.
POST /api/compliance/euaiact/export Export EU AI Act Art.12 structured log records (ADR-0076).
GET /api/compliance/euaiact/status Return EU AI Act Art.12 compliance configuration (ADR-0076).
POST /api/compliance/examiner/{format} Export a capsule in examiner format (bagit / pccp / iso42001).
POST /api/compliance/export/aibom Export CycloneDX 1.7 AI-SBOM (ML-BOM) — nova export-aibom.
POST /api/compliance/export/c2pa Export C2PA v2.3 manifest for a capsule (ADR-0074 / EU AI Act Art.50).
POST /api/compliance/export/hipaa-proof Export HIPAA Safe Harbor de-identification proof — nova export-hipaa-proof.
GET /api/compliance/export/kinds The registry catalog the dynamic panel renders (ADR-0200 §2).
POST /api/compliance/export/nist-rmf Export NIST AI RMF 1.0 quantitative risk report — nova export-nist-rmf.
POST /api/compliance/export/rocrate Export a capsule as W3C RO-Crate v1.1 ZIP (base64-encoded).
POST /api/compliance/export/ropa Export GDPR Art.30 Records of Processing Activities (RoPA) — nova export-ropa.
POST /api/compliance/export/{kind} Run one registry exporter — the same builder its CLI command calls.
GET /api/compliance/nis2 Build and return a NIS2 incident report as JSON.
POST /api/compliance/pii/erase Destroy a data subject's DEK — nova pii erase (experimental: makes PII unreadable only in capsules captured with --pii-subject, ADR-0314). While any legal hold is active the DEK is kept: 409 {error: "legal_hold_active", erased: false, receipt} (GDPR Art.17(3)(e)). Optional retention_months must be an integer in 0..1200 (default 6; 0 = no Art.17(3)(b) window, as nova pii erase --retention-months 0); a negative, non-integer, boolean or string value is 422 and no key is touched.
GET /api/compliance/subject-proof Return GDPR Art. 17 redaction proof for a data subject.
GET /api/governance/classify Classify an AI system risk tier inferred from a Run Capsule.
POST /api/governance/classify-manual Classify a manually described AI system — nova classify run.
GET /api/governance/vocabularies List vocabulary versions — nova classify list-vocabularies.

Generic compliance-export registry (works today, ADR-0200 §2)

GET /api/compliance/export/kinds returns the export-kind catalog (id, label, required/optional fields) that the dashboard's dynamic export panel renders; POST /api/compliance/export/{kind} runs the matching CLI-only exporter's own builder function given a JSON body instead of CLI flags, and every call is audit-logged with its cli_equivalent. This complements — and does not replace — the bespoke literal routes above (/api/compliance/export/aibom, c2pa, hipaa-proof, nist-rmf, rocrate, ropa), which stay in place; the generic {kind} route is registered last so it never shadows them (Starlette matches in registration order).

Policy & approval (8)

Method Path Summary
GET /api/policy/capture-level Return the current capture level, each tier's designed field allowlist (tiers — not enforced, allowlist_enforced: false), and per tier enforcement.<level>.{enforced, effect}: whether selecting it changes capture today (only forensic and air_gapped). DB-CAP-1, cap-004, ADR-0066 Am.1.
POST /api/policy/capture-level Validate a capture-level value and return restart instructions (DB-CAP-1).
POST /api/policy/check Evaluate a policy check interactively. Body: a PolicyInput plus optional explain and policy_source. read for the packaged policies; a non-blank policy_source needs admin (403 otherwise; 422 if it is not a string). opa runs sandboxed (see Authorization).
GET /api/policy/explain Look up a past policy decision from the audit log (nova policy explain). audit scope. decision_id is matched exactly against an entry's audit id or recorded decision id (no prefixes); an empty or wildcard id is 422. Bounded O(page) tail read, newest-first; limit (default 100, max 1000) + byte-offset cursor; adds next_cursor/truncated (ADR-0199 B2).
GET /api/policy/list List Rego bundle files and signed promotion policies — nova policy list.
GET /api/policy/recent-decisions Return recent decision IDs from the audit log for autocomplete. Bounded O(page) tail read; byte-offset cursor; adds next_cursor/truncated (ADR-0199 B2).
POST /api/policy/sign Sign and store a new promotion policy — nova policy sign.
POST /api/policy/test Run the Rego test suite for the policy bundle (nova policy test). read for the packaged bundle; a bundle_path needs admin and is confined to the read roots. opa test runs sandboxed.

Sealing & trust (10)

Method Path Summary
GET /api/seal/log/verify Verify internal consistency of the local Merkle log (ADR-0041). The log (NOVAFABRIC_SEAL_DB_PATH, merkle_db, then $NOVAFABRIC_HOME/novaseal-merkle.db) is opened read-only: nothing is created or migrated. A cleanly closed log is opened with SQLite's immutable=1, so not even its -wal/-shm side files are created; while a writer is connected (side files present) the open uses normal locking, reads every committed leaf and adds no file (review r8). 404 when no log exists, 501 for a Postgres log (use nova seal log verify; the DSN is shown without its password), 422 for a file that is not a NovaSeal log. capsule_included (with capsule_id, at most 256 characters) is an exact capsule_id match, never a pattern.
GET /api/seal/policy Return the latest promotion policy predicate.
POST /api/seal/ratchet/init Provision epoch-0 ratchet state for a node.
POST /api/seal/ratchet/rotate Advance to the next signing epoch; erase the previous chain key.
GET /api/seal/ratchet/status Show a node's current signing epoch and registry history.
POST /api/seal/sigstore/sign Sign a capsule artifact using Sigstore keyless signing (ADR-0071).
POST /api/seal/sigstore/verify Verify a stored Sigstore bundle for a capsule (ADR-0071).
POST /api/seal/{capsule_id}/bypass Create a time-limited SoD bypass, DSSE-signed and permanently logged (ADR-0059).
GET /api/seal/{capsule_id}/proposals List all proposals for a capsule with their approval status.
POST /api/seal/{capsule_id}/verify Run the five-check SoD verifier for a capsule's promote bundles.

Both routes bind to the served capsule's content (review cli-b-01): a bypass signs the SHA-256 of its capsule.yaml (an unknown capsule answers 404, nothing is signed), and verify recomputes it — a proposal or bypass signed for other content, or a capsule that no longer exists, fails with exit_code: 10; the response carries content_checked: true.

POST /api/seal/{capsule_id}/bypass fails closed: the bypass is stored only after its seal.bypass.create entry (actor serve:<token fingerprint>) is in the hash-chained audit log. If that entry cannot be written, the route answers 503 and stores nothing. If the entry was written but the bypass file then cannot be stored (disk full, I/O error), a compensating seal.bypass.abort entry for the same bypass_uuid withdraws it and the route answers 503 (review r8; it was a raw 500 with no dashboard record). Every attempt that reaches the store also writes a seal_bypass record to the dashboard audit log. A stored bypass counts in verification only while the audit log holds its seal.bypass.create entry and no seal.bypass.abort (NovaSeal configuration §4.1). The response adds notification_status (pending or not_configured) and notification_sent_to, the channel configuration the signed predicate records; the delivery outcome is the seal.bypass.notify audit entry (NovaSeal configuration §4.1).

Knowledge graph (18)

Method Path Summary
GET /api/kg/agents/{agent_id}/edges Return models, tools, and MCP servers called by an agent (DB-KG-1).
GET /api/kg/aliases List all alias-table entries, optionally filtered by canonical entity id.
POST /api/kg/aliases Register (upsert) an alias into the Tier-2 alias table.
POST /api/kg/attack-path Shortest attack path between two entities — mirrors nova kg attack-path (UC2).
POST /api/kg/blast-radius Impact/blast-radius of an entity — mirrors nova kg blast-radius (UC3).
POST /api/kg/detect Unsupervised SPKG anomaly scan of a capsule — mirrors nova kg detect (ADR-0111).
GET /api/kg/entity-queue Return pending ReviewItems from the Tier-3 human review queue (bounded: limit default 200, max 1000; adds true total + truncated).
GET /api/kg/entity-queue/stats Return pending/approved/rejected counts for the entity review queue.
POST /api/kg/entity-queue/{item_id}/approve Approve a review item. Body: {canonical: str, resolved_by: str}.
POST /api/kg/entity-queue/{item_id}/reject Reject a review item. Body: {resolved_by: str}.
POST /api/kg/ingest Ingest a capsule directory into the KG — mirrors nova kg ingest.
POST /api/kg/ingest-all Bulk-ingest all capsule directories into the KG.
POST /api/kg/init Initialise the KG schema (idempotent) — mirrors nova kg init.
GET /api/kg/status Return Capsule KG store health + entity counts (DB-KG-1, ADR-0067).
GET /api/kg/topology Return all KG nodes and edges for multi-layer topology visualization.
GET /v1/kg/audit KG health audit: node/edge counts, zero-call-count edges.
GET /v1/kg/query Query models and tools for an agent. Mirrors nova kg query.
GET /v1/kg/status KG store health check (Tier 2+ aware). Alias of /api/kg/status.

Assurance & MCP (3)

Method Path Summary
GET /api/assure/{run_id} Run OWASP Top 10 for LLM evidence checks against a capsule.
POST /api/mcp/risk-report Generate a structured OWASP LLM risk report for an MCP manifest.
POST /api/mcp/scan Scan an MCP server manifest for OWASP LLM supply-chain risks.

Storage & infrastructure (7)

Method Path Summary
POST /api/db/upgrade Run alembic upgrade to the specified revision (default: head).
GET /api/infra/backups List backup archives in NOVA_BACKUP_DIR (manifest-claimed, not hash-verified — that's nova backup verify); {detected: false} when unconfigured (ADR-0201 P7).
GET /api/infra/collector Return collector health. Returns {detected: false} if not running.
GET /api/storage/inspect/{run_id} Show dual-object split for a run (DB-STG-1, cap-003).
GET /api/storage/manifest-chain Return the last N entries in the manifest chain.
GET /api/storage/stats Return object capsule store statistics.
GET /api/storage/validate Validate S3 backend supports Object Lock COMPLIANCE (DB-STG-1, cap-009).

Cost & metrics (7)

Method Path Summary
POST /api/cost/attribute Compute productive-vs-wasted spend attribution over a request document — same core as nova cost attribute (ADR-0146 D3).
POST /api/cost/fairness Compute per-agent spend share + Gini fairness report — same core as nova cost fairness (ADR-0146 D5).
GET /api/cost/pricing Return the per-1k-token price table from CostInterceptor (DB-COST-1).
GET /api/cost/report Return aggregated LLM cost (DB-COST-1 / cap-002).
POST /api/cost/usage-breakdown Compute token usage-type composition — same core as nova cost usage-breakdown (ADR-0132).
GET /metrics metrics endpoint
GET /metrics/stream SSE metrics stream (FR-21: Last-Event-ID reconnect support).

The three /api/cost/* POST routes above are pure, deterministic, read-only wraps of the same cost cores their nova cost CLI counterparts call (no capsule I/O, no writes, not audit-logged — computing a bounded aggregate over caller-supplied numbers isn't a mutating or boundary-crossing action).

Reports (17)

Method Path Summary
GET /api/report Generate an asset inventory report — mirrors nova report.
GET /api/reports/alert-digest Registry-driven data route (R4) — {columns, rows, count} JSON or CSV.
GET /api/reports/api-key-inventory Registry-driven data route (R4) — {columns, rows, count} JSON or CSV.
GET /api/reports/capsule-compare report capsule compare
GET /api/reports/catalog Return the report registry (identity, filters, chart specs) — the single source of truth the dashboard ReportsTab reads for preview charts (ADR-0200/0201).
GET /api/reports/compliance-posture Registry-driven data route (R4) — {columns, rows, count} JSON or CSV.
GET /api/reports/cost-burn report cost burn
GET /api/reports/dashboard-audit Registry-driven data route (R4) — {columns, rows, count} JSON or CSV.
GET /api/reports/eval-regression report eval regression
GET /api/reports/evidence-inventory report evidence inventory
GET /api/reports/executive-summary report executive summary
GET /api/reports/policy-audit report policy audit
GET /api/reports/release-comparison report release comparison
GET /api/reports/run-history report run history
GET /api/reports/seal-verification report seal verification
GET /api/reports/throughput report throughput
GET /api/reports/{report_id}/export?format=html|pdf Export a self-contained report artifact with an inline-SVG chart where the registry declares one (ADR-0201: server-side chart rendering). HTML needs nothing; PDF lazily imports the optional WeasyPrint extra and degrades to 501 + install hint when absent (pip install 'novafabric[compliance]'). Scope is the exported report's own (audit for dashboard-audit/policy-audit, admin for api-key-inventory and unknown ids, otherwise read).
Method Path Summary
GET /api/holds list holds
POST /api/holds create hold
POST /api/holds/{hold_id}/release release hold

Schema (1)

Method Path Summary
GET /api/schema/list List the CapsuleEventType values + meta (DB-SCH-1, cap-001 / ADR-0066;

Topology (live) (5)

Method Path Summary
POST /api/topology/seed Seed the live topology store from capsules already on disk.
GET /api/topology/snapshot Return current topology counts for the SPA status bar.
GET /topology/cluster-edges Return inter-cluster edge aggregates for drawing edges between super-nodes.
GET /topology/cluster-list Return clusters as plain JSON rows (largest first) for the Table/Treemap views.
GET /topology/clusters Return Arrow IPC cluster layer (ADS v1 cluster_layer schema).

Admin (11)

Method Path Summary
GET /api/admin/api-keys Read-only, secret-free API-key projection (ADR-0193). Bounded: limit (default 500, max 2000) pushed into SQL; total is a true COUNT(*); adds truncated (ADR-0199 B2).
POST /api/admin/flush-jwks-cache Flush the JWKS cache, forcing a re-fetch from the OIDC provider.
GET /api/admin/new-run-id Generate a fresh ULID for use as NOVAFABRIC_GLOBAL_RUN_ID (cap-007 FR-27).
POST /api/admin/rebuild-metadata-db Disaster-recovery rebuild of the metadata DB from the chain log.
POST /api/admin/reindex-runs Confirm-gated ({"confirmed": true}, else 400), idempotent INSERT-OR-REPLACE rebuild of the runs_cache index from the capsule filesystem — never deletes a capsule or a row with a still-existing capsule (ADR-0201 P8). Optional {"prune": true} (default false) also drops rows whose capsule directory is gone; response gains pruned.
GET /api/admin/roles List role assignments (local mode).
POST /api/admin/roles Assign a role to a subject (idempotent). Local-mode admin shortcut.
DELETE /api/admin/roles/{subject}/{role} Revoke a role from a subject. 404 if not found, 409 if lockout would occur.
GET /api/admin/tokens List issued local tokens (stored in ~/.novafabric/tokens.jsonl), newest-first. Each row reports the scope it is enforced with: a record written before ADR-0228 has none and is enforced as admin; a record whose scope is not recognised is reported as none and holds no scope. Bounded: limit (default 200, max 2000) + byte-offset cursor; adds next_cursor/truncated (ADR-0199 B2). Requires admin scope.
POST /api/admin/tokens Issue a new local session token. The token is returned once and stored only as a sha256: digest (0600); it cannot be recovered from the file. It authenticates by Authorization: Bearer or ?token=. Optional scope — read, operate, admin (default) or audit (ADR-0228) — bounds what it may reach; the response echoes the scope it minted. Omitting it reproduces the pre-ADR-0228 behaviour, the same full access as the dashboard token. Requires admin scope.
DELETE /api/admin/tokens/{fingerprint} Revoke a token by fingerprint. Revocation takes effect immediately: the next request with that token gets 401.

alerts (1)

Method Path Summary
GET /api/alerts/recent recent alerts

analytics (1)

Method Path Summary
GET /api/analytics/summary analytics summary

incidents (5)

Method Path Summary
GET /api/incidents List the newest incidents with their nearest Art. 73 deadline.
POST /api/incidents Open a new incident (forward-only lifecycle; never deleted).
GET /api/incidents/{incident_id} Get one incident with computed Art. 73 deadlines.
GET /api/incidents/{incident_id}/export Export an incident as an OECD-AIM or NIS2 structured report.
POST /api/incidents/{incident_id}/transition Advance an incident's lifecycle (open → reported → closed).

ingest-capsule (1)

Method Path Summary
POST /api/ingest-capsule Index capsule(s) into the runs metadata store — nova ingest-capsule.

The route indexes without opening a file watcher. Before 2026-10-23 each request started an inotify instance and watcher threads that were never closed, so about 128 requests could exhaust the user's inotify instances.

lineage-store (1)

Method Path Summary
GET /api/lineage-store/profile Generate a lineage-store deployment profile — nova lineage-store profile.

livez (1)

Method Path Summary
GET /livez livez

ops (1)

Method Path Summary
GET /api/ops/daemon-status Report whether the warm capture daemon socket is alive (read-only).

query (1)

Method Path Summary
POST /api/query Execute a Capsule Query DSL document ({q, engine?}) — the same JSON/YAML query object nova query --query-file accepts, not a free-text/SQL string (ADR-0129, no new grammar). Read-only, capped at 5000 rows regardless of the plan's own limit; a 422 on any parse/allow-list failure, a 500 only on a malformed on-disk capsule index.

readyz (1)

Method Path Summary
GET /readyz readyz

Deprecation register (ADR-0188)

Maturity: mechanism, CI drift gate and CI removal gate work today (experimental); the register is empty. Policy and machinery: ADR-0188.

Scope. The lifecycle policy applies to the multi-user nova server API only (api/openapi.yaml, /v0 and later). The nova serve dashboard API documented in the tables above remains experimental/unversioned — no lifecycle promises — until the planned ADR-0183 consolidation.

Breaking-change definition (normative). Breaking: removing or renaming a path, query parameter, or response field; narrowing a type or enum; tightening auth or required scopes on an existing endpoint; changing error-envelope semantics (codes, envelope shape). Non-breaking: adding optional fields, endpoints, enum values documented as open, or response headers. If it is not on the breaking list, it may ship in any release; if it is, the lifecycle below is mandatory.

Lifecycle. A deprecation is announced in CHANGELOG.md, the release notes, and this register, all at once. From the deprecating release onward the endpoint emits three headers on every response (implemented in src/novafabric/server/deprecation.py):

Header Format Meaning
Deprecation: @<unix-timestamp> or true (RFC 9745) The endpoint is deprecated (and since when, if known).
Sunset: HTTP-date (RFC 8594) Earliest date the endpoint may be removed.
Link: <url>; rel="deprecation" Points at this register's entry for the endpoint.

Minimum window. A deprecated endpoint stays working for at least two minor releases. Removal lands only in a minor version bump pre-1.0 and only in a major bump post-1.0. No silent removals, ever. Every register row names the deprecating release, an earliest-removal release at least two minors later, and a replacement (or an explicit "none"); each row must match a deprecated: true operation in api/openapi.yaml (drift gate, tests/test_deprecation_drift_gate.py).

Removal gate (works today, experimental). tests/test_deprecation_removal_gate.py fails CI when this register breaks the lifecycle rules (pure checks in src/novafabric/server/deprecation_gate.py):

Removed rows stay in the table as the audit trail and are excluded from the drift gate (they no longer carry deprecated: true in api/openapi.yaml).

Register

No endpoints are currently deprecated.

Endpoint Deprecated in Earliest removal Sunset date Replacement Removed in
— — — — — —

Interactive API docs

When the server is running, the same route table is browsable interactively:

Summary & next steps

Where to go next: