# Human-in-the-Loop

> Human-in-the-Loop pauses a run for a person's approval at a boundary you choose (any delete, a given MCP server, a cell, an agent, a model) and gathers every request in one queue in the Console. Approve lets the run continue. Rules publish with your config; manage them on Govern → Human-in-the-Loop. As of September 25, 2026, OpenEng AI isn't live yet.

**Human-in-the-Loop** (HITL) pauses a run for a person's approval at a
boundary you choose — before any delete, before a call to a given MCP server,
before a cell runs — and gathers every request in one queue in the Console.
**Approve** proceeds immediately; **Reject** stops the run. Runs that reach a
token cap set to *prompt* park in the same queue. It lives on
**Govern → Human-in-the-Loop**, a page with three tabs: **Approvals**,
**Rules** and **Contacts**.

> [!WARNING] Status on September 25, 2026: OpenEng AI isn't live yet
> The Console and Web Console interfaces load, but the OpenEng AI API routes and the Broker aren't
> deployed. Nothing can be saved or published, and no AI engine can register or connect. Pro,
> Enterprise and pay-as-you-go launch in Q4 2026 and can't be purchased yet. This page describes
> OpenEng AI as it works once those services are live ([status](ai.md)).

*Figure: Govern → Human-in-the-Loop — the Approvals queue first, then the Rules builder and the Contacts registry; rules publish with your config.*

## How it works

1. You write **rules** on the Rules tab: which boundary to gate, and whether
   to ask before the call or before its result returns.
2. Rules **publish with your config** and freeze into that config version, so
   pinning or rolling back a config pins or rolls back its rule set too.
3. When the engine serves the config, it turns every enabled rule into a gate
   at its gateway, in the same ordered table as [plugins](plugins.md). Its log
   says `HITL: ACTIVE — config approval rules gate the served turn`.
4. When a run reaches a matching boundary, it pauses and the engine registers
   an approval request with the OpenEng API (the run, thread and config ids,
   the target and operation, and the rule's settings and message). The
   request appears on the **Approvals** tab.
5. You decide. **Approve** and the run resumes at once; **Reject** and the
   run halts.

HITL is active only on a Pro or Enterprise plan with at least one enabled
rule. Otherwise the engine logs `HITL: inert (Free plan or no enabled rules)`
and a matching rule lets the call proceed with a notice.

## Rules

**Rules** lists your rules; each row has a delete action. A new rule starts as
*approve every delete*, which you can change:

| Field | Values |
|---|---|
| **Name** | e.g. *Approve all deletes* |
| **Gate this kind of boundary** | **Worker**, **Agent**, **MCP server**, **Model**, **Cell**, **Function**, or **Any (everything)** |
| **Which one** | Depends on the kind (below); *Any* has no selector |
| **When** | **entry** — approve before the call; **exit** — approve before its result returns |
| **Pass information** | A setting for how much secret-scrubbed run context a request may carry: **none**, **digest** (the default) or **full**. The current engine sends the setting with the request but no run context |
| **Enabled** | A disabled rule never gates |

| Kind | Which one |
|---|---|
| Worker | A glob over the worker id and operation, e.g. `*.delete` (any delete) |
| Agent | An agent's `agt_` public id, or `*` for all agents |
| MCP server | An MCP server's `mcp_` public id, or `*` |
| Model | A model role, or `*` |
| Cell | A cell's `cell_` public id, or `*` |
| Function | A function name |

A rule gates a whole MCP server, never a single tool of one: the API refuses a
rule that names a tool. Every new rule asks in the Console queue, halts the run
on reject, and has order 10.

## Approvals

The queue lists every pending request and refreshes every 10 seconds. Each
request shows:

- the caller and the target, with the boundary type and point (entry or
  exit);
- the rule's message, if it has one;
- the config and version the run uses (`<config id>@<version>`), the pass
  information level, and when the request expires.

**Approve** resumes the run immediately; **Reject** halts it. A decision is
recorded once: deciding again returns the first decision. A request that no
one answers expires after 24 hours and counts as rejected.

**Token-cap breaches** appear here too, as a **Token cap reached** card with a
meter of the tokens used against the cap. **Proceed past cap** waives that cap
for the rest of the turn; **Stop the run** ends it. To raise a cap for good,
open the config (the card links to it) and change its
[Token caps](configs.md#token-caps) step. Account-wide
[token limits](govern.md#token-limits) raise the same prompt.

## Contacts

**Contacts** is a registry of approver channels a rule can reference:

- **Name**;
- **Channel** — email, Slack, webhook, or none;
- **Channel reference** — a `${ENV_VAR}` placeholder, never a literal address
  or URL.

Contacts publish with your config alongside its rules. The rule builder
doesn't link a rule to a contact yet, so decisions are made in the Approvals
queue.

## Plan & tier notes

Human-in-the-Loop is a **Pro and Enterprise** feature. On Free, the page
explains what it does in place of the tabs, and publishing a config with an
enabled rule is refused; the same rule disabled still publishes. Pro and
Enterprise launch in Q4 2026 and can't be purchased yet
([Plans & billing](account.md#ai-pricing)).

## Troubleshooting

**"A worker rule needs its workerMatch selector"** (or the same for another
kind). Fill in **Which one**, or pick *Any*.

**"mcpServerRef must be an mcp_ server publicId (never a tool — RD3)".** Use
the server's `mcp_…` id or `*`, not a tool name.

**"The approval queue is temporarily unavailable."** The queue couldn't be
loaded; it refreshes on its own.

**A rule never pauses anything.** Check that it's enabled, that the config is
published with it and the session runs that version, and that the engine log
says `HITL: ACTIVE`. `HITL: inert` means the plan isn't Pro or the served
config has no enabled rules.

**A request disappeared before I answered.** It expired (after 24 hours) and
counted as rejected.
