Skip to content

Commit f8eca49

Browse files
committed
fix(trust): quote whitespace argv via one shared MCP trust prompt helper
1 parent ec08d1b commit f8eca49

4 files changed

Lines changed: 40 additions & 16 deletions

File tree

‎src/exec/runner.ts‎

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
registerSourceCredential,
2121
} from "../config/source-credentials.js";
2222
import { formatDirectorSystemPrompt } from "../agent/directors/identity.js";
23+
import { formatMcpTrustQuestion } from "../trust/project-trust.js";
2324
import { DIRECTOR_REGISTRY } from "../agent/directors/registry.js";
2425
import type { DirectorId, DirectorPackage } from "../agent/directors/types.js";
2526
import { submitOutputDefinition } from "../agent/director.js";
@@ -164,14 +165,7 @@ const logger = getLogger([LOG_NAMESPACE_ROOT, "exec"]);
164165
const SELECTED_PROVIDER_FAILURE = "SelectedProviderFailure";
165166

166167
export function formatExecMcpTrustQuestion(server: MCPServerConfig): string {
167-
return (
168-
`Trust local MCP server "${server.name}" for this project?` +
169-
(server.command !== undefined
170-
? `\nCommand: ${server.command}${(server.args ?? []).length > 0 ? ` ${(server.args ?? []).join(" ")}` : ""}`
171-
: server.url !== undefined
172-
? `\nURL: ${server.url}`
173-
: "")
174-
);
168+
return formatMcpTrustQuestion(server);
175169
}
176170

177171
export async function refreshSelectedProviderCredential<T>(

‎src/trust/project-trust.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,32 @@ export function mcpServerFingerprint(server: MCPServerConfig): string {
298298
return createHash("sha256").update(payload).digest("hex");
299299
}
300300

301+
// Display-only argv quoting for the MCP trust prompt: an arg containing
302+
// whitespace (or a quote, or empty) renders double-quoted so ["a b"] and
303+
// ["a", "b"] never look alike. Approval identity still comes from
304+
// mcpServerFingerprint above, never from this rendering.
305+
function quoteMcpTrustArg(arg: string): string {
306+
if (arg !== "" && !/[\s"]/.test(arg)) return arg;
307+
return `"${arg.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
308+
}
309+
310+
function formatMcpSpawnCommand(command: string, args: string[]): string {
311+
return args.length === 0
312+
? command
313+
: `${command} ${args.map(quoteMcpTrustArg).join(" ")}`;
314+
}
315+
316+
export function formatMcpTrustQuestion(server: MCPServerConfig): string {
317+
return (
318+
`Trust local MCP server "${server.name}" for this project?` +
319+
(server.command !== undefined
320+
? `\nCommand: ${formatMcpSpawnCommand(server.command, server.args ?? [])}`
321+
: server.url !== undefined
322+
? `\nURL: ${server.url}`
323+
: "")
324+
);
325+
}
326+
301327
export function isMcpServerTrusted(
302328
store: ProjectTrustStore,
303329
server: MCPServerConfig,

‎src/tui/runner/session.ts‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
localSettingsPath,
1616
shellTimeoutFromSettings,
1717
toolWatchdogFromSettings,
18+
type MCPServerConfig,
1819
} from "../../config/settings.js";
1920
import { isCodexProviderName } from "../../config/codex-providers.js";
2021
import { peekSourceCredentialSecret } from "../../config/source-credentials.js";
@@ -130,6 +131,11 @@ import {
130131
type TUIStart,
131132
} from "./state.js";
132133
import { createParkedOverlayAbortBinding } from "./parked-overlay-abort.js";
134+
import { formatMcpTrustQuestion } from "../../trust/project-trust.js";
135+
136+
export function formatTuiMcpTrustQuestion(server: MCPServerConfig): string {
137+
return formatMcpTrustQuestion(server);
138+
}
133139

134140
export async function assembleTUISession(
135141
state: RunnerState,
@@ -414,13 +420,7 @@ export async function assembleTUISession(
414420
const timeout = approvalTimeout();
415421
const event: OperatorGateEvent = {
416422
id: randomUUID(),
417-
question:
418-
`Trust local MCP server "${server.name}" for this project?` +
419-
(server.command !== undefined
420-
? `\nCommand: ${server.command}${(server.args ?? []).length > 0 ? ` ${(server.args ?? []).join(" ")}` : ""}`
421-
: server.url !== undefined
422-
? `\nURL: ${server.url}`
423-
: ""),
423+
question: formatTuiMcpTrustQuestion(server),
424424
options: ["Trust and connect", "Deny"],
425425
resolve: finish,
426426
...(timeout !== undefined ? timeout : {}),

‎tests/unit/tui/mcp-trust-prompt-parity.test.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import { formatExecMcpTrustQuestion } from "../../../src/exec/runner.js";
44
import { formatTuiMcpTrustQuestion } from "../../../src/tui/runner/session.js";
55

66
const parityCases: MCPServerConfig[] = [
7-
{ name: "plain-stdio", command: "node", args: ["server.js", "--port", "3000"] },
7+
{
8+
name: "plain-stdio",
9+
command: "node",
10+
args: ["server.js", "--port", "3000"],
11+
},
812
{ name: "no-args", command: "node" },
913
{ name: "empty-args", command: "node", args: [] },
1014
{ name: "spaced-path", command: "server", args: ["--dir", "/tmp/my work"] },

0 commit comments

Comments
 (0)