diff --git a/sources/components/tools/PermissionFooter.tsx b/sources/components/tools/PermissionFooter.tsx index 0737d399..01c7d877 100644 --- a/sources/components/tools/PermissionFooter.tsx +++ b/sources/components/tools/PermissionFooter.tsx @@ -13,7 +13,7 @@ interface PermissionFooterProps { reason?: string; mode?: string; allowedTools?: string[]; - decision?: 'approved' | 'approved_for_session' | 'denied' | 'abort'; + decision?: 'approved' | 'approved_for_session' | 'approved_execpolicy_amendment' | 'denied' | 'abort'; }; sessionId: string; toolName: string; @@ -26,9 +26,19 @@ export const PermissionFooter: React.FC = ({ permission, const [loadingButton, setLoadingButton] = useState<'allow' | 'deny' | 'abort' | null>(null); const [loadingAllEdits, setLoadingAllEdits] = useState(false); const [loadingForSession, setLoadingForSession] = useState(false); + const [loadingExecPolicy, setLoadingExecPolicy] = useState(false); // Check if this is a Codex session - check both metadata.flavor and tool name prefix const isCodex = metadata?.flavor === 'codex' || toolName.startsWith('Codex'); + // Codex always provides proposed_execpolicy_amendment + const execPolicyCommand = (() => { + const proposedAmendment = toolInput?.proposedExecpolicyAmendment ?? toolInput?.proposed_execpolicy_amendment; + if (Array.isArray(proposedAmendment)) { + return proposedAmendment.filter((part: unknown): part is string => typeof part === 'string' && part.length > 0); + } + return []; + })(); + const canApproveExecPolicy = isCodex && execPolicyCommand.length > 0; const handleApprove = async () => { if (permission.status !== 'pending' || loadingButton !== null || loadingAllEdits || loadingForSession) return; @@ -93,7 +103,7 @@ export const PermissionFooter: React.FC = ({ permission, // Codex-specific handlers const handleCodexApprove = async () => { - if (permission.status !== 'pending' || loadingButton !== null || loadingForSession) return; + if (permission.status !== 'pending' || loadingButton !== null || loadingForSession || loadingExecPolicy) return; setLoadingButton('allow'); try { @@ -106,7 +116,7 @@ export const PermissionFooter: React.FC = ({ permission, }; const handleCodexApproveForSession = async () => { - if (permission.status !== 'pending' || loadingButton !== null || loadingForSession) return; + if (permission.status !== 'pending' || loadingButton !== null || loadingForSession || loadingExecPolicy) return; setLoadingForSession(true); try { @@ -117,9 +127,29 @@ export const PermissionFooter: React.FC = ({ permission, setLoadingForSession(false); } }; + + const handleCodexApproveExecPolicy = async () => { + if (permission.status !== 'pending' || loadingButton !== null || loadingForSession || loadingExecPolicy || !canApproveExecPolicy) return; + + setLoadingExecPolicy(true); + try { + await sessionAllow( + sessionId, + permission.id, + undefined, + undefined, + 'approved_execpolicy_amendment', + { command: execPolicyCommand } + ); + } catch (error) { + console.error('Failed to approve with execpolicy amendment:', error); + } finally { + setLoadingExecPolicy(false); + } + }; const handleCodexAbort = async () => { - if (permission.status !== 'pending' || loadingButton !== null || loadingForSession) return; + if (permission.status !== 'pending' || loadingButton !== null || loadingForSession || loadingExecPolicy) return; setLoadingButton('abort'); try { @@ -159,6 +189,7 @@ export const PermissionFooter: React.FC = ({ permission, // Codex-specific status detection with fallback const isCodexApproved = isCodex && isApproved && (permission.decision === 'approved' || !permission.decision); const isCodexApprovedForSession = isCodex && isApproved && permission.decision === 'approved_for_session'; + const isCodexApprovedExecPolicy = isCodex && isApproved && permission.decision === 'approved_execpolicy_amendment'; const isCodexAborted = isCodex && isDenied && permission.decision === 'abort'; const styles = StyleSheet.create({ @@ -268,10 +299,10 @@ export const PermissionFooter: React.FC = ({ permission, styles.button, isPending && styles.buttonAllow, isCodexApproved && styles.buttonSelected, - (isCodexAborted || isCodexApprovedForSession) && styles.buttonInactive + (isCodexAborted || isCodexApprovedForSession || isCodexApprovedExecPolicy) && styles.buttonInactive ]} onPress={handleCodexApprove} - disabled={!isPending || loadingButton !== null || loadingForSession} + disabled={!isPending || loadingButton !== null || loadingForSession || loadingExecPolicy} activeOpacity={isPending ? 0.7 : 1} > {loadingButton === 'allow' && isPending ? ( @@ -291,16 +322,47 @@ export const PermissionFooter: React.FC = ({ permission, )} + {/* Codex: Yes, always allow this command button */} + {canApproveExecPolicy && ( + + {loadingExecPolicy && isPending ? ( + + + + ) : ( + + + {t('codex.permissions.yesAlwaysAllowCommand')} + + + )} + + )} + {/* Codex: Yes, and don't ask for a session button */} {loadingForSession && isPending ? ( @@ -326,10 +388,10 @@ export const PermissionFooter: React.FC = ({ permission, styles.button, isPending && styles.buttonDeny, isCodexAborted && styles.buttonSelected, - (isCodexApproved || isCodexApprovedForSession) && styles.buttonInactive + (isCodexApproved || isCodexApprovedForSession || isCodexApprovedExecPolicy) && styles.buttonInactive ]} onPress={handleCodexAbort} - disabled={!isPending || loadingButton !== null || loadingForSession} + disabled={!isPending || loadingButton !== null || loadingForSession || loadingExecPolicy} activeOpacity={isPending ? 0.7 : 1} > {loadingButton === 'abort' && isPending ? ( @@ -477,4 +539,4 @@ export const PermissionFooter: React.FC = ({ permission, ); -}; \ No newline at end of file +}; diff --git a/sources/sync/ops.ts b/sources/sync/ops.ts index a4efca9e..87023df7 100644 --- a/sources/sync/ops.ts +++ b/sources/sync/ops.ts @@ -16,7 +16,10 @@ interface SessionPermissionRequest { reason?: string; mode?: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan'; allowTools?: string[]; - decision?: 'approved' | 'approved_for_session' | 'denied' | 'abort'; + decision?: 'approved' | 'approved_for_session' | 'approved_execpolicy_amendment' | 'denied' | 'abort'; + execPolicyAmendment?: { + command: string[]; + }; } // Mode change operation types @@ -299,8 +302,22 @@ export async function sessionAbort(sessionId: string): Promise { /** * Allow a permission request */ -export async function sessionAllow(sessionId: string, id: string, mode?: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan', allowedTools?: string[], decision?: 'approved' | 'approved_for_session'): Promise { - const request: SessionPermissionRequest = { id, approved: true, mode, allowTools: allowedTools, decision }; +export async function sessionAllow( + sessionId: string, + id: string, + mode?: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan', + allowedTools?: string[], + decision?: 'approved' | 'approved_for_session' | 'approved_execpolicy_amendment', + execPolicyAmendment?: { command: string[] } +): Promise { + const request: SessionPermissionRequest = { + id, + approved: true, + mode, + allowTools: allowedTools, + decision, + execPolicyAmendment + }; await apiSocket.sessionRPC(sessionId, 'permission', request); } @@ -520,4 +537,4 @@ export type { TreeNode, SessionRipgrepResponse, SessionKillResponse -}; \ No newline at end of file +}; diff --git a/sources/sync/reducer/reducer.ts b/sources/sync/reducer/reducer.ts index 542cc4f7..e71ffbb2 100644 --- a/sources/sync/reducer/reducer.ts +++ b/sources/sync/reducer/reducer.ts @@ -137,7 +137,7 @@ type StoredPermission = { reason?: string; mode?: string; allowedTools?: string[]; - decision?: 'approved' | 'approved_for_session' | 'denied' | 'abort'; + decision?: 'approved' | 'approved_for_session' | 'approved_execpolicy_amendment' | 'denied' | 'abort'; }; export type ReducerState = { @@ -351,16 +351,20 @@ export function reducer(state: ReducerState, messages: NormalizedMessage[], agen // Check if we already have a message for this permission ID const existingMessageId = state.toolIdToMessageId.get(permId); if (existingMessageId) { - // Update existing tool message with permission info + // Update existing tool message with permission info and latest arguments const message = state.messages.get(existingMessageId); - if (message?.tool && !message.tool.permission) { + if (message?.tool) { if (ENABLE_LOGGING) { console.log(`[REDUCER] Updating existing tool ${permId} with permission`); } - message.tool.permission = { - id: permId, - status: 'pending' - }; + // Always update input to get latest arguments (e.g., proposedExecpolicyAmendment) + message.tool.input = request.arguments; + if (!message.tool.permission) { + message.tool.permission = { + id: permId, + status: 'pending' + }; + } changed.add(existingMessageId); } } else { @@ -1147,4 +1151,4 @@ function convertReducerMessageToMessage(reducerMsg: ReducerMessage, state: Reduc } return null; -} \ No newline at end of file +} diff --git a/sources/sync/storageTypes.ts b/sources/sync/storageTypes.ts index bff187d0..f2fd3f8b 100644 --- a/sources/sync/storageTypes.ts +++ b/sources/sync/storageTypes.ts @@ -42,7 +42,7 @@ export const AgentStateSchema = z.object({ reason: z.string().nullish(), mode: z.string().nullish(), allowedTools: z.array(z.string()).nullish(), - decision: z.enum(['approved', 'approved_for_session', 'denied', 'abort']).nullish() + decision: z.enum(['approved', 'approved_for_session', 'approved_execpolicy_amendment', 'denied', 'abort']).nullish() })).nullish() }); @@ -153,4 +153,4 @@ export interface GitStatus { aheadCount?: number; // Commits ahead of upstream behindCount?: number; // Commits behind upstream stashCount?: number; // Number of stash entries -} \ No newline at end of file +} diff --git a/sources/sync/typesMessage.ts b/sources/sync/typesMessage.ts index e3b15cfe..d34d5556 100644 --- a/sources/sync/typesMessage.ts +++ b/sources/sync/typesMessage.ts @@ -16,7 +16,7 @@ export type ToolCall = { reason?: string; mode?: string; allowedTools?: string[]; - decision?: 'approved' | 'approved_for_session' | 'denied' | 'abort'; + decision?: 'approved' | 'approved_for_session' | 'approved_execpolicy_amendment' | 'denied' | 'abort'; date?: number; }; } @@ -59,4 +59,4 @@ export type ToolCallMessage = { meta?: MessageMeta; } -export type Message = UserTextMessage | AgentTextMessage | ToolCallMessage | ModeSwitchMessage; \ No newline at end of file +export type Message = UserTextMessage | AgentTextMessage | ToolCallMessage | ModeSwitchMessage; diff --git a/sources/sync/typesRaw.ts b/sources/sync/typesRaw.ts index 7956785a..dfca406b 100644 --- a/sources/sync/typesRaw.ts +++ b/sources/sync/typesRaw.ts @@ -54,7 +54,7 @@ const rawToolResultContentSchema = z.object({ result: z.enum(['approved', 'denied']), mode: z.string().optional(), allowedTools: z.array(z.string()).optional(), - decision: z.enum(['approved', 'approved_for_session', 'denied', 'abort']).optional(), + decision: z.enum(['approved', 'approved_for_session', 'approved_execpolicy_amendment', 'denied', 'abort']).optional(), }).optional(), }); export type RawToolResultContent = z.infer; @@ -158,7 +158,7 @@ type NormalizedAgentContent = result: 'approved' | 'denied'; mode?: string; allowedTools?: string[]; - decision?: 'approved' | 'approved_for_session' | 'denied' | 'abort'; + decision?: 'approved' | 'approved_for_session' | 'approved_execpolicy_amendment' | 'denied' | 'abort'; }; } | { type: 'summary', @@ -422,4 +422,4 @@ export function normalizeRawMessage(id: string, localId: string | null, createdA } } return null; -} \ No newline at end of file +} diff --git a/sources/text/_default.ts b/sources/text/_default.ts index 35edb098..95323efe 100644 --- a/sources/text/_default.ts +++ b/sources/text/_default.ts @@ -723,6 +723,7 @@ export const en = { codex: { // Codex permission dialog buttons permissions: { + yesAlwaysAllowCommand: 'Yes, always allow globally', yesForSession: "Yes, and don't ask for a session", stopAndExplain: 'Stop, and explain what to do', } diff --git a/sources/text/translations/ca.ts b/sources/text/translations/ca.ts index 5473516b..f29ea05f 100644 --- a/sources/text/translations/ca.ts +++ b/sources/text/translations/ca.ts @@ -723,6 +723,7 @@ export const ca: TranslationStructure = { codex: { // Codex permission dialog buttons permissions: { + yesAlwaysAllowCommand: 'Sí, permet globalment', yesForSession: 'Sí, i no preguntar per aquesta sessió', stopAndExplain: 'Atura, i explica què fer', } diff --git a/sources/text/translations/es.ts b/sources/text/translations/es.ts index 5bb5309e..da76c95e 100644 --- a/sources/text/translations/es.ts +++ b/sources/text/translations/es.ts @@ -723,6 +723,7 @@ export const es: TranslationStructure = { codex: { // Codex permission dialog buttons permissions: { + yesAlwaysAllowCommand: 'Sí, permitir globalmente', yesForSession: 'Sí, y no preguntar por esta sesión', stopAndExplain: 'Detener, y explicar qué hacer', } diff --git a/sources/text/translations/pl.ts b/sources/text/translations/pl.ts index 68a5778f..93dc777f 100644 --- a/sources/text/translations/pl.ts +++ b/sources/text/translations/pl.ts @@ -733,6 +733,7 @@ export const pl: TranslationStructure = { codex: { // Codex permission dialog buttons permissions: { + yesAlwaysAllowCommand: 'Tak, zezwól globalnie', yesForSession: 'Tak, i nie pytaj dla tej sesji', stopAndExplain: 'Zatrzymaj i wyjaśnij, co zrobić', } diff --git a/sources/text/translations/pt.ts b/sources/text/translations/pt.ts index 9a3d7ec2..75d514ea 100644 --- a/sources/text/translations/pt.ts +++ b/sources/text/translations/pt.ts @@ -723,6 +723,7 @@ export const pt: TranslationStructure = { codex: { // Codex permission dialog buttons permissions: { + yesAlwaysAllowCommand: 'Sim, permitir globalmente', yesForSession: 'Sim, e não perguntar para esta sessão', stopAndExplain: 'Parar, e explicar o que fazer', } diff --git a/sources/text/translations/ru.ts b/sources/text/translations/ru.ts index 0bfdf76d..c1947a67 100644 --- a/sources/text/translations/ru.ts +++ b/sources/text/translations/ru.ts @@ -721,6 +721,7 @@ export const ru: TranslationStructure = { codex: { // Codex permission dialog buttons permissions: { + yesAlwaysAllowCommand: 'Да, разрешить глобально', yesForSession: 'Да, и не спрашивать для этой сессии', stopAndExplain: 'Остановить и объяснить, что делать', } diff --git a/sources/text/translations/zh-Hans.ts b/sources/text/translations/zh-Hans.ts index 3307e34f..53b90ece 100644 --- a/sources/text/translations/zh-Hans.ts +++ b/sources/text/translations/zh-Hans.ts @@ -725,6 +725,7 @@ export const zhHans: TranslationStructure = { codex: { // Codex permission dialog buttons permissions: { + yesAlwaysAllowCommand: '是,全局永久允许', yesForSession: '是,并且本次会话不再询问', stopAndExplain: '停止,并说明该做什么', }