Skip to content
Merged
12 changes: 4 additions & 8 deletions docs/specs/relay.md
Original file line number Diff line number Diff line change
Expand Up @@ -1082,14 +1082,10 @@ bundle); `describePushTargets` in `lib/src/components/SettingsDialog.tsx`;
`lib/src/host/remote/enroll-offer.ts` for the offer's well-known per-platform
path, read by `readUsableOffer` in `lib/src/host/remote/service.ts`.

The `window.dormouseBurrow` console hook — the scripting seam — exposes the
seven enrollment commands: `enroll(password, label)`,
`enrollOffer(label)`, `beginHostedEnrollment(label)`,
`cancelHostedEnrollment`, `status`, `reconnect`, `clearEnrollment`. **Pairing confirmation is never here**: it is a
modal because it must interrupt, and because the digits it takes are read off a
phone ([remote-security-model.md](./remote-security-model.md) -> Pairing). The
one-time commands are not on the hook either; `status()` prints `serving`
beside `enrolled`.
**Never expose pairing confirmation or one-time commands on `window.dormouseBurrow`.**
Its enrollment scripting methods belong to `installBridgeMode`; `status()` includes
serving and enrollment state.
Source of truth: `installBridgeMode` in `lib/src/remote/burrow/activation.ts`.

`docs/stories/pairing.mdx` is a narrative Storybook page walking this section and
the pairing modal in sequence with the rest of the setup, rendering the real
Expand Down
26 changes: 10 additions & 16 deletions docs/specs/remote-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,7 @@ One protocol, two consumption depths: the **phone** (Dormouse Pocket) shipped, a

## v1 scope

**Scope: protocol-v1** — the shipped protocol, the smallest that lets a phone **sign in, pick a pane, see it live, and type into it**:

* Hello (version + viewer kind)
* `directory.watch`, snapshot-only (no deltas, no thumbnails), terminal entries only
* `surface.attach` / `surface.detach`, one attachment per session
* Terminal: attach-is-the-resize, live data, `terminal.write` / `terminal.resize`, last-attach-wins size authority
* One implicit grant: every authorized session — paired or one-time — has full input (selfhost is single-user), no layout operations
**Scope: protocol-v1** — the shipped protocol, the smallest that lets a phone **sign in, pick a pane, see it live, and type into it**. **Must restrict it to terminal listing, one attachment per session, and terminal input/resize; never layout operations.** The sections below own directory snapshots, attachment and size authority, and grants. Canonical method/event syntax lives in `remote-lib-common/src/remote/wire.ts`.

