← Commit history

1.9.133: library mint works without APS (defer the no_work_folder gate to the upload fallback), survive a sick data API (guarded Main-by-name fallback), wait for async first-save lineage, prove marks unobservable thumbnail as unverified not failed, AUTH-001 hint learns the developer-hub dead end (#93)

Colby Knox ·60fddf6584 ·1mo ago ·parent 87e86ad
3 files changed +111−26
CHANGELOG.md+26
@@ -3,6 +3,32 @@ All notable changes to the Adom Bridge Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/fusion-bridge · GitHub backup: `adom-inc/fusion-bridge` +## 1.9.133 - 2026-08-21 (library mint works without APS)++- **fusion_create_library / build_library_3d no longer require an APS sign-in.** The mint's+  primary path has saved the f3d straight to the cloud through the add-in (doc.saveAs into the+  'Adom AI Workspace' DataFolder) since 2026-08-15, but an up-front _discover_upload_target gate+  still hard-failed the whole chain with no_work_folder whenever APS could not resolve the+  workspace folder - killing every chain on a box with no APS auth even though the happy path+  never used it. The gate now lives at the APS upload FALLBACK, where APS is actually needed.+  Found live on a fresh install where AUTH-001 (the shared app lost its Data Management API+  product) made APS sign-in impossible (adom-bridge#93); with this change the full chain proved+  a library (4/4 frames) on that box with zero APS.+- **The mint's saveAs script survives a sick data API.** app.data.activeProject can raise+  InternalValidationError (id.size()) on some installs, and individual DataProject property+  reads can too. The script now falls back to resolving the default upload project by id then+  by name 'Main', with per-project guards so one poisoned project cannot abort the search.+- **First-ever cloud saves sync asynchronously.** Right after doc.saveAs on a fresh install,+  dataFile.id can still be the local cache path; the lineage urn appears when the upload lands.+  The script now waits (bounded) for the dm.lineage id instead of misreading the local path as+  a failed save.+- **The prove verdict distinguishes 'cannot check' from 'checked and absent'.** Without APS the+  cloud thumbnail state is unobservable; that now surfaces as thumbnailUnverified:+  aps_unavailable (with a note) instead of failing an otherwise 4/4-proven run.+- **AUTH-001 recovery hint learns the developer-hub dead end.** When the signing-in user is not+  a member of the APS developer hub, myapps shows no app and no portal driving from that account+  can help; the hint now says to check that FIRST and route the fix to the hub admin.+ ## 1.9.76 - 2026-08-18 (ralph, dashboard)  - **"Fusion is opening right now" LED (John's ask).** A launch takes 30-90s and the board used to
aps.py+9−10
@@ -119,16 +119,15 @@ def _aps_auth001_recovery_hint(client_id: str) -> str:         "Allow') - nbrowser_navigate the session to the new authUrl, then nbrowser_click 'Allow'; the URL "         "flips to localhost:<port>/fusion-aps/callback?code=... and the bridge captures the token. "         "(5) Poll fusion_aps_status until signedIn:true. Verified live 2026-07-14. "-        "⚠ STEP ZERO, before driving anything (adom-bridge#93, Colby on a fresh box, "-        "2026-08-21): CHECK THE ACCOUNT CAN SEE THE APP AT ALL. Open "-        "https://aps.autodesk.com/myapps/ - if it says 'you don't have a hub yet' or the "-        "app is not listed, this signed-in user is NOT a member of the app owner's APS "-        "DEVELOPER hub (being in the shared DATA hub does not grant it), and NO amount of "-        "portal driving from this account can fix AUTH-001. STOP, do not burn the user's "-        "time: file/route the re-commit to the APP OWNER (the account that created this "-        "Client ID - for the shared adom app that is the adom org owner), and tell the "-        "user their box will work the moment the owner saves the API Access checkbox - "-        "no action on their side."+        "⚠ FIRST CHECK before driving anything: open myapps AS THIS USER - if it shows "+        "'It seems you don't have a hub yet', this Autodesk account is NOT in the APS "+        "developer hub that owns the app, the app is invisible to it, and NO portal "+        "driving from this account can fix AUTH-001 (dead-ended live 2026-08-21, "+        "adom-bridge#93). Route the fix to the app's owner (the developer-hub admin), "+        "and have them add this user to the developer hub so the next AUTH-001 is "+        "self-servable. Meanwhile the library mint works WITHOUT APS since 1.9.133 "+        "(the add-in saves the f3d straight to the cloud); APS stays needed only for "+        "cloud search and thumbnail verification."     )  
server.py+76−16
@@ -3476,12 +3476,15 @@ def _orchestrate_make_3d_package(args: dict) -> dict:     if not lbr_path or not model_path:         return {"success": False,                 "error": "lbrPath (.lbr with the footprint) and modelPath (.step) are required (Windows paths)."}+    # No APS workspace folder (typically: APS not signed in) is NOT fatal here. The primary+    # path below saves the f3d STRAIGHT to the cloud through the add-in (doc.saveAs into a+    # workspace DataFolder the script creates itself), so APS is only needed at the upload+    # FALLBACK - the gate lives there now. The old up-front gate failed every chain on an+    # APS-less box whose happy path never touched APS (adom/adom-bridge#93, 2026-08-21).+    aps_folder_note = None     if not folder_id:-        return {"success": False, "errorCode": "no_work_folder",-                "error": "Could not resolve a non-root 'Adom AI Workspace' upload folder.",-                "_hint": "The bridge refuses to write to a shared project ROOT. Sign in (fusion_aps_signin) "-                         "so it can create 'Adom AI Workspace', or pass a real (non-root) folderId. NEVER "-                         "pass a project root folderId. See the fusion-cloud-hygiene skill."}+        aps_folder_note = ("APS has no 'Adom AI Workspace' folder (fusion_aps_signin not done?) - "+                           "relying on the add-in's direct cloud save; the APS upload fallback is unavailable.")     cap_label = args.get("captureLabel")  # when set, capture BEFORE (footprint) + AFTER (chip placed)     task = _safe_folder_name(args.get("task") or "AI 3D packages")  # saveAs subfolder (never root)     orient = args.get("orient", True)@@ -3535,16 +3538,51 @@ def _orchestrate_make_3d_package(args: dict) -> dict:         + orient_block +         "try:\n app.activeViewport.fit()\nexcept: pass\n"         # FOLDER HYGIENE: saveAs into an 'Adom AI Workspace'/<task> subfolder, NEVER the project root.-        "proj=app.data.activeProject; rf=proj.rootFolder\n"+        # activeProject is None on a fresh box until the user touches the data panel, so fall+        # back to the default upload project by id, then by name 'Main' (mirrors the add-in's+        # _resolve_project; hub->project listing only, never a recursive file walk).         "def _sub(p,nm):\n"         " for i in range(p.dataFolders.count):\n"         "  if p.dataFolders.item(i).name==nm: return p.dataFolders.item(i)\n"         " return p.dataFolders.add(nm)\n"-        f"wsf=_sub(_sub(rf,'Adom AI Workspace'),'{task}')\n"         "res={}\n"+        "proj=None\n"+        "try: proj=app.data.activeProject\n"+        "except Exception as e: res['projErr']=str(e)[:60]\n"+        # Per-item guards are load-bearing: on some installs individual DataProject+        # property reads raise InternalValidationError (id.size()), and one poisoned+        # project must not abort the search for a healthy one.+        "if not proj:\n"+        " try:\n"+        "  hubs=app.data.dataHubs\n"+        "  for hi in range(hubs.count):\n"+        "   ps=hubs.item(hi).dataProjects\n"+        "   for pj2 in range(ps.count):\n"+        "    p2=ps.item(pj2)\n"+        "    ok=False\n"+        f"    try: ok=(p2.id=='{_DEFAULT_UPLOAD_PROJECT}')\n"+        "    except Exception: ok=False\n"+        "    if not ok:\n"+        "     try: ok=(p2.name=='Main')\n"+        "     except Exception: continue\n"+        "    if ok: proj=p2; break\n"+        "   if proj: break\n"+        " except Exception as e: res['projErr']=str(e)[:60]\n"         "try:\n"+        " rf=proj.rootFolder\n"+        f" wsf=_sub(_sub(rf,'Adom AI Workspace'),'{task}')\n"         f" doc.saveAs('{name}', wsf, '3d package (footprint+chip)', '')\n"-        " res['path']=doc.dataFile.id if doc.dataFile else None\n"+        # A first-ever cloud save on a fresh install syncs ASYNCHRONOUSLY: right after+        # saveAs, dataFile.id can still be the local cache path; the lineage urn appears+        # once the upload lands. Wait for it (bounded well under the 120s script timeout).+        " import time as _tt\n"+        " fid=None\n"+        " for w in range(40):\n"+        "  try: fid=doc.dataFile.id if doc.dataFile else None\n"+        "  except Exception: fid=None\n"+        "  if fid and 'dm.lineage:' in str(fid): break\n"+        "  adsk.doEvents(); _tt.sleep(1.5)\n"+        " res['path']=fid\n"         " res['dims']=[round(dx*10,2),round(dy*10,2),round(dz*10,2)]\n"         "except Exception as e: res['err']=str(e)[:80]\n"         "result=res\n"@@ -3582,6 +3620,15 @@ def _orchestrate_make_3d_package(args: dict) -> dict:                             if isinstance(r, dict) and r.get("error") else ""),                 "before": before_shot, "after": after_shot,                 "_hint": "Confirm modelPath is a valid Windows .step path and Fusion is running."}+    if not folder_id:+        return {"success": False, "errorCode": "no_work_folder",+                "error": "In-app cloud save failed (" + str(res)[:120] + ") and APS has no "+                         "'Adom AI Workspace' folder to upload the local f3d to.",+                "before": before_shot, "after": after_shot,+                "_hint": "Two independent paths can mint the 3D package: the add-in's direct cloud "+                         "save (needs Fusion running + signed in; the res/projErr above says why it "+                         "failed) or the APS upload fallback (needs fusion_aps_signin). Fix either. "+                         "The bridge never writes to a shared project ROOT."}     up = aps.handle_upload({"projectId": project_id, "folderId": folder_id,                             "localPath": f3d_path, "fileName": f3d_name})     up_d = up.get("data", up) if isinstance(up, dict) else {}@@ -3720,12 +3767,9 @@ def _build_library_3d_core(args: dict) -> dict:     import os as _os2     task = args.get("task") or _os2.path.basename(combined).rsplit(".", 1)[0]     project_id, folder_id = _discover_upload_target({**args, "task": task})-    if not folder_id:-        return {"success": False, "errorCode": "no_work_folder",-                "error": "Could not resolve a non-root 'Adom AI Workspace' upload folder.",-                "_hint": "The bridge refuses to write to a shared project ROOT. Sign in "-                         "(fusion_aps_signin) so it can create the workspace folder, or pass a real "-                         "(non-root) folderId. See the fusion-cloud-hygiene skill."}+    # A missing APS workspace folder no longer gates the build: each part's mint saves+    # straight to the cloud through the add-in and only needs APS as its upload fallback+    # (adom/adom-bridge#93). folder_id=None flows down and the mint gates itself.     results = []; bindings = []; shots = []     for p in parts:         pkg = p.get("package")@@ -9586,14 +9630,24 @@ def _handle_prove_library(fusion_info: dict, args: dict) -> dict:     # while every local check passes. Cloud state, not pixels; waits up to 3 min because     # Fusion renders it asynchronously after a bind.     thumb = None+    thumb_unverified = None     if artifacts["package3d"]["present"]:         try:             thumb = _handle_check_3d_thumbnail(fusion_info,                                                {"libraryPath": lbr, "waitSeconds": 180})             artifacts["package3d"]["thumbnail"] = (thumb.get("thumbnail") or {}).get("state")             if not thumb.get("success"):-                missing.append("thumbnail(%s)" % ((thumb.get("thumbnail") or {}).get("state")-                                                  or thumb.get("errorCode") or "unknown"))+                if thumb.get("errorCode") == "no_work_folder":+                    # APS is not signed in on this box, so the cloud thumbnail state CANNOT+                    # be observed - that is "unverified", not "checked and absent". Autodesk+                    # renders the thumbnail from Fusion's own upload regardless of our APS+                    # app, so do not fail the whole verdict over an unobservable check+                    # (adom/adom-bridge#93); surface it loudly instead.+                    thumb_unverified = "aps_unavailable"+                    artifacts["package3d"]["thumbnail"] = "unverified(aps_unavailable)"+                else:+                    missing.append("thumbnail(%s)" % ((thumb.get("thumbnail") or {}).get("state")+                                                      or thumb.get("errorCode") or "unknown"))         except Exception as _e:             missing.append("thumbnail(check-failed)")     captured = [e for e in evidence if e.get("ok") and e.get("screenshot")]@@ -9608,6 +9662,12 @@ def _handle_prove_library(fusion_info: dict, args: dict) -> dict:     out = {"success": proven, "proven": proven, "libraryPath": lbr, "deviceset": want,            "artifacts": artifacts, "missing": missing,            "evidence": evidence, "imagesCaptured": len(captured), "imagesRequired": 4}+    if thumb_unverified:+        out["thumbnailUnverified"] = thumb_unverified+        out["_thumbnailNote"] = ("The cloud thumbnail could not be observed (APS not signed in "+                                 "on this box) - the 4-frame local evidence stands, but verify "+                                 "the Library Manager preview by eye or sign in APS and run "+                                 "fusion_check_3d_thumbnail.")     # RESOLVE OUR OWN DOCUMENT STATE BEFORE RETURNING (deterministic-close rule): the four     # views dirtied the library document; closing it here, verified, means no Save dialog     # can ever be raised at the user for this run's scratch work. The .lbr ON DISK is the