diff --git a/apps/gateway/src/agent/agent.service.ts b/apps/gateway/src/agent/agent.service.ts index 3a31a8e..1062b84 100644 --- a/apps/gateway/src/agent/agent.service.ts +++ b/apps/gateway/src/agent/agent.service.ts @@ -106,17 +106,22 @@ export class AgentService implements OnModuleDestroy { ) {} /** - * Build the full set of custom tools scoped to the given sandbox directory. + * Build the full set of custom tools scoped to the given sandbox directory and session user. * Brain/coord/memory/web tools are stateless with respect to cwd; file/git/shell * tools receive the resolved sandboxDir so they operate within the sandbox. + * Memory tools are bound to sessionUserId so the LLM cannot access another user's data. */ - private buildToolsForSandbox(sandboxDir: string): ToolDefinition[] { + private buildToolsForSandbox( + sandboxDir: string, + sessionUserId: string | undefined, + ): ToolDefinition[] { return [ ...createBrainTools(this.brain), ...createCoordTools(this.coordService), ...createMemoryTools( this.memory, this.embeddingService.available ? this.embeddingService : null, + sessionUserId, ), ...createFileTools(sandboxDir), ...createGitTools(sandboxDir), @@ -216,8 +221,8 @@ export class AgentService implements OnModuleDestroy { ); } - // Build per-session tools scoped to the sandbox directory - const sandboxTools = this.buildToolsForSandbox(sandboxDir); + // Build per-session tools scoped to the sandbox directory and authenticated user + const sandboxTools = this.buildToolsForSandbox(sandboxDir, mergedOptions?.userId); // Combine static tools with dynamically discovered MCP client tools and skill tools const mcpTools = this.mcpClientService.getToolDefinitions(); diff --git a/apps/gateway/src/agent/tools/memory-tools.ts b/apps/gateway/src/agent/tools/memory-tools.ts index c8a3b43..c420c89 100644 --- a/apps/gateway/src/agent/tools/memory-tools.ts +++ b/apps/gateway/src/agent/tools/memory-tools.ts @@ -3,23 +3,45 @@ import type { ToolDefinition } from '@mariozechner/pi-coding-agent'; import type { Memory } from '@mosaic/memory'; import type { EmbeddingProvider } from '@mosaic/memory'; +/** + * Create memory tools bound to the session's authenticated userId. + * + * SECURITY: userId is resolved from the authenticated session at tool-creation + * time and is never accepted as a user-supplied or LLM-supplied parameter. + * This prevents cross-user data access via parameter injection. + */ export function createMemoryTools( memory: Memory, embeddingProvider: EmbeddingProvider | null, + /** Authenticated user ID from the session. All memory operations are scoped to this user. */ + sessionUserId: string | undefined, ): ToolDefinition[] { + /** Return an error result when no session user is bound. */ + function noUserError() { + return { + content: [ + { + type: 'text' as const, + text: 'Memory tools unavailable — no authenticated user bound to this session', + }, + ], + details: undefined, + }; + } + const searchMemory: ToolDefinition = { name: 'memory_search', label: 'Search Memory', description: 'Search across stored insights and knowledge using natural language. Returns semantically similar results.', parameters: Type.Object({ - userId: Type.String({ description: 'User ID to search memory for' }), query: Type.String({ description: 'Natural language search query' }), limit: Type.Optional(Type.Number({ description: 'Max results (default 5)' })), }), async execute(_toolCallId, params) { - const { userId, query, limit } = params as { - userId: string; + if (!sessionUserId) return noUserError(); + + const { query, limit } = params as { query: string; limit?: number; }; @@ -37,7 +59,7 @@ export function createMemoryTools( } const embedding = await embeddingProvider.embed(query); - const results = await memory.insights.searchByEmbedding(userId, embedding, limit ?? 5); + const results = await memory.insights.searchByEmbedding(sessionUserId, embedding, limit ?? 5); return { content: [{ type: 'text' as const, text: JSON.stringify(results, null, 2) }], details: undefined, @@ -48,9 +70,8 @@ export function createMemoryTools( const getPreferences: ToolDefinition = { name: 'memory_get_preferences', label: 'Get User Preferences', - description: 'Retrieve stored preferences for a user.', + description: 'Retrieve stored preferences for the current session user.', parameters: Type.Object({ - userId: Type.String({ description: 'User ID' }), category: Type.Optional( Type.String({ description: 'Filter by category: communication, coding, workflow, appearance, general', @@ -58,11 +79,13 @@ export function createMemoryTools( ), }), async execute(_toolCallId, params) { - const { userId, category } = params as { userId: string; category?: string }; + if (!sessionUserId) return noUserError(); + + const { category } = params as { category?: string }; type Cat = 'communication' | 'coding' | 'workflow' | 'appearance' | 'general'; const prefs = category - ? await memory.preferences.findByUserAndCategory(userId, category as Cat) - : await memory.preferences.findByUser(userId); + ? await memory.preferences.findByUserAndCategory(sessionUserId, category as Cat) + : await memory.preferences.findByUser(sessionUserId); return { content: [{ type: 'text' as const, text: JSON.stringify(prefs, null, 2) }], details: undefined, @@ -76,7 +99,6 @@ export function createMemoryTools( description: 'Store a learned user preference (e.g., "prefers tables over paragraphs", "timezone: America/Chicago").', parameters: Type.Object({ - userId: Type.String({ description: 'User ID' }), key: Type.String({ description: 'Preference key' }), value: Type.String({ description: 'Preference value (JSON string)' }), category: Type.Optional( @@ -86,8 +108,9 @@ export function createMemoryTools( ), }), async execute(_toolCallId, params) { - const { userId, key, value, category } = params as { - userId: string; + if (!sessionUserId) return noUserError(); + + const { key, value, category } = params as { key: string; value: string; category?: string; @@ -100,7 +123,7 @@ export function createMemoryTools( parsedValue = value; } const pref = await memory.preferences.upsert({ - userId, + userId: sessionUserId, key, value: parsedValue, category: (category as Cat) ?? 'general', @@ -119,7 +142,6 @@ export function createMemoryTools( description: 'Store a learned insight, decision, or knowledge extracted from the current interaction.', parameters: Type.Object({ - userId: Type.String({ description: 'User ID' }), content: Type.String({ description: 'The insight or knowledge to store' }), category: Type.Optional( Type.String({ @@ -128,8 +150,9 @@ export function createMemoryTools( ), }), async execute(_toolCallId, params) { - const { userId, content, category } = params as { - userId: string; + if (!sessionUserId) return noUserError(); + + const { content, category } = params as { content: string; category?: string; }; @@ -141,7 +164,7 @@ export function createMemoryTools( } const insight = await memory.insights.create({ - userId, + userId: sessionUserId, content, embedding, source: 'agent',