JuliusBrussee/caveman · error
cave_memory_tenant_invalid
cave_memory_tenant_invalid
Error message
cave_memory_tenant_invalid
What it means
`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.
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.
Example fix
// before
createMemoryStore({ tenant: "acme.org" }); // dot rejected
// after
const slug = (t) => t.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/^[-_]+/, "").slice(0, 128) || "_";
createMemoryStore({ tenant: slug("acme.org") }); // "acme-org" Defensive patterns
Strategy: validation
Validate before calling
const TENANT_RE = /^[a-z0-9][a-z0-9_-]{0,127}$/i;
function safeTenant(raw) {
if (raw === undefined || raw === "_") return "_";
const t = String(raw).replace(/[^a-zA-Z0-9_-]/g, "-").replace(/^[-_]+/, "").slice(0, 128);
if (!t || !TENANT_RE.test(t)) throw new TypeError(`tenant invalid after slugify: ${raw}`);
return t;
}
createMemoryStore({ tenant: safeTenant(cfg.tenant) }); Type guard
const isValidTenant = (t) => t === "_" || (typeof t === "string" && /^[a-z0-9][a-z0-9_-]{0,127}$/i.test(t)); Try / catch
try {
const store = createMemoryStore({ tenant: cfg.tenant });
} catch (e) {
if (e.message === "cave_memory_tenant_invalid") {
throw new Error(`Tenant '${cfg.tenant}' not allowed; use [a-z0-9_-] only, <=128 chars, no dots/slashes`, { cause: e });
}
throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.
Related errors
- cave_memory_agent_invalid
- cave_memory_namespace_invalid
- apiKey, baseURL, and agent are required
- ASGI context resolver or request bounds are invalid
- bedrock endpoint must be runtime or mantle
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/92b1bac51d7eee72.
Report an issue: GitHub.
Appendix: source
Thrown at packages/agent/src/memory-store.ts:59
const NAMESPACE_PATTERN = /^[a-z0-9][a-z0-9_-]{0,95}$/;
function defaultRoot(): string {
return process.env.CAVE_AGENT_MEMORY_ROOT ?? join(homedir(), ".caveman", "agent-memory");
}
/**
* The durable file for (tenant, agentId, namespace). The three scoping
* components are validated to a `[a-z0-9_-]`-class charset with no `.` or path
* separator, so no component can traverse out of the memory root.
*/
export function memoryFilePath(
config: MemoryStoreConfig | undefined,
agentId: string,
namespace: string,
): string {
const tenant = config?.tenant ?? "_";
if (tenant !== "_" && !TENANT_PATTERN.test(tenant)) {
throw new Error("cave_memory_tenant_invalid");
}
if (!AGENT_PATTERN.test(agentId)) throw new Error("cave_memory_agent_invalid");
if (!NAMESPACE_PATTERN.test(namespace)) throw new Error("cave_memory_namespace_invalid");
return join(config?.root ?? defaultRoot(), tenant, agentId, `${namespace}.json`);
}
function isMemoryEntry(value: unknown): value is MemoryEntry {
return value !== null && typeof value === "object" &&
typeof (value as { text?: unknown }).text === "string" &&
Number.isSafeInteger((value as { createdAt?: unknown }).createdAt);
}
/** Read the durable entries. A missing or corrupt file is an empty store, never a throw into a run. */
export async function readMemories(filePath: string): Promise<MemoryEntry[]> {
try {
const parsed: unknown = JSON.parse(await readFile(filePath, "utf8"));
return Array.isArray(parsed) ? parsed.filter(isMemoryEntry) : [];
} catch {View on GitHub (pinned to 3ee70a1026)