The Gateway WS protocol is the single control plane + node transport for CoderClaw. All clients (CLI, web UI, macOS app, iOS/Android nodes, headless nodes) connect over WebSocket and declare their role + scope at handshake time.
connect request.Gateway → Client (pre-connect challenge):
{
"type": "event",
"event": "connect.challenge",
"payload": { "nonce": "…", "ts": 1737264000000 }
}
Client → Gateway:
{
"type": "req",
"id": "…",
"method": "connect",
"params": {
"minProtocol": 3,
"maxProtocol": 3,
"client": {
"id": "cli",
"version": "1.2.3",
"platform": "macos",
"mode": "operator"
},
"role": "operator",
"scopes": ["operator.read", "operator.write"],
"caps": [],
"commands": [],
"permissions": {},
"auth": { "token": "…" },
"locale": "en-US",
"userAgent": "coderclaw-cli/1.2.3",
"device": {
"id": "device_fingerprint",
"publicKey": "…",
"signature": "…",
"signedAt": 1737264000000,
"nonce": "…"
}
}
}
Gateway → Client:
{
"type": "res",
"id": "…",
"ok": true,
"payload": { "type": "hello-ok", "protocol": 3, "policy": { "tickIntervalMs": 15000 } }
}
When a device token is issued, hello-ok also includes:
{
"auth": {
"deviceToken": "…",
"role": "operator",
"scopes": ["operator.read", "operator.write"]
}
}
{
"type": "req",
"id": "…",
"method": "connect",
"params": {
"minProtocol": 3,
"maxProtocol": 3,
"client": {
"id": "ios-node",
"version": "1.2.3",
"platform": "ios",
"mode": "node"
},
"role": "node",
"scopes": [],
"caps": ["camera", "canvas", "screen", "location", "voice"],
"commands": ["camera.snap", "canvas.navigate", "screen.record", "location.get"],
"permissions": { "camera.capture": true, "screen.record": false },
"auth": { "token": "…" },
"locale": "en-US",
"userAgent": "coderclaw-ios/1.2.3",
"device": {
"id": "device_fingerprint",
"publicKey": "…",
"signature": "…",
"signedAt": 1737264000000,
"nonce": "…"
}
}
}
{type:"req", id, method, params}{type:"res", id, ok, payload|error}{type:"event", event, payload, seq?, stateVersion?}Side-effecting methods require idempotency keys (see schema).
operator = control plane client (CLI/UI/automation).node = capability host (camera/screen/canvas/system.run).Common scopes:
operator.readoperator.writeoperator.adminoperator.approvalsoperator.pairingNodes declare capability claims at connect time:
caps: high-level capability categories.commands: command allowlist for invoke.permissions: granular toggles (e.g. screen.record, camera.capture).The Gateway treats these as claims and enforces server-side allowlists.
system-presence returns entries keyed by device identity.deviceId, roles, and scopes so UIs can show a single row per device
even when it connects as both operator and node.skills.bins to fetch the current list of skill executables
for auto-allow checks.exec.approval.requested.exec.approval.resolve (requires operator.approvals scope).PROTOCOL_VERSION lives in src/gateway/protocol/schema.ts.minProtocol + maxProtocol; the server rejects mismatches.pnpm protocol:genpnpm protocol:gen:swiftpnpm protocol:checkCODERCLAW_GATEWAY_TOKEN (or --token) is set, connect.params.auth.token
must match or the socket is closed.hello-ok.auth.deviceToken and should be
persisted by the client for future connects.device.token.rotate and
device.token.revoke (requires operator.pairing scope).device.id) derived from a
keypair fingerprint.device identity during connect (operator + node).
Control UI can omit it only when gateway.controlUi.allowInsecureAuth is enabled
(or gateway.controlUi.dangerouslyDisableDeviceAuth for break-glass use).connect.challenge nonce.gateway.tls
config plus gateway.remote.tlsFingerprint or CLI --tls-fingerprint).This protocol exposes the full gateway API (status, channels, models, chat,
agent, sessions, nodes, approvals, etc.). The exact surface is defined by the
TypeBox schemas in src/gateway/protocol/schema.ts.