@opengeni/db 1.0.1 → 3.1.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) hide show
  1. package/dist/attached-browser-devices.d.ts +7 -0
  2. package/dist/automations.d.ts +154 -0
  3. package/dist/canonical-human-identities.js +3 -3
  4. package/dist/capability-integrations.d.ts +13 -0
  5. package/dist/child-lifecycle-notices.d.ts +68 -0
  6. package/dist/{chunk-LTG3S6ZY.js → chunk-EULAN2NP.js} +2 -2
  7. package/dist/{chunk-CUPM3FGB.js → chunk-NJVZPWXY.js} +3 -3
  8. package/dist/{chunk-4SR4HKKA.js → chunk-NMUBV7Z5.js} +19 -11
  9. package/dist/chunk-NMUBV7Z5.js.map +1 -0
  10. package/dist/chunk-OF5OBB76.js +4457 -0
  11. package/dist/chunk-OF5OBB76.js.map +1 -0
  12. package/dist/{chunk-6ZYKCRFO.js → chunk-PR2WMLOQ.js} +7580 -4417
  13. package/dist/chunk-PR2WMLOQ.js.map +1 -0
  14. package/dist/{chunk-ZWREYEWA.js → chunk-SCPUJS53.js} +41 -4
  15. package/dist/chunk-SCPUJS53.js.map +1 -0
  16. package/dist/{chunk-SK7YYHBI.js → chunk-SIBYBA2D.js} +46 -1
  17. package/dist/chunk-SIBYBA2D.js.map +1 -0
  18. package/dist/chunk-TIKF3KTI.js +383 -0
  19. package/dist/chunk-TIKF3KTI.js.map +1 -0
  20. package/dist/chunk-ZZ5LJEFX.js +516 -0
  21. package/dist/chunk-ZZ5LJEFX.js.map +1 -0
  22. package/dist/codemode-operations.d.ts +1 -6
  23. package/dist/company-brain-context-selection-schema.d.ts +705 -0
  24. package/dist/company-brain-context-selection.d.ts +94 -0
  25. package/dist/company-brain-governed-writes.d.ts +24 -0
  26. package/dist/company-brain.d.ts +55 -0
  27. package/dist/company-profile-agent-admin.d.ts +15 -0
  28. package/dist/company-profile-schema.d.ts +619 -2
  29. package/dist/company-profile.d.ts +9 -4
  30. package/dist/connection-authority.d.ts +95 -0
  31. package/dist/connection-scopes.d.ts +8 -0
  32. package/dist/connection-tenancy-backfill.d.ts +41 -0
  33. package/dist/connection-token-resolver.d.ts +30 -1
  34. package/dist/database.d.ts +19 -0
  35. package/dist/editable-artifact-durable-export.js +2 -2
  36. package/dist/editable-artifacts.d.ts +2 -2
  37. package/dist/editable-artifacts.js +3 -3
  38. package/dist/governed-learning-activation-schema.d.ts +984 -0
  39. package/dist/governed-learning-activation.d.ts +98 -0
  40. package/dist/governed-learning-evaluator-schema.d.ts +633 -0
  41. package/dist/governed-learning-evaluator.d.ts +28 -0
  42. package/dist/index.d.ts +1595 -81
  43. package/dist/index.js +21571 -8126
  44. package/dist/index.js.map +1 -1
  45. package/dist/insights.d.ts +18 -0
  46. package/dist/integration-bindings.d.ts +1 -0
  47. package/dist/integration-facets.d.ts +30 -0
  48. package/dist/interaction-placement-loss.d.ts +22 -0
  49. package/dist/managed-human-provisioning.d.ts +25 -0
  50. package/dist/memory-domain.d.ts +3 -3
  51. package/dist/migrate.d.ts +11 -1
  52. package/dist/migrate.js +1 -1
  53. package/dist/new-session-drafts.d.ts +43 -5
  54. package/dist/organization-membership-backfill.d.ts +174 -0
  55. package/dist/organization-membership-lifecycle.d.ts +162 -0
  56. package/dist/pr-review.d.ts +107 -0
  57. package/dist/preference-registry-schema.d.ts +2 -0
  58. package/dist/preference-registry.d.ts +2 -0
  59. package/dist/provision-roles.js +1 -1
  60. package/dist/runtime-posture.d.ts +22 -8
  61. package/dist/schema.d.ts +24914 -13703
  62. package/dist/schema.js +99 -1
  63. package/dist/scoped-knowledge-schema.d.ts +52 -1
  64. package/dist/session-background-commands.d.ts +125 -0
  65. package/dist/session-background-commands.js +34 -0
  66. package/dist/session-background-commands.js.map +1 -0
  67. package/dist/session-control.d.ts +156 -3
  68. package/dist/session-goal-pacing.d.ts +68 -0
  69. package/dist/session-queue-commands.d.ts +41 -4
  70. package/dist/session-realtime-ledger.d.ts +2 -0
  71. package/dist/session-tenancy.d.ts +71 -1
  72. package/dist/session-tenancy.js +25 -3
  73. package/dist/session-tool-call-settlement.d.ts +9 -4
  74. package/dist/slack-routing-personal-workspace.d.ts +23 -0
  75. package/dist/slack-routing.d.ts +153 -0
  76. package/dist/slack-task-policy-schema.d.ts +34 -0
  77. package/dist/slack-task-policy.d.ts +8 -0
  78. package/dist/slack-user-link-access.d.ts +1 -0
  79. package/dist/task-notes-schema.d.ts +594 -0
  80. package/dist/task-notes.d.ts +39 -0
  81. package/dist/turn-initiator.d.ts +2 -0
  82. package/dist/user-resource-authority.d.ts +109 -0
  83. package/dist/video-generation.js +3 -3
  84. package/dist/workspace-authority.d.ts +81 -0
  85. package/dist/workspace-instruction-policies-schema.d.ts +279 -0
  86. package/dist/workspace-instruction-policies.d.ts +62 -0
  87. package/dist/workspace-learning-policy.d.ts +1 -0
  88. package/dist/workspace-membership-permissions.d.ts +2 -0
  89. package/drizzle/0240_enrollment_connection_authority.sql +39 -0
  90. package/drizzle/0241_atomic_personal_resource_delegation.sql +1000 -0
  91. package/drizzle/0241_enrollment_agent_runtime.sql +70 -0
  92. package/drizzle/0244_slack_app_home_refresh_queue.sql +120 -0
  93. package/drizzle/0245_model_context_contribution_facts.sql +88 -0
  94. package/drizzle/0246_integration_personal_instance_authority.sql +70 -0
  95. package/drizzle/0247_terraform_stacks_provenance_repair.sql +428 -0
  96. package/drizzle/0248_terraform_stacks_component_resolution_fence.sql +83 -0
  97. package/drizzle/0249_personal_resource_delegation_authority_correction.sql +662 -0
  98. package/drizzle/0250_direct_retained_process_owner_liveness.sql +199 -0
  99. package/drizzle/0251_connected_machine_operation_policy.sql +45 -0
  100. package/drizzle/0252_scheduled_personal_resource_delegation.sql +1867 -0
  101. package/drizzle/0253_common_user_resource_authority_lifecycle.sql +1555 -0
  102. package/drizzle/0254_scoped_variable_set_authority.sql +1255 -0
  103. package/drizzle/0255_company_brain_governed_write_proposals.sql +524 -0
  104. package/drizzle/0256_connection_authority_delegation.sql +703 -0
  105. package/drizzle/0257_goal_revision_decisions_and_root_constraints.sql +436 -0
  106. package/drizzle/0258_three_scope_document_knowledge_authority.sql +1273 -0
  107. package/drizzle/0259_company_brain_context_selection_receipts.sql +869 -0
  108. package/drizzle/0260_task_note_knowledge_promotion.sql +820 -0
  109. package/drizzle/0261_preference_knowledge_proposal_actor_binding.sql +471 -0
  110. package/drizzle/0262_scoped_connected_machines_and_rigs.sql +1700 -0
  111. package/drizzle/0263_organization_membership_lifecycle.sql +3284 -0
  112. package/drizzle/0264_connection_authority_runtime_activation.sql +1084 -0
  113. package/drizzle/0266_company_brain_context_receipt_inspection.sql +156 -0
  114. package/drizzle/0268_governed_learning_decision_receipts.sql +825 -0
  115. package/drizzle/0269_governed_learning_activation_controller.sql +1874 -0
  116. package/drizzle/0270_governed_learning_history_inspection.sql +170 -0
  117. package/drizzle/0271_company_brain_retrieval_only_default.sql +524 -0
  118. package/drizzle/0272_human_confirmed_learning_activation.sql +591 -0
  119. package/drizzle/0273_scheduled_variable_set_materialization.sql +268 -0
  120. package/drizzle/0274_human_confirmed_knowledge_review.sql +310 -0
  121. package/drizzle/0275_scheduled_connection_authority.sql +5527 -0
  122. package/drizzle/0276_onboarding_proposal_initiating_human_guc.sql +198 -0
  123. package/drizzle/0277_workspace_writer_authority_attribution.sql +393 -0
  124. package/drizzle/0278_workspace_membership_removal_fencing.sql +1174 -0
  125. package/drizzle/0279_workspace_connection_use_lane.sql +343 -0
  126. package/drizzle/0280_connection_and_variable_set_audit_attribution.sql +773 -0
  127. package/drizzle/0281_viewer_holder_authority_claims.sql +52 -0
  128. package/drizzle/0282_variable_set_session_attach_attribution.sql +178 -0
  129. package/drizzle/0283_editable_spreadsheet_authored_state.sql +82 -0
  130. package/drizzle/0284_truthful_human_confirmed_review_reason.sql +930 -0
  131. package/drizzle/0285_organization_tenancy_inventory.sql +329 -0
  132. package/drizzle/0286_widen_task_note_expiry_ceiling.sql +391 -0
  133. package/drizzle/0287_open_suffix_pending_tool_calls.sql +18 -0
  134. package/drizzle/0288_attached_browser_reenrollment.sql +64 -0
  135. package/drizzle/0289_session_composer_policy_authority.sql +597 -0
  136. package/drizzle/0290_organization_membership_backfill.sql +268 -0
  137. package/drizzle/0291_resource_authority_classification_assertion.sql +371 -0
  138. package/drizzle/0292_truthful_tenancy_inventory_counters.sql +291 -0
  139. package/drizzle/0293_confirm_time_rule_rebaseline.sql +615 -0
  140. package/drizzle/0294_preference_activation_authority.sql +237 -0
  141. package/drizzle/0295_retire_legacy_standing_memory_mode.sql +476 -0
  142. package/drizzle/0296_force_rls_backfill_noop_repair.sql +215 -0
  143. package/drizzle/0297_session_ownership_classification_and_backfill.sql +752 -0
  144. package/drizzle/0298_organization_tenancy_parity.sql +609 -0
  145. package/drizzle/0299_organization_membership_lock_order.sql +291 -0
  146. package/drizzle/0300_tenancy_backfill_ledger.sql +459 -0
  147. package/drizzle/0301_session_snapshot_and_pin_visibility.sql +181 -0
  148. package/drizzle/0302_personal_workspace_session_ownership.sql +303 -0
  149. package/drizzle/0303_session_tenancy_product_activation.sql +919 -0
  150. package/drizzle/0304_personal_workspace_private_session_reads.sql +97 -0
  151. package/drizzle/0305_personal_resource_grant_management.sql +584 -0
  152. package/drizzle/0306_atomic_personal_resource_attachments.sql +1374 -0
  153. package/drizzle/0307_session_attention_state.sql +51 -0
  154. package/drizzle/0308_session_archives.sql +35 -0
  155. package/drizzle/0309_channel_project_pins.sql +6 -0
  156. package/drizzle/0310_channel_project_order.sql +27 -0
  157. package/drizzle/0311_company_scope_and_private_session_create.sql +656 -0
  158. package/drizzle/0312_quiescent_session_tree_deletion.sql +39 -0
  159. package/drizzle/0313_private_child_session_authority.sql +365 -0
  160. package/drizzle/0314_unregistered_organization_invitations.sql +606 -0
  161. package/drizzle/0315_personal_github_repository_selection.sql +624 -0
  162. package/drizzle/0316_human_confirmed_activation_resumed_generation.sql +869 -0
  163. package/drizzle/0317_session_goal_continuation_hold.sql +64 -0
  164. package/drizzle/0318_session_turn_startup_milestones.sql +115 -0
  165. package/drizzle/0319_event_triggered_automations.sql +254 -0
  166. package/drizzle/0320_pr_review_pack.sql +113 -0
  167. package/drizzle/0321_slack_bot_environment_display_name.sql +15 -0
  168. package/drizzle/0322_session_turns_unclaimed_prompt_trigger_index.sql +5 -0
  169. package/drizzle/0323_organization_private_session_enablement.sql +489 -0
  170. package/drizzle/0324_human_confirmed_company_profile_agent_admin.sql +856 -0
  171. package/drizzle/0325_child_lifecycle_notices.sql +44 -0
  172. package/drizzle/0326_interaction_operation_error_codes.sql +62 -0
  173. package/drizzle/0327_slack_first_task_hint.sql +29 -0
  174. package/drizzle/0328_session_background_commands.sql +590 -0
  175. package/drizzle/0329_slack_orchestration_delivery_events.sql +91 -0
  176. package/drizzle/0330_api_key_descriptions.sql +20 -0
  177. package/drizzle/0331_managed_organization_creation.sql +163 -0
  178. package/drizzle/0332_organization_shared_workspace_control_plane.sql +539 -0
  179. package/drizzle/0333_session_turn_prompt_routing.sql +45 -0
  180. package/drizzle/0334_connected_machine_workspace_root.sql +99 -0
  181. package/drizzle/0335_slack_workspace_routing.sql +460 -0
  182. package/drizzle/0336_atomic_session_fork_visibility.sql +667 -0
  183. package/drizzle/0337_slack_routed_action_handles.sql +189 -0
  184. package/drizzle/0338_atomic_connected_machine_attachments.sql +410 -0
  185. package/drizzle/0339_document_authority_reclassification.sql +1153 -0
  186. package/drizzle/0340_tenancy_backfill_activation_evidence.sql +1347 -0
  187. package/drizzle/0341_slack_routing_probe_organization_fence.sql +109 -0
  188. package/package.json +10 -6
  189. package/src/attached-browser-devices.ts +233 -5
  190. package/src/automations.ts +911 -0
  191. package/src/browser-sessions.ts +10 -0
  192. package/src/capability-integrations.ts +175 -1
  193. package/src/child-lifecycle-notices.ts +241 -0
  194. package/src/codemode-operations.ts +1 -35
  195. package/src/company-brain-context-selection-schema.ts +100 -0
  196. package/src/company-brain-context-selection.ts +359 -0
  197. package/src/company-brain-governed-writes.ts +1170 -0
  198. package/src/company-brain.ts +309 -0
  199. package/src/company-profile-agent-admin.ts +225 -0
  200. package/src/company-profile-schema.ts +121 -1
  201. package/src/company-profile.ts +21 -0
  202. package/src/computer-sessions.ts +10 -1
  203. package/src/connection-authority.ts +290 -0
  204. package/src/connection-scopes.ts +20 -0
  205. package/src/connection-tenancy-backfill.ts +81 -0
  206. package/src/connection-token-resolver.ts +263 -31
  207. package/src/database.ts +75 -2
  208. package/src/editable-artifacts-schema.ts +10 -5
  209. package/src/editable-artifacts.ts +21 -11
  210. package/src/governed-learning-activation-schema.ts +121 -0
  211. package/src/governed-learning-activation.ts +588 -0
  212. package/src/governed-learning-evaluator-schema.ts +72 -0
  213. package/src/governed-learning-evaluator.ts +217 -0
  214. package/src/index.ts +11651 -1548
  215. package/src/insights.ts +107 -0
  216. package/src/integration-bindings.ts +88 -6
  217. package/src/integration-facets.ts +287 -84
  218. package/src/interaction-placement-loss.ts +234 -0
  219. package/src/managed-human-provisioning.ts +116 -0
  220. package/src/memory-domain.ts +27 -32
  221. package/src/migrate.ts +77 -1
  222. package/src/new-session-drafts.ts +229 -35
  223. package/src/organization-membership-backfill.ts +735 -0
  224. package/src/organization-membership-lifecycle.ts +985 -0
  225. package/src/pr-review.ts +890 -0
  226. package/src/preference-registry.ts +31 -1
  227. package/src/provision-roles.ts +877 -6
  228. package/src/runtime-posture-cli.ts +2 -0
  229. package/src/runtime-posture.ts +1291 -76
  230. package/src/schema.ts +3303 -428
  231. package/src/scoped-knowledge-schema.ts +16 -8
  232. package/src/scoped-knowledge.ts +1 -1
  233. package/src/session-background-commands.ts +759 -0
  234. package/src/session-control.ts +640 -74
  235. package/src/session-goal-pacing.ts +126 -0
  236. package/src/session-queue-commands.ts +437 -84
  237. package/src/session-realtime-context.ts +3 -3
  238. package/src/session-realtime-ledger.ts +22 -11
  239. package/src/session-realtime-mirror.ts +1 -1
  240. package/src/session-tenancy.ts +403 -49
  241. package/src/session-tool-call-settlement.ts +38 -29
  242. package/src/slack-routing-personal-workspace.ts +67 -0
  243. package/src/slack-routing.ts +469 -0
  244. package/src/slack-task-policy-schema.ts +5 -0
  245. package/src/slack-task-policy.ts +18 -0
  246. package/src/slack-user-link-access.ts +17 -1
  247. package/src/task-notes-schema.ts +74 -0
  248. package/src/task-notes.ts +124 -2
  249. package/src/turn-initiator.ts +8 -0
  250. package/src/user-resource-authority.ts +377 -0
  251. package/src/workspace-authority.ts +160 -0
  252. package/src/workspace-instruction-policies-schema.ts +53 -1
  253. package/src/workspace-instruction-policies.ts +528 -77
  254. package/src/workspace-learning-policy.ts +21 -5
  255. package/src/workspace-membership-permissions.ts +9 -0
  256. package/dist/chunk-4SR4HKKA.js.map +0 -1
  257. package/dist/chunk-6ZYKCRFO.js.map +0 -1
  258. package/dist/chunk-NXAFLCQX.js +0 -2468
  259. package/dist/chunk-NXAFLCQX.js.map +0 -1
  260. package/dist/chunk-SK7YYHBI.js.map +0 -1
  261. package/dist/chunk-WD5ROSLL.js +0 -117
  262. package/dist/chunk-WD5ROSLL.js.map +0 -1
  263. package/dist/chunk-ZWREYEWA.js.map +0 -1
  264. /package/dist/{chunk-LTG3S6ZY.js.map → chunk-EULAN2NP.js.map} +0 -0
  265. /package/dist/{chunk-CUPM3FGB.js.map → chunk-NJVZPWXY.js.map} +0 -0
