@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,1174 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Migration 0278: workspace-membership removal becomes one fenced
|
|
3
|
+
-- SECURITY DEFINER teardown instead of a bare personal-row cleanup + DELETE.
|
|
4
|
+
--
|
|
5
|
+
-- Before this migration `removeWorkspaceMember` deleted the target's
|
|
6
|
+
-- session-list snapshots, pins, drafts and the membership row and fenced
|
|
7
|
+
-- nothing: the removed member's queued/live turns kept running, their
|
|
8
|
+
-- user-private sessions in the workspace kept their authority epoch, their
|
|
9
|
+
-- active realtime modes stayed open, and nothing woke the affected workflows.
|
|
10
|
+
-- Organization offboarding (migration 0263) already performs the canonical
|
|
11
|
+
-- teardown account-wide; this migration is the same teardown scoped to
|
|
12
|
+
-- exactly one workspace and one target subject.
|
|
13
|
+
--
|
|
14
|
+
-- Rolling window. Two new functions only; no table shape changes. An old API
|
|
15
|
+
-- image keeps calling the bare delete path until it is replaced - removal
|
|
16
|
+
-- through the old path stays exactly as weak as before, never weaker, and the
|
|
17
|
+
-- new path composes with every existing invariant:
|
|
18
|
+
-- * the canonical event-write lock order is preserved (control FOR SHARE ->
|
|
19
|
+
-- workspace FOR KEY SHARE -> sessions FOR NO KEY UPDATE -> turns
|
|
20
|
+
-- FOR UPDATE -> attempts FOR UPDATE);
|
|
21
|
+
-- * a turn with pending tool calls and no live attempt must be settled
|
|
22
|
+
-- through the canonical application protocol first - the command fails
|
|
23
|
+
-- closed (55000) exactly like the 0263 command, and the application runs
|
|
24
|
+
-- the same prepare/settle pass before invoking it;
|
|
25
|
+
-- * live attempts are interrupted, never presumed quiesced: the command
|
|
26
|
+
-- records `session_attempt_interruptions` (kind `authority_change`) and
|
|
27
|
+
-- registers workflow wakes; Temporal cancellation semantics are untouched;
|
|
28
|
+
-- * removal never touches other workspaces, other subjects, organization
|
|
29
|
+
-- membership status, personal-workspace pointers, or retention.
|
|
30
|
+
--
|
|
31
|
+
-- Guards, restated from the application layer as fail-closed authority:
|
|
32
|
+
-- * the actor cannot remove their own membership;
|
|
33
|
+
-- * the last member holding an administering permission
|
|
34
|
+
-- ('workspace:admin' or 'members:manage') cannot be removed;
|
|
35
|
+
-- * the actor must either hold an administering permission in this
|
|
36
|
+
-- workspace or be an active organization owner/admin.
|
|
37
|
+
--
|
|
38
|
+
-- Idempotency: the whole command is one transaction. A retry after success
|
|
39
|
+
-- finds no membership row and returns removed=false without re-tearing
|
|
40
|
+
-- anything down; there is no partial state to repair.
|
|
41
|
+
|
|
42
|
+
SET LOCAL lock_timeout = '5s';
|
|
43
|
+
SET LOCAL statement_timeout = '5min';
|
|
44
|
+
|
|
45
|
+
-- 1. Preparation: the same "settle pending tool calls first" contract as the
|
|
46
|
+
-- organization lifecycle, scoped to one workspace + one subject. Returns the
|
|
47
|
+
-- exact turns the application must settle through its canonical protocol
|
|
48
|
+
-- before calling the removal command.
|
|
49
|
+
|
|
50
|
+
CREATE FUNCTION prepare_workspace_membership_removal_settlements(
|
|
51
|
+
p_command jsonb
|
|
52
|
+
) RETURNS jsonb
|
|
53
|
+
LANGUAGE plpgsql
|
|
54
|
+
SECURITY DEFINER
|
|
55
|
+
SET search_path FROM CURRENT
|
|
56
|
+
AS $body$
|
|
57
|
+
DECLARE
|
|
58
|
+
account_id_value uuid := nullif(p_command ->> 'organizationId', '')::uuid;
|
|
59
|
+
workspace_id_value uuid := nullif(p_command ->> 'workspaceId', '')::uuid;
|
|
60
|
+
actor_subject text := p_command ->> 'actorSubjectId';
|
|
61
|
+
target_subject text := p_command ->> 'targetSubjectId';
|
|
62
|
+
target_membership_id uuid;
|
|
63
|
+
result jsonb := '[]'::jsonb;
|
|
64
|
+
previous_workspace text := pg_catalog.current_setting('opengeni.workspace_id', true);
|
|
65
|
+
BEGIN
|
|
66
|
+
IF p_command IS NULL
|
|
67
|
+
OR (p_command ->> 'action') IS DISTINCT FROM 'remove'
|
|
68
|
+
OR account_id_value IS NULL
|
|
69
|
+
OR workspace_id_value IS NULL
|
|
70
|
+
OR actor_subject IS NULL
|
|
71
|
+
OR target_subject IS NULL
|
|
72
|
+
OR actor_subject IS DISTINCT FROM opengeni_private.current_subject_id()
|
|
73
|
+
OR account_id_value IS DISTINCT FROM opengeni_private.current_account_id()
|
|
74
|
+
THEN
|
|
75
|
+
RAISE EXCEPTION 'workspace membership removal preparation authority is invalid'
|
|
76
|
+
USING ERRCODE = '42501';
|
|
77
|
+
END IF;
|
|
78
|
+
PERFORM 1 FROM workspaces workspace
|
|
79
|
+
WHERE workspace.account_id = account_id_value AND workspace.id = workspace_id_value;
|
|
80
|
+
IF NOT FOUND THEN
|
|
81
|
+
RAISE EXCEPTION 'workspace not found' USING ERRCODE = 'P0002';
|
|
82
|
+
END IF;
|
|
83
|
+
PERFORM opengeni_private.assert_workspace_membership_removal_actor(
|
|
84
|
+
account_id_value, workspace_id_value, actor_subject, target_subject
|
|
85
|
+
);
|
|
86
|
+
SELECT membership.id INTO target_membership_id
|
|
87
|
+
FROM organization_memberships membership
|
|
88
|
+
WHERE membership.account_id = account_id_value
|
|
89
|
+
AND membership.subject_id = target_subject;
|
|
90
|
+
|
|
91
|
+
PERFORM pg_catalog.set_config(
|
|
92
|
+
'opengeni.workspace_id', workspace_id_value::text, true
|
|
93
|
+
);
|
|
94
|
+
PERFORM 1 FROM workspace_inference_controls control
|
|
95
|
+
WHERE control.account_id = account_id_value
|
|
96
|
+
AND control.workspace_id = workspace_id_value
|
|
97
|
+
FOR SHARE;
|
|
98
|
+
PERFORM 1 FROM workspaces workspace
|
|
99
|
+
WHERE workspace.account_id = account_id_value
|
|
100
|
+
AND workspace.id = workspace_id_value
|
|
101
|
+
FOR KEY SHARE;
|
|
102
|
+
-- Scheduled-task rows before session rows, exactly the 0275 wrapper's
|
|
103
|
+
-- per-workspace task/session prefix: the scheduled-task writers lock
|
|
104
|
+
-- task -> session -> membership, so removal must never hold the membership
|
|
105
|
+
-- (or session) rows while waiting on a task row.
|
|
106
|
+
PERFORM 1 FROM scheduled_tasks task
|
|
107
|
+
WHERE task.account_id = account_id_value
|
|
108
|
+
AND task.workspace_id = workspace_id_value
|
|
109
|
+
AND task.deleted_at IS NULL
|
|
110
|
+
AND task.status = 'active'
|
|
111
|
+
AND (
|
|
112
|
+
(task.created_by_kind = 'subject' AND task.created_by_subject_id = target_subject)
|
|
113
|
+
OR EXISTS (
|
|
114
|
+
SELECT 1 FROM scheduled_task_revision_authorities authority
|
|
115
|
+
WHERE authority.task_id = task.id
|
|
116
|
+
AND authority.task_authority_revision = task.authority_revision
|
|
117
|
+
AND authority.subject_id = target_subject
|
|
118
|
+
)
|
|
119
|
+
OR EXISTS (
|
|
120
|
+
SELECT 1 FROM scheduled_task_personal_resource_authorities authority
|
|
121
|
+
WHERE authority.task_id = task.id
|
|
122
|
+
AND authority.task_authority_revision = task.authority_revision
|
|
123
|
+
AND authority.initiating_human_subject_id = target_subject
|
|
124
|
+
)
|
|
125
|
+
OR EXISTS (
|
|
126
|
+
SELECT 1 FROM scheduled_task_connection_authority_snapshots snapshot
|
|
127
|
+
WHERE snapshot.task_id = task.id
|
|
128
|
+
AND snapshot.task_authority_revision = task.authority_revision
|
|
129
|
+
AND snapshot.owner_subject_id = target_subject
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
ORDER BY task.id
|
|
133
|
+
FOR UPDATE;
|
|
134
|
+
PERFORM 1 FROM sessions affected
|
|
135
|
+
WHERE affected.account_id = account_id_value
|
|
136
|
+
AND affected.workspace_id = workspace_id_value
|
|
137
|
+
AND (
|
|
138
|
+
(target_membership_id IS NOT NULL
|
|
139
|
+
AND affected.owner_organization_membership_id = target_membership_id)
|
|
140
|
+
OR EXISTS (
|
|
141
|
+
SELECT 1 FROM session_turns initiated
|
|
142
|
+
WHERE initiated.session_id = affected.id
|
|
143
|
+
AND initiated.initiating_human_subject_id = target_subject
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
ORDER BY affected.id
|
|
147
|
+
FOR NO KEY UPDATE;
|
|
148
|
+
PERFORM 1 FROM session_turns turn_row
|
|
149
|
+
WHERE turn_row.account_id = account_id_value
|
|
150
|
+
AND turn_row.workspace_id = workspace_id_value
|
|
151
|
+
AND EXISTS (
|
|
152
|
+
SELECT 1 FROM sessions owned
|
|
153
|
+
WHERE owned.id = turn_row.session_id
|
|
154
|
+
AND (
|
|
155
|
+
(target_membership_id IS NOT NULL
|
|
156
|
+
AND owned.owner_organization_membership_id = target_membership_id)
|
|
157
|
+
OR turn_row.initiating_human_subject_id = target_subject
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
ORDER BY turn_row.id
|
|
161
|
+
FOR UPDATE;
|
|
162
|
+
PERFORM 1 FROM session_turn_attempts attempt
|
|
163
|
+
WHERE attempt.account_id = account_id_value
|
|
164
|
+
AND attempt.workspace_id = workspace_id_value
|
|
165
|
+
AND EXISTS (
|
|
166
|
+
SELECT 1 FROM sessions owned
|
|
167
|
+
WHERE owned.id = attempt.session_id
|
|
168
|
+
AND (
|
|
169
|
+
(target_membership_id IS NOT NULL
|
|
170
|
+
AND owned.owner_organization_membership_id = target_membership_id)
|
|
171
|
+
OR EXISTS (
|
|
172
|
+
SELECT 1 FROM session_turns initiated
|
|
173
|
+
WHERE initiated.id = attempt.turn_id
|
|
174
|
+
AND initiated.initiating_human_subject_id = target_subject
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
ORDER BY attempt.id
|
|
179
|
+
FOR UPDATE;
|
|
180
|
+
SELECT result || COALESCE(
|
|
181
|
+
pg_catalog.jsonb_agg(
|
|
182
|
+
pg_catalog.jsonb_build_object(
|
|
183
|
+
'accountId', turn_row.account_id,
|
|
184
|
+
'workspaceId', turn_row.workspace_id,
|
|
185
|
+
'sessionId', turn_row.session_id,
|
|
186
|
+
'turnId', turn_row.id,
|
|
187
|
+
'executionGeneration', turn_row.execution_generation,
|
|
188
|
+
'turnAssociation', CASE WHEN owned.active_turn_id = turn_row.id
|
|
189
|
+
THEN 'current' ELSE NULL END
|
|
190
|
+
) ORDER BY turn_row.session_id, turn_row.id
|
|
191
|
+
), '[]'::jsonb
|
|
192
|
+
) INTO result
|
|
193
|
+
FROM session_turns turn_row
|
|
194
|
+
JOIN sessions owned ON owned.id = turn_row.session_id
|
|
195
|
+
WHERE turn_row.account_id = account_id_value
|
|
196
|
+
AND turn_row.workspace_id = workspace_id_value
|
|
197
|
+
AND turn_row.status IN (
|
|
198
|
+
'queued', 'running', 'requires_action', 'recovering', 'waiting_capacity'
|
|
199
|
+
)
|
|
200
|
+
AND (
|
|
201
|
+
(target_membership_id IS NOT NULL
|
|
202
|
+
AND owned.owner_organization_membership_id = target_membership_id)
|
|
203
|
+
OR turn_row.initiating_human_subject_id = target_subject
|
|
204
|
+
)
|
|
205
|
+
AND EXISTS (
|
|
206
|
+
SELECT 1 FROM session_pending_tool_calls pending
|
|
207
|
+
WHERE pending.account_id = turn_row.account_id
|
|
208
|
+
AND pending.workspace_id = turn_row.workspace_id
|
|
209
|
+
AND pending.session_id = turn_row.session_id
|
|
210
|
+
AND pending.turn_id = turn_row.id
|
|
211
|
+
)
|
|
212
|
+
AND NOT EXISTS (
|
|
213
|
+
SELECT 1 FROM session_turn_attempts live_attempt
|
|
214
|
+
WHERE live_attempt.account_id = turn_row.account_id
|
|
215
|
+
AND live_attempt.workspace_id = turn_row.workspace_id
|
|
216
|
+
AND live_attempt.session_id = turn_row.session_id
|
|
217
|
+
AND live_attempt.turn_id = turn_row.id
|
|
218
|
+
AND live_attempt.id = turn_row.active_attempt_id
|
|
219
|
+
AND live_attempt.state IN ('claimed', 'running')
|
|
220
|
+
);
|
|
221
|
+
PERFORM pg_catalog.set_config(
|
|
222
|
+
'opengeni.workspace_id', COALESCE(previous_workspace, ''), true
|
|
223
|
+
);
|
|
224
|
+
RETURN result;
|
|
225
|
+
EXCEPTION WHEN OTHERS THEN
|
|
226
|
+
PERFORM pg_catalog.set_config(
|
|
227
|
+
'opengeni.workspace_id', COALESCE(previous_workspace, ''), true
|
|
228
|
+
);
|
|
229
|
+
RAISE;
|
|
230
|
+
END
|
|
231
|
+
$body$;
|
|
232
|
+
|
|
233
|
+
-- 2. Shared actor/guard authority. The exact application guards, restated as
|
|
234
|
+
-- fail-closed database authority so no future caller can skip them:
|
|
235
|
+
-- self-removal, last-admin, and actor administration. The checks themselves
|
|
236
|
+
-- are pure reads: the removal command serializes the administering roster
|
|
237
|
+
-- (ordered FOR UPDATE on every admin/actor/target membership row plus the
|
|
238
|
+
-- actor's organization membership) BEFORE re-running them, so two concurrent
|
|
239
|
+
-- removals of the two last administering members cannot both pass. The
|
|
240
|
+
-- preparation function's earlier call is a friendly unserialized precheck
|
|
241
|
+
-- only and authorizes nothing by itself.
|
|
242
|
+
|
|
243
|
+
CREATE FUNCTION opengeni_private.assert_workspace_membership_removal_actor(
|
|
244
|
+
p_account_id uuid,
|
|
245
|
+
p_workspace_id uuid,
|
|
246
|
+
p_actor_subject text,
|
|
247
|
+
p_target_subject text
|
|
248
|
+
) RETURNS void
|
|
249
|
+
LANGUAGE plpgsql
|
|
250
|
+
AS $body$
|
|
251
|
+
DECLARE
|
|
252
|
+
actor_can_administer boolean;
|
|
253
|
+
BEGIN
|
|
254
|
+
IF p_actor_subject = p_target_subject THEN
|
|
255
|
+
RAISE EXCEPTION 'a member cannot remove their own workspace membership'
|
|
256
|
+
USING ERRCODE = '55000';
|
|
257
|
+
END IF;
|
|
258
|
+
SELECT EXISTS (
|
|
259
|
+
SELECT 1 FROM workspace_memberships actor_row
|
|
260
|
+
WHERE actor_row.account_id = p_account_id
|
|
261
|
+
AND actor_row.workspace_id = p_workspace_id
|
|
262
|
+
AND actor_row.subject_id = p_actor_subject
|
|
263
|
+
AND actor_row.permissions ?| ARRAY['workspace:admin', 'members:manage']
|
|
264
|
+
) OR EXISTS (
|
|
265
|
+
SELECT 1 FROM organization_memberships org_actor
|
|
266
|
+
WHERE org_actor.account_id = p_account_id
|
|
267
|
+
AND org_actor.subject_id = p_actor_subject
|
|
268
|
+
AND org_actor.status = 'active'
|
|
269
|
+
AND org_actor.role IN ('owner', 'admin')
|
|
270
|
+
) INTO actor_can_administer;
|
|
271
|
+
IF NOT actor_can_administer THEN
|
|
272
|
+
RAISE EXCEPTION 'workspace member administration required' USING ERRCODE = '42501';
|
|
273
|
+
END IF;
|
|
274
|
+
IF EXISTS (
|
|
275
|
+
SELECT 1 FROM workspace_memberships target_row
|
|
276
|
+
WHERE target_row.account_id = p_account_id
|
|
277
|
+
AND target_row.workspace_id = p_workspace_id
|
|
278
|
+
AND target_row.subject_id = p_target_subject
|
|
279
|
+
AND target_row.permissions ?| ARRAY['workspace:admin', 'members:manage']
|
|
280
|
+
) AND NOT EXISTS (
|
|
281
|
+
SELECT 1 FROM workspace_memberships other
|
|
282
|
+
WHERE other.account_id = p_account_id
|
|
283
|
+
AND other.workspace_id = p_workspace_id
|
|
284
|
+
AND other.subject_id <> p_target_subject
|
|
285
|
+
AND other.permissions ?| ARRAY['workspace:admin', 'members:manage']
|
|
286
|
+
) THEN
|
|
287
|
+
RAISE EXCEPTION 'cannot remove the last administering workspace member'
|
|
288
|
+
USING ERRCODE = '55000';
|
|
289
|
+
END IF;
|
|
290
|
+
END
|
|
291
|
+
$body$;
|
|
292
|
+
|
|
293
|
+
-- 3. The removal command itself.
|
|
294
|
+
|
|
295
|
+
CREATE FUNCTION workspace_membership_removal_command(
|
|
296
|
+
p_command jsonb
|
|
297
|
+
) RETURNS jsonb
|
|
298
|
+
LANGUAGE plpgsql
|
|
299
|
+
SECURITY DEFINER
|
|
300
|
+
SET search_path FROM CURRENT
|
|
301
|
+
AS $body$
|
|
302
|
+
DECLARE
|
|
303
|
+
account_id_value uuid := nullif(p_command ->> 'organizationId', '')::uuid;
|
|
304
|
+
workspace_id_value uuid := nullif(p_command ->> 'workspaceId', '')::uuid;
|
|
305
|
+
actor_subject text := p_command ->> 'actorSubjectId';
|
|
306
|
+
target_subject text := p_command ->> 'targetSubjectId';
|
|
307
|
+
operation_id_value uuid := nullif(p_command ->> 'operationId', '')::uuid;
|
|
308
|
+
input_hash_value text;
|
|
309
|
+
target_membership_id uuid;
|
|
310
|
+
membership_row_id uuid;
|
|
311
|
+
now_value timestamptz := pg_catalog.clock_timestamp();
|
|
312
|
+
affected_session_ids uuid[] := ARRAY[]::uuid[];
|
|
313
|
+
affected_realtime_ids uuid[] := ARRAY[]::uuid[];
|
|
314
|
+
cancelled_turn_ids uuid[] := ARRAY[]::uuid[];
|
|
315
|
+
cancelled_turn record;
|
|
316
|
+
cancelled_request record;
|
|
317
|
+
cancelled_sequence bigint;
|
|
318
|
+
cancelled_association text;
|
|
319
|
+
settled_update_ids uuid[];
|
|
320
|
+
settled_history_item_id uuid;
|
|
321
|
+
interruption_operation_id uuid;
|
|
322
|
+
visibility_capability_id uuid;
|
|
323
|
+
activity_revision_value bigint;
|
|
324
|
+
previous_workspace text := pg_catalog.current_setting('opengeni.workspace_id', true);
|
|
325
|
+
previous_visibility_capability text := pg_catalog.current_setting(
|
|
326
|
+
'opengeni.session_visibility_write_capability', true
|
|
327
|
+
);
|
|
328
|
+
previous_activity_gate_state text := pg_catalog.current_setting(
|
|
329
|
+
'opengeni.session_activity_gate_state', true
|
|
330
|
+
);
|
|
331
|
+
previous_activity_gate_workspace text := pg_catalog.current_setting(
|
|
332
|
+
'opengeni.session_activity_gate_workspace_id', true
|
|
333
|
+
);
|
|
334
|
+
BEGIN
|
|
335
|
+
IF p_command IS NULL
|
|
336
|
+
OR (p_command ->> 'action') IS DISTINCT FROM 'remove'
|
|
337
|
+
OR account_id_value IS NULL
|
|
338
|
+
OR workspace_id_value IS NULL
|
|
339
|
+
OR actor_subject IS NULL
|
|
340
|
+
OR target_subject IS NULL
|
|
341
|
+
OR operation_id_value IS NULL
|
|
342
|
+
OR actor_subject IS DISTINCT FROM opengeni_private.current_subject_id()
|
|
343
|
+
OR account_id_value IS DISTINCT FROM opengeni_private.current_account_id()
|
|
344
|
+
THEN
|
|
345
|
+
RAISE EXCEPTION 'workspace membership removal authority is invalid'
|
|
346
|
+
USING ERRCODE = '42501';
|
|
347
|
+
END IF;
|
|
348
|
+
input_hash_value := pg_catalog.encode(
|
|
349
|
+
pg_catalog.sha256(pg_catalog.convert_to(p_command::text, 'UTF8')), 'hex'
|
|
350
|
+
);
|
|
351
|
+
PERFORM 1 FROM workspaces workspace
|
|
352
|
+
WHERE workspace.account_id = account_id_value AND workspace.id = workspace_id_value;
|
|
353
|
+
IF NOT FOUND THEN
|
|
354
|
+
RAISE EXCEPTION 'workspace not found' USING ERRCODE = 'P0002';
|
|
355
|
+
END IF;
|
|
356
|
+
|
|
357
|
+
-- The command runs after the application settled the target's pending
|
|
358
|
+
-- protocol state and finalized that workspace activity gate. Its own gated
|
|
359
|
+
-- session writes need the commit guards DEFERRED again (exactly the 0275
|
|
360
|
+
-- wrapper's reasoning).
|
|
361
|
+
SET CONSTRAINTS sessions_activity_insert_commit_guard,
|
|
362
|
+
sessions_activity_update_commit_guard DEFERRED;
|
|
363
|
+
|
|
364
|
+
-- The personal-state fence the pre-0278 delete path took: session listing
|
|
365
|
+
-- takes its shared counterpart and pin mutation the same exclusive one, so
|
|
366
|
+
-- a racing snapshot/pin writer cannot recreate personal rows after this
|
|
367
|
+
-- cleanup. Same key derivation as lockSessionPersonalStateExclusive.
|
|
368
|
+
PERFORM pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
|
|
369
|
+
'session-personal-state:' || workspace_id_value::text || ':' || target_subject, 0
|
|
370
|
+
));
|
|
371
|
+
|
|
372
|
+
visibility_capability_id := pg_catalog.gen_random_uuid();
|
|
373
|
+
INSERT INTO session_visibility_write_capabilities (
|
|
374
|
+
backend_pid, transaction_id, capability_id
|
|
375
|
+
) VALUES (
|
|
376
|
+
pg_catalog.pg_backend_pid(), pg_catalog.pg_current_xact_id(),
|
|
377
|
+
visibility_capability_id
|
|
378
|
+
);
|
|
379
|
+
PERFORM pg_catalog.set_config(
|
|
380
|
+
'opengeni.session_visibility_write_capability',
|
|
381
|
+
visibility_capability_id::text, true
|
|
382
|
+
);
|
|
383
|
+
PERFORM pg_catalog.set_config(
|
|
384
|
+
'opengeni.workspace_id', workspace_id_value::text, true
|
|
385
|
+
);
|
|
386
|
+
PERFORM pg_catalog.set_config(
|
|
387
|
+
'opengeni.session_activity_gate_state', 'open', true
|
|
388
|
+
);
|
|
389
|
+
PERFORM pg_catalog.set_config(
|
|
390
|
+
'opengeni.session_activity_gate_workspace_id', workspace_id_value::text, true
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
-- Canonical lock prefix for this workspace, then the exact affected rows.
|
|
394
|
+
PERFORM 1 FROM workspace_inference_controls control
|
|
395
|
+
WHERE control.account_id = account_id_value
|
|
396
|
+
AND control.workspace_id = workspace_id_value
|
|
397
|
+
FOR SHARE;
|
|
398
|
+
PERFORM 1 FROM workspaces workspace
|
|
399
|
+
WHERE workspace.account_id = account_id_value
|
|
400
|
+
AND workspace.id = workspace_id_value
|
|
401
|
+
FOR KEY SHARE;
|
|
402
|
+
-- Task rows before membership/session rows (0275 writer order; reentrant
|
|
403
|
+
-- with the preparation pass in the same transaction).
|
|
404
|
+
PERFORM 1 FROM scheduled_tasks task
|
|
405
|
+
WHERE task.account_id = account_id_value
|
|
406
|
+
AND task.workspace_id = workspace_id_value
|
|
407
|
+
AND task.deleted_at IS NULL
|
|
408
|
+
AND task.status = 'active'
|
|
409
|
+
AND (
|
|
410
|
+
(task.created_by_kind = 'subject' AND task.created_by_subject_id = target_subject)
|
|
411
|
+
OR EXISTS (
|
|
412
|
+
SELECT 1 FROM scheduled_task_revision_authorities authority
|
|
413
|
+
WHERE authority.task_id = task.id
|
|
414
|
+
AND authority.task_authority_revision = task.authority_revision
|
|
415
|
+
AND authority.subject_id = target_subject
|
|
416
|
+
)
|
|
417
|
+
OR EXISTS (
|
|
418
|
+
SELECT 1 FROM scheduled_task_personal_resource_authorities authority
|
|
419
|
+
WHERE authority.task_id = task.id
|
|
420
|
+
AND authority.task_authority_revision = task.authority_revision
|
|
421
|
+
AND authority.initiating_human_subject_id = target_subject
|
|
422
|
+
)
|
|
423
|
+
OR EXISTS (
|
|
424
|
+
SELECT 1 FROM scheduled_task_connection_authority_snapshots snapshot
|
|
425
|
+
WHERE snapshot.task_id = task.id
|
|
426
|
+
AND snapshot.task_authority_revision = task.authority_revision
|
|
427
|
+
AND snapshot.owner_subject_id = target_subject
|
|
428
|
+
)
|
|
429
|
+
)
|
|
430
|
+
ORDER BY task.id
|
|
431
|
+
FOR UPDATE;
|
|
432
|
+
-- Serialize the administering roster before re-running the guards: the
|
|
433
|
+
-- actor's organization membership (shared - the organization lifecycle
|
|
434
|
+
-- takes it FOR UPDATE) and every admin/actor/target workspace membership
|
|
435
|
+
-- row in id order. Two concurrent removals of the two last administering
|
|
436
|
+
-- members therefore serialize, and the loser re-reads a roster with only
|
|
437
|
+
-- one administering member left and fails closed.
|
|
438
|
+
PERFORM 1 FROM organization_memberships org_actor
|
|
439
|
+
WHERE org_actor.account_id = account_id_value
|
|
440
|
+
AND org_actor.subject_id = actor_subject
|
|
441
|
+
FOR SHARE;
|
|
442
|
+
PERFORM 1 FROM workspace_memberships roster
|
|
443
|
+
WHERE roster.account_id = account_id_value
|
|
444
|
+
AND roster.workspace_id = workspace_id_value
|
|
445
|
+
AND (
|
|
446
|
+
roster.subject_id IN (actor_subject, target_subject)
|
|
447
|
+
OR roster.permissions ?| ARRAY['workspace:admin', 'members:manage']
|
|
448
|
+
)
|
|
449
|
+
ORDER BY roster.id
|
|
450
|
+
FOR UPDATE;
|
|
451
|
+
PERFORM opengeni_private.assert_workspace_membership_removal_actor(
|
|
452
|
+
account_id_value, workspace_id_value, actor_subject, target_subject
|
|
453
|
+
);
|
|
454
|
+
SELECT membership.id INTO membership_row_id
|
|
455
|
+
FROM workspace_memberships membership
|
|
456
|
+
WHERE membership.account_id = account_id_value
|
|
457
|
+
AND membership.workspace_id = workspace_id_value
|
|
458
|
+
AND membership.subject_id = target_subject;
|
|
459
|
+
IF membership_row_id IS NULL THEN
|
|
460
|
+
DELETE FROM session_visibility_write_capabilities capability
|
|
461
|
+
WHERE capability.backend_pid = pg_catalog.pg_backend_pid()
|
|
462
|
+
AND capability.transaction_id = pg_catalog.pg_current_xact_id()
|
|
463
|
+
AND capability.capability_id = visibility_capability_id;
|
|
464
|
+
PERFORM pg_catalog.set_config(
|
|
465
|
+
'opengeni.session_visibility_write_capability',
|
|
466
|
+
coalesce(previous_visibility_capability, ''), true
|
|
467
|
+
);
|
|
468
|
+
PERFORM pg_catalog.set_config(
|
|
469
|
+
'opengeni.workspace_id', coalesce(previous_workspace, ''), true
|
|
470
|
+
);
|
|
471
|
+
PERFORM pg_catalog.set_config(
|
|
472
|
+
'opengeni.session_activity_gate_state',
|
|
473
|
+
coalesce(previous_activity_gate_state, ''), true
|
|
474
|
+
);
|
|
475
|
+
PERFORM pg_catalog.set_config(
|
|
476
|
+
'opengeni.session_activity_gate_workspace_id',
|
|
477
|
+
coalesce(previous_activity_gate_workspace, ''), true
|
|
478
|
+
);
|
|
479
|
+
RETURN pg_catalog.jsonb_build_object('removed', false);
|
|
480
|
+
END IF;
|
|
481
|
+
SELECT membership.id INTO target_membership_id
|
|
482
|
+
FROM organization_memberships membership
|
|
483
|
+
WHERE membership.account_id = account_id_value
|
|
484
|
+
AND membership.subject_id = target_subject;
|
|
485
|
+
PERFORM 1 FROM sessions affected
|
|
486
|
+
WHERE affected.account_id = account_id_value
|
|
487
|
+
AND affected.workspace_id = workspace_id_value
|
|
488
|
+
AND (
|
|
489
|
+
(target_membership_id IS NOT NULL
|
|
490
|
+
AND affected.owner_organization_membership_id = target_membership_id)
|
|
491
|
+
OR EXISTS (
|
|
492
|
+
SELECT 1 FROM session_turns initiated
|
|
493
|
+
WHERE initiated.session_id = affected.id
|
|
494
|
+
AND initiated.initiating_human_subject_id = target_subject
|
|
495
|
+
)
|
|
496
|
+
)
|
|
497
|
+
ORDER BY affected.id
|
|
498
|
+
FOR NO KEY UPDATE;
|
|
499
|
+
PERFORM 1 FROM session_turns turn_row
|
|
500
|
+
WHERE turn_row.account_id = account_id_value
|
|
501
|
+
AND turn_row.workspace_id = workspace_id_value
|
|
502
|
+
AND EXISTS (
|
|
503
|
+
SELECT 1 FROM sessions owned
|
|
504
|
+
WHERE owned.id = turn_row.session_id
|
|
505
|
+
AND (
|
|
506
|
+
(target_membership_id IS NOT NULL
|
|
507
|
+
AND owned.owner_organization_membership_id = target_membership_id)
|
|
508
|
+
OR turn_row.initiating_human_subject_id = target_subject
|
|
509
|
+
)
|
|
510
|
+
)
|
|
511
|
+
ORDER BY turn_row.id
|
|
512
|
+
FOR UPDATE;
|
|
513
|
+
PERFORM 1 FROM session_turn_attempts attempt
|
|
514
|
+
WHERE attempt.account_id = account_id_value
|
|
515
|
+
AND attempt.workspace_id = workspace_id_value
|
|
516
|
+
AND EXISTS (
|
|
517
|
+
SELECT 1 FROM sessions owned
|
|
518
|
+
WHERE owned.id = attempt.session_id
|
|
519
|
+
AND (
|
|
520
|
+
(target_membership_id IS NOT NULL
|
|
521
|
+
AND owned.owner_organization_membership_id = target_membership_id)
|
|
522
|
+
OR EXISTS (
|
|
523
|
+
SELECT 1 FROM session_turns initiated
|
|
524
|
+
WHERE initiated.id = attempt.turn_id
|
|
525
|
+
AND initiated.initiating_human_subject_id = target_subject
|
|
526
|
+
)
|
|
527
|
+
)
|
|
528
|
+
)
|
|
529
|
+
ORDER BY attempt.id
|
|
530
|
+
FOR UPDATE;
|
|
531
|
+
PERFORM 1 FROM session_realtime_modes realtime
|
|
532
|
+
WHERE realtime.account_id = account_id_value
|
|
533
|
+
AND realtime.workspace_id = workspace_id_value
|
|
534
|
+
AND realtime.owner_subject_id = target_subject
|
|
535
|
+
AND realtime.state = 'active'
|
|
536
|
+
ORDER BY realtime.id
|
|
537
|
+
FOR UPDATE;
|
|
538
|
+
SELECT COALESCE(pg_catalog.array_agg(realtime.id ORDER BY realtime.id), ARRAY[]::uuid[])
|
|
539
|
+
INTO affected_realtime_ids
|
|
540
|
+
FROM session_realtime_modes realtime
|
|
541
|
+
WHERE realtime.account_id = account_id_value
|
|
542
|
+
AND realtime.workspace_id = workspace_id_value
|
|
543
|
+
AND realtime.owner_subject_id = target_subject
|
|
544
|
+
AND realtime.state = 'active';
|
|
545
|
+
PERFORM 1 FROM session_realtime_connections connection
|
|
546
|
+
WHERE connection.account_id = account_id_value
|
|
547
|
+
AND connection.workspace_id = workspace_id_value
|
|
548
|
+
AND connection.realtime_id = ANY(affected_realtime_ids)
|
|
549
|
+
AND connection.state IN ('negotiating', 'ready', 'active')
|
|
550
|
+
ORDER BY connection.id
|
|
551
|
+
FOR UPDATE;
|
|
552
|
+
|
|
553
|
+
-- Pending tool calls with no live attempt need canonical protocol
|
|
554
|
+
-- settlement first, exactly as the organization lifecycle requires.
|
|
555
|
+
IF EXISTS (
|
|
556
|
+
SELECT 1
|
|
557
|
+
FROM session_turns turn_row
|
|
558
|
+
JOIN sessions owned ON owned.id = turn_row.session_id
|
|
559
|
+
WHERE turn_row.account_id = account_id_value
|
|
560
|
+
AND turn_row.workspace_id = workspace_id_value
|
|
561
|
+
AND turn_row.status IN (
|
|
562
|
+
'queued', 'running', 'requires_action', 'recovering', 'waiting_capacity'
|
|
563
|
+
)
|
|
564
|
+
AND (
|
|
565
|
+
(target_membership_id IS NOT NULL
|
|
566
|
+
AND owned.owner_organization_membership_id = target_membership_id)
|
|
567
|
+
OR turn_row.initiating_human_subject_id = target_subject
|
|
568
|
+
)
|
|
569
|
+
AND EXISTS (
|
|
570
|
+
SELECT 1 FROM session_pending_tool_calls pending
|
|
571
|
+
WHERE pending.account_id = turn_row.account_id
|
|
572
|
+
AND pending.workspace_id = turn_row.workspace_id
|
|
573
|
+
AND pending.session_id = turn_row.session_id
|
|
574
|
+
AND pending.turn_id = turn_row.id
|
|
575
|
+
)
|
|
576
|
+
AND NOT EXISTS (
|
|
577
|
+
SELECT 1 FROM session_turn_attempts live_attempt
|
|
578
|
+
WHERE live_attempt.account_id = turn_row.account_id
|
|
579
|
+
AND live_attempt.workspace_id = turn_row.workspace_id
|
|
580
|
+
AND live_attempt.session_id = turn_row.session_id
|
|
581
|
+
AND live_attempt.turn_id = turn_row.id
|
|
582
|
+
AND live_attempt.id = turn_row.active_attempt_id
|
|
583
|
+
AND live_attempt.state IN ('claimed', 'running')
|
|
584
|
+
)
|
|
585
|
+
) THEN
|
|
586
|
+
RAISE EXCEPTION 'pending tool calls require canonical protocol settlement'
|
|
587
|
+
USING ERRCODE = '55000';
|
|
588
|
+
END IF;
|
|
589
|
+
|
|
590
|
+
SELECT COALESCE(pg_catalog.array_agg(DISTINCT turn_row.session_id), ARRAY[]::uuid[])
|
|
591
|
+
INTO affected_session_ids
|
|
592
|
+
FROM session_turns turn_row
|
|
593
|
+
JOIN sessions owned
|
|
594
|
+
ON owned.account_id = turn_row.account_id
|
|
595
|
+
AND owned.workspace_id = turn_row.workspace_id
|
|
596
|
+
AND owned.id = turn_row.session_id
|
|
597
|
+
WHERE turn_row.account_id = account_id_value
|
|
598
|
+
AND turn_row.workspace_id = workspace_id_value
|
|
599
|
+
AND turn_row.status IN (
|
|
600
|
+
'queued', 'running', 'requires_action', 'recovering', 'waiting_capacity'
|
|
601
|
+
)
|
|
602
|
+
AND (
|
|
603
|
+
(target_membership_id IS NOT NULL
|
|
604
|
+
AND owned.owner_organization_membership_id = target_membership_id)
|
|
605
|
+
OR turn_row.initiating_human_subject_id = target_subject
|
|
606
|
+
);
|
|
607
|
+
affected_session_ids := ARRAY(
|
|
608
|
+
SELECT DISTINCT candidate.session_id
|
|
609
|
+
FROM (
|
|
610
|
+
SELECT unnest(affected_session_ids) AS session_id
|
|
611
|
+
UNION ALL
|
|
612
|
+
SELECT realtime.session_id
|
|
613
|
+
FROM session_realtime_modes realtime
|
|
614
|
+
WHERE realtime.id = ANY(affected_realtime_ids)
|
|
615
|
+
) candidate
|
|
616
|
+
ORDER BY candidate.session_id
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
interruption_operation_id := pg_catalog.gen_random_uuid();
|
|
620
|
+
INSERT INTO session_command_receipts (
|
|
621
|
+
id, account_id, workspace_id, actor_type, actor_subject_id,
|
|
622
|
+
action, operation_key, canonical_request_hash, result
|
|
623
|
+
) VALUES (
|
|
624
|
+
interruption_operation_id, account_id_value, workspace_id_value,
|
|
625
|
+
'human', actor_subject, 'workspace.membership.remove',
|
|
626
|
+
'workspace-membership:' || operation_id_value::text,
|
|
627
|
+
input_hash_value,
|
|
628
|
+
pg_catalog.jsonb_build_object(
|
|
629
|
+
'workspaceMembershipOperationId', operation_id_value,
|
|
630
|
+
'workspaceMembershipId', membership_row_id,
|
|
631
|
+
'targetSubjectId', target_subject
|
|
632
|
+
)
|
|
633
|
+
);
|
|
634
|
+
INSERT INTO session_attempt_interruptions (
|
|
635
|
+
account_id, workspace_id, session_id, operation_id, attempt_id,
|
|
636
|
+
kind, control_revision
|
|
637
|
+
)
|
|
638
|
+
SELECT attempt.account_id, attempt.workspace_id, attempt.session_id,
|
|
639
|
+
interruption_operation_id, attempt.id, 'authority_change',
|
|
640
|
+
CASE WHEN target_membership_id IS NOT NULL
|
|
641
|
+
AND owned.owner_organization_membership_id = target_membership_id
|
|
642
|
+
THEN owned.authority_epoch + 1 ELSE owned.authority_epoch END
|
|
643
|
+
FROM session_turn_attempts attempt
|
|
644
|
+
JOIN sessions owned ON owned.id = attempt.session_id
|
|
645
|
+
JOIN session_turns initiated ON initiated.id = attempt.turn_id
|
|
646
|
+
WHERE attempt.account_id = account_id_value
|
|
647
|
+
AND attempt.workspace_id = workspace_id_value
|
|
648
|
+
AND (
|
|
649
|
+
(target_membership_id IS NOT NULL
|
|
650
|
+
AND owned.owner_organization_membership_id = target_membership_id)
|
|
651
|
+
OR initiated.initiating_human_subject_id = target_subject
|
|
652
|
+
)
|
|
653
|
+
AND attempt.state IN ('claimed', 'running')
|
|
654
|
+
ON CONFLICT ON CONSTRAINT session_attempt_interruptions_operation_attempt_uq
|
|
655
|
+
DO NOTHING;
|
|
656
|
+
|
|
657
|
+
WITH cancelled AS (
|
|
658
|
+
UPDATE session_turns turn_row
|
|
659
|
+
SET status = 'cancelled', active_attempt_id = NULL,
|
|
660
|
+
cancelled_by = actor_subject, cancel_reason = 'authority_changed',
|
|
661
|
+
finished_at = now_value, updated_at = now_value,
|
|
662
|
+
version = turn_row.version + 1
|
|
663
|
+
WHERE turn_row.account_id = account_id_value
|
|
664
|
+
AND turn_row.workspace_id = workspace_id_value
|
|
665
|
+
AND turn_row.status IN (
|
|
666
|
+
'queued', 'running', 'requires_action', 'recovering', 'waiting_capacity'
|
|
667
|
+
)
|
|
668
|
+
AND EXISTS (
|
|
669
|
+
SELECT 1 FROM sessions owned
|
|
670
|
+
WHERE owned.account_id = turn_row.account_id
|
|
671
|
+
AND owned.workspace_id = turn_row.workspace_id
|
|
672
|
+
AND owned.id = turn_row.session_id
|
|
673
|
+
AND (
|
|
674
|
+
(target_membership_id IS NOT NULL
|
|
675
|
+
AND owned.owner_organization_membership_id = target_membership_id)
|
|
676
|
+
OR turn_row.initiating_human_subject_id = target_subject
|
|
677
|
+
)
|
|
678
|
+
)
|
|
679
|
+
AND NOT EXISTS (
|
|
680
|
+
SELECT 1 FROM session_turn_attempts live_attempt
|
|
681
|
+
WHERE live_attempt.account_id = turn_row.account_id
|
|
682
|
+
AND live_attempt.workspace_id = turn_row.workspace_id
|
|
683
|
+
AND live_attempt.session_id = turn_row.session_id
|
|
684
|
+
AND live_attempt.turn_id = turn_row.id
|
|
685
|
+
AND live_attempt.id = turn_row.active_attempt_id
|
|
686
|
+
AND live_attempt.state IN ('claimed', 'running')
|
|
687
|
+
)
|
|
688
|
+
RETURNING turn_row.id
|
|
689
|
+
)
|
|
690
|
+
SELECT COALESCE(pg_catalog.array_agg(cancelled.id ORDER BY cancelled.id), ARRAY[]::uuid[])
|
|
691
|
+
INTO cancelled_turn_ids
|
|
692
|
+
FROM cancelled;
|
|
693
|
+
|
|
694
|
+
FOR cancelled_turn IN
|
|
695
|
+
SELECT turn_row.id, turn_row.session_id, turn_row.execution_generation
|
|
696
|
+
FROM session_turns turn_row
|
|
697
|
+
WHERE turn_row.account_id = account_id_value
|
|
698
|
+
AND turn_row.workspace_id = workspace_id_value
|
|
699
|
+
AND turn_row.id = ANY(cancelled_turn_ids)
|
|
700
|
+
ORDER BY turn_row.session_id, turn_row.id
|
|
701
|
+
LOOP
|
|
702
|
+
SELECT affected.last_sequence,
|
|
703
|
+
CASE WHEN affected.active_turn_id = cancelled_turn.id
|
|
704
|
+
THEN 'current' ELSE NULL END
|
|
705
|
+
INTO cancelled_sequence, cancelled_association
|
|
706
|
+
FROM sessions affected
|
|
707
|
+
WHERE affected.account_id = account_id_value
|
|
708
|
+
AND affected.workspace_id = workspace_id_value
|
|
709
|
+
AND affected.id = cancelled_turn.session_id;
|
|
710
|
+
FOR cancelled_request IN
|
|
711
|
+
UPDATE session_human_input_requests request_row
|
|
712
|
+
SET status = 'cancelled', response = '{"outcome":"cancelled"}'::jsonb,
|
|
713
|
+
responded_by = actor_subject, responded_at = now_value,
|
|
714
|
+
updated_at = now_value
|
|
715
|
+
WHERE request_row.account_id = account_id_value
|
|
716
|
+
AND request_row.workspace_id = workspace_id_value
|
|
717
|
+
AND request_row.session_id = cancelled_turn.session_id
|
|
718
|
+
AND request_row.turn_id = cancelled_turn.id
|
|
719
|
+
AND request_row.status = 'pending'
|
|
720
|
+
RETURNING request_row.id, request_row.creation_attempt_id
|
|
721
|
+
LOOP
|
|
722
|
+
cancelled_sequence := cancelled_sequence + 1;
|
|
723
|
+
INSERT INTO session_events (
|
|
724
|
+
account_id, workspace_id, session_id, sequence, type, payload,
|
|
725
|
+
payload_codec_version, turn_id, turn_generation,
|
|
726
|
+
turn_attempt_id, turn_association, occurred_at
|
|
727
|
+
) VALUES (
|
|
728
|
+
account_id_value, workspace_id_value, cancelled_turn.session_id,
|
|
729
|
+
cancelled_sequence, 'user.humanInputResponse',
|
|
730
|
+
pg_catalog.jsonb_build_object(
|
|
731
|
+
'requestId', cancelled_request.id,
|
|
732
|
+
'response', pg_catalog.jsonb_build_object('outcome', 'cancelled')
|
|
733
|
+
), 1, cancelled_turn.id, cancelled_turn.execution_generation,
|
|
734
|
+
cancelled_request.creation_attempt_id, cancelled_association, now_value
|
|
735
|
+
);
|
|
736
|
+
END LOOP;
|
|
737
|
+
SELECT
|
|
738
|
+
pg_catalog.array_agg(
|
|
739
|
+
update_row.id ORDER BY update_row.created_at, update_row.id
|
|
740
|
+
),
|
|
741
|
+
(pg_catalog.array_agg(
|
|
742
|
+
update_row.delivered_history_item_id
|
|
743
|
+
ORDER BY update_row.created_at, update_row.id
|
|
744
|
+
))[1]
|
|
745
|
+
INTO settled_update_ids, settled_history_item_id
|
|
746
|
+
FROM session_system_updates update_row
|
|
747
|
+
WHERE update_row.account_id = account_id_value
|
|
748
|
+
AND update_row.workspace_id = workspace_id_value
|
|
749
|
+
AND update_row.session_id = cancelled_turn.session_id
|
|
750
|
+
AND update_row.delivered_turn_id = cancelled_turn.id
|
|
751
|
+
AND update_row.state = 'delivered';
|
|
752
|
+
IF COALESCE(pg_catalog.array_length(settled_update_ids, 1), 0) > 0
|
|
753
|
+
AND NOT EXISTS (
|
|
754
|
+
SELECT 1 FROM session_events prior_settlement
|
|
755
|
+
WHERE prior_settlement.account_id = account_id_value
|
|
756
|
+
AND prior_settlement.workspace_id = workspace_id_value
|
|
757
|
+
AND prior_settlement.session_id = cancelled_turn.session_id
|
|
758
|
+
AND prior_settlement.turn_id = cancelled_turn.id
|
|
759
|
+
AND prior_settlement.type = 'system.update.settled'
|
|
760
|
+
)
|
|
761
|
+
THEN
|
|
762
|
+
cancelled_sequence := cancelled_sequence + 1;
|
|
763
|
+
INSERT INTO session_events (
|
|
764
|
+
account_id, workspace_id, session_id, sequence, type, payload,
|
|
765
|
+
payload_codec_version, turn_id, turn_generation,
|
|
766
|
+
turn_association, occurred_at
|
|
767
|
+
) VALUES (
|
|
768
|
+
account_id_value, workspace_id_value, cancelled_turn.session_id,
|
|
769
|
+
cancelled_sequence, 'system.update.settled',
|
|
770
|
+
pg_catalog.jsonb_build_object(
|
|
771
|
+
'updateIds', pg_catalog.to_jsonb(settled_update_ids),
|
|
772
|
+
'count', pg_catalog.array_length(settled_update_ids, 1),
|
|
773
|
+
'historyItemId', settled_history_item_id,
|
|
774
|
+
'outcome', 'cancelled'
|
|
775
|
+
), 1, cancelled_turn.id, cancelled_turn.execution_generation,
|
|
776
|
+
cancelled_association, now_value
|
|
777
|
+
);
|
|
778
|
+
END IF;
|
|
779
|
+
IF NOT EXISTS (
|
|
780
|
+
SELECT 1 FROM session_events prior_cancel
|
|
781
|
+
WHERE prior_cancel.account_id = account_id_value
|
|
782
|
+
AND prior_cancel.workspace_id = workspace_id_value
|
|
783
|
+
AND prior_cancel.session_id = cancelled_turn.session_id
|
|
784
|
+
AND prior_cancel.turn_id = cancelled_turn.id
|
|
785
|
+
AND prior_cancel.type = 'turn.cancelled'
|
|
786
|
+
) THEN
|
|
787
|
+
cancelled_sequence := cancelled_sequence + 1;
|
|
788
|
+
INSERT INTO session_events (
|
|
789
|
+
account_id, workspace_id, session_id, sequence, type, payload,
|
|
790
|
+
payload_codec_version, turn_id, turn_generation,
|
|
791
|
+
turn_association, occurred_at
|
|
792
|
+
) VALUES (
|
|
793
|
+
account_id_value, workspace_id_value, cancelled_turn.session_id,
|
|
794
|
+
cancelled_sequence, 'turn.cancelled',
|
|
795
|
+
pg_catalog.jsonb_build_object('reason', 'authority_changed'),
|
|
796
|
+
1, cancelled_turn.id, cancelled_turn.execution_generation,
|
|
797
|
+
cancelled_association, now_value
|
|
798
|
+
);
|
|
799
|
+
END IF;
|
|
800
|
+
UPDATE sessions affected
|
|
801
|
+
SET last_sequence = cancelled_sequence, updated_at = now_value
|
|
802
|
+
WHERE affected.account_id = account_id_value
|
|
803
|
+
AND affected.workspace_id = workspace_id_value
|
|
804
|
+
AND affected.id = cancelled_turn.session_id;
|
|
805
|
+
END LOOP;
|
|
806
|
+
|
|
807
|
+
UPDATE interaction_interventions intervention
|
|
808
|
+
SET status = 'cancelled', response_actor_subject_id = actor_subject,
|
|
809
|
+
version = intervention.version + 1, settled_at = now_value,
|
|
810
|
+
updated_at = now_value
|
|
811
|
+
WHERE intervention.account_id = account_id_value
|
|
812
|
+
AND intervention.workspace_id = workspace_id_value
|
|
813
|
+
AND intervention.originating_turn_id = ANY(cancelled_turn_ids)
|
|
814
|
+
AND intervention.status = 'open';
|
|
815
|
+
DELETE FROM agent_run_states run_state
|
|
816
|
+
WHERE run_state.account_id = account_id_value
|
|
817
|
+
AND run_state.workspace_id = workspace_id_value
|
|
818
|
+
AND run_state.turn_id = ANY(cancelled_turn_ids);
|
|
819
|
+
UPDATE codex_capacity_waiters waiter
|
|
820
|
+
SET status = 'superseded', observed_wake_revision = waiter.wake_revision,
|
|
821
|
+
last_wake_reason = 'workspace_membership_remove',
|
|
822
|
+
updated_at = now_value
|
|
823
|
+
WHERE waiter.account_id = account_id_value
|
|
824
|
+
AND waiter.workspace_id = workspace_id_value
|
|
825
|
+
AND waiter.blocked_turn_id = ANY(cancelled_turn_ids)
|
|
826
|
+
AND waiter.status = 'waiting';
|
|
827
|
+
UPDATE xai_capacity_waiters waiter
|
|
828
|
+
SET status = 'superseded', observed_wake_revision = waiter.wake_revision,
|
|
829
|
+
last_wake_reason = 'workspace_membership_remove',
|
|
830
|
+
updated_at = now_value
|
|
831
|
+
WHERE waiter.account_id = account_id_value
|
|
832
|
+
AND waiter.workspace_id = workspace_id_value
|
|
833
|
+
AND waiter.blocked_turn_id = ANY(cancelled_turn_ids)
|
|
834
|
+
AND waiter.status = 'waiting';
|
|
835
|
+
UPDATE session_realtime_connections connection
|
|
836
|
+
SET state = 'closed', closed_at = now_value, updated_at = now_value
|
|
837
|
+
WHERE connection.account_id = account_id_value
|
|
838
|
+
AND connection.workspace_id = workspace_id_value
|
|
839
|
+
AND connection.realtime_id = ANY(affected_realtime_ids)
|
|
840
|
+
AND connection.state IN ('negotiating', 'ready', 'active');
|
|
841
|
+
WITH ended AS (
|
|
842
|
+
UPDATE session_realtime_modes realtime
|
|
843
|
+
SET state = 'ended', version = realtime.version + 1,
|
|
844
|
+
ended_at = now_value, end_reason = 'authority_revoked',
|
|
845
|
+
updated_at = now_value
|
|
846
|
+
WHERE realtime.account_id = account_id_value
|
|
847
|
+
AND realtime.workspace_id = workspace_id_value
|
|
848
|
+
AND realtime.id = ANY(affected_realtime_ids)
|
|
849
|
+
AND realtime.state = 'active'
|
|
850
|
+
RETURNING realtime.id, realtime.session_id, realtime.operation_id,
|
|
851
|
+
realtime.version, realtime.connection_epoch
|
|
852
|
+
), advanced AS (
|
|
853
|
+
UPDATE sessions affected
|
|
854
|
+
SET last_sequence = affected.last_sequence + 1, updated_at = now_value
|
|
855
|
+
FROM ended
|
|
856
|
+
WHERE affected.account_id = account_id_value
|
|
857
|
+
AND affected.workspace_id = workspace_id_value
|
|
858
|
+
AND affected.id = ended.session_id
|
|
859
|
+
RETURNING affected.id AS session_id, affected.last_sequence,
|
|
860
|
+
ended.id AS realtime_id, ended.operation_id,
|
|
861
|
+
ended.version, ended.connection_epoch
|
|
862
|
+
)
|
|
863
|
+
INSERT INTO session_events (
|
|
864
|
+
account_id, workspace_id, session_id, sequence, type, payload, occurred_at
|
|
865
|
+
)
|
|
866
|
+
SELECT account_id_value, workspace_id_value, advanced.session_id,
|
|
867
|
+
advanced.last_sequence, 'session.realtime.ended',
|
|
868
|
+
pg_catalog.jsonb_build_object(
|
|
869
|
+
'realtimeId', advanced.realtime_id,
|
|
870
|
+
'operationId', advanced.operation_id,
|
|
871
|
+
'reason', 'authority_revoked',
|
|
872
|
+
'version', advanced.version,
|
|
873
|
+
'connectionEpoch', advanced.connection_epoch
|
|
874
|
+
), now_value
|
|
875
|
+
FROM advanced;
|
|
876
|
+
UPDATE sessions affected
|
|
877
|
+
SET active_turn_id = CASE
|
|
878
|
+
WHEN affected.active_turn_id = ANY(cancelled_turn_ids) THEN NULL
|
|
879
|
+
ELSE affected.active_turn_id
|
|
880
|
+
END,
|
|
881
|
+
status = CASE
|
|
882
|
+
WHEN affected.active_turn_id = ANY(cancelled_turn_ids) THEN
|
|
883
|
+
CASE WHEN EXISTS (
|
|
884
|
+
SELECT 1 FROM session_turns queued
|
|
885
|
+
WHERE queued.account_id = affected.account_id
|
|
886
|
+
AND queued.workspace_id = affected.workspace_id
|
|
887
|
+
AND queued.session_id = affected.id
|
|
888
|
+
AND queued.status = 'queued'
|
|
889
|
+
) THEN 'queued' ELSE 'idle' END
|
|
890
|
+
ELSE affected.status
|
|
891
|
+
END,
|
|
892
|
+
queue_head_position = COALESCE((
|
|
893
|
+
SELECT min(queued.position)::bigint FROM session_turns queued
|
|
894
|
+
WHERE queued.account_id = affected.account_id
|
|
895
|
+
AND queued.workspace_id = affected.workspace_id
|
|
896
|
+
AND queued.session_id = affected.id
|
|
897
|
+
AND queued.status = 'queued'
|
|
898
|
+
), 0),
|
|
899
|
+
queue_tail_position = COALESCE((
|
|
900
|
+
SELECT max(queued.position)::bigint FROM session_turns queued
|
|
901
|
+
WHERE queued.account_id = affected.account_id
|
|
902
|
+
AND queued.workspace_id = affected.workspace_id
|
|
903
|
+
AND queued.session_id = affected.id
|
|
904
|
+
AND queued.status = 'queued'
|
|
905
|
+
), 0),
|
|
906
|
+
queue_version = affected.queue_version + 1,
|
|
907
|
+
updated_at = now_value
|
|
908
|
+
WHERE affected.account_id = account_id_value
|
|
909
|
+
AND affected.workspace_id = workspace_id_value
|
|
910
|
+
AND EXISTS (
|
|
911
|
+
SELECT 1 FROM session_turns cancelled
|
|
912
|
+
WHERE cancelled.account_id = affected.account_id
|
|
913
|
+
AND cancelled.workspace_id = affected.workspace_id
|
|
914
|
+
AND cancelled.session_id = affected.id
|
|
915
|
+
AND cancelled.id = ANY(cancelled_turn_ids)
|
|
916
|
+
);
|
|
917
|
+
UPDATE session_system_updates update_row SET state = 'cancelled'
|
|
918
|
+
WHERE update_row.account_id = account_id_value
|
|
919
|
+
AND update_row.workspace_id = workspace_id_value
|
|
920
|
+
AND update_row.state = 'pending'
|
|
921
|
+
AND target_membership_id IS NOT NULL
|
|
922
|
+
AND EXISTS (
|
|
923
|
+
SELECT 1 FROM sessions owned
|
|
924
|
+
WHERE owned.id = update_row.session_id
|
|
925
|
+
AND owned.owner_organization_membership_id = target_membership_id
|
|
926
|
+
);
|
|
927
|
+
UPDATE session_system_update_outbox outbox
|
|
928
|
+
SET status = 'failed', last_error = 'workspace_membership_remove',
|
|
929
|
+
updated_at = now_value
|
|
930
|
+
WHERE outbox.account_id = account_id_value
|
|
931
|
+
AND outbox.workspace_id = workspace_id_value
|
|
932
|
+
AND outbox.status = 'pending'
|
|
933
|
+
AND target_membership_id IS NOT NULL
|
|
934
|
+
AND EXISTS (
|
|
935
|
+
SELECT 1 FROM sessions source_session
|
|
936
|
+
WHERE source_session.id = outbox.source_session_id
|
|
937
|
+
AND source_session.owner_organization_membership_id = target_membership_id
|
|
938
|
+
);
|
|
939
|
+
UPDATE session_goals goal_row
|
|
940
|
+
SET status = 'paused', paused_reason = 'api',
|
|
941
|
+
rationale = 'Workspace membership was removed; explicit reauthorization is required.',
|
|
942
|
+
version = goal_row.version + 1, updated_at = now_value
|
|
943
|
+
WHERE goal_row.account_id = account_id_value
|
|
944
|
+
AND goal_row.workspace_id = workspace_id_value
|
|
945
|
+
AND goal_row.status = 'active'
|
|
946
|
+
AND target_membership_id IS NOT NULL
|
|
947
|
+
AND EXISTS (
|
|
948
|
+
SELECT 1 FROM sessions owned
|
|
949
|
+
WHERE owned.id = goal_row.session_id
|
|
950
|
+
AND owned.owner_organization_membership_id = target_membership_id
|
|
951
|
+
);
|
|
952
|
+
UPDATE scheduled_tasks task
|
|
953
|
+
SET status = 'paused', personal_connection_delegations = '[]'::jsonb,
|
|
954
|
+
authority_revision = task.authority_revision + 1,
|
|
955
|
+
updated_at = now_value
|
|
956
|
+
WHERE task.account_id = account_id_value
|
|
957
|
+
AND task.workspace_id = workspace_id_value
|
|
958
|
+
AND task.deleted_at IS NULL
|
|
959
|
+
AND task.status = 'active'
|
|
960
|
+
AND (
|
|
961
|
+
(task.created_by_kind = 'subject' AND task.created_by_subject_id = target_subject)
|
|
962
|
+
OR EXISTS (
|
|
963
|
+
SELECT 1 FROM scheduled_task_revision_authorities authority
|
|
964
|
+
WHERE authority.task_id = task.id
|
|
965
|
+
AND authority.task_authority_revision = task.authority_revision
|
|
966
|
+
AND authority.subject_id = target_subject
|
|
967
|
+
)
|
|
968
|
+
OR EXISTS (
|
|
969
|
+
SELECT 1 FROM scheduled_task_personal_resource_authorities authority
|
|
970
|
+
WHERE authority.task_id = task.id
|
|
971
|
+
AND authority.task_authority_revision = task.authority_revision
|
|
972
|
+
AND authority.initiating_human_subject_id = target_subject
|
|
973
|
+
)
|
|
974
|
+
OR EXISTS (
|
|
975
|
+
SELECT 1 FROM scheduled_task_connection_authority_snapshots snapshot
|
|
976
|
+
WHERE snapshot.task_id = task.id
|
|
977
|
+
AND snapshot.task_authority_revision = task.authority_revision
|
|
978
|
+
AND snapshot.owner_subject_id = target_subject
|
|
979
|
+
)
|
|
980
|
+
);
|
|
981
|
+
UPDATE organization_user_resource_grants grant_row
|
|
982
|
+
SET status = 'revoked', revoked_at = now_value,
|
|
983
|
+
generation = grant_row.generation + 1, updated_at = now_value
|
|
984
|
+
WHERE grant_row.account_id = account_id_value
|
|
985
|
+
AND grant_row.workspace_id = workspace_id_value
|
|
986
|
+
AND grant_row.status = 'active'
|
|
987
|
+
AND target_membership_id IS NOT NULL
|
|
988
|
+
AND grant_row.owner_organization_membership_id = target_membership_id;
|
|
989
|
+
UPDATE organization_user_resource_grants grant_row
|
|
990
|
+
SET status = 'revoked', revoked_at = now_value,
|
|
991
|
+
generation = grant_row.generation + 1, updated_at = now_value
|
|
992
|
+
WHERE grant_row.account_id = account_id_value
|
|
993
|
+
AND grant_row.workspace_id = workspace_id_value
|
|
994
|
+
AND grant_row.status = 'active'
|
|
995
|
+
AND target_membership_id IS NOT NULL
|
|
996
|
+
AND EXISTS (
|
|
997
|
+
SELECT 1 FROM sessions owned
|
|
998
|
+
WHERE owned.id = grant_row.session_id
|
|
999
|
+
AND owned.owner_organization_membership_id = target_membership_id
|
|
1000
|
+
AND owned.authority_epoch = grant_row.authority_epoch
|
|
1001
|
+
);
|
|
1002
|
+
IF target_membership_id IS NOT NULL THEN
|
|
1003
|
+
WITH advanced AS (
|
|
1004
|
+
UPDATE sessions owned SET
|
|
1005
|
+
authority_epoch = owned.authority_epoch + 1,
|
|
1006
|
+
initial_personal_connection_delegations = '[]'::jsonb,
|
|
1007
|
+
last_sequence = owned.last_sequence + 1
|
|
1008
|
+
WHERE owned.account_id = account_id_value
|
|
1009
|
+
AND owned.workspace_id = workspace_id_value
|
|
1010
|
+
AND owned.owner_organization_membership_id = target_membership_id
|
|
1011
|
+
RETURNING owned.id, owned.workspace_id, owned.last_sequence,
|
|
1012
|
+
owned.authority_epoch
|
|
1013
|
+
)
|
|
1014
|
+
INSERT INTO session_events (
|
|
1015
|
+
account_id, workspace_id, session_id, sequence, type, payload, occurred_at
|
|
1016
|
+
)
|
|
1017
|
+
SELECT account_id_value, advanced.workspace_id, advanced.id,
|
|
1018
|
+
advanced.last_sequence, 'session.authority.revoked',
|
|
1019
|
+
pg_catalog.jsonb_build_object(
|
|
1020
|
+
'operationId', operation_id_value,
|
|
1021
|
+
'organizationMembershipId', target_membership_id,
|
|
1022
|
+
'previousAuthorityEpoch', advanced.authority_epoch - 1,
|
|
1023
|
+
'authorityEpoch', advanced.authority_epoch
|
|
1024
|
+
), now_value
|
|
1025
|
+
FROM advanced;
|
|
1026
|
+
END IF;
|
|
1027
|
+
INSERT INTO session_workflow_wake_outbox (
|
|
1028
|
+
session_id, account_id, workspace_id, temporal_workflow_id,
|
|
1029
|
+
reason, control_revision
|
|
1030
|
+
)
|
|
1031
|
+
SELECT affected.id, affected.account_id, affected.workspace_id,
|
|
1032
|
+
COALESCE(affected.temporal_workflow_id, 'session-' || affected.id::text),
|
|
1033
|
+
'workspace_membership_remove', 1
|
|
1034
|
+
FROM sessions affected
|
|
1035
|
+
WHERE affected.account_id = account_id_value
|
|
1036
|
+
AND affected.workspace_id = workspace_id_value
|
|
1037
|
+
AND affected.id = ANY(affected_session_ids)
|
|
1038
|
+
ON CONFLICT (session_id) DO UPDATE SET
|
|
1039
|
+
wake_revision = session_workflow_wake_outbox.wake_revision + 1,
|
|
1040
|
+
control_revision = session_workflow_wake_outbox.wake_revision + 1,
|
|
1041
|
+
temporal_workflow_id = EXCLUDED.temporal_workflow_id,
|
|
1042
|
+
reason = EXCLUDED.reason, attempts = 0,
|
|
1043
|
+
next_attempt_at = now_value, last_error = NULL,
|
|
1044
|
+
updated_at = now_value;
|
|
1045
|
+
|
|
1046
|
+
-- Personal per-workspace rows and the membership itself, then the activity
|
|
1047
|
+
-- gate finalization exactly as the organization lifecycle performs it.
|
|
1048
|
+
DELETE FROM session_list_snapshots snapshot
|
|
1049
|
+
WHERE snapshot.workspace_id = workspace_id_value
|
|
1050
|
+
AND snapshot.subject_id = target_subject;
|
|
1051
|
+
DELETE FROM session_pins pin
|
|
1052
|
+
WHERE pin.workspace_id = workspace_id_value
|
|
1053
|
+
AND pin.subject_id = target_subject;
|
|
1054
|
+
DELETE FROM new_session_drafts draft
|
|
1055
|
+
WHERE draft.workspace_id = workspace_id_value
|
|
1056
|
+
AND draft.subject_id = target_subject;
|
|
1057
|
+
DELETE FROM workspace_memberships membership
|
|
1058
|
+
WHERE membership.id = membership_row_id;
|
|
1059
|
+
|
|
1060
|
+
PERFORM pg_catalog.set_config(
|
|
1061
|
+
'opengeni.session_activity_gate_state', 'preparing', true
|
|
1062
|
+
);
|
|
1063
|
+
SET CONSTRAINTS ALL IMMEDIATE;
|
|
1064
|
+
SET CONSTRAINTS sessions_activity_insert_commit_guard,
|
|
1065
|
+
sessions_activity_update_commit_guard DEFERRED;
|
|
1066
|
+
PERFORM pg_catalog.set_config(
|
|
1067
|
+
'opengeni.session_activity_gate_state', 'finalizing', true
|
|
1068
|
+
);
|
|
1069
|
+
UPDATE workspace_session_activity_revisions counter
|
|
1070
|
+
SET revision = counter.revision + 1
|
|
1071
|
+
WHERE counter.workspace_id = workspace_id_value
|
|
1072
|
+
RETURNING counter.revision INTO activity_revision_value;
|
|
1073
|
+
IF activity_revision_value IS NULL THEN
|
|
1074
|
+
RAISE EXCEPTION 'workspace membership session activity counter is unavailable'
|
|
1075
|
+
USING ERRCODE = '55000';
|
|
1076
|
+
END IF;
|
|
1077
|
+
UPDATE sessions affected
|
|
1078
|
+
SET activity_revision = activity_revision_value,
|
|
1079
|
+
activity_revision_pending_xid = NULL
|
|
1080
|
+
WHERE affected.account_id = account_id_value
|
|
1081
|
+
AND affected.workspace_id = workspace_id_value
|
|
1082
|
+
AND affected.activity_revision_pending_xid
|
|
1083
|
+
= pg_catalog.pg_current_xact_id()::text::bigint;
|
|
1084
|
+
SET CONSTRAINTS sessions_activity_insert_commit_guard,
|
|
1085
|
+
sessions_activity_update_commit_guard IMMEDIATE;
|
|
1086
|
+
|
|
1087
|
+
DELETE FROM session_visibility_write_capabilities capability
|
|
1088
|
+
WHERE capability.backend_pid = pg_catalog.pg_backend_pid()
|
|
1089
|
+
AND capability.transaction_id = pg_catalog.pg_current_xact_id()
|
|
1090
|
+
AND capability.capability_id = visibility_capability_id;
|
|
1091
|
+
PERFORM pg_catalog.set_config(
|
|
1092
|
+
'opengeni.session_visibility_write_capability',
|
|
1093
|
+
coalesce(previous_visibility_capability, ''), true
|
|
1094
|
+
);
|
|
1095
|
+
PERFORM pg_catalog.set_config(
|
|
1096
|
+
'opengeni.workspace_id', coalesce(previous_workspace, ''), true
|
|
1097
|
+
);
|
|
1098
|
+
PERFORM pg_catalog.set_config(
|
|
1099
|
+
'opengeni.session_activity_gate_state',
|
|
1100
|
+
coalesce(previous_activity_gate_state, ''), true
|
|
1101
|
+
);
|
|
1102
|
+
PERFORM pg_catalog.set_config(
|
|
1103
|
+
'opengeni.session_activity_gate_workspace_id',
|
|
1104
|
+
coalesce(previous_activity_gate_workspace, ''), true
|
|
1105
|
+
);
|
|
1106
|
+
RETURN pg_catalog.jsonb_build_object(
|
|
1107
|
+
'removed', true,
|
|
1108
|
+
'workspaceMembershipId', membership_row_id,
|
|
1109
|
+
'cancelledTurnIds', pg_catalog.to_jsonb(cancelled_turn_ids),
|
|
1110
|
+
'affectedSessionIds', pg_catalog.to_jsonb(affected_session_ids)
|
|
1111
|
+
);
|
|
1112
|
+
EXCEPTION WHEN OTHERS THEN
|
|
1113
|
+
PERFORM pg_catalog.set_config(
|
|
1114
|
+
'opengeni.workspace_id', coalesce(previous_workspace, ''), true
|
|
1115
|
+
);
|
|
1116
|
+
PERFORM pg_catalog.set_config(
|
|
1117
|
+
'opengeni.session_visibility_write_capability',
|
|
1118
|
+
coalesce(previous_visibility_capability, ''), true
|
|
1119
|
+
);
|
|
1120
|
+
PERFORM pg_catalog.set_config(
|
|
1121
|
+
'opengeni.session_activity_gate_state',
|
|
1122
|
+
coalesce(previous_activity_gate_state, ''), true
|
|
1123
|
+
);
|
|
1124
|
+
PERFORM pg_catalog.set_config(
|
|
1125
|
+
'opengeni.session_activity_gate_workspace_id',
|
|
1126
|
+
coalesce(previous_activity_gate_workspace, ''), true
|
|
1127
|
+
);
|
|
1128
|
+
RAISE;
|
|
1129
|
+
END
|
|
1130
|
+
$body$;
|
|
1131
|
+
|
|
1132
|
+
-- 4. Hardening + grants. Same posture as every lifecycle seam: pinned
|
|
1133
|
+
-- search_path, revoked from PUBLIC, EXECUTE granted only to the runtime role
|
|
1134
|
+
-- where it exists (role-free dedicated-schema/embedded replays skip it).
|
|
1135
|
+
|
|
1136
|
+
REVOKE ALL ON FUNCTION prepare_workspace_membership_removal_settlements(jsonb) FROM PUBLIC;
|
|
1137
|
+
REVOKE ALL ON FUNCTION workspace_membership_removal_command(jsonb) FROM PUBLIC;
|
|
1138
|
+
REVOKE ALL ON FUNCTION opengeni_private.assert_workspace_membership_removal_actor(
|
|
1139
|
+
uuid, uuid, text, text
|
|
1140
|
+
) FROM PUBLIC;
|
|
1141
|
+
|
|
1142
|
+
DO $workspace_membership_removal_seam$
|
|
1143
|
+
DECLARE
|
|
1144
|
+
data_schema text := pg_catalog.current_schema();
|
|
1145
|
+
BEGIN
|
|
1146
|
+
EXECUTE format(
|
|
1147
|
+
'ALTER FUNCTION %I.prepare_workspace_membership_removal_settlements(jsonb) '
|
|
1148
|
+
|| 'SET search_path = pg_catalog, %I, pg_temp',
|
|
1149
|
+
data_schema, data_schema
|
|
1150
|
+
);
|
|
1151
|
+
EXECUTE format(
|
|
1152
|
+
'ALTER FUNCTION %I.workspace_membership_removal_command(jsonb) '
|
|
1153
|
+
|| 'SET search_path = pg_catalog, %I, pg_temp',
|
|
1154
|
+
data_schema, data_schema
|
|
1155
|
+
);
|
|
1156
|
+
EXECUTE format(
|
|
1157
|
+
'ALTER FUNCTION opengeni_private.assert_workspace_membership_removal_actor'
|
|
1158
|
+
|| '(uuid,uuid,text,text) SET search_path = pg_catalog, %I, pg_temp',
|
|
1159
|
+
data_schema
|
|
1160
|
+
);
|
|
1161
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
|
|
1162
|
+
EXECUTE format(
|
|
1163
|
+
'GRANT EXECUTE ON FUNCTION %I.prepare_workspace_membership_removal_settlements(jsonb) '
|
|
1164
|
+
|| 'TO opengeni_app',
|
|
1165
|
+
data_schema
|
|
1166
|
+
);
|
|
1167
|
+
EXECUTE format(
|
|
1168
|
+
'GRANT EXECUTE ON FUNCTION %I.workspace_membership_removal_command(jsonb) '
|
|
1169
|
+
|| 'TO opengeni_app',
|
|
1170
|
+
data_schema
|
|
1171
|
+
);
|
|
1172
|
+
END IF;
|
|
1173
|
+
END
|
|
1174
|
+
$workspace_membership_removal_seam$;
|