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:
- A derived route is never weaker than its base.
GET /api/reports/{report_id}/exportneeds the scope ofGET /api/reports/{report_id}: exportingdashboard-auditorpolicy-auditneedsaudit, exportingapi-key-inventoryneedsadmin, and an unknown report id needsadmin(so a non-admin gets 403, not 404). - Rego the server did not ship needs
admin.POST /api/policy/checkwith a non-blankpolicy_source, andPOST /api/policy/testwith abundle_path, return 403 belowadmin(audited like any other denial). Areadcredential evaluates and tests the packaged policies only.
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.
Capsule files and symbolic links
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 aWS /api/tv5/wswebsocket. 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 localhostHostguard (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):
scope=meta(or omitted) — the existing behavior, unchanged:qmatchesrun_idand command metadata only.scope=content— experimental: full-text search (SQLite FTS5) over the redacted capsule text indexed at ingest (ADR-0204). Requires a non-emptyq(else400). Each item carries amatchesarray of{stream, ref, line_no, snippet}plusmatches_truncated(max 8 matches per run); the response addssnippet_markers(["«", "»"]). Results are relevance-ranked (bm25);next_cursoris alwaysnullfor content scopes in v0. User query input is quoted before it reaches the MATCH grammar — FTS5 operators (OR,NEAR,-,:) match literally.scope=all— experimental: content hits first (withmatches), then metadata hits in the existing newest-first order, deduplicated byrun_id.
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). |
Legal holds (3)
| 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):
- every row names a deprecation release, an earliest-removal release at least two
minors later (
earliest ≥ major.(minor+2).0of the deprecation release), and a replacement ornone; - a register-listed endpoint that is no longer served (neither mounted by the default
nova serverapp nor declared inapi/openapi.yaml) must keep its row and record the removing release in Removed in; that release must be ≥ the earliest-removal release, ≤ the current package version (novafabric.__version__), and a legal removal point — a minor0.Y.0pre-1.0, a majorX.0.0post-1.0; - a row that records Removed in while the endpoint is still served fails.
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:
- Swagger UI:
http://127.0.0.1:4321/api/docs?token=<token> - OpenAPI JSON:
http://127.0.0.1:4321/api/openapi.json
Summary & next steps
- This document covers the local, experimental
nova serve --experimentalHTTP surface — a read-first (Layer A) dashboard over capsules on your own machine. The separate multi-tenantnova serverAPI is specified inapi/openapi.yaml. api/openapi.yamlis a hand-curated spec, not a generated one — but is now essentially complete. As of this pass it documents 76 operations, matching all 76 always-mountednova serverroutes (the resource routers under/v0plus the/scim/v2/*surface; previously-noted gaps —/v0/orgs,/v0/workspaces,/v0/service-accounts,/v0/admin/roles,/v0/runs/suggest-register,/v0/seal/*,/v0/auth/saml/*, SCIM — are all present now). The one real remaining gap: the RFC 8628 device-authorization-grant demo trio (POST /v0/auth/device/code,POST /v0/auth/token,POST /v0/auth/approve,src/novafabric/server/routes/auth.py) is absent from the spec. That's arguably correct scoping rather than drift — those three are unauthenticated demo scaffolding mounted only when an operator opts in (config.demo_device_grant, defaultFalse; the HS256 tokens they issue are never honored by the real verifier) — but flagging it here rather than silently dropping it from this note.- Usage chargeback export (experimental, ADR-0208 P3):
GET /v0/usage/export?from=YYYY-MM&to=YYYY-MM&format=csv|ndjson(optionalworkspace=/org=) on thenova serverAPI streams the same rows asnova server usage exportas an attachment —text/csv; charset=utf-8; header=present(RFC 4180, formula-injection-safe cells) orapplication/x-ndjson, filenamenova-usage-<from>_<to>.<ext>. Admin/auditor see every workspace; any other principal only its membership workspaces (filtered, not 403). Bad period/format or a range wider than 120 periods →400error envelope (invalid_period/invalid_period_range/invalid_format). Read-only (registry openedmode=ro, membership lookup included); a locked/unreadable registry →503(usage_store_unavailable), never an empty 200; not audited. - Requests to
/api/*require the session token, in either of two forms (v0.97.0): a?token=<token>query parameter, or anAuthorization: Bearer <token>header — when the header carries a Bearer credential it is authoritative and the query parameter is ignored (an absent or non-Bearer header falls back to?token=). The query form is kept for the SPA and for printed/bookmarked URLs; prefer the header for scripts andcurl, so the secret stays out of shell history, proxy logs, andReferer. The sevenpublicroutes are open: the probesGET /api/health,GET /livezandGET /readyz, the interactive docs (GET /api/docs,GET /api/openapi.json,GET /docs/oauth2-redirect) and the no-dashboard placeholderGET /(/metricsrequires the token). The server binds to127.0.0.1and rejects non-localhostHostheaders. - The endpoint tables are generated — after any route change a maintainer
re-runs the generator so this file stays truthful. The generator lives in the
maintainers' private
design/tree and is not published, so a contributor cannot regenerate this page; note the drift in a PR and it will be regenerated. - A route existing in the table is not by itself a claim that everything behind it is
fully shipped — several endpoints are explicitly marked experimental inline above
(e.g. the ADR-0204 content-search
scopeparameter, the ADR-0210 GDPR erasure pair, the OTLP ingest endpoint); anything not so marked is works today, per this repo's docs-honesty convention (works today / experimental / planned / future design).
Where to go next:
- CLI reference (
docs/cli-reference.md) — the canonical, offline interface; every dashboard view maps to anovacommand. - Developer guide (
docs/developer-guide.md) — adding asset types, CLI commands, and report formats. nova serve --help— the authoritative, always-current list of server flags.