Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/agents/copilot-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> | 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");
Expand Down Expand Up @@ -121,6 +129,8 @@ export async function runCopilotCliAgent(params: {
systemPrompt,
timeoutMs: params.timeoutMs,
sessionId: params.cliSessionId,
availableTools,
excludedTools,
});

const text = result.text?.trim();
Expand Down
57 changes: 57 additions & 0 deletions src/agents/copilot-sdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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." },
Expand Down
29 changes: 23 additions & 6 deletions src/agents/copilot-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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",
Expand Down
Loading