main
Drew Owens 0.10.9 source 32473a3 6d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
//! IS THE USER WORKING IN THIS SOLIDWORKS? — the user-presence guard (0.9.9).
//!
//! MEASURED 2026-10-01 on hosted 0.9.8: a "leave the machine as found"
//! `swx_stop` on a bridge-launched, relaunched, EMPTY-LOOKING session returned
//! `stop_blocked_by_dialog` — the dialog was SOLIDWORKS's own File Open dialog,
//! because DREW HAD STARTED USING THE SESSION. Minutes later it held 34
//! documents: his crate assembly and its parts. Nothing was lost only because
//! his dialog happened to be up; a session he had just brought to the front
//! would have been quit under him, and a sweep a minute later would have closed
//! his documents. The bridge had no notion that the USER had taken over a
//! session it launched.
//!
//! So, before every verb that ENDS work — swx_stop, swx_shutdown,
//! swx_close_all_safely, swx_close_all_documents, swx_reset_workspace, and the
//! quit ladder's priming — presence is MEASURED:
//!
//!   * is a SOLIDWORKS window (the session's process or one of its children)
//!     the FOREGROUND window, and how long since the last user input
//!     (system-wide) — foreground AND input within `idle_secs()` (120 s);
//!   * is any document open that the bridge did not put there itself. THE
//!     BASELINE MAY CONTAIN ONLY DOCUMENTS THE BRIDGE ITSELF PUT THERE
//!     (adversary pass 46): it starts EMPTY for every session — bridge-launched
//!     or not — and grows only by what a bridge verb opened or created, measured
//!     as the difference across that verb; it is never re-read from the open
//!     set. So in a session the USER launched, every document is the user's, and
//!     quit / sweep / drain refuse `user_is_active` without the explicit
//!     override — the intended reading of "never end a session the user is
//!     working in";
//!   * was the session already marked `takenOverByUser`.
//!
//! PRESENT refuses `user_is_active` with the evidence. An unreadable
//! measurement is PRESENT (fail closed) — but only POSITIVE evidence marks the
//! session taken over, persistently for its (pid, creation time): from then on
//! the bridge never ends or sweeps it without `userPresentOverride:true` +
//! `userAuthorized:true` + a reason, even after the user steps away.
//! Attribution of the launch stays; authority to end the session does not.

use serde_json::{json, Value};

/// Input within this many seconds, with SOLIDWORKS in the foreground, is a
/// user at the keyboard. `SWX_PRESENCE_IDLE_SECS` overrides.
pub fn idle_secs() -> u64 {
    std::env::var("SWX_PRESENCE_IDLE_SECS").ok().and_then(|v| v.trim().parse().ok()).unwrap_or(120)
}

/// The verbs that end a user's work, and so are guarded.
///
/// `swx_reset_workspace` is NOT here (0.10.9): it closes only the bridge's own
/// never-saved scratch documents, and while the user is present it does so for
/// every one EXCEPT the active document (see `verbs3::reset_workspace`).
/// MEASURED 2026-10-14: with it refused outright, 17 resets in a row were
/// refused while 18 new scratch parts were allowed — 18 of the bridge's windows
/// piled up on the user's screen.
pub const GUARDED: &[&str] = &["swx_stop", "swx_shutdown", "swx_close_all_safely", "swx_close_all_documents"];

/// The explicit user-present override: `userPresentOverride:true` +
/// `userAuthorized:true` + a reason.
pub fn overridden(args: &Value) -> bool {
    args.get("userPresentOverride").and_then(|v| v.as_bool()) == Some(true)
        && args.get("userAuthorized").and_then(|v| v.as_bool()) == Some(true)
        && args.get("reason").and_then(|v| v.as_str()).map(|r| !r.trim().is_empty()).unwrap_or(false)
}

// ── IS THE USER AT THE MACHINE RIGHT NOW? (0.10.9) ───────────────────────────
//
// MEASURED 2026-10-14 (hosted 0.10.7): while Drew was at the keyboard, every
// close the bridge attempted was refused for his presence and every NEW scratch
// part was allowed — 18 of the bridge's windows piled up on his screen, checks
// went from 14 s to a 7-minute stall, SOLIDWORKS reached 15.1 GB and the
// machine 91 % committed. The rule is now symmetric: the bridge does not END a
// session the user is working in, and it does not POP its own windows on a
// user who is working.

