Repository navigation
Confirm Hosted approvals with two typed digits, one flow for remote control and Natural Voice - #1275
Conversation
Begin answers confirmDigits, the HMAC of the user code under RELAY_ENROLL_SECRET; the account's approval stores the two digits the person typed; a poll redeems an approval, for either purpose, only where they match, and answers mismatch otherwise. A live approval is never replaced, so each code takes one attempt. Migration 010 adds the column and drops approvals from before it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HostedApproval (lib/src/host/remote/hosted-approval.ts) holds the begin, the code waiting, the polls, and the redemption for both purposes; the Burrow service and the managed-voice host differ only in their flow: what begin holds back, how a poll reads, and what redemption keeps (a Burrow, or a voice token, deleted at Hosted when discarded). Voice's status now carries the same HostedEnrollmentState, parsed by the same reader, with typed end reasons in place of free-text messages. The desktop shows the two confirmation digits instead of the user code, through one view, HostedApprovalProgress, worded per purpose, and ends on a mismatch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The /enroll view asks for the digits in place of showing the user code to compare, and sends them with the approval. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hosted.md states the derivation and the poll's comparison, with the rationale for checking at the poll and the v1.3.0 break; relay.md puts managed voice's approval under Burrow side's rules; security-hosted.md adds FAIL IF S276; security.md and security-remote.md say what an approval now takes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… does Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deploying mouseterm with
|
| Latest commit: |
4f1fc9c
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://87a1598d.mouseterm.pages.dev |
| Branch Preview URL: | https://voice-active-code.mouseterm.pages.dev |
Renumbers the confirm-digits migration to 011 after main's 010. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dormouse-bot
left a comment
There was a problem hiding this comment.
CI is red at this head: Build & Test fails in run 39623281831. Both failures come from this PR's changes reaching files it doesn't touch, and main is green at the merge base (de92b3e). My earlier approval didn't account for this, so I'm withdrawing it.
1. vscode-ext/test/burrow.test.ts › "answers another window's Enroll with the code this one is showing". Its fetch stub returns a begin answer without confirmDigits. isBurrowEnrollBeginResponse now requires that field, so the begin is refused with "the Relay's answer was not an enrollment code." The test also expects userCode in the waiting result, which now carries confirmDigits instead. The fix adds the field to the stub and asserts on it:
userCode: '23AB-YZ9K',
+ confirmDigits: '42',
expiresAt: Date.now() + 10 * 60_000,
...
- expect(results(bound.posted)[0]).toMatchObject({ result: { status: 'waiting', userCode: '23AB-YZ9K' } });
+ expect(results(bound.posted)[0]).toMatchObject({ result: { status: 'waiting', confirmDigits: '42' } });
...
- expect(far.results[0]).toMatchObject({ result: { status: 'waiting', userCode: '23AB-YZ9K' } });
+ expect(far.results[0]).toMatchObject({ result: { status: 'waiting', confirmDigits: '42' } });2. scripts/security-audit-canary.test.mjs. The seed .github/audit/canaries/passkey-removal-keeps-sessions.patch no longer applies. Its migrations.test.ts hunk has }; as trailing context right after the 010 pin, and the new 011_enrollment_confirm_digits.sql pin now sits there. The fix widens that hunk to @@ -32,9 +32,9 @@ and adds the two 011 lines as context before };. With that change the canary test passes locally (17/17). I couldn't run the vscode-ext suite here.
Neither file is in this diff, so I can't attach suggestions. I can push both fixes if you'd like.
CI failed — vscode-ext burrow test and audit canary seed break on this PR's changes.
The poll's read now stamps "mismatchedAt" (added in migration 011,
unmerged, so no 012) on the approval it reads, in the same statement and
only where that approval is live, unredeemed, of the poll's purpose, typed
with other digits, and not stamped before. The redeeming statements keep
their digit check, so a stamped approval still redeems nothing. A
replaced expired approval starts unstamped.
A new cookie route, GET /api/relay/enrollments/:userCode, answers
{ mismatched } for the signed-in account's own live approval only;
another account's approval reads as none. While the account page waits
on an approval it reads that route with each re-check, and a mismatch
ends the wait with an error-toned notice, replacing the late notice too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ng else shows it
With Natural Voice already on, or no computers list read, an approval's
redemption changes nothing on the page, so it showed a fixed notice and a
wrong-digits entry went unshown. The approval read now answers
{ mismatched, redeemed } (still the signed-in account's own live approval
only), and approvalShows falls back to that redemption where no change
could show it: the page waits as it does elsewhere, ending in done, the
mismatch error, or the late notice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The vscode-ext begin stub now answers confirmDigits, which the begin guard requires, and the passkey-removal canary's migrations hunk takes the 011 pin as context. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What changes for the user
Approving a computer at
hosted.dormouse.sh/enrollnow takes two digits typed on the account page, for both remote control and Natural Voice. Dormouse shows the two digits (it used to show an 8-character code,7KQM-X4TD, to compare by eye). The account page asks for the digits before Approve. If the digits are wrong, nothing is approved, the code can't be approved again, Dormouse says the digits didn't match and offers a new code, and the account page says so too, within one 5-second re-check.The digits work like pairing's digits: they make approval active. A link alone no longer approves anything. Before this change, someone who clicked a stranger's
/enroll#CODElink and pressed Approve without comparing handed that stranger an enrollment or a voice token.One flow for both purposes
Before this PR, the two purposes already shared the Hosted routes, the approvals table, the
/enrollview and the code component. The desktop still had two copies of the run itself: one inBurrowService, one inmanaged-voice-host.ts, and they modeled outcomes differently. Now:HostedApprovalinlib/src/host/remote/hosted-approval.tsowns begin, the code waiting, the polls with their retry and slow-down, the deadline, cancel, and redemption.BurrowServiceand the managed-voice host each pass only a flow:pollHostedEnrollment/pollHostedVoiceApproval, which now return one answer shape);HostedEnrollmentStatewith typed end reasons, in place of free-text messages. Both webview readers parse it withparseHostedEnrollmentState. Both sections render it throughHostedApprovalProgress; only the wording differs by purpose./enrollview asks for the digits through a newConfirmDigitscomponent. After Approve, the page's re-check from Hosted account page: refetch after every mutation, wait out approvals #1266 also reads the approval itself (GET /api/relay/enrollments/:userCode→{ mismatched, redeemed }, the signed-in account's own live approval only). A mismatch ends the wait at once with an error notice. When nothing visible would change (Natural Voice already on, or no computers list read), the page waits onredeemedinstead of showing a fixed notice.What still differs, and why
:voice), so a code for one purpose can never redeem the other.RELAY_ENROLL_SECRET, which only the relay Worker holds (S79 forbids giving it to the account Worker). So the account page stores the typed digits and the poll compares them. A mismatched poll stampsmismatchedAton the approval, and the account page reads that stamp, so the page reports a typo once the desktop next polls, rather than as you type. The alternative was a new secret shared by both Workers, plus a production secret to set.Compatibility with released desktops (v1.3.0): a clean break
v1.3.0 desktops can no longer finish a Hosted approval, for remote control or Natural Voice, once this Hosted deploys.
mismatch, a status v1.3.0 doesn't know, so v1.3.0 ends with "could not finish".Migration 011 (renumbered after main's 010) drops approvals from before the columns existed, and adds
confirmDigitsandmismatchedAt. Approvals live 10 minutes, and none of them carried digits.Within one VS Code install, a window still running the old webview beside a new broker (or the reverse) won't draw a waiting code until it reloads.
Specs
docs/specs/hosted.md"Burrow enrollment": the digits in the diagram, how they're derived, the rule that a poll redeems only on matching digits (one attempt per code), the mismatch stamp, the approve body, and the approval read. The rationale file has a new "Confirmation digits" entry covering why the check sits at the poll and the v1.3.0 break.docs/specs/relay.md"Burrow side": the desktop shows the digits and stops polling onmismatch. Managed voice's approval is now the same run under these rules.docs/specs/security-hosted.md: new FAIL IFS276(nextbumped to 277) for the digit check, the mismatch stamp (written only by a poll under a live, unredeemed approval), and the approval read (owner only). Migration 011 is added to the Relay boundary's file list, and the.github/audit/hosted.mdenrollment question now asks about digits too.docs/specs/security.md"What is not defended" anddocs/specs/security-remote.mdS184: what an approval takes now.hosted.md,relay.mdandsecurity-hosted.md.Test plan
remote-lib-common:node --test(the digit derivation against an independent HMAC reading, the skipped draws, padding, the begin-response guard,mismatchin both poll guards)hosted:pnpm run test(begin's digits, mismatch for both purposes even for an approver no longer entitled, the approval route's 400 for missing or malformed digits, the migration pin)lib: fullvitest run(the shared runner through the remote service and the voice host, mismatch endings, the voice port's parser, both sections' rendering)hosted: the mismatch stamp's every condition, the approval read's owner scope, expiry and replacement reset, andpendingEnd/approvalShowsinsettle.test.tstsc -bin lib;tsc --noEmitin standalone, hosted, vscode-extpnpm run lint:specs,pnpm run lint:md,public-docs-lint,outbound-lint,e2e-lintRemoteControlSectionandManagedVoiceSectionrun undervitest.argos.config.ts(Chromium; screenshots looked over)/enroll#CODEasks for the digits, the field drops non-digits, and Approve stays disabled until two digits are typed (the dev account isn't entitled, so the approval itself answered 403 there)/enrollenrolls; typing others ends with "were not the ones shown here" on the desktop, and the account page shows the wrong-digits notice within ~5 s🤖 Generated with Claude Code