app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
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.
← Commit history
Update 155 file(s)
12 files changed
+48−40
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.357+1.9.358
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]"- }-}+ ]+}
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")))); }