← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.346\ No newline at end of file+1.9.347\ No newline at end of file
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.346",+  "version": "1.9.347",   "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.",
dashboard/Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "fusion-dashboard"-version = "1.9.346"+version = "1.9.347" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.346"+version = "1.9.347" edition = "2021"  [dependencies]
install.sh+1−1
@@ -21,7 +21,7 @@ echo "Installed fusion-bridge skills: $(ls "$PKG_DIR/skills" 2>/dev/null | tr '\ # binary (the three-artifact pattern: bridge zip -> ab, THIS pkg -> container skills + # this download, page media -> wiki). Per the wiki's PKG_VS_RELEASE policy, binaries are # platform-tagged RELEASE assets, never tarball content - the installer fetches them.-DASH_VERSION="1.9.346"+DASH_VERSION="1.9.347" DASH_URL="https://wiki.adom.inc/download/adom/fusion-bridge/${DASH_VERSION}/fusion-dashboard" DEST="" if [ -w /usr/local/bin ]; then DEST=/usr/local/bin
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.346",+  "version": "1.9.347",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {
page.json+11−27
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.346",+  "version": "1.9.347",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {@@ -11,6 +11,10 @@     "uninstall": "./uninstall.sh"   },   "tags": [+    "ab",+    "bridge",+    "fusion",+    "desktop-app",     "fusion360",     "autodesk",     "cad",@@ -75,7 +79,10 @@     "build a basic parts library",     "add a 3d model to a component",     "make a fusion 360 library with 3d",-    "set up the fusion bridge"+    "set up the fusion bridge",+    "fusion dashboard",+    "fusion status page",+    "open the fusion launch page"   ],   "discovery_pitch": "Use the Adom Bridge 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 Bridge. 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 Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.",@@ -104,28 +111,5 @@       "label": "Part volume",       "prompt": "How much material does each printed part in this assembly use?"     }-  ],-  "agent_permissions": {-    "allow_bash": [],-    "allow": [-      "adom-desktop-fusion-bridge read-only verbs (via the adom-bridge CLI): fusion_readiness, fusion_get_app_state, fusion_addin_status, fusion_check_dialogs - state queries; they change nothing.",-      "adom-desktop-fusion-bridge read-only verbs: fusion_alerts (reads Fusion's own log for toasts), fusion_document_info, fusion_window_info, fusion_board_info, fusion_library_parts - inspect only.",-      "adom-desktop-fusion-bridge read-only verbs: fusion_prove_result (a persisted run report), fusion_plan_library_import (report-only preflight), fusion_list_cloud_projects, fusion_describe.",-      "adom-desktop-fusion-bridge evidence captures fusion_screenshot_fusion and fusion_capture_library_views read pixels and write local image files only."-    ],-    "soft_deny": [-      "adom-desktop-fusion-bridge lifecycle verbs fusion_stop, fusion_kill, fusion_restart_for_addin close or kill the Fusion application - keep the human prompt.",-      "adom-desktop-fusion-bridge fusion_install_fusion silently installs Autodesk Fusion on the user's machine.",-      "adom-desktop-fusion-bridge fusion_cleanup_cloud_files deletes files from the user's Autodesk cloud hub.",-      "adom-desktop-fusion-bridge managed-library verbs (fusion_managed_library_push/pull/create/link/unlink) publish to or overwrite the user's library.io libraries."-    ],-    "environment": [-      "adom-desktop-fusion-bridge is the Fusion 360 bridge for Adom Bridge; its fusion_* verbs are invoked through the adom-bridge CLI and drive Autodesk Fusion on the user's own desktop.",-      "adom-desktop-fusion-bridge desktop actions also pass Adom Bridge's own human-approval gate (caller identity, per-call reason, Activity Log) - a container approval is the first of two gates."-    ]-  },-  "author": {-    "name": "John Lauer",-    "email": "[email protected]"-  }-}\ No newline at end of file+  ]+}
server.py+61−3
@@ -10162,9 +10162,15 @@ def _orchestrate_demo_tour(args: dict) -> dict:             # got in 1.9.182/188 - close every doc the tour opened, plus unmodified             # Untitleds, success or failure. Pre-existing docs are never touched.             try:+                _SWEEP_LAST_DEFERRED[:] = []                 state["tourDocsClosed"] = _close_docs_opened_since(_docs_before_tour)-            except Exception:-                pass+                if _SWEEP_LAST_DEFERRED:+                    state["tourDocsDeferred"] = list(_SWEEP_LAST_DEFERRED)+            except Exception as _swe:+                # Never silent (2026-09-03: tourDocsClosed came back None while five+                # ADOM-TEST tabs sat open, and nothing said why).+                state["tourDocsClosed"] = []+                state["tourDocsSweepError"] = str(_swe)[:200]             if args.get("captions", True):                 try:                     _ad_call("desktop_caption", {"action": "hide", "id": "fusion-tour"}, timeout=8)@@ -11828,6 +11834,16 @@ COMMAND_HANDLERS["list_library"] = _handle_list_library   +# Expected workspace per evidence view. Calibrated, not guessed: every stamp across three+# green multi-part runs on 2026-09-03 (15 parts, 52 frames) read exactly these.+_PROVE_EXPECTED_WORKSPACE = {+    "symbol-editor": "Electronics Library",+    "footprint-editor": "Electronics Library",+    "library-manager": "Electronics Library",+    "3d-editor": "Design",+}++ def _prove_capture(label: str, view: str) -> dict:     """Settle, capture full-screen, and MEASURE the frame. A capture is evidence only if it     is app-sized: a 237x39 title-bar sliver once passed a byte-size check and got reported as@@ -11880,6 +11896,23 @@ def _prove_capture(label: str, view: str) -> dict:                                 "(a title-bar sliver or a stray dialog)" % (int(w), int(h)))             else:                 out["ok"] = True+            # PER-VIEW HARD GATE (#52 item 2, calibrated 2026-09-03 from three green runs):+            # every symbol / footprint / ganged frame stamped "Electronics Library", every+            # 3D-editor frame stamped "Design". A frame whose workspace stamp names a+            # different editor photographed the wrong thing and is refused as evidence -+            # the false-pass class this pipeline exists to prevent. A MISSING stamp does+            # not fail the frame (it cannot be measured), it is only reported.+            _expected = _PROVE_EXPECTED_WORKSPACE.get(view)+            _stamp = out.get("activeWorkspace")+            if out["ok"] and _expected and _stamp and _stamp.lower() != _expected.lower():+                out["ok"] = False+                out["errorCode"] = "wrong_workspace"+                out["error"] = ("wrong_workspace: the %s frame was captured in the %r workspace, "+                                "expected %r - Fusion was showing a different editor when the "+                                "shot was taken, so this is not evidence of the %s view."+                                % (view, _stamp, _expected, label))+            elif out["ok"] and _expected and not _stamp:+                out["workspaceStamp"] = "missing (not gated)"             # INLINE COPY FOR BROWSER CALLERS (issue #47): the path is unreadable from             # wiki.adom.inc web control - it cannot open C:/tmp. A ~500px data URI per             # frame keeps the whole four-frame set renderable straight from the poll@@ -11945,6 +11978,9 @@ def _open_doc_names() -> set:         return set()  +_SWEEP_LAST_DEFERRED = []   # names the last sweep could not close (hub upload still active)++ def _close_docs_opened_since(before: set, keep: set = None) -> list:     """Close every document that was NOT open when `before` was snapshotted. Used by the     create_library chain on BOTH success and failure. Everything a chain opens is bridge@@ -11986,6 +12022,24 @@ def _close_docs_opened_since(before: set, keep: set = None) -> list:          targets = set(_open_doc_names() - set(before or ()))         targets |= {n for n in _open_doc_names() if _bridge_owned(n)}+        # DRAIN HUB JOBS ONCE, UP FRONT (the tour's 5-tab leak, 2026-09-03): after a 3D bind+        # the f3d upload keeps app.hasActiveJobs true for minutes, and every close then hit+        # the per-close "upload in flight, deferred" guard, so the sweep closed NOTHING and+        # the tour reported nothing. One bounded wait here, then the closes go through.+        import time as _tw+        _drain_deadline = _tw.time() + 240+        while _tw.time() < _drain_deadline:+            try:+                _jr = _proxy_to_addin("run_modeling_script",+                                      {"script": "import adsk.core\n"+                                                 "result={'jobs': adsk.core.Application.get().hasActiveJobs}"},+                                      timeout=10)+                if not (((_jr.get("data") or {}).get("result") or {}).get("jobs")):+                    break+            except Exception:+                break+            _tw.sleep(5)+        deferred = []         for n in sorted(targets):             if _bare(n) in keep:                 continue                 # the finished library stays open for the user@@ -11996,11 +12050,15 @@ def _close_docs_opened_since(before: set, keep: set = None) -> list:                     break                 r = _close_bridge_doc_verified(n)                 if not r.get("closed"):-                    break+                    if r.get("deferred"):+                        deferred.append(n)+                    break                # move on to the next family, never stall the sweep                 closed.append(n)                 total2, mine2 = _count_named(n)                 if mine2 >= mine:                     break                # the close did not reduce this family; stop looping+        if deferred:+            _SWEEP_LAST_DEFERRED[:] = deferred         # UNTITLED SWEEP (John's 5-tab screenshot + the []-swept rerun, 2026-08-23): the         # add-in names every unsaved default doc literally "Untitled" - a SET collapses         # them, so the before/after diff sees zero change however many pile up. Close