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
| Element | Event | Detail |
|---|---|---|
consent-capabilities-screen | capabilities-allow | { selectedCapabilityIds, selectedScopes } |
consent-capabilities-screen | capabilities-deny | none |
consent-permissions | change | { selected } |
consent-capability-card | capability-toggle | { id, checked } |
consent-action-bar | consent-allow, consent-deny | none |
mcp-consent | mcp-consent:approve, :deny, :error | error: { error, code? } |
consent-passkey | consent-passkey:complete, :error, :unsupported | see 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.
| Variable | What it colors |
|---|---|
--cs-surface-accent | The screen's Allow button |
--cs-surface-accent-text | Text on the Allow button |
--consent-primary | Primary buttons, checkmarks, accents |
--consent-on-primary | Text and checkmarks on a primary fill (falls back to white) |
--consent-secondary | Secondary fills such as scope badges |
--consent-on-secondary | Text on a secondary fill |
--consent-link | Primary-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
| Import | What's in it |
|---|---|
@kya-os/consent | Types, schemas, defaults and utilities |
@kya-os/consent/bundle | Registers the components. Never imports @kya-os/id |
@kya-os/consent/components | The Lit element classes |
@kya-os/consent/react | React wrappers for button, checkbox, input, shell, permissions, terms, OAuth button, OTP input and mcp-consent |
@kya-os/consent/passkey, /passkey/react | The passkey element. Needs @kya-os/id |
@kya-os/consent/templates | Server-side page rendering, including renderConsentPage |
@kya-os/consent/styles | CSS tokens and generators, contrast helpers |
@kya-os/consent/schemas | Zod schemas for consent configuration |
@kya-os/consent/types, /constants, /resolution | Shared types, default values, config resolution |
@kya-os/consent/security | XSS prevention and input validation utilities |
@kya-os/consent/mcp-app | The inline consent UI for MCP Apps |
@kya-os/consent/consent.js, /consent.min.js | The 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/passkeyimport "@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/idships 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) andcapabilities(id,label,description,icon,riskLevel,defaultOn,scopes, and an optionalcedarpolicy). - Out: a
capabilities-allowevent withselectedCapabilityIdsandselectedScopes, orcapabilities-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.querySelectorcan'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-screenandconsent-passkeymake no network calls.mcp-consentposts to yourserver-url, andconsent-oauth-buttonnavigates 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
verificationanddomainvalues 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.