🔒 fix: Address Codex re-review on memory capability (round 5)

- 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).
This commit is contained in:
Danny Avila 2026-06-20 23:36:14 -04:00
parent e52ca9d3d2
commit 74e6744150
2 changed files with 35 additions and 6 deletions

View file

@ -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<unknown> }} [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({

View file

@ -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,
}));