Skip to content

feat(profile): discovery enforcement — hidden_by_profile, policy-aware search (Spec 108-b) - #1390

Merged
github-actions[bot] merged 11 commits into
mainfrom
108-b-profile-discovery-enforcement
Sep 28, 2026
Merged

github-actions[bot] merged 11 commits into
mainfrom
108-b-profile-discovery-enforcement

Conversation

@Dumbris

@Dumbris Dumbris commented Sep 26, 2026 •

Copy link
Copy Markdown
Member

Summary

Implements profile-aware discovery for MCP and REST surfaces. Search now filters excluded tools before applying result limits and reports accurate hidden-by-profile counts; describe_tool and direct list routes hide excluded tools uniformly. Cache reuse is keyed by the profile policy so a policy change cannot reuse an admitted result from the prior policy.

The fail-closed rollout gate remains active until execution enforcement lands in Spec 108-d.

Spec and findings

  • Spec: specs/108-profiles-v3/ — FR-009a, FR-010–012, FR-015 and FR-015a.
  • UX/security finding: P3 profile discovery enforcement.
  • Frozen golden: internal/server/testdata/retrieve_tools_profile_v3.golden.json.

Verification

  • go build ./...
  • Race tests passed for internal/cache, internal/index, internal/runtime/supervisor, and internal/server with the server build tag and repository skip regex.
  • Both golangci-lint full passes were run; the local Go 1.26 tool reports existing diagnostics outside this PR. Both editions report 0 new diagnostics with --new-from-rev=origin/main. GitHub CI is running on the re-synced head.
  • Live isolated config check on port 18791 with scratch HOME/config/data and telemetry disabled: a real binary rejects a profile with max_tier: read, exits 4, and prints max_tier is not supported by this build (Profiles v3 enforcement incomplete), as required by FR-009a. Discovery-specific enforcement is covered by the PR's profile test suite; a live profile-policy session is enabled in 108-d when every execution path is enforced.
  • ZCode GLM-5.3: implementation review found and verified fixes for the discovery gaps; merge-resolution incremental review returned NO FINDINGS.

Re-sync

Clean automatic merge of current main after PR #1381; no manual conflict resolution.

Spec 108 (Profiles v3) FR-011: SearchToolsScoped's predicate only sees a
hit's server name, so a caller with a tool-level policy could have a
policy-excluded hit displace an admitted one from a limited window, and
hidden_by_profile would undercount.

SearchToolsAdmitted extends the index API with a hit-level predicate over
the canonical (server, tool) identity, returning a three-way Admission
(Admit / RejectScope / RejectPolicy) instead of a bool: the limit applies
to admitted hits only, and RejectPolicy (never RejectScope) is counted
over the full exhaustive match set. The index stores no annotations and
gains none; the predicate's tier classification comes from the caller.
PR 108-b (profile-discovery-enforcement), FR-010/011/012/015/019.

retrieve_tools now filters by tool policy before the ranked cut via
SearchToolsAdmitted, gains hidden_by_profile (non-legacy profiles only,
SC-003 byte parity preserved) and profile (url/session sources only,
never a pin). describe_tool (indexed and direct surfaces) answers an
excluded tool with the same uniform not-found response a nonexistent
tool gets. /mcp/all tools/list drops policy-excluded tools at list time
only, leaving the call-time filter chain untouched so a future call-time
gate (108-d) still reaches the handler. indexedToolVisible gets the
identical policy check as post-cut defence in depth.

resolveActiveProfileWithSource extends the existing resolver to also
report which precedence tier (pin/url/session) produced the effective
profile, needed to decide whether the new `profile` field may name the
slug at all.

Prompts are unaffected by design (FR-019): filterAggregatedPromptsForAuth
consults only server scope, never the tool policy.

Policy exercised only under the FR-009a test override until 108-d lifts
the rollout gate.
A read_cache page produced under one tool policy must be refused once
that policy changes, even when the profile's server set (already covered
pre-108) is unchanged.

