feat(discovery): surface quarantined tools in retrieve_tools (locked, name-only)#778
Conversation
… name-only)
Quarantined tools — both server-level quarantine and tool-level
pending/changed approvals — are intentionally absent from the BM25 search
index so their untrusted descriptions cannot expose a Tool Poisoning Attack
(TPA) payload to the agent. As a side effect, retrieve_tools could only answer
"no such tool" for a capability a quarantined server provides, even though the
correct remediation is "ask the user to approve it".
Add an opt-in second pass to retrieve_tools(include_disabled=true) that
enumerates quarantined tools from authoritative state and returns NAME-ONLY
locked entries (description and schema withheld) with a status + remediation:
- server-level quarantine -> new status `server_quarantined`
- tool-level pending/changed -> existing `pending_approval`
The pass is fully self-contained — it does NOT alter the shared callability or
classification helpers, so visible-tool counts and other consumers are
unchanged. Specifically it:
- matches the query against the tool NAME only (quarantined tools aren't in
the index, so they can't be BM25-ranked); short keywords (>=2 chars, e.g.
"ui"/"qa") are retained;
- applies the same agent-scope + profile filtering as the callable path, via
a shared `serverDiscoverable` helper (de-duplicates what were three inline
copies down to one for the discovery surface);
- dedups against tools the index loop already handled (a `seen` set), so the
brief post-quarantine reindex window can't list a tool as both callable and
locked, nor double-count it in the zero-result nudge;
- skips tools also denied by operator config (enabled_tools/disabled_tools),
which approval cannot unlock — so the agent isn't sent down a dead-end
remediation;
- prepends its matches so the shared min(limit,10) cap can't truncate them in
favor of index hits.
Quarantined tools remain non-callable: the call path already blocks
server-quarantine and pending/changed before execution (handleQuarantinedToolCall),
so no change to isToolCallable is needed — discovery only makes them VISIBLE,
never callable.
Tests: pending tool surfaced by name with withheld description + remediation;
server-level branch (tool-names source injected); query-scoped exclusion;
config-denied skipped; dedup against seen; short-keyword tokens; zero-result
nudge; quarantined tool still blocked at the call path; remediation present.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…value taxonomy) (#779) Aligns Spec 049 with PR #778, which adds a sixth disabled-tool status, server_quarantined, surfaced by a dedicated quarantined-tool discovery pass (quarantined tools are deliberately excluded from the search index as a TPA defense). Spec 049 pinned the taxonomy to exactly five values and assumed all locked tools live in the index, so #778's behavior was correct but undocumented. - FR-004: five -> six values; server_quarantined assigned by the discovery pass (not the classifier), name-only, description/schema withheld; config-denied tools skipped by the pass. - FR-003: note the name-only exception for quarantined entries. - Assumptions: quarantined tools are excluded from the index and enumerated from authoritative quarantine state. - contracts/mcp-deltas.md: add server_quarantined to the status enum + example response shape and remediation. - design doc taxonomy: five -> six, with the server_quarantined explanation. Related #778 Co-authored-by: Algis Dumbris <gordon.greatests@gmail.com>
|
Thank you, @electrolobzik — this is a genuinely well-crafted contribution. 🙏 A few things I appreciated reviewing it:
One thing surfaced in review: the new |
There was a problem hiding this comment.
✅ Gatekeeper approval — Codex review verdict: ACCEPT.
This approval is posted automatically by the MCPProxy Gatekeeper App on behalf of the Codex reviewer (verdict of record lives in the Paperclip review thread). Author≠approver satisfied; QA + CI gates enforced separately.
Auto-approved per Model B (MCP-1249).
Problem
Quarantined tools — both server-level quarantine and tool-level pending/changed approvals — are intentionally excluded from the BM25 search index so their untrusted descriptions can't expose a Tool Poisoning Attack (TPA) payload to the agent. That's a correct defense, but a side effect is that
retrieve_toolscan only answer "no such tool" for a capability a quarantined server provides — even when the right remediation is "ask the user to approve it." The agent never learns the capability exists.Change
Adds an opt-in second pass to
retrieve_tools(include_disabled=true)that enumerates quarantined tools from authoritative state and returns name-only locked entries (description and schema withheld) with a status + remediation:server_quarantinedpending_approvalThe pass is self-contained — it does not alter the shared callability/classification helpers, so visible-tool counts and other consumers are unchanged. It:
ui/qa) are retained;serverDiscoverablehelper (collapses what were duplicated inline copies for the discovery surface);seenset), so the brief post-quarantine reindex window can't list a tool as both callable and locked, nor double-count it in the zero-result nudge;enabled_tools/disabled_tools), which approval cannot unlock — so the agent isn't sent down a dead-end remediation;min(limit,10)cap can't truncate them in favor of index hits.Quarantined tools remain non-callable: the call path already blocks server-quarantine and pending/changed before execution, so discovery only makes them visible, never callable.
Tests
internal/server/mcp_quarantine_discovery_test.go: pending tool surfaced by name with withheld description + remediation; server-level branch (tool-names source injected for unit testing); query-scoped exclusion; config-denied skipped; dedup againstseen; short-keyword tokens; zero-result nudge; quarantined tool still blocked at the call path; remediation present.go build ./...,go vet, andgolangci-lint(repo.github/.golangci.yml) are clean;internal/{server,runtime,index,storage,contracts}suites pass (the binary-prereq E2E tier needs a pre-built binary and was not exercised).🤖 Generated with Claude Code