Documentation Index
Fetch the complete documentation index at: https://docs.kaireonai.com/llms.txt
Use this file to discover all available pages before exploring further.
What it does
/api/v1/approvals/[id] (POST approve | reject) refuses to record a
decision unless every governance invariant is satisfied. Two layers:
- Single-stage gate (W6.3) — refuses any decision where
approverId === requesterIdwhentenantSettings.aiAnalyzerSettings.governance.fourEyesEnabledis on. - Multi-stage chain (W14) — every approval is backed by an ordered
list of
ApprovalRequestStagerows. Each stage has arequiredRoleand anordinal. Decisions walk the chain in order, failing CLOSED on every guard.
Configuration
The W6.3 self-approval gate is opt-in:admin stage
per existing approval). To opt into a multi-stage chain, pass stages
on creation.
Creating a multi-stage approval
stages is optional. Omitting it produces a single default admin
stage — identical to the W6.3 single-stage path. requiredRole must be
one of viewer, editor, admin.
Per-stage decision walk
POST /api/v1/approvals/{id} finds the first pending stage,
records the decision against it, and updates the parent only when:
- The decision is
reject→ parent flips torejectedimmediately. - The decision is
approveAND the stage is the last ordinal → parent flips toapprovedand the storedpayloadis applied. - Otherwise → parent stays
pending; the next stage’s reviewer can decide.
Security defaults — fail CLOSED
Every decision-time guard inverts to “reject” on uncertainty:| Condition | Behavior |
|---|---|
tenantSettings lookup fails | Fail-CLOSED. evaluateFourEyesGate(_, { settingsUnavailable: true }) returns enforce: true, reason: "settings_unavailable". A lookup error during a self-approval attempt cannot bypass the gate. |
user.userId missing | 403 — every stage row must be attributed to a real identity. No literal “system” fallback. |
user.role not editor or admin | 403 — viewers cannot decide on stages even if requireRole is widened to allow editors through the route boundary. |
approval.requesterId missing | 403 (gate path). |
approverId === requesterId | 403 (W6.3 gate when on; W14 stage walk always). |
Same approverId decided on a prior stage | 403 — cross-stage four-eyes. One identity cannot single-handedly walk a multi-stage chain. |
| Stage rows lookup fails | 403. |
| Heal-insert (zero stage rows) fails | 403. |
Approver’s role does not match the current stage requiredRole | 403. |
A prior stage is still pending or was rejected | 409 (stage_order). |
| All stages already resolved | 409 (no_pending_stages). |
payload apply throws (Prisma error, unknown entityType, malformed JSON) | Whole transaction rolls back: the stage row’s status flip and the parent’s status flip are reverted together. Returns 409 with Failed to record decision; transaction rolled back. |
Reading an approval with its stages
GET /api/v1/approvals/{id} now returns the parent plus an
ordered stages array so the UI can render the chain progress in one
round-trip:
Audit trail
Every stage decision writes oneAuditLog row with:
action:"approve" | "reject"entityType:"approval_request"changes:{ stageOrdinal, stageName, stageStatus, parentStatus, decisionScope }decisionScope:"stage"when the parent stays pending,"request"when this decision flipped the parent to its final state.
decisionScope to filter audits when computing “how many requests
were resolved this week” — counting on action === "approve" alone now
overcounts in proportion to the stage depth.
Migration
prisma/manual-sql/09_parity_w11_to_w19.sql creates the
approval_request_stages table and backfills exactly one default
stage row per existing pending approval (ordinal=0,
requiredRole='admin', status mirrored from the parent). Existing
admin-only flows keep their behavior; nothing migrates “into”
multi-stage by default.
Known limit (V1)
requiredRole is a single value per stage — exact role match is
enforced. There is no role hierarchy (“admin satisfies editor-stage”).
A multi-role-per-stage policy is tracked separately as a future
extension of the in-memory recordDecision policy in
lib/governance/approval-workflow.ts.
See also: Compliance · Cron jobs · Approvals API