Skip to content

fix(memory): implement the memory/episode/raw-turn scope assertions - #611

Open
akushonkamen wants to merge 1 commit into
MemTensor:mainfrom
akushonkamen:fix/593-scope-assertions
Open

akushonkamen wants to merge 1 commit into
MemTensor:mainfrom
akushonkamen:fix/593-scope-assertions

Conversation

@akushonkamen

Copy link
Copy Markdown

Fixes #593

Root cause

assertMemoryInScope, assertEpisodeInScope and assertRawTurnInScope in Memory/src/service/memory-service.ts (pre-fix :2847/:2852/:2857) all had the body void memory; void namespace; β€” they discarded both parameters and enforced nothing. Yet the archive/delete/panel/redact paths, the retrieval pipeline, the read models, and the feedback/trial/import pipelines all inject these methods as the scope boundary, so every namespace-scoped call went through with no actual boundary.

Fix (issue option 1, semantics-conservative)

The three assertions now perform real checks (Memory/src/service/memory-service.ts:2851-2875):

  • No namespace β†’ return immediately. The historical whole-local-store semantics are preserved; single-namespace local installs see zero change.
  • Explicit userId/projectId contradicting the row's owner β†’ MemoryServiceError("forbidden"). This mirrors the existing in-function paradigm in deleteMemory (user-memory branch :1821, L3 World Model branches :1878-1881). Sparse namespaces (no userId/projectId) are not newly rejected.
  • RawTurnRecord has no projectId field, so its assertion only compares userId.

Contract alignment. Two existing tests prove single-item detail GETs are shared-resource reads, not authorization boundaries: the REST contract test ("enforces scoped API permissions … shared resource access") requires a cross-key GET /memory/:id to return 200, and the evolution test requires cross-user getMemory policy merging. The misleading assertMemoryInScope calls are therefore removed from the two shared read paths β€” getMemory (Memory/src/service/read-model/episode.ts:230) and getSkill (Memory/src/service/read-model/skill.ts:248, whose list endpoint GET /api/v1/skills was already namespace-unfiltered) β€” each replaced by a comment explaining why. Enforcement stays on all mutation paths (archive/delete/redact/deletePanelTask) and the session/turn/feedback/trial/import pipelines β€” exactly the issue's request to "distinguish a relevance/project filter from an authorization boundary".

Left untouched on purpose: the L3 v2 checks; assertSessionInScope (:2824, same-shaped no-op, outside this issue's three-assertion scope); scopeBundleTables (:3119, whole-store export path).

Test evidence

New negative test Memory/tests/service/memory-scope-assertions.test.ts (the two cross-namespace negative tests named by the issue author, plus a no-namespace whole-store control):

  • Pre-fix red: npx vitest run tests/service/memory-scope-assertions.test.ts β†’ 3 failed / 1 passed (cross-user archive/delete, cross-project delete, and episode delete + raw-turn redact all completed without throwing β€” the no-op confirmed).
  • Post-fix green: npx vitest run tests/service/memory-scope-assertions.test.ts β†’ 4 passed / 4.
  • Related subset: npx vitest run tests/service tests/contract β†’ 60 files / 568 tests all passed (includes the REST shared-read-200 contract case and the evolution cross-user policy-merge case).
  • Full Memory suite: npx vitest run --dir tests β†’ 117 files passed / 1 skipped; 1080 tests passed / 1 skipped / 0 failed.
  • Type check: npx tsc -p Memory/tsconfig.json --noEmit β†’ exit 0.

AI disclosure

This fix was prepared with AI assistance (Claude Code) and submitted by a human contributor, as disclosed in the issue thread.

assertMemoryInScope, assertEpisodeInScope and assertRawTurnInScope
discarded both parameters (void memory; void namespace), so the
get/delete/archive/retrieval call sites that pass a namespace had no
boundary at all (MemTensor#593).

The three assertions now reject a request whose namespace explicitly
carries a userId or projectId that contradicts the row's owner
(MemoryServiceError forbidden), mirroring the existing user-memory and
L3 World Model checks in deleteMemory. A request without a namespace
keeps the historical whole-local-store semantics, so single-namespace
local installs see no change, and sparse namespaces (no userId/projectId)
are not newly rejected.

Single-memory and skill-detail GETs stay reachable across namespaces on
purpose: the REST contract requires cross-key shared resource reads and
the evolution pipeline merges policies across user ids, while the skills
list endpoint is already namespace-unfiltered. The misleading
assertMemoryInScope call is therefore removed from those two shared read
paths; enforcement lives on the mutation paths (archive, delete,
redact, panel task delete) and the session/turn/feedback/trial/import
pipelines.

Negative tests cover cross-namespace archive/delete, cross-project
delete, no-namespace whole-store behavior, and cross-namespace episode
delete plus raw-turn redaction.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generic memory/episode/raw-turn scope assertions are no-ops

1 participant