Skip to content

fix(runtime): refuse SSH hosts in project setup instead of acting locally - #10799

Merged
brennanb2025 merged 2 commits into
mainfrom
brennanb2025/fix-ssh-host-setup-guard
Jul 27, 2026
Merged

brennanb2025 merged 2 commits into
mainfrom
brennanb2025/fix-ssh-host-setup-guard

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Jul 26, 2026 •

Copy link
Copy Markdown
Contributor

Before / After

Before — the loud failure. Setting up a project on an SSH host from the CLI fails with an error about a path on the local machine:

$ orca project setup-clone --project github:stablyai/orca \
    --host ssh:openclaw --url ... --destination /home/brennan
ENOENT: no such file or directory, mkdir '/home/brennan'

Nothing on openclaw was ever contacted — a target that doesn't exist at all (--host ssh:this-target-does-not-exist-xyz) produces the identical error.

Before — the quiet failure, which is the real problem. Pick a destination that happens to exist locally and the same command succeeds:

$ orca project setup-clone --project github:stablyai/orca \
    --host ssh:openclaw --url ... --destination ~/src
...
hostId: ssh:openclaw
path: /Users/brennan/src/orca
...

Orca reports the project as set up on openclaw and shows it there. But nothing was ever put on openclaw — the checkout is on the local Mac, and every later operation on that project keeps running on the Mac while the UI attributes it to the SSH host. The user is never told. There is no error to search for and nothing in the output looks wrong unless you notice the local path sitting under a remote host.

After. The command refuses immediately and says where to go instead:

$ orca project setup-clone --project github:stablyai/orca \
    --host ssh:openclaw --url ... --destination ~/src
SSH hosts are not supported by this operation. Set the project up from the Orca desktop app, which owns the SSH connection.

Exit code 1. Nothing is cloned, nothing is registered, no misattributed project is left behind. orca project setup-existing-folder --host ssh:... behaves the same way, and both commands now say so in --help.

Unchanged. Setting up projects on SSH hosts from the Orca desktop app works exactly as before — that path always did the real remote work and is where users are pointed. --host local and --host runtime:<env> are untouched.

Problem

projectHostSetup.clone and projectHostSetup.setupExistingFolder accept an executionHostId and thread it all the way down, but never use it for routing:

  • cloneRepoAfterPathLock runs an unconditional local mkdir and a local gitSpawn(['clone', ...]). executionHostId is used in exactly one place — runtimeRepoMatchesExecutionHost at the existing-repo lookup — never to pick a transport.
  • addRepo validates with isGitRepo(path), which is existsSync/statSync on the local box.

So --host ssh:<target> clones and probes on the local machine, then registers the result as living on the SSH host.

Found while setting up a real SSH host from the CLI:

$ orca project setup-clone --project github:stablyai/orca \
    --host ssh:openclaw --url ... --destination /home/brennan/orca
ENOENT: no such file or directory, mkdir '/home/brennan'

A deliberately fake target (--host ssh:this-target-does-not-exist-xyz) produces the identical error, confirming the SSH target is never resolved.

That only failed loudly because /home/brennan doesn't exist on macOS. With a plausible local destination the clone succeeds and writes a ProjectHostSetup record claiming a local checkout lives on the remote — silent state corruption rather than an error.

Why fail closed rather than route it

The two layers are complementary today, and the split is deliberate:

host path
local, ssh: desktop IPC → addLocalRepoFromPath / addRemoteRepoFromPath, cloneRemoteRepo
runtime: runtime RPC (this code)

The renderer's getProjectSetupRuntimeTarget maps only runtime: to the runtime RPC; every ssh: variant — including ephemeral-VM ssh:runtime-ssh-* — goes to IPC. The IPC handler symmetrically rejects runtime: with "Runtime hosts must be set up through the runtime projectHostSetup.setupExistingFolder RPC."

So no legitimate caller sends ssh: here; only the CLI can, and it is always wrong. This PR makes that explicit instead of silently doing local work.

Routing these RPCs through the SSH providers is a follow-up — SshGitProvider.clone() and addRemoteRepoFromPath already exist and are what the desktop calls, so the capability is there; it just isn't wired to the RPC.

Test

refuses SSH hosts instead of setting the project up on the local machine asserts both entry points reject, and — the part that matters — that gitSpawn was never called and no repo was registered. Verified it fails without the guard, with the same ENOENT ... mkdir seen in production.

Validation

  • orca-runtime.test.ts: 880/880 pass
  • typecheck:tsc:node: clean
  • oxlint on both files: clean
  • check:max-lines-ratchet: OK, no new bypasses

…ally

projectHostSetup.clone and .setupExistingFolder threaded executionHostId all
the way down but never used it for routing: cloneRepo runs a local mkdir plus
a local gitSpawn, and addRepo probes the path with existsSync/statSync. An
`ssh:` host therefore cloned and validated on the *local* machine and then
registered the result as living on the SSH host.

