-- Immutable Skill package identity for Agentic Rectification and Personal Reports. -- Additive only: existing records with incomplete provenance remain readable. -- New Agentic cases bind full identity through the v2 RPC; new Personal Reports -- bind it through the server-owned persistence service. do $migration$ begin if current_user <> 'schema_owner' then raise exception 'immutable_skill_registry_requires_schema_owner' using errcode = '42501'; end if; end $migration$; alter table public.agentic_rectification_cases add column if not exists skill_sha256 text check (skill_sha256 is null or skill_sha256 ~ '^[0-9a-f]{64}$'), add column if not exists skill_source_commit text check (skill_source_commit is null or skill_source_commit ~ '^[0-9a-f]{40}$'); -- All Case writes are mediated by owner-held SECURITY DEFINER RPCs. The -- runtime role must not be able to forge a Case identity through direct INSERT -- or bypass lifecycle validation through direct UPDATE/DELETE/TRUNCATE. alter table public.agentic_rectification_cases owner to current_user; revoke all on table public.agentic_rectification_cases from service_role; -- ReportDocument v1 predates named/versioned Skill identities. Keep these -- columns nullable so historical ready rows remain readable; all newly created -- reports bind both fields through the server-owned persistence service. alter table public.personal_reports add column if not exists skill_name text check ( skill_name is null or skill_name ~ '^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$' ), add column if not exists skill_version text check ( skill_version is null or ( length(skill_version) between 5 and 80 and skill_version ~ '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$' ) ); -- Skill identity is server-owned. Direct runtime table writes are revoked; -- this trigger remains defense-in-depth for any future role grant. Approved -- SECURITY DEFINER open/upgrade/adoption RPCs execute as the table owner. create or replace function public.guard_agentic_rectification_case_skill_identity() returns trigger language plpgsql set search_path = '' as $$ declare v_table_owner name; begin if new.skill_name is not distinct from old.skill_name and new.skill_version is not distinct from old.skill_version and new.skill_sha256 is not distinct from old.skill_sha256 and new.skill_source_commit is not distinct from old.skill_source_commit then return new; end if; select pg_catalog.pg_get_userbyid(c.relowner) into v_table_owner from pg_catalog.pg_class c where c.oid = tg_relid; if current_user <> v_table_owner then raise exception 'agentic_rectification_skill_identity_immutable' using errcode = '55000'; end if; return new; end; $$; revoke all on function public.guard_agentic_rectification_case_skill_identity() from public, anon, authenticated, service_role; drop trigger if exists agentic_rectification_case_skill_identity_guard on public.agentic_rectification_cases; create trigger agentic_rectification_case_skill_identity_guard before update of skill_name, skill_version, skill_sha256, skill_source_commit on public.agentic_rectification_cases for each row execute function public.guard_agentic_rectification_case_skill_identity(); create table if not exists public.agentic_rectification_skill_upgrade_receipts ( id uuid primary key default gen_random_uuid(), case_id uuid not null references public.agentic_rectification_cases(id) on delete cascade, user_id uuid not null references auth.users(id) on delete cascade, previous_skill_name text not null, previous_skill_version text not null, previous_skill_sha256 text check (previous_skill_sha256 is null or previous_skill_sha256 ~ '^[0-9a-f]{64}$'), previous_source_commit text check (previous_source_commit is null or previous_source_commit ~ '^[0-9a-f]{40}$'), previous_identity_status text not null default 'verified', upgrade_kind text not null default 'version_upgrade', skill_name text not null, skill_version text not null, skill_sha256 text not null check (skill_sha256 ~ '^[0-9a-f]{64}$'), source_commit text check (source_commit is null or source_commit ~ '^[0-9a-f]{40}$'), created_at timestamptz not null default pg_catalog.now() ); create index if not exists agentic_rectification_skill_upgrade_receipts_case_idx on public.agentic_rectification_skill_upgrade_receipts (case_id, created_at desc); -- A prior local draft required a verified previous hash. Legacy Cases predate -- immutable package receipts, so adoption records must preserve a NULL previous -- hash together with an explicit legacy_unverifiable status. alter table public.agentic_rectification_skill_upgrade_receipts alter column previous_skill_sha256 drop not null, add column if not exists previous_identity_status text not null default 'verified', add column if not exists upgrade_kind text not null default 'version_upgrade'; alter table public.agentic_rectification_skill_upgrade_receipts owner to current_user, drop constraint if exists agentic_rectification_skill_upgrade_receipts_previous_identity_status_check, add constraint agentic_rectification_skill_upgrade_receipts_previous_identity_status_check check (previous_identity_status in ('verified', 'legacy_unverifiable')), drop constraint if exists agentic_rectification_skill_upgrade_receipts_upgrade_kind_check, add constraint agentic_rectification_skill_upgrade_receipts_upgrade_kind_check check (upgrade_kind in ('version_upgrade', 'legacy_adoption')); drop index if exists public.agentic_rectification_skill_upgrade_receipts_legacy_adoption_idx; create unique index agentic_rectification_skill_upgrade_receipts_legacy_adoption_idx on public.agentic_rectification_skill_upgrade_receipts (case_id) where upgrade_kind = 'legacy_adoption'; alter table public.agentic_rectification_skill_upgrade_receipts enable row level security; revoke all on table public.agentic_rectification_skill_upgrade_receipts from public, anon, authenticated, service_role; create or replace function public.reject_agentic_rectification_skill_receipt_mutation() returns trigger language plpgsql set search_path = '' as $$ begin if tg_op = 'TRUNCATE' then raise exception 'agentic_rectification_skill_receipts_are_append_only' using errcode = '55000'; end if; if tg_op = 'UPDATE' then raise exception 'agentic_rectification_skill_receipts_are_append_only' using errcode = '55000'; end if; -- Direct receipt deletion is forbidden while every referenced parent still -- exists. PostgreSQL FK cascades run after the parent row is deleted, so an -- account/Case/Turn lifecycle purge is allowed without a mutable session flag -- or a runtime-role backdoor. if tg_table_name = 'agentic_rectification_skill_upgrade_receipts' then if exists ( select 1 from public.agentic_rectification_cases where id = old.case_id ) and exists ( select 1 from auth.users where id = old.user_id ) then raise exception 'agentic_rectification_skill_receipts_are_append_only' using errcode = '55000'; end if; elsif tg_table_name = 'agentic_rectification_skill_run_receipts' then if exists ( select 1 from public.agentic_rectification_cases where id = old.case_id ) and exists ( select 1 from public.agentic_rectification_turns where id = old.turn_id ) and exists ( select 1 from auth.users where id = old.user_id ) then raise exception 'agentic_rectification_skill_receipts_are_append_only' using errcode = '55000'; end if; else raise exception 'agentic_rectification_skill_receipts_are_append_only' using errcode = '55000'; end if; return old; end; $$; revoke all on function public.reject_agentic_rectification_skill_receipt_mutation() from public, anon, authenticated, service_role; drop trigger if exists agentic_rectification_skill_upgrade_receipts_append_only on public.agentic_rectification_skill_upgrade_receipts; create trigger agentic_rectification_skill_upgrade_receipts_append_only before update or delete on public.agentic_rectification_skill_upgrade_receipts for each row execute function public.reject_agentic_rectification_skill_receipt_mutation(); drop trigger if exists agentic_rectification_skill_upgrade_receipts_truncate_guard on public.agentic_rectification_skill_upgrade_receipts; create trigger agentic_rectification_skill_upgrade_receipts_truncate_guard before truncate on public.agentic_rectification_skill_upgrade_receipts for each statement execute function public.reject_agentic_rectification_skill_receipt_mutation(); -- On repeat apply, CREATE OR REPLACE FUNCTION requires the migration owner to -- retain EXECUTE on an existing function. Grant it only for the duration of this -- migration; the reconciliation block below removes it again, leaving service_role -- as the only runtime EXECUTE grant. do $migration$ declare v_function regprocedure; v_signature text; begin foreach v_signature in array array[ 'public.open_agentic_rectification_case_v2(uuid, uuid, text, uuid, text, text, text, text, text, jsonb, jsonb)', 'public.get_agentic_rectification_skill_identity(uuid, uuid)', 'public.get_agentic_rectification_skill_identity_status(uuid, uuid)', 'public.upgrade_agentic_rectification_skill_v2(uuid, uuid, text, text, text, text)', 'public.adopt_agentic_rectification_skill_v1(uuid, uuid, text, text, text, text)', 'public.insert_agentic_rectification_skill_run_receipt(uuid, uuid, uuid, uuid, text, text, text, text, text)' ] loop v_function := pg_catalog.to_regprocedure(v_signature); if v_function is not null then execute pg_catalog.format( 'grant execute on function %s to %I', v_function, current_user ); end if; end loop; end $migration$; create or replace function public.open_agentic_rectification_case_v2( p_user_id uuid, p_request_id uuid, p_intent text, p_session_id uuid, p_skill_name text, p_skill_version text, p_skill_sha256 text, p_skill_source_commit text, p_baseline_profile_fingerprint text, p_baseline_birth_snapshot jsonb, p_candidate_range jsonb ) returns jsonb language plpgsql security definer set search_path = '' as $$ declare v_result jsonb; v_case public.agentic_rectification_cases%rowtype; v_case_id uuid; v_is_new boolean; begin if p_skill_name is null or length(btrim(p_skill_name)) = 0 or p_skill_version is null or length(btrim(p_skill_version)) = 0 or p_skill_sha256 is null or p_skill_sha256 !~ '^[0-9a-f]{64}$' or (p_skill_source_commit is not null and p_skill_source_commit !~ '^[0-9a-f]{40}$') then raise exception 'agentic_rectification_invalid_skill_identity' using errcode = 'P0001'; end if; v_result := public.open_agentic_rectification_case( p_user_id, p_request_id, p_intent, p_session_id, p_skill_name, p_skill_version, p_baseline_profile_fingerprint, p_baseline_birth_snapshot, p_candidate_range ); v_case_id := (v_result ->> 'case_id')::uuid; v_is_new := coalesce((v_result ->> 'should_start_opening')::boolean, false); select * into v_case from public.agentic_rectification_cases where id = v_case_id and user_id = p_user_id for update; if not found then raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001'; end if; if v_case.skill_sha256 is null then if not v_is_new then raise exception 'agentic_rectification_skill_identity_missing' using errcode = 'P0001'; end if; update public.agentic_rectification_cases set skill_sha256 = p_skill_sha256, skill_source_commit = p_skill_source_commit where id = v_case.id returning * into v_case; end if; if v_case.skill_name is distinct from p_skill_name or v_case.skill_version is distinct from p_skill_version or v_case.skill_sha256 is distinct from p_skill_sha256 or v_case.skill_source_commit is distinct from p_skill_source_commit then raise exception 'agentic_rectification_skill_identity_mismatch' using errcode = 'P0001'; end if; return v_result || jsonb_build_object( 'skill_name', v_case.skill_name, 'skill_version', v_case.skill_version, 'skill_sha256', v_case.skill_sha256, 'skill_source_commit', v_case.skill_source_commit ); end; $$; revoke all on function public.open_agentic_rectification_case_v2( uuid, uuid, text, uuid, text, text, text, text, text, jsonb, jsonb ) from public, anon, authenticated, service_role; alter function public.open_agentic_rectification_case_v2( uuid, uuid, text, uuid, text, text, text, text, text, jsonb, jsonb ) owner to current_user; grant execute on function public.open_agentic_rectification_case_v2( uuid, uuid, text, uuid, text, text, text, text, text, jsonb, jsonb ) to service_role; create or replace function public.get_agentic_rectification_skill_identity( p_user_id uuid, p_case_id uuid ) returns jsonb language plpgsql stable security definer set search_path = '' as $$ declare v_case public.agentic_rectification_cases%rowtype; begin select * into v_case from public.agentic_rectification_cases where id = p_case_id and user_id = p_user_id; if not found then raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001'; end if; if v_case.skill_sha256 is null then raise exception 'agentic_rectification_legacy_skill_identity_unverifiable' using errcode = 'P0001'; end if; return jsonb_build_object( 'skill_name', v_case.skill_name, 'skill_version', v_case.skill_version, 'skill_sha256', v_case.skill_sha256, 'skill_source_commit', v_case.skill_source_commit ); end; $$; revoke all on function public.get_agentic_rectification_skill_identity(uuid, uuid) from public, anon, authenticated, service_role; alter function public.get_agentic_rectification_skill_identity(uuid, uuid) owner to current_user; grant execute on function public.get_agentic_rectification_skill_identity(uuid, uuid) to service_role; create or replace function public.get_agentic_rectification_skill_identity_status( p_user_id uuid, p_case_id uuid ) returns jsonb language plpgsql stable security definer set search_path = '' as $$ declare v_case public.agentic_rectification_cases%rowtype; begin select * into v_case from public.agentic_rectification_cases where id = p_case_id and user_id = p_user_id; if not found then raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001'; end if; return jsonb_build_object( 'skill_name', v_case.skill_name, 'skill_version', v_case.skill_version, 'skill_sha256', v_case.skill_sha256, 'skill_source_commit', v_case.skill_source_commit, 'skill_identity_status', case when v_case.skill_sha256 is null then 'legacy_unverifiable' else 'verified' end, 'requires_skill_adoption', v_case.skill_sha256 is null and v_case.status = any (public.agentic_rectification_resumable_statuses()) ); end; $$; revoke all on function public.get_agentic_rectification_skill_identity_status(uuid, uuid) from public, anon, authenticated, service_role; alter function public.get_agentic_rectification_skill_identity_status(uuid, uuid) owner to current_user; grant execute on function public.get_agentic_rectification_skill_identity_status(uuid, uuid) to service_role; create or replace function public.upgrade_agentic_rectification_skill_v2( p_user_id uuid, p_case_id uuid, p_skill_name text, p_skill_version text, p_skill_sha256 text, p_skill_source_commit text ) returns jsonb language plpgsql security definer set search_path = '' as $$ declare v_case public.agentic_rectification_cases%rowtype; v_receipt_id uuid; begin if p_skill_name is null or length(btrim(p_skill_name)) = 0 or p_skill_version is null or length(btrim(p_skill_version)) = 0 or p_skill_sha256 is null or p_skill_sha256 !~ '^[0-9a-f]{64}$' or (p_skill_source_commit is not null and p_skill_source_commit !~ '^[0-9a-f]{40}$') then raise exception 'agentic_rectification_invalid_skill_identity' using errcode = 'P0001'; end if; select * into v_case from public.agentic_rectification_cases where id = p_case_id and user_id = p_user_id for update; if not found then raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001'; end if; if not ( v_case.status = any (public.agentic_rectification_resumable_statuses()) ) then raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001'; end if; if v_case.skill_sha256 is null then raise exception 'agentic_rectification_skill_identity_missing' using errcode = 'P0001'; end if; if v_case.skill_name is distinct from p_skill_name then raise exception 'agentic_rectification_skill_name_mismatch' using errcode = 'P0001'; end if; if v_case.skill_version = p_skill_version and v_case.skill_sha256 = p_skill_sha256 and v_case.skill_source_commit is not distinct from p_skill_source_commit then return jsonb_build_object( 'success', true, 'case_id', v_case.id, 'previous_skill_name', v_case.skill_name, 'previous_skill_version', v_case.skill_version, 'previous_skill_sha256', v_case.skill_sha256, 'previous_source_commit', v_case.skill_source_commit, 'skill_name', v_case.skill_name, 'skill_version', v_case.skill_version, 'skill_sha256', v_case.skill_sha256, 'source_commit', v_case.skill_source_commit, 'receipt_id', null, 'idempotent', true ); end if; insert into public.agentic_rectification_skill_upgrade_receipts ( case_id, user_id, previous_skill_name, previous_skill_version, previous_skill_sha256, previous_source_commit, previous_identity_status, upgrade_kind, skill_name, skill_version, skill_sha256, source_commit ) values ( v_case.id, p_user_id, v_case.skill_name, v_case.skill_version, v_case.skill_sha256, v_case.skill_source_commit, 'verified', 'version_upgrade', p_skill_name, p_skill_version, p_skill_sha256, p_skill_source_commit ) returning id into v_receipt_id; update public.agentic_rectification_cases set skill_version = p_skill_version, skill_sha256 = p_skill_sha256, skill_source_commit = p_skill_source_commit, updated_at = pg_catalog.now(), last_activity_at = pg_catalog.now() where id = v_case.id; return jsonb_build_object( 'success', true, 'case_id', v_case.id, 'previous_skill_name', v_case.skill_name, 'previous_skill_version', v_case.skill_version, 'previous_skill_sha256', v_case.skill_sha256, 'previous_source_commit', v_case.skill_source_commit, 'skill_name', p_skill_name, 'skill_version', p_skill_version, 'skill_sha256', p_skill_sha256, 'source_commit', p_skill_source_commit, 'receipt_id', v_receipt_id, 'idempotent', false ); end; $$; revoke all on function public.upgrade_agentic_rectification_skill_v2( uuid, uuid, text, text, text, text ) from public, anon, authenticated, service_role; alter function public.upgrade_agentic_rectification_skill_v2( uuid, uuid, text, text, text, text ) owner to current_user; grant execute on function public.upgrade_agentic_rectification_skill_v2( uuid, uuid, text, text, text, text ) to service_role; create or replace function public.adopt_agentic_rectification_skill_v1( p_user_id uuid, p_case_id uuid, p_skill_name text, p_skill_version text, p_skill_sha256 text, p_skill_source_commit text ) returns jsonb language plpgsql security definer set search_path = '' as $$ declare v_case public.agentic_rectification_cases%rowtype; v_receipt public.agentic_rectification_skill_upgrade_receipts%rowtype; begin if p_skill_name is null or length(btrim(p_skill_name)) = 0 or p_skill_version is null or length(btrim(p_skill_version)) = 0 or p_skill_sha256 is null or p_skill_sha256 !~ '^[0-9a-f]{64}$' or (p_skill_source_commit is not null and p_skill_source_commit !~ '^[0-9a-f]{40}$') then raise exception 'agentic_rectification_invalid_skill_identity' using errcode = 'P0001'; end if; select * into v_case from public.agentic_rectification_cases where id = p_case_id and user_id = p_user_id for update; if not found then raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001'; end if; if not ( v_case.status = any (public.agentic_rectification_resumable_statuses()) ) then raise exception 'agentic_rectification_case_terminal' using errcode = 'P0001'; end if; if v_case.skill_name is distinct from p_skill_name then raise exception 'agentic_rectification_skill_name_mismatch' using errcode = 'P0001'; end if; if v_case.skill_sha256 is not null then select * into v_receipt from public.agentic_rectification_skill_upgrade_receipts where case_id = v_case.id and upgrade_kind = 'legacy_adoption' and skill_name = p_skill_name and skill_version = p_skill_version and skill_sha256 = p_skill_sha256 and source_commit is not distinct from p_skill_source_commit order by created_at desc limit 1; if found and v_case.skill_version = p_skill_version and v_case.skill_sha256 = p_skill_sha256 and v_case.skill_source_commit is not distinct from p_skill_source_commit then return jsonb_build_object( 'success', true, 'case_id', v_case.id, 'previous_skill_name', v_receipt.previous_skill_name, 'previous_skill_version', v_receipt.previous_skill_version, 'previous_skill_sha256', v_receipt.previous_skill_sha256, 'previous_source_commit', v_receipt.previous_source_commit, 'previous_identity_status', v_receipt.previous_identity_status, 'upgrade_kind', v_receipt.upgrade_kind, 'skill_name', v_case.skill_name, 'skill_version', v_case.skill_version, 'skill_sha256', v_case.skill_sha256, 'source_commit', v_case.skill_source_commit, 'receipt_id', v_receipt.id, 'idempotent', true ); end if; raise exception 'agentic_rectification_skill_identity_already_verified' using errcode = 'P0001'; end if; insert into public.agentic_rectification_skill_upgrade_receipts ( case_id, user_id, previous_skill_name, previous_skill_version, previous_skill_sha256, previous_source_commit, previous_identity_status, upgrade_kind, skill_name, skill_version, skill_sha256, source_commit ) values ( v_case.id, p_user_id, v_case.skill_name, v_case.skill_version, null, v_case.skill_source_commit, 'legacy_unverifiable', 'legacy_adoption', p_skill_name, p_skill_version, p_skill_sha256, p_skill_source_commit ) returning * into v_receipt; update public.agentic_rectification_cases set skill_version = p_skill_version, skill_sha256 = p_skill_sha256, skill_source_commit = p_skill_source_commit, updated_at = pg_catalog.now(), last_activity_at = pg_catalog.now() where id = v_case.id; return jsonb_build_object( 'success', true, 'case_id', v_case.id, 'previous_skill_name', v_case.skill_name, 'previous_skill_version', v_case.skill_version, 'previous_skill_sha256', null, 'previous_source_commit', v_case.skill_source_commit, 'previous_identity_status', 'legacy_unverifiable', 'upgrade_kind', 'legacy_adoption', 'skill_name', p_skill_name, 'skill_version', p_skill_version, 'skill_sha256', p_skill_sha256, 'source_commit', p_skill_source_commit, 'receipt_id', v_receipt.id, 'idempotent', false ); end; $$; revoke all on function public.adopt_agentic_rectification_skill_v1( uuid, uuid, text, text, text, text ) from public, anon, authenticated, service_role; alter function public.adopt_agentic_rectification_skill_v1( uuid, uuid, text, text, text, text ) owner to current_user; grant execute on function public.adopt_agentic_rectification_skill_v1( uuid, uuid, text, text, text, text ) to service_role; create table if not exists public.agentic_rectification_skill_run_receipts ( id uuid primary key default gen_random_uuid(), case_id uuid not null references public.agentic_rectification_cases(id) on delete cascade, turn_id uuid not null references public.agentic_rectification_turns(id) on delete cascade, request_id uuid not null, run_kind text not null, user_id uuid not null references auth.users(id) on delete cascade, skill_name text not null, skill_version text not null, skill_sha256 text not null check (skill_sha256 ~ '^[0-9a-f]{64}$'), source_commit text check (source_commit is null or source_commit ~ '^[0-9a-f]{40}$'), loaded_at timestamptz not null default pg_catalog.now() ); -- Keep repeat execution safe even if the table was created by a prior local -- draft before request-scoped immutable receipts were added. alter table public.agentic_rectification_skill_run_receipts add column if not exists request_id uuid, add column if not exists run_kind text; update public.agentic_rectification_skill_run_receipts set request_id = id where request_id is null; update public.agentic_rectification_skill_run_receipts set run_kind = 'turn' where run_kind is null; alter table public.agentic_rectification_skill_run_receipts owner to current_user, alter column request_id set not null, alter column run_kind set not null, drop constraint if exists agentic_rectification_skill_run_receipts_run_kind_check, add constraint agentic_rectification_skill_run_receipts_run_kind_check check (run_kind in ('turn', 'regeneration')); -- A prior draft used UNIQUE(turn_id), which prevents a regeneration from -- recording a second immutable receipt for the same Turn. Remove only unique -- constraints/indexes whose complete key is exactly turn_id. do $migration$ declare v_name name; begin for v_name in select c.conname from pg_catalog.pg_constraint c where c.conrelid = 'public.agentic_rectification_skill_run_receipts'::regclass and c.contype = 'u' and ( select pg_catalog.array_agg(a.attname::text order by k.ordinality) from pg_catalog.unnest(c.conkey) with ordinality as k(attnum, ordinality) join pg_catalog.pg_attribute a on a.attrelid = c.conrelid and a.attnum = k.attnum ) = array['turn_id']::text[] loop execute pg_catalog.format( 'alter table public.agentic_rectification_skill_run_receipts drop constraint %I', v_name ); end loop; for v_name in select ci.relname from pg_catalog.pg_index i join pg_catalog.pg_class ci on ci.oid = i.indexrelid where i.indrelid = 'public.agentic_rectification_skill_run_receipts'::regclass and i.indisunique and not i.indisprimary and not exists ( select 1 from pg_catalog.pg_constraint c where c.conindid = i.indexrelid ) and ( select pg_catalog.array_agg(a.attname::text order by k.ordinality) from pg_catalog.unnest(i.indkey) with ordinality as k(attnum, ordinality) join pg_catalog.pg_attribute a on a.attrelid = i.indrelid and a.attnum = k.attnum where k.attnum > 0 ) = array['turn_id']::text[] loop execute pg_catalog.format('drop index public.%I', v_name); end loop; end; $migration$; drop index if exists public.agentic_rectification_skill_run_receipts_request_idx; create unique index agentic_rectification_skill_run_receipts_request_idx on public.agentic_rectification_skill_run_receipts ( case_id, turn_id, request_id, run_kind ); create index if not exists agentic_rectification_skill_run_receipts_case_idx on public.agentic_rectification_skill_run_receipts (case_id, loaded_at desc); alter table public.agentic_rectification_skill_run_receipts enable row level security; revoke all on table public.agentic_rectification_skill_run_receipts from public, anon, authenticated, service_role; drop trigger if exists agentic_rectification_skill_run_receipts_append_only on public.agentic_rectification_skill_run_receipts; create trigger agentic_rectification_skill_run_receipts_append_only before update or delete on public.agentic_rectification_skill_run_receipts for each row execute function public.reject_agentic_rectification_skill_receipt_mutation(); drop trigger if exists agentic_rectification_skill_run_receipts_truncate_guard on public.agentic_rectification_skill_run_receipts; create trigger agentic_rectification_skill_run_receipts_truncate_guard before truncate on public.agentic_rectification_skill_run_receipts for each statement execute function public.reject_agentic_rectification_skill_receipt_mutation(); create or replace function public.insert_agentic_rectification_skill_run_receipt( p_user_id uuid, p_case_id uuid, p_turn_id uuid, p_request_id uuid, p_run_kind text, p_skill_name text, p_skill_version text, p_skill_sha256 text, p_source_commit text ) returns jsonb language plpgsql security definer set search_path = '' as $$ declare v_case public.agentic_rectification_cases%rowtype; v_receipt public.agentic_rectification_skill_run_receipts%rowtype; begin if p_request_id is null or p_run_kind is null or p_run_kind not in ('turn', 'regeneration') then raise exception 'agentic_rectification_invalid_skill_receipt' using errcode = 'P0001'; end if; select * into v_case from public.agentic_rectification_cases where id = p_case_id and user_id = p_user_id for update; if not found then raise exception 'agentic_rectification_case_not_found' using errcode = 'P0001'; end if; if not exists ( select 1 from public.agentic_rectification_turns where id = p_turn_id and case_id = p_case_id ) then raise exception 'agentic_rectification_turn_not_found' using errcode = 'P0001'; end if; if v_case.skill_name is distinct from p_skill_name or v_case.skill_version is distinct from p_skill_version or v_case.skill_sha256 is distinct from p_skill_sha256 or v_case.skill_source_commit is distinct from p_source_commit then raise exception 'agentic_rectification_skill_identity_mismatch' using errcode = 'P0001'; end if; insert into public.agentic_rectification_skill_run_receipts ( case_id, turn_id, request_id, run_kind, user_id, skill_name, skill_version, skill_sha256, source_commit ) values ( p_case_id, p_turn_id, p_request_id, p_run_kind, p_user_id, p_skill_name, p_skill_version, p_skill_sha256, p_source_commit ) on conflict (case_id, turn_id, request_id, run_kind) do nothing returning * into v_receipt; if v_receipt.id is null then select * into v_receipt from public.agentic_rectification_skill_run_receipts where case_id = p_case_id and turn_id = p_turn_id and request_id = p_request_id and run_kind = p_run_kind; if not found or v_receipt.user_id is distinct from p_user_id or v_receipt.skill_name is distinct from p_skill_name or v_receipt.skill_version is distinct from p_skill_version or v_receipt.skill_sha256 is distinct from p_skill_sha256 or v_receipt.source_commit is distinct from p_source_commit then raise exception 'agentic_rectification_skill_receipt_conflict' using errcode = 'P0001'; end if; end if; return jsonb_build_object( 'receipt_id', v_receipt.id, 'request_id', v_receipt.request_id, 'run_kind', v_receipt.run_kind, 'skill_name', v_receipt.skill_name, 'skill_version', v_receipt.skill_version, 'skill_sha256', v_receipt.skill_sha256, 'source_commit', v_receipt.source_commit ); end; $$; revoke all on function public.insert_agentic_rectification_skill_run_receipt( uuid, uuid, uuid, uuid, text, text, text, text, text ) from public, anon, authenticated, service_role; alter function public.insert_agentic_rectification_skill_run_receipt( uuid, uuid, uuid, uuid, text, text, text, text, text ) owner to current_user; grant execute on function public.insert_agentic_rectification_skill_run_receipt( uuid, uuid, uuid, uuid, text, text, text, text, text ) to service_role; -- CREATE OR REPLACE FUNCTION preserves existing explicit ACL entries. Reconcile -- historical grants on every SECURITY DEFINER RPC exposed by this migration, -- retaining only the intended service_role grant. Revoking a direct grant from -- service_runtime does not alter its GRANT service_role membership, so the runtime -- path that SET ROLE service_role retains effective EXECUTE. do $migration$ declare v_service_role oid; v_function record; v_grantee name; v_function_identity text; begin select r.oid into v_service_role from pg_catalog.pg_roles r where r.rolname = 'service_role'; if v_service_role is null then raise exception 'immutable_skill_registry_service_role_missing'; end if; for v_function in select p.oid, p.proname, pg_catalog.pg_get_function_identity_arguments(p.oid) as identity_arguments from pg_catalog.pg_proc p join pg_catalog.pg_namespace n on n.oid = p.pronamespace where n.nspname = 'public' and p.prosecdef and p.oid in ( 'public.open_agentic_rectification_case_v2(uuid, uuid, text, uuid, text, text, text, text, text, jsonb, jsonb)'::regprocedure, 'public.get_agentic_rectification_skill_identity(uuid, uuid)'::regprocedure, 'public.get_agentic_rectification_skill_identity_status(uuid, uuid)'::regprocedure, 'public.upgrade_agentic_rectification_skill_v2(uuid, uuid, text, text, text, text)'::regprocedure, 'public.adopt_agentic_rectification_skill_v1(uuid, uuid, text, text, text, text)'::regprocedure, 'public.insert_agentic_rectification_skill_run_receipt(uuid, uuid, uuid, uuid, text, text, text, text, text)'::regprocedure ) loop v_function_identity := pg_catalog.format( 'public.%I(%s)', v_function.proname, v_function.identity_arguments ); -- PUBLIC is represented by grantee OID 0 and must use the PUBLIC -- keyword, not a quoted role named "public". execute pg_catalog.format( 'revoke execute on function %s from public', v_function_identity ); for v_grantee in select distinct pg_catalog.pg_get_userbyid(a.grantee)::name from pg_catalog.pg_proc p cross join lateral pg_catalog.aclexplode(p.proacl) a where p.oid = v_function.oid and a.privilege_type = 'EXECUTE' and a.grantee <> 0 and a.grantee <> v_service_role loop execute pg_catalog.format( 'revoke execute on function %s from %I', v_function_identity, v_grantee ); end loop; end loop; end $migration$;