Skip to content

UIAP Policy Extension

FieldValue
StatusDraft
Version0.1
Date2026-03-27
Dependencies[UIAP-CORE]
EditorsPatrick

UIAP Policy Extension v0.1 defines how an application describes and evaluates at runtime machine-readable rules for:

  • permitted agent actions,
  • confirmations,
  • sensitive data,
  • redaction,
  • audit,
  • human handoff.

The Extension builds on UIAP Core v0.1, Capability Model v0.1, Web Profile v0.1, and Action Runtime v0.1.

The key words MUST, MUST NOT, SHOULD, MAY in this document are to be interpreted as described in RFC 2119 and BCP 14, when and only when they appear in ALL CAPS.

type ExtensionId = "uicp.policy";
type ExtensionVersion = "0.1";

An implementation using this extension MUST negotiate it during the handshake:

{
"id": "uicp.policy",
"versions": ["0.1"],
"required": false
}
  1. Policy is locally enforceable. An app or bridge MUST be able to enforce a policy decision even when the agent wants something different.
  2. Policy is decision-oriented, not prompt-oriented.
  3. Policy separates permission, risk, sensitivity, audit obligation, and handoff obligation.
  4. Policy MUST be evaluated before every non-trivial Action.
  5. Policy MUST NOT be weaker than browser or platform boundaries.

type PrincipalType = "user" | "agent" | "bridge" | "observer" | "system";
interface PolicyPrincipal {
type: PrincipalType;
id: string;
roles?: string[];
grants?: PolicyGrant[];
}
type PolicyGrant =
| "observe"
| "guide"
| "draft"
| "act"
| "admin"
| "read.sensitive"
| "read.secret"
| "write.sensitive"
| "billing"
| "identity"
| "security";
  • observe: Read the UI but change nothing
  • guide: Highlight/focus/navigate without side effects
  • draft: Reversible drafts or field suggestions
  • act: Normal operational actions
  • admin: Privileged actions
  • Additional grants govern sensitive domains and data
type DataClass =
| "public"
| "internal"
| "personal"
| "sensitive"
| "credential"
| "secret"
| "payment"
| "legal";
type SideEffectClass =
| "none"
| "local_ui"
| "internal_persist"
| "external_message"
| "identity_change"
| "billing_change"
| "security_change"
| "irreversible";
type PolicyEffect =
| "allow"
| "confirm"
| "deny"
| "handoff";
type PolicyReasonCode =
| "grant_missing"
| "route_denied"
| "target_denied"
| "risk_confirm"
| "risk_blocked"
| "sensitive_data"
| "secret_data"
| "credential_data"
| "external_effect"
| "privileged_action"
| "user_activation_missing"
| "human_actor_required"
| "unsafe_retry"
| "redaction_required"
| "policy_default";

interface PolicyDocument {
modelVersion: "0.1";
extension: "uicp.policy";
profile?: string; // e.g. "[email protected]"
defaults: PolicyDefaults;
rules: PolicyRule[];
redaction?: RedactionRule[];
audit?: AuditPolicy;
handoff?: HandoffPolicy;
metadata?: Record<string, unknown>;
}
interface PolicyDefaults {
onSafeRisk: PolicyEffect; // RECOMMENDED: "allow"
onConfirmRisk: PolicyEffect; // RECOMMENDED: "confirm"
onBlockedRisk: PolicyEffect; // RECOMMENDED: "handoff"
onUnknownAction: PolicyEffect; // RECOMMENDED: "deny"
onSensitiveRead: PolicyEffect; // RECOMMENDED: "confirm"
onSecretRead: PolicyEffect; // RECOMMENDED: "deny"
}
interface PolicyRule {
id: string;
enabled?: boolean;
priority?: number; // higher wins
when: PolicyPredicate;
effect: PolicyEffect;
obligations?: PolicyObligation[];
reason?: string;
}
interface PolicyPredicate {
actionIds?: string[];
routeIds?: string[];
stableIds?: string[];
roles?: UIRole[];
riskLevels?: RiskLevel[];
riskTags?: RiskTag[];
dataClasses?: DataClass[];
sideEffectClasses?: SideEffectClass[];
principals?: string[]; // principal.id
principalTypes?: PrincipalType[];
requiredGrants?: PolicyGrant[];
executionModes?: ExecutionMode[];
}
type PolicyObligation =
| {
type: "audit";
level?: AuditLevel;
}
| {
type: "redact";
paths: string[];
replacement?: string;
}
| {
type: "limitExecutionModes";
modes: ExecutionMode[];
}
| {
type: "requireVerification";
policy: "any" | "all";
signals?: SuccessSignal[];
}
| {
type: "requireUserActivation";
}
| {
type: "requireHumanActor";
reason?: string;
}
| {
type: "maxAttempts";
value: number;
};

interface PolicyContext {
sessionId?: string;
revision?: RevisionId;
principal: PolicyPrincipal;
actionId: ActionId;
target?: {
ref?: TargetRef;
stableId?: StableId;
role?: UIRole;
name?: string;
scopeId?: ScopeId;
documentId?: DocumentId;
};
risk?: RiskDescriptor;
dataClasses?: DataClass[];
sideEffectClass?: SideEffectClass;
executionMode?: ExecutionMode;
routeId?: string;
userActivation?: {
isActive?: boolean;
hasBeenActive?: boolean;
};
retryOfActionHandle?: string;
attempt?: number;
args?: Record<string, unknown>;
metadata?: Record<string, unknown>;
}

