[object Object]

← back to A2a Lab

TK Bridge: add gated dm-write skill (TK_BRIDGE_ALLOW_WRITE), verified over A2A on temp store

b4cdc890a04128fa1287f0d3802ff3c812daf368 · 2026-08-01 21:07:13 -0700 · Steve

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit b4cdc890a04128fa1287f0d3802ff3c812daf368
Author: Steve <steve@designerwallcoverings.com>
Date:   Sat Aug 1 21:07:13 2026 -0700

    TK Bridge: add gated dm-write skill (TK_BRIDGE_ALLOW_WRITE), verified over A2A on temp store
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 README.md   | 19 ++++++++----
 tk_agent.py | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 2 files changed, 111 insertions(+), 7 deletions(-)

diff --git a/README.md b/README.md
index 8e13932..33ffe04 100644
--- a/README.md
+++ b/README.md
@@ -88,13 +88,22 @@ A2A_BASE=http://127.0.0.1:41242 python client.py "inbox showroom-builder all"
 ```
 
 Verified live: "agents" → 214 known agent identities; "inbox <agent> all" → that
-agent's real DMs (mid / from→to / ticket / text) over A2A. Skills: `agents`, `inbox`.
-Writing DMs over A2A would be a separate, **gated** capability (not built).
+agent's real DMs (mid / from→to / ticket / text) over A2A.
+
+**Skills:** `agents`, `inbox` (read) + `dm <agent> <text>` (write, **gated**). The write
+is OFF by default — a discovery bridge must not mutate the shared cross-agent log on a
+POST. Enable per-process with `TK_BRIDGE_ALLOW_WRITE=1`; `TK_EVENTS=<path>` redirects the
+store (used to verify writes against a temp file with zero pollution of the real log):
+
+```bash
+TK_EVENTS=/tmp/tk-test.jsonl TK_BRIDGE_ALLOW_WRITE=1 python tk_agent.py
+A2A_BASE=http://127.0.0.1:41242 python client.py "dm vp-operations check the pg lock canary"
+# → sent M-00001 a2a-bridge→vp-operations: check the pg lock canary
+```
 
 ## Next steps (not done — future work)
 
 - Add an A2A *client-side* helper using the SDK's own `create_client` (proto
   `SendMessageRequest` + async `StreamResponse`) alongside the raw-JSON-RPC client.
-- A gated **write** skill on the TK Bridge (`dm <agent> <text>`) that appends a real
-  `dm` event via `lib.js` `withLock`/`append` — Steve-gated since it writes the shared log.
-- Publish agent cards for real cabinet agents (vp-*) so they interoperate over A2A.
+- Serve the generated cabinet agent cards (see `cabinet_cards.py`) from a live A2A
+  directory so officers/directors are discoverable and callable over the protocol.
diff --git a/tk_agent.py b/tk_agent.py
index c98df15..9ed2d23 100644
--- a/tk_agent.py
+++ b/tk_agent.py
@@ -18,7 +18,10 @@ Run: . .venv/bin/activate && python tk_agent.py     (listens on 127.0.0.1:41242)
 from __future__ import annotations
 
 import json
+import os
+import time
 import uuid
+from datetime import datetime, timezone
 from pathlib import Path
 
 import uvicorn
@@ -41,7 +44,15 @@ from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes
 
 HOST = "127.0.0.1"
 PORT = 41242
-EVENTS = Path.home() / ".claude" / "tickets" / "events.jsonl"
+# TK_EVENTS overrides the store path (used for pollution-free testing against a temp file).
+EVENTS = Path(
+    os.environ.get("TK_EVENTS", str(Path.home() / ".claude" / "tickets" / "events.jsonl"))
+)
+LOCK = EVENTS.parent / ".lock"
+# Writes are OFF unless explicitly enabled — a discovery bridge must not mutate the
+# shared cross-agent log just because someone POSTs a `dm` command.
+ALLOW_WRITE = os.environ.get("TK_BRIDGE_ALLOW_WRITE") == "1"
+BRIDGE_FROM = os.environ.get("TK_BRIDGE_FROM", "a2a-bridge")
 
 # ── tk store reads (faithful port of ticket-system/lib.js, READ path only) ──────
 _BROADCAST = {"all", "*", "everyone"}
@@ -136,12 +147,76 @@ def known_agents() -> list[str]:
     return sorted(s)
 
 
+# ── tk store WRITE (gated; faithful port of lib.js withLock/nextMid/append) ─────
+def _next_mid() -> str:
+    mx = 0
+    for ev in _read_events():
+        if ev.get("type") == "dm":
+            try:
+                n = int(str(ev.get("mid", "")).replace("M-", ""))
+            except ValueError:
+                n = 0
+            mx = max(mx, n)
+    return "M-" + str(mx + 1).zfill(5)
+
+
+def _with_lock(fn):
+    EVENTS.parent.mkdir(parents=True, exist_ok=True)
+    deadline = time.time() + 5.0
+    while True:
+        try:
+            fd = os.open(str(LOCK), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
+        except FileExistsError:
+            try:
+                if time.time() - LOCK.stat().st_mtime > 10:
+                    LOCK.unlink()
+                    continue
+            except FileNotFoundError:
+                continue
+            if time.time() > deadline:
+                raise TimeoutError("ticket store lock timeout")
+            time.sleep(0.02)
+            continue
+        try:
+            return fn()
+        finally:
+            os.close(fd)
+            try:
+                LOCK.unlink()
+            except FileNotFoundError:
+                pass
+
+
+def send_dm(to: str, text: str, ticket: str = "") -> dict:
+    """Append a real `dm` event to the tk log (same shape lib.js writes)."""
+    def _do():
+        ev = {
+            "ts": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
+            "type": "dm",
+            "mid": _next_mid(),
+            "from": BRIDGE_FROM,
+            "to": to,
+            "text": text,
+        }
+        if ticket:
+            ev["ticket"] = ticket
+        with open(EVENTS, "a", encoding="utf-8") as f:
+            f.write(json.dumps(ev) + "\n")
+        return ev
+
+    return _with_lock(_do)
+
+
 # ── command dispatch (message text -> reply text) ───────────────────────────────
 def handle(text: str) -> str:
     cmd = (text or "").strip()
     low = cmd.lower()
     if low in ("", "help"):
-        return "tk-bridge commands:  'agents'  |  'inbox <agent>'  (append 'all' for read+unread)"
+        w = "on" if ALLOW_WRITE else "off"
+        return (
+            "tk-bridge commands:  'agents'  |  'inbox <agent>' (append 'all')  |  "
+            f"'dm <agent> <text>' (write is {w})"
+        )
     if low == "agents":
         agents = known_agents()
         return f"{len(agents)} known agents:\n" + "\n".join(f"  • {a}" for a in agents)
@@ -160,6 +235,18 @@ def handle(text: str) -> str:
             tkt = f" [{m['ticket']}]" if m["ticket"] else ""
             lines.append(f"  {m['mid']} {m['from']}→{m['to']}{tkt}: {m['text'][:90]}")
         return "\n".join(lines)
+    if low.startswith("dm"):
+        if not ALLOW_WRITE:
+            return (
+                "dm write is DISABLED. This bridge is read-only by default; a real "
+                "cross-agent DM is a shared-log write. Start with TK_BRIDGE_ALLOW_WRITE=1 to enable."
+            )
+        parts = cmd.split(maxsplit=2)
+        if len(parts) < 3:
+            return "usage: dm <agent> <text>"
+        to, body = parts[1], parts[2]
+        ev = send_dm(to, body)
+        return f"sent {ev['mid']} {ev['from']}→{ev['to']}: {ev['text'][:80]}"
     return f"unknown command {cmd!r} — try 'help'"
 
 
@@ -211,6 +298,14 @@ def build_agent_card() -> AgentCard:
                 tags=["tk", "roster", "read-only"],
                 examples=["agents"],
             ),
+            AgentSkill(
+                id="dm",
+                name="Send a DM (gated)",
+                description="Append a real DM to the tk log. Write is OFF unless the "
+                "server runs with TK_BRIDGE_ALLOW_WRITE=1.",
+                tags=["tk", "dm", "write", "gated"],
+                examples=["dm vp-operations check the pg lock canary"],
+            ),
         ],
     )
 

← 89535d8 add read-only A2A bridge over the live tk cross-agent DM log  ·  back to A2a Lab  ·  generate A2A agent cards from cabinet.yaml (12 VPs / 166 ski 9ea5e3d →