From a5c2b97e1d5af8d70b1d6a89904a97d2926e9974 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 12 May 2026 12:00:20 +0800 Subject: [PATCH] Wire Copilot Allow All mode to ACP permissions (#935) --- .../src/server/agent/provider-manifest.ts | 7 +- .../server/agent/providers/acp-agent.test.ts | 174 +++++++++++++++++- .../src/server/agent/providers/acp-agent.ts | 174 ++++++++++++++++-- .../agent/providers/copilot-acp-agent.ts | 155 +++++++++++++++- .../src/server/daemon-e2e/agent-configs.ts | 2 +- 5 files changed, 478 insertions(+), 34 deletions(-) diff --git a/packages/server/src/server/agent/provider-manifest.ts b/packages/server/src/server/agent/provider-manifest.ts index 3edb52a7a..66b218415 100644 --- a/packages/server/src/server/agent/provider-manifest.ts +++ b/packages/server/src/server/agent/provider-manifest.ts @@ -11,6 +11,7 @@ export interface AgentModeVisuals { export type AgentProviderModeDefinition = Omit & AgentModeVisuals & { + // Marks the provider's most-permissioned no-prompt mode. Selecting it means tools run without approval; the runtime mechanism is provider-specific. isUnattended?: boolean; }; @@ -96,9 +97,9 @@ const COPILOT_MODES: AgentProviderModeDefinition[] = [ colorTier: "planning", }, { - id: "https://agentclientprotocol.com/protocol/session-modes#autopilot", - label: "Autopilot", - description: "Autonomous mode that runs until task completion without user interaction", + id: "allow-all", + label: "Allow All", + description: "Automatically approves all Copilot tool, path, and URL requests.", icon: "ShieldOff", colorTier: "dangerous", isUnattended: true, diff --git a/packages/server/src/server/agent/providers/acp-agent.test.ts b/packages/server/src/server/agent/providers/acp-agent.test.ts index 0a17d73d4..ae458e01a 100644 --- a/packages/server/src/server/agent/providers/acp-agent.test.ts +++ b/packages/server/src/server/agent/providers/acp-agent.test.ts @@ -21,6 +21,15 @@ import { resolveACPModeSelection, resolveACPModelSelection, } from "./acp-agent.js"; +import { + COPILOT_ALLOW_ALL_MODE_ID, + COPILOT_MODES, + beforeCopilotModeWriter, + transformCopilotConfigOptions, + transformCopilotModeId, + transformCopilotSessionResponse, + writeCopilotProviderMode, +} from "./copilot-acp-agent.js"; import { transformPiModels } from "./pi-direct-agent.js"; import type { AgentStreamEvent } from "../agent-sdk-types.js"; import { createTestLogger } from "../../../test-utils/test-logger.js"; @@ -140,6 +149,73 @@ function selectConfigOption( }; } +function createCopilotSessionWithConfig(modeId?: string | null): ACPAgentSession { + return new ACPAgentSession( + { + provider: "copilot", + cwd: "/tmp/paseo-acp-test", + modeId: modeId ?? undefined, + }, + { + provider: "copilot", + logger: createTestLogger(), + defaultCommand: ["copilot", "--acp"], + defaultModes: COPILOT_MODES, + sessionResponseTransformer: transformCopilotSessionResponse, + configOptionsTransformer: transformCopilotConfigOptions, + modeIdTransformer: transformCopilotModeId, + providerModeWriter: writeCopilotProviderMode, + beforeModeWriter: beforeCopilotModeWriter, + capabilities: { + supportsStreaming: true, + supportsSessionPersistence: true, + supportsDynamicModes: true, + supportsMcpServers: true, + supportsReasoningStream: true, + supportsToolInvocations: true, + }, + }, + ); +} + +function copilotModeConfigOption(currentValue: string): SessionConfigOption { + return { + id: "mode", + name: "Mode", + category: "mode", + type: "select", + currentValue, + options: [ + { + value: "https://agentclientprotocol.com/protocol/session-modes#agent", + name: "Agent", + }, + { + value: "https://agentclientprotocol.com/protocol/session-modes#plan", + name: "Plan", + }, + { + value: "https://agentclientprotocol.com/protocol/session-modes#autopilot", + name: "Autopilot", + }, + ], + }; +} + +function copilotAllowAllConfigOption(currentValue: "on" | "off"): SessionConfigOption { + return { + id: "allow_all", + name: "Allow All", + category: "permissions", + type: "select", + currentValue, + options: [ + { value: "on", name: "On" }, + { value: "off", name: "Off" }, + ], + }; +} + function selectConfigOptionName(category: "mode" | "model" | "thought_level"): string { if (category === "mode") { return "Mode"; @@ -796,12 +872,10 @@ describe("ACPAgentSession Zed parity", () => { }); }); - test("passes Copilot Autopilot ACP permission requests through to the user", async () => { - // Zed parity: Copilot Autopilot no longer auto-approves ACP permission requests, - // so the accepted UX regression is that every tool request reaches the user UI. + test("passes generic ACP permission requests through to the user", async () => { const session = createSessionWithConfig({ - provider: "copilot", - modeId: "https://agentclientprotocol.com/protocol/session-modes#autopilot", + provider: "cursor-acp", + modeId: "https://agentclientprotocol.com/protocol/session-modes#agent", }); const events: Array<{ type: string; request?: { id: string } }> = []; const permissionOptions: PermissionOption[] = [ @@ -835,6 +909,96 @@ describe("ACPAgentSession Zed parity", () => { outcome: { outcome: "selected", optionId: "allow-once" }, }); }); + + test("maps Copilot Allow All mode to allow_all ACP config on session start", async () => { + const setSessionConfigOption = vi.fn(async () => ({ + configOptions: [ + copilotModeConfigOption("https://agentclientprotocol.com/protocol/session-modes#agent"), + copilotAllowAllConfigOption("on"), + ], + })); + const setSessionMode = vi.fn(async () => undefined); + const session = createCopilotSessionWithConfig(COPILOT_ALLOW_ALL_MODE_ID); + const { internals } = prepareConfiguredOverrideSession(session, { + currentMode: "https://agentclientprotocol.com/protocol/session-modes#agent", + availableModes: COPILOT_MODES, + configOptions: [ + copilotModeConfigOption("https://agentclientprotocol.com/protocol/session-modes#agent"), + copilotAllowAllConfigOption("off"), + ], + connection: { setSessionConfigOption, setSessionMode }, + }); + const events: AgentStreamEvent[] = []; + const unsubscribe = session.subscribe((event) => events.push(event)); + await internals.applyConfiguredOverrides(); + unsubscribe(); + + expect(setSessionConfigOption).toHaveBeenCalledWith({ + sessionId: "session-1", + configId: "allow_all", + value: "on", + }); + expect(setSessionMode).not.toHaveBeenCalled(); + await expect(session.getCurrentMode()).resolves.toBe(COPILOT_ALLOW_ALL_MODE_ID); + expect(events.some((event) => event.type === "permission_requested")).toBe(false); + }); + + test("switching Copilot away from Allow All turns allow_all off before setting the ACP mode", async () => { + const setSessionConfigOption = vi.fn(async (input: { value: string }) => ({ + configOptions: [ + copilotModeConfigOption("https://agentclientprotocol.com/protocol/session-modes#agent"), + copilotAllowAllConfigOption(input.value === "on" ? "on" : "off"), + ], + })); + const setSessionMode = vi.fn(async () => undefined); + const session = createCopilotSessionWithConfig(COPILOT_ALLOW_ALL_MODE_ID); + prepareConfiguredOverrideSession(session, { + currentMode: COPILOT_ALLOW_ALL_MODE_ID, + availableModes: COPILOT_MODES, + configOptions: [ + copilotModeConfigOption(COPILOT_ALLOW_ALL_MODE_ID), + copilotAllowAllConfigOption("on"), + ], + connection: { setSessionConfigOption, setSessionMode }, + }); + + await session.setMode("https://agentclientprotocol.com/protocol/session-modes#agent"); + + expect(setSessionConfigOption).toHaveBeenCalledWith({ + sessionId: "session-1", + configId: "allow_all", + value: "off", + }); + expect(setSessionMode).toHaveBeenCalledWith({ + sessionId: "session-1", + modeId: "https://agentclientprotocol.com/protocol/session-modes#agent", + }); + }); + + test("trusts Copilot allow_all config updates as the current mode source", async () => { + const session = createCopilotSessionWithConfig(); + const internals = asInternals(session); + + const events = internals.translateSessionUpdate({ + sessionUpdate: "config_option_update", + configOptions: [ + copilotModeConfigOption("https://agentclientprotocol.com/protocol/session-modes#agent"), + copilotAllowAllConfigOption("on"), + ], + }); + + expect(events).toMatchObject([ + { + type: "mode_changed", + provider: "copilot", + currentModeId: COPILOT_ALLOW_ALL_MODE_ID, + availableModes: expect.arrayContaining([ + expect.objectContaining({ id: COPILOT_ALLOW_ALL_MODE_ID, label: "Allow All" }), + ]), + }, + ]); + await expect(session.getCurrentMode()).resolves.toBe(COPILOT_ALLOW_ALL_MODE_ID); + }); }); describe("deriveModelDefinitionsFromACP", () => { diff --git a/packages/server/src/server/agent/providers/acp-agent.ts b/packages/server/src/server/agent/providers/acp-agent.ts index 241e35c00..a98b57e77 100644 --- a/packages/server/src/server/agent/providers/acp-agent.ts +++ b/packages/server/src/server/agent/providers/acp-agent.ts @@ -226,7 +226,13 @@ interface ACPAgentClientOptions { defaultModes?: AgentMode[]; modelTransformer?: (models: AgentModelDefinition[]) => AgentModelDefinition[]; sessionResponseTransformer?: (response: SessionStateResponse) => SessionStateResponse; + configOptionsTransformer?: (configOptions: SessionConfigOption[]) => SessionConfigOption[]; + modeIdTransformer?: (modeId: string) => string | null; toolSnapshotTransformer?: (snapshot: ACPToolSnapshot) => ACPToolSnapshot; + providerModeWriter?: ( + context: ACPProviderModeWriterContext, + ) => Promise; + beforeModeWriter?: (context: ACPProviderModeWriterContext) => Promise; thinkingOptionWriter?: ( connection: ClientSideConnection, sessionId: string, @@ -245,7 +251,13 @@ interface ACPAgentSessionOptions { defaultModes: AgentMode[]; modelTransformer?: (models: AgentModelDefinition[]) => AgentModelDefinition[]; sessionResponseTransformer?: (response: SessionStateResponse) => SessionStateResponse; + configOptionsTransformer?: (configOptions: SessionConfigOption[]) => SessionConfigOption[]; + modeIdTransformer?: (modeId: string) => string | null; toolSnapshotTransformer?: (snapshot: ACPToolSnapshot) => ACPToolSnapshot; + providerModeWriter?: ( + context: ACPProviderModeWriterContext, + ) => Promise; + beforeModeWriter?: (context: ACPProviderModeWriterContext) => Promise; thinkingOptionWriter?: ( connection: ClientSideConnection, sessionId: string, @@ -337,6 +349,26 @@ interface ACPModelSelection { hasAvailableModels: boolean; } +export interface ACPProviderModeWriterContext { + connection: ClientSideConnection; + sessionId: string; + requestedModeId: string; + currentModeId: string | null; + selection: ACPModeSelection; + configOptions: SessionConfigOption[]; + logger: Logger; +} + +export interface ACPProviderModeWriteResult { + handled: boolean; + currentModeId?: string; + configOptions?: SessionConfigOption[]; +} + +export interface ACPBeforeModeWriteResult { + configOptions?: SessionConfigOption[]; +} + export function mapACPUsage(usage: Usage | null | undefined): AgentUsage | undefined { if (!usage) { return undefined; @@ -465,7 +497,17 @@ export class ACPAgentClient implements AgentClient { private readonly sessionResponseTransformer?: ( response: SessionStateResponse, ) => SessionStateResponse; + private readonly configOptionsTransformer?: ( + configOptions: SessionConfigOption[], + ) => SessionConfigOption[]; + private readonly modeIdTransformer?: (modeId: string) => string | null; private readonly toolSnapshotTransformer?: (snapshot: ACPToolSnapshot) => ACPToolSnapshot; + private readonly providerModeWriter?: ( + context: ACPProviderModeWriterContext, + ) => Promise; + private readonly beforeModeWriter?: ( + context: ACPProviderModeWriterContext, + ) => Promise; private readonly thinkingOptionWriter?: ( connection: ClientSideConnection, sessionId: string, @@ -483,7 +525,11 @@ export class ACPAgentClient implements AgentClient { this.defaultModes = options.defaultModes ?? []; this.modelTransformer = options.modelTransformer; this.sessionResponseTransformer = options.sessionResponseTransformer; + this.configOptionsTransformer = options.configOptionsTransformer; + this.modeIdTransformer = options.modeIdTransformer; this.toolSnapshotTransformer = options.toolSnapshotTransformer; + this.providerModeWriter = options.providerModeWriter; + this.beforeModeWriter = options.beforeModeWriter; this.thinkingOptionWriter = options.thinkingOptionWriter; this.waitForInitialCommands = options.waitForInitialCommands ?? false; this.initialCommandsWaitTimeoutMs = options.initialCommandsWaitTimeoutMs ?? 1500; @@ -504,7 +550,11 @@ export class ACPAgentClient implements AgentClient { defaultModes: this.defaultModes, modelTransformer: this.modelTransformer, sessionResponseTransformer: this.sessionResponseTransformer, + configOptionsTransformer: this.configOptionsTransformer, + modeIdTransformer: this.modeIdTransformer, toolSnapshotTransformer: this.toolSnapshotTransformer, + providerModeWriter: this.providerModeWriter, + beforeModeWriter: this.beforeModeWriter, thinkingOptionWriter: this.thinkingOptionWriter, capabilities: this.capabilities, launchEnv: launchContext?.env, @@ -545,7 +595,11 @@ export class ACPAgentClient implements AgentClient { defaultModes: this.defaultModes, modelTransformer: this.modelTransformer, sessionResponseTransformer: this.sessionResponseTransformer, + configOptionsTransformer: this.configOptionsTransformer, + modeIdTransformer: this.modeIdTransformer, toolSnapshotTransformer: this.toolSnapshotTransformer, + providerModeWriter: this.providerModeWriter, + beforeModeWriter: this.beforeModeWriter, thinkingOptionWriter: this.thinkingOptionWriter, capabilities: this.capabilities, handle, @@ -747,7 +801,16 @@ export class ACPAgentClient implements AgentClient { } protected transformSessionResponse(response: SessionStateResponse): SessionStateResponse { - return this.sessionResponseTransformer ? this.sessionResponseTransformer(response) : response; + const transformed = this.sessionResponseTransformer + ? this.sessionResponseTransformer(response) + : response; + if (!this.configOptionsTransformer || !transformed.configOptions) { + return transformed; + } + return { + ...transformed, + configOptions: this.configOptionsTransformer(transformed.configOptions), + }; } } @@ -763,7 +826,17 @@ export class ACPAgentSession implements AgentSession, ACPClient { private readonly sessionResponseTransformer?: ( response: SessionStateResponse, ) => SessionStateResponse; + private readonly configOptionsTransformer?: ( + configOptions: SessionConfigOption[], + ) => SessionConfigOption[]; + private readonly modeIdTransformer?: (modeId: string) => string | null; private readonly toolSnapshotTransformer?: (snapshot: ACPToolSnapshot) => ACPToolSnapshot; + private readonly providerModeWriter?: ( + context: ACPProviderModeWriterContext, + ) => Promise; + private readonly beforeModeWriter?: ( + context: ACPProviderModeWriterContext, + ) => Promise; private readonly thinkingOptionWriter?: ( connection: ClientSideConnection, sessionId: string, @@ -814,7 +887,11 @@ export class ACPAgentSession implements AgentSession, ACPClient { this.defaultModes = options.defaultModes; this.modelTransformer = options.modelTransformer; this.sessionResponseTransformer = options.sessionResponseTransformer; + this.configOptionsTransformer = options.configOptionsTransformer; + this.modeIdTransformer = options.modeIdTransformer; this.toolSnapshotTransformer = options.toolSnapshotTransformer; + this.providerModeWriter = options.providerModeWriter; + this.beforeModeWriter = options.beforeModeWriter; this.thinkingOptionWriter = options.thinkingOptionWriter; this.availableModes = options.defaultModes; this.launchEnv = options.launchEnv; @@ -1068,6 +1145,25 @@ export class ACPAgentSession implements AgentSession, ACPClient { throw new Error("ACP session not initialized"); } + const context = this.createProviderModeWriterContext(modeId, selection); + const providerResult = this.providerModeWriter + ? await this.providerModeWriter(context) + : { handled: false }; + if (providerResult.handled) { + this.currentMode = providerResult.currentModeId ?? modeId; + if (providerResult.configOptions) { + this.configOptions = this.transformConfigOptions(providerResult.configOptions); + } + this.availableModes = deriveModesFromACP(this.defaultModes, null, this.configOptions).modes; + this.pushEvent({ + type: "mode_changed", + provider: this.provider, + currentModeId: this.currentMode, + availableModes: [...this.availableModes], + }); + return; + } + if (selection.hasAvailableModes) { if (!selection.availableMode) { this.warnInvalidSelection( @@ -1078,7 +1174,32 @@ export class ACPAgentSession implements AgentSession, ACPClient { ); return; } + } else { + const modeOption = selection.configOption; + if (!modeOption) { + throw new Error(`${this.provider} does not expose ACP mode switching`); + } + if (!selection.configChoice) { + this.warnInvalidSelection( + modeId, + `is not valid ${this.provider} mode config option. Available options: ${flattenSelectOptions( + modeOption.options, + ) + .map((option) => option.value) + .join(", ")}`, + ); + return; + } + } + if (this.beforeModeWriter) { + const beforeResult = await this.beforeModeWriter(context); + if (beforeResult?.configOptions) { + this.configOptions = this.transformConfigOptions(beforeResult.configOptions); + } + } + + if (selection.hasAvailableModes) { await this.connection.setSessionMode({ sessionId: this.sessionId, modeId }); this.currentMode = modeId; this.pushEvent({ @@ -1094,17 +1215,6 @@ export class ACPAgentSession implements AgentSession, ACPClient { if (!modeOption) { throw new Error(`${this.provider} does not expose ACP mode switching`); } - if (!selection.configChoice) { - this.warnInvalidSelection( - modeId, - `is not valid ${this.provider} mode config option. Available options: ${flattenSelectOptions( - modeOption.options, - ) - .map((option) => option.value) - .join(", ")}`, - ); - return; - } const response = await this.connection.setSessionConfigOption({ sessionId: this.sessionId, @@ -1127,6 +1237,24 @@ export class ACPAgentSession implements AgentSession, ACPClient { }); } + private createProviderModeWriterContext( + requestedModeId: string, + selection: ACPModeSelection, + ): ACPProviderModeWriterContext { + if (!this.connection || !this.sessionId) { + throw new Error("ACP session not initialized"); + } + return { + connection: this.connection, + sessionId: this.sessionId, + requestedModeId, + currentModeId: this.currentMode, + selection, + configOptions: this.configOptions, + logger: this.logger, + }; + } + async setModel(modelId: string | null): Promise { if (!this.connection || !this.sessionId) { throw new Error("ACP session not initialized"); @@ -1281,9 +1409,9 @@ export class ACPAgentSession implements AgentSession, ACPClient { requestedValue: string; label: string; }): string { - this.configOptions = response.configOptions; + this.configOptions = this.transformConfigOptions(response.configOptions); const responseOption = findSelectConfigOption({ - configOptions: response.configOptions, + configOptions: this.configOptions, category, id: configId, }); @@ -1409,7 +1537,7 @@ export class ACPAgentSession implements AgentSession, ACPClient { } async requestPermission(params: RequestPermissionRequest): Promise { - // Match Zed acp.rs:3189-3220 — pure pass-through. Accepted UX regression: Copilot Autopilot will now prompt the user for every tool request. + // Match Zed acp.rs:3189-3220: generic ACP permission requests stay pure pass-through. const requestId = randomUUID(); let toolSnapshot = this.toolCalls.get(params.toolCall.toolCallId) ?? @@ -1622,7 +1750,7 @@ export class ACPAgentSession implements AgentSession, ACPClient { ? this.sessionResponseTransformer(response) : response; - this.configOptions = transformed.configOptions ?? []; + this.configOptions = this.transformConfigOptions(transformed.configOptions ?? []); const modeInfo = deriveModesFromACP(this.defaultModes, transformed.modes, this.configOptions); this.availableModes = modeInfo.modes; @@ -1635,6 +1763,16 @@ export class ACPAgentSession implements AgentSession, ACPClient { deriveCurrentConfigValue(this.configOptions, "thought_level") ?? this.thinkingOptionId; } + private transformConfigOptions(configOptions: SessionConfigOption[]): SessionConfigOption[] { + return this.configOptionsTransformer + ? this.configOptionsTransformer(configOptions) + : configOptions; + } + + private transformModeId(modeId: string): string | null { + return this.modeIdTransformer ? this.modeIdTransformer(modeId) : modeId; + } + private async applyConfiguredOverrides(): Promise { const configuredModeId = this.config.modeId; if (configuredModeId && configuredModeId !== this.currentMode) { @@ -1772,11 +1910,11 @@ export class ACPAgentSession implements AgentSession, ACPClient { } private handleCurrentModeUpdate(update: CurrentModeUpdate): void { - this.currentMode = update.currentModeId; + this.currentMode = this.transformModeId(update.currentModeId); } private handleConfigOptionUpdate(update: ConfigOptionUpdate): AgentStreamEvent[] { - this.configOptions = update.configOptions; + this.configOptions = this.transformConfigOptions(update.configOptions); const modeInfo = deriveModesFromACP(this.defaultModes, null, this.configOptions); const nextMode = modeInfo.currentModeId; const nextModel = deriveCurrentConfigValue(this.configOptions, "model"); diff --git a/packages/server/src/server/agent/providers/copilot-acp-agent.ts b/packages/server/src/server/agent/providers/copilot-acp-agent.ts index 53fec5e3f..57ac65718 100644 --- a/packages/server/src/server/agent/providers/copilot-acp-agent.ts +++ b/packages/server/src/server/agent/providers/copilot-acp-agent.ts @@ -1,10 +1,17 @@ import type { Logger } from "pino"; import { homedir } from "node:os"; +import type { SessionConfigOption } from "@agentclientprotocol/sdk"; import type { AgentCapabilityFlags, AgentMode } from "../agent-sdk-types.js"; import type { ProviderRuntimeSettings } from "../provider-launch-config.js"; import { findExecutable } from "../../../utils/executable.js"; -import { ACPAgentClient } from "./acp-agent.js"; +import { + ACPAgentClient, + type ACPBeforeModeWriteResult, + type ACPProviderModeWriteResult, + type ACPProviderModeWriterContext, + type SessionStateResponse, +} from "./acp-agent.js"; import { formatDiagnosticStatus, formatProviderDiagnostic, @@ -22,21 +29,31 @@ const COPILOT_CAPABILITIES: AgentCapabilityFlags = { supportsToolInvocations: true, }; -const COPILOT_MODES: AgentMode[] = [ +const COPILOT_AGENT_MODE_ID = "https://agentclientprotocol.com/protocol/session-modes#agent"; +const COPILOT_PLAN_MODE_ID = "https://agentclientprotocol.com/protocol/session-modes#plan"; +const COPILOT_AUTOPILOT_MODE_ID = + "https://agentclientprotocol.com/protocol/session-modes#autopilot"; +export const COPILOT_ALLOW_ALL_MODE_ID = "allow-all"; +const COPILOT_ALLOW_ALL_CONFIG_ID = "allow_all"; +const COPILOT_ALLOW_ALL_ON = "on"; +const COPILOT_ALLOW_ALL_OFF = "off"; +type SelectConfigOption = Extract; + +export const COPILOT_MODES: AgentMode[] = [ { - id: "https://agentclientprotocol.com/protocol/session-modes#agent", + id: COPILOT_AGENT_MODE_ID, label: "Agent", description: "Default agent mode for conversational interactions", }, { - id: "https://agentclientprotocol.com/protocol/session-modes#plan", + id: COPILOT_PLAN_MODE_ID, label: "Plan", description: "Plan mode for creating and executing multi-step plans", }, { - id: "https://agentclientprotocol.com/protocol/session-modes#autopilot", - label: "Autopilot", - description: "Autonomous mode that runs until task completion without user interaction", + id: COPILOT_ALLOW_ALL_MODE_ID, + label: "Allow All", + description: "Automatically approves all Copilot tool, path, and URL requests.", }, ]; @@ -53,6 +70,11 @@ export class CopilotACPAgentClient extends ACPAgentClient { runtimeSettings: options.runtimeSettings, defaultCommand: ["copilot", "--acp"], defaultModes: COPILOT_MODES, + sessionResponseTransformer: transformCopilotSessionResponse, + configOptionsTransformer: transformCopilotConfigOptions, + modeIdTransformer: transformCopilotModeId, + providerModeWriter: writeCopilotProviderMode, + beforeModeWriter: beforeCopilotModeWriter, capabilities: COPILOT_CAPABILITIES, }); } @@ -113,3 +135,122 @@ export class CopilotACPAgentClient extends ACPAgentClient { } } } + +export function transformCopilotSessionResponse( + response: SessionStateResponse, +): SessionStateResponse { + if (!response.modes) { + return response; + } + const allowAllEnabled = isCopilotAllowAllEnabled(response.configOptions ?? []); + return { + ...response, + modes: { + ...response.modes, + availableModes: response.modes.availableModes + ?.filter( + (mode) => mode.id !== COPILOT_AUTOPILOT_MODE_ID && mode.id !== COPILOT_ALLOW_ALL_MODE_ID, + ) + .concat({ + id: COPILOT_ALLOW_ALL_MODE_ID, + name: "Allow All", + description: "Automatically approves all Copilot tool, path, and URL requests.", + }), + currentModeId: allowAllEnabled + ? COPILOT_ALLOW_ALL_MODE_ID + : (transformCopilotModeId(response.modes.currentModeId ?? COPILOT_AGENT_MODE_ID) ?? + COPILOT_AGENT_MODE_ID), + }, + }; +} + +export function transformCopilotConfigOptions( + configOptions: SessionConfigOption[], +): SessionConfigOption[] { + const allowAllEnabled = isCopilotAllowAllEnabled(configOptions); + return configOptions.map((option) => { + if (option.type !== "select" || option.category !== "mode") { + return option; + } + // Trust Copilot's allow_all config value as the source of truth when it changes in-process. + const options = flattenCopilotModeOptions(option.options) + .filter( + (choice) => + choice.value !== COPILOT_AUTOPILOT_MODE_ID && choice.value !== COPILOT_ALLOW_ALL_MODE_ID, + ) + .concat({ + value: COPILOT_ALLOW_ALL_MODE_ID, + name: "Allow All", + description: "Automatically approves all Copilot tool, path, and URL requests.", + }); + return { + ...option, + currentValue: allowAllEnabled + ? COPILOT_ALLOW_ALL_MODE_ID + : (transformCopilotModeId(option.currentValue) ?? COPILOT_AGENT_MODE_ID), + options, + }; + }); +} + +function flattenCopilotModeOptions( + options: SelectConfigOption["options"], +): Array<{ value: string; name: string; description?: string | null }> { + const flattened: Array<{ value: string; name: string; description?: string | null }> = []; + for (const option of options) { + if ("value" in option) { + flattened.push(option); + continue; + } + flattened.push(...option.options); + } + return flattened; +} + +export function transformCopilotModeId(modeId: string): string | null { + return modeId === COPILOT_AUTOPILOT_MODE_ID ? COPILOT_AGENT_MODE_ID : modeId; +} + +export async function writeCopilotProviderMode( + context: ACPProviderModeWriterContext, +): Promise { + if (context.requestedModeId !== COPILOT_ALLOW_ALL_MODE_ID) { + return { handled: false }; + } + const response = await context.connection.setSessionConfigOption({ + sessionId: context.sessionId, + configId: COPILOT_ALLOW_ALL_CONFIG_ID, + value: COPILOT_ALLOW_ALL_ON, + }); + return { + handled: true, + currentModeId: COPILOT_ALLOW_ALL_MODE_ID, + configOptions: response.configOptions, + }; +} + +export async function beforeCopilotModeWriter( + context: ACPProviderModeWriterContext, +): Promise { + if ( + context.currentModeId !== COPILOT_ALLOW_ALL_MODE_ID || + context.requestedModeId === COPILOT_ALLOW_ALL_MODE_ID + ) { + return {}; + } + const response = await context.connection.setSessionConfigOption({ + sessionId: context.sessionId, + configId: COPILOT_ALLOW_ALL_CONFIG_ID, + value: COPILOT_ALLOW_ALL_OFF, + }); + return { configOptions: response.configOptions }; +} + +function isCopilotAllowAllEnabled(configOptions: SessionConfigOption[]): boolean { + return configOptions.some( + (option) => + option.type === "select" && + option.id === COPILOT_ALLOW_ALL_CONFIG_ID && + option.currentValue === COPILOT_ALLOW_ALL_ON, + ); +} diff --git a/packages/server/src/server/daemon-e2e/agent-configs.ts b/packages/server/src/server/daemon-e2e/agent-configs.ts index 58e3d1d37..3a2be3dfc 100644 --- a/packages/server/src/server/daemon-e2e/agent-configs.ts +++ b/packages/server/src/server/daemon-e2e/agent-configs.ts @@ -44,7 +44,7 @@ export const agentConfigs = { provider: "copilot", model: "claude-haiku-4.5", modes: { - full: "https://agentclientprotocol.com/protocol/session-modes#autopilot", + full: "allow-all", ask: "https://agentclientprotocol.com/protocol/session-modes#agent", }, },