auto approve in yolo mode for codex

This commit is contained in:
weishu
2026-03-31 18:51:34 +08:00
parent ca7cb9ac90
commit 4248540fb4
3 changed files with 207 additions and 11 deletions
+161
View File
@@ -66,6 +66,62 @@ function createHarness() {
};
}
function createHarnessWithMode(getPermissionMode: () => 'default' | 'read-only' | 'safe-yolo' | 'yolo') {
let agentState: FakeAgentState = {
requests: {},
completedRequests: {}
};
const rpcHandlers = new Map<string, (params: unknown) => Promise<unknown> | unknown>();
let permissionHandler: ((request: PermissionRequest) => void) | null = null;
const respondCalls: Array<{
sessionId: string;
request: PermissionRequest;
response: PermissionResponse;
}> = [];
const session = {
rpcHandlerManager: {
registerHandler(method: string, handler: (params: unknown) => Promise<unknown> | unknown) {
rpcHandlers.set(method, handler);
}
},
updateAgentState(handler: (state: FakeAgentState) => FakeAgentState) {
agentState = handler(agentState);
}
} as unknown as ApiSessionClient;
const backend: AgentBackend = {
async initialize() {},
async newSession() {
return 'agent-session';
},
async prompt() {},
async cancelPrompt() {},
async respondToPermission(sessionId, request, response) {
respondCalls.push({ sessionId, request, response });
},
onPermissionRequest(handler) {
permissionHandler = handler;
},
async disconnect() {}
};
new PermissionAdapter(session, backend, getPermissionMode);
return {
rpcHandlers,
respondCalls,
getAgentState: () => agentState,
emitPermissionRequest(request: PermissionRequest) {
if (!permissionHandler) {
throw new Error('Permission handler was not registered');
}
permissionHandler(request);
}
};
}
async function flushAsyncWork(): Promise<void> {
await Promise.resolve();
await Promise.resolve();
@@ -193,4 +249,109 @@ describe('PermissionAdapter', () => {
}
});
});
it('auto-approves non-title tools once in safe-yolo mode', async () => {
const harness = createHarnessWithMode(() => 'safe-yolo');
harness.emitPermissionRequest(buildRequest({
id: 'perm-safe',
toolCallId: 'perm-safe',
title: 'Read'
}));
await flushAsyncWork();
expect(harness.respondCalls).toEqual([
{
sessionId: 'session-1',
request: expect.objectContaining({
id: 'perm-safe',
title: 'Read'
}),
response: { outcome: 'selected', optionId: 'allow-once' }
}
]);
expect(harness.getAgentState().requests).toEqual({});
expect(harness.getAgentState().completedRequests).toMatchObject({
'perm-safe': {
tool: 'Read',
status: 'approved',
decision: 'approved'
}
});
});
it('auto-approves non-title tools for the session in yolo mode', async () => {
const harness = createHarnessWithMode(() => 'yolo');
harness.emitPermissionRequest(buildRequest({
id: 'perm-yolo',
toolCallId: 'perm-yolo',
title: 'Read'
}));
await flushAsyncWork();
expect(harness.respondCalls).toEqual([
{
sessionId: 'session-1',
request: expect.objectContaining({
id: 'perm-yolo',
title: 'Read'
}),
response: { outcome: 'selected', optionId: 'allow-always' }
}
]);
expect(harness.getAgentState().requests).toEqual({});
expect(harness.getAgentState().completedRequests).toMatchObject({
'perm-yolo': {
tool: 'Read',
status: 'approved',
decision: 'approved_for_session'
}
});
});
it('auto-approves read-only non-write tools but keeps writes pending', async () => {
const harness = createHarnessWithMode(() => 'read-only');
harness.emitPermissionRequest(buildRequest({
id: 'perm-read-only-read',
toolCallId: 'perm-read-only-read',
title: 'Read'
}));
await flushAsyncWork();
expect(harness.respondCalls).toEqual([
{
sessionId: 'session-1',
request: expect.objectContaining({
id: 'perm-read-only-read',
title: 'Read'
}),
response: { outcome: 'selected', optionId: 'allow-once' }
}
]);
expect(harness.getAgentState().completedRequests).toMatchObject({
'perm-read-only-read': {
tool: 'Read',
status: 'approved',
decision: 'approved'
}
});
harness.emitPermissionRequest(buildRequest({
id: 'perm-read-only-write',
toolCallId: 'perm-read-only-write',
title: 'Patch'
}));
expect(harness.respondCalls).toHaveLength(1);
expect(harness.getAgentState().requests).toMatchObject({
'perm-read-only-write': {
tool: 'Patch'
}
});
});
});
+5 -3
View File
@@ -1,5 +1,5 @@
import type { AgentBackend, PermissionRequest, PermissionResponse } from './types';
import type { AgentState } from '@/api/types';
import type { AgentState, SessionPermissionMode } from '@/api/types';
import type { ApiSessionClient } from '@/api/apiSession';
import { logger } from '@/ui/logger';
import { deriveToolName } from '@/agent/utils';
@@ -41,7 +41,8 @@ export class PermissionAdapter {
constructor(
private readonly session: ApiSessionClient,
private readonly backend: AgentBackend
private readonly backend: AgentBackend,
private readonly getPermissionMode?: () => SessionPermissionMode | undefined
) {
this.backend.onPermissionRequest((request) => this.handlePermissionRequest(request));
this.session.rpcHandlerManager.registerHandler<PermissionResponseMessage, void>(
@@ -59,7 +60,8 @@ export class PermissionAdapter {
rawInput: request.rawInput
});
const input = deriveToolInput(request);
const autoDecision = resolveToolAutoApprovalDecision(undefined, toolName, request.toolCallId);
const mode = this.getPermissionMode?.();
const autoDecision = resolveToolAutoApprovalDecision(mode, toolName, request.toolCallId);
if (autoDecision) {
void this.autoApproveRequest(request, toolName, input, autoDecision);
+41 -8
View File
@@ -1,4 +1,4 @@
import type { AgentState } from '@/api/types';
import type { AgentState, SessionPermissionMode } from '@/api/types';
import { logger } from '@/ui/logger';
import { MessageQueue2 } from '@/utils/MessageQueue2';
import { hashObject } from '@/utils/deterministicJson';
@@ -12,6 +12,8 @@ import { registerKillSessionHandler } from '@/claude/registerKillSessionHandler'
import { bootstrapSession } from '@/agent/sessionFactory';
import { formatMessageWithAttachments } from '@/utils/attachmentFormatter';
import { getInvokedCwd } from '@/utils/invokedCwd';
import { PermissionModeSchema } from '@hapi/protocol/schemas';
import { isPermissionModeAllowedForFlavor } from '@hapi/protocol';
function emitReadyIfIdle(props: {
queueSize: () => number;
@@ -28,12 +30,13 @@ function emitReadyIfIdle(props: {
export async function runAgentSession(opts: {
agentType: string;
startedBy?: 'runner' | 'terminal';
permissionMode?: SessionPermissionMode;
}): Promise<void> {
const workingDirectory = getInvokedCwd();
const initialState: AgentState = {
controlledByUser: false
};
const { session } = await bootstrapSession({
const { session, sessionInfo } = await bootstrapSession({
flavor: opts.agentType,
startedBy: opts.startedBy ?? 'terminal',
workingDirectory,
@@ -52,10 +55,12 @@ export async function runAgentSession(opts: {
messageQueue.push(formattedText, {});
});
let currentPermissionMode: SessionPermissionMode = opts.permissionMode ?? sessionInfo.permissionMode ?? 'default';
const backend: AgentBackend = AgentRegistry.create(opts.agentType);
await backend.initialize();
const permissionAdapter = new PermissionAdapter(session, backend);
const permissionAdapter = new PermissionAdapter(session, backend, () => currentPermissionMode);
const happyServer = await startHappyServer(session);
const bridgeCommand = getHappyCliCommand(['mcp', '--url', happyServer.url]);
@@ -77,9 +82,37 @@ export async function runAgentSession(opts: {
let shouldExit = false;
let waitAbortController: AbortController | null = null;
session.keepAlive(thinking, 'remote');
const syncKeepAlive = () => {
session.keepAlive(thinking, 'remote', {
permissionMode: currentPermissionMode
});
};
const resolvePermissionMode = (value: unknown): SessionPermissionMode => {
const parsed = PermissionModeSchema.safeParse(value);
if (!parsed.success || !isPermissionModeAllowedForFlavor(parsed.data, opts.agentType)) {
throw new Error('Invalid permission mode');
}
return parsed.data as SessionPermissionMode;
};
session.rpcHandlerManager.registerHandler('set-session-config', async (payload: unknown) => {
if (!payload || typeof payload !== 'object') {
throw new Error('Invalid session config payload');
}
const config = payload as { permissionMode?: unknown };
if (config.permissionMode !== undefined) {
currentPermissionMode = resolvePermissionMode(config.permissionMode);
}
syncKeepAlive();
return { applied: { permissionMode: currentPermissionMode } };
});
syncKeepAlive();
const keepAliveInterval = setInterval(() => {
session.keepAlive(thinking, 'remote');
syncKeepAlive();
}, 2000);
const sendReady = () => {
@@ -91,7 +124,7 @@ export async function runAgentSession(opts: {
await backend.cancelPrompt(agentSessionId);
await permissionAdapter.cancelAll('User aborted');
thinking = false;
session.keepAlive(thinking, 'remote');
syncKeepAlive();
sendReady();
if (waitAbortController) {
waitAbortController.abort();
@@ -131,7 +164,7 @@ export async function runAgentSession(opts: {
}];
thinking = true;
session.keepAlive(thinking, 'remote');
syncKeepAlive();
try {
await backend.prompt(agentSessionId, promptContent, (message) => {
@@ -148,7 +181,7 @@ export async function runAgentSession(opts: {
});
} finally {
thinking = false;
session.keepAlive(thinking, 'remote');
syncKeepAlive();
await permissionAdapter.cancelAll('Prompt finished');
emitReadyIfIdle({
queueSize: () => messageQueue.size(),