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
Publish 1.9.262
9 files changed
+48−51
.release-page-head+1−1@@ -1 +1 @@-8116cd531f525ee0ef89d9db630fd1fd2fabf357+5fbd5bc168a63b44db78b4e36d61d48729e23d9f
LIBRARY_FINDINGS.md+1−1@@ -111,7 +111,7 @@ Fusion error dialogs (e.g. "<file>.lbr has errors and cannot be opened") are **o NOT enumerate in `desktop_list_windows` and are invisible to a plain capture of the main window. A `fusion_*` verb can return `success:true` while an error dialog is actually up. -**`desktop_screenshot_window {hwnd}` (AD v1.8.177+) captures them automatically** and returns them at the+**`desktop_screenshot_window {hwnd}` (ab v1.8.177+) captures them automatically** and returns them at the TOP LEVEL: **`ownedPopupCount`** (always present; 0 = none) + **`_screenshots[]`** (each owned popup: `title`, `kind:"owned_popup"`, `localSafePath`, `rect`, `coordMap`). The CLI auto-pulls each popup PNG.
PUBLISHING.md+1−1@@ -106,7 +106,7 @@ adom-wiki repo push <org>/<slug> --files adom-bridge-fusion-manifest.json BRIDGE adom-bridge bridge_install '{"manifestUrl":"https://wiki.adom.inc/api/v1/pages/<slug>/files/adom-bridge-fusion-manifest.json","force":true}' ``` -- **Server-only change** (`server.py` / `describe.py` / `handlers/`): that's it. AD reaps + respawns+- **Server-only change** (`server.py` / `describe.py` / `handlers/`): that's it. ab reaps + respawns the bridge **server** from the new cache on the next verb call. **No `fusion_stop`.** Verify a new verb appears with `fusion_describe` (needs Fusion running - `fusion_start` first if it isn't; the describe gate returns `fusion_not_running` otherwise).
README.md+2−2@@ -359,7 +359,7 @@ The AI does the whole setup - the user never gets handed a download link: - **Detection first:** ab detects an existing Fusion via this bridge's `detect` paths - per-user (`%LOCALAPPDATA%`) AND system-wide (`%ProgramFiles%`, from `--globalinstall`) webdeploy installs.- `fusion_readiness` / AD's `bridge_readiness` answer installed/running/ready without launching+ `fusion_readiness` / ab's `bridge_readiness` answer installed/running/ready without launching anything. - **AI-driven install:** if Fusion is absent, the AI OFFERS to install it and does it on a yes - the fusion-onboarding skill silent-installs the **free trial / personal-use tier** (plenty for viewing@@ -446,7 +446,7 @@ _80 verbs._ **Self** -- `fusion_describe` - Self-describe every verb the bridge exposes (this list) for AD's Verbs tab + runner.+- `fusion_describe` - Self-describe every verb the bridge exposes (this list) for ab's Verbs tab + runner. ## Two install layers (they are NOT the same thing)
SKILL.md+6−6@@ -24,20 +24,20 @@ Branch on the stable `errorCode`, never the prose: | `ready:true` (fusion_readiness) | Fusion up + add-in current | drive | | `running:false` / `fusion_not_running` | installed, not launched | `fusion_start` (returns when the add-in is ready, or `stillLaunching` on a slow first launch) | | `stillLaunching:true` (fusion_start) | first launch, add-in not up yet (can take 2-4 min) | do NOT re-call `fusion_start`; POLL `fusion_readiness` every ~10s until `ready:true` |-| `installed:false` / `host_app_not_installed` (AD >=1.9.79, + a `hostApp` field) | no Fusion host app | OFFER to install, then **`fusion_install_fusion`** (no shell approval; elevation-aware, streams the free trial) -> poll `fusion_readiness` (`installing:true`) until `installed:true` -> `fusion_start`. Details: [fusion-onboarding](skills/fusion-onboarding/SKILL.md) |-| `needsSignin:true` (`fusion_start` **and** `fusion_readiness`) | Fusion is running but stuck at the first-run Autodesk sign-in (main UI blocked, add-in can't serve, so `ready:false`) | AUTOMATE it (proven sequence, see [fusion-onboarding](skills/fusion-onboarding/SKILL.md)): (1) clear STALE `signin.autodesk.com` tabs (a dead tab = expired code = "Sign-in request expired"); (2) `desktop_ui_click` "Retry browser sign-in" / "Sign In" for ONE fresh request; (3) click "Open Product" with **`desktop_click` (SendInput=isTrusted)** — `desktop_ui_click` (UIA) does NOT fire the protocol; (4) on the in-page "open Autodesk Identity Manager?" overlay (NOT an owned popup) tick "Always allow" + "Open" via `desktop_click`, FAST; (5) poll `fusion_readiness`. `nbrowser` (CDP) can do 3-4 headlessly if it's on the SAME browser window. NEVER auto-enter password/2FA. To ask the user, `notify_user` works directly (AD ≥1.9.84); add a cross-AD `target` if you're on a VM |+| `installed:false` / `host_app_not_installed` (ab >=1.9.79, + a `hostApp` field) | no Fusion host app | OFFER to install, then **`fusion_install_fusion`** (no shell approval; elevation-aware, streams the free trial) -> poll `fusion_readiness` (`installing:true`) until `installed:true` -> `fusion_start`. Details: [fusion-onboarding](skills/fusion-onboarding/SKILL.md) |+| `needsSignin:true` (`fusion_start` **and** `fusion_readiness`) | Fusion is running but stuck at the first-run Autodesk sign-in (main UI blocked, add-in can't serve, so `ready:false`) | AUTOMATE it (proven sequence, see [fusion-onboarding](skills/fusion-onboarding/SKILL.md)): (1) clear STALE `signin.autodesk.com` tabs (a dead tab = expired code = "Sign-in request expired"); (2) `desktop_ui_click` "Retry browser sign-in" / "Sign In" for ONE fresh request; (3) click "Open Product" with **`desktop_click` (SendInput=isTrusted)** — `desktop_ui_click` (UIA) does NOT fire the protocol; (4) on the in-page "open Autodesk Identity Manager?" overlay (NOT an owned popup) tick "Always allow" + "Open" via `desktop_click`, FAST; (5) poll `fusion_readiness`. `nbrowser` (CDP) can do 3-4 headlessly if it's on the SAME browser window. NEVER auto-enter password/2FA. To ask the user, `notify_user` works directly (ab ≥1.9.84); add a cross-ab `target` if you're on a VM | | `addinStale:true` / `addin_stale` | Fusion runs an OLD add-in that never re-synced (silent-failure risk) | `fusion_stop` then `fusion_start` — the bridge re-syncs the add-in from cache on start (Fusion must be CLOSED) |-| `bridge_restarting` + `stillRunning:true` (AD ≥1.9.77) | the bridge is respawning after a `bridge_install` | NEVER a not-installed/fatal signal: poll `fusion_readiness` until `ready:true`, then retry. Read-only verbs (`fusion_readiness`/`fusion_describe`/`fusion_get_app_state`) forward straight through even mid-respawn — no sleep-and-recheck hacks needed |+| `bridge_restarting` + `stillRunning:true` (ab ≥1.9.77) | the bridge is respawning after a `bridge_install` | NEVER a not-installed/fatal signal: poll `fusion_readiness` until `ready:true`, then retry. Read-only verbs (`fusion_readiness`/`fusion_describe`/`fusion_get_app_state`) forward straight through even mid-respawn — no sleep-and-recheck hacks needed | | `blockingDialogs:[…]` | a modal is blocking the add-in | dismiss in the BACKGROUND, then the op auto-resumes | -**Runtime note:** AD **provisions Python itself** (a portable no-UAC 3.12.13 runtime if none is on PATH — the bridge never bootstraps its own; check AD's `runtimes` verb). You never install Python.+**Runtime note:** ab **provisions Python itself** (a portable no-UAC 3.12.13 runtime if none is on PATH — the bridge never bootstraps its own; check ab's `runtimes` verb). You never install Python. ## The 8 conventions (from the Bridge SDK) -1. **Poll readiness, don't launch blindly.** `fusion_readiness` (or AD's `bridge_readiness`) is read-only — it reports `installed`/`running`/`ready`/`addinStale` WITHOUT launching anything. Loop: readiness → act on the row above → drive.+1. **Poll readiness, don't launch blindly.** `fusion_readiness` (or ab's `bridge_readiness`) is read-only — it reports `installed`/`running`/`ready`/`addinStale` WITHOUT launching anything. Loop: readiness → act on the row above → drive. 2. **`fusion_readiness` is the standardized cold-start probe.** (Fusion is a detected HOST APP with no heavy download, so there's no `prewarm` step — readiness is the whole gate.) 3. **Core verbs live in the table below; `fusion_describe '{}'` is the full machine-readable catalog.** Read the table to act fast; hit `describe` for completeness — don't guess a verb.-4. **Background by default — automate first, NOTIFY only at a true wall.** This bridge drives the AI's workspace, not the user's screen: dismiss dialogs in the BACKGROUND (`fusion_close_window`, never `fusion_send_key`/Escape, which steals focus). AUTOMATE sign-in / dialogs yourself (see the proven sign-in sequence in [fusion-onboarding](skills/fusion-onboarding/SKILL.md)); reserve an AD **`notify_user`** for something only the user can do — a **UAC/elevation** prompt, a license accept, a **2FA/password** step you must not auto-fill, or a licensing **DECISION** that risks their other machine. **`notify_user` works directly over the bridge's direct API (AD ≥1.9.84) — call it, don't relay a payload for the AI to send.** When this bridge runs UNATTENDED (e.g. on a VM) and the user is elsewhere, add a cross-AD **`target`** (a peer clientName from the `targets` verb, or `"all"`) so the toast lands where they are. Only foreground Fusion to SHOW the user something. See [fusion-driving](skills/fusion-driving/SKILL.md) and [fusion-onboarding](skills/fusion-onboarding/SKILL.md).+4. **Background by default — automate first, NOTIFY only at a true wall.** This bridge drives the AI's workspace, not the user's screen: dismiss dialogs in the BACKGROUND (`fusion_close_window`, never `fusion_send_key`/Escape, which steals focus). AUTOMATE sign-in / dialogs yourself (see the proven sign-in sequence in [fusion-onboarding](skills/fusion-onboarding/SKILL.md)); reserve an ab **`notify_user`** for something only the user can do — a **UAC/elevation** prompt, a license accept, a **2FA/password** step you must not auto-fill, or a licensing **DECISION** that risks their other machine. **`notify_user` works directly over the bridge's direct API (ab ≥1.9.84) — call it, don't relay a payload for the AI to send.** When this bridge runs UNATTENDED (e.g. on a VM) and the user is elsewhere, add a cross-ab **`target`** (a peer clientName from the `targets` verb, or `"all"`) so the toast lands where they are. Only foreground Fusion to SHOW the user something. See [fusion-driving](skills/fusion-driving/SKILL.md) and [fusion-onboarding](skills/fusion-onboarding/SKILL.md). 5. **`ok:true` is not enough — verify.** A long op that "times out" at ~60s is usually STILL RUNNING server-side (the relay caps the request, not the work). Verify by artifacts (the saved file / a screenshot / `fusion_get_app_state`), don't blindly retry. 6. **Reuse the running Fusion — and when a restart IS needed, YOU do it, never the user.** Fusion is a singleton: `fusion_readiness` before `fusion_start`; if `ready`, just drive. But a stale/missing add-in or a wedged session needs a restart, and that is YOUR job — `fusion_stop` then `fusion_start`. Restarting Fusion and enabling the add-in are the bridge's job, not something to hand back to the user — doing it for them is the entire point of this bridge. (Add-in installs land in ALL Fusion add-in dirs — 2025+ Fusion scans `%APPDATA%\Autodesk\FusionAddins`, older builds use `...\Autodesk Fusion[ 360]\API\AddIns` — so a restart always picks it up.) 7. **Surface `_hint` verbatim + branch on `errorCode`.** Every verb returns an actionable `_hint`; relay it to the user unchanged and key your logic off the stable `errorCode`, not the prose.
VERBS.md+2−2@@ -8,7 +8,7 @@ adom-bridge fusion_<verb> '<input json>' ``` `input` / `output` below are the SCHEMA (field -> type/notes); `example` is a real input.-`timeoutSeconds` is AD's wait; `longRunning` verbs keep working after a timeout - poll `statusVerb`.+`timeoutSeconds` is ab's wait; `longRunning` verbs keep working after a timeout - poll `statusVerb`. ## Verbs by group @@ -1325,7 +1325,7 @@ _timeout 30s · long-running (poll `fusion_get_app_state`)_ ### `fusion_describe` -Self-describe every verb the bridge exposes (this list) for AD's Verbs tab + runner.+Self-describe every verb the bridge exposes (this list) for ab's Verbs tab + runner. **Input** `{}`
ad_client.py+27−27@@ -1,22 +1,22 @@-"""Outbound calls from this bridge BACK to Adom Desktop (AD) core.+"""Outbound calls from this bridge BACK to Adom Bridge (ab) core. -Why this exists: as of **AD 1.9.84** a bridge's loopback callback-(`POST $ADOM_DIRECT_API_URL/command`) reaches the FULL AD verb set - not just+Why this exists: as of **ab 1.9.84** a bridge's loopback callback+(`POST $ADOM_DIRECT_API_URL/command`) reaches the FULL ab verb set - not just `desktop_*`. That means this bridge can, on its own: * `notify_user` the user directly (it used to be "Unknown desktop command", so the old code returned a `notifyUser` PAYLOAD and asked the driving AI to relay it - a workaround, now dropped); * enumerate peer ADs on the relay (`targets`) and route ANY verb to one via a- cross-AD `target` - so a bridge running on a VM can reach the user on their+ cross-ab `target` - so a bridge running on a VM can reach the user on their LAPTOP ("notify them where they actually are"). -Everything here is BEST-EFFORT and never raises: an older AD (pre-1.9.84), a+Everything here is BEST-EFFORT and never raises: an older ab (pre-1.9.84), a missing env var, or a closed port just yields None/False and the caller falls back to its prior behavior. We CAPABILITY-PROBE (`GET /commands`) rather than hardcode, per the SDK, so this keeps working as the verb set evolves. -Auth model (from disc #78): `X-Adom-Bridge-Token` = ATTRIBUTION only (AD binds+Auth model (from disc #78): `X-Adom-Bridge-Token` = ATTRIBUTION only (ab binds 127.0.0.1 and trusts-by-install), so calls run ungated; a stale token 403s, so we re-read it from the env on every call. """@@ -27,8 +27,8 @@ import urllib.request import urllib.error _TIMEOUT = 8-# Cache the capability set for the process lifetime (the AD verb surface doesn't-# change under a running AD). None = not-yet-probed / unreachable.+# Cache the capability set for the process lifetime (the ab verb surface doesn't+# change under a running ab). None = not-yet-probed / unreachable. _caps_cache = None _self_name_cache = None @@ -48,22 +48,22 @@ def _raw_get(url, timeout=4): def _looks_like_ad_api(data) -> bool:- """Does a /commands response look like AD's verb catalog (so we don't bind a random service)?"""+ """Does a /commands response look like ab's verb catalog (so we don't bind a random service)?""" if isinstance(data, dict): if isinstance(data.get("commands"), list): return True- # AD's /commands also carries a _hint mentioning POST /command+ # ab's /commands also carries a _hint mentioning POST /command if "command" in (data.get("_hint") or "").lower(): return True return isinstance(data, list) def _discover_direct_api():- """Find AD's loopback direct-API base by port-probing, for the case where AD did NOT inject- ADOM_DIRECT_API_URL into our spawn env (seen live on an AD 1.9.103 Hyper-V VM: the direct API+ """Find ab's loopback direct-API base by port-probing, for the case where ab did NOT inject+ ADOM_DIRECT_API_URL into our spawn env (seen live on an ab 1.9.103 Hyper-V VM: the direct API WAS up on 127.0.0.1 but the env var was absent, so available() short-circuited to False and the bridge fell back to slow CLI relays - John caught it 2026-07-14). We enumerate the loopback TCP- ports adom-desktop.exe is LISTENING on and probe each `/commands`; the one that returns AD's verb+ ports adom-desktop.exe is LISTENING on and probe each `/commands`; the one that returns ab's verb catalog is the direct API. Cached for the process lifetime. Windows-only + best-effort; never raises.""" global _discovered_base if _discovered_base: # cache SUCCESS only; a transient miss must not stick forever@@ -96,8 +96,8 @@ def _discover_direct_api(): def _base_url():- """The loopback direct-API base: the URL AD injects into our spawn env, else one we DISCOVER by- probing adom-desktop's loopback ports (AD sometimes fails to inject the env var even though the+ """The loopback direct-API base: the URL ab injects into our spawn env, else one we DISCOVER by+ probing adom-desktop's loopback ports (ab sometimes fails to inject the env var even though the API is up - see _discover_direct_api). None only if neither yields a reachable API.""" env = (os.environ.get("ADOM_DIRECT_API_URL") or "").rstrip("/") if env:@@ -106,7 +106,7 @@ def _base_url(): def _token():- # Re-read every call: after an AD restart the old token 403s.+ # Re-read every call: after an ab restart the old token 403s. return os.environ.get("ADOM_BRIDGE_TOKEN") or "" @@ -158,12 +158,12 @@ def _names_from_list(items): def _parse_command_names(data): """Extract the full verb-name set from a /commands response, tolerant of BOTH the legacy- `{"commands":[...]}` shape AND the current AD (1.9.x) shape, where verbs are split across:+ `{"commands":[...]}` shape AND the current ab (1.9.x) shape, where verbs are split across: topLevel: ["notify_user", "targets", ...] (flat name list) cliRequired: ["pull_file", "shell_execute", ...] (flat name list) bridges: [{name, verbs:[...]}, ...] (per-bridge verb lists) desktop: {commands:["screenshot_window", ...]} (SHORT names; real verb = "desktop_"+name)- Without this, capabilities() returned None on every current AD, so available() was always False+ Without this, capabilities() returned None on every current ab, so available() was always False and the bridge silently fell back to slow CLI relays (root-caused live, John 2026-07-14).""" names = set() if isinstance(data, list):@@ -188,7 +188,7 @@ def _parse_command_names(data): def capabilities():- """Set of AD command names from `GET /commands` (cached). None if unreachable/unparseable."""+ """Set of ab command names from `GET /commands` (cached). None if unreachable/unparseable.""" global _caps_cache if _caps_cache is not None: return _caps_cache@@ -212,11 +212,11 @@ def has(command: str): def call(command: str, args=None, target=None, timeout=_TIMEOUT):- """Dispatch ANY AD verb back through the direct API. Returns the parsed JSON+ """Dispatch ANY ab verb back through the direct API. Returns the parsed JSON response dict, or None on any failure (never raises). - target: a peer AD clientName (from `peers()`), or "all" to fan out. Omit to- run on THIS AD (the one hosting the bridge)."""+ target: a peer ab clientName (from `peers()`), or "all" to fan out. Omit to+ run on THIS ab (the one hosting the bridge).""" payload = {"command": command} if args: payload["args"] = args@@ -226,7 +226,7 @@ def call(command: str, args=None, target=None, timeout=_TIMEOUT): def self_name():- """This AD's own clientName (to exclude it from peer routing). Best-effort."""+ """This ab's own clientName (to exclude it from peer routing). Best-effort.""" global _self_name_cache if _self_name_cache is not None: return _self_name_cache or None@@ -239,7 +239,7 @@ def self_name(): def peers(): """clientNames of the OTHER ADs on the relay (excludes self). [] if none/unknown.""" res = call("targets") or {}- # tolerate shape drift: the peer list is under `clients` on current AD, `targets` on older.+ # tolerate shape drift: the peer list is under `clients` on current ab, `targets` on older. tlist = None if isinstance(res, dict): tlist = res.get("targets") or res.get("clients")@@ -255,12 +255,12 @@ def peers(): def notify(title, body, level="info", buttons=None, target=None, reach_user=False):- """Fire an AD `notify_user` toast. Returns the response dict, or None if the- direct API is unreachable or AD is too old to have notify_user (caller falls+ """Fire an ab `notify_user` toast. Returns the response dict, or None if the+ direct API is unreachable or ab is too old to have notify_user (caller falls back to returning a notifyUser payload for the driving AI to relay). reach_user: when this bridge may be running UNATTENDED (e.g. on a VM) while the- user is on another machine, set True to also reach peer ADs. Since AD can't yet+ user is on another machine, set True to also reach peer ADs. Since ab can't yet resolve which peer is "attended", we fan out to "all" when peers exist so the toast lands wherever the user actually is. `target` (explicit) overrides this. """
page.json+7−10@@ -11,10 +11,6 @@ "uninstall": "./uninstall.sh" }, "tags": [- "ab",- "bridge",- "fusion",- "desktop-app", "fusion360", "autodesk", "cad",@@ -79,10 +75,7 @@ "build a basic parts library", "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"+ "set up the fusion bridge" ], "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.",@@ -111,5 +104,9 @@ "label": "Part volume", "prompt": "How much material does each printed part in this assembly use?" }- ]-}+ ],+ "author": {+ "name": "John Lauer",+ "email": "[email protected]"+ }+}
skills/fusion-libraries/LIBRARY_FINDINGS.md+1−1@@ -111,7 +111,7 @@ Fusion error dialogs (e.g. "<file>.lbr has errors and cannot be opened") are **o NOT enumerate in `desktop_list_windows` and are invisible to a plain capture of the main window. A `fusion_*` verb can return `success:true` while an error dialog is actually up. -**`desktop_screenshot_window {hwnd}` (AD v1.8.177+) captures them automatically** and returns them at the+**`desktop_screenshot_window {hwnd}` (ab v1.8.177+) captures them automatically** and returns them at the TOP LEVEL: **`ownedPopupCount`** (always present; 0 = none) + **`_screenshots[]`** (each owned popup: `title`, `kind:"owned_popup"`, `localSafePath`, `rect`, `coordMap`). The CLI auto-pulls each popup PNG.