feat(policy): add MCP-aware JSON-RPC L7 governance#1938
Conversation
Signed-off-by: ddurst <267424412+ddurst-nvidia@users.noreply.github.com>
Signed-off-by: ddurst <267424412+ddurst-nvidia@users.noreply.github.com>
Signed-off-by: ddurst <267424412+ddurst-nvidia@users.noreply.github.com>
Signed-off-by: ddurst <267424412+ddurst-nvidia@users.noreply.github.com>
Signed-off-by: ddurst <267424412+ddurst-nvidia@users.noreply.github.com>
Signed-off-by: ddurst <267424412+ddurst-nvidia@users.noreply.github.com>
|
@krishicks - Mostly for your review. |
Signed-off-by: ddurst <267424412+ddurst-nvidia@users.noreply.github.com>
a432508 to
8c9606b
Compare
|
I don't have permissions to add the |
| max_body_bytes: 131072 | ||
| rules: | ||
| deny: | ||
| - mcp_method: tools/call |
There was a problem hiding this comment.
protocol is already mcp, so why do we need it to be mcp_method here vs just method?
There was a problem hiding this comment.
Good catch. Easy enough to remove, leaving unresolved until I can.
| mcp: | ||
| max_body_bytes: 131072 | ||
| rules: | ||
| deny: |
There was a problem hiding this comment.
this would be a deviation from a separate deny_rules block that sits at the same level as rules, is there a particular reason for that?
There was a problem hiding this comment.
Particular reasons are:
- Support method-level governance for MCP tool calls (JSON-RPC) in sandbox policy #1793 's proposed design section.
- feat(l7): add JSON-RPC policy enforcement #1865 (comment)
But I'll address it more completely in the other comment.
| | `rules` | list of rule objects | No | Fine-grained protocol-specific allow rules. Mutually exclusive with `access`. | | ||
| | `deny_rules` | list of deny rule objects | No | L7 deny rules that block specific requests even when allowed by `access` or `rules`. Deny rules take precedence over allow rules. | | ||
| | `rules` | list or grouped object | No | Fine-grained protocol-specific allow rules. For MCP, prefer grouped `rules.allow` and `rules.deny`. Mutually exclusive with `access`. | | ||
| | `deny_rules` | list of deny rule objects | No | L7 deny rules that block specific requests even when allowed by `access` or `rules`. Deny rules take precedence over allow rules. Existing policies can keep this shape; new MCP policies should prefer grouped `rules.deny`. | |
There was a problem hiding this comment.
If we're going to do this I would prefer we do it for all L7 and not just MCP. Which would be out of scope for this PR but having just a single L7 parser introduce this isn't good UX.
There was a problem hiding this comment.
My particular reasons, don't outweigh precedent & uniformity across these specific objects.
I agree that it should be addressed separately as a Policy Format breaking change.
I do feel strongly that rules and deny_rules is a pretty awkward semantic sibling pair, but is that something you'd be interested in considering changing? I don't know the history of how it came to be other than conjecture.
- If there's a track to change it elsewhere, no concerns with changing it to
rules_deny. - If there's no real want to change it, I'll protest with a small sign, but would still make the change here with the goal of advancing the mission 😄
(Either way, leaving it unresolved until I have an opportunity to change it)
Summary
Builds on #1865 by keeping its shared JSON-RPC-family L7 enforcement foundation and adding an MCP-specific policy surface on top of it.
PR #1865 introduces generic JSON-RPC method/params enforcement. This variant preserves generic JSON-RPC as a first-class supported protocol for non-MCP services, while adding MCP-specific policy spelling, MCP validation via
tower-mcp-types, and conformance-oriented e2e behavior.Related Issue
Addresses #1793.
Builds on #1865.
Changes
Relationship to #1865
PR #1865 adds the shared JSON-RPC-family L7 enforcement layer. This branch builds on that layer with an MCP-specific policy surface and MCP message validation via
tower-mcp-types, while preserving generic JSON-RPC as a first-class supported protocol for non-MCP services.How This Addresses #1793
Issue #1793 asks for method-level governance for MCP tool calls because MCP traffic is carried as JSON-RPC over a single allowed connection, making network-only policy all-or-nothing.
This branch addresses that by allowing policy to distinguish MCP requests inside the connection:
mcp_methodfilters MCP/JSON-RPC method names such astools/call.toolfilters MCP tool calls byparams.name.paramsmatchers allow policy to constrain scalar MCP params such asarguments.scope.Key Differences
protocol: mcpwhile preservingprotocol: json-rpc.mcp.max_body_bytesmcp_methodtoolparamsmaps for MCP readability.tower-mcp-typesto validate known MCP request/notification shapes.Rule / OPA Tightening
mcp_methodnormalizes to the existingrpc_methodfield, so OPA has one method-matching path.toolnormalizes to MCPparams.name, but only whenparams.namewas not explicitly set.protocol: json-rpcorprotocol: mcp.2025-11-25conformance compatibility path after host, path, binary, endpoint protocol, and parse-shape checks. MCP2026-07-28(draft) removes the GET stream endpoint and client-sent JSON-RPC responses, so this allowance should be version-gated or removed when OpenShell enforces that transport version.Type Enforcement Follow-up
This branch does not add typed MCP argument enforcement. The current L7 JSON-RPC-family policy input flattens scalar params into string values before OPA evaluation, so
1and"1"ortrueand"true"are not distinguishable by policy today.Typed argument enforcement would be better handled as a wider L7 policy input change, for example by preserving scalar JSON type metadata alongside the existing flattened matcher keys. That would let a future MCP argument policy express
type: integerortype: booleanwithout weakening the existing JSON-RPC compatibility path.Conformance Notes
The MCP conformance policy template now uses:
Expected-failure carve-outs are limited to one known upstream conformance-client behavior:
Note
elicitation-sep1034-client-defaultsis allowed to fail because the pinned upstream TypeScript conformance client appears to advertiseelicitation.applyDefaults, while@modelcontextprotocol/sdk 1.29.0applies defaults only forelicitation.form.applyDefaults. The failure reproduces with the upstream conformance client directly, without OpenShell in the path. The carve-out should be removed whenOPENSHELL_MCP_CONFORMANCE_REFis bumped to an upstream commit where the bundled client advertises the corrected capability shape.Testing
mise run pre-commitpassesLocal validation run for this branch includes:
mise run e2e:mcpfor the MCP conformance suite.elicitation-sep1034-client-defaults.cargo test -p openshell-supervisor-network mcp_tool_deny_rule_blocks_tools_callcargo clippy -p openshell-supervisor-network --all-targets -- -D warningscargo test -p openshell-policycargo test -p openshell-supervisor-networkChecklist