/// The user's ACTIVITY, read without COM: input within `idle_secs()`
/// system-wide (GetLastInputInfo) is a user at the machine, whatever
/// application is in front. An unreadable input time counts as active.
#[derive(Clone, Debug)]
pub struct Activity {
    pub active: bool,
    pub idle_seconds: Option<u64>,
    pub foreground: Value,
    pub foreground_is_solidworks: Option<bool>,
}

impl Activity {
    pub fn to_json(&self) -> Value {
        json!({
            "active": self.active,
            "idleSeconds": self.idle_seconds,
            "idleThresholdSecs": idle_secs(),
            "foreground": self.foreground,
            "foregroundIsSolidworks": self.foreground_is_solidworks,
            "rule": "active = the user gave input (anywhere on the machine) within idleThresholdSecs; \
                     an unreadable input time counts as active. While active, the bridge does not \
                     create documents for its own purposes (self-test, batteries) and does not end \
                     the session.",
        })
    }
}

pub fn activity() -> Activity {
    let os = crate::launchos::os();
    // A simulated SOLIDWORKS with no presence scenario is on nobody's screen.
    if crate::is_mock() && std::env::var(crate::launchos::ENV_FAKE_DIR).map(|d| d.is_empty()).unwrap_or(true) {
        return Activity { active: false, idle_seconds: None, foreground: Value::Null, foreground_is_solidworks: None };
    }
    let input = os.seconds_since_last_input().ok();
    let sw_pids: Vec<u32> = os
        .processes()
        .ok()
        .map(|c| {
            let sw: Vec<u32> = c.iter().filter(|p| p.is("SLDWORKS.exe")).map(|p| p.pid).collect();
            let mut all = sw.clone();
            all.extend(c.iter().filter(|p| p.ppid.map(|pp| sw.contains(&pp)).unwrap_or(false)).map(|p| p.pid));
            all
        })
        .unwrap_or_default();
    let (foreground, is_sw) = match os.foreground_window() {
        Ok(Some(w)) => (json!({"title": w.title, "pid": w.pid}), Some(sw_pids.contains(&w.pid))),
        Ok(None) => (json!({"unreadable": "no window has the foreground"}), None),
        Err(e) => (json!({"unreadable": e}), None),
    };
    Activity {
        active: input.map(|n| n < idle_secs()).unwrap_or(true),
        idle_seconds: input,
        foreground,
        foreground_is_solidworks: is_sw,
    }
}

/// How long a bridge-purpose creation waits for the user to go idle:
/// `waitForIdleSeconds`, else `SWX_WAIT_FOR_IDLE_SECS`, else 120 (at most 900).
pub fn wait_for_idle_secs(args: &Value) -> u64 {
    args.get("waitForIdleSeconds")
        .and_then(|v| v.as_u64())
        .or_else(|| std::env::var("SWX_WAIT_FOR_IDLE_SECS").ok().and_then(|v| v.trim().parse().ok()))
        .unwrap_or(120)
        .min(900)
}

/// WAIT FOR THE USER TO GO IDLE, up to `secs`. `Ok` = idle (or became idle);
/// `Err` = still active when the time ran out — with the last reading.
pub fn wait_for_idle(secs: u64) -> Result<Activity, Activity> {
    let poll = std::time::Duration::from_millis(
        std::env::var("SWX_PRESENCE_POLL_MS").ok().and_then(|v| v.trim().parse().ok()).unwrap_or(5000),
    );
    let deadline = std::time::Instant::now() + std::time::Duration::from_secs(secs);
    loop {
        let a = activity();
        if !a.active {
            return Ok(a);
        }
        if std::time::Instant::now() >= deadline {
            return Err(a);
        }
        std::thread::sleep(poll.min(deadline.saturating_duration_since(std::time::Instant::now()).max(std::time::Duration::from_millis(10))));
    }
}

/// A verb that would CREATE a visible document for the BRIDGE's own purposes:
/// the self-test's write level, and `swx_new_document` / `swx_run_script`
/// marked `purpose:"bridge"` (batteries, the bridge's own scratch). A user's
/// own request is not this.
pub fn creates_for_bridge(command: &str, args: &Value) -> bool {
    match command {
        "swx_selftest" => args.get("level").and_then(|l| l.as_str()) == Some("write"),
        "swx_new_document" | "swx_run_script" | "swx_import" => {
            args.get("purpose").and_then(|p| p.as_str()) == Some("bridge")
        }
        _ => false,
    }
}

