@opengeni/db 1.0.1 → 3.2.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 (271) 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-4SR4HKKA.js → chunk-2EH4WRHL.js} +19 -11
  7. package/dist/chunk-2EH4WRHL.js.map +1 -0
  8. package/dist/{chunk-CUPM3FGB.js → chunk-2PRS4J5Q.js} +3 -3
  9. package/dist/chunk-6HMM2R5T.js +390 -0
  10. package/dist/chunk-6HMM2R5T.js.map +1 -0
  11. package/dist/{chunk-ZWREYEWA.js → chunk-AZHW7R5V.js} +56 -11
  12. package/dist/chunk-AZHW7R5V.js.map +1 -0
  13. package/dist/{chunk-LTG3S6ZY.js → chunk-E6CLMKEE.js} +2 -2
  14. package/dist/{chunk-6ZYKCRFO.js → chunk-KRLPEMLH.js} +7591 -4417
  15. package/dist/chunk-KRLPEMLH.js.map +1 -0
  16. package/dist/chunk-NAB6FKFN.js +516 -0
  17. package/dist/chunk-NAB6FKFN.js.map +1 -0
  18. package/dist/{chunk-SK7YYHBI.js → chunk-SIBYBA2D.js} +46 -1
  19. package/dist/chunk-SIBYBA2D.js.map +1 -0
  20. package/dist/chunk-UZNXG44K.js +4508 -0
  21. package/dist/chunk-UZNXG44K.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 +85 -0
  33. package/dist/connection-token-resolver.d.ts +30 -1
  34. package/dist/database.d.ts +23 -4
  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 +1606 -81
  43. package/dist/index.js +21898 -8147
  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 +29 -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 +43 -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 +268 -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 +28 -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/drizzle/0342_slack_route_prompt_single_pending.sql +32 -0
  189. package/drizzle/0343_personal_document_force_rls_lock_repair.sql +146 -0
  190. package/drizzle/0344_private_session_visibility_transition_gate.sql +85 -0
  191. package/drizzle/0345_tenant_scoped_session_tenancy_fence.sql +2839 -0
  192. package/drizzle/0346_document_migration_audit_surface.sql +454 -0
  193. package/drizzle/0347_connection_authority_convergence_evidence.sql +396 -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 +167 -0
  212. package/src/connection-token-resolver.ts +263 -31
  213. package/src/database.ts +87 -2
  214. package/src/editable-artifacts-schema.ts +10 -5
  215. package/src/editable-artifacts.ts +21 -11
  216. package/src/governed-learning-activation-schema.ts +121 -0
  217. package/src/governed-learning-activation.ts +588 -0
  218. package/src/governed-learning-evaluator-schema.ts +72 -0
  219. package/src/governed-learning-evaluator.ts +217 -0
  220. package/src/index.ts +11676 -1550
  221. package/src/insights.ts +107 -0
  222. package/src/integration-bindings.ts +88 -6
  223. package/src/integration-facets.ts +287 -84
  224. package/src/interaction-placement-loss.ts +234 -0
  225. package/src/managed-human-provisioning.ts +116 -0
  226. package/src/memory-domain.ts +27 -32
  227. package/src/migrate.ts +77 -1
  228. package/src/new-session-drafts.ts +229 -35
  229. package/src/organization-membership-backfill.ts +735 -0
  230. package/src/organization-membership-lifecycle.ts +995 -0
  231. package/src/pr-review.ts +890 -0
  232. package/src/preference-registry.ts +31 -1
  233. package/src/provision-roles.ts +905 -6
  234. package/src/runtime-posture-cli.ts +2 -0
  235. package/src/runtime-posture.ts +1317 -72
  236. package/src/schema.ts +3316 -428
  237. package/src/scoped-knowledge-schema.ts +16 -8
  238. package/src/scoped-knowledge.ts +1 -1
  239. package/src/session-background-commands.ts +759 -0
  240. package/src/session-control.ts +640 -74
  241. package/src/session-goal-pacing.ts +126 -0
  242. package/src/session-queue-commands.ts +441 -85
  243. package/src/session-realtime-context.ts +4 -6
  244. package/src/session-realtime-ledger.ts +28 -14
  245. package/src/session-realtime-mirror.ts +1 -1
  246. package/src/session-tenancy.ts +410 -49
  247. package/src/session-tool-call-settlement.ts +38 -29
  248. package/src/slack-routing-personal-workspace.ts +67 -0
  249. package/src/slack-routing.ts +867 -0
  250. package/src/slack-task-policy-schema.ts +5 -0
  251. package/src/slack-task-policy.ts +18 -0
  252. package/src/slack-user-link-access.ts +138 -11
  253. package/src/task-notes-schema.ts +74 -0
  254. package/src/task-notes.ts +124 -2
  255. package/src/turn-initiator.ts +8 -0
  256. package/src/user-resource-authority.ts +377 -0
  257. package/src/workspace-authority.ts +160 -0
  258. package/src/workspace-instruction-policies-schema.ts +53 -1
  259. package/src/workspace-instruction-policies.ts +528 -77
  260. package/src/workspace-learning-policy.ts +21 -5
  261. package/src/workspace-membership-permissions.ts +9 -0
  262. package/dist/chunk-4SR4HKKA.js.map +0 -1
  263. package/dist/chunk-6ZYKCRFO.js.map +0 -1
  264. package/dist/chunk-NXAFLCQX.js +0 -2468
  265. package/dist/chunk-NXAFLCQX.js.map +0 -1
  266. package/dist/chunk-SK7YYHBI.js.map +0 -1
  267. package/dist/chunk-WD5ROSLL.js +0 -117
  268. package/dist/chunk-WD5ROSLL.js.map +0 -1
  269. package/dist/chunk-ZWREYEWA.js.map +0 -1
  270. /package/dist/{chunk-CUPM3FGB.js.map → chunk-2PRS4J5Q.js.map} +0 -0
  271. /package/dist/{chunk-LTG3S6ZY.js.map → chunk-E6CLMKEE.js.map} +0 -0
