Skip to content

feat(cloudwire): a watch reads FTW Cloud in one request - #1627

Merged
frahlg merged 1 commit into
masterfrom
watch-cloud-once
Oct 14, 2026
Merged

frahlg merged 1 commit into
masterfrom
watch-cloud-once

Conversation

@frahlg

@frahlg frahlg commented Oct 14, 2026

Copy link
Copy Markdown
Member

Problem and result

Milestone 6 of ADR 0009, decisions 7, 8 and 9: FTW Cloud's wire for a watch, in go/protocol/cloudwire and docs/protocol-cloud.md. A watch on its own reads its home from FTW Cloud in one HTTPS request; this PR specifies that request and its bytes. FTW Cloud serves it in step 7 (srcfl/ftw-cloud); nothing on the box changes.

  • The one-shot read. POST https://cloud.ftw.energy/v1/app/once?home=<id>, content-type: application/octet-stream. The body is Noise IK message 1 to a service key, with prologue ftw.cloud.once.v1: and the home id, so it can never stand in for an app session handshake. Message 1 carries one unsigned envelope: read, token.get, push.register or push.unregister; anything else, an owner's suspend among them, gets 400. Admission is the app session's; a refusal is 403 with an empty body. The answer is message 2 and then the envelopes that did not fit, each message after its length as u16 big endian. The service keeps no session and no Noise state.
  • read {have, hz}. have is {boot, seq}: the boot the watch's envelopes come from and the highest seq it holds per class. The service leaves out every box envelope have covers (Have.Covers). hz: 1 keeps the box's watch in force for 60 s.
  • token.get gets token {token, expiresInS} in message 2; push registration gets an empty payload once stored.
  • Limits. One read at a time per device, 30 a minute, message 1 at most 4 KiB, the answer within 10 s; 429, 400 and 501 as the ADR says.
  • push.register gains app, iphone (the default when absent) or watch, so the service picks the APNs topic. A watch and its phone get byte-identical alert payloads.
  • Roster. A device may carry role watch, with the scopes the box derives: the watch role's, cut to what its phone's role carries now. The box already sends them (feat(applink): an owner phone adds a watch as a device of its own #1625).

One finding against the ADR

The ADR says message 2 is followed by transport messages "each after its length as u16 big-endian", which leaves message 2 itself unframed. Its length varies with the snapshot, and a Noise message cannot be opened without knowing where it ends, so this PR puts the same u16 length in front of message 2 too. Every message in the body is framed alike.

Scope and safety

  • Wire types, framing helpers, docs and vectors only. No box code changes, and no existing vector changes: cloud-vectors.json, cloud-app-vectors.json and push-vectors.json are byte for byte as before.
  • FTW Cloud still never controls: the one-shot read takes only reads, a token and push registration.
  • app is additive and omitted when empty, so an existing phone's registration encodes as before.

Verification

  • TestOnceReadVectors writes and checks cloudwire/testdata/cloud-once-vectors.json: fixed keys on both ends, a read whose have covers the full live and the lp from cloud-vectors.json, message 2 with the live delta and prices, the hb after it as a transport message, and the whole answer body. It also plays the watch: splits the body, reads message 2, and opens the hb as the box's exact bytes.
  • TestOnceAndAppHandshakesStayApart: an app session responder cannot read a one-shot message 1.
  • TestHaveCoversOnlyWhatTheWatchHolds: older and equal seq covered; newer seq, another class, another boot and junk not.
  • TestOnceAnswerFraming and TestPushRegisterNamesTheApp.
  • make verify green.

Checklist

  • The change follows VISION.md and one selected scope.
  • I checked overlapping PRs and coordinated shared files/contracts.
  • Relevant checks cover the changed behaviour and failure paths.
  • A human reviewed changed UI in a browser, or no UI changed.
  • A Changeset is included, or the change is exempt.
  • Every commit has a DCO sign-off.

Independent of #1626. srcfl/ftw-cloud (step 7) and the watch app (steps 8 and 10) replay the vectors.

🤖 Generated with Claude Code

ADR 0009 milestone 6, decisions 7, 8 and 9.

cloudwire gains the one-shot read: POST /v1/app/once?home=<id> with
Noise IK message 1 as the body and prologue ftw.cloud.once.v1: plus the
home id, so it never stands in for an app session handshake. Message 1
carries read {have, hz}, token.get, push.register or push.unregister.
Have.Covers says which box envelopes the watch already holds. The answer
is message 2 and the envelopes that did not fit, each message after its
length as u16 big endian; message 2 is framed too, since a Noise message
cannot be opened without its end.

push.register gains app, iphone or watch, so the service picks the APNs
topic. docs/protocol-cloud.md specifies the read, its limits, app and
role watch in the roster. Golden vectors for one read are in
testdata/cloud-once-vectors.json; the other vector files are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-14T11:05:11.471683Z 27d5fc7 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@frahlg
frahlg merged commit 5d2e7fe into master Oct 14, 2026
24 of 26 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