Who can write
- There is no agent write path. No MCP tool registers a widget, theme, schema, action or secret. Agents learn the contract through
get_authoring_guide,list_widgets,list_schemas,list_actionsandlist_theme_tokensand draft import JSON; a person imports, validates and saves it in an authenticated session in the designer at widgentic.dev/app. See Authoring with an agent. - API keys are read-only by default because they travel into third-party hosts and prompt-injectable contexts. The
writescope can never be granted to a key. executeis an opt-in scope fixed at key creation. A read-only key still renders widgets that carry http actions, but their descriptors are markeddisabled: "scope"and noloaddescriptor is emitted;execute_actionanswersFORBIDDEN_SCOPE. See Keys and scopes.
Templates are data
- A template has no expressions. Data selects —
bindemits a value as text,mappicks an author-written literal,prefixcomposes an author-written prefix with a value,formatpresents a value through a closed number/currency/date vocabulary — and the author supplies every literal. Bindings only ever produce text and attribute strings, never markup. - Tags on the denylist fail validation with
FORBIDDEN_TAGand render as nothing if validation was bypassed:script,iframe,frame,frameset,object,embed,style,link,meta,base,template,noscript. on*andsrcdocattributes are rejected (FORBIDDEN_ATTRIBUTE) and skipped at render time; so are hand-writtendata-wg-*attributes — only a validatedactionbinding produces a descriptor.- URL-bearing attributes (
href,src,action,formaction,xlink:href,data,poster,ping) keep onlyhttp,https,mailto,telor relative references.data:is accepted solely for base64data:image/*on animgsrc. Aprefix-composed value faces the same guard. - Interpretation is bounded by a deterministic node budget (every
eachiteration costs at least one unit) and a maximum nesting depth, so a stored template driven by a large payload cannot spend the process. The values are on Template DSL. - Custom
stylesand theme values cannot escape a declaration or fetch a resource: no braces, semicolons, angle brackets,url(orexpression(, and style selectors must target.wg-classes.
Actions execute on the server
- An
httpaction runs server-side through a guarded fetch: publichttpsonly; targets that resolve to private, loopback, link-local or metadata addresses are refused before any bytes are read, with the connection pinned to the validated address; redirects are failures; an 8-second total deadline covers connection, headers and body; responses are capped at 256 KiB and must beapplication/json(orapplication/*+json) that parses and satisfies the declared output schema. - Arguments are accepted only for fields the input schema declares (
INVALID_ACTION_INPUTotherwise) and may not share a name with a fixedqueryparameter; the author’s fixedheadersandqueryare applied after the arguments, so they always win. - Bindings resolve from the store, never from the request.
execute_actionnames a widget kind and a binding identifier; the definition — URL, method, headers, schemas — comes from the caller’s composed catalog. Aurlorheadersfield on the request is ignored. promptactions never reach the server. The text is resolved at render time and the frame sends it asui/message; the host prefills the composer and the person decides to send.- The iframe never fetches. The app template has no external references; it talks only to the host, and http actions travel host to server to target. The one exception is images on declared resource domains (the operator’s, or the person’s own from Settings), which a host honoring the declaration loads directly in the viewer’s browser. That exposes the viewer’s address to those domains, and hosts add them to their script, style and font sources too, so a person should list only domains they control, never shared hosting. A declared domain that redirects loads only when its target is declared too. Stored widgets and render inputs cannot add a domain. Executions are rate-limited per principal (60 per minute by default) and request bodies are capped at 4 MiB.
- After a successful http action the widget posts its new payload to the model’s context (capped at 8 KiB per part), so the model and the visual never disagree.
Secrets
- Referenced by name only —
{ "secret": "weather-token" }in a header or query value, never in the URL, body or input mapping — and resolved from the executing principal’s own secrets at execution time. A missing one fails withUNKNOWN_SECRETbefore any network activity. - Never displayed. Listing returns
name,createdAtandupdatedAt; the authoring surfaces never show a value or preview after entry; a secret referenced by an action cannot be deleted (SECRET_IN_USE). - Always redacted. Every string emitted about an execution — errors, diagnostics, tool text, log lines — has each resolved value replaced by
***, including percent-encoded and JSON-escaped forms and object keys; store and vault error text is replaced by a fixed message before it leaves the server. - Envelope encryption. Each write generates a fresh random 256-bit data key, encrypts the value with AES-256-GCM, wraps the data key with the deployment’s key-encryption key through a cipher port, and persists only
{ alg, kekVersion, wrappedKey, iv, ciphertext, tag }plus the name and timestamps. Two writes of one value produce different ciphertext. In production the KEK is a vault key that never leaves the vault (@widgentic/mcp/secrets/keyvault); the executing app performs one unwrap per resolution, and records can be re-wrapped under a new KEK version without decrypting the value. A store without a cipher refuses to store or return secrets at all.