fix(codex): narrow app-server unsupported detection

This commit is contained in:
Brennan Benson
2026-07-16 01:20:46 -07:00
parent 1f9ab948e8
commit f64fb13961
4 changed files with 40 additions and 6 deletions
+1
View File
@@ -12,6 +12,7 @@
"../src/main/claude/hook-settings.ts",
"../src/main/claude/hook-service.ts",
"../src/main/codex/codex-app-server-capability-cache.ts",
"../src/main/codex/codex-app-server-capability-signal.ts",
"../src/main/codex/codex-app-server-client.ts",
"../src/main/codex/codex-app-server-grant-bridge.ts",
"../src/main/codex/codex-app-server-grant-envelope.ts",
@@ -0,0 +1,7 @@
// Why: only an argv parse failure that names app-server proves the RPC surface
// is absent; unrelated stderr must remain transient rather than poison the cache.
export function stderrIndicatesMissingAppServer(stderrTail: string): boolean {
return /^(?=.*\bapp-server\b).*(?:unrecognized subcommand|unexpected argument|invalid subcommand)/im.test(
stderrTail
)
}
@@ -49,6 +49,11 @@ if (config.scenario === 'no-subcommand') {
process.stderr.write("error: unrecognized subcommand 'app-server'\\n")
process.exit(2)
}
if (config.scenario === 'transient-stderr') {
process.stderr.write('config loader: unexpected argument in value\\n')
process.exit(1)
}
if (config.scenario === 'silent-exit-0') process.exit(0)
process.stdin.setEncoding('utf8')
process.stdin.on('data', (chunk) => {
buffer += chunk
@@ -278,6 +283,32 @@ describe('runCodexHookTrustGrantSession', () => {
expect(isCodexAppServerUnsupportedError(error)).toBe(true)
})
it('does not classify unrelated stderr as a missing app-server capability', async () => {
const { request } = createStubRequest({
scenario: 'transient-stderr',
hooks: [],
expectedTrustKeys: ['k'],
managedCommand: MANAGED_COMMAND
})
const error = await runCodexHookTrustGrantSession(request).catch((caught: unknown) => caught)
expect(error).toBeInstanceOf(Error)
expect(isCodexAppServerUnsupportedError(error)).toBe(false)
})
it('treats a silent exit zero before initialize as transient', async () => {
const { request } = createStubRequest({
scenario: 'silent-exit-0',
hooks: [],
expectedTrustKeys: ['k'],
managedCommand: MANAGED_COMMAND
})
const error = await runCodexHookTrustGrantSession(request).catch((caught: unknown) => caught)
expect(error).toBeInstanceOf(Error)
expect(isCodexAppServerUnsupportedError(error)).toBe(false)
})
it('kills a hung server at the session deadline', async () => {
const keys = ['/home/a/.codex/hooks.json:session_start:0:0']
const { request, pidFile } = createStubRequest({
+1 -6
View File
@@ -1,6 +1,7 @@
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
import { normalizeHookTrustKeyForLookup } from './config-toml-trust'
import { waitForProcessExitUntil } from './codex-process-exit-deadline'
import { stderrIndicatesMissingAppServer } from './codex-app-server-capability-signal'
// Why: Codex gates hooks on a `trusted_hash` it computes from a private
// canonical-JSON identity. Orca used to replicate that algorithm
@@ -94,12 +95,6 @@ function isMethodNotFoundError(error: { code?: number; message?: string }): bool
return error.code === JSON_RPC_METHOD_NOT_FOUND || /method not found/i.test(error.message ?? '')
}
// Why: a CLI predating the app-server subcommand fails argv parsing before
// speaking any JSON-RPC; that shape is a capability signal, not a transient.
function stderrIndicatesMissingAppServer(stderrTail: string): boolean {
return /unrecognized subcommand|unexpected argument|invalid subcommand/i.test(stderrTail)
}
function collectHookListings(result: unknown): CodexHookListing[] {
const data =
result && typeof result === 'object' && Array.isArray((result as { data?: unknown }).data)