← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.357\ No newline at end of file+1.9.358\ No newline at end of file
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.357",+  "version": "1.9.358",   "description": "Drive Autodesk Fusion 360 from the cloud: launch Fusion, electronics board layout, design rules, exports (STEP/IGES/STL/3MF/USDZ/OBJ/DXF/DWG/Gerbers/BOM/CPL), fast APS server-indexed cloud search plus browse/recent/file-info/versions, cloud file download/upload and folder creation, and in-app parametric modeling (fusion_run_modeling_script). Never-charge: APS calls are capped to the free tier.",   "homepage": "https://wiki.adom.inc/adom/fusion-bridge",   "author": "Adom Inc.",
dashboard/Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "fusion-dashboard"-version = "1.9.357"+version = "1.9.358" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.357"+version = "1.9.358" edition = "2021"  [dependencies]
dashboard/ui/index.html+3
@@ -60,6 +60,9 @@ header{height:44px;display:flex;align-items:center;gap:12px;padding:0 16px;backg    the panel still at 16 - a visible step at the seam that selfCheck cannot see, because    selfCheck only measures the panel. Page and panel now share one inset at every width. */ main{margin:0 auto;padding:20px 16px 60px}+/* the panel is a SIBLING of main, so it needs the same 16px gutter on its own mount: with+   none, its card sat flush to the pane edges (selfCheck: 0px left, 0px right; issue #57). */+#newbie{padding:0 16px} h2{font-family:'Familjen Grotesk',sans-serif;font-size:17px;color:var(--text);margin:26px 0 12px} .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:12px} .card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:14px}
install.sh+1−1
@@ -21,7 +21,7 @@ echo "Installed fusion-bridge skills: $(ls "$PKG_DIR/skills" 2>/dev/null | tr '\ # binary (the three-artifact pattern: bridge zip -> ab, THIS pkg -> container skills + # this download, page media -> wiki). Per the wiki's PKG_VS_RELEASE policy, binaries are # platform-tagged RELEASE assets, never tarball content - the installer fetches them.-DASH_VERSION="1.9.357"+DASH_VERSION="1.9.358" DASH_URL="https://wiki.adom.inc/download/adom/fusion-bridge/${DASH_VERSION}/fusion-dashboard" DEST="" if [ -w /usr/local/bin ]; then DEST=/usr/local/bin
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.357",+  "version": "1.9.358",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {
page.json+11−27
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.357",+  "version": "1.9.358",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {@@ -11,6 +11,10 @@     "uninstall": "./uninstall.sh"   },   "tags": [+    "ab",+    "bridge",+    "fusion",+    "desktop-app",     "fusion360",     "autodesk",     "cad",@@ -75,7 +79,10 @@     "build a basic parts library",     "add a 3d model to a component",     "make a fusion 360 library with 3d",-    "set up the fusion bridge"+    "set up the fusion bridge",+    "fusion dashboard",+    "fusion status page",+    "open the fusion launch page"   ],   "discovery_pitch": "Use the Adom Bridge Fusion 360 bridge when the user wants to DRIVE Autodesk Fusion 360 from the cloud: open/search their team's Fusion cloud designs, lay out a PCB or do electronics, export STEP/IGES/STL/Gerbers/BOM, or build a component library with real rendering 3D. It runs Fusion on the user's own machine via Adom Bridge. NOT for web/browser/login/form-filling (that's the native-browser extension) and NOT for KiCad (that's the kicad bridge).",   "brief": "Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.",@@ -104,28 +111,5 @@       "label": "Part volume",       "prompt": "How much material does each printed part in this assembly use?"     }-  ],-  "agent_permissions": {-    "allow_bash": [],-    "allow": [-      "adom-desktop-fusion-bridge read-only verbs (via the adom-bridge CLI): fusion_readiness, fusion_get_app_state, fusion_addin_status, fusion_check_dialogs - state queries; they change nothing.",-      "adom-desktop-fusion-bridge read-only verbs: fusion_alerts (reads Fusion's own log for toasts), fusion_document_info, fusion_window_info, fusion_board_info, fusion_library_parts - inspect only.",-      "adom-desktop-fusion-bridge read-only verbs: fusion_prove_result (a persisted run report), fusion_plan_library_import (report-only preflight), fusion_list_cloud_projects, fusion_describe.",-      "adom-desktop-fusion-bridge evidence captures fusion_screenshot_fusion and fusion_capture_library_views read pixels and write local image files only."-    ],-    "soft_deny": [-      "adom-desktop-fusion-bridge lifecycle verbs fusion_stop, fusion_kill, fusion_restart_for_addin close or kill the Fusion application - keep the human prompt.",-      "adom-desktop-fusion-bridge fusion_install_fusion silently installs Autodesk Fusion on the user's machine.",-      "adom-desktop-fusion-bridge fusion_cleanup_cloud_files deletes files from the user's Autodesk cloud hub.",-      "adom-desktop-fusion-bridge managed-library verbs (fusion_managed_library_push/pull/create/link/unlink) publish to or overwrite the user's library.io libraries."-    ],-    "environment": [-      "adom-desktop-fusion-bridge is the Fusion 360 bridge for Adom Bridge; its fusion_* verbs are invoked through the adom-bridge CLI and drive Autodesk Fusion on the user's own desktop.",-      "adom-desktop-fusion-bridge desktop actions also pass Adom Bridge's own human-approval gate (caller identity, per-call reason, Activity Log) - a container approval is the first of two gates."-    ]-  },-  "author": {-    "name": "John Lauer",-    "email": "[email protected]"-  }-}\ No newline at end of file+  ]+}
rust/Cargo.toml+1−1
@@ -4,7 +4,7 @@ members = ["core", "lbr", "os-win", "os-mac", "bridge"]  # One version for the whole bridge, in lockstep with BRIDGE_VERSION (tools/release.sh bumps both). [workspace.package]-version = "1.9.357"+version = "1.9.358" edition = "2021" license = "MIT" 
rust/PORTING.md+18
@@ -460,3 +460,21 @@ Landed after the drill started: the add-in sync and the orphaned-run reconcile a two Win32 fact hooks, badge shedding, the bridge-doc Unsaved prompt, idle dirty bridge docs, stray Create-new prompts, the crash reporter, the five-minute silent update), and the contract without the 18 chain executor verbs (134). `tools/exe-only-flip.sh` holds the package flip for John's word.++## 2026-09-04 (evening): 1.9.357, the first exe-only release, on the insiders tier++Live checks before the flip, all on ConfRoomROG from the exe: Preferences open/close, MCP enable+(second pass, as the hint predicts), the EPG chip with a marking, the silent update (nothing+offered), `signin` (already signed in), `signin_2fa` (no window), APS sign-in (unconfigured branch:+no peer holds the org client id), Library Manager (Hub tab), the managed-library picker (six rows+read over UIA, cancelled), unlink (clean). Two fixes fell out: Library Manager is a top-level+window the dialog array does not list, and Fusion's morning update moved the ribbon tab row+about 11 DIP (the LIBRARY.IO DIPs are re-measured, the old group row is the second try).++`tools/exe-only-flip.sh` then made the package exe-only and `tools/release.sh --bump` published+1.9.357 to the insiders manifest (public untouched). `bridge_install` from that manifest on+ConfRoomROG: ab spawned `adom-fusion-bridge.exe` (kind exe), 134 verbs, ready, the KiCad bridge's+python the only interpreter on the box. Note: `bridge_install` overlays the cache dir rather than+clearing it, so a box that had the Python bridge keeps a stale server.py on disk (inert: the exe+spawns no sidecar unless a pref asks); a fresh box gets the 44-file zip only. Public promotion is+`tools/promote.sh 1.9.357 --attest "<John's words>"`.
rust/bridge/src/aps_signin.rs+6−3
@@ -12,7 +12,10 @@ const ABE_UPSELL: &str = "OPTIONAL, offer it: installing the Adom browser extens  /// The sign-in in flight: {verifier, state, redirectUri, done, error, startedAt}. pub static SIGNIN_STATE: Mutex<Option<Value>> = Mutex::new(None);-pub fn signin_pending() -> bool { SIGNIN_STATE.lock().unwrap().as_ref().map(|s| s["done"].as_bool() != Some(true) && now() - s["startedAt"].as_f64().unwrap_or(0.0) < 180.0).unwrap_or(false) }+/// The listener's life: ten minutes (the Python bridge gave three, which a human at a+/// password prompt on another machine kept missing; the ports are only bound meanwhile).+pub const LISTENER_SECONDS: f64 = 600.0;+pub fn signin_pending() -> bool { SIGNIN_STATE.lock().unwrap().as_ref().map(|s| s["done"].as_bool() != Some(true) && now() - s["startedAt"].as_f64().unwrap_or(0.0) < LISTENER_SECONDS).unwrap_or(false) } fn now() -> f64 { std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs_f64()).unwrap_or(0.0) } fn redirect_uri(port: u16) -> String { format!("http://localhost:{port}{REDIRECT_PATH}") } fn ab_call(ctx: &Ctx, verb: &str, args: Value, timeout: u64) -> Value {@@ -113,7 +116,7 @@ fn setup_chain(opened: bool, abe: &Value) -> Value {  /// The localhost callback listener for one sign-in: answers the redirect, exchanges the code. fn serve_callback(server: tiny_http::Server, store: ApsStore) {-    let deadline = std::time::Instant::now() + Duration::from_secs(180);+    let deadline = std::time::Instant::now() + Duration::from_secs(LISTENER_SECONDS as u64);     while std::time::Instant::now() < deadline {         if SIGNIN_STATE.lock().unwrap().as_ref().map(|s| s["done"].as_bool() == Some(true)).unwrap_or(true) { break; }         let Ok(Some(req)) = server.recv_timeout(Duration::from_secs(1)) else { continue };@@ -149,7 +152,7 @@ pub fn signin(ctx: &Ctx, args: &Value) -> Value {     let mut sb = [0u8; 12]; let _ = getrandom::getrandom(&mut sb); let state = b64url(&sb);     let mut server = None; let mut bound = 0u16;     for port in REDIRECT_PORTS { if let Ok(s) = tiny_http::Server::http(("127.0.0.1", port)) { server = Some(s); bound = port; break; } }-    let Some(server) = server else { return json!({"success": false, "error": format!("All callback ports busy on this machine: {:?}", REDIRECT_PORTS), "_hint": "Close whatever is holding these ports, or a sign-in is already in progress. These are only bound for ~3 min during sign-in."}) };+    let Some(server) = server else { return json!({"success": false, "error": format!("All callback ports busy on this machine: {:?}", REDIRECT_PORTS), "_hint": "Close whatever is holding these ports, or a sign-in is already in progress. These are only bound for ~10 min during sign-in."}) };     let redirect = redirect_uri(bound);     *SIGNIN_STATE.lock().unwrap() = Some(json!({"verifier": verifier, "state": state, "redirectUri": redirect, "done": false, "error": null, "startedAt": now()}));     let st2 = ApsStore { dir: store.dir.clone() };
rust/bridge/src/signin.rs+3−3
@@ -77,7 +77,7 @@ fn advance_credentials_page(ctx: &Ctx, hwnd: i64, email: Option<&str>) -> Value     if ctrls.iter().any(|c| nm(c).contains("password")) { out["wall"] = json!("password"); out["detail"] = json!("password field present - the human step"); return out; }     let emailish: Vec<Value> = { let e: Vec<Value> = ctrls.iter().filter(|c| ["email", "username", "user id", "userid"].iter().any(|k| nm(c).contains(k))).cloned().collect(); if e.is_empty() { ctrls.iter().take(1).cloned().collect() } else { e } };     if let (Some(email), Some(c)) = (email, emailish.first()) {-        let r = unwrap(&ab_call(ctx, "desktop_ui_set", json!({"hwnd": hwnd, "automationId": c["automationId"], "name": c["name"], "value": email}), 30));+        let r = unwrap(&ab_call(ctx, "desktop_ui_set", json!({"hwnd": hwnd, "automationId": c["automationId"], "name": c["name"], "text": email, "value": email}), 30));         if truthy(&r["success"]) || truthy(&r["ok"]) {             let mut detail = format!("typed email {} into {} (background UIA)", fusion_lbr::py_repr_str(email), fusion_lbr::py_repr_str(c["name"].as_str().or(c["automationId"].as_str()).unwrap_or("None")));             for btn in ["Next", "NEXT", "Sign in", "Sign In", "Continue"] {@@ -408,10 +408,10 @@ pub fn signin_2fa(ctx: &Ctx, args: &Value) -> Value {     let mut steps: Vec<String> = Vec::new();     let mut filled = false;     if ctrls.len() >= digits.len() && ctrls.len() >= 4 {-        for (i, ch) in digits.chars().take(ctrls.len()).enumerate() { let _ = ab_call(ctx, "desktop_ui_set", json!({"hwnd": hwnd, "automationId": ctrls[i]["automationId"], "name": ctrls[i]["name"], "value": ch.to_string()}), 20); }+        for (i, ch) in digits.chars().take(ctrls.len()).enumerate() { let _ = ab_call(ctx, "desktop_ui_set", json!({"hwnd": hwnd, "automationId": ctrls[i]["automationId"], "name": ctrls[i]["name"], "text": ch.to_string(), "value": ch.to_string()}), 20); }         filled = true; steps.push(format!("typed {} digits into {} segmented boxes (background UIA)", digits.len(), ctrls.len()));     } else if let Some(c) = ctrls.first() {-        let r = unwrap(&ab_call(ctx, "desktop_ui_set", json!({"hwnd": hwnd, "automationId": c["automationId"], "name": c["name"], "value": digits}), 20));+        let r = unwrap(&ab_call(ctx, "desktop_ui_set", json!({"hwnd": hwnd, "automationId": c["automationId"], "name": c["name"], "text": digits, "value": digits}), 20));         filled = truthy(&r["ok"]) || truthy(&r["set"]) || r.is_object();         steps.push(format!("typed the code into {} (background UIA)", fusion_lbr::py_repr_str(c["name"].as_str().or(c["automationId"].as_str()).unwrap_or("None"))));     }