Skip to content

WebSocket Protocol

The WebSocket tunnel connects Gremia Shell to Gremia Cloud for bidirectional, real-time communication. All messages are encrypted with AES-256-GCM after the initial key exchange.

Endpoint

wss://api.gremia.io/api/v1/tunnel/ws

Connection Lifecycle

sequenceDiagram
    participant S as Shell
    participant C as Cloud

    Note over S,C: 1. WebSocket Handshake
    S->>C: Upgrade request + JWT in Sec-WebSocket-Protocol
    C->>C: Validate JWT
    C-->>S: 101 Switching Protocols

    Note over S,C: 2. Authentication
    S->>C: {"type":"auth","shell_id":"..."}
    C->>C: Bind shell_id to user

    Note over S,C: 3. Key Exchange
    C->>S: {"type":"key_exchange","session_key":"base64..."}
    S->>S: Store session key

    Note over S,C: 4. Encrypted Communication
    S->>C: EncryptedEnvelope (task, tool_result, heartbeat)
    C->>S: EncryptedEnvelope (tool_call, task_response, heartbeat)

Authentication

JWT is sent via the Sec-WebSocket-Protocol header (not a query parameter, to avoid logging):

Sec-WebSocket-Protocol: access_token.eyJhbGciOiJIUzI1NiIs...

The Cloud validates the JWT and echoes the subprotocol back during the 101 response.

After the WebSocket is accepted, the Shell must send an auth message with its shell_id within 10 seconds:

{
  "type": "auth",
  "shell_id": "unique-shell-identifier"
}

If the auth message is not received within the timeout, the connection is closed.

Client Certificate Validation

If mTLS is configured (via Nginx), the X-Client-Cert header is checked:

  1. Extract the certificate serial number
  2. Check against the revocation list in the database
  3. Reject if revoked; allow if valid or no cert present

mTLS is optional — connections without client certificates are allowed but may have reduced access.

Key Exchange

After successful authentication, the Cloud initiates a session key exchange:

{
  "type": "key_exchange",
  "session_key": "base64-encoded-32-byte-aes-256-key"
}

The Shell stores this key and uses it for all subsequent message encryption/decryption. The key is ephemeral and discarded when the connection closes.

Encrypted Envelope

After key exchange, all messages are wrapped in an encrypted envelope:

{
  "iv": "base64-encoded-12-byte-nonce",
  "ciphertext": "base64-encoded-aes-256-gcm-ciphertext"
}

Encryption process:

  1. Serialize the message to JSON
  2. Generate a random 12-byte nonce
  3. Encrypt with AES-256-GCM using the session key
  4. Base64-encode both nonce and ciphertext

Decryption process:

  1. Parse the envelope JSON
  2. Base64-decode the iv and ciphertext
  3. Decrypt with AES-256-GCM using the session key
  4. Parse the resulting JSON as a tunnel message

Message Types

Shell to Cloud

auth

Initial authentication message sent after WebSocket handshake.

{
  "type": "auth",
  "shell_id": "shell-uuid"
}

task

Submit a new task for execution.

{
  "type": "task",
  "shell_id": "shell-uuid",
  "payload": {
    "id": "task-uuid",
    "content": "Generate the weekly sales report",
    "manifest_id": "manifest-uuid"
  }
}

tool_result

Return the result of a tool execution.

{
  "type": "tool_result",
  "shell_id": "shell-uuid",
  "correlation_id": "corr-uuid",
  "payload": {
    "success": true,
    "output": "{\"rows\": 42}",
    "duration_ms": 150
  }
}
Field Description
correlation_id Matches the correlation_id from the tool_call message
success Whether the tool executed successfully
output Tool output (JSON string)
error Error message if success is false
duration_ms Execution time in milliseconds

heartbeat

Keep-alive ping from the Shell.

{
  "type": "heartbeat",
  "shell_id": "shell-uuid"
}

Cloud to Shell

heartbeat

Keep-alive ping from the Cloud. Sent every 30 seconds.

{
  "type": "heartbeat",
  "ts": 1739620000.123
}

tool_call

Request the Shell to execute a tool on a local MCP server.

{
  "type": "tool_call",
  "id": "call-uuid",
  "execution_id": "exec-uuid",
  "server_id": "google-sheets",
  "tool_name": "read_range",
  "arguments": {
    "spreadsheet_id": "abc123",
    "range": "Sales!A1:D100"
  },
  "requires_approval": false,
  "correlation_id": "corr-uuid",
  "timestamp": "2026-02-15T10:00:00Z"
}
Field Description
server_id MCP server ID from the manifest
tool_name Tool to invoke via JSON-RPC tools/call
arguments Tool arguments (JSON object)
requires_approval If true, Shell must get user consent before executing
correlation_id Used to match with the corresponding tool_result

task_response

Streamed response from agent execution.

{
  "type": "task_response",
  "id": "task-uuid",
  "execution_id": "exec-uuid",
  "content": "{\"output\": \"Report generated successfully\"}",
  "is_final": true,
  "agent_id": "report-gen",
  "event_type": "complete",
  "timestamp": "2026-02-15T10:00:10Z"
}
Field Description
is_final If true, this is the last message for this task
event_type step, complete, or error
agent_id Which agent produced this response

key_exchange

Session key delivery (sent once after auth).

{
  "type": "key_exchange",
  "session_key": "base64-encoded-32-byte-key"
}

error

Error notification from the Cloud.

{
  "type": "error",
  "detail": "unexpected type: unknown_type"
}

Heartbeat Protocol

The Cloud sends heartbeat messages every 30 seconds. If the Shell does not receive a heartbeat for 90 seconds, it should consider the connection lost and attempt reconnection.

The Shell may also send heartbeats, which the Cloud acknowledges silently.

Reconnection

When the connection drops, the Shell reconnects with exponential backoff:

Attempt 1: wait 2s  → reconnect
Attempt 2: wait 4s  → reconnect
Attempt 3: wait 8s  → reconnect
...
Attempt N: wait min(2^N, 64)s → reconnect
Attempt 10: give up

On reconnection:

  1. The full handshake repeats (JWT auth + shell_id + key exchange)
  2. A new session key is generated
  3. In-flight tool calls from the previous session are lost

Rate Limiting

Limit Value
Max connections per IP 5 per minute
Exceeded action WebSocket close code 4029

Close Codes

Code Meaning
1000 Normal closure
1001 Going away (server shutdown)
4001 Authentication failed
4029 Rate limit exceeded

Example: Full Session

import asyncio
import json
import websockets

async def connect():
    uri = "wss://api.gremia.io/api/v1/tunnel/ws"
    token = "eyJhbG..."

    async with websockets.connect(
        uri,
        subprotocols=[f"access_token.{token}"]
    ) as ws:
        # 1. Send auth
        await ws.send(json.dumps({
            "type": "auth",
            "shell_id": "my-shell-001"
        }))

        # 2. Receive key exchange
        msg = json.loads(await ws.recv())
        assert msg["type"] == "key_exchange"
        session_key = base64.b64decode(msg["session_key"])

        # 3. Send a task (would be encrypted in production)
        await ws.send(json.dumps({
            "type": "task",
            "shell_id": "my-shell-001",
            "payload": {
                "id": "task-001",
                "content": "Hello, agents!",
                "manifest_id": "manifest-001"
            }
        }))

        # 4. Receive responses
        async for message in ws:
            data = json.loads(message)
            print(data)
            if data.get("is_final"):
                break