{"record":{"id":"92b1bac51d7eee72","repo":"JuliusBrussee/caveman","slug":"cave-memory-tenant-invalid","errorCode":null,"errorMessage":"cave_memory_tenant_invalid","messagePattern":"cave_memory_tenant_invalid","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/agent/src/memory-store.ts","lineNumber":59,"sourceCode":"const NAMESPACE_PATTERN = /^[a-z0-9][a-z0-9_-]{0,95}$/;\n\nfunction 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 {","sourceCodeStart":41,"sourceCodeEnd":77,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/memory-store.ts#L41-L77","documentation":"memoryFilePath() validates the tenant component of the durable memory path against a strict [a-z0-9_-]-class pattern (no dots or path separators) so no tenant value can traverse out of the memory root. The sentinel \"_\" is reserved for the default tenant. A tenant containing uppercase, slashes, \"..\", \".json\", or other characters throws cave_memory_tenant_invalid.","triggerScenarios":"Setting config.tenant to values like \"Acme Corp\", \"tenant/../etc\", \"my.tenant\", or \"\" (empty string is not \"_\"). Only \"_\" and pattern-conforming strings pass; omitting tenant config entirely defaults to \"_\" and never throws.","commonSituations":"Using a raw hostname, email, or user display name as tenant id; forgetting to slugify tenant ids at system boundaries; or passing null/undefined wrapped as the string \"undefined\".","solutions":["Slugify the tenant before configuring the store: lowercase, replace everything outside [a-z0-9_-] with \"-\"","Use the reserved \"_\" only for the shared/default tenant, never as a real tenant id","Omit tenant in MemoryStoreConfig when you want the default","Add a unit test asserting your tenant ids match /^[a-z0-9_-]+$/ at the API boundary"],"exampleFix":"// before\nconst store = memoryStore({ tenant: account.organizationName }); // \"Acme Corp!\"\n\n// after\nconst slug = account.organizationName.toLowerCase().replace(/[^a-z0-9_-]+/g, \"-\").replace(/^-+|-+$/g, \"\") || \"_\";\nconst store = memoryStore({ tenant: slug });","handlingStrategy":"validation","validationCode":"const TENANT_RE = /^[a-z0-9_-]+$/;\nfunction tenantSlug(raw: string): string {\n  const slug = raw.toLowerCase().replace(/[^a-z0-9_-]+/g, \"-\").replace(/^-+|-+$/g, \"\");\n  if (slug === \"\" || !TENANT_RE.test(slug)) throw new Error(`cannot derive safe tenant id from ${JSON.stringify(raw)}`);\n  return slug;\n}","typeGuard":"const isSafeTenant = (t: string): boolean => t === \"_\" || /^[a-z0-9_-]+$/.test(t);","tryCatchPattern":null,"preventionTips":["Slugify tenant ids at the API boundary, never store raw org/user names","Reserve \"_\" for the default tenant in your data model","Assert the pattern in an integration test that covers every tenant source"],"tags":["memory","validation","path-traversal","security","multi-tenant"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}