Files
hapi/shared/src/modes.ts
T
ffd950a220 fix(codex): drop safe-yolo from Codex permission modes (#2183)
* fix(codex): drop safe-yolo from Codex permission modes

Every Codex session runs on the native shared runtime (runCodex is
runSharedCodex), which rejects safe-yolo. The mode stayed in
CODEX_PERMISSION_MODES and six filters hid it again in the web picker,
composer, hub permission-mode route, hapi set-mode and both native
apps. API callers that validate against the flavor catalog (e.g. a
spawn with permissionMode: 'safe-yolo') still passed it through and
only failed inside the runner.

Remove it from the Codex catalog (shared, iOS, Android, fixture) and
delete the filters. autoReview is the native replacement. Safe Yolo
stays for Gemini, Kimi and Copilot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ios): Codex option list now has four modes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:12:55 +08:00

233 lines
7.9 KiB
TypeScript

import { z } from 'zod'
/**
* @description The legacy payload type identifier used for all generic agent messages.
* Changing this value will affect the communication schema between CLI, Hub, and Web.
* A migration plan is required if this literal is ever modified.
*/
export const AGENT_MESSAGE_PAYLOAD_TYPE = 'codex' as const
export const AGENT_FLAVORS = ['agy', 'claude', 'codex', 'dsh', 'copilot', 'cursor', 'gemini', 'grok', 'kimi', 'opencode', 'pi'] as const
export type AgentFlavor = typeof AGENT_FLAVORS[number]
export const AgentFlavorSchema = z.enum(AGENT_FLAVORS)
// Flavors offered when CREATING a new session. Gemini CLI is intentionally
// excluded: Google sunset the consumer Gemini CLI (2026-06-18) so it can no
// longer be launched. It is kept in AGENT_FLAVORS / AgentFlavorSchema above so
// existing stored Gemini sessions still validate and remain viewable.
export const CREATABLE_AGENT_FLAVORS: readonly AgentFlavor[] = AGENT_FLAVORS.filter(
(flavor) => flavor !== 'gemini'
)
export const AGY_PERMISSION_MODES = ['request-review', 'always-proceed'] as const
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', 'autoReview', 'yolo'] as const
export type CodexPermissionMode = typeof CODEX_PERMISSION_MODES[number]
export const CODEX_COLLABORATION_MODES = ['default', 'plan'] as const
export type CodexCollaborationMode = typeof CODEX_COLLABORATION_MODES[number]
export const GEMINI_PERMISSION_MODES = ['default', 'read-only', 'safe-yolo', 'yolo'] as const
export type GeminiPermissionMode = typeof GEMINI_PERMISSION_MODES[number]
export const KIMI_PERMISSION_MODES = ['default', 'read-only', 'safe-yolo', 'yolo'] as const
export type KimiPermissionMode = typeof KIMI_PERMISSION_MODES[number]
export const COPILOT_PERMISSION_MODES = ['default', 'read-only', 'safe-yolo', 'yolo'] as const
export type CopilotPermissionMode = typeof COPILOT_PERMISSION_MODES[number]
export const GROK_PERMISSION_MODES = ['default', 'auto', 'plan', 'bypassPermissions'] as const
export type GrokPermissionMode = typeof GROK_PERMISSION_MODES[number]
export const OPENCODE_PERMISSION_MODES = ['default', 'plan', 'yolo'] as const
export type OpencodePermissionMode = typeof OPENCODE_PERMISSION_MODES[number]
export const CURSOR_PERMISSION_MODES = ['default', 'plan', 'ask', 'debug', 'autoReview', 'yolo'] as const
export type CursorPermissionMode = typeof CURSOR_PERMISSION_MODES[number]
export const PERMISSION_MODES = [
'default',
'acceptEdits',
'auto',
'bypassPermissions',
'plan',
'ask',
'debug',
'autoReview',
'read-only',
'safe-yolo',
'yolo',
'request-review',
'always-proceed'
] as const
export type PermissionMode = typeof PERMISSION_MODES[number]
export const PERMISSION_MODE_LABELS: Record<PermissionMode, string> = {
default: 'Default',
acceptEdits: 'Accept Edits',
auto: 'Auto',
plan: 'Plan Mode',
ask: 'Ask Mode',
debug: 'Debug Mode',
autoReview: 'Auto',
bypassPermissions: 'Yolo',
'read-only': 'Read Only',
'safe-yolo': 'Safe Yolo',
yolo: 'Yolo',
'request-review': 'Request Review',
'always-proceed': 'Always Proceed'
}
export type PermissionModeTone = 'neutral' | 'info' | 'warning' | 'danger'
export const PERMISSION_MODE_TONES: Record<PermissionMode, PermissionModeTone> = {
default: 'neutral',
acceptEdits: 'warning',
auto: 'warning',
plan: 'info',
ask: 'info',
debug: 'info',
autoReview: 'warning',
bypassPermissions: 'danger',
'read-only': 'warning',
'safe-yolo': 'warning',
yolo: 'danger',
'request-review': 'neutral',
'always-proceed': 'danger'
}
export type PermissionModeOption = {
mode: PermissionMode
label: string
tone: PermissionModeTone
}
export type CodexCollaborationModeOption = {
mode: CodexCollaborationMode
label: string
}
export const CODEX_COLLABORATION_MODE_LABELS: Record<CodexCollaborationMode, string> = {
default: 'Default',
plan: 'Plan'
}
export function getPermissionModeLabel(mode: PermissionMode): string {
return PERMISSION_MODE_LABELS[mode]
}
export function getPermissionModeTone(mode: PermissionMode): PermissionModeTone {
return PERMISSION_MODE_TONES[mode]
}
export function getCodexCollaborationModeLabel(mode: CodexCollaborationMode): string {
return CODEX_COLLABORATION_MODE_LABELS[mode]
}
export function getPermissionModesForFlavor(flavor?: string | null): readonly PermissionMode[] {
if (flavor === 'codex') {
return CODEX_PERMISSION_MODES
}
if (flavor === 'gemini') {
return GEMINI_PERMISSION_MODES
}
if (flavor === 'kimi') {
return KIMI_PERMISSION_MODES
}
if (flavor === 'dsh') {
return []
}
if (flavor === 'copilot') {
return COPILOT_PERMISSION_MODES
}
if (flavor === 'grok') {
return GROK_PERMISSION_MODES
}
if (flavor === 'opencode') {
return OPENCODE_PERMISSION_MODES
}
if (flavor === 'agy') {
return AGY_PERMISSION_MODES
}
if (flavor === 'cursor') {
return CURSOR_PERMISSION_MODES
}
if (flavor === 'pi') {
// Pi RPC mode has no runtime permission switching (always auto-approve);
// no permission modes are offered.
return []
}
return CLAUDE_PERMISSION_MODES
}
export function getPermissionModeOptionsForFlavor(flavor?: string | null): PermissionModeOption[] {
return getPermissionModesForFlavor(flavor).map((mode) => ({
mode,
label: getPermissionModeLabel(mode),
tone: getPermissionModeTone(mode)
}))
}
export function isPermissionModeAllowedForFlavor(mode: PermissionMode, flavor?: string | null): boolean {
return getPermissionModesForFlavor(flavor).includes(mode)
}
export function getCodexCollaborationModeOptions(): CodexCollaborationModeOption[] {
return CODEX_COLLABORATION_MODES.map((mode) => ({
mode,
label: getCodexCollaborationModeLabel(mode)
}))
}
/**
* Flavors that can deliver a queued message into an active turn on demand
* (per-message "Steer" from the waiting queue), without waiting for full-turn end.
*
* Steer = soft mid-turn delivery (same idea as Cursor GUI default "Send"):
* - Pi: native steer over the Pi runtime (first-class since #1466)
* - Codex: app-server `turn/steer` (true mid-turn inject)
* - Cursor ACP: concurrent `session/prompt` soft-send (no cancel). Legacy
* stream-json Cursor sessions are NOT steerable — gate with
* {@link isSteeringSupportedForSession}.
*
* Claude / others: not supported (no reachable soft-steer path) — UI hides Steer.
*/
export const STEERING_SUPPORTED_FLAVORS = ['codex', 'cursor', 'pi'] as const
export function isSteeringSupportedForFlavor(flavor?: string | null): boolean {
return (STEERING_SUPPORTED_FLAVORS as readonly string[]).includes(flavor ?? '')
}
/**
* Session-aware steer gate. Prefer this over {@link isSteeringSupportedForFlavor}
* when metadata is available: legacy Cursor stream-json sessions advertise
* flavor `cursor` but cannot steer.
*
* Matches CLI legacy detection: explicit `stream-json`, or a pre-ACP session
* that has `cursorSessionId` without `cursorSessionProtocol: 'acp'`.
*/
export function isSteeringSupportedForSession(metadata?: {
flavor?: string | null
cursorSessionId?: string | null
cursorSessionProtocol?: 'acp' | 'stream-json' | null
} | null): boolean {
if (metadata?.flavor === 'codex' || metadata?.flavor === 'pi') {
return true
}
if (metadata?.flavor !== 'cursor') {
return false
}
if (metadata.cursorSessionProtocol === 'stream-json') {
return false
}
if (!metadata.cursorSessionProtocol && metadata.cursorSessionId) {
return false
}
return true
}