/// The refusal for a bridge-purpose creation while the user is active.
pub fn user_present_refusal(verb: &str, waited: u64, a: &Activity) -> Value {
    let mut out = crate::verbs::err_code(
        "user_present",
        format!(
            "{verb} refused: the user is at the machine (input {} ago) and stayed active for the {waited} s \
             the bridge waited. It does not open its own windows on a user who is working. Nothing was created.",
            a.idle_seconds.map(|n| format!("{n} s")).unwrap_or_else(|| "at an unreadable time".into())
        ),
        "Run it when the user is away (swx_status.userPresence.active is false), or pass a longer \
         waitForIdleSeconds. A script the USER asked for is not refused — this is only for the \
         bridge's own documents (purpose:\"bridge\", batteries, the self-test).",
    );
    if let Value::Object(m) = &mut out {
        m.insert("userPresence".into(), a.to_json());
        m.insert("changedNothing".into(), json!(true));
        m.insert("waitedSeconds".into(), json!(waited));
    }
    crate::ledger::record("user_present_refusal", json!({"verb": verb, "kind": "creation", "waitedSeconds": waited}));
    out
}

fn path() -> std::path::PathBuf {
    crate::scratch::root().join("presence.json")
}

fn key(pid: u32, created: Option<u64>) -> String {
    format!("{pid}:{}", created.map(|c| c.to_string()).unwrap_or_else(|| "?".into()))
}

fn load() -> serde_json::Map<String, Value> {
    std::fs::read_to_string(path())
        .ok()
        .and_then(|t| serde_json::from_str::<Value>(&t).ok())
        .and_then(|v| v.as_object().cloned())
        .unwrap_or_default()
}

fn save(mut m: serde_json::Map<String, Value>) {
    while m.len() > 20 {
        let oldest = m
            .iter()
            .min_by_key(|(_, v)| v.get("atMs").and_then(|a| a.as_u64()).unwrap_or(0))
            .map(|(k, _)| k.clone());
        match oldest {
            Some(o) => {
                m.remove(&o);
            }
            None => break,
        }
    }
    let _ = crate::launch::write_atomic(&path(), &Value::Object(m).to_string());
}

fn entry(k: &str) -> Value {
    load().get(k).cloned().unwrap_or_else(|| json!({}))
}

fn put(k: &str, v: Value) {
    let mut m = load();
    let mut v = v;
    if let Value::Object(o) = &mut v {
        o.insert("atMs".into(), json!(crate::launchos::epoch_ms() as u64));
    }
    m.insert(k.to_string(), v);
    save(m);
}

/// The running session: (pid, creation time, documents, truncated). `None`
/// when SOLIDWORKS is not running or the host cannot say.
struct Session {
    pid: u32,
    created: Option<u64>,
    docs: Vec<String>,
    count: Option<u64>,
    titles: std::collections::HashMap<String, String>,
}

fn doc_key(d: &Value) -> String {
    let p = d.get("path").and_then(|p| p.as_str()).unwrap_or("");
    if !p.is_empty() {
        p.replace('/', "\\").to_lowercase()
    } else {
        format!("title:{}", d.get("title").and_then(|t| t.as_str()).unwrap_or("").to_lowercase())
    }
}

fn session(ctx: &crate::verbs::Ctx) -> Option<Session> {
    let st = ctx.call("app_state", json!({}), 30).ok()?;
    if st.get("running").and_then(|v| v.as_bool()) == Some(false) {
        return None;
    }
    let pid = st.get("processId").and_then(|v| v.as_u64()).map(|p| p as u32)?;
    let created = crate::launchos::os()
        .processes()
        .ok()
        .and_then(|ps| ps.iter().find(|p| p.pid == pid).and_then(|p| p.created_at))
        .or_else(|| ctx.call("startup_state", json!({}), 20).ok().and_then(|s| s.get("processCreatedAt").and_then(|v| v.as_u64())));
    let truncated = st.get("documentsTruncated").and_then(|v| v.as_bool()).unwrap_or(false);
    let list: Vec<Value> = st.get("documents").and_then(|d| d.as_array()).cloned().unwrap_or_default();
    let mut titles = std::collections::HashMap::new();
    let docs: Vec<String> = if truncated {
        Vec::new()
    } else {
        list.iter()
            .map(|d| {
                let k = doc_key(d);
                titles.insert(k.clone(), d.get("title").and_then(|t| t.as_str()).unwrap_or("").to_string());
                k
            })
            .collect()
    };
    let count = st
        .get("documentCount")
        .and_then(|v| v.as_u64())
        .or_else(|| (!truncated).then_some(docs.len() as u64));
    Some(Session { pid, created, docs, count, titles })
}

