Extension transport layer
apps/extension/src/transport/ is the single, swappable seam between the
browser-skill extension and whatever channel it uses to talk to the local
daemon. v0.1 ships exactly one implementation —
ws-transport.ts — which speaks the JSON-line wire
format defined in crates/bsk-protocol/schema/*.json over a plain WebSocket.
How to fork the transport
If you want to swap the WebSocket for native messaging, chrome.runtime
messaging, a shared MessageChannel, etc., implement the
Transport interface and instantiate your class in
apps/extension/src/entrypoints/background.ts instead of WSTransport.
The contract is:
import type { Transport } from "@/transport/transport";
import type { ProtocolFrame, ConnectionState } from "@/transport/types";
class MyTransport implements Transport {
readonly state: ConnectionState;
connect(): Promise<void>;
disconnect(): Promise<void>;
send(msg: ProtocolFrame): void;
onMessage(handler: (msg: ProtocolFrame) => void): { dispose(): void };
onConnectionStateChange(handler: (s: ConnectionState) => void): { dispose(): void };
}
Protocol-frame contract
A ProtocolFrame is one of three JSON shapes (mirrors bsk-protocol::Frame):
// Request — both peers may send
{ "id": "<rpc-id>", "method": "tool.snapshot", "params": { "session_id": "ab12" } }
// Response — one of result | error, never both
{ "id": "<rpc-id>", "result": { ... } }
{ "id": "<rpc-id>", "error": { "code": "not_found", "message": "session ab12 unknown" } }
// Event — no id, no response expected
{ "event": "session.window_closed", "payload": { "session_id": "ab12" } }
The transport is purely a byte pipe. It must not interpret RPC method
names, route by id, or re-order frames. Higher-level concerns
(handshake, dispatcher, ref-store) all live in tools/ and
session-manager/. The transport's only obligations are:
- Frame-oriented delivery. One
send()call ⇒ exactly one frame on the wire. Inbound bytes are split into discrete frames before being handed to message handlers. - Best-effort reconnect. When the underlying channel breaks
unexpectedly, the transport reopens it with backoff and surfaces
connecting/disconnectedviaonConnectionStateChange.WSTransportuses 1 s → 2 s → 4 s → 8 s → 16 s → 30 s (capped), resetting on a successful reconnect. - State observability.
statemust be one ofdisconnected,connecting,connected, orversion_skew. Only the handshake logic should setversion_skewwhenprotocol_versionstrings differ but majors match; app semver is not compared. The bare transport stays atconnectedonce the socket is open. Handshake may still include deprecatedmin_compatible_peer("0.0.0") alongsidemin_compatible_protocolfor backward compatibility. - No throwing on malformed frames. Drop non-JSON inbound traffic silently; surface decode failures through logs only.
Handshake responsibility
A fresh Transport instance is not authenticated. The first frame
sent over the wire must be system.handshake. Forks are expected to
preserve that contract — see lib/instance-id.ts and
transport/handshake.ts (added in M4.3) for the helper that owns the
extension side.
Testing forks
__tests__/ws-transport.test.ts is the reference test suite. It uses a
hand-rolled FakeSocket so a new transport can copy its structure
verbatim and adapt the inner channel. See the suite for: connect/
disconnect, send/onMessage, malformed-frame tolerance, and exponential-
backoff reconnect verification.