Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ftw-cloud-uplink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ftw": patch
---

Add the FTW Cloud uplink, off unless an owner turns it on. `GET /api/cloud` shows the grant and the bytes sent in the last 24 hours, `GET /api/cloud/preview` shows the exact bytes that leave the house, and `POST` and `DELETE /api/cloud/grant` turn it on and off. Release builds name no FTW Cloud service yet, so the box refuses a grant and never dials. Backups no longer include `cloud.key`.
24 changes: 24 additions & 0 deletions .github/scripts/check-cloudlink-imports.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# go/internal/cloudlink streams the box's state to FTW Cloud, and FTW Cloud
# reads and never controls. This check keeps the uplink's build graph honest:
# it may import go/protocol and nothing else of the box's own, so no control,
# API, loadpoint, planner or driver code can be reached from the code that
# talks to the service. Its tests are held to the same rule.

set -euo pipefail

cd "${1:-go}"

reached=$(go list -deps -test -f '{{.ImportPath}}' ./internal/cloudlink | awk '
$1 ~ /^github\.com\/srcfl\/ftw\/go\// &&
$1 !~ /^github\.com\/srcfl\/ftw\/go\/protocol(\/|$)/ &&
$1 !~ /^github\.com\/srcfl\/ftw\/go\/internal\/cloudlink(\.test| |$)/ { print $1 }
' | sort -u)

if [ -n "${reached}" ]; then
echo "go/internal/cloudlink may import only go/protocol from the box. It reaches:" >&2
printf '%s\n' "${reached}" >&2
exit 1
fi

echo "go/internal/cloudlink: $(go list -deps ./internal/cloudlink | wc -l | tr -d ' ') packages, none from the box but go/protocol"
4 changes: 4 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ jobs:
go test -count=1 ./...
- name: Protocol module imports nothing from the box
run: bash .github/scripts/check-protocol-boundary.sh go/protocol
# FTW Cloud reads and never controls. The uplink's build graph says so:
# it reaches no control, API or loadpoint code.
- name: FTW Cloud uplink imports nothing that controls
run: bash .github/scripts/check-cloudlink-imports.sh go

web:
name: web
Expand Down
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

.PHONY: help test compose-migration-test container-boundary-test release-workflow-test build build-arm64 build-amd64 release release-linux release-linux-arm64 release-linux-amd64 \
run-sim sim-ocpp dev fmt vet clean e2e ci ci-ui ci-hw-pi docs \
verify verify-all install-hooks driver-repository-validate driver-versions protocol-boundary \
verify verify-all install-hooks driver-repository-validate driver-versions protocol-boundary cloudlink-boundary \
drivers drivers-present driver-versions-across-pin

VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
Expand Down Expand Up @@ -48,6 +48,7 @@ help:
@echo " ci-hw-pi deploy candidate to Pi CI slot + browser smoke"
@echo " fmt vet Go format + static checks"
@echo " protocol-boundary prove go/protocol imports nothing from the box"
@echo " cloudlink-boundary prove the FTW Cloud uplink reaches no control code"
@echo " drivers fetch drivers/ from the pinned device-drivers commit"
@echo " clean nuke build artifacts"

Expand Down Expand Up @@ -147,7 +148,7 @@ ci-hw-pi:
#
# verify-all checks both supported Linux targets on the developer host.

verify: test compose-migration-test container-boundary-test release-workflow-test native-solver-test protocol-boundary
verify: test compose-migration-test container-boundary-test release-workflow-test native-solver-test protocol-boundary cloudlink-boundary
cd go && go vet -tags=$(GO_TAGS) ./...
cd go/protocol && go vet ./...
cd go && go build -tags=$(GO_TAGS) ./...
Expand Down Expand Up @@ -247,6 +248,9 @@ vet:
protocol-boundary:
bash .github/scripts/check-protocol-boundary.sh go/protocol

cloudlink-boundary:
bash .github/scripts/check-cloudlink-imports.sh go

clean:
rm -rf bin release
cd go && go clean
Expand Down
9 changes: 9 additions & 0 deletions contract/push-catalogue.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,12 @@ events:
# has. The box encrypts it before it ever leaves home.
title: Your box is out of reach
body: "It stopped answering. Power cut, or the internet at home is down."
- kind: cloud.granted
# To every owner, when an owner turns FTW Cloud on. Sharing a readable
# copy of the house is a choice every owner should hear about.
title: FTW Cloud is on
body: "Your box now shares a copy of its readings with FTW Cloud. You can turn it off in the app or on the box."
- kind: cloud.revoked
# To every owner, when FTW Cloud is turned off at either door.
title: FTW Cloud is off
body: "Your box stopped sharing with FTW Cloud and asked it to delete its copy."
27 changes: 24 additions & 3 deletions contract/registry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
# The single source for every name shared between this app and the box, and
# the same file in both repositories — byte for byte.
#
# In srcfl/ftw it generates Go constants: go/internal/appproto/contract_gen.go
# and go/internal/apiauth/contract_gen.go, from `go generate ./internal/...`.
# In srcfl/ftw it generates Go constants: go/protocol/contract_gen.go and
# go/internal/apiauth/contract_gen.go, from `go generate ./internal/...`.
# In srcfl/ftw-webapp there is no generator — src/lib/protocol/contract.ts and
# the error table in src/lib/protocol/messages.ts are written by hand and read
# back against this file by tests/registry-contract.test.ts.
Expand Down Expand Up @@ -76,6 +76,10 @@ capabilities:
# history.5m. Present does not mean every path is reachable: reads and
# configuration go through, anything that moves energy stays on cmd.
- api.passthrough
# The box can run the FTW Cloud uplink. Absent means the app uses the relay
# only and offers no FTW Cloud. It says nothing about whether the owner
# turned FTW Cloud on; the box's grant decides that.
- cloud.link

# ---------------------------------------------------------------------------
# Scopes. One object axis, two verb axes: <object>.<read|write>.
Expand All @@ -87,6 +91,7 @@ scopes:
- { name: ftw.plan.read, desc: Dispatch plan and its reasoning }
- { name: ftw.health.read, desc: Box and driver health }
- { name: ftw.assets.read, desc: Devices and their capabilities }
- { name: ftw.events.read, desc: Events behind notifications }
- { name: ftw.dispatch.write, desc: Control energy assets }
- { name: ftw.mode.write, desc: Change site operating mode }
- { name: ftw.members.read, desc: See who has access }
Expand All @@ -100,6 +105,22 @@ roles:
label: Viewer
scopes: [ftw.live.read]

# ---------------------------------------------------------------------------
# FTW Cloud. What an owner can let the box share with FTW Cloud, and why.
# docs/protocol-cloud.md in srcfl/ftw is the wire format.
#
# cloud_scopes are the scopes a grant may name: the box sends a class only
# when the grant holds its scope, and a device reads it only when its own
# role carries the scope too. ftw.assets.read is left out on purpose: the
# box sends no device inventory, names or capabilities to FTW Cloud.
#
# cloud_purposes are what the data may be used for. serve is the owner's own
# phones and widgets. stats is household statistics, a separate consent that
# is off unless the owner turns it on.
# ---------------------------------------------------------------------------
cloud_scopes: [ftw.live.read, ftw.history.read, ftw.plan.read, ftw.health.read, ftw.events.read]
cloud_purposes: [serve, stats]

# ---------------------------------------------------------------------------
# Command operations. What a `cmd` frame may name, and the scope the grant
# must carry before the box acts on it.
Expand Down Expand Up @@ -219,7 +240,7 @@ client_errors:
# the two into one enum cannot express "connected, but the inverter is quiet".
# ---------------------------------------------------------------------------
source_states: [live, lagging, stale, down, never]
carrier_states: [webrtc, relay, cache, none]
carrier_states: [webrtc, relay, cloud, cache, none]

# ---------------------------------------------------------------------------
# History resolutions backed by what the box actually stores.
Expand Down
Loading
Loading