mirror of
https://github.com/stablyai/orca.git
synced 2026-10-13 00:02:54 +00:00
Show Codex permission requests as waiting
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -473,7 +473,7 @@ describe('Codex hook normalization', () => {
|
||||
// Why: Codex's PreToolUse is NOT an approval prompt — it fires for every
|
||||
// tool call. We map it to `working` (never `waiting`) and use it only to
|
||||
// give the dashboard a live readout during the gap between prompt and
|
||||
// Stop. Real approval signals flow through Codex's `notify` callback.
|
||||
// Stop. Real approval signals flow through PermissionRequest.
|
||||
const result = _internals.normalizeHookPayload(
|
||||
'codex',
|
||||
buildBody({
|
||||
@@ -488,6 +488,26 @@ describe('Codex hook normalization', () => {
|
||||
expect(result?.payload.toolInput).toBe('git status')
|
||||
})
|
||||
|
||||
it('PermissionRequest maps to waiting and surfaces the pending tool input', () => {
|
||||
// Why: Codex asks for user attention through PermissionRequest. Orca's
|
||||
// sidebar red dot depends on this becoming `waiting`; treating it like
|
||||
// PreToolUse would leave the pane looking busy while it is blocked on the
|
||||
// user.
|
||||
const result = _internals.normalizeHookPayload(
|
||||
'codex',
|
||||
buildBody({
|
||||
hook_event_name: 'PermissionRequest',
|
||||
tool_name: 'exec_command',
|
||||
tool_input: { cmd: 'rm -rf build', workdir: '/tmp' }
|
||||
}),
|
||||
'production'
|
||||
)
|
||||
expect(result?.payload.state).toBe('waiting')
|
||||
expect(result?.payload.agentType).toBe('codex')
|
||||
expect(result?.payload.toolName).toBe('exec_command')
|
||||
expect(result?.payload.toolInput).toBe('rm -rf build')
|
||||
})
|
||||
|
||||
it('UserPromptSubmit does not extract tool fields even when the payload carries them', () => {
|
||||
// Why: UserPromptSubmit is a turn-boundary event; any tool_name on it
|
||||
// would be leftover noise and should not leak into the working-state
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mkdtempSync, readFileSync, rmSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import type * as Os from 'os'
|
||||
import { join } from 'path'
|
||||
|
||||
const { getPathMock, homedirMock } = vi.hoisted(() => ({
|
||||
getPathMock: vi.fn<(name: string) => string>(),
|
||||
homedirMock: vi.fn<() => string>()
|
||||
}))
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
app: {
|
||||
getPath: getPathMock
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('os', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof Os>()
|
||||
return {
|
||||
...actual,
|
||||
homedir: homedirMock
|
||||
}
|
||||
})
|
||||
|
||||
import { CodexHookService } from './hook-service'
|
||||
|
||||
let tmpHome: string
|
||||
let userDataDir: string
|
||||
|
||||
beforeEach(() => {
|
||||
tmpHome = mkdtempSync(join(tmpdir(), 'orca-codex-home-'))
|
||||
userDataDir = mkdtempSync(join(tmpdir(), 'orca-codex-user-data-'))
|
||||
homedirMock.mockReturnValue(tmpHome)
|
||||
getPathMock.mockImplementation((name: string) => {
|
||||
if (name === 'userData') {
|
||||
return userDataDir
|
||||
}
|
||||
throw new Error(`unexpected app.getPath(${name})`)
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(tmpHome, { recursive: true, force: true })
|
||||
rmSync(userDataDir, { recursive: true, force: true })
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('CodexHookService', () => {
|
||||
it('installs PermissionRequest with trust so Codex approval prompts reach Orca', () => {
|
||||
const status = new CodexHookService().install()
|
||||
|
||||
expect(status.state).toBe('installed')
|
||||
|
||||
const hooksConfig = JSON.parse(
|
||||
readFileSync(join(tmpHome, '.codex', 'hooks.json'), 'utf-8')
|
||||
) as {
|
||||
hooks: Record<string, { hooks?: { command?: string }[] }[]>
|
||||
}
|
||||
|
||||
expect(Object.keys(hooksConfig.hooks).sort()).toEqual(
|
||||
[
|
||||
'PermissionRequest',
|
||||
'PostToolUse',
|
||||
'PreToolUse',
|
||||
'SessionStart',
|
||||
'Stop',
|
||||
'UserPromptSubmit'
|
||||
].sort()
|
||||
)
|
||||
expect(hooksConfig.hooks.PermissionRequest?.[0]?.hooks?.[0]?.command).toContain('agent-hooks')
|
||||
expect(hooksConfig.hooks.PermissionRequest?.[0]?.hooks?.[0]?.command).toContain('codex-hook')
|
||||
|
||||
const trustConfig = readFileSync(join(tmpHome, '.codex', 'config.toml'), 'utf-8')
|
||||
expect(trustConfig).toContain(':permission_request:0:0')
|
||||
})
|
||||
})
|
||||
@@ -26,15 +26,14 @@ import {
|
||||
|
||||
// Why: PreToolUse/PostToolUse give the dashboard a live readout of the
|
||||
// in-flight tool (name + input preview) between UserPromptSubmit and Stop.
|
||||
// Without them, a long-running Codex turn looks like a silent gap after the
|
||||
// prompt is submitted. We keep both events mapped to `working` (see
|
||||
// normalizeCodexEvent); they do NOT mean "approval needed" — Codex's real
|
||||
// approval signal flows through its separate `notify` callback (different
|
||||
// install surface, different payload shape) and is deferred as a follow-up.
|
||||
// PermissionRequest is the human-input boundary: the managed script exits
|
||||
// without a decision so Codex still shows its normal approval UI, while Orca
|
||||
// can flip the pane to the red waiting state.
|
||||
const CODEX_EVENTS = [
|
||||
'SessionStart',
|
||||
'UserPromptSubmit',
|
||||
'PreToolUse',
|
||||
'PermissionRequest',
|
||||
'PostToolUse',
|
||||
'Stop'
|
||||
] as const
|
||||
@@ -55,6 +54,7 @@ const CODEX_EVENT_LABEL: Record<(typeof CODEX_EVENTS)[number], CodexEventLabel>
|
||||
SessionStart: 'session_start',
|
||||
UserPromptSubmit: 'user_prompt_submit',
|
||||
PreToolUse: 'pre_tool_use',
|
||||
PermissionRequest: 'permission_request',
|
||||
PostToolUse: 'post_tool_use',
|
||||
Stop: 'stop'
|
||||
}
|
||||
|
||||
@@ -481,7 +481,11 @@ function extractCodexToolFields(
|
||||
eventName: unknown,
|
||||
hookPayload: Record<string, unknown>
|
||||
): ToolSnapshot {
|
||||
if (eventName === 'PreToolUse' || eventName === 'PostToolUse') {
|
||||
if (
|
||||
eventName === 'PreToolUse' ||
|
||||
eventName === 'PermissionRequest' ||
|
||||
eventName === 'PostToolUse'
|
||||
) {
|
||||
const toolName = readString(hookPayload, 'tool_name') ?? readString(hookPayload, 'name')
|
||||
const toolInput =
|
||||
deriveToolInputPreview(toolName, hookPayload.tool_input) ??
|
||||
@@ -759,9 +763,11 @@ function normalizeCodexEvent(
|
||||
eventName === 'PreToolUse' ||
|
||||
eventName === 'PostToolUse'
|
||||
? 'working'
|
||||
: eventName === 'Stop'
|
||||
? 'done'
|
||||
: null
|
||||
: eventName === 'PermissionRequest'
|
||||
? 'waiting'
|
||||
: eventName === 'Stop'
|
||||
? 'done'
|
||||
: null
|
||||
|
||||
if (!stateName) {
|
||||
return null
|
||||
|
||||
Reference in New Issue
Block a user