@opengeni/db 4.4.0-canary.0 → 4.4.0-canary.2
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 +4 -3
- package/dist/{chunk-2ZMTQVTR.js → chunk-2KOLL33E.js} +2 -2
- package/dist/chunk-45CP2YKY.js +1062 -0
- package/dist/chunk-45CP2YKY.js.map +1 -0
- package/dist/{chunk-6TKL3GQO.js → chunk-5UUWZDAV.js} +6531 -6480
- package/dist/chunk-5UUWZDAV.js.map +1 -0
- package/dist/{chunk-QLA3UQNX.js → chunk-6IJD3XXF.js} +17 -3
- package/dist/chunk-6IJD3XXF.js.map +1 -0
- package/dist/{chunk-232VC2LW.js → chunk-6V3HDEMR.js} +2 -2
- package/dist/{chunk-M7PSMVFF.js → chunk-CHBLBEU6.js} +2 -2
- package/dist/chunk-M4PKBJRS.js +20318 -0
- package/dist/chunk-M4PKBJRS.js.map +1 -0
- package/dist/chunk-S2TZ3F4L.js +237 -0
- package/dist/chunk-S2TZ3F4L.js.map +1 -0
- package/dist/{chunk-UK57QA2H.js → chunk-UOXWVP3T.js} +2 -2
- package/dist/{chunk-GVZJ7XSR.js → chunk-WFU6PS54.js} +11 -4
- package/dist/chunk-WFU6PS54.js.map +1 -0
- package/dist/{chunk-THJGMEK5.js → chunk-XJP7KJ6U.js} +121 -66
- package/dist/chunk-XJP7KJ6U.js.map +1 -0
- package/dist/connection-token-resolver.d.ts +2 -0
- package/dist/database.d.ts +2 -0
- package/dist/editable-artifact-durable-export.js +3 -2
- package/dist/editable-artifact-durable-export.js.map +1 -1
- package/dist/editable-artifacts.js +4 -3
- package/dist/external-identities.d.ts +1 -0
- package/dist/external-membership-operations.d.ts +44 -0
- package/dist/governed-learning-evaluator.d.ts +1 -14
- package/dist/host-mcp-bindings.d.ts +4 -0
- package/dist/host-mcp-resolvers.d.ts +47 -0
- package/dist/host-mcp-task-authority.d.ts +2 -6
- package/dist/index.d.ts +29 -31
- package/dist/index.js +8616 -8321
- package/dist/index.js.map +1 -1
- package/dist/knowledge-document-preparation.d.ts +56 -0
- package/dist/knowledge-entries-schema.d.ts +2479 -0
- package/dist/knowledge-entries.d.ts +421 -0
- package/dist/knowledge-indexing.d.ts +99 -0
- package/dist/knowledge-migration.d.ts +6 -0
- package/dist/managed-auth-session-sets.js +4 -3
- package/dist/mcp-operations.js +3 -2
- package/dist/mcp-operations.js.map +1 -1
- package/dist/migrate.js +2 -1
- package/dist/provision-roles.js +1 -1
- package/dist/retained-provider-commands.js +3 -2
- package/dist/retained-provider-commands.js.map +1 -1
- package/dist/runtime-posture.d.ts +7 -7
- package/dist/schema.d.ts +115 -12
- package/dist/schema.js +26 -1
- package/dist/session-background-commands.js +5 -4
- package/dist/session-command-output.js +8 -6
- package/dist/session-command-output.js.map +1 -1
- package/dist/session-event-slices.js +6 -4
- package/dist/session-event-slices.js.map +1 -1
- package/dist/session-mcp-credential-rotation.js +5 -4
- package/dist/session-mcp-credential-rotation.js.map +1 -1
- package/dist/session-tenancy.js +4 -3
- package/dist/video-generation.js +4 -3
- package/dist/workspace-artifacts.d.ts +1 -0
- package/dist/workspace-instruction-policies-schema.d.ts +21 -2
- package/dist/workspace-instruction-policies.d.ts +1 -1
- package/dist/workspace-membership-access.d.ts +11 -0
- package/dist/workspace-tool-defaults.js +5 -4
- package/dist/workspace-tool-defaults.js.map +1 -1
- package/drizzle/0461_unified_knowledge.sql +2859 -0
- package/drizzle/0462_agent_instruction_non_destructive_edits.sql +212 -0
- package/drizzle/0463_host_mcp_resolver_registration.sql +111 -0
- package/drizzle/0464_external_workspace_membership_operations.sql +166 -0
- package/package.json +6 -6
- package/src/connection-token-resolver.ts +75 -6
- package/src/database.ts +12 -1
- package/src/external-identities.ts +1 -0
- package/src/external-membership-operations.ts +134 -0
- package/src/governed-learning-evaluator.ts +0 -69
- package/src/host-mcp-binding-match.ts +1 -0
- package/src/host-mcp-bindings.ts +24 -1
- package/src/host-mcp-resolvers.ts +266 -0
- package/src/host-mcp-task-authority.ts +11 -10
- package/src/index.ts +307 -128
- package/src/knowledge-document-preparation.ts +90 -0
- package/src/knowledge-entries-schema.ts +264 -0
- package/src/knowledge-entries.ts +860 -0
- package/src/knowledge-indexing.ts +126 -0
- package/src/knowledge-migration.ts +761 -0
- package/src/knowledge-source-sync.ts +46 -7
- package/src/migrate.ts +19 -0
- package/src/provision-roles.ts +23 -57
- package/src/runtime-posture.ts +103 -7
- package/src/schema.ts +15 -3
- package/src/skills.ts +4 -0
- package/src/video-generation.ts +14 -0
- package/src/workspace-artifacts.ts +2 -0
- package/src/workspace-instruction-policies-schema.ts +1 -0
- package/src/workspace-membership-access.ts +61 -0
- package/dist/chunk-6TKL3GQO.js.map +0 -1
- package/dist/chunk-EYF6MH6C.js +0 -4772
- package/dist/chunk-EYF6MH6C.js.map +0 -1
- package/dist/chunk-GSBONN55.js +0 -533
- package/dist/chunk-GSBONN55.js.map +0 -1
- package/dist/chunk-GVZJ7XSR.js.map +0 -1
- package/dist/chunk-QLA3UQNX.js.map +0 -1
- package/dist/chunk-THJGMEK5.js.map +0 -1
- /package/dist/{chunk-2ZMTQVTR.js.map → chunk-2KOLL33E.js.map} +0 -0
- /package/dist/{chunk-232VC2LW.js.map → chunk-6V3HDEMR.js.map} +0 -0
- /package/dist/{chunk-M7PSMVFF.js.map → chunk-CHBLBEU6.js.map} +0 -0
- /package/dist/{chunk-UK57QA2H.js.map → chunk-UOXWVP3T.js.map} +0 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Old application instances omit editMode and retain their complete-content
|
|
3
|
+
-- replacement behavior. New instances require an explicit append/edit/replace
|
|
4
|
+
-- choice, so the normal append path cannot erase an active instruction.
|
|
5
|
+
|
|
6
|
+
CREATE OR REPLACE FUNCTION agent_instruction_apply(p_account uuid,p_workspace uuid,p_actor jsonb,p_request jsonb)
|
|
7
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT AS $$
|
|
8
|
+
<<instruction_change>>
|
|
9
|
+
DECLARE actor jsonb; mode text; operation text:=p_request->>'operation'; fingerprint text; result jsonb;
|
|
10
|
+
operation_id uuid:=(p_request->>'operationId')::uuid; revision_id uuid; batch uuid; actor_subject text;
|
|
11
|
+
target jsonb:=p_request->'target'; context jsonb; expected_revision uuid; expected_version bigint;
|
|
12
|
+
head workspace_instruction_policy_heads%ROWTYPE; revision workspace_instruction_policy_revisions%ROWTYPE;
|
|
13
|
+
-- Keep this forward CREATE OR REPLACE compilable in historical partial-replay
|
|
14
|
+
-- fixtures that predate 0461. Supported deployments have the table before
|
|
15
|
+
-- this function can execute; the untyped record acquires its row descriptor
|
|
16
|
+
-- from the SELECT below.
|
|
17
|
+
prior record; content_hash text; outcome text; link record; version bigint;
|
|
18
|
+
edit_mode text; requested_content text; old_text text; new_text text; current_content text;
|
|
19
|
+
resolved_content text; first_position integer; second_position integer;
|
|
20
|
+
BEGIN
|
|
21
|
+
IF p_account IS DISTINCT FROM nullif(current_setting('opengeni.account_id',true),'')::uuid
|
|
22
|
+
OR p_workspace IS DISTINCT FROM nullif(current_setting('opengeni.workspace_id',true),'')::uuid THEN
|
|
23
|
+
RAISE EXCEPTION 'Instruction changes require exact tenant scope' USING ERRCODE='42501';
|
|
24
|
+
END IF;
|
|
25
|
+
-- Keep the workspace identity stable without excluding unrelated FK readers.
|
|
26
|
+
-- Publication serializes on the account advisory lock below before the
|
|
27
|
+
-- instruction head, matching other Knowledge writers without an advisory /
|
|
28
|
+
-- workspace-row lock inversion.
|
|
29
|
+
PERFORM 1 FROM workspaces WHERE id=p_workspace AND account_id=p_account FOR KEY SHARE;
|
|
30
|
+
actor:=knowledge_resolve_actor(p_account,p_workspace,p_actor);
|
|
31
|
+
IF operation='get' THEN
|
|
32
|
+
SELECT * INTO head FROM workspace_instruction_policy_heads h WHERE h.account_id=p_account AND h.workspace_id=p_workspace
|
|
33
|
+
AND h.kind=target->>'kind' AND h.scope=target->>'scope' AND h.role_key IS NOT DISTINCT FROM target->>'roleKey';
|
|
34
|
+
SELECT * INTO revision FROM workspace_instruction_policy_revisions r WHERE r.account_id=p_account AND r.workspace_id=p_workspace
|
|
35
|
+
AND r.id=head.revision_id;
|
|
36
|
+
RETURN jsonb_build_object('target',target,'expectedCurrentRevisionId',head.revision_id,
|
|
37
|
+
'expectedActivationVersion',coalesce(head.activation_version,(SELECT max(d.activation_version)
|
|
38
|
+
FROM workspace_instruction_policy_deactivation_events d WHERE d.account_id=p_account AND d.workspace_id=p_workspace
|
|
39
|
+
AND d.kind=target->>'kind' AND d.scope=target->>'scope' AND d.role_key IS NOT DISTINCT FROM target->>'roleKey'),0),
|
|
40
|
+
'content',revision.content);
|
|
41
|
+
END IF;
|
|
42
|
+
IF operation='save' THEN
|
|
43
|
+
IF actor->>'kind'<>'agent' OR (actor->>'defaultScope'='personal' AND get_workspace_kind(p_account,p_workspace)<>'personal') THEN
|
|
44
|
+
RAISE EXCEPTION 'Workspace instructions require a workspace agent context' USING ERRCODE='42501';
|
|
45
|
+
END IF;
|
|
46
|
+
mode:=actor#>>'{policy,effective,instructions}';
|
|
47
|
+
IF mode NOT IN ('automatic','review_first') OR mode IS NULL THEN
|
|
48
|
+
RAISE EXCEPTION 'Instruction learning is Off' USING ERRCODE='42501';
|
|
49
|
+
END IF;
|
|
50
|
+
actor_subject:='service:agent-learning:'||(p_actor->>'attemptId');
|
|
51
|
+
ELSE
|
|
52
|
+
IF actor->>'kind'<>'human' OR NOT coalesce((p_actor->'settingsScopes') ? 'workspace',false) THEN
|
|
53
|
+
RAISE EXCEPTION 'Instruction review requires workspace administration' USING ERRCODE='42501';
|
|
54
|
+
END IF;
|
|
55
|
+
actor_subject:=actor->>'subjectId';
|
|
56
|
+
END IF;
|
|
57
|
+
IF operation='list' THEN
|
|
58
|
+
RETURN (SELECT coalesce(jsonb_agg(item ORDER BY identity),'[]'::jsonb) FROM (
|
|
59
|
+
SELECT r.id AS identity,jsonb_build_object('revisionId',r.id,'target',jsonb_build_object('kind',r.kind,'scope',r.scope,'roleKey',r.role_key),
|
|
60
|
+
'content',r.content,'createdAt',r.created_at,'reviewBatchId',knowledge_instruction_context(r)->'reviewBatchId',
|
|
61
|
+
'sessionId',knowledge_instruction_context(r)#>'{actor,sessionId}','reason',knowledge_instruction_context(r)->'reason') AS item
|
|
62
|
+
FROM workspace_instruction_policy_revisions r WHERE r.account_id=p_account AND r.workspace_id=p_workspace
|
|
63
|
+
AND knowledge_instruction_context(r) IS NOT NULL
|
|
64
|
+
AND NOT EXISTS(SELECT 1 FROM workspace_instruction_policy_activation_events ev WHERE ev.account_id=p_account
|
|
65
|
+
AND ev.workspace_id=p_workspace AND ev.new_revision_id=r.id)
|
|
66
|
+
AND NOT EXISTS(SELECT 1 FROM agent_instruction_operations op WHERE op.account_id=p_account AND op.revision_id=r.id
|
|
67
|
+
AND op.receipt->>'outcome' IN ('published','rejected'))
|
|
68
|
+
AND NOT EXISTS(SELECT 1 FROM workspace_instruction_policy_revisions newer WHERE newer.account_id=p_account
|
|
69
|
+
AND newer.workspace_id=p_workspace AND newer.kind=r.kind AND newer.scope=r.scope
|
|
70
|
+
AND newer.role_key IS NOT DISTINCT FROM r.role_key AND newer.revision>r.revision)
|
|
71
|
+
AND (NOT(p_request ? 'cursor') OR r.id>(p_request->>'cursor')::uuid)
|
|
72
|
+
ORDER BY r.id LIMIT 51) visible);
|
|
73
|
+
END IF;
|
|
74
|
+
IF operation_id IS NULL OR operation NOT IN ('save','approve','reject') OR length(btrim(p_request->>'reason')) NOT BETWEEN 1 AND 4096 THEN
|
|
75
|
+
RAISE EXCEPTION 'Invalid instruction change' USING ERRCODE='22023';
|
|
76
|
+
END IF;
|
|
77
|
+
IF operation='save' THEN
|
|
78
|
+
-- Calls from pre-0462 application instances carry complete replacement
|
|
79
|
+
-- content and omit editMode. Preserve that rolling-deployment contract.
|
|
80
|
+
edit_mode:=CASE WHEN p_request ? 'editMode' THEN p_request->>'editMode' ELSE 'replace' END;
|
|
81
|
+
IF edit_mode NOT IN ('append','edit','replace') THEN
|
|
82
|
+
RAISE EXCEPTION 'Invalid instruction edit mode' USING ERRCODE='22023';
|
|
83
|
+
END IF;
|
|
84
|
+
IF edit_mode IN ('append','replace') THEN
|
|
85
|
+
IF jsonb_typeof(p_request->'content') IS DISTINCT FROM 'string'
|
|
86
|
+
OR length(p_request->>'content') NOT BETWEEN 1 AND 600
|
|
87
|
+
OR length(btrim(p_request->>'content'))=0
|
|
88
|
+
OR p_request ? 'oldText' OR p_request ? 'newText' THEN
|
|
89
|
+
RAISE EXCEPTION 'Append and replace instruction changes require only non-empty content' USING ERRCODE='22023';
|
|
90
|
+
END IF;
|
|
91
|
+
requested_content:=p_request->>'content';
|
|
92
|
+
ELSE
|
|
93
|
+
IF p_request ? 'content'
|
|
94
|
+
OR jsonb_typeof(p_request->'oldText') IS DISTINCT FROM 'string'
|
|
95
|
+
OR jsonb_typeof(p_request->'newText') IS DISTINCT FROM 'string'
|
|
96
|
+
OR length(p_request->>'oldText') NOT BETWEEN 1 AND 600
|
|
97
|
+
OR length(p_request->>'newText')>600 THEN
|
|
98
|
+
RAISE EXCEPTION 'Exact instruction edits require oldText and newText only' USING ERRCODE='22023';
|
|
99
|
+
END IF;
|
|
100
|
+
old_text:=p_request->>'oldText';
|
|
101
|
+
new_text:=p_request->>'newText';
|
|
102
|
+
END IF;
|
|
103
|
+
IF jsonb_typeof(p_request->'evidence') IS DISTINCT FROM 'array' OR jsonb_array_length(p_request->'evidence')>32 THEN
|
|
104
|
+
RAISE EXCEPTION 'Invalid instruction evidence' USING ERRCODE='22023';
|
|
105
|
+
END IF;
|
|
106
|
+
END IF;
|
|
107
|
+
fingerprint:=encode(sha256(convert_to(jsonb_build_array(
|
|
108
|
+
CASE WHEN p_actor->>'kind'='agent' THEN p_actor-'attemptId'-'executionGeneration' ELSE p_actor END,
|
|
109
|
+
p_request)::text,'UTF8')),'hex');
|
|
110
|
+
SELECT * INTO prior FROM agent_instruction_operations op WHERE op.account_id=p_account AND op.operation_id=instruction_change.operation_id;
|
|
111
|
+
IF FOUND THEN
|
|
112
|
+
IF prior.request_hash<>fingerprint OR prior.origin_workspace_id<>p_workspace THEN
|
|
113
|
+
RAISE EXCEPTION 'Instruction operation key reused' USING ERRCODE='23505'; END IF;
|
|
114
|
+
RETURN prior.receipt||jsonb_build_object('replayed',true);
|
|
115
|
+
END IF;
|
|
116
|
+
IF operation<>'save' THEN
|
|
117
|
+
SELECT * INTO revision FROM workspace_instruction_policy_revisions r WHERE r.account_id=p_account AND r.workspace_id=p_workspace
|
|
118
|
+
AND r.id=(p_request->>'revisionId')::uuid AND knowledge_instruction_context(r) IS NOT NULL;
|
|
119
|
+
IF NOT FOUND OR EXISTS(SELECT 1 FROM agent_instruction_operations op WHERE op.account_id=p_account AND op.revision_id=revision.id
|
|
120
|
+
AND op.receipt->>'outcome' IN ('published','rejected')) OR EXISTS(SELECT 1 FROM workspace_instruction_policy_revisions newer
|
|
121
|
+
WHERE newer.account_id=p_account AND newer.workspace_id=p_workspace AND newer.kind=revision.kind AND newer.scope=revision.scope
|
|
122
|
+
AND newer.role_key IS NOT DISTINCT FROM revision.role_key AND newer.revision>revision.revision) THEN
|
|
123
|
+
RAISE EXCEPTION 'Instruction review is no longer current' USING ERRCODE='40001'; END IF;
|
|
124
|
+
revision_id:=revision.id; context:=knowledge_instruction_context(revision);
|
|
125
|
+
target:=jsonb_build_object('kind',revision.kind,'scope',revision.scope,'roleKey',revision.role_key);
|
|
126
|
+
expected_revision:=(context->>'expectedCurrentRevisionId')::uuid;
|
|
127
|
+
expected_version:=(context->>'expectedActivationVersion')::bigint;
|
|
128
|
+
batch:=(context->>'reviewBatchId')::uuid;
|
|
129
|
+
outcome:=CASE WHEN operation='approve' THEN 'published' ELSE 'rejected' END;
|
|
130
|
+
ELSE
|
|
131
|
+
expected_revision:=(p_request->>'expectedCurrentRevisionId')::uuid;
|
|
132
|
+
expected_version:=(p_request->>'expectedActivationVersion')::bigint;
|
|
133
|
+
outcome:=CASE WHEN mode='automatic' THEN 'published' ELSE 'pending' END;
|
|
134
|
+
IF outcome='pending' THEN batch:=knowledge_review_batch_for_actor(p_account,p_workspace,actor); END IF;
|
|
135
|
+
context:=jsonb_build_object('actor',p_actor,'policy',actor->'policy','evidence',p_request->'evidence','reason',p_request->'reason',
|
|
136
|
+
'editMode',edit_mode,'expectedCurrentRevisionId',expected_revision,'expectedActivationVersion',expected_version,'reviewBatchId',batch);
|
|
137
|
+
END IF;
|
|
138
|
+
IF operation<>'reject' THEN
|
|
139
|
+
PERFORM pg_advisory_xact_lock(hashtextextended('knowledge-publication:'||p_account,0));
|
|
140
|
+
END IF;
|
|
141
|
+
SELECT * INTO head FROM workspace_instruction_policy_heads h WHERE h.account_id=p_account AND h.workspace_id=p_workspace
|
|
142
|
+
AND h.kind=target->>'kind' AND h.scope=target->>'scope' AND h.role_key IS NOT DISTINCT FROM target->>'roleKey' FOR UPDATE;
|
|
143
|
+
version:=coalesce(head.activation_version,(SELECT max(d.activation_version) FROM workspace_instruction_policy_deactivation_events d
|
|
144
|
+
WHERE d.account_id=p_account AND d.workspace_id=p_workspace AND d.kind=target->>'kind' AND d.scope=target->>'scope'
|
|
145
|
+
AND d.role_key IS NOT DISTINCT FROM target->>'roleKey'),0);
|
|
146
|
+
IF operation<>'reject' AND (head.revision_id IS DISTINCT FROM expected_revision OR version IS DISTINCT FROM expected_version) THEN
|
|
147
|
+
RAISE EXCEPTION 'Instruction head changed; read its current baseline' USING ERRCODE='40001'; END IF;
|
|
148
|
+
IF operation='save' THEN
|
|
149
|
+
SELECT r.content INTO current_content FROM workspace_instruction_policy_revisions r
|
|
150
|
+
WHERE r.account_id=p_account AND r.workspace_id=p_workspace AND r.id=head.revision_id;
|
|
151
|
+
IF edit_mode='append' THEN
|
|
152
|
+
resolved_content:=CASE
|
|
153
|
+
WHEN current_content IS NULL THEN requested_content
|
|
154
|
+
WHEN right(current_content,2)=E'\n\n' THEN current_content||requested_content
|
|
155
|
+
WHEN right(current_content,1)=E'\n' THEN current_content||E'\n'||requested_content
|
|
156
|
+
ELSE current_content||E'\n\n'||requested_content
|
|
157
|
+
END;
|
|
158
|
+
ELSIF edit_mode='edit' THEN
|
|
159
|
+
IF current_content IS NULL THEN
|
|
160
|
+
RAISE EXCEPTION 'Exact instruction edit requires an active instruction' USING ERRCODE='22023';
|
|
161
|
+
END IF;
|
|
162
|
+
first_position:=strpos(current_content,old_text);
|
|
163
|
+
IF first_position=0 THEN
|
|
164
|
+
RAISE EXCEPTION 'Exact instruction edit anchor was not found' USING ERRCODE='22023';
|
|
165
|
+
END IF;
|
|
166
|
+
second_position:=strpos(substring(current_content FROM first_position+1),old_text);
|
|
167
|
+
IF second_position>0 THEN
|
|
168
|
+
RAISE EXCEPTION 'Exact instruction edit anchor is ambiguous' USING ERRCODE='22023';
|
|
169
|
+
END IF;
|
|
170
|
+
resolved_content:=overlay(current_content PLACING new_text FROM first_position FOR length(old_text));
|
|
171
|
+
ELSE
|
|
172
|
+
resolved_content:=requested_content;
|
|
173
|
+
END IF;
|
|
174
|
+
IF length(resolved_content) NOT BETWEEN 1 AND 600 OR length(btrim(resolved_content))=0 THEN
|
|
175
|
+
RAISE EXCEPTION 'Resulting agent-authored instruction must contain 1 to 600 characters' USING ERRCODE='22023';
|
|
176
|
+
END IF;
|
|
177
|
+
END IF;
|
|
178
|
+
IF operation<>'reject' THEN
|
|
179
|
+
FOR link IN SELECT * FROM jsonb_array_elements(context->'evidence') LOOP
|
|
180
|
+
IF NOT EXISTS(SELECT 1 FROM knowledge_entries e WHERE e.account_id=p_account AND e.id=(link.value->>'entryId')::uuid
|
|
181
|
+
AND NOT e.archived AND e.scope IN ('workspace','organization') AND knowledge_scope_visible(e)
|
|
182
|
+
AND knowledge_revision_visible(p_account,e.id,(link.value->>'revisionId')::uuid,false)) THEN
|
|
183
|
+
RAISE EXCEPTION 'Instruction evidence is unavailable to workspace readers' USING ERRCODE='42501'; END IF;
|
|
184
|
+
END LOOP;
|
|
185
|
+
END IF;
|
|
186
|
+
IF operation='save' THEN
|
|
187
|
+
content_hash:=encode(sha256(convert_to(resolved_content,'UTF8')),'hex');
|
|
188
|
+
INSERT INTO workspace_instruction_policy_revisions(account_id,workspace_id,operation_id,request_fingerprint,kind,scope,role_key,
|
|
189
|
+
content,content_hash,provenance_source,provenance_source_id,created_by_subject_id,supersedes_revision_id,agent_learning_context)
|
|
190
|
+
VALUES(p_account,p_workspace,operation_id,fingerprint,target->>'kind',target->>'scope',target->>'roleKey',resolved_content,
|
|
191
|
+
content_hash,'agent_learning',p_actor->>'attemptId',actor_subject,expected_revision,context) RETURNING * INTO revision;
|
|
192
|
+
revision_id:=revision.id;
|
|
193
|
+
END IF;
|
|
194
|
+
IF outcome='published' THEN
|
|
195
|
+
INSERT INTO workspace_instruction_policy_activation_events(account_id,workspace_id,operation_id,request_fingerprint,
|
|
196
|
+
kind,scope,role_key,type,activation_version,old_revision_id,old_revision,old_content_hash,new_revision_id,new_revision,new_content_hash,
|
|
197
|
+
actor_subject_id,reason)
|
|
198
|
+
VALUES(p_account,p_workspace,operation_id,fingerprint,revision.kind,revision.scope,revision.role_key,'activate',version+1,
|
|
199
|
+
head.revision_id,head.revision,head.content_hash,revision.id,revision.revision,revision.content_hash,actor_subject,p_request->>'reason');
|
|
200
|
+
IF head.id IS NULL THEN
|
|
201
|
+
INSERT INTO workspace_instruction_policy_heads(account_id,workspace_id,kind,scope,role_key,revision_id,revision,content_hash,activation_version)
|
|
202
|
+
VALUES(p_account,p_workspace,revision.kind,revision.scope,revision.role_key,revision.id,revision.revision,revision.content_hash,version+1);
|
|
203
|
+
ELSE
|
|
204
|
+
UPDATE workspace_instruction_policy_heads h SET revision_id=revision.id,revision=revision.revision,content_hash=revision.content_hash,
|
|
205
|
+
activation_version=version+1,activated_at=transaction_timestamp() WHERE h.account_id=p_account AND h.id=head.id;
|
|
206
|
+
END IF;
|
|
207
|
+
END IF;
|
|
208
|
+
result:=jsonb_build_object('operationId',operation_id,'revisionId',revision_id,'outcome',outcome,'reviewBatchId',batch,'replayed',false);
|
|
209
|
+
INSERT INTO agent_instruction_operations(account_id,origin_workspace_id,operation_id,revision_id,request_hash,actor,receipt)
|
|
210
|
+
VALUES(p_account,p_workspace,operation_id,revision_id,fingerprint,p_actor,result);
|
|
211
|
+
RETURN result;
|
|
212
|
+
END $$;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
-- deployment-mode: maintenance
|
|
2
|
+
-- Native namespace routing changes the exact runtime role contract. Stop old
|
|
3
|
+
-- API/worker processes, migrate/provision roles, then start matching binaries.
|
|
4
|
+
SET LOCAL lock_timeout = '5s';
|
|
5
|
+
SET LOCAL statement_timeout = '5min';
|
|
6
|
+
DO $drain$
|
|
7
|
+
DECLARE roles jsonb := nullif(current_setting('opengeni.migration_application_roles', true), '')::jsonb;
|
|
8
|
+
BEGIN
|
|
9
|
+
IF roles IS NULL OR jsonb_typeof(roles) <> 'array' OR jsonb_array_length(roles) NOT BETWEEN 1 AND 16 THEN
|
|
10
|
+
RAISE EXCEPTION '0463 requires application database roles' USING ERRCODE = '55000';
|
|
11
|
+
END IF;
|
|
12
|
+
IF EXISTS (SELECT 1 FROM jsonb_array_elements(roles) item WHERE jsonb_typeof(item) <> 'string'
|
|
13
|
+
OR btrim(item #>> '{}') = '' OR item #>> '{}' <> btrim(item #>> '{}') OR octet_length(item #>> '{}') > 63)
|
|
14
|
+
OR EXISTS (SELECT 1 FROM pg_stat_activity a JOIN jsonb_array_elements_text(roles) r ON r = a.usename
|
|
15
|
+
WHERE a.datname = current_database() AND a.pid <> pg_backend_pid()) THEN
|
|
16
|
+
RAISE EXCEPTION '0463 requires valid stopped application roles' USING ERRCODE = '55000';
|
|
17
|
+
END IF;
|
|
18
|
+
END
|
|
19
|
+
$drain$;
|
|
20
|
+
|
|
21
|
+
CREATE TABLE host_mcp_resolvers (
|
|
22
|
+
id uuid PRIMARY KEY,
|
|
23
|
+
account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE CASCADE,
|
|
24
|
+
external_source text NOT NULL CHECK (length(external_source) BETWEEN 1 AND 200 AND external_source = btrim(external_source)),
|
|
25
|
+
url text NOT NULL CHECK (length(url) BETWEEN 1 AND 2048),
|
|
26
|
+
secret_encrypted text NOT NULL,
|
|
27
|
+
timeout_ms integer NOT NULL CHECK (timeout_ms BETWEEN 100 AND 30000),
|
|
28
|
+
generation bigint NOT NULL CHECK (generation BETWEEN 1 AND 9007199254740991),
|
|
29
|
+
status text NOT NULL CHECK (status IN ('active', 'revoked')),
|
|
30
|
+
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
31
|
+
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
32
|
+
UNIQUE (account_id, external_source)
|
|
33
|
+
);
|
|
34
|
+
CREATE TABLE host_mcp_resolver_operations (
|
|
35
|
+
account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE CASCADE,
|
|
36
|
+
operation_id uuid NOT NULL,
|
|
37
|
+
actor_subject_id text NOT NULL,
|
|
38
|
+
request_digest text NOT NULL CHECK (request_digest ~ '^[0-9a-f]{64}$'),
|
|
39
|
+
result jsonb NOT NULL,
|
|
40
|
+
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
41
|
+
PRIMARY KEY (account_id, operation_id)
|
|
42
|
+
);
|
|
43
|
+
ALTER TABLE host_mcp_resolvers ENABLE ROW LEVEL SECURITY;
|
|
44
|
+
ALTER TABLE host_mcp_resolvers FORCE ROW LEVEL SECURITY;
|
|
45
|
+
ALTER TABLE host_mcp_resolver_operations ENABLE ROW LEVEL SECURITY;
|
|
46
|
+
ALTER TABLE host_mcp_resolver_operations FORCE ROW LEVEL SECURITY;
|
|
47
|
+
CREATE POLICY host_mcp_resolvers_account ON host_mcp_resolvers
|
|
48
|
+
USING (account_id = opengeni_private.current_account_id())
|
|
49
|
+
WITH CHECK (account_id = opengeni_private.current_account_id());
|
|
50
|
+
CREATE POLICY host_mcp_resolver_operations_account ON host_mcp_resolver_operations
|
|
51
|
+
USING (account_id = opengeni_private.current_account_id())
|
|
52
|
+
WITH CHECK (account_id = opengeni_private.current_account_id());
|
|
53
|
+
|
|
54
|
+
CREATE FUNCTION opengeni_private.guard_host_mcp_resolver_write() RETURNS trigger
|
|
55
|
+
LANGUAGE plpgsql SET search_path FROM CURRENT AS $body$
|
|
56
|
+
DECLARE permissions_value jsonb;
|
|
57
|
+
BEGIN
|
|
58
|
+
SELECT credential.permissions INTO permissions_value FROM api_keys credential
|
|
59
|
+
WHERE credential.account_id = NEW.account_id
|
|
60
|
+
AND 'api_key:' || credential.id::text = opengeni_private.current_subject_id()
|
|
61
|
+
AND credential.workspace_id IS NULL AND credential.credential_kind = 'organization'
|
|
62
|
+
AND credential.revoked_at IS NULL
|
|
63
|
+
AND (credential.expires_at IS NULL OR credential.expires_at > clock_timestamp())
|
|
64
|
+
FOR SHARE;
|
|
65
|
+
IF NEW.account_id IS DISTINCT FROM opengeni_private.current_account_id()
|
|
66
|
+
OR NOT coalesce(permissions_value ? 'account:admin', false) THEN
|
|
67
|
+
RAISE EXCEPTION 'organization service administration required' USING ERRCODE = '42501';
|
|
68
|
+
END IF;
|
|
69
|
+
IF TG_TABLE_NAME = 'host_mcp_resolvers' THEN
|
|
70
|
+
IF TG_OP = 'UPDATE' THEN
|
|
71
|
+
IF ROW(NEW.id, NEW.account_id, NEW.external_source, NEW.created_at)
|
|
72
|
+
IS DISTINCT FROM ROW(OLD.id, OLD.account_id, OLD.external_source, OLD.created_at)
|
|
73
|
+
OR NEW.generation <> OLD.generation + 1 THEN
|
|
74
|
+
RAISE EXCEPTION 'resolver identity or generation conflict' USING ERRCODE = '40001';
|
|
75
|
+
END IF;
|
|
76
|
+
ELSIF NEW.generation <> 1 THEN
|
|
77
|
+
RAISE EXCEPTION 'initial resolver generation invalid' USING ERRCODE = '22023';
|
|
78
|
+
END IF;
|
|
79
|
+
ELSIF NEW.actor_subject_id IS DISTINCT FROM opengeni_private.current_subject_id() THEN
|
|
80
|
+
RAISE EXCEPTION 'resolver operation actor invalid' USING ERRCODE = '42501';
|
|
81
|
+
END IF;
|
|
82
|
+
RETURN NEW;
|
|
83
|
+
END
|
|
84
|
+
$body$;
|
|
85
|
+
CREATE TRIGGER host_mcp_resolver_write BEFORE INSERT OR UPDATE ON host_mcp_resolvers
|
|
86
|
+
FOR EACH ROW EXECUTE FUNCTION opengeni_private.guard_host_mcp_resolver_write();
|
|
87
|
+
CREATE TRIGGER host_mcp_resolver_operation_write BEFORE INSERT ON host_mcp_resolver_operations
|
|
88
|
+
FOR EACH ROW EXECUTE FUNCTION opengeni_private.guard_host_mcp_resolver_write();
|
|
89
|
+
REVOKE ALL ON FUNCTION opengeni_private.guard_host_mcp_resolver_write() FROM PUBLIC;
|
|
90
|
+
REVOKE ALL ON TABLE host_mcp_resolvers, host_mcp_resolver_operations FROM PUBLIC;
|
|
91
|
+
DO $acl$
|
|
92
|
+
DECLARE target_role record;
|
|
93
|
+
BEGIN
|
|
94
|
+
FOR target_role IN SELECT DISTINCT role.rolname FROM pg_class relation
|
|
95
|
+
CROSS JOIN LATERAL aclexplode(coalesce(relation.relacl, acldefault('r', relation.relowner))) privilege
|
|
96
|
+
JOIN pg_roles role ON role.oid = privilege.grantee
|
|
97
|
+
WHERE relation.oid IN ('host_mcp_resolvers'::regclass, 'host_mcp_resolver_operations'::regclass)
|
|
98
|
+
AND privilege.grantee <> relation.relowner
|
|
99
|
+
LOOP EXECUTE format('REVOKE ALL ON TABLE host_mcp_resolvers, host_mcp_resolver_operations FROM %I', target_role.rolname); END LOOP;
|
|
100
|
+
FOR target_role IN SELECT DISTINCT role.rolname FROM pg_proc routine
|
|
101
|
+
CROSS JOIN LATERAL aclexplode(coalesce(routine.proacl, acldefault('f', routine.proowner))) privilege
|
|
102
|
+
JOIN pg_roles role ON role.oid = privilege.grantee
|
|
103
|
+
WHERE routine.oid = 'opengeni_private.guard_host_mcp_resolver_write()'::regprocedure AND privilege.grantee <> routine.proowner
|
|
104
|
+
LOOP EXECUTE format('REVOKE ALL ON FUNCTION opengeni_private.guard_host_mcp_resolver_write() FROM %I', target_role.rolname); END LOOP;
|
|
105
|
+
IF EXISTS (SELECT 1 FROM pg_stat_activity a
|
|
106
|
+
JOIN jsonb_array_elements_text(current_setting('opengeni.migration_application_roles')::jsonb) r ON r = a.usename
|
|
107
|
+
WHERE a.datname = current_database() AND a.pid <> pg_backend_pid()) THEN
|
|
108
|
+
RAISE EXCEPTION '0463 requires stopped application sessions' USING ERRCODE = '55000';
|
|
109
|
+
END IF;
|
|
110
|
+
END
|
|
111
|
+
$acl$;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
-- deployment-mode: rolling
|
|
2
|
+
-- Opt-in external service operations reuse the native workspace receipt ledger
|
|
3
|
+
-- and membership-removal protocol. Unkeyed in-flight grants cannot be fenced.
|
|
4
|
+
SET LOCAL lock_timeout = '5s';
|
|
5
|
+
SET LOCAL statement_timeout = '5min';
|
|
6
|
+
|
|
7
|
+
ALTER TABLE organization_workspace_lifecycle_events
|
|
8
|
+
ALTER COLUMN actor_membership_id DROP NOT NULL,
|
|
9
|
+
ADD COLUMN actor_service_subject text,
|
|
10
|
+
ADD CONSTRAINT organization_workspace_lifecycle_actor_check CHECK (
|
|
11
|
+
(actor_membership_id IS NOT NULL AND actor_service_subject IS NULL)
|
|
12
|
+
OR (actor_membership_id IS NULL AND actor_service_subject IS NOT NULL AND actor_service_subject ~ '^api_key:[0-9a-f-]{36}$')
|
|
13
|
+
);
|
|
14
|
+
CREATE INDEX organization_workspace_cancelled_grant_idx
|
|
15
|
+
ON organization_workspace_operation_receipts (account_id, (result ->> 'fencedGrantOperationId'))
|
|
16
|
+
WHERE action = 'revoke' AND result ? 'fencedGrantOperationId';
|
|
17
|
+
|
|
18
|
+
-- Authentication is performed by the API. This is its live commit-time fence,
|
|
19
|
+
-- not permission to derive an actor from request JSON.
|
|
20
|
+
CREATE FUNCTION opengeni_private.external_workspace_service_permissions(p_account uuid, p_actor text)
|
|
21
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT AS $body$
|
|
22
|
+
DECLARE permissions_value jsonb;
|
|
23
|
+
BEGIN
|
|
24
|
+
IF p_account IS DISTINCT FROM opengeni_private.current_account_id()
|
|
25
|
+
OR p_actor IS NULL OR p_actor IS DISTINCT FROM opengeni_private.current_subject_id()
|
|
26
|
+
THEN RAISE EXCEPTION 'external workspace service scope invalid' USING ERRCODE = '42501'; END IF;
|
|
27
|
+
SELECT k.permissions INTO permissions_value FROM api_keys k
|
|
28
|
+
WHERE k.account_id = p_account AND 'api_key:' || k.id::text = p_actor
|
|
29
|
+
AND k.credential_kind = 'organization' AND k.workspace_id IS NULL
|
|
30
|
+
AND k.revoked_at IS NULL AND (k.expires_at IS NULL OR k.expires_at > clock_timestamp())
|
|
31
|
+
FOR SHARE;
|
|
32
|
+
IF NOT coalesce(permissions_value ?| ARRAY['members:manage', 'workspace:admin'], false)
|
|
33
|
+
THEN RAISE EXCEPTION 'external workspace service administration required' USING ERRCODE = '42501'; END IF;
|
|
34
|
+
RETURN permissions_value;
|
|
35
|
+
END $body$;
|
|
36
|
+
REVOKE ALL ON FUNCTION opengeni_private.external_workspace_service_permissions(uuid,text) FROM PUBLIC;
|
|
37
|
+
|
|
38
|
+
CREATE FUNCTION lookup_external_identity(p_account uuid, p_actor text, p_source text, p_external_id text)
|
|
39
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT AS $body$
|
|
40
|
+
DECLARE result jsonb; previous_marker text := current_setting('opengeni.organization_tenancy_lifecycle', true);
|
|
41
|
+
BEGIN
|
|
42
|
+
PERFORM opengeni_private.external_workspace_service_permissions(p_account, p_actor);
|
|
43
|
+
PERFORM set_config('opengeni.organization_tenancy_lifecycle', 'external_identity_provisioning', true);
|
|
44
|
+
SELECT jsonb_build_object('found', true, 'subjectId', i.subject_id,
|
|
45
|
+
'organizationMembershipId', m.id, 'identityStatus', i.status,
|
|
46
|
+
'identityAuthorizationRevision', i.authorization_revision,
|
|
47
|
+
'membershipStatus', m.status, 'membershipAuthorizationRevision', m.authorization_revision)
|
|
48
|
+
INTO result FROM external_identities i JOIN organization_memberships m
|
|
49
|
+
ON m.account_id = i.account_id AND m.id = i.organization_membership_id AND m.subject_id = i.subject_id
|
|
50
|
+
WHERE i.account_id = p_account AND i.source = p_source AND i.external_id = p_external_id;
|
|
51
|
+
PERFORM set_config('opengeni.organization_tenancy_lifecycle', coalesce(previous_marker,''), true);
|
|
52
|
+
RETURN coalesce(result, '{"found":false}'::jsonb);
|
|
53
|
+
END $body$;
|
|
54
|
+
REVOKE ALL ON FUNCTION lookup_external_identity(uuid,text,text,text) FROM PUBLIC;
|
|
55
|
+
|
|
56
|
+
-- Both preparation and recording run in the caller's ONE open transaction.
|
|
57
|
+
-- The same organization prefix serializes unstarted grants with cancellation.
|
|
58
|
+
CREATE FUNCTION prepare_external_workspace_membership_operation(p_command jsonb)
|
|
59
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT AS $body$
|
|
60
|
+
DECLARE
|
|
61
|
+
account_value uuid := (p_command ->> 'organizationId')::uuid;
|
|
62
|
+
workspace_value uuid := (p_command ->> 'workspaceId')::uuid;
|
|
63
|
+
operation_value uuid := (p_command ->> 'operationId')::uuid;
|
|
64
|
+
actor_value text := p_command ->> 'actorSubjectId';
|
|
65
|
+
action_value text := p_command ->> 'action';
|
|
66
|
+
cancelled_value uuid := (p_command ->> 'cancelGrantOperationId')::uuid;
|
|
67
|
+
service_permissions jsonb; identity_value jsonb; prior record; cancellation record;
|
|
68
|
+
hash_value text; previous_marker text := current_setting('opengeni.organization_tenancy_lifecycle', true);
|
|
69
|
+
BEGIN
|
|
70
|
+
IF account_value IS NULL OR workspace_value IS NULL OR operation_value IS NULL
|
|
71
|
+
OR action_value IS NULL OR action_value NOT IN ('grant','revoke')
|
|
72
|
+
OR (action_value = 'revoke' AND (cancelled_value IS NULL OR cancelled_value = operation_value))
|
|
73
|
+
OR (action_value = 'grant' AND cancelled_value IS NOT NULL)
|
|
74
|
+
THEN RAISE EXCEPTION 'invalid external workspace operation' USING ERRCODE = '22023'; END IF;
|
|
75
|
+
PERFORM pg_advisory_xact_lock(hashtextextended('organization-membership:' || account_value::text, 0));
|
|
76
|
+
service_permissions := opengeni_private.external_workspace_service_permissions(account_value, actor_value);
|
|
77
|
+
IF action_value = 'grant' AND (jsonb_typeof(p_command -> 'permissions') IS DISTINCT FROM 'array'
|
|
78
|
+
OR jsonb_array_length(p_command -> 'permissions') = 0
|
|
79
|
+
OR ((p_command -> 'permissions') ? 'secrets:read' AND NOT service_permissions ? 'secrets:read')
|
|
80
|
+
OR (NOT service_permissions ? 'workspace:admin' AND NOT (p_command -> 'permissions') <@ service_permissions))
|
|
81
|
+
THEN RAISE EXCEPTION 'external grant exceeds service authority' USING ERRCODE = '42501'; END IF;
|
|
82
|
+
PERFORM 1 FROM workspaces w WHERE w.account_id = account_value AND w.id = workspace_value FOR KEY SHARE;
|
|
83
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'workspace not found' USING ERRCODE = 'P0002'; END IF;
|
|
84
|
+
PERFORM set_config('opengeni.organization_tenancy_lifecycle', 'organization_membership_lifecycle', true);
|
|
85
|
+
IF EXISTS (SELECT 1 FROM organization_memberships m WHERE m.account_id = account_value AND m.personal_workspace_id = workspace_value)
|
|
86
|
+
THEN RAISE EXCEPTION 'personal workspace is not administrable' USING ERRCODE = '42501'; END IF;
|
|
87
|
+
-- Credential identity is audit attribution, not the operation's semantic
|
|
88
|
+
-- identity: an authorized replacement organization key may reconcile it.
|
|
89
|
+
hash_value := encode(sha256(convert_to((p_command - 'actorSubjectId')::text, 'UTF8')), 'hex');
|
|
90
|
+
SELECT * INTO prior FROM organization_workspace_operation_receipts
|
|
91
|
+
WHERE account_id = account_value AND operation_id = operation_value;
|
|
92
|
+
IF FOUND AND (prior.action <> action_value OR prior.input_hash <> hash_value)
|
|
93
|
+
THEN RAISE EXCEPTION 'operation identity reused' USING ERRCODE = '23505'; END IF;
|
|
94
|
+
IF action_value = 'grant' THEN
|
|
95
|
+
SELECT * INTO cancellation FROM organization_workspace_operation_receipts
|
|
96
|
+
WHERE account_id = account_value AND action = 'revoke'
|
|
97
|
+
AND result ->> 'fencedGrantOperationId' = operation_value::text LIMIT 1;
|
|
98
|
+
IF FOUND THEN RAISE EXCEPTION 'external membership grant cancelled' USING ERRCODE = '55000'; END IF;
|
|
99
|
+
END IF;
|
|
100
|
+
IF prior.operation_id IS NOT NULL THEN
|
|
101
|
+
PERFORM set_config('opengeni.organization_tenancy_lifecycle', coalesce(previous_marker,''), true);
|
|
102
|
+
RETURN jsonb_build_object('replay', true, 'result', prior.result);
|
|
103
|
+
END IF;
|
|
104
|
+
PERFORM set_config('opengeni.organization_tenancy_lifecycle', 'external_identity_provisioning', true);
|
|
105
|
+
IF action_value = 'grant' THEN
|
|
106
|
+
identity_value := ensure_external_identity(account_value, p_command #>> '{identity,source}', p_command #>> '{identity,externalId}');
|
|
107
|
+
ELSE
|
|
108
|
+
SELECT jsonb_build_object('subjectId', i.subject_id, 'organizationMembershipId', i.organization_membership_id)
|
|
109
|
+
INTO identity_value FROM external_identities i
|
|
110
|
+
WHERE i.account_id = account_value AND i.organization_membership_id = (p_command ->> 'membershipId')::uuid;
|
|
111
|
+
IF identity_value IS NULL THEN RAISE EXCEPTION 'external member not found' USING ERRCODE = 'P0002'; END IF;
|
|
112
|
+
PERFORM set_config('opengeni.organization_tenancy_lifecycle', 'organization_membership_lifecycle', true);
|
|
113
|
+
SELECT * INTO prior FROM organization_workspace_operation_receipts
|
|
114
|
+
WHERE account_id = account_value AND operation_id = cancelled_value;
|
|
115
|
+
IF FOUND AND (prior.action <> 'grant' OR prior.result ->> 'workspaceId' IS DISTINCT FROM workspace_value::text
|
|
116
|
+
OR prior.result #>> '{identity,organizationMembershipId}' IS DISTINCT FROM identity_value ->> 'organizationMembershipId')
|
|
117
|
+
THEN RAISE EXCEPTION 'cancelled operation target mismatch' USING ERRCODE = '23505'; END IF;
|
|
118
|
+
END IF;
|
|
119
|
+
PERFORM set_config('opengeni.organization_tenancy_lifecycle', coalesce(previous_marker,''), true);
|
|
120
|
+
RETURN jsonb_build_object('replay', false, 'identity', identity_value);
|
|
121
|
+
END $body$;
|
|
122
|
+
REVOKE ALL ON FUNCTION prepare_external_workspace_membership_operation(jsonb) FROM PUBLIC;
|
|
123
|
+
|
|
124
|
+
CREATE FUNCTION record_external_workspace_membership_operation(p_command jsonb, p_result jsonb)
|
|
125
|
+
RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT AS $body$
|
|
126
|
+
DECLARE prepared jsonb; subject_value text; member_value uuid; hash_value text;
|
|
127
|
+
account_value uuid := (p_command ->> 'organizationId')::uuid;
|
|
128
|
+
workspace_value uuid := (p_command ->> 'workspaceId')::uuid;
|
|
129
|
+
action_value text := p_command ->> 'action';
|
|
130
|
+
previous_marker text := current_setting('opengeni.organization_tenancy_lifecycle', true);
|
|
131
|
+
BEGIN
|
|
132
|
+
prepared := prepare_external_workspace_membership_operation(p_command);
|
|
133
|
+
IF (prepared ->> 'replay')::boolean THEN RAISE EXCEPTION 'operation already recorded' USING ERRCODE = '23505'; END IF;
|
|
134
|
+
subject_value := prepared #>> '{identity,subjectId}';
|
|
135
|
+
IF action_value = 'grant' AND p_result -> 'identity' IS DISTINCT FROM prepared -> 'identity'
|
|
136
|
+
THEN RAISE EXCEPTION 'grant receipt identity mismatch' USING ERRCODE = '55000'; END IF;
|
|
137
|
+
IF p_result ->> 'workspaceId' IS DISTINCT FROM workspace_value::text
|
|
138
|
+
OR (action_value = 'revoke' AND p_result ->> 'fencedGrantOperationId' IS DISTINCT FROM p_command ->> 'cancelGrantOperationId')
|
|
139
|
+
THEN RAISE EXCEPTION 'operation receipt scope mismatch' USING ERRCODE = '55000'; END IF;
|
|
140
|
+
SELECT id INTO member_value FROM workspace_memberships
|
|
141
|
+
WHERE account_id = account_value AND workspace_id = workspace_value AND subject_id = subject_value;
|
|
142
|
+
IF (action_value = 'grant' AND member_value IS NULL) OR (action_value = 'revoke' AND member_value IS NOT NULL)
|
|
143
|
+
THEN RAISE EXCEPTION 'operation effects not settled' USING ERRCODE = '55000'; END IF;
|
|
144
|
+
hash_value := encode(sha256(convert_to((p_command - 'actorSubjectId')::text, 'UTF8')), 'hex');
|
|
145
|
+
PERFORM set_config('opengeni.organization_tenancy_lifecycle', 'organization_membership_lifecycle', true);
|
|
146
|
+
INSERT INTO organization_workspace_operation_receipts (account_id, operation_id, action, input_hash, result)
|
|
147
|
+
VALUES (account_value, (p_command ->> 'operationId')::uuid, action_value, hash_value, p_result);
|
|
148
|
+
INSERT INTO organization_workspace_lifecycle_events (account_id, operation_id, actor_service_subject,
|
|
149
|
+
workspace_id, target_organization_membership_id, target_workspace_membership_id, kind, role)
|
|
150
|
+
VALUES (account_value, (p_command ->> 'operationId')::uuid, p_command ->> 'actorSubjectId', workspace_value,
|
|
151
|
+
(prepared #>> '{identity,organizationMembershipId}')::uuid, member_value, action_value,
|
|
152
|
+
CASE WHEN action_value = 'grant' THEN 'member' ELSE NULL END);
|
|
153
|
+
PERFORM set_config('opengeni.organization_tenancy_lifecycle', coalesce(previous_marker,''), true);
|
|
154
|
+
END $body$;
|
|
155
|
+
REVOKE ALL ON FUNCTION record_external_workspace_membership_operation(jsonb,jsonb) FROM PUBLIC;
|
|
156
|
+
|
|
157
|
+
DO $acl$
|
|
158
|
+
DECLARE target_role record;
|
|
159
|
+
BEGIN
|
|
160
|
+
FOR target_role IN SELECT DISTINCT grantee FROM information_schema.routine_privileges
|
|
161
|
+
WHERE routine_schema = current_schema() AND routine_name = 'ensure_external_identity'
|
|
162
|
+
AND grantee <> 'PUBLIC' AND privilege_type = 'EXECUTE'
|
|
163
|
+
LOOP
|
|
164
|
+
EXECUTE format('GRANT EXECUTE ON FUNCTION lookup_external_identity(uuid,text,text,text), prepare_external_workspace_membership_operation(jsonb), record_external_workspace_membership_operation(jsonb,jsonb) TO %I', target_role.grantee);
|
|
165
|
+
END LOOP;
|
|
166
|
+
END $acl$;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/db",
|
|
3
|
-
"version": "4.4.0-canary.
|
|
3
|
+
"version": "4.4.0-canary.2",
|
|
4
4
|
"description": "OpenGeni persistence: Drizzle schema, RLS-scoped query layer, the SQL migration runner, and role provisioning.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -102,11 +102,11 @@
|
|
|
102
102
|
"prepublishOnly": "bash ../../scripts/prepublish-guard"
|
|
103
103
|
},
|
|
104
104
|
"dependencies": {
|
|
105
|
-
"@opengeni/codemode": "^0.5.7-canary.
|
|
106
|
-
"@opengeni/codex": "^0.2.22-canary.
|
|
107
|
-
"@opengeni/config": "^1.2.0-canary.
|
|
108
|
-
"@opengeni/contracts": "^3.1.0-canary.
|
|
109
|
-
"@opengeni/network": "^0.3.1-canary.
|
|
105
|
+
"@opengeni/codemode": "^0.5.7-canary.2",
|
|
106
|
+
"@opengeni/codex": "^0.2.22-canary.14",
|
|
107
|
+
"@opengeni/config": "^1.2.0-canary.2",
|
|
108
|
+
"@opengeni/contracts": "^3.1.0-canary.2",
|
|
109
|
+
"@opengeni/network": "^0.3.1-canary.14",
|
|
110
110
|
"drizzle-orm": "^0.45.2",
|
|
111
111
|
"postgres": "^3.4.7"
|
|
112
112
|
},
|