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 166 file(s)
18 files changed
+265−36
BRIDGE_VERSION+1−1README.md+5−2bridge.json+4−1contracts/describe.json+53−1dashboard/Cargo.lock+1−1dashboard/Cargo.toml+1−1describe.py+6install.sh+1−1package.json+1−1page.json+10−26rust/Cargo.toml+1−1rust/bridge/src/main.rs+13rust/bridge/src/seat.rs+96skills/fusion-license-seat/SKILL.md+72tools/check-naming.shtools/check-portable.shtools/promote.shtools/release.shBRIDGE_VERSION+1−1@@ -1 +1 @@-1.10.43+1.11.0
README.md+5−2@@ -482,7 +482,7 @@ The AI does the whole setup - the user never gets handed a download link: ## Full verb catalog All verbs return a rich `_hint`. `fusion_describe '{}'` is the machine-readable source of truth; this table is generated from it.-_161 verbs._ Regenerate with `python3 tools/gen-readme-verbs.py`.+_164 verbs._ Regenerate with `python3 tools/gen-readme-verbs.py`. **Lifecycle and readiness** @@ -506,6 +506,8 @@ _161 verbs._ Regenerate with `python3 tools/gen-readme-verbs.py`. - `fusion_aps_set_browser` - Remember the browser+profile that authed to Autodesk so every later sign-in reuses it (power users have many browsers/profiles; the OS default is usually the... - `fusion_aps_set_client_id` - Set the org's APS (PKCE) client id used for cloud search. - `fusion_aps_signin` - Open the Autodesk sign-in in the REMEMBERED browser+profile (not the OS default); a background listener captures the token.+- `fusion_claim_seat` - Make THIS machine the active Fusion seat, the first-class way to move a licence between boxes while testing.+- `fusion_release_seat` - Give up the Autodesk seat from THIS machine so another box can claim it with no modal at all: closes Fusion cleanly (documents are NOT discarded; an unsaved... - `fusion_resolve_seat_dialog` - Readiness EXECUTOR (Rust port, Phase 3): detect + resolve the Autodesk seat/licensing modal ('Active Sessions Exceeded' / 'Suspend Remote Session') in the... - `fusion_set_seat_takeover` - Read/toggle the mid-session Autodesk seat-takeover sweep (STICKY per user, default ON). - `fusion_signin` - SELF-DRIVING as of 1.9.32: launches Fusion if needed, clicks Sign In, opens the OAuth in the right profile, TYPES THE EMAIL and advances to the exact remaining...@@ -668,6 +670,7 @@ _161 verbs._ Regenerate with `python3 tools/gen-readme-verbs.py`. **Other** +- `fusion_account` - WHO Fusion is signed in as on this machine, and whether this machine currently holds the Autodesk SEAT. - `fusion_execute_text_command` - Run a raw Fusion TEXT COMMAND (app.executeTextCommand) - the escape hatch to Fusion internals (Electron.* electronics commands included). - `fusion_get_preferences` - Read current Fusion appearance/navigation preferences (theme, activeTheme, invertScrollZoom, orbitScheme, modelingOrientation, gestureNav, cameraPivot... - `fusion_list_text_commands` - List Fusion's internal TEXT COMMANDS (TextCommands.List) whose name contains filter, case-insensitive; the discovery tool for fusion_execute_text_command.@@ -705,7 +708,7 @@ adom-wiki pkg install adom/fusion-bridge # the skills pack for your AI Auto-updates via `updateManifestUrl` (ab polls on launch + every 4h). Every release lands on the insiders manifest first, and the public manifest moves only after the pre-production gate, so the-two tiers can serve different versions. 161 verbs -+two tiers can serve different versions. 164 verbs - `fusion_describe '{}'` returns the full machine-readable catalog. More demos: <video controls width="100%" src="https://wiki.adom.inc/api/v1/pages/fusion-bridge/files/library2-3d-montage.mp4"></video>
bridge.json+4−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.10.43",+ "version": "1.11.0", "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.",@@ -21,6 +21,7 @@ "fusion_" ], "verbs": [+ "fusion_account", "fusion_action_ledger", "fusion_activate_document", "fusion_addin_status",@@ -51,6 +52,7 @@ "fusion_cancel_run", "fusion_capture_library_views", "fusion_check_recovery",+ "fusion_claim_seat", "fusion_cleanup_cloud_files", "fusion_close_all_documents", "fusion_close_document",@@ -144,6 +146,7 @@ "fusion_prewarm", "fusion_prove_result", "fusion_readiness",+ "fusion_release_seat", "fusion_reset_workspace", "fusion_resolve_seat_dialog", "fusion_restart_for_addin",
contracts/describe.json+53−1@@ -1,9 +1,27 @@ { "contract": "fusion-bridge verb catalog",- "frozen": "2026-09-22",+ "frozen": "2026-09-23", "note": "The Python bridge and the Rust bridge must both serve this catalog byte-for-byte in meaning: same names, same args, same result shapes, same timeouts. Regenerate ONLY from describe.py and diff in CI.", "source": "describe.py", "verbs": [+ {+ "example": {},+ "input": {},+ "longRunning": false,+ "name": "fusion_account",+ "output": {+ "account": "{email, name, userId, hub}",+ "holdsSeat": "bool",+ "license": "{authorized, trial, expiresAt, ...}",+ "running": "bool",+ "seatDialogOpen": "bool",+ "signedIn": "bool",+ "success": "bool"+ },+ "statusVerb": null,+ "summary": "WHO Fusion is signed in as on this machine, and whether this machine currently holds the Autodesk SEAT. One subscription runs ONE active Fusion at a time, so the seat moves between machines; this is the read. Returns the signed-in account (email, name, hub), Autodesk's own licence blob (authorized, trial, expiry, reauthorize time, from Application.LicenseInformation) and holdsSeat. Read-only: never starts Fusion, never signs anybody in or out.",+ "timeoutSeconds": 60+ }, { "example": { "limit": 20@@ -566,6 +584,24 @@ "summary": "Ask whether a cloud file has a crash-recovery document waiting, which would raise Fusion's recovery modal on open. BEST-EFFORT: Fusion's Python API does not reliably expose this (apiReliable reports whether the property existed), so a false negative is possible; the dependable signal is an open that times out, and fusion_dismiss_blocking_dialogs clears the modal. Finds the file by exact name in one project folder.", "timeoutSeconds": 60 },+ {+ "example": {},+ "input": {+ "profile": "optional str (browser profile to name in the sign-in hint, e.g. edge:[email protected])"+ },+ "longRunning": false,+ "name": "fusion_claim_seat",+ "output": {+ "account": "obj",+ "holdsSeat": "bool",+ "license": "obj",+ "steps": "[{step, ok}]",+ "success": "bool"+ },+ "statusVerb": "fusion_readiness",+ "summary": "Make THIS machine the active Fusion seat, the first-class way to move a licence between boxes while testing. Starts Fusion if needed, then answers Autodesk's 'Active Sessions Exceeded' modal in the BACKGROUND (Continue): this box takes the seat and the other machine's Fusion suspends RESUMABLY, keeping its documents. Answers needs_signin (never a credential prompt) when the box is not signed in: run fusion_signin first. Confirms with fusion_account and reports the account and licence it ended up with.",+ "timeoutSeconds": 300+ }, { "example": { "fileIds": [@@ -2376,6 +2412,22 @@ "summary": "FAST readiness check (does NOT launch Fusion): is the Fusion host app installed + running + the bridge ready to drive? ab DETECTS Fusion (never installs it) via bridge.json 'detect', and auto-installs Python if missing (ab >=1.9.47). Call this before driving Fusion; if installed-but-not-running, fusion_start. SELF-HEALS the seat/licensing dialog (background UIA + screenshot-verify) whenever it detects one. Pairs with ab's bridge_readiness.", "timeoutSeconds": 15 },+ {+ "example": {},+ "input": {+ "save": "optional bool (default false): save open documents before closing"+ },+ "longRunning": false,+ "name": "fusion_release_seat",+ "output": {+ "released": "bool",+ "success": "bool",+ "wasHeldBy": "obj"+ },+ "statusVerb": null,+ "summary": "Give up the Autodesk seat from THIS machine so another box can claim it with no modal at all: closes Fusion cleanly (documents are NOT discarded; an unsaved one refuses and says so) and leaves the account signed in, so this box can claim the seat straight back. The counterpart of fusion_claim_seat.",+ "timeoutSeconds": 180+ }, { "example": {}, "input": {
dashboard/Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "fusion-dashboard"-version = "1.10.43"+version = "1.11.0" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.10.43"+version = "1.11.0" edition = "2021" [dependencies]
describe.py+6@@ -40,6 +40,12 @@ _T = [ {"hwnd": "required int (the dialog window)"}, {"success": "bool"}, 30, None, False, {"hwnd": 123456}), ("fusion_get_app_state", "Current app state: running, active document, workspace, isElectronics. The poll/status verb for long-running calls.", {}, {"activeDocument": "str", "activeWorkspace": "str", "isElectronics": "bool"}, 20, None, False, {}),+ ("fusion_account", "WHO Fusion is signed in as on this machine, and whether this machine currently holds the Autodesk SEAT. One subscription runs ONE active Fusion at a time, so the seat moves between machines; this is the read. Returns the signed-in account (email, name, hub), Autodesk's own licence blob (authorized, trial, expiry, reauthorize time, from Application.LicenseInformation) and holdsSeat. Read-only: never starts Fusion, never signs anybody in or out.",+ {}, {"success": "bool", "running": "bool", "signedIn": "bool", "account": "{email, name, userId, hub}", "license": "{authorized, trial, expiresAt, ...}", "holdsSeat": "bool", "seatDialogOpen": "bool"}, 60, None, False, {}),+ ("fusion_claim_seat", "Make THIS machine the active Fusion seat, the first-class way to move a licence between boxes while testing. Starts Fusion if needed, then answers Autodesk's 'Active Sessions Exceeded' modal in the BACKGROUND (Continue): this box takes the seat and the other machine's Fusion suspends RESUMABLY, keeping its documents. Answers needs_signin (never a credential prompt) when the box is not signed in: run fusion_signin first. Confirms with fusion_account and reports the account and licence it ended up with.",+ {"profile": "optional str (browser profile to name in the sign-in hint, e.g. edge:[email protected])"}, {"success": "bool", "holdsSeat": "bool", "steps": "[{step, ok}]", "account": "obj", "license": "obj"}, 300, "fusion_readiness", False, {}),+ ("fusion_release_seat", "Give up the Autodesk seat from THIS machine so another box can claim it with no modal at all: closes Fusion cleanly (documents are NOT discarded; an unsaved one refuses and says so) and leaves the account signed in, so this box can claim the seat straight back. The counterpart of fusion_claim_seat.",+ {"save": "optional bool (default false): save open documents before closing"}, {"success": "bool", "released": "bool", "wasHeldBy": "obj"}, 180, None, False, {}), ("fusion_prewarm", "Warm the bridge ahead of the first real call, WITHOUT starting Fusion: confirms the add-in is synced into every Fusion add-in folder, readies the space-free staging folder, and caches the PCB design-rules catalog from adom/pcb-design-rules. Adom Bridge calls it once per installed version in the background (bridge.json prewarm.verb); safe to call again any time. Launching Fusion is never part of it.", {}, {"ok": "bool", "steps": "[{step, ok, ...}]", "fusionInstalled": "bool", "fusionStarted": "bool (always false)"}, 120, None, False, {}), ("fusion_readiness", "FAST readiness check (does NOT launch Fusion): is the Fusion host app installed + running + the bridge ready to drive? ab DETECTS Fusion (never installs it) via bridge.json 'detect', and auto-installs Python if missing (ab >=1.9.47). Call this before driving Fusion; if installed-but-not-running, fusion_start. SELF-HEALS the seat/licensing dialog (background UIA + screenshot-verify) whenever it detects one. Pairs with ab's bridge_readiness.",
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.10.43"+DASH_VERSION="1.11.0" 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.10.43",+ "version": "1.11.0", "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+10−26@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.10.43",+ "version": "1.11.0", "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",@@ -76,6 +80,9 @@ "add a 3d model to a component", "make a fusion 360 library with 3d", "set up the fusion bridge",+ "fusion dashboard",+ "fusion status page",+ "open the fusion launch page", "aps not configured", "share aps client id across my org", "set up fusion cloud search for my company",@@ -108,28 +115,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.10.43"+version = "1.11.0" edition = "2021" license = "MIT"
rust/bridge/src/main.rs+13@@ -21,6 +21,7 @@ mod etiquette; mod launch; mod lifecycle; mod readiness_exec;+mod seat; mod orchestrations; mod mint; mod prove;@@ -454,6 +455,18 @@ fn main() { "fusion_live_feed" | "live_feed" if addin_owner_is_exe(&**h) => { let pids: Vec<u32> = h.fusion_processes().iter().map(|p| p.pid).collect(); let mut r = livefeed::live_feed(&caller, pids, &parsed["args"]); r["shell"] = json!("rust"); r }, "fusion_check_3d_thumbnail" | "check_3d_thumbnail" if addin_owner_is_exe(&**h) => { let mut r = prove::check_3d_thumbnail(&parsed["args"]); r["shell"] = json!("rust"); r }, "fusion_resolve_seat_dialog" | "resolve_seat_dialog" if addin_owner_is_exe(&**h) => { let (sc, ca) = (sidecar.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); let dlg = |r: Value| r; let ctx = addin::Ctx { host: &**h, fusion_exe: h.fusion_executable(), short_path: |p| host().short_path(p), state_dir: h.bridge_state_dir(), caller: caller.clone(), pids: h.fusion_processes().iter().map(|p| p.pid).collect(), sidecar: &side, dialogs: &dlg }; let mut r = readiness_exec::resolve_seat_dialog_verb(&ctx, &parsed["args"]); r["shell"] = json!("rust"); r },+ v @ ("fusion_account" | "account" | "fusion_claim_seat" | "claim_seat" | "fusion_release_seat" | "release_seat") if addin_owner_is_exe(&**h) => {+ let (sc, ca) = (sidecar.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t);+ let sd = h.bridge_state_dir(); let dlg = |r: Value| r;+ let ctx = addin::Ctx { host: &**h, fusion_exe: h.fusion_executable(), short_path: |p| host().short_path(p), state_dir: sd.clone(), caller: caller.clone(), pids: h.fusion_processes().iter().map(|p| p.pid).collect(), sidecar: &side, dialogs: &dlg };+ let pt = |verb: &str, a: Value| addin::pass_through(verb, &a, &ctx);+ let mut r = match v.strip_prefix("fusion_").unwrap_or(v) {+ "account" => seat::account(&ctx, &pt),+ "claim_seat" => seat::claim_seat(&ctx, &parsed["args"], &pt),+ _ => seat::release_seat(&ctx, &parsed["args"], &pt),+ };+ r["shell"] = json!("rust"); r+ }, "fusion_prewarm" | "prewarm" => { // ab calls this once per installed version, in the background, after the first // boot that follows an install (adom-bridge 2.1.153, page issue #99). It fetches
rust/bridge/src/seat.rsadded+96@@ -0,0 +1,96 @@+//! The Autodesk SEAT (licence) and who Fusion is signed in as.+//!+//! One Autodesk subscription runs ONE active Fusion session at a time. Signing in on a second+//! machine raises "Active Sessions Exceeded"; answering Continue moves the seat there and+//! suspends the first machine RESUMABLY. John moves the seat between ConfRoomROG, arav-rog and+//! AdomLapper constantly while testing, so that move is a first-class verb instead of a recipe:+//! `fusion_account` says who is signed in and whether this box holds the seat, `fusion_claim_seat`+//! moves it here, `fusion_release_seat` gives it up so another box can take it without a dialog.+use serde_json::{json, Value};+use std::time::{Duration, Instant};++/// Autodesk's licence blob, read through Fusion's own `Application.LicenseInformation`.+fn license_info(pass: &dyn Fn(&str, Value) -> Value) -> Value {+ let r = pass("execute_text_command", json!({"command": "Application.LicenseInformation"}));+ if r["success"].as_bool() != Some(true) { return Value::Null; }+ let raw = r["output"].as_str().map(str::to_string).or_else(|| r["data"].as_str().map(str::to_string)).unwrap_or_default();+ let parsed: Value = serde_json::from_str(raw.trim()).unwrap_or(Value::Null);+ let svc = parsed.as_object().and_then(|m| m.values().next().cloned()).unwrap_or(Value::Null);+ if !svc.is_object() { return Value::Null; }+ let b = |k: &str| svc[k].as_str().map(|s| s == "true").or(svc[k].as_bool());+ json!({"authorized": svc["auth"].as_bool(), "userId": svc["o2id"], "issuedAt": svc["iat"], "expiresAt": svc["exp"],+ "reauthorizeAt": svc["reauthz"], "trial": b(".isTrialLicense"), "temporary": b(".isTempLicense"), "gracePeriod": b(".isGracePeriod")})+}++/// `fusion_account`: who Fusion is signed in as on THIS box, and whether it holds the seat.+pub fn account(ctx: &crate::addin::Ctx, pass: &dyn Fn(&str, Value) -> Value) -> Value {+ let running = !ctx.host.fusion_processes().is_empty();+ if !running {+ return json!({"success": true, "running": false, "signedIn": Value::Null, "holdsSeat": false,+ "_hint": "Fusion is not running here, so this box holds no seat. fusion_claim_seat starts it and takes the seat (the other machine suspends resumably)."});+ }+ let ab = fusion_core::ab::Loopback::from_env();+ let facts = ab.as_ref().map(|a| fusion_core::windows_facts::facts_from_ab(a, &ctx.caller)).unwrap_or_default();+ if facts.needs_signin {+ return json!({"success": true, "running": true, "signedIn": false, "needsSignin": true, "holdsSeat": false, "windowTitle": facts.main_title,+ "_hint": "Fusion is at the Autodesk sign-in screen: nobody is signed in on this box. fusion_signin {auto:true, profile:\"<browser>:<email>\"} drives it; a password or 2FA step comes back as signinWall for the user to finish."});+ }+ let who = pass("run_modeling_script", json!({"script": "import adsk.core\napp = adsk.core.Application.get()\nu = app.currentUser\nresult = {\"email\": u.email if u else None, \"name\": u.displayName if u else None, \"userId\": u.userId if u else None, \"hub\": app.data.activeHub.name if app.data.activeHub else None, \"offline\": app.isOffLine}"}));+ let r = if who["data"]["result"].is_object() { who["data"]["result"].clone() } else { who["result"].clone() };+ let lic = license_info(pass);+ // the seat is HELD here when Fusion is signed in, authorized and not showing the seat modal+ let holds = r["email"].is_string() && lic["authorized"].as_bool() != Some(false) && facts.licensing_dialog.is_none();+ json!({"success": true, "running": true, "signedIn": r["email"].is_string(), "account": r, "license": lic,+ "holdsSeat": holds, "seatDialogOpen": facts.licensing_dialog.is_some(), "windowTitle": facts.main_title,+ "_hint": if holds { "This box holds the Autodesk seat. Another box's fusion_claim_seat takes it from here and suspends this Fusion resumably." }+ else { "Signed in, but this box does not hold the seat right now (another machine has it, or the seat modal is up). fusion_claim_seat takes it." }})+}++/// `fusion_claim_seat`: make THIS box the active Fusion seat.+pub fn claim_seat(ctx: &crate::addin::Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value {+ let t0 = Instant::now();+ let mut steps = Vec::new();+ if ctx.host.fusion_processes().is_empty() {+ let r = crate::launch::start(ctx, &json!({}));+ steps.push(json!({"step": "start Fusion", "ok": r["success"].as_bool() != Some(false)}));+ if r["success"].as_bool() == Some(false) { return json!({"success": false, "errorCode": "start_failed", "steps": steps, "error": r["error"]}); }+ } else { steps.push(json!({"step": "Fusion already running", "ok": true})); }+ // the seat modal is the transfer itself: Continue takes the seat here and suspends the other box+ let mut resolved = json!({"clicks": 0});+ for _ in 0..3 {+ let ab = fusion_core::ab::Loopback::from_env();+ let facts = ab.as_ref().map(|a| fusion_core::windows_facts::facts_from_ab(a, &ctx.caller)).unwrap_or_default();+ if facts.licensing_dialog.is_none() { break; }+ resolved = crate::readiness_exec::resolve_seat_dialog_verb(ctx, &json!({}));+ steps.push(json!({"step": "answered the Autodesk seat modal (Continue: this box takes the seat)", "clicks": resolved["clicks"]}));+ std::thread::sleep(Duration::from_secs(2));+ }+ let acct = account(ctx, pass);+ if acct["needsSignin"].as_bool() == Some(true) {+ let mut a = acct.clone();+ a["success"] = json!(false); a["errorCode"] = json!("needs_signin"); a["steps"] = json!(steps);+ a["error"] = json!("Fusion on this box is at the Autodesk sign-in screen, so there is no seat to claim yet.");+ a["_hint"] = json!(format!("Sign this box in first: fusion_signin {{auto:true{}}}. Then fusion_claim_seat. A password or 2FA step comes back as signinWall for the user to finish; the bridge never types a credential.", args["profile"].as_str().map(|p| format!(", profile:\"{p}\"")).unwrap_or_default()));+ return a;+ }+ json!({"success": acct["holdsSeat"].as_bool() == Some(true), "steps": steps, "seatDialogAnswered": resolved["clicks"],+ "account": acct["account"], "license": acct["license"], "holdsSeat": acct["holdsSeat"], "durationMs": t0.elapsed().as_millis() as u64,+ "_hint": "This box now holds the Autodesk seat; any other machine signed into the same account suspended RESUMABLY (its documents are kept, it resumes when it takes the seat back). fusion_account on the other box confirms it lost the seat."})+}++/// `fusion_release_seat`: give up the seat from THIS box so another can take it cleanly.+pub fn release_seat(ctx: &crate::addin::Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value {+ if ctx.host.fusion_processes().is_empty() {+ return json!({"success": true, "released": true, "wasRunning": false, "_hint": "Fusion was not running here, so this box held no seat. Claim it on the box you want with fusion_claim_seat."});+ }+ let before = account(ctx, pass);+ let stop = crate::lifecycle::stop(ctx, &json!({"save": args["save"].as_bool().unwrap_or(false)}));+ if stop["success"].as_bool() == Some(false) {+ let mut s = stop.clone();+ s["_hint"] = json!("Fusion would not close cleanly, so the seat is still held here. Read the error: an unsaved document usually needs fusion_save_to_cloud or an explicit close first. Nothing was discarded.");+ s["heldBy"] = before["account"].clone();+ return s;+ }+ json!({"success": true, "released": true, "wasRunning": true, "wasHeldBy": before["account"], "stopped": stop["steps"],+ "_hint": "Fusion is closed here, so the seat is free: the next box's fusion_claim_seat takes it with no seat modal at all. Nothing was signed out, so this box claims it straight back later."})+}
skills/fusion-license-seat/SKILL.mdadded+72@@ -0,0 +1,72 @@+---+name: fusion-license-seat+description: Moving the Autodesk SEAT (the licence) between machines, which testing across several boxes needs constantly. One subscription runs ONE active Fusion at a time: fusion_account says who is signed in here and whether this box holds the seat, fusion_claim_seat takes it (the other machine suspends resumably), fusion_release_seat gives it up. Also what to do when a box is signed in as the WRONG person, and why the bridge never types a credential. Trigger words - autodesk seat, fusion licence, license, move the licence, switch boxes, Active Sessions Exceeded, Suspend Remote Session, seat modal, take the seat, release the seat, fusion_account, fusion_claim_seat, fusion_release_seat, signed in as the wrong user, sign fusion in, test on another box.+---++# The Autodesk seat: moving a licence between machines++One Autodesk subscription runs **one active Fusion session at a time**. Every machine can stay+signed in; only one holds the seat. Testing across ConfRoomROG, arav-rog and AdomLapper means+moving it, so it is three verbs, not a recipe.++## The three verbs++```bash+adom-bridge --target <box> fusion_account '{}' # who is signed in here, does this box hold the seat+adom-bridge --target <box> fusion_claim_seat '{}' # take the seat: the other machine suspends RESUMABLY+adom-bridge --target <box> fusion_release_seat '{}' # give it up: closes Fusion, stays signed in+```++- **`fusion_account`** is read-only. It never starts Fusion and never signs anyone in or out. It+ returns the account (email, name, hub), Autodesk's own licence blob (`authorized`, `trial`,+ `expiresAt`, from `Application.LicenseInformation`) and `holdsSeat`.+- **`fusion_claim_seat`** starts Fusion if needed and answers Autodesk's "Active Sessions Exceeded"+ modal in the background with Continue. The other machine's Fusion **suspends resumably**: its+ documents are kept, and it resumes when it takes the seat back.+- **`fusion_release_seat`** closes Fusion cleanly and leaves the account signed in, so the next+ box claims the seat with no modal at all. An unsaved document refuses the close and says so;+ nothing is ever discarded.++## Moving the seat, the normal case++Both boxes are signed in as the same person, so just claim it where you want it:++```bash+adom-bridge --target arav-rog fusion_claim_seat '{}' # arav-rog now holds it+adom-bridge --target ConfRoomROG fusion_account '{}' # confirms ConfRoomROG lost it+```++Cleaner when you know you are done with a box (no modal for the next one):+`fusion_release_seat` on the old box, then `fusion_claim_seat` on the new one.++## When a box is signed in as the WRONG person++The seat verbs move a licence; they do not change WHO is signed in. Fusion's window exposes no+automatable controls for the account menu, and **the bridge never types a credential**. So:++1. `fusion_account` tells you who it is.+2. To change it, the person signs out in Fusion (the avatar, top right, then Sign out) or signs in+ on the sign-in screen.+3. `fusion_signin {auto:true, profile:"<browser>:<email>"}` drives everything up to the wall:+ it opens the OAuth in the right browser profile and types the email. A password or 2FA step+ comes back as `signinWall` for the person, and `fusion_signin_2fa {code}` finishes a 2FA if you+ can read the code (adom-google can, from their Gmail).+4. A box whose Fusion sits on the sign-in screen answers `needsSignin` from `fusion_account` and+ from `fusion_claim_seat`, never a made-up failure.++## What the seat modal looks like, and why you rarely see it++"Active Sessions Exceeded" and "Suspend Remote Session" are the transfer itself. The bridge+answers them in the background: `fusion_claim_seat` does it, `fusion_readiness` does it during a+launch, and the passive sweep does it mid-session (`fusion_set_seat_takeover` turns that sweep off+for a user who would rather decide each time). `fusion_resolve_seat_dialog` is the low-level click+if you are driving a launch by hand.++## Rules++- **Never kill Fusion to move a seat.** `fusion_release_seat` closes it cleanly and protects+ unsaved work; a kill can lose a document and leaves recovery files behind.+- **Claiming is not rude, it is the design.** The other machine suspends resumably. Still, when+ another person is working on that box, tell them, or use `fusion_notify_owner`.+- **A trial or expired licence is not a seat problem.** `fusion_account.license` shows `trial`+ and `expiresAt`; when it has run out, no amount of claiming helps.
tools/check-naming.shtools/check-portable.shtools/promote.shtools/release.sh