From 57fc8610edf9bb4471344b06f2bd4a50e3307227 Mon Sep 17 00:00:00 2001 From: zhi Date: Thu, 14 May 2026 13:28:22 +0000 Subject: [PATCH] chore: bump submodules + update wire protocol + STATUS Submodules now have real implementations of the HTTP+WS bridge and the channel-side MCP server. STATUS reflects locked architecture decisions and the laptop smoke-test plan. wire-protocol.md updated to drop the auth token and permission_request descriptions (no longer in scope). --- STATUS.md | 91 +++++++-------------- SynthesisAgent.ClaudePlugin | 2 +- SynthesisAgent.OpenclawPlugin | 2 +- docs/wire-protocol.md | 147 ++++++++++++++-------------------- 4 files changed, 95 insertions(+), 147 deletions(-) diff --git a/STATUS.md b/STATUS.md index abe6fd5..66e030c 100644 --- a/STATUS.md +++ b/STATUS.md @@ -1,68 +1,39 @@ -# SynthesisAgent — Status (paused 2026-05-14) +# SynthesisAgent — Status (in development, 2026-05-14) -## Where we left off +## Architecture decisions (locked) -Scaffolding complete. Two submodules wired up with their core skeletons: +- **HTTP provider pattern**: OpenclawPlugin registers as model provider `synthesis-claude-bridge`. OpenClaw routes agent turns via `POST /v1/chat/completions`. (Mirrors contractor-agent.) +- **WebSocket bridge**: ClaudePlugin (in spawned claude) dials back into OpenclawPlugin's WS. Single long-lived connection per Claude process. +- **Session granularity**: `agent_id::chat_id` (matches contractor-agent's `buildSessionKey`). +- **Reply channel**: ClaudePlugin exposes a `reply(text, final?)` MCP tool. Model calls it. ClaudePlugin → WS → OpenclawPlugin → SSE → OpenClaw. +- **Channel dev mode**: `--dangerously-load-development-channels server:synthesis`. Process-manager pipes "1\n" to stdin to auto-confirm the dev dialog. +- **No auth, no permission_request reverse**: same-machine deployment, full perms. +- **Tool catalog v1**: ClaudePlugin only exposes `reply`. Claude Code's built-in tools (Read/Edit/Bash) do everything else. OpenClaw tool proxy deferred to v2. -- `SynthesisAgent.ClaudePlugin/server.ts` — stdio MCP server + bridge WebSocket client. Reads 4 env vars, forwards `tool_call` / `permission_request` to OpenClaw side, translates `inbound_message` frames to `notifications/claude/channel`. -- `SynthesisAgent.OpenclawPlugin` — process manager (spawn/resume/idle-GC), session mapping (JSON persist), bridge WS server, OpenClaw plugin entry. Tool-dispatch and inbound-event-subscription are stubbed. +## Implementation status -Wire protocol fully specified in `docs/wire-protocol.md`. +- ✅ Scaffold for both submodules + parent repo +- ✅ Submodules are separate gitea repos, referenced by SHA from parent +- ✅ Capability declaration uses `experimental.claude/channel` (validated bug fix) +- ✅ OpenclawPlugin: HTTP + WS server, process manager, session mapping, FIFO queue, SSE heartbeat, `--channels` spawn with dev-confirm auto-pipe +- ✅ ClaudePlugin: WS client, MCP server, `reply` tool, `notifications/claude/channel` emission, reconnect +- ✅ Wire protocol doc reflects current behavior +- ⏳ Not yet tested end-to-end on laptop — next step -## Open blockers (in priority order) +## Laptop smoke test plan -### Done / validated 2026-05-14 on laptop (SSH via T2) +See `SynthesisAgent.OpenclawPlugin/README.md` for the exact commands. Summary: +1. `claude mcp add --scope user synthesis -- bun run /server.ts` +2. `bun index.ts` in OpenclawPlugin (standalone mode) +3. `curl POST /v1/chat/completions` with a "say READY" prompt +4. Watch for "READY" in the SSE stream -- ✅ `--channels` flag exists in Claude Code 2.1.140/141 (hidden from `--help`) -- ✅ Syntax: `plugin:@` or `server:` -- ✅ `--plugin-dir ` IS documented and works for local plugin loading -- ✅ Capability declaration must be `experimental.claude/channel` not top-level — **scaffolding patched** -- ✅ `-p` mode does NOT trigger turns from channel notifications — interactive only -- ✅ Full gating chain decoded from binary (see `memory/claude_channels_protocol.md`) -- ❌ Did NOT capture an end-to-end "claude responds to channel notification" trace — got stuck at `--dangerously-load-development-channels` allowlist gate in script-PTY runs (interactive confirm may not have flowed through). Indirect evidence is overwhelming via the Discord plugin's working production pattern; remaining doubt is small. +## Deferred to v2 -### Remaining - -1. **`.mcp.json` not written** — sandbox flagged it as sensitive on Write. Five-line file, need to drop in by hand or approve the write. -2. **Confirm channel notification → new turn empirically** — Have user run interactively (no PTY tricks): - ``` - claude --mcp-config /tmp/synthesis-mcp.json \ - --channels server:synthesis-stub \ - --dangerously-load-development-channels server:synthesis-stub - ``` - (in a trusted dir), confirm dev mode at the prompt, watch for the stub's 1.5s-delayed channel-test ping triggering a turn. -3. **Decide deployment path**: - - `server:` channels are flagged "dangerous dev only" — fine for our own use, awkward for shipping - - `plugin:` channels need to be in marketplace allowlist (org `allowedChannelPlugins`) — requires distribution mechanism - - Easiest dev path: local marketplace pointing at our repo -4. **OpenClaw plugin-sdk inbound subscription API** — `index.ts` has a TODO where `api.channels.onInbound(...)` should live. Need to read `contractor-agent/index.ts` carefully and copy the actual pattern. -5. **Tool catalog source** — `web/bridge-server.ts` sends an empty `tools: []` in `hello_ack`. Must enumerate OpenClaw's real tool surface and forward. -6. **`tool_call` dispatch** — currently returns `not implemented`. Need to wire to OpenClaw's tool execution path. -7. **Permission routing back to source channel** — bridge auto-denies any `permission_request` today. - -## Quick-start tests to run when resuming - -```bash -# 0. write .mcp.json (blocked last time) - -# 1. verify --channels accepts local path or local marketplace -claude --channels plugin:synthesis-claude@/root/.openclaw/workspace/workspace-developer/SynthesisAgent/SynthesisAgent.ClaudePlugin 2>&1 | head -20 - -# 2. once #1 works: smoke-test notifications/claude/channel from a stub server -# (build a 30-line MCP server that fakes inbound_message after 5s) -``` - -## Files to start from when picking up - -- `README.md` — context refresher -- `docs/wire-protocol.md` — protocol contract -- `SynthesisAgent.OpenclawPlugin/README.md` — TODO list for that side -- `SynthesisAgent.ClaudePlugin/README.md` — TODO list for that side - -Reference implementations to crib from: -- `~/.claude/plugins/marketplaces/claude-plugins-official/external_plugins/discord/server.ts` — channel protocol model -- `/root/.openclaw/plugins/contractor-agent/index.ts` — OpenClaw plugin lifecycle model - -## Why paused - -User pivoted to investigate a recurrence of the Discord duplicate-messages bug ([[discord_duplicate_messages]]). SynthesisAgent is the longer-term fix for that whole class of problem; the duplicate-messages bug is the short-term hotfix path. +- OpenClaw tool proxy via MCP (read/edit/etc. forwarded as MCP tools instead of using Claude Code's built-ins) — only matters if we want OpenClaw-specific tools (memory_*, wiki_*, canvas) to be accessible from the bridged session +- Full session-key extraction from OpenClaw "Conversation info" block (currently using HTTP headers) +- `plugin:` channel form for marketplace distribution (currently `server:` + dev flag) +- Permission_request reverse channel (skipped per design) +- Install / uninstall script (mirrors contractor-agent's `scripts/install.mjs`) +- Better stop-hook integration for graceful shutdown +- Multi-machine deployment (currently same-host only) diff --git a/SynthesisAgent.ClaudePlugin b/SynthesisAgent.ClaudePlugin index cb94464..813f81a 160000 --- a/SynthesisAgent.ClaudePlugin +++ b/SynthesisAgent.ClaudePlugin @@ -1 +1 @@ -Subproject commit cb9446406010ee0b8eaf303a103ed9b45f3bd2c1 +Subproject commit 813f81af9a3e73ce9c5dfd3472c6b546499698e8 diff --git a/SynthesisAgent.OpenclawPlugin b/SynthesisAgent.OpenclawPlugin index 38ac6d2..0324a47 160000 --- a/SynthesisAgent.OpenclawPlugin +++ b/SynthesisAgent.OpenclawPlugin @@ -1 +1 @@ -Subproject commit 38ac6d20b79705d23c4808e209c8e52713ced7f3 +Subproject commit 0324a47d13a1d115926b70049d80f529abe53dac diff --git a/docs/wire-protocol.md b/docs/wire-protocol.md index 135b293..d0fc291 100644 --- a/docs/wire-protocol.md +++ b/docs/wire-protocol.md @@ -1,122 +1,99 @@ -# Wire Protocol — OpenclawPlugin ↔ ClaudePlugin +# Wire Protocol -A WebSocket connection from each spawned ClaudePlugin instance back to OpenclawPlugin's bridge server. JSON messages, one per frame. +Two channels between **SynthesisAgent.OpenclawPlugin** (OpenClaw side) and **SynthesisAgent.ClaudePlugin** (Claude side): -## Connection bootstrap +1. **HTTP** — `OpenClaw → OpenclawPlugin`. OpenAI-compatible `/v1/chat/completions` SSE. This is the public boundary; OpenClaw routes agent turns here. +2. **WebSocket** — `ClaudePlugin → OpenclawPlugin`. Single long-lived per Claude process. Carries the channel-notification protocol and the reply path. -On spawn, OpenclawPlugin sets these env vars on the Claude process so ClaudePlugin can find its way home: +Same-machine deployment — no auth, no permission_request reverse channel. -| Env var | Purpose | -|--------------------------------|---------------------------------------------------------------| -| `SYNTHESIS_BRIDGE_URL` | WebSocket URL, e.g. `ws://127.0.0.1:18801/bridge` | -| `SYNTHESIS_BRIDGE_TOKEN` | Shared secret, validated server-side | -| `SYNTHESIS_OPENCLAW_SESSION` | The OpenClaw session ID this Claude process serves | -| `SYNTHESIS_CLAUDE_SESSION` | The Claude session UUID (for traceability) | +## HTTP (OpenClaw → OpenclawPlugin) -ClaudePlugin connects on startup and sends: +``` +POST /v1/chat/completions + Headers (v1, transitional — read in this order, first wins): + X-Openclaw-Agent-Id agent identifier + X-Openclaw-Chat-Id channel/chat identifier (DM, group, …) + X-Openclaw-Workspace absolute workspace dir + Body: + OpenAI-format chat completion request (messages[], tools[], …) +``` + +Session key = `${agentId}::${chatId}` (matches contractor-agent's `buildSessionKey`). + +Response: SSE stream, OpenAI chat.completion.chunk events: +- empty content delta every 30s (heartbeat — empty delta counts as model progress for OpenClaw's idle watchdog; SSE comments do **not**) +- single content delta with the reply text once `ClaudePlugin` reports `reply` with `final: true` +- final stop chunk + `data: [DONE]` + +## WebSocket bridge + +OpenclawPlugin listens on `ws://127.0.0.1:/bridge`. Each spawned Claude process dials in once. + +### Connection bootstrap + +Spawn env vars set by OpenclawPlugin's process-manager: + +| Env var | Example | +|---|---| +| `SYNTHESIS_WS_URL` | `ws://127.0.0.1:18901/bridge` | +| `SYNTHESIS_OPENCLAW_SESSION` | `developer::discord:channel:123` | +| `SYNTHESIS_CLAUDE_SESSION` | UUID for traceability | + +ClaudePlugin sends: ```json { "type": "hello", - "openclaw_session": "", + "openclaw_session": "", "claude_session": "", - "pid": 12345, - "token": "" } + "pid": 12345 } ``` -OpenclawPlugin responds with: +OpenclawPlugin replies: ```json -{ "type": "hello_ack", "tools": [ /* tool catalog */ ], "session_meta": { ... } } +{ "type": "hello_ack" } ``` -If `hello` is rejected (bad token, unknown session), the server closes the socket. - -## Direction: OpenClaw → Claude (inbound events) +### `OpenclawPlugin → ClaudePlugin` ```json -{ "type": "inbound_message", - "request_id": "msg_abc123", - "content": "user message text", - "meta": { - "chat_id": "", - "message_id": "...", - "user": "alice", - "user_id": "...", - "ts": "2026-05-14T...Z", - "source_channel": "discord", - "attachments": [ ... ] - } -} +{ "type": "inbound", + "content": "", + "meta": { "chat_id":"…", "message_id":"…", "ts":"…" } } ``` -ClaudePlugin translates each `inbound_message` into: +ClaudePlugin translates this to an MCP notification on its stdio: ```json { "method": "notifications/claude/channel", - "params": { "content": ..., "meta": ... } } + "params": { "content": "…", "meta": {…} } } ``` -…and emits it on its MCP stdio. Claude Code reacts by starting a new turn. +Claude Code reacts by starting a new turn. -## Direction: OpenClaw → Claude (permission reply) - -When the user answers a permission prompt out of band (e.g. "yes abxyz" in Discord), OpenClaw forwards: +### `ClaudePlugin → OpenclawPlugin` ```json -{ "type": "permission_reply", - "request_id": "abxyz", - "behavior": "allow" } +{ "type": "reply", + "content": "", + "final": true } ``` -ClaudePlugin emits an MCP `notifications/claude/channel/permission` for that request_id. +`final: false` lets the model stream progress chunks. OpenclawPlugin buffers them and completes the pending SSE stream when a final `reply` arrives. -## Direction: Claude → OpenClaw (tool calls) - -Each `tools/call` Claude makes on ClaudePlugin (excluding internal ones the plugin owns) is forwarded over the bridge: +### Heartbeat ```json -{ "type": "tool_call", - "call_id": "tc_001", - "tool": "pcexec", - "args": { ... } } +{ "type": "ping" } +{ "type": "pong" } ``` -OpenclawPlugin invokes the corresponding OpenClaw tool and responds: - -```json -{ "type": "tool_result", - "call_id": "tc_001", - "ok": true, - "result": { ... } } -``` - -…or with `"ok": false, "error": "..."`. - -## Direction: Claude → OpenClaw (permission request) - -When Claude Code asks for a permission via `notifications/claude/channel/permission_request`, ClaudePlugin forwards: - -```json -{ "type": "permission_request", - "request_id": "<5 lowercase letters minus 'l'>", - "tool": "Bash", - "args": { ... }, - "rationale": "..." } -``` - -OpenclawPlugin routes this to the user via the source channel (e.g. posts a Discord message: "Approve? Reply `yes `"). - -## Direction: bidirectional health - -```json -{ "type": "ping", "ts": ... } -{ "type": "pong", "ts": ... } -``` - -OpenclawPlugin sends `ping` every 30s. ClaudePlugin missing 2 consecutive pings → server marks the connection dead, allows reconnect. +OpenclawPlugin sends `ping` every 30s. ClaudePlugin missing 2 in a row → terminate the connection (it will reconnect with exponential backoff). ## Reconnect semantics -- ClaudePlugin reconnects on socket close with exponential backoff (1s, 2s, 4s, max 30s). -- During disconnect, OpenclawPlugin buffers up to N inbound messages per session (configurable). Buffer overflow drops the oldest with a warning. -- On reconnect, the `hello` message re-establishes session binding. +- ClaudePlugin reconnects with exponential backoff (1s → 30s cap). +- During disconnect, OpenclawPlugin rejects any in-flight dispatch with an error (the HTTP caller sees an SSE error chunk + `[DONE]`). +- `hello` on reconnect re-establishes routing. +- No queued message buffering yet (v1 simplification — add later if needed).