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
v1.6.3: cloud folder hygiene (Adom AI Workspace, never root) + precise urn cleanup
9 files changed
+230−23
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.6.2+1.6.3
CHANGELOG.md+12@@ -3,6 +3,18 @@ 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.3 - 2026-06-29++- **Cloud folder hygiene - the bridge NEVER writes loose files to a project ROOT anymore.** Root cause+ of a real incident: `fusion_make_3d_package` defaulted its upload to the shared Adom project ROOT, so+ a big build dumped 100+ loose f3d files into the team's root and colleagues complained. Now uploads+ default to an AI-owned **`Adom AI Workspace`** folder with a **per-task subfolder**+ (`build_library_3d` derives the task from the .lbr name), and `fusion_aps_upload` **REFUSES** a known+ root folderId (`errorCode: refused_root_upload`). New **`fusion-cloud-hygiene`** skill.+- **NEW `fusion_cleanup_cloud_files`** - precisely delete a list of cloud files **by lineage urn** (not+ by name), so cleaning up AI clutter can't touch a teammate's file in a shared folder. The add-in+ delete gained a `fileId`/`lineageUrn` exact-match path.+ ## 1.6.2 - 2026-06-29 - **`fusion_capture_library_views` settle fix.** It fired the screenshot ~0.4s after `EDIT`, before
addin/AdomBridge/commands/cloud_documents.py+9−3@@ -526,8 +526,9 @@ def handle_delete_cloud_file(app: adsk.core.Application, args: dict) -> dict: - folderPath: Subfolder path (optional, defaults to root folder). """ file_name = args.get("fileName", "").strip()- if not file_name:- return {"success": False, "error": "No fileName specified."}+ file_id = (args.get("fileId") or args.get("lineageUrn") or "").strip() # PRECISE delete by id+ if not file_name and not file_id:+ return {"success": False, "error": "Provide fileName or fileId (lineage urn for a precise delete)."} project_name = args.get("projectName", "") folder_path = args.get("folderPath", "")@@ -552,6 +553,7 @@ def handle_delete_cloud_file(app: adsk.core.Application, args: dict) -> dict: target = None started = time.time() needle = file_name.lower()+ id_needle = file_id # exact lineage-urn match when provided (safe in shared folders) try: try: data_files = folder.dataFiles@@ -574,7 +576,11 @@ def handle_delete_cloud_file(app: adsk.core.Application, args: dict) -> dict: } try: df = data_files.item(i)- if df.name.lower() == needle:+ if id_needle:+ if df.id == id_needle:+ target = df+ break+ elif df.name.lower() == needle: target = df break except Exception:
adom-bridge-fusion-manifest.json+5−5@@ -1,13 +1,13 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.6.2",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.2/adom-bridge-fusion-v1.6.2.zip",- "sha256": "a3f9922f29273ca0b24eab7a0cd744fd3aa5b84a7c3439f2223b4b7066e97657",- "size": 285138,+ "version": "1.6.3",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.3/adom-bridge-fusion-v1.6.3.zip",+ "sha256": "27aab4981320faddb6828e69d70f4ebcb0b082ab2d38f2a0a5af5b855a1ee363",+ "size": 288303, "verbPrefixes": [ "fusion_" ], "healthEndpoint": "/status",- "released_at": "2026-06-29T19:30:00Z"+ "released_at": "2026-06-29T20:30:00Z" }
aps.py+10@@ -941,6 +941,16 @@ def handle_upload(args: dict) -> dict: project_id, folder_id, local = args.get("projectId"), args.get("folderId"), args.get("localPath") if not (project_id and folder_id and local): return {"success": False, "error": "Provide projectId, folderId, localPath."}+ # FOLDER HYGIENE: never dump loose files into a shared project ROOT (see fusion-cloud-hygiene).+ _KNOWN_ROOT_FOLDERS = {"urn:adsk.wipprod:fs.folder:co.jyO4vxQXR6S9zFpTQWnZAg"}+ if folder_id in _KNOWN_ROOT_FOLDERS:+ return {"success": False, "errorCode": "refused_root_upload",+ "error": "REFUSED: that folderId is a shared project ROOT - the bridge never writes "+ "loose files there (it clutters the team's cloud).",+ "_hint": "Create/choose a real subfolder first (fusion_aps_create_folder under an "+ "'Adom AI Workspace' folder), or just call fusion_make_3d_package / "+ "fusion_build_library_3d, which now default to a non-root workspace folder. "+ "See the fusion-cloud-hygiene skill."} if not os.path.isfile(local): return {"success": False, "error": f"Local file not found: {local}"} fname = args.get("fileName") or os.path.basename(local)
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.6.2",+ "version": "1.6.3", "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://github.com/adom-inc/adom-desktop-fusion-bridge", "author": "Adom Inc.",
describe.py+3@@ -86,6 +86,9 @@ _T = [ ("fusion_capture_library_views", "Capture the LIBRARY-EDITOR views EEs trust: the schematic SYMBOL (full pinout), the FOOTPRINT (pads + layer stack), and the COMPONENT/device view (Content Manager: symbol + the package table with the footprint<->package Mapped check + pin/pad counts). The 3D before/after shots don't show these. Requires the .lbr OPEN in the Electronics Library workspace (fusion_open_lbr first). Runs EDIT <pkg>.sym/.pac/.dev + WINDOW FIT + a background hwnd screenshot per view; returns the PNG paths on the box (C:/tmp/conduit-screenshots - pull with desktop_pull_file). EDIT uses the DEVICESET name (e.g. ESP32-S3FN8).", {"packages": "required [<deviceset name>] (or 'package': single name)", "views": "optional subset of ['component','symbol','footprint'] (default all)", "settle": "optional float secs after each EDIT before capture (default 1.5; raise if a shot shows the PREVIOUS part)"}, {"success": "bool", "captured": "[{package,view,path,ok}]"}, 300, _STATUS, True, {"packages": ["ESP32-S3FN8", "ATSAMD51J20A-AUT"]}),+ ("fusion_cleanup_cloud_files", "PRECISELY delete a list of cloud files by lineage urn - SAFE cleanup of AI-created clutter. Deletes ONLY the exact fileIds given (never name-guessing), so it can't touch a teammate's file in a shared folder. Loops server-side. Get the ids from fusion_aps_browse (item .id). Use this to remove f3d files the bridge created. A big list takes minutes (relay may time out at ~60s while deletes continue server-side - re-browse to confirm).",+ {"fileIds": "required [<lineage urn>]", "projectName": "optional str (default active project)", "folderPath": "optional str (default root - where the files live)"},+ {"success": "bool", "deletedCount": "int", "failedCount": "int", "failed": "[{fileId,error}]"}, 1800, _STATUS, True, {"fileIds": ["urn:adsk.wipprod:dm.lineage:abc123"], "projectName": "Adom"}), ("fusion_run_modeling_script", "Run an adsk.fusion modeling script in the live session (sketches/extrudes/params). Free programmatic CAD.", {"script": "required str (Python using adsk; globals: adsk, app, ui, design)"}, {"result": "obj", "documentName": "str"}, 180, None, True, {"script": "import adsk.fusion\n# ...build geometry..."}),
server.py+132−13@@ -1216,19 +1216,78 @@ def _orchestrate_attach_3d_package(args: dict) -> dict: } -# Default APS upload target for 3D-package f3d uploads (the MAIN Adom project + its-# bridge-uploads folder). Used when build_library_3d / make_3d_package are called WITHOUT-# an explicit projectId/folderId, so the AI never has to browse for them. Override by-# passing projectId/folderId.-_DEFAULT_UPLOAD_PROJECT = "a.YnVzaW5lc3M6YWRvbTMjMjAyMzExMjk3MDM5NjAzMzE"-_DEFAULT_UPLOAD_FOLDER = "urn:adsk.wipprod:fs.folder:co.jyO4vxQXR6S9zFpTQWnZAg"+# ── Fusion cloud FOLDER HYGIENE (never write loose files to a shared project ROOT) ───────────+# Hard lesson (2026-06-29): defaulting uploads to a project's ROOT folder dumped 100+ loose f3d+# files into the shared Adom team root and other employees complained. RULE: the bridge NEVER+# writes a file to a project root. It writes into an AI-OWNED "Adom AI Workspace" folder, with a+# per-task SUBfolder, keeping the cloud tidy. See the fusion-cloud-hygiene skill.+_DEFAULT_UPLOAD_PROJECT = "a.YnVzaW5lc3M6YWRvbTMjMjAyMzExMjk3MDM5NjAzMzE" # the Adom business project+# The shared team ROOT folder of that project - OFF LIMITS for loose files (only the workspace+# folder itself may live here). Known roots we must refuse as a write target.+_KNOWN_ROOT_FOLDERS = {"urn:adsk.wipprod:fs.folder:co.jyO4vxQXR6S9zFpTQWnZAg"}+_AI_WORKSPACE_NAME = "Adom AI Workspace" # the AI-owned work area (BRAND: "Adom AI", not "Claude")+_ws_folder_cache = {}+++def _safe_folder_name(name: str) -> str:+ import re as _re+ return _re.sub(r'[<>:"/\\|?*]', "", str(name or "")).strip()[:60] or "task"+++def _find_child_folder(project_id: str, parent_id: str, name: str):+ """folderId of a subfolder named `name` directly under parent_id, or None."""+ try:+ r = aps.handle_browse({"projectId": project_id, "folderId": parent_id})+ items = (r.get("data") or {}).get("items", []) if isinstance(r, dict) else []+ for it in items:+ if it.get("type") == "folders" and (it.get("name") or "").strip() == name:+ return it.get("id")+ except Exception:+ pass+ return None+++def _ensure_workspace_folder(project_id: str, task: str = None):+ """Return a folderId inside the AI-owned 'Adom AI Workspace' (NEVER a project root). Creates the+ workspace folder (one tidy folder under the project root) + an optional per-task subfolder if+ missing. Cached per (project, task). Returns None if it cannot be resolved (caller must NOT then+ fall back to root)."""+ task = _safe_folder_name(task) if task else None+ key = (project_id, task or "")+ if key in _ws_folder_cache:+ return _ws_folder_cache[key]+ root = next(iter(_KNOWN_ROOT_FOLDERS)) # parent for the single workspace folder+ ws = _find_child_folder(project_id, root, _AI_WORKSPACE_NAME)+ if not ws:+ cr = aps.handle_create_folder({"projectId": project_id, "parentFolderId": root, "name": _AI_WORKSPACE_NAME})+ ws = (cr.get("data") or {}).get("folderId") if isinstance(cr, dict) and cr.get("success") else None+ folder = ws+ if task and ws:+ sub = _find_child_folder(project_id, ws, task)+ if not sub:+ cr = aps.handle_create_folder({"projectId": project_id, "parentFolderId": ws, "name": task})+ sub = (cr.get("data") or {}).get("folderId") if isinstance(cr, dict) and cr.get("success") else None+ folder = sub or ws+ if folder:+ _ws_folder_cache[key] = folder+ return folder def _discover_upload_target(args: dict) -> tuple:- """(projectId, folderId) for f3d uploads: explicit args win, else the known MAIN-project- upload folder, so the AI never has to fusion_aps_browse for them."""- return (args.get("projectId") or _DEFAULT_UPLOAD_PROJECT,- args.get("folderId") or _DEFAULT_UPLOAD_FOLDER)+ """(projectId, folderId) for f3d uploads - ALWAYS a non-root, AI-owned folder.++ Defaults to 'Adom AI Workspace/<task>' (auto-created). If the caller explicitly passes a+ folderId that is a known project ROOT, it is REFUSED (we steer to the workspace instead) - the+ bridge must never write loose files to a shared root. Returns (projectId, folderId|None);+ folderId is None only if the workspace folder could not be created (caller must error, NOT+ fall back to root)."""+ project_id = args.get("projectId") or _DEFAULT_UPLOAD_PROJECT+ fid = args.get("folderId")+ if fid and fid in _KNOWN_ROOT_FOLDERS:+ fid = None # explicit root -> refuse, steer to workspace+ if not fid:+ fid = _ensure_workspace_folder(project_id, args.get("task"))+ return (project_id, fid) def _capture_labeled(label) -> str | None:@@ -1319,10 +1378,16 @@ def _orchestrate_make_3d_package(args: dict) -> dict: import os as _os, time as _time lbr_path = (args.get("lbrPath") or args.get("filePath") or "").replace("\\", "/") model_path = (args.get("modelPath") or "").replace("\\", "/")- project_id, folder_id = _discover_upload_target(args) # default to the MAIN-project upload folder+ project_id, folder_id = _discover_upload_target(args) # an AI-owned non-root folder (never project root) if not lbr_path or not model_path: return {"success": False, "error": "lbrPath (.lbr with the footprint) and modelPath (.step) are required (Windows paths)."}+ 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."} cap_label = args.get("captureLabel") # when set, capture BEFORE (footprint) + AFTER (chip placed) orient = args.get("orient", True) base = _os.path.basename(model_path).rsplit(".", 1)[0]@@ -1451,7 +1516,16 @@ def _orchestrate_build_library_3d(args: dict) -> dict: "projectId/folderId default to the MAIN-project upload folder."} out_path = (args.get("outLbrPath") or combined).replace("\\", "/") capture = args.get("capture", True)- project_id, folder_id = _discover_upload_target(args)+ # Put this library's f3d in its OWN task subfolder under 'Adom AI Workspace' (never project root).+ 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."} results = []; bindings = []; shots = [] for p in parts: pkg = p.get("package")@@ -1600,6 +1674,49 @@ def _orchestrate_capture_library_views(args: dict) -> dict: } +def _orchestrate_cleanup_cloud_files(args: dict) -> dict:+ """Precisely delete a LIST of cloud files by lineage urn - SAFE cleanup of AI-created clutter.++ Deletes ONLY the exact `fileIds` (lineage urns) given - never name-guessing, so it cannot touch a+ teammate's file in a shared folder. Loops server-side (one call deletes the whole list). Use this+ to clean up f3d files the bridge created. (Find the ids with fusion_aps_browse on the folder.)++ args: {fileIds: [<lineage urn>, ...], projectName?: str (default active), folderPath?: str+ (default root - where the file lives)}+ """+ file_ids = args.get("fileIds") or []+ if not file_ids:+ return {"success": False, "error": "fileIds [lineage urns] required.",+ "_hint": "Get them from fusion_aps_browse {projectId, folderId} (item .id)."}+ project_name = args.get("projectName", "")+ folder_path = args.get("folderPath", "")+ deleted = []; failed = []+ for fid in file_ids:+ try:+ r = _proxy_to_addin("delete_cloud_file",+ {"fileId": fid, "projectName": project_name, "folderPath": folder_path},+ timeout=40)+ if isinstance(r, dict) and r.get("success"):+ deleted.append(fid)+ else:+ failed.append({"fileId": fid, "error": (r.get("error") if isinstance(r, dict) else str(r))[:90]})+ except Exception as e:+ failed.append({"fileId": fid, "error": str(e)[:90]})+ return {+ "success": len(deleted) > 0 or not failed,+ "deletedCount": len(deleted),+ "failedCount": len(failed),+ "failed": failed[:25],+ "_hint": (+ f"Deleted {len(deleted)}/{len(file_ids)} cloud files by PRECISE lineage urn (no name-guessing, "+ "so teammates' files are untouched). NOTE: a large list takes minutes and the relay request "+ "may time out at ~60s while the deletes continue server-side - re-browse the folder to confirm "+ "the count dropped. Anything in failed[] usually means the file was already gone or you lack "+ "delete permission. See the fusion-cloud-hygiene skill."+ ),+ }++ def _orchestrate_save_lbr(args: dict) -> dict: """Save the currently open Electronics library as a .flbr file. @@ -1669,7 +1786,7 @@ def dispatch_command(command: str, args: dict) -> dict: # The user should launch Fusion themselves; we just report the status. # Commands that need Fusion running (add-in commands + orchestrated commands) _OPEN_WITH_SCREENSHOT = {"open_schematic", "open_board", "show_3d_board", "show_2d_board", "show_schematic", "import_electronics"}- if command in ADDIN_COMMANDS or command in ("open_lbr", "save_lbr", "attach_3d_package", "make_3d_package", "build_library_3d", "capture_library_views") or command in _OPEN_WITH_SCREENSHOT:+ if command in ADDIN_COMMANDS or command in ("open_lbr", "save_lbr", "attach_3d_package", "make_3d_package", "build_library_3d", "capture_library_views", "cleanup_cloud_files") or command in _OPEN_WITH_SCREENSHOT: if not fusion_info.get("installed"): return { "success": False,@@ -1779,6 +1896,8 @@ def dispatch_command(command: str, args: dict) -> dict: return _orchestrate_build_library_3d(args) if command == "capture_library_views": return _orchestrate_capture_library_views(args)+ if command == "cleanup_cloud_files":+ return _orchestrate_cleanup_cloud_files(args) # Add-in proxy commands if command in ADDIN_COMMANDS:
skills/fusion-cloud-hygiene/SKILL.mdadded+57@@ -0,0 +1,57 @@+---+name: fusion-cloud-hygiene+description: Keep the Fusion 360 / Autodesk cloud TIDY when the bridge creates files (f3d 3D packages, saved libraries, exports). The cardinal rule - NEVER write loose files into a shared project ROOT folder; always work inside an AI-owned "Adom AI Workspace" folder with a per-task SUBfolder. Covers why (a real incident where 100+ loose f3d files dumped into the shared Adom team root and colleagues complained), how the bridge now enforces it (defaults uploads to a non-root workspace folder, REFUSES a root folderId), and how to clean up AI-created clutter PRECISELY by lineage urn (fusion_cleanup_cloud_files) without ever touching a teammate's file. Read this before any verb that writes to the cloud - fusion_make_3d_package, fusion_build_library_3d, fusion_aps_upload, fusion_save_to_cloud, fusion_aps_create_folder. Trigger words - fusion cloud folder, project root, dumped files in root, clean up cloud files, fusion_cleanup_cloud_files, Adom AI Workspace, aps_upload folder, save to cloud folder, folder hierarchy fusion, f3d clutter, delete cloud file, lineage urn delete, shared project cloud, organize fusion cloud, never write to root.+---++# Fusion cloud folder hygiene (never clutter a shared root)++The Autodesk/Fusion cloud is a **shared team space**. When the bridge uploads f3d 3D packages, saves+libraries, or exports, those files are visible to every member of the hub/project. So keeping a clean+folder hierarchy is not optional - it is courtesy to your colleagues.++## ⛔ The cautionary tale (why this skill exists, 2026-06-29)++`fusion_make_3d_package` defaulted its upload target to the **project's ROOT folder**. Over two days a+138-part-library build dumped **100+ loose f3d files** (`R_0R_0402`, `C_220nF_3d`, `Untitled`, ...)+straight into the **shared Adom team root**, mixed in with everyone's real designs. Colleagues+complained that it was a mess. Every rule below is the fix.++## Rule 1 - NEVER write a loose file to a project ROOT folder++Not an f3d, not a save, not an export. A project root is the team's shared landing area; loose files+there clutter it for everyone. The bridge now **refuses** an upload whose `folderId` is a known root+(`errorCode: refused_root_upload`) and **defaults** every f3d upload to a non-root workspace folder.++## Rule 2 - work inside an AI-owned "Adom AI Workspace", with a per-task subfolder++The bridge auto-creates ONE tidy folder, **`Adom AI Workspace`**, and puts each task in its own+SUBfolder, e.g. `Adom AI Workspace/AdomBasicParts-3D/`. So:++- `fusion_make_3d_package` / `fusion_build_library_3d` with NO `folderId` → land in+ `Adom AI Workspace/<task>` automatically (`build_library_3d` derives `<task>` from the .lbr name; or+ pass `task`). You do not have to browse for a folder.+- If you DO pass a `folderId`, make it a real subfolder - never a root. (Create one with+ `fusion_aps_create_folder {projectId, parentFolderId, name}` first.)+- Different project/task/situation → a different subfolder. Keep it organized.++## Rule 3 - clean up after yourself, PRECISELY (by lineage urn, never by name)++To remove f3d clutter the bridge made, use **`fusion_cleanup_cloud_files {fileIds:[<lineage urn>...]}`**.+It deletes ONLY the exact ids you pass (gotten from `fusion_aps_browse` on the folder → each item's+`.id`), so it **cannot** delete a teammate's file even if names collide. NEVER bulk-delete by name in a+shared folder - a colleague may have a file with the same name (especially generic ones like+`Untitled`). A big list takes minutes (the relay request may time out at ~60s while the deletes+continue server-side - re-browse the folder to confirm the count dropped).++```jsonc+// find my files+fusion_aps_browse { "projectId": "...", "folderId": "<the folder>" } // items[].id are lineage urns+// delete exactly mine (protect everyone else's)+fusion_cleanup_cloud_files { "fileIds": ["urn:adsk.wipprod:dm.lineage:...", ...], "projectName": "Adom" }+```++## The discipline, in one line++Pick (or make) a real, non-root, AI-owned subfolder before you write anything to the cloud; clean up+your own files by precise urn when you're done. Treat the shared cloud the way you'd want a colleague+to treat it.