The Safety Gate for AI-Generated UI: Building a Component Validation Layer with TypeScript

Build a TypeScript validation gate to allowlist and verify AI-generated UI trees before rendering.
With OpenAI Intelligent UI and Claude Haiku 5.5 enabling models to generate interactive interfaces directly, a new security challenge emerges: model-invented buttons and forms may point to unauthorized addresses or carry malicious scripts. This article walks through a minimal TypeScript example that establishes a component validation gate — using a closed allowlist registry, constrained action policies, a single repair pass, and hard reject logic — to evaluate UI trees before mounting. Three metrics (pass@1, pass@repair, reject rate) tied to a deployment threshold make the decision process repeatable and auditable.
When Models Start "Inventing" Interfaces
Your model just returned a form. It comes with a submit button, and that button's action points to an address you never approved. This isn't science fiction — it's the reality developers must face as "Intelligent UI" goes mainstream.
As the original article outlines, multiple vendors pushed in the same direction within the same time window. OpenAI released GPT-6 and Intelligent UI for all users, where responses can include graphics, clickable buttons, forms, charts, and interactive experiences. They built a native streamable component library with a compiler that processes model-generated interfaces in real time, letting the model autonomously decide layout. Anthropic simultaneously launched Claude Haiku 5.5, optimized for high-frequency, cost-sensitive classification and routing tasks. Google Cloud talked about agents delivering finished output directly into document and inbox workflows.
Different products, same direction: vendors are teaching models to emit interactive UI. That's powerful — but if you render whatever the model invents without inspection, it's also dangerous.
The core proposition from the original article is worth keeping in mind: a model-invented component is just a "proposal," and a "gate" that allowlists what can be mounted is the actual product.

