Files
Jyotisha/frontend/supabase/migrations/20260814010000_immutable_skill_registry.sql
T

900 lines
34 KiB
PL/PgSQL

-- 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$;