import AppKit
import CryptoKit
import Darwin
import Foundation
import OpenClawKit
import OSLog

private let execApprovalsSocketTimeoutMs = 15000

struct ExecApprovalPromptRequest: Codable {
    var command: String
    var cwd: String?
    var host: String?
    var security: String?
    var ask: String?
    var agentId: String?
    var resolvedPath: String?
    var sessionKey: String?
    var allowedDecisions: [ExecApprovalDecision]?

    init(
        command: String,
        cwd: String? = nil,
        host: String? = nil,
        security: String? = nil,
        ask: String? = nil,
        agentId: String? = nil,
        resolvedPath: String? = nil,
        sessionKey: String? = nil,
        allowedDecisions: [ExecApprovalDecision]? = nil)
    {
        self.command = command
        self.cwd = cwd
        self.host = host
        self.security = security
        self.ask = ask
        self.agentId = agentId
        self.resolvedPath = resolvedPath
        self.sessionKey = sessionKey
        self.allowedDecisions = allowedDecisions
    }

    private enum CodingKeys: String, CodingKey {
        case command
        case cwd
        case host
        case security
        case ask
        case agentId
        case resolvedPath
        case sessionKey
        case allowedDecisions
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.command = try container.decode(String.self, forKey: .command)
        self.cwd = try container.decodeIfPresent(String.self, forKey: .cwd)
        self.host = try container.decodeIfPresent(String.self, forKey: .host)
        self.security = try container.decodeIfPresent(String.self, forKey: .security)
        self.ask = try container.decodeIfPresent(String.self, forKey: .ask)
        self.agentId = try container.decodeIfPresent(String.self, forKey: .agentId)
        self.resolvedPath = try container.decodeIfPresent(String.self, forKey: .resolvedPath)
        self.sessionKey = try container.decodeIfPresent(String.self, forKey: .sessionKey)
        let decodedDecisions = (try? container.decodeIfPresent(
            [DecodedExecApprovalDecision].self,
            forKey: .allowedDecisions)) ?? []
        self.allowedDecisions = decodedDecisions.compactMap(\.decision)
    }

    static func allowedDecisions(
        forAsk ask: String?,
        allowAlwaysEligible: Bool = true) -> [ExecApprovalDecision]
    {
        // Older payloads did not carry ask/allowedDecisions. Preserve their durable
        // approval option; explicit ask=always and allowedDecisions payloads are the
        // policy-carrying shapes that remove it.
        guard allowAlwaysEligible else { return [.allowOnce, .deny] }
        return ask == ExecAsk.always.rawValue
            ? [.allowOnce, .deny]
            : [.allowOnce, .allowAlways, .deny]
    }
}

private struct DecodedExecApprovalDecision: Decodable {
    var decision: ExecApprovalDecision?

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        guard let raw = try? container.decode(String.self) else {
            self.decision = nil
            return
        }
        self.decision = ExecApprovalDecision(rawValue: raw)
    }
}

private struct ExecApprovalSocketRequest: Codable {
    var type: String
    var token: String
    var id: String
    var request: ExecApprovalPromptRequest
}

private struct ExecApprovalSocketDecision: Codable {
    var type: String
    var id: String
    var decision: ExecApprovalDecision
}

private struct ExecHostSocketRequest: Codable {
    var type: String
    var id: String
    var nonce: String
    var ts: Int
    var hmac: String
    var requestJson: String
}

struct ExecHostRequest: Codable {
    var command: [String]
    var rawCommand: String?
    var cwd: String?
    var env: [String: String]?
    var timeoutMs: Int?
    var needsScreenRecording: Bool?
    var agentId: String?
    var sessionKey: String?
    var approvalDecision: ExecApprovalDecision?
    var approvalSource: String?
    var policySnapshot: OpenClawSystemRunApprovalPolicySnapshot?
}

private struct ExecHostRunResult: Codable {
    var exitCode: Int?
    var timedOut: Bool
    var success: Bool
    var stdout: String
    var stderr: String
    var error: String?
}

enum ExecHostOutputLimiter {
    static let maxJsonlResponseBytes = 16 * 1024 * 1024
    static let maxOutputFieldBytes = 1024 * 1024
    private static let truncationMarker = "... (truncated) "

    static func truncate(_ value: String) -> String {
        let bytes = value.utf8
        guard bytes.count > self.maxOutputFieldBytes else { return value }

        let tailBudget = self.maxOutputFieldBytes - self.truncationMarker.utf8.count
        var start = bytes.index(bytes.endIndex, offsetBy: -tailBudget)
        while start < bytes.endIndex, (bytes[start] & 0xC0) == 0x80 {
            start = bytes.index(after: start)
        }
        let tail = String(bytes: bytes[start...], encoding: .utf8) ?? ""
        return self.truncationMarker + tail
    }
}

struct ExecHostError: Codable, Error {
    var code: String
    var message: String
    var reason: String?
}

private struct ExecHostResponse: Codable {
    var type: String
    var id: String
    var ok: Bool
    var payload: ExecHostRunResult?
    var error: ExecHostError?
}

private func configureSocketTimeouts(_ fd: Int32, timeoutMs: Int) throws {
    guard timeoutMs > 0 else { return }
    var timeout = timeval(
        tv_sec: timeoutMs / 1000,
        tv_usec: Int32((timeoutMs % 1000) * 1000))
    let timeoutSize = socklen_t(MemoryLayout.size(ofValue: timeout))
    guard setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, timeoutSize) == 0,
          setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, timeoutSize) == 0
    else {
        throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO)
    }
}

private func readLineFromSocket(_ fd: Int32, maxBytes: Int) throws -> String? {
    // Foundation can wait for the full requested byte count on sockets. POSIX
    // recv returns short JSONL frames; the socket timeout bounds idle peers.
    var buffer = Data()
    while buffer.count < maxBytes {
        var chunk = [UInt8](repeating: 0, count: min(4096, maxBytes - buffer.count))
        let count = chunk.withUnsafeMutableBytes { bytes in
            recv(fd, bytes.baseAddress, bytes.count, 0)
        }
        if count == 0 {
            break
        }
        if count < 0 {
            if errno == EINTR {
                continue
            }
            throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO)
        }
        buffer.append(contentsOf: chunk.prefix(count))
        if buffer.contains(0x0A) {
            break
        }
    }
    guard let newlineIndex = buffer.firstIndex(of: 0x0A) else {
        guard !buffer.isEmpty else { return nil }
        return String(data: buffer, encoding: .utf8)
    }
    let lineData = buffer.subdata(in: 0..<newlineIndex)
    return String(data: lineData, encoding: .utf8)
}

func timingSafeHexStringEquals(_ lhs: String, _ rhs: String) -> Bool {
    let lhsBytes = Array(lhs.utf8)
    let rhsBytes = Array(rhs.utf8)
    guard lhsBytes.count == rhsBytes.count else {
        return false
    }

    var diff: UInt8 = 0
    for index in lhsBytes.indices {
        diff |= lhsBytes[index] ^ rhsBytes[index]
    }
    return diff == 0
}

func execHostTimestampIsFresh(
    nowMs: Int,
    requestMs: Int,
    toleranceMs: Int = 10000) -> Bool
{
    guard toleranceMs >= 0 else { return false }
    let (lowerBound, lowerOverflow) = nowMs.subtractingReportingOverflow(toleranceMs)
    if !lowerOverflow, requestMs < lowerBound {
        return false
    }
    let (upperBound, upperOverflow) = nowMs.addingReportingOverflow(toleranceMs)
    if !upperOverflow, requestMs > upperBound {
        return false
    }
    return true
}

enum ExecApprovalsSocketClient {
    private struct TimeoutError: LocalizedError {
        var message: String
        var errorDescription: String? {
            self.message
        }
    }

    static func requestDecision(
        socketPath: String,
        token: String,
        request: ExecApprovalPromptRequest,
        timeoutMs: Int = 15000) async -> ExecApprovalDecision?
    {
        let trimmedPath = socketPath.trimmingCharacters(in: .whitespacesAndNewlines)
        let trimmedToken = token.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmedPath.isEmpty, !trimmedToken.isEmpty else { return nil }
        do {
            return try await AsyncTimeout.withTimeoutMs(
                timeoutMs: timeoutMs,
                onTimeout: {
                    TimeoutError(message: "exec approvals socket timeout")
                },
                operation: {
                    try await Task.detached {
                        try self.requestDecisionSync(
                            socketPath: trimmedPath,
                            token: trimmedToken,
                            request: request,
                            timeoutMs: timeoutMs)
                    }.value
                })
        } catch {
            return nil
        }
    }

    private static func requestDecisionSync(
        socketPath: String,
        token: String,
        request: ExecApprovalPromptRequest,
        timeoutMs: Int) throws -> ExecApprovalDecision?
    {
        let fd = socket(AF_UNIX, SOCK_STREAM, 0)
        guard fd >= 0 else {
            throw NSError(domain: "ExecApprovals", code: 1, userInfo: [
                NSLocalizedDescriptionKey: "socket create failed",
            ])
        }
        defer { close(fd) }
        try configureSocketTimeouts(fd, timeoutMs: timeoutMs)

        var addr = sockaddr_un()
        addr.sun_family = sa_family_t(AF_UNIX)
        let maxLen = MemoryLayout.size(ofValue: addr.sun_path)
        if socketPath.utf8.count >= maxLen {
            throw NSError(domain: "ExecApprovals", code: 2, userInfo: [
                NSLocalizedDescriptionKey: "socket path too long",
            ])
        }
        socketPath.withCString { cstr in
            withUnsafeMutablePointer(to: &addr.sun_path) { ptr in
                let raw = UnsafeMutableRawPointer(ptr).assumingMemoryBound(to: Int8.self)
                strncpy(raw, cstr, maxLen - 1)
            }
        }
        let size = socklen_t(MemoryLayout.size(ofValue: addr))
        let result = withUnsafePointer(to: &addr) { ptr in
            ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { rebound in
                connect(fd, rebound, size)
            }
        }
        if result != 0 {
            throw NSError(domain: "ExecApprovals", code: 3, userInfo: [
                NSLocalizedDescriptionKey: "socket connect failed",
            ])
        }

        let handle = FileHandle(fileDescriptor: fd, closeOnDealloc: false)

        let message = ExecApprovalSocketRequest(
            type: "request",
            token: token,
            id: UUID().uuidString,
            request: request)
        let data = try JSONEncoder().encode(message)
        var payload = data
        payload.append(0x0A)
        try handle.write(contentsOf: payload)

        guard let line = try readLineFromSocket(fd, maxBytes: 256_000),
              let lineData = line.data(using: .utf8)
        else { return nil }
        let response = try JSONDecoder().decode(ExecApprovalSocketDecision.self, from: lineData)
        return response.decision
    }
}

@MainActor
final class ExecApprovalsPromptServer {
    static let shared = ExecApprovalsPromptServer()

    private let retryDelay: Duration
    private let resolveSocketCredentials: @Sendable () -> (socketPath: String, token: String)
    private let onPrompt: @Sendable (ExecApprovalPromptRequest) async -> ExecApprovalDecision?
    private var server: ExecApprovalsSocketServer?
    private var retryTask: Task<Void, Never>?
    private var previousStartupTask: Task<Void, Never>?
    private var startupGeneration: UInt64 = 0

    init(
        retryDelay: Duration = .seconds(1),
        resolveSocketCredentials: @escaping @Sendable () -> (socketPath: String, token: String) = {
            let approvals = ExecApprovalsStore.resolve(agentId: nil)
            return (approvals.socketPath, approvals.token)
        },
        onPrompt: @escaping @Sendable (ExecApprovalPromptRequest) async -> ExecApprovalDecision? = { request in
            await ExecApprovalsPromptPresenter.prompt(
                request,
                timeoutMs: execApprovalsSocketTimeoutMs)
        })
    {
        self.retryDelay = retryDelay
        self.resolveSocketCredentials = resolveSocketCredentials
        self.onPrompt = onPrompt
    }

    func start() {
        guard self.server == nil, self.retryTask == nil else { return }
        self.startupGeneration &+= 1
        let generation = self.startupGeneration
        let retryDelay = self.retryDelay
        let resolveSocketCredentials = self.resolveSocketCredentials
        let onPrompt = self.onPrompt
        let previousStartupTask = self.previousStartupTask
        // Keep one lifecycle-owned retry loop. Blocking lock acquisition stays
        // off MainActor, while generation checks prevent post-stop installation.
        self.retryTask = Task { @MainActor [weak self] in
            // A canceled startup may still be unwinding socket-path cleanup.
            // Never let a replacement generation race that cleanup.
            if let previousStartupTask {
                await previousStartupTask.value
            }
            guard !Task.isCancelled, self?.startupGeneration == generation else { return }

            var isFirstAttempt = true
            while !Task.isCancelled {
                if isFirstAttempt {
                    isFirstAttempt = false
                } else {
                    do {
                        try await Task.sleep(for: retryDelay)
                    } catch {
                        return
                    }
                }

                let credentials = await Task.detached(priority: .utility) {
                    resolveSocketCredentials()
                }.value
                guard !Task.isCancelled,
                      let self,
                      self.startupGeneration == generation
                else { return }

                let token = credentials.token.trimmingCharacters(in: .whitespacesAndNewlines)
                guard !token.isEmpty else { continue }

                let server = ExecApprovalsSocketServer(
                    socketPath: credentials.socketPath,
                    token: token,
                    onPrompt: onPrompt,
                    onExec: { request in
                        await ExecHostExecutor.handle(request)
                    },
                    onUnexpectedStop: { [weak self] stoppedServer in
                        Task { @MainActor [weak self] in
                            self?.handleUnexpectedStop(stoppedServer, generation: generation)
                        }
                    })
                let ready = await withTaskCancellationHandler {
                    await server.start()
                } onCancel: {
                    server.stop()
                }
                guard !Task.isCancelled, self.startupGeneration == generation else {
                    server.stop()
                    return
                }
                guard ready else {
                    server.stop()
                    continue
                }
                // The accept loop can fail after signaling readiness but before
                // this task resumes. Do not install an already-dead listener.
                guard server.isListening else {
                    server.stop()
                    continue
                }
                self.server = server
                self.retryTask = nil
                return
            }
        }
    }

