@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,459 @@
1
+ -- deployment-mode: rolling
2
+ -- Tenancy backfill receipt and unresolved-row ledger (organization-tenancy
3
+ -- backfill phase D).
4
+ --
5
+ -- `docs/organization-tenancy.md` phase D requires that backfill "record
6
+ -- backfill receipts and unresolved rows without widening access". This
7
+ -- migration owns only that ledger. It classifies nothing, reads no resource
8
+ -- table, and grants no authority: a receipt is content-free bookkeeping about
9
+ -- a backfill run, and an unresolved row records only that one exact resource
10
+ -- id could not be deterministically classified, plus a fixed reason code.
11
+ --
12
+ -- Two properties are load-bearing and deliberately enforced in the schema
13
+ -- rather than in application code:
14
+ --
15
+ -- 1. The ledger can never widen access. It stores resource ids and fixed
16
+ -- reason codes only - never resource content, names, subjects, owners, or
17
+ -- any proposed authority. An unresolved row is an explicit refusal to
18
+ -- infer, so it must not carry the inference it declined to make.
19
+ -- 2. `opengeni_app` holds no direct DML on either table. Both are FORCE RLS
20
+ -- and gated by the existing `opengeni.organization_tenancy_lifecycle`
21
+ -- GUC (0263's pattern) under the new `tenancy_backfill_ledger` operation
22
+ -- name, so the only write path is the SECURITY DEFINER seam below.
23
+ --
24
+ -- Unresolved rows are append-only: a row is a durable record that a human or a
25
+ -- later deterministic rule must resolve it, and silently mutating or deleting
26
+ -- one would erase the obligation. Receipts advance only forward through their
27
+ -- own lifecycle function.
28
+
29
+ SET LOCAL lock_timeout = '5s';
30
+ SET LOCAL statement_timeout = '10min';
31
+
32
+ CREATE TABLE "tenancy_backfill_receipts" (
33
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
34
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts" ("id") ON DELETE CASCADE,
35
+ "resource_family" text NOT NULL,
36
+ "run_key" text NOT NULL,
37
+ "status" text NOT NULL DEFAULT 'open',
38
+ "classified_count" bigint NOT NULL DEFAULT 0,
39
+ "skipped_count" bigint NOT NULL DEFAULT 0,
40
+ "unresolved_count" bigint NOT NULL DEFAULT 0,
41
+ "started_at" timestamptz NOT NULL DEFAULT now(),
42
+ "completed_at" timestamptz,
43
+ "created_at" timestamptz NOT NULL DEFAULT now(),
44
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
45
+ CONSTRAINT "tenancy_backfill_receipt_family_chk" CHECK (
46
+ "resource_family" IN (
47
+ 'organization_memberships',
48
+ 'personal_workspaces',
49
+ 'sessions',
50
+ 'variable_sets',
51
+ 'rigs',
52
+ 'machines',
53
+ 'connections',
54
+ 'documents',
55
+ 'codex_credentials'
56
+ )
57
+ ),
58
+ CONSTRAINT "tenancy_backfill_receipt_status_chk" CHECK (
59
+ "status" IN ('open', 'completed', 'failed')
60
+ ),
61
+ CONSTRAINT "tenancy_backfill_receipt_counts_chk" CHECK (
62
+ "classified_count" >= 0 AND "skipped_count" >= 0 AND "unresolved_count" >= 0
63
+ ),
64
+ CONSTRAINT "tenancy_backfill_receipt_run_key_chk" CHECK (
65
+ length("run_key") BETWEEN 1 AND 200
66
+ ),
67
+ -- A terminal receipt must be stamped; an open one must not be.
68
+ CONSTRAINT "tenancy_backfill_receipt_completion_chk" CHECK (
69
+ ("status" = 'open' AND "completed_at" IS NULL)
70
+ OR ("status" <> 'open' AND "completed_at" IS NOT NULL)
71
+ )
72
+ );
73
+
74
+ -- One receipt per (organization, resource family, run). This is the
75
+ -- idempotency anchor: a re-run with the same key adopts the existing receipt
76
+ -- rather than creating a second, so a retried or resumed sweep cannot
77
+ -- double-count.
78
+ CREATE UNIQUE INDEX "tenancy_backfill_receipt_run_uq"
79
+ ON "tenancy_backfill_receipts" ("account_id", "resource_family", "run_key");
80
+ CREATE INDEX "tenancy_backfill_receipt_account_time_idx"
81
+ ON "tenancy_backfill_receipts" ("account_id", "started_at", "id");
82
+
83
+ CREATE TABLE "tenancy_backfill_unresolved_rows" (
84
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
85
+ "receipt_id" uuid NOT NULL
86
+ REFERENCES "tenancy_backfill_receipts" ("id") ON DELETE CASCADE,
87
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts" ("id") ON DELETE CASCADE,
88
+ "resource_family" text NOT NULL,
89
+ "resource_id" uuid NOT NULL,
90
+ "reason_code" text NOT NULL,
91
+ "observed_at" timestamptz NOT NULL DEFAULT now(),
92
+ CONSTRAINT "tenancy_backfill_unresolved_family_chk" CHECK (
93
+ "resource_family" IN (
94
+ 'organization_memberships',
95
+ 'personal_workspaces',
96
+ 'sessions',
97
+ 'variable_sets',
98
+ 'rigs',
99
+ 'machines',
100
+ 'connections',
101
+ 'documents',
102
+ 'codex_credentials'
103
+ )
104
+ ),
105
+ -- Fixed, content-free vocabulary. Every code states why classification was
106
+ -- refused; none of them encodes a guess about what the answer might be.
107
+ CONSTRAINT "tenancy_backfill_unresolved_reason_chk" CHECK (
108
+ "reason_code" IN (
109
+ 'no_deterministic_evidence',
110
+ 'ambiguous_candidate_authority',
111
+ 'missing_organization_membership',
112
+ 'conflicting_authority_rows',
113
+ 'external_lane_owns_row',
114
+ 'legacy_shape_unrecognized'
115
+ )
116
+ )
117
+ );
118
+
119
+ -- One unresolved record per resource per receipt, so a resumed sweep that
120
+ -- re-observes the same row does not inflate the obligation count.
121
+ CREATE UNIQUE INDEX "tenancy_backfill_unresolved_row_uq"
122
+ ON "tenancy_backfill_unresolved_rows" ("receipt_id", "resource_family", "resource_id");
123
+ CREATE INDEX "tenancy_backfill_unresolved_account_family_idx"
124
+ ON "tenancy_backfill_unresolved_rows" ("account_id", "resource_family", "observed_at");
125
+
126
+ ALTER TABLE "tenancy_backfill_receipts" ENABLE ROW LEVEL SECURITY;
127
+ ALTER TABLE "tenancy_backfill_receipts" FORCE ROW LEVEL SECURITY;
128
+ ALTER TABLE "tenancy_backfill_unresolved_rows" ENABLE ROW LEVEL SECURITY;
129
+ ALTER TABLE "tenancy_backfill_unresolved_rows" FORCE ROW LEVEL SECURITY;
130
+
131
+ -- Writes are reachable only from the lifecycle seam below, which is the sole
132
+ -- setter of this GUC value. Reads additionally stay organization-scoped.
133
+ DROP POLICY IF EXISTS organization_tenancy_lifecycle ON "tenancy_backfill_receipts";
134
+ CREATE POLICY organization_tenancy_lifecycle ON "tenancy_backfill_receipts"
135
+ USING (current_setting('opengeni.organization_tenancy_lifecycle', true)
136
+ IN ('tenancy_backfill_ledger'))
137
+ WITH CHECK (current_setting('opengeni.organization_tenancy_lifecycle', true)
138
+ IN ('tenancy_backfill_ledger'));
139
+
140
+ DROP POLICY IF EXISTS organization_tenancy_lifecycle ON "tenancy_backfill_unresolved_rows";
141
+ CREATE POLICY organization_tenancy_lifecycle ON "tenancy_backfill_unresolved_rows"
142
+ USING (current_setting('opengeni.organization_tenancy_lifecycle', true)
143
+ IN ('tenancy_backfill_ledger'))
144
+ WITH CHECK (current_setting('opengeni.organization_tenancy_lifecycle', true)
145
+ IN ('tenancy_backfill_ledger'));
146
+
147
+ -- Unresolved rows are append-only evidence. Cascade cleanup from a deleted
148
+ -- organization or receipt is permitted; a direct rewrite is not.
149
+ --
150
+ -- The permitted-cascade test is deliberately NOT "is the parent row gone".
151
+ -- This function is SECURITY DEFINER, so its reads run as the table owner, and
152
+ -- under FORCE RLS with no lifecycle GUC set the owner sees zero rows in
153
+ -- `tenancy_backfill_receipts`. A parent-absence probe therefore reports
154
+ -- "parent gone" while the receipt is very much alive, and any nested delete
155
+ -- (`pg_trigger_depth() > 1`) would slip through - the guard would be weakest
156
+ -- exactly where the caller is most privileged. The test harness cannot catch
157
+ -- that, because it migrates as a superuser, for whom FORCE RLS never engages.
158
+ --
159
+ -- Instead the exemption is proven from referential provenance: PostgreSQL runs
160
+ -- FK `ON DELETE CASCADE` as an internal RI trigger, which is the only context
161
+ -- this guard may yield to. We claim the lifecycle GUC before probing so the
162
+ -- owner's own reads are policy-visible, and restore whatever the caller had.
163
+ CREATE OR REPLACE FUNCTION guard_tenancy_backfill_unresolved_row()
164
+ RETURNS trigger
165
+ LANGUAGE plpgsql
166
+ SECURITY DEFINER
167
+ SET search_path FROM CURRENT
168
+ AS $$
169
+ DECLARE
170
+ previous_lifecycle_marker text := pg_catalog.current_setting(
171
+ 'opengeni.organization_tenancy_lifecycle', true
172
+ );
173
+ parent_alive boolean;
174
+ BEGIN
175
+ IF TG_OP = 'DELETE' THEN
176
+ IF pg_trigger_depth() > 1 THEN
177
+ PERFORM pg_catalog.set_config(
178
+ 'opengeni.organization_tenancy_lifecycle', 'tenancy_backfill_ledger', true
179
+ );
180
+ SELECT EXISTS (SELECT 1 FROM managed_accounts WHERE id = OLD.account_id)
181
+ AND EXISTS (SELECT 1 FROM tenancy_backfill_receipts WHERE id = OLD.receipt_id)
182
+ INTO parent_alive;
183
+ PERFORM pg_catalog.set_config(
184
+ 'opengeni.organization_tenancy_lifecycle',
185
+ coalesce(previous_lifecycle_marker, ''), true
186
+ );
187
+ IF NOT parent_alive THEN
188
+ RETURN OLD;
189
+ END IF;
190
+ END IF;
191
+ RAISE EXCEPTION 'tenancy backfill unresolved rows are append-only'
192
+ USING ERRCODE = '42501';
193
+ END IF;
194
+ RAISE EXCEPTION 'tenancy backfill unresolved rows are immutable'
195
+ USING ERRCODE = '42501';
196
+ END;
197
+ $$;
198
+
199
+ CREATE TRIGGER guard_tenancy_backfill_unresolved_row
200
+ BEFORE UPDATE OR DELETE ON "tenancy_backfill_unresolved_rows"
201
+ FOR EACH ROW EXECUTE FUNCTION guard_tenancy_backfill_unresolved_row();
202
+
203
+ -- Opens or adopts the receipt for one exact (organization, family, run).
204
+ -- Idempotent: a resumed sweep re-adopts its own open receipt and never opens a
205
+ -- second one. Re-opening a settled receipt is refused rather than silently
206
+ -- reviving it, because its counts are already evidence.
207
+ --
208
+ -- Idempotency is CONCURRENT, not merely serial. `SELECT ... FOR UPDATE` locks
209
+ -- nothing when no row exists yet, so two sweeps opening the same run key would
210
+ -- both fall through to the INSERT and one would surface a raw `23505` instead
211
+ -- of adopting its peer's receipt. The insert therefore yields to the unique
212
+ -- index (`ON CONFLICT DO NOTHING`) and re-selects the winner's row under the
213
+ -- same lock. Under READ COMMITTED, PostgreSQL's speculative insertion has
214
+ -- already waited for the conflicting transaction, so the re-select either sees
215
+ -- the committed winner or (if that peer rolled back) the retried insert
216
+ -- succeeded and there is nothing to re-read.
217
+ CREATE OR REPLACE FUNCTION open_tenancy_backfill_receipt(
218
+ p_account_id uuid,
219
+ p_resource_family text,
220
+ p_run_key text
221
+ ) RETURNS uuid
222
+ LANGUAGE plpgsql
223
+ SECURITY DEFINER
224
+ SET search_path FROM CURRENT
225
+ AS $$
226
+ DECLARE
227
+ previous_lifecycle_marker text := pg_catalog.current_setting(
228
+ 'opengeni.organization_tenancy_lifecycle', true
229
+ );
230
+ receipt_id uuid;
231
+ receipt_status text;
232
+ BEGIN
233
+ IF p_account_id IS NULL OR p_resource_family IS NULL OR p_run_key IS NULL THEN
234
+ RAISE EXCEPTION 'tenancy backfill receipt requires organization, family, and run key'
235
+ USING ERRCODE = '22004';
236
+ END IF;
237
+
238
+ IF p_account_id IS DISTINCT FROM nullif(
239
+ pg_catalog.current_setting('opengeni.account_id', true), ''
240
+ )::uuid
241
+ THEN
242
+ RAISE EXCEPTION 'tenancy backfill ledger scope mismatch'
243
+ USING ERRCODE = '42501';
244
+ END IF;
245
+
246
+ PERFORM pg_catalog.set_config(
247
+ 'opengeni.organization_tenancy_lifecycle', 'tenancy_backfill_ledger', true
248
+ );
249
+
250
+ SELECT existing.id, existing.status INTO receipt_id, receipt_status
251
+ FROM tenancy_backfill_receipts existing
252
+ WHERE existing.account_id = p_account_id
253
+ AND existing.resource_family = p_resource_family
254
+ AND existing.run_key = p_run_key
255
+ FOR UPDATE;
256
+
257
+ IF receipt_id IS NULL THEN
258
+ INSERT INTO tenancy_backfill_receipts (account_id, resource_family, run_key)
259
+ VALUES (p_account_id, p_resource_family, p_run_key)
260
+ ON CONFLICT ("account_id", "resource_family", "run_key") DO NOTHING
261
+ RETURNING id, status INTO receipt_id, receipt_status;
262
+
263
+ IF receipt_id IS NULL THEN
264
+ -- A concurrent opener committed the same run key first: adopt its row.
265
+ SELECT existing.id, existing.status INTO receipt_id, receipt_status
266
+ FROM tenancy_backfill_receipts existing
267
+ WHERE existing.account_id = p_account_id
268
+ AND existing.resource_family = p_resource_family
269
+ AND existing.run_key = p_run_key
270
+ FOR UPDATE;
271
+
272
+ IF receipt_id IS NULL THEN
273
+ RAISE EXCEPTION 'tenancy backfill receipt for run key % could not be opened or adopted',
274
+ p_run_key
275
+ USING ERRCODE = '40001';
276
+ END IF;
277
+ END IF;
278
+ END IF;
279
+
280
+ IF receipt_status <> 'open' THEN
281
+ RAISE EXCEPTION 'tenancy backfill receipt % is already settled', receipt_id
282
+ USING ERRCODE = '55000';
283
+ END IF;
284
+
285
+ PERFORM pg_catalog.set_config(
286
+ 'opengeni.organization_tenancy_lifecycle',
287
+ coalesce(previous_lifecycle_marker, ''), true
288
+ );
289
+ RETURN receipt_id;
290
+ END;
291
+ $$;
292
+
293
+ -- Records that one exact resource could not be deterministically classified.
294
+ -- Deliberately takes no authority/owner/subject argument: this seam exists to
295
+ -- record a refusal to infer, so it must be impossible to smuggle a guess
296
+ -- through it. Idempotent per (receipt, family, resource).
297
+ CREATE OR REPLACE FUNCTION record_tenancy_backfill_unresolved(
298
+ p_receipt_id uuid,
299
+ p_resource_id uuid,
300
+ p_reason_code text
301
+ ) RETURNS void
302
+ LANGUAGE plpgsql
303
+ SECURITY DEFINER
304
+ SET search_path FROM CURRENT
305
+ AS $$
306
+ DECLARE
307
+ previous_lifecycle_marker text := pg_catalog.current_setting(
308
+ 'opengeni.organization_tenancy_lifecycle', true
309
+ );
310
+ receipt_row record;
311
+ inserted_count integer;
312
+ BEGIN
313
+ IF p_receipt_id IS NULL OR p_resource_id IS NULL OR p_reason_code IS NULL THEN
314
+ RAISE EXCEPTION 'tenancy backfill unresolved row requires receipt, resource, and reason'
315
+ USING ERRCODE = '22004';
316
+ END IF;
317
+
318
+ PERFORM pg_catalog.set_config(
319
+ 'opengeni.organization_tenancy_lifecycle', 'tenancy_backfill_ledger', true
320
+ );
321
+
322
+ SELECT receipt.id, receipt.account_id, receipt.resource_family, receipt.status
323
+ INTO receipt_row
324
+ FROM tenancy_backfill_receipts receipt
325
+ WHERE receipt.id = p_receipt_id
326
+ FOR UPDATE;
327
+
328
+ IF receipt_row.id IS NULL THEN
329
+ RAISE EXCEPTION 'tenancy backfill receipt % does not exist', p_receipt_id
330
+ USING ERRCODE = '23503';
331
+ END IF;
332
+ IF receipt_row.account_id IS DISTINCT FROM nullif(
333
+ pg_catalog.current_setting('opengeni.account_id', true), ''
334
+ )::uuid
335
+ THEN
336
+ RAISE EXCEPTION 'tenancy backfill ledger scope mismatch'
337
+ USING ERRCODE = '42501';
338
+ END IF;
339
+ IF receipt_row.status <> 'open' THEN
340
+ RAISE EXCEPTION 'tenancy backfill receipt % is already settled', p_receipt_id
341
+ USING ERRCODE = '55000';
342
+ END IF;
343
+
344
+ INSERT INTO tenancy_backfill_unresolved_rows (
345
+ receipt_id, account_id, resource_family, resource_id, reason_code
346
+ )
347
+ VALUES (
348
+ receipt_row.id, receipt_row.account_id, receipt_row.resource_family,
349
+ p_resource_id, p_reason_code
350
+ )
351
+ ON CONFLICT ("receipt_id", "resource_family", "resource_id") DO NOTHING;
352
+
353
+ GET DIAGNOSTICS inserted_count = ROW_COUNT;
354
+ IF inserted_count > 0 THEN
355
+ UPDATE tenancy_backfill_receipts
356
+ SET unresolved_count = unresolved_count + 1, updated_at = now()
357
+ WHERE id = receipt_row.id;
358
+ END IF;
359
+
360
+ PERFORM pg_catalog.set_config(
361
+ 'opengeni.organization_tenancy_lifecycle',
362
+ coalesce(previous_lifecycle_marker, ''), true
363
+ );
364
+ END;
365
+ $$;
366
+
367
+ -- Settles a receipt. The unresolved count is owned by the append path above
368
+ -- and is never supplied by the caller, so a caller cannot understate its own
369
+ -- outstanding obligations.
370
+ CREATE OR REPLACE FUNCTION complete_tenancy_backfill_receipt(
371
+ p_receipt_id uuid,
372
+ p_classified_count bigint,
373
+ p_skipped_count bigint,
374
+ p_status text DEFAULT 'completed'
375
+ ) RETURNS void
376
+ LANGUAGE plpgsql
377
+ SECURITY DEFINER
378
+ SET search_path FROM CURRENT
379
+ AS $$
380
+ DECLARE
381
+ previous_lifecycle_marker text := pg_catalog.current_setting(
382
+ 'opengeni.organization_tenancy_lifecycle', true
383
+ );
384
+ receipt_status text;
385
+ receipt_account_id uuid;
386
+ BEGIN
387
+ IF p_receipt_id IS NULL OR p_classified_count IS NULL OR p_skipped_count IS NULL THEN
388
+ RAISE EXCEPTION 'tenancy backfill completion requires receipt and counts'
389
+ USING ERRCODE = '22004';
390
+ END IF;
391
+ IF p_status NOT IN ('completed', 'failed') THEN
392
+ RAISE EXCEPTION 'tenancy backfill completion status must be completed or failed'
393
+ USING ERRCODE = '22023';
394
+ END IF;
395
+ IF p_classified_count < 0 OR p_skipped_count < 0 THEN
396
+ RAISE EXCEPTION 'tenancy backfill counts must be non-negative'
397
+ USING ERRCODE = '22023';
398
+ END IF;
399
+
400
+ PERFORM pg_catalog.set_config(
401
+ 'opengeni.organization_tenancy_lifecycle', 'tenancy_backfill_ledger', true
402
+ );
403
+
404
+ SELECT receipt.status, receipt.account_id INTO receipt_status, receipt_account_id
405
+ FROM tenancy_backfill_receipts receipt
406
+ WHERE receipt.id = p_receipt_id
407
+ FOR UPDATE;
408
+
409
+ IF receipt_status IS NULL THEN
410
+ RAISE EXCEPTION 'tenancy backfill receipt % does not exist', p_receipt_id
411
+ USING ERRCODE = '23503';
412
+ END IF;
413
+ IF receipt_account_id IS DISTINCT FROM nullif(
414
+ pg_catalog.current_setting('opengeni.account_id', true), ''
415
+ )::uuid
416
+ THEN
417
+ RAISE EXCEPTION 'tenancy backfill ledger scope mismatch'
418
+ USING ERRCODE = '42501';
419
+ END IF;
420
+ IF receipt_status <> 'open' THEN
421
+ RAISE EXCEPTION 'tenancy backfill receipt % is already settled', p_receipt_id
422
+ USING ERRCODE = '55000';
423
+ END IF;
424
+
425
+ UPDATE tenancy_backfill_receipts
426
+ SET status = p_status,
427
+ classified_count = p_classified_count,
428
+ skipped_count = p_skipped_count,
429
+ completed_at = now(),
430
+ updated_at = now()
431
+ WHERE id = p_receipt_id;
432
+
433
+ PERFORM pg_catalog.set_config(
434
+ 'opengeni.organization_tenancy_lifecycle',
435
+ coalesce(previous_lifecycle_marker, ''), true
436
+ );
437
+ END;
438
+ $$;
439
+
440
+ REVOKE ALL ON TABLE "tenancy_backfill_receipts" FROM PUBLIC;
441
+ REVOKE ALL ON TABLE "tenancy_backfill_unresolved_rows" FROM PUBLIC;
442
+ REVOKE ALL ON FUNCTION guard_tenancy_backfill_unresolved_row() FROM PUBLIC;
443
+ REVOKE ALL ON FUNCTION open_tenancy_backfill_receipt(uuid,text,text) FROM PUBLIC;
444
+ REVOKE ALL ON FUNCTION record_tenancy_backfill_unresolved(uuid,uuid,text) FROM PUBLIC;
445
+ REVOKE ALL ON FUNCTION complete_tenancy_backfill_receipt(uuid,bigint,bigint,text) FROM PUBLIC;
446
+
447
+ DO $tenancy_backfill_ledger_grants$
448
+ BEGIN
449
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
450
+ -- No direct DML: the lifecycle seam is the only write path, and reads go
451
+ -- through it or through an operator command running as the owner.
452
+ REVOKE ALL ON TABLE "tenancy_backfill_receipts" FROM opengeni_app;
453
+ REVOKE ALL ON TABLE "tenancy_backfill_unresolved_rows" FROM opengeni_app;
454
+ GRANT EXECUTE ON FUNCTION open_tenancy_backfill_receipt(uuid,text,text) TO opengeni_app;
455
+ GRANT EXECUTE ON FUNCTION record_tenancy_backfill_unresolved(uuid,uuid,text) TO opengeni_app;
456
+ GRANT EXECUTE ON FUNCTION complete_tenancy_backfill_receipt(uuid,bigint,bigint,text)
457
+ TO opengeni_app;
458
+ END IF;
459
+ END $tenancy_backfill_ledger_grants$;
@@ -0,0 +1,181 @@
1
+ -- deployment-mode: rolling
2
+ -- Visibility-isolate the two session references that migration 0225's
3
+ -- foreign-key loop could not reach correctly.
4
+ --
5
+ -- 1. `session_list_snapshots.ordinary_session_ids` is a bare `uuid[]` with no
6
+ -- foreign key to `sessions`, so 0225's loop (which enumerates relations that
7
+ -- carry an FK to `sessions.id`) never installed `session_visibility_isolation`
8
+ -- on it. A cached list page captured while a session was `workspace_shared`
9
+ -- therefore keeps holding that session id for the snapshot's whole TTL after
10
+ -- the session transitions to `user_private` — the exact "cache stripping"
11
+ -- prerequisite `docs/organization-tenancy.md` phase F names.
12
+ --
13
+ -- An explicit visibility predicate over the array was rejected: a snapshot
14
+ -- holds up to 5,000 ids and a subject may hold up to 32 live snapshots, so a
15
+ -- RESTRICTIVE policy would have to evaluate up to 160,000 per-element
16
+ -- `session_reference_visible()` calls on the hot first-page read, it would
17
+ -- turn a partially stale cache into a hard 410 for the whole row, and (like
18
+ -- the pin defect below) it would make the row permanently undeletable. The
19
+ -- identity is instead stripped once, at the transition, where the cost is
20
+ -- bounded by the snapshots that actually name the session.
21
+ --
22
+ -- The stripped slot is REPLACED by the reserved all-zero UUID rather than
23
+ -- removed, so snapshot cardinality and every in-flight cursor offset stay
24
+ -- byte-stable. `gen_random_uuid()` never produces the nil UUID, so the slot
25
+ -- matches no session and the read path drops it exactly as it already drops
26
+ -- a session that RLS hid between pages. `array_remove` would shift every
27
+ -- later offset and silently skip one still-visible session.
28
+ --
29
+ -- 2. `session_pins` IS covered by 0225's loop, with a `FOR ALL` RESTRICTIVE
30
+ -- policy. That makes a non-owner's stale pin both invisible and permanently
31
+ -- undeletable by the member who created it: PostgreSQL applies SELECT
32
+ -- policies to a DELETE that reads any column, so a command-scoped
33
+ -- `FOR DELETE` exemption cannot work. The USING side therefore gains an
34
+ -- explicit owner escape. It discloses nothing new: the pre-existing
35
+ -- permissive `workspace_isolation` policy already limits every visible pin to
36
+ -- `subject_id = current_subject_id()`, and a pin row's only session-derived
37
+ -- field is an id that same subject supplied. The WITH CHECK side keeps the
38
+ -- strict predicate, so a member still cannot pin — or flip `pinned` on — a
39
+ -- session they cannot see, and INSERT cannot be used as a session-existence
40
+ -- oracle through the foreign key.
41
+
42
+ SET LOCAL lock_timeout = '5s';
43
+ SET LOCAL statement_timeout = '10min';
44
+
45
+ DO $session_visibility_cache_stripping$
46
+ DECLARE
47
+ data_schema text := current_schema();
48
+ capability_predicate text;
49
+ BEGIN
50
+ -- The lifecycle needs to strip OTHER subjects' cached pages, which the
51
+ -- subject-scoped `workspace_isolation` policy forbids. Reuse 0225's
52
+ -- transaction-local capability instead of a forgeable GUC marker: the
53
+ -- capability row lives in `session_visibility_write_capabilities`, which is
54
+ -- FORCE RLS, owner-only, and fully revoked from the runtime role, so the
55
+ -- runtime role cannot mint one. `pg_current_xact_id_if_assigned()` keeps this
56
+ -- predicate from assigning a transaction id on read-only traffic.
57
+ EXECUTE format($ddl$
58
+ CREATE OR REPLACE FUNCTION %1$I.session_visibility_lifecycle_capability_held()
59
+ RETURNS boolean
60
+ LANGUAGE sql
61
+ STABLE
62
+ SECURITY DEFINER
63
+ SET search_path = %1$I, pg_catalog
64
+ AS $body$
65
+ SELECT EXISTS (
66
+ SELECT 1
67
+ FROM session_visibility_write_capabilities capability
68
+ WHERE capability.backend_pid = pg_catalog.pg_backend_pid()
69
+ AND capability.transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
70
+ AND capability.capability_id = nullif(
71
+ pg_catalog.current_setting(
72
+ 'opengeni.session_visibility_write_capability', true
73
+ ),
74
+ ''
75
+ )::uuid
76
+ )
77
+ $body$;
78
+ $ddl$, data_schema);
79
+ EXECUTE format(
80
+ 'REVOKE ALL ON FUNCTION %I.session_visibility_lifecycle_capability_held() FROM PUBLIC',
81
+ data_schema
82
+ );
83
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
84
+ EXECUTE format(
85
+ 'GRANT EXECUTE ON FUNCTION %I.session_visibility_lifecycle_capability_held() TO opengeni_app',
86
+ data_schema
87
+ );
88
+ END IF;
89
+
90
+ -- The cheap GUC test short-circuits ordinary traffic before the capability
91
+ -- lookup; the GUC alone is never authority.
92
+ capability_predicate := format(
93
+ 'nullif(pg_catalog.current_setting('
94
+ || '''opengeni.session_visibility_write_capability'', true), '''') IS NOT NULL '
95
+ || 'AND %I.session_visibility_lifecycle_capability_held()',
96
+ data_schema
97
+ );
98
+ EXECUTE format(
99
+ 'DROP POLICY IF EXISTS session_visibility_cache_stripping ON %I.session_list_snapshots',
100
+ data_schema
101
+ );
102
+ EXECUTE format(
103
+ 'CREATE POLICY session_visibility_cache_stripping ON %I.session_list_snapshots '
104
+ || 'FOR ALL USING (%s) WITH CHECK (%s)',
105
+ data_schema,
106
+ capability_predicate,
107
+ capability_predicate
108
+ );
109
+
110
+ EXECUTE format($ddl$
111
+ CREATE OR REPLACE FUNCTION %1$I.strip_private_session_list_snapshots()
112
+ RETURNS trigger
113
+ LANGUAGE plpgsql
114
+ SECURITY DEFINER
115
+ SET search_path = %1$I, pg_catalog
116
+ AS $body$
117
+ BEGIN
118
+ -- Every authorized visibility transition already holds the 0225 write
119
+ -- capability (its own `sessions` UPDATE is impossible without it), so the
120
+ -- strip rides on the exact same authority in the exact same transaction.
121
+ -- The owner's own cached pages stay intact: the session remains visible to
122
+ -- them, so their snapshot is not stale.
123
+ UPDATE session_list_snapshots snapshot
124
+ SET ordinary_session_ids = coalesce((
125
+ SELECT pg_catalog.array_agg(
126
+ CASE WHEN entry.element = NEW.id
127
+ THEN '00000000-0000-0000-0000-000000000000'::uuid
128
+ ELSE entry.element
129
+ END
130
+ ORDER BY entry.ordinality
131
+ )
132
+ FROM pg_catalog.unnest(snapshot.ordinary_session_ids)
133
+ WITH ORDINALITY AS entry(element, ordinality)
134
+ ), '{}'::uuid[])
135
+ WHERE snapshot.account_id = NEW.account_id
136
+ AND snapshot.workspace_id = NEW.workspace_id
137
+ AND snapshot.subject_id IS DISTINCT FROM NEW.owner_subject_id
138
+ AND NEW.id = ANY(snapshot.ordinary_session_ids);
139
+ RETURN NULL;
140
+ END;
141
+ $body$;
142
+ $ddl$, data_schema);
143
+ EXECUTE format(
144
+ 'DROP TRIGGER IF EXISTS sessions_strip_private_list_snapshots ON %I.sessions',
145
+ data_schema
146
+ );
147
+ EXECUTE format(
148
+ 'CREATE TRIGGER sessions_strip_private_list_snapshots '
149
+ || 'AFTER UPDATE OF visibility ON %1$I.sessions '
150
+ || 'FOR EACH ROW '
151
+ || 'WHEN (NEW.visibility = ''user_private'' '
152
+ || 'AND OLD.visibility IS DISTINCT FROM NEW.visibility) '
153
+ || 'EXECUTE FUNCTION %1$I.strip_private_session_list_snapshots()',
154
+ data_schema
155
+ );
156
+
157
+ EXECUTE format(
158
+ 'DROP POLICY IF EXISTS session_visibility_isolation ON %I.session_pins',
159
+ data_schema
160
+ );
161
+ EXECUTE format(
162
+ 'CREATE POLICY session_visibility_isolation ON %1$I.session_pins AS RESTRICTIVE '
163
+ || 'FOR ALL USING ('
164
+ || '%1$I.session_reference_visible(account_id, workspace_id, session_id) '
165
+ || 'OR subject_id = opengeni_private.current_subject_id()'
166
+ || ') WITH CHECK ('
167
+ || '%1$I.session_reference_visible(account_id, workspace_id, session_id)'
168
+ || ')',
169
+ data_schema
170
+ );
171
+ END
172
+ $session_visibility_cache_stripping$;
173
+
174
+ COMMENT ON COLUMN "session_list_snapshots"."ordinary_session_ids" IS
175
+ 'Ordered slots of one short-lived cached list page. A slot holding the reserved all-zero UUID is a redacted session identity stripped by a visibility transition; it preserves cursor offsets and resolves to no session.';
176
+
177
+ COMMENT ON FUNCTION strip_private_session_list_snapshots() IS
178
+ 'Strips a session identity from every other subject''s cached list page when that session becomes user_private, under the 0225 visibility write capability.';
179
+
180
+ COMMENT ON FUNCTION session_visibility_lifecycle_capability_held() IS
181
+ 'True only inside a transaction holding an owner-minted session-visibility write capability; the runtime role cannot mint one.';