{"record":{"id":"d487802912464e57","repo":"JuliusBrussee/caveman","slug":"cave-memory-agent-invalid","errorCode":null,"errorMessage":"cave_memory_agent_invalid","messagePattern":"cave_memory_agent_invalid","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/agent/src/memory-store.ts","lineNumber":61,"sourceCode":"function defaultRoot(): string {\n  return process.env.CAVE_AGENT_MEMORY_ROOT ?? join(homedir(), \".caveman\", \"agent-memory\");\n}\n\n/**\n * The durable file for (tenant, agentId, namespace). The three scoping\n * components are validated to a `[a-z0-9_-]`-class charset with no `.` or path\n * separator, so no component can traverse out of the memory root.\n */\nexport function memoryFilePath(\n  config: MemoryStoreConfig | undefined,\n  agentId: string,\n  namespace: string,\n): string {\n  const tenant = config?.tenant ?? \"_\";\n  if (tenant !== \"_\" && !TENANT_PATTERN.test(tenant)) {\n    throw new Error(\"cave_memory_tenant_invalid\");\n  }\n  if (!AGENT_PATTERN.test(agentId)) throw new Error(\"cave_memory_agent_invalid\");\n  if (!NAMESPACE_PATTERN.test(namespace)) throw new Error(\"cave_memory_namespace_invalid\");\n  return join(config?.root ?? defaultRoot(), tenant, agentId, `${namespace}.json`);\n}\n\nfunction isMemoryEntry(value: unknown): value is MemoryEntry {\n  return value !== null && typeof value === \"object\" &&\n    typeof (value as { text?: unknown }).text === \"string\" &&\n    Number.isSafeInteger((value as { createdAt?: unknown }).createdAt);\n}\n\n/** Read the durable entries. A missing or corrupt file is an empty store, never a throw into a run. */\nexport async function readMemories(filePath: string): Promise<MemoryEntry[]> {\n  try {\n    const parsed: unknown = JSON.parse(await readFile(filePath, \"utf8\"));\n    return Array.isArray(parsed) ? parsed.filter(isMemoryEntry) : [];\n  } catch {\n    return [];\n  }","sourceCodeStart":43,"sourceCodeEnd":79,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/memory-store.ts#L43-L79","documentation":"memoryFilePath() validates the agentId component of the durable memory path against a strict [a-z0-9_-]-class charset (no dots, no separators) to make path traversal from agentId impossible. An agentId with uppercase letters, slashes, \"..\", extensions, or spaces throws cave_memory_agent_invalid before any filesystem path is built.","triggerScenarios":"Calling memory read/write helpers with agentId values like \"Research Agent\", \"../shared\", \"agent.v2\", \"\", or \"AGENT-1\" (uppercase). The check applies to every memoryFilePath() call, i.e. every durable memory operation for that agent.","commonSituations":"Generating agent ids from free-text names or UUIDs with uppercase hex, reusing human-readable agent names as storage keys, or splitting agent ids on characters (\"team/agent\") that the pattern forbids.","solutions":["Generate agent ids from a slug or kebab-case convention at creation time","Normalize before storage: agentId.toLowerCase().replace(/[^a-z0-9_-]+/g, \"-\")","Lowercase UUIDs: crypto.randomUUID() is already lowercase-safe","Reject invalid ids at your own API boundary so the store never sees them"],"exampleFix":"// before\nawait store.append(agent.name, \"notes\", entry); // agent.name = \"Research Agent\"\n\n// after\nconst agentId = agent.name.toLowerCase().replace(/[^a-z0-9_-]+/g, \"-\");\nawait store.append(agentId, \"notes\", entry);","handlingStrategy":"validation","validationCode":"const AGENT_RE = /^[a-z0-9_-]+$/;\nfunction agentIdFrom(name: string): string {\n  const id = name.toLowerCase().replace(/[^a-z0-9_-]+/g, \"-\");\n  if (!AGENT_RE.test(id)) throw new Error(`cannot derive safe agent id from ${JSON.stringify(name)}`);\n  return id;\n}","typeGuard":"const isSafeAgentId = (id: string): boolean => /^[a-z0-9_-]+$/.test(id);","tryCatchPattern":null,"preventionTips":["Generate agent ids once at creation (slug or lowercase UUID) and store them as the canonical key","Never use display names or hierarchical paths (\"team/agent\") as memory keys","Add a repository-level lint or test asserting all agent ids match the charset"],"tags":["memory","validation","path-traversal","security","agent-ids"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}