    @discardableResult
    func stop() -> Task<Void, Never>? {
        self.startupGeneration &+= 1
        let pendingRetry = self.retryTask
        pendingRetry?.cancel()
        if let pendingRetry {
            self.previousStartupTask = pendingRetry
        }
        self.retryTask = nil
        self.server?.stop()
        self.server = nil
        return pendingRetry
    }

    private func handleUnexpectedStop(
        _ stoppedServer: ExecApprovalsSocketServer,
        generation: UInt64)
    {
        guard self.startupGeneration == generation,
              self.server === stoppedServer
        else { return }
        self.server = nil
        self.start()
    }

    #if DEBUG
    func _testFailActiveSocket() {
        self.server?.failForTesting()
    }

    static func _testPrecancelledSocketStart(
        socketPath: String) async -> (ready: Bool, preservedExistingListener: Bool)
    {
        let sentinel = ExecApprovalsSocketServer(
            socketPath: socketPath,
            token: "sentinel-token",
            onPrompt: { _ in nil },
            onExec: { request in
                await ExecHostExecutor.handle(request)
            },
            onUnexpectedStop: { _ in })
        guard await sentinel.start() else {
            sentinel.stop()
            return (false, false)
        }

        let socketServer = ExecApprovalsSocketServer(
            socketPath: socketPath,
            token: "test-token",
            onPrompt: { _ in nil },
            onExec: { request in
                await ExecHostExecutor.handle(request)
            },
            onUnexpectedStop: { _ in })
        let startup = Task.detached {
            withUnsafeCurrentTask { task in
                task?.cancel()
            }
            return await socketServer.start()
        }
        let ready = await startup.value
        socketServer.stop()
        let preservedExistingListener = sentinel.isListening &&
            (try? ExecApprovalsSocketPathGuard.pathKind(at: socketPath)) == .socket
        sentinel.stop()
        return (ready, preservedExistingListener)
    }

    static func _testSocketLeaseHandoff(
        socketPath: String) async -> (
        replacementBlockedWhileOwned: Bool,
        replacementStartedAfterRelease: Bool,
        replacementHasDistinctIdentity: Bool,
        replacementPreserved: Bool)
    {
        let first = ExecApprovalsSocketServer(
            socketPath: socketPath,
            token: "first-token",
            onPrompt: { _ in nil },
            onExec: { request in
                await ExecHostExecutor.handle(request)
            },
            onUnexpectedStop: { _ in })
        guard await first.start() else {
            first.stop()
            return (false, false, false, false)
        }
        let firstIdentity = try? ExecApprovalsSocketPathGuard.socketIdentity(at: socketPath)

        let replacement = ExecApprovalsSocketServer(
            socketPath: socketPath,
            token: "replacement-token",
            onPrompt: { _ in nil },
            onExec: { request in
                await ExecHostExecutor.handle(request)
            },
            onUnexpectedStop: { _ in })
        let replacementBlockedWhileOwned = await !replacement.start()
        first.stop()
        let replacementStartedAfterRelease = await replacement.start()
        let replacementIdentity = try? ExecApprovalsSocketPathGuard.socketIdentity(at: socketPath)
        let currentIdentity = try? ExecApprovalsSocketPathGuard.socketIdentity(at: socketPath)
        let result = (
            replacementBlockedWhileOwned: replacementBlockedWhileOwned,
            replacementStartedAfterRelease: replacementStartedAfterRelease,
            replacementHasDistinctIdentity: firstIdentity != nil &&
                replacementIdentity != nil &&
                firstIdentity != replacementIdentity,
            replacementPreserved: replacement.isListening && currentIdentity == replacementIdentity)
        replacement.stop()
        return result
    }

    static func _testExecHostTimestampFailureReason(_ timestamp: Int) async -> String? {
        let server = ExecApprovalsSocketServer(
            socketPath: "",
            token: "test-token",
            onPrompt: { _ in nil },
            onExec: { _ in
                ExecHostResponse(
                    type: "exec-res",
                    id: "unexpected-execution",
                    ok: true,
                    payload: nil,
                    error: nil)
            },
            onUnexpectedStop: { _ in })
        return await server.testExecHostTimestampFailureReason(timestamp)
    }
    #endif
}

enum ExecApprovalsPromptPresenter {
    private struct PendingPrompt {
        let id: UUID
        let continuation: CheckedContinuation<Bool, Never>
    }

    @MainActor
    private static var activePrompt: (id: UUID, alert: NSAlert?, cancelled: Bool)?
    @MainActor
    private static var pendingPrompts: [PendingPrompt] = []

    @MainActor
    static func prompt(
        _ request: ExecApprovalPromptRequest,
        timeoutMs: Int? = nil) async -> ExecApprovalDecision?
    {
        if let timeoutMs, timeoutMs <= 0 { return nil }
        let promptID = UUID()
        let timeoutWorkItem = timeoutMs.map { _ in
            DispatchWorkItem {
                MainActor.assumeIsolated {
                    self.cancelPrompt(id: promptID)
                }
            }
        }
        if let timeoutMs, let timeoutWorkItem {
            DispatchQueue.main.asyncAfter(
                deadline: .now() + .milliseconds(timeoutMs),
                execute: timeoutWorkItem)
        }
        defer { timeoutWorkItem?.cancel() }
        return await withTaskCancellationHandler {
            guard !Task.isCancelled, await self.acquirePrompt(id: promptID) else { return nil }
            guard !Task.isCancelled, self.activePrompt?.cancelled != true else {
                self.releasePrompt(id: promptID)
                return nil
            }
            let decision = self.runPrompt(request, id: promptID)
            let cancelled = self.activePrompt?.id == promptID && self.activePrompt?.cancelled == true
            self.releasePrompt(id: promptID)
            return Task.isCancelled || cancelled ? nil : decision
        } onCancel: {
            // Caller deadlines cancel the prompt task. Abort the matching modal
            // session so an expired approval cannot outlive or block later requests.
            DispatchQueue.main.async {
                MainActor.assumeIsolated {
                    self.cancelPrompt(id: promptID)
                }
            }
        }
    }

