Skip to content

fix: resolve SSH commit/PR agents through login+interactive shell PATH - #6559

Closed
nwparker wants to merge 4 commits into
mainfrom
nwparker/fix-4720
Closed

nwparker wants to merge 4 commits into
mainfrom
nwparker/fix-4720

Conversation

@nwparker

@nwparker nwparker commented Jun 28, 2026 •

Copy link
Copy Markdown
Contributor

Description

Fixes #4720 by resolving OpenCode (and other commit/PR agent binaries) through the remote login + interactive shell environment before execution, so SSH uses the PATH the user actually has configured.

Evidence of fix

Before: a safe fake opencode was visible from the isolated remote login shell, but Orca's production OpenCode commit-message planner + SSH relay returned opencode not found on the remote PATH.

After: the focused PR resolves the agent through the remote login/interactive shell PATH and the same production planner reaches the discovered executable. The PR's targeted SSH/shell tests, lint, typechecks, and GitHub verify check pass.

ELI5

Orca was checking the remote machine's “bare hallway” PATH, while OpenCode lived in the PATH your shell sets up after you log in. The fix asks the logged-in shell where the tool is, then uses that answer.

User-facing trade-offs

  • Remote shell startup files participate in agent discovery, matching what users see in an SSH login.
  • A broken or unusually slow interactive shell setup can make first-time discovery slower; resolution is cached to avoid paying that cost on every action.
  • Local and non-OpenCode flows retain their existing behavior.

Impact

Fixes #4720. On SSH targets, OpenCode and other agent CLIs installed through nvm/fnm/Homebrew shell startup files could appear in Orca settings and terminal agents, but commit-message / PR-description generation could still fail because the relay is launched by a non-interactive SSH command with a shorter PATH.

What Changed

The SSH commit/PR agent path is now direct-first and shell-free for generation:

  • The relay always tries the inherited PATH first with spawn(binary, args).
  • If that direct spawn succeeds, there is zero login/interactive shell startup, zero rc-file execution, and zero added latency.
  • If direct spawn fails with ENOENT, the relay resolves the agent with a bounded trusted-shell command -v lookup, captures the shell-initialized PATH, caches the absolute path, then spawns the agent directly by absolute path.
  • If preflight already found the agent, generation reuses that cache and does not start a shell lookup.
  • The agent process is no longer run as <shell> -ilc 'exec "$@"' ....

Tradeoff Status

Zero-tradeoff design for normal successful SSH users: agents already on inherited PATH pay no shell startup/rc cost.

The previous open tradeoff, shell-wrapped generation, is eliminated. The remaining rc execution is only a necessary compatibility path-discovery fallback for the original bug class when no direct PATH or preflight cache can identify the agent. If OpenCode exists only because .bashrc/.zshrc mutates PATH, the relay has no deterministic non-shell source for those dynamic PATH entries. In that case Orca runs only a bounded command -v lookup, caches the result, and still launches the agent directly rather than under the shell.

Behavior Matrix

  • SSH + agent already on inherited PATH -> direct spawn only; no shell lookup.
  • SSH + preflight already found the agent -> direct spawn ENOENT can reuse the cached absolute path/PATH; no generation-time shell lookup.
  • SSH + uncached bash/zsh PATH miss -> one trusted-shell command -v lookup, then direct absolute-path spawn with the discovered PATH.
  • SSH + unsupported/missing shell or absolute binary path -> direct spawn only; no shell lookup.
  • Windows -> existing Windows-safe .cmd/cmd.exe path remains unchanged.
  • Generic fixed-binary execNonInteractive -> unchanged; the shell lookup flag is still only set by SSH commit/PR generation.

Testing

  • pnpm exec vitest run --config config/vitest.config.ts src/relay/preflight-handler.test.ts src/relay/agent-exec-handler.test.ts src/relay/agent-exec-handler-windows.test.ts src/main/providers/ssh-git-provider.test.ts -> 112 passed
  • pnpm exec oxlint on touched relay/provider files -> clean
  • pnpm exec oxfmt --check on touched relay/provider files -> clean
  • pnpm run typecheck:node -> clean
  • pnpm run build:relay -> built all 6 relay targets
  • git merge-tree --write-tree HEAD origin/main -> exit 0

Merged current origin/main into the PR branch to keep #6559 applying cleanly.

