Context
Policy authors and operators want to preview a routing decision (sources_queried / sources_denied / sources_skipped / scope_restrictions / rule_trace) for a hypothetical request without querying any backend. Today the only way to see routing is to run a real request, which executes adapters and writes a normal audit entry.
Verified state of the code:
- A grep for
dry_run|explain|preview|what.if across nautilus/ finds only simulate_meta_rule_fire (nautilus/rkm/__init__.py:31) and UI token_preview strings — nothing on the request path.
- The broker's public entrypoints
request / arequest (nautilus/core/broker.py:988 / :1019) always run the full pipeline.
_run_pipeline (nautilus/core/broker.py:1357) is a clean linear sequence: intent analysis → Fathom routing → scope merge → temporal filter → _build_adapter_jobs fan-out (line 1373) → synthesis → attestation → _update_session. A short-circuit before _build_adapter_jobs is well-defined.
- Precedent already exists:
declare_handoff (nautilus/core/broker.py:1066) is documented as a "pure reasoning-only path (zero adapter calls, zero session-store mutation)" — the same shape this feature needs.
BrokerResponse (nautilus/core/models.py:148) already carries sources_queried/denied/skipped and scope_restrictions; AuditEntry (nautilus/core/models.py:188) already carries rule_trace and a block of optional Phase-2 fields that round-trip unchanged (NFR-5) — so adding a dry_run marker follows the established optional-field pattern.
- The write-surface roadmap (
design-docs/05-ecosystem-roadmap.md:513-515) already defines a dry_run: bool envelope audit flag for write rehearsals; this issue introduces the read-path analogue and should reuse the same audit-stamping convention. (This is distinct from the [P0] Write surface epic, which owns write rehearsal.)
Task
Add a dry-run path that runs intent analysis + Fathom routing + scope/temporal logic but skips _build_adapter_jobs / synthesis, returning routing + scope + denial + rule_trace with empty data.
- Add a
dry_run flag to the broker public API. Note BrokerRequest (nautilus/core/models.py:31) is only the REST body model — Broker.request/arequest take positional args, so this also needs either a new keyword param on those methods or a dedicated Broker.explain(...) method. Pick one in design.
- Short-circuit
_run_pipeline after _apply_temporal_filter (before line 1373) when dry-run is set.
- Cumulative-exposure / session state MUST NOT be mutated by a dry run (skip
_update_session).
- Attestation: either skipped or clearly marked dry-run — never emit a signed token implying real data access.
- Stamp the audit entry with a
dry_run marker (optional AuditEntry field, defaulting None/False to preserve NFR-5 round-trip).
- Surface via REST (a
dry_run body flag on POST /v1/request, or a dedicated POST /v1/explain) and a new nautilus CLI subcommand (none of request/query/explain exists today — only version/health/serve are wired in nautilus/cli/__init__.py).
Where
nautilus/core/broker.py — _run_pipeline (:1357), public request/arequest (:988/:1019); _build_adapter_jobs (:1565) is the boundary to skip
nautilus/core/models.py — BrokerRequest (:31), BrokerResponse (:148), AuditEntry (:188)
nautilus/transport/fastapi_app.py — /v1/request handler (~:274)
nautilus/cli/__init__.py — subparser registration (~:68)
Acceptance criteria
Size: L
Context
Policy authors and operators want to preview a routing decision (
sources_queried/sources_denied/sources_skipped/scope_restrictions/rule_trace) for a hypothetical request without querying any backend. Today the only way to see routing is to run a real request, which executes adapters and writes a normal audit entry.Verified state of the code:
dry_run|explain|preview|what.ifacrossnautilus/finds onlysimulate_meta_rule_fire(nautilus/rkm/__init__.py:31) and UItoken_previewstrings — nothing on the request path.request/arequest(nautilus/core/broker.py:988/:1019) always run the full pipeline._run_pipeline(nautilus/core/broker.py:1357) is a clean linear sequence: intent analysis → Fathom routing → scope merge → temporal filter →_build_adapter_jobsfan-out (line 1373) → synthesis → attestation →_update_session. A short-circuit before_build_adapter_jobsis well-defined.declare_handoff(nautilus/core/broker.py:1066) is documented as a "pure reasoning-only path (zero adapter calls, zero session-store mutation)" — the same shape this feature needs.BrokerResponse(nautilus/core/models.py:148) already carriessources_queried/denied/skippedandscope_restrictions;AuditEntry(nautilus/core/models.py:188) already carriesrule_traceand a block of optional Phase-2 fields that round-trip unchanged (NFR-5) — so adding adry_runmarker follows the established optional-field pattern.design-docs/05-ecosystem-roadmap.md:513-515) already defines adry_run: boolenvelope audit flag for write rehearsals; this issue introduces the read-path analogue and should reuse the same audit-stamping convention. (This is distinct from the [P0] Write surface epic, which owns write rehearsal.)Task
Add a dry-run path that runs intent analysis + Fathom routing + scope/temporal logic but skips
_build_adapter_jobs/ synthesis, returning routing + scope + denial + rule_trace with emptydata.dry_runflag to the broker public API. NoteBrokerRequest(nautilus/core/models.py:31) is only the REST body model —Broker.request/arequesttake positional args, so this also needs either a new keyword param on those methods or a dedicatedBroker.explain(...)method. Pick one in design._run_pipelineafter_apply_temporal_filter(before line 1373) when dry-run is set._update_session).dry_runmarker (optionalAuditEntryfield, defaultingNone/Falseto preserve NFR-5 round-trip).dry_runbody flag onPOST /v1/request, or a dedicatedPOST /v1/explain) and a newnautilusCLI subcommand (none ofrequest/query/explainexists today — onlyversion/health/serveare wired innautilus/cli/__init__.py).Where
nautilus/core/broker.py—_run_pipeline(:1357), publicrequest/arequest(:988/:1019);_build_adapter_jobs(:1565) is the boundary to skipnautilus/core/models.py—BrokerRequest(:31),BrokerResponse(:148),AuditEntry(:188)nautilus/transport/fastapi_app.py—/v1/requesthandler (~:274)nautilus/cli/__init__.py— subparser registration (~:68)Acceptance criteria
sources_queried/sources_denied/sources_skipped/scope_restrictions/rule_tracewith emptydataand zero adapter calls (assert no adapterconnect/queryinvoked).dry_runmarker distinguishing rehearsals from prod runs.nautilusCLI subcommand.uv run pytest -m unitpasses; new unit tests cover the no-adapter / no-session-mutation invariants.Size: L