    @MainActor
    private static func runPrompt(
        _ request: ExecApprovalPromptRequest,
        id: UUID) -> ExecApprovalDecision?
    {
        NSApp.activate(ignoringOtherApps: true)
        let alert = NSAlert()
        alert.alertStyle = .warning
        alert.messageText = "Allow this command?"
        alert.informativeText = "Review the command details before allowing."
        alert.accessoryView = self.buildAccessoryView(request)

        let decisions = self.allowedPromptDecisions(request)
        for decision in decisions {
            alert.addButton(withTitle: self.buttonTitle(for: decision))
        }
        if #available(macOS 11.0, *),
           let denyIndex = decisions.firstIndex(of: .deny),
           alert.buttons.indices.contains(denyIndex)
        {
            alert.buttons[denyIndex].hasDestructiveAction = true
        }

        guard self.activePrompt?.id == id else { return nil }
        self.activePrompt?.alert = alert
        defer { self.activePrompt?.alert = nil }
        return self.decision(forModalResponse: alert.runModal(), decisions: decisions)
    }

    @MainActor
    private static func acquirePrompt(id: UUID) async -> Bool {
        // AppKit cannot cancel nested modal loops independently. Queue behind one
        // active alert; caller cancellation and deadlines remove expired waiters.
        if self.activePrompt == nil {
            self.activePrompt = (id: id, alert: nil, cancelled: false)
            return true
        }
        return await withCheckedContinuation { continuation in
            self.pendingPrompts.append(PendingPrompt(id: id, continuation: continuation))
        }
    }

    @MainActor
    private static func releasePrompt(id: UUID) {
        guard self.activePrompt?.id == id else { return }
        self.activePrompt = nil
        guard !self.pendingPrompts.isEmpty else { return }
        let next = self.pendingPrompts.removeFirst()
        self.activePrompt = (id: next.id, alert: nil, cancelled: false)
        next.continuation.resume(returning: true)
    }

    @MainActor
    private static func cancelPrompt(id: UUID) {
        if self.activePrompt?.id == id {
            self.activePrompt?.cancelled = true
            guard let alert = self.activePrompt?.alert else { return }
            if NSApp.modalWindow === alert.window {
                NSApp.abortModal()
            }
            alert.window.close()
            return
        }
        guard let index = self.pendingPrompts.firstIndex(where: { $0.id == id }) else { return }
        let pending = self.pendingPrompts.remove(at: index)
        pending.continuation.resume(returning: false)
    }

    static func decision(
        forModalResponse response: NSApplication.ModalResponse,
        decisions: [ExecApprovalDecision]) -> ExecApprovalDecision?
    {
        let selectedIndex = response.rawValue
            - NSApplication.ModalResponse.alertFirstButtonReturn.rawValue
        if decisions.indices.contains(selectedIndex) {
            return decisions[selectedIndex]
        }
        return decisions.contains(.deny) ? .deny : nil
    }

    static func allowedPromptDecisions(_ request: ExecApprovalPromptRequest) -> [ExecApprovalDecision] {
        if let allowedDecisions = request.allowedDecisions, !allowedDecisions.isEmpty {
            return allowedDecisions
        }
        return ExecApprovalPromptRequest.allowedDecisions(forAsk: request.ask)
    }

    private static func buttonTitle(for decision: ExecApprovalDecision) -> String {
        switch decision {
        case .allowOnce:
            "Allow Once"
        case .allowAlways:
            "Always Allow"
        case .deny:
            "Don't Allow"
        }
    }

    @MainActor
    static func buildAccessoryView(_ request: ExecApprovalPromptRequest) -> NSView {
        let stack = NSStackView()
        stack.orientation = .vertical
        stack.spacing = 8
        stack.alignment = .leading
        stack.widthAnchor.constraint(greaterThanOrEqualToConstant: 380).isActive = true

        let commandTitle = NSTextField(labelWithString: "Command")
        commandTitle.font = NSFont.boldSystemFont(ofSize: NSFont.systemFontSize)
        stack.addArrangedSubview(commandTitle)

        let commandText = NSTextView()
        commandText.isEditable = false
        commandText.isSelectable = true
        commandText.drawsBackground = true
        commandText.backgroundColor = NSColor.textBackgroundColor
        commandText.font = NSFont.monospacedSystemFont(ofSize: NSFont.systemFontSize, weight: .regular)
        commandText.string = ExecApprovalCommandDisplaySanitizer.sanitize(request.command)
        commandText.textContainerInset = NSSize(width: 6, height: 6)
        commandText.textContainer?.lineFragmentPadding = 0
        commandText.textContainer?.widthTracksTextView = true
        commandText.isHorizontallyResizable = false
        commandText.isVerticallyResizable = true

        let commandScroll = NSScrollView()
        commandScroll.borderType = .lineBorder
        commandScroll.hasVerticalScroller = true
        commandScroll.hasHorizontalScroller = false
        commandScroll.autohidesScrollers = true
        commandScroll.documentView = commandText
        commandScroll.translatesAutoresizingMaskIntoConstraints = false
        commandScroll.widthAnchor.constraint(greaterThanOrEqualToConstant: 380).isActive = true
        commandScroll.widthAnchor.constraint(lessThanOrEqualToConstant: 440).isActive = true
        commandScroll.heightAnchor.constraint(greaterThanOrEqualToConstant: 56).isActive = true
        commandScroll.heightAnchor.constraint(lessThanOrEqualToConstant: 120).isActive = true
        stack.addArrangedSubview(commandScroll)

        let contextTitle = NSTextField(labelWithString: "Context")
        contextTitle.font = NSFont.boldSystemFont(ofSize: NSFont.systemFontSize)
        stack.addArrangedSubview(contextTitle)

        let contextStack = NSStackView()
        contextStack.orientation = .vertical
        contextStack.spacing = 4
        contextStack.alignment = .leading

        if let cwd = self.sanitizedContextValue(request.cwd) {
            self.addDetailRow(title: "Working directory", value: cwd, to: contextStack)
        }
        if let agent = self.sanitizedContextValue(request.agentId) {
            self.addDetailRow(title: "Agent", value: agent, to: contextStack)
        }
        if let path = self.sanitizedContextValue(request.resolvedPath) {
            self.addDetailRow(title: "Executable", value: path, to: contextStack)
        }
        if let host = self.sanitizedContextValue(request.host) {
            self.addDetailRow(title: "Host", value: host, to: contextStack)
        }
        if let security = self.sanitizedContextValue(request.security) {
            self.addDetailRow(title: "Security", value: security, to: contextStack)
        }
        if let ask = self.sanitizedContextValue(request.ask) {
            self.addDetailRow(title: "Ask mode", value: ask, to: contextStack)
        }

        if contextStack.arrangedSubviews.isEmpty {
            let empty = NSTextField(labelWithString: "No additional context provided.")
            empty.textColor = NSColor.secondaryLabelColor
            empty.font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize)
            contextStack.addArrangedSubview(empty)
        }

