{"record":{"id":"92b1bac51d7eee72","repo":"JuliusBrussee/caveman","slug":"cave-memory-tenant-invalid","errorCode":"cave_memory_tenant_invalid","errorMessage":"cave_memory_tenant_invalid","messagePattern":"cave_memory_tenant_invalid","errorType":"error_code","errorClass":null,"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/3ee70a102609e550bd2e68004bf5990a9341c851/packages/agent/src/memory-store.ts#L41-L77","documentation":"`memoryFilePath` builds the on-disk path `<root>/<tenant>/<agentId>/<namespace>.json` for the memory store and validates each path segment against strict patterns to prevent traversal. The `tenant` from `MemoryStoreConfig` defaults to \"_\" but, when set to anything else, must match `/^[a-z0-9][a-z0-9_-]{0,127}$/i`. Invalid tenants (empty after non-underscore, containing `.`/`/`/`\\`, starting with `-`/`_`, or longer than 128 chars) throw this error.","triggerScenarios":"Calling `createMemoryStore({ tenant: \"acme.org\" })` (dot not allowed), `tenant: \"../escape\"`, `tenant: \"\"` combined with a truthy check path, `tenant: \"-team\"` (must start alphanumeric), or a tenant longer than 128 characters.","commonSituations":"Using an email address, URL, or org domain as the tenant; injecting a path segment containing slashes or dots to organize tenants hierarchically; passing a full tenant key from another system without slugifying it first.","solutions":["Normalize the tenant to the allowed charset: alphanumeric, underscore, hyphen, max 128 chars, starting with a letter or digit — e.g. `tenant: \"acme-org\"` for \"acme.org\".","Slugify before passing: `tenant.replace(/[^a-zA-Z0-9_-]/g, \"-\").slice(0, 128).replace(/^[-_]+/, \"\") || \"_\"`.","Omit `tenant` (or set \"_\") if you do not need multi-tenant isolation."],"exampleFix":"// before\ncreateMemoryStore({ tenant: \"acme.org\" }); // dot rejected\n// after\nconst slug = (t) => t.replace(/[^a-zA-Z0-9_-]/g, \"-\").replace(/^[-_]+/, \"\").slice(0, 128) || \"_\";\ncreateMemoryStore({ tenant: slug(\"acme.org\") }); // \"acme-org\"","handlingStrategy":"validation","validationCode":"const TENANT_RE = /^[a-z0-9][a-z0-9_-]{0,127}$/i;\nfunction safeTenant(raw) {\n  if (raw === undefined || raw === \"_\") return \"_\";\n  const t = String(raw).replace(/[^a-zA-Z0-9_-]/g, \"-\").replace(/^[-_]+/, \"\").slice(0, 128);\n  if (!t || !TENANT_RE.test(t)) throw new TypeError(`tenant invalid after slugify: ${raw}`);\n  return t;\n}\ncreateMemoryStore({ tenant: safeTenant(cfg.tenant) });","typeGuard":"const isValidTenant = (t) => t === \"_\" || (typeof t === \"string\" && /^[a-z0-9][a-z0-9_-]{0,127}$/i.test(t));","tryCatchPattern":"try {\n  const store = createMemoryStore({ tenant: cfg.tenant });\n} catch (e) {\n  if (e.message === \"cave_memory_tenant_invalid\") {\n    throw new Error(`Tenant '${cfg.tenant}' not allowed; use [a-z0-9_-] only, <=128 chars, no dots/slashes`, { cause: e });\n  }\n  throw e;\n}","preventionTips":["Slugify external tenant identifiers (emails, domains, org names) before passing them","Never build tenant values from user-controlled path fragments — the pattern also blocks traversal","Keep tenants simple kebab-case slugs matching how they appear on disk","Add a startup assertion that all configured tenants pass the pattern"],"tags":["configuration","validation","identifier-format","path-safety"],"backgroundTag":"invalid-identifier-format","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}