@opengeni/db 4.2.2 → 4.3.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/canonical-human-identities.js +3 -3
- package/dist/{chunk-6GAPAYZR.js → chunk-7JPLGBSA.js} +562 -86
- package/dist/chunk-7JPLGBSA.js.map +1 -0
- package/dist/{chunk-SNLIKELL.js → chunk-IHADTE7C.js} +2 -2
- package/dist/{chunk-7WX3R3YL.js → chunk-ILKJKDII.js} +7 -2
- package/dist/chunk-ILKJKDII.js.map +1 -0
- package/dist/{chunk-MGVOYVPS.js → chunk-NCSPNCQK.js} +3 -3
- package/dist/{chunk-B2KR3TEM.js → chunk-SGJDIF2D.js} +2 -2
- package/dist/{chunk-S4Y42VSX.js → chunk-Y5XFVJJM.js} +207 -3
- package/dist/chunk-Y5XFVJJM.js.map +1 -0
- package/dist/{chunk-FPTI4NP4.js → chunk-YGBOVXC7.js} +2 -2
- package/dist/{chunk-6Y7ZDVJ5.js → chunk-YGRT6VZO.js} +2 -2
- package/dist/{chunk-6IQ4X35Y.js → chunk-YVYBAWBU.js} +46 -1
- package/dist/chunk-YVYBAWBU.js.map +1 -0
- package/dist/connect-attempts.d.ts +60 -0
- package/dist/connection-setup-authority.d.ts +10 -0
- package/dist/connection-token-resolver.d.ts +11 -2
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/external-identities.d.ts +16 -0
- package/dist/external-identity-links.d.ts +74 -0
- package/dist/external-link-work.d.ts +51 -0
- package/dist/host-mcp-binding-match.d.ts +5 -0
- package/dist/host-mcp-bindings.d.ts +73 -0
- package/dist/host-mcp-task-authority.d.ts +47 -0
- package/dist/host-selection-identity.d.ts +6 -0
- package/dist/index.d.ts +51 -35
- package/dist/index.js +15171 -13775
- package/dist/index.js.map +1 -1
- package/dist/live-session-attempt.d.ts +275 -0
- package/dist/managed-auth-session-sets.js +3 -3
- package/dist/memory-domain.d.ts +3 -3
- package/dist/provision-roles.js +1 -1
- package/dist/retained-provider-commands.js +2 -2
- package/dist/runtime-posture.d.ts +6 -6
- package/dist/scheduled-task-revision-authority.d.ts +11 -0
- package/dist/schema.d.ts +1279 -9
- package/dist/schema.js +11 -1
- package/dist/session-background-commands.js +4 -4
- package/dist/session-command-output.js +4 -4
- package/dist/session-event-slices.js +2 -2
- package/dist/session-queue-commands.d.ts +7 -0
- package/dist/session-tenancy.d.ts +2 -0
- package/dist/session-tenancy.js +3 -3
- package/dist/video-generation.js +3 -3
- package/dist/workspace-authority.d.ts +4 -4
- package/dist/workspace-tool-defaults.js +4 -4
- package/drizzle/0437_organization_scoped_external_workspaces.sql +71 -0
- package/drizzle/0438_durable_connect_attempts.sql +95 -0
- package/drizzle/0439_external_identity_provisioning.sql +128 -0
- package/drizzle/0440_external_workspace_member_removal.sql +59 -0
- package/drizzle/0441_external_identity_membership_lifecycle.sql +163 -0
- package/drizzle/0442_external_owning_user_authority.sql +81 -0
- package/drizzle/0443_host_mcp_binding_registry.sql +93 -0
- package/drizzle/0444_host_mcp_delegations.sql +129 -0
- package/drizzle/0445_host_mcp_turn_authorities.sql +145 -0
- package/drizzle/0446_host_mcp_causal_continuation.sql +126 -0
- package/drizzle/0447_host_mcp_task_authorities.sql +264 -0
- package/drizzle/0448_host_mcp_child_authority.sql +115 -0
- package/drizzle/0449_external_identity_link_lifecycle.sql +107 -0
- package/drizzle/0450_external_identity_link_work.sql +179 -0
- package/drizzle/0451_external_link_preview_and_permission_ceiling.sql +82 -0
- package/drizzle/0452_external_link_scheduled_origin.sql +34 -0
- package/drizzle/0453_host_mcp_native_owner.sql +35 -0
- package/drizzle/0454_connect_origin_authority.sql +42 -0
- package/drizzle/0455_external_link_inventory_labels.sql +44 -0
- package/drizzle/0456_social_connection_versions.sql +14 -0
- package/drizzle/0457_canonical_session_scope_subject.sql +46 -0
- package/drizzle/0458_skill_review_wire_compatibility.sql +36 -0
- package/package.json +6 -6
- package/src/connect-attempts.ts +317 -0
- package/src/connection-authority.ts +9 -0
- package/src/connection-setup-authority.ts +35 -0
- package/src/connection-token-resolver.ts +177 -48
- package/src/external-identities.ts +70 -0
- package/src/external-identity-links.ts +364 -0
- package/src/external-link-work.ts +228 -0
- package/src/host-mcp-binding-match.ts +33 -0
- package/src/host-mcp-bindings.ts +741 -0
- package/src/host-mcp-task-authority.ts +383 -0
- package/src/host-selection-identity.ts +29 -0
- package/src/index.ts +398 -147
- package/src/live-session-attempt.ts +85 -0
- package/src/managed-human-provisioning.ts +6 -1
- package/src/memory-domain.ts +6 -11
- package/src/provision-roles.ts +3 -0
- package/src/runtime-posture.ts +47 -0
- package/src/scheduled-task-revision-authority.ts +30 -0
- package/src/schema.ts +228 -3
- package/src/session-control.ts +14 -0
- package/src/session-queue-commands.ts +11 -0
- package/src/session-tenancy.ts +7 -0
- package/src/workspace-authority.ts +5 -5
- package/dist/chunk-6GAPAYZR.js.map +0 -1
- package/dist/chunk-6IQ4X35Y.js.map +0 -1
- package/dist/chunk-7WX3R3YL.js.map +0 -1
- package/dist/chunk-S4Y42VSX.js.map +0 -1
- /package/dist/{chunk-SNLIKELL.js.map → chunk-IHADTE7C.js.map} +0 -0
- /package/dist/{chunk-MGVOYVPS.js.map → chunk-NCSPNCQK.js.map} +0 -0
- /package/dist/{chunk-B2KR3TEM.js.map → chunk-SGJDIF2D.js.map} +0 -0
- /package/dist/{chunk-FPTI4NP4.js.map → chunk-YGBOVXC7.js.map} +0 -0
- /package/dist/{chunk-6Y7ZDVJ5.js.map → chunk-YGRT6VZO.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$;
|