← Commit history

1.6.56: remember the Autodesk sign-in browser+profile

John Lauer ·41cd784047 ·3mo ago ·parent bc3a5e2
7 files changed +195−21
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.6.55\ No newline at end of file+1.6.56\ No newline at end of file
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),