← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.10.43\ No newline at end of file+1.11.0\ No newline at end of file
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]"-  }-}\ 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.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.sh

  
tools/check-portable.sh

  
tools/promote.sh

  
tools/release.sh