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