Files
orca/src/shared/agent-chat-permission-mode.ts
T
Brennan Benson f0ccfcc0d3 fix(native-chat): seed the chat permission default from Claude's and Codex's own arguments (#28100)
* fix(native-chat): seed the chat permission default from the Yolo/Manual toggle as it shows

The one-time copy into Settings -> Chat -> Permissions gave Full access only
when every agent's arguments were exactly the bypass flag, so a profile the
toggle shows as Yolo (agents set differently, or an extra flag) got Ask.
Follow the toggle: Manual seeds Ask, anything it shows as Yolo seeds Full access.

* fix(native-chat): seed the chat permission default from Claude's and Codex's own arguments

The Yolo/Manual toggle has no saved value: it summarises every agent's arguments
and shows Yolo for any mix, including after Manual when another agent keeps custom
arguments. Seeding from it would grant Full access to people who chose Manual.
Read what Claude and Codex chats checked before they had modes instead: Full
access when both launch with their bypass flag, otherwise Ask.
2026-10-12 23:47:37 -07:00

122 lines
4.5 KiB
TypeScript

import type { GlobalSettings } from './global-settings-types'
import { resolvedTuiAgentArgsBypassPermissions } from './tui-agent-launch-defaults'
/** A chat's own permission choice. Saved in chat records and settings: never rename a value. */
export type AgentChatPermissionMode = 'ask' | 'accept-edits' | 'auto' | 'full-access'
/** Picker order: least to most access. */
export const AGENT_CHAT_PERMISSION_MODES = [
'ask',
'accept-edits',
'auto',
'full-access'
] as const satisfies readonly AgentChatPermissionMode[]
/** The chat option a mode is saved and written under. */
export const AGENT_CHAT_PERMISSION_MODE_OPTION_ID = 'permissionMode'
export function isAgentChatPermissionMode(value: unknown): value is AgentChatPermissionMode {
return AGENT_CHAT_PERMISSION_MODES.some((mode) => mode === value)
}
/** The modes one agent's chats offer. */
export type AgentChatPermissionModeSupport = {
modes: readonly AgentChatPermissionMode[]
/** `auto` runs only on a model the agent's listing says supports it. */
autoNeedsModelSupport?: true
}
const CLAUDE_CHAT_PERMISSION_MODES: AgentChatPermissionModeSupport = {
modes: AGENT_CHAT_PERMISSION_MODES,
autoNeedsModelSupport: true
}
const CODEX_CHAT_PERMISSION_MODES: AgentChatPermissionModeSupport = {
modes: ['ask', 'auto', 'full-access']
}
/** The one table of which agents' chats offer which modes; null keeps following Agent Permissions. */
export function agentChatPermissionModes(agent: string): AgentChatPermissionModeSupport | null {
return agent === 'claude'
? CLAUDE_CHAT_PERMISSION_MODES
: agent === 'codex'
? CODEX_CHAT_PERMISSION_MODES
: null
}
/** The saved choice if this agent offers it; anything else asks, never grants more access. */
export function savedChatPermissionMode(
support: AgentChatPermissionModeSupport,
intent: unknown
): AgentChatPermissionMode {
return support.modes.find((mode) => mode === intent) ?? 'ask'
}
/** The one rule for whether the chat's model runs `auto`: its listed row, or the listed CLI default
* when it names none. The host launches with it and the menu offers with it. */
export function chatModelSupportsAutoMode(
models: readonly { id: string; isDefault?: boolean; supportsAutoMode?: boolean }[],
model: string | null | undefined
): boolean {
const row = models.find((entry) =>
model === undefined || model === null || model === 'default'
? entry.isDefault === true
: entry.id === model
)
return row?.supportsAutoMode === true
}
/** Whether the mode a chat runs depends on its model's listed `auto` support. */
export function chatPermissionModeNeedsModelSupport(
support: AgentChatPermissionModeSupport,
intent: unknown
): boolean {
return (
support.autoNeedsModelSupport === true && savedChatPermissionMode(support, intent) === 'auto'
)
}
/** Saved choice, with `auto` on a model not listed for it read as Ask; never written back. */
export function effectiveChatPermissionMode(
support: AgentChatPermissionModeSupport,
intent: unknown,
autoSupported: boolean
): AgentChatPermissionMode {
const mode = savedChatPermissionMode(support, intent)
return mode === 'auto' && support.autoNeedsModelSupport && !autoSupported ? 'ask' : mode
}
export function offeredChatPermissionModes(
support: AgentChatPermissionModeSupport,
autoSupported: boolean
): readonly AgentChatPermissionMode[] {
return support.autoNeedsModelSupport && !autoSupported
? support.modes.filter((mode) => mode !== 'auto')
: support.modes
}
/** The host's new-chat default as an agent's chat starts with it; null for an agent with none. */
export function newChatPermissionMode(
agent: string,
setting: unknown
): AgentChatPermissionMode | null {
const support = agentChatPermissionModes(agent)
return support ? savedChatPermissionMode(support, setting) : null
}
/** Seeded once when absent, then stored; an unknown value asks. */
export function loadedChatPermissionSetting(
saved: unknown,
terminal: Partial<Pick<GlobalSettings, 'agentDefaultArgs' | 'terminalWindowsShell'>>,
platform: NodeJS.Platform
): AgentChatPermissionMode {
if (saved !== undefined) {
return isAgentChatPermissionMode(saved) ? saved : 'ask'
}
// What chats ran before they had modes: Claude's and Codex's own arguments, not the all-agent
// Yolo/Manual summary, which shows custom arguments on any agent as Yolo even after Manual.
const bypasses = (agent: 'claude' | 'codex'): boolean =>
resolvedTuiAgentArgsBypassPermissions(agent, terminal, platform)
return bypasses('claude') && bypasses('codex') ? 'full-access' : 'ask'
}