← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.314\ No newline at end of file+1.9.315\ 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.314",-  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.314/adom-bridge-fusion-v1.9.314.zip",-  "sha256": "78001812a20ff59e3bbd3dc0f06a632ae17f13159c904b8cbaab08a9435ce209",-  "size": 593655,+  "version": "1.9.315",+  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.315/adom-bridge-fusion-v1.9.315.zip",+  "sha256": "2743689adc299a92ca843d25c35b77eeb93d9fce353d53ae6ded36f9fc0e397d",+  "size": 595389,   "verbPrefixes": [     "fusion_"   ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.314",+  "version": "1.9.315",   "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.314"+version = "1.9.315" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.314"+version = "1.9.315" 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.314"+DASH_VERSION="1.9.315" 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.314",+  "version": "1.9.315",   "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−8
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.314",+  "version": "1.9.315",   "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,9 +111,5 @@       "label": "Part volume",       "prompt": "How much material does each printed part in this assembly use?"     }-  ],-  "author": {-    "name": "John Lauer",-    "email": "[email protected]"-  }-}\ No newline at end of file+  ]+}
server.py+92
@@ -2826,6 +2826,7 @@ COMMAND_HANDLERS = {         float(args.get("alertsSinceSec", 300))),     # Fusion's own error/warning toasts (the CEF overlays no window enumeration can see).     "managed_library_create": lambda fi, args: _handle_managed_library_create(fi, args),+    "web_signin": lambda fi, args: _handle_web_signin(fi, args),     "alerts": lambda fi, args: {         "success": True,         "alerts": _fusion_alerts(float(args.get("sinceSec", 300))),@@ -15729,6 +15730,97 @@ def _wait_for_dialog(title_substr: str, timeout_s: float = 20.0):     return None  +# ── Signing the user into Autodesk's WEB surfaces (John, 2026-09-01) ─────────+# Fusion's desktop app is signed in, but library.io, the hub portal (adom.autodesk360.com) and+# Autodesk Account are WEB properties with their own session, and pup starts with none. Every+# one of them offers "Continue with Google", and Adom already owns the user's Google identity,+# so ONE stored Google credential unlocks all of them for that user - no per-site passwords.+# The password itself never passes through this bridge or the AI: the user stores it once with+# ab's credential_set (OS keychain), and pup fills it in the background.+_AUTODESK_SIGNIN_HOST = "signin.autodesk.com"+++def _handle_web_signin(fusion_info: dict, args: dict) -> dict:+    """Sign a pup window into an Autodesk web surface, via Google SSO by default.++    args: {url (required), sessionId (default "autodesk-web"), provider ("google"|"password"),+           username (optional, for the Google chooser)}+    Returns where it got to, and NEVER guesses a credential: a missing password or a 2FA+    challenge is reported as a typed stop with the exact one-time command the USER runs.+    """+    import time as _t+    url = (args.get("url") or "").strip()+    if not url:+        return {"success": False, "error": "url is required (e.g. https://library.io/ or "+                                           "https://<hub>.autodesk360.com/)"}+    sess = args.get("sessionId") or "autodesk-web"+    provider = (args.get("provider") or "google").lower()+    user = args.get("username") or ""+    steps = []++    def _pup(verb, a, timeout=90):+        return _ad_call("pup_" + verb, a, timeout=timeout) or {}++    def _state():+        r = _pup("eval", {"sessionId": sess, "expression":+                          "({url:location.href,body:document.body.innerText"+                          ".replace(/\\s+/g,' ').slice(0,400)})"}, timeout=60)+        return (r.get("result") or {}) if isinstance(r, dict) else {}++    _pup("open_window", {"sessionId": sess, "url": url,+                         "reason": args.get("reason") or "sign in to an Autodesk web surface"},+         timeout=120)+    _t.sleep(8)+    st = _state(); steps.append({"step": "open", "url": (st.get("url") or "")[:120]})+    body = str(st.get("body") or "")+    if _AUTODESK_SIGNIN_HOST not in str(st.get("url") or "") and "Sign in to Autodesk" not in body:+        return {"success": True, "alreadySignedIn": True, "sessionId": sess, "steps": steps,+                "_hint": "That surface was already authenticated in this pup profile."}++    if provider == "google":+        r = _pup("eval", {"sessionId": sess, "expression":+                          "(()=>{const b=[...document.querySelectorAll('button,a,div[role=button]')]"+                          ".find(e=>/Continue with Google/i.test(e.textContent));"+                          "if(!b)return {found:false};b.click();return {clicked:true};})()"}, timeout=60)+        steps.append({"step": "google-sso", "result": (r.get("result") if isinstance(r, dict) else None)})+        _t.sleep(9)+        st = _state()+        if "accounts.google.com" not in str(st.get("url") or ""):+            return {"success": False, "errorCode": "google_sso_unavailable", "steps": steps,+                    "state": (st.get("body") or "")[:200],+                    "_hint": "The page did not hand off to Google. Retry with provider:'password'."}++    # Let pup fill from its vault. It never hands the secret to this bridge or to the AI.+    lg = _pup("login", {"sessionId": sess,+                        "url": "https://accounts.google.com/" if provider == "google" else url,+                        "username": user or None,+                        "reason": "sign the user in so Adom can drive their Autodesk web surfaces"},+              timeout=120)+    steps.append({"step": "vault-login", "used": lg.get("used"), "ok": lg.get("success")})+    _t.sleep(8)+    st = _state()+    at = str(st.get("url") or "")+    body = str(st.get("body") or "")+    if "accounts.google.com" in at or _AUTODESK_SIGNIN_HOST in at:+        needs_2fa = any(k in body.lower() for k in ("verify it", "2-step", "verification",+                                                    "authenticator", "code was sent"))+        return {"success": False,+                "errorCode": "human_credential_required" if not needs_2fa else "second_factor_required",+                "sessionId": sess, "steps": steps, "at": at[:120], "page": body[:200],+                "error": ("Stopped at the sign-in page: pup has no stored credential for this "+                          "user" if not needs_2fa else+                          "Google is asking for the second factor, which only the user can supply."),+                "_hint": ("ONE-TIME setup, run by the USER so no password ever reaches the AI:\n"+                          "  adom-bridge credential_set '{\"host\":\"accounts.google.com\",\"username\":"+                          "\"<you>@<domain>\",\"password\":\"<your Google password>\"}'\n"+                          "then re-run this verb. For 2FA, approve the prompt on your phone; the "+                          "session then persists in pup's profile and later runs are silent.")}+    return {"success": True, "signedIn": True, "sessionId": sess, "steps": steps,+            "at": at[:120],+            "_hint": "Signed in. The session persists in pup's profile, so library.io, the hub "+                     "portal and Autodesk Account are now drivable for this user."}++ def _handle_managed_library_create(fusion_info: dict, args: dict) -> dict:     """Publish the library that is IN FRONT as a library.io managed library. 
skills/fusion-web-signin/SKILL.mdadded+73
@@ -0,0 +1,73 @@+---+name: fusion-web-signin+description: Sign an Adom user into Autodesk's WEB surfaces (library.io, the Fusion hub portal <hub>.autodesk360.com, Autodesk Account) from a container, using pup plus ONE stored Google credential - because Fusion's desktop app being signed in does NOT sign in the web. Covers the one-time credential_set the user runs, the Continue-with-Google SSO path, what 2FA does, and why the AI never sees the password. Trigger words - library.io login, autodesk sign in, autodesk360, fusion hub portal, hub invite, managed folder sharing, sign into autodesk in pup, continue with google, credential_set, fusion_web_signin.+---++# Signing an Adom user into Autodesk's web surfaces++**The gap this closes.** Fusion the desktop app is signed in as the user, and the Fusion bridge+drives it happily. But the things you need for SHARING - library.io (managed libraries and their+folders), the hub portal `<hub>.autodesk360.com` (Members and Roles, invites), Autodesk Account -+are WEB properties with their own session. pup starts with none, and none of Fusion's tokens carry+over. Measured 2026-09-01: `library.io` and `fusion.autodesk.com` both landed logged out in pup on+two machines, and `adom.autodesk360.com` redirects straight to `signin.autodesk.com`.++## The design: one Google credential per user, every Autodesk surface++Every Autodesk sign-in page offers **Continue with Google**, and Adom already owns the user's+Google identity (that is how `adom-google` works). So an Adom user stores ONE credential and every+Autodesk web surface becomes drivable - no per-site passwords, no password ever reaching the AI.++**One-time, run BY THE USER** (the secret goes to the OS keychain; the index keeps host+username+only, and `credential_set` never echoes it back):++```bash+adom-bridge credential_set '{"host":"accounts.google.com",+                             "username":"<you>@<domain>",+                             "password":"<your Google password>"}'+```++Then, from any container, for any Autodesk web surface:++```bash+adom-bridge fusion_web_signin '{"url":"https://library.io/"}'+adom-bridge fusion_web_signin '{"url":"https://<hub>.autodesk360.com/","sessionId":"hub"}'+```++`fusion_web_signin` opens a BACKGROUND pup window, detects the Autodesk sign-in page, clicks+Continue with Google, and lets **pup** fill the credential from the vault. The bridge and the AI+never hold the secret.++## What it returns, and the two honest stops++| result | meaning |+|---|---|+| `alreadySignedIn: true` | that pup profile already had the session; nothing to do |+| `signedIn: true` | signed in; the session persists in the pup profile, so later runs are silent |+| `errorCode: human_credential_required` | no stored credential for this user - the response carries the exact `credential_set` line for them to run |+| `errorCode: second_factor_required` | Google wants the second factor; only the user can approve it (phone prompt). After one approval the pup profile keeps the session |+| `errorCode: google_sso_unavailable` | the page did not hand off to Google - retry with `provider:"password"` |++**Never** invent, guess, or ask for the password in chat, and never click through a 2FA prompt on+the user's behalf: those are the two stops this verb exists to report cleanly rather than fake.++## Why an Adom user wants this (the sharing story)++Fusion's own Library Manager banner says it plainly: *"Import private libraries into this Fusion+Hub to avoid permission errors for team members. We recommend using Hub libraries going forward."*+Distribution therefore happens through **hub membership**, and hub membership is managed on the+web:++- hub portal > **Members and Roles > Invite** (email address; the invitee must accept)+- the inviting admin needs an assigned license; a basic-access user cannot invite+- a **team** hub can set Project Administrator / Editor / Viewer per member; a **personal** hub can+  invite to a project but cannot manage permissions at all++None of that is reachable from the Fusion API or the desktop ribbon - it is a web surface, which is+exactly why this verb exists.++## Related++- `dev-skills/fusion-cloud-libraries` - what a `.flbr` is, how a library becomes a library.io+  managed library, and the measured cross-account result (a managed library does NOT cross+  Autodesk accounts on its own).