fix(skills): preserve SSH owner hydration

This commit is contained in:
Brennan Benson
2026-08-05 18:50:12 -07:00
parent 17e2ea2d6a
commit d2741dc203
4 changed files with 92 additions and 13 deletions
@@ -2,10 +2,21 @@ import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { parseSkillDiscoveryResult, SKILL_DISCOVERY_LIMITS } from '../../shared/skills'
import {
PaneSkillDiscoveryTargetSchema,
parseSkillDiscoveryResult,
SKILL_DISCOVERY_LIMITS
} from '../../shared/skills'
import { discoverSkills } from './discovery'
describe('skill discovery wire contract', () => {
it('accepts pane identities containing a valid long workspace path', () => {
const worktreeId = `repo-1::/${'nested/'.repeat(100)}repo`
expect(worktreeId.length).toBeGreaterThan(512)
expect(() => PaneSkillDiscoveryTargetSchema.parse({ worktreeId })).not.toThrow()
})
it('bounds scanner metadata before returning it to an SSH client', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-skills-'))
const home = join(root, 'home')
@@ -12,7 +12,9 @@ import { parseWorkspaceKey } from '../../../../shared/workspace-scope'
export type NativeChatSkillStateInputs = Pick<
AppState,
| 'activeRepoId'
| 'activeWorkspaceExecutionHostId'
| 'activeWorktreeId'
| 'detectedWorktreesByRepo'
| 'folderWorkspaces'
| 'projectGroups'
| 'projects'
@@ -53,7 +55,9 @@ export type NativeChatSkillDiscoveryContext =
export function selectNativeChatSkillStateInputs(state: AppState): NativeChatSkillStateInputs {
return {
activeRepoId: state.activeRepoId,
activeWorkspaceExecutionHostId: state.activeWorkspaceExecutionHostId,
activeWorktreeId: state.activeWorktreeId,
detectedWorktreesByRepo: state.detectedWorktreesByRepo,
folderWorkspaces: state.folderWorkspaces,
projectGroups: state.projectGroups,
projects: state.projects,
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'
import type { DiscoveredSkill, SkillDiscoveryResult } from '../../../../shared/skills'
import type { AppState } from '../../store/types'
import {
isNativeChatSkillForAgent,
resolveNativeChatSkillDiscoveryContext,
@@ -7,6 +8,7 @@ import {
} from './use-native-chat-skills'
import {
getNativeChatSkillDiscoverySubscriptionKey,
selectNativeChatSkillStateInputs,
type NativeChatSkillStateInputs
} from './native-chat-skill-discovery-context'
@@ -178,7 +180,9 @@ function connectionState(overrides: Record<string, unknown> = {}): Record<string
function sshInputs(overrides: Record<string, unknown> = {}): NativeChatSkillStateInputs {
return {
activeRepoId: 'repo-1',
activeWorkspaceExecutionHostId: null,
activeWorktreeId: 'worktree-1',
detectedWorktreesByRepo: {},
folderWorkspaces: [],
projectGroups: [],
projects: [],
@@ -197,6 +201,10 @@ function sshInputs(overrides: Record<string, unknown> = {}): NativeChatSkillStat
} as unknown as NativeChatSkillStateInputs
}
function selectedSshInputs(overrides: Record<string, unknown>): NativeChatSkillStateInputs {
return selectNativeChatSkillStateInputs(sshInputs(overrides) as unknown as AppState)
}
describe('resolveNativeChatSkillDiscoveryContext for SSH panes', () => {
it('builds a pane-bound SSH context keyed on the connection generation', () => {
const context = resolveNativeChatSkillDiscoveryContext(sshInputs(), 'tab-1')
@@ -210,7 +218,7 @@ describe('resolveNativeChatSkillDiscoveryContext for SSH panes', () => {
expect(context.key).toContain('7')
const regenerated = resolveNativeChatSkillDiscoveryContext(
sshInputs({
selectedSshInputs({
sshConnectionStates: new Map([['target-1', connectionState({ connectionGeneration: 8 })]])
}),
'tab-1'
@@ -225,7 +233,7 @@ describe('resolveNativeChatSkillDiscoveryContext for SSH panes', () => {
'tab-1'
)
const reconnecting = resolveNativeChatSkillDiscoveryContext(
sshInputs({
selectedSshInputs({
sshConnectionStates: new Map([['target-1', connectionState({ status: 'reconnecting' })]])
}),
'tab-1'
@@ -342,6 +350,59 @@ describe('resolveNativeChatSkillDiscoveryContext for SSH panes', () => {
expect(context.paneTarget).toEqual({ worktreeId, terminalTabId: 'tab-1' })
})
it('uses the active SSH host while duplicate catalogs are still hydrating', () => {
const worktreeId = 'repo-1::/remote/repo'
const context = resolveNativeChatSkillDiscoveryContext(
selectedSshInputs({
activeWorkspaceExecutionHostId: 'ssh:target-2',
activeWorktreeId: worktreeId,
repos: [
{ id: 'repo-1', path: '/remote/repo', connectionId: 'target-1' },
{ id: 'repo-1', path: '/remote/repo', connectionId: 'target-2' }
],
sshConnectionStates: new Map([['target-2', connectionState({ targetId: 'target-2' })]]),
tabsByWorktree: { [worktreeId]: [{ id: 'tab-1' }] },
worktreesByRepo: {}
}),
'tab-1'
)
expect(context?.executionHostKind).toBe('ssh')
expect(context?.key).toContain('ssh:target-2')
})
it('uses a detected-only SSH owner before the primary worktree catalog hydrates', () => {
const worktreeId = 'repo-1::/remote/repo'
const context = resolveNativeChatSkillDiscoveryContext(
selectedSshInputs({
activeWorktreeId: 'other-worktree',
detectedWorktreesByRepo: {
'repo-1': {
worktrees: [
{
id: worktreeId,
repoId: 'repo-1',
path: '/remote/repo',
hostId: 'ssh:target-2'
}
]
}
},
repos: [
{ id: 'repo-1', path: '/remote/repo', connectionId: 'target-1' },
{ id: 'repo-1', path: '/remote/repo', connectionId: 'target-2' }
],
sshConnectionStates: new Map([['target-2', connectionState({ targetId: 'target-2' })]]),
tabsByWorktree: { [worktreeId]: [{ id: 'tab-1' }] },
worktreesByRepo: {}
}),
'tab-1'
)
expect(context?.executionHostKind).toBe('ssh')
expect(context?.key).toContain('ssh:target-2')
})
it('keeps the same remote path on two SSH hosts cache-isolated', () => {
const onTargetTwo = resolveNativeChatSkillDiscoveryContext(
sshInputs({
+13 -10
View File
@@ -109,11 +109,23 @@ export type PaneSkillDiscoveryTarget = {
terminalTabId?: string
}
export const SKILL_DISCOVERY_LIMITS = {
descriptionLength: 8192,
nameLength: 512,
pathLength: 4096,
rootPaths: 64,
skills: 5000,
sources: 1000
} as const
/** Pane discovery carries workspace/pane identity only — never a path or SSH
* connection id. The owning runtime derives the scan directory from its own
* persisted state. */
export const PaneSkillDiscoveryTargetSchema: z.ZodType<PaneSkillDiscoveryTarget> = z.object({
worktreeId: z.string().min(1).max(512),
worktreeId: z
.string()
.min(1)
.max(SKILL_DISCOVERY_LIMITS.pathLength + 512),
terminalTabId: z.string().min(1).max(512).optional()
})
@@ -127,15 +139,6 @@ export type SkillDiscoveryForPaneResponse =
const SKILL_PROVIDER_VALUES = ['codex', 'claude', 'agent-skills'] as const
const SKILL_SOURCE_KIND_VALUES = ['home', 'repo', 'bundled', 'plugin'] as const
export const SKILL_DISCOVERY_LIMITS = {
descriptionLength: 8192,
nameLength: 512,
pathLength: 4096,
rootPaths: 64,
skills: 5000,
sources: 1000
} as const
const boundedString = (max: number): z.ZodString => z.string().max(max)
/** Validates untrusted skill metadata at the SSH relay boundary before it can