mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-07 01:21:37 +00:00
* 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>
233 lines
7.9 KiB
TypeScript
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
|
|
}
|