Fuaranfuaran

The source for generative UI

Security

Last reviewed: August 2026.

Generative UI raises one question before any other. If a model produces the interface, and that interface runs on your infrastructure, what stops it producing something that executes code, exhausts the machine, or reaches data it should not?

Most generate-and-run systems answer that afterwards: a container, a timeout, an allow-list policed at run time. Fuaran answers it earlier. What a model emits is not code. It is a bounded, closure-free data structure that a host decodes and renders. There is no interpreter to escape, because there is nothing to interpret.

This page states that property precisely, names the public artefact behind each claim, and then says which host enforces which part of it today. The properties do not hold uniformly across the roster, and the conformance table is the part of this page worth reading first. A safety claim is only as good as its stated boundary, so the boundary is not an appendix here.

The claims, and what backs each

1. The emission is data, not code

The wire format is a finite tree of typed nodes serialised as canonical JSON. Every function-shaped slot in the model, an event handler, a data accessor, a callback, serialises to a fixed sentinel on the wire, and the decoder reconstructs no callable behaviour from it. A host decodes JSON into a typed tree and renders it. It never evaluates, compiles, or invokes what it decoded, because there is no decode path that produces a function.

That is legible directly from the format rather than taken on trust: the sentinel, the closed node vocabulary, and the absence of any script or raw-markup slot are all normative text.

Read it in the specification, and check it against the conformance corpus, the shared fixture set every codec host is held byte-identical to.

2. Decoding refuses rather than guesses

Undeclared keys are rejected, not ignored. An unknown case in a closed vocabulary is an error, not a fallback. Every refusal is a typed error with a stable code and a path, and the specification pins the order in which failures are reported, so conformant hosts surface the same first error for the same bad input. A malformed or hostile document yields a structured rejection rather than a guessed-at render.

The reasoning behind that choice, including what it costs, is in default-deny by shape. The codes themselves are enumerated in the error-code reference, and the corpus carries a reject family that pins the expected code and path for each.

3. The document's structure is bounded

Semantic strictness says nothing about shape. A decoder for a recursive format is a recursive walk, and a few hundred kilobytes of nothing but opening brackets will drive an unguarded host off the end of its stack. On several host languages that is not even a catchable condition, which turns it into a one-request remote kill.

Section 21 of the specification closes this as part of the format rather than as a per-deployment choice: bounds on node depth, syntactic JSON depth, string length, array length, and total node count, with a typed LIMIT_EXCEEDED refusal that must not escape the host's declared error type, and must be enforced on the way down rather than measured afterwards. The section also records how the node-depth figure was derived, by measuring the true overflow depth of each walk in the reference host rather than by choosing a round number.

This is the property with the largest gap between specification and roster, and section 21.5 of the specification is itself the honest record of it: it names which hosts have adopted the limits and what the others do instead. The conformance table below says the same thing in short form.

4. Every string reaching the DOM has a declared posture

A tree is data, but rendering it still writes strings into a document. The language tier publishes a per-seam sanitisation contract covering text content, markdown bodies, attribute values, attribute names, URL props, and the Navigate route: which seam is escaped by the view layer, which is sanitised explicitly, which is host-trusted, and where each check lives in the source.

Two parts of it are worth singling out because they are the ones most often assumed rather than checked. URL-valued slots are scheme-allow-listed by default deny, with protocol-relative destinations rejected in all four spellings a browser folds into an off-origin authority, including the backslash forms. That one is not a host convention: section 19 of the specification makes it a normative obligation on every rendering host, precisely because a per-host choice meant a tree vetted on one host was not thereby safe on another. And attribute names are gated by a positive character allowlist rather than a prefix rule alone, because HTML has no escape for an illegal character in an attribute name: a key that terminates its own name is not a mangled attribute, it is several attributes, one of which can be a live event handler.

The artefacts are section 19 of the specification for the URL floor, and the language tier's sanitisation contract for the rest, which also states, per seam, what its markdown sweep does not catch and why that is deliberate.

5. Capabilities are denied unless the host grants them

An emitted tree can express an intent to reach the host, to navigate, to write state, or to invoke a host-registered tool, but that intent stays inert until a policy gate matches it against an allow-list the host controls. The absence of an explicit permission is a denial rather than a gap, so an emission nobody anticipated is refused rather than permitted by omission.

That default was inverted in release 0.14.0 of the .NET host, and it is worth saying why rather than only that it happened. Until then the shipped runtimes returned "allow" from the gate, on the reasoning that the renderer is not the trust boundary and the host's own gate is. That reasoning was self-defeating: those runtimes are what an unconfigured host receives, so for an unconfigured host "the host's gate" was exactly the line returning allow. The same release closed the descriptor set, since four wire-survivable actions had been reaching their substrates without consulting the gate at all. Release 0.15.0 followed with the guest-mount boundary: a decoded mount is clamped to an outbound-only channel, a guest with no host-installed policy receives a runtime that refuses every capability, and the custom-renderer registry is scoped so a tree cannot select a renderer registered for a different surface.

Both are recorded as breaking changes, with the argument for each, in the .NET host's stability declaration.

Where each property holds today

The properties above are properties of the format and of the reference implementation. They are not yet uniform across the host roster, and a page that said "Fuaran guarantees" would be describing a plan rather than a state. What follows is the state.

