Docs

Everything you need to put a consent screen in front of an AI agent's request.

This package is the human-approval step of KYA-OS, the protocol for agent identity and delegation. You can also use it on its own: the components only report what the person chose.

Install

npm install @kya-os/consent

Importing @kya-os/consent/bundle registers every consent-* element. If you'd rather not use a bundler, serve consent.min.js from the package as an ES module and load it with a <script type="module"> tag.

Your first screen

consent-capabilities-screen is presentational. It shows who is asking and what they want, and emits the person's selection. Your server remains responsible for enforcing the scopes.

import "@kya-os/consent/bundle";

const el = document.createElement("consent-capabilities-screen");
el.agentMetadata = {
  name: "Atlas",
  did: "did:web:atlas.northwind.dev",
  vendor: "Northwind Labs",
  verification: "domain",        // "verified" | "domain" | "unverified"
  domain: "atlas.northwind.dev", // named in the pill for "domain"
};
el.capabilities = [
  {
    id: "read-calendar",
    label: "See your calendar",
    description: "Read event titles and times.",
    icon: "calendar",
    riskLevel: "low",            // "low" | "medium" | "high" | "critical"
    defaultOn: true,
    scopes: ["calendar:read"],
  },
];
document.body.append(el);

A Cedar policy per capability is optional. When you supply cedar, the row gets a policy disclosure. Without it, the literal scopes are shown on each row.

Events

ElementEventDetail
consent-capabilities-screencapabilities-allow{ selectedCapabilityIds, selectedScopes }
consent-capabilities-screencapabilities-denynone
consent-permissionschange{ selected }
consent-capability-cardcapability-toggle{ id, checked }
consent-action-barconsent-allow, consent-denynone
mcp-consentmcp-consent:approve, :deny, :errorerror: { error, code? }
consent-passkeyconsent-passkey:complete, :error, :unsupportedsee Passkeys

The events bubble and cross shadow roots, so you can listen on any ancestor.

Theming

Set CSS custom properties on the element or any ancestor. There are two families. consent-capabilities-screen and its header and action bar read --cs-* tokens, such as --cs-surface-accent for the Allow button and --cs-surface-accent-text for the text on it. The primitives (button, checkbox, permissions, and so on) and the domain pill read --consent-primary. To re-brand the whole screen, set both.

VariableWhat it colors
--cs-surface-accentThe screen's Allow button
--cs-surface-accent-textText on the Allow button
--consent-primaryPrimary buttons, checkmarks, accents
--consent-on-primaryText and checkmarks on a primary fill (falls back to white)
--consent-secondarySecondary fills such as scope badges
--consent-on-secondaryText on a secondary fill
--consent-linkPrimary-colored links on the white card

If you set your own brand colors, set the on-* and link variables too. The helpers generateCSSVariables, contrastRatio and readableTextOn in @kya-os/consent/styles compute values that reach WCAG AA (4.5:1).

Using Tailwind

Tailwind utility classes on an element can't reach inside a shadow root, so you can't style our components by adding class="..." to them. That is the point of the boundary. You can still bring Tailwind in through two supported routes: its theme variables, and ::part(). The screen exposes headline, eyebrow, subhead, card, agent, permissions, actions, connector, and forwards submit and cancel from its action bar.

@import "tailwindcss";

consent-capabilities-screen {
  --cs-surface-accent: var(--color-emerald-600);
  --cs-surface-accent-text: white;
  --consent-primary: var(--color-emerald-600);
}

consent-capabilities-screen::part(headline) {
  @apply tracking-tight text-3xl;
}
consent-capabilities-screen::part(submit) {
  @apply rounded-xl shadow-lg;
}
consent-capabilities-screen::part(card) {
  @apply bg-emerald-50;
}

We compiled exactly this with the Tailwind v4.3.4 CLI and checked it against the real component: the Allow button took Tailwind's emerald-600, the button got a 12px radius, and the headline went to 30px with tight tracking. Other Tailwind versions should work the same way, but we haven't tested them.

Entry points

ImportWhat's in it
@kya-os/consentTypes, schemas, defaults and utilities
@kya-os/consent/bundleRegisters the components. Never imports @kya-os/id
@kya-os/consent/componentsThe Lit element classes
@kya-os/consent/reactReact wrappers for button, checkbox, input, shell, permissions, terms, OAuth button, OTP input and mcp-consent
@kya-os/consent/passkey, /passkey/reactThe passkey element. Needs @kya-os/id
@kya-os/consent/templatesServer-side page rendering, including renderConsentPage
@kya-os/consent/stylesCSS tokens and generators, contrast helpers
@kya-os/consent/schemasZod schemas for consent configuration
@kya-os/consent/types, /constants, /resolutionShared types, default values, config resolution
@kya-os/consent/securityXSS prevention and input validation utilities
@kya-os/consent/mcp-appThe inline consent UI for MCP Apps
@kya-os/consent/consent.js, /consent.min.jsThe self-contained browser bundle

Passkeys

consent-passkey runs one WebAuthn ceremony for a consent step. It ships from its own entry point because it imports @kya-os/id/wallet/passkey, an optional peer dependency.

npm install @kya-os/id   # only if you import @kya-os/consent/passkey
import "@kya-os/consent/passkey";

const el = document.createElement("consent-passkey");
el.ceremony = step.props.ceremony; // "register" | "authenticate"
el.options = step.props.options;   // passed on unmodified
el.addEventListener("consent-passkey:complete", (e) => post(e.detail));
el.addEventListener("consent-passkey:error", showError);       // { code, message }
el.addEventListener("consent-passkey:unsupported", blockUser); // { ceremony }
  • Without @kya-os/id, importing a passkey entry point fails with a module-not-found error for @kya-os/id/wallet/passkey. Install @kya-os/id@^0.6.0.
  • The ceremony starts only from a click on the element's button, never when the element connects.
  • The element makes no network calls. Your server verifies the result.
  • A browser without WebAuthn gets an unsupported state with no button and no fallback credential.
  • The passkey entry points are ESM only, because @kya-os/id ships no CommonJS build.

Consent servers

Most of the package needs no server. consent-capabilities-screen and the primitives render in the browser and report the decision through events, so your own code decides what to do with it.

mcp-consent is the full flow, and it does need one. On Allow it posts a form to {server-url}/consent/approve and expects a JSON response, then shows its success screen. The gallery runs it against a mock endpoint so you can see the whole flow. In production, point server-url at a server that implements that endpoint.

renderConsentPage(config, remoteConfig, mode) from @kya-os/consent/templates renders a standalone consent page that loads the elements from {serverUrl}/consent.js. Whichever path you use, your server must validate every submitted scope against its stored authorization request.

Why web components

The elements are standard custom elements with shadow DOM, a browser standard described by the W3C and maintained as living specifications by the WHATWG (custom elements, shadow trees). See also the MDN guide.

A consent screen renders in hosts you don't control, next to content an agent wrote, in whatever framework the host picked. A React or Vue component library needs that framework and shares the page's CSS. A web component needs a browser, keeps its styles inside a shadow root, and is used the same way everywhere: set properties, listen for events. We ran the snippets on the home page in React 19 and Vue 3. Other frameworks that support custom elements should work too, but we haven't tested them.

Plug it into your own trust

The screen is the human step in agent delegation. It takes plain data in and gives plain data back, so what happens next is yours to decide.

  • In: agentMetadata (name, did, vendor, verification, domain) and capabilities (id, label, description, icon, riskLevel, defaultOn, scopes, and an optional cedar policy).
  • Out: a capabilities-allow event with selectedCapabilityIds and selectedScopes, or capabilities-deny.
  • Then: issue a credential, grant an OAuth scope, ask a policy engine, or hand the decision to your agent framework. With a KYA-OS runtime, an approval is recorded as a delegation credential.

Identity and enforcement stay with you. The identity pill shows the values you pass in, and your server verifies the agent and validates every scope.

Security model

Each consent-* element renders in an open shadow root. That gives you encapsulation, which is useful against accidents and simple attacks, but it is not a sandbox.

  • Page CSS can't restyle the inside of a component, and document.querySelector can't find its internals.
  • Agent-supplied text, such as the agent name and capability descriptions, is rendered as text. Bindings are escaped, and the headline escapes the agent name before it builds markup. Link and logo URLs are sanitized.
  • consent-capabilities-screen and consent-passkey make no network calls. mcp-consent posts to your server-url, and consent-oauth-button navigates to the URL you give it.
  • A passkey ceremony starts only from a click on the element's own button.

What it doesn't do:

  • Script on the same page can read and drive open shadow roots. If untrusted content shares the page, render the screen in a sandboxed iframe or on its own origin.
  • The identity pill reflects the verification and domain values your app passes in. It doesn't verify the agent.
  • Your server must validate every submitted scope against its stored authorization request, whichever component sent it.