Skip to content

Add optional pre-execution judgment gate for the tool gateway (opt-in, disabled by default) - #93

Open
Federico Blanco Sánchez-Llanos (babyblueviper1) wants to merge 1 commit into
microsoft:mainfrom
babyblueviper1:review-gate-tool-executor
Open

Federico Blanco Sánchez-Llanos (babyblueviper1) wants to merge 1 commit into
microsoft:mainfrom
babyblueviper1:review-gate-tool-executor

Conversation

@babyblueviper1

What this closes

HttpToolExecutor.ExecuteToolAsync (the built-in "toolgateway" adapter) receives the parsed
RequestContext<CallToolRequestParams> — tool name and arguments — checks Operation.Read
RBAC on the registered ToolResource, then forwards the arguments byte-for-byte to
{toolName}-service.adapter.svc.cluster.local. That's an authorization check ("can this
principal invoke this tool"), not a content check ("is this specific call, with these specific
arguments, safe to run"). AdapterReverseProxyController's separate proxy path is stricter
still — HttpProxy.CreateProxiedHttpRequest wraps the request body in a raw StreamContent
and never deserializes it, so a content-level check isn't possible there even in principle;
RBAC there is checked once, at the adapter level, before any MCP method is parsed.

Verified directly against current main (this branch is based on 1a66c42, #62) before
writing anything — not assumed from the README.

Concrete failure shape this leaves open: a principal with valid Operation.Read access to
a registered tool (say, a file-delete or shell-exec tool a downstream MCP server legitimately
exposes) gets that call forwarded unmodified regardless of what the arguments actually are.
RBAC answers "may this caller ever call this tool," not "should this particular invocation, with
these particular arguments, go through right now." That's a different, complementary question,
and today nothing in the gateway itself asks it — the two most recent security-hardening PRs
on this repo (#88, #89) tightened the authorization boundary; this is adjacent but orthogonal
to both.

The change

ReviewGatedToolExecutor — an IToolExecutor decorator, registered via one conditional block
in Program.cs. Disabled by default; identical runtime behavior unless ReviewGate:Enabled
is explicitly set to true.
When enabled, it calls an independent judgment endpoint with the
tool name + arguments before HttpToolExecutor runs, blocks on a clean high-confidence
reject, and falls through to the real executor on everything else — approve,
low-confidence/ambiguous reject, or the gate itself being unavailable (fail-open by design,
documented inline: this repo's execution path is fully automated/Kubernetes-oriented with no
interactive human-in-the-loop surface anywhere, so there's nowhere to hand an uncertain verdict
for a human to resolve — see the file-level comment in ReviewGatedToolExecutor.cs).

It has no opinion on which judgment provider sits behind the HTTP call — the only assumption is
POST {baseUrl}/review returning {"verdict": "approve"|"approve_with_concerns"|"reject", "confidence": number, "summary": string}. Details, config keys, and a pointer to a
live-verification harness are in the new dotnet/Microsoft.McpGateway.Tools/ReviewGate.md.

Tested:

  • 5 new offline MSTest cases (high-confidence reject blocks + never touches the inner executor;
    approve falls through; low-confidence reject falls through; a non-2xx and a timeout from the
    gate both fail open) — no new test-only NuGet dependency, a minimal fake
    HttpMessageHandler stands in for Moq.Protected.
  • Full Microsoft.McpGateway.Tools.Tests suite: 32/32 green (27 pre-existing + 5 new).
  • Microsoft.McpGateway.Service builds unaffected (this change doesn't touch that project).
  • Live-verified against a real judgment endpoint (not part of CI, separate opt-in harness): a
    benign list_files call fell through unmodified; a run_shell call with
    rm -rf / --no-preserve-root came back verdict=reject, confidence=1.00 and was blocked.
    Reference client + the harness: https://github.com/babyblueviper1/invinoveritas/tree/main/integrations/mcp-gateway

Genuine question, not an assertion

Is content-level judgment on tool-call arguments intentionally out of scope for the gateway
itself — i.e. is that expected to live in each downstream MCP/tool server, with the gateway's
job staying strictly routing + RBAC? If so this is happy to be closed / reshaped into a docs
note instead of a code change. If a hook point of this shape is useful, the PR is here to
review as-is — opt-in, off by default, no change to existing deployments.

HttpToolExecutor forwards a tool call's arguments to its execution endpoint after an
Operation.Read RBAC check on the ToolResource -- authorization, not content-level judgment
on what the specific call actually does. AdapterReverseProxyController's separate reverse-proxy
path is stricter still: it forwards the raw request body as an opaque StreamContent and never
deserializes it, so no content-level check is possible there even in principle.

ReviewGatedToolExecutor is an opt-in IToolExecutor decorator (disabled by default, zero
behavior change unless ReviewGate:Enabled=true) that calls an independent judgment endpoint
with the tool name + arguments before HttpToolExecutor runs, blocking on a clean
high-confidence reject and falling through otherwise (approve, low-confidence/ambiguous
reject, or the gate being unavailable -- see the fail-open design note in the new file).

5 offline MSTest cases cover: high-confidence reject blocks and never touches the inner
executor; approve falls through; a low-confidence reject falls through (uncertainty, not a
clean call); a non-2xx and a timeout from the gate both fail open. No new test-only NuGet
dependency -- a minimal fake HttpMessageHandler stands in for Moq.Protected.

Full test suite (32/32, including the pre-existing 27) verified green with this change; the
sibling Service project builds unaffected. See dotnet/Microsoft.McpGateway.Tools/ReviewGate.md
for configuration and a pointer to a live-verification harness run against a real judgment
endpoint (not part of this PR / CI -- separate opt-in tooling).
Federico Blanco Sánchez-Llanos (babyblueviper1) pushed a commit to babyblueviper1/invinoveritas that referenced this pull request Aug 4, 2026
…upstream)

ReviewGatedToolExecutor -- an IToolExecutor decorator closing a real, independently
verified gap in mcp-gateway's own tool-call path (RBAC-only authorization, no
content-level judgment on what a call actually does). Built and tested against a real
clone of the target repo (32/32 tests passing), then live-verified against the real
production invinoveritas API before a real PR was opened:
microsoft/mcp-gateway#93
@babyblueviper1

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

@babyblueviper1

Copy link
Copy Markdown
Author

Checked in on this since the workflow run history shows the CI check has never actually executed on the current head (cc5c20b) — it looks like it's sitting behind the first-time-contributor approval gate rather than failing.

Verified independently in the meantime, not just asserting it builds: cloned this branch and main separately and built both locally. Both produce the identical 3 CS0121 ambiguous-overload errors in pre-existing test files this PR doesn't touch (AgentManagementServiceTests.cs, ToolManagementServiceTests.cs, AdapterManagementServiceTests.cs) — same failure on unmodified main, so it's an SDK-version artifact on my end, not a real break, and not something this PR introduces either way.

Happy to adjust scope or split this up if that would make it easier to review.

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.

1 participant