- The tools you hand the model. An agent can call what its tools call and nothing else. A projection narrows a tool further, down to the fields it may set. This is where you decide.
- What the credential is granted. The provider checks it on every call, whatever the tool asks for. It is the backstop for a tool, or a prompt, that asks for more than you meant.
Ask for exactly what the tools need
permissions_scopes_for.py
AnyOf, and
scopes_for picks the fewest, narrowest set that
covers every tool. Gmail’s readers accept gmail.readonly or gmail.modify, the
relabeller needs gmail.modify, so an agent that does both asks for
gmail.modify alone. Which scope covers which is never declared: it is read off
the tools’ own lists, which are the provider’s.
A tool that declares no scopes needs none, and scopes_for over it is []. A
tool you write for your own API works the same whether or not you declare any.
Kinds of credential
Some providers issue more than one kind of credential, and the names a tool needs differ by kind. A GitHub OAuth app asks forrepo on the consent screen; a
GitHub App is configured with issues=write where it is created. Each pack
declares its kinds as constants, every tool says what it needs under each, and
the question names one:
permissions_kinds.py
A kind’s
granted says where its names go. "consent" names travel in the
authorization request, so scopes_for is the list to pass to
authorize(). "configured" names are
ticked where the app or key is made, so scopes_for is the checklist for that
screen. A kind can also be named by its string, kind="app", and a misspelling
is refused with the kinds that exist; importing the constant cannot be misspelt.
A tool that cannot run under a kind says so, with
Unavailable, and scopes_for refuses rather
than build a grant that cannot run it:
A grant you already hold
missing_scopes says what to add before
handing out a new tool, so the user is asked once, for the difference:
permissions_missing.py
When the provider refuses anyway
A grant that does not cover a call raisesInsufficientScope, whichever provider
said it and however it said it. needed is what to add, narrowest first; the
token is kept, because a refresh returns the same grant.
permissions_refused.py
accepted holds them all
and needed is the narrowest of them the tool lists. Where it names none,
accepted is empty and needed is the tool’s own declaration.
GitHub answers a private repository the token cannot see with the same 404 as one
that does not exist. A classic token’s 404 counts as a refusal only when the token
holds none of the scopes GitHub accepts for the call; otherwise it is GitHub’s own
404.
Your own API
Declare scopes on your tools the way the packs do, and they behave the same:permissions_own_api.py
WWW-Authenticate: Bearer error="insufficient_scope" challenge needs no
ScopeRefusal: Charter reads that header on
every response. For an API with more than one kind of credential, declare
Kind constants and pass scopes={KIND: [...], ...}.
Narrowing a tool you hand out
tool.derived(scopes=[...]) makes a copy that asks for less, for an agent you
know reaches less. A Drive agent that only ever opens files your app created can
ask for drive.file; a GitHub agent that only works on public repositories can
ask for public_repo:
permissions_narrow.py
What a declaration cannot say
- Scopes that reach part of what a tool can name are not offered.
drive.file,public_repo,gmail.metadatafor a searcher, Shopify’s fulfillment orders at other kinds of location, thepull_requestspermission on a GitHub issue endpoint: each would pass the consent screen and then fail on part of the input. Narrow withderived(scopes=...)when you know the part. - What depends on the arguments is not mandated. GitHub wants
workflowfor a write that touches.github/workflows. A Firecrawl key can be restricted to output formats, which is a property of each call. A Granola key with onlypersonalaccess lists only personal notes, with no error for the rest. - Some access is approved, not consented to. Shopify approves
read_all_orders(orders older than 60 days) and protected customer data per public app, in the Partner Dashboard; until it does, older orders are simply absent, and an unapproved customer field isnullbeside an error the pack raises. Google’s restricted scopes need a security assessment before a published app can request them. Neither appears on a consent screen. See Shopify and Google. - Some APIs have no permissions at all. Tavily documents none for a key: every endpoint takes it, and the optional project id only groups usage.
- Microsoft’s application permissions are a different shape. An app-only
token cannot call
/me, which is every Outlook route, so the pack declares delegated permissions only.