← Commit history

fusion_signin: fix Fusion firing OAuth at the OS-default browser (wrong Autodesk account). Read the full authorize URL from the wrong browser's History DB and re-open it in the correct profile (idmgr/callback + autodesk:// handoff means any profile completes to the running Fusion). Remembers the profile; demo delegates to it. + fusion-multiprofile-signin skill

John Lauer ·5065ef7560 ·2mo ago ·parent 0065bfc
6 files changed +292−7
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.6.91+1.6.92
bridge.json+3−2
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.6.91",+  "version": "1.6.92",   "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.",@@ -173,7 +173,8 @@       "install_fusion": 90,       "set_preference": 40,       "get_preferences": 25,-      "demo": 300+      "demo": 300,+      "signin": 150     },     "prefixes": [       {
describe.py+4
@@ -37,6 +37,10 @@ _T = [      {"success": "bool", "targets": "list"}, 20, None, False,      {"title": "Fusion needs you", "body": "Please complete the Autodesk 2FA prompt on winvm - everything else is done."}), +    ("fusion_signin", "Sign Fusion in through the CORRECT browser profile - the fix for Fusion's dumb OAuth that fires at the OS-DEFAULT browser and lands in the wrong Autodesk account. Fusion opens its sign-in in whatever browser is default (e.g. a personal Chrome), but a user's Autodesk seat often lives in a different profile (work). This verb reads the FULL authorize URL (client_id/PKCE/state/request_id) that Fusion wrote into that wrong browser's HISTORY and re-opens it in the RIGHT profile IN THE BACKGROUND; the URL completes back to the running Fusion via the idmgr/callback + autodesk:// handoff, so the browser no longer has to be the OS default. Pass {profile:'chrome:[email protected]'} to fix + remember your Autodesk profile; else it probes each profile for a live Autodesk session (or asks). Staged: if Fusion hasn't opened the browser yet it returns stage:'click_signin' so you click Fusion's Sign In first. See the 'fusion-multiprofile-signin' skill.",+     {"profile": "optional str (chrome:<email>/edge:<email> - the profile your Autodesk account is in; remembered)"},+     {"stage": "str", "done": "bool", "signinWhere": "str", "signinProfile": "str", "signinProfileReason": "str", "signinWrongBrowser": "str", "signinAuthUrl": "str", "narrate": "str"},+     120, "fusion_signin", True, {}),     ("fusion_demo", "FIRST-TIME-USER DEMO in one verb - the thing to run when someone new wants to SEE what this bridge does (HD's installer calls this). Takes them from nothing to a real board: launches Fusion, FINISHES the Autodesk sign-in - it OPENS the sign-in in the user's NATIVE browser IN THE BACKGROUND (never interrupting their work) and returns signinWhere/signinProfile/signinSession so you can tell them exactly where it is waiting, then offer three paths: they finish it, you foreground it, or (with their OK) you drive it. Native browser via ABE/nbrowser_* because that profile is already signed into Autodesk - never pup, which is anonymous, sets up APS cloud search (or explains it if declined) and runs a LIVE sample search so they see 2-second results, then opens an electronics PROJECT and walks SCHEMATIC -> 2D BOARD -> 3D BOARD, screenshotting each view to show them. STAGED + RESUMABLE: every call advances as far as it safely can and returns stage/done/narrate/screenshots plus a _hint with the exact next action; keep calling it until done:true. NEVER leave a first-time user parked on the sign-in screen - that is the failure this verb exists to prevent. See the 'fusion-demo' skill.",      {"query": "optional str (design to demo; else picked from APS search)", "stage": "optional str ('skip_aps' to proceed without cloud search)"},      {"stage": "str", "done": "bool", "narrate": "str", "screenshots": "[{label,path}]", "steps": "[str]", "apsSampleSearch": "[{name,project}]"},
page.json+4−4
@@ -1,6 +1,6 @@ {   "slug": "adom-desktop-fusion-bridge",-  "version": "1.6.91",+  "version": "1.6.92",   "type": "app",   "description": "Installs this bridge's skills into your container so your AI knows how to drive the bridge. The bridge runtime itself is loaded by Adom Desktop from the release zip.",   "dependencies": {@@ -52,7 +52,8 @@     "skills/fusion-onboarding/**",     "skills/fusion-molecule-export/**",     "skills/pcb-stackup/**",-    "skills/fusion-demo/**"+    "skills/fusion-demo/**",+    "skills/fusion-multiprofile-signin/**"   ],   "discovery_triggers": [     "drive fusion 360",@@ -73,6 +74,5 @@     "set up the fusion bridge"   ],   "discovery_pitch": "Use the Adom Desktop 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 Desktop. 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 Desktop: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.",-  "visibility": "public"+  "brief": "Drive Autodesk Fusion 360 from the cloud via Adom Desktop: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling." }\ No newline at end of file
server.py+204
@@ -3646,6 +3646,198 @@ def _ad_call(verb: str, args: dict, timeout: int = 60) -> dict:     return {"_adCallError": "; ".join(why)}  +def _fusion_signin_cfg_path():+    import pathlib+    d = pathlib.Path(os.path.expanduser("~")) / ".adom" / "fusion-signin"+    d.mkdir(parents=True, exist_ok=True)+    return d / "profile.json"+++def _remembered_signin_profile():+    try:+        import json as _j+        p = _fusion_signin_cfg_path()+        if p.exists():+            return (_j.loads(p.read_text()) or {}).get("profile") or None+    except Exception:+        pass+    return None+++def _remember_signin_profile(profile: str):+    try:+        import json as _j+        _fusion_signin_cfg_path().write_text(_j.dumps({"profile": profile}))+    except Exception:+        pass+++def _chrome_authorize_urls(max_age_min: int = 12):+    """Scan Chrome + Edge profile History DBs for RECENT Autodesk-desktop OAuth authorize+    URLs (the Fusion Identity SDK ones - marked by `idsdk` / redirect to idmgr/callback).++    This is how we fix Fusion opening the WRONG browser profile: Fusion fires its OAuth at+    the OS-default browser, but the FULL authorize URL (client_id, PKCE challenge, state,+    request_id) lands in that browser's History. We read it back and re-open it in the+    profile the user actually authenticates Autodesk in. The URL is NOT tied to a browser+    (its redirect is accounts.autodesk.com/idmgr/callback + an autodesk:// protocol handoff+    matched by request_id), so completing it in ANY profile hands the token to the running+    Fusion. Verified live (John, 2026-07-21).++    Returns a list of {url, sourceProfileDir, browser, ageSec} newest first.+    """+    import glob, sqlite3, shutil, tempfile, time as _t+    la = os.environ.get("LOCALAPPDATA", "")+    roots = []+    if la:+        roots.append(("chrome", os.path.join(la, "Google", "Chrome", "User Data")))+        roots.append(("edge", os.path.join(la, "Microsoft", "Edge", "User Data")))+    now = _t.time()+    found = []+    for browser, root in roots:+        if not os.path.isdir(root):+            continue+        for hist in glob.glob(os.path.join(root, "*", "History")):+            prof_dir = os.path.basename(os.path.dirname(hist))+            tmp = os.path.join(tempfile.gettempdir(), "adom_hist_%s_%s.db" % (browser, prof_dir))+            try:+                shutil.copy2(hist, tmp)  # copy first - Chrome keeps History locked+                con = sqlite3.connect(tmp)+                rows = con.execute(+                    "SELECT url, last_visit_time FROM urls "+                    "WHERE url LIKE '%developer.api.autodesk.com/authentication/v2/authorize%' "+                    "OR url LIKE '%idp.auth.autodesk.com/as/authorize%' "+                    "ORDER BY last_visit_time DESC LIMIT 6").fetchall()+                con.close()+            except Exception:+                continue+            for url, cts in rows:+                if "idsdk" not in url and "idmgr" not in url:+                    continue  # only the DESKTOP-app (Fusion) flow, not a web app+                epoch = (cts / 1_000_000) - 11644473600+                age = now - epoch+                if age <= max_age_min * 60:+                    found.append({"url": url, "sourceProfileDir": prof_dir,+                                  "browser": browser, "ageSec": int(age)})+    found.sort(key=lambda x: x["ageSec"])+    return found+++def _orchestrate_signin(args: dict) -> dict:+    """Sign Fusion in through the CORRECT browser profile - the fix for Fusion firing its+    OAuth at the OS-default browser (often the wrong Autodesk account). See the+    fusion-multiprofile-signin skill.++    Staged: pick the target profile (remembered / arg / probed / ask) -> ensure Fusion has+    emitted its OAuth URL (click Sign In if not) -> read that URL from the wrong browser's+    History -> re-open it in the TARGET profile in the BACKGROUND -> report where it waits.+    """+    steps = []+    target = args.get("profile")           # explicit override, e.g. "chrome:[email protected]"+    if target:+        _remember_signin_profile(target)+    if not target:+        target = _remembered_signin_profile()++    rd = _handle_fusion_readiness({}, {})+    if not rd.get("running"):+        return {"success": True, "stage": "not_running", "done": False, "steps": steps,+                "_hint": "Fusion is not running. fusion_start, then fusion_signin.",+                "statusVerb": "fusion_signin"}+    if not rd.get("needsSignin"):+        return {"success": True, "stage": "already", "done": True, "steps": steps,+                "narrate": "Fusion is already signed in.",+                "_hint": "Already signed in (ready:%s). Nothing to do." % rd.get("ready"),+                "statusVerb": "fusion_signin"}++    # profiles known to ABE (for target selection + naming)+    profs = _ad_call("nbrowser_profiles", {}, timeout=40)+    pdata = profs.get("data", profs) if isinstance(profs, dict) else {}+    choices = [p for p in (pdata.get("profiles") or []) if isinstance(p, dict) and p.get("profile")]++    # read Fusion's authorize URL from browser history (it fires the OS-default browser)+    urls = _chrome_authorize_urls()+    if not urls:+        return {"success": True, "stage": "click_signin", "done": False, "steps": steps,+                "_hint": ("No recent Fusion OAuth URL in any browser's history yet - Fusion has not "+                          "opened the sign-in browser. CLICK Fusion's 'Sign In' button (it is a "+                          "webview with no UIA node, so image-space desktop_click on the button - "+                          "screenshot the Fusion window for a shotId first), wait ~4s, then call "+                          "fusion_signin again. This verb then grabs the URL Fusion opened in the "+                          "WRONG default browser and re-opens it in the RIGHT profile."),+                "statusVerb": "fusion_signin"}+    auth = urls[0]+    steps.append("captured Fusion OAuth URL from %s profile '%s' (%ss old)"+                 % (auth["browser"], auth["sourceProfileDir"], auth["ageSec"]))++    # choose the TARGET profile if not already fixed+    reason = ""+    if not target:+        # probe each live profile for a real Autodesk session; prefer signed-in, then work over consumer+        for c in choices:+            if not c.get("live"):+                continue+            ls = _ad_call("nbrowser_login_state",+                          {"profile": c["profile"], "url": "https://accounts.autodesk.com/"}, timeout=40)+            lsd = ls.get("data", ls) if isinstance(ls, dict) else {}+            c["_ad"] = "signed-in" if lsd.get("loggedIn") else ("signed-out" if lsd.get("ok") is not None else "unprobed")+            c["_conf"] = lsd.get("confidence"); c["_cookies"] = lsd.get("cookieCount")+        signed = [c for c in choices if c.get("_ad") == "signed-in"]+        if signed:+            signed.sort(key=lambda c: ({"high": 3, "medium": 2, "low": 1}.get(c.get("_conf"), 0), c.get("_cookies") or 0), reverse=True)+            target = signed[0]["profile"]; reason = "it holds a live Autodesk session"+        else:+            _CONSUMER = ("gmail.com", "outlook.com", "hotmail.com", "yahoo.com", "icloud.com", "live.com")+            work = [c for c in choices if c.get("live") and c.get("email") and not any((c["email"] or "").lower().endswith("@" + d) for d in _CONSUMER)]+            if work:+                target = work[0]["profile"]; reason = "it is a work/corporate profile (no live Autodesk session detected - CONFIRM)"+    if target:+        _remember_signin_profile(target)+        reason = reason or "you told me to use it"++    if not target:+        return {"success": True, "stage": "ask_profile", "done": False, "steps": steps,+                "signinAuthUrl": auth["url"], "profileChoices": choices,+                "_hint": ("Captured Fusion's OAuth URL but cannot tell which profile is your Autodesk "+                          "account. ASK the user which profile their Autodesk login belongs to, then "+                          "call fusion_signin {profile:'chrome:<their-account>'} - I will remember it. "+                          "profileChoices lists every profile."),+                "statusVerb": "fusion_signin"}++    tdesc = next((_describe_profile(c) for c in choices if c.get("profile") == target), target)+    r = _ad_call("nbrowser_open_window",+                 {"sessionId": "fusion-signin", "url": auth["url"], "background": True,+                  "profile": target, "thread": "fusion-signin",+                  "purpose": "Fusion Autodesk sign-in (correct profile)"}, timeout=60)+    opened = bool((r.get("data", r) if isinstance(r, dict) else {}).get("sessionId") or r.get("ok") or r.get("success"))+    steps.append(("re-opened it in %s" % tdesc) if opened else ("could not open %s: %s" % (tdesc, r.get("_adCallError"))))++    return {"success": True, "stage": "opened" if opened else "open_failed",+            "done": False, "steps": steps, "signinWhere": tdesc, "signinProfile": target,+            "signinProfileReason": reason, "signinSession": "fusion-signin" if opened else None,+            "signinWrongBrowser": "%s / %s" % (auth["browser"], auth["sourceProfileDir"]),+            "signinAuthUrl": auth["url"],+            "narrate": (("Fusion opened its sign-in in the WRONG browser (%s), so I moved the real "+                         "Autodesk sign-in URL into %s - I picked that because %s. Finish it there, "+                         "or I can drive it, and Fusion will pick up the login automatically."+                         % (auth["sourceProfileDir"], tdesc, reason)) if opened else+                        "I captured Fusion's sign-in URL but could not open the target profile."),+            "_hint": (("The REAL Fusion OAuth URL is now open in %s (session 'fusion-signin'). It "+                       "completes back to the running Fusion via the idmgr/callback + autodesk:// "+                       "protocol handoff (request_id match), so the browser profile no longer has to "+                       "be the OS default - THIS is the fix for Fusion signing into the wrong "+                       "account. TELL the user the exact profile (never 'your browser'), say WHY it "+                       "was chosen (signinProfileReason), and offer: they finish it, you foreground "+                       "it, or (with their OK) you drive 'Continue with Google/Apple/Microsoft'. "+                       "NEVER type their password/2FA. Then poll fusion_readiness until ready:true. "+                       "The wrong-browser tab (%s) can be closed. If the wrong ACCOUNT completes "+                       "anyway, sign Fusion out and re-run. Skill: fusion-multiprofile-signin."+                       % (tdesc, auth["sourceProfileDir"])) if opened else+                      "Open failed: %s. Retry, or open auth url yourself with nbrowser_open_window "+                      "{profile:'%s', url:<signinAuthUrl>}." % (r.get("_adCallError"), target)),+            "statusVerb": "fusion_signin"}++ def _orchestrate_demo(args: dict) -> dict:     """FIRST-TIME-USER DEMO: take a brand-new user from nothing to "wow" in one verb. @@ -3708,6 +3900,16 @@ def _orchestrate_demo(args: dict) -> dict:      # ── 2. SIGN-IN: finish it, do not sit on it ─────────────────────────────────     if rd.get("needsSignin"):+        # Delegate to the multi-profile sign-in fix (reads Fusion's OAuth URL out of the WRONG+        # default browser and re-opens it in the RIGHT profile). Returns its own rich stage.+        si = _orchestrate_signin({"profile": args.get("signinProfile")})+        si["demo"] = True+        if si.get("stage") in ("opened", "click_signin", "ask_profile", "open_failed"):+            si.setdefault("done", False)+            si["_hint"] = (si.get("_hint", "") + "  (This is the fusion_demo sign-in stage - after "+                           "the user is signed in and fusion_readiness is ready:true, call fusion_demo "+                           "again to continue the tour: APS -> project -> schematic -> 2D -> 3D.)")+            return si         # DO IT, don't just describe it: open the Autodesk sign-in in the user's OWN browser,         # in the BACKGROUND so we never yank them out of what they're doing. Then tell the AI         # exactly WHERE it is waiting and offer the three ways forward.@@ -4140,6 +4342,8 @@ def dispatch_command(command: str, args: dict) -> dict:         return _orchestrate_fetch_optimized_glb(args)     if command == "demo":         return _orchestrate_demo(args)+    if command == "signin":+        return _orchestrate_signin(args)     if command == "board_stackup":         return _orchestrate_board_stackup(args) 
skills/fusion-multiprofile-signin/SKILL.mdadded+76
@@ -0,0 +1,76 @@+---+name: fusion-multiprofile-signin+description: >+  Fix Fusion 360 signing into the WRONG Autodesk account because it fires its OAuth at the OS-DEFAULT+  browser. Fusion opens sign-in in whatever browser is default (often a personal Chrome profile), but+  a user's Autodesk seat usually lives in a DIFFERENT profile (their work Google/SSO). The bridge's+  fusion_signin verb reads the full authorize URL Fusion wrote into that wrong browser's history and+  re-opens it in the RIGHT profile, where it completes back to the running Fusion. Trigger words -+  fusion wrong browser, fusion wrong account, fusion signs into wrong profile, fusion opens personal+  chrome, autodesk wrong profile, multiple chrome profiles autodesk, fusion_signin, fusion sign in+  wrong google account, fusion default browser oauth, authenticate fusion multiple profiles.+---++# Signing Fusion into the right browser profile (fixing Fusion's dumb OAuth)++## The problem (Fusion's fault, we work around it)++Fusion's desktop sign-in fires an OAuth URL at the **OS-default browser**. If your Autodesk account+lives in a *different* Chrome/Edge profile - personal Chrome is default but your seat is+`[email protected]` in your work profile - Fusion signs you into the wrong account or stalls on the wrong+Google session. Autodesk should let you pick the profile; they don't. So we do.++Verified live (John, 2026-07-21): Fusion opened his personal Chrome (`[email protected]`) when his+Autodesk login is `[email protected]` in his work profile.++## The fix: `fusion_signin`++```bash+adom-desktop fusion_signin '{"profile":"chrome:[email protected]"}'   # fix + REMEMBER your profile+adom-desktop fusion_signin '{}'                                   # auto: probe/remember/ask+```++What it does:++1. **Reads the full authorize URL out of the wrong browser's History.** Fusion writes a complete+   `developer.api.autodesk.com/.../authorize?client_id=...&code_challenge=...&state=...&request_id=idsdk-...`+   into whatever browser it opened. The address bar rewrites within a second (the Autodesk SPA+   cleans it), but Chrome's/Edge's **History DB keeps the full URL** - so the bridge copies the+   locked History file and reads the newest Fusion (`idsdk`/`idmgr`) authorize entry. Racing the+   address bar over the relay does NOT work; the History read is the reliable capture.+2. **Re-opens that exact URL in the RIGHT profile, backgrounded** (ABE / `nbrowser_open_window`), so+   it never interrupts what the user is doing.+3. It completes back to the **running Fusion** on its own. The redirect is+   `accounts.autodesk.com/idmgr/callback` + an `autodesk://` protocol handoff matched by+   `request_id`, **not** a browser-specific localhost port - so the profile no longer has to be the+   OS default. Any profile that finishes the login hands the token to Fusion.++## Picking the right profile++- **Best:** the user tells you once (`{profile:"chrome:[email protected]"}`); the bridge **remembers** it+  (`~/.adom/fusion-signin/profile.json`) for every future sign-in.+- **Auto:** the verb probes each live profile with `nbrowser_login_state` against+  accounts.autodesk.com and prefers the one holding a real Autodesk session; failing that, a+  work/corporate identity over a consumer mailbox - and says it is guessing.+- **Unsure:** it returns `stage:"ask_profile"` - ASK which account their Autodesk login is, do not+  assume. Name profiles by identity (`Chrome - you ([email protected])`), never "your browser".++## The staged loop++- `stage:"click_signin"` - Fusion has not opened the browser yet. Click Fusion's **Sign In** (it is a+  webview with no UIA node, so an image-space `desktop_click` on the button; screenshot the Fusion+  window first for a shotId), wait ~4s, call `fusion_signin` again.+- `stage:"opened"` - the URL is now in the right profile (`signinWhere`). Tell the user where, why+  (`signinProfileReason`), and offer: they finish it, you foreground it, or (with their OK) you drive+  "Continue with Google/Apple/Microsoft". **Never type their password/2FA** - escalate a real secret+  with `fusion_notify_owner`. Poll `fusion_readiness` until `ready:true`.+- `stage:"ask_profile"` - ask which profile, then re-call with it.++## Gotchas++- Read History AFTER Fusion has opened the browser, and take the NEWEST authorize entry - a stale one+  from a previous attempt carries a dead `request_id` and Fusion (waiting on a new one) never gets it.+- If the wrong account completes in the default browser before you move it, sign Fusion out and re-run.+- Chrome keeps History locked; always COPY the file before opening it (the verb does this).+- This is also the sign-in stage `fusion_demo` uses, so a first-time demo never lands in the wrong+  Autodesk account.