Server, storage and backup commands
Part of the NovaFabric CLI reference. Both nova and novafabric run the same binary.
Storage and server commands (v0.7)
Two optional server-mode components ship in v0.7 — they are independent:
Command Package extra Purpose Auth nova serve --experimentalnovafabric[serve]Local dashboard — browse capsules, registry, and lineage in a browser. Single-user, loopback-only. Not read-only — it also exposes mutating and irreversible operations behind the session token. Single-use token nova server startnovafabric[server]Multi-user REST API — Postgres/SQLite backend, OIDC + RBAC, offline tokens. For teams and CI pipelines. OIDC Bearer / offline JWT Local CLI commands (
nova capture,nova validate,nova replay, etc.) require neither extra and work without any server.
Requires: pip install 'novafabric[server]'
nova doctor [--check-extras] [--check-storage] [--check-scheduler] [--check-tokens]
Run diagnostic checks on the NovaFabric installation.
nova doctor --check-extras
nova doctor --check-storage
nova doctor --check-storage --backend postgres
nova doctor --check-storage --backend postgres --postgres-dsn "postgresql://..."
nova doctor --check-storage --db-path /path/to/custom.db
nova doctor --check-scheduler
nova doctor --check-tokensWith --check-tokens: reports how many serve-token records still store the secret itself
rather than only a fingerprint. ADR-0252 stopped writing the secret, but could not rewrite
records already on disk, so an upgraded install can be correct for new tokens and still hold
old ones in the clear.
This one report also prints during any nova doctor run, not only with its flag — an
operator who does not already know such records exist is exactly the operator who will not
think to ask about them. Only the explicit --check-tokens makes it affect the exit code
(1 when any are found), so no existing invocation changes its result. The report counts
the affected records and never prints a secret.
With --check-extras: lists every optional extra as complete or incomplete, names the
distributions missing from each, and prints the exact pip install 'novafabric[<extra>]'
command. This answers "which extra do I need?" without reading pyproject.toml.
The extra names and their requirements are read from the installed distribution metadata,
so the report cannot drift from what the package actually declares. Presence is checked per
distribution, not by importing — a distribution name is not reliably its import name
(python-louvain imports as community), and a diagnostic should not execute third-party
imports just to look.
Exit code stays 0. Most installs deliberately omit most extras, so an incomplete extra is information, not failure.
$ nova doctor --check-extras
Optional extras
✓ query duckdb
✗ serve missing: fastapi, uvicorn
1 of 31 extras incomplete. Features that depend on them will fail at import.
Install one with:
pip install 'novafabric[serve]'
Developing on the repo? `uv sync --all-extras` installs every one.Without any flag, prints a hint and exits 0. With --check-storage:
- Reports the active backend (
sqliteorpostgres). - Shows the Alembic schema version and migration status.
- Prints per-table row counts.
- Exits 0 on success, 1 on error.
With --check-scheduler (works today, OQ-06 / PAR-ADR-003 condition 2): detects a
mismatch between a scheduler's own native env vars (e.g. Slurm's SLURM_JOB_ID, set by
the scheduler daemon itself) and the NOVAFABRIC_* env-var contract (FR-18) a submission
wrapper should have propagated into the job. This surfaces, on demand, the case FR-20's
runtime fallback otherwise handles silently: capture still defaults to
capsule_role=STANDALONE with a synthesised global_run_id so the workload is never
blocked, but that quietly loses parent/child linkage for the run. For Slurm specifically,
also reads SLURM_EXPORT_ENV to distinguish a site --export=NONE/NIL policy (env
export disabled at the cluster level) from a submission-script gap (the wrapper simply
never set NOVAFABRIC_GLOBAL_RUN_ID). Exits 0 if no scheduler is detected or the contract
vars are present; exits 1 with a diagnosis + remediation hint otherwise.
Options:
--check-storage— enable storage health report (ADR-0016)--check-scheduler— enable the scheduler/env-var contract check (OQ-06)--backend TEXT—sqlite(default) orpostgres--db-path PATH— override the SQLite path; defaults toNOVAFABRIC_DB_PATH--postgres-dsn TEXT— Postgres DSN; defaults toNOVAFABRIC_POSTGRES_DSN
nova ingest-capsule
Populate the runs_cache index from capsule files on disk. Does not require
nova serve to be running. Requires Scale-S1 (runs_cache table) which ships
with nova serve.
nova ingest-capsule [RUN_ID] [OPTIONS]Arguments:
RUN_ID— Run ID to ingest (required unless--allor--watch)
Options:
| Flag | Default | Description |
|---|---|---|
--all |
off | Re-index all capsules in capsule-dir |
--watch |
off | Foreground watcher loop (Ctrl+C to stop) |
--interval FLOAT |
2.0 | Poll interval in seconds (--watch only) |
--backend TEXT |
auto | auto | polling | watchdog |
--capsule-dir PATH |
$NOVAFABRIC_CAPSULE_DIR |
Override capsule directory |
--db-path PATH |
$NOVAFABRIC_DB_PATH |
Override registry DB path |
Environment variables:
| Variable | Default | Effect |
|---|---|---|
NOVA_WATCHER_BACKEND |
auto |
Override backend selection globally |
NOVA_WATCHER_INTERVAL |
2.0 |
Override poll interval globally |
Examples:
# Index a specific run
nova ingest-capsule abc123
# Full re-index after moving capsules from another machine
nova ingest-capsule --all
# Foreground watcher — prints each new capsule as it appears
nova ingest-capsule --watch --interval 5
# Use inotify/FSEvents backend (requires pip install novafabric[watch])
nova ingest-capsule --watch --backend watchdogWorks today. Requires novafabric ≥ v0.36.0.
nova migrate-to-postgres
One-time idempotent migration from a local SQLite registry to Postgres. Per ADR-0016.
# Dry run — see what would be migrated without writing
nova migrate-to-postgres --dry-run
# Full migration
nova migrate-to-postgres \
--source ~/.novafabric/registry.db \
--target "postgresql://user:pass@host/db"
# With a JSONL migration log
nova migrate-to-postgres --target "$NOVA_DSN" --log migration.jsonlOptions:
--source PATH— source SQLite database (default:~/.novafabric/registry.db)--target TEXT— Postgres DSN; can also be set viaNOVAFABRIC_POSTGRES_DSN--dry-run— list capsules that would be migrated without writing--log PATH— optional JSONL log file (one record per table)
Exit codes: 0 = success (row counts verified), 1 = verification failure, 2 = connection error.
The SQLite file is never modified or deleted. Upsert semantics prevent duplicates on re-run after a partial failure.
nova migrate-schema
Batch-migrates capsule directories from schema v0 to v1.0.0. Walks every capsule sub-directory under --capsule-dir, inspects the manifest, and applies any needed upgrades. Safe to re-run — already-migrated capsules are skipped.
# Preview changes without writing (recommended first pass)
nova migrate-schema --capsule-dir ~/.novafabric/capsules --dry-run
# Migrate in-place
nova migrate-schema --capsule-dir /data/nova/capsules
# Migrate with .v0.bak rollback copies
nova migrate-schema --capsule-dir /data/nova/capsules --backupWhat the migration does for each capsule whose manifest.json is below v1:
- Sets
schema_version→"1.0.0" - Renames
event_log.jsonl→model-calls.jsonl(legacy v0 file name) - Adds
format_version: "1"to the metadata block when absent
Options:
--capsule-dir PATH— root directory containing one sub-directory per capsule (default:~/.novafabric/capsules/)--dry-run— preview what would change; no files are written--backup— copy original files to<file>.v0.bakbefore overwriting; allows manual rollback
Exit codes: 0 = all capsules migrated or already at v1, 1 = one or more capsule migrations failed.
Implemented in src/novafabric/cli/migrate_schema.py (G-F track, v0.29.0).
nova migrate-format (experimental, ADR-0165 NF-332)
Experimental. Records a format-migration hop in a capsule's preservation facet
(facets.preservation.format_migration_chain) and walks the chain offline back to the
facet's original_root. Each hop carries from_version, to_version, migrated_at,
tool_ref (a sha256: digest or URI of the migrator — never the migrator itself),
pre_digest, post_digest, and parent (the prior hop's post_digest, null for the
first). parent and pre_digest are derived from the chain, never typed in.
Record-only. It does not run the migrator and never modifies a stored capsule —
rewriting capsule.yaml would change the bytes its seal covers. The updated facet is
written as JSON to stdout or to a new --output file (an existing file is never
overwritten). Unlike nova migrate / nova migrate-schema, which rewrite a capsule to the
current schema, this command only records that a migration happened elsewhere.
# Record the first hop; from_version defaults to run-capsule@<schema_version>
nova migrate-format --capsule 01HX... --to run-capsule@0.3.0 \
--tool sha256:<migrator-digest> --migrated-artifact migrated/capsule.yaml \
-o preservation.json
# Extend a standalone facet document by one more hop
nova migrate-format --facet preservation.json --to run-capsule@0.4.0 \
--tool https://tools.example.org/migrators/0.3-to-0.4 --post-digest sha256:<hex> \
-o preservation-v2.json
# Only verify the existing chain (offline); JSON verdict on stdout
nova migrate-format --facet preservation-v2.json --check --jsonThe walk reports chain_walk_ok (no broken/missing parent), reaches_original_root,
acyclic, and monotonic (each hop moves the version strictly forward within one format
family, hops are contiguous, time never runs backwards). A hop is refused if the existing
chain is already broken or if the new hop would break it. Each accepted hop also appends a
PREMIS migration event to provenance_events.
Options: --capsule REF or --facet PATH (exactly one) · --to VERSION · --tool REF ·
--post-digest DIGEST or --migrated-artifact PATH (exactly one; the file is hashed, not
copied) · --from VERSION · --migrated-at RFC3339 (default: now, UTC) ·
--output/-o PATH · --check · --json.
Exit codes: 0 = hop recorded / chain ok, 1 = chain broken or hop refused (including a
stored format_migration_chain that is malformed or tampered; with --check --json the
verdict is {"ok": false, "malformed_record": "..."}), 2 = bad input (flags, an
unreadable or unparseable file, no anchor). Every run prints the in-mission-boundary line on stderr: NovaFabric records
format-migration provenance only and makes no claim that a migration was faithful,
authorized, or regulator-accepted.
Not yet implemented (future design, ADR-0165 P3–P5): nova preserve, nova fixity,
nova export-preservation, and sealing the updated facet into an Evidence Bundle. (The
record-only half of P3 shipped as nova preservation — below.) Implemented in
src/novafabric/cli/migrate_format.py and src/novafabric/preservation/format_migration.py.
nova preservation (experimental, ADR-0165 NF-333/334)
Experimental, record-only. Records and verifies two evidence-longevity histories inside
a capsule's preservation facet (facets.preservation):
crypto_migration(NF-333) — re-seal events{from_alg, to_alg, resealed_at, upgrade_ref, original_sig_preserved: true, renewal_timestamp_ref}.upgrade_refis an opaque reference to the NF-192upgrade-signatureoperation that did the signing.original_sig_preservedis a required literaltrue: a re-seal that dropped or overwrote the original signature is a replacement, not a migration, and is refused.ltv_renewal_chain(NF-334, RFC 4998 semantics) — archive-timestamp renewals{renewal_type, covered_digest, new_timestamp_ref, new_hash_alg, renewed_before, parent, renewed_at?, timestamp_expires_at?}.parent(the previous renewal'snew_timestamp_ref) is derived, never typed in.
It generates no key, calls no Timestamp Authority, and performs no signature (there is
no ML-DSA code in NovaFabric); the operations are recorded by reference. --capsule is
read-only — the updated facet is written as JSON to stdout or to a new --output file
(an existing file is never overwritten).
# Record a re-seal (the original-signature assertion is mandatory)
nova preservation reseal record --facet preservation.json \
--from-alg ed25519 --to-alg ml-dsa-65 \
--upgrade-ref NF-192:upgrade-signature#op-1 \
--renewal-timestamp-ref sha256:<tst-digest> --original-sig-preserved -o v2.json
nova preservation reseal verify --facet v2.json --json
# Append an LTV renewal; covered_digest defaults to the previous token for a timestamp_renewal
nova preservation ltv append --capsule 01HX... --type timestamp_renewal \
--covered-digest sha256:<evidence+tst> --new-timestamp-ref sha256:<tst-2029> \
--new-hash-alg sha256 --renewed-before 2030-01-01 --expires-at 2035-01-01 -o v3.json
nova preservation ltv verify --facet v3.json --jsonltv verify reports covers_previous (each renewal's parent is the previous token; a
timestamp_renewal covers exactly that token; a hash_tree_renewal covers a fresh state
under its own hash), hash_algs_ok (no downgrade in the explicit order sha224/sha3-224 <
sha256/sha3-256 < sha384/sha3-384 < sha512/sha3-512; an unknown algorithm such as sha1 is a
finding; a timestamp_renewal may not change the hash), and renewed_in_time
(renewed_before never moves backwards and is no later than the previous timestamp's
recorded expiry). reseal verify checks the original-signature assertion, algorithm
continuity, no post-quantum→classic downgrade, no step down in signature strength
(signature_alg_downgrade — ranked PQC above classic, then by NIST PQC security category
for ML-DSA/SLH-DSA and by classical security bits per NIST SP 800-57 for RSA/ECDSA/EdDSA,
e.g. ml-dsa-87 → ml-dsa-44 or ed25519 → rsa-pkcs1-2048), known algorithm identifiers, monotonic time,
and a fresh renewal timestamp per re-seal. record/append refuse to extend a record that
already fails, or to add an entry that would break it; each accepted entry also appends a
provenance event.
Exit codes: 0 = recorded / ok, 1 = record broken or refused (including
--original-sig-dropped, and a stored crypto_migration / ltv_renewal_chain that is
malformed or tampered — with verify --json the verdict is {"ok": false, "malformed_record": "..."}), 2 = bad input (flags, an unreadable or unparseable file,
no anchor). Every run prints the in-mission-boundary line
on stderr. Not verified offline: timestamp tokens are not dereferenced and hash-tree
digests are not recomputed — that is the NF-339 re-verification receipt (future design).
Implemented in src/novafabric/cli/preservation.py and
src/novafabric/preservation/reseal.py.
nova settlement (experimental, ADR-0163 NF-315)
Experimental, record-only. nova settlement chain walks the agent-to-agent payment
provenance chain stored in a capsule's settlement facet (facets.settlement.a2a_payment_chain)
offline. Each hop is {hop_index, payer_agent_ref, payee_agent_ref, amount: {amount_minor, currency}, settlement_ref, parent_hop} — amounts are integer minor units (never a float),
settlement_ref is a sha256: digest, and parent_hop is null on the first hop and must
name an earlier hop otherwise. NovaFabric moves no value, holds no funds, contacts no
payment network, and decides no dispute; moved_value: false is on every verdict.
nova settlement chain --capsule 01HX... # read-only
nova settlement chain --facet settlement.json --depth 3 --jsonThe walk (one linear pass, at most 1024 hops) reports ordered (hop_index equals the
recorded position, no duplicates), no_broken_parent (first hop has no parent; every other
hop's parent resolves to an earlier hop), acyclic (no parent points at itself or a later
hop), linear (no hop is the parent of two hops — the chain does not fork), continuous
(each payer is its parent hop's payee) and currency_consistent (no FX between hops — a
rate would have to be chosen, which is adjudication). --depth N limits the hops shown
(newest first, following parent_hop back); the verdict always covers the whole chain. A
card number, IBAN or credential anywhere in the facet is refused and never echoed.
Exit codes: 0 = the walk passed, 1 = the evidence is broken or absent (a failing walk,
no settlement facet or no chain, a malformed or secret-bearing facet — fail-closed), 2 =
bad input (flags, an unreadable, oversized or unparseable file, no such capsule). Every run
prints the in-mission-boundary line on stderr. The negotiated-agreement (NF-316) and
invoice/receipt (NF-319) records ship as library APIs only (novafabric.settlement);
nova settlement bind|show|verify|reconcile|finality|reversals, nova dispute and
nova export-settlement remain planned. Implemented in
src/novafabric/cli/settlement.py and src/novafabric/settlement/chain.py.
nova db (Phase 5 — MetadataStore management)
MetadataStore management commands (ADR-0040, FR-05, FR-06). Requires novafabric[server] for Postgres operations.
nova db migrate-to-postgres
Idempotent migration of MetadataStore tables (runs, capsules, signatures, retention_policies) from a local SQLite metadata database to Postgres.
nova db migrate-to-postgres \
--source ~/.novafabric/metadata.db \
--target "postgresql://nova:pass@host:5432/novafabric"
# With batch size and JSON report
nova db migrate-to-postgres \
--source ~/.novafabric/metadata.db \
--target "$NOVA_META_DSN" \
--batch-size 500 \
--report /tmp/migration-report.jsonOptions:
--source PATH— source SQLite metadata.db (required)--target TEXT— Postgres DSN (required)--batch-size INT— rows per INSERT batch (default: 1000)--report PATH— write a JSON migration report to this path
Exit codes: 0 = success (row counts verified), 1 = count mismatch, 2 = connection/import error.
All inserts use ON CONFLICT DO NOTHING — safe to re-run after a partial failure. The source SQLite file is never modified.
Requires pip install novafabric[server] (psycopg3).
nova db upgrade
Run alembic upgrade <revision> for the selected migration track. Two
parallel Alembic tracks exist (ADR-0211 D5): the MetadataStore tier
(default — unchanged behavior) and the registry/server database (the DB
the server lifespan opens and nova backup create --profile pg dumps; the
startup schema-skew guard names this track). --track registry is
experimental.
# MetadataStore tier (default; reads NOVAFABRIC_DB_PATH,
# defaults to ~/.novafabric/metadata.db)
nova db upgrade
# MetadataStore tier on Postgres (reads NOVAFABRIC_METADATA_DSN)
export NOVAFABRIC_METADATA_DSN="postgresql://nova:pass@host:5432/novafabric"
nova db upgrade --backend postgres
# Registry/server DB (experimental, ADR-0211): SQLite registry
nova db upgrade --track registry --backend sqlite
# Registry/server DB on Postgres (reads NOVAFABRIC_POSTGRES_DSN) — the
# command the schema-skew guard and the pg restore runbook name
export NOVAFABRIC_POSTGRES_DSN="postgresql://nova:pass@host:5432/nova"
nova db upgrade --track registry --backend postgresOptions:
--backend TEXT—sqlite(default) orpostgres--track TEXT—metadata(default) orregistry(experimental)--revision TEXT— alembic revision target (defaulthead)
Environment variables consumed:
NOVAFABRIC_DB_PATH— SQLite database path (metadata track)NOVAFABRIC_METADATA_DSN— Postgres DSN (metadata track)NOVAFABRIC_HOME— registry DB location (registry track, sqlite)NOVAFABRIC_POSTGRES_DSN— Postgres DSN (registry track; never printed)
The registry-track migration trees are packaged into the wheel
(novafabric/migrations/registry/), so --track registry works from an
installed package, not only a source checkout.
Exit codes: 0 = success, 1 = alembic/database failure (registry track),
2 = config/connection error.
nova server start
Start the multi-user REST API server. Per ADR-0017 and ADR-0029.
nova server start
nova server start --backend postgres
nova server start --config /etc/novafabric/nova-server.yaml
nova server start --host 0.0.0.0 --port 7433
# Horizontal scaling (v0.98.0, experimental) — requires the postgres backend
nova server start --backend postgres --workers 4
# Machine-parseable logs with per-request correlation ids (v0.98.0, experimental)
nova server start --log-format jsonThe server reads ~/.config/novafabric/nova-server.yaml by default. CLI flags
override the config file. See docs/ops/server-deployment.md
for config examples.
Single-tenant scope (ADR-0178). Capsule storage is not partitioned per
organization — organizations and workspaces scope the registry tier, not
capsule bytes. The server therefore refuses to start when more than one
organization exists, unless you acknowledge this with
--i-accept-shared-capsule-store. Single-organization deployments (the default,
and what the bootstrap creates) are unaffected. Per-tenant capsule partitioning
is pending a security review; until it lands, do not rely on organizations for
capsule isolation.
Options:
--config, -c FILE— path to server YAML config (default:~/.config/novafabric/nova-server.yaml)--backend TEXT—sqlite(default) orpostgres--host TEXT— bind address (default from config or127.0.0.1)--port, -p INTEGER— bind port (default from config or7433)--insecure-no-auth— disable local-token auth; anonymous admin (ADR-0184). Loopback only unless also passing--i-know-this-is-public. Every such start appends aserver.insecure_no_authentry to the hash-chained audit log (NOVAFABRIC_AUDIT_LOG_PATH); if that entry cannot be written, the server refuses to start--i-know-this-is-public— second confirmation required to combine--insecure-no-authwith a non-loopback--host--i-accept-shared-capsule-store— acknowledge the unpartitioned capsule store and run multiple organizations anyway. Env:NOVAFABRIC_SERVER_I_ACCEPT_SHARED_CAPSULE_STORE--workers, -w INTEGER— uvicorn worker processes for horizontal scaling (default1, experimental, v0.98.0). Values>1require--backend postgres— multiple processes cannot safely share a SQLite file — and launch the app through an import-string factory (server/factory.py) so each worker reconstructs its config from the config file and environment--log-format TEXT—text(default) orjson(experimental, v0.98.0). JSON emits one object per log record including the request-correlation id. Env:NOVAFABRIC_SERVER_LOG_FORMAT; the CLI exports it so--workersprocesses inherit the format- Step-up fresh-auth for destructive actions (ADR-0246, experimental, default off). With
step_up: {enabled: true, max_age_seconds: 300}(or envNOVAFABRIC_SERVER_STEP_UP_ENABLED=1), role grant/revoke and capsule delete/bulk-delete additionally require the caller's OIDC authentication to be fresh (auth_time/iatwithin the window) — a stolen-but-valid session is no longer enough to destroy evidence; stale auth gets 401step_up_required. Credentials without a freshness signal (API keys, local token) are exempt in this slice. Role grant/revoke now also writes the hash-chained audit log (role.assign/role.revoke), not only the dashboard log - Listener TLS (ADR-0241, experimental). Set the
tls:block in the config file (enabled,cert_path,key_path) or envNOVA_TLS_ENABLED=1+NOVA_TLS_CERT_PATH+NOVA_TLS_KEY_PATHto serve HTTPS natively (works with--workers). Fail-closed at launch: missing material or a group/world-readable key refuses to start. The reverse-proxy posture remains supported; native TLS exists for deployments (HPC nodes, VMs) that have no proxy
Request correlation (v0.98.0, experimental). Every request carries an id — taken from an inbound
X-Request-ID header (sanitised: safe characters, max 128, otherwise regenerated to block log
injection) or freshly generated — echoed on the response and attached to every log record, so a
request can be traced across access logs, audit events, and SIEM egress.
Connection pooling (ADR-0221, experimental, opt-in). Set NOVAFABRIC_METADATA_DB_POOL=1 to
back the Postgres metadata store with a psycopg pool (NOVAFABRIC_METADATA_DB_POOL_MIN /
_MAX, default 1/10). Off by default; SQLite is unaffected. Pool utilisation is exported as
the nova_db_pool_in_use / nova_db_pool_size gauges, sampled at scrape time.
nova server issue-token
Issue a signed offline JWT for airgapped or SLURM deployments. Per ADR-0018.
nova server issue-token --subject user@example.com --roles writer --expires-in 90d
nova server issue-token --subject ci-runner --roles reader,writer --expires-in 30d
nova server issue-token --subject admin@cluster --roles admin \
--key-path /etc/novafabric/keys/offline-key.pemPrints the raw JWT to stdout. If the key file does not exist, a new ed25519 keypair is generated.
Options:
--subject TEXT(required) — token subject (email or identifier)--roles TEXT— comma-separated roles:reader,writer,admin,auditor(default:reader)--expires-in TEXT— token lifetime, e.g.90dor30d(default:90d)--key-path PATH— ed25519 private key PEM; defaults toNOVAFABRIC_OFFLINE_KEY_PATHor~/.novafabric/keys/offline-key.pem
nova server revoke-token <token-id>
Revoke an offline token by its jti claim. Records the revocation in the
token audit table; the token returns HTTP 401 on the next API call.
nova server revoke-token 01HX7K4P9DPBYK2WX01HXAY7MArguments:
TOKEN_ID(required) — thejtivalue from the issued JWT
Options:
--key-path PATH— path to ed25519 private key PEM (used to locate the audit store)
nova server assign-role <user> <role>
Assign a local role to a user. Writes to the role_assignments table on the
active backend. Per ADR-0018.
nova server assign-role user@example.com writer
nova server assign-role ci-runner@cluster reader --assigned-by ops-teamValid roles: reader, writer, admin, auditor.
Arguments:
USER(required) — subject (email or identifier)ROLE(required) — role to assign
Options:
--assigned-by TEXT— who is making the assignment (default:cli)--db-path PATH— SQLite database path (overrides default)
REST equivalent (per ADR-0060):
curl -X POST http://localhost:7433/v0/admin/roles \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-d '{"subject": "user@example.com", "role": "writer"}'nova server revoke-role <user> <role>
Revoke a role from a user. Per ADR-0060;
enforces the last-admin lockout invariant at the store layer — if revoking
would leave role_assignments with no admin row AND no NOVA_OIDC_ISSUER
configured, the command refuses with exit code 2.
nova server revoke-role user@example.com writer
nova server revoke-role old-bot@cluster admin --db-path /opt/nova/registry.dbArguments:
USER(required) — subject (email or identifier)ROLE(required) — role to revoke
Options:
--db-path PATH— SQLite database path (overrides default)
Exit codes:
0— success (role revoked)1— assignment not found, or other generic failure2— refused by last-admin lockout guard
REST equivalent:
curl -X DELETE http://localhost:7433/v0/admin/roles/user@example.com/writer \
-H "Authorization: Bearer $ADMIN_TOKEN"nova server scim-map-group <group> <role> (experimental, ADR-0139 D3)
Declare, --remove, or --list IdP-group → RBAC-role mappings in the server's
YAML config (scim.group_role_map, ADR-0029),
per ADR-0139 D3. SCIM group membership then
grants/revokes the mapped role through the /scim/v2/Groups routes. --list reads the
effective loaded config; setting or removing a mapping edits the YAML in place — a running
server picks up the change on restart. Scope: single server.
nova server scim-map-group Engineering writer
nova server scim-map-group SRE-Admins admin
nova server scim-map-group Engineering --remove
nova server scim-map-group --list
nova server scim-map-group --list --config /opt/nova/server.yamlArguments:
GROUP— IdP groupdisplayName(omit only with--list)ROLE— RBAC role:reader,writer,admin,auditor,promoter,approver(omit with--removeor--list)
Options:
--remove— remove the mapping forGROUPinstead of setting it--list— print the effective group→role map and exit--config PATH— server YAML config path (default:~/.config/novafabric/server.yaml)
nova server list-scim-events (experimental, ADR-0139 D5)
Read-only append-only audit trail of SCIM provisioning: who was provisioned or deprovisioned, when, and every group→role remap. Scope: single server.
nova server list-scim-events
nova server list-scim-events --subject alice@example.com
nova server list-scim-events --jsonOptions:
--subject TEXT— filter the trail to one subject (userName)--json— emit the events as a JSON array for tooling--db-path PATH— SQLite database path (overrides default)
nova server flush-jwks-cache
Force the running server to re-fetch its JWKS from the OIDC provider. Use after rotating signing keys at the identity provider.
nova server flush-jwks-cache --server http://nova.example.com:7433
nova server flush-jwks-cache --server http://localhost:7433 --token "$ADMIN_TOKEN"Options:
--server TEXT— server URL (default:http://localhost:7433)--token TEXT— Bearer token withadminrole; can also be set viaNOVA_ADMIN_TOKENor by runningnova loginfirst
nova server saml-metadata
Experimental (ADR-0138, partial slice). Emit this server's SAML 2.0
Service Provider metadata XML (entity ID, ACS URL, SP signing certificate) so
an IdP administrator can register NovaFabric as an SP. Read-only, no side
effects. Requires a saml: block with enabled: true in the server config
(ADR-0138,
spec).
nova server saml-metadata
nova server saml-metadata --config /etc/novafabric/server.yaml > sp-metadata.xmlOptions:
--config, -c PATH— server YAML config file (default:~/.config/novafabric/server.yaml)
Honest status: the server.saml config block, this metadata emitter,
attribute→role mapping, and the assertion validation policy are implemented;
live SAML login is not — the assertion consumer endpoint refuses (HTTP 501)
until the ADR-0138 D5 XML-signature library clears the ADR-0024
transitive-license gate. NovaFabric never consumes an assertion without
verified signatures.
nova server api-key create (experimental, ADR-0193)
Experimental (ADR-0193, first slice). Create a first-class API key
nvfk_<key_id>_<secret> bound to an owning principal and a role set from the
existing RBAC vocabulary (reader, writer, admin, auditor). Only the
sha256 of the secret is stored — the full key is printed once and cannot
be recovered later. Creation is appended to the hash-chained audit log
(spec).
nova server api-key create --owner alice@example.com --roles reader
nova server api-key create --owner svc:ci-bot --roles reader,writer --expires-in 90dRequests then authenticate with Authorization: Bearer nvfk_...; the server
resolves the key before any JWT parsing, so keys work in both OIDC and local
modes.
Options:
--owner TEXT(required) — owning principal (user orsvc:<name>)--roles TEXT— comma-separated roles (default:reader)--workspace TEXT— optional workspace scope (ADR-0178). Used for usage attribution; enforced at request time only when the server setsapi_keys.enforce_workspace_binding(ADR-0294, experimental)--expires-in TEXT— optional lifetime, e.g.90d(default: no expiry)--created-by TEXT— actor recorded in the audit log (default:cli)--db-path PATH— SQLite database path (overrides default)
Honest status: create/list/revoke work today; rotate (successor key
with an overlap window) and last_used_at tracking are the next ADR-0193
slice and are not implemented yet.
nova server api-key list (experimental, ADR-0193)
List API keys — metadata only (key_id, owner, roles, workspace, created, expiry, status). Secrets and hashes are never stored, so they can never be shown.
nova server api-key list
nova server api-key list --jsonOptions:
--json— emit a JSON array for tooling--db-path PATH— SQLite database path (overrides default)
nova server api-key revoke <key-id> (experimental, ADR-0193)
Revoke an API key by its public key_id — effective on the next request
(verification is a DB lookup; there is no token-style revocation-propagation
gap). The revocation is appended to the hash-chained audit log.
nova server api-key revoke a1b2c3d4Arguments:
KEY_ID(required) — the public key identifier shown bycreateandlist
Options:
--revoked-by TEXT— actor recorded in the audit log (default:cli)--db-path PATH— SQLite database path (overrides default)
Exit codes:
0— success (key revoked)1— key_id not found, or other failure
nova server api-key rotate <key-id> (experimental, ADR-0193)
Rotate an API key: mint a successor with identical bindings (owner, roles, workspace, expiry) and print it once. Both the predecessor and successor stay valid for a bounded, configurable overlap window; after it elapses the predecessor is auto-revoked at verify time (checked on the next request — there is no background job). A zero-downtime credential swap for deployed agents. Both transitions are appended to the hash-chained audit log.
nova server api-key rotate a1b2c3d4
nova server api-key rotate a1b2c3d4 --overlap-seconds 3600Arguments:
KEY_ID(required) — the public key identifier to rotate
Options:
--overlap-seconds INT— overlap window in seconds during which BOTH keys verify (default:NOVA_API_KEY_ROTATE_OVERLAP_S, or86400= 24h)--rotated-by TEXT— actor recorded in the audit log (default:cli)--db-path PATH— SQLite database path (overrides default)
Exit codes:
0— success (successor printed once)1— key_id not found, key already revoked, or other failure
The successor key is shown once and cannot be recovered — store it immediately.
last_used_at(coarse, at most one write perNOVA_API_KEY_LASTUSED_INTERVAL_S, default daily) is surfaced bylist.
The same lifecycle is also available over REST at the admin-gated
/v0/api-keys resource (POST create, GET list, DELETE {key_id} revoke,
POST {key_id}/rotate); the dashboard admin console reads it read-only via
GET /api/admin/api-keys.
nova server usage reconcile (experimental, ADR-0208)
Compare the metered usage ledger with the capsule store and report the drift
(derived minus metered). The metered side is the lifetime total — rollups
pruned past usage.rollup_retention_months are carried forward — so it
covers the same whole-history window as the store and a retention prune is
never reported as drift (GET /v0/usage's drift block still uses the
rolling retention window). Report-only by default. With --apply and a non-zero drift,
append one signed (±) adjustment row per drifting metric (capsules_created,
bytes_stored) to the default workspace only — attribution = 'reconciliation', ref = 'recon:<timestamp>'. Existing rows and counters
are never rewritten. Concurrent --apply runs are serialized by the SQLite
write lock, so a drift is booked at most once. Every run appends a
usage.reconcile entry to the hash-chained audit log.
nova server usage reconcile
nova server usage reconcile --json
nova server usage reconcile --apply --actor alice@example.comOptions:
--apply— append the adjustment rows (default: report only)--capsule-dir PATH— capsule store to measure (default: the server's)--actor TEXT— actor recorded on the ledger rows and audit entry (default:cli)--json— emit the result as JSON--config PATH— server YAML config (retention keys,db_path)--db-path PATH— SQLite database path (overrides the config)
Exit codes:
0— success (report printed; adjustment appended when--apply)1— refused (a negative adjustment would takedefaultbelow zero) or the adjustment committed but its audit entry failed2— invalid server config
nova server usage export (experimental, ADR-0208)
Chargeback export: one row per (period, org, workspace, metric) for a range
of YYYY-MM periods, deterministically sorted in that order. Finalized
periods come from the monthly rollups (status final); not-yet-finalized
periods, always including the current one, from the live counters
(provisional). CSV is RFC 4180 (CRLF, header row) with formula-injection-safe
text cells (leading = + - @ TAB CR LF, checked after NFKC normalization and
after leading whitespace, gets a ' prefix); NDJSON is one sorted-key object per line. Read-only.
nova server usage export --from 2026-07 --to 2026-09
nova server usage export --from 2026-09 --format ndjson --workspace ml-platform
nova server usage export --from 2026-09 -o chargeback-2026-09.csvOptions:
--from YYYY-MM— first period (default: current UTC period)--to YYYY-MM— last period, inclusive (default:--from; at most 120 periods)--format csv|ndjson— output format (default:csv)--workspace SLUG/--org SLUG— exact-match filters--output,-o PATH— write to a file instead of stdout--config PATH/--db-path PATH— as forreconcile
Columns: period, org, workspace, metric, total, status, finalized_at.
Exit codes:
0— success (an empty range prints the header only)2— invalid--format, malformed / inverted / over-wide period range, or invalid server config
nova login
Authenticate with a NovaFabric server via Device Authorization Grant (RFC 8628,
ADR-0018). Credentials are stored in
~/.config/novafabric/credentials.json at mode 0600.
nova login
nova login --server http://nova.example.com:7433The CLI prints a URL and user code. After the user approves in the browser,
the access token is stored and used automatically on subsequent commands.
Tokens are auto-refreshed; re-run nova login when the refresh token expires.
Options:
--server TEXT— server URL (default:http://localhost:7433)
Local CLI commands (nova capture, nova validate, nova replay, etc.) never
require credentials — authentication is only needed for server-mode operations.
nova logout
Remove stored credentials for a NovaFabric server.
nova logout --server http://nova.example.com:7433 # remove one server
nova logout # remove all serversOptions:
--server TEXT— server URL to log out of; omit to remove all stored credentials
Backup, restore, and support diagnostics (experimental)
Evidence-grade operational tooling: signed local backup sets with offline verification (ADR-0181), a verification-gated restore path, and a secret-safe diagnostics tarball for support (ADR-0187).
nova backup create (experimental, ADR-0181 / ADR-0216)
Create a backup set covering every persistent local store (ADR-0216):
registry, capsules, incidents, metadata, the PII DEK store, seal transparency
log, TSA nonces, ratchet state, dashboard state, spool, the audit log, and a
secret-redacted config. SQLite stores are snapshotted with the online-backup
API, so live writers are safe; dashboard.duckdb is snapshotted via DuckDB's
own consistent-copy mechanism (skipped honestly when a live nova serve
holds the writer lock — it is a rebuildable derived cache). The signed
manifest carries a coverage table: what was NOT captured is recorded,
never silent. The pg profile adds a pg_dump member; the manifest
profile (WORM object-store deployments) records chain heads + checkpoints
instead of blobs. Signing keys are excluded unless --include-keys.
Connection strings are treated as secrets: the DSN never appears in the set,
the manifest, or any output.
Custody note: the default set includes the PII DEK store (
dek.db) so restored PII stays readable — treat backup sets as sensitive artifacts and store them encrypted at rest. Crypto-shred replay on restore guarantees shredded subjects stay shredded regardless.
nova backup create
nova backup create -o /mnt/backups/
nova backup create -o nightly.tar.gz --include-keys
nova backup create --profile pg --dsn postgresql://… -o pg-nightly.tar.gz
nova backup create --profile manifest --backend s3 -o manifest-set.tar.gzOptions:
--output, -o PATH— target.tar.gzfile (or existing directory). Default:./nova-backup-<set_id>.tar.gz--home PATH— NovaFabric home to back up (default:NOVAFABRIC_HOMEor~/.novafabric)--profile TEXT— backup profile:local(SQLite deployment),pg(adds apg_dump --format=custommember), ormanifest(chain heads + checkpoints against a WORM object store, no blobs). Default:local--dsn TEXT— Postgres DSN for--profile pg(default:NOVA_DSNorNOVAFABRIC_POSTGRES_DSN). Never logged; the manifest stores only the redacted host/dbname--include-keys— ALSO pack the signing keyring andnovaseal.yaml+ its key/cert PEMs (ADR-0216 D4). Default off; a set created with this flag requires key-custody care--backend TEXT— object-store backend for--profile manifest:local | s3 | minio | ceph_rgw | azure_blob(env:NOVA_OCS_BACKEND)--tenant TEXT— scope the manifest listing to these tenant(s) (repeatable)--allow-pending-wal— proceed with--profile manifesteven when the local WAL has pending un-chained uploads (the gap is recorded in the listing); default: refuse--deep—--profile manifest: fully verify every chain at create time, not just pin the heads
Exit codes: 0 (set created), 1 (backup error).
nova backup verify (experimental, ADR-0181)
Verify a backup set offline against its manifest. Recomputes every member's SHA-256 and, when the set is signed, verifies the manifest's DSSE envelope. Requires no live deployment, network, or private keys.
nova backup verify nova-backup-01J....tar.gzOptions:
<set-path>(positional, required) — backup-set archive (.tar.gz) to verify offline
Exit codes: 0 (all members and signature verify), 1 (any mismatch).
nova restore <set-path> (experimental, ADR-0181 / ADR-0211)
Restore a backup set, then run the verification chain. The verified
manifest's profile drives dispatch — there is no restore-side --profile
flag. Normative order (ADR-0181/0216/0217): verify the set → prepare the home
→ extract (sensitive members restored 0600; external-origin members such as
the audit log restored to their real roots, never silently overwritten) →
migrate to head → replay crypto-shreds (shredded data stays shredded — a
moved-aside live audit log is also replayed, so shreds applied after the
backup survive) → advance regressed ratchet epochs → storage, seal-log, and
per-store integrity checks. The restore is complete ONLY when verification
passes — there is no flag to skip it.
pg-dump sets restore automatically (ADR-0217): a non-empty target DB is
refused without --force (which first takes a safety dump into the
.pre-restore-…/ directory), pg_restore runs in a single transaction
(failure leaves the DB unchanged), then alembic migrations, manifest-anchored
row counts, and RLS enforcement are verified. manifest-only sets verify
every pinned chain head against the live bucket and rebuild the metadata DB
from the chain.
nova restore nova-backup-01J….tar.gz
nova restore set.tar.gz --home /srv/novafabric --force
nova restore pg-nightly.tar.gz --dsn postgresql://…
nova restore manifest-set.tar.gz --backend s3 --sample 10Options:
<set-path>(positional, required) — backup-set archive (.tar.gz) to restore from--home PATH— target NovaFabric home (default:NOVAFABRIC_HOMEor~/.novafabric)--force— restore into a non-empty home: existing data is moved aside into a timestamped.pre-restore-…/directory (never deleted)--restore-keys— restorekey_materialmembers from a set created with--include-keys(ADR-0216 D4); default off — the opt-in is required on both sides--dsn TEXT— target Postgres DSN when restoring a pg-dump set (default:NOVA_DSNorNOVAFABRIC_POSTGRES_DSN). Never logged--backend TEXT— object-store backend when restoring a manifest-only set (env:NOVA_OCS_BACKEND)--sample INT— manifest-only sets: spot-check this many capsule payload hashes against the live bucket (default:0)
Exit codes: 0 (restore + verification chain passed), 1 (any failed step), 2 (manifest-only set: the listed bucket is unreachable).
nova support-bundle (experimental, ADR-0187)
Produce a secret-safe diagnostics tarball for support. Contains ONLY
allowlisted members: doctor.json, versions.json, env.txt
(NOVAFABRIC_*/NOVA_* variable names only — never values),
health.json, config.redacted.yaml (if a server config exists, with
secret-keyed values redacted), and a manifest.json with the SHA-256 of every
member plus the redaction ruleset version. No tokens, keys, credentials,
capsule payloads, prompts, or responses are ever included. Scope: global
(snapshots the whole installation).
nova support-bundle # default name in the current directory
nova support-bundle -o /tmp/diag.tar.gzOptions:
--output, -o PATH— output tarball path (default:./nova-support-bundle-<timestamp>.tar.gz)--log-window-hours INT— bound for the structured-log window recorded in the manifest (default:24)
Exit codes: 0 (bundle written), 1 (bundle error).
Audit-log SIEM egress (experimental, ADR-0191)
Export local audit logs in SIEM-native formats. NovaFabric produces correctly formatted, correctly redacted lines; the site's own shipper (Splunk UF, Filebeat, Vector, Fluent Bit, rsyslog) does transport — there is no network sender, no default endpoint, no background egress.
nova audit-log export (experimental, ADR-0191)
One-shot export of an audit source over a time window, one entry per line, to stdout or a file. The first output line is a manifest recording the redaction-ruleset versions in force. Every line passes the deny-by-default redaction pipeline (strict field allowlist plus the ADR-0187 support-bundle secret ruleset) — non-allowlisted fields never leave.
For --source audit (the hash-chained log) the chain is re-verified during
the walk; entry_hash/prev_hash are exported verbatim in jsonl and ride
in the OCSF unmapped object, so a SIEM analyst can check that what the SIEM
holds is what the chain produced. In ocsf format, audit event types map onto
OCSF classes (API Activity 6003, Application Lifecycle 6002, Authentication
3002) per the mapping table in design/spec/audit-siem-egress-v0.md (private); fields
OCSF has no slot for are preserved verbatim under unmapped — no silent loss.
In cef format (for legacy ArcSight-style collectors) each entry becomes one
CEF:0 event. The OCSF class selection above is reused, so the two formats
never disagree about what an event is; the CEF signature id keeps the native
event_type/action and the numeric OCSF class rides in cs5. The chain
hashes map to labelled custom strings (cs1=entryHash, cs2=prevHash) and
every remaining redacted field is packed into cs6 as compact JSON — again, no
silent loss. The manifest line is itself a CEF event, so a cef stream is pure
CEF with no JSON line to special-case. The full mapping and escaping tables live in a spec in the maintainers'
private design/ tree and are not published.
There is no server source and none is planned: the server app writes its
route events into the same log as the dashboard, so --source dashboard
already covers them (OQ-038, resolved).
nova audit-log export
nova audit-log export --source audit --format ocsf --out audit.ocsf.jsonl
nova audit-log export --source audit --format cef --out audit.cef
nova audit-log export --source dashboard --since 2026-07-01T00:00:00Z
nova audit-log export --since 2026-07-01T00:00:00Z --until 2026-07-08T00:00:00ZOptions:
--source TEXT— audit source:audit(hash-chained log) ordashboard(nova servemutation log). Default:audit--format TEXT— output format:jsonl(native, zero-mapping-loss),ocsf, orcef(ArcSight CEF:0 for legacy collectors). Default:jsonl--since TEXT— inclusive ISO-8601 lower bound (naive timestamps are treated as UTC)--until TEXT— exclusive ISO-8601 upper bound (naive timestamps are treated as UTC)--out PATH— output file (default: stdout)
Exit codes: 0 (exported OK), 2 (bad parameters), 3 (chain verification
failed — the export is still written, so pipelines can alert on the tamper
evidence itself).
nova audit-log tail (experimental, ADR-0191)
Streams audit entries to stdout as they are written, in the same three
formats and through the same redaction pipeline as export. This is a
foreground process you run — a systemd unit or a sidecar — not a
NovaFabric-managed daemon. There is no network sink and no default endpoint:
pipe stdout into your own shipper.
By default it starts at the end of the log (tail semantics); --from-start
replays the existing log first. Without --follow it makes one bounded pass
and exits, so it is safe in a script.
Log rotation (rename-based, as logrotate does by default) and in-place
truncation (copytruncate) are detected and followed; entries written
just before a rename are drained from the old file rather than lost. Chain
continuity across a rotation cannot be verified — the predecessor entry's
hash goes with the old file — so a restart is reported as a chain event
rather than passed off as an unbroken chain. Honest limit: a truncation that
is refilled past the old offset within a single poll interval is
indistinguishable from an append by size alone, and those entries are missed.
nova audit-log tail --follow | your-shipper
nova audit-log tail --follow --format cef --source dashboard
nova audit-log tail --from-start --format ocsfWith --out the sink is a size-bounded rotating file instead of stdout,
for a file shipper (Filebeat, Vector, Fluent Bit) to pick up. Rotation uses
the conventional numbered-suffix scheme (audit.cef → audit.cef.1 → …),
happens before a write that would cross the threshold so a rendered record
is never split across two files, and deletes the generation beyond
--backup-count. With --backup-count 0 the file is truncated instead of
kept, bounding disk use to --max-bytes total. Reopening appends rather than
truncating, so a restarted tailer does not destroy what it already shipped.
nova audit-log tail --follow | your-shipper
nova audit-log tail --follow --format cef --source dashboard
nova audit-log tail --from-start --format ocsf
nova audit-log tail --follow --format cef --out /var/log/nova/audit.cefOptions:
--source TEXT— audit source:auditordashboard. Default:audit--format TEXT— output format:jsonl,ocsf, orcef. Default:jsonl--follow / --no-follow,-f— keep running and render new entries as they arrive. Default: off (single bounded pass)--from-start— replay the existing log first, then follow--poll-interval FLOAT— seconds between polls when the log is idle. Default:1.0--out PATH— write to a rotating file instead of stdout--max-bytes INT— rotate--outat this size. Default:10485760(10 MiB)--backup-count INT— rotated generations to keep. Default:5--syslog ADDRESS— send RFC 5424 messages to a local syslog endpoint: a unix socket path (/dev/log) or a loopbackhost:port. No default.--syslog-transport TEXT—auto(unix for a path, else udp),unix,udp, ortcp. Default:auto
--out and --syslog are alternative sinks; passing both is an error.
Syslog sink. Messages are RFC 5424 (<134>1 <ts> <host> novafabric <pid> audit-<format> - <line>); the MSG body is byte-identical to what the other
sinks emit. TCP uses RFC 6587 octet counting so a stream receiver can find
boundaries. UDP messages are bounded and, if shortened, marked with
…[NOVAFABRIC-TRUNCATED] — a silently shortened audit record would read as
a complete one. Ship large CEF records over TCP or a unix stream socket.
The endpoint must be local. A non-loopback host is refused, not warned about: ADR-0191 D3 scopes this to a local endpoint, and getting audit data off the box remains your syslog daemon's job — it already owns the TLS, retry and buffering that NovaFabric deliberately does not implement. There are still no built-in senders to Splunk/Elastic/Sentinel (ADR-0191 D6).
Exit codes: 0 (streamed OK), 2 (bad parameters — absurd rotation config,
non-loopback syslog host, unreachable endpoint), 3 (chain verification
failed).