02 — Approval / Authorization State Machine (hardened, build-ready)
02 — Approval / Authorization State Machine (hardened, build-ready)
Package:
one-roof-axis-auth-proposal-operational-hardening-build-ready-design-2026-06-02Mode: DESIGN ONLY · READ-ONLY · NO COMMIT · NO MUTATION Hardens: prior…-2026-06-01/02-authorization-model-technical-vs-sovereign.md(the L0–L4 ladder sketch). Closes GPT review gap #1: "Authorization L0–L4 needs hardening into exact state machine, required records, anti-forgery proof, TTL/revocation, and verifier algorithm." Companion: doc 03 (the CREATE-TABLE-level substrate). This doc is the behaviour; doc 03 is the storage.
2.0 The problem this state machine solves ("one gate for two jobs")
The legacy master gate M-1 = "≥1 row in os_proposal_approvals scoping this step". Live evidence (doc 01 §1.4; Phase-1 index doc 114 §114.3): os_proposal_approvals is a Directus human e-signature collection in a sales module — 0 rows, columns signature_text/signature_image/first_name/last_name/email/ip_address/esignature_agreement/proposal. It captures a real person physically e-signing. An agent writing it = forging the sovereign's signature.
Using it as the generic COMMIT key conflates two different jobs:
- Technical build authorization — "may this reversible engineering step be committed?" (risk × reversibility; agent-or-human; per-step; revocable).
- Sovereign approval — "does the President personally assent to this irreversible/constitutional act?" (one human, legally binding, durable).
The hardened model separates them across five levels L0–L4 and gives each a distinct, verifiable, non-forgeable truth record. The state machine below governs the lifecycle of any authorization request through those levels.
2.1 The L0–L4 ladder (hardened)
Levels are assigned by risk × reversibility, not by who runs the step. (An agent and a human running the same low-risk reversible step are both L0.)
| Level | Trigger (risk × reversibility) | Who may REQUEST | APPROVER of record | Agent may approve? | Human approval mandatory? | Sovereign e-sign mandatory? | Truth store (live mechanism) |
|---|---|---|---|---|---|---|---|
| L0 Machine low-risk | low risk and fully reversible and action in governed allowlist | agent / human / DOT | automatic (governed allowlist) | Yes (allowlist lane only) | No | No | approval_requests(auto) + apr_approvals(approver_type='system'). Live: fn_auto_approve_add lane — after the P5 hardening (allowlist + fail-safe deny), not the current action='add' trap |
| L1 Owner-approved technical | medium risk, reversible (e.g. add_field) |
agent / human | exactly one named human technical owner, approver ≠ proposer |
No | Yes (1 human) | No | approval_requests + apr_approvals (1 human approve), quorum medium |
| L2 Council-approved governance | high risk, governed decision (ownership, policy, vocabulary, law-adjacent) | agent / human | council quorum per Điều 32 (high ⇒ ≥1 president + ≥2 ai_council) |
No (vote only, never self-decide) | Yes (quorum) | No (unless escalated) | approval_requests + apr_approvals quorum (fn_apr_quorum_check PASS) + governance_audit_log |
| L3 High-risk build / deploy (COMMITs substrate) | high risk, commits/alters substrate (DDL, governed rows, deploy) | agent / human (agent may prepare draft) |
L2 council quorum + named build owner, recorded in a build-auth row (granted_by ≠ build agent) |
No (may prepare draft; cannot set active) |
Yes (via the L2 quorum behind it) | No — unless a sovereignty trigger flips requires_sovereign_esign=true |
governance_build_authorization (NEW, doc 03) backed by L2 |
| L4 Sovereign e-sign | irreversible / constitutional / authority-model change / enact_nrm/amend_law / blast-radius over governed ceiling |
human (President) | the President, personally | Never | n/a | Yes | human e-sign collection — interim os_proposal_approvals (sovereign-only, agent-forbidden); target a purpose-built governance sovereign e-sign collection (doc 12 patch) |
Level-assignment is computed, not declared. approval_requests carries a computed sovereignty_required (see §2.5 escalation triggers); the level is a function of apr_action_types.risk_level for the row's proposed_action_code plus the reversibility/blast-radius of proposed_action. No hardcoded per-object level lists (Điều 37 no-hardcode).
2.2 The unified authorization lifecycle state machine
One state machine covers all levels; higher levels add states (L0/L1/L2 stop at APPROVED/APPLIED; L3 adds the grant → consume segment; L4 adds the sovereign e-sign dependency).
┌─────────────┐
│ DRAFTED │ (agent/human authors approval_requests row;
└──────┬──────┘ action='review' — NEVER 'add'; status='pending')
│ submit
▼
┌─────────────┐ reject (any reviewer) / quorum-fail
│ PENDING │───────────────────────────────┐
│ (under │ │
│ review) │◄──── vote (apr_approvals) │
└──────┬──────┘ ▼
quorum/owner PASS │ ┌────────────────┐
(fn_apr_quorum_ │ │ REJECTED │ (terminal)
check on ▼ └────────────────┘
pending→approved) ┌─────────────┐
│ APPROVED │ L0/L1/L2 decisions END here (the *decision*)
└──────┬──────┘
│ L3 only: build owner grants a scoped authorization
│ (granted_by ≠ build agent) → governance_build_authorization
▼
sovereignty trigger? ┌─────────────┐ no e-sign within TTL / revoked
┌───────yes──────────────│ GRANTED │──────────────────────────┐
▼ │ (status= │ │
┌──────────────┐ e-sign │ active, │◄── revoke (granted_by/ │
│ AWAITING_ │ valid │ commit_ │ council, anytime) │
│ SOVEREIGN │───────────────►│ allowed=t) │ │
│ (L4 e-sign │ └──────┬──────┘ │
│ referenced) │ │ COMMIT (verifier PASS; │
└──────────────┘ │ single-use; consumed_at set) ▼
▼ ┌────────────┐
┌─────────────┐ │ EXPIRED │ (terminal)
│ CONSUMED │ (terminal, │ REVOKED │ (terminal)
│ one COMMIT │ audit-complete) └────────────┘
└─────────────┘
Transition table (guards · actor · records written · anti-forgery check)
| # | From → To | Guard | Actor | Records written | Anti-forgery invariant enforced |
|---|---|---|---|---|---|
| T1 | ∅ → DRAFTED | proposed_action_code exists in apr_action_types; action='review' (≠'add') |
requester (agent ok) | approval_requests(status='pending') |
INV-2 (no auto-approve bypass) |
| T2 | DRAFTED → PENDING | implicit (row is pending) |
system | — | — |
| T3 | PENDING → REJECTED | any reviewer casts reject |
reviewer | apr_approvals(decision='reject') |
INV-3 (reject is sticky) |
| T4 | PENDING → APPROVED | quorum for risk_level met; proposer ∉ approvers |
owner/council | apr_approvals(approve…); approval_requests.status='approved' |
INV-1 (no self-approval), INV-4 (quorum recomputed by trigger) |
| T5 | APPROVED → GRANTED (L3) | build owner issues build-auth; granted_by ≠ build agent; backing quorum PASS; rollback plan present |
build owner (not the build agent) | governance_build_authorization(status='active', commit_allowed=true) |
INV-5 (granter ≠ executor), INV-6 (grant inert without backing quorum) |
| T5a | GRANTED → AWAITING_SOVEREIGN (L4 needed) | sovereignty_required=true |
system | governance_build_authorization.requires_sovereign_esign=true |
INV-7 (sovereign-only e-sign) |
| T5b | AWAITING_SOVEREIGN → GRANTED | valid L4 e-sign row referenced | President (out-of-band) | …sovereign_esign_ref set; valid_sovereign_esign() true |
INV-7, INV-8 (agent may only READ e-sign) |
| T6 | GRANTED → CONSUMED | verifier PASS at COMMIT; single-use | build agent (under grant) | governance_build_authorization.consumed_at/by; registry_changelog; governance_audit_log |
INV-9 (single-use), INV-10 (verifier reads v_build_auth_valid, not raw status) |
| T7 | GRANTED → EXPIRED | now() > expires_at and not consumed |
system | status='expired' (lazy/by sweep) |
INV-11 (TTL) |
| T8 | GRANTED/AWAITING → REVOKED | revoke issued | granter / council | revoked_at/by/reason; status='revoked' |
INV-12 (revocable anytime before consume) |
L0 fast-path: for an allowlisted low-risk reversible action, T1→T4 collapses into a single INSERT that lands status='approved' with apr_approvals(approver_type='system') — only if the action is in the governed allowlist and risk_level='low' (the P5-hardened fn_auto_approve_add; see INV-2). There is no L0 grant/consume segment because L0 does not COMMIT substrate.
2.3 Required records per level (the "what must exist" contract)
A verifier (human or agent) can confirm authorization only by the presence of these exact records. Absence = not authorized = COMMIT_FORBIDDEN.
| Level | approval_requests |
apr_approvals |
governance_build_authorization |
e-sign collection | Verifier reads |
|---|---|---|---|---|---|
| L0 | 1 row, status='approved', action in allowlist, risk_level='low' |
1 row approver_type='system' |
— | — | allowlist membership + the auto row |
| L1 | 1 row, status='approved', proposed_action_code risk='medium' |
≥1 approve, approver ≠ proposer |
— | — | status='approved' + approver≠proposer |
| L2 | 1 row, status='approved', risk='high' |
quorum: ≥1 president + ≥2 ai_council approve, 0 reject, proposer ∉ approvers |
— | — | fn_apr_quorum_check PASS |
| L3 | 1 row (the L2 request) | the L2 quorum | 1 row status='active', commit_allowed=true, in TTL, consumed_at IS NULL, granted_by≠agent |
— | v_build_auth_valid (doc 03) |
| L4 | 1 row | the L2 quorum | 1 row, requires_sovereign_esign=true, sovereign_esign_ref set |
1 sovereign-signed row scoping this act (signature fields populated, registered sovereign identity) | valid_sovereign_esign(sovereign_esign_ref) (READ only) |
2.4 Anti-forgery proof (invariants and why each forgery attempt fails)
The model is "non-forgeable" iff every path to a COMMIT requires a record that a single agent cannot fabricate. Below, each invariant pairs the mechanism with the attack it defeats.
- INV-1 No self-approval.
fn_apr_quorum_checkderives the proposer fromsource_contextand excludes them from the approver set. Attack: agent proposes and approves its own L2 request → fails: its own vote is not counted toward quorum; quorum unreachable alone. - INV-2 No auto-approve bypass. Every governance/build APR uses
action='review'. Attack: setaction='add'sofn_auto_approve_add(BEFORE INSERT) flips it toapproved, skipping quorum (the live "dangerous default" trap, doc 01 §1.3). Defeated by: mandatoryaction≠'add'convention plus the P5 hardening that restricts auto-approve to(allowlisted AND risk_level='low')with fail-safe deny. A high-risk action can never enter the auto lane. - INV-3 Reject is sticky. Any
rejectblocks thepending→approvedtransition for that request. - INV-4 Quorum recomputed at the gate. Quorum is enforced by a
BEFORE UPDATEtrigger reading liveapr_approvals+apr_action_types.risk_level, not by a writable status column. Attack: directlyUPDATE approval_requests SET status='approved'→ fails: the trigger recomputes and RAISES if quorum absent. - INV-5 Granter ≠ executor.
governance_build_authorization.granted_bymust differ from the identity that consumes the grant. Attack: build agent grants itself L3 → fails: verifier rejectsgranted_by = consuming agent. - INV-6 Grant inert without backing quorum. Validity is recomputed from the backing L2 quorum via
v_build_auth_valid(doc 03 §3.2), never asserted by the row's ownstatus. Attack: INSERT agovernance_build_authorizationrow withstatus='active', commit_allowed=truebut no real approved request behind it → fails:v_build_auth_validjoins toapproval_requests/apr_approvals, finds no passing quorum, returns the row as invalid; the verifier consults the view, never the raw status. (Hardening rule: NO consumer path may readgovernance_build_authorization.statusdirectly to decide a COMMIT — all consumers MUST usev_build_auth_valid/fn_build_commit_allowed. doc 03 §3.4 makes this a tested invariant.) - INV-7 Sovereign e-sign is human-only. L4 rows are written only by the President through the Directus e-sign flow, out-of-band. The constraint is governance, not transport: even though no DB constraint can tell a forged signature from a real one, the governance rule forbids any agent from writing the collection, and the read channel an agent has (
query_pg) is read-only. - INV-8 Agent reads, never writes, e-sign. Build/runtime agents may only
valid_sovereign_esign()-check (SELECT) an e-sign row; writing one is forbidden and is the single act the model never delegates (see bootstrap paradox §2.7). - INV-9 Single-use.
consumed_atis set in the same COMMIT txn that performs the step; a consumed grant fails the verifier thereafter. Attack: replay one grant across many COMMITs → fails:consumed_at IS NOT NULL⇒ invalid. - INV-10 Verifier uses the view. The COMMIT-time check calls
fn_build_commit_allowed/v_build_auth_valid, which recompute everything; the rawstatusflag is advisory display only. - INV-11 TTL. Grants carry
expires_at(default 24–72h); past it the grant is invalid even ifactive. - INV-12 Revocable. A granter or the council may set
revoked_aton any un-consumed grant; revoked ⇒ invalid immediately.
Forgery-resistance summary: to force a COMMIT an actor needs either (a) a real passing L2 quorum it did not solely cast (INV-1/4/6), or (b) for sovereign acts, a real human signature it physically cannot produce (INV-7/8). No single agent can manufacture either. The model is non-forgeable under the stated governance + read-only-transport assumptions.
2.5 TTL / expiry / revocation / escalation semantics
- TTL (L3):
granted_at+ window (expires_at, default 24–72h, set perrisk_level). After expiry →EXPIRED(terminal); a new step needs a new request+grant. L1 carries a configured apply window (apply within N days ofreviewed_at). L2 decisions don't expire (the decision is durable) but their L3 grant does. L4 e-sign is a durable legal record but its authority is scoped to one named act; reuse for another act is invalid. - Single-use:
consumed_at/consumed_byset at COMMIT; one grant ⇒ one COMMIT. - Revocation:
revoked_at/by/reasonmay be set by the granter or council on any active/awaiting grant before consume. Revocation is immediate (verifier rejects). No cascade is needed because a grant maps to exactly one un-started step; if a multi-step plan must be cancelled, each step's grant is revoked individually (per-step authorization, no blanket grant). - Escalation L3→L4 (sovereignty triggers, governed rows not literals):
enact_nrm/amend_law/any lawstatuschange; irreversible/destructive op; change to the authority model itself (editing the L0 allowlist top tier or quorum rules); blast radius beyond the governed ceiling (touchesbirth_registrytruth, or > N production objects). When any trigger is true,approval_requests.sovereignty_required=true⇒requires_sovereign_esign=true;commit_allowedis not honored untilsovereign_esign_refresolves valid.
2.6 The COMMIT-time verifier algorithm
Every build/runtime agent runs this before any COMMIT (design-only; the PG function is specified in doc 03 §3.4):
function authorize_commit(step_name, action_code, agent_identity) -> {ALLOW | DENY, reason}:
level := classify_level(action_code, proposed_action) # risk × reversibility
switch level:
L0: return ALLOW iff action_code ∈ governed_allowlist
AND risk_level(action_code) = 'low'
AND handler_implemented(action_code) # else DENY (fail-safe)
L1: r := latest approval_requests for step
return ALLOW iff r.status='approved'
AND exists apr_approvals(r) decision='approve' AND approver≠proposer
L2: return ALLOW iff fn_apr_quorum_check_passes(r) # recomputed, not a flag
L3: v := SELECT 1 FROM v_build_auth_valid # the ONLY source of truth
WHERE step_name=:step_name
AND commit_allowed = true
AND granted_by <> :agent_identity # INV-5
return ALLOW iff v exists # view already enforces
# quorum/TTL/consumed/revoked/esign
L4: return ALLOW iff (L3 ALLOW) AND valid_sovereign_esign(grant.sovereign_esign_ref)
default: return DENY # fail-closed
Properties: fail-closed (unknown ⇒ DENY); idempotent/read-only (the verifier itself writes nothing — consumed_at is written by the COMMIT txn, not the check); single source for L3 (v_build_auth_valid, never raw status).
2.7 Redefined M-1, os_proposal_approvals, and the bootstrap paradox
Redefined M-1 (per-step): M-1 is satisfied for a step iff fn_build_commit_allowed(step_name, agent) = ALLOW, i.e. a valid governance_build_authorization exists for this step (via v_build_auth_valid) and, if requires_sovereign_esign, a valid L4 e-sign is referenced. M-1 is no longer "≥1 row in os_proposal_approvals".
os_proposal_approvals = L4 only. It remains the sovereign e-sign surface, agent-forbidden, sovereign-only. After M-1 redefinition, no L3 step depends on it. (Target: stand up a purpose-built governance sovereign e-sign collection and return the sales module to sales — doc 12 patch P3; a council L2/L4 decision.)
Bootstrap paradox (stated honestly, not hidden — GPT acceptance criterion #14). The authorization model itself is a change to the authority model, which is an L4 sovereignty trigger. Therefore standing up SB-0 (governance_build_authorization + view + action-type rows) is itself a one-time L2 council + L4 sovereign-ratified constitutional act. The new model cannot remove from the President the single act of e-signing its own adoption. This is a decision dependency, not an engineering gap: once ratified and built, every subsequent reversible build step (SB-12…SB-1, axis substrate) is L3-authorized via SB-0 without further sovereign e-sign (unless individually escalated).
2.8 Worked traces
- L0 —
create_item(low, reversible, allowlisted): T1 INSERT landsapproved+apr_approvals(system). Verifier: allowlist∧low∧handler ⇒ ALLOW. No grant/consume. No human, no sovereign. - L1 —
add_field(medium): T1 draft (action='review') → T2 pending → human technical owner approves (approver≠proposer) → T4 APPROVED. Verifier L1 ⇒ ALLOW. No sovereign. - L3 — authorize building SB-12
governance_ruleset: T1 draftproposed_action_code='authorize_build_step'(risk=high,action='review') → council quorum (≥1 president + ≥2 ai_council) → T4 APPROVED → build owner issuesgovernance_build_authorizationforstep_name='SB-12',scope={objects_allowed:[governance_ruleset], txn_strategy:'single small txn', channel:'psql workflow_admin'},commit_allowed=true,expires_at=now+48h,granted_by=<owner>≠build agent→ GRANTED. Build agent runs verifier (v_build_auth_valid) ⇒ ALLOW → COMMIT setsconsumed_at→ CONSUMED. No sovereign (SB-12 is reversible, below ceiling). - L4 —
enact_nrm(law enactment): L2 quorum approves → grant created withrequires_sovereign_esign=true→ AWAITING_SOVEREIGN → President e-signs out-of-band →valid_sovereign_esigntrue → GRANTED → COMMIT → CONSUMED. Agent only ever READ the e-sign.
2.9 Audit / provenance / forbidden-compliance
- Provenance: every transition writes to
apr_approvals(votes),governance_build_authorization(grant/consume/revoke),registry_changelog+governance_audit_log(the COMMIT), and emits agovernance-domain event (after SB-11; register-before-emit, Điều 45). - Rollback: each grant carries
rollback_plan_ref; post-consume rollback uses the change-set reverse + grant revoke + runbook (doc 16). - Forbidden-compliance (this doc): design-only; no row created in any approval/e-sign table; no quorum cast; no grant issued; no law/event/DOT mutation;
os_proposal_approvalsneither read-for-write nor written.