app
Altium - the Altium Bridge
Public Made by Adomby adom
Adom Bridge for Altium Designer: a resident DelphiScript agent gives the AI eval, the 1,100-command process launcher, all 8 EDA surfaces with screenshots, compile/ERC, DRC, OutJob outputs, native SchLib/PcbLib authoring with STEP 3D bodies, and dialog handling by PID.
← Commit history
release.py: every shipped exe is Authenticode-signed before packaging; build_zip refuses an unsigned exe; --stage mode (signed release asset, no tier). Signing identifiers live in a private config outside the repo.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 file changed
+164−2
scripts/release.py+164−2@@ -15,6 +15,16 @@ pattern: adom/insiders skills + adom/pup-bridge tools/ship.sh, which did it firs # (promotion_attestation/promoted_by/_at). python3 scripts/release.py --rollback <ver> --attest "<words>" --by "<name>" # re-point PUBLIC at an older archived build.+ python3 scripts/release.py --stage "<log>" # build, sign, upload the release asset ONLY: no tier,+ # no skills pkg (a build waiting for a live battery).++SIGNING: every .exe the zip ships is Authenticode-signed as "Adom Industries, Inc." before+packaging (Windows Smart App Control blocks unsigned exes). The signing identifiers live in a+PRIVATE config outside this public repo ($ADOM_SIGNING_DIR, default ~/.adom/signing/altium-bridge:+config.json + sign-verify.ps1); the exe is signed on the Windows host that config names, through+that host's signer login, verified there (Get-AuthenticodeSignature + signtool verify /pa), pulled+back with a hash check, and verified AGAIN here. build_zip refuses any unsigned .exe, so no path+through this script can package one. Signing is the last change to the file before packaging. Rules (insiders-tier-publish, verbatim where it matters): every publish is insiders; public only on a human's words; an AI never promotes on its own judgment; one plain semver per build in both@@ -53,7 +63,7 @@ PUBLIC_MANIFEST = ROOT / "adom-bridge-altium-manifest.json" INCLUDE = [ ("bridge.json", SRC / "bridge.json"), ("BRIDGE_VERSION", SRC / "BRIDGE_VERSION"),- ("altium-bridge.exe", EXE),+ ("altium-bridge.exe", DIST / "signed" / "altium-bridge.exe"), ("agent/AdomAgent.pas.tmpl", SRC / "agent" / "AdomAgent.pas.tmpl"), ("agent/AdomAgent.PrjScr", SRC / "agent" / "AdomAgent.PrjScr"), ("resources/adom-overlay-32.png", SRC / "resources" / "adom-overlay-32.png"),@@ -146,10 +156,135 @@ def build_exe(): raise SystemExit(f"exe missing after build: {EXE}") +SIGNER_ORG = "Adom Industries, Inc."+++def signing_dir():+ return Path(os.environ.get("ADOM_SIGNING_DIR", Path.home() / ".adom" / "signing" / "altium-bridge"))+++def authenticode_subjects(path):+ """Certificate subjects in a PE's Authenticode signature, read here without Windows: the+ optional header's security data directory points at a WIN_CERTIFICATE holding PKCS#7 DER.+ [] means unsigned."""+ import struct+ import tempfile+ b = Path(path).read_bytes()+ pe = struct.unpack_from("<I", b, 0x3C)[0]+ if b[pe:pe + 4] != b"PE\0\0":+ raise SystemExit(f"{path}: not a PE file")+ opt = pe + 24+ magic = struct.unpack_from("<H", b, opt)[0]+ 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)+ if ctype != 2:+ return []+ with tempfile.NamedTemporaryFile(suffix=".p7b", delete=False) as f:+ f.write(b[off + 8:off + length])+ r = subprocess.run(["openssl", "pkcs7", "-inform", "DER", "-in", f.name, "-print_certs", "-noout"],+ capture_output=True, text=True)+ os.unlink(f.name)+ return [l[len("subject="):].strip() for l in r.stdout.splitlines() if l.startswith("subject=")]+++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} "+ f"(found {subs or 'no signature'}). Unsigned exes are blocked by Windows Smart App Control; "+ f"this script never packages one.")+ 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("{"):+ try:+ d = json.loads(o)+ except ValueError:+ pass+ return d+++def sign_exe(ver):+ """Sign EXE on the private config's Windows host, verify there, pull it back, verify here."""+ import base64+ sd = signing_dir()+ cfg_p, ps1 = sd / "config.json", sd / "sign-verify.ps1"+ if not cfg_p.exists() or not ps1.exists():+ raise SystemExit(f"REFUSED: private signing config missing at {sd} (config.json + sign-verify.ps1). "+ f"It is deliberately not in this public repo; ask the release owner for it.")+ cfg = json.loads(cfg_p.read_text())+ host, thread = cfg["host"], "altium-bridge release signing"+ stage = cfg["stageDir"].replace("%LOCALAPPDATA%", cfg.get("localAppData", r"C:\Users\cknox\AppData\Local")) + "\\" + ver+ work = DIST / "signing"+ shutil.rmtree(work, ignore_errors=True)+ work.mkdir(parents=True)+ unsigned = work / "altium-bridge.exe"+ shutil.copy2(EXE, unsigned)+ shutil.copy2(ps1, work / "sign-verify.ps1")+ print(f"== sign {unsigned.name} on the signing host ==")+ sent = ab_json("--target", host, "--ai-thread", thread, "send_files", json.dumps({+ "filePaths": [str(unsigned), str(work / "sign-verify.ps1")], "dest": stage,+ "reason": f"stage altium-bridge {ver} for Authenticode signing"}))+ if int(sent.get("filesReceived") or 0) != 2:+ raise SystemExit(f"signing host did not receive the files: {sent.get('error') or sent}")+ win_exe = stage + "\\altium-bridge.exe"+ cmd = (f"& '{stage}\\sign-verify.ps1' -Exe '{win_exe}' -Endpoint '{cfg['endpoint']}' "+ f"-Account '{cfg['account']}' -Profile '{cfg['profile']}' -Tenant '{cfg['tenant']}' "+ f"-Timestamp '{cfg['timestampUrl']}'")+ r = ab_json("--target", host, "--ai-thread", thread, "run_script", json.dumps({+ "interpreter": "powershell", "scriptB64": base64.b64encode(cmd.encode()).decode(), "timeout": 300,+ "reason": f"Authenticode-sign altium-bridge {ver} as {SIGNER_ORG}, then Get-AuthenticodeSignature + signtool verify"}))+ so = r.get("stdout") or ""+ try:+ v = json.loads(so[so.find("{"):])+ except ValueError:+ raise SystemExit(f"signing script gave no verdict: {so[:400]} {(r.get('stderr') or '')[:400]}")+ if v.get("error"):+ raise SystemExit(f"REFUSED: 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()):+ raise SystemExit(f"REFUSED: signature checks failed on the signing host: {checks}\n{json.dumps(v, indent=1)}")+ pulled = ab_json("--target", host, "--ai-thread", thread, "pull_file", json.dumps({+ "filePaths": [win_exe], "reason": f"pull the signed altium-bridge {ver} exe back for packaging"}))+ files = pulled.get("files") or []+ if not files:+ raise SystemExit(f"could not pull the signed exe back: {pulled.get('error') or pulled}")+ signed = DIST / "signed" / "altium-bridge.exe"+ signed.parent.mkdir(parents=True, exist_ok=True)+ shutil.move(files[0]["path"], signed)+ local_sha = hashlib.sha256(signed.read_bytes()).hexdigest()+ if local_sha != v.get("sha256"):+ raise SystemExit(f"REFUSED: signed exe changed in transit (host {v.get('sha256')}, here {local_sha})")+ subs = assert_signed(signed)+ report = {"file": "altium-bridge.exe", "status": v["status"], "subject": v["subject"],+ "timestamper": v["timestamper"], "signtoolVerify": v["signtoolVerify"],+ "signer": v.get("signer"), "sha256": local_sha, "localSubjects": subs}+ (DIST / "signed" / "signing-report.json").write_text(json.dumps(report, indent=2) + "\n")+ for k, ok in checks.items():+ print(f" {k}: {'ok' if ok else 'FAIL'}")+ print(f" verified again here: {SIGNER_ORG}, sha256 {local_sha[:12]}")+ return report++ def build_zip(ver): DIST.mkdir(exist_ok=True) zname = f"adom-bridge-altium-v{ver}.zip" zpath = DIST / zname+ for rel, p in INCLUDE:+ if rel.lower().endswith(".exe"):+ assert_signed(p) with zipfile.ZipFile(zpath, "w", zipfile.ZIP_DEFLATED) as z: for rel, p in INCLUDE: if not p.exists():@@ -195,6 +330,25 @@ def two_curl_split_check(): return served +def stage_only(log_line):+ """A build waiting for a live battery: signed and uploaded as a release asset, but NO tier moves+ and no skills pkg row. Its manifest is archived so a later publish or pin can point at it."""+ ver = json.loads((SRC / "bridge.json").read_text())["version"]+ lint_versions(ver)+ lint_agent_script()+ n = lint_verbs()+ print(f"verb lint ok: {n} verbs")+ build_exe()+ sign_exe(ver)+ zname, zpath, sha = build_zip(ver)+ subprocess.run(["adom-wiki", "release", "create", PAGE, ver, "--title", f"altium-bridge {ver}",+ "--changelog", f"STAGED, not on any tier: {log_line}"], check=False, capture_output=True)+ run("adom-wiki", "release", "upload", PAGE, ver, str(zpath), "--platform", "windows", "--arch", "x64")+ ARCHIVE.mkdir(exist_ok=True)+ (ARCHIVE / f"{ver}.json").write_text(json.dumps(manifest_for(ver, zname, zpath, sha, "insiders"), indent=2) + "\n")+ print(f"STAGED {ver}: signed zip sha256 {sha[:12]} uploaded; no tier moved. Archived scripts/releases/{ver}.json.")++ def ship_insiders(log_line): ver = json.loads((SRC / "bridge.json").read_text())["version"] lint_versions(ver)@@ -202,6 +356,7 @@ def ship_insiders(log_line): n = lint_verbs() print(f"verb lint ok: {n} verbs declared and dispatchable") build_exe()+ sign_exe(ver) zname, zpath, sha = build_zip(ver) print(f"== release {ver} on {PAGE} (bytes live on the product page, both tiers) ==")@@ -342,14 +497,21 @@ def main(): if not log_line: raise SystemExit('usage: release.py --publish "<one changelog sentence for the release log>"') ship_insiders(log_line)+ elif "--stage" in args:+ i = args.index("--stage")+ log_line = args[i + 1] if len(args) > i + 1 and not args[i + 1].startswith("--") else ""+ if not log_line:+ raise SystemExit('usage: release.py --stage "<one line: what this staged build is for>"')+ stage_only(log_line) else:+ # lint + build only: packaging needs a signed exe, so the zip is made by --stage/--publish ver = json.loads((SRC / "bridge.json").read_text())["version"] lint_versions(ver) lint_agent_script() n = lint_verbs() print(f"verb lint ok: {n} verbs") build_exe()- build_zip(ver)+ print("built the exe (unsigned). --stage or --publish signs, packages and uploads.") if __name__ == "__main__":