It only failed loudly here because the remote path did not exist locally. With
a plausible destination the clone succeeds and writes a setup record pointing
at the wrong machine.

Nothing legitimate sends `ssh:` to these RPCs: the renderer maps every ssh
host (including ephemeral-VM `ssh:runtime-ssh-*`) to the desktop IPC path,
which dispatches to addRemoteRepoFromPath/cloneRemoteRepo, and the IPC handler
symmetrically rejects `runtime:`. Only the CLI can reach here with `ssh:`.

Fail closed until the RPC learns to route through the SSH providers.
@coderabbitai

coderabbitai Bot commented Jul 26, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The runtime adds an SSH-host validation helper before project repository setup and cloning. SSH configurations now reject with an error directing setup through the Orca desktop app. CLI guidance documents this behavior. A runtime test covers both setup flows and verifies that no git operation occurs and no repository is recorded.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is on-topic, but it does not follow the required template and omits the Summary, Screenshots, Testing, AI Review Report, Security Audit, and Notes sections. Rewrite it using the repository template and add the missing sections, including screenshots/no visual change, testing checklist, review report, security audit, and notes.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly states the main change: refusing SSH hosts during project setup instead of handling them locally.
✨ 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.

The test asserted `gitSpawn` was never called and no repo was registered, but
neither assertion could fail. `/home/brennan` is unwritable on macOS, so the
pre-guard clone died at `mkdir` before reaching `gitSpawn`, and
`/home/brennan/orca` failed `isGitRepo` before reaching `addRepo` — the exact
side effects under test were unreachable either way. `rejects.toThrow` also
aborted the test before those lines ran.

Use a real temp destination and a real temp git repo, await both calls via
`.catch`, and assert the side effects before the wording. With the guard
disabled the test now fails on `gitSpawn` being called once with a real
`git clone`, and on a repo registered stamped `executionHostId: 'ssh:openclaw'`
— the silent local-clone-recorded-as-remote defect itself. `gitSpawn` is
stubbed so a regression records the call instead of hitting the network.

Also document the SSH restriction on `project setup-existing-folder`, which the
guard now rejects. `setup-clone` already carried that note; its sibling did not.

@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.

🧹 Nitpick comments (1)
src/main/runtime/orca-runtime.test.ts (1)

7163-7166: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Multi-line "Why" comments exceed the one-line guideline.

These explanatory comments span 2-3 lines each. As per coding guidelines, **/*.{ts,tsx,js,jsx}: "Comments must be concise, limited to non-obvious information, and preferably one line; do not explain obvious code or walk through implementation details." Consider condensing to a single line each.

Also applies to: 7170-7177, 7206-7208

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9031dd35-ae53-4ab1-9f98-f15ef704c632

📥 Commits

Reviewing files that changed from the base of the PR and between 75cf807 and 0c788bf.

📒 Files selected for processing (2)
  • src/cli/specs/project.ts
  • src/main/runtime/orca-runtime.test.ts

@brennanb2025
brennanb2025 merged commit 3baffb4 into main Jul 27, 2026
6 checks passed
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…ally (stablyai#10799)

* fix(runtime): refuse SSH hosts in project setup instead of acting locally

projectHostSetup.clone and .setupExistingFolder threaded executionHostId all
the way down but never used it for routing: cloneRepo runs a local mkdir plus
a local gitSpawn, and addRepo probes the path with existsSync/statSync. An
`ssh:` host therefore cloned and validated on the *local* machine and then
registered the result as living on the SSH host.

It only failed loudly here because the remote path did not exist locally. With
a plausible destination the clone succeeds and writes a setup record pointing
at the wrong machine.

Nothing legitimate sends `ssh:` to these RPCs: the renderer maps every ssh
host (including ephemeral-VM `ssh:runtime-ssh-*`) to the desktop IPC path,
which dispatches to addRemoteRepoFromPath/cloneRemoteRepo, and the IPC handler
symmetrically rejects `runtime:`. Only the CLI can reach here with `ssh:`.

Fail closed until the RPC learns to route through the SSH providers.

* test(runtime): make the SSH guard test observe the corruption it names

The test asserted `gitSpawn` was never called and no repo was registered, but
neither assertion could fail. `/home/brennan` is unwritable on macOS, so the
pre-guard clone died at `mkdir` before reaching `gitSpawn`, and
`/home/brennan/orca` failed `isGitRepo` before reaching `addRepo` — the exact
side effects under test were unreachable either way. `rejects.toThrow` also
aborted the test before those lines ran.

Use a real temp destination and a real temp git repo, await both calls via
`.catch`, and assert the side effects before the wording. With the guard
disabled the test now fails on `gitSpawn` being called once with a real
`git clone`, and on a repo registered stamped `executionHostId: 'ssh:openclaw'`
— the silent local-clone-recorded-as-remote defect itself. `gitSpawn` is
stubbed so a regression records the call instead of hitting the network.

Also document the SSH restriction on `project setup-existing-folder`, which the
guard now rejects. `setup-clone` already carried that note; its sibling did not.
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.

1 participant