Elements

Elements · Tool use

Permission grant

Granting a capability rather than approving one action, with the reach spelled out.

Filesystem accessrequested by filesystem-mcp
this grantsRead and write files under the workspaceRun commands you have already approvedNo network access
fig. 01

Installation

npx shadcn@latest add "@assistant-ui/elements-permission-grant"
First time? Set up a runtime

Runtime components read their state from an assistant-ui runtime. Add one to an existing project:

npx assistant-ui@latest init

Then wrap your app in a runtime provider:

import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";

export default function App() {
  const runtime = useChatRuntime({
    transport: new AssistantChatTransport({ api: "/api/chat" }),
  });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      {/* your components */}
    </AssistantRuntimeProvider>
  );
}

The installation guide covers new projects, templates, and API routes.

A gate for a capability, not a single action: what's being asked for, who's asking, and exactly what saying yes would let happen, before the user commits. With a runtime this gate comes from the tool call's approval state; standalone you hold the decision yourself.

Getting started

A runtime whose backend declares tool approval gates (an AG-UI or ACP agent, for example) attaches an approval object to the tool-call part. Read it and answer it from the same renderer that shows the tool's normal result.

Read the approval gate from the tool call

app/shell-toolkit.tsx
"use client";

import type { ToolApprovalResponse, ToolCallMessagePartProps } from "@assistant-ui/react";

function ShellApprovalGate({ args, approval, respondToApproval }: ToolCallMessagePartProps<{ command: string }, string>) {
  if (!approval || approval.approved !== undefined || approval.resolution !== undefined) {
    return null;
  }

  // A refused response rejects and a precondition (an unknown option, an answer
  // the request does not take) throws, so `try`/`await` covers both and the
  // controls stay actionable.
  const answer = async (response: ToolApprovalResponse) => {
    try {
      await respondToApproval(response);
    } catch (failure) {
      console.error(failure);
    }
  };

  return (
    <div className="flex flex-col gap-3.5 rounded-[20px] border p-4">
      <p className="text-sm font-medium">Run: {args.command}</p>
      <div className="flex flex-wrap gap-2">
        {(approval.options ?? []).map((option) => (
          <button key={option.id} type="button" onClick={() => void answer({ optionId: option.id })}>
            {option.label ?? option.kind}
          </button>
        ))}
      </div>
    </div>
  );
}

approval.approved === undefined and no resolution marks the gate as still open; once the host records a decision, the part re-renders with approval.approved set and this returns null.

Answer with respondToApproval

Each declared option carries its own grants, the patterns or rules choosing it would persist. Show them before the user commits, not after:

{(approval.options ?? []).map((option) => (
  <div key={option.id} className="flex flex-col gap-1">
    <button type="button" onClick={() => void answer({ optionId: option.id })}>
      {option.label ?? option.kind}
    </button>
    {option.grants?.map((grant) => <span key={grant}>{grant}</span>)}
  </div>
))}

Anatomy

<div data-slot="permission-grant">
  <div>{/* icon, capability, "requested by {requester}" */}</div>
  <div>{/* "this grants", then one line per reach item */}</div>
  <div>
    {/* pending: Deny / This session / Always */}
    {/* resolved: one badge, keyed on scope so it fades in on change */}
  </div>
</div>

The three pending buttons and the resolved badge are mutually exclusive: once scope is anything but "pending", the buttons are gone and the card reads either "denied" or "granted · {scope}".

Examples

Confirm a persistent grant

An option with confirm: true (or a { title, description } object) is a two-step commit: show its own confirmation before calling respondToApproval, matching what option.grants already promised.

const [confirmingId, setConfirmingId] = useState<string | null>(null);
const confirming = approval.options?.find((o) => o.id === confirmingId);

if (confirming) {
  return (
    <div>
      <p>{typeof confirming.confirm === "object" ? confirming.confirm.title : `${confirming.label}?`}</p>
      <button onClick={() => void answer({ optionId: confirming.id })}>Confirm</button>
      <button onClick={() => setConfirmingId(null)}>Back</button>
    </div>
  );
}

Plain allow or deny

approval.options is optional. When it's absent, the gate is a plain yes or no: respond with approved instead of an optionId.

<button type="button" onClick={() => void answer({ approved: true })}>
  Allow
</button>
<button type="button" onClick={() => void answer({ approved: false })}>
  Deny
</button>

Restyle the card

Both lanes take className on the root. The root uses the shared paper surface, the "this grants" label uses mono, the resolved badge uses field and mono together, and the Always button uses inkButton, so retargeting those in surfaces.tsx restyles this card along with everything else built on them.

API reference

Approval gate

FieldTypeDescription
approval.idstringIdentifier for this approval request.
approval.approvedboolean | undefinedundefined while the gate is still open.
approval.optionsreadonly ToolApprovalOption[] | undefinedAvailable decisions; absent means a plain allow or deny.
approval.optionIdstring | undefinedThe option chosen at resolution, when options were present.
approval.resolution"cancelled" | "expired" | undefinedSet by the host when the request ended without a user decision.
respondToApproval(response)(response: ToolApprovalResponse) => Promise<void>Answers the gate. Accepts { approved }, { optionId }, both, or { text } when the request takes a free-form answer. Only valid while approved and resolution are both unset.

ToolApprovalOption

FieldTypeDescription
idstringHost-defined identifier.
kind"allow-once" | "allow-always" | "reject-once" | "reject-always" or a custom stringKnown kinds resolve approved automatically; a custom kind always needs an explicit approved value.
labelstringOptional; renderers default per kind when it's absent.
grantsreadonly string[]Patterns or rules this option would persist, supplied by the host.
confirmboolean | { title?, description? }Opt-in confirmation step before this option resolves.