mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-08 01:43:07 +00:00
fix(codex): support app-server plan mode (#622)
* fix(codex): support app-server plan mode * fix(codex): broaden plan mode compatibility checks
This commit is contained in:
@@ -343,4 +343,17 @@ describe('appServerConfig', () => {
|
||||
});
|
||||
expect(params.model).toBeUndefined();
|
||||
});
|
||||
|
||||
it('can suppress collaboration mode while preserving top-level model', () => {
|
||||
const params = buildTurnStartParams({
|
||||
threadId: 'thread-1',
|
||||
message: 'hello',
|
||||
cwd: '/workspace/project',
|
||||
mode: { permissionMode: 'default', model: 'o3', collaborationMode: 'plan' },
|
||||
overrides: { suppressCollaborationMode: true }
|
||||
});
|
||||
|
||||
expect(params.collaborationMode).toBeUndefined();
|
||||
expect(params.model).toBe('o3');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -139,6 +139,7 @@ export function buildTurnStartParams(args: {
|
||||
approvalPolicy?: TurnStartParams['approvalPolicy'];
|
||||
sandboxPolicy?: TurnStartParams['sandboxPolicy'];
|
||||
model?: string;
|
||||
suppressCollaborationMode?: boolean;
|
||||
};
|
||||
}): TurnStartParams {
|
||||
const params: TurnStartParams = {
|
||||
@@ -163,7 +164,9 @@ export function buildTurnStartParams(args: {
|
||||
params.sandboxPolicy = sandboxPolicy;
|
||||
}
|
||||
|
||||
const collaborationMode = args.mode?.collaborationMode;
|
||||
const collaborationMode = args.overrides?.suppressCollaborationMode
|
||||
? undefined
|
||||
: args.mode?.collaborationMode;
|
||||
const model = args.overrides?.model ?? args.mode?.model;
|
||||
|
||||
if (args.mode?.modelReasoningEffort) {
|
||||
|
||||
@@ -78,6 +78,72 @@ describe('registerAppServerPermissionHandlers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('forwards generic tool approval requests with the app-server tool name', async () => {
|
||||
const { client, handlers } = createClient();
|
||||
const permissionHandler = {
|
||||
handleToolCall: vi.fn(async () => ({ decision: 'approved' }))
|
||||
};
|
||||
|
||||
registerAppServerPermissionHandlers({
|
||||
client: client as never,
|
||||
permissionHandler: permissionHandler as never
|
||||
});
|
||||
|
||||
const handler = handlers.get('item/tool/requestApproval');
|
||||
expect(handler).toBeTypeOf('function');
|
||||
|
||||
await expect(handler?.({
|
||||
itemId: 'tool-123',
|
||||
toolName: 'exit_plan_mode',
|
||||
input: { plan: '1. Edit files' }
|
||||
})).resolves.toEqual({ decision: 'accept' });
|
||||
|
||||
expect(permissionHandler.handleToolCall).toHaveBeenCalledWith(
|
||||
'tool-123',
|
||||
'exit_plan_mode',
|
||||
{ plan: '1. Edit files' }
|
||||
);
|
||||
});
|
||||
|
||||
it('maps latest permissions approval requests to granted permission profiles', async () => {
|
||||
const { client, handlers } = createClient();
|
||||
const permissions = {
|
||||
network: { enabled: true },
|
||||
fileSystem: null
|
||||
};
|
||||
const permissionHandler = {
|
||||
handleToolCall: vi.fn(async () => ({ decision: 'approved_for_session' }))
|
||||
};
|
||||
|
||||
registerAppServerPermissionHandlers({
|
||||
client: client as never,
|
||||
permissionHandler: permissionHandler as never
|
||||
});
|
||||
|
||||
const handler = handlers.get('item/permissions/requestApproval');
|
||||
expect(handler).toBeTypeOf('function');
|
||||
|
||||
await expect(handler?.({
|
||||
itemId: 'perm-123',
|
||||
reason: 'Need network',
|
||||
cwd: '/workspace/project',
|
||||
permissions
|
||||
})).resolves.toEqual({
|
||||
permissions,
|
||||
scope: 'session'
|
||||
});
|
||||
|
||||
expect(permissionHandler.handleToolCall).toHaveBeenCalledWith(
|
||||
'perm-123',
|
||||
'CodexPermission',
|
||||
{
|
||||
message: 'Need network',
|
||||
cwd: '/workspace/project',
|
||||
permissions
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts MCP elicitation requests with schema defaults', async () => {
|
||||
const { client, handlers } = createClient();
|
||||
const permissionHandler = {
|
||||
|
||||
@@ -29,6 +29,15 @@ function asString(value: unknown): string | undefined {
|
||||
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
function pickToolName(record: Record<string, unknown>): string {
|
||||
return asString(record.toolName)
|
||||
?? asString(record.tool_name)
|
||||
?? asString(record.tool)
|
||||
?? asString(record.name)
|
||||
?? asString(record.permission)
|
||||
?? 'CodexTool';
|
||||
}
|
||||
|
||||
function mapDecision(decision: PermissionDecision): { decision: string } {
|
||||
switch (decision) {
|
||||
case 'approved':
|
||||
@@ -42,6 +51,29 @@ function mapDecision(decision: PermissionDecision): { decision: string } {
|
||||
}
|
||||
}
|
||||
|
||||
function mapPermissionGrant(
|
||||
requested: unknown,
|
||||
decision: PermissionDecision
|
||||
): {
|
||||
permissions: unknown;
|
||||
scope: 'turn' | 'session';
|
||||
} {
|
||||
if (decision === 'approved' || decision === 'approved_for_session') {
|
||||
return {
|
||||
permissions: requested,
|
||||
scope: decision === 'approved_for_session' ? 'session' : 'turn'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
permissions: {
|
||||
network: null,
|
||||
fileSystem: null
|
||||
},
|
||||
scope: 'turn'
|
||||
};
|
||||
}
|
||||
|
||||
function firstString(values: unknown): string | undefined {
|
||||
if (!Array.isArray(values)) {
|
||||
return undefined;
|
||||
@@ -170,6 +202,38 @@ export function registerAppServerPermissionHandlers(args: {
|
||||
return mapDecision(result.decision);
|
||||
});
|
||||
|
||||
client.registerRequestHandler('item/permissions/requestApproval', async (params) => {
|
||||
const record = asRecord(params) ?? {};
|
||||
const toolCallId = asString(record.itemId) ?? randomUUID();
|
||||
const permissions = record.permissions ?? {};
|
||||
|
||||
const result = await permissionHandler.handleToolCall(
|
||||
toolCallId,
|
||||
'CodexPermission',
|
||||
{
|
||||
message: asString(record.reason),
|
||||
cwd: asString(record.cwd),
|
||||
permissions
|
||||
}
|
||||
) as PermissionResult;
|
||||
|
||||
return mapPermissionGrant(permissions, result.decision);
|
||||
});
|
||||
|
||||
client.registerRequestHandler('item/tool/requestApproval', async (params) => {
|
||||
const record = asRecord(params) ?? {};
|
||||
const toolCallId = asString(record.itemId) ?? asString(record.item_id) ?? randomUUID();
|
||||
const toolName = pickToolName(record);
|
||||
|
||||
const result = await permissionHandler.handleToolCall(
|
||||
toolCallId,
|
||||
toolName,
|
||||
record.input ?? record.arguments ?? params
|
||||
) as PermissionResult;
|
||||
|
||||
return mapDecision(result.decision);
|
||||
});
|
||||
|
||||
client.registerRequestHandler('item/tool/requestUserInput', async (params) => {
|
||||
const record = asRecord(params) ?? {};
|
||||
const requestId = asString(record.itemId) ?? randomUUID();
|
||||
|
||||
Reference in New Issue
Block a user