@@ -0,0 +1,2839 @@
1
+ -- deployment-mode: maintenance
2
+ SET lock_timeout = '5s';
3
+ SET statement_timeout = '10min';
4
+
5
+ -- opengeni_private contains historical singleton worker routines, so one
6
+ -- database can support exactly one dedicated OpenGeni target schema. Bind the
7
+ -- protocol before any shared CREATE OR REPLACE or target-schema mutation. A
8
+ -- same-target migration-ledger replay is harmless; a different target must
9
+ -- stop before it can repin a global routine or install a partial helper set.
10
+ CREATE TABLE IF NOT EXISTS opengeni_private.session_tenancy_fence_target_registry (
11
+ singleton boolean NOT NULL,
12
+ target_schema regnamespace NOT NULL,
13
+ CONSTRAINT session_tenancy_fence_target_registry_pk PRIMARY KEY (singleton),
14
+ CONSTRAINT session_tenancy_fence_target_registry_singleton_chk CHECK (singleton)
15
+ );
16
+
17
+ REVOKE ALL ON TABLE
18
+ opengeni_private.session_tenancy_fence_target_registry
19
+ FROM PUBLIC;
20
+
21
+ DO $session_tenancy_fence_target_registry_contract$
22
+ DECLARE
23
+ registry_table oid := pg_catalog.to_regclass(
24
+ 'opengeni_private.session_tenancy_fence_target_registry'
25
+ );
26
+ actual_columns text[];
27
+ actual_constraints text[];
28
+ registered_target pg_catalog.regnamespace;
29
+ requested_target pg_catalog.regnamespace :=
30
+ pg_catalog.current_schema()::pg_catalog.regnamespace;
31
+ BEGIN
32
+ IF registry_table IS NULL OR NOT EXISTS (
33
+ SELECT 1 FROM pg_catalog.pg_class relation
34
+ WHERE relation.oid = registry_table
35
+ AND relation.relkind = 'r'
36
+ AND relation.relowner = current_user::pg_catalog.regrole
37
+ ) THEN
38
+ RAISE EXCEPTION '0345 session tenancy target registry owner/type drift'
39
+ USING ERRCODE = '55000';
40
+ END IF;
41
+ SELECT pg_catalog.array_agg(
42
+ attribute.attname || ':'
43
+ || pg_catalog.format_type(attribute.atttypid, attribute.atttypmod) || ':'
44
+ || attribute.attnotnull::text
45
+ ORDER BY attribute.attnum
46
+ ) INTO actual_columns
47
+ FROM pg_catalog.pg_attribute attribute
48
+ WHERE attribute.attrelid = registry_table
49
+ AND attribute.attnum > 0
50
+ AND NOT attribute.attisdropped;
51
+ IF actual_columns IS DISTINCT FROM ARRAY[
52
+ 'singleton:boolean:true',
53
+ 'target_schema:regnamespace:true'
54
+ ]::text[] THEN
55
+ RAISE EXCEPTION '0345 session tenancy target registry column drift'
56
+ USING ERRCODE = '55000', DETAIL = actual_columns::text;
57
+ END IF;
58
+ SELECT pg_catalog.array_agg(
59
+ constraint_value.conname || ':' || constraint_value.contype::text || ':'
60
+ || pg_catalog.pg_get_constraintdef(constraint_value.oid, false)
61
+ ORDER BY constraint_value.conname
62
+ ) INTO actual_constraints
63
+ FROM pg_catalog.pg_constraint constraint_value
64
+ WHERE constraint_value.conrelid = registry_table;
65
+ IF actual_constraints IS DISTINCT FROM ARRAY[
66
+ 'session_tenancy_fence_target_registry_pk:p:PRIMARY KEY (singleton)',
67
+ 'session_tenancy_fence_target_registry_singleton_chk:c:CHECK (singleton)'
68
+ ]::text[] THEN
69
+ RAISE EXCEPTION '0345 session tenancy target registry constraint drift'
70
+ USING ERRCODE = '55000', DETAIL = actual_constraints::text;
71
+ END IF;
72
+ IF EXISTS (
73
+ SELECT 1
74
+ FROM pg_catalog.pg_class relation
75
+ CROSS JOIN LATERAL pg_catalog.aclexplode(
76
+ COALESCE(
77
+ relation.relacl,
78
+ pg_catalog.acldefault('r', relation.relowner)
79
+ )
80
+ ) acl
81
+ WHERE relation.oid = registry_table
82
+ AND acl.grantee <> relation.relowner
83
+ ) THEN
84
+ RAISE EXCEPTION '0345 session tenancy target registry ACL drift'
85
+ USING ERRCODE = '55000';
86
+ END IF;
87
+
88
+ INSERT INTO opengeni_private.session_tenancy_fence_target_registry (
89
+ singleton, target_schema
90
+ ) VALUES (true, requested_target)
91
+ ON CONFLICT (singleton) DO NOTHING;
92
+
93
+ SELECT registry.target_schema
94
+ INTO registered_target
95
+ FROM opengeni_private.session_tenancy_fence_target_registry registry
96
+ WHERE registry.singleton
97
+ FOR UPDATE;
98
+ IF registered_target IS DISTINCT FROM requested_target THEN
99
+ RAISE EXCEPTION
100
+ '0345 session tenancy target schema is already bound to %, not %',
101
+ registered_target,
102
+ requested_target
103
+ USING ERRCODE = '55000';
104
+ END IF;
105
+ END
106
+ $session_tenancy_fence_target_registry_contract$;
107
+
108
+ -- This is a protocol cutover, not merely a convention in the new TypeScript
109
+ -- image. Every hot-table mutation must arrive with the workspace fence already
110
+ -- held; the trigger deliberately refuses to acquire it after PostgreSQL may
111
+ -- have taken a tuple lock. Stop every old API/worker before this migration and
112
+ -- never restart a pre-0345 image after it commits.
113
+ CREATE OR REPLACE FUNCTION opengeni_private.require_session_tenancy_fence()
114
+ RETURNS trigger
115
+ LANGUAGE plpgsql
116
+ SECURITY DEFINER
117
+ SET search_path = pg_catalog
118
+ AS $fence$
119
+ DECLARE
120
+ workspace_id_value uuid;
121
+ lock_key bigint;
122
+ BEGIN
123
+ IF EXISTS (
124
+ SELECT 1 FROM pg_roles role_value
125
+ WHERE role_value.rolname = session_user
126
+ AND (role_value.rolsuper OR role_value.rolbypassrls)
127
+ ) THEN
128
+ IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
129
+ RETURN NEW;
130
+ END IF;
131
+ IF TG_OP = 'DELETE' THEN
132
+ workspace_id_value := (to_jsonb(OLD) ->> 'workspace_id')::uuid;
133
+ ELSE
134
+ workspace_id_value := (to_jsonb(NEW) ->> 'workspace_id')::uuid;
135
+ END IF;
136
+ IF workspace_id_value IS NULL THEN
137
+ RAISE EXCEPTION 'session tenancy mutation has no workspace fence target'
138
+ USING ERRCODE = '55000';
139
+ END IF;
140
+ lock_key := hashtextextended('session-tenancy:' || workspace_id_value::text, 0);
141
+ IF NOT EXISTS (
142
+ SELECT 1 FROM pg_locks held
143
+ WHERE held.locktype = 'advisory'
144
+ AND held.pid = pg_backend_pid()
145
+ AND held.granted
146
+ AND held.classid = (((lock_key >> 32) & 4294967295)::bigint)::oid
147
+ AND held.objid = ((lock_key & 4294967295)::bigint)::oid
148
+ AND held.objsubid = 1
149
+ AND held.mode IN ('ShareLock', 'ExclusiveLock')
150
+ ) THEN
151
+ RAISE EXCEPTION 'session tenancy mutation requires the workspace fence'
152
+ USING ERRCODE = '55000', DETAIL = TG_TABLE_NAME;
153
+ END IF;
154
+ IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
155
+ RETURN NEW;
156
+ END
157
+ $fence$;
158
+
159
+ REVOKE ALL ON FUNCTION opengeni_private.require_session_tenancy_fence() FROM PUBLIC;
160
+
161
+ -- FORCE RLS also applies to the non-bypass migration owner that owns these
162
+ -- SECURITY-DEFINER routines. A global sweep cannot know which workspace
163
+ -- fences to take until it can read a bounded workspace-id inventory, so give
164
+ -- only the private helpers below an exact-schema/backend/xact-bound read
165
+ -- capability. Every open gets an independent token: if an inner helper raises,
166
+ -- its insert rolls back before its exception handler runs, so closing that
167
+ -- exact token cannot consume an outer helper's authority. The capability is
168
+ -- closed before the helper returns any count. Once the fences are held, a
169
+ -- second owner-only policy admits the routine's actual reads and mutations for
170
+ -- those exact workspaces. opengeni_app cannot mint the capability, and merely
171
+ -- acquiring an advisory lock does not satisfy the current_user owner check.
172
+ CREATE TABLE IF NOT EXISTS opengeni_private.session_tenancy_fence_inventory_capabilities (
173
+ capability_id uuid PRIMARY KEY,
174
+ target_schema oid NOT NULL,
175
+ backend_pid integer NOT NULL,
176
+ transaction_id xid8 NOT NULL
177
+ );
178
+
179
+ REVOKE ALL ON TABLE
180
+ opengeni_private.session_tenancy_fence_inventory_capabilities
181
+ FROM PUBLIC;
182
+
183
+ -- opengeni_private is shared by dedicated target schemas. A second target may
184
+ -- reuse this ledger only when the first target installed the exact owner,
185
+ -- shape, primary key, and closed ACL expected here; otherwise stop rather than
186
+ -- inheriting ambiguous authority.
187
+ DO $session_tenancy_fence_inventory_contract$
188
+ DECLARE
189
+ capability_table oid := pg_catalog.to_regclass(
190
+ 'opengeni_private.session_tenancy_fence_inventory_capabilities'
191
+ );
192
+ actual_columns text[];
193
+ primary_key_columns text[];
194
+ BEGIN
195
+ IF capability_table IS NULL THEN
196
+ RAISE EXCEPTION '0345 session tenancy inventory ledger is missing'
197
+ USING ERRCODE = '55000';
198
+ END IF;
199
+ IF NOT EXISTS (
200
+ SELECT 1 FROM pg_catalog.pg_class relation
201
+ WHERE relation.oid = capability_table
202
+ AND relation.relkind = 'r'
203
+ AND relation.relowner = current_user::pg_catalog.regrole
204
+ ) THEN
205
+ RAISE EXCEPTION '0345 session tenancy inventory ledger owner/type drift'
206
+ USING ERRCODE = '55000';
207
+ END IF;
208
+ SELECT pg_catalog.array_agg(
209
+ attribute.attname || ':'
210
+ || pg_catalog.format_type(attribute.atttypid, attribute.atttypmod) || ':'
211
+ || attribute.attnotnull::text
212
+ ORDER BY attribute.attnum
213
+ ) INTO actual_columns
214
+ FROM pg_catalog.pg_attribute attribute
215
+ WHERE attribute.attrelid = capability_table
216
+ AND attribute.attnum > 0
217
+ AND NOT attribute.attisdropped;
218
+ IF actual_columns IS DISTINCT FROM ARRAY[
219
+ 'capability_id:uuid:true',
220
+ 'target_schema:oid:true',
221
+ 'backend_pid:integer:true',
222
+ 'transaction_id:xid8:true'
223
+ ]::text[] THEN
224
+ RAISE EXCEPTION '0345 session tenancy inventory ledger column drift'
225
+ USING ERRCODE = '55000', DETAIL = actual_columns::text;
226
+ END IF;
227
+ SELECT pg_catalog.array_agg(attribute.attname ORDER BY key.ordinality)
228
+ INTO primary_key_columns
229
+ FROM pg_catalog.pg_constraint constraint_value
230
+ CROSS JOIN LATERAL pg_catalog.unnest(constraint_value.conkey)
231
+ WITH ORDINALITY key(attnum, ordinality)
232
+ JOIN pg_catalog.pg_attribute attribute
233
+ ON attribute.attrelid = constraint_value.conrelid
234
+ AND attribute.attnum = key.attnum
235
+ WHERE constraint_value.conrelid = capability_table
236
+ AND constraint_value.contype = 'p';
237
+ IF primary_key_columns IS DISTINCT FROM ARRAY['capability_id']::text[]
238
+ OR (
239
+ SELECT pg_catalog.count(*) FROM pg_catalog.pg_constraint constraint_value
240
+ WHERE constraint_value.conrelid = capability_table
241
+ ) <> 1
242
+ THEN
243
+ RAISE EXCEPTION '0345 session tenancy inventory ledger constraint drift'
244
+ USING ERRCODE = '55000';
245
+ END IF;
246
+ IF EXISTS (
247
+ SELECT 1
248
+ FROM pg_catalog.pg_class relation
249
+ CROSS JOIN LATERAL pg_catalog.aclexplode(
250
+ COALESCE(
251
+ relation.relacl,
252
+ pg_catalog.acldefault('r', relation.relowner)
253
+ )
254
+ ) acl
255
+ WHERE relation.oid = capability_table
256
+ AND acl.grantee <> relation.relowner
257
+ ) THEN
258
+ RAISE EXCEPTION '0345 session tenancy inventory ledger ACL drift'
259
+ USING ERRCODE = '55000';
260
+ END IF;
261
+ END
262
+ $session_tenancy_fence_inventory_contract$;
263
+
264
+ CREATE OR REPLACE FUNCTION
265
+ opengeni_private.session_tenancy_fence_inventory_capability_active(
266
+ p_target_schema oid
267
+ )
268
+ RETURNS boolean
269
+ LANGUAGE sql
270
+ STABLE
271
+ SECURITY DEFINER
272
+ SET search_path = pg_catalog
273
+ AS $inventory_active$
274
+ SELECT EXISTS (
275
+ SELECT 1
276
+ FROM opengeni_private.session_tenancy_fence_inventory_capabilities capability
277
+ WHERE capability.target_schema = p_target_schema
278
+ AND capability.backend_pid = pg_catalog.pg_backend_pid()
279
+ AND capability.transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
280
+ )
281
+ $inventory_active$;
282
+
283
+ REVOKE ALL ON FUNCTION
284
+ opengeni_private.session_tenancy_fence_inventory_capability_active(oid)
285
+ FROM PUBLIC;
286
+
287
+ CREATE OR REPLACE FUNCTION
288
+ opengeni_private.open_session_tenancy_fence_inventory(p_target_schema oid)
289
+ RETURNS uuid
290
+ LANGUAGE plpgsql
291
+ SECURITY DEFINER
292
+ SET search_path = pg_catalog
293
+ AS $open_inventory$
294
+ DECLARE
295
+ capability_id_value uuid := pg_catalog.gen_random_uuid();
296
+ BEGIN
297
+ IF NOT EXISTS (
298
+ SELECT 1
299
+ FROM pg_catalog.pg_namespace namespace
300
+ JOIN opengeni_private.session_tenancy_fence_target_registry registry
301
+ ON registry.target_schema = namespace.oid
302
+ AND registry.singleton
303
+ WHERE namespace.oid = p_target_schema
304
+ ) THEN
305
+ RAISE EXCEPTION 'session tenancy inventory target schema is not registered'
306
+ USING ERRCODE = '22023';
307
+ END IF;
308
+ INSERT INTO opengeni_private.session_tenancy_fence_inventory_capabilities (
309
+ capability_id, target_schema, backend_pid, transaction_id
310
+ ) VALUES (
311
+ capability_id_value,
312
+ p_target_schema,
313
+ pg_catalog.pg_backend_pid(),
314
+ pg_catalog.pg_current_xact_id()
315
+ );
316
+ RETURN capability_id_value;
317
+ END
318
+ $open_inventory$;
319
+
320
+ CREATE OR REPLACE FUNCTION
321
+ opengeni_private.close_session_tenancy_fence_inventory(p_capability_id uuid)
322
+ RETURNS void
323
+ LANGUAGE sql
324
+ SECURITY DEFINER
325
+ SET search_path = pg_catalog
326
+ AS $close_inventory$
327
+ DELETE FROM opengeni_private.session_tenancy_fence_inventory_capabilities
328
+ WHERE capability_id = p_capability_id
329
+ AND backend_pid = pg_catalog.pg_backend_pid()
330
+ AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
331
+ $close_inventory$;
332
+
333
+ REVOKE ALL ON FUNCTION
334
+ opengeni_private.open_session_tenancy_fence_inventory(oid)
335
+ FROM PUBLIC;
336
+ REVOKE ALL ON FUNCTION
337
+ opengeni_private.close_session_tenancy_fence_inventory(uuid)
338
+ FROM PUBLIC;
339
+
340
+ -- Inventory authority ends before a helper returns. The caller may still need
341
+ -- owner-side RLS access while it mutates the rows protected by the locks, so
342
+ -- that phase has a separate exact-token capability. It is opened only after
343
+ -- all affected workspace fences are held and is closed by the patched caller
344
+ -- on both success and exception.
345
+ CREATE TABLE IF NOT EXISTS opengeni_private.session_tenancy_fenced_access_capabilities (
346
+ capability_id uuid PRIMARY KEY,
347
+ target_schema oid NOT NULL,
348
+ backend_pid integer NOT NULL,
349
+ transaction_id xid8 NOT NULL
350
+ );
351
+
352
+ REVOKE ALL ON TABLE
353
+ opengeni_private.session_tenancy_fenced_access_capabilities
354
+ FROM PUBLIC;
355
+
356
+ DO $session_tenancy_fenced_access_contract$
357
+ DECLARE
358
+ capability_table oid := pg_catalog.to_regclass(
359
+ 'opengeni_private.session_tenancy_fenced_access_capabilities'
360
+ );
361
+ actual_columns text[];
362
+ primary_key_columns text[];
363
+ BEGIN
364
+ IF capability_table IS NULL OR NOT EXISTS (
365
+ SELECT 1 FROM pg_catalog.pg_class relation
366
+ WHERE relation.oid = capability_table
367
+ AND relation.relkind = 'r'
368
+ AND relation.relowner = current_user::pg_catalog.regrole
369
+ ) THEN
370
+ RAISE EXCEPTION '0345 session tenancy fenced-access ledger owner/type drift'
371
+ USING ERRCODE = '55000';
372
+ END IF;
373
+ SELECT pg_catalog.array_agg(
374
+ attribute.attname || ':'
375
+ || pg_catalog.format_type(attribute.atttypid, attribute.atttypmod) || ':'
376
+ || attribute.attnotnull::text
377
+ ORDER BY attribute.attnum
378
+ ) INTO actual_columns
379
+ FROM pg_catalog.pg_attribute attribute
380
+ WHERE attribute.attrelid = capability_table
381
+ AND attribute.attnum > 0
382
+ AND NOT attribute.attisdropped;
383
+ IF actual_columns IS DISTINCT FROM ARRAY[
384
+ 'capability_id:uuid:true',
385
+ 'target_schema:oid:true',
386
+ 'backend_pid:integer:true',
387
+ 'transaction_id:xid8:true'
388
+ ]::text[] THEN
389
+ RAISE EXCEPTION '0345 session tenancy fenced-access ledger column drift'
390
+ USING ERRCODE = '55000', DETAIL = actual_columns::text;
391
+ END IF;
392
+ SELECT pg_catalog.array_agg(attribute.attname ORDER BY key.ordinality)
393
+ INTO primary_key_columns
394
+ FROM pg_catalog.pg_constraint constraint_value
395
+ CROSS JOIN LATERAL pg_catalog.unnest(constraint_value.conkey)
396
+ WITH ORDINALITY key(attnum, ordinality)
397
+ JOIN pg_catalog.pg_attribute attribute
398
+ ON attribute.attrelid = constraint_value.conrelid
399
+ AND attribute.attnum = key.attnum
400
+ WHERE constraint_value.conrelid = capability_table
401
+ AND constraint_value.contype = 'p';
402
+ IF primary_key_columns IS DISTINCT FROM ARRAY['capability_id']::text[]
403
+ OR (
404
+ SELECT pg_catalog.count(*) FROM pg_catalog.pg_constraint constraint_value
405
+ WHERE constraint_value.conrelid = capability_table
406
+ ) <> 1
407
+ OR EXISTS (
408
+ SELECT 1
409
+ FROM pg_catalog.pg_class relation
410
+ CROSS JOIN LATERAL pg_catalog.aclexplode(
411
+ COALESCE(
412
+ relation.relacl,
413
+ pg_catalog.acldefault('r', relation.relowner)
414
+ )
415
+ ) acl
416
+ WHERE relation.oid = capability_table
417
+ AND acl.grantee <> relation.relowner
418
+ )
419
+ THEN
420
+ RAISE EXCEPTION '0345 session tenancy fenced-access ledger constraint/ACL drift'
421
+ USING ERRCODE = '55000';
422
+ END IF;
423
+ END
424
+ $session_tenancy_fenced_access_contract$;
425
+
426
+ CREATE OR REPLACE FUNCTION
427
+ opengeni_private.open_session_tenancy_fenced_access(p_target_schema oid)
428
+ RETURNS uuid
429
+ LANGUAGE plpgsql
430
+ SECURITY DEFINER
431
+ SET search_path = pg_catalog
432
+ AS $open_fenced_access$
433
+ DECLARE
434
+ capability_id_value uuid := pg_catalog.gen_random_uuid();
435
+ BEGIN
436
+ IF NOT EXISTS (
437
+ SELECT 1
438
+ FROM pg_catalog.pg_namespace namespace
439
+ JOIN opengeni_private.session_tenancy_fence_target_registry registry
440
+ ON registry.target_schema = namespace.oid
441
+ AND registry.singleton
442
+ WHERE namespace.oid = p_target_schema
443
+ ) THEN
444
+ RAISE EXCEPTION 'session tenancy fenced-access target schema is not registered'
445
+ USING ERRCODE = '22023';
446
+ END IF;
447
+ INSERT INTO opengeni_private.session_tenancy_fenced_access_capabilities (
448
+ capability_id, target_schema, backend_pid, transaction_id
449
+ ) VALUES (
450
+ capability_id_value,
451
+ p_target_schema,
452
+ pg_catalog.pg_backend_pid(),
453
+ pg_catalog.pg_current_xact_id()
454
+ );
455
+ RETURN capability_id_value;
456
+ END
457
+ $open_fenced_access$;
458
+
459
+ CREATE OR REPLACE FUNCTION
460
+ opengeni_private.close_session_tenancy_fenced_access(p_capability_id uuid)
461
+ RETURNS void
462
+ LANGUAGE sql
463
+ SECURITY DEFINER
464
+ SET search_path = pg_catalog
465
+ AS $close_fenced_access$
466
+ DELETE FROM opengeni_private.session_tenancy_fenced_access_capabilities
467
+ WHERE capability_id = p_capability_id
468
+ AND backend_pid = pg_catalog.pg_backend_pid()
469
+ AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
470
+ $close_fenced_access$;
471
+
472
+ CREATE OR REPLACE FUNCTION
473
+ opengeni_private.session_tenancy_fenced_access_capability_active(
474
+ p_target_schema oid
475
+ )
476
+ RETURNS boolean
477
+ LANGUAGE sql
478
+ STABLE
479
+ SECURITY DEFINER
480
+ SET search_path = pg_catalog
481
+ AS $fenced_access_active$
482
+ SELECT EXISTS (
483
+ SELECT 1
484
+ FROM opengeni_private.session_tenancy_fenced_access_capabilities capability
485
+ WHERE capability.target_schema = p_target_schema
486
+ AND capability.backend_pid = pg_catalog.pg_backend_pid()
487
+ AND capability.transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
488
+ )
489
+ $fenced_access_active$;
490
+
491
+ REVOKE ALL ON FUNCTION
492
+ opengeni_private.open_session_tenancy_fenced_access(oid)
493
+ FROM PUBLIC;
494
+ REVOKE ALL ON FUNCTION
495
+ opengeni_private.close_session_tenancy_fenced_access(uuid)
496
+ FROM PUBLIC;
497
+ REVOKE ALL ON FUNCTION
498
+ opengeni_private.session_tenancy_fenced_access_capability_active(oid)
499
+ FROM PUBLIC;
500
+
501
+ -- CREATE OR REPLACE preserves a function's owner and direct role grants. Do
502
+ -- not let a pre-existing same-signature object silently retain mint/close or
503
+ -- capability-observation authority. All six shared seams must be owned only by
504
+ -- this migration owner and executable by no other role.
505
+ DO $session_tenancy_shared_capability_function_contract$
506
+ DECLARE
507
+ signature text;
508
+ routine oid;
509
+ BEGIN
510
+ FOREACH signature IN ARRAY ARRAY[
511
+ 'opengeni_private.session_tenancy_fence_inventory_capability_active(oid)',
512
+ 'opengeni_private.open_session_tenancy_fence_inventory(oid)',
513
+ 'opengeni_private.close_session_tenancy_fence_inventory(uuid)',
514
+ 'opengeni_private.session_tenancy_fenced_access_capability_active(oid)',
515
+ 'opengeni_private.open_session_tenancy_fenced_access(oid)',
516
+ 'opengeni_private.close_session_tenancy_fenced_access(uuid)'
517
+ ] LOOP
518
+ routine := pg_catalog.to_regprocedure(signature);
519
+ IF routine IS NULL OR NOT EXISTS (
520
+ SELECT 1 FROM pg_catalog.pg_proc procedure
521
+ WHERE procedure.oid = routine
522
+ AND procedure.proowner = current_user::pg_catalog.regrole
523
+ AND procedure.prosecdef
524
+ ) THEN
525
+ RAISE EXCEPTION
526
+ '0345 shared session tenancy capability function owner/type drift: %',
527
+ signature USING ERRCODE = '55000';
528
+ END IF;
529
+ IF EXISTS (
530
+ SELECT 1
531
+ FROM pg_catalog.pg_proc procedure
532
+ CROSS JOIN LATERAL pg_catalog.aclexplode(
533
+ COALESCE(
534
+ procedure.proacl,
535
+ pg_catalog.acldefault('f', procedure.proowner)
536
+ )
537
+ ) acl
538
+ WHERE procedure.oid = routine
539
+ AND acl.grantee <> procedure.proowner
540
+ ) THEN
541
+ RAISE EXCEPTION
542
+ '0345 shared session tenancy capability function ACL drift: %',
543
+ signature USING ERRCODE = '55000';
544
+ END IF;
545
+ END LOOP;
546
+ END
547
+ $session_tenancy_shared_capability_function_contract$;
548
+
549
+ -- The target identity is a literal namespace OID captured by this migration,
550
+ -- not a search_path-, GUC-, or caller-controlled string. Each dedicated schema
551
+ -- owns a separate copy, so later target migrations cannot overwrite it.
552
+ DO $session_tenancy_fence_target_identity$
553
+ DECLARE
554
+ target_schema text := pg_catalog.current_schema();
555
+ target_schema_oid oid := pg_catalog.current_schema()::pg_catalog.regnamespace;
556
+ BEGIN
557
+ EXECUTE pg_catalog.format($identity$
558
+ CREATE OR REPLACE FUNCTION %1$I.session_tenancy_fence_target_schema()
559
+ RETURNS oid
560
+ LANGUAGE sql
561
+ IMMUTABLE
562
+ SECURITY DEFINER
563
+ SET search_path = pg_catalog
564
+ AS $body$ SELECT %2$s::oid $body$;
565
+ REVOKE ALL ON FUNCTION %1$I.session_tenancy_fence_target_schema() FROM PUBLIC;
566
+ $identity$, target_schema, target_schema_oid);
567
+ END
568
+ $session_tenancy_fence_target_identity$;
569
+
570
+ CREATE OR REPLACE FUNCTION session_tenancy_fence_owner_policy_active(
571
+ p_actor text,
572
+ p_expected_owner text,
573
+ p_target_schema oid,
574
+ p_workspace_id uuid,
575
+ p_inventory boolean
576
+ )
577
+ RETURNS boolean
578
+ LANGUAGE plpgsql
579
+ STABLE
580
+ SECURITY DEFINER
581
+ SET search_path = pg_catalog
582
+ AS $owner_policy$
583
+ DECLARE
584
+ lock_key bigint;
585
+ BEGIN
586
+ IF p_actor IS DISTINCT FROM p_expected_owner THEN
587
+ RETURN false;
588
+ END IF;
589
+ IF p_inventory THEN
590
+ RETURN opengeni_private.session_tenancy_fence_inventory_capability_active(
591
+ p_target_schema
592
+ );
593
+ END IF;
594
+ IF p_workspace_id IS NULL THEN
595
+ RETURN false;
596
+ END IF;
597
+ IF NOT opengeni_private.session_tenancy_fenced_access_capability_active(
598
+ p_target_schema
599
+ ) THEN
600
+ RETURN false;
601
+ END IF;
602
+ lock_key := pg_catalog.hashtextextended(
603
+ 'session-tenancy:' || p_workspace_id::text,
604
+ 0
605
+ );
606
+ RETURN EXISTS (
607
+ SELECT 1 FROM pg_catalog.pg_locks held
608
+ WHERE held.locktype = 'advisory'
609
+ AND held.pid = pg_catalog.pg_backend_pid()
610
+ AND held.granted
611
+ AND held.classid = (((lock_key >> 32) & 4294967295)::bigint)::oid
612
+ AND held.objid = ((lock_key & 4294967295)::bigint)::oid
613
+ AND held.objsubid = 1
614
+ AND held.mode IN ('ShareLock', 'ExclusiveLock')
615
+ );
616
+ END
617
+ $owner_policy$;
618
+
619
+ REVOKE ALL ON FUNCTION
620
+ session_tenancy_fence_owner_policy_active(text, text, oid, uuid, boolean)
621
+ FROM PUBLIC;
622
+ -- This predicate returns only a boolean derived from its arguments and the
623
+ -- current backend's private capability/lock state. Every runtime role must be
624
+ -- able to plan RLS expressions that reference it; authority-minting functions
625
+ -- and the ledger remain fully revoked.
626
+ GRANT EXECUTE ON FUNCTION
627
+ session_tenancy_fence_owner_policy_active(text, text, oid, uuid, boolean)
628
+ TO PUBLIC;
629
+
630
+ DO $session_tenancy_fence_owner_policies$
631
+ DECLARE
632
+ target_schema text := pg_catalog.current_schema();
633
+ target_schema_oid oid := pg_catalog.current_schema()::pg_catalog.regnamespace;
634
+ migration_owner text := current_user;
635
+ table_name text;
636
+ BEGIN
637
+ FOREACH table_name IN ARRAY ARRAY[
638
+ 'sandbox_leases',
639
+ 'sandbox_lease_holders',
640
+ 'sandbox_retained_processes',
641
+ 'sessions'
642
+ ] LOOP
643
+ EXECUTE pg_catalog.format(
644
+ 'CREATE POLICY session_tenancy_fence_inventory_read ON %I.%I '
645
+ || 'FOR SELECT USING ('
646
+ || '%I.session_tenancy_fence_owner_policy_active('
647
+ || 'current_user, %L, %s::oid, workspace_id, true))',
648
+ target_schema,
649
+ table_name,
650
+ target_schema,
651
+ migration_owner,
652
+ target_schema_oid
653
+ );
654
+ END LOOP;
655
+
656
+ FOREACH table_name IN ARRAY ARRAY[
657
+ 'sandbox_leases',
658
+ 'sandbox_lease_holders',
659
+ 'sandbox_retained_processes'
660
+ ] LOOP
661
+ EXECUTE pg_catalog.format(
662
+ 'CREATE POLICY session_tenancy_fenced_owner_write ON %I.%I '
663
+ || 'FOR ALL USING ('
664
+ || '%I.session_tenancy_fence_owner_policy_active('
665
+ || 'current_user, %L, %s::oid, workspace_id, false)) '
666
+ || 'WITH CHECK ('
667
+ || '%I.session_tenancy_fence_owner_policy_active('
668
+ || 'current_user, %L, %s::oid, workspace_id, false))',
669
+ target_schema,
670
+ table_name,
671
+ target_schema,
672
+ migration_owner,
673
+ target_schema_oid,
674
+ target_schema,
675
+ migration_owner,
676
+ target_schema_oid
677
+ );
678
+ END LOOP;
679
+
680
+ FOREACH table_name IN ARRAY ARRAY[
681
+ 'sandbox_workspace_mutation_admissions',
682
+ 'sessions',
683
+ 'session_turns',
684
+ 'session_turn_attempts',
685
+ 'session_background_commands'
686
+ ] LOOP
687
+ EXECUTE pg_catalog.format(
688
+ 'CREATE POLICY session_tenancy_fenced_owner_read ON %I.%I '
689
+ || 'FOR SELECT USING ('
690
+ || '%I.session_tenancy_fence_owner_policy_active('
691
+ || 'current_user, %L, %s::oid, workspace_id, false))',
692
+ target_schema,
693
+ table_name,
694
+ target_schema,
695
+ migration_owner,
696
+ target_schema_oid
697
+ );
698
+ END LOOP;
699
+
700
+ -- `session_visibility_isolation` is RESTRICTIVE, so a permissive owner
701
+ -- policy alone cannot expose private sessions to the bounded inventory or
702
+ -- to a fenced definer. Admit only this exact owner+schema+xact capability or
703
+ -- an already-held workspace fence, then retain the original subject rule.
704
+ EXECUTE pg_catalog.format(
705
+ 'DROP POLICY IF EXISTS session_visibility_isolation ON %I.sessions',
706
+ target_schema
707
+ );
708
+ EXECUTE pg_catalog.format(
709
+ 'CREATE POLICY session_visibility_isolation ON %1$I.sessions AS RESTRICTIVE '
710
+ || 'FOR ALL USING ('
711
+ || '%1$I.session_tenancy_fence_owner_policy_active('
712
+ || 'current_user, %2$L, %3$s::oid, workspace_id, true) '
713
+ || 'OR %1$I.session_tenancy_fence_owner_policy_active('
714
+ || 'current_user, %2$L, %3$s::oid, workspace_id, false) '
715
+ || 'OR nullif(pg_catalog.current_setting(''opengeni.subject_id'', true), '''') '
716
+ || 'IS NULL OR visibility = ''workspace_shared'' '
717
+ || 'OR %1$I.session_private_actor_visible('
718
+ || 'account_id, workspace_id, owner_organization_membership_id, owner_subject_id)'
719
+ || ') WITH CHECK ('
720
+ || '%1$I.session_tenancy_fence_owner_policy_active('
721
+ || 'current_user, %2$L, %3$s::oid, workspace_id, true) '
722
+ || 'OR %1$I.session_tenancy_fence_owner_policy_active('
723
+ || 'current_user, %2$L, %3$s::oid, workspace_id, false) '
724
+ || 'OR nullif(pg_catalog.current_setting(''opengeni.subject_id'', true), '''') '
725
+ || 'IS NULL OR visibility = ''workspace_shared'' '
726
+ || 'OR %1$I.session_private_actor_visible('
727
+ || 'account_id, workspace_id, owner_organization_membership_id, owner_subject_id)'
728
+ || ')',
729
+ target_schema,
730
+ migration_owner,
731
+ target_schema_oid
732
+ );
733
+ END
734
+ $session_tenancy_fence_owner_policies$;
735
+
736
+ -- The lifecycle reaper is deliberately cross-workspace, but it still mutates
737
+ -- sandbox_lease_holders and therefore participates in the same quiescence
738
+ -- protocol. Acquire every currently active lease workspace in stable order
739
+ -- before the reaper locks any operation, lease, or holder row. These are
740
+ -- shared advisory locks: ordinary tenant writers continue concurrently, while
741
+ -- a fork/visibility transition waits only when its workspace has live holder
742
+ -- state that the global sweep may inspect or remove.
743
+ CREATE OR REPLACE FUNCTION acquire_sandbox_reaper_session_tenancy_fences()
744
+ RETURNS integer
745
+ LANGUAGE plpgsql
746
+ SECURITY DEFINER
747
+ SET search_path FROM CURRENT
748
+ AS $reaper_fences$
749
+ DECLARE
750
+ inventory_capability_id uuid;
751
+ workspace_id_value uuid;
752
+ locked_count integer := 0;
753
+ BEGIN
754
+ PERFORM pg_catalog.set_config('opengeni.sandbox_recovery_protocol_v2', '1', true);
755
+ inventory_capability_id :=
756
+ opengeni_private.open_session_tenancy_fence_inventory(
757
+ session_tenancy_fence_target_schema()
758
+ );
759
+ FOR workspace_id_value IN
760
+ SELECT DISTINCT lease.workspace_id
761
+ FROM sandbox_leases lease
762
+ WHERE lease.workspace_id IS NOT NULL
763
+ AND (
764
+ lease.liveness <> 'cold'
765
+ OR EXISTS (
766
+ SELECT 1 FROM sandbox_lease_holders holder
767
+ WHERE holder.lease_id = lease.id
768
+ )
769
+ )
770
+ ORDER BY lease.workspace_id
771
+ LOOP
772
+ PERFORM acquire_session_tenancy_fence(workspace_id_value);
773
+ locked_count := locked_count + 1;
774
+ END LOOP;
775
+ PERFORM opengeni_private.close_session_tenancy_fence_inventory(
776
+ inventory_capability_id
777
+ );
778
+ RETURN locked_count;
779
+ EXCEPTION WHEN OTHERS THEN
780
+ PERFORM opengeni_private.close_session_tenancy_fence_inventory(
781
+ inventory_capability_id
782
+ );
783
+ RAISE;
784
+ END
785
+ $reaper_fences$;
786
+
787
+ REVOKE ALL ON FUNCTION
788
+ acquire_sandbox_reaper_session_tenancy_fences()
789
+ FROM PUBLIC;
790
+
791
+ -- Routine-local entry points must not depend on a TypeScript wrapper having
792
+ -- opened the fence. This single-workspace helper is intentionally private so
793
+ -- direct app-executable SECURITY-DEFINER calls can enter the same shared
794
+ -- protocol after validating their scope and before taking their first row
795
+ -- lock or mutating a fenced table.
796
+ CREATE OR REPLACE FUNCTION acquire_session_tenancy_fence(
797
+ p_workspace_id uuid
798
+ )
799
+ RETURNS void
800
+ LANGUAGE plpgsql
801
+ SECURITY DEFINER
802
+ SET search_path = pg_catalog
803
+ AS $workspace_fence$
804
+ BEGIN
805
+ IF p_workspace_id IS NULL THEN
806
+ RAISE EXCEPTION 'session-tenancy fence requires a workspace'
807
+ USING ERRCODE = '22023';
808
+ END IF;
809
+ PERFORM pg_catalog.pg_advisory_xact_lock_shared(
810
+ pg_catalog.hashtextextended(
811
+ 'session-tenancy:' || p_workspace_id::text,
812
+ 0
813
+ )
814
+ );
815
+ END
816
+ $workspace_fence$;
817
+
818
+ REVOKE ALL ON FUNCTION
819
+ acquire_session_tenancy_fence(uuid)
820
+ FROM PUBLIC;
821
+
822
+ -- Global retained-process claiming is bounded by p_limit at mutation time,
823
+ -- but SKIP LOCKED means the exact batch cannot be known safely before its row
824
+ -- locks. Fence every currently due workspace, and no other workspace, in UUID
825
+ -- order before the claim CTE starts taking those locks.
826
+ CREATE OR REPLACE FUNCTION
827
+ acquire_due_retained_process_session_tenancy_fences()
828
+ RETURNS integer
829
+ LANGUAGE plpgsql
830
+ SECURITY DEFINER
831
+ SET search_path FROM CURRENT
832
+ AS $retained_process_fences$
833
+ DECLARE
834
+ inventory_capability_id uuid;
835
+ workspace_id_value uuid;
836
+ locked_count integer := 0;
837
+ BEGIN
838
+ PERFORM pg_catalog.set_config('opengeni.sandbox_recovery_protocol_v2', '1', true);
839
+ inventory_capability_id :=
840
+ opengeni_private.open_session_tenancy_fence_inventory(
841
+ session_tenancy_fence_target_schema()
842
+ );
843
+ FOR workspace_id_value IN
844
+ SELECT DISTINCT process.workspace_id
845
+ FROM sandbox_retained_processes process
846
+ WHERE process.workspace_id IS NOT NULL
847
+ AND process.state = 'active'
848
+ AND process.reconcile_after <= pg_catalog.now()
849
+ ORDER BY process.workspace_id
850
+ LOOP
851
+ PERFORM acquire_session_tenancy_fence(workspace_id_value);
852
+ locked_count := locked_count + 1;
853
+ END LOOP;
854
+ PERFORM opengeni_private.close_session_tenancy_fence_inventory(
855
+ inventory_capability_id
856
+ );
857
+ RETURN locked_count;
858
+ EXCEPTION WHEN OTHERS THEN
859
+ PERFORM opengeni_private.close_session_tenancy_fence_inventory(
860
+ inventory_capability_id
861
+ );
862
+ RAISE;
863
+ END
864
+ $retained_process_fences$;
865
+
866
+ REVOKE ALL ON FUNCTION
867
+ acquire_due_retained_process_session_tenancy_fences()
868
+ FROM PUBLIC;
869
+
870
+ -- Rotation deletes viewer holders only for due leases. Lock the distinct due
871
+ -- lease workspaces for that population, not every workspace with an unrelated
872
+ -- live lease.
873
+ CREATE OR REPLACE FUNCTION
874
+ acquire_due_sandbox_rotation_session_tenancy_fences(
875
+ p_lead_ms bigint
876
+ )
877
+ RETURNS integer
878
+ LANGUAGE plpgsql
879
+ SECURITY DEFINER
880
+ SET search_path FROM CURRENT
881
+ AS $rotation_fences$
882
+ DECLARE
883
+ inventory_capability_id uuid;
884
+ workspace_id_value uuid;
885
+ locked_count integer := 0;
886
+ BEGIN
887
+ PERFORM pg_catalog.set_config('opengeni.sandbox_recovery_protocol_v2', '1', true);
888
+ inventory_capability_id :=
889
+ opengeni_private.open_session_tenancy_fence_inventory(
890
+ session_tenancy_fence_target_schema()
891
+ );
892
+ FOR workspace_id_value IN
893
+ SELECT DISTINCT lease.workspace_id
894
+ FROM sandbox_leases lease
895
+ WHERE lease.workspace_id IS NOT NULL
896
+ AND lease.backend = 'modal'
897
+ AND lease.liveness IN ('warming', 'warm')
898
+ AND lease.provider_deadline_at IS NOT NULL
899
+ AND lease.provider_deadline_at
900
+ <= pg_catalog.now()
901
+ + pg_catalog.make_interval(secs => p_lead_ms / 1000.0)
902
+ AND lease.rotation_requested_at IS NULL
903
+ AND (
904
+ lease.reaper_hold_id IS NULL
905
+ OR lease.reaper_hold_until <= pg_catalog.now()
906
+ )
907
+ ORDER BY lease.workspace_id
908
+ LOOP
909
+ PERFORM acquire_session_tenancy_fence(workspace_id_value);
910
+ locked_count := locked_count + 1;
911
+ END LOOP;
912
+ PERFORM opengeni_private.close_session_tenancy_fence_inventory(
913
+ inventory_capability_id
914
+ );
915
+ RETURN locked_count;
916
+ EXCEPTION WHEN OTHERS THEN
917
+ PERFORM opengeni_private.close_session_tenancy_fence_inventory(
918
+ inventory_capability_id
919
+ );
920
+ RAISE;
921
+ END
922
+ $rotation_fences$;
923
+
924
+ REVOKE ALL ON FUNCTION
925
+ acquire_due_sandbox_rotation_session_tenancy_fences(bigint)
926
+ FROM PUBLIC;
927
+
928
+ -- One connected machine may back sessions in several workspaces. Discover
929
+ -- the exact update population without row locks, then acquire every workspace
930
+ -- fence in deterministic order after the detach routine opens its bounded
931
+ -- write capability but before it locks or mutates dependent-session state.
932
+ CREATE OR REPLACE FUNCTION
933
+ acquire_scoped_machine_session_tenancy_fences(
934
+ p_account_id uuid,
935
+ p_sandbox_id uuid
936
+ )
937
+ RETURNS integer
938
+ LANGUAGE plpgsql
939
+ SECURITY DEFINER
940
+ SET search_path FROM CURRENT
941
+ AS $machine_fences$
942
+ DECLARE
943
+ inventory_capability_id uuid;
944
+ workspace_id_value uuid;
945
+ locked_count integer := 0;
946
+ BEGIN
947
+ IF p_account_id IS NULL OR p_sandbox_id IS NULL THEN
948
+ RAISE EXCEPTION 'machine session-tenancy fences require complete scope'
949
+ USING ERRCODE = '22023';
950
+ END IF;
951
+ inventory_capability_id :=
952
+ opengeni_private.open_session_tenancy_fence_inventory(
953
+ session_tenancy_fence_target_schema()
954
+ );
955
+ FOR workspace_id_value IN
956
+ SELECT DISTINCT session.workspace_id
957
+ FROM sessions session
958
+ WHERE session.account_id = p_account_id
959
+ AND (
960
+ session.active_sandbox_id = p_sandbox_id
961
+ OR (
962
+ session.sandbox_group_id = p_sandbox_id
963
+ AND session.sandbox_backend = 'selfhosted'
964
+ )
965
+ )
966
+ ORDER BY session.workspace_id
967
+ LOOP
968
+ PERFORM acquire_session_tenancy_fence(workspace_id_value);
969
+ locked_count := locked_count + 1;
970
+ END LOOP;
971
+ PERFORM opengeni_private.close_session_tenancy_fence_inventory(
972
+ inventory_capability_id
973
+ );
974
+ RETURN locked_count;
975
+ EXCEPTION WHEN OTHERS THEN
976
+ PERFORM opengeni_private.close_session_tenancy_fence_inventory(
977
+ inventory_capability_id
978
+ );
979
+ RAISE;
980
+ END
981
+ $machine_fences$;
982
+
983
+ REVOKE ALL ON FUNCTION
984
+ acquire_scoped_machine_session_tenancy_fences(uuid, uuid)
985
+ FROM PUBLIC;
986
+
987
+ -- Both shipped global reapers are SECURITY-DEFINER routines that take row
988
+ -- locks internally. Inject the shared-fence entry at the start of their exact
989
+ -- frozen definitions so every call path, including a direct SQL invocation,
990
+ -- enters the protocol before those row locks. Refuse definition drift instead
991
+ -- of silently producing an unfenced partial repair.
992
+ DO $repair_global_sandbox_reapers$
993
+ DECLARE
994
+ interaction_definition text;
995
+ lease_definition text;
996
+ interaction_prefix constant text :=
997
+ E' IF p_interaction_holder_ttl_ms <= 0 THEN\n'
998
+ || E' RETURN 0;\n'
999
+ || E' END IF;';
1000
+ lease_prefix constant text := E' BEGIN\n PERFORM pg_catalog.set_config(''opengeni.sandbox_recovery_protocol_v2'', ''1'', true);';
1001
+ fenced_prefix constant text :=
1002
+ E' PERFORM acquire_sandbox_reaper_session_tenancy_fences();';
1003
+ BEGIN
1004
+ SELECT pg_catalog.pg_get_functiondef(
1005
+ 'opengeni_private.reap_stale_interaction_transitions(bigint)'::regprocedure
1006
+ ) INTO interaction_definition;
1007
+ IF pg_catalog.strpos(interaction_definition, interaction_prefix) = 0
1008
+ OR pg_catalog.strpos(
1009
+ pg_catalog.substr(
1010
+ interaction_definition,
1011
+ pg_catalog.strpos(interaction_definition, interaction_prefix)
1012
+ + pg_catalog.length(interaction_prefix)
1013
+ ),
1014
+ interaction_prefix
1015
+ ) > 0
1016
+ THEN
1017
+ RAISE EXCEPTION '0345 interaction reaper definition drift'
1018
+ USING ERRCODE = '55000';
1019
+ END IF;
1020
+
1021
+ EXECUTE pg_catalog.replace(
1022
+ interaction_definition,
1023
+ interaction_prefix,
1024
+ interaction_prefix || E'\n\n' || fenced_prefix
1025
+ );
1026
+
1027
+ SELECT pg_catalog.pg_get_functiondef(
1028
+ 'opengeni_private.reap_sandbox_leases(bigint,bigint,bigint,bigint)'::regprocedure
1029
+ ) INTO lease_definition;
1030
+ IF pg_catalog.strpos(lease_definition, lease_prefix) = 0
1031
+ OR pg_catalog.strpos(
1032
+ pg_catalog.substr(
1033
+ lease_definition,
1034
+ pg_catalog.strpos(lease_definition, lease_prefix)
1035
+ + pg_catalog.length(lease_prefix)
1036
+ ),
1037
+ lease_prefix
1038
+ ) > 0
1039
+ THEN
1040
+ RAISE EXCEPTION '0345 lease reaper definition drift'
1041
+ USING ERRCODE = '55000';
1042
+ END IF;
1043
+
1044
+ EXECUTE pg_catalog.replace(
1045
+ lease_definition,
1046
+ lease_prefix,
1047
+ E' BEGIN\n' || fenced_prefix || E'\n'
1048
+ || E' PERFORM pg_catalog.set_config(''opengeni.sandbox_recovery_protocol_v2'', ''1'', true);'
1049
+ );
1050
+ END
1051
+ $repair_global_sandbox_reapers$;
1052
+
1053
+ -- The retained-process claim and provider-deadline rotation sweeps are also
1054
+ -- global SECURITY-DEFINER mutators. Their TypeScript callers do not provide a
1055
+ -- workspace scope, so repair the installed SQL definitions themselves with
1056
+ -- population-specific, deterministically ordered fence helpers.
1057
+ DO $repair_global_background_session_tenancy_fences$
1058
+ DECLARE
1059
+ definition text;
1060
+ patched text;
1061
+ occurrences integer;
1062
+ protocol_prefix constant text :=
1063
+ E' PERFORM pg_catalog.set_config(''opengeni.sandbox_recovery_protocol_v2'', ''1'', true);';
1064
+ BEGIN
1065
+ definition := pg_catalog.pg_get_functiondef(
1066
+ 'opengeni_private.claim_terminal_retained_processes(uuid,integer,bigint)'
1067
+ ::regprocedure
1068
+ );
1069
+ occurrences := (
1070
+ pg_catalog.length(definition)
1071
+ - pg_catalog.length(pg_catalog.replace(definition, protocol_prefix, ''))
1072
+ ) / pg_catalog.length(protocol_prefix);
1073
+ IF occurrences <> 1
1074
+ OR pg_catalog.strpos(
1075
+ definition,
1076
+ 'acquire_due_retained_process_session_tenancy_fences'
1077
+ ) > 0
1078
+ THEN
1079
+ RAISE EXCEPTION '0345 retained-process claim definition drift'
1080
+ USING ERRCODE = '55000';
1081
+ END IF;
1082
+ patched := pg_catalog.replace(
1083
+ definition,
1084
+ protocol_prefix,
1085
+ E' PERFORM acquire_due_retained_process_session_tenancy_fences();\n\n'
1086
+ || protocol_prefix
1087
+ );
1088
+ EXECUTE patched;
1089
+
1090
+ definition := pg_catalog.pg_get_functiondef(
1091
+ 'opengeni_private.request_due_sandbox_rotations(bigint,integer)'
1092
+ ::regprocedure
1093
+ );
1094
+ occurrences := (
1095
+ pg_catalog.length(definition)
1096
+ - pg_catalog.length(pg_catalog.replace(definition, protocol_prefix, ''))
1097
+ ) / pg_catalog.length(protocol_prefix);
1098
+ IF occurrences <> 1
1099
+ OR pg_catalog.strpos(
1100
+ definition,
1101
+ 'acquire_due_sandbox_rotation_session_tenancy_fences'
1102
+ ) > 0
1103
+ THEN
1104
+ RAISE EXCEPTION '0345 due sandbox rotation definition drift'
1105
+ USING ERRCODE = '55000';
1106
+ END IF;
1107
+ patched := pg_catalog.replace(
1108
+ definition,
1109
+ protocol_prefix,
1110
+ E' PERFORM acquire_due_sandbox_rotation_session_tenancy_fences(\n'
1111
+ || E' p_lead_ms\n'
1112
+ || E' );\n\n'
1113
+ || protocol_prefix
1114
+ );
1115
+ EXECUTE patched;
1116
+ END
1117
+ $repair_global_background_session_tenancy_fences$;
1118
+
1119
+ -- Organization membership suspension/offboarding is account-wide: the
1120
+ -- preparation seam and both layers of the command can inspect or mutate
1121
+ -- session state in every workspace owned by the organization. Enter every
1122
+ -- workspace's shared fence in stable order immediately after the existing
1123
+ -- organization-membership advisory prefix and before any of those routines
1124
+ -- take row locks. Keeping this in their SECURITY-DEFINER SQL path also covers
1125
+ -- direct command invocations rather than relying on one TypeScript caller.
1126
+ CREATE OR REPLACE FUNCTION
1127
+ acquire_organization_session_tenancy_fences(
1128
+ p_account_id uuid
1129
+ )
1130
+ RETURNS integer
1131
+ LANGUAGE plpgsql
1132
+ SECURITY DEFINER
1133
+ SET search_path FROM CURRENT
1134
+ AS $membership_fences$
1135
+ DECLARE
1136
+ workspace_id_value uuid;
1137
+ locked_count integer := 0;
1138
+ BEGIN
1139
+ IF p_account_id IS NULL THEN
1140
+ RAISE EXCEPTION 'organization session-tenancy fence requires an organization'
1141
+ USING ERRCODE = '22023';
1142
+ END IF;
1143
+ PERFORM pg_catalog.pg_advisory_xact_lock(
1144
+ pg_catalog.hashtextextended(
1145
+ 'organization-membership:' || p_account_id::text,
1146
+ 0
1147
+ )
1148
+ );
1149
+ FOR workspace_id_value IN
1150
+ SELECT workspace.id
1151
+ FROM workspaces workspace
1152
+ WHERE workspace.account_id = p_account_id
1153
+ ORDER BY workspace.id
1154
+ LOOP
1155
+ PERFORM acquire_session_tenancy_fence(workspace_id_value);
1156
+ locked_count := locked_count + 1;
1157
+ END LOOP;
1158
+ RETURN locked_count;
1159
+ END
1160
+ $membership_fences$;
1161
+
1162
+ REVOKE ALL ON FUNCTION
1163
+ acquire_organization_session_tenancy_fences(uuid)
1164
+ FROM PUBLIC;
1165
+
1166
+ -- Migration 0299 installed the organization advisory prefix in all three
1167
+ -- lifecycle routines by exact definition repair. Extend that same prefix with
1168
+ -- the workspace fences, again refusing any definition drift instead of
1169
+ -- silently leaving one privileged mutation path outside the protocol.
1170
+ DO $repair_organization_membership_session_tenancy_fences$
1171
+ DECLARE
1172
+ signature text;
1173
+ definition text;
1174
+ patched text;
1175
+ occurrences integer;
1176
+ organization_prefix constant text :=
1177
+ E' PERFORM pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(\n'
1178
+ || E' ''organization-membership:'' || account_id_value::text, 0\n'
1179
+ || E' ));';
1180
+ fenced_prefix constant text := organization_prefix
1181
+ || E'\n PERFORM acquire_organization_session_tenancy_fences(\n'
1182
+ || E' account_id_value\n'
1183
+ || E' );';
1184
+ BEGIN
1185
+ FOREACH signature IN ARRAY ARRAY[
1186
+ 'prepare_organization_membership_protocol_settlements(jsonb)',
1187
+ 'organization_membership_command_0263(jsonb)',
1188
+ 'organization_membership_command(jsonb)'
1189
+ ] LOOP
1190
+ definition := pg_catalog.pg_get_functiondef(
1191
+ pg_catalog.to_regprocedure(
1192
+ pg_catalog.quote_ident(pg_catalog.current_schema()) || '.' || signature
1193
+ )
1194
+ );
1195
+ IF definition IS NULL THEN
1196
+ RAISE EXCEPTION
1197
+ '0345 organization membership lifecycle function % is missing', signature
1198
+ USING ERRCODE = '55000';
1199
+ END IF;
1200
+ occurrences := (
1201
+ pg_catalog.length(definition)
1202
+ - pg_catalog.length(
1203
+ pg_catalog.replace(definition, organization_prefix, '')
1204
+ )
1205
+ ) / pg_catalog.length(organization_prefix);
1206
+ IF occurrences <> 1
1207
+ OR pg_catalog.strpos(
1208
+ definition,
1209
+ 'acquire_organization_session_tenancy_fences'
1210
+ ) > 0
1211
+ THEN
1212
+ RAISE EXCEPTION
1213
+ '0345 organization membership session-tenancy prefix drift for %',
1214
+ signature USING ERRCODE = '55000';
1215
+ END IF;
1216
+ patched := pg_catalog.replace(
1217
+ definition,
1218
+ organization_prefix,
1219
+ fenced_prefix
1220
+ );
1221
+ IF pg_catalog.strpos(patched, fenced_prefix) = 0
1222
+ OR pg_catalog.strpos(patched, organization_prefix)
1223
+ > pg_catalog.strpos(patched, fenced_prefix)
1224
+ THEN
1225
+ RAISE EXCEPTION
1226
+ '0345 organization membership session-tenancy repair failed for %',
1227
+ signature USING ERRCODE = '55000';
1228
+ END IF;
1229
+ EXECUTE patched;
1230
+ END LOOP;
1231
+ END
1232
+ $repair_organization_membership_session_tenancy_fences$;
1233
+
1234
+ DO $repair_organization_retention_session_tenancy_fence$
1235
+ DECLARE
1236
+ definition text;
1237
+ patched text;
1238
+ occurrences integer;
1239
+ authority_prefix constant text :=
1240
+ E' PERFORM opengeni_private.assert_organization_retention_account(p_account_id);';
1241
+ fenced_prefix constant text := authority_prefix
1242
+ || E'\n PERFORM acquire_organization_session_tenancy_fences(\n'
1243
+ || E' p_account_id\n'
1244
+ || E' );';
1245
+ BEGIN
1246
+ definition := pg_catalog.pg_get_functiondef(
1247
+ pg_catalog.to_regprocedure(
1248
+ pg_catalog.quote_ident(pg_catalog.current_schema())
1249
+ || '.finalize_organization_retention_deletion(uuid,uuid,uuid,text)'
1250
+ )
1251
+ );
1252
+ IF definition IS NULL THEN
1253
+ RAISE EXCEPTION '0345 organization retention finalizer is missing'
1254
+ USING ERRCODE = '55000';
1255
+ END IF;
1256
+ occurrences := (
1257
+ pg_catalog.length(definition)
1258
+ - pg_catalog.length(
1259
+ pg_catalog.replace(definition, authority_prefix, '')
1260
+ )
1261
+ ) / pg_catalog.length(authority_prefix);
1262
+ IF occurrences <> 1
1263
+ OR pg_catalog.strpos(
1264
+ definition,
1265
+ 'acquire_organization_session_tenancy_fences'
1266
+ ) > 0
1267
+ THEN
1268
+ RAISE EXCEPTION '0345 organization retention session-tenancy prefix drift'
1269
+ USING ERRCODE = '55000';
1270
+ END IF;
1271
+ patched := pg_catalog.replace(
1272
+ definition,
1273
+ authority_prefix,
1274
+ fenced_prefix
1275
+ );
1276
+ IF pg_catalog.strpos(patched, fenced_prefix) = 0 THEN
1277
+ RAISE EXCEPTION '0345 organization retention session-tenancy repair failed'
1278
+ USING ERRCODE = '55000';
1279
+ END IF;
1280
+ EXECUTE patched;
1281
+ END
1282
+ $repair_organization_retention_session_tenancy_fence$;
1283
+
1284
+ -- Finish the direct-call cutover for every app-executable SECURITY-DEFINER
1285
+ -- routine that writes one of the fenced tables. Each repair anchors on one
1286
+ -- exact frozen source fragment and aborts the migration on definition drift.
1287
+ -- The fence is inside the routine, after authority/argument validation where
1288
+ -- available, and before its first row lock or protected-table mutation.
1289
+ DO $repair_app_mutator_session_tenancy_fences$
1290
+ DECLARE
1291
+ definition text;
1292
+ patched text;
1293
+ occurrences integer;
1294
+ anchor text;
1295
+ replacement text;
1296
+ target regprocedure;
1297
+ BEGIN
1298
+ target := pg_catalog.to_regprocedure(
1299
+ pg_catalog.quote_ident(pg_catalog.current_schema())
1300
+ || '.backfill_organization_session_ownership(uuid,integer,boolean,text)'
1301
+ );
1302
+ definition := pg_catalog.pg_get_functiondef(target);
1303
+ anchor := E' ledger_available := p_run_key IS NOT NULL';
1304
+ replacement :=
1305
+ E' IF NOT p_dry_run THEN\n'
1306
+ || E' PERFORM acquire_organization_session_tenancy_fences(\n'
1307
+ || E' p_organization_id\n'
1308
+ || E' );\n'
1309
+ || E' END IF;\n\n'
1310
+ || anchor;
1311
+ occurrences := (
1312
+ pg_catalog.length(definition)
1313
+ - pg_catalog.length(pg_catalog.replace(definition, anchor, ''))
1314
+ ) / pg_catalog.length(anchor);
1315
+ IF target IS NULL OR occurrences <> 1
1316
+ OR pg_catalog.strpos(
1317
+ definition,
1318
+ 'acquire_organization_session_tenancy_fences'
1319
+ ) > 0
1320
+ THEN
1321
+ RAISE EXCEPTION '0345 session ownership backfill definition drift'
1322
+ USING ERRCODE = '55000';
1323
+ END IF;
1324
+ patched := pg_catalog.replace(definition, anchor, replacement);
1325
+ EXECUTE patched;
1326
+
1327
+ target := pg_catalog.to_regprocedure(
1328
+ pg_catalog.quote_ident(pg_catalog.current_schema())
1329
+ || '.detach_scoped_machine_dependent_sessions(uuid,uuid,uuid)'
1330
+ );
1331
+ definition := pg_catalog.pg_get_functiondef(target);
1332
+ anchor :=
1333
+ E' INSERT INTO opengeni_private.scoped_compute_capabilities(\n'
1334
+ || E' backend_pid, transaction_id, capability_kind\n'
1335
+ || E' ) VALUES (pg_catalog.pg_backend_pid(), pg_catalog.pg_current_xact_id(), ''write'')\n'
1336
+ || E' ON CONFLICT DO NOTHING;';
1337
+ replacement :=
1338
+ anchor
1339
+ || E'\n PERFORM acquire_scoped_machine_session_tenancy_fences(\n'
1340
+ || E' p_account_id, p_sandbox_id\n'
1341
+ || E' );';
1342
+ occurrences := (
1343
+ pg_catalog.length(definition)
1344
+ - pg_catalog.length(pg_catalog.replace(definition, anchor, ''))
1345
+ ) / pg_catalog.length(anchor);
1346
+ IF target IS NULL OR occurrences <> 1
1347
+ OR pg_catalog.strpos(
1348
+ definition,
1349
+ 'acquire_scoped_machine_session_tenancy_fences'
1350
+ ) > 0
1351
+ THEN
1352
+ RAISE EXCEPTION '0345 scoped machine detach definition drift'
1353
+ USING ERRCODE = '55000';
1354
+ END IF;
1355
+ patched := pg_catalog.replace(definition, anchor, replacement);
1356
+ EXECUTE patched;
1357
+
1358
+ target := pg_catalog.to_regprocedure(
1359
+ pg_catalog.quote_ident(pg_catalog.current_schema())
1360
+ || '.accept_turn_personal_resource_attachment(uuid,uuid,uuid,uuid,text,integer,boolean,integer)'
1361
+ );
1362
+ definition := pg_catalog.pg_get_functiondef(target);
1363
+ anchor :=
1364
+ E' RAISE EXCEPTION ''session tenancy product is not activated'' USING ERRCODE = ''42501'';\n'
1365
+ || E' END IF;';
1366
+ replacement :=
1367
+ anchor
1368
+ || E'\n\n PERFORM acquire_session_tenancy_fence(p_workspace_id);';
1369
+ occurrences := (
1370
+ pg_catalog.length(definition)
1371
+ - pg_catalog.length(pg_catalog.replace(definition, anchor, ''))
1372
+ ) / pg_catalog.length(anchor);
1373
+ IF target IS NULL OR occurrences <> 1
1374
+ OR pg_catalog.strpos(definition, 'acquire_session_tenancy_fence') > 0
1375
+ THEN
1376
+ RAISE EXCEPTION '0345 accepted-turn attachment definition drift'
1377
+ USING ERRCODE = '55000';
1378
+ END IF;
1379
+ patched := pg_catalog.replace(definition, anchor, replacement);
1380
+ EXECUTE patched;
1381
+
1382
+ target := pg_catalog.to_regprocedure(
1383
+ pg_catalog.quote_ident(pg_catalog.current_schema())
1384
+ || '.materialize_scheduled_task_reusable_session_from_run('
1385
+ || 'uuid,uuid,uuid,uuid,uuid,bigint,text)'
1386
+ );
1387
+ definition := pg_catalog.pg_get_functiondef(target);
1388
+ anchor :=
1389
+ E' IF p_source_revision <= 0 OR p_source_digest !~ ''^[0-9a-f]{64}$'' THEN';
1390
+ replacement :=
1391
+ E' PERFORM acquire_session_tenancy_fence(p_workspace_id);\n\n'
1392
+ || anchor;
1393
+ occurrences := (
1394
+ pg_catalog.length(definition)
1395
+ - pg_catalog.length(pg_catalog.replace(definition, anchor, ''))
1396
+ ) / pg_catalog.length(anchor);
1397
+ IF target IS NULL OR occurrences <> 1
1398
+ OR pg_catalog.strpos(definition, 'acquire_session_tenancy_fence') > 0
1399
+ THEN
1400
+ RAISE EXCEPTION '0345 scheduled reusable-session materializer definition drift'
1401
+ USING ERRCODE = '55000';
1402
+ END IF;
1403
+ patched := pg_catalog.replace(definition, anchor, replacement);
1404
+ EXECUTE patched;
1405
+
1406
+ target := pg_catalog.to_regprocedure(
1407
+ pg_catalog.quote_ident(pg_catalog.current_schema())
1408
+ || '.workspace_membership_removal_command(jsonb)'
1409
+ );
1410
+ definition := pg_catalog.pg_get_functiondef(target);
1411
+ anchor := E' input_hash_value := pg_catalog.encode(';
1412
+ replacement :=
1413
+ E' PERFORM pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(\n'
1414
+ || E' ''organization-membership:'' || account_id_value::text, 0\n'
1415
+ || E' ));\n'
1416
+ || E' PERFORM acquire_session_tenancy_fence(workspace_id_value);\n\n'
1417
+ || anchor;
1418
+ occurrences := (
1419
+ pg_catalog.length(definition)
1420
+ - pg_catalog.length(pg_catalog.replace(definition, anchor, ''))
1421
+ ) / pg_catalog.length(anchor);
1422
+ IF target IS NULL OR occurrences <> 1
1423
+ OR pg_catalog.strpos(definition, 'acquire_session_tenancy_fence') > 0
1424
+ THEN
1425
+ RAISE EXCEPTION '0345 workspace membership removal definition drift'
1426
+ USING ERRCODE = '55000';
1427
+ END IF;
1428
+ patched := pg_catalog.replace(definition, anchor, replacement);
1429
+ EXECUTE patched;
1430
+ END
1431
+ $repair_app_mutator_session_tenancy_fences$;
1432
+
1433
+ -- A held advisory lock is necessary but not sufficient for the owner-only RLS
1434
+ -- branch. These five routines are the paths that need owner access after their
1435
+ -- bounded inventory helper returns. Give each invocation its own fenced-access
1436
+ -- token after the locks are acquired, and delete that exact token on every
1437
+ -- normal or exceptional exit. Refuse all frozen-definition drift.
1438
+ DO $repair_owner_fenced_access_scopes$
1439
+ DECLARE
1440
+ definition text;
1441
+ patched text;
1442
+ anchor text;
1443
+ replacement text;
1444
+ tail_anchor text;
1445
+ tail_replacement text;
1446
+ before_tail text;
1447
+ occurrences integer;
1448
+ BEGIN
1449
+ definition := pg_catalog.pg_get_functiondef(
1450
+ 'opengeni_private.reap_stale_interaction_transitions(bigint)'::regprocedure
1451
+ );
1452
+ anchor := E' settled_count integer := 0;';
1453
+ replacement := anchor || E'\n fenced_access_capability_id uuid;';
1454
+ occurrences := (
1455
+ pg_catalog.length(definition)
1456
+ - pg_catalog.length(pg_catalog.replace(definition, anchor, ''))
1457
+ ) / pg_catalog.length(anchor);
1458
+ IF occurrences <> 1
1459
+ OR pg_catalog.strpos(definition, 'fenced_access_capability_id') > 0
1460
+ THEN
1461
+ RAISE EXCEPTION '0345 interaction reaper fenced-access definition drift'
1462
+ USING ERRCODE = '55000';
1463
+ END IF;
1464
+ patched := pg_catalog.replace(definition, anchor, replacement);
1465
+ anchor := E' PERFORM acquire_sandbox_reaper_session_tenancy_fences();';
1466
+ replacement := anchor
1467
+ || E'\n fenced_access_capability_id :='
1468
+ || E'\n opengeni_private.open_session_tenancy_fenced_access('
1469
+ || E'\n session_tenancy_fence_target_schema()'
1470
+ || E'\n );';
1471
+ occurrences := (
1472
+ pg_catalog.length(patched)
1473
+ - pg_catalog.length(pg_catalog.replace(patched, anchor, ''))
1474
+ ) / pg_catalog.length(anchor);
1475
+ IF occurrences <> 1 THEN
1476
+ RAISE EXCEPTION '0345 interaction reaper fenced-access open anchor drift'
1477
+ USING ERRCODE = '55000';
1478
+ END IF;
1479
+ patched := pg_catalog.replace(patched, anchor, replacement);
1480
+ anchor := E' RETURN settled_count;';
1481
+ replacement :=
1482
+ E' PERFORM opengeni_private.close_session_tenancy_fenced_access('
1483
+ || E'\n fenced_access_capability_id'
1484
+ || E'\n );\n'
1485
+ || anchor;
1486
+ occurrences := (
1487
+ pg_catalog.length(patched)
1488
+ - pg_catalog.length(pg_catalog.replace(patched, anchor, ''))
1489
+ ) / pg_catalog.length(anchor);
1490
+ IF occurrences <> 1 THEN
1491
+ RAISE EXCEPTION '0345 interaction reaper fenced-access normal-close anchor drift'
1492
+ USING ERRCODE = '55000';
1493
+ END IF;
1494
+ patched := pg_catalog.replace(patched, anchor, replacement);
1495
+ tail_anchor := E' END;\n $function$\n';
1496
+ tail_replacement :=
1497
+ E' EXCEPTION WHEN OTHERS THEN\n'
1498
+ || E' PERFORM opengeni_private.close_session_tenancy_fenced_access(\n'
1499
+ || E' fenced_access_capability_id\n'
1500
+ || E' );\n'
1501
+ || E' RAISE;\n'
1502
+ || E' END;\n'
1503
+ || E' $function$\n';
1504
+ occurrences := (
1505
+ pg_catalog.length(patched)
1506
+ - pg_catalog.length(pg_catalog.replace(patched, tail_anchor, ''))
1507
+ ) / pg_catalog.length(tail_anchor);
1508
+ IF occurrences <> 1
1509
+ OR pg_catalog.right(patched, pg_catalog.length(tail_anchor))
1510
+ IS DISTINCT FROM tail_anchor
1511
+ THEN
1512
+ RAISE EXCEPTION '0345 interaction reaper fenced-access tail drift'
1513
+ USING ERRCODE = '55000';
1514
+ END IF;
1515
+ before_tail := patched;
1516
+ patched := pg_catalog.regexp_replace(
1517
+ patched,
1518
+ E' END;\\n \\$function\\$\\n$',
1519
+ tail_replacement
1520
+ );
1521
+ occurrences := (
1522
+ pg_catalog.length(patched)
1523
+ - pg_catalog.length(pg_catalog.replace(
1524
+ patched,
1525
+ 'close_session_tenancy_fenced_access',
1526
+ ''
1527
+ ))
1528
+ ) / pg_catalog.length('close_session_tenancy_fenced_access');
1529
+ IF patched IS NOT DISTINCT FROM before_tail
1530
+ OR pg_catalog.right(patched, pg_catalog.length(tail_replacement))
1531
+ IS DISTINCT FROM tail_replacement
1532
+ OR pg_catalog.strpos(patched, 'fenced_access_capability_id uuid;') = 0
1533
+ OR pg_catalog.strpos(patched, 'open_session_tenancy_fenced_access') = 0
1534
+ OR occurrences <> 2
1535
+ THEN
1536
+ RAISE EXCEPTION '0345 interaction reaper fenced-access repair failed'
1537
+ USING ERRCODE = '55000';
1538
+ END IF;
1539
+ EXECUTE patched;
1540
+
1541
+ definition := pg_catalog.pg_get_functiondef(
1542
+ 'opengeni_private.reap_sandbox_leases(bigint,bigint,bigint,bigint)'
1543
+ ::regprocedure
1544
+ );
1545
+ anchor := E' DECLARE\n locked_ids uuid[];';
1546
+ replacement := E' DECLARE\n fenced_access_capability_id uuid;\n'
1547
+ || E' locked_ids uuid[];';
1548
+ occurrences := (
1549
+ pg_catalog.length(definition)
1550
+ - pg_catalog.length(pg_catalog.replace(definition, anchor, ''))
1551
+ ) / pg_catalog.length(anchor);
1552
+ IF occurrences <> 1
1553
+ OR pg_catalog.strpos(definition, 'fenced_access_capability_id') > 0
1554
+ THEN
1555
+ RAISE EXCEPTION '0345 lease reaper fenced-access definition drift'
1556
+ USING ERRCODE = '55000';
1557
+ END IF;
1558
+ patched := pg_catalog.replace(definition, anchor, replacement);
1559
+ anchor := E' PERFORM acquire_sandbox_reaper_session_tenancy_fences();';
1560
+ replacement := anchor
1561
+ || E'\n fenced_access_capability_id :='
1562
+ || E'\n opengeni_private.open_session_tenancy_fenced_access('
1563
+ || E'\n session_tenancy_fence_target_schema()'
1564
+ || E'\n );';
1565
+ occurrences := (
1566
+ pg_catalog.length(patched)
1567
+ - pg_catalog.length(pg_catalog.replace(patched, anchor, ''))
1568
+ ) / pg_catalog.length(anchor);
1569
+ IF occurrences <> 1 THEN
1570
+ RAISE EXCEPTION '0345 lease reaper fenced-access open anchor drift'
1571
+ USING ERRCODE = '55000';
1572
+ END IF;
1573
+ patched := pg_catalog.replace(patched, anchor, replacement);
1574
+ anchor :=
1575
+ E' IF pg_catalog.cardinality(locked_ids) = 0 THEN\n'
1576
+ || E' RETURN;\n'
1577
+ || E' END IF;';
1578
+ replacement :=
1579
+ E' IF pg_catalog.cardinality(locked_ids) = 0 THEN\n'
1580
+ || E' PERFORM opengeni_private.close_session_tenancy_fenced_access(\n'
1581
+ || E' fenced_access_capability_id\n'
1582
+ || E' );\n'
1583
+ || E' RETURN;\n'
1584
+ || E' END IF;';
1585
+ occurrences := (
1586
+ pg_catalog.length(patched)
1587
+ - pg_catalog.length(pg_catalog.replace(patched, anchor, ''))
1588
+ ) / pg_catalog.length(anchor);
1589
+ IF occurrences <> 1 THEN
1590
+ RAISE EXCEPTION '0345 lease reaper fenced-access early-close anchor drift'
1591
+ USING ERRCODE = '55000';
1592
+ END IF;
1593
+ patched := pg_catalog.replace(patched, anchor, replacement);
1594
+ tail_anchor := E' END;\n $function$\n';
1595
+ tail_replacement :=
1596
+ E' PERFORM opengeni_private.close_session_tenancy_fenced_access(\n'
1597
+ || E' fenced_access_capability_id\n'
1598
+ || E' );\n'
1599
+ || E' EXCEPTION WHEN OTHERS THEN\n'
1600
+ || E' PERFORM opengeni_private.close_session_tenancy_fenced_access(\n'
1601
+ || E' fenced_access_capability_id\n'
1602
+ || E' );\n'
1603
+ || E' RAISE;\n'
1604
+ || E' END;\n'
1605
+ || E' $function$\n';
1606
+ occurrences := (
1607
+ pg_catalog.length(patched)
1608
+ - pg_catalog.length(pg_catalog.replace(patched, tail_anchor, ''))
1609
+ ) / pg_catalog.length(tail_anchor);
1610
+ IF occurrences <> 1
1611
+ OR pg_catalog.right(patched, pg_catalog.length(tail_anchor))
1612
+ IS DISTINCT FROM tail_anchor
1613
+ THEN
1614
+ RAISE EXCEPTION '0345 lease reaper fenced-access tail drift'
1615
+ USING ERRCODE = '55000';
1616
+ END IF;
1617
+ before_tail := patched;
1618
+ patched := pg_catalog.regexp_replace(
1619
+ patched,
1620
+ E' END;\\n \\$function\\$\\n$',
1621
+ tail_replacement
1622
+ );
1623
+ occurrences := (
1624
+ pg_catalog.length(patched)
1625
+ - pg_catalog.length(pg_catalog.replace(
1626
+ patched,
1627
+ 'close_session_tenancy_fenced_access',
1628
+ ''
1629
+ ))
1630
+ ) / pg_catalog.length('close_session_tenancy_fenced_access');
1631
+ IF patched IS NOT DISTINCT FROM before_tail
1632
+ OR pg_catalog.right(patched, pg_catalog.length(tail_replacement))
1633
+ IS DISTINCT FROM tail_replacement
1634
+ OR pg_catalog.strpos(patched, 'fenced_access_capability_id uuid;') = 0
1635
+ OR pg_catalog.strpos(patched, 'open_session_tenancy_fenced_access') = 0
1636
+ OR occurrences <> 3
1637
+ THEN
1638
+ RAISE EXCEPTION '0345 lease reaper fenced-access repair failed'
1639
+ USING ERRCODE = '55000';
1640
+ END IF;
1641
+ EXECUTE patched;
1642
+
1643
+ definition := pg_catalog.pg_get_functiondef(
1644
+ 'opengeni_private.request_due_sandbox_rotations(bigint,integer)'
1645
+ ::regprocedure
1646
+ );
1647
+ anchor := E' DECLARE\n requested integer;';
1648
+ replacement := E' DECLARE\n fenced_access_capability_id uuid;\n'
1649
+ || E' requested integer;';
1650
+ occurrences := (
1651
+ pg_catalog.length(definition)
1652
+ - pg_catalog.length(pg_catalog.replace(definition, anchor, ''))
1653
+ ) / pg_catalog.length(anchor);
1654
+ IF occurrences <> 1
1655
+ OR pg_catalog.strpos(definition, 'fenced_access_capability_id') > 0
1656
+ THEN
1657
+ RAISE EXCEPTION '0345 rotation fenced-access definition drift'
1658
+ USING ERRCODE = '55000';
1659
+ END IF;
1660
+ patched := pg_catalog.replace(definition, anchor, replacement);
1661
+ anchor :=
1662
+ E' PERFORM acquire_due_sandbox_rotation_session_tenancy_fences(\n'
1663
+ || E' p_lead_ms\n'
1664
+ || E' );';
1665
+ replacement := anchor
1666
+ || E'\n fenced_access_capability_id :='
1667
+ || E'\n opengeni_private.open_session_tenancy_fenced_access('
1668
+ || E'\n session_tenancy_fence_target_schema()'
1669
+ || E'\n );';
1670
+ occurrences := (
1671
+ pg_catalog.length(patched)
1672
+ - pg_catalog.length(pg_catalog.replace(patched, anchor, ''))
1673
+ ) / pg_catalog.length(anchor);
1674
+ IF occurrences <> 1 THEN
1675
+ RAISE EXCEPTION '0345 rotation fenced-access open anchor drift'
1676
+ USING ERRCODE = '55000';
1677
+ END IF;
1678
+ patched := pg_catalog.replace(patched, anchor, replacement);
1679
+ anchor := E' RETURN requested;';
1680
+ replacement :=
1681
+ E' PERFORM opengeni_private.close_session_tenancy_fenced_access(\n'
1682
+ || E' fenced_access_capability_id\n'
1683
+ || E' );\n'
1684
+ || anchor;
1685
+ occurrences := (
1686
+ pg_catalog.length(patched)
1687
+ - pg_catalog.length(pg_catalog.replace(patched, anchor, ''))
1688
+ ) / pg_catalog.length(anchor);
1689
+ IF occurrences <> 1 THEN
1690
+ RAISE EXCEPTION '0345 rotation fenced-access normal-close anchor drift'
1691
+ USING ERRCODE = '55000';
1692
+ END IF;
1693
+ patched := pg_catalog.replace(patched, anchor, replacement);
1694
+ tail_anchor := E' END;\n $function$\n';
1695
+ tail_replacement :=
1696
+ E' EXCEPTION WHEN OTHERS THEN\n'
1697
+ || E' PERFORM opengeni_private.close_session_tenancy_fenced_access(\n'
1698
+ || E' fenced_access_capability_id\n'
1699
+ || E' );\n'
1700
+ || E' RAISE;\n'
1701
+ || E' END;\n'
1702
+ || E' $function$\n';
1703
+ occurrences := (
1704
+ pg_catalog.length(patched)
1705
+ - pg_catalog.length(pg_catalog.replace(patched, tail_anchor, ''))
1706
+ ) / pg_catalog.length(tail_anchor);
1707
+ IF occurrences <> 1
1708
+ OR pg_catalog.right(patched, pg_catalog.length(tail_anchor))
1709
+ IS DISTINCT FROM tail_anchor
1710
+ THEN
1711
+ RAISE EXCEPTION '0345 rotation fenced-access tail drift'
1712
+ USING ERRCODE = '55000';
1713
+ END IF;
1714
+ before_tail := patched;
1715
+ patched := pg_catalog.regexp_replace(
1716
+ patched,
1717
+ E' END;\\n \\$function\\$\\n$',
1718
+ tail_replacement
1719
+ );
1720
+ occurrences := (
1721
+ pg_catalog.length(patched)
1722
+ - pg_catalog.length(pg_catalog.replace(
1723
+ patched,
1724
+ 'close_session_tenancy_fenced_access',
1725
+ ''
1726
+ ))
1727
+ ) / pg_catalog.length('close_session_tenancy_fenced_access');
1728
+ IF patched IS NOT DISTINCT FROM before_tail
1729
+ OR pg_catalog.right(patched, pg_catalog.length(tail_replacement))
1730
+ IS DISTINCT FROM tail_replacement
1731
+ OR pg_catalog.strpos(patched, 'fenced_access_capability_id uuid;') = 0
1732
+ OR pg_catalog.strpos(patched, 'open_session_tenancy_fenced_access') = 0
1733
+ OR occurrences <> 2
1734
+ THEN
1735
+ RAISE EXCEPTION '0345 rotation fenced-access repair failed'
1736
+ USING ERRCODE = '55000';
1737
+ END IF;
1738
+ EXECUTE patched;
1739
+
1740
+ definition := pg_catalog.pg_get_functiondef(
1741
+ 'opengeni_private.claim_terminal_retained_processes(uuid,integer,bigint)'
1742
+ ::regprocedure
1743
+ );
1744
+ anchor := E' BEGIN\n IF p_limit < 1 OR p_limit > 100 THEN';
1745
+ replacement :=
1746
+ E' DECLARE\n'
1747
+ || E' fenced_access_capability_id uuid;\n'
1748
+ || E' BEGIN\n'
1749
+ || E' IF p_limit < 1 OR p_limit > 100 THEN';
1750
+ occurrences := (
1751
+ pg_catalog.length(definition)
1752
+ - pg_catalog.length(pg_catalog.replace(definition, anchor, ''))
1753
+ ) / pg_catalog.length(anchor);
1754
+ IF occurrences <> 1
1755
+ OR pg_catalog.strpos(definition, 'fenced_access_capability_id') > 0
1756
+ THEN
1757
+ RAISE EXCEPTION '0345 retained-process fenced-access definition drift'
1758
+ USING ERRCODE = '55000';
1759
+ END IF;
1760
+ patched := pg_catalog.replace(definition, anchor, replacement);
1761
+ anchor := E' PERFORM acquire_due_retained_process_session_tenancy_fences();';
1762
+ replacement := anchor
1763
+ || E'\n fenced_access_capability_id :='
1764
+ || E'\n opengeni_private.open_session_tenancy_fenced_access('
1765
+ || E'\n session_tenancy_fence_target_schema()'
1766
+ || E'\n );';
1767
+ occurrences := (
1768
+ pg_catalog.length(patched)
1769
+ - pg_catalog.length(pg_catalog.replace(patched, anchor, ''))
1770
+ ) / pg_catalog.length(anchor);
1771
+ IF occurrences <> 1 THEN
1772
+ RAISE EXCEPTION '0345 retained-process fenced-access open anchor drift'
1773
+ USING ERRCODE = '55000';
1774
+ END IF;
1775
+ patched := pg_catalog.replace(patched, anchor, replacement);
1776
+ anchor := E' SET CONSTRAINTS '
1777
+ || pg_catalog.quote_ident(pg_catalog.current_schema())
1778
+ || E'.sandbox_retained_processes_identity_v2 DEFERRED;';
1779
+ replacement := anchor
1780
+ || E'\n PERFORM opengeni_private.close_session_tenancy_fenced_access('
1781
+ || E'\n fenced_access_capability_id'
1782
+ || E'\n );';
1783
+ occurrences := (
1784
+ pg_catalog.length(patched)
1785
+ - pg_catalog.length(pg_catalog.replace(patched, anchor, ''))
1786
+ ) / pg_catalog.length(anchor);
1787
+ IF occurrences <> 1 THEN
1788
+ RAISE EXCEPTION '0345 retained-process fenced-access normal-close anchor drift'
1789
+ USING ERRCODE = '55000';
1790
+ END IF;
1791
+ patched := pg_catalog.replace(patched, anchor, replacement);
1792
+ tail_anchor := E' END;\n $function$\n';
1793
+ tail_replacement :=
1794
+ E' EXCEPTION WHEN OTHERS THEN\n'
1795
+ || E' PERFORM opengeni_private.close_session_tenancy_fenced_access(\n'
1796
+ || E' fenced_access_capability_id\n'
1797
+ || E' );\n'
1798
+ || E' RAISE;\n'
1799
+ || E' END;\n'
1800
+ || E' $function$\n';
1801
+ occurrences := (
1802
+ pg_catalog.length(patched)
1803
+ - pg_catalog.length(pg_catalog.replace(patched, tail_anchor, ''))
1804
+ ) / pg_catalog.length(tail_anchor);
1805
+ IF occurrences <> 1
1806
+ OR pg_catalog.right(patched, pg_catalog.length(tail_anchor))
1807
+ IS DISTINCT FROM tail_anchor
1808
+ THEN
1809
+ RAISE EXCEPTION '0345 retained-process fenced-access tail drift'
1810
+ USING ERRCODE = '55000';
1811
+ END IF;
1812
+ before_tail := patched;
1813
+ patched := pg_catalog.regexp_replace(
1814
+ patched,
1815
+ E' END;\\n \\$function\\$\\n$',
1816
+ tail_replacement
1817
+ );
1818
+ occurrences := (
1819
+ pg_catalog.length(patched)
1820
+ - pg_catalog.length(pg_catalog.replace(
1821
+ patched,
1822
+ 'close_session_tenancy_fenced_access',
1823
+ ''
1824
+ ))
1825
+ ) / pg_catalog.length('close_session_tenancy_fenced_access');
1826
+ IF patched IS NOT DISTINCT FROM before_tail
1827
+ OR pg_catalog.right(patched, pg_catalog.length(tail_replacement))
1828
+ IS DISTINCT FROM tail_replacement
1829
+ OR pg_catalog.strpos(patched, 'fenced_access_capability_id uuid;') = 0
1830
+ OR pg_catalog.strpos(patched, 'open_session_tenancy_fenced_access') = 0
1831
+ OR occurrences <> 2
1832
+ THEN
1833
+ RAISE EXCEPTION '0345 retained-process fenced-access repair failed'
1834
+ USING ERRCODE = '55000';
1835
+ END IF;
1836
+ EXECUTE patched;
1837
+
1838
+ definition := pg_catalog.pg_get_functiondef(
1839
+ pg_catalog.to_regprocedure(
1840
+ pg_catalog.quote_ident(pg_catalog.current_schema())
1841
+ || '.detach_scoped_machine_dependent_sessions(uuid,uuid,uuid)'
1842
+ )
1843
+ );
1844
+ anchor := E'DECLARE\n dependent_workspace_id uuid;';
1845
+ replacement := E'DECLARE\n fenced_access_capability_id uuid;\n'
1846
+ || E' dependent_workspace_id uuid;';
1847
+ occurrences := (
1848
+ pg_catalog.length(definition)
1849
+ - pg_catalog.length(pg_catalog.replace(definition, anchor, ''))
1850
+ ) / pg_catalog.length(anchor);
1851
+ IF occurrences <> 1
1852
+ OR pg_catalog.strpos(definition, 'fenced_access_capability_id') > 0
1853
+ THEN
1854
+ RAISE EXCEPTION '0345 machine detach fenced-access definition drift'
1855
+ USING ERRCODE = '55000';
1856
+ END IF;
1857
+ patched := pg_catalog.replace(definition, anchor, replacement);
1858
+ anchor :=
1859
+ E' PERFORM acquire_scoped_machine_session_tenancy_fences(\n'
1860
+ || E' p_account_id, p_sandbox_id\n'
1861
+ || E' );';
1862
+ replacement := anchor
1863
+ || E'\n fenced_access_capability_id :='
1864
+ || E'\n opengeni_private.open_session_tenancy_fenced_access('
1865
+ || E'\n session_tenancy_fence_target_schema()'
1866
+ || E'\n );';
1867
+ occurrences := (
1868
+ pg_catalog.length(patched)
1869
+ - pg_catalog.length(pg_catalog.replace(patched, anchor, ''))
1870
+ ) / pg_catalog.length(anchor);
1871
+ IF occurrences <> 1 THEN
1872
+ RAISE EXCEPTION '0345 machine detach fenced-access open anchor drift'
1873
+ USING ERRCODE = '55000';
1874
+ END IF;
1875
+ patched := pg_catalog.replace(patched, anchor, replacement);
1876
+ anchor :=
1877
+ E' PERFORM pg_catalog.set_config(\n'
1878
+ || E' ''opengeni.session_activity_gate_workspace_id'', '
1879
+ || E'p_origin_workspace_id::text, true\n'
1880
+ || E' );\n'
1881
+ || E' DELETE FROM opengeni_private.scoped_compute_capabilities\n'
1882
+ || E' WHERE backend_pid = pg_catalog.pg_backend_pid()';
1883
+ replacement :=
1884
+ E' PERFORM pg_catalog.set_config(\n'
1885
+ || E' ''opengeni.session_activity_gate_workspace_id'', '
1886
+ || E'p_origin_workspace_id::text, true\n'
1887
+ || E' );\n'
1888
+ || E' PERFORM opengeni_private.close_session_tenancy_fenced_access(\n'
1889
+ || E' fenced_access_capability_id\n'
1890
+ || E' );\n'
1891
+ || E' DELETE FROM opengeni_private.scoped_compute_capabilities\n'
1892
+ || E' WHERE backend_pid = pg_catalog.pg_backend_pid()';
1893
+ occurrences := (
1894
+ pg_catalog.length(patched)
1895
+ - pg_catalog.length(pg_catalog.replace(patched, anchor, ''))
1896
+ ) / pg_catalog.length(anchor);
1897
+ IF occurrences <> 1 THEN
1898
+ RAISE EXCEPTION '0345 machine detach fenced-access normal-close anchor drift'
1899
+ USING ERRCODE = '55000';
1900
+ END IF;
1901
+ patched := pg_catalog.replace(patched, anchor, replacement);
1902
+ anchor :=
1903
+ E'EXCEPTION WHEN OTHERS THEN\n'
1904
+ || E' DELETE FROM opengeni_private.scoped_compute_capabilities\n'
1905
+ || E' WHERE backend_pid = pg_catalog.pg_backend_pid()';
1906
+ replacement :=
1907
+ E'EXCEPTION WHEN OTHERS THEN\n'
1908
+ || E' PERFORM opengeni_private.close_session_tenancy_fenced_access(\n'
1909
+ || E' fenced_access_capability_id\n'
1910
+ || E' );\n'
1911
+ || E' DELETE FROM opengeni_private.scoped_compute_capabilities\n'
1912
+ || E' WHERE backend_pid = pg_catalog.pg_backend_pid()';
1913
+ occurrences := (
1914
+ pg_catalog.length(patched)
1915
+ - pg_catalog.length(pg_catalog.replace(patched, anchor, ''))
1916
+ ) / pg_catalog.length(anchor);
1917
+ IF occurrences <> 1 THEN
1918
+ RAISE EXCEPTION '0345 machine detach fenced-access exception-close anchor drift'
1919
+ USING ERRCODE = '55000';
1920
+ END IF;
1921
+ patched := pg_catalog.replace(patched, anchor, replacement);
1922
+ tail_anchor := E' RAISE;\nEND\n$function$\n';
1923
+ occurrences := (
1924
+ pg_catalog.length(patched)
1925
+ - pg_catalog.length(pg_catalog.replace(patched, tail_anchor, ''))
1926
+ ) / pg_catalog.length(tail_anchor);
1927
+ IF occurrences <> 1
1928
+ OR pg_catalog.right(patched, pg_catalog.length(tail_anchor))
1929
+ IS DISTINCT FROM tail_anchor
1930
+ THEN
1931
+ RAISE EXCEPTION '0345 machine detach fenced-access tail drift'
1932
+ USING ERRCODE = '55000';
1933
+ END IF;
1934
+ occurrences := (
1935
+ pg_catalog.length(patched)
1936
+ - pg_catalog.length(pg_catalog.replace(
1937
+ patched,
1938
+ 'close_session_tenancy_fenced_access',
1939
+ ''
1940
+ ))
1941
+ ) / pg_catalog.length('close_session_tenancy_fenced_access');
1942
+ IF pg_catalog.strpos(patched, 'fenced_access_capability_id uuid;') = 0
1943
+ OR pg_catalog.strpos(patched, 'open_session_tenancy_fenced_access') = 0
1944
+ OR occurrences <> 2
1945
+ THEN
1946
+ RAISE EXCEPTION '0345 machine detach fenced-access repair failed'
1947
+ USING ERRCODE = '55000';
1948
+ END IF;
1949
+ EXECUTE patched;
1950
+ END
1951
+ $repair_owner_fenced_access_scopes$;
1952
+
1953
+ DO $install_session_tenancy_fences$
1954
+ DECLARE
1955
+ table_name text;
1956
+ hot_tables constant text[] := ARRAY[
1957
+ 'sessions', 'session_turns', 'session_turn_attempts',
1958
+ 'session_attempt_interruptions', 'session_system_updates',
1959
+ 'session_human_input_requests', 'session_pending_tool_calls', 'agent_run_states',
1960
+ 'session_goals', 'codex_capacity_waiters', 'xai_capacity_waiters',
1961
+ 'session_realtime_modes', 'session_realtime_connections', 'scheduled_tasks',
1962
+ 'sandbox_workspace_mutation_admissions', 'sandbox_retained_processes',
1963
+ 'sandbox_lease_holders'
1964
+ ];
1965
+ BEGIN
1966
+ FOREACH table_name IN ARRAY hot_tables LOOP
1967
+ EXECUTE format(
1968
+ 'CREATE TRIGGER session_tenancy_workspace_fence '
1969
+ || 'BEFORE INSERT OR UPDATE OR DELETE ON %I '
1970
+ || 'FOR EACH ROW EXECUTE FUNCTION opengeni_private.require_session_tenancy_fence()',
1971
+ table_name
1972
+ );
1973
+ END LOOP;
1974
+ END
1975
+ $install_session_tenancy_fences$;
1976
+
1977
+ CREATE OR REPLACE FUNCTION assert_session_tenancy_quiescent(
1978
+ p_account_id uuid,
1979
+ p_workspace_id uuid,
1980
+ p_session_id uuid,
1981
+ p_require_singleton_group boolean
1982
+ ) RETURNS void
1983
+ LANGUAGE plpgsql SECURITY DEFINER
1984
+ SET search_path FROM CURRENT
1985
+ AS $$
1986
+ DECLARE
1987
+ session_group_id uuid;
1988
+ previous_subject text := current_setting('opengeni.subject_id', true);
1989
+ blocker text;
1990
+ BEGIN
1991
+
1992
+ SELECT sandbox_group_id INTO session_group_id FROM sessions
1993
+ WHERE account_id = p_account_id AND workspace_id = p_workspace_id
1994
+ AND id = p_session_id;
1995
+
1996
+ IF EXISTS (SELECT 1 FROM session_turns WHERE workspace_id = p_workspace_id
1997
+ AND session_id = p_session_id
1998
+ AND status IN ('queued','running','requires_action','recovering','waiting_capacity'))
1999
+ THEN blocker := 'nonterminal_turn';
2000
+ ELSIF EXISTS (SELECT 1 FROM session_turn_attempts WHERE workspace_id = p_workspace_id
2001
+ AND session_id = p_session_id AND state <> 'closed')
2002
+ THEN blocker := 'nonterminal_attempt';
2003
+ ELSIF EXISTS (SELECT 1 FROM session_attempt_interruptions WHERE workspace_id = p_workspace_id
2004
+ AND session_id = p_session_id AND state IN ('pending','delivered','acknowledged'))
2005
+ THEN blocker := 'unsettled_interruption';
2006
+ ELSIF EXISTS (SELECT 1 FROM session_system_updates WHERE workspace_id = p_workspace_id
2007
+ AND session_id = p_session_id AND state = 'pending')
2008
+ THEN blocker := 'pending_system_update';
2009
+ ELSIF EXISTS (SELECT 1 FROM session_human_input_requests WHERE workspace_id = p_workspace_id
2010
+ AND session_id = p_session_id AND status = 'pending')
2011
+ THEN blocker := 'pending_human_input';
2012
+ ELSIF EXISTS (SELECT 1 FROM session_pending_tool_calls WHERE workspace_id = p_workspace_id
2013
+ AND session_id = p_session_id)
2014
+ THEN blocker := 'pending_tool_receipt';
2015
+ ELSIF EXISTS (SELECT 1 FROM agent_run_states WHERE workspace_id = p_workspace_id
2016
+ AND session_id = p_session_id)
2017
+ THEN blocker := 'run_state';
2018
+ ELSIF EXISTS (SELECT 1 FROM session_goals WHERE workspace_id = p_workspace_id
2019
+ AND session_id = p_session_id AND status = 'active')
2020
+ THEN blocker := 'active_goal';
2021
+ ELSIF EXISTS (SELECT 1 FROM codex_capacity_waiters WHERE workspace_id = p_workspace_id
2022
+ AND session_id = p_session_id AND status = 'waiting')
2023
+ OR EXISTS (SELECT 1 FROM xai_capacity_waiters WHERE workspace_id = p_workspace_id
2024
+ AND session_id = p_session_id AND status = 'waiting')
2025
+ THEN blocker := 'capacity_waiter';
2026
+ ELSIF EXISTS (SELECT 1 FROM session_realtime_modes WHERE workspace_id = p_workspace_id
2027
+ AND session_id = p_session_id AND state = 'active')
2028
+ OR EXISTS (SELECT 1 FROM session_realtime_connections WHERE workspace_id = p_workspace_id
2029
+ AND session_id = p_session_id
2030
+ AND state IN ('negotiating','ready','active'))
2031
+ THEN blocker := 'active_realtime';
2032
+ ELSIF EXISTS (SELECT 1 FROM scheduled_tasks WHERE workspace_id = p_workspace_id
2033
+ AND reusable_session_id = p_session_id AND status = 'active')
2034
+ THEN blocker := 'active_scheduled_task';
2035
+ ELSIF EXISTS (SELECT 1 FROM sandbox_workspace_mutation_admissions
2036
+ WHERE workspace_id = p_workspace_id AND session_id = p_session_id
2037
+ AND settled_at IS NULL)
2038
+ THEN blocker := 'workspace_mutation_admission';
2039
+ ELSIF EXISTS (SELECT 1 FROM sandbox_retained_processes
2040
+ WHERE workspace_id = p_workspace_id AND session_id = p_session_id
2041
+ AND state = 'active')
2042
+ THEN blocker := 'retained_process';
2043
+ ELSIF EXISTS (
2044
+ SELECT 1 FROM sandbox_lease_holders holder
2045
+ JOIN sandbox_leases lease ON lease.id = holder.lease_id
2046
+ WHERE lease.workspace_id = p_workspace_id
2047
+ AND lease.sandbox_group_id = session_group_id
2048
+ AND holder.kind IN ('viewer', 'interaction')
2049
+ )
2050
+ THEN blocker := 'active_sandbox_access';
2051
+ END IF;
2052
+
2053
+ IF blocker IS NULL AND p_require_singleton_group THEN
2054
+ -- The caller's own actor-scoped RLS must not hide a sibling. The function
2055
+ -- has already locked and identified the authorized source session.
2056
+ PERFORM set_config('opengeni.subject_id', '', true);
2057
+ IF EXISTS (SELECT 1 FROM sessions sibling
2058
+ WHERE sibling.account_id = p_account_id
2059
+ AND sibling.workspace_id = p_workspace_id
2060
+ AND sibling.sandbox_group_id = session_group_id
2061
+ AND sibling.id <> p_session_id)
2062
+ THEN blocker := 'shared_sandbox_group'; END IF;
2063
+ PERFORM set_config(
2064
+ 'opengeni.subject_id', CASE WHEN previous_subject IS NULL THEN '' ELSE previous_subject END, true
2065
+ );
2066
+ END IF;
2067
+
2068
+ IF blocker IS NOT NULL THEN
2069
+ RAISE EXCEPTION 'session tenancy mutation requires a quiescent session'
2070
+ USING ERRCODE = '55P03', DETAIL = blocker;
2071
+ END IF;
2072
+ EXCEPTION WHEN OTHERS THEN
2073
+ PERFORM set_config(
2074
+ 'opengeni.subject_id', CASE WHEN previous_subject IS NULL THEN '' ELSE previous_subject END, true
2075
+ );
2076
+ RAISE;
2077
+ END
2078
+ $$;
2079
+
2080
+ CREATE OR REPLACE FUNCTION transition_session_visibility(
2081
+ p_account_id uuid,
2082
+ p_workspace_id uuid,
2083
+ p_session_id uuid,
2084
+ p_actor_subject_id text,
2085
+ p_target_visibility text,
2086
+ p_expected_authority_epoch integer,
2087
+ p_operation_key text,
2088
+ p_canonical_request_hash text,
2089
+ p_activation_version integer
2090
+ ) RETURNS TABLE (
2091
+ operation_id uuid,
2092
+ event_id uuid,
2093
+ event_sequence integer,
2094
+ visibility text,
2095
+ authority_epoch integer,
2096
+ owner_organization_membership_id uuid,
2097
+ changed boolean,
2098
+ replay boolean,
2099
+ interrupted_attempt_count integer,
2100
+ cancelled_turn_count integer,
2101
+ cancelled_update_count integer,
2102
+ paused_goal_count integer,
2103
+ revoked_grant_count integer
2104
+ )
2105
+ LANGUAGE plpgsql SECURITY DEFINER
2106
+ SET search_path FROM CURRENT
2107
+ AS $$
2108
+ DECLARE
2109
+ actor_membership organization_memberships%ROWTYPE;
2110
+ session_row sessions%ROWTYPE;
2111
+ receipt_row session_command_receipts%ROWTYPE;
2112
+ new_epoch integer;
2113
+ grant_count integer := 0;
2114
+ event_row_id uuid;
2115
+ event_row_sequence integer;
2116
+ visibility_write_capability_id uuid := gen_random_uuid();
2117
+ previous_visibility_capability text := current_setting(
2118
+ 'opengeni.session_visibility_write_capability', true
2119
+ );
2120
+ previous_lifecycle text := current_setting('opengeni.organization_tenancy_lifecycle', true);
2121
+ BEGIN
2122
+ IF p_account_id IS NULL OR p_workspace_id IS NULL OR p_session_id IS NULL
2123
+ OR p_actor_subject_id IS NULL OR p_target_visibility IS NULL
2124
+ OR p_expected_authority_epoch IS NULL OR p_operation_key IS NULL
2125
+ OR p_canonical_request_hash IS NULL OR p_activation_version IS NULL
2126
+ THEN RAISE EXCEPTION 'session visibility transition requires complete authority'
2127
+ USING ERRCODE = '42501'; END IF;
2128
+ IF p_account_id IS DISTINCT FROM opengeni_private.current_account_id()
2129
+ OR p_workspace_id IS DISTINCT FROM opengeni_private.current_workspace_id()
2130
+ OR p_actor_subject_id IS DISTINCT FROM opengeni_private.current_subject_id()
2131
+ THEN RAISE EXCEPTION 'session visibility transition authority is invalid'
2132
+ USING ERRCODE = '42501'; END IF;
2133
+ IF p_target_visibility NOT IN ('user_private', 'workspace_shared')
2134
+ OR p_expected_authority_epoch < 1
2135
+ OR p_actor_subject_id <> btrim(p_actor_subject_id)
2136
+ OR length(p_actor_subject_id) NOT BETWEEN 1 AND 1024
2137
+ OR p_operation_key <> btrim(p_operation_key)
2138
+ OR length(p_operation_key) NOT BETWEEN 1 AND 1024
2139
+ OR p_canonical_request_hash !~ '^[0-9a-f]{64}$'
2140
+ OR p_activation_version <> 1
2141
+ THEN RAISE EXCEPTION 'session visibility transition request is invalid'
2142
+ USING ERRCODE = '22023'; END IF;
2143
+ IF NOT session_tenancy_product_activated(p_account_id, p_activation_version) THEN
2144
+ RAISE EXCEPTION 'session tenancy product surface is not activated for this organization'
2145
+ USING ERRCODE = '55000';
2146
+ END IF;
2147
+
2148
+ -- Match the canonical organization-membership lifecycle prefix before any
2149
+ -- table/row lock. This keeps visibility changes from reintroducing the
2150
+ -- workspace/account lock cycle repaired by migration 0299.
2151
+ PERFORM pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
2152
+ 'organization-membership:' || p_account_id::text, 0
2153
+ ));
2154
+
2155
+ PERFORM pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
2156
+ 'session-tenancy:' || p_workspace_id::text, 0
2157
+ ));
2158
+
2159
+
2160
+ PERFORM set_config('opengeni.organization_tenancy_lifecycle',
2161
+ 'session_visibility_activation', true);
2162
+ PERFORM 1 FROM workspaces workspace_row
2163
+ WHERE workspace_row.id = p_workspace_id AND workspace_row.account_id = p_account_id
2164
+ FOR UPDATE;
2165
+ IF NOT FOUND THEN RAISE EXCEPTION 'session visibility transition workspace is unavailable'
2166
+ USING ERRCODE = '42501'; END IF;
2167
+
2168
+ SELECT membership.* INTO actor_membership
2169
+ FROM organization_memberships membership
2170
+ WHERE membership.account_id = p_account_id
2171
+ AND membership.subject_id = p_actor_subject_id
2172
+ AND membership.status = 'active'
2173
+ FOR UPDATE;
2174
+ IF NOT FOUND OR NOT (
2175
+ actor_membership.personal_workspace_id = p_workspace_id
2176
+ OR EXISTS (
2177
+ SELECT 1 FROM workspace_memberships workspace_membership
2178
+ WHERE workspace_membership.account_id = p_account_id
2179
+ AND workspace_membership.workspace_id = p_workspace_id
2180
+ AND workspace_membership.subject_id = p_actor_subject_id
2181
+ )
2182
+ ) THEN RAISE EXCEPTION 'session visibility transition requires active membership'
2183
+ USING ERRCODE = '42501'; END IF;
2184
+
2185
+ SELECT session.* INTO session_row FROM sessions session
2186
+ WHERE session.account_id = p_account_id AND session.workspace_id = p_workspace_id
2187
+ AND session.id = p_session_id
2188
+ FOR UPDATE;
2189
+ IF NOT FOUND THEN RAISE EXCEPTION 'session visibility transition session is unavailable'
2190
+ USING ERRCODE = 'P0002'; END IF;
2191
+ IF session_row.owner_organization_membership_id IS DISTINCT FROM actor_membership.id
2192
+ OR session_row.owner_subject_id IS DISTINCT FROM actor_membership.subject_id
2193
+ THEN RAISE EXCEPTION 'session visibility transition is owner-only'
2194
+ USING ERRCODE = '42501'; END IF;
2195
+
2196
+ INSERT INTO session_command_receipts (
2197
+ account_id, workspace_id, actor_type, actor_subject_id, action,
2198
+ target_session_id, operation_key, canonical_request_hash
2199
+ ) VALUES (
2200
+ p_account_id, p_workspace_id, 'human', p_actor_subject_id,
2201
+ 'session.visibility.change', p_session_id, p_operation_key, p_canonical_request_hash
2202
+ ) ON CONFLICT DO NOTHING;
2203
+ SELECT receipt.* INTO receipt_row FROM session_command_receipts receipt
2204
+ WHERE receipt.workspace_id = p_workspace_id AND receipt.actor_type = 'human'
2205
+ AND receipt.actor_subject_id = p_actor_subject_id
2206
+ AND receipt.actor_attempt_id IS NULL
2207
+ AND receipt.action = 'session.visibility.change'
2208
+ AND receipt.target_session_id = p_session_id AND receipt.target_turn_id IS NULL
2209
+ AND receipt.operation_key = p_operation_key
2210
+ FOR UPDATE;
2211
+ IF receipt_row.canonical_request_hash <> p_canonical_request_hash THEN
2212
+ RAISE EXCEPTION 'session visibility transition idempotency conflict'
2213
+ USING ERRCODE = '23505';
2214
+ END IF;
2215
+ IF receipt_row.result ->> 'status' = 'applied' THEN
2216
+ operation_id := receipt_row.id;
2217
+ event_id := nullif(receipt_row.result ->> 'eventId', '')::uuid;
2218
+ event_sequence := nullif(receipt_row.result ->> 'eventSequence', '')::integer;
2219
+ visibility := receipt_row.result ->> 'visibility';
2220
+ authority_epoch := (receipt_row.result ->> 'authorityEpoch')::integer;
2221
+ owner_organization_membership_id := actor_membership.id;
2222
+ changed := (receipt_row.result ->> 'changed')::boolean;
2223
+ replay := true;
2224
+ interrupted_attempt_count := 0; cancelled_turn_count := 0;
2225
+ cancelled_update_count := 0; paused_goal_count := 0;
2226
+ revoked_grant_count := (receipt_row.result ->> 'revokedGrantCount')::integer;
2227
+ RETURN NEXT; RETURN;
2228
+ END IF;
2229
+ IF session_row.authority_epoch <> p_expected_authority_epoch THEN
2230
+ RAISE EXCEPTION 'session visibility transition authority epoch conflict'
2231
+ USING ERRCODE = '40001';
2232
+ END IF;
2233
+
2234
+ new_epoch := session_row.authority_epoch;
2235
+ IF session_row.visibility <> p_target_visibility THEN
2236
+ PERFORM assert_session_tenancy_quiescent(
2237
+ p_account_id, p_workspace_id, p_session_id, p_target_visibility = 'user_private'
2238
+ );
2239
+ new_epoch := session_row.authority_epoch + 1;
2240
+ IF new_epoch < 2 THEN RAISE EXCEPTION 'session authority epoch exhausted'
2241
+ USING ERRCODE = '22003'; END IF;
2242
+
2243
+ UPDATE organization_user_resource_grants grant_row
2244
+ SET status = 'revoked', revoked_at = clock_timestamp(),
2245
+ generation = grant_row.generation + 1, updated_at = clock_timestamp()
2246
+ WHERE grant_row.account_id = p_account_id
2247
+ AND grant_row.workspace_id = p_workspace_id
2248
+ AND grant_row.session_id = p_session_id
2249
+ AND grant_row.authority_epoch = session_row.authority_epoch
2250
+ AND grant_row.status = 'active';
2251
+ GET DIAGNOSTICS grant_count = ROW_COUNT;
2252
+
2253
+ INSERT INTO session_visibility_write_capabilities (
2254
+ backend_pid, transaction_id, capability_id
2255
+ ) VALUES (pg_backend_pid(), pg_current_xact_id(), visibility_write_capability_id);
2256
+ PERFORM set_config('opengeni.session_visibility_write_capability',
2257
+ visibility_write_capability_id::text, true);
2258
+ UPDATE sessions transition_target SET
2259
+ visibility = p_target_visibility,
2260
+ authority_epoch = new_epoch,
2261
+ initial_personal_connection_delegations = '[]'::jsonb,
2262
+ last_sequence = session_row.last_sequence + 1,
2263
+ updated_at = clock_timestamp()
2264
+ WHERE transition_target.id = p_session_id
2265
+ AND transition_target.authority_epoch = session_row.authority_epoch;
2266
+ IF NOT FOUND THEN RAISE EXCEPTION 'session visibility transition lost authority epoch CAS'
2267
+ USING ERRCODE = '40001'; END IF;
2268
+
2269
+ INSERT INTO session_events (
2270
+ account_id, workspace_id, session_id, sequence, type, payload, occurred_at
2271
+ ) VALUES (
2272
+ p_account_id, p_workspace_id, p_session_id, session_row.last_sequence + 1,
2273
+ 'session.visibility.changed',
2274
+ jsonb_build_object(
2275
+ 'operationId', receipt_row.id,
2276
+ 'fromVisibility', CASE session_row.visibility WHEN 'user_private' THEN 'private' ELSE 'workspace' END,
2277
+ 'toVisibility', CASE p_target_visibility WHEN 'user_private' THEN 'private' ELSE 'workspace' END,
2278
+ 'previousAuthorityEpoch', session_row.authority_epoch,
2279
+ 'authorityEpoch', new_epoch,
2280
+ 'interruptedAttemptCount', 0, 'cancelledTurnCount', 0,
2281
+ 'cancelledUpdateCount', 0, 'pausedGoalCount', 0,
2282
+ 'revokedGrantCount', grant_count
2283
+ ), clock_timestamp()
2284
+ ) RETURNING id, sequence INTO event_row_id, event_row_sequence;
2285
+ DELETE FROM session_visibility_write_capabilities capability
2286
+ WHERE capability.backend_pid = pg_backend_pid()
2287
+ AND capability.transaction_id = pg_current_xact_id()
2288
+ AND capability.capability_id = visibility_write_capability_id;
2289
+ PERFORM set_config('opengeni.session_visibility_write_capability',
2290
+ CASE WHEN previous_visibility_capability IS NULL THEN '' ELSE previous_visibility_capability END,
2291
+ true);
2292
+ END IF;
2293
+
2294
+ UPDATE session_command_receipts SET result = jsonb_build_object(
2295
+ 'status', 'applied', 'eventId', event_row_id,
2296
+ 'eventSequence', event_row_sequence, 'visibility', p_target_visibility,
2297
+ 'authorityEpoch', new_epoch, 'changed', session_row.visibility <> p_target_visibility,
2298
+ 'revokedGrantCount', grant_count
2299
+ ), updated_at = clock_timestamp() WHERE id = receipt_row.id;
2300
+
2301
+ operation_id := receipt_row.id; event_id := event_row_id;
2302
+ event_sequence := event_row_sequence; visibility := p_target_visibility;
2303
+ authority_epoch := new_epoch;
2304
+ owner_organization_membership_id := actor_membership.id;
2305
+ changed := session_row.visibility <> p_target_visibility; replay := false;
2306
+ interrupted_attempt_count := 0; cancelled_turn_count := 0;
2307
+ cancelled_update_count := 0; paused_goal_count := 0;
2308
+ revoked_grant_count := grant_count;
2309
+ PERFORM set_config('opengeni.organization_tenancy_lifecycle',
2310
+ CASE WHEN previous_lifecycle IS NULL THEN '' ELSE previous_lifecycle END, true);
2311
+ RETURN NEXT;
2312
+ EXCEPTION WHEN OTHERS THEN
2313
+ PERFORM set_config('opengeni.organization_tenancy_lifecycle',
2314
+ CASE WHEN previous_lifecycle IS NULL THEN '' ELSE previous_lifecycle END, true);
2315
+ PERFORM set_config('opengeni.session_visibility_write_capability',
2316
+ CASE WHEN previous_visibility_capability IS NULL THEN '' ELSE previous_visibility_capability END,
2317
+ true);
2318
+ RAISE;
2319
+ END
2320
+ $$;
2321
+
2322
+ CREATE OR REPLACE FUNCTION fork_session_content(
2323
+ p_account_id uuid,
2324
+ p_source_workspace_id uuid,
2325
+ p_source_session_id uuid,
2326
+ p_actor_subject_id text,
2327
+ p_destination_workspace_id uuid,
2328
+ p_destination_visibility text,
2329
+ p_workspace_shared_acknowledged boolean,
2330
+ p_operation_key text,
2331
+ p_canonical_request_hash text,
2332
+ p_activation_version integer
2333
+ ) RETURNS TABLE (
2334
+ operation_id uuid,
2335
+ event_id uuid,
2336
+ event_sequence integer,
2337
+ session_id uuid,
2338
+ workspace_id uuid,
2339
+ visibility text,
2340
+ authority_epoch integer,
2341
+ copied_history_item_count integer,
2342
+ replay boolean
2343
+ )
2344
+ LANGUAGE plpgsql SECURITY DEFINER
2345
+ SET search_path FROM CURRENT
2346
+ AS $$
2347
+ DECLARE
2348
+ actor_membership organization_memberships%ROWTYPE;
2349
+ source_session sessions%ROWTYPE;
2350
+ destination_workspace workspaces%ROWTYPE;
2351
+ receipt_row session_command_receipts%ROWTYPE;
2352
+ destination_session_id uuid;
2353
+ history_count integer := 0;
2354
+ destination_activity_revision bigint;
2355
+ destination_depth integer;
2356
+ destination_depth_source text;
2357
+ destination_resources jsonb := '[]'::jsonb;
2358
+ event_row_id uuid;
2359
+ public_destination_visibility text;
2360
+ visibility_write_capability_id uuid := gen_random_uuid();
2361
+ previous_lifecycle text := current_setting('opengeni.organization_tenancy_lifecycle', true);
2362
+ previous_gate_state text := current_setting('opengeni.session_activity_gate_state', true);
2363
+ previous_gate_workspace text := current_setting(
2364
+ 'opengeni.session_activity_gate_workspace_id', true
2365
+ );
2366
+ previous_visibility_capability text := current_setting(
2367
+ 'opengeni.session_visibility_write_capability', true
2368
+ );
2369
+ BEGIN
2370
+ IF p_account_id IS NULL OR p_source_workspace_id IS NULL
2371
+ OR p_source_session_id IS NULL OR p_actor_subject_id IS NULL
2372
+ OR p_destination_workspace_id IS NULL OR p_destination_visibility IS NULL
2373
+ OR p_workspace_shared_acknowledged IS NULL
2374
+ OR p_operation_key IS NULL OR p_canonical_request_hash IS NULL
2375
+ OR p_activation_version IS NULL
2376
+ THEN RAISE EXCEPTION 'session fork requires complete authority'
2377
+ USING ERRCODE = '42501'; END IF;
2378
+ IF p_account_id IS DISTINCT FROM opengeni_private.current_account_id()
2379
+ OR p_source_workspace_id IS DISTINCT FROM opengeni_private.current_workspace_id()
2380
+ OR p_actor_subject_id IS DISTINCT FROM opengeni_private.current_subject_id()
2381
+ THEN RAISE EXCEPTION 'session fork authority is invalid'
2382
+ USING ERRCODE = '42501'; END IF;
2383
+ IF p_destination_workspace_id IS DISTINCT FROM p_source_workspace_id
2384
+ OR p_destination_visibility NOT IN ('user_private', 'workspace_shared')
2385
+ OR (p_destination_visibility = 'user_private' AND p_workspace_shared_acknowledged)
2386
+ OR p_actor_subject_id <> btrim(p_actor_subject_id)
2387
+ OR length(p_actor_subject_id) NOT BETWEEN 1 AND 1024
2388
+ OR p_operation_key <> btrim(p_operation_key)
2389
+ OR length(p_operation_key) NOT BETWEEN 1 AND 1024
2390
+ OR p_canonical_request_hash !~ '^[0-9a-f]{64}$'
2391
+ OR p_activation_version <> 1
2392
+ THEN RAISE EXCEPTION 'session fork request is invalid'
2393
+ USING ERRCODE = '22023'; END IF;
2394
+ IF nullif(previous_gate_state, '') IS NOT NULL
2395
+ OR nullif(previous_gate_workspace, '') IS NOT NULL
2396
+ THEN RAISE EXCEPTION 'session fork requires ownership of the activity gate'
2397
+ USING ERRCODE = '55000'; END IF;
2398
+ IF NOT session_tenancy_product_activated(p_account_id, p_activation_version) THEN
2399
+ RAISE EXCEPTION 'session tenancy product surface is not activated for this organization'
2400
+ USING ERRCODE = '55000';
2401
+ END IF;
2402
+
2403
+ PERFORM pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
2404
+ 'organization-membership:' || p_account_id::text, 0
2405
+ ));
2406
+
2407
+ PERFORM pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
2408
+ 'session-tenancy:' || p_source_workspace_id::text, 0
2409
+ ));
2410
+
2411
+ PERFORM set_config('opengeni.organization_tenancy_lifecycle',
2412
+ 'session_visibility_activation', true);
2413
+ SELECT * INTO destination_workspace FROM workspaces workspace_row
2414
+ WHERE workspace_row.account_id = p_account_id
2415
+ AND workspace_row.id = p_source_workspace_id
2416
+ FOR UPDATE;
2417
+ IF NOT FOUND THEN RAISE EXCEPTION 'session fork workspace is unavailable'
2418
+ USING ERRCODE = '42501'; END IF;
2419
+
2420
+ SELECT membership.* INTO actor_membership
2421
+ FROM organization_memberships membership
2422
+ WHERE membership.account_id = p_account_id
2423
+ AND membership.subject_id = p_actor_subject_id
2424
+ AND membership.status = 'active'
2425
+ FOR UPDATE;
2426
+ IF NOT FOUND OR NOT (
2427
+ actor_membership.personal_workspace_id = p_source_workspace_id
2428
+ OR EXISTS (
2429
+ SELECT 1 FROM workspace_memberships workspace_membership
2430
+ WHERE workspace_membership.account_id = p_account_id
2431
+ AND workspace_membership.workspace_id = p_source_workspace_id
2432
+ AND workspace_membership.subject_id = p_actor_subject_id
2433
+ )
2434
+ ) THEN RAISE EXCEPTION 'session fork requires active workspace authority'
2435
+ USING ERRCODE = '42501'; END IF;
2436
+
2437
+ -- Resolve an already-applied receipt before inspecting mutable source state.
2438
+ -- The same still-authorized workspace actor can therefore recover the exact
2439
+ -- destination after a lost response even if the source owner later makes the
2440
+ -- source private. A new key still reaches the current source authorization
2441
+ -- check below and is denied.
2442
+ SELECT receipt.* INTO receipt_row FROM session_command_receipts receipt
2443
+ WHERE receipt.account_id = p_account_id
2444
+ AND receipt.workspace_id = p_source_workspace_id
2445
+ AND receipt.actor_type = 'human' AND receipt.actor_subject_id = p_actor_subject_id
2446
+ AND receipt.actor_attempt_id IS NULL AND receipt.action = 'session.fork'
2447
+ AND receipt.target_session_id = p_source_session_id
2448
+ AND receipt.target_turn_id IS NULL AND receipt.operation_key = p_operation_key
2449
+ AND receipt.result ->> 'status' = 'applied';
2450
+ IF receipt_row.canonical_request_hash <> p_canonical_request_hash THEN
2451
+ RAISE EXCEPTION 'session fork idempotency conflict' USING ERRCODE = '23505';
2452
+ END IF;
2453
+ IF receipt_row.result ->> 'status' = 'applied' THEN
2454
+ operation_id := receipt_row.id;
2455
+ event_id := nullif(receipt_row.result ->> 'eventId', '')::uuid;
2456
+ event_sequence := (receipt_row.result ->> 'eventSequence')::integer;
2457
+ session_id := (receipt_row.result ->> 'sessionId')::uuid;
2458
+ workspace_id := (receipt_row.result ->> 'workspaceId')::uuid;
2459
+ visibility := receipt_row.result ->> 'visibility'; authority_epoch := 1;
2460
+ copied_history_item_count :=
2461
+ (receipt_row.result ->> 'copiedHistoryItemCount')::integer;
2462
+ replay := true; RETURN NEXT; RETURN;
2463
+ END IF;
2464
+
2465
+ -- Product decision, distinct from authority, and the same one migration 0323
2466
+ -- makes on the create path: a private destination inside a shared workspace
2467
+ -- requires the organization's private-session setting. A personal workspace
2468
+ -- is exempt exactly as it is there. This is deliberately placed after the
2469
+ -- keyed replay above and before any source read, so a fork that already
2470
+ -- committed still replays byte-identically after an owner disables the
2471
+ -- setting, while a fresh key fails closed with the same SQLSTATE the create
2472
+ -- path raises.
2473
+ IF p_destination_visibility = 'user_private'
2474
+ AND actor_membership.personal_workspace_id IS DISTINCT FROM p_source_workspace_id
2475
+ AND NOT organization_private_sessions_enabled(p_account_id)
2476
+ THEN
2477
+ RAISE EXCEPTION
2478
+ 'private sessions are not enabled for this organization''s shared workspaces'
2479
+ USING ERRCODE = '55000';
2480
+ END IF;
2481
+
2482
+ SELECT session.* INTO source_session FROM sessions session
2483
+ WHERE session.account_id = p_account_id
2484
+ AND session.workspace_id = p_source_workspace_id
2485
+ AND session.id = p_source_session_id
2486
+ FOR UPDATE;
2487
+ IF NOT FOUND THEN RAISE EXCEPTION 'session fork source session is unavailable'
2488
+ USING ERRCODE = 'P0002'; END IF;
2489
+ -- A private source remains owner-only. A workspace-shared source follows
2490
+ -- current workspace authority, and the fork becomes a fresh session owned
2491
+ -- by the actor. This is what lets any authorized collaborator fork shared
2492
+ -- work privately without retaining the source owner's authority.
2493
+ IF source_session.visibility = 'user_private' AND (
2494
+ source_session.owner_organization_membership_id IS DISTINCT FROM actor_membership.id
2495
+ OR source_session.owner_subject_id IS DISTINCT FROM actor_membership.subject_id
2496
+ )
2497
+ THEN RAISE EXCEPTION 'session fork source session is private'
2498
+ USING ERRCODE = '42501'; END IF;
2499
+
2500
+ INSERT INTO session_command_receipts (
2501
+ account_id, workspace_id, actor_type, actor_subject_id, action,
2502
+ target_session_id, operation_key, canonical_request_hash
2503
+ ) VALUES (
2504
+ p_account_id, p_source_workspace_id, 'human', p_actor_subject_id,
2505
+ 'session.fork', p_source_session_id, p_operation_key, p_canonical_request_hash
2506
+ ) ON CONFLICT DO NOTHING;
2507
+ SELECT receipt.* INTO receipt_row FROM session_command_receipts receipt
2508
+ WHERE receipt.account_id = p_account_id
2509
+ AND receipt.workspace_id = p_source_workspace_id
2510
+ AND receipt.actor_type = 'human' AND receipt.actor_subject_id = p_actor_subject_id
2511
+ AND receipt.actor_attempt_id IS NULL AND receipt.action = 'session.fork'
2512
+ AND receipt.target_session_id = p_source_session_id
2513
+ AND receipt.target_turn_id IS NULL AND receipt.operation_key = p_operation_key
2514
+ FOR UPDATE;
2515
+ IF receipt_row.canonical_request_hash <> p_canonical_request_hash THEN
2516
+ RAISE EXCEPTION 'session fork idempotency conflict' USING ERRCODE = '23505';
2517
+ END IF;
2518
+ IF receipt_row.result ->> 'status' = 'applied' THEN
2519
+ operation_id := receipt_row.id;
2520
+ event_id := nullif(receipt_row.result ->> 'eventId', '')::uuid;
2521
+ event_sequence := (receipt_row.result ->> 'eventSequence')::integer;
2522
+ session_id := (receipt_row.result ->> 'sessionId')::uuid;
2523
+ workspace_id := (receipt_row.result ->> 'workspaceId')::uuid;
2524
+ visibility := receipt_row.result ->> 'visibility'; authority_epoch := 1;
2525
+ copied_history_item_count :=
2526
+ (receipt_row.result ->> 'copiedHistoryItemCount')::integer;
2527
+ replay := true; RETURN NEXT; RETURN;
2528
+ END IF;
2529
+
2530
+ -- The acknowledgement is content-exposure evidence, not a blanket checkbox:
2531
+ -- it is required only when private source content crosses into workspace scope.
2532
+ IF source_session.visibility = 'user_private'
2533
+ AND p_destination_visibility = 'workspace_shared'
2534
+ AND NOT p_workspace_shared_acknowledged
2535
+ THEN RAISE EXCEPTION 'private-to-workspace fork requires explicit acknowledgement'
2536
+ USING ERRCODE = '22023'; END IF;
2537
+
2538
+ PERFORM assert_session_tenancy_quiescent(
2539
+ p_account_id, p_source_workspace_id, p_source_session_id, true
2540
+ );
2541
+
2542
+ SELECT coalesce(jsonb_agg(
2543
+ CASE WHEN resource.value ->> 'connectionType' = 'github_personal'
2544
+ THEN resource.value
2545
+ - 'provider' - 'connectionType' - 'credentialBindingId' - 'access'
2546
+ - 'repositoryId' - 'installationId' - 'projectId' - 'connectionId'
2547
+ - 'githubInstallationId' - 'githubRepositoryId'
2548
+ ELSE resource.value
2549
+ - 'credentialBindingId' - 'connectionId' - 'installationId'
2550
+ - 'projectId' - 'githubInstallationId'
2551
+ END
2552
+ ORDER BY resource.ordinality
2553
+ ), '[]'::jsonb) INTO destination_resources
2554
+ FROM jsonb_array_elements(source_session.resources)
2555
+ WITH ORDINALITY AS resource(value, ordinality);
2556
+
2557
+ DROP TABLE IF EXISTS pg_temp.opengeni_session_fork_history_spool;
2558
+ CREATE TEMP TABLE opengeni_session_fork_history_spool (
2559
+ position numeric NOT NULL,
2560
+ item jsonb NOT NULL,
2561
+ item_codec_version integer,
2562
+ active boolean NOT NULL,
2563
+ provider_artifact_invalidated_at timestamptz,
2564
+ provider_artifact_invalidation_reason text,
2565
+ provider_artifact_invalidated_by_attempt_id uuid,
2566
+ created_at timestamptz NOT NULL
2567
+ ) ON COMMIT DROP;
2568
+ INSERT INTO pg_temp.opengeni_session_fork_history_spool (
2569
+ position, item, item_codec_version, active,
2570
+ provider_artifact_invalidated_at, provider_artifact_invalidation_reason,
2571
+ provider_artifact_invalidated_by_attempt_id, created_at
2572
+ ) SELECT source_item.position, source_item.item,
2573
+ source_item.item_codec_version, source_item.active,
2574
+ source_item.provider_artifact_invalidated_at,
2575
+ source_item.provider_artifact_invalidation_reason,
2576
+ source_item.provider_artifact_invalidated_by_attempt_id,
2577
+ source_item.created_at
2578
+ FROM session_history_items source_item
2579
+ WHERE source_item.account_id = p_account_id
2580
+ AND source_item.workspace_id = p_source_workspace_id
2581
+ AND source_item.session_id = p_source_session_id
2582
+ ORDER BY source_item.position;
2583
+ GET DIAGNOSTICS history_count = ROW_COUNT;
2584
+
2585
+ SELECT coalesce(
2586
+ CASE WHEN (destination_workspace.settings ->> 'maxNestedAgentDepth') ~ '^\d+$'
2587
+ THEN (destination_workspace.settings ->> 'maxNestedAgentDepth')::integer END,
2588
+ configuration.max_nested_agent_depth
2589
+ ),
2590
+ CASE WHEN (destination_workspace.settings ->> 'maxNestedAgentDepth') ~ '^\d+$'
2591
+ THEN 'workspace' ELSE configuration.policy_source END
2592
+ INTO destination_depth, destination_depth_source
2593
+ FROM nested_agent_depth_configuration configuration
2594
+ WHERE configuration.singleton = true;
2595
+ IF destination_depth IS NULL OR destination_depth_source IS NULL THEN
2596
+ RAISE EXCEPTION 'session fork destination depth policy is unavailable'
2597
+ USING ERRCODE = '55000';
2598
+ END IF;
2599
+
2600
+ destination_session_id := gen_random_uuid();
2601
+ public_destination_visibility := CASE p_destination_visibility
2602
+ WHEN 'user_private' THEN 'private' ELSE 'workspace' END;
2603
+ INSERT INTO session_visibility_write_capabilities (
2604
+ backend_pid, transaction_id, capability_id
2605
+ ) VALUES (pg_backend_pid(), pg_current_xact_id(), visibility_write_capability_id);
2606
+ PERFORM set_config('opengeni.session_visibility_write_capability',
2607
+ visibility_write_capability_id::text, true);
2608
+ PERFORM set_config('opengeni.session_activity_gate_state', 'open', true);
2609
+ PERFORM set_config('opengeni.session_activity_gate_workspace_id',
2610
+ p_source_workspace_id::text, true);
2611
+
2612
+ -- This one insert establishes fresh ownership, authority epoch, provenance,
2613
+ -- root identity and singleton sandbox group. No live source authority is copied.
2614
+ INSERT INTO sessions (
2615
+ id, account_id, workspace_id, status,
2616
+ initial_message, initial_message_codec_version,
2617
+ title, title_source, instructions, policy_role,
2618
+ resources, skills, tools, metadata,
2619
+ created_by_kind, created_by_subject_id, created_by_context,
2620
+ owner_organization_membership_id, owner_subject_id,
2621
+ visibility, create_requested_visibility, authority_epoch,
2622
+ forked_from_session_id, forked_from_authority_epoch,
2623
+ forked_from_visibility, forked_at, forked_by_organization_membership_id,
2624
+ model, reasoning_effort, latency_mode, sandbox_backend, sandbox_os, sandbox_group_id,
2625
+ first_party_mcp_permissions, first_party_mcp_tools,
2626
+ initial_personal_connection_delegations, tool_policy,
2627
+ root_session_id, nested_agent_depth,
2628
+ max_nested_agent_depth_override, effective_max_nested_agent_depth,
2629
+ nested_agent_depth_policy_source, nested_agent_depth_policy_session_id,
2630
+ temporal_workflow_id, active_turn_id, variable_set_id,
2631
+ rig_id, rig_version_id, active_sandbox_id, active_epoch,
2632
+ working_dir, codex_pinned_credential_id, codex_last_credential_id,
2633
+ codex_pin_source, codex_compaction_mode,
2634
+ queue_version, queue_head_position, queue_tail_position, last_sequence
2635
+ ) VALUES (
2636
+ destination_session_id, p_account_id, p_source_workspace_id, 'idle',
2637
+ source_session.initial_message, source_session.initial_message_codec_version,
2638
+ source_session.title, source_session.title_source,
2639
+ source_session.instructions, source_session.policy_role,
2640
+ destination_resources, '[]'::jsonb, '[]'::jsonb, '{}'::jsonb,
2641
+ 'subject', p_actor_subject_id, jsonb_build_object(
2642
+ 'fork', true,
2643
+ 'sourceSessionId', p_source_session_id,
2644
+ 'sourceAuthorityEpoch', source_session.authority_epoch,
2645
+ 'sourceVisibility', CASE source_session.visibility
2646
+ WHEN 'user_private' THEN 'private' ELSE 'workspace' END,
2647
+ 'workspaceSharedAcknowledged', p_workspace_shared_acknowledged
2648
+ ),
2649
+ actor_membership.id, actor_membership.subject_id,
2650
+ -- create_requested_visibility mirrors the destination the caller actually
2651
+ -- asked for. Leaving it at the column default made a private fork row
2652
+ -- internally inconsistent with its own visibility.
2653
+ p_destination_visibility, p_destination_visibility, 1,
2654
+ p_source_session_id, source_session.authority_epoch,
2655
+ source_session.visibility, clock_timestamp(), actor_membership.id,
2656
+ source_session.model, source_session.reasoning_effort, source_session.latency_mode,
2657
+ source_session.sandbox_backend, source_session.sandbox_os, destination_session_id,
2658
+ NULL, '[]'::jsonb, '[]'::jsonb,
2659
+ '{"mode":"explicit","inheritedFromSessionId":null}'::jsonb,
2660
+ destination_session_id, 0, NULL, destination_depth,
2661
+ destination_depth_source, NULL,
2662
+ NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, NULL,
2663
+ source_session.codex_compaction_mode,
2664
+ 0, 0, 0, 1
2665
+ );
2666
+
2667
+ INSERT INTO session_history_items (
2668
+ account_id, workspace_id, session_id, turn_id,
2669
+ position, item, item_codec_version, active,
2670
+ provider_artifact_invalidated_at, provider_artifact_invalidation_reason,
2671
+ provider_artifact_invalidated_by_attempt_id, created_at
2672
+ ) SELECT p_account_id, p_source_workspace_id, destination_session_id, NULL,
2673
+ source_item.position, source_item.item, source_item.item_codec_version,
2674
+ source_item.active, source_item.provider_artifact_invalidated_at,
2675
+ source_item.provider_artifact_invalidation_reason,
2676
+ source_item.provider_artifact_invalidated_by_attempt_id, source_item.created_at
2677
+ FROM pg_temp.opengeni_session_fork_history_spool source_item
2678
+ ORDER BY source_item.position;
2679
+
2680
+ INSERT INTO session_events (
2681
+ account_id, workspace_id, session_id, sequence, type, payload, occurred_at
2682
+ ) VALUES (
2683
+ p_account_id, p_source_workspace_id, destination_session_id, 1,
2684
+ 'session.created', jsonb_build_object(
2685
+ 'forked', true, 'sourceSessionId', p_source_session_id,
2686
+ 'sourceAuthorityEpoch', source_session.authority_epoch,
2687
+ 'sourceVisibility', CASE source_session.visibility
2688
+ WHEN 'user_private' THEN 'private' ELSE 'workspace' END,
2689
+ 'visibility', public_destination_visibility,
2690
+ 'workspaceSharedAcknowledged', p_workspace_shared_acknowledged,
2691
+ 'copiedHistoryItemCount', history_count
2692
+ ), clock_timestamp()
2693
+ ) RETURNING id INTO event_row_id;
2694
+
2695
+ PERFORM set_config('opengeni.session_activity_gate_state', 'preparing', true);
2696
+ SET CONSTRAINTS ALL IMMEDIATE;
2697
+ SET CONSTRAINTS sessions_activity_insert_commit_guard,
2698
+ sessions_activity_update_commit_guard DEFERRED;
2699
+ PERFORM set_config('opengeni.session_activity_gate_state', 'finalizing', true);
2700
+ UPDATE workspace_session_activity_revisions counter
2701
+ SET revision = counter.revision + 1
2702
+ WHERE counter.workspace_id = p_source_workspace_id
2703
+ RETURNING counter.revision INTO destination_activity_revision;
2704
+ IF destination_activity_revision IS NULL THEN
2705
+ RAISE EXCEPTION 'session fork destination activity counter is unavailable'
2706
+ USING ERRCODE = '55000';
2707
+ END IF;
2708
+ UPDATE sessions destination_session SET
2709
+ activity_revision = destination_activity_revision,
2710
+ activity_revision_pending_xid = NULL
2711
+ WHERE destination_session.id = destination_session_id
2712
+ AND destination_session.activity_revision_pending_xid
2713
+ = pg_current_xact_id()::text::bigint;
2714
+ IF NOT FOUND THEN RAISE EXCEPTION 'session fork activity was not finalized'
2715
+ USING ERRCODE = '55000'; END IF;
2716
+ SET CONSTRAINTS sessions_activity_insert_commit_guard,
2717
+ sessions_activity_update_commit_guard IMMEDIATE;
2718
+
2719
+ PERFORM set_config('opengeni.session_activity_gate_state', '', true);
2720
+ PERFORM set_config('opengeni.session_activity_gate_workspace_id', '', true);
2721
+ DELETE FROM session_visibility_write_capabilities capability
2722
+ WHERE capability.backend_pid = pg_backend_pid()
2723
+ AND capability.transaction_id = pg_current_xact_id()
2724
+ AND capability.capability_id = visibility_write_capability_id;
2725
+ PERFORM set_config('opengeni.session_visibility_write_capability',
2726
+ CASE WHEN previous_visibility_capability IS NULL THEN '' ELSE previous_visibility_capability END,
2727
+ true);
2728
+
2729
+ UPDATE session_command_receipts SET result = jsonb_build_object(
2730
+ 'status', 'applied', 'eventId', event_row_id, 'eventSequence', 1,
2731
+ 'sessionId', destination_session_id, 'workspaceId', p_source_workspace_id,
2732
+ 'visibility', p_destination_visibility, 'authorityEpoch', 1,
2733
+ 'workspaceSharedAcknowledged', p_workspace_shared_acknowledged,
2734
+ 'copiedHistoryItemCount', history_count
2735
+ ), updated_at = clock_timestamp() WHERE id = receipt_row.id;
2736
+
2737
+ operation_id := receipt_row.id; event_id := event_row_id; event_sequence := 1;
2738
+ session_id := destination_session_id; workspace_id := p_source_workspace_id;
2739
+ visibility := p_destination_visibility; authority_epoch := 1;
2740
+ copied_history_item_count := history_count; replay := false;
2741
+ PERFORM set_config('opengeni.organization_tenancy_lifecycle',
2742
+ CASE WHEN previous_lifecycle IS NULL THEN '' ELSE previous_lifecycle END, true);
2743
+ RETURN NEXT;
2744
+ EXCEPTION WHEN OTHERS THEN
2745
+ PERFORM set_config('opengeni.organization_tenancy_lifecycle',
2746
+ CASE WHEN previous_lifecycle IS NULL THEN '' ELSE previous_lifecycle END, true);
2747
+ PERFORM set_config('opengeni.session_activity_gate_state',
2748
+ CASE WHEN previous_gate_state IS NULL THEN '' ELSE previous_gate_state END, true);
2749
+ PERFORM set_config('opengeni.session_activity_gate_workspace_id',
2750
+ CASE WHEN previous_gate_workspace IS NULL THEN '' ELSE previous_gate_workspace END, true);
2751
+ PERFORM set_config('opengeni.session_visibility_write_capability',
2752
+ CASE WHEN previous_visibility_capability IS NULL THEN '' ELSE previous_visibility_capability END,
2753
+ true);
2754
+ RAISE;
2755
+ END
2756
+ $$;
2757
+
2758
+ -- CREATE OR REPLACE evaluates `SET search_path FROM CURRENT` against the
2759
+ -- migrator connection. Re-pin every replaced definer to the target schema so
2760
+ -- a dedicated-schema install cannot retain the connection's broader fallback
2761
+ -- path (notably opengeni_private/public) in its function metadata.
2762
+ DO $pin_session_tenancy_definers$
2763
+ DECLARE
2764
+ target_schema text := current_schema();
2765
+ BEGIN
2766
+ EXECUTE format(
2767
+ 'ALTER FUNCTION opengeni_private.reap_stale_interaction_transitions(bigint) '
2768
+ || 'SET search_path = pg_catalog, %I, pg_temp',
2769
+ target_schema
2770
+ );
2771
+ EXECUTE format(
2772
+ 'ALTER FUNCTION opengeni_private.reap_sandbox_leases(bigint,bigint,bigint,bigint) '
2773
+ || 'SET search_path = pg_catalog, %I, pg_temp',
2774
+ target_schema
2775
+ );
2776
+ EXECUTE format(
2777
+ 'ALTER FUNCTION opengeni_private.claim_terminal_retained_processes(uuid,integer,bigint) '
2778
+ || 'SET search_path = pg_catalog, %I, pg_temp',
2779
+ target_schema
2780
+ );
2781
+ EXECUTE format(
2782
+ 'ALTER FUNCTION opengeni_private.request_due_sandbox_rotations(bigint,integer) '
2783
+ || 'SET search_path = pg_catalog, %I, pg_temp',
2784
+ target_schema
2785
+ );
2786
+ EXECUTE format(
2787
+ 'ALTER FUNCTION %I.acquire_sandbox_reaper_session_tenancy_fences() '
2788
+ || 'SET search_path = pg_catalog, %I, pg_temp',
2789
+ target_schema,
2790
+ target_schema
2791
+ );
2792
+ EXECUTE format(
2793
+ 'ALTER FUNCTION %I.acquire_due_retained_process_session_tenancy_fences() '
2794
+ || 'SET search_path = pg_catalog, %I, pg_temp',
2795
+ target_schema,
2796
+ target_schema
2797
+ );
2798
+ EXECUTE format(
2799
+ 'ALTER FUNCTION %I.acquire_due_sandbox_rotation_session_tenancy_fences(bigint) '
2800
+ || 'SET search_path = pg_catalog, %I, pg_temp',
2801
+ target_schema,
2802
+ target_schema
2803
+ );
2804
+ EXECUTE format(
2805
+ 'ALTER FUNCTION %I.acquire_scoped_machine_session_tenancy_fences(uuid,uuid) '
2806
+ || 'SET search_path = pg_catalog, %I, pg_temp',
2807
+ target_schema,
2808
+ target_schema
2809
+ );
2810
+ EXECUTE format(
2811
+ 'ALTER FUNCTION %I.acquire_organization_session_tenancy_fences(uuid) '
2812
+ || 'SET search_path = pg_catalog, %I, pg_temp',
2813
+ target_schema,
2814
+ target_schema
2815
+ );
2816
+ EXECUTE format(
2817
+ 'ALTER FUNCTION %I.assert_session_tenancy_quiescent(uuid,uuid,uuid,boolean) '
2818
+ || 'SET search_path = pg_catalog, %I, pg_temp',
2819
+ target_schema,
2820
+ target_schema
2821
+ );
2822
+ EXECUTE format(
2823
+ 'ALTER FUNCTION %I.transition_session_visibility(uuid,uuid,uuid,text,text,integer,text,text,integer) '
2824
+ || 'SET search_path = pg_catalog, %I, pg_temp',
2825
+ target_schema,
2826
+ target_schema
2827
+ );
2828
+ EXECUTE format(
2829
+ 'ALTER FUNCTION %I.fork_session_content(uuid,uuid,uuid,text,uuid,text,boolean,text,text,integer) '
2830
+ || 'SET search_path = pg_catalog, %I, pg_temp',
2831
+ target_schema,
2832
+ target_schema
2833
+ );
2834
+ END
2835
+ $pin_session_tenancy_definers$;
2836
+
2837
+
2838
+ RESET statement_timeout;
2839
+ RESET lock_timeout;