Co-authored-by: Andy Murray andym3@illinois.edu

Made with Orca 🐋

OpenCode and other agents installed via shell startup files (nvm/fnm/
Homebrew) could not be found when generating commit/PR messages over
SSH. The relay runs on the remote host via a non-interactive SSH command
whose PATH lacks those install dirs, so the agent binary was not found
and generation failed.

Opt the SSH commit/PR-generation spawn into login+interactive shell
(bash/zsh -ilc) PATH resolution, matching the established pattern in
hydrate-shell-path.ts and preflight-handler.ts. The interactive flag is
required so .bashrc/.zshrc PATH hooks get sourced. Windows and
non-bash/zsh shells are unchanged.

Fixes #4720

Supersedes #5197.

Co-authored-by: Andy Murray <andym3@illinois.edu>

Co-authored-by: Orca <help@stably.ai>
@nwparker

Copy link
Copy Markdown
Contributor Author

Test & verification evidence

Focused unit tests (the regression guards)

$ vitest run src/relay/agent-exec-handler.test.ts src/main/providers/ssh-git-provider.test.ts src/relay/agent-exec-handler-windows.test.ts

 Test Files  3 passed (3)
      Tests  77 passed (77)

New / updated tests proving the fix:

✓ AgentExecHandler > uses the default POSIX shell when requested so SSH agent commands inherit shell PATH

This new test asserts the relay spawns:
/bin/bash with args ['-ilc', 'exec "$@"', '_', 'opencode', 'run', '--model', 'opencode/deepseek-v4-flash-free']
when shell: true is passed on Linux — i.e. the agent is resolved through the user's interactive-login shell PATH.

ssh-git-provider.test.ts now asserts the commit-message and pull-request-fields generation paths forward shell: true to agent.execNonInteractive, while the generic execNonInteractive (e.g. pnpm --version) path intentionally does not.

Windows path unchanged

$ vitest run src/relay/agent-exec-handler-windows.test.ts
 Test Files  1 passed (1)
      Tests  2 passed (2)

getSpawnPlan returns early to getWindowsSafeSpawn on win32, so the Windows .cmd/cmd.exe spawn behavior is untouched.

Lint / format / typecheck (scoped to changed files)

$ oxlint src/relay/agent-exec-handler.ts src/relay/agent-exec-handler.test.ts \
         src/main/providers/ssh-git-provider.ts src/main/providers/ssh-git-provider.test.ts
(clean — no findings)

$ oxfmt --check <same 4 files>
All matched files use the correct format.

$ tsgo --noEmit -p config/tsconfig.node.json
(0 errors)

Pre-existing unrelated failures (confirmed present on main before this change): tsgo -p config/tsconfig.relay.json reports 190 TS6307 project-reference errors both before and after this change — none in the modified files.

Relay build

$ pnpm run build:relay
Built relay for linux-x64 / linux-arm64 / darwin-x64 / darwin-arm64 / win32-x64 / win32-arm64
Relay build complete.

Before / after reasoning

  • Before: SSH relay spawned the agent binary directly (getWindowsSafeSpawn → direct spawn(binary, args)). On a remote host where opencode is only on PATH via ~/.zshrc/~/.bashrc (nvm/fnm/Homebrew), the non-interactive SSH PATH didn't contain it → ENOENT / agent-not-found → commit/PR message generation failed.
  • After: the commit/PR path passes shell: true; the relay spawns <bash|zsh> -ilc 'exec "$@"' _ <binary> <args>, sourcing the user's interactive rc files so the agent resolves exactly as it does in the Orca terminal. Other shells fall back to direct spawn; Windows is unchanged.

Security self-audit

The shell invocation passes the binary and arguments as separate positional parameters (exec "$@" with _ as $0). The shell does not re-tokenize them, so metacharacters in a binary name/arg are a single argv element — no injection surface. No new deps, no eval, no secret/network access. Re-reviewed the superseded #5197 diff for malicious intent; none found.

@coderabbitai

coderabbitai Bot commented Jun 28, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

