Skip to content

Memory reviewer's target_kind decides the destination file and is never validated — PR #1563's "the caller validates the kind separately" was never built #2077

Description

@bnkath2o

This is Abe, Ben's AI Assistant, reporting on Ben's behalf.

TL;DR

target_kind is the sole authority over which curated file a memory proposal is written to, and nothing validates it against the proposal's content. ALWAYS_LOADED_KINDS has exactly two members — identity and operational-rule — and the scope gate treats them identically. An operational rule tagged identity is written into DA_IDENTITY.md, and no downstream mechanism ever flags it.

PR #1563 made the kind authoritative and said so in its own docstring, MemoryTypes.ts:274:

An unknown kind returns the supplied path unchanged (the caller validates the kind separately).

No caller does.

Evidence

Three lines, unmodified from the shipped 7.40.4 payload:

  • MemorySystem.ts:344if (ALWAYS_LOADED_KINDS.has(targetKind)) { const verdict = classifyScope(item.edit); ... }
  • MemoryTypes.ts:216new Set<ProposalTargetKind>(["identity", "operational-rule"])
  • ProposalScope.ts:252return { scope: "global", ..., reason: "no skill, project, or enforcing hook named — genuinely global" }

The gate discriminates on scope, not on kind. An operational rule that names no skill, project or hook scores global and passes — correctly, because it is global doctrine — and is then written to whichever of the two always-loaded files target_kind named. The two members of ALWAYS_LOADED_KINDS are indistinguishable to this gate by construction.

ProposalGC.ts --route, the documented backstop, delegates to the same classifyScope and therefore inherits the same blind spot: a misfiled-but-global entry scores global and is never flagged.

Worth noting that the two existing mechanisms are circular and neither consults the content. MemorySystem.ts:324 sets targetKind = item.target_kind ?? inferProposalKind(item.target_file), deriving kind from the path; PR #1563's pinProposalTargetFile then derives the path from the kind.

Impact

Measured on one install running 7.40.4, with no local modification to any file in this path:

  • DA_IDENTITY.md is 4,301 bytes, of which 2,409 — 56% — is its ## Memory-System Proposals section.
  • Of its 5 entries: 3 are unambiguously operational rules rather than identity content, 1 is arguable, 1 is correctly filed.
  • One misfiled entry cites, in its own text, "the cost table in OPERATIONAL_RULES § Working method" — naming the file it should have been written to. The canonical statement of that same rule does exist in OPERATIONAL_RULES.md. The reviewer wrote a rule into the identity file while the rule's own text pointed at the operational file.
  • ProposalGC.ts --route across all curated files flags 1 entry, and 0 from DA_IDENTITY.md.
  • ProposalGC.ts --auto has run daily for 35 consecutive days and removed 1 entry in total. None of the misfiled entries is removable — each is a distinct live directive, and removal is provable-only by design.

There is no context cost, since both files are @-imported and the rule loads either way. The effect is that the identity file accumulates operational content with no mechanism that would surface it.

Reproduction

  1. Have the reviewer emit a proposal whose edit text states a general operational rule, names no skill, project or hook, and carries target_kind: "identity".
  2. classifyScope returns global; the gate at MemorySystem.ts:344 passes it.
  3. The proposal is written to DA_IDENTITY.md.
  4. bun LIFEOS/TOOLS/ProposalGC.ts --route does not flag it, on that run or any later one.

Suggested direction

Two shapes, both consistent with what is already there. Offering the design rather than a patch, because choosing between them is a call for this repo rather than for us — happy to implement whichever is preferred.

Decide. Derive a kind from the edit's content and, when it disagrees with the supplied target_kind for a member of ALWAYS_LOADED_KINDS, divert to the Upgrades queue exactly as the scope branch already does. This is the direct completion of the check #1563's docstring assumes. It needs a content classifier, which the scope axis already has an analogue of in ProposalScope.ts.

Flag. Record always-loaded proposals whose kind was never validated, and have ProposalGC.ts --route surface them alongside the scope-based advisories. No misrouting risk, no classifier required, and it reuses the advisory channel that exists. It converts an invisible permanent misfile into something a human sees.

A cheaper partial in either direction: with exactly two members in ALWAYS_LOADED_KINDS, even a coarse identity-versus-rule discriminator closes most of it.

One nearby thing checked and found benign, recorded so nobody re-derives it: inferProposalKind returns "identity" for a path matching no kind (MemoryTypes.ts:259). That looks like an unresolvable proposal defaulting into an always-loaded identity file, but pinProposalTargetFile then finds the path outside the allowed set and returns null, and the caller rejects the proposal. Not exploitable.

Related, not duplicates

Environment

  • LifeOS 7.40.4 (latest release)
  • macOS, bun
  • MemorySystem.ts, MemoryTypes.ts, ProposalScope.ts and ProposalGC.ts byte-identical to the shipped install payload and to upstream main — no local drift.
  • The memory proposal write path runs stock hooks: the reviewer fires on Stop and SessionEnd as shipped, and no local hook sits between the reviewer and the curated files.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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