Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions .claude/resolve-budgets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import re
p='scripts/spec-word-budgets.json'
s=open(p).read()
def merge(m):
vals, order = {}, []
for block in (m.group(1), m.group(2)):
for k, v in re.findall(r'"([^"]+)":\s*(\d+)', block):
if k not in vals: order.append(k)
vals[k] = max(vals.get(k, 0), int(v))
return ''.join(f' "{k}": {vals[k]},\n' for k in order)
s = re.sub(r"<<<<<<< [^\n]*\n(.*?)(?:\|\|\|\|\|\|\| [^\n]*\n.*?)?=======\n(.*?)>>>>>>> [^\n]*\n", merge, s, flags=re.S)
open(p,'w').write(s)
10 changes: 10 additions & 0 deletions .claude/resolve-hosted-md.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Take the replayed commit's side of each hosted.md conflict, then restore
# #867's relay-smoke clause on the production deploy-order sentence.
import re, sys
p='docs/specs/hosted.md'
s=open(p).read()
s=re.sub(r"<<<<<<< [^\n]*\n(.*?)(?:\|\|\|\|\|\|\| [^\n]*\n.*?)?=======\n(.*?)>>>>>>> [^\n]*\n", lambda m: m.group(2), s, flags=re.S)
clause="Deploy relay, voice, then account, stopping at a failure; the relay must pass its revision check, push config, and `oneTimeSmoke` before the next deploy (rationale)."
s=s.replace("Deploy relay, voice, then account, stopping at a failure (rationale).", clause)
if clause not in s: sys.exit("clause anchor not found")
open(p,'w').write(s)
2 changes: 1 addition & 1 deletion .github/audit/application-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ Be adversarial, and go past the `FAIL IF` list. Ask specifically:
Follow `DORMOUSE_RELAY_ORIGIN` from `scripts/relay-origin.mjs` into both host
bundles and the standalone webview (`standalone/vite.config.ts`), then list
every request a build baked with a non-default origin could make to
`dormouse.sh` or `hosted.dormouse.sh` — the one-time half of `service.ts`,
`dormouse.sh` or any of its subdomains (`hosted.`, `relay.`, `voice.`) — the one-time half of `service.ts`,
`lib/src/host/managed-voice-host.ts`, `standalone/src/updater.ts` and the
updater endpoint `standalone/scripts/tauri.mjs` overlays away, and anything
else that fetches. A release build that accepts `DORMOUSE_RELAY_IS_HOSTED`
Expand Down
48 changes: 30 additions & 18 deletions .github/audit/hosted.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,19 @@

**Output file:** `audit-hosted.md`

This is a code-and-specs audit of the Hosted account application and the
one-time rendezvous it serves. You need no
This is a code-and-specs audit of Hosted's three Workers — the account
application (`hosted.dormouse.sh`), the relay that serves the one-time
rendezvous (`relay.dormouse.sh`), and managed voice (`voice.dormouse.sh`). You need no
PAT — do not use one. The two pgstencil provenance checks below do read the
GitHub API, but only a public repository, which the workflow's default
`GITHUB_TOKEN` and the operator's own `gh` login both reach; if that API is
unreachable, report those two checks as `UNVERIFIABLE`.

Read `docs/specs/hosted.md`, `docs/specs/one-time.md` (its "Wire contract",
"Hosted rendezvous", and "Phone page"), `hosted/server/`, `hosted/src/`,
`hosted/scripts/`, `hosted/wrangler.jsonc`,
`remote-lib-common/src/remote/one-time-wire.ts`, the phone page Hosted serves —
`hosted/scripts/`, `hosted/wrangler.jsonc`, `hosted/wrangler.relay.jsonc`,
`hosted/wrangler.voice.jsonc`,
`remote-lib-common/src/remote/one-time-wire.ts`, the phone page the relay serves —
`lib/vite.one-time.config.ts`, `lib/one-time/`, `lib/src/remote/one-time-app/`,
and `lib/scripts/assert-pocket-worker.mjs` — and
`.github/workflows/hosted-preview.yml` and
Expand Down Expand Up @@ -76,13 +78,21 @@ report the same finding twice.

Be adversarial, and go past the `FAIL IF` list. Ask specifically:

