← Commit history

0.9.304: Tour identity fix (John's dashboard run, 2026-09-03): ab refused the demo panel, every caption, every raise and every window close from the tour thread with 'carries no caller identity' (toast fallback, no captions, nothing in front). Every loopback call the bridge makes now carries identity: the X-Adom-Caller headers of the request being served, or the bridge's own self caller otherwise; the tour thread and ab's callback thread adopt the identity of the request that started the tour. Also: a failed panel show no longer demotes later tours to the toast (per-tour reset, one retry), and every 3D viewer plus the Footprint Editor are closed before the beats so the footprint beat opens a fresh, re-indexed editor.

John Lauer ·4581eb5ab5 ·1mo ago ·parent 5a73793
4 files changed +79−4
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.303\ No newline at end of file+0.9.304\ No newline at end of file
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "kicad",   "displayName": "KiCad EDA",-  "version": "0.9.303",+  "version": "0.9.304",   "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-bridge",   "author": "Adom Inc.",
handlers/ad_client.py+17
@@ -120,6 +120,16 @@ def _http(method, path, body=None, timeout=_TIMEOUT):     url = base + path     data = None     headers = {"Accept": "application/json"}+    # Caller identity on EVERY loopback call (John's dashboard run, 2026-09-03:+    # ab refused the demo panel, captions, raises and closes from the tour+    # thread with "carries no caller identity"). Forward the request's+    # X-Adom-Caller-* headers when we are serving one; otherwise the bridge+    # speaks for itself (self_caller in args, below), never anonymously.+    try:+        import caller_identity as _ci+        headers.update(_ci.forward_headers() or {})+    except Exception:  # pylint: disable=broad-except+        pass     tok = _token()     if tok:         headers["X-Adom-Bridge-Token"] = tok@@ -220,6 +230,13 @@ def call(command: str, args=None, target=None, timeout=_TIMEOUT):     target: a peer AD clientName (from `peers()`), or "all" to fan out. Omit to     run on THIS AD (the one hosting the bridge)."""     payload = {"command": command}+    args = dict(args or {})+    try:+        import caller_identity as _ci+        if not _ci.thread_name() and "caller" not in args:+            args["caller"] = _ci.self_caller()      # a bridge-initiated call names the bridge+    except Exception:  # pylint: disable=broad-except+        pass     if args:         payload["args"] = args     if target:
handlers/demo.py+60−2
@@ -914,8 +914,8 @@ def _panel_show(stage: int, state: str | None = "playing") -> bool:             args["state"] = state         r = ad_client.call("desktop_demo_panel", args, timeout=8) or {}         err = str(r.get("error") or "")-        if "unknown verb" in err.lower() or "no installed bridge" in err.lower():-            _PANEL["ok"] = False+        if "unknown verb" in err.lower():+            _PANEL["ok"] = False           # this ab predates the panel: toast for the rest of the tour             return False         out = r.get("output") if isinstance(r, dict) else None         if isinstance(out, str):@@ -927,6 +927,20 @@ def _panel_show(stage: int, state: str | None = "playing") -> bool:         data = (out or {}).get("data") if isinstance(out, dict) else None         d = data or out or {}         shown = bool(d.get("shown")) or (r.get("success") is True and not err)+        if not shown and not r:+            # AD unreachable at this instant (it restarts around installs): retry+            # once after a beat rather than falling back to the toast for good.+            _t_retry = __import__("time"); _t_retry.sleep(2.0)+            r = ad_client.call("desktop_demo_panel", args, timeout=8) or {}+            out = r.get("output") if isinstance(r, dict) else None+            if isinstance(out, str):+                try:+                    import json as _j+                    out = _j.loads(out)+                except Exception:  # pylint: disable=broad-except+                    out = {}+            d = ((out or {}).get("data") if isinstance(out, dict) else None) or out or {}+            shown = bool(d.get("shown")) or (r.get("success") is True and not str(r.get("error") or ""))         _job_update(panel={"shown": shown, "stage": int(stage), "callbackUrl": args.get("eventCallbackUrl"),                            "error": (err or d.get("error") or None),                            "reply": {k: d.get(k) for k in ("shown", "id", "state", "stage", "callbackAccepted", "error") if k in d}})@@ -962,6 +976,7 @@ def on_panel_event(ev: dict) -> dict:     """ab's instant callback (POST /demo/event) for one press. Applies mute at     once, stops narration at once on pause/stop, and queues the transport     command for the beat boundary (a beat in flight cannot be interrupted)."""+    _adopt_tour_identity()     if not isinstance(ev, dict) or str(ev.get("id") or _PANEL_ID) != _PANEL_ID:         return {"ignored": True}     at = ev.get("at")@@ -1128,9 +1143,43 @@ def _caption(text: str, duration_ms: int = 60000) -> None:         pass  +def _adopt_tour_identity() -> None:+    """Re-apply the identity of the request that STARTED the tour on this thread.++    John's dashboard run, 2026-09-03: the tour thread called ab with no caller+    identity (identity is thread-local to the HTTP request that spawned the+    job), so ab REFUSED the demo panel, every caption, every raise and every+    window close: no remote, no captions, toast fallback, nothing in front.+    Identity is mandatory on the desktop; the tour carries the starter's."""+    try:+        import caller_identity+        fwd = _JOB.get("callerFwd") or {}+        if fwd:+            caller_identity.set_from_headers(fwd)+    except Exception:  # pylint: disable=broad-except+        pass+++def _close_stale_tour_windows() -> None:+    """A clean slate for the windows the tour will drive: every '3D Viewer'+    and the Footprint Editor, which otherwise sit on last time's part with a+    stale library tree (the footprint beat failed against exactly that)."""+    try:+        from handlers.kicad_ui import _find_all_kicad_windows+        for w in _find_all_kicad_windows():+            t = (w.get("title") or "").lower()+            if "3d viewer" in t or "footprint editor" in t:+                _close_hwnd(int(w["hwnd"]))+        import time as _t+        _t.sleep(1.5)+    except Exception:  # pylint: disable=broad-except+        pass++ def _run_demo_background(kicad_info: dict) -> None:     """Thread body: the six beats with live progress accounting."""     import time as _t+    _adopt_tour_identity()     t0 = _t.monotonic()     total_est = sum(_BEAT_EST.values())     # BACKGROUND CONTRACT: the dispatcher's focus guardian only covers the@@ -1150,6 +1199,7 @@ def _run_demo_background(kicad_info: dict) -> None:         _EXIT["now"] = False         _PANEL_SEEN.clear()         _PANEL_SHOWN_ONCE["v"] = False+        _PANEL["ok"] = None            # a failed show in an EARLIER tour must not demote this one to the toast         _PANEL["scriptState"] = None         # John, 2026-09-03: "no audio" and "jumped to step 3 instantly". The mute         # flag outlived the previous tour, and ab keeps undrained presses for a@@ -1159,6 +1209,7 @@ def _run_demo_background(kicad_info: dict) -> None:         with _CTRL_LOCK:             _CTRL["cmd"] = None            # a control queued against the previous tour must not steer this one         _drain_stale_presses()+        _close_stale_tour_windows()         _panel_show(0, "playing")         _caption(_BEAT_CAPTIONS["prepare"], duration_ms=_BEAT_CAPTION_MS["prepare"])         _play_narration("prepare")@@ -1426,6 +1477,13 @@ def handle_demo(kicad_info: dict, args: dict) -> dict:                         stepLabel="Starting the tour", beatIndex=0,                         beatCount=len(STEPS), percent=0, elapsedSec=0,                         estimatedSec=est)+        try:+            import caller_identity+            with _JOB_LOCK:+                _JOB["callerFwd"] = dict(caller_identity.forward_headers() or {})+        except Exception:  # pylint: disable=broad-except+            pass+         _threading.Thread(target=_run_demo_background, args=(kicad_info,),                           daemon=True, name="kicad-demo-job").start()         return {"success": True, "started": True, "estimatedSec": est,