interface PolicyDecision {
decision: PolicyEffect;
reasonCodes: PolicyReasonCode[];
obligations?: PolicyObligation[];
effectiveExecutionModes?: ExecutionMode[];
redactions?: RedactionPlan[];
audit?: AuditDirective;
cacheTtlMs?: number;
}
interface RedactionPlan {
path: string;
replacement: string;
}
type AuditLevel = "none" | "decision" | "result" | "full";
interface AuditDirective {
level: AuditLevel;
emitRecord: boolean;
}

interface PolicyGetPayload {}
interface PolicyDocumentPayload {
policy: PolicyDocument;
revision?: string;
}

interface PolicyEvaluatePayload {
context: PolicyContext;
}
interface PolicyDecisionPayload {
contextHash?: string;
decision: PolicyDecision;
}

interface PolicyChangedPayload {
revision: string;
reason?: "role_change" | "tenant_change" | "feature_flag" | "policy_update" | string;
policy: PolicyDocument;
}

interface PolicyAuditPayload {
record: PolicyAuditRecord;
}
interface PolicyAuditRecord {
auditId: string;
ts: string;
sessionId?: string;
principal: PolicyPrincipal;
actionId?: ActionId;
target?: TargetRef;
decision: PolicyEffect;
reasonCodes: PolicyReasonCode[];
obligations?: PolicyObligation[];
sideEffectClass?: SideEffectClass;
outcome?: "preflight" | "granted" | "confirmed" | "executed" | "failed" | "denied" | "handoff";
stateRevision?: RevisionId;
metadata?: Record<string, unknown>;
}

A Policy Executor MUST derive decisions in this order:

  1. Explicit deny rules
  2. Grant check
  3. Target/route blocklists
  4. Data classes and redaction obligations
  5. Risk level and risk tags
  6. Side-effect class
  7. User activation / human actor obligations
  8. Defaults
  • safe + sufficient grants -> allow
  • confirm -> confirm
  • blocked -> handoff
  • secret/credential read without special grant -> deny
  • Non-idempotent retry with sideEffectState="unknown" -> deny or handoff

Redaction MUST be modeled separately from action permissibility.

interface RedactionRule {
id: string;
when: {
dataClasses?: DataClass[];
stableIds?: StableId[];
routeIds?: string[];
};
applyTo: Array<"snapshot" | "signal" | "returnValue" | "audit">;
replacement?: string; // default: "[REDACTED]"
}
  1. Redaction MAY partially mask an object without denying the Action itself.
  2. credential and secret SHOULD be redacted by default.
  3. Audit data SHOULD be redacted more aggressively than runtime data.

Browsers and platforms require genuine user interaction or trusted input in certain cases. Therefore, handoff is not an error but a normal policy outcome. navigator.userActivation provides the activation state, Event.isTrusted distinguishes browser/user-agent-generated events from dispatchEvent() events, and HTMLElement.click() does not automatically replace every user-activation-gated situation. ([MDN Web Docs][2])

interface HandoffPolicy {
triggers: HandoffTrigger[];
defaultMessage?: string;
}
type HandoffTrigger =
| "user_activation_required"
| "credential_entry"
| "payment_approval"
| "external_auth"
| "captcha"
| "legal_acknowledgement"
| "ambiguity"
| "security_sensitive";
  • When an obligation contains requireUserActivation and userActivation.isActive !== true, the decision MUST be at least handoff.
  • When requireHumanActor is active, no autonomous execution MAY follow.
  • handoff SHOULD be able to produce a human-readable explanation text.

A conforming [email protected] implementation MUST:

  • Support uicp.policy.get and uicp.policy.evaluate,
  • Validate PolicyDocument and PolicyDecision,
  • Distinguish between confirm, deny, handoff, and allow,
  • Be able to apply Redaction Rules,
  • Be able to produce Audit Records,
  • Treat blocked risk more strictly than confirm.

{
"modelVersion": "0.1",
"extension": "uicp.policy",
"profile": "[email protected]",
"defaults": {
"onSafeRisk": "allow",
"onConfirmRisk": "confirm",
"onBlockedRisk": "handoff",
"onUnknownAction": "deny",
"onSensitiveRead": "confirm",
"onSecretRead": "deny"
},
"rules": [
{
"id": "deny-credentials",
"priority": 100,
"when": {
"dataClasses": ["credential", "secret"]
},
"effect": "deny",
"obligations": [
{
"type": "audit",
"level": "decision"
}
]
},
{
"id": "confirm-create-video",
"priority": 50,
"when": {
"actionIds": ["video.create"]
},
"effect": "confirm",
"obligations": [
{
"type": "requireVerification",
"policy": "all",
"signals": [
{ "kind": "route.changed", "pattern": "/videos/:id" },
{ "kind": "toast.contains", "text": "erstellt" }
]
},
{
"type": "audit",
"level": "result"
}
]
}
],
"redaction": [
{
"id": "mask-secrets",
"when": {
"dataClasses": ["secret", "credential"]
},
"applyTo": ["snapshot", "audit", "returnValue"],
"replacement": "[REDACTED]"
}
],
"audit": {
"level": "result",
"includeArgs": false,
"includeReturnValue": false
},
"handoff": {
"triggers": [
"user_activation_required",
"credential_entry",
"payment_approval",
"external_auth"
],
"defaultMessage": "Please complete this step yourself."
}
}

  • [UIAP-CORE] UIAP Core v0.1
  • [RFC2119] Key words for use in RFCs to Indicate Requirement Levels, BCP 14
  • Policy documents SHOULD only be loaded from trusted sources.
  • A local deny decision MUST always take precedence and MUST NOT be overridden by an external service.
  • Audit logs MUST be stored in a tamper-proof manner.
  • Sensitive data MUST be redacted before logging.
  • requireUserActivation SHOULD be set for all irreversible or externally effective actions.
VersionDateChanges
0.12026-03-27Initial draft