Skip to content

fix(durable): enforce encryption_gate INV-8 policy at runtime#6040

Merged
bug-ops merged 1 commit into
mainfrom
fix/5996-durable-encryption-gate-inv8
Jul 11, 2026
Merged

fix(durable): enforce encryption_gate INV-8 policy at runtime#6040
bug-ops merged 1 commit into
mainfrom
fix/5996-durable-encryption-gate-inv8

Conversation

@bug-ops

@bug-ops bug-ops commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Summary

  • zeph_durable::encryption_gate (the documented INV-8 AEAD enforcement policy) was a unit-tested pure function that no production call site ever reached. load_write_cipher/open_backend in src/commands/durable.rs each made their own cipher decision from encrypt_payload alone, ignoring backend and shared-database status, so a misconfigured shared-DB deployment silently persisted durable journal payloads (tool outputs, agent-turn state) in plaintext with zero warning and zero error.
  • Both call sites now evaluate encryption_gate before deciding on a cipher: a forbidden combination (encrypt_payload = false on a non-local or shared-DB backend) fails closed with a hard error, and the permitted single-user local override emits the documented startup tracing::warn!.
  • Added a new [durable] shared_db config field (default false) so operators can declare a shared-database deployment explicitly; a postgres:///postgresql:// resolved journal URL is also treated as shared automatically, as defense in depth.
  • The TUI durable panel poller (durable_poll_task, feature tui) had the same gap and bypassed the gate entirely — it now evaluates the same policy and degrades gracefully to the panel's existing "non-durable mode" state on rejection, instead of rendering a journal the CLI would refuse to open.
  • Also fixed a related ordering issue in scheduler_daemon.rs::build_durable_adapter: the gate check now runs before the backend is opened, avoiding a stray empty journal file being created on a forbidden config.
  • Includes the mandatory integration points: config.toml documentation, a config-migration step (step 79), an --init wizard prompt, mdBook docs, and a testing playbook scenario + coverage-status.md row.

Closes #5996

Test plan

  • cargo +nightly fmt --check — clean
  • cargo clippy --profile ci --workspace --all-targets --features "desktop,ide,server,chat,pdf,scheduler,testing" -- -D warnings — clean
  • cargo nextest run --config-file .github/nextest.toml --workspace --features "desktop,ide,server,chat,pdf,scheduler" --lib --bins — 12808 passed, 0 failed
  • RUSTFLAGS="-D warnings" RUSTDOCFLAGS="--deny rustdoc::broken_intra_doc_links" cargo doc --no-deps --workspace --features "desktop,ide,server,chat,pdf,scheduler" — clean
  • New regression tests cover both the write path (load_write_cipher) and the read path (open_backend) rejecting the forbidden encrypt_payload=false + shared-DB combination, plus the permitted local-only override and postgres:// URL-scheme detection
  • Adversarially reviewed: traced every durable cipher/backend-construction site workspace-wide (runner.rs orchestration + agent-turn adapters, scheduler_daemon, subagent durable-context path, TUI poller) to confirm no bypass remains

zeph_durable::encryption_gate was a unit-tested pure function that no
production call site ever reached. load_write_cipher and open_backend
in src/commands/durable.rs each made their own cipher decision from
encrypt_payload alone, ignoring backend and shared-database status, so
a misconfigured shared-DB deployment silently persisted durable
journal payloads in plaintext with no warning and no error.

Both call sites, plus the TUI durable panel poller which had the same
gap, now evaluate encryption_gate before deciding on a cipher: a
forbidden combination fails closed with a hard error (or, in the TUI
poller, a graceful non-durable-mode fallback), and the permitted
single-user local override emits the documented startup warning.

Adds a `[durable] shared_db` config field so operators can declare a
shared-database deployment explicitly, with a postgres:// URL-scheme
sniff as defense in depth. Includes a config-migration step, --init
wizard prompt, and regression tests for both the write and read paths.

Closes #5996
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working config Configuration file changes documentation Improvements or additions to documentation rust Rust code changes size/L Large PR (201-500 lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

durable: encryption_gate (INV-8 shared-DB AEAD enforcement) never called at runtime

1 participant