From 75cf807f5addec16a699050bbe129ca3e97b4072 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Sun, 26 Jul 2026 15:58:37 -0700 Subject: [PATCH] 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. --- src/main/runtime/orca-runtime.test.ts | 39 +++++++++++++++++++++++++++ src/main/runtime/orca-runtime.ts | 16 +++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index 6d815452211..c1cfd6f47a6 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -7159,6 +7159,45 @@ describe('OrcaRuntimeService', () => { } }) + it('refuses SSH hosts instead of setting the project up on the local machine', async () => { + const spawnSpy = vi.spyOn(gitRunner, 'gitSpawn') + const repos: Record[] = [] + const runtimeStore = { + ...store, + getRepos: () => [...repos] as never, + addRepo: (repo: Record) => { + repos.push(repo) + } + } + const runtime = new OrcaRuntimeService(runtimeStore as never) + + try { + await expect( + runtime.setupProjectClone({ + projectId: 'github:stablyai/orca', + hostId: 'ssh:openclaw', + url: 'https://example.com/orca.git', + destination: '/home/brennan' + }) + ).rejects.toThrow(/SSH hosts are not supported/) + + await expect( + runtime.setupProjectExistingFolder({ + projectId: 'github:stablyai/orca', + hostId: 'ssh:openclaw', + path: '/home/brennan/orca', + kind: 'git' + }) + ).rejects.toThrow(/SSH hosts are not supported/) + + // Why: the defect was a silent local clone/probe recorded as remote, not a bad message. + expect(spawnSpy).not.toHaveBeenCalled() + expect(repos).toHaveLength(0) + } finally { + spawnSpy.mockRestore() + } + }) + it('adopts public clone repos into host-qualified project setup', async () => { const destination = await mkdtemp(join(tmpdir(), 'orca-runtime-project-clone-')) const clonePath = join(destination, 'orca') diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 6e66cda4e60..ce66d582c79 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -1801,6 +1801,19 @@ function runtimeRepoMatchesExecutionHost( return repo.connectionId == null } +// Why: this runtime only has local git and local fs, so an ssh: host here would clone and +// probe the wrong machine and then register the result as remote. SSH setup is owned by the +// desktop IPC path (addRemoteRepoFromPath / cloneRemoteRepo), which the renderer routes to; +// only `local` and `runtime:` legitimately reach these RPCs. +function assertProjectHostSetupHostIsSupported(hostId: ExecutionHostId | null | undefined): void { + if (parseExecutionHostId(hostId)?.kind !== 'ssh') { + return + } + throw new Error( + 'SSH hosts are not supported by this operation. Set the project up from the Orca desktop app, which owns the SSH connection.' + ) +} + function getRuntimeFolderWorkspaceInstanceId(repo: Repo, instanceId: string): string { return `${getRuntimeFolderWorkspaceRootId(repo)}${FOLDER_WORKSPACE_INSTANCE_SEPARATOR}${instanceId}` } @@ -14929,6 +14942,7 @@ export class OrcaRuntimeService { if (!this.store) { throw new Error('runtime_unavailable') } + assertProjectHostSetupHostIsSupported(args.hostId) let repo = await this.addRepo(args.path, args.kind === 'folder' ? 'folder' : 'git', args.hostId) let setup = getProjectHostSetupForRepo(this.listProjectHostSetups(), repo) if (setup.projectId !== args.projectId) { @@ -14971,6 +14985,8 @@ export class OrcaRuntimeService { } async setupProjectClone(args: ProjectHostSetupCloneArgs): Promise { + // Why: guard before cloneRepo, which would otherwise clone to the local disk. + assertProjectHostSetupHostIsSupported(args.hostId) const repo = await this.cloneRepo(args.url, args.destination, args.hostId) return await this.setupProjectExistingFolder({ projectId: args.projectId,