900 lines
34 KiB
PL/PgSQL
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$;
|