From 74e674415038a7e5d73e72126d00a356078cc4eb Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 20 Jun 2026 23:36:14 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=20fix:=20Address=20Codex=20re-revi?= =?UTF-8?q?ew=20on=20memory=20capability=20(round=205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Gate inline memory registration (memoryAvailable) on the memory WRITE permissions (USE+CREATE+UPDATE), so a read-only-memory role no longer has set_memory/delete_memory shown to the model only for the runtime loader to refuse them (api/server/services/Endpoints/agents/initialize.js). - Enforce the per-agent memory opt-in at execution: handleTools now refuses to construct set_memory/delete_memory unless the agent actually declared them (toolDefinitions/tools), blocking hallucinated/undeclared memory tool calls from mutating memory. - Fail closed when getFormattedMemories errors with a configured tokenLimit, instead of writing as if storage were empty and bypassing the cap (api/app/clients/tools/util/handleTools.js). --- api/app/clients/tools/util/handleTools.js | 30 +++++++++++++++++-- .../services/Endpoints/agents/initialize.js | 11 ++++--- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/api/app/clients/tools/util/handleTools.js b/api/app/clients/tools/util/handleTools.js index fd90f3f505..f9f0d92f18 100644 --- a/api/app/clients/tools/util/handleTools.js +++ b/api/app/clients/tools/util/handleTools.js @@ -85,6 +85,23 @@ async function isMemoryToolUsable(req, writePermissions = []) { } } +/** + * Whether the agent actually declared the requested inline memory tool. The + * event-driven executor loads tools by requested name, so a hallucinated or + * undeclared `set_memory`/`delete_memory` call (e.g. on an agent that never + * opted into the memory capability) must not be constructed. + * @param {{ toolDefinitions?: Array<{ name?: string }>, tools?: Array }} [agent] + * @param {string} toolName + * @returns {boolean} + */ +function agentDeclaredMemoryTool(agent, toolName) { + if (!agent) { + return false; + } + const declared = (entry) => (typeof entry === 'string' ? entry : entry?.name) === toolName; + return (agent.toolDefinitions ?? []).some(declared) || (agent.tools ?? []).some(declared); +} + /** * Validates the availability and authentication of tools for a user based on environment variables or user-specific plugin authentication values. * Tools without required authentication or with valid authentication are considered valid. @@ -397,7 +414,10 @@ const loadTools = async ({ const validKeys = memoryConfig?.validKeys; const tokenLimit = memoryConfig?.tokenLimit; requestedTools[tool] = async () => { - if (!(await isMemoryToolUsable(options.req, [Permissions.CREATE, Permissions.UPDATE]))) { + if ( + !agentDeclaredMemoryTool(agent, SET_MEMORY_TOOL_NAME) || + !(await isMemoryToolUsable(options.req, [Permissions.CREATE, Permissions.UPDATE])) + ) { return null; } let totalTokens = 0; @@ -407,6 +427,9 @@ const loadTools = async ({ totalTokens = formatted?.totalTokens ?? 0; } catch (error) { logger.error('[handleTools] Failed to load memory token count for set_memory', error); + /** Fail closed: without the current usage total a configured + * tokenLimit could be silently bypassed. */ + return null; } } return createMemoryTool({ userId: user, setMemory, validKeys, tokenLimit, totalTokens }); @@ -415,7 +438,10 @@ const loadTools = async ({ } else if (tool === DELETE_MEMORY_TOOL_NAME) { const memoryConfig = options.req?.config?.memory; requestedTools[tool] = async () => { - if (!(await isMemoryToolUsable(options.req, [Permissions.UPDATE]))) { + if ( + !agentDeclaredMemoryTool(agent, DELETE_MEMORY_TOOL_NAME) || + !(await isMemoryToolUsable(options.req, [Permissions.UPDATE])) + ) { return null; } return createDeleteMemoryTool({ diff --git a/api/server/services/Endpoints/agents/initialize.js b/api/server/services/Endpoints/agents/initialize.js index 7bda67c545..530901e691 100644 --- a/api/server/services/Endpoints/agents/initialize.js +++ b/api/server/services/Endpoints/agents/initialize.js @@ -153,9 +153,12 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { const skillDbMethods = getSkillDbMethods(); /** Run-level gate for inline memory tools: the `memory` capability must be - * enabled, memory must be configured, and the user must not have opted out - * or lost the `MEMORIES.USE` permission. Agents (or the ephemeral memory - * badge) opt in per-agent via the `memory` marker on `tools`. */ + * enabled, memory must be configured, and the user must not have opted out. + * Requires the memory WRITE permissions (CREATE + UPDATE) — both inline tools + * mutate memory — so the tools aren't registered (and shown to the model) for + * read-only-memory roles that the runtime loader would then refuse to build. + * Agents (or the ephemeral memory badge) opt in per-agent via the `memory` + * marker on `tools`. */ const memoryAvailable = enabledCapabilities.has(AgentCapabilities.memory) && isMemoryEnabled(appConfig?.memory) && @@ -163,7 +166,7 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => { (await checkAccess({ user: req.user, permissionType: PermissionTypes.MEMORIES, - permissions: [Permissions.USE], + permissions: [Permissions.USE, Permissions.CREATE, Permissions.UPDATE], getRoleByName: db.getRoleByName, }));