diff --git a/docs/providers.md b/docs/providers.md index 992b6c555f..fa70bf8228 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -8,7 +8,8 @@ This guide walks through adding a new agent provider end-to-end. There are two i names and nesting are the provider's native contract; options are not portable between providers. Paseo validates the object with the selected provider's strict schema before constructing a session. Unknown keys fail with their `providerOptions.*` path. Paseo-owned controls such as cwd, model, -prompt, environment, session identity, MCP transport, callbacks, and hooks cannot be passed here. +prompt, environment, session identity, MCP transport, callbacks, and hooks are not accepted as +top-level provider options. This Paseo version accepts these keys: @@ -19,10 +20,18 @@ This Paseo version accepts these keys: `allow_local_binding`, `allow_upstream_proxy`, `dangerously_allow_all_unix_sockets`, `dangerously_allow_non_loopback_proxy`, `domains`, and `unix_sockets`. See the [Codex configuration reference](https://developers.openai.com/codex/config-reference). -- **Claude:** `allowedTools`, `disallowedTools`, `additionalDirectories`, `sandbox`, and `settings`. - The accepted sandbox fields cover enablement, fail-if-unavailable behavior, excluded and - unsandboxed commands, filesystem read/write rules, network domain/socket/local-binding rules, - weaker nested sandboxing, ignored violations, and the ripgrep command. `settings` accepts native +- **Claude:** `allowedTools`, `disallowedTools`, `additionalDirectories`, `extraArgs`, `sandbox`, and + `settings`. `providerOptions.extraArgs` passes the SDK's documented + [`Options.extraArgs`](https://platform.claude.com/docs/en/agent-sdk/typescript#options) map + unchanged: keys omit the leading `--`, string values supply an argument value, and `null` + supplies a boolean flag. For example, `providerOptions: { extraArgs: { chrome: null } }` + passes `--chrome`, and `providerOptions: { extraArgs: { model: "x" } }` passes `--model x`. + Set it in session configuration or a plugin's `server.before("agent.create", ...)` hook; see + [plugin configuration hooks](../public-docs/plugins/reference.md#change-configuration-and-inject-an-mcp-server). Values are literal; + shell expressions such as `$(command)` are not evaluated. The accepted sandbox + fields cover enablement, fail-if-unavailable behavior, excluded and unsandboxed commands, + filesystem read/write rules, network domain/socket/local-binding rules, weaker nested + sandboxing, ignored violations, and the ripgrep command. `settings` accepts native `permissions.{allow,ask,deny}` and sandbox settings. See the [Claude Agent SDK TypeScript reference](https://platform.claude.com/docs/en/agent-sdk/typescript) and [Claude settings reference](https://code.claude.com/docs/en/settings). diff --git a/packages/server/src/server/agent/provider-options.test.ts b/packages/server/src/server/agent/provider-options.test.ts index 911c6dcddd..9b642d8b5e 100644 --- a/packages/server/src/server/agent/provider-options.test.ts +++ b/packages/server/src/server/agent/provider-options.test.ts @@ -51,6 +51,7 @@ describe("provider-owned option schemas", () => { ClaudeProviderOptionsSchema.parse({ allowedTools: ["Read"], disallowedTools: ["Bash(rm *)"], + extraArgs: { chrome: null, model: "x" }, sandbox: { enabled: true, failIfUnavailable: true, @@ -63,7 +64,26 @@ describe("provider-owned option schemas", () => { }, settings: { permissions: { ask: ["Bash(*)"], deny: ["Edit(.env)"] } }, }), - ).toMatchObject({ sandbox: { enabled: true, failIfUnavailable: true } }); + ).toMatchObject({ + extraArgs: { chrome: null, model: "x" }, + sandbox: { enabled: true, failIfUnavailable: true }, + }); + }); + + test.each([true, 42, ["x"]])("rejects invalid Claude extra argument values: %j", (value) => { + expect(() => + validateProviderOptions("claude", ClaudeProviderOptionsSchema, { + extraArgs: { chrome: value }, + }), + ).toThrow("providerOptions.extraArgs.chrome"); + }); + + test("rejects a raw argument list for the SDK argument map", () => { + expect(() => + validateProviderOptions("claude", ClaudeProviderOptionsSchema, { + extraArgs: ["--chrome"], + }), + ).toThrow("providerOptions.extraArgs"); }); test("reports the exact invalid Claude option path", () => { diff --git a/packages/server/src/server/agent/providers/claude/agent.test.ts b/packages/server/src/server/agent/providers/claude/agent.test.ts index 4a9538d00a..96022d851d 100644 --- a/packages/server/src/server/agent/providers/claude/agent.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.test.ts @@ -785,6 +785,29 @@ describe("ClaudeAgentSession features", () => { await session.close(); }); + test("passes extra Claude Code CLI arguments to the SDK", async () => { + const { queryFactory, launches } = createQueryMock(); + const client = new ClaudeAgentClient({ + logger, + queryFactory, + resolveBinary: async () => "/test/claude/bin", + }); + const session = await client.createSession({ + provider: "claude", + cwd: process.cwd(), + providerOptions: { + extraArgs: { chrome: null, model: "x" }, + }, + }); + + await expect(session.startTurn("hello")).resolves.toEqual({ + turnId: expect.stringMatching(/^foreground-turn-/), + }); + + expect(launches[0]?.options.extraArgs).toEqual({ chrome: null, model: "x" }); + await session.close(); + }); + test("lists fast mode only for supported Opus models", async () => { const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" }); diff --git a/packages/server/src/server/agent/providers/claude/options.ts b/packages/server/src/server/agent/providers/claude/options.ts index 78766c504b..591accce19 100644 --- a/packages/server/src/server/agent/providers/claude/options.ts +++ b/packages/server/src/server/agent/providers/claude/options.ts @@ -48,6 +48,7 @@ export const ClaudeProviderOptionsSchema = z allowedTools: z.array(z.string()).optional(), disallowedTools: z.array(z.string()).optional(), additionalDirectories: z.array(z.string()).optional(), + extraArgs: z.record(z.string(), z.string().nullable()).optional(), sandbox: z .object({ enabled: z.boolean().optional(),