-- Guarded KB review/apply prerequisites. Run as postgres at deploy time. -- -- Authority is deliberately split: -- * kb_review can read proposals and execute one constrained approval function; -- * kb_apply can read approved proposals, perform narrow canonical writes, and -- execute payload-bound assert/finish functions; -- * kb_gate_owner is a NOLOGIN owner for gate tables/functions; -- * neither login role can directly UPDATE kb_stage.kb_proposals or mutate the -- immutable approval snapshot. -- -- The deployer provisions passwords separately in kb-review.env/kb-apply.env. -- This file contains no credentials and is safe to reuse in disposable clones. begin; do $deployer$ begin if not exists ( select 1 from pg_catalog.pg_roles where rolname = current_user and rolsuper ) then raise exception 'kb_apply_prereqs: current role % must be a superuser', current_user; end if; if not exists (select 1 from pg_catalog.pg_roles where rolname = 'kb_apply') then raise exception 'kb_apply_prereqs: required runtime role kb_apply does not exist'; end if; end $deployer$; do $roles$ begin if not exists (select 1 from pg_catalog.pg_roles where rolname = 'kb_gate_owner') then create role kb_gate_owner nologin; end if; if not exists (select 1 from pg_catalog.pg_roles where rolname = 'kb_review') then create role kb_review login; end if; end $roles$; alter role kb_gate_owner nologin noinherit nosuperuser nocreatedb nocreaterole noreplication nobypassrls; alter role kb_review login noinherit nosuperuser nocreatedb nocreaterole noreplication nobypassrls; alter role kb_apply login noinherit nosuperuser nocreatedb nocreaterole noreplication nobypassrls; -- Protected roles are standalone principals. Any membership involving one of -- them creates an inherited or SET ROLE path that this migration cannot safely -- classify, so fail instead of silently revoking an operator-managed grant. do $memberships$ declare v_memberships text; begin select pg_catalog.string_agg( pg_catalog.format('%I -> %I', member_role.rolname, granted_role.rolname), ', ' order by member_role.rolname, granted_role.rolname ) into v_memberships from pg_catalog.pg_auth_members membership join pg_catalog.pg_roles member_role on member_role.oid = membership.member join pg_catalog.pg_roles granted_role on granted_role.oid = membership.roleid where member_role.rolname in ('kb_gate_owner', 'kb_review', 'kb_apply') or granted_role.rolname in ('kb_gate_owner', 'kb_review', 'kb_apply'); if v_memberships is not null then raise exception 'kb_apply_prereqs: unexpected protected role membership: %', v_memberships; end if; end $memberships$; -- Service identity used for the applied_by_agent_id foreign key. kb_apply can -- read this row but can never create or modify agent rows. insert into public.agents (id, handle, kind) values ('44444444-4444-4444-4444-444444444444', 'kb-apply', 'service') on conflict (handle) do nothing; -- DB-login to human-reviewer mapping. Only the gate owner/admin can change it; -- the approval function verifies --reviewed-by against this mapping rather than -- trusting caller-supplied identity. The current dedicated reviewer credential -- maps to the existing canonical m3ta human agent when that row is present. create table if not exists kb_stage.kb_review_principals ( db_role name primary key, reviewed_by_handle text not null, reviewed_by_agent_id uuid not null references public.agents(id), active boolean not null default true, created_at timestamptz not null default now() ); -- One immutable snapshot per reviewed proposal. Apply is bound to this row as -- well as the current proposal ledger, so an approved payload cannot drift. create table if not exists kb_stage.kb_proposal_approvals ( proposal_id uuid primary key references kb_stage.kb_proposals(id), proposal_type text not null, payload jsonb not null, reviewed_by_handle text not null, reviewed_by_agent_id uuid not null references public.agents(id), reviewed_by_db_role name not null, reviewed_at timestamptz not null, review_note text not null ); -- Existing canonical/proposal tables must remain owned by the deployer. Gate -- tables may be owned by the deployer during a legacy upgrade or kb_gate_owner -- after an earlier guarded run. Anything else is ownership drift. do $gate_preflight$ declare v_drift text; begin select pg_catalog.string_agg( pg_catalog.format('%I.%I owned by %I', namespace.nspname, relation.relname, owner_role.rolname), ', ' order by namespace.nspname, relation.relname ) into v_drift from pg_catalog.pg_class relation join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace join pg_catalog.pg_roles owner_role on owner_role.oid = relation.relowner where (namespace.nspname, relation.relname) in ( ('public', 'agents'), ('public', 'strategies'), ('public', 'strategy_nodes'), ('public', 'claim_evidence'), ('public', 'claim_edges'), ('public', 'claims'), ('public', 'sources'), ('public', 'reasoning_tools'), ('kb_stage', 'kb_proposals'), ('kb_stage', 'kb_review_principals'), ('kb_stage', 'kb_proposal_approvals') ) and relation.relkind in ('r', 'p') and not ( (namespace.nspname = 'kb_stage' and relation.relname in ('kb_review_principals', 'kb_proposal_approvals') and owner_role.rolname in (current_user, 'kb_gate_owner')) or (not ( namespace.nspname = 'kb_stage' and relation.relname in ('kb_review_principals', 'kb_proposal_approvals') ) and owner_role.rolname = current_user) ); if v_drift is not null then raise exception 'kb_apply_prereqs: unexpected protected table owner: %', v_drift; end if; select pg_catalog.string_agg( pg_catalog.format( '%I.%I(%s) owned by %I', namespace.nspname, procedure.proname, pg_catalog.oidvectortypes(procedure.proargtypes), owner_role.rolname ), ', ' order by procedure.proname, pg_catalog.oidvectortypes(procedure.proargtypes) ) into v_drift from pg_catalog.pg_proc procedure join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace join pg_catalog.pg_roles owner_role on owner_role.oid = procedure.proowner where namespace.nspname = 'kb_stage' and procedure.proname in ( 'approve_strict_proposal', 'assert_approved_proposal', 'finish_approved_proposal' ) and owner_role.rolname not in (current_user, 'kb_gate_owner'); if v_drift is not null then raise exception 'kb_apply_prereqs: unexpected protected function owner: %', v_drift; end if; select pg_catalog.string_agg( pg_catalog.format( '%I.%I(%s)', namespace.nspname, procedure.proname, pg_catalog.oidvectortypes(procedure.proargtypes) ), ', ' order by procedure.proname, pg_catalog.oidvectortypes(procedure.proargtypes) ) into v_drift from pg_catalog.pg_proc procedure join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace where namespace.nspname = 'kb_stage' and procedure.proname in ( 'approve_strict_proposal', 'assert_approved_proposal', 'finish_approved_proposal' ) and not ( (procedure.proname = 'approve_strict_proposal' and pg_catalog.oidvectortypes(procedure.proargtypes) in ( 'uuid, text, text', 'uuid, text, jsonb, text, text' )) or (procedure.proname = 'assert_approved_proposal' and pg_catalog.oidvectortypes(procedure.proargtypes) = 'uuid, text, jsonb, text, uuid, timestamp with time zone, text') or (procedure.proname = 'finish_approved_proposal' and pg_catalog.oidvectortypes(procedure.proargtypes) = 'uuid, text, jsonb, text, uuid, timestamp with time zone, text, text') ); if v_drift is not null then raise exception 'kb_apply_prereqs: unexpected protected function overload: %', v_drift; end if; select pg_catalog.string_agg( pg_catalog.format( '%I.%I -> %s', namespace.nspname, relation.relname, case when acl.grantee = 0 then 'PUBLIC' else pg_catalog.pg_get_userbyid(acl.grantee) end ), ', ' order by namespace.nspname, relation.relname, acl.grantee ) into v_drift from pg_catalog.pg_class relation join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace cross join lateral pg_catalog.aclexplode( coalesce(relation.relacl, pg_catalog.acldefault('r', relation.relowner)) ) acl where (namespace.nspname, relation.relname) in ( ('kb_stage', 'kb_review_principals'), ('kb_stage', 'kb_proposal_approvals') ) and acl.grantee <> 0 and acl.grantee <> relation.relowner and pg_catalog.pg_get_userbyid(acl.grantee) not in ( 'kb_gate_owner', 'kb_review', 'kb_apply' ); if v_drift is not null then raise exception 'kb_apply_prereqs: unexpected protected table grantee: %', v_drift; end if; select pg_catalog.string_agg( pg_catalog.format( '%I.%I.%I -> %s', namespace.nspname, relation.relname, attribute.attname, case when acl.grantee = 0 then 'PUBLIC' else pg_catalog.pg_get_userbyid(acl.grantee) end ), ', ' order by namespace.nspname, relation.relname, attribute.attnum, acl.grantee ) into v_drift from pg_catalog.pg_attribute attribute join pg_catalog.pg_class relation on relation.oid = attribute.attrelid join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace cross join lateral pg_catalog.aclexplode(attribute.attacl) acl where (namespace.nspname, relation.relname) in ( ('kb_stage', 'kb_review_principals'), ('kb_stage', 'kb_proposal_approvals') ) and not attribute.attisdropped and acl.grantee <> 0 and acl.grantee <> relation.relowner and pg_catalog.pg_get_userbyid(acl.grantee) not in ( 'kb_gate_owner', 'kb_review', 'kb_apply' ); if v_drift is not null then raise exception 'kb_apply_prereqs: unexpected protected table grantee: %', v_drift; end if; select pg_catalog.string_agg( pg_catalog.format( '%I.%I(%s) -> %s', namespace.nspname, procedure.proname, pg_catalog.oidvectortypes(procedure.proargtypes), case when acl.grantee = 0 then 'PUBLIC' else pg_catalog.pg_get_userbyid(acl.grantee) end ), ', ' order by procedure.proname, pg_catalog.oidvectortypes(procedure.proargtypes), acl.grantee ) into v_drift from pg_catalog.pg_proc procedure join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace cross join lateral pg_catalog.aclexplode( coalesce(procedure.proacl, pg_catalog.acldefault('f', procedure.proowner)) ) acl where namespace.nspname = 'kb_stage' and procedure.proname in ( 'approve_strict_proposal', 'assert_approved_proposal', 'finish_approved_proposal' ) and acl.grantee <> 0 and acl.grantee <> procedure.proowner and pg_catalog.pg_get_userbyid(acl.grantee) not in ( 'kb_gate_owner', 'kb_review', 'kb_apply' ); if v_drift is not null then raise exception 'kb_apply_prereqs: unexpected protected function grantee: %', v_drift; end if; end $gate_preflight$; -- This legacy three-argument SECURITY DEFINER path trusted caller-supplied -- reviewer text. Its exact signature is the only obsolete overload auto-removed. drop function if exists kb_stage.approve_strict_proposal(uuid, text, text); alter table kb_stage.kb_review_principals owner to kb_gate_owner; alter table kb_stage.kb_proposal_approvals owner to kb_gate_owner; insert into kb_stage.kb_review_principals (db_role, reviewed_by_handle, reviewed_by_agent_id, active) select 'kb_review'::name, a.handle, a.id, true from public.agents a where a.handle = 'm3ta' on conflict (db_role) do nothing; -- Reset every explicit privilege held by the protected roles before granting -- the documented matrix. This removes stale DELETE/TRUNCATE/REFERENCES/TRIGGER -- rights from upgrades instead of layering narrow grants on top of them. revoke all privileges on schema public, kb_stage from kb_gate_owner, kb_apply, kb_review; grant usage on schema public, kb_stage to kb_gate_owner; grant usage on schema public, kb_stage to kb_apply; grant usage on schema public, kb_stage to kb_review; revoke all privileges on table public.agents, public.strategies, public.strategy_nodes, public.claim_evidence, public.claim_edges, public.claims, public.sources, public.reasoning_tools, kb_stage.kb_proposals, kb_stage.kb_review_principals, kb_stage.kb_proposal_approvals from kb_gate_owner, kb_apply, kb_review; revoke all privileges on table kb_stage.kb_proposals, kb_stage.kb_review_principals, kb_stage.kb_proposal_approvals from public; -- Column grants are stored separately from table ACLs and survive a table-level -- REVOKE. Clear them explicitly so stale per-column writes cannot bypass the -- normalized table matrix. do $column_acl_reset$ declare protected_table record; begin for protected_table in select namespace.nspname as schema_name, relation.relname as table_name, pg_catalog.string_agg( pg_catalog.format('%I', attribute.attname), ', ' order by attribute.attnum ) as column_list from pg_catalog.pg_class relation join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace join pg_catalog.pg_attribute attribute on attribute.attrelid = relation.oid where (namespace.nspname, relation.relname) in ( ('public', 'agents'), ('public', 'strategies'), ('public', 'strategy_nodes'), ('public', 'claim_evidence'), ('public', 'claim_edges'), ('public', 'claims'), ('public', 'sources'), ('public', 'reasoning_tools'), ('kb_stage', 'kb_proposals'), ('kb_stage', 'kb_review_principals'), ('kb_stage', 'kb_proposal_approvals') ) and attribute.attnum > 0 and not attribute.attisdropped group by namespace.nspname, relation.relname loop execute pg_catalog.format( 'revoke all privileges (%s) on table %I.%I from kb_gate_owner, kb_apply, kb_review', protected_table.column_list, protected_table.schema_name, protected_table.table_name ); if protected_table.schema_name = 'kb_stage' and protected_table.table_name in ( 'kb_proposals', 'kb_review_principals', 'kb_proposal_approvals' ) then execute pg_catalog.format( 'revoke all privileges (%s) on table %I.%I from public', protected_table.column_list, protected_table.schema_name, protected_table.table_name ); end if; end loop; end $column_acl_reset$; grant select on public.agents to kb_gate_owner; grant select on public.agents to kb_apply; grant select on public.agents to kb_review; grant select, insert, update on public.strategies to kb_apply; grant select, insert, update on public.strategy_nodes to kb_apply; grant select, insert on public.claim_evidence to kb_apply; grant select, insert on public.claim_edges to kb_apply; grant select, insert on public.claims to kb_apply; grant select, insert on public.sources to kb_apply; grant select, insert on public.reasoning_tools to kb_apply; -- Keep the primary ledger denial explicit in addition to the full reset above. revoke update on kb_stage.kb_proposals from kb_apply; grant select on kb_stage.kb_proposals to kb_apply; grant select on kb_stage.kb_proposals to kb_review; grant select, update on kb_stage.kb_proposals to kb_gate_owner; grant select on kb_stage.kb_review_principals to kb_gate_owner; grant select, insert on kb_stage.kb_proposal_approvals to kb_gate_owner; -- Required conflict arbiters. Existing production constraints satisfy these -- checks; fresh clones receive named indexes only when no equivalent unique -- index exists. Migration failure on pre-existing duplicates is intentional. do $indexes$ begin if not exists ( select 1 from pg_catalog.pg_index i join pg_catalog.pg_class t on t.oid = i.indrelid join pg_catalog.pg_namespace n on n.oid = t.relnamespace where n.nspname = 'public' and t.relname = 'sources' and i.indisunique and i.indpred is null and replace(pg_catalog.pg_get_indexdef(i.indexrelid), ' ', '') like '%(hash)%' ) then execute 'create unique index kb_apply_sources_hash_unique on public.sources (hash)'; end if; if not exists ( select 1 from pg_catalog.pg_index i join pg_catalog.pg_class t on t.oid = i.indrelid join pg_catalog.pg_namespace n on n.oid = t.relnamespace where n.nspname = 'public' and t.relname = 'claim_evidence' and i.indisunique and i.indpred is null and replace(pg_catalog.pg_get_indexdef(i.indexrelid), ' ', '') like '%(claim_id,source_id,role)%' ) then execute 'create unique index kb_apply_claim_evidence_semantic_unique ' 'on public.claim_evidence (claim_id, source_id, role)'; end if; end $indexes$; create unique index if not exists one_active_strategy_per_agent on public.strategies (agent_id) where active; -- Reviewer-only state transition. The expected type/payload are the exact -- Python-validated snapshot; a concurrent pending-row mutation makes the -- locked WHERE clause fail. Reviewer identity comes from session_user mapping. create or replace function kb_stage.approve_strict_proposal( p_proposal_id uuid, p_expected_proposal_type text, p_expected_payload jsonb, p_expected_reviewed_by_handle text, p_review_note text ) returns jsonb language plpgsql security definer set search_path = pg_catalog, pg_temp as $function$ declare v_row kb_stage.kb_proposals%rowtype; v_principal kb_stage.kb_review_principals%rowtype; v_reviewed_at timestamptz; begin if nullif(pg_catalog.btrim(p_expected_reviewed_by_handle), '') is null then raise exception 'approve_strict_proposal: expected reviewer handle is required'; end if; if nullif(pg_catalog.btrim(p_review_note), '') is null then raise exception 'approve_strict_proposal: review note is required'; end if; select * into v_principal from kb_stage.kb_review_principals where db_role = session_user::name and active; if not found then raise exception 'approve_strict_proposal: session role % has no active reviewer principal', session_user; end if; if v_principal.reviewed_by_handle <> pg_catalog.btrim(p_expected_reviewed_by_handle) then raise exception 'approve_strict_proposal: session role % is mapped to reviewer %, not %', session_user, v_principal.reviewed_by_handle, p_expected_reviewed_by_handle; end if; select * into v_row from kb_stage.kb_proposals where id = p_proposal_id and status = 'pending_review' and proposal_type = p_expected_proposal_type and payload = p_expected_payload for update; if not found then raise exception 'approve_strict_proposal: proposal % is not the exact pending snapshot', p_proposal_id; end if; if v_row.proposal_type not in ('revise_strategy', 'add_edge', 'attach_evidence', 'approve_claim') then raise exception 'approve_strict_proposal: unsupported proposal_type %', v_row.proposal_type; end if; if pg_catalog.jsonb_typeof(v_row.payload) <> 'object' or not (v_row.payload ? 'apply_payload') or pg_catalog.jsonb_typeof(v_row.payload->'apply_payload') <> 'object' then raise exception 'approve_strict_proposal: proposal % lacks object payload.apply_payload', p_proposal_id; end if; v_reviewed_at := pg_catalog.clock_timestamp(); insert into kb_stage.kb_proposal_approvals (proposal_id, proposal_type, payload, reviewed_by_handle, reviewed_by_agent_id, reviewed_by_db_role, reviewed_at, review_note) values (v_row.id, v_row.proposal_type, v_row.payload, v_principal.reviewed_by_handle, v_principal.reviewed_by_agent_id, session_user::name, v_reviewed_at, pg_catalog.btrim(p_review_note)); update kb_stage.kb_proposals set status = 'approved', reviewed_by_handle = v_principal.reviewed_by_handle, reviewed_by_agent_id = v_principal.reviewed_by_agent_id, reviewed_at = v_reviewed_at, review_note = pg_catalog.btrim(p_review_note), updated_at = pg_catalog.clock_timestamp() where id = p_proposal_id and status = 'pending_review' and proposal_type = p_expected_proposal_type and payload = p_expected_payload returning * into v_row; if not found then raise exception 'approve_strict_proposal: proposal % changed while approval was recorded', p_proposal_id; end if; return pg_catalog.jsonb_build_object( 'id', v_row.id::text, 'proposal_type', v_row.proposal_type, 'status', v_row.status, 'reviewed_by_handle', v_row.reviewed_by_handle, 'reviewed_by_agent_id', v_row.reviewed_by_agent_id::text, 'reviewed_by_db_role', session_user::text, 'reviewed_at', v_row.reviewed_at::text, 'review_note', v_row.review_note ); end $function$; -- Apply-side precondition. It locks the proposal for the surrounding transaction -- and compares both the mutable ledger and immutable approval snapshot. create or replace function kb_stage.assert_approved_proposal( p_proposal_id uuid, p_proposal_type text, p_payload jsonb, p_reviewed_by_handle text, p_reviewed_by_agent_id uuid, p_reviewed_at timestamptz, p_review_note text ) returns void language plpgsql security definer set search_path = pg_catalog, pg_temp as $function$ begin perform 1 from kb_stage.kb_proposals p join kb_stage.kb_proposal_approvals a on a.proposal_id = p.id where p.id = p_proposal_id and p.status = 'approved' and p.proposal_type = p_proposal_type and p.payload = p_payload and p.reviewed_by_handle = p_reviewed_by_handle and p.reviewed_by_agent_id is not distinct from p_reviewed_by_agent_id and p.reviewed_at = p_reviewed_at and p.review_note = p_review_note and a.proposal_type = p_proposal_type and a.payload = p_payload and a.reviewed_by_handle = p_reviewed_by_handle and a.reviewed_by_agent_id is not distinct from p_reviewed_by_agent_id and a.reviewed_at = p_reviewed_at and a.review_note = p_review_note for update of p; if not found then raise exception 'assert_approved_proposal: proposal % is not the exact immutable approved snapshot', p_proposal_id; end if; end $function$; -- Apply-side terminal transition. It repeats every snapshot comparison while -- the proposal row remains locked, resolves a real service-agent FK, and changes -- ledger fields only. kb_apply remains a trusted narrow canonical writer; the -- Python transaction performs and independently verifies typed writes first. create or replace function kb_stage.finish_approved_proposal( p_proposal_id uuid, p_proposal_type text, p_payload jsonb, p_reviewed_by_handle text, p_reviewed_by_agent_id uuid, p_reviewed_at timestamptz, p_review_note text, p_applied_by_handle text ) returns jsonb language plpgsql security definer set search_path = pg_catalog, pg_temp as $function$ declare v_row kb_stage.kb_proposals%rowtype; v_applied_by_agent_id uuid; begin if nullif(pg_catalog.btrim(p_applied_by_handle), '') is null then raise exception 'finish_approved_proposal: applied-by handle is required'; end if; perform 1 from kb_stage.kb_proposals p join kb_stage.kb_proposal_approvals a on a.proposal_id = p.id where p.id = p_proposal_id and p.status = 'approved' and p.proposal_type = p_proposal_type and p.payload = p_payload and p.reviewed_by_handle = p_reviewed_by_handle and p.reviewed_by_agent_id is not distinct from p_reviewed_by_agent_id and p.reviewed_at = p_reviewed_at and p.review_note = p_review_note and a.proposal_type = p_proposal_type and a.payload = p_payload and a.reviewed_by_handle = p_reviewed_by_handle and a.reviewed_by_agent_id is not distinct from p_reviewed_by_agent_id and a.reviewed_at = p_reviewed_at and a.review_note = p_review_note for update of p; if not found then raise exception 'finish_approved_proposal: proposal % is not the locked immutable approved snapshot', p_proposal_id; end if; select id into v_applied_by_agent_id from public.agents where handle = pg_catalog.btrim(p_applied_by_handle) limit 1; if v_applied_by_agent_id is null then raise exception 'finish_approved_proposal: applied-by handle % has no public.agents row', p_applied_by_handle; end if; update kb_stage.kb_proposals set status = 'applied', applied_by_handle = pg_catalog.btrim(p_applied_by_handle), applied_by_agent_id = v_applied_by_agent_id, applied_at = pg_catalog.clock_timestamp(), updated_at = pg_catalog.clock_timestamp() where id = p_proposal_id and status = 'approved' and proposal_type = p_proposal_type and payload = p_payload and reviewed_by_handle = p_reviewed_by_handle and reviewed_by_agent_id is not distinct from p_reviewed_by_agent_id and reviewed_at = p_reviewed_at and review_note = p_review_note returning * into v_row; if not found then raise exception 'finish_approved_proposal: proposal % changed during apply', p_proposal_id; end if; return pg_catalog.jsonb_build_object( 'id', v_row.id::text, 'proposal_type', v_row.proposal_type, 'status', v_row.status, 'applied_by_handle', v_row.applied_by_handle, 'applied_by_agent_id', v_row.applied_by_agent_id::text, 'applied_at', v_row.applied_at::text ); end $function$; alter function kb_stage.approve_strict_proposal(uuid, text, jsonb, text, text) owner to kb_gate_owner; alter function kb_stage.assert_approved_proposal(uuid, text, jsonb, text, uuid, timestamptz, text) owner to kb_gate_owner; alter function kb_stage.finish_approved_proposal(uuid, text, jsonb, text, uuid, timestamptz, text, text) owner to kb_gate_owner; revoke all on function kb_stage.approve_strict_proposal(uuid, text, jsonb, text, text) from public, kb_gate_owner, kb_apply, kb_review; revoke all on function kb_stage.assert_approved_proposal(uuid, text, jsonb, text, uuid, timestamptz, text) from public, kb_gate_owner, kb_apply, kb_review; revoke all on function kb_stage.finish_approved_proposal(uuid, text, jsonb, text, uuid, timestamptz, text, text) from public, kb_gate_owner, kb_apply, kb_review; grant execute on function kb_stage.approve_strict_proposal(uuid, text, jsonb, text, text) to kb_review; grant execute on function kb_stage.assert_approved_proposal(uuid, text, jsonb, text, uuid, timestamptz, text) to kb_apply; grant execute on function kb_stage.finish_approved_proposal(uuid, text, jsonb, text, uuid, timestamptz, text, text) to kb_apply; -- Assert the complete protected catalog contract before commit. These checks -- make reruns an upgrade proof rather than a best-effort sequence of grants. do $final_invariants$ declare v_drift text; begin select pg_catalog.string_agg(role.rolname, ', ' order by role.rolname) into v_drift from pg_catalog.pg_roles role where role.rolname in ('kb_gate_owner', 'kb_review', 'kb_apply') and ( role.rolsuper or role.rolinherit or role.rolcreatedb or role.rolcreaterole or role.rolreplication or role.rolbypassrls or (role.rolname = 'kb_gate_owner' and role.rolcanlogin) or (role.rolname in ('kb_review', 'kb_apply') and not role.rolcanlogin) ); if v_drift is not null then raise exception 'kb_apply_prereqs: protected role attributes did not normalize: %', v_drift; end if; select pg_catalog.string_agg( pg_catalog.format('%I -> %I', member_role.rolname, granted_role.rolname), ', ' order by member_role.rolname, granted_role.rolname ) into v_drift from pg_catalog.pg_auth_members membership join pg_catalog.pg_roles member_role on member_role.oid = membership.member join pg_catalog.pg_roles granted_role on granted_role.oid = membership.roleid where member_role.rolname in ('kb_gate_owner', 'kb_review', 'kb_apply') or granted_role.rolname in ('kb_gate_owner', 'kb_review', 'kb_apply'); if v_drift is not null then raise exception 'kb_apply_prereqs: protected role membership appeared during migration: %', v_drift; end if; select pg_catalog.string_agg( pg_catalog.format( '%I on %I usage=%s create=%s', protected_role.rolname, protected_schema.nspname, pg_catalog.has_schema_privilege(protected_role.rolname, protected_schema.nspname, 'USAGE'), pg_catalog.has_schema_privilege(protected_role.rolname, protected_schema.nspname, 'CREATE') ), ', ' order by protected_role.rolname, protected_schema.nspname ) into v_drift from (values ('kb_gate_owner'), ('kb_review'), ('kb_apply')) protected_role(rolname) cross join (values ('public'), ('kb_stage')) protected_schema(nspname) where not pg_catalog.has_schema_privilege( protected_role.rolname, protected_schema.nspname, 'USAGE' ) or pg_catalog.has_schema_privilege( protected_role.rolname, protected_schema.nspname, 'CREATE' ); if v_drift is not null then raise exception 'kb_apply_prereqs: protected schema ACL mismatch: %', v_drift; end if; select pg_catalog.string_agg( pg_catalog.format('%I.%I owned by %I', namespace.nspname, relation.relname, owner_role.rolname), ', ' order by namespace.nspname, relation.relname ) into v_drift from pg_catalog.pg_class relation join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace join pg_catalog.pg_roles owner_role on owner_role.oid = relation.relowner where (namespace.nspname, relation.relname) in ( ('public', 'agents'), ('public', 'strategies'), ('public', 'strategy_nodes'), ('public', 'claim_evidence'), ('public', 'claim_edges'), ('public', 'claims'), ('public', 'sources'), ('public', 'reasoning_tools'), ('kb_stage', 'kb_proposals'), ('kb_stage', 'kb_review_principals'), ('kb_stage', 'kb_proposal_approvals') ) and not ( (namespace.nspname = 'kb_stage' and relation.relname in ('kb_review_principals', 'kb_proposal_approvals') and owner_role.rolname = 'kb_gate_owner') or (not ( namespace.nspname = 'kb_stage' and relation.relname in ('kb_review_principals', 'kb_proposal_approvals') ) and owner_role.rolname = current_user) ); if v_drift is not null then raise exception 'kb_apply_prereqs: protected table ownership mismatch: %', v_drift; end if; with expected(signature) as ( values ('approve_strict_proposal(uuid, text, jsonb, text, text)'), ('assert_approved_proposal(uuid, text, jsonb, text, uuid, timestamp with time zone, text)'), ('finish_approved_proposal(uuid, text, jsonb, text, uuid, timestamp with time zone, text, text)') ), actual(signature) as ( select procedure.proname || '(' || pg_catalog.oidvectortypes(procedure.proargtypes) || ')' from pg_catalog.pg_proc procedure join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace where namespace.nspname = 'kb_stage' and procedure.proname in ( 'approve_strict_proposal', 'assert_approved_proposal', 'finish_approved_proposal' ) ), delta(kind, signature) as ( select 'unexpected', actual.signature from actual where not exists (select 1 from expected where expected.signature = actual.signature) union all select 'missing', expected.signature from expected where not exists (select 1 from actual where actual.signature = expected.signature) ) select pg_catalog.string_agg(kind || ' ' || signature, ', ' order by kind, signature) into v_drift from delta; if v_drift is not null then raise exception 'kb_apply_prereqs: protected function signature mismatch: %', v_drift; end if; select pg_catalog.string_agg( pg_catalog.format( '%I.%I(%s) owner=%I security_definer=%s config=%s', namespace.nspname, procedure.proname, pg_catalog.oidvectortypes(procedure.proargtypes), owner_role.rolname, procedure.prosecdef, procedure.proconfig ), ', ' order by procedure.proname ) into v_drift from pg_catalog.pg_proc procedure join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace join pg_catalog.pg_roles owner_role on owner_role.oid = procedure.proowner where namespace.nspname = 'kb_stage' and procedure.proname in ( 'approve_strict_proposal', 'assert_approved_proposal', 'finish_approved_proposal' ) and ( owner_role.rolname <> 'kb_gate_owner' or not procedure.prosecdef or pg_catalog.array_to_string(procedure.proconfig, ',') is distinct from 'search_path=pg_catalog, pg_temp' ); if v_drift is not null then raise exception 'kb_apply_prereqs: protected function attribute mismatch: %', v_drift; end if; with tracked(schema_name, object_name) as ( values ('public', 'agents'), ('public', 'strategies'), ('public', 'strategy_nodes'), ('public', 'claim_evidence'), ('public', 'claim_edges'), ('public', 'claims'), ('public', 'sources'), ('public', 'reasoning_tools'), ('kb_stage', 'kb_proposals'), ('kb_stage', 'kb_review_principals'), ('kb_stage', 'kb_proposal_approvals') ) select pg_catalog.string_agg( pg_catalog.format( '%I.%I.%I -> %s %s grantable=%s', namespace.nspname, relation.relname, attribute.attname, case when acl.grantee = 0 then 'PUBLIC' else pg_catalog.pg_get_userbyid(acl.grantee) end, pg_catalog.upper(acl.privilege_type), acl.is_grantable ), ', ' order by namespace.nspname, relation.relname, attribute.attnum, acl.grantee ) into v_drift from tracked join pg_catalog.pg_namespace namespace on namespace.nspname = tracked.schema_name join pg_catalog.pg_class relation on relation.relnamespace = namespace.oid and relation.relname = tracked.object_name join pg_catalog.pg_attribute attribute on attribute.attrelid = relation.oid cross join lateral pg_catalog.aclexplode(attribute.attacl) acl left join pg_catalog.pg_roles grantee_role on grantee_role.oid = acl.grantee where attribute.attnum > 0 and not attribute.attisdropped and (acl.grantee = 0 or grantee_role.rolname in ('kb_gate_owner', 'kb_review', 'kb_apply')); if v_drift is not null then raise exception 'kb_apply_prereqs: protected column ACL mismatch: %', v_drift; end if; with expected(schema_name, object_name, grantee, privilege_type, is_grantable) as ( values ('public', 'agents', 'kb_gate_owner', 'SELECT', false), ('public', 'agents', 'kb_apply', 'SELECT', false), ('public', 'agents', 'kb_review', 'SELECT', false), ('public', 'strategies', 'kb_apply', 'SELECT', false), ('public', 'strategies', 'kb_apply', 'INSERT', false), ('public', 'strategies', 'kb_apply', 'UPDATE', false), ('public', 'strategy_nodes', 'kb_apply', 'SELECT', false), ('public', 'strategy_nodes', 'kb_apply', 'INSERT', false), ('public', 'strategy_nodes', 'kb_apply', 'UPDATE', false), ('public', 'claim_evidence', 'kb_apply', 'SELECT', false), ('public', 'claim_evidence', 'kb_apply', 'INSERT', false), ('public', 'claim_edges', 'kb_apply', 'SELECT', false), ('public', 'claim_edges', 'kb_apply', 'INSERT', false), ('public', 'claims', 'kb_apply', 'SELECT', false), ('public', 'claims', 'kb_apply', 'INSERT', false), ('public', 'sources', 'kb_apply', 'SELECT', false), ('public', 'sources', 'kb_apply', 'INSERT', false), ('public', 'reasoning_tools', 'kb_apply', 'SELECT', false), ('public', 'reasoning_tools', 'kb_apply', 'INSERT', false), ('kb_stage', 'kb_proposals', 'kb_gate_owner', 'SELECT', false), ('kb_stage', 'kb_proposals', 'kb_gate_owner', 'UPDATE', false), ('kb_stage', 'kb_proposals', 'kb_apply', 'SELECT', false), ('kb_stage', 'kb_proposals', 'kb_review', 'SELECT', false) ), tracked(schema_name, object_name) as ( values ('public', 'agents'), ('public', 'strategies'), ('public', 'strategy_nodes'), ('public', 'claim_evidence'), ('public', 'claim_edges'), ('public', 'claims'), ('public', 'sources'), ('public', 'reasoning_tools'), ('kb_stage', 'kb_proposals'), ('kb_stage', 'kb_review_principals'), ('kb_stage', 'kb_proposal_approvals') ), actual(schema_name, object_name, grantee, privilege_type, is_grantable) as ( select distinct namespace.nspname::text, relation.relname::text, (case when acl.grantee = 0 then 'PUBLIC' else grantee_role.rolname end)::text, pg_catalog.upper(acl.privilege_type), acl.is_grantable from tracked join pg_catalog.pg_namespace namespace on namespace.nspname = tracked.schema_name join pg_catalog.pg_class relation on relation.relnamespace = namespace.oid and relation.relname = tracked.object_name cross join lateral pg_catalog.aclexplode( coalesce(relation.relacl, pg_catalog.acldefault('r', relation.relowner)) ) acl left join pg_catalog.pg_roles grantee_role on grantee_role.oid = acl.grantee where acl.grantee <> relation.relowner and (acl.grantee = 0 or grantee_role.rolname in ('kb_gate_owner', 'kb_review', 'kb_apply')) ), delta(kind, schema_name, object_name, grantee, privilege_type, is_grantable) as ( select 'unexpected', actual.* from actual where not exists ( select 1 from expected where pg_catalog.to_jsonb(expected) = pg_catalog.to_jsonb(actual) ) union all select 'missing', expected.* from expected where not exists ( select 1 from actual where pg_catalog.to_jsonb(actual) = pg_catalog.to_jsonb(expected) ) ) select pg_catalog.string_agg( pg_catalog.format( '%s %I.%I -> %I %s grantable=%s', kind, schema_name, object_name, grantee, privilege_type, is_grantable ), ', ' order by kind, schema_name, object_name, grantee, privilege_type ) into v_drift from delta; if v_drift is not null then raise exception 'kb_apply_prereqs: protected table ACL mismatch: %', v_drift; end if; with expected(signature, grantee, privilege_type, is_grantable) as ( values ('approve_strict_proposal(uuid, text, jsonb, text, text)', 'kb_review', 'EXECUTE', false), ('assert_approved_proposal(uuid, text, jsonb, text, uuid, timestamp with time zone, text)', 'kb_apply', 'EXECUTE', false), ('finish_approved_proposal(uuid, text, jsonb, text, uuid, timestamp with time zone, text, text)', 'kb_apply', 'EXECUTE', false) ), actual(signature, grantee, privilege_type, is_grantable) as ( select distinct procedure.proname || '(' || pg_catalog.oidvectortypes(procedure.proargtypes) || ')', (case when acl.grantee = 0 then 'PUBLIC' else pg_catalog.pg_get_userbyid(acl.grantee) end)::text, pg_catalog.upper(acl.privilege_type), acl.is_grantable from pg_catalog.pg_proc procedure join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace cross join lateral pg_catalog.aclexplode( coalesce(procedure.proacl, pg_catalog.acldefault('f', procedure.proowner)) ) acl where namespace.nspname = 'kb_stage' and procedure.proname in ( 'approve_strict_proposal', 'assert_approved_proposal', 'finish_approved_proposal' ) and acl.grantee <> procedure.proowner ), delta(kind, signature, grantee, privilege_type, is_grantable) as ( select 'unexpected', actual.* from actual where not exists ( select 1 from expected where pg_catalog.to_jsonb(expected) = pg_catalog.to_jsonb(actual) ) union all select 'missing', expected.* from expected where not exists ( select 1 from actual where pg_catalog.to_jsonb(actual) = pg_catalog.to_jsonb(expected) ) ) select pg_catalog.string_agg( pg_catalog.format( '%s %s -> %I %s grantable=%s', kind, signature, grantee, privilege_type, is_grantable ), ', ' order by kind, signature, grantee, privilege_type ) into v_drift from delta; if v_drift is not null then raise exception 'kb_apply_prereqs: protected function ACL mismatch: %', v_drift; end if; end $final_invariants$; commit;