> ## Documentation Index
> Fetch the complete documentation index at: https://sdlc-rstack.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Approvals & Governance Queue

> How pending human gates, guardrail overrides, and decisions surface in the Business Hub and reach the real .rstack approvals.json.

RStack's governed loop stops at human gates by design — a missing artifact
approval, a spent attempt budget, an unresolved Decision Queue item. The
Business Hub is where a manager actually sees and clears those gates from a
browser, without shelling into the run directory. This page covers the three
surfaces that carry that work: the **Action Inbox** (one aggregated queue),
the **Approvals** page (the page that can actually act), and the
**Decisions** page (the DoR/Decision Queue). Guardrail-override one-shot
approvals and the audit trust boundary that every approval record passes
through are covered inline, because both pages render them.

## How it works

### Action Inbox — one source-linked queue, read-only

`src/observability/dashboard/ui/pages/action-inbox.js` renders `s.actions`
into a single filterable list (`all` / `blocking` / `approvals` / `decisions`
/ `failures` / `resolved`, via `ACTION_INBOX_FILTERS`). Each action already
carries a `type` (`approval`, `decision`, `configuration`, `alert`, `audit`,
or a generic failure), a `severity`, a `status`, an `availability` flag, and
an `allowedActions` array. `actionInboxIsResolved` treats
`approved`/`rejected`/`consumed`/`resolved`/`expired` as closed.

The Inbox never mutates state itself — `actionInboxCardHtml` renders a
**route** button (`actionInboxRoute`) that sends you to the surface that can:
an `approval` action routes to `approvals`, a `decision` routes to
`decisions`, an `audit` action (a rejected approval record) also routes to
`approvals` since that's where the Audit Rejections panel lives, and
everything else falls through to `alerts-guardrails`. If the server didn't
list any `allowedActions` for the action (or `availability !== 'available'`),
the card says plainly **"No mutation available here"** instead of implying a
button exists.

### Approvals page — the surface that can act

`src/observability/dashboard/ui/pages/approvals.js` splits `s.approvals`
into `pending` (`!item.status || item.status === 'pending'`) and `resolved`
(anything else, capped to the last 20). Each pending card renders an
**Approve** / **Reject** button pair that calls `resolveApproval(id, action)`.

Two things a card can say about itself, both server-derived, never guessed
client-side:

* **One-shot guardrail overrides.** An approval whose `artifact` starts with
  `guardrail-override:` is flagged with a note explaining that approving it
  grants **exactly one** further attempt for the blocked task, after which
  the override is spent and the task blocks again on the next attempt.
* **Consumed lifecycle.** `item.lifecycle === 'consumed'` (computed server-side
  by `annotateApprovalLifecycle` in `state/approvals.js`, cross-referencing
  the run-level approval history) flips the card's status to `consumed` so a
  spent override never reads as a standing "approved" forever.

### Audit Rejections — the tampering-visibility panel

The same module injects an **Audit Rejections** panel
(`OPS_AUDIT_PANEL_HTML`) fed from `approval_audit_failed` events in the feed.
Every approval record is audited before it is trusted (harness-side, see
below); a record that fails that audit is treated as **absent** — the gate
it would have unblocked stays closed — and this panel is where that
rejection becomes visible instead of silently disappearing. Each entry shows
the artifact, the audit's stated reason, any structured `issues[]`, the
record id, and the claimed status.

### Decisions page — the Decision Queue and DoR status

`src/observability/dashboard/ui/pages/decisions.js` renders three things
from `buildDecisionState` (`state/decisions.js`, which reads
`readDecisions`/`dorCheck` per run): the pending **Decision Queue** items
(`decision_id`, `question`, `recommendation`, `impact`, `required_before_stage`),
a **Readiness** list (per-run Definition-of-Ready `PASS`/`WARN`/`FAIL` with a
score), and a **Decision Log** — the chronological record of who resolved or
waived each decision and when, built client-side from any decision whose
status is `resolved` or `waived`. This page is read-only in the sense that
resolving a decision happens through `sdlc_decisions` / `rstack-agents
decisions`, not a Hub button — no fetch call for decisions appears in this
module.

### Guardrail triggers, retry state, and overrides (Alerts & Guardrails)

