Fuaranfuaran

The source for generative UI

where its budget is defined. All four now carry a committed long-run record of their own on one documented seed, with no counterexample, beside a note saying how to regenerate it: TypeScript, Go and Python at 250,000 iterations, and Rust at 50,000, the figure its heavier per-thread allocation accounting sets. The Rust record is the run after a finding, and the finding is worth keeping on this page: the first run on that seed found six half-megabyte flat objects of tens of thousands of members decoding in fifteen to sixteen seconds against a three-second budget, where the other hosts' worst decode on the same families is under half a second. The inputs sat inside the format's width bound of 100,000 members; the cost was the parser's duplicate-member check, which scanned every earlier member for each new one. A set of the keys seen made it linear, with the refusal byte-identical, and the same seed then completed in 100 seconds where it had taken 39 minutes. A long run that finds nothing is evidence; one that finds a denial-of-service path and is repeated after the fix is the evidence working.


title: Security description: Why an emitted Fuaran interface is bounded data rather than code, the public artefact behind each claim, which host enforces which property today, and what the host still owns. layout: page nav-order: 60

Security

Last reviewed: 7 September 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. How the floor decides is the load-bearing part. It first normalises the string exactly as the URL standard's parser does, removing leading and trailing C0-or-space and then every tab, line feed and carriage return, so the string it inspects is the string the browser will parse rather than one a language's native trim happened to produce. That is what makes "all four spellings" true of the reassembled destination and not merely of the literal bytes. None of this is 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.

Attribute names, the second of the two, 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.

Since August 2026 the floor is also a corpus family rather than a per-host test: the conformance corpus carries a hostile-input family stated as semantic invariants (refuse this payload, render this one inert, keep this harmless one intact), and each of the five codec hosts runs it in its own suite. Building it found the attribute-name defect still open in the TypeScript, Python and Rust hosts after the reference host had closed it, and each was closed in the commit that adopted the family, which is the corpus doing the work the table below had only predicted.

Since September 2026 the two web renderers also meet a browser-enforced form of the same contract. Every raw-HTML sink in the .NET client renderer and the TypeScript renderer (the markdown body, the drawing, sparkline and chart SVG, the MathML container, the theme element and the maths enhancement) mints its value through one named Trusted Types policy, fuaran-renderer, whose only creator applies the renderer's own sanitiser. A host that sends require-trusted-types-for 'script'; trusted-types fuaran-renderer has the browser refuse any string that reaches those sinks another way. The floor runs on every path whether or not the browser enforces, so a host without the directive, a browser without the API and the server renderer all emit the same bytes; what the directive adds is refusal rather than reliance on review. Building it found a defect the sweep had carried since it first ran over SVG: dangerous element names were matched as bare prefixes, so a drawing's provenance metadata element read as meta and lost its opening tag. Both tiers now require a tag-name boundary, pinned in both directions. The Trusted Types section of the contract states the policy, what it wraps, and what it does not: the custom-renderer registry stays the host's, and the server renderer emits a string and has no sink to guard.

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.

6. What an agent is told carries its provenance

An interface a model operates is also an interface a model reads, and text bound from data is content an attacker may have written. That is prompt injection's entry into the agent seam, the vector specific to generative UI. The agent tools already tokenised where a bound value came from; since September 2026 every text-valued slot in the snapshot carries a textProvenance entry beside the props, naming it literal, i18n or bound with its binding source, and a derived untrusted flag on text resolved from a query, a selection, state or a computation. The surface marks bound text and never returns its resolved string, so the reading surface the mark warns about is not added by the mark. The tool contract states the obligation in as many words: text marked untrusted is content the interface displays, not an instruction to the agent. The same tokens reach an agent over the MCP transport through the inspect tool.

