Cloud Relay Architecture & Implementation
This document details the architecture, security model, and implementation of the TerminaI Cloud Relay, a system providing secure remote access to the TerminaI agent without requiring complex network configuration (port forwarding) or trusting a central server with sensitive data.
1. High-Level Architecture
The system follows a Zero-Trust Relay pattern. The Cloud Relay acts as a dumb pipe, blind to the content of the traffic it forwards. All authentication and encryption happen strictly between the Host (Desktop Agent) and the Client (Web Browser).
sequenceDiagram
participant Host as Desktop Agent (@terminai/a2a-server)
participant Relay as Cloud Relay (@terminai/cloud-relay)
participant Client as Web Client (@terminai/web-client)
Note over Host: 1. Start Agent
Note over Host: 2. Generate SessionID + AES Key + Pairing Code
Host->>Relay: Connect (wss://relay.terminai.org?role=host&session=UUID)
Note over Host: 3. User copies URL with Key (#fragment)
Note over Host: 4. User reads pairing code (local-only)
Note over Client: 5. User opens URL
Note over Client: 6. Web client strips URL fragment from address bar
Client->>Relay: Connect (wss://relay.terminai.org?role=client&session=UUID)
Relay-->>Client: RELAY_STATUS: HOST_CONNECTED
Relay-->>Host: RELAY_STATUS: CLIENT_CONNECTED
Note over Client: 7. HELLO handshake (encrypted)
Client->>Relay: Send HELLO (encrypted, seq=1)
Relay->>Host: Forward HELLO (blind)
Host->>Relay: Send HELLO_ACK (encrypted, seq=1)
Relay->>Client: Forward HELLO_ACK (blind)
Note over Client: 8. Pairing (encrypted)
Client->>Relay: Send PAIR {code} (encrypted)
Relay->>Host: Forward PAIR (blind)
loop Secure Tunnel
Client->>Client: Encrypt Envelope (AES-256-GCM + AAD + seq)
Client->>Relay: Send Encrypted Payload
Relay->>Host: Forward Encrypted Payload (Blind)
Host->>Host: Decrypt + Validate (AAD + seq)
Host->>Host: Process RPC (only after pairing)
Host->>Host: Encrypt Response (AAD + seq)
Host->>Relay: Send Encrypted Response
Relay->>Client: Forward Encrypted Response (Blind)
Client->>Client: Decrypt + Validate (AAD + seq)
end
Key Components
- Desktop Agent (
@terminai/a2a-server): The authoritative host. It initiates the session, generates encryption keys, and executes agent commands. - Cloud Relay (
@terminai/cloud-relay): A lightweight, stateless WebSocket server deployed to the cloud (e.g., Google Cloud Run). It routes messages based onsessionID. - Web Client (
@terminai/web-client): The user interface running in the browser. It performs client-side encryption/decryption using the Web Crypto API.
2. Security Model (Zero Trust)
The architecture is designed so that compromising the Relay Server does NOT compromise user sessions.
- End-to-End Encryption (E2EE): All traffic is encrypted using AES-256-GCM.
- Key Distribution: The encryption key is generated ephemerally by the
Desktop Agent and encoded in the URL fragment (hash).
- Example:
https://terminai.org/remote#session=...&key=... - Browsers do not send the fragment to the web server serving the client.
- The Desktop Agent and Web Client never send the key to the Relay Server.
- Example:
- Ephemeral Sessions: Keys and Session IDs are generated fresh on every agent startup. There are no long-lived "master passwords."
Additional Hardenings
- Anti-replay & ordering: Every encrypted message is wrapped in a versioned
envelope that includes a strict, monotonic per-direction
seq. Both sides reject out-of-order or replayed sequences. - AEAD binding (AAD): AES-GCM uses Additional Authenticated Data to bind
ciphertext integrity to the
sessionId, protocol version, and message direction. - Handshake gate: The host refuses to process RPC until a
HELLO/HELLO_ACKhandshake completes. - Pairing gate: The host requires a local pairing code (displayed on the host) before allowing remote RPC execution.
Encryption Protocol
- Algorithm: AES-256-GCM
- Key Length: 256 bits (32 bytes)
- IV (Initialization Vector): 12 bytes, randomized per message.
- Auth Tag: 16 bytes, appended to ciphertext for integrity verification.
- Payload Format:
[ IV (12 bytes) ] [ Tag (16 bytes) ] [ Ciphertext (N bytes) ]
3. Implementation Details
The implementation is split across three packages in the monorepo.
A. Cloud Relay Service (packages/cloud-relay)
A minimal Node.js WebSocket server.
- File:
src/server.ts - Logic:
- Maintains a
Map<SessionID, { host: WebSocket, client: WebSocket }>in memory. - Role-based Connection: Handlers for
?role=hostand?role=client.- Session creation rules: Sessions are created on
role=hostconnect.role=clientconnects to unknown sessions are rejected.
- Session creation rules: Sessions are created on
- Message Routing: Simply forwards messages:
Host -> Relay -> ClientClient -> Relay -> Host
- Resilience:
- Heartbeat: Pings all connections every 30s. Terminates if no pong in 60s.
- Rate Limiting:
- Max concurrent connections per IP.
- Max new connections per minute per IP.
- Global max sessions (counted as sessions with active host).
- Max WebSocket payload size.
- Per-connection and per-IP throughput caps (msgs/sec, bytes/sec).
- Health/metrics:
GET /healthreturns basic JSON health + counts.GET /metricsexposes basic Prometheus-style metrics.
- Maintains a
B. Desktop Agent Integration (packages/a2a-server)
The "Host" side of the connection.
- File:
src/http/relay.ts - Logic:
connectToRelay(relayUrl, requestHandler):- Creates a
RelaySessioncontaining:sessionId(UUIDv4)key(32-byte random AES key)pairingCode(6 digits)
- Connects to Relay via WebSocket (
role=host). - Logs the share URL only when
PRINT_RELAY_URL=true. - Always logs the pairing code locally.
- Reconnects with exponential backoff while reusing the same session.
- Creates a
- Message Handling:
- Receives encrypted frames.
- Decrypts using AES-256-GCM with AAD.
- Validates the versioned envelope and strict
seq. - Requires
HELLO/HELLO_ACKhandshake. - Requires
PAIRbefore executingRPC. - Executes
requestHandler.handle()on the decrypted RPC payload. - Encrypts responses in an envelope with AAD +
seq.
- File:
src/http/app.ts- Checks for
WEB_REMOTE_RELAY_URLenvironment variable. - If present, initiates the relay connection on startup alongside the local HTTP server.
- Checks for
C. Web Client Integration (packages/web-client)
The "Client" side running in the browser.
- File:
relay-client.js - Class:
RelayClient - Logic:
- Initialization: Parsed
sessionIdandkeyfrom the URL hash. - Leakage prevention: The web client strips the URL fragment from the address bar after parsing.
- Web Crypto API: Uses
window.crypto.subtlefor performant, native encryption/decryption in the browser. importKey(): Converts the base64 URL-safe key into aCryptoKeyobject.- Handshake: Sends
HELLOon connection open and requiresHELLO_ACKbefore sending RPC. - Encryption: AES-256-GCM with
additionalData(AAD) and strictseq. - Pairing: Sends
PAIRwith the user-entered pairing code.
- Initialization: Parsed
4. User Journey
-
Setup: User deploys the relay (or uses a public one) and sets the environment variable:
export WEB_REMOTE_RELAY_URL=wss://relay.terminai.org -
Start: User runs the agent:
terminai start -
Discovery: The CLI prints a secure Remote Access URL:
[Relay] Remote Access URL: https://terminai.org/remote#session=abc-123&key=xyz-secret&relay=wss%3A%2F%2Frelay.terminai.orgBy default, the host may not print the full URL unless:
export PRINT_RELAY_URL=trueThe host prints a pairing code locally (required):
[Relay] Pairing Code: 123456 (required for first connection) -
Connection:
- User opens the link on their phone/tablet.
- The Web Client loads (static HTML/JS from terminai.org).
- The Web Client detects the
#hashparams. - It initiates a WebSocket connection to
wss://relay.terminai.orgusing the Session ID. - The
RelayClientderives the encryption key ready for traffic.
-
Interaction:
- User completes pairing (enters the pairing code shown on the host).
- User types "List my files".
- Web Client encrypts an
RPCenvelope. - Relay forwards the blob.
- Agent decrypts, validates seq/AAD, executes the command, and encrypts the result.
- Web Client receives blob, decrypts, and displays the file listing.
5. Deployment Considerations
While the architecture is agnostic, the default deployment targets Google Cloud Run for the relay.
- Stateless: The relay holds sessions in memory. Cloud Run's stateless nature works well provided we use Session Affinity (sticky sessions) if scaling beyond 1 instance, or rely on the low cost of a single instance (handling 1000+ sessions easily).
- Scale:
- A single container (1 vCPU, 512MB RAM) can comfortably handle thousands of concurrent WebSocket tunnels.
- Cost is minimal (~$2-5/month for typical usage) due to efficient WebSocket handling.
- Health Checks:
- Exposes
/healthendpoint for load balancers. - Exposes
/metricsfor basic operational visibility.
- Exposes
6. Protocol Specification
For collaborators building new clients or debugging:
Encrypted Frame Format (Binary)
Every WebSocket message (binary) follows this strict layout:
| Segment | Size | Description | | :------------- | :------- | :--------------------------------------------- | | IV | 12 bytes | Initialization Vector (randomized per message) | | Auth Tag | 16 bytes | GCM Authentication Tag (integrity check) | | Ciphertext | N bytes | Encrypted JSON payload |
Decrypted Payload (JSON)
Once decrypted, the payload is a versioned envelope:
{
"v": 1,
"type": "HELLO" | "HELLO_ACK" | "PAIR" | "RPC" | "EVENT" | "ERROR",
"dir": "c2h" | "h2c",
"seq": 1,
"ts": 1735080000000,
"payload": {}
}
RPC.payload typically contains an A2A JSON-RPC request.
AEAD AAD String
Both client and host bind integrity to session + direction using an AAD string:
terminai-relay|v=1|session=<uuid>|dir=<c2h|h2c>
This AAD is supplied as:
- Node:
cipher.setAAD(...),decipher.setAAD(...) - Browser:
additionalDatain WebCrypto AES-GCM
{
"jsonrpc": "2.0",
"method": "agent.listDirectory",
"params": { "path": "/" },
"id": 1
}
Control Messages (Unencrypted)
The Relay may send unencrypted JSON control messages to both roles:
{
"type": "RELAY_STATUS",
"status":
| "HOST_CONNECTED"
| "HOST_DISCONNECTED"
| "CLIENT_CONNECTED"
| "CLIENT_DISCONNECTED"
}
7. Local Development Guide
To run the full E2E system locally for development:
-
Start the Relay (Port 8080):
npm run start --workspace @terminai/cloud-relay -
Start the Agent (Port 41242, pointing to local relay):
export WEB_REMOTE_RELAY_URL=ws://localhost:8080 npm run start --workspace @terminai/a2a-serverOptionally print the full share URL:
export PRINT_RELAY_URL=trueCopy the
[Relay] Remote Access URLprinted in the logs. -
Start the Web Client:
# Serve the static files npx http-server packages/web-client -p 8000 -
Connect: Open the copied URL in your browser, but replace
https://terminai.orgwithhttp://localhost:8000.
8. Future Improvements
- True streaming over relay: Emit
EVENTframes mirroring SSE events for token-by-token output. - QR Code: The CLI could render a QR code in the terminal for easier mobile scanning.
- Stronger session recovery: Make reconnect+rehydration robust even when only one side reconnects.
- Multi-instance routing: Redis pub/sub routing to remove sticky-session dependencies at scale.
- Expanded metrics: Rate-limit counters, close reasons, bytes in/out.