@opengeni/db 0.36.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 (274) 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-GYO7ED2Q.js → chunk-EULAN2NP.js} +2 -2
  7. package/dist/{chunk-VTPRIXKG.js → chunk-NJVZPWXY.js} +3 -3
  8. package/dist/{chunk-HSOWSYOX.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-5KTIEDX7.js → chunk-PR2WMLOQ.js} +7714 -4443
  13. package/dist/chunk-PR2WMLOQ.js.map +1 -0
  14. package/dist/{chunk-Z7VYMNPZ.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.d.ts +10 -0
  36. package/dist/editable-artifact-durable-export.js +18 -3
  37. package/dist/editable-artifact-durable-export.js.map +1 -1
  38. package/dist/editable-artifacts.d.ts +2 -2
  39. package/dist/editable-artifacts.js +3 -3
  40. package/dist/governed-learning-activation-schema.d.ts +984 -0
  41. package/dist/governed-learning-activation.d.ts +98 -0
  42. package/dist/governed-learning-evaluator-schema.d.ts +633 -0
  43. package/dist/governed-learning-evaluator.d.ts +28 -0
  44. package/dist/index.d.ts +1633 -87
  45. package/dist/index.js +22004 -8109
  46. package/dist/index.js.map +1 -1
  47. package/dist/insights.d.ts +18 -0
  48. package/dist/integration-bindings.d.ts +1 -0
  49. package/dist/integration-facets.d.ts +30 -0
  50. package/dist/interaction-placement-loss.d.ts +22 -0
  51. package/dist/knowledge-source-sync-schema.d.ts +730 -0
  52. package/dist/knowledge-source-sync.d.ts +60 -0
  53. package/dist/managed-human-provisioning.d.ts +25 -0
  54. package/dist/memory-domain.d.ts +3 -3
  55. package/dist/migrate.d.ts +11 -1
  56. package/dist/migrate.js +1 -1
  57. package/dist/new-session-drafts.d.ts +43 -5
  58. package/dist/organization-membership-backfill.d.ts +174 -0
  59. package/dist/organization-membership-lifecycle.d.ts +162 -0
  60. package/dist/pr-review.d.ts +107 -0
  61. package/dist/preference-registry-schema.d.ts +2 -0
  62. package/dist/preference-registry.d.ts +2 -0
  63. package/dist/provision-roles.js +1 -1
  64. package/dist/runtime-posture.d.ts +23 -9
  65. package/dist/schema.d.ts +24925 -13697
  66. package/dist/schema.js +103 -1
  67. package/dist/scoped-knowledge-schema.d.ts +52 -1
  68. package/dist/session-background-commands.d.ts +125 -0
  69. package/dist/session-background-commands.js +34 -0
  70. package/dist/session-background-commands.js.map +1 -0
  71. package/dist/session-control.d.ts +156 -3
  72. package/dist/session-goal-pacing.d.ts +68 -0
  73. package/dist/session-queue-commands.d.ts +42 -5
  74. package/dist/session-realtime-ledger.d.ts +3 -0
  75. package/dist/session-tenancy.d.ts +71 -1
  76. package/dist/session-tenancy.js +25 -3
  77. package/dist/session-tool-call-settlement.d.ts +9 -4
  78. package/dist/slack-routing-personal-workspace.d.ts +23 -0
  79. package/dist/slack-routing.d.ts +153 -0
  80. package/dist/slack-task-policy-schema.d.ts +34 -0
  81. package/dist/slack-task-policy.d.ts +8 -0
  82. package/dist/slack-user-link-access.d.ts +1 -0
  83. package/dist/task-notes-schema.d.ts +594 -0
  84. package/dist/task-notes.d.ts +39 -0
  85. package/dist/turn-initiator.d.ts +2 -0
  86. package/dist/user-resource-authority.d.ts +109 -0
  87. package/dist/video-generation.js +3 -3
  88. package/dist/workspace-authority.d.ts +81 -0
  89. package/dist/workspace-instruction-policies-schema.d.ts +279 -0
  90. package/dist/workspace-instruction-policies.d.ts +62 -0
  91. package/dist/workspace-learning-policy.d.ts +1 -0
  92. package/dist/workspace-membership-permissions.d.ts +2 -0
  93. package/drizzle/0240_enrollment_connection_authority.sql +39 -0
  94. package/drizzle/0240_model_context_user_messages.sql +204 -0
  95. package/drizzle/0241_atomic_personal_resource_delegation.sql +1000 -0
  96. package/drizzle/0241_enrollment_agent_runtime.sql +70 -0
  97. package/drizzle/0243_google_drive_object_acl_authority.sql +554 -0
  98. package/drizzle/0244_slack_app_home_refresh_queue.sql +120 -0
  99. package/drizzle/0245_model_context_contribution_facts.sql +88 -0
  100. package/drizzle/0246_integration_personal_instance_authority.sql +70 -0
  101. package/drizzle/0247_terraform_stacks_provenance_repair.sql +428 -0
  102. package/drizzle/0248_terraform_stacks_component_resolution_fence.sql +83 -0
  103. package/drizzle/0249_personal_resource_delegation_authority_correction.sql +662 -0
  104. package/drizzle/0250_direct_retained_process_owner_liveness.sql +199 -0
  105. package/drizzle/0251_connected_machine_operation_policy.sql +45 -0
  106. package/drizzle/0252_scheduled_personal_resource_delegation.sql +1867 -0
  107. package/drizzle/0253_common_user_resource_authority_lifecycle.sql +1555 -0
  108. package/drizzle/0254_scoped_variable_set_authority.sql +1255 -0
  109. package/drizzle/0255_company_brain_governed_write_proposals.sql +524 -0
  110. package/drizzle/0256_connection_authority_delegation.sql +703 -0
  111. package/drizzle/0257_goal_revision_decisions_and_root_constraints.sql +436 -0
  112. package/drizzle/0258_three_scope_document_knowledge_authority.sql +1273 -0
  113. package/drizzle/0259_company_brain_context_selection_receipts.sql +869 -0
  114. package/drizzle/0260_task_note_knowledge_promotion.sql +820 -0
  115. package/drizzle/0261_preference_knowledge_proposal_actor_binding.sql +471 -0
  116. package/drizzle/0262_scoped_connected_machines_and_rigs.sql +1700 -0
  117. package/drizzle/0263_organization_membership_lifecycle.sql +3284 -0
  118. package/drizzle/0264_connection_authority_runtime_activation.sql +1084 -0
  119. package/drizzle/0266_company_brain_context_receipt_inspection.sql +156 -0
  120. package/drizzle/0268_governed_learning_decision_receipts.sql +825 -0
  121. package/drizzle/0269_governed_learning_activation_controller.sql +1874 -0
  122. package/drizzle/0270_governed_learning_history_inspection.sql +170 -0
  123. package/drizzle/0271_company_brain_retrieval_only_default.sql +524 -0
  124. package/drizzle/0272_human_confirmed_learning_activation.sql +591 -0
  125. package/drizzle/0273_scheduled_variable_set_materialization.sql +268 -0
  126. package/drizzle/0274_human_confirmed_knowledge_review.sql +310 -0
  127. package/drizzle/0275_scheduled_connection_authority.sql +5527 -0
  128. package/drizzle/0276_onboarding_proposal_initiating_human_guc.sql +198 -0
  129. package/drizzle/0277_workspace_writer_authority_attribution.sql +393 -0
  130. package/drizzle/0278_workspace_membership_removal_fencing.sql +1174 -0
  131. package/drizzle/0279_workspace_connection_use_lane.sql +343 -0
  132. package/drizzle/0280_connection_and_variable_set_audit_attribution.sql +773 -0
  133. package/drizzle/0281_viewer_holder_authority_claims.sql +52 -0
  134. package/drizzle/0282_variable_set_session_attach_attribution.sql +178 -0
  135. package/drizzle/0283_editable_spreadsheet_authored_state.sql +82 -0
  136. package/drizzle/0284_truthful_human_confirmed_review_reason.sql +930 -0
  137. package/drizzle/0285_organization_tenancy_inventory.sql +329 -0
  138. package/drizzle/0286_widen_task_note_expiry_ceiling.sql +391 -0
  139. package/drizzle/0287_open_suffix_pending_tool_calls.sql +18 -0
  140. package/drizzle/0288_attached_browser_reenrollment.sql +64 -0
  141. package/drizzle/0289_session_composer_policy_authority.sql +597 -0
  142. package/drizzle/0290_organization_membership_backfill.sql +268 -0
  143. package/drizzle/0291_resource_authority_classification_assertion.sql +371 -0
  144. package/drizzle/0292_truthful_tenancy_inventory_counters.sql +291 -0
  145. package/drizzle/0293_confirm_time_rule_rebaseline.sql +615 -0
  146. package/drizzle/0294_preference_activation_authority.sql +237 -0
  147. package/drizzle/0295_retire_legacy_standing_memory_mode.sql +476 -0
  148. package/drizzle/0296_force_rls_backfill_noop_repair.sql +215 -0
  149. package/drizzle/0297_session_ownership_classification_and_backfill.sql +752 -0
  150. package/drizzle/0298_organization_tenancy_parity.sql +609 -0
  151. package/drizzle/0299_organization_membership_lock_order.sql +291 -0
  152. package/drizzle/0300_tenancy_backfill_ledger.sql +459 -0
  153. package/drizzle/0301_session_snapshot_and_pin_visibility.sql +181 -0
  154. package/drizzle/0302_personal_workspace_session_ownership.sql +303 -0
  155. package/drizzle/0303_session_tenancy_product_activation.sql +919 -0
  156. package/drizzle/0304_personal_workspace_private_session_reads.sql +97 -0
  157. package/drizzle/0305_personal_resource_grant_management.sql +584 -0
  158. package/drizzle/0306_atomic_personal_resource_attachments.sql +1374 -0
  159. package/drizzle/0307_session_attention_state.sql +51 -0
  160. package/drizzle/0308_session_archives.sql +35 -0
  161. package/drizzle/0309_channel_project_pins.sql +6 -0
  162. package/drizzle/0310_channel_project_order.sql +27 -0
  163. package/drizzle/0311_company_scope_and_private_session_create.sql +656 -0
  164. package/drizzle/0312_quiescent_session_tree_deletion.sql +39 -0
  165. package/drizzle/0313_private_child_session_authority.sql +365 -0
  166. package/drizzle/0314_unregistered_organization_invitations.sql +606 -0
  167. package/drizzle/0315_personal_github_repository_selection.sql +624 -0
  168. package/drizzle/0316_human_confirmed_activation_resumed_generation.sql +869 -0
  169. package/drizzle/0317_session_goal_continuation_hold.sql +64 -0
  170. package/drizzle/0318_session_turn_startup_milestones.sql +115 -0
  171. package/drizzle/0319_event_triggered_automations.sql +254 -0
  172. package/drizzle/0320_pr_review_pack.sql +113 -0
  173. package/drizzle/0321_slack_bot_environment_display_name.sql +15 -0
  174. package/drizzle/0322_session_turns_unclaimed_prompt_trigger_index.sql +5 -0
  175. package/drizzle/0323_organization_private_session_enablement.sql +489 -0
  176. package/drizzle/0324_human_confirmed_company_profile_agent_admin.sql +856 -0
  177. package/drizzle/0325_child_lifecycle_notices.sql +44 -0
  178. package/drizzle/0326_interaction_operation_error_codes.sql +62 -0
  179. package/drizzle/0327_slack_first_task_hint.sql +29 -0
  180. package/drizzle/0328_session_background_commands.sql +590 -0
  181. package/drizzle/0329_slack_orchestration_delivery_events.sql +91 -0
  182. package/drizzle/0330_api_key_descriptions.sql +20 -0
  183. package/drizzle/0331_managed_organization_creation.sql +163 -0
  184. package/drizzle/0332_organization_shared_workspace_control_plane.sql +539 -0
  185. package/drizzle/0333_session_turn_prompt_routing.sql +45 -0
  186. package/drizzle/0334_connected_machine_workspace_root.sql +99 -0
  187. package/drizzle/0335_slack_workspace_routing.sql +460 -0
  188. package/drizzle/0336_atomic_session_fork_visibility.sql +667 -0
  189. package/drizzle/0337_slack_routed_action_handles.sql +189 -0
  190. package/drizzle/0338_atomic_connected_machine_attachments.sql +410 -0
  191. package/drizzle/0339_document_authority_reclassification.sql +1153 -0
  192. package/drizzle/0340_tenancy_backfill_activation_evidence.sql +1347 -0
  193. package/drizzle/0341_slack_routing_probe_organization_fence.sql +109 -0
  194. package/package.json +10 -6
  195. package/src/attached-browser-devices.ts +233 -5
  196. package/src/automations.ts +911 -0
  197. package/src/browser-sessions.ts +10 -0
  198. package/src/capability-integrations.ts +175 -1
  199. package/src/child-lifecycle-notices.ts +241 -0
  200. package/src/codemode-operations.ts +1 -35
  201. package/src/company-brain-context-selection-schema.ts +100 -0
  202. package/src/company-brain-context-selection.ts +359 -0
  203. package/src/company-brain-governed-writes.ts +1170 -0
  204. package/src/company-brain.ts +309 -0
  205. package/src/company-profile-agent-admin.ts +225 -0
  206. package/src/company-profile-schema.ts +121 -1
  207. package/src/company-profile.ts +21 -0
  208. package/src/computer-sessions.ts +10 -1
  209. package/src/connection-authority.ts +290 -0
  210. package/src/connection-scopes.ts +20 -0
  211. package/src/connection-tenancy-backfill.ts +81 -0
  212. package/src/connection-token-resolver.ts +263 -31
  213. package/src/database.ts +75 -2
  214. package/src/editable-artifact-durable-export.ts +27 -0
  215. package/src/editable-artifacts-schema.ts +10 -5
  216. package/src/editable-artifacts.ts +21 -11
  217. package/src/governed-learning-activation-schema.ts +121 -0
  218. package/src/governed-learning-activation.ts +588 -0
  219. package/src/governed-learning-evaluator-schema.ts +72 -0
  220. package/src/governed-learning-evaluator.ts +217 -0
  221. package/src/index.ts +12001 -1699
  222. package/src/insights.ts +107 -0
  223. package/src/integration-bindings.ts +88 -6
  224. package/src/integration-facets.ts +287 -84
  225. package/src/interaction-placement-loss.ts +234 -0
  226. package/src/knowledge-source-sync-schema.ts +83 -0
  227. package/src/knowledge-source-sync.ts +530 -0
  228. package/src/managed-human-provisioning.ts +116 -0
  229. package/src/memory-domain.ts +27 -32
  230. package/src/migrate.ts +77 -1
  231. package/src/new-session-drafts.ts +229 -35
  232. package/src/organization-membership-backfill.ts +735 -0
  233. package/src/organization-membership-lifecycle.ts +985 -0
  234. package/src/pr-review.ts +890 -0
  235. package/src/preference-registry.ts +31 -1
  236. package/src/provision-roles.ts +931 -24
  237. package/src/runtime-posture-cli.ts +2 -0
  238. package/src/runtime-posture.ts +1315 -52
  239. package/src/schema.ts +3336 -438
  240. package/src/scoped-knowledge-schema.ts +16 -8
  241. package/src/scoped-knowledge.ts +1 -1
  242. package/src/session-background-commands.ts +759 -0
  243. package/src/session-control.ts +640 -74
  244. package/src/session-goal-pacing.ts +126 -0
  245. package/src/session-queue-commands.ts +450 -98
  246. package/src/session-realtime-context.ts +19 -3
  247. package/src/session-realtime-ledger.ts +75 -12
  248. package/src/session-realtime-mirror.ts +1 -1
  249. package/src/session-tenancy.ts +403 -49
  250. package/src/session-tool-call-settlement.ts +38 -29
  251. package/src/slack-routing-personal-workspace.ts +67 -0
  252. package/src/slack-routing.ts +469 -0
  253. package/src/slack-task-policy-schema.ts +5 -0
  254. package/src/slack-task-policy.ts +18 -0
  255. package/src/slack-user-link-access.ts +17 -1
  256. package/src/task-notes-schema.ts +74 -0
  257. package/src/task-notes.ts +124 -2
  258. package/src/turn-initiator.ts +8 -0
  259. package/src/user-resource-authority.ts +377 -0
  260. package/src/workspace-authority.ts +160 -0
  261. package/src/workspace-instruction-policies-schema.ts +53 -1
  262. package/src/workspace-instruction-policies.ts +528 -77
  263. package/src/workspace-learning-policy.ts +21 -5
  264. package/src/workspace-membership-permissions.ts +9 -0
  265. package/dist/chunk-5KTIEDX7.js.map +0 -1
  266. package/dist/chunk-FPVAD4CO.js +0 -117
  267. package/dist/chunk-FPVAD4CO.js.map +0 -1
  268. package/dist/chunk-HSOWSYOX.js.map +0 -1
  269. package/dist/chunk-SK7YYHBI.js.map +0 -1
  270. package/dist/chunk-VKLB2ZGI.js +0 -2389
  271. package/dist/chunk-VKLB2ZGI.js.map +0 -1
  272. package/dist/chunk-Z7VYMNPZ.js.map +0 -1
  273. /package/dist/{chunk-GYO7ED2Q.js.map → chunk-EULAN2NP.js.map} +0 -0
  274. /package/dist/{chunk-VTPRIXKG.js.map → chunk-NJVZPWXY.js.map} +0 -0
@@ -0,0 +1,1374 @@
1
+ -- deployment-mode: maintenance
2
+ -- Migration 0306: bind personal Variable Set/Rig issuance to one accepted
3
+ -- logical turn. Old workers select/consume `once` at attempt claim and cannot
4
+ -- safely recover that turn, so every API/control/turn worker must be stopped
5
+ -- before this cutover and no pre-0306 worker may restart afterwards.
6
+
7
+ SET LOCAL lock_timeout = '5s';
8
+ SET LOCAL statement_timeout = '10min';
9
+
10
+ DO $atomic_personal_resource_writer_drain_before_lock$
11
+ DECLARE
12
+ configured_roles_text text := nullif(
13
+ current_setting('opengeni.migration_application_roles', true), ''
14
+ );
15
+ configured_roles jsonb;
16
+ BEGIN
17
+ IF configured_roles_text IS NULL THEN
18
+ RAISE EXCEPTION
19
+ '0306 atomic personal-resource activation requires an explicit application database role list'
20
+ USING ERRCODE = '55000';
21
+ END IF;
22
+ BEGIN
23
+ configured_roles := configured_roles_text::jsonb;
24
+ EXCEPTION WHEN OTHERS THEN
25
+ RAISE EXCEPTION
26
+ '0306 atomic personal-resource activation received a malformed application database role list'
27
+ USING ERRCODE = '55000';
28
+ END;
29
+ IF jsonb_typeof(configured_roles) <> 'array'
30
+ OR jsonb_array_length(configured_roles) NOT BETWEEN 1 AND 16
31
+ OR EXISTS (
32
+ SELECT 1 FROM jsonb_array_elements(configured_roles) AS roles(value)
33
+ WHERE jsonb_typeof(value) <> 'string'
34
+ OR btrim(value #>> '{}') = ''
35
+ OR octet_length(value #>> '{}') > 63
36
+ )
37
+ OR (
38
+ SELECT count(*) FROM jsonb_array_elements_text(configured_roles)
39
+ ) <> (
40
+ SELECT count(DISTINCT value)
41
+ FROM jsonb_array_elements_text(configured_roles) AS roles(value)
42
+ )
43
+ THEN
44
+ RAISE EXCEPTION
45
+ '0306 atomic personal-resource activation received an invalid application database role list'
46
+ USING ERRCODE = '55000';
47
+ END IF;
48
+ IF EXISTS (
49
+ SELECT 1 FROM pg_stat_activity activity
50
+ JOIN jsonb_array_elements_text(configured_roles) roles(role_name)
51
+ ON roles.role_name = activity.usename
52
+ WHERE activity.datname = current_database()
53
+ AND activity.pid <> pg_backend_pid()
54
+ )
55
+ THEN
56
+ RAISE EXCEPTION
57
+ '0306 atomic personal-resource activation requires all configured OpenGeni application database sessions to be stopped'
58
+ USING ERRCODE = '55000';
59
+ END IF;
60
+ END
61
+ $atomic_personal_resource_writer_drain_before_lock$;
62
+
63
+ LOCK TABLE sessions IN ACCESS EXCLUSIVE MODE;
64
+ LOCK TABLE session_turns IN ACCESS EXCLUSIVE MODE;
65
+ LOCK TABLE session_turn_attempts IN ACCESS EXCLUSIVE MODE;
66
+ LOCK TABLE organization_user_resource_grants IN ACCESS EXCLUSIVE MODE;
67
+
68
+ DO $atomic_personal_resource_writer_drain_after_lock$
69
+ DECLARE
70
+ configured_roles jsonb := current_setting(
71
+ 'opengeni.migration_application_roles', false
72
+ )::jsonb;
73
+ BEGIN
74
+ IF EXISTS (
75
+ SELECT 1 FROM pg_stat_activity activity
76
+ JOIN jsonb_array_elements_text(configured_roles) roles(role_name)
77
+ ON roles.role_name = activity.usename
78
+ WHERE activity.datname = current_database()
79
+ AND activity.pid <> pg_backend_pid()
80
+ )
81
+ THEN
82
+ RAISE EXCEPTION
83
+ '0306 atomic personal-resource activation requires all configured OpenGeni application database sessions to be stopped'
84
+ USING ERRCODE = '55000';
85
+ END IF;
86
+ END
87
+ $atomic_personal_resource_writer_drain_after_lock$;
88
+
89
+ -- The production migrator is a NOSUPERUSER/NOBYPASSRLS table owner, so FORCE
90
+ -- RLS would otherwise make this global drained-work guard see an empty set.
91
+ -- These five tables are read-only inside this exact transaction-local window;
92
+ -- later mutation begins only after FORCE has been restored on every table.
93
+ ALTER TABLE sessions NO FORCE ROW LEVEL SECURITY;
94
+ ALTER TABLE session_turns NO FORCE ROW LEVEL SECURITY;
95
+ ALTER TABLE rigs NO FORCE ROW LEVEL SECURITY;
96
+ ALTER TABLE rig_versions NO FORCE ROW LEVEL SECURITY;
97
+ ALTER TABLE workspace_variable_sets NO FORCE ROW LEVEL SECURITY;
98
+
99
+ DO $atomic_personal_resource_work_drain$
100
+ BEGIN
101
+ IF EXISTS (
102
+ SELECT 1
103
+ FROM session_turns turn_value
104
+ JOIN sessions session_value
105
+ ON session_value.id = turn_value.session_id
106
+ AND session_value.account_id = turn_value.account_id
107
+ AND session_value.workspace_id = turn_value.workspace_id
108
+ WHERE turn_value.status IN (
109
+ 'queued', 'running', 'requires_action', 'recovering', 'waiting_capacity'
110
+ )
111
+ AND (
112
+ EXISTS (
113
+ SELECT 1 FROM workspace_variable_sets variable_set
114
+ WHERE variable_set.id = session_value.variable_set_id
115
+ AND variable_set.authority_scope = 'user'
116
+ )
117
+ OR EXISTS (
118
+ SELECT 1 FROM rigs rig
119
+ WHERE rig.id = session_value.rig_id AND rig.authority_scope = 'user'
120
+ )
121
+ OR EXISTS (
122
+ SELECT 1
123
+ FROM rig_versions version_value
124
+ CROSS JOIN LATERAL jsonb_array_elements_text(
125
+ version_value.default_variable_set_ids
126
+ ) default_id(value)
127
+ JOIN workspace_variable_sets variable_set
128
+ ON variable_set.id = default_id.value::uuid
129
+ AND variable_set.authority_scope = 'user'
130
+ WHERE version_value.id = session_value.rig_version_id
131
+ AND version_value.rig_id = session_value.rig_id
132
+ )
133
+ )
134
+ ) THEN
135
+ RAISE EXCEPTION
136
+ '0306 requires all executable turns with personal Variable Set/Rig resources to be drained'
137
+ USING ERRCODE = '55000';
138
+ END IF;
139
+ END
140
+ $atomic_personal_resource_work_drain$;
141
+
142
+ ALTER TABLE sessions FORCE ROW LEVEL SECURITY;
143
+ ALTER TABLE session_turns FORCE ROW LEVEL SECURITY;
144
+ ALTER TABLE rigs FORCE ROW LEVEL SECURITY;
145
+ ALTER TABLE rig_versions FORCE ROW LEVEL SECURITY;
146
+ ALTER TABLE workspace_variable_sets FORCE ROW LEVEL SECURITY;
147
+
148
+ -- No active legacy once grant has an accepted logical-turn owner. Settle them
149
+ -- before activating the new protocol so no new turn can adopt ambiguous work.
150
+ ALTER TABLE organization_user_resource_grants NO FORCE ROW LEVEL SECURITY;
151
+ UPDATE organization_user_resource_grants
152
+ SET status = 'revoked', revoked_at = coalesce(revoked_at, clock_timestamp()),
153
+ generation = generation + 1, updated_at = clock_timestamp()
154
+ WHERE status = 'active' AND mode = 'once';
155
+ ALTER TABLE organization_user_resource_grants FORCE ROW LEVEL SECURITY;
156
+
157
+ ALTER TABLE sessions ADD COLUMN "initial_personal_resource_attachment_intent" jsonb;
158
+ ALTER TABLE session_turns
159
+ ADD COLUMN "personal_resource_attachment_summary" jsonb,
160
+ ADD COLUMN "personal_resource_protocol_version" integer NOT NULL DEFAULT 0;
161
+ ALTER TABLE session_turn_attempts
162
+ ADD COLUMN "personal_resource_protocol_version" integer NOT NULL DEFAULT 0;
163
+
164
+ ALTER TABLE session_turns ADD CONSTRAINT "session_turns_personal_resource_protocol_chk"
165
+ CHECK (personal_resource_protocol_version IN (0, 1));
166
+ ALTER TABLE session_turn_attempts
167
+ ADD CONSTRAINT "session_turn_attempts_personal_resource_protocol_chk"
168
+ CHECK (personal_resource_protocol_version IN (0, 1));
169
+
170
+ CREATE TABLE turn_personal_resource_attachment_receipts (
171
+ turn_id uuid PRIMARY KEY REFERENCES session_turns(id) ON DELETE CASCADE,
172
+ account_id uuid NOT NULL,
173
+ workspace_id uuid NOT NULL,
174
+ session_id uuid NOT NULL,
175
+ initiating_human_subject_id text NOT NULL,
176
+ owner_organization_membership_id uuid NOT NULL,
177
+ membership_authorization_revision bigint NOT NULL,
178
+ session_visibility text NOT NULL,
179
+ session_authority_epoch integer NOT NULL,
180
+ grant_mode text NOT NULL,
181
+ shared_output_warning_version integer NOT NULL,
182
+ shared_output_acknowledged boolean NOT NULL,
183
+ request_digest bytea NOT NULL,
184
+ resource_count integer NOT NULL,
185
+ created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
186
+ CONSTRAINT turn_personal_resource_attachment_receipts_identity_chk CHECK (
187
+ octet_length(initiating_human_subject_id) BETWEEN 1 AND 512
188
+ AND membership_authorization_revision > 0
189
+ AND session_authority_epoch > 0
190
+ AND grant_mode IN ('once', 'session', 'always')
191
+ AND session_visibility IN ('user_private', 'workspace_shared')
192
+ AND shared_output_warning_version = 1
193
+ AND resource_count BETWEEN 1 AND 27
194
+ AND (session_visibility <> 'workspace_shared' OR shared_output_acknowledged IS TRUE)
195
+ ),
196
+ CONSTRAINT turn_personal_resource_attachment_receipts_workspace_turn_fk
197
+ FOREIGN KEY (workspace_id, turn_id)
198
+ REFERENCES session_turns(workspace_id, id) ON DELETE CASCADE
199
+ );
200
+
201
+ CREATE TABLE turn_personal_resource_snapshots (
202
+ turn_id uuid NOT NULL REFERENCES turn_personal_resource_attachment_receipts(turn_id)
203
+ ON DELETE CASCADE,
204
+ account_id uuid NOT NULL,
205
+ workspace_id uuid NOT NULL,
206
+ session_id uuid NOT NULL,
207
+ resource_kind text NOT NULL,
208
+ resource_id uuid NOT NULL,
209
+ resource_version_id uuid,
210
+ selection_sources text[] NOT NULL,
211
+ action text NOT NULL,
212
+ origin_workspace_id uuid NOT NULL,
213
+ owner_organization_membership_id uuid NOT NULL,
214
+ membership_authorization_revision bigint NOT NULL,
215
+ authority_id uuid NOT NULL,
216
+ authority_generation bigint NOT NULL,
217
+ grant_id uuid NOT NULL,
218
+ grant_generation bigint NOT NULL,
219
+ grant_mode text NOT NULL,
220
+ grant_context text NOT NULL,
221
+ grant_session_id uuid,
222
+ grant_authority_epoch integer,
223
+ canonical_delegation jsonb NOT NULL,
224
+ snapshot_digest bytea NOT NULL,
225
+ created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
226
+ PRIMARY KEY (turn_id, resource_kind, resource_id),
227
+ CONSTRAINT turn_personal_resource_snapshots_kind_chk CHECK (
228
+ (resource_kind = 'variable_set' AND action = 'variable_set.use'
229
+ AND resource_version_id IS NULL)
230
+ OR (resource_kind = 'rig' AND action = 'rig.use'
231
+ AND resource_version_id IS NOT NULL)
232
+ ),
233
+ CONSTRAINT turn_personal_resource_snapshots_generation_chk CHECK (
234
+ membership_authorization_revision > 0
235
+ AND authority_generation > 0 AND grant_generation > 0
236
+ AND cardinality(selection_sources) BETWEEN 1 AND 26
237
+ ),
238
+ CONSTRAINT turn_personal_resource_snapshots_grant_chk CHECK (
239
+ grant_mode IN ('once', 'session', 'always')
240
+ AND grant_context IN ('user_private', 'workspace_shared')
241
+ AND ((grant_mode = 'always' AND grant_session_id IS NULL
242
+ AND grant_authority_epoch IS NULL)
243
+ OR (grant_mode IN ('once', 'session') AND grant_session_id = session_id
244
+ AND grant_authority_epoch > 0))
245
+ )
246
+ );
247
+ CREATE INDEX turn_personal_resource_snapshots_authority_idx
248
+ ON turn_personal_resource_snapshots(authority_id);
249
+ CREATE INDEX turn_personal_resource_snapshots_grant_idx
250
+ ON turn_personal_resource_snapshots(grant_id);
251
+
252
+ CREATE TABLE turn_personal_resource_once_receipts (
253
+ grant_id uuid PRIMARY KEY,
254
+ turn_id uuid NOT NULL REFERENCES turn_personal_resource_attachment_receipts(turn_id)
255
+ ON DELETE CASCADE,
256
+ account_id uuid NOT NULL,
257
+ authority_id uuid NOT NULL,
258
+ authority_generation bigint NOT NULL,
259
+ grant_generation bigint NOT NULL,
260
+ consumed_at timestamptz NOT NULL DEFAULT clock_timestamp(),
261
+ CONSTRAINT turn_personal_resource_once_receipts_generation_chk CHECK (
262
+ authority_generation > 0 AND grant_generation > 0
263
+ )
264
+ );
265
+
266
+ ALTER TABLE turn_personal_resource_attachment_receipts ENABLE ROW LEVEL SECURITY;
267
+ ALTER TABLE turn_personal_resource_attachment_receipts FORCE ROW LEVEL SECURITY;
268
+ ALTER TABLE turn_personal_resource_snapshots ENABLE ROW LEVEL SECURITY;
269
+ ALTER TABLE turn_personal_resource_snapshots FORCE ROW LEVEL SECURITY;
270
+ ALTER TABLE turn_personal_resource_once_receipts ENABLE ROW LEVEL SECURITY;
271
+ ALTER TABLE turn_personal_resource_once_receipts FORCE ROW LEVEL SECURITY;
272
+
273
+ CREATE POLICY turn_personal_resource_attachment_internal
274
+ ON turn_personal_resource_attachment_receipts
275
+ USING (
276
+ current_setting('opengeni.organization_tenancy_lifecycle', true)
277
+ = 'personal_resource_grant_management'
278
+ OR EXISTS (
279
+ SELECT 1 FROM opengeni_private.personal_resource_delegation_capabilities capability
280
+ WHERE capability.backend_pid = pg_backend_pid()
281
+ AND capability.transaction_id = pg_current_xact_id_if_assigned()
282
+ AND capability.capability_kind IN ('admit', 'resolve')
283
+ )
284
+ )
285
+ WITH CHECK (
286
+ current_setting('opengeni.organization_tenancy_lifecycle', true)
287
+ = 'personal_resource_grant_management'
288
+ OR EXISTS (
289
+ SELECT 1 FROM opengeni_private.personal_resource_delegation_capabilities capability
290
+ WHERE capability.backend_pid = pg_backend_pid()
291
+ AND capability.transaction_id = pg_current_xact_id_if_assigned()
292
+ AND capability.capability_kind = 'admit'
293
+ )
294
+ );
295
+ CREATE POLICY turn_personal_resource_snapshot_internal
296
+ ON turn_personal_resource_snapshots
297
+ USING (
298
+ current_setting('opengeni.organization_tenancy_lifecycle', true)
299
+ = 'personal_resource_grant_management'
300
+ OR EXISTS (
301
+ SELECT 1 FROM opengeni_private.personal_resource_delegation_capabilities capability
302
+ WHERE capability.backend_pid = pg_backend_pid()
303
+ AND capability.transaction_id = pg_current_xact_id_if_assigned()
304
+ AND capability.capability_kind IN ('admit', 'resolve')
305
+ )
306
+ )
307
+ WITH CHECK (
308
+ current_setting('opengeni.organization_tenancy_lifecycle', true)
309
+ = 'personal_resource_grant_management'
310
+ );
311
+ CREATE POLICY turn_personal_resource_once_internal
312
+ ON turn_personal_resource_once_receipts
313
+ USING (
314
+ current_setting('opengeni.organization_tenancy_lifecycle', true)
315
+ = 'personal_resource_grant_management'
316
+ OR EXISTS (
317
+ SELECT 1 FROM opengeni_private.personal_resource_delegation_capabilities capability
318
+ WHERE capability.backend_pid = pg_backend_pid()
319
+ AND capability.transaction_id = pg_current_xact_id_if_assigned()
320
+ AND capability.capability_kind IN ('admit', 'resolve')
321
+ )
322
+ )
323
+ WITH CHECK (
324
+ current_setting('opengeni.organization_tenancy_lifecycle', true)
325
+ = 'personal_resource_grant_management'
326
+ );
327
+
328
+ CREATE FUNCTION digest_turn_personal_resource_snapshot(
329
+ p_snapshot turn_personal_resource_snapshots
330
+ ) RETURNS bytea
331
+ LANGUAGE sql
332
+ IMMUTABLE
333
+ PARALLEL SAFE
334
+ SET search_path FROM CURRENT
335
+ AS $body$
336
+ SELECT digest(convert_to(
337
+ (to_jsonb(p_snapshot) - 'snapshot_digest' - 'created_at')::text,
338
+ 'UTF8'
339
+ ), 'sha256')
340
+ $body$;
341
+
342
+ CREATE FUNCTION stamp_turn_personal_resource_snapshot_digest()
343
+ RETURNS trigger
344
+ LANGUAGE plpgsql
345
+ SECURITY DEFINER
346
+ SET search_path FROM CURRENT
347
+ AS $body$
348
+ BEGIN
349
+ NEW.snapshot_digest := digest_turn_personal_resource_snapshot(NEW);
350
+ RETURN NEW;
351
+ END
352
+ $body$;
353
+
354
+ CREATE TRIGGER turn_personal_resource_snapshots_digest
355
+ BEFORE INSERT ON turn_personal_resource_snapshots
356
+ FOR EACH ROW EXECUTE FUNCTION stamp_turn_personal_resource_snapshot_digest();
357
+
358
+ CREATE FUNCTION reject_turn_personal_resource_snapshot_update()
359
+ RETURNS trigger
360
+ LANGUAGE plpgsql
361
+ SECURITY DEFINER
362
+ SET search_path FROM CURRENT
363
+ AS $body$
364
+ BEGIN
365
+ RAISE EXCEPTION 'accepted turn personal-resource evidence is immutable'
366
+ USING ERRCODE = '42501';
367
+ END
368
+ $body$;
369
+
370
+ CREATE TRIGGER turn_personal_resource_attachment_receipts_immutable
371
+ BEFORE UPDATE ON turn_personal_resource_attachment_receipts
372
+ FOR EACH ROW EXECUTE FUNCTION reject_turn_personal_resource_snapshot_update();
373
+ CREATE TRIGGER turn_personal_resource_snapshots_immutable
374
+ BEFORE UPDATE ON turn_personal_resource_snapshots
375
+ FOR EACH ROW EXECUTE FUNCTION reject_turn_personal_resource_snapshot_update();
376
+ CREATE TRIGGER turn_personal_resource_once_receipts_immutable
377
+ BEFORE UPDATE ON turn_personal_resource_once_receipts
378
+ FOR EACH ROW EXECUTE FUNCTION reject_turn_personal_resource_snapshot_update();
379
+
380
+ CREATE FUNCTION fence_turn_personal_resource_protocol_update()
381
+ RETURNS trigger
382
+ LANGUAGE plpgsql
383
+ SECURITY DEFINER
384
+ SET search_path FROM CURRENT
385
+ AS $body$
386
+ BEGIN
387
+ IF OLD.personal_resource_protocol_version = 1 AND (
388
+ NEW.personal_resource_protocol_version IS DISTINCT FROM OLD.personal_resource_protocol_version
389
+ OR NEW.personal_resource_attachment_summary
390
+ IS DISTINCT FROM OLD.personal_resource_attachment_summary
391
+ ) THEN
392
+ RAISE EXCEPTION 'accepted turn personal-resource binding is immutable'
393
+ USING ERRCODE = '42501';
394
+ END IF;
395
+ RETURN NEW;
396
+ END
397
+ $body$;
398
+
399
+ CREATE TRIGGER session_turns_personal_resource_binding_immutable
400
+ BEFORE UPDATE ON session_turns
401
+ FOR EACH ROW EXECUTE FUNCTION fence_turn_personal_resource_protocol_update();
402
+
403
+ CREATE FUNCTION fence_attempt_personal_resource_protocol_update()
404
+ RETURNS trigger
405
+ LANGUAGE plpgsql
406
+ SECURITY DEFINER
407
+ SET search_path FROM CURRENT
408
+ AS $body$
409
+ BEGIN
410
+ IF NEW.personal_resource_protocol_version
411
+ IS DISTINCT FROM OLD.personal_resource_protocol_version
412
+ THEN
413
+ RAISE EXCEPTION 'attempt personal-resource protocol version is immutable'
414
+ USING ERRCODE = '42501';
415
+ END IF;
416
+ RETURN NEW;
417
+ END
418
+ $body$;
419
+
420
+ CREATE TRIGGER session_turn_attempts_personal_resource_protocol_immutable
421
+ BEFORE UPDATE ON session_turn_attempts
422
+ FOR EACH ROW EXECUTE FUNCTION fence_attempt_personal_resource_protocol_update();
423
+
424
+ CREATE FUNCTION fence_session_initial_personal_resource_intent_update()
425
+ RETURNS trigger
426
+ LANGUAGE plpgsql
427
+ SECURITY DEFINER
428
+ SET search_path FROM CURRENT
429
+ AS $body$
430
+ BEGIN
431
+ IF NEW.initial_personal_resource_attachment_intent
432
+ IS DISTINCT FROM OLD.initial_personal_resource_attachment_intent
433
+ THEN
434
+ RAISE EXCEPTION 'initial personal-resource attachment intent is immutable'
435
+ USING ERRCODE = '42501';
436
+ END IF;
437
+ RETURN NEW;
438
+ END
439
+ $body$;
440
+
441
+ CREATE TRIGGER sessions_initial_personal_resource_intent_immutable
442
+ BEFORE UPDATE ON sessions
443
+ FOR EACH ROW EXECUTE FUNCTION fence_session_initial_personal_resource_intent_update();
444
+
445
+ REVOKE ALL ON turn_personal_resource_attachment_receipts FROM PUBLIC;
446
+ REVOKE ALL ON turn_personal_resource_snapshots FROM PUBLIC;
447
+ REVOKE ALL ON turn_personal_resource_once_receipts FROM PUBLIC;
448
+ REVOKE ALL ON FUNCTION digest_turn_personal_resource_snapshot(
449
+ turn_personal_resource_snapshots
450
+ ) FROM PUBLIC;
451
+ REVOKE ALL ON FUNCTION stamp_turn_personal_resource_snapshot_digest() FROM PUBLIC;
452
+ REVOKE ALL ON FUNCTION reject_turn_personal_resource_snapshot_update() FROM PUBLIC;
453
+ REVOKE ALL ON FUNCTION fence_turn_personal_resource_protocol_update() FROM PUBLIC;
454
+ REVOKE ALL ON FUNCTION fence_attempt_personal_resource_protocol_update() FROM PUBLIC;
455
+ REVOKE ALL ON FUNCTION fence_session_initial_personal_resource_intent_update() FROM PUBLIC;
456
+
457
+ CREATE OR REPLACE FUNCTION accept_turn_personal_resource_attachment(
458
+ p_account_id uuid,
459
+ p_workspace_id uuid,
460
+ p_session_id uuid,
461
+ p_turn_id uuid,
462
+ p_mode text,
463
+ p_expected_authority_epoch integer,
464
+ p_workspace_shared_acknowledged boolean,
465
+ p_shared_output_warning_version integer
466
+ ) RETURNS TABLE (
467
+ grant_mode text,
468
+ grant_context text,
469
+ resource_count integer,
470
+ resource_kinds text[],
471
+ shared_output_warning_version integer,
472
+ replay boolean
473
+ )
474
+ LANGUAGE plpgsql
475
+ SECURITY DEFINER
476
+ SET search_path FROM CURRENT
477
+ AS $body$
478
+ #variable_conflict use_column
479
+ DECLARE
480
+ caller_subject text := nullif(current_setting('opengeni.subject_id', true), '');
481
+ session_row sessions%ROWTYPE;
482
+ turn_row session_turns%ROWTYPE;
483
+ member_row organization_memberships%ROWTYPE;
484
+ resource_row record;
485
+ authority_row organization_user_resource_authorities%ROWTYPE;
486
+ grant_row organization_user_resource_grants%ROWTYPE;
487
+ existing_receipt turn_personal_resource_attachment_receipts%ROWTYPE;
488
+ canonical jsonb;
489
+ request_hash bytea;
490
+ selected_resources jsonb := '[]'::jsonb;
491
+ selected_count integer := 0;
492
+ unauthorized_count integer := 0;
493
+ kinds text[] := ARRAY[]::text[];
494
+ BEGIN
495
+ PERFORM set_config(
496
+ 'opengeni.organization_tenancy_lifecycle', 'personal_resource_grant_management', true
497
+ );
498
+ IF p_account_id IS DISTINCT FROM nullif(current_setting('opengeni.account_id', true), '')::uuid
499
+ OR p_workspace_id IS DISTINCT FROM nullif(
500
+ current_setting('opengeni.workspace_id', true), ''
501
+ )::uuid
502
+ OR caller_subject IS NULL
503
+ OR p_mode IS NULL OR p_mode NOT IN ('once', 'session', 'always')
504
+ OR p_expected_authority_epoch IS NULL OR p_expected_authority_epoch <= 0
505
+ OR p_shared_output_warning_version IS DISTINCT FROM 1
506
+ THEN
507
+ RAISE EXCEPTION 'invalid atomic personal-resource attachment request'
508
+ USING ERRCODE = '22023';
509
+ END IF;
510
+ IF NOT EXISTS (
511
+ SELECT 1 FROM session_tenancy_activations activation
512
+ WHERE activation.account_id = p_account_id AND activation.activation_version = 1
513
+ ) THEN
514
+ RAISE EXCEPTION 'session tenancy product is not activated' USING ERRCODE = '42501';
515
+ END IF;
516
+
517
+ PERFORM 1 FROM workspace_inference_controls control_row
518
+ WHERE control_row.account_id = p_account_id AND control_row.workspace_id = p_workspace_id
519
+ FOR SHARE;
520
+ PERFORM 1 FROM workspaces workspace_value
521
+ WHERE workspace_value.account_id = p_account_id AND workspace_value.id = p_workspace_id
522
+ FOR KEY SHARE;
523
+ SELECT session_value.* INTO STRICT session_row
524
+ FROM sessions session_value
525
+ WHERE session_value.id = p_session_id
526
+ AND session_value.account_id = p_account_id
527
+ AND session_value.workspace_id = p_workspace_id
528
+ FOR NO KEY UPDATE;
529
+ SELECT turn_value.* INTO STRICT turn_row
530
+ FROM session_turns turn_value
531
+ WHERE turn_value.id = p_turn_id
532
+ AND turn_value.account_id = p_account_id
533
+ AND turn_value.workspace_id = p_workspace_id
534
+ AND turn_value.session_id = p_session_id
535
+ FOR UPDATE;
536
+
537
+ IF coalesce(nullif(btrim(turn_row.initiating_human_subject_id), ''),
538
+ CASE WHEN turn_row.initiator_kind = 'subject'
539
+ THEN nullif(btrim(turn_row.initiator_subject_id), '') END)
540
+ IS DISTINCT FROM caller_subject
541
+ OR session_row.authority_epoch IS DISTINCT FROM p_expected_authority_epoch
542
+ THEN
543
+ RAISE EXCEPTION 'atomic personal-resource attachment is not exact accepted work'
544
+ USING ERRCODE = '42501';
545
+ END IF;
546
+ IF session_row.visibility = 'workspace_shared'
547
+ AND p_workspace_shared_acknowledged IS NOT TRUE
548
+ THEN
549
+ RAISE EXCEPTION 'workspace-shared personal-resource attachment requires warning acknowledgement'
550
+ USING ERRCODE = '42501';
551
+ END IF;
552
+
553
+ request_hash := digest(convert_to(jsonb_build_object(
554
+ 'accountId', p_account_id, 'workspaceId', p_workspace_id,
555
+ 'sessionId', p_session_id, 'turnId', p_turn_id, 'mode', p_mode,
556
+ 'authorityEpoch', p_expected_authority_epoch,
557
+ 'sessionVisibility', session_row.visibility,
558
+ 'workspaceSharedAcknowledged', p_workspace_shared_acknowledged IS TRUE,
559
+ 'sharedOutputWarningVersion', p_shared_output_warning_version
560
+ )::text, 'UTF8'), 'sha256');
561
+
562
+ SELECT receipt.* INTO existing_receipt
563
+ FROM turn_personal_resource_attachment_receipts receipt
564
+ WHERE receipt.turn_id = p_turn_id;
565
+ IF FOUND THEN
566
+ IF existing_receipt.request_digest IS DISTINCT FROM request_hash THEN
567
+ RAISE EXCEPTION 'atomic personal-resource attachment replay changed its request'
568
+ USING ERRCODE = '23505';
569
+ END IF;
570
+ grant_mode := existing_receipt.grant_mode;
571
+ grant_context := existing_receipt.session_visibility;
572
+ resource_count := existing_receipt.resource_count;
573
+ SELECT array_agg(DISTINCT snapshot.resource_kind ORDER BY snapshot.resource_kind)
574
+ INTO resource_kinds
575
+ FROM turn_personal_resource_snapshots snapshot
576
+ WHERE snapshot.turn_id = p_turn_id;
577
+ shared_output_warning_version := existing_receipt.shared_output_warning_version;
578
+ replay := true;
579
+ RETURN NEXT;
580
+ RETURN;
581
+ END IF;
582
+
583
+ IF session_row.status = 'cancelled'
584
+ OR turn_row.status <> 'queued'
585
+ OR turn_row.active_attempt_id IS NOT NULL
586
+ THEN
587
+ RAISE EXCEPTION 'personal-resource issuance requires new queued accepted work'
588
+ USING ERRCODE = '42501';
589
+ END IF;
590
+
591
+ SELECT membership.* INTO STRICT member_row
592
+ FROM organization_memberships membership
593
+ WHERE membership.account_id = p_account_id
594
+ AND membership.subject_id = caller_subject
595
+ AND membership.status = 'active' AND membership.revoked_at IS NULL
596
+ FOR SHARE;
597
+ IF member_row.personal_workspace_id IS DISTINCT FROM p_workspace_id
598
+ AND NOT EXISTS (
599
+ SELECT 1 FROM workspace_memberships workspace_membership
600
+ WHERE workspace_membership.account_id = p_account_id
601
+ AND workspace_membership.workspace_id = p_workspace_id
602
+ AND workspace_membership.subject_id = caller_subject
603
+ )
604
+ THEN
605
+ RAISE EXCEPTION 'owner lacks target-workspace access' USING ERRCODE = '42501';
606
+ END IF;
607
+
608
+ -- Freeze every row that can change the server-derived closure before
609
+ -- evaluating it. The closure is materialized once below and reused for grant
610
+ -- issuance, so a concurrent resource edit cannot create a mixed snapshot.
611
+ PERFORM 1 FROM rigs rig
612
+ WHERE rig.id = session_row.rig_id AND rig.account_id = p_account_id
613
+ FOR SHARE;
614
+ PERFORM 1 FROM rig_versions version_value
615
+ WHERE version_value.id = session_row.rig_version_id
616
+ AND version_value.rig_id = session_row.rig_id
617
+ AND version_value.account_id = p_account_id
618
+ FOR SHARE;
619
+ PERFORM 1 FROM workspace_variable_sets variable_set
620
+ WHERE variable_set.id = session_row.variable_set_id
621
+ AND variable_set.account_id = p_account_id
622
+ FOR SHARE;
623
+ PERFORM variable_set.id
624
+ FROM rig_versions version_value
625
+ CROSS JOIN LATERAL jsonb_array_elements_text(version_value.default_variable_set_ids)
626
+ default_id(value)
627
+ JOIN workspace_variable_sets variable_set
628
+ ON variable_set.id = default_id.value::uuid
629
+ AND variable_set.account_id = p_account_id
630
+ WHERE version_value.id = session_row.rig_version_id
631
+ AND version_value.rig_id = session_row.rig_id
632
+ AND version_value.account_id = p_account_id
633
+ ORDER BY variable_set.id
634
+ FOR SHARE OF variable_set;
635
+
636
+ WITH selected AS (
637
+ SELECT 'variable_set'::text AS resource_kind, variable_set.id AS resource_id,
638
+ NULL::uuid AS resource_version_id, 'variable_set.use'::text AS action,
639
+ 'session_variable_set'::text AS selection_source,
640
+ variable_set.origin_workspace_id, variable_set.authority_id,
641
+ variable_set.owner_organization_membership_id
642
+ FROM workspace_variable_sets variable_set
643
+ WHERE variable_set.id = session_row.variable_set_id
644
+ AND variable_set.account_id = p_account_id
645
+ AND variable_set.authority_scope = 'user'
646
+ UNION ALL
647
+ SELECT 'rig', rig.id, session_row.rig_version_id, 'rig.use', 'session_rig',
648
+ rig.origin_workspace_id, rig.authority_id, rig.owner_organization_membership_id
649
+ FROM rigs rig
650
+ WHERE rig.id = session_row.rig_id
651
+ AND rig.account_id = p_account_id AND rig.authority_scope = 'user'
652
+ UNION ALL
653
+ SELECT 'variable_set', variable_set.id, NULL::uuid, 'variable_set.use',
654
+ 'rig_default_variable_set:' || default_id.ordinality::text,
655
+ variable_set.origin_workspace_id, variable_set.authority_id,
656
+ variable_set.owner_organization_membership_id
657
+ FROM rig_versions version_value
658
+ CROSS JOIN LATERAL jsonb_array_elements_text(version_value.default_variable_set_ids)
659
+ WITH ORDINALITY default_id(value, ordinality)
660
+ JOIN workspace_variable_sets variable_set
661
+ ON variable_set.id = default_id.value::uuid
662
+ AND variable_set.account_id = p_account_id
663
+ AND variable_set.authority_scope = 'user'
664
+ WHERE version_value.id = session_row.rig_version_id
665
+ AND version_value.rig_id = session_row.rig_id
666
+ AND version_value.account_id = p_account_id
667
+ ), grouped AS (
668
+ SELECT resource_kind, resource_id,
669
+ min(resource_version_id::text)::uuid AS resource_version_id,
670
+ action, array_agg(selection_source ORDER BY selection_source) AS selection_sources,
671
+ min(origin_workspace_id::text)::uuid AS origin_workspace_id,
672
+ min(authority_id::text)::uuid AS authority_id,
673
+ min(owner_organization_membership_id::text)::uuid
674
+ AS owner_organization_membership_id
675
+ FROM selected
676
+ GROUP BY resource_kind, resource_id, action
677
+ )
678
+ SELECT count(*)::integer,
679
+ array_agg(DISTINCT resource_kind ORDER BY resource_kind),
680
+ count(*) FILTER (
681
+ WHERE owner_organization_membership_id IS DISTINCT FROM member_row.id
682
+ )::integer,
683
+ coalesce(jsonb_agg(jsonb_build_object(
684
+ 'resource_kind', resource_kind,
685
+ 'resource_id', resource_id,
686
+ 'resource_version_id', resource_version_id,
687
+ 'action', action,
688
+ 'selection_sources', selection_sources,
689
+ 'origin_workspace_id', origin_workspace_id,
690
+ 'authority_id', authority_id,
691
+ 'owner_organization_membership_id', owner_organization_membership_id
692
+ ) ORDER BY resource_kind, resource_id), '[]'::jsonb)
693
+ INTO selected_count, kinds, unauthorized_count, selected_resources
694
+ FROM grouped;
695
+ IF selected_count = 0 THEN
696
+ RAISE EXCEPTION 'no personal Variable Set or Rig is selected by this session'
697
+ USING ERRCODE = '22023';
698
+ END IF;
699
+ IF selected_count > 27 THEN
700
+ RAISE EXCEPTION 'personal Variable Set/Rig closure exceeds the accepted-work bound'
701
+ USING ERRCODE = '22023';
702
+ END IF;
703
+ IF unauthorized_count <> 0 THEN
704
+ RAISE EXCEPTION 'a nonowner cannot attach another user''s personal resource'
705
+ USING ERRCODE = '42501';
706
+ END IF;
707
+
708
+ INSERT INTO turn_personal_resource_attachment_receipts (
709
+ turn_id, account_id, workspace_id, session_id,
710
+ initiating_human_subject_id, owner_organization_membership_id,
711
+ membership_authorization_revision, session_visibility,
712
+ session_authority_epoch, grant_mode, shared_output_warning_version,
713
+ shared_output_acknowledged, request_digest, resource_count
714
+ ) VALUES (
715
+ p_turn_id, p_account_id, p_workspace_id, p_session_id, caller_subject,
716
+ member_row.id, member_row.authorization_revision, session_row.visibility,
717
+ session_row.authority_epoch, p_mode, p_shared_output_warning_version,
718
+ p_workspace_shared_acknowledged IS TRUE, request_hash, selected_count
719
+ );
720
+
721
+ FOR resource_row IN
722
+ SELECT * FROM jsonb_to_recordset(selected_resources) AS selected(
723
+ resource_kind text,
724
+ resource_id uuid,
725
+ resource_version_id uuid,
726
+ action text,
727
+ selection_sources text[],
728
+ origin_workspace_id uuid,
729
+ authority_id uuid,
730
+ owner_organization_membership_id uuid
731
+ )
732
+ ORDER BY resource_kind, resource_id
733
+ LOOP
734
+ SELECT authority.* INTO STRICT authority_row
735
+ FROM organization_user_resource_authorities authority
736
+ WHERE authority.id = resource_row.authority_id
737
+ AND authority.account_id = p_account_id
738
+ AND authority.organization_membership_id = member_row.id
739
+ AND authority.resource_kind = resource_row.resource_kind
740
+ AND authority.resource_id = resource_row.resource_id
741
+ AND authority.status = 'active' AND authority.revoked_at IS NULL
742
+ FOR UPDATE;
743
+
744
+ UPDATE organization_user_resource_grants grant_value
745
+ SET status = 'expired', updated_at = clock_timestamp()
746
+ WHERE grant_value.account_id = p_account_id
747
+ AND grant_value.authority_id = authority_row.id
748
+ AND grant_value.status = 'active'
749
+ AND grant_value.expires_at IS NOT NULL
750
+ AND grant_value.expires_at <= clock_timestamp();
751
+
752
+ INSERT INTO organization_user_resource_grants (
753
+ account_id, authority_id, owner_organization_membership_id, workspace_id,
754
+ session_id, action, mode, context, authority_epoch, generation, status
755
+ ) VALUES (
756
+ p_account_id, authority_row.id, member_row.id, p_workspace_id,
757
+ CASE WHEN p_mode = 'always' THEN NULL ELSE p_session_id END,
758
+ resource_row.action, p_mode, session_row.visibility,
759
+ CASE WHEN p_mode = 'always' THEN NULL ELSE session_row.authority_epoch END,
760
+ 1, 'active'
761
+ )
762
+ ON CONFLICT (account_id, authority_id, workspace_id, action, mode, context,
763
+ (coalesce(session_id, '00000000-0000-0000-0000-000000000000'::uuid)),
764
+ (coalesce(authority_epoch, 0))) WHERE status = 'active'
765
+ DO UPDATE SET updated_at = organization_user_resource_grants.updated_at
766
+ RETURNING * INTO grant_row;
767
+
768
+ canonical := jsonb_build_object(
769
+ 'authorityId', authority_row.id, 'grantId', grant_row.id,
770
+ 'organizationId', p_account_id, 'workspaceId', p_workspace_id,
771
+ 'sessionId', grant_row.session_id, 'action', resource_row.action,
772
+ 'mode', grant_row.mode, 'context', grant_row.context,
773
+ 'authorityEpoch', grant_row.authority_epoch,
774
+ 'authorityGeneration', authority_row.generation,
775
+ 'grantGeneration', grant_row.generation,
776
+ 'resourceVersionId', resource_row.resource_version_id
777
+ );
778
+ canonical := canonical - CASE WHEN resource_row.resource_version_id IS NULL
779
+ THEN 'resourceVersionId' ELSE '__not_present__' END;
780
+
781
+ INSERT INTO turn_personal_resource_snapshots (
782
+ turn_id, account_id, workspace_id, session_id, resource_kind, resource_id,
783
+ resource_version_id, selection_sources, action, origin_workspace_id,
784
+ owner_organization_membership_id, membership_authorization_revision,
785
+ authority_id, authority_generation, grant_id, grant_generation, grant_mode,
786
+ grant_context, grant_session_id, grant_authority_epoch,
787
+ canonical_delegation, snapshot_digest
788
+ ) VALUES (
789
+ p_turn_id, p_account_id, p_workspace_id, p_session_id,
790
+ resource_row.resource_kind, resource_row.resource_id,
791
+ resource_row.resource_version_id, resource_row.selection_sources,
792
+ resource_row.action, resource_row.origin_workspace_id, member_row.id,
793
+ member_row.authorization_revision, authority_row.id, authority_row.generation,
794
+ grant_row.id, grant_row.generation, grant_row.mode, grant_row.context,
795
+ grant_row.session_id, grant_row.authority_epoch, canonical,
796
+ decode('', 'hex')
797
+ );
798
+
799
+ IF p_mode = 'once' THEN
800
+ UPDATE organization_user_resource_grants
801
+ SET status = 'consumed', updated_at = clock_timestamp()
802
+ WHERE id = grant_row.id AND account_id = p_account_id
803
+ AND generation = grant_row.generation AND status = 'active';
804
+ IF NOT FOUND THEN
805
+ RAISE EXCEPTION 'once grant lost its accepted-turn race' USING ERRCODE = '40001';
806
+ END IF;
807
+ INSERT INTO turn_personal_resource_once_receipts (
808
+ grant_id, turn_id, account_id, authority_id,
809
+ authority_generation, grant_generation
810
+ ) VALUES (
811
+ grant_row.id, p_turn_id, p_account_id, authority_row.id,
812
+ authority_row.generation, grant_row.generation
813
+ );
814
+ END IF;
815
+ END LOOP;
816
+
817
+ UPDATE session_turns
818
+ SET personal_resource_attachment_summary = jsonb_build_object(
819
+ 'mode', p_mode, 'context', session_row.visibility,
820
+ 'resourceCount', selected_count, 'resourceKinds', to_jsonb(kinds),
821
+ 'sharedOutputWarningVersion', p_shared_output_warning_version
822
+ ),
823
+ personal_resource_protocol_version = 1,
824
+ updated_at = clock_timestamp()
825
+ WHERE id = p_turn_id AND account_id = p_account_id AND workspace_id = p_workspace_id;
826
+
827
+ INSERT INTO audit_events (
828
+ account_id, workspace_id, subject_id, action, target_type, target_id,
829
+ metadata, metadata_codec_version
830
+ ) VALUES (
831
+ p_account_id, p_workspace_id, caller_subject,
832
+ 'session.personal_resources.attach', 'session_turn', p_turn_id::text,
833
+ jsonb_build_object(
834
+ 'sessionId', p_session_id, 'turnId', p_turn_id, 'mode', p_mode,
835
+ 'context', session_row.visibility, 'authorityEpoch', session_row.authority_epoch,
836
+ 'sharedOutputWarningVersion', p_shared_output_warning_version,
837
+ 'sharedOutputAcknowledged', p_workspace_shared_acknowledged IS TRUE,
838
+ 'resourceCount', selected_count, 'resourceKinds', to_jsonb(kinds)
839
+ ), 1
840
+ );
841
+
842
+ grant_mode := p_mode;
843
+ grant_context := session_row.visibility;
844
+ resource_count := selected_count;
845
+ resource_kinds := kinds;
846
+ shared_output_warning_version := p_shared_output_warning_version;
847
+ replay := false;
848
+ RETURN NEXT;
849
+ END
850
+ $body$;
851
+
852
+ REVOKE ALL ON FUNCTION accept_turn_personal_resource_attachment(
853
+ uuid, uuid, uuid, uuid, text, integer, boolean, integer
854
+ ) FROM PUBLIC;
855
+
856
+ -- Keep legacy attempt selection only for version-0 turns. In particular it no
857
+ -- longer has any path that can consume an unbound active once grant.
858
+ DO $legacy_personal_resource_trigger_fence$
859
+ DECLARE data_schema text := current_schema();
860
+ BEGIN
861
+ EXECUTE format(
862
+ 'DROP TRIGGER IF EXISTS session_attempt_personal_resource_admission ON %I.session_turn_attempts',
863
+ data_schema
864
+ );
865
+ EXECUTE format(
866
+ 'CREATE TRIGGER session_attempt_personal_resource_admission_legacy '
867
+ || 'AFTER INSERT ON %I.session_turn_attempts FOR EACH ROW '
868
+ || 'WHEN (NEW.personal_resource_protocol_version = 0) '
869
+ || 'EXECUTE FUNCTION %I.admit_session_attempt_personal_resources()',
870
+ data_schema, data_schema
871
+ );
872
+ END
873
+ $legacy_personal_resource_trigger_fence$;
874
+
875
+ -- The v1 trigger and resolver are appended below after the legacy function is
876
+ -- renamed; keeping them in this migration makes the cutover indivisible.
877
+
878
+ CREATE OR REPLACE FUNCTION admit_session_attempt_personal_resources_v1()
879
+ RETURNS trigger
880
+ LANGUAGE plpgsql
881
+ SECURITY DEFINER
882
+ SET search_path FROM CURRENT
883
+ AS $body$
884
+ DECLARE
885
+ session_row sessions%ROWTYPE;
886
+ turn_row session_turns%ROWTYPE;
887
+ receipt_row turn_personal_resource_attachment_receipts%ROWTYPE;
888
+ copied_count integer;
889
+ invalid_count integer;
890
+ BEGIN
891
+ INSERT INTO opengeni_private.personal_resource_delegation_capabilities (
892
+ backend_pid, transaction_id, capability_kind
893
+ ) VALUES (pg_backend_pid(), pg_current_xact_id(), 'admit')
894
+ ON CONFLICT DO NOTHING;
895
+
896
+ SELECT session_value.* INTO STRICT session_row
897
+ FROM sessions session_value
898
+ WHERE session_value.id = NEW.session_id
899
+ AND session_value.account_id = NEW.account_id
900
+ AND session_value.workspace_id = NEW.workspace_id
901
+ FOR SHARE;
902
+ SELECT turn_value.* INTO STRICT turn_row
903
+ FROM session_turns turn_value
904
+ WHERE turn_value.id = NEW.turn_id
905
+ AND turn_value.account_id = NEW.account_id
906
+ AND turn_value.workspace_id = NEW.workspace_id
907
+ AND turn_value.session_id = NEW.session_id
908
+ FOR SHARE;
909
+ SELECT receipt.* INTO STRICT receipt_row
910
+ FROM turn_personal_resource_attachment_receipts receipt
911
+ WHERE receipt.turn_id = NEW.turn_id
912
+ AND receipt.account_id = NEW.account_id
913
+ AND receipt.workspace_id = NEW.workspace_id
914
+ AND receipt.session_id = NEW.session_id;
915
+
916
+ IF NEW.execution_generation <= 0
917
+ OR NEW.state NOT IN ('claimed', 'running')
918
+ OR NEW.closed_at IS NOT NULL OR NEW.quiesced_at IS NOT NULL
919
+ OR session_row.active_turn_id IS DISTINCT FROM NEW.turn_id
920
+ OR turn_row.active_attempt_id IS DISTINCT FROM NEW.id
921
+ OR turn_row.execution_generation IS DISTINCT FROM NEW.execution_generation
922
+ OR turn_row.status <> 'running'
923
+ OR turn_row.personal_resource_protocol_version <> 1
924
+ OR NEW.personal_resource_protocol_version <> 1
925
+ OR NEW.authority_visibility IS DISTINCT FROM session_row.visibility
926
+ OR NEW.authority_epoch IS DISTINCT FROM session_row.authority_epoch
927
+ OR NEW.authority_owner_organization_membership_id
928
+ IS DISTINCT FROM session_row.owner_organization_membership_id
929
+ OR receipt_row.session_visibility IS DISTINCT FROM session_row.visibility
930
+ OR receipt_row.session_authority_epoch IS DISTINCT FROM session_row.authority_epoch
931
+ OR receipt_row.initiating_human_subject_id IS DISTINCT FROM coalesce(
932
+ nullif(btrim(turn_row.initiating_human_subject_id), ''),
933
+ CASE WHEN turn_row.initiator_kind = 'subject'
934
+ THEN nullif(btrim(turn_row.initiator_subject_id), '') END
935
+ )
936
+ THEN
937
+ RAISE EXCEPTION 'turn-bound personal-resource admission requires the exact current attempt'
938
+ USING ERRCODE = '42501';
939
+ END IF;
940
+
941
+ SELECT count(*)::integer INTO invalid_count
942
+ FROM turn_personal_resource_snapshots snapshot
943
+ WHERE snapshot.turn_id = NEW.turn_id
944
+ AND NOT (
945
+ snapshot.account_id = NEW.account_id
946
+ AND snapshot.workspace_id = NEW.workspace_id
947
+ AND snapshot.session_id = NEW.session_id
948
+ AND snapshot.owner_organization_membership_id
949
+ = receipt_row.owner_organization_membership_id
950
+ AND snapshot.membership_authorization_revision
951
+ = receipt_row.membership_authorization_revision
952
+ AND snapshot.grant_context = receipt_row.session_visibility
953
+ AND snapshot.grant_mode = receipt_row.grant_mode
954
+ AND EXISTS (
955
+ SELECT 1 FROM organization_memberships membership
956
+ WHERE membership.id = snapshot.owner_organization_membership_id
957
+ AND membership.account_id = snapshot.account_id
958
+ AND membership.subject_id = receipt_row.initiating_human_subject_id
959
+ AND membership.status = 'active' AND membership.revoked_at IS NULL
960
+ AND membership.authorization_revision = snapshot.membership_authorization_revision
961
+ AND (membership.personal_workspace_id = snapshot.workspace_id OR EXISTS (
962
+ SELECT 1 FROM workspace_memberships workspace_membership
963
+ WHERE workspace_membership.account_id = membership.account_id
964
+ AND workspace_membership.workspace_id = snapshot.workspace_id
965
+ AND workspace_membership.subject_id = membership.subject_id
966
+ ))
967
+ )
968
+ AND EXISTS (
969
+ SELECT 1 FROM organization_user_resource_authorities authority
970
+ WHERE authority.id = snapshot.authority_id
971
+ AND authority.account_id = snapshot.account_id
972
+ AND authority.organization_membership_id = snapshot.owner_organization_membership_id
973
+ AND authority.resource_kind = snapshot.resource_kind
974
+ AND authority.resource_id = snapshot.resource_id
975
+ AND authority.generation = snapshot.authority_generation
976
+ AND authority.status = 'active' AND authority.revoked_at IS NULL
977
+ )
978
+ AND EXISTS (
979
+ SELECT 1 FROM organization_user_resource_grants grant_value
980
+ WHERE grant_value.id = snapshot.grant_id
981
+ AND grant_value.account_id = snapshot.account_id
982
+ AND grant_value.authority_id = snapshot.authority_id
983
+ AND grant_value.owner_organization_membership_id
984
+ = snapshot.owner_organization_membership_id
985
+ AND grant_value.workspace_id = snapshot.workspace_id
986
+ AND grant_value.action = snapshot.action
987
+ AND grant_value.mode = snapshot.grant_mode
988
+ AND grant_value.context = snapshot.grant_context
989
+ AND grant_value.generation = snapshot.grant_generation
990
+ AND grant_value.session_id IS NOT DISTINCT FROM snapshot.grant_session_id
991
+ AND grant_value.authority_epoch IS NOT DISTINCT FROM snapshot.grant_authority_epoch
992
+ AND (grant_value.expires_at IS NULL OR grant_value.expires_at > clock_timestamp())
993
+ AND ((snapshot.grant_mode = 'once' AND grant_value.status = 'consumed'
994
+ AND EXISTS (
995
+ SELECT 1 FROM turn_personal_resource_once_receipts once_receipt
996
+ WHERE once_receipt.grant_id = snapshot.grant_id
997
+ AND once_receipt.turn_id = snapshot.turn_id
998
+ AND once_receipt.account_id = snapshot.account_id
999
+ AND once_receipt.authority_id = snapshot.authority_id
1000
+ AND once_receipt.authority_generation = snapshot.authority_generation
1001
+ AND once_receipt.grant_generation = snapshot.grant_generation
1002
+ )) OR (snapshot.grant_mode IN ('session', 'always')
1003
+ AND grant_value.status = 'active'))
1004
+ )
1005
+ AND snapshot.snapshot_digest = digest_turn_personal_resource_snapshot(snapshot)
1006
+ );
1007
+ IF invalid_count <> 0 THEN
1008
+ RAISE EXCEPTION 'turn-bound personal-resource snapshot is no longer live'
1009
+ USING ERRCODE = '42501';
1010
+ END IF;
1011
+
1012
+ INSERT INTO session_attempt_personal_resource_admissions (
1013
+ attempt_id, account_id, workspace_id, session_id, turn_id,
1014
+ execution_generation, initiating_human_subject_id,
1015
+ owner_organization_membership_id, membership_authorization_revision,
1016
+ session_visibility, session_authority_epoch, resource_count
1017
+ ) VALUES (
1018
+ NEW.id, NEW.account_id, NEW.workspace_id, NEW.session_id, NEW.turn_id,
1019
+ NEW.execution_generation, receipt_row.initiating_human_subject_id,
1020
+ receipt_row.owner_organization_membership_id,
1021
+ receipt_row.membership_authorization_revision,
1022
+ receipt_row.session_visibility, receipt_row.session_authority_epoch,
1023
+ receipt_row.resource_count
1024
+ );
1025
+
1026
+ INSERT INTO session_attempt_personal_resource_snapshots (
1027
+ attempt_id, account_id, workspace_id, session_id, turn_id,
1028
+ execution_generation, resource_kind, resource_id, resource_version_id,
1029
+ selection_sources, action, origin_workspace_id,
1030
+ owner_organization_membership_id, membership_authorization_revision,
1031
+ authority_id, authority_generation, target_workspace_id,
1032
+ session_visibility, session_authority_epoch, grant_id, grant_generation,
1033
+ grant_mode, grant_context, grant_session_id, grant_authority_epoch
1034
+ )
1035
+ SELECT NEW.id, snapshot.account_id, snapshot.workspace_id, snapshot.session_id,
1036
+ snapshot.turn_id, NEW.execution_generation, snapshot.resource_kind,
1037
+ snapshot.resource_id, snapshot.resource_version_id, snapshot.selection_sources,
1038
+ snapshot.action, snapshot.origin_workspace_id,
1039
+ snapshot.owner_organization_membership_id,
1040
+ snapshot.membership_authorization_revision, snapshot.authority_id,
1041
+ snapshot.authority_generation, snapshot.workspace_id,
1042
+ receipt_row.session_visibility, receipt_row.session_authority_epoch,
1043
+ snapshot.grant_id, snapshot.grant_generation, snapshot.grant_mode,
1044
+ snapshot.grant_context, snapshot.grant_session_id, snapshot.grant_authority_epoch
1045
+ FROM turn_personal_resource_snapshots snapshot
1046
+ WHERE snapshot.turn_id = NEW.turn_id
1047
+ ORDER BY snapshot.resource_kind, snapshot.resource_id;
1048
+ GET DIAGNOSTICS copied_count = ROW_COUNT;
1049
+ IF copied_count <> receipt_row.resource_count THEN
1050
+ RAISE EXCEPTION 'turn-bound personal-resource snapshot collection is incomplete'
1051
+ USING ERRCODE = '42501';
1052
+ END IF;
1053
+
1054
+ DELETE FROM opengeni_private.personal_resource_delegation_capabilities
1055
+ WHERE backend_pid = pg_backend_pid()
1056
+ AND transaction_id = pg_current_xact_id_if_assigned()
1057
+ AND capability_kind = 'admit';
1058
+ RETURN NEW;
1059
+ EXCEPTION WHEN OTHERS THEN
1060
+ DELETE FROM opengeni_private.personal_resource_delegation_capabilities
1061
+ WHERE backend_pid = pg_backend_pid()
1062
+ AND transaction_id = pg_current_xact_id_if_assigned()
1063
+ AND capability_kind = 'admit';
1064
+ RAISE;
1065
+ END
1066
+ $body$;
1067
+
1068
+ DO $v1_personal_resource_trigger_install$
1069
+ DECLARE data_schema text := current_schema();
1070
+ BEGIN
1071
+ EXECUTE format(
1072
+ 'CREATE TRIGGER session_attempt_personal_resource_admission_v1 '
1073
+ || 'AFTER INSERT ON %I.session_turn_attempts FOR EACH ROW '
1074
+ || 'WHEN (NEW.personal_resource_protocol_version = 1) '
1075
+ || 'EXECUTE FUNCTION %I.admit_session_attempt_personal_resources_v1()',
1076
+ data_schema, data_schema
1077
+ );
1078
+ END
1079
+ $v1_personal_resource_trigger_install$;
1080
+
1081
+ ALTER FUNCTION resolve_session_attempt_personal_resources(uuid, uuid, uuid)
1082
+ RENAME TO resolve_session_attempt_personal_resources_legacy_0305;
1083
+
1084
+ CREATE FUNCTION resolve_session_attempt_personal_resources(
1085
+ p_account_id uuid,
1086
+ p_workspace_id uuid,
1087
+ p_attempt_id uuid
1088
+ ) RETURNS TABLE (
1089
+ resource_kind text,
1090
+ resource_id uuid,
1091
+ resource_version_id uuid,
1092
+ selection_sources text[],
1093
+ action text,
1094
+ authority_id uuid,
1095
+ authority_generation bigint,
1096
+ grant_id uuid,
1097
+ grant_generation bigint,
1098
+ grant_mode text
1099
+ )
1100
+ LANGUAGE plpgsql
1101
+ SECURITY DEFINER
1102
+ SET search_path FROM CURRENT
1103
+ AS $body$
1104
+ #variable_conflict use_column
1105
+ DECLARE
1106
+ attempt_protocol integer;
1107
+ admission_row session_attempt_personal_resource_admissions%ROWTYPE;
1108
+ invalid_count integer;
1109
+ caller_subject text := coalesce(
1110
+ nullif(current_setting('opengeni.initiating_human_subject_id', true), ''),
1111
+ nullif(current_setting('opengeni.subject_id', true), '')
1112
+ );
1113
+ BEGIN
1114
+ IF p_account_id IS DISTINCT FROM nullif(current_setting('opengeni.account_id', true), '')::uuid
1115
+ OR p_workspace_id IS DISTINCT FROM nullif(
1116
+ current_setting('opengeni.workspace_id', true), ''
1117
+ )::uuid
1118
+ THEN
1119
+ RAISE EXCEPTION 'personal-resource resolve scope mismatch' USING ERRCODE = '42501';
1120
+ END IF;
1121
+ SELECT attempt.personal_resource_protocol_version INTO STRICT attempt_protocol
1122
+ FROM session_turn_attempts attempt
1123
+ WHERE attempt.id = p_attempt_id
1124
+ AND attempt.account_id = p_account_id
1125
+ AND attempt.workspace_id = p_workspace_id;
1126
+ IF attempt_protocol = 0 THEN
1127
+ RETURN QUERY SELECT * FROM resolve_session_attempt_personal_resources_legacy_0305(
1128
+ p_account_id, p_workspace_id, p_attempt_id
1129
+ );
1130
+ RETURN;
1131
+ ELSIF attempt_protocol <> 1 THEN
1132
+ RAISE EXCEPTION 'unsupported personal-resource protocol version'
1133
+ USING ERRCODE = '42501';
1134
+ END IF;
1135
+
1136
+ INSERT INTO opengeni_private.personal_resource_delegation_capabilities (
1137
+ backend_pid, transaction_id, capability_kind
1138
+ ) VALUES (pg_backend_pid(), pg_current_xact_id(), 'resolve')
1139
+ ON CONFLICT DO NOTHING;
1140
+ SELECT admission.* INTO STRICT admission_row
1141
+ FROM session_attempt_personal_resource_admissions admission
1142
+ WHERE admission.attempt_id = p_attempt_id
1143
+ AND admission.account_id = p_account_id
1144
+ AND admission.workspace_id = p_workspace_id;
1145
+ IF caller_subject IS DISTINCT FROM admission_row.initiating_human_subject_id THEN
1146
+ RAISE EXCEPTION 'personal-resource resolve initiating human mismatch'
1147
+ USING ERRCODE = '42501';
1148
+ END IF;
1149
+
1150
+ PERFORM 1
1151
+ FROM session_turn_attempts attempt
1152
+ JOIN sessions session_value
1153
+ ON session_value.id = attempt.session_id
1154
+ AND session_value.account_id = attempt.account_id
1155
+ AND session_value.workspace_id = attempt.workspace_id
1156
+ JOIN session_turns turn_value
1157
+ ON turn_value.id = attempt.turn_id
1158
+ AND turn_value.account_id = attempt.account_id
1159
+ AND turn_value.workspace_id = attempt.workspace_id
1160
+ AND turn_value.session_id = attempt.session_id
1161
+ WHERE attempt.id = admission_row.attempt_id
1162
+ AND session_value.active_turn_id = admission_row.turn_id
1163
+ AND turn_value.active_attempt_id = admission_row.attempt_id
1164
+ AND turn_value.execution_generation = admission_row.execution_generation
1165
+ AND turn_value.personal_resource_protocol_version = 1
1166
+ AND turn_value.status = 'running'
1167
+ AND attempt.execution_generation = admission_row.execution_generation
1168
+ AND attempt.personal_resource_protocol_version = 1
1169
+ AND attempt.state IN ('claimed', 'running')
1170
+ AND attempt.closed_at IS NULL AND attempt.quiesced_at IS NULL
1171
+ AND attempt.authority_visibility = admission_row.session_visibility
1172
+ AND attempt.authority_epoch = admission_row.session_authority_epoch
1173
+ AND session_value.visibility = admission_row.session_visibility
1174
+ AND session_value.authority_epoch = admission_row.session_authority_epoch
1175
+ AND NOT EXISTS (
1176
+ SELECT 1 FROM session_attempt_interruptions interruption
1177
+ WHERE interruption.account_id = attempt.account_id
1178
+ AND interruption.workspace_id = attempt.workspace_id
1179
+ AND interruption.session_id = attempt.session_id
1180
+ AND interruption.attempt_id = attempt.id
1181
+ AND interruption.state IN ('pending', 'delivered', 'acknowledged')
1182
+ )
1183
+ FOR SHARE OF session_value, turn_value
1184
+ FOR UPDATE OF attempt;
1185
+ IF NOT FOUND THEN
1186
+ RAISE EXCEPTION 'personal-resource resolve requires the exact current uninterrupted attempt'
1187
+ USING ERRCODE = '42501';
1188
+ END IF;
1189
+
1190
+ SELECT count(*)::integer INTO invalid_count
1191
+ FROM session_attempt_personal_resource_snapshots snapshot
1192
+ JOIN turn_personal_resource_snapshots accepted
1193
+ ON accepted.turn_id = snapshot.turn_id
1194
+ AND accepted.resource_kind = snapshot.resource_kind
1195
+ AND accepted.resource_id = snapshot.resource_id
1196
+ WHERE snapshot.attempt_id = admission_row.attempt_id
1197
+ AND NOT (
1198
+ snapshot.account_id = accepted.account_id
1199
+ AND snapshot.workspace_id = accepted.workspace_id
1200
+ AND snapshot.session_id = accepted.session_id
1201
+ AND snapshot.resource_version_id IS NOT DISTINCT FROM accepted.resource_version_id
1202
+ AND snapshot.selection_sources = accepted.selection_sources
1203
+ AND snapshot.action = accepted.action
1204
+ AND snapshot.origin_workspace_id = accepted.origin_workspace_id
1205
+ AND snapshot.owner_organization_membership_id
1206
+ = accepted.owner_organization_membership_id
1207
+ AND snapshot.membership_authorization_revision
1208
+ = accepted.membership_authorization_revision
1209
+ AND snapshot.authority_id = accepted.authority_id
1210
+ AND snapshot.authority_generation = accepted.authority_generation
1211
+ AND snapshot.grant_id = accepted.grant_id
1212
+ AND snapshot.grant_generation = accepted.grant_generation
1213
+ AND snapshot.grant_mode = accepted.grant_mode
1214
+ AND snapshot.grant_context = accepted.grant_context
1215
+ AND snapshot.grant_session_id IS NOT DISTINCT FROM accepted.grant_session_id
1216
+ AND snapshot.grant_authority_epoch IS NOT DISTINCT FROM accepted.grant_authority_epoch
1217
+ AND accepted.snapshot_digest = digest_turn_personal_resource_snapshot(accepted)
1218
+ AND EXISTS (
1219
+ SELECT 1 FROM organization_memberships membership
1220
+ WHERE membership.id = accepted.owner_organization_membership_id
1221
+ AND membership.account_id = accepted.account_id
1222
+ AND membership.subject_id = admission_row.initiating_human_subject_id
1223
+ AND membership.status = 'active' AND membership.revoked_at IS NULL
1224
+ AND membership.authorization_revision = accepted.membership_authorization_revision
1225
+ AND (membership.personal_workspace_id = accepted.workspace_id OR EXISTS (
1226
+ SELECT 1 FROM workspace_memberships workspace_membership
1227
+ WHERE workspace_membership.account_id = membership.account_id
1228
+ AND workspace_membership.workspace_id = accepted.workspace_id
1229
+ AND workspace_membership.subject_id = membership.subject_id
1230
+ ))
1231
+ )
1232
+ AND EXISTS (
1233
+ SELECT 1 FROM organization_user_resource_authorities authority
1234
+ WHERE authority.id = accepted.authority_id
1235
+ AND authority.account_id = accepted.account_id
1236
+ AND authority.organization_membership_id = accepted.owner_organization_membership_id
1237
+ AND authority.resource_kind = accepted.resource_kind
1238
+ AND authority.resource_id = accepted.resource_id
1239
+ AND authority.generation = accepted.authority_generation
1240
+ AND authority.status = 'active' AND authority.revoked_at IS NULL
1241
+ )
1242
+ AND EXISTS (
1243
+ SELECT 1 FROM organization_user_resource_grants grant_value
1244
+ WHERE grant_value.id = accepted.grant_id
1245
+ AND grant_value.account_id = accepted.account_id
1246
+ AND grant_value.authority_id = accepted.authority_id
1247
+ AND grant_value.owner_organization_membership_id
1248
+ = accepted.owner_organization_membership_id
1249
+ AND grant_value.workspace_id = accepted.workspace_id
1250
+ AND grant_value.action = accepted.action
1251
+ AND grant_value.mode = accepted.grant_mode
1252
+ AND grant_value.context = accepted.grant_context
1253
+ AND grant_value.generation = accepted.grant_generation
1254
+ AND grant_value.session_id IS NOT DISTINCT FROM accepted.grant_session_id
1255
+ AND grant_value.authority_epoch IS NOT DISTINCT FROM accepted.grant_authority_epoch
1256
+ AND (grant_value.expires_at IS NULL OR grant_value.expires_at > clock_timestamp())
1257
+ AND ((accepted.grant_mode = 'once' AND grant_value.status = 'consumed'
1258
+ AND EXISTS (
1259
+ SELECT 1 FROM turn_personal_resource_once_receipts once_receipt
1260
+ WHERE once_receipt.grant_id = accepted.grant_id
1261
+ AND once_receipt.turn_id = accepted.turn_id
1262
+ AND once_receipt.account_id = accepted.account_id
1263
+ AND once_receipt.authority_id = accepted.authority_id
1264
+ AND once_receipt.authority_generation = accepted.authority_generation
1265
+ AND once_receipt.grant_generation = accepted.grant_generation
1266
+ )) OR (accepted.grant_mode IN ('session', 'always')
1267
+ AND grant_value.status = 'active'))
1268
+ )
1269
+ );
1270
+ IF invalid_count <> 0 THEN
1271
+ RAISE EXCEPTION 'personal-resource authority snapshot is no longer live'
1272
+ USING ERRCODE = '42501';
1273
+ END IF;
1274
+ SELECT count(*)::integer INTO invalid_count
1275
+ FROM session_attempt_personal_resource_snapshots snapshot
1276
+ WHERE snapshot.attempt_id = admission_row.attempt_id;
1277
+ IF invalid_count <> admission_row.resource_count THEN
1278
+ RAISE EXCEPTION 'personal-resource snapshot collection is incomplete'
1279
+ USING ERRCODE = '42501';
1280
+ END IF;
1281
+
1282
+ RETURN QUERY
1283
+ SELECT snapshot.resource_kind, snapshot.resource_id, snapshot.resource_version_id,
1284
+ snapshot.selection_sources, snapshot.action, snapshot.authority_id,
1285
+ snapshot.authority_generation, snapshot.grant_id,
1286
+ snapshot.grant_generation, snapshot.grant_mode
1287
+ FROM session_attempt_personal_resource_snapshots snapshot
1288
+ WHERE snapshot.attempt_id = admission_row.attempt_id
1289
+ ORDER BY snapshot.resource_kind, snapshot.resource_id;
1290
+
1291
+ DELETE FROM opengeni_private.personal_resource_delegation_capabilities
1292
+ WHERE backend_pid = pg_backend_pid()
1293
+ AND transaction_id = pg_current_xact_id_if_assigned()
1294
+ AND capability_kind = 'resolve';
1295
+ RETURN;
1296
+ EXCEPTION WHEN OTHERS THEN
1297
+ DELETE FROM opengeni_private.personal_resource_delegation_capabilities
1298
+ WHERE backend_pid = pg_backend_pid()
1299
+ AND transaction_id = pg_current_xact_id_if_assigned()
1300
+ AND capability_kind = 'resolve';
1301
+ RAISE;
1302
+ END
1303
+ $body$;
1304
+
1305
+ REVOKE ALL ON FUNCTION admit_session_attempt_personal_resources_v1() FROM PUBLIC;
1306
+ REVOKE ALL ON FUNCTION resolve_session_attempt_personal_resources(uuid, uuid, uuid) FROM PUBLIC;
1307
+ REVOKE ALL ON FUNCTION resolve_session_attempt_personal_resources_legacy_0305(
1308
+ uuid, uuid, uuid
1309
+ ) FROM PUBLIC;
1310
+
1311
+ DO $atomic_personal_resource_function_hardening$
1312
+ DECLARE data_schema text := current_schema();
1313
+ BEGIN
1314
+ EXECUTE format(
1315
+ 'ALTER FUNCTION %I.digest_turn_personal_resource_snapshot('
1316
+ || '%I.turn_personal_resource_snapshots) '
1317
+ || 'SET search_path TO pg_catalog, %I, pg_temp',
1318
+ data_schema, data_schema, data_schema
1319
+ );
1320
+ EXECUTE format(
1321
+ 'ALTER FUNCTION %I.stamp_turn_personal_resource_snapshot_digest() '
1322
+ || 'SET search_path TO pg_catalog, %I, pg_temp',
1323
+ data_schema, data_schema
1324
+ );
1325
+ EXECUTE format(
1326
+ 'ALTER FUNCTION %I.reject_turn_personal_resource_snapshot_update() '
1327
+ || 'SET search_path TO pg_catalog, %I, pg_temp',
1328
+ data_schema, data_schema
1329
+ );
1330
+ EXECUTE format(
1331
+ 'ALTER FUNCTION %I.fence_turn_personal_resource_protocol_update() '
1332
+ || 'SET search_path TO pg_catalog, %I, pg_temp',
1333
+ data_schema, data_schema
1334
+ );
1335
+ EXECUTE format(
1336
+ 'ALTER FUNCTION %I.fence_attempt_personal_resource_protocol_update() '
1337
+ || 'SET search_path TO pg_catalog, %I, pg_temp',
1338
+ data_schema, data_schema
1339
+ );
1340
+ EXECUTE format(
1341
+ 'ALTER FUNCTION %I.fence_session_initial_personal_resource_intent_update() '
1342
+ || 'SET search_path TO pg_catalog, %I, pg_temp',
1343
+ data_schema, data_schema
1344
+ );
1345
+ EXECUTE format(
1346
+ 'ALTER FUNCTION %I.accept_turn_personal_resource_attachment('
1347
+ || 'uuid,uuid,uuid,uuid,text,integer,boolean,integer) '
1348
+ || 'SET search_path TO pg_catalog, %I, pg_temp',
1349
+ data_schema, data_schema
1350
+ );
1351
+ EXECUTE format(
1352
+ 'ALTER FUNCTION %I.admit_session_attempt_personal_resources_v1() '
1353
+ || 'SET search_path TO pg_catalog, %I, pg_temp',
1354
+ data_schema, data_schema
1355
+ );
1356
+ EXECUTE format(
1357
+ 'ALTER FUNCTION %I.resolve_session_attempt_personal_resources(uuid,uuid,uuid) '
1358
+ || 'SET search_path TO pg_catalog, %I, pg_temp',
1359
+ data_schema, data_schema
1360
+ );
1361
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
1362
+ EXECUTE format(
1363
+ 'GRANT EXECUTE ON FUNCTION %I.accept_turn_personal_resource_attachment('
1364
+ || 'uuid,uuid,uuid,uuid,text,integer,boolean,integer) TO opengeni_app',
1365
+ data_schema
1366
+ );
1367
+ EXECUTE format(
1368
+ 'GRANT EXECUTE ON FUNCTION %I.resolve_session_attempt_personal_resources('
1369
+ || 'uuid,uuid,uuid) TO opengeni_app',
1370
+ data_schema
1371
+ );
1372
+ END IF;
1373
+ END
1374
+ $atomic_personal_resource_function_hardening$;