Authorization gains PolicyFingerprint (the effective profile's compiled
policy fingerprint, hex-encoded) and ToolTierGeneration, both folded into
the existing digest so the frame header's stamp mechanics are unchanged.
Supervisor.ToolTierGeneration is a coarse, whole-fleet counter bumped once
per publishDiscoveredTools call, so an out-of-band tool re-annotation
invalidates cached pages with no config edit at all.
T001-T003 (shared setup, already done in 108-a) and T016-T026
(profile-discovery-enforcement) checked off. Regenerated ROADMAP.md.
Picks up 281b5c8 (Spec 109 health status vocabulary), unrelated to
this PR's files.
1. SearchToolsAdmitted stopped counting hiddenByPolicy as soon as the
   page filled with `limit` admitted hits, undercounting any
   policy-excluded match ranked below the cut. FR-011 requires the count
   over the full match set; the page now stops growing at `limit` but
   the scan keeps counting to the end.

2. collectQuarantinedToolMatches (the quarantined/pending "locked" entry
   pass) filtered only by server scope, so a tool that was BOTH
   policy-excluded and quarantined/pending/changed was named in the
   disabled[] array and counted as "locked" instead of staying silently
   invisible like every other excluded tool. Post-filtered through the
   same CompiledPolicy.Decide the rest of discovery uses.

3. describe_tool's policy re-check ran after the quarantine/pending/
   callability gates, so an excluded-and-locked tool answered its lock
   reason (confirming existence) instead of the uniform not-found the
   contract requires. Moved right after the scope check, before every
   gate that would otherwise narrow an admitted tool's shape.

Each fix ships with a regression test reproducing the exact scenario.
… nit)

Round 2 review confirmed the round-1 fixes are correct, but flagged that
the doc comment still claimed identical exhaustive paging to
SearchToolsScoped -- the two now differ in stop rule on purpose (this
method never early-returns, so hiddenByPolicy counts the full match
set). Comment-only change; behaviour unchanged.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 632cb1b
Status: ✅  Deploy successful!
Preview URL: https://55ccb927.mcpproxy-docs.pages.dev
Branch Preview URL: https://108-b-profile-discovery-enfo.mcpproxy-docs.pages.dev

View logs

@codecov-commenter

codecov-commenter commented Sep 26, 2026 •

Copy link
Copy Markdown

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

Codecov Report

❌ Patch coverage is 91.13924% with 14 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/index/bleve.go 86.66% 2 Missing and 2 partials ⚠️
internal/index/manager.go 66.66% 1 Missing and 1 partial ⚠️
internal/runtime/supervisor/supervisor.go 33.33% 2 Missing ⚠️
internal/server/mcp.go 95.65% 1 Missing and 1 partial ⚠️
internal/server/mcp_describe_direct.go 80.00% 1 Missing and 1 partial ⚠️
internal/server/mcp_visibility.go 90.90% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

📦 Build Artifacts

Workflow Run: View Run
Branch: 108-b-profile-discovery-enforcement

Available Artifacts

  • archive-darwin-amd64 (30 MB)
  • archive-darwin-arm64 (27 MB)
  • archive-linux-amd64 (18 MB)
  • archive-linux-arm64 (16 MB)
  • archive-windows-amd64 (30 MB)
  • archive-windows-arm64 (27 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (24 MB)
  • installer-dmg-darwin-arm64 (22 MB)
  • smart-mcp-proxymcpproxy-goZTX0Y5.dockerbuild (0 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 36392819630 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

- retrieve_tools' usageStatEligible (include_stats filter) now applies the
  same Spec 108 policy gate its sibling resolvers (toolVisibleToSession,
  indexedToolVisible) already had, so a policy-hidden tool can no longer be
  named in usage_summary.top_tools for a profile-scoped caller even though
  an earlier caller's usage created a fleet-wide stats record for it (F1).
- TestScopeOracleV3_HiddenByProfileIdenticalAcrossFixtures now probes with
  the hidden servers' own sentinel tool names, not only queries that can
  never match them, so the differential oracle can actually detect a scope
  leak instead of comparing two empty result sets (F2).
- TestRetrieveTools_ScopeLatency_ProfileV3VsLegacy now rebuilds the two
  per-profile Bleve indexes from the shared one before measuring, so the
  benchmark scans real hits and actually pays the policy path's per-hit
  cost instead of two no-op searches against empty ForProfile stores (F3).

Each fix is verified test-first: the new/extended assertions reproduce red
against the pre-fix code and pass after it.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved: exact-head live QA passed and ZCode merge-only review returned PASS for 632cb1b.

@github-actions
github-actions Bot merged commit 6e1615c into main Sep 28, 2026
57 checks passed
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