Skip to content
Closed
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
184 changes: 176 additions & 8 deletions Sources/CodexBarCore/PathEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ public enum BinaryLocator {
commandV: (String, String?, TimeInterval, FileManager) -> String? = ShellCommandLocator.commandV,
aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = ShellCommandLocator
.resolveAlias,
launchCandidateFilter: (String, FileManager) -> Bool = CodexLaunchPreflight.isLaunchCandidateAllowed,
fileManager: FileManager = .default,
home: String = NSHomeDirectory()) -> String?
{
Expand All @@ -93,10 +94,25 @@ public enum BinaryLocator {
loginPATH: loginPATH,
commandV: commandV,
aliasResolver: aliasResolver,
wellKnownPaths: self.codexWellKnownPaths(home: home),
launchCandidateFilter: launchCandidateFilter,
fileManager: fileManager,
home: home)
}

/// Well-known installation paths for the signed Codex desktop app CLI.
/// Keep these after PATH lookups, but use them as a safe fallback when a PATH shim is blocked.
static func codexWellKnownPaths(home: String) -> [String] {
#if os(macOS)
[
"\(home)/Applications/Codex.app/Contents/Resources/codex",
"/Applications/Codex.app/Contents/Resources/codex",
]
#else
[]
#endif
}

public static func resolveGeminiBinary(
env: [String: String] = ProcessInfo.processInfo.environment,
loginPATH: [String]? = LoginShellPathCache.shared.current,
Expand Down Expand Up @@ -179,6 +195,7 @@ public enum BinaryLocator {
commandV: (String, String?, TimeInterval, FileManager) -> String?,
aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String?,
wellKnownPaths: [String] = [],
launchCandidateFilter: (String, FileManager) -> Bool = { _, _ in true },
fileManager: FileManager,
home: String) -> String?
{
Expand All @@ -190,7 +207,11 @@ public enum BinaryLocator {

// 2) Login-shell PATH (captured once per launch)
if let loginPATH,
let pathHit = self.find(name, in: loginPATH, fileManager: fileManager)
let pathHit = self.find(
name,
in: loginPATH,
fileManager: fileManager,
launchCandidateFilter: launchCandidateFilter)
{
return pathHit
}
Expand All @@ -200,51 +221,198 @@ public enum BinaryLocator {
let pathHit = self.find(
name,
in: existingPATH.split(separator: ":").map(String.init),
fileManager: fileManager)
fileManager: fileManager,
launchCandidateFilter: launchCandidateFilter)
{
return pathHit
}

// 4) Well-known installation paths (e.g. Homebrew, cmux.app bundle, ~/.claude/bin).
// Prefer these before shell probing to avoid running interactive shell init for common installs.
for candidate in wellKnownPaths where fileManager.isExecutableFile(atPath: candidate) {
for candidate in wellKnownPaths
where fileManager.isExecutableFile(atPath: candidate) && launchCandidateFilter(candidate, fileManager)
{
return candidate
}

// 5) Interactive login shell lookup (captures nvm/fnm/mise paths from .zshrc/.bashrc)
if let shellHit = commandV(name, env["SHELL"], 2.0, fileManager),
fileManager.isExecutableFile(atPath: shellHit)
fileManager.isExecutableFile(atPath: shellHit),
launchCandidateFilter(shellHit, fileManager)
{
return shellHit
}

// 5b) Alias fallback (login shell); only attempt after all standard lookups fail.
if let aliasHit = aliasResolver(name, env["SHELL"], 2.0, fileManager, home),
fileManager.isExecutableFile(atPath: aliasHit)
fileManager.isExecutableFile(atPath: aliasHit),
launchCandidateFilter(aliasHit, fileManager)
{
return aliasHit
}

// 6) Minimal fallback
let fallback = ["/usr/bin", "/bin", "/usr/sbin", "/sbin"]
if let pathHit = self.find(name, in: fallback, fileManager: fileManager) {
if let pathHit = self.find(
name,
in: fallback,
fileManager: fileManager,
launchCandidateFilter: launchCandidateFilter)
{
return pathHit
}

return nil
}

private static func find(_ binary: String, in paths: [String], fileManager: FileManager) -> String? {
private static func find(
_ binary: String,
in paths: [String],
fileManager: FileManager,
launchCandidateFilter: (String, FileManager) -> Bool = { _, _ in true }) -> String?
{
for path in paths where !path.isEmpty {
let candidate = "\(path.hasSuffix("/") ? String(path.dropLast()) : path)/\(binary)"
if fileManager.isExecutableFile(atPath: candidate) {
if fileManager.isExecutableFile(atPath: candidate), launchCandidateFilter(candidate, fileManager) {
return candidate
}
}
return nil
}
}

public enum CodexLaunchPreflight {
public static func isLaunchCandidateAllowed(path: String, fileManager: FileManager = .default) -> Bool {
#if os(macOS)
let realPath = URL(fileURLWithPath: path).resolvingSymlinksInPath().path
let pathsToCheck = [path, realPath] + self.nativeCodexExecutableCandidates(
for: realPath,
fileManager: fileManager)

for candidate in Set(pathsToCheck) where self.hasBlockedExtendedAttribute(path: candidate) {
return false
}

guard let native = pathsToCheck.first(where: { self.isMachOExecutable(atPath: $0) }),
let assessment = self.spctlAssessment(path: native)
else {
return true
}

return !self.isExplicitlyBlockedAssessment(assessment)
#else
_ = path
_ = fileManager
return true
#endif
}

#if os(macOS)
private static func nativeCodexExecutableCandidates(for path: String, fileManager: FileManager) -> [String] {
let url = URL(fileURLWithPath: path)
guard url.lastPathComponent == "codex.js" else { return [] }

let packageRoot = url.deletingLastPathComponent().deletingLastPathComponent()
return self.npmNativeCodexCandidates(packageRoot: packageRoot)
.map(\.path)
.filter { fileManager.isExecutableFile(atPath: $0) }
}

private static func npmNativeCodexCandidates(packageRoot: URL) -> [URL] {
guard let target = self.darwinCodexTarget else { return [] }
let optionalPackage = packageRoot
.appendingPathComponent("node_modules")
.appendingPathComponent("@openai")
.appendingPathComponent(target.packageName)

return [
optionalPackage,
packageRoot,
].map {
$0.appendingPathComponent("vendor")
.appendingPathComponent(target.triple)
.appendingPathComponent("codex")
.appendingPathComponent("codex")
}
}

private static var darwinCodexTarget: (packageName: String, triple: String)? {
#if arch(arm64)
("codex-darwin-arm64", "aarch64-apple-darwin")
#elseif arch(x86_64)
("codex-darwin-x64", "x86_64-apple-darwin")
#else
nil
#endif
}

private static func hasBlockedExtendedAttribute(path: String) -> Bool {
self.hasExtendedAttribute("com.apple.malware", path: path) ||
self.hasExtendedAttribute("com.apple.quarantine", path: path)
}

private static func hasExtendedAttribute(_ name: String, path: String) -> Bool {
path.withCString { pathPointer in
name.withCString { namePointer in
getxattr(pathPointer, namePointer, nil, 0, 0, 0) >= 0
}
}
}

private static func isMachOExecutable(atPath path: String) -> Bool {
guard let handle = try? FileHandle(forReadingFrom: URL(fileURLWithPath: path)) else { return false }
defer { try? handle.close() }

guard let data = try? handle.read(upToCount: 4), data.count == 4 else { return false }
let bytes = [UInt8](data)
return bytes == [0xFE, 0xED, 0xFA, 0xCE] ||
bytes == [0xCE, 0xFA, 0xED, 0xFE] ||
bytes == [0xFE, 0xED, 0xFA, 0xCF] ||
bytes == [0xCF, 0xFA, 0xED, 0xFE] ||
bytes == [0xCA, 0xFE, 0xBA, 0xBE] ||
bytes == [0xCA, 0xFE, 0xBA, 0xBF]
}

private static func spctlAssessment(path: String, timeout: TimeInterval = 2.0) -> String? {
let spctlPath = "/usr/sbin/spctl"
guard FileManager.default.isExecutableFile(atPath: spctlPath) else { return nil }

let process = Process()
process.executableURL = URL(fileURLWithPath: spctlPath)
process.arguments = ["--assess", "--type", "execute", "--verbose=4", path]

let output = Pipe()
process.standardOutput = output
process.standardError = output

let finished = DispatchSemaphore(value: 0)
process.terminationHandler = { _ in finished.signal() }

do {
try process.run()
} catch {
return nil
}

if finished.wait(timeout: .now() + timeout) != .success {
process.terminate()
return nil
}

let data = output.fileHandleForReading.readDataToEndOfFile()
return String(data: data, encoding: .utf8)
}

private static func isExplicitlyBlockedAssessment(_ assessment: String) -> Bool {
let lower = assessment.lowercased()
return lower.contains("cssmerr_tp_cert_revoked") ||
lower.contains("revoked") ||
lower.contains("malware") ||
lower.contains("quarantine")
}
#endif
}

public enum ShellCommandLocator {
static func test_runShellCommand(
shell: String,
Expand Down
44 changes: 44 additions & 0 deletions Tests/CodexBarTests/PathBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,50 @@ struct PathBuilderTests {
#expect(resolved == "/env/bin/codex")
}

@Test
func `skips blocked codex path and falls back to signed app binary`() {
let blockedPath = "/usr/local/bin/codex"
let appPath = "/Applications/Codex.app/Contents/Resources/codex"
let fm = MockFileManager(executables: [blockedPath, appPath])
var checked: [String] = []

let resolved = BinaryLocator.resolveCodexBinary(
env: ["PATH": "/usr/local/bin"],
loginPATH: nil,
commandV: { _, _, _, _ in nil },
aliasResolver: { _, _, _, _, _ in nil },
launchCandidateFilter: { path, _ in
checked.append(path)
return path != blockedPath
},
fileManager: fm,
home: "/Users/test")

#expect(resolved == appPath)
#expect(checked == [blockedPath, appPath])
}

@Test
func `explicit codex override bypasses launch candidate fallback`() {
let overridePath = "/custom/bin/codex"
let appPath = "/Applications/Codex.app/Contents/Resources/codex"
let fm = MockFileManager(executables: [overridePath, appPath])
var checked: [String] = []

let resolved = BinaryLocator.resolveCodexBinary(
env: ["CODEX_CLI_PATH": overridePath],
loginPATH: nil,
launchCandidateFilter: { path, _ in
checked.append(path)
return false
},
fileManager: fm,
home: "/Users/test")

#expect(resolved == overridePath)
#expect(checked.isEmpty)
}

@Test
func `resolves codex from interactive shell`() {
let fm = MockFileManager(executables: ["/shell/bin/codex"])
Expand Down