Skip to content

Commit 2463712

Browse files
Managed via TartManaged via Tart
authored andcommitted
Fix live runtime settings refresh
1 parent 42f2e78 commit 2463712

10 files changed

Lines changed: 227 additions & 28 deletions

macos/OpenBridge/Agent/AgentSessionManager.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,17 @@ final class AgentSessionManager {
159159

160160
func refreshConnectorConfiguration() {
161161
isVMReady = true
162+
vmLoadError = nil
163+
Task { [weak self] in
164+
guard let self else { return }
165+
do {
166+
_ = try await ensureRuntimeReady()
167+
await refreshLoadedSessionRuntimeConfiguration()
168+
} catch {
169+
vmLoadError = error
170+
logger.warning("Failed to refresh local connector configuration: \(error.localizedDescription)")
171+
}
172+
}
162173
}
163174

164175
func restartConnector() {
@@ -168,6 +179,25 @@ final class AgentSessionManager {
168179
isVMReady = true
169180
}
170181

182+
func applyLocalEnvironmentPermissionModeChange(_ mode: LocalEnvironmentPermissionMode) {
183+
connector?.applyPermissionModeChange(mode)
184+
Task { [weak self] in
185+
guard let self else { return }
186+
await refreshLoadedSessionRuntimeConfiguration()
187+
}
188+
}
189+
190+
func reloadAIProviderConfiguration() async {
191+
await BridgeAIProviderRegistry.registerProviders()
192+
await refreshLoadedSessionRuntimeConfiguration()
193+
}
194+
195+
private func refreshLoadedSessionRuntimeConfiguration() async {
196+
for session in sessions.values {
197+
await session.refreshRuntimeConfiguration()
198+
}
199+
}
200+
171201
private func disconnectVMConnector(shutdownBridge: Bool = false) {
172202
guard vmConnector != nil || embeddedVMBridge != nil else { return }
173203
vmConnector?.disconnect()

macos/OpenBridge/Agent/BridgeAIProviderRegistry.swift

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,39 @@ enum BridgeAIProviderRegistry {
5151

5252
static func selectedModel() async -> Model {
5353
let settings = await BridgeAIProviderSecretStore.readSettings()
54-
return runtimeModel(
55-
provider: settings.selectedModelProvider,
56-
id: settings.selectedModelID,
54+
return selectedModel(settings: settings)
55+
}
56+
57+
static func selectedModel(settings: BridgeAIProviderSettings) -> Model {
58+
let selected = enabledModel(
59+
runtimeModel(
60+
provider: settings.selectedModelProvider,
61+
id: settings.selectedModelID,
62+
settings: settings
63+
),
5764
settings: settings
58-
) ?? defaultModel(settings: settings)
65+
)
66+
return selected ?? defaultModel(settings: settings)
67+
}
68+
69+
static func selectedDisplayModel(settings: BridgeAIProviderSettings) -> Model {
70+
let selected = enabledModel(
71+
displayModel(
72+
provider: settings.selectedModelProvider,
73+
id: settings.selectedModelID,
74+
settings: settings
75+
),
76+
settings: settings
77+
)
78+
return selected ?? defaultModel(settings: settings)
79+
}
80+
81+
private static func enabledModel(_ model: Model?, settings: BridgeAIProviderSettings) -> Model? {
82+
model.flatMap { model in
83+
BridgeAIProvider.provider(for: model).flatMap { provider in
84+
settings[provider].isEnabled ? model : nil
85+
}
86+
}
5987
}
6088

6189
static func runtimeModel(provider: String, id: String) async -> Model? {
@@ -92,10 +120,14 @@ enum BridgeAIProviderRegistry {
92120
}
93121

94122
static func displayModel(provider: String, id: String) -> Model? {
123+
displayModel(provider: provider, id: id, settings: BridgeAIProviderSettings())
124+
}
125+
126+
private static func displayModel(provider: String, id: String, settings: BridgeAIProviderSettings) -> Model? {
95127
if provider == BridgeAIProvider.openAIChatCompletions.rawValue {
96128
return openAIChatCompletionsModel(
97129
id: id,
98-
config: BridgeAIProviderSettings()[.openAIChatCompletions]
130+
config: settings[.openAIChatCompletions]
99131
)
100132
}
101133
return ModelsCatalog.model(provider: provider, id: id)

macos/OpenBridge/Agent/LocalRuntime/LocalAgentSession.swift

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ final class LocalAgentSession: Identifiable {
4848
private var localBackgroundManager: BackgroundTaskManager?
4949
private var localAgentUnsubscribe: Unsubscribe?
5050
private var localRunTask: Task<Void, Never>?
51+
private var pendingRuntimeConfigurationRefresh = false
5152
private var localAssistantMessageID: String?
5253
private var localAssistantText = ""
5354
private var localToolStates: [String: AssistantToolCallState] = [:]
@@ -184,7 +185,6 @@ final class LocalAgentSession: Identifiable {
184185
let now = Int64(Date().timeIntervalSince1970)
185186
listCreatedAt = listCreatedAt ?? now
186187
listUpdatedAt = listUpdatedAt ?? now
187-
_ = try await ensureLocalAgent()
188188
}
189189

190190
func teardown() async {
@@ -270,6 +270,7 @@ final class LocalAgentSession: Identifiable {
270270
AgentSessionManager.shared.clearLocalPermission(sessionId: sessionID)
271271
updateLocalAssistantState(phase: "cancelled", isStreaming: false)
272272
onSessionFinished?("cancelled", nil)
273+
refreshRuntimeConfigurationAfterRunIfNeeded()
273274
return true
274275
}
275276

@@ -321,24 +322,41 @@ final class LocalAgentSession: Identifiable {
321322

322323
// MARK: - Local KWWK Agent
323324

324-
private func ensureLocalAgent() async throws -> Agent {
325-
if let localAgent {
326-
return localAgent
325+
func refreshRuntimeConfiguration() async {
326+
guard let localAgent else { return }
327+
guard !isProcessing else {
328+
pendingRuntimeConfigurationRefresh = true
329+
return
327330
}
331+
pendingRuntimeConfigurationRefresh = false
332+
localAgent.state.model = await BridgeAIProviderRegistry.selectedModel()
333+
localAgent.state.systemPrompt = await makeLocalAgentSystemPrompt()
334+
}
328335

329-
await BridgeAIProviderRegistry.registerProviders()
330-
let backgroundManager = BackgroundTaskManager()
336+
private func makeLocalAgentSystemPrompt() async -> String {
331337
let skillManager = SkillManager.shared
332338
let cwd = skillManager.skillDirs.workspace.path
333339
let memoryPrompt = await MemoryRepository.shared.systemPromptSection()
334340
let environmentInventory = try? await AgentSessionManager.shared.localEnvironmentSystemPromptSection()
335-
let systemPrompt = OpenBridgeSystemPromptBuilder.build(
341+
return OpenBridgeSystemPromptBuilder.build(
336342
cwd: cwd,
337343
skills: skillManager.skills,
338344
memory: memoryPrompt,
339345
computerUsePrompt: OpenBridgeComputerUseAgent.systemPromptWithStartupInventory(clientStore: computerUseClientStore),
340346
environmentInventory: environmentInventory
341347
)
348+
}
349+
350+
private func ensureLocalAgent() async throws -> Agent {
351+
if let localAgent {
352+
return localAgent
353+
}
354+
355+
await BridgeAIProviderRegistry.registerProviders()
356+
let backgroundManager = BackgroundTaskManager()
357+
let skillManager = SkillManager.shared
358+
let cwd = skillManager.skillDirs.workspace.path
359+
let systemPrompt = await makeLocalAgentSystemPrompt()
342360
localBackgroundManager = backgroundManager
343361

344362
let config = await CodingAgentConfig(
@@ -472,12 +490,21 @@ final class LocalAgentSession: Identifiable {
472490
AgentSessionManager.shared.clearLocalPermission(sessionId: sessionID)
473491
updateLocalAssistantState(phase: lastFinishState ?? "completed", isStreaming: false)
474492
onSessionFinished?(lastFinishState ?? "completed", error?.localizedDescription)
493+
refreshRuntimeConfigurationAfterRunIfNeeded()
475494
Task { [weak self] in
476495
guard let self else { return }
477496
await refreshWorkspaceState()
478497
}
479498
}
480499

500+
private func refreshRuntimeConfigurationAfterRunIfNeeded() {
501+
guard pendingRuntimeConfigurationRefresh else { return }
502+
Task { [weak self] in
503+
guard let self else { return }
504+
await refreshRuntimeConfiguration()
505+
}
506+
}
507+
481508
private func requestComputerUseStartConfirmation(apps: [String]) async -> PermissionConfirmationReply {
482509
let confirmationId = "local-\(UUID().uuidString)"
483510
let message = makeComputerUsePermissionRequestMessage(confirmationId: confirmationId, apps: apps)

macos/OpenBridge/Agent/LocalRuntime/LocalRuntimeConnector.swift

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ private struct PendingConfirmation {
3939
var continuations: [CheckedContinuation<PermissionConfirmationReply, Never>]
4040
let sessionId: String?
4141
let grantKey: PermissionGrantKey?
42+
let respondsToPermissionMode: Bool
4243
}
4344

4445
private enum PermissionReplyReason {
@@ -149,6 +150,17 @@ final class LocalRuntimeConnector {
149150
killAllProcesses()
150151
}
151152

153+
func applyPermissionModeChange(_ mode: LocalEnvironmentPermissionMode) {
154+
guard environmentKind == .localMacOS else { return }
155+
156+
switch mode {
157+
case .default:
158+
revokeGrantedPermissions()
159+
case .fullAccess:
160+
approvePendingHostAccessPermissions()
161+
}
162+
}
163+
152164
private func currentArch() -> String {
153165
#if arch(arm64)
154166
return "arm64"
@@ -497,6 +509,19 @@ private extension LocalRuntimeConnector {
497509
grantedPermissionKeys = Set(grantedPermissionKeys.filter { $0.sessionKey != sessionKey })
498510
}
499511

512+
func revokeGrantedPermissions() {
513+
grantedPermissionKeys.removeAll()
514+
}
515+
516+
func approvePendingHostAccessPermissions() {
517+
let confirmationIDs = pendingConfirmations.compactMap { id, pending in
518+
pending.respondsToPermissionMode ? id : nil
519+
}
520+
for confirmationID in confirmationIDs {
521+
resolveConfirmation(id: confirmationID, approved: true)
522+
}
523+
}
524+
500525
/// Injects a permission_request message into the session that triggered the
501526
/// request (identified by sessionId) and suspends until the user clicks
502527
/// Allow or Deny in the chat UI. Returns false immediately if no session
@@ -599,7 +624,8 @@ private extension LocalRuntimeConnector {
599624
pendingConfirmations[confirmationId] = PendingConfirmation(
600625
continuations: [continuation],
601626
sessionId: sessionId,
602-
grantKey: grantKey
627+
grantKey: grantKey,
628+
respondsToPermissionMode: computerUseStart == nil
603629
)
604630
if let grantKey {
605631
pendingPermissionConfirmationIDsByGrantKey[grantKey] = confirmationId

macos/OpenBridge/Agent/LocalRuntime/OpenBridgeSystemPromptBuilder.swift

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ enum OpenBridgeSystemPromptBuilder {
3939
- Treat environment="sandbox" as the default environment for file reads, writes, commands, and project work.
4040
- Do not switch to or target environment="local" on your own initiative. Use environment="local" only when the user explicitly asks for direct host work or when sandbox cannot complete the task.
4141
- environment="local" is protected because it operates directly on this Mac. Host writes, host commands, and sensitive host paths require explicit user approval.
42-
- Local permission is temporary for the current task execution. Do not assume a past approval applies to a later user request.
43-
- Before using bash, write, or edit in environment="local", call request_permission(environment="local", description="...") with a clear, specific description of what you plan to do so the user can make an informed decision.
42+
\(localPermissionModeInstruction())
43+
\(localPermissionPersistenceInstruction())
44+
\(localPermissionRequestInstruction())
4445
- If local permission is pending and you can still make progress in sandbox, continue there. If blocked on approval, wait and explain what you are waiting for.
4546
- Do not operate on sandbox and local filesystems in the same task unless the user explicitly asks for that handoff. Choose one environment for filesystem work.
4647
- After completing sandbox file operations, call current_changes to review staged sandbox changes before finishing.
@@ -59,6 +60,33 @@ enum OpenBridgeSystemPromptBuilder {
5960
"""
6061
}
6162

63+
private static func localPermissionModeInstruction() -> String {
64+
switch SettingsManager.shared.localEnvironmentPermissionMode {
65+
case .default:
66+
"- Current local permission mode: Default. Host writes, host commands, and sensitive host paths require request_permission before use."
67+
case .fullAccess:
68+
"- Current local permission mode: Full Access. Host writes, host commands, and sensitive host paths are already approved for this app session; do not call request_permission solely for local permission, but still prefer sandbox unless local access is required."
69+
}
70+
}
71+
72+
private static func localPermissionPersistenceInstruction() -> String {
73+
switch SettingsManager.shared.localEnvironmentPermissionMode {
74+
case .default:
75+
"- Local permission is temporary for the current task execution. Do not assume a past approval applies to a later user request."
76+
case .fullAccess:
77+
"- Full Access remains available only while the user keeps this app setting enabled. If the setting changes back to Default, request local permission again before protected host access."
78+
}
79+
}
80+
81+
private static func localPermissionRequestInstruction() -> String {
82+
switch SettingsManager.shared.localEnvironmentPermissionMode {
83+
case .default:
84+
"- Before using bash, write, or edit in environment=\"local\", call request_permission(environment=\"local\", description=\"...\") with a clear, specific description of what you plan to do so the user can make an informed decision."
85+
case .fullAccess:
86+
"- Before using bash, write, or edit in environment=\"local\", confirm local access is truly required; request_permission is not needed while Full Access is active."
87+
}
88+
}
89+
6290
private static func skillSection(skills: [Skill]) -> String {
6391
let entries = skills
6492
.filter { !$0.disabled && $0.visibility != .hidden }

macos/OpenBridge/Interface/Chat/ChatEditorViewModel+ComposerEditing.swift

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -166,16 +166,7 @@ extension ChatEditorViewModel {
166166
selectedModelID = settings.selectedModelID
167167
return
168168
}
169-
let selected = BridgeAIProviderRegistry.displayModel(
170-
provider: settings.selectedModelProvider,
171-
id: settings.selectedModelID
172-
)
173-
.flatMap { model in
174-
BridgeAIProvider.provider(for: model).flatMap { provider in
175-
settings[provider].isEnabled ? model : nil
176-
}
177-
}
178-
?? BridgeAIProviderRegistry.defaultModel(settings: settings)
169+
let selected = BridgeAIProviderRegistry.selectedDisplayModel(settings: settings)
179170
selectedModelProvider = selected.provider
180171
selectedModelID = selected.id
181172
}
@@ -205,6 +196,7 @@ extension ChatEditorViewModel {
205196
SettingsManager.shared.localEnvironmentPermissionMode != mode
206197
else { return }
207198
SettingsManager.shared.localEnvironmentPermissionMode = mode
199+
AgentSessionManager.shared.applyLocalEnvironmentPermissionModeChange(mode)
208200
}
209201

210202
private func openAIProviderSettings() {

macos/OpenBridge/Interface/Chat/ChatEditorViewModel.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ final class ChatEditorViewModel {
6666
var voicePendingWaveformPeak: Double = 0
6767
@ObservationIgnored
6868
private var quoteFocusRequestCounter: Int = 0
69+
@ObservationIgnored
70+
private var aiProviderSettingsCancellable: AnyCancellable?
6971

7072
/// Skill selected for the current conversation (delegated to Chat)
7173
var selectedSkill: Skill? {
@@ -125,6 +127,13 @@ final class ChatEditorViewModel {
125127
text: String = ""
126128
) {
127129
self.text = text
130+
aiProviderSettingsCancellable = NotificationCenter.default
131+
.publisher(for: .aiProviderSettingsDidChange)
132+
.sink { [weak self] _ in
133+
Task { @MainActor [weak self] in
134+
await self?.loadSelectedModel()
135+
}
136+
}
128137
Task { await loadSelectedModel() }
129138
}
130139

0 commit comments

Comments
 (0)