feat(codex): expose Approve for me as a permission mode (#2016) (#2090)

* feat(codex): expose Approve for me as a permission mode (#2016)

Add autoReview to the Codex permission catalog. It keeps the
workspace-write sandbox with on-request approvals and routes escalations
to Codex's native reviewer (approvals_reviewer = "auto_review"), the same
config as `hapi codex --approve-for-me`. It works at launch and mid-session
via thread/settings/update; explicit modes now reset the reviewer to user,
and sessions report autoReview from the native reviewer setting.

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

* test(native): expect Auto-review in the Codex permission lists (#2016)

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

* feat(modes): label autoReview as Auto, matching Claude's auto mode (#2016)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
SSU-WEI HUANG
2026-10-26 09:59:47 +08:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 60a4971087
commit 878ca2058c
17 changed files with 80 additions and 36 deletions
+7 -5
View File
@@ -32,7 +32,7 @@ export type RootHost = {
end(root: SharedCodexRoot, nativeArchive?: boolean): Promise<void>;
};
const SettingsSchema = z.object({
permissionMode: z.enum(['default', 'read-only', 'safe-yolo', 'yolo']).optional(),
permissionMode: z.enum(['default', 'read-only', 'safe-yolo', '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(),
@@ -264,7 +264,8 @@ export class SharedCodexRoot {
const sandbox = record(value.sandboxPolicy ?? value.sandbox).type;
this.settings = { ...this.settings, model: value.model,
modelReasoningEffort: string(value.effort ?? value.reasoningEffort) ?? null,
...(sandbox ? { permissionMode: sandbox === 'dangerFullAccess' ? 'yolo' : sandbox === 'readOnly' ? 'read-only' : 'default' } : {}),
...(sandbox ? { permissionMode: sandbox === 'dangerFullAccess' ? 'yolo' : sandbox === 'readOnly' ? 'read-only'
: this.settingsNative.approvalsReviewer === 'auto_review' ? 'autoReview' : 'default' } : {}),
...('serviceTier' in value ? { serviceTier: value.serviceTier === 'priority' ? 'fast' : 'standard' } : {}),
...('collaborationMode' in value ? { collaborationMode: record(value.collaborationMode).mode === 'plan' ? 'plan' as const : 'default' as const } : {})
};
@@ -405,7 +406,7 @@ 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, or yolo');
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) {
@@ -427,7 +428,8 @@ export class SharedCodexRoot {
...(config.personality ? { personality: config.personality } : {}),
...(config.modelReasoningEffort !== undefined ? { effort: config.modelReasoningEffort } : {}),
...(config.serviceTier !== undefined ? { serviceTier: config.serviceTier === 'fast' ? 'priority' : null } : {}),
...(permission ? { approvalPolicy: permission.approvalPolicy, sandboxPolicy: permission.sandboxPolicy } : {}),
...(permission ? { approvalPolicy: permission.approvalPolicy, sandboxPolicy: permission.sandboxPolicy,
approvalsReviewer: config.permissionMode === 'autoReview' ? 'auto_review' : 'user' } : {}),
...(config.collaborationMode ? { collaborationMode: { mode: config.collaborationMode, settings: {
model: config.model ?? this.settings.model, reasoning_effort: config.modelReasoningEffort ?? this.settings.modelReasoningEffort,
developer_instructions: null
@@ -576,7 +578,7 @@ export class SharedCodexRoot {
if (command === '/exit') { this.notice('/exit closes a native terminal, not this Web view. Use End session to archive this conversation.'); return null; }
if (!command.startsWith('/')) return text;
const slash = resolveCodexSlashCommand(text, { commands: await listSlashCommands('codex', this.bootstrap.workingDirectory),
permissionMode: this.settings.permissionMode === 'yolo' ? 'yolo' : this.settings.permissionMode === 'read-only' ? 'read-only' : 'default',
permissionMode: this.settings.permissionMode === 'yolo' || this.settings.permissionMode === 'read-only' || this.settings.permissionMode === 'autoReview' ? this.settings.permissionMode : 'default',
collaborationMode: this.settings.collaborationMode ?? 'default', model: this.settings.model ?? undefined,
modelReasoningEffort: parseReasoningEffortValue(this.settings.modelReasoningEffort), serviceTier: this.settings.serviceTier });
if (slash.kind === 'passthrough') return text;