ensure mcp waits surface permissions

This commit is contained in:
Mohamed Boudra
2025-12-24 09:05:24 +00:00
parent a24f578916
commit 9318048f51
3 changed files with 76 additions and 32 deletions
@@ -60,6 +60,7 @@ export type AgentManagerOptions = {
export type WaitForAgentOptions = { export type WaitForAgentOptions = {
signal?: AbortSignal; signal?: AbortSignal;
waitForActive?: boolean;
}; };
export type WaitForAgentResult = { export type WaitForAgentResult = {
@@ -611,9 +612,12 @@ export class AgentManager {
}; };
} }
if (!isAgentBusy(snapshot.lifecycle)) { const initialStatus = snapshot.lifecycle;
const initialBusy = isAgentBusy(initialStatus);
const waitForActive = options?.waitForActive ?? false;
if (!waitForActive && !initialBusy) {
return { return {
status: snapshot.lifecycle, status: initialStatus,
permission: null, permission: null,
lastMessage: this.getLastAssistantMessage(agentId) lastMessage: this.getLastAssistantMessage(agentId)
}; };
@@ -632,7 +636,8 @@ export class AgentManager {
return; return;
} }
let currentStatus: AgentLifecycleStatus = snapshot.lifecycle; let currentStatus: AgentLifecycleStatus = initialStatus;
let hasStarted = initialBusy;
// Bug #3 Fix: Declare unsubscribe and abortHandler upfront so cleanup can reference them // Bug #3 Fix: Declare unsubscribe and abortHandler upfront so cleanup can reference them
let unsubscribe: (() => void) | null = null; let unsubscribe: (() => void) | null = null;
@@ -691,7 +696,11 @@ export class AgentManager {
finish(pending); finish(pending);
return; return;
} }
if (!isAgentBusy(event.agent.lifecycle)) { if (isAgentBusy(event.agent.lifecycle)) {
hasStarted = true;
return;
}
if (!waitForActive || hasStarted) {
finish(null); finish(null);
} }
} }
@@ -160,6 +160,12 @@ describe("agent MCP end-to-end", () => {
let agentId: string | null = null; let agentId: string | null = null;
try { try {
const initialPrompt = [
"You must call the tool named shell.",
"Run this command exactly: [\"bash\", \"-lc\", \"echo ok > mcp-smoke.txt\"].",
"After the tool runs, reply with done and stop.",
].join("\n");
const result = (await client.callTool({ const result = (await client.callTool({
name: "create_agent", name: "create_agent",
args: { args: {
@@ -167,6 +173,7 @@ describe("agent MCP end-to-end", () => {
title: "MCP e2e smoke", title: "MCP e2e smoke",
agentType: "claude", agentType: "claude",
initialMode: "default", initialMode: "default",
initialPrompt,
background: false, background: false,
}, },
})) as McpToolResult; })) as McpToolResult;
@@ -175,35 +182,13 @@ describe("agent MCP end-to-end", () => {
expect(payload).toBeTruthy(); expect(payload).toBeTruthy();
agentId = payload?.agentId as string | null; agentId = payload?.agentId as string | null;
expect(agentId).toBeTruthy(); expect(agentId).toBeTruthy();
const createPermission = payload?.permission as PermissionPayload | null;
const prompt = [ expect(createPermission?.id).toBeTruthy();
"You must call the tool named shell.",
"Run this command exactly: [\"bash\", \"-lc\", \"echo ok > mcp-smoke.txt\"].",
"After the tool runs, reply with done and stop.",
].join("\n");
await client.callTool({
name: "send_agent_prompt",
args: {
agentId,
prompt,
sessionMode: "default",
background: true,
},
});
const waitResult = (await client.callTool({
name: "wait_for_agent",
args: { agentId },
})) as McpToolResult;
const waitPayload = getStructuredContent(waitResult);
const permission = waitPayload?.permission as PermissionPayload | null;
expect(permission?.id).toBeTruthy();
await client.callTool({ await client.callTool({
name: "respond_to_permission", name: "respond_to_permission",
args: { args: {
agentId, agentId,
requestId: permission!.id, requestId: createPermission!.id,
response: { behavior: "allow" }, response: { behavior: "allow" },
}, },
}); });
@@ -226,6 +211,50 @@ describe("agent MCP end-to-end", () => {
throw new Error(`${(error as Error).message}\n${details}`); throw new Error(`${(error as Error).message}\n${details}`);
} }
expect(contents.trim()).toBe("ok"); expect(contents.trim()).toBe("ok");
const prompt = [
"You must call the tool named shell.",
"Run this command exactly: [\"bash\", \"-lc\", \"echo ok-2 > mcp-smoke-2.txt\"].",
"After the tool runs, reply with done and stop.",
].join("\n");
const promptResult = (await client.callTool({
name: "send_agent_prompt",
args: {
agentId,
prompt,
sessionMode: "default",
background: false,
},
})) as McpToolResult;
const promptPayload = getStructuredContent(promptResult);
const promptPermission = promptPayload?.permission as PermissionPayload | null;
expect(promptPermission?.id).toBeTruthy();
const waitPermissionResult = (await client.callTool({
name: "wait_for_agent",
args: { agentId },
})) as McpToolResult;
const waitPermissionPayload = getStructuredContent(waitPermissionResult);
const waitPermission =
waitPermissionPayload?.permission as PermissionPayload | null;
expect(waitPermission?.id).toBe(promptPermission?.id);
await client.callTool({
name: "respond_to_permission",
args: {
agentId,
requestId: promptPermission!.id,
response: { behavior: "allow" },
},
});
await waitForAgentCompletion(client, agentId);
const secondFilePath = path.join(agentCwd, "mcp-smoke-2.txt");
const secondContents = await waitForFile(secondFilePath);
expect(secondContents.trim()).toBe("ok-2");
} finally { } finally {
if (agentId) { if (agentId) {
await client.callTool({ name: "kill_agent", args: { agentId } }); await client.callTool({ name: "kill_agent", args: { agentId } });
@@ -62,7 +62,8 @@ function expandPath(path: string): string {
async function waitForAgentWithTimeout( async function waitForAgentWithTimeout(
agentManager: AgentManager, agentManager: AgentManager,
agentId: string, agentId: string,
existingSignal?: AbortSignal existingSignal?: AbortSignal,
options?: { waitForActive?: boolean }
): Promise<WaitForAgentResult> { ): Promise<WaitForAgentResult> {
const timeoutSignal = AbortSignal.timeout(AGENT_WAIT_TIMEOUT_MS); const timeoutSignal = AbortSignal.timeout(AGENT_WAIT_TIMEOUT_MS);
const abortController = new AbortController(); const abortController = new AbortController();
@@ -99,6 +100,7 @@ async function waitForAgentWithTimeout(
try { try {
const result = await agentManager.waitForAgentEvent(agentId, { const result = await agentManager.waitForAgentEvent(agentId, {
signal: abortController.signal, signal: abortController.signal,
waitForActive: options?.waitForActive,
}); });
return result; return result;
} catch (error) { } catch (error) {
@@ -297,7 +299,9 @@ export async function createAgentMcpServer(
// If not running in background, wait for completion // If not running in background, wait for completion
if (!background) { if (!background) {
const result = await waitForAgentWithTimeout(agentManager, snapshot.id); const result = await waitForAgentWithTimeout(agentManager, snapshot.id, undefined, {
waitForActive: true,
});
const responseData = { const responseData = {
agentId: snapshot.id, agentId: snapshot.id,
@@ -509,7 +513,9 @@ export async function createAgentMcpServer(
// If not running in background, wait for completion // If not running in background, wait for completion
if (!background) { if (!background) {
const result = await waitForAgentWithTimeout(agentManager, agentId); const result = await waitForAgentWithTimeout(agentManager, agentId, undefined, {
waitForActive: true,
});
const responseData = { const responseData = {
success: true, success: true,