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_schemasandlist_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 — 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 touches the network. The app template has no external references and declares no CSP domains; it talks only to the host, and http actions travel host to server to target. 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.