@opengeni/db 4.2.2 → 4.3.2-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/dist/canonical-human-identities.js +3 -3
  2. package/dist/{chunk-S4Y42VSX.js → chunk-AI4NC6XW.js} +212 -7
  3. package/dist/chunk-AI4NC6XW.js.map +1 -0
  4. package/dist/{chunk-SNLIKELL.js → chunk-DVSLOUEI.js} +2 -2
  5. package/dist/{chunk-6GAPAYZR.js → chunk-F4BIHJ7J.js} +562 -86
  6. package/dist/chunk-F4BIHJ7J.js.map +1 -0
  7. package/dist/{chunk-B2KR3TEM.js → chunk-I4ITNMX2.js} +2 -2
  8. package/dist/{chunk-MGVOYVPS.js → chunk-PFBUHOI5.js} +3 -3
  9. package/dist/{chunk-6Y7ZDVJ5.js → chunk-UUPY6IGP.js} +2 -2
  10. package/dist/{chunk-FPTI4NP4.js → chunk-UVBPOGA7.js} +2 -2
  11. package/dist/{chunk-7WX3R3YL.js → chunk-XBXANJQS.js} +7 -2
  12. package/dist/chunk-XBXANJQS.js.map +1 -0
  13. package/dist/{chunk-6IQ4X35Y.js → chunk-YVYBAWBU.js} +46 -1
  14. package/dist/chunk-YVYBAWBU.js.map +1 -0
  15. package/dist/connect-attempts.d.ts +60 -0
  16. package/dist/connection-setup-authority.d.ts +10 -0
  17. package/dist/connection-token-resolver.d.ts +11 -2
  18. package/dist/editable-artifact-durable-export.js +2 -2
  19. package/dist/editable-artifacts.js +3 -3
  20. package/dist/external-identities.d.ts +16 -0
  21. package/dist/external-identity-links.d.ts +74 -0
  22. package/dist/external-link-work.d.ts +51 -0
  23. package/dist/host-mcp-binding-match.d.ts +5 -0
  24. package/dist/host-mcp-bindings.d.ts +73 -0
  25. package/dist/host-mcp-task-authority.d.ts +47 -0
  26. package/dist/host-selection-identity.d.ts +6 -0
  27. package/dist/index.d.ts +60 -35
  28. package/dist/index.js +15247 -13793
  29. package/dist/index.js.map +1 -1
  30. package/dist/live-session-attempt.d.ts +275 -0
  31. package/dist/managed-auth-session-sets.js +3 -3
  32. package/dist/memory-domain.d.ts +3 -3
  33. package/dist/provision-roles.js +1 -1
  34. package/dist/retained-provider-commands.js +2 -2
  35. package/dist/runtime-posture.d.ts +6 -6
  36. package/dist/scheduled-task-revision-authority.d.ts +11 -0
  37. package/dist/schema.d.ts +1279 -9
  38. package/dist/schema.js +11 -1
  39. package/dist/session-background-commands.js +4 -4
  40. package/dist/session-command-output.js +4 -4
  41. package/dist/session-event-slices.js +2 -2
  42. package/dist/session-queue-commands.d.ts +7 -0
  43. package/dist/session-tenancy.d.ts +2 -0
  44. package/dist/session-tenancy.js +3 -3
  45. package/dist/video-generation.js +3 -3
  46. package/dist/workspace-authority.d.ts +4 -4
  47. package/dist/workspace-tool-defaults.js +4 -4
  48. package/drizzle/0437_organization_scoped_external_workspaces.sql +71 -0
  49. package/drizzle/0438_durable_connect_attempts.sql +95 -0
  50. package/drizzle/0439_external_identity_provisioning.sql +128 -0
  51. package/drizzle/0440_external_workspace_member_removal.sql +59 -0
  52. package/drizzle/0441_external_identity_membership_lifecycle.sql +163 -0
  53. package/drizzle/0442_external_owning_user_authority.sql +81 -0
  54. package/drizzle/0443_host_mcp_binding_registry.sql +93 -0
  55. package/drizzle/0444_host_mcp_delegations.sql +129 -0
  56. package/drizzle/0445_host_mcp_turn_authorities.sql +145 -0
  57. package/drizzle/0446_host_mcp_causal_continuation.sql +126 -0
  58. package/drizzle/0447_host_mcp_task_authorities.sql +264 -0
  59. package/drizzle/0448_host_mcp_child_authority.sql +115 -0
  60. package/drizzle/0449_external_identity_link_lifecycle.sql +107 -0
  61. package/drizzle/0450_external_identity_link_work.sql +179 -0
  62. package/drizzle/0451_external_link_preview_and_permission_ceiling.sql +82 -0
  63. package/drizzle/0452_external_link_scheduled_origin.sql +34 -0
  64. package/drizzle/0453_host_mcp_native_owner.sql +35 -0
  65. package/drizzle/0454_connect_origin_authority.sql +42 -0
  66. package/drizzle/0455_external_link_inventory_labels.sql +44 -0
  67. package/drizzle/0456_social_connection_versions.sql +14 -0
  68. package/drizzle/0457_canonical_session_scope_subject.sql +46 -0
  69. package/drizzle/0458_skill_review_wire_compatibility.sql +36 -0
  70. package/package.json +6 -6
  71. package/src/connect-attempts.ts +317 -0
  72. package/src/connection-authority.ts +9 -0
  73. package/src/connection-setup-authority.ts +35 -0
  74. package/src/connection-token-resolver.ts +177 -48
  75. package/src/external-identities.ts +70 -0
  76. package/src/external-identity-links.ts +364 -0
  77. package/src/external-link-work.ts +228 -0
  78. package/src/host-mcp-binding-match.ts +33 -0
  79. package/src/host-mcp-bindings.ts +741 -0
  80. package/src/host-mcp-task-authority.ts +383 -0
  81. package/src/host-selection-identity.ts +29 -0
  82. package/src/index.ts +481 -151
  83. package/src/live-session-attempt.ts +85 -0
  84. package/src/managed-human-provisioning.ts +6 -1
  85. package/src/memory-domain.ts +6 -11
  86. package/src/provision-roles.ts +3 -0
  87. package/src/runtime-posture.ts +47 -0
  88. package/src/scheduled-task-revision-authority.ts +30 -0
  89. package/src/schema.ts +233 -7
  90. package/src/session-control.ts +14 -0
  91. package/src/session-queue-commands.ts +11 -0
  92. package/src/session-tenancy.ts +7 -0
  93. package/src/workspace-artifacts.ts +10 -4
  94. package/src/workspace-authority.ts +5 -5
  95. package/dist/chunk-6GAPAYZR.js.map +0 -1
  96. package/dist/chunk-6IQ4X35Y.js.map +0 -1
  97. package/dist/chunk-7WX3R3YL.js.map +0 -1
  98. package/dist/chunk-S4Y42VSX.js.map +0 -1
  99. /package/dist/{chunk-SNLIKELL.js.map → chunk-DVSLOUEI.js.map} +0 -0
  100. /package/dist/{chunk-B2KR3TEM.js.map → chunk-I4ITNMX2.js.map} +0 -0
  101. /package/dist/{chunk-MGVOYVPS.js.map → chunk-PFBUHOI5.js.map} +0 -0
  102. /package/dist/{chunk-6Y7ZDVJ5.js.map → chunk-UUPY6IGP.js.map} +0 -0
  103. /package/dist/{chunk-FPTI4NP4.js.map → chunk-UVBPOGA7.js.map} +0 -0
