A normal permission check asks whether a user can take an action on a resource. Once an agent is acting for that user, the check has more inputs:
can(delegator, agent, scope, action, resource, conditions)
The delegator is the human who handed off the work. The agent is its own principal. Scope is what the human actually agreed to, like reading invoices or triaging tickets. Conditions cover tenant, time, trust level, and whether this action needs a person to approve it.
Problems tend to show up in the space between what the user consented to and what the agent's credentials allow. Say someone connects an agent to their mailbox so it can schedule meetings, and the token behind it can also send mail to outside addresses. Nobody agreed to that, and nothing blocks it.
Some things that help:
- Give the agent short-lived tokens scoped to the delegation, and keep the real upstream credentials away from it. Then revoking the human's access or consent cuts the agent off on its next call.
- Check each tool call that has a side effect. A session that started as "summarize my invoices" can drift into "buy gift cards" after a prompt injection, and a check at session start won't catch it.
- Cap the agent below the human. An admin can still run an agent that is limited to read-only on one tenant.
- Keep a decision log next to your traces. A trace shows that a tool ran. The decision log records who delegated, which agent acted, which rule matched, and whether a required approval happened.
Disclosure: I work at Permit.io. This is a short version of a longer post from our team, which also covers making an agent identify itself before tools unlock and putting enforcement in front of MCP servers:
Top comments (2)
The
can(delegator, agent, scope, action, resource, conditions)framing is useful. Two things I've run into when testing this in practice:A cheap test for the per-call check: inject an instruction into a document the agent reads that asks for a side-effecting call outside the delegated scope, and confirm you get a deny and a log entry with the delegator. If you only get the deny, the audit trail is the next gap.
Intersection along the chain is the failure mode I keep hitting too. A hop that re-mints from its own service identity can widen the leaf call past what the user approved. On logging, I keep the consented scope string the user saw plus the policy version id, because "mail.send allowed" alone never answers whether it matched the grant. Your inject-into-document test is a clean deny check; asserting the decision record still names the original delegator covers the audit half.