Skip to content

fix(mcp): forget ignores scope "global" when the env pins read scopes (#109) - #142

Merged
devinmlowe merged 2 commits into
mainfrom
fix/issue-109-scope-bypass
Sep 21, 2026
Merged

devinmlowe merged 2 commits into
mainfrom
fix/issue-109-scope-bypass

Conversation

@devinmlowe

Copy link
Copy Markdown
Owner

Closes #109.

Problem

forget accepted scope: "global" from any client as an override that skips assertScope and widens the query-mode candidate search to every scope. A stdio child pinned by ENGRAM_READ_SCOPES (the Hermes memory-provider transport) could therefore delete, or search for and delete, another tenant's memories. On an unrestricted server the override changed nothing (readScopes is already undefined), so it only ever added escalation.

Fix

handleForget (src/interfaces/mcp/server.ts) now honours the override only when the server's own env does not pin reads:

const envPinned = getTenantScoping(process.env).readScopes !== undefined;
const scope = envPinned ? undefined : params.scope;

scope feeds both forgetMemory and the candidate-search scopes. forgetMemory itself is unchanged — the MCP handler is its only caller that can pass scope (the CLI never does). The tool description states the rule, so the #119 tool-schemas snapshot is re-recorded for that sentence only. README, CHANGELOG and the ScopeGate.scope doc comment updated.

Not addressed: read_scopes replacing the env instead of intersecting (#108). Until that lands an env-pinned caller can still widen via read_scopes; the intersection fix subsumes this guard.

Tests

  • New: scope: "global" is no override when ENGRAM_READ_SCOPES pins the process (#109) in tests/interfaces/mcp/forget-tool.test.tsmemory_id path refused with outside read_scopes, no change-log row, memory still active; query + confirm path returns forgotten="none" and does not list the other tenant's id.
  • Existing override test renamed to say it covers the unrestricted env.
  • npm run test:run: 132 files / 1690 tests pass. npm run lint clean.

Note: the matching one-line CLAUDE.md edit is deliberately not in this branch.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HyK4uwXAVufWKdSRc7VRey

devinmlowe and others added 2 commits September 21, 2026 01:19
…#109)

`scope: "global"` was documented as the override that lets `forget` act on
a memory in any scope. `handleForget` passed it straight through to
`assertScope` and used it to widen the query-mode candidate search, so a
stdio child pinned by ENGRAM_READ_SCOPES (the Hermes memory-provider
transport) could delete another tenant's memories.

The override now applies only when `getTenantScoping(process.env).readScopes`
is undefined, i.e. the server itself is unrestricted — the one case where
the override changed nothing before. Env-pinned callers get the ordinary
read_scopes refusal on memory_id and see no cross-tenant candidates on
query. `forgetMemory` itself is unchanged; the CLI never passes `scope`.

Regression test in forget-tool.test.ts; the tool description states the
rule, so the tool-schemas snapshot is re-recorded for that sentence.
`read_scopes` replacing the env (rather than intersecting) is #108 and is
not addressed here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyK4uwXAVufWKdSRc7VRey
…lets in CHANGELOG

Conflict was only CHANGELOG.md: main added the #87 bullet under Unreleased/Fixed,
this branch added the #109 bullet. Both kept.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xo3z3uv6caDhR8zjrv5QF
@devinmlowe
devinmlowe merged commit 78dbb66 into main Sep 21, 2026
10 of 12 checks passed
@devinmlowe
devinmlowe deleted the fix/issue-109-scope-bypass branch September 21, 2026 23:12
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.

[mcp] forget's scope: "global" bypasses assertScope for every caller — an env-pinned tenant can delete another tenant's memories

1 participant