s-m-r-t
Open the s-m-r-t source on GitHub Switch to dark color scheme

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.

  1. 01 Generated model tools page
  2. 02 Mounted UI tools page
  3. 03 Component tools page
  4. 04 Server principal tools server

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.

01 Generated model tools Tools written from the record declarations; calls run over the application's web routes. Browser · the signed-in person
02 Mounted UI tools Six fixed tools that read and operate the forms and tables on the page. Browser · the signed-in person
03 Component tools A tool one component declares for itself while it is on the page. Browser · the signed-in person
04 Server principal tools The application's own agents, working as a persona's bound user. Server · a bound user account

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.
Manifest
RESTMCPWebMCPCLIBrowser
Each interface reads the same declared model capabilities.

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.

src/lib/objects/Report.ts
typescript
@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.

The default is read-only

Registering tools without an exposure policy offers only read-effect tools. Write and destructive tools appear only when the application names those effects deliberately. The policy can also prefix the page's tool names and cap how many register at once.

@happyvertical/smrt-web · index.d.ts
typescript
export interface WebMcpExposurePolicy {
  /** Allowed effects. Omitted means read-only exposure. */
  effects?: readonly ('read' | 'write' | 'destructive')[];
  /** Prefix every registered tool name with `<namespace>_`. */
  namespace?: string;
  /** Optional maximum tools registered by one call. */
  maxTools?: number;
}

A component's own tool follows the same rule

A component can declare a tool of its own while it is mounted. That tool goes through the same policy as the generated set. One that does not declare its effect counts as destructive, the same fail-closed reading an undeclared custom action gets. Under the read-only default it is left out. The policy on the page's Provider always wins over anything the component says for itself, even when the Provider's policy is narrower.

  • The name prefix and the tool cap apply only to the generated set. A component tool keeps the name its author chose and is never counted against a generated set's budget.
  • The tool a rich form opts into declares itself a write, never destructive. It falls back to allowing reads and writes for itself only when no Provider above it states a policy.
  • On a browser without the interface, or under a policy that excludes the tool's effect, registering it does nothing.

Selection and permission are different questions

The exposure policy answers one question: which tools does this page offer. It does not answer whether a call succeeds. The package contract says so in its own words: this capability policy is not authorization — the authenticated REST surface remains the auth, tenant, field-write, and sensitive-data boundary.

Where a mistake fails closed

Suppose a page offers a tool it should not have. What happens next depends on the source. A generated model tool still arrives at an authenticated route as the signed-in person, in that person's tenant, under that person's permissions and field rules. A tool the person could not use fails there, the same way it would fail for the person.

  • A component tool runs whatever its component wrote, in the browser. If that code calls the application's routes, the same server checks apply. If it does not, there is no server round-trip to fail closed; the exposure policy is what bounds it.
  • A declared interaction never calls the application's routes. It dispatches one registry command marked as an agent's, so its boundary is the registry's own rules: staged review and a person's own confirming gesture.

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 browser plane: the person's own authority

A generated tool registered in the page executes over the application's web routes as the signed-in user — the same session, tenant, and permissions the person already has. Persona allow-lists and agent-class ceilings do not participate on this plane. There is nothing for them to bound, because a page tool holds no identity of its own.

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.

BROWSER PLANE

Page agent → registered tools → generated fetchers

acts as: the signed-in person.

SERVER PLANE

Application agent → persona → principal run

acts as: the persona's bound user, inside three limits.

Authenticated application routes identity · tenant · permission · field rules — checked on every request.
Records

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.

Scripted demonstration · released registry

A proposal succeeds. A protected change fails.

This scripted adapter uses real controls, validation, classification, staging, and confirmation policies. No language model runs in this demo.

Small example object

Subject
Profile · 42
Form ID
profile-demo

3–30 characters. Start with a letter.

A stored secret exists. The form and adapter do not receive its value.

Permitted proposal

  1. Find and explainRead stable identity, label, constraints, and policy.
  2. Validate the proposalUse the same rule as the visible form.
  3. Stage separatelyKeep the proposal apart from the live value.
  4. ReviewCompare the live and proposed values.
  5. Confirm and applyA real click supplies the confirmation an agent cannot send.
  6. UndoRestore the previous registry-held value.
Live valueWillow Reed
Proposed valueNot staged
Proposal checkNot checked

Policy refusal

Awareness is not authority

The registry can identify the recovery-key control. Its secret classification removes read and mutation authority.

No request sent The adapter supplies no protected value to this attempt.

Choose the first step to run the permitted proposal.

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.

src/routes/+layout.svelte (starter template)
svelte
<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>
LayerDefaultWhat changes it
Installed libraryOffers agents nothingThe application registers definitions in a page
Registration with no policyReads onlyAn exposure policy that names write or destructive effects
Mounted UI toolsOff, even when model tools are onA second, separate opt-in on the Provider
Starter templateRead-only model tools on, feature-gatedEditing one derived value in the root layout

Continue by task

Use the section that owns the next question.