WebSocket Protocol
This is the complete message contract between the Antimatter Gateway and the mobile client (Android or iOS). All frames are UTF-8 JSON text objects with a type discriminator field.
- Compression:
permessage-deflateenabled - Max payload: 10 MiB
- Binding: Gateway binds to
127.0.0.1:8765(IPC) and is exposed via Cloudflare tunnel
Connection Lifecycle
Section titled βConnection LifecycleβMobile App Antimatter Gateway β β β WSS upgrade (Authorization: Bearer) β β βββββββββββββββββββββββββββββββββββββββββΊβ Origin check + token verify β βββββββββββ 101 Switching Protocols ββββββ (or 401/403/close 4001) β β β AUTH_CHALLENGE { challenge: base64 } β β βββββββββββββββββββββββββββββββββββββββββΊβ Sign nonce with Ed25519 key β βββββββ AUTH_RESPONSE { signature } βββββ Client marked authenticated β β β GET_AVAILABLE_AGENTS β β βββββββββββββββββββββββββββββββββββββββββΊβ β βββββββ AVAILABLE_AGENTS { agents[] } βββ β β β βββββ Bidirectional message exchange ββββΊβ β SEND_MESSAGE / GET_FILES / WRITE_FILE... β1. Transport & Origin Validation
Section titled β1. Transport & Origin ValidationβThe Gateway rejects WebSocket upgrades unless the Origin header matches the allow-list:
vscode-webview://β¦(extension webview)https://<team>.cloudflareaccess.com(Cloudflare Access)- Mobile app origins (dynamic β derived from platform)
Rejected origins β HTTP 403 Forbidden (Cross-Site WebSocket Hijacking prevention).
2. Bearer Token Authentication
Section titled β2. Bearer Token AuthenticationβThe mobile client presents the 256-bit pairing token, checked in priority order:
Authorization: Bearer <token>header- First
Sec-WebSocket-Protocolvalue ?token=<token>URL query parameter
The token is compared with timing-safe equality (crypto.timingSafeEqual). Invalid token β close code 4001 Unauthorized.
3. Ed25519 Handshake
Section titled β3. Ed25519 HandshakeβAfter the socket opens, the client sends AUTH_CHALLENGE with a random base64 nonce. The Gateway signs the raw nonce bytes with its persistent Ed25519 private key (stored in OS keychain) and returns AUTH_RESPONSE. The client verifies the signature against the Ed25519 public key received during QR pairing β proving the Gatewayβs identity and preventing MITM attacks.
WebSocket Close Codes
Section titled βWebSocket Close Codesβ| Code | Meaning | Cause |
|---|---|---|
1000 |
Normal closure | Clean disconnect |
4000 |
Rate limited | Too many failed token attempts (60s ban) |
4001 |
Unauthorized | Missing or invalid pairing token |
403 |
Forbidden Origin (HTTP) | Origin header not in allow-list |
Inbound Messages (App β Gateway)
Section titled βInbound Messages (App β Gateway)βThese are sent from the mobile client to the Gateway.
AUTH_CHALLENGE
Section titled βAUTH_CHALLENGEβBegin the Ed25519 cryptographic handshake.
{ "type": "AUTH_CHALLENGE", "challenge": "base64-encoded-random-nonce"}GET_AVAILABLE_AGENTS
Section titled βGET_AVAILABLE_AGENTSβRequest the list of currently connected adapters.
{ "type": "GET_AVAILABLE_AGENTS"}SEND_MESSAGE
Section titled βSEND_MESSAGEβInject a prompt into the target AI agent.
{ "type": "SEND_MESSAGE", "target": "ag", "content": "Refactor the authentication module"}| Field | Type | Required | Description |
|---|---|---|---|
target |
string |
β | Adapter name ("ag", "ag2", "cc") |
content |
string |
β | Text prompt to inject |
images |
string[] |
β | Base64-encoded image attachments |
GET_FILES
Section titled βGET_FILESβRequest the workspace file tree from the target adapter.
{ "type": "GET_FILES", "target": "ag", "path": "/"}READ_FILE
Section titled βREAD_FILEβRead the contents of a specific file.
{ "type": "READ_FILE", "target": "ag", "path": "/src/main/App.kt"}WRITE_FILE
Section titled βWRITE_FILEβWrite content to a file in the workspace.
{ "type": "WRITE_FILE", "target": "ag", "path": "/src/main/App.kt", "content": "package com.example\n..."}SUBSCRIBE_CONVERSATION
Section titled βSUBSCRIBE_CONVERSATIONβSubscribe to real-time trajectory updates for a specific conversation.
{ "type": "SUBSCRIBE_CONVERSATION", "conversationId": "uuid-string"}Keepalive ping to prevent Cloudflare from closing idle connections.
{ "type": "PING" }Outbound Messages (Gateway β App)
Section titled βOutbound Messages (Gateway β App)βThese are sent from the Gateway to the mobile client.
AUTH_RESPONSE
Section titled βAUTH_RESPONSEβReply to AUTH_CHALLENGE with the Ed25519 signature.
{ "type": "AUTH_RESPONSE", "signature": "base64-encoded-ed25519-signature"}AVAILABLE_AGENTS
Section titled βAVAILABLE_AGENTSβList of currently registered and connected adapters.
{ "type": "AVAILABLE_AGENTS", "agents": ["ag", "ag2", "cc"]}A single trajectory step from the AI agent β streamed in real-time.
{ "type": "STEP", "conversationId": "uuid-string", "stepIndex": 42, "stepType": "tool_call", "content": "...", "toolName": "bash", "toolInput": { "command": "git status" }, "toolResult": "On branch main..."}stepType |
Description |
|---|---|
thinking |
Agent internal reasoning (may be truncated) |
text |
Agent text response |
tool_call |
Agent invokes a tool |
tool_result |
Tool execution result |
artifact |
File artifact created by agent |
FILE_TREE
Section titled βFILE_TREEβResponse to GET_FILES.
{ "type": "FILE_TREE", "path": "/", "entries": [ { "name": "src", "isDirectory": true }, { "name": "README.md", "isDirectory": false, "size": 4096 } ]}FILE_CONTENT
Section titled βFILE_CONTENTβResponse to READ_FILE.
{ "type": "FILE_CONTENT", "path": "/src/main/App.kt", "content": "package com.example\n...", "encoding": "utf-8"}Reply to PING.
{ "type": "PONG" }Returned when a handler throws or an unknown message type is received.
{ "type": "ERROR", "code": "UNKNOWN_TYPE", "message": "Unrecognized message type: FOO"}Writing a Custom Adapter
Section titled βWriting a Custom AdapterβAny adapter that speaks this IPC protocol can integrate with Antimatter. Hereβs a minimal example:
import asyncio, websockets, json
async def main(): async with websockets.connect("ws://127.0.0.1:8765") as ws: # Register with the Gateway await ws.send(json.dumps({ "type": "REGISTER_ADAPTER", "name": "my-custom-agent" }))
async for message in ws: msg = json.loads(message) if msg["type"] == "SEND_MESSAGE": print(f"Received prompt: {msg['content']}") # Process and respond...
asyncio.run(main())