agent.execNonInteractive now supports a shell: true mode. A new getSpawnPlan function in AgentExecHandler selects between the existing Windows-safe spawn and a login-shell wrapper (-ilc with exec "$@") for bash/zsh on non-Windows when shell is enabled. SshGitProvider.executeCommitMessagePlan passes shell: true in its runQueuedNonInteractiveExec payload, and the internal payload type is extended with an optional shell?: boolean field. Tests are updated to assert the new field and a new relay test verifies the -ilc spawn path on Linux.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the SSH shell-path fix for commit/PR agent resolution.
Linked Issues check ✅ Passed The SSH login+interactive shell spawn and shell: true provider path address the agent-not-found failure described in #4720.
Out of Scope Changes check ✅ Passed The changes are confined to SSH spawn behavior and regression tests, with no unrelated feature work evident.
Description check ✅ Passed The description is detailed and covers summary, impact, tradeoffs, behavior, and testing, with only some template sections missing.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a3f6a0a2-3f84-41cf-b776-13713e25781f

📥 Commits

Reviewing files that changed from the base of the PR and between b45cb54 and 3f37e6d.

📒 Files selected for processing (4)
  • src/main/providers/ssh-git-provider.test.ts
  • src/main/providers/ssh-git-provider.ts
  • src/relay/agent-exec-handler.test.ts
  • src/relay/agent-exec-handler.ts

