From 272bb1a8e6ea3da27ecc7046a1141c2de0933d50 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 3 Aug 2026 20:52:17 -0700 Subject: [PATCH] fix(skills): trust SSH PTY pane mirrors --- .../pane-skill-discovery-workspace.test.ts | 76 ++++++++++++++++++- .../skills/pane-skill-discovery-workspace.ts | 19 +++-- 2 files changed, 87 insertions(+), 8 deletions(-) diff --git a/src/main/skills/pane-skill-discovery-workspace.test.ts b/src/main/skills/pane-skill-discovery-workspace.test.ts index f27f754b79c..a4153c0d8e7 100644 --- a/src/main/skills/pane-skill-discovery-workspace.test.ts +++ b/src/main/skills/pane-skill-discovery-workspace.test.ts @@ -38,10 +38,14 @@ function worktreeMeta(hostId: WorktreeMeta['hostId']): WorktreeMeta { } } -function session(worktreeId: string, startupCwd?: string): WorkspaceSessionState { +function session( + worktreeId: string, + startupCwd?: string, + ptyId: string | null = null +): WorkspaceSessionState { return { tabsByWorktree: { - [worktreeId]: [{ id: 'tab-1', startupCwd }] + [worktreeId]: [{ id: 'tab-1', ptyId, startupCwd }] } } as WorkspaceSessionState } @@ -83,6 +87,74 @@ describe('resolvePaneSkillDiscoveryWorkspace', () => { ).toEqual({ connectionId: 'target-1', cwd: '/work/demo-project' }) }) + it('accepts a legacy local SSH PTY mirror that omits its startup directory', () => { + const worktreeId = 'repo-1::/work/demo-project' + const ptyId = 'ssh:target-1@@pty-1' + + expect( + resolvePaneSkillDiscoveryWorkspace({ + worktreeId, + terminalTabId: 'tab-1', + repos: [repo({ path: '/work/demo-project' })], + projectGroups: [], + folderWorkspaces: [], + worktreeMeta: worktreeMeta('ssh:target-1'), + sessions: [ + { hostId: 'local', session: session(worktreeId, undefined, ptyId) }, + { + hostId: 'ssh:target-1', + session: session(worktreeId, '/work/demo-project', ptyId) + } + ] + }) + ).toEqual({ connectionId: 'target-1', cwd: '/work/demo-project' }) + }) + + it('rejects missing startup-directory mirrors with different PTY identities', () => { + const worktreeId = 'repo-1::/work/demo-project' + + expect(() => + resolvePaneSkillDiscoveryWorkspace({ + worktreeId, + terminalTabId: 'tab-1', + repos: [repo({ path: '/work/demo-project' })], + projectGroups: [], + folderWorkspaces: [], + worktreeMeta: worktreeMeta('ssh:target-1'), + sessions: [ + { hostId: 'local', session: session(worktreeId, undefined, 'ssh:target-1@@pty-1') }, + { + hostId: 'ssh:target-1', + session: session(worktreeId, '/work/demo-project', 'ssh:target-1@@pty-2') + } + ] + }) + ).toThrow('pane_skill_discovery_owner_ambiguous') + }) + + it('rejects a matching PTY identity from a different SSH target', () => { + const worktreeId = 'repo-1::/work/demo-project' + const ptyId = 'ssh:target-2@@pty-1' + + expect(() => + resolvePaneSkillDiscoveryWorkspace({ + worktreeId, + terminalTabId: 'tab-1', + repos: [repo({ path: '/work/demo-project' })], + projectGroups: [], + folderWorkspaces: [], + worktreeMeta: worktreeMeta('ssh:target-1'), + sessions: [ + { hostId: 'local', session: session(worktreeId, undefined, ptyId) }, + { + hostId: 'ssh:target-1', + session: session(worktreeId, '/work/demo-project', ptyId) + } + ] + }) + ).toThrow('pane_skill_discovery_owner_ambiguous') + }) + it('accepts normalized Windows variants of a mirrored SSH startup directory', () => { const worktreeId = 'repo-1::C:\\Repo' diff --git a/src/main/skills/pane-skill-discovery-workspace.ts b/src/main/skills/pane-skill-discovery-workspace.ts index 9e5d33be27a..f836972ff9d 100644 --- a/src/main/skills/pane-skill-discovery-workspace.ts +++ b/src/main/skills/pane-skill-discovery-workspace.ts @@ -5,6 +5,7 @@ import { toSshExecutionHostId, type ExecutionHostId } from '../../shared/execution-host' +import { parseAppSshPtyId } from '../../shared/ssh-pty-id' import type { FolderWorkspace, ProjectGroup, @@ -174,7 +175,7 @@ function findPaneSession( worktreeId: string, terminalTabId: string | undefined, authoritativeHostId?: ExecutionHostId | null -): { hostId: ExecutionHostId; tab: { startupCwd?: string } } | null { +): { hostId: ExecutionHostId; tab: { ptyId: string | null; startupCwd?: string } } | null { if (!terminalTabId) { return null } @@ -183,7 +184,7 @@ function findPaneSession( worktreeIdsEqual(candidateWorktreeId, worktreeId) ? tabs .filter((tab) => tab.id === terminalTabId) - .map((tab) => ({ hostId, tab: { startupCwd: tab.startupCwd } })) + .map((tab) => ({ hostId, tab: { ptyId: tab.ptyId, startupCwd: tab.startupCwd } })) : [] ) ) @@ -200,7 +201,7 @@ function findPaneSession( (match) => match === authoritativeMatch || (match.hostId === LOCAL_EXECUTION_HOST_ID && - paneSessionsMirror(match.tab, authoritativeMatch.tab)) + paneSessionsMirror(match.tab, authoritativeMatch.tab, authoritativeMatch.hostId)) ) ) { return authoritativeMatch @@ -211,13 +212,19 @@ function findPaneSession( } function paneSessionsMirror( - left: { startupCwd?: string }, - right: { startupCwd?: string } + left: { ptyId: string | null; startupCwd?: string }, + right: { ptyId: string | null; startupCwd?: string }, + authoritativeHostId: ExecutionHostId ): boolean { const leftCwd = left.startupCwd?.trim() || null const rightCwd = right.startupCwd?.trim() || null if (!leftCwd || !rightCwd) { - return leftCwd === rightCwd + if (leftCwd === rightCwd) { + return true + } + const host = parseExecutionHostId(authoritativeHostId) + const pty = left.ptyId && left.ptyId === right.ptyId ? parseAppSshPtyId(left.ptyId) : null + return host?.kind === 'ssh' && pty?.connectionId === host.targetId } return normalizeRuntimePathForComparison(leftCwd) === normalizeRuntimePathForComparison(rightCwd) }