Skip to content

feat: the phone approves requests and answers questions (spec 28) - #222

Merged
httpsphl merged 8 commits into
mainfrom
feat/mobile-relay
Oct 8, 2026
Merged

httpsphl merged 8 commits into
mainfrom
feat/mobile-relay

Conversation

@httpsphl

@httpsphl httpsphl commented Oct 8, 2026

Copy link
Copy Markdown
Owner

What

The phone (spec section 28): a PWA that approves requests and answers questions from the owner's bots, from anywhere. It is a remote control only: no chat, no files, no messages to bots. The bots keep running on the computer and the daemon still listens only on 127.0.0.1.

How it works:

  • Sealed end to end. ECDH P-256 + HKDF + AES-256-GCM (all WebCrypto-native, so the page carries no crypto library). The relay carries bytes it cannot open. docs/test-vectors/mobile.json is written by the daemon's tests and read by the page's tests, and was also checked against Node's WebCrypto.
  • Connecting by QR. The QR holds #p=<id>&s=<secret>&d=<computer key> after the #, which the browser never sends. The phone proves it read the secret, both screens show the same six digits, the owner accepts at the computer, and the phone collects its token once.
  • A phone token reaches only the relay, push, /v1/me and logout. Never the backups.
  • What the phone may allow is limited to plain tools (Bash, PowerShell, WebFetch, WebSearch, file tools, the plan), shown whole (<= 8 KiB), once only. Everything else, and anything cut, can only be denied there. The daemon checks again when the answer arrives.
  • Notices are empty, VAPID-signed Web Pushes (nothing for a push service to read). The page's service worker writes the text. The server calls only the browsers' push hosts.

Slices, one commit each (spec CE1–CE7):

CE1 spec section 28
CE2 botloft-cloud: pairings, WebSocket relay, queue, restricted phone token
CE3 botloftd: relay client, sealing, pairing, approval/question bridge, mobile.* methods
CE4 app: "Phone" block in Settings (QR drawn in the app, code comparison, disconnect)
CE5 the phone's page (app/src/phone), served by the account server at /m; Docker stage builds it
CE6 Web Push ([push], botloft-cloud vapid-key)
CE7 rehearsal results and the real-device runbook (spec 28.10, 19)

How it was checked

  • cargo fmt, cargo clippy -D warnings, cargo test --workspace: pass. pnpm check (types, lint, 510 tests): passes.
  • A real browser as the phone against the real server and daemon (in process): connect with the code, receive the list, allow a command, deny the rest, answer a question, see the "disconnected" notice. Run it with cargo test -p botloftd --test mobile_live -- --ignored --nocapture.
  • The full Docker image builds and runs read-only and non-root; /m is served under its policy, ..%2f stays inside the folder.
  • Rust and the page's WebCrypto agree byte for byte on keys, proofs, the code and sealed messages.

Not checked yet (needs the VPS updated and two phones)

The runbook is in spec 28.10 and the results table in section 19 says "em parte":

  • the service worker and the notice arriving on a locked phone (Android Chrome, iPhone Safari with the page on the Home Screen). The app's own browser pane cannot register service workers at all, so this cannot be seen there;
  • the system camera keeping the # of the QR on iOS and Android;
  • WebCrypto on Safari; wss and the 25 s ping behind Cloudflare for hours.

For whoever deploys

The deployed server (botloft.comitium.com.br) is the old version. It needs this image, phone_dir = "/srv/phone" and, for notices, [push] with a key from botloft-cloud vapid-key (docs/cloud-deploy.md). Without [push] everything else works and the phone shows only what is there when it is opened.

Known: the page's JavaScript is 711 KB (223 KB gzipped) because it carries the app's strings; it can be slimmed later.

Design for the phone: sealed end-to-end relay through the cloud server, QR pairing, restricted phone token, approval/question bridge rules, Web Push without content, milestones CE1-CE7. Stays local until the implementation lands (no docs-only PRs).
devices get a kind (computer/phone) and a peer; a phone token reaches only the relay, /v1/me and logout. Pairings (open, join, accept, collect once), a WebSocket relay that routes sealed bodies without opening them, a queue per phone acknowledged by the computer, presence, revocation that closes sockets, and rate limits. Spec 28 updated to match.
…(spec 28, CE3)

The daemon opens one relay connection while a phone is connected or being connected. P-256/HKDF/AES-GCM sealing (vectors in docs/test-vectors, checked against WebCrypto), QR pairing with a code to compare, keys in secrets/mobile.json, cards for approvals and questions, and the phone's answers through the same code as the app's, allowed only for plain tools shown whole. mobile.* methods and notifications, bindings, FakeBotloft handlers. The server sends a paired frame so the computer learns the phone's id.
…nnect (spec 28, CE4)

Settings > Backup gets a Phone block under the account: connect a phone by showing a QR drawn in the app, compare the six-digit code the phone shows, accept or refuse, list the connected phones and disconnect one. Texts in English, Portuguese and Spanish.
… CE5)

A PWA in app/src/phone: WebCrypto sealing checked against the computer's test vectors, the relay client (counters saved first, replay and tampering refused, reconnection, revocation), QR joining with a code to compare, an inbox of requests and questions with Allow/Deny only for what is shown whole, three languages, manifest and service worker. botloft-cloud serves it from phone_dir at /m under a strict policy; the Docker image builds it. Checked by hand with a real browser against a real daemon and server (cargo test -p botloftd --test mobile_live -- --ignored).
….8, CE6)

The computer's wake frame makes the server send each subscribed phone of that computer an empty VAPID-signed push (nothing for a push service to read), merged when they come close together, to the browsers' push services only. Phones subscribe from their page (one tap), the service worker writes the notice in the phone's language, and the page keeps its address fresh. botloft-cloud vapid-key makes the key; [push] in cloud.toml turns it on.
…8, 19)

Records what was seen with the full Docker image and a real browser, the finding that the app's browser pane cannot register service workers, and the step-by-step for the VPS and the two phones.
On macOS the status still said connected for a moment after the server cut the relay, so the question opened while the relay was down and the phone never heard of it. A phone that sees its computer come back asks again, so the test does too.
@httpsphl
httpsphl merged commit 9dc848d into main Oct 8, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant