Skip to content

feat(cloud): FTW Cloud C3: app session wire, K11 session info, statistics consent - #1603

Closed
frahlg wants to merge 4 commits into
claude/cloud-c2-a453befrom
claude/cloud-c3-core
Closed

frahlg wants to merge 4 commits into
claude/cloud-c2-a453befrom
claude/cloud-c3-core

Conversation

@frahlg

@frahlg frahlg commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

FTW Cloud C3, Core's part: what the phone needs to read its home from FTW Cloud. Draft. Based on the C2 branch (#1595), so the diff here is C3 alone.

This is the first half of a wire pair. srcfl/ftw-cloud#5 pins go/protocol to this branch at c6301a0b, and srcfl/ftw-app#19 replays the new vectors in Swift.

What changes

The app session's wire, in go/protocol.

  • cloudwire/app.go: the prologue ftw.cloud.v1:<home>, the now, watch, suspend and snapshot bodies, the close codes, and ValidHome.
  • cloudwire/testdata/cloud-app-vectors.json: one whole session, byte for byte. It has fixed keys on both ends, message 1 with now, message 2 with a snapshot of the signed vectors, and transport messages both ways.
  • appwire/initiator.go: the initiator half of Noise IK. It is checked against the same Cacophony vector as the responder. The service's tests and any Go client use it; Core never runs it.
  • appwire.HandshakeState.RemoteStatic() names the initiator's key once message 1 has proved it. The service needs this to check the phone against the box's roster before message 2 carries anything.

K11: FTW Cloud in hello_ok.

  • While the grant is on, every role gets hello_ok.cloud = {pk, home, boot, sources, loadpoints} over its own Noise session:
    • the box's P-256 key;
    • the home id;
    • the cloud link's boot id;
    • the tables from anonymous ids to driver names and loadpoint ids.
  • The phone checks every envelope FTW Cloud forwards with that key, and learns rule U from this session.
  • The field is nil when the grant is off or revoking, when there is no key, and in floor mode.

cloud.link is in the caps when the build can run the uplink. Release builds have no service key, so they do not advertise it.

What the grant flow needs. POST /api/cloud/stats {policy} and DELETE /api/cloud/stats record and withdraw an owner's consent to statistics.

  • POST uses the grant's gate and is owner only.
  • POST answers 409 when the grant is off or revoking.
  • A statsId is drawn once.
  • The change marks the roster due.
  • No stats messages are sent; that is C7.
  • GET /api/cloud shows the purposes. A viewer gets 403 on every cloud route.

cloudwire.View fixes. The Swift port found two bugs in the reference.

  • Accept reset the view for a new boot, and turned rule N off, before it had decoded the body and checked seq. A signed message that it then dropped could still wipe what the phone held. It now checks everything first.
  • An unknown source state ranked as live when it was the second argument of worse. It now ranks as never on either side.
  • The doc's case 11 has a test now. Case 15 (alerts) is C4's.

The roster's device key is the same X25519 key the phone uses as its Noise static toward the box. enroll.Authorise(noise.RemoteStatic, …) stores it, and the roster copies it unchanged. FTW Cloud admits phones by that key.

Deviations from docs/protocol-cloud.md (#1589) the doc should absorb

  1. The phone names the home in the query, /v1/app?home=<id>. The prologue binds it, so a wrong one fails the handshake.
  2. Message 2 is an unsigned snapshot envelope, {envelopes, lastSeenMs, online, more}. It is not a bare array. Envelopes that do not fit in one Noise message follow as frames, and more counts them.
  3. A phone's watch {hz} holds until it changes or the session closes, not for 60 s. While any phone looks, the service renews the box's watch every 50 s, inside the box's 60 s.
  4. The close codes on /v1/app:
    • 4400: the phone broke the protocol.
    • 4403: any refusal; it says nothing about which check failed.
    • 4408: replaced by a newer session from the same phone.
    • 4409: the roster changed this phone's access; it reconnects at once.
    • 4429: the home is busy, as at the relay.
  5. The phone asks again slowly after 4403, from 7.5 s up to 5 min. A service that just restarted refuses until the box reconnects.
  6. The name tables reflect the drivers and loadpoints known at hello. One added later has no name on the phone until the next session.
  7. Turning statistics on or off sends no push, like a scope change in C2. DELETE drops the statsId at once. C7 must keep it until purged {what: "stats"} arrives.
  8. The grant record gains statsPolicy, because stats carries its own policy.
  9. Case 12 says "every source stale". The reference shows no sources and no fields, because the old table is dropped and the new one is not held yet.
  10. go/protocol generates no role names, so ftw-cloud spells "owner" by hand. A follow-up could generate the registry's roles.

Checks

  • go/protocol: go vet ./... && go test ./....
  • From go/: go test -race ./internal/cloudlink/, then go test ./internal/api/ ./internal/appproto/ ./internal/appuplink/ ./cmd/ftw/. All pass.
  • make protocol-boundary: 135 packages, none from the box.
  • make cloudlink-boundary: 208 packages, none from the box but go/protocol.
  • A changeset covers the user-visible part. Release, version and chore(release): version packages #1584 are untouched.

End to end

The run used a Core from this branch, a local ftwcloudd from srcfl/ftw-cloud#5, a simulated house and the app from srcfl/ftw-app#19 over the production relay. Numbers and screenshots are in srcfl/ftw-app#19.

  • The grant went on over the owner's Noise session through the consent flow.
  • hello_ok.cloud named the key, home and boot.
  • Revoking a phone closed its cloud stream 3–12 ms after the request.
  • Turning FTW Cloud off in the app's About screen got the purge receipt 7 ms later.

🤖 Generated with Claude Code

frahlg and others added 4 commits October 13, 2026 07:18
The phone reads its home from FTW Cloud over Noise IK with the service as
responder. cloudwire gains the prologue, the now, watch, suspend and
snapshot bodies, the close codes, and golden vectors for one whole
session. appwire gains the initiator half of IK, checked against the
same Cacophony vector as the responder, for the service's tests and any
Go client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A responder that admits initiators by key decides before message 2
carries anything. FTW Cloud checks the phone's key against the box's
roster there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cs consent

hello_ok gains an optional `cloud` field for every role while an owner
has FTW Cloud on: the box's FTW Cloud key, the home id, the uplink's boot
id and the tables from opaque source and loadpoint ids to names. A phone
checks what the service forwards with that key, never one the service
gives it. Floor mode leaves it out.

hello_ok says cloud.link when this build names a service. Release builds
name none and stay silent.

POST /api/cloud/stats {policy} adds the stats purpose under a grant
already on (409 when off or revoking), draws a 16-byte statsId once and
keeps it, saves the grant, marks the roster due and writes the audit
line. DELETE /api/cloud/stats removes the purpose and forgets the id. No
statistics are sent yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
View.Accept reset the view for a new boot, and turned rule N off,
before it had decoded the body and checked seq, so a signed message it
then dropped could still wipe what the phone held. It now checks
everything first. An unknown source state ranked as live when it was
the second argument of worse; it ranks as never on either side. Found
by the Swift port. Adds the doc's case 11 and a changeset for C3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frahlg added a commit that referenced this pull request Oct 13, 2026
* docs: propose FTW Cloud (ADR 0008) and its protocol

ADR 0008, status proposed, records Fredrik's decisions of 12 October
2026: a readable, read-only copy of what a home's owner turns on, served
to the iPhone app, its widgets and APNs from ftwcloudd in srcfl/ftw-cloud
on AWS eu-north-1. The box signs every message; the service never
controls and never chooses actions; the relay, escrow and lane 0 do not
change. It states the statistics rules now, because consent comes at
first use, orders the work core first (uplink, cache, first screen),
and records the turn away from #1323 and ADR 0006's single remote path.

docs/protocol-cloud.md is the normative wire format: unpadded lane 1
frames, one signing rule, a handshake bound to the TLS session, a field
table per class and scope, cadence, the closed set the service may send
the box, roster, extension token, push, freshness with eight vectors
described in text, and the new registry names.

VISION.md and AGENTS.md gain the matching paragraph and invariants;
architecture.md and ADR 0006 point to the ADR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(cloud): close freshness gaps found in review

An independent review found ways stale data could still show as live.

- hb now names the newest live and lp it covers (liveSeq, lpSeq) and
  carries each source's reading age, so the phone grades a reading by
  the box's own ladder instead of keeping it live for another S.
- The box sends no hb while a changed live or lp waits, nor past the
  byte budget, so an hb never vouches for values the box has not sent.
- The phone keys its table by boot, tracks seq per class, takes the
  earlier of wallMs and arrival under rule N, adds a drift margin and a
  24 h lifetime under rule U, and shows old alerts without buttons.
- hello now carries the box's TLS exporter value too, so a proxy that
  ends TLS cannot hold a socket for the home; the label starts with
  EXPERIMENTAL as RFC 5705 asks.
- Restores, roster expiry, purge receipts, tiles for phones, Live
  Activity tokens and what Apple sees are now stated; timers agree
  between the ADR and the protocol. Eight new vectors cover the cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(cloud): take in what C2 and C3 built

The protocol was written before the code. #1595 (C2) and
#1603 (C3) each list where the code had to differ. This moves
those differences into the text so it stays normative.

From C2: fieldSources travels only with sources; a roster at every
connect; a deadband on lp's measured values; [] and null on the wire;
session as a plug-in count; manual.reason as the limit reason;
hello.caps as the classes sent; prices from the start of today; the
service signs over the box's home id; no step-up on the grant; a
restore never brings cloud.key back.

From C3: /v1/app?home=<id>; message 2 as an unsigned snapshot
envelope with spill-over frames; a phone's watch holds until it
changes, and the service renews the box's every 50 s; close codes
4400, 4403, 4408, 4409, 4429 and 1013, with the phone's retries;
session limits; hello_ok.cloud (K11); the statistics consent routes,
which push nothing; case 12 shows no sources rather than stale ones;
role names are not generated in go/protocol.

ADR 0008: turning FTW Cloud on or off notifies owners, not every
change; the protocol has sixteen cases, not eight.

TestFieldsMatchTheProtocolDocument passes against this text on the
C3 branch (c6301a0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg

frahlg commented Oct 13, 2026

Copy link
Copy Markdown
Member Author

Included in #1607 (FTW Cloud C2, C3 and C4 joined), squash-merged to master.

@frahlg frahlg closed this Oct 13, 2026
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