PropertyF# (reference)TypeScriptPythonGoRustSwift, Kotlin
Closure-free decode, no callable reconstructedEnforcedEnforcedEnforcedEnforcedEnforcedInherited from the Rust core
Typed refusals for semantic violationsEnforcedEnforcedEnforcedEnforcedEnforcedInherited from the Rust core
Structural limits with a typed refusalEnforcedNot yetNot yetNot assessedNot assessedFollows the Rust core
URL scheme allowlist and protocol-relative rejectionEnforcedEnforcedEnforcedEnforcedEnforcedExposed, not enforced
Markdown and text escaping floorEnforcedEnforcedEnforcedEnforcedEnforcedNot applicable
Attribute-name character allowlistEnforcedPrefix rule onlyPrefix rule onlySeam absentPrefix rule onlyNot applicable
Dispatch gate denies by defaultEnforcedSeam present, absent gate allowsNo gateNo gateNo gateEmbedding app owns dispatch
Guest-mount clamp and scoped renderer registryEnforcedNot yetNot yetNot yetNot yetNot yet

Five notes, because the cells compress more than they should.

Structural limits are the real gap. The TypeScript parser and the decodeNode entry point wrap the walk in no counter and no catch, so the engine's RangeError escapes the declared result contract as a throw. The Python decoder catches ValueError around JSON parsing, and CPython raises RecursionError on deep nesting, which is not a ValueError, so it escapes the same way. Go and Rust have not been measured, and the specification asks for measurement before either adopts a figure. Until a host adopts the limits, a deeply nested document is a denial-of-service vector against it, and a transport-level request-body cap is the mitigation available to you today. The corpus deliberately carries no limit fixtures yet, because a shared gate landing ahead of the hosts turns every host's build red for a rule none of them has adopted.

The URL floor has a known evasion, and it is in the specification, not only in the implementations. Rule 1 of the floor strips leading and trailing whitespace, and rule 5 then asks whether the first two characters are each a slash or a backslash. Browsers, following the URL standard, strip tab, line feed and carriage return from anywhere in a URL before parsing it. A destination whose two leading slashes are separated by one of those characters therefore passes rule 5 as an ordinary relative reference, and is then reassembled by the browser into an off-origin authority. Every host implements the rule as written, so every host including the reference is affected. The consequence is an off-origin link or an off-origin subresource request that leaks a referrer, rather than script execution: rules 2 to 4 already normalise control characters out of the scheme candidate, so about:blank and about:blank destinations are still refused. It is stated here because it is open, and a host relying on the floor to hold a same-origin intent should know the shape of the hole.

The native surfaces hold a different contract. The Swift and Kotlin surfaces are render projections over the Rust core rather than independent codec hosts, so the decode-side properties are the Rust core's. They render into native views and have no HTML sink, which removes the direct script-injection seam by construction. What that does not remove is the destination seam: a link, an image source, or a navigation route still arrives from the tree, and an app that hands one to a system open call has a scheme-injection sink. Both surfaces therefore ship the URL policy as a function the embedding app calls at the moment it has a resolved value, rather than enforcing it at decode, because a binding may not resolve to a literal until render time. The obligation is the app's, and it is stated in the surface's own source.

The dispatch gate and the mount clamp are reference-tier properties. The TypeScript renderer carries the gate seam, but an absent gate allows, which is the posture the .NET host left in 0.14.0. The other hosts have no gate. If you are running a non-reference host, capability restriction is yours to impose.

The attribute-name seam is host-side, not wire-reachable. The decoder hard-codes the extra-attributes slot to empty, so a decoded emission cannot carry one at all today. The reachable surface is a host or adapter mapping its own untrusted data into a tree. The gate lives at render time rather than at the decoder so that it stays correct if that ever changes.

What this does not cover

Naming the boundary is the point of the section, not a disclaimer at the end of one.

  • It protects the host, not the meaning of the output. The bounded tree says nothing about whether the generated interface is correct, appropriate, or well labelled. A model can emit a wrong number in a perfectly conformant tree. Generation quality and structural safety are different axes, and the evaluation is where the first one is measured.
  • Content-Security-Policy is yours. The renderer sets no headers. A policy, and on a static host the caveat that a meta-tag policy carries no frame-ancestors or referrer directive at all, is the deploying host's to get right.
  • The custom-renderer registry is a host trust boundary. A host may register its own renderer for a component the language tier does not ship. That closure is host-authored code and the host owns and escapes its output; the renderer does not police it. The decoder cannot construct such a closure from the wire, so an emitted tree can only select among renderers the host itself registered, and since 0.15.0 only those registered in the scope it renders under.
  • Transport-level size limits are yours. The structural limits bound depth and count, not total bytes. A size cap cannot express "no more than 24 levels deep", and a structural limit cannot express "no more than 8 MB". They are complements, and only one of them is in the format.
  • DOM clobbering and Trusted Types are out of scope. Author-supplied identifiers that collide with global names are an author trust boundary, and Trusted Types is an opt-in browser surface a consumer wires up in its own runtime.
  • The op-stream hash chain is corruption detection, not tamper evidence. The chain is an unkeyed SHA-256 digest over canonical bytes. It detects accidental corruption and casual alteration, and it will not detect an attacker who can write the store, because anyone who can rewrite an entry can recompute the digests that follow it. Authenticity needs signing, which is a separate seam and is not shipped. Anywhere you see the chain described as tamper evidence, including in this project's own older prose, read it as this instead.
  • There is no published fuzz or totality evidence yet. The totality claim rests on the specification's obligations and on the reference host's implementation of them, not on a published corpus of hostile inputs. A cross-host hostile-input family in the conformance corpus is the artefact that would settle it, and it does not exist today.

Every claim on this page names something you can read: a normative section, a published contract, a corpus, a recorded release decision. Where a claim would have to rest on a test rather than an artefact, it is not made here.

Reporting a concern

If you find a way that emitted output could reach code execution, unbounded cost, or a capability the host did not allow, that is the finding worth having and we want it. Report it privately through the relevant repository's security policy under the fuaran-ui organisation rather than in a public issue. Please allow a reasonable window for a fix before disclosure.

See also