diff --git a/packages/server/src/server/bootstrap.smoke.test.ts b/packages/server/src/server/bootstrap.smoke.test.ts index 620309459..744cfa027 100644 --- a/packages/server/src/server/bootstrap.smoke.test.ts +++ b/packages/server/src/server/bootstrap.smoke.test.ts @@ -41,6 +41,52 @@ describe("paseo daemon bootstrap", () => { } }); + test("redacts Agent MCP debug request credentials and bodies", async () => { + const logLines: string[] = []; + const logger = pino( + { level: "debug" }, + { + write: (line: string) => { + logLines.push(line); + }, + }, + ); + const daemonHandle = await createTestPaseoDaemon({ + logger, + mcpDebug: true, + }); + + try { + const response = await fetch(`http://127.0.0.1:${daemonHandle.port}/mcp/agents`, { + method: "POST", + headers: { + Authorization: "Bearer secret-debug-token", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { + apiKey: "secret-body-token", + }, + }), + }); + + expect(response.status).toBe(400); + const logs = logLines.join("\n"); + expect(logs).toContain("Agent MCP request"); + expect(logs).toContain("[redacted]"); + expect(logs).toContain('"method":"tools/call"'); + expect(logs).toContain('"hasParams":true'); + expect(logs).not.toContain("secret-debug-token"); + expect(logs).not.toContain("secret-body-token"); + expect(logs).not.toContain("apiKey"); + } finally { + await daemonHandle.close(); + } + }); + test("fails fast when OpenAI speech provider is configured without credentials", async () => { const paseoHomeRoot = await mkdtemp(path.join(os.tmpdir(), "paseo-openai-config-")); const paseoHome = path.join(paseoHomeRoot, ".paseo"); diff --git a/packages/server/src/server/bootstrap.ts b/packages/server/src/server/bootstrap.ts index 37134e0d8..eb505721f 100644 --- a/packages/server/src/server/bootstrap.ts +++ b/packages/server/src/server/bootstrap.ts @@ -141,6 +141,9 @@ import { createRequireBearerMiddleware, type DaemonAuthConfig } from "./auth.js" type AgentMcpTransportMap = Map; +const MAX_MCP_DEBUG_BATCH_ITEMS = 10; +const REDACTED_LOG_VALUE = "[redacted]"; + function formatHostForHttpUrl(host: string): string { return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host; } @@ -155,6 +158,38 @@ function createAgentMcpBaseUrl(listenTarget: ListenTarget | null): string | null ).toString(); } +function summarizeAgentMcpDebugMessage(body: unknown): Record { + if (!body || typeof body !== "object" || Array.isArray(body)) { + return { + type: body === null ? "null" : typeof body, + }; + } + + const record = body as Record; + const method = typeof record.method === "string" ? record.method : undefined; + return { + type: "object", + ...(typeof record.jsonrpc === "string" ? { jsonrpc: record.jsonrpc } : {}), + ...(method ? { method } : {}), + hasId: Object.prototype.hasOwnProperty.call(record, "id"), + hasParams: Object.prototype.hasOwnProperty.call(record, "params"), + }; +} + +function summarizeAgentMcpDebugBody(body: unknown): Record { + if (!Array.isArray(body)) { + return summarizeAgentMcpDebugMessage(body); + } + + const messages = body.slice(0, MAX_MCP_DEBUG_BATCH_ITEMS).map(summarizeAgentMcpDebugMessage); + return { + type: "batch", + count: body.length, + messages, + ...(body.length > messages.length ? { omitted: body.length - messages.length } : {}), + }; +} + export type PaseoOpenAIConfig = OpenAiSpeechProviderConfig; export type PaseoLocalSpeechConfig = LocalSpeechProviderConfig; @@ -671,8 +706,8 @@ export async function createPaseoDaemon( method: req.method, url: req.originalUrl, sessionId: req.header("mcp-session-id"), - authorization: req.header("authorization"), - body: req.body, + authorization: req.header("authorization") ? REDACTED_LOG_VALUE : undefined, + body: summarizeAgentMcpDebugBody(req.body), }, "Agent MCP request", ); diff --git a/packages/server/src/server/test-utils/paseo-daemon.ts b/packages/server/src/server/test-utils/paseo-daemon.ts index 58d347b65..4c67a4840 100644 --- a/packages/server/src/server/test-utils/paseo-daemon.ts +++ b/packages/server/src/server/test-utils/paseo-daemon.ts @@ -18,6 +18,7 @@ interface TestPaseoDaemonOptions { corsAllowedOrigins?: string[]; listen?: string; logger?: Parameters[1]; + mcpDebug?: boolean; relayEnabled?: boolean; relayEndpoint?: string; agentClients?: Partial>; @@ -152,7 +153,7 @@ async function prepareTestDaemonConfig( hostnames: true, mcpEnabled: true, staticDir, - mcpDebug: false, + mcpDebug: options.mcpDebug ?? false, agentClients: options.agentClients ?? createTestAgentClients(), agentStoragePath: path.join(paseoHome, "agents"), relayEnabled: options.relayEnabled ?? false,