Repository navigation
feat(cloud): a watch's exchange rides FTW Cloud - #1819
Merged
Merged
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
This was referenced Oct 23, 2026
frahlg
added a commit
that referenced
this pull request
Oct 23, 2026
From the review of #1819: - A message 1 that FTW Cloud names under a device that is no watch costs nothing. Handshakes: 12 a minute per watch, 60 in all. - CheckWatch decides and changes nothing; a session commits (stamps the visit) once its first transport message decrypts. A watch holds two sessions and the box eight; the oldest unproved goes first, never the newcomer, so replays end no working session and no other watch's. - The watch-code path is gone: a watch request without a key is refused, and a hello with a code gets silence. - x answers keep to a tenth of the tunnel budget and stop at nine tenths; the box takes 120 requests a minute. A request whose answer could not leave is dropped and runs nothing. - The exchange takes loadpoint.hold alone; other ops get E_OP_NOT_GRANTED. - Tests for each, a revoke mid-session, and a restart's clock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frahlg
force-pushed
the
claude/watch-cloud
branch
from
October 23, 2026 16:10
a37987f to
f681e28
Compare
frahlg
changed the base branch from
codex/remove-retired-relay-20261017
to
master
October 23, 2026 16:10
Member
Author
|
Fredrik approved this in chat on 23 October 2026: the "Through FTW Cloud" section of ADR 0009 (all six points) and the 🤖 Generated with Claude Code |
The relay stopped on 17 October 2026, and with it the socket that carried
a watch's exchange (ADR 0009). The exchange sessions live on the box and
never needed the relay; only the carrier did.
- appuplink: the exchange table keeps its sessions; its relay socket gives
way to an Exchange the cloud link delivers x to. A request is tried only
against the sessions of the device FTW Cloud names, and a handshake that
proves another key gets silence, which goes back as an empty d.
- cloudlink: reads x, sends it with the tun lane 0 frames, says x in
hello.caps beside tun.
- The roster gives a watch tun; the box says cmd.exchange while the link
takes x.
- POST /api/app-link/pairing {"role":"watch","key":...}: an owner's phone
adds its watch by key, with the same step-up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the review of #1819: - A message 1 that FTW Cloud names under a device that is no watch costs nothing. Handshakes: 12 a minute per watch, 60 in all. - CheckWatch decides and changes nothing; a session commits (stamps the visit) once its first transport message decrypts. A watch holds two sessions and the box eight; the oldest unproved goes first, never the newcomer, so replays end no working session and no other watch's. - The watch-code path is gone: a watch request without a key is refused, and a hello with a code gets silence. - x answers keep to a tenth of the tunnel budget and stop at nine tenths; the box takes 120 requests a minute. A request whose answer could not leave is dropped and runs nothing. - The exchange takes loadpoint.hold alone; other ops get E_OP_NOT_GRANTED. - Tests for each, a revoke mid-session, and a restart's clock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TestAKeyThatIsNoWatchIsRefused sent two requests with id 1 on one session, in map order, and CI's race run read a mixed answer. Each request now has its own id, in a fixed order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frahlg
force-pushed
the
claude/watch-cloud
branch
from
October 23, 2026 17:17
8442d99 to
a521f7e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A watch without its iPhone has HTTPS and nothing lower. Its exchange sessions (ADR 0009) live on the box and never needed the relay. Only their carrier did, and the relay stopped on 17 October 2026. #1691 removes the relay and the exchange with it. This PR brings the exchange back, carried by FTW Cloud.
Base: master. Rebased on master after #1818 (21e3d16) and #1817 (C6) merged, so it carries only its own three commits. ADR 0009 keeps master's accepted wording. It no longer stacks on #1691. When #1691 rebases on top of this, it must keep the exchange files (
appuplink/exchange.go,appproto/exchange.goand their tests).What
appuplink: the exchange table returns, without its relay socket.Exchangetakes eachxthe cloud link delivers, eight at a time, and answers with onexunder the samedevandid.d, so the service answers the watch at once rather than at its deadline.appproto: the exchange handler (hello,cmd,now, the 400 ms floor, two frames of one length) returns, with wording for FTW Cloud.cloudlink:x;xon the tun lane 0 queue, under its own share of the tun budget;xinhello.capswhen it has an Exchange beside its Tunnel.tun. The box answers a watch's key on the exchange alone (TestAWatchKeyGetsNoStream).cmd.exchangeinhello_okwhile the link takesx, so the phone offers "Apple Watch".POST /api/app-link/pairing {"role":"watch","key":"<base64url>"}: an owner's phone adds its watch by key. It needs a session, an owner, and a step-up on the request itself.appenroll.AddWatchhandles it, and adding the same watch again is a no-op. A request without a key gets 400: the watch-code path is gone (MintWatchCode,AuthoriseWatch,WatchTTL).exchange-vectors.json: X1 carries no code, and each step also has thexframe it crosses the uplink in.minor(approved by Fredrik on 23 October 2026).Review fixes
CheckWatchis pure. Itscommitstamps the watch and saves only after the first transport message opens.devbefore it spends a token or a DH. Each watch has its own 12 handshakes a minute, and the box 60.xreplies may use a tenth of the tun budget and stop once the tunnel has used nine tenths, so lane 0 keeps its headroom. The exchange takes 120 requests a minute. A request it cannot answer runs nothing: it is dropped before the handler sees it, so no command runs whose answer cannot leave.cmdIdsent again after one can run again. The protocol doc and ADR 0009 say so. The watch side (feat(watch): the watch on its own through FTW Cloud ftw-app#99) compareshello_okclocks and does not send a command again to a box that restarted.loadpoint.holdalone. Any other op getsE_OP_NOT_GRANTED.Proof (local)
make verifyon the rebased branch (a521f7e): green. Before the rebase,go test -raceonappuplink,cloudlink,appproto,appenrollandapiwas green too. CI runs here now that the base is master. Its first run caught a test of mine that sent two requests with one id on one session (TestAKeyThatIsNoWatchIsRefused); the last commit (a521f7e after the rebase) gives each its own id, and the test passed 20 runs in a row under-race.TestAWatchCommandThroughFTWCloudruns a real cloud link against a mini FTW Cloud: the roster lists the watch withtun, the box saysx, the watch charges, and only roster, notice andxleave the box with the copy off.TestReplaysEndNoWorkingSession;TestHandshakesAreRationed;xwith its share spent:TestXKeepsToItsShareOfTheTunnelBudget,TestNoAnswerMeansNoCommand;TestARestartedBoxSaysSoInItsClock;TestARevokeMidSessionRefusesTheNextCommand;TestTheExchangeTakesAMinutesRequestsAndNoMore;TestAnExchangeTakesTheChargersHoldAlone;TestCheckingAWatchWritesNothingUntilItCommits;TestAWatchWithoutAKeyIsRefused.xfrom a key that is no watch is tested in feat(once): carry a watch's exchange to its box as x ftw-cloud#49.ftwclouddfrom feat(once): carry a watch's exchange to its box as x ftw-cloud#49 and this box in a test process. A paired iPhone simulator was shut down. The watch charged through FTW Cloud, and the box recorded the holdSetBy:app:<watch id>. Pictures are in feat(watch): the watch on its own through FTW Cloud ftw-app#99.Not tested on the home box or on a real watch.
Wire: #1818. FTW Cloud: srcfl/ftw-cloud#49. App: srcfl/ftw-app#99.
🤖 Generated with Claude Code