        stack.addArrangedSubview(contextStack)

        let footer = NSTextField(labelWithString: "This runs on this machine.")
        footer.textColor = NSColor.secondaryLabelColor
        footer.font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize)
        stack.addArrangedSubview(footer)

        // NSAlert reserves accessory space from the view frame, not from Auto Layout constraints.
        // Give the top-level accessory an explicit frame so its subviews do not paint over the
        // alert title, message, and buttons while the frame remains zero-sized.
        stack.frame = NSRect(origin: .zero, size: stack.fittingSize)
        return stack
    }

    static func sanitizedContextValue(_ value: String?) -> String? {
        let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        guard !trimmed.isEmpty else { return nil }
        return ExecApprovalCommandDisplaySanitizer.sanitize(trimmed)
    }

    @MainActor
    private static func addDetailRow(title: String, value: String, to stack: NSStackView) {
        let row = NSStackView()
        row.orientation = .horizontal
        row.spacing = 6
        row.alignment = .firstBaseline

        let titleLabel = NSTextField(labelWithString: "\(title):")
        titleLabel.font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize, weight: .semibold)
        titleLabel.textColor = NSColor.secondaryLabelColor

        let valueLabel = NSTextField(labelWithString: value)
        valueLabel.font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize)
        valueLabel.lineBreakMode = .byTruncatingMiddle
        valueLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)

        row.addArrangedSubview(titleLabel)
        row.addArrangedSubview(valueLabel)
        stack.addArrangedSubview(row)
    }
}

#if DEBUG
extension ExecApprovalsPromptPresenter {
    @MainActor
    static func reservePromptForTesting() -> UUID? {
        guard self.activePrompt == nil else { return nil }
        let id = UUID()
        self.activePrompt = (id: id, alert: nil, cancelled: false)
        return id
    }

    @MainActor
    static func releasePromptForTesting(id: UUID) {
        self.releasePrompt(id: id)
    }

    @MainActor
    static var pendingPromptCountForTesting: Int {
        self.pendingPrompts.count
    }
}
#endif

@MainActor
private enum ExecHostExecutor {
    static func handle(_ request: ExecHostRequest) async -> ExecHostResponse {
        let validatedRequest: ExecHostValidatedRequest
        switch ExecHostRequestEvaluator.validateRequest(request) {
        case let .success(request):
            validatedRequest = request
        case let .failure(error):
            return self.errorResponse(error)
        }

        let context = await self.buildContext(
            request: request,
            command: validatedRequest.command,
            rawCommand: validatedRequest.evaluationRawCommand,
            displayCommand: validatedRequest.displayCommand)
        let approvalSource = validatedRequest.approvalSource
        let security = ExecHostRequestEvaluator.effectiveSecurity(
            context: context,
            approvalSource: approvalSource)
        var explicitlyApproved = approvalSource == .autoReview ||
            request.approvalDecision == .allowOnce ||
            request.approvalDecision == .allowAlways
        var persistAllowlist = request.approvalDecision == .allowAlways

        switch ExecHostRequestEvaluator.evaluate(
            context: context,
            approvalDecision: request.approvalDecision,
            approvalSource: approvalSource)
        {
        case let .deny(error):
            return self.errorResponse(error)
        case .allow:
            break
        case .requiresPrompt:
            guard let decision = await ExecApprovalsPromptPresenter.prompt(
                ExecApprovalPromptRequest(
                    command: context.displayCommand,
                    cwd: request.cwd,
                    host: "node",
                    security: context.security.rawValue,
                    ask: context.ask.rawValue,
                    agentId: context.agentId,
                    resolvedPath: context.resolution?.resolvedPath,
                    sessionKey: request.sessionKey,
                    allowedDecisions: ExecApprovalPromptRequest.allowedDecisions(
                        forAsk: context.ask.rawValue,
                        allowAlwaysEligible: context.canPersistAllowAlways)),
                timeoutMs: execApprovalsSocketTimeoutMs)
            else {
                return self.errorResponse(
                    code: "UNAVAILABLE",
                    message: "SYSTEM_RUN_DENIED: approval prompt closed without decision",
                    reason: "approval-cancelled")
            }

            let followupDecision: ExecApprovalDecision
            switch decision {
            case .deny:
                followupDecision = .deny
            case .allowAlways:
                explicitlyApproved = true
                followupDecision = .allowAlways
            case .allowOnce:
                explicitlyApproved = true
                followupDecision = .allowOnce
            }
            persistAllowlist = followupDecision == .allowAlways

            switch ExecHostRequestEvaluator.evaluate(
                context: context,
                approvalDecision: followupDecision,
                approvalSource: approvalSource)
            {
            case let .deny(error):
                return self.errorResponse(error)
            case .allow:
                break
            case .requiresPrompt:
                return self.errorResponse(
                    code: "INVALID_REQUEST",
                    message: "unexpected approval state",
                    reason: "invalid")
            }
        }

        let authorizationBasis = context.authorizationBasis
        let reusableAuthorization = security == .allowlist &&
            !explicitlyApproved &&
            authorizationBasis != nil

        let executionCommand: [String]
        if reusableAuthorization {
            guard let boundCommand = context.boundCommand else {
                return self.errorResponse(
                    code: "UNAVAILABLE",
                    message: "SYSTEM_RUN_DENIED: reusable approval could not bind executable",
                    reason: "allowlist-unbound")
            }
            executionCommand = boundCommand
        } else {
            executionCommand = validatedRequest.command
        }

        if let errorResponse = await self.ensureScreenRecordingAccess(request.needsScreenRecording) {
            return errorResponse
        }

        let executionCommit = ExecApprovalExecutionCommit.build(
            context: context,
            effectiveSecurity: security,
            approvalSource: approvalSource,
            explicitlyApproved: explicitlyApproved,
            persistAllowlist: persistAllowlist,
            delayedPolicySnapshot: validatedRequest.delayedPolicySnapshot)
        let timeoutSec = request.timeoutMs.flatMap { Double($0) / 1000.0 }
        let cwd = request.cwd
        let env = context.env
        if case .failure = ExecApprovalsStore.commitExecution(executionCommit) {
            return self.approvalStoreErrorResponse()
        }

        // The store commit linearizes authorization. Enqueue before the next
        // suspension so no unrelated MainActor work sits between those steps.
        let execution = Task.detached { () -> ShellExecutor.ShellResult in
            await ShellExecutor.runDetailed(
                command: executionCommand,
                cwd: cwd,
                env: env,
                timeout: timeoutSec)
        }
        return await self.commandResponse(execution: execution)
    }

    private static func buildContext(
        request: ExecHostRequest,
        command: [String],
        rawCommand: String?,
        displayCommand: String) async -> ExecApprovalEvaluation
    {
        await ExecApprovalEvaluator.evaluate(
            command: command,
            rawCommand: rawCommand,
            displayCommand: displayCommand,
            cwd: request.cwd,
            envOverrides: request.env,
            agentId: request.agentId)
    }

    private static func approvalStoreErrorResponse() -> ExecHostResponse {
        self.errorResponse(
            code: "UNAVAILABLE",
            message: "SYSTEM_RUN_DENIED: exec approvals update unavailable",
            reason: "approval-store-unavailable")
    }

    private static func ensureScreenRecordingAccess(_ needsScreenRecording: Bool?) async -> ExecHostResponse? {
        guard needsScreenRecording == true else { return nil }
        let authorized = await PermissionManager
            .grantedStatus([.screenRecording])[.screenRecording] ?? false
        if authorized {
            return nil
        }
        return self.errorResponse(
            code: "UNAVAILABLE",
            message: "PERMISSION_MISSING: screenRecording",
            reason: "permission:screenRecording")
    }

    private static func commandResponse(
        execution: Task<ShellExecutor.ShellResult, Never>) async -> ExecHostResponse
    {
        let result = await execution.value
        let payload = ExecHostRunResult(
            exitCode: result.exitCode,
            timedOut: result.timedOut,
            success: result.success,
            stdout: ExecHostOutputLimiter.truncate(result.stdout),
            stderr: ExecHostOutputLimiter.truncate(result.stderr),
            error: result.errorMessage)
        return self.successResponse(payload)
    }

    private static func errorResponse(
        _ error: ExecHostError) -> ExecHostResponse
    {
        ExecHostResponse(
            type: "response",
            id: UUID().uuidString,
            ok: false,
            payload: nil,
            error: error)
    }

    private static func errorResponse(
        code: String,
        message: String,
        reason: String?) -> ExecHostResponse
    {
        ExecHostResponse(
            type: "exec-res",
            id: UUID().uuidString,
            ok: false,
            payload: nil,
            error: ExecHostError(code: code, message: message, reason: reason))
    }

    private static func successResponse(_ payload: ExecHostRunResult) -> ExecHostResponse {
        ExecHostResponse(
            type: "exec-res",
            id: UUID().uuidString,
            ok: true,
            payload: payload,
            error: nil)
    }
}

private final class ExecApprovalsSocketLifecycleLease: @unchecked Sendable {
    private static let processLock = NSLock()
    private nonisolated(unsafe) static var reservedPaths = Set<String>()

    private let descriptor: Int32
    private let path: String
    private let stateLock = NSLock()
    private var released = false

    private init(descriptor: Int32, path: String) {
        self.descriptor = descriptor
        self.path = path
    }

    static func acquire(for socketPath: String) throws -> ExecApprovalsSocketLifecycleLease {
        let socketURL = URL(fileURLWithPath: socketPath).standardizedFileURL
        let canonicalSocketPath = socketURL.deletingLastPathComponent()
            .resolvingSymlinksInPath()
            .appendingPathComponent(socketURL.lastPathComponent)
            .path
        let lockPath = "\(canonicalSocketPath).lifecycle.lock"
        let reserved = self.processLock.withLock { () -> Bool in
            guard !self.reservedPaths.contains(lockPath) else { return false }
            self.reservedPaths.insert(lockPath)
            return true
        }
        guard reserved else {
            throw ExecApprovalsSocketPathGuardError.lifecycleLockBusy(path: lockPath)
        }

        let descriptor = open(
            lockPath,
            O_RDWR | O_CREAT | O_CLOEXEC | O_NOFOLLOW,
            S_IRUSR | S_IWUSR)
        guard descriptor >= 0 else {
            self.releaseProcessReservation(lockPath)
            throw ExecApprovalsSocketPathGuardError.lifecycleLockOpenFailed(
                path: lockPath,
                code: errno)
        }

        do {
            var descriptorStatus = stat()
            var pathStatus = stat()
            guard fstat(descriptor, &descriptorStatus) == 0,
                  lstat(lockPath, &pathStatus) == 0,
                  descriptorStatus.st_mode & mode_t(S_IFMT) == mode_t(S_IFREG),
                  descriptorStatus.st_uid == geteuid(),
                  descriptorStatus.st_nlink == 1,
                  descriptorStatus.st_mode & mode_t(0o022) == 0,
                  descriptorStatus.st_dev == pathStatus.st_dev,
                  descriptorStatus.st_ino == pathStatus.st_ino
            else {
                throw ExecApprovalsSocketPathGuardError.lifecycleLockInvalid(path: lockPath)
            }
            guard flock(descriptor, LOCK_EX | LOCK_NB) == 0 else {
                throw ExecApprovalsSocketPathGuardError.lifecycleLockBusy(path: lockPath)
            }
            return ExecApprovalsSocketLifecycleLease(
                descriptor: descriptor,
                path: lockPath)
        } catch {
            close(descriptor)
            self.releaseProcessReservation(lockPath)
            throw error
        }
    }

    func release() {
        let shouldRelease = self.stateLock.withLock { () -> Bool in
            guard !self.released else { return false }
            self.released = true
            return true
        }
        guard shouldRelease else { return }
        _ = flock(self.descriptor, LOCK_UN)
        close(self.descriptor)
        Self.releaseProcessReservation(self.path)
    }

    deinit {
        self.release()
    }

    private static func releaseProcessReservation(_ path: String) {
        _ = self.processLock.withLock {
            self.reservedPaths.remove(path)
        }
    }
}

private final class ExecApprovalsSocketServer: @unchecked Sendable {
    private struct OpenedSocket {
        let fd: Int32
        let identity: ExecApprovalsSocketPathIdentity
        let lifecycleLease: ExecApprovalsSocketLifecycleLease
    }

    private let logger = Logger(subsystem: "ai.openclaw", category: "exec-approvals.socket")
    private let socketPath: String
    private let token: String
    private let onPrompt: @Sendable (ExecApprovalPromptRequest) async -> ExecApprovalDecision?
    private let onExec: @Sendable (ExecHostRequest) async -> ExecHostResponse
    private let onUnexpectedStop: @Sendable (ExecApprovalsSocketServer) -> Void
    private let stateLock = NSLock()
    private var socketFD: Int32 = -1
    private var socketIdentity: ExecApprovalsSocketPathIdentity?
    private var socketLifecycleLease: ExecApprovalsSocketLifecycleLease?
    private var acceptTask: Task<Void, Never>?
    private var isRunning = false

    init(
        socketPath: String,
        token: String,
        onPrompt: @escaping @Sendable (ExecApprovalPromptRequest) async -> ExecApprovalDecision?,
        onExec: @escaping @Sendable (ExecHostRequest) async -> ExecHostResponse,
        onUnexpectedStop: @escaping @Sendable (ExecApprovalsSocketServer) -> Void)
    {
        self.socketPath = socketPath
        self.token = token
        self.onPrompt = onPrompt
        self.onExec = onExec
        self.onUnexpectedStop = onUnexpectedStop
    }

    var isListening: Bool {
        self.stateLock.withLock { self.isRunning && self.socketFD >= 0 }
    }

    func start() async -> Bool {
        let shouldStart = self.stateLock.withLock {
            guard !Task.isCancelled, !self.isRunning else { return false }
            self.isRunning = true
            return true
        }
        guard shouldStart else {
            return self.stateLock.withLock { self.socketFD >= 0 }
        }

        return await withCheckedContinuation { continuation in
            let task = Task.detached { [weak self] in
                guard let self else {
                    continuation.resume(returning: false)
                    return
                }
                await self.runAcceptLoop { ready in
                    continuation.resume(returning: ready)
                }
            }
            self.stateLock.withLock {
                self.acceptTask = task
                if !self.isRunning {
                    task.cancel()
                }
            }
        }
    }

    func stop() {
        let (task, fd, identity, lifecycleLease) = self.stateLock.withLock {
            self.isRunning = false
            let task = self.acceptTask
            self.acceptTask = nil
            let fd = self.socketFD
            self.socketFD = -1
            let identity = self.socketIdentity
            self.socketIdentity = nil
            let lifecycleLease = self.socketLifecycleLease
            self.socketLifecycleLease = nil
            return (task, fd, identity, lifecycleLease)
        }
        task?.cancel()
        self.closeOwnedSocket(
            fd: fd,
            identity: identity,
            lifecycleLease: lifecycleLease)
    }

    private func runAcceptLoop(onReady: @escaping @Sendable (Bool) -> Void) async {
        let shouldOpen = self.stateLock.withLock { self.isRunning && !Task.isCancelled }
        guard shouldOpen else {
            self.stateLock.withLock {
                self.isRunning = false
                self.acceptTask = nil
            }
            onReady(false)
            return
        }

        guard let openedSocket = self.openSocket() else {
            self.stateLock.withLock {
                self.isRunning = false
                self.acceptTask = nil
            }
            onReady(false)
            return
        }
        let fd = openedSocket.fd

        let shouldAccept = self.stateLock.withLock {
            guard self.isRunning, !Task.isCancelled else { return false }
            self.socketFD = fd
            self.socketIdentity = openedSocket.identity
            self.socketLifecycleLease = openedSocket.lifecycleLease
            return true
        }
        guard shouldAccept else {
            self.closeOwnedSocket(
                fd: fd,
                identity: openedSocket.identity,
                lifecycleLease: openedSocket.lifecycleLease)
            onReady(false)
            return
        }

        onReady(true)
        while self.stateLock.withLock({ self.isRunning }), !Task.isCancelled {
            var addr = sockaddr_un()
            var len = socklen_t(MemoryLayout.size(ofValue: addr))
            let client = withUnsafeMutablePointer(to: &addr) { ptr in
                ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { rebound in
                    accept(fd, rebound, &len)
                }
            }
            if client < 0 {
                if errno == EINTR {
                    continue
                }
                break
            }
            Task.detached { [weak self] in
                await self?.handleClient(fd: client)
            }
        }

        let termination = self.stateLock.withLock {
            let stoppedUnexpectedly = self.isRunning && !Task.isCancelled
            let ownsDescriptor = self.socketFD == fd
            if ownsDescriptor {
                self.socketFD = -1
            }
            let identity = ownsDescriptor ? self.socketIdentity : nil
            let lifecycleLease = ownsDescriptor ? self.socketLifecycleLease : nil
            if ownsDescriptor {
                self.socketIdentity = nil
                self.socketLifecycleLease = nil
            }
            self.isRunning = false
            self.acceptTask = nil
            return (
                ownsDescriptor: ownsDescriptor,
                identity: identity,
                lifecycleLease: lifecycleLease,
                stoppedUnexpectedly: stoppedUnexpectedly)
        }
        if termination.ownsDescriptor {
            self.closeOwnedSocket(
                fd: fd,
                identity: termination.identity,
                lifecycleLease: termination.lifecycleLease)
        }
        if termination.stoppedUnexpectedly {
            self.onUnexpectedStop(self)
        }
    }

    private func closeOwnedSocket(
        fd: Int32,
        identity: ExecApprovalsSocketPathIdentity?,
        lifecycleLease: ExecApprovalsSocketLifecycleLease?)
    {
        if fd >= 0 {
            close(fd)
        }
        if !self.socketPath.isEmpty, let identity {
            do {
                // Keep the cross-process lease through the identity check and
                // unlink so no replacement can bind between those operations.
                try ExecApprovalsSocketPathGuard.removeSocket(
                    at: self.socketPath,
                    ifIdentityMatches: identity)
            } catch {
                self.logger
                    .warning("exec approvals socket cleanup failed: \(error.localizedDescription, privacy: .public)")
            }
        }
        lifecycleLease?.release()
    }

    #if DEBUG
    fileprivate func failForTesting() {
        let shutdown: (
            task: Task<Void, Never>?,
            fd: Int32,
            identity: ExecApprovalsSocketPathIdentity?,
            lifecycleLease: ExecApprovalsSocketLifecycleLease?) = self.stateLock.withLock {
            guard self.isRunning, self.socketFD >= 0 else {
                return (task: nil, fd: -1, identity: nil, lifecycleLease: nil)
            }
            self.isRunning = false
            let task = self.acceptTask
            self.acceptTask = nil
            let fd = self.socketFD
            self.socketFD = -1
            let identity = self.socketIdentity
            self.socketIdentity = nil
            let lifecycleLease = self.socketLifecycleLease
            self.socketLifecycleLease = nil
            return (task: task, fd: fd, identity: identity, lifecycleLease: lifecycleLease)
        }
        guard shutdown.fd >= 0 else { return }
        shutdown.task?.cancel()
        self.closeOwnedSocket(
            fd: shutdown.fd,
            identity: shutdown.identity,
            lifecycleLease: shutdown.lifecycleLease)
        self.onUnexpectedStop(self)
    }
    #endif

