diff --git a/src/agents/copilot-runner.ts b/src/agents/copilot-runner.ts index 252708ccd43c0..91966cab133af 100644 --- a/src/agents/copilot-runner.ts +++ b/src/agents/copilot-runner.ts @@ -67,9 +67,17 @@ export async function runCopilotCliAgent(params: { const modelId = backendConfig ? normalizeCliModel(rawModelId, backendConfig.config) : rawModelId; const modelDisplay = `copilot-cli/${modelId}`; + // Resolve tool filters from backend config + const toolConfig = backendConfig?.config as Record | undefined; + const availableTools = (toolConfig?.availableTools as string[] | undefined) ?? undefined; + const excludedTools = (toolConfig?.excludedTools as string[] | undefined) ?? undefined; + const hasToolFilters = !!(availableTools?.length || excludedTools?.length); + const extraSystemPrompt = [ params.extraSystemPrompt?.trim(), - "Tools are disabled in this session. Do not call tools.", + // Only append the blanket "tools disabled" message when no SDK-level tool + // filters are configured — in that case tools really are fully disabled. + ...(!hasToolFilters ? ["Tools are disabled in this session. Do not call tools."] : []), ] .filter(Boolean) .join("\n"); @@ -121,6 +129,8 @@ export async function runCopilotCliAgent(params: { systemPrompt, timeoutMs: params.timeoutMs, sessionId: params.cliSessionId, + availableTools, + excludedTools, }); const text = result.text?.trim(); diff --git a/src/agents/copilot-sdk.test.ts b/src/agents/copilot-sdk.test.ts index 593a680949cc8..581265a331a32 100644 --- a/src/agents/copilot-sdk.test.ts +++ b/src/agents/copilot-sdk.test.ts @@ -181,6 +181,63 @@ describe("copilot-sdk", () => { expect(mockClient.stop).toHaveBeenCalled(); }); + it("passes availableTools to session config and auto-approves permissions", async () => { + mockSession.sendAndWait.mockResolvedValueOnce({ + data: { content: "tools!" }, + }); + + const { runCopilotAgent } = await import("./copilot-sdk.js"); + await runCopilotAgent({ + prompt: "use tools", + availableTools: ["read", "write", "exec"], + timeoutMs: 5_000, + }); + + const sessionConfig = mockClient.createSession.mock.calls[0]?.[0]; + expect(sessionConfig.availableTools).toEqual(["read", "write", "exec"]); + expect(sessionConfig.excludedTools).toBeUndefined(); + // Permission handler should auto-approve when tool filters are set + const permResult = await sessionConfig.onPermissionRequest(); + expect(permResult.kind).toBe("approved"); + }); + + it("passes excludedTools to session config and auto-approves permissions", async () => { + mockSession.sendAndWait.mockResolvedValueOnce({ + data: { content: "tools!" }, + }); + + const { runCopilotAgent } = await import("./copilot-sdk.js"); + await runCopilotAgent({ + prompt: "use tools", + excludedTools: ["exec", "write"], + timeoutMs: 5_000, + }); + + const sessionConfig = mockClient.createSession.mock.calls[0]?.[0]; + expect(sessionConfig.excludedTools).toEqual(["exec", "write"]); + expect(sessionConfig.availableTools).toBeUndefined(); + const permResult = await sessionConfig.onPermissionRequest(); + expect(permResult.kind).toBe("approved"); + }); + + it("denies permissions when no tool filters are set", async () => { + mockSession.sendAndWait.mockResolvedValueOnce({ + data: { content: "no tools" }, + }); + + const { runCopilotAgent } = await import("./copilot-sdk.js"); + await runCopilotAgent({ + prompt: "hi", + timeoutMs: 5_000, + }); + + const sessionConfig = mockClient.createSession.mock.calls[0]?.[0]; + expect(sessionConfig.availableTools).toBeUndefined(); + expect(sessionConfig.excludedTools).toBeUndefined(); + const permResult = await sessionConfig.onPermissionRequest(); + expect(permResult.kind).toBe("denied-interactively-by-user"); + }); + it("passes system prompt as append mode", async () => { mockSession.sendAndWait.mockResolvedValueOnce({ data: { content: "Got it." }, diff --git a/src/agents/copilot-sdk.ts b/src/agents/copilot-sdk.ts index 067dffbb9c0b9..a62f2ece9c526 100644 --- a/src/agents/copilot-sdk.ts +++ b/src/agents/copilot-sdk.ts @@ -116,6 +116,10 @@ export type CopilotAgentRunOptions = { systemPrompt?: string; timeoutMs?: number; sessionId?: string; + /** SDK-level tool allowlist — only these tools are available (takes precedence over excludedTools). */ + availableTools?: string[]; + /** SDK-level tool blocklist — all tools except these are available. */ + excludedTools?: string[]; }; export type CopilotAgentRunResult = { @@ -139,18 +143,31 @@ export async function runCopilotAgent( try { await ensureAuthenticated(client); + const hasToolFilters = !!(options.availableTools?.length || options.excludedTools?.length); + const sessionConfig: SessionConfig = { model: options.model, workingDirectory: options.workspaceDir, streaming: true, - // Deny tool-use permission requests by default (security: callers must - // opt-in to specific capabilities through session configuration). - onPermissionRequest: async () => ({ - kind: "denied-interactively-by-user", - feedback: "Tool use is not permitted in this session.", - }), + ...(options.availableTools?.length && { availableTools: options.availableTools }), + ...(options.excludedTools?.length && { excludedTools: options.excludedTools }), + // When tool filters are configured, auto-approve permission requests since + // the SDK enforces the allowlist/blocklist. Otherwise deny all by default. + onPermissionRequest: hasToolFilters + ? async () => ({ kind: "approved" as const }) + : async () => ({ + kind: "denied-interactively-by-user" as const, + feedback: "Tool use is not permitted in this session.", + }), }; + if (hasToolFilters) { + log.info("copilot session tool filters configured", { + availableTools: options.availableTools, + excludedTools: options.excludedTools, + }); + } + if (options.systemPrompt) { sessionConfig.systemMessage = { mode: "append",