From afe48c7b52ff98fe27fc4bd64b6a2db9fb54c29e Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Sun, 18 Oct 2026 18:10:01 +0200 Subject: [PATCH 1/2] Make MCP setup an explicit owner choice in Access --- appleApp/FTW/Access/GrantView.swift | 23 +++++++++++++++---- appleApp/FTW/Access/OfferFlowView.swift | 14 +++++++++-- .../Sources/FTWDemo/SimulatedAccess.swift | 4 ++-- .../Sources/FTWKit/Access/AccessModel.swift | 5 ++-- .../Sources/FTWKit/Access/AccessWire.swift | 1 + .../Sources/FTWKit/Access/AccessWords.swift | 16 +++++++++++-- .../Sources/FTWKit/Access/OfferFlow.swift | 5 +++- .../Sources/FTWKit/Protocol/Contract.swift | 4 +++- .../Tests/FTWKitTests/AccessFlowTests.swift | 20 +++++++++++++++- protocol/registry.yaml | 16 +++++++++++-- 10 files changed, 90 insertions(+), 18 deletions(-) diff --git a/appleApp/FTW/Access/GrantView.swift b/appleApp/FTW/Access/GrantView.swift index e9badf91..8c0f3d21 100644 --- a/appleApp/FTW/Access/GrantView.swift +++ b/appleApp/FTW/Access/GrantView.swift @@ -10,6 +10,7 @@ struct GrantView: View { let id: String @State private var scopes: Set = [] @State private var control = false + @State private var setup = false @State private var lengthMs: Int64 = 0 @State private var loaded = false @Environment(\.dismiss) private var dismiss @@ -26,15 +27,16 @@ struct GrantView: View { } private func load(_ row: AgentRow) { - scopes = Set(row.scopes).subtracting(AccessWire.controlScopes) + scopes = Set(row.scopes).subtracting(AccessWire.controlScopes).subtracting([AccessWire.scopeSetup]) control = row.hasControl + setup = row.scopes.contains(AccessWire.scopeSetup) lengthMs = 0 loaded = true } private func form(_ row: AgentRow) -> some View { let now = Date().timeIntervalSince1970 * 1000 - let reads = (Contract.agentRoleScopes[row.role] ?? []).filter { !AccessWire.controlScopes.contains($0) } + let reads = (Contract.agentRoleScopes[row.role] ?? []).filter { !AccessWire.controlScopes.contains($0) && $0 != AccessWire.scopeSetup } let lengths = Grant.lengths(role: row.role, boxMs: access.boxLongestMs(row.role)) return List { Section { @@ -73,6 +75,16 @@ struct GrantView: View { } } + if row.role != Contract.roleRelease { + Section { + Toggle(AccessWords.text("Allow setup"), isOn: $setup) + .tint(Theme.live) + .accessibilityIdentifier("grant.setup") + } footer: { + Text(AccessWords.text("Off until you choose it. This includes equipment names and connection settings. Review and confirm the exact draft before it saves, and confirm any restart separately. Equipment settings can affect operation; the box keeps its safety checks.")) + } + } + if row.role != Contract.roleRelease { let canTurnOn = row.hasControl || !(access.asked(row)?.askedOps.isEmpty ?? true) Section { @@ -84,7 +96,7 @@ struct GrantView: View { if !canTurnOn { Text(AccessWords.text("Add it again from its page to let it control.")) } else { - Text(control ? AccessWords.control(g.caps, scopes: g.scopes) : AccessWords.text("Off: it reads and changes nothing.")) + Text(control ? AccessWords.control(g.caps, scopes: g.scopes) : AccessWords.text(setup ? "Short control actions are off." : "Off: it reads and changes nothing.")) } } if control { @@ -132,7 +144,7 @@ struct GrantView: View { } } .buttonStyle(FTWButtonStyle(kind: .primary)) - .disabled(scopes.isEmpty || access.working != nil) + .disabled(grant(row).scopes.isEmpty || access.working != nil) .padding(16) .background(Theme.surface) .transition(.move(edge: .bottom).combined(with: .opacity)) @@ -147,6 +159,7 @@ struct GrantView: View { /// phone approved it, never more; control off drops both. private func grant(_ row: AgentRow) -> Grant { var all = scopes + if setup { all.insert(AccessWire.scopeSetup) } var ops = row.ops var caps = row.caps if control, row.hasControl { @@ -169,6 +182,6 @@ struct GrantView: View { } private func changed(_ row: AgentRow) -> Bool { - scopes != Set(row.scopes).subtracting(AccessWire.controlScopes) || control != row.hasControl || lengthMs != 0 + scopes != Set(row.scopes).subtracting(AccessWire.controlScopes).subtracting([AccessWire.scopeSetup]) || setup != row.scopes.contains(AccessWire.scopeSetup) || control != row.hasControl || lengthMs != 0 } } diff --git a/appleApp/FTW/Access/OfferFlowView.swift b/appleApp/FTW/Access/OfferFlowView.swift index 13df331e..e2b6808d 100644 --- a/appleApp/FTW/Access/OfferFlowView.swift +++ b/appleApp/FTW/Access/OfferFlowView.swift @@ -145,11 +145,21 @@ struct OfferReview: View { } Section(AccessWords.text("It may")) { - ForEach(AccessWords.ordered(grant.scopes.filter { !AccessWire.controlScopes.contains($0) }), id: \.self) { scope in + ForEach(AccessWords.ordered(grant.scopes.filter { !AccessWire.controlScopes.contains($0) && $0 != AccessWire.scopeSetup }), id: \.self) { scope in Label(AccessWords.scope(scope), systemImage: "eye") } } + if offer.scopes.contains(AccessWire.scopeSetup) { + Section { + Toggle(AccessWords.text("Allow setup"), isOn: $flow.setup) + .tint(Theme.live) + .accessibilityIdentifier("offer.setup") + } footer: { + Text(AccessWords.text("Off until you choose it. This includes equipment names and connection settings. Review and confirm the exact draft before it saves, and confirm any restart separately. Equipment settings can affect operation; the box keeps its safety checks.")) + } + } + if offer.asksControl { Section { Toggle(AccessWords.text("Also let it control"), isOn: $flow.control) @@ -157,7 +167,7 @@ struct OfferReview: View { .accessibilityIdentifier("offer.control") } footer: { let asked = Grant.from(offer, control: true, lengthMs: flow.lengthMs) - Text(flow.control ? AccessWords.control(asked.caps, scopes: asked.scopes) : AccessWords.text("Off: it reads and changes nothing.")) + Text(flow.control ? AccessWords.control(asked.caps, scopes: asked.scopes) : AccessWords.text(flow.setup ? "Short control actions are off." : "Off: it reads and changes nothing.")) } if flow.control { Section(AccessWords.text("With control it may")) { diff --git a/appleApp/FTWKit/Sources/FTWDemo/SimulatedAccess.swift b/appleApp/FTWKit/Sources/FTWDemo/SimulatedAccess.swift index a9320598..407651f6 100644 --- a/appleApp/FTWKit/Sources/FTWDemo/SimulatedAccess.swift +++ b/appleApp/FTWKit/Sources/FTWDemo/SimulatedAccess.swift @@ -247,7 +247,7 @@ public final class SimulatedAccess { o.clientId = AccessWire.supportClientID o.clientHost = "cloud.ftw.energy" o.verified = true - o.scopes = ["ftw.config.read", "ftw.diag.run", "ftw.dispatch.hold", "ftw.events.read", "ftw.goals.request", "ftw.health.read", "ftw.history.read", "ftw.live.read", "ftw.logs.read", "ftw.plan.read"] + o.scopes = [AccessWire.scopeSetup, "ftw.config.read", "ftw.diag.run", "ftw.dispatch.hold", "ftw.events.read", "ftw.goals.request", "ftw.health.read", "ftw.history.read", "ftw.live.read", "ftw.logs.read", "ftw.plan.read"] o.ticket = ticket o.ttlS = UInt64(AccessWire.longestMs(Contract.roleSupport) / 1000) } else { @@ -256,7 +256,7 @@ public final class SimulatedAccess { o.clientId = "https://claude.ai/oauth/mcp-oauth-client-metadata" o.clientHost = "claude.ai" o.verified = true - o.scopes = ["ftw.dispatch.hold", "ftw.events.read", "ftw.goals.request", "ftw.health.read", "ftw.history.read", "ftw.live.read", "ftw.plan.read"] + o.scopes = [AccessWire.scopeSetup, "ftw.config.read", "ftw.diag.run", "ftw.dispatch.hold", "ftw.events.read", "ftw.goals.request", "ftw.health.read", "ftw.history.read", "ftw.live.read", "ftw.plan.read"] o.ttlS = UInt64(AccessWire.longestMs(Contract.roleAgent) / 1000) } o.ops = ["car.goal.set", "loadpoint.hold", "planner.prefs.set", "site.mode.set"] diff --git a/appleApp/FTWKit/Sources/FTWKit/Access/AccessModel.swift b/appleApp/FTWKit/Sources/FTWKit/Access/AccessModel.swift index 598d5bb1..bcd29854 100644 --- a/appleApp/FTWKit/Sources/FTWKit/Access/AccessModel.swift +++ b/appleApp/FTWKit/Sources/FTWKit/Access/AccessModel.swift @@ -108,10 +108,11 @@ public struct Grant: Equatable, Sendable { /// What the owner grants from an offer: what it reads, and control only /// when ticked. Never more than it asks, and never past the box's own /// limits. - public static func from(_ offer: GrantOffer, control: Bool, lengthMs: Int64, boxMs: Int64? = nil) -> Grant { + public static func from(_ offer: GrantOffer, control: Bool, lengthMs: Int64, boxMs: Int64? = nil, setup: Bool = false) -> Grant { let allowed = Set(Contract.agentRoleScopes[offer.role] ?? []) let asked = Set(offer.scopes).intersection(allowed) - let scopes = control ? asked : asked.subtracting(AccessWire.controlScopes) + var scopes = (control ? asked : asked.subtracting(AccessWire.controlScopes)).subtracting([AccessWire.scopeSetup]) + if setup, asked.contains(AccessWire.scopeSetup) { scopes.insert(AccessWire.scopeSetup) } let ops = offer.ops.filter { op in Contract.agentOps[op].map(scopes.contains) ?? false } var caps = Caps(perMin: min(offer.caps.perMin, 60)) if scopes.contains(AccessWire.scopeHold) { diff --git a/appleApp/FTWKit/Sources/FTWKit/Access/AccessWire.swift b/appleApp/FTWKit/Sources/FTWKit/Access/AccessWire.swift index 680008c8..e5da14bd 100644 --- a/appleApp/FTWKit/Sources/FTWKit/Access/AccessWire.swift +++ b/appleApp/FTWKit/Sources/FTWKit/Access/AccessWire.swift @@ -42,6 +42,7 @@ public enum AccessWire { } /// The scopes an owner ticks "control" to add. + public static let scopeSetup = "ftw.setup.write" public static let scopeHold = "ftw.dispatch.hold" public static let scopeRequest = "ftw.goals.request" public static let controlScopes: Set = [scopeHold, scopeRequest] diff --git a/appleApp/FTWKit/Sources/FTWKit/Access/AccessWords.swift b/appleApp/FTWKit/Sources/FTWKit/Access/AccessWords.swift index 35f1533f..6e212e64 100644 --- a/appleApp/FTWKit/Sources/FTWKit/Access/AccessWords.swift +++ b/appleApp/FTWKit/Sources/FTWKit/Access/AccessWords.swift @@ -93,7 +93,8 @@ public enum AccessWords { "ftw.events.read": "See events", "ftw.logs.read": "Read the box's log, with names taken out", "ftw.config.read": "Read the settings, with names and secrets taken out", - "ftw.diag.run": "Run checks that change nothing", + "ftw.diag.run": "Read full diagnostics, source, logs and backups, which can contain saved credentials", + "ftw.setup.write": "Save confirmed setup and restart to apply it when you confirm", "ftw.dispatch.hold": "Charge the car for a short while", "ftw.goals.request": "Ask you for lasting changes", ] @@ -107,7 +108,10 @@ public enum AccessWords { /// One line for a grant: "Reads" or "Reads and controls". public static func summary(_ scopes: [String]) -> String { - AccessWire.controlScopes.isDisjoint(with: scopes) ? text("Reads") : text("Reads and controls") + if scopes.contains(AccessWire.scopeSetup) { + return text(AccessWire.controlScopes.isDisjoint(with: scopes) ? "Reads and setup" : "Reads, setup and control") + } + return AccessWire.controlScopes.isDisjoint(with: scopes) ? text("Reads") : text("Reads and controls") } /// What control means for these caps: how long and how hard it may @@ -486,6 +490,14 @@ public enum AccessWords { "May": "Får", "Reads": "Läser", "Reads and controls": "Läser och styr", + "Reads and setup": "Läser och konfigurerar", + "Reads, setup and control": "Läser, konfigurerar och styr", + "Read full diagnostics, source, logs and backups, which can contain saved credentials": "Läsa full diagnostik, källkod, loggar och säkerhetskopior som kan innehålla sparade inloggningsuppgifter", + "Save confirmed setup and restart to apply it when you confirm": "Spara bekräftad konfiguration och starta om när du godkänner det", + "Allow setup": "Tillåt konfigurering", + "Off until you choose it. This includes equipment names and connection settings. Review and confirm the exact draft before it saves, and confirm any restart separately. Equipment settings can affect operation; the box keeps its safety checks.": "Av tills du väljer det. Det omfattar utrustningens namn och anslutningsinställningar. Granska och bekräfta det exakta utkastet före sparning och godkänn varje omstart för sig. Utrustningens inställningar kan påverka driften; boxens säkerhetskontroller gäller.", + "Short control actions are off.": "Tillfälliga styråtgärder är avstängda.", + "Control": "Styra", "Also let it control": "Låt den också styra", "See live readings": "Se mätvärden just nu", diff --git a/appleApp/FTWKit/Sources/FTWKit/Access/OfferFlow.swift b/appleApp/FTWKit/Sources/FTWKit/Access/OfferFlow.swift index 73eac381..ce1b305a 100644 --- a/appleApp/FTWKit/Sources/FTWKit/Access/OfferFlow.swift +++ b/appleApp/FTWKit/Sources/FTWKit/Access/OfferFlow.swift @@ -34,6 +34,8 @@ public final class OfferFlow { public var typed = "" /// Control is a separate switch, off until ticked. public var control = false + /// Setup needs its own explicit choice, even when control is on. + public var setup = false /// FTW Cloud does not know this client: the owner says they started /// this and trust its address before Face ID. public var trustsUnknown = false @@ -156,7 +158,7 @@ public final class OfferFlow { /// What the owner has chosen so far, within what the box takes. public func grant(_ offer: GrantOffer, boxMs: Int64? = nil) -> Grant { - Grant.from(offer, control: control, lengthMs: lengthMs, boxMs: boxMs) + Grant.from(offer, control: control, lengthMs: lengthMs, boxMs: boxMs, setup: setup) } /// The lengths to pick from, within the offer and what the box takes. @@ -176,6 +178,7 @@ public final class OfferFlow { private func review(_ offer: GrantOffer) { lengthMs = Grant.startLength(role: offer.role, in: Grant.lengths(role: offer.role, offeredS: offer.ttlS)) control = false + setup = false trustsUnknown = false step = .review(offer) } diff --git a/appleApp/FTWKit/Sources/FTWKit/Protocol/Contract.swift b/appleApp/FTWKit/Sources/FTWKit/Protocol/Contract.swift index 384d6d14..2a3b5f9e 100644 --- a/appleApp/FTWKit/Sources/FTWKit/Protocol/Contract.swift +++ b/appleApp/FTWKit/Sources/FTWKit/Protocol/Contract.swift @@ -24,6 +24,7 @@ public enum Contract { "ftw.logs.read", "ftw.config.read", "ftw.diag.run", + "ftw.setup.write", "ftw.dispatch.hold", "ftw.goals.request", "ftw.update.request", @@ -65,7 +66,7 @@ public enum Contract { ] static let agentScopes = [ "ftw.live.read", "ftw.history.read", "ftw.plan.read", "ftw.health.read", "ftw.events.read", - "ftw.logs.read", "ftw.config.read", "ftw.diag.run", "ftw.dispatch.hold", "ftw.goals.request", + "ftw.logs.read", "ftw.config.read", "ftw.diag.run", "ftw.setup.write", "ftw.dispatch.hold", "ftw.goals.request", ] /// Each scope's description, from the registry. Words for an owner come @@ -84,6 +85,7 @@ public enum Contract { "ftw.logs.read": "Log excerpts the box redacts", "ftw.config.read": "Configuration the box redacts", "ftw.diag.run": "Full technical diagnostics and read API", + "ftw.setup.write": "Save confirmed system setup and restart to apply it after user confirmation", "ftw.dispatch.hold": "Hold energy assets for a capped time", "ftw.goals.request": "Ask an owner to change a durable goal", "ftw.update.request": "Ask an owner to install a version", diff --git a/appleApp/FTWKit/Tests/FTWKitTests/AccessFlowTests.swift b/appleApp/FTWKit/Tests/FTWKitTests/AccessFlowTests.swift index dd760730..9d879dc0 100644 --- a/appleApp/FTWKit/Tests/FTWKitTests/AccessFlowTests.swift +++ b/appleApp/FTWKit/Tests/FTWKitTests/AccessFlowTests.swift @@ -150,7 +150,8 @@ import FTWDemo #expect(w.keyring.unlocks == 1) #expect(w.box.access.keys[AccessModel.demoFor.deviceID] != nil) #expect(w.access.boxHasKey) - #expect(added.scopes == ["ftw.events.read", "ftw.health.read", "ftw.history.read", "ftw.live.read", "ftw.plan.read"]) + #expect(added.scopes == ["ftw.config.read", "ftw.diag.run", "ftw.events.read", "ftw.health.read", "ftw.history.read", "ftw.live.read", "ftw.plan.read"]) + #expect(!added.scopes.contains(AccessWire.scopeSetup)) #expect(added.ops.isEmpty) #expect(added.principal.clientId == "https://claude.ai/oauth/mcp-oauth-client-metadata") // Within the role's longest, as the box holds it. @@ -604,6 +605,23 @@ import FTWDemo } /// Every word an owner reads has its Swedish. + @Test func setupNeedsItsOwnChoiceEvenWithControl() { + var offer = GrantOffer() + offer.role = Contract.roleAgent + offer.scopes = ["ftw.config.read", AccessWire.scopeHold, AccessWire.scopeSetup] + offer.ttlS = 30 * 86_400 + offer.caps = Caps(holdS: 300, maxW: 3000, perMin: 6) + let reading = Grant.from(offer, control: false, lengthMs: 86_400_000) + let controlling = Grant.from(offer, control: true, lengthMs: 86_400_000) + #expect(!reading.scopes.contains(AccessWire.scopeSetup)) + #expect(!controlling.scopes.contains(AccessWire.scopeSetup)) + let setup = Grant.from(offer, control: false, lengthMs: 86_400_000, setup: true) + #expect(setup.scopes.contains(AccessWire.scopeSetup)) + #expect(!setup.scopes.contains(AccessWire.scopeHold)) + offer.scopes.removeAll { $0 == AccessWire.scopeSetup } + #expect(!Grant.from(offer, control: false, lengthMs: 86_400_000, setup: true).scopes.contains(AccessWire.scopeSetup)) + } + @Test func everyWordHasItsSwedish() { for scope in Contract.agentRoleScopes[Contract.roleAgent]! { #expect(AccessWords.sv[AccessWords.scopeWords[scope] ?? ""] != nil, "\(scope)") diff --git a/protocol/registry.yaml b/protocol/registry.yaml index 0ae5357a..b8c70b46 100644 --- a/protocol/registry.yaml +++ b/protocol/registry.yaml @@ -142,6 +142,7 @@ scopes: - { name: ftw.logs.read, desc: Log excerpts the box redacts } - { name: ftw.config.read, desc: Configuration the box redacts } - { name: ftw.diag.run, desc: Full technical diagnostics and read API } + - { name: ftw.setup.write, desc: Save confirmed system setup and restart to apply it after user confirmation } - { name: ftw.dispatch.hold, desc: Hold energy assets for a capped time } - { name: ftw.goals.request, desc: Ask an owner to change a durable goal } # Reserved: no op asks for an update yet, and no role but owner carries it. @@ -173,10 +174,10 @@ roles: # is the release check, which reads a health summary and nothing else. agent: label: Assistant - scopes: [ftw.live.read, ftw.history.read, ftw.plan.read, ftw.health.read, ftw.events.read, ftw.logs.read, ftw.config.read, ftw.diag.run, ftw.dispatch.hold, ftw.goals.request] + scopes: [ftw.live.read, ftw.history.read, ftw.plan.read, ftw.health.read, ftw.events.read, ftw.logs.read, ftw.config.read, ftw.diag.run, ftw.setup.write, ftw.dispatch.hold, ftw.goals.request] support: label: Sourceful support - scopes: [ftw.live.read, ftw.history.read, ftw.plan.read, ftw.health.read, ftw.events.read, ftw.logs.read, ftw.config.read, ftw.diag.run, ftw.dispatch.hold, ftw.goals.request] + scopes: [ftw.live.read, ftw.history.read, ftw.plan.read, ftw.health.read, ftw.events.read, ftw.logs.read, ftw.config.read, ftw.diag.run, ftw.setup.write, ftw.dispatch.hold, ftw.goals.request] release: label: Release check scopes: [ftw.health.read] @@ -510,3 +511,14 @@ charger_car_fields: - { name: carCorrected, kind: bool, unit: null, desc: A better rule replaced the car the box first named } - { name: carCorrectedFrom, kind: text, unit: null, desc: The car the box named before that } - { name: choose, kind: text_list, unit: null, desc: While asking the cars a tap may pick. At most four } + +# Agent setup calls use the box's typed draft service. They do not carry an +# HTTP path or grant general configuration or membership authority. +agent_calls: + - { msg: setup.schema, const: MsgSetupSchema, scope: ftw.diag.run, desc: Read this build's complete setup schema } + - { msg: setup.status, const: MsgSetupStatus, scope: ftw.diag.run, desc: Resume the masked setup draft } + - { msg: setup.edit, const: MsgSetupEdit, scope: ftw.setup.write, desc: Patch the exact current setup draft } + - { msg: setup.reset, const: MsgSetupReset, scope: ftw.setup.write, desc: Start a new draft from current settings } + - { msg: setup.validate, const: MsgSetupValidate, scope: ftw.setup.write, desc: Check the exact setup draft without applying it } + - { msg: setup.commit, const: MsgSetupCommit, scope: ftw.setup.write, desc: Save the exact checked and confirmed draft } + - { msg: setup.restart, const: MsgSetupRestart, scope: ftw.setup.write, desc: Restart to apply the exact saved setup after user confirmation } From 6393e3401df1976421e6ae682bbda4f00d271e2a Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Sun, 18 Oct 2026 20:22:28 +0200 Subject: [PATCH 2/2] Describe only the rights an assistant holds --- .../Sources/FTWKit/Access/AccessWords.swift | 21 +++++++++++++++---- .../FTWKit/Tests/FTWKitTests/OfferTests.swift | 19 +++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/appleApp/FTWKit/Sources/FTWKit/Access/AccessWords.swift b/appleApp/FTWKit/Sources/FTWKit/Access/AccessWords.swift index 6e212e64..701f0962 100644 --- a/appleApp/FTWKit/Sources/FTWKit/Access/AccessWords.swift +++ b/appleApp/FTWKit/Sources/FTWKit/Access/AccessWords.swift @@ -106,12 +106,21 @@ public enum AccessWords { return scopes.sorted { (order.firstIndex(of: $0) ?? 99) < (order.firstIndex(of: $1) ?? 99) } } - /// One line for a grant: "Reads" or "Reads and controls". + /// One line for the rights this grant actually holds. public static func summary(_ scopes: [String]) -> String { - if scopes.contains(AccessWire.scopeSetup) { - return text(AccessWire.controlScopes.isDisjoint(with: scopes) ? "Reads and setup" : "Reads, setup and control") + let setup = scopes.contains(AccessWire.scopeSetup) + let control = !AccessWire.controlScopes.isDisjoint(with: scopes) + let reads = scopes.contains { $0 != AccessWire.scopeSetup && !AccessWire.controlScopes.contains($0) } + switch (reads, setup, control) { + case (true, true, true): return text("Reads, setup and control") + case (true, true, false): return text("Reads and setup") + case (true, false, true): return text("Reads and controls") + case (true, false, false): return text("Reads") + case (false, true, true): return text("Configures and controls") + case (false, true, false): return text("Configures") + case (false, false, true): return text("Controls") + case (false, false, false): return text("No access") } - return AccessWire.controlScopes.isDisjoint(with: scopes) ? text("Reads") : text("Reads and controls") } /// What control means for these caps: how long and how hard it may @@ -490,6 +499,10 @@ public enum AccessWords { "May": "Får", "Reads": "Läser", "Reads and controls": "Läser och styr", + "Configures and controls": "Konfigurerar och styr", + "Configures": "Konfigurerar", + "Controls": "Styr", + "No access": "Ingen åtkomst", "Reads and setup": "Läser och konfigurerar", "Reads, setup and control": "Läser, konfigurerar och styr", "Read full diagnostics, source, logs and backups, which can contain saved credentials": "Läsa full diagnostik, källkod, loggar och säkerhetskopior som kan innehålla sparade inloggningsuppgifter", diff --git a/appleApp/FTWKit/Tests/FTWKitTests/OfferTests.swift b/appleApp/FTWKit/Tests/FTWKitTests/OfferTests.swift index 43f25578..faba4c37 100644 --- a/appleApp/FTWKit/Tests/FTWKitTests/OfferTests.swift +++ b/appleApp/FTWKit/Tests/FTWKitTests/OfferTests.swift @@ -14,6 +14,25 @@ import Testing v = try ApprovalVectors.load().offer } + @Test func grantSummaryNamesOnlyItsRights() { + let read = "ftw.live.read" + let setup = AccessWire.scopeSetup + let control = AccessWire.scopeHold + let cases: [([String], String)] = [ + ([], "No access"), ([setup], "Configures"), ([control], "Controls"), + ([setup, control], "Configures and controls"), ([read], "Reads"), + ([read, setup], "Reads and setup"), ([read, control], "Reads and controls"), + ([read, setup, control], "Reads, setup and control"), + ] + for (scopes, expected) in cases { + #expect(AccessWords.summary(scopes) == AccessWords.text(expected)) + } + #expect(AccessWords.sv["Configures"] == "Konfigurerar") + #expect(AccessWords.sv["Configures and controls"] == "Konfigurerar och styr") + #expect(AccessWords.sv["Controls"] == "Styr") + #expect(AccessWords.sv["No access"] == "Ingen åtkomst") + } + var pinned: OfferKeys { OfferKeys(pins: ["\(v.kid):\(Base64url.encode(Bytes(hex: v.pubHex)))"]) } var signed: SignedOffer { get throws { try SignedOffer(base64url: Base64url.encode(Bytes(hex: v.signedOfferHex))) } }