Skip to main content

Module shell

Module shell 

Source
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: 0x03 raises 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’s stty size reports 0 0, which is why run_shell sends 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 (microvms_app::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§

ShellOutcome
What run_shell hands back: the session’s identity and how it ended.

Enums§

ShellEnd
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.