@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,334 @@
1
+ -- deployment-mode: maintenance
2
+ -- Stop old API/control/turn workers; old binaries do not enforce exact accounts.
3
+ SET LOCAL lock_timeout = '5s';
4
+ SET LOCAL statement_timeout = '5min';
5
+ DO $drain$
6
+ DECLARE roles jsonb := nullif(current_setting('opengeni.migration_application_roles', true), '')::jsonb;
7
+ BEGIN
8
+ IF roles IS NULL OR jsonb_typeof(roles) <> 'array' OR jsonb_array_length(roles) NOT BETWEEN 1 AND 16
9
+ OR EXISTS (SELECT 1 FROM jsonb_array_elements(roles) item WHERE jsonb_typeof(item) <> 'string'
10
+ OR btrim(item #>> '{}') = '' OR item #>> '{}' <> btrim(item #>> '{}') OR octet_length(item #>> '{}') > 63)
11
+ OR EXISTS (SELECT 1 FROM pg_stat_activity a JOIN jsonb_array_elements_text(roles) r ON r = a.usename
12
+ WHERE a.datname = current_database() AND a.pid <> pg_backend_pid()) THEN
13
+ RAISE EXCEPTION 'exact MCP accounts migration requires stopped application roles' USING ERRCODE = '55000';
14
+ END IF;
15
+ END $drain$;
16
+
17
+ -- SQL NULL is a historical/unbound receipt, never equivalent to an explicit
18
+ -- empty accepted selection. [] denies every connection route at use time.
19
+ ALTER TABLE sessions ADD COLUMN initial_mcp_account_bindings jsonb DEFAULT NULL
20
+ CHECK (initial_mcp_account_bindings IS NULL OR jsonb_typeof(initial_mcp_account_bindings) = 'array');
21
+ ALTER TABLE session_turns ADD COLUMN mcp_account_bindings jsonb DEFAULT NULL
22
+ CHECK (mcp_account_bindings IS NULL OR jsonb_typeof(mcp_account_bindings) = 'array');
23
+ ALTER TABLE session_system_updates ADD COLUMN mcp_account_bindings jsonb DEFAULT NULL
24
+ CHECK (mcp_account_bindings IS NULL OR jsonb_typeof(mcp_account_bindings) = 'array');
25
+ ALTER TABLE session_system_update_outbox ADD COLUMN mcp_account_bindings jsonb DEFAULT NULL
26
+ CHECK (mcp_account_bindings IS NULL OR jsonb_typeof(mcp_account_bindings) = 'array');
27
+
28
+ -- This helper validates evidence, not authority. Personal authority continues
29
+ -- through the sender snapshot capture and live 0478 membership/resource fences.
30
+ CREATE FUNCTION opengeni_private.validate_mcp_account_bindings(bindings jsonb, delegations jsonb)
31
+ RETURNS void LANGUAGE plpgsql SET search_path = pg_catalog AS $body$
32
+ DECLARE item jsonb; ref jsonb; key text; seen text[] := ARRAY[]::text[];
33
+ pairs jsonb := '[]'::jsonb; pair jsonb;
34
+ BEGIN
35
+ IF bindings IS NULL THEN RETURN; END IF;
36
+ IF jsonb_typeof(bindings) IS DISTINCT FROM 'array' OR jsonb_array_length(bindings) > 128 THEN
37
+ RAISE EXCEPTION 'invalid MCP account bindings' USING ERRCODE = '22023';
38
+ END IF;
39
+ FOR item IN SELECT value FROM jsonb_array_elements(bindings) LOOP
40
+ IF jsonb_typeof(item) IS DISTINCT FROM 'object' THEN
41
+ RAISE EXCEPTION 'invalid MCP account binding' USING ERRCODE = '22023';
42
+ END IF;
43
+ FOREACH key IN ARRAY ARRAY['serverId','canonicalServerId','connectionId','originWorkspaceId',
44
+ 'subjectScope','accountLabel','providerDomain','kind'] LOOP
45
+ IF jsonb_typeof(item -> key) IS DISTINCT FROM 'string' OR nullif(btrim(item ->> key),'') IS NULL THEN
46
+ RAISE EXCEPTION 'invalid MCP account binding field %', key USING ERRCODE = '22023';
47
+ END IF;
48
+ END LOOP;
49
+ PERFORM (item ->> 'connectionId')::uuid, (item ->> 'originWorkspaceId')::uuid;
50
+ ref := item -> 'connectionRef';
51
+ pair := jsonb_build_array(item ->> 'canonicalServerId',item ->> 'connectionId');
52
+ IF length(item ->> 'serverId') > 256 OR length(item ->> 'canonicalServerId') > 256
53
+ OR length(item ->> 'accountLabel') > 512 OR length(item ->> 'providerDomain') > 2048
54
+ OR item ->> 'serverId' = ANY(seen)
55
+ OR pairs @> jsonb_build_array(pair)
56
+ OR item - ARRAY['serverId','canonicalServerId','connectionId','originWorkspaceId',
57
+ 'subjectScope','ownerSubjectId','accountLabel','providerDomain','kind','connectionRef',
58
+ 'connectionAuthorityGeneration'] <> '{}'::jsonb
59
+ OR item ->> 'subjectScope' NOT IN ('workspace','subject')
60
+ OR item ->> 'kind' NOT IN ('oauth2','api_key','app_install','delegated')
61
+ OR NOT item ? 'ownerSubjectId'
62
+ OR (item ->> 'subjectScope' = 'workspace' AND item -> 'ownerSubjectId' IS DISTINCT FROM 'null'::jsonb)
63
+ OR (item ->> 'subjectScope' = 'subject' AND (jsonb_typeof(item -> 'ownerSubjectId') IS DISTINCT FROM 'string'
64
+ OR length(btrim(item ->> 'ownerSubjectId')) NOT BETWEEN 1 AND 512))
65
+ OR jsonb_typeof(ref) IS DISTINCT FROM 'object'
66
+ OR ref ? 'authoritySource' OR ref ? 'hostBinding'
67
+ OR ref - ARRAY['connectionId','provider','providerDomain','kind','scopes','resource',
68
+ 'selectedResources','subjectScope'] <> '{}'::jsonb
69
+ OR ref ->> 'connectionId' IS DISTINCT FROM item ->> 'connectionId'
70
+ OR ref ->> 'subjectScope' IS DISTINCT FROM item ->> 'subjectScope'
71
+ OR ref ->> 'providerDomain' IS DISTINCT FROM item ->> 'providerDomain'
72
+ OR ref ->> 'kind' IS DISTINCT FROM item ->> 'kind'
73
+ OR (item ? 'connectionAuthorityGeneration' AND (
74
+ jsonb_typeof(item -> 'connectionAuthorityGeneration') IS DISTINCT FROM 'number'
75
+ OR (item ->> 'connectionAuthorityGeneration') !~ '^[1-9][0-9]*$'))
76
+ THEN RAISE EXCEPTION 'invalid MCP account binding identity' USING ERRCODE = '22023'; END IF;
77
+ IF item ->> 'subjectScope' = 'subject' AND NOT EXISTS (
78
+ SELECT 1 FROM jsonb_array_elements(coalesce(delegations,'[]'::jsonb)) d
79
+ WHERE d ->> 'serverId' = item ->> 'serverId'
80
+ AND d ->> 'connectionId' = item ->> 'connectionId'
81
+ AND d ->> 'originWorkspaceId' = item ->> 'originWorkspaceId'
82
+ AND d ->> 'ownerSubjectId' = item ->> 'ownerSubjectId'
83
+ AND d ->> 'providerDomain' = item ->> 'providerDomain'
84
+ AND d ->> 'kind' = item ->> 'kind'
85
+ AND d ->> 'connectionType' = 'mcp'
86
+ ) THEN RAISE EXCEPTION 'MCP account binding requires exact sender delegation' USING ERRCODE = '42501'; END IF;
87
+ seen := array_append(seen,item ->> 'serverId');
88
+ pairs := pairs || jsonb_build_array(pair);
89
+ END LOOP;
90
+ END $body$;
91
+
92
+ CREATE FUNCTION opengeni_private.fence_mcp_account_bindings()
93
+ RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT AS $body$
94
+ DECLARE document jsonb := to_jsonb(NEW); old_document jsonb; bindings jsonb; old_bindings jsonb;
95
+ delegations jsonb; item jsonb; connection_row record; column_name text; run_bindings jsonb;
96
+ visibility_cleanup boolean := false;
97
+ inherited_bindings boolean := false;
98
+ BEGIN
99
+ column_name := CASE WHEN TG_TABLE_NAME = 'sessions' THEN 'initial_mcp_account_bindings'
100
+ ELSE 'mcp_account_bindings' END;
101
+ IF TG_TABLE_NAME = 'scheduled_task_runs' THEN
102
+ bindings := nullif(document #> '{accepted_execution_snapshot,mcpAccountBindings}','null'::jsonb);
103
+ delegations := document #> '{accepted_execution_snapshot,personalConnectionDelegations}';
104
+ ELSE
105
+ bindings := nullif(document -> column_name,'null'::jsonb);
106
+ delegations := document -> CASE WHEN TG_TABLE_NAME = 'sessions' THEN 'initial_personal_connection_delegations'
107
+ ELSE 'personal_connection_delegations' END;
108
+ END IF;
109
+ IF TG_OP = 'UPDATE' THEN
110
+ old_document := to_jsonb(OLD);
111
+ old_bindings := CASE WHEN TG_TABLE_NAME = 'scheduled_task_runs'
112
+ THEN nullif(old_document #> '{accepted_execution_snapshot,mcpAccountBindings}','null'::jsonb)
113
+ ELSE nullif(old_document -> column_name,'null'::jsonb) END;
114
+ IF TG_TABLE_NAME = 'sessions' AND document ->> 'visibility' IS DISTINCT FROM old_document ->> 'visibility' THEN
115
+ -- Match 0481's native visibility cleanup capability. A caller-set GUC
116
+ -- alone cannot clear a receipt or re-enable legacy ambient fallback.
117
+ IF (document ->> 'authority_epoch')::integer IS DISTINCT FROM (old_document ->> 'authority_epoch')::integer + 1
118
+ OR document -> 'owner_subject_id' IS DISTINCT FROM old_document -> 'owner_subject_id'
119
+ OR document -> 'owner_organization_membership_id' IS DISTINCT FROM old_document -> 'owner_organization_membership_id'
120
+ OR document -> 'parent_turn_id' IS DISTINCT FROM old_document -> 'parent_turn_id'
121
+ OR (bindings IS DISTINCT FROM old_bindings AND bindings IS DISTINCT FROM '[]'::jsonb)
122
+ THEN RAISE EXCEPTION 'invalid MCP account visibility cleanup' USING ERRCODE = '42501'; END IF;
123
+ IF NOT EXISTS (
124
+ SELECT 1 FROM session_visibility_write_capabilities capability
125
+ WHERE capability.backend_pid = pg_backend_pid()
126
+ AND capability.transaction_id = pg_current_xact_id()
127
+ AND capability.capability_id = nullif(current_setting('opengeni.session_visibility_write_capability',true),'')::uuid
128
+ ) THEN RAISE EXCEPTION 'MCP account cleanup requires the visibility lifecycle capability' USING ERRCODE = '42501'; END IF;
129
+ bindings := '[]'::jsonb;
130
+ NEW := jsonb_populate_record(NEW,jsonb_build_object(column_name,bindings));
131
+ visibility_cleanup := true;
132
+ END IF;
133
+ IF bindings IS DISTINCT FROM old_bindings AND NOT visibility_cleanup THEN
134
+ RAISE EXCEPTION 'accepted MCP account bindings are immutable' USING ERRCODE = '42501';
135
+ END IF;
136
+ END IF;
137
+ PERFORM opengeni_private.validate_mcp_account_bindings(bindings,delegations);
138
+ -- Only new execution acceptance validates live workspace identity. Updates
139
+ -- and outbox rows carry already accepted causal evidence: revocation must
140
+ -- not strand terminal settlement or delivery. Shape/immutability still apply
141
+ -- to those carriers above; execution revalidates live authority below.
142
+ IF TG_OP = 'INSERT' AND TG_TABLE_NAME IN ('sessions','session_turns','scheduled_task_runs') THEN
143
+ IF TG_TABLE_NAME = 'session_turns' AND document ->> 'scheduled_task_run_id' IS NOT NULL THEN
144
+ SELECT nullif(r.accepted_execution_snapshot -> 'mcpAccountBindings','null'::jsonb)
145
+ INTO run_bindings FROM scheduled_task_runs r
146
+ WHERE r.id = (document ->> 'scheduled_task_run_id')::uuid
147
+ AND r.account_id = NEW.account_id AND r.workspace_id = NEW.workspace_id FOR SHARE;
148
+ IF NOT FOUND OR bindings IS DISTINCT FROM run_bindings THEN
149
+ RAISE EXCEPTION 'scheduled turn differs from accepted MCP account bindings' USING ERRCODE = '42501';
150
+ END IF;
151
+ END IF;
152
+ IF TG_TABLE_NAME = 'session_turns'
153
+ AND document ->> 'source' IN ('system','goal')
154
+ AND document ->> 'status' = 'running'
155
+ THEN
156
+ -- Claim marks the exact delivered batch before inserting its turn (the
157
+ -- delivery FK is deferred). A copied receipt is not new account admission.
158
+ -- Require both the delivered carrier and its immutable causal turn; a
159
+ -- caller-supplied lineage, actor, or matching account alone is insufficient.
160
+ SELECT EXISTS (
161
+ SELECT 1 FROM session_system_updates update_value
162
+ JOIN session_turns origin ON origin.account_id = NEW.account_id
163
+ AND origin.workspace_id = NEW.workspace_id
164
+ AND origin.id::text = CASE
165
+ WHEN update_value.kind IN ('agent_message','agent_steer_instruction')
166
+ THEN update_value.lineage ->> 'callerTurnId'
167
+ WHEN update_value.kind LIKE 'child_%'
168
+ THEN update_value.lineage ->> 'parentTurnId'
169
+ ELSE update_value.lineage ->> 'causalTurnId' END
170
+ AND origin.session_id::text = CASE
171
+ WHEN update_value.kind IN ('agent_message','agent_steer_instruction')
172
+ THEN update_value.lineage ->> 'callerSessionId'
173
+ ELSE document ->> 'session_id' END
174
+ WHERE update_value.account_id = NEW.account_id
175
+ AND update_value.workspace_id = NEW.workspace_id
176
+ AND update_value.session_id = NEW.session_id
177
+ AND update_value.state = 'delivered'
178
+ AND update_value.delivered_turn_id = NEW.id
179
+ AND update_value.mcp_account_bindings IS NOT DISTINCT FROM bindings
180
+ AND origin.mcp_account_bindings IS NOT DISTINCT FROM bindings
181
+ AND (coalesce(origin.initiating_human_subject_id,
182
+ CASE WHEN origin.initiator_kind = 'subject' THEN origin.initiator_subject_id END)
183
+ IS NOT DISTINCT FROM coalesce(NEW.initiating_human_subject_id,
184
+ CASE WHEN NEW.initiator_kind = 'subject' THEN NEW.initiator_subject_id END)
185
+ OR (
186
+ -- Ordinary workspace-only agent messages execute as a service,
187
+ -- not as the origin's human. Preserve that absence of personal
188
+ -- authority without stranding delivery of the exact receipt.
189
+ update_value.kind = 'agent_message'
190
+ AND NEW.initiator_kind = 'service'
191
+ AND NEW.initiating_human_subject_id IS NULL
192
+ AND origin.personal_connection_delegations = '[]'::jsonb
193
+ AND update_value.personal_connection_delegations = '[]'::jsonb
194
+ AND delegations = '[]'::jsonb
195
+ AND NOT EXISTS (SELECT 1 FROM jsonb_array_elements(bindings) selected
196
+ WHERE selected ->> 'subjectScope' IS DISTINCT FROM 'workspace')
197
+ ))
198
+ ) AND NOT EXISTS (
199
+ SELECT 1 FROM session_system_updates update_value
200
+ WHERE update_value.account_id = NEW.account_id
201
+ AND update_value.workspace_id = NEW.workspace_id
202
+ AND update_value.session_id = NEW.session_id
203
+ AND update_value.state = 'delivered'
204
+ AND update_value.delivered_turn_id = NEW.id
205
+ AND update_value.mcp_account_bindings IS DISTINCT FROM bindings
206
+ ) INTO inherited_bindings;
207
+ END IF;
208
+ FOR item IN SELECT value FROM jsonb_array_elements(bindings) LOOP
209
+ IF item ->> 'subjectScope' = 'workspace' AND NOT inherited_bindings THEN
210
+ SELECT c.* INTO connection_row FROM opengeni_private.read_sender_connection(
211
+ NEW.account_id, NEW.workspace_id, (item ->> 'connectionId')::uuid, NULL) c;
212
+ IF NOT FOUND THEN
213
+ RAISE EXCEPTION 'MCP workspace account missing' USING ERRCODE = '42501';
214
+ END IF;
215
+ IF item ->> 'originWorkspaceId' IS DISTINCT FROM NEW.workspace_id::text
216
+ OR connection_row.authority_scope IS DISTINCT FROM 'workspace'
217
+ OR connection_row.workspace_id IS DISTINCT FROM NEW.workspace_id
218
+ OR connection_row.origin_workspace_id IS DISTINCT FROM NEW.workspace_id
219
+ OR connection_row.status IS DISTINCT FROM 'active'
220
+ OR lower(connection_row.provider_domain) IS DISTINCT FROM lower(item ->> 'providerDomain')
221
+ OR connection_row.kind IS DISTINCT FROM item ->> 'kind'
222
+ OR (item ->> 'connectionAuthorityGeneration')::bigint IS DISTINCT FROM connection_row.authority_generation
223
+ THEN RAISE EXCEPTION 'MCP workspace account identity changed' USING ERRCODE = '42501'; END IF;
224
+ END IF;
225
+ END LOOP;
226
+ END IF;
227
+ RETURN NEW;
228
+ END $body$;
229
+
230
+ CREATE TRIGGER mcp_account_bindings_fence BEFORE INSERT OR UPDATE ON sessions
231
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.fence_mcp_account_bindings();
232
+ CREATE TRIGGER mcp_account_bindings_fence BEFORE INSERT OR UPDATE ON session_turns
233
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.fence_mcp_account_bindings();
234
+ CREATE TRIGGER mcp_account_bindings_fence BEFORE INSERT OR UPDATE ON session_system_updates
235
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.fence_mcp_account_bindings();
236
+ CREATE TRIGGER mcp_account_bindings_fence BEFORE INSERT OR UPDATE ON session_system_update_outbox
237
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.fence_mcp_account_bindings();
238
+ CREATE TRIGGER mcp_account_bindings_fence BEFORE INSERT OR UPDATE ON scheduled_task_runs
239
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.fence_mcp_account_bindings();
240
+
241
+ DO $binding_path$
242
+ BEGIN
243
+ EXECUTE format('ALTER FUNCTION opengeni_private.fence_mcp_account_bindings() SET search_path = pg_catalog, %I, pg_temp', current_schema());
244
+ END $binding_path$;
245
+
246
+ -- Patch only a checked insertion point: preserve all sender, lock, replay,
247
+ -- attribution and revocation behavior in the installed resolver.
248
+ DO $resolver$
249
+ DECLARE definition text; anchor text := E' IF reason IS NULL THEN\n SELECT authority_snapshot.* INTO snapshot';
250
+ insertion text;
251
+ BEGIN
252
+ insertion := $gate$
253
+ -- 0494 exact accepted account gate, inside the canonical lifecycle locks.
254
+ IF reason IS NULL THEN
255
+ SELECT value INTO exact_binding FROM jsonb_array_elements(turn_row.mcp_account_bindings)
256
+ WHERE value ->> 'serverId' = p_server_id;
257
+ IF FOUND THEN
258
+ IF p_connection_id IS DISTINCT FROM (exact_binding ->> 'connectionId')::uuid
259
+ OR p_subject_scope IS DISTINCT FROM exact_binding ->> 'subjectScope'
260
+ OR p_owner_subject_id IS DISTINCT FROM exact_binding ->> 'ownerSubjectId'
261
+ OR lower(p_provider_domain) IS DISTINCT FROM lower(exact_binding ->> 'providerDomain')
262
+ OR p_connection_kind IS DISTINCT FROM exact_binding ->> 'kind'
263
+ THEN reason := 'accepted_account_binding_changed';
264
+ ELSE
265
+ SELECT c.* INTO connection_row FROM opengeni_private.read_sender_connection(
266
+ p_account_id, (exact_binding ->> 'originWorkspaceId')::uuid, p_connection_id,
267
+ exact_binding ->> 'ownerSubjectId') c;
268
+ IF NOT FOUND THEN reason := 'connection_missing';
269
+ ELSIF connection_row.status IS DISTINCT FROM 'active' THEN reason := 'connection_status_inactive';
270
+ ELSIF exact_binding ? 'connectionAuthorityGeneration' AND connection_row.authority_generation
271
+ IS DISTINCT FROM (exact_binding ->> 'connectionAuthorityGeneration')::bigint
272
+ THEN reason := 'connection_generation_changed';
273
+ ELSIF p_subject_scope = 'workspace' AND (
274
+ NOT exact_binding ? 'connectionAuthorityGeneration'
275
+ OR exact_binding ->> 'originWorkspaceId' IS DISTINCT FROM p_workspace_id::text)
276
+ THEN reason := 'accepted_account_binding_changed';
277
+ END IF;
278
+ END IF;
279
+ ELSIF p_server_id = 'github:personal' AND p_subject_scope = 'subject'
280
+ AND lower(p_provider_domain) = 'github.com' AND p_connection_kind = 'oauth2'
281
+ AND EXISTS (
282
+ SELECT 1 FROM turn_connection_authority_snapshots specialized
283
+ WHERE specialized.account_id = p_account_id AND specialized.workspace_id = p_workspace_id
284
+ AND specialized.session_id = p_session_id AND specialized.turn_id = p_turn_id
285
+ AND specialized.server_id = p_server_id AND specialized.connection_id = p_connection_id
286
+ AND specialized.authority_source = 'sender' AND specialized.authority_scope = 'user'
287
+ AND specialized.owner_subject_id = p_owner_subject_id
288
+ ) AND EXISTS (
289
+ SELECT 1 FROM jsonb_array_elements(turn_row.personal_connection_delegations) selected
290
+ WHERE selected ->> 'connectionType' = 'github_personal'
291
+ AND selected ->> 'serverId' = p_server_id
292
+ AND selected ->> 'connectionId' = p_connection_id::text
293
+ AND selected ->> 'ownerSubjectId' = p_owner_subject_id
294
+ ) THEN
295
+ -- Git broker and GitHub REST use a separate repository-selection receipt,
296
+ -- not a generic MCP alias. Continue into the existing snapshot-only lane
297
+ -- below, including its digest, sender, membership and revocation checks.
298
+ NULL;
299
+ ELSIF turn_row.mcp_account_bindings IS NOT NULL OR p_server_id ~ '^account-[0-9a-f]{64}$' THEN
300
+ reason := 'accepted_account_binding_required';
301
+ END IF;
302
+ END IF;
303
+ IF reason IS NULL THEN
304
+ SELECT authority_snapshot.* INTO snapshot$gate$;
305
+ definition := pg_get_functiondef('resolve_accepted_connection_use(uuid,uuid,uuid,uuid,uuid,integer,uuid,text,text,uuid,text,text,text,text)'::regprocedure);
306
+ IF (length(definition)-length(replace(definition,anchor,'')))/length(anchor) <> 1
307
+ OR strpos(definition,' scheduled_run_id uuid;') = 0 THEN
308
+ RAISE EXCEPTION '0494 connection resolver prerequisite drift' USING ERRCODE = '55000';
309
+ END IF;
310
+ definition := replace(definition,' scheduled_run_id uuid;',E' scheduled_run_id uuid;\n exact_binding jsonb;');
311
+ EXECUTE replace(definition,anchor,insertion);
312
+ END $resolver$;
313
+
314
+ -- Changing RETURNS TABLE requires recreation. Preserve the installed body and
315
+ -- its hardened search_path; role provisioning grants only the release role.
316
+ DO $outbox$
317
+ DECLARE definition text;
318
+ BEGIN
319
+ definition := pg_get_functiondef('opengeni_private.claim_session_system_update_outbox(integer)'::regprocedure);
320
+ IF strpos(definition,'personal_connection_delegations jsonb') = 0
321
+ OR strpos(definition,'o.personal_connection_delegations,') = 0 THEN
322
+ RAISE EXCEPTION '0494 outbox claim prerequisite drift' USING ERRCODE = '55000';
323
+ END IF;
324
+ definition := replace(definition,'personal_connection_delegations jsonb',
325
+ 'mcp_account_bindings jsonb, personal_connection_delegations jsonb');
326
+ definition := replace(definition,'o.personal_connection_delegations,',
327
+ 'o.mcp_account_bindings, o.personal_connection_delegations,');
328
+ DROP FUNCTION opengeni_private.claim_session_system_update_outbox(integer);
329
+ EXECUTE definition;
330
+ END $outbox$;
331
+
332
+ REVOKE ALL ON FUNCTION opengeni_private.validate_mcp_account_bindings(jsonb,jsonb) FROM PUBLIC;
333
+ REVOKE ALL ON FUNCTION opengeni_private.fence_mcp_account_bindings() FROM PUBLIC;
334
+ REVOKE ALL ON FUNCTION opengeni_private.claim_session_system_update_outbox(integer) FROM PUBLIC;
@@ -0,0 +1,220 @@
1
+ -- deployment-mode: rolling
2
+ -- Consent stays DB-disabled until an operator verifies compatible immutable
3
+ -- API/control/turn images and templates. The permanent claim guard rejects
4
+ -- returning old workers for affected sessions even after consent is disabled.
5
+ SET LOCAL lock_timeout = '5s';
6
+ SET LOCAL statement_timeout = '5min';
7
+
8
+ DO $roles$
9
+ DECLARE roles jsonb := nullif(current_setting('opengeni.migration_application_roles', true), '')::jsonb;
10
+ BEGIN
11
+ IF roles IS NULL OR jsonb_typeof(roles) <> 'array' THEN
12
+ RAISE EXCEPTION '0495 requires application database roles' USING ERRCODE = '55000';
13
+ END IF;
14
+ IF jsonb_array_length(roles) NOT BETWEEN 1 AND 16 OR EXISTS (
15
+ SELECT 1 FROM jsonb_array_elements(roles) item WHERE jsonb_typeof(item) <> 'string'
16
+ OR btrim(item #>> '{}') = '' OR item #>> '{}' <> btrim(item #>> '{}')
17
+ OR octet_length(item #>> '{}') > 63
18
+ ) THEN RAISE EXCEPTION '0495 invalid application roles' USING ERRCODE = '55000'; END IF;
19
+ END
20
+ $roles$;
21
+
22
+ -- Operator-owned release activation, never an API/UI or environment flag.
23
+ -- Runtime roles may inspect this one non-secret row but cannot activate it.
24
+ CREATE TABLE opengeni_private.sandbox_recovery_rollout (
25
+ singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
26
+ consent_enabled boolean NOT NULL DEFAULT false,
27
+ release_evidence text,
28
+ CHECK (NOT consent_enabled OR nullif(btrim(release_evidence), '') IS NOT NULL)
29
+ );
30
+ INSERT INTO opengeni_private.sandbox_recovery_rollout (singleton) VALUES (true);
31
+ REVOKE ALL ON opengeni_private.sandbox_recovery_rollout FROM PUBLIC;
32
+ DO $activation_acl$
33
+ DECLARE grantee_name text;
34
+ BEGIN
35
+ FOR grantee_name IN
36
+ SELECT DISTINCT role.rolname FROM pg_class relation
37
+ CROSS JOIN LATERAL aclexplode(coalesce(relation.relacl, acldefault('r', relation.relowner))) acl
38
+ JOIN pg_roles role ON role.oid = acl.grantee
39
+ WHERE relation.oid = 'opengeni_private.sandbox_recovery_rollout'::regclass
40
+ AND acl.grantee <> relation.relowner
41
+ LOOP
42
+ EXECUTE format('REVOKE ALL ON opengeni_private.sandbox_recovery_rollout FROM %I', grantee_name);
43
+ END LOOP;
44
+ END
45
+ $activation_acl$;
46
+ DO $read_grants$
47
+ DECLARE runtime_role text;
48
+ BEGIN
49
+ FOR runtime_role IN SELECT jsonb_array_elements_text(
50
+ current_setting('opengeni.migration_application_roles')::jsonb)
51
+ LOOP
52
+ -- Fresh installation migrates before provisioning runtime roles. Existing
53
+ -- rolling roles retain read access now; provisionRoles converges late roles.
54
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = runtime_role) THEN
55
+ EXECUTE format('GRANT SELECT ON opengeni_private.sandbox_recovery_rollout TO %I', runtime_role);
56
+ END IF;
57
+ END LOOP;
58
+ END
59
+ $read_grants$;
60
+
61
+ ALTER TABLE sandbox_leases ADD COLUMN public_recovery jsonb;
62
+ ALTER TABLE sandbox_leases ADD CONSTRAINT sandbox_public_recovery_shape CHECK (
63
+ public_recovery IS NULL OR coalesce((
64
+ jsonb_typeof(public_recovery) = 'object'
65
+ AND public_recovery->>'version' = '1'
66
+ AND public_recovery->>'status' IN ('accepted', 'verified', 'failed')
67
+ AND public_recovery->>'sessionId' IS NOT NULL
68
+ AND public_recovery->>'operationId' IS NOT NULL
69
+ AND public_recovery->>'subjectId' IS NOT NULL
70
+ AND jsonb_typeof(public_recovery->'selection') = 'object'
71
+ ), false)
72
+ );
73
+
74
+ -- All session writers already hold the 0345 workspace tenancy fence. Consent
75
+ -- takes it exclusively BEFORE reading all group members. Afterwards this guard
76
+ -- prevents an attachment/route change through the entire provider restore.
77
+ CREATE FUNCTION guard_public_sandbox_recovery_session() RETURNS trigger
78
+ LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
79
+ BEGIN
80
+ IF TG_OP = 'UPDATE' AND NEW.sandbox_group_id IS NOT DISTINCT FROM OLD.sandbox_group_id
81
+ AND NEW.active_sandbox_id IS NOT DISTINCT FROM OLD.active_sandbox_id
82
+ AND NEW.active_epoch IS NOT DISTINCT FROM OLD.active_epoch THEN RETURN NEW; END IF;
83
+ PERFORM 1 FROM sandbox_leases lease
84
+ WHERE lease.workspace_id = NEW.workspace_id
85
+ AND (lease.sandbox_group_id = NEW.sandbox_group_id
86
+ OR (TG_OP = 'UPDATE' AND lease.sandbox_group_id = OLD.sandbox_group_id))
87
+ AND lease.public_recovery->>'status' = 'accepted'
88
+ FOR SHARE;
89
+ IF FOUND THEN
90
+ RAISE EXCEPTION 'consented sandbox recovery protects group membership and route'
91
+ USING ERRCODE = '55000';
92
+ END IF;
93
+ RETURN NEW;
94
+ END $$;
95
+ REVOKE ALL ON FUNCTION guard_public_sandbox_recovery_session() FROM PUBLIC;
96
+ CREATE TRIGGER public_sandbox_recovery_session_guard
97
+ BEFORE INSERT OR UPDATE OF sandbox_group_id, active_sandbox_id, active_epoch
98
+ ON sessions FOR EACH ROW EXECUTE FUNCTION guard_public_sandbox_recovery_session();
99
+
100
+ -- The selected CURRENT reference is also the existing GC pin. A late capture
101
+ -- cannot replace it while restoration owns this exact choice. Failed creation
102
+ -- keeps provenance but ends this attempt; late callbacks still lose epoch CAS.
103
+ CREATE FUNCTION guard_public_sandbox_recovery_lease() RETURNS trigger
104
+ LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
105
+ BEGIN
106
+ IF NEW.public_recovery->>'status' = 'accepted'
107
+ AND (TG_OP = 'INSERT' OR OLD.public_recovery->>'status' IS DISTINCT FROM 'accepted'
108
+ OR NEW.public_recovery->>'operationId' IS DISTINCT FROM OLD.public_recovery->>'operationId')
109
+ AND NOT coalesce((SELECT consent_enabled FROM opengeni_private.sandbox_recovery_rollout WHERE singleton), false) THEN
110
+ RAISE EXCEPTION 'public checkpoint consent is not activated' USING ERRCODE = '55000';
111
+ END IF;
112
+ IF TG_OP = 'UPDATE' AND OLD.public_recovery->>'status' = 'accepted' THEN
113
+ IF NEW.current_checkpoint_artifact_id IS DISTINCT FROM OLD.current_checkpoint_artifact_id
114
+ OR NEW.workspace_generation IS DISTINCT FROM OLD.workspace_generation
115
+ OR NEW.archive_generation IS DISTINCT FROM OLD.archive_generation THEN
116
+ RAISE EXCEPTION 'consented sandbox recovery pins the exact current checkpoint'
117
+ USING ERRCODE = '55000';
118
+ END IF;
119
+ IF OLD.liveness = 'warming' AND NEW.liveness = 'cold'
120
+ AND NEW.public_recovery->>'status' = 'accepted' THEN
121
+ NEW.public_recovery := jsonb_set(OLD.public_recovery, '{status}', '"failed"'::jsonb);
122
+ END IF;
123
+ END IF;
124
+ RETURN NEW;
125
+ END $$;
126
+ REVOKE ALL ON FUNCTION guard_public_sandbox_recovery_lease() FROM PUBLIC;
127
+ CREATE TRIGGER public_sandbox_recovery_lease_guard BEFORE INSERT OR UPDATE ON sandbox_leases
128
+ FOR EACH ROW EXECUTE FUNCTION guard_public_sandbox_recovery_lease();
129
+
130
+ -- Finalized consent is the monotonic affected-session marker, independent of
131
+ -- lease lifetime, provider success/failure, history compaction and activation.
132
+ -- Only the actual FK cascade from deleting its owning session may remove it.
133
+ CREATE FUNCTION guard_sandbox_recovery_consent_receipt() RETURNS trigger
134
+ LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
135
+ DECLARE prior_subject text;
136
+ prior_account text;
137
+ prior_workspace text;
138
+ parent_exists boolean;
139
+ BEGIN
140
+ IF TG_OP <> 'INSERT' AND OLD.action = 'sandbox.recovery.consent' AND OLD.result ? 'operationId' THEN
141
+ IF TG_OP = 'DELETE' THEN
142
+ -- Same tenant scope, neutral actor: an RLS-hidden private session must
143
+ -- never look deleted. Its FK cascade sees the actual parent row gone.
144
+ prior_subject := coalesce(current_setting('opengeni.subject_id', true), '');
145
+ prior_account := coalesce(current_setting('opengeni.account_id', true), '');
146
+ prior_workspace := coalesce(current_setting('opengeni.workspace_id', true), '');
147
+ PERFORM set_config('opengeni.subject_id', '', true);
148
+ PERFORM set_config('opengeni.account_id', OLD.account_id::text, true);
149
+ PERFORM set_config('opengeni.workspace_id', OLD.workspace_id::text, true);
150
+ SELECT EXISTS (SELECT 1 FROM sessions WHERE id = OLD.target_session_id
151
+ AND account_id = OLD.account_id AND workspace_id = OLD.workspace_id) INTO parent_exists;
152
+ PERFORM set_config('opengeni.subject_id', prior_subject, true);
153
+ PERFORM set_config('opengeni.account_id', prior_account, true);
154
+ PERFORM set_config('opengeni.workspace_id', prior_workspace, true);
155
+ IF parent_exists THEN
156
+ RAISE EXCEPTION 'checkpoint consent warning receipt is permanent for this session' USING ERRCODE = '55000';
157
+ END IF;
158
+ RETURN OLD;
159
+ END IF;
160
+ IF NEW IS DISTINCT FROM OLD THEN
161
+ RAISE EXCEPTION 'checkpoint consent warning receipt is immutable' USING ERRCODE = '55000';
162
+ END IF;
163
+ END IF;
164
+ IF TG_OP <> 'DELETE' AND NEW.action = 'sandbox.recovery.consent'
165
+ AND NEW.result ? 'operationId' AND (TG_OP = 'INSERT'
166
+ OR OLD.action IS DISTINCT FROM 'sandbox.recovery.consent' OR NOT (OLD.result ? 'operationId'))
167
+ AND NOT coalesce((SELECT consent_enabled FROM opengeni_private.sandbox_recovery_rollout WHERE singleton), false) THEN
168
+ RAISE EXCEPTION 'public checkpoint consent is not activated' USING ERRCODE = '55000';
169
+ END IF;
170
+ IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
171
+ RETURN NEW;
172
+ END $$;
173
+ REVOKE ALL ON FUNCTION guard_sandbox_recovery_consent_receipt() FROM PUBLIC;
174
+ CREATE TRIGGER sandbox_recovery_consent_receipt_guard
175
+ BEFORE INSERT OR UPDATE OR DELETE ON session_command_receipts
176
+ FOR EACH ROW EXECUTE FUNCTION guard_sandbox_recovery_consent_receipt();
177
+
178
+ -- Canonical claims always INSERT, including exact-attempt ON CONFLICT replay.
179
+ -- BEFORE INSERT runs before conflict handling, so returning old workers cannot
180
+ -- reattach to a compatible worker's already-admitted attempt without warning.
181
+ CREATE FUNCTION guard_sandbox_recovery_warning_claim() RETURNS trigger
182
+ LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
183
+ DECLARE prior_subject text := coalesce(current_setting('opengeni.subject_id', true), '');
184
+ prior_account text := coalesce(current_setting('opengeni.account_id', true), '');
185
+ prior_workspace text := coalesce(current_setting('opengeni.workspace_id', true), '');
186
+ warning_required boolean;
187
+ BEGIN
188
+ -- Read the authoritative requirement, not an actor-filtered receipt list.
189
+ -- The ordinary attempt RLS/admission guards still authorize the INSERT.
190
+ PERFORM set_config('opengeni.subject_id', '', true);
191
+ PERFORM set_config('opengeni.account_id', NEW.account_id::text, true);
192
+ PERFORM set_config('opengeni.workspace_id', NEW.workspace_id::text, true);
193
+ SELECT EXISTS (SELECT 1 FROM session_command_receipts receipt
194
+ WHERE receipt.account_id = NEW.account_id AND receipt.workspace_id = NEW.workspace_id
195
+ AND receipt.target_session_id = NEW.session_id
196
+ AND receipt.action = 'sandbox.recovery.consent' AND receipt.result ? 'operationId') INTO warning_required;
197
+ PERFORM set_config('opengeni.subject_id', prior_subject, true);
198
+ PERFORM set_config('opengeni.account_id', prior_account, true);
199
+ PERFORM set_config('opengeni.workspace_id', prior_workspace, true);
200
+ IF warning_required AND current_setting('opengeni.filesystem_discontinuity_protocol_v1', true) IS DISTINCT FROM '1' THEN
201
+ RAISE EXCEPTION 'session requires filesystem discontinuity warning protocol v1'
202
+ USING ERRCODE = '55000';
203
+ END IF;
204
+ RETURN NEW;
205
+ END $$;
206
+ REVOKE ALL ON FUNCTION guard_sandbox_recovery_warning_claim() FROM PUBLIC;
207
+ CREATE TRIGGER sandbox_recovery_warning_claim_guard BEFORE INSERT ON session_turn_attempts
208
+ FOR EACH ROW EXECUTE FUNCTION guard_sandbox_recovery_warning_claim();
209
+
210
+ -- Explicit pg_temp last: an application-created temporary table must not
211
+ -- shadow the authoritative lease in an invoker trigger.
212
+ DO $paths$
213
+ DECLARE target_schema text := current_schema();
214
+ BEGIN
215
+ EXECUTE format('ALTER FUNCTION %I.guard_public_sandbox_recovery_session() SET search_path TO pg_catalog, %I, pg_temp', target_schema, target_schema);
216
+ EXECUTE format('ALTER FUNCTION %I.guard_public_sandbox_recovery_lease() SET search_path TO pg_catalog, %I, pg_temp', target_schema, target_schema);
217
+ EXECUTE format('ALTER FUNCTION %I.guard_sandbox_recovery_consent_receipt() SET search_path TO pg_catalog, %I, pg_temp', target_schema, target_schema);
218
+ EXECUTE format('ALTER FUNCTION %I.guard_sandbox_recovery_warning_claim() SET search_path TO pg_catalog, %I, pg_temp', target_schema, target_schema);
219
+ END
220
+ $paths$;