agent-transport-ws Specification
Purpose
WebSocket carrier for the Robota SDK. The ws dependency and carrier lifecycle are isolated in
this package; transport-neutral wire messages and session handling belong to
@robota-sdk/agent-transport so browser and monitor consumers can depend on those contracts
without pulling in ws, React, Ink, or Hono.
Non-goals / Boundaries
- Does not own the session bridge or the wire protocol (
TClientMessage/TServerMessage) — both were extracted to@robota-sdk/agent-transportso a non-WS transport can reuse them; this package imports rather than redefines them. - Does not own the channel contracts (
IPayloadChannel,IChannelDescriptor,IBinaryFrame) — owned byagent-interface-transport. - No other transport package may depend on this one; default transport-registry wiring that pre-registers the WS transport belongs to the CLI composition root, not the transport core.
- Consumers import execution-workspace contract types directly from
agent-interface-transportrather than through a pass-through re-export from this package.
Design decision: transport admission default
A prior state had two transports independently answering the same "auto-mint a credential, or stay
open?" question differently. The admission decision now has exactly one owner
(resolveAdmission in agent-transport), so there is one place to read and one place to change
it, rather than transports drifting apart on a shared question. Opting to stay open additionally
requires a written reason, because "no credential" and "nobody thought about it" must remain
distinguishable outcomes.
Invariants and guarantees
- Payload-agnostic carrier. The transport routes by WebSocket frame opcode: the text-agent
protocol is one profile riding on the transport, not the transport itself. A binary frame for an
unregistered channel, an undeclared event name, or a malformed envelope always gets an explicit
protocol_errorreply — never a silent drop. An application adds a new event type by declaring a channel, never by forking the agent wire protocol. - Protocol-scoped session attachment. Both WebSocket adapters require only the session roles the shared protocol bridge uses; a host may attach a full interactive session, but the carrier itself does not depend on unrelated runtime capabilities.
- Authentication ordering is fixed. Channel sinks attach only after the token check, so an unauthenticated socket never receives channel traffic even if a channel was registered before the connection existed.
- Authenticated by default, constant-time comparison, closed before data. Every connection must
present the token or is closed before any session data is emitted; comparison is constant-time. A
host may supply the token; otherwise a random per-launch token is minted and exposed only for delivery
to the co-located client. Running without auth needs the explicit, discouraged
openopt-out, and combiningopenwith a token is rejected as a contradiction. - Lifecycle rejects out-of-order use. Starting before attaching, or a repeated active start, is rejected rather than silently reinitializing; a repeated stop is safe but clears the session so restart requires reattaching.
- Outbound delivery failures are carrier failures, not session-operation failures, and this applies uniformly to every outbound frame, not only the session-event fan-out. The socket sink is private; the only way onto the wire is through one delivery path, so nothing can bypass its failure handling. A non-open socket is treated as a failed send, and both synchronous and asynchronous send errors route through one idempotent cleanup/close path — latched, so a burst of post-close frames triggers that cleanup only once.
- The host controls capability wiring and surface identity, not the client: usage-reporting capabilities are injected by the host at construction, and the driver identity persisted with a session comes from the host's trusted launch path rather than a client-claimed value.