← 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

John Lauer ·e661d5d367 ·2mo ago ·parent f84c99a
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)