Avoid priority annexing unlisted tools - #6127
Conversation
ChrisJBurns
left a comment
There was a problem hiding this comment.
Multi-Agent Consensus Review
Agents consulted: correctness-security, test-coverage, code-quality (pr-reviewer specialists)
Consensus Summary
| # | Finding | Consensus | Severity | Action |
|---|---|---|---|---|
| 1 | Missing test coverage for 3+-way conflicts mixing listed and unlisted backends | 8/10 | MEDIUM | Suggest |
| 2 | selectWinner's nil-return doc comment is stale relative to its only call site |
8/10 | MEDIUM | Suggest |
| 3 | Fallback rename of a listed backend's tool is only logged at Debug | 7/10 | MEDIUM | Discuss |
Overall
This PR closes #6097 correctly: it inverts the conflict-resolution control flow so that any tool-name collision involving a backend absent from priorityOrder is treated as not safely rank-comparable, prefixing every candidate instead of letting a listed backend silently annex the bare name. Tracing the fix against the issue's exact repro steps confirms the annexation hole is closed, and the "forbid fails closed" invariant is preserved since the fallback renames but never drops candidates.
The remaining findings are refinements, not correctness gaps: the fix already generalizes to N-way conflicts by checking "any candidate unlisted" rather than "exactly one," but no test proves it; a stale doc comment on selectWinner no longer matches the guarantees of its only call site; and the fallback path — now reached more often since it broadened from "all unlisted" to "any unlisted" — renames a listed backend's previously-bare, Cedar-policy-bound tool name while only logging at Debug.
Documentation
No documentation files are affected by this diff; consider a one-line note in the PR description that the rename can also affect a listed backend's tool when it collides with an unlisted one, not just the unlisted backend's tool.
Generated with Claude Code
|
@kocaemre you able to address the above? |
b9b3789 to
0fa55cc
Compare
|
Addressed, thanks. I added the 3-way mixed listed/unlisted regression, updated the stale resolver comments, and changed the prefix-fallback log from Debug to Warn. Also updated the PR description with the reviewer follow-up and latest verification commands. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #6127 +/- ##
==========================================
- Coverage 78.98% 78.91% -0.08%
==========================================
Files 782 782
Lines 78065 78058 -7
==========================================
- Hits 61658 61597 -61
- Misses 16402 16456 +54
Partials 5 5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
One remaining legacy I attempted to rerun the failed check from the CLI; if GitHub permissions allow it, this comment can be ignored. Otherwise, could a maintainer rerun that failed legacy matrix job? |
0fa55cc to
47e17ca
Compare
|
Refreshed this PR branch onto current No code changes beyond replaying the existing PR commit on top of current upstream. Local verification in this cron environment: PATH=/usr/local/go/bin:/root/go/bin:$PATH go test -ldflags=-extldflags=-Wl,-w -race ./pkg/vmcp/aggregator -run TestPriorityConflictResolver
# ok github.com/stacklok/toolhive/pkg/vmcp/aggregator 1.215s
PATH=/usr/local/go/bin:/root/go/bin:$PATH golangci-lint run --allow-parallel-runners ./pkg/vmcp/aggregator/...
# 0 issues.
PATH=/usr/local/go/bin:/root/go/bin:$PATH go vet ./pkg/vmcp/aggregator/...
# passed
git diff --check origin/main..HEAD
# passed
git log --format='%h %s%n%b' origin/main..HEAD
# 47e17ca6 Avoid priority annexing unlisted tools
# Signed-off-by: Emre K <110906681+kocaemre@users.noreply.github.com>I also attempted the full repo GitHub had not populated the refreshed check rollup yet immediately after the push ( |
|
Follow-up on the refreshed CI run: the only red check I see is Failure evidence from the job log: This looks like an E2E runner port collision in I attempted to rerun the failed job from the CLI, but GitHub refused it for my account on this repo: gh api -X POST repos/stacklok/toolhive/actions/runs/34322045823/rerun-failed-jobs
# HTTP 403: Must have admin rights to Repository.Could a maintainer rerun that failed E2E core job when convenient? |
47e17ca to
0944c34
Compare
|
Refreshed this PR branch onto current No code changes beyond replaying the existing priority-conflict fix on top of current upstream. The previous failed GitHub check looked unrelated to this PR: Local verification after the refresh: git diff --check origin/main..HEAD
# passed
PATH=/usr/local/go/bin:/root/go/bin:$PATH go test -run 'TestPriorityConflictResolver' ./pkg/vmcp/aggregator
# ok github.com/stacklok/toolhive/pkg/vmcp/aggregator 0.086s
PATH=/usr/local/go/bin:/root/go/bin:$PATH go test ./pkg/vmcp/aggregator
# ok github.com/stacklok/toolhive/pkg/vmcp/aggregator 0.091s
PATH=/usr/local/go/bin:/root/go/bin:$PATH golangci-lint run --allow-parallel-runners ./pkg/vmcp/aggregator
# 0 issues.I attempted the repo-level |
|
Investigated the new red Failure evidence from the job log: That failing test is in Local checks from the refreshed branch: git diff --check origin/main..HEAD
# passed
/usr/local/go/bin/go test -race ./pkg/vmcp/aggregator -run TestPriorityConflictResolver
# ok github.com/stacklok/toolhive/pkg/vmcp/aggregator 1.193s
/usr/local/go/bin/go test -ldflags=-extldflags=-Wl,-w -race ./pkg/transport/proxy/transparent -run TestRoundTripReinitializesPreservesNonUUIDBackendSessionID -count=1 -v
# PASS
# ok github.com/stacklok/toolhive/pkg/transport/proxy/transparent 1.114sI also tried to rerun the failed job, but GitHub rejected it for my account: gh api -X POST repos/stacklok/toolhive/actions/runs/34422700480/rerun-failed-jobs
# HTTP 403: Must have admin rights to Repository.This looks like another transient/unrelated test-runner failure rather than a regression from this PR. Could a maintainer rerun the failed |
JAORMX
left a comment
There was a problem hiding this comment.
Panel review of 0944c34c0f441363d5368a5e157e58bae89f368b against main cdb04c94f2462553fc27572bc8db9bb88a0c3c78 and #6097.
Blocking — prefix fallback is an authorization fail-open
The mixed listed/unlisted path prefixes every candidate (pkg/vmcp/aggregator/priority_resolver.go:86-98). That re-advertises a tool under a name outside existing policies, so a name-scoped forbid(... Tool::"deploy") that previously denied the tool no longer applies to github_deploy / prod_deploy. This is the exact reason the architecture requires priority losers to be dropped rather than aliased (docs/arch/10-virtual-mcp-architecture.md:177-180). With name-only Cedar authorization, retaining availability cannot override the fail-closed policy invariant. Drop all candidates in an unrankable mixed collision and log it at error level; add coverage combining a broad permit with a bare-name forbid.
Blocking — generated prefix names can collide and redirect a permitted call
addPrefixedCandidates writes directly into resolved (pkg/vmcp/aggregator/priority_resolver.go:143-160) without globally checking advertised-name collisions. For example, a listed github/deploy, unlisted prod/deploy, and an evil/github_deploy tool can all claim github_deploy; random map iteration determines the surviving route. A Cedar permit for Tool::"github_deploy" can therefore execute another backend's tool. Resolve/validate all proposed names before inserting routes, and drop every claimant to an ambiguous output name (the established prompts behavior at docs/arch/10-virtual-mcp-architecture.md:182-193); test the three-backend routing and authorization case.
The fallback also changes published tool names but does not update the operator-facing priority documentation or give an actionable warning; removing the unsafe fallback resolves that compatibility/documentation concern. Standards, reuse, and duplication checks found no other merge blockers.
|
Addressed the two blocking review findings in What changed:
Local verification: export PATH=/usr/local/go/bin:/root/go/bin:$PATH
git diff --check origin/main..HEAD
# passed
go test -run 'TestPriorityConflictResolver' ./pkg/vmcp/aggregator
# ok github.com/stacklok/toolhive/pkg/vmcp/aggregator 0.060s
go test -race ./pkg/vmcp/aggregator -run TestPriorityConflictResolver -count=1
# ok github.com/stacklok/toolhive/pkg/vmcp/aggregator 1.186s
go test -race ./pkg/vmcp/aggregator -count=1
# ok github.com/stacklok/toolhive/pkg/vmcp/aggregator 1.250s
golangci-lint run --allow-parallel-runners ./pkg/vmcp/aggregator
# 0 issues.Repo-level checks attempted: task lint
# failed in pre-existing/out-of-scope gci formatting issues:
# ../../work/toolhive/pkg/authserver/server/provider.go:391:1: File is not properly formatted (gci)
# pkg/authserver/server_impl.go:194:1: File is not properly formatted (gci)
task test
# failed before package results due gotestfmt panic:
# panic: BUG: Empty package name encountered.Those repo-level failures are outside |
|
CI/status follow-up for the review-fix head
No new code changes in this follow-up; this is just the evidence snapshot after CI completed on the pushed blocker fix. |
Signed-off-by: Emre K <110906681+kocaemre@users.noreply.github.com>
Signed-off-by: Emre K <110906681+kocaemre@users.noreply.github.com>
4389cc3 to
834d834
Compare
|
Refreshed this PR branch onto current No code changes beyond replaying the existing two PR commits on top of current upstream; DCO sign-offs are preserved. Local verification after the refresh: git diff --check upstream/main..HEAD
# passed
git log --format='%h %s%n%b' upstream/main..HEAD
# 834d8346 Drop unrankable priority conflicts
# Signed-off-by: Emre K <110906681+kocaemre@users.noreply.github.com>
# ee4954a2 Avoid priority annexing unlisted tools
# Signed-off-by: Emre K <110906681+kocaemre@users.noreply.github.com>
PATH=/usr/local/go/bin:/root/go/bin:$PATH go test -run 'TestPriorityConflictResolver' ./pkg/vmcp/aggregator
# ok github.com/stacklok/toolhive/pkg/vmcp/aggregator 0.055s
PATH=/usr/local/go/bin:/root/go/bin:$PATH go test -race ./pkg/vmcp/aggregator -run TestPriorityConflictResolver -count=1
# ok github.com/stacklok/toolhive/pkg/vmcp/aggregator 1.272sI also attempted the repo-level Taskfile lint after the rebase, but this cron runner's installed PATH=/usr/local/go/bin:/root/go/bin:$PATH task lint
# Error: can't load config: the Go language version (go1.26) used to build golangci-lint is lower than the targeted Go version (1.27.0)
# task: Failed to run task "lint": exit status 3GitHub CI has been retriggered on the refreshed head and was queued at the time of this comment. |
|
Current refreshed head Status snapshot from this run:
I also tried to re-run So the remaining blocker is only the stale |
|
Hey @kocaemre, sorry for taking so long to get back to this! You followed up on my review and I left you waiting. Thanks for working through the changes and keeping the branch up to date. I'm taking another look now. |
JAORMX
left a comment
There was a problem hiding this comment.
Thanks for working through this! Re-reviewed at 834d8346. Both blockers from my previous review are addressed: unrankable collisions now drop every candidate, and removing the prefix fallback also removes the generated-name overwrite path. The dropped tools stay out of both advertising and routing.
All 42 current checks are green. I reviewed the code and routing flow; I didn't rerun tests locally.
The remaining items are nonblocking: the PR description still describes prefix fallback, and the forbid-bypass test repeats the mixed-conflict case rather than adding coverage. We'll correct the description and follow up with a distinct alias-shaped-name regression and clearer priority documentation.
Summary
priorityOrder. Because Cedar tool authorization is name-only, this could redirect an existing permit to a different backend's tool.priorityOrder, and log the dropped conflict at error level. Do not generate prefix aliases: renaming could bypass existing name-scoped forbids or collide with another tool's real name.Fixes #6097
Type of change
Test plan
task test) — GitHub CI on834d8346task test-e2e) — GitHub CI core/operator matrices and MCP conformance on834d8346task lint-fix)The author reported focused aggregator tests, race tests, and
git diff --checkpassing after the rework/rebase; see the verification comments for exact commands and local Taskfile/toolchain limitations. All 42 GitHub checks passed on834d8346, including Go tests and lint. Re-review traced the resolver results through aggregation and routing; no additional local test run was performed for that review.Does this introduce a user-facing change?
Yes. With the priority strategy, a tool-name collision involving any backend absent from
priorityOrdernow removes every candidate from advertising and routing, with an error-level diagnostic. This affects both listed and unlisted participants. There is no prefix fallback. Conflict-free tools and fully listed priority conflicts retain their existing behavior.Special notes for reviewers
The original prefix-based approach was removed in response to review to preserve name-scoped authorization. Removing generated aliases also removes the reported cross-name overwrite path; no new collision-resolution machinery is needed for that path.
The resolver tests assert absence of bare and generated-prefix names; they do not evaluate Cedar policies. Follow-up cleanup will replace the duplicated mixed-conflict test with a distinct real alias-shaped-name regression and clarify the priority documentation.