From aecbaba35dabbbb5297099dfba70357ccb549e89 Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Wed, 14 Oct 2026 10:14:16 +0200 Subject: [PATCH] feat(contract): role watch and capability cmd.exchange ADR 0009 step 2: the shared registry copied byte for byte from srcfl/ftw-webapp. Contract.swift gains the role and the capability, so ContractTests still matches. Nothing uses them yet; step 9 offers Add watch only when a box says cmd.exchange. Co-Authored-By: Claude Opus 5.5 --- .../FTWKit/Sources/FTWKit/Protocol/Contract.swift | 7 ++++++- protocol/registry.yaml | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/appleApp/FTWKit/Sources/FTWKit/Protocol/Contract.swift b/appleApp/FTWKit/Sources/FTWKit/Protocol/Contract.swift index 689cb35..cd0534f 100644 --- a/appleApp/FTWKit/Sources/FTWKit/Protocol/Contract.swift +++ b/appleApp/FTWKit/Sources/FTWKit/Protocol/Contract.swift @@ -23,17 +23,20 @@ public enum Contract { public static let roleOwner = "owner" public static let roleViewer = "viewer" + public static let roleWatch = "watch" /// What each role carries, with the registry's `*` expanded. Used only /// for a box from before roles, which sends no scope list. public static let roleScopes: [String: [String]] = [ roleOwner: scopes, roleViewer: ["ftw.live.read"], + roleWatch: ["ftw.live.read", "ftw.plan.read", "ftw.events.read", "ftw.dispatch.write"], ] public static let roleLabels: [String: String] = [ roleOwner: "Owner", roleViewer: "Viewer", + roleWatch: "Watch", ] public static let scopeModeWrite = "ftw.mode.write" @@ -44,7 +47,7 @@ public enum Contract { "cmd.lease", "cmd.precondition", "cmd.readback", "der.battery", "der.ev", "der.v2x", "plan.dispatch", "net.webrtc", "price.spot", "api.passthrough", - "cloud.link", + "cloud.link", "cmd.exchange", ] public static let capHistory5m = "history.5m" @@ -55,6 +58,8 @@ public enum Contract { /// The box can run the FTW Cloud uplink. Absent, the app offers no FTW /// Cloud and reads through the relay alone. public static let capCloudLink = "cloud.link" + /// The box can take a watch. Absent, the phone offers no Add watch. + public static let capCmdExchange = "cmd.exchange" /// The scopes an FTW Cloud grant may name, and what its data may be /// used for: the registry's `cloud_scopes` and `cloud_purposes`. diff --git a/protocol/registry.yaml b/protocol/registry.yaml index 5d71d3a..18f3cd5 100644 --- a/protocol/registry.yaml +++ b/protocol/registry.yaml @@ -80,6 +80,10 @@ capabilities: # 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 + # The box can take a watch: it opens the relay's exchange socket and mints + # watch codes. Absent means the phone offers no Add watch. The wire is + # docs/watch-exchange.md in srcfl/ftw-webapp. + - cmd.exchange # --------------------------------------------------------------------------- # Scopes. One object axis, two verb axes: .. @@ -104,6 +108,13 @@ roles: viewer: label: Viewer scopes: [ftw.live.read] + # A watch that an owner phone added. That phone is its parent, and each check + # keeps only the scopes below that the parent holds at that moment, so a + # watch never does more than its phone can do now. Read and charge: no site + # mode, no members, no history and no passthrough. + watch: + label: Watch + scopes: [ftw.live.read, ftw.plan.read, ftw.events.read, ftw.dispatch.write] # --------------------------------------------------------------------------- # FTW Cloud. What an owner can let the box share with FTW Cloud, and why.