/// The open document set before a bridge verb runs — for
/// `note_bridge_changed`. `None` when it cannot be listed.
pub fn snapshot(ctx: &crate::verbs::Ctx) -> Option<(u32, Option<u64>, Vec<String>)> {
    let s = session(ctx)?;
    if s.docs.is_empty() && s.count.unwrap_or(0) > 0 {
        return None; // truncated: unlistable, not empty
    }
    Some((s.pid, s.created, s.docs))
}

/// WHAT THE VERB ITSELF CHANGED goes into the baseline — the documents open
/// after it that were not open before it — and documents no longer open leave
/// it. Never a re-read of the whole open set (adversary pass 46, B3): a
/// document the user opened BETWEEN bridge verbs stays the user's. (One it
/// opens DURING a bridge verb cannot be told apart — the stated limit.)
pub fn note_bridge_changed(ctx: &crate::verbs::Ctx, before: Option<(u32, Option<u64>, Vec<String>)>) {
    let Some(after) = session(ctx) else { return };
    let k = key(after.pid, after.created);
    let mut e = entry(&k);
    let mut docs: Vec<String> = e
        .get("bridgeDocs")
        .and_then(|v| v.as_array())
        .map(|a| a.iter().filter_map(|x| x.as_str().map(String::from)).collect())
        .unwrap_or_default();
    let truncated = after.docs.is_empty() && after.count.unwrap_or(0) > 0;
    if !truncated {
        docs.retain(|d| after.docs.contains(d));
        if let Some((pid, created, before_docs)) = before {
            if pid == after.pid && created == after.created {
                for d in &after.docs {
                    if !before_docs.contains(d) && !docs.contains(d) {
                        docs.push(d.clone());
                    }
                }
            }
        }
    }
    if let Value::Object(o) = &mut e {
        o.insert("bridgeDocs".into(), json!(docs));
    }
    put(&k, e);
}

/// The running session's taken-over record, if it has one — for the reply of a
/// verb that adds a document to it (allowed: it adds, never removes).
pub fn taken_over_now(ctx: &crate::verbs::Ctx) -> Option<Value> {
    let st = ctx.call("startup_state", json!({}), 20).ok()?;
    let pid = st.get("processId").and_then(|v| v.as_u64()).map(|p| p as u32)?;
    let created = crate::launchos::os()
        .processes()
        .ok()
        .and_then(|ps| ps.iter().find(|p| p.pid == pid).and_then(|p| p.created_at))
        .or_else(|| st.get("processCreatedAt").and_then(|v| v.as_u64()));
    taken_over(pid, created).map(|e| {
        json!({"takenOverByUser": true, "evidence": e.get("evidence").cloned().unwrap_or(Value::Null),
               "note": "the user has taken over this session; documents may be added, and nothing \
                        will be closed or quit without the user-present override"})
    })
}

/// Is this session marked as taken over by the user?
pub fn taken_over(pid: u32, created: Option<u64>) -> Option<Value> {
    let e = entry(&key(pid, created));
    (e.get("takenOverByUser").and_then(|v| v.as_bool()) == Some(true)).then_some(e)
}

fn mark_taken_over(pid: u32, created: Option<u64>, evidence: &Value) {
    let k = key(pid, created);
    let mut e = entry(&k);
    if e.get("takenOverByUser").and_then(|v| v.as_bool()) == Some(true) {
        return;
    }
    if let Value::Object(o) = &mut e {
        o.insert("takenOverByUser".into(), json!(true));
        o.insert("takenOverAtMs".into(), json!(crate::launchos::epoch_ms() as u64));
        o.insert("evidence".into(), evidence.clone());
    }
    put(&k, e);
    crate::ledger::record("session_taken_over_by_user", json!({"pid": pid, "createdAt": created, "evidence": evidence}));
}