- **Is the Hosted origin the only one that can drive Hosted?** Trace a request
from `hosted/server/worker.ts` through `workerApp`'s origin gate and
`secureHeaders`: a foreign `Host`, a preview hostname, a misconfigured
deployment's error path, and the SPA fallback must each answer without
credentialed CORS, without a cacheable shell, and without inline script.
Check that authentication cookies stay `__Host-`, Secure, HttpOnly, `Path=/`
and Domain-less, and that no session token reaches browser JSON or storage.
- **Is each Worker's origin the only one that can drive it?** Trace a request
from `hosted/server/worker.ts`, `hosted/server/relay-worker.ts`, and
`hosted/server/voice-worker.ts` through `workerApp`'s origin gate and
`secureHeaders`: a foreign `Host`, a sibling Worker's origin, a preview
hostname, a misconfigured deployment's error path, and the account's SPA
fallback must each answer without credentialed CORS, without a cacheable
shell, and without inline script. The siblings are same-site, so the login
cookie rides their requests to the account: every account cookie route must
refuse their `Origin`. Check that authentication cookies stay `__Host-`,
Secure, HttpOnly, `Path=/` and Domain-less, and that no session token
reaches browser JSON or storage.
- **Does a secret reach a Worker that has no use for it?** Read each mapper in
`hosted/server/bindings.ts` and each Wrangler config: the relay and voice
Workers must hold and pass no auth secret and never import Better Auth, the
relay no Hyperdrive, the account no ElevenLabs key.
- **Can a Hosted login become terminal access, or an account become someone
else's?** `authPolicy` must keep explicit linking and independent logins; a
callback whose initiating login was revoked must fail; an unused or unknown
Expand All @@ -95,13 +105,15 @@ Be adversarial, and go past the `FAIL IF` list. Ask specifically:
forwarded. Look for a second phone admitted across an await or a hibernation,
a room that outlives its alarm, a web page that can mint a room, a join from
another origin, a room id a caller can choose, and a limit a caller can step
around. Account cookies ride the phone's upgrade to this same origin: no
one-time route or the room may read them or reach auth.
- **Does anything from the test or preview build reach production?** The
production Worker must not export the captured-email inbox, the deterministic
clock, or the testing injection module; preview must not copy production
routes, bindings, or credentials, must not call real mail or OAuth, and its
cleanup must check out the base branch rather than the closed PR's.
around. The relay is same-site with the account, so account cookies can
ride the phone's upgrade: no one-time route or the room may read them or
reach auth.
- **Does anything from the test or preview build reach production?** No
production Worker may export the captured-email inbox, the deterministic
clock, or the testing injection module; previews must not copy production
routes, bindings, triggers, or credentials, must not call real mail, OAuth,
or ElevenLabs, and their cleanup must check out the base branch rather than
the closed PR's.

