app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
← Commit history
0.9.28 source
14 files changed
+141−11
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.27+0.9.28
Cargo.lock+1−1@@ -122,7 +122,7 @@ dependencies = [ [[package]] name = "swx-bridge"-version = "0.9.27"+version = "0.9.28" dependencies = [ "miniz_oxide", "serde",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "swx-bridge"-version = "0.9.27"+version = "0.9.28" edition = "2021" rust-version = "1.80" description = "Adom SOLIDWORKS bridge — swx_* verbs over the SOLIDWORKS COM API, as a single native binary"
README.md+8−1@@ -492,7 +492,14 @@ not. ## Roadmap -- v0.9.27 (this): **the local sign-in helper.** ab hands a saved password only to+- v0.9.28 (this): **a launch record that outlived its processes is ended, not+ attached.** Measured 2026-10-12: ten days after a launch, with SOLIDWORKS not+ running, `swx_status.launch` still said "up and COM-attached. Carry on." The+ record is now checked against the live processes (pid plus creation time, so+ a reused pid does not count) and reports `stage:"ended"`, `endedWhy` and+ honest advice. The skills also say where `signInWindow` lives: at the top+ level of `swx_status`, beside `waitingOn`.+- v0.9.27: **the local sign-in helper.** ab hands a saved password only to a local caller, never to a bridge or over the relay, and a cloud agent is not allowed to start that local caller either (both measured 2026-10-12). So the page now ships `tools/local-sign-in.ps1`: run on the PC, it asks the vault
SKILL.md+2−1@@ -69,7 +69,8 @@ BEFORE you do it: `host_app_starting` until `solidworks.ready` is true — wait `retryAfterSeconds` and call again. `startingUnverifiable:true` means only reads may run. `waitingOn:"userSignIn"` means a 3DEXPERIENCE sign-in is up. `signInWindow`- names it `{hwnd, title, ownerImage, host}`, and the host can be the platform+ (at the TOP level of the reply, not inside `solidworks`) names it+ `{hwnd, title, ownerImage, host}`, and the host can be the platform updater (`desktopUpdate`, a window titled just "3DEXPERIENCE" that parks every app start). Tell the user, or call `swx_sign_in {reason}` (add `userAuthorized:true, window:<hwnd>` on their say-so for a window the
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "solidworks", "displayName": "SOLIDWORKS",- "version": "0.9.27",+ "version": "0.9.28", "description": "Bridge for SOLIDWORKS desktop CAD — 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/LAUNCH-RESILIENCE.md+14@@ -1379,3 +1379,17 @@ false, waitingOn: null`. The bridge saw nothing, while a sign-in sat on screen. second step: rejection markers cannot be read from a blind tree, so an unclosed window is `sign_in_unconfirmed`, never retried); whether "Remember me" can be read here; whether any ab verb reads a field's value on this host.++**MEASURED 2026-10-12 19:00Z (hosted 0.9.27), and the fix after it.** With+SOLIDWORKS not running (processCount 0) and the updater's login up,+`swx_status.launch` still said stage "attached" and advice "SOLIDWORKS is up and+COM-attached. Carry on." about the launch of 2026-10-02 (`ageSeconds` 878026,+attribution pid 32224). The record had outlived its process by ten days and+several bridge restarts, and the advice contradicted `solidworks.running:false`+in the same reply. The top-level `waitingOn` / `signInWindow` were right: they+come from a live census, never from the record.++| | Fix |+|---|---|+| A record that outlived its processes | When COM is not attached and no launch is in flight, a finished or "attached" record is checked against the census by IDENTITY (pid + creation time, so a reused pid does not count): the attributed process, the SOLIDWORKS it attached to, and its chain. If none of them is running, `launch` reports `stage:"ended"`, `finished:true`, `endedWhy` (the pids it named), `recordedStage` (what the record said), and advice that the launch is over and `swx_start` begins a new one. It never says "attached" or "carry on" then. The record on disk is not rewritten; the report is |+
install.shpackage.json+1−1@@ -1,6 +1,6 @@ { "slug": "solidworks-bridge",- "version": "0.9.27",+ "version": "0.9.28", "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": [
skills/solidworks-sign-in/SKILL.md+4−1@@ -50,7 +50,10 @@ Every line below is marked **[measured]** (seen on a real machine, 2026-10-02 to whatever its title, class or owner. The two titles above are only shortcuts. `swx_status` reports `waitingOn: "userSignIn"` and `signInWindow {hwnd, title, ownerImage, host, recognizedBy}` (host is- `solidworks`, `visualize` or `desktopUpdate`) while any of them is up.+ `solidworks`, `visualize` or `desktopUpdate`) while any of them is up. Both+ sit at the TOP level of the `swx_status` reply, next to each other, not inside+ its `solidworks` block. The `visualize` block repeats `signInWindow` only+ while Visualize is parked behind the sign-in (`parkedBehind`). ## When the tree shows only the frame
src/launch.rs+57@@ -3240,6 +3240,45 @@ pub fn report(os: &dyn LaunchOs) -> Value { report_with_attach(os, false) } +/// HAS THIS RECORD OUTLIVED EVERY PROCESS IT NAMES? MEASURED 2026-10-12 (hosted+/// 0.9.27): with SOLIDWORKS not running, `launch` still said stage "attached",+/// advice "SOLIDWORKS is up and COM-attached. Carry on.", about a launch of+/// 2026-10-02 — ten days and several bridge restarts after its process was+/// gone. The record names processes by IDENTITY (pid + creation time); when the+/// census holds none of them, the launch is over. `None` = something it names is+/// still alive, or it names nothing (cannot tell).+pub fn record_outlived(a: &Attempt, census: &[ProcInfo]) -> Option<String> {+ let mut named: Vec<(u32, Option<u64>, String)> = Vec::new();+ if let Some(at) = &a.attribution {+ named.push((at.pid, at.created_at, "the launch's attributed process".into()));+ }+ if let Some((pid, created)) = a.attributed_sldworks {+ named.push((pid, Some(created), "the SOLIDWORKS it attached to".into()));+ }+ for p in &a.chain {+ named.push((p.pid, p.created_at, p.image.clone()));+ }+ if named.is_empty() {+ return None;+ }+ let alive = |pid: u32, created: Option<u64>| {+ census.iter().any(|p| {+ p.pid == pid+ && match (created, p.created_at) {+ (Some(a), Some(b)) => a.abs_diff(b) <= 2,+ _ => true,+ }+ })+ };+ if named.iter().any(|(pid, c, _)| alive(*pid, *c)) {+ return None;+ }+ Some(format!(+ "none of the processes this record names is running any more ({}); the launch it describes is over",+ named.iter().map(|(pid, _, what)| format!("pid {pid}: {what}")).collect::<Vec<_>>().join(", ")+ ))+}+ /// The same report, told whether COM is attached RIGHT NOW — which is what lets /// a session that came up after a sign-in be re-claimed (see /// [`reattribute_after_attach`]). Only `swx_status` knows that fact, so only@@ -3299,6 +3338,24 @@ pub fn report_with_attach(os: &dyn LaunchOs, attached: bool) -> Value { obj.insert("advice".into(), json!(a.stage.advice())); obj.insert("consecutiveFailures".into(), json!(store.consecutive_failures)); obj.insert("backoffSeconds".into(), json!(backoff));+ // A RECORD THAT OUTLIVED ITS PROCESSES IS ENDED (0.9.28): never+ // "attached", never "carry on" while nothing it names runs.+ if !attached && live.is_none() && (a.finished || a.stage == Stage::Attached) {+ if let Ok(census) = os.processes() {+ if let Some(why) = record_outlived(&a, &census) {+ obj.insert("recordedStage".into(), json!(a.stage.name()));+ obj.insert("stage".into(), json!("ended"));+ obj.insert("finished".into(), json!(true));+ obj.insert("endedWhy".into(), json!(why));+ obj.insert(+ "advice".into(),+ json!("This launch record is OVER: the processes it names are gone, so SOLIDWORKS from \+ that launch is not running. Read `solidworks.running` for now; swx_start begins \+ a new launch."),+ );+ }+ }+ } } v }
tests/launch.rs+42@@ -1745,3 +1745,45 @@ fn re_attribution_claims_only_the_process_com_is_attached_to() { format!("{}", st["launch"]["reattributionRefusedBecause"]) ); }++/// MEASURED 2026-10-12 (hosted 0.9.27): with SOLIDWORKS NOT running, `launch`+/// still said stage "attached" and "SOLIDWORKS is up and COM-attached. Carry+/// on." about the launch of 2026-10-02 — its process ten days gone. A record+/// that outlived every process it names is ENDED.+#[test]+fn a_launch_record_that_outlived_its_processes_is_ended_not_attached() {+ let m = Machine::new("launch-stale-record", json!({}));+ // pid 32224 has been REUSED by an unrelated process created just now: the+ // same number, another process (identity is pid + creation time).+ let mut rows: Vec<Value> = serde_json::from_str(&std::fs::read_to_string(m.dir.join("preexisting.json")).unwrap()).unwrap();+ rows.push(json!({"pid": 32224, "image": "SWXDesktopUpdate.exe", "ageSeconds": 5}));+ std::fs::write(m.dir.join("preexisting.json"), Value::Array(rows).to_string()).unwrap();+ let scratch = TempDir::new("launch-stale-record-scratch");+ std::fs::write(+ scratch.join("launch-state.json"),+ json!({"attempt": {+ "id": 7, "startedAt": 1790953690u64, "invokedAt": 1790953690u64, "route": "shellExecute",+ "stage": "attached", "finished": true, "outcome": "attached",+ "attribution": {"pid": 32224, "createdAt": 1790953692u64, "source": "shellExecuteHandle"},+ "attributedSolidworks": {"pid": 41000, "createdAt": 1790953720u64},+ "chainPids": [{"pid": 32224, "image": "CATSTART.exe", "createdAt": 1790953692u64}]+ }}).to_string(),+ )+ .unwrap();+ let b = m.boot(TempDir::borrowed(&scratch.0), &[]);+ let st = b.call("swx_status", json!({}));+ let l = &st["launch"];+ check!(+ b,+ "stage ended (not attached), finished, endedWhy names the gone pids, the old stage kept as recordedStage",+ l["stage"] == "ended" && l["finished"] == true && l["recordedStage"] == "attached"+ && l["endedWhy"].as_str().unwrap_or("").contains("32224"),+ s(l)+ );+ check!(+ b,+ "the advice never says carry on while nothing it names runs",+ !l["advice"].as_str().unwrap_or("").to_lowercase().contains("carry on") && l["advice"].as_str().unwrap_or("").contains("OVER"),+ s(l)+ );+}
tests/signin.rs+9−3@@ -826,12 +826,18 @@ fn keystroke_ab() -> common::AbStub { fn drive_keystrokes(m: &Machine, ab: &common::AbStub, typed_lands: Option<String>, focus_follows: bool) { let deadline = Instant::now() + Duration::from_secs(25); let mut state = 0;+ let mut focused = false; while Instant::now() < deadline { let clicks = ab.calls_to("desktop_ui_click"); let clicked = |n: &str| clicks.iter().any(|c| c["args"]["name"] == n);- if focus_follows && !ab.calls_to("desktop_raise_window").is_empty() {- std::fs::write(m.dir.join("foreground.json"),- json!({"hwnd": 777001, "pid": 1, "title": "Login - 3DEXPERIENCE ID", "className": "x"}).to_string()).unwrap();+ // Written ONCE: rewriting it every tick truncates the file under a+ // reader, which then sees "no window has the foreground" (a harness+ // race seen under full-suite load, 2026-10-12).+ if focus_follows && !focused && !ab.calls_to("desktop_raise_window").is_empty() {+ let tmp = m.dir.join("foreground.json.tmp");+ std::fs::write(&tmp, json!({"hwnd": 777001, "pid": 1, "title": "Login - 3DEXPERIENCE ID", "className": "x"}).to_string()).unwrap();+ std::fs::rename(&tmp, m.dir.join("foreground.json")).unwrap();+ focused = true; } match state { 0 if !ab.calls_to("desktop_type").is_empty() => {
uninstall.sh