.. include:: /Includes.rst.txt .. _adr-109: ============================================================================ ADR-109: Agent Runs approvals inbox (backend module) ============================================================================ :Status: Accepted :Date: 2026-07-22 :Authors: Netresearch DTT GmbH .. _adr-109-context: Context ======= The human-in-the-loop suspension features had service plumbing but no operator UI. A run can suspend WAITING_FOR_APPROVAL (:ref:`ADR-084 `) or WAITING_FOR_INPUT (:ref:`ADR-105 `); ``AgentRuntime::approve()`` and ``submitInput()`` continue it. Until now the only caller was the admin-only Tool Playground developer tool — there was no first-class surface for an operator to find the runs that need a decision and act on them. .. _adr-109-decision: Decision ======== Ship a new admin-only backend submodule ``nrllm_runs`` ("Agent Runs") — an approvals **inbox** focused on the runs that need a human, plus a read-only list of recent terminal runs for context. Progressive enhancement, no AJAX -------------------------------- The page works fully with JavaScript OFF: native ```` POST to module-route ``controllerActions``, a POST-redirect-GET flush with session flash messages, native ``
`` for long tool arguments, and native form validation. The one JavaScript module is enhancement only — it moves focus to a 422 error summary reliably across browsers. There is no JSON/AjaxRoutes path and no content-negotiation dual-path — a deliberate simplification over the playground's batch-JSON contract. Authorization is the module ``access => admin`` on all three actions (``list`` / ``approve`` / ``submitInput``). A module-route action cannot be reached without it, so ``RequiresBackendAdminTrait`` (whose JSON 403 body would be wrong for an HTML page) is not used here. **Any** admin may act on any run; the recorded ``decidedBy`` / ``submittedBy`` uid is audit-only. The CSRF defence is the backend module route token the ```` URL carries (validated by the ``RouteDispatcher``), not ``__trustedProperties``. Four non-negotiable correctness/security properties --------------------------------------------------- 1. **No-JS coercion.** A native ``
`` posts every field as a string, but the validator is strict and ``submitInput()`` validates verbatim. A ``SchemaInputCoercer`` casts the POST to the schema's declared types AND omits empty OPTIONAL fields, so a blank optional integer/boolean does not 422 the whole submission; a non-numeric string for an integer is left uncoerced so the validator rejects it with a clear per-field error. A shared ``SchemaPropertyClassifier`` is the single ``type → control`` mapping used by both the widget factory and the coercer, so a rendered widget can never drift from its coercion. 2. **Never an empty form.** ``InputSchema::isUsable()`` returns ``true`` for a scalar top-level schema like ``{"type":"string"}``, which would render an empty, unsubmittable no-JS form. The input form renders ONLY for an object schema with at least one property; anything else is classified ``unreadable`` and shows a fail-closed notice, never an empty ````. 3. **Stale-review binding.** ``approve()`` binds only to the run uuid and reloads whatever suspended state is CURRENT — a deny/continuation can re-suspend the SAME run with a NEW pending turn, so a stale tab (or a second admin) could authorize a call the operator never reviewed. The reviewed turn's digest (a SHA-256 of the pending calls) travels as a hidden field; the controller recomputes it from the freshly-loaded current state and refuses ``approve()`` on a mismatch, re-rendering "the pending action changed, please re-review". The narrow check→approve window is consciously accepted; eliminating it entirely would require a runtime change to ``ApprovalDecision`` and is out of scope. 4. **Honest load errors.** The two new persister queries return ``null`` strictly on a store error (an empty list only when there genuinely are none), so a DB hiccup shows a visible error infobox instead of a silently empty inbox that could hide waiting runs. Accessibility (owner priority) ------------------------------ WCAG 2.1 AA and full keyboard operability are first-class: ``h1 → h2`` section landmarks → ``h3`` per run; native ``