genui

package
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 1, 2026 License: MIT Imports: 21 Imported by: 0

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

View Source
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.

View Source
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.

View Source
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).

View Source
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.

View Source
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

func UIHostOption() uihost.Option

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.

func WithActionsContext

func WithActionsContext(ctx context.Context, actions []string) context.Context

WithActionsContext returns ctx carrying the action allow-list. The plugin calls this before invoking a Composer; a host calling a Composer directly can use it too.

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

type Composition struct {
	SchemaVersion string `json:"schemaVersion"`
	Root          *Node  `json:"root"`
}

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

func WithActions(actions ...string) Option

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

func WithCapabilities(caps ...string) Option

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

func WithComposer(c Composer) Option

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

func New(opts ...Option) *Plugin

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) Actions

func (p *Plugin) Actions() []string

Actions returns the action allow-list this instance enforces (copy).

func (*Plugin) Capabilities

func (p *Plugin) Capabilities() []string

Capabilities returns the grant set this plugin advertises to the frame.

func (*Plugin) Init

func (p *Plugin) Init(app *framework.App) error

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

func (*Plugin) Name

func (p *Plugin) Name() string

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.

func (Registry) Lookup

func (r Registry) Lookup(name string) (ComponentSpec, bool)

Lookup returns the spec for name. ok is false for anything not in the registry — which is exactly the case Validate refuses.

func (Registry) Names

func (r Registry) Names() []string

Names returns every registered component name in declaration order. The frame's registry must name the same set; the drift test pins that.

type ValidationError

type ValidationError struct {
	Code    string
	Path    string
	Message string
}

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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL