@opengeni/db 1.0.1 → 3.2.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.
- package/dist/attached-browser-devices.d.ts +7 -0
- package/dist/automations.d.ts +154 -0
- package/dist/canonical-human-identities.js +3 -3
- package/dist/capability-integrations.d.ts +13 -0
- package/dist/child-lifecycle-notices.d.ts +68 -0
- package/dist/{chunk-4SR4HKKA.js → chunk-2EH4WRHL.js} +19 -11
- package/dist/chunk-2EH4WRHL.js.map +1 -0
- package/dist/{chunk-CUPM3FGB.js → chunk-2PRS4J5Q.js} +3 -3
- package/dist/chunk-6HMM2R5T.js +390 -0
- package/dist/chunk-6HMM2R5T.js.map +1 -0
- package/dist/{chunk-ZWREYEWA.js → chunk-AZHW7R5V.js} +56 -11
- package/dist/chunk-AZHW7R5V.js.map +1 -0
- package/dist/{chunk-LTG3S6ZY.js → chunk-E6CLMKEE.js} +2 -2
- package/dist/{chunk-6ZYKCRFO.js → chunk-KRLPEMLH.js} +7591 -4417
- package/dist/chunk-KRLPEMLH.js.map +1 -0
- package/dist/chunk-NAB6FKFN.js +516 -0
- package/dist/chunk-NAB6FKFN.js.map +1 -0
- package/dist/{chunk-SK7YYHBI.js → chunk-SIBYBA2D.js} +46 -1
- package/dist/chunk-SIBYBA2D.js.map +1 -0
- package/dist/chunk-UZNXG44K.js +4508 -0
- package/dist/chunk-UZNXG44K.js.map +1 -0
- package/dist/codemode-operations.d.ts +1 -6
- package/dist/company-brain-context-selection-schema.d.ts +705 -0
- package/dist/company-brain-context-selection.d.ts +94 -0
- package/dist/company-brain-governed-writes.d.ts +24 -0
- package/dist/company-brain.d.ts +55 -0
- package/dist/company-profile-agent-admin.d.ts +15 -0
- package/dist/company-profile-schema.d.ts +619 -2
- package/dist/company-profile.d.ts +9 -4
- package/dist/connection-authority.d.ts +95 -0
- package/dist/connection-scopes.d.ts +8 -0
- package/dist/connection-tenancy-backfill.d.ts +85 -0
- package/dist/connection-token-resolver.d.ts +30 -1
- package/dist/database.d.ts +23 -4
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.d.ts +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/governed-learning-activation-schema.d.ts +984 -0
- package/dist/governed-learning-activation.d.ts +98 -0
- package/dist/governed-learning-evaluator-schema.d.ts +633 -0
- package/dist/governed-learning-evaluator.d.ts +28 -0
- package/dist/index.d.ts +1606 -81
- package/dist/index.js +21898 -8147
- package/dist/index.js.map +1 -1
- package/dist/insights.d.ts +18 -0
- package/dist/integration-bindings.d.ts +1 -0
- package/dist/integration-facets.d.ts +30 -0
- package/dist/interaction-placement-loss.d.ts +22 -0
- package/dist/managed-human-provisioning.d.ts +25 -0
- package/dist/memory-domain.d.ts +3 -3
- package/dist/migrate.d.ts +11 -1
- package/dist/migrate.js +1 -1
- package/dist/new-session-drafts.d.ts +43 -5
- package/dist/organization-membership-backfill.d.ts +174 -0
- package/dist/organization-membership-lifecycle.d.ts +162 -0
- package/dist/pr-review.d.ts +107 -0
- package/dist/preference-registry-schema.d.ts +2 -0
- package/dist/preference-registry.d.ts +2 -0
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +29 -8
- package/dist/schema.d.ts +24914 -13703
- package/dist/schema.js +99 -1
- package/dist/scoped-knowledge-schema.d.ts +52 -1
- package/dist/session-background-commands.d.ts +125 -0
- package/dist/session-background-commands.js +34 -0
- package/dist/session-background-commands.js.map +1 -0
- package/dist/session-control.d.ts +156 -3
- package/dist/session-goal-pacing.d.ts +68 -0
- package/dist/session-queue-commands.d.ts +43 -4
- package/dist/session-realtime-ledger.d.ts +2 -0
- package/dist/session-tenancy.d.ts +71 -1
- package/dist/session-tenancy.js +25 -3
- package/dist/session-tool-call-settlement.d.ts +9 -4
- package/dist/slack-routing-personal-workspace.d.ts +23 -0
- package/dist/slack-routing.d.ts +268 -0
- package/dist/slack-task-policy-schema.d.ts +34 -0
- package/dist/slack-task-policy.d.ts +8 -0
- package/dist/slack-user-link-access.d.ts +28 -0
- package/dist/task-notes-schema.d.ts +594 -0
- package/dist/task-notes.d.ts +39 -0
- package/dist/turn-initiator.d.ts +2 -0
- package/dist/user-resource-authority.d.ts +109 -0
- package/dist/video-generation.js +3 -3
- package/dist/workspace-authority.d.ts +81 -0
- package/dist/workspace-instruction-policies-schema.d.ts +279 -0
- package/dist/workspace-instruction-policies.d.ts +62 -0
- package/dist/workspace-learning-policy.d.ts +1 -0
- package/dist/workspace-membership-permissions.d.ts +2 -0
- package/drizzle/0240_enrollment_connection_authority.sql +39 -0
- package/drizzle/0241_atomic_personal_resource_delegation.sql +1000 -0
- package/drizzle/0241_enrollment_agent_runtime.sql +70 -0
- package/drizzle/0244_slack_app_home_refresh_queue.sql +120 -0
- package/drizzle/0245_model_context_contribution_facts.sql +88 -0
- package/drizzle/0246_integration_personal_instance_authority.sql +70 -0
- package/drizzle/0247_terraform_stacks_provenance_repair.sql +428 -0
- package/drizzle/0248_terraform_stacks_component_resolution_fence.sql +83 -0
- package/drizzle/0249_personal_resource_delegation_authority_correction.sql +662 -0
- package/drizzle/0250_direct_retained_process_owner_liveness.sql +199 -0
- package/drizzle/0251_connected_machine_operation_policy.sql +45 -0
- package/drizzle/0252_scheduled_personal_resource_delegation.sql +1867 -0
- package/drizzle/0253_common_user_resource_authority_lifecycle.sql +1555 -0
- package/drizzle/0254_scoped_variable_set_authority.sql +1255 -0
- package/drizzle/0255_company_brain_governed_write_proposals.sql +524 -0
- package/drizzle/0256_connection_authority_delegation.sql +703 -0
- package/drizzle/0257_goal_revision_decisions_and_root_constraints.sql +436 -0
- package/drizzle/0258_three_scope_document_knowledge_authority.sql +1273 -0
- package/drizzle/0259_company_brain_context_selection_receipts.sql +869 -0
- package/drizzle/0260_task_note_knowledge_promotion.sql +820 -0
- package/drizzle/0261_preference_knowledge_proposal_actor_binding.sql +471 -0
- package/drizzle/0262_scoped_connected_machines_and_rigs.sql +1700 -0
- package/drizzle/0263_organization_membership_lifecycle.sql +3284 -0
- package/drizzle/0264_connection_authority_runtime_activation.sql +1084 -0
- package/drizzle/0266_company_brain_context_receipt_inspection.sql +156 -0
- package/drizzle/0268_governed_learning_decision_receipts.sql +825 -0
- package/drizzle/0269_governed_learning_activation_controller.sql +1874 -0
- package/drizzle/0270_governed_learning_history_inspection.sql +170 -0
- package/drizzle/0271_company_brain_retrieval_only_default.sql +524 -0
- package/drizzle/0272_human_confirmed_learning_activation.sql +591 -0
- package/drizzle/0273_scheduled_variable_set_materialization.sql +268 -0
- package/drizzle/0274_human_confirmed_knowledge_review.sql +310 -0
- package/drizzle/0275_scheduled_connection_authority.sql +5527 -0
- package/drizzle/0276_onboarding_proposal_initiating_human_guc.sql +198 -0
- package/drizzle/0277_workspace_writer_authority_attribution.sql +393 -0
- package/drizzle/0278_workspace_membership_removal_fencing.sql +1174 -0
- package/drizzle/0279_workspace_connection_use_lane.sql +343 -0
- package/drizzle/0280_connection_and_variable_set_audit_attribution.sql +773 -0
- package/drizzle/0281_viewer_holder_authority_claims.sql +52 -0
- package/drizzle/0282_variable_set_session_attach_attribution.sql +178 -0
- package/drizzle/0283_editable_spreadsheet_authored_state.sql +82 -0
- package/drizzle/0284_truthful_human_confirmed_review_reason.sql +930 -0
- package/drizzle/0285_organization_tenancy_inventory.sql +329 -0
- package/drizzle/0286_widen_task_note_expiry_ceiling.sql +391 -0
- package/drizzle/0287_open_suffix_pending_tool_calls.sql +18 -0
- package/drizzle/0288_attached_browser_reenrollment.sql +64 -0
- package/drizzle/0289_session_composer_policy_authority.sql +597 -0
- package/drizzle/0290_organization_membership_backfill.sql +268 -0
- package/drizzle/0291_resource_authority_classification_assertion.sql +371 -0
- package/drizzle/0292_truthful_tenancy_inventory_counters.sql +291 -0
- package/drizzle/0293_confirm_time_rule_rebaseline.sql +615 -0
- package/drizzle/0294_preference_activation_authority.sql +237 -0
- package/drizzle/0295_retire_legacy_standing_memory_mode.sql +476 -0
- package/drizzle/0296_force_rls_backfill_noop_repair.sql +215 -0
- package/drizzle/0297_session_ownership_classification_and_backfill.sql +752 -0
- package/drizzle/0298_organization_tenancy_parity.sql +609 -0
- package/drizzle/0299_organization_membership_lock_order.sql +291 -0
- package/drizzle/0300_tenancy_backfill_ledger.sql +459 -0
- package/drizzle/0301_session_snapshot_and_pin_visibility.sql +181 -0
- package/drizzle/0302_personal_workspace_session_ownership.sql +303 -0
- package/drizzle/0303_session_tenancy_product_activation.sql +919 -0
- package/drizzle/0304_personal_workspace_private_session_reads.sql +97 -0
- package/drizzle/0305_personal_resource_grant_management.sql +584 -0
- package/drizzle/0306_atomic_personal_resource_attachments.sql +1374 -0
- package/drizzle/0307_session_attention_state.sql +51 -0
- package/drizzle/0308_session_archives.sql +35 -0
- package/drizzle/0309_channel_project_pins.sql +6 -0
- package/drizzle/0310_channel_project_order.sql +27 -0
- package/drizzle/0311_company_scope_and_private_session_create.sql +656 -0
- package/drizzle/0312_quiescent_session_tree_deletion.sql +39 -0
- package/drizzle/0313_private_child_session_authority.sql +365 -0
- package/drizzle/0314_unregistered_organization_invitations.sql +606 -0
- package/drizzle/0315_personal_github_repository_selection.sql +624 -0
- package/drizzle/0316_human_confirmed_activation_resumed_generation.sql +869 -0
- package/drizzle/0317_session_goal_continuation_hold.sql +64 -0
- package/drizzle/0318_session_turn_startup_milestones.sql +115 -0
- package/drizzle/0319_event_triggered_automations.sql +254 -0
- package/drizzle/0320_pr_review_pack.sql +113 -0
- package/drizzle/0321_slack_bot_environment_display_name.sql +15 -0
- package/drizzle/0322_session_turns_unclaimed_prompt_trigger_index.sql +5 -0
- package/drizzle/0323_organization_private_session_enablement.sql +489 -0
- package/drizzle/0324_human_confirmed_company_profile_agent_admin.sql +856 -0
- package/drizzle/0325_child_lifecycle_notices.sql +44 -0
- package/drizzle/0326_interaction_operation_error_codes.sql +62 -0
- package/drizzle/0327_slack_first_task_hint.sql +29 -0
- package/drizzle/0328_session_background_commands.sql +590 -0
- package/drizzle/0329_slack_orchestration_delivery_events.sql +91 -0
- package/drizzle/0330_api_key_descriptions.sql +20 -0
- package/drizzle/0331_managed_organization_creation.sql +163 -0
- package/drizzle/0332_organization_shared_workspace_control_plane.sql +539 -0
- package/drizzle/0333_session_turn_prompt_routing.sql +45 -0
- package/drizzle/0334_connected_machine_workspace_root.sql +99 -0
- package/drizzle/0335_slack_workspace_routing.sql +460 -0
- package/drizzle/0336_atomic_session_fork_visibility.sql +667 -0
- package/drizzle/0337_slack_routed_action_handles.sql +189 -0
- package/drizzle/0338_atomic_connected_machine_attachments.sql +410 -0
- package/drizzle/0339_document_authority_reclassification.sql +1153 -0
- package/drizzle/0340_tenancy_backfill_activation_evidence.sql +1347 -0
- package/drizzle/0341_slack_routing_probe_organization_fence.sql +109 -0
- package/drizzle/0342_slack_route_prompt_single_pending.sql +32 -0
- package/drizzle/0343_personal_document_force_rls_lock_repair.sql +146 -0
- package/drizzle/0344_private_session_visibility_transition_gate.sql +85 -0
- package/drizzle/0345_tenant_scoped_session_tenancy_fence.sql +2839 -0
- package/drizzle/0346_document_migration_audit_surface.sql +454 -0
- package/drizzle/0347_connection_authority_convergence_evidence.sql +396 -0
- package/package.json +10 -6
- package/src/attached-browser-devices.ts +233 -5
- package/src/automations.ts +911 -0
- package/src/browser-sessions.ts +10 -0
- package/src/capability-integrations.ts +175 -1
- package/src/child-lifecycle-notices.ts +241 -0
- package/src/codemode-operations.ts +1 -35
- package/src/company-brain-context-selection-schema.ts +100 -0
- package/src/company-brain-context-selection.ts +359 -0
- package/src/company-brain-governed-writes.ts +1170 -0
- package/src/company-brain.ts +309 -0
- package/src/company-profile-agent-admin.ts +225 -0
- package/src/company-profile-schema.ts +121 -1
- package/src/company-profile.ts +21 -0
- package/src/computer-sessions.ts +10 -1
- package/src/connection-authority.ts +290 -0
- package/src/connection-scopes.ts +20 -0
- package/src/connection-tenancy-backfill.ts +167 -0
- package/src/connection-token-resolver.ts +263 -31
- package/src/database.ts +87 -2
- package/src/editable-artifacts-schema.ts +10 -5
- package/src/editable-artifacts.ts +21 -11
- package/src/governed-learning-activation-schema.ts +121 -0
- package/src/governed-learning-activation.ts +588 -0
- package/src/governed-learning-evaluator-schema.ts +72 -0
- package/src/governed-learning-evaluator.ts +217 -0
- package/src/index.ts +11676 -1550
- package/src/insights.ts +107 -0
- package/src/integration-bindings.ts +88 -6
- package/src/integration-facets.ts +287 -84
- package/src/interaction-placement-loss.ts +234 -0
- package/src/managed-human-provisioning.ts +116 -0
- package/src/memory-domain.ts +27 -32
- package/src/migrate.ts +77 -1
- package/src/new-session-drafts.ts +229 -35
- package/src/organization-membership-backfill.ts +735 -0
- package/src/organization-membership-lifecycle.ts +995 -0
- package/src/pr-review.ts +890 -0
- package/src/preference-registry.ts +31 -1
- package/src/provision-roles.ts +905 -6
- package/src/runtime-posture-cli.ts +2 -0
- package/src/runtime-posture.ts +1317 -72
- package/src/schema.ts +3316 -428
- package/src/scoped-knowledge-schema.ts +16 -8
- package/src/scoped-knowledge.ts +1 -1
- package/src/session-background-commands.ts +759 -0
- package/src/session-control.ts +640 -74
- package/src/session-goal-pacing.ts +126 -0
- package/src/session-queue-commands.ts +441 -85
- package/src/session-realtime-context.ts +4 -6
- package/src/session-realtime-ledger.ts +28 -14
- package/src/session-realtime-mirror.ts +1 -1
- package/src/session-tenancy.ts +410 -49
- package/src/session-tool-call-settlement.ts +38 -29
- package/src/slack-routing-personal-workspace.ts +67 -0
- package/src/slack-routing.ts +867 -0
- package/src/slack-task-policy-schema.ts +5 -0
- package/src/slack-task-policy.ts +18 -0
- package/src/slack-user-link-access.ts +138 -11
- package/src/task-notes-schema.ts +74 -0
- package/src/task-notes.ts +124 -2
- package/src/turn-initiator.ts +8 -0
- package/src/user-resource-authority.ts +377 -0
- package/src/workspace-authority.ts +160 -0
- package/src/workspace-instruction-policies-schema.ts +53 -1
- package/src/workspace-instruction-policies.ts +528 -77
- package/src/workspace-learning-policy.ts +21 -5
- package/src/workspace-membership-permissions.ts +9 -0
- package/dist/chunk-4SR4HKKA.js.map +0 -1
- package/dist/chunk-6ZYKCRFO.js.map +0 -1
- package/dist/chunk-NXAFLCQX.js +0 -2468
- package/dist/chunk-NXAFLCQX.js.map +0 -1
- package/dist/chunk-SK7YYHBI.js.map +0 -1
- package/dist/chunk-WD5ROSLL.js +0 -117
- package/dist/chunk-WD5ROSLL.js.map +0 -1
- package/dist/chunk-ZWREYEWA.js.map +0 -1
- /package/dist/{chunk-CUPM3FGB.js.map → chunk-2PRS4J5Q.js.map} +0 -0
- /package/dist/{chunk-LTG3S6ZY.js.map → chunk-E6CLMKEE.js.map} +0 -0
|
@@ -0,0 +1,2839 @@
|
|
|
1
|
+
-- deployment-mode: maintenance
|
|
2
|
+
SET lock_timeout = '5s';
|
|
3
|
+
SET statement_timeout = '10min';
|
|
4
|
+
|
|
5
|
+
-- opengeni_private contains historical singleton worker routines, so one
|
|
6
|
+
-- database can support exactly one dedicated OpenGeni target schema. Bind the
|
|
7
|
+
-- protocol before any shared CREATE OR REPLACE or target-schema mutation. A
|
|
8
|
+
-- same-target migration-ledger replay is harmless; a different target must
|
|
9
|
+
-- stop before it can repin a global routine or install a partial helper set.
|
|
10
|
+
CREATE TABLE IF NOT EXISTS opengeni_private.session_tenancy_fence_target_registry (
|
|
11
|
+
singleton boolean NOT NULL,
|
|
12
|
+
target_schema regnamespace NOT NULL,
|
|
13
|
+
CONSTRAINT session_tenancy_fence_target_registry_pk PRIMARY KEY (singleton),
|
|
14
|
+
CONSTRAINT session_tenancy_fence_target_registry_singleton_chk CHECK (singleton)
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
REVOKE ALL ON TABLE
|
|
18
|
+
opengeni_private.session_tenancy_fence_target_registry
|
|
19
|
+
FROM PUBLIC;
|
|
20
|
+
|
|
21
|
+
DO $session_tenancy_fence_target_registry_contract$
|
|
22
|
+
DECLARE
|
|
23
|
+
registry_table oid := pg_catalog.to_regclass(
|
|
24
|
+
'opengeni_private.session_tenancy_fence_target_registry'
|
|
25
|
+
);
|
|
26
|
+
actual_columns text[];
|
|
27
|
+
actual_constraints text[];
|
|
28
|
+
registered_target pg_catalog.regnamespace;
|
|
29
|
+
requested_target pg_catalog.regnamespace :=
|
|
30
|
+
pg_catalog.current_schema()::pg_catalog.regnamespace;
|
|
31
|
+
BEGIN
|
|
32
|
+
IF registry_table IS NULL OR NOT EXISTS (
|
|
33
|
+
SELECT 1 FROM pg_catalog.pg_class relation
|
|
34
|
+
WHERE relation.oid = registry_table
|
|
35
|
+
AND relation.relkind = 'r'
|
|
36
|
+
AND relation.relowner = current_user::pg_catalog.regrole
|
|
37
|
+
) THEN
|
|
38
|
+
RAISE EXCEPTION '0345 session tenancy target registry owner/type drift'
|
|
39
|
+
USING ERRCODE = '55000';
|
|
40
|
+
END IF;
|
|
41
|
+
SELECT pg_catalog.array_agg(
|
|
42
|
+
attribute.attname || ':'
|
|
43
|
+
|| pg_catalog.format_type(attribute.atttypid, attribute.atttypmod) || ':'
|
|
44
|
+
|| attribute.attnotnull::text
|
|
45
|
+
ORDER BY attribute.attnum
|
|
46
|
+
) INTO actual_columns
|
|
47
|
+
FROM pg_catalog.pg_attribute attribute
|
|
48
|
+
WHERE attribute.attrelid = registry_table
|
|
49
|
+
AND attribute.attnum > 0
|
|
50
|
+
AND NOT attribute.attisdropped;
|
|
51
|
+
IF actual_columns IS DISTINCT FROM ARRAY[
|
|
52
|
+
'singleton:boolean:true',
|
|
53
|
+
'target_schema:regnamespace:true'
|
|
54
|
+
]::text[] THEN
|
|
55
|
+
RAISE EXCEPTION '0345 session tenancy target registry column drift'
|
|
56
|
+
USING ERRCODE = '55000', DETAIL = actual_columns::text;
|
|
57
|
+
END IF;
|
|
58
|
+
SELECT pg_catalog.array_agg(
|
|
59
|
+
constraint_value.conname || ':' || constraint_value.contype::text || ':'
|
|
60
|
+
|| pg_catalog.pg_get_constraintdef(constraint_value.oid, false)
|
|
61
|
+
ORDER BY constraint_value.conname
|
|
62
|
+
) INTO actual_constraints
|
|
63
|
+
FROM pg_catalog.pg_constraint constraint_value
|
|
64
|
+
WHERE constraint_value.conrelid = registry_table;
|
|
65
|
+
IF actual_constraints IS DISTINCT FROM ARRAY[
|
|
66
|
+
'session_tenancy_fence_target_registry_pk:p:PRIMARY KEY (singleton)',
|
|
67
|
+
'session_tenancy_fence_target_registry_singleton_chk:c:CHECK (singleton)'
|
|
68
|
+
]::text[] THEN
|
|
69
|
+
RAISE EXCEPTION '0345 session tenancy target registry constraint drift'
|
|
70
|
+
USING ERRCODE = '55000', DETAIL = actual_constraints::text;
|
|
71
|
+
END IF;
|
|
72
|
+
IF EXISTS (
|
|
73
|
+
SELECT 1
|
|
74
|
+
FROM pg_catalog.pg_class relation
|
|
75
|
+
CROSS JOIN LATERAL pg_catalog.aclexplode(
|
|
76
|
+
COALESCE(
|
|
77
|
+
relation.relacl,
|
|
78
|
+
pg_catalog.acldefault('r', relation.relowner)
|
|
79
|
+
)
|
|
80
|
+
) acl
|
|
81
|
+
WHERE relation.oid = registry_table
|
|
82
|
+
AND acl.grantee <> relation.relowner
|
|
83
|
+
) THEN
|
|
84
|
+
RAISE EXCEPTION '0345 session tenancy target registry ACL drift'
|
|
85
|
+
USING ERRCODE = '55000';
|
|
86
|
+
END IF;
|
|
87
|
+
|
|
88
|
+
INSERT INTO opengeni_private.session_tenancy_fence_target_registry (
|
|
89
|
+
singleton, target_schema
|
|
90
|
+
) VALUES (true, requested_target)
|
|
91
|
+
ON CONFLICT (singleton) DO NOTHING;
|
|
92
|
+
|
|
93
|
+
SELECT registry.target_schema
|
|
94
|
+
INTO registered_target
|
|
95
|
+
FROM opengeni_private.session_tenancy_fence_target_registry registry
|
|
96
|
+
WHERE registry.singleton
|
|
97
|
+
FOR UPDATE;
|
|
98
|
+
IF registered_target IS DISTINCT FROM requested_target THEN
|
|
99
|
+
RAISE EXCEPTION
|
|
100
|
+
'0345 session tenancy target schema is already bound to %, not %',
|
|
101
|
+
registered_target,
|
|
102
|
+
requested_target
|
|
103
|
+
USING ERRCODE = '55000';
|
|
104
|
+
END IF;
|
|
105
|
+
END
|
|
106
|
+
$session_tenancy_fence_target_registry_contract$;
|
|
107
|
+
|
|
108
|
+
-- This is a protocol cutover, not merely a convention in the new TypeScript
|
|
109
|
+
-- image. Every hot-table mutation must arrive with the workspace fence already
|
|
110
|
+
-- held; the trigger deliberately refuses to acquire it after PostgreSQL may
|
|
111
|
+
-- have taken a tuple lock. Stop every old API/worker before this migration and
|
|
112
|
+
-- never restart a pre-0345 image after it commits.
|
|
113
|
+
CREATE OR REPLACE FUNCTION opengeni_private.require_session_tenancy_fence()
|
|
114
|
+
RETURNS trigger
|
|
115
|
+
LANGUAGE plpgsql
|
|
116
|
+
SECURITY DEFINER
|
|
117
|
+
SET search_path = pg_catalog
|
|
118
|
+
AS $fence$
|
|
119
|
+
DECLARE
|
|
120
|
+
workspace_id_value uuid;
|
|
121
|
+
lock_key bigint;
|
|
122
|
+
BEGIN
|
|
123
|
+
IF EXISTS (
|
|
124
|
+
SELECT 1 FROM pg_roles role_value
|
|
125
|
+
WHERE role_value.rolname = session_user
|
|
126
|
+
AND (role_value.rolsuper OR role_value.rolbypassrls)
|
|
127
|
+
) THEN
|
|
128
|
+
IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
|
|
129
|
+
RETURN NEW;
|
|
130
|
+
END IF;
|
|
131
|
+
IF TG_OP = 'DELETE' THEN
|
|
132
|
+
workspace_id_value := (to_jsonb(OLD) ->> 'workspace_id')::uuid;
|
|
133
|
+
ELSE
|
|
134
|
+
workspace_id_value := (to_jsonb(NEW) ->> 'workspace_id')::uuid;
|
|
135
|
+
END IF;
|
|
136
|
+
IF workspace_id_value IS NULL THEN
|
|
137
|
+
RAISE EXCEPTION 'session tenancy mutation has no workspace fence target'
|
|
138
|
+
USING ERRCODE = '55000';
|
|
139
|
+
END IF;
|
|
140
|
+
lock_key := hashtextextended('session-tenancy:' || workspace_id_value::text, 0);
|
|
141
|
+
IF NOT EXISTS (
|
|
142
|
+
SELECT 1 FROM pg_locks held
|
|
143
|
+
WHERE held.locktype = 'advisory'
|
|
144
|
+
AND held.pid = pg_backend_pid()
|
|
145
|
+
AND held.granted
|
|
146
|
+
AND held.classid = (((lock_key >> 32) & 4294967295)::bigint)::oid
|
|
147
|
+
AND held.objid = ((lock_key & 4294967295)::bigint)::oid
|
|
148
|
+
AND held.objsubid = 1
|
|
149
|
+
AND held.mode IN ('ShareLock', 'ExclusiveLock')
|
|
150
|
+
) THEN
|
|
151
|
+
RAISE EXCEPTION 'session tenancy mutation requires the workspace fence'
|
|
152
|
+
USING ERRCODE = '55000', DETAIL = TG_TABLE_NAME;
|
|
153
|
+
END IF;
|
|
154
|
+
IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
|
|
155
|
+
RETURN NEW;
|
|
156
|
+
END
|
|
157
|
+
$fence$;
|
|
158
|
+
|
|
159
|
+
REVOKE ALL ON FUNCTION opengeni_private.require_session_tenancy_fence() FROM PUBLIC;
|
|
160
|
+
|
|
161
|
+
-- FORCE RLS also applies to the non-bypass migration owner that owns these
|
|
162
|
+
-- SECURITY-DEFINER routines. A global sweep cannot know which workspace
|
|
163
|
+
-- fences to take until it can read a bounded workspace-id inventory, so give
|
|
164
|
+
-- only the private helpers below an exact-schema/backend/xact-bound read
|
|
165
|
+
-- capability. Every open gets an independent token: if an inner helper raises,
|
|
166
|
+
-- its insert rolls back before its exception handler runs, so closing that
|
|
167
|
+
-- exact token cannot consume an outer helper's authority. The capability is
|
|
168
|
+
-- closed before the helper returns any count. Once the fences are held, a
|
|
169
|
+
-- second owner-only policy admits the routine's actual reads and mutations for
|
|
170
|
+
-- those exact workspaces. opengeni_app cannot mint the capability, and merely
|
|
171
|
+
-- acquiring an advisory lock does not satisfy the current_user owner check.
|
|
172
|
+
CREATE TABLE IF NOT EXISTS opengeni_private.session_tenancy_fence_inventory_capabilities (
|
|
173
|
+
capability_id uuid PRIMARY KEY,
|
|
174
|
+
target_schema oid NOT NULL,
|
|
175
|
+
backend_pid integer NOT NULL,
|
|
176
|
+
transaction_id xid8 NOT NULL
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
REVOKE ALL ON TABLE
|
|
180
|
+
opengeni_private.session_tenancy_fence_inventory_capabilities
|
|
181
|
+
FROM PUBLIC;
|
|
182
|
+
|
|
183
|
+
-- opengeni_private is shared by dedicated target schemas. A second target may
|
|
184
|
+
-- reuse this ledger only when the first target installed the exact owner,
|
|
185
|
+
-- shape, primary key, and closed ACL expected here; otherwise stop rather than
|
|
186
|
+
-- inheriting ambiguous authority.
|
|
187
|
+
DO $session_tenancy_fence_inventory_contract$
|
|
188
|
+
DECLARE
|
|
189
|
+
capability_table oid := pg_catalog.to_regclass(
|
|
190
|
+
'opengeni_private.session_tenancy_fence_inventory_capabilities'
|
|
191
|
+
);
|
|
192
|
+
actual_columns text[];
|
|
193
|
+
primary_key_columns text[];
|
|
194
|
+
BEGIN
|
|
195
|
+
IF capability_table IS NULL THEN
|
|
196
|
+
RAISE EXCEPTION '0345 session tenancy inventory ledger is missing'
|
|
197
|
+
USING ERRCODE = '55000';
|
|
198
|
+
END IF;
|
|
199
|
+
IF NOT EXISTS (
|
|
200
|
+
SELECT 1 FROM pg_catalog.pg_class relation
|
|
201
|
+
WHERE relation.oid = capability_table
|
|
202
|
+
AND relation.relkind = 'r'
|
|
203
|
+
AND relation.relowner = current_user::pg_catalog.regrole
|
|
204
|
+
) THEN
|
|
205
|
+
RAISE EXCEPTION '0345 session tenancy inventory ledger owner/type drift'
|
|
206
|
+
USING ERRCODE = '55000';
|
|
207
|
+
END IF;
|
|
208
|
+
SELECT pg_catalog.array_agg(
|
|
209
|
+
attribute.attname || ':'
|
|
210
|
+
|| pg_catalog.format_type(attribute.atttypid, attribute.atttypmod) || ':'
|
|
211
|
+
|| attribute.attnotnull::text
|
|
212
|
+
ORDER BY attribute.attnum
|
|
213
|
+
) INTO actual_columns
|
|
214
|
+
FROM pg_catalog.pg_attribute attribute
|
|
215
|
+
WHERE attribute.attrelid = capability_table
|
|
216
|
+
AND attribute.attnum > 0
|
|
217
|
+
AND NOT attribute.attisdropped;
|
|
218
|
+
IF actual_columns IS DISTINCT FROM ARRAY[
|
|
219
|
+
'capability_id:uuid:true',
|
|
220
|
+
'target_schema:oid:true',
|
|
221
|
+
'backend_pid:integer:true',
|
|
222
|
+
'transaction_id:xid8:true'
|
|
223
|
+
]::text[] THEN
|
|
224
|
+
RAISE EXCEPTION '0345 session tenancy inventory ledger column drift'
|
|
225
|
+
USING ERRCODE = '55000', DETAIL = actual_columns::text;
|
|
226
|
+
END IF;
|
|
227
|
+
SELECT pg_catalog.array_agg(attribute.attname ORDER BY key.ordinality)
|
|
228
|
+
INTO primary_key_columns
|
|
229
|
+
FROM pg_catalog.pg_constraint constraint_value
|
|
230
|
+
CROSS JOIN LATERAL pg_catalog.unnest(constraint_value.conkey)
|
|
231
|
+
WITH ORDINALITY key(attnum, ordinality)
|
|
232
|
+
JOIN pg_catalog.pg_attribute attribute
|
|
233
|
+
ON attribute.attrelid = constraint_value.conrelid
|
|
234
|
+
AND attribute.attnum = key.attnum
|
|
235
|
+
WHERE constraint_value.conrelid = capability_table
|
|
236
|
+
AND constraint_value.contype = 'p';
|
|
237
|
+
IF primary_key_columns IS DISTINCT FROM ARRAY['capability_id']::text[]
|
|
238
|
+
OR (
|
|
239
|
+
SELECT pg_catalog.count(*) FROM pg_catalog.pg_constraint constraint_value
|
|
240
|
+
WHERE constraint_value.conrelid = capability_table
|
|
241
|
+
) <> 1
|
|
242
|
+
THEN
|
|
243
|
+
RAISE EXCEPTION '0345 session tenancy inventory ledger constraint drift'
|
|
244
|
+
USING ERRCODE = '55000';
|
|
245
|
+
END IF;
|
|
246
|
+
IF EXISTS (
|
|
247
|
+
SELECT 1
|
|
248
|
+
FROM pg_catalog.pg_class relation
|
|
249
|
+
CROSS JOIN LATERAL pg_catalog.aclexplode(
|
|
250
|
+
COALESCE(
|
|
251
|
+
relation.relacl,
|
|
252
|
+
pg_catalog.acldefault('r', relation.relowner)
|
|
253
|
+
)
|
|
254
|
+
) acl
|
|
255
|
+
WHERE relation.oid = capability_table
|
|
256
|
+
AND acl.grantee <> relation.relowner
|
|
257
|
+
) THEN
|
|
258
|
+
RAISE EXCEPTION '0345 session tenancy inventory ledger ACL drift'
|
|
259
|
+
USING ERRCODE = '55000';
|
|
260
|
+
END IF;
|
|
261
|
+
END
|
|
262
|
+
$session_tenancy_fence_inventory_contract$;
|
|
263
|
+
|
|
264
|
+
CREATE OR REPLACE FUNCTION
|
|
265
|
+
opengeni_private.session_tenancy_fence_inventory_capability_active(
|
|
266
|
+
p_target_schema oid
|
|
267
|
+
)
|
|
268
|
+
RETURNS boolean
|
|
269
|
+
LANGUAGE sql
|
|
270
|
+
STABLE
|
|
271
|
+
SECURITY DEFINER
|
|
272
|
+
SET search_path = pg_catalog
|
|
273
|
+
AS $inventory_active$
|
|
274
|
+
SELECT EXISTS (
|
|
275
|
+
SELECT 1
|
|
276
|
+
FROM opengeni_private.session_tenancy_fence_inventory_capabilities capability
|
|
277
|
+
WHERE capability.target_schema = p_target_schema
|
|
278
|
+
AND capability.backend_pid = pg_catalog.pg_backend_pid()
|
|
279
|
+
AND capability.transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
|
|
280
|
+
)
|
|
281
|
+
$inventory_active$;
|
|
282
|
+
|
|
283
|
+
REVOKE ALL ON FUNCTION
|
|
284
|
+
opengeni_private.session_tenancy_fence_inventory_capability_active(oid)
|
|
285
|
+
FROM PUBLIC;
|
|
286
|
+
|
|
287
|
+
CREATE OR REPLACE FUNCTION
|
|
288
|
+
opengeni_private.open_session_tenancy_fence_inventory(p_target_schema oid)
|
|
289
|
+
RETURNS uuid
|
|
290
|
+
LANGUAGE plpgsql
|
|
291
|
+
SECURITY DEFINER
|
|
292
|
+
SET search_path = pg_catalog
|
|
293
|
+
AS $open_inventory$
|
|
294
|
+
DECLARE
|
|
295
|
+
capability_id_value uuid := pg_catalog.gen_random_uuid();
|
|
296
|
+
BEGIN
|
|
297
|
+
IF NOT EXISTS (
|
|
298
|
+
SELECT 1
|
|
299
|
+
FROM pg_catalog.pg_namespace namespace
|
|
300
|
+
JOIN opengeni_private.session_tenancy_fence_target_registry registry
|
|
301
|
+
ON registry.target_schema = namespace.oid
|
|
302
|
+
AND registry.singleton
|
|
303
|
+
WHERE namespace.oid = p_target_schema
|
|
304
|
+
) THEN
|
|
305
|
+
RAISE EXCEPTION 'session tenancy inventory target schema is not registered'
|
|
306
|
+
USING ERRCODE = '22023';
|
|
307
|
+
END IF;
|
|
308
|
+
INSERT INTO opengeni_private.session_tenancy_fence_inventory_capabilities (
|
|
309
|
+
capability_id, target_schema, backend_pid, transaction_id
|
|
310
|
+
) VALUES (
|
|
311
|
+
capability_id_value,
|
|
312
|
+
p_target_schema,
|
|
313
|
+
pg_catalog.pg_backend_pid(),
|
|
314
|
+
pg_catalog.pg_current_xact_id()
|
|
315
|
+
);
|
|
316
|
+
RETURN capability_id_value;
|
|
317
|
+
END
|
|
318
|
+
$open_inventory$;
|
|
319
|
+
|
|
320
|
+
CREATE OR REPLACE FUNCTION
|
|
321
|
+
opengeni_private.close_session_tenancy_fence_inventory(p_capability_id uuid)
|
|
322
|
+
RETURNS void
|
|
323
|
+
LANGUAGE sql
|
|
324
|
+
SECURITY DEFINER
|
|
325
|
+
SET search_path = pg_catalog
|
|
326
|
+
AS $close_inventory$
|
|
327
|
+
DELETE FROM opengeni_private.session_tenancy_fence_inventory_capabilities
|
|
328
|
+
WHERE capability_id = p_capability_id
|
|
329
|
+
AND backend_pid = pg_catalog.pg_backend_pid()
|
|
330
|
+
AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
|
|
331
|
+
$close_inventory$;
|
|
332
|
+
|
|
333
|
+
REVOKE ALL ON FUNCTION
|
|
334
|
+
opengeni_private.open_session_tenancy_fence_inventory(oid)
|
|
335
|
+
FROM PUBLIC;
|
|
336
|
+
REVOKE ALL ON FUNCTION
|
|
337
|
+
opengeni_private.close_session_tenancy_fence_inventory(uuid)
|
|
338
|
+
FROM PUBLIC;
|
|
339
|
+
|
|
340
|
+
-- Inventory authority ends before a helper returns. The caller may still need
|
|
341
|
+
-- owner-side RLS access while it mutates the rows protected by the locks, so
|
|
342
|
+
-- that phase has a separate exact-token capability. It is opened only after
|
|
343
|
+
-- all affected workspace fences are held and is closed by the patched caller
|
|
344
|
+
-- on both success and exception.
|
|
345
|
+
CREATE TABLE IF NOT EXISTS opengeni_private.session_tenancy_fenced_access_capabilities (
|
|
346
|
+
capability_id uuid PRIMARY KEY,
|
|
347
|
+
target_schema oid NOT NULL,
|
|
348
|
+
backend_pid integer NOT NULL,
|
|
349
|
+
transaction_id xid8 NOT NULL
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
REVOKE ALL ON TABLE
|
|
353
|
+
opengeni_private.session_tenancy_fenced_access_capabilities
|
|
354
|
+
FROM PUBLIC;
|
|
355
|
+
|
|
356
|
+
DO $session_tenancy_fenced_access_contract$
|
|
357
|
+
DECLARE
|
|
358
|
+
capability_table oid := pg_catalog.to_regclass(
|
|
359
|
+
'opengeni_private.session_tenancy_fenced_access_capabilities'
|
|
360
|
+
);
|
|
361
|
+
actual_columns text[];
|
|
362
|
+
primary_key_columns text[];
|
|
363
|
+
BEGIN
|
|
364
|
+
IF capability_table IS NULL OR NOT EXISTS (
|
|
365
|
+
SELECT 1 FROM pg_catalog.pg_class relation
|
|
366
|
+
WHERE relation.oid = capability_table
|
|
367
|
+
AND relation.relkind = 'r'
|
|
368
|
+
AND relation.relowner = current_user::pg_catalog.regrole
|
|
369
|
+
) THEN
|
|
370
|
+
RAISE EXCEPTION '0345 session tenancy fenced-access ledger owner/type drift'
|
|
371
|
+
USING ERRCODE = '55000';
|
|
372
|
+
END IF;
|
|
373
|
+
SELECT pg_catalog.array_agg(
|
|
374
|
+
attribute.attname || ':'
|
|
375
|
+
|| pg_catalog.format_type(attribute.atttypid, attribute.atttypmod) || ':'
|
|
376
|
+
|| attribute.attnotnull::text
|
|
377
|
+
ORDER BY attribute.attnum
|
|
378
|
+
) INTO actual_columns
|
|
379
|
+
FROM pg_catalog.pg_attribute attribute
|
|
380
|
+
WHERE attribute.attrelid = capability_table
|
|
381
|
+
AND attribute.attnum > 0
|
|
382
|
+
AND NOT attribute.attisdropped;
|
|
383
|
+
IF actual_columns IS DISTINCT FROM ARRAY[
|
|
384
|
+
'capability_id:uuid:true',
|
|
385
|
+
'target_schema:oid:true',
|
|
386
|
+
'backend_pid:integer:true',
|
|
387
|
+
'transaction_id:xid8:true'
|
|
388
|
+
]::text[] THEN
|
|
389
|
+
RAISE EXCEPTION '0345 session tenancy fenced-access ledger column drift'
|
|
390
|
+
USING ERRCODE = '55000', DETAIL = actual_columns::text;
|
|
391
|
+
END IF;
|
|
392
|
+
SELECT pg_catalog.array_agg(attribute.attname ORDER BY key.ordinality)
|
|
393
|
+
INTO primary_key_columns
|
|
394
|
+
FROM pg_catalog.pg_constraint constraint_value
|
|
395
|
+
CROSS JOIN LATERAL pg_catalog.unnest(constraint_value.conkey)
|
|
396
|
+
WITH ORDINALITY key(attnum, ordinality)
|
|
397
|
+
JOIN pg_catalog.pg_attribute attribute
|
|
398
|
+
ON attribute.attrelid = constraint_value.conrelid
|
|
399
|
+
AND attribute.attnum = key.attnum
|
|
400
|
+
WHERE constraint_value.conrelid = capability_table
|
|
401
|
+
AND constraint_value.contype = 'p';
|
|
402
|
+
IF primary_key_columns IS DISTINCT FROM ARRAY['capability_id']::text[]
|
|
403
|
+
OR (
|
|
404
|
+
SELECT pg_catalog.count(*) FROM pg_catalog.pg_constraint constraint_value
|
|
405
|
+
WHERE constraint_value.conrelid = capability_table
|
|
406
|
+
) <> 1
|
|
407
|
+
OR EXISTS (
|
|
408
|
+
SELECT 1
|
|
409
|
+
FROM pg_catalog.pg_class relation
|
|
410
|
+
CROSS JOIN LATERAL pg_catalog.aclexplode(
|
|
411
|
+
COALESCE(
|
|
412
|
+
relation.relacl,
|
|
413
|
+
pg_catalog.acldefault('r', relation.relowner)
|
|
414
|
+
)
|
|
415
|
+
) acl
|
|
416
|
+
WHERE relation.oid = capability_table
|
|
417
|
+
AND acl.grantee <> relation.relowner
|
|
418
|
+
)
|
|
419
|
+
THEN
|
|
420
|
+
RAISE EXCEPTION '0345 session tenancy fenced-access ledger constraint/ACL drift'
|
|
421
|
+
USING ERRCODE = '55000';
|
|
422
|
+
END IF;
|
|
423
|
+
END
|
|
424
|
+
$session_tenancy_fenced_access_contract$;
|
|
425
|
+
|
|
426
|
+
CREATE OR REPLACE FUNCTION
|
|
427
|
+
opengeni_private.open_session_tenancy_fenced_access(p_target_schema oid)
|
|
428
|
+
RETURNS uuid
|
|
429
|
+
LANGUAGE plpgsql
|
|
430
|
+
SECURITY DEFINER
|
|
431
|
+
SET search_path = pg_catalog
|
|
432
|
+
AS $open_fenced_access$
|
|
433
|
+
DECLARE
|
|
434
|
+
capability_id_value uuid := pg_catalog.gen_random_uuid();
|
|
435
|
+
BEGIN
|
|
436
|
+
IF NOT EXISTS (
|
|
437
|
+
SELECT 1
|
|
438
|
+
FROM pg_catalog.pg_namespace namespace
|
|
439
|
+
JOIN opengeni_private.session_tenancy_fence_target_registry registry
|
|
440
|
+
ON registry.target_schema = namespace.oid
|
|
441
|
+
AND registry.singleton
|
|
442
|
+
WHERE namespace.oid = p_target_schema
|
|
443
|
+
) THEN
|
|
444
|
+
RAISE EXCEPTION 'session tenancy fenced-access target schema is not registered'
|
|
445
|
+
USING ERRCODE = '22023';
|
|
446
|
+
END IF;
|
|
447
|
+
INSERT INTO opengeni_private.session_tenancy_fenced_access_capabilities (
|
|
448
|
+
capability_id, target_schema, backend_pid, transaction_id
|
|
449
|
+
) VALUES (
|
|
450
|
+
capability_id_value,
|
|
451
|
+
p_target_schema,
|
|
452
|
+
pg_catalog.pg_backend_pid(),
|
|
453
|
+
pg_catalog.pg_current_xact_id()
|
|
454
|
+
);
|
|
455
|
+
RETURN capability_id_value;
|
|
456
|
+
END
|
|
457
|
+
$open_fenced_access$;
|
|
458
|
+
|
|
459
|
+
CREATE OR REPLACE FUNCTION
|
|
460
|
+
opengeni_private.close_session_tenancy_fenced_access(p_capability_id uuid)
|
|
461
|
+
RETURNS void
|
|
462
|
+
LANGUAGE sql
|
|
463
|
+
SECURITY DEFINER
|
|
464
|
+
SET search_path = pg_catalog
|
|
465
|
+
AS $close_fenced_access$
|
|
466
|
+
DELETE FROM opengeni_private.session_tenancy_fenced_access_capabilities
|
|
467
|
+
WHERE capability_id = p_capability_id
|
|
468
|
+
AND backend_pid = pg_catalog.pg_backend_pid()
|
|
469
|
+
AND transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
|
|
470
|
+
$close_fenced_access$;
|
|
471
|
+
|
|
472
|
+
CREATE OR REPLACE FUNCTION
|
|
473
|
+
opengeni_private.session_tenancy_fenced_access_capability_active(
|
|
474
|
+
p_target_schema oid
|
|
475
|
+
)
|
|
476
|
+
RETURNS boolean
|
|
477
|
+
LANGUAGE sql
|
|
478
|
+
STABLE
|
|
479
|
+
SECURITY DEFINER
|
|
480
|
+
SET search_path = pg_catalog
|
|
481
|
+
AS $fenced_access_active$
|
|
482
|
+
SELECT EXISTS (
|
|
483
|
+
SELECT 1
|
|
484
|
+
FROM opengeni_private.session_tenancy_fenced_access_capabilities capability
|
|
485
|
+
WHERE capability.target_schema = p_target_schema
|
|
486
|
+
AND capability.backend_pid = pg_catalog.pg_backend_pid()
|
|
487
|
+
AND capability.transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
|
|
488
|
+
)
|
|
489
|
+
$fenced_access_active$;
|
|
490
|
+
|
|
491
|
+
REVOKE ALL ON FUNCTION
|
|
492
|
+
opengeni_private.open_session_tenancy_fenced_access(oid)
|
|
493
|
+
FROM PUBLIC;
|
|
494
|
+
REVOKE ALL ON FUNCTION
|
|
495
|
+
opengeni_private.close_session_tenancy_fenced_access(uuid)
|
|
496
|
+
FROM PUBLIC;
|
|
497
|
+
REVOKE ALL ON FUNCTION
|
|
498
|
+
opengeni_private.session_tenancy_fenced_access_capability_active(oid)
|
|
499
|
+
FROM PUBLIC;
|
|
500
|
+
|
|
501
|
+
-- CREATE OR REPLACE preserves a function's owner and direct role grants. Do
|
|
502
|
+
-- not let a pre-existing same-signature object silently retain mint/close or
|
|
503
|
+
-- capability-observation authority. All six shared seams must be owned only by
|
|
504
|
+
-- this migration owner and executable by no other role.
|
|
505
|
+
DO $session_tenancy_shared_capability_function_contract$
|
|
506
|
+
DECLARE
|
|
507
|
+
signature text;
|
|
508
|
+
routine oid;
|
|
509
|
+
BEGIN
|
|
510
|
+
FOREACH signature IN ARRAY ARRAY[
|
|
511
|
+
'opengeni_private.session_tenancy_fence_inventory_capability_active(oid)',
|
|
512
|
+
'opengeni_private.open_session_tenancy_fence_inventory(oid)',
|
|
513
|
+
'opengeni_private.close_session_tenancy_fence_inventory(uuid)',
|
|
514
|
+
'opengeni_private.session_tenancy_fenced_access_capability_active(oid)',
|
|
515
|
+
'opengeni_private.open_session_tenancy_fenced_access(oid)',
|
|
516
|
+
'opengeni_private.close_session_tenancy_fenced_access(uuid)'
|
|
517
|
+
] LOOP
|
|
518
|
+
routine := pg_catalog.to_regprocedure(signature);
|
|
519
|
+
IF routine IS NULL OR NOT EXISTS (
|
|
520
|
+
SELECT 1 FROM pg_catalog.pg_proc procedure
|
|
521
|
+
WHERE procedure.oid = routine
|
|
522
|
+
AND procedure.proowner = current_user::pg_catalog.regrole
|
|
523
|
+
AND procedure.prosecdef
|
|
524
|
+
) THEN
|
|
525
|
+
RAISE EXCEPTION
|
|
526
|
+
'0345 shared session tenancy capability function owner/type drift: %',
|
|
527
|
+
signature USING ERRCODE = '55000';
|
|
528
|
+
END IF;
|
|
529
|
+
IF EXISTS (
|
|
530
|
+
SELECT 1
|
|
531
|
+
FROM pg_catalog.pg_proc procedure
|
|
532
|
+
CROSS JOIN LATERAL pg_catalog.aclexplode(
|
|
533
|
+
COALESCE(
|
|
534
|
+
procedure.proacl,
|
|
535
|
+
pg_catalog.acldefault('f', procedure.proowner)
|
|
536
|
+
)
|
|
537
|
+
) acl
|
|
538
|
+
WHERE procedure.oid = routine
|
|
539
|
+
AND acl.grantee <> procedure.proowner
|
|
540
|
+
) THEN
|
|
541
|
+
RAISE EXCEPTION
|
|
542
|
+
'0345 shared session tenancy capability function ACL drift: %',
|
|
543
|
+
signature USING ERRCODE = '55000';
|
|
544
|
+
END IF;
|
|
545
|
+
END LOOP;
|
|
546
|
+
END
|
|
547
|
+
$session_tenancy_shared_capability_function_contract$;
|
|
548
|
+
|
|
549
|
+
-- The target identity is a literal namespace OID captured by this migration,
|
|
550
|
+
-- not a search_path-, GUC-, or caller-controlled string. Each dedicated schema
|
|
551
|
+
-- owns a separate copy, so later target migrations cannot overwrite it.
|
|
552
|
+
DO $session_tenancy_fence_target_identity$
|
|
553
|
+
DECLARE
|
|
554
|
+
target_schema text := pg_catalog.current_schema();
|
|
555
|
+
target_schema_oid oid := pg_catalog.current_schema()::pg_catalog.regnamespace;
|
|
556
|
+
BEGIN
|
|
557
|
+
EXECUTE pg_catalog.format($identity$
|
|
558
|
+
CREATE OR REPLACE FUNCTION %1$I.session_tenancy_fence_target_schema()
|
|
559
|
+
RETURNS oid
|
|
560
|
+
LANGUAGE sql
|
|
561
|
+
IMMUTABLE
|
|
562
|
+
SECURITY DEFINER
|
|
563
|
+
SET search_path = pg_catalog
|
|
564
|
+
AS $body$ SELECT %2$s::oid $body$;
|
|
565
|
+
REVOKE ALL ON FUNCTION %1$I.session_tenancy_fence_target_schema() FROM PUBLIC;
|
|
566
|
+
$identity$, target_schema, target_schema_oid);
|
|
567
|
+
END
|
|
568
|
+
$session_tenancy_fence_target_identity$;
|
|
569
|
+
|
|
570
|
+
CREATE OR REPLACE FUNCTION session_tenancy_fence_owner_policy_active(
|
|
571
|
+
p_actor text,
|
|
572
|
+
p_expected_owner text,
|
|
573
|
+
p_target_schema oid,
|
|
574
|
+
p_workspace_id uuid,
|
|
575
|
+
p_inventory boolean
|
|
576
|
+
)
|
|
577
|
+
RETURNS boolean
|
|
578
|
+
LANGUAGE plpgsql
|
|
579
|
+
STABLE
|
|
580
|
+
SECURITY DEFINER
|
|
581
|
+
SET search_path = pg_catalog
|
|
582
|
+
AS $owner_policy$
|
|
583
|
+
DECLARE
|
|
584
|
+
lock_key bigint;
|
|
585
|
+
BEGIN
|
|
586
|
+
IF p_actor IS DISTINCT FROM p_expected_owner THEN
|
|
587
|
+
RETURN false;
|
|
588
|
+
END IF;
|
|
589
|
+
IF p_inventory THEN
|
|
590
|
+
RETURN opengeni_private.session_tenancy_fence_inventory_capability_active(
|
|
591
|
+
p_target_schema
|
|
592
|
+
);
|
|
593
|
+
END IF;
|
|
594
|
+
IF p_workspace_id IS NULL THEN
|
|
595
|
+
RETURN false;
|
|
596
|
+
END IF;
|
|
597
|
+
IF NOT opengeni_private.session_tenancy_fenced_access_capability_active(
|
|
598
|
+
p_target_schema
|
|
599
|
+
) THEN
|
|
600
|
+
RETURN false;
|
|
601
|
+
END IF;
|
|
602
|
+
lock_key := pg_catalog.hashtextextended(
|
|
603
|
+
'session-tenancy:' || p_workspace_id::text,
|
|
604
|
+
0
|
|
605
|
+
);
|
|
606
|
+
RETURN EXISTS (
|
|
607
|
+
SELECT 1 FROM pg_catalog.pg_locks held
|
|
608
|
+
WHERE held.locktype = 'advisory'
|
|
609
|
+
AND held.pid = pg_catalog.pg_backend_pid()
|
|
610
|
+
AND held.granted
|
|
611
|
+
AND held.classid = (((lock_key >> 32) & 4294967295)::bigint)::oid
|
|
612
|
+
AND held.objid = ((lock_key & 4294967295)::bigint)::oid
|
|
613
|
+
AND held.objsubid = 1
|
|
614
|
+
AND held.mode IN ('ShareLock', 'ExclusiveLock')
|
|
615
|
+
);
|
|
616
|
+
END
|
|
617
|
+
$owner_policy$;
|
|
618
|
+
|
|
619
|
+
REVOKE ALL ON FUNCTION
|
|
620
|
+
session_tenancy_fence_owner_policy_active(text, text, oid, uuid, boolean)
|
|
621
|
+
FROM PUBLIC;
|
|
622
|
+
-- This predicate returns only a boolean derived from its arguments and the
|
|
623
|
+
-- current backend's private capability/lock state. Every runtime role must be
|
|
624
|
+
-- able to plan RLS expressions that reference it; authority-minting functions
|
|
625
|
+
-- and the ledger remain fully revoked.
|
|
626
|
+
GRANT EXECUTE ON FUNCTION
|
|
627
|
+
session_tenancy_fence_owner_policy_active(text, text, oid, uuid, boolean)
|
|
628
|
+
TO PUBLIC;
|
|
629
|
+
|
|
630
|
+
DO $session_tenancy_fence_owner_policies$
|
|
631
|
+
DECLARE
|
|
632
|
+
target_schema text := pg_catalog.current_schema();
|
|
633
|
+
target_schema_oid oid := pg_catalog.current_schema()::pg_catalog.regnamespace;
|
|
634
|
+
migration_owner text := current_user;
|
|
635
|
+
table_name text;
|
|
636
|
+
BEGIN
|
|
637
|
+
FOREACH table_name IN ARRAY ARRAY[
|
|
638
|
+
'sandbox_leases',
|
|
639
|
+
'sandbox_lease_holders',
|
|
640
|
+
'sandbox_retained_processes',
|
|
641
|
+
'sessions'
|
|
642
|
+
] LOOP
|
|
643
|
+
EXECUTE pg_catalog.format(
|
|
644
|
+
'CREATE POLICY session_tenancy_fence_inventory_read ON %I.%I '
|
|
645
|
+
|| 'FOR SELECT USING ('
|
|
646
|
+
|| '%I.session_tenancy_fence_owner_policy_active('
|
|
647
|
+
|| 'current_user, %L, %s::oid, workspace_id, true))',
|
|
648
|
+
target_schema,
|
|
649
|
+
table_name,
|
|
650
|
+
target_schema,
|
|
651
|
+
migration_owner,
|
|
652
|
+
target_schema_oid
|
|
653
|
+
);
|
|
654
|
+
END LOOP;
|
|
655
|
+
|
|
656
|
+
FOREACH table_name IN ARRAY ARRAY[
|
|
657
|
+
'sandbox_leases',
|
|
658
|
+
'sandbox_lease_holders',
|
|
659
|
+
'sandbox_retained_processes'
|
|
660
|
+
] LOOP
|
|
661
|
+
EXECUTE pg_catalog.format(
|
|
662
|
+
'CREATE POLICY session_tenancy_fenced_owner_write ON %I.%I '
|
|
663
|
+
|| 'FOR ALL USING ('
|
|
664
|
+
|| '%I.session_tenancy_fence_owner_policy_active('
|
|
665
|
+
|| 'current_user, %L, %s::oid, workspace_id, false)) '
|
|
666
|
+
|| 'WITH CHECK ('
|
|
667
|
+
|| '%I.session_tenancy_fence_owner_policy_active('
|
|
668
|
+
|| 'current_user, %L, %s::oid, workspace_id, false))',
|
|
669
|
+
target_schema,
|
|
670
|
+
table_name,
|
|
671
|
+
target_schema,
|
|
672
|
+
migration_owner,
|
|
673
|
+
target_schema_oid,
|
|
674
|
+
target_schema,
|
|
675
|
+
migration_owner,
|
|
676
|
+
target_schema_oid
|
|
677
|
+
);
|
|
678
|
+
END LOOP;
|
|
679
|
+
|
|
680
|
+
FOREACH table_name IN ARRAY ARRAY[
|
|
681
|
+
'sandbox_workspace_mutation_admissions',
|
|
682
|
+
'sessions',
|
|
683
|
+
'session_turns',
|
|
684
|
+
'session_turn_attempts',
|
|
685
|
+
'session_background_commands'
|
|
686
|
+
] LOOP
|
|
687
|
+
EXECUTE pg_catalog.format(
|
|
688
|
+
'CREATE POLICY session_tenancy_fenced_owner_read ON %I.%I '
|
|
689
|
+
|| 'FOR SELECT USING ('
|
|
690
|
+
|| '%I.session_tenancy_fence_owner_policy_active('
|
|
691
|
+
|| 'current_user, %L, %s::oid, workspace_id, false))',
|
|
692
|
+
target_schema,
|
|
693
|
+
table_name,
|
|
694
|
+
target_schema,
|
|
695
|
+
migration_owner,
|
|
696
|
+
target_schema_oid
|
|
697
|
+
);
|
|
698
|
+
END LOOP;
|
|
699
|
+
|
|
700
|
+
-- `session_visibility_isolation` is RESTRICTIVE, so a permissive owner
|
|
701
|
+
-- policy alone cannot expose private sessions to the bounded inventory or
|
|
702
|
+
-- to a fenced definer. Admit only this exact owner+schema+xact capability or
|
|
703
|
+
-- an already-held workspace fence, then retain the original subject rule.
|
|
704
|
+
EXECUTE pg_catalog.format(
|
|
705
|
+
'DROP POLICY IF EXISTS session_visibility_isolation ON %I.sessions',
|
|
706
|
+
target_schema
|
|
707
|
+
);
|
|
708
|
+
EXECUTE pg_catalog.format(
|
|
709
|
+
'CREATE POLICY session_visibility_isolation ON %1$I.sessions AS RESTRICTIVE '
|
|
710
|
+
|| 'FOR ALL USING ('
|
|
711
|
+
|| '%1$I.session_tenancy_fence_owner_policy_active('
|
|
712
|
+
|| 'current_user, %2$L, %3$s::oid, workspace_id, true) '
|
|
713
|
+
|| 'OR %1$I.session_tenancy_fence_owner_policy_active('
|
|
714
|
+
|| 'current_user, %2$L, %3$s::oid, workspace_id, false) '
|
|
715
|
+
|| 'OR nullif(pg_catalog.current_setting(''opengeni.subject_id'', true), '''') '
|
|
716
|
+
|| 'IS NULL OR visibility = ''workspace_shared'' '
|
|
717
|
+
|| 'OR %1$I.session_private_actor_visible('
|
|
718
|
+
|| 'account_id, workspace_id, owner_organization_membership_id, owner_subject_id)'
|
|
719
|
+
|| ') WITH CHECK ('
|
|
720
|
+
|| '%1$I.session_tenancy_fence_owner_policy_active('
|
|
721
|
+
|| 'current_user, %2$L, %3$s::oid, workspace_id, true) '
|
|
722
|
+
|| 'OR %1$I.session_tenancy_fence_owner_policy_active('
|
|
723
|
+
|| 'current_user, %2$L, %3$s::oid, workspace_id, false) '
|
|
724
|
+
|| 'OR nullif(pg_catalog.current_setting(''opengeni.subject_id'', true), '''') '
|
|
725
|
+
|| 'IS NULL OR visibility = ''workspace_shared'' '
|
|
726
|
+
|| 'OR %1$I.session_private_actor_visible('
|
|
727
|
+
|| 'account_id, workspace_id, owner_organization_membership_id, owner_subject_id)'
|
|
728
|
+
|| ')',
|
|
729
|
+
target_schema,
|
|
730
|
+
migration_owner,
|
|
731
|
+
target_schema_oid
|
|
732
|
+
);
|
|
733
|
+
END
|
|
734
|
+
$session_tenancy_fence_owner_policies$;
|
|
735
|
+
|
|
736
|
+
-- The lifecycle reaper is deliberately cross-workspace, but it still mutates
|
|
737
|
+
-- sandbox_lease_holders and therefore participates in the same quiescence
|
|
738
|
+
-- protocol. Acquire every currently active lease workspace in stable order
|
|
739
|
+
-- before the reaper locks any operation, lease, or holder row. These are
|
|
740
|
+
-- shared advisory locks: ordinary tenant writers continue concurrently, while
|
|
741
|
+
-- a fork/visibility transition waits only when its workspace has live holder
|
|
742
|
+
-- state that the global sweep may inspect or remove.
|
|
743
|
+
CREATE OR REPLACE FUNCTION acquire_sandbox_reaper_session_tenancy_fences()
|
|
744
|
+
RETURNS integer
|
|
745
|
+
LANGUAGE plpgsql
|
|
746
|
+
SECURITY DEFINER
|
|
747
|
+
SET search_path FROM CURRENT
|
|
748
|
+
AS $reaper_fences$
|
|
749
|
+
DECLARE
|
|
750
|
+
inventory_capability_id uuid;
|
|
751
|
+
workspace_id_value uuid;
|
|
752
|
+
locked_count integer := 0;
|
|
753
|
+
BEGIN
|
|
754
|
+
PERFORM pg_catalog.set_config('opengeni.sandbox_recovery_protocol_v2', '1', true);
|
|
755
|
+
inventory_capability_id :=
|
|
756
|
+
opengeni_private.open_session_tenancy_fence_inventory(
|
|
757
|
+
session_tenancy_fence_target_schema()
|
|
758
|
+
);
|
|
759
|
+
FOR workspace_id_value IN
|
|
760
|
+
SELECT DISTINCT lease.workspace_id
|
|
761
|
+
FROM sandbox_leases lease
|
|
762
|
+
WHERE lease.workspace_id IS NOT NULL
|
|
763
|
+
AND (
|
|
764
|
+
lease.liveness <> 'cold'
|
|
765
|
+
OR EXISTS (
|
|
766
|
+
SELECT 1 FROM sandbox_lease_holders holder
|
|
767
|
+
WHERE holder.lease_id = lease.id
|
|
768
|
+
)
|
|
769
|
+
)
|
|
770
|
+
ORDER BY lease.workspace_id
|
|
771
|
+
LOOP
|
|
772
|
+
PERFORM acquire_session_tenancy_fence(workspace_id_value);
|
|
773
|
+
locked_count := locked_count + 1;
|
|
774
|
+
END LOOP;
|
|
775
|
+
PERFORM opengeni_private.close_session_tenancy_fence_inventory(
|
|
776
|
+
inventory_capability_id
|
|
777
|
+
);
|
|
778
|
+
RETURN locked_count;
|
|
779
|
+
EXCEPTION WHEN OTHERS THEN
|
|
780
|
+
PERFORM opengeni_private.close_session_tenancy_fence_inventory(
|
|
781
|
+
inventory_capability_id
|
|
782
|
+
);
|
|
783
|
+
RAISE;
|
|
784
|
+
END
|
|
785
|
+
$reaper_fences$;
|
|
786
|
+
|
|
787
|
+
REVOKE ALL ON FUNCTION
|
|
788
|
+
acquire_sandbox_reaper_session_tenancy_fences()
|
|
789
|
+
FROM PUBLIC;
|
|
790
|
+
|
|
791
|
+
-- Routine-local entry points must not depend on a TypeScript wrapper having
|
|
792
|
+
-- opened the fence. This single-workspace helper is intentionally private so
|
|
793
|
+
-- direct app-executable SECURITY-DEFINER calls can enter the same shared
|
|
794
|
+
-- protocol after validating their scope and before taking their first row
|
|
795
|
+
-- lock or mutating a fenced table.
|
|
796
|
+
CREATE OR REPLACE FUNCTION acquire_session_tenancy_fence(
|
|
797
|
+
p_workspace_id uuid
|
|
798
|
+
)
|
|
799
|
+
RETURNS void
|
|
800
|
+
LANGUAGE plpgsql
|
|
801
|
+
SECURITY DEFINER
|
|
802
|
+
SET search_path = pg_catalog
|
|
803
|
+
AS $workspace_fence$
|
|
804
|
+
BEGIN
|
|
805
|
+
IF p_workspace_id IS NULL THEN
|
|
806
|
+
RAISE EXCEPTION 'session-tenancy fence requires a workspace'
|
|
807
|
+
USING ERRCODE = '22023';
|
|
808
|
+
END IF;
|
|
809
|
+
PERFORM pg_catalog.pg_advisory_xact_lock_shared(
|
|
810
|
+
pg_catalog.hashtextextended(
|
|
811
|
+
'session-tenancy:' || p_workspace_id::text,
|
|
812
|
+
0
|
|
813
|
+
)
|
|
814
|
+
);
|
|
815
|
+
END
|
|
816
|
+
$workspace_fence$;
|
|
817
|
+
|
|
818
|
+
REVOKE ALL ON FUNCTION
|
|
819
|
+
acquire_session_tenancy_fence(uuid)
|
|
820
|
+
FROM PUBLIC;
|
|
821
|
+
|
|
822
|
+
-- Global retained-process claiming is bounded by p_limit at mutation time,
|
|
823
|
+
-- but SKIP LOCKED means the exact batch cannot be known safely before its row
|
|
824
|
+
-- locks. Fence every currently due workspace, and no other workspace, in UUID
|
|
825
|
+
-- order before the claim CTE starts taking those locks.
|
|
826
|
+
CREATE OR REPLACE FUNCTION
|
|
827
|
+
acquire_due_retained_process_session_tenancy_fences()
|
|
828
|
+
RETURNS integer
|
|
829
|
+
LANGUAGE plpgsql
|
|
830
|
+
SECURITY DEFINER
|
|
831
|
+
SET search_path FROM CURRENT
|
|
832
|
+
AS $retained_process_fences$
|
|
833
|
+
DECLARE
|
|
834
|
+
inventory_capability_id uuid;
|
|
835
|
+
workspace_id_value uuid;
|
|
836
|
+
locked_count integer := 0;
|
|
837
|
+
BEGIN
|
|
838
|
+
PERFORM pg_catalog.set_config('opengeni.sandbox_recovery_protocol_v2', '1', true);
|
|
839
|
+
inventory_capability_id :=
|
|
840
|
+
opengeni_private.open_session_tenancy_fence_inventory(
|
|
841
|
+
session_tenancy_fence_target_schema()
|
|
842
|
+
);
|
|
843
|
+
FOR workspace_id_value IN
|
|
844
|
+
SELECT DISTINCT process.workspace_id
|
|
845
|
+
FROM sandbox_retained_processes process
|
|
846
|
+
WHERE process.workspace_id IS NOT NULL
|
|
847
|
+
AND process.state = 'active'
|
|
848
|
+
AND process.reconcile_after <= pg_catalog.now()
|
|
849
|
+
ORDER BY process.workspace_id
|
|
850
|
+
LOOP
|
|
851
|
+
PERFORM acquire_session_tenancy_fence(workspace_id_value);
|
|
852
|
+
locked_count := locked_count + 1;
|
|
853
|
+
END LOOP;
|
|
854
|
+
PERFORM opengeni_private.close_session_tenancy_fence_inventory(
|
|
855
|
+
inventory_capability_id
|
|
856
|
+
);
|
|
857
|
+
RETURN locked_count;
|
|
858
|
+
EXCEPTION WHEN OTHERS THEN
|
|
859
|
+
PERFORM opengeni_private.close_session_tenancy_fence_inventory(
|
|
860
|
+
inventory_capability_id
|
|
861
|
+
);
|
|
862
|
+
RAISE;
|
|
863
|
+
END
|
|
864
|
+
$retained_process_fences$;
|
|
865
|
+
|
|
866
|
+
REVOKE ALL ON FUNCTION
|
|
867
|
+
acquire_due_retained_process_session_tenancy_fences()
|
|
868
|
+
FROM PUBLIC;
|
|
869
|
+
|
|
870
|
+
-- Rotation deletes viewer holders only for due leases. Lock the distinct due
|
|
871
|
+
-- lease workspaces for that population, not every workspace with an unrelated
|
|
872
|
+
-- live lease.
|
|
873
|
+
CREATE OR REPLACE FUNCTION
|
|
874
|
+
acquire_due_sandbox_rotation_session_tenancy_fences(
|
|
875
|
+
p_lead_ms bigint
|
|
876
|
+
)
|
|
877
|
+
RETURNS integer
|
|
878
|
+
LANGUAGE plpgsql
|
|
879
|
+
SECURITY DEFINER
|
|
880
|
+
SET search_path FROM CURRENT
|
|
881
|
+
AS $rotation_fences$
|
|
882
|
+
DECLARE
|
|
883
|
+
inventory_capability_id uuid;
|
|
884
|
+
workspace_id_value uuid;
|
|
885
|
+
locked_count integer := 0;
|
|
886
|
+
BEGIN
|
|
887
|
+
PERFORM pg_catalog.set_config('opengeni.sandbox_recovery_protocol_v2', '1', true);
|
|
888
|
+
inventory_capability_id :=
|
|
889
|
+
opengeni_private.open_session_tenancy_fence_inventory(
|
|
890
|
+
session_tenancy_fence_target_schema()
|
|
891
|
+
);
|
|
892
|
+
FOR workspace_id_value IN
|
|
893
|
+
SELECT DISTINCT lease.workspace_id
|
|
894
|
+
FROM sandbox_leases lease
|
|
895
|
+
WHERE lease.workspace_id IS NOT NULL
|
|
896
|
+
AND lease.backend = 'modal'
|
|
897
|
+
AND lease.liveness IN ('warming', 'warm')
|
|
898
|
+
AND lease.provider_deadline_at IS NOT NULL
|
|
899
|
+
AND lease.provider_deadline_at
|
|
900
|
+
<= pg_catalog.now()
|
|
901
|
+
+ pg_catalog.make_interval(secs => p_lead_ms / 1000.0)
|
|
902
|
+
AND lease.rotation_requested_at IS NULL
|
|
903
|
+
AND (
|
|
904
|
+
lease.reaper_hold_id IS NULL
|
|
905
|
+
OR lease.reaper_hold_until <= pg_catalog.now()
|
|
906
|
+
)
|
|
907
|
+
ORDER BY lease.workspace_id
|
|
908
|
+
LOOP
|
|
909
|
+
PERFORM acquire_session_tenancy_fence(workspace_id_value);
|
|
910
|
+
locked_count := locked_count + 1;
|
|
911
|
+
END LOOP;
|
|
912
|
+
PERFORM opengeni_private.close_session_tenancy_fence_inventory(
|
|
913
|
+
inventory_capability_id
|
|
914
|
+
);
|
|
915
|
+
RETURN locked_count;
|
|
916
|
+
EXCEPTION WHEN OTHERS THEN
|
|
917
|
+
PERFORM opengeni_private.close_session_tenancy_fence_inventory(
|
|
918
|
+
inventory_capability_id
|
|
919
|
+
);
|
|
920
|
+
RAISE;
|
|
921
|
+
END
|
|
922
|
+
$rotation_fences$;
|
|
923
|
+
|
|
924
|
+
REVOKE ALL ON FUNCTION
|
|
925
|
+
acquire_due_sandbox_rotation_session_tenancy_fences(bigint)
|
|
926
|
+
FROM PUBLIC;
|
|
927
|
+
|
|
928
|
+
-- One connected machine may back sessions in several workspaces. Discover
|
|
929
|
+
-- the exact update population without row locks, then acquire every workspace
|
|
930
|
+
-- fence in deterministic order after the detach routine opens its bounded
|
|
931
|
+
-- write capability but before it locks or mutates dependent-session state.
|
|
932
|
+
CREATE OR REPLACE FUNCTION
|
|
933
|
+
acquire_scoped_machine_session_tenancy_fences(
|
|
934
|
+
p_account_id uuid,
|
|
935
|
+
p_sandbox_id uuid
|
|
936
|
+
)
|
|
937
|
+
RETURNS integer
|
|
938
|
+
LANGUAGE plpgsql
|
|
939
|
+
SECURITY DEFINER
|
|
940
|
+
SET search_path FROM CURRENT
|
|
941
|
+
AS $machine_fences$
|
|
942
|
+
DECLARE
|
|
943
|
+
inventory_capability_id uuid;
|
|
944
|
+
workspace_id_value uuid;
|
|
945
|
+
locked_count integer := 0;
|
|
946
|
+
BEGIN
|
|
947
|
+
IF p_account_id IS NULL OR p_sandbox_id IS NULL THEN
|
|
948
|
+
RAISE EXCEPTION 'machine session-tenancy fences require complete scope'
|
|
949
|
+
USING ERRCODE = '22023';
|
|
950
|
+
END IF;
|
|
951
|
+
inventory_capability_id :=
|
|
952
|
+
opengeni_private.open_session_tenancy_fence_inventory(
|
|
953
|
+
session_tenancy_fence_target_schema()
|
|
954
|
+
);
|
|
955
|
+
FOR workspace_id_value IN
|
|
956
|
+
SELECT DISTINCT session.workspace_id
|
|
957
|
+
FROM sessions session
|
|
958
|
+
WHERE session.account_id = p_account_id
|
|
959
|
+
AND (
|
|
960
|
+
session.active_sandbox_id = p_sandbox_id
|
|
961
|
+
OR (
|
|
962
|
+
session.sandbox_group_id = p_sandbox_id
|
|
963
|
+
AND session.sandbox_backend = 'selfhosted'
|
|
964
|
+
)
|
|
965
|
+
)
|
|
966
|
+
ORDER BY session.workspace_id
|
|
967
|
+
LOOP
|
|
968
|
+
PERFORM acquire_session_tenancy_fence(workspace_id_value);
|
|
969
|
+
locked_count := locked_count + 1;
|
|
970
|
+
END LOOP;
|
|
971
|
+
PERFORM opengeni_private.close_session_tenancy_fence_inventory(
|
|
972
|
+
inventory_capability_id
|
|
973
|
+
);
|
|
974
|
+
RETURN locked_count;
|
|
975
|
+
EXCEPTION WHEN OTHERS THEN
|
|
976
|
+
PERFORM opengeni_private.close_session_tenancy_fence_inventory(
|
|
977
|
+
inventory_capability_id
|
|
978
|
+
);
|
|
979
|
+
RAISE;
|
|
980
|
+
END
|
|
981
|
+
$machine_fences$;
|
|
982
|
+
|
|
983
|
+
REVOKE ALL ON FUNCTION
|
|
984
|
+
acquire_scoped_machine_session_tenancy_fences(uuid, uuid)
|
|
985
|
+
FROM PUBLIC;
|
|
986
|
+
|
|
987
|
+
-- Both shipped global reapers are SECURITY-DEFINER routines that take row
|
|
988
|
+
-- locks internally. Inject the shared-fence entry at the start of their exact
|
|
989
|
+
-- frozen definitions so every call path, including a direct SQL invocation,
|
|
990
|
+
-- enters the protocol before those row locks. Refuse definition drift instead
|
|
991
|
+
-- of silently producing an unfenced partial repair.
|
|
992
|
+
DO $repair_global_sandbox_reapers$
|
|
993
|
+
DECLARE
|
|
994
|
+
interaction_definition text;
|
|
995
|
+
lease_definition text;
|
|
996
|
+
interaction_prefix constant text :=
|
|
997
|
+
E' IF p_interaction_holder_ttl_ms <= 0 THEN\n'
|
|
998
|
+
|| E' RETURN 0;\n'
|
|
999
|
+
|| E' END IF;';
|
|
1000
|
+
lease_prefix constant text := E' BEGIN\n PERFORM pg_catalog.set_config(''opengeni.sandbox_recovery_protocol_v2'', ''1'', true);';
|
|
1001
|
+
fenced_prefix constant text :=
|
|
1002
|
+
E' PERFORM acquire_sandbox_reaper_session_tenancy_fences();';
|
|
1003
|
+
BEGIN
|
|
1004
|
+
SELECT pg_catalog.pg_get_functiondef(
|
|
1005
|
+
'opengeni_private.reap_stale_interaction_transitions(bigint)'::regprocedure
|
|
1006
|
+
) INTO interaction_definition;
|
|
1007
|
+
IF pg_catalog.strpos(interaction_definition, interaction_prefix) = 0
|
|
1008
|
+
OR pg_catalog.strpos(
|
|
1009
|
+
pg_catalog.substr(
|
|
1010
|
+
interaction_definition,
|
|
1011
|
+
pg_catalog.strpos(interaction_definition, interaction_prefix)
|
|
1012
|
+
+ pg_catalog.length(interaction_prefix)
|
|
1013
|
+
),
|
|
1014
|
+
interaction_prefix
|
|
1015
|
+
) > 0
|
|
1016
|
+
THEN
|
|
1017
|
+
RAISE EXCEPTION '0345 interaction reaper definition drift'
|
|
1018
|
+
USING ERRCODE = '55000';
|
|
1019
|
+
END IF;
|
|
1020
|
+
|
|
1021
|
+
EXECUTE pg_catalog.replace(
|
|
1022
|
+
interaction_definition,
|
|
1023
|
+
interaction_prefix,
|
|
1024
|
+
interaction_prefix || E'\n\n' || fenced_prefix
|
|
1025
|
+
);
|
|
1026
|
+
|
|
1027
|
+
SELECT pg_catalog.pg_get_functiondef(
|
|
1028
|
+
'opengeni_private.reap_sandbox_leases(bigint,bigint,bigint,bigint)'::regprocedure
|
|
1029
|
+
) INTO lease_definition;
|
|
1030
|
+
IF pg_catalog.strpos(lease_definition, lease_prefix) = 0
|
|
1031
|
+
OR pg_catalog.strpos(
|
|
1032
|
+
pg_catalog.substr(
|
|
1033
|
+
lease_definition,
|
|
1034
|
+
pg_catalog.strpos(lease_definition, lease_prefix)
|
|
1035
|
+
+ pg_catalog.length(lease_prefix)
|
|
1036
|
+
),
|
|
1037
|
+
lease_prefix
|
|
1038
|
+
) > 0
|
|
1039
|
+
THEN
|
|
1040
|
+
RAISE EXCEPTION '0345 lease reaper definition drift'
|
|
1041
|
+
USING ERRCODE = '55000';
|
|
1042
|
+
END IF;
|
|
1043
|
+
|
|
1044
|
+
EXECUTE pg_catalog.replace(
|
|
1045
|
+
lease_definition,
|
|
1046
|
+
lease_prefix,
|
|
1047
|
+
E' BEGIN\n' || fenced_prefix || E'\n'
|
|
1048
|
+
|| E' PERFORM pg_catalog.set_config(''opengeni.sandbox_recovery_protocol_v2'', ''1'', true);'
|
|
1049
|
+
);
|
|
1050
|
+
END
|
|
1051
|
+
$repair_global_sandbox_reapers$;
|
|
1052
|
+
|
|
1053
|
+
-- The retained-process claim and provider-deadline rotation sweeps are also
|
|
1054
|
+
-- global SECURITY-DEFINER mutators. Their TypeScript callers do not provide a
|
|
1055
|
+
-- workspace scope, so repair the installed SQL definitions themselves with
|
|
1056
|
+
-- population-specific, deterministically ordered fence helpers.
|
|
1057
|
+
DO $repair_global_background_session_tenancy_fences$
|
|
1058
|
+
DECLARE
|
|
1059
|
+
definition text;
|
|
1060
|
+
patched text;
|
|
1061
|
+
occurrences integer;
|
|
1062
|
+
protocol_prefix constant text :=
|
|
1063
|
+
E' PERFORM pg_catalog.set_config(''opengeni.sandbox_recovery_protocol_v2'', ''1'', true);';
|
|
1064
|
+
BEGIN
|
|
1065
|
+
definition := pg_catalog.pg_get_functiondef(
|
|
1066
|
+
'opengeni_private.claim_terminal_retained_processes(uuid,integer,bigint)'
|
|
1067
|
+
::regprocedure
|
|
1068
|
+
);
|
|
1069
|
+
occurrences := (
|
|
1070
|
+
pg_catalog.length(definition)
|
|
1071
|
+
- pg_catalog.length(pg_catalog.replace(definition, protocol_prefix, ''))
|
|
1072
|
+
) / pg_catalog.length(protocol_prefix);
|
|
1073
|
+
IF occurrences <> 1
|
|
1074
|
+
OR pg_catalog.strpos(
|
|
1075
|
+
definition,
|
|
1076
|
+
'acquire_due_retained_process_session_tenancy_fences'
|
|
1077
|
+
) > 0
|
|
1078
|
+
THEN
|
|
1079
|
+
RAISE EXCEPTION '0345 retained-process claim definition drift'
|
|
1080
|
+
USING ERRCODE = '55000';
|
|
1081
|
+
END IF;
|
|
1082
|
+
patched := pg_catalog.replace(
|
|
1083
|
+
definition,
|
|
1084
|
+
protocol_prefix,
|
|
1085
|
+
E' PERFORM acquire_due_retained_process_session_tenancy_fences();\n\n'
|
|
1086
|
+
|| protocol_prefix
|
|
1087
|
+
);
|
|
1088
|
+
EXECUTE patched;
|
|
1089
|
+
|
|
1090
|
+
definition := pg_catalog.pg_get_functiondef(
|
|
1091
|
+
'opengeni_private.request_due_sandbox_rotations(bigint,integer)'
|
|
1092
|
+
::regprocedure
|
|
1093
|
+
);
|
|
1094
|
+
occurrences := (
|
|
1095
|
+
pg_catalog.length(definition)
|
|
1096
|
+
- pg_catalog.length(pg_catalog.replace(definition, protocol_prefix, ''))
|
|
1097
|
+
) / pg_catalog.length(protocol_prefix);
|
|
1098
|
+
IF occurrences <> 1
|
|
1099
|
+
OR pg_catalog.strpos(
|
|
1100
|
+
definition,
|
|
1101
|
+
'acquire_due_sandbox_rotation_session_tenancy_fences'
|
|
1102
|
+
) > 0
|
|
1103
|
+
THEN
|
|
1104
|
+
RAISE EXCEPTION '0345 due sandbox rotation definition drift'
|
|
1105
|
+
USING ERRCODE = '55000';
|
|
1106
|
+
END IF;
|
|
1107
|
+
patched := pg_catalog.replace(
|
|
1108
|
+
definition,
|
|
1109
|
+
protocol_prefix,
|
|
1110
|
+
E' PERFORM acquire_due_sandbox_rotation_session_tenancy_fences(\n'
|
|
1111
|
+
|| E' p_lead_ms\n'
|
|
1112
|
+
|| E' );\n\n'
|
|
1113
|
+
|| protocol_prefix
|
|
1114
|
+
);
|
|
1115
|
+
EXECUTE patched;
|
|
1116
|
+
END
|
|
1117
|
+
$repair_global_background_session_tenancy_fences$;
|
|
1118
|
+
|
|
1119
|
+
-- Organization membership suspension/offboarding is account-wide: the
|
|
1120
|
+
-- preparation seam and both layers of the command can inspect or mutate
|
|
1121
|
+
-- session state in every workspace owned by the organization. Enter every
|
|
1122
|
+
-- workspace's shared fence in stable order immediately after the existing
|
|
1123
|
+
-- organization-membership advisory prefix and before any of those routines
|
|
1124
|
+
-- take row locks. Keeping this in their SECURITY-DEFINER SQL path also covers
|
|
1125
|
+
-- direct command invocations rather than relying on one TypeScript caller.
|
|
1126
|
+
CREATE OR REPLACE FUNCTION
|
|
1127
|
+
acquire_organization_session_tenancy_fences(
|
|
1128
|
+
p_account_id uuid
|
|
1129
|
+
)
|
|
1130
|
+
RETURNS integer
|
|
1131
|
+
LANGUAGE plpgsql
|
|
1132
|
+
SECURITY DEFINER
|
|
1133
|
+
SET search_path FROM CURRENT
|
|
1134
|
+
AS $membership_fences$
|
|
1135
|
+
DECLARE
|
|
1136
|
+
workspace_id_value uuid;
|
|
1137
|
+
locked_count integer := 0;
|
|
1138
|
+
BEGIN
|
|
1139
|
+
IF p_account_id IS NULL THEN
|
|
1140
|
+
RAISE EXCEPTION 'organization session-tenancy fence requires an organization'
|
|
1141
|
+
USING ERRCODE = '22023';
|
|
1142
|
+
END IF;
|
|
1143
|
+
PERFORM pg_catalog.pg_advisory_xact_lock(
|
|
1144
|
+
pg_catalog.hashtextextended(
|
|
1145
|
+
'organization-membership:' || p_account_id::text,
|
|
1146
|
+
0
|
|
1147
|
+
)
|
|
1148
|
+
);
|
|
1149
|
+
FOR workspace_id_value IN
|
|
1150
|
+
SELECT workspace.id
|
|
1151
|
+
FROM workspaces workspace
|
|
1152
|
+
WHERE workspace.account_id = p_account_id
|
|
1153
|
+
ORDER BY workspace.id
|
|
1154
|
+
LOOP
|
|
1155
|
+
PERFORM acquire_session_tenancy_fence(workspace_id_value);
|
|
1156
|
+
locked_count := locked_count + 1;
|
|
1157
|
+
END LOOP;
|
|
1158
|
+
RETURN locked_count;
|
|
1159
|
+
END
|
|
1160
|
+
$membership_fences$;
|
|
1161
|
+
|
|
1162
|
+
REVOKE ALL ON FUNCTION
|
|
1163
|
+
acquire_organization_session_tenancy_fences(uuid)
|
|
1164
|
+
FROM PUBLIC;
|
|
1165
|
+
|
|
1166
|
+
-- Migration 0299 installed the organization advisory prefix in all three
|
|
1167
|
+
-- lifecycle routines by exact definition repair. Extend that same prefix with
|
|
1168
|
+
-- the workspace fences, again refusing any definition drift instead of
|
|
1169
|
+
-- silently leaving one privileged mutation path outside the protocol.
|
|
1170
|
+
DO $repair_organization_membership_session_tenancy_fences$
|
|
1171
|
+
DECLARE
|
|
1172
|
+
signature text;
|
|
1173
|
+
definition text;
|
|
1174
|
+
patched text;
|
|
1175
|
+
occurrences integer;
|
|
1176
|
+
organization_prefix constant text :=
|
|
1177
|
+
E' PERFORM pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(\n'
|
|
1178
|
+
|| E' ''organization-membership:'' || account_id_value::text, 0\n'
|
|
1179
|
+
|| E' ));';
|
|
1180
|
+
fenced_prefix constant text := organization_prefix
|
|
1181
|
+
|| E'\n PERFORM acquire_organization_session_tenancy_fences(\n'
|
|
1182
|
+
|| E' account_id_value\n'
|
|
1183
|
+
|| E' );';
|
|
1184
|
+
BEGIN
|
|
1185
|
+
FOREACH signature IN ARRAY ARRAY[
|
|
1186
|
+
'prepare_organization_membership_protocol_settlements(jsonb)',
|
|
1187
|
+
'organization_membership_command_0263(jsonb)',
|
|
1188
|
+
'organization_membership_command(jsonb)'
|
|
1189
|
+
] LOOP
|
|
1190
|
+
definition := pg_catalog.pg_get_functiondef(
|
|
1191
|
+
pg_catalog.to_regprocedure(
|
|
1192
|
+
pg_catalog.quote_ident(pg_catalog.current_schema()) || '.' || signature
|
|
1193
|
+
)
|
|
1194
|
+
);
|
|
1195
|
+
IF definition IS NULL THEN
|
|
1196
|
+
RAISE EXCEPTION
|
|
1197
|
+
'0345 organization membership lifecycle function % is missing', signature
|
|
1198
|
+
USING ERRCODE = '55000';
|
|
1199
|
+
END IF;
|
|
1200
|
+
occurrences := (
|
|
1201
|
+
pg_catalog.length(definition)
|
|
1202
|
+
- pg_catalog.length(
|
|
1203
|
+
pg_catalog.replace(definition, organization_prefix, '')
|
|
1204
|
+
)
|
|
1205
|
+
) / pg_catalog.length(organization_prefix);
|
|
1206
|
+
IF occurrences <> 1
|
|
1207
|
+
OR pg_catalog.strpos(
|
|
1208
|
+
definition,
|
|
1209
|
+
'acquire_organization_session_tenancy_fences'
|
|
1210
|
+
) > 0
|
|
1211
|
+
THEN
|
|
1212
|
+
RAISE EXCEPTION
|
|
1213
|
+
'0345 organization membership session-tenancy prefix drift for %',
|
|
1214
|
+
signature USING ERRCODE = '55000';
|
|
1215
|
+
END IF;
|
|
1216
|
+
patched := pg_catalog.replace(
|
|
1217
|
+
definition,
|
|
1218
|
+
organization_prefix,
|
|
1219
|
+
fenced_prefix
|
|
1220
|
+
);
|
|
1221
|
+
IF pg_catalog.strpos(patched, fenced_prefix) = 0
|
|
1222
|
+
OR pg_catalog.strpos(patched, organization_prefix)
|
|
1223
|
+
> pg_catalog.strpos(patched, fenced_prefix)
|
|
1224
|
+
THEN
|
|
1225
|
+
RAISE EXCEPTION
|
|
1226
|
+
'0345 organization membership session-tenancy repair failed for %',
|
|
1227
|
+
signature USING ERRCODE = '55000';
|
|
1228
|
+
END IF;
|
|
1229
|
+
EXECUTE patched;
|
|
1230
|
+
END LOOP;
|
|
1231
|
+
END
|
|
1232
|
+
$repair_organization_membership_session_tenancy_fences$;
|
|
1233
|
+
|
|
1234
|
+
DO $repair_organization_retention_session_tenancy_fence$
|
|
1235
|
+
DECLARE
|
|
1236
|
+
definition text;
|
|
1237
|
+
patched text;
|
|
1238
|
+
occurrences integer;
|
|
1239
|
+
authority_prefix constant text :=
|
|
1240
|
+
E' PERFORM opengeni_private.assert_organization_retention_account(p_account_id);';
|
|
1241
|
+
fenced_prefix constant text := authority_prefix
|
|
1242
|
+
|| E'\n PERFORM acquire_organization_session_tenancy_fences(\n'
|
|
1243
|
+
|| E' p_account_id\n'
|
|
1244
|
+
|| E' );';
|
|
1245
|
+
BEGIN
|
|
1246
|
+
definition := pg_catalog.pg_get_functiondef(
|
|
1247
|
+
pg_catalog.to_regprocedure(
|
|
1248
|
+
pg_catalog.quote_ident(pg_catalog.current_schema())
|
|
1249
|
+
|| '.finalize_organization_retention_deletion(uuid,uuid,uuid,text)'
|
|
1250
|
+
)
|
|
1251
|
+
);
|
|
1252
|
+
IF definition IS NULL THEN
|
|
1253
|
+
RAISE EXCEPTION '0345 organization retention finalizer is missing'
|
|
1254
|
+
USING ERRCODE = '55000';
|
|
1255
|
+
END IF;
|
|
1256
|
+
occurrences := (
|
|
1257
|
+
pg_catalog.length(definition)
|
|
1258
|
+
- pg_catalog.length(
|
|
1259
|
+
pg_catalog.replace(definition, authority_prefix, '')
|
|
1260
|
+
)
|
|
1261
|
+
) / pg_catalog.length(authority_prefix);
|
|
1262
|
+
IF occurrences <> 1
|
|
1263
|
+
OR pg_catalog.strpos(
|
|
1264
|
+
definition,
|
|
1265
|
+
'acquire_organization_session_tenancy_fences'
|
|
1266
|
+
) > 0
|
|
1267
|
+
THEN
|
|
1268
|
+
RAISE EXCEPTION '0345 organization retention session-tenancy prefix drift'
|
|
1269
|
+
USING ERRCODE = '55000';
|
|
1270
|
+
END IF;
|
|
1271
|
+
patched := pg_catalog.replace(
|
|
1272
|
+
definition,
|
|
1273
|
+
authority_prefix,
|
|
1274
|
+
fenced_prefix
|
|
1275
|
+
);
|
|
1276
|
+
IF pg_catalog.strpos(patched, fenced_prefix) = 0 THEN
|
|
1277
|
+
RAISE EXCEPTION '0345 organization retention session-tenancy repair failed'
|
|
1278
|
+
USING ERRCODE = '55000';
|
|
1279
|
+
END IF;
|
|
1280
|
+
EXECUTE patched;
|
|
1281
|
+
END
|
|
1282
|
+
$repair_organization_retention_session_tenancy_fence$;
|
|
1283
|
+
|
|
1284
|
+
-- Finish the direct-call cutover for every app-executable SECURITY-DEFINER
|
|
1285
|
+
-- routine that writes one of the fenced tables. Each repair anchors on one
|
|
1286
|
+
-- exact frozen source fragment and aborts the migration on definition drift.
|
|
1287
|
+
-- The fence is inside the routine, after authority/argument validation where
|
|
1288
|
+
-- available, and before its first row lock or protected-table mutation.
|
|
1289
|
+
DO $repair_app_mutator_session_tenancy_fences$
|
|
1290
|
+
DECLARE
|
|
1291
|
+
definition text;
|
|
1292
|
+
patched text;
|
|
1293
|
+
occurrences integer;
|
|
1294
|
+
anchor text;
|
|
1295
|
+
replacement text;
|
|
1296
|
+
target regprocedure;
|
|
1297
|
+
BEGIN
|
|
1298
|
+
target := pg_catalog.to_regprocedure(
|
|
1299
|
+
pg_catalog.quote_ident(pg_catalog.current_schema())
|
|
1300
|
+
|| '.backfill_organization_session_ownership(uuid,integer,boolean,text)'
|
|
1301
|
+
);
|
|
1302
|
+
definition := pg_catalog.pg_get_functiondef(target);
|
|
1303
|
+
anchor := E' ledger_available := p_run_key IS NOT NULL';
|
|
1304
|
+
replacement :=
|
|
1305
|
+
E' IF NOT p_dry_run THEN\n'
|
|
1306
|
+
|| E' PERFORM acquire_organization_session_tenancy_fences(\n'
|
|
1307
|
+
|| E' p_organization_id\n'
|
|
1308
|
+
|| E' );\n'
|
|
1309
|
+
|| E' END IF;\n\n'
|
|
1310
|
+
|| anchor;
|
|
1311
|
+
occurrences := (
|
|
1312
|
+
pg_catalog.length(definition)
|
|
1313
|
+
- pg_catalog.length(pg_catalog.replace(definition, anchor, ''))
|
|
1314
|
+
) / pg_catalog.length(anchor);
|
|
1315
|
+
IF target IS NULL OR occurrences <> 1
|
|
1316
|
+
OR pg_catalog.strpos(
|
|
1317
|
+
definition,
|
|
1318
|
+
'acquire_organization_session_tenancy_fences'
|
|
1319
|
+
) > 0
|
|
1320
|
+
THEN
|
|
1321
|
+
RAISE EXCEPTION '0345 session ownership backfill definition drift'
|
|
1322
|
+
USING ERRCODE = '55000';
|
|
1323
|
+
END IF;
|
|
1324
|
+
patched := pg_catalog.replace(definition, anchor, replacement);
|
|
1325
|
+
EXECUTE patched;
|
|
1326
|
+
|
|
1327
|
+
target := pg_catalog.to_regprocedure(
|
|
1328
|
+
pg_catalog.quote_ident(pg_catalog.current_schema())
|
|
1329
|
+
|| '.detach_scoped_machine_dependent_sessions(uuid,uuid,uuid)'
|
|
1330
|
+
);
|
|
1331
|
+
definition := pg_catalog.pg_get_functiondef(target);
|
|
1332
|
+
anchor :=
|
|
1333
|
+
E' INSERT INTO opengeni_private.scoped_compute_capabilities(\n'
|
|
1334
|
+
|| E' backend_pid, transaction_id, capability_kind\n'
|
|
1335
|
+
|| E' ) VALUES (pg_catalog.pg_backend_pid(), pg_catalog.pg_current_xact_id(), ''write'')\n'
|
|
1336
|
+
|| E' ON CONFLICT DO NOTHING;';
|
|
1337
|
+
replacement :=
|
|
1338
|
+
anchor
|
|
1339
|
+
|| E'\n PERFORM acquire_scoped_machine_session_tenancy_fences(\n'
|
|
1340
|
+
|| E' p_account_id, p_sandbox_id\n'
|
|
1341
|
+
|| E' );';
|
|
1342
|
+
occurrences := (
|
|
1343
|
+
pg_catalog.length(definition)
|
|
1344
|
+
- pg_catalog.length(pg_catalog.replace(definition, anchor, ''))
|
|
1345
|
+
) / pg_catalog.length(anchor);
|
|
1346
|
+
IF target IS NULL OR occurrences <> 1
|
|
1347
|
+
OR pg_catalog.strpos(
|
|
1348
|
+
definition,
|
|
1349
|
+
'acquire_scoped_machine_session_tenancy_fences'
|
|
1350
|
+
) > 0
|
|
1351
|
+
THEN
|
|
1352
|
+
RAISE EXCEPTION '0345 scoped machine detach definition drift'
|
|
1353
|
+
USING ERRCODE = '55000';
|
|
1354
|
+
END IF;
|
|
1355
|
+
patched := pg_catalog.replace(definition, anchor, replacement);
|
|
1356
|
+
EXECUTE patched;
|
|
1357
|
+
|
|
1358
|
+
target := pg_catalog.to_regprocedure(
|
|
1359
|
+
pg_catalog.quote_ident(pg_catalog.current_schema())
|
|
1360
|
+
|| '.accept_turn_personal_resource_attachment(uuid,uuid,uuid,uuid,text,integer,boolean,integer)'
|
|
1361
|
+
);
|
|
1362
|
+
definition := pg_catalog.pg_get_functiondef(target);
|
|
1363
|
+
anchor :=
|
|
1364
|
+
E' RAISE EXCEPTION ''session tenancy product is not activated'' USING ERRCODE = ''42501'';\n'
|
|
1365
|
+
|| E' END IF;';
|
|
1366
|
+
replacement :=
|
|
1367
|
+
anchor
|
|
1368
|
+
|| E'\n\n PERFORM acquire_session_tenancy_fence(p_workspace_id);';
|
|
1369
|
+
occurrences := (
|
|
1370
|
+
pg_catalog.length(definition)
|
|
1371
|
+
- pg_catalog.length(pg_catalog.replace(definition, anchor, ''))
|
|
1372
|
+
) / pg_catalog.length(anchor);
|
|
1373
|
+
IF target IS NULL OR occurrences <> 1
|
|
1374
|
+
OR pg_catalog.strpos(definition, 'acquire_session_tenancy_fence') > 0
|
|
1375
|
+
THEN
|
|
1376
|
+
RAISE EXCEPTION '0345 accepted-turn attachment definition drift'
|
|
1377
|
+
USING ERRCODE = '55000';
|
|
1378
|
+
END IF;
|
|
1379
|
+
patched := pg_catalog.replace(definition, anchor, replacement);
|
|
1380
|
+
EXECUTE patched;
|
|
1381
|
+
|
|
1382
|
+
target := pg_catalog.to_regprocedure(
|
|
1383
|
+
pg_catalog.quote_ident(pg_catalog.current_schema())
|
|
1384
|
+
|| '.materialize_scheduled_task_reusable_session_from_run('
|
|
1385
|
+
|| 'uuid,uuid,uuid,uuid,uuid,bigint,text)'
|
|
1386
|
+
);
|
|
1387
|
+
definition := pg_catalog.pg_get_functiondef(target);
|
|
1388
|
+
anchor :=
|
|
1389
|
+
E' IF p_source_revision <= 0 OR p_source_digest !~ ''^[0-9a-f]{64}$'' THEN';
|
|
1390
|
+
replacement :=
|
|
1391
|
+
E' PERFORM acquire_session_tenancy_fence(p_workspace_id);\n\n'
|
|
1392
|
+
|| anchor;
|
|
1393
|
+
occurrences := (
|
|
1394
|
+
pg_catalog.length(definition)
|
|
1395
|
+
- pg_catalog.length(pg_catalog.replace(definition, anchor, ''))
|
|
1396
|
+
) / pg_catalog.length(anchor);
|
|
1397
|
+
IF target IS NULL OR occurrences <> 1
|
|
1398
|
+
OR pg_catalog.strpos(definition, 'acquire_session_tenancy_fence') > 0
|
|
1399
|
+
THEN
|
|
1400
|
+
RAISE EXCEPTION '0345 scheduled reusable-session materializer definition drift'
|
|
1401
|
+
USING ERRCODE = '55000';
|
|
1402
|
+
END IF;
|
|
1403
|
+
patched := pg_catalog.replace(definition, anchor, replacement);
|
|
1404
|
+
EXECUTE patched;
|
|
1405
|
+
|
|
1406
|
+
target := pg_catalog.to_regprocedure(
|
|
1407
|
+
pg_catalog.quote_ident(pg_catalog.current_schema())
|
|
1408
|
+
|| '.workspace_membership_removal_command(jsonb)'
|
|
1409
|
+
);
|
|
1410
|
+
definition := pg_catalog.pg_get_functiondef(target);
|
|
1411
|
+
anchor := E' input_hash_value := pg_catalog.encode(';
|
|
1412
|
+
replacement :=
|
|
1413
|
+
E' PERFORM pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(\n'
|
|
1414
|
+
|| E' ''organization-membership:'' || account_id_value::text, 0\n'
|
|
1415
|
+
|| E' ));\n'
|
|
1416
|
+
|| E' PERFORM acquire_session_tenancy_fence(workspace_id_value);\n\n'
|
|
1417
|
+
|| anchor;
|
|
1418
|
+
occurrences := (
|
|
1419
|
+
pg_catalog.length(definition)
|
|
1420
|
+
- pg_catalog.length(pg_catalog.replace(definition, anchor, ''))
|
|
1421
|
+
) / pg_catalog.length(anchor);
|
|
1422
|
+
IF target IS NULL OR occurrences <> 1
|
|
1423
|
+
OR pg_catalog.strpos(definition, 'acquire_session_tenancy_fence') > 0
|
|
1424
|
+
THEN
|
|
1425
|
+
RAISE EXCEPTION '0345 workspace membership removal definition drift'
|
|
1426
|
+
USING ERRCODE = '55000';
|
|
1427
|
+
END IF;
|
|
1428
|
+
patched := pg_catalog.replace(definition, anchor, replacement);
|
|
1429
|
+
EXECUTE patched;
|
|
1430
|
+
END
|
|
1431
|
+
$repair_app_mutator_session_tenancy_fences$;
|
|
1432
|
+
|
|
1433
|
+
-- A held advisory lock is necessary but not sufficient for the owner-only RLS
|
|
1434
|
+
-- branch. These five routines are the paths that need owner access after their
|
|
1435
|
+
-- bounded inventory helper returns. Give each invocation its own fenced-access
|
|
1436
|
+
-- token after the locks are acquired, and delete that exact token on every
|
|
1437
|
+
-- normal or exceptional exit. Refuse all frozen-definition drift.
|
|
1438
|
+
DO $repair_owner_fenced_access_scopes$
|
|
1439
|
+
DECLARE
|
|
1440
|
+
definition text;
|
|
1441
|
+
patched text;
|
|
1442
|
+
anchor text;
|
|
1443
|
+
replacement text;
|
|
1444
|
+
tail_anchor text;
|
|
1445
|
+
tail_replacement text;
|
|
1446
|
+
before_tail text;
|
|
1447
|
+
occurrences integer;
|
|
1448
|
+
BEGIN
|
|
1449
|
+
definition := pg_catalog.pg_get_functiondef(
|
|
1450
|
+
'opengeni_private.reap_stale_interaction_transitions(bigint)'::regprocedure
|
|
1451
|
+
);
|
|
1452
|
+
anchor := E' settled_count integer := 0;';
|
|
1453
|
+
replacement := anchor || E'\n fenced_access_capability_id uuid;';
|
|
1454
|
+
occurrences := (
|
|
1455
|
+
pg_catalog.length(definition)
|
|
1456
|
+
- pg_catalog.length(pg_catalog.replace(definition, anchor, ''))
|
|
1457
|
+
) / pg_catalog.length(anchor);
|
|
1458
|
+
IF occurrences <> 1
|
|
1459
|
+
OR pg_catalog.strpos(definition, 'fenced_access_capability_id') > 0
|
|
1460
|
+
THEN
|
|
1461
|
+
RAISE EXCEPTION '0345 interaction reaper fenced-access definition drift'
|
|
1462
|
+
USING ERRCODE = '55000';
|
|
1463
|
+
END IF;
|
|
1464
|
+
patched := pg_catalog.replace(definition, anchor, replacement);
|
|
1465
|
+
anchor := E' PERFORM acquire_sandbox_reaper_session_tenancy_fences();';
|
|
1466
|
+
replacement := anchor
|
|
1467
|
+
|| E'\n fenced_access_capability_id :='
|
|
1468
|
+
|| E'\n opengeni_private.open_session_tenancy_fenced_access('
|
|
1469
|
+
|| E'\n session_tenancy_fence_target_schema()'
|
|
1470
|
+
|| E'\n );';
|
|
1471
|
+
occurrences := (
|
|
1472
|
+
pg_catalog.length(patched)
|
|
1473
|
+
- pg_catalog.length(pg_catalog.replace(patched, anchor, ''))
|
|
1474
|
+
) / pg_catalog.length(anchor);
|
|
1475
|
+
IF occurrences <> 1 THEN
|
|
1476
|
+
RAISE EXCEPTION '0345 interaction reaper fenced-access open anchor drift'
|
|
1477
|
+
USING ERRCODE = '55000';
|
|
1478
|
+
END IF;
|
|
1479
|
+
patched := pg_catalog.replace(patched, anchor, replacement);
|
|
1480
|
+
anchor := E' RETURN settled_count;';
|
|
1481
|
+
replacement :=
|
|
1482
|
+
E' PERFORM opengeni_private.close_session_tenancy_fenced_access('
|
|
1483
|
+
|| E'\n fenced_access_capability_id'
|
|
1484
|
+
|| E'\n );\n'
|
|
1485
|
+
|| anchor;
|
|
1486
|
+
occurrences := (
|
|
1487
|
+
pg_catalog.length(patched)
|
|
1488
|
+
- pg_catalog.length(pg_catalog.replace(patched, anchor, ''))
|
|
1489
|
+
) / pg_catalog.length(anchor);
|
|
1490
|
+
IF occurrences <> 1 THEN
|
|
1491
|
+
RAISE EXCEPTION '0345 interaction reaper fenced-access normal-close anchor drift'
|
|
1492
|
+
USING ERRCODE = '55000';
|
|
1493
|
+
END IF;
|
|
1494
|
+
patched := pg_catalog.replace(patched, anchor, replacement);
|
|
1495
|
+
tail_anchor := E' END;\n $function$\n';
|
|
1496
|
+
tail_replacement :=
|
|
1497
|
+
E' EXCEPTION WHEN OTHERS THEN\n'
|
|
1498
|
+
|| E' PERFORM opengeni_private.close_session_tenancy_fenced_access(\n'
|
|
1499
|
+
|| E' fenced_access_capability_id\n'
|
|
1500
|
+
|| E' );\n'
|
|
1501
|
+
|| E' RAISE;\n'
|
|
1502
|
+
|| E' END;\n'
|
|
1503
|
+
|| E' $function$\n';
|
|
1504
|
+
occurrences := (
|
|
1505
|
+
pg_catalog.length(patched)
|
|
1506
|
+
- pg_catalog.length(pg_catalog.replace(patched, tail_anchor, ''))
|
|
1507
|
+
) / pg_catalog.length(tail_anchor);
|
|
1508
|
+
IF occurrences <> 1
|
|
1509
|
+
OR pg_catalog.right(patched, pg_catalog.length(tail_anchor))
|
|
1510
|
+
IS DISTINCT FROM tail_anchor
|
|
1511
|
+
THEN
|
|
1512
|
+
RAISE EXCEPTION '0345 interaction reaper fenced-access tail drift'
|
|
1513
|
+
USING ERRCODE = '55000';
|
|
1514
|
+
END IF;
|
|
1515
|
+
before_tail := patched;
|
|
1516
|
+
patched := pg_catalog.regexp_replace(
|
|
1517
|
+
patched,
|
|
1518
|
+
E' END;\\n \\$function\\$\\n$',
|
|
1519
|
+
tail_replacement
|
|
1520
|
+
);
|
|
1521
|
+
occurrences := (
|
|
1522
|
+
pg_catalog.length(patched)
|
|
1523
|
+
- pg_catalog.length(pg_catalog.replace(
|
|
1524
|
+
patched,
|
|
1525
|
+
'close_session_tenancy_fenced_access',
|
|
1526
|
+
''
|
|
1527
|
+
))
|
|
1528
|
+
) / pg_catalog.length('close_session_tenancy_fenced_access');
|
|
1529
|
+
IF patched IS NOT DISTINCT FROM before_tail
|
|
1530
|
+
OR pg_catalog.right(patched, pg_catalog.length(tail_replacement))
|
|
1531
|
+
IS DISTINCT FROM tail_replacement
|
|
1532
|
+
OR pg_catalog.strpos(patched, 'fenced_access_capability_id uuid;') = 0
|
|
1533
|
+
OR pg_catalog.strpos(patched, 'open_session_tenancy_fenced_access') = 0
|
|
1534
|
+
OR occurrences <> 2
|
|
1535
|
+
THEN
|
|
1536
|
+
RAISE EXCEPTION '0345 interaction reaper fenced-access repair failed'
|
|
1537
|
+
USING ERRCODE = '55000';
|
|
1538
|
+
END IF;
|
|
1539
|
+
EXECUTE patched;
|
|
1540
|
+
|
|
1541
|
+
definition := pg_catalog.pg_get_functiondef(
|
|
1542
|
+
'opengeni_private.reap_sandbox_leases(bigint,bigint,bigint,bigint)'
|
|
1543
|
+
::regprocedure
|
|
1544
|
+
);
|
|
1545
|
+
anchor := E' DECLARE\n locked_ids uuid[];';
|
|
1546
|
+
replacement := E' DECLARE\n fenced_access_capability_id uuid;\n'
|
|
1547
|
+
|| E' locked_ids uuid[];';
|
|
1548
|
+
occurrences := (
|
|
1549
|
+
pg_catalog.length(definition)
|
|
1550
|
+
- pg_catalog.length(pg_catalog.replace(definition, anchor, ''))
|
|
1551
|
+
) / pg_catalog.length(anchor);
|
|
1552
|
+
IF occurrences <> 1
|
|
1553
|
+
OR pg_catalog.strpos(definition, 'fenced_access_capability_id') > 0
|
|
1554
|
+
THEN
|
|
1555
|
+
RAISE EXCEPTION '0345 lease reaper fenced-access definition drift'
|
|
1556
|
+
USING ERRCODE = '55000';
|
|
1557
|
+
END IF;
|
|
1558
|
+
patched := pg_catalog.replace(definition, anchor, replacement);
|
|
1559
|
+
anchor := E' PERFORM acquire_sandbox_reaper_session_tenancy_fences();';
|
|
1560
|
+
replacement := anchor
|
|
1561
|
+
|| E'\n fenced_access_capability_id :='
|
|
1562
|
+
|| E'\n opengeni_private.open_session_tenancy_fenced_access('
|
|
1563
|
+
|| E'\n session_tenancy_fence_target_schema()'
|
|
1564
|
+
|| E'\n );';
|
|
1565
|
+
occurrences := (
|
|
1566
|
+
pg_catalog.length(patched)
|
|
1567
|
+
- pg_catalog.length(pg_catalog.replace(patched, anchor, ''))
|
|
1568
|
+
) / pg_catalog.length(anchor);
|
|
1569
|
+
IF occurrences <> 1 THEN
|
|
1570
|
+
RAISE EXCEPTION '0345 lease reaper fenced-access open anchor drift'
|
|
1571
|
+
USING ERRCODE = '55000';
|
|
1572
|
+
END IF;
|
|
1573
|
+
patched := pg_catalog.replace(patched, anchor, replacement);
|
|
1574
|
+
anchor :=
|
|
1575
|
+
E' IF pg_catalog.cardinality(locked_ids) = 0 THEN\n'
|
|
1576
|
+
|| E' RETURN;\n'
|
|
1577
|
+
|| E' END IF;';
|
|
1578
|
+
replacement :=
|
|
1579
|
+
E' IF pg_catalog.cardinality(locked_ids) = 0 THEN\n'
|
|
1580
|
+
|| E' PERFORM opengeni_private.close_session_tenancy_fenced_access(\n'
|
|
1581
|
+
|| E' fenced_access_capability_id\n'
|
|
1582
|
+
|| E' );\n'
|
|
1583
|
+
|| E' RETURN;\n'
|
|
1584
|
+
|| E' END IF;';
|
|
1585
|
+
occurrences := (
|
|
1586
|
+
pg_catalog.length(patched)
|
|
1587
|
+
- pg_catalog.length(pg_catalog.replace(patched, anchor, ''))
|
|
1588
|
+
) / pg_catalog.length(anchor);
|
|
1589
|
+
IF occurrences <> 1 THEN
|
|
1590
|
+
RAISE EXCEPTION '0345 lease reaper fenced-access early-close anchor drift'
|
|
1591
|
+
USING ERRCODE = '55000';
|
|
1592
|
+
END IF;
|
|
1593
|
+
patched := pg_catalog.replace(patched, anchor, replacement);
|
|
1594
|
+
tail_anchor := E' END;\n $function$\n';
|
|
1595
|
+
tail_replacement :=
|
|
1596
|
+
E' PERFORM opengeni_private.close_session_tenancy_fenced_access(\n'
|
|
1597
|
+
|| E' fenced_access_capability_id\n'
|
|
1598
|
+
|| E' );\n'
|
|
1599
|
+
|| E' EXCEPTION WHEN OTHERS THEN\n'
|
|
1600
|
+
|| E' PERFORM opengeni_private.close_session_tenancy_fenced_access(\n'
|
|
1601
|
+
|| E' fenced_access_capability_id\n'
|
|
1602
|
+
|| E' );\n'
|
|
1603
|
+
|| E' RAISE;\n'
|
|
1604
|
+
|| E' END;\n'
|
|
1605
|
+
|| E' $function$\n';
|
|
1606
|
+
occurrences := (
|
|
1607
|
+
pg_catalog.length(patched)
|
|
1608
|
+
- pg_catalog.length(pg_catalog.replace(patched, tail_anchor, ''))
|
|
1609
|
+
) / pg_catalog.length(tail_anchor);
|
|
1610
|
+
IF occurrences <> 1
|
|
1611
|
+
OR pg_catalog.right(patched, pg_catalog.length(tail_anchor))
|
|
1612
|
+
IS DISTINCT FROM tail_anchor
|
|
1613
|
+
THEN
|
|
1614
|
+
RAISE EXCEPTION '0345 lease reaper fenced-access tail drift'
|
|
1615
|
+
USING ERRCODE = '55000';
|
|
1616
|
+
END IF;
|
|
1617
|
+
before_tail := patched;
|
|
1618
|
+
patched := pg_catalog.regexp_replace(
|
|
1619
|
+
patched,
|
|
1620
|
+
E' END;\\n \\$function\\$\\n$',
|
|
1621
|
+
tail_replacement
|
|
1622
|
+
);
|
|
1623
|
+
occurrences := (
|
|
1624
|
+
pg_catalog.length(patched)
|
|
1625
|
+
- pg_catalog.length(pg_catalog.replace(
|
|
1626
|
+
patched,
|
|
1627
|
+
'close_session_tenancy_fenced_access',
|
|
1628
|
+
''
|
|
1629
|
+
))
|
|
1630
|
+
) / pg_catalog.length('close_session_tenancy_fenced_access');
|
|
1631
|
+
IF patched IS NOT DISTINCT FROM before_tail
|
|
1632
|
+
OR pg_catalog.right(patched, pg_catalog.length(tail_replacement))
|
|
1633
|
+
IS DISTINCT FROM tail_replacement
|
|
1634
|
+
OR pg_catalog.strpos(patched, 'fenced_access_capability_id uuid;') = 0
|
|
1635
|
+
OR pg_catalog.strpos(patched, 'open_session_tenancy_fenced_access') = 0
|
|
1636
|
+
OR occurrences <> 3
|
|
1637
|
+
THEN
|
|
1638
|
+
RAISE EXCEPTION '0345 lease reaper fenced-access repair failed'
|
|
1639
|
+
USING ERRCODE = '55000';
|
|
1640
|
+
END IF;
|
|
1641
|
+
EXECUTE patched;
|
|
1642
|
+
|
|
1643
|
+
definition := pg_catalog.pg_get_functiondef(
|
|
1644
|
+
'opengeni_private.request_due_sandbox_rotations(bigint,integer)'
|
|
1645
|
+
::regprocedure
|
|
1646
|
+
);
|
|
1647
|
+
anchor := E' DECLARE\n requested integer;';
|
|
1648
|
+
replacement := E' DECLARE\n fenced_access_capability_id uuid;\n'
|
|
1649
|
+
|| E' requested integer;';
|
|
1650
|
+
occurrences := (
|
|
1651
|
+
pg_catalog.length(definition)
|
|
1652
|
+
- pg_catalog.length(pg_catalog.replace(definition, anchor, ''))
|
|
1653
|
+
) / pg_catalog.length(anchor);
|
|
1654
|
+
IF occurrences <> 1
|
|
1655
|
+
OR pg_catalog.strpos(definition, 'fenced_access_capability_id') > 0
|
|
1656
|
+
THEN
|
|
1657
|
+
RAISE EXCEPTION '0345 rotation fenced-access definition drift'
|
|
1658
|
+
USING ERRCODE = '55000';
|
|
1659
|
+
END IF;
|
|
1660
|
+
patched := pg_catalog.replace(definition, anchor, replacement);
|
|
1661
|
+
anchor :=
|
|
1662
|
+
E' PERFORM acquire_due_sandbox_rotation_session_tenancy_fences(\n'
|
|
1663
|
+
|| E' p_lead_ms\n'
|
|
1664
|
+
|| E' );';
|
|
1665
|
+
replacement := anchor
|
|
1666
|
+
|| E'\n fenced_access_capability_id :='
|
|
1667
|
+
|| E'\n opengeni_private.open_session_tenancy_fenced_access('
|
|
1668
|
+
|| E'\n session_tenancy_fence_target_schema()'
|
|
1669
|
+
|| E'\n );';
|
|
1670
|
+
occurrences := (
|
|
1671
|
+
pg_catalog.length(patched)
|
|
1672
|
+
- pg_catalog.length(pg_catalog.replace(patched, anchor, ''))
|
|
1673
|
+
) / pg_catalog.length(anchor);
|
|
1674
|
+
IF occurrences <> 1 THEN
|
|
1675
|
+
RAISE EXCEPTION '0345 rotation fenced-access open anchor drift'
|
|
1676
|
+
USING ERRCODE = '55000';
|
|
1677
|
+
END IF;
|
|
1678
|
+
patched := pg_catalog.replace(patched, anchor, replacement);
|
|
1679
|
+
anchor := E' RETURN requested;';
|
|
1680
|
+
replacement :=
|
|
1681
|
+
E' PERFORM opengeni_private.close_session_tenancy_fenced_access(\n'
|
|
1682
|
+
|| E' fenced_access_capability_id\n'
|
|
1683
|
+
|| E' );\n'
|
|
1684
|
+
|| anchor;
|
|
1685
|
+
occurrences := (
|
|
1686
|
+
pg_catalog.length(patched)
|
|
1687
|
+
- pg_catalog.length(pg_catalog.replace(patched, anchor, ''))
|
|
1688
|
+
) / pg_catalog.length(anchor);
|
|
1689
|
+
IF occurrences <> 1 THEN
|
|
1690
|
+
RAISE EXCEPTION '0345 rotation fenced-access normal-close anchor drift'
|
|
1691
|
+
USING ERRCODE = '55000';
|
|
1692
|
+
END IF;
|
|
1693
|
+
patched := pg_catalog.replace(patched, anchor, replacement);
|
|
1694
|
+
tail_anchor := E' END;\n $function$\n';
|
|
1695
|
+
tail_replacement :=
|
|
1696
|
+
E' EXCEPTION WHEN OTHERS THEN\n'
|
|
1697
|
+
|| E' PERFORM opengeni_private.close_session_tenancy_fenced_access(\n'
|
|
1698
|
+
|| E' fenced_access_capability_id\n'
|
|
1699
|
+
|| E' );\n'
|
|
1700
|
+
|| E' RAISE;\n'
|
|
1701
|
+
|| E' END;\n'
|
|
1702
|
+
|| E' $function$\n';
|
|
1703
|
+
occurrences := (
|
|
1704
|
+
pg_catalog.length(patched)
|
|
1705
|
+
- pg_catalog.length(pg_catalog.replace(patched, tail_anchor, ''))
|
|
1706
|
+
) / pg_catalog.length(tail_anchor);
|
|
1707
|
+
IF occurrences <> 1
|
|
1708
|
+
OR pg_catalog.right(patched, pg_catalog.length(tail_anchor))
|
|
1709
|
+
IS DISTINCT FROM tail_anchor
|
|
1710
|
+
THEN
|
|
1711
|
+
RAISE EXCEPTION '0345 rotation fenced-access tail drift'
|
|
1712
|
+
USING ERRCODE = '55000';
|
|
1713
|
+
END IF;
|
|
1714
|
+
before_tail := patched;
|
|
1715
|
+
patched := pg_catalog.regexp_replace(
|
|
1716
|
+
patched,
|
|
1717
|
+
E' END;\\n \\$function\\$\\n$',
|
|
1718
|
+
tail_replacement
|
|
1719
|
+
);
|
|
1720
|
+
occurrences := (
|
|
1721
|
+
pg_catalog.length(patched)
|
|
1722
|
+
- pg_catalog.length(pg_catalog.replace(
|
|
1723
|
+
patched,
|
|
1724
|
+
'close_session_tenancy_fenced_access',
|
|
1725
|
+
''
|
|
1726
|
+
))
|
|
1727
|
+
) / pg_catalog.length('close_session_tenancy_fenced_access');
|
|
1728
|
+
IF patched IS NOT DISTINCT FROM before_tail
|
|
1729
|
+
OR pg_catalog.right(patched, pg_catalog.length(tail_replacement))
|
|
1730
|
+
IS DISTINCT FROM tail_replacement
|
|
1731
|
+
OR pg_catalog.strpos(patched, 'fenced_access_capability_id uuid;') = 0
|
|
1732
|
+
OR pg_catalog.strpos(patched, 'open_session_tenancy_fenced_access') = 0
|
|
1733
|
+
OR occurrences <> 2
|
|
1734
|
+
THEN
|
|
1735
|
+
RAISE EXCEPTION '0345 rotation fenced-access repair failed'
|
|
1736
|
+
USING ERRCODE = '55000';
|
|
1737
|
+
END IF;
|
|
1738
|
+
EXECUTE patched;
|
|
1739
|
+
|
|
1740
|
+
definition := pg_catalog.pg_get_functiondef(
|
|
1741
|
+
'opengeni_private.claim_terminal_retained_processes(uuid,integer,bigint)'
|
|
1742
|
+
::regprocedure
|
|
1743
|
+
);
|
|
1744
|
+
anchor := E' BEGIN\n IF p_limit < 1 OR p_limit > 100 THEN';
|
|
1745
|
+
replacement :=
|
|
1746
|
+
E' DECLARE\n'
|
|
1747
|
+
|| E' fenced_access_capability_id uuid;\n'
|
|
1748
|
+
|| E' BEGIN\n'
|
|
1749
|
+
|| E' IF p_limit < 1 OR p_limit > 100 THEN';
|
|
1750
|
+
occurrences := (
|
|
1751
|
+
pg_catalog.length(definition)
|
|
1752
|
+
- pg_catalog.length(pg_catalog.replace(definition, anchor, ''))
|
|
1753
|
+
) / pg_catalog.length(anchor);
|
|
1754
|
+
IF occurrences <> 1
|
|
1755
|
+
OR pg_catalog.strpos(definition, 'fenced_access_capability_id') > 0
|
|
1756
|
+
THEN
|
|
1757
|
+
RAISE EXCEPTION '0345 retained-process fenced-access definition drift'
|
|
1758
|
+
USING ERRCODE = '55000';
|
|
1759
|
+
END IF;
|
|
1760
|
+
patched := pg_catalog.replace(definition, anchor, replacement);
|
|
1761
|
+
anchor := E' PERFORM acquire_due_retained_process_session_tenancy_fences();';
|
|
1762
|
+
replacement := anchor
|
|
1763
|
+
|| E'\n fenced_access_capability_id :='
|
|
1764
|
+
|| E'\n opengeni_private.open_session_tenancy_fenced_access('
|
|
1765
|
+
|| E'\n session_tenancy_fence_target_schema()'
|
|
1766
|
+
|| E'\n );';
|
|
1767
|
+
occurrences := (
|
|
1768
|
+
pg_catalog.length(patched)
|
|
1769
|
+
- pg_catalog.length(pg_catalog.replace(patched, anchor, ''))
|
|
1770
|
+
) / pg_catalog.length(anchor);
|
|
1771
|
+
IF occurrences <> 1 THEN
|
|
1772
|
+
RAISE EXCEPTION '0345 retained-process fenced-access open anchor drift'
|
|
1773
|
+
USING ERRCODE = '55000';
|
|
1774
|
+
END IF;
|
|
1775
|
+
patched := pg_catalog.replace(patched, anchor, replacement);
|
|
1776
|
+
anchor := E' SET CONSTRAINTS '
|
|
1777
|
+
|| pg_catalog.quote_ident(pg_catalog.current_schema())
|
|
1778
|
+
|| E'.sandbox_retained_processes_identity_v2 DEFERRED;';
|
|
1779
|
+
replacement := anchor
|
|
1780
|
+
|| E'\n PERFORM opengeni_private.close_session_tenancy_fenced_access('
|
|
1781
|
+
|| E'\n fenced_access_capability_id'
|
|
1782
|
+
|| E'\n );';
|
|
1783
|
+
occurrences := (
|
|
1784
|
+
pg_catalog.length(patched)
|
|
1785
|
+
- pg_catalog.length(pg_catalog.replace(patched, anchor, ''))
|
|
1786
|
+
) / pg_catalog.length(anchor);
|
|
1787
|
+
IF occurrences <> 1 THEN
|
|
1788
|
+
RAISE EXCEPTION '0345 retained-process fenced-access normal-close anchor drift'
|
|
1789
|
+
USING ERRCODE = '55000';
|
|
1790
|
+
END IF;
|
|
1791
|
+
patched := pg_catalog.replace(patched, anchor, replacement);
|
|
1792
|
+
tail_anchor := E' END;\n $function$\n';
|
|
1793
|
+
tail_replacement :=
|
|
1794
|
+
E' EXCEPTION WHEN OTHERS THEN\n'
|
|
1795
|
+
|| E' PERFORM opengeni_private.close_session_tenancy_fenced_access(\n'
|
|
1796
|
+
|| E' fenced_access_capability_id\n'
|
|
1797
|
+
|| E' );\n'
|
|
1798
|
+
|| E' RAISE;\n'
|
|
1799
|
+
|| E' END;\n'
|
|
1800
|
+
|| E' $function$\n';
|
|
1801
|
+
occurrences := (
|
|
1802
|
+
pg_catalog.length(patched)
|
|
1803
|
+
- pg_catalog.length(pg_catalog.replace(patched, tail_anchor, ''))
|
|
1804
|
+
) / pg_catalog.length(tail_anchor);
|
|
1805
|
+
IF occurrences <> 1
|
|
1806
|
+
OR pg_catalog.right(patched, pg_catalog.length(tail_anchor))
|
|
1807
|
+
IS DISTINCT FROM tail_anchor
|
|
1808
|
+
THEN
|
|
1809
|
+
RAISE EXCEPTION '0345 retained-process fenced-access tail drift'
|
|
1810
|
+
USING ERRCODE = '55000';
|
|
1811
|
+
END IF;
|
|
1812
|
+
before_tail := patched;
|
|
1813
|
+
patched := pg_catalog.regexp_replace(
|
|
1814
|
+
patched,
|
|
1815
|
+
E' END;\\n \\$function\\$\\n$',
|
|
1816
|
+
tail_replacement
|
|
1817
|
+
);
|
|
1818
|
+
occurrences := (
|
|
1819
|
+
pg_catalog.length(patched)
|
|
1820
|
+
- pg_catalog.length(pg_catalog.replace(
|
|
1821
|
+
patched,
|
|
1822
|
+
'close_session_tenancy_fenced_access',
|
|
1823
|
+
''
|
|
1824
|
+
))
|
|
1825
|
+
) / pg_catalog.length('close_session_tenancy_fenced_access');
|
|
1826
|
+
IF patched IS NOT DISTINCT FROM before_tail
|
|
1827
|
+
OR pg_catalog.right(patched, pg_catalog.length(tail_replacement))
|
|
1828
|
+
IS DISTINCT FROM tail_replacement
|
|
1829
|
+
OR pg_catalog.strpos(patched, 'fenced_access_capability_id uuid;') = 0
|
|
1830
|
+
OR pg_catalog.strpos(patched, 'open_session_tenancy_fenced_access') = 0
|
|
1831
|
+
OR occurrences <> 2
|
|
1832
|
+
THEN
|
|
1833
|
+
RAISE EXCEPTION '0345 retained-process fenced-access repair failed'
|
|
1834
|
+
USING ERRCODE = '55000';
|
|
1835
|
+
END IF;
|
|
1836
|
+
EXECUTE patched;
|
|
1837
|
+
|
|
1838
|
+
definition := pg_catalog.pg_get_functiondef(
|
|
1839
|
+
pg_catalog.to_regprocedure(
|
|
1840
|
+
pg_catalog.quote_ident(pg_catalog.current_schema())
|
|
1841
|
+
|| '.detach_scoped_machine_dependent_sessions(uuid,uuid,uuid)'
|
|
1842
|
+
)
|
|
1843
|
+
);
|
|
1844
|
+
anchor := E'DECLARE\n dependent_workspace_id uuid;';
|
|
1845
|
+
replacement := E'DECLARE\n fenced_access_capability_id uuid;\n'
|
|
1846
|
+
|| E' dependent_workspace_id uuid;';
|
|
1847
|
+
occurrences := (
|
|
1848
|
+
pg_catalog.length(definition)
|
|
1849
|
+
- pg_catalog.length(pg_catalog.replace(definition, anchor, ''))
|
|
1850
|
+
) / pg_catalog.length(anchor);
|
|
1851
|
+
IF occurrences <> 1
|
|
1852
|
+
OR pg_catalog.strpos(definition, 'fenced_access_capability_id') > 0
|
|
1853
|
+
THEN
|
|
1854
|
+
RAISE EXCEPTION '0345 machine detach fenced-access definition drift'
|
|
1855
|
+
USING ERRCODE = '55000';
|
|
1856
|
+
END IF;
|
|
1857
|
+
patched := pg_catalog.replace(definition, anchor, replacement);
|
|
1858
|
+
anchor :=
|
|
1859
|
+
E' PERFORM acquire_scoped_machine_session_tenancy_fences(\n'
|
|
1860
|
+
|| E' p_account_id, p_sandbox_id\n'
|
|
1861
|
+
|| E' );';
|
|
1862
|
+
replacement := anchor
|
|
1863
|
+
|| E'\n fenced_access_capability_id :='
|
|
1864
|
+
|| E'\n opengeni_private.open_session_tenancy_fenced_access('
|
|
1865
|
+
|| E'\n session_tenancy_fence_target_schema()'
|
|
1866
|
+
|| E'\n );';
|
|
1867
|
+
occurrences := (
|
|
1868
|
+
pg_catalog.length(patched)
|
|
1869
|
+
- pg_catalog.length(pg_catalog.replace(patched, anchor, ''))
|
|
1870
|
+
) / pg_catalog.length(anchor);
|
|
1871
|
+
IF occurrences <> 1 THEN
|
|
1872
|
+
RAISE EXCEPTION '0345 machine detach fenced-access open anchor drift'
|
|
1873
|
+
USING ERRCODE = '55000';
|
|
1874
|
+
END IF;
|
|
1875
|
+
patched := pg_catalog.replace(patched, anchor, replacement);
|
|
1876
|
+
anchor :=
|
|
1877
|
+
E' PERFORM pg_catalog.set_config(\n'
|
|
1878
|
+
|| E' ''opengeni.session_activity_gate_workspace_id'', '
|
|
1879
|
+
|| E'p_origin_workspace_id::text, true\n'
|
|
1880
|
+
|| E' );\n'
|
|
1881
|
+
|| E' DELETE FROM opengeni_private.scoped_compute_capabilities\n'
|
|
1882
|
+
|| E' WHERE backend_pid = pg_catalog.pg_backend_pid()';
|
|
1883
|
+
replacement :=
|
|
1884
|
+
E' PERFORM pg_catalog.set_config(\n'
|
|
1885
|
+
|| E' ''opengeni.session_activity_gate_workspace_id'', '
|
|
1886
|
+
|| E'p_origin_workspace_id::text, true\n'
|
|
1887
|
+
|| E' );\n'
|
|
1888
|
+
|| E' PERFORM opengeni_private.close_session_tenancy_fenced_access(\n'
|
|
1889
|
+
|| E' fenced_access_capability_id\n'
|
|
1890
|
+
|| E' );\n'
|
|
1891
|
+
|| E' DELETE FROM opengeni_private.scoped_compute_capabilities\n'
|
|
1892
|
+
|| E' WHERE backend_pid = pg_catalog.pg_backend_pid()';
|
|
1893
|
+
occurrences := (
|
|
1894
|
+
pg_catalog.length(patched)
|
|
1895
|
+
- pg_catalog.length(pg_catalog.replace(patched, anchor, ''))
|
|
1896
|
+
) / pg_catalog.length(anchor);
|
|
1897
|
+
IF occurrences <> 1 THEN
|
|
1898
|
+
RAISE EXCEPTION '0345 machine detach fenced-access normal-close anchor drift'
|
|
1899
|
+
USING ERRCODE = '55000';
|
|
1900
|
+
END IF;
|
|
1901
|
+
patched := pg_catalog.replace(patched, anchor, replacement);
|
|
1902
|
+
anchor :=
|
|
1903
|
+
E'EXCEPTION WHEN OTHERS THEN\n'
|
|
1904
|
+
|| E' DELETE FROM opengeni_private.scoped_compute_capabilities\n'
|
|
1905
|
+
|| E' WHERE backend_pid = pg_catalog.pg_backend_pid()';
|
|
1906
|
+
replacement :=
|
|
1907
|
+
E'EXCEPTION WHEN OTHERS THEN\n'
|
|
1908
|
+
|| E' PERFORM opengeni_private.close_session_tenancy_fenced_access(\n'
|
|
1909
|
+
|| E' fenced_access_capability_id\n'
|
|
1910
|
+
|| E' );\n'
|
|
1911
|
+
|| E' DELETE FROM opengeni_private.scoped_compute_capabilities\n'
|
|
1912
|
+
|| E' WHERE backend_pid = pg_catalog.pg_backend_pid()';
|
|
1913
|
+
occurrences := (
|
|
1914
|
+
pg_catalog.length(patched)
|
|
1915
|
+
- pg_catalog.length(pg_catalog.replace(patched, anchor, ''))
|
|
1916
|
+
) / pg_catalog.length(anchor);
|
|
1917
|
+
IF occurrences <> 1 THEN
|
|
1918
|
+
RAISE EXCEPTION '0345 machine detach fenced-access exception-close anchor drift'
|
|
1919
|
+
USING ERRCODE = '55000';
|
|
1920
|
+
END IF;
|
|
1921
|
+
patched := pg_catalog.replace(patched, anchor, replacement);
|
|
1922
|
+
tail_anchor := E' RAISE;\nEND\n$function$\n';
|
|
1923
|
+
occurrences := (
|
|
1924
|
+
pg_catalog.length(patched)
|
|
1925
|
+
- pg_catalog.length(pg_catalog.replace(patched, tail_anchor, ''))
|
|
1926
|
+
) / pg_catalog.length(tail_anchor);
|
|
1927
|
+
IF occurrences <> 1
|
|
1928
|
+
OR pg_catalog.right(patched, pg_catalog.length(tail_anchor))
|
|
1929
|
+
IS DISTINCT FROM tail_anchor
|
|
1930
|
+
THEN
|
|
1931
|
+
RAISE EXCEPTION '0345 machine detach fenced-access tail drift'
|
|
1932
|
+
USING ERRCODE = '55000';
|
|
1933
|
+
END IF;
|
|
1934
|
+
occurrences := (
|
|
1935
|
+
pg_catalog.length(patched)
|
|
1936
|
+
- pg_catalog.length(pg_catalog.replace(
|
|
1937
|
+
patched,
|
|
1938
|
+
'close_session_tenancy_fenced_access',
|
|
1939
|
+
''
|
|
1940
|
+
))
|
|
1941
|
+
) / pg_catalog.length('close_session_tenancy_fenced_access');
|
|
1942
|
+
IF pg_catalog.strpos(patched, 'fenced_access_capability_id uuid;') = 0
|
|
1943
|
+
OR pg_catalog.strpos(patched, 'open_session_tenancy_fenced_access') = 0
|
|
1944
|
+
OR occurrences <> 2
|
|
1945
|
+
THEN
|
|
1946
|
+
RAISE EXCEPTION '0345 machine detach fenced-access repair failed'
|
|
1947
|
+
USING ERRCODE = '55000';
|
|
1948
|
+
END IF;
|
|
1949
|
+
EXECUTE patched;
|
|
1950
|
+
END
|
|
1951
|
+
$repair_owner_fenced_access_scopes$;
|
|
1952
|
+
|
|
1953
|
+
DO $install_session_tenancy_fences$
|
|
1954
|
+
DECLARE
|
|
1955
|
+
table_name text;
|
|
1956
|
+
hot_tables constant text[] := ARRAY[
|
|
1957
|
+
'sessions', 'session_turns', 'session_turn_attempts',
|
|
1958
|
+
'session_attempt_interruptions', 'session_system_updates',
|
|
1959
|
+
'session_human_input_requests', 'session_pending_tool_calls', 'agent_run_states',
|
|
1960
|
+
'session_goals', 'codex_capacity_waiters', 'xai_capacity_waiters',
|
|
1961
|
+
'session_realtime_modes', 'session_realtime_connections', 'scheduled_tasks',
|
|
1962
|
+
'sandbox_workspace_mutation_admissions', 'sandbox_retained_processes',
|
|
1963
|
+
'sandbox_lease_holders'
|
|
1964
|
+
];
|
|
1965
|
+
BEGIN
|
|
1966
|
+
FOREACH table_name IN ARRAY hot_tables LOOP
|
|
1967
|
+
EXECUTE format(
|
|
1968
|
+
'CREATE TRIGGER session_tenancy_workspace_fence '
|
|
1969
|
+
|| 'BEFORE INSERT OR UPDATE OR DELETE ON %I '
|
|
1970
|
+
|| 'FOR EACH ROW EXECUTE FUNCTION opengeni_private.require_session_tenancy_fence()',
|
|
1971
|
+
table_name
|
|
1972
|
+
);
|
|
1973
|
+
END LOOP;
|
|
1974
|
+
END
|
|
1975
|
+
$install_session_tenancy_fences$;
|
|
1976
|
+
|
|
1977
|
+
CREATE OR REPLACE FUNCTION assert_session_tenancy_quiescent(
|
|
1978
|
+
p_account_id uuid,
|
|
1979
|
+
p_workspace_id uuid,
|
|
1980
|
+
p_session_id uuid,
|
|
1981
|
+
p_require_singleton_group boolean
|
|
1982
|
+
) RETURNS void
|
|
1983
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
1984
|
+
SET search_path FROM CURRENT
|
|
1985
|
+
AS $$
|
|
1986
|
+
DECLARE
|
|
1987
|
+
session_group_id uuid;
|
|
1988
|
+
previous_subject text := current_setting('opengeni.subject_id', true);
|
|
1989
|
+
blocker text;
|
|
1990
|
+
BEGIN
|
|
1991
|
+
|
|
1992
|
+
SELECT sandbox_group_id INTO session_group_id FROM sessions
|
|
1993
|
+
WHERE account_id = p_account_id AND workspace_id = p_workspace_id
|
|
1994
|
+
AND id = p_session_id;
|
|
1995
|
+
|
|
1996
|
+
IF EXISTS (SELECT 1 FROM session_turns WHERE workspace_id = p_workspace_id
|
|
1997
|
+
AND session_id = p_session_id
|
|
1998
|
+
AND status IN ('queued','running','requires_action','recovering','waiting_capacity'))
|
|
1999
|
+
THEN blocker := 'nonterminal_turn';
|
|
2000
|
+
ELSIF EXISTS (SELECT 1 FROM session_turn_attempts WHERE workspace_id = p_workspace_id
|
|
2001
|
+
AND session_id = p_session_id AND state <> 'closed')
|
|
2002
|
+
THEN blocker := 'nonterminal_attempt';
|
|
2003
|
+
ELSIF EXISTS (SELECT 1 FROM session_attempt_interruptions WHERE workspace_id = p_workspace_id
|
|
2004
|
+
AND session_id = p_session_id AND state IN ('pending','delivered','acknowledged'))
|
|
2005
|
+
THEN blocker := 'unsettled_interruption';
|
|
2006
|
+
ELSIF EXISTS (SELECT 1 FROM session_system_updates WHERE workspace_id = p_workspace_id
|
|
2007
|
+
AND session_id = p_session_id AND state = 'pending')
|
|
2008
|
+
THEN blocker := 'pending_system_update';
|
|
2009
|
+
ELSIF EXISTS (SELECT 1 FROM session_human_input_requests WHERE workspace_id = p_workspace_id
|
|
2010
|
+
AND session_id = p_session_id AND status = 'pending')
|
|
2011
|
+
THEN blocker := 'pending_human_input';
|
|
2012
|
+
ELSIF EXISTS (SELECT 1 FROM session_pending_tool_calls WHERE workspace_id = p_workspace_id
|
|
2013
|
+
AND session_id = p_session_id)
|
|
2014
|
+
THEN blocker := 'pending_tool_receipt';
|
|
2015
|
+
ELSIF EXISTS (SELECT 1 FROM agent_run_states WHERE workspace_id = p_workspace_id
|
|
2016
|
+
AND session_id = p_session_id)
|
|
2017
|
+
THEN blocker := 'run_state';
|
|
2018
|
+
ELSIF EXISTS (SELECT 1 FROM session_goals WHERE workspace_id = p_workspace_id
|
|
2019
|
+
AND session_id = p_session_id AND status = 'active')
|
|
2020
|
+
THEN blocker := 'active_goal';
|
|
2021
|
+
ELSIF EXISTS (SELECT 1 FROM codex_capacity_waiters WHERE workspace_id = p_workspace_id
|
|
2022
|
+
AND session_id = p_session_id AND status = 'waiting')
|
|
2023
|
+
OR EXISTS (SELECT 1 FROM xai_capacity_waiters WHERE workspace_id = p_workspace_id
|
|
2024
|
+
AND session_id = p_session_id AND status = 'waiting')
|
|
2025
|
+
THEN blocker := 'capacity_waiter';
|
|
2026
|
+
ELSIF EXISTS (SELECT 1 FROM session_realtime_modes WHERE workspace_id = p_workspace_id
|
|
2027
|
+
AND session_id = p_session_id AND state = 'active')
|
|
2028
|
+
OR EXISTS (SELECT 1 FROM session_realtime_connections WHERE workspace_id = p_workspace_id
|
|
2029
|
+
AND session_id = p_session_id
|
|
2030
|
+
AND state IN ('negotiating','ready','active'))
|
|
2031
|
+
THEN blocker := 'active_realtime';
|
|
2032
|
+
ELSIF EXISTS (SELECT 1 FROM scheduled_tasks WHERE workspace_id = p_workspace_id
|
|
2033
|
+
AND reusable_session_id = p_session_id AND status = 'active')
|
|
2034
|
+
THEN blocker := 'active_scheduled_task';
|
|
2035
|
+
ELSIF EXISTS (SELECT 1 FROM sandbox_workspace_mutation_admissions
|
|
2036
|
+
WHERE workspace_id = p_workspace_id AND session_id = p_session_id
|
|
2037
|
+
AND settled_at IS NULL)
|
|
2038
|
+
THEN blocker := 'workspace_mutation_admission';
|
|
2039
|
+
ELSIF EXISTS (SELECT 1 FROM sandbox_retained_processes
|
|
2040
|
+
WHERE workspace_id = p_workspace_id AND session_id = p_session_id
|
|
2041
|
+
AND state = 'active')
|
|
2042
|
+
THEN blocker := 'retained_process';
|
|
2043
|
+
ELSIF EXISTS (
|
|
2044
|
+
SELECT 1 FROM sandbox_lease_holders holder
|
|
2045
|
+
JOIN sandbox_leases lease ON lease.id = holder.lease_id
|
|
2046
|
+
WHERE lease.workspace_id = p_workspace_id
|
|
2047
|
+
AND lease.sandbox_group_id = session_group_id
|
|
2048
|
+
AND holder.kind IN ('viewer', 'interaction')
|
|
2049
|
+
)
|
|
2050
|
+
THEN blocker := 'active_sandbox_access';
|
|
2051
|
+
END IF;
|
|
2052
|
+
|
|
2053
|
+
IF blocker IS NULL AND p_require_singleton_group THEN
|
|
2054
|
+
-- The caller's own actor-scoped RLS must not hide a sibling. The function
|
|
2055
|
+
-- has already locked and identified the authorized source session.
|
|
2056
|
+
PERFORM set_config('opengeni.subject_id', '', true);
|
|
2057
|
+
IF EXISTS (SELECT 1 FROM sessions sibling
|
|
2058
|
+
WHERE sibling.account_id = p_account_id
|
|
2059
|
+
AND sibling.workspace_id = p_workspace_id
|
|
2060
|
+
AND sibling.sandbox_group_id = session_group_id
|
|
2061
|
+
AND sibling.id <> p_session_id)
|
|
2062
|
+
THEN blocker := 'shared_sandbox_group'; END IF;
|
|
2063
|
+
PERFORM set_config(
|
|
2064
|
+
'opengeni.subject_id', CASE WHEN previous_subject IS NULL THEN '' ELSE previous_subject END, true
|
|
2065
|
+
);
|
|
2066
|
+
END IF;
|
|
2067
|
+
|
|
2068
|
+
IF blocker IS NOT NULL THEN
|
|
2069
|
+
RAISE EXCEPTION 'session tenancy mutation requires a quiescent session'
|
|
2070
|
+
USING ERRCODE = '55P03', DETAIL = blocker;
|
|
2071
|
+
END IF;
|
|
2072
|
+
EXCEPTION WHEN OTHERS THEN
|
|
2073
|
+
PERFORM set_config(
|
|
2074
|
+
'opengeni.subject_id', CASE WHEN previous_subject IS NULL THEN '' ELSE previous_subject END, true
|
|
2075
|
+
);
|
|
2076
|
+
RAISE;
|
|
2077
|
+
END
|
|
2078
|
+
$$;
|
|
2079
|
+
|
|
2080
|
+
CREATE OR REPLACE FUNCTION transition_session_visibility(
|
|
2081
|
+
p_account_id uuid,
|
|
2082
|
+
p_workspace_id uuid,
|
|
2083
|
+
p_session_id uuid,
|
|
2084
|
+
p_actor_subject_id text,
|
|
2085
|
+
p_target_visibility text,
|
|
2086
|
+
p_expected_authority_epoch integer,
|
|
2087
|
+
p_operation_key text,
|
|
2088
|
+
p_canonical_request_hash text,
|
|
2089
|
+
p_activation_version integer
|
|
2090
|
+
) RETURNS TABLE (
|
|
2091
|
+
operation_id uuid,
|
|
2092
|
+
event_id uuid,
|
|
2093
|
+
event_sequence integer,
|
|
2094
|
+
visibility text,
|
|
2095
|
+
authority_epoch integer,
|
|
2096
|
+
owner_organization_membership_id uuid,
|
|
2097
|
+
changed boolean,
|
|
2098
|
+
replay boolean,
|
|
2099
|
+
interrupted_attempt_count integer,
|
|
2100
|
+
cancelled_turn_count integer,
|
|
2101
|
+
cancelled_update_count integer,
|
|
2102
|
+
paused_goal_count integer,
|
|
2103
|
+
revoked_grant_count integer
|
|
2104
|
+
)
|
|
2105
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
2106
|
+
SET search_path FROM CURRENT
|
|
2107
|
+
AS $$
|
|
2108
|
+
DECLARE
|
|
2109
|
+
actor_membership organization_memberships%ROWTYPE;
|
|
2110
|
+
session_row sessions%ROWTYPE;
|
|
2111
|
+
receipt_row session_command_receipts%ROWTYPE;
|
|
2112
|
+
new_epoch integer;
|
|
2113
|
+
grant_count integer := 0;
|
|
2114
|
+
event_row_id uuid;
|
|
2115
|
+
event_row_sequence integer;
|
|
2116
|
+
visibility_write_capability_id uuid := gen_random_uuid();
|
|
2117
|
+
previous_visibility_capability text := current_setting(
|
|
2118
|
+
'opengeni.session_visibility_write_capability', true
|
|
2119
|
+
);
|
|
2120
|
+
previous_lifecycle text := current_setting('opengeni.organization_tenancy_lifecycle', true);
|
|
2121
|
+
BEGIN
|
|
2122
|
+
IF p_account_id IS NULL OR p_workspace_id IS NULL OR p_session_id IS NULL
|
|
2123
|
+
OR p_actor_subject_id IS NULL OR p_target_visibility IS NULL
|
|
2124
|
+
OR p_expected_authority_epoch IS NULL OR p_operation_key IS NULL
|
|
2125
|
+
OR p_canonical_request_hash IS NULL OR p_activation_version IS NULL
|
|
2126
|
+
THEN RAISE EXCEPTION 'session visibility transition requires complete authority'
|
|
2127
|
+
USING ERRCODE = '42501'; END IF;
|
|
2128
|
+
IF p_account_id IS DISTINCT FROM opengeni_private.current_account_id()
|
|
2129
|
+
OR p_workspace_id IS DISTINCT FROM opengeni_private.current_workspace_id()
|
|
2130
|
+
OR p_actor_subject_id IS DISTINCT FROM opengeni_private.current_subject_id()
|
|
2131
|
+
THEN RAISE EXCEPTION 'session visibility transition authority is invalid'
|
|
2132
|
+
USING ERRCODE = '42501'; END IF;
|
|
2133
|
+
IF p_target_visibility NOT IN ('user_private', 'workspace_shared')
|
|
2134
|
+
OR p_expected_authority_epoch < 1
|
|
2135
|
+
OR p_actor_subject_id <> btrim(p_actor_subject_id)
|
|
2136
|
+
OR length(p_actor_subject_id) NOT BETWEEN 1 AND 1024
|
|
2137
|
+
OR p_operation_key <> btrim(p_operation_key)
|
|
2138
|
+
OR length(p_operation_key) NOT BETWEEN 1 AND 1024
|
|
2139
|
+
OR p_canonical_request_hash !~ '^[0-9a-f]{64}$'
|
|
2140
|
+
OR p_activation_version <> 1
|
|
2141
|
+
THEN RAISE EXCEPTION 'session visibility transition request is invalid'
|
|
2142
|
+
USING ERRCODE = '22023'; END IF;
|
|
2143
|
+
IF NOT session_tenancy_product_activated(p_account_id, p_activation_version) THEN
|
|
2144
|
+
RAISE EXCEPTION 'session tenancy product surface is not activated for this organization'
|
|
2145
|
+
USING ERRCODE = '55000';
|
|
2146
|
+
END IF;
|
|
2147
|
+
|
|
2148
|
+
-- Match the canonical organization-membership lifecycle prefix before any
|
|
2149
|
+
-- table/row lock. This keeps visibility changes from reintroducing the
|
|
2150
|
+
-- workspace/account lock cycle repaired by migration 0299.
|
|
2151
|
+
PERFORM pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
|
|
2152
|
+
'organization-membership:' || p_account_id::text, 0
|
|
2153
|
+
));
|
|
2154
|
+
|
|
2155
|
+
PERFORM pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
|
|
2156
|
+
'session-tenancy:' || p_workspace_id::text, 0
|
|
2157
|
+
));
|
|
2158
|
+
|
|
2159
|
+
|
|
2160
|
+
PERFORM set_config('opengeni.organization_tenancy_lifecycle',
|
|
2161
|
+
'session_visibility_activation', true);
|
|
2162
|
+
PERFORM 1 FROM workspaces workspace_row
|
|
2163
|
+
WHERE workspace_row.id = p_workspace_id AND workspace_row.account_id = p_account_id
|
|
2164
|
+
FOR UPDATE;
|
|
2165
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'session visibility transition workspace is unavailable'
|
|
2166
|
+
USING ERRCODE = '42501'; END IF;
|
|
2167
|
+
|
|
2168
|
+
SELECT membership.* INTO actor_membership
|
|
2169
|
+
FROM organization_memberships membership
|
|
2170
|
+
WHERE membership.account_id = p_account_id
|
|
2171
|
+
AND membership.subject_id = p_actor_subject_id
|
|
2172
|
+
AND membership.status = 'active'
|
|
2173
|
+
FOR UPDATE;
|
|
2174
|
+
IF NOT FOUND OR NOT (
|
|
2175
|
+
actor_membership.personal_workspace_id = p_workspace_id
|
|
2176
|
+
OR EXISTS (
|
|
2177
|
+
SELECT 1 FROM workspace_memberships workspace_membership
|
|
2178
|
+
WHERE workspace_membership.account_id = p_account_id
|
|
2179
|
+
AND workspace_membership.workspace_id = p_workspace_id
|
|
2180
|
+
AND workspace_membership.subject_id = p_actor_subject_id
|
|
2181
|
+
)
|
|
2182
|
+
) THEN RAISE EXCEPTION 'session visibility transition requires active membership'
|
|
2183
|
+
USING ERRCODE = '42501'; END IF;
|
|
2184
|
+
|
|
2185
|
+
SELECT session.* INTO session_row FROM sessions session
|
|
2186
|
+
WHERE session.account_id = p_account_id AND session.workspace_id = p_workspace_id
|
|
2187
|
+
AND session.id = p_session_id
|
|
2188
|
+
FOR UPDATE;
|
|
2189
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'session visibility transition session is unavailable'
|
|
2190
|
+
USING ERRCODE = 'P0002'; END IF;
|
|
2191
|
+
IF session_row.owner_organization_membership_id IS DISTINCT FROM actor_membership.id
|
|
2192
|
+
OR session_row.owner_subject_id IS DISTINCT FROM actor_membership.subject_id
|
|
2193
|
+
THEN RAISE EXCEPTION 'session visibility transition is owner-only'
|
|
2194
|
+
USING ERRCODE = '42501'; END IF;
|
|
2195
|
+
|
|
2196
|
+
INSERT INTO session_command_receipts (
|
|
2197
|
+
account_id, workspace_id, actor_type, actor_subject_id, action,
|
|
2198
|
+
target_session_id, operation_key, canonical_request_hash
|
|
2199
|
+
) VALUES (
|
|
2200
|
+
p_account_id, p_workspace_id, 'human', p_actor_subject_id,
|
|
2201
|
+
'session.visibility.change', p_session_id, p_operation_key, p_canonical_request_hash
|
|
2202
|
+
) ON CONFLICT DO NOTHING;
|
|
2203
|
+
SELECT receipt.* INTO receipt_row FROM session_command_receipts receipt
|
|
2204
|
+
WHERE receipt.workspace_id = p_workspace_id AND receipt.actor_type = 'human'
|
|
2205
|
+
AND receipt.actor_subject_id = p_actor_subject_id
|
|
2206
|
+
AND receipt.actor_attempt_id IS NULL
|
|
2207
|
+
AND receipt.action = 'session.visibility.change'
|
|
2208
|
+
AND receipt.target_session_id = p_session_id AND receipt.target_turn_id IS NULL
|
|
2209
|
+
AND receipt.operation_key = p_operation_key
|
|
2210
|
+
FOR UPDATE;
|
|
2211
|
+
IF receipt_row.canonical_request_hash <> p_canonical_request_hash THEN
|
|
2212
|
+
RAISE EXCEPTION 'session visibility transition idempotency conflict'
|
|
2213
|
+
USING ERRCODE = '23505';
|
|
2214
|
+
END IF;
|
|
2215
|
+
IF receipt_row.result ->> 'status' = 'applied' THEN
|
|
2216
|
+
operation_id := receipt_row.id;
|
|
2217
|
+
event_id := nullif(receipt_row.result ->> 'eventId', '')::uuid;
|
|
2218
|
+
event_sequence := nullif(receipt_row.result ->> 'eventSequence', '')::integer;
|
|
2219
|
+
visibility := receipt_row.result ->> 'visibility';
|
|
2220
|
+
authority_epoch := (receipt_row.result ->> 'authorityEpoch')::integer;
|
|
2221
|
+
owner_organization_membership_id := actor_membership.id;
|
|
2222
|
+
changed := (receipt_row.result ->> 'changed')::boolean;
|
|
2223
|
+
replay := true;
|
|
2224
|
+
interrupted_attempt_count := 0; cancelled_turn_count := 0;
|
|
2225
|
+
cancelled_update_count := 0; paused_goal_count := 0;
|
|
2226
|
+
revoked_grant_count := (receipt_row.result ->> 'revokedGrantCount')::integer;
|
|
2227
|
+
RETURN NEXT; RETURN;
|
|
2228
|
+
END IF;
|
|
2229
|
+
IF session_row.authority_epoch <> p_expected_authority_epoch THEN
|
|
2230
|
+
RAISE EXCEPTION 'session visibility transition authority epoch conflict'
|
|
2231
|
+
USING ERRCODE = '40001';
|
|
2232
|
+
END IF;
|
|
2233
|
+
|
|
2234
|
+
new_epoch := session_row.authority_epoch;
|
|
2235
|
+
IF session_row.visibility <> p_target_visibility THEN
|
|
2236
|
+
PERFORM assert_session_tenancy_quiescent(
|
|
2237
|
+
p_account_id, p_workspace_id, p_session_id, p_target_visibility = 'user_private'
|
|
2238
|
+
);
|
|
2239
|
+
new_epoch := session_row.authority_epoch + 1;
|
|
2240
|
+
IF new_epoch < 2 THEN RAISE EXCEPTION 'session authority epoch exhausted'
|
|
2241
|
+
USING ERRCODE = '22003'; END IF;
|
|
2242
|
+
|
|
2243
|
+
UPDATE organization_user_resource_grants grant_row
|
|
2244
|
+
SET status = 'revoked', revoked_at = clock_timestamp(),
|
|
2245
|
+
generation = grant_row.generation + 1, updated_at = clock_timestamp()
|
|
2246
|
+
WHERE grant_row.account_id = p_account_id
|
|
2247
|
+
AND grant_row.workspace_id = p_workspace_id
|
|
2248
|
+
AND grant_row.session_id = p_session_id
|
|
2249
|
+
AND grant_row.authority_epoch = session_row.authority_epoch
|
|
2250
|
+
AND grant_row.status = 'active';
|
|
2251
|
+
GET DIAGNOSTICS grant_count = ROW_COUNT;
|
|
2252
|
+
|
|
2253
|
+
INSERT INTO session_visibility_write_capabilities (
|
|
2254
|
+
backend_pid, transaction_id, capability_id
|
|
2255
|
+
) VALUES (pg_backend_pid(), pg_current_xact_id(), visibility_write_capability_id);
|
|
2256
|
+
PERFORM set_config('opengeni.session_visibility_write_capability',
|
|
2257
|
+
visibility_write_capability_id::text, true);
|
|
2258
|
+
UPDATE sessions transition_target SET
|
|
2259
|
+
visibility = p_target_visibility,
|
|
2260
|
+
authority_epoch = new_epoch,
|
|
2261
|
+
initial_personal_connection_delegations = '[]'::jsonb,
|
|
2262
|
+
last_sequence = session_row.last_sequence + 1,
|
|
2263
|
+
updated_at = clock_timestamp()
|
|
2264
|
+
WHERE transition_target.id = p_session_id
|
|
2265
|
+
AND transition_target.authority_epoch = session_row.authority_epoch;
|
|
2266
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'session visibility transition lost authority epoch CAS'
|
|
2267
|
+
USING ERRCODE = '40001'; END IF;
|
|
2268
|
+
|
|
2269
|
+
INSERT INTO session_events (
|
|
2270
|
+
account_id, workspace_id, session_id, sequence, type, payload, occurred_at
|
|
2271
|
+
) VALUES (
|
|
2272
|
+
p_account_id, p_workspace_id, p_session_id, session_row.last_sequence + 1,
|
|
2273
|
+
'session.visibility.changed',
|
|
2274
|
+
jsonb_build_object(
|
|
2275
|
+
'operationId', receipt_row.id,
|
|
2276
|
+
'fromVisibility', CASE session_row.visibility WHEN 'user_private' THEN 'private' ELSE 'workspace' END,
|
|
2277
|
+
'toVisibility', CASE p_target_visibility WHEN 'user_private' THEN 'private' ELSE 'workspace' END,
|
|
2278
|
+
'previousAuthorityEpoch', session_row.authority_epoch,
|
|
2279
|
+
'authorityEpoch', new_epoch,
|
|
2280
|
+
'interruptedAttemptCount', 0, 'cancelledTurnCount', 0,
|
|
2281
|
+
'cancelledUpdateCount', 0, 'pausedGoalCount', 0,
|
|
2282
|
+
'revokedGrantCount', grant_count
|
|
2283
|
+
), clock_timestamp()
|
|
2284
|
+
) RETURNING id, sequence INTO event_row_id, event_row_sequence;
|
|
2285
|
+
DELETE FROM session_visibility_write_capabilities capability
|
|
2286
|
+
WHERE capability.backend_pid = pg_backend_pid()
|
|
2287
|
+
AND capability.transaction_id = pg_current_xact_id()
|
|
2288
|
+
AND capability.capability_id = visibility_write_capability_id;
|
|
2289
|
+
PERFORM set_config('opengeni.session_visibility_write_capability',
|
|
2290
|
+
CASE WHEN previous_visibility_capability IS NULL THEN '' ELSE previous_visibility_capability END,
|
|
2291
|
+
true);
|
|
2292
|
+
END IF;
|
|
2293
|
+
|
|
2294
|
+
UPDATE session_command_receipts SET result = jsonb_build_object(
|
|
2295
|
+
'status', 'applied', 'eventId', event_row_id,
|
|
2296
|
+
'eventSequence', event_row_sequence, 'visibility', p_target_visibility,
|
|
2297
|
+
'authorityEpoch', new_epoch, 'changed', session_row.visibility <> p_target_visibility,
|
|
2298
|
+
'revokedGrantCount', grant_count
|
|
2299
|
+
), updated_at = clock_timestamp() WHERE id = receipt_row.id;
|
|
2300
|
+
|
|
2301
|
+
operation_id := receipt_row.id; event_id := event_row_id;
|
|
2302
|
+
event_sequence := event_row_sequence; visibility := p_target_visibility;
|
|
2303
|
+
authority_epoch := new_epoch;
|
|
2304
|
+
owner_organization_membership_id := actor_membership.id;
|
|
2305
|
+
changed := session_row.visibility <> p_target_visibility; replay := false;
|
|
2306
|
+
interrupted_attempt_count := 0; cancelled_turn_count := 0;
|
|
2307
|
+
cancelled_update_count := 0; paused_goal_count := 0;
|
|
2308
|
+
revoked_grant_count := grant_count;
|
|
2309
|
+
PERFORM set_config('opengeni.organization_tenancy_lifecycle',
|
|
2310
|
+
CASE WHEN previous_lifecycle IS NULL THEN '' ELSE previous_lifecycle END, true);
|
|
2311
|
+
RETURN NEXT;
|
|
2312
|
+
EXCEPTION WHEN OTHERS THEN
|
|
2313
|
+
PERFORM set_config('opengeni.organization_tenancy_lifecycle',
|
|
2314
|
+
CASE WHEN previous_lifecycle IS NULL THEN '' ELSE previous_lifecycle END, true);
|
|
2315
|
+
PERFORM set_config('opengeni.session_visibility_write_capability',
|
|
2316
|
+
CASE WHEN previous_visibility_capability IS NULL THEN '' ELSE previous_visibility_capability END,
|
|
2317
|
+
true);
|
|
2318
|
+
RAISE;
|
|
2319
|
+
END
|
|
2320
|
+
$$;
|
|
2321
|
+
|
|
2322
|
+
CREATE OR REPLACE FUNCTION fork_session_content(
|
|
2323
|
+
p_account_id uuid,
|
|
2324
|
+
p_source_workspace_id uuid,
|
|
2325
|
+
p_source_session_id uuid,
|
|
2326
|
+
p_actor_subject_id text,
|
|
2327
|
+
p_destination_workspace_id uuid,
|
|
2328
|
+
p_destination_visibility text,
|
|
2329
|
+
p_workspace_shared_acknowledged boolean,
|
|
2330
|
+
p_operation_key text,
|
|
2331
|
+
p_canonical_request_hash text,
|
|
2332
|
+
p_activation_version integer
|
|
2333
|
+
) RETURNS TABLE (
|
|
2334
|
+
operation_id uuid,
|
|
2335
|
+
event_id uuid,
|
|
2336
|
+
event_sequence integer,
|
|
2337
|
+
session_id uuid,
|
|
2338
|
+
workspace_id uuid,
|
|
2339
|
+
visibility text,
|
|
2340
|
+
authority_epoch integer,
|
|
2341
|
+
copied_history_item_count integer,
|
|
2342
|
+
replay boolean
|
|
2343
|
+
)
|
|
2344
|
+
LANGUAGE plpgsql SECURITY DEFINER
|
|
2345
|
+
SET search_path FROM CURRENT
|
|
2346
|
+
AS $$
|
|
2347
|
+
DECLARE
|
|
2348
|
+
actor_membership organization_memberships%ROWTYPE;
|
|
2349
|
+
source_session sessions%ROWTYPE;
|
|
2350
|
+
destination_workspace workspaces%ROWTYPE;
|
|
2351
|
+
receipt_row session_command_receipts%ROWTYPE;
|
|
2352
|
+
destination_session_id uuid;
|
|
2353
|
+
history_count integer := 0;
|
|
2354
|
+
destination_activity_revision bigint;
|
|
2355
|
+
destination_depth integer;
|
|
2356
|
+
destination_depth_source text;
|
|
2357
|
+
destination_resources jsonb := '[]'::jsonb;
|
|
2358
|
+
event_row_id uuid;
|
|
2359
|
+
public_destination_visibility text;
|
|
2360
|
+
visibility_write_capability_id uuid := gen_random_uuid();
|
|
2361
|
+
previous_lifecycle text := current_setting('opengeni.organization_tenancy_lifecycle', true);
|
|
2362
|
+
previous_gate_state text := current_setting('opengeni.session_activity_gate_state', true);
|
|
2363
|
+
previous_gate_workspace text := current_setting(
|
|
2364
|
+
'opengeni.session_activity_gate_workspace_id', true
|
|
2365
|
+
);
|
|
2366
|
+
previous_visibility_capability text := current_setting(
|
|
2367
|
+
'opengeni.session_visibility_write_capability', true
|
|
2368
|
+
);
|
|
2369
|
+
BEGIN
|
|
2370
|
+
IF p_account_id IS NULL OR p_source_workspace_id IS NULL
|
|
2371
|
+
OR p_source_session_id IS NULL OR p_actor_subject_id IS NULL
|
|
2372
|
+
OR p_destination_workspace_id IS NULL OR p_destination_visibility IS NULL
|
|
2373
|
+
OR p_workspace_shared_acknowledged IS NULL
|
|
2374
|
+
OR p_operation_key IS NULL OR p_canonical_request_hash IS NULL
|
|
2375
|
+
OR p_activation_version IS NULL
|
|
2376
|
+
THEN RAISE EXCEPTION 'session fork requires complete authority'
|
|
2377
|
+
USING ERRCODE = '42501'; END IF;
|
|
2378
|
+
IF p_account_id IS DISTINCT FROM opengeni_private.current_account_id()
|
|
2379
|
+
OR p_source_workspace_id IS DISTINCT FROM opengeni_private.current_workspace_id()
|
|
2380
|
+
OR p_actor_subject_id IS DISTINCT FROM opengeni_private.current_subject_id()
|
|
2381
|
+
THEN RAISE EXCEPTION 'session fork authority is invalid'
|
|
2382
|
+
USING ERRCODE = '42501'; END IF;
|
|
2383
|
+
IF p_destination_workspace_id IS DISTINCT FROM p_source_workspace_id
|
|
2384
|
+
OR p_destination_visibility NOT IN ('user_private', 'workspace_shared')
|
|
2385
|
+
OR (p_destination_visibility = 'user_private' AND p_workspace_shared_acknowledged)
|
|
2386
|
+
OR p_actor_subject_id <> btrim(p_actor_subject_id)
|
|
2387
|
+
OR length(p_actor_subject_id) NOT BETWEEN 1 AND 1024
|
|
2388
|
+
OR p_operation_key <> btrim(p_operation_key)
|
|
2389
|
+
OR length(p_operation_key) NOT BETWEEN 1 AND 1024
|
|
2390
|
+
OR p_canonical_request_hash !~ '^[0-9a-f]{64}$'
|
|
2391
|
+
OR p_activation_version <> 1
|
|
2392
|
+
THEN RAISE EXCEPTION 'session fork request is invalid'
|
|
2393
|
+
USING ERRCODE = '22023'; END IF;
|
|
2394
|
+
IF nullif(previous_gate_state, '') IS NOT NULL
|
|
2395
|
+
OR nullif(previous_gate_workspace, '') IS NOT NULL
|
|
2396
|
+
THEN RAISE EXCEPTION 'session fork requires ownership of the activity gate'
|
|
2397
|
+
USING ERRCODE = '55000'; END IF;
|
|
2398
|
+
IF NOT session_tenancy_product_activated(p_account_id, p_activation_version) THEN
|
|
2399
|
+
RAISE EXCEPTION 'session tenancy product surface is not activated for this organization'
|
|
2400
|
+
USING ERRCODE = '55000';
|
|
2401
|
+
END IF;
|
|
2402
|
+
|
|
2403
|
+
PERFORM pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
|
|
2404
|
+
'organization-membership:' || p_account_id::text, 0
|
|
2405
|
+
));
|
|
2406
|
+
|
|
2407
|
+
PERFORM pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
|
|
2408
|
+
'session-tenancy:' || p_source_workspace_id::text, 0
|
|
2409
|
+
));
|
|
2410
|
+
|
|
2411
|
+
PERFORM set_config('opengeni.organization_tenancy_lifecycle',
|
|
2412
|
+
'session_visibility_activation', true);
|
|
2413
|
+
SELECT * INTO destination_workspace FROM workspaces workspace_row
|
|
2414
|
+
WHERE workspace_row.account_id = p_account_id
|
|
2415
|
+
AND workspace_row.id = p_source_workspace_id
|
|
2416
|
+
FOR UPDATE;
|
|
2417
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'session fork workspace is unavailable'
|
|
2418
|
+
USING ERRCODE = '42501'; END IF;
|
|
2419
|
+
|
|
2420
|
+
SELECT membership.* INTO actor_membership
|
|
2421
|
+
FROM organization_memberships membership
|
|
2422
|
+
WHERE membership.account_id = p_account_id
|
|
2423
|
+
AND membership.subject_id = p_actor_subject_id
|
|
2424
|
+
AND membership.status = 'active'
|
|
2425
|
+
FOR UPDATE;
|
|
2426
|
+
IF NOT FOUND OR NOT (
|
|
2427
|
+
actor_membership.personal_workspace_id = p_source_workspace_id
|
|
2428
|
+
OR EXISTS (
|
|
2429
|
+
SELECT 1 FROM workspace_memberships workspace_membership
|
|
2430
|
+
WHERE workspace_membership.account_id = p_account_id
|
|
2431
|
+
AND workspace_membership.workspace_id = p_source_workspace_id
|
|
2432
|
+
AND workspace_membership.subject_id = p_actor_subject_id
|
|
2433
|
+
)
|
|
2434
|
+
) THEN RAISE EXCEPTION 'session fork requires active workspace authority'
|
|
2435
|
+
USING ERRCODE = '42501'; END IF;
|
|
2436
|
+
|
|
2437
|
+
-- Resolve an already-applied receipt before inspecting mutable source state.
|
|
2438
|
+
-- The same still-authorized workspace actor can therefore recover the exact
|
|
2439
|
+
-- destination after a lost response even if the source owner later makes the
|
|
2440
|
+
-- source private. A new key still reaches the current source authorization
|
|
2441
|
+
-- check below and is denied.
|
|
2442
|
+
SELECT receipt.* INTO receipt_row FROM session_command_receipts receipt
|
|
2443
|
+
WHERE receipt.account_id = p_account_id
|
|
2444
|
+
AND receipt.workspace_id = p_source_workspace_id
|
|
2445
|
+
AND receipt.actor_type = 'human' AND receipt.actor_subject_id = p_actor_subject_id
|
|
2446
|
+
AND receipt.actor_attempt_id IS NULL AND receipt.action = 'session.fork'
|
|
2447
|
+
AND receipt.target_session_id = p_source_session_id
|
|
2448
|
+
AND receipt.target_turn_id IS NULL AND receipt.operation_key = p_operation_key
|
|
2449
|
+
AND receipt.result ->> 'status' = 'applied';
|
|
2450
|
+
IF receipt_row.canonical_request_hash <> p_canonical_request_hash THEN
|
|
2451
|
+
RAISE EXCEPTION 'session fork idempotency conflict' USING ERRCODE = '23505';
|
|
2452
|
+
END IF;
|
|
2453
|
+
IF receipt_row.result ->> 'status' = 'applied' THEN
|
|
2454
|
+
operation_id := receipt_row.id;
|
|
2455
|
+
event_id := nullif(receipt_row.result ->> 'eventId', '')::uuid;
|
|
2456
|
+
event_sequence := (receipt_row.result ->> 'eventSequence')::integer;
|
|
2457
|
+
session_id := (receipt_row.result ->> 'sessionId')::uuid;
|
|
2458
|
+
workspace_id := (receipt_row.result ->> 'workspaceId')::uuid;
|
|
2459
|
+
visibility := receipt_row.result ->> 'visibility'; authority_epoch := 1;
|
|
2460
|
+
copied_history_item_count :=
|
|
2461
|
+
(receipt_row.result ->> 'copiedHistoryItemCount')::integer;
|
|
2462
|
+
replay := true; RETURN NEXT; RETURN;
|
|
2463
|
+
END IF;
|
|
2464
|
+
|
|
2465
|
+
-- Product decision, distinct from authority, and the same one migration 0323
|
|
2466
|
+
-- makes on the create path: a private destination inside a shared workspace
|
|
2467
|
+
-- requires the organization's private-session setting. A personal workspace
|
|
2468
|
+
-- is exempt exactly as it is there. This is deliberately placed after the
|
|
2469
|
+
-- keyed replay above and before any source read, so a fork that already
|
|
2470
|
+
-- committed still replays byte-identically after an owner disables the
|
|
2471
|
+
-- setting, while a fresh key fails closed with the same SQLSTATE the create
|
|
2472
|
+
-- path raises.
|
|
2473
|
+
IF p_destination_visibility = 'user_private'
|
|
2474
|
+
AND actor_membership.personal_workspace_id IS DISTINCT FROM p_source_workspace_id
|
|
2475
|
+
AND NOT organization_private_sessions_enabled(p_account_id)
|
|
2476
|
+
THEN
|
|
2477
|
+
RAISE EXCEPTION
|
|
2478
|
+
'private sessions are not enabled for this organization''s shared workspaces'
|
|
2479
|
+
USING ERRCODE = '55000';
|
|
2480
|
+
END IF;
|
|
2481
|
+
|
|
2482
|
+
SELECT session.* INTO source_session FROM sessions session
|
|
2483
|
+
WHERE session.account_id = p_account_id
|
|
2484
|
+
AND session.workspace_id = p_source_workspace_id
|
|
2485
|
+
AND session.id = p_source_session_id
|
|
2486
|
+
FOR UPDATE;
|
|
2487
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'session fork source session is unavailable'
|
|
2488
|
+
USING ERRCODE = 'P0002'; END IF;
|
|
2489
|
+
-- A private source remains owner-only. A workspace-shared source follows
|
|
2490
|
+
-- current workspace authority, and the fork becomes a fresh session owned
|
|
2491
|
+
-- by the actor. This is what lets any authorized collaborator fork shared
|
|
2492
|
+
-- work privately without retaining the source owner's authority.
|
|
2493
|
+
IF source_session.visibility = 'user_private' AND (
|
|
2494
|
+
source_session.owner_organization_membership_id IS DISTINCT FROM actor_membership.id
|
|
2495
|
+
OR source_session.owner_subject_id IS DISTINCT FROM actor_membership.subject_id
|
|
2496
|
+
)
|
|
2497
|
+
THEN RAISE EXCEPTION 'session fork source session is private'
|
|
2498
|
+
USING ERRCODE = '42501'; END IF;
|
|
2499
|
+
|
|
2500
|
+
INSERT INTO session_command_receipts (
|
|
2501
|
+
account_id, workspace_id, actor_type, actor_subject_id, action,
|
|
2502
|
+
target_session_id, operation_key, canonical_request_hash
|
|
2503
|
+
) VALUES (
|
|
2504
|
+
p_account_id, p_source_workspace_id, 'human', p_actor_subject_id,
|
|
2505
|
+
'session.fork', p_source_session_id, p_operation_key, p_canonical_request_hash
|
|
2506
|
+
) ON CONFLICT DO NOTHING;
|
|
2507
|
+
SELECT receipt.* INTO receipt_row FROM session_command_receipts receipt
|
|
2508
|
+
WHERE receipt.account_id = p_account_id
|
|
2509
|
+
AND receipt.workspace_id = p_source_workspace_id
|
|
2510
|
+
AND receipt.actor_type = 'human' AND receipt.actor_subject_id = p_actor_subject_id
|
|
2511
|
+
AND receipt.actor_attempt_id IS NULL AND receipt.action = 'session.fork'
|
|
2512
|
+
AND receipt.target_session_id = p_source_session_id
|
|
2513
|
+
AND receipt.target_turn_id IS NULL AND receipt.operation_key = p_operation_key
|
|
2514
|
+
FOR UPDATE;
|
|
2515
|
+
IF receipt_row.canonical_request_hash <> p_canonical_request_hash THEN
|
|
2516
|
+
RAISE EXCEPTION 'session fork idempotency conflict' USING ERRCODE = '23505';
|
|
2517
|
+
END IF;
|
|
2518
|
+
IF receipt_row.result ->> 'status' = 'applied' THEN
|
|
2519
|
+
operation_id := receipt_row.id;
|
|
2520
|
+
event_id := nullif(receipt_row.result ->> 'eventId', '')::uuid;
|
|
2521
|
+
event_sequence := (receipt_row.result ->> 'eventSequence')::integer;
|
|
2522
|
+
session_id := (receipt_row.result ->> 'sessionId')::uuid;
|
|
2523
|
+
workspace_id := (receipt_row.result ->> 'workspaceId')::uuid;
|
|
2524
|
+
visibility := receipt_row.result ->> 'visibility'; authority_epoch := 1;
|
|
2525
|
+
copied_history_item_count :=
|
|
2526
|
+
(receipt_row.result ->> 'copiedHistoryItemCount')::integer;
|
|
2527
|
+
replay := true; RETURN NEXT; RETURN;
|
|
2528
|
+
END IF;
|
|
2529
|
+
|
|
2530
|
+
-- The acknowledgement is content-exposure evidence, not a blanket checkbox:
|
|
2531
|
+
-- it is required only when private source content crosses into workspace scope.
|
|
2532
|
+
IF source_session.visibility = 'user_private'
|
|
2533
|
+
AND p_destination_visibility = 'workspace_shared'
|
|
2534
|
+
AND NOT p_workspace_shared_acknowledged
|
|
2535
|
+
THEN RAISE EXCEPTION 'private-to-workspace fork requires explicit acknowledgement'
|
|
2536
|
+
USING ERRCODE = '22023'; END IF;
|
|
2537
|
+
|
|
2538
|
+
PERFORM assert_session_tenancy_quiescent(
|
|
2539
|
+
p_account_id, p_source_workspace_id, p_source_session_id, true
|
|
2540
|
+
);
|
|
2541
|
+
|
|
2542
|
+
SELECT coalesce(jsonb_agg(
|
|
2543
|
+
CASE WHEN resource.value ->> 'connectionType' = 'github_personal'
|
|
2544
|
+
THEN resource.value
|
|
2545
|
+
- 'provider' - 'connectionType' - 'credentialBindingId' - 'access'
|
|
2546
|
+
- 'repositoryId' - 'installationId' - 'projectId' - 'connectionId'
|
|
2547
|
+
- 'githubInstallationId' - 'githubRepositoryId'
|
|
2548
|
+
ELSE resource.value
|
|
2549
|
+
- 'credentialBindingId' - 'connectionId' - 'installationId'
|
|
2550
|
+
- 'projectId' - 'githubInstallationId'
|
|
2551
|
+
END
|
|
2552
|
+
ORDER BY resource.ordinality
|
|
2553
|
+
), '[]'::jsonb) INTO destination_resources
|
|
2554
|
+
FROM jsonb_array_elements(source_session.resources)
|
|
2555
|
+
WITH ORDINALITY AS resource(value, ordinality);
|
|
2556
|
+
|
|
2557
|
+
DROP TABLE IF EXISTS pg_temp.opengeni_session_fork_history_spool;
|
|
2558
|
+
CREATE TEMP TABLE opengeni_session_fork_history_spool (
|
|
2559
|
+
position numeric NOT NULL,
|
|
2560
|
+
item jsonb NOT NULL,
|
|
2561
|
+
item_codec_version integer,
|
|
2562
|
+
active boolean NOT NULL,
|
|
2563
|
+
provider_artifact_invalidated_at timestamptz,
|
|
2564
|
+
provider_artifact_invalidation_reason text,
|
|
2565
|
+
provider_artifact_invalidated_by_attempt_id uuid,
|
|
2566
|
+
created_at timestamptz NOT NULL
|
|
2567
|
+
) ON COMMIT DROP;
|
|
2568
|
+
INSERT INTO pg_temp.opengeni_session_fork_history_spool (
|
|
2569
|
+
position, item, item_codec_version, active,
|
|
2570
|
+
provider_artifact_invalidated_at, provider_artifact_invalidation_reason,
|
|
2571
|
+
provider_artifact_invalidated_by_attempt_id, created_at
|
|
2572
|
+
) SELECT source_item.position, source_item.item,
|
|
2573
|
+
source_item.item_codec_version, source_item.active,
|
|
2574
|
+
source_item.provider_artifact_invalidated_at,
|
|
2575
|
+
source_item.provider_artifact_invalidation_reason,
|
|
2576
|
+
source_item.provider_artifact_invalidated_by_attempt_id,
|
|
2577
|
+
source_item.created_at
|
|
2578
|
+
FROM session_history_items source_item
|
|
2579
|
+
WHERE source_item.account_id = p_account_id
|
|
2580
|
+
AND source_item.workspace_id = p_source_workspace_id
|
|
2581
|
+
AND source_item.session_id = p_source_session_id
|
|
2582
|
+
ORDER BY source_item.position;
|
|
2583
|
+
GET DIAGNOSTICS history_count = ROW_COUNT;
|
|
2584
|
+
|
|
2585
|
+
SELECT coalesce(
|
|
2586
|
+
CASE WHEN (destination_workspace.settings ->> 'maxNestedAgentDepth') ~ '^\d+$'
|
|
2587
|
+
THEN (destination_workspace.settings ->> 'maxNestedAgentDepth')::integer END,
|
|
2588
|
+
configuration.max_nested_agent_depth
|
|
2589
|
+
),
|
|
2590
|
+
CASE WHEN (destination_workspace.settings ->> 'maxNestedAgentDepth') ~ '^\d+$'
|
|
2591
|
+
THEN 'workspace' ELSE configuration.policy_source END
|
|
2592
|
+
INTO destination_depth, destination_depth_source
|
|
2593
|
+
FROM nested_agent_depth_configuration configuration
|
|
2594
|
+
WHERE configuration.singleton = true;
|
|
2595
|
+
IF destination_depth IS NULL OR destination_depth_source IS NULL THEN
|
|
2596
|
+
RAISE EXCEPTION 'session fork destination depth policy is unavailable'
|
|
2597
|
+
USING ERRCODE = '55000';
|
|
2598
|
+
END IF;
|
|
2599
|
+
|
|
2600
|
+
destination_session_id := gen_random_uuid();
|
|
2601
|
+
public_destination_visibility := CASE p_destination_visibility
|
|
2602
|
+
WHEN 'user_private' THEN 'private' ELSE 'workspace' END;
|
|
2603
|
+
INSERT INTO session_visibility_write_capabilities (
|
|
2604
|
+
backend_pid, transaction_id, capability_id
|
|
2605
|
+
) VALUES (pg_backend_pid(), pg_current_xact_id(), visibility_write_capability_id);
|
|
2606
|
+
PERFORM set_config('opengeni.session_visibility_write_capability',
|
|
2607
|
+
visibility_write_capability_id::text, true);
|
|
2608
|
+
PERFORM set_config('opengeni.session_activity_gate_state', 'open', true);
|
|
2609
|
+
PERFORM set_config('opengeni.session_activity_gate_workspace_id',
|
|
2610
|
+
p_source_workspace_id::text, true);
|
|
2611
|
+
|
|
2612
|
+
-- This one insert establishes fresh ownership, authority epoch, provenance,
|
|
2613
|
+
-- root identity and singleton sandbox group. No live source authority is copied.
|
|
2614
|
+
INSERT INTO sessions (
|
|
2615
|
+
id, account_id, workspace_id, status,
|
|
2616
|
+
initial_message, initial_message_codec_version,
|
|
2617
|
+
title, title_source, instructions, policy_role,
|
|
2618
|
+
resources, skills, tools, metadata,
|
|
2619
|
+
created_by_kind, created_by_subject_id, created_by_context,
|
|
2620
|
+
owner_organization_membership_id, owner_subject_id,
|
|
2621
|
+
visibility, create_requested_visibility, authority_epoch,
|
|
2622
|
+
forked_from_session_id, forked_from_authority_epoch,
|
|
2623
|
+
forked_from_visibility, forked_at, forked_by_organization_membership_id,
|
|
2624
|
+
model, reasoning_effort, latency_mode, sandbox_backend, sandbox_os, sandbox_group_id,
|
|
2625
|
+
first_party_mcp_permissions, first_party_mcp_tools,
|
|
2626
|
+
initial_personal_connection_delegations, tool_policy,
|
|
2627
|
+
root_session_id, nested_agent_depth,
|
|
2628
|
+
max_nested_agent_depth_override, effective_max_nested_agent_depth,
|
|
2629
|
+
nested_agent_depth_policy_source, nested_agent_depth_policy_session_id,
|
|
2630
|
+
temporal_workflow_id, active_turn_id, variable_set_id,
|
|
2631
|
+
rig_id, rig_version_id, active_sandbox_id, active_epoch,
|
|
2632
|
+
working_dir, codex_pinned_credential_id, codex_last_credential_id,
|
|
2633
|
+
codex_pin_source, codex_compaction_mode,
|
|
2634
|
+
queue_version, queue_head_position, queue_tail_position, last_sequence
|
|
2635
|
+
) VALUES (
|
|
2636
|
+
destination_session_id, p_account_id, p_source_workspace_id, 'idle',
|
|
2637
|
+
source_session.initial_message, source_session.initial_message_codec_version,
|
|
2638
|
+
source_session.title, source_session.title_source,
|
|
2639
|
+
source_session.instructions, source_session.policy_role,
|
|
2640
|
+
destination_resources, '[]'::jsonb, '[]'::jsonb, '{}'::jsonb,
|
|
2641
|
+
'subject', p_actor_subject_id, jsonb_build_object(
|
|
2642
|
+
'fork', true,
|
|
2643
|
+
'sourceSessionId', p_source_session_id,
|
|
2644
|
+
'sourceAuthorityEpoch', source_session.authority_epoch,
|
|
2645
|
+
'sourceVisibility', CASE source_session.visibility
|
|
2646
|
+
WHEN 'user_private' THEN 'private' ELSE 'workspace' END,
|
|
2647
|
+
'workspaceSharedAcknowledged', p_workspace_shared_acknowledged
|
|
2648
|
+
),
|
|
2649
|
+
actor_membership.id, actor_membership.subject_id,
|
|
2650
|
+
-- create_requested_visibility mirrors the destination the caller actually
|
|
2651
|
+
-- asked for. Leaving it at the column default made a private fork row
|
|
2652
|
+
-- internally inconsistent with its own visibility.
|
|
2653
|
+
p_destination_visibility, p_destination_visibility, 1,
|
|
2654
|
+
p_source_session_id, source_session.authority_epoch,
|
|
2655
|
+
source_session.visibility, clock_timestamp(), actor_membership.id,
|
|
2656
|
+
source_session.model, source_session.reasoning_effort, source_session.latency_mode,
|
|
2657
|
+
source_session.sandbox_backend, source_session.sandbox_os, destination_session_id,
|
|
2658
|
+
NULL, '[]'::jsonb, '[]'::jsonb,
|
|
2659
|
+
'{"mode":"explicit","inheritedFromSessionId":null}'::jsonb,
|
|
2660
|
+
destination_session_id, 0, NULL, destination_depth,
|
|
2661
|
+
destination_depth_source, NULL,
|
|
2662
|
+
NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, NULL, NULL,
|
|
2663
|
+
source_session.codex_compaction_mode,
|
|
2664
|
+
0, 0, 0, 1
|
|
2665
|
+
);
|
|
2666
|
+
|
|
2667
|
+
INSERT INTO session_history_items (
|
|
2668
|
+
account_id, workspace_id, session_id, turn_id,
|
|
2669
|
+
position, item, item_codec_version, active,
|
|
2670
|
+
provider_artifact_invalidated_at, provider_artifact_invalidation_reason,
|
|
2671
|
+
provider_artifact_invalidated_by_attempt_id, created_at
|
|
2672
|
+
) SELECT p_account_id, p_source_workspace_id, destination_session_id, NULL,
|
|
2673
|
+
source_item.position, source_item.item, source_item.item_codec_version,
|
|
2674
|
+
source_item.active, source_item.provider_artifact_invalidated_at,
|
|
2675
|
+
source_item.provider_artifact_invalidation_reason,
|
|
2676
|
+
source_item.provider_artifact_invalidated_by_attempt_id, source_item.created_at
|
|
2677
|
+
FROM pg_temp.opengeni_session_fork_history_spool source_item
|
|
2678
|
+
ORDER BY source_item.position;
|
|
2679
|
+
|
|
2680
|
+
INSERT INTO session_events (
|
|
2681
|
+
account_id, workspace_id, session_id, sequence, type, payload, occurred_at
|
|
2682
|
+
) VALUES (
|
|
2683
|
+
p_account_id, p_source_workspace_id, destination_session_id, 1,
|
|
2684
|
+
'session.created', jsonb_build_object(
|
|
2685
|
+
'forked', true, 'sourceSessionId', p_source_session_id,
|
|
2686
|
+
'sourceAuthorityEpoch', source_session.authority_epoch,
|
|
2687
|
+
'sourceVisibility', CASE source_session.visibility
|
|
2688
|
+
WHEN 'user_private' THEN 'private' ELSE 'workspace' END,
|
|
2689
|
+
'visibility', public_destination_visibility,
|
|
2690
|
+
'workspaceSharedAcknowledged', p_workspace_shared_acknowledged,
|
|
2691
|
+
'copiedHistoryItemCount', history_count
|
|
2692
|
+
), clock_timestamp()
|
|
2693
|
+
) RETURNING id INTO event_row_id;
|
|
2694
|
+
|
|
2695
|
+
PERFORM set_config('opengeni.session_activity_gate_state', 'preparing', true);
|
|
2696
|
+
SET CONSTRAINTS ALL IMMEDIATE;
|
|
2697
|
+
SET CONSTRAINTS sessions_activity_insert_commit_guard,
|
|
2698
|
+
sessions_activity_update_commit_guard DEFERRED;
|
|
2699
|
+
PERFORM set_config('opengeni.session_activity_gate_state', 'finalizing', true);
|
|
2700
|
+
UPDATE workspace_session_activity_revisions counter
|
|
2701
|
+
SET revision = counter.revision + 1
|
|
2702
|
+
WHERE counter.workspace_id = p_source_workspace_id
|
|
2703
|
+
RETURNING counter.revision INTO destination_activity_revision;
|
|
2704
|
+
IF destination_activity_revision IS NULL THEN
|
|
2705
|
+
RAISE EXCEPTION 'session fork destination activity counter is unavailable'
|
|
2706
|
+
USING ERRCODE = '55000';
|
|
2707
|
+
END IF;
|
|
2708
|
+
UPDATE sessions destination_session SET
|
|
2709
|
+
activity_revision = destination_activity_revision,
|
|
2710
|
+
activity_revision_pending_xid = NULL
|
|
2711
|
+
WHERE destination_session.id = destination_session_id
|
|
2712
|
+
AND destination_session.activity_revision_pending_xid
|
|
2713
|
+
= pg_current_xact_id()::text::bigint;
|
|
2714
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'session fork activity was not finalized'
|
|
2715
|
+
USING ERRCODE = '55000'; END IF;
|
|
2716
|
+
SET CONSTRAINTS sessions_activity_insert_commit_guard,
|
|
2717
|
+
sessions_activity_update_commit_guard IMMEDIATE;
|
|
2718
|
+
|
|
2719
|
+
PERFORM set_config('opengeni.session_activity_gate_state', '', true);
|
|
2720
|
+
PERFORM set_config('opengeni.session_activity_gate_workspace_id', '', true);
|
|
2721
|
+
DELETE FROM session_visibility_write_capabilities capability
|
|
2722
|
+
WHERE capability.backend_pid = pg_backend_pid()
|
|
2723
|
+
AND capability.transaction_id = pg_current_xact_id()
|
|
2724
|
+
AND capability.capability_id = visibility_write_capability_id;
|
|
2725
|
+
PERFORM set_config('opengeni.session_visibility_write_capability',
|
|
2726
|
+
CASE WHEN previous_visibility_capability IS NULL THEN '' ELSE previous_visibility_capability END,
|
|
2727
|
+
true);
|
|
2728
|
+
|
|
2729
|
+
UPDATE session_command_receipts SET result = jsonb_build_object(
|
|
2730
|
+
'status', 'applied', 'eventId', event_row_id, 'eventSequence', 1,
|
|
2731
|
+
'sessionId', destination_session_id, 'workspaceId', p_source_workspace_id,
|
|
2732
|
+
'visibility', p_destination_visibility, 'authorityEpoch', 1,
|
|
2733
|
+
'workspaceSharedAcknowledged', p_workspace_shared_acknowledged,
|
|
2734
|
+
'copiedHistoryItemCount', history_count
|
|
2735
|
+
), updated_at = clock_timestamp() WHERE id = receipt_row.id;
|
|
2736
|
+
|
|
2737
|
+
operation_id := receipt_row.id; event_id := event_row_id; event_sequence := 1;
|
|
2738
|
+
session_id := destination_session_id; workspace_id := p_source_workspace_id;
|
|
2739
|
+
visibility := p_destination_visibility; authority_epoch := 1;
|
|
2740
|
+
copied_history_item_count := history_count; replay := false;
|
|
2741
|
+
PERFORM set_config('opengeni.organization_tenancy_lifecycle',
|
|
2742
|
+
CASE WHEN previous_lifecycle IS NULL THEN '' ELSE previous_lifecycle END, true);
|
|
2743
|
+
RETURN NEXT;
|
|
2744
|
+
EXCEPTION WHEN OTHERS THEN
|
|
2745
|
+
PERFORM set_config('opengeni.organization_tenancy_lifecycle',
|
|
2746
|
+
CASE WHEN previous_lifecycle IS NULL THEN '' ELSE previous_lifecycle END, true);
|
|
2747
|
+
PERFORM set_config('opengeni.session_activity_gate_state',
|
|
2748
|
+
CASE WHEN previous_gate_state IS NULL THEN '' ELSE previous_gate_state END, true);
|
|
2749
|
+
PERFORM set_config('opengeni.session_activity_gate_workspace_id',
|
|
2750
|
+
CASE WHEN previous_gate_workspace IS NULL THEN '' ELSE previous_gate_workspace END, true);
|
|
2751
|
+
PERFORM set_config('opengeni.session_visibility_write_capability',
|
|
2752
|
+
CASE WHEN previous_visibility_capability IS NULL THEN '' ELSE previous_visibility_capability END,
|
|
2753
|
+
true);
|
|
2754
|
+
RAISE;
|
|
2755
|
+
END
|
|
2756
|
+
$$;
|
|
2757
|
+
|
|
2758
|
+
-- CREATE OR REPLACE evaluates `SET search_path FROM CURRENT` against the
|
|
2759
|
+
-- migrator connection. Re-pin every replaced definer to the target schema so
|
|
2760
|
+
-- a dedicated-schema install cannot retain the connection's broader fallback
|
|
2761
|
+
-- path (notably opengeni_private/public) in its function metadata.
|
|
2762
|
+
DO $pin_session_tenancy_definers$
|
|
2763
|
+
DECLARE
|
|
2764
|
+
target_schema text := current_schema();
|
|
2765
|
+
BEGIN
|
|
2766
|
+
EXECUTE format(
|
|
2767
|
+
'ALTER FUNCTION opengeni_private.reap_stale_interaction_transitions(bigint) '
|
|
2768
|
+
|| 'SET search_path = pg_catalog, %I, pg_temp',
|
|
2769
|
+
target_schema
|
|
2770
|
+
);
|
|
2771
|
+
EXECUTE format(
|
|
2772
|
+
'ALTER FUNCTION opengeni_private.reap_sandbox_leases(bigint,bigint,bigint,bigint) '
|
|
2773
|
+
|| 'SET search_path = pg_catalog, %I, pg_temp',
|
|
2774
|
+
target_schema
|
|
2775
|
+
);
|
|
2776
|
+
EXECUTE format(
|
|
2777
|
+
'ALTER FUNCTION opengeni_private.claim_terminal_retained_processes(uuid,integer,bigint) '
|
|
2778
|
+
|| 'SET search_path = pg_catalog, %I, pg_temp',
|
|
2779
|
+
target_schema
|
|
2780
|
+
);
|
|
2781
|
+
EXECUTE format(
|
|
2782
|
+
'ALTER FUNCTION opengeni_private.request_due_sandbox_rotations(bigint,integer) '
|
|
2783
|
+
|| 'SET search_path = pg_catalog, %I, pg_temp',
|
|
2784
|
+
target_schema
|
|
2785
|
+
);
|
|
2786
|
+
EXECUTE format(
|
|
2787
|
+
'ALTER FUNCTION %I.acquire_sandbox_reaper_session_tenancy_fences() '
|
|
2788
|
+
|| 'SET search_path = pg_catalog, %I, pg_temp',
|
|
2789
|
+
target_schema,
|
|
2790
|
+
target_schema
|
|
2791
|
+
);
|
|
2792
|
+
EXECUTE format(
|
|
2793
|
+
'ALTER FUNCTION %I.acquire_due_retained_process_session_tenancy_fences() '
|
|
2794
|
+
|| 'SET search_path = pg_catalog, %I, pg_temp',
|
|
2795
|
+
target_schema,
|
|
2796
|
+
target_schema
|
|
2797
|
+
);
|
|
2798
|
+
EXECUTE format(
|
|
2799
|
+
'ALTER FUNCTION %I.acquire_due_sandbox_rotation_session_tenancy_fences(bigint) '
|
|
2800
|
+
|| 'SET search_path = pg_catalog, %I, pg_temp',
|
|
2801
|
+
target_schema,
|
|
2802
|
+
target_schema
|
|
2803
|
+
);
|
|
2804
|
+
EXECUTE format(
|
|
2805
|
+
'ALTER FUNCTION %I.acquire_scoped_machine_session_tenancy_fences(uuid,uuid) '
|
|
2806
|
+
|| 'SET search_path = pg_catalog, %I, pg_temp',
|
|
2807
|
+
target_schema,
|
|
2808
|
+
target_schema
|
|
2809
|
+
);
|
|
2810
|
+
EXECUTE format(
|
|
2811
|
+
'ALTER FUNCTION %I.acquire_organization_session_tenancy_fences(uuid) '
|
|
2812
|
+
|| 'SET search_path = pg_catalog, %I, pg_temp',
|
|
2813
|
+
target_schema,
|
|
2814
|
+
target_schema
|
|
2815
|
+
);
|
|
2816
|
+
EXECUTE format(
|
|
2817
|
+
'ALTER FUNCTION %I.assert_session_tenancy_quiescent(uuid,uuid,uuid,boolean) '
|
|
2818
|
+
|| 'SET search_path = pg_catalog, %I, pg_temp',
|
|
2819
|
+
target_schema,
|
|
2820
|
+
target_schema
|
|
2821
|
+
);
|
|
2822
|
+
EXECUTE format(
|
|
2823
|
+
'ALTER FUNCTION %I.transition_session_visibility(uuid,uuid,uuid,text,text,integer,text,text,integer) '
|
|
2824
|
+
|| 'SET search_path = pg_catalog, %I, pg_temp',
|
|
2825
|
+
target_schema,
|
|
2826
|
+
target_schema
|
|
2827
|
+
);
|
|
2828
|
+
EXECUTE format(
|
|
2829
|
+
'ALTER FUNCTION %I.fork_session_content(uuid,uuid,uuid,text,uuid,text,boolean,text,text,integer) '
|
|
2830
|
+
|| 'SET search_path = pg_catalog, %I, pg_temp',
|
|
2831
|
+
target_schema,
|
|
2832
|
+
target_schema
|
|
2833
|
+
);
|
|
2834
|
+
END
|
|
2835
|
+
$pin_session_tenancy_definers$;
|
|
2836
|
+
|
|
2837
|
+
|
|
2838
|
+
RESET statement_timeout;
|
|
2839
|
+
RESET lock_timeout;
|