/// PRESENCE WITHOUT COM (0.9.11, L4): for swx_user_presence while SOLIDWORKS
/// has not finished starting (or COM cannot answer) — the foreground window and
/// the last input are measured exactly as always, from the process census; the
/// DOCUMENT part is unknown, and unknown counts as present.
pub fn measure_without_com() -> Option<(bool, Value)> {
    let os = crate::launchos::os();
    let census = os.processes().ok()?;
    let sw: Vec<&crate::launchos::ProcInfo> =
        census.iter().filter(|p| p.image.eq_ignore_ascii_case("SLDWORKS.exe")).collect();
    let [one] = sw.as_slice() else { return None };
    let (pid, created) = (one.pid, one.created_at);
    let children: Vec<u32> = census.iter().filter(|p| p.ppid == Some(pid)).map(|p| p.pid).collect();
    let fg = os.foreground_window();
    let input = os.seconds_since_last_input().ok();
    let (fg_json, fg_is_sw) = match &fg {
        Ok(Some(w)) => (
            json!({"hwnd": w.hwnd, "pid": w.pid, "title": w.title, "className": w.class_name}),
            w.pid == pid || children.contains(&w.pid),
        ),
        Ok(None) => (json!({"unreadable": "no window has the foreground"}), false),
        Err(e) => (json!({"unreadable": e}), false),
    };
    let at_keyboard = fg_is_sw && input.map(|n| n < idle_secs()).unwrap_or(false);
    let taken = taken_over(pid, created).is_some();
    let evidence = json!({
        "present": true,
        "foregroundWindow": fg_json,
        "foregroundIsSolidworks": fg_is_sw,
        "secondsSinceLastInput": input,
        "idleThresholdSecs": idle_secs(),
        "foreignDocuments": Value::Null,
        "documentsUnknown": true,
        "takenOverByUser": taken || at_keyboard,
        "takenOverEarlier": taken,
        "processId": pid,
        "processCreatedAt": created,
        "basis": if at_keyboard {
            "SOLIDWORKS is the foreground window and the user gave input recently"
        } else {
            "SOLIDWORKS cannot be asked about its documents right now (it has not finished starting, \
             or COM cannot answer), so whether the user has documents open is UNKNOWN — which counts \
             as present"
        },
    });
    if at_keyboard {
        mark_taken_over(pid, created, &evidence);
    }
    Some((true, evidence))
}

/// MEASURE PRESENCE. `(present, evidence)`.
pub fn measure(ctx: &crate::verbs::Ctx) -> Option<(bool, Value)> {
    let s = session(ctx)?;
    let os = crate::launchos::os();
    let k = key(s.pid, s.created);
    let e = entry(&k);
    let taken = e.get("takenOverByUser").and_then(|v| v.as_bool()) == Some(true);

    // FOREGROUND: the session's process, or one of its children (a sign-in
    // web view is a child process).
    let census = os.processes().ok();
    let children: Vec<u32> = census
        .as_ref()
        .map(|c| c.iter().filter(|p| p.ppid == Some(s.pid)).map(|p| p.pid).collect())
        .unwrap_or_default();
    let fg = os.foreground_window();
    let input = os.seconds_since_last_input();
    let simulator_without_scenario = crate::is_mock() && std::env::var(crate::launchos::ENV_FAKE_DIR).map(|d| d.is_empty()).unwrap_or(true);
    let (fg_json, fg_is_sw, fg_unreadable) = match &fg {
        Ok(Some(w)) => (
            json!({"hwnd": w.hwnd, "pid": w.pid, "title": w.title, "className": w.class_name}),
            w.pid == s.pid || children.contains(&w.pid),
            false,
        ),
        // NO FOREGROUND WINDOW is not "not SOLIDWORKS": Windows returns NULL on
        // the secure desktop (a UAC prompt, the lock screen) and in a service or
        // disconnected session, where the last-input time is stale too. It is
        // UNREADABLE, which counts as present (adversary pass 46, B4).
        Ok(None) => (json!({"unreadable": "no window has the foreground (secure desktop, lock screen, or a disconnected session)"}), false, true),
        Err(e) => (json!({"unreadable": e}), false, true),
    };
    let (input_secs, input_unreadable) = match &input {
        Ok(n) => (Some(*n), false),
        Err(_) => (None, true),
    };
    // A simulated SOLIDWORKS with no presence scenario is on nobody's screen.
    let (fg_unreadable, input_unreadable) = if simulator_without_scenario {
        (false, false)
    } else {
        (fg_unreadable, input_unreadable)
    };
    let at_keyboard = fg_is_sw && input_secs.map(|n| n < idle_secs()).unwrap_or(false);

    // FOREIGN DOCUMENTS: open now and NOT put there by the bridge. The
    // baseline starts empty and holds only what bridge verbs opened or created.
    let bridge_docs: Vec<String> = e
        .get("bridgeDocs")
        .and_then(|v| v.as_array())
        .map(|a| a.iter().filter_map(|x| x.as_str().map(String::from)).collect())
        .unwrap_or_default();
    let truncated = s.docs.is_empty() && s.count.unwrap_or(0) > 0;
    let foreign: Vec<String> = s.docs.iter().filter(|d| !bridge_docs.contains(d)).cloned().collect();
    let foreign_titles: Vec<String> =
        foreign.iter().map(|k| s.titles.get(k).cloned().unwrap_or_else(|| k.clone())).collect();
    // A simulated SOLIDWORKS with no presence scenario has no user and no
    // user's documents (mock only — LiveOs never takes this path).
    let foreign_seen = !simulator_without_scenario
        && (!foreign.is_empty() || (truncated && s.count.unwrap_or(0) > bridge_docs.len() as u64));
    let unreadable = fg_unreadable || input_unreadable;
    let positive = at_keyboard || foreign_seen;
    let present = positive || unreadable || taken;
    let evidence = json!({
        "present": present,
        "foregroundWindow": fg_json,
        "foregroundIsSolidworks": fg_is_sw,
        "secondsSinceLastInput": input_secs,
        "idleThresholdSecs": idle_secs(),
        "foreignDocuments": {"count": if simulator_without_scenario { 0 } else { foreign.len() },
                              "titles": if simulator_without_scenario { json!([]) } else { json!(foreign_titles) },
                              "listTruncated": truncated,
                              "rule": "a document is the bridge's only if a bridge verb opened or created it; every other open document is the user's"},
        "takenOverByUser": taken || positive,
        "takenOverEarlier": taken,
        "measurementUnreadable": unreadable,
        "processId": s.pid,
        "processCreatedAt": s.created,
        "basis": if taken {
            "this session was taken over by the user earlier; the bridge does not end it without the override"
        } else if at_keyboard {
            "SOLIDWORKS is the foreground window and the user gave input recently"
        } else if foreign_seen {
            "documents are open that this bridge did not open or create (in a session the user launched, every document is theirs)"
        } else if unreadable {
            "presence could not be measured, which counts as present"
        } else {
            "no sign of the user: SOLIDWORKS is not in the foreground with recent input, and no document appeared that the bridge did not open"
        },
    });
    if positive {
        mark_taken_over(s.pid, s.created, &evidence);
    }
    Some((present, evidence))
}

