You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Distinguish tool discovery from tool invocation in authorization and audit #4048
The current authorization model uses the tool:call verb for both the ingress check
on tool listing requests (tools/list) and for egress response filtering of the tool
list. This conflates two semantically distinct operations and creates two practical
problems:
Policy granularity: There is no way to write a Cedar policy that allows a
client to discover which tools are available without also granting permission to invoke them. This prevents least-privilege policy designs.
Audit ambiguity: An audit trail showing tool:call events cannot distinguish
between a user who listed tools (a read-only, side-effect-free operation) and one who
actually invoked a tool (a potentially mutable action). This reduces the forensic
value of logs.
Proposed Change
Introduce a clear semantic split:
Ingress: Add a new tool:list verb (mapped to MCPOperationList on MCPFeatureTool) so that incoming tools/list requests are authorized independently
of tool:call.
Egress: Change the per-tool filtering of tools/list responses from tool:call
to tool:read (mapped to MCPOperationRead on MCPFeatureTool), reflecting that
determining visibility of a tool is a read-only discovery check, not an invocation.
The resulting verb taxonomy becomes:
Verb
Meaning
tool:list
Client is requesting the list of available tools
tool:read
A specific tool is visible/discoverable to the client
tool:call
Client is invoking a tool (may have side effects)
Impact
Breaking change for users with existing Cedar policies. Any policy that previously
used tool:call to control access to tools/list will need to be updated to use tool:list (for the list request itself) and/or tool:read (for per-tool visibility
in the response).
Hi @ghaskins interesting issue! Thank you for opening it. I'd like to make sure I understand the motivations better.
Can you explain why you'd want to give a user ability to list / read a tool but not call the tool? Are you imagining the client performs some step-up [1] when they discover they don't have permission to invoke the tool? Also, can you say more about the need for bothtool:list and tool:read? If a user doesn't have tool:list authorization, then would they be able to discover any tools?
Examples would help paint a picture for why this change is necessary.
Breaking change for users with existing Cedar policies. Any policy that previously
used tool:call to control access to tools/list will need to be updated to use
tool:list (for the list request itself) and/or tool:read (for per-tool visibility
in the response).
Can we find a way to implement this that is not a breaking change?
For example, in the absence of tool:read approvals, if a user does not have tool:call authorization on a given tool, then they will not be able to discover the tool. This is the same the behavior we expect today, so it is not a breaking change.
@jerm-dro Ty for taking the time to look. Answers below:
Can you explain why you'd want to give a user ability to list / read a tool but not call the tool?
The primary reason is not related to the permission as much as it is about audit. If someone were to review the audit log as seen by the authorizer: without this change there is no distinction between someone running a discovery operation vs someone trying to perform a tool:call. From an adverse event detection perspective, the former is more inert and typically innocent, whereas the latter might not be. Separating them gives a stronger signal.
That said, its generally also better to block requests early from a resource efficiency standpoint: I.e. if the user is not allowed to see any tools, it would be better to block them at tools:list than N times tool:read.
So, the separation from tool:call to tool:list/tool:read/tool:call delineates intent in a meaningful way in the audit trail and provides more efficient ingress filtering.
It is NOT about having a separate control for tool:read vs tool:call. I would generally expect them to track 1:1
if a user does not have tool:call authorization on a given tool, then they will not be able to discover the tool.
As you point out, if an authorizer has a default-deny posture, then a user that is prohibited from making tool:call will probably naturally be prohibited from performing tool:list/tool:read by virtue of omission.
The breaking change i was referring to I think would be on the other side: If a user is permissioned to invoke tool:call, they may find themselves suddenly unable to run tool:list on account of the new verbs. This is the breaking change I was referring to. I dont know how to make this so its non-breaking, since this would be a function of deployed Cedar policies that I dont have control over. If you have ideas regarding this, I am open to them.
The breaking change i was referring to I think would be on the other side: If a user is permissioned to invoke tool:call, they may find themselves suddenly unable to run tool:list on account of the new verbs. This is the breaking change I was referring to. I dont know how to make this so its non-breaking, since this would be a function of deployed Cedar policies that I dont have control over. If you have ideas regarding this, I am open to them.
Thanks for explaining @ghaskins. Thinking about how we could make this non-breaking: what if tool:call permission implied tool:read permission? Then:
Existing policies which permit tool:call will continue to function without modification.
Existing policies which forbid tool:call for some user either explicitly or with the default-deny behavior will not get the implied tool:read permission.
On another note, as someone who would configure these policies, I would find it cumbersome to explicitly grant both tool:read and tool:call permissions. The above also solves that problem.
Slightly unrelated, do we really need the tool:list verb within the authorization system? As an audit event, I'm fine with it, but it feels redundant with the auth verb tool:read. Two options:
Similar to the point above, could the approval for tool:list be inferred from the other policies? If a user has authorization to tool:read then they gain permission to tool:list.
Alternatively, all authenticated users may tool:list, but they won't see any tools if they don't have any tool:read permissions. In this event, we'd see tool:list for the user, but no corresponding tool:read events.
What do you think?
Summarizing my thoughts: it's important we find an implementation that doesn't modify the behavior of pre-existing policies. I believe an implicit relationship between tool:(call|read|list) would solve this, but I'm not too opinionated on the solution as long as it doesn't break existing policies.
I think @jerm-dro 's suggestion makes sense because it seems to me like it's implementable without major headaches. If we make tool:call imply tool:read (as an invariant to a choice by the policy author) then existing policies that only use tool:call continue to work while you can now write new policies that allow you to say that a tool is visible but not callable.
I think this makes sense even outside the argument of backwards compatiblity, because if you can call a tool, I don't see a point of not being able to see the tool.
On the implementation side (I just wanted to go through the exercise of planning a possible implementation with Claude's help to see if it's viable), it seems like an application level or is better for backwards compatibility. Cedar supports action hierarchy, so we could have written call_tool memberOf read_tool but that would only evaluate for policies that use the in operator and wouldn't have an effect for policies that just specify action == Action::call_tool.
Would it also work to make the tool:list operation an audit-only event as well?
@ghaskins the separation of tool:list vs tool:read vs tool:call is the right call. The audit trail distinction between discovery and invocation is exactly the kind of signal that matters when you're doing forensics on an agent incident.
One pattern we've found useful: making the authorization decision itself an auditable artifact, not just the tool call outcome. When Cedar evaluates a policy and returns permit/deny, that decision plus the exact policy ID, the principal, and the action should be signed and stored as a receipt. That way if someone questions "why was this agent allowed to call tool X at time T?", you can replay the exact authorization state - not reconstruct it from logs.
The tool:call implying tool:read approach @jerm-dro suggested is clean for backwards compatibility. Worth noting that the inverse should be strict - tool:read should never imply tool:call. An agent that can see a tool but not invoke it is a useful security posture for monitoring/audit-only roles.
I am looking at this from the perspective of the HTTP-PDP, though I think the same challenge exists in the cedar provider: That is, I do not see how you implement the proposed idea without changing the authorizer.
Thinking about implementation... yes, the authorizer itself needs to change to support the implication semantics.
One approach: a decorator around the Authorizer interface that handles "call implies discover" centrally. The middleware sends the semantically correct operation, and if the discover check is denied, the decorator falls back to evaluating the policy again with the tool:call verb. The implication logic lives in one place and we can avoid rewriting the existing implementation.
The discover/invoke split looks right. I would also keep the audit record shape different for the two operations.
For discovery, the record can say: principal X was allowed to see that tool Y exists under policy P.
For invocation, the record needs enough binding to explain the side effect that was authorized:
principal / workload identity
tool name and server identity
operation class: discovery vs invocation
policy id/version evaluated
canonical args digest for invocation
decision: allow / deny / approval_required
decision time and expiry
invocation result ref or error ref when execution happens
That keeps "call implies discover" as a compatibility rule without losing the stronger invariant: a discovery grant is not itself an execution grant, and an execution audit event should be replayable against the exact policy and arguments that were authorized.
Picking this thread back up, @ghaskins. Reading it back, the design questions were answered and nobody said so, which left your PR closed with no signal to restart. Sorry about that.
Here is where we think the discussion landed, so you can tell us if you read it differently:
No new ingress verb. @JAORMX's point stands: blocking tools/list at ingress produces a failure mode where auth passes and tools silently vanish, which is worse than the granularity is worth.
Egress per-tool filtering moves from tool:call to tool:read, which is the part your original proposal was really after.
tool:call implies tool:read, so existing policies that permit tool:call keep working and the change is not breaking. @jerm-dro's decorator around the Authorizer interface is the shape we prefer: the middleware sends the semantically correct operation, and the decorator handles the implication in one place rather than each authorizer reimplementing it. That also answers your question from the PR about how the egress verb can change without forcing policy rewrites.
The implication is strictly one-way. tool:read never implies tool:call, as @jagmarques noted.
If that matches your understanding, we would welcome a fresh PR along those lines and will review it properly this time. Please ask for the issue to be assigned first, per the contributor guidelines.
@jagmarques and @rpelevin, thank you both for the audit input. Signed, replayable authorization receipts and a distinct record shape for discovery versus invocation are a bigger change than this issue covers, so please open a separate issue for that and we will treat it on its own merits rather than letting it ride on this one.
Thanks for picking this back up, @ChrisJBurns. This matches the direction I suggested on the original PR.
Reading the discussion again, I think we should make the two goals explicit: distinguishing discovery from invocation in the audit log, and allowing a user to discover a tool they cannot call. Audit clarity alone wouldn’t require a separate permission. The second goal is what makes tool:read useful.
I mentioned step-up auth as a possible use case in my earlier comment. I’d still appreciate a concrete example of how that would work for a client, but I’m comfortable supporting the capability without making that a blocker.
So, my position hasn’t changed: no new ingress gate, tool:call continues to grant visibility, and tool:read never grants invocation. The decorator approach sounds reasonable.
One detail to keep in mind for the implementation: when discovery falls back to a tool:call check, the audit trail still needs to identify it as discovery. Otherwise we’d preserve compatibility but keep the ambiguity that prompted this issue in the first place.
@ChrisJBurns@JAORMX I'd like to take this and open a fresh PR along the lines of the summary in #4048 (comment). Could you assign the issue to me first, per the contributor guidelines?
The PR would:
Make tool:call imply tool:read, one way only, through a decorator around the Authorizer interface.
Check tool:read for per-tool filtering on egress.
Mark any discovery decision that falls back to a tool:call check as discovery in the audit log, as JAORMX asked.
Add tests: tool:call still allows listing, tool:read alone doesn't allow invocation, tool:read doesn't imply tool:call, and a fallback decision is logged as discovery.
It won't add a new ingress verb or signed, replayable audit receipts. Per ChrisJBurns's request, I'll open a separate issue for those after you assign this one.
@ChrisJBurns@JAORMX@jerm-dro The PR for this is up: #6903. It implements the design from this thread: tool:call implies tool:read, one way, through a decorator, with discovery fallbacks marked in the audit trail. CI hasn't started on it, since it comes from my fork (rkb32/toolhive). Could one of you approve the workflow run when you have a moment? Thanks.
Activity
jerm-dro commentedon Mar 17, 2026
Hi @ghaskins interesting issue! Thank you for opening it. I'd like to make sure I understand the motivations better.
Can you explain why you'd want to give a user ability to list / read a tool but not call the tool? Are you imagining the client performs some step-up [1] when they discover they don't have permission to invoke the tool? Also, can you say more about the need for both
tool:listandtool:read? If a user doesn't havetool:listauthorization, then would they be able to discover any tools?Examples would help paint a picture for why this change is necessary.
Can we find a way to implement this that is not a breaking change?
For example, in the absence of
tool:readapprovals, if a user does not havetool:callauthorization on a given tool, then they will not be able to discover the tool. This is the same the behavior we expect today, so it is not a breaking change.[1] https://modelcontextprotocol.io/specification/draft/basic/authorization#step-up-authorization-flow
ghaskins commentedon Mar 17, 2026
@jerm-dro Ty for taking the time to look. Answers below:
The primary reason is not related to the permission as much as it is about audit. If someone were to review the audit log as seen by the authorizer: without this change there is no distinction between someone running a discovery operation vs someone trying to perform a tool:call. From an adverse event detection perspective, the former is more inert and typically innocent, whereas the latter might not be. Separating them gives a stronger signal.
That said, its generally also better to block requests early from a resource efficiency standpoint: I.e. if the user is not allowed to see any tools, it would be better to block them at tools:list than N times tool:read.
So, the separation from tool:call to tool:list/tool:read/tool:call delineates intent in a meaningful way in the audit trail and provides more efficient ingress filtering.
It is NOT about having a separate control for tool:read vs tool:call. I would generally expect them to track 1:1
As you point out, if an authorizer has a default-deny posture, then a user that is prohibited from making tool:call will probably naturally be prohibited from performing tool:list/tool:read by virtue of omission.
The breaking change i was referring to I think would be on the other side: If a user is permissioned to invoke tool:call, they may find themselves suddenly unable to run tool:list on account of the new verbs. This is the breaking change I was referring to. I dont know how to make this so its non-breaking, since this would be a function of deployed Cedar policies that I dont have control over. If you have ideas regarding this, I am open to them.
jerm-dro commentedon Mar 17, 2026
Thanks for explaining @ghaskins. Thinking about how we could make this non-breaking: what if
tool:callpermission impliedtool:readpermission? Then:tool:callwill continue to function without modification.tool:callfor some user either explicitly or with the default-deny behavior will not get the impliedtool:readpermission.On another note, as someone who would configure these policies, I would find it cumbersome to explicitly grant both
tool:readandtool:callpermissions. The above also solves that problem.Slightly unrelated, do we really need the
tool:listverb within the authorization system? As an audit event, I'm fine with it, but it feels redundant with the auth verbtool:read. Two options:tool:listbe inferred from the other policies? If a user has authorization totool:readthen they gain permission totool:list.tool:list, but they won't see any tools if they don't have anytool:readpermissions. In this event, we'd seetool:listfor the user, but no correspondingtool:readevents.What do you think?
Summarizing my thoughts: it's important we find an implementation that doesn't modify the behavior of pre-existing policies. I believe an implicit relationship between
tool:(call|read|list)would solve this, but I'm not too opinionated on the solution as long as it doesn't break existing policies.jhrozek commentedon Mar 18, 2026
I think @jerm-dro 's suggestion makes sense because it seems to me like it's implementable without major headaches. If we make
tool:callimplytool:read(as an invariant to a choice by the policy author) then existing policies that only usetool:callcontinue to work while you can now write new policies that allow you to say that a tool is visible but not callable.I think this makes sense even outside the argument of backwards compatiblity, because if you can call a tool, I don't see a point of not being able to see the tool.
On the implementation side (I just wanted to go through the exercise of planning a possible implementation with Claude's help to see if it's viable), it seems like an application level
oris better for backwards compatibility. Cedar supports action hierarchy, so we could have writtencall_tool memberOf read_toolbut that would only evaluate for policies that use theinoperator and wouldn't have an effect for policies that just specifyaction == Action::call_tool.Would it also work to make the
tool:listoperation an audit-only event as well?jagmarques commentedon Apr 6, 2026
@ghaskins the separation of tool:list vs tool:read vs tool:call is the right call. The audit trail distinction between discovery and invocation is exactly the kind of signal that matters when you're doing forensics on an agent incident.
One pattern we've found useful: making the authorization decision itself an auditable artifact, not just the tool call outcome. When Cedar evaluates a policy and returns permit/deny, that decision plus the exact policy ID, the principal, and the action should be signed and stored as a receipt. That way if someone questions "why was this agent allowed to call tool X at time T?", you can replay the exact authorization state - not reconstruct it from logs.
The tool:call implying tool:read approach @jerm-dro suggested is clean for backwards compatibility. Worth noting that the inverse should be strict - tool:read should never imply tool:call. An agent that can see a tool but not invoke it is a useful security posture for monitoring/audit-only roles.
ghaskins commentedon Apr 7, 2026
FWIW, Here is a presentation that includes #4048 and #4112 in action
https://www.youtube.com/live/RQ0ghqqgYTA?si=jJxbig3CI13_HfWT&t=8187
I am looking at this from the perspective of the HTTP-PDP, though I think the same challenge exists in the cedar provider: That is, I do not see how you implement the proposed idea without changing the authorizer.
jerm-dro commentedon Apr 13, 2026
Thanks for sharing @ghaskins!
Thinking about implementation... yes, the authorizer itself needs to change to support the implication semantics.
One approach: a decorator around the
Authorizerinterface that handles "call implies discover" centrally. The middleware sends the semantically correct operation, and if the discover check is denied, the decorator falls back to evaluating the policy again with thetool:callverb. The implication logic lives in one place and we can avoid rewriting the existing implementation.rpelevin commentedon Jun 6, 2026
The discover/invoke split looks right. I would also keep the audit record shape different for the two operations.
For discovery, the record can say: principal X was allowed to see that tool Y exists under policy P.
For invocation, the record needs enough binding to explain the side effect that was authorized:
That keeps "call implies discover" as a compatibility rule without losing the stronger invariant: a discovery grant is not itself an execution grant, and an execution audit event should be replayable against the exact policy and arguments that were authorized.
ChrisJBurns commentedon Jul 30, 2026
Picking this thread back up, @ghaskins. Reading it back, the design questions were answered and nobody said so, which left your PR closed with no signal to restart. Sorry about that.
Here is where we think the discussion landed, so you can tell us if you read it differently:
tools/listat ingress produces a failure mode where auth passes and tools silently vanish, which is worse than the granularity is worth.tool:calltotool:read, which is the part your original proposal was really after.tool:callimpliestool:read, so existing policies that permittool:callkeep working and the change is not breaking. @jerm-dro's decorator around theAuthorizerinterface is the shape we prefer: the middleware sends the semantically correct operation, and the decorator handles the implication in one place rather than each authorizer reimplementing it. That also answers your question from the PR about how the egress verb can change without forcing policy rewrites.tool:readnever impliestool:call, as @jagmarques noted.If that matches your understanding, we would welcome a fresh PR along those lines and will review it properly this time. Please ask for the issue to be assigned first, per the contributor guidelines.
@jagmarques and @rpelevin, thank you both for the audit input. Signed, replayable authorization receipts and a distinct record shape for discovery versus invocation are a bigger change than this issue covers, so please open a separate issue for that and we will treat it on its own merits rather than letting it ride on this one.
JAORMX commentedon Oct 12, 2026
Thanks for picking this back up, @ChrisJBurns. This matches the direction I suggested on the original PR.
Reading the discussion again, I think we should make the two goals explicit: distinguishing discovery from invocation in the audit log, and allowing a user to discover a tool they cannot call. Audit clarity alone wouldn’t require a separate permission. The second goal is what makes
tool:readuseful.I mentioned step-up auth as a possible use case in my earlier comment. I’d still appreciate a concrete example of how that would work for a client, but I’m comfortable supporting the capability without making that a blocker.
So, my position hasn’t changed: no new ingress gate,
tool:callcontinues to grant visibility, andtool:readnever grants invocation. The decorator approach sounds reasonable.One detail to keep in mind for the implementation: when discovery falls back to a
tool:callcheck, the audit trail still needs to identify it as discovery. Otherwise we’d preserve compatibility but keep the ambiguity that prompted this issue in the first place.rkb32 commentedon Oct 19, 2026
@ChrisJBurns @JAORMX I'd like to take this and open a fresh PR along the lines of the summary in #4048 (comment). Could you assign the issue to me first, per the contributor guidelines?
The PR would:
tool:callimplytool:read, one way only, through a decorator around theAuthorizerinterface.tool:readfor per-tool filtering on egress.tool:callcheck as discovery in the audit log, as JAORMX asked.tool:callstill allows listing,tool:readalone doesn't allow invocation,tool:readdoesn't implytool:call, and a fallback decision is logged as discovery.It won't add a new ingress verb or signed, replayable audit receipts. Per ChrisJBurns's request, I'll open a separate issue for those after you assign this one.
rkb32 commentedon Oct 21, 2026
@ChrisJBurns @JAORMX @jerm-dro The PR for this is up: #6903. It implements the design from this thread:
tool:callimpliestool:read, one way, through a decorator, with discovery fallbacks marked in the audit trail. CI hasn't started on it, since it comes from my fork (rkb32/toolhive). Could one of you approve the workflow run when you have a moment? Thanks.Make tool:call imply tool:read for tools/list filtering