mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
fix(cli): implicit worktree selection is scoped to the caller's host (#27108)
* fix(cli): implicit worktree selection is scoped to the caller's host A CLI run from a shell on SSH host D matched its cwd against every host's worktrees, so a same-path twin on host A could win. Candidates are now limited to the caller's execution host (the relay's SSH stamp, else this machine), and a pane's forwarded ORCA_WORKTREE_ID is scoped the same way. * fix(cli): a local caller keeps runtime-stamped checkouts this runtime holds --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import { resolve as resolvePath } from 'node:path'
|
||||
import { isPathInsideOrEqual } from '../shared/cross-platform-path'
|
||||
import {
|
||||
LOCAL_EXECUTION_HOST_ID,
|
||||
parseExecutionHostId,
|
||||
toSshExecutionHostId,
|
||||
type ExecutionHostId
|
||||
} from '../shared/execution-host'
|
||||
import { readOrchestrationCompatibilityEvidence } from '../shared/orchestration-compatibility-evidence'
|
||||
import type { RuntimeWorktreeListResult, RuntimeWorktreeRecord } from '../shared/runtime-types'
|
||||
import type { RuntimeClient } from './runtime-client'
|
||||
|
||||
type CatalogRow = Pick<RuntimeWorktreeRecord, 'id' | 'path' | 'hostId'> & {
|
||||
identity?: { key: string }
|
||||
}
|
||||
|
||||
/** The execution host this CLI process runs on: the SSH target its relay stamped, else this machine. */
|
||||
export function getCallerExecutionHostId(
|
||||
env: Readonly<Record<string, string | undefined>> = process.env
|
||||
): ExecutionHostId {
|
||||
const host = readOrchestrationCompatibilityEvidence(env)?.host
|
||||
return host?.kind === 'ssh' ? toSshExecutionHostId(host.targetId) : LOCAL_EXECUTION_HOST_ID
|
||||
}
|
||||
|
||||
// Why: the catalog lists SSH hosts' worktrees too and two hosts can hold the same path, so an SSH
|
||||
// caller may only take its own host's rows and a local caller none of an SSH host's. `runtime:*`
|
||||
// rows in this runtime's catalog are checkouts it holds itself, and unstamped rows predate stamping.
|
||||
function isOnCallerHost(row: CatalogRow, callerHostId: ExecutionHostId): boolean {
|
||||
const rowHostId = row.hostId ?? LOCAL_EXECUTION_HOST_ID
|
||||
return parseExecutionHostId(callerHostId)?.kind === 'ssh'
|
||||
? rowHostId === callerHostId
|
||||
: parseExecutionHostId(rowHostId)?.kind !== 'ssh'
|
||||
}
|
||||
|
||||
// Why: `id:` keeps repo inference and the runtime's scoped fast path; only a bare id that another
|
||||
// host's row shares needs the exact identity, or the runtime would refuse it as ambiguous.
|
||||
function toHostScopedSelector(row: CatalogRow, catalog: readonly CatalogRow[]): string {
|
||||
const sharedId = catalog.some((other) => other !== row && other.id === row.id)
|
||||
return sharedId && row.identity?.key ? `identity:${row.identity.key}` : `id:${row.id}`
|
||||
}
|
||||
|
||||
/** The caller host's innermost worktree enclosing `cwd`, as a selector; undefined when none does. */
|
||||
export function selectEnclosingWorktreeOnHost(
|
||||
catalog: readonly CatalogRow[],
|
||||
cwd: string,
|
||||
hostId: ExecutionHostId
|
||||
): string | undefined {
|
||||
const currentPath = resolvePath(cwd)
|
||||
let enclosing: CatalogRow | undefined
|
||||
let enclosingPathLength = -1
|
||||
for (const row of catalog) {
|
||||
if (!isOnCallerHost(row, hostId)) {
|
||||
continue
|
||||
}
|
||||
const rowPath = resolvePath(row.path)
|
||||
if (!isPathInsideOrEqual(rowPath, currentPath) || rowPath.length <= enclosingPathLength) {
|
||||
continue
|
||||
}
|
||||
enclosing = row
|
||||
enclosingPathLength = rowPath.length
|
||||
}
|
||||
return enclosing ? toHostScopedSelector(enclosing, catalog) : undefined
|
||||
}
|
||||
|
||||
/** Scopes a pane's forwarded bare worktree id to the caller's host; undefined when it has none. */
|
||||
export function selectWorktreeIdOnHost(
|
||||
catalog: readonly CatalogRow[],
|
||||
worktreeId: string,
|
||||
hostId: ExecutionHostId
|
||||
): string | undefined {
|
||||
const match = catalog.find((row) => row.id === worktreeId && isOnCallerHost(row, hostId))
|
||||
return match ? toHostScopedSelector(match, catalog) : undefined
|
||||
}
|
||||
|
||||
/** Scopes a pane's forwarded bare `ORCA_WORKTREE_ID` to the caller's host before it is used. */
|
||||
export async function resolveCallerPaneWorktreeSelector(
|
||||
worktreeId: string,
|
||||
client: RuntimeClient
|
||||
): Promise<string | undefined> {
|
||||
const callerHostId = getCallerExecutionHostId()
|
||||
if (callerHostId === LOCAL_EXECUTION_HOST_ID) {
|
||||
return worktreeId
|
||||
}
|
||||
const worktrees = await client.call<RuntimeWorktreeListResult>('worktree.list', {
|
||||
limit: 10_000
|
||||
})
|
||||
return selectWorktreeIdOnHost(worktrees.result.worktrees, worktreeId, callerHostId)
|
||||
}
|
||||
+28
-32
@@ -1,11 +1,6 @@
|
||||
import { resolve as resolvePath } from 'node:path'
|
||||
import type {
|
||||
ComputerAppQuery,
|
||||
RuntimeWorktreeListResult,
|
||||
RuntimeWorktreeRecord
|
||||
} from '../shared/runtime-types'
|
||||
import type { ComputerAppQuery, RuntimeWorktreeListResult } from '../shared/runtime-types'
|
||||
import {
|
||||
isPathInsideOrEqual,
|
||||
isWslUncPathForCallerLinuxPath,
|
||||
isWslUncPathForLinuxMountedPath
|
||||
} from '../shared/cross-platform-path'
|
||||
@@ -13,6 +8,11 @@ import { parseWslUncPath } from '../shared/wsl-paths'
|
||||
import type { RuntimeClient } from './runtime-client'
|
||||
import { RuntimeClientError } from './runtime/types'
|
||||
import { getOptionalStringFlag, getRequiredStringFlag } from './flags'
|
||||
import {
|
||||
getCallerExecutionHostId,
|
||||
resolveCallerPaneWorktreeSelector,
|
||||
selectEnclosingWorktreeOnHost
|
||||
} from './caller-host-worktree-selection'
|
||||
|
||||
export type BrowserCliTarget = {
|
||||
worktree?: string
|
||||
@@ -107,36 +107,25 @@ export async function resolveCurrentWorktreeSelector(
|
||||
): Promise<string> {
|
||||
assertLocalCwdWorktreeSelector('current', client)
|
||||
|
||||
const currentPath = resolvePath(cwd)
|
||||
const worktrees = await client.call<RuntimeWorktreeListResult>('worktree.list', {
|
||||
limit: 10_000
|
||||
})
|
||||
let enclosingWorktree: RuntimeWorktreeRecord | undefined
|
||||
let enclosingPathLength = -1
|
||||
for (const worktree of worktrees.result.worktrees) {
|
||||
const worktreePath = resolvePath(worktree.path)
|
||||
if (
|
||||
!isPathInsideOrEqual(worktreePath, currentPath) ||
|
||||
worktreePath.length <= enclosingPathLength
|
||||
) {
|
||||
continue
|
||||
}
|
||||
enclosingWorktree = worktree
|
||||
enclosingPathLength = worktreePath.length
|
||||
}
|
||||
|
||||
if (!enclosingWorktree) {
|
||||
throw new RuntimeClientError(
|
||||
'selector_not_found',
|
||||
`No Orca-managed worktree contains the current directory: ${currentPath}`
|
||||
)
|
||||
}
|
||||
|
||||
// Why: users expect "active/current" to mean the enclosing managed worktree
|
||||
// even from nested subdirectories. Resolve to the concrete runtime id here:
|
||||
// even from nested subdirectories. Resolve to the concrete runtime row here:
|
||||
// duplicate repo registrations can expose the same Git worktree path, and a
|
||||
// path selector would throw selector_ambiguous after losing the repo id.
|
||||
return `id:${enclosingWorktree.id}`
|
||||
const selector = selectEnclosingWorktreeOnHost(
|
||||
worktrees.result.worktrees,
|
||||
cwd,
|
||||
getCallerExecutionHostId()
|
||||
)
|
||||
if (!selector) {
|
||||
throw new RuntimeClientError(
|
||||
'selector_not_found',
|
||||
`No Orca-managed worktree contains the current directory: ${resolvePath(cwd)}`
|
||||
)
|
||||
}
|
||||
return selector
|
||||
}
|
||||
|
||||
export async function getOptionalWorktreeSelector(
|
||||
@@ -301,7 +290,10 @@ export async function getEmulatorWorktreeSelector(
|
||||
}
|
||||
const terminalWorktreeId = process.env.ORCA_WORKTREE_ID
|
||||
if (terminalWorktreeId?.trim()) {
|
||||
return terminalWorktreeId
|
||||
const scoped = await resolveCallerPaneWorktreeSelector(terminalWorktreeId, client)
|
||||
if (scoped) {
|
||||
return scoped
|
||||
}
|
||||
}
|
||||
const folderWorkspaceId = process.env.ORCA_WORKSPACE_ID?.trim()
|
||||
if (folderWorkspaceId?.startsWith('folder:')) {
|
||||
@@ -323,7 +315,11 @@ export async function getEmulatorCommandTarget(
|
||||
const emulator = getOptionalStringFlag(flags, 'emulator')
|
||||
const worktree = await getEmulatorWorktreeSelector(flags, cwd, client)
|
||||
if (device || emulator) {
|
||||
return { device: device || undefined, emulator: emulator || undefined, worktree }
|
||||
return {
|
||||
device: device || undefined,
|
||||
emulator: emulator || undefined,
|
||||
worktree
|
||||
}
|
||||
}
|
||||
return { worktree }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* #21210: several SSH hosts hold a worktree of one repo at the same path. A CLI on host D must
|
||||
* resolve `current` (and a pane's forwarded worktree id) to host D's row, never another host's twin.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
getCallerExecutionHostId,
|
||||
selectEnclosingWorktreeOnHost,
|
||||
selectWorktreeIdOnHost
|
||||
} from './caller-host-worktree-selection'
|
||||
|
||||
const PATH = '/srv/app'
|
||||
|
||||
function row(hostId: 'local' | `ssh:${string}` | undefined, id: string, identityKey?: string) {
|
||||
return {
|
||||
id,
|
||||
path: PATH,
|
||||
...(hostId ? { hostId } : {}),
|
||||
...(identityKey ? { identity: { key: identityKey } } : {})
|
||||
}
|
||||
}
|
||||
|
||||
const SSH_CALLER_D = {
|
||||
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_KIND: 'ssh',
|
||||
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_ID: 'host d',
|
||||
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_INCARNATION: 'inc-1',
|
||||
ORCA_ORCHESTRATION_COMPATIBILITY_ATTACHMENT: 'att-1'
|
||||
}
|
||||
|
||||
const TWINS = [
|
||||
row('ssh:host-a', `repo-a::${PATH}`),
|
||||
row('ssh:host%20d', `repo-d::${PATH}`),
|
||||
row('local', `repo-l::${PATH}`)
|
||||
]
|
||||
|
||||
describe('implicit worktree selection is scoped to the caller host (#21210)', () => {
|
||||
it('reads the caller host from the relay stamp, encoding the SSH target', () => {
|
||||
expect(getCallerExecutionHostId(SSH_CALLER_D)).toBe('ssh:host%20d')
|
||||
expect(getCallerExecutionHostId({})).toBe('local')
|
||||
// A WSL stamp is still this machine.
|
||||
expect(
|
||||
getCallerExecutionHostId({
|
||||
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_KIND: 'wsl',
|
||||
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_ID: 'local',
|
||||
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_INCARNATION: 'Ubuntu'
|
||||
})
|
||||
).toBe('local')
|
||||
})
|
||||
|
||||
it("resolves an SSH caller's cwd to its own host's twin", () => {
|
||||
const hostId = getCallerExecutionHostId(SSH_CALLER_D)
|
||||
|
||||
expect(selectEnclosingWorktreeOnHost(TWINS, `${PATH}/src`, hostId)).toBe(`id:repo-d::${PATH}`)
|
||||
})
|
||||
|
||||
it('resolves a caller with no host stamp to the local row only', () => {
|
||||
expect(selectEnclosingWorktreeOnHost(TWINS, PATH, 'local')).toBe(`id:repo-l::${PATH}`)
|
||||
})
|
||||
|
||||
// A runtime-stamped row in this runtime's own catalog is a checkout it holds locally.
|
||||
it('keeps a runtime-stamped local checkout for a local caller', () => {
|
||||
const rows = [
|
||||
TWINS[0]!,
|
||||
{ id: `repo-r::${PATH}`, path: PATH, hostId: 'runtime:env-1' as const }
|
||||
]
|
||||
|
||||
expect(selectEnclosingWorktreeOnHost(rows, `${PATH}/src`, 'local')).toBe(`id:repo-r::${PATH}`)
|
||||
expect(selectEnclosingWorktreeOnHost(rows, PATH, 'ssh:host%20d')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('never adopts another host or an unstamped row for an SSH caller', () => {
|
||||
expect(
|
||||
selectEnclosingWorktreeOnHost([TWINS[0]!, row(undefined, 'repo-u::x')], PATH, 'ssh:host-z')
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('names the exact identity when another host shares the bare id', () => {
|
||||
const twins = [
|
||||
row('ssh:host-a', `repo-1::${PATH}`, 'wt2:a'),
|
||||
row('ssh:host%20d', `repo-1::${PATH}`, 'wt2:d')
|
||||
]
|
||||
|
||||
expect(selectEnclosingWorktreeOnHost(twins, PATH, 'ssh:host%20d')).toBe('identity:wt2:d')
|
||||
expect(selectWorktreeIdOnHost(twins, `repo-1::${PATH}`, 'ssh:host%20d')).toBe('identity:wt2:d')
|
||||
expect(selectWorktreeIdOnHost(twins, `repo-1::${PATH}`, 'ssh:host-z')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user