Fix slow Codex hook statuses on Windows (#6398)
Co-authored-by: Neil <neil@stably.ai>
This commit is contained in:
parent
ef477199af
commit
fa3fd3ae02
|
|
@ -345,12 +345,9 @@ describe('wrapWindowsHookCommand', () => {
|
|||
return Buffer.from(encodedCommand!, 'base64').toString('utf16le')
|
||||
}
|
||||
|
||||
it('invokes the .cmd through an encoded PowerShell command', () => {
|
||||
it('uses the script path directly when no cmd escaping is needed', () => {
|
||||
const command = wrapWindowsHookCommand('C:\\Users\\alice\\.orca\\agent-hooks\\codex-hook.cmd')
|
||||
expect(command).toMatch(/^powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand \S+$/)
|
||||
expect(decodeWindowsHookCommand(command)).toBe(
|
||||
"& 'C:\\Users\\alice\\.orca\\agent-hooks\\codex-hook.cmd'; exit $LASTEXITCODE"
|
||||
)
|
||||
expect(command).toBe('C:\\Users\\alice\\.orca\\agent-hooks\\codex-hook.cmd')
|
||||
})
|
||||
|
||||
// Why: a user profile path like `C:\Users\Jane Doe` is the regression from
|
||||
|
|
@ -358,6 +355,7 @@ describe('wrapWindowsHookCommand', () => {
|
|||
// the whole path inside the encoded command so shells do not split it.
|
||||
it('preserves spaces in the script path (user profile with space case)', () => {
|
||||
const cmd = wrapWindowsHookCommand('C:\\Users\\Jorge Silva\\.orca\\agent-hooks\\codex-hook.cmd')
|
||||
expect(cmd).toMatch(/^powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand \S+$/)
|
||||
expect(decodeWindowsHookCommand(cmd)).toBe(
|
||||
"& 'C:\\Users\\Jorge Silva\\.orca\\agent-hooks\\codex-hook.cmd'; exit $LASTEXITCODE"
|
||||
)
|
||||
|
|
@ -390,15 +388,17 @@ describe('wrapWindowsHookCommand', () => {
|
|||
})
|
||||
|
||||
describe('buildWindowsAgentHookPostCommand', () => {
|
||||
it('forces UTF-8 for redirected hook stdin and POST bodies', () => {
|
||||
it('posts hook stdin through bounded curl without spawning PowerShell', () => {
|
||||
const command = buildWindowsAgentHookPostCommand('codex')
|
||||
|
||||
expect(command).toContain('[Console]::InputEncoding=$utf8')
|
||||
expect(command).toContain('[Console]::OutputEncoding=$utf8')
|
||||
expect(command).toContain('$bodyBytes=$utf8.GetBytes($body)')
|
||||
expect(command).toContain("-ContentType 'application/json; charset=utf-8'")
|
||||
expect(command).toContain('-TimeoutSec 2')
|
||||
expect(command).toContain('curl.exe -sS -X POST')
|
||||
expect(command).toContain('--connect-timeout 0.5 --max-time 1.5')
|
||||
expect(command).toContain('-H "Content-Type: application/x-www-form-urlencoded"')
|
||||
expect(command).toContain('-H "X-Orca-Agent-Hook-Token: %ORCA_AGENT_HOOK_TOKEN%"')
|
||||
expect(command).toContain('--data-urlencode "paneKey=%ORCA_PANE_KEY%"')
|
||||
expect(command).toContain('--data-urlencode "payload@-"')
|
||||
expect(command).toContain('/hook/codex')
|
||||
expect(command).not.toContain("'Content-Type'='application/json'")
|
||||
expect(command).not.toContain('powershell')
|
||||
expect(command).not.toContain('Invoke-WebRequest')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -147,23 +147,33 @@ function quotePowerShellString(value: string): string {
|
|||
return `'${value.replaceAll("'", "''")}'`
|
||||
}
|
||||
|
||||
// Why: Windows splits a raw hook command on whitespace, so a user profile path
|
||||
// like `C:\Users\Jane Doe` makes the agent try to execute `C:\Users\Jane` and
|
||||
// fail with exit code 1. Keep the script path inside an encoded PowerShell
|
||||
// command so cmd.exe never gets a chance to expand legal path characters like
|
||||
// `%` or `^` before the .cmd is invoked. #6078.
|
||||
export function wrapWindowsHookCommand(scriptPath: string): string {
|
||||
if (!/[ \t&()^|<>%!"]/u.test(scriptPath)) {
|
||||
return scriptPath
|
||||
}
|
||||
// Why: Windows splits raw hook commands on whitespace, and cmd.exe expands
|
||||
// `%`/`^`; use PowerShell only for paths that need that compatibility shim.
|
||||
const command = `& ${quotePowerShellString(scriptPath)}; exit $LASTEXITCODE`
|
||||
const encodedCommand = Buffer.from(command, 'utf16le').toString('base64')
|
||||
return `powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand ${encodedCommand}`
|
||||
}
|
||||
|
||||
export function buildWindowsAgentHookPostCommand(source: AgentHookSource): string {
|
||||
// Why: Windows PowerShell 5.1 defaults redirected stdin/request bodies to the
|
||||
// active code page. Hook payloads are UTF-8 JSON, so force UTF-8 on both read
|
||||
// and POST or CJK prompts arrive in Orca as literal question marks. Timeout
|
||||
// caps best-effort hook posts if the local listener stalls.
|
||||
return `powershell -NoProfile -ExecutionPolicy Bypass -Command "$utf8=[System.Text.UTF8Encoding]::new($false); [Console]::InputEncoding=$utf8; [Console]::OutputEncoding=$utf8; $inputData=[Console]::In.ReadToEnd(); if ([string]::IsNullOrWhiteSpace($inputData)) { exit 0 }; try { $body=@{ paneKey=$env:ORCA_PANE_KEY; launchToken=$env:ORCA_AGENT_LAUNCH_TOKEN; tabId=$env:ORCA_TAB_ID; worktreeId=$env:ORCA_WORKTREE_ID; env=$env:ORCA_AGENT_HOOK_ENV; version=$env:ORCA_AGENT_HOOK_VERSION; payload=($inputData | ConvertFrom-Json) } | ConvertTo-Json -Depth 100 -Compress; $bodyBytes=$utf8.GetBytes($body); Invoke-WebRequest -UseBasicParsing -Method Post -Uri ('http://127.0.0.1:' + $env:ORCA_AGENT_HOOK_PORT + '/hook/${source}') -ContentType 'application/json; charset=utf-8' -Headers @{ 'X-Orca-Agent-Hook-Token'=$env:ORCA_AGENT_HOOK_TOKEN } -Body $bodyBytes -TimeoutSec 2 | Out-Null } catch {}"`
|
||||
// Why: Codex runs these hooks inline on every turn. PowerShell startup alone
|
||||
// makes trusted Windows hooks visibly slow, so mirror the POSIX curl path.
|
||||
return [
|
||||
`curl.exe -sS -X POST "http://127.0.0.1:%ORCA_AGENT_HOOK_PORT%/hook/${source}" ^`,
|
||||
' --connect-timeout 0.5 --max-time 1.5 ^',
|
||||
' -H "Content-Type: application/x-www-form-urlencoded" ^',
|
||||
' -H "X-Orca-Agent-Hook-Token: %ORCA_AGENT_HOOK_TOKEN%" ^',
|
||||
' --data-urlencode "paneKey=%ORCA_PANE_KEY%" ^',
|
||||
' --data-urlencode "tabId=%ORCA_TAB_ID%" ^',
|
||||
' --data-urlencode "launchToken=%ORCA_AGENT_LAUNCH_TOKEN%" ^',
|
||||
' --data-urlencode "worktreeId=%ORCA_WORKTREE_ID%" ^',
|
||||
' --data-urlencode "env=%ORCA_AGENT_HOOK_ENV%" ^',
|
||||
' --data-urlencode "version=%ORCA_AGENT_HOOK_VERSION%" ^',
|
||||
' --data-urlencode "payload@-" >nul 2>nul'
|
||||
].join('\r\n')
|
||||
}
|
||||
|
||||
export function removeManagedCommands(
|
||||
|
|
|
|||
|
|
@ -111,6 +111,16 @@ function markHookTrustDisabled(toml: string, header: string): string {
|
|||
return `${toml.slice(0, headerIndex)}${block.replace('enabled = true', 'enabled = false')}${toml.slice(blockEnd)}`
|
||||
}
|
||||
|
||||
function localManagedCodexEvents(): string[] {
|
||||
return process.platform === 'win32'
|
||||
? ['PermissionRequest', 'PostToolUse', 'PreToolUse']
|
||||
: ['PermissionRequest', 'PostToolUse', 'PreToolUse', 'SessionStart', 'Stop', 'UserPromptSubmit']
|
||||
}
|
||||
|
||||
function localHasManagedCodexLifecycleHooks(): boolean {
|
||||
return process.platform !== 'win32'
|
||||
}
|
||||
|
||||
describe('CodexHookService', () => {
|
||||
it('installs PermissionRequest with trust so Codex approval prompts reach Orca', () => {
|
||||
const systemCodexHome = join(tmpHome, '.codex')
|
||||
|
|
@ -130,16 +140,7 @@ describe('CodexHookService', () => {
|
|||
hooks: Record<string, { hooks?: { command?: string }[] }[]>
|
||||
}
|
||||
|
||||
expect(Object.keys(hooksConfig.hooks).sort()).toEqual(
|
||||
[
|
||||
'PermissionRequest',
|
||||
'PostToolUse',
|
||||
'PreToolUse',
|
||||
'SessionStart',
|
||||
'Stop',
|
||||
'UserPromptSubmit'
|
||||
].sort()
|
||||
)
|
||||
expect(Object.keys(hooksConfig.hooks).sort()).toEqual(localManagedCodexEvents())
|
||||
expect(
|
||||
isCodexManagedCommand(hooksConfig.hooks.PermissionRequest?.[0]?.hooks?.[0]?.command)
|
||||
).toBe(true)
|
||||
|
|
@ -198,7 +199,7 @@ describe('CodexHookService', () => {
|
|||
readFileSync(join(managedCodexHome, 'hooks.json'), 'utf-8')
|
||||
) as { hooks: Record<string, { hooks?: { command?: string }[] }[]> }
|
||||
|
||||
for (const eventName of ['SessionStart', 'UserPromptSubmit', 'Stop']) {
|
||||
for (const eventName of localManagedCodexEvents()) {
|
||||
const command = hooksConfig.hooks[eventName]?.[0]?.hooks?.[0]?.command
|
||||
expect(command).toMatch(
|
||||
/^powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand \S+$/
|
||||
|
|
@ -259,12 +260,12 @@ describe('CodexHookService', () => {
|
|||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
devHooks.hooks.Stop?.some((definition) =>
|
||||
devHooks.hooks.PreToolUse?.some((definition) =>
|
||||
isCodexManagedCommand(definition.hooks?.[0]?.command)
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
prodHooks.hooks.Stop?.some((definition) =>
|
||||
prodHooks.hooks.PreToolUse?.some((definition) =>
|
||||
isCodexManagedCommand(definition.hooks?.[0]?.command)
|
||||
)
|
||||
).toBe(true)
|
||||
|
|
@ -334,13 +335,18 @@ describe('CodexHookService', () => {
|
|||
{ matcher?: string; hooks?: { command?: string; statusMessage?: string }[] }[]
|
||||
>
|
||||
}
|
||||
expect(runtimeHooks.hooks.Stop?.[1]?.matcher).toBe('*')
|
||||
expect(runtimeHooks.hooks.Stop?.[1]?.hooks?.[0]?.command).toBe('user-hook')
|
||||
expect(runtimeHooks.hooks.Stop?.[1]?.hooks?.[0]?.statusMessage).toBe('Running user hook')
|
||||
const userStopIndex = localHasManagedCodexLifecycleHooks() ? 1 : 0
|
||||
expect(runtimeHooks.hooks.Stop?.[userStopIndex]?.matcher).toBe('*')
|
||||
expect(runtimeHooks.hooks.Stop?.[userStopIndex]?.hooks?.[0]?.command).toBe('user-hook')
|
||||
expect(runtimeHooks.hooks.Stop?.[userStopIndex]?.hooks?.[0]?.statusMessage).toBe(
|
||||
'Running user hook'
|
||||
)
|
||||
|
||||
const runtimeToml = readFileSync(join(managedCodexHome, 'config.toml'), 'utf-8')
|
||||
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:0:0`))
|
||||
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:1:0`))
|
||||
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:${userStopIndex}:0`))
|
||||
if (localHasManagedCodexLifecycleHooks()) {
|
||||
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:0:0`))
|
||||
}
|
||||
expect(runtimeToml).not.toContain(hookTrustHeader(`${systemHooksPath}:stop:0:0`))
|
||||
})
|
||||
|
||||
|
|
@ -440,7 +446,9 @@ describe('CodexHookService', () => {
|
|||
const runtimeToml = readFileSync(join(managedCodexHome, 'config.toml'), 'utf-8')
|
||||
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:0:0`))
|
||||
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:1:0`))
|
||||
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:2:0`))
|
||||
if (localHasManagedCodexLifecycleHooks()) {
|
||||
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:2:0`))
|
||||
}
|
||||
expect(runtimeToml).not.toContain(hookTrustHeader(`${systemHooksPath}:stop:0:0`))
|
||||
expect(runtimeToml).not.toContain(hookTrustHeader(`${systemHooksPath}:stop:1:0`))
|
||||
})
|
||||
|
|
@ -515,7 +523,9 @@ describe('CodexHookService', () => {
|
|||
) ?? []
|
||||
|
||||
expect(stopCommands).toContain(userCommand)
|
||||
expect(stopCommands.some((command) => isCodexManagedCommand(command))).toBe(true)
|
||||
expect(stopCommands.some((command) => isCodexManagedCommand(command))).toBe(
|
||||
localHasManagedCodexLifecycleHooks()
|
||||
)
|
||||
expect(runtimeHooks.hooks.PreCompact).toBeUndefined()
|
||||
for (const command of pluginCommands) {
|
||||
expect(runtimeHooksText).not.toContain(command)
|
||||
|
|
@ -523,7 +533,9 @@ describe('CodexHookService', () => {
|
|||
|
||||
const runtimeToml = readFileSync(join(managedCodexHome, 'config.toml'), 'utf-8')
|
||||
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:0:0`))
|
||||
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:1:0`))
|
||||
if (localHasManagedCodexLifecycleHooks()) {
|
||||
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:1:0`))
|
||||
}
|
||||
for (const command of pluginCommands) {
|
||||
expect(runtimeToml).not.toContain(command)
|
||||
}
|
||||
|
|
@ -621,7 +633,10 @@ describe('CodexHookService', () => {
|
|||
|
||||
const managedCodexHome = join(userDataDir, 'codex-runtime-home', 'home')
|
||||
const managedHooksPath = join(managedCodexHome, 'hooks.json')
|
||||
const runtimeUserTrustHeader = hookTrustHeader(`${managedHooksPath}:stop:1:0`)
|
||||
const runtimeUserStopIndex = localHasManagedCodexLifecycleHooks() ? 1 : 0
|
||||
const runtimeUserTrustHeader = hookTrustHeader(
|
||||
`${managedHooksPath}:stop:${runtimeUserStopIndex}:0`
|
||||
)
|
||||
expect(readFileSync(join(managedCodexHome, 'config.toml'), 'utf-8')).toContain(
|
||||
runtimeUserTrustHeader
|
||||
)
|
||||
|
|
@ -631,7 +646,11 @@ describe('CodexHookService', () => {
|
|||
|
||||
const runtimeToml = readFileSync(join(managedCodexHome, 'config.toml'), 'utf-8')
|
||||
expect(runtimeToml).not.toContain(runtimeUserTrustHeader)
|
||||
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:0:0`))
|
||||
if (localHasManagedCodexLifecycleHooks()) {
|
||||
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:0:0`))
|
||||
} else {
|
||||
expect(runtimeToml).not.toContain(':stop:0:0')
|
||||
}
|
||||
})
|
||||
|
||||
it('refreshes mirrored system user hooks when the system hooks file changes', () => {
|
||||
|
|
@ -1079,7 +1098,9 @@ describe('CodexHookService', () => {
|
|||
(definition) => definition.hooks?.map((hook) => hook.command ?? '') ?? []
|
||||
) ?? []
|
||||
expect(stopCommands).toContain(userCommand)
|
||||
expect(stopCommands.some((command) => isCodexManagedCommand(command))).toBe(true)
|
||||
expect(stopCommands.some((command) => isCodexManagedCommand(command))).toBe(
|
||||
localHasManagedCodexLifecycleHooks()
|
||||
)
|
||||
expect(
|
||||
isCodexManagedCommand(runtimeHooks.hooks.PermissionRequest?.[0]?.hooks?.[0]?.command)
|
||||
).toBe(true)
|
||||
|
|
@ -1200,33 +1221,35 @@ describe('CodexHookService', () => {
|
|||
const managedCodexHome = join(userDataDir, 'codex-runtime-home', 'home')
|
||||
const managedHooksPath = join(managedCodexHome, 'hooks.json')
|
||||
const runtimeTomlPath = join(managedCodexHome, 'config.toml')
|
||||
const canonicalSessionStartHeader = hookTrustHeader(`${managedHooksPath}:session_start:0:0`)
|
||||
const legacySessionStartHeader = `[hooks.state."${escapeTomlBasicString(
|
||||
`${realpathSync.native(managedHooksPath).replace(/\\/g, '/')}:session_start:0:0`
|
||||
const canonicalPermissionHeader = hookTrustHeader(
|
||||
`${managedHooksPath}:permission_request:0:0`
|
||||
)
|
||||
const legacyPermissionHeader = `[hooks.state."${escapeTomlBasicString(
|
||||
`${realpathSync.native(managedHooksPath).replace(/\\/g, '/')}:permission_request:0:0`
|
||||
)}"]`
|
||||
const installedToml = readFileSync(runtimeTomlPath, 'utf-8')
|
||||
expect(installedToml).toContain(canonicalSessionStartHeader)
|
||||
expect(installedToml).toContain(canonicalPermissionHeader)
|
||||
|
||||
writeFileSync(
|
||||
runtimeTomlPath,
|
||||
installedToml.replace(canonicalSessionStartHeader, legacySessionStartHeader),
|
||||
installedToml.replace(canonicalPermissionHeader, legacyPermissionHeader),
|
||||
'utf-8'
|
||||
)
|
||||
|
||||
const legacyToml = readFileSync(runtimeTomlPath, 'utf-8')
|
||||
expect(legacyToml).toContain(legacySessionStartHeader)
|
||||
expect(legacyToml).toContain(legacyPermissionHeader)
|
||||
expect(service.getStatus().state).toBe('installed')
|
||||
|
||||
expect(service.install().state).toBe('installed')
|
||||
|
||||
const repairedToml = readFileSync(runtimeTomlPath, 'utf-8')
|
||||
expect(repairedToml).not.toContain(legacySessionStartHeader)
|
||||
expect(repairedToml).toContain(canonicalSessionStartHeader)
|
||||
expect(repairedToml).not.toContain(legacyPermissionHeader)
|
||||
expect(repairedToml).toContain(canonicalPermissionHeader)
|
||||
expect(service.getStatus().state).toBe('installed')
|
||||
}
|
||||
)
|
||||
|
||||
it('repairs duplicate managed SessionStart trust tables on restart install', () => {
|
||||
it('repairs duplicate managed PermissionRequest trust tables on restart install', () => {
|
||||
const systemCodexHome = join(tmpHome, '.codex')
|
||||
mkdirSync(systemCodexHome, { recursive: true })
|
||||
writeFileSync(join(systemCodexHome, 'config.toml'), 'model = "system-model"\n', 'utf-8')
|
||||
|
|
@ -1237,21 +1260,24 @@ describe('CodexHookService', () => {
|
|||
const managedCodexHome = join(userDataDir, 'codex-runtime-home', 'home')
|
||||
const managedHooksPath = join(managedCodexHome, 'hooks.json')
|
||||
const runtimeTomlPath = join(managedCodexHome, 'config.toml')
|
||||
const sessionStartHeader = hookTrustHeader(`${managedHooksPath}:session_start:0:0`)
|
||||
const permissionRequestHeader = hookTrustHeader(`${managedHooksPath}:permission_request:0:0`)
|
||||
const installedToml = readFileSync(runtimeTomlPath, 'utf-8')
|
||||
const sessionStartIndex = installedToml.indexOf(sessionStartHeader)
|
||||
expect(sessionStartIndex).not.toBe(-1)
|
||||
const permissionRequestIndex = installedToml.indexOf(permissionRequestHeader)
|
||||
expect(permissionRequestIndex).not.toBe(-1)
|
||||
const nextHeaderIndex = installedToml.indexOf(
|
||||
'\n[',
|
||||
sessionStartIndex + sessionStartHeader.length
|
||||
permissionRequestIndex + permissionRequestHeader.length
|
||||
)
|
||||
const sessionStartBlock = installedToml
|
||||
.slice(sessionStartIndex, nextHeaderIndex === -1 ? installedToml.length : nextHeaderIndex)
|
||||
const permissionRequestBlock = installedToml
|
||||
.slice(
|
||||
permissionRequestIndex,
|
||||
nextHeaderIndex === -1 ? installedToml.length : nextHeaderIndex
|
||||
)
|
||||
.trimEnd()
|
||||
const staleDisabledBlock = sessionStartBlock
|
||||
const staleDisabledBlock = permissionRequestBlock
|
||||
.replace('enabled = true', 'enabled = false')
|
||||
.replace(/trusted_hash = "[^"]+"/, 'trusted_hash = "sha256:STALE_DISABLED"')
|
||||
const staleEnabledBlock = sessionStartBlock.replace(
|
||||
const staleEnabledBlock = permissionRequestBlock.replace(
|
||||
/trusted_hash = "[^"]+"/,
|
||||
'trusted_hash = "sha256:STALE_ENABLED"'
|
||||
)
|
||||
|
|
@ -1259,20 +1285,20 @@ describe('CodexHookService', () => {
|
|||
runtimeTomlPath,
|
||||
`${installedToml.slice(
|
||||
0,
|
||||
sessionStartIndex
|
||||
permissionRequestIndex
|
||||
)}${staleDisabledBlock}\n\n${staleEnabledBlock}${installedToml.slice(
|
||||
nextHeaderIndex === -1 ? installedToml.length : nextHeaderIndex
|
||||
)}`,
|
||||
'utf-8'
|
||||
)
|
||||
expect(readFileSync(runtimeTomlPath, 'utf-8').split(sessionStartHeader)).toHaveLength(3)
|
||||
expect(readFileSync(runtimeTomlPath, 'utf-8').split(permissionRequestHeader)).toHaveLength(3)
|
||||
|
||||
// Why: preserving `enabled = false` is the repair contract; status can be
|
||||
// partial because the user-disabled managed hook remains disabled.
|
||||
expect(['installed', 'partial']).toContain(service.install().state)
|
||||
|
||||
const repairedToml = readFileSync(runtimeTomlPath, 'utf-8')
|
||||
expect(repairedToml.split(sessionStartHeader)).toHaveLength(2)
|
||||
expect(repairedToml.split(permissionRequestHeader)).toHaveLength(2)
|
||||
expect(repairedToml).toContain('enabled = false')
|
||||
expect(repairedToml).not.toContain('STALE_DISABLED')
|
||||
expect(repairedToml).not.toContain('STALE_ENABLED')
|
||||
|
|
|
|||
|
|
@ -59,6 +59,16 @@ const CODEX_EVENTS = [
|
|||
'Stop'
|
||||
] as const
|
||||
|
||||
const CODEX_WINDOWS_LOCAL_EVENTS = ['PreToolUse', 'PermissionRequest', 'PostToolUse'] as const
|
||||
|
||||
function getLocalCodexManagedEvents(): readonly (typeof CODEX_EVENTS)[number][] {
|
||||
// Why: Codex renders synchronous lifecycle hooks as prominent TUI progress
|
||||
// rows on Windows, where even fast hook processes leave multi-second stale
|
||||
// screen fragments. Keep the tool/permission hooks Orca needs for live
|
||||
// status and approvals, but avoid chat lifecycle hooks in local Windows PTYs.
|
||||
return process.platform === 'win32' ? CODEX_WINDOWS_LOCAL_EVENTS : CODEX_EVENTS
|
||||
}
|
||||
|
||||
function getConfigPath(): string {
|
||||
return join(getOrcaManagedCodexHomePath(), 'hooks.json')
|
||||
}
|
||||
|
|
@ -86,9 +96,9 @@ const CODEX_EVENT_LABEL: Record<(typeof CODEX_EVENTS)[number], CodexEventLabel>
|
|||
Stop: 'stop'
|
||||
}
|
||||
|
||||
const CODEX_MANAGED_EVENT_LABELS = new Set<CodexEventLabel>(
|
||||
CODEX_EVENTS.map((eventName) => CODEX_EVENT_LABEL[eventName])
|
||||
)
|
||||
function getLocalCodexManagedEventLabels(): Set<CodexEventLabel> {
|
||||
return new Set(getLocalCodexManagedEvents().map((eventName) => CODEX_EVENT_LABEL[eventName]))
|
||||
}
|
||||
|
||||
const CODEX_HOOK_EVENT_LABEL: Record<string, CodexEventLabel> = {
|
||||
...CODEX_EVENT_LABEL,
|
||||
|
|
@ -450,7 +460,7 @@ function moveMirroredRuntimeUserTrustAfterManagedStatusHook(
|
|||
entries: readonly MirroredRuntimeUserHookTrustEntry[]
|
||||
): MirroredRuntimeUserHookTrustEntry[] {
|
||||
return entries.map(({ entry, enabled }) => {
|
||||
if (!CODEX_MANAGED_EVENT_LABELS.has(entry.eventLabel)) {
|
||||
if (!getLocalCodexManagedEventLabels().has(entry.eventLabel)) {
|
||||
return { entry, enabled }
|
||||
}
|
||||
return {
|
||||
|
|
@ -655,9 +665,11 @@ function removeRuntimeManagedHookTrustEntries(configPath: string): void {
|
|||
// recognize (and clean up) its own managed trust entries.
|
||||
timeoutSec: MANAGED_HOOK_TIMEOUT_SECONDS
|
||||
}
|
||||
const expectedHash = computeTrustedHash(expectedEntry)
|
||||
const legacyHash = computeTrustedHash({ ...expectedEntry, timeoutSec: undefined })
|
||||
if (state.trustedHash !== expectedHash && state.trustedHash !== legacyHash) {
|
||||
const recognizedHashes = new Set([
|
||||
computeTrustedHash(expectedEntry),
|
||||
computeTrustedHash({ ...expectedEntry, timeoutSec: undefined })
|
||||
])
|
||||
if (!state.trustedHash || !recognizedHashes.has(state.trustedHash)) {
|
||||
continue
|
||||
}
|
||||
ourKeys.push(key)
|
||||
|
|
@ -763,7 +775,7 @@ export class CodexHookService {
|
|||
const trustMissing: string[] = []
|
||||
const disabled: string[] = []
|
||||
let presentCount = 0
|
||||
for (const eventName of CODEX_EVENTS) {
|
||||
for (const eventName of getLocalCodexManagedEvents()) {
|
||||
const definitions = Array.isArray(config.hooks?.[eventName]) ? config.hooks![eventName]! : []
|
||||
// Why: older installs appended this command, while current installs
|
||||
// prepend it. Picking the last match keeps status repair conservative
|
||||
|
|
@ -867,7 +879,8 @@ export class CodexHookService {
|
|||
const command = getManagedCommand(scriptPath)
|
||||
const hookPlan = getRuntimeHooksWithSystemUserHooks(config.hooks, isManagedCommand)
|
||||
const nextHooks = hookPlan.hooks
|
||||
const managedEvents = new Set<string>(CODEX_EVENTS)
|
||||
const localManagedEvents = getLocalCodexManagedEvents()
|
||||
const managedEvents = new Set<string>(localManagedEvents)
|
||||
|
||||
// Why: sweep managed entries out of events we no longer subscribe to
|
||||
// (e.g., PreToolUse from a prior install). Without this, users who
|
||||
|
|
@ -900,7 +913,7 @@ export class CodexHookService {
|
|||
hookPlan.trustEntries
|
||||
)
|
||||
const trustEntries: CodexTrustEntry[] = mirroredUserTrustEntries.map(({ entry }) => entry)
|
||||
for (const eventName of CODEX_EVENTS) {
|
||||
for (const eventName of localManagedEvents) {
|
||||
const current = Array.isArray(nextHooks[eventName]) ? nextHooks[eventName] : []
|
||||
const cleaned = removeManagedCommands(current, isManagedCommand)
|
||||
const definition: HookDefinition = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue