app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Reference implementation of the KiCad bridge: multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.
← Commit history
issues #12/#13: capture + log the X-Adom-Caller-* identity per verb; caller_identity module with forward_headers()/ad_callback() that echoes caller + adds Delegate:kicad; best-effort window labeling when an hwnd is present
6 files changed
+140−8
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.66+0.9.67
adom-bridge-kicad-manifest.json+5−5@@ -1,15 +1,15 @@ { "manifest_version": 1, "name": "kicad",- "version": "0.10.0",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-kicad-bridge/0.10.0/adom-bridge-kicad-v0.10.0.zip",- "sha256": "42dfbdc396af9b011fef9fb9da125c5a9287374c3643432517d8b0506b6b89c9",- "size": 372370,+ "version": "0.9.67",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-kicad-bridge/0.9.67/adom-bridge-kicad-v0.9.67.zip",+ "sha256": "62752964540ef2e2fd5410b6969418da23b2750c4405f0d981a3214301e81c69",+ "size": 365363, "verbPrefixes": [ "kicad_" ], "healthEndpoint": "/status",- "released_at": "2026-07-23T18:13:17Z",+ "released_at": "2026-07-25T13:41:07Z", "hero": "https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/kicad-hero.png", "languages": [ "Python",
adom-bridge-kicad-v0.9.67.zipadded⋯ 1 unchanged line ⋯
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "kicad", "displayName": "KiCad EDA",- "version": "0.9.66",+ "version": "0.9.67", "description": "Reverse bridge for KiCad \u2014 board/schematic introspection, lint via kicad-cli, plugin install, multi-instance probe, in-process DRC.", "homepage": "https://wiki.adom.inc/adom/adom-desktop", "author": "Adom Inc.",
caller_identity.pyadded+105@@ -0,0 +1,105 @@+"""Caller provenance — WHO asked (AD 1.9.180 + 1.9.183).++A user runs ~20 AI threads against one desktop. AD now stamps three headers on+every relayed request so a bridge can attribute work to the thread that caused+it. This module keeps that identity for the life of a request and re-forwards it+on any AD callback the bridge makes while carrying out that request, so the+Activity Log shows `chip-fetcher tab 3 (via kicad)` instead of a nameless bridge.++Trust rule (AD's, and it is not optional): these values are SELF-ASSERTED, like+`reason`. Use them for logging, UX, window labels and arbitration hints — NEVER+for authorization. `if thread == "admin-tab": allow()` is a hole, not a check.+"""++import threading++# The identity of the request THIS thread is currently serving. A threading+# HTTPServer handles each request on its own thread, so thread-local is the+# right scope — no request can see another's caller.+_current = threading.local()++_HEADERS = ("X-Adom-Caller-Thread", "X-Adom-Caller-Container", "X-Adom-Caller-Reason")++# What we name ourselves when we forward. Kept generic ("kicad") to match the+# verb prefix AD already knows us by.+DELEGATE = "kicad"+++def set_from_headers(headers) -> dict:+ """Capture the caller headers off an incoming request. `headers` is anything+ with .get() (http.server's message object). Returns the captured dict."""+ ident = {h: (headers.get(h, "") or "") for h in _HEADERS}+ _current.ident = ident+ return ident+++def clear() -> None:+ _current.ident = None+++def thread_name() -> str:+ """The caller's thread name, or '' if none was supplied (loopback callers —+ HD, another bridge, the local CLI — are exempt and send nothing)."""+ ident = getattr(_current, "ident", None) or {}+ return ident.get("X-Adom-Caller-Thread", "")+++def log_suffix() -> str:+ """A `caller="..."` fragment for per-verb log lines, or '' when anonymous."""+ t = thread_name()+ return f' caller="{t}"' if t else ""+++def forward_headers() -> dict:+ """Headers to put on an AD callback made WHILE serving an AI's verb. Echoes+ the three we were handed and ADDS X-Adom-Caller-Delegate naming us, so AD can+ show both facts: who asked AND who ran it. Empty when there's no caller to+ forward (e.g. a self-initiated poll) — see self_caller() for that case."""+ ident = getattr(_current, "ident", None) or {}+ if not ident.get("X-Adom-Caller-Thread"):+ return {}+ fwd = {h: ident.get(h, "") for h in _HEADERS}+ fwd["X-Adom-Caller-Delegate"] = DELEGATE+ return fwd+++def self_caller() -> dict:+ """The `caller` args block to send on a callback the bridge makes on its OWN+ behalf (health poll, timer, crash cleanup). An explicit caller in args always+ wins over the forwarded headers, so this never leaks a stale thread name."""+ return {"aiThread": "kicad bridge (self)", "containerName": "local"}+++def ad_callback(command: str, args: dict = None, on_behalf: bool = True, timeout: int = 30):+ """Call an AD verb back over the loopback direct API, forwarding the caller.++ `on_behalf=True` (default): this call is part of serving an AI's verb, so it+ forwards the X-Adom-Caller-* headers + adds our Delegate — AD shows+ "chip-fetcher tab 3 (via kicad)". `on_behalf=False`: a self-initiated call+ (poll/timer/cleanup) — sends our own `caller` in args so no stale thread leaks.++ Reads ADOM_DIRECT_API_URL + ADOM_BRIDGE_TOKEN from the spawn env (AD injects+ both). Returns the parsed JSON, or {"success": False, ...} if AD is+ unreachable or the env isn't populated. NEVER raises — a labeling/notify+ callback failing must not fail the KiCad verb the user actually asked for.+ """+ import os, json as _json, urllib.request+ base = os.environ.get("ADOM_DIRECT_API_URL", "").rstrip("/")+ if not base:+ return {"success": False, "error": "ADOM_DIRECT_API_URL not in env (inbound-only spawn?)"}+ body = {"command": command, "args": dict(args or {})}+ headers = {"Content-Type": "application/json"}+ tok = os.environ.get("ADOM_BRIDGE_TOKEN")+ if tok:+ headers["X-Adom-Bridge-Token"] = tok # attribution badge, not an auth gate+ if on_behalf:+ headers.update(forward_headers())+ else:+ body["args"].setdefault("caller", self_caller())+ try:+ req = urllib.request.Request(f"{base}/command", data=_json.dumps(body).encode(),+ headers=headers, method="POST")+ with urllib.request.urlopen(req, timeout=timeout) as r:+ return _json.loads(r.read().decode() or "{}")+ except Exception as e:+ return {"success": False, "error": f"AD callback failed: {e}"}
server.py+28−1@@ -67,6 +67,7 @@ from handlers.demo import handle_demo from handlers.upgrade import handle_check_for_updates, handle_upgrade from adom_library import ensure_adom_library import plugin_install+import caller_identity DEFAULT_PORT = 8772 @@ -735,6 +736,26 @@ def dispatch_command(command: str, args: dict) -> dict: except Exception: pass + # #12.3: label the KiCad window we just raised with the owning AI thread,+ # so the USER's taskbar/Alt-Tab shows which of their ~20 tabs owns it. Uses+ # the caller captured in do_POST and forwards it (via kicad) per #13. Best+ # effort and non-blocking: if AD is unreachable, or this is a loopback+ # caller with no identity, we simply skip — never fail the user's verb.+ try:+ _thread = caller_identity.thread_name()+ _hwnd = result.get("hwnd") or (result.get("data") or {}).get("hwnd") if isinstance(result, dict) else None+ # TODO: open_* handlers don't surface hwnd yet, so this only fires for+ # verbs that do (place_footprint). Wire hwnd into the open handlers'+ # results to light up labeling for every editor window.+ if _thread and _hwnd:+ caller_identity.ad_callback("desktop_set_window_identity",+ {"hwnd": int(_hwnd), "appName": f"KiCad ({_thread})",+ "tooltip": f"KiCad · driven by {_thread}"})+ if isinstance(result, dict):+ result["_windowLabeledFor"] = _thread+ except Exception:+ pass+ # Attach plugin auto-install report ONCE per bridge boot (on the # first kicad_* command's response) so the AI knows whether the # Phase 2 reverse-bridge plugin was deployed / updated / already@@ -811,7 +832,11 @@ class KiCadBridgeHandler(BaseHTTPRequestHandler): command = request.get("command", "") args = request.get("args", {}) - print(f"[KiCad Bridge] Command: {command} | Args: {json.dumps(args)}")+ # AD 1.9.180: stamp WHO asked (self-asserted; logging/UX/arbitration only,+ # never authorization). Kept for the life of this request so any AD+ # callback we make while serving it can forward the identity (AD 1.9.183).+ caller = caller_identity.set_from_headers(self.headers)+ print(f"[KiCad Bridge] {command}{caller_identity.log_suffix()} | Args: {json.dumps(args)}") try: result = dispatch_command(command, args)@@ -823,6 +848,8 @@ class KiCadBridgeHandler(BaseHTTPRequestHandler): "success": False, "error": f"Internal error: {e}", })+ finally:+ caller_identity.clear() def _respond(self, status: int, data: dict): self.send_response(status)