diff --git a/src/main/ipc/pty.test.ts b/src/main/ipc/pty.test.ts index a8c27bce5d4..cb843c5c105 100644 --- a/src/main/ipc/pty.test.ts +++ b/src/main/ipc/pty.test.ts @@ -5296,7 +5296,10 @@ describe('registerPtyHandlers', () => { paneKey?: string tabId?: string }) => ({ - id: 'ssh-pty' + id: 'ssh-pty', + // The host attests the shell's identity at spawn; the lease has to carry it or the + // reconnect fence has nothing to compare and attaches by pty id alone. + incarnationId: 'inc-host-ssh-pty' }) ) const store = { @@ -5383,6 +5386,9 @@ describe('registerPtyHandlers', () => { worktreeId: 'wt-1', tabId: 'tab-1', leafId, + // Without this the field exists, loads and is read — and is never written, so the + // reconnect fence silently permits everything while every other clause stays green. + incarnationId: 'inc-host-ssh-pty', state: 'attached' }) ) @@ -5393,7 +5399,8 @@ describe('registerPtyHandlers', () => { worktreeId: 'wt-1', tabId: 'tab-1', leafId, - ptyId: 'ssh-pty' + ptyId: 'ssh-pty', + incarnationId: 'inc-host-ssh-pty' }) expect(store.persistPtyBinding.mock.calls.at(-1)).toHaveLength(1) diff --git a/src/main/ipc/pty.ts b/src/main/ipc/pty.ts index 63348251d5b..e69f88cf89d 100644 --- a/src/main/ipc/pty.ts +++ b/src/main/ipc/pty.ts @@ -88,6 +88,7 @@ import { isSshPtyIdentityMismatchError, isSshPtyNotFoundError } from '../providers/ssh-pty-errors' +import { isRelayAttestedPtyIncarnationId } from '../../shared/pty-incarnation' import { parseAppSshPtyId, toAppSshPtyId, toRelaySshPtyId } from '../providers/ssh-pty-id' import { createPtySpawnTiming } from './pty-spawn-timing' import { @@ -5273,6 +5274,12 @@ export function registerPtyHandlers( ...(typeof args.leafId === 'string' && isTerminalLeafId(args.leafId) ? { leafId: args.leafId } : {}), + // The shell this lease names, as the host just attested it. Without it the record says + // only "pty-N", and a replaced relay reissues that from 1 — so reconnect would have + // nothing to compare and would attach whatever now answers to the id. + ...(isRelayAttestedPtyIncarnationId(result.incarnationId) + ? { incarnationId: result.incarnationId } + : {}), state: 'attached', lastAttachedAt: Date.now() }) @@ -6791,6 +6798,11 @@ export function registerPtyHandlers( ...(typeof args.worktreeId === 'string' ? { worktreeId: args.worktreeId } : {}), ...(typeof args.tabId === 'string' ? { tabId: args.tabId } : {}), ...(validatedLeafId ? { leafId: validatedLeafId } : {}), + // See the sibling writer: a lease that names only "pty-N" cannot survive a relay that + // reissues ids from 1. + ...(isRelayAttestedPtyIncarnationId(result.incarnationId) + ? { incarnationId: result.incarnationId } + : {}), state: 'attached', lastAttachedAt: Date.now() })