Persist agent permission mode
This commit is contained in:
1 parent
aa136347da
commit
09f5d027bf
3 files changed
+67
-8
No files matched your search
@@ -30,6 +30,8 @@ nonisolated enum SettingsKey {
|
||||
}
|
||||
/// Restore the previous window's tabs on launch.
|
||||
static let restoreTabs = "window.restoreTabs"
|
||||
/// The last approval posture selected in the Assistant composer.
|
||||
static let agentPermissionMode = "assistant.permissionMode"
|
||||
static let sftpGlobalTransferLimit = "sftp.transfers.globalLimit"
|
||||
static let sftpPerHostTransferLimit = "sftp.transfers.perHostLimit"
|
||||
static let defaultSFTPGlobalTransferLimit = 3
|
||||
|
||||
@@ -27,8 +27,8 @@ final class AIStore {
|
||||
private(set) var lastSelectedModel: AIModelRef?
|
||||
|
||||
/// The approval posture new and otherwise-unset conversations follow.
|
||||
/// Session-only. Unlike the model selection, approval posture is not part
|
||||
/// of provider configuration and is deliberately not persisted here.
|
||||
/// Stored in local preferences rather than provider configuration so it
|
||||
/// survives relaunches without becoming part of the synced SwiftData model.
|
||||
private(set) var lastSelectedPermissionMode: ChatPermissionMode
|
||||
|
||||
/// Live MCP connection state, keyed by server.
|
||||
@@ -44,6 +44,7 @@ final class AIStore {
|
||||
private var secrets: [String: String] = [:]
|
||||
private let keychain: KeychainSecretStore
|
||||
private let persistsSecrets: Bool
|
||||
private let permissionDefaults: UserDefaults?
|
||||
private(set) var secretStorageError: String?
|
||||
private var persistence: AIPersistence?
|
||||
private(set) var storageError: String?
|
||||
@@ -55,23 +56,33 @@ final class AIStore {
|
||||
lastSelectedModel: AIModelRef? = nil,
|
||||
lastSelectedPermissionMode: ChatPermissionMode = .askForApproval,
|
||||
keychain: KeychainSecretStore = KeychainSecretStore(),
|
||||
persistsSecrets: Bool = true
|
||||
persistsSecrets: Bool = true,
|
||||
permissionDefaults: UserDefaults? = nil
|
||||
) {
|
||||
self.modelProviders = modelProviders
|
||||
self.mcpServers = mcpServers
|
||||
self.roles = roles
|
||||
self.lastSelectedModel = lastSelectedModel
|
||||
self.lastSelectedPermissionMode = lastSelectedPermissionMode
|
||||
self.lastSelectedPermissionMode = permissionDefaults
|
||||
.flatMap { $0.string(forKey: SettingsKey.agentPermissionMode) }
|
||||
.flatMap(ChatPermissionMode.init(rawValue:))
|
||||
?? lastSelectedPermissionMode
|
||||
self.keychain = keychain
|
||||
self.persistsSecrets = persistsSecrets
|
||||
self.permissionDefaults = permissionDefaults
|
||||
}
|
||||
|
||||
convenience init(
|
||||
modelContext: ModelContext,
|
||||
keychain: KeychainSecretStore = KeychainSecretStore(),
|
||||
persistsSecrets: Bool = true
|
||||
persistsSecrets: Bool = true,
|
||||
permissionDefaults: UserDefaults = .standard
|
||||
) {
|
||||
self.init(keychain: keychain, persistsSecrets: persistsSecrets)
|
||||
self.init(
|
||||
keychain: keychain,
|
||||
persistsSecrets: persistsSecrets,
|
||||
permissionDefaults: permissionDefaults
|
||||
)
|
||||
let persistence = AIPersistence(context: modelContext)
|
||||
self.persistence = persistence
|
||||
do {
|
||||
@@ -378,10 +389,11 @@ final class AIStore {
|
||||
mutatePersistedState { lastSelectedModel = ref }
|
||||
}
|
||||
|
||||
/// Records a composer permission choice for subsequent unset conversations.
|
||||
/// It does not alter command or MCP execution policy.
|
||||
/// Records a composer permission choice for subsequent unset conversations
|
||||
/// and future app launches. It does not alter command or MCP execution policy.
|
||||
func select(_ permissionMode: ChatPermissionMode) {
|
||||
lastSelectedPermissionMode = permissionMode
|
||||
permissionDefaults?.set(permissionMode.rawValue, forKey: SettingsKey.agentPermissionMode)
|
||||
}
|
||||
|
||||
// MARK: - Provider persistence
|
||||
|
||||
@@ -101,6 +101,51 @@ struct ChatComposerStateTests {
|
||||
#expect(store.resolve(conversation: nil as ChatPermissionMode?) == .approveForMe)
|
||||
}
|
||||
|
||||
@Test
|
||||
func permissionDefaultPersistsAcrossStoreInstancesIncludingFullAccess() throws {
|
||||
let suiteName = "ChatPermissionMode-\(UUID().uuidString)"
|
||||
let defaults = try #require(UserDefaults(suiteName: suiteName))
|
||||
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||
|
||||
let first = AIStore(persistsSecrets: false, permissionDefaults: defaults)
|
||||
first.select(ChatPermissionMode.approveForMe)
|
||||
|
||||
let second = AIStore(persistsSecrets: false, permissionDefaults: defaults)
|
||||
#expect(second.lastSelectedPermissionMode == .approveForMe)
|
||||
|
||||
second.select(ChatPermissionMode.fullAccess)
|
||||
|
||||
let third = AIStore(persistsSecrets: false, permissionDefaults: defaults)
|
||||
#expect(third.lastSelectedPermissionMode == .fullAccess)
|
||||
|
||||
let explicit = ConversationModel()
|
||||
explicit.permissionMode = .askForApproval
|
||||
#expect(third.resolve(conversation: explicit.permissionMode) == .askForApproval)
|
||||
}
|
||||
|
||||
@Test
|
||||
func missingPersistedPermissionDefaultsToAskForApproval() throws {
|
||||
let suiteName = "ChatPermissionMode-Missing-\(UUID().uuidString)"
|
||||
let defaults = try #require(UserDefaults(suiteName: suiteName))
|
||||
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||
|
||||
let store = AIStore(persistsSecrets: false, permissionDefaults: defaults)
|
||||
|
||||
#expect(store.lastSelectedPermissionMode == .askForApproval)
|
||||
}
|
||||
|
||||
@Test
|
||||
func invalidPersistedPermissionDefaultsToAskForApproval() throws {
|
||||
let suiteName = "ChatPermissionMode-Invalid-\(UUID().uuidString)"
|
||||
let defaults = try #require(UserDefaults(suiteName: suiteName))
|
||||
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||
defaults.set("unknown-mode", forKey: SettingsKey.agentPermissionMode)
|
||||
|
||||
let store = AIStore(persistsSecrets: false, permissionDefaults: defaults)
|
||||
|
||||
#expect(store.lastSelectedPermissionMode == .askForApproval)
|
||||
}
|
||||
|
||||
@Test
|
||||
func modelSelectionStillFeedsUnsetConversationsWithoutOverridingExplicitOnes() {
|
||||
let firstModel = AIModel(id: "first")
|
||||
|
||||
Reference in new issue
Block a user