mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-08 01:43:07 +00:00
* 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:
co-authored by
Claude Opus 5.5
parent
60a4971087
commit
878ca2058c
@@ -47,6 +47,11 @@ describe('shared launch configuration', () => {
|
||||
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');
|
||||
expect(result.threadParams).toMatchObject({ approvalPolicy: 'on-request', approvalsReviewer: 'auto_review', sandbox: 'workspace-write' });
|
||||
expect(result.serverArgs).toEqual(['-c', 'approval_policy="on-request"', '-c', 'sandbox_mode="workspace-write"', '-c', 'approvals_reviewer="auto_review"']);
|
||||
});
|
||||
it('preserves launch-time provider and automatic-review policy', () => {
|
||||
const result = sharedLaunchConfig({ codexArgs: ['--oss', '--local-provider', 'ollama', '--approve-for-me'] }, '/tmp');
|
||||
expect(result.threadParams).toMatchObject({ modelProvider: 'ollama', approvalsReviewer: 'auto_review', sandbox: 'workspace-write' });
|
||||
|
||||
@@ -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', 'yolo']).optional(),
|
||||
permissionMode: z.enum(['default', 'read-only', 'safe-yolo', '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,7 @@ export function resolveSharedCodex(): CodexCommand {
|
||||
export function sharedLaunchConfig(options: SharedLaunchOptions, cwd: string): {
|
||||
cwd: string; serverArgs: string[]; tuiArgs: string[]; threadParams: Record<string, unknown>;
|
||||
} {
|
||||
if (options.permissionMode === 'safe-yolo') throw new Error('safe-yolo is not a native shared permission mode. Choose default, read-only, yolo, or native --approve-for-me.');
|
||||
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[] = [];
|
||||
@@ -123,12 +123,13 @@ export function sharedLaunchConfig(options: SharedLaunchOptions, cwd: string): {
|
||||
if (extraDirs.length) config['sandbox_workspace_write.writable_roots'] = extraDirs.map(path => resolve(directory, path));
|
||||
if (provider) config.model_provider = provider;
|
||||
const permission = options.permissionMode ? resolveCodexPermissionModeConfig(options.permissionMode) : undefined;
|
||||
const reviewer = options.permissionMode === 'autoReview' ? 'auto_review' : 'user';
|
||||
if (permission) {
|
||||
// Explicit HAPI permission selection wins on every launch surface:
|
||||
// initial thread, attached TUI, and later native /new threads.
|
||||
config.approval_policy = permission.approvalPolicy;
|
||||
config.sandbox_mode = permission.sandbox;
|
||||
config.approvals_reviewer = 'user';
|
||||
config.approvals_reviewer = reviewer;
|
||||
}
|
||||
// The attached TUI resumes a remote task, which rejects permission flags.
|
||||
// The initial thread already has their resolved policy; inherit it there.
|
||||
@@ -145,7 +146,7 @@ export function sharedLaunchConfig(options: SharedLaunchOptions, cwd: string): {
|
||||
...(overrides.approvalPolicy ? { approvalPolicy: overrides.approvalPolicy } : {}),
|
||||
...(overrides.sandbox ? { sandbox: overrides.sandbox } : {}),
|
||||
...(autoReview ? { approvalPolicy: 'on-request', approvalsReviewer: 'auto_review', sandbox: 'workspace-write' } : {}),
|
||||
...(permission ? { approvalPolicy: permission.approvalPolicy, approvalsReviewer: 'user', sandbox: permission.sandbox } : {}),
|
||||
...(permission ? { approvalPolicy: permission.approvalPolicy, approvalsReviewer: reviewer, sandbox: permission.sandbox } : {}),
|
||||
config: { ...config, ...(options.modelReasoningEffort ? { model_reasoning_effort: options.modelReasoningEffort } : {}) },
|
||||
developerInstructions: getCodexSystemPrompt()
|
||||
}
|
||||
|
||||
@@ -201,6 +201,16 @@ async function completePlan(f: Awaited<ReturnType<typeof fixture>>, status = 'co
|
||||
return codexPlanProposalId('thread', turn.id, 'plan-item');
|
||||
}
|
||||
|
||||
describe('shared permission modes', () => {
|
||||
it('switches native auto-review on and back to the human reviewer', async () => {
|
||||
const f = await fixture();
|
||||
expect((await f.root.applySettings({ permissionMode: 'autoReview' })).applied.permissionMode).toBe('autoReview');
|
||||
expect(f.root.client).toMatchObject({ settings: { approvalPolicy: 'on-request', approvalsReviewer: 'auto_review', sandboxPolicy: { type: 'workspaceWrite' } } });
|
||||
expect((await f.root.applySettings({ permissionMode: 'default' })).applied.permissionMode).toBe('default');
|
||||
expect(f.root.client).toMatchObject({ settings: { approvalsReviewer: 'user' } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('shared plan actions', () => {
|
||||
it('applies remote change_title as metadata.name then lets native terminal rename win', async () => {
|
||||
const f = await fixture();
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user