Skip to content

fix(mcp): intersect per-call scope params with env-pinned tenant scoping (#108) - #143

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

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

Conversation

@devinmlowe

Copy link
Copy Markdown
Owner

Closes #108.

Summary

resolveCallScoping let request params replace the env: read_scopes replaced ENGRAM_READ_SCOPES and scope replaced ENGRAM_SCOPE, so a stdio child pinned to one Hermes profile could read and write any tenant with one tool argument.

Now, when the env restricts (getTenantScoping(env).readScopes defined):

  • read_scopes is intersected with the env read scopes; empty intersection throws.
  • scope must be one of the env read scopes (write only where you may read); otherwise throws.
  • The env read scopes stay the read default.

With the env unset (shared HTTP daemon, params are the tenant identity) behaviour is unchanged.

One guard in the shared resolver covers all nine scoped call sites in server.ts. provider.py's docstring ("no tool argument can write outside this profile's scope") is now true. #109 (forget scope: "global" bypass) and #87 (bridge drops env) are untouched siblings.

Changes

  • src/interfaces/mcp/scoping.ts — intersect / membership checks with actionable error messages.
  • tests/core/tenant-scoping.test.ts — 4 new cases; existing override case now stays inside ENGRAM_READ_SCOPES.
  • tests/interfaces/mcp/per-request-scoping.test.ts — env-pinned test asserts both escapes return isError (it previously asserted the escape).
  • docs/integrate-your-agent.md, CHANGELOG.md.

Test plan

  • npm run test:run — 132 files / 1692 tests pass, 13 skipped
  • npm run lint clean

🤖 Generated with Claude Code

https://claude.ai/code/session_014FJcVdAMkcEH6U3ZMKrEwv

devinmlowe and others added 2 commits September 21, 2026 01:20
…ing (#108)

Client-supplied read_scopes replaced ENGRAM_READ_SCOPES and scope replaced
ENGRAM_SCOPE, so a stdio child pinned to one Hermes profile could read and
write any tenant with one tool argument. resolveCallScoping now treats the
env read scopes as the ceiling when they are set: read_scopes is intersected
(empty intersection throws) and scope must be one of them. With the env
unset (shared HTTP daemon) the params remain the tenant identity, unchanged.

Tests flipped where they asserted the old escape; docs and changelog say
which deployment pins the tenant where.

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

Conflict was only CHANGELOG.md (Unreleased/Fixed: #87 and #109 on main, #108 here). All three kept.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xo3z3uv6caDhR8zjrv5QF
@devinmlowe
devinmlowe merged commit f5c0ba8 into main Sep 21, 2026
10 of 12 checks passed
@devinmlowe
devinmlowe deleted the fix/issue-108-scope-bypass branch September 21, 2026 23:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant