Fix browser annotation sends to active Windows agents (#8309)

* fix: authorize guarded sends to active ConPTY agents

* Fix stale-handle rejection using controller-less status checks and contr

- Distinguish `terminal_handle_stale` (PTY rebind mid-check) from `terminal_exited`
  so callers get an accurate reason instead of a generic `terminal_not_writable`.
- Move the PTY binding assertion to always run after the fresh foreground read,
  even when no controller is present, closing a gap where a rebind during a
  controller-less check went undetected.
- Call `confirmForegroundProcess` through its owning controller instance so the
  method keeps its `this` receiver instead of being invoked unbound.
- Update reliability gates and design doc to reflect the new test coverage and
  corrected error semantics.

* rm design doc
This commit is contained in:
Jinjing
2026-07-11 14:32:55 -07:00
committed by GitHub
parent 800967278e
commit 2802f0419d
7 changed files with 652 additions and 24 deletions
+259 -4
View File
@@ -30,10 +30,7 @@ import {
removeWorktree
} from '../git/worktree'
import * as gitRunner from '../git/runner'
import {
clearSubmodulePathsCacheForTests,
listSubmodulePaths
} from '../git/status'
import { clearSubmodulePathsCacheForTests, listSubmodulePaths } from '../git/status'
import {
createSetupRunnerScript,
getEffectiveHooks,
@@ -1179,6 +1176,67 @@ const store = {
getProjects: () => []
}
async function createExplicitAgentStatusHarness(options: {
getForegroundProcess: (ptyId: string) => Promise<string | null>
confirmForegroundProcess?: (ptyId: string) => Promise<string | null>
title?: string
}): Promise<{
runtime: OrcaRuntimeService
handle: string
syncPty: (ptyId: string | null) => void
}> {
const leafId = '11111111-1111-4111-8111-111111111111'
const paneKey = makePaneKey('tab-1', leafId)
const runtime = new OrcaRuntimeService(store, undefined, {
getAgentStatusSnapshot: () => [
{
paneKey,
state: 'working',
prompt: '',
agentType: 'codex',
connectionId: null,
receivedAt: Date.now(),
stateStartedAt: Date.now(),
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID
}
]
})
runtime.setPtyController({
spawn: vi.fn().mockResolvedValue({ id: 'pty-1' }),
write: () => true,
kill: () => true,
getForegroundProcess: options.getForegroundProcess,
confirmForegroundProcess: options.confirmForegroundProcess
})
runtime.attachWindow(1)
const syncPty = (ptyId: string | null): void => {
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
title: options.title ?? 'repo terminal',
activeLeafId: leafId,
layout: null
}
],
leaves: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
leafId,
paneRuntimeId: 1,
ptyId
}
]
})
}
syncPty('pty-1')
const [terminal] = (await runtime.listTerminals()).terminals
return { runtime, handle: terminal.handle, syncPty }
}
function makeHeadlessTerminalLayout(
ptyIdsByLeafId: Record<string, string | undefined>
): TerminalLayoutSnapshot {
@@ -8486,6 +8544,203 @@ describe('OrcaRuntimeService', () => {
})
})
it('uses strong provider confirmation to authorize fresh hook state over a shell foreground', async () => {
const getForegroundProcess = vi.fn(async () => 'powershell.exe')
const confirmForegroundProcess = vi.fn(async () => 'claude')
const { runtime, handle } = await createExplicitAgentStatusHarness({
getForegroundProcess,
confirmForegroundProcess
})
await expect(runtime.getTerminalAgentStatus(handle)).resolves.toEqual({
handle,
isRunningAgent: true,
status: 'working'
})
expect(getForegroundProcess).toHaveBeenCalledOnce()
expect(getForegroundProcess).toHaveBeenCalledWith('pty-1')
expect(confirmForegroundProcess).toHaveBeenCalledOnce()
expect(confirmForegroundProcess).toHaveBeenCalledWith('pty-1')
})
it('calls foreground confirmation with its controller receiver', async () => {
const getForegroundProcess = vi.fn(async () => 'powershell.exe')
const confirmForegroundProcess = vi.fn(
async function (this: { getForegroundProcess: typeof getForegroundProcess }) {
return this.getForegroundProcess === getForegroundProcess ? 'codex' : null
}
)
const { runtime, handle } = await createExplicitAgentStatusHarness({
getForegroundProcess,
confirmForegroundProcess
})
await expect(runtime.getTerminalAgentStatus(handle)).resolves.toMatchObject({
isRunningAgent: true,
status: 'working'
})
expect(confirmForegroundProcess).toHaveBeenCalledOnce()
})
it.each([
['shell', async () => 'pwsh.exe'],
['non-agent', async () => 'vim'],
['unavailable', async () => null],
[
'failure',
async () => {
throw new Error('provider unavailable')
}
]
])('fails closed when shell-conflict confirmation returns %s', async (_case, confirm) => {
const confirmForegroundProcess = vi.fn(confirm)
const { runtime, handle } = await createExplicitAgentStatusHarness({
getForegroundProcess: async () => 'zsh',
confirmForegroundProcess
})
await expect(runtime.getTerminalAgentStatus(handle)).resolves.toEqual({
handle,
isRunningAgent: false,
status: null
})
expect(confirmForegroundProcess).toHaveBeenCalledOnce()
})
it('fails closed on a shell conflict when the controller cannot confirm it', async () => {
const { runtime, handle } = await createExplicitAgentStatusHarness({
getForegroundProcess: async () => 'zsh'
})
await expect(runtime.getTerminalAgentStatus(handle)).resolves.toEqual({
handle,
isRunningAgent: false,
status: null
})
})
it('skips strong confirmation when ordinary foreground evidence recognizes an agent', async () => {
const getForegroundProcess = vi.fn(async () => 'codex')
const confirmForegroundProcess = vi.fn(async () => 'codex')
const { runtime, handle } = await createExplicitAgentStatusHarness({
getForegroundProcess,
confirmForegroundProcess
})
await expect(runtime.getTerminalAgentStatus(handle)).resolves.toMatchObject({
isRunningAgent: true,
status: 'working'
})
expect(getForegroundProcess).toHaveBeenCalledOnce()
expect(confirmForegroundProcess).not.toHaveBeenCalled()
})
it('skips both foreground reads when current title evidence blocks explicit hook state', async () => {
const getForegroundProcess = vi.fn(async () => 'zsh')
const confirmForegroundProcess = vi.fn(async () => 'codex')
const { runtime, handle } = await createExplicitAgentStatusHarness({
getForegroundProcess,
confirmForegroundProcess,
title: 'zsh'
})
await expect(runtime.getTerminalAgentStatus(handle)).resolves.toMatchObject({
isRunningAgent: false,
status: null
})
expect(getForegroundProcess).not.toHaveBeenCalled()
expect(confirmForegroundProcess).not.toHaveBeenCalled()
})
it('skips foreground reads for permission title and blocked wait evidence', async () => {
for (const blocked of ['title', 'wait'] as const) {
const getForegroundProcess = vi.fn(async () => 'zsh')
const confirmForegroundProcess = vi.fn(async () => 'codex')
const { runtime, handle } = await createExplicitAgentStatusHarness({
getForegroundProcess,
confirmForegroundProcess
})
runtime.onPtyData(
'pty-1',
blocked === 'title'
? '\x1b]0;Codex waiting for permission\x07'
: 'Hooks need review. Press enter to confirm\n',
Date.now() + 1000
)
getForegroundProcess.mockClear()
confirmForegroundProcess.mockClear()
await expect(runtime.getTerminalAgentStatus(handle)).resolves.toMatchObject({
isRunningAgent: true,
status: 'permission'
})
expect(getForegroundProcess).not.toHaveBeenCalled()
expect(confirmForegroundProcess).not.toHaveBeenCalled()
}
})
it('rejects foreground evidence when the handle rebinds during the ordinary read', async () => {
const foreground = deferred<string | null>()
const getForegroundProcess = vi.fn(() => foreground.promise)
const confirmForegroundProcess = vi.fn(async () => 'codex')
const { runtime, handle, syncPty } = await createExplicitAgentStatusHarness({
getForegroundProcess,
confirmForegroundProcess
})
const status = runtime.getTerminalAgentStatus(handle)
await vi.waitFor(() => expect(getForegroundProcess).toHaveBeenCalledWith('pty-1'))
syncPty('pty-2')
foreground.resolve('zsh')
await expect(status).rejects.toThrow('terminal_handle_stale')
expect(confirmForegroundProcess).not.toHaveBeenCalled()
})
it('rejects a handle rebind while a controller-less status check yields', async () => {
const { runtime, handle, syncPty } = await createExplicitAgentStatusHarness({
getForegroundProcess: async () => 'zsh'
})
runtime.setPtyController(null)
const status = runtime.getTerminalAgentStatus(handle)
syncPty('pty-2')
await expect(status).rejects.toThrow('terminal_handle_stale')
})
it('rejects confirmation evidence when the handle rebinds during the fresh read', async () => {
const confirmation = deferred<string | null>()
const confirmForegroundProcess = vi.fn(() => confirmation.promise)
const { runtime, handle, syncPty } = await createExplicitAgentStatusHarness({
getForegroundProcess: async () => 'powershell.exe',
confirmForegroundProcess
})
const status = runtime.getTerminalAgentStatus(handle)
await vi.waitFor(() => expect(confirmForegroundProcess).toHaveBeenCalledWith('pty-1'))
syncPty('pty-2')
confirmation.resolve('codex')
await expect(status).rejects.toThrow('terminal_handle_stale')
})
it('rejects confirmation evidence when the owning PTY exits', async () => {
const confirmation = deferred<string | null>()
const confirmForegroundProcess = vi.fn(() => confirmation.promise)
const { runtime, handle } = await createExplicitAgentStatusHarness({
getForegroundProcess: async () => 'powershell.exe',
confirmForegroundProcess
})
const status = runtime.getTerminalAgentStatus(handle)
await vi.waitFor(() => expect(confirmForegroundProcess).toHaveBeenCalledWith('pty-1'))
runtime.onPtyExit('pty-1', 0)
confirmation.resolve('codex')
await expect(status).rejects.toThrow('terminal_exited')
})
it('reports permission from a title-derived action-required agent state', async () => {
const runtime = new OrcaRuntimeService(store)
runtime.setPtyController({