Skip to content

Distinguish tool discovery from tool invocation in authorization and audit #4048

Open

Description

@ghaskins

Summary

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:

  1. 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.

  2. 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).

Activity

  1. jerm-dro commented on Mar 17, 2026

    @jerm-dro
    Collaborator

    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.

    [1] https://modelcontextprotocol.io/specification/draft/basic/authorization#step-up-authorization-flow

  2. ghaskins commented on Mar 17, 2026

    @ghaskins
    ContributorAuthor

    @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.

  3. jerm-dro commented on Mar 17, 2026

    @jerm-dro
    Collaborator

    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:

    1. 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.
    2. 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.

  4. jhrozek commented on Mar 18, 2026

    @jhrozek
    Contributor

    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?

  5. jagmarques commented on Apr 6, 2026

    @jagmarques

    @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.

  6. ghaskins commented on Apr 7, 2026

    @ghaskins
    ContributorAuthor

    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.

  7. jerm-dro commented on Apr 13, 2026

    @jerm-dro
    Collaborator

    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 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.

  8. rpelevin commented on Jun 6, 2026

    @rpelevin

    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.

  9. ChrisJBurns commented on Jul 30, 2026

    @ChrisJBurns
    Collaborator

    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.

  10. JAORMX commented on Oct 12, 2026

    @JAORMX
    Collaborator

    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.

  11. rkb32 commented on Oct 19, 2026

    @rkb32

    @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.

  12. rkb32 commented on Oct 21, 2026

    @rkb32

    @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.

  13. added a commit that references this issue on Oct 23, 2026
    0e44ae5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

apiItems related to the APIauditauthenticationenhancementNew feature or requestgoPull requests that update go codekubernetesItems related to Kubernetes

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions