Repository navigation
feat(cloudwire): join FTW Cloud C3 and C4; alerts dated by rule U - #1607
Conversation
Copied byte for byte from srcfl/ftw-webapp, which holds the source: scope ftw.events.read, the cloud_scopes and cloud_purposes lists, capability cloud.link, carrier state cloud, and push kinds cloud.granted and cloud.revoked. The generator now writes CloudScopes and CloudPurposes into go/protocol, and the registry header names the file it actually generates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
go/protocol/cloudwire holds what docs/protocol-cloud.md specifies:
the unpadded lane 1 frame, the {t, b, sig} envelope and its ES256
signing rule, home and opaque ids, the message bodies, the scope of
each class and the field allowlist. A test fails when a body grows a
field the allowlist lacks, and compares the allowlist with the
protocol document's tables when the document is present.
Golden vectors in testdata/cloud-vectors.json freeze the bytes for
srcfl/ftw-cloud and the phone. View is the phone's freshness rules as
a reference, with the document's cases as tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
go/internal/cloudlink is the box's uplink to FTW Cloud. It is off until an owner grants it, and a grant whose key fingerprint does not match cloud.key reads as off. - One WebSocket over TLS 1.3 with no proxy. The box sends nothing but hello until the service has signed a hello_ok bound to this TLS session's exporter value and the box's nonce. - Every message is signed with cloud.key and carries seq, boot, uptimeMs and wallMs while NTP has set the clock. - live, lp and prices go on change, at most every 5 s (1 s while watched, lp at least 2 s); a table change goes at once with a new srcRev; hb goes after H and never while a changed live or lp waits. - One writer per socket: a slow socket gets the newest values, nothing queues, and a write blocked for 10 s closes it. Past 20 MB in 24 h of uptime only roster and bye go. - Driver and loadpoint names become opaque ids; capability fields are not in the shapes. The downlink is a closed set of five types, under a fuzz test. - A revoke sends bye with purge and keeps the key until the service's signed receipt arrives. CI runs go list -deps to keep control, API and loadpoint code out of the package's build graph. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- GET /api/cloud and /api/cloud/preview for an owner: the grant, the bytes sent in 24 hours, and the exact frames that left the house. - POST /api/cloud/grant from an owner's session, or on the LAN only from the box itself or with the house password. DELETE from any door. - Enrolment lists whole keys for the signed roster and says when who is paired, or as what, has changed. - The adapters read the live snapshot the app session sends, the loadpoint view GET /api/loadpoints composes and the price store. - Owners hear cloud.granted and cloud.revoked; these two rules are on by default, unlike the rest. - Backups skip cloud.key, so a restored box has FTW Cloud off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A charging car moves its measured power every reading, so lp went at every period: about 12 MB a day on a simulated site, against an estimate of 5 MB. lp now counts power and actual watts as changed only past max(100 W, 5 %), session energy past 100 Wh and charge level past 1 %. A change of state still goes at once, the full lp every 15 minutes carries exact values, and live field 10 carries the charging power at every period. Also say which way a dev root CA file failed to load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sender looked at live, loadpoints and the roster number on every 250 ms tick. On a Pi the loadpoint view composes control feedback, and the roster number is a SQLite read. Now live is read once a second (so a table change still goes within a second), loadpoints once a period, and the roster only when it changed, at connect and every six hours. An hb still reads fresh ages on the tick it goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The service closes the uplink right after it sends purged. The receipt went to the sender through a channel, and the reader could see the close first and end the connection with the receipt unread; the box then had to ask again. The reader now checks the receipt itself and ends the connection only after the key and the grant are gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
FTW Cloud C4 carries notifications. cloudwire gains the event class body (eventId, ts, kind, severity, args, src, lp), the phone's push.register and push.unregister, and the APNs payload the service builds: the same aps for every alert, and the box's envelope unchanged under "ftw". ReadAlert is the reference for what a phone's notification extension makes of a payload: a fresh, anchored, box-signed event of a known kind, the service's own box.unreachable, or the generic line. The catalogue gains charging.goal_at_risk. cloud-vectors.json gains three signed events; push-vectors.json holds fourteen payloads, the phone's clock and the verdict each must get, for the service and the app to replay. 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>
K5 and the Core half of FTW Cloud C4.
- A catalogue message carries its kind, its arguments, the device it
concerns and when it happened (dispatchCatalogue set no kind before).
- Every transport publishes side by side under its own 10 s limit, so a
gateway that hangs costs only itself.
- notifications.CloudPublisher only queues. cloudlink.QueueEvent keeps up
to 32 events; the writer signs each as an event with eventId, ts and
seq, cuts opaque ids for the driver and loadpoint, and drops name
arguments. An event older than ten minutes is dropped and counted in
GET /api/cloud (eventsDropped). The grant's events scope gates it all.
- Link.Bye sends bye{why:"update"} for a restart the box chose, and
bye{why:"shutdown"} on SIGTERM, then stops dialling, so FTW Cloud holds
back box.unreachable through an update.
- charging.goal_at_risk: once per plug-in, from a fresh plan's shortfall
(at least 1 kWh), only on a level that is not assumed and a ready time
still ahead. Opt-in like the other charging rules.
- charging.session_complete for a car that reports nothing: when the
box's estimate from a level someone set reaches the target, in kWh,
once, and not after a restart beside a car that is already done.
- charging.connected waits 90 s and goes only when the level is assumed
or no goal is set.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /charger/{id}/stall makes the simulated charger deliver nothing
while it still reports Charging, as a tripped relay does; /resume lets it
go on. It is what charging.interrupted is for, and the FTW Cloud C4
end-to-end test raises one with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e/cloud-c3c4-core # Conflicts: # go/cmd/ftw/cloud_link.go
A box without NTP sends events with no wallMs, and the phone showed them as the generic line even when it held an uptime offset for that boot. ReadAlert now takes the offset the phone keeps for a home and anchors an event as View anchors a message: the earlier of rule N and rule U, with rule U's drift margin counted against the age. The protocol's Push table already says "anchored", so the text does not change. Four push vectors cover rule U: no box clock, a box clock ahead, an offset from another boot, and one learned over a day ago. -update now keeps stored signatures that still verify, so adding a case leaves the other cases' bytes alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0ff1837088
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| s.handle("GET /api/cloud", Read, s.handleCloudStatus) | ||
| s.handle("GET /api/cloud/preview", Read, s.handleCloudPreview) | ||
| s.handle("POST /api/cloud/grant", Configure, s.handleCloudGrant, NoStepUp) | ||
| s.handle("DELETE /api/cloud/grant", Configure, s.handleCloudRevoke, NoStepUp) |
There was a problem hiding this comment.
Let cloud revocation bypass the outer LAN-auth gate
When api.lan_auth is enabled, a non-loopback LAN request without the house password is rejected by Authenticate before handleCloudRevoke reaches cloudGate, because DELETE requests require mutation protection and these paths are not LAN-auth exemptions. Consequently the documented and tested intent that any local LAN client can narrow access by revoking the grant also fails for DELETE /api/cloud/stats; the current tests call the handlers directly and therefore miss the full middleware path.
Useful? React with 👍 / 👎.
| if settle > 0 { | ||
| time.Sleep(settle) | ||
| } | ||
| f, ok := facts(loadpointID) | ||
| if !ok || !f.Plugged || (f.LevelKnown && f.HasGoal) { |
There was a problem hiding this comment.
Bind delayed plug-in checks to the originating session
The 90-second sleeper remembers only the loadpoint ID, so an unplug followed by a replug before it wakes makes the old goroutine inspect the new session and send its notification. The new plug-in also starts its own sleeper, which can produce a second notification for that same session once the 60-second default cooldown has elapsed—for example, unplugging 70 seconds after the first connection and immediately reconnecting. Capture a session generation or cancel the pending check on disconnect so a prior session cannot notify about a later one.
Useful? React with 👍 / 👎.
| l.mu.Unlock() | ||
|
|
||
| key, err := createKey(l.opts.KeyPath) |
There was a problem hiding this comment.
Serialize first-grant creation
If two owner sessions submit the first grant concurrently, both can observe l.grant == nil and proceed after this unlock. They then share cloud.key.tmp: one request can rename bytes written by the other while retaining its own generated key in memory, persist a fingerprint for that in-memory key, and leave a different key on disk. The link works until restart, when the fingerprint mismatch silently turns FTW Cloud off; a concurrent revoke can likewise return before the still-running grant turns the service back on. Keep first-grant creation and installation under a dedicated operation lock or recheck state before committing.
Useful? React with 👍 / 👎.
srcfl/ftw#1607 landed as b1b34697. go/protocol there is byte for byte what a9e6f66f on the claude/cloud-c3c4-core branch held, so nothing else changes. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Joins FTW Cloud C3 (#1603) and C4 (#1602) in one branch, so srcfl/ftw-cloud and the app can pin a
go/protocolthat has bothcloudwire/app.goandcloudwire/push.go. Adds rule U to the alert reference.What changes
go/cmd/ftw/cloud_link.go: C3'scloudSessionand C4'scloudEventQueuewere added in the same place. Both stay.ReadAlerttakes the rule U offset the phone holds for a home. It dates an event the wayViewdates a message: the earlier of rule N and rule U, and when rule U gives the time, it subtracts the drift margin so the margin counts against the age. Before this change, an event from a box without NTP, or with a clock more than 2 s ahead, always showed the generic line. Now it gets its words and buttons when the phone has learned an offset over Noise. The protocol's Push table already says "anchored", so the doc stays as it is.-updatenow keeps stored signatures that still verify, so the 14 old cases keep their bytes and the file only grows.Stack
Draft, stacked on #1595 → #1603 + #1602. When they land, rebase this branch; the diff then shrinks to a9e6f66. Pairs: srcfl/ftw-cloud#7 pins this commit, and srcfl/ftw-app#20 ports rule U to
PushAlertand replays the four vectors.Checks
go/protocol:go vet ./... && go test ./...pass.go:go test -race ./cmd/ftw ./internal/cloudlink ./internal/notifications ./internal/loadpointpass.gofmt -lflagsgo/protocol/cloudwire/vectors_test.go. The C3 and C4 heads both have the same problem, so I left it alone.End to end, with Core from this branch, see srcfl/ftw-cloud#7: a real app session registered its token, and a
charging.connectedfrom Core reached the simulator through Apple's sandbox, 703 ms after Core sent it.🤖 Generated with Claude Code