A session end to end, and proving it locally
The agent interprets exactly three control messages and one byte stream. This unit walks a session from open to close, then reproduces the whole relay on your own machine with no Diyar broker involved.
The wire protocol
Every hop - operator to broker, broker to agent - carries the same two message kinds, symmetric on both sides of the broker:
- WS Text = control JSON:
{"t":"open"},{"t":"resize","cols":100,"rows":30},{"t":"close"}. - WS Binary = raw terminal bytes. Operator→agent is what you type (PTY stdin); agent→operator is what the shell prints (PTY stdout).
The broker itself only ever pipes frames between the two sides; it does not interpret them. The agent is what owns the PTY and reacts to control, which is what makes the broker replaceable - massar-broker-dev in this unit and the real Diyar broker both speak the exact same protocol to the agent.
What happens at each stage
Open. open_session allocates a fresh PTY (native_pty_system().openpty), sized to whatever cols/rows came with the request or defaulting to 80x24, then spawns the platform's own default shell on it:
fn default_shell() -> String {
if cfg!(windows) {
std::env::var("COMSPEC").unwrap_or_else(|_| "powershell.exe".into())
} else {
std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".into())
}
}The shell is started with TERM=xterm-256color and, when HOME/USERPROFILE is available, with that as its working directory. A dedicated OS thread blocks on reading the PTY's master side and forwards every chunk it reads to the broker as a Binary frame; a second thread drains a channel fed by incoming Binary frames and writes them to the PTY's stdin. Both are ordinary blocking reads and writes on their own threads, deliberately kept off the async runtime.
Resize. A resize control message with cols/rows calls the PTY master's own resize directly - no restart of the shell, no new session.
Close. An operator-initiated close kills the child process outright. Independently, when the shell itself exits - the user typed exit, or the process died - the PTY reader thread hits EOF, and the agent sends {"t":"exit"} back over the control channel so the operator side learns the session ended instead of just going silent.
There is nothing outside of this lifecycle: the agent holds no standing shell. README is explicit about it: "a PTY exists only for the duration of an explicitly-opened, TTL-bounded session." It is torn down on an operator close, on the shell exiting on its own, or on the broker connection dropping - whichever comes first.
Proving it locally
The full path - operator client, broker, agent, a real PTY, a real shell - runs on one machine with no Diyar account, no network beyond loopback, using the dev-tools feature:
cargo build --features dev-tools
massar-broker-dev & # listens 127.0.0.1:8088
MASSAR_BROKER_URL=ws://127.0.0.1:8088/agent MASSAR_DEVICE_ID=dev MASSAR_AGENT_TOKEN=dev massar-agent &
MASSAR_DEVICE_ID=dev massar-op-test # PASS = sentinel round-tripped through the PTYmassar-broker-devis an axum server exposingGET /agent(wheremassar-agentdials in and registers itself bydevice_id) andGET /op(where an operator attaches; the broker tells the registered agent toopen, then pipes frames both ways verbatim). Its own doc comment is explicit about what it is not: "NOT for production (no Vent RBAC, no audit, no session recording, no TLS) - those land when this moves into diyar."massar-agenthere is the identical binary you would install on a real device - it does not need thedev-toolsfeature to build or run; only the dev broker and the test client require it.massar-op-testattaches to/opthe same way a browser-based terminal would: it sends aresize, typesecho MASSAR_PROOF_OK\rfollowed byexit\r, and requires the sentinelMASSAR_PROOF_OKto appear twice in the transcript - once as the command it typed, once as the shell's own echo of the result - before printingPASSand exiting0. Fewer than two hits is aFAIL, exit1.
TIP
On Windows, ConPTY sends a cursor-position query (ESC[6n, a Device Status Report) the moment the shell starts, and stalls rendering until it gets an answer. A real terminal - the xterm.js web terminal on the operator side - answers this automatically. massar-op-test does the same thing (replying ESC[1;1R), which is why the automated relay test passes on Windows as well as Linux rather than only on one of them.
What you'll see if it's working
Both massar-broker-dev and massar-agent log at info level by default (no RUST_LOG needed). Across the three processes you should see, in order: the broker logging agent registered as massar-agent dials in, the agent logging connected to console broker and then session opened once massar-op-test attaches, the broker logging operator attached, and finally massar-op-test printing the relayed transcript followed by PASS: sentinel round-tripped through the relayed PTY (2x).
| Control message | Direction | Carries |
|---|---|---|
open | broker → agent | nothing (agent defaults to 80×24) |
resize | operator → agent | cols, rows |
close | operator → agent | nothing |
exit | agent → operator | nothing (shell exited on its own) |
This is the Day-0 proof that actually runs end to end in this repository: the local massar-broker-dev / massar-op-test harness, not a production Diyar deployment. It's the automated cross-platform relay test the project keeps green (ADR-0002), and it's what you can reproduce yourself before ever pointing a real agent at a real broker URL.