macOS/onboarding: add one-time security acknowledgement

This commit is contained in:
ImLukeF 2026-03-18 17:30:13 +11:00
parent fbbca10c83
commit 5f2f9a05a3
No known key found for this signature in database
6 changed files with 96 additions and 1 deletions

View File

@ -6,6 +6,7 @@ let launchdLabel = "ai.openclaw.mac"
let gatewayLaunchdLabel = "ai.openclaw.gateway"
let onboardingVersionKey = "openclaw.onboardingVersion"
let onboardingSeenKey = "openclaw.onboardingSeen"
let onboardingSecurityAcknowledgedKey = "openclaw.onboardingSecurityAcknowledged"
let currentOnboardingVersion = 7
let pauseDefaultsKey = "openclaw.pauseEnabled"
let iconAnimationsEnabledKey = "openclaw.iconAnimationsEnabled"

View File

@ -25,6 +25,7 @@ final class OnboardingController {
if ProcessInfo.processInfo.isNixMode {
// Nix mode is fully declarative; onboarding would suggest interactive setup that doesn't apply.
UserDefaults.standard.set(true, forKey: "openclaw.onboardingSeen")
UserDefaults.standard.set(true, forKey: onboardingSecurityAcknowledgedKey)
UserDefaults.standard.set(currentOnboardingVersion, forKey: onboardingVersionKey)
AppStateStore.shared.onboardingSeen = true
return
@ -90,6 +91,7 @@ struct OnboardingView: View {
@State var onboardingWizard = OnboardingWizardModel()
@State var didLoadOnboardingSkills = false
@State var localGatewayProbe: LocalGatewayProbe?
@State var securityNoticeAcknowledged: Bool
@Bindable var state: AppState
var permissionMonitor: PermissionMonitor
@ -148,13 +150,27 @@ struct OnboardingView: View {
self.activePageIndex == self.wizardPageIndex && !self.onboardingWizard.isComplete
}
var isSecurityNoticeBlocking: Bool {
self.activePageIndex == 0 && !self.securityNoticeAcknowledged
}
var canAdvance: Bool {
if self.isSecurityNoticeBlocking {
return false
}
if self.activePageIndex == self.cliPageIndex {
return self.cliInstalled && !self.installingCLI
}
return !self.isWizardBlocking
}
static func resolveSecurityNoticeAcknowledged(
onboardingSeen: Bool,
storedAcknowledgement: Bool) -> Bool
{
storedAcknowledgement || onboardingSeen
}
var devLinkCommand: String {
let version = GatewayEnvironment.expectedGatewayVersionString() ?? "latest"
return "npm install -g openclaw@\(version)"
@ -177,6 +193,10 @@ struct OnboardingView: View {
self.state = state
self.permissionMonitor = permissionMonitor
self._gatewayDiscovery = State(initialValue: discoveryModel)
self._securityNoticeAcknowledged = State(
initialValue: Self.resolveSecurityNoticeAcknowledged(
onboardingSeen: state.onboardingSeen,
storedAcknowledgement: UserDefaults.standard.bool(forKey: onboardingSecurityAcknowledgedKey)))
self._onboardingChatModel = State(
initialValue: OpenClawChatViewModel(
sessionKey: "onboarding",

View File

@ -45,7 +45,7 @@ extension OnboardingView {
}
func handleNext() {
if self.isWizardBlocking { return }
if !self.canAdvance { return }
if self.currentPage < self.pageCount - 1 {
withAnimation { self.currentPage += 1 }
} else {
@ -53,7 +53,13 @@ extension OnboardingView {
}
}
func setSecurityNoticeAcknowledged(_ acknowledged: Bool) {
self.securityNoticeAcknowledged = acknowledged
UserDefaults.standard.set(acknowledged, forKey: onboardingSecurityAcknowledgedKey)
}
func finish() {
UserDefaults.standard.set(true, forKey: onboardingSecurityAcknowledgedKey)
UserDefaults.standard.set(true, forKey: "openclaw.onboardingSeen")
UserDefaults.standard.set(currentOnboardingVersion, forKey: onboardingVersionKey)
OnboardingController.shared.close()

View File

@ -72,6 +72,29 @@ extension OnboardingView {
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(Color.orange.opacity(0.06)))
.frame(maxWidth: 520)
self.onboardingCard(spacing: 8, padding: 14) {
if self.securityNoticeAcknowledged {
Label("Security notice acknowledged on this Mac.", systemImage: "checkmark.shield.fill")
.font(.callout.weight(.medium))
.foregroundStyle(Color(nsColor: .systemGreen))
} else {
Toggle(
isOn: Binding(
get: { self.securityNoticeAcknowledged },
set: { self.setSecurityNoticeAcknowledged($0) }))
{
Text("I understand the risks and want to continue.")
.font(.callout.weight(.medium))
}
.toggleStyle(.checkbox)
Text("You only need to acknowledge this once on this Mac.")
.font(.caption)
.foregroundStyle(.secondary)
}
}
.frame(maxWidth: 520)
}
.padding(.top, 16)
}

View File

@ -59,6 +59,7 @@ extension OnboardingView {
_ = view.connectionPage()
view.currentPage = 0
view.setSecurityNoticeAcknowledged(true)
view.handleNext()
view.handleBack()

View File

@ -27,6 +27,50 @@ struct OnboardingViewSmokeTests {
#expect(!order.contains(8))
}
@Test func `fresh installs require security acknowledgement before advancing`() {
let defaults = UserDefaults.standard
let previous = defaults.object(forKey: onboardingSecurityAcknowledgedKey)
defaults.removeObject(forKey: onboardingSecurityAcknowledgedKey)
defer {
if let previous {
defaults.set(previous, forKey: onboardingSecurityAcknowledgedKey)
} else {
defaults.removeObject(forKey: onboardingSecurityAcknowledgedKey)
}
}
let freshState = AppState(preview: true)
freshState.onboardingSeen = false
let freshView = OnboardingView(
state: freshState,
permissionMonitor: PermissionMonitor.shared,
discoveryModel: GatewayDiscoveryModel(localDisplayName: InstanceIdentity.displayName))
#expect(freshView.isSecurityNoticeBlocking)
#expect(!freshView.canAdvance)
defaults.set(true, forKey: onboardingSecurityAcknowledgedKey)
let acknowledgedState = AppState(preview: true)
acknowledgedState.onboardingSeen = false
let acknowledgedView = OnboardingView(
state: acknowledgedState,
permissionMonitor: PermissionMonitor.shared,
discoveryModel: GatewayDiscoveryModel(localDisplayName: InstanceIdentity.displayName))
#expect(!acknowledgedView.isSecurityNoticeBlocking)
#expect(acknowledgedView.canAdvance)
}
@Test func `existing onboarded users keep their acknowledgement`() {
#expect(OnboardingView.resolveSecurityNoticeAcknowledged(
onboardingSeen: true,
storedAcknowledgement: false))
#expect(!OnboardingView.resolveSecurityNoticeAcknowledged(
onboardingSeen: false,
storedAcknowledgement: false))
}
@Test func `select remote gateway clears stale ssh target when endpoint unresolved`() async {
let override = FileManager().temporaryDirectory
.appendingPathComponent("openclaw-config-\(UUID().uuidString)")