`ui/pages/alerts-guardrails.js` is the deeper operational view behind a
guardrail block: a **Retry State** panel folds `retry_decision` and
`task_retry_*` events per task into a state (`scheduled`, `exhausted`,
`human_required`, `validator_blocked`, `resolved`), and a **Guardrail
Triggers** panel lists `guardrail_triggered` events. For each trigger,
`opsOverrideStatusHtml` looks up the exact override — checking for a
`guardrail_overridden` consumption event first, then the run's own
`approvals[]` for a `guardrail-override:<taskId>` record — and reports one of
four honest states: consumed, approved (with approver), rejected, or "no
override on file, the task stays blocked."

### How a browser approval reaches `.rstack/approvals.json`

`resolveApproval(id, action)` in `approvals.js` first resolves an approver
name (prompted once, cached in `localStorage`) and an approval token
(prompted once, cached in `sessionStorage`), then POSTs to `/api/approve` or
`/api/reject` with the token in the `x-rstack-approval-token` header.

Server-side (`src/observability/dashboard/server.js`):

* The route is **disabled by default** — `approvalAuthError` returns 403
  unless `RSTACK_APPROVAL_TOKEN` (or `RSTACK_APPROVAL_TOKEN_FILE`, re-read
  per request so rotation needs no restart) is set.
* CSRF is closed by requiring the token header **and** a same-origin/absent
  `Origin` (`LOCALHOST_ORIGIN`) — a cross-site form POST can't set custom
  headers and would carry a foreign origin anyway.
* The handler calls into `resolveApproval(projectRoot, id, decision,
  resolvedBy, options)` in `src/core/tracker/approvals.js`, which locks the
  project's `.rstack/approvals.jsonl` queue for the read-modify-write, then
  (outside the lock) writes the **run-level** `approvals.json` record via
  `appendRunApproval` with `status: APPROVED` or `REJECTED`.

**Content/digest binding.** For an `APPROVED` record whose artifact is
file-backed, `resolveApproval` calls `computeApprovalArtifactDigest` (from
`src/core/harness/approval-audit.js`) to hash the artifact's current bytes
with SHA-256 and stores it as `artifact_sha256` on the run record. Virtual
approvals (guardrail overrides, stage ids) resolve to no file and carry no
digest — that's expected, not a gap. This digest is what lets the claim gate
detect a TOCTOU: if the artifact's bytes change after approval but before the
gated work is claimed, the approval no longer matches what was signed off.

The written record also carries `actor: { name, via: 'api', tokenVerified:
false, ts }` and a `source: 'business-hub'` marker, so anything reading run
history can tell a dashboard-originated approval apart from one made via
`sdlc_approve`.

Every approval record — regardless of origin — passes through
`validateApprovalRecord` / `auditRunApprovals` /
`trustedApprovedArtifacts` in `approval-audit.js` before a gate will trust
it: casing, actor shape, timestamp validity, run binding, and (if
`RSTACK_APPROVAL_SIGNING_KEY` is configured) an HMAC signature over
`artifact`, `status`, `runId`, `id`, `timestamp`, `approver`, and
`artifact_sha256` together. A record that fails this audit is dropped —
which is exactly what feeds the Audit Rejections panel above.

<Info>
  There is no email-based or link-based approval flow in this code path —
  approvals happen either via `sdlc_approve` from an agent/CLI context, or
  through this browser flow gated by `RSTACK_APPROVAL_TOKEN`. Don't assume
  any other approval channel exists without checking the source.
</Info>

## Try it

Enable dashboard approvals and approve a blocked gate from the browser:

```bash theme={null}
export RSTACK_APPROVAL_TOKEN="a-long-random-string"
npx rstack-business --port 3008 --project .
```

Open the Hub, go to **Approvals**, and click **Approve** on a pending card —
you'll be prompted once for your name and the token (both cached for the
session). Reject the same way with **Reject**. To see the aggregated queue
across approvals, decisions, and failures in one filterable list instead,
open **Action Inbox**.

<Warning>
  Without `RSTACK_APPROVAL_TOKEN` (or `_FILE`) set, `/api/approve` and
  `/api/reject` return 403 — the dashboard cannot mint manager identity from
  an unauthenticated request. Approve via `sdlc_approve` instead, or set the
  token to enable the browser path.
</Warning>

## Related

* [Business Hub overview & navigation](/business-hub/overview-and-navigation)
* [Business Hub overview](/business-hub/overview-and-navigation)
* [Approvals reference](/reference/approvals)
* [Decisions & Readiness](/reference/decision-readiness)
