@opengeni/db 5.0.1 → 6.0.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 (212) hide show
  1. package/dist/automations.d.ts +0 -2
  2. package/dist/canonical-human-identities.d.ts +4 -0
  3. package/dist/canonical-human-identities.js +3 -3
  4. package/dist/capability-components.d.ts +5 -13
  5. package/dist/capability-integrations.d.ts +23 -8
  6. package/dist/capability-reconciliation.d.ts +5 -0
  7. package/dist/capability-reconciliation.js +21 -0
  8. package/dist/capability-reconciliation.js.map +1 -0
  9. package/dist/{chunk-6T2GGYWE.js → chunk-4UJCXBR2.js} +4 -5800
  10. package/dist/chunk-4UJCXBR2.js.map +1 -0
  11. package/dist/{chunk-MWMXWHJD.js → chunk-7JCDSMX2.js} +2 -2
  12. package/dist/chunk-AJA2ZIUW.js +219 -0
  13. package/dist/chunk-AJA2ZIUW.js.map +1 -0
  14. package/dist/chunk-CDAT6EKW.js +247 -0
  15. package/dist/chunk-CDAT6EKW.js.map +1 -0
  16. package/dist/{chunk-DPONPYJD.js → chunk-CJI6G254.js} +70 -18
  17. package/dist/chunk-CJI6G254.js.map +1 -0
  18. package/dist/chunk-G32NVMRJ.js +276 -0
  19. package/dist/chunk-G32NVMRJ.js.map +1 -0
  20. package/dist/chunk-GABXCG3N.js +5811 -0
  21. package/dist/chunk-GABXCG3N.js.map +1 -0
  22. package/dist/{chunk-ERKVSPDX.js → chunk-LVY33EWL.js} +3 -3
  23. package/dist/{chunk-45CP2YKY.js → chunk-MAMBI2HR.js} +9 -3
  24. package/dist/{chunk-45CP2YKY.js.map → chunk-MAMBI2HR.js.map} +1 -1
  25. package/dist/{chunk-F73UKJY6.js → chunk-MN5PZMEK.js} +91 -108
  26. package/dist/chunk-MN5PZMEK.js.map +1 -0
  27. package/dist/chunk-PJI4MUZ7.js +131 -0
  28. package/dist/chunk-PJI4MUZ7.js.map +1 -0
  29. package/dist/{chunk-T6BWIGWJ.js → chunk-RIYHP2WU.js} +2 -2
  30. package/dist/{chunk-7HMK3B45.js → chunk-RL2MUXM3.js} +16 -7
  31. package/dist/chunk-RL2MUXM3.js.map +1 -0
  32. package/dist/{chunk-HQDVI2DB.js → chunk-SYKHLPTK.js} +4 -3
  33. package/dist/{chunk-HQDVI2DB.js.map → chunk-SYKHLPTK.js.map} +1 -1
  34. package/dist/{chunk-T6YLHJLS.js → chunk-XIXVVBW2.js} +198 -51
  35. package/dist/chunk-XIXVVBW2.js.map +1 -0
  36. package/dist/chunk-ZPJ6V2VG.js +156 -0
  37. package/dist/chunk-ZPJ6V2VG.js.map +1 -0
  38. package/dist/codex-capacity-recovery.d.ts +12 -0
  39. package/dist/codex-failure-eligibility.d.ts +9 -0
  40. package/dist/company-profile-schema.d.ts +4 -2
  41. package/dist/connect-attempts.d.ts +15 -0
  42. package/dist/connect-attempts.js +25 -0
  43. package/dist/connect-attempts.js.map +1 -0
  44. package/dist/connection-authority.d.ts +7 -63
  45. package/dist/connection-refresh-lock.d.ts +4 -0
  46. package/dist/connection-token-resolver.d.ts +8 -43
  47. package/dist/database-timing.d.ts +10 -0
  48. package/dist/database.d.ts +4 -0
  49. package/dist/editable-artifact-durable-export.js +2 -2
  50. package/dist/editable-artifacts.d.ts +9 -0
  51. package/dist/editable-artifacts.js +7 -5
  52. package/dist/index.d.ts +447 -78
  53. package/dist/index.js +12705 -12872
  54. package/dist/index.js.map +1 -1
  55. package/dist/insights-usage-bundle.d.ts +3 -0
  56. package/dist/integration-bindings.d.ts +1 -1
  57. package/dist/integration-facets.d.ts +18 -1
  58. package/dist/knowledge-entries-schema.d.ts +2 -0
  59. package/dist/knowledge-entries.d.ts +1 -0
  60. package/dist/knowledge-indexing.d.ts +1 -0
  61. package/dist/live-session-attempt.d.ts +35 -137
  62. package/dist/managed-auth-session-sets.js +3 -3
  63. package/dist/mcp-account-bindings.d.ts +4 -0
  64. package/dist/mcp-oauth.d.ts +11 -0
  65. package/dist/mcp-operations.js +2 -2
  66. package/dist/migrate.js +1 -1
  67. package/dist/organization-integration-policy.d.ts +51 -0
  68. package/dist/organization-integration-policy.js +18 -0
  69. package/dist/organization-integration-policy.js.map +1 -0
  70. package/dist/organization-usage.d.ts +20 -0
  71. package/dist/plugin-packages.d.ts +9 -12
  72. package/dist/pr-review.d.ts +0 -11
  73. package/dist/preference-registry-schema.d.ts +17 -0
  74. package/dist/provision-roles.js +1 -1
  75. package/dist/retained-provider-commands.d.ts +124 -6
  76. package/dist/retained-provider-commands.js +29 -97
  77. package/dist/retained-provider-commands.js.map +1 -1
  78. package/dist/retired-session-create-metadata.d.ts +4 -0
  79. package/dist/runtime-posture.d.ts +7 -5
  80. package/dist/schema.d.ts +876 -656
  81. package/dist/schema.js +5 -7
  82. package/dist/session-admission-block.d.ts +32 -0
  83. package/dist/session-attempt-writers.d.ts +4 -0
  84. package/dist/session-background-commands.js +5 -4
  85. package/dist/session-command-output.js +8 -126
  86. package/dist/session-command-output.js.map +1 -1
  87. package/dist/session-control.d.ts +5 -1
  88. package/dist/session-event-slices.d.ts +10 -0
  89. package/dist/session-event-slices.js +8 -222
  90. package/dist/session-event-slices.js.map +1 -1
  91. package/dist/session-file-attachments.d.ts +28 -0
  92. package/dist/session-goal-reports.d.ts +43 -0
  93. package/dist/session-mcp-approval.d.ts +4 -0
  94. package/dist/session-mcp-credential-rotation.js +5 -4
  95. package/dist/session-mcp-credential-rotation.js.map +1 -1
  96. package/dist/session-message-search.d.ts +22 -0
  97. package/dist/session-queue-commands.d.ts +2 -6
  98. package/dist/session-retry.d.ts +36 -0
  99. package/dist/session-tenancy.js +3 -3
  100. package/dist/skill-source-release-impact.d.ts +18 -0
  101. package/dist/skill-source-release.d.ts +9 -0
  102. package/dist/skills.d.ts +7 -1
  103. package/dist/user-resource-authority.d.ts +0 -8
  104. package/dist/video-generation.js +3 -3
  105. package/dist/workspace-tool-defaults.js +5 -4
  106. package/dist/workspace-tool-defaults.js.map +1 -1
  107. package/drizzle/0468_knowledge_relationship_projection.sql +30 -0
  108. package/drizzle/0469_knowledge_source_discovery.sql +85 -0
  109. package/drizzle/0470_organization_integration_policy.sql +151 -0
  110. package/drizzle/0471_usage_events_account_recent_index.sql +4 -0
  111. package/drizzle/0472_usage_events_workspace_recent_index.sql +4 -0
  112. package/drizzle/0473_organization_usage_analytical_capability.sql +237 -0
  113. package/drizzle/0474_goal_report_requirements.sql +156 -0
  114. package/drizzle/0475_session_event_history_statistics.sql +15 -0
  115. package/drizzle/0476_session_event_history_policy_planning.sql +29 -0
  116. package/drizzle/0477_managed_sign_in_methods.sql +303 -0
  117. package/drizzle/0478_sender_owned_connections.sql +2772 -0
  118. package/drizzle/0479_connection_create_idempotency.sql +16 -0
  119. package/drizzle/0480_private_session_organization_key_administration.sql +154 -0
  120. package/drizzle/0481_personal_connection_visibility_cleanup.sql +45 -0
  121. package/drizzle/0482_remove_packs.sql +417 -0
  122. package/drizzle/0483_preclaim_admission_block.sql +85 -0
  123. package/drizzle/0484_insights_usage_projection.sql +211 -0
  124. package/drizzle/0485_inherited_mcp_approval_policies.sql +6 -0
  125. package/drizzle/0486_reasoning_configuration_message_forks.sql +35 -0
  126. package/drizzle/0487_skill_catalog_message_forks.sql +26 -0
  127. package/drizzle/0488_permanent_skill_removal.sql +207 -0
  128. package/drizzle/0489_modal_command_byte_offsets.sql +17 -0
  129. package/drizzle/0490_periodic_capture_attempt_cadence.sql +6 -0
  130. package/drizzle/0491_warm_capture_holder_reclamation.sql +57 -0
  131. package/drizzle/0492_codex_accepted_source_authority.sql +249 -0
  132. package/drizzle/0493_stopping_command_error_containment.sql +58 -0
  133. package/drizzle/0494_mcp_account_bindings.sql +334 -0
  134. package/drizzle/0495_consented_sandbox_recovery.sql +220 -0
  135. package/drizzle/0496_supervised_command_settlement.sql +386 -0
  136. package/drizzle/0497_modal_inventory_owner_read_capability.sql +112 -0
  137. package/drizzle/0498_enrollment_membership_fence.sql +87 -0
  138. package/drizzle/0499_session_attachment_access.sql +224 -0
  139. package/drizzle/0500_session_execution_epoch.sql +37 -0
  140. package/drizzle/0501_session_sharing_execution.sql +591 -0
  141. package/drizzle/0502_active_message_boundary_forks.sql +30 -0
  142. package/package.json +17 -5
  143. package/src/automations.ts +17 -73
  144. package/src/canonical-human-identities.ts +17 -5
  145. package/src/capability-components.ts +7 -24
  146. package/src/capability-integrations.ts +301 -21
  147. package/src/capability-reconciliation.ts +19 -0
  148. package/src/codex-capacity-recovery.ts +47 -0
  149. package/src/codex-failure-eligibility.ts +93 -0
  150. package/src/connect-attempts.ts +49 -18
  151. package/src/connection-authority.ts +12 -158
  152. package/src/connection-refresh-lock.ts +22 -0
  153. package/src/connection-token-resolver.ts +58 -530
  154. package/src/database-timing.ts +43 -0
  155. package/src/database.ts +61 -26
  156. package/src/editable-artifacts.ts +1 -1
  157. package/src/index.ts +3578 -1273
  158. package/src/insights-model-bundle.ts +19 -7
  159. package/src/insights-usage-bundle.ts +16 -19
  160. package/src/integration-bindings.ts +2 -7
  161. package/src/integration-facets.ts +73 -6
  162. package/src/knowledge-entries.ts +8 -1
  163. package/src/live-session-attempt.ts +0 -22
  164. package/src/mcp-account-bindings.ts +12 -0
  165. package/src/mcp-oauth.ts +26 -0
  166. package/src/organization-integration-policy.ts +235 -0
  167. package/src/organization-usage.ts +83 -0
  168. package/src/plugin-packages.ts +226 -42
  169. package/src/pr-review.ts +7 -69
  170. package/src/preference-registry-schema.ts +1 -0
  171. package/src/provision-roles.ts +50 -37
  172. package/src/retained-provider-commands.ts +286 -9
  173. package/src/retired-session-create-metadata.ts +16 -0
  174. package/src/runtime-posture.ts +217 -13
  175. package/src/schema.ts +104 -120
  176. package/src/session-admission-block.ts +29 -0
  177. package/src/session-attempt-writers.ts +48 -0
  178. package/src/session-control.ts +21 -5
  179. package/src/session-event-slices.ts +76 -26
  180. package/src/session-file-attachments.ts +66 -0
  181. package/src/session-goal-reports.ts +255 -0
  182. package/src/session-mcp-approval.ts +30 -0
  183. package/src/session-message-search.ts +348 -0
  184. package/src/session-queue-commands.ts +60 -36
  185. package/src/session-retry.ts +319 -0
  186. package/src/skill-config-migration.ts +11 -2
  187. package/src/skill-source-release-impact.ts +33 -0
  188. package/src/skill-source-release.ts +67 -26
  189. package/src/skills.ts +44 -6
  190. package/src/user-resource-authority.ts +0 -56
  191. package/dist/chunk-6T2GGYWE.js.map +0 -1
  192. package/dist/chunk-7HMK3B45.js.map +0 -1
  193. package/dist/chunk-DPONPYJD.js.map +0 -1
  194. package/dist/chunk-F73UKJY6.js.map +0 -1
  195. package/dist/chunk-T6YLHJLS.js.map +0 -1
  196. package/dist/host-mcp-binding-match.d.ts +0 -5
  197. package/dist/host-mcp-bindings.d.ts +0 -79
  198. package/dist/host-mcp-resolvers.d.ts +0 -47
  199. package/dist/host-mcp-task-authority.d.ts +0 -43
  200. package/dist/host-selection-identity.d.ts +0 -6
  201. package/dist/pack-components.d.ts +0 -83
  202. package/dist/pack-installations.d.ts +0 -86
  203. package/src/host-mcp-binding-match.ts +0 -34
  204. package/src/host-mcp-bindings.ts +0 -777
  205. package/src/host-mcp-resolvers.ts +0 -266
  206. package/src/host-mcp-task-authority.ts +0 -384
  207. package/src/host-selection-identity.ts +0 -29
  208. package/src/pack-components.ts +0 -980
  209. package/src/pack-installations.ts +0 -950
  210. /package/dist/{chunk-MWMXWHJD.js.map → chunk-7JCDSMX2.js.map} +0 -0
  211. /package/dist/{chunk-ERKVSPDX.js.map → chunk-LVY33EWL.js.map} +0 -0
  212. /package/dist/{chunk-T6BWIGWJ.js.map → chunk-RIYHP2WU.js.map} +0 -0
@@ -0,0 +1,386 @@
1
+ -- deployment-mode: rolling
2
+ -- Expand first; launchers must check readiness before starting an idle supervisor.
3
+ -- Legacy commands stay legacy. Old writers may read supervised rows, but cannot
4
+ -- release their blockers or erase their proof. Deploy compatible control readers
5
+ -- before enabling new launches; rolling rollback leaves supervised work fenced.
6
+ SET LOCAL lock_timeout = '5s';
7
+
8
+ ALTER TABLE sandbox_retained_processes
9
+ -- Unlike xmin, this remains the INSERT transaction after later updates.
10
+ ADD COLUMN supervision_retention_xid xid8,
11
+ ADD COLUMN supervision_receipt jsonb,
12
+ ADD COLUMN supervision_output_captured boolean NOT NULL DEFAULT false,
13
+ ADD COLUMN cancellation_requested_at timestamptz,
14
+ ADD COLUMN cancellation_reason text;
15
+
16
+ -- No historical backfill or volatile-default table rewrite. NULL denotes an
17
+ -- already-committed legacy row and can never be upgraded to supervision.
18
+ ALTER TABLE sandbox_retained_processes ALTER COLUMN supervision_retention_xid SET DEFAULT pg_current_xact_id();
19
+
20
+ CREATE FUNCTION opengeni_private.supervised_command_guard() RETURNS trigger
21
+ LANGUAGE plpgsql SET search_path FROM CURRENT AS $guard$
22
+ DECLARE
23
+ descriptor jsonb;
24
+ receipt jsonb;
25
+ stream text;
26
+ loss_binding jsonb;
27
+ capture_lease sandbox_leases%ROWTYPE;
28
+ BEGIN
29
+ IF TG_OP = 'DELETE' THEN
30
+ IF OLD.provider_command ? 'supervision' AND (
31
+ OLD.state = 'active' OR (OLD.state <> 'lost' AND (OLD.supervision_receipt IS NULL OR NOT OLD.supervision_output_captured))
32
+ ) THEN
33
+ RAISE EXCEPTION 'Unsettled supervised command evidence cannot be deleted' USING ERRCODE = '55000';
34
+ END IF;
35
+ RETURN OLD;
36
+ END IF;
37
+ descriptor := NEW.provider_command->'supervision';
38
+ IF TG_OP = 'UPDATE' THEN
39
+ IF NEW.supervision_retention_xid IS DISTINCT FROM OLD.supervision_retention_xid THEN
40
+ RAISE EXCEPTION 'Retention transaction identity is immutable' USING ERRCODE = '55000';
41
+ END IF;
42
+ IF OLD.provider_command ? 'supervision' AND (
43
+ descriptor IS DISTINCT FROM OLD.provider_command->'supervision'
44
+ OR (NEW.provider_command - 'streams') IS DISTINCT FROM (OLD.provider_command - 'streams')
45
+ OR ROW(NEW.lease_id, NEW.lease_epoch, NEW.provider_backend, NEW.provider_instance_id,
46
+ NEW.provider_binding_key, NEW.provider_binding, NEW.parent_admission_id, NEW.holder_id,
47
+ NEW.account_id, NEW.workspace_id, NEW.session_id, NEW.sandbox_group_id, NEW.id,
48
+ NEW.provider_session_id, NEW.route_kind, NEW.route_target_id, NEW.route_epoch)
49
+ IS DISTINCT FROM ROW(OLD.lease_id, OLD.lease_epoch, OLD.provider_backend, OLD.provider_instance_id,
50
+ OLD.provider_binding_key, OLD.provider_binding, OLD.parent_admission_id, OLD.holder_id,
51
+ OLD.account_id, OLD.workspace_id, OLD.session_id, OLD.sandbox_group_id, OLD.id,
52
+ OLD.provider_session_id, OLD.route_kind, OLD.route_target_id, OLD.route_epoch)
53
+ ) THEN
54
+ RAISE EXCEPTION 'Supervised command identity is immutable' USING ERRCODE = '55000';
55
+ END IF;
56
+ -- The retainer promotes then attaches the locator in ONE transaction.
57
+ -- Never retrofit supervision to a committed descriptor-free legacy row.
58
+ IF descriptor IS NOT NULL AND NOT (coalesce(OLD.provider_command, '{}'::jsonb) ? 'supervision')
59
+ AND OLD.supervision_retention_xid IS DISTINCT FROM pg_current_xact_id() THEN
60
+ RAISE EXCEPTION 'Supervision requires initial retention' USING ERRCODE = '55000';
61
+ END IF;
62
+ IF OLD.cancellation_requested_at IS NOT NULL AND
63
+ ROW(NEW.cancellation_requested_at, NEW.cancellation_reason) IS DISTINCT FROM
64
+ ROW(OLD.cancellation_requested_at, OLD.cancellation_reason) THEN
65
+ RAISE EXCEPTION 'Cancellation intent is monotonic' USING ERRCODE = '55000';
66
+ END IF;
67
+ IF OLD.supervision_receipt IS NOT NULL AND NEW.supervision_receipt IS DISTINCT FROM OLD.supervision_receipt
68
+ OR OLD.supervision_output_captured AND NOT NEW.supervision_output_captured THEN
69
+ RAISE EXCEPTION 'Supervision proof is immutable' USING ERRCODE = '55000';
70
+ END IF;
71
+ IF (OLD.cancellation_requested_at IS NOT NULL OR OLD.supervision_receipt IS NOT NULL)
72
+ AND NEW.provider_command_input_index IS DISTINCT FROM OLD.provider_command_input_index THEN
73
+ RAISE EXCEPTION 'Supervised stdin admission is closed' USING ERRCODE = '55000';
74
+ END IF;
75
+ IF descriptor IS NOT NULL AND NEW.provider_command_input_index IS DISTINCT FROM OLD.provider_command_input_index
76
+ AND NOT EXISTS (
77
+ SELECT 1 FROM sandbox_workspace_mutation_admissions child
78
+ WHERE child.account_id = NEW.account_id AND child.workspace_id = NEW.workspace_id
79
+ AND child.actor_kind = 'process' AND child.actor_id = NEW.id AND child.settled_at IS NULL
80
+ AND child.lease_id = NEW.lease_id AND child.lease_epoch = NEW.lease_epoch
81
+ AND child.provider_backend = NEW.provider_backend AND child.provider_instance_id = NEW.provider_instance_id
82
+ ) THEN
83
+ RAISE EXCEPTION 'Supervised stdin requires an unsettled child admission' USING ERRCODE = '55000';
84
+ END IF;
85
+ IF descriptor IS NOT NULL AND NEW.provider_command_input_index < OLD.provider_command_input_index THEN
86
+ RAISE EXCEPTION 'Supervised stdin sequence cannot regress' USING ERRCODE = '55000';
87
+ END IF;
88
+ IF OLD.cancellation_requested_at IS NOT NULL AND
89
+ ROW(NEW.owner_actor_kind, NEW.owner_actor_id, NEW.owner_turn_id, NEW.owner_attempt_id, NEW.owner_execution_generation)
90
+ IS DISTINCT FROM ROW(OLD.owner_actor_kind, OLD.owner_actor_id, OLD.owner_turn_id, OLD.owner_attempt_id, OLD.owner_execution_generation) THEN
91
+ RAISE EXCEPTION 'Cancelled command cannot be adopted' USING ERRCODE = '55000';
92
+ END IF;
93
+ IF OLD.provider_command ? 'supervision' AND OLD.state <> 'active' AND
94
+ ROW(NEW.state, NEW.exit_code, NEW.settled_at) IS DISTINCT FROM ROW(OLD.state, OLD.exit_code, OLD.settled_at) THEN
95
+ RAISE EXCEPTION 'Supervised terminal state is immutable' USING ERRCODE = '55000';
96
+ END IF;
97
+ IF OLD.provider_command ? 'supervision' THEN
98
+ -- Old JSON writers must not skip output bytes that a newer reader would
99
+ -- otherwise mistake for captured output. This transaction-local protocol
100
+ -- marker is set only around captureRetainedRouterOutput's atomic event +
101
+ -- cursor update. It is not an authentication boundary against DB owners.
102
+ IF ((NEW.provider_command->'streams') IS DISTINCT FROM (OLD.provider_command->'streams')
103
+ OR NEW.supervision_output_captured IS DISTINCT FROM OLD.supervision_output_captured)
104
+ AND current_setting('opengeni.supervised_output_capture_process_id', true) IS DISTINCT FROM NEW.id::text THEN
105
+ RAISE EXCEPTION 'Supervised output requires atomic capture protocol' USING ERRCODE = '55000';
106
+ END IF;
107
+ FOREACH stream IN ARRAY ARRAY['stdout', 'stderr'] LOOP
108
+ IF (NEW.provider_command->'streams'->stream->>'byteOffset')::bigint <
109
+ (OLD.provider_command->'streams'->stream->>'byteOffset')::bigint
110
+ OR (OLD.provider_command->'streams'->stream->>'eof')::boolean AND
111
+ ((NEW.provider_command->'streams'->stream) - 'exitCode') IS DISTINCT FROM ((OLD.provider_command->'streams'->stream) - 'exitCode')
112
+ OR OLD.provider_command->'streams'->stream->>'exitCode' IS NOT NULL AND
113
+ NEW.provider_command->'streams'->stream IS DISTINCT FROM OLD.provider_command->'streams'->stream THEN
114
+ RAISE EXCEPTION 'Supervised output proof cannot regress' USING ERRCODE = '55000';
115
+ END IF;
116
+ END LOOP;
117
+ END IF;
118
+ END IF;
119
+ IF (NEW.cancellation_requested_at IS NULL) <> (NEW.cancellation_reason IS NULL)
120
+ OR NEW.cancellation_reason IS NOT NULL AND NEW.cancellation_reason NOT IN ('provider_deadline', 'explicit_stop') THEN
121
+ RAISE EXCEPTION 'Invalid cancellation intent' USING ERRCODE = '23514';
122
+ END IF;
123
+ IF descriptor IS NULL THEN
124
+ IF NEW.supervision_receipt IS NOT NULL OR NEW.supervision_output_captured THEN
125
+ RAISE EXCEPTION 'Legacy command cannot acquire supervision proof' USING ERRCODE = '23514';
126
+ END IF;
127
+ RETURN NEW;
128
+ END IF;
129
+ -- Initial attachment and capture serialize on the lease. A committed legacy
130
+ -- row cannot be upgraded (above); an in-flight initial attachment must not
131
+ -- appear after an older worker has already admitted capture/containment.
132
+ IF TG_OP = 'INSERT' OR NOT (coalesce(OLD.provider_command, '{}'::jsonb) ? 'supervision') THEN
133
+ SELECT * INTO capture_lease FROM sandbox_leases WHERE id = NEW.lease_id FOR UPDATE;
134
+ IF capture_lease.archive_capture_id IS NOT NULL OR capture_lease.unobservable_command_drain_ids IS NOT NULL THEN
135
+ RAISE EXCEPTION 'Supervised launch cannot join checkpoint containment' USING ERRCODE = '55000';
136
+ END IF;
137
+ END IF;
138
+ IF NOT ((jsonb_typeof(descriptor) = 'object'
139
+ AND NEW.provider_command->>'kind' = 'modal-router-v1'
140
+ AND NOT coalesce((NEW.provider_command->>'pty')::boolean, false)
141
+ AND descriptor->>'protocol' = 'native-subreaper-v1'
142
+ AND descriptor->>'invocationId' ~ '^[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$'
143
+ AND descriptor->>'nonce' ~ '^[0-9a-f]{64}$'
144
+ AND descriptor->>'controlPath' ~ '^/tmp/opengeni-supervision/[a-f0-9-]{36}\.sock$'
145
+ AND descriptor - ARRAY['protocol', 'invocationId', 'nonce', 'controlPath'] = '{}'::jsonb) IS TRUE) THEN
146
+ RAISE EXCEPTION 'Invalid supervision descriptor' USING ERRCODE = '23514';
147
+ END IF;
148
+ receipt := NEW.supervision_receipt;
149
+ IF receipt IS NOT NULL AND NOT ((jsonb_typeof(receipt) = 'object'
150
+ AND receipt->>'protocol' = descriptor->>'protocol'
151
+ AND receipt->>'invocationId' = descriptor->>'invocationId'
152
+ AND receipt->>'receiptId' ~ '^[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$'
153
+ AND jsonb_typeof(receipt->'leaderExitCode') = 'number'
154
+ AND receipt->>'leaderExitCode' ~ '^-?[0-9]+$'
155
+ AND (receipt->>'leaderExitCode')::numeric BETWEEN -2147483648 AND 2147483647
156
+ AND receipt - ARRAY['protocol', 'invocationId', 'receiptId', 'leaderExitCode'] = '{}'::jsonb) IS TRUE) THEN
157
+ RAISE EXCEPTION 'Invalid invocation-bound supervision receipt' USING ERRCODE = '23514';
158
+ END IF;
159
+ IF NEW.supervision_output_captured AND NOT ((
160
+ NEW.provider_command->'streams'->'stdout'->>'eof' = 'true'
161
+ AND NEW.provider_command->'streams'->'stderr'->>'eof' = 'true'
162
+ AND NEW.provider_command->'streams'->'stdout'->>'utf8Remainder' = ''
163
+ AND NEW.provider_command->'streams'->'stderr'->>'utf8Remainder' = ''
164
+ -- Zero is the supervisor's successful durable-receipt ACK exit. The user
165
+ -- shell result is independent and remains receipt.leaderExitCode.
166
+ AND NEW.provider_command->'streams'->'stdout'->>'exitCode' = '0'
167
+ AND NEW.provider_command->'streams'->'stderr'->>'exitCode' = '0'
168
+ ) IS TRUE) THEN
169
+ RAISE EXCEPTION 'Supervised output capture is incomplete' USING ERRCODE = '55000';
170
+ END IF;
171
+ IF NEW.state = 'lost' THEN
172
+ -- Missing provider is a distinct terminal truth, never successful tree
173
+ -- quiescence. The typed loss seam locks blockers then the exact lease and
174
+ -- opens this marker only after revalidating the original physical binding.
175
+ -- A deferred trigger below ALSO requires the loss-bearing cold successor
176
+ -- at commit, so merely forging a GUC cannot drop a live provider's blockers.
177
+ IF TG_OP <> 'UPDATE' OR NEW.exit_code IS NOT NULL OR NEW.settled_at IS NULL
178
+ OR ROW(NEW.provider_command, NEW.supervision_receipt, NEW.supervision_output_captured,
179
+ NEW.provider_command_input_index)
180
+ IS DISTINCT FROM ROW(OLD.provider_command, OLD.supervision_receipt, OLD.supervision_output_captured,
181
+ OLD.provider_command_input_index) THEN
182
+ RAISE EXCEPTION 'Supervised loss cannot fabricate execution or output proof' USING ERRCODE = '55000';
183
+ END IF;
184
+ IF NEW.settlement_reason = 'provider_start_rejected' THEN
185
+ -- A distinct authenticated "never started" response licenses only an
186
+ -- untouched retained launch, not a missing provider or failed supervisor.
187
+ loss_binding := nullif(current_setting('opengeni.supervised_launch_rejection_binding', true), '')::jsonb;
188
+ IF NEW.supervision_receipt IS NOT NULL OR NEW.supervision_output_captured
189
+ OR NEW.provider_command_input_index <> 0 OR NEW.reconcile_proof_outcome IS NOT NULL
190
+ OR NEW.provider_command->'streams' IS DISTINCT FROM
191
+ '{"stdout":{"byteOffset":0,"utf8Remainder":"","eof":false,"exitCode":null},"stderr":{"byteOffset":0,"utf8Remainder":"","eof":false,"exitCode":null}}'::jsonb
192
+ OR EXISTS (SELECT 1 FROM sandbox_workspace_mutation_admissions child
193
+ WHERE child.account_id=NEW.account_id AND child.workspace_id=NEW.workspace_id
194
+ AND child.actor_kind='process' AND child.actor_id=NEW.id)
195
+ OR (OLD.state = 'lost' AND OLD.settlement_reason IS DISTINCT FROM NEW.settlement_reason)
196
+ OR (OLD.state <> 'lost' AND (OLD.state <> 'active' OR loss_binding IS DISTINCT FROM
197
+ jsonb_build_object('processId', OLD.id, 'command', OLD.provider_command))) THEN
198
+ RAISE EXCEPTION 'Supervised launch rejection requires an exact pristine invocation' USING ERRCODE = '55000';
199
+ END IF;
200
+ RETURN NEW;
201
+ END IF;
202
+ loss_binding := nullif(current_setting('opengeni.supervised_provider_loss_binding', true), '')::jsonb;
203
+ IF NEW.settlement_reason IS DISTINCT FROM 'provider_instance_lost'
204
+ OR (OLD.state = 'lost' AND OLD.settlement_reason IS DISTINCT FROM NEW.settlement_reason)
205
+ OR (OLD.state <> 'lost' AND (
206
+ OLD.state <> 'active' OR loss_binding IS DISTINCT FROM jsonb_build_object(
207
+ 'accountId', OLD.account_id, 'workspaceId', OLD.workspace_id,
208
+ 'leaseId', OLD.lease_id, 'sandboxGroupId', OLD.sandbox_group_id,
209
+ 'lostEpoch', OLD.lease_epoch, 'lostBackend', OLD.provider_backend,
210
+ 'lostInstanceId', OLD.provider_instance_id
211
+ )
212
+ )) THEN
213
+ RAISE EXCEPTION 'Supervised loss requires the exact typed provider-loss transition' USING ERRCODE = '55000';
214
+ END IF;
215
+ RETURN NEW;
216
+ END IF;
217
+ IF receipt IS NOT NULL AND EXISTS (
218
+ SELECT 1 FROM sandbox_workspace_mutation_admissions child
219
+ WHERE child.account_id = NEW.account_id AND child.workspace_id = NEW.workspace_id
220
+ AND child.actor_kind = 'process' AND child.actor_id = NEW.id AND child.settled_at IS NULL
221
+ ) THEN
222
+ RAISE EXCEPTION 'Admitted command input must settle before quiescence' USING ERRCODE = '55000';
223
+ END IF;
224
+ IF NEW.state <> 'active' OR NEW.settled_at IS NOT NULL THEN
225
+ IF NOT ((NEW.state = 'exited' AND receipt IS NOT NULL
226
+ AND NEW.supervision_output_captured
227
+ AND NEW.exit_code = (receipt->>'leaderExitCode')::integer
228
+ ) IS TRUE) THEN
229
+ RAISE EXCEPTION 'Supervised settlement requires quiescence, provider terminal and captured output' USING ERRCODE = '55000';
230
+ END IF;
231
+ END IF;
232
+ RETURN NEW;
233
+ END
234
+ $guard$;
235
+ REVOKE ALL ON FUNCTION opengeni_private.supervised_command_guard() FROM PUBLIC;
236
+ CREATE TRIGGER supervised_command_guard BEFORE INSERT OR UPDATE OR DELETE
237
+ ON sandbox_retained_processes FOR EACH ROW EXECUTE FUNCTION opengeni_private.supervised_command_guard();
238
+
239
+ CREATE FUNCTION opengeni_private.supervised_provider_loss_commit_guard() RETURNS trigger
240
+ LANGUAGE plpgsql SET search_path FROM CURRENT AS $loss$
241
+ BEGIN
242
+ IF NOT EXISTS (
243
+ SELECT 1 FROM sandbox_leases lease
244
+ WHERE lease.id = NEW.lease_id AND lease.account_id = NEW.account_id
245
+ AND lease.workspace_id = NEW.workspace_id AND lease.sandbox_group_id = NEW.sandbox_group_id
246
+ AND lease.backend = NEW.provider_backend AND lease.lease_epoch = NEW.lease_epoch + 1
247
+ AND lease.liveness = 'cold' AND lease.instance_id IS NULL
248
+ AND lease.resume_state #>> '{opengeniRecovery,provider,status}' = 'missing'
249
+ AND lease.resume_state #>> '{opengeniRecovery,provider,instanceId}' = NEW.provider_instance_id
250
+ AND lease.resume_state #>> '{opengeniRecovery,restore,status}' IN ('pending', 'degraded', 'unrecoverable')
251
+ AND lease.resume_state #>> '{opengeniRecovery,workspace,status}' IN ('not_ready', 'degraded', 'unrecoverable')
252
+ FOR SHARE OF lease
253
+ ) THEN
254
+ RAISE EXCEPTION 'Supervised loss requires matching cold missing-provider recovery truth' USING ERRCODE = '55000';
255
+ END IF;
256
+ RETURN NEW;
257
+ END
258
+ $loss$;
259
+ REVOKE ALL ON FUNCTION opengeni_private.supervised_provider_loss_commit_guard() FROM PUBLIC;
260
+ CREATE CONSTRAINT TRIGGER supervised_provider_loss_commit_guard AFTER UPDATE ON sandbox_retained_processes
261
+ DEFERRABLE INITIALLY DEFERRED FOR EACH ROW
262
+ WHEN (OLD.state = 'active' AND NEW.state = 'lost' AND NEW.provider_command ? 'supervision'
263
+ AND NEW.settlement_reason = 'provider_instance_lost')
264
+ EXECUTE FUNCTION opengeni_private.supervised_provider_loss_commit_guard();
265
+
266
+ CREATE FUNCTION opengeni_private.supervised_command_blocker_guard() RETURNS trigger
267
+ LANGUAGE plpgsql SET search_path FROM CURRENT AS $blockers$
268
+ DECLARE p sandbox_retained_processes%ROWTYPE;
269
+ BEGIN
270
+ IF TG_TABLE_NAME = 'sandbox_workspace_mutation_admissions' THEN
271
+ IF TG_OP <> 'INSERT' AND OLD.actor_kind = 'process' THEN
272
+ SELECT * INTO p FROM sandbox_retained_processes
273
+ WHERE id = OLD.actor_id AND account_id = OLD.account_id AND workspace_id = OLD.workspace_id
274
+ AND provider_command ? 'supervision';
275
+ IF FOUND THEN
276
+ IF TG_OP = 'DELETE' THEN
277
+ IF p.state = 'active' OR OLD.settled_at IS NULL THEN
278
+ RAISE EXCEPTION 'Supervised child admission evidence is immutable' USING ERRCODE = '55000';
279
+ END IF;
280
+ RETURN OLD;
281
+ END IF;
282
+ IF p.state = 'lost' AND OLD.settled_at IS NULL AND NEW.provider_outcome IS DISTINCT FROM 'rejected' THEN
283
+ RAISE EXCEPTION 'Lost provider child admission cannot resolve successfully' USING ERRCODE = '55000';
284
+ END IF;
285
+ IF
286
+ (to_jsonb(NEW) - ARRAY['provider_outcome', 'settled_at']) IS DISTINCT FROM
287
+ (to_jsonb(OLD) - ARRAY['provider_outcome', 'settled_at']) OR
288
+ OLD.settled_at IS NOT NULL AND ROW(NEW.settled_at, NEW.provider_outcome) IS DISTINCT FROM ROW(OLD.settled_at, OLD.provider_outcome) THEN
289
+ RAISE EXCEPTION 'Supervised child admission evidence is immutable' USING ERRCODE = '55000';
290
+ END IF;
291
+ END IF;
292
+ END IF;
293
+ IF TG_OP = 'UPDATE' AND NEW.actor_kind = 'process' AND
294
+ ROW(NEW.actor_id, NEW.actor_kind, NEW.account_id, NEW.workspace_id) IS DISTINCT FROM
295
+ ROW(OLD.actor_id, OLD.actor_kind, OLD.account_id, OLD.workspace_id) THEN
296
+ SELECT * INTO p FROM sandbox_retained_processes
297
+ WHERE id = NEW.actor_id AND account_id = NEW.account_id AND workspace_id = NEW.workspace_id
298
+ AND provider_command ? 'supervision' FOR UPDATE;
299
+ IF FOUND THEN
300
+ RAISE EXCEPTION 'Supervised child admission requires initial insert' USING ERRCODE = '55000';
301
+ END IF;
302
+ END IF;
303
+ IF TG_OP = 'INSERT' THEN
304
+ IF NEW.actor_kind = 'process' THEN
305
+ SELECT * INTO p FROM sandbox_retained_processes
306
+ WHERE id = NEW.actor_id AND account_id = NEW.account_id AND workspace_id = NEW.workspace_id FOR UPDATE;
307
+ IF p.cancellation_requested_at IS NOT NULL OR p.supervision_receipt IS NOT NULL THEN
308
+ RAISE EXCEPTION 'Command mutation admission is closed' USING ERRCODE = '55000';
309
+ END IF;
310
+ IF p.provider_command ? 'supervision' AND (p.state <> 'active' OR
311
+ ROW(NEW.session_id, NEW.lease_id, NEW.lease_epoch, NEW.provider_backend, NEW.provider_instance_id,
312
+ NEW.holder_kind, NEW.holder_id, NEW.route_kind, NEW.route_target_id, NEW.route_epoch)
313
+ IS DISTINCT FROM ROW(p.session_id, p.lease_id, p.lease_epoch, p.provider_backend, p.provider_instance_id,
314
+ 'process'::text, p.holder_id, p.route_kind, p.route_target_id, p.route_epoch)) THEN
315
+ RAISE EXCEPTION 'Supervised child admission changed execution identity' USING ERRCODE = '55000';
316
+ END IF;
317
+ END IF;
318
+ RETURN NEW;
319
+ END IF;
320
+ -- Terminal state and proof are immutable. A snapshot is sufficient for a
321
+ -- release; acquiring process locks after admission/holder row locks would
322
+ -- invert canonical settlement's process -> admission -> lease ordering.
323
+ SELECT * INTO p FROM sandbox_retained_processes
324
+ WHERE parent_admission_id = OLD.id AND provider_command ? 'supervision';
325
+ IF FOUND AND (TG_OP = 'DELETE' OR NEW IS DISTINCT FROM OLD) THEN
326
+ IF p.state = 'active' THEN
327
+ RAISE EXCEPTION 'Supervised parent admission remains retained' USING ERRCODE = '55000';
328
+ END IF;
329
+ IF TG_OP <> 'DELETE' AND p.state = 'lost' AND NEW.provider_outcome IS DISTINCT FROM 'rejected' THEN
330
+ RAISE EXCEPTION 'Lost provider parent admission cannot resolve successfully' USING ERRCODE = '55000';
331
+ END IF;
332
+ END IF;
333
+ ELSE
334
+ SELECT * INTO p FROM sandbox_retained_processes
335
+ WHERE lease_id = OLD.lease_id AND holder_id = OLD.holder_id AND OLD.kind = 'process'
336
+ AND provider_command ? 'supervision';
337
+ IF FOUND AND p.state = 'active' AND (TG_OP = 'DELETE' OR NEW IS DISTINCT FROM OLD) THEN
338
+ RAISE EXCEPTION 'Supervised process holder remains retained' USING ERRCODE = '55000';
339
+ END IF;
340
+ END IF;
341
+ IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
342
+ RETURN NEW;
343
+ END
344
+ $blockers$;
345
+ REVOKE ALL ON FUNCTION opengeni_private.supervised_command_blocker_guard() FROM PUBLIC;
346
+ CREATE TRIGGER supervised_command_admission_guard BEFORE INSERT OR UPDATE OR DELETE
347
+ ON sandbox_workspace_mutation_admissions FOR EACH ROW EXECUTE FUNCTION opengeni_private.supervised_command_blocker_guard();
348
+ CREATE TRIGGER supervised_command_holder_guard BEFORE UPDATE OR DELETE
349
+ ON sandbox_lease_holders FOR EACH ROW EXECUTE FUNCTION opengeni_private.supervised_command_blocker_guard();
350
+
351
+ -- Old control workers also run the legacy containment path. Refuse its durable
352
+ -- enrollment/capture/publication transitions before provider I/O, not merely
353
+ -- the later process settlement. Typed provider loss may clear a capture and
354
+ -- preserve an older archive; it must not create a fresh checkpoint.
355
+ CREATE FUNCTION opengeni_private.supervised_command_capture_guard() RETURNS trigger
356
+ LANGUAGE plpgsql SET search_path FROM CURRENT AS $capture$
357
+ BEGIN
358
+ IF (
359
+ NEW.unobservable_command_drain_ids IS NOT NULL AND
360
+ NEW.unobservable_command_drain_ids IS DISTINCT FROM OLD.unobservable_command_drain_ids
361
+ OR NEW.archive_capture_id IS NOT NULL AND NEW.archive_capture_id IS DISTINCT FROM OLD.archive_capture_id
362
+ OR NEW.archive_capture_published_at IS NOT NULL AND
363
+ NEW.archive_capture_published_at IS DISTINCT FROM OLD.archive_capture_published_at
364
+ OR NEW.archive_generation IS NOT NULL AND NEW.archive_generation IS DISTINCT FROM OLD.archive_generation
365
+ OR (NEW.resume_state #> '{sessionState,workspaceArchive}') IS DISTINCT FROM
366
+ (OLD.resume_state #> '{sessionState,workspaceArchive}')
367
+ OR (NEW.resume_state #> '{sessionState,workspaceArchiveMeta}') IS DISTINCT FROM
368
+ (OLD.resume_state #> '{sessionState,workspaceArchiveMeta}')
369
+ OR (NEW.resume_state #> '{sessionState,workspaceArchiveRef}') IS DISTINCT FROM
370
+ (OLD.resume_state #> '{sessionState,workspaceArchiveRef}')
371
+ ) AND EXISTS (
372
+ SELECT 1 FROM sandbox_retained_processes
373
+ WHERE lease_id = NEW.id AND state = 'active' AND provider_command ? 'supervision'
374
+ ) THEN
375
+ RAISE EXCEPTION 'Supervised command blocks legacy containment and checkpoint publication' USING ERRCODE = '55000';
376
+ END IF;
377
+ RETURN NEW;
378
+ END
379
+ $capture$;
380
+ REVOKE ALL ON FUNCTION opengeni_private.supervised_command_capture_guard() FROM PUBLIC;
381
+ CREATE TRIGGER supervised_command_capture_guard BEFORE UPDATE OF
382
+ unobservable_command_drain_ids, archive_capture_id, archive_capture_published_at,
383
+ archive_generation, resume_state ON sandbox_leases
384
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.supervised_command_capture_guard();
385
+
386
+ RESET lock_timeout;
@@ -0,0 +1,112 @@
1
+ -- deployment-mode: rolling
2
+ -- The global orphan-safety inventory must not interpret FORCE-RLS blindness
3
+ -- as an empty fleet. Restore its existing cross-tenant read contract only
4
+ -- while the audited function runs as the exact non-bypass migration owner.
5
+ SET LOCAL lock_timeout = '5s';
6
+ SET LOCAL statement_timeout = '5min';
7
+
8
+ CREATE TABLE opengeni_private.modal_inventory_read_capabilities (
9
+ backend_pid integer NOT NULL,
10
+ transaction_id xid8 NOT NULL,
11
+ data_schema text NOT NULL,
12
+ PRIMARY KEY (backend_pid, transaction_id, data_schema)
13
+ );
14
+ REVOKE ALL ON TABLE opengeni_private.modal_inventory_read_capabilities FROM PUBLIC;
15
+
16
+ -- Hostile owner defaults must not give any non-owner a mint/read/delete seam.
17
+ DO $capability_acl$
18
+ DECLARE role_name text;
19
+ BEGIN
20
+ FOR role_name IN
21
+ SELECT DISTINCT role.rolname
22
+ FROM pg_catalog.pg_class relation
23
+ CROSS JOIN LATERAL pg_catalog.aclexplode(coalesce(
24
+ relation.relacl, pg_catalog.acldefault('r', relation.relowner)
25
+ )) acl
26
+ JOIN pg_catalog.pg_roles role ON role.oid = acl.grantee
27
+ WHERE relation.oid = 'opengeni_private.modal_inventory_read_capabilities'::regclass
28
+ AND acl.grantee <> relation.relowner
29
+ LOOP
30
+ EXECUTE pg_catalog.format(
31
+ 'REVOKE ALL ON TABLE opengeni_private.modal_inventory_read_capabilities FROM %I',
32
+ role_name
33
+ );
34
+ END LOOP;
35
+ END
36
+ $capability_acl$;
37
+
38
+ DO $inventory$
39
+ DECLARE
40
+ data_schema text := pg_catalog.current_schema();
41
+ migration_owner text := current_user;
42
+ role_name text;
43
+ BEGIN
44
+ -- RLS checks function EXECUTE before short-circuiting the owner comparison.
45
+ -- This public, value-free predicate cannot create a capability. Authority
46
+ -- checks current_user in the policy, not a role name supplied by its caller.
47
+ EXECUTE pg_catalog.format($ddl$
48
+ CREATE FUNCTION %1$I.modal_inventory_read_capability_active()
49
+ RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER
50
+ SET search_path = pg_catalog AS $body$
51
+ SELECT EXISTS (
52
+ SELECT 1 FROM opengeni_private.modal_inventory_read_capabilities capability
53
+ WHERE capability.backend_pid = pg_catalog.pg_backend_pid()
54
+ AND capability.transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
55
+ AND capability.data_schema = %2$L
56
+ )
57
+ $body$;
58
+ REVOKE ALL ON FUNCTION %1$I.modal_inventory_read_capability_active() FROM PUBLIC;
59
+ GRANT EXECUTE ON FUNCTION %1$I.modal_inventory_read_capability_active() TO PUBLIC;
60
+ CREATE POLICY modal_inventory_owner_read ON %1$I.sandbox_leases FOR SELECT
61
+ USING (current_user = %3$L AND %1$I.modal_inventory_read_capability_active());
62
+ $ddl$, data_schema, data_schema, migration_owner);
63
+
64
+ -- RETURN QUERY materializes its rows before cleanup and before the caller
65
+ -- regains control. A nested invocation retains any pre-existing capability;
66
+ -- the outer owner remains responsible for its lifetime. Exception rollback
67
+ -- also removes a capability inserted by this invocation.
68
+ EXECUTE pg_catalog.format($ddl$
69
+ CREATE OR REPLACE FUNCTION opengeni_private.list_live_modal_sandbox_leases()
70
+ RETURNS TABLE (
71
+ lease_id uuid, workspace_id uuid, sandbox_group_id uuid,
72
+ instance_id text, liveness text
73
+ ) LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $body$
74
+ DECLARE opened integer;
75
+ BEGIN
76
+ INSERT INTO opengeni_private.modal_inventory_read_capabilities (
77
+ backend_pid, transaction_id, data_schema
78
+ ) VALUES (pg_catalog.pg_backend_pid(), pg_catalog.pg_current_xact_id(), %2$L)
79
+ ON CONFLICT DO NOTHING;
80
+ GET DIAGNOSTICS opened = ROW_COUNT;
81
+
82
+ RETURN QUERY SELECT lease.id, lease.workspace_id, lease.sandbox_group_id,
83
+ lease.instance_id, lease.liveness
84
+ FROM %1$I.sandbox_leases lease
85
+ WHERE lease.liveness IN ('warming', 'warm', 'draining')
86
+ AND (lease.backend = 'modal' OR lease.resume_backend_id = 'modal');
87
+
88
+ IF opened = 1 THEN
89
+ DELETE FROM opengeni_private.modal_inventory_read_capabilities capability
90
+ WHERE capability.backend_pid = pg_catalog.pg_backend_pid()
91
+ AND capability.transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
92
+ AND capability.data_schema = %2$L;
93
+ END IF;
94
+ RETURN;
95
+ END
96
+ $body$;
97
+ $ddl$, data_schema, data_schema);
98
+ REVOKE ALL ON FUNCTION opengeni_private.list_live_modal_sandbox_leases() FROM PUBLIC;
99
+ -- Preserve existing explicit runtime grants; support existing configured
100
+ -- roles and migrate-before-provision installs without requiring role creation.
101
+ FOR role_name IN SELECT jsonb_array_elements_text(coalesce(nullif(
102
+ pg_catalog.current_setting('opengeni.migration_application_roles', true), ''
103
+ )::jsonb, '[]'::jsonb)) LOOP
104
+ IF EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = role_name) THEN
105
+ EXECUTE pg_catalog.format(
106
+ 'GRANT EXECUTE ON FUNCTION opengeni_private.list_live_modal_sandbox_leases() TO %I',
107
+ role_name
108
+ );
109
+ END IF;
110
+ END LOOP;
111
+ END
112
+ $inventory$;
@@ -0,0 +1,87 @@
1
+ -- deployment-mode: maintenance
2
+ -- Stop old API/control/turn workers. Never restart pre-0498 approval writers:
3
+ -- their tenancy/request locks precede the organization membership fence.
4
+ SET LOCAL lock_timeout = '5s';
5
+ SET LOCAL statement_timeout = '5min';
6
+ DO $drain$
7
+ DECLARE roles jsonb := nullif(current_setting('opengeni.migration_application_roles', true), '')::jsonb;
8
+ BEGIN
9
+ IF roles IS NULL OR jsonb_typeof(roles) <> 'array' OR jsonb_array_length(roles) NOT BETWEEN 1 AND 16
10
+ OR EXISTS (SELECT 1 FROM jsonb_array_elements(roles) item WHERE jsonb_typeof(item) <> 'string'
11
+ OR btrim(item #>> '{}') = '' OR item #>> '{}' <> btrim(item #>> '{}') OR octet_length(item #>> '{}') > 63)
12
+ OR EXISTS (SELECT 1 FROM pg_stat_activity a JOIN jsonb_array_elements_text(roles) r ON r = a.usename
13
+ WHERE a.datname = current_database() AND a.pid <> pg_backend_pid()) THEN
14
+ RAISE EXCEPTION 'enrollment membership migration requires stopped application roles' USING ERRCODE = '55000';
15
+ END IF;
16
+ END $drain$;
17
+
18
+ -- Only the organization read is RLS-blinded. workspace_memberships is non-RLS:
19
+ -- keep its FOR KEY SHARE to exclude raw runtime DELETE, not just lifecycle SQL.
20
+ -- No policy or privilege is widened. Preserve the exact existing function's
21
+ -- owner, signature, search_path, capability cleanup, and downstream writes.
22
+ -- The backfill checker also sees SQL inside transformation strings. Use the
23
+ -- existing owner-only transformation window (0343); application RLS is not
24
+ -- relaxed and FORCE is restored within this migration's one transaction.
25
+ ALTER TABLE organization_memberships NO FORCE ROW LEVEL SECURITY;
26
+ DO $repair$
27
+ DECLARE
28
+ target regprocedure;
29
+ definition text;
30
+ old_fragment text := $old$ ELSIF p_scope = 'user' THEN
31
+ SELECT membership.* INTO STRICT owner_membership
32
+ FROM organization_memberships membership
33
+ WHERE membership.account_id = p_account_id AND membership.subject_id = caller_subject
34
+ AND membership.status = 'active' AND membership.revoked_at IS NULL FOR SHARE;$old$;
35
+ new_fragment text := $new$ ELSIF p_scope = 'user' THEN
36
+ -- Direct/nested callers may already hold tenancy or request locks. Never
37
+ -- wait for the organization fence here and create a reverse-order cycle.
38
+ -- The public approval wrapper acquires this fence before entering RLS scope.
39
+ IF NOT pg_catalog.pg_try_advisory_xact_lock(pg_catalog.hashtextextended(
40
+ 'organization-membership:' || p_account_id::text, 0
41
+ )) THEN
42
+ RAISE EXCEPTION 'enrollment membership is changing; retry the transaction'
43
+ USING ERRCODE = '55P03';
44
+ END IF;
45
+ SELECT membership.* INTO STRICT owner_membership
46
+ FROM organization_memberships membership
47
+ WHERE membership.account_id = p_account_id AND membership.subject_id = caller_subject
48
+ AND membership.status = 'active' AND membership.revoked_at IS NULL;$new$;
49
+ BEGIN
50
+ target := pg_catalog.to_regprocedure(pg_catalog.format(
51
+ '%I.finalize_scoped_enrollment(uuid,uuid,text,text,boolean,boolean,text,text,text,boolean)',
52
+ pg_catalog.current_schema()
53
+ ));
54
+ definition := pg_catalog.pg_get_functiondef(target);
55
+ IF target IS NULL OR (length(definition) - length(replace(definition, old_fragment, '')))
56
+ / length(old_fragment) <> 1 THEN
57
+ RAISE EXCEPTION '0498 enrollment membership definition drift' USING ERRCODE = '55000';
58
+ END IF;
59
+ EXECUTE replace(definition, old_fragment, new_fragment);
60
+
61
+ -- The command already has the 0345 organization/tenancy prefix. A direct
62
+ -- prepare-then-command caller must acquire that fence BEFORE preparation
63
+ -- takes downstream row locks too. Unknown nested callers fail closed.
64
+ target := pg_catalog.to_regprocedure(pg_catalog.format(
65
+ '%I.prepare_workspace_membership_removal_settlements(jsonb)',
66
+ pg_catalog.current_schema()
67
+ ));
68
+ definition := pg_catalog.pg_get_functiondef(target);
69
+ old_fragment := $old_prepare$ PERFORM 1 FROM workspaces workspace
70
+ WHERE workspace.account_id = account_id_value AND workspace.id = workspace_id_value;
71
+ IF NOT FOUND THEN$old_prepare$;
72
+ new_fragment := $new_prepare$ IF NOT pg_catalog.pg_try_advisory_xact_lock(pg_catalog.hashtextextended(
73
+ 'organization-membership:' || account_id_value::text, 0
74
+ )) THEN
75
+ RAISE EXCEPTION 'workspace membership is changing; retry the transaction'
76
+ USING ERRCODE = '55P03';
77
+ END IF;
78
+ PERFORM 1 FROM workspaces workspace
79
+ WHERE workspace.account_id = account_id_value AND workspace.id = workspace_id_value;
80
+ IF NOT FOUND THEN$new_prepare$;
81
+ IF target IS NULL OR (length(definition) - length(replace(definition, old_fragment, '')))
82
+ / length(old_fragment) <> 1 THEN
83
+ RAISE EXCEPTION '0498 workspace membership preparation definition drift' USING ERRCODE = '55000';
84
+ END IF;
85
+ EXECUTE replace(definition, old_fragment, new_fragment);
86
+ END $repair$;
87
+ ALTER TABLE organization_memberships FORCE ROW LEVEL SECURITY;