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
v1.6.8: stale add-in detection (#55) + SDK re-audit (verbs sync, 8 conventions)
9 files changed
+194−65
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.6.7+1.6.8
CHANGELOG.md+16@@ -3,6 +3,22 @@ All notable changes to the Adom Desktop Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/adom-desktop-fusion-bridge · GitHub backup: `adom-inc/adom-desktop-fusion-bridge` +## 1.6.8 - 2026-06-30++- **Stale add-in is no longer a SILENT failure (fixes #55, Drew's report).** The add-in now REPORTS+ its version (`AdomBridge.manifest` → `/health` + `/status`), and the bridge compares it to the+ version it bundles. `fusion_readiness` and `fusion_addin_status` now return `addinVersion` /+ `expectedAddinVersion` / `addinStale` (+ a re-sync `_hint`), and any add-in command the running+ add-in is too old to know (e.g. `open_by_urn`, which silently broke `fusion_aps_open` for Drew) now+ comes back with `errorCode:"addin_stale"` and the exact fix (`fusion_stop` then `fusion_start` to+ re-sync from cache with Fusion closed). Add-in manifest bumped 1.0.2 → 1.0.3.+- **Bridge-SDK re-audit against the updated SDK.** (1) `bridge.json` `verbs[]` re-synced to the+ authoritative `fusion_describe` catalog (was stale: 40 → 53 verbs; added the library/3D + board ++ app-state verbs; `statusVerb` `fusion_get_app_state` is now listed; dropped the dead+ `fusion_apply_instapcb_rules`). (2) The USER skill's cold-start section now encodes all **8** SDK+ conventions (was 5), led by an `errorCode` response→meaning→action table (incl. the new+ `addin_stale` row) and the reuse-don't-relaunch + surface-`_hint`-verbatim conventions.+ ## 1.6.7 - 2026-06-30 - **AD 1.9.63 alignment (discussion #57).** (1) The bridge HTTP server now honors **`ADOM_BIND_HOST`**
SKILL.md+24−15@@ -14,22 +14,31 @@ adom-desktop fusion_<action> '<json_args>' The leading `fusion_` routes to the Fusion plugin; `<action>` is the plugin-side name. -## Read this before you panic (cold-start + conventions)+## FIRST-TIME / COLD-START — read before you panic -1. **Check readiness first, don't launch blindly.** Call **`fusion_readiness`** (or AD's- `bridge_readiness`) - it reports `installed` / `running` / `ready` WITHOUT launching anything.- `installed:false` → the user needs Fusion (see [fusion-onboarding](skills/fusion-onboarding/SKILL.md));- AD **detects** Fusion (never installs it) and **provisions Python itself** (a portable no-UAC- 3.12.13 runtime if none is on PATH - never bootstrap your own; check AD's `runtimes` verb for- state). `running:false` → `fusion_start` (it blocks until the add-in is ready). `ready:true` → drive.-2. **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 / screenshots), don't blindly retry.-3. **Never fly blind past a dialog.** Mutating verbs return a dialog array; analyze it and dismiss in- the BACKGROUND (`fusion_close_window`, never `fusion_send_key`/Escape which steals focus). See- [fusion-driving](skills/fusion-driving/SKILL.md).-4. **Never write files to a shared cloud ROOT.** Uploads default to an `Adom AI Workspace` folder; see- [fusion-cloud-hygiene](skills/fusion-cloud-hygiene/SKILL.md).-5. Read each verb's `_hint` in its OUTPUT - it tells you the next step.+A fresh laptop may not have Fusion running (or may run a stale add-in). That is EXPECTED, not defeat.+Branch on the stable `errorCode`, never the prose:++| response (`errorCode`) | what it means | what you do |+|---|---|---|+| `ready:true` (fusion_readiness) | Fusion up + add-in current | drive |+| `running:false` / `fusion_not_running` | installed, not launched | `fusion_start` (blocks until the add-in is ready), retry |+| `installed:false` | no Fusion host app | [fusion-onboarding](skills/fusion-onboarding/SKILL.md); AD DETECTS Fusion (never installs it) |+| `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) |+| `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.++## 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.+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 — don't disrupt the user.** 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); only foreground to SHOW the user something. See [fusion-driving](skills/fusion-driving/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, don't relaunch.** Fusion is a singleton — `fusion_readiness` before `fusion_start` (especially after a compaction); if it's already `ready`, just drive. Relaunching churns state.+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.+8. **Never write files to a shared cloud ROOT.** Uploads default to an `Adom AI Workspace` folder; see [fusion-cloud-hygiene](skills/fusion-cloud-hygiene/SKILL.md). ## [!] Electronics PCBs: ALWAYS open the PROJECT file (never the schematic / board / 3D)
addin/AdomBridge/AdomBridge.manifest+1−1@@ -6,7 +6,7 @@ "description": { "": "Adom Desktop Conduit bridge add-in. Exposes a localhost HTTP API for external automation of Fusion 360." },- "version": "1.0.2",+ "version": "1.0.3", "runOnStartup": true, "supportedOS": "windows" }
addin/AdomBridge/http_server.py+19−1@@ -20,6 +20,7 @@ Threading model: """ import json+import os import threading import time import uuid@@ -31,6 +32,21 @@ from .commands.cloud_documents import get_walk_progress PORT = 8774 +# Add-in version — read from the sibling manifest so /health + /status can+# REPORT it. The bridge compares this against the version it bundles to catch a+# STALE add-in (issue #55: an old Roaming add-in that never re-synced from the+# bridge cache, which made fusion_aps_open fail SILENTLY). Sourced from the+# manifest so a single bump is the source of truth (no hardcode to drift).+def _read_addin_version():+ try:+ mf = os.path.join(os.path.dirname(__file__), "AdomBridge.manifest")+ with open(mf, "r", encoding="utf-8") as f:+ return (json.load(f) or {}).get("version", "unknown")+ except Exception:+ return "unknown"++ADDIN_VERSION = _read_addin_version()+ _server = None _server_thread = None _custom_event_id = None@@ -256,6 +272,7 @@ class AdomBridgeHandler(BaseHTTPRequestHandler): self._respond(200, { "status": "ok", "addin": "AdomBridge",+ "version": ADDIN_VERSION, "port": PORT, "main_thread": main_status, "main_thread_locked": main_lock_held,@@ -274,6 +291,7 @@ class AdomBridgeHandler(BaseHTTPRequestHandler): elapsed = round(time.time() - busy["started_at"], 1) resp = { "busy": True,+ "version": ADDIN_VERSION, "busyCommand": busy["command"], "elapsedSeconds": elapsed, "requestId": busy["request_id"],@@ -295,7 +313,7 @@ class AdomBridgeHandler(BaseHTTPRequestHandler): ) self._respond(200, resp) else:- self._respond(200, {"busy": False})+ self._respond(200, {"busy": False, "version": ADDIN_VERSION}) else: self._respond(404, {"error": "Not found"})
adom-bridge-fusion-manifest.json+5−5@@ -1,13 +1,13 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.6.7",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.7/adom-bridge-fusion-v1.6.7.zip",- "sha256": "c9aa67c269f6da763d08ded74a33eb5e120a5e1894eed2989964793a892613ee",- "size": 294279,+ "version": "1.6.8",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.8/adom-bridge-fusion-v1.6.8.zip",+ "sha256": "9dc5bed4d8a7db0dbdc7fc4863c42d12b95b0747e9081dbb8ebd4cb6661428da",+ "size": 297014, "verbPrefixes": [ "fusion_" ], "healthEndpoint": "/status",- "released_at": "2026-06-30T16:30:00Z"+ "released_at": "2026-06-30T17:15:00Z" }
bridge.json+47−33@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.6.7",+ "version": "1.6.8", "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/adom-desktop-fusion-bridge", "author": "Adom Inc.",@@ -20,45 +20,59 @@ "fusion_" ], "verbs": [- "fusion_describe",- "fusion_readiness",- "fusion_start",- "fusion_kill",- "fusion_stop",- "fusion_close_window",- "fusion_apply_instapcb_rules",- "fusion_load_design_rules",- "fusion_export_step",- "fusion_export_iges",- "fusion_export_stl",- "fusion_export_3mf",- "fusion_export_usdz",- "fusion_export_obj",- "fusion_export_dxf",- "fusion_export_dwg",- "fusion_export_gerbers",- "fusion_walk_cloud_tree",- "fusion_search_cloud_files",- "fusion_dismiss_blocking_dialogs",- "fusion_aps_status",- "fusion_aps_set_client_id",- "fusion_aps_signin",- "fusion_aps_search",- "fusion_aps_get", "fusion_aps_browse",+ "fusion_aps_create_folder",+ "fusion_aps_download",+ "fusion_aps_file_info",+ "fusion_aps_get", "fusion_aps_open",- "fusion_open_by_urn",- "fusion_run_modeling_script", "fusion_aps_recent",- "fusion_aps_file_info",+ "fusion_aps_search",+ "fusion_aps_set_client_id",+ "fusion_aps_signin",+ "fusion_aps_status", "fusion_aps_upload",- "fusion_aps_create_folder",- "fusion_aps_download", "fusion_aps_versions",- "fusion_electron_zoom",+ "fusion_attach_3d_package",+ "fusion_board_info",+ "fusion_build_library_3d",+ "fusion_capture_library_views",+ "fusion_cleanup_cloud_files",+ "fusion_close_window",+ "fusion_describe",+ "fusion_detect_layers",+ "fusion_dismiss_blocking_dialogs", "fusion_electron_pan",+ "fusion_electron_run", "fusion_electron_select",- "fusion_show_schematic"+ "fusion_electron_zoom",+ "fusion_export_3mf",+ "fusion_export_bom",+ "fusion_export_cpl",+ "fusion_export_dwg",+ "fusion_export_dxf",+ "fusion_export_gerbers",+ "fusion_export_iges",+ "fusion_export_obj",+ "fusion_export_step",+ "fusion_export_stl",+ "fusion_export_usdz",+ "fusion_get_app_state",+ "fusion_kill",+ "fusion_load_design_rules",+ "fusion_make_3d_package",+ "fusion_open_by_urn",+ "fusion_open_lbr",+ "fusion_readiness",+ "fusion_run_modeling_script",+ "fusion_search_cloud_files",+ "fusion_set_design_rules",+ "fusion_show_2d_board",+ "fusion_show_3d_board",+ "fusion_show_schematic",+ "fusion_start",+ "fusion_stop",+ "fusion_walk_cloud_tree" ], "dependencies": { "python": ">=3.11",
page.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-desktop-fusion-bridge",- "version": "1.6.8",+ "version": "1.6.9", "type": "app", "description": "Drive Autodesk Fusion 360 from the cloud via Adom Desktop: 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 with browse/recent/versions, cloud download/upload and folder creation, and in-app parametric modeling. Never-charge: APS calls are capped to the free tier.", "dependencies": {},
server.py+80−8@@ -47,6 +47,46 @@ try: except Exception: BRIDGE_VERSION = "unknown" +# The add-in version this bridge BUNDLES (the one install_addin syncs into+# Fusion's Roaming AddIns dir). Read from our bundled manifest so it can't drift+# from a hardcode. Compared against the RUNNING add-in's reported version to+# catch a stale add-in (issue #55) - the running copy only re-syncs when Fusion+# is closed on bridge start, so a user who never restarted Fusion keeps an old+# add-in and commands like fusion_aps_open fail silently. We make it LOUD.+try:+ EXPECTED_ADDIN_VERSION = (json.loads(+ (Path(__file__).parent / "addin" / "AdomBridge" / "AdomBridge.manifest").read_text(encoding="utf-8")+ ) or {}).get("version", "unknown")+except Exception:+ EXPECTED_ADDIN_VERSION = "unknown"+++def _addin_staleness(reported_version) -> dict:+ """Compare the running add-in's reported version to the bundled one.++ Returns {addinVersion, expectedAddinVersion, addinStale, _staleHint?}. A+ stale add-in is the #55 silent-failure: re-sync it by RESTARTING Fusion+ (fusion_stop then fusion_start) so the bridge's on-start install_addin can+ overwrite the Roaming copy (which is file-locked while Fusion runs)."""+ info = {+ "addinVersion": reported_version or "unknown",+ "expectedAddinVersion": EXPECTED_ADDIN_VERSION,+ }+ stale = (+ reported_version not in (None, "unknown")+ and EXPECTED_ADDIN_VERSION not in (None, "unknown")+ and str(reported_version) != str(EXPECTED_ADDIN_VERSION)+ )+ info["addinStale"] = bool(stale)+ if stale:+ info["_staleHint"] = (+ f"STALE add-in: Fusion is running add-in v{reported_version} but this bridge "+ f"bundles v{EXPECTED_ADDIN_VERSION}. Newer verbs (e.g. fusion_aps_open/open_by_urn) "+ f"can fail silently. Fix: fusion_stop then fusion_start - the bridge re-syncs the "+ f"add-in from its cache on start (only possible with Fusion CLOSED)."+ )+ return info+ # Populated at startup fusion_info = None @@ -568,6 +608,25 @@ def _handle_fusion_readiness(fusion_info: dict, args: dict) -> dict: installed = bool(fusion_info.get("installed")) running = _is_fusion_running() if installed else False ready = installed and running++ # When Fusion is up, also check the add-in isn't STALE (issue #55) so a fresh+ # readiness call surfaces the mismatch instead of a later verb failing silently.+ stale_info = {}+ if running:+ addin_status = _check_addin_status(timeout=1.0)+ if addin_status is not None:+ stale_info = _addin_staleness(addin_status.get("version"))++ if ready and stale_info.get("addinStale"):+ hint = stale_info["_staleHint"]+ elif ready:+ hint = "READY - drive Fusion (fusion_open_lbr / build_library_3d / etc.)."+ elif installed:+ hint = "Fusion is INSTALLED but not running - call fusion_start (blocks until the add-in is ready), then retry."+ else:+ hint = ("Fusion 360 is NOT installed. AD detects (never installs) the host app via bridge.json 'detect'; "+ "ask the user to install Fusion from autodesk.com. (AD provisions Python itself - no bridge bootstrap.)")+ return { "success": True, "hostApp": "Fusion 360",@@ -575,13 +634,8 @@ def _handle_fusion_readiness(fusion_info: dict, args: dict) -> dict: "running": running, "ready": ready, "bridgeVersion": BRIDGE_VERSION,- "_hint": (- "READY - drive Fusion (fusion_open_lbr / build_library_3d / etc.)." if ready else- ("Fusion is INSTALLED but not running - call fusion_start (blocks until the add-in is ready), then retry."- if installed else- "Fusion 360 is NOT installed. AD detects (never installs) the host app via bridge.json 'detect'; "- "ask the user to install Fusion from autodesk.com. (Python is auto-installed by AD if missing.)")- ),+ **stale_info,+ "_hint": hint, } @@ -917,7 +971,12 @@ def _check_addin_status(timeout: float = 3.0) -> dict | None: def _handle_addin_status() -> dict: """Bridge-level handler for addin_status — wraps /status in standard format.""" status = _check_addin_status(timeout=3.0) or {"busy": False}- return {"success": True, "output": json.dumps(status), **status}+ stale = _addin_staleness(status.get("version"))+ status = {**status, **stale}+ result = {"success": True, "output": json.dumps(status), **status}+ if stale.get("addinStale"):+ result["_hint"] = stale["_staleHint"]+ return result def _proxy_to_addin(command: str, args: dict, timeout: int = 30) -> dict:@@ -943,6 +1002,19 @@ def _proxy_to_addin(command: str, args: dict, timeout: int = 30) -> dict: and "timed out" in result.get("error", "").lower()): return _diagnose_addin_timeout(command, result) + # If the add-in rejected the command as unknown/unsupported, it may be a+ # STALE add-in that predates this verb (issue #55 - Drew's silent failure:+ # old add-in had no open_by_urn). Enrich with a version comparison + the+ # re-sync hint + a stable errorCode so the failure is LOUD, not silent.+ if not result.get("success"):+ err = (result.get("error") or "").lower()+ if any(s in err for s in ("unknown command", "no such command", "not supported", "unsupported command")):+ stale = _addin_staleness((_check_addin_status(timeout=1.0) or {}).get("version"))+ if stale.get("addinStale"):+ result["errorCode"] = "addin_stale"+ result["_hint"] = stale["_staleHint"]+ result.update({k: stale[k] for k in ("addinVersion", "expectedAddinVersion", "addinStale")})+ return result except urllib.error.URLError as e: