Files
hapi/ios/Packages/HapiKit/Sources/HapiProtocol/Catalog/PermissionModes.swift
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

193 lines
6.1 KiB
Swift

import Foundation
/// Visual weight of a permission mode in pickers.
///
/// Ports `PermissionModeTone` (`shared/src/modes.ts`).
public enum PermissionModeTone: String, Codable, Sendable {
case neutral
case info
case warning
case danger
}
/// Union of every flavor's permission mode ids.
///
/// Ports `PERMISSION_MODES` (`shared/src/modes.ts:52-66`); declaration order
/// matches the TS array. Labels and tones port
/// `PERMISSION_MODE_LABELS` / `PERMISSION_MODE_TONES` verbatim (English —
/// localization is an M5 concern layered above the catalog).
public enum PermissionMode: String, Codable, CaseIterable, Sendable {
case `default`
case acceptEdits
case auto
case bypassPermissions
case plan
case ask
case debug
case autoReview
case readOnly = "read-only"
case safeYolo = "safe-yolo"
case yolo
case requestReview = "request-review"
case alwaysProceed = "always-proceed"
public var label: String {
switch self {
case .default: return "Default"
case .acceptEdits: return "Accept Edits"
case .auto: return "Auto"
case .bypassPermissions: return "Yolo"
case .plan: return "Plan Mode"
case .ask: return "Ask Mode"
case .debug: return "Debug Mode"
case .autoReview: return "Auto"
case .readOnly: return "Read Only"
case .safeYolo: return "Safe Yolo"
case .yolo: return "Yolo"
case .requestReview: return "Request Review"
case .alwaysProceed: return "Always Proceed"
}
}
public var tone: PermissionModeTone {
switch self {
case .default: return .neutral
case .acceptEdits: return .warning
case .auto: return .warning
case .bypassPermissions: return .danger
case .plan: return .info
case .ask: return .info
case .debug: return .info
case .autoReview: return .warning
case .readOnly: return .warning
case .safeYolo: return .warning
case .yolo: return .danger
case .requestReview: return .neutral
case .alwaysProceed: return .danger
}
}
}
/// A picker-ready permission mode entry (`PermissionModeOption`).
public struct PermissionModeOption: Equatable, Sendable {
public let mode: PermissionMode
public let label: String
public let tone: PermissionModeTone
public init(mode: PermissionMode) {
self.mode = mode
self.label = mode.label
self.tone = mode.tone
}
}
extension AgentFlavor {
/// Permission modes offered for this flavor, in picker order.
///
/// Ports the per-flavor arrays in `shared/src/modes.ts`
/// (`getPermissionModesForFlavor`): unknown flavors fall back to the
/// Claude set, and Pi offers none (its RPC mode always auto-approves).
public var permissionModes: [PermissionMode] {
switch self {
case .codex:
return [.default, .readOnly, .autoReview, .yolo]
case .gemini, .kimi, .copilot:
return [.default, .readOnly, .safeYolo, .yolo]
case .grok:
return [.default, .auto, .plan, .bypassPermissions]
case .opencode:
return [.default, .plan, .yolo]
case .dsh:
return []
case .agy:
return [.requestReview, .alwaysProceed]
case .cursor:
return [.default, .plan, .ask, .debug, .autoReview, .yolo]
case .pi:
return []
case .claude, .other:
return [.default, .acceptEdits, .auto, .bypassPermissions, .plan]
}
}
}
/// String-based mirror of `getPermissionModesForFlavor(flavor)`:
/// `nil` and unknown flavors yield the Claude set.
public func permissionModes(forFlavor flavor: String?) -> [PermissionMode] {
AgentFlavor(rawValue: flavor ?? "claude").permissionModes
}
/// Ports `getPermissionModeOptionsForFlavor(flavor)`.
public func permissionModeOptions(forFlavor flavor: String?) -> [PermissionModeOption] {
permissionModes(forFlavor: flavor).map(PermissionModeOption.init(mode:))
}
/// Ports `isPermissionModeAllowedForFlavor(mode, flavor)`.
public func isPermissionModeAllowed(_ mode: PermissionMode, forFlavor flavor: String?) -> Bool {
permissionModes(forFlavor: flavor).contains(mode)
}
// MARK: - Codex collaboration modes
/// Ports `CODEX_COLLABORATION_MODES` + labels (`shared/src/modes.ts`).
public enum CodexCollaborationMode: String, Codable, CaseIterable, Sendable {
case `default`
case plan
public var label: String {
switch self {
case .default: return "Default"
case .plan: return "Plan"
}
}
}
// MARK: - Copilot agent modes
/// Ports `COPILOT_AGENT_MODES` + labels (`shared/src/copilotModes.ts`).
///
/// Decoding accepts the legacy `"fleet"` value and coerces it to
/// `.interactive`, mirroring `CopilotAgentModeSchema` (`shared/src/schemas.ts:9-12`).
public enum CopilotAgentMode: String, CaseIterable, Sendable {
case interactive
case plan
case autopilot
public var label: String {
switch self {
case .interactive: return "Interactive"
case .plan: return "Plan"
case .autopilot: return "Autopilot"
}
}
/// Ports `normalizeCopilotAgentMode`: `"fleet"` and anything invalid
/// coerce to `.interactive`.
public static func normalize(_ value: String?) -> CopilotAgentMode {
guard let value, value != "fleet" else { return .interactive }
return CopilotAgentMode(rawValue: value) ?? .interactive
}
}
extension CopilotAgentMode: Codable {
public init(from decoder: Decoder) throws {
let raw = try decoder.singleValueContainer().decode(String.self)
if raw == "fleet" {
self = .interactive
return
}
guard let mode = CopilotAgentMode(rawValue: raw) else {
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "Unknown CopilotAgentMode '\(raw)'"
))
}
self = mode
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(rawValue)
}
}