Wire Copilot Allow All mode to ACP permissions (#935)
This commit is contained in:
@@ -11,6 +11,7 @@ export interface AgentModeVisuals {
|
||||
|
||||
export type AgentProviderModeDefinition = Omit<AgentMode, "icon" | "colorTier"> &
|
||||
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,
|
||||
|
||||
@@ -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<ACPSessionInternals>(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", () => {
|
||||
|
||||
@@ -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<ACPProviderModeWriteResult>;
|
||||
beforeModeWriter?: (context: ACPProviderModeWriterContext) => Promise<ACPBeforeModeWriteResult>;
|
||||
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<ACPProviderModeWriteResult>;
|
||||
beforeModeWriter?: (context: ACPProviderModeWriterContext) => Promise<ACPBeforeModeWriteResult>;
|
||||
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<ACPProviderModeWriteResult>;
|
||||
private readonly beforeModeWriter?: (
|
||||
context: ACPProviderModeWriterContext,
|
||||
) => Promise<ACPBeforeModeWriteResult>;
|
||||
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<ACPProviderModeWriteResult>;
|
||||
private readonly beforeModeWriter?: (
|
||||
context: ACPProviderModeWriterContext,
|
||||
) => Promise<ACPBeforeModeWriteResult>;
|
||||
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<void> {
|
||||
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<RequestPermissionResponse> {
|
||||
// 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<void> {
|
||||
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");
|
||||
|
||||
@@ -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<SessionConfigOption, { type: "select" }>;
|
||||
|
||||
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<ACPProviderModeWriteResult> {
|
||||
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<ACPBeforeModeWriteResult> {
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user