main
Colby Knox 0.2.58 source (not yet released, needs signing): autoroute, layer_stack, rule descriptors, sch_zoom f82b5b5 10h ago
//! 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}");
        }
    }
}