Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4d01916
Hosted: one entitlement seam in place of isAdmin
nedtwigg Oct 4, 2026
8186464
Sign in to Dormouse Hosted from the desktop: managed voice for members
nedtwigg Oct 4, 2026
f141646
Specs and stories for Hosted sign-in and member voice
nedtwigg Oct 4, 2026
f797dc3
Simplify: share the sign-out confirm and sign-in-again, one error slot
nedtwigg Oct 4, 2026
1b639b1
Code review: stale-token answers change nothing; a 401 or a lone toke…
nedtwigg Oct 4, 2026
6d0fc3c
Merge main into hosted-signin
nedtwigg Oct 17, 2026
f7e5638
No device limit for Hosted members
nedtwigg Oct 17, 2026
74ba731
Remote control: enrollment hint says "Enroll this Dormouse" again
nedtwigg Oct 18, 2026
290cf58
Desktop copy says Enroll and Disconnect, not Sign in and Sign out
nedtwigg Oct 19, 2026
1119091
Hosted page: machines enroll, not sign in
nedtwigg Oct 19, 2026
2d36631
Merge main into hosted-signin
nedtwigg Oct 19, 2026
4216837
Point the disclosure at alert.md, and drop built enrollment from Futu…
nedtwigg Oct 19, 2026
8b0a395
A computer's voice token is revoked only by removing the computer
nedtwigg Oct 19, 2026
65f0308
Separate managed voice from remote-control enrollment and fail closed…
nedtwigg Oct 19, 2026
ee11fa3
Address independent voice enrollment review feedback
nedtwigg Oct 19, 2026
7222ab1
Merge main and finish voice token review cleanup
nedtwigg Oct 19, 2026
f51d1ac
Refresh outbound guard canary context
nedtwigg Oct 19, 2026
1ccd0d9
Merge remote-tracking branch 'origin/main' into hosted-signin
nedtwigg Oct 19, 2026
2df99d3
Drop a blank line left in the voice token row
nedtwigg Oct 19, 2026
9edc3f8
Type the voice poll's answer in the shared wire contract
nedtwigg Oct 19, 2026
53d2259
Never name the voice token in desktop copy
nedtwigg Oct 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/audit/canaries/burrow-fetch-global-fallback.patch
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ index de4434a62..0ac7ab7df 100644
- },
'lib/src/remote/burrow/enrollment.ts': {
class: 'guarded:burrow-service',
reason: 'The enrollment exchange calls only the fetch parameter, which the service passes as its guarded fetch.',
reason: 'The enrollment exchange calls only its fetch parameter: the Burrow service or managed-voice host supplies a network-policy guard.',
@@ -159,6 +155,10 @@ const OUTBOUND_SITES = {
reason: 'runUpdateCheck calls check() only when the policy is not nothing and autoUpdate is on; Check now is a click; openUrl opens the changelog or an issue search on a click.',
},
Expand Down
2 changes: 1 addition & 1 deletion .github/audit/canaries/cookie-gate-read-origin.patch
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ index 8c7c5aa89..dc5d94aab 100644
+ * `Sec-Fetch-Site: same-origin` or `same-site` — the relay and voice pages
+ * share the login cookie — and an `Origin`, if presented, is exactly
+ * `origin`. Another site's page, a typed URL, and a request without Fetch
* Metadata all fail. {@link cookieAdmin}'s in-process login lookup vouches
* Metadata all fail. {@link cookieEntitled}'s in-process login lookup vouches
* for a request its gate admitted by carrying the header itself.
*/
export function ownPageRead(request: Request, origin: string): boolean {
Expand Down
13 changes: 8 additions & 5 deletions docs/specs/alert.md
Original file line number Diff line number Diff line change
Expand Up @@ -406,10 +406,13 @@ Every rule above holds for both engines.
- **A host may speak only at its build's voice origin; a self-host build's has none** and makes no request (`docs/specs/relay.md` → "Relay origin"), nor does any host under the network policy's `nothing` (`docs/specs/remote-network.md` → "Policy").
- **Must fall back to Web Speech for the same utterance, inside the same attempt, on any failure before managed audio starts** — `unconfigured`, offline, non-2xx, host timeout, undecodable or refused playback. **Never play both**: audio that started and then failed ends the attempt instead (rationale). Nothing is retried.
- `speaking` / `spoken` follow the audio element's `playing` / `ended`. **Cut-off and teardown must stop the audio**; a request still in flight runs out in the host and its answer is ignored.
- **Never let the voice token reach a renderer**; the host adds it to the request (rationale). Where it may go: `docs/specs/security-local.md` → "Persisted state".
- **Must bound the host request** at `MANAGED_VOICE_REQUEST_TIMEOUT_MS` (inside `SPEECH_ENGINE_TIMEOUT_MS`, so a fallback still fits) and `MAX_AUDIO_BYTES`, accepting only `audio/mpeg`, and validate the token (`dmv_…`) and voice id grammar before storing either.
- **Must send exactly the `toSpokenText` label and the member's voice id**, never terminal content, a notification body, or a Session id.
- **Never let the voice token reach a renderer**; the host adds it to the request (rationale), and only managed-voice approval writes it (`docs/specs/hosted.md` -> "Managed voice"). Where it may go: `docs/specs/security-local.md` → "Persisted state".
- **Must bound the host request** at `MANAGED_VOICE_REQUEST_TIMEOUT_MS` (inside `SPEECH_ENGINE_TIMEOUT_MS`, so a fallback still fits) and `MAX_AUDIO_BYTES`, accepting only `audio/mpeg`, and store only a token of the `dmv_…` shape and a voice of the curated set (`MANAGED_VOICES`).
- **Must cache clips by voice and text in the host's memory, bounded in count and bytes, never persisted**; a hit makes no request, and a token change empties it. **A 403 latches `notEntitled`**, which bypasses the cache until a speak succeeds, so a lapsed member hears the system voice; **a 401 forgets the token**. An answer to an older token changes nothing.
- **Must disable managed voice in memory immediately on Disconnect, including cached and in-flight results, even if clearing its persisted token fails.** A failure stays visible and retryable; it must never report a successful disconnect while the token remains on disk.

Source of truth: `withFallback` in `lib/src/lib/speech-engine.ts`; `createManagedVoiceEngine` in `lib/src/lib/managed-voice-engine.ts`; `createManagedVoiceHost` in `lib/src/host/managed-voice-host.ts`.
Source of truth: `withFallback` in `lib/src/lib/speech-engine.ts`; `createManagedVoiceEngine` in `lib/src/lib/managed-voice-engine.ts`; `createManagedVoiceHost` in `lib/src/host/managed-voice-host.ts`; `MANAGED_VOICES` in `remote-lib-common/src/remote/managed-voice.ts`.

### Push notifications

Expand All @@ -431,10 +434,10 @@ The Settings dialog's Notifications topic is this spec's; Network's content is `
- A baseboard alarm button toggles only its own setting, as an override for that Workspace; components without a Workspace scope edit application defaults. **Must open a separate Workspace alert dialog from either button's right-click or focused `Shift+F10`/`ContextMenu`**, only with a Workspace scope, preserving per-field inheritance and reset-all.
- The watched-command list removes rules and cannot add one — creating a rule stays the terminal context of a Pane running the command. It is the only place a rule set on a since-closed Pane can be removed.
- **The push group's device line must name every device a push would reach**, and otherwise say why there is none.
- **The Managed voice group must never show the token**, and states what is sent to Hosted, and how long ElevenLabs keeps it, before one is saved. It is hidden without `managedVoice`, which a self-host build's adapter omits, and hidden unless the port offers setup (`offerSetup`, a dev build's adapter) or a token is already configured, managed voice being an admin-only test slice; wherever it shows, the speech row names managed voice instead of linking the `/hosted` preview ([website-docs.md](./website-docs.md) -> `/hosted`).
- **The Managed voice group is shown wherever the adapter has `managedVoice`**, which a self-host build's omits; there the speech row names managed voice instead of linking the `/hosted` preview ([website-docs.md](./website-docs.md) -> `/hosted`). **Must state what is sent to Hosted, and that ElevenLabs' copy has no guaranteed deletion time, before the enrollment that enables it.** Managed voice has its own approval and Disconnect, independent of remote control; under Nowhere it explains and links Network instead, changing no policy. Enabled, it offers the curated voices and Disconnect, or, where speak says the plan lapsed, the plans. **No control takes a token, and no copy names one.**
- Each sink carries a test control, usable while the sink is off: the test sound goes through the shared speech queue and selected voice, reporting admission or an unavailable backend, never Session delivery state; the test push takes the real Burrow→ACL→Relay path and distinguishes no targets, zero, partial, and full delivery, hidden without a Burrow service.

Source of truth: `SettingsDialog` in `lib/src/components/SettingsDialog.tsx`; `WorkspaceAlarmSettingsDialog` in `lib/src/components/WorkspaceAlarmSettings.tsx`; `Baseboard` in `lib/src/components/Baseboard.tsx`.
Source of truth: `SettingsDialog` in `lib/src/components/SettingsDialog.tsx`; `ManagedVoiceSection` in `lib/src/components/ManagedVoiceSection.tsx`; `WorkspaceAlarmSettingsDialog` in `lib/src/components/WorkspaceAlarmSettings.tsx`; `Baseboard` in `lib/src/components/Baseboard.tsx`.

## Workspace union

Expand Down
2 changes: 2 additions & 0 deletions docs/specs/alert.rationale.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@

## Managed voice

Disconnect failure (2026-10): the atomic writer can fail while the old token remains on disk. Disabling in memory first prevents further label uploads; the retryable error tells the user that a restart could still reload that token.

**Why the token stays in the host.** A renderer runs terminal-supplied text beside framed pages; a bearer credential there is one script injection away from leaving the machine. The sidecar keeps it with the Burrow's credential (`docs/specs/security-local.rationale.md` → "Persisted state") and makes the request without the webview's CSP granting any remote `connect-src`. The cost is one IPC round trip per managed utterance, its audio returning as base64.

**Why fallback stops once audio starts.** Falling back after managed audio began would repeat a half-heard label in a second voice, and the Session would publish `speaking` twice for one delivery. A failure before `playing` has produced no sound, so speaking the same text through Web Speech still delivers the alarm once. Audio refused by the webview's autoplay policy lands on the fallback path; Web Speech may refuse the same gesture-less call (Chrome's synchronous `not-allowed`, commented at `webSpeechEngine` in `lib/src/lib/speech-engine.ts`), in which case the attempt ends unstarted.
Expand Down
Loading
Loading