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).
This commit is contained in:
@@ -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:<channelWsPort>/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": "<sid>",
|
||||
"openclaw_session": "<session_key>",
|
||||
"claude_session": "<uuid>",
|
||||
"pid": 12345,
|
||||
"token": "<bridge_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": "<openclaw_session_id>",
|
||||
"message_id": "...",
|
||||
"user": "alice",
|
||||
"user_id": "...",
|
||||
"ts": "2026-05-14T...Z",
|
||||
"source_channel": "discord",
|
||||
"attachments": [ ... ]
|
||||
}
|
||||
}
|
||||
{ "type": "inbound",
|
||||
"content": "<user message text>",
|
||||
"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": "<text chunk>",
|
||||
"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 <code>`").
|
||||
|
||||
## 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).
|
||||
|
||||
Reference in New Issue
Block a user