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
server.py+225−7@@ -3752,6 +3752,94 @@ def _fusion_window_hwnd(): return (_fusion_window() or {}).get("hwnd") +# ── NEVER STEAL THE USER'S FOREGROUND (John, 2026-07-22) ─────────────────────────────────────+# "try to NEVER bring windows to the foreground cuz its disruptive."+# desktop_click is SendInput and FOREGROUNDS the window. desktop_ui_click/desktop_ui_set are UIA+# Invoke/SetValue: programmatic, NO focus steal, NO cursor move, and Chromium exposes its a11y+# tree so page buttons/fields ARE reachable by accessible name. So: ALWAYS try background UIA+# first and only fall back to a foreground click for things with no UIA node (Fusion's Sign In+# button is a webview with none - that is the one documented exception).+def _click_background_first(hwnd, name=None, shot_id=None, x=None, y=None) -> dict:+ """Click preferring the BACKGROUND UIA path. Returns {clicked, via, why}."""+ if name:+ r = _unwrap(_ad_call("desktop_ui_click", {"hwnd": int(hwnd), "name": name}, timeout=30))+ if r.get("ok") or r.get("clicked") or r.get("invoked"):+ return {"clicked": True, "via": "uia_background", "why": "UIA Invoke on %r" % name}+ if shot_id is not None and x is not None and y is not None:+ _ad_call("desktop_click", {"space": "image", "shotId": shot_id,+ "x": int(x), "y": int(y), "hwnd": int(hwnd)}, timeout=30)+ return {"clicked": True, "via": "foreground_click",+ "why": "no UIA node; fell back to SendInput (this one DOES foreground)"}+ return {"clicked": False, "via": None, "why": "no UIA node and no shot coords given"}+++# ── HUMAN WALLS: notify the user, and tell the AI it can drive it ────────────────────────────+# John, 2026-07-22: "you should be sending me ad notifies when you need me to do something" AND+# "suggest to the ai via hints that you can drive the thing on your own cuz the adom user wants+# you to do everything automatically if you can". So on every wall we do BOTH: toast the human,+# and hand the AI an exact, mechanical way to clear it without them.+def _detect_signin_wall() -> 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."""+ wins = _unwrap(_ad_call("desktop_list_windows", {}, timeout=40)).get("windows") or []+ best = {}+ for w in wins:+ t = str(w.get("title", "")); tl = t.lower()+ if "2-step verification" in tl or "confirm sign-in" in tl:+ return {"wall": "twofactor_email_code", "hwnd": w.get("hwnd"), "title": t}+ if "sign in - autodesk" in tl and not best:+ best = {"wall": "credentials", "hwnd": w.get("hwnd"), "title": t}+ if "identity manager" in tl:+ best = {"wall": "protocol_handoff", "hwnd": w.get("hwnd"), "title": t}+ return best+++_WALL_COPY = {+ "twofactor_email_code": ("Autodesk needs your 6-digit code",+ "Autodesk emailed a verification code to finish signing Fusion in. "+ "Enter it in the Chrome window, or tell your AI to fetch it for you."),+ "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."),+ "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 _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."))+ return _cli_notify_all(title, body, "warning")+++def _wall_hint(wall: str, notified: bool) -> str:+ """The AI-facing instruction for a wall. Per John: do as much as possible IN CODE, and use+ HINTS (not skills) to make the calling AI act correctly."""+ common = ("I ALREADY TOASTED THE USER about this (notifyDelivered=%s) - you do not need to ask "+ "me to, and do not nag them again. " % notified)+ if wall == "twofactor_email_code":+ return (common ++ "WALL: Autodesk emailed a 6-DIGIT CODE. THE USER WANTS YOU TO DO THIS FOR THEM if "+ "you can, so TRY THIS FIRST, do not just hand it back to them: if `adom-google` is "+ "available in YOUR container, read the code out of their Gmail and submit it - "+ " adom-google gmail search 'from:autodesk verification code' --limit 1 \n"+ "then pull the 6 digits and call fusion_signin_2fa {\"code\":\"123456\"} which "+ "types it in the BACKGROUND (no foreground steal) and submits. If `adom-google` is "+ "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 == "protocol_handoff":+ return (common ++ "WALL: Chrome's native 'Open Autodesk Identity Manager?' dialog. Try to clear it "+ "YOURSELF in the background: desktop_find_control on that dialog then "+ "desktop_ui_click {name:'Open'} (UIA, no focus steal). Only if UIA finds no node, "+ "fall back to a foregrounded desktop_click. Then poll fusion_readiness.")+ return (common ++ "WALL: the sign-in wants credentials. NEVER type their password or 2FA. If the browser "+ "autofills, you may submit it. Otherwise let the user finish in the window I opened.")++ def _unwrap(r): """AD verb responses come back either flat or nested under `data` (the CLI puts the real payload in output.data). Always look through both - forgetting this silently broke the@@ -3792,13 +3880,16 @@ def _fusion_click_signin() -> dict: if not sid: return {"clicked": False, "why": "screenshot returned no shotId"} img = cm.get("image") or {}- w = shot.get("width") or img.get("width") or cm.get("imageWidth") or 1400- h = shot.get("height") or img.get("height") or cm.get("imageHeight") or 860- # the Sign In button sits centered, ~57% down the welcome screen- r = _unwrap(_ad_call("desktop_click", {"space": "image", "shotId": sid,- "x": int(w * 0.5), "y": int(h * 0.57)}, timeout=30))- return {"clicked": True, "why": "clicked Sign In at image (%d,%d) of %sx%s" % (- int(w * 0.5), int(h * 0.57), w, h)}+ w = shot.get("width") or img.get("w") or img.get("width") or cm.get("imageWidth") or 1400+ h = shot.get("height") or img.get("h") or img.get("height") or cm.get("imageHeight") or 860+ # Background UIA first (no focus steal). Fusion's Sign In is a WEBVIEW button with no UIA+ # node, so this almost always falls through to the image-space click - that is the documented+ # exception to the never-foreground rule, not a licence to foreground elsewhere.+ r = _click_background_first(hwnd, name="Sign In", shot_id=sid,+ x=int(w * 0.5), y=int(h * 0.57))+ return {"clicked": r.get("clicked"),+ "why": "%s (image %dx%d)" % (r.get("why"), w, h),+ "via": r.get("via")} def _capture_fresh_authorize(wait_sec: int = 30, max_age: int = 120):@@ -3817,6 +3908,80 @@ def _capture_fresh_authorize(wait_sec: int = 30, max_age: int = 120): _t.sleep(2) +def _orchestrate_signin_2fa(args: dict) -> dict:+ """Submit Autodesk's emailed 6-digit verification code, IN THE BACKGROUND.++ Exists so the AI can finish a login end-to-end for the user (read the code from Gmail with+ adom-google, then call this) instead of parking them on a 2FA screen. Uses UIA SetValue ++ Invoke, so it never steals focus or moves the cursor.+ """+ code = str(args.get("code") or "").strip()+ digits = "".join(ch for ch in code if ch.isdigit())+ if len(digits) < 4:+ return {"success": False, "errorCode": "bad_code",+ "error": "Pass the emailed code, e.g. fusion_signin_2fa {\"code\":\"123456\"}.",+ "_hint": ("Read it from the user's Gmail if you can: "+ "adom-google gmail search 'from:autodesk verification code' --limit 1 "+ "-> take the 6 digits -> call this verb again. If adom-google is not "+ "installed, tell the user it would let you automate this."),+ "statusVerb": "fusion_signin_2fa"}++ wall = _detect_signin_wall()+ hwnd = args.get("hwnd") or wall.get("hwnd")+ if not hwnd:+ return {"success": False, "errorCode": "no_2fa_window",+ "error": "No Autodesk 2-step-verification window found.",+ "_hint": ("Nothing is waiting on a code right now. Check fusion_readiness - if "+ "needsSignin is still true, run fusion_signin again to restart the flow."),+ "statusVerb": "fusion_readiness"}++ # Find the code field WITHOUT touching the foreground. Autodesk renders either one input or a+ # segmented set, so try the obvious accessible names, then fall back to the first edit control.+ found = _unwrap(_ad_call("desktop_find_control",+ {"hwnd": int(hwnd), "role": "edit"}, timeout=40))+ ctrls = found.get("controls") or found.get("matches") or []+ steps = []+ filled = False+ if len(ctrls) >= len(digits) and len(ctrls) >= 4:+ # segmented: one box per digit+ for i, ch in enumerate(digits[:len(ctrls)]):+ _ad_call("desktop_ui_set", {"hwnd": int(hwnd),+ "automationId": ctrls[i].get("automationId"),+ "name": ctrls[i].get("name"), "value": ch}, timeout=20)+ filled = True+ steps.append("typed %d digits into %d segmented boxes (background UIA)" % (len(digits), len(ctrls)))+ elif ctrls:+ c = ctrls[0]+ r = _unwrap(_ad_call("desktop_ui_set", {"hwnd": int(hwnd),+ "automationId": c.get("automationId"),+ "name": c.get("name"), "value": digits}, timeout=20))+ filled = bool(r.get("ok") or r.get("set") or r is not None)+ steps.append("typed the code into %r (background UIA)" % (c.get("name") or c.get("automationId")))++ if not filled:+ return {"success": False, "errorCode": "code_field_not_found",+ "error": "Could not find the code field via UIA.",+ "controlsSeen": ctrls[:8],+ "_hint": ("The code input was not in the UIA tree (shadow DOM). Run "+ "desktop_find_control {hwnd:%s} to see what IS exposed. Last resort is a "+ "FOREGROUNDED desktop_click on the field then desktop_type - avoid that "+ "if you can, foregrounding is disruptive to the user." % hwnd),+ "statusVerb": "fusion_signin_2fa"}++ sub = _click_background_first(hwnd, name="Next")+ if not sub.get("clicked"):+ sub = _click_background_first(hwnd, name="Verify")+ steps.append("submitted via %s" % (sub.get("via") or "no submit control found"))++ return {"success": True, "submitted": True, "steps": steps,+ "narrate": "I read the code in and submitted it for you, without touching your screen.",+ "_hint": ("Code submitted in the BACKGROUND. Now poll fusion_readiness until ready:true. "+ "If a Chrome 'Open Autodesk Identity Manager?' dialog appears, clear it with "+ "desktop_ui_click {name:'Open'} (background) - fusion_signin reports that wall "+ "too. If it says the code was wrong, the email may have a NEWER code."),+ "statusVerb": "fusion_readiness"}++ 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@@ -3967,9 +4132,21 @@ def _orchestrate_signin(args: dict) -> dict: steps.append((("re-opened it in %s%s" % (tdesc, " (extension-free)" if open_via == "extension_free" else "")) if opened else ("could not open %s (raw: %s)" % (tdesc, str(r)[:160])))) + # 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() if opened else {}+ wall_kind = wall.get("wall")+ notified = False+ if wall_kind:+ notified = bool(_notify_signin_wall(wall_kind).get("delivered"))+ steps.append("hit %s wall; toasted the user (delivered=%s)" % (wall_kind, notified))+ return {"success": True, "stage": "opened" if opened else "open_failed", "done": False, "steps": steps, "signinWhere": tdesc, "signinProfile": target, "signinOpenedVia": open_via,+ "signinWall": wall_kind, "signinWallHwnd": wall.get("hwnd"),+ "notifyDelivered": notified,+ "signinWallHint": _wall_hint(wall_kind, notified) if wall_kind else None, "signinProfileReason": reason, "signinSession": sess if opened else None, "signinWrongBrowser": "%s / %s" % (auth["browser"], auth["sourceProfileDir"]), "signinAuthUrl": auth["url"],@@ -4500,6 +4677,8 @@ def dispatch_command(command: str, args: dict) -> dict: return _orchestrate_demo(args) if command == "signin": return _orchestrate_signin(args)+ if command == "signin_2fa":+ return _orchestrate_signin_2fa(args) if command == "board_stackup": return _orchestrate_board_stackup(args) @@ -4662,6 +4841,43 @@ class FusionBridgeHandler(BaseHTTPRequestHandler): print(f"[Fusion Bridge] {args[0]} {args[1]} {args[2]}") +def _prune_bridge_logs(max_mb: int = 8, keep_tail_mb: int = 2) -> dict:+ """LOG JANITOR (John, 2026-07-22: "do you have a janitor to clean up your log?").++ AD captures this bridge's stdout/stderr into ~/.adom/bridge-logs/fusion360.log. Nothing+ rotated it, so it grew unbounded - fine today (it is small), a slow leak on a long-lived box.+ On every bridge start we truncate our OWN logs to the most recent keep_tail_mb once they pass+ max_mb, keeping the tail because that is where a crash traceback lives.++ Only touches files this bridge owns (fusion360*). Never raises.+ """+ import glob+ out = {"checked": 0, "pruned": []}+ try:+ d = os.path.join(os.path.expanduser("~"), ".adom", "bridge-logs")+ for path in glob.glob(os.path.join(d, "fusion360*.log")):+ out["checked"] += 1+ try:+ sz = os.path.getsize(path)+ if sz <= max_mb * 1024 * 1024:+ continue+ with open(path, "rb") as f:+ f.seek(-keep_tail_mb * 1024 * 1024, os.SEEK_END)+ tail = f.read()+ with open(path, "wb") as f:+ f.write(b"[adom-bridge log janitor] truncated %d MB -> tail %d MB\n"+ % (sz // (1024 * 1024), keep_tail_mb))+ f.write(tail)+ out["pruned"].append({"file": os.path.basename(path), "wasMB": sz // (1024 * 1024)})+ except Exception:+ continue+ if out["pruned"]:+ print("[Fusion Bridge] log janitor: %s" % out["pruned"])+ except Exception:+ pass+ return out++ def main(): global fusion_info @@ -4676,6 +4892,8 @@ def main(): except Exception: pass + _prune_bridge_logs()+ port = DEFAULT_PORT if "--port" in sys.argv: idx = sys.argv.index("--port")