RFC: Add immutable / locked surface policy for safety-critical confirmations
Summary
I would like to propose a standard way to mark an A2UI surface, or parts of a surface, as immutable after it has been presented as a safety-critical confirmation.
A2UI’s incremental update model is very useful for progressive UI generation. However, for irreversible or high-risk user actions such as payments, bank transfers, medical consent, legal approval, account recovery, or permission grants, it is important that the confirmation content the user saw cannot later be changed by updateComponents, updateDataModel, or deleteSurface.
The proposal is to add a first-class surface policy, or equivalent protocol-level mechanism, that allows a trusted host/orchestrator to lock a surface after final confirmation content has been rendered.
Motivation
A2UI currently allows agents to progressively build and update a surface. This is valuable for normal UI flows, but it creates ambiguity for confirmation UIs where the displayed content has security or compliance meaning.
For example, consider a transfer confirmation surface:
{
"version": "v0.9",
"updateDataModel": {
"surfaceId": "transfer-confirmation-123",
"path": "/",
"value": {
"recipientName": "Hanako Yamada",
"amount": 50000,
"currency": "JPY",
"status": "ready"
}
}
}
If the user sees:
Transfer JPY 50,000 to Hanako Yamada
a later message could still update the bound data model:
{
"version": "v0.9",
"updateDataModel": {
"surfaceId": "transfer-confirmation-123",
"path": "/recipientName",
"value": "Taro Yamada"
}
}
or replace the visible component via updateComponents.
For safety-critical confirmations, this is problematic because the user may believe they approved one set of details while the UI surface later shows, or submits, another set of details.
Applications can implement their own orchestrator-side filtering today, but there does not appear to be a standard protocol-level way for an A2UI host, renderer, or remote agent to agree that a surface is now locked.
Goals
- Provide a standard way to declare that a surface is immutable or locked.
- Allow a trusted host/orchestrator to enforce this before messages reach the renderer.
- Allow renderers to reject or no-op invalid updates to locked surfaces.
- Support common safety-critical flows where most fields are frozen, but status fields may still update.
- Preserve A2UI’s progressive rendering model before the surface is locked.
- Make violations observable through errors or telemetry.
Non-goals
- This should not replace backend authorization, transaction signing, or server-side intent validation.
- This should not make client-side data model values the source of truth for irreversible operations.
- This should not rely on an untrusted LLM or remote agent self-declaring that a surface is immutable.
- This should not prevent applications from using their own stricter policy gates.
Proposal
Introduce a standard surface mutability policy. The exact naming is open to discussion, but conceptually it could look like this.
Option A: policy on createSurface
{
"version": "v1.0",
"createSurface": {
"surfaceId": "transfer-confirmation-123",
"catalogId": "https://example.com/catalog.json",
"surfacePolicy": {
"mutability": "locked",
"reason": "safetyCriticalConfirmation",
"allowedDataModelPaths": [
"/status",
"/progress",
"/errorMessage",
"/completedAt"
],
"allowComponentUpdates": false,
"allowDeleteSurface": false
}
}
}
Option B: separate lockSurface message
This may be more compatible with progressive UI construction, because many surfaces are mutable while they are being built and should only become immutable once final confirmation content is shown.
{
"version": "v1.0",
"lockSurface": {
"surfaceId": "transfer-confirmation-123",
"reason": "safetyCriticalConfirmation",
"allowedDataModelPaths": [
"/status",
"/progress",
"/errorMessage",
"/completedAt"
],
"allowComponentUpdates": false,
"allowDeleteSurface": false
}
}
After this message is accepted, the surface is considered locked.
Expected behavior
For a locked surface:
updateComponents
By default, updateComponents should be rejected or ignored.
Rationale: component updates can change visible labels, button text, images, action wiring, or other UI elements that the user may rely on when approving an irreversible action.
updateDataModel
updateDataModel should only be allowed for explicitly permitted paths.
For example, these should be allowed:
{
"version": "v1.0",
"updateDataModel": {
"surfaceId": "transfer-confirmation-123",
"path": "/status",
"value": "processing"
}
}
{
"version": "v1.0",
"updateDataModel": {
"surfaceId": "transfer-confirmation-123",
"path": "/errorMessage",
"value": "Transfer failed. Please try again."
}
}
But these should be rejected:
{
"version": "v1.0",
"updateDataModel": {
"surfaceId": "transfer-confirmation-123",
"path": "/recipientName",
"value": "Taro Yamada"
}
}
{
"version": "v1.0",
"updateDataModel": {
"surfaceId": "transfer-confirmation-123",
"path": "/",
"value": {
"recipientName": "Taro Yamada",
"amount": 50000
}
}
}
The root path / should be rejected by default for locked surfaces unless explicitly allowed, because it can replace frozen fields indirectly.
Path matching should also treat parent and child paths as touching the same protected region. For example, if /recipient is frozen, then /recipient/name, /recipient/accountNumber, and / should not be allowed to modify it.
deleteSurface
For locked surfaces, deleteSurface should be rejected by default.
If a confirmation must be canceled or superseded, the recommended pattern should be:
- mark the old confirmation as revoked or canceled through an allowed status path;
- create a new surface with a new confirmation snapshot;
- do not mutate the old confirmation content in place.
Policy authority
The policy should be set by a trusted host/orchestrator or renderer integration, not by an untrusted remote agent alone.
In multi-agent scenarios, a remote agent may propose A2UI messages, but the user-facing agent or host orchestrator should be able to enforce the final surface policy before messages are forwarded to the client.
Suggested model:
Remote Agent / LLM
-> proposes A2UI messages
User-facing Agent / Orchestrator
-> validates messages
-> attaches or enforces surface policy
-> forwards only allowed messages
Renderer
-> applies only valid messages
-> rejects or reports policy violations
Error handling
When a message violates a locked surface policy, the implementation should provide a structured error.
Example:
{
"type": "A2UI_POLICY_VIOLATION",
"surfaceId": "transfer-confirmation-123",
"messageType": "updateDataModel",
"path": "/recipientName",
"reason": "surface is locked and path is not mutable"
}
Open question: should this be reported to the agent, the orchestrator, the renderer host app, or all of them?
Security considerations
This feature should be treated as a UI and protocol integrity mechanism, not as the sole security boundary for the underlying operation.
For irreversible operations, the application should still use a server-side intent or transaction object as the source of truth. For example:
surfaceId -> locked surface policy -> transactionIntentId -> server-side signed intent
The A2UI data model may display or reference the transaction intent, but the backend should not trust the client-side data model as the source of truth.
Recommended application pattern:
- create a server-side intent;
- render a confirmation surface from a signed or otherwise server-controlled snapshot;
- lock the confirmation surface;
- allow only status/progress/error updates;
- execute the operation using the server-side intent, not the mutable A2UI data model.
Backward compatibility
If current schemas do not allow additional fields on createSurface, this should probably be introduced as either:
- a v1.x protocol addition;
- an extension namespace;
- a renderer capability-gated feature;
- a separate
lockSurface message type.
Renderers that do not support the feature should advertise that clearly, so hosts can avoid using A2UI surfaces for final safety-critical confirmation in those environments.
Possible capability:
{
"capabilities": {
"surfacePolicies": true,
"lockedSurfacePolicy": true
}
}
Questions for discussion
- Should immutability be surface-level only, or should the protocol support field-level and component-level policies?
- Should
updateComponents always be denied for locked surfaces, or should component ID/property allowlists be supported?
- Should
deleteSurface be denied, or should there be a special revokeSurface / supersedeSurface pattern?
- Should policy violations be hard errors, no-ops, or host-configurable?
- Should the lock be represented as a
createSurface property, a separate lockSurface message, or an extension?
- Should only the host/orchestrator be allowed to lock/unlock surfaces?
- Should unlocking be supported at all, or should the model be append-only once locked?
- Should this be tied to action handling, so actions from locked confirmation surfaces can carry a confirmation hash or lock token?
Example use case
A payment or bank transfer confirmation should allow this after lock:
{
"version": "v1.0",
"updateDataModel": {
"surfaceId": "transfer-confirmation-123",
"path": "/status",
"value": "completed"
}
}
But should reject these after lock:
{
"version": "v1.0",
"updateDataModel": {
"surfaceId": "transfer-confirmation-123",
"path": "/amount",
"value": 500000
}
}
{
"version": "v1.0",
"updateComponents": {
"surfaceId": "transfer-confirmation-123",
"components": [
{
"id": "recipient-label",
"component": {
"Text": {
"text": {
"literalString": "Transfer to Taro Yamada"
}
}
}
}
]
}
}
Suggested acceptance criteria
A renderer or host that claims support for immutable / locked surfaces should:
- reject or ignore component updates to locked surfaces unless explicitly allowed;
- reject data model updates outside the allowed path list;
- treat omitted
path as /;
- reject root replacement by default;
- reject parent-path updates that would affect frozen child paths;
- reject child-path updates under frozen parent paths;
- reject
deleteSurface by default;
- emit a structured policy violation event;
- preserve the originally displayed confirmation content;
- allow safe status/progress/error updates when configured.
RFC: Add immutable / locked surface policy for safety-critical confirmations
Summary
I would like to propose a standard way to mark an A2UI surface, or parts of a surface, as immutable after it has been presented as a safety-critical confirmation.
A2UI’s incremental update model is very useful for progressive UI generation. However, for irreversible or high-risk user actions such as payments, bank transfers, medical consent, legal approval, account recovery, or permission grants, it is important that the confirmation content the user saw cannot later be changed by
updateComponents,updateDataModel, ordeleteSurface.The proposal is to add a first-class surface policy, or equivalent protocol-level mechanism, that allows a trusted host/orchestrator to lock a surface after final confirmation content has been rendered.
Motivation
A2UI currently allows agents to progressively build and update a surface. This is valuable for normal UI flows, but it creates ambiguity for confirmation UIs where the displayed content has security or compliance meaning.
For example, consider a transfer confirmation surface:
{ "version": "v0.9", "updateDataModel": { "surfaceId": "transfer-confirmation-123", "path": "/", "value": { "recipientName": "Hanako Yamada", "amount": 50000, "currency": "JPY", "status": "ready" } } }If the user sees:
a later message could still update the bound data model:
{ "version": "v0.9", "updateDataModel": { "surfaceId": "transfer-confirmation-123", "path": "/recipientName", "value": "Taro Yamada" } }or replace the visible component via
updateComponents.For safety-critical confirmations, this is problematic because the user may believe they approved one set of details while the UI surface later shows, or submits, another set of details.
Applications can implement their own orchestrator-side filtering today, but there does not appear to be a standard protocol-level way for an A2UI host, renderer, or remote agent to agree that a surface is now locked.
Goals
Non-goals
Proposal
Introduce a standard surface mutability policy. The exact naming is open to discussion, but conceptually it could look like this.
Option A: policy on
createSurface{ "version": "v1.0", "createSurface": { "surfaceId": "transfer-confirmation-123", "catalogId": "https://example.com/catalog.json", "surfacePolicy": { "mutability": "locked", "reason": "safetyCriticalConfirmation", "allowedDataModelPaths": [ "/status", "/progress", "/errorMessage", "/completedAt" ], "allowComponentUpdates": false, "allowDeleteSurface": false } } }Option B: separate
lockSurfacemessageThis may be more compatible with progressive UI construction, because many surfaces are mutable while they are being built and should only become immutable once final confirmation content is shown.
{ "version": "v1.0", "lockSurface": { "surfaceId": "transfer-confirmation-123", "reason": "safetyCriticalConfirmation", "allowedDataModelPaths": [ "/status", "/progress", "/errorMessage", "/completedAt" ], "allowComponentUpdates": false, "allowDeleteSurface": false } }After this message is accepted, the surface is considered locked.
Expected behavior
For a locked surface:
updateComponentsBy default,
updateComponentsshould be rejected or ignored.Rationale: component updates can change visible labels, button text, images, action wiring, or other UI elements that the user may rely on when approving an irreversible action.
updateDataModelupdateDataModelshould only be allowed for explicitly permitted paths.For example, these should be allowed:
{ "version": "v1.0", "updateDataModel": { "surfaceId": "transfer-confirmation-123", "path": "/status", "value": "processing" } }{ "version": "v1.0", "updateDataModel": { "surfaceId": "transfer-confirmation-123", "path": "/errorMessage", "value": "Transfer failed. Please try again." } }But these should be rejected:
{ "version": "v1.0", "updateDataModel": { "surfaceId": "transfer-confirmation-123", "path": "/recipientName", "value": "Taro Yamada" } }{ "version": "v1.0", "updateDataModel": { "surfaceId": "transfer-confirmation-123", "path": "/", "value": { "recipientName": "Taro Yamada", "amount": 50000 } } }The root path
/should be rejected by default for locked surfaces unless explicitly allowed, because it can replace frozen fields indirectly.Path matching should also treat parent and child paths as touching the same protected region. For example, if
/recipientis frozen, then/recipient/name,/recipient/accountNumber, and/should not be allowed to modify it.deleteSurfaceFor locked surfaces,
deleteSurfaceshould be rejected by default.If a confirmation must be canceled or superseded, the recommended pattern should be:
Policy authority
The policy should be set by a trusted host/orchestrator or renderer integration, not by an untrusted remote agent alone.
In multi-agent scenarios, a remote agent may propose A2UI messages, but the user-facing agent or host orchestrator should be able to enforce the final surface policy before messages are forwarded to the client.
Suggested model:
Error handling
When a message violates a locked surface policy, the implementation should provide a structured error.
Example:
{ "type": "A2UI_POLICY_VIOLATION", "surfaceId": "transfer-confirmation-123", "messageType": "updateDataModel", "path": "/recipientName", "reason": "surface is locked and path is not mutable" }Open question: should this be reported to the agent, the orchestrator, the renderer host app, or all of them?
Security considerations
This feature should be treated as a UI and protocol integrity mechanism, not as the sole security boundary for the underlying operation.
For irreversible operations, the application should still use a server-side intent or transaction object as the source of truth. For example:
The A2UI data model may display or reference the transaction intent, but the backend should not trust the client-side data model as the source of truth.
Recommended application pattern:
Backward compatibility
If current schemas do not allow additional fields on
createSurface, this should probably be introduced as either:lockSurfacemessage type.Renderers that do not support the feature should advertise that clearly, so hosts can avoid using A2UI surfaces for final safety-critical confirmation in those environments.
Possible capability:
{ "capabilities": { "surfacePolicies": true, "lockedSurfacePolicy": true } }Questions for discussion
updateComponentsalways be denied for locked surfaces, or should component ID/property allowlists be supported?deleteSurfacebe denied, or should there be a specialrevokeSurface/supersedeSurfacepattern?createSurfaceproperty, a separatelockSurfacemessage, or an extension?Example use case
A payment or bank transfer confirmation should allow this after lock:
{ "version": "v1.0", "updateDataModel": { "surfaceId": "transfer-confirmation-123", "path": "/status", "value": "completed" } }But should reject these after lock:
{ "version": "v1.0", "updateDataModel": { "surfaceId": "transfer-confirmation-123", "path": "/amount", "value": 500000 } }{ "version": "v1.0", "updateComponents": { "surfaceId": "transfer-confirmation-123", "components": [ { "id": "recipient-label", "component": { "Text": { "text": { "literalString": "Transfer to Taro Yamada" } } } } ] } }Suggested acceptance criteria
A renderer or host that claims support for immutable / locked surfaces should:
pathas/;deleteSurfaceby default;