Walk PATH (and honor defaults.binaries) for agent binary discovery#485
Conversation
…W-484) `OpenAICodexAgent.findBinary()` and `CursorAgent.findBinary()` only checked three hardcoded paths (`/opt/homebrew/bin/...`, `/usr/local/bin/...`, `~/.local/bin/...`). Codex is distributed as `npm i -g @openai/codex` — under nvm/Volta/pnpm/asdf the binary never lands in any of those. When discovery returned nil, `AppDelegate.launchMainApp` skipped registration, so the picker silently never showed the agent even though it was installed. This refactors discovery into a default `CodingAgent.findBinary()` impl: explicit `defaults.binaries.<kind>` override → PATH walk via `ShellEnvironment.findExecutable` (same way `command -v` resolves) → hardcoded `fallbackCandidates`. Per-agent impls are gone; each agent just declares its fallback list. AppDelegate now loads config before registration, populates `BinaryOverrides.shared`, and logs the resolved path so users can verify which install Crow picked up. 🐦⬛ Generated with Claude Code, orchestrated by Crow Co-Authored-By: Claude <noreply@anthropic.com> Crow-Session: CEB4D4CB-61A8-468F-8A3A-0E72FF5610C6
dhilgaertner
left a comment
There was a problem hiding this comment.
Code & Security Review
Solid, well-scoped refactor: collapsing three near-identical findBinary() impls into one default on CodingAgent (override → PATH walk → fallback) is the right shape, the kind raw-values line up with the JSON keys, the stale-override fall-through is handled, and the config field decodes forward-compatibly. Verified launchCommandToken (codex/agent/claude) and kind (codex/cursor/claude-code) both resolve correctly, and CrowCore builds clean. One blocking test defect and one behavioral concern.
Critical Issues
🔴 Red — BinaryOverrides test suite is racy and fails reproducibly under parallel execution
Packages/CrowCore/Tests/CrowCoreTests/BinaryOverridesTests.swift
All four tests mutate the process-global BinaryOverrides.shared singleton, and Swift Testing runs tests in parallel within a suite by default. The per-test defer { set([:]) } resets don't isolate anything — a concurrent test's set(...) clobbers another's state mid-assertion. The PR's "all green" claim doesn't hold: running swift test --filter BinaryOverrides 6× here failed 5 times, e.g.:
✘ emptyByDefault() — (path(for: .cursor) → "/tmp/agent") == nil
✘ setStoresRawKeyedByAgentKind() — (path(for: .cursor) → nil) == "/tmp/agent"
✘ setReplacesPreviousMap() — (path(for: .cursor) → nil) == "/tmp/agent"
This will flake CI. Fix: serialize the suite — @Suite("BinaryOverrides", .serialized) — and likewise add .serialized to @Suite("OpenAICodexAgent") in Packages/CrowCodex/Tests/CrowCodexTests/OpenAICodexAgentTests.swift, since its two new findBinary* tests mutate the same global (that suite passed 6/6 here only by timing luck — findBinaryHonorsBinaryOverride vs findBinaryIgnoresOverrideWhenPathMissing race on codex). .serialized is sufficient because only these two suites touch the global, and they live in separate targets.
Code Quality
🟡 Yellow — PATH walk for Cursor's generic agent binary name risks false-positive registration
Packages/CrowCore/Sources/CrowCore/Agent/CodingAgent.swift:129 + CursorAgent.launchCommandToken = "agent"
The old impl checked only three explicit .../agent paths. The new PATH walk registers Cursor for any executable named agent anywhere on the user's $PATH. agent is a notably generic name — CI build agents (Azure DevOps, TeamCity), etc., ship a binary literally called agent. On such a machine Crow would now mis-register Cursor and later launch the wrong binary. codex/claude are specific enough not to collide; agent is the outlier. Worth at minimum a code comment acknowledging the tradeoff, and ideally a sanity check (or documenting defaults.binaries.cursor as the escape hatch in the user-facing config docs).
Security Review
Strengths:
- No injection surface added —
findExecutabledoes filesystemisExecutableFilechecks, not shell evaluation, and POSIX empty-PATH entries (which would mean CWD) are dropped bysplit(separator:), avoiding accidental CWD execution. BinaryOverridesis an opt-in absolute-path override from the user's own config; stale/missing paths fall through safely rather than breaking registration.
Concerns: None beyond the Yellow above.
Summary Table
| Color | Meaning | Verdict effect |
|---|---|---|
| Red | Must fix | Request changes |
| Yellow | Should fix | Request changes |
| Green | Consider | Approve allowed |
Recommendation: Request Changes — driven by [1 Red, 1 Yellow, 0 Green] findings.
…lision risk Reviewer (dhilgaertner) caught two issues on #485: 1. (Red) `BinaryOverrides` and `OpenAICodexAgent` suites mutate the process-global `BinaryOverrides.shared` singleton. Swift Testing runs tests in parallel within a suite by default, so the per-test `defer { set([:]) }` resets don't isolate anything — a concurrent `set(...)` clobbers another test's state mid-assertion (the reviewer reproduced 5/6 failures under `--filter`). Marking both suites `.serialized` is sufficient because only these two touch the global, and they live in separate test targets. 2. (Yellow) Cursor's CLI binary is named `agent`, which is generic enough that CI runner installs (Azure DevOps, TeamCity) ship a binary with the same name. The PATH walk in the default `findBinary()` could mis-register Cursor on a build machine. Document the tradeoff at the `launchCommandToken` declaration and point users at `defaults.binaries.cursor` as the escape hatch — the explicit-override step runs before the PATH walk and pins the resolution. 🐦⬛ Generated with Claude Code, orchestrated by Crow Co-Authored-By: Claude <noreply@anthropic.com> Crow-Session: CEB4D4CB-61A8-468F-8A3A-0E72FF5610C6
dhilgaertner
left a comment
There was a problem hiding this comment.
Code & Security Review
Refactors per-agent binary discovery into a single default CodingAgent.findBinary() impl: explicit defaults.binaries.<kind> override → PATH walk (ShellEnvironment.findExecutable) → hardcoded fallbackCandidates. This correctly fixes the CROW-484 gap where npm-global / nvm / Volta / pnpm / asdf installs were invisible. Verified by building and running the full test suites for CrowCore, CrowCodex, CrowCursor, and CrowClaude — all green (269 + 29 + 29 + 41 tests).
Critical Issues
None.
Security Review
Strengths:
- No new attack surface: discovery is read-only (
isExecutableFile), no shell-out, no command construction from user input. - Stale-override handling is correct and defensive —
findBinary()re-verifiesisExecutableFile(atPath:)on the configured path before returning it (CodingAgent.swift:124), so a moved/uninstalled binary falls through to PATH/fallback rather than pinning a dead path or breaking registration. Covered byfindBinaryIgnoresOverrideWhenPathMissing. BinaryOverridessingleton is properly guarded byNSLock; test suites that mutate the shared state are.serializedto avoid cross-suite leakage.
Concerns:
- None blocking. The Cursor
agentPATH-collision risk (a CI runner'sagentbinary could resolve as Cursor's CLI on a build machine) is real but is thoroughly documented atCursorAgent.swiftand fully mitigated by thedefaults.binaries.cursorescape hatch. Acceptable tradeoff for a desktop workstation app.
Code Quality
- Clean abstraction: the discovery logic now lives in one place and each agent just declares
fallbackCandidates;hasCommandis correctly re-expressed asfindExecutable(_:) != nilso the two stay in sync (pinned byhasCommandAgreesWithFindExecutable). ConfigDefaults.binariesdecoding is forward/backward-compatible — a missing key decodes to[:](configDefaultsBinariesDefaultsEmpty), and the field round-trips through the synthesized encoder.AppDelegate.launchMainAppconfig-load reorder is safe: config is loaded once up front,BinaryOverrides.sharedis populated before the registration gates run, and the later duplicate load was correctly removed. The newregistered at <path>NSLog aids field debugging.- Good test coverage across all three layers (override map, PATH resolver, config decode, agent-level honor/ignore).
Summary Table
| Color | Meaning | Verdict effect |
|---|---|---|
| Red | Must fix | Request changes |
| Yellow | Should fix | Request changes |
| Green | Consider | Approve allowed |
Recommendation: Approve — driven by [0 Red, 0 Yellow, 1 Green] findings. The single Green item (Cursor agent name collision) is already documented and mitigated in the PR.
…87) (#489) Closes #487 ## Summary - **Scaffolder**: build a per-devroot symlink farm at `${devRoot}/.claude/bin/<name>` from every `defaults.binaries` entry whose target is executable. Loop is idempotent, reaps stale symlinks when keys are removed, and skips non-executable / nonexistent targets so a misconfigured path never shadows a working PATH install. - **TmuxBackend**: `configure(crowBinDir:)` threads the bin dir through; `registerTerminal` exports `CROW_BIN_DIR` and seeds the spawned window's `PATH` with the bin dir in front via `tmux new-window -e`. - **Shell wrapper**: re-prepends `$CROW_BIN_DIR` to `PATH` after sourcing the user's `.zshrc` / `.bashrc` so a user `export PATH=…` can't shadow the symlink farm. Fish / unknown-shell branches inherit the already-seeded `PATH` from the wrapper's outer scope. ## Effect ``` agent shell> which corveil /Users/you/devroot/.claude/bin/corveil agent shell> corveil --version # the binary you configured in Settings → General ``` Embedded skills like `/query-corveil` (shipped inside the `corveil` binary itself) keep using bare `corveil` invocations — Crow makes those resolve to the user-configured binary without touching the skill content. Generic in the map: `codex`, `cursor`, and any future tools share the same mechanism via #484's existing `defaults.binaries` schema. ## Test plan - [x] `swift test --filter "ScaffolderBinarySymlinkTests"` — 6 tests covering executable targets, non-executable skip, nonexistent skip, stale reaping, non-symlink files left alone, reconfiguration re-points. - [x] `swift test --filter "TmuxBackendCrowBinDirTests"` — `configure(crowBinDir:)` propagation + default empty. - [ ] Manual: set `defaults.binaries.corveil` in Settings to an executable; open a Claude Code session; run `which corveil` → reports `${devRoot}/.claude/bin/corveil`; `corveil --version` matches the configured binary; `/query-corveil` invokes that binary. - [ ] Manual: unset `defaults.binaries.corveil`; relaunch; symlink removed; bare `corveil` falls back to PATH (or unresolved if not installed). - [ ] Manual: set `defaults.binaries.corveil` to a nonexistent path; relaunch; one-line warning in `[Scaffolder]` log; no broken symlink created. - [ ] Manual: also set `defaults.binaries.codex` / `defaults.binaries.cursor`; both symlinks materialize. ## Related - #482 — Corveil CLI picker that introduced `defaults.binaries.corveil`. - #485 — PATH-walk + `defaults.binaries` override for agent binary discovery; this PR reuses its schema (`[String: String]` map). 🐦⬛ Generated with Crow via Claude Code
Closes #484
Summary
OpenAICodexAgent.findBinary()andCursorAgent.findBinary()only checked three hardcoded install paths and returnednilfor npm-global / nvm / Volta / pnpm / asdf installs, silently hiding installed agents from the per-session picker.CodingAgent.findBinary()impl: explicitdefaults.binaries.<kind>override → PATH walk (ShellEnvironment.findExecutable, same waycommand -vresolves) → hardcodedfallbackCandidates.fallbackCandidates; the discovery logic lives in one place.AppDelegate.launchMainApploads config first, populatesBinaryOverrides.sharedfromdefaults.binaries, then registers agents and logs the resolved path so users can verify which install was picked up.Acceptance
brew install-style at/opt/homebrew/bin/codex(existing path, still covered via fallback)npm i -g @openai/codexunder standard npm prefix (resolved via PATH)~/.nvm/versions/node/*/bin/codex(resolved via PATH)~/.local/bin/codex(existing path, still covered via fallback)agentbinary).defaults.binaries.codexto an explicit path causes Crow to use it regardless of discovery (verified byfindBinaryHonorsBinaryOverridetest).findBinary()returnsnilwhen override + PATH + fallback all miss)."OpenAI Codex agent registered at <resolved path>"so users can verify which install was picked up.~/.local/bin/codex) stops being necessary.Note: the Corveil binary picker referenced in #482 has not landed on this branch yet. This PR introduces the
defaults.binariesschema field; when #482 merges it just adds acorveilrow to the same map.Test plan
swift test --arch arm64in each of the package roots (CrowCore,CrowCodex,CrowCursor,CrowClaude,CrowPersistence) and at the repo root — all green.BinaryOverrides(4 cases),ShellEnvironment.findExecutable(3 cases),ConfigDefaults.binariesdecode (2 cases),OpenAICodexAgent.findBinaryhonors/ignores stale override (2 cases).🤖 Generated with Claude Code