Skip to content

Add read-path dry-run/explain mode: preview route + scope + deny without executing adapters #125

Description

@se-jo-ma

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.pyBrokerRequest (:31), BrokerResponse (:148), AuditEntry (:188)
  • nautilus/transport/fastapi_app.py/v1/request handler (~:274)
  • nautilus/cli/__init__.py — subparser registration (~:68)

Acceptance criteria

  • Dry-run returns sources_queried/sources_denied/sources_skipped/scope_restrictions/rule_trace with empty data and zero adapter calls (assert no adapter connect/query invoked).
  • Cumulative-exposure / session-store state is NOT mutated by a dry run.
  • The audit entry carries a dry_run marker distinguishing rehearsals from prod runs.
  • Attestation is skipped or clearly marked dry-run; no signed token is issued that implies real access.
  • Reachable via both REST and a nautilus CLI subcommand.
  • uv run pytest -m unit passes; new unit tests cover the no-adapter / no-session-mutation invariants.

Size: L

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium priority: v2 epic or non-blocking gaparea/clinautilus CLI commandsarea/coreBroker core, CLIPS routing, session stateenhancementNew feature or requestneeds-designDesign not yet settled — please don't open a PR yetsize/L<1 week: cross-cutting feature

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions