Skip to content

fix(capabilities): client may only narrow the seeded ceiling (audit B1)#89

Merged
temrjan merged 2 commits into
mainfrom
fix/capability-ceiling-b1
Jul 24, 2026
Merged

fix(capabilities): client may only narrow the seeded ceiling (audit B1)#89
temrjan merged 2 commits into
mainfrom
fix/capability-ceiling-b1

Conversation

@temrjan

@temrjan temrjan commented Jul 24, 2026

Copy link
Copy Markdown
Member

Summary

Second half of closing audit B1 end-to-end (first half: rustok-org/core#96, merged as eec38c1). The capability ceiling is now authoritative in the core; the MCP layer only reads it and can never widen it.

  • Self-grant removed (handlers.py): initialize now intersects the client-declared capability list with the transport-seeded ceiling instead of replacing it. An operator launching with RUSTOK_MCP_CAPABILITIES=read_wallet gets a session the agent cannot talk out of. No seeded ceiling fails closed to an empty set — the server seeds, the client only narrows.
  • Policy mode read from the core: initialize reads policy_mode via WalletContext (core increment 1). read_only leaves read+preview tools; supervised/autonomous keep the full set while the core itself parks/denies writes. Unreachable core → transport ceiling alone with a warning — MCP-side filtering is advisory; enforcement stays in the core (no policy enforcement duplicated in this layer).
  • SSE second-initialize hole closed: the guard was the falsy empty set, so a standard MCP capabilities object (parses to empty) left the session open to a second, wider grant. Sessions now track initialized explicitly.
  • sign_message schema matches its contract: sign_type enum is ["eip191"] (it listed eip712 while the description said EIP-712 is not supported).

Spec: .claude/specs/2026-07-24-mcp-ceiling-integration.md

Test plan

  • pytest — 195 passed
  • ruff check . — clean
  • B1 regression: seeded {read_wallet} + client asks all → {read_wallet}; no seed → empty set (fail-closed)
  • read_only mode: tools/list hides execute/sign, keeps read+preview; supervised keeps the full set
  • Gateway error → transport ceiling fallback (advisory)
  • SSE: second initialize does not change session capabilities
  • sign_type enum == ["eip191"]
  • CHANGELOG (Unreleased) updated

temrjan added 2 commits July 24, 2026 15:39
- initialize now intersects client-declared capabilities with the
  transport-seeded ceiling instead of replacing it; no seed fails
  closed to an empty set
- policy mode ceiling read from the core (WalletContext.policy_mode):
  read_only leaves read+preview; unreachable core falls back to the
  transport ceiling with a warning (MCP-side filtering is advisory,
  enforcement lives in the core)
- SSE: a second initialize can no longer change session capabilities
  (explicit initialized flag instead of the falsy-set guard)
- sign_message: sign_type enum is ["eip191"] only, matching the
  documented EIP-191-only contract

Spec: .claude/specs/2026-07-24-mcp-ceiling-integration.md
Review delta for PR #89:
- SSE sessions are now seeded from the same operator ceiling, so the
  initialize intersection actually bites there too (B1 closed end-to-end;
  unset env keeps the historical gated-until-granted contract)
- ruff format compliance (CI gate was red on tests/test_handlers.py)
- test: seeded SSE session narrows client grants, keeps the stored ceiling
@temrjan
temrjan merged commit 0800a28 into main Jul 24, 2026
9 checks passed
@temrjan
temrjan deleted the fix/capability-ceiling-b1 branch July 24, 2026 11:05
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.

1 participant