Enrollment in the Receiver: enter a code or scan a QR, and stop reading trust out of her prose #40
Labels
No labels
needs-info
needs-triage
ready-for-agent
ready-for-human
v2
wayfinder:grilling
wayfinder:map
wayfinder:prototype
wayfinder:research
wayfinder:task
wayfinder:ticket
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
aiko/denpa#40
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The client half of ADR-0011 (
denpa/docs/adr/0011-devices-pair-to-the-go-between.mdin the go-between repo).Blocked by the go-between's enrollment work (ollvt-hermes-bridge#26) — there is nothing to redeem a code against until that lands.
What this deletes
The Receiver currently learns its own trust state by parsing her prose. That is the root cause of a defect that survived five review rounds: any notice shaped like an approve command reads as a fresh pairing offer, and because that is also how a genuine revocation arrives, it clears the onboarded flag and reopens setup. A turn where she answers "what command do I run?" could un-pair a working device.
Delete, from
src/renderer/src/services/pairing.ts:pairingNoticeFromand its two regexes (:58-66,:94-101)isPairingRefusalandPAIRING_REFUSAL(:49-51)PAIRING_PROBE_TEXTandpairingProbe(:73-75)PAIRING_POLL_MS(:83)And from
src/renderer/src/hooks/receiver/use-onboarding.ts, the probe effect (:115-124) and the revocation inference (:129-134).ADR-0011's second rule is the invariant to build against: nothing the client parses out of prose may decide its own trust state. Trust arrives as a typed message or not at all.
What to build
On 周波数, replacing the code-and-wait block: a field to enter the enrollment code, and a QR scan path. The screen already holds the address (
ADDRESS_LABEL) — the QR should carry the origin too, which also kills the real papercut of typing a LAN IP on a tablet keyboard. Reserve the certificate-fingerprint field the ADR names, unused today.The token lives in Rust, not the renderer. ADR-0002 routes anything touching a device through Rust, and
localStoragein a webview is not a secure store. It also keeps the token out of the URL, where the device id lands today (services/websocket-handler.tsx:302) — send it in the first frame after connect.canAdvance's third fact changes.onboarding-spec.ts:116-121currently gates onpairing.phase === 'paired', which is proven by a turn's text. It becomes "the go-between accepted our token". Hops one and two are unchanged and stay honest — an unreported hop two is still drawn as unknown rather than as failure.Keep what the wait screen got right. 「承認を待っている」, 「失敗ではない。誰かが席まで歩く時間だけかかる。」 and the elapsed clock are the correct register for a human walking to another machine, and an enrollment code still involves that walk. What goes is the implication that the client will notice on its own without being asked.
Asking stays deliberate (decided on
#39, and it carries): enrollment is requested by the operator, never on a timer.Constraints
#28both close. The token is a credential the go-between validates; possession is not the same as a client-side boolean that opens a gate.#37's reset path becomes the deliberate way to re-enroll. There is interrupted work for it atcf7d0e0onissue/denpa-37-39-pairing-slice— unreviewed WIP, and the#38/#39parts of that branch are superseded.components/receiver/onboarding-spec.ts.npm test.npm run typecheckhas 585 pre-existing errors andnpm run lintdies on a missingeslint-config-airbnb— neither is pass/fail; check only that touched files add no new errors.Acceptance
localStorage, and never appears in a URLnotice, anaudiopayload, or any other prose — the deleted functions have no replacementscanAdvancegates on the token being accepted, and hop two unreported is still drawn as unknown rather than as failureReference
#37— the reset path, now re-enrollollvt-hermes-bridge#23— the branch this makes mostly deletableEnrolment UX decided — see #42. Two entry paths, not one.
The consequence for this issue: the address step becomes conditional. Onboarding's 周波数 currently asks for
ws://192.168.1.24:12393/client-ws, typed on a touch keyboard on the device least able to verify it. A scanned device should never see that field; a typed-code device still needs it.Also still true from this issue's original scope:
pairingNoticeFrom,isPairingRefusal,PAIRING_PROBE_TEXTandPAIRING_POLL_MSall go. NotePAIRING_PROBE_TEXTisきこえてる?— and because it is the only sentence she has ever been sent, it is currently why she answers in Japanese regardless of what you say. Deleting it removes that side effect too.Note from #57, which needed a live restart to verify: this is no longer just a cleanup of prose-parsed trust — it is hard-blocking every connection.
ollvt-hermes-bridge#26merged the device gate (device_gate.py:45, called unconditionally fromroutes.py:44), so a go-between started from currentmaincloses any socket whose first frame is notauthenticateorenroll. This repo sends neither, so a restarted go-between admits no client at all: the client logsUnknown message type: auth-resultand the socket closes. Reproduced today against the running stack.With #57 merged the client now retries forever on a 30s backoff against that refusal instead of sitting dead, which keeps the log quiet but does not connect anything. Whatever builds the
authenticatefirst frame here should send it beforeinitializeConnectionruns inwebsocket-service.tsx— the gate wants it as the very first frame on every connect, including every reconnect.Landed on
mainas0d323ed(merge of950c593, branchissue/denpa-40-enrolment).What changed
Deleted, not made more careful.
services/pairing.tsandservices/device-id.tsare gone, withpairingNoticeFrom,isPairingRefusal,PAIRING_REFUSAL,PAIRING_PROBE_TEXT,pairingProbeandPAIRING_POLL_MS.use-onboarding.tslost the probe effect and the revocation inference. A test walks the whole renderer tree and fails if any of those names, or a regex readinghermes pairing approve/pairing codeout of text, comes back (onboarding-wired.test.ts:76-110).The credential lives in Rust.
src-tauri/src/device.rsholds{device_id, token, device_key}underapp_local_data_dir()/device.json, withdevice_credential/save_device_credential/forget_device_credential.device_keyis minted there on first read (ADR-0011 step 4) and survives a re-enrolment.lib/tauri-device.tsis the only renderer path to it, and it never toucheslocalStorage.The first frame.
websocket-service.tsxtakes asetFirstFrameprovider and sends it ononopenbeforeinitializeConnection, on every connect including every reconnect — which is the hard block from your second comment: currentmainof the go-between closes any socket whose first frame is notauthenticateorenroll. A socket with nothing to present now sends nothing at all rather thanfetch-backgroundsinto a gate. The device id is out of the URL entirely;websocket-handler.tsxdials the operator's address unchanged.Two entry paths on 周波数. A code field (normalises spacing and case, refuses anything off Hermes' alphabet before spending a socket) and a scan field that takes the QR payload, reads
origin+code+ the reservedfp, and sets the address from the origin. A scanned device never sees the address input — it is drawn as読み取ったアドレスtext instead (#42's conditional address step).canAdvancenow gates onenrollment.phase === 'accepted', which is only reachable fromauth-result ok:true. Hop two unreported is stillunknown, not failure.onboardingAfter'sunpairedbecameunenrolledand is driven byauth-result ok:false / unknown device or token— a typed frame, so a genuine revocation still reopens setup while nothing she says can.Kept: 「承認を待っている」, 「失敗ではない。誰かが席まで歩く時間だけかかる。」 and the elapsed clock, now running from the moment 周波数 is reached. Dropped:
RETRY_NOTE「承認が済めば、こちらで気づく。」 — the client no longer notices on its own. Nothing is on a timer; the onlysetIntervalleft is the clock, and it sends nothing.#37's reset:
最初からやるnow callsforgetDeviceCredential()before reloading, so a start-over actually re-enrols.Not fully met
enrollment.py:redeemreturnsNonein every case,device_gate.py:97sends"code unknown or spent"), so the client says 「その合図は通らなかった。期限が切れたか、もう使われている。」 with a next action, and does not guess which. Splitting it needs a distinction on the wire first.tauri-plugin-barcode-scanner(mobile-only, and I cannot build or verify an Android target here) or a webviewgetUserMediadecoder, which ADR-0002 rules out. Today a phone scans with its own camera app and the payload is pasted into 読み取り. Worth its own ticket.%LOCALAPPDATA%\hermeswas not synced and no go-between was started.Gates
(801 before;
pairing.test.tsanddevice-id.test.tswent with their modules, andenrollment.test.ts+tauri-device.test.tscame in.)(18 before, plus five in
device.rs.)npx tsc --noEmit -p tsconfig.web.jsonis 584 errors, one below the 585 baseline, and none of them is in a file this touched.