What's new
read as.mdThe GGUI protocol is a pre-1.0 draft (currently draft-2026-09-10) — it can still change. This page tracks releases and protocol-affecting changes as they ship. For the semver rules behind what counts as breaking, see Version policy.
Unreleased — next: v0.27.0
Section titled “Unreleased — next: v0.27.0”- Changed, in
@ggui-ai/ui-gen(#1711): the visual judge is no longer askedstate.feedbackon a capture with no post-action state. Its own text makes it not evaluated then, and a judged frame is one capture taken before any action. The row now readsn/a(“not evaluated: no post-action capture”) and stays in the block’s verdicts, so it is counted as considered. With a post-action capture (postActionCapture), the judge is asked as for any row.REQUIRES_POST_ACTION_CAPTUREnames the rows this applies to. Report-only; no score changes. - Changed, in
@ggui-ai/design(#1567): low-emphasis text reads as text in every shipped theme. Thesubtletone now resolves to the theme’s muted ink (onSunken), likemuted; ten components that painted hint text withneutral-500(Autocomplete, Breadcrumb, FormField, ChatWindow, CommandPalette, CommentThread, DataTable, FileUploader, Footer, NotificationCenter) use the tone, so a theme shows one muted grey; NotificationCenter’s unread row sits on the sunken ground and its timestamps use the tone; and the default theme’s muted ink is darker in light (#596273) and lighter in dark (#adb9ca). Every shipped theme’s muted ink now clears 4.5:1 on both the container and the sunken ground. In gguiDark, hint text goes from 4.36:1 to 5.80:1 on sunken; gguiLight is unchanged. The tone keeps its name, so code that asks forsubtleneeds no change. - Fixed, in
@ggui-ai/mcp-server-coreand@ggui-ai/mcp-server-handlers(#1339): the host-session pair aggui_renderrequest carries on_meta["ai.ggui/host-session"]is now set once by the first commit that carries it — fill-absent-never-overwrite, the rule the row’s subject already follows — so a session row born before the render (a hosted server creates it at the handshake’s provisional preview) still takes the pair; the in-memory and sqlite stores set it on their replace path, the conformance kit pins it on every store, and a later commit with a different pair is kept as it was and named once in the server’s log (host_session_conflict, ids only). A slice on theggui_handshakerequest is ignored, and the extension doc now says so.
2026-10-02 — v0.26.0: the server refuses a spent one-shot, the SMTP sender moves to nodemailer 10 — upgrade if you embed it — and unused exports go
Section titled “2026-10-02 — v0.26.0: the server refuses a spent one-shot, the SMTP sender moves to nodemailer 10 — upgrade if you embed it — and unused exports go”-
Removed exports (a pre-1.0 minor, VERSION-POLICY §3.7). Before 1.0, a minor release may remove an export without a deprecation window. A caret range such as
^0.25.0never picks up a new minor, so nothing changes for you until you raise the range. Every export 0.25.0 had that 0.26.0 does not:@ggui-ai/negotiator:hashContract,buildVariant;@ggui-ai/mcp-server-core:mintSessionToken,DEFAULT_SESSION_TOKEN_TTL_SEC,nextSpentOneShotsRecord, and the variant selector (selectVariantWithLlm,encodeSelectedReason,preFilterCandidates,computeVariantSelectionCacheKey,VariantSelectionContext,VariantSelectionDecision,VariantSelectionCache,VariantSelectionCacheEntry,VariantSelectionPickFn,VariantSelectionResult,VariantSelectorWithLlmOptions,DEFAULT_VARIANT_SELECTION_CONFIDENCE_THRESHOLD,DEFAULT_VARIANT_SELECTION_CACHE_TTL_SEC,DEFAULT_VARIANT_SELECTION_SHORTLIST_SIZE,BlueprintSelector,BlueprintSelectorContext,createDeterministicBlueprintSelector, and on/in-memoryInMemoryVariantSelectionCache,InMemoryVariantSelectionCacheOptions);@ggui-ai/mcp-server-handlers:GenerationDeps.resolveLlmCaller,HandshakeNegotiator.selectVariant,REFRESH_WINDOW_CLOSEDinGguiRefreshWsTokenOutput’scode, andvalidatorScoreonggui_ops_generate_blueprint’s output;@ggui-ai/mcp-server:GguiSessionChannelBootstrap.issueSessionToken,createGguiServer’sblueprintSelectoroption andGguiServer.blueprintSelector;@ggui-ai/protocol:validatorScoreonopsGenerateBlueprintOutputSchema.
Each is described, with its replacement where it has one, in its own line below.
AckPayload.sessionTokenandTokenKind’s'session'stay one more release. -
Added, in
@ggui-ai/iframe-runtime(#1679): a card whose component throws past the error boundary’s retry is now reported to the server — oneggui_runtime_report_render_failureper session, carrying the phase (mountorupdate), the thrown value’s class name and the boundary’s catch count, with the view proof on the call. Nothing the visitor sees changes, and a report that is refused or lost is never resent. The tool itself has been served since #1609; this is the first runtime that sends it. In@ggui-ai/protocol,renderFailureErrorNamenow recognises anErrorfrom another realm by its brand, so a card module evaluated in the document’s realm reports its class name rather thanError; the report’s vocabulary gains its own pure-const subpath,@ggui-ai/protocol/render-failure, for runtimes that must not bundle the schema modules (the root re-exports the same names). -
Fixed, in
@ggui-ai/mcp-server-handlersand@ggui-ai/mcp-server-core(#1424): the server now refuses a second gesture on a spent one-shot action.ggui_runtime_submit_actionclaims the spend before it appends the gesture, and a later dispatch of the same declaredoneShoton the same card under a differentactionIdanswers{ok:false, code:"CONTRACT_VIOLATION"}with one violation atactionSpec.<name>.oneShotand reaches no agent. Before, the spend was recorded after the append and never changed the answer, so a client that was not the ggui runtime could fire a one-shot twice. The sameactionIdis still a retry; a holder that claimed and never delivered is taken over by the next gesture, which is delivered once. The session store’srecordSpentOneShotnow answers aSpentOneShotClaim(recorded,already-spentwith the holder and its delivery mark,superseded,released). A store that still answers nothing keeps the earlier behaviour, and the conformance kit names it as a skip. The read viewspentOneShotsis unchanged. -
Changed (a pre-1.0 minor, VERSION-POLICY §3.7), in
@ggui-ai/mcp-server-core(#1424):nextSpentOneShotsRecordis removed. Its replacement isclaimSpentOneShotover aSpentOneShotsLedger, which claims the spend instead of recording it after the fact. A custom session store that built its record with the old helper moves to the claim; a store that never recorded spends (itsrecordSpentOneShotis optional) changes nothing. -
Removed (a pre-1.0 minor, VERSION-POLICY §3.7), in
@ggui-ai/negotiator(#1334):hashContractandbuildVariant. This ships in 0.26.0, a minor, and must not ride a 0.25.x patch: a caret range on 0.25.x never picks it up. Neither had a caller.hashContracthashed the intent alone and was never the cache key: everycontractHashon the wire isblueprintKey(contract). Replacements: forhashContract,blueprintKeyfrom@ggui-ai/protocol/blueprint-key; forbuildVariant, none (variantKey, from the same subpath, is the key of a blueprint’s variance and is not the same thing).invokeRequestSchema(@ggui-ai/protocol) stays and now names who speaks it: the host helpers’useInvokesends that body, and its test parses the body with the schema. -
Fixed, in
@ggui-ai/mcp-server-handlersand@ggui-ai/protocol(#1339):ggui_rendernow captures the host’s conversation-grouping slice,_meta["ai.ggui/host-session"](hostName+hostSessionId), on the session it creates, soggui_list_sessionsfiltered by that pair returns the session. The slice, the store field and the list filter were documented and shipped, but no render passed the slice to the store, so the filter matched nothing. The render request is the one that must carry the slice (the handshake creates no session). Each field is now bounded at 256 characters (MCP_APP_HOST_SESSION_FIELD_MAX_LENGTH, new, from@ggui-ai/protocol/integrations/mcp-apps):parseMcpAppAiGguiHostSessionMetareturnsMALFORMED_HOST_SESSIONfor a longer field, and the server treats a malformed slice as absent, with onehost_session_malformedlog line; the render still succeeds. A deployment whose session store persistshostSessionstarts writing it on new rows. -
Fixed, in
@ggui-ai/mcp-server-handlers(#1428): whenggui_handshakeproposes a saved interface found by similarity, it now also compares the members of each action both contracts declare that change what happens on a gesture:oneShot,confirmandnextStep. If the saved interface’s action lacks one your draft declares (or names a differentnextStep), the proposal carries aCOVERAGE_GAPwarning atactionSpec.<name>.<member>, so you can accept it knowing the card will not behave that way, or override. Before, only action names were compared: a saved card withoutoneShotwas proposed for a draft that declared it, with no warning. Reuse stays the default; a proposal with such a gap now ranks below one without any gap. An exact-key match is unaffected (those members are part of the key). -
Fixed, in
@ggui-ai/mcp-server-handlers(#1429): a render whose generator returns nocontractnow commits the contract the handshake agreed, whole. Before, onlypropsSpecwas kept: the session committed noactionSpec,streamSpecorcontextSpec, so a declaredoneShotwas not recorded as spent andggui_update/ggui_amendvalidated against nothing. A returned contract is still taken as given: it is complete, not a patch over the agreed one, andUIGenerationResponse.contract(@ggui-ai/protocol) now says so. When a returned contract leaves out a spec or an entry the agreed contract declares, the server logs onegenerator_contract_narrowerline naming what was dropped. The generator shipped in@ggui-ai/ui-genreturns the agreed contract, so its renders do not change. -
Fixed, in
@ggui-ai/mcp-server(#1562):nodemailermoves from^9.1.1to^10.0.13. That clears two high-severity advisories (GHSA-prgh-xp8r-p3m5 and GHSA-v53p-9fqp-m79j: address parsing that a crafted address can make quadratic, a denial of service) and three moderate ones (GHSA-g57g-f23g-4646, GHSA-8vvx-rff5-p5rq, GHSA-6vj9-mwq6-2f5v). The built-in email login bounds the address it sends to, butSmtpEmailSenderis exported, and an embedder passing its own addresses was exposed. nodemailer 10 needs Node.js 20 or later (this package already requires 22) and ships its own types, so@types/nodemaileris no longer a dependency. -
Changed, in
@ggui-ai/ui-gen(#1652): the runtime-render check looks for each action by walking the card’s screens in a mount of its own, pressing each control once per screen. A wizard’s last step, an Edit button that becomes Save, and a form behind a button are reached, so their actions are verified instead of reported as not dispatched. Screens are told apart by their controls (labels and disabled state), never by typed values or generated names, so Back and Next cannot loop. Nothing bounds it while it is on the first screen with controls left to press there; once a press leaves the first screen, it stops at 60 presses, 3 s per action, or the check’s time budget. An action it cannot dispatch says how far the walk got (RenderCheckIssue.diagnostics.walk), not the first button on the page. The action check no longer presses the copy of the card that prop coverage, prop sensitivity and stream re-render read (it is still primed as before, and only the duplicate-label check still clicks there). Prop sensitivity therefore compares two unpressed renders, and can report a baked prop it used to miss on a card the old check pressed. Prop coverage counts a prop shown on any screen the walk reached. -
Added, in
@ggui-ai/mcp-server-handlers(#1553):renderReadVerdict(from@ggui-ai/mcp-server-handlers/renders) returns the render read gate’s answer with the rung that decided it;renderReadAllowedis its boolean. The two doors that use the gate, theui://render resource read andggui_runtime_refresh_ws_token, now log one structured line,render_read_app_trust_over_subject{door, appId, source}, when they admit a caller with no end-user identity to a session that has one. That is the one case where this gate and the agent doors’ visibility check disagree, logged so its frequency can be measured before the two are unified. No access changes. -
Fixed, in
@ggui-ai/negotiator(#1430): a repaired handshake draft keeps every member the agent declared on each entry it keeps under the same key, on every spec, not only on actions: a stream’smode,replayandcomplete, a prop’sdefault, a context slot’sdebounceMs, a tool’sserverInfo,exampleandtoolInfo.description, and the rest. A tool the repair dropped from the agent’sagentCapabilities.toolsis put back whole and namedREPAIR_ENTRY_RESTORED(severitywarn), so an action’s validnextStepsurvives the repair. A draft entry the repair renamed, moved or removed is named once,REPAIR_ENTRY_DROPPED, on every spec. The repair’sreasoningcounts drops and restored tools apart. -
Fixed, in
@ggui-ai/protocol(#1518): when the self-contained shell (gguiShellHtml) cannot load its runtime bundle, it no longer leaves its loading mark animating. After it postsggui:bootstrap-failed/BUNDLE_FETCH_FAILED, it removes the mark and paints a failure line, “This view could not load”, with a Retry that reloads the frame. That is the wording the thin shell already uses. A host that ignores the envelope, such as a third-party MCP Apps host reading the shell throughresources/read, now shows the failure instead of a spinner. Nothing is painted while the unhashed twin is still being tried. -
Removed, in
@ggui-ai/mcp-server-handlers(#1510, a wire removal under VERSION-POLICY §3.6):ggui_runtime_refresh_ws_token’s advertised output schema no longer namesREFRESH_WINDOW_CLOSEDin itscodeenum, and the exportedGguiRefreshWsTokenOutputno longer types it. No server has sent it since v0.25.0 made the refresh an authorized re-mint (#1496). It stayed declared for that one release, so a client that cached the older schema accepted every answer, and such a client still does. -
Added, in
@ggui-ai/ui-gen(#1640):judgedFrameTokens(from@ggui-ai/ui-gen/evaluation) sums a judged frame’s or a visual evaluation’s spend over every leg the judge reports: the scoring calls’inputTokens/outputTokensand the report-only criteria call’scriteriaTokens. A cost total built on it counts the criteria call, and a token leg added to the judge’s result fails the package’s build until the sum counts it. Additive; the judge’s results are unchanged. -
Added, in
@ggui-ai/protocol(#1381): a@ggui-ai/protocol/runtime-telemetrysubpath with the runtime telemetry vocabulary.RUNTIME_TELEMETRY_KINDSlists every kind a card’s runtime emits, which of the two batches it rides, and the onedetailshape it admits, so the emitter and a host that admits only a closed set import the same names.CHANNEL_LOG_EVENTSandChannelLogEventmove here from@ggui-ai/live-channel, which re-exports them unchanged. The subpath has no runtime imports.@ggui-ai/live-channelnow depends on@ggui-ai/protocol, where it had no dependencies before: installing it alone now installs protocol and its dependencies, while a bundle takes only this subpath’s constants. The server still logs a kind it does not name and never refuses one. -
GguiServer.close()in@ggui-ai/mcp-serveris bounded (#1631). It stops accepting connections and closes idle ones at once, then lets active ones finish for up tograceMs(defaultDEFAULT_CLOSE_GRACE_MS, 5 s, now exported) before it ends every connection still open. So a client that never finishes its request, or never reads a response, can no longer hold a shutdown open. Passclose({ graceMs: Infinity })for the previous unbounded wait. -
Added, in
@ggui-ai/ui-gen(#1663): a report-only alignment measurement. Before each screenshot, the visual evaluator reads the card’s left edges from the DOM (EDGE_BLOCKS_EXPRESSION, typed byparseEdgeProbe) and applies the alignment rule to them (judgeEdges), reporting every offset in px. In a criteria block, thespace.edgerow gainsinstrument: { verdict, evidence }beside the judge’s verdict, which it does not change; a stored frame with no page to read says so.CriterionVerdict.instrumentis optional, and nothing changes for a block without that row. -
Added, in
@ggui-ai/ui-gen(#1687): the visual judge’s record says which model answered and what its criteria call returned. Each canvas’sjudgerecord gainsmodels: { requested, served };servedlists the names the provider gave for the model that answered (Anthropic’smodel, Google’smodelVersion), and is empty when none did. A criteria block gainscall: whether the answer held acriteriaarray, how many entries it had, how many were kept, how many were dropped and why, how many named no row of the bank, and the call’s stop reason. A row the judge left unanswered still readsn/a; the block now says whether the call came back empty, cut, or with entries the parser dropped. Both fields are optional and report-only. -
@ggui-ai/ui-gen’s runtime-render check and render smoke test no longer mistake a component’s own console output for their verdict. Each worker now writes its verdict on one marked line, and the host reads that line. Before, a generated component that logged even one line while it was checked turned its check into anunverified“verdict was not valid JSON”. -
Changed, in
@ggui-ai/mcp-server(#1397): aggui_consumeresult that drains a gesture no longer leads with a plain-text sentence telling the agent to repaint and then callggui_consumeagain. ThenextStephint toggui_amendstays in the structured result. Measured on two models: the sentence led one of them to answer an informational tap with a silent re-poll instead of a reply, and removing it kept every repaint. -
Fixed, in
@ggui-ai/protocol(#1637): the JSON Schema served ontools/listforggui_handshake,ggui_renderandggui_runtime_sync_contextis now one Google’s Gemini API accepts as function parameters. The JSON-value definition’s object arm no longer refers back to itself (its members are open,additionalProperties: {}, and checked in code). Before, Gemini refused the whole tool list. The values accepted are unchanged; an invalid value nested inside an object is now reported at the member that holds it. -
Added, in
@ggui-ai/protocol(#1175): the themeGETnames a stored theme it cannot read. When the stored document fails the carry shape, the response istheme: nullbesideuninterpretable: { issueCount, issues: [{ path, code }] }, so it no longer reads like “no theme”. Codes come from the protocol’s own closedTHEME_ISSUE_CODES, mapped bydescribeUninterpretableTheme. No stored value is carried, and a baretheme: nullstill means no theme. A reader that ignores the new member sees exactly the previous response. -
A copied blueprint row now says it is a copy (#1570).
ggui_ops_register_blueprinttakes an optionalclonedFrom, the id of the row whose bytes it registers, and so doescreateRegisterGeneratedBlueprint(@ggui-ai/mcp-server-handlers, the in-process entry), which parses the same input. The durable row carries it, soggui_ops_list_blueprintslists it. Over MCP a server from before this strips the member, so a copy sent to it lands unmarked; check itstools/listbefore relying on the mark. An empty id, the row’s own id, and an id that is not a row of the registering app are refused before anything is persisted (ClonedFromRefusedError), with the same answer for another app’s row as for a missing one. Nothing that serves reads the field. -
A card’s runtime telemetry travels in two batches (#1383), never mixed in one
ggui_runtime_telemetrycall. The health batch carries which rung the card booted on (boot.path,boot.static_only_no_bridge), its connection status (status.<connecting|connected|disconnected|reconnecting>),subscribe.resolved,doorbell.ringand the live channel’s transport events (thechannel_*names). Each health kind admits onedetailshape (none, the card’s own render id, or a JSON object of booleans), and a detail outside it is left off the event. The diagnostic batch carries the gesture trail and the ladder’s finer story, and no longer carries free text:gesture.dispatchkeeps{toolName},gesture.resultkeeps{ok}and the JSON-RPC error code (never the message),gesture.dropped_supersededcarries no detail, andsubscribe.resolvedis{hasAck}only. The health batch flushes first, and the two batches have separate per-session flush caps (8 and 4).@ggui-ai/iframe-runtimeexports the one table of kinds asRUNTIME_TELEMETRY_KINDS(withRuntimeTelemetryKind), and@ggui-ai/live-channelexports its transports’ log events asCHANNEL_LOG_EVENTS(withChannelLogEvent, whichChannelLogger’s methods now take). -
Added, in
@ggui-ai/protocoland@ggui-ai/mcp-server-handlers(#1609):ggui_runtime_report_render_failure, an app-only runtime tool a card uses to say its render failed. It carries the phase (mountorupdate), the thrown value’s class name (a code identifier, or"Error") and the catch count, never a message or a stack. The server records a report only for the session the call’s view proof binds, answersokwhatever the verdict, and never treats a report as an agent turn.@ggui-ai/mcp-servernames each proven report (render_failed), and an unproven one without its session (render_failure_unproven). A deployment keeps a mark through the handler’srecordRenderFailure. No card sends it yet (SPEC §4.9). New exports:reportRenderFailureInputShape/Schema,reportRenderFailureOutputSchema,GguiReportRenderFailureInput/Output,RENDER_FAILURE_PHASES,RENDER_FAILURE_ERROR_NAME_PATTERN,RENDER_FAILURE_MAX_CATCHES,RENDER_FAILURE_MAX_ID_LENGTH(sessionIdandappIdare 1 to 256 characters),renderFailureErrorName, andcreateGguiReportRenderFailureHandler. A card sends at most one report per session.VIEW_PROOF_V1_BOUND_ARGSgains the tool’s row. -
Added, in
@ggui-ai/mcp-server-core(#1568):RenderIdentityRecord.requestedVariantKey, thevariantKey()of the variance the agent itself named for a render (its renderoverride.variance, else its handshake draft’s), beside thevariantKeythat was served. The two differ exactly when something other than the agent chose the served variance.ggui_renderwrites it on every commit. A record written before it existed reads without it, and absent means unknown, never the default variant. The render-identity refresh (ggui_update,ggui_runtime_sync_context) now carries every member it does not refresh from the stored record, so it keeps this member and any added later.runRenderIdentityStoreConformanceasserts the round-trip and the absence. -
Added, in
@ggui-ai/ui-gen(#1398): the runtime-render check reads whether a control whose click dispatched an action visibly shows it is working while the action is pending (disabled, busy, new text, or replaced), and reports it asEvalResult.runtimeProbe.pendingAffordance:{ dispatched, visible, missing, gone }. It runs after every other check, so it cannot change what they find. A form is read through its control that names the action. A pressed control stays that action’s control while it is on the page, even when it relabels itself. One that left the page, or now names another action, isgone, never credited to another control. It is report-only: it adds no issue and doesn’t change the probe’s verdict. It is absent when no action walk ran, or when too little of the check’s time was left. -
Added, in
@ggui-ai/mcp-server-handlers(#1568): a deployment can reserve rows from the semantic (similar-interface) reuse tier.MatchBlueprintOptions.excludeFromSemanticnames rows bysemanticExclusionKey(contractKey, variantKey). A named row is never offered to the reuse judge or returned as a candidate, and it stays reachable by its exact key.HandshakeDecisionAdapter.semanticExclusionanswers once per request:{ kind: 'rows', keys }, or{ kind: 'all', reason }when the deployment cannot say which rows are reserved.'all', or a hook that fails, turns the semantic tier off for that request rather than risk offering a reserved row.MatchBlueprintOptions.disableSemanticCausenames why the tier is off, so a trace never reports the exact-only policy for an exclusion outage. Nothing changes for a deployment that sets neither. -
A theme’s
motion.reduce: 'ignore'is accepted and treated as'respect'(#1326). A card never overrides its viewer’s reduced-motion request: the reduced-motion rule applies underprefers-reduced-motion: reducefor every theme, because the preference is the viewer’s own accessibility setting. That was already the behaviour; the theme types and the document door now say so, and a stored document that says'ignore'still parses. -
The host-helper conformance kit grades the two host obligations of the view-origin proof (#1415, SPEC §4.7), when the host supplies its rules.
V1-view-material-withheldfeeds the host’s model-context rules a render result and a render read body whose_meta["ai.ggui/render"]slice carries a marker in every value. None may reach the model, raw, percent-encoded or inside a base64 run, the result’s own marker must, and the host’s model-facing resource list, when it has one, must withholdui://ggui/render/*.L1-view-locator-bindingfeeds the host’s relay decision a view’s own and another session’s reads, in every locator form, and every session-naming call a view may make. Both are optional (modelContext,viewBindingoptions) and grade the rule, not a running host. -
On hosted GGUI, TypeSafe AI’s Jev model now decides whether a render reuses a cached interface, and which one (#1235). GGUI’s LLM judge decides when Jev does not answer in time, returns an error, or is not confident. Jev is shown the same fields the judge is, except the similarity scores, and what a render costs does not change. See Trust & security.
-
The conformance kit’s
n1-compatcatalog grades the view-origin proof across releases (#1415), on a newview-proofwire. A later proof version must still fit the relay door shape and readversion_unknown, nevermalformed; a v1 proof whose root carries a claim a later release adds must still parse. -
The conformance kit grades SHOULDs (#1526). A fixture may declare
"level": "should"; an unmet SHOULD is a warning, never a failure: it lands in the newConformanceResult.warnedbucket with the same evidence a failure carries, streams asWARNthrough the newConformanceReporter.onFixtureWarn, and is printed by the newformatWarnings. The CLI counts a warned fixture as executed and never changes its exit code for one. The first SHOULD case,fresh-subscribe-replays-reserved-preview(SPEC §12.2.1), expects a fresh subscribe to replay a known-reserved channel’s retained envelope after the ack, at aseqat or below itsstreamSeq, graded by the newreplayAfterAckoption on astream-updateexpectation. Changed:ConformanceResultgains the requiredwarnedfield, so code that builds aConformanceResultby hand addswarned: []; the runner always sets it. -
@ggui-ai/mcp-serverissues view keys (#1415). The render slice’s newviewKeyrides theggui_renderandggui_updateresults and the data-planeresources/readof a render locator. It is minted with the slice’swsToken, which now carries the signing secret’s key id and the door (kid,src). The control plane, isolated services, an anonymous caller,ggui_list_sessions,/state, the refresh and the console inspector never get one, and their tokens carry neither stamp; a read door that withholds one logsview_key_not_issuedwith why, as does a mint whose token is too long to carry a key. Under the development allow-all adapter every caller holds one identity and is keyed. A card from this release signs its runtime calls with it, and the server’s gate measures them.createGguiServergainsviewProof.keyedSince, from which a call without a valid proof readscurrentrather thanlegacyon its line. A runtime from before this release ignores the field. -
GET /api/sessions/:id/statelogsstate_chain_expired {sessionId, rootAgeSec}for each renewal it refuses because the chain expired: the presented token is past itsexp, or its chain has no second left (#1540). Only a token the server signed is counted. ArootAgeSecat or past the refresh window is a chain that reached its bound, and a smaller one is a token that went unrenewed. The 410 is unchanged. -
GET /api/sessions/:id/eventslogsrender_channel_polled_first {sessionId, appId, kind}on its first successful read per server process, session and kind (#1614), so a view on the HTTP polling rung leaves a mount mark tagged with its app, asrender_channel_subscribeddoes on the WebSocket and SSE streams.kindiscold-mountfor the boot fetch (sinceSequence=0&limit=1) andpollotherwise. The line comes only after every gate passes, never on a refusal, and never carries the query string. The server remembers the last 10,000 session-and-kind pairs, so a pair read again after that many newer ones logs again. Responses are unchanged. -
The live channel carries the stream counter’s epoch (#1531).
@ggui-ai/mcp-serverstampsstreamEpochon every stream envelope and on the subscribe ack (from a buffer that has epochs; the in-memory one does). A subscriber whose session’s counter restarts while it stays connected now receives the new generation’s frames instead of losing every one whoseseqis below its cursor, and the server logsstream_epoch_changedwhen that happens; a late frame of the epoch a subscriber left is dropped, so it never moves the subscriber back. A resume whosefromEpoch(WebSocket payload or?fromEpoch=on the SSE stream) is not the session’s current epoch, or whosefromSeqis past anything the counter has assigned, replays everything retained and setsreplayTruncated. A buffer without epochs behaves as before. -
Added, in
@ggui-ai/protocol/integrations/mcp-apps(#1415):McpAppAiGguiRenderMeta.viewKey, the view key a view proves its app-only runtime calls with, andparseMcpAppAiGguiRenderMetakeeps it only beside thewsUrl/wsTokenpair it is rooted in and only in its 43-character base64url shape. No server issues one yet. -
A card captures its render slice’s view key root (#1415, the view-origin proof; nothing signs with it yet). That root is
P, the payload segment of the slice’swsToken, and the slice’sviewKey.McpAppAiGguiMetaParseResult(@ggui-ai/iframe-runtime) carries it asviewRooton theokandEXPIRED_BOOTSTRAParms, beside the meta and never inside it, captured before an expired credential leaves the meta.ViewRootis exported. The card adopts a root only whenPfits a proof and decodes to the slice’s own session. For that session it moves only to a lateriat, and a later slice without a key never replaces it. Added, in@ggui-ai/protocol/integrations/mcp-apps:decodeViewRootClaims, which decodes a root to its claims, unauthenticated, and names why a string is not one (malformed,wrong_kind). -
Once a card’s boot slice is resolved, the runtime removes the inline copies of its envelope that the shell left behind (#1415):
globalThis.__GGUI_META__, the pre-load__GGUI_PENDING_TOOL_RESULTS__buffer, and the self-contained shell’s envelope<script>element, whose text outlives the global. Nothing in the runtime reads them after boot. Code that read__GGUI_META__after the runtime booted no longer finds it. This is hygiene, not a boundary: card code runs in the same realm and can still ask the host to read its own locator. -
A card proves its app-only runtime calls (#1415, the view-origin proof; the server measures it and refuses nothing at this release). Every
tools/callofggui_runtime_submit_action,ggui_runtime_sync_contextandggui_runtime_pullthe card sends (a dispatch and its audits, the context mirror, the bridge’s pull) carries a v1 proof atparams._meta["ai.ggui/view"]when the card holds a view key root for the call’s session. It sends the JSON-normalized arguments it signed. A dispatch signs whether the gesture carried user activation (measured, never enforced). Any other tool is sent unchanged. Signing is synchronous, through@noble/hashes(a new dependency of@ggui-ai/iframe-runtime), and total: a card that cannot sign sends the call without a proof. A dispatch answeredVIEW_ORIGIN_UNPROVENshows the ordinary error toast and never reads as a host that cannot relay. A relay forwards_meta["ai.ggui/view"]byte-identical. On a call made through the MCP AppsApp,_metaalso carries the MCP SDK’s ownprogressToken, as it already does today. -
Added, in
@ggui-ai/protocol(#1531, the live channel’s stream counter generation):StreamEnvelope.streamEpoch,AckPayload.streamEpochandSubscribePayload.fromEpoch, all optional, andmakeStreamEnvelopestamps astreamEpochbeside aseq, never alone. A new epoch means the session’s counter restarted andseqvalues may repeat: a client that dedupes byseqresets to nothing applied instead of dropping every frame, and a server holding another epoch than a resume’sfromEpochreplays everything it retains. An absent epoch is unknown, never a mismatch. -
A card reads the stream counter’s generation (#1531). Its
seqdedupe resets to nothing applied when an envelope or a subscribe ack names a differentstreamEpochthan the one it last saw, so a restarted counter’s frames are applied instead of dropped as repeats. A resumed subscribe sendsfromEpochbesidefromSeq, in the WebSocket frame and as&fromEpoch=on the SSE stream, and never without it. A server that sends no epoch gets the earlier behaviour, including the reset on an ack whosestreamSeqis below the cursor. Added:RegistrySseOptions.fromEpoch(@ggui-ai/live-channel), a getter read only whenfromSeqgives a cursor. -
ggui_runtime_submit_action,ggui_runtime_sync_contextandggui_runtime_pullverify a view proof when a view sends one atparams._meta["ai.ggui/view"](#1415), and name the verdict on theirtool_invokedline. No call is refused or answered differently. The line carriesviewProof(valid,missingorinvalid). When the proof is not valid it addsviewProofReason, the caller’sclaimedSessionId, and for a dispatch or a syncviewProofEra, read from the session’s row. When it is valid it addsviewProofSessionId, the proof root’s age and door, a bucket of the view clock’s skew, user activation on a dispatch, andviewProofRepeatwhen this server saw the same proof nonce for the session within 10 minutes. The proof, its nonce and its tags are never logged. These lines, andggui_runtime_declare_tool_catalog’s, also carryauthSource. A refused sync now showsok: falseand itscodeon its line. A server with MCP Apps off keys no view and does not verify proofs: its lines sayviewProofUnverifiable: true, and it logsview_proof_unverifiableonce at boot. -
Added, in
@ggui-ai/mcp-server-handlers(#1415, the view-origin proof; measured, never refused):SharedHandler.viewProof, which declares whether a tool’s calls carry a view proof and what it is for (ggui_runtime_submit_actionrequires one forkind: "dispatch"and measures the other kinds,ggui_runtime_sync_contextrequires one,ggui_runtime_pullmeasures one), withviewProofUseFor;HandlerContext.viewProof, the verdict the transport will put there; andHandlerContext.sessionRows(createSessionRowReads,readSessionRow), a per-request memo so a proof gate and the handler read a session’s row once.ggui_runtime_submit_actionandggui_runtime_sync_contextnow declareVIEW_ORIGIN_UNPROVENon their closed output schemas intools/list. No server answers with it at this release: a client that cached the schema will accept it when a later release does. -
Added, in
@ggui-ai/protocol/integrations/mcp-apps(#1415):VIEW_ROOT_SRC(['result', 'read']),ViewRootSrcandisViewRootSrc, the one list of doors a view root’ssrcmay name. It is a closed set: a verifier refuses any other value, so a new door is added to the list one release before any door mints with it.ViewRootClaims.srcand, in@ggui-ai/mcp-server-core,WsTokenClaims.srcare typedViewRootSrc. -
Added, in
@ggui-ai/mcp-server-core(#1415, the view-origin proof’s server side; no door calls it yet):mintViewRoot, a root ws token stamped with the signing secret’s key id and the issuing door (kid,src), and the view key derived from its payload, orviewKeyNotIssued: 'oversize'when the payload is too long for a proof;defaultViewKid,deriveViewKey,viewRootFits;verifyViewProof, which reads the proof from the request’s_metaand names the first failing check (missing:meta_absentorkey_absent;invalid: the grammar,unknown_key,bad_mac,session_mismatch,app_mismatch,args_mismatch) and never throws (an internal failure isverifier_error, with the class of what was thrown); andViewProofRepeatCache, a bounded replica-local record of seen(session, nonce)pairs, O(1) per observation.WsTokenClaimsgains the optionalkidandsrc; tokens without them verify as before. -
Added, in
@ggui-ai/protocol/integrations/mcp-apps(#1415, the view-origin proof, not yet used by any server or runtime): the request keyMCP_APP_AI_GGUI_VIEW_META_KEY(ai.ggui/view), the relay door shapeVIEW_PROOF_RELAY_SHAPE, the v1 grammarVIEW_PROOF_V1_PATTERN, the labels and size bounds, the bound-argument tableVIEW_PROOF_V1_BOUND_ARGS, the byte buildersviewKeyInputBytes,viewProofArgsBytesandviewProofCallBytes,parseViewProofandformatViewProofV1, the result codeVIEW_ORIGIN_UNPROVEN, and a known-answer vectorVIEW_PROOF_V1_VECTORS. The module holds no crypto; a signer and a verifier HMAC these bytes and reproduce the vector. -
A live-channel subscribe without
fromSeqno longer carriesreplayTruncated: trueon its ack when the stream buffer reports its reserved-channel walk truncated, for example once a session’s retention has expired.AckPayload.replayTruncatedis documented as absent on fresh subscribes, and a fresh client has no history that could have a gap. -
A live-channel subscribe that fails after the server registered the subscriber, such as a failed event-ledger read, no longer leaves that subscriber behind. The client may already hold the ack, so the server unregisters the subscriber and ends the transport: close code 1011 on the WebSocket, the stream ended on SSE. The client re-subscribes with its ordinary backoff and gets a fresh replay, instead of running on a partial one with no signal. The first page of the ledger is read before the ack, so a read that fails sends no ack, and a client that resets its retry budget on an accepted ack still backs off to its cap. It is not 1012, whose retry comes at once, because a failure that repeats for one session would then become a tight reconnect loop. Before, an SSE subscriber was never unregistered, and a WebSocket stayed half-subscribed. (#1528)
-
The live channel keeps its order, ack → replay → live, on a subscribe that asks for the event ledger (
sinceSequence), which every SSE open does. The server awaits the ledger read before writing the replay, and a live frame arriving in that window, published on this server or delivered from another one, used to reach the client first, ahead of replay frames with lowerseq. A client that dedupes byseqthen dropped the replay. Such frames are now held and sent after the replay. (#1525) -
SPEC erratum (§12.2.1), with no wire change: a subscribe without
fromSeqreplays no channel the render declares, as before, but the server SHOULD send the retained envelopes of the reserved_ggui:*channels right after the ack, atseqat or below the ack’sstreamSeq, as both reference servers already do (so a late viewer sees the_ggui:previewskeleton). A client MUST apply those frames, and dedupe against the highestseqit has applied, never againststreamSeq. The old text said a fresh subscribe replays nothing and told clients to seedlastSeenSeqfromstreamSeq; a client that did so dropped the reserved replay.SubscribePayload.fromSeq,AckPayload.streamSeqandStreamEnvelope.seqin@ggui-ai/protocolare re-documented to match. (#1521) -
@ggui-ai/protocol/wireexports the Plane-2 reader, so a browser can read a tool’s<code>: <detail>error text:parseDomainErrorText,ParsedDomainErrorText,isDomainErrorCode,DOMAIN_ERROR_CODESandDomainErrorCode. The reader carries only the code names (about 0.6 KB minified), not the registry’s descriptions: the names now live in a module of their own, and the registry rows are typed exhaustively over them. The root exports are unchanged. Changed (VERSION-POLICY §3.7, compatible):DOMAIN_ERROR_CODESis a readonly tuple, in the same order, instead of areadonly DomainErrorCode[]. -
ggui_runtime_sync_contextanswers a session read that fails exactly as it answers a session that does not exist,SESSION_NOT_FOUND, asggui_runtime_submit_actionalready did withPIPE_NOT_FOUND, instead of throwing the store’s error. It still writes nothing. The cause is logged on theruntime_ownership_unverifiedline, which now carries the store’s error text. The cross-app class contract test gains a failed-read arm: both runtime write tools must answer it byte-identically to a missing session, and every other session-keyed tool is pinned as it answers today (they throw). (#1514) -
A relay that retries a
ggui_runtime_submit_actiondispatch with the sameactionId(for example after losing the response) no longer writes a seconduser.submittedledger row. The dispatch appends to the pipe first and writes the ledger row only when the pipe stored the gesture; the retry is logged assubmit_action_duplicate_dispatch. TheoneShotspend is applied on a retry too, because recording it twice is one spend, so a gesture whose first call failed between the append and the spend is still spent. Changed (VERSION-POLICY §3.7, compatible):PendingEventConsumer.append(@ggui-ai/mcp-server-core) resolves'appended'or'duplicate'(PendingEventAppendOutcome). An adapter that resolves nothing still conforms and is read as'appended', which keeps the earlier behaviour. The publishedrunPendingEventConsumerConformancesuite asserts the outcome. (#1517) -
The self-contained shell (the per-render document a
resources/readof aui://ggui/render/…locator returns) now reports a runtime bundle that fails to load: it posts oneggui:bootstrap-failedwithBUNDLE_FETCH_FAILEDto the host, after retrying a content-hashed URL’s unhashed twin, or at once for any other URL, as the thin shell already did. Before, the host heard nothing. The card itself is unchanged: it keeps its loading mark (or stays blank without one), and only the host is told. The runtime<script>of every such shell now carriesdata-ggui-runtime="src"beside a small inline listener, so its bytes change. (#1503) -
ggui_ops_register_blueprint(andcreateRegisterGeneratedBlueprint, which shares its core) andggui_ops_generate_blueprintmirror a registration into the cache under the durable row’s own id. A card served from that cache row records that id, and the store a re-mint reads now holds the row and its body under it, whether that store is the ops store itself or a separate one the mirror writes through to. So re-minting the card’s locator finds the blueprint instead of reporting it gone. A mirror that writes through to the ops store itself no longer leaves a second durable row per registration. Cache rows bound before this keep the id they were served under. (#1497) -
The live channel no longer mints a reconnect
sessionTokeninto the subscribe ack (#1488). No server ever verified that token, so on the bearer reconnect it was documented for it proved nothing of its own, and no client read it; a reconnect presents a livewsTokenagain.AckPayload.sessionTokenstays declared, optional and@deprecatedthrough this release, and is removed in the next (VERSION-POLICY §3.6). Therender_channel_subscribedlog now reports the credential a subscribe came on:bootstrap(true for a wsToken, on the WebSocket or the SSE stream) andsource(ws_token,console_cookie, or the auth adapter’s source). -
Removed (a pre-1.0 minor, VERSION-POLICY §3.7), each with no replacement:
mintSessionTokenandDEFAULT_SESSION_TOKEN_TTL_SEC(@ggui-ai/mcp-server-core), andGguiSessionChannelBootstrap.issueSessionToken(@ggui-ai/mcp-server).TokenKindkeeps'session', deliberately, until the release that removesAckPayload.sessionToken:kindis a claim inside tokens that cross the wire, and a session token an earlier release minted must still be refused aswrong_kind. -
Removed (a pre-1.0 minor, VERSION-POLICY §3.7), each with no replacement: the variant selector, which nothing called. In
@ggui-ai/mcp-server-core:selectVariantWithLlm,encodeSelectedReason,preFilterCandidates,computeVariantSelectionCacheKey, theVariantSelectionContext,VariantSelectionDecision,VariantSelectionCache,VariantSelectionCacheEntry,VariantSelectionPickFn,VariantSelectionResultandVariantSelectorWithLlmOptionstypes, theDEFAULT_VARIANT_SELECTION_CONFIDENCE_THRESHOLD,DEFAULT_VARIANT_SELECTION_CACHE_TTL_SECandDEFAULT_VARIANT_SELECTION_SHORTLIST_SIZEconstants,BlueprintSelector,BlueprintSelectorContextandcreateDeterministicBlueprintSelector, and on/in-memoryInMemoryVariantSelectionCacheandInMemoryVariantSelectionCacheOptions. In@ggui-ai/mcp-server-handlers: the optionalHandshakeNegotiator.selectVariantmethod. In@ggui-ai/mcp-server:createGguiServer’sblueprintSelectoroption andGguiServer.blueprintSelector. None of these types a wire member.BlueprintMeta.selectedReason, thehandshake.decidedevent andextractSelectionConfidencestay: aconf=<n>suffix onselectedReasonis still read into the event’sselectionConfidence, and nothing in@ggui-ai/*writes one. -
Removed (a pre-1.0 minor, VERSION-POLICY §3.7), with no replacement:
GenerationDeps.resolveLlmCallerin@ggui-ai/mcp-server-handlers(#1625). Nothing read it: the render handler resolves the blueprint the handshake decided, and the matcher’s judge is wired through the handshake negotiator. It does not type a wire member.GenerationDeps.installedBlueprintsstays, becausecreateGguiServerforwards it to the handshake negotiator. -
Generations now stamp the durable blueprint record with the build that made them (
Blueprint.build, declared in 0.25.0): aggui_rendercold generation andggui_ops_generate_blueprintwrite the generation’sbuild, and so doescreateRegisterGeneratedBlueprintwhen its caller passes the generation’s build (GeneratedBlueprintBytes.build?), on the durable row and on the row a cache mirror writes through to. A dedup keeps the row’s first stamp. Rows registered throughggui_ops_register_blueprint, installed and seed-imported rows carry none: the operator door takes no build (its schema names none, and over MCP an unknownbuildis stripped before the handler runs).admitGeneratorBuild(@ggui-ai/mcp-server-handlers/renders) is the one admission rule: a stamp is kept only on LLM-generated code and only when it passesgeneratorBuildSchema, else it is dropped and the row registers unstamped.RegisterBlueprintInputgainsbuild?. The conformance kit gains anops-list-blueprintsn1-compat wire. (#1280) -
ggui_consumenow confirms the session belongs to the caller before it registers as that session’s consumer. Before, a consume refused for another app’s session counted as that session’s consumer while its store read ran, and its end stamped the session’s last consumer exit, which changed how long that session’s dispatches waited for a consumer over the next minute.InMemoryActiveConsumerRegistryalso forgets a consumer’s exit after 60 s (ACTIVE_CONSUMER_EXIT_RETENTION_MS, exported from@ggui-ai/mcp-server-core/in-memory), so it keeps the last minute’s exits instead of one per session that ever had a consumer;msSinceLastExitreadsundefinedpast that.ggui_runtime_submit_actionwaits as before, except for a session re-rendered in place after its consumer exited (on the in-memory and SQLite stores), which now waits as a fresh render does for 60 s; it also reads the session row once per dispatch instead of twice. (#1485) -
Snapshot-read renewals are bounded (ggui#1496).
/api/sessions/<id>/statestill renews the wsToken it is given, but each renewal carries its chain’s root forward as a new optionalrootIatclaim, and its expiry never passesrootIatplus the refresh window. A chain of renewals therefore ends one window after its root was minted, and/state,/events,/streamand the live channel all refuse its last token in the same second, as long as every server sharing the secret uses the same window and it has not shrunk. After that, a new credential needs an authorized door, such asggui_runtime_refresh_ws_token, which mints a new root for a caller admitted to the session. A token withoutrootIat(every earlier token) is its own root. -
Added:
createGguiServeroptionswsTokenTtlSec(default 180, the lifetime of the ws tokens the server mints; a snapshot-read renewal gets less when its chain’s window ends sooner) andwsTokenRefreshWindowSec(default twice the TTL; the factory throws when it is shorter than the TTL);WsTokenClaims.rootIat?andMintWsTokenInput(mintWsToken’s input, withrootIat?) in@ggui-ai/mcp-server-core. -
Changed (VERSION-POLICY §3.7):
WsEnvelopeVerdict.iat(@ggui-ai/mcp-server-handlers) is replaced byrootIat, the chain root’s issued-at (a renewal’srootIatclaim, else itsiat), so the refresh log’srootAgeSecages the chain rather than its last link. -
A render’s
postSuccessHooknow receives the token counts the generation reported, ongeneration.usage(GguiSessionPostSuccessArgs,@ggui-ai/mcp-server-handlers):inputTokens(the input not served from a prompt cache),outputTokens, andcacheReadTokens/cacheCreationTokenswhen the provider reports them. With@ggui-ai/ui-gen’s generator those are its coding turns and any in-loop text-evaluation rounds, summed; the in-loop visual judge’s calls are not included.generationstaysnullon a blueprint reuse. (#1513) -
A card that receives live updates through the host’s
tools/callbridge (ggui_runtime_pull, the path for hosts whose iframe can’t reach the server) now pulls again as soon as a pull returns, as the bridge’s subscription mode was designed to. It no longer waits 15 seconds after every successful pull, so an update landing between pulls reaches the card up to 15 seconds sooner. After three pulls in a row that return nothing, the card still drops to pulling every 15 seconds. (#1527) -
A card whose live credential (
wsToken) expires now renews it through its host instead of going quiet. It renews at mount, when the slice it booted from has expired. It also renews mid-session: when the server refuses the credential as expired (BOOTSTRAP_EXPIRED), when HTTP polling answers410, or when the card falls back to the host’s bridge after its expiry. It callsggui_runtime_refresh_ws_tokenonce per credential through the host’stools/callrelay. It then moves its live channel onto the new credential and resumes from the last update it applied. A card whose session is confirmed gone, or whose host confirms it cannot relaytools/call, says so and stops pulling, keeping what it painted. Stream envelopes (data) are now applied once each by theirseq, and a resumed subscribe passes the last one applied asfromSeq. A subscribe ack whosestreamSeqis below that cursor shows the server’s counter restarted. The card then forgets the cursor instead of dropping the new envelopes as repeats. (#1496) -
Changed (VERSION-POLICY §3.7): in
McpAppAiGguiMetaParseResult(@ggui-ai/iframe-runtime), theokarm gainsheld?, the expired live credential the parse now keeps instead of discarding. TheEXPIRED_BOOTSTRAParm now carriesmetaandheld.HeldCredentialis exported. (#1496) -
Added:
RegistrySseOptions.initialSinceSequence(@ggui-ai/live-channel) also accepts a function, read when the SSE rung connects. The newRegistrySseOptions.fromSeqgetter appends the stream cursor (&fromSeq=) to every connect. (#1496) -
inLoopEvaluationandrunsInLoopEvaluation(@ggui-ai/ui-gen) name which in-loop evaluation legs a generation runs: the text evaluator whenevaluation.enabled, the visual judge whenvisualEvaluation.enabledorqualityConfig.visualEval. They are the harness’s own gate, so a host pricing a generation’susagecan tell whether those counts may include evaluation calls, with the options it configured the generator with. (#1513) -
A card’s WebSocket counts a subscription as accepted only once it has held for 5 seconds. A server whose subscribe fails right after its ack, and closes, no longer resets the card’s retry budget on every attempt. The card backs off as for any failed attempt and, after the usual ten retries, moves to its next transport. It used to retry every second indefinitely. (#1533)
-
The generation’s cost cap (
qualityConfig.maxCostPerGeneration,@ggui-ai/ui-gen) now counts all of a generation’s spend, so under the same cap the eval loop can stop sooner than before. It now counts the in-loop visual judge’s calls, including a configured criteria bank’s report-only calls (the cap bounds money spent, so these count toward it, through their cost and never their answers), and the prompt cache’s reads and writes, priced at the model’s cache rates. A model with no cache-write rate is priced at the largest write premium the model registry states, so a write is never counted under. Added: the generation result carries the in-loop visual judge’s spend as its own part,inLoopVisualTokens(besidetokens, never inside it). -
A tap the card’s own action contract refuses is no longer silent (#1536). The runtime posts an
action-refusedobservability event to the embedding host (ggui:observe), naming the render, the action and each violation by its field path and schema keyword, never by value. It posts at most ten per render, and marks the tenthcapped: true. The visitor sees the ordinary error toast, named by the action’s declared label. Nothing is sent, as before. Added:ActionRefusedEvent(@ggui-ai/iframe-runtime), and, in@ggui-ai/wire,BuildWireConfigOptions.onDispatchRefusedwith its payloadDispatchRefusedInfo, called afteronViolationwith the refused action’s name. -
ggui’s own relays carry a view’s proof, and refuse a view’s call to a model-only tool (#1415).
@ggui-ai/agent-server’sPOST /agent {kind:'tool-call'}takes an optionalmeta, the view’s request_meta, and forwards only itsai.ggui/viewstring, verbatim, as the relayed call’s_meta. A call to a tool whose declared_meta.ui.visibilitylacksapp(ggui_render,ggui_update) is answered-32602Unknown tool: <name>and never relayed. The relay reads each server’stools/listat most once every 5 minutes, and once more for a name it has not seen. When a re-read fails, it keeps answering from the last list it read and logsrelay_tool_visibility_stale. A tool it has never seen while the list cannot be read is answered-32603tool visibility unavailable. In@ggui-ai/mcp-apps-react-native,onToolCallgains an optional third argument,McpAppViewCallMeta(the proof alone), and<McpAppIframe>gains atoolVisibilityprop that refuses the same calls. Theggui-basic-websample sends its view’s_metato the relay. Added, in@ggui-ai/protocol/integrations/mcp-apps:toolVisibleToApp, the app-side twin oftoolVisibleToModel. -
A card never moves its stream cursor back to an epoch it has left (#1531). It remembers the last four epochs it left, drops their stamped frames and ignores their acks, as the server does. Before, a late frame of the old generation moved it back with nothing applied, and the next frame of the new generation moved it forward again with an empty floor, so a duplicate in the new generation could pass. An unstamped frame still always passes, and no longer moves the epoch.
-
ggui_ops_delete_blueprinttakes a blueprint out of reuse (#1541).ggui_ops_register_blueprintandggui_ops_generate_blueprintwrite a blueprint twice: its durable row, and a mirror in the cache the handshake’s exact-key probe matches from. Delete removed only the durable row, so the next handshake on the same contract still reused the deleted blueprint. It now removes the cache entry (its vector and its exact-key binding) first, then the durable row. A cache delete that fails fails the call with the row intact, so a retry completes it. Re-running a delete for a blueprint whose row an earlier delete already removed clears its leftover cache entry, in the caller’s own app only. Added:GguiOpsDeleteBlueprintDeps.cacheRegistry(@ggui-ai/mcp-server-handlers), whichcreateGguiServerbinds to the same registry as the writers. -
The ggui dark theme’s muted text is dimmer:
onSunkenis#a3a3a9(6.57:1 on a card, 5.80:1 on a sunken well), where it was#d9d9d9, close enough to the primary text that secondary text did not read as secondary (#1566). Six components drew text you read with the muted role, in both themes, and now draw it withonContainer: the secondary and ghostButtonlabels, the option labels ofCheckbox,RadioGroupandToggle,Accordionanswers,Tablebody cells, aSelect’s displayed option, and the other party’s messages inChatWindow. What is muted staysonSunken: option descriptions, theRadioGroupfield label, theTableheader and caption, aSelectplaceholder while it shows, and the other party’s timestamps. In the light theme that text moves from#5a5a5ato the card’s ink. -
Added, in
@ggui-ai/ui-gen:visualEvaluation.actOn('critical'or'major', default'critical'), which of the in-loop visual leg’s own findings may start another eval round when the quality mode isfast. With'major', the visual judge’s major findings start a fix round as a failure does; the fit measurement’s warn-level overflow and the text evaluator’s warnings never count. The default decides exactly as before, and the other quality modes, which already act on every warning, are unchanged. A round that continues with the setting present logs why (continuing: fails=…, visual majors acted=… (actOn=…)). (#1542) -
ggui servekeeps a session’s live-stream counter across a restart (#1534). A server that stores its sessions in SQLite, asggui servedoes by default whenbetter-sqlite3is installed (unless you pass--ephemeral), and wheneverggui.jsondeclaresstorage.renderswithdriver: "sqlite", now keeps the live channel’s stream buffer in the same database: each session’sseq, itsstreamEpochand the envelopes it retains for replay. After a restart, a session continues itsseqin the same epoch, and a client that reconnects withfromSeqgets the frames it missed. Before, every restart started each surviving session’sseqagain at 1 in a new epoch, with nothing retained. Added, in@ggui-ai/mcp-server-core:SqliteGguiSessionStreamBuffer(/sqlite), andrunGguiSessionStreamBufferConformance(/contract-tests), the stream buffer port’s conformance suite, which the in-memory and SQLite buffers both run.resolveStorageFromConfigreturns the buffer asstreamBuffer, andcreateGguiServerkeeps its in-memory default unless you pass one. -
A retried
ggui_runtime_submit_actiondispatch is one gesture, promised on the wire (#1519, SPEC §11.1). A host or relay that could not confirm a dispatch may retry it: a server SHOULD answer a repeat of an earlier committed(sessionId, actionId)as it answered the first, with no second pending event, no seconduser.submittedrow and no second spend, even when both requests are in flight at once. The promise becomes a MUST in the next release.@ggui-ai/mcp-serveralready behaves this way. AnactionIdreused for a different gesture (a differentintentoractionData) keeps the first gesture. This release the second is still answered as a duplicate and dropped, so the view shows success, but no longer without a trace: it is logged assubmit_action_action_id_reused, and nothing of it is spent. Its answer,ACTION_ID_REUSED, is declared in the tool’s output schema this release and emitted from the next, when the view shows the user an error toast (as on anyok: false); the second gesture still does not reach the agent. SPEC §11.1 names the promise’s two residuals. Agents keep deduping consumed events byactionId: the promise serves relays. Added:dispatchGestureBytesandACTION_ID_REUSED(@ggui-ai/protocol/integrations/mcp-apps);PendingEventConsumer.appendtakes an optionalgestureDigestand reports'conflict'(@ggui-ai/mcp-server-core), and the SQLite pending-event store adds its digest column on open. If you runrunPendingEventConsumerConformanceagainst your own store, its three conflict cases run only when your factory setsreportsConflict: true(they are skipped otherwise, and become required with the MUST); thedispatch-idempotencyconformance catalog (@ggui-ai/protocol-conformance/dispatch-idempotency-conformance), at SHOULD level. -
ggui_runtime_submit_action’s description intools/list, and its twoPIPE_NOT_FOUNDmessages, no longer say the view falls back toui/message(#1571). The view never did: on anyok: falseit shows the user an error toast and sends nothing (SPEC §4.7). The texts now say the gesture was not enqueued.ggui_renderalso logs[ggui_render.pipe_open_failed]with its reason when a session’s pending-event pipe fails to open; before, that failure was silent, and the only sign was each later gesture on the session being refusedPIPE_NOT_FOUND. -
A blueprint row that copies another row’s bytes can say so (#1570).
Blueprint.clonedFrom(optional) names the row it copies. The copy’ssource,buildandcodeHashstay its original’s, which are true about the bytes, and nothing that serves reads the new field. It is declared this release and written from the next, because the previous release’s strictblueprintSchemawould refuse a row that carries it. The portable form does not carry it. If you implementBlueprintStore, persist it verbatim: the kit’s store suite now grades that. Two things to know if you read rows: a row without it is not necessarily produced in your store (an imported row carries itssourceand noclonedFrom), and removing a copy is a row delete, never a content takedown, because a copy shares its original’s body. -
Changed, in
@ggui-ai/mcp-server-handlers:ggui_render’scheckRenderContractsseam now receives exactly the shape its type declares,{ actionSpec?, streamSpec?, agentCapabilities? }, built from the render and its contract (#1579). It used to receive the whole committed session, with the contract’sagentCapabilitiesspread onto it.ComponentGguiSessiondeclares no such field, so the committed session row carried an undeclared property that nothing read after the check. The row no longer carries it. If your own binding read anything else off its argument, read it from the render instead; the schema-compat check itself sees exactly what it saw before. -
The
default,aggressiveandalwaysMCP instructions presets (@ggui-ai/mcp-server) are corrected to match the shipped tools (#1579). Among the fixes: after consuming a gesture, the agent repaints the same card withggui_amend(they said to render a new UI); an event names its action inintentand carries its payload inactionData(they described anextSteponactionData); a render response carries asessionIdand aui://resourceUri, never a URL to show; the card itself posts the doorbell message when nobody is consuming; andggui_list_sessionsis described as offered only where a server registers it.ggui_render‘s tool description, and the presets, no longer tell the agent to read afixByfield that no refusal carries: retry anafter-fixrefusal only once you have performedrefusal.fixyourself. The presets’ opening rendering policy is unchanged. -
Changed, in
@ggui-ai/mcp-server-handlers: a credit ledger entry’skindonggui_ops_list_credit_transactionsis an open vocabulary (#1532). The known kinds, now exported asKNOWN_CREDIT_TRANSACTION_KINDS, arefree_credit,render_charge,topupandrefund, and a deployment may add its own. The output schema declareskindas a non-empty string where it was a closed enum,CreditTransactionView.kindis aCreditTransactionKind(a known kind, or any other non-empty string), and the descriptions tell a reader to accept an unknown kind as named, never as a render charge. If your code switches onkind, give it a default arm. -
ggui serveenables the console’s theme upload (#1579). The CLI built the uploader and never passed it to the server, so the upload answered 501 (upload_not_configured) underggui serve, which its own message named as the way to get uploads.ggui servealso stops writingrender-signer-secret.hexinto the persistent directory. The server stopped reading it when render signing was removed with the/r/viewer; an existing file is harmless and can be deleted. -
An MCP instructions preset or custom string you set now reaches hosts, and setting none sends none (#1579).
createGguiServer’smcpInstructions(--mcp-instructions/GGUI_MCP_INSTRUCTIONSonggui serve) was passed where the MCP SDK does not read it, so no server’s instructions ever reached a host; themcp_instructions_setboot line was the only trace. It is now sent in theinitializeresult. The no-flag default isoff: it was nominallydefault, never delivered, so an operator who set nothing sees no change.mcp_instructions_setandmcp_instructions_offnow carrysentInInitialize. -
Removed, in
@ggui-ai/protocoland@ggui-ai/mcp-server-handlers:validatorScoreonggui_ops_generate_blueprint’s output (opsGenerateBlueprintOutputSchema) (#1579). It was read off the generator’s metadata through an undeclared field that no generator sets, so it was never populated, and no release ever returned it. A client that cached the oldtools/listexpects an optional field it never received.Blueprint.validatorScoreon blueprint rows is unchanged. -
In a fullscreen canvas, a band that runs edge to edge keeps its content on the card’s edge (#1556). The frame gives a card’s content a 16 px inset, and a band that bleeds (a
surface="hero"band that opens the card, or any element that declaresbleed) took that inset back for its content as well as its ground, so an opening band’s heading sat 16 px left of the body and chips below it. The band’s ground still meets the panel’s edge; its content now keeps the inset, so a card whose edges agree in the chat card agree in the panel too. Only the left and right edges changed; a band’s top and bottom are as before. -
Added, in
@ggui-ai/mcp-server-handlers:GguiSessionPostSuccessArgs.proposalServed(#1331), optional.ggui_render’s post-success hook now learns whether the render served the stored blueprint its handshake proposed, as proposed:truewhen the proposal was accepted and the proposed blueprint is what served,falseon a re-aimed render (even to the same blueprint), a cold generation, a failed render, or a store that now serves a different card. It is the same predicate as the blueprint resolution event’sstrategy: 'proposed',served: 'stored'andblueprintIdequal toproposedBlueprintId. -
Changed, in
@ggui-ai/mcp-server-core:BlueprintIndex.deleteId(scope, exactKey, expectedId)takes the blueprint id the unbind is aimed at, and removes the binding only while the key still points at it (#1603). Deleting a blueprint, self-healing a dangling binding, and evicting a row each rebuilt a key and unbound whatever it held, so a key re-bound since to a newer design lost it, and the newer design dropped out of exact-key reuse. All four callers now pass the id they mean. A customBlueprintIndexmust remove the binding only when it points atexpectedId(a no-op otherwise, and on a missing binding);runBlueprintIndexConformancegrades it. A failed unbind after a delete or an eviction is now logged asblueprint_index_unbind_failed, where it was swallowed. -
The operator-default mark now says what it does (#1611).
ggui_ops_update_blueprint,ggui_ops_generate_blueprint, the ops schemas’isOperatorDefault/setAsOperatorDefaultdescriptions (which ship intools/list) and the console’s blueprint pages said the flag pins a blueprint, and the console said the handshake picks by it. The mark orders blueprint listings (the default first) and is shown to operators; it does not decide which blueprint serves a render, which the handshake resolves from the blueprint index. No behaviour changes. -
A
Selectwith aplaceholderand neithervaluenordefaultValuenow shows the placeholder, muted, until the user picks (#1569). The placeholder is a disabled first option, and nothing selected it, so the browser showed the first real option as if the user had chosen it, and a form reading the element’s value got that option. It now starts on the empty choice, and the text reads at body strength once a choice is made. AvalueordefaultValuestill wins, and aSelectwithout a placeholder is unchanged.
2026-09-28 — v0.25.0: four security fixes to cross-app access — upgrade if you run @ggui-ai/mcp-server — and a tapped control shows it is working
Section titled “2026-09-28 — v0.25.0: four security fixes to cross-app access — upgrade if you run @ggui-ai/mcp-server — and a tapped control shows it is working”- Security fix (
@ggui-ai/mcp-server-handlers):ggui_runtime_submit_actionandggui_runtime_sync_contextact only on a session that belongs to the caller’s own app, asggui_runtime_pullalready did. A call for another app’s session or a missing session is answered as not-found before any effect:PIPE_NOT_FOUNDonggui_runtime_submit_action,SESSION_NOT_FOUNDonggui_runtime_sync_context. A failed session read is answeredPIPE_NOT_FOUNDonsubmit_action; onsync_contextit threw the store’s error, with nothing written, until the next release (#1514). (Corrected: this note first saidPIPE_NOT_FOUNDfor all three cases on both tools.) If your host relays runtime calls under a credential whose app differs from the render’s (for example, on the universal endpoint with an identity-default app), those calls now get not-found. Relay them at the render’s app endpoint. - Security fix (
@ggui-ai/mcp-server): on the live channel’s bearer path, a subscribe that declares an app other than the one its credential resolves to now needs the deployment’s per-app authorization to pass, through a new optionalauthorizeAppseam thatcreateGguiServerfills fromperAppRouting.authorize. Otherwise it is refused withAPP_MISMATCH. The token and console-cookie paths are unchanged. If your clients subscribe with a bearer credential and declare another app, make sureperAppRouting.authorizeadmits that pairing. - Security fix (
@ggui-ai/mcp-server-handlers), and a silent change: aggui_renderthat names a session the caller can’t see (another app’s or another subject’s) now gets a fresh session id instead of writing into that session. The render succeeds under the new id, and the other session is never written. This applies only when a handshake negotiator setstarget.sessionId. There is no error to catch, so if you pre-assign session ids, read the returned id. - Security (
@ggui-ai/mcp-server), new and opt-in:perAppRouting.perAppOnlySourceslists credential sources that are valid only at their own app’s endpoint. Such a credential is refused on an MCP mount without an app in its URL, and authorized on the live channel. - The conformance kit does not yet grade cross-app refusal, so the four fixes above are verified by the packages’ own tests, not by the kit.
ggui_runtime_refresh_ws_tokenis now an authorized re-mint. It re-mints a view’s live credential, at any age, only for a caller the render read door admits to the envelope’s session (the app, then the subject). A session the caller cannot see gets the same not-found errorggui_runtime_pullthrows, and there is no refresh window. Its advertised output schema is unchanged.- Added:
withWsToken(url, wsToken)in@ggui-ai/protocol(swaps the token in a token-bearing session-API URL);verifyWsTokenSignaturein@ggui-ai/mcp-server-core(a ws envelope’s signature and kind, at any age);renderReadAllowedandRenderReadRowViewexported from@ggui-ai/mcp-server-handlers/renders; anddefaultHandlers’ optionrender.wsTokenVerify. - Changed:
GguiRefreshWsTokenHandlerDepsis now{ renderStore, verify, mint }. - Removed (a pre-1.0 minor, VERSION-POLICY §3.7). Before 1.0, a minor release may remove an export without a deprecation window. A caret range such as
^0.24.0never picks up a new minor, so nothing changes for you until you raise your range:refreshWsToken,RefreshWsTokenOptionsandRefreshWsTokenResult(@ggui-ai/mcp-server-core), replaced byverifyWsTokenSignatureplus the handler’s gate and your minter;- the
'refresh_window_closed'member ofVerifyTokenFailure, with no replacement; WsTokenRefreshSeam(@ggui-ai/mcp-server-handlers), replaced byGguiRefreshWsTokenHandlerDeps’verifyandmint;GguiSessionChannelBootstrap.refreshandGguiSessionChannelBootstrapRefreshResult(@ggui-ai/mcp-server), with no replacement, because the channel never called it;defaultHandlers’ optionrender.bootstrapRefresh, replaced byrender.wsTokenVerify.
- A card whose runtime bundle URL carries a content hash that no serving replica has (during a rolling deploy, after a rollback, or on a replayed card) no longer fails at once: both shells retry the bundle’s unhashed twin, same origin and path, one time (the thin shell then reports
BUNDLE_FETCH_FAILEDas before; the self-contained shell still reports no bundle failure, tracked as ggui#1503). New in@ggui-ai/protocol:runtimeBundlePlainTwin,runtimeBundleHashedNameSource, and theruntimeBundlePlainNameoption ongguiShellHtml. The server logsruntime_bundle_plain_servedfor each serve of the unhashed name while the hashed name is mounted. @ggui-ai/mcp-server-handlersexportscreateRegisterGeneratedBlueprint, an in-process entry (not an MCP tool) that registers engine-generated component code together with the authored source it was compiled from. It records the generation’s ownllmprovenance (validated; no default) and hands the source to the blueprint registry, so a render that reuses the registration serves it throughggui_get_render_sourceand can be saved to a library. The publicggui_ops_register_blueprintinput is unchanged and still takes no source, because it cannot verify that supplied source compiles to the supplied code.attachAuthoredSourceadds that source to a registration made from compiled code alone, only when the given code is byte-for-byte what the registration serves, and has a dry-run mode that writes nothing. It reads its write back, and reportslost_racerather thanattachedwhen a concurrent rewrite of the row erases the hash. Alongside them, a blueprint registry wired with a code store but no durable blueprint store now writes the authored-source body itssourceCodeHashpoints at; before, the hash was recorded with nothing behind it.ggui_amend,ggui_updateandggui_renderlog their session on thetool_invokedline, as the runtime tools do, so a reaction joins the tap that caused it by session instead of by app and time order. Amend and update log the session they app-scope-gated (the caller’s input rides an error line asclaimedSessionId); render logs the session id it minted, on arenderedand on afailedresult, and none on a refusal (the id is the call’s, not proof that a row backs it).GguiSessionPostSuccessArgs.generationgainseffort?— the named level the generation applied, as the engine reported it (GenerationMetadata.effort) — so a post-success hook reads the level that ran rather than a stored profile. Absent when no level was applied;nullgeneration (a reuse or a failed generation) is unchanged.- The durable render identity record gains
blueprintIdentity?: 'ephemeral', set when the render served an ephemeralblueprintId;RenderIdentityStoreimplementations must persist it (a new conformance case), and aui://ggui/render/…re-mint of such a render answersBLUEPRINT_UNRESOLVABLEnaming the ephemeral identity instead of reporting a blueprint as gone. - The ephemeral
blueprintIdcovers every fresh generation whose key is already bound to DIFFERENT code — aforceCreate, a render that took no index read (e.g. a deployment with no reuse negotiator), or one that lost a race — not only aforceCreate. When the regenerated code is byte-identical to the bound blueprint’s, that blueprint’s id is returned (identity: 'existing'). - The
ggui_renderresult declares an optionaleffortmember — the named effort level the generation that produced the render ran — andGenerationMetadatagainseffort?, set by ui-gen when a named level’s dials apply. Declared now, sent from the next release (the result’s schema is closed attools/list). - A
forceCreaterender at an occupied key now serves an ephemeral, content-addressedblueprintIdof its own instead of the incumbent’s; theonBlueprintResolutionevent’sidentitygains'ephemeral'(a self-hosted sink that switches on the three earlier values must add an'ephemeral'arm). - The durable blueprint record declares its minting engine’s build (
Blueprint.build?: GeneratorBuild,generatorBuildSchema, andbuild?on the portable blueprint;GeneratorBuildnow lives in@ggui-ai/protocoland@ggui-ai/mcp-server-corere-exports it). Declared now, emitted next release: no server stamps a row yet, and a row without a stamp reads asmintedBy: unknown. ggui_consumenow sends thenextStepit declared in 0.24.0. When the drain returned events, the result carries aggui_amend({ sessionId })hint to repaint the same card in place instead of rendering a new one, and its text leads with the same copy-paste example. It is absent on an empty drain and on a late drain from an expired render.MODELS.openrouterlistsopenai/gpt-6-sol,openai/gpt-6-lunaandanthropic/claude-opus-5.5, so they autocomplete as OpenRouter routes; every<author>/<model>string was already accepted by shape. They were listed after an OpenRouter tools smoke passed (3 of 3 tool calls returned per id withtool_choice: required, 0 errors); Opus 5.5 followed the two GPT-6 ids once its dotted id resolved to a price row.- A repaired handshake draft keeps what you declared on its actions. When a draft fails validation and the server repairs it (
suggestion.origin: "synth"), the proposal now carries every member you declared on each action entry —oneShot,confirm,nextStep,description,example,icon— changing only what the findings name. Before, a repair rebuilt each action from its label and schema alone, so a card whose draft tripped any validation error was served without itsoneShotand rendered with no one-shot guard, and nothing said so. Anything the repair cannot keep is named at its own path: a declared member the repaired contract cannot carry (anextStepwhose tool the repair did not re-declare) as aREPAIR_MEMBER_DROPPEDfinding atactionSpec.<name>.<member>, an action the repair renamed or removed asREPAIR_ENTRY_DROPPEDatactionSpec.<name>, each with the gate’s reason in its message; a member the gate refused on your draft is named by thatCTR_*finding at the same path, and a salvaged subset names each cut with the gate’s code at the cut path. The deterministic normalization tier, which never calls a model, keeps the same members; its allowed keys are now derived from the protocol’s entry schemas instead of a hand-kept list, so a stream entry’smode,replay,completeandexamplesurvive it too. - The handshake suggestion’s
blueprintMeta.matchedIntent, declared in 0.24.0, is now sent. On a judged semantic hit from the app’s own pool it carries the matched card’s stored intent, at most 280 characters, so an agent can compare it with its own intent before accepting the reuse. It is absent on an exact-key hit, where no judge ran, and on a hit from a pool shared across apps. ggui_runtime_submit_actionnow checks a dispatch against the card’sactionSpec, as the live channel already did. An undeclared action, or a payload the declared schema rejects, is answered as a normal tool result,{ ok: false, code: 'CONTRACT_VIOLATION', message, violations }, and never reachesggui_consume. A card that declares noactionSpecis not checked, on either path.- Hosts built on
@ggui-ai/agent-serveroffer the model only the tools it may be offered. Each MCP tool’s declared_meta.ui.visibilityis now read, and a tool whose visibility lacks"model"(the six app-onlyggui_runtime_*tools) is no longer declared as the agent’s capability.listModelVisibleTools({ url, bearer })returns one server’s model-visible tool names for your agent SDK’s tool filter, and the Google ADK, OpenAI Agents and Claude Agent samples now use it. The Claude sample’s default allowlist is derived the same way instead of kept by hand, and an explicitallowedToolsByServerstill wins. @ggui-ai/ui-gen/checkexportsgenerateSampleProps, the props a render check falls back to for a contract: each prop’sexample, then itsdefault, then a value filled from its schema type.- A handshake can use a judge other than the LLM caller.
HandshakeDecisionAdapterin@ggui-ai/mcp-server-handlers/renderstakes an optionalresolveRerank(ctx)that returns aRerankPair(a judge with the threshold it was measured on, now a named export), passed to the matcher as itsrerankpair; without it, matching is unchanged.@ggui-ai/negotiatorexportsRERANK_SYSTEM_PROMPT, the match / no-match definitions the built-in judge asks, so another engine’s judge can ask the same question. - A card now reads the host’s answer to its wake-up message. When a tap needs a new agent turn, the card sends the host a
ui/messageand says it was sent to chat; if the host refuses that message in-band, the card now says so and asks the user to send a message to continue, instead of leaving a tap that silently did nothing. The refusal is recorded as adoorbell.refusedevent with the host’s error code. The tap itself was never lost: it is already queued for the agent. - Text in a card that isn’t wrapped in
<Text>is now set at the theme’s body size,--ggui-font-size-base, instead of the browser’s 16px. On a theme whose body size is1.1rem, a plain paragraph now reads 17.6px, as<Text>already did. Every built-in theme’s body size is 16px, so a card on a built-in theme looks the same. Cards that@ggui-ai/mcp-apps-reactrenders into your own page take the same rule, so their plain text follows the card’s theme rather than your page’s font size.cssTokensForAppThemein@ggui-ai/ui-gensets the same size on the judged page’s body, so a visual judge reads text at the size a visitor sees. @ggui-ai/negotiator’sLLMCallerhas an optionalcallStructuredMetered, which returns{ value, usage? }: the tool input, with the call’s token usage as the provider reported it. The rerank judge uses it when a caller has it, soRerankDecision.tokenCostcarries real token counts.tokenCostis now optional. Absent means the call wasn’t metered: the caller has no metered method, the provider reported no usage, or the call threw.{ input: 0, output: 0 }now means only that no provider call was made. Code that readstokenCostdirectly must handle it being absent. ExistingLLMCallerimplementations compile unchanged;TokenUsageandMeteredare exported.@ggui-ai/mcp-server’s Anthropic caller implementscallStructuredMetered, so a handshake your server judges with an Anthropic model reports the response’s token usage as the decision’stokenCost. A response with no readable usage reports none, never zeros.@ggui-ai/mcp-server-handlers/rendersexportsgenerationInputsForHandshake(record, resolved): the input aggui_renderof a stored handshake hands its generator, minus the render’s ownsessionId. It comes with the steps it is built from,storyForHandshake,generatorInputForStory,appGadgetsForContractandfetchGadgetTypes. The render calls the same functions, so a server that starts that generation before the render arrives derives the same input. A render that sends anoverrideorinfraderives with them.- A handshake that carries a contract now searches your saved interfaces the way they were saved: its query embeds the contract summary with the intent, as every stored interface’s vector does, instead of the intent alone. Its similarity scores therefore sit on a higher scale, so they get their own floor of 0.50 before the reuse judge. A handshake with no contract searches by intent alone as before and keeps the 0.2 floor.
minCosineForRerankstill overrides both. The 0.50 floor was chosen by a replay of 28 labelled request pairs through the real matcher under two embedding models, as the highest value that finds every match found today without adding a wrong reuse. - The with-guuey samples move to
@guuey/*0.30.0 and@silverprotocol/*0.11.0 (AgJSON1.0.0-draft.8).@ggui-ai/agent-serverand@ggui-ai/mcp-apps-reactrun their contract tests against the same core; it is a development dependency of both, so nothing they install changes. The AgJSON version check stays major-only, so earlier1.0.0drafts are still accepted. In core 0.11.0, AgJSON’s reasoningeffortis an open string (unrelated to ggui’s effort levels, which stay a closed set), a memory record may carry_meta, andprompt.blockedmay carryreasonRaw. - A card that has its component and still shows nothing now says so. After each commit, the runtime checks what it drew. The host’s
component-emptyobservability event gains three reasons:rendered-nothing(the component drew no element),detached(the tree landed in a part of the page that is no longer shown, because another mount took over the card), andno-commit(the card never finished drawing within five seconds, for example a component that suspends with no Suspense boundary). Each is reported once per blank, with a console line naming it. Before, these blanks were silent on every channel. If your host switches on the reason, treat an unknown one as a blank of unknown cause. @ggui-ai/wireexportsuseActionPending(actionName):truefrom the moment a card’s action is sent until the agent’s answer lands on the card (a props update, an amend or a new render for the session), or 20 seconds pass. A control can show it is working (disabled,aria-busy, “Sending…”) instead of looking untouched while the agent reacts. The iframe runtime provides it, and a card’simport { useActionPending } from '@ggui-ai/wire'resolves. A dispatch the contract validator refuses, or a repeat of a spent one-shot action, never reads as pending.buildWireConfigtakes the matchingpendingInputsChangednotice and an optionalactionPendingBoundMs.- Generated cards use it. Every control that sends an action (a button, a select whose change sends, an input that sends on Enter) is disabled and marked
aria-busyfrom the moment it sends until the agent’s answer repaints the card, and a button shows a working label (“Saving…”). Only the control that sent the action looks busy; other controls of the same record may be disabled, and everything else stays enabled. - Breaking, if your server bound them:
@ggui-ai/mcp-server-handlersno longer ships the orgs, connector-keys and coupon handler families (the./ops-orgs,./ops-connector-keysand./ops-couponsubpaths and their root exports), andcreateGguiServerno longer takesopsOrgs,opsConnectorKeysoropsCoupon. Organisations, wallets and user API keys are features of the hosted service, whose eleven tools keep their names and shapes there.SINGLE_CALL_OPSdrops their three reads (ggui_ops_list_orgs,ggui_ops_list_connector_keys,ggui_ops_get_org_balance); a server that registers its own tools under those names passes them incontrol.singleCallOps. The apps family (opsApps) stays open, and its tool descriptions no longer describe the hosted service. A deployment that bound a removed family can stay on v0.24.0, or copy the handlers into its own code and register them throughcreateGguiServer’shandlersoption. - Text blocks carry an optional
phase:"interim"marks narration between tool calls, such as “Looking that up…”, and anything else or absent is answer text. The thread routes and the invoke client keep it. - A tap on a card no longer shows the visitor the action’s internal name or its data. While it is sent, the card announces “Sending…” to assistive technology only, named by the action’s declared
labelwhen it has one. When the host takes the wake-up message, nothing is drawn over the card. Only a tap the visitor must act on draws a notice: the host cannot receive a message, or refused it. - A card’s live channel no longer reconnects forever when the server refuses its credential.
@ggui-ai/live-channel’sChannelRegistryandWSTransporttake an optionalclassifyFrame, typedFrameClassifier, which returns aFrameVerdict. A frame you class'accepted'resets the reconnect budget, and one you class'refused-terminal'fails the WebSocket at once, so the registry moves to its next transport. Without it, behaviour is unchanged. The iframe runtime supplies one. @ggui-ai/mcp-apps-react-native’s theme provider accepts React Native 0.82 and later, whoseuseColorScheme()can return'unspecified'. A consumer on RN 0.82+ no longer fails to typecheck, and'unspecified'counts as no system scheme: thecolorSchemeprop wins, and without it the provider uses'light'.- If you use
@ggui-ai/ui-gen’s visual evaluator, scores on the inline (xs) canvas move. The card is captured at its natural height inside a stand-in for the host’s frame, and content wider than the card fails the new critical dimensioncanvas-overflow-x. The criteria block is now asked in a report-only call of its own, so it never changes a score, and its cost is reported apart from the judgement’s.
2026-09-26 — v0.24.0: generate on your Claude login, and a card knows its one-shot action is spent
Section titled “2026-09-26 — v0.24.0: generate on your Claude login, and a card knows its one-shot action is spent”- An exact contract match is served without waiting for the installed-blueprints walk, which used to hold up an app’s first handshake after a server start. The walk still runs alongside it, and a marketplace-installed match, a miss and every similarity match wait for it, so an uninstalled interface is never served.
createInstalledBlueprintsProvidertakes an optionalonWalkreporter that receives each walk’sscope,entries,evicted,listedRowsandsweepMs, and a reporter that throws is reported as a newwalk-report-threwissue kind. - A component can read whether its once-only action is already spent.
useActionSpent(name)in@ggui-ai/wireistruewhen the card’s contract declaresnameoneShotand the action already fired, in this render or before a reload, so a consumed control can paint as submitted on first paint instead of looking live until a refused press. It flips when the card’s own dispatch commits and when a later frame carries a spend recorded elsewhere. This release declares the hook; generated components are taught it in the next, because a card imports@ggui-ai/wirethrough its runtime’s fixed export list and a name that list lacks fails at load. For hosts that build their own wiring,buildWireConfignow returns the config together with thatactionSpentsource and takes an optionalspentInputsChangednotice. ggui serve --local-cli-loginis an opt-in way to generate without an API key: it uses the Claude login already on this machine. Your Claude plan’s terms and usage limits apply, and it is about 2x slower than an API key (2.2x at the median). It covers Anthropic routes only, a real Anthropic key always wins, and it is refused with--multi-userand--public-demo, because one person’s plan never serves other users.@ggui-ai/ui-genexportsCLAUDE_CODE_LOGIN_CREDENTIAL, the value a custom key resolver returns to select this path.- The handshake suggestion’s
blueprintMetadeclares an optionalmatchedIntent: the stored purpose of the saved interface a judged cache hit proposes, at most 280 characters, so an agent can compare it with its own intent before accepting, and re-handshake withforceCreateif it names a different task. Declared in this release; servers start sending it one release later, so a host that cached this release’s tool schema accepts it. Theintentyou send is stored with the interface and read again when later requests of this app are matched against it: describe the task, never the end user. - A handshake with
forceCreatenever proposes a saved interface. It skips the deployment pre-match and every find-similar probe, and goes straight to validating (and, if needed, repairing) the draft for a fresh build, which is what the pairedggui_renderalready did. Before, an agent that asked for a fresh build after an unwanted cache suggestion could be handed the same suggestion again. handshake.decidedtelemetry no longer carries the decision’s reason text, which could quote the agent’s draft or name its contract’s fields.selectionReasonis replaced byselectionReasonKind, one ofcurated-prematch,match-exact,match-semantic,verbatim,normalized,llm-repair,salvaged-subset,no-creds,negotiator-degraded,no-negotiatororunclassified, andselectionReasonHash, the first 16 hex characters of the reason’s SHA-256, so equal reasons still correlate. ATelemetrySinkthat readselectionReasonmust switch to the kind.@ggui-ai/mcp-server-handlers/renders: a negotiator’s decision takes an optionalreasonKind, typed by the newly exportedHandshakeReasonKind, and every negotiator the package ships sets it. Breaking for direct callers of the decision builders:buildCacheReuseResulttakes a required fourth argument, the tier that hit ('match-exact'or'match-semantic'), andbuildCreateFallbacktakes a required third argument, the fallback kind ('no-creds'or'negotiator-degraded'), placed before the optional request variance, so a variance passed third moves to fourth.ggui_runtime_pull’stool_invokedlog line carries thesessionIdof a successful pull, so the first and last pull of a session bound how long its card stayed mounted. A pull that fails, and every other tool’s line, are unchanged.- A generator with the runtime probe wired and no evaluator runs the probe once after the coding turns (before, it did not run at all). That covers
createUiGenerator({ enableRuntimeRender: true })without anevaluationconfig, and a directdispatchGenerationcall without one, where the probe is wired by default. The probe never blocks a card: a recoverable render crash gets one repair turn (one more coding-model call) and one re-probe, then the generation serves what it has, and any other result serves as it is. The generation’s metadata reports the result inruntimeProbeandevalMs.runtimeProbeis a status-discriminated record:status: 'ran'carries averdict('pass', or'fail'with the non-emptyfailChecksthat failed, in the order the check kinds are declared);'timed-out'/'infra-skipped'/'not-applicable'carry no verdict; every status that reached the check carrieselapsedMs, andqueuedMswhen the check waited for a probe slot. When a repair turn was taken,repairis{ attempted: true, compiled: false }(the pre-repair card is served and the top-level outcome is the probe that bought the turn) or{ attempted: true, compiled: true, trigger }(the repaired card is served, the top-level outcome is its re-probe, andtriggeris the probe that bought the turn). The bounds are the deployment’s inputs:createUiGenerator({ runtimeRenderProbe: { timeoutMs, heapMb, maxConcurrent } })builds one check per generator,maxConcurrentcaps that generator’s live probe workers (extra checks wait their turn in order),createRuntimeRenderCheck(config)builds a check directly,dispatchGenerationaccepts one asruntimeRender, andwarmupRuntimeRenderWorker()runs one real probe worker at boot and reports whether it could run, without throwing. Defaults unchanged: 30 s, 512 MB, no concurrency cap. @ggui-ai/negotiatorexports the rerank judge as a seam:RerankJudge, a function from a query and its candidates to aRerankDecision, andllmRerankJudge(llm), today’s LLM judge in that shape.MatchBlueprintDepsin@ggui-ai/mcp-server-handlers/renderstakes an optionalrerank: { judge, threshold }pair: the judge decides, its ownthresholdis the cut (judgeThresholddoes not apply to it), and a pair wins over a barellm. Without a pair, matching behaves exactly as before. Breaking for direct readers of the decision:RerankDecision.reasonis now optional, because a judge may decide without prose, so code that reads it as astringneeds a fallback.- The relay’s contract-violation answer is declared:
ggui_runtime_submit_actioncan answerCONTRACT_VIOLATIONwith theviolationsthat failed the card’sactionSpec, and@ggui-ai/protocolexportscontractViolationSchema. Declared in this release; the relay starts enforcing it one release later, so a host that cached this release’s tool schema accepts the answer. ggui_consume’s result declares an optionalnextStep, aggui_amend({ sessionId })hint present when the drain returned events, so an agent answering a tap repaints the same card instead of rendering a new one. Declared in this release; servers start sending it one release later.ggui_consumeandggui_runtime_submit_actionlog their session on thetool_invokedline, asggui_runtime_pulldoes:sessionIdplus counts and flags (eventCount,status,timeoutSandabortedon consume;consumerPresenton a committed dispatch), never gesture content.onBlueprintResolutionevents carry their request context: optionalappId,handshakeIdandsessionId, plusproposedBlueprintIdwhen the consumed handshake proposed a saved interface.@ggui-ai/mcp-serverexportsbuildLlmCaller, whose Anthropic calls take an optional{ signal }, so a deployment that builds its own handshake negotiator can bound them with a timeout. Without a signal, calls run as before.@ggui-ai/mcp-apps-reactand@ggui-ai/mcp-apps-react-nativedescribe themselves as MCP Apps host helpers in their npm description, keywords (mcp-apps), README and docs.
2026-09-25 — v0.23.0: a once-only action survives a reload, and ggui.json can clear what a deploy set
Section titled “2026-09-25 — v0.23.0: a once-only action survives a reload, and ggui.json can clear what a deploy set”- A spent once-only action stays spent after a reload. After a committed dispatch of a
oneShotaction, the built-in in-memory and SQLite session stores keep a record for that card, theai.ggui/renderslice lists the card’s spent names asspentOneShots, and the runtime seeds its guard from that record. On a re-served card, the first gesture on an action the card already spent is stopped and traced asone-shot-spent, exactly like a second gesture on a live card. The record counts only on the card it was written on, and only for names that card still declaresoneShot; a card replaced byggui_updatestarts fresh. The runtime stops the gesture, and how the control looks afterwards is still up to the component. Hosts that build their own wiring can passgetSpentOneShotsto@ggui-ai/wire’sbuildWireConfig. A customGguiSessionStorecan implement the new optionalrecordSpentOneShot. A store without it keeps today’s behaviour, and the server says so once at startup (spent_one_shots_not_durable). A malformedspentOneShotsis dropped and reported to the host asspent-one-shots-invalid, never silently. ggui.jsonaccepts"theme": nulland"generation": null. Locally they mean the same as leaving the field out: the shipped default tokens, and no generation route. Onggui deploythey clear the app’s stored theme or generation route, and the deploy printstheme cleared/generation cleared. Leaving a field out still leaves the stored value untouched.@ggui-ai/protocoladds the write form of an app theme.appThemeWriteSchemaadmitsnullon the optional members outside the attestation (mode,name,frameless,fonts,imagery), andsplitAppThemeWriteseparates those clears from the document to store, so a write door can clear one member without clearing the theme. Attested members (cssVariables,keyframes) and required ones never clear bynull.- A font face in the app’s stored theme that the card’s CSP blocks is reported to the host as
font-face-blocked, the same event a blocked host-announced font raises; it used to fail with no event. A face’s origin is admitted when the card mounts, so a face added on a new origin while a card is open loads on that card’s next mount. - An expanded card (
fit: 'fill') has one frame rhythm: a 16 px inset, first-level corners concentric with the panel’s, and ableedprop for an element that should meet the panel’s edges. A hero band that opens the card bleeds on its own, so cards already served keep their band edge to edge. - The handshake’s semantic match returns to its own similarity floor, 0.2. Version 0.22.0 raised it to 0.3 to match
ggui_search_blueprints, but the handshake scores a request’s intent alone against saved interfaces stored with their contract and intent. On that scale a real match (the same task, with the same contract) can score under 0.3, and 0.22.0 generated those fresh instead of reusing them.ggui_search_blueprintskeeps 0.3. The two floors become one number again once the handshake scores a request the way it stores an interface. - A host that declares
ui-message-turnin itsGgui-Host-Capabilitiesconnection header, because it delivers a later gesture as the agent’s next turn, gets nonextSteponggui_render, so the agent ends its turn when the card paints instead of long-pollingggui_consume.@ggui-ai/protocolexports the header name, the token andparseHostCapabilitiesHeader. A host that declares nothing, or only tokens the server does not know, sees no change. - The conformance kit grades the read-plane-only posture, where a server withholds the
ai.ggui/renderslice from tool results. A new catalog,runReadPlaneOnlyConformancefrom@ggui-ai/protocol-conformance/read-plane-only-conformance, checks that such a result carries the render locator on bothstructuredContent.resourceUriand_meta.ui.resourceUri, with the same value, that it carries no slice and nowsToken, and that reading the locator mounts the view. The posture now has its own SPEC section, §7.10.6. The host-helper catalog’s H2 check is stricter: a helper that advertisesserverResourcesis now sent aresources/read, and refusing, dropping or re-shaping it fails where it used to pass. Helpers that advertise onlyserverTools, as both first-party helpers do, grade as before. SPEC §5.5.2 adds that emitters SHOULD sendMISSING_META_GGUI_BOOTSTRAP, and that hosts SHOULD treatBOOTSTRAP_META_MISSINGthe same way. - The model registry’s lineup, the models a picker shows first, gains Claude Opus 5.5 and GPT-6 Luna. Claude Opus 5 becomes legacy and stays selectable. The default model, Claude Haiku 4.5, does not change.
ggui_ops_generate_blueprintreturns the full 64-charactercodeHash, the same keyggui_ops_register_blueprintreturns for the same code and the only form a code store accepts. It used to return a 32-character prefix, so its code could not be written to a guarded code store.@ggui-ai/negotiator’sLLMCaller.callStructuredreturnsPromise<unknown>and no longer takes a type parameter: an implementation returns the tool input as the model produced it, and the caller parses it, as the rerank judge and contract synthesis already did. An implementation written ascallStructured<T>(…): Promise<T>still satisfies the interface. A call that passes a type argument no longer compiles; drop it and validate the result.@ggui-ai/iframe-runtimeexports every observability event type from its package root.OneShotUnenforceableEvent, new in 0.22.0, andComponentEmptyEventwere reachable only from the./observabilitysubpath.- When the OAuth flow hands consent to an external consent page, that page is given only the name the client registered. For a client registered without a name, a
client_nameon the authorize link used to reach the page and show as the client’s self-declared name. It is now dropped, and the client reads as an unnamed client, as it does on the built-in consent page. - Generation: a runtime probe that runs out of time is reported as
timed-out, with its elapsed time and the host’s load, never as a component crash. A slow host no longer tells the coding agent to fix a crash that did not happen. A patch the engine refuses before applying anything is reported to the coding agent asPATCH_INVALIDand logged, instead of being counted as a failed self-check that never ran.
2026-09-23 — v0.22.0: a once-only action holds without a live session, and the self-hosted card never waits in silence
Section titled “2026-09-23 — v0.22.0: a once-only action holds without a live session, and the self-hosted card never waits in silence”- A once-only action now holds even when the card paints without a live session: past the session token’s lifetime, before the live channel connects, or on a host with no live channel. The
ai.ggui/renderslice carries the contract’sactionSpecwhole, on every transport, and the runtime’soneShotguard reads it from there. When a card has an action but no action spec reached it (for example, a render from an older server), its first dispatch postsone-shot-unenforceableto the host once. Before, the guard failed open without any signal. A spec the runtime can read only in part is reported, never dropped quietly:action-spec-invalidfor a spec the write door would refuse, andaction-spec-member-strippedfor an entry member from a newer server.@ggui-ai/wire’sbuildWireConfigtakes an optionalonActionSpecAbsentfor hosts that build their own wiring. - The default MCP Apps shell a self-hosted server serves never waits in silence. If the host never sends the tool result, or never answers the view’s
resources/read, the card shows a failure with Retry after 30 seconds and postsggui:bootstrap-failedwithMISSING_META_GGUI_BOOTSTRAP; a failed read’s cause rides in the message instead of being reported asMALFORMED_BOOTSTRAP. A host that never answers or refusesui/initializenow getsUI_INITIALIZE_FAILEDposted, not only a card. The reasons and the 30-second bound are the hosted runtime’s. - Blueprint reuse is stricter: the handshake’s semantic match now uses the same similarity floor as
ggui_search_blueprints(0.3, was 0.2), so a short request that is the same task in fewer words may generate fresh instead of reusing a saved interface. - A saved interface whose intent is only a stand-in — an operator placeholder, an id, a manifest
name, or a persona — is never proposed by semantic match; it is still reused when a request’s contract matches exactly. A row a persistent store kept from an earlier version carries no such mark until it is registered again. To keep an installed blueprint eligible, give its manifest adescription; an operator registration keeps it with aseedPrompt. - Every generation names the build that made it:
GenerationMetadatagains an optionalbuild(GeneratorBuildin@ggui-ai/mcp-server-core— an optional packageversion, an engine-definedmode, and contentdigests), so results can be grouped by the engine build that produced them.@ggui-ai/ui-gen’screateUiGeneratorfills it on every generation, as its preciseUiGenBuild(the design mode, and digests of its prompt and boilerplate templates);createAdvancedUiGeneratorand any generator that reports no build leave it absent. - Under the hood: an app theme written through
ggui_ops_set_app_themethat carries overlay variables this server does not know, such as ones a newer@ggui-ai/designprojects, is stored verbatim and the names are logged, instead of refused, so a newer caller can write to an older server during a rolling upgrade (a theme that leaves a known variable uncovered is still refused). In generation, a component that ignores a declared prop gets one bounded feedback round instead of shipping, a declared chat card is composed as a card for the chat bubble, eval calls’ prompt-cache tokens count towardtokens.total, a run the same-exchange guard ends says so onGenerationResult.sameExchangeBreak, and the Claude Code login child starts with an allowlisted environment instead of the parent’s, so a provider key or switch in the parent’s environment no longer reaches it.
2026-09-23 — v0.21.0: three new models, and the tool-call guards they need
Section titled “2026-09-23 — v0.21.0: three new models, and the tool-call guards they need”- New models:
claude-opus-5-5,gpt-6-solandgpt-6-lunacan be selected by name; no default moves. Models that refuse a forced tool call — Claude Opus 5.5 and Claude Fable 5.1 — are no longer sent one, in generation or in the blueprint judge, and the gpt-6 generation sends the token-cap field the OpenAI API expects. - OAuth: the authorization response names its issuer (RFC 9207
iss, advertised in the server metadata), and client registration is rate-limited per IP (10 registrations per 10 minutes). - Sandboxed frames can preflight the public read routes — the runtime bundle, component code and session reads — and get the same open access the reads themselves always had.
- Generation and reuse: the visual judge checks the card at the size it is declared to render, prompt-cache reads are reported for Google and OpenAI routes, and an action’s control shows its contract label verbatim. The blueprint judge now weighs only cached cards that clear the similarity floor (before, only the closest one had to), and
GGUI_CACHE_TRACE=scoreslogs each reuse decision with its keys and scores but none of the request text. - A theme’s motion tempo now reaches the card. Every design-system transition reads
--ggui-motion-duration-{fast,base,slow}and--ggui-motion-easing-{standard,emphasized,exit}, so a theme that statesmotion.duration/motion.easinganimates the card at its own tempo; a theme that states none animates at the shipped tempo, as before. (Recorded late: this entry first went out without it.)
2026-09-19 — v0.20.0: a security fix to the OAuth registration door — upgrade if you run --oauth
Section titled “2026-09-19 — v0.20.0: a security fix to the OAuth registration door — upgrade if you run --oauth”- Security fix (ggui-ai/ggui
@ggui-ai/mcp-server): the Dynamic Client Registration door now shape-checks everyredirect_urisentry before storing it, boundsclient_name, and the consent page names who is asking and where the browser will be sent. Before this release a registration could carry a malformed or unintended redirect target through the door. Self-hosters runningggui serve --oauthshould upgrade to0.20.0; the hosted service already has the fix (the hosted consent page named the client and destination from 2026-09-21). - The theme manifest is treated as a wire: a variable added to it in a newer release completes from what an older release’s theme already carries, so a theme composed on the previous version is not refused at the door — and the previous release’s manifest is pinned as the fixture that catches the next such growth.
- Generation: form inputs are primed before the action-wiring probe runs, so a control that only enables once its field has a value is exercised rather than reported missing; the OpenAI adapters now report non-cached input and cache-read tokens separately, so a cost row reads what the provider actually billed.
2026-09-17 — v0.19.0: a spent action stays spent, and the theme names what the card cannot read
Section titled “2026-09-17 — v0.19.0: a spent action stays spent, and the theme names what the card cannot read”- A once-only action is spent for the life of the render, not just disabled on screen: a second gesture on a
oneShotcontrol is stopped by the runtime and traced as suppressed — it never reaches the agent and it is never dropped silently. - The theme says what it cannot do instead of skipping it: a document member the card’s projector cannot read produces a named diagnostic rather than the same output as if it were absent, the members no card paints yet say so on their own type, and corner radius is now projected by role — a host’s cards and its controls take two values, and the control primitives read theirs.
- The stored theme can be read back in the shape a later write carries: beside the render-time read, a carry read returns exactly what was stored, so a host can reproduce a theme without re-deriving it.
- Under the hood: a render names how it resolved its blueprint — what it looked for, whether it hit, and whether the registry deduplicated — an
override.contractre-aims at its key the wayoverride.variancealready did, and the Google adapter hands its key to the model directly, so a staleGOOGLE_GENAI_API_KEYin the environment can no longer win over the key you configured.
2026-09-16 — v0.18.0: a contract can say an action fires once, and the card can be dismissed
Section titled “2026-09-16 — v0.18.0: a contract can say an action fires once, and the card can be dismissed”- An action that should happen once is now declared, not guessed: an action entry can carry
oneShot, the generated card disables that control after it fires, and the agent is bound by the contract rather than by reading labels. Everything not markedoneShotstays repeatable — a control is never guarded by accident — andconfirmremains an advisory signal for the author, not a gate. - The card can be dismissed by the host: a new host-bound
ggui:dismissintent forwards the user’s dismiss gesture to the host and never acts on it inside the card, so closing means whatever the host decides it means. - The doors are stricter and say why: a theme write that would destroy stored members it does not carry is refused, and the refusal names exactly which members would be lost (clearing is an explicit
theme: null, never an omission); a stored contract or theme carrying a member from a later release is read with that member stripped and named, not silently dropped; and the generation profile has one read door for everything that reads it. - The host’s type scale and rhythm now reach the card:
typeScalemoves the type scale andrhythm.basere-derives the spacing — carried on the wire since 0.17.0, painted from this release.
2026-09-15 — v0.17.0: a theme can name its typeface and imagery, and the hosted card loads them
Section titled “2026-09-15 — v0.17.0: a theme can name its typeface and imagery, and the hosted card loads them”- A theme declares its own typeface and imagery:
typography.facesin the document (https:sources) rides the wire asfonts, alongsideimageryslots for a brand mark, a hero image and a repeating pattern. The hosted card admits every declared origin in its content-security policy and inlines the@font-facerules in the page it serves, so a named family actually loads — andggui deploynow carries the faces the document declares instead of dropping them. - Declared but not yet painted: the type-role scale, the bounded motion tempo, and the
scrimoverlay are carried on the wire and validated at the door, but no card wears them yet — the work that makes them visible is still in flight. Nothing breaks if you declare them today; nothing changes on screen either. The motion tempo became visible in v0.21.0. - A read door never drops a theme in silence. A theme this release refuses is reported to the embedding host with the reasons, and a theme carrying members a NEWER writer sent is kept, stripped, and reported with the member names — so a rolling deployment can see exactly what an older card could not read.
- A card the host shows fullscreen fills the frame it was given: the frameless silhouette is a fit rule now, the fill is anchored on the frame rather than a parent, and a mounted list no longer inherits the browser’s bullets and indent.
- A long-held poll ends the moment its caller goes away, and a relay error counts toward backing off — a card whose host times out its own calls no longer re-fires held requests forever.
- For anyone running the evaluators: the visual judge names the design tree it painted with, a terminal action the static probe cannot reach reads as not-measured rather than as a failure, and an evaluation round that throws reports the failure as its result instead of returning nothing.
2026-09-10 — v0.16.0: theming revised — layering roles, one anchor per family, the app theme on the wire
Section titled “2026-09-10 — v0.16.0: theming revised — layering roles, one anchor per family, the app theme on the wire”- Theming is revised (
draft-2026-09-10): atheme.jsonstates the six layering roles —ground/onGround,container/onContainer,sunken/onSunken— and one500anchor per colour family; the ramps, inks, containers, outlines and the type scale are derived.font.size,font.lineHeight,motion.duration,motion.easingand$metadata.fontUrlare refused, andggui serverefuses to start on a theme file in the old shape —ggui theme validate <path>names the field. - An app’s theme on the wire is its projection for both modes:
{ overlays: { light, dark }, overlayHash, mode?, name? }. The embedding host owns the runtime mode — its announced mode wins and the app’smodeis a default — and a theme the write door refuses answersinvalid_app_configwith one of four bodies (uncovered,unknown,overlayHash: "mismatch",refused: "v1 shape"). - Fonts are declared as
typography.faces(https:sources only);ggui deploynames families the hosted path does not deliver yet. - The runtime theme-registration tools (
ggui_ops_register_theme/ggui_ops_list_themes/ggui_ops_delete_theme) are gone. ggui servestops cleanly on Ctrl-C / SIGTERM: exit code 0, nothing native on stderr. Before, with the local embedding model loaded, every stop ended in alibc++abi … mutex lock failedabort (SIGABRT). The boot log now also says when the model is ready ([ggui:embedding] warm — local model loaded in …ms), so a supervisor can tell semantic search is live. The drain is bounded: if a handle ever keeps the process alive past 5 s after shutdown,ggui servenames it on stderr and exits with the same code instead of hanging.ggui serve --multi-tenantis nowggui serve --multi-user. The flag turns on per-user LLM-key management at/settings(each authenticated user manages their own keys; builder identities are rejected at that gate) — the old name described a party this server does not have. No alias: the old flag is refused as unknown.- The endpoint-level refusal’s
data.refusalcarriesappId— the app the refused endpoint serves, equal to the endpoint path’s{appId}— so a repair loop keys on it instead of parsing the message. A server built against 0.15.0 sends the refusal without it. UNAUTHORIZEDmoves from-32001to-32007:-32001is the MCP SDK client’s ownRequestTimeout, minted locally — a client reading the number could not tell a server’s refusal from its own timeout, the class the-32000move closed.-32001is retired-reserved.- Two refusal codes lose the word “tier”:
model_not_in_tier→model_not_allowed(the caller picks a model the app is allowed to use — the one refusal an agent may act on itself) andalready_on_tier→subscription_unchanged(the subscription requested is the one the app already holds). A server built against 0.15.0 still emits the old names; the 0.16.0 conformance kit refuses them. - A
PendingEventConsumeradapter appends and drains the protocol’sPendingEvent—{id, envelope, createdAt}, the envelope always the entry object; thesequencefield, which nothing ever wrote or read, is gone. Every row is validated through the newpendingEventSchemaat the store boundary:appendrefuses a malformed row before storing it (PendingEventMalformedError), and a drain never returns a row that fails the schema nor drops a well-formed sibling because one failed — the sqlite adapter refuses the drain whole and rolls back, a DynamoDB-style adapter quarantines the row with apending_event_malformedlog and delivers the rest, andggui_consumereports a refused drain as a typed failure carrying{events: [], status}rather than a raw error. The bytes of a well-formed row are unchanged. - The server-level instructions and the tool descriptions now say the same thing, and the true thing, about the render response: a refusal is five keys (
{code, message, fix, retry, handshake: 'intact'}); the retry rule follows the refusal’sretryclass (after-fixis yours only whenfixByiscaller;laterretries after its delay;next-periodandneverare not retried); a missinghandshakeIdis an input-validation error (-32602) while an unknown one fails as not found; arenderedorfailedrender consumes the handshake and arefusedrender or a recoverable validation error leaves it intact; a consume event carries seven keys (type,sessionId, and the five gesture fields);ggui_consumeteaches end-your-turn on an empty drain instead of “exit only when expired”; the never-emittedmissing_propscode is gone from both, andggui_list_sessionsis described by thehostName+hostSessionIdpair it actually takes. A contract test pins every one of these against the booted server’stools/list. @ggui-ai/wire’s connection state splits into a read view and a claimed writer: the root barrel exportsconnectionSource({subscribe, getSnapshot}, whatuseRender().isConnectedreads) and nothing that writes;connectionStore/ConnectionStoreare gone. The writer lives on@ggui-ai/wire/internal—claimConnectionWriter(name)— and a deployment’s runtime claims it once, eagerly at boot; a second claim throwsConnectionWriterConflictErrornaming both parties — the store is one per document (anchored onglobalThisunderSymbol.for('ai.ggui.wire/connection-store'), shared by every copy of the module), so the same name twice really is two copies of the runtime in one document, and a foreign value at that slot is refused withConnectionStoreSlotErrorrather than adopted or overwritten; a released writer that writes throwsConnectionWriterReleasedError. Generated component code cannot reach the writer: it is not on the barrel, the import rewriter refuses@ggui-ai/wire/internalat load time, and the generation gate refuses it before any code is emitted — one forbidden list, three doors.- A render denied by the per-app render-rate cap is a refusal like any other render-gate refusal —
outcome: 'refused',refusal.code: 'app_rate_limited',retry: 'later'carrying the wait, handshake intact — instead of a thrown error whose text lost the code, the fix and the retry class. RateLimitedErroris gone from@ggui-ai/mcp-server-core: a limiter denial has no thrown form — the render handler returns the registry’sapp_rate_limitedrefusal; theRateLimiterseam (RateLimitCheckInput,RateLimitDecision) stays. Hosted GGUI’sRateLimitedError → 429arm went with it, so no first-party server emits-32013any more.- A rate-limit decision can now say whose cap bound:
RateLimitDecision.scopeis'app'(the default when absent — existing limiters are unchanged) or'issuer', and the render gate’s refusal follows it — an issuer-cap denial isissuer_rate_limitedand names the issuing identity, where before every denial said the app was over its cap. - All 32 published
@ggui-ai/*packages moved to0.16.0in lockstep.
2026-09-05 — v0.15.0: declarations that resolve everywhere, and a browser entry
Section titled “2026-09-05 — v0.15.0: declarations that resolve everywhere, and a browser entry”- Every published type declaration now carries explicit
.jsextensions on its relative imports, so@ggui-ai/*typechecks under TypeScript’sNodeNext/Node16module resolution as well asbundler. Before this, a NodeNext project sawno exported memberon names such asPLATFORM_ERROR_CODES. @ggui-ai/protocol/wireis a browser entry: the same names, only the modules a browser validates. The iframe runtime and the React / React Native host helpers import it; the iframe bundle is 37 KB smaller.- Authorization refusals on the MCP endpoints answer JSON-RPC
-32001(UNAUTHORIZED) instead of the generic-32000, so a client can tell a refusal from a dropped connection. A deployment’s error mapper may attach JSON-RPCdata— a structured reason — to such a refusal. ggui_consume,ggui_list_sessionsandggui_emitdeclare their wire shapes from the protocol;ggui_emitnow advertises theseqit always returned.- The per-app MCP endpoint answers GET and DELETE with
405+Allow: POSTinstead of a text/html 404, so an optional SSE listener (Google ADK’s) stops loggingNot Foundevery turn. - If a host refuses the iframe’s
ui/initialize, the iframe runtime now records the boot as failed and keeps its relay closed, instead of forwarding gestures from a document that never connected. - All 32 published
@ggui-ai/*packages moved to0.15.0in lockstep.
2026-09-05 — v0.14.0: a refusal is an outcome, and Node 22 is the floor
Section titled “2026-09-05 — v0.14.0: a refusal is an outcome, and Node 22 is the floor”- A render can now be refused before anything is generated, and the refusal is its own outcome — not a failure dressed as one.
ggui.jsondeclares protocoldraft-2026-09-04; the loader refuses a manifest whose stamp it does not support withUPGRADE_REQUIRED, and a dated migration doc walks the change. ggui_renderaccepts a model route in either wire form (provider:modelorprovider/model), and LiteLLM aliases resolve in both.- Handlers are built with
defineHandler— a handler’s output type derives from its schema, so a result that omits or invents a key fails to compile rather than being silently stripped on the wire. Failed calls can now be observed withpostFailureHook. - Rich text renders GFM pipe tables as tables.
- Every
@ggui-ai/*package now requires Node 22 or newer. - All 32 published
@ggui-ai/*packages moved to0.14.0in lockstep.
2026-08-31 — v0.13.0: calm chrome for launch day
Section titled “2026-08-31 — v0.13.0: calm chrome for launch day”- The pre-render loading surface is neutral and scheme-aware now — a light ground in light mode, a dark ground in dark mode — instead of the fixed dark slab that flashed while components rendered. Themes still repaint it the moment they load, and embedders keep a one-variable override.
- A branded working-state mark plays while a component renders: GGUI’s four glyphs assemble, swap inks, and clear on a calm cycle. Serving deployments can replace it with their own mark (or disable it) via the shell assembler’s new
loadingIndicatoroption; it honors reduced-motion and retires the instant real content paints. - The generated-component feedback row is rebuilt and off by default: two theme-aware thumb icons (up / down) replace the old text buttons, and the row renders only for deployments that opt in via the runtime’s
uiFeedbackboot option. - Package builds publish atomically — a dependency’s compiled output is never observably half-present to concurrent tooling.
- All 32 published
@ggui-ai/*packages moved to0.13.0in lockstep.
2026-08-24 — v0.12.0: the server tells the truth about itself
Section titled “2026-08-24 — v0.12.0: the server tells the truth about itself”- The MCP
serverInfo, the MCP AppsappInfo, and/ggui/healthnow all report the real shipped release instead of placeholder versions — every connected agent sees which build it is actually talking to, and a new runtime constant keeps that identity locked to the published packages. - A registered theme now travels with the render: the server delivers the theme’s full light/dark token ladders alongside the overlay, and the runtime installs the delivered ladder as the base — a registered theme looks the same everywhere, with no compiled-in copy to drift.
- Theme registration gained guardrails: a coverage validator proves a theme defines every token the design system consumes, unknown theme ids are refused where they were typed, and a brand-shaped overlay gets a pointer to full registration instead of silently painting on defaults.
- Solid-accent components (buttons, active tabs, checkbox marks) read their text color from the theme’s on-accent tokens instead of hardcoded white, with a WCAG AA contrast sweep over every producible pairing now standing in the test suite.
- Generated UIs are checked for prop sensitivity — a displayed value that ignores its prop is caught before it ships.
- All 32 published
@ggui-ai/*packages moved to0.12.0in lockstep.
2026-08-21 — v0.11.0: rendered cards match the app around them
Section titled “2026-08-21 — v0.11.0: rendered cards match the app around them”- Host-announced color palettes now reach generated UI: the spec’s
--color-*variables translate onto GGUI’s design tokens and layer beneath the app’s own theme — the app’s theme always wins, the host fills what it didn’t set. - A per-app theme now selects the full base token set, not just an overlay: its color mode picks the right light/dark ladder, and a theme whose name matches a registered preset gets that preset as its base. Themes can also declare themselves
framelesswhen the embedding app draws the card’s outline itself. - The React Native host view no longer paints its own border, and it reports content-size changes through a new
onSizeChangedcallback so embedders can auto-size the card. - Tool-result listeners now register before the host handshake on every boot path, closing a timing gap on re-emitted results.
- All 32 published
@ggui-ai/*packages moved to0.11.0in lockstep.
2026-08-19 — v0.10.0: renders validate against the schema the handshake promised
Section titled “2026-08-19 — v0.10.0: renders validate against the schema the handshake promised”- The handshake now returns the enforced props schema for a blueprint — plus its hash and profile — and every render is validated against that exact schema before it ships. What the agent was promised is what the view receives.
- New protocol helpers for working with props schemas: build the enforced schema, canonicalize it to stable bytes, classify its profile, and validate data against it. Servers get a dedicated
props-schema-hashentry point for cache keys. - The conformance kit gains a props-schema catalog — seven polyglot cases any implementation can run to prove its schema handling matches the spec.
- All 32 published
@ggui-ai/*packages moved to0.10.0in lockstep.
2026-08-13 — v0.9.0: host helpers get their real name
Section titled “2026-08-13 — v0.9.0: host helpers get their real name”- Breaking:
@ggui-ai/reactis now@ggui-ai/mcp-apps-react, and@ggui-ai/react-nativeis now@ggui-ai/mcp-apps-react-native. Update your imports — the old package names are retired. The new names say what these packages are: MCP Apps host-helper libraries, not a GGUI SDK. GguiRenderanduseWebSocketare removed from both packages. Rendering semantics live in the mounted view (iframe runtime), never in the helper.- First release of the React Native host helper:
@ggui-ai/mcp-apps-react-native, with<McpAppIframe>as its front door. - All 32 published
@ggui-ai/*packages moved to0.9.0in lockstep.
2026-08-11 — hub.ggui.ai: browse the public registry
Section titled “2026-08-11 — hub.ggui.ai: browse the public registry”- The registry browser is live at hub.ggui.ai — search published blueprints and gadgets, filter by the MCP tool or server they bind to, and see works-with chips and verified-publisher badges at a glance.
2026-08-10 — v0.7.0: renders that outlive their cache
Section titled “2026-08-10 — v0.7.0: renders that outlive their cache”- Reopening an old card from chat history now works even after its render was evicted from cache:
resources/readre-mints the view from its stored blueprint and committed body, so old cards rehydrate instead of going stale. - Resource-read failures are typed against a closed error enum, so a host can tell “gone” from “denied” from “malformed” instead of guessing.
- Hosted renders are kept indefinitely by default.
- All
@ggui-ai/*packages moved to0.7.0in lockstep.
2026-08-08 — Host-capability awareness
Section titled “2026-08-08 — Host-capability awareness”- The iframe runtime now reads the host’s advertised capabilities at boot, instead of assuming every embedding host behaves the same way.
- Hosts that don’t relay
ui/messageback to the agent get an honest, one-time explanation in the UI instead of a gesture that silently goes nowhere. - Relay incapability is only latched after a confirmed failure — a host is never assumed broken just because it hasn’t advertised a capability yet.
2026-08-08 — Browser CORS support on the MCP plane
Section titled “2026-08-08 — Browser CORS support on the MCP plane”- The MCP server now mounts a browser CORS layer with an allowlisted-origins model, so browser-based MCP clients can call it directly without a proxy.
- Origin and Host validation is enforced on both the HTTP and WebSocket upgrade ingresses, closing a DNS-rebinding gap.
- The CLI gained a
--browser-originflag (andGGUI_BROWSER_ORIGINSenvironment variable) to configure which origins are allowed. - The
/ggui/healthendpoint now surfaces the effective allowed origins for debugging.
2026-08-07 — v0.6.3: rich text
Section titled “2026-08-07 — v0.6.3: rich text”- UI components generated by GGUI can now carry rich-text descriptions — bold, italics, links, and inline code — instead of plain strings only.
- The rich-text parser is now vendored directly in the design package rather than pulled in as an external dependency.
- All
@ggui-ai/*packages moved to0.6.3in lockstep.
Protocol baseline
Section titled “Protocol baseline”The current protocol draft is draft-2026-09-10. See Version policy for how draft versions relate to semver on @ggui-ai/protocol, and what changes would trigger the next draft bump.