mirror of
https://github.com/stablyai/orca.git
synced 2026-10-29 08:07:18 +00:00
fix(skills): reject ambiguous SSH runtime routes
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
import type { AppState } from '../../store/types'
|
||||
import type { PaneSkillDiscoveryTarget, SkillDiscoveryTarget } from '../../../../shared/skills'
|
||||
import { isRuntimeOwnedSshTargetId, parseExecutionHostId } from '../../../../shared/execution-host'
|
||||
import {
|
||||
isRuntimeOwnedSshTargetId,
|
||||
parseExecutionHostId,
|
||||
toSshExecutionHostId,
|
||||
type ExecutionHostId
|
||||
} from '../../../../shared/execution-host'
|
||||
import { getRepoIdFromWorktreeId } from '../../../../shared/worktree-id'
|
||||
import type { RuntimeClientTarget } from '@/runtime/runtime-rpc-client'
|
||||
import {
|
||||
getExplicitRuntimeEnvironmentIdForWorktree,
|
||||
@@ -71,6 +77,46 @@ export function selectNativeChatSkillStateInputs(state: AppState): NativeChatSki
|
||||
}
|
||||
}
|
||||
|
||||
function hasRuntimeSshTransportEvidence(
|
||||
state: NativeChatSkillStateInputs,
|
||||
worktreeId: string,
|
||||
hostId: ExecutionHostId
|
||||
): boolean {
|
||||
const scope = parseWorkspaceKey(worktreeId)
|
||||
if (scope?.type === 'folder') {
|
||||
return false
|
||||
}
|
||||
const rawWorktreeId = scope?.type === 'worktree' ? scope.worktreeId : worktreeId
|
||||
const repoIds = new Set([getRepoIdFromWorktreeId(rawWorktreeId)])
|
||||
const catalogs = [
|
||||
...Object.values(state.worktreesByRepo ?? {}),
|
||||
...Object.values(state.detectedWorktreesByRepo ?? {}).map((result) => result.worktrees)
|
||||
]
|
||||
for (const worktrees of catalogs) {
|
||||
for (const worktree of worktrees) {
|
||||
if (worktree.id !== rawWorktreeId || parseExecutionHostId(worktree.hostId)?.id !== hostId) {
|
||||
continue
|
||||
}
|
||||
repoIds.add(worktree.repoId)
|
||||
if (worktree.runtimeOwnerEnvironmentId?.trim()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const repo of state.repos) {
|
||||
const connectionId = repo.connectionId?.trim()
|
||||
if (
|
||||
repoIds.has(repo.id) &&
|
||||
parseExecutionHostId(repo.executionHostId)?.kind === 'runtime' &&
|
||||
connectionId &&
|
||||
toSshExecutionHostId(connectionId) === hostId
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export function resolveNativeChatSkillDiscoveryCwd(
|
||||
state: NativeChatSkillWorktreeState,
|
||||
terminalTabId: string
|
||||
@@ -106,9 +152,13 @@ export function resolveNativeChatSkillDiscoveryContext(
|
||||
const parsedHost = parseExecutionHostId(hostId)
|
||||
if (parsedHost?.kind === 'ssh') {
|
||||
const runtimeEnvironmentId = getExplicitRuntimeEnvironmentIdForWorktree(state, worktreeId)
|
||||
// Why: a runtime-owned SSH target without an explicit owner stamp is
|
||||
// ambiguous; failing closed beats resolving it against local providers.
|
||||
if (isRuntimeOwnedSshTargetId(parsedHost.targetId) && !runtimeEnvironmentId) {
|
||||
// Why: unresolved paired-runtime transport can collide with a local target
|
||||
// id; failing closed prevents the identity-only RPC reaching the wrong host.
|
||||
if (
|
||||
!runtimeEnvironmentId &&
|
||||
(isRuntimeOwnedSshTargetId(parsedHost.targetId) ||
|
||||
hasRuntimeSshTransportEvidence(state, worktreeId, parsedHost.id))
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const connectionState = runtimeEnvironmentId
|
||||
|
||||
@@ -310,6 +310,45 @@ describe('resolveNativeChatSkillDiscoveryContext for SSH panes', () => {
|
||||
expect(context.sshDisconnected).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps a non-ephemeral paired SSH target on its owning runtime', () => {
|
||||
const context = resolveNativeChatSkillDiscoveryContext(
|
||||
selectedSshInputs({
|
||||
repos: [],
|
||||
sshConnectionStates: new Map([
|
||||
['private-target', connectionState({ targetId: 'private-target' })]
|
||||
]),
|
||||
sshStateByEnvironment: new Map([
|
||||
[
|
||||
'hub-a',
|
||||
{
|
||||
connectionStates: new Map([
|
||||
['private-target', connectionState({ targetId: 'private-target' })]
|
||||
])
|
||||
}
|
||||
]
|
||||
]),
|
||||
worktreesByRepo: {
|
||||
'repo-1': [
|
||||
{
|
||||
id: 'worktree-1',
|
||||
repoId: 'repo-1',
|
||||
path: '/repo/worktree',
|
||||
hostId: 'ssh:private-target',
|
||||
runtimeOwnerEnvironmentId: 'hub-a'
|
||||
}
|
||||
]
|
||||
}
|
||||
}),
|
||||
'tab-1'
|
||||
)
|
||||
|
||||
expect(context?.executionHostKind).toBe('ssh')
|
||||
if (context?.executionHostKind !== 'ssh') {
|
||||
throw new Error('expected ssh context')
|
||||
}
|
||||
expect(context.runtimeTarget).toEqual({ kind: 'environment', environmentId: 'hub-a' })
|
||||
})
|
||||
|
||||
it('treats an unhydrated environment bucket as unknown rather than disconnected', () => {
|
||||
const context = resolveNativeChatSkillDiscoveryContext(
|
||||
sshInputs({
|
||||
@@ -403,6 +442,42 @@ describe('resolveNativeChatSkillDiscoveryContext for SSH panes', () => {
|
||||
expect(context?.key).toContain('ssh:target-2')
|
||||
})
|
||||
|
||||
it('fails closed when two runtimes project a local SSH target id', () => {
|
||||
const worktreeId = 'repo-1::/remote/repo'
|
||||
const context = resolveNativeChatSkillDiscoveryContext(
|
||||
selectedSshInputs({
|
||||
activeWorkspaceExecutionHostId: 'ssh:shared-target',
|
||||
activeWorktreeId: worktreeId,
|
||||
repos: [],
|
||||
sshConnectionStates: new Map([
|
||||
['shared-target', connectionState({ targetId: 'shared-target' })]
|
||||
]),
|
||||
tabsByWorktree: { [worktreeId]: [{ id: 'tab-1' }] },
|
||||
worktreesByRepo: {
|
||||
'repo-1': [
|
||||
{
|
||||
id: worktreeId,
|
||||
repoId: 'repo-1',
|
||||
path: '/remote/repo',
|
||||
hostId: 'ssh:shared-target',
|
||||
runtimeOwnerEnvironmentId: 'hub-a'
|
||||
},
|
||||
{
|
||||
id: worktreeId,
|
||||
repoId: 'repo-1',
|
||||
path: '/remote/repo',
|
||||
hostId: 'ssh:shared-target',
|
||||
runtimeOwnerEnvironmentId: 'hub-b'
|
||||
}
|
||||
]
|
||||
}
|
||||
}),
|
||||
'tab-1'
|
||||
)
|
||||
|
||||
expect(context).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps the same remote path on two SSH hosts cache-isolated', () => {
|
||||
const onTargetTwo = resolveNativeChatSkillDiscoveryContext(
|
||||
sshInputs({
|
||||
|
||||
Reference in New Issue
Block a user