@opengeni/db 1.0.1 → 3.1.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) hide show
  1. package/dist/attached-browser-devices.d.ts +7 -0
  2. package/dist/automations.d.ts +154 -0
  3. package/dist/canonical-human-identities.js +3 -3
  4. package/dist/capability-integrations.d.ts +13 -0
  5. package/dist/child-lifecycle-notices.d.ts +68 -0
  6. package/dist/{chunk-LTG3S6ZY.js → chunk-EULAN2NP.js} +2 -2
  7. package/dist/{chunk-CUPM3FGB.js → chunk-NJVZPWXY.js} +3 -3
  8. package/dist/{chunk-4SR4HKKA.js → chunk-NMUBV7Z5.js} +19 -11
  9. package/dist/chunk-NMUBV7Z5.js.map +1 -0
  10. package/dist/chunk-OF5OBB76.js +4457 -0
  11. package/dist/chunk-OF5OBB76.js.map +1 -0
  12. package/dist/{chunk-6ZYKCRFO.js → chunk-PR2WMLOQ.js} +7580 -4417
  13. package/dist/chunk-PR2WMLOQ.js.map +1 -0
  14. package/dist/{chunk-ZWREYEWA.js → chunk-SCPUJS53.js} +41 -4
  15. package/dist/chunk-SCPUJS53.js.map +1 -0
  16. package/dist/{chunk-SK7YYHBI.js → chunk-SIBYBA2D.js} +46 -1
  17. package/dist/chunk-SIBYBA2D.js.map +1 -0
  18. package/dist/chunk-TIKF3KTI.js +383 -0
  19. package/dist/chunk-TIKF3KTI.js.map +1 -0
  20. package/dist/chunk-ZZ5LJEFX.js +516 -0
  21. package/dist/chunk-ZZ5LJEFX.js.map +1 -0
  22. package/dist/codemode-operations.d.ts +1 -6
  23. package/dist/company-brain-context-selection-schema.d.ts +705 -0
  24. package/dist/company-brain-context-selection.d.ts +94 -0
  25. package/dist/company-brain-governed-writes.d.ts +24 -0
  26. package/dist/company-brain.d.ts +55 -0
  27. package/dist/company-profile-agent-admin.d.ts +15 -0
  28. package/dist/company-profile-schema.d.ts +619 -2
  29. package/dist/company-profile.d.ts +9 -4
  30. package/dist/connection-authority.d.ts +95 -0
  31. package/dist/connection-scopes.d.ts +8 -0
  32. package/dist/connection-tenancy-backfill.d.ts +41 -0
  33. package/dist/connection-token-resolver.d.ts +30 -1
  34. package/dist/database.d.ts +19 -0
  35. package/dist/editable-artifact-durable-export.js +2 -2
  36. package/dist/editable-artifacts.d.ts +2 -2
  37. package/dist/editable-artifacts.js +3 -3
  38. package/dist/governed-learning-activation-schema.d.ts +984 -0
  39. package/dist/governed-learning-activation.d.ts +98 -0
  40. package/dist/governed-learning-evaluator-schema.d.ts +633 -0
  41. package/dist/governed-learning-evaluator.d.ts +28 -0
  42. package/dist/index.d.ts +1595 -81
  43. package/dist/index.js +21571 -8126
  44. package/dist/index.js.map +1 -1
  45. package/dist/insights.d.ts +18 -0
  46. package/dist/integration-bindings.d.ts +1 -0
  47. package/dist/integration-facets.d.ts +30 -0
  48. package/dist/interaction-placement-loss.d.ts +22 -0
  49. package/dist/managed-human-provisioning.d.ts +25 -0
  50. package/dist/memory-domain.d.ts +3 -3
  51. package/dist/migrate.d.ts +11 -1
  52. package/dist/migrate.js +1 -1
  53. package/dist/new-session-drafts.d.ts +43 -5
  54. package/dist/organization-membership-backfill.d.ts +174 -0
  55. package/dist/organization-membership-lifecycle.d.ts +162 -0
  56. package/dist/pr-review.d.ts +107 -0
  57. package/dist/preference-registry-schema.d.ts +2 -0
  58. package/dist/preference-registry.d.ts +2 -0
  59. package/dist/provision-roles.js +1 -1
  60. package/dist/runtime-posture.d.ts +22 -8
  61. package/dist/schema.d.ts +24914 -13703
  62. package/dist/schema.js +99 -1
  63. package/dist/scoped-knowledge-schema.d.ts +52 -1
  64. package/dist/session-background-commands.d.ts +125 -0
  65. package/dist/session-background-commands.js +34 -0
  66. package/dist/session-background-commands.js.map +1 -0
  67. package/dist/session-control.d.ts +156 -3
  68. package/dist/session-goal-pacing.d.ts +68 -0
  69. package/dist/session-queue-commands.d.ts +41 -4
  70. package/dist/session-realtime-ledger.d.ts +2 -0
  71. package/dist/session-tenancy.d.ts +71 -1
  72. package/dist/session-tenancy.js +25 -3
  73. package/dist/session-tool-call-settlement.d.ts +9 -4
  74. package/dist/slack-routing-personal-workspace.d.ts +23 -0
  75. package/dist/slack-routing.d.ts +153 -0
  76. package/dist/slack-task-policy-schema.d.ts +34 -0
  77. package/dist/slack-task-policy.d.ts +8 -0
  78. package/dist/slack-user-link-access.d.ts +1 -0
  79. package/dist/task-notes-schema.d.ts +594 -0
  80. package/dist/task-notes.d.ts +39 -0
  81. package/dist/turn-initiator.d.ts +2 -0
  82. package/dist/user-resource-authority.d.ts +109 -0
  83. package/dist/video-generation.js +3 -3
  84. package/dist/workspace-authority.d.ts +81 -0
  85. package/dist/workspace-instruction-policies-schema.d.ts +279 -0
  86. package/dist/workspace-instruction-policies.d.ts +62 -0
  87. package/dist/workspace-learning-policy.d.ts +1 -0
  88. package/dist/workspace-membership-permissions.d.ts +2 -0
  89. package/drizzle/0240_enrollment_connection_authority.sql +39 -0
  90. package/drizzle/0241_atomic_personal_resource_delegation.sql +1000 -0
  91. package/drizzle/0241_enrollment_agent_runtime.sql +70 -0
  92. package/drizzle/0244_slack_app_home_refresh_queue.sql +120 -0
  93. package/drizzle/0245_model_context_contribution_facts.sql +88 -0
  94. package/drizzle/0246_integration_personal_instance_authority.sql +70 -0
  95. package/drizzle/0247_terraform_stacks_provenance_repair.sql +428 -0
  96. package/drizzle/0248_terraform_stacks_component_resolution_fence.sql +83 -0
  97. package/drizzle/0249_personal_resource_delegation_authority_correction.sql +662 -0
  98. package/drizzle/0250_direct_retained_process_owner_liveness.sql +199 -0
  99. package/drizzle/0251_connected_machine_operation_policy.sql +45 -0
  100. package/drizzle/0252_scheduled_personal_resource_delegation.sql +1867 -0
  101. package/drizzle/0253_common_user_resource_authority_lifecycle.sql +1555 -0
  102. package/drizzle/0254_scoped_variable_set_authority.sql +1255 -0
  103. package/drizzle/0255_company_brain_governed_write_proposals.sql +524 -0
  104. package/drizzle/0256_connection_authority_delegation.sql +703 -0
  105. package/drizzle/0257_goal_revision_decisions_and_root_constraints.sql +436 -0
  106. package/drizzle/0258_three_scope_document_knowledge_authority.sql +1273 -0
  107. package/drizzle/0259_company_brain_context_selection_receipts.sql +869 -0
  108. package/drizzle/0260_task_note_knowledge_promotion.sql +820 -0
  109. package/drizzle/0261_preference_knowledge_proposal_actor_binding.sql +471 -0
  110. package/drizzle/0262_scoped_connected_machines_and_rigs.sql +1700 -0
  111. package/drizzle/0263_organization_membership_lifecycle.sql +3284 -0
  112. package/drizzle/0264_connection_authority_runtime_activation.sql +1084 -0
  113. package/drizzle/0266_company_brain_context_receipt_inspection.sql +156 -0
  114. package/drizzle/0268_governed_learning_decision_receipts.sql +825 -0
  115. package/drizzle/0269_governed_learning_activation_controller.sql +1874 -0
  116. package/drizzle/0270_governed_learning_history_inspection.sql +170 -0
  117. package/drizzle/0271_company_brain_retrieval_only_default.sql +524 -0
  118. package/drizzle/0272_human_confirmed_learning_activation.sql +591 -0
  119. package/drizzle/0273_scheduled_variable_set_materialization.sql +268 -0
  120. package/drizzle/0274_human_confirmed_knowledge_review.sql +310 -0
  121. package/drizzle/0275_scheduled_connection_authority.sql +5527 -0
  122. package/drizzle/0276_onboarding_proposal_initiating_human_guc.sql +198 -0
  123. package/drizzle/0277_workspace_writer_authority_attribution.sql +393 -0
  124. package/drizzle/0278_workspace_membership_removal_fencing.sql +1174 -0
  125. package/drizzle/0279_workspace_connection_use_lane.sql +343 -0
  126. package/drizzle/0280_connection_and_variable_set_audit_attribution.sql +773 -0
  127. package/drizzle/0281_viewer_holder_authority_claims.sql +52 -0
  128. package/drizzle/0282_variable_set_session_attach_attribution.sql +178 -0
  129. package/drizzle/0283_editable_spreadsheet_authored_state.sql +82 -0
  130. package/drizzle/0284_truthful_human_confirmed_review_reason.sql +930 -0
  131. package/drizzle/0285_organization_tenancy_inventory.sql +329 -0
  132. package/drizzle/0286_widen_task_note_expiry_ceiling.sql +391 -0
  133. package/drizzle/0287_open_suffix_pending_tool_calls.sql +18 -0
  134. package/drizzle/0288_attached_browser_reenrollment.sql +64 -0
  135. package/drizzle/0289_session_composer_policy_authority.sql +597 -0
  136. package/drizzle/0290_organization_membership_backfill.sql +268 -0
  137. package/drizzle/0291_resource_authority_classification_assertion.sql +371 -0
  138. package/drizzle/0292_truthful_tenancy_inventory_counters.sql +291 -0
  139. package/drizzle/0293_confirm_time_rule_rebaseline.sql +615 -0
  140. package/drizzle/0294_preference_activation_authority.sql +237 -0
  141. package/drizzle/0295_retire_legacy_standing_memory_mode.sql +476 -0
  142. package/drizzle/0296_force_rls_backfill_noop_repair.sql +215 -0
  143. package/drizzle/0297_session_ownership_classification_and_backfill.sql +752 -0
  144. package/drizzle/0298_organization_tenancy_parity.sql +609 -0
  145. package/drizzle/0299_organization_membership_lock_order.sql +291 -0
  146. package/drizzle/0300_tenancy_backfill_ledger.sql +459 -0
  147. package/drizzle/0301_session_snapshot_and_pin_visibility.sql +181 -0
  148. package/drizzle/0302_personal_workspace_session_ownership.sql +303 -0
  149. package/drizzle/0303_session_tenancy_product_activation.sql +919 -0
  150. package/drizzle/0304_personal_workspace_private_session_reads.sql +97 -0
  151. package/drizzle/0305_personal_resource_grant_management.sql +584 -0
  152. package/drizzle/0306_atomic_personal_resource_attachments.sql +1374 -0
  153. package/drizzle/0307_session_attention_state.sql +51 -0
  154. package/drizzle/0308_session_archives.sql +35 -0
  155. package/drizzle/0309_channel_project_pins.sql +6 -0
  156. package/drizzle/0310_channel_project_order.sql +27 -0
  157. package/drizzle/0311_company_scope_and_private_session_create.sql +656 -0
  158. package/drizzle/0312_quiescent_session_tree_deletion.sql +39 -0
  159. package/drizzle/0313_private_child_session_authority.sql +365 -0
  160. package/drizzle/0314_unregistered_organization_invitations.sql +606 -0
  161. package/drizzle/0315_personal_github_repository_selection.sql +624 -0
  162. package/drizzle/0316_human_confirmed_activation_resumed_generation.sql +869 -0
  163. package/drizzle/0317_session_goal_continuation_hold.sql +64 -0
  164. package/drizzle/0318_session_turn_startup_milestones.sql +115 -0
  165. package/drizzle/0319_event_triggered_automations.sql +254 -0
  166. package/drizzle/0320_pr_review_pack.sql +113 -0
  167. package/drizzle/0321_slack_bot_environment_display_name.sql +15 -0
  168. package/drizzle/0322_session_turns_unclaimed_prompt_trigger_index.sql +5 -0
  169. package/drizzle/0323_organization_private_session_enablement.sql +489 -0
  170. package/drizzle/0324_human_confirmed_company_profile_agent_admin.sql +856 -0
  171. package/drizzle/0325_child_lifecycle_notices.sql +44 -0
  172. package/drizzle/0326_interaction_operation_error_codes.sql +62 -0
  173. package/drizzle/0327_slack_first_task_hint.sql +29 -0
  174. package/drizzle/0328_session_background_commands.sql +590 -0
  175. package/drizzle/0329_slack_orchestration_delivery_events.sql +91 -0
  176. package/drizzle/0330_api_key_descriptions.sql +20 -0
  177. package/drizzle/0331_managed_organization_creation.sql +163 -0
  178. package/drizzle/0332_organization_shared_workspace_control_plane.sql +539 -0
  179. package/drizzle/0333_session_turn_prompt_routing.sql +45 -0
  180. package/drizzle/0334_connected_machine_workspace_root.sql +99 -0
  181. package/drizzle/0335_slack_workspace_routing.sql +460 -0
  182. package/drizzle/0336_atomic_session_fork_visibility.sql +667 -0
  183. package/drizzle/0337_slack_routed_action_handles.sql +189 -0
  184. package/drizzle/0338_atomic_connected_machine_attachments.sql +410 -0
  185. package/drizzle/0339_document_authority_reclassification.sql +1153 -0
  186. package/drizzle/0340_tenancy_backfill_activation_evidence.sql +1347 -0
  187. package/drizzle/0341_slack_routing_probe_organization_fence.sql +109 -0
  188. package/package.json +10 -6
  189. package/src/attached-browser-devices.ts +233 -5
  190. package/src/automations.ts +911 -0
  191. package/src/browser-sessions.ts +10 -0
  192. package/src/capability-integrations.ts +175 -1
  193. package/src/child-lifecycle-notices.ts +241 -0
  194. package/src/codemode-operations.ts +1 -35
  195. package/src/company-brain-context-selection-schema.ts +100 -0
  196. package/src/company-brain-context-selection.ts +359 -0
  197. package/src/company-brain-governed-writes.ts +1170 -0
  198. package/src/company-brain.ts +309 -0
  199. package/src/company-profile-agent-admin.ts +225 -0
  200. package/src/company-profile-schema.ts +121 -1
  201. package/src/company-profile.ts +21 -0
  202. package/src/computer-sessions.ts +10 -1
  203. package/src/connection-authority.ts +290 -0
  204. package/src/connection-scopes.ts +20 -0
  205. package/src/connection-tenancy-backfill.ts +81 -0
  206. package/src/connection-token-resolver.ts +263 -31
  207. package/src/database.ts +75 -2
  208. package/src/editable-artifacts-schema.ts +10 -5
  209. package/src/editable-artifacts.ts +21 -11
  210. package/src/governed-learning-activation-schema.ts +121 -0
  211. package/src/governed-learning-activation.ts +588 -0
  212. package/src/governed-learning-evaluator-schema.ts +72 -0
  213. package/src/governed-learning-evaluator.ts +217 -0
  214. package/src/index.ts +11651 -1548
  215. package/src/insights.ts +107 -0
  216. package/src/integration-bindings.ts +88 -6
  217. package/src/integration-facets.ts +287 -84
  218. package/src/interaction-placement-loss.ts +234 -0
  219. package/src/managed-human-provisioning.ts +116 -0
  220. package/src/memory-domain.ts +27 -32
  221. package/src/migrate.ts +77 -1
  222. package/src/new-session-drafts.ts +229 -35
  223. package/src/organization-membership-backfill.ts +735 -0
  224. package/src/organization-membership-lifecycle.ts +985 -0
  225. package/src/pr-review.ts +890 -0
  226. package/src/preference-registry.ts +31 -1
  227. package/src/provision-roles.ts +877 -6
  228. package/src/runtime-posture-cli.ts +2 -0
  229. package/src/runtime-posture.ts +1291 -76
  230. package/src/schema.ts +3303 -428
  231. package/src/scoped-knowledge-schema.ts +16 -8
  232. package/src/scoped-knowledge.ts +1 -1
  233. package/src/session-background-commands.ts +759 -0
  234. package/src/session-control.ts +640 -74
  235. package/src/session-goal-pacing.ts +126 -0
  236. package/src/session-queue-commands.ts +437 -84
  237. package/src/session-realtime-context.ts +3 -3
  238. package/src/session-realtime-ledger.ts +22 -11
  239. package/src/session-realtime-mirror.ts +1 -1
  240. package/src/session-tenancy.ts +403 -49
  241. package/src/session-tool-call-settlement.ts +38 -29
  242. package/src/slack-routing-personal-workspace.ts +67 -0
  243. package/src/slack-routing.ts +469 -0
  244. package/src/slack-task-policy-schema.ts +5 -0
  245. package/src/slack-task-policy.ts +18 -0
  246. package/src/slack-user-link-access.ts +17 -1
  247. package/src/task-notes-schema.ts +74 -0
  248. package/src/task-notes.ts +124 -2
  249. package/src/turn-initiator.ts +8 -0
  250. package/src/user-resource-authority.ts +377 -0
  251. package/src/workspace-authority.ts +160 -0
  252. package/src/workspace-instruction-policies-schema.ts +53 -1
  253. package/src/workspace-instruction-policies.ts +528 -77
  254. package/src/workspace-learning-policy.ts +21 -5
  255. package/src/workspace-membership-permissions.ts +9 -0
  256. package/dist/chunk-4SR4HKKA.js.map +0 -1
  257. package/dist/chunk-6ZYKCRFO.js.map +0 -1
  258. package/dist/chunk-NXAFLCQX.js +0 -2468
  259. package/dist/chunk-NXAFLCQX.js.map +0 -1
  260. package/dist/chunk-SK7YYHBI.js.map +0 -1
  261. package/dist/chunk-WD5ROSLL.js +0 -117
  262. package/dist/chunk-WD5ROSLL.js.map +0 -1
  263. package/dist/chunk-ZWREYEWA.js.map +0 -1
  264. /package/dist/{chunk-LTG3S6ZY.js.map → chunk-EULAN2NP.js.map} +0 -0
  265. /package/dist/{chunk-CUPM3FGB.js.map → chunk-NJVZPWXY.js.map} +0 -0
