← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.9+0.9.10
Cargo.lock+1−1
@@ -107,7 +107,7 @@ dependencies = [  [[package]] name = "swx-bridge"-version = "0.9.9"+version = "0.9.10" dependencies = [  "serde",  "serde_json",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "swx-bridge"-version = "0.9.9"+version = "0.9.10" edition = "2021" rust-version = "1.80" description = "Adom SOLIDWORKS bridge — swx_* verbs over the SOLIDWORKS COM API, as a single native binary"
README.md+11−1
@@ -458,7 +458,17 @@ not.  ## Roadmap -- v0.9.9 (this): **never end a session the user is working in.** 0.9.8 was+- v0.9.10 (this): **the bridge's documents are only the ones it opened.**+  Adversary pass 46 found the user's own documents could enter the presence+  guard's baseline three ways (a session the user launched, a document opened+  before the bridge's first verb, and any later bridge verb re-reading the open+  set) — each measured sweeping the user's documents. The baseline now starts+  empty for every session and grows only by what a bridge verb itself opened or+  created; in a session the user launched every document is theirs, so quit and+  sweep refuse `user_is_active` without the explicit override. No foreground+  window (UAC prompt, lock screen) counts as present; override ledger lines name+  the caller; the re-check before priming is pinned mid-verb. 497 tests.+- v0.9.9: **never end a session the user is working in.** 0.9.8 was   measured LIVE on 2026-10-01 (launch 16/16, drain 5/5, quit gate 11/11) — and   then Drew started using the bridge-launched session: a "leave as found" quit   met his File Open dialog, and minutes later the session held his 34-document
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "solidworks",   "displayName": "SOLIDWORKS",-  "version": "0.9.9",+  "version": "0.9.10",   "description": "Bridge for SOLIDWORKS desktop CAD \u2014 create and open documents, a modeling DSL, file management (find/open/save/export/Pack and Go), custom properties, BOM generation, design analysis (mass properties, bodies, parameters, interference, rebuild health) and background window/dialog etiquette, over the SOLIDWORKS COM API. Single native binary: no interpreter, no PowerShell, ~10ms cold start.",   "author": "Adom Inc.",   "license": "MIT",
docs/ECOSYSTEM-TABLE.md+1−1
@@ -112,7 +112,7 @@ templates**, alisamsam **22**. | 44 | **Graceful QUIT that is measured, not assumed** (never a kill, never answers a dialog, proves the exit from the process census) | ✗ | **✓ LIVE-PROVEN 2026-09-17 (rung a)** — `swx_stop {userAuthorized, maxRung:"a"}` quit a bridge-launched session on the FIRST rung (UserControl=false; ExitApp; every COM reference released), verified absent from the process census; `swx_shutdown` then removed a stray empty instance (pid 145188) and `tasklist` showed no `sldworks.exe` at all. **`ExitApp` is NOT declined at zero documents on this edition** — the long-held `host_app_stop_declined` belief was a SAVE-PROMPT STALL misread as a refusal, which is why the ladder was built pinned at rung (a) and why the preserve-then-close sweep is what actually unblocked it. A rung ladder (a `ExitApp` → f Restart Manager *without* `RmForceShutdown`), pinned at rung (a); 7 re-measured preconditions where UNKNOWN refuses and `force` waives the UNSAVED question ONLY; every COM reference released before and after each rung, with outstanding refs named in the report; success = *this pid AND this creation time* absent from the census, never `GetActiveObject`; per-session children reaped while the documented platform daemons are left alone; a dialog stops the ladder permanently and is never answered; nothing disowned until the exit is verified. every precondition re-measured before EVERY rung (a mid-ladder refusal is a real outcome), the exit POLLED to the rung's budget rather than sampled once (a real `ExitApp` returns before the process goes), the dialog census covering the `swCefSubProc` web-view children by parentage, the count cross-checked against the enumeration, and the message rungs re-verifying window ownership before they send. `tools/quit-gate.py` proves the session was bridge-launched, and **its FULL CYCLE PASSED LIVE 2026-09-30 through the bridge ab hosts**: rung (a), process gone 29.5 s, per-session servers reaped, nothing on screen, no crash reporter, every rung reported, and SOLIDWORKS relaunched by the bridge 54.7 s later. The gate now reaches the bridge through ab's verb routing (the side-loaded dev copy on 24695 is terminated by ab 2.1.163 within 30 s) and passes `userAuthorized:true` — the standing authorization — rather than the write-gate pair, which the hosted bridge rightly never enables. **MEASURED 2026-10-01: the decline is about session HISTORY** — rung (a) was declined three times on a session that had never held a document and quit in 3.4 s once one had been created and closed; since 0.9.3 the ladder PRIMES such a session (empty, no dialog, user-authorized only) and asks again — **LIVE-PROVEN 2026-10-01 on 0.9.4**: declined at 130 s, primed, quit in 3.4 s (declines measured from 50 s to 8 min old; such sessions quit unprimed at ~25 s), and `swx_status` exposes the session's document history — and the gate's "no longer running" claim — which had passed vacuously after declines — now requires `running:false` and is self-tested | ✗ | ✗ | ✗ | ✗ | ✗ |  | 48 | **A title collision between SAVED documents is impossible here — measured, not assumed** | ✗ | **✓ MEASURED LIVE 2026-09-17** — `tools/drain-gate.py --fixture new` copied a bridge-created document's own file to a second directory under the same basename and called `swx_open` on it. SOLIDWORKS REFUSED: `errors = 65536` (`swFileWithSameTitleAlreadyOpen`). So the drain's title-collision guard — which refuses to close any document whose title another open document shares, and names both paths — can only ever fire for PATHLESS documents, which is exactly the state identity-at-birth removes. The guard stays (a document the bridge did not create can still be pathless, and an older bridge's leftovers are), and the measurement is recorded next to it in `com.rs` so nobody "simplifies" it on the assumption that saved collisions are the common case. No MCP server measures this at all | ✗ | ✗ | ✗ | ✗ | ✗ |-| 52 | **Never ends a session the user is working in** (a measured user-presence guard on every quit and sweep) | ✗ | **✓ 0.9.9, from an event MEASURED 2026-10-01**: Drew took over a bridge-launched session — a "leave as found" quit met his File Open dialog, and minutes later the session held his 34-document crate assembly. Every quit, sweep and priming now measures presence (SOLIDWORKS foreground + input under 120 s, documents the bridge did not open, an earlier take-over), refuses `user_is_active` with the evidence, fails closed on an unreadable measurement, and marks the session taken over for good; only the user's explicit override proceeds, and the gates never pass it. No MCP server has a presence guard at all | ✗ | ✗ | ✗ | ✗ | ✗ |+| 52 | **Never ends a session the user is working in** (a measured user-presence guard on every quit and sweep) | ✗ | **✓ 0.9.9, from an event MEASURED 2026-10-01**: Drew took over a bridge-launched session — a "leave as found" quit met his File Open dialog, and minutes later the session held his 34-document crate assembly. Every quit, sweep and priming now measures presence (SOLIDWORKS foreground + input under 120 s, documents the bridge did not open, an earlier take-over), refuses `user_is_active` with the evidence, fails closed on an unreadable measurement, and marks the session taken over for good; only the user's explicit override proceeds, and the gates never pass it. 0.9.10: a document is the bridge's only if a bridge verb opened or created it — in a session the user launched, every document is theirs. No MCP server has a presence guard at all | ✗ | ✗ | ✗ | ✗ | ✗ | | 51 | **Dialogs judged by MEASURED MODALITY, not by existence** (a notification never blocks; a modal always does) | ✗ | **✓ LIVE-PROVEN 2026-10-01 on 0.9.4** — with the 3DEXPERIENCE "Connected" notice on screen, `swx_stop` quit in 5.1 s and reported it as a notification with its evidence. **0.9.4, from a blocker MEASURED 2026-10-01**: the 3DEXPERIENCE "Connected to …" notice — a title-less `#32770` tool window whose owner frame stayed ENABLED for 95 s, with no Button-class control — blocked every quit, sweep and import for minutes. Now a window blocks when its kind is known-blocking, its owner frame is disabled, or it has a Button-class control; it is a `notification` (reported with owner, ownerEnabled, styles, rect and child controls; never answered) only when all of that was measured; unread evidence fails closed. Template placeholder captions are flagged, never presented as the window's words. **0.9.5 (adversary pass 43): the window's WORDS count — a save/recovery/import kind in its text blocks whatever the evidence, two or more button-like captions in any control class block, and measured notification evidence overrides a licence-type title alone; 0.9.7: swx_dismiss_blocking_dialogs uses the same title-and-words classifier before it closes anything; 0.9.8, from a live measurement: an owner frame read disabled for an instant (SOLIDWORKS busy) is re-read before a notice is called a modal.** No MCP server looks at a dialog at all | ✗ | ✗ | ✗ | ✗ | ✗ | | 50 | **Readiness gating — attached is not ready** (no document command reaches a SOLIDWORKS that has not finished starting) | ✗ | **✓ 0.9.3, from a crash MEASURED 2026-10-01**: `swx_new_document` 8 s after COM attach, `swx_open` at 9 s and `swx_close_all_safely` at 12 s crashed SOLIDWORKS while its 3DEXPERIENCE add-in was still initialising (performance log: LOAD_ADDIN still running, SWEXH/UNHANDLED, the 3DEXPERIENCE marker only after the document commands, SWERR_REPORT); a control launch with no document commands crashed nothing. Now a READY stage follows `attached`, decided by two independent signals — COM `StartupProcessCompleted` and the performance log's 3DEXPERIENCE marker after the last LOAD_ADDIN — with an honest grace and timeout that never promote to ready; every verb that reaches SOLIDWORKS refuses `host_app_starting` with `retryAfterSeconds` until then — **0.9.5: gated by default with a pinned 16-verb exemption list, the performance log bound to this session's process creation time (an earlier session's log is never "complete"), COM alone never enough on a machine that writes markers, an on-demand add-in told from a start by quiet + which add-in, the grace unlocking reads only, and the window-message quit rungs ungated under userAuthorized; LIVE 2026-10-01 a document verb was refused during startup. 0.9.6: every gated reply carries the readiness its gate evaluated, the log is read from its per-start header, and the on-demand rule keys on the platform DLL SWK3DCompassAddin.dll; 0.9.7 (LIVE-PROVEN 0.9.6): no marker history — a disabled platform add-in never locks the bridge out, and a log still being written holds even a machine's first start**; a sweep during which SOLIDWORKS dies says `host_app_gone` with the log's crash markers. The rescue held under the crash (26,129 bytes verified). No MCP server distinguishes attached from ready at all | ✗ | ✗ | ✗ | ✗ | ✗ | | 49 | **The user's rescued work survives the bridge's own updates — and ab's** (recovery never lives in a directory anything else owns) | ✗ | **✓ 0.9.1: the data root is `%LOCALAPPDATA%\adom-solidworks-bridge\`, a directory only this bridge writes to** — moved out of `Adom Bridge\` altogether after adversary pass 39 found 0.9.0's `Adom Bridge\solidworks-data` sitting inside the directory ab's installer and uninstaller own, whose fate on an ab uninstall or reinstall is documented nowhere; both earlier locations are migrated oldest first, every copy recorded by (root, path, size, mtime, sha256) so a legacy file that changes after its copy is copied again BESIDE it and reported `changedSinceCopy`, never settled by name — and since 0.9.2 a small file whose size and mtime match is hashed too, and two versions of the ledger or the ownership registry are MERGED (union, both versions kept beside) rather than one picked; a temp-directory fallback makes every sweep refuse (`data_root_is_temp`), and an unwritable recovery folder is refused once before the first save (`recovery_dir_unwritable`). **0.9.0, from a SEV-1 MEASURED 2026-09-30**: `bridges-cache\solidworks` was found CREATED 2026-09-18 09:42 by ab's auto-install of 0.8.9 — the updater re-extracts the whole directory, and every recovery folder from the day before, the ledger and the ownership registry went with it (test documents only; nothing of Drew's was lost). The data root is now `%LOCALAPPDATA%\Adom Bridge\solidworks-data\`, a sibling of the install cache; the first start MIGRATES what an older bridge left (copied under a temporary name and renamed into place once verified byte for byte — so a start that dies mid-copy never leaves a truncated rescue under the real name — never over an existing file, originals left in place, each file once, a copy that does not verify removed and retried next start); `swx_recover_list` still reads the old location and marks each rescue there `atRiskFromUpdate`, saying whether a verified copy exists — and in the data root calls a file whose copy has not landed `notCopiedYet` (pointing at its only copy), never "gone"; and a sweep REFUSES (`recovery_inside_install_dir`, nothing written, nothing closed) if its recovery folder would land under the install directory or the install cache — however the path is spelled (links, `..`, case). Every rule mutation-verified. No MCP server keeps a recovery folder at all | ✗ | ✗ | ✗ | ✗ | ✗ |
docs/QUIT-INVESTIGATION.md+40
@@ -2151,3 +2151,43 @@ measured, its owner's enablement was not.   notice with its owner read DISABLED across the whole 0.9.8 settle window (5   reads, ~2 s). The next sweep closed everything. It cannot be measured further   today: the user is working in the session.++### 5.24 0.9.10 — the baseline holds only what the bridge put there (adversary pass 46)++The 0.9.9 guard's document signal was "foreign = open now but not in the+remembered set", and the user's own documents got INTO that set three ways —+each measured ending a session with the user's documents (2 open → 0 after+`swx_close_all_safely`):++* **B1** a session the USER started: with no baseline nothing was foreign, and+  a guard that read the user as absent wrote the user's documents in as the+  baseline — a user-launched session with their assembly open, the user reading+  its drawing in another app, was swept on the bridge's first look;+* **B2** a document opened between launch-ready and the bridge's first document+  verb was adopted by that verb's own check — the 10-01 shape exactly;+* **B3** every changing verb re-read the whole open set into the baseline, so+  everything the user opened since the previous bridge verb was adopted.++**The one rule:** the baseline may contain ONLY documents the bridge itself put+there. It starts EMPTY for every session, bridge-launched or not, and grows+only by what a bridge verb opened or created — the difference across that verb+— and loses what is no longer open. It is never re-read from the open set.+**Consequence, stated plainly:** in a session the user launched, every document+is the user's, so swx_stop, swx_shutdown, the sweeps and the drain refuse+`user_is_active` until the user explicitly asks (`userPresentOverride:true` ++`userAuthorized:true` + a reason). That is the intended reading of "never end a+session the user is working in". A harness step that sweeps the user's own+documents on their request (the live "ownership sweep") must now pass the+override explicitly; the quit and drain gates never do. The stated limit: a+document the user opens DURING a bridge verb is attributed to that verb.++Also: **B4** no foreground window (secure desktop — a UAC prompt, the lock+screen — or a disconnected session, where the last-input time is stale too) is+UNREADABLE, so present; **B5** every override ledger line records who passed it+(`caller.aiThread`, `containerName`, `source`; self-asserted, recorded, never+used to authorize); **B6** the re-check before priming is pinned with a mid-verb+fixture (`presence-after-decline.json`: the user comes back between the+declined rung and the prime — the prime is refused, the decline stands). The+adversary's process notes are kept in `tests/adversary44.rs`: the simulator adds+the user's document only when app state is read, and `swx_user_presence` itself+marks a session taken over, so tests must not call it as a probe.
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "solidworks-bridge",-  "version": "0.9.9",+  "version": "0.9.10",   "type": "app",   "description": "Skills for your container so your AI knows how to drive the SOLIDWORKS bridge (swx_* verbs). The bridge runtime itself is the release zip — a single native swx-bridge.exe; Adom Bridge loads that.",   "tags": [
page.json+1−7
@@ -22,11 +22,5 @@       "label": "Change a dimension",       "prompt": "Set the extrusion depth on my SolidWorks part to 25mm"     }-  ],-  "hero": {-    "type": "billboard",-    "auto": true,-    "headline": "solidworks-bridge",-    "subhead": "On the Adom Wiki."-  }+  ] }\ No newline at end of file
references/closing-and-recovery.md+4−1
@@ -127,7 +127,10 @@ Read this before closing any document, quitting SOLIDWORKS, draining leaked impo - **`user_is_active` — the session is the user's.** Every quit and sweep   measures presence first (SOLIDWORKS in front with recent input, documents the   bridge did not open, or an earlier take-over) and refuses with the evidence-  under `userPresence`. Leave the session alone. `swx_user_presence` reads the+  under `userPresence`. A document is the bridge's only if a bridge verb opened+  or created it — in a session the user launched, every document is theirs, so+  a quit or sweep there always needs their explicit say-so. Leave the session+  alone. `swx_user_presence` reads the   same measurement; `userPresentOverride:true` (with `userAuthorized:true` and   a reason) is only for the user's own explicit request. - **`swx_shutdown` is idempotent.** SOLIDWORKS not running answers `success:true,
src/comhost/mock.rs+19
@@ -1376,6 +1376,25 @@ impl MockBackend {                 // THE USER OPENS A DOCUMENT just as the rung is declined — the                 // moment priming must notice, because priming is never done to a                 // session somebody is working in.+                // THE USER COMES BACK just as the rung is declined (0.9.10, a+                // mid-verb fixture): `presence-after-decline.json` in the+                // simulator dir — `{"foreground": {...}, "lastInputSecs": n}` —+                // becomes the machine's presence the moment ExitApp is declined,+                // between the declined rung and the prime.+                if outcome == "declined" {+                    if let Some(d) = fake_dir() {+                        if let Ok(t) = std::fs::read_to_string(d.join("presence-after-decline.json")) {+                            if let Ok(v) = serde_json::from_str::<Value>(&t) {+                                if let Some(fg) = v.get("foreground") {+                                    let _ = std::fs::write(d.join("foreground.json"), fg.to_string());+                                }+                                if let Some(n) = v.get("lastInputSecs").and_then(|n| n.as_u64()) {+                                    let _ = std::fs::write(d.join("last-input-secs"), n.to_string());+                                }+                            }+                        }+                    }+                }                 if outcome == "declined" && fault("mock-doc-appears-after-decline") {                     let path = "C:\\Users\\drew\\work\\opened-meanwhile.SLDPRT".to_string();                     if self.find_by_name(&path).is_none() {
src/launchos.rs+12−2
@@ -1180,13 +1180,23 @@ impl LaunchOs for FakeOs {         })     } -    /// `foreground.json`: `{"hwnd","pid","title","className"}`, or absent / `{}`-    /// for "no window has the foreground". `presence-unreadable` makes both+    /// `foreground.json`: `{"hwnd","pid","title","className"}`; `{"none": true}`+    /// for "no window has the foreground"; absent = another application. `presence-unreadable` makes both     /// presence reads fail.     fn foreground_window(&self) -> Result<Option<ForegroundWindow>, String> {         if self.dir.join("presence-unreadable").exists() {             return Err("simulated: the foreground window could not be read".into());         }+        // No scenario: the user's desktop has some other application in front.+        // `{"none": true}` is "no window has the foreground" (secure desktop).+        if !self.dir.join("foreground.json").exists() {+            return Ok(Some(ForegroundWindow {+                hwnd: 1,+                pid: 1,+                title: "(simulated: another application)".into(),+                class_name: "SimulatedOtherApp".into(),+            }));+        }         let v = self.read_json("foreground.json");         let Some(hwnd) = v.get("hwnd").and_then(|h| h.as_i64()) else { return Ok(None) };         Ok(Some(ForegroundWindow {
src/presence.rs+85−41
@@ -17,9 +17,15 @@ //!   * 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);-//!   * have documents appeared that the bridge did not open — any document-//!     not in the set the bridge last looked at, which is refreshed after every-//!     verb the bridge runs that can add one;+//!   * 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@@ -143,16 +149,45 @@ fn session(ctx: &crate::verbs::Ctx) -> Option<Session> {     Some(Session { pid, created, docs, count, titles }) } -/// THE BRIDGE LOOKED: the documents open right now are known to it. Called-/// after every verb the bridge runs that can change the document set, and by a-/// presence check that finds the user absent.-pub fn note_bridge_looked(ctx: &crate::verbs::Ctx) {-    let Some(s) = session(ctx) else { return };-    let k = key(s.pid, s.created);+/// 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("baselineDocs".into(), json!(s.docs));-        o.insert("baselineCount".into(), json!(s.count));+        o.insert("bridgeDocs".into(), json!(docs));     }     put(&k, e); }@@ -219,7 +254,11 @@ pub fn measure(ctx: &crate::verbs::Ctx) -> Option<(bool, Value)> {             w.pid == s.pid || children.contains(&w.pid),             false,         ),-        Ok(None) => (Value::Null, false, 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 {@@ -234,24 +273,21 @@ pub fn measure(ctx: &crate::verbs::Ctx) -> Option<(bool, Value)> {     };     let at_keyboard = fg_is_sw && input_secs.map(|n| n < idle_secs()).unwrap_or(false); -    // FOREIGN DOCUMENTS: open now, not in the set the bridge last looked at.-    let baseline: Option<Vec<String>> = e-        .get("baselineDocs")+    // 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());-    let baseline_count = e.get("baselineCount").and_then(|v| v.as_u64());-    let foreign: Vec<String> = match &baseline {-        Some(b) => s.docs.iter().filter(|d| !b.contains(d)).cloned().collect(),-        None => Vec::new(),-    };-    let count_rose = match (baseline_count, s.count) {-        (Some(b), Some(n)) => n > b,-        _ => false,-    };+        .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();-    let foreign_seen = !foreign.is_empty() || (count_rose && s.docs.is_empty());-+    // 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;@@ -261,8 +297,10 @@ pub fn measure(ctx: &crate::verbs::Ctx) -> Option<(bool, Value)> {         "foregroundIsSolidworks": fg_is_sw,         "secondsSinceLastInput": input_secs,         "idleThresholdSecs": idle_secs(),-        "foreignDocuments": {"count": foreign.len(), "titles": foreign_titles,-                              "countRoseWithoutTheBridge": count_rose},+        "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,@@ -273,7 +311,7 @@ pub fn measure(ctx: &crate::verbs::Ctx) -> Option<(bool, Value)> {         } else if at_keyboard {             "SOLIDWORKS is the foreground window and the user gave input recently"         } else if foreign_seen {-            "documents appeared that this bridge did not open"+            "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 {@@ -283,15 +321,6 @@ pub fn measure(ctx: &crate::verbs::Ctx) -> Option<(bool, Value)> {     if positive {         mark_taken_over(s.pid, s.created, &evidence);     }-    if !present {-        // The bridge looked, and the user is not here: this is the set it knows.-        let mut e = entry(&k);-        if let Value::Object(o) = &mut e {-            o.insert("baselineDocs".into(), json!(s.docs));-            o.insert("baselineCount".into(), json!(s.count));-        }-        put(&k, e);-    }     Some((present, evidence)) } @@ -308,9 +337,22 @@ pub fn guard(ctx: &crate::verbs::Ctx, verb: &str, args: &Value) -> Result<Value,         && 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);     if overridden {+        let who = crate::caller::current();         crate::ledger::record(             "user_present_override",-            json!({"verb": verb, "reason": args.get("reason"), "evidence": evidence}),+            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 {@@ -326,7 +368,9 @@ pub fn guard(ctx: &crate::verbs::Ctx, verb: &str, args: &Value) -> Result<Value,             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. Only if the user has explicitly asked for it to be ended, pass \+         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.",     );
src/verbs.rs+11−7
@@ -3243,6 +3243,12 @@ pub fn dispatch(ctx: &Ctx, command: &str, args: &Value) -> Value {             }         }     }+    let changes_documents = is_known(command)+        && startup_touch(command) == crate::readiness::Touch::Mutate+        && !is_startup_exempt(command);+    // WHAT WAS OPEN BEFORE THE VERB — so the baseline grows only by what the+    // verb itself opened or created (0.9.10, adversary pass 46).+    let before = if changes_documents { crate::presence::snapshot(ctx) } else { None };     let mut out = dispatch_after_gate(ctx, command, args);     if let (Some(summary), Value::Object(m)) = (at_gate, &mut out) {         m.entry("readiness").or_insert(summary);@@ -3250,9 +3256,7 @@ pub fn dispatch(ctx: &Ctx, command: &str, args: &Value) -> Value {     if let (Some(evidence), Value::Object(m)) = (presence, &mut out) {         m.entry("userPresence").or_insert(evidence);     }-    if is_known(command) && startup_touch(command) == crate::readiness::Touch::Mutate-        && !is_startup_exempt(command)-    {+    if changes_documents {         // A verb that adds documents on a session the user has taken over is         // allowed — it adds, never removes — and says so.         if crate::session_history::ADDS_DOCUMENTS.contains(&command) {@@ -3262,10 +3266,10 @@ pub fn dispatch(ctx: &Ctx, command: &str, args: &Value) -> Value {                 }             }         }-        // THE BRIDGE LOOKED: whatever this verb did to the document set is the-        // bridge's own doing, so it is the new baseline. A document that-        // appears between two bridge verbs is the user's.-        crate::presence::note_bridge_looked(ctx);+        // WHAT THIS VERB OPENED OR CREATED becomes the bridge's; nothing else+        // does. A document that appears between two bridge verbs stays the+        // user's.+        crate::presence::note_bridge_changed(ctx, before);     }     out }
tests/adversary28.rs+4−1
@@ -161,7 +161,10 @@ fn a_sweep_records_its_verdicts_beside_the_files_it_wrote() {         &[("SWX_LAUNCH_FAKE_DIR", &machine.path())],     );     b.call("swx_start", json!({}));-    let swept = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    // BUILDER NOTE (0.9.10): the documents here were not opened by the bridge, so+    // the presence guard counts them as the user's; the override stands in for+    // the user's explicit request to sweep them.+    let swept = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     assert!(swept["done"] == json!(true), "precondition: the sweep ran: {}", s(&swept));      let listed = b.call("swx_recover_list", json!({}));
tests/adversary44.rsadded+232
@@ -0,0 +1,232 @@+//! ADVERSARIAL REVIEW OF 0.9.9 — pass forty-six: the user-presence guard only.+//!+//! presence.rs, the guard's call sites and FakeOs compile here and were+//! exercised. LiveOs (`GetForegroundWindow`, `GetLastInputInfo`) is+//! cfg(windows): READ, NOT RUN.+//!+//! WHAT CHECKS OUT+//!  * (3) takenOverByUser lives in `presence.json` in the DATA ROOT, keyed+//!    pid:creationTime: it survives a com-host respawn and a bridge update, and+//!    a new session (new creation time) does not inherit it. With the creation+//!    time unknown the key is `pid:?`, which errs present. Never cleared, but+//!    keyed, and pruned at 20 entries — harmless.+//!  * (4) The override needs userPresentOverride:true AND userAuthorized:true+//!    AND a non-empty reason; userAuthorized alone is not enough. None of the+//!    three is underscore-prefixed, so the dispatcher's stripping neither+//!    removes nor supplies them, and nothing defaults them true. The ledger+//!    line records the verb, the reason and the evidence — not WHO; there is no+//!    caller identity in it.+//!  * (6) Read, not run: a NULL foreground window becomes `Ok(None)` —+//!    "readable, and not SOLIDWORKS" — rather than unknown. Windows returns NULL+//!    on the secure desktop (a UAC prompt, the lock screen) and in a service or+//!    disconnected session, where GetLastInputInfo also reports that session's+//!    stale input. So a bridge in those contexts reads the user as ABSENT. Under+//!    a UAC prompt the user is certainly present. Treat a NULL foreground as+//!    unreadable (which already counts as present).+//!  * (5) The pre-priming re-check cannot be pinned through the verb surface:+//!    the guard runs before the ladder, so any presence evidence planted before+//!    the call refuses there, and FakeOs has no hook to change foreground or+//!    input DURING a verb. For the document case it is also redundant: the+//!    prime's own `documentCount == 0` check (mutant MH, pass 45: caught)+//!    refuses a session in which a document appeared. Its unique case — the+//!    user brings SOLIDWORKS forward between the declined rung and the prime,+//!    with no document — needs a mid-verb fixture.+//!+//! THE FINDINGS are both in the FALSE-ABSENT direction: the starting-set rule.++mod common;+use common::*;+use serde_json::{json, Value};++fn s(v: &Value) -> String {+    serde_json::to_string_pretty(v).unwrap_or_default()+}++struct Sim {+    dir: TempDir,+}++impl Sim {+    fn new(tag: &str) -> Sim {+        let dir = TempDir::new(tag);+        std::fs::write(dir.join("quit-script.json"), json!({"a": "exited"}).to_string()).unwrap();+        std::fs::write(+            dir.join("sldworks.json"),+            json!({"responding": true, "hwnd": 6622700, "privateMB": 900, "handles": 4000}).to_string(),+        )+        .unwrap();+        std::fs::write(dir.join("windows.json"), "[]").unwrap();+        Sim { dir }+    }++    fn boot_with(&self, scratch: TempDir) -> Bridge {+        let b = Bridge::start_in(+            scratch,+            &[+                ("SWX_LAUNCH_FAKE_DIR", Box::leak(self.dir.path().into_boxed_str())),+                ("SWX_ALLOW_USER_WRITES", "1"),+                ("SWX_QUIT_SETTLE_MS", "80"),+                ("SWX_QUIT_EXIT_WAIT_SECS", "3"),+            ],+        );+        b.call("swx_start", json!({}));+        b+    }++    fn user_busy_in_another_app(&self) {+        std::fs::write(+            self.dir.join("foreground.json"),+            json!({"hwnd": 777, "pid": 4321, "title": "Drawing ASM-CRATE.pdf - Acrobat", "className": "AcrobatSDIWindow"})+                .to_string(),+        )+        .unwrap();+        std::fs::write(self.dir.join("last-input-secs"), "5").unwrap();+    }+}++/// SEVERITY 2 — A SESSION THE USER STARTED, WITH THE USER'S DOCUMENTS OPEN, IS+/// SWEPT THE FIRST TIME THE BRIDGE LOOKS AWAY.+///+/// The guard has two positive signals: SOLIDWORKS foreground with input in the+/// last 120 s, or documents that were not in the set the bridge last looked+/// at. The second needs a baseline, and with NO baseline `foreign` is empty —+/// "on the bridge's first look at a session whatever is open is accepted as the+/// starting set". Then `!present` writes the user's documents in AS that set.+///+/// For a session the USER launched, every document in it is the user's by+/// definition. The guard does not consult launch attribution at all, so the+/// whole decision for "user-launched session, user's assembly open, user reading+/// a PDF of its drawing in another window" is:+///   (a) foreground is Acrobat -> not at keyboard;+///   (b) no baseline -> nothing foreign;+///   (c) never taken over;+///   -> ABSENT -> swx_close_all_safely proceeds: preserves, then CLOSES the+///   user's documents.+/// Nothing is lost — the sweep saves first, and swx_stop would still refuse on+/// open documents / unsaved work — which is why this is sev-2. But it is the+/// exact harm this guard was built after on 10-01: ending a person's working+/// session while they are working in it, avoided that day by luck of timing.+///+/// Fix: a session this bridge did not launch (attribution says so) starts with+/// an EMPTY baseline — everything open in it is the user's. Only a bridge-+/// launched session may adopt a starting set, and only at launch.+#[test]+fn the_users_own_session_is_not_swept_because_the_bridge_had_not_looked_before() {+    let m = Sim::new("adv44-user-session");+    let scratch = TempDir::new("adv44-user-session-scratch");+    std::fs::write(+        scratch.join("mock-solidworks-open-docs.json"),+        json!({"running": true, "untitled": 0, "active": "C:\\Users\\Public\\adom-crate\\ASM-CRATE.SLDASM",+               "docs": [+                   {"docType": "assembly", "path": "C:\\Users\\Public\\adom-crate\\ASM-CRATE.SLDASM",+                    "title": "ASM-CRATE.SLDASM", "saved": true, "dirty": false},+                   {"docType": "part", "path": "C:\\Users\\Public\\adom-crate\\PANEL-SIDE.SLDPRT",+                    "title": "PANEL-SIDE.SLDPRT", "saved": true, "dirty": false}+               ],+               "files": {}}).to_string(),+    )+    .unwrap();+    let b = m.boot_with(scratch);+    m.user_busy_in_another_app();+    let before = b.call("swx_docs", json!({}));+    assert!(before["count"].as_u64().unwrap_or(0) == 2, "precondition: the user's two documents are open: {}", s(&before));++    let r = b.call("swx_close_all_safely", json!({"reason": "leave the machine as found"}));+    let after = b.call("swx_docs", json!({}));+    assert!(+        r["errorCode"] == json!("user_is_active"),+        "the user's own session — their assembly and a part open, the user reading the drawing in \+         another window five seconds ago — was judged ABSENT and swept: with no baseline, nothing \+         is foreign, and the guard never asks whether the bridge launched this session. Documents \+         before: {}, after: {}. Reply: {}",+        before["count"],+        after["count"],+        s(&r)+    );+}++/// SEVERITY 2 — ON A BRIDGE-LAUNCHED SESSION, A DOCUMENT THE USER OPENS BEFORE+/// THE BRIDGE'S FIRST DOCUMENT VERB IS ADOPTED AS THE BRIDGE'S STARTING SET.+///+/// `note_bridge_looked` runs after bridge verbs that can change documents.+/// Launch-and-attach (swx_start) is not one of them, so the first baseline is+/// written by the first guarded verb's own measurement — and it adopts whatever+/// is open, including what the user opened in the window between launch-ready+/// and that verb. That is exactly the 10-01 shape: a bridge-launched empty+/// session, the user starts working in it, the harness's "leave the machine as+/// found" verb arrives. Measured here with the user in another app: the user's+/// assembly is swept.+#[test]+fn a_document_opened_between_launch_and_the_first_bridge_verb_is_the_users() {+    let m = Sim::new("adv44-launch-gap");+    let b = m.boot_with(TempDir::new("adv44-launch-gap-scratch"));+    m.user_busy_in_another_app();+    // The user opens their assembly in the bridge-launched session.+    std::fs::write(b.scratch.join("mock-user-opens-document"), "").unwrap();+    // The simulator adds the user's document on an app_state read.+    let _ = b.call("swx_get_app_state", json!({}));+    let before = b.call("swx_docs", json!({}));+    assert!(s(&before).contains("ASM-CRATE"), "precondition: the user's assembly is open: {}", s(&before));+    let _ = std::fs::remove_file(b.scratch.join("mock-user-opens-document"));++    let r = b.call("swx_close_all_safely", json!({"reason": "leave the machine as found"}));+    let after = b.call("swx_docs", json!({}));+    assert!(+        r["errorCode"] == json!("user_is_active"),+        "SOLIDWORKS was launched by the bridge and empty; the user opened ASM-CRATE.SLDASM before \+         the bridge's first document verb. The first guarded verb took its baseline from what was \+         open — the user's assembly — so nothing was foreign, and the sweep proceeded. Take the \+         baseline at launch-ready (empty for a bridge-launched session). After: {}. Reply: {}",+        s(&after),+        s(&r)+    );+}++/// SEVERITY 2 — ANY MUTATING BRIDGE VERB LAUNDERS THE USER'S DOCUMENTS INTO THE+/// BASELINE.+///+/// After every mutating, non-exempt verb, verbs.rs:3268 calls+/// `note_bridge_looked`, which sets the baseline to EVERYTHING open now. The+/// stated limit is "a document the user opens DURING a bridge verb cannot be+/// attributed". The real window is wider: everything the user opened SINCE THE+/// PREVIOUS bridge verb is adopted by the next one, whatever that verb was. So+/// the 10-01 sequence with one extra step defeats the guard:+///   1. the bridge makes a part (baseline = {part});+///   2. the user opens ASM-CRATE.SLDASM (foreign — the guard would see it);+///   3. the bridge makes another part (baseline := {part, part, ASM-CRATE});+///   4. "leave the machine as found": the user is reading in another window,+///      nothing is foreign -> the sweep closes their assembly.+/// The refresh should ADD what the verb itself changed (the documents it+/// created or opened, by their reply), not re-read the world.+#[test]+fn a_later_bridge_verb_does_not_adopt_the_users_document_as_its_own() {+    let m = Sim::new("adv44-launder");+    let b = m.boot_with(TempDir::new("adv44-launder-scratch"));+    let mine = b.call("swx_new_document", json!({"type": "part"}));+    assert!(mine["success"] == json!(true), "precondition: the bridge made a part: {}", s(&mine));+    std::fs::write(b.scratch.join("mock-user-opens-document"), "").unwrap();+    let _ = b.call("swx_get_app_state", json!({}));+    let _ = std::fs::remove_file(b.scratch.join("mock-user-opens-document"));+    // NOT swx_user_presence here: measuring presence marks the session+    // taken-over (sticky) when it sees the foreign document, which would make+    // the sweep below refuse for that reason instead. (Measured: at this point+    // the guard does report ASM-CRATE.SLDASM as foreign, count 1.) A harness+    // that does not happen to ask in between gets no such protection.+    let open_now = b.call("swx_docs", json!({}));+    assert!(s(&open_now).contains("ASM-CRATE"), "precondition: the user's assembly is open: {}", s(&open_now));+    let mine2 = b.call("swx_new_document", json!({"type": "part"}));+    assert!(mine2["success"] == json!(true), "precondition: the bridge made another part: {}", s(&mine2));+    m.user_busy_in_another_app();++    let r = b.call("swx_close_all_safely", json!({"reason": "leave the machine as found"}));+    let after = b.call("swx_docs", json!({}));+    assert!(+        r["errorCode"] == json!("user_is_active"),+        "the guard saw the user's ASM-CRATE.SLDASM as foreign, then the bridge ran ONE more \+         document verb of its own and the baseline was re-read from everything open — adopting the \+         user's assembly. The sweep then closed it. Refresh the baseline with what the verb \+         changed, not with the world. After: {}. Reply: {}",+        s(&after),+        s(&r)+    );+}
tests/data_root.rs+12−6
@@ -21,6 +21,12 @@ //!   * a sweep REFUSES to place recovery under an install location, however the //!     path is spelled — and closes nothing when it refuses. +//+// 0.9.10: the documents these sessions are seeded with were not opened by the+// bridge, so the user-presence guard counts them as the USER's (adversary pass+// 46: the baseline may contain only documents the bridge put there). Every+// sweep below passes `userPresentOverride` + `userAuthorized`, standing in for+// the user's explicit request — which is what these sweeps model. mod common;  use common::{Bridge, TempDir};@@ -169,7 +175,7 @@ fn recovery_is_written_to_the_data_root_never_into_the_install_cache() {         ("SWX_LAUNCH_FAKE_DIR", &machine.path()),     ]);     b.call("swx_start", json!({}));-    let r = b.call("swx_close_all_safely", json!({"reason": "the user asked me to close SOLIDWORKS"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "the user asked me to close SOLIDWORKS"}));     check!(b, "swx_close_all_safely: the sweep runs in the default layout", r["success"] == true && r["done"] == true, s(&r));      let dir = PathBuf::from(r["recoveryDir"].as_str().unwrap_or(""));@@ -531,7 +537,7 @@ fn a_sweep_refuses_to_place_recovery_under_an_install_location() {         ]);         b.call("swx_start", json!({}));         let before = b.call("swx_docs", json!({}));-        let r = b.call("swx_close_all_safely", json!({"reason": "the user asked me to close SOLIDWORKS"}));+        let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "the user asked me to close SOLIDWORKS"}));         check!(             b,             &format!("swx_close_all_safely: refuses a recovery folder {label}"),@@ -570,7 +576,7 @@ fn a_sweep_refuses_to_place_recovery_under_an_install_location() {         ("SWX_LAUNCH_FAKE_DIR", &machine.path()),     ]);     b.call("swx_start", json!({}));-    let r = b.call("swx_close_all_safely", json!({"reason": "the user asked me to close SOLIDWORKS"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "the user asked me to close SOLIDWORKS"}));     check!(         b,         "swx_close_all_safely: a data root beside the install directory is not refused",@@ -1322,7 +1328,7 @@ fn with_no_profile_location_every_sweep_refuses_rather_than_write_to_temp() {     );     b.call("swx_start", json!({}));     let before = b.call("swx_docs", json!({}));-    let r = b.call("swx_close_all_safely", json!({"reason": "the user asked me to close SOLIDWORKS"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "the user asked me to close SOLIDWORKS"}));     check!(         b,         "swx_close_all_safely: REFUSES to put the only copy of unsaved work in the temp directory",@@ -1355,7 +1361,7 @@ fn with_no_profile_location_every_sweep_refuses_rather_than_write_to_temp() {         ("SWX_LAUNCH_FAKE_DIR", &machine.path()),     ]);     b.call("swx_start", json!({}));-    let r = b.call("swx_close_all_safely", json!({"reason": "the user asked me to close SOLIDWORKS"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "the user asked me to close SOLIDWORKS"}));     check!(         b,         "swx_close_all_safely: with USERPROFILE the root is the profile's, and the sweep runs there",@@ -1508,7 +1514,7 @@ fn a_recovery_folder_that_cannot_be_made_is_refused_once_by_name() {     let machine = a_visible_machine("dataroot-unwritable-machine");     let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[("SWX_LAUNCH_FAKE_DIR", &machine.path())]);     b.call("swx_start", json!({}));-    let r = b.call("swx_close_all_safely", json!({"reason": "the user asked me to close SOLIDWORKS"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "the user asked me to close SOLIDWORKS"}));     check!(         b,         "swx_close_all_safely: refuses with recovery_dir_unwritable, naming the cause once",
tests/presence.rs+86
@@ -215,3 +215,89 @@ fn an_unreadable_presence_is_presence_but_never_a_take_over() {         s(&r)     ); }++/// NO FOREGROUND WINDOW IS UNREADABLE, NOT ABSENT (0.9.10, adversary pass 46+/// B4): Windows returns NULL on the secure desktop (a UAC prompt, the lock+/// screen) and in a disconnected session — under a UAC prompt the user is+/// certainly there.+#[test]+fn no_foreground_window_counts_as_present() {+    let m = Sim::new("presence-no-foreground");+    let b = m.boot();+    std::fs::write(m.dir.join("foreground.json"), json!({"none": true}).to_string()).unwrap();+    m.last_input(3600);+    let r = b.call("swx_stop", authorized("a"));+    check!(+        b,+        "swx_stop: no window has the foreground — unreadable, refused as present",+        r["errorCode"] == "user_is_active" && r["userPresence"]["measurementUnreadable"] == true,+        s(&r)+    );+    check!(b, "...and SOLIDWORKS was not quit", !m.quit_happened(), String::new());+}++/// THE PRE-PRIMING RE-CHECK (0.9.10, B6), pinned with a mid-verb fixture: the+/// front gate finds the user absent, rung (a) is declined, and in that moment+/// the user brings SOLIDWORKS forward. The prime — which would create and close a+/// document in their session — is refused, and the decline stands.+#[test]+fn a_user_who_comes_back_during_the_ladder_stops_the_prime() {+    let m = Sim::new("presence-mid-ladder");+    std::fs::write(m.dir.join("quit-script.json"), json!({"a": "declined"}).to_string()).unwrap();+    let b = m.boot();+    let pid = b.call("swx_get_app_state", json!({}))["processId"].as_u64().unwrap();+    std::fs::write(+        m.dir.join("presence-after-decline.json"),+        json!({"foreground": {"hwnd": 6622700, "pid": pid, "title": "SOLIDWORKS Premium 2026",+                              "className": "Afx:00007FF6B0000000:8"},+               "lastInputSecs": 3})+        .to_string(),+    )+    .unwrap();+    let r = b.call("swx_stop", authorized("a"));+    let tries = r["rungsTried"].as_array().cloned().unwrap_or_default();+    check!(+        b,+        "swx_stop: the front gate let it through (the user was away), rung (a) declined, NOT primed",+        r["stopped"] == false+            && tries.len() == 1+            && tries[0]["detail"].as_str().unwrap_or("").contains("not primed")+            && tries[0]["detail"].as_str().unwrap_or("").contains("user is working"),+        s(&r)+    );+    let ledger = b.call("swx_action_ledger", json!({"limit": 200}));+    check!(+        b,+        "...and nothing was created to prime it",+        !ledger["actions"].as_array().map(|a| a.iter().any(|e| e["action"] == "quit_prime")).unwrap_or(true),+        s(&ledger)+    );+}++/// WHO PASSED THE OVERRIDE is on the ledger line (0.9.10, B5) — self-asserted,+/// like the reason: recorded, never used to authorize.+#[test]+fn the_override_ledger_line_names_the_caller() {+    let m = Sim::new("presence-override-caller");+    let b = m.boot();+    m.foreground_solidworks(&b);+    m.last_input(1);+    let mut over = authorized("a");+    over["userPresentOverride"] = json!(true);+    over["caller"] = json!({"aiThread": "drew-live-harness", "containerName": "adom-drew", "source": "cli"});+    let r = b.call("swx_stop", over);+    check!(b, "precondition: the override proceeded", r["stopped"] == true, s(&r));+    let ledger = b.call("swx_action_ledger", json!({"limit": 200}));+    let line = ledger["actions"]+        .as_array()+        .and_then(|a| a.iter().find(|e| e["action"] == "user_present_override").cloned())+        .unwrap_or(Value::Null);+    check!(+        b,+        "the user_present_override line records the caller's identity",+        line["detail"]["caller"]["aiThread"] == "drew-live-harness"+            && line["detail"]["caller"]["containerName"] == "adom-drew"+            && line["detail"]["caller"]["source"] == "cli",+        s(&line)+    );+}
tests/quit.rs+5−4
@@ -536,10 +536,11 @@ fn the_session_changing_between_rungs_is_caught_by_the_per_rung_sweep() {     // THE SEQUENCE, counted from the start of the `swx_stop` call (the cursor is     // reset immediately before it, below):     //   read 1  `app_state` — the USER-PRESENCE guard (0.9.9), before the verb-    //   read 2  `status`    — the "is anything running" check-    //   read 3  `app_state` — the FRONT GATE sweep: still the claimed session-    //   read 4  `app_state` — the PER-RUNG sweep before rung (b): a different one-    std::fs::write(scratch.join("mock-com-pid"), "700900 700900 700900 700901").unwrap();+    //   read 2  `app_state` — the document snapshot before a changing verb (0.9.10)+    //   read 3  `status`    — the "is anything running" check+    //   read 4  `app_state` — the FRONT GATE sweep: still the claimed session+    //   read 5  `app_state` — the PER-RUNG sweep before rung (b): a different one+    std::fs::write(scratch.join("mock-com-pid"), "700900 700900 700900 700900 700901").unwrap();      let b = Bridge::start_in(         TempDir::borrowed(&scratch.0),
tests/readiness.rs+3−3
@@ -237,7 +237,7 @@ fn a_sweep_during_which_solidworks_dies_says_host_app_gone() {     );     b.call("swx_start", json!({}));     std::fs::write(scratch.join("mock-crash-after-first-save"), "").unwrap();-    let r = b.call("swx_close_all_safely", json!({"reason": "the user asked me to close SOLIDWORKS"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "the user asked me to close SOLIDWORKS"}));     let evidence: Vec<String> = r["crashEvidence"]         .as_array()         .map(|a| a.iter().filter_map(|x| x.as_str().map(|s| s.to_string())).collect())@@ -318,7 +318,7 @@ fn a_notification_does_not_stop_a_sweep_and_is_reported() {     .unwrap();     let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[("SWX_LAUNCH_FAKE_DIR", &machine.path())]);     b.call("swx_start", json!({}));-    let r = b.call("swx_close_all_safely", json!({"reason": "the user asked me to close SOLIDWORKS"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "the user asked me to close SOLIDWORKS"}));     check!(         b,         "swx_close_all_safely: the notification does not stop the sweep",@@ -813,7 +813,7 @@ fn sweep_with_notice(tag: &str, owner_reads: Value, extra_controls: Value) -> (B         &[("SWX_LAUNCH_FAKE_DIR", &machine.path()), ("SWX_OWNER_SETTLE_MS", "30")],     );     b.call("swx_start", json!({}));-    let r = b.call("swx_close_all_safely", json!({"reason": "the user asked me to close SOLIDWORKS"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "the user asked me to close SOLIDWORKS"}));     (b, r, scratch, machine) } 
tests/shutdown.rs+41−35
@@ -9,6 +9,12 @@ //! //!   **nothing closes before it is on disk.** +//+// 0.9.10: the documents these sessions are seeded with were not opened by the+// bridge, so the user-presence guard counts them as the USER's (adversary pass+// 46: the baseline may contain only documents the bridge put there). Every+// sweep below passes `userPresentOverride` + `userAuthorized`, standing in for+// the user's explicit request — which is what these sweeps model. mod common;  use common::{Bridge, TempDir};@@ -77,7 +83,7 @@ fn every_document_is_on_disk_before_anything_closes() {      // DEFAULT POSTURE. No write opt-in, no per-document confirmation: the user     // has already said this may happen. Only a reason.-    let r = b.call("swx_close_all_safely", json!({"reason": "the user asked me to close SOLIDWORKS"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "the user asked me to close SOLIDWORKS"}));     check!(b, "the sweep succeeds in the default posture", r["success"] == true, s(&r));     check!(b, "...and finishes", r["done"] == true, s(&r)); @@ -147,7 +153,7 @@ fn a_document_that_cannot_be_saved_is_not_closed() {     b.call("swx_start", json!({}));     std::fs::write(b.scratch.join("mock-saveas-fails"), "").unwrap(); -    let r = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     check!(b, "the sweep does NOT report itself done", r["done"] != true, s(&r));     let remaining = r["remaining"].as_array().cloned().unwrap_or_default();     check!(b, "...and names what it could not preserve", !remaining.is_empty(), s(&r));@@ -183,7 +189,7 @@ fn a_dialog_stops_the_sweep_and_is_never_answered() {     );     b.call("swx_start", json!({})); -    let r = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     check!(         b,         "the sweep STOPS on a dialog",@@ -227,14 +233,14 @@ fn swx_shutdown_preserves_everything_then_quits() {      // NO write opt-in and NO launch attribution: the user's authorization is what     // makes this allowed, and this bridge did not launch the session.-    let r = b.call("swx_shutdown", json!({"reason": "the user asked me to close SOLIDWORKS"}));+    let r = b.call("swx_shutdown", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "the user asked me to close SOLIDWORKS"}));     check!(b, "the shutdown succeeds", r["success"] == true, s(&r));     check!(b, "...having preserved every document", r["recovered"].as_array().map(|a| a.len() >= 4).unwrap_or(false), s(&r));     check!(b, "...emptied the session", r["done"] == true, s(&r));     check!(b, "...and quit SOLIDWORKS", r["stopped"] == true, s(&r));      // IDEMPOTENT: the last thing a caller does should be safe to repeat.-    let again = b.call("swx_shutdown", json!({"reason": "making sure"}));+    let again = b.call("swx_shutdown", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "making sure"}));     check!(         b,         "a second shutdown is a no-op, not a failure",@@ -255,7 +261,7 @@ fn swx_recover_list_finds_what_was_preserved() {         &[("SWX_LAUNCH_FAKE_DIR", &machine.path())],     );     b.call("swx_start", json!({}));-    let swept = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    let swept = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     check!(b, "precondition: the sweep ran", swept["done"] == true, s(&swept));      let r = b.call("swx_recover_list", json!({}));@@ -286,7 +292,7 @@ fn an_unreadable_dialog_census_preserves_but_closes_nothing() {     let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[]);     b.call("swx_start", json!({})); -    let r = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     check!(         b,         "a census nobody can read stops the closes",@@ -362,7 +368,7 @@ fn the_users_original_file_is_untouched_and_the_document_re_points() {     );     b.call("swx_start", json!({})); -    let r = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     check!(b, "the sweep finished", r["success"] == true && r["done"] == true, s(&r));     let rec = r["recovered"].as_array().cloned().unwrap_or_default();     let theirs_rec = rec.iter().find(|d| d["title"] == "bracket.SLDPRT").cloned().unwrap_or(json!({}));@@ -431,7 +437,7 @@ fn no_component_closes_before_its_own_recovery_write() {     );     b.call("swx_start", json!({})); -    let r = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     check!(b, "the sweep finished", r["success"] == true && r["done"] == true, s(&r));     let rec = r["recovered"].as_array().cloned().unwrap_or_default();     check!(b, "all three documents were preserved", rec.len() == 3, s(&r));@@ -515,7 +521,7 @@ fn a_zero_byte_recovery_file_does_not_authorise_a_close() {     // The disk fills: SaveAs3 "succeeds", the file appears, and it is empty.     std::fs::write(b.scratch.join("mock-recovery-partial-write"), "").unwrap(); -    let r = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     check!(b, "the sweep does not report itself done", r["done"] != true, s(&r));     let rec = r["recovered"].as_array().cloned().unwrap_or_default();     let short: Vec<&Value> = rec.iter().filter(|d| d["preserved"] == false).collect();@@ -560,7 +566,7 @@ fn a_truncated_recovery_file_does_not_authorise_a_close() {     // A real header, and then the disk fills.     std::fs::write(b.scratch.join("mock-recovery-truncated"), "").unwrap(); -    let r = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     check!(b, "the sweep does not report itself done", r["done"] != true, s(&r));     let rec = r["recovered"].as_array().cloned().unwrap_or_default();     let short: Vec<&Value> = rec.iter().filter(|d| d["preserved"] == false).collect();@@ -605,7 +611,7 @@ fn a_recovery_file_with_the_wrong_signature_does_not_authorise_a_close() {     // Full size, wrong container.     std::fs::write(b.scratch.join("mock-recovery-bad-header"), "").unwrap(); -    let r = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     check!(b, "the sweep does not report itself done", r["done"] != true, s(&r));     let rec = r["recovered"].as_array().cloned().unwrap_or_default();     let bad: Vec<&Value> = rec.iter().filter(|d| d["preserved"] == false).collect();@@ -654,7 +660,7 @@ fn a_dialog_raised_by_a_save_stops_the_closes() {     // The FIRST save puts a prompt on screen.     std::fs::write(b.scratch.join("mock-dialog-after-save"), "").unwrap(); -    let r = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     check!(         b,         "the sweep STOPS on a dialog a SAVE raised",@@ -707,7 +713,7 @@ fn a_dialog_raised_by_a_close_stops_the_remaining_closes() {     check!(b, "precondition: a full session", n >= 4, s(&before));     std::fs::write(b.scratch.join("mock-dialog-during-close"), "").unwrap(); -    let r = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     check!(         b,         "the sweep STOPS on a dialog a CLOSE raised",@@ -865,7 +871,7 @@ fn a_legacy_compound_document_is_preserved_and_closed() {     b.call("swx_start", json!({}));     std::fs::write(b.scratch.join("mock-recovery-legacy-container"), "").unwrap(); -    let r = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     check!(b, "the sweep finished", r["success"] == true && r["done"] == true, s(&r));     let rec = r["recovered"].as_array().cloned().unwrap_or_default();     check!(@@ -909,7 +915,7 @@ fn a_sweep_that_cannot_fit_refuses_to_start() {     let n = before["count"].as_u64().unwrap_or(0);     check!(b, "precondition: a full session", n >= 4, s(&before)); -    let r = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     check!(         b,         "the sweep refuses to begin",@@ -976,7 +982,7 @@ fn a_disk_that_fills_mid_sweep_stops_the_sweep() {     let before = b.call("swx_docs", json!({}));     let n = before["count"].as_u64().unwrap_or(0); -    let r = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     check!(         b,         "the sweep stops when the volume fills under it",@@ -1024,7 +1030,7 @@ fn a_successful_sweep_still_reports_the_room_it_had() {         &[("SWX_LAUNCH_FAKE_DIR", &machine.path())],     );     b.call("swx_start", json!({}));-    let r = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     check!(b, "the sweep finished", r["success"] == true && r["done"] == true, s(&r));     check!(         b,@@ -1079,7 +1085,7 @@ fn an_assemblys_type_decides_both_its_recovery_name_and_its_floor() {     );     b.call("swx_start", json!({})); -    let r = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     check!(b, "the sweep finished", r["success"] == true && r["done"] == true, s(&r));     let rec = r["recovered"].as_array().cloned().unwrap_or_default();     let d = rec.first().cloned().unwrap_or(json!({}));@@ -1135,7 +1141,7 @@ fn an_unreadable_type_falls_back_to_the_documents_own_path() {     b.call("swx_start", json!({}));     std::fs::write(b.scratch.join("mock-doctype-unreadable"), "").unwrap(); -    let r = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     let rec = r["recovered"].as_array().cloned().unwrap_or_default();     let d = rec.first().cloned().unwrap_or(json!({}));     check!(b, "the document was preserved", d["preserved"] == true, s(&r));@@ -1194,7 +1200,7 @@ fn a_document_nothing_can_classify_is_named_as_a_part_but_never_called_one() {     b.call("swx_start", json!({}));     std::fs::write(b.scratch.join("mock-doctype-unreadable"), "").unwrap(); -    let r = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     let rec = r["recovered"].as_array().cloned().unwrap_or_default();     let d = rec.first().cloned().unwrap_or(json!({}));     check!(b, "the kind is reported as unknown, not invented", d["docKind"].is_null(), s(&r));@@ -1247,7 +1253,7 @@ fn a_neutral_name_is_used_where_solidworks_accepts_one() {     std::fs::write(b.scratch.join("mock-doctype-unreadable"), "").unwrap();     std::fs::write(b.scratch.join("mock-neutral-extension-accepted"), "").unwrap(); -    let r = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     let d = r["recovered"].as_array().and_then(|a| a.first()).cloned().unwrap_or(json!({}));     check!(         b,@@ -1294,7 +1300,7 @@ fn a_refused_neutral_name_falls_back_to_a_part_and_says_so() {     std::fs::write(b.scratch.join("mock-doctype-unreadable"), "").unwrap();     std::fs::write(b.scratch.join("mock-saveas-rejects-unknown-extension"), "").unwrap(); -    let r = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     let rec = r["recovered"].as_array().cloned().unwrap_or_default();     let d = rec.first().cloned().unwrap_or(json!({}));     check!(b, "the document was still preserved", d["preserved"] == true, s(&r));@@ -1355,7 +1361,7 @@ fn recover_list_tells_each_file_how_to_be_reopened() {     );     b.call("swx_start", json!({}));     std::fs::write(b.scratch.join("mock-doctype-unreadable"), "").unwrap();-    let swept = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    let swept = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     check!(b, "precondition: the sweep preserved both", swept["recovered"].as_array().map(|a| a.len()) == Some(2), s(&swept));      let r = b.call("swx_recover_list", json!({}));@@ -1516,7 +1522,7 @@ fn the_listing_reports_the_sweeps_own_verdicts_from_its_manifest() {         &[("SWX_LAUNCH_FAKE_DIR", &machine.path())],     );     b.call("swx_start", json!({}));-    let swept = b.call("swx_close_all_safely", json!({"reason": "the user asked me to close"}));+    let swept = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "the user asked me to close"}));     check!(b, "precondition: the sweep ran", swept["done"] == true, s(&swept));      let r = b.call("swx_recover_list", json!({}));@@ -1582,7 +1588,7 @@ fn an_interrupted_sweep_still_leaves_a_record_of_what_it_decided() {     // an interruption would leave it.     std::fs::write(b.scratch.join("mock-dialog-after-save"), "").unwrap(); -    let r = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     check!(b, "precondition: the sweep stopped part-way", r["done"] != true, s(&r));      let listed = b.call("swx_recover_list", json!({}));@@ -1638,7 +1644,7 @@ fn two_sweeps_in_the_same_second_keep_separate_records() {     b.call("swx_start", json!({}));      // FIRST SWEEP: preserves and closes everything.-    let first = b.call("swx_close_all_safely", json!({"reason": "first sweep"}));+    let first = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "first sweep"}));     check!(b, "the first sweep ran", first["done"] == true, s(&first));     let first_dir = first["recoveryDir"].as_str().unwrap_or("").to_string();     let first_manifest = std::path::Path::new(&first_dir).join("manifest.json");@@ -1647,7 +1653,7 @@ fn two_sweeps_in_the_same_second_keep_separate_records() {      // SECOND SWEEP, IMMEDIATELY: the session is empty, so it preserves nothing —     // the case whose cleanup used to delete somebody else's record.-    let second = b.call("swx_close_all_safely", json!({"reason": "second sweep"}));+    let second = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "second sweep"}));     let second_dir = second["recoveryDir"].as_str().unwrap_or("").to_string();     check!(         b,@@ -1718,7 +1724,7 @@ fn the_record_is_written_atomically() {         &[("SWX_LAUNCH_FAKE_DIR", &machine.path())],     );     b.call("swx_start", json!({}));-    let r = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     let dir = r["recoveryDir"].as_str().unwrap_or("").to_string();     check!(         b,@@ -1772,7 +1778,7 @@ fn a_blocking_dialog_reports_its_text_and_its_buttons() {     );     b.call("swx_start", json!({})); -    let r = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     check!(b, "the sweep stops", r["errorCode"] == "stopped_by_dialog", s(&r));     let d = r["stoppedByDialog"].clone();     check!(b, "the dialog is classified", d["kind"] == "license", s(&d));@@ -1813,7 +1819,7 @@ fn a_blocking_dialog_reports_its_text_and_its_buttons() {         &[("SWX_LAUNCH_FAKE_DIR", &machine2.path())],     );     b2.call("swx_start", json!({}));-    let r2 = b2.call("swx_close_all_safely", json!({"reason": "closing"}));+    let r2 = b2.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     let d2 = r2["stoppedByDialog"].clone();     check!(         b2,@@ -1850,7 +1856,7 @@ fn a_dialog_whose_controls_cannot_be_read_says_so() {     );     b.call("swx_start", json!({})); -    let r = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     let d = r["stoppedByDialog"].clone();     check!(b, "the sweep still stops", r["errorCode"] == "stopped_by_dialog", s(&r));     check!(b, "the contents are UNKNOWN, not empty", d["contentsReadable"] == false, s(&d));@@ -1886,7 +1892,7 @@ fn a_refused_document_is_recorded_as_refused_in_the_manifest() {     // these and leaves the documents open.     std::fs::write(b.scratch.join("mock-recovery-truncated"), "").unwrap(); -    let r = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     check!(b, "the sweep did not finish", r["done"] != true, s(&r));     let rec = r["recovered"].as_array().cloned().unwrap_or_default();     let refused: Vec<&Value> = rec.iter().filter(|d| d["preserved"] == false).collect();@@ -1966,7 +1972,7 @@ fn a_host_failure_during_a_sweep_carries_its_structured_reason() {         &[("SWX_LAUNCH_FAKE_DIR", &machine.path())],     ); -    let r = b.call("swx_close_all_safely", json!({"reason": "closing"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "closing"}));     let failed = r["closeFailed"].as_array().cloned().unwrap_or_default();     check!(b, "the host failed the sweep", !failed.is_empty(), s(&r));     let e = failed.first().cloned().unwrap_or(json!({}));@@ -2051,7 +2057,7 @@ fn a_clean_document_is_recorded_as_closed_in_place_and_listed_untouched() {         &[("SWX_LAUNCH_FAKE_DIR", &machine.path())],     );     b.call("swx_start", json!({}));-    let r = b.call("swx_close_all_safely", json!({"reason": "the user asked me to close SOLIDWORKS"}));+    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "the user asked me to close SOLIDWORKS"}));     check!(b, "precondition: the sweep ran and emptied the session", r["done"] == true, s(&r));     check!(         b,