← Commit history
CHANGELOG.md+7−1
@@ -3,9 +3,15 @@ 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.12.0 (unreleased)+## 1.12.0 (2026-10-27, insiders)  The board model, to the [EDA Bridge Standard](https://wiki.adom.inc/adom/eda-bridge-standard) (fusion-lane thread).+Needs AI Flow 0.2.22 or later for its Fusion lane (a pad's `name` is now its own; `key` is `U1.3`).++- The daily verb suite now runs 20 of the new verbs on the canonical BQ25792 board (the board model, stack-up,+  netlist, pours, the reads, 3D bodies, the BOM's part numbers, the board_summary alias): 83 pass on+  DESKTOP-BAR6S0F; the 5 failures are the APS verbs on a box without the APS sign-in.+- Release exes are signed through Azure Trusted Signing (`tools/sign_pe.py`, from the KiCad bridge's signer).  - `fusion_routing_state` returns the standard's board model (`model: "eda-bridge-standard/board 0.1"`),   computed from Fusion's own board export in the background:
dashboard/Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "fusion-dashboard"-version = "1.11.44"+version = "1.12.0" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.11.44"+version = "1.12.0" 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.11.44"+DASH_VERSION="1.12.0" 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.11.44",+  "version": "1.12.0",   "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+1−1
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.11.44",+  "version": "1.12.0",   "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": {
tests/run_verb_tests.py+40−2
@@ -86,6 +86,20 @@ def expect(d, spec):         no_design = "No active Fusion Design" in str(d.get("error"))  # honest refusal when the open document is not a Design         if not (ok or budget or no_design):             errs.append("expected a BOM or the budget refusal, got error=%s code=%s" % (str(d.get("error"))[:60], d.get("errorCode")))+    if spec.get("pred") == "board_model_ok":+        # the EDA Bridge Standard board model (1.12.0): every pad sized (custom shapes as their+        # bounding rectangle, never 0 x 0), copper layers by KiCad's names, unconnected computed+        pads = d.get("pads") or []+        if d.get("model") != "eda-bridge-standard/board 0.1": errs.append("model is %r, not the standard board model" % d.get("model"))+        if not pads: errs.append("no pads")+        if any(not (p.get("sizeX", 0) > 0 and p.get("sizeY", 0) > 0) for p in pads): errs.append("a pad has no size")+        if "F.Cu" not in (d.get("copperLayers") or []): errs.append("copper layers not named F.Cu..B.Cu")+        if not isinstance(d.get("unconnected"), int): errs.append("no unconnected count")+    if spec.get("pred") == "bom_has_part_numbers":+        data = d.get("data") or {}+        head = (d.get("csv") or "").splitlines()[:1]+        if not head or not all(c in head[0] for c in ("MPN", "LCSC")): errs.append("BOM has no MPN/LCSC columns")+        if not data.get("withLcsc") and not data.get("withMpn"): errs.append("no part carries an MPN or LCSC number")     if d.get("_testTimeout"): errs.append("timed out")     return errs @@ -165,6 +179,29 @@ t("fusion_show_schematic", "daily", {}, {"ok": True}) t("fusion_show_2d_board", "daily", {}, {"ok": True}) t("fusion_board_info", "daily", {}, {"ok": True, "fields": ["rawXml"]}, "needs the 2D board view ACTIVE") t("fusion_open_electronics", "daily", {}, {"ok": True, "fields": ["isElectronics"]}, "no-args form is a pure workspace read; 2D board view active")+# 1.12.0: the EDA Bridge Standard reads, on the canonical board, 2D view active. All read-only:+# they export the board in the background and change nothing (run_drc, repour and the saves write,+# so they stay out of the daily run on a shared cloud design).+B = {"expectDocument": "BQ25792"}+t("fusion_routing_state", "daily", B, {"ok": True, "pred": "board_model_ok"}, "the standard board model: pads sized, copper layers named, unconnected from the copper")+t("fusion_layer_stack", "daily", B, {"ok": True, "fields": ["layers", "totalUm", "copperLayers"]})+t("fusion_netlist", "daily", B, {"ok": True, "fields": ["nets", "netCount"]})+t("fusion_pour_state", "daily", B, {"ok": True, "fields": ["zones", "filledAreaByLayerMm2"]})+t("fusion_component_data", "daily", dict(B, designator="IC1"), {"ok": True, "fields": ["component"]})+t("fusion_component_pins", "daily", dict(B, designator="IC1"), {"ok": True, "fields": ["pins", "count"]})+t("fusion_list_nets", "daily", B, {"ok": True, "fields": ["nets", "count"]})+t("fusion_net_connections", "daily", dict(B, net="VBUS"), {"ok": True, "fields": ["pads", "nets"]})+t("fusion_list_designators", "daily", B, {"ok": True, "fields": ["designators", "count"]})+t("fusion_layers", "daily", B, {"ok": True, "fields": ["layers"]})+t("fusion_copper_layers", "daily", B, {"ok": True, "fields": ["layers"]})+t("fusion_layers_in_use", "daily", B, {"ok": True, "fields": ["copperLayersInUse"]})+t("fusion_read_rules", "daily", B, {"ok": True, "fields": ["rules", "ruleSet"]})+t("fusion_rules_by_kind", "daily", B, {"ok": True, "fields": ["rulesByKind"]})+t("fusion_silk_state", "daily", B, {"ok": True, "fields": ["components", "texts"]})+t("fusion_net_classes", "daily", B, {"ok": True, "fields": ["classes"]})+t("fusion_board_summary", "daily", {}, {"ok": True, "fields": ["aliasOf"]}, "EDA Bridge Standard alias of fusion_board_info")+t("fusion_zoom_to", "daily", dict(B, ref="IC1"), {"ok": True, "fields": ["rect"]}, "frames the view; changes nothing")+t("fusion_export_bom#parts", "daily", {"outputPath": f"{SCRATCH}/bom-parts.csv"}, {"ok": True, "pred": "bom_has_part_numbers"}, "1.12.0: MPN/LCSC columns from the part attributes (page issue #105)") t("fusion_list_text_commands", "daily", {"filter": "Electron"}, {"ok": True, "fields": ["commands"]}, "TextCommands.List needs the electronics editor context") t("fusion_export_source", "weekly", {"outputPath": f"{SCRATCH}/verbtest.fbrd"}, {"ok": True, "fields": ["savedTo"]}, ".fbrd needs the 2D board view ACTIVE (Document.CopyToDesktop)") t("fusion_export_eagle_source", "weekly", {"outputPath": f"{SCRATCH}/verbtest.brd"}, {"ok": True, "fields": ["savedTo"]}, ".brd needs the 2D board view ACTIVE; extracts the EAGLE XML from the .fbrd container")@@ -179,6 +216,7 @@ t("fusion_export_gerbers", "weekly", {"outputDir": SCRATCH}, {"ok": True}, "2D b t("fusion_export_bom", "weekly", {"outputPath": f"{SCRATCH}/bom.csv"}, {"ok": True}, "2D board view active") t("fusion_export_cpl", "weekly", {"outputPath": f"{SCRATCH}/cpl.csv"}, {"ok": True}, "2D board view active") t("fusion_board_stackup", "weekly", {}, {"ok": True}, "exports the .brd - needs the board context")+t("fusion_bodies", "daily", {"expectDocument": "BQ25792"}, {"ok": True, "fields": ["bodies", "noBody", "suspect"]}, "switches to the 3D PCB and back to the 2D board") t("fusion_show_3d_board", "daily", {}, {"ok": True}) t("fusion_inspect_bodies", "daily", {}, {"ok": True, "fields": ["bodies", "units"]}) t("fusion_mcp_call", "weekly", {"tool": "fusion_mcp_read", "arguments": {"queryType": "document"}}, {"ok": True},@@ -332,13 +370,13 @@ t("fusion_route_net#guard", "negative", {"expectDocument": "ZZZ_NoSuchDoc_TEST",   "board/revision guards must refuse before any copper is drawn") t("fusion_routing_drc#guard", "negative", {"expectDocument": "ZZZ_NoSuchDoc_TEST", "boardId": "x", "expectedRevision": "x"}, {"pred": "must_refuse"}, "same guards as route_net") t("fusion_plan_route#guard", "negative", {"expectDocument": "ZZZ_NoSuchDoc_TEST", "net": "NET_1"}, {"pred": "must_refuse"}, "planner needs the live board it was asked about")-for v in ["fusion_routing_state", "fusion_route_net", "fusion_routing_drc", "fusion_plan_route", "fusion_move_parts", "fusion_place_vias", "fusion_apply_pours", "fusion_pour_state", "fusion_board_source", "fusion_ripup_nets", "fusion_schematic_run", "fusion_board_bom", "fusion_change_pours", "fusion_board_seed", "fusion_schematic_plan", "fusion_remove_pours", "fusion_layer_stack", "fusion_netlist", "fusion_bodies", "fusion_component_data", "fusion_component_pins", "fusion_list_nets", "fusion_net_connections", "fusion_list_designators", "fusion_layers", "fusion_copper_layers", "fusion_layers_in_use", "fusion_read_rules", "fusion_rules_by_kind", "fusion_silk_state", "fusion_net_classes", "fusion_run_drc", "fusion_repour", "fusion_zoom_to"]:+for v in ["fusion_route_net", "fusion_routing_drc", "fusion_plan_route", "fusion_move_parts", "fusion_place_vias", "fusion_apply_pours", "fusion_board_source", "fusion_ripup_nets", "fusion_schematic_run", "fusion_board_bom", "fusion_change_pours", "fusion_board_seed", "fusion_schematic_plan", "fusion_remove_pours", "fusion_run_drc", "fusion_repour"]:     t(v, "manual", note="live routing on a fixture board: demo/routing/run_demo.py is the weekly proof (needs a board open)") for v in ["fusion_apply_fusion_update", "fusion_resolve_seat_dialog", "fusion_interactive", "fusion_narrate", "fusion_view", "fusion_electronics_query"]:     t(v, "manual", note="lifecycle, seat or presentation verbs: need a human-visible Fusion state; manual tier")  t("fusion_schematic#guard", "negative", {}, {"pred": "must_refuse"}, "no action: must refuse with the menu, never open a dialog")-for v in ["fusion_launch", "fusion_export_gerber", "fusion_dismiss_dialogs", "fusion_board_summary", "fusion_install"]:+for v in ["fusion_launch", "fusion_export_gerber", "fusion_dismiss_dialogs", "fusion_install"]:     t(v, "manual", note="EDA Bridge Standard alias of an existing verb (the reply carries aliasOf); tested through the verb it names") t("fusion_save_document", "manual", note="saves a cloud design in place (a new version): exercised live on a scratch design in a test folder") t("fusion_read_sheet_file#guard", "negative", {"path": "C:/no/such/file.sch"}, {"pred": "must_refuse"}, "a missing file refuses with file_not_found, never an empty parts list")
tools/release.sh+3−2
@@ -354,7 +354,7 @@ step "Commit + repo push" git add -A git commit -q -m "release $VERSION (lockstep): bridge + dashboard + package all at $VERSION -Co-Authored-By: Claude Opus 4.8 <[email protected]>" || echo "  (nothing to commit)"+Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>" || echo "  (nothing to commit)" # THE LIST WAS INCOMPLETE, AND HAD BEEN FOR MONTHS. Oliver reported it in PR #34 back in # July - "the page-repo server.py is behind the shipped release" - and he was being kind: # describe.py (the verb catalog the AI reads), the WHOLE addin/ tree, and skills/ were@@ -369,7 +369,8 @@ for f in handlers/*.py addin/AdomBridge/*.py addin/AdomBridge/commands/*.py \          addin/AdomBridge/*.manifest skills/*/SKILL.md tools/release.sh CLAUDE.md \          tools/promote.sh tools/promote.py tools/check-portable.sh tools/check-naming.sh tools/freeze-contract.py \          contracts/describe.json rust/Cargo.toml rust/PORTING.md \-         rust/*/Cargo.toml rust/*/src/*.rs \+         rust/*/Cargo.toml rust/*/src/*.rs rust/bridge/src/*.py rust/bridge/src/*.json rust/bridge/src/*.xml \+         tools/sign_pe.py tools/gen-readme-verbs.py tests/*.py contracts/eda-bridge-standard/*.json CHANGELOG.md \          library_bundle.py README.md docs/*.md docs/img/*.svg docs/img/*.png \          docs/img/*/*.png dashboard/ui/*.html; do   [ -f "$f" ] && PUSH_FILES="$PUSH_FILES $f"
tools/sign_pe.py+178−95
@@ -1,18 +1,24 @@ #!/usr/bin/env python3-"""Authenticode-sign Windows PE files as "Adom Industries, Inc." and replace them IN PLACE with the-signed, verified copies. tools/release.sh calls it for the bridge exe; it never publishes anything.--    python3 tools/sign_pe.py <version> <file.exe> [<file.dll> ...]--The same path the altium-bridge release uses (scripts/release.py sign_pe_files): Azure Trusted-Signing runs on the Windows host the PRIVATE config names, reached through Adom Bridge. That config-($ADOM_SIGNING_DIR, default ~/.adom/signing/altium-bridge: config.json + sign-verify.ps1) is-deliberately not in this public repo; without it this script refuses and nothing is signed.--Per file it gates on what the host reports (Get-AuthenticodeSignature status Valid, the expected-subject, a Microsoft timestamp, signtool verify /pa), pulls the signed file back, checks the bytes-arrived intact and that the signer is "Adom Industries, Inc." when read here, and only then-replaces the local file. Exit 0 only when every file passed; a JSON report goes to stdout's last line.+"""Authenticode-sign adom-fusion-bridge.exe as Adom Industries, Inc. through Azure Trusted Signing, the path every+Adom release uses (adom/altium-bridge scripts/release.py sign_pe_files; adapted from adom/kicad-bridge+tools/sign_azure.py).++The signing runs on a Windows host reached through Adom Bridge, under that host's own Azure login. Every+identifier (host, staging folder, endpoint, account, profile, tenant, timestamp URL, expected subject) and the+sign-verify.ps1 script live in a PRIVATE folder outside this public repo: $ADOM_SIGNING_DIR, else+~/.adom/signing/fusion-bridge, else ~/.adom/signing/altium-bridge (the same account signs every Adom bridge).+Nothing from it is ever written to this repo, a reply, a log line or an issue.++  python3 tools/sign_pe.py <version> <exe>     # sign one exe in place; exits non-zero unless verified+                                               # (tools/release.sh runs it; the last stdout line is the report)++On the signing machine itself (this WSL runs on the config's host) it signs locally through powershell.exe,+staging under the host's %LOCALAPPDATA%; elsewhere it goes through Adom Bridge. The flow, each step a gate:+  1. stage the exe and sign-verify.ps1 on the host (send_files; the folder name has no dots);+  2. one run_script signs and verifies there: Get-AuthenticodeSignature Valid, the expected subject,+     a Microsoft timestamp, signtool verify /pa;+  3. pull the signed exe back, check its sha256 matches the host's and differs from the unsigned build;+  4. read its Authenticode signer here (openssl): CN = "Adom Industries, Inc.". """ import base64 import hashlib@@ -29,24 +35,28 @@ SIGNER_ORG = "Adom Industries, Inc." THREAD = "fusion-bridge release signing"  -def die(msg):-    sys.exit("REFUSED: " + msg)-- def signing_dir():-    return Path(os.environ.get("ADOM_SIGNING_DIR", Path.home() / ".adom" / "signing" / "altium-bridge"))+    env = os.environ.get("ADOM_SIGNING_DIR")+    if env:+        return Path(env)+    for name in ("fusion-bridge", "altium-bridge"):+        d = Path.home() / ".adom" / "signing" / name+        if (d / "config.json").is_file():+            return d+    return Path.home() / ".adom" / "signing" / "fusion-bridge"   def authenticode_subjects(path):-    """Certificate subjects in a PE's Authenticode signature, read without Windows: the optional-    header's security directory points at a WIN_CERTIFICATE holding PKCS#7 DER. [] = unsigned."""+    """Certificate subjects in a PE's Authenticode signature, read without Windows: the security data+    directory points at a WIN_CERTIFICATE holding PKCS#7 DER. [] means unsigned."""     b = Path(path).read_bytes()     pe = struct.unpack_from("<I", b, 0x3C)[0]     if b[pe:pe + 4] != b"PE\0\0":-        die(f"{path} is not a PE file")+        raise SystemExit(f"{path}: not a PE file")     opt = pe + 24     magic = struct.unpack_from("<H", b, opt)[0]-    off, size = struct.unpack_from("<II", b, opt + (112 if magic == 0x20B else 96) + 4 * 8)+    dirs = opt + (112 if magic == 0x20B else 96)+    off, size = struct.unpack_from("<II", b, dirs + 4 * 8)     if not off or not size:         return []     length, _rev, ctype = struct.unpack_from("<IHH", b, off)@@ -59,9 +69,15 @@ def authenticode_subjects(path):     return [l[len("subject="):].strip() for l in r.stdout.splitlines() if l.startswith("subject=")]  -def ab(host, verb, args, timeout=600):-    out = subprocess.run(["adom-bridge", "--target", host, "--ai-thread", THREAD, verb, json.dumps(args)],-                         capture_output=True, text=True, timeout=timeout).stdout+def assert_signed(path):+    subs = authenticode_subjects(path)+    if not any(f'CN = "{SIGNER_ORG}"' in s for s in subs):+        raise SystemExit(f"REFUSED: {path} is not Authenticode-signed by {SIGNER_ORG} (found {subs or 'no signature'})")+    return subs+++def ab_json(*args, timeout=600):+    out = subprocess.run(["adom-bridge", *args], capture_output=True, text=True, timeout=timeout).stdout     d = json.loads(out[out.find("{"):]) if "{" in out else {}     o = d.get("output")     if isinstance(o, str) and o.strip().startswith("{"):@@ -72,84 +88,151 @@ def ab(host, verb, args, timeout=600):     return d  -def main():-    if len(sys.argv) < 3:-        sys.exit(__doc__)-    ver, files = sys.argv[1], [Path(p) for p in sys.argv[2:]]-    for f in files:-        if not f.is_file():-            die(f"{f} does not exist")+POWERSHELL = "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe"+++def redact(text, cfg):+    """A message with every private config value blanked: the signing script's hints name the tenant."""+    text = str(text)+    for k in ("endpoint", "account", "profile", "tenant", "timestampUrl", "stageDir"):+        v = str(cfg.get(k) or "")+        if len(v) > 3:+            text = text.replace(v, f"<{k}>")+    return text+++def local_windows_host():+    """This machine's Windows name when we run in WSL on it (the signing machine itself), else None."""+    if not os.path.exists(POWERSHELL):+        return None+    r = subprocess.run(["/mnt/c/Windows/System32/cmd.exe", "/c", "hostname"], capture_output=True, text=True, cwd="/mnt/c")+    return r.stdout.strip() or None+++def checks_of(v, cfg):+    return {+        "Status Valid": v.get("status") == "Valid",+        "signer subject": cfg["expectSubject"] in (v.get("subject") or ""),+        "timestamped": "Microsoft" in (v.get("timestamper") or ""),+        "signtool verify /pa": "Successfully verified" in (v.get("signtoolVerify") or ""),+    }+++def sign_locally(exe, ver, cfg, ps1):+    """On the signing machine itself (WSL on it): stage under its own %LOCALAPPDATA%, run sign-verify.ps1+    with powershell.exe, no Adom Bridge round trip."""+    win = lambda p: subprocess.run(["wslpath", "-w", str(p)], capture_output=True, text=True).stdout.strip()+    lad = subprocess.run(["/mnt/c/Windows/System32/cmd.exe", "/c", "echo %LOCALAPPDATA%"], capture_output=True, text=True, cwd="/mnt/c").stdout.strip()+    stage_win = cfg["stageDir"].replace("%LOCALAPPDATA%", lad) + "\\fusion-build-" + ver.replace(".", "_")+    stage = Path(subprocess.run(["wslpath", "-u", stage_win], capture_output=True, text=True).stdout.strip())+    shutil.rmtree(stage, ignore_errors=True)+    stage.mkdir(parents=True)+    unsigned_sha = hashlib.sha256(exe.read_bytes()).hexdigest()+    shutil.copy2(exe, stage / exe.name)+    shutil.copy2(ps1, stage / "sign-verify.ps1")+    print(f"== sign {exe.name} {ver} here (the signing machine) ==", file=sys.stderr)+    r = subprocess.run([POWERSHELL, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", win(stage / "sign-verify.ps1"),+                        "-Exe", win(stage / exe.name), "-Endpoint", cfg["endpoint"], "-Account", cfg["account"],+                        "-Profile", cfg["profile"], "-Tenant", cfg["tenant"], "-Timestamp", cfg["timestampUrl"]],+                       capture_output=True, text=True, cwd="/mnt/c", timeout=540)+    out = r.stdout+    try:+        v = json.loads(out[out.find("{"):out.rfind("}") + 1])+    except ValueError:+        raise SystemExit(f"REFUSED: the signing script gave no verdict (exit {r.returncode}): {redact((out + r.stderr)[-600:], cfg)}")+    if v.get("error"):+        raise SystemExit(f"REFUSED: cannot sign ({v['error']}): {redact(v.get('detail', ''), cfg)}\n  fix: {redact(v.get('fix', ''), cfg)}")+    checks = checks_of(v, cfg)+    if not all(checks.values()):+        raise SystemExit(f"REFUSED: signature checks failed: {checks}")+    signed = stage / exe.name+    signed_sha = hashlib.sha256(signed.read_bytes()).hexdigest()+    if signed_sha == unsigned_sha:+        raise SystemExit("REFUSED: the exe is byte-identical after signing")+    subs = assert_signed(signed)+    shutil.copy2(signed, exe)+    shutil.rmtree(stage, ignore_errors=True)+    print("   " + ", ".join(f"{k} {'ok' if ok else 'FAIL'}" for k, ok in checks.items()) + f"; verified again here, sha256 {signed_sha[:12]}", file=sys.stderr)+    return {"file": exe.name, "version": ver, "status": v["status"], "subject": SIGNER_ORG, "timestamper": v["timestamper"],+            "signtoolVerify": "Successfully verified", "sha256Unsigned": unsigned_sha, "sha256": signed_sha,+            "localSubjects": [x for x in subs if SIGNER_ORG in x], "where": "signing machine (local)"}+++def sign(exe, ver):+    """Sign `exe` in place: here when this is the signing machine (WSL on it), else on the signing host+    through Adom Bridge. Returns the signing report (no private values in it)."""+    exe = Path(exe)     sd = signing_dir()     cfg_p, ps1 = sd / "config.json", sd / "sign-verify.ps1"-    if not cfg_p.exists() or not ps1.exists():-        die(f"private signing config missing at {sd} (config.json + sign-verify.ps1). It is deliberately not in "-            "this public repo; ask the release owner to copy the folder over privately.")+    if not cfg_p.is_file() or not ps1.is_file():+        raise SystemExit(f"REFUSED: private signing config missing at {sd} (config.json + sign-verify.ps1). It is "+                         f"deliberately not in this public repo; ask Colby to copy it over privately.")     cfg = json.loads(cfg_p.read_text())     host = cfg["host"]-    stage_root = cfg["stageDir"]-    if "%LOCALAPPDATA%" in stage_root:-        if not cfg.get("localAppData"):-            die("config.json's stageDir uses %LOCALAPPDATA% but gives no localAppData to expand it with")-        stage_root = stage_root.replace("%LOCALAPPDATA%", cfg["localAppData"])-    stage = stage_root + "\\fusion-build-" + ver.replace(".", "_")   # a dotted folder name reads as a file to send_files-    names = [f.name for f in files]-    if len(set(names)) != len(names):-        die("two files share a name; the staging folder is flat")-    unsigned_sha = {f.name: hashlib.sha256(f.read_bytes()).hexdigest() for f in files}-+    here = local_windows_host()+    if here and here.lower() == host.lower():+        return sign_locally(exe, ver, cfg, ps1)+    lad = cfg.get("localAppData") or (ab_json("--target", host, "--ai-thread", THREAD, "run_script", json.dumps({+        "interpreter": "powershell", "scriptB64": base64.b64encode(b"$env:LOCALAPPDATA").decode(), "timeout": 30,+        "reason": "read the signing host's LOCALAPPDATA for the staging folder"})).get("stdout") or "").strip()+    if not lad:+        raise SystemExit("REFUSED: could not read the signing host's LOCALAPPDATA for the staging folder")+    stage = cfg["stageDir"].replace("%LOCALAPPDATA%", lad) \+        + "\\fusion-build-" + ver.replace(".", "_")  # a dotted folder name reads as a file path to send_files+    unsigned_sha = hashlib.sha256(exe.read_bytes()).hexdigest()     work = Path(tempfile.mkdtemp(prefix="fusion-sign-"))-    for f in files:-        shutil.copy2(f, work / f.name)+    name = exe.name+    shutil.copy2(exe, work / name)     shutil.copy2(ps1, work / "sign-verify.ps1")-    print(f"== signing {', '.join(names)} ({ver}) on the signing host ==", file=sys.stderr)-    sent = ab(host, "send_files", {"filePaths": [str(work / n) for n in names] + [str(work / "sign-verify.ps1")], "dest": stage,-                                   "reason": f"stage fusion-bridge {ver} for Authenticode signing"})-    if int(sent.get("filesReceived") or 0) != len(names) + 1:-        die(f"the signing host did not receive the files: {sent.get('error') or sent}")-    calls = "; ".join(-        f"(& '{stage}\\sign-verify.ps1' -Exe '{stage}\\{n}' -Endpoint '{cfg['endpoint']}' -Account '{cfg['account']}' "-        f"-Profile '{cfg['profile']}' -Tenant '{cfg['tenant']}' -Timestamp '{cfg['timestampUrl']}' | Out-String)" for n in names)-    cmd = f"$r = @({calls}); '[' + ($r -join ',') + ']'"-    r = ab(host, "run_script", {"interpreter": "powershell", "scriptB64": base64.b64encode(cmd.encode()).decode(), "timeout": 540,-                                "reason": f"Authenticode-sign fusion-bridge {ver} ({', '.join(names)}) as {SIGNER_ORG}, then Get-AuthenticodeSignature + signtool verify"},-           timeout=900)+    print(f"== sign {name} {ver} on the signing host ==", file=sys.stderr)+    sent = ab_json("--target", host, "--ai-thread", THREAD, "send_files", json.dumps({+        "filePaths": [str(work / name), str(work / "sign-verify.ps1")], "dest": stage, "destIsFolder": True,+        "reason": f"stage fusion-bridge {ver} for Authenticode signing"}))+    if int(sent.get("filesReceived") or 0) != 2:+        shutil.rmtree(work, ignore_errors=True)+        raise SystemExit(f"REFUSED: the signing host did not receive both files: {sent.get('error') or sent.get('_hint') or 'no reply'}")+    call = (f"(& '{stage}\\sign-verify.ps1' -Exe '{stage}\\{name}' -Endpoint '{cfg['endpoint']}' -Account '{cfg['account']}' "+            f"-Profile '{cfg['profile']}' -Tenant '{cfg['tenant']}' -Timestamp '{cfg['timestampUrl']}' | Out-String)")+    cmd = f"$r = @({call}); '[' + ($r -join ',') + ']'"+    r = ab_json("--target", host, "--ai-thread", THREAD, "run_script", json.dumps({+        "interpreter": "powershell", "scriptB64": base64.b64encode(cmd.encode()).decode(), "timeout": 540,+        "reason": f"Authenticode-sign fusion-bridge {ver} as {SIGNER_ORG}, then Get-AuthenticodeSignature + signtool verify"}), timeout=900)     so = r.get("stdout") or ""     try:-        verdicts = json.loads(so[so.find("["):])-    except ValueError:-        die(f"the signing script gave no verdict: {so[:400]} {(r.get('stderr') or r.get('error') or '')[:400]}")-    if len(verdicts) != len(names):-        die(f"expected {len(names)} verdicts, got {len(verdicts)}")-    report = []-    for f, v in zip(files, verdicts):-        if v.get("error"):-            die(f"the signing host cannot sign ({v['error']}): {v.get('detail', '')}\n  fix: {v.get('fix', '')}")-        checks = {"Status Valid": v.get("status") == "Valid",-                  "signer subject": cfg["expectSubject"] in (v.get("subject") or ""),-                  "timestamped": "Microsoft" in (v.get("timestamper") or ""),-                  "signtool verify /pa": "Successfully verified" in (v.get("signtoolVerify") or "")}-        if not all(checks.values()):-            die(f"signature checks failed for {f.name}: {checks}\n{json.dumps(v, indent=1)}")-        pulled = ab(host, "pull_file", {"filePaths": [f"{stage}\\{f.name}"], "reason": f"pull the signed {f.name} ({ver}) back for packaging"})-        got = pulled.get("files") or []-        if not got:-            die(f"could not pull the signed {f.name} back: {pulled.get('error') or pulled}")-        signed = Path(got[0]["path"])-        sha = hashlib.sha256(signed.read_bytes()).hexdigest()-        if v.get("sha256") and sha != v["sha256"]:-            die(f"{f.name} changed in transit (host {v['sha256']}, here {sha})")-        if sha == unsigned_sha[f.name]:-            die(f"{f.name} came back unchanged: it was not signed")-        subs = authenticode_subjects(signed)-        if not any(f'CN = "{SIGNER_ORG}"' in s or f"CN={SIGNER_ORG}" in s for s in subs):-            die(f"{f.name} is not signed by {SIGNER_ORG} when read here (found {subs or 'no signature'})")-        shutil.copy2(signed, f)        # the signed bytes replace the build, so the zip takes exactly them-        report.append({"file": f.name, "status": v["status"], "subject": v["subject"], "timestamper": v["timestamper"],-                       "signtoolVerify": v["signtoolVerify"].split(":")[0], "unsignedSha256": unsigned_sha[f.name], "sha256": sha})-        print(f"   {f.name}: " + ", ".join(f"{k} ok" for k in checks) + f"; verified again here, sha256 {sha[:12]}", file=sys.stderr)+        v = json.loads(so[so.find("["):])[0]+    except (ValueError, IndexError):+        shutil.rmtree(work, ignore_errors=True)+        raise SystemExit(f"REFUSED: the signing script gave no verdict (stdout {len(so)} chars, stderr {len(r.get('stderr') or '')} chars, error {r.get('errorCode') or r.get('error') or '-'})")+    if v.get("error"):+        shutil.rmtree(work, ignore_errors=True)+        raise SystemExit(f"REFUSED: the signing host cannot sign ({v['error']}): {redact(v.get('detail', ''), cfg)}\n  fix: {redact(v.get('fix', ''), cfg)}")+    checks = checks_of(v, cfg)+    if not all(checks.values()):+        shutil.rmtree(work, ignore_errors=True)+        raise SystemExit(f"REFUSED: signature checks failed on the host: {checks}")+    pulled = ab_json("--target", host, "--ai-thread", THREAD, "pull_file", json.dumps({+        "filePaths": [f"{stage}\\{name}"], "saveTo": str(work / "back"), "reason": f"pull the signed fusion-bridge {ver} back for packaging"}))+    files = pulled.get("files") or []+    if not files:+        shutil.rmtree(work, ignore_errors=True)+        raise SystemExit(f"REFUSED: could not pull the signed exe back: {pulled.get('error') or 'no files'}")+    back = Path(files[0]["path"])+    signed_sha = hashlib.sha256(back.read_bytes()).hexdigest()+    if v.get("sha256") and signed_sha != v["sha256"].lower():+        raise SystemExit("REFUSED: the signed exe changed in transit (host and local sha256 differ)")+    if signed_sha == unsigned_sha:+        raise SystemExit("REFUSED: the pulled-back exe is byte-identical to the unsigned build")+    subs = assert_signed(back)+    shutil.copy2(back, exe)     shutil.rmtree(work, ignore_errors=True)-    print(json.dumps({"signed": report}))+    report = {"file": name, "version": ver, "status": v["status"], "subject": SIGNER_ORG, "timestamper": v["timestamper"],+              "signtoolVerify": "Successfully verified", "sha256Unsigned": unsigned_sha, "sha256": signed_sha,+              "localSubjects": [s for s in subs if SIGNER_ORG in s]}+    print("   " + ", ".join(f"{k} {'ok' if ok else 'FAIL'}" for k, ok in checks.items()) + f"; verified again here, sha256 {signed_sha[:12]}", file=sys.stderr)+    return report   if __name__ == "__main__":-    main()+    if len(sys.argv) != 3:+        sys.exit(__doc__)+    print(json.dumps(sign(sys.argv[2], sys.argv[1])))