← Commit history

v1.9.32: self-driving sign-in to the exact human wall

John Lauer ·a319229ce6 · ·parent 2c828dd
6 files changed +146−10
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.31\ No newline at end of file+1.9.32\ No newline at end of file
adom-bridge-fusion-manifest.json+4−4
@@ -1,10 +1,10 @@ {     "manifest_version": 1,     "name": "fusion360",-    "version": "1.9.31",-    "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.31/adom-bridge-fusion-v1.9.31.zip",-    "sha256": "747295223d124465a0a477f6f3e01b8ceaedd9e0e16bae78fba1cf4ab246db52",-    "size": 381112,+    "version": "1.9.32",+    "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.32/adom-bridge-fusion-v1.9.32.zip",+    "sha256": "d0c532dafa7ae77bad1048c70bf375a7ae8bf5c4379211ebb4ca0f2eb99df076",+    "size": 383092,     "verbPrefixes": [         "fusion_"     ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.31",+  "version": "1.9.32",   "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/fusion-bridge",   "author": "Adom Inc.",
describe.py+1−1
@@ -37,7 +37,7 @@ _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. BEST: pass {auto:true} to run the WHOLE chain in ONE server-side call - it clicks Fusion's Sign In, polls the browser history on-box for the fresh authorize URL, and re-opens it in the target profile, all without extra remote round-trips, so it fits inside Fusion's ~2-min OAuth request expiry even on a slow/flaky relay (the manual staged path is the fallback). Fusion's request EXPIRES in ~2 min, so a stale captured URL returns stage:'stale' - restart Fusion (fusion_stop/start) for a fresh request and retry auto PROMPTLY. See the 'fusion-multiprofile-signin' skill.",+    ("fusion_signin", "SELF-DRIVING as of 1.9.32: launches Fusion if needed, clicks Sign In, opens the OAuth in the right profile, TYPES THE EMAIL and advances to the exact remaining wall (password/2FA/handoff), toasting the user at that step only - see signinWall + _hint in the response, and the fusion-autodesk-signin skill. 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. BEST: pass {auto:true} to run the WHOLE chain in ONE server-side call - it clicks Fusion's Sign In, polls the browser history on-box for the fresh authorize URL, and re-opens it in the target profile, all without extra remote round-trips, so it fits inside Fusion's ~2-min OAuth request expiry even on a slow/flaky relay (the manual staged path is the fallback). Fusion's request EXPIRES in ~2 min, so a stale captured URL returns stage:'stale' - restart Fusion (fusion_stop/start) for a fresh request and retry auto PROMPTLY. See the 'fusion-multiprofile-signin' skill.",      {"profile": "optional str (chrome:<email>/edge:<email> - the profile your Autodesk account is in; remembered)", "auto": "optional bool (do click+capture+reopen in one on-box call - recommended)", "waitSec": "optional int (server-side poll budget for the fresh URL, default 32)"},      {"stage": "str", "done": "bool", "signinWhere": "str", "signinProfile": "str", "signinProfileReason": "str", "signinWrongBrowser": "str", "signinAuthUrl": "str", "signinOpenedVia": "str (extension|extension_free)", "signinWall": "str|null (twofactor_email_code|credentials|protocol_handoff)", "notifyDelivered": "bool", "signinWallHint": "str|null", "narrate": "str"},      120, "fusion_signin", True, {}),
server.py+112−3
@@ -4796,6 +4796,54 @@ def _autodesk_window_hwnds() -> set:     return out  +def _advance_credentials_page(hwnd, email) -> dict:+    """Self-drive the Autodesk credentials page as far as possible WITHOUT secrets+    (John, 2026-08-16: 'do as much self driving automation as you can... only let the+    AI know when you need it to know'). Background UIA only - no foreground, no focus+    steal: set the email/username field, invoke Next/Sign in, then classify what the+    page became. Returns {advanced, wall, detail}. NEVER touches password fields."""+    import time as _t+    out = {"advanced": False, "wall": "credentials", "detail": ""}+    try:+        found = _unwrap(_ad_call("desktop_find_control", {"hwnd": int(hwnd), "role": "edit"}, timeout=40))+        ctrls = found.get("controls") or found.get("matches") or []+        if not ctrls and found.get("best"):+            ctrls = [found["best"]]+        pwd = [c for c in ctrls if "password" in str(c.get("name", "")).lower()+               or "password" in str(c.get("automationId", "")).lower()]+        if pwd:+            out["wall"] = "password"+            out["detail"] = "password field present - the human step"+            return out+        emailish = [c for c in ctrls if any(k in (str(c.get("name", "")) + str(c.get("automationId", ""))).lower()+                                            for k in ("email", "username", "user id", "userid"))] or ctrls[:1]+        if email and emailish:+            c = emailish[0]+            r = _unwrap(_ad_call("desktop_ui_set", {"hwnd": int(hwnd),+                                                    "automationId": c.get("automationId"),+                                                    "name": c.get("name"), "value": email}, timeout=30))+            if r.get("success") or r.get("ok"):+                out["detail"] = "typed email %r into %r (background UIA)" % (email, c.get("name") or c.get("automationId"))+                for btn in ("Next", "NEXT", "Sign in", "Sign In", "Continue"):+                    rb = _unwrap(_ad_call("desktop_ui_click", {"hwnd": int(hwnd), "name": btn}, timeout=25))+                    if rb.get("success") or rb.get("ok") or rb.get("status") == "ok":+                        out["advanced"] = True+                        out["detail"] += "; invoked %r" % btn+                        break+                if out["advanced"]:+                    _t.sleep(4)+                    found2 = _unwrap(_ad_call("desktop_find_control", {"hwnd": int(hwnd), "role": "edit"}, timeout=40))+                    ctrls2 = found2.get("controls") or found2.get("matches") or []+                    if not ctrls2 and found2.get("best"):+                        ctrls2 = [found2["best"]]+                    if any("password" in (str(c.get("name", "")) + str(c.get("automationId", ""))).lower() for c in ctrls2):+                        out["wall"] = "password"+                        out["detail"] += "; page advanced to the PASSWORD step"+    except Exception as e:+        out["detail"] = "advance error: " + str(e)[:120]+    return out++ def _detect_signin_wall(ignore_hwnds=None) -> dict:     """Classify what the Autodesk sign-in is waiting on, from BROWSER WINDOW TITLES.     Title-based on purpose: cheap, needs no OCR, and never touches/foregrounds the window.@@ -4827,12 +4875,29 @@ _WALL_COPY = {     "credentials": ("Autodesk sign-in needs you",                     "The Autodesk sign-in is asking for credentials. I never type your password "                     "or 2FA - please complete it in the Chrome window."),+    "password": ("Just your password, everything else is done",+                 "Adom drove the Fusion sign-in all the way: email entered, page advanced. "+                 "The ONLY step left is your password (and I never type secrets). Type it in "+                 "the browser window and everything continues automatically."),     "protocol_handoff": ("Allow Fusion to finish signing in",                          "Chrome is asking permission to open Autodesk Identity Manager. Click "                          "Open to hand the login back to Fusion."), }  +def _resolve_and_advance_wall(wall_info, email) -> dict:+    """Given a detected wall, push it as far as code can go (email typing on the+    credentials page), refining 'credentials' into 'password' when that is what+    remains. Returns possibly-updated wall_info with 'advance' detail attached."""+    if wall_info.get("wall") == "credentials" and wall_info.get("hwnd"):+        adv = _advance_credentials_page(wall_info["hwnd"], email)+        wall_info = dict(wall_info)+        wall_info["wall"] = adv.get("wall") or wall_info["wall"]+        wall_info["advance"] = adv.get("detail")+        wall_info["advanced"] = adv.get("advanced")+    return wall_info++ def _notify_signin_wall(wall: str) -> dict:     """Toast the user's real desktop about a human wall. Best-effort, never raises."""     title, body = _WALL_COPY.get(wall, ("Fusion sign-in needs you", "Please check the sign-in window."))@@ -4855,6 +4920,14 @@ def _wall_hint(wall: str, notified: bool) -> str:                 "NOT installed, TELL THE USER that installing adom-google would let you finish "                 "logins like this automatically next time, then let them type the code. NEVER ask "                 "them for the code if you can read it yourself.")+    if wall == "password":+        return (common ++                "WALL: PASSWORD ONLY. The bridge already typed the user's email and advanced the "+                "page (see steps[]) - the single remaining human act is typing their password in "+                "the browser window. Do NOT ask them for the password, do NOT try to type it, and "+                "do NOT re-drive the page. After they type it, the flow continues automatically - "+                "poll fusion_readiness until ready:true (a 2FA wall may follow; if it does you may "+                "be able to finish it yourself via fusion_signin_2fa + adom-google Gmail).")     if wall == "protocol_handoff":         return (common +                 "WALL: Chrome's native 'Open Autodesk Identity Manager?' dialog. Try to clear it "@@ -5092,9 +5165,25 @@ def _orchestrate_signin(args: dict) -> dict:      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"}+        # Self-drive (John, 2026-08-16): never hand "go start Fusion" back to the caller.+        steps.append("Fusion was not running - launching it (background) as part of sign-in")+        try:+            dispatch_command("start", {})+        except Exception as e:+            steps.append("launch failed: " + str(e)[:80])+        import time as _lt+        deadline = _lt.time() + 120+        while _lt.time() < deadline:+            _lt.sleep(8)+            rd = _handle_fusion_readiness({}, {})+            if rd.get("running"):+                break+        if not rd.get("running"):+            return {"success": False, "stage": "launch_failed", "done": False, "steps": steps,+                    "error": "Fusion did not come up within 120s of launch.",+                    "_hint": "Check fusion_readiness; the box may be mid-update. Retry fusion_signin.",+                    "statusVerb": "fusion_readiness"}+        _lt.sleep(5)     if not rd.get("needsSignin"):         return {"success": True, "stage": "already", "done": True, "steps": steps,                 "narrate": "Fusion is already signed in.",@@ -5208,6 +5297,9 @@ def _orchestrate_signin(args: dict) -> dict:         steps.append("Fusion already opened the sign-in in %s (profile dir %r) - no re-open needed"                      % (target, target_dir))         wall0 = _detect_signin_wall()+        _email0 = args.get("email") or next((c.get("email") for c in choices if c.get("profile") == target and c.get("email")), None)+        if wall0.get("wall"):+            wall0 = _resolve_and_advance_wall(wall0, _email0)         notified0 = bool(_notify_signin_wall(wall0["wall"]).get("delivered")) if wall0.get("wall") else False         return {"success": True, "stage": "already_right_profile", "done": False, "steps": steps,                 "signinProfile": target, "signinProfileDir": target_dir,@@ -5319,6 +5411,9 @@ def _orchestrate_signin(args: dict) -> dict:     # Did the reopened sign-in immediately hit a HUMAN WALL (2FA, credentials, protocol dialog)?     # If so, toast the user AND tell the AI how to clear it for them.     wall = _detect_signin_wall(ignore_hwnds=pre_hwnds) if opened else {}+    _email1 = args.get("email") or next((c.get("email") for c in choices if c.get("profile") == target and c.get("email")), None)+    if wall.get("wall"):+        wall = _resolve_and_advance_wall(wall, _email1)     wall_kind = wall.get("wall")     notified = False     if wall_kind:@@ -5711,6 +5806,20 @@ def _orchestrate_demo_tour(args: dict) -> dict:                         except Exception:                             pass                     if not ready:+                        # Not ready usually means the SIGN-IN wall on a fresh box. Drive it as far+                        # as code can (email typed, page advanced), toast the human at the exact+                        # remaining step, and fail the tour with instructions instead of a shrug.+                        rd = _handle_fusion_readiness({}, {})+                        if rd.get("needsSignin"):+                            state.update(step="signin", stepLabel="Signing in (one-time, driving it for you)")+                            si = _orchestrate_signin({"auto": True})+                            wallname = si.get("signinWall") or "credentials"+                            state.setdefault("warnings", []).append(+                                "sign-in required: drove to wall %r; user toasted; %s" % (wallname, str(si.get("stage"))))+                            raise RuntimeError(+                                "One-time Autodesk sign-in needed on this machine. I drove it as far "+                                "as possible and toasted the user at the %r step - after they finish, "+                                "re-run the tour and it completes." % wallname)                         raise RuntimeError("Fusion launched but its add-in never became ready (150s)")                 elif key == "stage":                     lbr = _demo_tour_sample_lbr()
skills/fusion-autodesk-signin/SKILL.md+27
@@ -110,3 +110,30 @@ time (learned the hard way here). Tell the user up front: "I need to install ABE browser reliably." Install: `bridge_install {manifestUrl: .../adom-browser-extension/files/native-browser-bridge-manifest.json}`, then the `installing-the-extension` flow, then `nbrowser_readiness` until `state:"ready"`. Coordinate- driving is the fallback of last resort, never the default.+++## AUTOMATED NOW: the verb self-drives this whole playbook (v1.9.32)++Everything above is what `fusion_signin` DOES ITSELF as of 1.9.32 - the manual playbook+remains as the fallback and the explanation. The contract for a calling AI:++1. Call `fusion_signin {}` (add `email` to override; the target profile's email is used+   otherwise). It SELF-STARTS Fusion if needed, clicks Sign In for the user, captures+   the OAuth URL, re-opens it in the correct profile, TYPES THE EMAIL and advances the+   page (background UIA, no focus steal), and classifies the remaining wall.+2. Walls you can get back, in `signinWall`:+   - `password` - the ONE human secret step. The user was ALREADY TOASTED with "just+     your password". Do nothing except poll `fusion_readiness`.+   - `twofactor_email_code` - fetch the code from Gmail (adom-google) and call+     `fusion_signin_2fa {code}` yourself; only fall back to the human if you cannot.+   - `protocol_handoff` - the browser asks to open Autodesk Identity Manager; the user+     was toasted to click Open.+3. NEVER re-toast, never ask the user for secrets, never re-drive an advanced page -+   the response `_hint` tells you exactly what remains.+4. The demo tour integrates this: on a signed-out box it drives sign-in to the wall,+   toasts, and fails with instructions - rerun the tour after the human's one step.++The philosophy (John, 2026-08-16): the code automates EVERYTHING automatable and+notifies the human at exactly the moment they are needed - they let this run in the+background, so the toast IS the interface. Hints teach the AI; the AI never improvises+clicks on auth pages.