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