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"> &
|
export type AgentProviderModeDefinition = Omit<AgentMode, "icon" | "colorTier"> &
|
||||||
AgentModeVisuals & {
|
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;
|
isUnattended?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -96,9 +97,9 @@ const COPILOT_MODES: AgentProviderModeDefinition[] = [
|
|||||||
colorTier: "planning",
|
colorTier: "planning",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "https://agentclientprotocol.com/protocol/session-modes#autopilot",
|
id: "allow-all",
|
||||||
label: "Autopilot",
|
label: "Allow All",
|
||||||
description: "Autonomous mode that runs until task completion without user interaction",
|
description: "Automatically approves all Copilot tool, path, and URL requests.",
|
||||||
icon: "ShieldOff",
|
icon: "ShieldOff",
|
||||||
colorTier: "dangerous",
|
colorTier: "dangerous",
|
||||||
isUnattended: true,
|
isUnattended: true,
|
||||||
|
|||||||
@@ -21,6 +21,15 @@ import {
|
|||||||
resolveACPModeSelection,
|
resolveACPModeSelection,
|
||||||
resolveACPModelSelection,
|
resolveACPModelSelection,
|
||||||
} from "./acp-agent.js";
|
} 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 { transformPiModels } from "./pi-direct-agent.js";
|
||||||
import type { AgentStreamEvent } from "../agent-sdk-types.js";
|
import type { AgentStreamEvent } from "../agent-sdk-types.js";
|
||||||
import { createTestLogger } from "../../../test-utils/test-logger.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 {
|
function selectConfigOptionName(category: "mode" | "model" | "thought_level"): string {
|
||||||
if (category === "mode") {
|
if (category === "mode") {
|
||||||
return "Mode";
|
return "Mode";
|
||||||
@@ -796,12 +872,10 @@ describe("ACPAgentSession Zed parity", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("passes Copilot Autopilot ACP permission requests through to the user", async () => {
|
test("passes generic 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.
|
|
||||||
const session = createSessionWithConfig({
|
const session = createSessionWithConfig({
|
||||||
provider: "copilot",
|
provider: "cursor-acp",
|
||||||
modeId: "https://agentclientprotocol.com/protocol/session-modes#autopilot",
|
modeId: "https://agentclientprotocol.com/protocol/session-modes#agent",
|
||||||
});
|
});
|
||||||
const events: Array<{ type: string; request?: { id: string } }> = [];
|
const events: Array<{ type: string; request?: { id: string } }> = [];
|
||||||
const permissionOptions: PermissionOption[] = [
|
const permissionOptions: PermissionOption[] = [
|
||||||
@@ -835,6 +909,96 @@ describe("ACPAgentSession Zed parity", () => {
|
|||||||
outcome: { outcome: "selected", optionId: "allow-once" },
|
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", () => {
|
describe("deriveModelDefinitionsFromACP", () => {
|
||||||
|
|||||||
@@ -226,7 +226,13 @@ interface ACPAgentClientOptions {
|
|||||||
defaultModes?: AgentMode[];
|
defaultModes?: AgentMode[];
|
||||||
modelTransformer?: (models: AgentModelDefinition[]) => AgentModelDefinition[];
|
modelTransformer?: (models: AgentModelDefinition[]) => AgentModelDefinition[];
|
||||||
sessionResponseTransformer?: (response: SessionStateResponse) => SessionStateResponse;
|
sessionResponseTransformer?: (response: SessionStateResponse) => SessionStateResponse;
|
||||||
|
configOptionsTransformer?: (configOptions: SessionConfigOption[]) => SessionConfigOption[];
|
||||||
|
modeIdTransformer?: (modeId: string) => string | null;
|
||||||
toolSnapshotTransformer?: (snapshot: ACPToolSnapshot) => ACPToolSnapshot;
|
toolSnapshotTransformer?: (snapshot: ACPToolSnapshot) => ACPToolSnapshot;
|
||||||
|
providerModeWriter?: (
|
||||||
|
context: ACPProviderModeWriterContext,
|
||||||
|
) => Promise<ACPProviderModeWriteResult>;
|
||||||
|
beforeModeWriter?: (context: ACPProviderModeWriterContext) => Promise<ACPBeforeModeWriteResult>;
|
||||||
thinkingOptionWriter?: (
|
thinkingOptionWriter?: (
|
||||||
connection: ClientSideConnection,
|
connection: ClientSideConnection,
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
@@ -245,7 +251,13 @@ interface ACPAgentSessionOptions {
|
|||||||
defaultModes: AgentMode[];
|
defaultModes: AgentMode[];
|
||||||
modelTransformer?: (models: AgentModelDefinition[]) => AgentModelDefinition[];
|
modelTransformer?: (models: AgentModelDefinition[]) => AgentModelDefinition[];
|
||||||
sessionResponseTransformer?: (response: SessionStateResponse) => SessionStateResponse;
|
sessionResponseTransformer?: (response: SessionStateResponse) => SessionStateResponse;
|
||||||
|
configOptionsTransformer?: (configOptions: SessionConfigOption[]) => SessionConfigOption[];
|
||||||
|
modeIdTransformer?: (modeId: string) => string | null;
|
||||||
toolSnapshotTransformer?: (snapshot: ACPToolSnapshot) => ACPToolSnapshot;
|
toolSnapshotTransformer?: (snapshot: ACPToolSnapshot) => ACPToolSnapshot;
|
||||||
|
providerModeWriter?: (
|
||||||
|
context: ACPProviderModeWriterContext,
|
||||||
|
) => Promise<ACPProviderModeWriteResult>;
|
||||||
|
beforeModeWriter?: (context: ACPProviderModeWriterContext) => Promise<ACPBeforeModeWriteResult>;
|
||||||
thinkingOptionWriter?: (
|
thinkingOptionWriter?: (
|
||||||
connection: ClientSideConnection,
|
connection: ClientSideConnection,
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
@@ -337,6 +349,26 @@ interface ACPModelSelection {
|
|||||||
hasAvailableModels: boolean;
|
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 {
|
export function mapACPUsage(usage: Usage | null | undefined): AgentUsage | undefined {
|
||||||
if (!usage) {
|
if (!usage) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -465,7 +497,17 @@ export class ACPAgentClient implements AgentClient {
|
|||||||
private readonly sessionResponseTransformer?: (
|
private readonly sessionResponseTransformer?: (
|
||||||
response: SessionStateResponse,
|
response: SessionStateResponse,
|
||||||
) => SessionStateResponse;
|
) => SessionStateResponse;
|
||||||
|
private readonly configOptionsTransformer?: (
|
||||||
|
configOptions: SessionConfigOption[],
|
||||||
|
) => SessionConfigOption[];
|
||||||
|
private readonly modeIdTransformer?: (modeId: string) => string | null;
|
||||||
private readonly toolSnapshotTransformer?: (snapshot: ACPToolSnapshot) => ACPToolSnapshot;
|
private readonly toolSnapshotTransformer?: (snapshot: ACPToolSnapshot) => ACPToolSnapshot;
|
||||||
|
private readonly providerModeWriter?: (
|
||||||
|
context: ACPProviderModeWriterContext,
|
||||||
|
) => Promise<ACPProviderModeWriteResult>;
|
||||||
|
private readonly beforeModeWriter?: (
|
||||||
|
context: ACPProviderModeWriterContext,
|
||||||
|
) => Promise<ACPBeforeModeWriteResult>;
|
||||||
private readonly thinkingOptionWriter?: (
|
private readonly thinkingOptionWriter?: (
|
||||||
connection: ClientSideConnection,
|
connection: ClientSideConnection,
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
@@ -483,7 +525,11 @@ export class ACPAgentClient implements AgentClient {
|
|||||||
this.defaultModes = options.defaultModes ?? [];
|
this.defaultModes = options.defaultModes ?? [];
|
||||||
this.modelTransformer = options.modelTransformer;
|
this.modelTransformer = options.modelTransformer;
|
||||||
this.sessionResponseTransformer = options.sessionResponseTransformer;
|
this.sessionResponseTransformer = options.sessionResponseTransformer;
|
||||||
|
this.configOptionsTransformer = options.configOptionsTransformer;
|
||||||
|
this.modeIdTransformer = options.modeIdTransformer;
|
||||||
this.toolSnapshotTransformer = options.toolSnapshotTransformer;
|
this.toolSnapshotTransformer = options.toolSnapshotTransformer;
|
||||||
|
this.providerModeWriter = options.providerModeWriter;
|
||||||
|
this.beforeModeWriter = options.beforeModeWriter;
|
||||||
this.thinkingOptionWriter = options.thinkingOptionWriter;
|
this.thinkingOptionWriter = options.thinkingOptionWriter;
|
||||||
this.waitForInitialCommands = options.waitForInitialCommands ?? false;
|
this.waitForInitialCommands = options.waitForInitialCommands ?? false;
|
||||||
this.initialCommandsWaitTimeoutMs = options.initialCommandsWaitTimeoutMs ?? 1500;
|
this.initialCommandsWaitTimeoutMs = options.initialCommandsWaitTimeoutMs ?? 1500;
|
||||||
@@ -504,7 +550,11 @@ export class ACPAgentClient implements AgentClient {
|
|||||||
defaultModes: this.defaultModes,
|
defaultModes: this.defaultModes,
|
||||||
modelTransformer: this.modelTransformer,
|
modelTransformer: this.modelTransformer,
|
||||||
sessionResponseTransformer: this.sessionResponseTransformer,
|
sessionResponseTransformer: this.sessionResponseTransformer,
|
||||||
|
configOptionsTransformer: this.configOptionsTransformer,
|
||||||
|
modeIdTransformer: this.modeIdTransformer,
|
||||||
toolSnapshotTransformer: this.toolSnapshotTransformer,
|
toolSnapshotTransformer: this.toolSnapshotTransformer,
|
||||||
|
providerModeWriter: this.providerModeWriter,
|
||||||
|
beforeModeWriter: this.beforeModeWriter,
|
||||||
thinkingOptionWriter: this.thinkingOptionWriter,
|
thinkingOptionWriter: this.thinkingOptionWriter,
|
||||||
capabilities: this.capabilities,
|
capabilities: this.capabilities,
|
||||||
launchEnv: launchContext?.env,
|
launchEnv: launchContext?.env,
|
||||||
@@ -545,7 +595,11 @@ export class ACPAgentClient implements AgentClient {
|
|||||||
defaultModes: this.defaultModes,
|
defaultModes: this.defaultModes,
|
||||||
modelTransformer: this.modelTransformer,
|
modelTransformer: this.modelTransformer,
|
||||||
sessionResponseTransformer: this.sessionResponseTransformer,
|
sessionResponseTransformer: this.sessionResponseTransformer,
|
||||||
|
configOptionsTransformer: this.configOptionsTransformer,
|
||||||
|
modeIdTransformer: this.modeIdTransformer,
|
||||||
toolSnapshotTransformer: this.toolSnapshotTransformer,
|
toolSnapshotTransformer: this.toolSnapshotTransformer,
|
||||||
|
providerModeWriter: this.providerModeWriter,
|
||||||
|
beforeModeWriter: this.beforeModeWriter,
|
||||||
thinkingOptionWriter: this.thinkingOptionWriter,
|
thinkingOptionWriter: this.thinkingOptionWriter,
|
||||||
capabilities: this.capabilities,
|
capabilities: this.capabilities,
|
||||||
handle,
|
handle,
|
||||||
@@ -747,7 +801,16 @@ export class ACPAgentClient implements AgentClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected transformSessionResponse(response: SessionStateResponse): SessionStateResponse {
|
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?: (
|
private readonly sessionResponseTransformer?: (
|
||||||
response: SessionStateResponse,
|
response: SessionStateResponse,
|
||||||
) => SessionStateResponse;
|
) => SessionStateResponse;
|
||||||
|
private readonly configOptionsTransformer?: (
|
||||||
|
configOptions: SessionConfigOption[],
|
||||||
|
) => SessionConfigOption[];
|
||||||
|
private readonly modeIdTransformer?: (modeId: string) => string | null;
|
||||||
private readonly toolSnapshotTransformer?: (snapshot: ACPToolSnapshot) => ACPToolSnapshot;
|
private readonly toolSnapshotTransformer?: (snapshot: ACPToolSnapshot) => ACPToolSnapshot;
|
||||||
|
private readonly providerModeWriter?: (
|
||||||
|
context: ACPProviderModeWriterContext,
|
||||||
|
) => Promise<ACPProviderModeWriteResult>;
|
||||||
|
private readonly beforeModeWriter?: (
|
||||||
|
context: ACPProviderModeWriterContext,
|
||||||
|
) => Promise<ACPBeforeModeWriteResult>;
|
||||||
private readonly thinkingOptionWriter?: (
|
private readonly thinkingOptionWriter?: (
|
||||||
connection: ClientSideConnection,
|
connection: ClientSideConnection,
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
@@ -814,7 +887,11 @@ export class ACPAgentSession implements AgentSession, ACPClient {
|
|||||||
this.defaultModes = options.defaultModes;
|
this.defaultModes = options.defaultModes;
|
||||||
this.modelTransformer = options.modelTransformer;
|
this.modelTransformer = options.modelTransformer;
|
||||||
this.sessionResponseTransformer = options.sessionResponseTransformer;
|
this.sessionResponseTransformer = options.sessionResponseTransformer;
|
||||||
|
this.configOptionsTransformer = options.configOptionsTransformer;
|
||||||
|
this.modeIdTransformer = options.modeIdTransformer;
|
||||||
this.toolSnapshotTransformer = options.toolSnapshotTransformer;
|
this.toolSnapshotTransformer = options.toolSnapshotTransformer;
|
||||||
|
this.providerModeWriter = options.providerModeWriter;
|
||||||
|
this.beforeModeWriter = options.beforeModeWriter;
|
||||||
this.thinkingOptionWriter = options.thinkingOptionWriter;
|
this.thinkingOptionWriter = options.thinkingOptionWriter;
|
||||||
this.availableModes = options.defaultModes;
|
this.availableModes = options.defaultModes;
|
||||||
this.launchEnv = options.launchEnv;
|
this.launchEnv = options.launchEnv;
|
||||||
@@ -1068,6 +1145,25 @@ export class ACPAgentSession implements AgentSession, ACPClient {
|
|||||||
throw new Error("ACP session not initialized");
|
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.hasAvailableModes) {
|
||||||
if (!selection.availableMode) {
|
if (!selection.availableMode) {
|
||||||
this.warnInvalidSelection(
|
this.warnInvalidSelection(
|
||||||
@@ -1078,7 +1174,32 @@ export class ACPAgentSession implements AgentSession, ACPClient {
|
|||||||
);
|
);
|
||||||
return;
|
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 });
|
await this.connection.setSessionMode({ sessionId: this.sessionId, modeId });
|
||||||
this.currentMode = modeId;
|
this.currentMode = modeId;
|
||||||
this.pushEvent({
|
this.pushEvent({
|
||||||
@@ -1094,17 +1215,6 @@ export class ACPAgentSession implements AgentSession, ACPClient {
|
|||||||
if (!modeOption) {
|
if (!modeOption) {
|
||||||
throw new Error(`${this.provider} does not expose ACP mode switching`);
|
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({
|
const response = await this.connection.setSessionConfigOption({
|
||||||
sessionId: this.sessionId,
|
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> {
|
async setModel(modelId: string | null): Promise<void> {
|
||||||
if (!this.connection || !this.sessionId) {
|
if (!this.connection || !this.sessionId) {
|
||||||
throw new Error("ACP session not initialized");
|
throw new Error("ACP session not initialized");
|
||||||
@@ -1281,9 +1409,9 @@ export class ACPAgentSession implements AgentSession, ACPClient {
|
|||||||
requestedValue: string;
|
requestedValue: string;
|
||||||
label: string;
|
label: string;
|
||||||
}): string {
|
}): string {
|
||||||
this.configOptions = response.configOptions;
|
this.configOptions = this.transformConfigOptions(response.configOptions);
|
||||||
const responseOption = findSelectConfigOption({
|
const responseOption = findSelectConfigOption({
|
||||||
configOptions: response.configOptions,
|
configOptions: this.configOptions,
|
||||||
category,
|
category,
|
||||||
id: configId,
|
id: configId,
|
||||||
});
|
});
|
||||||
@@ -1409,7 +1537,7 @@ export class ACPAgentSession implements AgentSession, ACPClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async requestPermission(params: RequestPermissionRequest): Promise<RequestPermissionResponse> {
|
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();
|
const requestId = randomUUID();
|
||||||
let toolSnapshot =
|
let toolSnapshot =
|
||||||
this.toolCalls.get(params.toolCall.toolCallId) ??
|
this.toolCalls.get(params.toolCall.toolCallId) ??
|
||||||
@@ -1622,7 +1750,7 @@ export class ACPAgentSession implements AgentSession, ACPClient {
|
|||||||
? this.sessionResponseTransformer(response)
|
? this.sessionResponseTransformer(response)
|
||||||
: response;
|
: response;
|
||||||
|
|
||||||
this.configOptions = transformed.configOptions ?? [];
|
this.configOptions = this.transformConfigOptions(transformed.configOptions ?? []);
|
||||||
|
|
||||||
const modeInfo = deriveModesFromACP(this.defaultModes, transformed.modes, this.configOptions);
|
const modeInfo = deriveModesFromACP(this.defaultModes, transformed.modes, this.configOptions);
|
||||||
this.availableModes = modeInfo.modes;
|
this.availableModes = modeInfo.modes;
|
||||||
@@ -1635,6 +1763,16 @@ export class ACPAgentSession implements AgentSession, ACPClient {
|
|||||||
deriveCurrentConfigValue(this.configOptions, "thought_level") ?? this.thinkingOptionId;
|
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> {
|
private async applyConfiguredOverrides(): Promise<void> {
|
||||||
const configuredModeId = this.config.modeId;
|
const configuredModeId = this.config.modeId;
|
||||||
if (configuredModeId && configuredModeId !== this.currentMode) {
|
if (configuredModeId && configuredModeId !== this.currentMode) {
|
||||||
@@ -1772,11 +1910,11 @@ export class ACPAgentSession implements AgentSession, ACPClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handleCurrentModeUpdate(update: CurrentModeUpdate): void {
|
private handleCurrentModeUpdate(update: CurrentModeUpdate): void {
|
||||||
this.currentMode = update.currentModeId;
|
this.currentMode = this.transformModeId(update.currentModeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleConfigOptionUpdate(update: ConfigOptionUpdate): AgentStreamEvent[] {
|
private handleConfigOptionUpdate(update: ConfigOptionUpdate): AgentStreamEvent[] {
|
||||||
this.configOptions = update.configOptions;
|
this.configOptions = this.transformConfigOptions(update.configOptions);
|
||||||
const modeInfo = deriveModesFromACP(this.defaultModes, null, this.configOptions);
|
const modeInfo = deriveModesFromACP(this.defaultModes, null, this.configOptions);
|
||||||
const nextMode = modeInfo.currentModeId;
|
const nextMode = modeInfo.currentModeId;
|
||||||
const nextModel = deriveCurrentConfigValue(this.configOptions, "model");
|
const nextModel = deriveCurrentConfigValue(this.configOptions, "model");
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import type { Logger } from "pino";
|
import type { Logger } from "pino";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
|
import type { SessionConfigOption } from "@agentclientprotocol/sdk";
|
||||||
|
|
||||||
import type { AgentCapabilityFlags, AgentMode } from "../agent-sdk-types.js";
|
import type { AgentCapabilityFlags, AgentMode } from "../agent-sdk-types.js";
|
||||||
import type { ProviderRuntimeSettings } from "../provider-launch-config.js";
|
import type { ProviderRuntimeSettings } from "../provider-launch-config.js";
|
||||||
import { findExecutable } from "../../../utils/executable.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 {
|
import {
|
||||||
formatDiagnosticStatus,
|
formatDiagnosticStatus,
|
||||||
formatProviderDiagnostic,
|
formatProviderDiagnostic,
|
||||||
@@ -22,21 +29,31 @@ const COPILOT_CAPABILITIES: AgentCapabilityFlags = {
|
|||||||
supportsToolInvocations: true,
|
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",
|
label: "Agent",
|
||||||
description: "Default agent mode for conversational interactions",
|
description: "Default agent mode for conversational interactions",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "https://agentclientprotocol.com/protocol/session-modes#plan",
|
id: COPILOT_PLAN_MODE_ID,
|
||||||
label: "Plan",
|
label: "Plan",
|
||||||
description: "Plan mode for creating and executing multi-step plans",
|
description: "Plan mode for creating and executing multi-step plans",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "https://agentclientprotocol.com/protocol/session-modes#autopilot",
|
id: COPILOT_ALLOW_ALL_MODE_ID,
|
||||||
label: "Autopilot",
|
label: "Allow All",
|
||||||
description: "Autonomous mode that runs until task completion without user interaction",
|
description: "Automatically approves all Copilot tool, path, and URL requests.",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -53,6 +70,11 @@ export class CopilotACPAgentClient extends ACPAgentClient {
|
|||||||
runtimeSettings: options.runtimeSettings,
|
runtimeSettings: options.runtimeSettings,
|
||||||
defaultCommand: ["copilot", "--acp"],
|
defaultCommand: ["copilot", "--acp"],
|
||||||
defaultModes: COPILOT_MODES,
|
defaultModes: COPILOT_MODES,
|
||||||
|
sessionResponseTransformer: transformCopilotSessionResponse,
|
||||||
|
configOptionsTransformer: transformCopilotConfigOptions,
|
||||||
|
modeIdTransformer: transformCopilotModeId,
|
||||||
|
providerModeWriter: writeCopilotProviderMode,
|
||||||
|
beforeModeWriter: beforeCopilotModeWriter,
|
||||||
capabilities: COPILOT_CAPABILITIES,
|
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",
|
provider: "copilot",
|
||||||
model: "claude-haiku-4.5",
|
model: "claude-haiku-4.5",
|
||||||
modes: {
|
modes: {
|
||||||
full: "https://agentclientprotocol.com/protocol/session-modes#autopilot",
|
full: "allow-all",
|
||||||
ask: "https://agentclientprotocol.com/protocol/session-modes#agent",
|
ask: "https://agentclientprotocol.com/protocol/session-modes#agent",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user