feat(agents): add guarded Auto permission presets and per-agent controls

This commit is contained in:
Neil
2026-09-07 15:03:26 -07:00
parent 2265fce591
commit 793966d4ed
32 changed files with 911 additions and 316 deletions
@@ -0,0 +1,49 @@
# Agent permission presets
Orca's permission presets map one product choice onto agent-specific CLI arguments or
environment variables. The word **Auto** does not describe one uniform security policy:
depending on the agent, it can mean classifier-reviewed actions, edits-only approval,
risk-threshold approval, or a sandboxed approval reviewer. Keep the exact behavior visible in
the UI and do not imply that every agent provides the same safety boundary.
This matrix records vendor-documented presets checked on September 6, 2026. A local help
check identifies the syntax accepted by the installed CLI; the linked vendor source defines
its semantics.
| Agent | Auto preset | Verified behavior | Evidence |
| --- | --- | --- | --- |
| Claude Code | `--permission-mode auto` | Uses Claude's auto-mode permission classifier. The installed Claude Code 2.1.263 help lists `auto` separately from `acceptEdits`, `manual`, and `bypassPermissions`. | [Claude Code permissions](https://code.claude.com/docs/en/permissions) and `claude --help` |
| Codex | `--approve-for-me` | Routes approval requests through automatic review while retaining the workspace-write sandbox. The installed Codex CLI 0.153.4 help documents this behavior. | [Codex security](https://developers.openai.com/codex/security/) and [Codex CLI argument tests](https://github.com/openai/codex/blob/main/codex-rs/cli/src/main.rs) |
| Gemini CLI | `--approval-mode auto_edit` | Auto-approves edit tools while continuing to prompt for other tools. The installed Gemini CLI 0.58.0 help lists `default`, `auto_edit`, `yolo`, and `plan`. | [Gemini CLI repository](https://github.com/google-gemini/gemini-cli) and `gemini --help` |
| goose | `GOOSE_MODE=smart_approve` | Automatically approves low-risk actions and asks for approval on higher-risk actions. | [goose permission modes](https://github.com/block/goose/blob/main/documentation/docs/guides/managing-tools/goose-permissions.md) and [mode values](https://github.com/block/goose/blob/main/crates/goose-provider-types/src/goose_mode.rs) |
| Qwen Code | `--approval-mode auto` | Uses an LLM classifier for shell commands, network calls, and out-of-workspace edits; risky or uncertain actions still prompt. | [Qwen Code approval modes](https://github.com/QwenLM/qwen-code/blob/main/docs/users/features/approval-mode.md) |
| Devin CLI | `--permission-mode smart` | Auto-approves workspace edits and uses a fast model to judge other actions, falling back to a prompt when unsafe, uncertain, or unavailable. Smart is rolling out gradually and may be unavailable for some accounts. | [Devin permissions](https://docs.devin.ai/cli/reference/permissions.md) and [commands and flags](https://docs.devin.ai/cli/reference/commands.md) |
| Droid | `--auto medium` | Auto-approves edits, low-risk tools, and reversible workspace changes such as installs, builds, local commits, moves, and copies. Higher-risk actions still prompt. | [Droid autonomy levels](https://docs.factory.ai/autonomy-and-safety/auto-run.md) and `droid --help` from Droid 0.205.0 |
Gemini's `auto_edit` is deliberately narrower than classifier-based Auto modes. Droid also
offers `--auto low` and `--auto high`; Medium is the balanced candidate because Low covers
edits and low-risk tools, while High permits high-risk actions unless a separate safety check
intervenes. Devin's `accept-edits` is a narrower fallback if Smart is unavailable, but it is
not behaviorally equivalent to Smart.
Claude Agent Teams forwards its arguments to the Claude binary through `orca claude-teams`
and uses the same `--permission-mode auto` preset (see `src/cli/handlers/core.ts`).
The global Auto preset uses Manual for harnesses without a verified mapping. Per-agent
controls omit Auto for those harnesses. Custom argument and environment profiles are preserved.
Settings apply to future launches, including folder workspaces and SSH/WSL execution hosts.
The installed CLI and account must support the selected mode; Orca never retries with bypass
when a guarded mode is rejected. Existing default permission preferences are unchanged.
## No verified Auto preset yet
The remaining agents in `YOLO_TUI_AGENT_ARGS` have no intermediate preset verified for this
work: OpenClaude, Antigravity, Aider, Amp, Kiro, Crush, Autohand, Cline,
Command Code, Continue, Cursor, Kimi, Mistral Vibe, Rovo, Hermes, Copilot, Grok, Ante, and Trae.
This means only that this review did not find sufficiently clear vendor evidence for a preset;
it is not a claim that the agent lacks intermediate permission controls. Amp's `smart` agent
mode, for example, controls its model, system prompt, and tool selection rather than command
approval policy, so it must not be treated as an Auto permission preset.
Re-check vendor documentation and current `--help` output before adding any of these agents.
CLI flags and rollout availability change independently of Orca releases.
@@ -247,3 +247,17 @@ predicate. It is unobservable today — the host publishes neither field for a c
all, so a mirror has nothing to take either way. If the capability-gated publish this section
anticipates ever lands, narrow them the same way rather than by placement kind: a mirror should
take a failure it cannot otherwise see, and only the hosting client should refuse it.
## Auto permission presets on paired settings
`settings.agent-permission-auto.v1` identifies clients that understand Auto launch
argument and environment presets. New hosts project those presets as manual in every
settings response to older clients: their onboarding otherwise treats Auto as a custom
mixed profile and can apply bypass to the remaining harnesses. When the host contains
an Auto preset, unchanged legacy permission-map roundtrips are ignored, and changed
permission maps are rejected with a client-upgrade message. Unrelated settings remain
editable. Capable clients receive and edit the actual presets.
New clients can store Auto presets on older hosts because their existing argument and
environment normalizers preserve these strings. Such hosts cannot protect the profile
from a second, older client changing settings; that protection requires the host upgrade.
@@ -18,6 +18,7 @@ import { launchOrcaApp } from './launch'
import { addEnvironmentFromPairingCode } from './environments'
import { RuntimeClientError } from './types'
import {
AGENT_PERMISSION_AUTO_RUNTIME_CAPABILITY,
AGENT_SESSION_BOUNDARY_RUNTIME_CAPABILITY,
AUTOMATION_OWNER_FENCING_RUNTIME_CAPABILITY,
MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION,
@@ -72,6 +73,7 @@ describe('CLI remote WebSocket transport', () => {
clientCapabilities: [
SESSION_TAB_CLOSE_INTENT_RUNTIME_CAPABILITY,
SESSION_TABS_AUTHORITATIVE_INVENTORY_RUNTIME_CAPABILITY,
AGENT_PERMISSION_AUTO_RUNTIME_CAPABILITY,
AGENT_SESSION_BOUNDARY_RUNTIME_CAPABILITY,
SKILL_INSTALL_RESULT_V2_CAPABILITY,
WORKTREE_GITHUB_PR_SUPPRESSION_RUNTIME_CAPABILITY,
@@ -0,0 +1,110 @@
import { describe, expect, it, vi } from 'vitest'
import { AGENT_PERMISSION_AUTO_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version'
import {
applyAgentPermissionMode,
AUTO_TUI_AGENT_ARGS,
AUTO_TUI_AGENT_ENV
} from '../../../../shared/tui-agent-permissions'
import { remoteRuntimeClientCapabilities } from '../../../../shared/remote-runtime-client-capabilities'
import type { TuiAgent } from '../../../../shared/tui-agent'
import type { OrcaRuntimeService } from '../../orca-runtime'
import { RpcDispatcher } from '../dispatcher'
import { CLIENT_UI_METHODS } from './client-ui'
function setup(mode: 'auto' | 'manual' = 'auto') {
const settings = { ...applyAgentPermissionMode({ mode }), compactWorktreeCards: false }
const runtime = {
getRuntimeId: () => 'test-runtime',
getClientSettings: vi.fn(() => settings),
updateClientSettings: vi.fn(async (updates) => Object.assign(settings, updates)),
updateClientPRBotAuthorOverride: vi.fn(() => settings)
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: CLIENT_UI_METHODS })
const request = (method: string, params?: unknown, capabilities: string[] = []) =>
dispatcher.dispatch(
{ id: '1', authToken: 'token', method, params },
{ clientKind: 'runtime', clientCapabilities: capabilities }
)
return { settings, runtime, request }
}
function resultSettings(response: Awaited<ReturnType<ReturnType<typeof setup>['request']>>) {
expect(response.ok).toBe(true)
if (!response.ok) {
throw new Error(response.error.message)
}
return (response.result as { settings: ReturnType<typeof setup>['settings'] }).settings
}
describe('Auto permissions across remote versions', () => {
it('advertises Auto support on remote request transports', () => {
expect(remoteRuntimeClientCapabilities()).toContain(AGENT_PERMISSION_AUTO_RUNTIME_CAPABILITY)
})
it('projects Auto as manual to old clients without changing host settings', async () => {
const { settings, request } = setup()
const before = structuredClone(settings)
const projected = resultSettings(await request('settings.get'))
for (const agent of Object.keys(AUTO_TUI_AGENT_ARGS)) {
expect(projected.agentDefaultArgs[agent as TuiAgent]).toBe('')
}
for (const agent of Object.keys(AUTO_TUI_AGENT_ENV)) {
expect(projected.agentDefaultEnv[agent as TuiAgent]).toEqual({})
}
expect(settings).toEqual(before)
})
it('preserves Auto when an old client echoes its projection with an unrelated edit', async () => {
const { settings, runtime, request } = setup()
const before = structuredClone(settings)
const projected = resultSettings(await request('settings.get'))
const updated = resultSettings(
await request('settings.update', { ...projected, compactWorktreeCards: true })
)
expect(runtime.updateClientSettings).toHaveBeenCalledWith({ compactWorktreeCards: true })
expect(settings.agentDefaultArgs).toEqual(before.agentDefaultArgs)
expect(settings.agentDefaultEnv).toEqual(before.agentDefaultEnv)
expect(updated).toEqual({ ...projected, compactWorktreeCards: true })
})
it('rejects old-client permission widening, including unsupported Auto fallback agents', async () => {
const { settings, runtime, request } = setup()
const before = structuredClone(settings)
const projected = resultSettings(await request('settings.get'))
const response = await request('settings.update', {
...applyAgentPermissionMode({ ...projected, mode: 'yolo' })
})
expect(response).toMatchObject({
ok: false,
error: { message: expect.stringContaining('Update this Orca client') }
})
expect(runtime.updateClientSettings).not.toHaveBeenCalled()
expect(settings).toEqual(before)
})
it('allows a capable client to view and change Auto', async () => {
const { settings, request } = setup()
const capabilities = [AGENT_PERMISSION_AUTO_RUNTIME_CAPABILITY]
expect(resultSettings(await request('settings.get', undefined, capabilities))).toEqual(settings)
const manual = applyAgentPermissionMode({ mode: 'manual' })
expect(resultSettings(await request('settings.update', manual, capabilities))).toMatchObject(
manual
)
})
it('keeps permission edits available to legacy clients on hosts without Auto', async () => {
const { request } = setup('manual')
const bypass = applyAgentPermissionMode({ mode: 'yolo' })
expect(resultSettings(await request('settings.update', bypass))).toMatchObject(bypass)
})
it('projects settings published by unrelated bot-author updates too', async () => {
const { request } = setup()
const projected = resultSettings(await request('settings.get'))
expect(
resultSettings(
await request('settings.updatePRBotAuthorOverride', { author: 'bot', isBot: true })
)
).toEqual(projected)
})
})
@@ -0,0 +1,68 @@
import { isDeepStrictEqual } from 'node:util'
import { AGENT_PERMISSION_AUTO_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version'
import { AUTO_TUI_AGENT_ARGS, AUTO_TUI_AGENT_ENV } from '../../../../shared/tui-agent-permissions'
import type { TuiAgent } from '../../../../shared/tui-agent'
import type {
RuntimeClientSettings,
RuntimeClientSettingsUpdate
} from '../../runtime-client-settings'
import type { RpcContext } from '../core'
type ClientContext = Pick<RpcContext, 'clientCapabilities' | 'clientKind'>
export function projectClientPermissionSettings(
settings: RuntimeClientSettings,
context: ClientContext
): RuntimeClientSettings {
if (
context.clientKind === undefined ||
context.clientCapabilities?.includes(AGENT_PERMISSION_AUTO_RUNTIME_CAPABILITY)
) {
return settings
}
let projected = settings
for (const [agent, preset] of Object.entries(AUTO_TUI_AGENT_ARGS)) {
if (settings.agentDefaultArgs?.[agent as TuiAgent]?.trim() === preset) {
projected = {
...projected,
agentDefaultArgs: { ...projected.agentDefaultArgs, [agent]: '' }
}
}
}
for (const [agent, preset] of Object.entries(AUTO_TUI_AGENT_ENV)) {
if (isDeepStrictEqual(settings.agentDefaultEnv?.[agent as TuiAgent], preset)) {
projected = {
...projected,
agentDefaultEnv: { ...projected.agentDefaultEnv, [agent]: {} }
}
}
}
return projected
}
export function protectClientPermissionUpdate(
updates: RuntimeClientSettingsUpdate,
context: RpcContext
): RuntimeClientSettingsUpdate {
if (
context.clientKind === undefined ||
context.clientCapabilities?.includes(AGENT_PERMISSION_AUTO_RUNTIME_CAPABILITY) ||
(updates.agentDefaultArgs === undefined && updates.agentDefaultEnv === undefined)
) {
return updates
}
const settings = context.runtime.getClientSettings()
const projected = projectClientPermissionSettings(settings, context)
if (projected === settings) {
return updates
}
// Legacy onboarding turns an unrecognized permission profile into bypass.
for (const field of ['agentDefaultArgs', 'agentDefaultEnv'] as const) {
if (updates[field] !== undefined && !isDeepStrictEqual(updates[field], projected[field])) {
throw new Error('Update this Orca client to change permissions while Auto mode is enabled.')
}
}
const { agentDefaultArgs: _args, agentDefaultEnv: _env, ...remaining } = updates
return remaining
}
+18 -5
View File
@@ -6,6 +6,11 @@ import {
PRBotAuthorOverrideUpdate,
SettingsUpdate
} from './client-settings-schemas'
import {
projectClientPermissionSettings,
protectClientPermissionUpdate
} from './client-permission-projection'
import { FeatureInteractionIdParam, UiUpdate } from './client-ui-schemas'
// Type-only side effect: keeps the schema/PersistedUIState parity assertions in
// the typecheck graph so drift fails the build instead of a paired client.
@@ -16,13 +21,18 @@ export const CLIENT_UI_METHODS: RpcMethod[] = [
defineMethod({
name: 'settings.get',
params: null,
handler: (_params, { runtime }) => ({ settings: runtime.getClientSettings() })
handler: (_params, context) => ({
settings: projectClientPermissionSettings(context.runtime.getClientSettings(), context)
})
}),
defineMethod({
name: 'settings.update',
params: SettingsUpdate,
handler: async (params, { runtime }) => ({
settings: await runtime.updateClientSettings(params)
handler: async (params, context) => ({
settings: projectClientPermissionSettings(
await context.runtime.updateClientSettings(protectClientPermissionUpdate(params, context)),
context
)
})
}),
defineMethod({
@@ -44,8 +54,11 @@ export const CLIENT_UI_METHODS: RpcMethod[] = [
defineMethod({
name: 'settings.updatePRBotAuthorOverride',
params: PRBotAuthorOverrideUpdate,
handler: (params, { runtime }) => ({
settings: runtime.updateClientPRBotAuthorOverride(params)
handler: (params, context) => ({
settings: projectClientPermissionSettings(
context.runtime.updateClientPRBotAuthorOverride(params),
context
)
})
}),
defineMethod({
@@ -13,17 +13,17 @@ describe('AgentStep', () => {
onSelect={vi.fn()}
detectedSet={new Set([AGENT_CATALOG[0].id])}
isDetecting={false}
yoloPermissions
onYoloPermissionsChange={vi.fn()}
permissionMode="auto"
onPermissionModeChange={vi.fn()}
/>
</TooltipProvider>
)
expect(html).toContain(`Show ${AGENT_CATALOG.length - 1} more agents→`)
expect(html).toContain('data-agent-grid-scroll')
expect(html).toContain('data-slot="checkbox"')
expect(html).toContain('Yolo / Dangerously skip permissions')
expect(html).not.toContain('role="radiogroup"')
expect(html).toContain('>Auto</button>')
expect(html).toContain('agents without Auto use Manual')
expect(html).toContain('role="radiogroup"')
})
it('labels the fallback agents summary as hide when expanded', () => {
@@ -34,8 +34,8 @@ describe('AgentStep', () => {
onSelect={vi.fn()}
detectedSet={new Set([AGENT_CATALOG[0].id])}
isDetecting={false}
yoloPermissions
onYoloPermissionsChange={vi.fn()}
permissionMode="auto"
onPermissionModeChange={vi.fn()}
/>
</TooltipProvider>
)
@@ -1,10 +1,10 @@
import { useLayoutEffect, useRef, useState } from 'react'
import { Check, ExternalLink, Info } from 'lucide-react'
import { Check, ExternalLink } from 'lucide-react'
import { getAgentCatalog, AgentIcon, type AgentCatalogEntry } from '@/lib/agent-catalog'
import { cn } from '@/lib/utils'
import { Checkbox } from '@/components/ui/checkbox'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { AgentPermissionsSetting } from '../settings/AgentPermissionsSetting'
import type { AgentPermissionMode } from '../../../../shared/tui-agent-permissions'
import type { TuiAgent } from '../../../../shared/tui-agent'
import { translate } from '@/i18n/i18n'
@@ -18,8 +18,8 @@ type AgentStepProps = {
onSelect: (agent: TuiAgent, fromCollapsedSection: boolean) => void
detectedSet: Set<TuiAgent>
isDetecting: boolean
yoloPermissions?: boolean
onYoloPermissionsChange?: (enabled: boolean) => void
permissionMode?: AgentPermissionMode
onPermissionModeChange?: (mode: Exclude<AgentPermissionMode, 'mixed'>) => void
}
function useAgentGridScrollMaxHeight(
@@ -64,8 +64,8 @@ export function AgentStep({
onSelect,
detectedSet,
isDetecting,
yoloPermissions = true,
onYoloPermissionsChange
permissionMode,
onPermissionModeChange
}: AgentStepProps) {
const agentCatalog = getAgentCatalog()
const detected = agentCatalog.filter((agent) => detectedSet.has(agent.id))
@@ -190,65 +190,15 @@ export function AgentStep({
</div>
</div>
</section>
<YoloPermissionsControl
yoloPermissions={yoloPermissions}
onYoloPermissionsChange={onYoloPermissionsChange}
/>
{permissionMode && onPermissionModeChange && (
<div className="mt-auto shrink-0 rounded-lg border border-border bg-muted/25 px-4 py-3">
<AgentPermissionsSetting mode={permissionMode} onChange={onPermissionModeChange} />
</div>
)}
</div>
)
}
function YoloPermissionsControl({
yoloPermissions,
onYoloPermissionsChange
}: {
yoloPermissions: boolean
onYoloPermissionsChange?: (enabled: boolean) => void
}): React.JSX.Element {
return (
<label className="mt-auto flex shrink-0 cursor-pointer items-center justify-between gap-4 rounded-lg border border-border bg-muted/25 px-4 py-3 transition-colors hover:bg-muted/40">
<span className="flex min-w-0 items-center gap-3">
<Checkbox
checked={yoloPermissions}
onCheckedChange={(checked) => onYoloPermissionsChange?.(checked === true)}
className="border-border bg-card data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground"
aria-label={translate(
'auto.components.onboarding.AgentStep.yoloPermissionsLabel',
'Yolo / Dangerously skip permissions'
)}
/>
<span className="min-w-0 text-sm font-medium text-foreground">
{translate(
'auto.components.onboarding.AgentStep.yoloPermissionsLabel',
'Yolo / Dangerously skip permissions'
)}
</span>
</span>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label={translate(
'auto.components.onboarding.AgentStep.yoloPermissionsInfo',
'Agent permission info'
)}
onPointerDown={(event) => event.preventDefault()}
className="grid size-6 shrink-0 place-items-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
<Info className="size-3.5" />
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={6} style={{ zIndex: 120 }}>
{translate(
'auto.components.onboarding.AgentStep.yoloPermissionsTooltip',
'Skip permission checks for agents for less interruptions'
)}
</TooltipContent>
</Tooltip>
</label>
)
}
function SectionHeader({
label,
count,
@@ -314,8 +314,8 @@ export default function OnboardingFlow({
onSelect={flow.setSelectedAgent}
detectedSet={flow.detectedSet}
isDetecting={flow.isDetectingAgents}
yoloPermissions={flow.yoloPermissions}
onYoloPermissionsChange={flow.setYoloPermissions}
permissionMode={flow.permissionMode}
onPermissionModeChange={flow.setPermissionMode}
/>
)}
{currentStep.id === 'theme' && (
@@ -3,7 +3,7 @@
import { createElement, useEffect, act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { getDefaultOnboardingState } from '../../../../shared/constants'
import { getDefaultOnboardingState, getDefaultSettings } from '../../../../shared/constants'
import type { OnboardingState } from '../../../../shared/onboarding-state-types'
const trackMock = vi.hoisted(() => vi.fn())
@@ -16,6 +16,7 @@ import {
buildCompletedOnboardingNotificationSettings,
buildOnboardingDismissedPayload,
useCloseWith,
usePersistCurrentStep,
type DismissedExtras,
trackOnboardingDismissed
} from './use-onboarding-flow-persistence'
@@ -90,6 +91,48 @@ describe('onboarding flow persistence', () => {
vi.useRealTimers()
})
it.each([null, 'auto'] as const)(
'preserves permissions unless a preset is explicitly selected (%s)',
async (selection) => {
const settings = getDefaultSettings('/tmp')
settings.agentDefaultArgs = {
...settings.agentDefaultArgs,
claude: '',
codex: '--model custom'
}
const updateSettings = vi.fn()
let persist: (() => Promise<{ ok: boolean }>) | undefined
function Probe(): null {
persist = usePersistCurrentStep({
currentStepId: 'agent',
selectedAgent: 'claude',
permissionModeSelection: selection,
theme: settings.theme,
settings,
updateSettings,
onboardingChecklist: getDefaultOnboardingState().checklist,
onOnboardingChange: vi.fn(),
setError: vi.fn()
})
return null
}
container = document.createElement('div')
root = createRoot(container)
act(() => root?.render(createElement(Probe)))
await act(async () => {
await persist?.()
})
const update = updateSettings.mock.calls[0][0]
if (selection === null) {
expect(update).toEqual({ defaultTuiAgent: 'claude' })
} else {
expect(update.agentDefaultArgs.claude).toBe('--permission-mode auto')
expect(update.agentDefaultArgs.codex).toBe('--model custom')
expect(update.agentDefaultArgs.aider).toBe('')
}
}
)
it('builds dismissed telemetry with the triggering advance path', () => {
expect(
buildOnboardingDismissedPayload(3, {
@@ -6,7 +6,10 @@ import type { EventProps } from '../../../../shared/telemetry-events'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import type { OnboardingState } from '../../../../shared/onboarding-state-types'
import type { TuiAgent } from '../../../../shared/tui-agent'
import { applyAgentPermissionMode } from '../../../../shared/tui-agent-permissions'
import {
applyAgentPermissionMode,
type AgentPermissionMode
} from '../../../../shared/tui-agent-permissions'
import type { StepId, StepNumber } from './use-onboarding-flow-types'
export async function persistStep(
@@ -132,7 +135,7 @@ export function useCloseWith({ onOnboardingChange, startTimeRef, setError }: Clo
type PersistCurrentStepDeps = {
currentStepId: StepId
selectedAgent: TuiAgent | null
yoloPermissions: boolean
permissionModeSelection: Exclude<AgentPermissionMode, 'mixed'> | null
theme: GlobalSettings['theme']
settings: GlobalSettings | null
updateSettings: (updates: Partial<GlobalSettings>) => Promise<void> | void
@@ -148,7 +151,7 @@ export type PersistCurrentStepResult = {
export function usePersistCurrentStep({
currentStepId,
selectedAgent,
yoloPermissions,
permissionModeSelection,
theme,
settings,
updateSettings,
@@ -165,11 +168,13 @@ export function usePersistCurrentStep({
const defaultTuiAgent = selectedAgentOrBlank(selectedAgent)
await updateSettings({
defaultTuiAgent,
...applyAgentPermissionMode({
mode: yoloPermissions ? 'yolo' : 'manual',
agentDefaultArgs: settings.agentDefaultArgs,
agentDefaultEnv: settings.agentDefaultEnv
})
...(permissionModeSelection === null
? {}
: applyAgentPermissionMode({
mode: permissionModeSelection,
agentDefaultArgs: settings.agentDefaultArgs,
agentDefaultEnv: settings.agentDefaultEnv
}))
})
const choseAgent = defaultTuiAgent !== 'blank'
const wasAlreadyChosen = onboardingChecklist.choseAgent
@@ -226,7 +231,7 @@ export function usePersistCurrentStep({
settings,
theme,
updateSettings,
yoloPermissions,
permissionModeSelection,
setError
])
}
@@ -12,7 +12,10 @@ import { STEPS } from './use-onboarding-flow-types'
import { persistStep, useCloseWith, usePersistCurrentStep } from './use-onboarding-flow-persistence'
import { resolveOnboardingSettingsHydration } from './onboarding-settings-hydration'
import { translate } from '@/i18n/i18n'
import { resolveAgentPermissionModeSummary } from '../../../../shared/tui-agent-permissions'
import {
resolveAgentPermissionModeSummary,
type AgentPermissionMode
} from '../../../../shared/tui-agent-permissions'
import { isWindowsUserAgent } from '@/components/terminal-pane/pane-helpers'
import {
isSkippedStepIndex,
@@ -66,12 +69,16 @@ export function useOnboardingFlow(
? settings.defaultTuiAgent
: null
)
const [yoloPermissions, setYoloPermissions] = useState(
const [permissionModeSelection, setPermissionModeSelection] = useState<Exclude<
AgentPermissionMode,
'mixed'
> | null>(null)
const permissionMode =
permissionModeSelection ??
resolveAgentPermissionModeSummary({
agentDefaultArgs: settings?.agentDefaultArgs,
agentDefaultEnv: settings?.agentDefaultEnv
}) !== 'manual'
)
})
// Why: hydrate theme from saved settings so users who already chose one see it preselected.
const [theme, setTheme] = useState<GlobalSettings['theme']>(settings?.theme ?? 'dark')
const [busyLabel, setBusyLabel] = useState<string | null>(null)
@@ -80,7 +87,6 @@ export function useOnboardingFlow(
// Why: settings hydrate async after the lazy initializers run; re-sync once before commit unless the user edited the field.
const themeInteractedRef = useRef(false)
const agentInteractedRef = useRef(false)
const yoloPermissionsInteractedRef = useRef(false)
const [settingsHydrated, setSettingsHydrated] = useState(settings != null)
const settingsHydration = resolveOnboardingSettingsHydration({
settings,
@@ -99,17 +105,6 @@ export function useOnboardingFlow(
setSelectedAgent(settingsHydration.selectedAgent)
}
}
if (settings && !yoloPermissionsInteractedRef.current) {
const nextYoloPermissions =
resolveAgentPermissionModeSummary({
agentDefaultArgs: settings.agentDefaultArgs,
agentDefaultEnv: settings.agentDefaultEnv
}) !== 'manual'
if (nextYoloPermissions !== yoloPermissions) {
setYoloPermissions(nextYoloPermissions)
}
}
// Why: track interaction so async settings hydration doesn't overwrite a value the user chose.
const setThemeInteractive = useCallback((value: GlobalSettings['theme']) => {
themeInteractedRef.current = true
@@ -152,11 +147,6 @@ export function useOnboardingFlow(
},
[]
)
const setYoloPermissionsInteractive = useCallback((enabled: boolean) => {
yoloPermissionsInteractedRef.current = true
setYoloPermissions(enabled)
}, [])
const detectedSet = useMemo(() => new Set(detectedAgentIds ?? []), [detectedAgentIds])
const currentStep = STEPS[stepIndex]
// Why: the stepper shows only steps the user will land on; skipped optional steps are dropped, not rendered as dead dots.
@@ -278,7 +268,7 @@ export function useOnboardingFlow(
const persistCurrentStep = usePersistCurrentStep({
currentStepId: currentStep.id,
selectedAgent,
yoloPermissions,
permissionModeSelection,
theme,
settings,
updateSettings,
@@ -318,8 +308,8 @@ export function useOnboardingFlow(
currentStep,
selectedAgent,
setSelectedAgent: setSelectedAgentInteractive,
yoloPermissions,
setYoloPermissions: setYoloPermissionsInteractive,
permissionMode,
setPermissionMode: setPermissionModeSelection,
theme,
setTheme: setThemeInteractive,
busyLabel,
@@ -1,5 +1,7 @@
import { useState } from 'react'
import { Check, ChevronDown, ExternalLink } from 'lucide-react'
import { AgentPermissionModeControl } from './AgentPermissionsSetting'
import type { AgentPermissionMode } from '../../../../shared/tui-agent-permissions'
import type { TuiAgent } from '../../../../shared/tui-agent'
import { AgentIcon } from '@/lib/agent-catalog'
import { cn } from '@/lib/utils'
@@ -68,6 +70,8 @@ export type AgentCatalogRowProps = {
cmdOverride: string | undefined
argsOverride: string
envOverride: Record<string, string>
permissionMode?: AgentPermissionMode
onSetPermissionMode?: (mode: Exclude<AgentPermissionMode, 'mixed'>) => void
onSetDefault: () => void
onSetEnabled: (enabled: boolean) => void
onSaveOverride: (value: string) => void
@@ -89,6 +93,8 @@ export function AgentCatalogRow({
cmdOverride,
argsOverride,
envOverride,
permissionMode,
onSetPermissionMode,
onSetDefault,
onSetEnabled,
onSaveOverride,
@@ -98,9 +104,7 @@ export function AgentCatalogRow({
}: AgentCatalogRowProps): React.JSX.Element {
const envSummary = stringifyAgentDefaultEnvDraft(envOverride)
const defaultEnvSummary = stringifyAgentDefaultEnvDraft(defaultEnv)
const [cmdOpen, setCmdOpen] = useState(
Boolean(cmdOverride) || argsOverride !== defaultArgs || envSummary !== defaultEnvSummary
)
const [cmdOpen, setCmdOpen] = useState(Boolean(cmdOverride) || permissionMode === 'mixed')
return (
<div className={cn('py-3', !isDetected && 'opacity-70')}>
@@ -181,12 +185,12 @@ export function AgentCatalogRow({
aria-label={
cmdOpen
? translate(
'auto.components.settings.AgentsPane.cea7d97be1',
'Collapse command override'
'auto.components.settings.AgentsPane.collapseLaunchSettings',
'Collapse launch settings'
)
: translate(
'auto.components.settings.AgentsPane.dc4a2ffdc0',
'Expand command override'
'auto.components.settings.AgentsPane.expandLaunchSettings',
'Expand launch settings'
)
}
className="size-7 text-muted-foreground hover:text-foreground"
@@ -202,6 +206,31 @@ export function AgentCatalogRow({
{isDetected && cmdOpen && (
<div className="mt-3 pl-10">
{permissionMode && onSetPermissionMode && (
<div className="mb-3 space-y-2">
<div className="flex flex-wrap items-center justify-between gap-2">
<span className="text-xs font-medium">
{translate(
'auto.components.settings.AgentsPane.agentPermissions',
'Agent Permissions'
)}
</span>
<AgentPermissionModeControl
mode={permissionMode}
onChange={onSetPermissionMode}
agent={agentId}
/>
</div>
{permissionMode === 'mixed' && (
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.AgentsPane.agentPermissionsEditCustom',
'Custom configuration. Edit or reset the launch arguments and environment below to use a permission preset.'
)}
</p>
)}
</div>
)}
<AgentCommandOverrideInput
key={cmdOverride ?? defaultCmd}
defaultCmd={defaultCmd}
@@ -101,10 +101,10 @@ export function AgentDefaultArgsInput({
event.currentTarget.blur()
}
}}
placeholder={
defaultArgs ||
translate('auto.components.settings.AgentsPane.6f99bf5dd0', 'No default arguments')
}
placeholder={translate(
'auto.components.settings.AgentsPane.6f99bf5dd0',
'No default arguments'
)}
spellCheck={false}
className="h-7 flex-1 font-mono text-xs"
/>
@@ -175,10 +175,10 @@ export function AgentDefaultEnvInput({
event.currentTarget.blur()
}
}}
placeholder={
defaultEnvText ||
translate('auto.components.settings.AgentsPane.2d133152fa', 'No default environment')
}
placeholder={translate(
'auto.components.settings.AgentsPane.2d133152fa',
'No default environment'
)}
spellCheck={false}
aria-invalid={envDraftTooLarge || undefined}
aria-describedby={envDraftTooLarge ? envDraftErrorId : undefined}
@@ -0,0 +1,36 @@
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it, vi } from 'vitest'
import { AgentPermissionModeControl } from './AgentPermissionsSetting'
describe('agent permission choices', () => {
it('offers Auto only to supported agents', () => {
const supported = renderToStaticMarkup(
<AgentPermissionModeControl agent="claude" mode="auto" onChange={vi.fn()} />
)
const unsupported = renderToStaticMarkup(
<AgentPermissionModeControl agent="aider" mode="manual" onChange={vi.fn()} />
)
expect(supported).toContain('>Auto</button>')
expect(unsupported).not.toContain('>Auto</button>')
expect(unsupported).toContain('>Manual</button>')
expect(unsupported).toContain('>Yolo</button>')
})
it('disables presets while custom launch settings need editing', () => {
const html = renderToStaticMarkup(
<AgentPermissionModeControl agent="claude" mode="mixed" onChange={vi.fn()} />
)
expect(html.match(/aria-disabled="true"/g)).toHaveLength(3)
expect(html).not.toContain('aria-checked="true"')
})
it('passes an explicitly selected global preset through', () => {
const onChange = vi.fn()
const element = AgentPermissionModeControl({ mode: 'mixed', onChange })
element.props.onChange('auto')
expect(onChange).toHaveBeenCalledWith('auto')
expect(element.props.options.every((option: { disabled: boolean }) => !option.disabled)).toBe(
true
)
})
})
@@ -0,0 +1,96 @@
import type { TuiAgent } from '../../../../shared/tui-agent'
import {
supportsTuiAgentAutoPermissionMode,
type AgentPermissionMode
} from '../../../../shared/tui-agent-permissions'
import { translate } from '@/i18n/i18n'
import { SettingsSegmentedControl, SettingsSubsectionHeader } from './SettingsFormControls'
export function AgentPermissionModeControl({
mode,
onChange,
agent
}: {
mode: AgentPermissionMode
onChange: (mode: Exclude<AgentPermissionMode, 'mixed'>) => void
agent?: TuiAgent
}): React.JSX.Element {
const disabled = agent !== undefined && mode === 'mixed'
return (
<SettingsSegmentedControl<AgentPermissionMode>
value={mode}
onChange={(next) => {
if (next !== 'mixed') {
onChange(next)
}
}}
ariaLabel={translate(
'auto.components.settings.AgentsPane.agentPermissions',
'Agent Permissions'
)}
size="sm"
options={[
{
value: 'manual',
label: translate('auto.components.settings.AgentsPane.agentPermissionsManual', 'Manual'),
disabled
},
...(!agent || supportsTuiAgentAutoPermissionMode(agent)
? [
{
value: 'auto' as const,
label: translate(
'auto.components.settings.AgentsPane.agentPermissionsAuto',
'Auto'
),
disabled
}
]
: []),
{
value: 'yolo',
label: translate('auto.components.settings.AgentsPane.agentPermissionsYolo', 'Yolo'),
disabled
}
]}
/>
)
}
export function AgentPermissionsSetting({
mode,
onChange
}: {
mode: AgentPermissionMode
onChange: (mode: Exclude<AgentPermissionMode, 'mixed'>) => void
}): React.JSX.Element {
return (
<section className="space-y-2">
<SettingsSubsectionHeader
title={translate(
'auto.components.settings.AgentsPane.agentPermissions',
'Agent Permissions'
)}
className="flex-wrap"
action={<AgentPermissionModeControl mode={mode} onChange={onChange} />}
/>
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.AgentsPane.agentPermissionsModesDescription',
'Manual asks for approval. Auto uses the agent’s guarded approval mode; agents without Auto use Manual. Yolo skips permission checks.'
)}
</p>
<p className="text-xs text-muted-foreground">
{mode === 'mixed'
? translate(
'auto.components.settings.AgentsPane.agentPermissionsCustom',
'Custom configuration. Choose a preset to update standard modes; custom launch arguments and environment values are preserved.'
)
: translate(
'auto.components.settings.AgentsPane.agentPermissionsPreserveCustom',
'Applies to all agents. Custom launch arguments and environment values are preserved.'
)}
</p>
</section>
)
}
@@ -414,22 +414,11 @@ describe('AgentsPane', () => {
expect(matchesSettingsSearch('manual', getAgentsPaneSearchEntries())).toBe(true)
})
it('applies the selected agent permission mode from settings without a mixed segment', () => {
const onChange = vi.fn()
const element = AgentPermissionsSetting({ mode: 'mixed', onChange })
const props = element.props.children.props.action.props as {
value: 'yolo'
onChange: (value: 'yolo' | 'manual' | 'mixed') => void
options: { value: string }[]
}
expect(props.value).toBe('yolo')
expect(props.options.map((option) => option.value)).toEqual(['yolo', 'manual'])
props.onChange('mixed')
expect(onChange).not.toHaveBeenCalled()
props.onChange('manual')
expect(onChange).toHaveBeenCalledWith('manual')
it('shows mixed permission settings without selecting a preset', () => {
const html = renderToStaticMarkup(<AgentPermissionsSetting mode="mixed" onChange={vi.fn()} />)
expect(html).toContain('Custom configuration')
expect(html).toContain('>Auto</button>')
expect(html).not.toContain('aria-checked="true"')
})
it('keeps catalog agent ids, labels, and commands discoverable in settings search', () => {
@@ -1,5 +1,4 @@
import { useMemo } from 'react'
import { Info } from 'lucide-react'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import type { TuiAgent } from '../../../../shared/tui-agent'
import { getAgentCatalog } from '@/lib/agent-catalog'
@@ -14,11 +13,7 @@ import {
getAgentGeneratedTabTitlesTitle
} from './agent-generated-tab-title-copy'
import { getAgentStatusHooksDescription, getAgentStatusHooksTitle } from './agent-status-hooks-copy'
import {
SettingsSegmentedControl,
SettingsSubsectionHeader,
SettingsSwitchRow
} from './SettingsFormControls'
import { SettingsSwitchRow } from './SettingsFormControls'
import {
isTuiAgentEnabled,
normalizeDisabledTuiAgents
@@ -31,12 +26,11 @@ import {
} from '../../../../shared/tui-agent-launch-defaults'
import {
applyAgentPermissionMode,
applyTuiAgentPermissionMode,
resolveAgentPermissionModeSummary,
type AgentPermissionMode
resolveTuiAgentPermissionMode
} from '../../../../shared/tui-agent-permissions'
import { getSettingOwnershipSummary } from './setting-ownership'
import { translate } from '@/i18n/i18n'
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'
import { isPairedWebClientWindow } from '@/lib/desktop-window-chrome'
import { getAgentsPaneSearchEntries } from './agents-search'
import {
@@ -44,6 +38,8 @@ import {
createAgentAvailabilityUpdateQueue
} from './agent-availability-settings'
import { AgentAvailabilityControl, type AgentCatalogRowProps } from './AgentCatalogRow'
import { AgentPermissionsSetting } from './AgentPermissionsSetting'
export { AgentPermissionsSetting } from './AgentPermissionsSetting'
import { AgentDefaultSetting } from './AgentDefaultSetting'
import { AgentDetectionCatalog } from './AgentDetectionCatalog'
@@ -65,79 +61,6 @@ type AgentsPaneProps = {
const enqueueAgentAvailabilityUpdate = createAgentAvailabilityUpdateQueue()
export function AgentPermissionsSetting({
mode,
onChange
}: {
mode: AgentPermissionMode
onChange: (mode: Exclude<AgentPermissionMode, 'mixed'>) => void
}): React.JSX.Element {
const visibleMode: Exclude<AgentPermissionMode, 'mixed'> = mode === 'manual' ? 'manual' : 'yolo'
return (
<section className="space-y-3">
<SettingsSubsectionHeader
title={
<span className="flex items-center gap-2">
{translate('auto.components.settings.AgentsPane.agentPermissions', 'Agent Permissions')}
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label={translate(
'auto.components.settings.AgentsPane.agentPermissionsInfo',
'Agent permissions info'
)}
className="grid size-5 place-items-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
<Info className="size-3.5" />
</button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={6}>
{translate(
'auto.components.settings.AgentsPane.agentPermissionsTooltip',
"Doesn't apply to agents where you've overridden launch arguments."
)}
</TooltipContent>
</Tooltip>
</span>
}
description={translate(
'auto.components.settings.AgentsPane.agentPermissionsDescription',
'Choose whether Orca launches agents with fewer permission prompts or with manual checks.'
)}
action={
<SettingsSegmentedControl<AgentPermissionMode>
value={visibleMode}
onChange={(nextMode) => {
if (nextMode !== 'mixed') {
onChange(nextMode)
}
}}
ariaLabel={translate(
'auto.components.settings.AgentsPane.agentPermissions',
'Agent Permissions'
)}
size="sm"
options={[
{
value: 'yolo',
label: translate('auto.components.settings.AgentsPane.agentPermissionsYolo', 'Yolo')
},
{
value: 'manual',
label: translate(
'auto.components.settings.AgentsPane.agentPermissionsManual',
'Manual'
)
}
]}
/>
}
/>
</section>
)
}
export function AgentsPane({
settings,
updateSettings,
@@ -212,6 +135,23 @@ export function AgentsPane({
cmdOverride: isDetected ? cmdOverrides[agent.id] : undefined,
argsOverride: resolveTuiAgentLaunchArgs(agent.id, agentDefaultArgs),
envOverride: resolveTuiAgentLaunchEnv(agent.id, agentDefaultEnv),
permissionMode: resolveTuiAgentPermissionMode({
agent: agent.id,
agentArgs: agentDefaultArgs[agent.id],
agentEnv: agentDefaultEnv[agent.id]
}),
onSetPermissionMode: (mode) => {
const next = applyTuiAgentPermissionMode({
agent: agent.id,
mode,
agentArgs: resolveTuiAgentLaunchArgs(agent.id, agentDefaultArgs),
agentEnv: resolveTuiAgentLaunchEnv(agent.id, agentDefaultEnv)
})
updateSettings({
agentDefaultArgs: { ...agentDefaultArgs, [agent.id]: next.agentArgs },
agentDefaultEnv: { ...agentDefaultEnv, [agent.id]: next.agentEnv }
})
},
onSetDefault: isDetected ? () => updateSettings({ defaultTuiAgent: agent.id }) : () => {},
onSetEnabled: (enabled) => setAgentEnabled(agent.id, enabled),
onSaveOverride: isDetected
@@ -34,6 +34,7 @@ function buildAgentSettingsKeywords(): string[] {
{ key: 'auto.components.settings.agents.search.permission', fallback: 'permission' },
{ key: 'auto.components.settings.agents.search.permissions', fallback: 'permissions' },
{ key: 'auto.components.settings.agents.search.yolo', fallback: 'yolo', englishOnly: true },
{ key: 'auto.components.settings.agents.search.auto', fallback: 'auto' },
{ key: 'auto.components.settings.agents.search.manual', fallback: 'manual' },
{
key: 'auto.components.settings.agents.search.e2b7c0dcd7',
@@ -126,7 +127,7 @@ const getAllAgentsPaneSearchEntries = createLocalizedCatalog(() => [
),
description: translate(
'auto.components.settings.agents.search.agentPermissionsDescription',
'Switch agent permission defaults between Yolo and Manual.'
'Switch agent permission defaults between Manual, Auto, and Yolo.'
),
keywords: [
...translateSearchKeyword('auto.components.settings.agents.search.permission', 'permission'),
@@ -135,6 +136,7 @@ const getAllAgentsPaneSearchEntries = createLocalizedCatalog(() => [
'permissions'
),
...translateSearchKeyword('auto.components.settings.agents.search.yolo', 'yolo'),
...translateSearchKeyword('auto.components.settings.agents.search.auto', 'auto'),
...translateSearchKeyword('auto.components.settings.agents.search.manual', 'manual'),
...translateSearchKeyword('auto.components.settings.agents.search.skip', 'skip'),
...translateSearchKeyword('auto.components.settings.agents.search.checks', 'checks')
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { YOLO_TUI_AGENT_ARGS } from '../../../../shared/tui-agent-permissions'
import { AUTO_TUI_AGENT_ARGS, YOLO_TUI_AGENT_ARGS } from '../../../../shared/tui-agent-permissions'
import { createTestStore, makeTab } from '../../store/slices/store-test-helpers'
import type { AppState } from '../../store/types'
import {
@@ -102,6 +102,28 @@ describe('Codex auto-approval status suppression', () => {
).toBe(false)
})
it.each(['waiting', 'blocked'] as const)(
'preserves Auto %s attention, including user questions',
(state) => {
registerCodexLaunchConfig({ agentArgs: AUTO_TUI_AGENT_ARGS.codex ?? '', launchToken })
for (const toolName of [undefined, 'request_user_input']) {
expect(
shouldSuppressCodexAutoApprovalStatus(
{ state, prompt: 'needs attention', agentType: 'codex', toolName },
{ paneKey, tabId: 'tab-1', launchToken }
)
).toBe(false)
}
expect(
shouldSuppressCodexAutoApprovalSyntheticTitle('Codex - action required', {
paneKey,
tabId: 'tab-1',
launchToken
})
).toBe(false)
}
)
it('preserves manual Codex permission attention', () => {
registerCodexLaunchConfig({ agentArgs: '', launchToken })
+11 -1
View File
@@ -675,6 +675,11 @@
}
}
}
},
"AgentStep": {
"yoloPermissionsInfo": "Agent permission info",
"yoloPermissionsLabel": "Yolo / Dangerously skip permissions",
"yoloPermissionsTooltip": "Skip permission checks for agents for less interruptions"
}
},
"right": {
@@ -1087,7 +1092,12 @@
"9b175d0f5e": "Pre-selected agent when opening a new workspace.",
"c8794e622e": "Detected",
"db9e9e5887": "Customize command",
"df123171d1": "Not installed"
"df123171d1": "Not installed",
"agentPermissionsDescription": "Choose whether Orca launches agents with fewer permission prompts or with manual checks.",
"agentPermissionsInfo": "Agent permissions info",
"agentPermissionsTooltip": "Doesn't apply to agents where you've overridden launch arguments.",
"cea7d97be1": "Collapse command override",
"dc4a2ffdc0": "Expand command override"
},
"AppearancePane": {
"0f28e7b30c": "Choose how Orca looks in the app window.",
+11 -3
View File
@@ -6583,7 +6583,14 @@
"codexSessionSourceInfo": "About importing Codex history",
"codexSessionSourceTooltip": "Orca runs Codex in an isolated home. Point this at your existing Codex home to import that session history. Empty uses ~/.codex.",
"storedDefaultUndetected": "Saved as your default, but not detected right now",
"noAgentsDetected": "No agents detected. If one is installed, the probe may have timed out."
"noAgentsDetected": "No agents detected. If one is installed, the probe may have timed out.",
"agentPermissionsAuto": "Auto",
"agentPermissionsModesDescription": "Manual asks for approval. Auto uses the agent’s guarded approval mode; agents without Auto use Manual. Yolo skips permission checks.",
"agentPermissionsCustom": "Custom configuration. Choose a preset to update standard modes; custom launch arguments and environment values are preserved.",
"agentPermissionsPreserveCustom": "Applies to all agents. Custom launch arguments and environment values are preserved.",
"agentPermissionsEditCustom": "Custom configuration. Edit or reset the launch arguments and environment below to use a permission preset.",
"collapseLaunchSettings": "Collapse launch settings",
"expandLaunchSettings": "Expand launch settings"
},
"AppIconSelector": {
"d5a112dc9b": "Next icon",
@@ -8944,7 +8951,7 @@
"a79d266f71": "session",
"afbf35be68": "stable session",
"agentPermissions": "Agent Permissions",
"agentPermissionsDescription": "Switch agent permission defaults between Yolo and Manual.",
"agentPermissionsDescription": "Switch agent permission defaults between Manual, Auto, and Yolo.",
"agentRuntime": "Agent Runtime",
"agentRuntimeDescription": "Choose whether agents are detected and launched on Windows or in WSL by default.",
"runtime": "runtime",
@@ -8955,7 +8962,8 @@
"yolo": "yolo",
"manual": "manual",
"skip": "skip",
"checks": "checks"
"checks": "checks",
"auto": "auto"
}
},
"appearance": {
+11 -3
View File
@@ -5556,7 +5556,14 @@
"03e1a5081a": "on {{value0}}",
"25a41a9aad": "Re-detect agents installed on the active server",
"remoteDetectionFailed": "Couldn’t detect installed agents. Check the host connection and try again.",
"retryDetection": "Retry"
"retryDetection": "Retry",
"agentPermissionsAuto": "Auto",
"agentPermissionsModesDescription": "Manual pide aprobación. Auto usa el modo de aprobación protegido del agente; los agentes sin Auto usan Manual. Yolo omite las comprobaciones de permisos.",
"agentPermissionsCustom": "Configuración personalizada. Elige un ajuste para actualizar los modos estándar; se conservan los argumentos de inicio y las variables de entorno personalizados.",
"agentPermissionsPreserveCustom": "Se aplica a todos los agentes. Se conservan los argumentos de inicio y las variables de entorno personalizados.",
"agentPermissionsEditCustom": "Configuración personalizada. Edita o restablece los argumentos de inicio y el entorno de abajo para usar un ajuste de permisos.",
"collapseLaunchSettings": "Contraer ajustes de inicio",
"expandLaunchSettings": "Expandir ajustes de inicio"
},
"AppIconSelector": {
"d5a112dc9b": "Icono siguiente",
@@ -7788,9 +7795,10 @@
"a79d266f71": "sesión",
"afbf35be68": "sesión estable",
"agentPermissions": "Permisos de agentes",
"agentPermissionsDescription": "Cambia los permisos predeterminados de los agentes entre Yolo y Manual.",
"agentPermissionsDescription": "Cambia los permisos predeterminados de los agentes entre Manual, Auto y Yolo.",
"agentRuntime": "Runtime del agente",
"agentRuntimeDescription": "Elige si los agentes se detectan y ejecutan en Windows o en WSL de forma predeterminada."
"agentRuntimeDescription": "Elige si los agentes se detectan y ejecutan en Windows o en WSL de forma predeterminada.",
"auto": "automático"
}
},
"appearance": {
+11 -3
View File
@@ -6298,7 +6298,14 @@
"codexSessionSourceInfo": "À propos de l'import de l'historique Codex",
"codexSessionSourceTooltip": "Orca exécute Codex dans un home isolé. Pointez ceci vers votre home Codex existant pour en importer l'historique de sessions. Si vide, ~/.codex est utilisé.",
"storedDefaultUndetected": "Enregistré par défaut, mais non détecté actuellement",
"noAgentsDetected": "Aucun agent détecté. Si un agent est installé, la détection a peut-être expiré."
"noAgentsDetected": "Aucun agent détecté. Si un agent est installé, la détection a peut-être expiré.",
"agentPermissionsAuto": "Auto",
"agentPermissionsModesDescription": "Manual demande une approbation. Auto utilise le mode d’approbation protégé de l’agent ; les agents sans Auto utilisent Manual. Yolo ignore les vérifications de permissions.",
"agentPermissionsCustom": "Configuration personnalisée. Choisissez un préréglage pour modifier les modes standard ; les arguments de lancement et les valeurs d’environnement personnalisés sont conservés.",
"agentPermissionsPreserveCustom": "S’applique à tous les agents. Les arguments de lancement et les valeurs d’environnement personnalisés sont conservés.",
"agentPermissionsEditCustom": "Configuration personnalisée. Modifiez ou réinitialisez les arguments de lancement et l’environnement ci-dessous pour utiliser un préréglage de permissions.",
"collapseLaunchSettings": "Réduire les paramètres de lancement",
"expandLaunchSettings": "Développer les paramètres de lancement"
},
"AppIconSelector": {
"d5a112dc9b": "Icône suivante",
@@ -8629,9 +8636,10 @@
"a79d266f71": "session",
"afbf35be68": "session stable",
"agentPermissions": "Permissions des agents",
"agentPermissionsDescription": "Basculer les autorisations d'agent par défaut entre Yolo et Manuelle.",
"agentPermissionsDescription": "Choisissez les permissions par défaut des agents : Manual, Auto ou Yolo.",
"agentRuntime": "Runtime des agents",
"agentRuntimeDescription": "Choisissez si les agents sont détectés et lancés par défaut sous Windows ou dans WSL."
"agentRuntimeDescription": "Choisissez si les agents sont détectés et lancés par défaut sous Windows ou dans WSL.",
"auto": "automatique"
}
},
"appearance": {
+11 -3
View File
@@ -5541,7 +5541,14 @@
"03e1a5081a": "{{value0}} 上",
"25a41a9aad": "アクティブなサーバーにインストールされている Agent を再検出",
"remoteDetectionFailed": "インストールされている Agent を検出できませんでした。ホストへの接続を確認して、もう一度お試しください。",
"retryDetection": "再試行"
"retryDetection": "再試行",
"agentPermissionsAuto": "Auto",
"agentPermissionsModesDescription": "Manual は承認を求めます。Auto はAgentの保護付き承認モードを使用します。Auto 非対応のAgentは Manual を使用します。Yolo は権限チェックをスキップします。",
"agentPermissionsCustom": "カスタム設定です。プリセットを選択すると標準モードを更新します。カスタムの起動引数と環境変数の値は保持されます。",
"agentPermissionsPreserveCustom": "すべてのAgentに適用します。カスタムの起動引数と環境変数の値は保持されます。",
"agentPermissionsEditCustom": "カスタム設定です。権限プリセットを使用するには、以下の起動引数と環境変数を編集またはリセットしてください。",
"collapseLaunchSettings": "起動設定を折りたたむ",
"expandLaunchSettings": "起動設定を展開"
},
"AppIconSelector": {
"d5a112dc9b": "次へのアイコン",
@@ -7810,9 +7817,10 @@
"a79d266f71": "セッション",
"afbf35be68": "安定したセッション",
"agentPermissions": "Agent の権限",
"agentPermissionsDescription": "Agent の権限の既定値を Yolo と Manual で切り替えます。",
"agentPermissionsDescription": "Agentの既定の権限を Manual、Auto、Yolo から選択します。",
"agentRuntime": "Agent のランタイム",
"agentRuntimeDescription": "デフォルトで Agent を Windows または WSL のどちらで検出および起動するかを選択します。"
"agentRuntimeDescription": "デフォルトで Agent を Windows または WSL のどちらで検出および起動するかを選択します。",
"auto": "自動"
}
},
"appearance": {
+11 -3
View File
@@ -5546,7 +5546,14 @@
"03e1a5081a": "on {{value0}}",
"25a41a9aad": "Re-detect agents installed on the active server",
"remoteDetectionFailed": "Couldn’t detect installed agents. Check the host connection and try again.",
"retryDetection": "Retry"
"retryDetection": "Retry",
"agentPermissionsAuto": "Auto",
"agentPermissionsModesDescription": "Manual은 승인을 요청합니다. Auto는 에이전트의 보호된 승인 모드를 사용하며, Auto를 지원하지 않는 에이전트는 Manual을 사용합니다. Yolo는 권한 검사를 건너뜁니다.",
"agentPermissionsCustom": "사용자 지정 설정입니다. 프리셋을 선택하면 표준 모드가 변경되며, 사용자 지정 실행 인수와 환경 변수 값은 유지됩니다.",
"agentPermissionsPreserveCustom": "모든 에이전트에 적용됩니다. 사용자 지정 실행 인수와 환경 변수 값은 유지됩니다.",
"agentPermissionsEditCustom": "사용자 지정 설정입니다. 권한 프리셋을 사용하려면 아래 실행 인수와 환경 변수를 편집하거나 초기화하세요.",
"collapseLaunchSettings": "실행 설정 접기",
"expandLaunchSettings": "실행 설정 펼치기"
},
"AppIconSelector": {
"d5a112dc9b": "다음 아이콘",
@@ -7778,9 +7785,10 @@
"a79d266f71": "세션",
"afbf35be68": "안정적인 세션",
"agentPermissions": "Agent 권한",
"agentPermissionsDescription": "agent 권한 기본값을 Yolo와 수동 사이에서 전환합니다.",
"agentPermissionsDescription": "agent의 기본 권한을 Manual, Auto, Yolo 중에서 선택합니다.",
"agentRuntime": "Agent 런타임",
"agentRuntimeDescription": "기본적으로 Windows 또는 WSL에서 agents를 감지하고 시작할지 선택합니다."
"agentRuntimeDescription": "기본적으로 Windows 또는 WSL에서 agents를 감지하고 시작할지 선택합니다.",
"auto": "자동"
}
},
"appearance": {
+11 -3
View File
@@ -5607,7 +5607,14 @@
"03e1a5081a": "on {{value0}}",
"25a41a9aad": "重新检测活动服务器上安装的智能体",
"remoteDetectionFailed": "无法检测已安装的智能体。请检查主机连接后重试。",
"retryDetection": "Retry"
"retryDetection": "Retry",
"agentPermissionsAuto": "Auto",
"agentPermissionsModesDescription": "Manual 会请求批准。Auto 使用智能体带保护机制的审批模式;不支持 Auto 的智能体使用 Manual。Yolo 会跳过权限检查。",
"agentPermissionsCustom": "自定义配置。选择预设以更新标准模式;自定义启动参数和环境变量值会保留。",
"agentPermissionsPreserveCustom": "应用于所有智能体。自定义启动参数和环境变量值会保留。",
"agentPermissionsEditCustom": "自定义配置。编辑或重置下方的启动参数和环境变量以使用权限预设。",
"collapseLaunchSettings": "收起启动设置",
"expandLaunchSettings": "展开启动设置"
},
"AppIconSelector": {
"d5a112dc9b": "下一个图标",
@@ -7843,9 +7850,10 @@
"a79d266f71": "会话",
"afbf35be68": "稳定会话",
"agentPermissions": "智能体权限",
"agentPermissionsDescription": "在 Yolo 和手动之间切换智能体权限默认值。",
"agentPermissionsDescription": "在 Manual、Auto 和 Yolo 之间切换智能体的默认权限。",
"agentRuntime": "智能体运行时",
"agentRuntimeDescription": "选择默认在 Windows 还是 WSL 中检测并启动智能体。"
"agentRuntimeDescription": "选择默认在 Windows 还是 WSL 中检测并启动智能体。",
"auto": "自动"
}
},
"appearance": {
+4
View File
@@ -173,6 +173,8 @@ export const GITLAB_READY_FOR_REVIEW_RUNTIME_CAPABILITY =
'gitlab.updateMR.readyForReview.v1' as const
export const GITLAB_READY_FOR_REVIEW_UPDATE_REQUIRED_MESSAGE =
'Marking a merge request ready requires a newer Orca server. Update the server and try again.'
// Older clients cannot round-trip Auto permission presets safely.
export const AGENT_PERMISSION_AUTO_RUNTIME_CAPABILITY = 'settings.agent-permission-auto.v1' as const
export const WORKTREE_VISIBILITY_DEFAULTS_RUNTIME_CAPABILITY =
'worktree.visibility-defaults.v1' as const
export const WORKTREE_VISIBILITY_SOURCE_DEFAULTS_RUNTIME_CAPABILITY =
@@ -194,6 +196,7 @@ export const AUTOMATION_CREATE_IDEMPOTENCY_RUNTIME_CAPABILITY =
export const NATIVE_REMOTE_RUNTIME_CLIENT_CAPABILITIES = [
SESSION_TAB_CLOSE_INTENT_RUNTIME_CAPABILITY,
AGENT_SESSION_BOUNDARY_RUNTIME_CAPABILITY,
AGENT_PERMISSION_AUTO_RUNTIME_CAPABILITY,
WORKTREE_VISIBILITY_DEFAULTS_RUNTIME_CAPABILITY,
WORKTREE_VISIBILITY_SOURCE_DEFAULTS_RUNTIME_CAPABILITY,
WORKTREE_GITHUB_PR_SUPPRESSION_RUNTIME_CAPABILITY,
@@ -266,6 +269,7 @@ export const RUNTIME_CAPABILITIES = [
FILE_MUTATION_OWNERSHIP_RUNTIME_CAPABILITY,
GITHUB_MARK_PR_READY_RUNTIME_CAPABILITY,
GITLAB_READY_FOR_REVIEW_RUNTIME_CAPABILITY,
AGENT_PERMISSION_AUTO_RUNTIME_CAPABILITY,
WORKTREE_VISIBILITY_DEFAULTS_RUNTIME_CAPABILITY,
WORKTREE_VISIBILITY_SOURCE_DEFAULTS_RUNTIME_CAPABILITY,
ACCOUNT_IMPORT_RUNTIME_CAPABILITY,
@@ -1,4 +1,5 @@
import {
AGENT_PERMISSION_AUTO_RUNTIME_CAPABILITY,
AGENT_SESSION_BOUNDARY_RUNTIME_CAPABILITY,
AUTOMATION_OWNER_FENCING_RUNTIME_CAPABILITY,
SESSION_TAB_CLOSE_INTENT_RUNTIME_CAPABILITY,
@@ -18,6 +19,7 @@ export function remoteRuntimeClientCapabilities(
new Set([
SESSION_TAB_CLOSE_INTENT_RUNTIME_CAPABILITY,
SESSION_TABS_AUTHORITATIVE_INVENTORY_RUNTIME_CAPABILITY,
AGENT_PERMISSION_AUTO_RUNTIME_CAPABILITY,
AGENT_SESSION_BOUNDARY_RUNTIME_CAPABILITY,
SKILL_INSTALL_RESULT_V2_CAPABILITY,
WORKTREE_GITHUB_PR_SUPPRESSION_RUNTIME_CAPABILITY,
@@ -0,0 +1,57 @@
import { describe, expect, it } from 'vitest'
import { applyAgentPermissionMode, AUTO_TUI_AGENT_ARGS } from './tui-agent-permissions'
import {
normalizeTuiAgentArgsRecord,
normalizeTuiAgentEnvRecord,
resolveTuiAgentLaunchArgs,
resolveTuiAgentLaunchEnv
} from './tui-agent-launch-defaults'
import { buildAgentStartupPlan } from './tui-agent-startup'
import type { TuiAgent } from './tui-agent'
import { tokenizeStartupCommand } from './tui-agent-startup-shell'
describe('Auto permission launch settings', () => {
it.each(['darwin', 'linux', 'win32'] as const)(
'retains guarded presets through normalization and local/remote %s launch planning',
(platform) => {
const profile = applyAgentPermissionMode({ mode: 'auto' })
const args = normalizeTuiAgentArgsRecord(profile.agentDefaultArgs)
const env = normalizeTuiAgentEnvRecord(profile.agentDefaultEnv)
for (const isRemote of [false, true]) {
for (const agent of [...Object.keys(AUTO_TUI_AGENT_ARGS), 'goose', 'amp'] as TuiAgent[]) {
const agentArgs = resolveTuiAgentLaunchArgs(agent, args)
const agentEnv = resolveTuiAgentLaunchEnv(agent, env)
const plan = buildAgentStartupPlan({
agent,
agentArgs,
agentEnv,
platform,
isRemote,
cmdOverrides: {},
prompt: '',
allowEmptyPromptLaunch: true
})
expect(plan).not.toBeNull()
if (agent in AUTO_TUI_AGENT_ARGS) {
const parsed = tokenizeStartupCommand(
plan?.launchCommand ?? '',
platform === 'win32' ? 'powershell' : 'posix'
)
expect(parsed.ok).toBe(true)
if (parsed.ok) {
const flags = AUTO_TUI_AGENT_ARGS[agent]?.split(' ') ?? []
expect(parsed.tokens.slice(-flags.length)).toEqual(flags)
}
}
if (agent === 'goose') {
expect(plan?.env).toEqual({ GOOSE_MODE: 'smart_approve' })
}
if (agent === 'amp') {
expect(agentArgs).toBe('')
expect(plan?.launchCommand).not.toContain('--dangerously-allow-all')
}
}
}
}
)
})
+86
View File
@@ -1,6 +1,10 @@
import { describe, expect, it } from 'vitest'
import {
applyAgentPermissionMode,
applyTuiAgentPermissionMode,
AUTO_TUI_AGENT_ARGS,
AUTO_TUI_AGENT_ENV,
supportsTuiAgentAutoPermissionMode,
resolveAgentPermissionModeSummary,
resolveTuiAgentPermissionMode,
YOLO_TUI_AGENT_ARGS,
@@ -86,3 +90,85 @@ describe('tui agent permissions', () => {
).toBe('yolo')
})
})
describe('intermediate permission presets', () => {
it('round trips all presets including manual fallbacks from an Auto profile', () => {
let profile = applyAgentPermissionMode({ mode: 'yolo' })
for (const mode of ['auto', 'manual', 'yolo', 'auto'] as const) {
profile = applyAgentPermissionMode({ ...profile, mode })
expect(resolveAgentPermissionModeSummary(profile)).toBe(mode)
for (const agent of Object.keys(
YOLO_TUI_AGENT_ARGS
) as (keyof typeof YOLO_TUI_AGENT_ARGS)[]) {
expect(
resolveTuiAgentPermissionMode({ agent, agentArgs: profile.agentDefaultArgs[agent] })
).toBe(mode === 'auto' && !supportsTuiAgentAutoPermissionMode(agent) ? 'manual' : mode)
}
expect(
resolveTuiAgentPermissionMode({ agent: 'goose', agentEnv: profile.agentDefaultEnv.goose })
).toBe(mode)
}
})
it('keeps unknown and custom settings when applying a global preset', () => {
const profile = applyAgentPermissionMode({
mode: 'auto',
agentDefaultArgs: {
claude: '--model custom',
opencode: '--port 1234',
codex: ' --approve-for-me '
},
agentDefaultEnv: {
goose: { GOOSE_MODE: 'approve', CUSTOM: 'value' },
claude: { CUSTOM: 'value' }
}
})
expect(profile.agentDefaultArgs.claude).toBe('--model custom')
expect(profile.agentDefaultArgs.codex).toBe(' --approve-for-me ')
expect(profile.agentDefaultArgs.opencode).toBe('--port 1234')
expect(profile.agentDefaultEnv.goose).toEqual({ GOOSE_MODE: 'approve', CUSTOM: 'value' })
expect(profile.agentDefaultEnv.claude).toEqual({ CUSTOM: 'value' })
expect(resolveAgentPermissionModeSummary(profile)).toBe('mixed')
})
it('reports custom per-agent choices as mixed rather than misleadingly showing Auto or Yolo', () => {
const profile = applyAgentPermissionMode({ mode: 'auto' })
expect(
resolveAgentPermissionModeSummary({
...profile,
agentDefaultArgs: { ...profile.agentDefaultArgs, codex: '' }
})
).toBe('mixed')
expect(
resolveAgentPermissionModeSummary({
...profile,
agentDefaultArgs: { ...profile.agentDefaultArgs, amp: YOLO_TUI_AGENT_ARGS.amp }
})
).toBe('mixed')
})
it('uses the existing args and env launch fields for per-agent choices', () => {
expect(
applyTuiAgentPermissionMode({
agent: 'claude',
mode: 'auto',
agentArgs: YOLO_TUI_AGENT_ARGS.claude,
agentEnv: { CUSTOM: 'value' }
})
).toEqual({ agentArgs: AUTO_TUI_AGENT_ARGS.claude, agentEnv: { CUSTOM: 'value' } })
expect(
applyTuiAgentPermissionMode({
agent: 'goose',
mode: 'auto',
agentEnv: YOLO_TUI_AGENT_ENV.goose
})
).toEqual({ agentArgs: '', agentEnv: AUTO_TUI_AGENT_ENV.goose })
expect(
applyTuiAgentPermissionMode({
agent: 'amp',
mode: 'auto',
agentArgs: YOLO_TUI_AGENT_ARGS.amp
})
).toEqual({ agentArgs: '', agentEnv: {} })
})
})
+102 -72
View File
@@ -1,7 +1,7 @@
import { TUI_AGENT_CONFIG } from './tui-agent-config'
import type { TuiAgent } from './tui-agent'
export type AgentPermissionMode = 'yolo' | 'manual' | 'mixed'
export type AgentPermissionMode = 'yolo' | 'auto' | 'manual' | 'mixed'
export const YOLO_TUI_AGENT_ARGS: Partial<Record<TuiAgent, string>> = {
claude: '--dangerously-skip-permissions',
@@ -36,12 +36,27 @@ export const YOLO_TUI_AGENT_ENV: Partial<Record<TuiAgent, Record<string, string>
goose: { GOOSE_MODE: 'auto' }
}
// Only documented intermediate CLI presets belong here; unsupported agents use Manual.
export const AUTO_TUI_AGENT_ARGS: Partial<Record<TuiAgent, string>> = {
claude: '--permission-mode auto',
'claude-agent-teams': '--permission-mode auto',
codex: '--approve-for-me',
gemini: '--approval-mode auto_edit',
'qwen-code': '--approval-mode auto',
devin: '--permission-mode smart',
droid: '--auto medium'
}
export const AUTO_TUI_AGENT_ENV: Partial<Record<TuiAgent, Record<string, string>>> = {
goose: { GOOSE_MODE: 'smart_approve' }
}
const PERMISSION_AGENT_IDS = Object.keys(TUI_AGENT_CONFIG).filter(
(agent): agent is TuiAgent => agent in YOLO_TUI_AGENT_ARGS || agent in YOLO_TUI_AGENT_ENV
)
function normalizeArgs(value: string | null | undefined): string {
return value?.trim() ?? ''
export function supportsTuiAgentAutoPermissionMode(agent: TuiAgent): boolean {
return agent in AUTO_TUI_AGENT_ARGS || agent in AUTO_TUI_AGENT_ENV
}
function sameEnv(
@@ -49,49 +64,15 @@ function sameEnv(
right: Record<string, string> | null | undefined
): boolean {
const leftEntries = Object.entries(left ?? {})
const rightEntries = Object.entries(right ?? {})
if (leftEntries.length !== rightEntries.length) {
return false
}
return leftEntries.every(([name, value]) => right?.[name] === value)
}
function resolveAgentPermissionMode(args: string, yoloArgs: string): AgentPermissionMode {
if (!args) {
return 'manual'
}
return args === yoloArgs ? 'yolo' : 'mixed'
}
function resolveAgentEnvPermissionMode(
env: Record<string, string> | null | undefined,
yoloEnv: Record<string, string> | undefined
): AgentPermissionMode {
if (sameEnv(env, {})) {
return 'manual'
}
return sameEnv(env, yoloEnv) ? 'yolo' : 'mixed'
return (
leftEntries.length === Object.keys(right ?? {}).length &&
leftEntries.every(([name, value]) => right?.[name] === value)
)
}
function combinePermissionModes(modes: AgentPermissionMode[]): AgentPermissionMode {
let sawYolo = false
let sawManual = false
let sawMixed = false
for (const mode of modes) {
if (mode === 'yolo') {
sawYolo = true
} else if (mode === 'manual') {
sawManual = true
} else {
sawMixed = true
}
}
if (sawMixed || (sawYolo && sawManual)) {
return 'mixed'
}
return sawYolo ? 'yolo' : 'manual'
const distinct = new Set(modes)
return distinct.size > 1 ? 'mixed' : (modes[0] ?? 'manual')
}
export function resolveTuiAgentPermissionMode(args: {
@@ -101,17 +82,28 @@ export function resolveTuiAgentPermissionMode(args: {
}): AgentPermissionMode {
const modes: AgentPermissionMode[] = []
if (args.agent in YOLO_TUI_AGENT_ARGS) {
const value = args.agentArgs ?? ''
modes.push(
resolveAgentPermissionMode(
normalizeArgs(args.agentArgs),
YOLO_TUI_AGENT_ARGS[args.agent] ?? ''
)
!value
? 'manual'
: value === YOLO_TUI_AGENT_ARGS[args.agent]
? 'yolo'
: value === AUTO_TUI_AGENT_ARGS[args.agent]
? 'auto'
: 'mixed'
)
}
if (args.agent in YOLO_TUI_AGENT_ENV) {
modes.push(resolveAgentEnvPermissionMode(args.agentEnv, YOLO_TUI_AGENT_ENV[args.agent]))
modes.push(
sameEnv(args.agentEnv, {})
? 'manual'
: sameEnv(args.agentEnv, YOLO_TUI_AGENT_ENV[args.agent])
? 'yolo'
: AUTO_TUI_AGENT_ENV[args.agent] && sameEnv(args.agentEnv, AUTO_TUI_AGENT_ENV[args.agent])
? 'auto'
: 'mixed'
)
}
return combinePermissionModes(modes)
}
@@ -119,19 +111,62 @@ export function resolveAgentPermissionModeSummary(args: {
agentDefaultArgs?: Partial<Record<TuiAgent, string>> | null
agentDefaultEnv?: Partial<Record<TuiAgent, Record<string, string>>> | null
}): AgentPermissionMode {
const modes: AgentPermissionMode[] = []
for (const agent of PERMISSION_AGENT_IDS) {
modes.push(
resolveTuiAgentPermissionMode({
agent,
agentArgs: args.agentDefaultArgs?.[agent],
agentEnv: args.agentDefaultEnv?.[agent]
})
const modes = PERMISSION_AGENT_IDS.map((agent) => ({
agent,
mode: resolveTuiAgentPermissionMode({
agent,
agentArgs: args.agentDefaultArgs?.[agent],
agentEnv: args.agentDefaultEnv?.[agent]
})
}))
if (
modes.some(({ mode }) => mode === 'auto') &&
modes.every(
({ agent, mode }) =>
mode === 'auto' || (mode === 'manual' && !supportsTuiAgentAutoPermissionMode(agent))
)
) {
return 'auto'
}
return combinePermissionModes(modes.map(({ mode }) => mode))
}
return combinePermissionModes(modes)
export function applyTuiAgentPermissionMode(args: {
agent: TuiAgent
mode: Exclude<AgentPermissionMode, 'mixed'>
agentArgs?: string | null
agentEnv?: Record<string, string> | null
}): { agentArgs: string; agentEnv: Record<string, string> } {
let agentArgs = args.agentArgs ?? ''
let agentEnv = { ...args.agentEnv }
if (
args.agent in YOLO_TUI_AGENT_ARGS &&
(!agentArgs ||
agentArgs === YOLO_TUI_AGENT_ARGS[args.agent] ||
agentArgs === AUTO_TUI_AGENT_ARGS[args.agent])
) {
agentArgs =
(args.mode === 'yolo'
? YOLO_TUI_AGENT_ARGS[args.agent]
: args.mode === 'auto'
? AUTO_TUI_AGENT_ARGS[args.agent]
: '') ?? ''
}
if (
args.agent in YOLO_TUI_AGENT_ENV &&
(sameEnv(agentEnv, {}) ||
sameEnv(agentEnv, YOLO_TUI_AGENT_ENV[args.agent]) ||
(AUTO_TUI_AGENT_ENV[args.agent] && sameEnv(agentEnv, AUTO_TUI_AGENT_ENV[args.agent])))
) {
agentEnv = {
...(args.mode === 'yolo'
? YOLO_TUI_AGENT_ENV[args.agent]
: args.mode === 'auto'
? AUTO_TUI_AGENT_ENV[args.agent]
: {})
}
}
return { agentArgs, agentEnv }
}
export function applyAgentPermissionMode(args: {
@@ -144,24 +179,19 @@ export function applyAgentPermissionMode(args: {
} {
const nextArgs = { ...args.agentDefaultArgs }
const nextEnv = { ...args.agentDefaultEnv }
for (const agent of PERMISSION_AGENT_IDS) {
const result = applyTuiAgentPermissionMode({
agent,
mode: args.mode,
agentArgs: nextArgs[agent],
agentEnv: nextEnv[agent]
})
if (agent in YOLO_TUI_AGENT_ARGS) {
const yoloArgs = YOLO_TUI_AGENT_ARGS[agent] ?? ''
const currentArgs = normalizeArgs(nextArgs[agent])
if (!currentArgs || currentArgs === yoloArgs) {
nextArgs[agent] = args.mode === 'yolo' ? yoloArgs : ''
}
nextArgs[agent] = result.agentArgs
}
if (agent in YOLO_TUI_AGENT_ENV) {
const yoloEnv = YOLO_TUI_AGENT_ENV[agent]
const currentEnv = nextEnv[agent]
if (sameEnv(currentEnv, {}) || sameEnv(currentEnv, yoloEnv)) {
nextEnv[agent] = args.mode === 'yolo' ? { ...yoloEnv } : {}
}
nextEnv[agent] = result.agentEnv
}
}
return { agentDefaultArgs: nextArgs, agentDefaultEnv: nextEnv }
}