# AI agents and approvals

> Let the AI perform declared actions, with an autonomy ladder and a human approval queue.

_Updated: 2026-07-21_

The AI in Railbase can do more than answer questions — it can *perform work* in
your instance. What it may perform is never open-ended: every action is declared
in advance by a plugin, ranked on an autonomy ladder, and above a certain rung it
stops and waits for a person. The controls live under **Settings → AI**
(`/_/settings/ai`) on the **Agents**, **Policies** and **Approvals** tabs.

## The AI can only run declared actions

A plugin declares the actions the AI may perform in its bundle manifest, under
`manifest.ai`. Each declared action carries:

- a **name** — a dotted identifier such as `helpdesk.reply.send`;
- a **verb** — an existing `METHOD /api/…` route of that plugin. A declaration
  binds the AI to a route that already exists; it never creates a new endpoint.
- an **RBAC permission** — mandatory at every tier except `read` and `analysis`;
- an **autonomy tier** (see below);
- an optional **impact** class: `money`, `legal`, `security` or `destructive`.

Validation is fail-closed. An unknown tier, a malformed verb, an unknown impact
class, a duplicate action name, or a permission-less action outside
`read`/`analysis` makes the bundle fail to install rather than land
under-specified.

When the AI plans, the model may only choose from that declared catalogue. The
verb, permission and autonomy of a step are copied from the **declaration**, not
proposed by the model, so the model cannot raise its own privileges or retarget a
step. Any step naming something that was not declared is discarded — the admin
console lists what it threw away under **Dropped (undeclared)**.

> [!NOTE]
> The catalogue only holds actions from plugins that are installed **and** whose
> license is active or on trial. A dormant plugin's actions are not executable and
> do not appear. See [Installing plugins](installing-plugins).

## The autonomy ladder

Every declared action sits on one rung of a seven-step ladder, and the rung
decides what happens when the action is run:

| Tier | What happens |
|---|---|
| `read`, `analysis`, `draft`, `safe_write` | Runs immediately |
| `business_write`, `external_side_effect` | Pauses for a human decision |
| `dangerous` | Never runs — denied and recorded |

Execution always goes through the same REST pipeline a human request goes
through, under the AI's own account. Authentication, licensing, RBAC and audit
apply exactly as they would to a person, so a permission the agent does not hold
comes back as a plain denial rather than a silent success.

## Policy can tighten, never loosen

On the **Policies** tab, **Autonomy overrides** lets you demote an action beyond
what its plugin declared. Type the capability — that is the action name, for
example `helpdesk.reply.send` — pick a tier (`safe_write`, `business_write`,
`external_side_effect` or `dangerous`) and press **Demote**. **Lift** removes an
override and returns the action to its declared tier.

The rule is **tighten-only**, and it is enforced in the code, not by convention:

- an override that is *more* restrictive than the declaration wins;
- an override that is *less* restrictive is ignored — the declaration wins;
- a malformed or unrecognised value is ignored, falling back to the declared tier
  rather than to something open;
- `dangerous` stays forbidden regardless of any policy row.

The override is applied before planning as well as before execution, so the plan
you see in the console already shows the tier that will actually be enforced.

## The approvals queue

An action that needs confirmation does not fail and does not run. It **pauses**:
Railbase writes a `waiting_approval` record holding the action name, the verb,
the tier and the exact input, and returns that record's id to the caller.

The **Approvals** tab is where a person decides. Filter by
`waiting_approval` (the default), `approved`, `denied`, `consumed`, or all
states. Each row shows the action, its verb, its tier, when it was requested, and
the **full input as JSON** — what you approve is what you can read on screen.
Waiting rows offer **Approve** and **Deny**; decided rows show who decided and
when.

Two properties make an approval hard to abuse:

- **Single-use.** Spending an approval atomically flips it to `consumed`. The same
  approval can never authorise a second execution.
- **Input-bound.** The approval stores a hash of the exact input the human
  reviewed. Resuming with different input, or with a different action name, is
  refused — so nothing can be swapped in between your click and the execution.

A decided row is immutable: only a `waiting_approval` row can be decided, so an
approval cannot be flipped after the fact, and a denial cannot later be turned
into an approval.

> [!NOTE]
> The queue an administrator sees spans every tenant on the instance — that is
> deliberate, since the operator reviews the whole instance. The resume path is
> tenant-scoped: an approval raised in one tenant cannot be spent from another.

## Money, legal and security always need a person

When a plugin declares an action's impact as `money`, `legal`, `security` or
`destructive`, the human sign-off becomes **unconditional**:

- it applies even when the tier alone would have run automatically;
- the caller's own "I already confirmed this" flag is **not accepted** — a plugin
  cannot self-confirm its way past it. Only a real approval you decided in the
  admin lets the action execute.

That distinction survives in the record. The audit entry for an executed action
that needed sign-off notes whether it was authorised by a verified approval (with
the approval id and the administrator who decided it) or merely asserted by the
caller, so an auditor can tell the two apart forever after.

> [!NOTE]
> The Policies tab also has **Deliberate on risky actions** — an optional extra
> gate that routes side-effecting actions through a model council and verifier
> before they run, denying anything that comes back as a blocking risk. It is off
> unless you switch it on, and if no model is reachable it declines to block
> rather than stopping your work.

## Granting the agent permissions

The **Agents** tab manages the AI's own account. It is a first-class identity —
not a borrowed user login, not an impersonation — created on first use, one per
tenant, with a stable id shown on the screen.

- **Enable / Disable** pauses or resumes all agent actions. Disabling keeps the
  account and its grants; a disabled agent's actions are denied.
- **Role grants** is where you authorise it. Pick a role and press **Grant** — a
  grant made here is instance-wide, and each listed grant shows its scope
  (`site`, or a tenant id). **Revoke** takes one back. Until the agent holds a
  role carrying the required permission, a permission-gated action is denied by
  the normal RBAC check. With no grants at all it can only run permission-less
  read and analysis actions. Roles themselves are managed in
  [Users & access](users-and-access).
- **Plan & run** is the operator console: state a goal, and the model plans over
  every installed plugin's declared actions. Each step shows its action name and
  tier, and **Run** executes it. A step that pauses shows `waiting_approval` with
  its approval id, and the button becomes **Resume** — approve the row in
  Approvals, then Resume spends that one approval.

Granting a role to the agent is an explicit, audited operator act. Nothing grants
it implicitly: installing a plugin that declares actions does not give the AI the
permissions those actions need.

## What gets recorded

Every agent action that is executed or denied lands in the AI audit trail on the
**Audit** tab, attributed to the AI as actor, with the capability, the effective
autonomy tier, the decision and the outcome. A pause leaves its record in the
approvals queue rather than the audit trail. Entries are hash-chained and can be
verified from the same screen, and exported to XLSX. The instance-wide audit
timeline under **Logs** is covered in [Operating from the admin](operations).
