Shell WebSocket Interaction
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:
- Call
createShellAccessTokenV1 - Capture
gatewayUrl,sessionId, andaccessTokenfrom that response, and obtain a fresh userjwtseparately from the browser’s own Cognito session — the create-token response does not contain one - 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:
sessionIdmust match the created shell sessionaccessTokenmust be the short-lived single-use token returned by the REST create-token call — the opaque, platform-minted one, used to authenticate this browser to the gatewayjwtmust be non-empty, and must be the user’s Cognito JWT: the credential the browser already holds from its Cognito session. It is not the value ofaccessToken, and not any other platform-minted shell token —createShellAccessTokenV1returns nojwtfield at all
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:
- storage synchronization notices
- pod startup status lines
- provisioning delays
- shell credential preparation
- storage and network quota banners
- the final shell-ready banner
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:
- The gateway uses the same
datamessage type for both stdout and stderr. - Status banners and quota notices are also delivered as
data. - If the browser sends a non-JSON frame after connection, the gateway treats it as raw terminal input.
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:
- a hard deadline enforced by the pod lifetime
- a soft inactivity deadline enforced by the gateway
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:
extensionTokentokenEIDjwt— note the direction reverses here: on connect the browser supplies the user JWT, whereas this extension response returns a fresh one. It plays the same cargo role, and the gateway writes it to/run/credentials/jwtin the pod after consuming the extension tokenhardDeadlinesoftDeadlineexpiresAt
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
- Send the initial connect payload immediately after the WebSocket opens.
- Render
dataframes directly into the terminal emulator. - Treat startup notices and quota notices as ordinary terminal output.
- Send
resizeframes whenever terminal dimensions change. - Use the REST extension endpoint plus WebSocket
extendframes to keep the session alive. - Update local countdown UI from
extend_ackandtimeout_warning. - Stop input and treat the session as ended after
timeout,session_ended, orpod_terminated.