--- a/crates/aiflow-bridge/src/lib.rs 2026-11-05 18:53:59.485195031 -0600 +++ b/crates/aiflow-bridge/src/lib.rs 2026-11-05 18:41:34.107515976 -0600 @@ -295,6 +295,34 @@ result } +/// The live (IPC) verbs a task-owned, isolated KiCad instance answers for both reads and edits. +const LIVE_IPC_VERBS: &[&str] = &["kicad_routing_state", "kicad_routing_validate", "kicad_placement_state", + "kicad_placement_validate", "kicad_zone_state", "kicad_move_footprint", "kicad_route_net", "kicad_remove_route", + "kicad_apply_tracks", "kicad_apply_zone_plan", "kicad_add_zone", "kicad_select_net", "kicad_select_clear"]; + +/// Opt-in endpoint for EVERY live IPC verb, reads and edits (ADOM_AIFLOW_KICAD_SOCKET). Use it only for a +/// task-owned KiCad instance launched with its own API pipe (a private TEMP/TMP) while another session's +/// KiCad owns the default pipe; window, capture and file verbs are never redirected. An explicit +/// socketPath in the call wins. ADOM_AIFLOW_KICAD_READ_SOCKET keeps its read-only meaning. +fn live_socket_args(verb: &str, args: &Value, socket: Option<&str>) -> Value { + let mut result = args.clone(); + if LIVE_IPC_VERBS.contains(&verb) { + if let (Some(object), Some(socket)) = (result.as_object_mut(), socket) { + if !socket.trim().is_empty() && !object.contains_key("socketPath") { + object.insert("socketPath".into(), Value::String(socket.into())); + } + } + } + result +} + +fn socket_args(verb: &str, args: &Value) -> Value { + let live = std::env::var("ADOM_AIFLOW_KICAD_SOCKET").ok(); + let args = live_socket_args(verb, args, live.as_deref()); + let read = std::env::var("ADOM_AIFLOW_KICAD_READ_SOCKET").ok(); + read_socket_args(verb, &args, read.as_deref()) +} + /// The PCB editor's window in a `kicad_window_info` reply: the editor whose `kind` is `pcb_editor`. /// The macOS KiCad bridge (0.11.x) names no kind at all (`{app, hwnd, pid, rect, title}`), so there, /// and only when no editor carries a kind, it is the first window titled ` — PCB Editor`. @@ -344,8 +372,7 @@ /// process is killed and the reply is `errorCode: aiflow_timeout`. pub fn raw_call_bounded(&self, verb: &str, args: &Value, limit: std::time::Duration) -> Value { use std::io::Read; - let socket = std::env::var("ADOM_AIFLOW_KICAD_READ_SOCKET").ok(); - let args = read_socket_args(verb, args, socket.as_deref()); + let args = socket_args(verb, args); let child = Command::new("adom-bridge").args(["--ai-thread", &self.ai_thread, "--target", &self.target, verb, &args.to_string()]) .stdout(std::process::Stdio::piped()).stderr(std::process::Stdio::null()).spawn(); let mut child = match child { Ok(c) => c, Err(e) => return json!({"status": "error", "error": format!("adom-bridge not runnable: {e}")}) }; @@ -365,8 +392,7 @@ /// One adom-bridge verb, as is. pub fn raw_call(&self, verb: &str, args: &Value) -> Value { - let socket = std::env::var("ADOM_AIFLOW_KICAD_READ_SOCKET").ok(); - let args = read_socket_args(verb, args, socket.as_deref()); + let args = socket_args(verb, args); let out = Command::new("adom-bridge").args(["--ai-thread", &self.ai_thread, "--target", &self.target, verb, &args.to_string()]).output(); match out { Ok(o) => { @@ -1137,3 +1163,21 @@ assert_eq!(pcb_editor_of(&json!({"data": {"editors": []}})), None); } } + +#[cfg(test)] +mod live_socket_tests { + use super::*; + #[test] + fn live_socket_reaches_edits_and_reads_but_never_windows() { + let args = serde_json::json!({"filePath": "C:/b.kicad_pcb"}); + for verb in ["kicad_move_footprint", "kicad_route_net", "kicad_apply_zone_plan", "kicad_routing_state", "kicad_zone_state"] { + assert_eq!(live_socket_args(verb, &args, Some("ipc://iso/api.sock"))["socketPath"], "ipc://iso/api.sock", "{verb}"); + } + for verb in ["kicad_open_board", "kicad_send_key", "kicad_window_info", "kicad_screenshot_all", "kicad_run_drc"] { + assert!(live_socket_args(verb, &args, Some("ipc://iso/api.sock")).get("socketPath").is_none(), "{verb}"); + } + let explicit = serde_json::json!({"filePath": "C:/b.kicad_pcb", "socketPath": "mine"}); + assert_eq!(live_socket_args("kicad_move_footprint", &explicit, Some("other")), explicit); + assert_eq!(live_socket_args("kicad_move_footprint", &args, None), args); + } +}