fix: avoid duplicate exec approval prompts

This commit is contained in:
Peter Steinberger
2026-01-22 09:53:36 +00:00
parent 6822d509d7
commit ced9efd964
3 changed files with 38 additions and 2 deletions

View File

@@ -1,5 +1,6 @@
import ClawdbotKit import ClawdbotKit
import ClawdbotProtocol import ClawdbotProtocol
import CoreGraphics
import Foundation import Foundation
import OSLog import OSLog
@@ -44,6 +45,7 @@ final class ExecApprovalsGatewayPrompter {
do { do {
let data = try JSONEncoder().encode(payload) let data = try JSONEncoder().encode(payload)
let request = try JSONDecoder().decode(GatewayApprovalRequest.self, from: data) let request = try JSONDecoder().decode(GatewayApprovalRequest.self, from: data)
guard self.shouldPresent(request: request) else { return }
let decision = ExecApprovalsPromptPresenter.prompt(request.request) let decision = ExecApprovalsPromptPresenter.prompt(request.request)
try await GatewayConnection.shared.requestVoid( try await GatewayConnection.shared.requestVoid(
method: .execApprovalResolve, method: .execApprovalResolve,
@@ -56,4 +58,35 @@ final class ExecApprovalsGatewayPrompter {
self.logger.error("exec approval handling failed \(error.localizedDescription, privacy: .public)") self.logger.error("exec approval handling failed \(error.localizedDescription, privacy: .public)")
} }
} }
private func shouldPresent(request: GatewayApprovalRequest) -> Bool {
let mode = AppStateStore.shared.connectionMode
let activeSession = WebChatManager.shared.activeSessionKey?.trimmingCharacters(in: .whitespacesAndNewlines)
let requestSession = request.request.sessionKey?.trimmingCharacters(in: .whitespacesAndNewlines)
let recentlyActive = self.isRecentlyActive(mode: mode, thresholdSeconds: 120)
if let session = requestSession, !session.isEmpty {
if let active = activeSession, !active.isEmpty {
return active == session
}
return recentlyActive
}
if let active = activeSession, !active.isEmpty {
return true
}
return mode == .local
}
private func isRecentlyActive(mode: AppState.ConnectionMode, thresholdSeconds: Int) -> Bool {
guard let seconds = Self.lastInputSeconds() else { return mode == .local }
return seconds <= thresholdSeconds
}
private static func lastInputSeconds() -> Int? {
let anyEvent = CGEventType(rawValue: UInt32.max) ?? .null
let seconds = CGEventSource.secondsSinceLastEventType(.combinedSessionState, eventType: anyEvent)
if seconds.isNaN || seconds.isInfinite || seconds < 0 { return nil }
return Int(seconds.rounded())
}
} }

View File

@@ -13,6 +13,7 @@ struct ExecApprovalPromptRequest: Codable, Sendable {
var ask: String? var ask: String?
var agentId: String? var agentId: String?
var resolvedPath: String? var resolvedPath: String?
var sessionKey: String?
} }
private struct ExecApprovalSocketRequest: Codable { private struct ExecApprovalSocketRequest: Codable {
@@ -412,7 +413,8 @@ private enum ExecHostExecutor {
security: context.security.rawValue, security: context.security.rawValue,
ask: context.ask.rawValue, ask: context.ask.rawValue,
agentId: context.trimmedAgent, agentId: context.trimmedAgent,
resolvedPath: context.resolution?.resolvedPath)) resolvedPath: context.resolution?.resolvedPath,
sessionKey: request.sessionKey))
switch decision { switch decision {
case .deny: case .deny:

View File

@@ -679,7 +679,8 @@ actor MacNodeRuntime {
security: context.security.rawValue, security: context.security.rawValue,
ask: context.ask.rawValue, ask: context.ask.rawValue,
agentId: context.agentId, agentId: context.agentId,
resolvedPath: context.resolution?.resolvedPath)) resolvedPath: context.resolution?.resolvedPath,
sessionKey: context.sessionKey))
} }
switch decision { switch decision {
case .deny: case .deny: