03 — Authorization Build-Ready Substrate Design (SB-0 / L3)
03 — Authorization Build-Ready Substrate Design (SB-0 / L3)
Package:
one-roof-axis-auth-proposal-operational-hardening-build-ready-design-2026-06-02Mode: DESIGN ONLY · READ-ONLY · NO COMMIT · NO MUTATION · paper DDL only — nothing created Hardens GPT review gap #1 to CREATE-TABLE level. Behaviour is in doc 02. Naming: this substrate is SB-0 (the new first build step; see doc 11/12). It is additive: no existing table is ALTERed, no existing CHECK is widened.
3.0 Scope & invariants
SB-0 = the smallest substrate that makes L3 technical build-authorization real and non-forgeable:
- one table
governance_build_authorization(the grant); - one view
v_build_auth_valid(the recomputed validity, the anti-forgery keystone); - a small set of
apr_action_typesrows (the vocabulary, data not schema); - one design-only verifier function
fn_build_commit_allowed(the gate).
Reuses for the decision layer (L0–L2): approval_requests, apr_approvals, apr_action_types, fn_apr_quorum_check (all live, doc 01 §1.3). Reserves os_proposal_approvals for L4 only.
Hard constraints (from live audit + Điều 37): additive-only; no ALTER of governance_relations/approval_requests/apr_action_types schema; no-hardcode (risk/level/allowlist are rows); no local island (writes route the audit/event spine).
3.1 governance_build_authorization — column-level design (paper DDL)
Additive new table. Types chosen to match live conventions (text codes like approval_requests.code; jsonb payloads; timestamptz).
| Column | Type | Null | Default | Constraint / notes |
|---|---|---|---|---|
auth_code |
text | NOT NULL | — | PK. Human-readable, e.g. BUILD-AUTH-NNN (assign via existing code-assignment convention; see F-83-1 note §3.6). |
request_ref |
text | NOT NULL | — | FK → approval_requests.code. The backing L2 request. |
approval_ref |
jsonb | NOT NULL | — | Immutable snapshot of the quorum at grant time (approver ids/types/decisions/timestamps). Evidence, not the source of validity (validity is recomputed — INV-6). |
step_name |
text | NOT NULL | — | Exactly ONE build step (e.g. SB-12, SB-2, axis_registry). No multi-step/blanket grants. |
scope |
jsonb | NOT NULL | — | {objects_allowed:[], objects_forbidden:[], channel, txn_strategy, max_objects}. The blast-radius contract the verifier/operator honors. |
risk_level |
text | NOT NULL | — | CHECK ∈ (low,medium,high,sovereign). Mirrors apr_action_types.risk_level of the request's action. |
commit_allowed |
boolean | NOT NULL | false |
Granter intent flag. Necessary but not sufficient — the view still recomputes quorum/TTL/etc. |
requires_sovereign_esign |
boolean | NOT NULL | false |
Set true by escalation triggers (doc 02 §2.5). |
sovereign_esign_ref |
text | NULL | — | Ref to the L4 e-sign row (interim os_proposal_approvals.id; target gov sovereign collection). CHECK: required iff requires_sovereign_esign ((requires_sovereign_esign = false) OR (sovereign_esign_ref IS NOT NULL)). |
rollback_plan_ref |
text | NOT NULL | — | Ref to the rollback runbook/change-set for this step (doc 16). No grant without a rollback plan. |
granted_by |
text | NOT NULL | — | Identity of the build owner who granted. Must differ from the consuming agent (INV-5; enforced in verifier + view, not a table CHECK since the consumer is unknown at grant time). |
granted_at |
timestamptz | NOT NULL | now() |
— |
expires_at |
timestamptz | NOT NULL | — | TTL window end (default granted_at + interval '48 hours', set per risk). CHECK expires_at > granted_at. |
consumed_at |
timestamptz | NULL | — | Set in the COMMIT txn. Single-use (INV-9). |
consumed_by |
text | NULL | — | Identity that consumed. CHECK: set iff consumed_at set. |
revoked_at |
timestamptz | NULL | — | Revocation time. |
revoked_by |
text | NULL | — | — |
revoked_reason |
text | NULL | — | CHECK: revoked_by/revoked_reason set iff revoked_at set. |
status |
text | NOT NULL | 'draft' |
CHECK ∈ (draft,active,consumed,expired,revoked). Display/lifecycle only — never the COMMIT authority (INV-10). |
evidence |
jsonb | NULL | — | Rehearsal/preflight evidence, links to docs. |
created_by |
text | NOT NULL | — | Authoring identity (agent may be created_by for a draft; cannot be granted_by). |
created_at |
timestamptz | NOT NULL | now() |
— |
Constraints summary:
- PK(
auth_code); FK(request_ref)→approval_requests(code). - CHECK
risk_level/statusenums. - CHECK
expires_at > granted_at. - CHECK sovereign-esign presence; CHECK consumed/revoked field-pairing.
- Partial UNIQUE index
uq_one_active_grant_per_step:UNIQUE (step_name) WHERE status='active' AND consumed_at IS NULL AND revoked_at IS NULL— at most one live grant per step at a time (prevents grant-stacking). - Index on (
request_ref), (status), (expires_at) for the validity view and the expiry sweep. - Trigger-less by default. (If a birth/audit trigger is later attached, it must pass the code column — see F-83-1, §3.6.)
3.2 v_build_auth_valid — the anti-forgery keystone (paper SQL)
The view recomputes validity from the backing quorum; it never trusts governance_build_authorization.status. This is the single mechanism that makes a forged row inert (INV-6).
CREATE VIEW v_build_auth_valid AS
SELECT g.auth_code, g.step_name, g.scope, g.risk_level,
g.commit_allowed, g.granted_by, g.requires_sovereign_esign,
g.sovereign_esign_ref, g.expires_at
FROM governance_build_authorization g
JOIN approval_requests ar ON ar.code = g.request_ref
WHERE g.commit_allowed = true
AND g.consumed_at IS NULL
AND g.revoked_at IS NULL
AND now() < g.expires_at -- INV-11 TTL
AND ar.status = 'approved' -- backing decision exists
AND quorum_passed(g.request_ref) -- INV-6 recomputed quorum
AND ( g.requires_sovereign_esign = false
OR valid_sovereign_esign(g.sovereign_esign_ref) ); -- INV-7 L4 branch
Helper predicates (design-only; specify exactly so they are buildable — GPT "key claim to verify"):
-- quorum_passed(request_code): re-derives the SAME rule fn_apr_quorum_check enforces,
-- as a read-only boolean, so the view and the trigger can never disagree.
quorum_passed(code) :=
WITH r AS (SELECT proposed_action_code,
(source_context->>'proposer') AS proposer
FROM approval_requests WHERE code = $code),
lvl AS (SELECT risk_level FROM apr_action_types
WHERE action_code = (SELECT proposed_action_code FROM r)),
votes AS (SELECT approver, approver_type, decision
FROM apr_approvals a JOIN approval_requests ar ON ar.id=a.apr_id
WHERE ar.code = $code
AND a.approver <> (SELECT proposer FROM r)) -- INV-1 self-exclusion
SELECT CASE (SELECT risk_level FROM lvl)
WHEN 'high' THEN (count(*) FILTER (WHERE decision='approve' AND approver_type='president') >= 1)
AND (count(*) FILTER (WHERE decision='approve' AND approver_type='ai_council') >= 2)
WHEN 'medium' THEN (count(*) FILTER (WHERE decision='approve' AND approver_type='president') >= 1)
WHEN 'low' THEN (count(*) FILTER (WHERE decision='approve') >= 1)
END
AND count(*) FILTER (WHERE decision='reject') = 0 -- INV-3
FROM votes;
-- valid_sovereign_esign(ref): READ-ONLY check of a sovereign e-sign row.
-- Interim target = os_proposal_approvals; verifies signature fields populated,
-- esignature_agreement=true, signer = registered sovereign identity, scope matches the act.
-- An agent may only SELECT this; never INSERT/UPDATE (INV-8).
The hardening rule (tested invariant, doc 02 INV-10): no application/agent path may SELECT … FROM governance_build_authorization WHERE status='active' to authorize a COMMIT. All COMMIT decisions go through v_build_auth_valid / fn_build_commit_allowed. A coverage detector (doc 09) flags any code/DOT that reads the raw status for an authorization decision.
3.3 New apr_action_types rows (DATA — Phase-A fail-closed)
Authorization needs vocabulary. These are rows, not enums/schema (no-hardcode). All land Phase-A: handler_ref='unimplemented', status='active', so the vocabulary exists and quorum can pass, but apply is blocked by fn_apr_block_unimplemented_handler until a Phase-B handler is authored under its own authorization. This copies the live precedent of amend_law/enact_nrm (doc 01 §1.3).
| action_code | risk_level | handler_ref | Purpose | Level |
|---|---|---|---|---|
authorize_build_step |
high | unimplemented | Issue an L3 governance_build_authorization for one step |
L2→L3 |
activate_event_type |
high | unimplemented | Flip a registered event type active=false→true (SB-11) |
L3 |
register_axis |
high | unimplemented | Add/activate an axis_registry row (doc 04/05) |
L2→L3 |
register_topic_node |
high | unimplemented | Promote a taxonomy FAC-08 candidate to active/born (doc 07) |
L2→L3 |
assign_governance_owner |
high | unimplemented | SB-2 ownership write | L2 |
grant_governance_exception |
high | unimplemented | SB-2 governed exception | L2 |
delegate_authority |
high | unimplemented | SB-2 delegated owner (TTL'd) | L2 |
assign_axis_owner |
high | unimplemented | Axis owner-per-scope | L2 |
The first four are SB-0/axis vocabulary (new in this package); the last four are the SB-1 governance action types that prior memory described — confirmed unbuilt by the live audit (doc 01 §1.3). All eight should be authored together as the governance action vocabulary, under one L2 approval, in the SB-0/SB-1 window.
Convention (INV-2): every APR using these actions sets action='review' (never 'add').
3.4 fn_build_commit_allowed — the verifier function (design-only)
-- DESIGN ONLY. Read-only. Returns ALLOW/DENY + reason. Writes nothing.
fn_build_commit_allowed(p_step text, p_action_code text, p_agent text)
RETURNS TABLE(decision text, reason text) AS $$
-- 1. classify level from apr_action_types.risk_level(p_action_code) + reversibility(proposed_action)
-- 2. L0: ALLOW iff p_action_code in governed allowlist AND risk='low' AND handler implemented
-- 3. L1: ALLOW iff request approved AND apr_approvals approve with approver<>proposer
-- 4. L2: ALLOW iff quorum_passed(request)
-- 5. L3: ALLOW iff EXISTS(SELECT 1 FROM v_build_auth_valid
-- WHERE step_name=p_step AND granted_by<>p_agent) -- INV-5
-- 6. L4: ALLOW iff (L3) AND valid_sovereign_esign(grant.sovereign_esign_ref)
-- default DENY (fail-closed)
$$ LANGUAGE sql STABLE; -- STABLE/read-only: the function never mutates
consumed_at is not written by this function; it is written by the COMMIT transaction that performs the step (so the check stays read-only and idempotent).
3.5 Links, FKs, and the os_proposal_approvals resolution
request_ref→approval_requests.code(live).approval_ref(jsonb) snapshotsapr_approvalsrows at grant time (evidence).sovereign_esign_ref:- Interim: points at
os_proposal_approvals.id(char).valid_sovereign_esignchecks the signature fields +esignature_agreement=true+ signer = registered sovereign identity + scope match. Agent: SELECT only. - Target (doc 12 patch P3, a council L2/L4 decision): a purpose-built
governance_sovereign_esigncollection so the salesos_proposal_*module returns to sales.sovereign_esign_refthen points there. The FK target is deliberately left as a soft text ref (not a hard FK) so the interim→target migration does not require an ALTER.
- Interim: points at
rollback_plan_ref→ the rollback runbook/change-set id (doc 16 mechanism).
3.6 F-83-1 build-time prerequisite (carried, now applies to SB-0 too)
fn_birth_registry_auto is argless and fires AFTER INSERT on apr_action_types via trg_birth_apr_action_types (doc 01 §1.5). Inserting the §3.3 action-type rows will hit the F-83-1 hazard (NULL entity_code → birth_registry NOT-NULL violation). Therefore the SB-0/SB-1 build txn must, before any apr_action_types INSERT, re-wire the trigger:
DROP TRIGGER trg_birth_apr_action_types ON apr_action_types;
CREATE TRIGGER trg_birth_apr_action_types AFTER INSERT ON apr_action_types
FOR EACH ROW EXECUTE FUNCTION fn_birth_registry_auto('action_code'); -- mirrors approval_requests('code')
governance_build_authorization itself is trigger-less at birth, so creating the table does not hit F-83-1; only inserting the action-type rows does. auth_code assignment uses the existing code-assignment convention (no argless-birth dependency).
3.7 Build classification & order placement
- SB-0 is the new FIRST build step, but it is gated on the one-time L2 council + L4 sovereign ratification of the authorization model (the bootstrap, doc 02 §2.7). It cannot self-authorize.
- Once SB-0 exists, the order becomes: ratify-auth-model (L2+L4) → build SB-0 → SB-12 → SB-13 → SB-10 → SB-11(inactive) → SB-2 → SB-1, each subsequent step L3-authorized through SB-0 (a
governance_build_authorizationper step). - Transaction strategy: SB-0 itself = one small txn (table + view + indexes), then the action-type rows in the SB-1 txn (so the F-83-1 re-wire and the row INSERTs are atomic). Per-step small txns thereafter (doc 11).
3.8 Rehearsal plan (design-only; BEGIN..ROLLBACK; 5 negative tests)
To be run later in author-mode (psql -U workflow_admin, BEGIN..ROLLBACK, entry==exit, zero COMMIT). The verifier must DENY all five forgeries:
- Forged grant, no backing quorum: INSERT a
governance_build_authorizationrowstatus='active', commit_allowed=truewith arequest_refthat has no passing quorum →v_build_auth_validreturns 0 rows ⇒ DENY (INV-6). ✔ proves the keystone. - Expired:
expires_at < now()⇒ DENY (INV-11). - Consumed:
consumed_atset ⇒ DENY (INV-9). - Revoked:
revoked_atset ⇒ DENY (INV-12). - Self-grant:
granted_by = consuming agent⇒ verifier DENY (INV-5). Plus: raw-status bypass — confirm astatus='active'row with a failing quorum is NOT returned by the view (anti-INV-10 regression).
3.9 Open decisions & forbidden-compliance
Open (explicit, none hidden):
- O-AUTH-1: interim
os_proposal_approvalsvs purpose-builtgovernance_sovereign_esign(council L2/L4 — doc 12 P3). - O-AUTH-2: exact TTL per risk tier (default 48h; council may set per-tier).
- O-AUTH-3: the precise sovereignty-threshold ceiling (
Nproduction objects; whether anybirth_registrywrite always escalates). Recommend: any write tobirth_registrytruth or lawstatus⇒ L4. - O-AUTH-4: whether
granted_by≠agentshould also be a deferred table CHECK at consume-time (currently verifier-enforced; recommend keep in verifier + add a consume-time trigger when SB-0 Phase-B handler is built).
Forbidden-compliance: paper DDL only; nothing created/altered; no row inserted into any table; no quorum, grant, e-sign, event, DOT, or law mutation; os_proposal_approvals not written; read-only audit only.