restage_a2ui 0.1.1
restage_a2ui: ^0.1.1 copied to clipboard
Fail-closed pre-render capability checks and the Restage capability sidecar for apps rendering cached A2UI (Google genui) surfaces.
restage_a2ui #
The app-side half of Restage's A2UI emit target: a fail-closed, pre-render capability check plus a capability sidecar for cached A2UI payloads.
Restage's build-time toolchain can emit your widget catalog as an A2UI component catalog (a catalog of
widget schemas a generative-UI model renders against, via Google's genui
SDK). restage_a2ui is what an app uses on the other end: it checks an A2UI payload against the catalog
the app actually registered before handing it to genui, so a payload your build can't render faithfully
fails with a clean, actionable diagnostic instead of throwing mid-render.
What this is — and is not #
- It is an app-side safety wrapper: a pre-render check + a capability sidecar you can wrap your cached payloads in.
- It is not an A2UI delivery mechanism. It does not fetch, host, or stream A2UI — your app (or your model session) owns that. It does not generate the catalog either; that is the build-time toolchain.
- It depends on
genui. Your app already depends ongenuito render A2UI, so this adds no new render stack — only the check.
Quickstart #
Add the dependency:
dependencies:
restage_a2ui:
path: packages/restage_a2ui # not yet published to pub.dev
genui: ^0.9.2
Build the check once (it is immutable — reuse it for every payload), then gate each cached payload before handing it to genui:
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:genui/genui.dart';
import 'package:restage_a2ui/restage_a2ui.dart';
// 1. The genui catalog your build emitted — the `CatalogItem` set genui renders
// against. `buildRestageCatalogItems()` is generated by the toolchain (see
// "Producing the catalog and stamp" below).
final catalog = Catalog(buildRestageCatalogItems());
// 2. What that catalog PROVIDES, parsed from the `restageCapability` block the
// toolchain emits next to the catalog. This is the available side of the
// version check; supply it so Restage-stamped payloads can be verified.
final installed = A2uiInstalledCapability.fromStampJson(restageCapability);
// 3. The check — one instance, reused.
final check = RestageA2uiPreRenderCheck(catalog: catalog, installed: installed);
// 4. Gate every payload before render. `check.check` accepts either a raw A2UI
// payload or a Restage sidecar wrapping one.
Widget? renderCached(String cachedJson) {
final cached = jsonDecode(cachedJson) as Map<String, Object?>;
switch (check.check(cached)) {
case A2uiRenderable():
// Safe to render. If you cached the sidecar (recommended — it carries the
// version stamp), unwrap it and hand the inner payload to genui:
final payload = RestageA2uiSidecar.isRestageSidecar(cached)
? RestageA2uiSidecar.fromJson(cached).a2ui
: cached;
return renderWithGenui(payload); // your genui render call
case A2uiRejected(:final diagnostic, :final gap):
// Do NOT render. Fall back to a built-in surface and log why.
debugPrint('A2UI rejected: $diagnostic${gap == null ? '' : ' ($gap)'}');
return null;
}
}
The check fails closed at every path: a malformed envelope, an unknown component, an unmet version, or
a stamped payload with no installed descriptor all yield an A2uiRejected — never a throw at the render
seam.
If you do not cache the sidecar and only have raw A2UI payloads, you can omit installed; then only the
existence walk runs and any Restage-stamped payload is rejected as unverifiable (fail-closed). Caching the
sidecar is what enables the version check.
Producing the catalog and stamp #
The two inputs above — buildRestageCatalogItems() and the restageCapability block — are emitted by
Restage's build-time toolchain (restage_codegen), which projects your widget catalog (the built-ins
plus any @RestageWidget libraries) into two artifacts:
| Artifact | What it is | Who consumes it |
|---|---|---|
The generated CatalogItem Dart (buildRestageCatalogItems()) |
the functional contract genui renders against — one CatalogItem per widget, with its data schema and widget builder |
genui, at render time |
The stamped catalog document ({ restageCapability, a2uiCatalog }) |
the A2UI catalog JSON plus the two-axis capability stamp (built-in floor + custom-library versions) | this package's check (the restageCapability block → A2uiInstalledCapability.fromStampJson) |
Both are emitted over the same A2UI-emittable widget set, so they agree by construction — a widget the emitter scopes out is absent from both.
The emit entrypoints in restage_codegen are emitA2uiCatalogDart(catalog) (the CatalogItem Dart source)
and emitA2uiCatalog(catalog).toJson() (the stamped { restageCapability, a2uiCatalog } document). A2UI is
an opt-in emit target: a turnkey CLI/build wrapper is still landing (see Status), so today the emit is
driven through these toolchain APIs. RFW remains Restage's native delivery path; A2UI emission is additive.
Why a pre-render check #
genui resolves a payload's component types against the catalog you registered at render time. A
component the catalog lacks throws CatalogItemNotFoundException part-way through building the surface. And
a payload your app cached against an older catalog can reference a component whose shape has since
changed — a drift the app, not genui, owns (genui has no built-in payload cache; you cache the serializable
A2UI JSON yourself).
restage_a2ui moves that check before render and makes it explicit:
- Existence walk (any payload). Every component type the payload references must exist in the catalog you registered. Works for any A2UI payload — including one generated live by a model — and catches a missing component before genui would throw.
- Version satisfaction (Restage-stamped payloads). A payload wrapped in a Restage sidecar carries the catalog version it was generated against. The check verifies your installed catalog meets that version — across both the built-in widgets and any custom widget libraries — so a payload that needs more than your build provides is rejected up front rather than rendered wrong.
Both fail closed: a malformed payload, an unknown component, or an unmet version yields a Rejected
result with a diagnostic. The check never throws at the render seam.
The capability sidecar #
A2UI's envelope has no place for a per-payload version stamp, so Restage wraps a cached payload:
{
"restageCapability": {
"builtInFloor": 2,
"requiredLibraries": [{ "namespace": "acme.widgets", "minVersion": 3 }],
"perItemSinceVersion": { "Text": 1, "AcmeBanner": 3 }
},
"a2ui": { "...": "the A2UI payload" }
}
Cache the wrapper; run the check on it before rendering a2ui. The version comparison rests on the
catalog's cumulative-render-support invariant: an incompatible change to a component forks a new identity, so
an existence walk plus a version compare together are sound. (Without the stamp, name-existence alone is
not — a same-name shape change would slip through.)
App Review #
A2UI emission is declarative-data-only — a catalog of widget schemas plus version metadata, no server-shipped executable code. This package adds only a pre-render check over that data.
Status #
Pre-1.0, tracking genui ^0.9.2 (A2UI protocol v0.9). genui is alpha and its API is expected to change;
this package isolates the integration so a churn moves one place.