← Commit history

1.6.53: slow-open verify-by-state + fusion_notify_owner + parent/child shot hints

John Lauer ·bbe3b441bb ·3mo ago ·parent 9b89ff1
6 files changed +119−22
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.6.47\ No newline at end of file+1.6.53\ No newline at end of file
CHANGELOG.md+17
@@ -3,6 +3,23 @@ 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.53 - 2026-07-07++- **Fixed slow-machine opens being mislabeled "add-in not running" (caught live on a GPU-less Azure+  VM).** `_orchestrate_open_lbr`'s `Document.newDesignFromLocal` call had a fixed 30s read timeout;+  on software-rendered machines the open takes 60s+, so the proxy timed out and the canned error+  claimed the ADD-IN was down while the document was actually opening fine -+  `fusion_build_library_3d` then aborted every part on a lie. Now: 120s timeout AND on any failure+  the open is VERIFIED BY STATE (poll `get_app_state` for the expected document) before failing -+  the doc actually being open outranks the synchronous return.+- **NEW verb `fusion_notify_owner` - deterministic last-resort human escalation (John's rule).**+  Toasts the user's MAIN computer by fanning out cross-AD to every relay peer, so a bridge on an+  unattended VM still reaches them. For true human walls only (password/2FA, UAC); hints tell the+  driving AI to exhaust programmatic options first and not to re-toast.+- **Hints now teach the parent/child screenshot discipline**: never poll blind, never fullscreen -+  `desktop_screenshot_window` returns a `screenshots[]` array where every OWNED POPUP rides back as+  its own child shot; launch/sign-in hints now say to READ those.+ ## 1.6.48-1.6.52 - 2026-07-06 (pkg hygiene, no runtime change)  - Declared the `adom/adom-desktop` pkg dependency (SDK two-artifact audit, #83).
adom-bridge-fusion-manifest.json+5−5
@@ -1,13 +1,13 @@ {   "manifest_version": 1,   "name": "fusion360",-  "version": "1.6.47",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.47/adom-bridge-fusion-v1.6.47.zip",-  "sha256": "9cfa11c55d99baef1c268a4a13cf69a338eb4af53a8ed5ffe13541e16ec7b3cd",-  "size": 345799,+  "version": "1.6.53",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.53/adom-bridge-fusion-v1.6.53.zip",+  "sha256": "93e1b055a799133d452813822f1979dd67a79d2af52bd586c5bf147a29e5758d",+  "size": 334089,   "verbPrefixes": [     "fusion_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-07-06T15:05:00Z"+  "released_at": "2026-07-07T12:55:00Z" }
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.6.47",+  "version": "1.6.53",   "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+6−1
@@ -24,9 +24,14 @@ _T = [      {"name": "optional str (default: active)"}, {"success": "bool"}, 30, None, False, {}),     ("fusion_get_app_state", "Current app state: running, active document, workspace, isElectronics. The poll/status verb for long-running calls.",      {}, {"activeDocument": "str", "activeWorkspace": "str", "isElectronics": "bool"}, 20, None, False, {}),-    ("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. Pairs with AD's bridge_readiness.",+    ("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_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,+     {"title": "Fusion needs you", "body": "Please complete the Autodesk 2FA prompt on winvm - everything else is done."}),+     # ── Cloud search & files (APS) ─────────────────────────────────────────     ("fusion_aps_status", "APS state: configured, signed in, token live, never-charge meter.",      {}, {"configured": "bool", "signedIn": "bool", "neverCharge": "bool"}, 20, None, False, {}),
server.py+89−14
@@ -198,6 +198,43 @@ def _resolve_seat_dialog(max_clicks: int = 8) -> dict:     return {"sawDialog": saw, "clicks": clicks, "verified": verified}  +def _handle_notify_owner(args: dict) -> dict:+    """LAST-RESORT: toast the user's MAIN desktop (cross-AD) to ask for a human step.++    John's standing rule (2026-07-07): the bridge/AI does EVERYTHING itself; the only+    legitimate uses are true human walls - password/2FA entry, UAC elevation, a physical+    action. When this bridge runs on an unattended VM, the toast must reach the machine+    the user is actually AT - ad_client.notify(reach_user=True) fans out to all peer ADs+    on the relay, so it lands on their main computer too. Returns which targets were hit."""+    title = args.get("title") or "Fusion bridge needs you"+    body = args.get("body") or args.get("message") or "A human step is required to continue."+    level = args.get("level") or "warning"+    if not ad_client.available():+        return {+            "success": False,+            "error": "AD direct API unavailable - cannot send a toast from the bridge.",+            "notifyUser": {"title": title, "body": body, "level": level},+            "_hint": ("Relay the included notifyUser payload yourself via AD's notify_user verb "+                      "(add a cross-AD `target` if this bridge runs on a VM and the user is elsewhere)."),+        }+    peers = []+    try:+        peers = ad_client.peers()+    except Exception:+        pass+    res = ad_client.notify(title, body, level=level, reach_user=True)+    ok = bool(res and (res.get("success") or res.get("status") == "ok"))+    return {+        "success": ok,+        "targets": (["self"] + peers) if peers else ["self"],+        "_hint": ("Toast sent" + (" to this machine AND all peer ADs (user may be on another computer)."+                  if peers else " to this machine (no peer ADs on the relay).")+                  + " This is the LAST RESORT - only use after exhausting programmatic options "+                  "(UIA background clicks, seat auto-resolve, warm-SSO sign-in). Now WAIT and poll "+                  "fusion_readiness for the state to clear; do not re-toast within a few minutes."),+    }++ def _handle_launch(fusion_info: dict, args: dict) -> dict:     """Launch Fusion 360 and optionally wait for the AdomBridge add-in."""     # LIVE detect (not the stale bridge-start fusion_info snapshot) so a launch right@@ -320,7 +357,10 @@ def _handle_launch(fusion_info: dict, args: dict) -> dict:                 "NOT a failure and NOT a retry-able timeout: do NOT re-call fusion_start. WHAT HAPPENS "                 "NEXT: POLL fusion_readiness every ~10s; it returns needsSignin/licensingDialog/updating "                 "if something is mid-flight (the bridge handles the seat dialog itself), and ready:true "-                "when it is drivable - then run your fusion_* verbs."+                "when it is drivable - then run your fusion_* verbs. NEVER poll blind past ~2 min: "+                "desktop_screenshot_window the Fusion hwnd and READ the response's screenshots[] array "+                "- every OWNED POPUP rides back as its own child shot (this is how you SEE a blocking "+                "dialog; never desktop_screenshot_screen, the user is doing other things)."             ),         } @@ -980,11 +1020,13 @@ def _handle_fusion_readiness(fusion_info: dict, args: dict) -> dict:     elif needs_signin:         hint = ("Fusion is RUNNING but stuck at the first-run Autodesk SIGN-IN (main UI blocked, "                 "add-in cannot serve) - NOT ready. AUTOMATE the sign-in (see the fusion-onboarding "-                "skill for the proven sequence: clear stale signin.autodesk.com tabs, 'Retry browser "-                "sign-in', click 'Open Product' via desktop_click/isTrusted, tick 'Always allow' + "-                "'Open' on the protocol overlay), then poll until ready:true. NEVER enter the "-                "password/2FA; to ask the user, notify_user works directly (add a cross-AD `target` "-                "if you're on a VM).")+                "skill for the proven sequence: desktop_screenshot_window the Fusion hwnd and READ "+                "its screenshots[] array - owned popups ride back as child shots; UIA-click 'Sign In', "+                "'Continue with Google' (warm SSO), then isTrusted-click 'Open Product' + 'Always "+                "allow'+'Open' on the protocol overlay), then poll until ready:true. NEVER enter the "+                "password/2FA yourself; if the flow truly needs the user (password/2FA prompt), call "+                "fusion_notify_owner {title, body} - it toasts their MAIN computer cross-AD. Last "+                "resort only.")     elif ready and stale_info.get("addinStale"):         hint = stale_info["_staleHint"]     elif ready:@@ -1227,6 +1269,12 @@ COMMAND_HANDLERS = {             "_hint": "No Fusion dialogs/owned popups are currently up."}     ),     "addin_status": lambda fi, args: _handle_addin_status(),+    # LAST-RESORT human escalation (John 2026-07-07): when the bridge is truly blocked+    # on the user (password/2FA, UAC), send an AD toast that reaches their MAIN machine+    # (reach_user=True fans out to peer ADs, so a bridge running on an unattended VM+    # still lands the toast where the user actually is). ALWAYS exhaust programmatic+    # options first - this exists so the AI has ONE deterministic call when it must ask.+    "notify_owner": lambda fi, args: _handle_notify_owner(args),     # APS cloud search (pure HTTPS, works with Fusion closed). Lives in     # COMMAND_HANDLERS so it returns BEFORE any Fusion-running gate.     "aps_status": lambda fi, args: aps.handle_status(args),@@ -1721,18 +1769,45 @@ def _orchestrate_open_lbr(args: dict) -> dict:     file_path = file_path.replace("\\", "/")     results = [] -    # Step 1: Open the .lbr via Document.newDesignFromLocal+    # Step 1: Open the .lbr via Document.newDesignFromLocal.+    # ⚠️ TIMEOUT + VERIFY-BY-STATE (fixed 2026-07-07, caught live on a GPU-less Azure+    # VM): the open can take 60s+ on slow/software-rendered machines, so a fixed 30s+    # read timeout expired mid-open and the canned proxy error claimed the ADD-IN was+    # "not running" while the document was actually opening fine (fusion_build_library_3d+    # then aborted all parts on a lie). Now: a generous timeout, AND on ANY failure we+    # poll get_app_state for the expected document name - the doc actually being open+    # outranks whatever the synchronous return claimed.     open_result = _proxy_to_addin("execute_text_command", {         "command": f"Document.newDesignFromLocal {file_path}",-    }, timeout=30)+    }, timeout=120)      if not open_result.get("success"):-        return {-            "success": False,-            "error": f"Document.newDesignFromLocal failed: {open_result.get('error', 'unknown')}",-            "data": {"filePath": file_path},-        }-    results.append("Document.newDesignFromLocal: ok")+        # Verify by STATE before failing: did the doc open anyway?+        expected = file_path.replace("\\", "/").rsplit("/", 1)[-1]+        expected = expected.rsplit(".", 1)[0].lower()+        opened_anyway = False+        for _ in range(20):  # up to ~60s of settling+            time.sleep(3)+            try:+                st = _proxy_to_addin("get_app_state", {}, timeout=8)+                active = str((st.get("data") or {}).get("activeDocument", "")).lower()+                if expected and expected in active:+                    opened_anyway = True+                    break+            except Exception:+                pass+        if not opened_anyway:+            return {+                "success": False,+                "error": f"Document.newDesignFromLocal failed: {open_result.get('error', 'unknown')}",+                "_hint": ("The open did not complete AND the document never became active. On slow/"+                          "software-rendered machines opens can take 60s+; this call already waited + "+                          "verified by state. Check fusion_check_dialogs for a blocking modal, then retry."),+                "data": {"filePath": file_path},+            }+        results.append("Document.newDesignFromLocal: ok (verified by app state after slow open)")+    else:+        results.append("Document.newDesignFromLocal: ok")      # Step 2: Navigate to symbol if requested     if symbol_name: