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
artifactstarts withguardrail-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 byannotateApprovalLifecycleinstate/approvals.js, cross-referencing the run-level approval history) flips the card’s status toconsumedso 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 —
approvalAuthErrorreturns 403 unlessRSTACK_APPROVAL_TOKEN(orRSTACK_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)insrc/core/tracker/approvals.js, which locks the project’s.rstack/approvals.jsonlqueue for the read-modify-write, then (outside the lock) writes the run-levelapprovals.jsonrecord viaappendRunApprovalwithstatus: APPROVEDorREJECTED.
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.
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.