diff --git a/android/app/src/main/kotlin/app/hapi/companion/feature/chat/ChatViewModel.kt b/android/app/src/main/kotlin/app/hapi/companion/feature/chat/ChatViewModel.kt index ccefd920..ae23c456 100644 --- a/android/app/src/main/kotlin/app/hapi/companion/feature/chat/ChatViewModel.kt +++ b/android/app/src/main/kotlin/app/hapi/companion/feature/chat/ChatViewModel.kt @@ -1671,9 +1671,7 @@ class ChatViewModel( active = detail?.active ?: summary?.active ?: false, controlledByUser = terminalControlled(detail), permissionMode = detail?.permissionMode, - permissionModes = PermissionModes.forFlavor(flavor).filter { - detail?.metadata?.capabilities?.concurrentClients != true || it != PermissionMode.SafeYolo - }, + permissionModes = PermissionModes.forFlavor(flavor), model = model, modelOptions = modelOptions, modelOptionsLoading = modelOptionsLoading, diff --git a/android/app/src/main/kotlin/app/hapi/companion/feature/chat/PermissionModeChip.kt b/android/app/src/main/kotlin/app/hapi/companion/feature/chat/PermissionModeChip.kt index 040e3504..e8f8db5c 100644 --- a/android/app/src/main/kotlin/app/hapi/companion/feature/chat/PermissionModeChip.kt +++ b/android/app/src/main/kotlin/app/hapi/companion/feature/chat/PermissionModeChip.kt @@ -25,9 +25,7 @@ import app.hapi.protocol.catalog.PermissionModes * it only when the flavor offers it, and hide `default` everywhere except * copilot (model=auto sessions still surface the bottom-right mode chip). * - * This consults the *unfiltered* flavor catalog: the concurrent-clients - * Safe-Yolo carve-out only narrows the switcher, never the chip. Unknown - * wire ids stay hidden (no raw fallback — same as the web span, which keys + * Unknown wire ids stay hidden (no raw fallback — same as the web span, which keys * off `isPermissionModeAllowedForFlavor`). */ object PermissionModeChipRule { diff --git a/android/app/src/main/kotlin/app/hapi/companion/feature/newsession/NewSessionForm.kt b/android/app/src/main/kotlin/app/hapi/companion/feature/newsession/NewSessionForm.kt index dec06da8..bee1f8c7 100644 --- a/android/app/src/main/kotlin/app/hapi/companion/feature/newsession/NewSessionForm.kt +++ b/android/app/src/main/kotlin/app/hapi/companion/feature/newsession/NewSessionForm.kt @@ -126,7 +126,7 @@ object NewSessionLogic { } val permissionMode = if (usesSharedPermissionModeState(agent)) { stored?.permissionMode - ?.takeIf { mode -> PermissionModes.forLaunch(agent).any { it.wireId == mode } } + ?.takeIf { mode -> PermissionModes.forFlavor(agent).any { it.wireId == mode } } ?: "default" } else { null @@ -352,7 +352,7 @@ object NewSessionLogic { sessionType = draft.sessionType, worktreeName = draft.worktreeName, ) - val allowedModes = PermissionModes.forLaunch(base.agent).map { it.wireId } + val allowedModes = PermissionModes.forFlavor(base.agent).map { it.wireId } val sessionType = if (base.sessionType == SESSION_TYPE_WORKTREE) SESSION_TYPE_WORKTREE else SESSION_TYPE_SIMPLE return base.copy( permissionMode = if (base.permissionMode in allowedModes) base.permissionMode else "default", diff --git a/android/app/src/main/kotlin/app/hapi/companion/feature/newsession/NewSessionViewModel.kt b/android/app/src/main/kotlin/app/hapi/companion/feature/newsession/NewSessionViewModel.kt index bf10b34e..447646bb 100644 --- a/android/app/src/main/kotlin/app/hapi/companion/feature/newsession/NewSessionViewModel.kt +++ b/android/app/src/main/kotlin/app/hapi/companion/feature/newsession/NewSessionViewModel.kt @@ -940,7 +940,7 @@ class NewSessionViewModel( val permission: PermissionUi = when { agent == "pi" || agent == "dsh" -> PermissionUi.Managed usesNativePermissionSelect(agent) -> PermissionUi.NativeSelect( - PermissionModes.forLaunch(agent).map { mode -> + PermissionModes.forFlavor(agent).map { mode -> val unavailable = grokAutoUnavailable && mode == PermissionMode.Auto OptionItem( value = mode.wireId, diff --git a/android/app/src/test/kotlin/app/hapi/companion/feature/chat/PermissionModeChipTest.kt b/android/app/src/test/kotlin/app/hapi/companion/feature/chat/PermissionModeChipTest.kt index c4bba703..58aead43 100644 --- a/android/app/src/test/kotlin/app/hapi/companion/feature/chat/PermissionModeChipTest.kt +++ b/android/app/src/test/kotlin/app/hapi/companion/feature/chat/PermissionModeChipTest.kt @@ -51,11 +51,4 @@ class PermissionModeChipTest { assertNull(PermissionModeChipRule.displayMode(null, "claude")) assertNull(PermissionModeChipRule.displayMode("", "copilot")) } - - @Test fun safeYoloShowsOnTheChipEvenThoughTheSwitcherFiltersIt() { - // The concurrent-clients carve-out (buildConfigUi) narrows the - // switcher only; the chip consults the unfiltered flavor catalog, - // like the web span. - assertEquals(PermissionMode.SafeYolo, PermissionModeChipRule.displayMode("safe-yolo", "codex")) - } } diff --git a/android/app/src/test/kotlin/app/hapi/companion/feature/newsession/NewSessionViewModelTest.kt b/android/app/src/test/kotlin/app/hapi/companion/feature/newsession/NewSessionViewModelTest.kt index 676f68aa..c93b6fa6 100644 --- a/android/app/src/test/kotlin/app/hapi/companion/feature/newsession/NewSessionViewModelTest.kt +++ b/android/app/src/test/kotlin/app/hapi/companion/feature/newsession/NewSessionViewModelTest.kt @@ -415,7 +415,7 @@ class NewSessionLogicTest { NewSessionForm(agent = "codex", permissionMode = "safe-yolo"), ) assertEquals("default", staleMode.permissionMode) - assertEquals(listOf("default", "read-only", "autoReview", "yolo"), app.hapi.protocol.catalog.PermissionModes.forLaunch("codex").map { it.wireId }) + assertEquals(listOf("default", "read-only", "autoReview", "yolo"), app.hapi.protocol.catalog.PermissionModes.forFlavor("codex").map { it.wireId }) assertEquals("safe-yolo", NewSessionLogic.sanitizeDraft(NewSessionForm(agent = "kimi", permissionMode = "safe-yolo")).permissionMode) } diff --git a/android/core/protocol/src/main/kotlin/app/hapi/protocol/catalog/Modes.kt b/android/core/protocol/src/main/kotlin/app/hapi/protocol/catalog/Modes.kt index 3a9e517f..17b42592 100644 --- a/android/core/protocol/src/main/kotlin/app/hapi/protocol/catalog/Modes.kt +++ b/android/core/protocol/src/main/kotlin/app/hapi/protocol/catalog/Modes.kt @@ -57,7 +57,7 @@ object PermissionModes { /** `CODEX_PERMISSION_MODES`. */ val CODEX = listOf( PermissionMode.Default, PermissionMode.ReadOnly, - PermissionMode.SafeYolo, PermissionMode.AutoReview, PermissionMode.Yolo, + PermissionMode.AutoReview, PermissionMode.Yolo, ) /** `GEMINI_PERMISSION_MODES` (kimi/copilot share the same list). */ @@ -103,10 +103,6 @@ object PermissionModes { /** `isPermissionModeAllowedForFlavor`. */ fun isAllowedForFlavor(mode: PermissionMode, flavor: String?): Boolean = mode in forFlavor(flavor) - - /** New Codex sessions use the shared runtime, without HAPI Safe Yolo. */ - fun forLaunch(flavor: String?): List = - forFlavor(flavor).filter { flavor != "codex" || it != PermissionMode.SafeYolo } } /** `CODEX_COLLABORATION_MODES` + labels. */ diff --git a/android/core/protocol/src/test/kotlin/app/hapi/protocol/catalog/CatalogTest.kt b/android/core/protocol/src/test/kotlin/app/hapi/protocol/catalog/CatalogTest.kt index 3934ab0d..7f821435 100644 --- a/android/core/protocol/src/test/kotlin/app/hapi/protocol/catalog/CatalogTest.kt +++ b/android/core/protocol/src/test/kotlin/app/hapi/protocol/catalog/CatalogTest.kt @@ -50,7 +50,7 @@ class CatalogTest { PermissionModes.forFlavor("claude").map { it.wireId } ) assertEquals( - listOf("default", "read-only", "safe-yolo", "autoReview", "yolo"), + listOf("default", "read-only", "autoReview", "yolo"), PermissionModes.forFlavor("codex").map { it.wireId } ) assertEquals( diff --git a/cli/src/codex/codexLocalLauncher.test.ts b/cli/src/codex/codexLocalLauncher.test.ts index 052f4705..1427960c 100644 --- a/cli/src/codex/codexLocalLauncher.test.ts +++ b/cli/src/codex/codexLocalLauncher.test.ts @@ -66,7 +66,7 @@ function createQueueStub() { } function createSessionStub( - permissionMode: 'default' | 'read-only' | 'safe-yolo' | 'yolo', + permissionMode: 'default' | 'read-only' | 'yolo', codexArgs?: string[], path = '/tmp/worktree', initialTranscriptPath: string | null = null, @@ -362,29 +362,6 @@ describe('codexLocalLauncher', () => { ]); }); - it('keeps sandbox escalation available in safe-yolo mode', async () => { - const { session } = createSessionStub('safe-yolo', [ - '--ask-for-approval', - 'never', - '--sandbox', - 'danger-full-access', - '--model', - 'o3' - ]); - - await codexLocalLauncher(session as never); - - expect(harness.launches).toHaveLength(1); - expect(harness.launches[0]?.codexArgs).toEqual([ - '--ask-for-approval', - 'on-request', - '--sandbox', - 'workspace-write', - '--model', - 'o3' - ]); - }); - it('does not emit a session warning while waiting for the first transcript path', async () => { const { session, sessionEvents, getLocalLaunchFailure } = createSessionStub('default', undefined, 'c:\\workspace\\project'); let releaseRunBarrier: (() => void) | undefined; diff --git a/cli/src/codex/codexLocalLauncher.ts b/cli/src/codex/codexLocalLauncher.ts index a70d8664..64a6bc70 100644 --- a/cli/src/codex/codexLocalLauncher.ts +++ b/cli/src/codex/codexLocalLauncher.ts @@ -77,7 +77,7 @@ export async function codexLocalLauncher(session: CodexSession): Promise<'switch const pendingExecWrappers = new Map(); const toolHookBridge = new CodexToolHookBridge(); const permissionMode = session.getPermissionMode(); - const managedPermissionMode = permissionMode === 'read-only' || permissionMode === 'safe-yolo' || permissionMode === 'yolo' + const managedPermissionMode = permissionMode === 'read-only' || permissionMode === 'yolo' ? permissionMode : null; const codexArgs = managedPermissionMode diff --git a/cli/src/codex/codexRemoteLauncher.ts b/cli/src/codex/codexRemoteLauncher.ts index f6ad1b0f..5224953b 100644 --- a/cli/src/codex/codexRemoteLauncher.ts +++ b/cli/src/codex/codexRemoteLauncher.ts @@ -604,7 +604,7 @@ class CodexRemoteLauncher extends RemoteLauncherBase { const getCurrentCodexPermissionMode = () => { const mode = session.getPermissionMode(); - return mode === 'default' || mode === 'read-only' || mode === 'safe-yolo' || mode === 'yolo' + return mode === 'default' || mode === 'read-only' || mode === 'yolo' ? mode : undefined; }; diff --git a/cli/src/codex/shared/launch.test.ts b/cli/src/codex/shared/launch.test.ts index e1bf65c7..c274e27b 100644 --- a/cli/src/codex/shared/launch.test.ts +++ b/cli/src/codex/shared/launch.test.ts @@ -45,7 +45,6 @@ describe('shared launch configuration', () => { it('does not interpret flags after -- as configuration', () => { expect(sharedLaunchConfig({ codexArgs: ['--', '--profile=explain'] }, '/tmp').tuiArgs).toEqual(['--', '--profile=explain']); expect(sharedLaunchConfig({ codexArgs: ['--', '-C', 'literal'] }, '/tmp').tuiArgs).toEqual(['--', '-C', 'literal']); - expect(() => sharedLaunchConfig({ permissionMode: 'safe-yolo' }, '/tmp')).toThrow('not a native shared'); }); it('maps HAPI autoReview to native auto-review in the workspace-write sandbox', () => { const result = sharedLaunchConfig({ permissionMode: 'autoReview', codexArgs: ['--yolo'] }, '/tmp'); diff --git a/cli/src/codex/shared/launch.ts b/cli/src/codex/shared/launch.ts index b76b8c15..74cda49c 100644 --- a/cli/src/codex/shared/launch.ts +++ b/cli/src/codex/shared/launch.ts @@ -10,7 +10,7 @@ import type { CodexAppServerClient } from '../codexAppServerClient'; export const SharedLaunchSchema = z.object({ startedBy: z.enum(['runner', 'terminal']).optional(), codexArgs: z.array(z.string()).optional(), - permissionMode: z.enum(['default', 'read-only', 'safe-yolo', 'autoReview', 'yolo']).optional(), + permissionMode: z.enum(['default', 'read-only', 'autoReview', 'yolo']).optional(), resumeSessionId: z.string().optional(), resumeLast: z.boolean().optional(), resumeAll: z.boolean().optional(), existingSessionId: z.string().optional(), /** Hub-preallocated stub — create/adopt, not reopen. */ @@ -46,7 +46,6 @@ export function resolveSharedCodex(): CodexCommand { export function sharedLaunchConfig(options: SharedLaunchOptions, cwd: string): { cwd: string; serverArgs: string[]; tuiArgs: string[]; threadParams: Record; } { - if (options.permissionMode === 'safe-yolo') throw new Error('safe-yolo is not a native shared permission mode. Choose default, read-only, autoReview, or yolo.'); const args = options.codexArgs ?? []; const serverArgs: string[] = []; let tuiArgs: string[] = []; diff --git a/cli/src/codex/shared/root.ts b/cli/src/codex/shared/root.ts index 4aa8429e..ecd630fd 100644 --- a/cli/src/codex/shared/root.ts +++ b/cli/src/codex/shared/root.ts @@ -32,7 +32,7 @@ export type RootHost = { end(root: SharedCodexRoot, nativeArchive?: boolean): Promise; }; const SettingsSchema = z.object({ - permissionMode: z.enum(['default', 'read-only', 'safe-yolo', 'autoReview', 'yolo']).optional(), + permissionMode: z.enum(['default', 'read-only', 'autoReview', 'yolo']).optional(), model: z.string().min(1).optional(), modelReasoningEffort: z.string().nullable().optional(), collaborationMode: z.enum(['default', 'plan']).optional(), serviceTier: z.enum(['fast', 'standard']).nullable().optional(), @@ -407,7 +407,6 @@ export class SharedCodexRoot { } async applySettings(raw: unknown): Promise<{ applied: RuntimeSettings }> { const config = SettingsSchema.parse(raw); - if (config.permissionMode === 'safe-yolo') throw new Error('safe-yolo is not a native shared permission mode; choose default, read-only, autoReview, or yolo'); if (Object.keys(config).length === 0) return { applied: this.settings }; if (typeof config.modelReasoningEffort === 'string') config.modelReasoningEffort = parseReasoningEffortValue(config.modelReasoningEffort); if (config.modelReasoningEffort === null) { diff --git a/cli/src/codex/utils/appServerConfig.test.ts b/cli/src/codex/utils/appServerConfig.test.ts index d45e677f..2a45177e 100644 --- a/cli/src/codex/utils/appServerConfig.test.ts +++ b/cli/src/codex/utils/appServerConfig.test.ts @@ -156,17 +156,6 @@ describe('appServerConfig', () => { }); }); - it('keeps supported escalation approvals for safe-yolo threads', () => { - const params = buildThreadStartParams({ - cwd: '/workspace/project', - mode: { permissionMode: 'safe-yolo', collaborationMode: 'default' }, - mcpServers - }); - - expect(params.sandbox).toBe('workspace-write'); - expect(params.approvalPolicy).toBe('on-request'); - }); - it('allows MCP elicitation without enabling sandbox prompts for read-only threads', () => { const params = buildThreadStartParams({ cwd: '/workspace/project', @@ -583,7 +572,7 @@ describe('appServerConfig', () => { threadId: 'thread-1', message: 'hello', cwd: '/workspace/project', - mode: { permissionMode: 'safe-yolo', model: 'o3', collaborationMode: 'default' }, + mode: { permissionMode: 'autoReview', model: 'o3', collaborationMode: 'default' }, cliOverrides: { sandbox: 'read-only', approvalPolicy: 'never' } }); diff --git a/cli/src/codex/utils/permissionHandler.test.ts b/cli/src/codex/utils/permissionHandler.test.ts index 8367a5f8..e52c5fc2 100644 --- a/cli/src/codex/utils/permissionHandler.test.ts +++ b/cli/src/codex/utils/permissionHandler.test.ts @@ -8,7 +8,7 @@ type FakeAgentState = { }; function createHarness( - mode: 'default' | 'read-only' | 'safe-yolo' | 'yolo', + mode: 'default' | 'read-only' | 'yolo', collaborationMode?: 'default' | 'plan' ) { let agentState: FakeAgentState = { @@ -91,23 +91,6 @@ describe('CodexPermissionHandler', () => { await expect(resultPromise).resolves.toMatchObject({ decision: 'approved' }); }); - it('auto-approves safe-yolo requests once', async () => { - const { handler, getAgentState } = createHarness('safe-yolo'); - - await expect(handler.handleToolCall('perm-1', 'CodexBash', { command: 'pwd' })).resolves.toEqual({ - decision: 'approved' - }); - - expect(getAgentState().requests).toEqual({}); - expect(getAgentState().completedRequests).toMatchObject({ - 'perm-1': { - tool: 'CodexBash', - status: 'approved', - decision: 'approved' - } - }); - }); - it('keeps default mode requests pending until a permission RPC arrives', async () => { const { handler, rpcHandlers, getAgentState } = createHarness('default'); const resultPromise = handler.handleToolCall('perm-1', 'CodexPatch', { grantRoot: '/tmp' }); diff --git a/cli/src/codex/utils/permissionModeConfig.test.ts b/cli/src/codex/utils/permissionModeConfig.test.ts index 98ec14e7..9cbb453d 100644 --- a/cli/src/codex/utils/permissionModeConfig.test.ts +++ b/cli/src/codex/utils/permissionModeConfig.test.ts @@ -9,12 +9,4 @@ describe('resolveCodexPermissionModeConfig', () => { sandboxPolicy: { type: 'workspaceWrite' } }); }); - - it('keeps safe-yolo escalation available with a supported approval policy', () => { - expect(resolveCodexPermissionModeConfig('safe-yolo')).toEqual({ - approvalPolicy: 'on-request', - sandbox: 'workspace-write', - sandboxPolicy: { type: 'workspaceWrite' } - }); - }); }); diff --git a/cli/src/codex/utils/permissionModeConfig.ts b/cli/src/codex/utils/permissionModeConfig.ts index bb750817..a080ec93 100644 --- a/cli/src/codex/utils/permissionModeConfig.ts +++ b/cli/src/codex/utils/permissionModeConfig.ts @@ -25,15 +25,6 @@ export function resolveCodexPermissionModeConfig(mode: CodexPermissionMode): Cod sandbox: 'read-only', sandboxPolicy: { type: 'readOnly' } }; - case 'safe-yolo': - return { - // Current Codex versions reject the removed `on-failure` policy. Keep - // escalation available through `on-request`; HAPI auto-approves these - // requests in safe-yolo mode. - approvalPolicy: 'on-request', - sandbox: 'workspace-write', - sandboxPolicy: { type: 'workspaceWrite' } - }; case 'yolo': return { approvalPolicy: 'never', diff --git a/cli/src/codex/utils/slashCommands.ts b/cli/src/codex/utils/slashCommands.ts index accd4706..0d03a457 100644 --- a/cli/src/codex/utils/slashCommands.ts +++ b/cli/src/codex/utils/slashCommands.ts @@ -318,7 +318,7 @@ export function resolveCodexSlashCommand( '- `/reasoning [level|default]` — show or set reasoning effort', '- `/personality [friendly|pragmatic|none]` — show or set response style (sticky on thread)', '- `/fast [on|off|status]` — toggle Fast mode (GPT-5.5 / GPT-5.4, ChatGPT login)', - '- `/permissions [default|read-only|safe-yolo|yolo]` — show or set permission mode', + '- `/permissions [default|read-only|autoReview|yolo]` — show or set permission mode', '', 'Custom `/commands` from `.codex/prompts` are expanded before sending.' ].join('\n') diff --git a/cli/src/modules/pingPeer/pingPeer.ts b/cli/src/modules/pingPeer/pingPeer.ts index 878bf14d..fe6b2ea3 100644 --- a/cli/src/modules/pingPeer/pingPeer.ts +++ b/cli/src/modules/pingPeer/pingPeer.ts @@ -65,7 +65,6 @@ export type PingPeerSessionSummary = { lifecycleState?: string | null piSessionId?: string summary?: { text?: string } | null - capabilities?: { concurrentClients?: boolean } | null } | null } @@ -811,9 +810,7 @@ export async function setPeerPermissionMode(options: SetPeerPermissionModeOption const live = await getSession(apiUrl, jwt, matched.id, http) // Hub treats a missing flavor as claude; mirror it so the allowed list matches. const flavor = live.metadata?.flavor ?? matched.metadata?.flavor ?? 'claude' - // Shared (concurrentClients) sessions cannot launch with Safe Yolo; same filter as the web picker. - const shared = Boolean(live.metadata?.capabilities?.concurrentClients) - const allowed: readonly string[] = getPermissionModesForFlavor(flavor).filter((m) => !shared || m !== 'safe-yolo') + const allowed: readonly string[] = getPermissionModesForFlavor(flavor) if (allowed.length === 0) { throw new PingPeerError('bad_args', `${flavor} sessions do not support permission modes`) } diff --git a/cli/src/modules/pingPeer/setPeerPermissionMode.test.ts b/cli/src/modules/pingPeer/setPeerPermissionMode.test.ts index a4a6c308..1242101a 100644 --- a/cli/src/modules/pingPeer/setPeerPermissionMode.test.ts +++ b/cli/src/modules/pingPeer/setPeerPermissionMode.test.ts @@ -60,12 +60,12 @@ describe('setPeerPermissionMode', () => { expect(error).toBeInstanceOf(PingPeerError) expect((error as PingPeerError).code).toBe('bad_args') - expect((error as PingPeerError).message).toContain('allowed: default, read-only, safe-yolo, autoReview, yolo') + expect((error as PingPeerError).message).toContain('allowed: default, read-only, autoReview, yolo') expect(http.post).toHaveBeenCalledTimes(1) }) - it('rejects Safe Yolo for a shared session without calling the hub', async () => { - const http = createHttpMock({ flavor: 'codex', active: false, shared: true }) + it('rejects Safe Yolo for Codex without calling the hub', async () => { + const http = createHttpMock({ flavor: 'codex', active: false }) await expect(setPeerPermissionMode({ ...base, sessionIdPrefix: sessionId, mode: 'safe-yolo', http: http as never })) .rejects.toMatchObject({ code: 'bad_args', message: "invalid mode 'safe-yolo' for codex; allowed: default, read-only, autoReview, yolo" }) diff --git a/docs/api/client-contract/rest.md b/docs/api/client-contract/rest.md index 866ec809..ffbee589 100644 --- a/docs/api/client-contract/rest.md +++ b/docs/api/client-contract/rest.md @@ -124,8 +124,6 @@ Source: `hub/src/web/routes/sessions.ts`; flavor gates in `shared/src/modes.ts` ownership mode. `/switch` returns HTTP 409 with `code: 'control_mode_not_applicable'`; do not offer takeover. The remote-only Codex configuration restrictions above do not apply to these sessions. -Shared Codex rejects `safe-yolo` even though it remains in the historical -Codex permission-mode schema; do not offer it for concurrent sessions. Shared `/clear` has no global `supersededBySessionId` change; clients other than the caller stay on the original thread. Fork may return an already-bound shared diff --git a/docs/guide/codex-shared-sessions.md b/docs/guide/codex-shared-sessions.md index 83a0e021..ddd44958 100644 --- a/docs/guide/codex-shared-sessions.md +++ b/docs/guide/codex-shared-sessions.md @@ -119,9 +119,8 @@ endpoints/tokens never go into HAPI session metadata or SSE. - Cold resume accepts root threads only. To revisit a child agent, resume its parent HAPI session; HAPI does not independently start a child execution. -- HAPI's legacy `safe-yolo` auto-approval policy is not a native Codex mode; - shared sessions reject it instead of silently weakening or misreporting it. - Use `default`, `read-only`, `autoReview`, or `yolo`. `autoReview` keeps the +- Codex permission modes are `default`, `read-only`, `autoReview`, and + `yolo`; HAPI's `safe-yolo` is not a native Codex mode. `autoReview` keeps the workspace-write sandbox and routes escalations to Codex's native auto-review (`approvals_reviewer = "auto_review"`, same as `--approve-for-me`). - Web-created new/fork roots preserve the standard native sandbox restrictions. diff --git a/hub/src/web/routes/sessions.test.ts b/hub/src/web/routes/sessions.test.ts index ed744e56..1c6ca8f4 100644 --- a/hub/src/web/routes/sessions.test.ts +++ b/hub/src/web/routes/sessions.test.ts @@ -1216,10 +1216,10 @@ describe('sessions routes', () => { ]) }) - it('rejects Safe Yolo for shared sessions', async () => { + it('rejects Safe Yolo for Codex', async () => { const session = createSession({ active: false, - metadata: { path: '/tmp/project', host: 'localhost', flavor: 'codex', capabilities: { concurrentClients: true } } + metadata: { path: '/tmp/project', host: 'localhost', flavor: 'codex' } }) const { app, applySessionConfigCalls } = createApp(session) @@ -1230,7 +1230,7 @@ describe('sessions routes', () => { }) expect(response.status).toBe(400) - expect(await response.json()).toEqual({ error: 'Safe Yolo is not available for shared sessions' }) + expect(await response.json()).toEqual({ error: 'Invalid permission mode for session flavor' }) expect(applySessionConfigCalls).toEqual([]) }) diff --git a/hub/src/web/routes/sessions.ts b/hub/src/web/routes/sessions.ts index 448d058c..82ef1761 100644 --- a/hub/src/web/routes/sessions.ts +++ b/hub/src/web/routes/sessions.ts @@ -601,9 +601,6 @@ export function createSessionsRoutes(getSyncEngine: () => SyncEngine | null): Ho if (!isPermissionModeAllowedForFlavor(mode, flavor)) { return c.json({ error: 'Invalid permission mode for session flavor' }, 400) } - if (mode === 'safe-yolo' && sessionResult.session.metadata?.capabilities?.concurrentClients) { - return c.json({ error: 'Safe Yolo is not available for shared sessions' }, 400) - } if (flavor === 'opencode' && mode === 'plan' && sessionResult.session.agentState?.controlledByUser === true && !sessionResult.session.metadata?.capabilities?.concurrentClients) { return c.json({ error: 'OpenCode plan mode is only supported for remote sessions' }, 409) } diff --git a/ios/Hapi/Features/NewSession/NewSessionModel.swift b/ios/Hapi/Features/NewSession/NewSessionModel.swift index 668f879e..8bc6e36c 100644 --- a/ios/Hapi/Features/NewSession/NewSessionModel.swift +++ b/ios/Hapi/Features/NewSession/NewSessionModel.swift @@ -374,7 +374,7 @@ final class NewSessionModel { return .managed } if NewSessionLogic.usesNativePermissionSelect(agent) { - return .nativeSelect(agent.launchPermissionModes.map { PermissionModeOption(mode: $0) }) + return .nativeSelect(agent.permissionModes.map { PermissionModeOption(mode: $0) }) } return .yoloToggle(nativeModeLabel: NewSessionLogic.hapiYoloNativeMode(for: agent)?.label) } diff --git a/ios/HapiTests/SessionConfigTests.swift b/ios/HapiTests/SessionConfigTests.swift index df5665b2..30f10d91 100644 --- a/ios/HapiTests/SessionConfigTests.swift +++ b/ios/HapiTests/SessionConfigTests.swift @@ -225,7 +225,6 @@ final class SessionConfigTests: XCTestCase { // Offline/terminal notices do not introduce a blanket UI authorization gate. harness.store.updateDetailLocal("config") { $0.metadata?.capabilities = nil } XCTAssertTrue(harness.model.config.controlledByUser) - XCTAssertTrue(harness.model.config.permissionModes.contains { $0.mode == .safeYolo }) } } diff --git a/ios/Packages/HapiKit/Sources/HapiClient/Chat/ChatInteractionState.swift b/ios/Packages/HapiKit/Sources/HapiClient/Chat/ChatInteractionState.swift index 90416e87..1b872299 100644 --- a/ios/Packages/HapiKit/Sources/HapiClient/Chat/ChatInteractionState.swift +++ b/ios/Packages/HapiKit/Sources/HapiClient/Chat/ChatInteractionState.swift @@ -300,9 +300,7 @@ public func buildSessionConfigState( active: active, controlledByUser: controlledByUser, permissionMode: detail?.permissionMode, - permissionModes: permissionModeOptions(forFlavor: flavor).filter { - detail?.metadata?.capabilities?.concurrentClients != true || $0.mode.rawValue != "safe-yolo" - }, + permissionModes: permissionModeOptions(forFlavor: flavor), collaborationMode: detail?.collaborationMode, canChangeCollaborationMode: detail != nil && flavor == "codex" && active && !controlledByUser, model: model, diff --git a/ios/Packages/HapiKit/Sources/HapiClient/NewSession/NewSessionForm.swift b/ios/Packages/HapiKit/Sources/HapiClient/NewSession/NewSessionForm.swift index cdffc144..6ac7500a 100644 --- a/ios/Packages/HapiKit/Sources/HapiClient/NewSession/NewSessionForm.swift +++ b/ios/Packages/HapiKit/Sources/HapiClient/NewSession/NewSessionForm.swift @@ -416,7 +416,7 @@ public enum NewSessionLogic { sessionType: draft.sessionType, worktreeName: draft.worktreeName ) - if !base.agent.launchPermissionModes.contains(base.permissionMode) { + if !base.agent.permissionModes.contains(base.permissionMode) { base.permissionMode = .default } return base diff --git a/ios/Packages/HapiKit/Sources/HapiProtocol/Catalog/PermissionModes.swift b/ios/Packages/HapiKit/Sources/HapiProtocol/Catalog/PermissionModes.swift index d3232bd0..667c02a3 100644 --- a/ios/Packages/HapiKit/Sources/HapiProtocol/Catalog/PermissionModes.swift +++ b/ios/Packages/HapiKit/Sources/HapiProtocol/Catalog/PermissionModes.swift @@ -82,11 +82,6 @@ public struct PermissionModeOption: Equatable, Sendable { } extension AgentFlavor { - /// New Codex sessions use the shared runtime, without HAPI Safe Yolo. - public var launchPermissionModes: [PermissionMode] { - permissionModes.filter { self != .codex || $0 != .safeYolo } - } - /// Permission modes offered for this flavor, in picker order. /// /// Ports the per-flavor arrays in `shared/src/modes.ts` @@ -95,7 +90,7 @@ extension AgentFlavor { public var permissionModes: [PermissionMode] { switch self { case .codex: - return [.default, .readOnly, .safeYolo, .autoReview, .yolo] + return [.default, .readOnly, .autoReview, .yolo] case .gemini, .kimi, .copilot: return [.default, .readOnly, .safeYolo, .yolo] case .grok: diff --git a/ios/Packages/HapiKit/Tests/HapiClientTests/NewSessionFormTests.swift b/ios/Packages/HapiKit/Tests/HapiClientTests/NewSessionFormTests.swift index 218971bb..e5844376 100644 --- a/ios/Packages/HapiKit/Tests/HapiClientTests/NewSessionFormTests.swift +++ b/ios/Packages/HapiKit/Tests/HapiClientTests/NewSessionFormTests.swift @@ -279,7 +279,7 @@ struct NewSessionLogicTests { NewSessionForm(agent: .codex, permissionMode: .safeYolo) ) #expect(staleMode.permissionMode == .default) - #expect(AgentFlavor.codex.launchPermissionModes == [.default, .readOnly, .autoReview, .yolo]) + #expect(AgentFlavor.codex.permissionModes == [.default, .readOnly, .autoReview, .yolo]) #expect(NewSessionLogic.sanitizeDraft(NewSessionForm(agent: .kimi, permissionMode: .safeYolo)).permissionMode == .safeYolo) } diff --git a/ios/Packages/HapiKit/Tests/HapiProtocolTests/CatalogTests.swift b/ios/Packages/HapiKit/Tests/HapiProtocolTests/CatalogTests.swift index 4b3890d7..c466dbe0 100644 --- a/ios/Packages/HapiKit/Tests/HapiProtocolTests/CatalogTests.swift +++ b/ios/Packages/HapiKit/Tests/HapiProtocolTests/CatalogTests.swift @@ -78,7 +78,7 @@ struct CatalogTests { @Test func codexFamilyFlavorsShareTheCodexModeSet() { let expected: [PermissionMode] = [.default, .readOnly, .safeYolo, .yolo] - #expect(permissionModes(forFlavor: "codex") == [.default, .readOnly, .safeYolo, .autoReview, .yolo]) + #expect(permissionModes(forFlavor: "codex") == [.default, .readOnly, .autoReview, .yolo]) #expect(permissionModes(forFlavor: "gemini") == expected) #expect(permissionModes(forFlavor: "kimi") == expected) #expect(permissionModes(forFlavor: "copilot") == expected) @@ -144,7 +144,7 @@ struct CatalogTests { @Test func optionListsCarryLabelAndTone() { let options = permissionModeOptions(forFlavor: "codex") - #expect(options.count == 5) + #expect(options.count == 4) #expect(options.first == PermissionModeOption(mode: .default)) #expect(options.first?.label == "Default") #expect(options.first?.tone == .neutral) diff --git a/shared/fixtures/catalogs/modes.json b/shared/fixtures/catalogs/modes.json index bdbfbfb4..41cf0c37 100644 --- a/shared/fixtures/catalogs/modes.json +++ b/shared/fixtures/catalogs/modes.json @@ -60,11 +60,6 @@ "mode": "read-only", "tone": "warning" }, - { - "label": "Safe Yolo", - "mode": "safe-yolo", - "tone": "warning" - }, { "label": "Auto", "mode": "autoReview", diff --git a/shared/src/modes.test.ts b/shared/src/modes.test.ts index e2e13abd..bd11f668 100644 --- a/shared/src/modes.test.ts +++ b/shared/src/modes.test.ts @@ -3,7 +3,6 @@ import { AGENT_FLAVORS, AgentFlavorSchema, CREATABLE_AGENT_FLAVORS, - getLaunchPermissionModesForFlavor, getPermissionModeLabel, getPermissionModeOptionsForFlavor, getPermissionModeTone, @@ -102,7 +101,7 @@ describe('isPermissionModeAllowedForFlavor', () => { }) test("codex offers autoReview at launch and mid-session", () => { - expect(getLaunchPermissionModesForFlavor('codex')).toEqual(['default', 'read-only', 'autoReview', 'yolo']) + expect(getPermissionModesForFlavor('codex')).toEqual(['default', 'read-only', 'autoReview', 'yolo']) expect(isPermissionModeAllowedForFlavor('autoReview', 'codex')).toBe(true) expect(isPermissionModeAllowedForFlavor('autoReview', 'gemini')).toBe(false) }) diff --git a/shared/src/modes.ts b/shared/src/modes.ts index 94d37bf5..8f5c0515 100644 --- a/shared/src/modes.ts +++ b/shared/src/modes.ts @@ -25,7 +25,7 @@ export type AgyPermissionMode = typeof AGY_PERMISSION_MODES[number] export const CLAUDE_PERMISSION_MODES = ['default', 'acceptEdits', 'auto', 'bypassPermissions', 'plan'] as const export type ClaudePermissionMode = typeof CLAUDE_PERMISSION_MODES[number] -export const CODEX_PERMISSION_MODES = ['default', 'read-only', 'safe-yolo', 'autoReview', 'yolo'] as const +export const CODEX_PERMISSION_MODES = ['default', 'read-only', 'autoReview', 'yolo'] as const export type CodexPermissionMode = typeof CODEX_PERMISSION_MODES[number] export const CODEX_COLLABORATION_MODES = ['default', 'plan'] as const @@ -177,11 +177,6 @@ export function isPermissionModeAllowedForFlavor(mode: PermissionMode, flavor?: return getPermissionModesForFlavor(flavor).includes(mode) } -/** New Codex sessions use the native shared runtime, without HAPI Safe Yolo. */ -export function getLaunchPermissionModesForFlavor(flavor?: string | null): readonly PermissionMode[] { - return getPermissionModesForFlavor(flavor).filter(mode => flavor !== 'codex' || mode !== 'safe-yolo') -} - export function getCodexCollaborationModeOptions(): CodexCollaborationModeOption[] { return CODEX_COLLABORATION_MODES.map((mode) => ({ mode, diff --git a/web/src/components/AssistantChat/HappyComposer.tsx b/web/src/components/AssistantChat/HappyComposer.tsx index ac091740..c6b06a45 100644 --- a/web/src/components/AssistantChat/HappyComposer.tsx +++ b/web/src/components/AssistantChat/HappyComposer.tsx @@ -309,7 +309,6 @@ export function HappyComposer(props: { /** Model for the context-window heuristic; see StatusBar.contextModel. */ contextModel?: string | null controlledByUser?: boolean - concurrentClients?: boolean agentFlavor?: string | null availableModelOptions?: Array<{ value: string | null; label: string; group?: string }> /** Full Pi model data with thinkingLevelMap for provider grouping + thinking level filtering */ @@ -418,7 +417,6 @@ export function HappyComposer(props: { contextWindow, contextModel, controlledByUser = false, - concurrentClients = false, agentFlavor, availableModelOptions, piModels, @@ -1020,8 +1018,8 @@ export function HappyComposer(props: { }, [switchDisabled, onSwitchToRemote, haptic]) const permissionModeOptions = useMemo( - () => getPermissionModeOptionsForFlavor(agentFlavor).filter(option => !concurrentClients || option.mode !== 'safe-yolo'), - [agentFlavor, concurrentClients] + () => getPermissionModeOptionsForFlavor(agentFlavor), + [agentFlavor] ) const collaborationModeOptions = useMemo( () => agentFlavor === 'codex' ? getCodexCollaborationModeOptions() : [], diff --git a/web/src/components/NewSession/PermissionField.tsx b/web/src/components/NewSession/PermissionField.tsx index c25d5b0a..e24f9550 100644 --- a/web/src/components/NewSession/PermissionField.tsx +++ b/web/src/components/NewSession/PermissionField.tsx @@ -1,7 +1,7 @@ import { getAgentConfigDescriptor, getPermissionModeLabel, - getLaunchPermissionModesForFlavor, + getPermissionModesForFlavor, resolveHapiYoloPermissionMode, type PermissionMode } from '@hapi/protocol' @@ -61,7 +61,7 @@ export function PermissionField(props: PermissionFieldProps) { } if (field.kind === 'select' && usesNativePermissionSelect(props.agent)) { - const options = getLaunchPermissionModesForFlavor(props.agent) + const options = getPermissionModesForFlavor(props.agent) return (