Shell WebSocket Interaction

Back to Shell Access

This page documents the required browser-to-gateway WebSocket protocol for shell access. It describes the actual message contract used by the shell gateway.

Before You Connect

Before opening the WebSocket, the browser must:

  1. Call createShellAccessTokenV1
  2. Capture gatewayUrl, sessionId, and accessToken from that response, and obtain a fresh user jwt separately from the browser’s own Cognito session — the create-token response does not contain one
  3. Open ${gatewayUrl}/ws

The initial application message must be sent promptly. The gateway expects the connect payload within roughly 10 seconds after the socket is accepted.

Initial Connect Payload

The first WebSocket message must be JSON with all three required fields:

{
  "sessionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "accessToken": "base64url-single-use-token",
  "jwt": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Rules:

The two credentials are different, and both are required

A shell session involves two credentials with different origins, forms and purposes. Sending one where the other belongs is a known client mistake, so the distinction is spelled out here rather than left to the field names:

  accessToken jwt
origin minted by the platform by createShellAccessTokenV1 issued by Cognito to the browser, as part of the user’s sign-in session
form opaque — Base64URL-encoded cryptographically random bytes, with no readable structure a decodable JWT, carrying claims including exp
lifetime short-lived and single-use — the gateway consumes it on connect the Cognito session’s own lifetime, typically far longer
role authenticates this browser to the gateway cargo — the gateway does not authenticate with it; it copies it into the shell pod
where it ends up consumed and discarded written into the pod at /run/credentials/jwt, so merlin inside the shell can call the API as the user

Because jwt is cargo rather than an authenticator, the gateway does not verify it as a credential of its own — which is exactly why supplying the wrong value fails later, inside the pod, rather than at connect time.

Minimum remaining lifetime

jwt carries an exp, and the platform publishes a minimum remaining lifetime a client must guarantee before starting or extending a shell session: shellCredentialMinimumRemainingLifeSeconds, read from GET /v2/system/configuration.

Read that value rather than hardcoding a threshold. A value of 0 means the gate is disabled and the client should apply no proactive check; 0 is what the platform currently ships, so the gate is a no-op today. When it is enabled, a connect payload whose jwt has less than that much life left is rejected — and so is one that is not a decodable token at all, which is what a client that put the opaque accessToken in this field would be sending.

The gateway consumes the access token, writes startup credential files into the shell pod, waits for shell readiness, and only then begins the interactive relay.

What The Browser Should Expect During Startup

During startup, the gateway emits terminal-oriented status text as ordinary data messages. This includes:

These are not separate control-message types. They arrive through the same terminal data channel as shell output and may contain ANSI escape sequences.

Runtime Message Types

After the initial connect payload, the browser and gateway communicate using JSON messages with a type field.

data

Used for terminal stdin and stdout/stderr relay.

Browser to gateway:

{
  "type": "data",
  "data": "ls -la\r"
}

Gateway to browser:

{
  "type": "data",
  "data": "total 8\r\n-rw-r--r-- 1 shell shell 0 notes.txt\r\n"
}

Notes:

resize

Sent by the browser when the terminal dimensions change.

{
  "type": "resize",
  "resize": {
    "cols": 120,
    "rows": 36
  }
}

ping and pong

The browser may send:

{
  "type": "ping"
}

The gateway responds with:

{
  "type": "pong"
}

Session Deadline Messages

Shell sessions use:

Initial extend_ack

Once the shell is ready, the gateway sends an initial deadline message:

{
  "type": "extend_ack",
  "extendAck": {
    "hardDeadline": "2026-05-25T20:00:00Z",
    "softDeadline": "2026-05-25T12:05:00Z",
    "extended": false
  }
}

extended: false here does not indicate failure. It means this frame is the initial deadline publication rather than a successful extension event.

Timeout Warning

Shortly before inactivity timeout, the gateway warns the browser:

{
  "type": "timeout_warning",
  "timeoutWarning": {
    "secondsRemaining": 30,
    "softDeadline": "2026-05-25T12:05:00Z"
  }
}

Timeout

If the soft deadline expires, the gateway sends:

{
  "type": "timeout",
  "data": "Session timed out due to inactivity"
}

The gateway then cancels the shell session and closes the underlying execution flow.

Extension Flow

The browser extends inactivity timeouts in two steps.

Step 1: Create an Extension Token

Call extendShellAccessTokenV1.

That REST call returns:

Step 2: Send the Extension Token Through WebSocket

{
  "type": "extend",
  "extend": {
    "extensionToken": "base64url-extension-token",
    "tokenEID": "b1b2b3b4-c5c6-7890-abcd-ef1234567890"
  }
}

tokenEID is optional for ordinary extensions, but browsers should send it when available so the gateway can recover the authoritative replay state if a retry hits an already-consumed extension token.

Success Response

On success, the gateway consumes the extension token, updates the soft deadline, queues the fresh JWT for pod delivery, and sends:

{
  "type": "extend_ack",
  "extendAck": {
    "hardDeadline": "2026-05-25T20:00:00Z",
    "softDeadline": "2026-05-25T12:07:00Z",
    "extended": true
  }
}

The gateway may also emit additional quota notices as terminal data messages after a successful extension.

Failed Extension

If token consumption fails, the gateway still sends extend_ack, but with:

{
  "type": "extend_ack",
  "extendAck": {
    "hardDeadline": "2026-05-25T20:00:00Z",
    "softDeadline": "2026-05-25T12:05:00Z",
    "extended": false
  }
}

In this case, the previous deadlines remain authoritative.

Session End Notifications

Clean Shell Exit

When the shell process exits cleanly and the client is still connected, the gateway sends:

{
  "type": "session_ended",
  "sessionEnded": {
    "code": 1000,
    "reason": "session ended"
  }
}

The gateway then closes the WebSocket with normal closure code 1000.

Unexpected Pod Termination

If the shell pod terminates unexpectedly, the gateway sends:

{
  "type": "pod_terminated",
  "podTerminated": {
    "reason": "OOMKilled",
    "message": "Container terminated unexpectedly"
  }
}

Clients should treat this as terminal and close the local terminal UI once the socket closes.

Error Signaling

Startup or authentication failures are sent as terminal-style data messages rather than as structured error control frames. The browser should therefore render the final text line to the terminal and expect the socket to close shortly afterward.

Example shape:

{
  "type": "data",
  "data": "\r\n\u001b[31mError: Authentication failed\u001b[0m\r\n"
}

Browser Implementation Checklist