native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
7.8 kB · 245 lines
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246import Foundation
#if canImport(Darwin) import Darwin#elseif canImport(Glibc) import Glibc#endif
nonisolated enum CodexConfigReadProcessError: Error, Equatable, Sendable { case cancelled case executableUnavailable case invalidProfile case outputTooLarge case processFailed case timeout}
nonisolated struct CodexConfigReadProcess: Sendable { private final class ProcessBox: @unchecked Sendable { private let lock = NSLock() private var process: Process?
func install(_ process: Process) { lock.lock() self.process = process lock.unlock() }
func terminate() { lock.lock() let process = self.process lock.unlock() if process?.isRunning == true { process?.terminate() } } }
let executableURL: URL let temporaryBaseDirectory: URL let timeout: TimeInterval
init( executableURL: URL = URL(filePath: "/usr/bin/env", directoryHint: .notDirectory), temporaryBaseDirectory: URL = FileManager.default.temporaryDirectory, timeout: TimeInterval = 1 ) { self.executableURL = executableURL self.temporaryBaseDirectory = temporaryBaseDirectory self.timeout = max(0.05, timeout) }
func usingExecutable(_ executableURL: URL) -> CodexConfigReadProcess { CodexConfigReadProcess( executableURL: executableURL, temporaryBaseDirectory: temporaryBaseDirectory, timeout: timeout ) }
func query(_ query: CodexConfigQuery) async throws -> Data { let fileManager = FileManager.default var parserHome: URL? let effectiveHome: URL switch query.kind { case .base: effectiveHome = query.codexHome case .profile(let profileURL): let data = try readStableProfile(profileURL) let home = try makeTemporaryHome(fileManager: fileManager) parserHome = home let configURL = home.appending(path: "config.toml", directoryHint: .notDirectory) try data.write(to: configURL, options: .atomic) try fileManager.setAttributes( [.posixPermissions: 0o600], ofItemAtPath: configURL.path(percentEncoded: false) ) effectiveHome = home case .explicitNotify: let home = try makeTemporaryHome(fileManager: fileManager) parserHome = home effectiveHome = home } defer { if let parserHome { try? fileManager.removeItem(at: parserHome) } }
let processBox = ProcessBox() let task = Task.detached(priority: .userInitiated) { try Self.run( query: query, effectiveHome: effectiveHome, executableURL: executableURL, timeout: timeout, processBox: processBox ) } return try await withTaskCancellationHandler { try await task.value } onCancel: { task.cancel() processBox.terminate() } }
private func makeTemporaryHome(fileManager: FileManager) throws -> URL { try fileManager.createDirectory( at: temporaryBaseDirectory, withIntermediateDirectories: true, attributes: [.posixPermissions: 0o700] ) let home = temporaryBaseDirectory.appending( path: "prowl-codex-parser-\(UUID().uuidString)", directoryHint: .isDirectory ) try fileManager.createDirectory( at: home, withIntermediateDirectories: false, attributes: [.posixPermissions: 0o700] ) try fileManager.setAttributes( [.posixPermissions: 0o700], ofItemAtPath: home.path(percentEncoded: false) ) return home }
private func readStableProfile(_ url: URL) throws -> Data { guard case .stable(let data) = StableOwnerFileReader.read( url, maximumBytes: 256 * 1_024 ) else { throw CodexConfigReadProcessError.invalidProfile } return data }
private static func run( query: CodexConfigQuery, effectiveHome: URL, executableURL: URL, timeout: TimeInterval, processBox: ProcessBox ) throws -> Data { guard FileManager.default.isExecutableFile(atPath: executableURL.path(percentEncoded: false)) else { throw CodexConfigReadProcessError.executableUnavailable } let process = Process() process.executableURL = executableURL var arguments: [String] if executableURL.path(percentEncoded: false) == "/usr/bin/env" { arguments = ["codex", "app-server", "--listen", "stdio://"] } else { arguments = ["app-server", "--listen", "stdio://"] } for override in query.overrides { arguments += ["-c", override] } process.arguments = arguments process.currentDirectoryURL = query.cwd var environment = ProcessInfo.processInfo.environment environment["CODEX_HOME"] = effectiveHome.path(percentEncoded: false) process.environment = environment let input = Pipe() let output = Pipe() process.standardInput = input process.standardOutput = output process.standardError = FileHandle.nullDevice guard configureNoSigPipe(input.fileHandleForWriting.fileDescriptor) else { throw CodexConfigReadProcessError.processFailed } processBox.install(process) try process.run() defer { stop(process) try? input.fileHandleForWriting.close() try? output.fileHandleForReading.close() } let request = CodexConfigReadProtocol.requestData( cwd: query.cwd.path(percentEncoded: false) ) // The request pipe stays open until the response has arrived: Codex ≥ 0.149.1's // app-server shuts down on stdin EOF and drops any request it has not answered yet. // The deferred `stop` ends the server and closes the pipe once the read loop returns. do { try input.fileHandleForWriting.write(contentsOf: request) } catch { throw CodexConfigReadProcessError.processFailed }
let descriptor = output.fileHandleForReading.fileDescriptor let deadline = DispatchTime.now().uptimeNanoseconds + UInt64(timeout * 1_000_000_000) var transcript = Data()
while DispatchTime.now().uptimeNanoseconds < deadline { if Task.isCancelled { throw CodexConfigReadProcessError.cancelled } var pollDescriptor = pollfd(fd: descriptor, events: Int16(POLLIN | POLLHUP), revents: 0) let status = poll(&pollDescriptor, 1, 25) if status < 0 { if errno == EINTR { continue } throw CodexConfigReadProcessError.processFailed } if status == 0 { continue } var buffer = [UInt8](repeating: 0, count: 64 * 1_024) let count = buffer.withUnsafeMutableBytes { Darwin.read(descriptor, $0.baseAddress, $0.count) } if count > 0 { transcript.append(contentsOf: buffer.prefix(count)) guard transcript.count <= 1_024 * 1_024 else { throw CodexConfigReadProcessError.outputTooLarge } do { // A decoded nil is a complete response with no notifier, not missing output. _ = try CodexConfigReadProtocol.decodeNotify(from: transcript) return transcript } catch CodexConfigReadError.missingResponse { continue } } else if count == 0 { throw CodexConfigReadProcessError.processFailed } else if errno != EINTR && errno != EAGAIN { throw CodexConfigReadProcessError.processFailed } } throw CodexConfigReadProcessError.timeout }
static func configureNoSigPipe(_ descriptor: Int32) -> Bool { #if canImport(Darwin) fcntl(descriptor, F_SETNOSIGPIPE, 1) == 0 #else true #endif }
private static func stop(_ process: Process) { if process.isRunning { process.terminate() } for _ in 0..<100 where process.isRunning { usleep(1_000) } if process.isRunning { kill(process.processIdentifier, SIGKILL) } process.waitUntilExit() }}