Local Browser Broker
The broker is the loopback relay between the one VS Code extension host and the browser views of a workspace session. Each session uses a launch token and a one-time bootstrap credential. The standalone plugin service binds 127.0.0.1 only, requires that token, and never binds 0.0.0.0. start.sh and server_v2.py use the same loopback default and refuse a public bind unless TENSORVIZ_LOCAL_TOKEN is set. AUTH_DISABLED does not grant that bind. See service bind. This page documents the HTTP and WebSocket surface. Implementation: backend/services/ide/local_browser_broker.py (relay) and backend/api/routers/local_browser.py (HTTP routes). backend/server_v2.py mounts the router outside the get_current_user group. backend/standalone_server.py mounts the same router for the plugin, with source_proposals=False. The browser cannot hold the launch token, so the router carries its own session-scoped credential model.
Endpoints
| Route | Caller | Auth | Purpose |
|---|---|---|---|
POST /local/sessions | extension | per-launch token (see below) | mint a workspace session, TTL, credentials |
POST /local/bootstrap | browser page | URL-fragment credential in the JSON body, once | trade the fragment for the session cookie |
WS /local/ws | both roles | the session cookie plus exact launch Origin | relay typed protocol messages |
Authentication and bootstrap
- The backend is only reachable in this model when launched with
TENSORVIZ_LOCAL_TOKEN. Without that variablePOST /local/sessionsanswers 404, so no sessions can exist and bootstrap plus the socket are unreachable in practice — the same inert-by-default property as the IDE middleware. - The extension proves itself with the token in the
x-tensorviz-tokenheader, compared withsecrets.compare_digest, checked at request time. The browser never sees this token. - The mint response returns
workspaceSessionId,launchOrigin,expiresAt,protocolVersion, the declared browser capabilities,viewCredential(meant only for the URL fragment), andhostCookieKey(extension-side only; it never enters a browser flow, and bootstrap refuses it). launchOriginis normalized at mint (case-folded scheme/host, no credentials, path, query or fragment) so later checks are exact string equality against the header.- The page posts the fragment credential once, same-origin, to
POST /local/bootstrap. Refusal order is session lookup, origin, expiry, then credential digest. The successful response sets the session cookie and returns session info only — never a credential. - Browser view cookies use
tensorviz_local_browser_<origin-hash>so loopback windows with different forwarded ports do not overwrite one another. The hash is the first 16 hexadecimal characters of SHA-256 of the exact launch origin. Host sockets keep the fixedtensorviz_local_browsername in their explicit cookie header. Both useHttpOnly; SameSite=Strict; Path=/local, session-scoped, andSecureonly when the launch origin is HTTPS. Its value is an opaque per-role cookie key, not the fragment credential, so after bootstrap the fragment secret exists nowhere the browser can reach. - Capabilities are fixed to read-only:
readOnly,canNavigateSourcetrue;canWriteSource,canRunAgent,canEditGraphfalse. The optionalcanProposeSourceEdits: trueenables typed proposals for native host review; it grants no direct source or graph write authority.
Topology
- One
hostconnection and manyviewconnections per session. A second host handshake is refused withHOST_ALREADY_CONNECTED; existing views are unaffected. - Direction is enforced per role: host state (
activeTarget,projection,syncState,staleState,focusNode,ack,error) broadcasts to every view; view traffic (snapshotRequest,revealNode,ack,error) goes only to the host and is never echoed to other views. Broker-owned types (connected,connectionState, handshakes) cannot be injected. - A view connecting before the host receives
connectionState: disconnected; views are toldconnected/disconnectedas the host arrives and leaves. A host disconnect clears the session's host, notifies every view, and restarts the ordering epoch when a new host registers. - A connection whose registration was revoked mid-flight (a failed send marks its peer dead) stops relaying: the relay loop exits instead of letting a ghost peer feed state after the broker declared it gone.
- Host teardown and view teardown are idempotent; a view leaving also discards its pending snapshot requests.
Ordering
- Only host-originated ordered state carries
hostSequence, and it must strictly increase within one host epoch. A repeat or rewind is refused to the host withINVALID_MESSAGEand dropped — never relayed — so no view ever sees the same sequence twice. A new host epoch restarts ordering from scratch.
Snapshot routing and reconnect
- A view refreshes by reusing its cookie on a brand-new socket and sending
snapshotRequestwith its last acceptedlastHostSequence. - Requests are routed to the host; the host's
snapshotis delivered to the oldest live asker, one request per answer. A view asking twice keeps only its newest request. A host snapshot with no pending request is refused withINVALID_MESSAGE. - Requests sent while no host is connected are queued and flushed, oldest first, when a host registers; asks from views that have since left are skipped.
Source proposal and validation routing
sourceEditRequest/sourceEditCancel and validateRequest/validateCancel travel view → host. The corresponding result returns only to the originating view, correlated by request id and target, without a broadcast sequence. Reply kinds cannot cross: a validation result cannot settle a source proposal.
A request without a host or with an in-flight duplicate id is refused. Only its requesting view can cancel it. View disconnect synthesizes cancellation; host disconnect clears pending requests and the page reports loss of service. Late replies for cancelled requests are ignored. The broker relays typed intent and results, never workspace writes. See source editing.
Capture inspection and portable export use the same private correlation/ownership rules. Their reply kinds cannot settle each other or another request type. Detached captures are bounded to 16 MiB UTF-8 JSON, never broadcast and never stored in reconnect snapshots. A disconnected view cancels its export preparation.
Publish (graphPublishRequest/graphPublishCancel) follows the same rules. The request carries the graph file gzipped in base64, bounded by the website's 10 MB file limit; the broker remembers who asked, not the file. The host answers the asking view with graphPublishState progress (signing_in with the website's http(s) connect page, then uploading) and one terminal graphPublishResult: published with the graph's page and Share page, failed, or cancelled. The view opens those pages only on the member's click. See Add graphs to tensorViz.
Expiry
- TTL defaults to 8 hours, bounded to [1s, 24h] at mint. When it fires, every session socket is closed with code 4402 /
CREDENTIAL_EXPIREDand every credential — view keys and the host key — is revoked in one sweep. - The session then remains an inert tombstone for a bounded retention window (
LOCAL_BROWSER_TOMBSTONE_RETENTION_SECONDS, default 1 hour) so a straggling bootstrap is answered 410CREDENTIAL_EXPIRED— actionable — rather than theINVALID_CREDENTIALSan unknown session gets. The reaper deletes the tombstone afterwards, so minted sessions cannot accumulate over the process's life.
Refusals, close codes, and limits
- Transport-level failures (missing/wrong cookie, expired session, wrong origin) refuse before
accept()with the application close code; there is no authenticated channel to carry an error. Protocol-level failures after the handshake are answered with the typederrormessage; single bad frames do not kill the connection, while handshake failures also close with the paired code. - Close codes pair 1:1 with contract error codes: 4401
INVALID_CREDENTIALS, 4402CREDENTIAL_EXPIRED, 4403INVALID_ORIGIN, 4404PROTOCOL_VERSION_MISMATCH, 4405SESSION_MISMATCH, 4406INVALID_MESSAGE, 4407UNKNOWN_MESSAGE, 4408HOST_ALREADY_CONNECTED. - Frames are capped at 16 MiB before JSON parsing; binary frames are refused as
INVALID_MESSAGE. - Every inbound message is validated against the frozen contract once: wrong protocol version, cross-session envelope, malformed payload, and unknown types are refused in that order.
- No secret — launch token, view credential, or cookie key — ever appears in request logs, error bodies, protocol messages, or workspace files; refusal messages are static text that never quotes request content.
Verification
Use the backend environment from developer setup:
./venv/bin/python -m pytest backend/tests/api/test_local_browser_broker.py backend/tests/api/test_local_browser_protocol.py -qCoverage includes authentication, origin/session isolation, direction and ordering, request ownership, reconnect, cancellation, expiry, redaction and frame limits.
Candidate review adds private nonterminal sourceEditReview updates on the existing source-request route, preserving its Apply receipt. A supplied source viewId and every candidateRequest.viewId must match the actual socket. Candidate replies are host-only and directed to one connected view, never broadcast. The host owns retained proposal/candidate authorization after the source request completes and uses existing broker viewState disconnection notifications for revocation. test_local_browser_candidates.py covers forged identities/replies, private progress, historical reads and late replies after disconnect.