# Scoped Grant

> Pattern 01 · Granting access · part of Agent Consent Patterns.
> Human page: https://agentconsent.dev/patterns/scoped-grant/

Make capability granularity legible at the moment of connection: which resources, and read versus write versus delete, each an individually reviewable permission, not one opaque "allow access".

## Problem

The consent screen inherited from OAuth answers the wrong question. "Inbox Assistant wants to access your Gmail. Allow?" collapses an enormous range of capability into a single toggle. Reading one message in one label and permanently deleting the entire inbox arrive as the same request, worded the same way, granted by the same click. For a human app the blast radius was bounded by a UI a person still had to drive. An agent has no such bound: whatever you grant, it can exercise at machine speed, unattended, across every message the scope touches. The screen that used to be a formality is now the main place a user can set the blast radius, and it still reads like a formality.

## Solution

Break the grant into capabilities the user can actually evaluate, and make two dimensions visible that the old screen hid:

- **Access level.** Every capability is tagged read, write, or delete as text and weight, never by color alone. The user sees at a glance whether they are authorizing observation or destruction.
- **Resource scope.** Capabilities are grouped by the resource they touch, with the narrowing stated ("Board 2026 label only," "Board Decks folder only"). Access is scoped to a resource, not granted against a whole account by default.

Each capability is an individual toggle. Read is the least-privilege default; write and delete are opt-in. Mandatory scopes are shown locked-on and labeled, never bundled in silently. The grant button commits to a count the user can see.

> **Live demo** — an interactive Scoped Grant demo runs on the page: https://agentconsent.dev/patterns/scoped-grant/

The demo is the `ScopedGrant` component from `@agentconsent/react`. Toggle capabilities to watch the grant count change; the read scopes are marked required and cannot be turned off.

## Anatomy

> **Anatomy** — a labeled breakdown of the Scoped Grant component's parts is shown on the interactive page: https://agentconsent.dev/patterns/scoped-grant/

## When to use it

- **Connecting an agent to a service** (email, calendar, files, a payments API, a repo) where the underlying API exposes more than one capability.
- **When the API distinguishes access levels.** If read, write, and delete are separately grantable, the consent screen should let the user grant them separately rather than flattening to all-or-nothing.
- **When scope can be narrowed to a resource:** a folder, a label, a project. Per-resource grants turn "access your Drive" into "access this one folder," shrinking the blast radius for free.

## When not to use it

- **Genuinely all-or-nothing integrations.** If the service exposes a single capability, a granular picker is theater. A Connection Card confirming the one thing is more honest.
- **Escalation in the middle of a task.** When the agent hits a wall and needs one more permission right now, don't send the user back to the full grant screen. That is Progressive Scope.
- **Per-action approval.** Scoped Grant sets standing capability; it is not where you approve a specific send or payment. That is Action Preview and the Irreversibility Gate.

## Real-world examples

- **Google's granular OAuth consent.** For sensitive scopes, Google's consent screen presents each requested permission as its own checkbox, labeled "Select what [app] can access," so a user can grant calendar read access while declining contacts. The single Allow button became a per-capability decision, which is exactly this pattern retrofitted onto OAuth.
- **GitHub App installation.** A GitHub App declares each permission with an access level (read-only vs. read-and-write), and the install screen forces a resource decision: **All repositories** or **Only select repositories**. Access level and resource scope are both legible before anything is granted.
- **Plaid account linking.** After the user authenticates with their bank, Plaid asks which specific accounts the requesting app may see. The connection is to the institution; the grant is per-account.

*Annotated screenshots of these flows are being collected. Products are credited, annotations follow the site-wide callout conventions, and any screenshot is removed on request. See [About](/about/).*

## Accessibility

- The grant is a `<form>` labeled by its title, so screen readers announce what is being connected on entry, and the whole thing submits with a real submit button.
- Each capability is a native `<input type="checkbox">` with an associated `<label>`; its plain-language description is wired with `aria-describedby`, so the level, the name, and the consequence are all announced together.
- Capabilities are clustered in `<fieldset>`/`<legend>` groups, so assistive tech announces the resource each cluster applies to before its scopes.
- **Access level is never carried by color alone.** The read/write/delete badge is text; the weighting (and a granted-state emphasis on write and delete) is layered on top of a label that already reads without it.
- **Required scopes are `disabled` and `checked`**, with a text "required" tag. The lock is announced, not just implied by a greyed box, and the mandatory permission is still fully described.
- The grant count lives in the button label, so a screen-reader user hears exactly how many permissions the button will grant before activating it.

## Anti-patterns

- **The all-or-nothing toggle.** "Allow access to your Gmail." The single scope that could mean anything is the pattern's reason to exist; a nicer-looking version of it is still it.
- **Access level by color only.** A red dot on the dangerous scope fails every user who can't see it and most who can't be bothered to decode it. Say "delete."
- **Silent bundling.** Slipping write or delete into a grant the user thinks is read-only, or hiding required scopes in a "…and 4 more", converts consent into a trick.
- **Whole-account default.** Requesting the entire mailbox when the task touches one label. Default to the narrowest resource that works and let the user widen, not the reverse.
- **Consent by pre-check.** Every optional scope checked by default, betting the user won't uncheck. Least privilege means write and delete start off.
- **Grant now, explain never.** A picker with no per-scope description makes the user authorize words they can't evaluate. If you can't say what a scope does, don't ask for it.
- **Escalation past the principal.** Offering (or granting) a scope the *user themselves* doesn't hold. An agent's access is the intersection of its own capabilities and the user's rights; the grant screen must never be a path to more than the user could reach on their own.

## Code

```tsx
import { ScopedGrant } from "@agentconsent/react";
import "@agentconsent/react/theme.css";

<ScopedGrant.Root
  requiredScopes={["gmail.read"]}         // always granted, shown locked
  defaultValue={["gmail.read"]}
  onGrant={(ids) => connect(ids)}
  onCancel={() => dismiss()}
>
  <ScopedGrant.Header>
    <ScopedGrant.Icon>✦</ScopedGrant.Icon>
    <ScopedGrant.Title>Connect Inbox Assistant</ScopedGrant.Title>
  </ScopedGrant.Header>

  <ScopedGrant.Description>
    Grant only what the task needs. You can change this later.
  </ScopedGrant.Description>

  <ScopedGrant.Group label="Gmail" resource="“Board 2026” label only">
    <ScopedGrant.Scope
      id="gmail.read"
      access="read"
      label="Read messages"
      description="See subjects and bodies of messages in the label."
    />
    <ScopedGrant.Scope
      id="gmail.send"
      access="write"
      label="Send replies on your behalf"
      description="Compose and send email as you, in reply to threads in the label."
    />
    <ScopedGrant.Scope
      id="gmail.delete"
      access="delete"
      label="Delete messages"
      description="Move messages in the label to trash."
    />
  </ScopedGrant.Group>

  <ScopedGrant.Actions>
    <ScopedGrant.Cancel>Don't connect</ScopedGrant.Cancel>
    <ScopedGrant.Grant>
      {({ count }) => `Connect · ${count} permissions`}
    </ScopedGrant.Grant>
  </ScopedGrant.Actions>
</ScopedGrant.Root>
```

Selection is controllable with `value` / `onValueChange`, or uncontrolled via `defaultValue`. Declare mandatory scopes once in `requiredScopes`. They are always in the `onGrant` payload and cannot be toggled off. All parts are unstyled primitives with `data-acp` attributes; skip `theme.css` and style them yourself, or override the `--acp-*` tokens to retheme.
