diff --git a/Sources/Shellraiser/Infrastructure/Agents/AgentRuntimeBridge.swift b/Sources/Shellraiser/Infrastructure/Agents/AgentRuntimeBridge.swift index b355842..8de057d 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() { @@ -105,53 +104,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) @@ -236,8 +191,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 @@ -366,8 +322,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 ca57b40..89fe5b7 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() @@ -71,10 +103,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"))