Skip to content

v6.0.0 Linux: MutationTier/MemoryWriter allowlists compare path strings — LifeOS↔LIFEOS duality rejects every Tier-A memory write (ETIER_MISMATCH), symlink workaround insufficient #1410

Description

@badosanjos

Summary

Sibling of #1391 / #1278, one layer deeper. LIFEOS/TOOLS/MemoryTypes.ts builds storage paths with pathJoin(CLAUDE_ROOT, "LifeOS") (mixed case) while MutationTier.ts and MemoryWriter.ts build their allowlists with "LIFEOS/..." (all caps). The allowlists match on path strings, so on case-sensitive filesystems every Tier-A hot-layer write is rejected:

ETIER_MISMATCH: Type 'memory' declares tier A, but resolved path
~/.claude/LifeOS/USER/PRINCIPAL/PRINCIPAL_MEMORY.md classifies as tier D.

and after the tier layer, MemoryWriter.validatePath() fails the same way (EINVAL_PATH: Path not in allowlist).

Crucially, the #1391 symlink workaround does not help here: LifeOS → LIFEOS fixes file I/O, but path.resolve() does not resolve symlinks, so the string comparison still misses. On macOS (case-insensitive APFS) both bugs are invisible.

Net effect on Linux: the autonomic memory loop runs, the reviewer extracts items, and every memory write is refused — combined with the template bug (filed separately) the memory subsystem is fully inert on Linux installs.

Environment

Ubuntu 24.04, LifeOS v6.0.0, ~/.claude/LifeOS → ~/.claude/LIFEOS symlink in place per #1391.

Repro

bun LIFEOS/TOOLS/MemoryReviewer.ts test → E2E dispatch failure with ETIER_MISMATCH (above).

Fix suggestion (verified locally)

Canonicalize (symlink-resolve) both the allowlist constants and the classify/validate input. For not-yet-existing files, realpath the deepest existing ancestor and re-attach the tail:

function canonicalize(absolutePath: string): string {
  let head = absolutePath, tail = "";
  while (head !== "/" && !existsSync(head)) {
    tail = tail ? pathJoin(basename(head), tail) : basename(head);
    head = dirname(head);
  }
  try {
    const real = realpathSync(head);
    return tail ? pathJoin(real, tail) : real;
  } catch { return absolutePath; }
}

Applied in MutationTier.ts (allowlists + getTier) and MemoryWriter.ts (ALLOWED_FILES + validatePath). Side benefit: a symlink planted inside an allowlisted dir can no longer smuggle a write to its real target, which strengthens the classifier's default-deny posture. All five memory smoke suites pass after the patch (MutationTier 24/24 incl. three new symlink-spelling regression cases).

Longer term: one exported constant (or shared resolve helper, cf. DeployComponents.resolveLifeosDir mentioned in #1391) for the LifeOS dir would remove the dual spelling at the source. Happy to send a PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions