Expand description
The client half of microvm shell: a real PTY carried over the endpoint’s shell
WebSocket. Issue #69, Option B — a thin client over the platform’s session, not a
line-mode wrapper over exec.
Everything protocol-shaped in this module is measured, not inferred
(docs/PLATFORM.md, “The shell endpoint is a real PTY over a WebSocket”, 2026-08-15):
- The endpoint is the VM’s ordinary endpoint URL, opened as a WebSocket with two
subprotocols — the marker and the authentication value carrying the shell JWE.
There is no port subprotocol: the shell is not a port, and offering
lambda-microvms.port.<n>neither helps nor hurts, so this client offers exactly what suffices. - The server sends one text frame on connect
(
{"type":"session_init","session_id":"<uuid>"}), then binary frames carry raw terminal bytes both ways. Client input is raw keystrokes:0x03raises SIGINT in the guest,"exit\n"ends the shell. - A resize is a JSON control frame,
{"type":"resize","cols":120,"rows":40}. Before the first one the guest’sstty sizereports0 0, which is whyrun_shellsends the caller’s initial size unprompted. - A clean exit is close code 1000, reason
shell exited.
§Sharp edge 1: a malformed control frame becomes keystrokes, not an error
The platform does not reject an unrecognized control frame — it injects it into the
shell as literal input. Measured: {"type":"window_size",...} produced
bash: type:window_sizestty: command not found in the session. A typo in a control
message corrupts the terminal instead of erroring, so every outbound text frame
goes through validate_control_frame before it is sent, and the only constructor
for one is resize_frame. A frame the validator does not recognize is refused on
this side of the wire, where the failure has a message instead of a corrupted prompt.
§Sharp edge 2: there is no exit-status channel, and this module does not invent one
The shell’s exit is a WebSocket close, full stop. There are no exec ids, no separated
stdout/stderr, and no exit codes — which is precisely why this is its own surface and
not a method on the exec path (TRAP-11). ShellEnd reports how the transport
ended and nothing about the last command’s status; a caller who needs a command’s
status must ask the shell itself (echo $?) and read the byte stream. Anything else
would be a fabricated exit code.
§The token gates the handshake, not the session
The shell JWE comes from CreateMicrovmShellAuthToken
(crate::control::ControlPlane::mint_shell_auth_token) and is spent once, on the
upgrade. An established session outlives the token’s expiry, so there is no refresh
loop here — one mint, one handshake, one PTY.
Structs§
- Shell
Outcome - What
run_shellhands back: the session’s identity and how it ended.
Enums§
- Shell
End - How a shell session ended. A value rather than an error, because every variant is a session that did its transport job — see the module docs on the missing exit-status channel before adding anything status-shaped here.
Constants§
- SHELL_
CHUNK_ BYTES - Bytes read from the local input per relayed frame.
- SHELL_
EXITED_ REASON - The close reason the platform sends when the shell process exits.
Functions§
- resize_
frame - A resize control frame, validated before it leaves this function.
- run_
shell - Opens the shell and relays raw terminal bytes until one side ends the session.
- shell_
subprotocols - The two subprotocols a shell handshake offers: the marker and the credential.
- shell_
url - The
wss://URL for a VM’s shell: the endpoint itself, no path. - validate_
control_ frame - Refuses any outbound text frame the platform would not recognize as a control frame.