← 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 →