Documentation
¶
Overview ¶
Package genui is the GoFastr generative-UI plugin: a model composes a view out of a FIXED registry of React components, and nothing else.
The bounded registry is the entire containment story. A composition is a tree of {component, props, action?, children?} where the component MUST name a registry entry, the props MUST match that entry's declared schema exactly (unknown key → rejected, wrong type → rejected; there is no passthrough, no style, no className, no dangerouslySetInnerHTML — no entry declares them), an optional action MUST name an entry in the host-supplied allow-list, and children only exist where the entry accepts them. Depth is bounded (16) and node count is bounded (200): a model that emits a runaway tree fails validation, never the renderer.
Where the model runs: in Go, never in the frame. The composition is produced HOST-side and arrives at the frame already validated; the frame renders a tree and never talks to a model, holds a key, or opens a socket — the framed CSP still says connect-src 'none'. That direction is the design: an API key in a browser is not a key, and a frame that could call a model could exfiltrate the document it was composing over.
only thing ever served. The frame validates again before rendering, because "the host already checked it" is exactly the assumption that makes a second bug fatal; its copy is cheap (same rules, same registry, no trust in the bridge).
Async by design: POST /compose starts a generation and returns an id immediately; the host polls GET /composition/{id} and pushes the finished tree over the bridge when it is ready. No streaming tokens into the DOM.
Capabilities: genui:compose and theme:read. The registry, the allow-list and the composer all live host-side, so the frame's grants stay minimal. pluginhost.Allow is a capability gate, NOT authentication: it passes for anonymous callers, so a host that persists or acts on compositions for real must check the session in its own wrapper — WithDevGrantAll skips the gate entirely and MUST NOT survive into a production mount.
Index ¶
- Constants
- func DefaultActions() []string
- func DefaultCapabilities() []string
- func Mount(cfg MountConfig) render.HTML
- func UIHostOption() uihost.Option
- func Validate(c Composition, reg Registry, actions []string) error
- func WithActionsContext(ctx context.Context, actions []string) context.Context
- type AnthropicComposer
- type AnthropicConfig
- type ComponentSpec
- type Composer
- type Composition
- type FixtureComposer
- type MountConfig
- type Node
- type Option
- type Plugin
- type PropSpec
- type PropType
- type Registry
- type ValidationError
Constants ¶
const ( // DefaultAnthropicModel is the model used when none is configured. DefaultAnthropicModel = "claude-sonnet-5" // DefaultAnthropicEndpoint is the Messages API endpoint. DefaultAnthropicEndpoint = "https://api.anthropic.com/v1/messages" )
The Anthropic Messages API composer.
Written against the HTTP API with net/http rather than an SDK. The registry, the schema and the retry loop are the whole client; an SDK would add a dependency to this module and a version to track for about forty lines of request building. If a host wants the SDK it can implement Composer itself — that is what the interface is for.
The model NEVER produces UI. It produces a composition tree naming registry components, which is then put through exactly the same Validate every other composition goes through. A model that emits something outside the registry gets the refusal text back and one more attempt; a model that fails twice fails the request. There is no path where unvalidated model output reaches a frame, and no path where a validation failure is "fixed up" instead of refused — quietly repairing a bad composition would teach nobody anything and hide the case this plugin exists to demonstrate.
const ( Name = "genui" Version = "0.1.0" RoutePrefix = "/__gofastr/plugin/genui" FrameHTMLURL = RoutePrefix + "/genui.html" GenuiJSURL = RoutePrefix + "/genui.js" GenuiCSSURL = RoutePrefix + "/genui.css" AdapterScriptURL = RoutePrefix + "/adapter.js" ConfigScriptURL = RoutePrefix + "/config.js" ComposeURL = RoutePrefix + "/compose" // CompositionBaseURL carries the per-id route // CompositionBaseURL + "/{id}". CompositionBaseURL = RoutePrefix + "/composition" DemoURL = "/genui" SchemaVersion = "genui-v1" // CapCompose gates both routes: starting a generation and reading one // back. The frame never calls either (connect-src 'none'); the // privileged host adapter does, on the page's authority. CapCompose = "genui:compose" )
Identity and route constants. Both this plugin and host/adapter.js hard-code these exactly. The demo lives at /genui.
These mirror the genui row in plugins.json (added by the coordinator); internal/registry tests pin Name + RoutePrefix against that row, so they MUST NOT drift.
const ( // MaxStringRunes caps any single string prop value. MaxStringRunes = 500 // MaxArrayLen caps the length of a "strings" prop and the number of rows // in a "rows" prop. MaxArrayLen = 64 // MaxRowLen caps the cells in one row of a "rows" prop. MaxRowLen = 64 )
Bounds on prop VALUES. The depth (16) and node-count (200) bounds cap the tree; these cap the payloads inside a single node, so a hostile or runaway "Table" cannot smuggle a megabyte of string per cell past a validator that only counted nodes. The frame's copy caps only depth and nodes; Go being stricter here is the safe direction (a tree Go accepts always passes the frame).
const ( // MaxDepth is the deepest node allowed, root = depth 1. MaxDepth = 16 // MaxNodes is the total node count allowed, root included. MaxNodes = 200 )
Composition bounds. A model that emits a runaway tree must fail VALIDATION (here, with a path), never the renderer.
const ( ErrBadVersion = "E_BAD_VERSION" ErrNoRoot = "E_NO_ROOT" ErrUnknownComponent = "E_UNKNOWN_COMPONENT" ErrUnknownProp = "E_UNKNOWN_PROP" ErrPropType = "E_PROP_TYPE" ErrPropValue = "E_PROP_VALUE" ErrRequiredProp = "E_REQUIRED_PROP" ErrChildren = "E_CHILDREN" ErrAction = "E_ACTION" ErrDepth = "E_DEPTH" ErrNodeCount = "E_NODE_COUNT" )
Error codes, so routes, tests and the frame's status line branch on a stable vocabulary instead of parsing free text.
Variables ¶
This section is empty.
Functions ¶
func DefaultActions ¶
func DefaultActions() []string
DefaultActions is the default action allow-list: the actions a generated Button may name. A generated button cannot point anywhere the host did not name; override with WithActions when the host's vocabulary differs.
func DefaultCapabilities ¶
func DefaultCapabilities() []string
DefaultCapabilities is the grant set advertised to the frame: composing and bridging theme tokens. There are deliberately no optional capabilities — the frame has no write surface at all.
func Mount ¶
func Mount(cfg MountConfig) render.HTML
Mount renders the generic mount marker. A genui mount has no hidden form field — nothing round-trips on submit; the marker is the whole mount and the adapter drives the frame from the page. All interpolated values are HTML-escaped inside pluginhost.MountMarker.
func UIHostOption ¶
UIHostOption injects the platform broker, this plugin's config script, and this plugin's adapter (in that order — the adapter reads the config global the config script publishes, and registers with the broker the former defines).
func Validate ¶
func Validate(c Composition, reg Registry, actions []string) error
Validate enforces every genui-v1 invariant against the given registry and host-supplied action allow-list. A nil error means the composition is safe to persist and safe to render. Rejections are deterministic (prop keys are walked in sorted order) so the same bad tree always yields the same path.
Types ¶
type AnthropicComposer ¶
type AnthropicComposer struct {
// contains filtered or unexported fields
}
AnthropicComposer composes through the Anthropic Messages API.
It is NOT the default: FixtureComposer is, so the demo and the whole test suite run with no credentials. A plugin whose tests need an API key is a plugin nobody can contribute to. Wire this one deliberately:
genui.New(genui.WithComposer(genui.NewAnthropicComposer(genui.AnthropicConfig{})))
func NewAnthropicComposer ¶
func NewAnthropicComposer(cfg AnthropicConfig) *AnthropicComposer
NewAnthropicComposer builds a composer over the Messages API.
func (*AnthropicComposer) Compose ¶
func (a *AnthropicComposer) Compose(ctx context.Context, prompt string, r Registry) (Composition, error)
Compose asks the model for a composition and refuses anything the validator refuses. Implements Composer.
type AnthropicConfig ¶
type AnthropicConfig struct {
// APIKey authenticates the call. Empty means read ANTHROPIC_API_KEY at
// COMPOSE time, not at construction: a host that builds its plugin graph
// before its secrets are loaded should still work.
APIKey string
// Model defaults to [DefaultAnthropicModel].
Model string
// Endpoint defaults to [DefaultAnthropicEndpoint]. Tests point this at an
// httptest server, which is why this composer needs no key to be tested.
Endpoint string
// MaxTokens bounds the response. Compositions are small; the default is
// deliberately modest so a runaway generation costs little.
MaxTokens int
// HTTPClient defaults to a client with a 60s timeout.
HTTPClient *http.Client
// Retries is how many EXTRA attempts a refused composition gets, each one
// carrying the validator's complaint back to the model. Default 1: one
// correction is worth having, an unbounded loop is a bill.
Retries int
}
AnthropicConfig configures AnthropicComposer. The zero value reads the key from ANTHROPIC_API_KEY and uses the default model, endpoint and timeout.
type ComponentSpec ¶
type ComponentSpec struct {
// Name is the component key a composition's node must carry.
Name string
// Props are the declared props, in declaration order.
Props []PropSpec
// AcceptsChildren reports whether a node of this component may carry a
// children array at all (children on Text is refused, not ignored).
AcceptsChildren bool
// CarriesAction reports whether a node of this component may carry an
// action at all. True for Button only: a generated control is the one
// thing in a composition with behaviour, and the allow-list still gates
// whichever name it carries.
CarriesAction bool
}
ComponentSpec declares one registry entry.
type Composer ¶
type Composer interface {
Compose(ctx context.Context, prompt string, r Registry) (Composition, error)
}
Composer produces a composition for a prompt. Implementations run host- side, may take arbitrarily long (the route polls), and must expect their output to be validated against r before anything persists — a Composer never gets to bypass the registry by construction.
type Composition ¶
Composition is the canonical genui-v1 document.
type FixtureComposer ¶
type FixtureComposer struct{}
FixtureComposer is the deterministic offline default: keyword matching over a fixed fixture table, then a fallback card for anything else. No network, no key, no randomness — the same prompt always yields the same tree, which is what makes the demo and the tests reproducible.
func (FixtureComposer) Compose ¶
func (FixtureComposer) Compose(_ context.Context, prompt string, _ Registry) (Composition, error)
Compose maps prompt to a fixture composition. The prompt is normalized (lowercased, whitespace-collapsed) before keyword matching; anything that matches no row gets the fallback card, never an error — "I did not understand that" is a renderable answer, a failed generation is not.
type MountConfig ¶
type MountConfig struct {
// DocID is the genui identity for this mount (logging/debug key; a
// composition is ephemeral state, not a persisted doc).
DocID string
// MinHeight is the composition viewport height. Defaults to 460px.
MinHeight string
// Capabilities is an optional CSV grant override.
Capabilities string
}
MountConfig configures Mount.
type Node ¶
type Node struct {
Component string `json:"component"`
Props map[string]any `json:"props,omitempty"`
Action string `json:"action,omitempty"`
Children []Node `json:"children,omitempty"`
}
Node is one element of a composition tree: a registry component, its declared props, an optional host-allow-listed action, and children only where the component declares them. There is deliberately no field for markup, style, classes or code — a node is data the renderer interprets, never input it executes.
type Option ¶
type Option func(*Plugin)
Option configures a Plugin.
func WithActions ¶
WithActions overrides the action allow-list (default: DefaultActions). Every action named in a composition must be in this list or the composition is refused at validation; the same list is published to the frame via config.js so both sides enforce one vocabulary. An empty or duplicated name panics in New: a typo'd allow-list entry silently never matches, which is the worst way to learn about it.
func WithCapabilities ¶
WithCapabilities overrides the grant set advertised to the frame. Default: DefaultCapabilities. There is nothing to expand into — the frame has no optional capabilities — but the override exists for hosts that mint scoped tokens ("genui:*" implies genui:compose under the framework's wildcard grammar, and the runtime gate matches it).
func WithComposer ¶
WithComposer swaps the composition producer (default: FixtureComposer, deterministic and offline). This is the seam a real model client goes behind later; its output is validated exactly like any other composer's.
func WithDemoPage ¶
func WithDemoPage() Option
WithDemoPage registers the self-contained themed demo page at DemoURL.
func WithDevGrantAll ¶
func WithDevGrantAll() Option
WithDevGrantAll short-circuits the capability gate (demo / tests). Both routes stay gated for real mounts; production hosts never set this.
type Plugin ¶
type Plugin struct {
// contains filtered or unexported fields
}
Plugin is the generative-UI plugin. It implements framework.Plugin and mirrors the scanner shape (opaque-origin sandboxed iframe, protocol v1 over postMessage, go:embed'd frame bundle) with the genui inversion: the EXPENSIVE side (the model) runs host-side behind Composer, and the frame only ever renders trees that already passed Validate.
func New ¶
New constructs a Plugin. The platform manifest is built and Validate()'d here, and the action allow-list is fail-loud validated, so a bad isolation/sandbox config or a typo'd action aborts construction rather than surfacing as a composition that can never validate.
func (*Plugin) Capabilities ¶
Capabilities returns the grant set this plugin advertises to the frame.
func (*Plugin) Init ¶
Init registers every asset and route on the app's router. The frame's assets are framed (AssetServer applies the framing/CORP/CSP relaxation and the fixed framedCSP — connect-src 'none', sandbox allow-scripts — to exactly those); the adapter and config.js are host-page scripts. The two data routes are capability-gated on CapCompose; see handlers below for the auth posture.
func (*Plugin) Manifest ¶
func (p *Plugin) Manifest() pluginhost.Manifest
type PropSpec ¶
type PropSpec struct {
// Name is the prop key. Anything else in a node's props object is an
// unknown prop and the composition is refused.
Name string
// Type is the value shape; see the PropType constants.
Type PropType
// Required means the prop must be present. An optional prop may be
// absent, but present-but-null is still a wrong type, not absent.
Required bool
// Enum, when non-nil, is the closed vocabulary for a PropString. A value
// outside it is refused (gap: "huge" is not a layout the renderer knows).
Enum []string
// Min/Max, when non-nil, bound a PropInt/PropNumber value.
Min, Max *float64
}
PropSpec declares one prop of one component.
type PropType ¶
type PropType string
PropType is the closed vocabulary of prop value shapes a component may declare. Values arrive as decoded JSON (map[string]any), so "number" means a float64 and "int" means an integral number — JSON has no integer type to decode into. Go-built compositions may equally hand us an int literal; the validator accepts both as numbers.
const ( // PropString is a plain string value. PropString PropType = "string" // PropInt is a number that must be integral (level: 2, not 2.5). PropInt PropType = "int" // PropNumber is any finite number (delta: 12.5). PropNumber PropType = "number" // PropStrings is an array of strings (Table columns). PropStrings PropType = "strings" // PropRows is an array of string arrays (Table rows). PropRows PropType = "rows" )
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry is the fixed component table. It is built once (DefaultRegistry) and treated as immutable; Compose receives it, the validator reads it, and nothing mutates it, so a value copy is safe to hand to untrusted composer code.
func DefaultRegistry ¶
func DefaultRegistry() Registry
DefaultRegistry returns the registry of the eight genui-v1 components: Stack, Card, Heading, Text, Stat, Badge, Table, Button. The schemas below mirror the frame's registry (genui/js/src/registry.tsx) exactly: Stack states its geometry (gap and direction both required), Heading's level is the closed 1|2|3 set, Badge's tones are neutral|good|warn|bad, and Button is the only entry that carries an action.
type ValidationError ¶
ValidationError is a validation refusal: a stable machine-readable code, the offending path, and a human sentence. The path is the contract — tests pin it per rule.
func (*ValidationError) Error ¶
func (e *ValidationError) Error() string