94 lines
5.9 KiB
PL/PgSQL
94 lines
5.9 KiB
PL/PgSQL
-- Additive durable scoped manual inverse-one charge. No execution grant.
|
|
-- Existing 0043 is immutable. Logical charge identities outlive family FKs.
|
|
BEGIN;
|
|
DO $$ BEGIN
|
|
IF to_regclass('public.mcp_routing_manual_inverse_charges') IS NOT NULL
|
|
THEN RAISE EXCEPTION 'unmarked manual inverse storage requires reconciliation'; END IF;
|
|
END $$;
|
|
CREATE TABLE public.mcp_routing_manual_inverse_charges (
|
|
recovery_operation_id uuid PRIMARY KEY,
|
|
recovery_plan_id uuid NOT NULL,
|
|
source_operation_id uuid NOT NULL,
|
|
source_plan_id uuid NOT NULL,
|
|
source_step_index integer NOT NULL CHECK(source_step_index=0),
|
|
profile_id uuid NOT NULL,
|
|
profile_version integer NOT NULL CHECK(profile_version>0),
|
|
connection_id uuid NOT NULL,
|
|
owner_id text NOT NULL CHECK(octet_length(owner_id) BETWEEN 1 AND 1000),
|
|
charge_units integer NOT NULL CHECK(charge_units=1),
|
|
binding_sealed text NOT NULL CHECK(octet_length(binding_sealed) BETWEEN 1 AND 262144),
|
|
binding_proof text NOT NULL CHECK(binding_proof ~ '^[a-f0-9]{64}$'),
|
|
created_at timestamptz NOT NULL,
|
|
CONSTRAINT mcp_manual_inverse_source_step_unique UNIQUE(source_operation_id,source_step_index)
|
|
);
|
|
CREATE INDEX mcp_routing_manual_inverse_charges_window
|
|
ON public.mcp_routing_manual_inverse_charges(profile_id,created_at);
|
|
CREATE INDEX mcp_routing_manual_inverse_charges_retention
|
|
ON public.mcp_routing_manual_inverse_charges(created_at,recovery_operation_id);
|
|
CREATE INDEX mcp_scoped_recovery_manual_history
|
|
ON public.mcp_scoped_recovery_intents(mode,source_operation_id,source_step_index)
|
|
WHERE mode='manual';
|
|
CREATE FUNCTION public.mcp_routing_manual_inverse_charge_source() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
IF NOT EXISTS(
|
|
SELECT 1 FROM public.mcp_operations o
|
|
JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
|
|
JOIN public.mcp_routing_profiles r ON r.id=NEW.profile_id AND r.connection_id=o.connection_id
|
|
JOIN public.mcp_operations source ON source.id=NEW.source_operation_id AND source.connection_id=o.connection_id
|
|
JOIN public.mcp_plans sp ON sp.id=source.plan_id AND sp.connection_id=source.connection_id
|
|
JOIN public.mcp_connections c ON c.id=o.connection_id
|
|
WHERE o.id=NEW.recovery_operation_id AND p.id=NEW.recovery_plan_id
|
|
AND o.connection_id=NEW.connection_id AND c.owner_id=NEW.owner_id
|
|
AND r.version=NEW.profile_version AND o.state='pending' AND p.state='executing'
|
|
AND p.operation_id='operations.recover' AND p.operation_version=7
|
|
AND sp.id=NEW.source_plan_id AND sp.operation_id='routing.community.switch' AND sp.operation_version=7
|
|
AND source.state IN('applied','partial')
|
|
) THEN RAISE EXCEPTION 'manual inverse charge differs from pending admission'; END IF;
|
|
RETURN NEW;
|
|
END $$;
|
|
CREATE TRIGGER mcp_routing_manual_inverse_charge_source BEFORE INSERT ON public.mcp_routing_manual_inverse_charges
|
|
FOR EACH ROW EXECUTE FUNCTION public.mcp_routing_manual_inverse_charge_source();
|
|
CREATE FUNCTION public.mcp_routing_manual_inverse_charge_immutable() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
DECLARE retention integer;
|
|
BEGIN
|
|
IF TG_OP<>'DELETE' THEN RAISE EXCEPTION 'immutable manual inverse charge; rewrite/refund forbidden'; END IF;
|
|
SELECT requests_retention_days INTO retention FROM public.mcp_settings WHERE id=1 FOR SHARE;
|
|
IF retention IS NULL OR retention NOT BETWEEN 1 AND 365
|
|
OR OLD.created_at>clock_timestamp()-interval '1 hour'
|
|
OR OLD.created_at>=clock_timestamp()-make_interval(days=>retention)
|
|
OR EXISTS(SELECT 1 FROM public.mcp_operations WHERE id=OLD.recovery_operation_id AND state IN('pending','running'))
|
|
THEN RAISE EXCEPTION 'manual inverse charge still retained or unresolved'; END IF;
|
|
RETURN OLD;
|
|
END $$;
|
|
CREATE TRIGGER mcp_routing_manual_inverse_charge_immutable BEFORE UPDATE OR DELETE ON public.mcp_routing_manual_inverse_charges
|
|
FOR EACH ROW EXECUTE FUNCTION public.mcp_routing_manual_inverse_charge_immutable();
|
|
CREATE TRIGGER mcp_routing_manual_inverse_charge_no_truncate BEFORE TRUNCATE ON public.mcp_routing_manual_inverse_charges
|
|
FOR EACH STATEMENT EXECUTE FUNCTION public.mcp_routing_manual_inverse_charge_immutable();
|
|
-- This is a version/identity fence, never authentication of encrypted source.
|
|
-- Old binaries cannot create a fresh v6 manual intent after this DDL commits.
|
|
-- Already-claimed old work requires the normal drained-writer cutover.
|
|
CREATE FUNCTION public.mcp_scoped_recovery_manual_version() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
IF NEW.mode='manual' AND NOT pg_try_advisory_xact_lock(hashtextextended('mcp-runner-admission',0))
|
|
THEN RAISE EXCEPTION 'manual intent admission snapshot busy'; END IF;
|
|
IF NEW.mode='manual' AND NOT EXISTS(
|
|
SELECT 1 FROM public.mcp_operations o
|
|
JOIN public.mcp_plans p ON p.id=o.plan_id AND p.connection_id=o.connection_id
|
|
JOIN public.mcp_operation_steps s ON s.operation_id=o.id AND s.step_index=0
|
|
JOIN public.mcp_operations source ON source.id=NEW.source_operation_id AND source.connection_id=o.connection_id
|
|
WHERE o.id=NEW.recovery_operation_id AND o.state='running'
|
|
AND p.state='executing' AND p.operation_id='operations.recover' AND p.operation_version=7
|
|
AND NEW.source_step_index=0 AND s.state='intent'
|
|
AND s.intended_state->>'kind'='service'
|
|
AND s.intended_state->>'service'='routing.community.scoped.recover'
|
|
AND s.intended_state->'input'->>'mode'='manual'
|
|
AND s.intended_state->'input'->>'sourceOperationId'=NEW.source_operation_id::text
|
|
AND s.intended_state->'input'->>'receiptId'=NEW.recovery_receipt_id::text
|
|
) THEN RAISE EXCEPTION 'manual inverse requires current exact running recovery'; END IF;
|
|
RETURN NEW;
|
|
END $$;
|
|
CREATE TRIGGER mcp_scoped_recovery_manual_version BEFORE INSERT ON public.mcp_scoped_recovery_intents
|
|
FOR EACH ROW EXECUTE FUNCTION public.mcp_scoped_recovery_manual_version();
|
|
INSERT INTO public.schema_migrations(id) VALUES('0044_mcp_routing_manual_inverse_charges') ON CONFLICT(id) DO NOTHING;
|
|
COMMIT;
|