Skip to content

Security model ​

A block is text an agent wrote. The agent may be wrong, and the text may carry instructions it picked up from a file or a web page. So the package treats every block as untrusted input. This page lists what a block can do, what it cannot, and what the host must still check.

What a block can do ​

  • Draw components from the catalog with props that pass their zod schema.
  • Declare local state with literal JSON defaults, and bind it to visible controls.
  • Compute text and props with the expression language.
  • Set or reset its own local state from a Button.
  • Declare a read of a source the host registered, with arguments that match the host's schema.
  • Name a file, a diff, a commit or a node for the host to check.
  • Offer Choices, each of which sends one visible message once.

What a block cannot do ​

  • Run code. There is no JavaScript, no function call besides the fixed helpers, no assignment and no loop besides @Filter and Each.
  • Reach the network. No component takes an address it loads. A Source is opened only by the host, after a person clicked it, and only for http or https.
  • Read a file. File, Diff, Commit and Node are names the host checks; Image takes only an attachment id of the chat or generated="latest", never a URL or a path.
  • Style anything. Unknown props are refused with refused_prop and dropped. No prop takes a class, a color or a size.
  • Reach prototypes or accessors. __proto__, constructor and prototype are refused anywhere a key is read or written (safeKey). Only own data fields are read.
  • Change anything outside itself. State lives in the page and resets when it reloads.
  • Send a message without a person. Only a click on an enabled Choice sends, and only once per block.

Budgets ​

Every step of work is counted, so a block cannot hang the backend or the page. UiBudget counts parser steps, nodes, nesting, iterations, string length and elapsed time against UI_LIMITS, inside helpers as well as in ordinary evaluation. Each reserves all its iterations before it allocates a child. A reply is also capped as a whole by UI_REPLY_LIMITS, and the host's reads per block by UI_HOST_LIMITS. The numbers are on Compilation and streaming.

A block that exceeds its budget is replaced by its text and one budget_exceeded diagnosis. A part that fails alone keeps its own fallback; the parts beside it still draw.

Values that cross in ​

The interpreter never touches a host object. copyUiValue copies every value that enters it: state defaults, query results, submitted input and the variables of evaluateUiExpression. It accepts finite JSON only and refuses class instances, accessors, sparse arrays, cycles and non-finite numbers, without calling a getter. Numbers that an expression produces must stay finite too.

Inputs and choices ​

A choice sends what a person could see, and nothing else.

  • A control changes state only through its binding, only after its own tag closed, and only with a value its schema accepts. A read-only control (a prop that is not a bare reference to a local variable) has no binding.
  • resolveUiChoice runs on the backend, over the stored block, never over what the client says the block is. The client sends only identities and the values of declared inputs.
  • A submitted value must equal its default, be held by a visible complete control within the options it offers, or be the exact value a visible enabled Button sets. Anything else is refused with invalid_value.
  • A hidden, disabled or unfinished Choice cannot be sent. Neither can a block that is still streaming.
  • The message is the Choice's evaluated context, or its label without one. Both were on screen.

Actions ​

A Button holds one action: @Set($name, literal), @Reset($name) or @Reset(). The compiler refuses anything else, and refuses a @Set whose value is not a constant. That keeps a pressed Button's result independent of the state it was pressed in, so the backend can check it. An action changes local state only. It never reads a query, sends a choice or calls the host.

Live queries ​

  • The source of @Query is a literal, and the host's registered schema must accept the arguments, or the block does not compile.
  • Arguments may read only declared local state. A query result can never choose a source or another query's arguments.
  • The backend captures the writing chat's access once per reply and keeps it on the backend. A client never sees it and cannot replace it. A fork captures its own chat's rights.
  • Every read authorizes again against that captured access and the chat's current rights, including a read the cache answers.
  • A choice in a block with queries needs a fresh read id for every query. The backend uses the values it issued, never values the client sends.

The agent names a target; the host decides. uiLinkTargets lists only targets of visible, complete nodes. The host checks each against the writer's captured access and its current scope, answers a chip or plain text, and checks the stored node again when a person opens it. A client never sends a free target: it names a node, and the backend reads the target from the stored block.

What the host still owns ​

  • Which sources exist, their argument and result schemas, and who may read them.
  • Whether a path, a commit or a node is inside the scope of the chat.
  • How a Source address opens, and that a page never preloads it.
  • Which clients may send chat.uiChoice, ui.query and ui.link. The agent host answers them only for a client attached to the chat.

Licenses and third-party notices are listed in each package.