    private func openSocket() -> OpenedSocket? {
        let lifecycleLease: ExecApprovalsSocketLifecycleLease
        do {
            try ExecApprovalsSocketPathGuard.hardenParentDirectory(for: self.socketPath)
            lifecycleLease = try ExecApprovalsSocketLifecycleLease.acquire(for: self.socketPath)
            try ExecApprovalsSocketPathGuard.removeExistingSocket(at: self.socketPath)
        } catch {
            self.logger
                .error("exec approvals socket path hardening failed: \(error.localizedDescription, privacy: .public)")
            return nil
        }
        let fd = socket(AF_UNIX, SOCK_STREAM, 0)
        guard fd >= 0 else {
            self.logger.error("exec approvals socket create failed")
            lifecycleLease.release()
            return nil
        }
        var addr = sockaddr_un()
        addr.sun_family = sa_family_t(AF_UNIX)
        let maxLen = MemoryLayout.size(ofValue: addr.sun_path)
        if self.socketPath.utf8.count >= maxLen {
            self.logger.error("exec approvals socket path too long")
            close(fd)
            lifecycleLease.release()
            return nil
        }
        self.socketPath.withCString { cstr in
            withUnsafeMutablePointer(to: &addr.sun_path) { ptr in
                let raw = UnsafeMutableRawPointer(ptr).assumingMemoryBound(to: Int8.self)
                memset(raw, 0, maxLen)
                strncpy(raw, cstr, maxLen - 1)
            }
        }
        let size = socklen_t(MemoryLayout.size(ofValue: addr))
        let result = withUnsafePointer(to: &addr) { ptr in
            ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { rebound in
                bind(fd, rebound, size)
            }
        }
        if result != 0 {
            self.logger.error("exec approvals socket bind failed")
            close(fd)
            lifecycleLease.release()
            return nil
        }
        let identity: ExecApprovalsSocketPathIdentity
        do {
            guard let boundIdentity = try ExecApprovalsSocketPathGuard.socketIdentity(at: self.socketPath) else {
                self.logger.error("exec approvals socket identity unavailable after bind")
                close(fd)
                try? ExecApprovalsSocketPathGuard.removeExistingSocket(at: self.socketPath)
                lifecycleLease.release()
                return nil
            }
            identity = boundIdentity
        } catch {
            self.logger.error(
                "exec approvals socket identity failed: \(error.localizedDescription, privacy: .public)")
            close(fd)
            try? ExecApprovalsSocketPathGuard.removeExistingSocket(at: self.socketPath)
            lifecycleLease.release()
            return nil
        }
        if chmod(self.socketPath, 0o600) != 0 {
            self.logger.error("exec approvals socket chmod failed")
            self.closeOwnedSocket(
                fd: fd,
                identity: identity,
                lifecycleLease: lifecycleLease)
            return nil
        }
        if listen(fd, 16) != 0 {
            self.logger.error("exec approvals socket listen failed")
            self.closeOwnedSocket(
                fd: fd,
                identity: identity,
                lifecycleLease: lifecycleLease)
            return nil
        }
        self.logger.info("exec approvals socket listening at \(self.socketPath, privacy: .public)")
        return OpenedSocket(
            fd: fd,
            identity: identity,
            lifecycleLease: lifecycleLease)
    }

    private func handleClient(fd: Int32) async {
        let handle = FileHandle(fileDescriptor: fd, closeOnDealloc: true)
        do {
            guard self.isAllowedPeer(fd: fd) else {
                try self.sendApprovalResponse(handle: handle, id: UUID().uuidString, decision: .deny)
                return
            }
            try configureSocketTimeouts(fd, timeoutMs: execApprovalsSocketTimeoutMs)
            guard let line = try readLineFromSocket(fd, maxBytes: 256_000),
                  let data = line.data(using: .utf8)
            else {
                return
            }
            guard
                let envelope = try JSONSerialization.jsonObject(with: data) as? [String: Any],
                let type = envelope["type"] as? String
            else {
                return
            }

            if type == "request" {
                let request = try JSONDecoder().decode(ExecApprovalSocketRequest.self, from: data)
                guard request.token == self.token else {
                    try self.sendApprovalResponse(handle: handle, id: request.id, decision: .deny)
                    return
                }
                guard let decision = await self.onPrompt(request.request) else { return }
                try self.sendApprovalResponse(handle: handle, id: request.id, decision: decision)
                return
            }

            if type == "exec" {
                let request = try JSONDecoder().decode(ExecHostSocketRequest.self, from: data)
                let response = await self.handleExecRequest(request)
                try self.sendExecResponse(handle: handle, response: response)
                return
            }
        } catch {
            self.logger.error("exec approvals socket handling failed: \(error.localizedDescription, privacy: .public)")
        }
    }

    private func sendApprovalResponse(
        handle: FileHandle,
        id: String,
        decision: ExecApprovalDecision) throws
    {
        let response = ExecApprovalSocketDecision(type: "decision", id: id, decision: decision)
        let data = try JSONEncoder().encode(response)
        var payload = data
        payload.append(0x0A)
        try handle.write(contentsOf: payload)
    }

    private func sendExecResponse(handle: FileHandle, response: ExecHostResponse) throws {
        let data = try JSONEncoder().encode(response)
        var payload = data
        payload.append(0x0A)
        try handle.write(contentsOf: payload)
    }

    private func isAllowedPeer(fd: Int32) -> Bool {
        var uid = uid_t(0)
        var gid = gid_t(0)
        if getpeereid(fd, &uid, &gid) != 0 {
            return false
        }
        return uid == geteuid()
    }

    private func handleExecRequest(_ request: ExecHostSocketRequest) async -> ExecHostResponse {
        let nowMs = Int(Date().timeIntervalSince1970 * 1000)
        if !execHostTimestampIsFresh(nowMs: nowMs, requestMs: request.ts) {
            return ExecHostResponse(
                type: "exec-res",
                id: request.id,
                ok: false,
                payload: nil,
                error: ExecHostError(code: "INVALID_REQUEST", message: "expired request", reason: "ttl"))
        }
        let expected = self.hmacHex(nonce: request.nonce, ts: request.ts, requestJson: request.requestJson)
        if !timingSafeHexStringEquals(expected, request.hmac) {
            return ExecHostResponse(
                type: "exec-res",
                id: request.id,
                ok: false,
                payload: nil,
                error: ExecHostError(code: "INVALID_REQUEST", message: "invalid auth", reason: "hmac"))
        }
        guard let requestData = request.requestJson.data(using: .utf8),
              let payload = try? JSONDecoder().decode(ExecHostRequest.self, from: requestData)
        else {
            return ExecHostResponse(
                type: "exec-res",
                id: request.id,
                ok: false,
                payload: nil,
                error: ExecHostError(code: "INVALID_REQUEST", message: "invalid payload", reason: "json"))
        }
        let response = await self.onExec(payload)
        return ExecHostResponse(
            type: "exec-res",
            id: request.id,
            ok: response.ok,
            payload: response.payload,
            error: response.error)
    }

    #if DEBUG
    fileprivate func testExecHostTimestampFailureReason(_ timestamp: Int) async -> String? {
        let response = await self.handleExecRequest(ExecHostSocketRequest(
            type: "exec",
            id: "timestamp-test",
            nonce: "nonce",
            ts: timestamp,
            hmac: "unauthenticated",
            requestJson: #"{"command":["/usr/bin/true"]}"#))
        return response.error?.reason
    }
    #endif

    private func hmacHex(nonce: String, ts: Int, requestJson: String) -> String {
        let key = SymmetricKey(data: Data(self.token.utf8))
        let message = "\(nonce):\(ts):\(requestJson)"
        let mac = HMAC<SHA256>.authenticationCode(for: Data(message.utf8), using: key)
        return mac.map { String(format: "%02x", $0) }.joined()
    }
}