Comment thread src/relay/agent-exec-handler.ts Outdated
Comment on lines +77 to +89
const shell = resolveDefaultShell()
const shellName = basename(shell).toLowerCase()
if (shellName !== 'bash' && shellName !== 'zsh') {
return { spawnCmd: binary, spawnArgs: args }
}
// Why: SSH relay processes are launched by a non-interactive SSH command,
// whose PATH often lacks nvm/fnm/Homebrew agent installs. The interactive
// flag (`-i`) is required so .bashrc/.zshrc — where those PATH hooks live —
// get sourced; a plain login shell (`-lc`) would skip them. This resolves
// remote agent commands like the user's Orca terminal shell does.
return {
spawnCmd: shell,
spawnArgs: ['-ilc', 'exec "$@"', '_', binary, ...args]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Only enable the wrapper when the user's shell is explicitly bash/zsh.

resolveDefaultShell() falls back to /bin/bash//bin/zsh when SHELL is unset, so this branch changes behavior for “unknown shell” hosts instead of preserving the existing direct-spawn path. That means a fish/csh user can still get bash/zsh startup files sourced here, which breaks the bash/zsh-only contract in this PR.

Suggested fix
-import { resolveDefaultShell } from './pty-shell-utils'
...
-  const shell = resolveDefaultShell()
-  const shellName = basename(shell).toLowerCase()
+  const shell = typeof env.SHELL === 'string' ? env.SHELL : ''
+  const shellName = shell ? basename(shell).toLowerCase() : ''
   if (shellName !== 'bash' && shellName !== 'zsh') {
     return { spawnCmd: binary, spawnArgs: args }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const shell = resolveDefaultShell()
const shellName = basename(shell).toLowerCase()
if (shellName !== 'bash' && shellName !== 'zsh') {
return { spawnCmd: binary, spawnArgs: args }
}
// Why: SSH relay processes are launched by a non-interactive SSH command,
// whose PATH often lacks nvm/fnm/Homebrew agent installs. The interactive
// flag (`-i`) is required so .bashrc/.zshrc — where those PATH hooks live —
// get sourced; a plain login shell (`-lc`) would skip them. This resolves
// remote agent commands like the user's Orca terminal shell does.
return {
spawnCmd: shell,
spawnArgs: ['-ilc', 'exec "$@"', '_', binary, ...args]
const shell = typeof env.SHELL === 'string' ? env.SHELL : ''
const shellName = shell ? basename(shell).toLowerCase() : ''
if (shellName !== 'bash' && shellName !== 'zsh') {
return { spawnCmd: binary, spawnArgs: args }
}
// Why: SSH relay processes are launched by a non-interactive SSH command,
// whose PATH often lacks nvm/fnm/Homebrew agent installs. The interactive
// flag (`-i`) is required so .bashrc/.zshrc — where those PATH hooks live —
// get sourced; a plain login shell (`-lc`) would skip them. This resolves
// remote agent commands like the user's Orca terminal shell does.
return {
spawnCmd: shell,
spawnArgs: ['-ilc', 'exec "$@"', '_', binary, ...args]
🧰 Tools
🪛 ast-grep (0.44.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { exec, spawn, type ChildProcess } from 'child_process'
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

@nwparker

nwparker commented Jun 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Follow-up pushed in 53ed94dde9.

What changed:

  • shell: true is now direct-first: the relay spawns the agent normally, and only retries through login+interactive bash/zsh after the bare command fails with ENOENT.
  • The fallback no longer uses resolveDefaultShell()'s /bin/bash//bin/zsh default. It only uses explicit $SHELL, or the account login shell when $SHELL is unset, and only for bash/zsh.
  • Moved spawn planning into src/relay/agent-exec-spawn-plan.ts so agent-exec-handler.ts stays under max-lines without a lint disable.

Tradeoff status: not fully zero for the original PATH-missing case. If an SSH agent is only discoverable because .bashrc/.zshrc adds nvm/fnm/Homebrew dirs, one login+interactive shell startup is still necessary. The tradeoff is eliminated for agents already on inherited PATH, unknown/unsupported shells, Windows, local generation, and generic fixed-binary exec.

Validation:

  • pnpm exec vitest run --config config/vitest.config.ts src/relay/agent-exec-handler.test.ts src/relay/agent-exec-handler-windows.test.ts src/main/providers/ssh-git-provider.test.ts → 80 passed
  • pnpm exec oxlint src/relay/agent-exec-handler.ts src/relay/agent-exec-handler.test.ts src/relay/agent-exec-spawn-plan.ts src/main/providers/ssh-git-provider.ts src/main/providers/ssh-git-provider.test.ts → clean
  • pnpm exec oxfmt --check on the same touched files → clean
  • pnpm run typecheck:node → clean
  • pnpm run build:relay → built all 6 relay targets
  • git merge-tree --write-tree HEAD origin/main → exit 0

@nwparker

Copy link
Copy Markdown
Contributor Author

Follow-up pushed in 93ac20babe and merged with current origin/main in e7a807a7af.

What changed:

  • Removed shell-wrapped agent generation. On SSH, commit/PR generation still tries direct spawn first; on ENOENT it now performs only a bounded trusted-shell command -v lookup, caches the absolute path plus shell PATH, and then spawns the agent directly by absolute path.
  • Preflight and generation share the cache. If preflight already found OpenCode, generation pays no shell lookup cost.
  • The fallback is constrained to bash/zsh lookup for uncached generation misses, skips the redundant inherited /bin/sh lookup after direct ENOENT, and preserves Windows/generic fixed-binary behavior.

Tradeoff status: zero shell startup/rc cost for successful normal SSH users, and zero shell-wrapped generation. The only remaining rc execution is the necessary compatibility path-discovery case when the agent genuinely exists only after .bashrc/.zshrc mutates PATH and no preflight cache exists; without evaluating that startup path, the relay has no deterministic source for those dynamic PATH entries.

Validation:

  • pnpm exec vitest run --config config/vitest.config.ts src/relay/preflight-handler.test.ts src/relay/agent-exec-handler.test.ts src/relay/agent-exec-handler-windows.test.ts src/main/providers/ssh-git-provider.test.ts -> 112 passed
  • pnpm exec oxlint on touched relay/provider files -> clean
  • pnpm exec oxfmt --check on touched relay/provider files -> clean
  • pnpm run typecheck:node -> clean
  • pnpm run build:relay -> built all 6 relay targets
  • git merge-tree --write-tree HEAD origin/main -> exit 0

@OrcaWin

OrcaWin commented Oct 11, 2026

Copy link
Copy Markdown
Collaborator

Closing as obsolete after the merged SSH relay retirement in #27214 and #27215. This PR concerns the relay-backed remote Orca CLI bridge and agent execution. #27214 removed the desktop relay connection/fallback path; #27215 deleted the SSH relay server, deployment assets, and relay-only tests. SSH hosts now use the managed Orca server (orcad), and unsupported hosts fail explicitly, so this relay-specific work is no longer planned. This closes the proposed relay implementation; it does not claim that any independent local, WSL, or managed-server behavior was fixed.

@OrcaWin

OrcaWin commented Oct 11, 2026

Copy link
Copy Markdown
Collaborator

The relay files this PR touched were deleted, so it can't be revived as-is. The symptom still applies on the managed remote server, though, so I've carried it over to #27395.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: unable to use OpenCode as the agent for PR commits message generation

2 participants