trusty-mpm 1.8.2

trusty-mpm: unified multi-agent orchestration platform (core, daemon, CLI, TUI, Telegram)
//! Process-wide mutual exclusion for read-modify-write cycles on a
//! `.claude.json`.
//!
//! Why (issue #4072): two independent seeders in this crate perform an
//! unsynchronised load → mutate → store cycle against the SAME
//! `~/.claude.json` file —
//! [`crate::core::home_trust_seed::preseed_home_trust`] (workspace trust +
//! renderer-upsell dismissal) and
//! `crate::core::session_launch::settings::preseed_workspace_trust`
//! (workspace trust + `enabledMcpjsonServers` approval). Both run inside the
//! daemon, which provisions and spawns sessions CONCURRENTLY: the workspace
//! provisioner calls the former and then, via `prepare_session`, the latter,
//! once per session, from independent tokio tasks. Two overlapping cycles are
//! a textbook lost update — the slower writer serialises the snapshot it read
//! BEFORE the faster writer's store, so the faster writer's whole
//! `projects.<workspace>` entry silently disappears. The operator's symptom is
//! a session that stalls on the "Do you trust the files in this folder?" or
//! "new MCP servers found" dialog it was supposed to have pre-dismissed, with
//! nothing logged anywhere — both writes "succeeded".
//!
//! This also surfaced as a recurring CI flake: `trusty-mpm`'s lib test binary
//! runs `#[serial]` tests that redirect the process-global `$HOME` to a temp
//! dir alongside non-serial tests that resolve `$HOME` at call time and drive
//! the real provisioner, so a foreign test's seed lands in — and clobbered —
//! the serial test's temp `.claude.json`
//! (`core::session_launch::tests_manifest_toggle_trust_3934` failed this way
//! on PRs #4057 and #4067).
//!
//! What: [`lock`] hands out a guard on one process-wide mutex. Every seeder
//! that read-modify-writes a `.claude.json` holds it across the WHOLE cycle
//! (read, mutate, write), never just the write. Four callers hold it, over two
//! file classes:
//!
//! - `~/.claude.json` — [`crate::core::home_trust_seed::preseed_home_trust`]
//!   and `crate::core::session_launch::settings::preseed_workspace_trust`, the
//!   two seeders named above.
//! - `<claude_config_dir>/.claude.json` —
//!   [`crate::core::mcp_config::seed_builtin_servers`] and
//!   [`crate::core::standalone::trust_seed::preseed_managed_trust`] (#4076).
//!
//! #7762 took the `settings.json` / `settings.local.json` writers OFF this
//! mutex. They were here for the right reason under the wrong primitive: their
//! racing writers are `tm launch`, the daemon and `tm doctor --fix`, which are
//! separate PROCESSES, and the SCOPE note below says plainly that an in-process
//! mutex cannot serialise those. They now hold
//! [`crate::core::settings_lock`]'s `flock(2)` sidecar, which serialises threads
//! and processes alike. Nothing acquires both locks, so there is no acquisition
//! order to get wrong.
//!
//! The guard is deliberately not keyed by path. That is still the right
//! granularity for the seeders left on it: a path key would let two callers that
//! spell the same file differently — `$HOME` resolved at different moments, a
//! symlinked config dir — take different locks and race anyway, and the cost of
//! over-serialising is a few microseconds on a once-per-session path.
//!
//! SCOPE: this is an in-process lock. It does not (and is not meant to)
//! serialise two separate `tm` processes racing the same file — that needs an
//! OS file lock. `tm launch` seeds from the CLI process while the daemon seeds
//! from its own, so such a race is real, and its consequence is a LOST UPDATE:
//! one process's `projects.<workspace>` entry silently overwritten by the
//! other's. It is not a torn file. Every write goes through
//! `trusty_common::claude_config::write_json_atomic`, which since #4077 stages
//! through a per-call filename and publishes by `rename`, so a reader always
//! sees one writer's complete bytes. Before #4077 that claim was false — the
//! shared fixed `<path>.tmp` meant a cross-process race COULD publish a torn
//! file, and this comment asserted otherwise. Closing the lost update for
//! `.claude.json` too is the natural extension of #7762's sidecar and is
//! deliberately not done here.
//!
//! Test: `concurrent_seeds_preserve_every_workspace_entry` and
//! `concurrent_home_and_workspace_seeds_preserve_both_entries`, in
//! `tests_claude_json_concurrency_4072`;
//! `concurrent_managed_seeds_preserve_every_workspace_entry` (#4076), in
//! `core::standalone::trust_seed`'s tests.

use std::sync::{Mutex, MutexGuard};

/// The one process-wide `.claude.json` read-modify-write mutex.
static CLAUDE_JSON_LOCK: Mutex<()> = Mutex::new(());

/// Acquire the process-wide `.claude.json` read-modify-write lock.
///
/// Why: see the module doc — holding this across a seeder's whole load →
/// mutate → store cycle is what makes two concurrent seeds merge instead of
/// clobbering each other.
/// What: locks [`CLAUDE_JSON_LOCK`], recovering from poisoning by taking the
/// inner guard. Poison recovery is correct here because the mutex guards no
/// in-memory invariant — its payload is `()`; a panicking seeder leaves the
/// on-disk file either untouched or atomically replaced, so the next holder
/// starts from a consistent read either way. Panicking instead would turn one
/// unrelated seeder panic into a permanently un-seedable config for the rest
/// of the process's life.
/// Test: `concurrent_seeds_preserve_every_workspace_entry`,
/// `concurrent_home_and_workspace_seeds_preserve_both_entries`,
/// `concurrent_managed_seeds_preserve_every_workspace_entry`.
pub(crate) fn lock() -> MutexGuard<'static, ()> {
    CLAUDE_JSON_LOCK
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner())
}