Everything else, browser-surface remoting included, is staged in [Future](#future).

Expand All @@ -35,7 +29,7 @@ Source of truth: `remote-lib-common/src/remote/wire.ts` (the fixed wire contract

**The Burrow runs in the process that owns the PTYs, never a webview** (`docs/specs/relay.md` → "Burrow side"). Within it, `RemoteApiSession` speaks this protocol and nothing else: surface ids, PTY ids, sizes, bytes.

**Every environment-specific answer sits behind `BurrowSurfaceProvider`** — `collectDirectory` / `watchDirectory`, `resolveSurface` returning a `SurfaceHandle`, `releaseSurface`, `writePty` / `resizePty` / `streamPty` — because *where* a named surface lives is a deployment fact, not a protocol concept. **The session imports no platform adapter, no store, and no `document`**, and both installations share the ask-backed half, so an attach cannot be answered differently in one burrow than the other.
**Must keep environment-specific answers behind `BurrowSurfaceProvider`.** **The session imports no platform adapter, no store, and no `document`**, and both installations share the ask-backed half, so an attach cannot be answered differently in one burrow than the other.

**`SurfaceHandle.ptyId` is a provider-local routing key**, not necessarily the PTY process's own id — the VS Code provider mints an opaque per-peer handle. (rationale)

Expand All @@ -45,15 +39,15 @@ Source of truth: `BurrowSurfaceProvider` in `lib/src/remote/burrow/burrow-surfac

## Terminology

A Surface is named on the wire by `surfaceId`; the picker lists Panes, so attaching to a Pane means attaching to its selected Surface. Remote-only vocabulary:
A Surface is named on the wire by `surfaceId`; the picker projects registered terminal Surfaces, as defined in "Directory (the phone's picker)". Remote-only vocabulary:

* **Viewer** — one connected Client session. Multiple viewers may coexist.

Source of truth: the surface model the wire shapes reuse — `dor/src/protocol.ts`, `dor/src/commands/types.ts`.

## Transport

**Every message below is JSON, carried as one length-prefixed application message on one authorized Noise session** that the WebSocket relay pipes without decoding, the Burrow multiplexing every session over its single relay socket (`docs/specs/relay.md` → "Routing", "E2E framing"). **Terminal data rides that same stream** — it is small and ordering matters; media channels arrive with browser surfaces ([Future](#future)). **The API and the security model are identical in selfhost and (future) SaaS modes**, where only account creation differs (`docs/specs/relay.md` → Future).
**Every message below is JSON, carried as one length-prefixed application message on one authorized Noise session** that the WebSocket relay pipes without decoding, the Burrow multiplexing every session over its single relay socket (`docs/specs/relay.md` → "Routing", "E2E framing"). **Terminal data rides that same stream** — it is small and ordering matters; media channels arrive with browser surfaces ([Future](#future)). **Must use the same API and session authorization for self-host and Hosted accounts.** Account login and Burrow enrollment belong to `docs/specs/relay.md` and `docs/specs/hosted.md`.

**A `RemoteApiSession` exists only for an authorized session.** Created at promotion — presence proof and ACL conjunction both passed ([remote-security-model.md](./remote-security-model.md) → Connection) — and disposed when the Client disconnects, when the Burrow reaps the session, and by any promotion that replaces it, so **a re-authorizing Client can never inherit the previous session's attachment**.

Expand All @@ -75,8 +69,8 @@ on.
**Every signal rides inside the session**, as one of four control messages
([relay.md](./relay.md) → E2E framing) on the established session over the relay
path: `direct-offer` (Client→Burrow, SDP), `direct-answer` (Burrow→Client, SDP),
`direct-decline` (Burrow→Client), `direct-switch` (either direction) — each
`{ v: 1, t }` with exact keys and no other field. **The Relay never sees an SDP,
`direct-decline` (Burrow→Client), `direct-switch` (either direction) — all guarded by `DirectSignalV1` in
`remote-lib-common/src/security/direct-path.ts`. **The Relay never sees an SDP,
a candidate, or that a direct path exists.** **An unknown control shape on an
established session is ignored, never a session failure**, so a peer without
this stack simply stays relayed.
Expand Down Expand Up @@ -203,7 +197,7 @@ by `BurrowRuntime.#promoteConnection` in

Requests are correlated by `requestId`, events by `subId` (`RemoteRequest`, `RemoteResponse`, `RemoteEventMsg`).

**A subscribing method (`directory.watch`, `surface.attach`) opens its stream under the request's own id** — `requestId` reused as the `subId` — so the Client installs its handler before sending and never races a snapshot or a first data frame. **The six methods and three events are named constants** (`REMOTE_METHODS`, `REMOTE_EVENTS`) dispatched by name, so a future event lands additively and an old client ignores what it does not know.
**A subscribing method (`directory.watch`, `surface.attach`) opens its stream under the request's own id** — `requestId` reused as the `subId` — so the Client installs its handler before sending and never races a snapshot or a first data frame. **Must dispatch by the canonical `REMOTE_METHODS` and `REMOTE_EVENTS` names** in `remote-lib-common/src/remote/wire.ts`, so a future event lands additively and an old client ignores what it does not know.

**Every peer-supplied `cols`/`rows` passes through `clampTerminalDimension`** — 1 … `MAX_TERMINAL_DIMENSION` (2000), falling back to the current size when absent or non-finite — on the Burrow, in the webview responder driving the real xterm, and in the Client adapter. The upper bound is the security-relevant half. (rationale)

Expand All @@ -215,7 +209,7 @@ Reserved: a `capabilities` field on the client hello (what the client can render

## Directory (the phone's picker)

`directory.watch` subscribes to a live, lightweight listing of every pane — enough to render the picker and know which pane wants attention, without attaching. `DirectoryEntry` / `DirectorySnapshot` carry the terminal-only payload: identity, derived title, focus, semantic state, PTY liveness, and the `ringing` / `hasTODO` badges. Nothing else — thumbnails are staged.
**Must list registered terminal Surfaces, excluding helper Sessions.** A Tool remains listed through its terminal even while showing its browser capability. `directory.watch` subscribes without attaching; `DirectoryEntry` / `DirectorySnapshot` in `remote-lib-common/src/remote/wire.ts` own the payload. Thumbnails are staged.

Reserved: **`paneRef` is set to the same value as `surfaceId`** and no Client
reads it — it becomes the Pane handle when `window.watch` lands ([Future](#future),
Expand All @@ -233,7 +227,7 @@ not render yet.

**A late answer — one for an ask that already settled — invalidates the directory rather than being dropped**: only the next collect repairs a snapshot missing what it names. Each burrow's ask bridge applies it (`docs/specs/standalone.md`, `docs/specs/vscode.md`).

**Browser and iframe surfaces are neither listed nor attachable** — they never enter the xterm registry the directory collects from, so `surface.attach` cannot resolve them either. ([Future](#future) stages browser remoting; iframes stay unsupported even there.)
**Never list or attach standalone browser or iframe Surfaces**: neither enters the xterm registry. ([Future](#future) stages browser remoting; iframes stay unsupported even there.)

**`alive` is real PTY-process liveness**, distinct from `exitCode` — the last finished command's shell-integration status: a pane may report `alive: true` with an `exitCode` set, or `alive: false` with none. **An exited pane stays listed at `alive: false`**, since Dormouse keeps it open until the user closes it, and the picker stops offering it — attaching would transfer nothing.

Expand Down Expand Up @@ -269,7 +263,7 @@ Source of truth: `resize` in `standalone/sidecar/pty-core.js`, shared by both ho

**Normal-screen history does not regenerate on resize** and is absent from the shipped protocol (see [Future](#future): in-flight replay, then semantic scrollback).

Payloads: `AttachParams`, `TerminalAttachResult`, `TerminalDataEvent`, `TerminalClosedEvent`, `TerminalWriteParams`, `TerminalResizeParams`. PTY bytes are base64url.
**Must encode PTY bytes as base64url.** Payload types live in `remote-lib-common/src/remote/wire.ts`.

`terminal.data` and `terminal.closed` are the whole v1 stream: **a viewer is not notified when another display takes size authority**, and semantic state (activity/cwd/title) reaches the client only through `directory.snapshot`. The burrow→client `terminal.resize` and `terminal.semantic` events are staged in [Future](#future) (item 5).

Expand Down
4 changes: 2 additions & 2 deletions docs/specs/remote-network.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

## Policy

**The policy is one record, `{ level, allowed, autoUpdate }`, held host-side** in the Burrow state store: `dormouse.burrow.network-policy` in VS Code's `globalState`, and the sidecar's own `network-policy.json`, 0600 beside `burrow.json`. **Never keep it in `burrow.json`**, which a build from before the policy rewrites without it, letting the default recompute. **Never take it from a Client, Relay, or Hosted response**; the webview reads it with `networkPolicy` and writes it with `setNetworkPolicy`, and the Burrow service is its only writer.
**The policy is one record, `{ level, allowed, autoUpdate }`, held host-side** in the Burrow state store: `dormouse.burrow.network-policy` in VS Code's `globalState`, and the sidecar's own `network-policy.json` beside `burrow.json`, protected by the credential-directory boundary (`docs/specs/security-remote.md` -> "Credentials at rest"). **Never keep it in `burrow.json`**, which a build from before the policy rewrites without it, letting the default recompute. **Never take it from a Client, Relay, or Hosted response**; the webview reads it with `networkPolicy` and writes it with `setNetworkPolicy`, and the Burrow service is its only writer.

| Level | Offered in | What Dormouse opens on its own |
|---|---|---|
Expand Down Expand Up @@ -40,7 +40,7 @@ Under `local` each runtime — a one-time link, or the persistent Burrow — is
- **The attempt's UDP socket binds the one allowed address when exactly one is present**: a single interface holds every address in the allowed networks, loopback and link-local aside, and exactly one in its preferred family, IPv4 over IPv6. **Otherwise it listens on every interface** (`docs/specs/remote-security-model.md` -> "Direct path"), and the level restricts the path, not the listener. Chosen per attempt (rationale).
- **Must strip every candidate outside the allowed networks from the Burrow's answer**, and send a default address outside them as `0.0.0.0`. **An answer left with no candidate refuses the attempt.**
- **Must strip the phone's offer the same way before the Burrow applies it**, a hostname, mDNS name, or unreadable candidate included, so its ICE agent sends no check and makes no lookup toward an address the level does not hold. **An offer left with no candidate is still answered**: the phone's checks reach the answer's candidates, and the pair forms peer-reflexive (rationale).
- **Must check the selected candidate pair on the Burrow before its channel reports open**, and again while it is open and `connected` — on every ICE or connection state change, and every `DIRECT_PATH_RECHECK_MS` (rationale): both ends parse as IP addresses — IPv4-mapped IPv6 matching its IPv4 range — each inside an allowed CIDR. **A hostname, an mDNS name, or a pair the stack will not report refuses**, and a frame arriving before the open is checked first; once open, a reading with no pair is left to the connection's own state (rationale).
- **Must check the selected candidate pair on the Burrow before its channel reports open**, and again while it is open and `connected` — on every ICE or connection state change, and every `DIRECT_PATH_RECHECK_MS` (1,000 ms; rationale): both ends parse as IP addresses — IPv4-mapped IPv6 matching its IPv4 range — each inside an allowed CIDR. **A hostname, an mDNS name, or a pair the stack will not report refuses**, and a frame arriving before the open is checked first; once open, a reading with no pair is left to the connection's own state (rationale).
- **Never trust SDP candidates, Hosted-observed addresses, or Client claims** as path evidence; only the Burrow's own ICE agent answers (rationale).
- **A refusal is a violation**: it ends the session `network-not-allowed` (`docs/specs/one-time.md` -> "Burrow runtime"), switched or not.
- **The check gates terminal traffic, not approval** (rationale): an off-network phone holding a link can reach the two-digit prompt and still receives no terminal byte.
Expand Down
8 changes: 4 additions & 4 deletions docs/specs/remote-network.rationale.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@ needs no policy to run.
## Settings → Network

**Why the list states only what is built (2026-09-30).** The list is what a
person reads to decide which level to trust, so a row for a connection no code
makes — the Hosted Relay — would promise traffic that never
happens, and a missing row would hide one that does. The prototype listed the
whole design; the real list drops each row until its stage ships.
person reads to decide which level to trust, so a row for unbuilt behavior
promises traffic that never happens, and a missing row hides one that does.
The 2026-09-30 prototype listed the whole design; `connectionsFor` now derives
rows from the shipped policy and runtime facts, including Hosted enrollment.

**Why the push row names its condition (2026-09-30).** Push is on by the
application default or by any Workspace's own override, and Workspaces in other
Expand Down
Loading
Loading