The Useful Boundary: A UI Contract, Not a Magic Protocol
A2UI is a JSON-based streaming Agent-to-UI protocol: an Agent emits declarative surface, component, and data-model messages, and a product-owned renderer maps them to local UI code. Version 0.9.1 is the current production release. Version 1.0 is a release candidate with a different envelope and bidirectional function calls, so the two versions must not be mixed implicitly.
It is not a complete application architecture. A UI description does not authenticate a user, prove ownership of a booking, validate a price, approve a payment, or make an action happen exactly once. Those are properties of the application around the UI.
Before adopting any A2UI specification or implementation, record the exact surface that was tested:
{
"ui_contract": "a2ui-v0.9.1",
"status": "current-production",
"transport_profile": "ag-ui-1.0-or-other-pinned-binding",
"schema_bundle": "server_to_client+common_types+catalog",
"renderer": "product-renderer@revision",
"component_catalog": "product-catalog@revision",
"action_api": "server-contract-version",
"accessibility_baseline": "tested-platforms-and-assistive-tech"
}
The message types, data-binding syntax, renderers, and SDKs published by an ecosystem can change. Verify them against the pinned source and your integration tests. Do not promote a package name, roadmap, or example message from a blog into a production dependency without that check.
Pin the Protocol Before Building
A2UI version selection is part of the application contract, not a package-manager detail.
| Capability | v0.9.1 Current | v1.0 Candidate |
|---|---|---|
| Core stream | createSurface, updateComponents, updateDataModel, deleteSurface |
Same lifecycle plus revised Agent/Renderer terminology |
| Initial UI | Surface first, followed by component and data updates | Components and initial data may be included in createSurface |
| Return path | Transport-specific action channel | Typed bidirectional function-call messages |
| Catalog | Component and function catalog with a fixed catalogId |
Revised catalog maps, stricter identifiers, accessibility attributes |
| Production posture | Pin and test as the current release | Isolate behind an adapter; expect schema and behavior changes |
The transport must preserve message order and framing. Capability negotiation determines which catalogs and versions a renderer can consume. A schema-valid payload for one version is not evidence of conformance to another.
When a Generated Surface Helps
Chat is good at explanation. It is weak at comparison, constrained input, and visible state. A travel assistant may make it easier to compare refundable fares in a table, but a "Book" button should not silently turn a model suggestion into a purchase.
Use a generated surface when it improves a concrete user task:
| User need | Suitable contract | Keep outside the Agent |
|---|---|---|
| Compare search results | Read-only cards with citations and refresh time | Ranking policy, price source, and disclosure |
| Choose filters | A fixed allowlisted filter form | Query authorization and server-side validation |
| Review a draft | Editable content with explicit save | Ownership, version conflict handling, and audit |
| Request a consequential action | Summary plus confirmation step | Object authorization, approval, idempotency, and execution |
Prefer a fixed interface for stable product workflows. Prefer text when interaction adds no value. A dynamic surface is a presentation technique, not evidence that a task deserves more autonomy.
Treat Every UI Payload as Untrusted Input
The Agent, retrieved documents, tool results, and remote services can all influence a UI payload. Even valid JSON can attempt to exhaust a renderer, impersonate a trusted screen, smuggle an external URL, or steer a user toward an unsafe action.
A renderer should reject rather than repair an invalid contract. Its admission checks should include:
- schema revision and message type;
- maximum bytes, component count, nesting depth, update rate, and retained surfaces;
- allowed component names and per-component property schemas;
- bounded text, image dimensions, rich-text subset, locale keys, and data references;
- URL scheme, host allowlist, redirect policy, and media retrieval policy;
- unique identifiers, valid parent relations, and cycle detection;
- surface ownership, expiry, and correlation to the current session.
This is defense in depth. JSON Schema can validate shape, but it cannot determine whether a price is current, an account belongs to the user, or a button is appropriate for a sensitive operation.
A Small, Product-Owned Catalog
Start with a deliberately small catalog such as Text, List, Notice, Select, TextInput, Button, and ConfirmationSummary. Each entry should have an explicit accessibility and data contract. Do not pass arbitrary properties through to a framework component.
package main
import (
"errors"
"fmt"
"net/url"
)
type Action struct {
Name string
Token string
}
type Node struct {
ID string
Kind string
Text string
URL string
Action *Action
}
func ValidateNode(node Node, allowedHosts map[string]bool) error {
if node.ID == "" {
return errors.New("missing node id")
}
switch node.Kind {
case "Text":
if len(node.Text) == 0 || len(node.Text) > 500 {
return errors.New("invalid text")
}
case "Link":
target, err := url.Parse(node.URL)
if err != nil || target.Scheme != "https" || !allowedHosts[target.Host] {
return errors.New("unsafe link")
}
case "Button":
if node.Action == nil || node.Action.Token == "" {
return errors.New("missing action")
}
if node.Action.Name != "select_offer" &&
node.Action.Name != "request_confirmation" {
return errors.New("unknown action")
}
default:
return errors.New("unknown component")
}
return nil
}
func main() {
node := Node{
ID: "confirm",
Kind: "Button",
Action: &Action{
Name: "request_confirmation",
Token: "opaque-short-lived-token",
},
}
fmt.Println(ValidateNode(node, map[string]bool{"docs.example": true}))
// Output: <nil>
}
This executable example is an application admission gate, not a complete A2UI validator. A conforming implementation must validate the pinned A2UI envelope and catalog schemas as well. Never render model-produced HTML, styles, scripts, event handlers, or arbitrary deep links.
Data References Need Scope
Separating component structure from data can reduce repeated payloads, but a reference is not permission. Namespace data by surface and session. Forbid references that escape the current surface, and resolve sensitive values only after the server has filtered them for the authenticated user.
Avoid putting secrets, raw tool responses, internal identifiers, or authorization claims in a client-side data model merely because a component might need them. A display identifier and a server-issued opaque action token are usually safer than an account record or a model-selected object ID.
Rendering Is Not Authorization
Consider a user reviewing a proposed order. The Agent may render a summary and a request_confirmation control. The browser can submit that intent, but it cannot establish what the user may buy or what price applies.
The server should construct its decision from trusted session context and authoritative records. The request may contain only an opaque action token and the user's intent; the server resolves that token for the authenticated subject, reloads the object and price, applies current policy, and returns either a rejection or a server-created confirmation record.
A consequential write needs a separate endpoint that rechecks the authenticated subject, object state, exact parameters, confirmation binding, expiry, idempotency key, and business policy immediately before the effect.
Never trust a model-generated user ID, role, price, destination, approval state, or authorization flag. Never let a component name, a schema-valid field, a prompt instruction, or a model refusal decide access.
Design the Action Lifecycle
A safe interaction distinguishes a proposal from an effect:
Use an opaque, short-lived action token that the server binds to the subject, tenant, surface, allowed action, and normalized parameters. Recheck state on every transition. A completed UI update is not proof that an email, payment, deletion, or reservation occurred exactly once.
For long-running work, expose clear states such as pending, needs_confirmation, running, succeeded, rejected, failed, and outcome_unknown. Define refresh, reconnect, cancellation, duplicate-submit, and expiry behavior. A user should be able to understand whether the system has performed an effect or cannot yet prove the result.
Resist Injection, Phishing, and Resource Abuse
Generated UI can make untrusted instructions look official. A retrieved document may try to rename a destructive action, request a credential, or introduce a plausible-looking support link. Rendering a catalog component does not remove this social attack surface.
Build controls at multiple layers:
- label the Agent-generated portion of the screen and preserve product-owned navigation;
- reserve security-sensitive language, identity prompts, and payment controls for fixed application components;
- render external content as text by default, with provenance and a user-mediated open action;
- allow images and URLs only through a proxy or verified allowlist, with size and content-type limits;
- require deliberate confirmation for destructive or externally visible actions;
- prevent the Agent from choosing callback URLs, telemetry destinations, or permission scopes;
- rate-limit surface creation and updates, and measure rejected payloads without retaining sensitive raw content.
Model output is input, not a policy engine. The same is true of retrieved text, tool annotations, and a remote Agent's UI proposal.
Accessibility, Localization, and Privacy Are Contract Requirements
A surface that only looks correct in a screenshot is not a native-quality interface. Make accessibility part of the catalog definition:
- semantic roles, labels, error messages, focus order, and keyboard operation;
- visible focus, sufficient contrast, zoom and reduced-motion behavior;
- screen-reader announcements for incremental updates without repetitive noise;
- localization-ready message keys, plural rules, date/number formatting, and right-to-left layouts;
- no color-only meaning and no time-critical interaction without an accessible alternative.
Do not send sensitive content to telemetry by default. Record a redacted contract revision, rejection class, component counts, latency, action state, and correlation ID. Give user-facing content a retention policy and ensure deletion reaches cached surfaces, analytics payloads, and artifact stores.
Test the Contract Like an API
Unit tests of a happy-path renderer are insufficient. Maintain versioned fixtures for accepted, rejected, and legacy payloads, then test:
| Test class | Example failure to catch |
|---|---|
| Schema and limits | Unknown component, cyclic tree, excessive depth, oversized text |
| Renderer safety | HTML-like text, unexpected property, unsafe URL, update storm |
| Authorization | Cross-tenant token reuse, stale offer, model-supplied account ID |
| Side effects | Duplicate confirmation, timeout after execution, cancellation race |
| Accessibility | Keyboard-only completion, focus after updates, screen-reader labels |
| Localization | Long translations, pluralization, RTL layout, locale-specific amounts |
| Abuse and recovery | Prompt-injected labels, renderer rollback, disconnected client |
Run these tests against the pinned specification and supported clients before an upgrade. Capture an immutable audit event for policy decisions, but keep raw conversation and UI data out of logs unless there is a documented, justified retention basis.
A2UI, AG-UI, MCP Apps, and A2A Are Different Boundaries
These technologies can compose, but they do not solve the same problem:
| Boundary | Primary responsibility | What it does not prove |
|---|---|---|
| A2UI | Declarative component tree, data model, and UI updates | Transport delivery, identity, authorization, or effect success |
| AG-UI 1.0 | Ordered runtime event stream between Agent backend and user-facing application | How a dynamic component tree is rendered |
| MCP Apps | An MCP Tool references an interactive HTML resource rendered in a sandboxed host view | That arbitrary HTML is safe or that Tool actions are authorized |
| A2A | Task and message exchange between independently operated Agents | Renderer conformance or end-user approval |
A2UI can travel over AG-UI, A2A, MCP, or another ordered transport. It can also host an MCP App component or be rendered inside an MCP App, but that composition introduces both the declarative A2UI boundary and the embedded-app sandbox boundary. Each action still needs trusted identity, tenant and object checks, budget controls, and an auditable side-effect policy.
FAQ
Is declarative UI safer than model-generated HTML?
It can materially reduce one class of code-execution risk when the renderer maps a strict catalog to reviewed local code. It does not protect against unsafe URLs, deceptive labels, data leakage, denial of service, or unauthorized server actions. Treat the description as untrusted input either way.
Should a renderer accept unknown components for forward compatibility?
Not by default. Reject the payload or render a product-owned safe fallback, report the contract mismatch, and upgrade catalog and renderer together through a tested compatibility release. Silent interpretation creates ambiguous behavior and makes review harder.
Can a UI schema validate a payment or deletion?
No. A schema validates structure and sometimes value ranges. The action service must validate identity, tenant, ownership, current state, amount, approval, idempotency, and business invariants against authoritative records at execution time.
What is the smallest useful production rollout?
Start with a read-only surface using a short catalog, server-filtered data, strict payload limits, accessible fixed controls, and observable rejection paths. Add action intents only after the authorization, confirmation, and recovery contracts have been tested.
Further Reading
- A2A Protocol: Inter-Agent Trust and Task Boundaries
- MCP Protocol: Production Boundaries for Tools and Resources
- MCP Apps: Interactive Tool UI Boundaries
- A2UI vs AG-UI vs Vercel AI SDK
- Prompt Injection: Threat Model and Defenses