Skip to content

refactor(quarantine): unify tool-policy decision logic across entrypoints#529

Merged
Dumbris merged 1 commit into
mainfrom
followup/quarantine-callability-cleanup
May 26, 2026
Merged

refactor(quarantine): unify tool-policy decision logic across entrypoints#529
Dumbris merged 1 commit into
mainfrom
followup/quarantine-callability-cleanup

Conversation

@Dumbris
Copy link
Copy Markdown
Member

@Dumbris Dumbris commented May 26, 2026

Summary

Follow-up to #526 (direct-mode callability) and #527 (output-schema approval hash), addressing the maintainability concerns raised in review. Behavior-preserving cleanup — no functional change for users.

Motivation

  • fix: enforce direct-mode tool callability #526 introduced a third place that derives "is this tool callable" and builds the TOOL_QUARANTINED pending/changed payload, separate from handleCallToolVariant and isToolCallable/blockedToolMessage. Three sources of truth drift over time — exactly the kind of gap that could let call_tool_* and direct mode disagree on policy.
  • fix: include output schema in tool approval hash migration #527's captureOutputSchemaJSON baked a marshal-error JSON payload into the contract hash on failure, and added a third copy of "canonical JSON" normalization.

Changes

  • Single source of truth for block payloads. Extract toolPendingApprovalResult / toolChangedApprovalResult / toolPolicyJSONResult (mcp_tool_policy_result.go). Both call_tool_* and direct mode now call them, so the two entrypoints return byte-identical responses and can't drift.
  • Single config-denial authority. Add MCPProxyServer.isToolConfigDenied — prefers the live runtime config (the same source isToolCallable consults), falls back to the stored server config when the runtime is unavailable (unit tests). blockedToolMessage and the direct-mode evaluator both route through it, removing the divergent serverConfig.IsToolAllowedByConfig path.
  • No error payloads in the hash. captureOutputSchemaJSON returns "" on marshal failure instead of an error-keyed JSON object, so a transient marshal blip can't spuriously flip a tool to changed.
  • One JSON normalizer. Consolidate runtime.normalizeJSON and core.normalizeRawJSON onto a single exported hash.NormalizeJSON.

Net −33 lines.

Validation

go test -race ./internal/hash ./internal/upstream/core ./internal/runtime -count=1
go test ./internal/server -count=1   # requires repo-root binary for the E2E binary tests
  • The approval-hash stability canary (TestCalculateToolApprovalHash_Stability) passes unchanged — the shared normalizer is byte-identical to the originals.
  • All direct-mode / call_tool / quarantine tests pass unchanged.
  • Adds drift-guard tests: TestToolPendingApprovalResult_Shape, TestToolChangedApprovalResult_Shape, TestNormalizeJSON.

…ints

Follow-up to #526 and #527, addressing review concerns.

call_tool_* variants and direct mode (#526) duplicated the pending/changed
TOOL_QUARANTINED payload and independently derived config-denial — a second
source of truth that could drift from isToolCallable/blockedToolMessage.

- Extract toolPendingApprovalResult/toolChangedApprovalResult/toolPolicyJSONResult
  as the single source of truth; call_tool_* and direct mode both use them.
- Add MCPProxyServer.isToolConfigDenied as the one config-denial authority
  (prefers live runtime config, falls back to stored server config); both
  blockedToolMessage and the direct evaluator route through it.
- captureOutputSchemaJSON (#527) returns "" on marshal failure instead of
  baking an error payload into the contract hash (which would spuriously flip
  the tool to "changed").
- Consolidate the three JSON normalizers (runtime.normalizeJSON,
  core.normalizeRawJSON) onto a single exported hash.NormalizeJSON.

Net -33 lines. Behavior-preserving: the approval-hash stability canary and all
direct/call_tool/quarantine tests pass unchanged; adds drift-guard tests for the
shared builders and NormalizeJSON.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 2c7ef4e
Status: ✅  Deploy successful!
Preview URL: https://5eeb2372.mcpproxy-docs.pages.dev
Branch Preview URL: https://followup-quarantine-callabil.mcpproxy-docs.pages.dev

View logs

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 26, 2026

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 82.69231% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/upstream/core/output_schema.go 0.00% 3 Missing ⚠️
internal/hash/hash.go 80.00% 1 Missing and 1 partial ⚠️
internal/server/mcp.go 77.77% 2 Missing ⚠️
internal/server/mcp_tool_policy_result.go 92.30% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@github-actions
Copy link
Copy Markdown

📦 Build Artifacts

Workflow Run: View Run
Branch: followup/quarantine-callability-cleanup

Available Artifacts

  • archive-darwin-amd64 (27 MB)
  • archive-darwin-arm64 (25 MB)
  • archive-linux-amd64 (16 MB)
  • archive-linux-arm64 (14 MB)
  • archive-windows-amd64 (27 MB)
  • archive-windows-arm64 (24 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (21 MB)
  • installer-dmg-darwin-arm64 (18 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 26432262885 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

@Dumbris Dumbris merged commit ef8ea69 into main May 26, 2026
28 checks passed
@Dumbris Dumbris deleted the followup/quarantine-callability-cleanup branch May 26, 2026 04:58
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.

2 participants