mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
@@ -457,6 +457,38 @@ Paseo tools such as subagent creation come from the shared internal tool catalog
|
||||
}
|
||||
```
|
||||
|
||||
ACP agents normally receive access to Paseo's filesystem and terminal through
|
||||
the client capabilities advertised during initialization. For a compliant agent
|
||||
running in a container or remote environment, disable those capabilities so it
|
||||
keeps file and command execution in the agent environment:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"providers": {
|
||||
"container-agent": {
|
||||
"extends": "acp",
|
||||
"label": "Container Agent",
|
||||
"command": ["container-agent", "acp"],
|
||||
"params": {
|
||||
"clientCapabilities": {
|
||||
"fs": {
|
||||
"readTextFile": false,
|
||||
"writeTextFile": false
|
||||
},
|
||||
"terminal": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The defaults remain enabled for compatibility with local ACP agents. Configure
|
||||
both environments with equivalent absolute workspace paths before enabling
|
||||
client filesystem or terminal delegation across a process boundary.
|
||||
|
||||
### Generic ACP diagnostics
|
||||
|
||||
Paseo diagnostics for `extends: "acp"` providers report the configured command, resolved launcher binary, version output, ACP `initialize`, ACP `session/new`, model count, modes, and final status.
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
ACPAgentSession,
|
||||
type SpawnedACPProcess,
|
||||
type SessionStateResponse,
|
||||
buildACPClientCapabilities,
|
||||
createLoggedNdJsonStream,
|
||||
deriveModelDefinitionsFromACP,
|
||||
deriveModesFromACP,
|
||||
@@ -50,6 +51,39 @@ import { buildStringCommandShellInvocation } from "../../../utils/string-command
|
||||
import { asInternals } from "../../test-utils/class-mocks.js";
|
||||
import * as spawnUtils from "../../../utils/spawn.js";
|
||||
|
||||
describe("buildACPClientCapabilities", () => {
|
||||
test("preserves the default client filesystem and terminal capabilities", () => {
|
||||
expect(buildACPClientCapabilities()).toEqual({
|
||||
fs: {
|
||||
readTextFile: true,
|
||||
writeTextFile: true,
|
||||
},
|
||||
terminal: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("applies provider capability overrides without dropping metadata", () => {
|
||||
expect(
|
||||
buildACPClientCapabilities(
|
||||
{ source: "provider" },
|
||||
{
|
||||
fs: {
|
||||
readTextFile: false,
|
||||
},
|
||||
terminal: false,
|
||||
},
|
||||
),
|
||||
).toEqual({
|
||||
fs: {
|
||||
readTextFile: false,
|
||||
writeTextFile: true,
|
||||
},
|
||||
terminal: false,
|
||||
_meta: { source: "provider" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
interface ACPSessionInternals {
|
||||
sessionId: string | null;
|
||||
connection: { prompt: (...args: unknown[]) => Promise<PromptResponse> };
|
||||
|
||||
@@ -239,14 +239,19 @@ const BASE_ACP_CLIENT_CAPABILITIES: ACPClientCapabilities = {
|
||||
|
||||
export type ACPClientCapabilityMeta = Record<string, unknown>;
|
||||
|
||||
export function buildACPClientCapabilities(meta?: ACPClientCapabilityMeta): ACPClientCapabilities {
|
||||
if (!meta || Object.keys(meta).length === 0) {
|
||||
return BASE_ACP_CLIENT_CAPABILITIES;
|
||||
}
|
||||
return {
|
||||
export function buildACPClientCapabilities(
|
||||
meta?: ACPClientCapabilityMeta,
|
||||
override?: ACPClientCapabilities,
|
||||
): ACPClientCapabilities {
|
||||
const capabilities: ACPClientCapabilities = {
|
||||
...BASE_ACP_CLIENT_CAPABILITIES,
|
||||
_meta: meta,
|
||||
...override,
|
||||
fs: {
|
||||
...BASE_ACP_CLIENT_CAPABILITIES.fs,
|
||||
...override?.fs,
|
||||
},
|
||||
};
|
||||
return meta && Object.keys(meta).length > 0 ? { ...capabilities, _meta: meta } : capabilities;
|
||||
}
|
||||
|
||||
// Suppress interactive auth side-effects (e.g. Gemini CLI opening a Google
|
||||
@@ -371,6 +376,7 @@ interface ACPAgentClientOptions {
|
||||
sessionResponseTransformer?: (response: SessionStateResponse) => SessionStateResponse;
|
||||
configOptionsTransformer?: (configOptions: SessionConfigOption[]) => SessionConfigOption[];
|
||||
configFeatureOptions?: ACPConfigFeatureOption[];
|
||||
clientCapabilities?: ACPClientCapabilities;
|
||||
clientCapabilityMeta?: ACPClientCapabilityMeta;
|
||||
modeIdTransformer?: (modeId: string) => string | null;
|
||||
toolSnapshotTransformer?: (snapshot: ACPToolSnapshot) => ACPToolSnapshot;
|
||||
@@ -400,6 +406,7 @@ interface ACPAgentSessionOptions {
|
||||
sessionResponseTransformer?: (response: SessionStateResponse) => SessionStateResponse;
|
||||
configOptionsTransformer?: (configOptions: SessionConfigOption[]) => SessionConfigOption[];
|
||||
configFeatureOptions?: ACPConfigFeatureOption[];
|
||||
clientCapabilities?: ACPClientCapabilities;
|
||||
clientCapabilityMeta?: ACPClientCapabilityMeta;
|
||||
modeIdTransformer?: (modeId: string) => string | null;
|
||||
toolSnapshotTransformer?: (snapshot: ACPToolSnapshot) => ACPToolSnapshot;
|
||||
@@ -708,6 +715,7 @@ export class ACPAgentClient implements AgentClient {
|
||||
configOptions: SessionConfigOption[],
|
||||
) => SessionConfigOption[];
|
||||
private readonly configFeatureOptions: ACPConfigFeatureOption[];
|
||||
private readonly clientCapabilities?: ACPClientCapabilities;
|
||||
private readonly clientCapabilityMeta?: ACPClientCapabilityMeta;
|
||||
private readonly modeIdTransformer?: (modeId: string) => string | null;
|
||||
private readonly toolSnapshotTransformer?: (snapshot: ACPToolSnapshot) => ACPToolSnapshot;
|
||||
@@ -742,6 +750,7 @@ export class ACPAgentClient implements AgentClient {
|
||||
this.sessionResponseTransformer = options.sessionResponseTransformer;
|
||||
this.configOptionsTransformer = options.configOptionsTransformer;
|
||||
this.configFeatureOptions = options.configFeatureOptions ?? [];
|
||||
this.clientCapabilities = options.clientCapabilities;
|
||||
this.clientCapabilityMeta = options.clientCapabilityMeta;
|
||||
this.modeIdTransformer = options.modeIdTransformer;
|
||||
this.toolSnapshotTransformer = options.toolSnapshotTransformer;
|
||||
@@ -770,6 +779,7 @@ export class ACPAgentClient implements AgentClient {
|
||||
sessionResponseTransformer: this.sessionResponseTransformer,
|
||||
configOptionsTransformer: this.configOptionsTransformer,
|
||||
configFeatureOptions: this.configFeatureOptions,
|
||||
clientCapabilities: this.clientCapabilities,
|
||||
clientCapabilityMeta: this.clientCapabilityMeta,
|
||||
modeIdTransformer: this.modeIdTransformer,
|
||||
toolSnapshotTransformer: this.toolSnapshotTransformer,
|
||||
@@ -819,6 +829,7 @@ export class ACPAgentClient implements AgentClient {
|
||||
sessionResponseTransformer: this.sessionResponseTransformer,
|
||||
configOptionsTransformer: this.configOptionsTransformer,
|
||||
configFeatureOptions: this.configFeatureOptions,
|
||||
clientCapabilities: this.clientCapabilities,
|
||||
clientCapabilityMeta: this.clientCapabilityMeta,
|
||||
modeIdTransformer: this.modeIdTransformer,
|
||||
toolSnapshotTransformer: this.toolSnapshotTransformer,
|
||||
@@ -1057,7 +1068,10 @@ export class ACPAgentClient implements AgentClient {
|
||||
Promise.race([
|
||||
transport.connection.initialize({
|
||||
protocolVersion: PROTOCOL_VERSION,
|
||||
clientCapabilities: buildACPClientCapabilities(this.clientCapabilityMeta),
|
||||
clientCapabilities: buildACPClientCapabilities(
|
||||
this.clientCapabilityMeta,
|
||||
this.clientCapabilities,
|
||||
),
|
||||
clientInfo: { name: "Paseo", version: "dev" },
|
||||
}),
|
||||
transport.spawnError,
|
||||
@@ -1269,6 +1283,7 @@ export class ACPAgentSession implements AgentSession, ACPClient {
|
||||
configOptions: SessionConfigOption[],
|
||||
) => SessionConfigOption[];
|
||||
private readonly configFeatureOptions: ACPConfigFeatureOption[];
|
||||
private readonly clientCapabilities?: ACPClientCapabilities;
|
||||
private readonly clientCapabilityMeta?: ACPClientCapabilityMeta;
|
||||
private readonly modeIdTransformer?: (modeId: string) => string | null;
|
||||
private readonly toolSnapshotTransformer?: (snapshot: ACPToolSnapshot) => ACPToolSnapshot;
|
||||
@@ -1334,6 +1349,7 @@ export class ACPAgentSession implements AgentSession, ACPClient {
|
||||
this.sessionResponseTransformer = options.sessionResponseTransformer;
|
||||
this.configOptionsTransformer = options.configOptionsTransformer;
|
||||
this.configFeatureOptions = options.configFeatureOptions ?? [];
|
||||
this.clientCapabilities = options.clientCapabilities;
|
||||
this.clientCapabilityMeta = options.clientCapabilityMeta;
|
||||
this.modeIdTransformer = options.modeIdTransformer;
|
||||
this.toolSnapshotTransformer = options.toolSnapshotTransformer;
|
||||
@@ -2320,7 +2336,10 @@ export class ACPAgentSession implements AgentSession, ACPClient {
|
||||
const initialize = await this.runACPRequest(() =>
|
||||
connection.initialize({
|
||||
protocolVersion: PROTOCOL_VERSION,
|
||||
clientCapabilities: buildACPClientCapabilities(this.clientCapabilityMeta),
|
||||
clientCapabilities: buildACPClientCapabilities(
|
||||
this.clientCapabilityMeta,
|
||||
this.clientCapabilities,
|
||||
),
|
||||
clientInfo: { name: "Paseo", version: "dev" },
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -9,6 +9,13 @@ import { buildVersionProbeCommand, GenericACPAgentClient } from "./generic-acp-a
|
||||
|
||||
const TEST_ACP_TIMEOUT_MS = 1_000;
|
||||
|
||||
function parseInitializeTrace(content: string): Array<{ clientCapabilities: unknown }> {
|
||||
return content
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line) as { clientCapabilities: unknown });
|
||||
}
|
||||
|
||||
describe("GenericACPAgentClient diagnostics", () => {
|
||||
test("probes npx-backed agent packages instead of npx itself", () => {
|
||||
expect(buildVersionProbeCommand(["npx", "-y", "@google/gemini-cli@0.41.1", "--acp"])).toEqual({
|
||||
@@ -92,6 +99,53 @@ describe("GenericACPAgentClient diagnostics", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("sends configured client capabilities in catalog and live session initialization", async () => {
|
||||
await withFakeACPAgent("success", async (scriptPath, mode, testDir) => {
|
||||
const initializeTracePath = path.join(testDir, "initialize.jsonl");
|
||||
const client = new GenericACPAgentClient({
|
||||
logger: createTestLogger(),
|
||||
command: [process.execPath, scriptPath, mode, "", initializeTracePath],
|
||||
providerParams: {
|
||||
clientCapabilities: {
|
||||
fs: {
|
||||
readTextFile: false,
|
||||
writeTextFile: false,
|
||||
},
|
||||
terminal: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await client.fetchCatalog({ cwd: testDir, force: true, timeoutMs: TEST_ACP_TIMEOUT_MS });
|
||||
const session = await client.createSession({ provider: "acp", cwd: testDir });
|
||||
await session.close();
|
||||
|
||||
const initializeRequests = parseInitializeTrace(await readFile(initializeTracePath, "utf8"));
|
||||
|
||||
expect(initializeRequests).toHaveLength(2);
|
||||
expect(initializeRequests).toEqual([
|
||||
{
|
||||
clientCapabilities: {
|
||||
fs: {
|
||||
readTextFile: false,
|
||||
writeTextFile: false,
|
||||
},
|
||||
terminal: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
clientCapabilities: {
|
||||
fs: {
|
||||
readTextFile: false,
|
||||
writeTextFile: false,
|
||||
},
|
||||
terminal: false,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test("reports a missing launcher without dropping the rest of the diagnostic", async () => {
|
||||
await withTempDir("paseo-missing-acp-agent-", async (testDir) => {
|
||||
const missingCommand = path.join(testDir, "missing-acp-agent");
|
||||
@@ -167,6 +221,7 @@ const readline = require("node:readline");
|
||||
|
||||
const mode = process.argv[2];
|
||||
const pidPath = process.argv[3];
|
||||
const initializeTracePath = process.argv[4];
|
||||
if (pidPath) {
|
||||
fs.writeFileSync(pidPath, String(process.pid));
|
||||
}
|
||||
@@ -179,6 +234,12 @@ function send(id, result) {
|
||||
rl.on("line", (line) => {
|
||||
const message = JSON.parse(line);
|
||||
if (message.method === "initialize") {
|
||||
if (initializeTracePath) {
|
||||
fs.appendFileSync(
|
||||
initializeTracePath,
|
||||
JSON.stringify({ clientCapabilities: message.params?.clientCapabilities }) + "\\n",
|
||||
);
|
||||
}
|
||||
send(message.id, {
|
||||
protocolVersion: message.params?.protocolVersion ?? 1,
|
||||
agentCapabilities: {},
|
||||
|
||||
@@ -20,6 +20,17 @@ import {
|
||||
export const GenericACPProviderParamsSchema = z
|
||||
.object({
|
||||
supportsMcpServers: z.boolean().optional(),
|
||||
clientCapabilities: z
|
||||
.object({
|
||||
fs: z
|
||||
.object({
|
||||
readTextFile: z.boolean().optional(),
|
||||
writeTextFile: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
terminal: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
@@ -47,6 +58,7 @@ export class GenericACPAgentClient extends ACPAgentClient {
|
||||
private readonly diagnosticPhaseTimeoutMs?: number;
|
||||
|
||||
constructor(options: GenericACPAgentClientOptions) {
|
||||
const providerParams = parseGenericACPProviderParams(options.providerParams);
|
||||
super({
|
||||
provider: "acp",
|
||||
logger: options.logger,
|
||||
@@ -54,9 +66,10 @@ export class GenericACPAgentClient extends ACPAgentClient {
|
||||
env: options.env,
|
||||
},
|
||||
defaultCommand: options.command,
|
||||
capabilities: buildGenericACPCapabilities(options),
|
||||
capabilities: buildGenericACPCapabilities(providerParams),
|
||||
waitForInitialCommands: options.waitForInitialCommands,
|
||||
initialCommandsWaitTimeoutMs: options.initialCommandsWaitTimeoutMs,
|
||||
clientCapabilities: providerParams.clientCapabilities,
|
||||
clientCapabilityMeta: options.clientCapabilityMeta,
|
||||
configFeatureOptions: options.configFeatureOptions,
|
||||
extensionCommandsParser: options.extensionCommandsParser,
|
||||
@@ -145,8 +158,7 @@ export class GenericACPAgentClient extends ACPAgentClient {
|
||||
}
|
||||
}
|
||||
|
||||
function buildGenericACPCapabilities(options: GenericACPAgentClientOptions): AgentCapabilityFlags {
|
||||
const params = parseGenericACPProviderParams(options.providerParams);
|
||||
function buildGenericACPCapabilities(params: GenericACPProviderParams): AgentCapabilityFlags {
|
||||
return {
|
||||
...DEFAULT_ACP_CAPABILITIES,
|
||||
supportsMcpServers: params.supportsMcpServers ?? DEFAULT_ACP_CAPABILITIES.supportsMcpServers,
|
||||
|
||||
Reference in New Issue
Block a user