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
1.6.56: remember the Autodesk sign-in browser+profile
7 files changed
+195−21
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.6.55+1.6.56
CHANGELOG.md+14@@ -3,6 +3,20 @@ 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.56 - 2026-07-08++- **APS sign-in now REMEMBERS the browser+profile that authed to Autodesk and reuses it, instead of+ firing `webbrowser.open` at the OS default (John: "power users have tons of browsers with tons of+ profiles ... cache what browser you used when you successfully logged in ... don't be lazy").** The+ old code blindly opened the OS default browser, which on a multi-browser box is usually the wrong+ one (no warm Autodesk/SSO session / wrong profile) and forces a needless fresh login. Now+ `start_signin`: (1) launches a browser+profile passed in the call, else (2) the REMEMBERED one+ (`~/.adom/fusion-aps/signin-browser.json`) via a real `--profile-directory`/`--new-window` launch,+ else (3) opens NOTHING and returns `needsExtensionOpen:true` so the caller drives `authUrl` in the+ user's real browser via `nbrowser_open_window` (new window) — the OS default is only used when the+ caller explicitly passes `allowDefaultBrowser:true`. New verbs `fusion_aps_set_browser` /+ `fusion_aps_get_browser` / `fusion_aps_forget_browser` persist the choice across sessions.+ ## 1.6.55 - 2026-07-07 - **Universal error-dialog guard, baked into every mutating/open verb (John: "you have to
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.6.55",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.55/adom-bridge-fusion-v1.6.55.zip",- "sha256": "f1cf50ad2e471dc9ba6bb1e9cf2184562b382c03d8efc10fcca3b7ead58770cf",- "size": 339847,+ "version": "1.6.56",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.56/adom-bridge-fusion-v1.6.56.zip",+ "sha256": "613ea7473dc62606aa44cbd93de69d9e5ef86bf15fe4813a3092ad6d1f8af2a6",+ "size": 343646, "verbPrefixes": [ "fusion_" ],
aps.py+157−13@@ -27,6 +27,7 @@ import hashlib import json import os import secrets+import subprocess import threading import time import urllib.parse@@ -77,6 +78,64 @@ APS_DIR = Path(os.path.expanduser("~")) / ".adom" / "fusion-aps" CONFIG_FILE = APS_DIR / "config.json" TOKENS_FILE = APS_DIR / "tokens.json" USAGE_FILE = APS_DIR / "usage.json"+# Which browser+profile the user's Autodesk sign-in ACTUALLY succeeded in. Power users+# run many browsers and many profiles; the OS default is usually NOT the one holding the+# warm Autodesk/SSO session. So we REMEMBER the browser+profile that worked and reuse it,+# instead of blindly firing webbrowser.open (which hits the OS default and forces a fresh+# login in the wrong place). Set on a successful driven sign-in; reused on every later one.+SIGNIN_BROWSER_FILE = APS_DIR / "signin-browser.json"+++def load_signin_browser() -> dict | None:+ """The remembered {name, exe, profileDir, newWindow} that last authed to Autodesk, or None."""+ try:+ return json.loads(SIGNIN_BROWSER_FILE.read_text(encoding="utf-8"))+ except Exception:+ return None+++def save_signin_browser(pref: dict) -> bool:+ """Remember the browser+profile that just worked, so the next sign-in reuses it."""+ try:+ APS_DIR.mkdir(parents=True, exist_ok=True)+ keep = {k: pref.get(k) for k in ("name", "exe", "profileDir", "newWindow", "how")+ if pref.get(k) is not None}+ keep["updatedAt"] = time.strftime("%Y-%m-%dT%H:%M:%S")+ SIGNIN_BROWSER_FILE.write_text(json.dumps(keep, indent=2), encoding="utf-8")+ return True+ except Exception:+ return False+++def forget_signin_browser() -> bool:+ try:+ SIGNIN_BROWSER_FILE.unlink()+ return True+ except FileNotFoundError:+ return True+ except Exception:+ return False+++def _launch_url_in_browser(pref: dict, url: str) -> bool:+ """Launch a SPECIFIC browser+profile (not the OS default) at url. Chromium browsers+ take --profile-directory + --new-window; a bare exe still opens its default profile.+ Returns True if the process spawned."""+ exe = pref.get("exe")+ if not exe or not os.path.exists(exe):+ return False+ cmd = [exe]+ prof = pref.get("profileDir")+ if prof:+ cmd.append(f"--profile-directory={prof}")+ if pref.get("newWindow", True):+ cmd.append("--new-window")+ cmd.append(url)+ try:+ subprocess.Popen(cmd, close_fds=True)+ return True+ except Exception:+ return False # ── NEVER-CHARGE GUARANTEE (hard rule) ─────────────────────────────────────── # A user must NEVER be charged money — a card on file is only ever for Autodesk's@@ -304,8 +363,9 @@ class _CallbackHandler(BaseHTTPRequestHandler): pass -def start_signin() -> dict:+def start_signin(args: dict | None = None) -> dict: global _signin_state, _active_redirect_uri+ args = args or {} client_id = get_client_id() if not client_id: return {"success": False, "error": "No APS Client ID configured.",@@ -367,23 +427,72 @@ def start_signin() -> dict: # re-login every time. } auth_url = AUTH_URL + "?" + urllib.parse.urlencode(params)- try:- webbrowser.open(auth_url)- except Exception:++ # ── SMART BROWSER CHOICE (John, 2026-07-08: "power users have tons of browsers with tons+ # of profiles ... cache what browser you used when you successfully logged in ... don't+ # be lazy") ─────────────────────────────────────────────────────────────────────────+ # Priority: (1) a browser+profile passed in THIS call, (2) the one we REMEMBERED from the+ # last successful sign-in, (3) if neither AND the caller can drive the extension, open+ # NOTHING and tell it to open in the user's REAL browser (new window) via nbrowser_* — that+ # window holds the warm Autodesk/SSO session. Only fall back to the OS default browser when+ # explicitly allowed, because on a multi-browser machine the default is usually the WRONG+ # one (no session / wrong profile) and forces a needless fresh login.+ passed = None+ if args.get("exe"):+ passed = {"name": args.get("browser") or args.get("name"), "exe": args.get("exe"),+ "profileDir": args.get("profileDir"), "newWindow": args.get("newWindow", True)}+ pref = passed or load_signin_browser()+ opened_via = None+ opened_pref = None+ if pref and _launch_url_in_browser(pref, auth_url):+ opened_via = "remembered_browser" if not passed else "specified_browser"+ opened_pref = {k: pref.get(k) for k in ("name", "exe", "profileDir")}+ # A browser passed explicitly is now the remembered one (it's what the caller chose to+ # drive). A previously-remembered one is refreshed to keep its timestamp warm.+ save_signin_browser(pref)+ elif args.get("allowDefaultBrowser"):+ # Legacy fallback — only when the caller opts in (e.g. no extension available). try:- os.startfile(auth_url) # noqa: Windows+ webbrowser.open(auth_url)+ opened_via = "os_default_browser" except Exception:- pass+ try:+ os.startfile(auth_url) # noqa: Windows+ opened_via = "os_default_browser"+ except Exception:+ opened_via = None+ else:+ opened_via = "none_use_extension"++ if opened_via in ("remembered_browser", "specified_browser"):+ _hint = (f"Opened the Autodesk consent in the REMEMBERED browser ({opened_pref.get('name') or 'cached'}"+ f"{'/' + opened_pref['profileDir'] if opened_pref.get('profileDir') else ''}) — the one that "+ "authed last time. A background listener on port "+ f"{bound_port} captures the token once you approve. Poll fusion_aps_status until signedIn:true.")+ elif opened_via == "os_default_browser":+ _hint = ("Opened in the OS DEFAULT browser (fallback). On a multi-browser machine this may be the "+ "wrong profile; prefer driving authUrl in the user's real browser via nbrowser_open_window "+ "(new window), then call fusion_aps_set_browser to remember it. Poll fusion_aps_status.")+ else:+ _hint = ("NO browser opened on purpose. This machine has no REMEMBERED Autodesk browser yet, and a "+ "multi-browser box's OS default is usually the wrong one. OPEN authUrl in the user's REAL "+ "browser via a NEW window (adom-desktop nbrowser_open_window {url:authUrl}) so its warm "+ "Autodesk/SSO session is used, then call fusion_aps_set_browser {name,exe,profileDir} to "+ "REMEMBER it for next time. A listener on port "+ f"{bound_port} captures the token once you approve. Poll fusion_aps_status until signedIn:true.") return { "success": True,- "output": "Opened the Autodesk sign-in page in your browser. Approve access, "- "then call fusion_aps_status to confirm.",+ "output": ("Autodesk sign-in started. " + (+ "Approve access in the opened browser, then call fusion_aps_status."+ if opened_via and opened_via != "none_use_extension"+ else "Open authUrl in the user's real browser (new window) to approve.")), "authUrl": auth_url, "redirectUri": _active_redirect_uri,- "_hint": "A background listener on port "- f"{bound_port} will capture the token automatically once you approve. "- "Poll fusion_aps_status until signedIn:true (up to ~3 min).",+ "openedVia": opened_via,+ "openedBrowser": opened_pref,+ "needsExtensionOpen": opened_via == "none_use_extension",+ "_hint": _hint, } @@ -443,14 +552,49 @@ def handle_set_client_id(args: dict) -> dict: def handle_signin(args: dict) -> dict:- res = start_signin()+ res = start_signin(args or {}) # Move custom fields under data so the relay forwards them. if res.get("success"): res["data"] = {"authUrl": res.pop("authUrl", None),- "redirectUri": res.pop("redirectUri", None)}+ "redirectUri": res.pop("redirectUri", None),+ "openedVia": res.pop("openedVia", None),+ "openedBrowser": res.pop("openedBrowser", None),+ "needsExtensionOpen": res.pop("needsExtensionOpen", False)} return res +def handle_set_browser(args: dict) -> dict:+ """Remember the browser+profile that successfully authed to Autodesk, so every later+ fusion_aps_signin reuses it instead of the OS default. Call this right after a driven+ sign-in succeeds (with the browser you opened authUrl in). Fields: name, exe, profileDir,+ newWindow. `exe` is the browser binary on the host; profileDir is e.g. 'Default'/'Profile 2'."""+ exe = (args.get("exe") or "").strip()+ pref = {"name": args.get("name") or args.get("browser"), "exe": exe or None,+ "profileDir": args.get("profileDir"), "newWindow": args.get("newWindow", True),+ "how": args.get("how") or ("extension" if not exe else "launch")}+ if not exe and pref["how"] != "extension":+ return {"success": False, "error": "Provide exe (browser binary path) or how:'extension'.",+ "_hint": "e.g. {name:'Edge', exe:'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe', profileDir:'Default'}"}+ ok = save_signin_browser(pref)+ return {"success": ok, "output": f"Remembered Autodesk sign-in browser: {pref.get('name') or exe or pref['how']}.",+ "data": {"saved": ok, "browser": load_signin_browser()}}+++def handle_get_browser(args: dict) -> dict:+ """Show the remembered Autodesk sign-in browser (None if not set yet)."""+ pref = load_signin_browser()+ return {"success": True, "output": ("Remembered: " + (pref.get("name") or pref.get("exe") or pref.get("how"))+ if pref else "No Autodesk sign-in browser remembered yet."),+ "data": {"browser": pref}}+++def handle_forget_browser(args: dict) -> dict:+ """Forget the remembered browser (next sign-in re-detects / uses the extension)."""+ ok = forget_signin_browser()+ return {"success": ok, "output": "Forgot the remembered Autodesk sign-in browser." if ok else "Could not clear it.",+ "data": {"cleared": ok}}++ def handle_get(args: dict) -> dict: """Raw authenticated GET — dev/verify helper for nailing the APS grammar.""" path = args.get("path")
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.6.55",+ "version": "1.6.56", "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.",
describe.py+15−2@@ -27,6 +27,11 @@ _T = [ ("fusion_readiness", "FAST readiness check (does NOT launch Fusion): is the Fusion host app installed + running + the bridge ready to drive? AD DETECTS Fusion (never installs it) via bridge.json 'detect', and auto-installs Python if missing (AD >=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 AD's bridge_readiness.", {}, {"installed": "bool", "running": "bool", "ready": "bool", "hostApp": "str", "bridgeVersion": "str"}, 15, None, False, {}), + ("fusion_new_electronics_from_eagle", "Import a legacy EAGLE .sch (+ paired .brd) into a NEW Fusion electronics design so the parts INSTANTIATE (schematic + populated board, lands in the PCB editor). The ONLY way to author a board from EAGLE source: Document.newDesignFromLocal opens the schematic editor but does NOT instantiate parts, and the Fusion-native .fsch/.fbrd container is opaque binary you can't build offline. This fires Fusion's own ImportSCHAndBRDCmd and drives BOTH native Open dialogs (the .sch then the .brd) in the BACKGROUND via desktop_ui_click by accessible name (no foreground). Stage both files to Windows first (send_files). After: fusion_show_2d_board, RATSNEST + 'AUTO ;' to autoroute, fusion_show_3d_board for 3D.",+ {"schPath": "required str (Windows path to .sch)", "brdPath": "optional str (defaults to sibling .brd)"},+ {"success": "bool", "imported": "list"}, 200, None, False,+ {"schPath": "C:/Users/me/adom-lib/MyBoard.sch", "brdPath": "C:/Users/me/adom-lib/MyBoard.brd"}),+ ("fusion_notify_owner", "LAST-RESORT human escalation: toast the user's MAIN computer (fans out cross-AD to every peer on the relay, so it reaches them even when this bridge runs on an unattended VM). ONLY for true human walls - password/2FA entry, UAC elevation, a physical step. Exhaust programmatic options first (UIA background clicks, seat auto-resolve, warm-SSO sign-in). After sending, WAIT and poll fusion_readiness; do not re-toast within a few minutes.", {"title": "optional str", "body": "required str", "level": "optional str info|warning|error"}, {"success": "bool", "targets": "list"}, 20, None, False,@@ -37,8 +42,16 @@ _T = [ {}, {"configured": "bool", "signedIn": "bool", "neverCharge": "bool"}, 20, None, False, {}), ("fusion_aps_set_client_id", "Set the org's APS (PKCE) client id used for cloud search.", {"clientId": "required str"}, {"success": "bool"}, 20, None, False, {"clientId": "D5nn...EPAq"}),- ("fusion_aps_signin", "Open the Autodesk sign-in in the browser; a background listener captures the token.",- {}, {"success": "bool", "authUrl": "str"}, 30, "fusion_aps_status", False, {}),+ ("fusion_aps_signin", "Open the Autodesk sign-in in the REMEMBERED browser+profile (not the OS default); a background listener captures the token. Pass {exe,profileDir,name} to use+remember a specific browser, or {allowDefaultBrowser:true} to fall back to the OS default. If nothing is remembered, returns needsExtensionOpen so the caller opens authUrl in the user's real browser via nbrowser_open_window.",+ {"exe": "optional str (browser binary to launch+remember)", "profileDir": "optional str (e.g. 'Default','Profile 2')", "name": "optional str", "allowDefaultBrowser": "optional bool"},+ {"success": "bool", "authUrl": "str", "openedVia": "str", "needsExtensionOpen": "bool"}, 30, "fusion_aps_status", False, {}),+ ("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 wrong one). Call after a driven sign-in succeeds.",+ {"name": "optional str", "exe": "str (browser binary) OR how:'extension'", "profileDir": "optional str", "how": "optional 'extension'|'launch'"},+ {"success": "bool", "browser": "{name,exe,profileDir}"}, 10, None, False, {"name": "Edge", "exe": "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe", "profileDir": "Default"}),+ ("fusion_aps_get_browser", "Show the remembered Autodesk sign-in browser (None until set).",+ {}, {"success": "bool", "browser": "{name,exe,profileDir}|null"}, 10, None, False, {}),+ ("fusion_aps_forget_browser", "Forget the remembered Autodesk sign-in browser (re-detect next time).",+ {}, {"success": "bool", "cleared": "bool"}, 10, None, False, {}), ("fusion_aps_search", "Server-indexed search across the whole team hub (seconds, not a 30-min folder walk).", {"query": "required str", "limit": "optional int (default 50)", "hubId": "optional str", "projectId": "optional str"}, {"count": "int", "results": "[{name,id,projectName,lastModified}]"}, 60, None, False, {"query": "Molecule", "limit": 10}),
server.py+3@@ -1450,6 +1450,9 @@ COMMAND_HANDLERS = { "aps_status": lambda fi, args: aps.handle_status(args), "aps_set_client_id": lambda fi, args: aps.handle_set_client_id(args), "aps_signin": lambda fi, args: aps.handle_signin(args),+ "aps_set_browser": lambda fi, args: aps.handle_set_browser(args),+ "aps_get_browser": lambda fi, args: aps.handle_get_browser(args),+ "aps_forget_browser": lambda fi, args: aps.handle_forget_browser(args), "aps_search": lambda fi, args: aps.handle_search(args), "aps_browse": lambda fi, args: aps.handle_browse(args), "aps_recent": lambda fi, args: aps.handle_recent(args),