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
0.2.29 source: altium_install verb + detect.installVerb/hostAppOptionalVerbs; battery covers it; install skill starts with the verb
8 files changed
+223−8
bridge-rs/Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "altium-bridge"-version = "0.2.28"+version = "0.2.29" dependencies = [ "getrandom", "serde",
bridge-rs/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "altium-bridge"-version = "0.2.28"+version = "0.2.29" edition = "2021" description = "Adom altium-bridge — the bridge process, in Rust. Calls ab's verbs for the desktop; speaks file IPC to the resident DelphiScript agent inside Altium." license = "MIT"
bridge-rs/src/install.rsadded+190@@ -0,0 +1,190 @@+//! `altium_install`: install Altium Designer on this machine, staged, with the rules IN the verb.+//!+//! Written 2026-09-20 after an install on a fresh box where the AI driving it kept forgetting the+//! rules: it asked the human to "sign in" on a page that was already signed in (it never looked),+//! it left Release / Assign / Use License / Refresh for the human although those are ordinary+//! clicks, it never toasted him when it did need him, and it launched the installer while he was+//! away so the UAC prompt timed out. John: "build all of the rules into an install verb so you+//! stop screwing up and forgetting", "put in ai hints", "build that into your verbs now".+//!+//! THE ONE RULE: `needsHuman` may only ever name (1) a PASSWORD / 2FA, (2) the UAC prompt (secure+//! desktop, unreachable by any software), (3) clicks inside an ELEVATED window (UIPI blocks them+//! from a non-elevated bridge, and the bridge must never be elevated). Everything else the verb+//! does itself, and it LOOKS before it asks: every stage reads the real state first.++use crate::{ab, health, instance, license};+use serde_json::{json, Value};++/// Run a PowerShell snippet hidden; stdout trimmed, or None.+fn ps(script: &str) -> Option<String> {+ let mut cmd = std::process::Command::new("powershell");+ cmd.args(["-NoProfile", "-NonInteractive", "-Command", script]);+ #[cfg(windows)]+ {+ use std::os::windows::process::CommandExt;+ cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW+ }+ let out = cmd.output().ok()?;+ Some(String::from_utf8_lossy(&out.stdout).trim().to_string())+}++fn s(args: &Value, k: &str) -> Option<String> {+ args.get(k).and_then(|v| v.as_str()).map(str::to_string).filter(|t| !t.is_empty())+}++/// The newest `AltiumDesignerSetup*.exe` in the user's Downloads, with its signature facts.+fn find_installer(explicit: Option<String>) -> Value {+ let script = match explicit {+ Some(p) => format!("$f = Get-Item -LiteralPath '{}' -ErrorAction SilentlyContinue", p.replace('\'', "''")),+ None => "$f = Get-ChildItem \"$env:USERPROFILE\\Downloads\" -Filter 'AltiumDesignerSetup*.exe' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1".to_string(),+ } + "; if ($f) { $s = Get-AuthenticodeSignature $f.FullName; @{path=$f.FullName; mb=[math]::Round($f.Length/1MB,2); status=\"$($s.Status)\"; signer=\"$($s.SignerCertificate.Subject)\"; version=$f.VersionInfo.FileVersion} | ConvertTo-Json -Compress } else { '{}' }";+ ps(&script).and_then(|t| serde_json::from_str::<Value>(&t).ok()).unwrap_or_else(|| json!({}))+}++/// Valid Authenticode signature by Altium. Anything else must never be run.+pub fn signature_ok(info: &Value) -> bool {+ info.get("status").and_then(|v| v.as_str()) == Some("Valid")+ && info.get("signer").and_then(|v| v.as_str()).map(|t| t.contains("CN=Altium LLC")).unwrap_or(false)+}++/// Altium is installed when the bridge can find its X2.EXE.+fn altium_installed() -> bool {+ crate::lifecycle::x2_exe_pub().is_some()+}++/// Toast the person at THIS machine and say truthfully whether it displayed.+fn toast(title: &str, body: &str) -> Value {+ let r = ab::call("notify_user", json!({"title": title, "body": body, "level": "warning", "scenario": "reminder",+ "buttons": [{"label": "On it"}], "reason": "altium_install needs the human for a step no software can do"}), &[], 20);+ let shown = r.get("action").and_then(|a| a.as_str()) == Some("displayed");+ json!({"displayed": shown, "error": r.get("error").cloned().unwrap_or(Value::Null),+ "_hint": if shown { "Shown on THIS machine. If the user sits at a different machine they will not see it: also toast them where they are (notify_user with NO --target reaches the machine your container is attached to; `--target <mesh machine>` answered target_not_found on 2026-09-20) AND tell them in chat." }+ else { "The toast did NOT display. Do not claim you notified the user: tell them in chat, and toast them with notify_user (no --target) from your side." }})+}++const HUMAN_ONLY: &str = "needsHuman may only ever name a password/2FA, the UAC prompt, or clicks inside an ELEVATED window. Release, Assign, Use License, Refresh, Sign in buttons, save prompts and browser navigation are ordinary clicks: do them yourself (UIA first, then desktop_click with a fresh shotId), and LOOK at a screenshot before you ask the human for anything.";++pub fn install(args: &Value) -> Value {+ let stage = s(args, "stage").unwrap_or_else(|| "auto".into());+ let installed = altium_installed();+ let running = health::is_running();+ let stage = if stage == "auto" {+ if installed && running { "license" } else if installed { "launch_altium" } else if find_installer(s(args, "installerPath")).get("path").is_some() { "launch" } else { "preflight" }+ } else { stage.as_str() }.to_string();++ let mut out = match stage.as_str() {+ "preflight" => {+ let facts = ps("$c = Get-PSDrive C; $p = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()); $k = Get-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System'; @{freeGB=[math]::Round($c.Free/1GB); os=(Get-CimInstance Win32_OperatingSystem).Caption; bridgeElevated=$p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator); uacOn=[bool]$k.EnableLUA; uacSecureDesktop=[bool]$k.PromptOnSecureDesktop; user=$env:USERNAME} | ConvertTo-Json -Compress")+ .and_then(|t| serde_json::from_str::<Value>(&t).ok()).unwrap_or_else(|| json!({}));+ let free = facts.get("freeGB").and_then(|v| v.as_f64()).unwrap_or(0.0);+ json!({"success": free >= 25.0, "stage": "preflight", "installed": installed, "facts": facts,+ "errorCode": if free >= 25.0 { Value::Null } else { json!("not_enough_disk") },+ "_hint": "Altium needs ~10 GB installed and 15-25 GB with libraries and installer temp. bridgeElevated is normally false and must STAY false: it only tells you which windows you will be able to click.",+ "_next": ["altium_install {stage:'get_installer'}"],+ "pitfalls": ["There is NO public installer URL: downloads.altium.com holds only legacy archives. The current installer is behind an altium.com sign-in.",+ "Never use the installer's unattended switches: Altium's silent mode takes the account PASSWORD on the command line."]})+ }+ "get_installer" => {+ let info = find_installer(s(args, "installerPath"));+ if info.get("path").is_none() {+ json!({"success": false, "stage": "get_installer", "errorCode": "installer_not_found",+ "error": "no AltiumDesignerSetup*.exe in this user's Downloads",+ "_hint": "Get it with the user's altium.com session, on ANY machine, then copy the file here (send_files): 1) pup_open_window https://www.altium.com/products/downloads and pup_lower_os_window it at once. 2) pup_eval: signed in = zero `a.login-form` links and the buttons read 'Online Installer'. 3) If signed out: navigate to auth.altium.com, fill the account email, click Next, click the first `button.round-button` (Google). If Google asks for a password, THAT is the human step: raise the window (pup_raise_os_window), toast, and wait. 4) Signed in: click `a[data-id-in]` for the newest version INSIDE the page (pup_fetch_url sends no cookies and gets 401). 5) Verify the signature here with this stage again.",+ "needsHuman": ["the Google/Altium PASSWORD, only if the browser session is not already signed in"],+ "_next": ["altium_install {stage:'get_installer'}"]})+ } else if !signature_ok(&info) {+ json!({"success": false, "stage": "get_installer", "errorCode": "installer_signature_invalid", "installer": info,+ "error": "the installer's Authenticode signature is not a Valid signature by Altium LLC: it will not be run",+ "_hint": "Delete it and download again from altium.com. Never run an unsigned or differently signed installer."})+ } else {+ json!({"success": true, "stage": "get_installer", "installer": info,+ "_hint": "Signature Valid, signer Altium LLC. Launch it only when the human is AT this machine: the UAC prompt auto-denies after about two minutes and the installer then exits.",+ "_next": ["altium_install {stage:'launch'}"]})+ }+ }+ "launch" => {+ let info = find_installer(s(args, "installerPath"));+ if !signature_ok(&info) {+ return json!({"success": false, "stage": "launch", "errorCode": "installer_not_verified", "installer": info,+ "_hint": "Run stage get_installer first; a missing or wrongly signed installer is never launched."});+ }+ if args.get("humanIsPresent").and_then(|b| b.as_bool()) != Some(true) {+ return json!({"success": false, "stage": "launch", "errorCode": "human_presence_required", "installer": info,+ "error": "launching raises a UAC prompt that only a person can answer and that auto-denies after ~2 minutes",+ "_hint": "Ask the user to be at this machine's screen (RustDesk counts), THEN call again with humanIsPresent:true. Launching into an empty room wastes the prompt (it did, 2026-09-20).",+ "needsHuman": ["be at this machine's screen before the launch"]});+ }+ let path = info.get("path").and_then(|p| p.as_str()).unwrap_or("").replace('\'', "''");+ let _ = ps(&format!("Start-Process -FilePath '{path}'"));+ let t = toast("Altium installer: I need you at this machine NOW",+ "1) Yes on the UAC prompt (it auto-denies in ~2 min). 2) Next. 3) Accept the EULA (your agreement, not mine). 4) Sign in when the browser opens. 5) Keep the default features. 6) Install, then Finish. I will do everything after that myself.");+ json!({"success": true, "stage": "launch", "installer": info, "toast": t,+ "needsHuman": ["UAC: Yes (secure desktop: no software can click it)",+ "the wizard: Next, accept the EULA, sign in in the browser, Install, Finish (the window is ELEVATED; UIPI blocks clicks from the non-elevated bridge)"],+ "_hint": "The human's part ENDS at Finish. After that everything is yours, including licensing. Poll stage 'wait'. Tell the user in chat as well as by toast, and check `toast.displayed` before you say you notified anyone.",+ "_next": ["altium_install {stage:'wait'}"],+ "pitfalls": ["Screenshots of the elevated installer WORK (desktop_screenshot_screen); only clicks are blocked. Look at it to tell the user which page they are on.",+ "The EULA is the user's agreement: say plainly that it is theirs to accept."]})+ }+ "wait" => {+ let st = ps("@{consent=[bool](Get-Process consent -ErrorAction SilentlyContinue); installer=[bool](Get-Process | Where-Object { $_.Name -like 'AltiumDesignerSetup*' }); dir=(Test-Path 'C:\\Program Files\\Altium')} | ConvertTo-Json -Compress")+ .and_then(|t| serde_json::from_str::<Value>(&t).ok()).unwrap_or_else(|| json!({}));+ let consent = st.get("consent").and_then(|b| b.as_bool()).unwrap_or(false);+ let inst = st.get("installer").and_then(|b| b.as_bool()).unwrap_or(false);+ let hint = if altium_installed() { "Installed. Go straight to stage 'license': do NOT ask the user to sign in, the installer's browser sign-in normally carries over." }+ else if consent { "The UAC prompt is UP and unanswered. It auto-denies in ~2 minutes: get the human to it NOW (toast + chat)." }+ else if inst { "The installer is running (past UAC). desktop_screenshot_screen shows which page it is on." }+ else { "No installer process and nothing installed: the UAC prompt was denied or timed out. Stage 'launch' again, with the human present." };+ json!({"success": true, "stage": "wait", "installed": altium_installed(), "uacPromptUp": consent, "installerRunning": inst,+ "_hint": hint, "_next": [if altium_installed() { "altium_install {stage:'license'}" } else { "altium_install {stage:'wait'}" }]})+ }+ "launch_altium" => json!({"success": true, "stage": "launch_altium", "installed": true,+ "_hint": "Altium is installed but not running. altium_launch (cold start can exceed 2 minutes), then stage 'license'.",+ "_next": ["altium_launch", "altium_install {stage:'license'}"]}),+ "license" => {+ if !running {+ return json!({"success": false, "stage": "license", "errorCode": "altium_not_running", "_next": ["altium_launch"]});+ }+ let before = instance::licence_state();+ if before.get("licensed").and_then(|b| b.as_bool()) == Some(true) {+ return json!({"success": true, "stage": "license", "licensed": true, "licenceState": before.get("state"),+ "_hint": "Already licensed. Done: altium_agent_start and drive.", "_next": ["altium_agent_start"]});+ }+ // DO IT, do not ask: Refresh, then Use License, then read the state again.+ let refreshed = license::license(&json!({"action": "refresh"}));+ std::thread::sleep(std::time::Duration::from_secs(4));+ let mut used = Value::Null;+ if instance::licence_state().get("licensed").and_then(|b| b.as_bool()) != Some(true) {+ used = license::license(&json!({"action": "use_license"}));+ std::thread::sleep(std::time::Duration::from_secs(5));+ }+ let after = instance::licence_state();+ let ok = after.get("licensed").and_then(|b| b.as_bool()) == Some(true);+ json!({"success": ok, "stage": "license", "licensed": ok, "licenceState": after.get("state"),+ "did": {"refresh": refreshed.get("clicked"), "useLicense": used.get("clicked")},+ "errorCode": if ok { Value::Null } else { json!("no_seat_available") },+ "_hint": if ok { "Licensed by the bridge (Refresh / Use License), no human involved. altium_agent_start and drive." }+ else { "Still Viewer. READ the page before asking anyone: altium_license {action:'show'} returns a screenshot. 'Sign Out' visible = the user IS signed in, no password needed. 'There are no seats available for this license' = the account's seat is held by ANOTHER running Altium (one On-Demand seat serves every Altium, Designer and Develop alike): altium_close it on that machine (cleanly, a kill strands the seat), then run this stage again. Release / Assign / Use License / Refresh are ordinary clicks: do them, do not hand them to the user." },+ "needsHuman": if ok { json!([]) } else { json!(["a PASSWORD, only if the page shows 'Sign in' instead of 'Sign Out'"]) },+ "_next": [if ok { "altium_agent_start" } else { "altium_license {action:'show'}" }]})+ }+ other => json!({"success": false, "errorCode": "unknown_stage", "error": format!("unknown stage '{other}'"),+ "_hint": "stages, in order: preflight, get_installer, launch, wait, license (or omit stage: 'auto' picks the right one from the machine's state)"}),+ };+ out["rule"] = json!(HUMAN_ONLY);+ out+}++#[cfg(test)]+mod tests {+ use super::*;++ #[test]+ fn only_a_valid_altium_signature_is_accepted() {+ assert!(signature_ok(&json!({"status": "Valid", "signer": "CN=Altium LLC, O=Altium LLC, L=La Jolla, S=California, C=US"})));+ assert!(!signature_ok(&json!({"status": "NotSigned", "signer": ""})));+ assert!(!signature_ok(&json!({"status": "Valid", "signer": "CN=Somebody Else"})));+ assert!(!signature_ok(&json!({"status": "HashMismatch", "signer": "CN=Altium LLC"})));+ assert!(!signature_ok(&json!({})));+ }+}
bridge-rs/src/main.rs+2−1@@ -7,6 +7,7 @@ mod agent; mod board; mod dialogs; mod health;+mod install; mod instance; mod library; mod license;@@ -25,7 +26,7 @@ mod win32; use std::time::Instant; use tiny_http::{Header, Method, Response, Server}; -pub const BRIDGE_VERSION: &str = "0.2.28";+pub const BRIDGE_VERSION: &str = "0.2.29"; fn json_response(status: u16, body: &serde_json::Value) -> Response<std::io::Cursor<Vec<u8>>> { let data = serde_json::to_vec(body).unwrap_or_else(|_| b"{}".to_vec());
bridge-rs/src/verbs.rs+13−1@@ -35,7 +35,7 @@ pub const KNOWN: &[&str] = &[ "clone_symbol", "rename_symbol", "rename_footprint", "remove_symbol", "remove_footprint", "copy_footprint", "xcopy_footprint", "swap_footprint", "symbol_mode", "cleanup_libs", "install_library", "install_library_bundle", "install_receipt", "compile_intlib", "extract_intlib",- "place_on_sch", "place_on_pcb", "import_part", "add_symbol_art", "symbol_prims", "set_models", "selftest_script_error",+ "place_on_sch", "place_on_pcb", "import_part", "add_symbol_art", "symbol_prims", "set_models", "selftest_script_error", "install", // licence / account "license", "license_status", "ensure_licensed", "setup_trial", "signin", "signin_status", "connect_workspace",@@ -110,6 +110,17 @@ fn describe() -> Value { verb_doc("plate_vias", "Make every drilled pad in a PcbLib a plated MULTI-LAYER pad (all footprints, or footprintName). adom-lbr writes heatsink vias as pads with a hole; Altium loaded them on the Top Layer, so a thermal pad's vias were not through-holes until this ran.", json!({"libraryPath": "path (.PcbLib)", "footprintName": "string (optional filter)"}), json!({"holes": "int", "plated": "int", "fixed": "Fp:Pad;..."}), 90, "altium_save the PcbLib afterwards; footprint_primitives on a placed part should then report layerId 74 for the vias.", &["normalize_bodies", "footprint_primitives", "save"], &["Read-modify in place: run once per library install."], json!({"libraryPath": "C:/lib/Adom_ToF.PcbLib"})),+ verb_doc("install", "Install Altium Designer on this machine, STAGED, with the rules in the verb (from 0.2.29). stage: preflight | get_installer | launch | wait | license, or omit it and 'auto' picks the stage from the machine's state. The verb looks before it asks, verifies the installer's Authenticode signature (Valid, CN=Altium LLC) before anything runs, refuses to launch into an empty room, toasts the human itself and says truthfully whether the toast displayed, and does the licensing clicks (Refresh / Use License) ITSELF.",+ json!({"stage": "auto (default) | preflight | get_installer | launch | wait | license", "installerPath": "optional path; default = newest AltiumDesignerSetup*.exe in Downloads", "humanIsPresent": "bool, REQUIRED true for stage launch: the UAC prompt auto-denies in ~2 minutes"}),+ json!({"stage": "string", "needsHuman": "[string] - only ever a password, the UAC prompt, or clicks in an elevated window", "toast": "{displayed, _hint}", "licensed": "bool", "_next": "[string]"}), 120,+ "Call it with no args and follow _next. The human's part ENDS at the installer's Finish button; licensing is yours.",+ &["ensure_licensed", "license", "launch", "readiness"],+ &["needsHuman may only name a password/2FA, the UAC prompt, or clicks inside an ELEVATED window. Release, Assign, Use License, Refresh and save prompts are ordinary clicks: do them.",+ "LOOK before you ask: 'Sign Out' on the licence page means the user is ALREADY signed in (the installer's browser sign-in carries over).",+ "'There are no seats available' means another running Altium holds the account's single On-Demand seat: altium_close it there, then stage license again.",+ "Never use the installer's unattended switches: they take the account password on the command line.",+ "A toast only reaches the machine it is shown on. Check toast.displayed and ALSO tell the user in chat."],+ json!({"stage": "launch", "humanIsPresent": true})), verb_doc("selftest_script_error", "Regression test for the script-error safety net (from 0.2.23): makes the agent raise a REAL DelphiScript runtime error on purpose and checks that the bridge photographs Altium's Error box, stops the halted script itself and leaves no dialog behind. Flashes one error box for under a second. Run it after any change to agent.rs / dialogs.rs and once on every new Altium build.", json!({}), json!({"netWorks": "bool", "outcome": "halted_and_recovered | contained_by_agent | unhandled", "errorShot": "base64 png (halted outcome only)", "autoRecovered": "bool", "dialogsLeft": "int"}), 60, "netWorks:true is the pass. The agent is stopped afterwards by design; the next verb restarts it.", &["agent_recover", "dialog_log", "agent_status"], &["It really does halt the engine for a moment: do not run it while a user is mid-edit in Altium."], json!({})),@@ -522,6 +533,7 @@ pub fn dispatch(command: &str, args: &Value, _forwarded: &[(String, String)]) -> }); r }+ "install" => crate::install::install(args), "symbol_prims" => library::symbol_prims(args), "set_models" => library::set_models(args), "place_on_pcb" => library::place_on_pcb(args),
bridge.json+11−2@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "altium", "displayName": "Altium Designer",- "version": "0.2.28",+ "version": "0.2.29", "description": "Drive Altium Designer from Adom: a resident DelphiScript agent (eval + the 1,100-command process launcher), the X2 -R command line, UIA over the VCL shell, dialogs by PID, screenshots of every Altium window, and Altium 365.", "homepage": "https://wiki.adom.inc/adom/altium-bridge", "docs": "https://wiki.adom.inc/adom/altium-bridge",@@ -76,6 +76,7 @@ "altium_foreground", "altium_import_kicad", "altium_import_part",+ "altium_install", "altium_install_extension", "altium_install_library", "altium_install_library_bundle",@@ -193,6 +194,14 @@ "windows": [ "%ProgramFiles%\\Altium\\AD*\\X2.EXE" ]- }+ },+ "installVerb": "altium_install",+ "hostAppOptionalVerbs": [+ "describe",+ "diagnostics",+ "install",+ "readiness",+ "status"+ ] } }
dev-skills/altium-bridge-test/run_verb_tests.py+4−1@@ -95,6 +95,7 @@ def matrix(w): "a365_oauth_status"]: M[v] = dict(phase=0, **ok()) M["dialog_log"] = dict(phase=0, **ok(limit=5))+ M["install"] = dict(phase=0, **ok(stage="preflight"), note="read-only stage: disk, OS, elevation, UAC policy. Never launches an installer") M["list_commands"] = dict(phase=0, **ok(query="PCB", limit=5)) M["uninstall"] = dict(phase=0, **ok(dryRun=True), note="SAFETY: dryRun only, never removes anything") M["extension_ping"] = dict(phase=0, **anyo("the Altium extension is optional; ok or a named 'not installed' both pass"))@@ -110,7 +111,9 @@ def matrix(w): M["ensure_licensed"] = dict(phase=1, timeout=300, **ok(), note="defaults only: never signs out, never starts a trial") M["state"] = dict(phase=1, **ok(screenshots=False)) M["eval"] = dict(phase=1, **ok(text="Begin End;"))- M["run_command"] = dict(phase=1, **anyo("a harmless view command", command="PCB:Zoom", parameters="Action=Redraw"))+ # NEVER a process that can open a dialog: PCB:Zoom raised Altium's "Zoom Value" box on the user's+ # screen (2026-09-20) and the bridge's dialog scan did not see it. No command given = a named refusal.+ M["run_command"] = dict(phase=1, **ref("no command given: must refuse by name (a real process can open a dialog)")) # ---- phase 2: library lane on SCRATCH copies --------------------------------------------- M["install_library"] = dict(phase=2, **ok(libraryPath=SL))
page.json+1−1@@ -140,4 +140,4 @@ "skills/altium-wiki-import/SKILL.md", "uninstall.sh" ]-}+}