@@ -0,0 +1,126 @@
1
+ -- deployment-mode: maintenance
2
+ -- Preserve exact host selections across same-session causal resumptions only.
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' THEN
9
+ RAISE EXCEPTION '0446 requires application database roles' USING ERRCODE = '55000';
10
+ END IF;
11
+ IF jsonb_array_length(roles) NOT BETWEEN 1 AND 16 OR EXISTS (
12
+ SELECT 1 FROM jsonb_array_elements(roles) item WHERE jsonb_typeof(item) <> 'string'
13
+ OR btrim(item #>> '{}') = '' OR item #>> '{}' <> btrim(item #>> '{}')
14
+ OR octet_length(item #>> '{}') > 63
15
+ ) THEN RAISE EXCEPTION '0446 invalid application roles' USING ERRCODE = '55000'; END IF;
16
+ IF EXISTS (SELECT 1 FROM pg_stat_activity a JOIN jsonb_array_elements_text(roles) r ON r = a.usename
17
+ WHERE a.datname = current_database() AND a.pid <> pg_backend_pid()) THEN
18
+ RAISE EXCEPTION '0446 requires stopped application sessions' USING ERRCODE = '55000';
19
+ END IF;
20
+ END
21
+ $drain$;
22
+
23
+ CREATE FUNCTION opengeni_private.guard_host_mcp_inherited_turn_authority() RETURNS trigger
24
+ LANGUAGE plpgsql SET search_path FROM CURRENT AS $body$
25
+ DECLARE
26
+ s sessions%ROWTYPE;
27
+ t session_turns%ROWTYPE;
28
+ prior host_mcp_turn_authorities%ROWTYPE;
29
+ d host_mcp_delegations%ROWTYPE;
30
+ b host_mcp_bindings%ROWTYPE;
31
+ membership jsonb;
32
+ expected jsonb;
33
+ causal_id text;
34
+ BEGIN
35
+ IF TG_OP <> 'INSERT' OR NEW.account_id IS DISTINCT FROM opengeni_private.current_account_id()
36
+ OR NEW.workspace_id IS DISTINCT FROM opengeni_private.current_workspace_id()
37
+ OR NEW.owner_subject_id IS DISTINCT FROM opengeni_private.current_subject_id()
38
+ OR current_setting('opengeni.session_inference_claim', true) IS DISTINCT FROM '1' THEN
39
+ RAISE EXCEPTION 'host inherited authority scope unavailable' USING ERRCODE = '42501';
40
+ END IF;
41
+ PERFORM pg_advisory_xact_lock(hashtextextended('organization-membership:' || NEW.account_id::text, 0));
42
+ SELECT value INTO membership FROM jsonb_array_elements(list_self_organization_memberships(NEW.owner_subject_id))
43
+ WHERE value ->> 'organizationId' = NEW.account_id::text AND value ->> 'status' = 'active';
44
+ IF membership IS NULL OR NOT (EXISTS (
45
+ SELECT 1 FROM workspace_memberships w WHERE w.workspace_id = NEW.workspace_id AND w.subject_id = NEW.owner_subject_id
46
+ ) OR coalesce(membership ->> 'personalWorkspaceId' = NEW.workspace_id::text, false)) THEN
47
+ RAISE EXCEPTION 'host inherited membership unavailable' USING ERRCODE = '42501';
48
+ END IF;
49
+ SELECT * INTO s FROM sessions WHERE id = NEW.session_id AND account_id = NEW.account_id AND workspace_id = NEW.workspace_id FOR UPDATE;
50
+ IF NOT FOUND OR (s.visibility = 'user_private' AND s.owner_subject_id IS DISTINCT FROM NEW.owner_subject_id) THEN
51
+ RAISE EXCEPTION 'host inherited session unavailable' USING ERRCODE = '42501';
52
+ END IF;
53
+ SELECT * INTO t FROM session_turns WHERE id = NEW.turn_id AND session_id = NEW.session_id
54
+ AND account_id = NEW.account_id AND workspace_id = NEW.workspace_id FOR UPDATE;
55
+ IF NOT FOUND OR t.source NOT IN ('goal','system') OR t.status <> 'running'
56
+ OR t.execution_generation <> 1 OR t.active_attempt_id IS NULL OR t.scheduled_task_run_id IS NOT NULL
57
+ OR t.initiating_human_subject_id IS DISTINCT FROM NEW.owner_subject_id
58
+ OR EXISTS (SELECT 1 FROM session_turn_attempts a WHERE a.turn_id = t.id AND a.workspace_id = NEW.workspace_id) THEN
59
+ RAISE EXCEPTION 'host inherited turn unavailable' USING ERRCODE = '42501';
60
+ END IF;
61
+ causal_id := NEW.canonical_snapshot #>> '{source,turnId}';
62
+ IF causal_id IS NULL OR causal_id = NEW.turn_id::text
63
+ OR NEW.canonical_snapshot #>> '{source,sessionId}' IS DISTINCT FROM NEW.session_id::text THEN
64
+ RAISE EXCEPTION 'host inherited source unavailable' USING ERRCODE = '42501';
65
+ END IF;
66
+ -- The canonical claim has already delivered the exact machine input. Never
67
+ -- inherit from the latest turn, a creator, or an unconsumed queued notice.
68
+ IF NOT EXISTS (SELECT 1 FROM session_system_updates u
69
+ WHERE u.account_id = NEW.account_id AND u.workspace_id = NEW.workspace_id AND u.session_id = NEW.session_id
70
+ AND u.delivered_turn_id = NEW.turn_id AND u.state = 'delivered' AND u.delivered_history_item_id IS NOT NULL
71
+ AND ((u.kind = 'goal_continuation' AND u.lineage ->> 'causalTurnId' = causal_id)
72
+ OR (u.kind IN ('child_terminal_result','child_requires_action','child_requires_action_resolved','child_paused','child_waiting_capacity','child_progress')
73
+ AND u.lineage ->> 'parentTurnId' = causal_id))) THEN
74
+ RAISE EXCEPTION 'host inherited causal delivery unavailable' USING ERRCODE = '42501';
75
+ END IF;
76
+ SELECT * INTO prior FROM host_mcp_turn_authorities WHERE turn_id = causal_id::uuid
77
+ AND server_id = NEW.server_id AND session_id = NEW.session_id AND workspace_id = NEW.workspace_id
78
+ AND account_id = NEW.account_id AND owner_subject_id = NEW.owner_subject_id;
79
+ IF NOT FOUND OR prior.binding_id <> NEW.binding_id OR prior.delegation_id <> NEW.delegation_id
80
+ OR prior.canonical_snapshot ->> 'targetSessionVisibility' IS DISTINCT FROM s.visibility
81
+ OR prior.canonical_snapshot ->> 'targetSessionAuthorityEpoch' IS DISTINCT FROM s.authority_epoch::text
82
+ OR prior.canonical_snapshot ->> 'ownerOrganizationMembershipId' IS DISTINCT FROM membership ->> 'id'
83
+ OR prior.canonical_snapshot ->> 'ownerMembershipAuthorizationRevision' IS DISTINCT FROM membership ->> 'authorizationRevision' THEN
84
+ RAISE EXCEPTION 'host inherited source changed' USING ERRCODE = '42501';
85
+ END IF;
86
+ SELECT * INTO d FROM host_mcp_delegations WHERE id = NEW.delegation_id AND account_id = NEW.account_id
87
+ AND workspace_id = NEW.workspace_id AND owner_subject_id = NEW.owner_subject_id FOR SHARE;
88
+ IF NOT FOUND OR d.status <> 'active' OR d.revoked_at IS NOT NULL OR d.binding_id <> NEW.binding_id
89
+ OR d.generation::text IS DISTINCT FROM prior.canonical_snapshot ->> 'delegationGeneration'
90
+ OR d.owner_authorization_revision::text IS DISTINCT FROM membership ->> 'authorizationRevision' THEN
91
+ RAISE EXCEPTION 'host inherited delegation unavailable' USING ERRCODE = '42501';
92
+ END IF;
93
+ SELECT * INTO b FROM host_mcp_bindings WHERE id = NEW.binding_id AND account_id = NEW.account_id
94
+ AND workspace_id = NEW.workspace_id AND owner_subject_id = NEW.owner_subject_id FOR SHARE;
95
+ IF NOT FOUND OR b.status <> 'active' OR b.revoked_at IS NOT NULL OR b.generation <> d.binding_generation
96
+ OR b.generation::text IS DISTINCT FROM prior.canonical_snapshot ->> 'bindingGeneration'
97
+ OR b.definition IS DISTINCT FROM prior.canonical_snapshot -> 'definition'
98
+ OR b.authorization_revision <> d.owner_authorization_revision THEN
99
+ RAISE EXCEPTION 'host inherited binding unavailable' USING ERRCODE = '42501';
100
+ END IF;
101
+ expected := prior.canonical_snapshot || jsonb_build_object(
102
+ 'acceptedWork', jsonb_build_object('kind','turn','turnId',NEW.turn_id),
103
+ 'source', jsonb_build_object('kind','inherited_turn','sessionId',NEW.session_id,'turnId',causal_id));
104
+ IF NEW.canonical_snapshot IS DISTINCT FROM expected THEN
105
+ RAISE EXCEPTION 'host inherited authority is not an exact copy' USING ERRCODE = '42501';
106
+ END IF;
107
+ RETURN NEW;
108
+ END
109
+ $body$;
110
+ REVOKE ALL ON FUNCTION opengeni_private.guard_host_mcp_inherited_turn_authority() FROM PUBLIC;
111
+ DROP TRIGGER host_mcp_turn_authority_guard ON host_mcp_turn_authorities;
112
+ CREATE TRIGGER host_mcp_turn_authority_guard BEFORE INSERT OR UPDATE ON host_mcp_turn_authorities
113
+ FOR EACH ROW WHEN (NEW.canonical_snapshot #>> '{source,kind}' IS DISTINCT FROM 'inherited_turn')
114
+ EXECUTE FUNCTION opengeni_private.guard_host_mcp_turn_authority();
115
+ CREATE TRIGGER host_mcp_inherited_turn_authority_guard BEFORE INSERT OR UPDATE ON host_mcp_turn_authorities
116
+ FOR EACH ROW WHEN (NEW.canonical_snapshot #>> '{source,kind}' = 'inherited_turn')
117
+ EXECUTE FUNCTION opengeni_private.guard_host_mcp_inherited_turn_authority();
118
+ DO $drain$
119
+ BEGIN
120
+ IF EXISTS (SELECT 1 FROM pg_stat_activity a
121
+ JOIN jsonb_array_elements_text(current_setting('opengeni.migration_application_roles')::jsonb) r ON r = a.usename
122
+ WHERE a.datname = current_database() AND a.pid <> pg_backend_pid()) THEN
123
+ RAISE EXCEPTION '0446 requires stopped application sessions' USING ERRCODE = '55000';
124
+ END IF;
125
+ END
126
+ $drain$;
@@ -0,0 +1,264 @@
1
+ -- deployment-mode: maintenance
2
+ SET LOCAL lock_timeout = '5s';
3
+ SET LOCAL statement_timeout = '5min';
4
+ -- Recording a revision reads and locks its exact organization membership.
5
+ -- Preserve native checks under a NOSUPERUSER migration owner with FORCE RLS.
6
+ -- Restore the constrained lifecycle marker on every return and exception.
7
+ DO $revision_membership_read$
8
+ DECLARE definition text; signature text;
9
+ BEGIN
10
+ definition := pg_get_functiondef('record_scheduled_task_revision_authority(uuid,uuid,uuid,bigint)'::regprocedure);
11
+ IF strpos(definition, 'resource_bearing boolean;') = 0
12
+ OR strpos(definition, 'RETURN subject_value;') = 0 THEN
13
+ RAISE EXCEPTION 'scheduled revision membership reader drifted' USING ERRCODE = '55000';
14
+ END IF;
15
+ definition := replace(definition, 'resource_bearing boolean;',
16
+ 'resource_bearing boolean; previous_membership_marker text := current_setting(''opengeni.organization_tenancy_lifecycle'', true);');
17
+ definition := replace(definition, E'BEGIN\n', E'BEGIN\n PERFORM set_config(''opengeni.organization_tenancy_lifecycle'', ''personal_resource_grant_management'', true);\n');
18
+ definition := replace(definition, 'RETURN NULL;',
19
+ 'PERFORM set_config(''opengeni.organization_tenancy_lifecycle'', coalesce(previous_membership_marker, ''''), true); RETURN NULL;');
20
+ definition := replace(definition, E'RETURN subject_value;\nEND',
21
+ E'PERFORM set_config(''opengeni.organization_tenancy_lifecycle'', coalesce(previous_membership_marker, ''''), true); RETURN subject_value;\nEXCEPTION WHEN OTHERS THEN\n PERFORM set_config(''opengeni.organization_tenancy_lifecycle'', coalesce(previous_membership_marker, ''''), true);\n RAISE;\nEND');
22
+ EXECUTE definition;
23
+ -- Admission and physical-use revalidation read the same membership. Retain
24
+ -- their exact native revision/workspace checks; do not add an owner bypass.
25
+ FOREACH signature IN ARRAY ARRAY[
26
+ 'admit_scheduled_agent_run_execution()',
27
+ 'validate_scheduled_agent_run_live_authority(uuid,uuid,uuid)'
28
+ ] LOOP
29
+ definition := pg_get_functiondef(signature::regprocedure);
30
+ IF strpos(definition, E'DECLARE\n') = 0 OR strpos(definition, E'BEGIN\n') = 0
31
+ OR strpos(definition, 'organization_memberships') = 0 THEN
32
+ RAISE EXCEPTION 'scheduled membership reader drifted: %', signature USING ERRCODE = '55000';
33
+ END IF;
34
+ definition := regexp_replace(definition, E'DECLARE\n', E'DECLARE\n previous_membership_marker text := current_setting(''opengeni.organization_tenancy_lifecycle'', true);\n');
35
+ definition := regexp_replace(definition, E'BEGIN\n', E'BEGIN\n PERFORM set_config(''opengeni.organization_tenancy_lifecycle'', ''personal_resource_grant_management'', true);\n');
36
+ definition := regexp_replace(definition, 'RETURN (NEW|NULL|''[^'']*'');',
37
+ E'PERFORM set_config(''opengeni.organization_tenancy_lifecycle'', coalesce(previous_membership_marker, ''''), true); RETURN \\1;', 'g');
38
+ -- An exception unwinds the statement/subtransaction and its LOCAL marker.
39
+ EXECUTE definition;
40
+ END LOOP;
41
+ END
42
+ $revision_membership_read$;
43
+ DO $drain$
44
+ DECLARE roles jsonb := nullif(current_setting('opengeni.migration_application_roles', true), '')::jsonb;
45
+ BEGIN
46
+ IF roles IS NULL OR jsonb_typeof(roles) <> 'array' THEN
47
+ RAISE EXCEPTION '0447 requires application database roles' USING ERRCODE = '55000';
48
+ END IF;
49
+ IF jsonb_array_length(roles) NOT BETWEEN 1 AND 16 OR EXISTS (
50
+ SELECT 1 FROM jsonb_array_elements(roles) item WHERE jsonb_typeof(item) <> 'string'
51
+ OR btrim(item #>> '{}') = '' OR item #>> '{}' <> btrim(item #>> '{}')
52
+ OR octet_length(item #>> '{}') > 63
53
+ ) THEN RAISE EXCEPTION '0447 invalid application roles' USING ERRCODE = '55000'; END IF;
54
+ IF EXISTS (SELECT 1 FROM pg_stat_activity a JOIN jsonb_array_elements_text(roles) r ON r = a.usename
55
+ WHERE a.datname = current_database() AND a.pid <> pg_backend_pid()) THEN
56
+ RAISE EXCEPTION '0447 requires stopped application sessions' USING ERRCODE = '55000';
57
+ END IF;
58
+ END
59
+ $drain$;
60
+ CREATE TABLE host_mcp_task_authorities (
61
+ task_id uuid NOT NULL REFERENCES scheduled_tasks(id) ON DELETE CASCADE,
62
+ task_authority_revision bigint NOT NULL CHECK (task_authority_revision > 0),
63
+ server_id text NOT NULL CHECK (octet_length(server_id) BETWEEN 1 AND 1024),
64
+ account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE CASCADE,
65
+ workspace_id uuid NOT NULL,
66
+ owner_subject_id text NOT NULL,
67
+ binding_id uuid NOT NULL REFERENCES host_mcp_bindings(id),
68
+ delegation_id uuid NOT NULL REFERENCES host_mcp_delegations(id),
69
+ canonical_snapshot jsonb NOT NULL CHECK (jsonb_typeof(canonical_snapshot) = 'object' AND octet_length(canonical_snapshot::text) <= 262144),
70
+ created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
71
+ PRIMARY KEY (task_id, task_authority_revision, server_id),
72
+ FOREIGN KEY (workspace_id, account_id) REFERENCES workspaces(id, account_id) ON DELETE CASCADE
73
+ );
74
+ ALTER TABLE host_mcp_task_authorities ENABLE ROW LEVEL SECURITY;
75
+ ALTER TABLE host_mcp_task_authorities FORCE ROW LEVEL SECURITY;
76
+ CREATE POLICY host_mcp_task_authorities_owner_scope ON host_mcp_task_authorities
77
+ USING (account_id = opengeni_private.current_account_id() AND workspace_id = opengeni_private.current_workspace_id()
78
+ AND owner_subject_id = opengeni_private.current_subject_id())
79
+ WITH CHECK (account_id = opengeni_private.current_account_id() AND workspace_id = opengeni_private.current_workspace_id()
80
+ AND owner_subject_id = opengeni_private.current_subject_id());
81
+ CREATE FUNCTION opengeni_private.guard_host_mcp_task_authority() RETURNS trigger
82
+ LANGUAGE plpgsql SET search_path FROM CURRENT AS $body$
83
+ DECLARE
84
+ t scheduled_tasks%ROWTYPE;
85
+ s sessions%ROWTYPE;
86
+ d host_mcp_delegations%ROWTYPE;
87
+ b host_mcp_bindings%ROWTYPE;
88
+ membership jsonb;
89
+ revision_authority jsonb;
90
+ expected jsonb;
91
+ target_context text := 'workspace_shared';
92
+ BEGIN
93
+ IF TG_OP <> 'INSERT' OR NEW.account_id IS DISTINCT FROM opengeni_private.current_account_id()
94
+ OR NEW.workspace_id IS DISTINCT FROM opengeni_private.current_workspace_id()
95
+ OR NEW.owner_subject_id IS DISTINCT FROM opengeni_private.current_subject_id() THEN
96
+ RAISE EXCEPTION 'host task authority scope unavailable' USING ERRCODE = '42501';
97
+ END IF;
98
+ PERFORM pg_advisory_xact_lock(hashtextextended('organization-membership:' || NEW.account_id::text, 0));
99
+ SELECT value INTO membership FROM jsonb_array_elements(list_self_organization_memberships(NEW.owner_subject_id))
100
+ WHERE value ->> 'organizationId' = NEW.account_id::text AND value ->> 'status' = 'active';
101
+ IF membership IS NULL OR NOT (EXISTS (
102
+ SELECT 1 FROM workspace_memberships w WHERE w.workspace_id = NEW.workspace_id AND w.subject_id = NEW.owner_subject_id
103
+ ) OR coalesce(membership ->> 'personalWorkspaceId' = NEW.workspace_id::text, false)) THEN
104
+ RAISE EXCEPTION 'host task membership unavailable' USING ERRCODE = '42501';
105
+ END IF;
106
+ SELECT * INTO t FROM scheduled_tasks WHERE id = NEW.task_id AND account_id = NEW.account_id
107
+ AND workspace_id = NEW.workspace_id AND authority_revision = NEW.task_authority_revision FOR SHARE;
108
+ IF NOT FOUND OR t.deleted_at IS NOT NULL OR t.action ->> 'kind' <> 'agent_turn' THEN
109
+ RAISE EXCEPTION 'host task revision unavailable' USING ERRCODE = '42501';
110
+ END IF;
111
+ revision_authority := scheduled_task_revision_authority_snapshot(NEW.account_id, NEW.workspace_id, NEW.task_id, NEW.task_authority_revision);
112
+ IF revision_authority IS NULL OR revision_authority ->> 'subjectId' IS DISTINCT FROM NEW.owner_subject_id
113
+ OR revision_authority ->> 'organizationMembershipId' IS DISTINCT FROM membership ->> 'id'
114
+ OR revision_authority ->> 'membershipAuthorizationRevision' IS DISTINCT FROM membership ->> 'authorizationRevision' THEN
115
+ RAISE EXCEPTION 'host task causal authority unavailable' USING ERRCODE = '42501';
116
+ END IF;
117
+ IF t.run_mode = 'existing_session' THEN
118
+ SELECT * INTO s FROM sessions WHERE id = t.reusable_session_id AND workspace_id = NEW.workspace_id AND account_id = NEW.account_id FOR SHARE;
119
+ IF NOT FOUND OR (s.visibility = 'user_private' AND s.owner_subject_id IS DISTINCT FROM NEW.owner_subject_id) THEN
120
+ RAISE EXCEPTION 'host task target unavailable' USING ERRCODE = '42501';
121
+ END IF;
122
+ target_context := s.visibility;
123
+ END IF;
124
+ IF NOT EXISTS (SELECT 1 FROM jsonb_array_elements(
125
+ CASE WHEN t.run_mode = 'existing_session' THEN s.tools ELSE t.agent_config -> 'tools' END
126
+ ) tool WHERE tool ->> 'kind' = 'mcp' AND tool ->> 'id' = NEW.server_id) THEN
127
+ RAISE EXCEPTION 'host task server is not selected' USING ERRCODE = '42501';
128
+ END IF;
129
+ SELECT * INTO d FROM host_mcp_delegations WHERE id = NEW.delegation_id AND account_id = NEW.account_id
130
+ AND workspace_id = NEW.workspace_id AND owner_subject_id = NEW.owner_subject_id FOR SHARE;
131
+ IF NOT FOUND OR d.status <> 'active' OR d.revoked_at IS NOT NULL OR d.binding_id <> NEW.binding_id
132
+ OR d.owner_authorization_revision::text IS DISTINCT FROM membership ->> 'authorizationRevision'
133
+ OR d.grant_definition ->> 'context' IS DISTINCT FROM target_context
134
+ OR (d.grant_definition ->> 'mode' = 'session' AND (t.run_mode <> 'existing_session'
135
+ OR d.grant_definition ->> 'sessionId' IS DISTINCT FROM s.id::text
136
+ OR d.grant_definition ->> 'expectedAuthorityEpoch' IS DISTINCT FROM s.authority_epoch::text)) THEN
137
+ RAISE EXCEPTION 'host task delegation unavailable' USING ERRCODE = '42501';
138
+ END IF;
139
+ SELECT * INTO b FROM host_mcp_bindings WHERE id = NEW.binding_id AND account_id = NEW.account_id
140
+ AND workspace_id = NEW.workspace_id AND owner_subject_id = NEW.owner_subject_id FOR SHARE;
141
+ IF NOT FOUND OR b.status <> 'active' OR b.revoked_at IS NOT NULL OR b.generation <> d.binding_generation
142
+ OR b.authorization_revision <> d.owner_authorization_revision OR b.definition ->> 'serverId' IS DISTINCT FROM NEW.server_id THEN
143
+ RAISE EXCEPTION 'host task binding unavailable' USING ERRCODE = '42501';
144
+ END IF;
145
+ expected := jsonb_build_object('version',1,'accountId',NEW.account_id,'workspaceId',NEW.workspace_id,
146
+ 'taskId',t.id,'taskAuthorityRevision',t.authority_revision,'taskExecutionDigest',t.execution_digest,
147
+ 'ownerSubjectId',NEW.owner_subject_id,'ownerOrganizationMembershipId',membership ->> 'id',
148
+ 'ownerMembershipAuthorizationRevision',(membership ->> 'authorizationRevision')::bigint,
149
+ 'bindingId',b.id,'bindingGeneration',b.generation,'delegationId',d.id,'delegationGeneration',d.generation,
150
+ 'definition',b.definition,'context',target_context);
151
+ IF NEW.canonical_snapshot IS DISTINCT FROM expected THEN
152
+ RAISE EXCEPTION 'host task snapshot is not canonical' USING ERRCODE = '42501';
153
+ END IF;
154
+ RETURN NEW;
155
+ END
156
+ $body$;
157
+ CREATE TRIGGER host_mcp_task_authority_guard BEFORE INSERT OR UPDATE ON host_mcp_task_authorities
158
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.guard_host_mcp_task_authority();
159
+ REVOKE ALL ON TABLE host_mcp_task_authorities FROM PUBLIC;
160
+ REVOKE ALL ON FUNCTION opengeni_private.guard_host_mcp_task_authority() FROM PUBLIC;
161
+
162
+ CREATE FUNCTION opengeni_private.guard_host_mcp_scheduled_turn_authority() RETURNS trigger
163
+ LANGUAGE plpgsql SET search_path FROM CURRENT AS $body$
164
+ DECLARE
165
+ t session_turns%ROWTYPE;
166
+ s sessions%ROWTYPE;
167
+ r scheduled_task_runs%ROWTYPE;
168
+ a host_mcp_task_authorities%ROWTYPE;
169
+ d host_mcp_delegations%ROWTYPE;
170
+ b host_mcp_bindings%ROWTYPE;
171
+ membership jsonb;
172
+ expected jsonb;
173
+ origin jsonb;
174
+ BEGIN
175
+ IF TG_OP <> 'INSERT' OR NEW.account_id IS DISTINCT FROM opengeni_private.current_account_id()
176
+ OR NEW.workspace_id IS DISTINCT FROM opengeni_private.current_workspace_id()
177
+ OR NEW.owner_subject_id IS DISTINCT FROM opengeni_private.current_subject_id()
178
+ OR current_setting('opengeni.session_inference_claim', true) IS DISTINCT FROM '1' THEN
179
+ RAISE EXCEPTION 'host scheduled turn scope unavailable' USING ERRCODE = '42501';
180
+ END IF;
181
+ PERFORM pg_advisory_xact_lock(hashtextextended('organization-membership:' || NEW.account_id::text, 0));
182
+ SELECT value INTO membership FROM jsonb_array_elements(list_self_organization_memberships(NEW.owner_subject_id))
183
+ WHERE value ->> 'organizationId' = NEW.account_id::text AND value ->> 'status' = 'active';
184
+ SELECT * INTO s FROM sessions WHERE id = NEW.session_id AND account_id = NEW.account_id AND workspace_id = NEW.workspace_id FOR SHARE;
185
+ IF NOT FOUND OR membership IS NULL OR (s.visibility = 'user_private' AND s.owner_subject_id IS DISTINCT FROM NEW.owner_subject_id) THEN
186
+ RAISE EXCEPTION 'host scheduled session authority unavailable' USING ERRCODE = '42501';
187
+ END IF;
188
+ SELECT * INTO t FROM session_turns WHERE id = NEW.turn_id AND session_id = NEW.session_id
189
+ AND account_id = NEW.account_id AND workspace_id = NEW.workspace_id FOR SHARE;
190
+ IF NOT FOUND OR t.source <> 'system' OR t.status <> 'running' OR t.execution_generation <> 1
191
+ OR t.active_attempt_id IS NULL OR t.scheduled_task_run_id IS NULL
192
+ OR t.initiating_human_subject_id IS DISTINCT FROM NEW.owner_subject_id
193
+ OR EXISTS (SELECT 1 FROM session_turn_attempts attempt WHERE attempt.turn_id = t.id AND attempt.workspace_id = NEW.workspace_id) THEN
194
+ RAISE EXCEPTION 'host scheduled turn unavailable' USING ERRCODE = '42501';
195
+ END IF;
196
+ SELECT * INTO r FROM scheduled_task_runs WHERE id = t.scheduled_task_run_id AND account_id = NEW.account_id
197
+ AND workspace_id = NEW.workspace_id AND session_id = NEW.session_id FOR SHARE;
198
+ IF NOT FOUND OR r.status <> 'dispatched' OR r.accepted_execution_snapshot IS NULL
199
+ OR r.accepted_execution_snapshot ->> 'causalHumanSubjectId' IS DISTINCT FROM NEW.owner_subject_id
200
+ OR validate_scheduled_agent_run_live_authority(NEW.account_id, NEW.workspace_id, r.id) IS NOT NULL THEN
201
+ RAISE EXCEPTION 'host scheduled run authority unavailable' USING ERRCODE = '42501';
202
+ END IF;
203
+ SELECT * INTO a FROM host_mcp_task_authorities WHERE task_id = r.task_id AND task_authority_revision = r.task_authority_revision
204
+ AND server_id = NEW.server_id AND account_id = NEW.account_id AND workspace_id = NEW.workspace_id
205
+ AND owner_subject_id = NEW.owner_subject_id;
206
+ IF NOT FOUND OR a.binding_id <> NEW.binding_id OR a.delegation_id <> NEW.delegation_id
207
+ OR a.canonical_snapshot ->> 'taskExecutionDigest' IS DISTINCT FROM r.task_execution_digest
208
+ OR a.canonical_snapshot ->> 'context' IS DISTINCT FROM s.visibility
209
+ OR a.canonical_snapshot ->> 'ownerOrganizationMembershipId' IS DISTINCT FROM membership ->> 'id'
210
+ OR a.canonical_snapshot ->> 'ownerMembershipAuthorizationRevision' IS DISTINCT FROM membership ->> 'authorizationRevision' THEN
211
+ RAISE EXCEPTION 'host scheduled selection changed' USING ERRCODE = '42501';
212
+ END IF;
213
+ SELECT * INTO d FROM host_mcp_delegations WHERE id = NEW.delegation_id AND account_id = NEW.account_id
214
+ AND workspace_id = NEW.workspace_id AND owner_subject_id = NEW.owner_subject_id FOR SHARE;
215
+ IF NOT FOUND OR d.status <> 'active' OR d.revoked_at IS NOT NULL
216
+ OR d.generation::text IS DISTINCT FROM a.canonical_snapshot ->> 'delegationGeneration'
217
+ OR d.owner_authorization_revision::text IS DISTINCT FROM membership ->> 'authorizationRevision'
218
+ OR (d.grant_definition ->> 'mode' = 'session' AND (
219
+ d.grant_definition ->> 'sessionId' IS DISTINCT FROM s.id::text
220
+ OR d.grant_definition ->> 'expectedAuthorityEpoch' IS DISTINCT FROM s.authority_epoch::text)) THEN
221
+ RAISE EXCEPTION 'host scheduled delegation unavailable' USING ERRCODE = '42501';
222
+ END IF;
223
+ SELECT * INTO b FROM host_mcp_bindings WHERE id = NEW.binding_id AND account_id = NEW.account_id
224
+ AND workspace_id = NEW.workspace_id AND owner_subject_id = NEW.owner_subject_id FOR SHARE;
225
+ IF NOT FOUND OR b.status <> 'active' OR b.revoked_at IS NOT NULL OR b.generation <> d.binding_generation
226
+ OR b.generation::text IS DISTINCT FROM a.canonical_snapshot ->> 'bindingGeneration'
227
+ OR b.definition IS DISTINCT FROM a.canonical_snapshot -> 'definition' THEN
228
+ RAISE EXCEPTION 'host scheduled binding unavailable' USING ERRCODE = '42501';
229
+ END IF;
230
+ origin := jsonb_build_object('taskId',r.task_id,'taskAuthorityRevision',r.task_authority_revision,'runId',r.id);
231
+ expected := (a.canonical_snapshot - ARRAY['taskId','taskAuthorityRevision','taskExecutionDigest','context'])
232
+ || jsonb_build_object('targetSessionId',s.id,'targetSessionVisibility',s.visibility,'targetSessionAuthorityEpoch',s.authority_epoch,
233
+ 'acceptedWork',origin || jsonb_build_object('kind','scheduled_task'),'scheduledOrigin',origin,
234
+ 'source',jsonb_build_object('kind','scheduled_task'));
235
+ IF NEW.canonical_snapshot IS DISTINCT FROM expected THEN
236
+ RAISE EXCEPTION 'host scheduled snapshot is not canonical' USING ERRCODE = '42501';
237
+ END IF;
238
+ RETURN NEW;
239
+ END
240
+ $body$;
241
+ REVOKE ALL ON FUNCTION opengeni_private.guard_host_mcp_scheduled_turn_authority() FROM PUBLIC;
242
+ DROP TRIGGER host_mcp_turn_authority_guard ON host_mcp_turn_authorities;
243
+ CREATE TRIGGER host_mcp_turn_authority_guard BEFORE INSERT OR UPDATE ON host_mcp_turn_authorities
244
+ FOR EACH ROW WHEN (NEW.canonical_snapshot #>> '{source,kind}' IS DISTINCT FROM 'inherited_turn'
245
+ AND NEW.canonical_snapshot #>> '{source,kind}' IS DISTINCT FROM 'scheduled_task')
246
+ EXECUTE FUNCTION opengeni_private.guard_host_mcp_turn_authority();
247
+ CREATE TRIGGER host_mcp_scheduled_turn_authority_guard BEFORE INSERT OR UPDATE ON host_mcp_turn_authorities
248
+ FOR EACH ROW WHEN (NEW.canonical_snapshot #>> '{source,kind}' = 'scheduled_task')
249
+ EXECUTE FUNCTION opengeni_private.guard_host_mcp_scheduled_turn_authority();
250
+ DO $acl$
251
+ DECLARE target_role record;
252
+ BEGIN
253
+ FOR target_role IN SELECT DISTINCT role.rolname FROM pg_class relation
254
+ CROSS JOIN LATERAL aclexplode(coalesce(relation.relacl, acldefault('r', relation.relowner))) privilege
255
+ JOIN pg_roles role ON role.oid = privilege.grantee
256
+ WHERE relation.oid = 'host_mcp_task_authorities'::regclass AND privilege.grantee <> relation.relowner
257
+ LOOP EXECUTE format('REVOKE ALL ON TABLE host_mcp_task_authorities FROM %I', target_role.rolname); END LOOP;
258
+ IF EXISTS (SELECT 1 FROM pg_stat_activity a
259
+ JOIN jsonb_array_elements_text(current_setting('opengeni.migration_application_roles')::jsonb) r ON r = a.usename
260
+ WHERE a.datname = current_database() AND a.pid <> pg_backend_pid()) THEN
261
+ RAISE EXCEPTION '0447 requires stopped application sessions' USING ERRCODE = '55000';
262
+ END IF;
263
+ END
264
+ $acl$;
@@ -0,0 +1,115 @@
1
+ -- deployment-mode: maintenance
2
+ SET LOCAL lock_timeout = '5s';
3
+ SET LOCAL statement_timeout = '5min';
4
+ DO $drain$
5
+ DECLARE roles jsonb := nullif(current_setting('opengeni.migration_application_roles', true), '')::jsonb;
6
+ BEGIN
7
+ IF roles IS NULL OR jsonb_typeof(roles) <> 'array' THEN
8
+ RAISE EXCEPTION '0448 requires application database roles' USING ERRCODE = '55000';
9
+ END IF;
10
+ IF jsonb_array_length(roles) NOT BETWEEN 1 AND 16 OR EXISTS (
11
+ SELECT 1 FROM jsonb_array_elements(roles) item WHERE jsonb_typeof(item) <> 'string'
12
+ OR btrim(item #>> '{}') = '' OR item #>> '{}' <> btrim(item #>> '{}')
13
+ OR octet_length(item #>> '{}') > 63
14
+ ) THEN RAISE EXCEPTION '0448 invalid application roles' USING ERRCODE = '55000'; END IF;
15
+ IF EXISTS (SELECT 1 FROM pg_stat_activity a JOIN jsonb_array_elements_text(roles) r ON r = a.usename
16
+ WHERE a.datname = current_database() AND a.pid <> pg_backend_pid()) THEN
17
+ RAISE EXCEPTION '0448 requires stopped application sessions' USING ERRCODE = '55000';
18
+ END IF;
19
+ END
20
+ $drain$;
21
+ CREATE FUNCTION opengeni_private.guard_host_mcp_child_turn_authority() RETURNS trigger
22
+ LANGUAGE plpgsql SET search_path FROM CURRENT AS $body$
23
+ DECLARE
24
+ s sessions%ROWTYPE;
25
+ p sessions%ROWTYPE;
26
+ t session_turns%ROWTYPE;
27
+ a host_mcp_turn_authorities%ROWTYPE;
28
+ d host_mcp_delegations%ROWTYPE;
29
+ b host_mcp_bindings%ROWTYPE;
30
+ membership jsonb;
31
+ expected jsonb;
32
+ BEGIN
33
+ IF TG_OP <> 'INSERT' OR NEW.account_id IS DISTINCT FROM opengeni_private.current_account_id()
34
+ OR NEW.workspace_id IS DISTINCT FROM opengeni_private.current_workspace_id()
35
+ OR NEW.owner_subject_id IS DISTINCT FROM opengeni_private.current_subject_id() THEN
36
+ RAISE EXCEPTION 'host child authority scope unavailable' USING ERRCODE = '42501';
37
+ END IF;
38
+ PERFORM pg_advisory_xact_lock(hashtextextended('organization-membership:' || NEW.account_id::text, 0));
39
+ SELECT value INTO membership FROM jsonb_array_elements(list_self_organization_memberships(NEW.owner_subject_id))
40
+ WHERE value ->> 'organizationId' = NEW.account_id::text AND value ->> 'status' = 'active';
41
+ IF membership IS NULL OR NOT (EXISTS (SELECT 1 FROM workspace_memberships w
42
+ WHERE w.workspace_id = NEW.workspace_id AND w.subject_id = NEW.owner_subject_id)
43
+ OR coalesce(membership ->> 'personalWorkspaceId' = NEW.workspace_id::text, false)) THEN
44
+ RAISE EXCEPTION 'host child membership unavailable' USING ERRCODE = '42501';
45
+ END IF;
46
+ SELECT * INTO s FROM sessions WHERE id = NEW.session_id AND account_id = NEW.account_id AND workspace_id = NEW.workspace_id FOR SHARE;
47
+ IF NOT FOUND OR s.parent_session_id IS NULL OR s.parent_turn_id IS NULL
48
+ OR (s.visibility = 'user_private' AND s.owner_subject_id IS DISTINCT FROM NEW.owner_subject_id) THEN
49
+ RAISE EXCEPTION 'host child parent unavailable' USING ERRCODE = '42501';
50
+ END IF;
51
+ SELECT * INTO p FROM sessions WHERE id = s.parent_session_id AND account_id = NEW.account_id AND workspace_id = NEW.workspace_id FOR SHARE;
52
+ IF NOT FOUND OR p.visibility <> s.visibility THEN
53
+ RAISE EXCEPTION 'host child visibility changed' USING ERRCODE = '42501';
54
+ END IF;
55
+ SELECT * INTO t FROM session_turns WHERE id = NEW.turn_id AND session_id = NEW.session_id
56
+ AND account_id = NEW.account_id AND workspace_id = NEW.workspace_id FOR SHARE;
57
+ IF NOT FOUND OR t.status <> 'queued' OR t.active_attempt_id IS NOT NULL OR t.source NOT IN ('user','api')
58
+ OR t.initiating_human_subject_id IS DISTINCT FROM NEW.owner_subject_id
59
+ OR EXISTS (SELECT 1 FROM session_turns other WHERE other.session_id = s.id AND other.id <> t.id)
60
+ OR EXISTS (SELECT 1 FROM session_turn_attempts attempt WHERE attempt.turn_id = t.id)
61
+ OR NOT EXISTS (SELECT 1 FROM jsonb_array_elements(s.tools) tool WHERE tool ->> 'kind' = 'mcp' AND tool ->> 'id' = NEW.server_id) THEN
62
+ RAISE EXCEPTION 'host child initial turn unavailable' USING ERRCODE = '42501';
63
+ END IF;
64
+ SELECT * INTO a FROM host_mcp_turn_authorities WHERE turn_id = s.parent_turn_id AND session_id = s.parent_session_id
65
+ AND server_id = NEW.server_id AND account_id = NEW.account_id AND workspace_id = NEW.workspace_id AND owner_subject_id = NEW.owner_subject_id;
66
+ IF NOT FOUND OR a.binding_id <> NEW.binding_id OR a.delegation_id <> NEW.delegation_id
67
+ OR a.canonical_snapshot ->> 'targetSessionAuthorityEpoch' IS DISTINCT FROM p.authority_epoch::text
68
+ OR a.canonical_snapshot ->> 'targetSessionVisibility' IS DISTINCT FROM p.visibility
69
+ OR a.canonical_snapshot ->> 'ownerOrganizationMembershipId' IS DISTINCT FROM membership ->> 'id'
70
+ OR a.canonical_snapshot ->> 'ownerMembershipAuthorizationRevision' IS DISTINCT FROM membership ->> 'authorizationRevision' THEN
71
+ RAISE EXCEPTION 'host child source changed' USING ERRCODE = '42501';
72
+ END IF;
73
+ SELECT * INTO d FROM host_mcp_delegations WHERE id = NEW.delegation_id AND account_id = NEW.account_id
74
+ AND workspace_id = NEW.workspace_id AND owner_subject_id = NEW.owner_subject_id FOR SHARE;
75
+ IF NOT FOUND OR d.status <> 'active' OR d.revoked_at IS NOT NULL OR d.grant_definition ->> 'mode' <> 'always'
76
+ OR d.generation::text IS DISTINCT FROM a.canonical_snapshot ->> 'delegationGeneration'
77
+ OR d.owner_authorization_revision::text IS DISTINCT FROM membership ->> 'authorizationRevision' THEN
78
+ RAISE EXCEPTION 'host child delegation unavailable' USING ERRCODE = '42501';
79
+ END IF;
80
+ SELECT * INTO b FROM host_mcp_bindings WHERE id = NEW.binding_id AND account_id = NEW.account_id
81
+ AND workspace_id = NEW.workspace_id AND owner_subject_id = NEW.owner_subject_id FOR SHARE;
82
+ IF NOT FOUND OR b.status <> 'active' OR b.revoked_at IS NOT NULL OR b.generation <> d.binding_generation
83
+ OR b.generation::text IS DISTINCT FROM a.canonical_snapshot ->> 'bindingGeneration'
84
+ OR b.definition IS DISTINCT FROM a.canonical_snapshot -> 'definition' THEN
85
+ RAISE EXCEPTION 'host child binding unavailable' USING ERRCODE = '42501';
86
+ END IF;
87
+ expected := a.canonical_snapshot || jsonb_build_object('targetSessionId',s.id,
88
+ 'targetSessionAuthorityEpoch',s.authority_epoch,'targetSessionVisibility',s.visibility,
89
+ 'acceptedWork',jsonb_build_object('kind','turn','turnId',t.id),
90
+ 'source',jsonb_build_object('kind','child_turn','sessionId',p.id,'turnId',s.parent_turn_id));
91
+ IF NEW.canonical_snapshot IS DISTINCT FROM expected THEN
92
+ RAISE EXCEPTION 'host child authority is not an exact copy' USING ERRCODE = '42501';
93
+ END IF;
94
+ RETURN NEW;
95
+ END
96
+ $body$;
97
+ REVOKE ALL ON FUNCTION opengeni_private.guard_host_mcp_child_turn_authority() FROM PUBLIC;
98
+ DROP TRIGGER host_mcp_turn_authority_guard ON host_mcp_turn_authorities;
99
+ CREATE TRIGGER host_mcp_turn_authority_guard BEFORE INSERT OR UPDATE ON host_mcp_turn_authorities
100
+ FOR EACH ROW WHEN (NEW.canonical_snapshot #>> '{source,kind}' IS DISTINCT FROM 'inherited_turn'
101
+ AND NEW.canonical_snapshot #>> '{source,kind}' IS DISTINCT FROM 'scheduled_task'
102
+ AND NEW.canonical_snapshot #>> '{source,kind}' IS DISTINCT FROM 'child_turn')
103
+ EXECUTE FUNCTION opengeni_private.guard_host_mcp_turn_authority();
104
+ CREATE TRIGGER host_mcp_child_turn_authority_guard BEFORE INSERT OR UPDATE ON host_mcp_turn_authorities
105
+ FOR EACH ROW WHEN (NEW.canonical_snapshot #>> '{source,kind}' = 'child_turn')
106
+ EXECUTE FUNCTION opengeni_private.guard_host_mcp_child_turn_authority();
107
+ DO $drain$
108
+ BEGIN
109
+ IF EXISTS (SELECT 1 FROM pg_stat_activity a
110
+ JOIN jsonb_array_elements_text(current_setting('opengeni.migration_application_roles')::jsonb) r ON r = a.usename
111
+ WHERE a.datname = current_database() AND a.pid <> pg_backend_pid()) THEN
112
+ RAISE EXCEPTION '0448 requires stopped application sessions' USING ERRCODE = '55000';
113
+ END IF;
114
+ END
115
+ $drain$;
@@ -0,0 +1,107 @@
1
+ -- deployment-mode: maintenance
2
+ -- Optional native links are delegations, never merges. This storage does not
3
+ -- admit linked requests by itself; the access resolver must validate both lanes.
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' THEN
10
+ RAISE EXCEPTION '0449 requires application database roles' USING ERRCODE = '55000';
11
+ END IF;
12
+ IF jsonb_array_length(roles) NOT BETWEEN 1 AND 16 OR EXISTS (
13
+ SELECT 1 FROM jsonb_array_elements(roles) item WHERE jsonb_typeof(item) <> 'string'
14
+ OR btrim(item #>> '{}') = '' OR item #>> '{}' <> btrim(item #>> '{}')
15
+ OR octet_length(item #>> '{}') > 63
16
+ ) THEN RAISE EXCEPTION '0449 invalid application roles' USING ERRCODE = '55000'; END IF;
17
+ IF EXISTS (SELECT 1 FROM pg_stat_activity a JOIN jsonb_array_elements_text(roles) r ON r = a.usename
18
+ WHERE a.datname = current_database() AND a.pid <> pg_backend_pid()) THEN
19
+ RAISE EXCEPTION '0449 requires stopped application sessions' USING ERRCODE = '55000';
20
+ END IF;
21
+ END
22
+ $drain$;
23
+
24
+ CREATE TABLE external_identity_links (
25
+ id uuid PRIMARY KEY,
26
+ account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE CASCADE,
27
+ external_identity_id uuid NOT NULL REFERENCES external_identities(id),
28
+ external_subject_id text NOT NULL,
29
+ external_authorization_revision bigint NOT NULL CHECK (external_authorization_revision BETWEEN 1 AND 9007199254740991),
30
+ native_subject_id text CHECK (native_subject_id ~ '^user:[^\r\n]+$' AND octet_length(native_subject_id) <= 4096),
31
+ native_membership_id uuid,
32
+ native_authorization_revision bigint CHECK (native_authorization_revision BETWEEN 1 AND 9007199254740991),
33
+ status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','active','revoked')),
34
+ revision bigint NOT NULL DEFAULT 1 CHECK (revision BETWEEN 1 AND 9007199254740991),
35
+ permissions jsonb NOT NULL CHECK (jsonb_typeof(permissions) = 'array' AND jsonb_array_length(permissions) BETWEEN 1 AND 512),
36
+ challenge_digest text NOT NULL CHECK (challenge_digest ~ '^[0-9a-f]{64}$'),
37
+ confirm_before timestamptz NOT NULL,
38
+ expires_at timestamptz,
39
+ created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
40
+ confirmed_at timestamptz,
41
+ revoked_at timestamptz,
42
+ FOREIGN KEY (account_id, external_subject_id) REFERENCES external_identities(account_id, subject_id),
43
+ FOREIGN KEY (native_membership_id, account_id) REFERENCES organization_memberships(id, account_id),
44
+ CHECK ((status = 'pending' AND native_subject_id IS NULL AND native_membership_id IS NULL
45
+ AND native_authorization_revision IS NULL AND confirmed_at IS NULL)
46
+ OR (status = 'active' AND native_subject_id IS NOT NULL AND native_membership_id IS NOT NULL
47
+ AND native_authorization_revision IS NOT NULL AND confirmed_at IS NOT NULL)
48
+ OR status = 'revoked'),
49
+ CHECK ((status = 'revoked') = (revoked_at IS NOT NULL)),
50
+ CHECK (external_subject_id = 'external_user:' || external_identity_id::text)
51
+ );
52
+ CREATE INDEX external_identity_links_external_idx ON external_identity_links(account_id, external_identity_id, created_at, id);
53
+ CREATE INDEX external_identity_links_native_idx ON external_identity_links(account_id, native_subject_id, id) WHERE native_subject_id IS NOT NULL;
54
+ ALTER TABLE external_identity_links ENABLE ROW LEVEL SECURITY;
55
+ ALTER TABLE external_identity_links FORCE ROW LEVEL SECURITY;
56
+ CREATE POLICY external_identity_links_account_scope ON external_identity_links
57
+ USING (account_id = opengeni_private.current_account_id())
58
+ WITH CHECK (account_id = opengeni_private.current_account_id());
59
+
60
+ CREATE FUNCTION opengeni_private.guard_external_identity_link_lifecycle() RETURNS trigger
61
+ LANGUAGE plpgsql SET search_path FROM CURRENT AS $body$
62
+ BEGIN
63
+ IF TG_OP = 'INSERT' THEN
64
+ IF NEW.status <> 'pending' OR NEW.revision <> 1 OR NEW.confirm_before <= clock_timestamp()
65
+ OR NEW.confirm_before > clock_timestamp() + interval '11 minutes'
66
+ OR (NEW.expires_at IS NOT NULL AND NEW.expires_at <= clock_timestamp()) THEN
67
+ RAISE EXCEPTION 'invalid identity link request' USING ERRCODE = '42501';
68
+ END IF;
69
+ RETURN NEW;
70
+ END IF;
71
+ IF ROW(NEW.id, NEW.account_id, NEW.external_identity_id, NEW.external_subject_id,
72
+ NEW.external_authorization_revision, NEW.challenge_digest, NEW.confirm_before, NEW.expires_at, NEW.created_at)
73
+ IS DISTINCT FROM ROW(OLD.id, OLD.account_id, OLD.external_identity_id, OLD.external_subject_id,
74
+ OLD.external_authorization_revision, OLD.challenge_digest, OLD.confirm_before, OLD.expires_at, OLD.created_at)
75
+ OR NEW.revision <> OLD.revision + 1 OR OLD.status = 'revoked' THEN
76
+ RAISE EXCEPTION 'identity link provenance is immutable' USING ERRCODE = '42501';
77
+ END IF;
78
+ IF NEW.status = 'active' THEN
79
+ IF OLD.status <> 'pending' OR OLD.confirm_before <= clock_timestamp()
80
+ OR (OLD.expires_at IS NOT NULL AND OLD.expires_at <= clock_timestamp())
81
+ OR NOT NEW.permissions <@ OLD.permissions THEN
82
+ RAISE EXCEPTION 'identity link confirmation unavailable' USING ERRCODE = '42501';
83
+ END IF;
84
+ ELSIF NEW.status = 'revoked' THEN
85
+ IF ROW(NEW.native_subject_id, NEW.native_membership_id, NEW.native_authorization_revision,
86
+ NEW.permissions, NEW.confirmed_at) IS DISTINCT FROM
87
+ ROW(OLD.native_subject_id, OLD.native_membership_id, OLD.native_authorization_revision,
88
+ OLD.permissions, OLD.confirmed_at) THEN
89
+ RAISE EXCEPTION 'revocation cannot retarget identity' USING ERRCODE = '42501';
90
+ END IF;
91
+ ELSE RAISE EXCEPTION 'identity link cannot be reset' USING ERRCODE = '42501'; END IF;
92
+ RETURN NEW;
93
+ END
94
+ $body$;
95
+ CREATE TRIGGER external_identity_link_lifecycle BEFORE INSERT OR UPDATE ON external_identity_links
96
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.guard_external_identity_link_lifecycle();
97
+ REVOKE ALL ON TABLE external_identity_links FROM PUBLIC;
98
+ REVOKE ALL ON FUNCTION opengeni_private.guard_external_identity_link_lifecycle() FROM PUBLIC;
99
+ DO $drain$
100
+ BEGIN
101
+ IF EXISTS (SELECT 1 FROM pg_stat_activity a
102
+ JOIN jsonb_array_elements_text(current_setting('opengeni.migration_application_roles')::jsonb) r ON r = a.usename
103
+ WHERE a.datname = current_database() AND a.pid <> pg_backend_pid()) THEN
104
+ RAISE EXCEPTION '0449 requires stopped application sessions' USING ERRCODE = '55000';
105
+ END IF;
106
+ END
107
+ $drain$;