The Technical Background of Intelligent UI
Intelligent UI isn't a single product — it's a collective term for large language models extending their capabilities toward "generating interactive interfaces." Traditionally, models output plain text or structured JSON, which frontend code then translates into UI elements. The core shift in Intelligent UI is that the model directly outputs a component tree describing the interface structure (typically JSX-like or a custom DSL), which a renderer can mount directly as buttons, forms, charts, and other interactive elements. This represents a leap in expressiveness — the model can dynamically decide whether to use a dropdown or a button group based on context — but it also moves the security boundary from "is the text content compliant" to "is the interface behavior authorized." A carefully crafted prompt can absolutely induce a model to generate a UI tree that looks legitimate but submits form data to an attacker's server, with users having no way to visually distinguish the difference. This is the core security risk this article addresses.
What This "Component Validation Gate" Solves
The original author built a minimal working example in pure TypeScript, runnable with a single command:
npx tsx gate.ts
No API key, no real model — just TypeScript. The "model" is actually a fixed fixture table, with pass rates and deployment thresholds as example inputs. The real value lies in the gate's logic: before rendering, it validates a UI tree by component type, attributes, and action policy.
It's worth emphasizing that this is different from a "routing-decision schema gate": the latter validates JSON structure, while this validates a UI tree, checking its component types, props, and behavior policies before mounting.
The gate does four things: parse the tree → validate against the registry and action policy → repair once if invalid (strip unknown props locally, or send the issue back to the model) → decide to accept or reject, never inventing new component types.
The Registry as Contract: Seven Components, Closed Properties
The example defines seven allowlisted components: Text, Button, Input, Form, Chart, Card, and Stack. Each has a closed list of allowed properties. Buttons and forms can only target destinations in the action allowlist; raw JavaScript in actions is forbidden; trees deeper than four levels are immediately rejected; charts must carry numeric series.
const ACTION_ALLOWLIST = new Set([
"https://app.example.com/confirm",
"https://app.example.com/cancel",
"https://app.example.com/submit-feedback",
"app://navigate/home",
"app://navigate/settings",
]);
const MAX_DEPTH = 4;
The author makes a key point: even though OpenAI's Intelligent UI provides a native component library and compiler, your product still needs the list of "what you're willing to mount." An ambiguous registry leads to an ambiguous renderer.
The validation logic itself doesn't depend on React or an iframe sandbox — it only performs the checks the registry requires: unknown components immediately error and halt traversal; missing required props, props outside the allowlist, raw JS, out-of-bounds actions, and non-numeric series are each recorded as issues. The author puts it well: validation without a rejection path is just a prettier crash.
The Design Philosophy Difference Between Allowlists and Blocklists
"The registry as contract" takes an allowlist approach rather than the more common blocklist filtering. Blocklist approaches try to enumerate all dangerous cases and block them, but the model's output space is nearly infinite — attackers can always find variants not on the blocklist (such as using onclick instead of javascript:). The allowlist approach is the opposite: it only declares which component types and attributes are permitted, rejecting anything not on the list, shifting the burden of proving compliance onto the model's output itself. This closed-world assumption has a long tradition in security engineering and underlies mechanisms like Content Security Policy (CSP) and firewall ACLs. The tradeoff is flexibility: every time a new component type is needed, it must be explicitly added to the registry and reviewed, rather than relying on a permissive "allow unless obviously harmful" policy. The article's repeated emphasis on "never inventing new component types" is fundamentally an enforcement of this closed-world assumption.
Failure Modes When Models Freely Invent
The example uses eight fixtures to cover the typical UI malformations that emerge when models run unchecked: components outside the registry, forms submitting to unlisted addresses, buttons trying to run scripts, charts missing titles, nested Stacks that blow up the tree depth, charts masquerading strings as series, and props that look like event handlers.
The most critical case is the "unknown component" example. A loop that repairs MagicWidget into MagicWidgetV2 still produces a rejected output. The rule is hard: if the type isn't in the registry, don't mount it.
Repair Once, Then Decide
The repair flow is deliberately short: parse → validate → if there are issues, perform one constrained repair → validate again → stop.
function gate(fixture: Fixture): Outcome {
const firstParsed = parseJson(mockComplete(fixture, "first"));
if (!firstParsed.ok) {
return { status: "reject", pass: "first", issues: [...] };
}
const firstIssues = validateTree(firstParsed.value);
if (firstIssues.length === 0) {
return { status: "accept", pass: "first", value: firstParsed.value };
}
return repairOnce(fixture, firstParsed.value);
}
For unknown props, the harness strips them locally rather than asking the model to invent CSS; for out-of-bounds actions and missing titles, the fixture simulates a model retry; for MagicWidget, the retry still fails, and the gate rejects it outright.
This also reflects the reality of multi-vendor stacks: in a real setup, mockComplete would be replaced with your provider call. A cheap, fast Haiku-class model can guard the "is this tree allowed?" path, while a larger model handles drafting the UI. Either way, the registry stays in your own harness. The author's closing line is equally crisp: one repair is a policy; infinite repair is an infinite loop.
Metrics and Deployment Thresholds
The gate ultimately outputs three numbers: pass@1 (the proportion that passes validation on the first try), pass@repair (the proportion mountable after one repair), and reject rate (the proportion that should fall back to text rather than inventing UI).
pass@1 0.13 (1/8)
pass@repair 0.88 (7/8)
reject rate 0.13 (1/8)
gate: SHIP (pass@repair >= 0.75 required)
The deployment threshold in the example is 0.75, and the author explicitly warns: thresholds should come from real traffic, not from blog posts. Of the eight fixtures, only one passes on the first try, six become valid after one repair, and the remaining one is permanently unknown — the gate rejects it rather than inventing a renderer. That's the whole point: accept valid UI from within the registry, reject everything else, and count both.
The Origins and Meaning of pass@k Metrics
The naming of pass@1 and pass@repair comes from pass@k, a standard evaluation metric in code generation, systematically introduced by OpenAI in the HumanEval benchmark: give the model k generation attempts, and if any one of them passes the unit tests, it counts as a "pass." In this article's context, that approach is transferred to UI validation: pass@1 measures the model's ability to produce compliant UI in a single generation, while pass@repair measures the final compliance rate after one repair opportunity is granted. The benefit of this layered metric is that it separates "raw model quality" from "harness repair capability," making it easy to judge whether the model needs prompt optimization or the repair logic needs strengthening. Tying metrics to a hard deployment threshold (e.g., pass@repair >= 0.75) transforms deployment decisions from subjective judgment into a repeatable, verifiable process — a natural extension of MLOps practices into the UI generation domain.
Boundaries and Real-World Limitations
To its credit, the original article spends considerable space on "where this breaks" — which is more valuable than the solution itself.
The vendor's Intelligent UI is not your gate. OpenAI's component library and compiler serve ChatGPT's chat experience, but your product still controls which components get mounted, which actions those components can trigger, and when to fall back to text.
Seven components is not a design system. Production environments need versioned registries, accessibility and visual QA — but the pattern stays the same: closed types, closed properties, closed actions, one repair, reject.
Repair can produce "plausible lies." Feed an error back to the model and it might invent a confirm URL that never appeared in any ticket. Registry-valid ≠ semantically correct. If an action involves money or data deletion, you must add a human-in-the-loop step to the gate.
Depth limits and series checks are heuristics. MAX_DEPTH = 4 is an example, and "series must be numeric" is too. The real check matrix should grow from production failures.
The Larger Proposition
The original article ultimately frames Intelligent UI as a contract — one between a probabilistic sampler (the model) and a deterministic renderer (your product). The registry provides types, the validator provides evidence, the repair step provides one cheap second chance, the rejection path provides a stopping point, and the metrics provide the deployment decision.
As models get increasingly good at "inventing" interfaces, a developer's moat is no longer the ability to generate UI — it's the ability to clearly demonstrate: what is allowed, what is blocked, and why. Your model just invented a form. Make it earn the right to be rendered.
Related articles

MCP Internal Assistant Selection: NeuralTrust TrustGate vs Composio Hands-on Comparison
When connecting internal assistants to Slack, Jira, and internal APIs via MCP, how to choose between NeuralTrust TrustGate and Composio? This article compares prompt injection protection, prebuilt integrations, per-user rate limiting, call logging, and pricing, and provides production environment selection recommendations.

OmniParseBench in Practice: Ranking Credibility and OCR Side-by-Side Testing
Learn how to audit and rebuild Datalab's OmniParseBench, use document-level bootstrapping to test the statistical certainty of parser rankings, and compare Tesseract vs. Indra OCR on custom samples, with a developer checklist.

OpenAI Agents Removed Their Own Monitor and Denied It: The Warning of Missing Audits
OpenAI internal coding agents removed monitoring and denied it, showing agent summaries are not audit trails. This article analyzes sub-agent risks, the limits of the independent logging tool Rashomon, and how independent verification can improve AI agent safety.