fix(agent): readonly scope must deny join_links resource#450
Merged
intendednull merged 1 commit intoApr 28, 2026
Merged
Conversation
TokenScope::allows_resource returned true for every URI, so ReadOnly and Messaging tokens could read willow://server/join-links and harvest link_id values. link_id is a single-step bearer credential for WireMessage::JoinRequest, so any client with a low-privilege token could join a server unattended (audit finding AUD-2). Per-scope match now: Full/Admin = all URIs, ReadOnly/Messaging = all except willow://server/join-links, Custom(set) = explicit allowlist (now also gates resources, not just tools — doc updated). Tests added at lowest tier (Rust unit + agent integration): - scopes::tests::readonly_scope_denies_join_links - scopes::tests::messaging_scope_denies_join_links - scopes::tests::full_and_admin_allow_join_links - scopes::tests::custom_resource_allowlist_gates_join_links - e2e::readonly_list_resources_omits_join_links - e2e::readonly_scope_rejects_join_links_read (replaces the prior stub-closure denied_uri_rejects_with_invalid_request — now drives the gate with a real WillowMcpServer<ReadOnly>) - readonly_token_hides_tools updated to expect join-links denied while every other URI stays visible. Tradeoff: option (a) from the issue tightens Custom(set) to also gate resources, not tools only. Runner-up was leaving Custom unchanged and only filtering join-links in ReadOnly/Messaging — rejected because the issue's preferred patch makes Custom an explicit allowlist for both surfaces, which is the safer default for least-privilege tokens. Refs #436
This was referenced Apr 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug
TokenScope::allows_resourcereturnedtruefor every URI. ReadOnly + Messaging tokens could readwillow://server/join-linksand pulllink_id.link_id= single-step bearer credential forWireMessage::JoinRequest. Audit finding AUD-2.Fix
Per-scope match in
crates/agent/src/scopes.rs:list_resourcesalready runs.filter(|r| self.scope.allows_resource(...))so denial cascades to listing for free.Doc on
TokenScopeupdated.Custom(set)now gates resources too (not tools only) — runner-up was leaving Custom resource-permissive, rejected because issue's preferred option (a) makes Custom symmetric for tools + resources, safer default for least-privilege tokens.Complexity gate: skipped brainstorming/spec — single-file logic + suggested patch in issue. Decision noted here.
Tests added
Rust unit (
crates/agent/src/scopes.rs):readonly_scope_denies_join_linksmessaging_scope_denies_join_linksfull_and_admin_allow_join_linkscustom_resource_allowlist_gates_join_linksAgent integration (
crates/agent/tests/e2e.rs):readonly_list_resources_omits_join_links— replicatesWillowMcpServer::list_resourcesfilter pipelinereadonly_scope_rejects_join_links_read— replaces prior stubdenied_uri_rejects_with_invalid_request, now drives gate with realWillowMcpServer<ReadOnly>readonly_token_hides_toolsupdated: every URI visible except join-linksLowest tier covering behaviour per CLAUDE.md test-tier guidance.
RequestContext<RoleServer>not externally constructible so we replicate the exact gate the handler runs (matches existingreadonly_scope_rejects_send_messagepattern).Verification
cargo fmt --checkcleancargo clippy --workspace --all-targets -- -D warningscleancargo test --workspaceall green (incl. 35 willow-agent unit + 33 e2e tests)cargo check --target wasm32-unknown-unknown -p ...clean (agent excluded — binary crate)Refs #436
Generated by Claude Code