@@ -0,0 +1,268 @@
1
+ -- deployment-mode: rolling
2
+ -- Migration 0290: read-only organization-membership backfill enumeration seam.
3
+ --
4
+ -- Phase D of docs/organization-tenancy.md requires provisioning personal
5
+ -- workspaces for active memberships "through an idempotent lifecycle
6
+ -- operation". That lifecycle operation already exists - 0219's
7
+ -- ensure_managed_human_personal_workspace(uuid, text, uuid) - and 0263's
8
+ -- membership lifecycle owns invitation-driven provisioning. What is missing is
9
+ -- a way for an operator to FIND the humans that need it: the population the
10
+ -- 0285 inventory counts as workspaceMemberSubjectsWithoutMembershipAnchor
11
+ -- (humans who held workspace access before 0219 and never re-authenticated
12
+ -- afterwards, so nothing ever ran the provisioning hook for them) plus any
13
+ -- membership that carries no personal workspace.
14
+ --
15
+ -- This migration therefore adds NO tables, NO write path, and NO new authority.
16
+ -- It adds exactly two read-only SECURITY DEFINER enumerations over
17
+ -- organization_memberships, which is FORCE ROW LEVEL SECURITY with zero direct
18
+ -- application-role privileges. Every other table the backfill driver needs
19
+ -- (workspaces, workspace_memberships, managed_accounts, auth_users) is read by
20
+ -- the ordinary application role under its normal account RLS, exactly like the
21
+ -- existing managed-human provisioning hook - so this seam deliberately does not
22
+ -- need 0285's capability-row pattern, and cannot silently mis-read under a
23
+ -- non-BYPASSRLS function owner.
24
+ --
25
+ -- Visibility is granted through the established transaction-local lifecycle
26
+ -- marker rather than a widened policy, and the new marker is added to the
27
+ -- organization_memberships policy's USING clause ONLY. It is structurally
28
+ -- incapable of authorizing a write: WITH CHECK keeps exactly the three
29
+ -- pre-existing write markers.
30
+
31
+ SET LOCAL lock_timeout = '5s';
32
+ SET LOCAL statement_timeout = '5min';
33
+
34
+ -- Read-only marker. USING gains 'organization_membership_backfill'; WITH CHECK
35
+ -- deliberately does not, so the backfill enumeration can never write here.
36
+ DROP POLICY IF EXISTS organization_tenancy_lifecycle ON "organization_memberships";
37
+ CREATE POLICY organization_tenancy_lifecycle ON "organization_memberships"
38
+ USING (current_setting('opengeni.organization_tenancy_lifecycle', true)
39
+ IN (
40
+ 'managed_human_provisioning',
41
+ 'session_visibility_activation',
42
+ 'organization_membership_lifecycle',
43
+ 'organization_membership_backfill'
44
+ ))
45
+ WITH CHECK (current_setting('opengeni.organization_tenancy_lifecycle', true)
46
+ IN (
47
+ 'managed_human_provisioning',
48
+ 'session_visibility_activation',
49
+ 'organization_membership_lifecycle'
50
+ ));
51
+
52
+ -- Anchor state for an explicit, bounded set of subjects. The driver already
53
+ -- knows which subjects hold workspace access (an ordinary application-role
54
+ -- read); this answers only "does an organization membership anchor exist, and
55
+ -- what does it point at". It never enumerates subjects on its own, so it
56
+ -- cannot become a cross-organization membership discovery path.
57
+ CREATE OR REPLACE FUNCTION list_organization_membership_backfill_anchors(
58
+ p_organization_id uuid,
59
+ p_subject_ids text[]
60
+ ) RETURNS jsonb
61
+ LANGUAGE plpgsql
62
+ SECURITY DEFINER
63
+ SET search_path FROM CURRENT
64
+ SET statement_timeout = '30s'
65
+ AS $body$
66
+ DECLARE
67
+ result jsonb;
68
+ subject_id_value text;
69
+ previous_marker text := pg_catalog.current_setting(
70
+ 'opengeni.organization_tenancy_lifecycle', true
71
+ );
72
+ BEGIN
73
+ IF p_organization_id IS NULL THEN
74
+ RAISE EXCEPTION 'membership backfill requires an organization id'
75
+ USING ERRCODE = '22004';
76
+ END IF;
77
+ IF p_organization_id IS DISTINCT FROM nullif(
78
+ pg_catalog.current_setting('opengeni.account_id', true), ''
79
+ )::uuid
80
+ THEN
81
+ RAISE EXCEPTION 'membership backfill scope mismatch' USING ERRCODE = '42501';
82
+ END IF;
83
+ IF p_subject_ids IS NULL
84
+ OR pg_catalog.array_length(p_subject_ids, 1) IS NULL
85
+ OR pg_catalog.array_length(p_subject_ids, 1) > 500
86
+ THEN
87
+ RAISE EXCEPTION 'membership backfill requires 1..500 subjects'
88
+ USING ERRCODE = '22023';
89
+ END IF;
90
+ FOREACH subject_id_value IN ARRAY p_subject_ids LOOP
91
+ IF subject_id_value IS NULL
92
+ OR subject_id_value <> pg_catalog.btrim(subject_id_value)
93
+ OR pg_catalog.length(subject_id_value) NOT BETWEEN 1 AND 1024
94
+ OR subject_id_value NOT LIKE 'user:%'
95
+ THEN
96
+ RAISE EXCEPTION 'membership backfill subject is invalid'
97
+ USING ERRCODE = '22023';
98
+ END IF;
99
+ END LOOP;
100
+
101
+ PERFORM pg_catalog.set_config(
102
+ 'opengeni.organization_tenancy_lifecycle',
103
+ 'organization_membership_backfill',
104
+ true
105
+ );
106
+ SELECT COALESCE(pg_catalog.jsonb_agg(pg_catalog.jsonb_build_object(
107
+ 'subjectId', anchor.subject_id,
108
+ 'membershipId', anchor.id,
109
+ 'status', anchor.status,
110
+ 'personalWorkspaceId', anchor.personal_workspace_id
111
+ ) ORDER BY anchor.subject_id), '[]'::jsonb)
112
+ INTO result
113
+ FROM organization_memberships anchor
114
+ WHERE anchor.account_id = p_organization_id
115
+ AND anchor.subject_id = ANY (p_subject_ids);
116
+
117
+ PERFORM pg_catalog.set_config(
118
+ 'opengeni.organization_tenancy_lifecycle',
119
+ COALESCE(previous_marker, ''),
120
+ true
121
+ );
122
+ RETURN result;
123
+ EXCEPTION WHEN OTHERS THEN
124
+ PERFORM pg_catalog.set_config(
125
+ 'opengeni.organization_tenancy_lifecycle',
126
+ COALESCE(previous_marker, ''),
127
+ true
128
+ );
129
+ RAISE;
130
+ END
131
+ $body$;
132
+
133
+ -- Memberships that exist but carry no personal workspace. 0285 counts these as
134
+ -- organizationMemberships.activeWithoutPersonalWorkspace; this returns the
135
+ -- exact bounded, deterministically ordered subjects behind that count so the
136
+ -- driver can converge them through the same lifecycle seam.
137
+ --
138
+ -- p_after_subject_id is the resumable keyset cursor. Without it a bounded pass
139
+ -- would return the same first p_limit subjects forever, so an organization
140
+ -- whose first p_limit rows cannot be resolved from deterministic evidence
141
+ -- (they stay in this population permanently) would starve every subject behind
142
+ -- them. The cursor is strictly exclusive and the ordering is the same
143
+ -- subject_id ordering the driver merges on, so repeated passes walk the whole
144
+ -- population exactly once instead of re-reading one window.
145
+ CREATE OR REPLACE FUNCTION list_organization_memberships_without_personal_workspace(
146
+ p_organization_id uuid,
147
+ p_limit integer DEFAULT 25,
148
+ p_after_subject_id text DEFAULT NULL
149
+ ) RETURNS jsonb
150
+ LANGUAGE plpgsql
151
+ SECURITY DEFINER
152
+ SET search_path FROM CURRENT
153
+ SET statement_timeout = '30s'
154
+ AS $body$
155
+ DECLARE
156
+ result jsonb;
157
+ previous_marker text := pg_catalog.current_setting(
158
+ 'opengeni.organization_tenancy_lifecycle', true
159
+ );
160
+ BEGIN
161
+ IF p_organization_id IS NULL THEN
162
+ RAISE EXCEPTION 'membership backfill requires an organization id'
163
+ USING ERRCODE = '22004';
164
+ END IF;
165
+ IF p_organization_id IS DISTINCT FROM nullif(
166
+ pg_catalog.current_setting('opengeni.account_id', true), ''
167
+ )::uuid
168
+ THEN
169
+ RAISE EXCEPTION 'membership backfill scope mismatch' USING ERRCODE = '42501';
170
+ END IF;
171
+ IF p_limit IS NULL OR p_limit NOT BETWEEN 1 AND 100 THEN
172
+ RAISE EXCEPTION 'membership backfill limit must be 1..100'
173
+ USING ERRCODE = '22023';
174
+ END IF;
175
+ IF p_after_subject_id IS NOT NULL
176
+ AND (
177
+ p_after_subject_id <> pg_catalog.btrim(p_after_subject_id)
178
+ OR pg_catalog.length(p_after_subject_id) NOT BETWEEN 1 AND 1024
179
+ OR p_after_subject_id NOT LIKE 'user:%'
180
+ )
181
+ THEN
182
+ RAISE EXCEPTION 'membership backfill cursor is invalid'
183
+ USING ERRCODE = '22023';
184
+ END IF;
185
+
186
+ PERFORM pg_catalog.set_config(
187
+ 'opengeni.organization_tenancy_lifecycle',
188
+ 'organization_membership_backfill',
189
+ true
190
+ );
191
+ SELECT COALESCE(pg_catalog.jsonb_agg(pg_catalog.jsonb_build_object(
192
+ 'subjectId', pending.subject_id,
193
+ 'membershipId', pending.id,
194
+ 'status', pending.status,
195
+ 'personalWorkspaceId', NULL
196
+ ) ORDER BY pending.subject_id COLLATE "C"), '[]'::jsonb)
197
+ INTO result
198
+ FROM (
199
+ -- `COLLATE "C"` is load-bearing, not cosmetic. The driver merges this page
200
+ -- with its own workspace-access page in JavaScript and re-sorts the union
201
+ -- with the default comparator, which is code-unit (byte) order. The
202
+ -- database's default collation is locale-aware (`en_US.utf8` in practice),
203
+ -- where `_` is ignorable and case is secondary, so `user:Alpha_two` sorts
204
+ -- BEFORE `user:a_b` in JavaScript and AFTER it in the database. A keyset
205
+ -- cursor taken from the JavaScript order would then skip every subject the
206
+ -- database considers already behind it - a silent, permanent drop that
207
+ -- still reports `drained: true`. Pinning both sides to `C` makes the two
208
+ -- orders identical.
209
+ SELECT candidate.id, candidate.subject_id, candidate.status
210
+ FROM organization_memberships candidate
211
+ WHERE candidate.account_id = p_organization_id
212
+ AND candidate.personal_workspace_id IS NULL
213
+ AND candidate.status IN ('active', 'provisioning')
214
+ AND (
215
+ p_after_subject_id IS NULL
216
+ OR candidate.subject_id COLLATE "C" > p_after_subject_id COLLATE "C"
217
+ )
218
+ ORDER BY candidate.subject_id COLLATE "C"
219
+ LIMIT p_limit
220
+ ) pending;
221
+
222
+ PERFORM pg_catalog.set_config(
223
+ 'opengeni.organization_tenancy_lifecycle',
224
+ COALESCE(previous_marker, ''),
225
+ true
226
+ );
227
+ RETURN result;
228
+ EXCEPTION WHEN OTHERS THEN
229
+ PERFORM pg_catalog.set_config(
230
+ 'opengeni.organization_tenancy_lifecycle',
231
+ COALESCE(previous_marker, ''),
232
+ true
233
+ );
234
+ RAISE;
235
+ END
236
+ $body$;
237
+
238
+ DO $membership_backfill_search_path$
239
+ DECLARE
240
+ data_schema text := current_schema();
241
+ BEGIN
242
+ EXECUTE format(
243
+ 'ALTER FUNCTION %I.list_organization_membership_backfill_anchors(uuid,text[]) '
244
+ || 'SET search_path = pg_catalog, %I, pg_temp',
245
+ data_schema, data_schema
246
+ );
247
+ EXECUTE format(
248
+ 'ALTER FUNCTION %I.list_organization_memberships_without_personal_workspace(uuid,integer,text) '
249
+ || 'SET search_path = pg_catalog, %I, pg_temp',
250
+ data_schema, data_schema
251
+ );
252
+ END
253
+ $membership_backfill_search_path$;
254
+
255
+ REVOKE ALL ON FUNCTION
256
+ list_organization_membership_backfill_anchors(uuid, text[]) FROM PUBLIC;
257
+ REVOKE ALL ON FUNCTION
258
+ list_organization_memberships_without_personal_workspace(uuid, integer, text) FROM PUBLIC;
259
+ DO $membership_backfill_grant$
260
+ BEGIN
261
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
262
+ GRANT EXECUTE ON FUNCTION
263
+ list_organization_membership_backfill_anchors(uuid, text[]) TO opengeni_app;
264
+ GRANT EXECUTE ON FUNCTION
265
+ list_organization_memberships_without_personal_workspace(uuid, integer, text) TO opengeni_app;
266
+ END IF;
267
+ END
268
+ $membership_backfill_grant$;
@@ -0,0 +1,371 @@
1
+ -- deployment-mode: rolling
2
+ -- Migration 0291: explicit resource-classification assertion and receipt seam
3
+ -- for Variable Sets, Rigs, and Connected Machines (organization-tenancy phase D, slices 4-5).
4
+ --
5
+ -- WHAT PHASE D ASKS FOR, AND WHAT THE SCHEMA ALREADY GUARANTEES
6
+ -- ------------------------------------------------------------
7
+ -- `docs/organization-tenancy.md` phase D requires that every existing resource
8
+ -- be classified explicitly as workspace-owned unless there is reviewed,
9
+ -- deterministic evidence for user ownership, and that the run "record backfill
10
+ -- receipts and unresolved rows without widening access".
11
+ --
12
+ -- For these three families the classification is ALREADY terminal for every
13
+ -- row, and no data rewrite is possible without inventing a signal phase D
14
+ -- forbids:
15
+ --
16
+ -- * `authority_scope` is `text NOT NULL DEFAULT 'workspace'` on all three
17
+ -- tables (0230 for variable sets and rigs, 0262 for enrollments), so every
18
+ -- pre-existing row carries an explicit workspace classification already.
19
+ -- * Each `*_authority_shape_check` REQUIRES `authority_id IS NULL` and
20
+ -- `owner_organization_membership_id IS NULL` for organization/workspace
21
+ -- scope, and all three were `VALIDATE`d at creation. PostgreSQL has
22
+ -- therefore already proven the shape of every row.
23
+ -- * A legacy unmigrated row and a deliberately workspace-scoped row are
24
+ -- consequently BYTE-IDENTICAL. There is no discriminator, and 0262's own
25
+ -- header records the reviewed decision: "Existing rows remain
26
+ -- workspace-owned."
27
+ -- * `origin_workspace_id` does not rescue that: it is provenance, not
28
+ -- classification. No shape check constrains it for these three families
29
+ -- (unlike `connections`, whose 0256 workspace branch requires
30
+ -- `origin_workspace_id = workspace_id`), every read of it is gated behind
31
+ -- `authority_scope = 'user'` (0241/0252/0253), and the user lane is always
32
+ -- populated by the lifecycle functions. Its polarity is also inconsistent
33
+ -- across the three families, so it carries no usable signal either way.
34
+ --
35
+ -- Migration 0256 is the one sibling family with a genuine discriminator -
36
+ -- `connections.subject_id` plus an active `organization_memberships` row - so
37
+ -- it could deterministically mint user authority. None of these three tables
38
+ -- has a `subject_id`, and phase D explicitly forbids substituting `created_by`,
39
+ -- connection attribution, a default workspace, a resource name, or current
40
+ -- access. This migration therefore rewrites NOTHING and asserts instead.
41
+ --
42
+ -- WHY THIS IS A SECURITY DEFINER SEAM AND NOT A MIGRATION-TIME `UPDATE`
43
+ -- --------------------------------------------------------------------
44
+ -- All three tables are FORCE ROW LEVEL SECURITY and their ordinary policy is
45
+ -- `workspace_rls_visible(account_id, workspace_id)`, which is false whenever
46
+ -- the `opengeni.workspace_id` GUC is unset - as it is during migration. FORCE
47
+ -- RLS applies to the table owner, and OpenGeni's documented deployment posture
48
+ -- (docs/deployment.md) is a NON-superuser migration principal without
49
+ -- BYPASSRLS. A bare `UPDATE ... WHERE ...` in a migration body therefore
50
+ -- matches zero rows and reports success on such a deployment; it only appears
51
+ -- to work in the test harness, which migrates as a superuser for whom FORCE
52
+ -- RLS never engages. Any classification work for these families has to run
53
+ -- behind a capability-claiming definer seam, which is exactly why 0254, 0262,
54
+ -- and 0285 are all shaped this way.
55
+ --
56
+ -- WHAT THIS SEAM ADDS OVER 0285's INVENTORY
57
+ -- -----------------------------------------
58
+ -- 0285 counts rows by `authority_scope`. It cannot say whether a row that
59
+ -- CLAIMS user ownership actually holds it, because no constraint ties a
60
+ -- resource's `authority_id` to an authority row of the matching
61
+ -- `resource_kind`/`resource_id`, nor to a live authority or a live
62
+ -- organization membership. Those are the only genuinely unenforced parts of
63
+ -- the classification, and they are what this seam proves per row. Anything it
64
+ -- cannot prove is recorded as an unresolved obligation through migration
65
+ -- 0286's ledger - never guessed, never rewritten.
66
+ --
67
+ -- The seam is read-only over every resource table: its sole writes are its own
68
+ -- transaction-scoped capability row and, when a run key is supplied, the 0286
69
+ -- ledger. Receipt recording is guarded by `to_regprocedure` so this migration
70
+ -- is order-independent with respect to the ledger migration and reports
71
+ -- `ledgerAvailable` honestly rather than failing or silently skipping.
72
+
73
+ SET LOCAL lock_timeout = '5s';
74
+ SET LOCAL statement_timeout = '5min';
75
+
76
+ -- Freeze the active data schema into the SECURITY DEFINER routines below.
77
+ -- Standalone installs resolve to public; embedded installs resolve to their
78
+ -- dedicated schema. pg_temp remains explicitly last.
79
+ SELECT pg_catalog.set_config(
80
+ 'search_path',
81
+ pg_catalog.format('%I, pg_catalog, pg_temp', pg_catalog.current_schema()),
82
+ true
83
+ );
84
+
85
+ -- A transaction-scoped, migration-owner-only capability. Deliberately NOT a
86
+ -- reuse of 0285's inventory capability: that policy pins the literal
87
+ -- `current_user` observed while 0285 ran, so reusing it would silently fail on
88
+ -- any deployment whose migration principal changed between the two migrations,
89
+ -- and it carries no policy for `organization_user_resource_authorities`, which
90
+ -- this seam must join.
91
+ CREATE TABLE opengeni_private.resource_classification_capabilities (
92
+ "backend_pid" integer NOT NULL,
93
+ "transaction_id" xid8 NOT NULL,
94
+ CONSTRAINT "resource_classification_capabilities_pk" PRIMARY KEY (
95
+ "backend_pid", "transaction_id"
96
+ )
97
+ );
98
+ REVOKE ALL ON TABLE opengeni_private.resource_classification_capabilities FROM PUBLIC;
99
+
100
+ CREATE OR REPLACE FUNCTION opengeni_private.resource_classification_capability_active()
101
+ RETURNS boolean
102
+ LANGUAGE sql
103
+ STABLE
104
+ SECURITY DEFINER
105
+ SET search_path = pg_catalog
106
+ AS $capability_active$
107
+ SELECT EXISTS (
108
+ SELECT 1
109
+ FROM opengeni_private.resource_classification_capabilities capability
110
+ WHERE capability.backend_pid = pg_catalog.pg_backend_pid()
111
+ AND capability.transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
112
+ )
113
+ $capability_active$;
114
+ REVOKE ALL ON FUNCTION
115
+ opengeni_private.resource_classification_capability_active() FROM PUBLIC;
116
+
117
+ DO $resource_classification_capability_policies$
118
+ DECLARE
119
+ data_schema text := current_schema();
120
+ migration_owner text := current_user;
121
+ table_name text;
122
+ BEGIN
123
+ FOREACH table_name IN ARRAY ARRAY[
124
+ 'workspace_variable_sets',
125
+ 'rigs',
126
+ 'enrollments',
127
+ 'organization_user_resource_authorities',
128
+ 'organization_memberships'
129
+ ] LOOP
130
+ EXECUTE format(
131
+ 'CREATE POLICY resource_classification_capability_read ON %I.%I '
132
+ || 'FOR SELECT USING (current_user = %L AND '
133
+ || 'opengeni_private.resource_classification_capability_active())',
134
+ data_schema,
135
+ table_name,
136
+ migration_owner
137
+ );
138
+ END LOOP;
139
+ END
140
+ $resource_classification_capability_policies$;
141
+
142
+ -- Asserts the explicit authority classification of every Variable Set, Rig,
143
+ -- and Connected Machine in one organization.
144
+ --
145
+ -- `p_run_key` is the durability switch. Omitted (NULL) the call is a pure
146
+ -- read. Supplied, the same verdicts are additionally recorded through the 0286
147
+ -- ledger as one receipt per family plus one append-only unresolved row per
148
+ -- resource that could not be proven - which is also why the run key must be
149
+ -- fresh per run: the ledger refuses to re-open a settled receipt.
150
+ CREATE OR REPLACE FUNCTION verify_organization_resource_classification(
151
+ p_organization_id uuid,
152
+ p_run_key text DEFAULT NULL
153
+ ) RETURNS jsonb
154
+ LANGUAGE plpgsql
155
+ SECURITY DEFINER
156
+ SET search_path FROM CURRENT
157
+ SET statement_timeout = '5min'
158
+ AS $$
159
+ DECLARE
160
+ -- family label (0286 vocabulary), resource table, authority resource_kind
161
+ families constant text[][] := ARRAY[
162
+ ARRAY['variable_sets', 'workspace_variable_sets', 'variable_set'],
163
+ ARRAY['rigs', 'rigs', 'rig'],
164
+ ARRAY['machines', 'enrollments', 'connected_machine']
165
+ ];
166
+ family text[];
167
+ claimed_rows integer := 0;
168
+ claimed boolean := false;
169
+ ledger_available boolean;
170
+ classification_sql text;
171
+ family_result jsonb;
172
+ families_result jsonb := '{}'::jsonb;
173
+ receipt_id uuid;
174
+ unresolved_row record;
175
+ BEGIN
176
+ IF p_organization_id IS NULL THEN
177
+ RAISE EXCEPTION 'resource classification requires an organization id'
178
+ USING ERRCODE = '22004';
179
+ END IF;
180
+ IF p_organization_id IS DISTINCT FROM nullif(
181
+ pg_catalog.current_setting('opengeni.account_id', true), ''
182
+ )::uuid
183
+ THEN
184
+ RAISE EXCEPTION 'resource classification scope mismatch'
185
+ USING ERRCODE = '42501';
186
+ END IF;
187
+ IF p_run_key IS NOT NULL AND length(btrim(p_run_key)) = 0 THEN
188
+ RAISE EXCEPTION 'resource classification run key must not be blank'
189
+ USING ERRCODE = '22023';
190
+ END IF;
191
+
192
+ -- The ledger lives in its own migration. Probe rather than assume, so this
193
+ -- seam neither hard-fails nor silently drops the obligation when the two
194
+ -- land in either order.
195
+ ledger_available := p_run_key IS NOT NULL
196
+ AND to_regprocedure('open_tenancy_backfill_receipt(uuid, text, text)') IS NOT NULL
197
+ AND to_regprocedure('record_tenancy_backfill_unresolved(uuid, uuid, text)') IS NOT NULL
198
+ AND to_regprocedure(
199
+ 'complete_tenancy_backfill_receipt(uuid, bigint, bigint, text)'
200
+ ) IS NOT NULL;
201
+
202
+ INSERT INTO opengeni_private.resource_classification_capabilities (
203
+ backend_pid, transaction_id
204
+ ) VALUES (pg_catalog.pg_backend_pid(), pg_catalog.pg_current_xact_id())
205
+ ON CONFLICT DO NOTHING;
206
+ -- Only release what this call claimed, so a nested or enclosing holder of
207
+ -- the same transaction-scoped capability is never revoked underneath.
208
+ GET DIAGNOSTICS claimed_rows = ROW_COUNT;
209
+ claimed := claimed_rows > 0;
210
+
211
+ FOREACH family SLICE 1 IN ARRAY families LOOP
212
+ -- One verdict per row. The first two branches are the phase D terminal
213
+ -- states; every other branch is an explicit refusal to classify, carrying
214
+ -- a fixed 0286 reason code and never a proposed owner.
215
+ classification_sql := format($classification$
216
+ SELECT resource.id AS resource_id,
217
+ CASE
218
+ WHEN resource.authority_scope = 'workspace'
219
+ AND resource.authority_id IS NULL
220
+ AND resource.owner_organization_membership_id IS NULL
221
+ THEN 'workspace_owned'
222
+ -- Organization scope is a deliberate lifecycle choice (0254/0262),
223
+ -- never a legacy default, so it is reported separately rather than
224
+ -- folded into the workspace population it is not part of.
225
+ WHEN resource.authority_scope = 'organization'
226
+ AND resource.authority_id IS NULL
227
+ AND resource.owner_organization_membership_id IS NULL
228
+ THEN 'organization_owned'
229
+ WHEN resource.authority_scope <> 'user'
230
+ OR resource.authority_id IS NULL
231
+ OR resource.owner_organization_membership_id IS NULL
232
+ THEN 'legacy_shape_unrecognized'
233
+ WHEN authority.id IS NULL
234
+ OR authority.resource_kind <> %2$L
235
+ OR authority.resource_id <> resource.id
236
+ OR authority.organization_membership_id
237
+ <> resource.owner_organization_membership_id
238
+ THEN 'conflicting_authority_rows'
239
+ WHEN membership.id IS NULL
240
+ OR membership.status <> 'active'
241
+ OR membership.revoked_at IS NOT NULL
242
+ THEN 'missing_organization_membership'
243
+ WHEN authority.status <> 'active' OR authority.revoked_at IS NOT NULL
244
+ THEN 'ambiguous_candidate_authority'
245
+ WHEN resource.origin_workspace_id IS NULL
246
+ THEN 'no_deterministic_evidence'
247
+ ELSE 'user_owned'
248
+ END AS verdict
249
+ FROM %1$I resource
250
+ LEFT JOIN organization_user_resource_authorities authority
251
+ ON authority.id = resource.authority_id
252
+ AND authority.account_id = resource.account_id
253
+ LEFT JOIN organization_memberships membership
254
+ ON membership.id = resource.owner_organization_membership_id
255
+ AND membership.account_id = resource.account_id
256
+ WHERE resource.account_id = $1
257
+ $classification$, family[2], family[3]);
258
+
259
+ EXECUTE format($summary$
260
+ WITH classified AS (%1$s)
261
+ SELECT pg_catalog.jsonb_build_object(
262
+ 'total', count(*)::int,
263
+ 'workspaceOwned', count(*) FILTER (WHERE verdict = 'workspace_owned')::int,
264
+ 'organizationOwned', count(*) FILTER (
265
+ WHERE verdict = 'organization_owned'
266
+ )::int,
267
+ 'userOwned', count(*) FILTER (WHERE verdict = 'user_owned')::int,
268
+ 'unresolved', count(*) FILTER (
269
+ WHERE verdict NOT IN ('workspace_owned', 'organization_owned', 'user_owned')
270
+ )::int,
271
+ 'unresolvedRows', coalesce((
272
+ SELECT pg_catalog.jsonb_agg(
273
+ pg_catalog.jsonb_build_object(
274
+ 'resourceId', bounded.resource_id, 'reasonCode', bounded.verdict
275
+ ) ORDER BY bounded.resource_id
276
+ )
277
+ FROM (
278
+ SELECT resource_id, verdict FROM classified
279
+ WHERE verdict NOT IN (
280
+ 'workspace_owned', 'organization_owned', 'user_owned'
281
+ )
282
+ ORDER BY resource_id LIMIT 1000
283
+ ) bounded
284
+ ), '[]'::jsonb),
285
+ 'unresolvedTruncated', count(*) FILTER (
286
+ WHERE verdict NOT IN ('workspace_owned', 'organization_owned', 'user_owned')
287
+ ) > 1000
288
+ )
289
+ FROM classified
290
+ $summary$, classification_sql)
291
+ USING p_organization_id
292
+ INTO family_result;
293
+
294
+ IF ledger_available THEN
295
+ EXECUTE 'SELECT open_tenancy_backfill_receipt($1, $2, $3)'
296
+ USING p_organization_id, family[1], p_run_key
297
+ INTO receipt_id;
298
+ FOR unresolved_row IN EXECUTE format(
299
+ 'SELECT resource_id, verdict FROM (%1$s) classified '
300
+ || 'WHERE verdict NOT IN '
301
+ || '(''workspace_owned'', ''organization_owned'', ''user_owned'') '
302
+ || 'ORDER BY resource_id',
303
+ classification_sql
304
+ ) USING p_organization_id LOOP
305
+ EXECUTE 'SELECT record_tenancy_backfill_unresolved($1, $2, $3)'
306
+ USING receipt_id, unresolved_row.resource_id, unresolved_row.verdict;
307
+ END LOOP;
308
+ -- `classified_count` is the population this run PROVED already carries a
309
+ -- terminal non-user classification; `skipped_count` is the reviewed
310
+ -- user-owned population the backfill must not touch. Neither reflects a
311
+ -- rewrite: this run changes no resource row.
312
+ EXECUTE 'SELECT complete_tenancy_backfill_receipt($1, $2, $3, $4)'
313
+ USING receipt_id,
314
+ (family_result ->> 'workspaceOwned')::bigint
315
+ + (family_result ->> 'organizationOwned')::bigint,
316
+ (family_result ->> 'userOwned')::bigint,
317
+ 'completed';
318
+ family_result := family_result
319
+ || pg_catalog.jsonb_build_object('receiptId', receipt_id);
320
+ END IF;
321
+
322
+ families_result := families_result
323
+ || pg_catalog.jsonb_build_object(family[1], family_result);
324
+ END LOOP;
325
+
326
+ IF claimed THEN
327
+ DELETE FROM opengeni_private.resource_classification_capabilities
328
+ WHERE backend_pid = pg_catalog.pg_backend_pid()
329
+ AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned();
330
+ END IF;
331
+
332
+ RETURN pg_catalog.jsonb_build_object(
333
+ 'schemaVersion', 1,
334
+ 'organizationId', p_organization_id,
335
+ 'runKey', p_run_key,
336
+ 'ledgerAvailable', ledger_available,
337
+ 'rewroteResourceRows', false,
338
+ 'families', families_result
339
+ );
340
+ EXCEPTION WHEN OTHERS THEN
341
+ IF claimed THEN
342
+ DELETE FROM opengeni_private.resource_classification_capabilities
343
+ WHERE backend_pid = pg_catalog.pg_backend_pid()
344
+ AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned();
345
+ END IF;
346
+ RAISE;
347
+ END
348
+ $$;
349
+
350
+ REVOKE ALL ON FUNCTION
351
+ verify_organization_resource_classification(uuid, text) FROM PUBLIC;
352
+
353
+ DO $resource_classification_grants$
354
+ BEGIN
355
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
356
+ GRANT EXECUTE ON FUNCTION
357
+ verify_organization_resource_classification(uuid, text) TO opengeni_app;
358
+ -- The predicate must be independently EXECUTE-granted (0254's and 0285's
359
+ -- exact lesson): an ordinary opengeni_app read of ANY table this
360
+ -- capability protects evaluates every PERMISSIVE policy on it, so without
361
+ -- this grant an unrelated read of `rigs`, `enrollments`, or
362
+ -- `workspace_variable_sets` fails closed with a permission error instead
363
+ -- of the policy branch simply evaluating false.
364
+ GRANT EXECUTE ON FUNCTION
365
+ opengeni_private.resource_classification_capability_active() TO opengeni_app;
366
+ END IF;
367
+ END
368
+ $resource_classification_grants$;
369
+
370
+ COMMENT ON FUNCTION verify_organization_resource_classification(uuid, text) IS
371
+ 'Organization-tenancy phase D assertion seam for Variable Sets, Rigs, and Connected Machines. Read-only over every resource table: it proves each row already carries an explicit terminal authority classification and records what it cannot prove through the 0286 ledger. It never rewrites a resource row and never infers user ownership.';