TencentCloud/TencentDB-Agent-Memory · error

Invalid scoped storage key: ${JSON.stringify(key)}

Error message

Invalid scoped storage key: ${JSON.stringify(key)}

What it means

ScopedStorageAdapter.key() rejects keys that are not plain strings, contain a NUL byte, or start with '/' or '\'. The adapter namespaces all keys under a prefix, so raw absolute-looking or malformed keys would break the scoping contract; it throws rather than silently normalizing them.

Source

Thrown at MemoryCore/src/core/storage/adapter.ts:28

 * Eventually, callers may inline IStorageBackend calls directly and
 * this adapter can be removed.
 */

import type { IStorageBackend, StorageObject, ListEntry, ListObjectsOptions, ListResult, PutObjectOptions } from "./types.js";

class ScopedStorageBackend implements IStorageBackend {
  readonly type: "local" | "cos";
  private readonly prefix: string;

  constructor(private readonly base: IStorageBackend, prefix: string) {
    this.type = base.type;
    const normalized = prefix.replace(/^\/+/, "").replace(/\/+/g, "/");
    this.prefix = normalized && !normalized.endsWith("/") ? `${normalized}/` : normalized;
  }

  private key(key: string): string {
    if (typeof key !== "string" || key.includes("\0") || key.startsWith("/") || key.startsWith("\\")) {
      throw new Error(`Invalid scoped storage key: ${JSON.stringify(key)}`);
    }
    const normalized = key.replace(/^\/+/, "").replace(/\\+/g, "/").replace(/\/+/g, "/");
    if (normalized.split("/").some((part) => part === "..")) {
      throw new Error(`Path traversal rejected in scoped storage key: ${key}`);
    }
    return `${this.prefix}${normalized}`;
  }

  private unkey(key: string): string {
    return key.startsWith(this.prefix) ? key.slice(this.prefix.length) : key;
  }

  async putObject(key: string, content: string | Buffer, opts?: PutObjectOptions): Promise<void> {
    return this.base.putObject(this.key(key), content, opts);
  }

  async appendObject(key: string, content: string | Buffer): Promise<void> {
    return this.base.appendObject(this.key(key), content);

View on GitHub (pinned to 3efcd317b8)

Solutions

  1. Pass a relative, prefix-free key such as 'persona/main.md' instead of '/persona/main.md'
  2. Convert Windows backslash separators to forward slashes before calling the adapter
  3. Sanitize or strip NUL/control characters from keys derived from user input
  4. Ensure the key argument is always a non-null string (validate at the caller boundary)

Example fix

// before
await adapter.putObject('/users/' + name, buf);
// after
const rel = String(name).replace(/^[\\/]+/, '');
await adapter.putObject(`users/${rel}`, buf);
Defensive patterns

Strategy: validation

Validate before calling

function assertSafeKey(key) {
  if (typeof key !== 'string' || key.length === 0) throw new TypeError('key must be a non-empty string');
  if (key.includes('\0')) throw new Error('key contains NUL byte');
  if (key.startsWith('/') || key.startsWith('\\')) throw new Error(`key must be relative: ${key}`);
}

Type guard

const isValidKey = (k) => typeof k === 'string' && k.length > 0 && !k.includes('\0') && !k.startsWith('/') && !k.startsWith('\\');

Try / catch

try {
  await adapter.putObject(key, buf);
} catch (e) {
  if (String(e.message).startsWith('Invalid scoped storage key')) {
    throw new BadRequestError(`invalid storage key: ${key}`);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling putObject, appendObject, obj, exists, result, or deleteObject with a key that is not a string, contains '\0', or starts with '/' or '\'.

Common situations: Concatenating a path-like base ('/data/') with the key; Windows-style '\dir\file' paths; null/undefined keys from broken callers; keys containing binary junk from decoding errors.

Related errors


AI-assisted analysis of TencentCloud/TencentDB-Agent-Memory@3efcd317b8 (2026-09-01). Data as JSON: /api/errors/2c87dd356a1e49af. Report an issue: GitHub.