NovaFabric User Guide
This guide is the working reference for the shipped nova command surface:
what each command does, how to invoke it, and when to reach for it. It picks up
where the getting started guide leaves off — that guide
walks you from install to your first capsule; this one is what you return to
once you are capturing, replaying, diffing, and auditing real runs.
What you will learn
- How to capture any command as a portable, schema-valid, secret-scanned Run Capsule — with no changes to your application code.
- How to inspect and validate a capsule, and how to gate CI on capsule integrity and on the secret scanner's findings.
- How to replay a captured run in each of the five honest modes
(
forensic,semantic,exact,mocked, and the experimentalintervention) and diff two runs structurally as a regression gate. - How to query the lineage graph — provenance, blast-radius, replay-chain, time-travel — and emit OpenLineage events to a data catalog.
- How to use the trust layer — redaction and signed Evidence Bundles an
auditor can verify offline with only
sha256sumand an ed25519 verifier. - How to capture non-Python clients through the transparent MCP and API proxies, and how to manage assets through the local Asset Registry.
- Which capabilities are shipped and work today, and which are planned design intent that you should not depend on yet.
NovaFabric is local-first: every command below runs entirely inside your own infrastructure — laptop to cluster, online or air-gapped, with no accounts and no telemetry. The five primitives it is built on — Asset Registry, Run Capsule, Replay, Lineage, and Evidence Bundle — map directly onto the workflow areas in this guide.
The guide is organized by workflow area, not alphabetically. Use the table of contents to jump to what you need.
Both
novaandnovafabricrefer to the same binary. All examples usenova.
Maturity. Nearly all commands documented here ship today but carry
experimentalmaturity: they work, but on-disk formats and interface details may change before the v1.0 schema freeze. Anything labeled planned or future design is documented intent only and is not implemented — do not build on it. See ROADMAP.md for sequencing, and What shipped experimental in v0.59 for the newest command groups.
Table of contents
- Capture
- Inspect and validate
- Replay and diff
- Lineage graph
- Trust layer
- Non-Python client capture
- Asset registry
- Local dashboard
- Environment variables
- What shipped experimental in v0.59
- What shipped in v0.75–v0.94
- Summary and next steps
Capture
Capture is the entry point to everything else. It turns a command — a script, an agent, an HPC training run, a notebook cell — into a Run Capsule: a directory identified by a time-sortable ULID that holds every observable fact of one execution. A capsule is the unit you later replay, diff, trace, and export.
nova capture
The core capture command. Wraps any command and records its execution as a replayable Run Capsule. No changes to your code are required.
nova capture python train.py --lr 0.001
nova capture python my_agent.py
nova capture -- python -c "import sys; sys.exit(1)"Capsules are written to $NOVAFABRIC_HOME/capsules/<ulid>/ by default
(~/.novafabric/capsules/<ulid>/ when NOVAFABRIC_HOME is unset;
$NOVAFABRIC_CAPSULE_DIR overrides the base). Use --output-dir to change the
base directory for one run:
nova capture --output-dir /mnt/experiment-runs python train.pyUse -- to separate nova capture flags from commands that contain their
own flags:
nova capture -- python script.py -v --config cfg.yamlNovaFabric's exit code mirrors the wrapped command's exit code. A command that
exits 1 will cause nova capture to exit 1 — but the capsule is still written
on failure, with status: failure, an error block, and the full artifacts.
Capture never silently drops a run: success and failure are both evidence.
On success:
✓ Capsule written: /home/me/.novafabric/capsules/01HXAY7M5JZ8R7K4P9DPBYK2WX (run_id=01HXAY7M5JZ8R7K4P9DPBYK2WX)What gets captured. The following transports are hooked automatically via a
sitecustomize.py injected over PYTHONPATH — no SDK decorator, no import in
your code. If the library is not installed, its hook is silently skipped, and a
capsule is still written even when no AI SDK is present at all:
| Transport | What is recorded |
|---|---|
openai SDK |
chat.completions.create calls → model-calls.jsonl; since v0.105.0 also responses.create, async and streamed calls |
anthropic SDK |
messages.create calls → model-calls.jsonl; since v0.105.0 also async and streamed calls |
httpx |
Requests to URL-registry-classified hosts → model-calls.jsonl and network_events.jsonl |
httpx2 |
Same as httpx, for the client openai 3.x uses by default; patched only when httpx2 is installed (works today) |
requests |
Same classification as httpx; covers LangChain, LlamaIndex, boto3 |
aiohttp |
Async wire-level capture; covers async LangChain, FastAPI agents |
urllib3 |
Lowest-tier wire-level capture; covers boto3/Bedrock |
mcp SDK |
ClientSession.call_tool invocations → tool-calls.jsonl |
| Plugins | Any class in the novafabric.hooks entry-point group (experimental) |
The requests and urllib3 hooks use a shared layering guard so a single
requests.post() produces exactly one record even though requests calls
urllib3 internally.
OTel GenAI semantic conventions. Every model-call record follows the
OpenTelemetry GenAI semantic conventions.
All "Required when applicable" fields are extracted: temperature, top_p,
top_k, max_tokens, stop_sequences, seed, frequency_penalty,
presence_penalty, response.id, and finish_reasons. The temperature,
top_p, and seed fields are what make a later exact replay eligibility
check possible (see nova replay).
Export the run as portable OTel spans (experimental, NF-032/033). Because those
semconv attributes are already stored, --emit-otel-genai maps the finished capsule
outward to OTel GenAI gen_ai.* spans (a root invoke_agent span, a chat client
span per model call, an execute_tool span per tool call) written to
<capsule>/otel-genai-spans.json. Message content is off by default (ADR-0021); add
--capture-content to include request messages, routed through the same secret-redaction
gate as capture and size-bounded. A model/tool call that recorded a log_level
(ADR-0127) also carries its OTel SeverityNumber projection as
novafabric.severity_number / novafabric.severity_text (debug→5, info→9, warn→13,
error→17; experimental); a call without a level gets neither attribute — no severity is
fabricated. The OTLP ingest endpoint (POST /api/otlp/v1/traces) reads the pair back
(experimental, ADR-0127 P4 inbound): an exported span re-imported into a capsule keeps
its log_level (recorded with log_level_source: adapter; an ERROR span status still
wins when more severe). A span event's OTel SeverityNumber is consumed the same way
(TRACE collapses to debug, FATAL to error); malformed values are ignored, never guessed.
Emit with:
nova capture --emit-otel-genai python my_agent.pyStandalone OTLP logs (resourceLogs) are ingested by POST /api/otlp/v1/logs
(experimental, ADR-0293; OTLP/JSON, or OTLP/protobuf with the otlp extra). They are
not written into any capsule — a sealed capsule is never amended — but appended to a
separate sidecar store, $NOVAFABRIC_OTLP_LOG_DIR (default $NOVAFABRIC_HOME/otlp-logs), one
JSONL stream per run (novafabric.run_id attribute), else per trace id, else per UTC day.
Each record notes whether its run's capsule was sealed, unsealed or absent when the
log arrived. The sidecar is correlation data, not sealed evidence. By default only
metadata is kept: times, severity (mapped to log_level exactly as above), ids,
service.name, attribute keys, and the body's type, length and SHA-256. Set
NOVAFABRIC_OTLP_LOGS_STORE_BODY=1 to also keep body text and attribute values, redacted
with the capture secret rules and truncated. Bounds: 16 MiB per request, 10 000 records per
request, 64 MiB per stream file; records over the file cap come back as OTLP
partialSuccess.rejectedLogRecords.
Example (the ingest function the route calls, run against a temporary directory; a record
carrying a novafabric.run_id resource attribute lands in that run's stream, metadata only):
runs/01RUNEXAMPLE00000000000001.jsonl
{"schema": "novafabric/otlp-log-record/v0", "run_id": "01RUNEXAMPLE00000000000001",
"service_name": "agent-svc", "severity_number": 17, "log_level": "error",
"time": "2026-09-21T14:13:20.000000Z", "attribute_keys": [],
"body": {"type": "string", "bytes": 17, "sha256": "b84afc0d..."}, ...}The body text itself is absent (only its length and SHA-256) until you set
NOVAFABRIC_OTLP_LOGS_STORE_BODY=1. Operator settings are in the
operator guide; the route is in the
API reference.
Secrets are redacted before the capsule is finalized. A secret scanner
rewrites matches in place as [REDACTED:rule-id] and writes
redaction-proof.json before the capsule closes. It scans every file in the
capsule: the call and event streams, env.lock, assets.jsonl,
lineage.jsonl, everything under inputs/ and outputs/, and the manifest. A
final pass rescans the finished capsule before it is digested and sealed. The
rule pack matches known key formats (LLM providers, vector stores, AWS, GitHub,
NovaFabric's own keys); a secret in a format it does not know is not detected —
for example PEM private keys, JWTs, passwords and connection-string credentials.
The full list is in what the scanner does not detect,
so read a clean proof as "scanned", not "secret-free". A capsule that lacks this
proof is invalid to nova validate and cannot be exported as an Evidence
Bundle — redaction is a precondition of trust, not an afterthought.
SDK decorator (in-process capture)
Use @agent when you own the entry point and want to capture without a
subprocess wrapper. The decorator installs capture hooks before calling the
function and removes them afterward — even if the function raises.
from novafabric.sdk.agent import agent
@agent(name="research-agent", version="0.1.0", capsule_dir="capsules/")
def run():
# openai, anthropic, httpx calls made here are auto-captured
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello"}],
)
return response
run()The capsule structure is the same as nova capture's, with one difference: the
decorator runs inside your process, so the capsule records capture_mode: sdk-decorator and a label (@agent:<name>@<version>) instead of a command.
nova replay --mode mocked therefore refuses it (CapsuleNotReplayable, exit 1);
forensic and semantic replay, nova diff and nova validate work as usual.
capsule_dir is a capsule store, like nova capture --output-dir: every call
writes its own capsule to capsules/<run_id>/. Without capsule_dir,
the decorator emits OTel spans only — no capsule is written. This is the
original v0.1 observability mode and is still useful if you only need traces.
Same finalization as nova capture (since v0.105.0). The capsule goes
through the same secret scan, manifest redaction, residual pass, lineage.jsonl
and evidence_digests, and it is sealed when a signing profile exists
(nova seal init), so nova verify checks it like any other capsule. Sealing
stays opt-in. If finalization fails, your function's return value or exception is
unchanged: the capsule is left unsealed and metadata.finalization_error says why.
The same holds when an earlier capture step fails (for example, a full disk while
writing env.lock): the reason reads capture incomplete: env.lock: OSError.
The framework adapters behave the same way when the capsule cannot even be opened
(an unwritable data_dir): the call runs uncaptured and the failure is logged.
Optional record-only tags (experimental, v0.59). The decorator also accepts
deployment_environment= (ADR-0126), variant= (ADR-0116, A/B attribution),
and session_id=/session_sequence= (ADR-0122, multi-turn sessions). Each is
recorded verbatim as an additive optional manifest field, never inferred; the
matching CLI flags and NOVAFABRIC_* env vars take precedence. See
Python API: SDK decorator.
See examples/minimal-agent-run/agent.py for a complete working example.
When to use which. Prefer nova capture for anything you can invoke as a
command — it needs zero code changes and works for non-Python subprocesses.
Reach for the @agent decorator only when you own the Python entry point and
want capture to live inside the process (for example, inside a long-running
service where spawning a subprocess is awkward).
Runners
nova capture delegates subprocess execution to a runner. The runner
abstraction is what lets the same capture command target a laptop, a container,
a Kubernetes cluster, or a SLURM batch queue without changing your workload.
Choose one with --runner:
nova capture --runner local python agent.py # default: runs on this machine
nova capture --runner docker \
--runner-option image=myorg/agent-runtime:latest \
python agent.py
nova capture --runner kubernetes \
--runner-option image=myorg/agent-runtime:latest \
--runner-option namespace=ml-jobs \
python agent.py
nova capture --runner slurm \
--runner-option partition=gpu \
python train.pyOne thing does change with the workload: its environment.
--runner localruns as you, on your machine, and keeps your full environment. Every other runner sends the workload somewhere else, so it receives onlyNOVAFABRIC_*variables — plusPATHforslurm, and anything you name explicitly inextra_envfordocker,kubernetes,slurmandlsf:nova capture --runner kubernetes \ --runner-option image=myorg/agent-runtime:latest \ --runner-option namespace=ml-jobs \ --runner-option extra_env=LOG_LEVEL=debug \ python agent.pyCredentials do not belong in
extra_env. A value typed on the command line is innova capture's argv, readable withpsby every user on the host for the whole run; it is then stored in the KubernetesJobobject, indocker inspect, or with the SLURM/LSF job script (whichslurmandlsfpipe to the scheduler on stdin, never on a command line). On Kubernetes use aSecretbound to the pod'sservice_account. Seeoperator-guide.md§3.3–3.4, ADR-0270 and ADR-0311.
Pass runner-specific options with --runner-option key=value (repeatable). All
runners are non-privileged by design — see the enforcement notes per runner.
What the capsule records about where the run happened (experimental, since v0.105.0,
ADR-0307). capsule.yaml names the runner under host.runner.name. For docker and
kubernetes it also records the image you passed and the digest the runtime actually
resolved it to:
host:
runner:
name: docker
image:
reference: python:3.12-slim
image_id: sha256:…
repo_digests: [docker.io/library/python@sha256:…]
resolved_by: docker-image-inspectWhen no digest can be resolved, image carries unresolved_reason instead; a digest is
never inferred from the tag. When nova capture runs inside a Slurm job, or you use
--runner slurm, host.slurm records the job id (plus array ids, partition, cluster and node
count when Slurm sets them). Node names are recorded only as a hash, the same way the hostname
is. The environment lock still describes the machine that ran nova, not the container.
Every field, and what is not recorded:
run-capsule.md.
Capturing a framework agent without nova capture
A runner captures a command. If your agent is a framework object inside a Python process, wrap it instead — each invocation writes its own Run Capsule, and the wire-level hooks record the model calls the framework makes.
from novafabric.adapters import wrap_llamaindex, wrap_pydantic_ai, wrap_haystack
engine = wrap_llamaindex(index.as_query_engine()) # LlamaIndex
agent = wrap_pydantic_ai(agent, run_name="support-bot") # Pydantic AI
pipe = wrap_haystack(pipe, run_name="rag-qa") # HaystackEach wrapper patches the entry point in place and returns the same object, so
existing references keep working (LangGraph's wrap_langgraph is the exception: it
returns a wrapper, so use the returned object). Streaming calls are captured too
(experimental): LangGraph stream / astream, LlamaIndex stream_chat / astream_chat
and streaming=True query engines, Pydantic AI run_stream / iter. The capsule stays open until the stream ends and records
status: partial (with metadata.partial_reason) when a stream is abandoned or cancelled
before it finishes — see cli-reference.md §Framework Adapters for
exactly what each case records. Install the framework itself (pip install llama-index / pydantic-ai / haystack-ai) — these are not NovaFabric extras,
and the wrap_* call raises ImportError naming the command if the framework is
absent.
Adapters ship for eleven frameworks. Each is importable by name from
novafabric.adapters:
| Framework | Entry point | Wraps |
|---|---|---|
| LangGraph | wrap_langgraph(graph) |
a compiled graph |
| AutoGen | wrap_autogen(agent) |
an agent (initiate_chat, a_initiate_chat) |
| CrewAI | wrap_crewai(crew) |
a crew (kickoff, kickoff_async) |
| DSPy | wrap_dspy(program) |
a program (forward / __call__, aforward / acall) |
| LlamaIndex | wrap_llamaindex(engine) |
a query/chat engine or agent |
| Pydantic AI | wrap_pydantic_ai(agent) |
an Agent |
| Haystack | wrap_haystack(pipeline) |
a Pipeline |
| OpenAI Agents SDK | register_openai_agents() |
registers a trace processor |
| Google ADK | make_google_adk_plugin() |
returns a plugin |
| AWS Bedrock AgentCore | wrap_bedrock_agentcore(client) |
a boto3 bedrock-agent-runtime client (returns a wrapped client) |
| A2A SDK | make_a2a_interceptor() |
returns an interceptor |
The OpenAI Agents SDK, Google ADK and A2A entry points are not wrappers — they hook the framework's own extension point
(ADR-0078) rather than patching a method. A wrapper patches the listed methods on the object you pass and returns the
same object; the patched methods take exactly the arguments the framework's do, positional ones included, and each
call (sync or async) is one capsule — a captured call that makes another, such as kickoff_async running kickoff,
records into the outer capsule.
Evidence and sealing (since v0.105.0). Every adapter finalizes its
capsule through the same path as nova capture: secret scan, manifest redaction,
residual pass, lineage.jsonl, evidence_digests, and a seal when a signing
profile exists (opt-in), so nova verify works on an adapter capsule. Configured
maskers (nova capture --masker) do not run in-process. A finalization failure
never breaks the framework call: the capsule is left unsealed and
metadata.finalization_error records why.
Where adapter capsules land, and how to refer to them. An adapter writes to the
data_dir you pass it; with none, to $NOVAFABRIC_HOME/runs/<run-id>/ when
NOVAFABRIC_HOME is set, otherwise to ./.novafabric/runs/<run-id>/ under the
working directory. That is not where nova capture writes
($NOVAFABRIC_CAPSULE_DIR, default ~/.novafabric/capsules/). A capsule path
always works (nova replay .novafabric/runs/<run-id>/ --mode forensic). Since v0.105.0
(v0.104.0 resolved a bare id in the capture store only) the bare run id
works too: nova replay, nova diff, nova validate and every other command that
takes a run id also look in those two adapter defaults, print a note: on stderr
naming the directory the capsule was found in, and fail with an error listing both
paths if the same id exists in two of them. Passing --capsule-dir to a command
that has it searches only that directory.
Replaying an adapter capsule. An adapter capsule is captured inside a framework
call, so it records capture_mode: sdk-decorator and a @framework:name label, not a
command. nova replay --mode forensic and --mode semantic work on it; --mode mocked
refuses it up front (CapsuleNotReplayable, exit 1) because there is nothing to
re-run; --mode exact reports it not eligible. Re-running a framework agent from its
capsule is future design (ADR-0306, open question 7). To get a mocked-replayable
capsule, capture the script that runs the agent with nova capture -- python agent.py. Full reference, including each
adapter's entry-point and capture-mode details, is in
cli-reference.md §Framework Adapters.
Google ADK tools in a mocked replay (experimental, since v0.106.0).
For an ADK agent captured with nova capture -- python agent.py, add
novafabric.adapters.google_adk.make_tool_plugin() as the first plugin of the
Runner. Under capture each ADK tool call is recorded (payloads at
NOVA_CAPTURE_LEVEL=forensic); under nova replay the recorded result is returned
through ADK's documented before_tool_callback short-circuit and the tool body never
runs. Unmatched or unservable calls are refused (fail closed), --permissive and
replay.yaml tool_overrides work as for MCP and record.tool, and the mutation
class that gates them is declared in make_tool_plugin({...}). It does not make
capsules from make_google_adk_plugin() replayable — those keep a pseudo-command
(ADR-0306 open question 7). Details: cli-reference.md §Google
ADK adapter and architecture/replay-modes.md.
OpenAI Agents SDK tools in a mocked replay (experimental, since v0.106.0).
For an Agents SDK workload captured with nova capture -- python agent.py, pass the
agent's tools through novafabric.adapters.openai_agents.wrap_tools([...], {"tool": "read-only"}).
Under capture each function-tool call is recorded (payloads at
NOVA_CAPTURE_LEVEL=forensic); under nova replay the recorded result is returned and the
tool body never runs. Unmatched or unservable calls are refused (fail closed), and the
mutation class that gates --permissive and replay.yaml allow: true is declared in
wrap_tools. Limits: the Agents SDK has no documented way to skip a tool, so this
replaces FunctionTool.on_invoke_tool — an undocumented seam, checked against
openai-agents 0.17.x only; tools you do not pass through wrap_tools still run live; and
capsules from register() stay non-replayable (pseudo-command). Details:
cli-reference.md §Agents SDK tool seam.
local (default). Runs the workload as a local subprocess. Equivalent to
the pre-v0.6 behavior. No additional setup required.
docker. Runs the workload inside a container. Required option: image.
The image must already have NovaFabric installed. The capsule directory is
mounted as a volume at /novafabric/capsule; artifacts land on the host
filesystem.
nova capture --runner docker \
--runner-option image=myorg/agent-runtime:abc1234 \
--runner-option network=bridge \
--runner-option user=1000:1000 \
python my_agent.pyOptional options: network, workdir, user, extra_volumes, extra_env.
The runner enforces no --privileged, no docker-socket mount, no host
namespace defaults.
kubernetes. Runs the workload as a Kubernetes Job via kubectl. Required
options: image, namespace. Optional: service_account, node_selector,
resources. Capsule artifacts are pulled back via kubectl cp after completion.
The job manifest enforces privileged: false, hostNetwork: false,
backoffLimit: 0 — no root, no host namespace, no silent retries. A failed status
poll (an API-server blip, a throttled kubectl, an expired token) is retried until the
timeout; the count and last error are recorded as runner_metadata.poll_errors and
last_poll_error. Releases up to v0.107.0 treated one failed poll as the end: they
deleted the still-running Job and reported a timeout.
slurm. Runs the workload as a SLURM batch job via sbatch. Required option:
partition. Optional: account, qos, time, nodes, gres, mem,
constraint. The capsule directory must be on a shared filesystem (NFS,
Lustre, GPFS) visible from every compute node — the runner trusts the shared FS
rather than rsyncing artifacts. This is what makes the shared-filesystem
registry usable as a laptop↔cluster handoff protocol.
export NOVAFABRIC_SLURM_SHARED_DIR=/home/vagrant # shared path
nova capture --runner slurm \
--runner-option partition=gpu \
--output-dir $NOVAFABRIC_SLURM_SHARED_DIR/runs \
python train.pyPrototype. Multi-node distributed runs recorded as a parent/child capsule tree under a single
global_run_idshipped as a prototype in v0.10+ (ADR-0039;nova run new-run-id / validate-distributed / show / lineage— see the CLI reference). It is implemented and tested but not yet validated at target scale. Each single-nodenova capturestill produces one capsule with one writer; a one-node run is the degenerate case of the distributed model.
Customizing the URL registry
The wire-level hooks (httpx, requests, aiohttp, urllib3) classify
outbound URLs against a vendored YAML registry. Default coverage includes
OpenAI, Anthropic, Cohere, Together, Mistral, Replicate, AWS Bedrock, and
Ollama on localhost:11434.
To add a private endpoint or a provider not in the default list, create
~/.novafabric/url_registry.yaml. A user-override file replaces the
vendored default entirely — copy any entries you still want from the default
before editing.
schema_version: "0.1.0"
patterns:
- match: "internal-llm.example.com"
gen_ai_system: "internal-prod"
transport: "http"
- match: "api.openai.com"
gen_ai_system: "openai"
transport: "http"
- match: "api.anthropic.com"
gen_ai_system: "anthropic"
transport: "http"To override per-invocation:
NOVAFABRIC_URL_REGISTRY=/path/to/registry.yaml nova capture python agent.pyBecause the registry is a local, editable file and never hardcodes a hosted endpoint, you can point every classified call at a private mirror or proxy — NovaFabric requires no connection to any NovaFabric-operated service.
Inspect and validate
Once you have a capsule, the first thing to do is confirm it is well-formed and free of leaked secrets. Both checks are CI-friendly and exit non-zero on failure, so you can wire them into a pipeline.
nova validate
Validates a capsule directory, an asset YAML spec, or a replay result directory
against their JSON schemas. The command routes automatically based on what
path contains.
Capsule validation:
nova validate ~/.novafabric/capsules/01HXAY7M5JZ8R7K4P9DPBYK2WX/
# ✓ Valid capsule: 01HXAY7M5JZ8R7K4P9DPBYK2WX status=successChecks capsule.yaml, env.lock, redaction-proof.json, lineage.jsonl
(if present), and the presence of all required files and directories.
Asset spec validation (see Asset registry):
nova validate my-model.yaml
# ✓ Valid model spec: my-model@1.0.0Replay result validation:
nova validate ~/.novafabric/replays/01HXB.../
# ✓ Valid replay result: 01HXB... mode=forensic status=successUse in CI to gate on capsule integrity:
nova capture python agent.py
nova validate ~/.novafabric/capsules/$(ls -t ~/.novafabric/capsules/ | head -1)nova scan-secrets
Read-only inspection of a capsule's redaction-proof.json. Reports what the
secret scanner found during capture. Does not modify the capsule — to re-run the
scanner with different strategies, use nova redact.
nova scan-secrets ~/.novafabric/capsules/01HX.../Clean output:
✓ 01HXAY7M5JZ8R7K4P9DPBYK2WX: no findingsWith findings:
! 01HXAY7M5JZ8R7K4P9DPBYK2WX: 2 findings (critical=1, high=1, medium=0, low=0)
rule_id severity target strategy
openai-api-key critical model-calls.jsonl mask
pii-email high outputs/stdout.txt maskUse --fail-on <severity> to gate CI on severity level:
nova scan-secrets <capsule> --fail-on critical # exits 2 on critical findings
nova scan-secrets <capsule> --fail-on high # exits 2 on critical or highExit codes: 0 (clean or below threshold), 1 (missing proof / bad input),
2 (threshold exceeded).
Use --json to emit the full redaction-proof.json as parseable JSON to
stdout — useful for piping into jq or a CI reporting tool.
Searching run content (experimental)
Status: experimental (ADR-0204, first slice). nova search finds runs by
what the agent actually said or did — not just by run id or command:
nova search "invoice INV-2291" # which run mentioned this invoice?
nova search "rm -rf" # where did an agent run this? (literal)
nova search "429" --status failure # failed runs that saw a rate limit
nova search --reindex # one-time backfill of older capsulesThe index lives in the local registry DB (SQLite FTS5, zero new
dependencies) and only ever contains post-redaction capsule text — the
same four files the secret scanner redacts before a capsule is complete.
New capsules are indexed automatically at ingest (nova serve /
nova ingest-capsule); run nova search --reindex once to backfill
capsules captured before this feature. A capsule missing one of those files
simply contributes fewer (or no) content rows; the minimal capture level does
not drop any of them today (it is planned, not implemented — see
nova policy capture-level). All query terms
must match (AND); a trailing * does prefix search; FTS5 operators like
OR/NEAR/- are matched literally. The same search is available in the
dashboard API via GET /api/runs/search?scope=content. Set
NOVA_CONTENT_INDEX=off to disable indexing. Postgres server-mode parity
is planned; indexing the extended event streams is planned (ADR-0204
P2 — the ADR-0209 scanner-coverage prerequisite has shipped; the corpus
stays pinned to the four v0 streams until the P2 slice bumps it). See the
CLI reference.
Replay and diff
This is the core of "replayable AI infrastructure." Tracing tells you what
happened; replay tells you whether a past run can be re-executed under controlled
conditions, and diff tells you exactly what changed between two runs. Every
replay writes a replay_result.yaml report; only an intervention replay also
writes a new (counterfactual) capsule that you can diff against its original.
nova replay
Replay a captured run from its capsule, in one of five honest, falsifiable
modes (the fifth, intervention, is experimental and described in
Concepts). NovaFabric deliberately does not claim byte-exact replay of remote
LLM calls; the mode you choose reflects how much determinism the run actually
supports.
Forensic mode (recommended as a first step):
nova replay ~/.novafabric/capsules/01HX.../ --mode forensicNo subprocess is launched, no network calls are made. NovaFabric reads the manifest, traces, and model calls directly from the capsule. Safe to run in any context — including on capsules received from colleagues or downloaded from an artifact store. This is the mode for audit and post-incident review.
Semantic mode (read-only similarity analysis):
nova replay ~/.novafabric/capsules/01HX.../ --mode semanticComputes pairwise text similarity across model call responses using
difflib.SequenceMatcher. Returns a similarity_score (0.0–1.0). No
subprocess, no network. Useful for checking whether a run's responses are
internally consistent or vary significantly across calls — the pragmatic answer
when the upstream LLM is remote and non-deterministic.
Exact mode (eligibility check):
nova replay ~/.novafabric/capsules/01HX.../ --mode exactChecks whether the capsule meets the requirements for byte-exact replay:
env.lock mode: deterministic (the legacy lock_mode key is still read), a
gen_ai.request.seed on every model call, no tool-schema drift, and a command
to re-run (a framework-adapter capsule is never eligible).
Returns exact_eligible (bool) and a list of exact_reasons if not eligible.
No subprocess, no network. Remote LLMs are almost never exact-eligible; this
mode is for local / on-prem / compliance runs where determinism is controllable.
Mocked mode:
nova replay ~/.novafabric/capsules/01HX.../ --mode mockedThe original command is re-spawned as a subprocess (Python workloads; what
v0.104.0 served versus v0.105.0 is listed under
Release scope). A capsule
that records no command — written by a framework adapter or @agent
(capture_mode: sdk-decorator), or imported from OpenTelemetry spans — is refused
before anything is spawned (CapsuleNotReplayable, exit 1, also under --dry-run).
Recorded
responses are served, in recorded order, for OpenAI chat.completions.create,
OpenAI responses.create and Anthropic messages.create calls — sync or async,
streamed or not (ADR-0304) — and recorded results for MCP
ClientSession.call_tool calls (one recorded result per call). Every other
tool runs live — HTTP requests, shell commands, file writes, framework-native
tools, functions you did not declare — so run replays of such agents in a sandbox
or against test credentials.
Declaring your own Python tools (experimental, since v0.105.0 — ADR-0306 slice 1).
Decorate a tool function with record.tool and a mocked replay returns its
recorded result instead of running it:
from novafabric.capture import record
@record.tool(mutation_class="external-side-effect", ignore=("ctx",))
def send_invoice(ctx, customer_id: str, amount_cents: int) -> dict: ...NOVA_CAPTURE_LEVEL=forensic nova capture python agent.py # keeps arguments and results
nova replay <run-id> # send_invoice's body does not runThe arguments and result are stored only at the forensic (or air_gapped)
capture level; at the default level the capsule holds digests and the replay
fails with a "re-capture with payload capture enabled" reason. Those digests are
taken after secret redaction, but a secret the rules miss (a short password, a PIN)
still feeds them, so do not decorate tools that take credentials at the default
level. Results must be
JSON-native (no tuples or objects) and at most 1 MiB. See
Python API: record.tool.
The result lists the outbound connections the replay made
(replay_contract.network_connections_live); they are reported, not blocked.
Since v0.105.0 (experimental), a recorded call that failed (a rate limit, a 4xx, a 5xx
after the SDK's retries, a timeout) is replayed by raising the same SDK exception class at the same
position, so the workload's error handling runs again.
The replay is fail-closed (ADR-0300): an extra model call, a call on an
unsupported surface (parse, legacy completions, with_raw_response), an MCP or
record.tool call with no recorded result (the live tool is not run), a
record.tool call whose record cannot be served (tool_result_not_servable), or a
recorded response that is never requested makes the replay failure with a
divergence_reason. --permissive keeps the older warn-and-continue behaviour and
only reports — except that an unmatched MCP or record.tool call runs live only if
a safety-ladder flag permits its mutation_class (the decorator's declared class;
every MCP call counts as unknown, so it needs --allow-unknown-mutation).
Changed in v0.105.0 (ADR-0306): --permissive alone used to run every unmatched
MCP call live.
The safety-ladder flags classify the capsule's recorded tool calls for the
--dry-run report, and any write-permitting rung (--allow-mutating,
--allow-external-side-effects or --allow-unknown-mutation) adds an audited
replay_mutating policy gate before the replay starts; a deny refuses the replay. Inside the replayed process they gate only the intercepted tool
surfaces (MCP, record.tool and, experimental, Google ADK tools through the tool
plugin and OpenAI Agents tools through wrap_tools) — an unmatched call under --permissive, and a replay.yaml
allow: true override — and never stop a tool replay does not intercept:
nova replay ~/.novafabric/capsules/01HX.../ --mode mocked \
--allow-readonly # rung: read-only tool calls
--allow-mutating # rung: idempotent / non-idempotent writes (+ policy gate)
--allow-external-side-effects # rung: external-side-effect tools (+ policy gate)
--allow-unknown-mutation # rung: unclassified tools (+ policy gate)Per-tool overrides (experimental, since v0.105.0, ADR-0306). A capsule's
replay.yaml may carry tool_overrides; in mocked mode they are enforced inside
the replayed process, and --dry-run prints the same decisions:
tool_overrides:
- tool_name: send_email
allow: false # never run live, even under --permissive
- tool_name: lookup_order
allow: true # re-execute -- only if you also pass the ladder flag for its classreplay.yaml travels with the capsule, so its restrictions hold and its
permissions need your consent: allow: true without the matching --allow-* flag
serves the recorded result and reports override_not_honoured. An allow: false
on a tool recorded on a surface replay cannot intercept (HTTP, shell, an undeclared
function) cannot be enforced: a strict replay refuses to start
(ToolOverrideUnenforceable, exit 3), and --permissive runs it and reports
override_unenforceable. Each override's outcome is listed under
replay_contract.tool_overrides in replay_result.yaml.
This is the mode for CI and regression testing: re-run the recorded command against its recorded responses and diff the result.
Dry-run (see what would happen without running):
nova replay ~/.novafabric/capsules/01HX.../ --dry-runResults land in $NOVAFABRIC_HOME/replays/<replay-ulid>/replay_result.yaml. Use
--output-dir to change the base directory.
Mode comparison:
| Mode | Re-executes command | LLM calls | Tool calls | Output | Primary use |
|---|---|---|---|---|---|
forensic |
No | From capsule (read-only) | Not re-executed | Inspection report | Audit / post-incident |
semantic |
No | Read-only analysis | Not re-executed | similarity_score (0–1.0) |
Drift detection |
exact |
No | Read-only analysis | Not re-executed | exact_eligible + exact_reasons[] |
Determinism / compliance check |
mocked |
Yes (refused for a capsule with no command) | OpenAI Chat Completions / Responses API and Anthropic Messages, sync or async, streamed or not: from the capsule, recorded errors raised again; unsupported surfaces refused | MCP call_tool: from the capsule or refused; other tools live |
Replay result + divergence report | CI / regression |
intervention (experimental) |
Yes, under mocked semantics | From the capsule, with one substituted event | Other tools live; a substituted tool result is not delivered | Replay result + counterfactual capsule | Counterfactual root cause |
nova diff
Structurally compare two Run Capsules. Aligned field-by-field: model calls
(a span id unique on both sides, then sequence position anchored on identical
requests), tool calls (by tool name + argument hash, then position), environment,
and every file under outputs/, nested ones included (by capsule-relative path
and content hash). This is what turns "it worked yesterday, fails
today" into a precise, mechanical answer.
nova diff ~/.novafabric/capsules/01HX.../ ~/.novafabric/capsules/01HY.../Sample output:
Diff: 01HX... → 01HY...
changed=1 added=0 removed=0
Outputs:
~ outputs/stdout.txtUse as a CI gate. --assert-no-regressions exits 1 if any changes are
detected — a changed, added or removed entry in any section — and, since v0.105.0
(ADR-0303), exits 2 when
the comparison could not be made (a capsule ref that does not resolve, a
capsule.yaml or env.lock that cannot be read or parsed, or a record line
that could not be read, which is counted in skipped_malformed_lines and warned
about on stderr). Since v0.105.0 a model call whose recorded request parameters
(temperature, seed, …) changed also counts as a change (ADR-0303 Amendment 2). Wire it into CI to catch
behavioral regressions before they reach production:
nova diff cap-a/ cap-b/ --assert-no-regressionsOutput formats:
nova diff cap-a/ cap-b/ --output-format json # machine-readable
nova diff cap-a/ cap-b/ --output-format github-annotation # for PR checksgithub-annotation emits each difference as an error line — an added or
removed call included — and a single notice when there is none: an error
appears exactly when --assert-no-regressions would exit 1.
Asset spec diff. If both arguments contain @, the command falls through
to the asset registry diff (see nova inspect):
nova diff my-model@1.0.0 my-model@1.1.0Group by recorded A/B variant (ADR-0116). --group-by variant labels the
diff as cross-arm or within-arm using each capsule's recorded
(experiment_id, variant_id) — read-only, capsule paths only, text/json output
only:
nova diff --group-by variant runs/arm-a/ runs/arm-b/Statistical regression gate (experimental, NF-007). --significance runs a
Wald SPRT test over stored boolean-outcome scores instead of a structural diff —
a distinct mode with no positional capsule refs. It reports Wilson confidence
intervals for both sides and exits non-zero on a statistically significant
regression:
nova diff --significance --baseline base/ --candidate cand/ --metric task_passA common pattern is capture → replay → diff: capture a baseline; after a code
change, replay it in mocked mode (which fails closed if the new code makes a
model or MCP call the recording does not hold), then capture the changed run and
diff that capture against the baseline under --assert-no-regressions. A
mocked replay writes a report, not a capsule, so it is not itself a diff
input; an intervention replay's counterfactual capsule is.
Lineage graph
Replay and diff answer questions about individual runs; lineage answers questions across runs — what a run consumed, what it produced, and what would be affected if an upstream artifact changed.
Every nova capture run automatically writes lineage.jsonl into the capsule.
Three edge types are inferred:
| Edge type | Source | Meaning |
|---|---|---|
consumed |
assets.jsonl entries |
The run used this asset |
produced_by |
Output files in capsule.yaml |
This artifact was produced by the run |
replayed_from |
Replay metadata | This run is a replay of another |
The edges are also indexed into a local SQLite graph at
~/.novafabric/registry.db (same file as the asset registry). The database
is a rebuildable cache — if you lose it, run nova lineage import <runs-dir>.
The lineage.jsonl files inside the capsules are the source of truth; the graph
is a derived index.
nova lineage provenance
What did this run (or asset, or artifact) depend on? Walks ancestors.
nova lineage provenance <run-id>
nova lineage provenance registry:my-dataset@1.0.0
nova lineage provenance <run-id> --depth 3
nova lineage provenance <run-id> --output jsonOptions:
--depth N— maximum traversal depth (default: 5)--kind run|asset|artifact— filter to a specific node kind--output text|json— output format (default: text)
nova lineage blast-radius
If this asset or artifact changes, which runs would need to be re-evaluated? Walks descendants — the inverse of provenance.
nova lineage blast-radius local:datasets/training-set@1.0.0
nova lineage blast-radius <run-id> --depth 2This is the primary "impact analysis" query. Run it before updating a shared dataset to see what downstream runs depend on it, or during incident response to scope what a bad artifact touched.
Options: --depth, --kind, --output (same as provenance).
nova lineage replay-chain
Show the replay ancestry of a run, back to the original captured run.
nova lineage replay-chain <replay-run-id>Useful when you have several generations of replays and want to trace which original capture they all stem from.
Options: --output text|json.
nova lineage time-travel
Show the lineage state of a node as of a specific timestamp. Useful for reconstructing what was known at a point in time, for audits or incident reviews ("what did the dependency graph look like on the day of the incident?").
nova lineage time-travel <run-id> --asof 2026-05-01T00:00:00Z
nova lineage time-travel registry:my-dataset@1.0.0 --asof 2026-04-15T12:00:00ZOptions: --kind, --output (same as provenance).
nova lineage import
(Re-)index lineage from capsule directories into the local SQLite graph. Use this to backfill runs captured before the lineage graph was set up, or to rebuild the index after moving the database.
nova lineage import ~/.novafabric/capsules/01HX.../ # single capsule
nova lineage import ~/.novafabric/capsules/ # all capsules in the directoryThe graph is fully derivable from lineage.jsonl files in the capsule
directories — losing the SQLite index is not data loss.
Scale note. The SQLite lineage graph is the local-mode default and is designed for graphs below roughly 1M edges. A KuzuDB-backed v2 tier for larger graphs shipped as experimental in v0.10+ (
nova lineage-store migrate/profile; see the lineage migration guide). Postgres (recursive CTE), Apache AGE (openCypher) and JanusGraph (Gremlin) backends also ship as experimental library classes innovafabric.lineage.backends(no CLI flag selects them yet). Cross-site lineage federation remains future design (a protocol prototype lives innovafabric.lineage.federation; nothing in the CLI or server uses it).
nova lineage emit-openlineage
Emit capsule runs as OpenLineage 2.0.2 events (START / COMPLETE / FAIL). Enables integration with data catalog tools such as Marquez, Atlan, and OpenMetadata.
# Print to stdout
nova lineage emit-openlineage ~/.novafabric/capsules/01HX.../ --output -
# Post to an HTTP endpoint
nova lineage emit-openlineage ~/.novafabric/capsules/ --output http://marquez:5000/api/v1/lineage
# Write to a file
nova lineage emit-openlineage ~/.novafabric/capsules/ --output events.jsonlIf --output is omitted, the target is resolved from OPENLINEAGE_URL →
OPENLINEAGE_FILE → stdout.
You can also trigger automatic emission at capture time by setting
OPENLINEAGE_URL before running nova capture — the orchestrator will post
events without an explicit emit-openlineage call.
Custom run facets (experimental, NF-036/037). Add --with-facets to attach
NovaFabric's custom facets to the COMPLETE event — novafabric_capsule (capsule/run id
and hash), novafabric_eval (verdict + suite + metrics), novafabric_policy (promotion
gate decision), and the standard executionParameters facet. Add --otel-correlation
(implies --with-facets) to also attach novafabric_otel_correlation (trace_id /
span_id) so a lineage node links to its OTel GenAI spans. Facets are additive and
schema-validated before emission, so a consumer that ignores them still sees unchanged
core OpenLineage events:
nova lineage emit-openlineage ~/.novafabric/capsules/01HX.../ --with-facets --otel-correlationNote: these OpenLineage run facets are a different thing from the capsule
facetscontainer (ADR-0196, experimental) — the additivefacets.<domain>evidence objects that record-only modules such assettlement,embodied, andscienceattach to a capsule. Same word, two unrelated places; see Evidence facets in Concepts.
Graph analytics (experimental — ADRs 0212–0215)
Beyond traversal, NovaFabric can turn the whole lineage graph into insight — all read-only, local-first, and deterministic:
# Structurally critical nodes: hubs + single points of failure
nova lineage metrics --top 10
# Rank the upstream root-cause suspects for a failed run
nova lineage root-cause 01HX...
# Export the graph for Gephi / yEd / Neo4j
nova lineage export-graph --format cypher -o lineage.cypher
# One synthesized report (hubs, communities, orphans, health, cost)
nova insights --output markdown -o insights.mdmetrics ranks by degree / PageRank / betweenness and flags articulation
points; root-cause combines error signals, recency, edge confidence, and
cross-run failure correlation and never fabricates a culprit when there is no
signal; export-graph emits byte-stable GraphML/GEXF/Cypher; insights
composes all of it plus seeded Louvain communities and best-effort cost, and
reports any unavailable data source as unavailable rather than inventing it.
See the CLI reference for full flags.
nova lineage consume (experimental)
Runs the JetStream LineageConsumer as a foreground daemon: it pulls
NovaFabric lineage events off a NATS subject and bulk-COPYs derived edges into
a KuzuDB graph on a size-or-time flush trigger. This is the cluster-scale
ingestion path alongside the local nova lineage import — local-mode capture
never requires it; use it only when events already flow through a NATS
deployment rather than sitting in capsule directories on a shared filesystem.
Requires pip install 'novafabric[scale,scale-kg]' (nats-py + kuzu) and a
running NATS JetStream server.
nova lineage consume --nats-url nats://nats:4222 --kuzu-path .nova/kg/lineage.kuzuOptions include --subject (default novafabric.lineage.>), --batch-size,
--fetch-timeout, --flush-batch-size, and --flush-interval-s (max seconds
between flushes even below the batch-size threshold). Not exactly-once: a
NATS message is acked once its edges are extracted, before the flush actually
persists them — see the
CLI reference
for the full flag list and delivery-semantics detail.
Trust layer
The trust layer is what makes a capsule shareable and auditable: verifiable redaction of secrets, and a signed Evidence Bundle a reviewer can verify offline with no NovaFabric runtime. This is the compliance-supporting surface of the product — it produces evidence that supports compliance workflows; it does not certify or guarantee compliance.
nova redact
Re-scan a capsule and update its redaction-proof.json. Use this if you want
to re-run the scanner with a different strategy, or if you received a capsule
with potentially unscanned secrets.
Re-scan with default strategies (mask in-place):
nova redact ~/.novafabric/capsules/01HX.../Re-scan with strategy overrides. Choose mask (default), hash
(SHA-256 first-8 hex chars), or drop (remove the field entirely):
nova redact <capsule> --strategy-override openai-api-key:hash
nova redact <capsule> --strategy-override pii-email:dropMark a finding as an acknowledged false positive:
nova redact <capsule> --mark-unsafe-skip <finding-id> --rationale "test fixture — not a real key"False positives recorded in unsafe_skips are preserved across future
re-scans unless you clear them:
nova redact <capsule> --clear-unsafe-skipsCapsules with unsafe_skips entries block nova export-evidence unless you
pass --allow-unsafe-skips — so an acknowledged false positive is always a
deliberate, recorded decision, never a silent one.
nova export-evidence
Build a signed Evidence Bundle ZIP from a capsule. The bundle is a
self-contained, verifiable archive: a copy of the capsule, its lineage edges, the JSON schemas, Ed25519-signed in-toto DSSE attestations, and a manifest.json with a SHA-256 for every file plus the verification recipe
(full list).
A reviewer can verify the bundle with nothing but sha256sum and an
ed25519 verifier — no NovaFabric runtime required. This is the primitive
that makes a capsule portable audit evidence you own.
Generate a signing key (one time): nova init writes an Ed25519 pair to
$NOVAFABRIC_HOME/keys/signing_key.pem (private, mode 600) and
signing_key.pub.pem (public). Re-running nova init keeps an existing pair.
nova initBuild the bundle:
nova export-evidence <capsule> \
--key ~/.novafabric/keys/signing_key.pem \
--output evidence.zipOptions:
--key PATH— PEM-encoded ed25519 private key. Not defaulted: without it the command exits 1 and prints the pathnova initwould use.--output PATH/-o(required) — output ZIP path--allow-unsafe-skips— export even ifredaction-proof.jsoncontains acknowledged false positives--timestamp/--timestamp-url URL/--timestamp-optional— opt-in RFC 3161 timestamp of the bundle (off by default; seenova export-evidence --help)--sigstore— publish the run attestation's DSSE envelope to a Rekor transparency log (see the note below)
Bundle layout:
evidence.zip
├── manifest.json # index + manifest_hash
├── run-capsule/ # byte-identical capsule copy
├── lineage-subgraph/edges.jsonl # lineage for this run
├── attestations/ # in-toto Statement v1, DSSE-enveloped
│ ├── run.intoto.json
│ ├── redaction.intoto.json
│ ├── lineage.intoto.json
│ └── bundle.intoto.json
├── signatures/ # raw signature + public key per envelope
│ ├── *.sig
│ └── *.cert
├── schemas/ # all JSON schemas vendored
└── README.md # human-readable verification recipeNote.
--sigstoreis Rekor publication, not keyless signing: whenNOVA_REKOR_URLis set it uploads the run attestation's DSSE envelope to that Rekor log and prints the entry id; when it is unset it prints a warning and the export still exits 0. The bundle is always signed by--key. Sigstore keyless signing is planned.
Cryptographic sealing (experimental)
Status: experimental — shipped in v0.10+, interfaces may change before the v1.0 schema freeze.
The NovaSeal in-process signing core is implemented and tested: DSSE signing (ECDSA P-256), an opt-in RFC 3161 trusted timestamp (since v0.103, ADR-0292: with no
tsa_urlinnovaseal.yamlno timestamp is requested and no network call is made — earlier releases silently defaulted to FreeTSA), and an append-only SQLite Merkle log, verified bynova verify <capsule>— which reportssignature_ok,timestamp_ok, andlog_integrity_ok. Verification is self-contained: new seals carry their Merkle inclusion proof, so an auditor needs only the capsule — nonovaseal.yamland no copy of your log; a check that cannot run printsNOT CHECKED/NOT PRESENTand does not fail the command. Sealing is opt-in via$NOVAFABRIC_HOME/novaseal.yaml(~/.novafabric/novaseal.yamlby default); maker-checker seal flows arenova seal propose / approve / verify(ADR-0059). See the NovaSeal configuration guide and the CLI reference.
Still planned (ADR-0041 design intent, not shipped): the dedicated, network-hardened NovaSeal signing service, qualified timestamps, Sigstore-keyless signing as the default path, and WORM-backed retention at the seal layer. For the most stable portable proof today, use the Evidence Bundle above — ed25519-signed bundles with in-toto DSSE attestations and verifiable redaction proofs, verifiable offline.
Non-Python client capture
For agents that do not run in Python — Claude Code, Cursor, Continue.dev, Node.js, Go, Rust — NovaFabric provides two transparent proxy commands that capture LLM API calls without modifying the client. Both write into the same capsule schemas as the in-process hooks.
nova mcp-proxy (experimental)
A transparent proxy that sits between an MCP client and an upstream MCP server,
recording every tools/call request/response pair into a capsule.
Two transport modes are supported.
Stdio mode (default): wraps the upstream MCP server process.
NOVAFABRIC_CAPSULE_DIR=/path/to/.novafabric/runs/01HX...
nova mcp-proxy -- npx -y @modelcontextprotocol/server-filesystem /tmpOr with an explicit flag:
nova mcp-proxy \
--capsule-dir .novafabric/runs/01HX.../ \
-- /usr/local/bin/my-mcp-server --flagIf --capsule-dir is omitted and NOVAFABRIC_CAPSULE_DIR is not set, a fresh
capsule directory is allocated automatically under .novafabric/runs/.
Claude Desktop integration. Replace the upstream server entry in
claude_desktop_config.json:
{
"mcpServers": {
"filesystem": {
"command": "nova",
"args": [
"mcp-proxy",
"--capsule-dir", "/Users/me/.novafabric/runs/01HX...",
"--",
"npx", "-y", "@modelcontextprotocol/server-filesystem", "/tmp"
]
}
}
}HTTP mode (v0.6.3): listens on a TCP port and forwards POST/SSE to an upstream HTTP MCP server.
nova mcp-proxy \
--listen 127.0.0.1:8765 \
--upstream-url http://upstream-mcp.internal:9000/mcpStdio and HTTP modes are mutually exclusive per invocation. One proxy invocation handles one upstream server.
Records use the same tool-calls.jsonl schema as the SDK hook, plus an
extension field extensions["io.novafabric.capture_method"] = "proxy".
Status: experimental. The API may change.
nova api-proxy (experimental)
A transparent HTTP proxy for non-Python LLM clients. The client points its
*_BASE_URL environment variable at the proxy; the proxy forwards to the
upstream LLM API and records every call into model-calls.jsonl.
# Terminal 1: start the proxy
nova api-proxy \
--upstream-url https://api.openai.com \
--listen 127.0.0.1:8765
# Terminal 2: point your LLM client at the proxy
export OPENAI_BASE_URL=http://127.0.0.1:8765
claude ... # Claude Code
# or: cursor, continue, any Node/Go/Rust LLM clientFor Anthropic:
nova api-proxy \
--upstream-url https://api.anthropic.com \
--listen 127.0.0.1:8765
export ANTHROPIC_BASE_URL=http://127.0.0.1:8765Options:
| Flag | Default | Description |
|---|---|---|
--upstream-url |
(required) | The LLM API base URL to forward to |
--listen |
127.0.0.1:8765 |
Proxy listener address as host:port |
--capsule-dir |
auto-allocated | Where to write model-calls.jsonl; falls back to NOVAFABRIC_CAPSULE_DIR |
--upstream-timeout |
60.0 |
Timeout per upstream read, in seconds (httpx applies it to each connect/read, not to the whole call) |
The proxy buffers every response. It relays the upstream status, body and
headers (including retry-after, x-should-retry, x-ratelimit-* and
request-id, so client SDK retries see them) only after the whole upstream
response has arrived. A stream: true client therefore receives nothing until
the model finishes — time to first token equals total latency (planned:
relaying chunks as they arrive). For the record, an SSE stream is merged into
a synthesized non-streaming envelope.
The proxy is base-URL override only. No TLS MITM, no local CA.
Status: experimental. ADR-0026.
Asset registry
The Asset Registry is NovaFabric's identity layer: a local SQLite database at
~/.novafabric/registry.db (override: NOVAFABRIC_DB_PATH) that tracks AI
assets — models, agents, prompts, tools, datasets, evaluation suites, and
deployment endpoints — with lifecycle statuses and promotion history. Each asset
is addressed as name@version and pinned to a git SHA at registration.
Promotion is governance metadata only. Promoting an asset updates one DB record. It does not restart, deploy, or redeploy anything — it records a lifecycle decision, and (for agents) gates on a passing eval.
nova register
Register an asset from a YAML spec file.
nova register my-model.yaml
nova register research-agent.yamlModel spec example:
novafabric_spec_version: "1"
asset_type: model
name: fraud-detection-model
version: 1.2.0
status: development
description: "XGBoost fraud detection model trained on transactions dataset"
spec:
framework: xgboost
artifact_path: s3://ml-artifacts/fraud-detection/v1.2.0Agent spec example:
novafabric_spec_version: "1"
asset_type: agent
name: support-triage-agent
version: v1.0.0
status: development
description: "LLM agent that triages incoming support tickets"
spec:
model:
provider: anthropic
name: claude-haiku-4-5-20251001
temperature: 0.1
tools:
- search_knowledge_base
- create_ticket
prompts:
system: "You are a support triage assistant. Classify incoming tickets by priority."
policies: []
evals:
- triage_accuracy_suiteSpec files are validated strictly: a key NovaFabric does not define — at the top level or
inside spec: — is a validation error that names the key, so a typo such as suite_name:
for suite: is caught instead of silently dropped. Put vendor or custom data under the
optional top-level extensions: mapping (reverse-DNS keys, kept verbatim), or in tags: /
external_refs: (string values). (Before this change, unknown keys were accepted and
discarded.)
extensions:
com.example.owner: fraud-team
com.example.cost-centre: {id: 4711}Supported asset types and their spec fields (src/novafabric/spec/models.py):
asset_type |
Required spec fields |
Optional spec fields |
|---|---|---|
model |
framework, artifact_path |
— |
agent |
model (provider, name), tools, prompts, policies (may be []), evals (at least one) |
memory, model.temperature |
prompt |
— (spec: itself is optional) |
template |
tool |
— (spec: itself is optional) |
entrypoint |
dataset |
— (spec: itself is optional) |
path |
evaluation |
— (spec: itself is optional) |
suite |
deployment |
— (spec: itself is optional) |
endpoint |
Exits 0 on success, 1 on validation error or duplicate. Use nova validate <spec.yaml> to validate without registering.
nova suggest-register
Analyze captured Run Capsules and suggest assets to register. This inverts the onboarding workflow: capture first, then let NovaFabric propose what to register from observed evidence (models seen, tools called, agent command).
# Interactive: scan 10 most recent capsules, prompt per suggestion
nova suggest-register
# Write draft YAML files without registering
nova suggest-register --draft-only --output-dir ./drafts/
# Auto-register high-confidence suggestions (models + tools, skip agents)
nova suggest-register --auto --min-confidence 0.8 --skip-types agent
# Analyze a specific run by ID
nova suggest-register 01HXAY7M5JZ8R7K4P9DPBYK2WXConfidence levels: models = 100% (every observed call), tools = 80% (min 2 calls),
agents = 70%. Post-capture, a one-line hint is printed when unregistered assets are
detected. Disable with NOVAFABRIC_SUGGEST=0.
nova list
List registered assets. Filter by type or lifecycle status. Use --stale to surface
assets with no promotion, consumption, or eval activity in the last N days.
nova list
nova list --type agent
nova list --type model --status staging
nova list --status production
# Stale asset detection
nova list --stale # assets inactive > 30 days
nova list --stale --stale-days 60 # custom threshold
nova list --stale --status production # stale production assets only--stale output adds last_activity_at and days_stale columns. Activity includes
any promotion, capsule consumption (assets.jsonl), or eval run.
nova inspect
Show full metadata for an asset. If version is omitted, shows the latest.
nova inspect fraud-detection-model@1.2.0
nova inspect support-triage-agent # latest versionOutput includes the full spec YAML, lifecycle status, git commit at registration, and eval results for agent assets.
nova promote
nova promote is a sub-group with four commands (direct, keygen, propose, approve). Valid lifecycle transitions:
development → staging → production → archivedDirect transitions (e.g. development → production) are permitted. Once
archived, no further promotion is possible.
nova promote direct
Single-actor promotion (original behaviour):
nova promote direct fraud-detection-model@1.2.0 --to staging
nova promote direct fraud-detection-model@1.2.0 --to archivedProtected stages (ADR-0312). production is protected. nova promote direct --to production is refused unless an OPA policy explicitly allows the promote_direct
action, and the shipped default policy denies it. With no OPA installed there is no
explicit allow, so it is refused. --force does not bypass this. Use propose +
approve below, or see the CLI reference for loading an allow policy. The dashboard's
Promote button and the HTTP promote endpoints go through the same gate and answer 403.
Eval gate. Agent assets require a passing nova eval result before
promotion to staging or production. Use --force to override
(requires interactive confirmation; recorded as forced in the audit trail):
nova promote direct support-triage-agent@v1.0.0 --to staging --forceSLSA provenance on promotion (experimental). Add --slsa-provenance to emit a
DSSE-signed slsa.dev/provenance/v1 attestation for the promotion, and
--slsa-ml-profile to emit the SLSA-for-ML profile instead — its buildDefinition
captures dataset versions/hashes and seeds, and its byproducts bind the promoted model to
the digest of its recorded eval results (omitted when none is recorded) (NF-057). Both verify with nova verify-envelope or stock
cosign:
nova promote direct my-model@1.0.0 --to staging \
--slsa-provenance --slsa-ml-profile --slsa-out my-model.slsa.jsonnova promote propose + nova promote approve
Maker-checker two-step flow for regulated deployments (ADR-0058, verified since ADR-0312). Two different people, each with their own Ed25519 key, and each key bound to that person by an operator-held signer trust list.
One-time setup.
# Each signer creates a key; this prints the trust-list entry for it.
nova promote keygen --identity alice
nova promote keygen --identity bobAn operator copies the printed entries into the trust list. It is read from
NOVAFABRIC_PROMOTE_SIGNERS if set, otherwise $NOVAFABRIC_HOME/promote-signers.yaml.
Each entry should keep only the roles that person holds:
signers:
- identity: alice
public_key: <from keygen>
roles: [proposer]
- identity: bob
public_key: <from keygen>
roles: [approver]Each promotion.
# Step 1 — maker proposes (signature verified before the proposal is stored)
nova promote propose fraud-detection-model@1.2.0 --to production --identity alice
# Step 2 — checker approves (both signatures verified, then the asset is promoted)
nova promote approve fraud-detection-model@1.2.0 --identity bobWhat is enforced today:
--identityis required on both commands. It must name a key that already exists. Neither command ever creates a key.- Each signature is verified with the public key the trust list binds to the claimed
identity. An unlisted key, a key bound to another name, a missing role, or a
signature that does not verify is refused: exit
3on the proposal side,4on the approval side. - The approval is bound to one proposal: its id, target stage and the maker's signature. Editing the stored proposal makes approval fail.
- Separation of duties compares the bound identities and keys, so one person cannot
approve their own proposal under a second name. Self-approval exits
6. - Proposals stored before ADR-0312 still list, as
unverified. They cannot be approved. A newproposesupersedes them.
Every single-actor route into production is gated the same way. nova promote direct,
nova prompt promote, the dashboard and nova serve promote endpoints, the nova server
route PUT /v0/assets/{id}/promote (403 promote_direct_denied), and a nova rollback to a
version that was never in production all need an explicit promote_direct policy allow;
--force does not bypass it. PUT /v0/assets/{id}/promote records the authenticated principal
(server:<subject>) as promoted_by, as registration does; a request-body actor is ignored.
Limit. The trust list is the trust anchor. Anyone who can edit it — or who can set
NOVAFABRIC_PROMOTE_SIGNERS / NOVAFABRIC_HOME for the process doing the approving — can bind
a second key to a second name. Keep it owned by an operator and read-only to signers, and do
not let signers choose the environment of the process that verifies them (ADR-0312 Limits).
nova rollback
Roll back an asset to its most recent previous production version in one command. Atomically archives the current production version and restores the previous one. Designed for incident response — no manual registry queries needed.
nova rollback my-agent --actor on-call-eng
nova rollback my-agent --to v1.8.0 --actor on-call-eng # explicit targetSteps performed in a single DB transaction:
- Find the current
productionversion (error if none). - Find the most recent prior
productionversion (auto, or use--to). - Archive the current production version.
- Promote the prior version back to
production. - Write an audit log entry with a
rollback_reasonand agatefield.
"Prior production version" comes from the registry's append-only status history. A version
that was in production before is restored at once, even if it is archived — that is what
incident response needs. A --to target that was never in production is refused unless
the promote_direct policy action explicitly allows it (ADR-0312), the same gate as
nova promote direct; otherwise promote --to staging followed by a rollback would be a
single-person path into production. --actor is recorded in the audit trail.
nova capture — asset status gate
Before spawning the subprocess, nova capture can verify that a named asset is in
the required lifecycle status. Nothing is written to disk if the check fails.
# Hard block — exits non-zero if my-agent@v1 is not in staging or production
nova capture --asset my-agent@v1 --require-asset-status staging,production -- python agent.py
# Soft warn — logs a warning but does not block
nova capture --asset my-agent@v1 --warn-if-asset-status development -- python agent.py
# Block if the asset is not registered at all (default: warn only)
nova capture --asset my-agent@v1 --require-asset-status production --require-registered -- python agent.pyThis is an opt-in gate. Without --asset, capture behaviour is unchanged.
nova eval
Run evaluation suites declared in an agent's spec and store the results in
the registry. Suites are resolved via the novafabric.evals Python entry-point
group.
nova eval agent support-triage-agent@v1.0.0After a passing eval, nova promote direct <name@version> --to staging will succeed without
--force. production is a protected stage: reach it with nova promote propose +
nova promote approve, unless a policy explicitly allows promote_direct (ADR-0312).
The bundled standard eval suites (GAIA, SWE-bench, AgentBench, MMLU, Smoke) run in OCI-pinned containers, and promotion can be Rego-gated to block on a regression against a recorded baseline.
Guard against benchmark contamination (experimental, NF-028). Contamination silently inflates scores, so NovaFabric records the dataset + split content hashes an eval ran against. Check a capsule against a configurable registry of known-bad hashes:
nova eval contamination-check ./my-capsule --registry known-bad.json --jsonIt reports a status per dataset (current / superseded / contaminated / unknown)
and exits 4 when any dataset is contaminated or superseded, so CI can gate on it.
It fails closed: a facet it cannot read or validate is reported as invalid, with a
reason, and also exits 4.
Detection only — no remediation. See the feature tour
for the dataset provenance and SLSA-for-ML surfaces that pair with it.
nova report
Generate an asset inventory report. Defaults to Markdown on stdout.
nova report # Markdown to stdout
nova report --format json # JSON to stdout
nova report --output report.md # write to file
nova report --format html --output inventory.html # self-contained HTML + by-type chart (ADR-0201)
nova report --format pdf --output inventory.pdf # requires the optional WeasyPrint extra--format pdf requires --output and the optional pip install 'novafabric[compliance]'
extra (WeasyPrint); without it the command exits 1 with an install hint.
Sample Markdown output:
# NovaFabric Asset Report
## Summary
| Type | Count |
|---|---|
| model | 3 |
| agent | 2 |
...Local dashboard
nova serve (experimental)
An opt-in local HTTP dashboard for browsing capsules, registry assets, and the
lineage graph. The CLI remains the canonical interface; the dashboard is a
read-oriented satellite. Every dashboard mutation displays the equivalent nova
command and is logged to ~/.novafabric/dashboard-audit.jsonl, so nothing the
dashboard does is invisible to the CLI-first audit trail.
Install the optional extra (one time):
pip install 'novafabric[serve]'This adds FastAPI and uvicorn (both Apache-2.0 / MIT licensed).
Start the server:
nova serve --experimentalThe --experimental flag is mandatory and acknowledges that the dashboard
API may change.
The terminal prints a URL with an embedded session token:
Dashboard: http://127.0.0.1:4321/?token=<token>
API docs: http://127.0.0.1:4321/api/docs?token=<token>Click the URL to open the dashboard in your browser. The token is also
written to $NOVAFABRIC_HOME/.serve-token (mode 0600) — the start-up panel
prints the resolved path, which is ~/.novafabric/.serve-token only when
NOVAFABRIC_HOME is unset.
The token is not rotated on every restart. nova serve reuses an existing
.serve-token file so that a restart does not break open browser sessions, so
a URL you were given earlier keeps working after a restart — and the token
stays readable on disk (mode 0600) after the server exits. Delete the file if
you want the next start to mint a fresh token.
Flags:
| Flag | Default | Description |
|---|---|---|
--experimental |
(required) | Acknowledges the experimental gate |
--port |
4321 |
TCP port |
--host |
127.0.0.1 |
Bind address; localhost only without --insecure |
--capsule-dir |
$NOVAFABRIC_CAPSULE_DIR, else $NOVAFABRIC_HOME/capsules |
Where to look for capsules (where nova capture writes) |
--db-path |
$NOVAFABRIC_DB_PATH, else $NOVAFABRIC_HOME/registry.db |
Registry/lineage SQLite path |
--no-browser |
off | Do not auto-open a browser tab |
Capsule index. nova serve keeps a runs_cache SQLite index so the Runs
tab stays fast regardless of how many capsule directories are on disk. The index
is built on startup and refreshed every 2 s by CapsuleWatcher. If capsules
were added while the server was stopped, force a full re-index:
nova ingest-capsule --all # re-index everything
nova ingest-capsule <run_id> # index a single capsule
nova ingest-capsule --watch # foreground watcher loopTwo watcher backends: WatchdogBackend (pip install novafabric[watch]; uses
inotify/FSEvents) and PollingBackend (zero extra deps). The default, auto, uses
watchdog when it is installed and falls back to polling otherwise. Force one with
NOVA_WATCHER_BACKEND=polling|watchdog and set the poll period with
NOVA_WATCHER_INTERVAL=<seconds> (default 2); an unknown value is refused at start-up.
Navigating. The 30 tabs are grouped in the sidebar under seven headings:
Overview · Runs & Debug · Govern & Promote · Provenance & Trust · Compliance ·
Platform · Reports & Export. Since v0.98.3 the groups start collapsed, each
showing its tab count — click a heading to expand it. The group holding the
tab you are on is always expanded, so your current location is never hidden,
and whatever you expand or collapse is remembered in that browser. Every tab is
deep-linkable as ?tab=<id>, and the
Compliance tab is itself a hub whose five panel groups (Frameworks · Audits ·
Privacy · Exports · Assurance) are deep-linkable as ?sub=<group>. Navigation
shortcuts are mnemonic two-key sequences — press g then the tab's letter
(g h Home, g r Runs, g c Compliance); press ? for the full map, and
⌘K / Ctrl-K for the command palette. (v0.97.0 replaced the earlier
positional 1–9 keys with these sequences; tab ids and ?tab= links are
unchanged.)
What the dashboard covers:
- Runs tab — list, search, and filter capsules; status filter pill-bar; hover copy-run-ID; inspect file tree; validate schema; view secret scan results; replay (forensic / dry-run); redact; export evidence. Multi-select up to 5 for N-run diff.
- Registry tab — list and inspect registered assets; register; run evals; promote (
directsub-command); bulk-promote checkbox; compare two spec versions (diff table); eval trend sparkline. - Evidence tab — list signed bundles; in-browser ed25519 verify; full server-side cryptographic verify of the bundle.
- Holds tab — place and release legal holds on registries; view all active holds with duration and reason; sidebar count badge.
- Lineage tab — interactive DAG rendered with React Flow; provenance, blast-radius, and replay-chain highlight modes; ancestry breadcrumb; click or double-click a node to select it.
- Diff tab — structural diff; 2-run or N-run (up to 5) comparison; stacked collapsible cards in N-run mode; URL-persistent
?run_ids=a,b,cfor sharing. - Audit tab — unified mutation audit log (every dashboard write action with equivalent
novacommand); action-type filter. - Policy tab — interactive OPA/Rego policy tester; ALLOW/DENY badge; explain toggle for full OPA trace output. Checking the packaged policies needs a
readtoken; submitting your own Rego needsadmin, andopaalways runs sandboxed (no server environment, noopa.runtime/http.send/net.lookup_ip_addr). - Commands tab — live command builders across journey tracks with copy buttons.
- Home tab — staleness indicator (amber border on resume cards > 24 h).
- Capture tab — recent capsules panel with "Open folder" links for local paths.
- Runs filter bar and saved views (experimental) —
status:error -model:gpt-4ofilters arenova query --wherepredicates, with a node / root / tree scope; "save current as" writes anova viewfile thatnova view runcan re-run. Honest aggregates say "no value" and refuse rather than show$0.00for an unpriced model. See dashboard.md. - Evidence cart (experimental,
adminscope) — collect runs, then export them as one signed Evidence Bundle. See dashboard.md. - Dashboards tab (experimental,
g 4) — render, validate and save ADR-0235 widget and dashboard JSON files (the same filesnova dashboardmanages). See dashboard.md.
The dashboard also surfaces a component-status view covering the cluster-scale subsystems (NovaSeal, collector, object store, metadata DB, parent/child capsules). These shipped in v0.10+ as experimental / prototype tiers — implemented and tested, but not validated at target scale. Check each component's maturity label in ROADMAP.md rather than treating presence in the status view as production readiness.
What requires the CLI. Some operations are intentionally CLI-only:
nova report, lineage time-travel, OpenLineage emission, nova mcp-proxy,
nova api-proxy, cluster runners, mocked replay, semantic/exact replay,
nova promote propose/approve (maker-checker). See
docs/dashboard.md for the full capability matrix.
Security model:
- Localhost only by default (binds
127.0.0.1) - Session token required on every
/api/*request — either as?token=<token>or as anAuthorization: Bearer <token>header (when the header is present it is authoritative) - DNS-rebinding defence (
Hostheader validated) - CORS restricted to
localhostand127.0.0.1origins - Every write action is confirm-gated and audit-logged
Stop the server with Ctrl+C. The session token is invalidated on stop.
Status: experimental. The dashboard API (endpoint shapes, field names) may change between minor versions.
Environment variables
| Variable | Default | Purpose |
|---|---|---|
NOVAFABRIC_HOME |
~/.novafabric |
Root directory for all internal NovaFabric data. Set this once to redirect all files (registry.db, .serve-token, dashboard audit log) to a shared location. |
NOVAFABRIC_DB_PATH |
$NOVAFABRIC_HOME/registry.db |
SQLite path for asset registry and lineage graph. Overrides NOVAFABRIC_HOME for this path only. |
NOVAFABRIC_METADATA_DB_PATH |
$NOVAFABRIC_HOME/metadata.db |
SQLite path for the MetadataStore (runtime and nova db upgrade). NOVAFABRIC_DB_PATH does not select it. |
NOVAFABRIC_URL_REGISTRY |
(bundled) | Override path for the wire-level URL classification registry |
NOVAFABRIC_CAPSULE_DIR |
— | Capsule storage directory. Set by nova capture in the subprocess; used by hook loader and proxy commands. Set explicitly to redirect captures to a shared path (e.g. a shared NFS mount). |
NOVAFABRIC_SPAN_ID |
— | Root OTel span id injected into the subprocess by the orchestrator |
NOVAFABRIC_SLURM_SHARED_DIR |
— | Shared filesystem path for SLURM live tests (ops / CI use) |
NOVAFABRIC_SUGGEST |
— | Set to 0 to disable the post-capture suggest-register hint |
OPENLINEAGE_URL |
— | HTTP endpoint for automatic OpenLineage emission at capture time |
OPENLINEAGE_FILE |
— | File path for OpenLineage emission (fallback if OPENLINEAGE_URL not set) |
NOVAFABRIC_DASHBOARD_AUDIT_FILE |
$NOVAFABRIC_HOME/dashboard-audit.jsonl |
Audit log destination for dashboard mutations. Overrides NOVAFABRIC_HOME for this path only. |
NOVA_WATCHER_BACKEND |
auto |
Capsule-index watcher backend (auto, polling or watchdog); an unknown value is refused. auto uses watchdog when installed, else polling. nova ingest-capsule --watch --backend overrides it; nova serve has no such flag. |
NOVA_WATCHER_INTERVAL |
2.0 |
Poll interval in seconds for the capsule-index watcher; an explicit --interval flag wins. |
NOVA_CONTENT_INDEX |
on | Set to off to disable content indexing at ingest (experimental, ADR-0204) |
NOVA_CONTENT_INDEX_MAX_DOC_KB |
64 |
Per-doc text cap for the content index (operator tuning) |
NOVA_CONTENT_INDEX_MAX_DOCS |
10000 |
Per-capsule doc cap for the content index (operator tuning) |
Cost analytics (experimental)
Beyond the offline nova cost estimate and the ClickHouse-backed nova cost report (both covered in the CLI reference),
four additional nova cost subcommands report descriptive cost evidence over
records you already hold. Each is experimental, read-only, and never a verdict —
none applies a threshold, quota, or pass/fail; whether a figure is acceptable is
the operator's call. Each also takes --json.
nova cost attribute <runs.json>(ADR-0146, NF-148) — splits recorded spend into productive vs wasted outcomes with a per-status breakdown, from a{runs: [{run_id, status, cost}], productive_statuses?}document.nova cost fairness <totals.json>(ADR-0146, NF-150) — per-agent fairness ledger (each agent's share, Gini coefficient, max/mean ratio) over per-dimension resource totals (cost/energy/calls) in a{totals: {dimension: {agent: total}}}document.nova cost usage-breakdown <manifest.json>(ADR-0132) — token usage-type composition of a capsule (each usage type's share, cached-read ratio, reasoning / multimodal flags). Composition only — no cost, no verdict; honours "absent != zero".nova cost rollup <delegation.json> <capsule|attribution.json>(ADR-0146, NF-142) — rolls the per-agentcost_attributionfacet (NF-141) up the acted-as delegation chain (ADR-0106): each hop's own cost and subtree cost, plus a conservation block (root subtree vs run total, exactDecimalequality) that names unchained and unattributed cost. Cycles and broken chains are reported as findings (basis: partial), never a crash. Report-only — nothing is written to the capsule, and grant signatures are not re-verified.
See the CLI reference for flags, input shapes, and exit codes.
What shipped experimental in v0.59
v0.59.0 added a large cohort of experimental command groups — first slices of the Langfuse-parity ADRs (0112–0141) plus interop and forensics surfaces. All are additive, off unless you opt in, and their interfaces may change. This guide does not duplicate them; each is fully documented in the CLI reference, and the grouped overview lives in the v0.59.0 release notes:
| Area | Commands / surfaces (all experimental) |
|---|---|
| Prompt lifecycle | nova prompt register/get/list/history/diff/compose/tree, nova label (deployment labels + protected maker-checker moves) |
| Evaluation & annotation | nova eval score config, nova annotate, nova score submit (+ novafabric.scores.submit), nova comment, nova experiment run/compare |
| Capture completeness | nova session new/add/list/reindex/show/replay/export/verify-bundle/import, nova graph agent, nova capture --capture-media + nova media list, --environment, variant attribution (--experiment/--variant), observation log levels, nova validate --schemas |
| Offline analytics | nova query, nova view, nova trend, per-usage-type token accounting, nova pricing + nova cost estimate |
| Governance | nova retention plan/apply/status/explain, PII masking plugins (--masker), the budget promotion gate (Rego), nova events webhooks, SCIM 2.0 provisioning, partial SAML SSO (metadata + assertion-validation policy; live assertion consumption shipped opt-in in v0.73.0 and is off by default) |
| Portability & interop | nova export --html, nova export-blob + manifest nova verify, OTLP GenAI-span ingest (POST /api/otlp/v1/traces), nova eval import-inspect/export-inspect, nova diagnose --intervene, nova pii status |
What shipped in v0.75–v0.94
A second large cohort landed after v0.59, mostly ADR-0101 (attribution/root-cause)
and a set of standalone trust primitives. All are experimental; some are
CLI-exposed today, others are Python-API only — no nova subcommand wraps
them yet, so build against the module, not a shipped command:
| Capability | Status | Where |
|---|---|---|
| Counterfactual root-cause search (ADR-0101 NF-018/020) | CLI, experimental | nova diagnose <run-id> --search-root-cause [--max-interventions N] — sweeps ranked causal-root candidates with bounded intervention replays until one confirms |
| Causal-graph back-trace (ADR-0101 NF-019/022) | Python API only | diagnose.causal_root_candidates() — used internally by --search-root-cause; no direct CLI verb |
| Span-level claim-grounding audit (ADR-0101 NF-021) | Python API only | diagnose.audit_claims() — flags a model-span claim ungrounded when no tool-span evidence precedes it; not wired into nova diagnose output yet |
| NATS lineage consumer (cap-006, ADR-0061/0066/0219) | CLI, experimental | nova lineage consume |
| EU AI Act Art. 12 record-keeping export | CLI, experimental | nova euaiact export / nova euaiact status |
| Compliance cohort (ISO 42001/42005, Art.72 PMM, GPAI Art.53, NIST GenAI/CSA) | CLI, experimental | nova export-compliance {iso42001,pmm,gpai53,genai-profile} |
| Portable agent-passport projection (ADR-0149) | CLI, experimental | nova passport issue / nova passport verify |
| SAML SSO (ADR-0138) | CLI, experimental, partial | nova server saml-metadata emits SP metadata only; assertion-consumption is opt-in server config, not a CLI verb |
| x509 certificate-pinned signing identity (ADR-0055) | Python API only | trust/novaseal/x509_identity.py |
| x509 CA-bundle signer chain validation (ADR-0055 trust step 2) | CLI, experimental | nova verify --ca-bundle <pem> (or ca_bundle: in novaseal.yaml); library: validate_certificate_chain() in trust/novaseal/x509_identity.py |
| Crypto-agility hybrid-signature envelope (ADR-0072) | Python API only | trust/novaseal/hybrid_signature.py |
did:key + Verifiable Credentials (ADR-0075) |
Python API only | trust/did.py |
| "Acted-as" delegation chains (ADR-0106) | Python API only | trust/delegation.py |
| Transparency-log witness cosigning (ADR-0097) | Python API only | trust/novaseal/witness.py |
| Jurisdiction sovereignty site-seals (ADR-0077) | Python API only | compliance/sovereignty.py |
The Python-API-only rows above have no nova command and no dashboard panel —
they are real, tested modules (see the corresponding ADR for design intent),
but there is no shipped end-user workflow around them yet. Do not follow a
tutorial that shows a nova command for them; none exists. See
Python API and docs/developer-guide.md
for the module-level detail.
Summary and next steps
You now have the full shipped nova command surface, organized around the five
primitives:
| Primitive | Commands |
|---|---|
| Run Capsule | nova capture, @agent decorator, runners, nova validate, nova scan-secrets |
| Replay | nova replay (forensic / mocked / semantic / exact; intervention experimental), nova diff |
| Lineage | nova lineage provenance / blast-radius / replay-chain / time-travel / import / emit-openlineage |
| Evidence Bundle | nova redact, nova export-evidence |
| Asset Registry | nova register, nova suggest-register, nova list, nova inspect, nova promote, nova rollback, nova eval, nova report |
Two journeys branch from the Run Capsule: Capture → Replay → Diff (developer:
what happened, what changed?) and Capture → Seal → Verify → Audit (trust: can I
preserve and verify it later?). Sealing is opt-in and experimental — see
Cryptographic sealing. Everything above
runs locally today, offline, with no accounts and no telemetry.
A good next move, depending on your goal:
- Wire a CI regression gate — capture a baseline, replay in
mockedmode after a change, and runnova diff --assert-no-regressions. Addnova scan-secrets --fail-on highas a second gate. - Produce audit evidence —
nova redactthennova export-evidence, and hand the ZIP to a reviewer who verifies it with onlysha256sumand an ed25519 verifier. - Understand impact before a change — run
nova lineage blast-radiuson the asset you are about to update.
Where to go from here:
- Getting Started — narrative walkthrough from install to first capsule
- Concepts — capsule structure, replay modes, lineage edge types
- Local Dashboard — full capability matrix vs CLI, security model
- Python API — programmatic usage
- Architecture — how the subsystems fit together
- Writing a hook plugin — extend capture to new transports
- Tutorials — getting started, why NovaFabric, capture internals, multi-agent, cluster scale, Langfuse comparison
- ROADMAP.md — what is planned, including the NovaSeal seal layer and cluster-scale tiers