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).
100 lines
3.1 KiB
Markdown
100 lines
3.1 KiB
Markdown
# Wire Protocol
|
|
|
|
Two channels between **SynthesisAgent.OpenclawPlugin** (OpenClaw side) and **SynthesisAgent.ClaudePlugin** (Claude side):
|
|
|
|
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.
|
|
|
|
Same-machine deployment — no auth, no permission_request reverse channel.
|
|
|
|
## HTTP (OpenClaw → OpenclawPlugin)
|
|
|
|
```
|
|
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": "<session_key>",
|
|
"claude_session": "<uuid>",
|
|
"pid": 12345 }
|
|
```
|
|
|
|
OpenclawPlugin replies:
|
|
|
|
```json
|
|
{ "type": "hello_ack" }
|
|
```
|
|
|
|
### `OpenclawPlugin → ClaudePlugin`
|
|
|
|
```json
|
|
{ "type": "inbound",
|
|
"content": "<user message text>",
|
|
"meta": { "chat_id":"…", "message_id":"…", "ts":"…" } }
|
|
```
|
|
|
|
ClaudePlugin translates this to an MCP notification on its stdio:
|
|
|
|
```json
|
|
{ "method": "notifications/claude/channel",
|
|
"params": { "content": "…", "meta": {…} } }
|
|
```
|
|
|
|
Claude Code reacts by starting a new turn.
|
|
|
|
### `ClaudePlugin → OpenclawPlugin`
|
|
|
|
```json
|
|
{ "type": "reply",
|
|
"content": "<text chunk>",
|
|
"final": true }
|
|
```
|
|
|
|
`final: false` lets the model stream progress chunks. OpenclawPlugin buffers them and completes the pending SSE stream when a final `reply` arrives.
|
|
|
|
### Heartbeat
|
|
|
|
```json
|
|
{ "type": "ping" }
|
|
{ "type": "pong" }
|
|
```
|
|
|
|
OpenclawPlugin sends `ping` every 30s. ClaudePlugin missing 2 in a row → terminate the connection (it will reconnect with exponential backoff).
|
|
|
|
## Reconnect semantics
|
|
|
|
- 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).
|