@@ -0,0 +1,45 @@
1
+ -- deployment-mode: rolling
2
+ -- A physically queued turn is not necessarily part of the operator-visible
3
+ -- prompt queue: an idle Send and a Steer are accepted immediately, then wait
4
+ -- only for the worker claim boundary. Persist that admission decision so UI
5
+ -- projections never infer product semantics from worker implementation state.
6
+
7
+ SET LOCAL lock_timeout = '5s';
8
+ SET LOCAL statement_timeout = '5min';
9
+
10
+ ALTER TABLE session_turns
11
+ ADD COLUMN prompt_routing text;
12
+
13
+ ALTER TABLE session_turns
14
+ ADD CONSTRAINT session_turns_prompt_routing_check CHECK (
15
+ prompt_routing IS NULL OR prompt_routing IN (
16
+ 'accepted_for_execution',
17
+ 'queued_for_execution',
18
+ 'accepted_for_steering'
19
+ )
20
+ ) NOT VALID;
21
+
22
+ ALTER TABLE session_turns
23
+ VALIDATE CONSTRAINT session_turns_prompt_routing_check;
24
+
25
+ CREATE FUNCTION opengeni_private.reject_session_turn_prompt_routing_mutation()
26
+ RETURNS trigger
27
+ LANGUAGE plpgsql
28
+ AS $$
29
+ BEGIN
30
+ IF NEW.prompt_routing IS DISTINCT FROM OLD.prompt_routing THEN
31
+ RAISE EXCEPTION 'turn prompt routing is immutable after admission'
32
+ USING ERRCODE = '55000';
33
+ END IF;
34
+ RETURN NEW;
35
+ END;
36
+ $$;
37
+
38
+ REVOKE ALL ON FUNCTION opengeni_private.reject_session_turn_prompt_routing_mutation() FROM PUBLIC;
39
+
40
+ CREATE TRIGGER session_turns_prompt_routing_immutable
41
+ BEFORE UPDATE OF prompt_routing ON session_turns
42
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.reject_session_turn_prompt_routing_mutation();
43
+
44
+ COMMENT ON COLUMN session_turns.prompt_routing IS
45
+ 'Immutable admission intent for human/API prompts. Null is rolling/legacy compatibility; status remains physical execution state.';
@@ -0,0 +1,99 @@
1
+ -- deployment-mode: rolling
2
+
3
+ ALTER TABLE "enrollments"
4
+ ADD COLUMN "workspace_root" text;
5
+
6
+ ALTER TABLE "enrollments"
7
+ ADD CONSTRAINT "enrollments_workspace_root_shape"
8
+ CHECK (
9
+ "workspace_root" IS NULL
10
+ OR (
11
+ length("workspace_root") BETWEEN 1 AND 4096
12
+ )
13
+ );
14
+
15
+ -- Scoped machine reads return an explicit JSON contract rather than row_to_json,
16
+ -- so the new root must be projected here as well. This preserves organization,
17
+ -- workspace, and user-private machine authority without a second unscoped read.
18
+ CREATE OR REPLACE FUNCTION list_scoped_enrollments(
19
+ p_account_id uuid, p_workspace_id uuid, p_enrollment_id uuid DEFAULT NULL,
20
+ p_status text DEFAULT 'active'
21
+ ) RETURNS SETOF jsonb
22
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT AS $$
23
+ DECLARE actor_membership_id uuid; enrollment_row record;
24
+ BEGIN
25
+ INSERT INTO opengeni_private.scoped_compute_capabilities(
26
+ backend_pid, transaction_id, capability_kind
27
+ ) VALUES (pg_catalog.pg_backend_pid(), pg_catalog.pg_current_xact_id(), 'read')
28
+ ON CONFLICT DO NOTHING;
29
+ actor_membership_id := scoped_compute_actor_membership(p_account_id, p_workspace_id);
30
+ IF p_status NOT IN ('active', 'revoked') THEN
31
+ RAISE EXCEPTION 'invalid enrollment status' USING ERRCODE = '22023';
32
+ END IF;
33
+ FOR enrollment_row IN
34
+ SELECT enrollment.*, sandbox.id AS sandbox_id, sandbox.name AS sandbox_name
35
+ FROM enrollments enrollment
36
+ LEFT JOIN LATERAL (
37
+ SELECT value.id, value.name FROM sandboxes value
38
+ WHERE value.enrollment_id = enrollment.id AND value.account_id = enrollment.account_id
39
+ ORDER BY value.created_at LIMIT 1
40
+ ) sandbox ON true
41
+ WHERE enrollment.account_id = p_account_id AND enrollment.status = p_status
42
+ AND (p_enrollment_id IS NULL OR enrollment.id = p_enrollment_id)
43
+ AND (enrollment.authority_scope = 'organization'
44
+ OR (enrollment.authority_scope = 'workspace'
45
+ AND enrollment.workspace_id = p_workspace_id)
46
+ OR (enrollment.authority_scope = 'user'
47
+ AND enrollment.owner_organization_membership_id = actor_membership_id))
48
+ ORDER BY enrollment.created_at DESC, enrollment.id
49
+ LOOP
50
+ RETURN NEXT pg_catalog.jsonb_build_object(
51
+ 'id', enrollment_row.id, 'accountId', enrollment_row.account_id,
52
+ 'workspaceId', enrollment_row.workspace_id, 'scope', enrollment_row.authority_scope,
53
+ 'generation', enrollment_row.generation, 'pubkey', enrollment_row.pubkey,
54
+ 'exposure', enrollment_row.exposure, 'hasDisplay', enrollment_row.has_display,
55
+ 'opStream', enrollment_row.op_stream,
56
+ 'desktopUnavailableReason', enrollment_row.desktop_unavailable_reason,
57
+ 'allowScreenControl', enrollment_row.allow_screen_control,
58
+ 'operationPolicy', pg_catalog.jsonb_build_object(
59
+ 'memoryMaxBytes', enrollment_row.operation_memory_max_bytes,
60
+ 'memoryHighBytes', enrollment_row.operation_memory_high_bytes,
61
+ 'cpuMaxMillicores', enrollment_row.operation_cpu_max_millicores,
62
+ 'revision', enrollment_row.operation_policy_revision,
63
+ 'updatedAt', enrollment_row.operation_policy_updated_at
64
+ ),
65
+ 'status', enrollment_row.status,
66
+ 'credentialGeneration', enrollment_row.credential_generation,
67
+ 'connectionInstanceId', enrollment_row.connection_instance_id,
68
+ 'connectionGeneration', enrollment_row.connection_generation,
69
+ 'connectionLeaseExpiresAt', enrollment_row.connection_lease_expires_at,
70
+ 'connectionDuplicateDeniedCount', enrollment_row.connection_duplicate_denied_count,
71
+ 'connectionDuplicateDeniedAt', enrollment_row.connection_duplicate_denied_at,
72
+ 'os', enrollment_row.os, 'arch', enrollment_row.arch,
73
+ 'workspaceRoot', enrollment_row.workspace_root,
74
+ 'lastSeenAt', enrollment_row.last_seen_at,
75
+ 'wentOfflineAt', enrollment_row.went_offline_at,
76
+ 'wentOfflineReason', enrollment_row.went_offline_reason,
77
+ 'agentVersion', enrollment_row.agent_version,
78
+ 'agentBinarySha256', enrollment_row.agent_binary_sha256,
79
+ 'agentUpdateChannel', enrollment_row.agent_update_channel,
80
+ 'agentCapabilities', enrollment_row.agent_capabilities,
81
+ 'agentUpdate', NULL, 'sandboxId', enrollment_row.sandbox_id,
82
+ 'sandboxName', enrollment_row.sandbox_name,
83
+ 'createdAt', enrollment_row.created_at, 'revokedAt', enrollment_row.revoked_at,
84
+ 'updatedAt', enrollment_row.updated_at
85
+ );
86
+ END LOOP;
87
+ DELETE FROM opengeni_private.scoped_compute_capabilities
88
+ WHERE backend_pid = pg_catalog.pg_backend_pid()
89
+ AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
90
+ AND capability_kind = 'read';
91
+ RETURN;
92
+ EXCEPTION WHEN OTHERS THEN
93
+ DELETE FROM opengeni_private.scoped_compute_capabilities
94
+ WHERE backend_pid = pg_catalog.pg_backend_pid()
95
+ AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned();
96
+ RAISE;
97
+ END
98
+ $$;
99
+ REVOKE ALL ON FUNCTION list_scoped_enrollments(uuid, uuid, uuid, text) FROM PUBLIC;
@@ -0,0 +1,460 @@
1
+ -- deployment-mode: rolling
2
+ -- Per-channel and per-DM Slack workspace routing. The installation binding is
3
+ -- untouched: one team still resolves to one credential and one home workspace.
4
+ --
5
+ -- Two tenancy scopes, not one relocated workspace. HOME is the installation
6
+ -- binding's (account_id, workspace_id): it owns the `connections` row and bot
7
+ -- credential, the bot post/update/delete ledgers, `slack_interaction_inbox`,
8
+ -- `slack_bot_user_links`, `slack_app_home_refreshes`, reaction-summon settings,
9
+ -- Slack task policy, and every table created here. TARGET is the routed
10
+ -- (account_id, workspace_id): it owns the `slack_interactions` row, its action
11
+ -- handles, its progress deliveries, the grant, the session, and the session
12
+ -- events. Posting from a routed workspace on the installation's connection
13
+ -- fails hard, so the bot client is always built from HOME.
14
+ --
15
+ -- Cross-organization targeting is out of scope: `target_account_id =
16
+ -- account_id` is a table CHECK. Dropping that CHECK later is a rolling
17
+ -- migration; adding it later is not.
18
+ --
19
+ -- Rolling: every element is additive and there is no backfill. `route_state IS
20
+ -- NULL` on the inbox means "legacy / never routed" and is exactly today's
21
+ -- behaviour, so an old API image ignores these tables and columns and keeps
22
+ -- single-workspace behaviour.
23
+ SET LOCAL lock_timeout = '5s';
24
+ SET LOCAL statement_timeout = '10min';
25
+
26
+ -- One channel routes to one workspace within the installation's organization.
27
+ -- The row is the durable ask-once memory: once a human picks, or an admin sets
28
+ -- it in the web sheet, later messages in that channel never ask again.
29
+ CREATE TABLE "slack_channel_routes" (
30
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
31
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
32
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
33
+ "connection_id" uuid NOT NULL REFERENCES "connections"("id") ON DELETE CASCADE,
34
+ "slack_team_id" text NOT NULL,
35
+ "slack_channel_id" text NOT NULL,
36
+ "target_account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
37
+ "target_workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
38
+ "decided_by_subject_id" text NOT NULL,
39
+ "decided_by_slack_user_id" text NOT NULL,
40
+ "source" text NOT NULL,
41
+ "version" integer NOT NULL DEFAULT 1,
42
+ "created_at" timestamptz NOT NULL DEFAULT now(),
43
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
44
+ CONSTRAINT "slack_channel_routes_channel_uq"
45
+ UNIQUE ("connection_id", "slack_channel_id"),
46
+ CONSTRAINT "slack_channel_routes_workspace_account_fk"
47
+ FOREIGN KEY ("workspace_id", "account_id")
48
+ REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE,
49
+ CONSTRAINT "slack_channel_routes_target_workspace_account_fk"
50
+ FOREIGN KEY ("target_workspace_id", "target_account_id")
51
+ REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE,
52
+ CONSTRAINT "slack_channel_routes_same_account_check"
53
+ CHECK ("target_account_id" = "account_id"),
54
+ CONSTRAINT "slack_channel_routes_bounds_check"
55
+ CHECK (
56
+ octet_length("slack_team_id") BETWEEN 1 AND 64
57
+ AND octet_length("slack_channel_id") BETWEEN 1 AND 64
58
+ AND octet_length("decided_by_subject_id") BETWEEN 1 AND 1024
59
+ AND octet_length("decided_by_slack_user_id") BETWEEN 1 AND 64
60
+ AND "source" IN ('picker', 'admin')
61
+ AND "version" > 0
62
+ )
63
+ );
64
+
65
+ CREATE INDEX "slack_channel_routes_target_idx"
66
+ ON "slack_channel_routes" ("target_workspace_id", "slack_team_id");
67
+
68
+ -- One Slack human's direct messages route to one workspace. Absent a row, a DM
69
+ -- derives that human's own personal workspace from their active organization
70
+ -- membership pointer; the id is never accepted from a Slack payload.
71
+ CREATE TABLE "slack_user_dm_routes" (
72
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
73
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
74
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
75
+ "connection_id" uuid NOT NULL REFERENCES "connections"("id") ON DELETE CASCADE,
76
+ "slack_team_id" text NOT NULL,
77
+ "slack_user_id" text NOT NULL,
78
+ "target_account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
79
+ "target_workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
80
+ "decided_by_subject_id" text NOT NULL,
81
+ "decided_by_slack_user_id" text NOT NULL,
82
+ "source" text NOT NULL,
83
+ "version" integer NOT NULL DEFAULT 1,
84
+ "created_at" timestamptz NOT NULL DEFAULT now(),
85
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
86
+ CONSTRAINT "slack_user_dm_routes_user_uq"
87
+ UNIQUE ("connection_id", "slack_user_id"),
88
+ CONSTRAINT "slack_user_dm_routes_workspace_account_fk"
89
+ FOREIGN KEY ("workspace_id", "account_id")
90
+ REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE,
91
+ CONSTRAINT "slack_user_dm_routes_target_workspace_account_fk"
92
+ FOREIGN KEY ("target_workspace_id", "target_account_id")
93
+ REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE,
94
+ CONSTRAINT "slack_user_dm_routes_same_account_check"
95
+ CHECK ("target_account_id" = "account_id"),
96
+ CONSTRAINT "slack_user_dm_routes_bounds_check"
97
+ CHECK (
98
+ octet_length("slack_team_id") BETWEEN 1 AND 64
99
+ AND octet_length("slack_user_id") BETWEEN 1 AND 64
100
+ AND octet_length("decided_by_subject_id") BETWEEN 1 AND 1024
101
+ AND octet_length("decided_by_slack_user_id") BETWEEN 1 AND 64
102
+ AND "source" IN ('picker', 'admin')
103
+ AND "version" > 0
104
+ )
105
+ );
106
+
107
+ CREATE INDEX "slack_user_dm_routes_target_idx"
108
+ ON "slack_user_dm_routes" ("target_workspace_id", "slack_team_id");
109
+
110
+ -- The pending first-use picker. `slack_interaction_action_handles` cannot carry
111
+ -- it: that table's `session_id` is NOT NULL and composite-FK'd to an existing
112
+ -- `slack_interactions` row, while a picker exists BEFORE any session.
113
+ --
114
+ -- `inbox_id` deliberately carries no foreign key. The `awaiting_choice` inbox
115
+ -- row is settled `processed` before the human answers, so the answer arrives as
116
+ -- its own `block_action` inbox row; this column is provenance, not a live edge.
117
+ --
118
+ -- `request_text` mirrors `slack_interaction_inbox_bounds_check`'s exact 12000
119
+ -- byte bound so a prompt can always carry the originating row's text
120
+ -- losslessly, and `provider_event_id` is the ORIGINAL event so the answer can
121
+ -- re-materialize it under the inbox's own dedupe unique.
122
+ CREATE TABLE "slack_route_prompts" (
123
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
124
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
125
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
126
+ "connection_id" uuid NOT NULL REFERENCES "connections"("id") ON DELETE CASCADE,
127
+ "inbox_id" uuid NOT NULL,
128
+ "slack_team_id" text NOT NULL,
129
+ "slack_user_id" text NOT NULL,
130
+ "slack_channel_id" text NOT NULL,
131
+ "slack_message_ts" text NOT NULL,
132
+ "provider_event_id" text NOT NULL,
133
+ "trigger_kind" text NOT NULL,
134
+ "request_text" text NOT NULL,
135
+ "has_files" boolean NOT NULL DEFAULT false,
136
+ "slack_thread_ts" text,
137
+ "message_operation_id" uuid NOT NULL,
138
+ "status" text NOT NULL DEFAULT 'pending',
139
+ "answered_target_account_id" uuid,
140
+ "answered_target_workspace_id" uuid,
141
+ "answered_by_subject_id" text,
142
+ "answered_at" timestamptz,
143
+ "expires_at" timestamptz NOT NULL,
144
+ "created_at" timestamptz NOT NULL DEFAULT now(),
145
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
146
+ CONSTRAINT "slack_route_prompts_inbox_uq" UNIQUE ("connection_id", "inbox_id"),
147
+ CONSTRAINT "slack_route_prompts_event_uq" UNIQUE ("connection_id", "provider_event_id"),
148
+ CONSTRAINT "slack_route_prompts_identity_uq" UNIQUE ("account_id", "workspace_id", "id"),
149
+ CONSTRAINT "slack_route_prompts_workspace_account_fk"
150
+ FOREIGN KEY ("workspace_id", "account_id")
151
+ REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE,
152
+ CONSTRAINT "slack_route_prompts_status_check"
153
+ CHECK ("status" IN ('pending', 'answered', 'expired', 'cancelled')),
154
+ CONSTRAINT "slack_route_prompts_trigger_check"
155
+ CHECK (
156
+ "trigger_kind" IN (
157
+ 'app_mention', 'dm', 'reaction', 'slash_command',
158
+ 'message_shortcut', 'thread_reply', 'block_action'
159
+ )
160
+ ),
161
+ CONSTRAINT "slack_route_prompts_answer_check"
162
+ CHECK (
163
+ ("status" = 'answered') = ("answered_target_workspace_id" IS NOT NULL)
164
+ AND ("answered_target_account_id" IS NULL) = ("answered_target_workspace_id" IS NULL)
165
+ AND ("answered_target_account_id" IS NULL OR "answered_target_account_id" = "account_id")
166
+ AND ("answered_target_workspace_id" IS NULL) = ("answered_at" IS NULL)
167
+ ),
168
+ CONSTRAINT "slack_route_prompts_bounds_check"
169
+ CHECK (
170
+ octet_length("slack_team_id") BETWEEN 1 AND 64
171
+ AND octet_length("slack_user_id") BETWEEN 1 AND 64
172
+ AND octet_length("slack_channel_id") BETWEEN 1 AND 64
173
+ AND octet_length("slack_message_ts") BETWEEN 1 AND 64
174
+ AND octet_length("provider_event_id") BETWEEN 1 AND 256
175
+ AND octet_length("request_text") BETWEEN 1 AND 12000
176
+ AND ("slack_thread_ts" IS NULL OR octet_length("slack_thread_ts") BETWEEN 1 AND 64)
177
+ AND (
178
+ "answered_by_subject_id" IS NULL
179
+ OR octet_length("answered_by_subject_id") BETWEEN 1 AND 1024
180
+ )
181
+ )
182
+ );
183
+
184
+ CREATE INDEX "slack_route_prompts_pending_idx"
185
+ ON "slack_route_prompts" ("expires_at", "id")
186
+ WHERE "status" = 'pending';
187
+
188
+ -- One row per offered workspace. The row `id` is the Slack button `value`, so
189
+ -- it must be a UUID: the block-action normalizer already requires that shape,
190
+ -- which is why the picker uses buttons rather than a `static_select`.
191
+ --
192
+ -- These rows are a snapshot taken at prompt time and are NEVER authority. The
193
+ -- answer path re-authorizes the chosen workspace live.
194
+ CREATE TABLE "slack_route_prompt_options" (
195
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
196
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
197
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
198
+ "prompt_id" uuid NOT NULL,
199
+ "candidate_account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
200
+ "candidate_workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
201
+ "candidate_label" text NOT NULL,
202
+ "position" integer NOT NULL,
203
+ "created_at" timestamptz NOT NULL DEFAULT now(),
204
+ CONSTRAINT "slack_route_prompt_options_workspace_account_fk"
205
+ FOREIGN KEY ("workspace_id", "account_id")
206
+ REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE,
207
+ CONSTRAINT "slack_route_prompt_options_prompt_fk"
208
+ FOREIGN KEY ("account_id", "workspace_id", "prompt_id")
209
+ REFERENCES "slack_route_prompts"("account_id", "workspace_id", "id") ON DELETE CASCADE,
210
+ CONSTRAINT "slack_route_prompt_options_candidate_workspace_account_fk"
211
+ FOREIGN KEY ("candidate_workspace_id", "candidate_account_id")
212
+ REFERENCES "workspaces"("id", "account_id") ON DELETE CASCADE,
213
+ CONSTRAINT "slack_route_prompt_options_prompt_workspace_uq"
214
+ UNIQUE ("prompt_id", "candidate_workspace_id"),
215
+ CONSTRAINT "slack_route_prompt_options_prompt_position_uq"
216
+ UNIQUE ("prompt_id", "position"),
217
+ CONSTRAINT "slack_route_prompt_options_same_account_check"
218
+ CHECK ("candidate_account_id" = "account_id"),
219
+ CONSTRAINT "slack_route_prompt_options_bounds_check"
220
+ CHECK (
221
+ octet_length("candidate_label") BETWEEN 1 AND 128
222
+ AND "position" >= 0
223
+ )
224
+ );
225
+
226
+ ALTER TABLE "slack_channel_routes" ENABLE ROW LEVEL SECURITY;
227
+ ALTER TABLE "slack_channel_routes" FORCE ROW LEVEL SECURITY;
228
+ ALTER TABLE "slack_user_dm_routes" ENABLE ROW LEVEL SECURITY;
229
+ ALTER TABLE "slack_user_dm_routes" FORCE ROW LEVEL SECURITY;
230
+ ALTER TABLE "slack_route_prompts" ENABLE ROW LEVEL SECURITY;
231
+ ALTER TABLE "slack_route_prompts" FORCE ROW LEVEL SECURITY;
232
+ ALTER TABLE "slack_route_prompt_options" ENABLE ROW LEVEL SECURITY;
233
+ ALTER TABLE "slack_route_prompt_options" FORCE ROW LEVEL SECURITY;
234
+
235
+ CREATE POLICY workspace_isolation ON "slack_channel_routes"
236
+ USING (opengeni_private.workspace_rls_visible("account_id", "workspace_id"))
237
+ WITH CHECK (opengeni_private.workspace_rls_visible("account_id", "workspace_id"));
238
+ CREATE POLICY workspace_isolation ON "slack_user_dm_routes"
239
+ USING (opengeni_private.workspace_rls_visible("account_id", "workspace_id"))
240
+ WITH CHECK (opengeni_private.workspace_rls_visible("account_id", "workspace_id"));
241
+ CREATE POLICY workspace_isolation ON "slack_route_prompts"
242
+ USING (opengeni_private.workspace_rls_visible("account_id", "workspace_id"))
243
+ WITH CHECK (opengeni_private.workspace_rls_visible("account_id", "workspace_id"));
244
+ CREATE POLICY workspace_isolation ON "slack_route_prompt_options"
245
+ USING (opengeni_private.workspace_rls_visible("account_id", "workspace_id"))
246
+ WITH CHECK (opengeni_private.workspace_rls_visible("account_id", "workspace_id"));
247
+
248
+ -- The inbox keeps HOME tenancy. `account_id`/`workspace_id` stay NOT NULL on
249
+ -- purpose: `opengeni_private.workspace_rls_visible` is strict equality, so a
250
+ -- NULL `workspace_id` would make an un-routed row invisible to EVERY tenant
251
+ -- including its own, and `settleSlackInteractionInbox` would update zero rows
252
+ -- while reporting success.
253
+ --
254
+ -- `target_workspace_id` deliberately carries no foreign key. An inbox row is
255
+ -- short-lived and definer-claimed; a runtime re-check degrades to "ask again",
256
+ -- while an FK would hard-fail on a workspace deleted mid-flight.
257
+ ALTER TABLE "slack_interaction_inbox"
258
+ ADD COLUMN "route_state" text,
259
+ ADD COLUMN "target_account_id" uuid,
260
+ ADD COLUMN "target_workspace_id" uuid,
261
+ ADD COLUMN "route_prompt_id" uuid,
262
+ ADD COLUMN "route_prompt_expires_at" timestamptz;
263
+
264
+ ALTER TABLE "slack_interaction_inbox"
265
+ ADD CONSTRAINT "slack_interaction_inbox_route_check"
266
+ CHECK (
267
+ "route_state" IS NULL
268
+ OR (
269
+ "route_state" IN ('resolved', 'awaiting_choice', 'denied')
270
+ AND ("route_state" = 'resolved') = ("target_workspace_id" IS NOT NULL)
271
+ AND ("target_account_id" IS NULL) = ("target_workspace_id" IS NULL)
272
+ AND ("target_account_id" IS NULL OR "target_account_id" = "account_id")
273
+ AND ("route_state" = 'awaiting_choice') = ("route_prompt_id" IS NOT NULL)
274
+ )
275
+ ) NOT VALID;
276
+
277
+ ALTER TABLE "slack_interaction_inbox"
278
+ VALIDATE CONSTRAINT "slack_interaction_inbox_route_check";
279
+
280
+ -- Frozen at bind time, never looked up at post time: a workspace rename between
281
+ -- the original post and a reconciliation would otherwise make the byte-compare
282
+ -- in `reconcilePostMessage` raise `post_reconciliation_mismatch`.
283
+ ALTER TABLE "slack_interactions"
284
+ ADD COLUMN "routed_workspace_label" text;
285
+
286
+ ALTER TABLE "slack_interactions"
287
+ ADD CONSTRAINT "slack_interactions_routed_label_check"
288
+ CHECK (
289
+ "routed_workspace_label" IS NULL
290
+ OR octet_length("routed_workspace_label") BETWEEN 1 AND 128
291
+ ) NOT VALID;
292
+
293
+ ALTER TABLE "slack_interactions"
294
+ VALIDATE CONSTRAINT "slack_interactions_routed_label_check";
295
+
296
+ -- The ONE new SECURITY DEFINER routine: a narrow, content-free tenancy probe.
297
+ --
298
+ -- Thread continuation must cross workspaces because `slack_interactions_route_uq`
299
+ -- is `(connection_id, route_key)` and therefore connection-global, while every
300
+ -- ordinary interaction read is workspace-fenced. This returns ids only - no
301
+ -- text, no subject, no session content - and it cannot widen visibility,
302
+ -- because the caller must then re-read the full row under the returned
303
+ -- tenancy's own RLS.
304
+ --
305
+ -- SECURITY DEFINER alone is NOT enough here. `slack_interactions` carries
306
+ -- `FORCE ROW LEVEL SECURITY`, which binds the TABLE OWNER, and OpenGeni's
307
+ -- migration principal is a non-superuser without `BYPASSRLS`. The routine
308
+ -- therefore executes as a role the strict `workspace_isolation` policy still
309
+ -- applies to, and a bare definer probe returns ZERO rows for exactly the
310
+ -- cross-workspace lookup it exists for. This uses the same private
311
+ -- transaction-capability seam migration 0243 established for Google Drive
312
+ -- object ACLs: one transaction-local row in a private table opens one
313
+ -- owner-checked permissive SELECT policy for the duration of the probe.
314
+ -- `opengeni_app` holds no privilege on the capability table and cannot forge
315
+ -- the path through a custom GUC.
316
+ --
317
+ -- `resolve_slack_installation`, `slack_installation_bindings_active_team_uq`,
318
+ -- and `sync_slack_installation_binding()` are deliberately NOT touched: one team
319
+ -- still installs into exactly one home workspace and one credential. Routing is
320
+ -- a separate, additive fact.
321
+ CREATE TABLE opengeni_private.slack_routing_runtime_capabilities (
322
+ "backend_pid" integer NOT NULL,
323
+ "transaction_id" xid8 NOT NULL,
324
+ "capability_kind" text NOT NULL,
325
+ "created_at" timestamptz NOT NULL DEFAULT clock_timestamp(),
326
+ CONSTRAINT "slack_routing_runtime_capabilities_kind_chk" CHECK (
327
+ "capability_kind" IN ('interaction_tenancy')
328
+ ),
329
+ CONSTRAINT "slack_routing_runtime_capabilities_pk" PRIMARY KEY (
330
+ "backend_pid", "transaction_id", "capability_kind"
331
+ )
332
+ );
333
+ REVOKE ALL ON TABLE opengeni_private.slack_routing_runtime_capabilities FROM PUBLIC;
334
+
335
+ CREATE FUNCTION opengeni_private.slack_routing_capability_active()
336
+ RETURNS boolean
337
+ LANGUAGE sql STABLE SECURITY DEFINER
338
+ SET search_path = opengeni_private, pg_catalog
339
+ AS $$
340
+ SELECT EXISTS (
341
+ SELECT 1
342
+ FROM slack_routing_runtime_capabilities capability
343
+ WHERE capability.backend_pid = pg_catalog.pg_backend_pid()
344
+ AND capability.transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
345
+ AND capability.capability_kind = 'interaction_tenancy'
346
+ );
347
+ $$;
348
+ REVOKE ALL ON FUNCTION opengeni_private.slack_routing_capability_active() FROM PUBLIC;
349
+
350
+ -- Permissive SELECT, additive to the existing `workspace_isolation` policy and
351
+ -- gated on BOTH the effective role being the table owner AND a live capability
352
+ -- row. The pre-existing RESTRICTIVE `session_visibility_isolation` policy on
353
+ -- this table still applies, so this never widens session visibility.
354
+ DO $slack_routing_capability_policy$
355
+ DECLARE data_schema text := current_schema();
356
+ BEGIN
357
+ EXECUTE format($policy$
358
+ CREATE POLICY slack_routing_capability_read ON %1$I.slack_interactions
359
+ FOR SELECT USING (
360
+ current_user = pg_catalog.pg_get_userbyid(
361
+ (SELECT relation.relowner
362
+ FROM pg_catalog.pg_class relation
363
+ WHERE relation.oid = %2$L::pg_catalog.regclass)
364
+ )
365
+ AND opengeni_private.slack_routing_capability_active()
366
+ )
367
+ $policy$, data_schema, data_schema || '.slack_interactions');
368
+ END
369
+ $slack_routing_capability_policy$;
370
+
371
+ DO $privileged_functions$
372
+ DECLARE data_schema text := current_schema();
373
+ BEGIN
374
+ EXECUTE format($ddl$
375
+ CREATE FUNCTION opengeni_private.resolve_slack_interaction_tenancy(
376
+ p_connection_id uuid,
377
+ p_route_key text
378
+ )
379
+ RETURNS TABLE (account_id uuid, workspace_id uuid, interaction_id uuid)
380
+ LANGUAGE plpgsql
381
+ SECURITY DEFINER
382
+ SET search_path = %1$I, pg_catalog
383
+ AS $body$
384
+ BEGIN
385
+ IF p_connection_id IS NULL OR p_route_key IS NULL THEN
386
+ RETURN;
387
+ END IF;
388
+ INSERT INTO opengeni_private.slack_routing_runtime_capabilities (
389
+ backend_pid, transaction_id, capability_kind
390
+ ) VALUES (
391
+ pg_catalog.pg_backend_pid(), pg_catalog.pg_current_xact_id(), 'interaction_tenancy'
392
+ ) ON CONFLICT DO NOTHING;
393
+ BEGIN
394
+ RETURN QUERY
395
+ SELECT I.account_id, I.workspace_id, I.id
396
+ FROM slack_interactions I
397
+ WHERE I.connection_id = p_connection_id
398
+ AND I.route_key = p_route_key
399
+ LIMIT 1;
400
+ DELETE FROM opengeni_private.slack_routing_runtime_capabilities
401
+ WHERE backend_pid = pg_catalog.pg_backend_pid()
402
+ AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
403
+ AND capability_kind = 'interaction_tenancy';
404
+ RETURN;
405
+ EXCEPTION WHEN OTHERS THEN
406
+ DELETE FROM opengeni_private.slack_routing_runtime_capabilities
407
+ WHERE backend_pid = pg_catalog.pg_backend_pid()
408
+ AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
409
+ AND capability_kind = 'interaction_tenancy';
410
+ RAISE;
411
+ END;
412
+ END;
413
+ $body$
414
+ $ddl$, data_schema);
415
+ END
416
+ $privileged_functions$;
417
+
418
+ REVOKE ALL ON FUNCTION opengeni_private.resolve_slack_interaction_tenancy(uuid, text)
419
+ FROM PUBLIC;
420
+
421
+ DO $grants$
422
+ DECLARE target_schema text := current_schema();
423
+ BEGIN
424
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
425
+ EXECUTE format(
426
+ 'GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE %I.slack_channel_routes TO opengeni_app',
427
+ target_schema
428
+ );
429
+ EXECUTE format(
430
+ 'GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE %I.slack_user_dm_routes TO opengeni_app',
431
+ target_schema
432
+ );
433
+ EXECUTE format(
434
+ 'GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE %I.slack_route_prompts TO opengeni_app',
435
+ target_schema
436
+ );
437
+ EXECUTE format(
438
+ 'GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE %I.slack_route_prompt_options TO opengeni_app',
439
+ target_schema
440
+ );
441
+ GRANT EXECUTE ON FUNCTION opengeni_private.resolve_slack_interaction_tenancy(uuid, text)
442
+ TO opengeni_app;
443
+ REVOKE ALL ON TABLE opengeni_private.slack_routing_runtime_capabilities FROM opengeni_app;
444
+ GRANT EXECUTE ON FUNCTION opengeni_private.slack_routing_capability_active()
445
+ TO opengeni_app;
446
+ END IF;
447
+ END
448
+ $grants$;
449
+
450
+ COMMENT ON TABLE "slack_channel_routes" IS
451
+ 'Per-channel Slack workspace routing within one organization; home tenancy owns the row, target tenancy owns the session.';
452
+ COMMENT ON TABLE "slack_user_dm_routes" IS
453
+ 'Per-Slack-human direct-message workspace routing; absent a row a DM derives that human own personal workspace.';
454
+ COMMENT ON TABLE "slack_route_prompts" IS
455
+ 'Pending first-use Slack workspace picker plus its bounded original request; the answer arrives as its own block_action event.';
456
+ COMMENT ON TABLE "slack_route_prompt_options" IS
457
+ 'Offered picker workspaces; the row id is the Slack button value and is a prompt-time snapshot, never authority.';
458
+
459
+ RESET statement_timeout;
460
+ RESET lock_timeout;