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.
main
Colby Knox
0.2.58 source (not yet released, needs signing): autoroute, layer_stack, rule descriptors, sch_zoom
f82b5b5
10h ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
//! altium-bridge (Rust) — spawned by ab with `--port <n>`, binds ADOM_BIND_HOST.
//! GET /health|/status -> {status, led, summary, tooltip, bridgeVersion, licence}
//! POST /command {command, args} -> verb envelope. Everything desktop-shaped goes to ab.
mod a365;
mod ab;
mod agent;
mod board;
mod bom;
mod sch;
mod dialogs;
mod health;
mod install;
mod instance;
mod library;
mod license;
mod lifecycle;
mod manufacturing;
mod misc;
mod outputs;
mod progress;
mod review;
mod route;
mod routing;
mod shot;
mod support;
mod surfaces;
mod taskbar;
mod verbs;
mod win32;
use std::time::Instant;
use tiny_http::{Header, Method, Response, Server};
/// Attached to every failed reply (see the /command handler).
const REPORT_ISSUES: &str = "If this looks like an altium-bridge BUG (not a refusal you expected), report it: check for a duplicate with `adom-wiki issue list adom/altium-bridge`, then `adom-wiki issue create adom/altium-bridge --title \"<verb>: <one-line symptom>\" --category bug-report --body \"<what you ran, what you expected, what happened, the VERBATIM response JSON (and errorShot if present), bridgeVersion, the Altium version from altium_status, the OS>\"`. A relay, routing, toast or lifecycle problem belongs on adom/adom-bridge instead. Reports from real machines are how this bridge gets correct: three different Altium builds behaved three different ways in one weekend.";
pub const BRIDGE_VERSION: &str = "0.2.58";
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());
Response::from_data(data)
.with_status_code(status)
.with_header(Header::from_bytes("Content-Type", "application/json").unwrap())
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let port: u16 = args
.iter()
.position(|a| a == "--port")
.and_then(|i| args.get(i + 1))
.and_then(|p| p.parse().ok())
.unwrap_or(0);
let host = std::env::var("ADOM_BIND_HOST").unwrap_or_else(|_| "127.0.0.1".into());
let server = Server::http(format!("{host}:{port}")).expect("bind");
let bound = server.server_addr();
println!("[altium] bridge {BRIDGE_VERSION} listening on {bound}");
// Standing services, started separately so one failing cannot take the other down:
// the dialog reaper pre-empts Altium's nuisance modals (John: "always pre-empt these from
// ever showing up"), the idle watcher clears the taskbar overlay after 5 quiet minutes.
dialogs::start_reaper();
taskbar::start_idle_watcher();
lifecycle::start_close_watchdog(); // adom/altium-bridge#4: exit the agent the instant a user-initiated close begins
// ONE THREAD PER REQUEST. Until 0.2.28 requests ran inline in this loop, so a single slow verb
// (a 25 s UIA search in signin, an agent call waiting on a parked script engine) blocked EVERY
// request behind it, /health included; ab dropped its connection at its own budget while the
// bridge kept grinding the abandoned request, and the queue only grew (the 30-verb cascades the
// verb battery found, 2026-09-20). Agent-bound work still serialises where it must: the agent's
// one-file inbox, which now answers a NAMED agent_busy instead of a dropped connection.
for mut req in server.incoming_requests() {
let spawned = std::thread::Builder::new().name("altium-request".into()).spawn(move || {
let t0 = Instant::now();
let path = req.url().to_string();
let method = req.method().clone();
let resp = match (method, path.as_str()) {
(Method::Get, "/health") | (Method::Get, "/status") => json_response(200, &health::report()),
(Method::Get, "/verbs") => json_response(200, &serde_json::json!({"verbs": verbs::names(), "bridgeVersion": BRIDGE_VERSION})),
(Method::Post, "/command") => {
let mut body = String::new();
let _ = req.as_reader().read_to_string(&mut body);
let forwarded: Vec<(String, String)> = req
.headers()
.iter()
.filter(|h| h.field.as_str().as_str().to_ascii_lowercase().starts_with("x-adom-caller"))
.map(|h| (h.field.as_str().to_string(), h.value.as_str().to_string()))
.collect();
match serde_json::from_str::<serde_json::Value>(if body.is_empty() { "{}" } else { &body }) {
Err(e) => json_response(400, &serde_json::json!({"success": false, "error": format!("Invalid JSON: {e}")})),
Ok(v) => {
let command = v.get("command").and_then(|c| c.as_str()).unwrap_or("").to_string();
let vargs = v.get("args").cloned().unwrap_or_else(|| serde_json::json!({}));
let mut out = verbs::handle(&command, &vargs, &forwarded);
if let Some(o) = out.as_object_mut() {
// Bridge SDK: invite the bug report IN THE OUTPUT, on every failure. The AI driving
// this bridge is standing on the one machine where the failure happened and holds
// the only copy of the evidence. Never overwrites a field a handler already set.
if o.get("success").and_then(|b| b.as_bool()) == Some(false) && !o.contains_key("_reportIssues") {
o.insert("_reportIssues".into(), serde_json::json!(REPORT_ISSUES));
}
o.insert("durationMs".into(), serde_json::json!(t0.elapsed().as_millis() as u64));
o.entry("bridgeVersion").or_insert(serde_json::json!(BRIDGE_VERSION));
}
json_response(200, &out)
}
}
}
_ => json_response(404, &serde_json::json!({"error": "Not found"})),
};
let _ = req.respond(resp);
});
if let Err(e) = spawned {
eprintln!("[altium] could not spawn a request thread: {e}");
}
}
}