Does the shipped code still match what the spec and this section claim? Spec
drift is a finding; say which side is wrong.
11 changes: 6 additions & 5 deletions .github/workflows/hosted-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ jobs:
- run: pnpm install --frozen-lockfile
- run: pnpm test:hosted
- run: pnpm build:hosted
# Every Worker's static files, each under its own `dist/<worker>/`.
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: preview-assets
Expand Down Expand Up @@ -132,17 +133,17 @@ jobs:
run: pnpm --filter dormouse-hosted db:migrate --preview && pnpm --filter dormouse-hosted db:validate --preview
env:
DATABASE_URL: ${{ steps.database.outputs.db_url }}
- name: Deploy Worker and Hyperdrive
- name: Deploy the account, relay, and voice Workers and Hyperdrive
id: deploy
run: pnpm --filter dormouse-hosted preview:deploy
env:
DATABASE_URL: ${{ steps.database.outputs.db_url }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
PREVIEW_AUTH_SECRET: ${{ secrets.PREVIEW_AUTH_SECRET }}
- name: Check deployed revision, database, cookies and routes
run: pnpm --filter dormouse-hosted preview:smoke "$PREVIEW_ORIGIN" "$BUILD_SHA"
- name: Check deployed revisions, database, cookies, routes and the rendezvous
run: pnpm --filter dormouse-hosted preview:smoke "$PREVIEW_ORIGINS" "$BUILD_SHA"
env:
PREVIEW_ORIGIN: ${{ steps.deploy.outputs.url }}
PREVIEW_ORIGINS: ${{ steps.deploy.outputs.origins }}

cleanup:
if: >-
Expand All @@ -162,7 +163,7 @@ jobs:
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: package.json
- name: Remove this PR's Worker, Hyperdrive and Neon branch
- name: Remove this PR's Workers, Hyperdrive and Neon branch
run: node hosted/scripts/preview.mjs cleanup
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
Expand Down
9 changes: 5 additions & 4 deletions .github/workflows/hosted-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ on:
workflow_dispatch:
inputs:
promote:
description: Deploy to hosted.dormouse.sh after verification
description: Deploy the account, relay, and voice Workers after verification
type: boolean
default: false
permissions:
Expand All @@ -30,6 +30,7 @@ jobs:
- run: pnpm build:hosted
- name: Require accepted package provenance
run: node --input-type=module -e 'import { verifyPackages } from "./hosted/scripts/production.mjs"; await verifyPackages();'
# Every Worker's static files, each under its own `dist/<worker>/`.
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: hosted-production-assets
Expand Down Expand Up @@ -65,7 +66,7 @@ jobs:
with:
name: hosted-production-assets
path: hosted/dist
- name: Validate production identity, uncached Hyperdrive and Worker secrets
- name: Validate production identities, uncached Hyperdrive and each Worker's secrets
run: node hosted/scripts/production.mjs preflight
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
Expand All @@ -87,12 +88,12 @@ jobs:
run: pnpm --filter dormouse-hosted db:migrate && pnpm --filter dormouse-hosted db:validate
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
- name: Deploy verified build
- name: Deploy the relay and verify its rendezvous, then the voice and account Workers
run: node hosted/scripts/production.mjs deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- name: Verify live production revision and auth boundary
- name: Verify live production revisions, auth boundary and rendezvous
id: live
run: |
node hosted/scripts/production.mjs smoke
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ The Tool shows the harness in its own pane and prints the command to drive it
- **`vscode-ext/`** — VS Code extension wrapping the lib in a webview (esbuild; node-pty via forked child process; direct-path WebRTC via node-datachannel, every platform's addon in one VSIX)
- **`website/`** — Marketing site (Vite) bundling part of the lib as an interactive demo on `FakePtyAdapter`
- **`relay/`** — Selfhost coordinating Relay for remote control (Hono): accounts + passkey auth in local JSON files (no database), WebSocket routing between Clients and Burrows, serves the built Pocket app
- **`hosted/`** — Separate Hosted account frontend and Hono Worker; packed pgstencil Better Auth, Postgres, and provider configuration.
- **`hosted/`** — Hosted's three Hono Workers: account and Better Auth (`hosted.dormouse.sh`), one-time rendezvous (`relay.`), voice (`voice.`); Postgres.
- **`dor/`** — The `dor` CLI (stricli) staged onto the `PATH` of every Dormouse-launched terminal; talks to its host over a private control socket
- **`remote-lib-common/`** — Security primitives + remote wire contract shared by `relay`, the Burrow module in `lib`, and the Pocket app (bare ES2022 — no DOM or Node types)
- **`dor-lib-common/`** — Cross-platform external-process spawning (`spawnAndCapture`) shared by `dor` and the `lib` host. Despite the parallel names, the two `*-lib-common` packages are unrelated: `remote-lib-common` is remote security/wire, `dor-lib-common` is spawn plumbing.
Expand Down
4 changes: 2 additions & 2 deletions SELF_HOST.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ installed release, which the installer and `manage status` both print.
DORMOUSE_RELAY_ORIGIN=https://<laptop>.<tailnet>.ts.net pnpm dogfood:vscode
```

That is a self-host build: it sends nothing to `dormouse.sh` or
`hosted.dormouse.sh` on its own, so it has no one-time connection, no managed
That is a self-host build: it sends nothing to `dormouse.sh` or any host
under it on its own, so it has no one-time connection, no managed
voice, and no auto-update — update it by rebuilding
(`docs/specs/relay.md` → "Relay origin").

Expand Down
2 changes: 1 addition & 1 deletion docs/specs/alert.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ Source of truth: `toSpokenText` / `startAlertSpeech` in `lib/src/lib/alert-speec
Every rule above holds for both engines.

- **Must try managed voice first while the adapter's `managedVoice` status says a token is saved** (`docs/specs/transport.md` → "Managed voice"); otherwise the utterance goes straight to Web Speech.
- **A self-host build's host has no Hosted origin** 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").
- **A host speaks 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".
Expand Down
Loading
Loading