Agents
Agents work with borrowed authority.
A s-m-r-t application can offer its operations to software agents: an agent in the browser page, an agent connected over MCP, or one of the application's own scheduled and chat agents. Each connects through a defined door, and every door is a choice the application makes.
Whichever door it uses, an agent acts as someone — the signed-in person on the page, or a user account bound to it on the server — and the server checks every request against that identity. The front page says an agent never has more power than the person it works for. This section is the mechanism behind that sentence.
About agents that use a running application. Coding agents, which help build one, are covered in Tooling.
Agents 01
Four ways in, on two levels of trust
An agent reaches a s-m-r-t application through one of four defined surfaces. Three live in the browser page and act as the person using it. The fourth runs on the server and is never visible to the browser.Three doors open from the page
A page can offer an agent tools from three sources. Generated model tools come from the application's record declarations and run over its own web routes. Six fixed UI tools describe and operate the forms and tables currently on the page. A component can also declare a tool of its own for as long as it stays mounted.
- Generated model tools carry the same names, descriptions, and input rules as the rest of the application.
- The six UI tools list, inspect, and operate form controls and data views.
- A component tool answers to the same exposure policy as the generated tools, and it is removed when its component leaves the page.
- All three run as the signed-in page user. No page tool carries an account of its own.
The fourth door never reaches the browser
The application's own agents — the ones that follow written instructions, run on schedules, and talk in chat — do their work on the server, as a user account bound to their persona. These server tools are a separate surface, and they are never registered in a browser page.
- A server agent acts as the user account bound to its persona.
- Its allowed tools are a fixed list stored with that persona.
- Its actions are recorded as done on behalf of the person they serve.
One set of rules behind every door
Whichever surface a request arrives on, the server resolves the same questions: who is asking, in which tenant, with which permissions, touching which fields. The door decides how a request arrives, never whether it is allowed.
Three doors share one plane and one identity; the fourth has its own. The rest of this page follows that split.
Agents 02
One declaration writes every tool
The actions a record declares become its web routes, its commands, and its agent tools. The api selection on the model gates all of them, and every action carries an effect class — read, write, or destructive — that exposure policies filter on.Tools are projections, not copies
REST, CLI, MCP, and WebMCP are four projections of one declared operation set, sharing one discovery and invocation contract. A browser tool is named after its record and action — an Article's list action is the tool article_list on every agent surface.
- The api selection gates the browser tools too: an action excluded from the web API has no browser tool.
- Descriptions and input rules come from the model's own fields and metadata.
- Whether an action needs a record id is decided by the method itself — an instance method is item-scoped, a collection method is not — and route configuration cannot change that.
Every action has a fixed effect class
Listing and reading are read. Creating and updating are write. Deleting is destructive. This classification is fixed for the standard actions — configuration cannot soften it — and it is what an exposure policy filters on.
An undeclared custom action counts as destructive
A custom method with no declared route metadata is classified destructive, non-idempotent, and open-world, so a browser capability policy never fails open. Under the read-only default that makes it invisible to page agents until its declaration says otherwise — and the contract asks you to declare safer semantics only when they are true.
@smrt({
api: {
exclude: ['delete'],
routes: {
preview: {
method: 'GET',
effect: 'read',
idempotent: true,
openWorld: false
}
}
}
})
export class Report extends SmrtObject {
async preview() {
// A declared read: visible to page agents under the read-only default.
}
}Agents 03
Showing a tool grants nothing
An exposure policy chooses which tools a page offers, and offering none of it is the default: registration without a policy exposes reads only. Whether a call to the application succeeds is decided at the authenticated route it runs through — never by the fact that a tool was visible.Agents 04
Browser agents and server agents never trade places
A page tool always acts as the signed-in person; personas and agent-class limits play no part there. A server agent always acts as its persona's bound user, inside three limits at once. The two planes meet only at the application's authenticated routes.The server plane: three limits at once
An application agent runs its work as the user account bound to its persona. What it can actually do is the overlap of three lists: what that user may do, what its class of agent may ever do, and what this persona is allowed to call. An action outside any one of the three does not run.
- The database applies the bound user's row rules to every query in the run.
- Every action is recorded as done on behalf of the person it serves.
- Server principal tools are never registered in a browser page.
They meet only at the boundary
Both planes end in the same place: one authenticated application request, resolved as one identity, in one tenant, against one permission set. Nothing else crosses between them. This is the mechanism behind a sentence the front page states without proof: an agent never has more power than the person it works for.
Page agent → registered tools → generated fetchers
acts as: the signed-in person.
Application agent → persona → principal run
acts as: the persona's bound user, inside three limits.
Agents 05
An agent asks the page what it means
Mounted forms and data views register their identity, meaning, constraints, and current state. Agent tools read those registries — never the rendered screen — and a proposed change waits for a person's own click to apply.Controls carry their own description
Every registered control has a stable identity and a published meaning: kind, label, options, constraints, unit, sensitivity, and current state. A data view registers its columns the same way. The six fixed UI tools read these registries at call time; they do not inspect or simulate the screen.
- The six tools do not change as components mount and unmount — the answers do.
- Secret values are never serialized into a tool response.
- Tool responses are marked as untrusted content for the agent reading them.
Propose, then a person confirms
An agent can stage a value on a control. Staging records who proposed it and when, and places the proposal beside the current value for review. Applying it requires a person's own gesture, checked by the registry while the gesture is happening — input that merely claims to be a confirmed user action is rejected.
- The review surface is a released component, StagedControlReview — not custom code each application writes.
- Apply, clear, and undo stay on the human side of the line.
- A protected field refuses staging outright. The refusal is part of the demo below.
A component can declare an interaction as data
A component can also declare one interaction of its own as data with no code in it, such as advancing a table one page. The declaration holds an id, a description, an effect it may declare, and which registered control or data view it addresses. Bound to that control or view while the component is on the page, it becomes a page tool like any other. Running it dispatches one registry command marked as an agent's, so the staged review and the person's own gesture above apply unchanged.
- The declaration has no place for a function, a URL, or a route. What runs is built from the target it names, and it never calls the application's routes.
- A declared interaction follows the page's exposure policy like every other page tool, and one that does not declare its effect counts as destructive.
- A declared interaction needs registries to dispatch to: the Provider's mounted-UI registries, or ones handed to the component directly. With neither, binding it registers nothing.
- The six fixed UI tools are unchanged. A declared interaction sits above them and cannot claim their reserved prefix.
See it run
The same demo that runs on the front page and the UI overview: an agent finds a field, checks its proposal against the same rule a person sees, stages it, and is refused on a protected field. Nothing changes until the confirm click — which an agent cannot send.
The same demo, with its technical notes, is on the UI overview and in the Playground.
Agents 06
The defaults are quiet, and every step up is deliberate
The framework always prepares the tool descriptions; the application decides whether any reach a browser. Saying nothing more offers reads only, and the mounted UI tools take a second, separate decision. The starter template makes the read-only choice for you.A description is not an offer
Build time always emits the tool definitions — there is no switch for that, and none is needed, because a definition in a bundle does nothing. Tools exist for an agent only after the application registers them in a page.
One ladder of decisions
Each row is a separate decision with its own quiet default. No earlier row implies a later one. A component's own tool adds no row. The exposure policy on the second row governs it too, and a component tool that declares no effect counts as destructive.
The starter's choice, in full
The starter template registers the generated read-only tools in its root layout. Three qualifications travel with that choice: it is read-only; it is feature-gated, so server rendering and browsers without the interface get nothing; and it does not enable the six UI tools, which remain a separate decision.
<script lang="ts">
import { webMcpToolDefinitions } from '@happyvertical/smrt-virt-web';
import { Provider } from '@happyvertical/smrt-svelte';
// Keep the optional WebMCP registration out of browsers that do not expose
// it. SSR remains safe because the feature check is document-guarded.
const webmcp = $derived(
typeof document !== 'undefined' && 'modelContext' in document
? {
definitions: webMcpToolDefinitions,
basePath: '/api',
effects: ['read'] as const,
}
: false,
);
</script>
<Provider {webmcp}>
<!-- … -->
</Provider>| Layer | Default | What changes it |
|---|---|---|
| Installed library | Offers agents nothing | The application registers definitions in a page |
| Registration with no policy | Reads only | An exposure policy that names write or destructive effects |
| Mounted UI tools | Off, even when model tools are on | A second, separate opt-in on the Provider |
| Starter template | Read-only model tools on, feature-gated | Editing one derived value in the root layout |
Continue by task