20260818_553_governed_plugin_platform.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. """Create the closed, fixture-only P3-WP13 plugin control plane."""
  2. from alembic import op
  3. revision = "20260818_553"
  4. down_revision = "20260818_552"
  5. branch_labels = None
  6. depends_on = None
  7. _TABLES = (
  8. "plugin_registry_versions",
  9. "plugin_approvals",
  10. "plugin_runtime_leases",
  11. "plugin_runs",
  12. "plugin_dead_letters",
  13. "plugin_audit_outbox",
  14. )
  15. def upgrade() -> None:
  16. op.get_bind().exec_driver_sql(r'''
  17. DO $roles$ BEGIN
  18. IF NOT EXISTS(SELECT 1 FROM pg_roles WHERE rolname='dataops_plugin_platform_owner' AND NOT rolcanlogin AND NOT rolsuper AND NOT rolcreaterole)
  19. OR NOT EXISTS(SELECT 1 FROM pg_roles WHERE rolname='dataops_plugin_platform_control' AND NOT rolcanlogin)
  20. OR NOT EXISTS(SELECT 1 FROM pg_roles WHERE rolname='dataops_app_runtime') THEN
  21. RAISE EXCEPTION 'WP13 roles must be provisioned by role-init';
  22. END IF;
  23. END $roles$;
  24. CREATE TABLE public.plugin_registry_versions (
  25. plugin_uid text NOT NULL CHECK(plugin_uid ~ '^[a-z][a-z0-9-]{2,62}$'),
  26. version text NOT NULL CHECK(version ~ '^[0-9]+[.][0-9]+[.][0-9]+([-.][A-Za-z0-9.]+)?$'),
  27. plugin_type text NOT NULL CHECK(plugin_type IN ('connector','parser','quality','notification','approval','agent_mcp')),
  28. manifest_digest char(64) NOT NULL CHECK(manifest_digest ~ '^[0-9a-f]{64}$'),
  29. artifact_digest char(64) NOT NULL CHECK(artifact_digest ~ '^[0-9a-f]{64}$'),
  30. trust_store_key_id text NOT NULL CHECK(trust_store_key_id='local-fixture-key-v1'),
  31. signature_digest char(64) NOT NULL CHECK(signature_digest ~ '^[0-9a-f]{64}$'),
  32. sbom_digest char(64) NOT NULL CHECK(sbom_digest ~ '^[0-9a-f]{64}$'),
  33. license_digest char(64) NOT NULL CHECK(license_digest ~ '^[0-9a-f]{64}$'),
  34. vulnerability_digest char(64) NOT NULL CHECK(vulnerability_digest ~ '^[0-9a-f]{64}$'),
  35. provenance_digest char(64) NOT NULL CHECK(provenance_digest ~ '^[0-9a-f]{64}$'),
  36. fixture_id text NOT NULL CHECK(fixture_id='ENGINEERING_EVIDENCE_ONLY'),
  37. permissions jsonb NOT NULL CHECK(permissions='{"file": false, "network": false, "secret": false, "child_process": false}'::jsonb),
  38. state text NOT NULL DEFAULT 'draft' CHECK(state IN ('draft','reviewed','approved','canary','active','paused','revoked','rolled_back','recovery')),
  39. review_actor text, reviewed_at timestamptz, activated_at timestamptz,
  40. created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
  41. PRIMARY KEY(plugin_uid,version), UNIQUE(manifest_digest), UNIQUE(artifact_digest)
  42. );
  43. CREATE UNIQUE INDEX plugin_registry_one_active ON public.plugin_registry_versions(plugin_uid) WHERE state='active';
  44. CREATE TABLE public.plugin_approvals (
  45. approval_uid uuid PRIMARY KEY DEFAULT gen_random_uuid(), plugin_uid text NOT NULL, version text NOT NULL,
  46. action_name text NOT NULL CHECK(action_name IN ('approve','canary','activate','pause','rollback','revoke','recover')),
  47. actor_ref text NOT NULL CHECK(actor_ref ~ '^[A-Za-z0-9_.:-]{1,120}$'),
  48. reviewer_ref text NOT NULL CHECK(reviewer_ref ~ '^[A-Za-z0-9_.:-]{1,120}$' AND reviewer_ref<>actor_ref),
  49. tenant_ref text NOT NULL CHECK(tenant_ref ~ '^[a-z][a-z0-9-]{0,62}$'), domain_ref text NOT NULL CHECK(domain_ref ~ '^[a-z][a-z0-9-]{0,62}$'),
  50. manifest_digest char(64) NOT NULL CHECK(manifest_digest ~ '^[0-9a-f]{64}$'), expires_at timestamptz NOT NULL,
  51. consumed_at timestamptz, created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
  52. FOREIGN KEY(plugin_uid,version) REFERENCES public.plugin_registry_versions(plugin_uid,version) ON DELETE RESTRICT,
  53. UNIQUE(plugin_uid,version,action_name,actor_ref,tenant_ref,domain_ref,manifest_digest)
  54. );
  55. CREATE TABLE public.plugin_runtime_leases (
  56. plugin_uid text NOT NULL, version text NOT NULL, tenant_ref text NOT NULL,
  57. lease_owner text, lease_token uuid, lease_fence bigint NOT NULL DEFAULT 0 CHECK(lease_fence>=0), lease_expires_at timestamptz,
  58. PRIMARY KEY(plugin_uid,version,tenant_ref), FOREIGN KEY(plugin_uid,version) REFERENCES public.plugin_registry_versions(plugin_uid,version) ON DELETE RESTRICT
  59. );
  60. CREATE TABLE public.plugin_runs (
  61. run_uid uuid PRIMARY KEY DEFAULT gen_random_uuid(), plugin_uid text NOT NULL, version text NOT NULL,
  62. tenant_ref text NOT NULL, domain_ref text NOT NULL, principal_ref text NOT NULL, operation_name text NOT NULL,
  63. idempotency_key text NOT NULL CHECK(idempotency_key ~ '^[A-Za-z0-9_.:-]{1,120}$'),
  64. input_digest char(64) NOT NULL CHECK(input_digest ~ '^[0-9a-f]{64}$'), output_digest char(64),
  65. request_digest char(64) NOT NULL CHECK(request_digest ~ '^[0-9a-f]{64}$'), lease_fence bigint NOT NULL CHECK(lease_fence>=0),
  66. state text NOT NULL CHECK(state IN ('running','succeeded','failed','dead_letter')),
  67. created_at timestamptz NOT NULL DEFAULT clock_timestamp(), completed_at timestamptz,
  68. FOREIGN KEY(plugin_uid,version) REFERENCES public.plugin_registry_versions(plugin_uid,version) ON DELETE RESTRICT,
  69. UNIQUE(plugin_uid,version,tenant_ref,idempotency_key)
  70. );
  71. CREATE TABLE public.plugin_dead_letters (
  72. dead_letter_uid uuid PRIMARY KEY DEFAULT gen_random_uuid(), run_uid uuid NOT NULL UNIQUE REFERENCES public.plugin_runs(run_uid) ON DELETE RESTRICT,
  73. failure_digest char(64) NOT NULL CHECK(failure_digest ~ '^[0-9a-f]{64}$'), incident_ref text, created_at timestamptz NOT NULL DEFAULT clock_timestamp()
  74. );
  75. CREATE TABLE public.plugin_audit_outbox (
  76. audit_uid uuid PRIMARY KEY DEFAULT gen_random_uuid(), plugin_uid text NOT NULL, version text NOT NULL, tenant_ref text NOT NULL,
  77. event_type text NOT NULL CHECK(event_type IN ('registered','reviewed','approved','canary','active','paused','rolled_back','revoked','recovery','invoked','dead_letter')),
  78. actor_ref text NOT NULL, payload_digest char(64) NOT NULL CHECK(payload_digest ~ '^[0-9a-f]{64}$'), created_at timestamptz NOT NULL DEFAULT clock_timestamp()
  79. );
  80. DO $owner$ DECLARE t text; BEGIN
  81. FOREACH t IN ARRAY ARRAY['plugin_registry_versions','plugin_approvals','plugin_runtime_leases','plugin_runs','plugin_dead_letters','plugin_audit_outbox'] LOOP
  82. EXECUTE 'ALTER TABLE public.'||quote_ident(t)||' OWNER TO dataops_plugin_platform_owner';
  83. EXECUTE 'REVOKE ALL ON TABLE public.'||quote_ident(t)||' FROM PUBLIC,dataops_app,dataops_app_runtime';
  84. END LOOP;
  85. END $owner$;
  86. CREATE FUNCTION public.plugin_platform_runtime_execute(p jsonb) RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path=pg_catalog,public AS $run$
  87. DECLARE r record; digest text; out_digest text;
  88. BEGIN
  89. IF NOT pg_has_role(session_user,'dataops_app_runtime','MEMBER') OR jsonb_typeof(p)<>'object'
  90. OR p-ARRAY['plugin_uid','version','tenant_ref','domain_ref','principal_ref','operation_name','idempotency_key','input_digest','lease_token','lease_fence']<>'{}'::jsonb
  91. OR p->>'plugin_uid' !~ '^[a-z][a-z0-9-]{2,62}$' OR p->>'input_digest' !~ '^[0-9a-f]{64}$' THEN RAISE EXCEPTION 'plugin_runtime_denied'; END IF;
  92. SELECT * INTO r FROM public.plugin_registry_versions WHERE plugin_uid=p->>'plugin_uid' AND version=p->>'version' AND state='active' AND fixture_id='ENGINEERING_EVIDENCE_ONLY' AND permissions='{"file": false, "network": false, "secret": false, "child_process": false}'::jsonb;
  93. IF NOT FOUND THEN RAISE EXCEPTION 'plugin_not_active'; END IF;
  94. digest:=encode(sha256(convert_to((p-ARRAY['lease_token'])::text,'utf8')),'hex');
  95. SELECT encode(sha256(convert_to('ENGINEERING_EVIDENCE_ONLY|'||r.manifest_digest||'|'||digest,'utf8')),'hex') INTO out_digest;
  96. INSERT INTO public.plugin_runs(plugin_uid,version,tenant_ref,domain_ref,principal_ref,operation_name,idempotency_key,input_digest,output_digest,request_digest,lease_fence,state,completed_at)
  97. VALUES(r.plugin_uid,r.version,p->>'tenant_ref',p->>'domain_ref',p->>'principal_ref',p->>'operation_name',p->>'idempotency_key',p->>'input_digest',out_digest,digest,(p->>'lease_fence')::bigint,'succeeded',clock_timestamp())
  98. ON CONFLICT(plugin_uid,version,tenant_ref,idempotency_key) DO NOTHING;
  99. RETURN jsonb_build_object('plugin_uid',r.plugin_uid,'version',r.version,'output_digest',out_digest,'fixture_id','ENGINEERING_EVIDENCE_ONLY');
  100. END $run$;
  101. ALTER FUNCTION public.plugin_platform_runtime_execute(jsonb) OWNER TO dataops_plugin_platform_owner;
  102. REVOKE ALL ON FUNCTION public.plugin_platform_runtime_execute(jsonb) FROM PUBLIC,dataops_app;
  103. GRANT EXECUTE ON FUNCTION public.plugin_platform_runtime_execute(jsonb) TO dataops_app_runtime;
  104. ''')
  105. def downgrade() -> None:
  106. bind = op.get_bind()
  107. checks = " OR ".join(f"EXISTS(SELECT 1 FROM public.{table} LIMIT 1)" for table in _TABLES)
  108. if bind.exec_driver_sql(f"SELECT {checks}").scalar():
  109. raise RuntimeError("downgrade refused: WP13 plugin facts are nonempty")
  110. bind.exec_driver_sql("""
  111. REVOKE ALL ON FUNCTION public.plugin_platform_runtime_execute(jsonb) FROM PUBLIC,dataops_app,dataops_app_runtime;
  112. DROP FUNCTION public.plugin_platform_runtime_execute(jsonb);
  113. DROP TABLE public.plugin_audit_outbox,public.plugin_dead_letters,public.plugin_runs,public.plugin_runtime_leases,public.plugin_approvals,public.plugin_registry_versions;
  114. """)