Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 4 additions & 47 deletions Sources/Shellraiser/Infrastructure/Agents/AgentRuntimeBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions Tests/ShellraiserTests/AgentRuntimeBridgeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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"))
Expand Down
Loading