From 7a9270dea36690c1dbebd1f2d6d46935870ecfe6 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:28:33 -0700 Subject: [PATCH] fix(terminal): guard the paste pane key and document the evidence policy Readiness review follow-ups, none behaviour-changing for reachable inputs. makePaneKey throws on a malformed leaf/tab id. It is unreachable today (pane.leafId is a minted UUID and the same pair is already called unguarded from a hotter site), but the failure mode was bad: the throw escapes before the paste helper's catch is attached, so the paste would be a silent no-op with no error surface. Degrade to the pre-fix path instead, with a test. Also record two things a future reader needs: the process-confirmed branch is dead for remote-runtime and SSH panes because foreground tracking is disabled there, so a remote pane's status row is its only evidence; and why this resolver deliberately omits the shellForeground/routingRevoked/routingTrusted gates its two siblings enforce - they route input bytes, this only wraps a paste whose payload is ESC-sanitized downstream. Refs STA-4294 --- .../terminal-agent-paste-bracketing.test.ts | 10 +++++++++ .../terminal-agent-paste-bracketing.ts | 21 ++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/components/terminal-pane/terminal-agent-paste-bracketing.test.ts b/src/renderer/src/components/terminal-pane/terminal-agent-paste-bracketing.test.ts index 003ab987f7d..e30764b788e 100644 --- a/src/renderer/src/components/terminal-pane/terminal-agent-paste-bracketing.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-agent-paste-bracketing.test.ts @@ -110,6 +110,16 @@ describe('shouldForceBracketedMultilinePasteForPane', () => { ).toBe(true) }) + it('degrades to the pre-fix path instead of throwing on a malformed pane key', () => { + // Why: makePaneKey throws on a non-UUID leaf or a tabId containing ':'. The throw + // would escape before the paste helper's catch is attached, making the paste a + // silent no-op with no error surface. + expect(() => decide({ leafId: 'not-a-uuid' })).not.toThrow() + expect(decide({ leafId: 'not-a-uuid' })).toBe(false) + expect(() => decide({ tabId: 'tab:with:colons' })).not.toThrow() + expect(decide({ tabId: 'tab:with:colons' })).toBe(false) + }) + it('still brackets when shellForeground is latched true while an agent owns the pane', () => { // Why: shellForeground is republished only at OSC 133 boundaries, so a shell with // no 133 integration leaves it true while an agent runs. Measured live: vetoing on diff --git a/src/renderer/src/components/terminal-pane/terminal-agent-paste-bracketing.ts b/src/renderer/src/components/terminal-pane/terminal-agent-paste-bracketing.ts index 4a617efc4d5..13b6c69231f 100644 --- a/src/renderer/src/components/terminal-pane/terminal-agent-paste-bracketing.ts +++ b/src/renderer/src/components/terminal-pane/terminal-agent-paste-bracketing.ts @@ -21,6 +21,13 @@ import type { PaneForegroundAgentEntry } from '../../store/slices/pane-foregroun * in transit (ConPTY, replay) — never an opt-out. Keep this narrow for that reason. * Bracketing a program that never negotiated is measurably worse than useless: the * markers arrive as literal payload bytes and ICRNL still turns the CR into a submit. + * + * Why this diverges from terminal-ctrl-enter / terminal-windows-shift-enter, which veto + * on shellForeground/routingRevoked and require routingTrusted: those resolvers decide + * where to ROUTE input bytes, so a forged identity misdelivers keystrokes. This one only + * decides whether to WRAP a paste, and the payload is ESC-sanitized downstream, so the + * worst a forged identity buys is a literal ESC[200~ printed at a program. Do not + * "align" this with those gates — measured live, that reinstates the submit bug. */ export function shouldForceBracketedMultilinePasteForPane({ isWindowsClient, @@ -38,8 +45,20 @@ export function shouldForceBracketedMultilinePasteForPane({ if (isWindowsClient) { return true } - const paneKey = makePaneKey(tabId, leafId) + let paneKey: string + try { + paneKey = makePaneKey(tabId, leafId) + } catch { + // Why: unreachable today (pane.leafId is a minted UUID), but a legacy/malformed + // layout must degrade to the pre-fix path, not throw out of the paste handler -- + // the throw would escape before pasteTerminalClipboard's catch is attached and + // the paste would be a silent no-op with no error surface. + return false + } // Why: a process-table confirmed agent is the strongest evidence there is. + // Note this branch is dead for remote-runtime and SSH panes: isForegroundTrackingAllowed + // (pty-connection) returns false for them, so paneForegroundAgentByPaneKey stays empty + // and the status row below is the only evidence a remote pane ever has. if (isTuiAgent(paneForegroundAgentByPaneKey[paneKey]?.agent)) { return true }