@opengeni/db 0.36.1 → 3.1.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/dist/attached-browser-devices.d.ts +7 -0
  2. package/dist/automations.d.ts +154 -0
  3. package/dist/canonical-human-identities.js +3 -3
  4. package/dist/capability-integrations.d.ts +13 -0
  5. package/dist/child-lifecycle-notices.d.ts +68 -0
  6. package/dist/{chunk-GYO7ED2Q.js → chunk-EULAN2NP.js} +2 -2
  7. package/dist/{chunk-VTPRIXKG.js → chunk-NJVZPWXY.js} +3 -3
  8. package/dist/{chunk-HSOWSYOX.js → chunk-NMUBV7Z5.js} +19 -11
  9. package/dist/chunk-NMUBV7Z5.js.map +1 -0
  10. package/dist/chunk-OF5OBB76.js +4457 -0
  11. package/dist/chunk-OF5OBB76.js.map +1 -0
  12. package/dist/{chunk-5KTIEDX7.js → chunk-PR2WMLOQ.js} +7714 -4443
  13. package/dist/chunk-PR2WMLOQ.js.map +1 -0
  14. package/dist/{chunk-Z7VYMNPZ.js → chunk-SCPUJS53.js} +41 -4
  15. package/dist/chunk-SCPUJS53.js.map +1 -0
  16. package/dist/{chunk-SK7YYHBI.js → chunk-SIBYBA2D.js} +46 -1
  17. package/dist/chunk-SIBYBA2D.js.map +1 -0
  18. package/dist/chunk-TIKF3KTI.js +383 -0
  19. package/dist/chunk-TIKF3KTI.js.map +1 -0
  20. package/dist/chunk-ZZ5LJEFX.js +516 -0
  21. package/dist/chunk-ZZ5LJEFX.js.map +1 -0
  22. package/dist/codemode-operations.d.ts +1 -6
  23. package/dist/company-brain-context-selection-schema.d.ts +705 -0
  24. package/dist/company-brain-context-selection.d.ts +94 -0
  25. package/dist/company-brain-governed-writes.d.ts +24 -0
  26. package/dist/company-brain.d.ts +55 -0
  27. package/dist/company-profile-agent-admin.d.ts +15 -0
  28. package/dist/company-profile-schema.d.ts +619 -2
  29. package/dist/company-profile.d.ts +9 -4
  30. package/dist/connection-authority.d.ts +95 -0
  31. package/dist/connection-scopes.d.ts +8 -0
  32. package/dist/connection-tenancy-backfill.d.ts +41 -0
  33. package/dist/connection-token-resolver.d.ts +30 -1
  34. package/dist/database.d.ts +19 -0
  35. package/dist/editable-artifact-durable-export.d.ts +10 -0
  36. package/dist/editable-artifact-durable-export.js +18 -3
  37. package/dist/editable-artifact-durable-export.js.map +1 -1
  38. package/dist/editable-artifacts.d.ts +2 -2
  39. package/dist/editable-artifacts.js +3 -3
  40. package/dist/governed-learning-activation-schema.d.ts +984 -0
  41. package/dist/governed-learning-activation.d.ts +98 -0
  42. package/dist/governed-learning-evaluator-schema.d.ts +633 -0
  43. package/dist/governed-learning-evaluator.d.ts +28 -0
  44. package/dist/index.d.ts +1633 -87
  45. package/dist/index.js +22004 -8109
  46. package/dist/index.js.map +1 -1
  47. package/dist/insights.d.ts +18 -0
  48. package/dist/integration-bindings.d.ts +1 -0
  49. package/dist/integration-facets.d.ts +30 -0
  50. package/dist/interaction-placement-loss.d.ts +22 -0
  51. package/dist/knowledge-source-sync-schema.d.ts +730 -0
  52. package/dist/knowledge-source-sync.d.ts +60 -0
  53. package/dist/managed-human-provisioning.d.ts +25 -0
  54. package/dist/memory-domain.d.ts +3 -3
  55. package/dist/migrate.d.ts +11 -1
  56. package/dist/migrate.js +1 -1
  57. package/dist/new-session-drafts.d.ts +43 -5
  58. package/dist/organization-membership-backfill.d.ts +174 -0
  59. package/dist/organization-membership-lifecycle.d.ts +162 -0
  60. package/dist/pr-review.d.ts +107 -0
  61. package/dist/preference-registry-schema.d.ts +2 -0
  62. package/dist/preference-registry.d.ts +2 -0
  63. package/dist/provision-roles.js +1 -1
  64. package/dist/runtime-posture.d.ts +23 -9
  65. package/dist/schema.d.ts +24925 -13697
  66. package/dist/schema.js +103 -1
  67. package/dist/scoped-knowledge-schema.d.ts +52 -1
  68. package/dist/session-background-commands.d.ts +125 -0
  69. package/dist/session-background-commands.js +34 -0
  70. package/dist/session-background-commands.js.map +1 -0
  71. package/dist/session-control.d.ts +156 -3
  72. package/dist/session-goal-pacing.d.ts +68 -0
  73. package/dist/session-queue-commands.d.ts +42 -5
  74. package/dist/session-realtime-ledger.d.ts +3 -0
  75. package/dist/session-tenancy.d.ts +71 -1
  76. package/dist/session-tenancy.js +25 -3
  77. package/dist/session-tool-call-settlement.d.ts +9 -4
  78. package/dist/slack-routing-personal-workspace.d.ts +23 -0
  79. package/dist/slack-routing.d.ts +153 -0
  80. package/dist/slack-task-policy-schema.d.ts +34 -0
  81. package/dist/slack-task-policy.d.ts +8 -0
  82. package/dist/slack-user-link-access.d.ts +1 -0
  83. package/dist/task-notes-schema.d.ts +594 -0
  84. package/dist/task-notes.d.ts +39 -0
  85. package/dist/turn-initiator.d.ts +2 -0
  86. package/dist/user-resource-authority.d.ts +109 -0
  87. package/dist/video-generation.js +3 -3
  88. package/dist/workspace-authority.d.ts +81 -0
  89. package/dist/workspace-instruction-policies-schema.d.ts +279 -0
  90. package/dist/workspace-instruction-policies.d.ts +62 -0
  91. package/dist/workspace-learning-policy.d.ts +1 -0
  92. package/dist/workspace-membership-permissions.d.ts +2 -0
  93. package/drizzle/0240_enrollment_connection_authority.sql +39 -0
  94. package/drizzle/0240_model_context_user_messages.sql +204 -0
  95. package/drizzle/0241_atomic_personal_resource_delegation.sql +1000 -0
  96. package/drizzle/0241_enrollment_agent_runtime.sql +70 -0
  97. package/drizzle/0243_google_drive_object_acl_authority.sql +554 -0
  98. package/drizzle/0244_slack_app_home_refresh_queue.sql +120 -0
  99. package/drizzle/0245_model_context_contribution_facts.sql +88 -0
  100. package/drizzle/0246_integration_personal_instance_authority.sql +70 -0
  101. package/drizzle/0247_terraform_stacks_provenance_repair.sql +428 -0
  102. package/drizzle/0248_terraform_stacks_component_resolution_fence.sql +83 -0
  103. package/drizzle/0249_personal_resource_delegation_authority_correction.sql +662 -0
  104. package/drizzle/0250_direct_retained_process_owner_liveness.sql +199 -0
  105. package/drizzle/0251_connected_machine_operation_policy.sql +45 -0
  106. package/drizzle/0252_scheduled_personal_resource_delegation.sql +1867 -0
  107. package/drizzle/0253_common_user_resource_authority_lifecycle.sql +1555 -0
  108. package/drizzle/0254_scoped_variable_set_authority.sql +1255 -0
  109. package/drizzle/0255_company_brain_governed_write_proposals.sql +524 -0
  110. package/drizzle/0256_connection_authority_delegation.sql +703 -0
  111. package/drizzle/0257_goal_revision_decisions_and_root_constraints.sql +436 -0
  112. package/drizzle/0258_three_scope_document_knowledge_authority.sql +1273 -0
  113. package/drizzle/0259_company_brain_context_selection_receipts.sql +869 -0
  114. package/drizzle/0260_task_note_knowledge_promotion.sql +820 -0
  115. package/drizzle/0261_preference_knowledge_proposal_actor_binding.sql +471 -0
  116. package/drizzle/0262_scoped_connected_machines_and_rigs.sql +1700 -0
  117. package/drizzle/0263_organization_membership_lifecycle.sql +3284 -0
  118. package/drizzle/0264_connection_authority_runtime_activation.sql +1084 -0
  119. package/drizzle/0266_company_brain_context_receipt_inspection.sql +156 -0
  120. package/drizzle/0268_governed_learning_decision_receipts.sql +825 -0
  121. package/drizzle/0269_governed_learning_activation_controller.sql +1874 -0
  122. package/drizzle/0270_governed_learning_history_inspection.sql +170 -0
  123. package/drizzle/0271_company_brain_retrieval_only_default.sql +524 -0
  124. package/drizzle/0272_human_confirmed_learning_activation.sql +591 -0
  125. package/drizzle/0273_scheduled_variable_set_materialization.sql +268 -0
  126. package/drizzle/0274_human_confirmed_knowledge_review.sql +310 -0
  127. package/drizzle/0275_scheduled_connection_authority.sql +5527 -0
  128. package/drizzle/0276_onboarding_proposal_initiating_human_guc.sql +198 -0
  129. package/drizzle/0277_workspace_writer_authority_attribution.sql +393 -0
  130. package/drizzle/0278_workspace_membership_removal_fencing.sql +1174 -0
  131. package/drizzle/0279_workspace_connection_use_lane.sql +343 -0
  132. package/drizzle/0280_connection_and_variable_set_audit_attribution.sql +773 -0
  133. package/drizzle/0281_viewer_holder_authority_claims.sql +52 -0
  134. package/drizzle/0282_variable_set_session_attach_attribution.sql +178 -0
  135. package/drizzle/0283_editable_spreadsheet_authored_state.sql +82 -0
  136. package/drizzle/0284_truthful_human_confirmed_review_reason.sql +930 -0
  137. package/drizzle/0285_organization_tenancy_inventory.sql +329 -0
  138. package/drizzle/0286_widen_task_note_expiry_ceiling.sql +391 -0
  139. package/drizzle/0287_open_suffix_pending_tool_calls.sql +18 -0
  140. package/drizzle/0288_attached_browser_reenrollment.sql +64 -0
  141. package/drizzle/0289_session_composer_policy_authority.sql +597 -0
  142. package/drizzle/0290_organization_membership_backfill.sql +268 -0
  143. package/drizzle/0291_resource_authority_classification_assertion.sql +371 -0
  144. package/drizzle/0292_truthful_tenancy_inventory_counters.sql +291 -0
  145. package/drizzle/0293_confirm_time_rule_rebaseline.sql +615 -0
  146. package/drizzle/0294_preference_activation_authority.sql +237 -0
  147. package/drizzle/0295_retire_legacy_standing_memory_mode.sql +476 -0
  148. package/drizzle/0296_force_rls_backfill_noop_repair.sql +215 -0
  149. package/drizzle/0297_session_ownership_classification_and_backfill.sql +752 -0
  150. package/drizzle/0298_organization_tenancy_parity.sql +609 -0
  151. package/drizzle/0299_organization_membership_lock_order.sql +291 -0
  152. package/drizzle/0300_tenancy_backfill_ledger.sql +459 -0
  153. package/drizzle/0301_session_snapshot_and_pin_visibility.sql +181 -0
  154. package/drizzle/0302_personal_workspace_session_ownership.sql +303 -0
  155. package/drizzle/0303_session_tenancy_product_activation.sql +919 -0
  156. package/drizzle/0304_personal_workspace_private_session_reads.sql +97 -0
  157. package/drizzle/0305_personal_resource_grant_management.sql +584 -0
  158. package/drizzle/0306_atomic_personal_resource_attachments.sql +1374 -0
  159. package/drizzle/0307_session_attention_state.sql +51 -0
  160. package/drizzle/0308_session_archives.sql +35 -0
  161. package/drizzle/0309_channel_project_pins.sql +6 -0
  162. package/drizzle/0310_channel_project_order.sql +27 -0
  163. package/drizzle/0311_company_scope_and_private_session_create.sql +656 -0
  164. package/drizzle/0312_quiescent_session_tree_deletion.sql +39 -0
  165. package/drizzle/0313_private_child_session_authority.sql +365 -0
  166. package/drizzle/0314_unregistered_organization_invitations.sql +606 -0
  167. package/drizzle/0315_personal_github_repository_selection.sql +624 -0
  168. package/drizzle/0316_human_confirmed_activation_resumed_generation.sql +869 -0
  169. package/drizzle/0317_session_goal_continuation_hold.sql +64 -0
  170. package/drizzle/0318_session_turn_startup_milestones.sql +115 -0
  171. package/drizzle/0319_event_triggered_automations.sql +254 -0
  172. package/drizzle/0320_pr_review_pack.sql +113 -0
  173. package/drizzle/0321_slack_bot_environment_display_name.sql +15 -0
  174. package/drizzle/0322_session_turns_unclaimed_prompt_trigger_index.sql +5 -0
  175. package/drizzle/0323_organization_private_session_enablement.sql +489 -0
  176. package/drizzle/0324_human_confirmed_company_profile_agent_admin.sql +856 -0
  177. package/drizzle/0325_child_lifecycle_notices.sql +44 -0
  178. package/drizzle/0326_interaction_operation_error_codes.sql +62 -0
  179. package/drizzle/0327_slack_first_task_hint.sql +29 -0
  180. package/drizzle/0328_session_background_commands.sql +590 -0
  181. package/drizzle/0329_slack_orchestration_delivery_events.sql +91 -0
  182. package/drizzle/0330_api_key_descriptions.sql +20 -0
  183. package/drizzle/0331_managed_organization_creation.sql +163 -0
  184. package/drizzle/0332_organization_shared_workspace_control_plane.sql +539 -0
  185. package/drizzle/0333_session_turn_prompt_routing.sql +45 -0
  186. package/drizzle/0334_connected_machine_workspace_root.sql +99 -0
  187. package/drizzle/0335_slack_workspace_routing.sql +460 -0
  188. package/drizzle/0336_atomic_session_fork_visibility.sql +667 -0
  189. package/drizzle/0337_slack_routed_action_handles.sql +189 -0
  190. package/drizzle/0338_atomic_connected_machine_attachments.sql +410 -0
  191. package/drizzle/0339_document_authority_reclassification.sql +1153 -0
  192. package/drizzle/0340_tenancy_backfill_activation_evidence.sql +1347 -0
  193. package/drizzle/0341_slack_routing_probe_organization_fence.sql +109 -0
  194. package/package.json +10 -6
  195. package/src/attached-browser-devices.ts +233 -5
  196. package/src/automations.ts +911 -0
  197. package/src/browser-sessions.ts +10 -0
  198. package/src/capability-integrations.ts +175 -1
  199. package/src/child-lifecycle-notices.ts +241 -0
  200. package/src/codemode-operations.ts +1 -35
  201. package/src/company-brain-context-selection-schema.ts +100 -0
  202. package/src/company-brain-context-selection.ts +359 -0
  203. package/src/company-brain-governed-writes.ts +1170 -0
  204. package/src/company-brain.ts +309 -0
  205. package/src/company-profile-agent-admin.ts +225 -0
  206. package/src/company-profile-schema.ts +121 -1
  207. package/src/company-profile.ts +21 -0
  208. package/src/computer-sessions.ts +10 -1
  209. package/src/connection-authority.ts +290 -0
  210. package/src/connection-scopes.ts +20 -0
  211. package/src/connection-tenancy-backfill.ts +81 -0
  212. package/src/connection-token-resolver.ts +263 -31
  213. package/src/database.ts +75 -2
  214. package/src/editable-artifact-durable-export.ts +27 -0
  215. package/src/editable-artifacts-schema.ts +10 -5
  216. package/src/editable-artifacts.ts +21 -11
  217. package/src/governed-learning-activation-schema.ts +121 -0
  218. package/src/governed-learning-activation.ts +588 -0
  219. package/src/governed-learning-evaluator-schema.ts +72 -0
  220. package/src/governed-learning-evaluator.ts +217 -0
  221. package/src/index.ts +12001 -1699
  222. package/src/insights.ts +107 -0
  223. package/src/integration-bindings.ts +88 -6
  224. package/src/integration-facets.ts +287 -84
  225. package/src/interaction-placement-loss.ts +234 -0
  226. package/src/knowledge-source-sync-schema.ts +83 -0
  227. package/src/knowledge-source-sync.ts +530 -0
  228. package/src/managed-human-provisioning.ts +116 -0
  229. package/src/memory-domain.ts +27 -32
  230. package/src/migrate.ts +77 -1
  231. package/src/new-session-drafts.ts +229 -35
  232. package/src/organization-membership-backfill.ts +735 -0
  233. package/src/organization-membership-lifecycle.ts +985 -0
  234. package/src/pr-review.ts +890 -0
  235. package/src/preference-registry.ts +31 -1
  236. package/src/provision-roles.ts +931 -24
  237. package/src/runtime-posture-cli.ts +2 -0
  238. package/src/runtime-posture.ts +1315 -52
  239. package/src/schema.ts +3336 -438
  240. package/src/scoped-knowledge-schema.ts +16 -8
  241. package/src/scoped-knowledge.ts +1 -1
  242. package/src/session-background-commands.ts +759 -0
  243. package/src/session-control.ts +640 -74
  244. package/src/session-goal-pacing.ts +126 -0
  245. package/src/session-queue-commands.ts +450 -98
  246. package/src/session-realtime-context.ts +19 -3
  247. package/src/session-realtime-ledger.ts +75 -12
  248. package/src/session-realtime-mirror.ts +1 -1
  249. package/src/session-tenancy.ts +403 -49
  250. package/src/session-tool-call-settlement.ts +38 -29
  251. package/src/slack-routing-personal-workspace.ts +67 -0
  252. package/src/slack-routing.ts +469 -0
  253. package/src/slack-task-policy-schema.ts +5 -0
  254. package/src/slack-task-policy.ts +18 -0
  255. package/src/slack-user-link-access.ts +17 -1
  256. package/src/task-notes-schema.ts +74 -0
  257. package/src/task-notes.ts +124 -2
  258. package/src/turn-initiator.ts +8 -0
  259. package/src/user-resource-authority.ts +377 -0
  260. package/src/workspace-authority.ts +160 -0
  261. package/src/workspace-instruction-policies-schema.ts +53 -1
  262. package/src/workspace-instruction-policies.ts +528 -77
  263. package/src/workspace-learning-policy.ts +21 -5
  264. package/src/workspace-membership-permissions.ts +9 -0
  265. package/dist/chunk-5KTIEDX7.js.map +0 -1
  266. package/dist/chunk-FPVAD4CO.js +0 -117
  267. package/dist/chunk-FPVAD4CO.js.map +0 -1
  268. package/dist/chunk-HSOWSYOX.js.map +0 -1
  269. package/dist/chunk-SK7YYHBI.js.map +0 -1
  270. package/dist/chunk-VKLB2ZGI.js +0 -2389
  271. package/dist/chunk-VKLB2ZGI.js.map +0 -1
  272. package/dist/chunk-Z7VYMNPZ.js.map +0 -1
  273. /package/dist/{chunk-GYO7ED2Q.js.map → chunk-EULAN2NP.js.map} +0 -0
  274. /package/dist/{chunk-VTPRIXKG.js.map → chunk-NJVZPWXY.js.map} +0 -0
