affaan-m/ECC · error · Error

Memory roots must include a trusted boundary policy.

Error message

Memory roots must include a trusted boundary policy.

What it means

Thrown by assertMemoryRootSafe() as its opening guard: the roots argument must be a non-null, non-array object. roots is expected to be the frozen object produced by resolveVaultRoots() — a plain object with 'project', 'team', and 'user' string keys plus a hidden Symbol-keyed boundary map. Passing anything else is a programming error in the caller.

Source

Thrown at scripts/lib/memory-vault.js:99

        ? realpathNearestExisting(projectVault)
        : projectRoot,
      team: env.ECC_MEMORY_PROJECT_ROOT
        ? realpathNearestExisting(projectVault)
        : projectRoot,
      user: env.ECC_MEMORY_USER_ROOT
        ? realpathNearestExisting(userVault)
        : homeDir,
    }),
    enumerable: false,
    configurable: false,
    writable: false,
  });
  return Object.freeze(roots);
}

function assertMemoryRootSafe(roots, scope) {
  if (!roots || typeof roots !== 'object' || Array.isArray(roots)) {
    throw new Error('Memory roots must include a trusted boundary policy.');
  }
  const root = roots[scope];
  if (typeof root !== 'string' || root.length === 0) {
    throw new Error(`No memory root is configured for scope "${scope}".`);
  }
  const boundary = roots[VAULT_ROOT_BOUNDARIES]?.[scope];
  if (typeof boundary !== 'string' || boundary.length === 0) {
    throw new Error(`No trusted boundary policy is configured for memory scope "${scope}".`);
  }
  assertWithinTrustedRoot(root, boundary, 'access memory through a symlink');
  if (fs.existsSync(root) && fs.lstatSync(root).isSymbolicLink()) {
    throw new Error(`Refusing to access memory through symlink root: ${root}`);
  }
  return root;
}

function assertMemoryDirectorySafe(directory, root) {
  assertWithinTrustedRoot(directory, root, 'access memory directory');

View on GitHub (pinned to 01e15490f0)

Solutions

  1. Always obtain roots via resolveVaultRoots(options) — never build the object by hand.
  2. If you must override, call resolveVaultRoots({ ECC_MEMORY_PROJECT_ROOT: '/path' }) with env-style options.
  3. Pass options through to saveMemory()/searchMemories()/readMemoryFiles() and let them resolve roots themselves.
  4. Add an assertion in your wrapper: if (!options.roots) options.roots = resolveVaultRoots(options);

Example fix

// before
saveMemory(input, { roots: '/custom/path' }); // string, not a roots object

// after
const roots = resolveVaultRoots({ ECC_MEMORY_PROJECT_ROOT: '/custom/path' });
saveMemory(input, { roots });
Defensive patterns

Strategy: type-guard

Validate before calling

if (!options.roots) options.roots = resolveVaultRoots(options);
if (!options.roots || typeof options.roots !== 'object' || Array.isArray(options.roots)) {
  throw new TypeError('options.roots must be a resolveVaultRoots() result');
}

Type guard

function isRootsObject(value): value is Record<'project'|'team'|'user', string> {
  return value !== null && typeof value === 'object' && !Array.isArray(value)
    && typeof value.project === 'string'
    && typeof value.team === 'string'
    && typeof value.user === 'string';
}

Prevention

When it happens

Trigger: Calling assertMemoryRootSafe(null, scope), assertMemoryRootSafe(undefined, scope) when options.roots was not set, assertMemoryRootSafe([], scope) when a list of roots was passed by mistake, or a wrapper that built its own roots object without using resolveVaultRoots() and passed a string path instead.

Common situations: Custom wrapper that wanted to override roots but passed a single string. Refactor that changed the shape passed to initializeVault/readMemoryFiles/saveMemory. Test stub that constructed roots manually and forgot the Symbol boundary property.

Related errors


AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13). Data as JSON: /api/errors/81b3fd6720e47b25. Report an issue: GitHub.