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 --experimental novafabric[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 start novafabric[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-tokens

With --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:

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:


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:

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 watchdog

Works 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.jsonl

Options:

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 --backup

What the migration does for each capsule whose manifest.json is below v1:

  1. Sets schema_version → "1.0.0"
  2. Renames event_log.jsonl → model-calls.jsonl (legacy v0 file name)
  3. Adds format_version: "1" to the metadata block when absent

Options:

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 --json

The 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):

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 --json

ltv 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 --json

The 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.json

Options:

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 postgres

Options:

Environment variables consumed:

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 json

The 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:

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.pem

Prints the raw JWT to stdout. If the key file does not exist, a new ed25519 keypair is generated.

Options:


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 01HX7K4P9DPBYK2WX01HXAY7M

Arguments:

Options:


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-team

Valid roles: reader, writer, admin, auditor.

Arguments:

Options:

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.db

Arguments:

Options:

Exit codes:

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.yaml

Arguments:

Options:


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 --json

Options:


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:


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.xml

Options:

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 90d

Requests 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:

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 --json

Options:


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 a1b2c3d4

Arguments:

Options:

Exit codes:


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 3600

Arguments:

Options:

Exit codes:

The successor key is shown once and cannot be recovered — store it immediately. last_used_at (coarse, at most one write per NOVA_API_KEY_LASTUSED_INTERVAL_S, default daily) is surfaced by list.

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.com

Options:

Exit codes:

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.csv

Options:

Columns: period, org, workspace, metric, total, status, finalized_at.

Exit codes:


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:7433

The 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:

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 servers

Options:


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.gz

Options:

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.gz

Options:

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 10

Options:

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.gz

Options:

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:00Z

Options:

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 ocsf

With --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.cef

Options:

--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).