/// THE GUARD, for a verb that ends work. `Ok(evidence)` lets it run (absent,
/// not running, or overridden); `Err(refusal)` is `user_is_active`.
pub fn guard(ctx: &crate::verbs::Ctx, verb: &str, args: &Value) -> Result<Value, Value> {
    let Some((present, evidence)) = measure(ctx) else {
        return Ok(json!({"present": Value::Null, "why": "no running SOLIDWORKS session to measure"}));
    };
    if !present {
        return Ok(evidence);
    }
    if overridden(args) {
        let who = crate::caller::current();
        crate::ledger::record(
            "user_present_override",
            json!({
                "verb": verb,
                "reason": args.get("reason"),
                "evidence": evidence,
                // WHO passed it (self-asserted, like the reason — recorded, never
                // used to authorize).
                "caller": who.as_ref().map(|c| json!({
                    "aiThread": c.thread,
                    "containerName": c.container,
                    "source": args.pointer("/caller/source").cloned().unwrap_or(Value::Null),
                    "callerReason": c.reason,
                })),
            }),
        );
        let mut e = evidence;
        if let Value::Object(o) = &mut e {
            o.insert("overridden".into(), json!(true));
        }
        return Ok(e);
    }
    let mut out = crate::verbs::err_code(
        "user_is_active",
        format!(
            "{verb} refused: the user is working in this SOLIDWORKS ({}). The session is now the \
             user's — the bridge does not end it or sweep it.",
            evidence.get("basis").and_then(|b| b.as_str()).unwrap_or("present")
        ),
        "Nothing was closed, saved or quit. This session belongs to the person using it: leave \
         it alone. (A document counts as the bridge's only if a bridge verb opened or created it; \
         in a session the user launched, every document is the user's, so quitting or sweeping it \
         always needs their explicit say-so.) Only if the user has explicitly asked for it to be ended, pass \
         userPresentOverride:true together with userAuthorized:true and their reason — a sweep \
         under the override still preserves every document first.",
    );
    if let Value::Object(m) = &mut out {
        m.insert("userPresence".into(), evidence);
    }
    crate::ledger::record("user_present_refusal", json!({"verb": verb}));
    Err(out)
}