Strip relay and pairing URLs from daemon logs
The pairing URL and relay WebSocket URLs contain secrets (serverId, daemon public key, relay endpoint) that should never leak into log files. Access to pairing info should only happen via explicit `paseo daemon pair` commands. Removed `url` from all relay-transport and bootstrap log entries while keeping connectionId and log messages for debuggability.
This commit is contained in:
@@ -6,7 +6,6 @@ import { randomUUID } from "node:crypto";
|
||||
import { hostname as getHostname } from "node:os";
|
||||
import path from "node:path";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
||||
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
||||
import type { Logger } from "pino";
|
||||
|
||||
@@ -96,7 +95,11 @@ import { AgentManager } from "./agent/agent-manager.js";
|
||||
import { AgentStorage } from "./agent/agent-storage.js";
|
||||
import { attachAgentStoragePersistence } from "./persistence-hooks.js";
|
||||
import { createAgentMcpServer } from "./agent/mcp-server.js";
|
||||
import { createAllClients, shutdownProviders } from "./agent/provider-registry.js";
|
||||
import {
|
||||
buildProviderRegistry,
|
||||
createAllClients,
|
||||
shutdownProviders,
|
||||
} from "./agent/provider-registry.js";
|
||||
import { bootstrapWorkspaceRegistries } from "./workspace-registry-bootstrap.js";
|
||||
import { FileBackedProjectRegistry, FileBackedWorkspaceRegistry } from "./workspace-registry.js";
|
||||
import { FileBackedChatService } from "./chat/chat-service.js";
|
||||
@@ -112,36 +115,21 @@ import { resolveDaemonVersion } from "./daemon-version.js";
|
||||
import type { AgentClient, AgentProvider } from "./agent/agent-sdk-types.js";
|
||||
import type { AgentProviderRuntimeSettingsMap } from "./agent/provider-launch-config.js";
|
||||
import { isHostAllowed, type AllowedHostsConfig } from "./allowed-hosts.js";
|
||||
import {
|
||||
createVoiceMcpSocketBridgeManager,
|
||||
type VoiceMcpSocketBridgeManager,
|
||||
} from "./voice-mcp-bridge.js";
|
||||
import { resolveVoiceMcpBridgeFromRuntime } from "./voice-mcp-bridge-command.js";
|
||||
|
||||
type AgentMcpTransportMap = Map<string, StreamableHTTPServerTransport>;
|
||||
|
||||
function resolveVoiceMcpBridgeCommand(
|
||||
logger: Logger,
|
||||
): { command: string; baseArgs: string[] } | null {
|
||||
try {
|
||||
const decision = resolveVoiceMcpBridgeFromRuntime({
|
||||
bootstrapModuleUrl: import.meta.url,
|
||||
execPath: process.execPath,
|
||||
explicitScriptPath: process.env.PASEO_MCP_STDIO_SOCKET_BRIDGE_SCRIPT,
|
||||
});
|
||||
logger.info(
|
||||
{
|
||||
source: decision.source,
|
||||
command: decision.resolved.command,
|
||||
baseArgs: decision.resolved.baseArgs,
|
||||
},
|
||||
"Resolved voice MCP bridge command",
|
||||
);
|
||||
return decision.resolved;
|
||||
} catch (err) {
|
||||
logger.warn({ err }, "Voice MCP bridge script not available — voice MCP via stdio disabled");
|
||||
function formatHostForHttpUrl(host: string): string {
|
||||
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
||||
}
|
||||
|
||||
function createAgentMcpBaseUrl(listenTarget: ListenTarget | null): string | null {
|
||||
if (!listenTarget || listenTarget.type !== "tcp") {
|
||||
return null;
|
||||
}
|
||||
return new URL(
|
||||
"/mcp/agents",
|
||||
`http://${formatHostForHttpUrl(listenTarget.host)}:${listenTarget.port}`,
|
||||
).toString();
|
||||
}
|
||||
|
||||
export type PaseoOpenAIConfig = OpenAiSpeechProviderConfig;
|
||||
@@ -171,6 +159,7 @@ export type PaseoDaemonConfig = {
|
||||
corsAllowedOrigins: string[];
|
||||
allowedHosts?: AllowedHostsConfig;
|
||||
mcpEnabled?: boolean;
|
||||
mcpInjectIntoAgents?: boolean;
|
||||
staticDir: string;
|
||||
mcpDebug: boolean;
|
||||
agentClients: Partial<Record<AgentProvider, AgentClient>>;
|
||||
@@ -360,6 +349,9 @@ export async function createPaseoDaemon(
|
||||
registry: agentStorage,
|
||||
logger,
|
||||
});
|
||||
const providerRegistry = buildProviderRegistry(logger, {
|
||||
runtimeSettings: config.agentProviderSettings,
|
||||
});
|
||||
|
||||
const terminalManager = createTerminalManager();
|
||||
|
||||
@@ -410,31 +402,9 @@ export async function createPaseoDaemon(
|
||||
);
|
||||
logger.info({ elapsed: elapsed() }, "Preparing voice and MCP runtime");
|
||||
let wsServer: VoiceAssistantWebSocketServer | null = null;
|
||||
let voiceMcpBridgeManager: VoiceMcpSocketBridgeManager | null = null;
|
||||
|
||||
// Create in-memory transport for Session's Agent MCP client (voice assistant tools)
|
||||
const createInMemoryAgentMcpTransport = async (): Promise<InMemoryTransport> => {
|
||||
const agentMcpServer = await createAgentMcpServer({
|
||||
agentManager,
|
||||
agentStorage,
|
||||
terminalManager,
|
||||
paseoHome: config.paseoHome,
|
||||
enableVoiceTools: false,
|
||||
resolveSpeakHandler: (callerAgentId) =>
|
||||
wsServer?.resolveVoiceSpeakHandler(callerAgentId) ?? null,
|
||||
resolveCallerContext: (callerAgentId) =>
|
||||
wsServer?.resolveVoiceCallerContext(callerAgentId) ?? null,
|
||||
logger,
|
||||
});
|
||||
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
|
||||
await agentMcpServer.connect(serverTransport);
|
||||
|
||||
return clientTransport;
|
||||
};
|
||||
|
||||
const mcpEnabled = config.mcpEnabled ?? true;
|
||||
let agentMcpBaseUrl: string | null = null;
|
||||
if (mcpEnabled) {
|
||||
const agentMcpRoute = "/mcp/agents";
|
||||
const agentMcpTransports: AgentMcpTransportMap = new Map();
|
||||
@@ -444,6 +414,8 @@ export async function createPaseoDaemon(
|
||||
agentManager,
|
||||
agentStorage,
|
||||
terminalManager,
|
||||
scheduleService,
|
||||
providerRegistry,
|
||||
paseoHome: config.paseoHome,
|
||||
callerAgentId,
|
||||
enableVoiceTools: false,
|
||||
@@ -554,25 +526,6 @@ export async function createPaseoDaemon(
|
||||
logger.info("Agent MCP HTTP endpoint disabled");
|
||||
}
|
||||
|
||||
const voiceMcpSocketDir = path.join(config.paseoHome, "runtime", "voice-mcp");
|
||||
const voiceMcpBridgeCommand = resolveVoiceMcpBridgeCommand(logger);
|
||||
voiceMcpBridgeManager = createVoiceMcpSocketBridgeManager({
|
||||
runtimeDir: voiceMcpSocketDir,
|
||||
logger,
|
||||
createAgentMcpServerForCaller: async (callerAgentId) => {
|
||||
return createAgentMcpServer({
|
||||
agentManager,
|
||||
agentStorage,
|
||||
terminalManager,
|
||||
paseoHome: config.paseoHome,
|
||||
callerAgentId,
|
||||
voiceOnly: true,
|
||||
resolveSpeakHandler: (agentId) => wsServer?.resolveVoiceSpeakHandler(agentId) ?? null,
|
||||
resolveCallerContext: (agentId) => wsServer?.resolveVoiceCallerContext(agentId) ?? null,
|
||||
logger,
|
||||
});
|
||||
},
|
||||
});
|
||||
const speechService = createSpeechService({
|
||||
logger,
|
||||
openaiConfig: config.openai,
|
||||
@@ -580,55 +533,6 @@ export async function createPaseoDaemon(
|
||||
});
|
||||
logger.info({ elapsed: elapsed() }, "Speech service created");
|
||||
|
||||
wsServer = new VoiceAssistantWebSocketServer(
|
||||
httpServer,
|
||||
logger,
|
||||
serverId,
|
||||
agentManager,
|
||||
agentStorage,
|
||||
downloadTokenStore,
|
||||
config.paseoHome,
|
||||
createInMemoryAgentMcpTransport,
|
||||
{ allowedOrigins, allowedHosts: config.allowedHosts },
|
||||
speechService,
|
||||
terminalManager,
|
||||
{
|
||||
voiceAgentMcpStdio: voiceMcpBridgeCommand
|
||||
? {
|
||||
command: voiceMcpBridgeCommand.command,
|
||||
baseArgs: [...voiceMcpBridgeCommand.baseArgs],
|
||||
env: {
|
||||
ELECTRON_RUN_AS_NODE: "1",
|
||||
PASEO_HOME: config.paseoHome,
|
||||
},
|
||||
}
|
||||
: null,
|
||||
ensureVoiceMcpSocketForAgent: (agentId) =>
|
||||
voiceMcpBridgeManager?.ensureBridgeForCaller(agentId) ??
|
||||
Promise.reject(new Error("Voice MCP bridge manager is not initialized")),
|
||||
removeVoiceMcpSocketForAgent: (agentId) =>
|
||||
voiceMcpBridgeManager?.removeBridgeForCaller(agentId) ?? Promise.resolve(),
|
||||
},
|
||||
{
|
||||
finalTimeoutMs: config.dictationFinalTimeoutMs,
|
||||
},
|
||||
config.agentProviderSettings,
|
||||
daemonVersion,
|
||||
(intent) => {
|
||||
try {
|
||||
config.onLifecycleIntent?.(intent);
|
||||
} catch (error) {
|
||||
logger.error({ err: error, intent }, "Failed to handle daemon lifecycle intent");
|
||||
}
|
||||
},
|
||||
projectRegistry,
|
||||
workspaceRegistry,
|
||||
chatService,
|
||||
loopService,
|
||||
scheduleService,
|
||||
checkoutDiffManager,
|
||||
);
|
||||
|
||||
logger.info({ elapsed: elapsed() }, "Bootstrap complete, ready to start listening");
|
||||
|
||||
const start = async () => {
|
||||
@@ -642,6 +546,9 @@ export async function createPaseoDaemon(
|
||||
httpServer.off("error", onError);
|
||||
const logAndResolve = async () => {
|
||||
boundListenTarget = resolveBoundListenTarget(listenTarget, httpServer);
|
||||
const mcpBaseUrl = mcpEnabled ? createAgentMcpBaseUrl(boundListenTarget) : null;
|
||||
agentMcpBaseUrl = config.mcpInjectIntoAgents === false ? null : mcpBaseUrl;
|
||||
agentManager.setMcpBaseUrl(agentMcpBaseUrl);
|
||||
const relayEnabled = config.relayEnabled ?? true;
|
||||
const relayEndpoint = config.relayEndpoint ?? "relay.paseo.sh:443";
|
||||
const relayPublicEndpoint = config.relayPublicEndpoint ?? relayEndpoint;
|
||||
@@ -660,9 +567,41 @@ export async function createPaseoDaemon(
|
||||
logger.info(
|
||||
{ path: boundListenTarget.path, elapsed: elapsed() },
|
||||
`Server listening on ${boundListenTarget.path}`,
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
wsServer = new VoiceAssistantWebSocketServer(
|
||||
httpServer,
|
||||
logger,
|
||||
serverId,
|
||||
agentManager,
|
||||
agentStorage,
|
||||
downloadTokenStore,
|
||||
config.paseoHome,
|
||||
mcpBaseUrl,
|
||||
{ allowedOrigins, allowedHosts: config.allowedHosts },
|
||||
speechService,
|
||||
terminalManager,
|
||||
{
|
||||
finalTimeoutMs: config.dictationFinalTimeoutMs,
|
||||
},
|
||||
config.agentProviderSettings,
|
||||
daemonVersion,
|
||||
(intent) => {
|
||||
try {
|
||||
config.onLifecycleIntent?.(intent);
|
||||
} catch (error) {
|
||||
logger.error({ err: error, intent }, "Failed to handle daemon lifecycle intent");
|
||||
}
|
||||
},
|
||||
projectRegistry,
|
||||
workspaceRegistry,
|
||||
chatService,
|
||||
loopService,
|
||||
scheduleService,
|
||||
checkoutDiffManager,
|
||||
);
|
||||
|
||||
if (typeof process.send === "function" && process.env.PASEO_SUPERVISED === "1") {
|
||||
process.send({
|
||||
type: "paseo:ready",
|
||||
@@ -681,7 +620,7 @@ export async function createPaseoDaemon(
|
||||
});
|
||||
|
||||
const url = encodeOfferToFragmentUrl({ offer, appBaseUrl });
|
||||
logger.info({ url }, "pairing_offer");
|
||||
logger.info("pairing_offer");
|
||||
|
||||
relayTransport?.stop().catch(() => undefined);
|
||||
relayTransport = startRelayTransport({
|
||||
@@ -734,9 +673,6 @@ export async function createPaseoDaemon(
|
||||
if (wsServer) {
|
||||
await wsServer.close();
|
||||
}
|
||||
if (voiceMcpBridgeManager) {
|
||||
await voiceMcpBridgeManager.stop().catch(() => undefined);
|
||||
}
|
||||
await new Promise<void>((resolve) => {
|
||||
httpServer.close(() => resolve());
|
||||
});
|
||||
|
||||
@@ -149,7 +149,7 @@ export function startRelayTransport({
|
||||
clearTimeout(controlReadyTimeout);
|
||||
controlReadyTimeout = null;
|
||||
}
|
||||
relayLogger.info({ url, connectionId }, "relay_control_connected");
|
||||
relayLogger.info({ connectionId }, "relay_control_connected");
|
||||
};
|
||||
|
||||
socket.on("open", () => {
|
||||
@@ -203,7 +203,7 @@ export function startRelayTransport({
|
||||
try {
|
||||
socket.send(JSON.stringify({ type: "ping", ts: now }));
|
||||
} catch (error) {
|
||||
relayLogger.warn({ err: error, url, connectionId }, "relay_control_ping_send_failed");
|
||||
relayLogger.warn({ err: error, connectionId }, "relay_control_ping_send_failed");
|
||||
try {
|
||||
socket.terminate();
|
||||
} catch {
|
||||
@@ -214,14 +214,14 @@ export function startRelayTransport({
|
||||
try {
|
||||
socket.send(JSON.stringify({ type: "ping", ts: Date.now() }));
|
||||
} catch (error) {
|
||||
relayLogger.warn({ err: error, url, connectionId }, "relay_control_ping_send_failed");
|
||||
relayLogger.warn({ err: error, connectionId }, "relay_control_ping_send_failed");
|
||||
try {
|
||||
socket.terminate();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
relayLogger.debug({ url, connectionId }, "relay_control_open_waiting_for_ready");
|
||||
relayLogger.debug({ connectionId }, "relay_control_open_waiting_for_ready");
|
||||
});
|
||||
|
||||
socket.on("close", (code, reason) => {
|
||||
@@ -244,7 +244,7 @@ export function startRelayTransport({
|
||||
|
||||
socket.on("error", (err) => {
|
||||
if (controlWs !== socket) return;
|
||||
relayLogger.warn({ err, url, connectionId }, "relay_error");
|
||||
relayLogger.warn({ err, connectionId }, "relay_error");
|
||||
// close event will schedule reconnect
|
||||
});
|
||||
|
||||
@@ -319,7 +319,7 @@ export function startRelayTransport({
|
||||
const openTimeout = setTimeout(() => {
|
||||
if (stopped) return;
|
||||
if (socket.readyState === WebSocket.OPEN) return;
|
||||
relayLogger.warn({ url, connectionId }, "relay_data_open_timeout_terminating");
|
||||
relayLogger.warn({ connectionId }, "relay_data_open_timeout_terminating");
|
||||
try {
|
||||
socket.terminate();
|
||||
} catch {
|
||||
@@ -329,7 +329,7 @@ export function startRelayTransport({
|
||||
|
||||
socket.on("open", () => {
|
||||
clearTimeout(openTimeout);
|
||||
relayLogger.info({ url, connectionId }, "relay_data_connected");
|
||||
relayLogger.info({ connectionId }, "relay_data_connected");
|
||||
if (attached) return;
|
||||
attached = true;
|
||||
const externalMetadata: ExternalSocketMetadata = {
|
||||
@@ -361,7 +361,7 @@ export function startRelayTransport({
|
||||
});
|
||||
|
||||
socket.on("error", (err) => {
|
||||
relayLogger.warn({ err, url, connectionId }, "relay_data_error");
|
||||
relayLogger.warn({ err, connectionId }, "relay_data_error");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user