mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
auto approve in yolo mode for codex
This commit is contained in:
@@ -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'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user