From ee9efcada16ec15584f387ce84da2cea9e0b99c4 Mon Sep 17 00:00:00 2001 From: Morten Trydal Date: Wed, 18 Mar 2026 22:36:23 +0100 Subject: [PATCH 1/2] Avoid main-thread executable resolution --- .../Agents/AgentRuntimeBridge.swift | 45 ------------------- .../AgentRuntimeBridgeTests.swift | 32 +++++++++++++ 2 files changed, 32 insertions(+), 45 deletions(-) diff --git a/Sources/Shellraiser/Infrastructure/Agents/AgentRuntimeBridge.swift b/Sources/Shellraiser/Infrastructure/Agents/AgentRuntimeBridge.swift index 151a2b1..d77aa50 100644 --- a/Sources/Shellraiser/Infrastructure/Agents/AgentRuntimeBridge.swift +++ b/Sources/Shellraiser/Infrastructure/Agents/AgentRuntimeBridge.swift @@ -11,7 +11,6 @@ final class AgentRuntimeBridge: AgentRuntimeSupporting { let eventLogURL: URL private let fileManager: FileManager - private var cachedExecutablePaths: [String: String?] = [:] /// Creates the bridge rooted in the process temp directory to avoid path escaping issues. private convenience init() { @@ -101,53 +100,9 @@ final class AgentRuntimeBridge: AgentRuntimeSupporting { environment["SHELLRAISER_ORIGINAL_PATH"] = inheritedPath } - if let claudePath = resolveExecutable(named: "claude", searchPath: inheritedPath) { - environment["SHELLRAISER_REAL_CLAUDE"] = claudePath - } - - if let codexPath = resolveExecutable(named: "codex", searchPath: inheritedPath) { - environment["SHELLRAISER_REAL_CODEX"] = codexPath - } - return environment } - /// Resolves the current machine path for an executable before wrapper PATH injection takes effect. - private func resolveExecutable(named name: String, searchPath: String) -> String? { - if let cached = cachedExecutablePaths[name] { - return cached - } - - let process = Process() - process.executableURL = URL(fileURLWithPath: "/usr/bin/which") - process.arguments = [name] - process.environment = ["PATH": searchPath] - - let stdout = Pipe() - let stderr = Pipe() - process.standardOutput = stdout - process.standardError = stderr - - do { - try process.run() - process.waitUntilExit() - guard process.terminationStatus == 0 else { - cachedExecutablePaths[name] = nil - return nil - } - - let data = stdout.fileHandleForReading.readDataToEndOfFile() - let resolved = String(decoding: data, as: UTF8.self) - .trimmingCharacters(in: .whitespacesAndNewlines) - let value = resolved.isEmpty ? nil : resolved - cachedExecutablePaths[name] = value - return value - } catch { - cachedExecutablePaths[name] = nil - return nil - } - } - /// Writes an executable helper script if contents have changed. private func writeExecutable(named name: String, contents: String) throws { let fileURL = binDirectory.appendingPathComponent(name) diff --git a/Tests/ShellraiserTests/AgentRuntimeBridgeTests.swift b/Tests/ShellraiserTests/AgentRuntimeBridgeTests.swift index 0894c62..81c56a0 100644 --- a/Tests/ShellraiserTests/AgentRuntimeBridgeTests.swift +++ b/Tests/ShellraiserTests/AgentRuntimeBridgeTests.swift @@ -17,6 +17,38 @@ final class AgentRuntimeBridgeTests: XCTestCase { return AgentRuntimeBridge(rootURL: directory) } + /// Verifies environment assembly stays local and does not require synchronous executable lookup. + func testEnvironmentInjectsManagedWrapperVariablesWithoutResolvedBinaryPaths() throws { + let bridge = try makeBridge() + let surfaceId = UUID(uuidString: "00000000-0000-0000-0000-000000000901")! + + let environment = bridge.environment( + for: surfaceId, + shellPath: "/bin/zsh", + baseEnvironment: [ + "PATH": "/usr/local/bin:/usr/bin:/bin", + "TERM": "xterm-256color" + ] + ) + + XCTAssertEqual( + environment["PATH"], + "\(bridge.binDirectory.path):/usr/local/bin:/usr/bin:/bin" + ) + XCTAssertEqual(environment["TERM"], "xterm-256color") + XCTAssertEqual(environment["SHELLRAISER_EVENT_LOG"], bridge.eventLogURL.path) + XCTAssertEqual(environment["SHELLRAISER_SURFACE_ID"], surfaceId.uuidString) + XCTAssertEqual( + environment["SHELLRAISER_HELPER_PATH"], + bridge.binDirectory.appendingPathComponent("shellraiser-agent-complete").path + ) + XCTAssertEqual(environment["ZDOTDIR"], bridge.zshShimDirectory.path) + XCTAssertEqual(environment["SHELLRAISER_WRAPPER_BIN"], bridge.binDirectory.path) + XCTAssertEqual(environment["SHELLRAISER_ORIGINAL_PATH"], "/usr/local/bin:/usr/bin:/bin") + XCTAssertNil(environment["SHELLRAISER_REAL_CLAUDE"]) + XCTAssertNil(environment["SHELLRAISER_REAL_CODEX"]) + } + /// Verifies the Claude wrapper emits start, stop, permission-request, and selected notification hooks. func testPrepareRuntimeSupportWritesClaudeWrapperWithMappedNotificationHooks() throws { let bridge = try makeBridge() From d17dca008524d61fc31b3d0b8ae824f3684890ea Mon Sep 17 00:00:00 2001 From: Morten Trydal Date: Wed, 18 Mar 2026 22:44:14 +0100 Subject: [PATCH 2/2] Fix wrapper executable fallback lookup --- .../Infrastructure/Agents/AgentRuntimeBridge.swift | 6 ++++-- Tests/ShellraiserTests/AgentRuntimeBridgeTests.swift | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Sources/Shellraiser/Infrastructure/Agents/AgentRuntimeBridge.swift b/Sources/Shellraiser/Infrastructure/Agents/AgentRuntimeBridge.swift index d77aa50..d284139 100644 --- a/Sources/Shellraiser/Infrastructure/Agents/AgentRuntimeBridge.swift +++ b/Sources/Shellraiser/Infrastructure/Agents/AgentRuntimeBridge.swift @@ -181,8 +181,9 @@ final class AgentRuntimeBridge: AgentRuntimeSupporting { set -eu real="${SHELLRAISER_REAL_CLAUDE:-}" + lookup_path="${SHELLRAISER_ORIGINAL_PATH:-${PATH:-}}" if [ -z "$real" ] || [ "$real" = "$0" ]; then - real="$(/usr/bin/which claude 2>/dev/null || true)" + real="$(PATH="$lookup_path" /usr/bin/which claude 2>/dev/null || true)" fi if [ -z "$real" ] || [ "$real" = "$0" ]; then @@ -311,8 +312,9 @@ final class AgentRuntimeBridge: AgentRuntimeSupporting { set -eu real="${SHELLRAISER_REAL_CODEX:-}" + lookup_path="${SHELLRAISER_ORIGINAL_PATH:-${PATH:-}}" if [ -z "$real" ] || [ "$real" = "$0" ]; then - real="$(/usr/bin/which codex 2>/dev/null || true)" + real="$(PATH="$lookup_path" /usr/bin/which codex 2>/dev/null || true)" fi if [ -z "$real" ] || [ "$real" = "$0" ]; then diff --git a/Tests/ShellraiserTests/AgentRuntimeBridgeTests.swift b/Tests/ShellraiserTests/AgentRuntimeBridgeTests.swift index 81c56a0..0f773bb 100644 --- a/Tests/ShellraiserTests/AgentRuntimeBridgeTests.swift +++ b/Tests/ShellraiserTests/AgentRuntimeBridgeTests.swift @@ -99,10 +99,14 @@ final class AgentRuntimeBridgeTests: XCTestCase { let codexWrapperContents = try String(contentsOf: codexWrapperURL, encoding: .utf8) XCTAssertTrue(claudeWrapperContents.contains("hook-session")) + XCTAssertTrue(claudeWrapperContents.contains("lookup_path=\"${SHELLRAISER_ORIGINAL_PATH:-${PATH:-}}\"")) + XCTAssertTrue(claudeWrapperContents.contains("PATH=\"$lookup_path\" /usr/bin/which claude")) XCTAssertFalse(claudeWrapperContents.contains("SHELLRAISER_PREFERRED_CLAUDE_SESSION_ID")) XCTAssertFalse(claudeWrapperContents.contains("--session-id")) XCTAssertTrue(claudeWrapperContents.contains("claudeCode \"$surface\" exited")) XCTAssertTrue(codexWrapperContents.contains("monitor_codex_session")) + XCTAssertTrue(codexWrapperContents.contains("lookup_path=\"${SHELLRAISER_ORIGINAL_PATH:-${PATH:-}}\"")) + XCTAssertTrue(codexWrapperContents.contains("PATH=\"$lookup_path\" /usr/bin/which codex")) XCTAssertTrue(codexWrapperContents.contains("codex \"$surface\" session")) XCTAssertTrue(codexWrapperContents.contains("codex \"$surface\" exited")) XCTAssertFalse(codexWrapperContents.contains("codex \"$surface\" started"))