27 — Auto-Approve (action='add') Bypass Hardening Risk Note (design/analysis-only, no mutation, 2026-06-01)
27 — Auto-Approve (action='add') Bypass Hardening Risk Note
Path:
knowledge/dev/reports/architecture/one-roof-governance-technical-addendum-and-implementation-index-2026-06-01/Doc: 27. Track: Branch E. Builds on doc 16 §3.2, doc 18 §4.3, GPT review (auto-approve risk flag). Status: RISK ANALYSIS + HARDENING DESIGN ONLY. NO MUTATION. No code is changed, no trigger altered, no data written, no APR created.query_pgis read-only and AST-rejects DDL/DML; all "tests" below are author-modeBEGIN..ROLLBACKrehearsal plans for an operator, never executed here. Evidence base: re-verified read-only against DBdirectus(contabopostgres) on 2026-06-01 — full function source and trigger definitions in §1.
1. Live behavior evidence (re-verified verbatim, 2026-06-01)
1.1 The auto-approve function — fn_auto_approve_add
CREATE OR REPLACE FUNCTION public.fn_auto_approve_add() RETURNS trigger AS $$
BEGIN
IF NEW.action = 'add' AND NEW.status = 'pending' THEN
NEW.status := 'approved';
NEW.reviewed_by := 'system_auto_approve';
NEW.reviewed_at := NOW();
NEW.review_note := 'Auto-approved: action=add (low risk, reversible)';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
1.2 The trigger wiring on approval_requests (6 triggers, verbatim)
| Trigger | Timing / event | When-clause | Function |
|---|---|---|---|
trg_approval_auto_code |
BEFORE INSERT | — | fn_approval_auto_code (auto-code) |
trg_apr_auto_approve |
BEFORE INSERT | — | fn_auto_approve_add (the bypass) |
trg_apr_quorum_check |
BEFORE UPDATE OF status | WHEN new.status='approved' AND old.status='pending' |
fn_apr_quorum_check |
trg_apr_block_unimplemented |
BEFORE UPDATE | WHEN new.status='applied' AND old.status<>'applied' |
fn_apr_block_unimplemented_handler |
trg_apr_lifecycle |
BEFORE UPDATE OF status | — | fn_enforce_apr_lifecycle |
trg_birth_approval_requests |
AFTER INSERT | — | fn_birth_registry_auto('code') |
1.3 The quorum function — fn_apr_quorum_check (key guard, verbatim)
-- Only fire on transition pending -> approved
IF NOT (NEW.status = 'approved' AND OLD.status = 'pending') THEN RETURN NEW; END IF;
... v_risk := risk_level FROM apr_action_types WHERE action_code = NEW.proposed_action_code;
-- any reject blocks; self-approve (proposer==approver) prohibited;
-- high => president>=1 AND ai_council>=2 ; medium => president>=1 ; low => total>=1
1.4 Column facts
approval_requests.actionCHECK∈ {add, modify, delete, review}withDEFAULT 'add'.approval_requests.statusCHECK∈ {pending, approved, applied, rejected, expired}.proposed_action_codeFK →apr_action_types(action_code).
2. Exact bypass mechanics (the trigger interaction)
The quorum gate (fn_apr_quorum_check) is wired as a BEFORE UPDATE trigger that only fires on the pending → approved status transition. The auto-approve (fn_auto_approve_add) is a BEFORE INSERT trigger that sets NEW.status := 'approved' inside the INSERT itself.
Therefore, for an APR submitted with action='add':
- The row is inserted already at
status='approved'(set by the BEFORE INSERT trigger). - There is no
pending → approvedUPDATE — the row never sat atpending. Sotrg_apr_quorum_checknever fires. - Result: the APR reaches
approvedwith zero quorum votes, noapr_approvalsrows, and no self-approve check —reviewed_by='system_auto_approve'.
The handler_ref='unimplemented' block (trg_apr_block_unimplemented) fires only on the later → applied UPDATE — so in Phase A it still prevents execution of a reserved-handler action even if the APR was auto-approved. But it does not prevent the approval-without-quorum itself, and it offers no protection once Phase B flips the handler to a real ref.
The dangerous value is the default.
actiondefaults to'add'. An APR inserted without explicitly settingactionis auto-approved. The safe path (action ∈ {review, modify, delete}) must be explicitly chosen; forgetting it lands on the bypass.
3. Risk scenarios
| ID | Scenario | Mechanism | Severity |
|---|---|---|---|
| R1 — default trap | A governance APR is inserted without explicitly setting action → defaults to 'add' → auto-approved on INSERT, no quorum. |
column DEFAULT 'add' + BEFORE INSERT auto-approve |
high (latent; the unsafe path is the default) |
| R2 — Phase-B unquorumed execution | After Phase B flips a governance action-type's handler_ref to a real handler, an action='add' governance APR auto-approves (no quorum) and can be applied → an owner-assignment / exception / delegation executes without president+2-council quorum. |
bypass + implemented handler | critical |
| R3 — approval-record pollution | Even in Phase A, action='add' governance APRs appear approved by system_auto_approve with zero apr_approvals votes — polluting the audit trail and potentially satisfying naive "is it approved?" checks downstream. |
bypass produces status='approved' |
high |
| R4 — self-approve evasion | The self-approve prohibition lives only in fn_apr_quorum_check (the UPDATE path). An action='add' APR skips it → a proposer could self-grant. |
bypass skips quorum fn | high |
These are exactly why SB-1 (doc 16) flagged the bypass and why governance APRs must not use action='add'.
4. Why governance actions must NOT use action='add'
Governance action-types (assign_governance_owner, grant_governance_exception, delegate_authority, assign_axis_owner) are all risk_level='high' — they require president + ≥2 ai_council quorum and a self-approve prohibition. That enforcement lives only on the pending → approved UPDATE path. action='add' short-circuits that path entirely (approved on INSERT). Submitting a governance change as action='add' would therefore grant the change with no quorum, no self-approve check, and (Phase B) executable — the precise opposite of the high-risk routing the design depends on. Governance APRs must traverse pending → approved so the quorum trigger fires.
5. Recommended action naming / pattern (interim convention — no code change)
- Every governance APR is submitted with
action='review'(explicitly;modify/deletealso acceptable — anything exceptadd). This is a fixed protocol constant of the Điều 32 contract (like an HTTP method), not a discovered datum — so it is not a no-hardcode violation. - The
dot_governance_gap_proposeDOT (doc 25 §6.3) setsaction='review'by contract and is paired-tested to refuseaction='add'. - Until the substrate hardening (§6) ships, the two independent Phase-A safeties are: (a) the
action≠'add'convention (forces the quorum path), and (b)handler_ref='unimplemented'(blocks execution at the→ appliedstep). Note (a) protects the approval; (b) protects only the apply — so (a) is the load-bearing one for R1/R3/R4.
6. Future hardening options (substrate; T11-gated; design only)
Ordered by recommendation. Each is proposed, not applied.
- H-OPT-1 — risk-aware auto-approve guard (primary). Modify
fn_auto_approve_addto auto-approve only low-risk, non-governance actions. Proposed predicate:
Closes R2 and most of R1/R3/R4 for governance (and-- inside fn_auto_approve_add, before flipping to 'approved':IF NEW.action = 'add' AND NEW.status = 'pending' THEN SELECT risk_level INTO v_risk FROM apr_action_types WHERE action_code = NEW.proposed_action_code; IF NEW.proposed_action_code IS NULL OR v_risk = 'low' THEN NEW.status := 'approved'; NEW.reviewed_by := 'system_auto_approve'; ... END IF; -- high/medium-risk actions are NEVER auto-approved, even with action='add'END IF;patch_ops_code/amend_law/enact_nrm, which are also high/medium). - H-OPT-2 — fail-safe column default. Change
approval_requests.actiondefault from'add'to'review'(or drop the default → force an explicit choice). Closes the default trap (R1) at the source. (A surgical-but-semantic change — requires council awareness because it alters submission ergonomics for all APR producers; not a §4G silent drift patch.) - H-OPT-3 — defensive block trigger (belt-and-suspenders). Add a BEFORE INSERT trigger that RAISES if a governance-family
proposed_action_codeis submitted withaction='add'— fail-closed at INSERT regardless of the auto-approve logic. - H-OPT-4 — data-driven auto-approvable allowlist (cleanest, no-hardcode). Add
apr_action_types.auto_approvable boolean DEFAULT false; gate auto-approve on… AND (SELECT auto_approvable FROM apr_action_types WHERE action_code = NEW.proposed_action_code). Makes "what may auto-approve" data in the same registry that drives quorum — no risk-tier literal in code, future-proof for new action-types. (Recommended pairing: H-OPT-4 as the mechanism + H-OPT-2 as the fail-safe default.) - H-OPT-5 — (assessed, not recommended) per-action-type quorum config table. See §7.
All of the above are DDL/function changes → forbidden in this macro; each requires its own council awareness, a BEGIN..ROLLBACK rehearsal, and the H-1 enact path before any commit.
7. Per-action-type quorum config — assessment (is it warranted?)
Not warranted. Quorum is already data-driven off apr_action_types.risk_level (fn_apr_quorum_check), and the design sets every governance action-type to risk_level='high' (council may pick medium for display-only axes). A separate per-action quorum table would add a parallel config surface (a mini-island) without adding expressive power the risk_level model lacks. Recommendation: keep the risk_level-driven quorum model unchanged. The real gap is not the quorum model — it is the auto-approve bypass, which H-OPT-1/H-OPT-4 close directly.
8. Tests (author-mode BEGIN..ROLLBACK rehearsal plan — NOT executed)
To be run by the operator in a transaction that is always rolled back (doc 19 style; query_pg cannot run these — they are author-mode). All assert behavior, commit nothing.
| Test | Setup | Expected (current) | Expected (after H-OPT-1/4) |
|---|---|---|---|
| T-1 | Insert governance APR action='review', proposed_action_code='assign_governance_owner' |
row stays status='pending' |
unchanged |
| T-2 | Then UPDATE … SET status='approved' with no apr_approvals votes |
fn_apr_quorum_check RAISES (high needs president+2 council) |
unchanged |
| T-3 | Insert governance APR action='add' (demonstrate bypass) |
auto-approved on INSERT, no quorum (BUG) | NOT auto-approved (risk='high') → stays pending |
| T-4 | Insert governance APR with no action (default trap) |
defaults to 'add' → auto-approved (BUG) |
with H-OPT-2: defaults to 'review' → pending; with H-OPT-1: not auto-approved |
| T-5 | Approved governance APR, UPDATE status='applied' while handler_ref='unimplemented' |
fn_apr_block_unimplemented_handler RAISES (Phase-A safety holds) |
unchanged |
| T-6 | Proposer self-approves via action='add' |
bypass evades self-approve check (BUG) | blocked (never auto-approved) |
Each rehearsal: BEGIN; … assertions …; ROLLBACK; — entry/exit row counts identical; zero net mutation.
9. No-mutation attestation & gates
- No mutation occurred. This doc is read-only analysis + design. No function/trigger/column/row was changed. The function and trigger source above were obtained via read-only
SELECT pg_get_functiondef(...)/pg_get_triggerdef(...)ascontext_pack_readonly. - Gates: any hardening (H-OPT-1..4) is a substrate change → NO-GO until a T11 build authorization + council awareness (H-OPT-2 changes submission ergonomics) +
BEGIN..ROLLBACKrehearsal + H-1 enact path. No gate may be self-approved. - Interim protection in force regardless: the
action='review'convention (§5) +handler_ref='unimplemented'(Phase A) keep governance safe until the hardening ships; the hardening is the durable fix for Phase B (R2).
10. Verdict
Auto-approve hardening risk note: COMPLETE (Branch E). Live behavior is re-verified verbatim (function source + exact trigger timing: BEFORE INSERT auto-approve vs BEFORE UPDATE quorum); the bypass mechanics, four risk scenarios (incl. the critical Phase-B execution risk R2 and the default-trap R1), the reason governance must avoid action='add', the interim action='review' convention, five hardening options (recommended: H-OPT-4 allowlist + H-OPT-2 fail-safe default, with H-OPT-1 as the direct risk-aware guard), the per-action-quorum assessment (not warranted), and an author-mode BEGIN..ROLLBACK test plan are all specified. No mutation, no code change, no APR. Next: doc 28 (bundle integration/readiness).