@@ -0,0 +1,752 @@
1
+ -- deployment-mode: rolling
2
+ -- Migration 0297: session ownership classification plus the one deterministic
3
+ -- backfill (organization-tenancy phase D, slice 6).
4
+ --
5
+ -- THE PREMISE THIS SLICE WAS SCOPED ON WAS WRONG
6
+ -- ----------------------------------------------
7
+ -- Slice 6 was planned as "sessions are 100% owner-NULL; nothing writes session
8
+ -- ownership". That is false. Migration 0225's `guard_session_authority_write`
9
+ -- trigger DERIVES session ownership on every INSERT, and 0225's
10
+ -- `session_visibility_isolation` policies are live on `sessions` and on every
11
+ -- table that references it. A session created today through the ordinary
12
+ -- `createSession` path by a managed human who holds a `workspace_memberships`
13
+ -- row comes out with `owner_organization_membership_id` and `owner_subject_id`
14
+ -- populated. There is therefore no undifferentiated ownerless backlog to drain
15
+ -- - there is a live derivation rule, and a precisely shaped set of rows it
16
+ -- cannot reach.
17
+ --
18
+ -- THE ACTUAL POPULATION (measured against the live trigger, not assumed)
19
+ -- ---------------------------------------------------------------------
20
+ -- 0225's derivation has exactly two branches, both INSERT-only and both taken
21
+ -- only when BOTH owner columns arrive NULL:
22
+ --
23
+ -- 1. `parent_session_id IS NOT NULL` - inherit the parent's owner pair
24
+ -- verbatim from the same (account, workspace). The parent branch WINS
25
+ -- even when it resolves NULL, so a child of an ownerless parent is
26
+ -- ownerless no matter who created it.
27
+ -- 2. otherwise, `created_by_kind = 'subject'` - the active
28
+ -- `organization_memberships` row for `created_by_subject_id`, but ONLY
29
+ -- when that subject also holds a `workspace_memberships` row in the
30
+ -- session's workspace.
31
+ --
32
+ -- Everything else silently keeps a NULL owner pair. Enumerated:
33
+ --
34
+ -- * `created_by_kind = 'service'` - the scheduler, site-auth maintenance,
35
+ -- delegated embedding-host service initiators, and the pre-0096
36
+ -- `unattributed-legacy` default. No human is named at all.
37
+ -- * `created_by_kind = 'subject'` with a non-`user:` subject - `api_key:*`
38
+ -- and `configured:*`. Both 0219 (`p_subject_id NOT LIKE 'user:%'` rejects
39
+ -- provisioning) and 0263 (invitations, role backfill, self-membership
40
+ -- listing) are gated to `user:%`, so these subjects can NEVER acquire an
41
+ -- organization anchor. These rows are permanently unrepairable, and API
42
+ -- keys keep minting new ones, so this is a GROWING population and not a
43
+ -- drainable backlog.
44
+ -- * a `user:` subject with no active organization membership.
45
+ -- * A MANAGED HUMAN IN THEIR OWN PERSONAL WORKSPACE. Slice B provisions the
46
+ -- personal workspace as a pointer on the membership row WITHOUT a durable
47
+ -- `workspace_memberships` row (docs/organization-tenancy.md). Branch 2's
48
+ -- `EXISTS (workspace_memberships ...)` guard therefore fails, and the
49
+ -- human's own personal-workspace sessions come out ownerless. This is the
50
+ -- one population where the organization's own durable lifecycle data
51
+ -- already names exactly one subject.
52
+ --
53
+ -- WHAT IS REPAIRABLE, AND WHAT IS NOT
54
+ -- -----------------------------------
55
+ -- REPAIRABLE (this migration's backfill, and nothing else):
56
+ --
57
+ -- A. Personal-workspace convergence. `sessions.workspace_id` is exactly the
58
+ -- `personal_workspace_id` of one ACTIVE `organization_memberships` row -
59
+ -- a 1:1 lifecycle anchor enforced by 0218's unique
60
+ -- `organization_memberships_personal_workspace_idx` on
61
+ -- (account_id, personal_workspace_id) - AND the session's
62
+ -- `created_by_subject_id` is that same membership's `subject_id`. Two
63
+ -- independent durable facts converge on one subject, and the workspace
64
+ -- anchor alone already identifies the owner; `created_by` is used only as
65
+ -- a veto, never as the evidence. This is the same convergence the
66
+ -- activated 0264 runtime exception uses to derive an owner-only
67
+ -- personal-workspace grant. When the two facts DISAGREE the row is
68
+ -- recorded `ambiguous_candidate_authority` and left alone.
69
+ -- B. Parent inheritance closure. An ownerless session whose same-workspace
70
+ -- parent now HAS an owner pair. This is branch 1 of the live trigger
71
+ -- replayed against durable parent data - not a new rule - and it is what
72
+ -- makes (A) leave the tree in exactly the state 0225 would have produced
73
+ -- had the root been attributable in the first place. It is also what
74
+ -- makes the driver resumable across a crash between the root write and
75
+ -- the closure.
76
+ --
77
+ -- NOT REPAIRABLE, deliberately:
78
+ --
79
+ -- * A managed human's session in an ORDINARY SHARED workspace that predates
80
+ -- 0225 (or that was created while the human lacked a workspace-membership
81
+ -- row). Branch 2 could be replayed, but its `workspace_memberships`
82
+ -- predicate is evaluated against TODAY's grants, and phase D says in as
83
+ -- many words: never infer user authority from `created_by`, connection
84
+ -- attribution, a default workspace, a resource name, or CURRENT ACCESS. A
85
+ -- workspace membership granted after the session was created, or revoked
86
+ -- and re-granted since, would silently manufacture authority. Recorded
87
+ -- `no_deterministic_evidence`.
88
+ -- * Every `service`-created session (`legacy_shape_unrecognized`) and every
89
+ -- non-`user:` subject session (`external_lane_owns_row`). No amount of
90
+ -- later lifecycle work can attribute these; they are permanently
91
+ -- ownerless by construction.
92
+ --
93
+ -- WHY A SECURITY DEFINER SEAM AND NOT A MIGRATION-TIME `UPDATE`
94
+ -- -------------------------------------------------------------
95
+ -- `sessions` is FORCE ROW LEVEL SECURITY and its ordinary policy is
96
+ -- workspace-scoped, which is false while the `opengeni.workspace_id` GUC is
97
+ -- unset - as it is during migration. FORCE RLS applies to the table owner, and
98
+ -- OpenGeni's documented deployment posture is a NON-superuser migration
99
+ -- principal without BYPASSRLS. A bare `UPDATE sessions ... WHERE ...` in a
100
+ -- migration body therefore matches ZERO rows and reports success on such a
101
+ -- deployment; it only appears to work in a harness that migrates as superuser.
102
+ -- Both routines below consequently run behind a transaction-scoped
103
+ -- capability-claiming policy, exactly like 0254, 0262, 0285, and the sibling
104
+ -- classification seam.
105
+ --
106
+ -- WRITE FENCES THIS SEAM HONOURS
107
+ -- ------------------------------
108
+ -- * 0225 block C: any UPDATE touching the owner pair needs a matching
109
+ -- `session_visibility_write_capabilities` row plus the
110
+ -- `opengeni.session_visibility_write_capability` GUC. The backfill mints
111
+ -- one for the duration of its own write and deletes it afterwards, on both
112
+ -- the success and the exception path.
113
+ -- * 0225 block B: the owner pair must move together, and the backfill never
114
+ -- touches `visibility` or `authority_epoch`. Every candidate row is
115
+ -- necessarily `workspace_shared` (0218's `sessions_private_owner_check`
116
+ -- forbids `user_private` without an owner), and
117
+ -- `session_visibility_isolation` short-circuits on `workspace_shared`, so
118
+ -- attributing an owner changes NO read for anybody. It is classification,
119
+ -- not a grant.
120
+ -- * 0214's commit gate fires only `BEFORE INSERT OR UPDATE OF updated_at,
121
+ -- activity_revision, activity_revision_pending_xid`. The backfill writes
122
+ -- only the two owner columns, so it neither needs nor forges a session
123
+ -- activity gate, and it does not bump `updated_at` - an operator
124
+ -- classification pass is not session activity.
125
+ -- * No `session_events` row is appended. The timeline is exact human/audit
126
+ -- truth for accepted payloads; the evidence for this run is the tenancy
127
+ -- backfill ledger receipt, which is what phase D actually asks for.
128
+ --
129
+ -- Receipt recording is guarded by `to_regprocedure` so this migration is
130
+ -- order-independent with respect to the ledger migration and reports
131
+ -- `ledgerAvailable` honestly rather than failing or silently skipping.
132
+
133
+ SET LOCAL lock_timeout = '5s';
134
+ SET LOCAL statement_timeout = '5min';
135
+
136
+ -- Freeze the active data schema into the SECURITY DEFINER routines below.
137
+ -- Standalone installs resolve to public; embedded installs resolve to their
138
+ -- dedicated schema. pg_temp remains explicitly last.
139
+ SELECT pg_catalog.set_config(
140
+ 'search_path',
141
+ pg_catalog.format('%I, pg_catalog, pg_temp', pg_catalog.current_schema()),
142
+ true
143
+ );
144
+
145
+ -- A transaction-scoped, migration-owner-only capability. Deliberately NOT a
146
+ -- reuse of 0285's inventory capability or the sibling classification one: each
147
+ -- of those policies pins the literal `current_user` observed while its own
148
+ -- migration ran, and neither grants the UPDATE this seam needs on `sessions`.
149
+ CREATE TABLE opengeni_private.session_ownership_capabilities (
150
+ "backend_pid" integer NOT NULL,
151
+ "transaction_id" xid8 NOT NULL,
152
+ CONSTRAINT "session_ownership_capabilities_pk" PRIMARY KEY (
153
+ "backend_pid", "transaction_id"
154
+ )
155
+ );
156
+ REVOKE ALL ON TABLE opengeni_private.session_ownership_capabilities FROM PUBLIC;
157
+
158
+ CREATE OR REPLACE FUNCTION opengeni_private.session_ownership_capability_active()
159
+ RETURNS boolean
160
+ LANGUAGE sql
161
+ STABLE
162
+ SECURITY DEFINER
163
+ SET search_path = pg_catalog
164
+ AS $capability_active$
165
+ SELECT EXISTS (
166
+ SELECT 1
167
+ FROM opengeni_private.session_ownership_capabilities capability
168
+ WHERE capability.backend_pid = pg_catalog.pg_backend_pid()
169
+ AND capability.transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
170
+ )
171
+ $capability_active$;
172
+ REVOKE ALL ON FUNCTION
173
+ opengeni_private.session_ownership_capability_active() FROM PUBLIC;
174
+
175
+ DO $session_ownership_capability_policies$
176
+ DECLARE
177
+ data_schema text := current_schema();
178
+ migration_owner text := current_user;
179
+ BEGIN
180
+ -- `sessions` needs FOR ALL because the backfill both reads and writes it;
181
+ -- the WITH CHECK half is the same predicate, so the capability can never
182
+ -- move a row out of the organization it was claimed for. The membership
183
+ -- table stays read-only here - this seam never mints or mutates authority.
184
+ EXECUTE format(
185
+ 'CREATE POLICY session_ownership_capability_write ON %I.sessions '
186
+ || 'FOR ALL USING (current_user = %L AND '
187
+ || 'opengeni_private.session_ownership_capability_active()) '
188
+ || 'WITH CHECK (current_user = %L AND '
189
+ || 'opengeni_private.session_ownership_capability_active())',
190
+ data_schema,
191
+ migration_owner,
192
+ migration_owner
193
+ );
194
+ EXECUTE format(
195
+ 'CREATE POLICY session_ownership_capability_read ON %I.organization_memberships '
196
+ || 'FOR SELECT USING (current_user = %L AND '
197
+ || 'opengeni_private.session_ownership_capability_active())',
198
+ data_schema,
199
+ migration_owner
200
+ );
201
+ END
202
+ $session_ownership_capability_policies$;
203
+
204
+ -- Shared preconditions for both routines. The subject check is load-bearing:
205
+ -- `session_visibility_isolation` is RESTRICTIVE and short-circuits only when
206
+ -- no `opengeni.subject_id` GUC is set. Running this seam inside an actor scope
207
+ -- would silently hide another owner's `user_private` sessions and UNDERCOUNT
208
+ -- rather than fail, so it is refused outright.
209
+ CREATE OR REPLACE FUNCTION opengeni_private.assert_session_ownership_scope(
210
+ p_organization_id uuid,
211
+ p_run_key text
212
+ ) RETURNS void
213
+ LANGUAGE plpgsql
214
+ STABLE
215
+ SET search_path FROM CURRENT
216
+ AS $$
217
+ BEGIN
218
+ IF p_organization_id IS NULL THEN
219
+ RAISE EXCEPTION 'session ownership classification requires an organization id'
220
+ USING ERRCODE = '22004';
221
+ END IF;
222
+ IF p_organization_id IS DISTINCT FROM nullif(
223
+ pg_catalog.current_setting('opengeni.account_id', true), ''
224
+ )::uuid
225
+ THEN
226
+ RAISE EXCEPTION 'session ownership classification scope mismatch'
227
+ USING ERRCODE = '42501';
228
+ END IF;
229
+ IF nullif(pg_catalog.current_setting('opengeni.subject_id', true), '') IS NOT NULL THEN
230
+ RAISE EXCEPTION 'session ownership classification must run outside an actor subject scope'
231
+ USING ERRCODE = '42501';
232
+ END IF;
233
+ IF p_run_key IS NOT NULL AND length(btrim(p_run_key)) = 0 THEN
234
+ RAISE EXCEPTION 'session ownership run key must not be blank'
235
+ USING ERRCODE = '22023';
236
+ END IF;
237
+ END;
238
+ $$;
239
+ REVOKE ALL ON FUNCTION
240
+ opengeni_private.assert_session_ownership_scope(uuid, text) FROM PUBLIC;
241
+
242
+ CREATE OR REPLACE FUNCTION opengeni_private.tenancy_backfill_ledger_available()
243
+ RETURNS boolean
244
+ LANGUAGE sql
245
+ STABLE
246
+ SET search_path FROM CURRENT
247
+ AS $$
248
+ SELECT to_regprocedure('open_tenancy_backfill_receipt(uuid, text, text)') IS NOT NULL
249
+ AND to_regprocedure('record_tenancy_backfill_unresolved(uuid, uuid, text)') IS NOT NULL
250
+ AND to_regprocedure(
251
+ 'complete_tenancy_backfill_receipt(uuid, bigint, bigint, text)'
252
+ ) IS NOT NULL
253
+ $$;
254
+ REVOKE ALL ON FUNCTION
255
+ opengeni_private.tenancy_backfill_ledger_available() FROM PUBLIC;
256
+
257
+ -- One verdict per session in one organization.
258
+ --
259
+ -- `p_run_key` is the durability switch. Omitted (NULL) the call is a pure
260
+ -- read. Supplied, the same verdicts are additionally recorded through the
261
+ -- tenancy backfill ledger as one `sessions` receipt plus one append-only
262
+ -- unresolved row per session that could not be attributed - which is also why
263
+ -- the run key must be fresh per run: the ledger refuses to re-open a settled
264
+ -- receipt.
265
+ --
266
+ -- This routine NEVER writes a session row. `rewroteSessionRows` is reported
267
+ -- literally false so a reader cannot mistake it for the backfill.
268
+ CREATE OR REPLACE FUNCTION classify_organization_session_ownership(
269
+ p_organization_id uuid,
270
+ p_run_key text DEFAULT NULL
271
+ ) RETURNS jsonb
272
+ LANGUAGE plpgsql
273
+ SECURITY DEFINER
274
+ SET search_path FROM CURRENT
275
+ SET statement_timeout = '5min'
276
+ AS $$
277
+ DECLARE
278
+ claimed_rows integer := 0;
279
+ claimed boolean := false;
280
+ ledger_available boolean;
281
+ summary jsonb;
282
+ receipt_id uuid;
283
+ -- One verdict per session row. Re-executed for the ledger statement rather
284
+ -- than staged in a temp table: a temp relation outlives the call inside the
285
+ -- caller's transaction and would collide on a second call in that same
286
+ -- transaction. $1 is the organization id.
287
+ classification_sql constant text := $classification$
288
+ SELECT session_row.id AS session_id,
289
+ CASE
290
+ -- Already attributed. The two 0225/0218 FKs prove the membership id
291
+ -- and the (account, subject) pair each EXIST, but nothing proves they
292
+ -- are the SAME row, that it is live, or that the pair is internally
293
+ -- consistent. Those are the only unenforced parts of an attributed
294
+ -- session, so they are exactly what this branch proves.
295
+ WHEN session_row.owner_organization_membership_id IS NOT NULL
296
+ OR session_row.owner_subject_id IS NOT NULL THEN
297
+ CASE
298
+ WHEN session_row.owner_organization_membership_id IS NULL
299
+ OR session_row.owner_subject_id IS NULL
300
+ OR owner_membership.id IS NULL
301
+ OR owner_membership.subject_id IS DISTINCT FROM session_row.owner_subject_id
302
+ THEN 'conflicting_authority_rows'
303
+ WHEN owner_membership.status <> 'active'
304
+ OR owner_membership.revoked_at IS NOT NULL
305
+ THEN 'missing_organization_membership'
306
+ ELSE 'owner_attributed'
307
+ END
308
+ -- Ownerless from here down. Parent inheritance first, mirroring the
309
+ -- live trigger's own branch order.
310
+ WHEN owned_parent.id IS NOT NULL THEN 'repairable_parent_inheritance'
311
+ WHEN session_row.created_by_kind <> 'subject' THEN 'legacy_shape_unrecognized'
312
+ WHEN session_row.created_by_subject_id NOT LIKE 'user:%'
313
+ THEN 'external_lane_owns_row'
314
+ WHEN personal_anchor.id IS NOT NULL
315
+ AND personal_anchor.subject_id = session_row.created_by_subject_id
316
+ THEN 'repairable_personal_workspace'
317
+ -- The workspace anchor names a DIFFERENT subject than the creator.
318
+ -- Two durable facts disagree; refusing is the only correct answer.
319
+ WHEN personal_anchor.id IS NOT NULL THEN 'ambiguous_candidate_authority'
320
+ WHEN creator_membership.id IS NULL THEN 'missing_organization_membership'
321
+ -- An active managed human in an ordinary shared workspace. Replaying
322
+ -- the trigger's second branch here would evaluate TODAY's workspace
323
+ -- grants against a historical session; phase D forbids exactly that.
324
+ ELSE 'no_deterministic_evidence'
325
+ END AS verdict
326
+ FROM sessions session_row
327
+ LEFT JOIN organization_memberships owner_membership
328
+ ON owner_membership.id = session_row.owner_organization_membership_id
329
+ AND owner_membership.account_id = session_row.account_id
330
+ LEFT JOIN sessions owned_parent
331
+ ON owned_parent.id = session_row.parent_session_id
332
+ AND owned_parent.account_id = session_row.account_id
333
+ AND owned_parent.workspace_id = session_row.workspace_id
334
+ AND owned_parent.owner_organization_membership_id IS NOT NULL
335
+ AND owned_parent.owner_subject_id IS NOT NULL
336
+ LEFT JOIN organization_memberships personal_anchor
337
+ ON personal_anchor.account_id = session_row.account_id
338
+ AND personal_anchor.personal_workspace_id = session_row.workspace_id
339
+ AND personal_anchor.status = 'active'
340
+ AND personal_anchor.revoked_at IS NULL
341
+ LEFT JOIN organization_memberships creator_membership
342
+ ON creator_membership.account_id = session_row.account_id
343
+ AND creator_membership.subject_id = session_row.created_by_subject_id
344
+ AND creator_membership.status = 'active'
345
+ AND creator_membership.revoked_at IS NULL
346
+ WHERE session_row.account_id = $1
347
+ $classification$;
348
+ -- The three terminal/repairable verdicts. Everything else is an explicit
349
+ -- refusal carrying a fixed ledger reason code and never a proposed owner.
350
+ resolved_verdicts constant text :=
351
+ '(''owner_attributed'', ''repairable_personal_workspace'',
352
+ ''repairable_parent_inheritance'')';
353
+ BEGIN
354
+ PERFORM opengeni_private.assert_session_ownership_scope(p_organization_id, p_run_key);
355
+ ledger_available := p_run_key IS NOT NULL
356
+ AND opengeni_private.tenancy_backfill_ledger_available();
357
+
358
+ INSERT INTO opengeni_private.session_ownership_capabilities (
359
+ backend_pid, transaction_id
360
+ ) VALUES (pg_catalog.pg_backend_pid(), pg_catalog.pg_current_xact_id())
361
+ ON CONFLICT DO NOTHING;
362
+ -- Only release what this call claimed, so a nested or enclosing holder of
363
+ -- the same transaction-scoped capability is never revoked underneath.
364
+ GET DIAGNOSTICS claimed_rows = ROW_COUNT;
365
+ claimed := claimed_rows > 0;
366
+
367
+ EXECUTE format($summary$
368
+ WITH classified AS (%1$s)
369
+ SELECT pg_catalog.jsonb_build_object(
370
+ 'total', count(*)::int,
371
+ 'ownerAttributed', count(*) FILTER (WHERE verdict = 'owner_attributed')::int,
372
+ 'repairablePersonalWorkspace', count(*) FILTER (
373
+ WHERE verdict = 'repairable_personal_workspace'
374
+ )::int,
375
+ 'repairableParentInheritance', count(*) FILTER (
376
+ WHERE verdict = 'repairable_parent_inheritance'
377
+ )::int,
378
+ 'unresolved', count(*) FILTER (WHERE verdict NOT IN %2$s)::int,
379
+ 'unresolvedByReason', coalesce((
380
+ SELECT pg_catalog.jsonb_object_agg(reason, reason_count)
381
+ FROM (
382
+ SELECT verdict AS reason, count(*)::int AS reason_count
383
+ FROM classified
384
+ WHERE verdict NOT IN %2$s
385
+ GROUP BY verdict
386
+ ) grouped
387
+ ), '{}'::jsonb),
388
+ 'unresolvedRows', coalesce((
389
+ SELECT pg_catalog.jsonb_agg(
390
+ pg_catalog.jsonb_build_object(
391
+ 'sessionId', bounded.session_id, 'reasonCode', bounded.verdict
392
+ ) ORDER BY bounded.session_id
393
+ )
394
+ FROM (
395
+ SELECT session_id, verdict FROM classified
396
+ WHERE verdict NOT IN %2$s
397
+ ORDER BY session_id LIMIT 1000
398
+ ) bounded
399
+ ), '[]'::jsonb),
400
+ 'unresolvedTruncated', count(*) FILTER (WHERE verdict NOT IN %2$s) > 1000
401
+ )
402
+ FROM classified
403
+ $summary$, classification_sql, resolved_verdicts)
404
+ USING p_organization_id
405
+ INTO summary;
406
+
407
+ IF ledger_available THEN
408
+ EXECUTE 'SELECT open_tenancy_backfill_receipt($1, $2, $3)'
409
+ USING p_organization_id, 'sessions', p_run_key
410
+ INTO receipt_id;
411
+ -- One set-based statement, not one round trip per unresolved session. The
412
+ -- recorded set is identical; only the plpgsql loop is gone. `receipt_id` is
413
+ -- inlined as a literal because `classification_sql` already owns `$1`.
414
+ --
415
+ -- The ledger is recorded in full on purpose: this routine summarizes an
416
+ -- entire organization and the receipt is its evidence, so truncating it
417
+ -- would make the receipt a sample. The cost is therefore proportional to
418
+ -- the unresolved population, and the classification query is evaluated
419
+ -- once for the summary and once here - the deliberate trade against
420
+ -- materializing an unbounded row set in memory. `--classify` is opt-in and
421
+ -- writes no session row.
422
+ EXECUTE format(
423
+ 'SELECT record_tenancy_backfill_unresolved(%3$L::uuid, unresolved.session_id, '
424
+ || 'unresolved.verdict) FROM ('
425
+ || 'SELECT session_id, verdict FROM (%1$s) classified '
426
+ || 'WHERE verdict NOT IN %2$s ORDER BY session_id) unresolved',
427
+ classification_sql,
428
+ resolved_verdicts,
429
+ receipt_id
430
+ ) USING p_organization_id;
431
+ -- `classified_count` is the population this run PROVED already carries a
432
+ -- live, internally consistent owner; `skipped_count` is the deterministically
433
+ -- repairable population this classification run deliberately did NOT touch.
434
+ -- Neither reflects a rewrite: this routine changes no session row.
435
+ EXECUTE 'SELECT complete_tenancy_backfill_receipt($1, $2, $3, $4)'
436
+ USING receipt_id,
437
+ (summary ->> 'ownerAttributed')::bigint,
438
+ (summary ->> 'repairablePersonalWorkspace')::bigint
439
+ + (summary ->> 'repairableParentInheritance')::bigint,
440
+ 'completed';
441
+ summary := summary || pg_catalog.jsonb_build_object('receiptId', receipt_id);
442
+ END IF;
443
+
444
+ IF claimed THEN
445
+ DELETE FROM opengeni_private.session_ownership_capabilities
446
+ WHERE backend_pid = pg_catalog.pg_backend_pid()
447
+ AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned();
448
+ END IF;
449
+
450
+ RETURN pg_catalog.jsonb_build_object(
451
+ 'schemaVersion', 1,
452
+ 'organizationId', p_organization_id,
453
+ 'runKey', p_run_key,
454
+ 'ledgerAvailable', ledger_available,
455
+ 'rewroteSessionRows', false,
456
+ 'sessions', summary
457
+ );
458
+ EXCEPTION WHEN OTHERS THEN
459
+ IF claimed THEN
460
+ DELETE FROM opengeni_private.session_ownership_capabilities
461
+ WHERE backend_pid = pg_catalog.pg_backend_pid()
462
+ AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned();
463
+ END IF;
464
+ RAISE;
465
+ END
466
+ $$;
467
+ REVOKE ALL ON FUNCTION
468
+ classify_organization_session_ownership(uuid, text) FROM PUBLIC;
469
+
470
+ -- One bounded, resumable, idempotent batch of the ONLY deterministic repair.
471
+ --
472
+ -- `p_dry_run` defaults TRUE: an operator who forgets the flag gets a report,
473
+ -- not a write. `p_limit` bounds the root candidates claimed with
474
+ -- `FOR UPDATE ... SKIP LOCKED`, so concurrent drivers never contend and a
475
+ -- killed driver leaves no half-claimed state. Re-running is a no-op once a row
476
+ -- is attributed, because an attributed row no longer matches either candidate
477
+ -- predicate.
478
+ CREATE OR REPLACE FUNCTION backfill_organization_session_ownership(
479
+ p_organization_id uuid,
480
+ p_limit integer DEFAULT 500,
481
+ p_dry_run boolean DEFAULT true,
482
+ p_run_key text DEFAULT NULL
483
+ ) RETURNS jsonb
484
+ LANGUAGE plpgsql
485
+ SECURITY DEFINER
486
+ SET search_path FROM CURRENT
487
+ SET statement_timeout = '5min'
488
+ AS $$
489
+ DECLARE
490
+ claimed_rows integer := 0;
491
+ claimed boolean := false;
492
+ ledger_available boolean;
493
+ visibility_capability_id uuid := pg_catalog.gen_random_uuid();
494
+ previous_visibility_capability text;
495
+ personal_candidates integer := 0;
496
+ inherited_candidates integer := 0;
497
+ personal_attributed integer := 0;
498
+ inherited_attributed integer := 0;
499
+ inheritance_passes integer := 0;
500
+ closure_incomplete boolean := false;
501
+ pass_rows integer;
502
+ receipt_id uuid;
503
+ BEGIN
504
+ PERFORM opengeni_private.assert_session_ownership_scope(p_organization_id, p_run_key);
505
+ IF p_limit IS NULL OR p_limit < 1 OR p_limit > 5000 THEN
506
+ RAISE EXCEPTION 'session ownership backfill limit must be between 1 and 5000'
507
+ USING ERRCODE = '22023';
508
+ END IF;
509
+ IF p_dry_run IS NULL THEN
510
+ RAISE EXCEPTION 'session ownership backfill requires an explicit dry-run flag'
511
+ USING ERRCODE = '22004';
512
+ END IF;
513
+ ledger_available := p_run_key IS NOT NULL
514
+ AND opengeni_private.tenancy_backfill_ledger_available();
515
+
516
+ INSERT INTO opengeni_private.session_ownership_capabilities (
517
+ backend_pid, transaction_id
518
+ ) VALUES (pg_catalog.pg_backend_pid(), pg_catalog.pg_current_xact_id())
519
+ ON CONFLICT DO NOTHING;
520
+ GET DIAGNOSTICS claimed_rows = ROW_COUNT;
521
+ claimed := claimed_rows > 0;
522
+
523
+ IF p_dry_run THEN
524
+ -- No locking clause on a dry run: a report must not block a live writer.
525
+ SELECT count(*)::integer INTO personal_candidates
526
+ FROM sessions session_row
527
+ JOIN organization_memberships anchor
528
+ ON anchor.account_id = session_row.account_id
529
+ AND anchor.personal_workspace_id = session_row.workspace_id
530
+ AND anchor.status = 'active'
531
+ AND anchor.revoked_at IS NULL
532
+ AND anchor.subject_id = session_row.created_by_subject_id
533
+ WHERE session_row.account_id = p_organization_id
534
+ AND session_row.owner_organization_membership_id IS NULL
535
+ AND session_row.owner_subject_id IS NULL
536
+ AND session_row.created_by_kind = 'subject'
537
+ -- Same fence the classifier applies before it ever reaches
538
+ -- 'repairable_personal_workspace': a non-`user:` creator is
539
+ -- 'external_lane_owns_row', which this migration calls permanently
540
+ -- unrepairable. Without this line the write path would be more
541
+ -- permissive than the classifier that authorizes it, and a dry run
542
+ -- would promise an attribution the classifier refuses.
543
+ AND session_row.created_by_subject_id LIKE 'user:%';
544
+
545
+ -- Only the inheritance backlog that exists RIGHT NOW. It deliberately
546
+ -- does not model the closure a real run's root writes would create, so a
547
+ -- dry run never over-promises.
548
+ SELECT count(*)::integer INTO inherited_candidates
549
+ FROM sessions session_row
550
+ JOIN sessions parent_row
551
+ ON parent_row.id = session_row.parent_session_id
552
+ AND parent_row.account_id = session_row.account_id
553
+ AND parent_row.workspace_id = session_row.workspace_id
554
+ AND parent_row.owner_organization_membership_id IS NOT NULL
555
+ AND parent_row.owner_subject_id IS NOT NULL
556
+ WHERE session_row.account_id = p_organization_id
557
+ AND session_row.owner_organization_membership_id IS NULL
558
+ AND session_row.owner_subject_id IS NULL;
559
+ ELSE
560
+ -- 0225 block C fences every owner-pair UPDATE behind this capability.
561
+ previous_visibility_capability := pg_catalog.current_setting(
562
+ 'opengeni.session_visibility_write_capability', true
563
+ );
564
+ INSERT INTO session_visibility_write_capabilities (
565
+ backend_pid, transaction_id, capability_id
566
+ ) VALUES (
567
+ pg_catalog.pg_backend_pid(),
568
+ pg_catalog.pg_current_xact_id(),
569
+ visibility_capability_id
570
+ );
571
+ PERFORM pg_catalog.set_config(
572
+ 'opengeni.session_visibility_write_capability',
573
+ visibility_capability_id::text,
574
+ true
575
+ );
576
+
577
+ WITH candidates AS (
578
+ SELECT session_row.id AS session_id,
579
+ anchor.id AS owner_membership_id,
580
+ anchor.subject_id AS owner_subject_id
581
+ FROM sessions session_row
582
+ JOIN organization_memberships anchor
583
+ ON anchor.account_id = session_row.account_id
584
+ AND anchor.personal_workspace_id = session_row.workspace_id
585
+ AND anchor.status = 'active'
586
+ AND anchor.revoked_at IS NULL
587
+ AND anchor.subject_id = session_row.created_by_subject_id
588
+ WHERE session_row.account_id = p_organization_id
589
+ AND session_row.owner_organization_membership_id IS NULL
590
+ AND session_row.owner_subject_id IS NULL
591
+ AND session_row.created_by_kind = 'subject'
592
+ -- See the dry-run branch: `external_lane_owns_row` is unrepairable by
593
+ -- construction here, not merely by the absence of a matching anchor.
594
+ AND session_row.created_by_subject_id LIKE 'user:%'
595
+ ORDER BY session_row.id
596
+ LIMIT p_limit
597
+ FOR UPDATE OF session_row SKIP LOCKED
598
+ )
599
+ UPDATE sessions target
600
+ SET owner_organization_membership_id = candidates.owner_membership_id,
601
+ owner_subject_id = candidates.owner_subject_id
602
+ FROM candidates
603
+ WHERE target.id = candidates.session_id
604
+ AND target.account_id = p_organization_id;
605
+ GET DIAGNOSTICS personal_attributed = ROW_COUNT;
606
+ personal_candidates := personal_attributed;
607
+ inherited_candidates := 0;
608
+
609
+ -- Parent-inheritance closure: branch 1 of the live trigger replayed
610
+ -- against durable parent data. Bounded by tree depth and terminated by a
611
+ -- pass that changes nothing, so it is finite even on a malformed graph.
612
+ LOOP
613
+ inheritance_passes := inheritance_passes + 1;
614
+ IF inheritance_passes > 64 THEN
615
+ -- A tree deeper than 64 (or wider than p_limit at some level) is left
616
+ -- for the next batch rather than run unbounded; `moreLikely` says so.
617
+ closure_incomplete := true;
618
+ EXIT;
619
+ END IF;
620
+ WITH candidates AS (
621
+ SELECT session_row.id AS session_id,
622
+ parent_row.owner_organization_membership_id AS owner_membership_id,
623
+ parent_row.owner_subject_id AS owner_subject_id
624
+ FROM sessions session_row
625
+ JOIN sessions parent_row
626
+ ON parent_row.id = session_row.parent_session_id
627
+ AND parent_row.account_id = session_row.account_id
628
+ AND parent_row.workspace_id = session_row.workspace_id
629
+ AND parent_row.owner_organization_membership_id IS NOT NULL
630
+ AND parent_row.owner_subject_id IS NOT NULL
631
+ WHERE session_row.account_id = p_organization_id
632
+ AND session_row.owner_organization_membership_id IS NULL
633
+ AND session_row.owner_subject_id IS NULL
634
+ ORDER BY session_row.id
635
+ LIMIT p_limit
636
+ FOR UPDATE OF session_row SKIP LOCKED
637
+ )
638
+ UPDATE sessions target
639
+ SET owner_organization_membership_id = candidates.owner_membership_id,
640
+ owner_subject_id = candidates.owner_subject_id
641
+ FROM candidates
642
+ WHERE target.id = candidates.session_id
643
+ AND target.account_id = p_organization_id;
644
+ GET DIAGNOSTICS pass_rows = ROW_COUNT;
645
+ inherited_attributed := inherited_attributed + pass_rows;
646
+ IF pass_rows >= p_limit THEN
647
+ closure_incomplete := true;
648
+ END IF;
649
+ EXIT WHEN pass_rows = 0;
650
+ END LOOP;
651
+
652
+ DELETE FROM session_visibility_write_capabilities
653
+ WHERE backend_pid = pg_catalog.pg_backend_pid()
654
+ AND transaction_id = pg_catalog.pg_current_xact_id()
655
+ AND capability_id = visibility_capability_id;
656
+ PERFORM pg_catalog.set_config(
657
+ 'opengeni.session_visibility_write_capability',
658
+ coalesce(previous_visibility_capability, ''),
659
+ true
660
+ );
661
+ END IF;
662
+
663
+ IF ledger_available THEN
664
+ EXECUTE 'SELECT open_tenancy_backfill_receipt($1, $2, $3)'
665
+ USING p_organization_id, 'sessions', p_run_key
666
+ INTO receipt_id;
667
+ -- `classified_count` is what this batch actually attributed (0 on a dry
668
+ -- run); `skipped_count` is what it left for the next batch or for another
669
+ -- concurrent driver. The unresolved obligations belong to the classifier,
670
+ -- which is the routine that can name a reason for every row.
671
+ EXECUTE 'SELECT complete_tenancy_backfill_receipt($1, $2, $3, $4)'
672
+ USING receipt_id,
673
+ (personal_attributed + inherited_attributed)::bigint,
674
+ CASE WHEN p_dry_run
675
+ THEN (personal_candidates + inherited_candidates)::bigint
676
+ ELSE 0::bigint
677
+ END,
678
+ 'completed';
679
+ END IF;
680
+
681
+ IF claimed THEN
682
+ DELETE FROM opengeni_private.session_ownership_capabilities
683
+ WHERE backend_pid = pg_catalog.pg_backend_pid()
684
+ AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned();
685
+ END IF;
686
+
687
+ RETURN pg_catalog.jsonb_build_object(
688
+ 'schemaVersion', 1,
689
+ 'organizationId', p_organization_id,
690
+ 'runKey', p_run_key,
691
+ 'ledgerAvailable', ledger_available,
692
+ 'dryRun', p_dry_run,
693
+ 'limit', p_limit,
694
+ 'receiptId', receipt_id,
695
+ 'personalWorkspaceCandidates', personal_candidates,
696
+ 'parentInheritanceCandidates', inherited_candidates,
697
+ 'personalWorkspaceAttributed', personal_attributed,
698
+ 'parentInheritanceAttributed', inherited_attributed,
699
+ 'attributed', personal_attributed + inherited_attributed,
700
+ -- The driver keeps calling while this is true. It is deliberately derived
701
+ -- from THIS batch's own result rather than from a fresh count, so a row
702
+ -- another driver holds cannot spin this loop forever.
703
+ 'moreLikely',
704
+ NOT p_dry_run AND (personal_attributed >= p_limit OR closure_incomplete)
705
+ );
706
+ EXCEPTION WHEN OTHERS THEN
707
+ IF NOT p_dry_run THEN
708
+ DELETE FROM session_visibility_write_capabilities
709
+ WHERE backend_pid = pg_catalog.pg_backend_pid()
710
+ AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
711
+ AND capability_id = visibility_capability_id;
712
+ PERFORM pg_catalog.set_config(
713
+ 'opengeni.session_visibility_write_capability',
714
+ coalesce(previous_visibility_capability, ''),
715
+ true
716
+ );
717
+ END IF;
718
+ IF claimed THEN
719
+ DELETE FROM opengeni_private.session_ownership_capabilities
720
+ WHERE backend_pid = pg_catalog.pg_backend_pid()
721
+ AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned();
722
+ END IF;
723
+ RAISE;
724
+ END
725
+ $$;
726
+
727
+ REVOKE ALL ON FUNCTION
728
+ backfill_organization_session_ownership(uuid, integer, boolean, text) FROM PUBLIC;
729
+
730
+ DO $session_ownership_grants$
731
+ BEGIN
732
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
733
+ GRANT EXECUTE ON FUNCTION
734
+ classify_organization_session_ownership(uuid, text) TO opengeni_app;
735
+ GRANT EXECUTE ON FUNCTION
736
+ backfill_organization_session_ownership(uuid, integer, boolean, text) TO opengeni_app;
737
+ -- The predicate must be independently EXECUTE-granted (0254's, 0285's, and
738
+ -- the sibling classification seam's exact lesson): an ordinary opengeni_app
739
+ -- read of ANY table this capability protects evaluates every PERMISSIVE
740
+ -- policy on it, so without this grant an unrelated read of `sessions` or
741
+ -- `organization_memberships` fails closed with a permission error instead
742
+ -- of the policy branch simply evaluating false.
743
+ GRANT EXECUTE ON FUNCTION
744
+ opengeni_private.session_ownership_capability_active() TO opengeni_app;
745
+ END IF;
746
+ END
747
+ $session_ownership_grants$;
748
+
749
+ COMMENT ON FUNCTION classify_organization_session_ownership(uuid, text) IS
750
+ 'Organization-tenancy phase D session ownership classification. Read-only over sessions: it proves every attributed session points at one live, internally consistent organization membership and names a fixed reason for every session it refuses to attribute, recording those refusals through the tenancy backfill ledger. It never writes a session row and never infers user authority from created_by, a default workspace, or current workspace access.';
751
+ COMMENT ON FUNCTION backfill_organization_session_ownership(uuid, integer, boolean, text) IS
752
+ 'Organization-tenancy phase D session ownership backfill. Attributes ONLY the two deterministic populations: a session in exactly one active membership''s personal workspace whose creator subject is that same membership, and the parent-inheritance closure that migration 0225''s own trigger would have produced. Bounded, resumable (FOR UPDATE SKIP LOCKED), idempotent, dry-run by default; it never changes visibility, authority epoch, or any read.';