Gate project host setup runtime capability

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo-H
2026-06-11 12:33:59 -07:00
co-authored by Orca
parent a5bbad37d3
commit 4e9bc9b6c6
14 changed files with 829 additions and 19 deletions
@@ -406,8 +406,10 @@ Needs:
- structured errors for unsupported old-server actions
- old client / new server behavior that degrades safely
Current branch status: runtime compatibility exists; project/setup capability
gating needs to be layered on.
Current branch status: runtime compatibility exists. Runtimes now advertise
`project-host-setup.v1`; renderer reads fall back to repo-derived projection
when the capability is missing, and remote setup/clone mutations are blocked
before contacting older servers. Broader UI/CLI version-skew validation remains.
### 10. Caches And Local State
@@ -489,6 +491,8 @@ Already partially landed:
- inline `Run on` clone setup for not-yet-set-up local, runtime, and SSH hosts
- repo-backed setup method metadata so imported/cloned setup methods survive
compatibility projection and persistence sync
- project-host setup runtime capability advertisement, read fallback, and
remote mutation gating for older runtime servers
Not complete yet:
@@ -499,7 +503,7 @@ Not complete yet:
- project settings split into global and host-specific ownership
- host settings/capability UI aligned with project setup
- complete cache/request ownership audit
- complete version-skew capability gating
- broader UI/CLI version-skew validation beyond the runtime capability guard
- full Electron and SSH end-to-end validation
## Recommended Implementation Shape
@@ -0,0 +1,548 @@
# Project-First Host Model Conversation
## Purpose
This document captures the VM / SSH / remote-server discussion so far and turns
it into a concrete change inventory.
The product question was:
```text
Should Orca organize work by machine first, or by project first?
```
The current answer is:
```text
Project -> ProjectHostSetup -> Workspace
```
Hosts still matter. They are where code runs. But they should usually be
represented as places where a project is available, not as isolated silos that
own the user's projects.
## TL;DR
The durable model should be project-first.
```text
Project
Host setup
Workspace
```
There are **12 major change surfaces** needed to fully fit this model.
If we count smaller implementation tasks inside those surfaces, it is closer to
**35-45 concrete changes**, but the clean planning answer is 12 major areas.
Some of the work is already partially implemented on this branch. The model is
not complete until data model, persistence, creation, setup, settings, sidebar,
runtime routing, cache ownership, CLI/API, version skew, SSH behavior, and
Electron validation all agree on the same project-first language.
## Current Model
The current multi-host direction makes hosts highly visible:
```text
Local Mac
Orca
feature-a
openclaw 2
Orca
fix-ssh-agent-status
```
That is useful for operational awareness. It helps answer:
- which hosts exist
- which hosts are online or disconnected
- where workspaces are running
- which SSH/remote machine might be causing a problem
The weakness is that it makes the machine feel like the outermost product
concept. Users can end up feeling like they are switching into separate machine
silos, even when they are really working on the same project.
## Desired Model
Projects/repos remain the outermost durable concept:
```text
Orca
Local Mac
feature-a
openclaw 2
fix-ssh-agent-status
```
In plain English:
```text
I am working on Project A.
Where should this workspace run?
```
not:
```text
I am inside Machine B.
Which copy of Project A is here?
```
## Core Concepts
### Project
A `Project` is the durable repo/project identity the user recognizes.
Examples:
- Orca
- a Linux-only CUDA repo
- a work repo available only on a company host
- a personal repo available both locally and on an SSH server
### Host
A `Host` is a place where code can run.
Examples:
- local Mac
- SSH target
- remote server
- user-managed VM
- remote Orca runtime
- future Orca-provisioned cloud VM
For now, "VM" is a loose product term. A VM can be modeled as a host with extra
capabilities, provisioning metadata, and eventually billing metadata.
### ProjectHostSetup
A `ProjectHostSetup` means:
```text
this project is available on this host at this path with this setup state
```
Host-local facts belong here:
- checkout path
- worktree base path
- setup status
- clone/import/provision method
- setup scripts
- host-specific project settings
- platform or capability constraints
### Workspace
A `Workspace` is a branch/task/worktree running from one project setup on one
host.
It should eventually know:
- `projectId`
- `hostId`
- `projectHostSetupId`
- host-local worktree path
## Important Product Decisions
### Host-First Is Still Useful
Host-first grouping should not disappear completely. It is a useful operational
view/filter for troubleshooting, host health, and remote machine management.
The important distinction is that host-first should be a view mode or filter,
not the only durable organization model.
### Focused Host Mode Is A Filter
Focused host mode is not a separate durable model.
Selecting "Local Mac" or another host as a filter can satisfy the same use case.
If there is only one host and no meaningful host choice, the host UI should be
reduced or hidden.
### Configured SSH Target Is Not Project Availability
A configured SSH target should not automatically appear under every project.
Recommended sidebar behavior:
- show a disconnected host when it has relevant project setup history or
workspaces
- do not show a never-used SSH target in a project sidebar just because it is
configured
- keep SSH connection management in host/settings surfaces
- use clear disconnected labels/actions instead of relying only on a gray dot
### A Remote-Only Project Is Normal
Some projects only exist on one machine. That is fine.
Examples:
- a Linux-only project
- a GPU-heavy project
- a work project only available on a work machine
- a repo checked out only on an SSH server
The UI should not imply that every project can or should run everywhere.
## Workspace Creation Direction
Creating a workspace should eventually ask:
1. Which project?
2. Which host should run it?
3. What branch/task/workspace name?
If the project is not available on the selected host, Orca should offer:
- clone project to that host
- import an existing folder on that host
- select a different host
The user should not need to understand old `repoId` compatibility records.
## Project Setup Direction
"Add project" and "make this project available on another host" are related but
different actions.
Important flows:
- import a local folder as a new project
- import an SSH folder as a new project
- set up an existing project on another host
- clone an existing project onto a selected host
- when adding a new host, optionally initialize one or more projects there
- later, provision an Orca cloud VM and materialize selected projects there
## Settings Direction
Settings need explicit ownership.
| Setting type | Owner | Examples |
| --- | --- | --- |
| Client setting | desktop client | theme, local UI preferences |
| Host setting | machine/runtime | SSH connection, display name, health, server version |
| Project setting | durable project | project name, provider identity |
| Project-host setup setting | project on one host | checkout path, worktree base path, setup script |
A host dropdown or host table inside project settings is probably sufficient for
host-specific project settings, similar to existing Windows/WSL-specific
settings patterns.
## Version Skew Direction
Remote servers may not match the desktop client version.
Needed behavior:
- new client + old server should degrade explicitly
- project/setup reads can fall back to compatibility projections where possible
- setup/create actions should be blocked before mutation if the server lacks
the required capability
- old client + new server should keep working through compatibility APIs where
possible
The user-facing reasons should be concrete:
- host is offline
- server version does not support project-host setup
- project is not set up on this host
- selected host does not support required platform/capability
- required runtime/agent is unavailable
## Reference Comparison
### Superset
Superset is the closer reference for Orca's desired data model.
Its conceptual model is effectively:
```text
Project + Host -> Workspace
```
Specific lessons for Orca:
- project identity is durable
- a host is where the project can be materialized
- workspace creation targets both project and host
- a project can be set up on multiple hosts
- if a project is not available on a host, the UI can block and offer setup
- project settings can contain host-specific path/worktree settings
This maps well to Orca because Orca already has durable repos/projects,
worktrees, agents, terminals, source control, and host-aware runtimes.
### Cmux
Cmux is more session/workspace-first.
Its conceptual model is closer to:
```text
Workspace/session -> local or remote execution context
```
Specific lessons for Orca:
- SSH should feel first class
- remote terminals, file views, browser panes, and localhost routing should
follow the remote execution context
- reconnect and persistence behavior matter
- remote execution should not feel bolted on
Cmux is useful for SSH/session polish. It is less useful as the core durable
data model reference because it does not appear to center "this project is
available on these hosts" as the main object.
## What Needs To Change
There are **12 major change surfaces**.
### 1. Shared Data Model
Current `Repo` mixes durable project identity with host-local checkout details.
The new model needs first-class project/setup concepts.
Needed:
- `Project`
- `Host`
- `ProjectHostSetup`
- explicit workspace ownership by `projectId`, `hostId`, and
`projectHostSetupId`
- compatibility projection from old repo-shaped records
### 2. Persistence And Migration
Existing users need a boring migration.
Needed:
- derive one project per reliable durable identity
- derive one setup per existing repo checkout
- avoid merging same-name folders unless provider/setup identity is reliable
- preserve old ids or aliases where compatibility requires it
- backfill existing workspaces with project/setup ownership when safe
### 3. Runtime And Request Ownership
The UI can be project-first, but execution still happens on a host.
Needed:
- route terminals, agents, filesystem, browser, source control, hooks, and
automations through the workspace's owning host
- avoid using the currently focused host as a hidden global default for
workspace-owned operations
- scope cancellation and stale-response handling to the host/setup that owns the
request
### 4. Workspace Creation
Creation must target a project and host, not only a repo id.
Needed:
- project picker
- run-on host picker
- unavailable-host reasons
- inline clone/import setup actions
- compatibility resolver from `{ projectId, hostId }` to current repo/setup
internals while old APIs remain
### 5. Project Setup Flow
"Add repo" becomes a family of project/setup flows.
Needed:
- import existing folder on local host
- import existing folder over SSH
- clone project onto selected host
- set up an existing project on another host
- bulk setup when adding a new host
- future cloud provisioning hook
### 6. Sidebar Row Model
The sidebar should be built from projects, hosts, setups, and workspaces rather
than repo-only grouping.
Needed:
- project-first grouping
- host labels/subgroups only when useful
- host filters and online/offline status retained
- clear disconnected-host behavior
- drag/reorder rules for projects, host sections, and workspaces
### 7. Project Settings
Project settings need a global area plus host-specific setup sections.
Needed:
- project-global settings
- host-specific paths, worktree paths, setup scripts, and platform constraints
- host dropdown/table inside settings
- provider-neutral source-control settings
### 8. Host Settings
Host settings should describe the machine/runtime, not duplicate every project
setting.
Needed:
- connection details
- display name
- health/status
- server version and protocol compatibility
- platform/capabilities
- host-wide defaults and overrides
### 9. Version And Capability Compatibility
New clients and old servers will coexist.
Needed:
- host capability probing
- fallback projection when project/setup APIs are missing
- disabled states with specific reasons
- structured errors for unsupported old-server actions
- old client / new server behavior that degrades safely
### 10. Caches And Local State
Some caches are project-global; many are host/setup-local.
Needed:
- classify caches as project, host, setup, or workspace scoped
- include host/setup ids in cache keys for refs, git status, filesystem state,
capabilities, terminals, browser sessions, and remote results
- prevent a response from one host from overwriting another host's state for the
same project
### 11. CLI And API
External commands should speak project-first language.
Needed:
- `orca project list`
- `orca project setups`
- `orca project setup-existing-folder --project <id> --host <id> --path <path>`
- `orca worktree create --project <id> --host <id> ...`
- `orca worktree create --project-host-setup <id> ...`
- compatibility aliases for old repo/worktree commands
- structured availability errors
### 12. Tests And Verification
This crosses storage, routing, UI, SSH, and compatibility.
Needed:
- migration tests
- selector/sidebar grouping tests
- create-workspace tests
- setup-on-host tests
- settings ownership tests
- SSH end-to-end validation
- version mismatch tests
- Electron validation for sidebar, creation, and settings
## Current Branch Status
Already partially landed:
- shared `Project` and `ProjectHostSetup` types
- compatibility projection from existing `Repo[]`
- persisted compatibility fields for projects and project-host setups
- renderer hydration and fallback for older runtimes
- project-aware sidebar grouping in existing repo grouping paths
- default all-host Projects sidebar keeps projects outermost while preserving
host-section operational views for explicit host filters
- first-pass host context badges for mixed-host project groups
- project-host-aware workspace creation target resolver
- optional workspace metadata for `projectId`, `hostId`, and
`projectHostSetupId`
- discovery-time backfill for missing workspace ownership fields
- setup-existing-folder API plumbing through local IPC/preload/runtime paths
- project settings existing-folder setup form for known local, SSH, and active
runtime hosts
- CLI commands for listing projects/setups and creating worktrees by
project/setup
- composer `Run on` selector when a project has multiple ready setups
- setup-target `Run on` rows for known hosts where the selected project is not
set up yet
- inline `Run on` import-existing-folder setup
- inline `Run on` clone setup for not-yet-set-up local, runtime, and SSH hosts
- repo-backed setup method metadata so imported/cloned setup methods survive
compatibility projection and persistence sync
- project-host setup runtime capability advertisement, read fallback, and
remote mutation gating for older runtime servers
Not complete yet:
- independent project-host setup persistence beyond repo-backed compatibility
records
- bulk setup flows and setup for hosts that are not already known to the client
- SSH streamed clone progress parity
- full project settings split into global and host-specific ownership
- host settings/capability UI aligned with project setup
- complete cache/request ownership audit
- broader UI/CLI version-skew validation beyond the runtime capability guard
- full Electron and SSH end-to-end validation
## Change Count
Short answer:
```text
12 major change surfaces
```
More practical engineering answer:
```text
About 35-45 concrete implementation tasks
```
The reason for the difference is that each major surface has several necessary
subtasks. For example, "workspace creation" includes resolver changes, UI
changes, setup actions, unavailable-host reasons, runtime routing, persistence,
tests, and version-skew handling.
The largest remaining areas are:
1. independent `ProjectHostSetup` persistence
2. project-first creation/setup UI completion
3. project and host settings ownership split
4. cache/request ownership audit
5. broader version-skew validation
6. SSH parity and validation
7. full Electron validation of sidebar, creation, and settings
## Final Position
Keep hosts visible, but do not make them the durable outermost object.
The long-term model should be project-first because it matches how users think
about repo/worktree/task work. Hosts, SSH machines, VMs, remote servers, and
future Orca cloud compute become first-class places where a project can run.
+7 -3
View File
@@ -445,8 +445,10 @@ Needs:
- structured errors for unsupported old-server actions
- old client / new server behavior that degrades safely
Current branch status: runtime compatibility exists; project/setup capability
gating needs to be layered on.
Current branch status: runtime compatibility exists. Runtimes now advertise
`project-host-setup.v1`; renderer reads fall back to repo-derived projection
when the capability is missing, and remote setup/clone mutations are blocked
before contacting older servers. Broader UI/CLI version-skew validation remains.
### 10. Caches And Local State
@@ -528,6 +530,8 @@ Already partially landed:
- inline `Run on` clone setup for not-yet-set-up local, runtime, and SSH hosts
- repo-backed setup method metadata so imported and cloned setups survive
compatibility projection and persistence sync
- project-host setup runtime capability advertisement, read fallback, and
remote mutation gating for older runtime servers
Not complete yet:
@@ -539,7 +543,7 @@ Not complete yet:
- full project settings split into global and host-specific ownership
- host settings/capability UI aligned with project setup
- complete cache/request ownership audit
- complete version-skew capability gating
- broader UI/CLI version-skew validation beyond the runtime capability guard
- full Electron and SSH end-to-end validation
## Recommended Implementation Shape
+7 -2
View File
@@ -315,7 +315,10 @@ Needs:
- structured errors for unsupported old-server actions
- old client / new server behavior that degrades safely
Status: runtime compatibility exists; project/setup capability gating remains.
Status: runtime compatibility exists. Runtimes advertise
`project-host-setup.v1`; renderer reads fall back to repo-derived projection
when the capability is missing, and remote setup/clone mutations are blocked
before contacting older servers. Broader UI/CLI version-skew validation remains.
### 10. Caches And Local State
@@ -410,6 +413,8 @@ Already partially landed:
- inline clone setup from the composer for not-yet-set-up local, runtime, and
SSH hosts
- repo-backed setup method metadata for imported/cloned project-host setups
- project-host setup runtime capability advertisement, read fallback, and
remote mutation gating for older runtime servers
Still needed:
@@ -420,7 +425,7 @@ Still needed:
- project settings split into global and host-specific ownership
- host settings/capability UI aligned with project setup
- complete cache/request ownership audit
- complete version-skew capability gating
- broader UI/CLI version-skew validation beyond the runtime capability guard
- full Electron and SSH end-to-end validation
## Final Product Position
+14 -8
View File
@@ -762,15 +762,20 @@ Landed so far:
- Added repo-backed setup method metadata so imported and cloned setup flows
survive persistence/projection sync as `imported-existing-folder` or `cloned`
while `Repo` remains the compatibility source of truth.
- Added a runtime capability contract for project-host setup. New runtimes
advertise `project-host-setup.v1`; new clients fall back to repo-derived
project/setup projection for reads when that capability is missing and block
remote setup/clone mutations before contacting an older server.
- Added tests for local repos, SSH repos, same-provider multi-host grouping,
no-identity same-name non-grouping, selector cache behavior, persistence
backfill, repo mutation synchronization, renderer hydration, runtime RPC
routing, setup method persistence, local/runtime/SSH clone setup composition,
remote clone IPC, and GitHub clone URL inference. Sidebar row-builder tests
now cover project-first multi-host grouping and same-name repo separation
without project identity. Workspace target tests cover local-only fallback,
focused-host setup selection, explicit project-plus-host resolution, same-name
non-merging, and unavailable setup reasons.
routing, project-host setup capability gating, setup method persistence,
local/runtime/SSH clone setup composition, remote clone IPC, and GitHub clone
URL inference. Sidebar row-builder tests now cover project-first multi-host
grouping and same-name repo separation without project identity. Workspace
target tests cover local-only fallback, focused-host setup selection, explicit
project-plus-host resolution, same-name non-merging, and unavailable setup
reasons.
Important limitation:
@@ -805,8 +810,9 @@ Remaining end-to-end work:
handling for host/setup-local ownership
- add project-first CLI/API commands with compatibility aliases for existing
repo/worktree commands
- validate migration, creation, settings, sidebar, SSH, and version-skew flows
end to end
- broaden version-skew UI/CLI coverage beyond the current runtime capability
guard, and validate migration, creation, settings, sidebar, SSH, and
version-skew flows end to end
## Recommendation
+1
View File
@@ -897,6 +897,7 @@ describe('OrcaRuntimeService', () => {
expect(status.capabilities).toContain('terminal.binary-stream.v1')
expect(status.capabilities).toContain('workspace-ports.v1')
expect(status.capabilities).toContain('mobile.tasks.v1')
expect(status.capabilities).toContain('project-host-setup.v1')
expect(typeof status.protocolVersion).toBe('number')
expect(typeof status.minCompatibleMobileVersion).toBe('number')
expect(status.protocolVersion).toBeGreaterThanOrEqual(1)
@@ -2,6 +2,7 @@ import type { RuntimeRpcResponse } from '../../../shared/runtime-rpc-envelope'
import type { RuntimeStatus } from '../../../shared/runtime-types'
import {
MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION,
RUNTIME_CAPABILITIES,
RUNTIME_PROTOCOL_VERSION
} from '../../../shared/protocol-version'
@@ -23,7 +24,8 @@ export function createCompatibleRuntimeStatusResponse(
liveTabCount: 0,
liveLeafCount: 0,
runtimeProtocolVersion: RUNTIME_PROTOCOL_VERSION,
minCompatibleRuntimeClientVersion: MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION
minCompatibleRuntimeClientVersion: MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION,
capabilities: [...RUNTIME_CAPABILITIES]
},
_meta: { runtimeId }
}
@@ -1,6 +1,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
callRuntimeRpc,
assertRuntimeEnvironmentCapability,
clearRuntimeCompatibilityCacheForTests,
getActiveRuntimeTarget,
RuntimeRpcCallError,
@@ -135,6 +136,52 @@ describe('runtime RPC client routing', () => {
])
})
it('checks advertised runtime capabilities after protocol compatibility', async () => {
runtimeEnvironmentCall.mockResolvedValue({
id: 'status',
ok: true,
result: {
runtimeId: 'remote-runtime',
graphStatus: 'ready',
runtimeProtocolVersion: RUNTIME_PROTOCOL_VERSION,
minCompatibleRuntimeClientVersion: MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION,
capabilities: ['project-host-setup.v1']
},
_meta: { runtimeId: 'remote-runtime' }
})
await expect(
assertRuntimeEnvironmentCapability(
'env-1',
'project-host-setup.v1',
'Project setup is unavailable.'
)
).resolves.toBeUndefined()
})
it('rejects missing advertised runtime capabilities with the caller message', async () => {
runtimeEnvironmentCall.mockResolvedValue({
id: 'status',
ok: true,
result: {
runtimeId: 'remote-runtime',
graphStatus: 'ready',
runtimeProtocolVersion: RUNTIME_PROTOCOL_VERSION,
minCompatibleRuntimeClientVersion: MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION,
capabilities: []
},
_meta: { runtimeId: 'remote-runtime' }
})
await expect(
assertRuntimeEnvironmentCapability(
'env-1',
'project-host-setup.v1',
'Project setup is unavailable.'
)
).rejects.toThrow('Project setup is unavailable.')
})
it('marks remote UI-owned runtime calls so feature interaction tracking can ignore them', async () => {
runtimeEnvironmentCall.mockImplementation(({ method }: { method: string }) => {
const result =
@@ -1,6 +1,7 @@
import type { GlobalSettings } from '../../../shared/types'
import type { RuntimeRpcFailure, RuntimeRpcResponse } from '../../../shared/runtime-rpc-envelope'
import type { RuntimeStatus } from '../../../shared/runtime-types'
import type { RuntimeCapability } from '../../../shared/protocol-version'
import { withBrowserPaneUiRuntimeRpcSource } from '../../../shared/runtime-rpc-feature-interaction-source'
import { assertRuntimeStatusCompatible } from './runtime-protocol-compat'
@@ -141,6 +142,35 @@ export function markRuntimeEnvironmentCompatible(environmentId: string): void {
rememberRuntimeEnvironmentCompatibility(trimmed, Promise.resolve())
}
export async function getRuntimeEnvironmentStatus(
environmentId: string,
timeoutMs?: number
): Promise<RuntimeStatus> {
const response = await window.api.runtimeEnvironments.call({
selector: environmentId,
method: 'status.get',
timeoutMs
})
const status = unwrapRuntimeRpcResult<RuntimeStatus>(
response as RuntimeRpcResponse<RuntimeStatus>
)
assertRuntimeStatusCompatible(status)
markRuntimeEnvironmentCompatible(environmentId)
return status
}
export async function assertRuntimeEnvironmentCapability(
environmentId: string,
capability: RuntimeCapability,
message: string,
timeoutMs?: number
): Promise<void> {
const status = await getRuntimeEnvironmentStatus(environmentId, timeoutMs)
if (!status.capabilities?.includes(capability)) {
throw new Error(message)
}
}
export function clearRuntimeCompatibilityCacheForTests(): void {
clearRuntimeCompatibilityCache()
}
@@ -0,0 +1,119 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { Repo } from '../../../../shared/types'
import { clearRuntimeCompatibilityCacheForTests } from '../../runtime/runtime-rpc-client'
import type { RuntimeEnvironmentCallRequest } from '../../runtime/runtime-compatibility-test-fixture'
import { createTestStore } from './store-test-helpers'
const remoteRepo: Repo = {
id: 'remote-repo',
path: '/remote',
displayName: 'Remote',
badgeColor: '#111',
addedAt: 2
}
const reposList = vi.fn()
const reposClone = vi.fn()
const reposCloneRemote = vi.fn()
const runtimeEnvironmentCall = vi.fn()
const runtimeEnvironmentTransportCall = vi.fn()
function runtimeStatusWithoutProjectHostSetup() {
return {
id: 'status',
ok: true,
result: {
runtimeId: 'runtime-remote',
rendererGraphEpoch: 0,
graphStatus: 'ready',
authoritativeWindowId: null,
liveTabCount: 0,
liveLeafCount: 0,
runtimeProtocolVersion: 3,
minCompatibleRuntimeClientVersion: 2,
capabilities: []
},
_meta: { runtimeId: 'runtime-remote' }
}
}
beforeEach(() => {
clearRuntimeCompatibilityCacheForTests()
reposList.mockReset()
reposClone.mockReset()
reposCloneRemote.mockReset()
runtimeEnvironmentCall.mockReset()
runtimeEnvironmentTransportCall.mockReset()
runtimeEnvironmentTransportCall.mockImplementation((args: RuntimeEnvironmentCallRequest) => {
if (args.method === 'status.get') {
return runtimeStatusWithoutProjectHostSetup()
}
return runtimeEnvironmentCall(args)
})
vi.stubGlobal('window', {
api: {
repos: {
list: reposList,
clone: reposClone,
cloneRemote: reposCloneRemote
},
runtimeEnvironments: { call: runtimeEnvironmentTransportCall }
}
})
})
describe('repo slice project-host setup runtime capability', () => {
it('falls back to repo-derived project setup state when a remote runtime lacks support', async () => {
runtimeEnvironmentCall.mockResolvedValue({
id: 'rpc-1',
ok: true,
result: { repos: [remoteRepo] },
_meta: { runtimeId: 'runtime-remote' }
})
const store = createTestStore()
store.setState({ settings: { activeRuntimeEnvironmentId: 'env-1' } as never })
await store.getState().fetchRepos()
expect(store.getState().projectHostSetups).toEqual([
expect.objectContaining({ id: 'remote-repo', hostId: 'runtime:env-1' })
])
expect(runtimeEnvironmentCall).toHaveBeenCalledTimes(1)
expect(runtimeEnvironmentCall).toHaveBeenCalledWith({
selector: 'env-1',
method: 'repo.list',
params: undefined,
timeoutMs: 15_000
})
})
it('blocks runtime project setup when the server does not advertise support', async () => {
const store = createTestStore()
await expect(
store.getState().setupProjectExistingFolder({
projectId: 'project-1',
hostId: 'runtime:env-1',
path: '/srv/project',
kind: 'git'
})
).resolves.toBeNull()
expect(runtimeEnvironmentCall).not.toHaveBeenCalled()
})
it('blocks runtime project clone before mutating unsupported servers', async () => {
const store = createTestStore()
await expect(
store.getState().setupProjectClone({
projectId: 'project-1',
hostId: 'runtime:env-1',
url: 'https://github.com/stablyai/orca.git',
destination: '/srv'
})
).resolves.toBeNull()
expect(runtimeEnvironmentCall).not.toHaveBeenCalled()
})
})
+25 -1
View File
@@ -20,6 +20,7 @@ import {
projectHostSetupProjectionFromRepos,
type ProjectHostSetupProjection
} from '../../../../shared/project-host-setup-projection'
import { PROJECT_HOST_SETUP_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version'
import { isGitRepoKind } from '../../../../shared/repo-kind'
import { sanitizeRepoIcon } from '../../../../shared/repo-icon'
import { normalizeRepoBadgeColor } from '../../../../shared/repo-badge-color'
@@ -27,7 +28,11 @@ import { getProjectGroupSubtreeIds } from '../../../../shared/project-groups'
import { getRepoIdFromWorktreeId } from './worktree-helpers'
import { reconcileFetchedRepos } from './repo-identity-reconcile'
import { splitRepoReorderByHost } from './repo-reorder-host-split'
import { callRuntimeRpc, getActiveRuntimeTarget } from '../../runtime/runtime-rpc-client'
import {
assertRuntimeEnvironmentCapability,
callRuntimeRpc,
getActiveRuntimeTarget
} from '../../runtime/runtime-rpc-client'
import { toRuntimeWorktreeSelector } from '../../runtime/runtime-worktree-selector'
import { buildDismissedOnboardingFolderAgentStartup } from '@/lib/onboarding-folder-agent-startup'
import { markOnboardingProjectAdded } from '@/lib/onboarding-project-checklist'
@@ -184,6 +189,7 @@ async function fetchProjectHostSetupCompatibility(
setups: await projectsApi.listHostSetups()
}
}
await assertProjectHostSetupRuntimeCapability(target)
const [projectResponse, setupResponse] = await Promise.all([
callRuntimeRpc<{ projects: Project[] }>(target, 'project.list', undefined, {
timeoutMs: 15_000
@@ -203,6 +209,20 @@ async function fetchProjectHostSetupCompatibility(
}
}
async function assertProjectHostSetupRuntimeCapability(
target: ReturnType<typeof getActiveRuntimeTarget>
): Promise<void> {
if (target.kind !== 'environment') {
return
}
await assertRuntimeEnvironmentCapability(
target.environmentId,
PROJECT_HOST_SETUP_RUNTIME_CAPABILITY,
'The selected Orca server does not support project host setup yet. Update Orca on the server and try again.',
15_000
)
}
function projectCompatibilityFromRepos(
repos: readonly Repo[]
): Pick<RepoSlice, 'projects' | 'projectHostSetups'> {
@@ -672,6 +692,7 @@ export const createRepoSlice: StateCreator<AppState, [], [], RepoSlice> = (set,
setupProjectExistingFolder: async (args) => {
try {
const target = getProjectSetupRuntimeTarget(args.hostId)
await assertProjectHostSetupRuntimeCapability(target)
const result =
target.kind === 'local'
? await window.api.projects.setupExistingFolder(args)
@@ -720,6 +741,9 @@ export const createRepoSlice: StateCreator<AppState, [], [], RepoSlice> = (set,
try {
const parsedHost = parseExecutionHostId(args.hostId)
const target = getProjectSetupRuntimeTarget(args.hostId)
if (parsedHost?.kind !== 'ssh') {
await assertProjectHostSetupRuntimeCapability(target)
}
const repo =
parsedHost?.kind === 'ssh'
? await window.api.repos.cloneRemote({
@@ -1056,6 +1056,18 @@ describe('web file preload API', () => {
).rejects.toThrow('Remote file download is unavailable in paired web clients.')
})
it('rejects SSH clone requests in paired web clients', async () => {
const { api } = await installApi('Linux')
await expect(
api.repos.cloneRemote({
connectionId: 'ssh-1',
url: 'https://github.com/stablyai/orca.git',
destination: '/workspace'
})
).rejects.toThrow('SSH clone is unavailable in paired web clients.')
})
it('returns false for runtime missing-path errors from fs.pathExists', async () => {
const runtimeCalls: { method: string; params: unknown }[] = []
const worktree = {
+5
View File
@@ -997,6 +997,11 @@ function createReposApi(): NonNullable<Partial<PreloadApi>['repos']> {
await callRuntimeResult<{ repo: Repo }>('repo.clone', { url, destination }, 10 * 60_000)
).repo
},
cloneRemote: async () => {
// Why: SSH relay cloning is owned by the desktop main process; paired web
// clients must not pretend they can run that local IPC path directly.
throw new Error('SSH clone is unavailable in paired web clients.')
},
cloneAbort: () => Promise.resolve(),
addRemote: async ({ remotePath, displayName, kind }) => {
invalidateRuntimeWorktreeCaches()
+4 -1
View File
@@ -21,6 +21,8 @@ export const RUNTIME_PROTOCOL_VERSION = 3
export const MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION = 2
export const MIN_COMPATIBLE_RUNTIME_SERVER_VERSION = 3
export const PROJECT_HOST_SETUP_RUNTIME_CAPABILITY = 'project-host-setup.v1' as const
export const RUNTIME_CAPABILITIES = [
'runtime.status.compat.v1',
'runtime.environments.v1',
@@ -28,7 +30,8 @@ export const RUNTIME_CAPABILITIES = [
'terminal.binary-stream.v1',
'terminal.multiplex.v1',
'workspace-ports.v1',
'mobile.tasks.v1'
'mobile.tasks.v1',
PROJECT_HOST_SETUP_RUNTIME_CAPABILITY
] as const
export type RuntimeCapability = (typeof RUNTIME_CAPABILITIES)[number] | (string & {})