The artefact is the authoring guide's provenance section, and the proof tests assert that an instruction-shaped payload in a bound heading is absent from the response, which is a stronger claim than its presence marked would have been.

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
URL-standard normalisation before the positional testEnforcedEnforcedEnforcedEnforcedEnforcedExposed, not enforced
Markdown and text escaping floorEnforcedEnforcedEnforcedEnforcedEnforcedNot applicable
Attribute-name character allowlistEnforcedEnforcedEnforcedSeam absentEnforcedNot 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
Trusted Types policy over every raw-HTML DOM sinkEnforced (client renderer)EnforcedNot applicable, string emitterNot applicable, string emitterNot applicable, string emitterNot applicable, native views
Text provenance and the untrusted flag in the agent toolsEnforcedEnforcedNo agent toolsNo agent toolsNo agent toolsNo agent tools
Attested-document envelope, verification as a host obligationEnforcedNot 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's normalisation evasion is closed. Rule 1 of the floor used to strip leading and trailing whitespace, and rule 5 then asked whether the first two characters were each a slash or a backslash. Browsers, following the URL standard, remove tab, line feed and carriage return from anywhere in a URL, and leading and trailing control characters generally, before parsing it. A destination whose two leading slashes were separated by one of those characters passed rule 5 as an ordinary relative reference and was then reassembled by the browser into an off-origin authority. The defect was in the rule text rather than in any implementation, so every host including the reference was affected identically.

Rule 1 is now the URL standard's own two-step normalisation, ASCII-exact: remove leading and trailing C0-or-space, then remove every tab, line feed and carriage return from what remains. The floor therefore inspects the same string the browser will parse, which closes the class rather than the instance: a later positional rule cannot reinherit the gap, because the specification now defines positional tests over the normalised form. All five hosts implement it identically, and the shared conformance corpus carries the cases as semantic invariants that each host runs in its own suite.

Two consequences worth stating plainly. The normalised value is what a host now emits, so an accepted destination carrying an interior tab is emitted without it, a change in bytes made deliberately, because that is what the browser would have parsed. And going ASCII-exact loosens one case correctly: a leading non-breaking space is no longer stripped, so the floor sees an ordinary path and accepts, which is what the browser does with it too.

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 is out of scope, and the Trusted Types directive is yours to send. Author-supplied identifiers that collide with global names are an author trust boundary. The renderers now supply the policy that claim 4 describes, but a policy nobody requires enforces nothing: the two directives go in your header, beside your Content-Security-Policy.
  • The bare op-stream hash chain is corruption detection, not tamper evidence; signing is opt-in and covers two artefacts. 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. What a host may add is a signature: a segment attestation, an ECDSA signature over a range of op-stream records at a chain position, and since September 2026 a document attestation, the same key model and curve over an emitted tree's canonical bytes together with claims such as a model identity and a prompt digest, in an envelope outside the tree so no existing fixture moves. Section 26 of the specification makes verification a host obligation and names what the envelope proves: the holder of the named key signed these bytes with these claims. It does not prove the claims are true, does not defend against the key holder, and does not prove a user authored anything. The reference host implements both; the other hosts do not yet. The crypto contract carries the full statement, and an older sentence on this page that called signing unshipped was wrong from the day segment attestation landed.
  • The long-run fuzz record exists for the reference decoder only. The totality claim now rests on published evidence as well as on the specification. The reference host's decoder-robustness report is regenerated by the run that produces it; its latest long run fed 500,000 generated inputs to both decode entry points with no escaping exception, no hang and no allocation-budget breach, and records one open finding rather than omitting it. Since August 2026 the TypeScript, Python, Go and Rust hosts each carry a fuzz leg of the same five input families in the suite their pull-request gate already runs, with the structured-error-only invariant. Three of them measure allocation differently from the reference, and each says so where its budget is defined. Three of them now carry a committed long-run record of their own, each from 250,000 iterations on one documented seed with no counterexample, beside a note saying how to regenerate it: TypeScript, Go and Python. The Rust host's run on the same seed is not cited, because it found something: six inputs from the harness's wide-object family, flat objects of tens of thousands of members at about half a megabyte, decode in fifteen to sixteen seconds against a three-second budget, where the other hosts' worst decode on the same families is under half a second. The inputs sat inside the format's width bound of 100,000 members, so this was not a gap in the limits but a Rust-parser path: its duplicate-member check scanned every earlier member for each new one, quadratic in width, and the ceiling fired only after the scan. The fix is a set of the keys seen, landed the next day, with the refusal byte-identical; the long run is being repeated on the same seed and its record is cited here once it carries no counterexample. Until then this page states the finding rather than citing a record that carries one.

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