@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,2859 @@
|
|
|
1
|
+
-- deployment-mode: maintenance
|
|
2
|
+
-- Unified retained Knowledge. Old binaries must stay stopped after activation.
|
|
3
|
+
SET LOCAL lock_timeout = '5s';
|
|
4
|
+
SET LOCAL statement_timeout = '10min';
|
|
5
|
+
|
|
6
|
+
DO $drain$
|
|
7
|
+
DECLARE roles jsonb := nullif(current_setting('opengeni.migration_application_roles', true), '')::jsonb;
|
|
8
|
+
BEGIN
|
|
9
|
+
IF to_regclass('pg_temp.knowledge_conversion_0461') IS NULL THEN
|
|
10
|
+
RAISE EXCEPTION '0461 requires the codec-aware TypeScript migration runner' USING ERRCODE='55000';
|
|
11
|
+
END IF;
|
|
12
|
+
IF roles IS NULL OR jsonb_typeof(roles) <> 'array' THEN
|
|
13
|
+
RAISE EXCEPTION '0461 requires explicit application database roles' USING ERRCODE='55000';
|
|
14
|
+
END IF;
|
|
15
|
+
IF jsonb_array_length(roles) NOT BETWEEN 1 AND 16 OR EXISTS (
|
|
16
|
+
SELECT 1 FROM jsonb_array_elements(roles) item WHERE jsonb_typeof(item) <> 'string'
|
|
17
|
+
OR octet_length(item #>> '{}') NOT BETWEEN 1 AND 63
|
|
18
|
+
OR item #>> '{}' <> btrim(item #>> '{}')
|
|
19
|
+
) THEN RAISE EXCEPTION '0461 received invalid application roles' USING ERRCODE='55000'; END IF;
|
|
20
|
+
IF EXISTS (SELECT 1 FROM pg_stat_activity a
|
|
21
|
+
JOIN jsonb_array_elements_text(roles) r ON r.value=a.usename
|
|
22
|
+
WHERE a.datname=current_database() AND a.pid<>pg_backend_pid()) THEN
|
|
23
|
+
RAISE EXCEPTION '0461 requires drained application sessions' USING ERRCODE='55000';
|
|
24
|
+
END IF;
|
|
25
|
+
END $drain$;
|
|
26
|
+
|
|
27
|
+
-- The old toggle remains readable for historical settings bags, but is no
|
|
28
|
+
-- longer a writable policy. Full old-client bags may echo the unchanged value.
|
|
29
|
+
CREATE FUNCTION knowledge_legacy_memory_setting_retired() RETURNS trigger
|
|
30
|
+
LANGUAGE plpgsql SET search_path=pg_catalog AS $$
|
|
31
|
+
BEGIN
|
|
32
|
+
IF NEW.settings->'memoryEnabled' IS DISTINCT FROM OLD.settings->'memoryEnabled' THEN
|
|
33
|
+
RAISE EXCEPTION 'Memory settings moved to Agent learning' USING ERRCODE='0A000';
|
|
34
|
+
END IF;
|
|
35
|
+
RETURN NEW;
|
|
36
|
+
END $$;
|
|
37
|
+
CREATE TRIGGER knowledge_legacy_memory_setting_retired BEFORE UPDATE OF settings ON workspaces
|
|
38
|
+
FOR EACH ROW EXECUTE FUNCTION knowledge_legacy_memory_setting_retired();
|
|
39
|
+
|
|
40
|
+
-- Generated source sessions use the existing private-create capability and
|
|
41
|
+
-- remain bound atomically to the exact accepted scheduled execution. All other
|
|
42
|
+
-- generated-session identity checks are preserved byte for byte.
|
|
43
|
+
DO $source_session_fence$
|
|
44
|
+
DECLARE definition text; old_fragment text; new_fragment text;
|
|
45
|
+
BEGIN
|
|
46
|
+
definition:=pg_get_functiondef('fence_scheduled_task_run_connection_session_identity()'::regprocedure);
|
|
47
|
+
old_fragment:=$old$OR session_row.visibility <> 'workspace_shared'
|
|
48
|
+
OR session_row.authority_epoch <> 1
|
|
49
|
+
OR session_row.owner_organization_membership_id IS NOT NULL
|
|
50
|
+
OR session_row.owner_subject_id IS NOT NULL$old$;
|
|
51
|
+
new_fragment:=$new$OR session_row.visibility IS DISTINCT FROM (CASE
|
|
52
|
+
WHEN task_snapshot#>>'{agentConfig,knowledgeSource,destination,kind}'='personal' THEN 'user_private' ELSE 'workspace_shared' END)
|
|
53
|
+
OR session_row.authority_epoch <> 1
|
|
54
|
+
OR session_row.owner_organization_membership_id IS DISTINCT FROM (CASE
|
|
55
|
+
WHEN task_snapshot#>>'{agentConfig,knowledgeSource,destination,kind}'='personal'
|
|
56
|
+
THEN (accepted#>>'{causalHumanAuthority,organizationMembershipId}')::uuid END)
|
|
57
|
+
OR session_row.owner_subject_id IS DISTINCT FROM (CASE
|
|
58
|
+
WHEN task_snapshot#>>'{agentConfig,knowledgeSource,destination,kind}'='personal'
|
|
59
|
+
THEN accepted->>'causalHumanSubjectId' END)
|
|
60
|
+
OR (task_snapshot#>>'{agentConfig,knowledgeSource,destination,kind}'='personal' AND (
|
|
61
|
+
accepted->>'causalHumanSubjectId' IS NULL
|
|
62
|
+
OR task_snapshot#>>'{agentConfig,knowledgeSource,destination,subjectId}' IS DISTINCT FROM accepted->>'causalHumanSubjectId'
|
|
63
|
+
OR task_snapshot#>>'{agentConfig,knowledgeSource,initiatingSubjectId}' IS DISTINCT FROM accepted->>'causalHumanSubjectId'
|
|
64
|
+
OR task_snapshot#>>'{agentConfig,knowledgeSource,connection,ownerSubjectId}' IS DISTINCT FROM accepted->>'causalHumanSubjectId'
|
|
65
|
+
OR accepted#>>'{causalHumanAuthority,subjectId}' IS DISTINCT FROM accepted->>'causalHumanSubjectId'
|
|
66
|
+
OR task_snapshot->>'runMode'<>'new_session_per_run'))$new$;
|
|
67
|
+
IF strpos(definition,old_fragment)=0 THEN RAISE EXCEPTION 'Scheduled generated-session fence changed before 0461'; END IF;
|
|
68
|
+
EXECUTE replace(definition,old_fragment,new_fragment);
|
|
69
|
+
END $source_session_fence$;
|
|
70
|
+
|
|
71
|
+
DO $source_private_create$
|
|
72
|
+
DECLARE definition text; old_fragment text; new_fragment text;
|
|
73
|
+
BEGIN
|
|
74
|
+
definition:=pg_get_functiondef('guard_session_authority_write()'::regprocedure);
|
|
75
|
+
old_fragment:=$old$AND capability.actor_subject_id = NEW.created_by_subject_id
|
|
76
|
+
AND capability.owner_membership_id = NEW.owner_organization_membership_id
|
|
77
|
+
AND NEW.created_by_kind = 'subject'$old$;
|
|
78
|
+
new_fragment:=$new$AND capability.owner_membership_id = NEW.owner_organization_membership_id
|
|
79
|
+
AND ((NEW.created_by_kind='subject' AND capability.actor_subject_id=NEW.created_by_subject_id)
|
|
80
|
+
OR (NEW.created_by_kind='service' AND NEW.created_by_subject_id='scheduler' AND EXISTS (
|
|
81
|
+
SELECT 1 FROM scheduled_task_runs run WHERE run.account_id=NEW.account_id
|
|
82
|
+
AND run.workspace_id=NEW.workspace_id AND run.id::text=NEW.metadata->>'scheduledTaskRunId'
|
|
83
|
+
AND run.action_kind='agent_turn' AND run.session_id IS NULL AND run.status IN ('queued','dispatched')
|
|
84
|
+
AND run.accepted_execution_snapshot#>>'{generatedSessionBinding,createIdempotencyKey}'=NEW.create_idempotency_key
|
|
85
|
+
AND run.accepted_execution_snapshot->>'causalHumanSubjectId'=capability.actor_subject_id
|
|
86
|
+
AND run.accepted_execution_snapshot#>>'{causalHumanAuthority,subjectId}'=capability.actor_subject_id
|
|
87
|
+
AND run.accepted_execution_snapshot#>>'{causalHumanAuthority,organizationMembershipId}'=capability.owner_membership_id::text
|
|
88
|
+
AND run.accepted_execution_snapshot#>>'{task,runMode}'='new_session_per_run'
|
|
89
|
+
AND run.accepted_execution_snapshot#>>'{task,agentConfig,knowledgeSource,destination,kind}'='personal'
|
|
90
|
+
AND run.accepted_execution_snapshot#>>'{task,agentConfig,knowledgeSource,destination,subjectId}'=capability.actor_subject_id
|
|
91
|
+
AND run.accepted_execution_snapshot#>>'{task,agentConfig,knowledgeSource,connection,ownerSubjectId}'=capability.actor_subject_id
|
|
92
|
+
AND run.accepted_execution_snapshot#>>'{task,agentConfig,knowledgeSource,initiatingSubjectId}'=capability.actor_subject_id
|
|
93
|
+
)))$new$;
|
|
94
|
+
IF strpos(definition,old_fragment)=0 THEN RAISE EXCEPTION 'Private-create authority fence changed before 0461'; END IF;
|
|
95
|
+
EXECUTE replace(definition,old_fragment,new_fragment);
|
|
96
|
+
END $source_private_create$;
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
-- Original chat attachments have an explicit owner before any byte upload.
|
|
100
|
+
-- Shared files retain the existing workspace policy. Personal files additionally
|
|
101
|
+
-- require a host-verified owner scope; a workspace or service subject is not one.
|
|
102
|
+
ALTER TABLE files ADD COLUMN private_owner_subject_ids text[] DEFAULT
|
|
103
|
+
CASE WHEN nullif(current_setting('opengeni.private_file_owner',true),'') IS NULL THEN NULL
|
|
104
|
+
ELSE ARRAY[current_setting('opengeni.private_file_owner',true)] END
|
|
105
|
+
CHECK(private_owner_subject_ids IS NULL OR (cardinality(private_owner_subject_ids)>0 AND array_position(private_owner_subject_ids,NULL) IS NULL));
|
|
106
|
+
-- Derive old original ownership from typed resource authority, never prose,
|
|
107
|
+
-- filenames, or the uploading service's subject. A file that was explicitly
|
|
108
|
+
-- shared as well as used privately keeps that existing shared authority.
|
|
109
|
+
ALTER TABLE files NO FORCE ROW LEVEL SECURITY;
|
|
110
|
+
ALTER TABLE documents NO FORCE ROW LEVEL SECURITY;
|
|
111
|
+
ALTER TABLE sessions NO FORCE ROW LEVEL SECURITY;
|
|
112
|
+
ALTER TABLE session_turns NO FORCE ROW LEVEL SECURITY;
|
|
113
|
+
ALTER TABLE generated_image_artifacts NO FORCE ROW LEVEL SECURITY;
|
|
114
|
+
ALTER TABLE generated_video_artifacts NO FORCE ROW LEVEL SECURITY;
|
|
115
|
+
ALTER TABLE retained_screenshot_artifacts NO FORCE ROW LEVEL SECURITY;
|
|
116
|
+
ALTER TABLE organization_memberships NO FORCE ROW LEVEL SECURITY;
|
|
117
|
+
CREATE TEMP TABLE knowledge_session_file_owners_0461 ON COMMIT DROP AS
|
|
118
|
+
|
|
119
|
+
SELECT s.id,s.account_id,s.resources,CASE
|
|
120
|
+
WHEN s.visibility='user_private' THEN s.owner_subject_id
|
|
121
|
+
WHEN s.memory_scope='user' THEN s.scope_subject_id
|
|
122
|
+
ELSE personal.subject_id END AS owner
|
|
123
|
+
FROM sessions s LEFT JOIN organization_memberships personal
|
|
124
|
+
ON personal.account_id=s.account_id AND personal.personal_workspace_id=s.workspace_id
|
|
125
|
+
;
|
|
126
|
+
CREATE TEMP TABLE knowledge_file_owners_0461 ON COMMIT DROP AS
|
|
127
|
+
WITH session_owners AS (SELECT * FROM knowledge_session_file_owners_0461), original_references AS (
|
|
128
|
+
SELECT d.file_id,d.account_id,
|
|
129
|
+
CASE WHEN d.authority_kind='personal' THEN d.authority_subject_id END AS owner
|
|
130
|
+
FROM documents d WHERE d.file_id IS NOT NULL
|
|
131
|
+
UNION ALL
|
|
132
|
+
SELECT (r->>'fileId')::uuid,s.account_id,s.owner FROM session_owners s
|
|
133
|
+
CROSS JOIN LATERAL jsonb_array_elements(s.resources) r
|
|
134
|
+
WHERE r->>'kind'='file' AND r->>'fileId' ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
|
135
|
+
UNION ALL
|
|
136
|
+
SELECT (r->>'fileId')::uuid,s.account_id,s.owner FROM session_owners s
|
|
137
|
+
JOIN session_turns t ON t.session_id=s.id AND t.account_id=s.account_id
|
|
138
|
+
CROSS JOIN LATERAL jsonb_array_elements(t.resources) r
|
|
139
|
+
WHERE r->>'kind'='file' AND r->>'fileId' ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
|
140
|
+
UNION ALL
|
|
141
|
+
SELECT a.artifact_id,a.account_id,s.owner FROM generated_image_artifacts a
|
|
142
|
+
JOIN session_owners s ON s.id=a.session_id AND s.account_id=a.account_id
|
|
143
|
+
UNION ALL
|
|
144
|
+
SELECT a.primary_file_id,a.account_id,s.owner FROM generated_video_artifacts a
|
|
145
|
+
JOIN session_owners s ON s.id=a.session_id AND s.account_id=a.account_id
|
|
146
|
+
UNION ALL
|
|
147
|
+
SELECT a.artifact_id,a.account_id,s.owner FROM retained_screenshot_artifacts a
|
|
148
|
+
JOIN session_owners s ON s.id=a.session_id AND s.account_id=a.account_id
|
|
149
|
+
)
|
|
150
|
+
SELECT file_id,account_id,array_agg(DISTINCT owner ORDER BY owner) AS owners FROM original_references
|
|
151
|
+
GROUP BY file_id,account_id
|
|
152
|
+
HAVING bool_and(owner IS NOT NULL);
|
|
153
|
+
UPDATE files f SET private_owner_subject_ids=ownership.owners
|
|
154
|
+
FROM knowledge_file_owners_0461 ownership WHERE f.id=ownership.file_id AND f.account_id=ownership.account_id;
|
|
155
|
+
-- Async upload/media settlement and cleanup retain the accepted file owner
|
|
156
|
+
-- even after the source session/turn is removed. This is operation provenance,
|
|
157
|
+
-- never a grant for a user or agent to read another person's originals.
|
|
158
|
+
ALTER TABLE file_uploads NO FORCE ROW LEVEL SECURITY;
|
|
159
|
+
ALTER TABLE file_uploads ADD COLUMN private_file_owner_subject_id text
|
|
160
|
+
DEFAULT nullif(current_setting('opengeni.private_file_owner',true),'');
|
|
161
|
+
UPDATE file_uploads u SET private_file_owner_subject_id=f.private_owner_subject_ids[1]
|
|
162
|
+
FROM files f WHERE u.file_id=f.id AND u.account_id=f.account_id;
|
|
163
|
+
ALTER TABLE file_uploads FORCE ROW LEVEL SECURITY;
|
|
164
|
+
ALTER TABLE video_generation_operations NO FORCE ROW LEVEL SECURITY;
|
|
165
|
+
ALTER TABLE video_generation_operations ADD COLUMN private_file_owner_subject_id text
|
|
166
|
+
DEFAULT nullif(current_setting('opengeni.private_file_owner',true),'');
|
|
167
|
+
ALTER TABLE retained_screenshot_artifacts ADD COLUMN private_file_owner_subject_id text
|
|
168
|
+
DEFAULT nullif(current_setting('opengeni.private_file_owner',true),'');
|
|
169
|
+
UPDATE video_generation_operations o SET private_file_owner_subject_id=s.owner
|
|
170
|
+
FROM knowledge_session_file_owners_0461 s WHERE o.session_id=s.id AND o.account_id=s.account_id;
|
|
171
|
+
UPDATE retained_screenshot_artifacts a SET private_file_owner_subject_id=s.owner
|
|
172
|
+
FROM knowledge_session_file_owners_0461 s WHERE a.session_id=s.id AND a.account_id=s.account_id;
|
|
173
|
+
ALTER TABLE video_generation_operations FORCE ROW LEVEL SECURITY;
|
|
174
|
+
CREATE FUNCTION knowledge_media_file_owner_immutable() RETURNS trigger
|
|
175
|
+
LANGUAGE plpgsql SET search_path=pg_catalog AS $$
|
|
176
|
+
BEGIN
|
|
177
|
+
IF NEW.private_file_owner_subject_id IS DISTINCT FROM OLD.private_file_owner_subject_id THEN
|
|
178
|
+
RAISE EXCEPTION 'An operation retains its accepted original-file owner' USING ERRCODE='55000';
|
|
179
|
+
END IF;
|
|
180
|
+
RETURN NEW;
|
|
181
|
+
END $$;
|
|
182
|
+
CREATE TRIGGER file_upload_owner_immutable BEFORE UPDATE ON file_uploads
|
|
183
|
+
FOR EACH ROW EXECUTE FUNCTION knowledge_media_file_owner_immutable();
|
|
184
|
+
CREATE TRIGGER video_generation_file_owner_immutable BEFORE UPDATE ON video_generation_operations
|
|
185
|
+
FOR EACH ROW EXECUTE FUNCTION knowledge_media_file_owner_immutable();
|
|
186
|
+
CREATE TRIGGER retained_screenshot_file_owner_immutable BEFORE UPDATE ON retained_screenshot_artifacts
|
|
187
|
+
FOR EACH ROW EXECUTE FUNCTION knowledge_media_file_owner_immutable();
|
|
188
|
+
ALTER TABLE organization_memberships FORCE ROW LEVEL SECURITY;
|
|
189
|
+
ALTER TABLE retained_screenshot_artifacts FORCE ROW LEVEL SECURITY;
|
|
190
|
+
ALTER TABLE generated_video_artifacts FORCE ROW LEVEL SECURITY;
|
|
191
|
+
ALTER TABLE generated_image_artifacts FORCE ROW LEVEL SECURITY;
|
|
192
|
+
ALTER TABLE session_turns FORCE ROW LEVEL SECURITY;
|
|
193
|
+
ALTER TABLE sessions FORCE ROW LEVEL SECURITY;
|
|
194
|
+
ALTER TABLE documents FORCE ROW LEVEL SECURITY;
|
|
195
|
+
ALTER TABLE files FORCE ROW LEVEL SECURITY;
|
|
196
|
+
|
|
197
|
+
CREATE POLICY files_personal_owner ON files AS RESTRICTIVE FOR ALL
|
|
198
|
+
USING(private_owner_subject_ids IS NULL OR nullif(current_setting('opengeni.private_file_owner',true),'')=ANY(private_owner_subject_ids))
|
|
199
|
+
WITH CHECK(private_owner_subject_ids IS NULL OR nullif(current_setting('opengeni.private_file_owner',true),'')=ANY(private_owner_subject_ids));
|
|
200
|
+
-- A personal original belongs to its organization/user. Its origin workspace
|
|
201
|
+
-- is provenance, while shared files still cascade with their owning workspace.
|
|
202
|
+
ALTER TABLE files DROP CONSTRAINT files_workspace_id_fkey;
|
|
203
|
+
ALTER TABLE files DROP CONSTRAINT files_workspace_account_fk;
|
|
204
|
+
ALTER TABLE files ADD COLUMN retention_workspace_id uuid GENERATED ALWAYS AS
|
|
205
|
+
(CASE WHEN private_owner_subject_ids IS NULL THEN workspace_id END) STORED;
|
|
206
|
+
ALTER TABLE files ADD CONSTRAINT files_retention_workspace_account_fk FOREIGN KEY(retention_workspace_id,account_id)
|
|
207
|
+
REFERENCES workspaces(id,account_id) ON DELETE CASCADE;
|
|
208
|
+
CREATE POLICY files_personal_read ON files FOR SELECT USING (
|
|
209
|
+
account_id=nullif(current_setting('opengeni.account_id',true),'')::uuid
|
|
210
|
+
AND nullif(current_setting('opengeni.private_file_owner',true),'')=ANY(private_owner_subject_ids));
|
|
211
|
+
CREATE FUNCTION knowledge_file_owner_immutable() RETURNS trigger
|
|
212
|
+
LANGUAGE plpgsql SET search_path=pg_catalog AS $$
|
|
213
|
+
BEGIN
|
|
214
|
+
IF NEW.private_owner_subject_ids IS DISTINCT FROM OLD.private_owner_subject_ids
|
|
215
|
+
OR NEW.workspace_id IS DISTINCT FROM OLD.workspace_id OR NEW.account_id IS DISTINCT FROM OLD.account_id THEN
|
|
216
|
+
RAISE EXCEPTION 'File ownership is immutable; share an explicitly authorized copy' USING ERRCODE='55000';
|
|
217
|
+
END IF;
|
|
218
|
+
RETURN NEW;
|
|
219
|
+
END $$;
|
|
220
|
+
CREATE TRIGGER files_personal_owner_immutable BEFORE UPDATE ON files
|
|
221
|
+
FOR EACH ROW EXECUTE FUNCTION knowledge_file_owner_immutable();
|
|
222
|
+
|
|
223
|
+
-- Defaults and context overrides are revisions of one policy authority. Empty
|
|
224
|
+
-- overrides mean inheritance. Personal defaults are organization-user owned;
|
|
225
|
+
-- origin_workspace_id records provenance, not a cross-workspace access grant.
|
|
226
|
+
-- Provenance UUIDs deliberately have no workspace foreign key: deleting an
|
|
227
|
+
-- originating workspace must not erase personal/organization retained content
|
|
228
|
+
-- or its receipts. Only actual ownership columns cascade workspace deletion.
|
|
229
|
+
CREATE TABLE agent_learning_revisions (
|
|
230
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
231
|
+
account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE CASCADE,
|
|
232
|
+
origin_workspace_id uuid NOT NULL,
|
|
233
|
+
owner_key text NOT NULL,
|
|
234
|
+
owner_workspace_id uuid GENERATED ALWAYS AS
|
|
235
|
+
(CASE WHEN subject_id IS NULL THEN origin_workspace_id END) STORED,
|
|
236
|
+
subject_id text,
|
|
237
|
+
context_key text NOT NULL DEFAULT 'defaults',
|
|
238
|
+
version integer NOT NULL CHECK (version>0),
|
|
239
|
+
settings jsonb NOT NULL CHECK (jsonb_typeof(settings)='object'),
|
|
240
|
+
operation_id uuid NOT NULL,
|
|
241
|
+
request_hash text NOT NULL,
|
|
242
|
+
actor_subject_id text NOT NULL,
|
|
243
|
+
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
244
|
+
UNIQUE(account_id,owner_key,context_key,version),
|
|
245
|
+
UNIQUE(account_id,operation_id),
|
|
246
|
+
FOREIGN KEY(owner_workspace_id,account_id) REFERENCES workspaces(id,account_id) ON DELETE CASCADE,
|
|
247
|
+
CHECK ((subject_id IS NULL AND owner_key='workspace:'||origin_workspace_id::text)
|
|
248
|
+
OR (length(subject_id) BETWEEN 1 AND 1024 AND owner_key='personal:'||subject_id))
|
|
249
|
+
);
|
|
250
|
+
CREATE INDEX agent_learning_revisions_current_idx ON agent_learning_revisions
|
|
251
|
+
(account_id,owner_key,context_key,version DESC);
|
|
252
|
+
|
|
253
|
+
CREATE TABLE agent_learning_snapshots (
|
|
254
|
+
account_id uuid NOT NULL,
|
|
255
|
+
workspace_id uuid NOT NULL,
|
|
256
|
+
session_id uuid NOT NULL,
|
|
257
|
+
turn_id uuid NOT NULL,
|
|
258
|
+
snapshot jsonb NOT NULL,
|
|
259
|
+
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
260
|
+
PRIMARY KEY(account_id,turn_id),
|
|
261
|
+
FOREIGN KEY(workspace_id,account_id) REFERENCES workspaces(id,account_id) ON DELETE CASCADE,
|
|
262
|
+
FOREIGN KEY(workspace_id,turn_id) REFERENCES session_turns(workspace_id,id) ON DELETE CASCADE
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
CREATE TABLE knowledge_entries (
|
|
266
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
267
|
+
account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE CASCADE,
|
|
268
|
+
origin_workspace_id uuid NOT NULL,
|
|
269
|
+
scope text NOT NULL CHECK(scope IN ('organization','workspace','personal')),
|
|
270
|
+
scope_workspace_id uuid,
|
|
271
|
+
scope_subject_id text,
|
|
272
|
+
version integer NOT NULL DEFAULT 0 CHECK(version>=0),
|
|
273
|
+
published_revision_id uuid,
|
|
274
|
+
latest_revision_id uuid,
|
|
275
|
+
archived boolean NOT NULL DEFAULT false,
|
|
276
|
+
-- A migrated scope is an additional restriction, never a broader owner.
|
|
277
|
+
legacy_memory_id uuid UNIQUE,
|
|
278
|
+
legacy_document_id uuid UNIQUE,
|
|
279
|
+
legacy_claim_id uuid UNIQUE,
|
|
280
|
+
prepared_file_id uuid,
|
|
281
|
+
document_preparation jsonb,
|
|
282
|
+
legacy_document_version_id uuid UNIQUE,
|
|
283
|
+
legacy_scope_workspace_id uuid,
|
|
284
|
+
access_document_id uuid,
|
|
285
|
+
access_file_id uuid,
|
|
286
|
+
legacy_scope_type text,
|
|
287
|
+
legacy_scope_role_key text,
|
|
288
|
+
legacy_scope_session_id uuid,
|
|
289
|
+
valid_from timestamptz NOT NULL DEFAULT now(),
|
|
290
|
+
valid_until timestamptz,
|
|
291
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
292
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
293
|
+
UNIQUE(account_id,id),
|
|
294
|
+
FOREIGN KEY(scope_workspace_id,account_id) REFERENCES workspaces(id,account_id) ON DELETE CASCADE,
|
|
295
|
+
CHECK ((scope='organization' AND scope_workspace_id IS NULL AND scope_subject_id IS NULL)
|
|
296
|
+
OR (scope='workspace' AND scope_workspace_id IS NOT NULL AND scope_subject_id IS NULL)
|
|
297
|
+
OR (scope='personal' AND scope_workspace_id IS NULL AND length(scope_subject_id) BETWEEN 1 AND 1024))
|
|
298
|
+
);
|
|
299
|
+
CREATE INDEX knowledge_entries_scope_idx ON knowledge_entries(account_id,scope,scope_workspace_id,scope_subject_id,id);
|
|
300
|
+
|
|
301
|
+
CREATE TABLE knowledge_review_batches (
|
|
302
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
303
|
+
account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE CASCADE,
|
|
304
|
+
origin_workspace_id uuid NOT NULL,
|
|
305
|
+
owner_key text NOT NULL,
|
|
306
|
+
session_id uuid,
|
|
307
|
+
turn_id uuid,
|
|
308
|
+
scheduled_task_run_id uuid,
|
|
309
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
310
|
+
UNIQUE(account_id,id),
|
|
311
|
+
UNIQUE(account_id,owner_key,turn_id)
|
|
312
|
+
);
|
|
313
|
+
CREATE UNIQUE INDEX knowledge_review_batches_run_idx ON knowledge_review_batches
|
|
314
|
+
(account_id,owner_key,scheduled_task_run_id) WHERE scheduled_task_run_id IS NOT NULL;
|
|
315
|
+
|
|
316
|
+
CREATE TABLE knowledge_entry_revisions (
|
|
317
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
318
|
+
account_id uuid NOT NULL,
|
|
319
|
+
entry_id uuid NOT NULL,
|
|
320
|
+
number integer NOT NULL CHECK(number>0),
|
|
321
|
+
change_kind text NOT NULL DEFAULT 'upsert' CHECK(change_kind IN ('upsert','archive')),
|
|
322
|
+
body jsonb NOT NULL CHECK(jsonb_typeof(body)='object'),
|
|
323
|
+
body_codec_version integer,
|
|
324
|
+
legacy_snapshot jsonb,
|
|
325
|
+
preview text NOT NULL DEFAULT '',
|
|
326
|
+
preview_codec_version integer,
|
|
327
|
+
previous_revision_id uuid,
|
|
328
|
+
restored_from_revision_id uuid,
|
|
329
|
+
actor jsonb NOT NULL,
|
|
330
|
+
created_by_session_id uuid,
|
|
331
|
+
created_by_turn_id uuid,
|
|
332
|
+
review_batch_id uuid,
|
|
333
|
+
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
334
|
+
UNIQUE(account_id,entry_id,id),
|
|
335
|
+
UNIQUE(account_id,entry_id,number),
|
|
336
|
+
FOREIGN KEY(account_id,entry_id) REFERENCES knowledge_entries(account_id,id) ON DELETE CASCADE,
|
|
337
|
+
FOREIGN KEY(account_id,entry_id,previous_revision_id) REFERENCES knowledge_entry_revisions(account_id,entry_id,id),
|
|
338
|
+
FOREIGN KEY(account_id,entry_id,restored_from_revision_id) REFERENCES knowledge_entry_revisions(account_id,entry_id,id),
|
|
339
|
+
FOREIGN KEY(account_id,review_batch_id) REFERENCES knowledge_review_batches(account_id,id),
|
|
340
|
+
CHECK(body_codec_version IS NULL OR body_codec_version=1)
|
|
341
|
+
);
|
|
342
|
+
ALTER TABLE knowledge_entries ADD CONSTRAINT knowledge_entries_published_fk
|
|
343
|
+
FOREIGN KEY(account_id,id,published_revision_id) REFERENCES knowledge_entry_revisions(account_id,entry_id,id)
|
|
344
|
+
DEFERRABLE INITIALLY DEFERRED;
|
|
345
|
+
ALTER TABLE knowledge_entries ADD CONSTRAINT knowledge_entries_latest_fk
|
|
346
|
+
FOREIGN KEY(account_id,id,latest_revision_id) REFERENCES knowledge_entry_revisions(account_id,entry_id,id)
|
|
347
|
+
DEFERRABLE INITIALLY DEFERRED;
|
|
348
|
+
|
|
349
|
+
-- Relational indexes of each immutable revision. Labels, quotes and locations
|
|
350
|
+
-- live once in its exact body. Membership does not copy or authorize content.
|
|
351
|
+
CREATE TABLE knowledge_entry_links (
|
|
352
|
+
account_id uuid NOT NULL,
|
|
353
|
+
entry_id uuid NOT NULL,
|
|
354
|
+
revision_id uuid NOT NULL,
|
|
355
|
+
ordinal integer NOT NULL CHECK(ordinal>=0),
|
|
356
|
+
target_entry_id uuid NOT NULL,
|
|
357
|
+
target_revision_id uuid,
|
|
358
|
+
relation text NOT NULL CHECK(relation IN
|
|
359
|
+
('evidence','group','related_to','depends_on','applies_to','conflicts_with','supersedes')),
|
|
360
|
+
PRIMARY KEY(revision_id,ordinal),
|
|
361
|
+
FOREIGN KEY(account_id,entry_id,revision_id) REFERENCES knowledge_entry_revisions(account_id,entry_id,id) ON DELETE CASCADE,
|
|
362
|
+
FOREIGN KEY(account_id,target_entry_id) REFERENCES knowledge_entries(account_id,id),
|
|
363
|
+
FOREIGN KEY(account_id,target_entry_id,target_revision_id) REFERENCES knowledge_entry_revisions(account_id,entry_id,id),
|
|
364
|
+
CHECK(entry_id<>target_entry_id),
|
|
365
|
+
CHECK((relation='evidence')=(target_revision_id IS NOT NULL))
|
|
366
|
+
);
|
|
367
|
+
CREATE INDEX knowledge_entry_links_target_idx ON knowledge_entry_links(account_id,target_entry_id,relation,revision_id);
|
|
368
|
+
|
|
369
|
+
CREATE TABLE knowledge_entry_decisions (
|
|
370
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
371
|
+
account_id uuid NOT NULL,
|
|
372
|
+
entry_id uuid NOT NULL,
|
|
373
|
+
revision_id uuid NOT NULL,
|
|
374
|
+
version integer NOT NULL CHECK(version>0),
|
|
375
|
+
outcome text NOT NULL CHECK(outcome IN ('published','pending','rejected','archived','restored')),
|
|
376
|
+
actor jsonb NOT NULL,
|
|
377
|
+
policy_snapshot jsonb,
|
|
378
|
+
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
379
|
+
UNIQUE(account_id,entry_id,version),
|
|
380
|
+
FOREIGN KEY(account_id,entry_id,revision_id) REFERENCES knowledge_entry_revisions(account_id,entry_id,id) ON DELETE CASCADE
|
|
381
|
+
);
|
|
382
|
+
CREATE TABLE knowledge_entry_operations (
|
|
383
|
+
account_id uuid NOT NULL,
|
|
384
|
+
origin_workspace_id uuid NOT NULL,
|
|
385
|
+
operation_id uuid NOT NULL,
|
|
386
|
+
entry_id uuid NOT NULL,
|
|
387
|
+
actor jsonb NOT NULL,
|
|
388
|
+
request_hash text NOT NULL,
|
|
389
|
+
receipt jsonb NOT NULL,
|
|
390
|
+
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
391
|
+
PRIMARY KEY(account_id,operation_id),
|
|
392
|
+
FOREIGN KEY(account_id,entry_id) REFERENCES knowledge_entries(account_id,id) ON DELETE CASCADE
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
-- Rebuildable search projection. It never owns retained content or publication.
|
|
396
|
+
-- Instruction content remains in its native revision/head lifecycle. This
|
|
397
|
+
-- append-only ledger owns operation replay and review decisions only.
|
|
398
|
+
CREATE TABLE agent_instruction_operations (
|
|
399
|
+
account_id uuid NOT NULL,
|
|
400
|
+
origin_workspace_id uuid NOT NULL,
|
|
401
|
+
operation_id uuid NOT NULL,
|
|
402
|
+
revision_id uuid NOT NULL REFERENCES workspace_instruction_policy_revisions(id) ON DELETE CASCADE,
|
|
403
|
+
request_hash text NOT NULL,
|
|
404
|
+
actor jsonb NOT NULL,
|
|
405
|
+
receipt jsonb NOT NULL,
|
|
406
|
+
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
407
|
+
PRIMARY KEY(account_id,operation_id),
|
|
408
|
+
FOREIGN KEY(origin_workspace_id,account_id) REFERENCES workspaces(id,account_id) ON DELETE CASCADE
|
|
409
|
+
);
|
|
410
|
+
ALTER TABLE workspace_instruction_policy_revisions ADD COLUMN agent_learning_context jsonb;
|
|
411
|
+
ALTER TABLE workspace_instruction_policy_revisions DROP CONSTRAINT workspace_instruction_policy_revisions_provenance_chk;
|
|
412
|
+
ALTER TABLE workspace_instruction_policy_revisions ADD CONSTRAINT workspace_instruction_policy_revisions_provenance_chk
|
|
413
|
+
CHECK(provenance_source IN ('human','onboarding','knowledge_proposal','legacy_import','agent_learning')
|
|
414
|
+
AND (provenance_source_id IS NULL OR length(provenance_source_id) BETWEEN 1 AND 512)
|
|
415
|
+
AND ((provenance_source='agent_learning')=(agent_learning_context IS NOT NULL)));
|
|
416
|
+
|
|
417
|
+
-- Search caches are projections, never a content or publication authority.
|
|
418
|
+
CREATE TABLE knowledge_index_jobs (
|
|
419
|
+
account_id uuid NOT NULL,
|
|
420
|
+
entry_id uuid NOT NULL,
|
|
421
|
+
revision_id uuid PRIMARY KEY,
|
|
422
|
+
state text NOT NULL DEFAULT 'pending' CHECK(state IN ('pending','running','ready','obsolete')),
|
|
423
|
+
model text,
|
|
424
|
+
dimensions integer,
|
|
425
|
+
generation integer NOT NULL DEFAULT 0,
|
|
426
|
+
completed_generation integer,
|
|
427
|
+
next_index integer NOT NULL DEFAULT 0,
|
|
428
|
+
lease_id uuid,
|
|
429
|
+
lease_until timestamptz,
|
|
430
|
+
attempts integer NOT NULL DEFAULT 0,
|
|
431
|
+
next_attempt_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
432
|
+
last_failure text,
|
|
433
|
+
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
434
|
+
FOREIGN KEY(account_id,entry_id,revision_id) REFERENCES knowledge_entry_revisions(account_id,entry_id,id) ON DELETE CASCADE
|
|
435
|
+
);
|
|
436
|
+
CREATE INDEX knowledge_index_jobs_due ON knowledge_index_jobs(next_attempt_at,revision_id) WHERE state<>'obsolete';
|
|
437
|
+
CREATE TABLE knowledge_entry_vectors (
|
|
438
|
+
account_id uuid NOT NULL,
|
|
439
|
+
entry_id uuid NOT NULL,
|
|
440
|
+
revision_id uuid NOT NULL,
|
|
441
|
+
generation integer NOT NULL,
|
|
442
|
+
chunk_index integer NOT NULL,
|
|
443
|
+
model text NOT NULL,
|
|
444
|
+
dimensions integer NOT NULL CHECK(dimensions BETWEEN 1 AND 4096),
|
|
445
|
+
field text NOT NULL CHECK(field IN ('title','content')),
|
|
446
|
+
start_offset integer NOT NULL CHECK(start_offset>=0),
|
|
447
|
+
end_offset integer NOT NULL CHECK(end_offset>=start_offset),
|
|
448
|
+
text text NOT NULL,
|
|
449
|
+
text_codec_version integer NOT NULL CHECK(text_codec_version=1),
|
|
450
|
+
embedding vector NOT NULL,
|
|
451
|
+
PRIMARY KEY(account_id,entry_id,revision_id,generation,chunk_index),
|
|
452
|
+
FOREIGN KEY(account_id,entry_id,revision_id) REFERENCES knowledge_entry_revisions(account_id,entry_id,id) ON DELETE CASCADE,
|
|
453
|
+
CHECK(vector_dims(embedding)=dimensions AND vector_norm(embedding)>0)
|
|
454
|
+
);
|
|
455
|
+
CREATE FUNCTION knowledge_enqueue_index() RETURNS trigger
|
|
456
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT AS $$
|
|
457
|
+
BEGIN
|
|
458
|
+
IF NEW.change_kind<>'archive' THEN
|
|
459
|
+
INSERT INTO knowledge_index_jobs(account_id,entry_id,revision_id) VALUES(NEW.account_id,NEW.entry_id,NEW.id);
|
|
460
|
+
END IF;
|
|
461
|
+
RETURN NEW;
|
|
462
|
+
END $$;
|
|
463
|
+
CREATE TRIGGER knowledge_revision_index_queue AFTER INSERT ON knowledge_entry_revisions
|
|
464
|
+
FOR EACH ROW EXECUTE FUNCTION knowledge_enqueue_index();
|
|
465
|
+
-- Only the owner-definer dispatcher can use this scope. Setting the GUC on an
|
|
466
|
+
-- application connection cannot reveal jobs or turn it into a content reader.
|
|
467
|
+
CREATE POLICY knowledge_index_dispatcher ON knowledge_index_jobs USING (
|
|
468
|
+
current_user=pg_get_userbyid((SELECT relowner FROM pg_class WHERE oid='knowledge_index_jobs'::regclass))
|
|
469
|
+
AND current_setting('opengeni.knowledge_index_dispatcher',true)='1');
|
|
470
|
+
CREATE FUNCTION knowledge_index_claim(p_model text,p_dimensions integer,p_limit integer)
|
|
471
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT AS $$
|
|
472
|
+
DECLARE previous text:=current_setting('opengeni.knowledge_index_dispatcher',true); result jsonb;
|
|
473
|
+
BEGIN
|
|
474
|
+
IF p_model IS NULL OR p_dimensions IS NULL OR p_limit IS NULL OR length(p_model) NOT BETWEEN 1 AND 512 OR p_dimensions NOT BETWEEN 1 AND 4096 OR p_limit NOT BETWEEN 1 AND 20 THEN
|
|
475
|
+
RAISE EXCEPTION 'Invalid Knowledge indexing claim' USING ERRCODE='22023'; END IF;
|
|
476
|
+
PERFORM set_config('opengeni.knowledge_index_dispatcher','1',true);
|
|
477
|
+
WITH candidates AS (
|
|
478
|
+
SELECT j.revision_id FROM knowledge_index_jobs j WHERE j.state<>'obsolete' AND j.next_attempt_at<=clock_timestamp()
|
|
479
|
+
AND (j.lease_until IS NULL OR j.lease_until<=clock_timestamp())
|
|
480
|
+
AND (j.state<>'ready' OR j.model IS DISTINCT FROM p_model OR j.dimensions IS DISTINCT FROM p_dimensions)
|
|
481
|
+
ORDER BY j.next_attempt_at,j.revision_id LIMIT p_limit FOR UPDATE SKIP LOCKED
|
|
482
|
+
), claimed AS (
|
|
483
|
+
UPDATE knowledge_index_jobs j SET state='running',lease_id=gen_random_uuid(),lease_until=clock_timestamp()+interval '5 minutes',
|
|
484
|
+
generation=CASE WHEN j.model IS DISTINCT FROM p_model OR j.dimensions IS DISTINCT FROM p_dimensions THEN j.generation+1 ELSE j.generation END,
|
|
485
|
+
next_index=CASE WHEN j.model IS DISTINCT FROM p_model OR j.dimensions IS DISTINCT FROM p_dimensions THEN 0 ELSE j.next_index END,
|
|
486
|
+
model=p_model,dimensions=p_dimensions,attempts=j.attempts+1
|
|
487
|
+
FROM candidates c WHERE j.revision_id=c.revision_id RETURNING j.*
|
|
488
|
+
) SELECT coalesce(jsonb_agg(jsonb_build_object('accountId',account_id,'entryId',entry_id,'revisionId',revision_id,
|
|
489
|
+
'leaseId',lease_id,'model',model,'dimensions',dimensions,'generation',generation,'nextIndex',next_index)),'[]'::jsonb) INTO result FROM claimed;
|
|
490
|
+
PERFORM set_config('opengeni.knowledge_index_dispatcher',coalesce(previous,''),true);
|
|
491
|
+
RETURN result;
|
|
492
|
+
EXCEPTION WHEN OTHERS THEN
|
|
493
|
+
PERFORM set_config('opengeni.knowledge_index_dispatcher',coalesce(previous,''),true);
|
|
494
|
+
RAISE;
|
|
495
|
+
END $$;
|
|
496
|
+
-- Projection processing is a lease capability, never an impersonated human or
|
|
497
|
+
-- an agent attempt. It may process retained bytes but cannot publish revisions.
|
|
498
|
+
-- Recheck source lifecycle for the entry and its pinned evidence before every
|
|
499
|
+
-- provider batch and cache commit; retrieval still applies the requesting
|
|
500
|
+
-- subject's current ACL independently.
|
|
501
|
+
CREATE FUNCTION knowledge_index_retention_active(p_account uuid,p_entry uuid,p_revision uuid,p_preparing_document uuid DEFAULT NULL)
|
|
502
|
+
RETURNS boolean LANGUAGE plpgsql VOLATILE SET search_path FROM CURRENT AS $$
|
|
503
|
+
DECLARE dep record; document_id uuid; file_id uuid; file_subject text; allowed boolean:=true;
|
|
504
|
+
previous_workspace text:=current_setting('opengeni.workspace_id',true);
|
|
505
|
+
previous_subject text:=current_setting('opengeni.subject_id',true);
|
|
506
|
+
previous_owner text:=current_setting('opengeni.private_file_owner',true);
|
|
507
|
+
BEGIN
|
|
508
|
+
FOR dep IN WITH RECURSIVE dependencies(entry_id,revision_id) AS (
|
|
509
|
+
SELECT p_entry,p_revision UNION
|
|
510
|
+
SELECT l.target_entry_id,l.target_revision_id FROM dependencies d JOIN knowledge_entry_links l
|
|
511
|
+
ON l.account_id=p_account AND l.entry_id=d.entry_id AND l.revision_id=d.revision_id AND l.relation='evidence'
|
|
512
|
+
) SELECT e.*,r.body,r.id AS revision_id FROM dependencies d
|
|
513
|
+
LEFT JOIN knowledge_entries e ON e.account_id=p_account AND e.id=d.entry_id
|
|
514
|
+
LEFT JOIN knowledge_entry_revisions r ON r.account_id=p_account AND r.entry_id=d.entry_id AND r.id=d.revision_id
|
|
515
|
+
LOOP
|
|
516
|
+
IF dep.id IS NULL OR dep.revision_id IS NULL OR dep.archived
|
|
517
|
+
OR dep.valid_from>clock_timestamp() OR dep.valid_until<=clock_timestamp() THEN allowed:=false; EXIT; END IF;
|
|
518
|
+
PERFORM set_config('opengeni.workspace_id',coalesce(dep.scope_workspace_id,dep.origin_workspace_id)::text,true);
|
|
519
|
+
PERFORM set_config('opengeni.subject_id',coalesce(dep.scope_subject_id,''),true);
|
|
520
|
+
PERFORM set_config('opengeni.private_file_owner',coalesce(dep.scope_subject_id,''),true);
|
|
521
|
+
IF dep.legacy_document_version_id IS NOT NULL AND NOT EXISTS (
|
|
522
|
+
SELECT 1 FROM knowledge_document_versions v
|
|
523
|
+
JOIN knowledge_source_objects o ON o.account_id=v.account_id AND o.id=v.object_id AND o.lifecycle_state='active'
|
|
524
|
+
JOIN knowledge_sources s ON s.account_id=v.account_id AND s.id=v.source_id AND s.lifecycle_state='active'
|
|
525
|
+
JOIN knowledge_providers p ON p.account_id=s.account_id AND p.id=s.provider_id AND p.lifecycle_state='active'
|
|
526
|
+
JOIN knowledge_source_acl_versions a ON a.account_id=s.account_id AND a.source_id=s.id AND a.generation=s.current_acl_generation
|
|
527
|
+
WHERE v.account_id=p_account AND v.id=dep.legacy_document_version_id
|
|
528
|
+
) THEN allowed:=false; EXIT; END IF;
|
|
529
|
+
document_id:=coalesce(dep.access_document_id,dep.legacy_document_id,(dep.body#>>'{source,documentId}')::uuid);
|
|
530
|
+
IF document_id IS NOT NULL AND NOT EXISTS(SELECT 1 FROM documents d
|
|
531
|
+
WHERE d.account_id=p_account AND d.id=document_id AND d.status='ready') THEN allowed:=false; EXIT; END IF;
|
|
532
|
+
file_id:=coalesce(dep.access_file_id,(dep.body#>>'{source,fileId}')::uuid);
|
|
533
|
+
-- The retained original's immutable initiator is processing provenance, not
|
|
534
|
+
-- a human grant. Ordinary Knowledge reads still check the current caller.
|
|
535
|
+
file_subject:=dep.scope_subject_id;
|
|
536
|
+
IF document_id IS NOT NULL THEN
|
|
537
|
+
SELECT coalesce(d.authority_subject_id,d.created_by) INTO file_subject FROM documents d
|
|
538
|
+
WHERE d.account_id=p_account AND d.id=document_id;
|
|
539
|
+
END IF;
|
|
540
|
+
PERFORM set_config('opengeni.subject_id',coalesce(file_subject,''),true);
|
|
541
|
+
IF file_id IS NOT NULL AND (NOT EXISTS(SELECT 1 FROM files f
|
|
542
|
+
WHERE f.account_id=p_account AND f.id=file_id AND f.status='ready')
|
|
543
|
+
OR ((p_preparing_document IS NULL OR document_id IS DISTINCT FROM p_preparing_document) AND NOT google_drive_file_authorized(p_account,coalesce(dep.scope_workspace_id,dep.origin_workspace_id),file_subject,file_id)))
|
|
544
|
+
THEN allowed:=false; EXIT; END IF;
|
|
545
|
+
END LOOP;
|
|
546
|
+
PERFORM set_config('opengeni.workspace_id',coalesce(previous_workspace,''),true);
|
|
547
|
+
PERFORM set_config('opengeni.subject_id',coalesce(previous_subject,''),true);
|
|
548
|
+
PERFORM set_config('opengeni.private_file_owner',coalesce(previous_owner,''),true);
|
|
549
|
+
RETURN allowed;
|
|
550
|
+
EXCEPTION WHEN OTHERS THEN
|
|
551
|
+
PERFORM set_config('opengeni.workspace_id',coalesce(previous_workspace,''),true);
|
|
552
|
+
PERFORM set_config('opengeni.subject_id',coalesce(previous_subject,''),true);
|
|
553
|
+
PERFORM set_config('opengeni.private_file_owner',coalesce(previous_owner,''),true);
|
|
554
|
+
RAISE;
|
|
555
|
+
END $$;
|
|
556
|
+
|
|
557
|
+
CREATE FUNCTION knowledge_index_work(p_account uuid,p_revision uuid,p_lease uuid,p_request jsonb)
|
|
558
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT AS $$
|
|
559
|
+
DECLARE job knowledge_index_jobs%ROWTYPE; revision knowledge_entry_revisions%ROWTYPE; entry knowledge_entries%ROWTYPE;
|
|
560
|
+
operation text:=p_request->>'operation'; chunk jsonb; ordinal integer:=0; vector_value vector;
|
|
561
|
+
BEGIN
|
|
562
|
+
IF p_account IS DISTINCT FROM opengeni_private.current_account_id() THEN
|
|
563
|
+
RAISE EXCEPTION 'Knowledge index tenant mismatch' USING ERRCODE='42501'; END IF;
|
|
564
|
+
SELECT * INTO job FROM knowledge_index_jobs j WHERE j.account_id=p_account AND j.revision_id=p_revision
|
|
565
|
+
AND j.lease_id=p_lease AND j.state='running' AND j.lease_until>clock_timestamp() FOR UPDATE;
|
|
566
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'Knowledge index lease unavailable' USING ERRCODE='40001'; END IF;
|
|
567
|
+
SELECT * INTO entry FROM knowledge_entries e WHERE e.account_id=p_account AND e.id=job.entry_id;
|
|
568
|
+
SELECT * INTO revision FROM knowledge_entry_revisions r WHERE r.account_id=p_account AND r.id=p_revision;
|
|
569
|
+
IF entry.id IS NULL OR revision.id IS NULL OR entry.archived OR revision.change_kind='archive'
|
|
570
|
+
OR (p_revision IS DISTINCT FROM entry.latest_revision_id AND p_revision IS DISTINCT FROM entry.published_revision_id) THEN
|
|
571
|
+
UPDATE knowledge_index_jobs SET state='obsolete',lease_id=NULL,lease_until=NULL WHERE revision_id=p_revision;
|
|
572
|
+
DELETE FROM knowledge_entry_vectors WHERE account_id=p_account AND revision_id=p_revision;
|
|
573
|
+
RETURN jsonb_build_object('status','obsolete');
|
|
574
|
+
END IF;
|
|
575
|
+
IF operation NOT IN ('fail','continue') AND NOT knowledge_index_retention_active(p_account,entry.id,p_revision) THEN
|
|
576
|
+
UPDATE knowledge_index_jobs SET state='pending',lease_id=NULL,lease_until=NULL,last_failure='source_unavailable',
|
|
577
|
+
next_attempt_at=clock_timestamp()+interval '1 hour' WHERE revision_id=p_revision;
|
|
578
|
+
RETURN jsonb_build_object('status','pending');
|
|
579
|
+
END IF;
|
|
580
|
+
IF operation='read' THEN
|
|
581
|
+
UPDATE knowledge_index_jobs SET lease_until=clock_timestamp()+interval '5 minutes' WHERE revision_id=p_revision;
|
|
582
|
+
RETURN jsonb_build_object('status','running','body',revision.body,'codecVersion',revision.body_codec_version,
|
|
583
|
+
'originWorkspaceId',entry.origin_workspace_id,'scope',entry.scope,'subjectId',entry.scope_subject_id,
|
|
584
|
+
'nextIndex',job.next_index,'generation',job.generation);
|
|
585
|
+
ELSIF operation='append' THEN
|
|
586
|
+
IF jsonb_typeof(p_request->'chunks') IS DISTINCT FROM 'array' OR jsonb_array_length(p_request->'chunks') NOT BETWEEN 1 AND 64
|
|
587
|
+
OR (p_request->>'expectedNextIndex')::integer IS DISTINCT FROM job.next_index THEN
|
|
588
|
+
RAISE EXCEPTION 'Knowledge index checkpoint changed' USING ERRCODE='40001'; END IF;
|
|
589
|
+
FOR chunk IN SELECT value FROM jsonb_array_elements(p_request->'chunks') LOOP
|
|
590
|
+
IF (chunk->>'index')::integer IS DISTINCT FROM job.next_index+ordinal
|
|
591
|
+
OR jsonb_typeof(chunk->'text') IS DISTINCT FROM 'string' OR length(chunk->>'text')>16384
|
|
592
|
+
OR jsonb_typeof(chunk->'embedding') IS DISTINCT FROM 'array' OR jsonb_array_length(chunk->'embedding')<>job.dimensions THEN
|
|
593
|
+
RAISE EXCEPTION 'Invalid Knowledge index chunk' USING ERRCODE='22023'; END IF;
|
|
594
|
+
vector_value:=(chunk->'embedding')::text::vector;
|
|
595
|
+
INSERT INTO knowledge_entry_vectors(account_id,entry_id,revision_id,generation,chunk_index,model,dimensions,
|
|
596
|
+
field,start_offset,end_offset,text,text_codec_version,embedding)
|
|
597
|
+
VALUES(p_account,job.entry_id,p_revision,job.generation,job.next_index+ordinal,job.model,job.dimensions,
|
|
598
|
+
chunk->>'field',(chunk->>'start')::integer,(chunk->>'end')::integer,chunk->>'text',1,vector_value);
|
|
599
|
+
ordinal:=ordinal+1;
|
|
600
|
+
END LOOP;
|
|
601
|
+
UPDATE knowledge_index_jobs SET next_index=next_index+ordinal,lease_until=clock_timestamp()+interval '5 minutes' WHERE revision_id=p_revision;
|
|
602
|
+
RETURN jsonb_build_object('status','running','nextIndex',job.next_index+ordinal);
|
|
603
|
+
ELSIF operation='complete' THEN
|
|
604
|
+
IF (p_request->>'expectedNextIndex')::integer IS DISTINCT FROM job.next_index OR job.next_index<1 THEN
|
|
605
|
+
RAISE EXCEPTION 'Knowledge index checkpoint changed' USING ERRCODE='40001'; END IF;
|
|
606
|
+
UPDATE knowledge_index_jobs SET state='ready',completed_generation=generation,lease_id=NULL,lease_until=NULL,last_failure=NULL WHERE revision_id=p_revision;
|
|
607
|
+
DELETE FROM knowledge_entry_vectors WHERE account_id=p_account AND revision_id=p_revision AND generation<>job.generation;
|
|
608
|
+
RETURN jsonb_build_object('status','ready');
|
|
609
|
+
ELSIF operation='continue' THEN
|
|
610
|
+
UPDATE knowledge_index_jobs SET state='pending',lease_id=NULL,lease_until=NULL,next_attempt_at=clock_timestamp(),attempts=0
|
|
611
|
+
WHERE revision_id=p_revision;
|
|
612
|
+
RETURN jsonb_build_object('status','pending');
|
|
613
|
+
ELSIF operation='fail' THEN
|
|
614
|
+
UPDATE knowledge_index_jobs SET state='pending',lease_id=NULL,lease_until=NULL,last_failure='embedding_unavailable',
|
|
615
|
+
next_attempt_at=clock_timestamp()+least(3600,power(2,least(attempts,10))*5)*interval '1 second' WHERE revision_id=p_revision;
|
|
616
|
+
RETURN jsonb_build_object('status','pending');
|
|
617
|
+
END IF;
|
|
618
|
+
RAISE EXCEPTION 'Unknown Knowledge index operation' USING ERRCODE='22023';
|
|
619
|
+
END $$;
|
|
620
|
+
|
|
621
|
+
-- A Drive ACL refresh can finish just after the projector deferred a source.
|
|
622
|
+
-- Wake its existing job immediately; the normal lease and access checks remain
|
|
623
|
+
-- authoritative at read and commit, including a concurrent revocation.
|
|
624
|
+
CREATE FUNCTION knowledge_source_index_wake() RETURNS trigger
|
|
625
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT AS $$
|
|
626
|
+
BEGIN
|
|
627
|
+
IF NEW.status='indexed' AND NEW.acl_eligibility='eligible' THEN
|
|
628
|
+
UPDATE knowledge_index_jobs j SET next_attempt_at=clock_timestamp()
|
|
629
|
+
FROM knowledge_entries e WHERE e.account_id=NEW.account_id AND j.account_id=e.account_id
|
|
630
|
+
AND j.entry_id=e.id AND j.state='pending'
|
|
631
|
+
AND (e.legacy_document_id=NEW.document_id OR e.legacy_document_version_id=NEW.knowledge_document_version_id);
|
|
632
|
+
END IF;
|
|
633
|
+
RETURN NEW;
|
|
634
|
+
END $$;
|
|
635
|
+
CREATE TRIGGER knowledge_source_index_ready AFTER UPDATE OF status,acl_eligibility,google_drive_acl_evidence_id
|
|
636
|
+
ON knowledge_source_sync_index_obligations FOR EACH ROW EXECUTE FUNCTION knowledge_source_index_wake();
|
|
637
|
+
|
|
638
|
+
CREATE TABLE knowledge_entry_search (
|
|
639
|
+
account_id uuid NOT NULL,
|
|
640
|
+
entry_id uuid NOT NULL,
|
|
641
|
+
revision_id uuid NOT NULL,
|
|
642
|
+
chunk_index integer NOT NULL CHECK(chunk_index>=0),
|
|
643
|
+
search_vector tsvector NOT NULL,
|
|
644
|
+
PRIMARY KEY(account_id,entry_id,revision_id,chunk_index),
|
|
645
|
+
FOREIGN KEY(account_id,entry_id,revision_id) REFERENCES knowledge_entry_revisions(account_id,entry_id,id) ON DELETE CASCADE
|
|
646
|
+
);
|
|
647
|
+
CREATE INDEX knowledge_entry_search_vector_idx ON knowledge_entry_search USING gin(search_vector);
|
|
648
|
+
|
|
649
|
+
-- Bounded, overlapping projections avoid PostgreSQL's tsvector size limit for
|
|
650
|
+
-- long sources. Canonical text is always the exact revision body above.
|
|
651
|
+
CREATE FUNCTION knowledge_index_revision(p_account uuid,p_entry uuid,p_revision uuid,p_text text)
|
|
652
|
+
RETURNS void LANGUAGE sql SET search_path FROM CURRENT AS $$
|
|
653
|
+
INSERT INTO knowledge_entry_search(account_id,entry_id,revision_id,chunk_index,search_vector)
|
|
654
|
+
SELECT p_account,p_entry,p_revision,(n/14000)::integer,
|
|
655
|
+
setweight(to_tsvector('simple',r.body->>'title'),'A') ||
|
|
656
|
+
setweight(to_tsvector('simple',substring(p_text FROM n+1 FOR 16000)),'B')
|
|
657
|
+
FROM generate_series(0,greatest(length(p_text)-1,0),14000) n
|
|
658
|
+
JOIN knowledge_entry_revisions r ON r.account_id=p_account AND r.entry_id=p_entry AND r.id=p_revision
|
|
659
|
+
$$;
|
|
660
|
+
|
|
661
|
+
CREATE FUNCTION knowledge_reject_history_mutation() RETURNS trigger
|
|
662
|
+
LANGUAGE plpgsql SET search_path=pg_catalog AS $$
|
|
663
|
+
BEGIN
|
|
664
|
+
IF TG_OP='DELETE' AND pg_trigger_depth()>1 THEN RETURN OLD; END IF;
|
|
665
|
+
RAISE EXCEPTION 'Knowledge history is immutable' USING ERRCODE='55000';
|
|
666
|
+
END $$;
|
|
667
|
+
|
|
668
|
+
-- Tenant fences apply even inside owner lifecycle functions. No runtime table
|
|
669
|
+
-- grants: the read capability also checks publication, source ACLs and links.
|
|
670
|
+
DO $tables$
|
|
671
|
+
DECLARE t text; runtime_role text;
|
|
672
|
+
BEGIN
|
|
673
|
+
FOREACH t IN ARRAY ARRAY['agent_learning_revisions','agent_learning_snapshots','agent_instruction_operations','knowledge_entries','knowledge_review_batches',
|
|
674
|
+
'knowledge_entry_revisions','knowledge_entry_links','knowledge_entry_decisions',
|
|
675
|
+
'knowledge_entry_operations','knowledge_entry_search','knowledge_index_jobs','knowledge_entry_vectors'] LOOP
|
|
676
|
+
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY',t);
|
|
677
|
+
EXECUTE format('ALTER TABLE %I FORCE ROW LEVEL SECURITY',t);
|
|
678
|
+
EXECUTE format('CREATE POLICY knowledge_tenant ON %I USING (account_id = nullif(current_setting(''opengeni.account_id'',true),'''')::uuid)',t);
|
|
679
|
+
EXECUTE format('REVOKE ALL ON %I FROM PUBLIC',t);
|
|
680
|
+
FOR runtime_role IN SELECT jsonb_array_elements_text(current_setting('opengeni.migration_application_roles')::jsonb) LOOP
|
|
681
|
+
IF runtime_role <> current_user AND EXISTS(SELECT 1 FROM pg_roles WHERE rolname=runtime_role) THEN
|
|
682
|
+
EXECUTE format('REVOKE ALL ON %I FROM %I',t,runtime_role);
|
|
683
|
+
END IF;
|
|
684
|
+
END LOOP;
|
|
685
|
+
END LOOP;
|
|
686
|
+
FOREACH t IN ARRAY ARRAY['agent_learning_revisions','agent_learning_snapshots','agent_instruction_operations','knowledge_entry_revisions','knowledge_entry_links',
|
|
687
|
+
'knowledge_entry_decisions','knowledge_entry_operations'] LOOP
|
|
688
|
+
EXECUTE format('CREATE TRIGGER knowledge_immutable BEFORE UPDATE OR DELETE ON %I FOR EACH ROW EXECUTE FUNCTION knowledge_reject_history_mutation()',t);
|
|
689
|
+
END LOOP;
|
|
690
|
+
END $tables$;
|
|
691
|
+
|
|
692
|
+
-- Compatibility selectors are retained as restrictions. A migration never
|
|
693
|
+
-- interprets a legacy label or source-created identity as a personal owner.
|
|
694
|
+
CREATE FUNCTION knowledge_scope_visible(e knowledge_entries) RETURNS boolean
|
|
695
|
+
LANGUAGE sql STABLE SET search_path FROM CURRENT AS $$
|
|
696
|
+
SELECT coalesce(e.account_id=nullif(current_setting('opengeni.account_id',true),'')::uuid
|
|
697
|
+
AND (e.scope='organization'
|
|
698
|
+
OR (e.scope='workspace' AND e.scope_workspace_id=nullif(current_setting('opengeni.workspace_id',true),'')::uuid)
|
|
699
|
+
OR (e.scope='personal' AND e.scope_subject_id=nullif(current_setting('opengeni.subject_id',true),'')
|
|
700
|
+
AND (current_setting('opengeni.knowledge_actor_kind',true)='human'
|
|
701
|
+
OR current_setting('opengeni.knowledge_default_scope',true)='personal')))
|
|
702
|
+
AND (e.legacy_scope_workspace_id IS NULL OR e.legacy_scope_workspace_id=nullif(current_setting('opengeni.workspace_id',true),'')::uuid)
|
|
703
|
+
AND e.valid_from<=transaction_timestamp() AND (e.valid_until IS NULL OR e.valid_until>transaction_timestamp())
|
|
704
|
+
AND (e.legacy_memory_id IS NULL OR e.origin_workspace_id=nullif(current_setting('opengeni.workspace_id',true),'')::uuid)
|
|
705
|
+
AND (e.legacy_scope_type IS NULL OR opengeni_private.memory_scope_visible(
|
|
706
|
+
e.legacy_scope_type,e.scope_subject_id,e.legacy_scope_role_key,e.legacy_scope_session_id,e.valid_from,e.valid_until)),false)
|
|
707
|
+
$$;
|
|
708
|
+
|
|
709
|
+
CREATE FUNCTION knowledge_learning_settings_valid(value jsonb, complete boolean) RETURNS boolean
|
|
710
|
+
LANGUAGE sql IMMUTABLE SET search_path=pg_catalog AS $$
|
|
711
|
+
SELECT coalesce(jsonb_typeof(value)='object'
|
|
712
|
+
AND value-'knowledge'-'instructions'-'skills'='{}'::jsonb
|
|
713
|
+
AND (NOT complete OR value ?& ARRAY['knowledge','instructions','skills'])
|
|
714
|
+
AND NOT EXISTS(SELECT 1 FROM jsonb_each(value) p
|
|
715
|
+
WHERE jsonb_typeof(p.value)<>'string' OR p.value#>>'{}' NOT IN ('automatic','review_first','off')),false)
|
|
716
|
+
$$;
|
|
717
|
+
ALTER TABLE agent_learning_revisions ADD CONSTRAINT agent_learning_settings_valid
|
|
718
|
+
CHECK(knowledge_learning_settings_valid(settings,context_key='defaults'));
|
|
719
|
+
|
|
720
|
+
CREATE FUNCTION knowledge_learning_resolve(p_account uuid,p_workspace uuid,p_subject text,
|
|
721
|
+
p_context text,p_at timestamptz) RETURNS jsonb
|
|
722
|
+
LANGUAGE plpgsql STABLE SET search_path FROM CURRENT AS $$
|
|
723
|
+
DECLARE base jsonb; overrides jsonb; base_version integer; context_version integer;
|
|
724
|
+
owner text:=CASE WHEN p_subject IS NULL THEN 'workspace:'||p_workspace ELSE 'personal:'||p_subject END;
|
|
725
|
+
BEGIN
|
|
726
|
+
SELECT settings,version INTO base,base_version FROM agent_learning_revisions
|
|
727
|
+
WHERE account_id=p_account AND owner_key=owner AND context_key='defaults' AND created_at<=p_at
|
|
728
|
+
ORDER BY version DESC LIMIT 1;
|
|
729
|
+
base:=coalesce(base,'{"knowledge":"automatic","instructions":"review_first","skills":"review_first"}'::jsonb);
|
|
730
|
+
SELECT settings,version INTO overrides,context_version FROM agent_learning_revisions
|
|
731
|
+
WHERE account_id=p_account AND owner_key=owner AND context_key=p_context AND created_at<=p_at
|
|
732
|
+
ORDER BY version DESC LIMIT 1;
|
|
733
|
+
RETURN jsonb_build_object('ownerKey',owner,'defaultsVersion',coalesce(base_version,0),
|
|
734
|
+
'contextKey',p_context,'contextVersion',coalesce(context_version,0),
|
|
735
|
+
'defaults',base,'overrides',coalesce(overrides,'{}'::jsonb),'effective',base||coalesce(overrides,'{}'::jsonb));
|
|
736
|
+
END $$;
|
|
737
|
+
|
|
738
|
+
-- Backfill only policy, not authority. Existing explicit Memory opt-outs and
|
|
739
|
+
-- learning modes become category defaults. Per-record source exceptions remain
|
|
740
|
+
-- historical proof for retired proposal writers, not new task/chat policy.
|
|
741
|
+
ALTER TABLE agent_learning_revisions NO FORCE ROW LEVEL SECURITY;
|
|
742
|
+
ALTER TABLE workspace_learning_policy_heads NO FORCE ROW LEVEL SECURITY;
|
|
743
|
+
ALTER TABLE workspace_learning_policy_revisions NO FORCE ROW LEVEL SECURITY;
|
|
744
|
+
INSERT INTO agent_learning_revisions(account_id,origin_workspace_id,owner_key,version,settings,
|
|
745
|
+
operation_id,request_hash,actor_subject_id,created_at)
|
|
746
|
+
SELECT w.account_id,w.id,'workspace:'||w.id,1,
|
|
747
|
+
jsonb_build_object('knowledge',CASE WHEN w.settings->>'memoryEnabled'='false' THEN 'off' ELSE 'automatic' END,
|
|
748
|
+
'instructions',CASE WHEN coalesce(r.workspace_mode,'suggest')='suggest' THEN 'review_first' ELSE r.workspace_mode END,
|
|
749
|
+
'skills',CASE WHEN coalesce(r.workspace_mode,'suggest')='suggest' THEN 'review_first' ELSE r.workspace_mode END),
|
|
750
|
+
gen_random_uuid(),'migration:0461','service:knowledge-migration:0461','epoch'::timestamptz
|
|
751
|
+
FROM workspaces w LEFT JOIN workspace_learning_policy_heads h ON h.workspace_id=w.id AND h.account_id=w.account_id
|
|
752
|
+
LEFT JOIN workspace_learning_policy_revisions r ON r.id=h.revision_id AND r.account_id=w.account_id;
|
|
753
|
+
-- The Personal workspace's explicit settings become its owner's defaults.
|
|
754
|
+
-- Existing private chats keep a workspace-level opt-out as a sparse chat override.
|
|
755
|
+
ALTER TABLE organization_memberships NO FORCE ROW LEVEL SECURITY;
|
|
756
|
+
ALTER TABLE sessions NO FORCE ROW LEVEL SECURITY;
|
|
757
|
+
INSERT INTO agent_learning_revisions(account_id,origin_workspace_id,owner_key,subject_id,version,settings,
|
|
758
|
+
operation_id,request_hash,actor_subject_id,created_at)
|
|
759
|
+
SELECT m.account_id,w.id,'personal:'||m.subject_id,m.subject_id,1,r.settings,
|
|
760
|
+
gen_random_uuid(),'migration:0461:personal','service:knowledge-migration:0461','epoch'::timestamptz
|
|
761
|
+
FROM organization_memberships m JOIN workspaces w ON w.account_id=m.account_id AND w.id=m.personal_workspace_id
|
|
762
|
+
JOIN agent_learning_revisions r ON r.account_id=m.account_id AND r.owner_key='workspace:'||w.id AND r.context_key='defaults';
|
|
763
|
+
INSERT INTO agent_learning_revisions(account_id,origin_workspace_id,owner_key,subject_id,context_key,version,settings,
|
|
764
|
+
operation_id,request_hash,actor_subject_id,created_at)
|
|
765
|
+
SELECT s.account_id,s.workspace_id,
|
|
766
|
+
CASE WHEN (s.visibility='user_private' OR s.memory_scope='user' OR pm.id IS NOT NULL) AND coalesce(s.owner_subject_id,s.scope_subject_id,pm.subject_id) IS NOT NULL
|
|
767
|
+
THEN 'personal:'||coalesce(s.owner_subject_id,s.scope_subject_id,pm.subject_id) ELSE 'workspace:'||s.workspace_id END,
|
|
768
|
+
CASE WHEN s.visibility='user_private' OR s.memory_scope='user' OR pm.id IS NOT NULL THEN coalesce(s.owner_subject_id,s.scope_subject_id,pm.subject_id) END,
|
|
769
|
+
'chat:'||s.id,1,'{"knowledge":"off"}',gen_random_uuid(),'migration:0461:chat','service:knowledge-migration:0461','epoch'::timestamptz
|
|
770
|
+
FROM sessions s JOIN workspaces w ON w.account_id=s.account_id AND w.id=s.workspace_id
|
|
771
|
+
LEFT JOIN organization_memberships pm ON pm.account_id=w.account_id AND pm.personal_workspace_id=w.id
|
|
772
|
+
WHERE s.memory_scope='off' OR (w.settings->>'memoryEnabled'='false'
|
|
773
|
+
AND (s.visibility='user_private' OR s.memory_scope='user' OR pm.id IS NOT NULL));
|
|
774
|
+
ALTER TABLE sessions FORCE ROW LEVEL SECURITY;
|
|
775
|
+
ALTER TABLE organization_memberships FORCE ROW LEVEL SECURITY;
|
|
776
|
+
ALTER TABLE workspace_learning_policy_revisions FORCE ROW LEVEL SECURITY;
|
|
777
|
+
ALTER TABLE workspace_learning_policy_heads FORCE ROW LEVEL SECURITY;
|
|
778
|
+
ALTER TABLE agent_learning_revisions FORCE ROW LEVEL SECURITY;
|
|
779
|
+
|
|
780
|
+
-- Resolve only immutable accepted-work lineage. A later settings edit cannot
|
|
781
|
+
-- alter a run already accepted, including its children and causal resumptions.
|
|
782
|
+
CREATE FUNCTION knowledge_learning_for_turn(p_account uuid,p_workspace uuid,p_turn uuid)
|
|
783
|
+
RETURNS jsonb LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
|
784
|
+
DECLARE t session_turns%ROWTYPE; s sessions%ROWTYPE; run scheduled_task_runs%ROWTYPE;
|
|
785
|
+
result jsonb; inherited jsonb; current_policy jsonb; subject text; destination text;
|
|
786
|
+
producer uuid; source_context text; accepted_at timestamptz;
|
|
787
|
+
BEGIN
|
|
788
|
+
SELECT snapshot INTO result FROM agent_learning_snapshots WHERE account_id=p_account AND turn_id=p_turn;
|
|
789
|
+
IF result IS NOT NULL THEN RETURN result; END IF;
|
|
790
|
+
SELECT * INTO t FROM session_turns WHERE account_id=p_account AND workspace_id=p_workspace AND id=p_turn;
|
|
791
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'Learning accepted turn unavailable' USING ERRCODE='42501'; END IF;
|
|
792
|
+
SELECT * INTO s FROM sessions WHERE account_id=p_account AND workspace_id=p_workspace AND id=t.session_id;
|
|
793
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'Learning session unavailable' USING ERRCODE='42501'; END IF;
|
|
794
|
+
subject:=coalesce(t.initiating_human_subject_id,CASE WHEN t.initiator_kind='subject' THEN t.initiator_subject_id END);
|
|
795
|
+
-- The accepted initiating-human field is authority; subject spelling is not.
|
|
796
|
+
-- Opaque authenticated host/local subjects retain the same personal layer.
|
|
797
|
+
destination:=CASE WHEN s.visibility='user_private' OR s.memory_scope='user'
|
|
798
|
+
OR get_workspace_kind(p_account,p_workspace)='personal' THEN 'personal' ELSE 'workspace' END;
|
|
799
|
+
IF destination='personal' AND (subject IS NULL OR (s.visibility='user_private' AND s.owner_subject_id IS DISTINCT FROM subject)) THEN
|
|
800
|
+
RAISE EXCEPTION 'Personal learning requires an initiating owner' USING ERRCODE='42501';
|
|
801
|
+
END IF;
|
|
802
|
+
IF destination='workspace' THEN subject:=NULL; END IF;
|
|
803
|
+
source_context:='chat:'||s.id; accepted_at:=t.created_at;
|
|
804
|
+
IF t.scheduled_task_run_id IS NOT NULL THEN
|
|
805
|
+
SELECT * INTO run FROM scheduled_task_runs WHERE id=t.scheduled_task_run_id AND account_id=p_account AND workspace_id=p_workspace;
|
|
806
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'Learning scheduled run unavailable' USING ERRCODE='42501'; END IF;
|
|
807
|
+
source_context:='scheduled_task:'||run.task_id; accepted_at:=run.created_at;
|
|
808
|
+
ELSE
|
|
809
|
+
IF t.source IN ('goal','system') THEN
|
|
810
|
+
SELECT prior.id INTO producer FROM session_system_updates u
|
|
811
|
+
JOIN session_turns prior ON prior.id=CASE WHEN u.kind IN ('goal_continuation','background_command_result','session_wait_timeout')
|
|
812
|
+
THEN (u.lineage->>'causalTurnId')::uuid ELSE (u.lineage->>'parentTurnId')::uuid END
|
|
813
|
+
AND prior.account_id=p_account AND prior.workspace_id=p_workspace AND prior.session_id=s.id
|
|
814
|
+
WHERE u.account_id=p_account AND u.workspace_id=p_workspace AND u.session_id=s.id
|
|
815
|
+
AND u.delivered_turn_id=t.id AND u.state='delivered' AND u.delivered_history_item_id IS NOT NULL
|
|
816
|
+
AND u.kind IN ('goal_continuation','background_command_result','session_wait_timeout','child_terminal_result','child_requires_action','child_requires_action_resolved',
|
|
817
|
+
'child_paused','child_waiting_capacity','child_progress')
|
|
818
|
+
AND prior.created_at<t.created_at
|
|
819
|
+
ORDER BY prior.position DESC LIMIT 1;
|
|
820
|
+
ELSIF s.parent_turn_id IS NOT NULL AND NOT EXISTS(SELECT 1 FROM session_turns other
|
|
821
|
+
WHERE other.account_id=p_account AND other.session_id=s.id AND other.position<t.position) THEN
|
|
822
|
+
SELECT prior.id INTO producer FROM session_turns prior JOIN sessions parent ON parent.id=prior.session_id
|
|
823
|
+
AND parent.account_id=prior.account_id AND parent.workspace_id=prior.workspace_id
|
|
824
|
+
WHERE prior.account_id=p_account AND prior.workspace_id=p_workspace AND prior.id=s.parent_turn_id
|
|
825
|
+
AND prior.session_id=s.parent_session_id AND prior.created_at<t.created_at AND parent.visibility=s.visibility
|
|
826
|
+
AND prior.initiating_human_subject_id IS NOT DISTINCT FROM t.initiating_human_subject_id;
|
|
827
|
+
END IF;
|
|
828
|
+
END IF;
|
|
829
|
+
current_policy:=knowledge_learning_resolve(p_account,p_workspace,subject,source_context,accepted_at);
|
|
830
|
+
-- Old clients may still create a session with memoryScope=off. Preserve that
|
|
831
|
+
-- accepted opt-out as a sparse context setting, rather than silently enabling
|
|
832
|
+
-- writes after cutover. A later explicit context revision (including an empty
|
|
833
|
+
-- Use-default reset) owns future turns. Never backfill over such a revision.
|
|
834
|
+
IF s.memory_scope='off' AND (current_policy->>'contextVersion')::integer=0 THEN
|
|
835
|
+
INSERT INTO agent_learning_revisions(account_id,origin_workspace_id,owner_key,subject_id,context_key,
|
|
836
|
+
version,settings,operation_id,request_hash,actor_subject_id,created_at)
|
|
837
|
+
SELECT p_account,p_workspace,current_policy->>'ownerKey',subject,source_context,1,'{"knowledge":"off"}'::jsonb,
|
|
838
|
+
gen_random_uuid(),'legacy-session-off:'||s.id,'service:knowledge-compatibility',accepted_at
|
|
839
|
+
WHERE NOT EXISTS(SELECT 1 FROM agent_learning_revisions r WHERE r.account_id=p_account
|
|
840
|
+
AND r.owner_key=current_policy->>'ownerKey' AND r.context_key=source_context)
|
|
841
|
+
ON CONFLICT DO NOTHING;
|
|
842
|
+
current_policy:=knowledge_learning_resolve(p_account,p_workspace,subject,source_context,accepted_at);
|
|
843
|
+
-- A setting can have been edited after this work was accepted. It must not
|
|
844
|
+
-- alter the accepted opt-out, even when that later row prevented the seed.
|
|
845
|
+
current_policy:=jsonb_set(jsonb_set(current_policy,'{effective,knowledge}','"off"'::jsonb),
|
|
846
|
+
'{overrides}',(current_policy->'overrides')||'{"knowledge":"off"}'::jsonb);
|
|
847
|
+
END IF;
|
|
848
|
+
IF producer IS NOT NULL THEN
|
|
849
|
+
inherited:=knowledge_learning_for_turn(p_account,p_workspace,producer);
|
|
850
|
+
-- A child may deliberately narrow workspace learning to personal or Off.
|
|
851
|
+
-- Keep its independently resolved owner and destination while inheriting
|
|
852
|
+
-- only accepted policy modes and scheduled-run provenance. An explicit
|
|
853
|
+
-- child/chat override still wins.
|
|
854
|
+
result:=current_policy||jsonb_build_object('effective',(inherited->'effective')||(current_policy->'overrides'),
|
|
855
|
+
'producerTurnId',producer,'scheduledTaskRunId',inherited->'scheduledTaskRunId');
|
|
856
|
+
ELSE
|
|
857
|
+
result:=current_policy||jsonb_build_object('scheduledTaskRunId',t.scheduled_task_run_id);
|
|
858
|
+
END IF;
|
|
859
|
+
result:=result||jsonb_build_object('defaultScope',destination,'subjectId',subject,'acceptedAt',accepted_at);
|
|
860
|
+
INSERT INTO agent_learning_snapshots(account_id,workspace_id,session_id,turn_id,snapshot)
|
|
861
|
+
VALUES(p_account,p_workspace,s.id,t.id,result) ON CONFLICT(account_id,turn_id) DO NOTHING;
|
|
862
|
+
SELECT snapshot INTO result FROM agent_learning_snapshots WHERE account_id=p_account AND turn_id=t.id;
|
|
863
|
+
RETURN result;
|
|
864
|
+
END $$;
|
|
865
|
+
|
|
866
|
+
-- Internal authority resolver. HTTP principals are already authenticated and
|
|
867
|
+
-- permission checked by core; an agent receives only a host-bound attempt tuple.
|
|
868
|
+
-- Never expose this function as a runtime capability or accept its actor JSON
|
|
869
|
+
-- from a model/client request. Lock order matches the session lifecycle.
|
|
870
|
+
CREATE FUNCTION knowledge_resolve_actor(p_account uuid,p_workspace uuid,p_actor jsonb)
|
|
871
|
+
RETURNS jsonb LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
|
872
|
+
DECLARE prepared knowledge_entries%ROWTYPE; s sessions%ROWTYPE; t session_turns%ROWTYPE; a session_turn_attempts%ROWTYPE;
|
|
873
|
+
subject text; destination text; policy jsonb; run scheduled_task_runs%ROWTYPE;
|
|
874
|
+
context_key text; resolved_actor jsonb;
|
|
875
|
+
BEGIN
|
|
876
|
+
IF p_account IS DISTINCT FROM nullif(current_setting('opengeni.account_id',true),'')::uuid
|
|
877
|
+
OR p_workspace IS DISTINCT FROM nullif(current_setting('opengeni.workspace_id',true),'')::uuid THEN
|
|
878
|
+
RAISE EXCEPTION 'Knowledge requires exact workspace authority' USING ERRCODE='42501';
|
|
879
|
+
END IF;
|
|
880
|
+
PERFORM 1 FROM workspaces WHERE id=p_workspace AND account_id=p_account FOR KEY SHARE;
|
|
881
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'Knowledge workspace unavailable' USING ERRCODE='42501'; END IF;
|
|
882
|
+
IF p_actor->>'kind'='source_preparation' THEN
|
|
883
|
+
SELECT * INTO prepared FROM knowledge_entries e WHERE e.account_id=p_account
|
|
884
|
+
AND e.id=(p_actor->>'entryId')::uuid AND e.origin_workspace_id=p_workspace FOR SHARE;
|
|
885
|
+
IF NOT FOUND OR prepared.legacy_document_id IS NULL
|
|
886
|
+
OR prepared.document_preparation ? 'completion'
|
|
887
|
+
OR prepared.document_preparation->>'leaseId' IS DISTINCT FROM p_actor->>'leaseId'
|
|
888
|
+
OR (prepared.document_preparation->>'leaseUntil')::timestamptz<=clock_timestamp() THEN
|
|
889
|
+
RAISE EXCEPTION 'Document preparation lease unavailable' USING ERRCODE='42501'; END IF;
|
|
890
|
+
PERFORM set_config('opengeni.subject_id',coalesce(prepared.scope_subject_id,''),true);
|
|
891
|
+
PERFORM set_config('opengeni.private_file_owner',coalesce(prepared.scope_subject_id,''),true);
|
|
892
|
+
PERFORM set_config('opengeni.knowledge_default_scope',prepared.scope,true);
|
|
893
|
+
PERFORM set_config('opengeni.knowledge_actor_kind','source_preparation',true);
|
|
894
|
+
PERFORM set_config('opengeni.knowledge_prepared_document',prepared.legacy_document_id::text,true);
|
|
895
|
+
RETURN jsonb_build_object('kind','source_preparation','entryId',prepared.id,'defaultScope',prepared.scope,
|
|
896
|
+
'subjectId',prepared.scope_subject_id,'review',false,'policy',prepared.document_preparation->'policy',
|
|
897
|
+
'scheduledTaskRunId',prepared.document_preparation->'scheduledTaskRunId');
|
|
898
|
+
END IF;
|
|
899
|
+
IF p_actor->>'kind'='service' THEN
|
|
900
|
+
IF p_actor->>'principalKind' NOT IN ('service','api_key','configured_key','mcp_gateway')
|
|
901
|
+
OR p_actor->>'principalKind' IS DISTINCT FROM nullif(current_setting('opengeni.principal_kind',true),'')
|
|
902
|
+
OR p_actor->>'subjectId' IS DISTINCT FROM nullif(current_setting('opengeni.subject_id',true),'') THEN
|
|
903
|
+
RAISE EXCEPTION 'Knowledge service authority unavailable' USING ERRCODE='42501'; END IF;
|
|
904
|
+
PERFORM set_config('opengeni.subject_id','',true);
|
|
905
|
+
PERFORM set_config('opengeni.knowledge_actor_kind','service',true);
|
|
906
|
+
PERFORM set_config('opengeni.private_file_owner','',true);
|
|
907
|
+
RETURN jsonb_build_object('kind','service','defaultScope','workspace','review',false,
|
|
908
|
+
'writeScopes',CASE WHEN p_actor->>'principalKind'='mcp_gateway' THEN '[]'::jsonb
|
|
909
|
+
ELSE coalesce(p_actor->'writeScopes','[]'::jsonb)-'personal' END);
|
|
910
|
+
END IF;
|
|
911
|
+
IF p_actor->>'kind'='human' THEN
|
|
912
|
+
subject:=p_actor->>'subjectId';
|
|
913
|
+
IF subject IS NULL OR length(subject) NOT BETWEEN 1 AND 1024
|
|
914
|
+
OR subject IS DISTINCT FROM nullif(current_setting('opengeni.subject_id',true),'')
|
|
915
|
+
OR p_actor->>'principalKind' IS DISTINCT FROM 'human_session'
|
|
916
|
+
OR nullif(current_setting('opengeni.principal_kind',true),'') IS DISTINCT FROM 'human_session' THEN
|
|
917
|
+
RAISE EXCEPTION 'Knowledge human authority unavailable' USING ERRCODE='42501';
|
|
918
|
+
END IF;
|
|
919
|
+
PERFORM set_config('opengeni.knowledge_actor_kind','human',true);
|
|
920
|
+
PERFORM set_config('opengeni.private_file_owner',subject,true);
|
|
921
|
+
RETURN jsonb_build_object('kind','human','subjectId',subject,'defaultScope','workspace',
|
|
922
|
+
'writeScopes',coalesce(p_actor->'writeScopes','[]'::jsonb),'review',coalesce((p_actor->>'review')::boolean,false));
|
|
923
|
+
END IF;
|
|
924
|
+
IF p_actor->>'kind' IS DISTINCT FROM 'agent' THEN
|
|
925
|
+
RAISE EXCEPTION 'Knowledge requires a human or exact agent attempt' USING ERRCODE='42501';
|
|
926
|
+
END IF;
|
|
927
|
+
SELECT * INTO s FROM sessions WHERE id=(p_actor->>'sessionId')::uuid
|
|
928
|
+
AND workspace_id=p_workspace AND account_id=p_account FOR SHARE;
|
|
929
|
+
IF NOT FOUND OR s.active_turn_id IS DISTINCT FROM (p_actor->>'turnId')::uuid THEN
|
|
930
|
+
RAISE EXCEPTION 'Knowledge session attempt is stale' USING ERRCODE='42501';
|
|
931
|
+
END IF;
|
|
932
|
+
SELECT * INTO t FROM session_turns WHERE id=s.active_turn_id AND session_id=s.id
|
|
933
|
+
AND workspace_id=p_workspace AND account_id=p_account FOR SHARE;
|
|
934
|
+
IF NOT FOUND OR t.active_attempt_id IS DISTINCT FROM (p_actor->>'attemptId')::uuid
|
|
935
|
+
OR t.execution_generation IS DISTINCT FROM (p_actor->>'executionGeneration')::integer
|
|
936
|
+
OR t.status NOT IN ('running','requires_action','recovering','waiting_capacity') THEN
|
|
937
|
+
RAISE EXCEPTION 'Knowledge turn attempt is stale' USING ERRCODE='42501';
|
|
938
|
+
END IF;
|
|
939
|
+
SELECT * INTO a FROM session_turn_attempts WHERE id=t.active_attempt_id AND turn_id=t.id
|
|
940
|
+
AND session_id=s.id AND account_id=p_account AND workspace_id=p_workspace FOR SHARE;
|
|
941
|
+
IF NOT FOUND OR a.state NOT IN ('claimed','running') OR a.execution_generation<>t.execution_generation THEN
|
|
942
|
+
RAISE EXCEPTION 'Knowledge attempt is stale' USING ERRCODE='42501';
|
|
943
|
+
END IF;
|
|
944
|
+
-- Fresh read after locks: interruption writers acquire the attempt FOR UPDATE.
|
|
945
|
+
IF EXISTS(SELECT 1 FROM session_attempt_interruptions WHERE workspace_id=p_workspace
|
|
946
|
+
AND attempt_id=a.id AND state IN ('pending','delivered','acknowledged')) THEN
|
|
947
|
+
RAISE EXCEPTION 'Knowledge attempt is interrupted' USING ERRCODE='42501';
|
|
948
|
+
END IF;
|
|
949
|
+
subject:=coalesce(t.initiating_human_subject_id,CASE WHEN t.initiator_kind='subject' THEN t.initiator_subject_id END);
|
|
950
|
+
-- The accepted initiating-human field is authority; subject spelling is not.
|
|
951
|
+
-- Opaque authenticated host/local subjects retain the same personal layer.
|
|
952
|
+
destination:=CASE WHEN s.visibility='user_private' OR s.memory_scope='user'
|
|
953
|
+
OR get_workspace_kind(p_account,p_workspace)='personal'
|
|
954
|
+
THEN 'personal' ELSE 'workspace' END;
|
|
955
|
+
IF destination='personal' AND (subject IS NULL
|
|
956
|
+
OR (s.visibility='user_private' AND s.owner_subject_id IS DISTINCT FROM subject)) THEN
|
|
957
|
+
RAISE EXCEPTION 'Personal Knowledge requires the verified initiating owner' USING ERRCODE='42501';
|
|
958
|
+
END IF;
|
|
959
|
+
-- Keep the verified subject for provider ACL checks. The independent Knowledge
|
|
960
|
+
-- scope selector prevents a shared agent from ambient-reading personal entries.
|
|
961
|
+
PERFORM set_config('opengeni.subject_id',coalesce(subject,''),true);
|
|
962
|
+
PERFORM set_config('opengeni.knowledge_default_scope',destination,true);
|
|
963
|
+
PERFORM set_config('opengeni.knowledge_actor_kind','agent',true);
|
|
964
|
+
PERFORM set_config('opengeni.private_file_owner',CASE WHEN destination='personal' THEN coalesce(subject,'') ELSE '' END,true);
|
|
965
|
+
PERFORM set_config('opengeni.knowledge_session_id',s.id::text,true);
|
|
966
|
+
PERFORM set_config('opengeni.knowledge_attempt_id',a.id::text,true);
|
|
967
|
+
PERFORM set_config('opengeni.memory_session_id',s.id::text,true);
|
|
968
|
+
PERFORM set_config('opengeni.memory_role_key',coalesce(s.metadata->>'memoryRoleKey',''),true);
|
|
969
|
+
policy:=knowledge_learning_for_turn(p_account,p_workspace,t.id);
|
|
970
|
+
IF policy->>'defaultScope' IS DISTINCT FROM destination OR (destination='personal' AND policy->>'subjectId' IS DISTINCT FROM subject) THEN
|
|
971
|
+
RAISE EXCEPTION 'Learning scope changed after acceptance' USING ERRCODE='42501';
|
|
972
|
+
END IF;
|
|
973
|
+
resolved_actor:=jsonb_build_object('kind','agent','subjectId',subject,'defaultScope',destination,
|
|
974
|
+
'sessionId',s.id,'turnId',t.id,'attemptId',a.id,'executionGeneration',a.execution_generation,
|
|
975
|
+
'scheduledTaskRunId',policy->'scheduledTaskRunId','retentionScope',CASE WHEN s.memory_scope='session' THEN 'session' ELSE 'normal' END,'policy',policy);
|
|
976
|
+
RETURN resolved_actor;
|
|
977
|
+
END $$;
|
|
978
|
+
|
|
979
|
+
-- Read one exact active note through its existing task-tree authority. The
|
|
980
|
+
-- temporary read capability is removed before returning retained source bytes.
|
|
981
|
+
CREATE FUNCTION knowledge_task_note_source(p_account uuid,p_workspace uuid,p_actor jsonb,p_note uuid,p_version integer)
|
|
982
|
+
RETURNS jsonb LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
|
983
|
+
DECLARE authority record; note task_notes%ROWTYPE; capability uuid:=gen_random_uuid();
|
|
984
|
+
previous text:=current_setting('opengeni.task_note_write_capability',true);
|
|
985
|
+
BEGIN
|
|
986
|
+
IF p_actor->>'kind'<>'agent' OR p_version IS DISTINCT FROM 1 THEN
|
|
987
|
+
RAISE EXCEPTION 'Task-note promotion requires an exact agent and note version' USING ERRCODE='42501'; END IF;
|
|
988
|
+
SELECT * INTO STRICT authority FROM resolve_task_note_attempt_authority(p_account,p_workspace,
|
|
989
|
+
(p_actor->>'sessionId')::uuid,(p_actor->>'turnId')::uuid,(p_actor->>'attemptId')::uuid,(p_actor->>'executionGeneration')::integer);
|
|
990
|
+
INSERT INTO task_note_write_capabilities(backend_pid,transaction_id,capability_id)
|
|
991
|
+
VALUES(pg_backend_pid(),pg_current_xact_id(),capability);
|
|
992
|
+
PERFORM set_config('opengeni.task_note_write_capability',capability::text,true);
|
|
993
|
+
SELECT * INTO note FROM task_notes n WHERE n.account_id=p_account AND n.workspace_id=p_workspace
|
|
994
|
+
AND n.root_session_id=authority.root_session_id AND n.id=p_note AND n.version=p_version
|
|
995
|
+
AND n.status='active' AND n.expires_at>statement_timestamp() FOR SHARE;
|
|
996
|
+
DELETE FROM task_note_write_capabilities c WHERE c.backend_pid=pg_backend_pid() AND c.transaction_id=pg_current_xact_id()
|
|
997
|
+
AND c.capability_id=capability;
|
|
998
|
+
PERFORM set_config('opengeni.task_note_write_capability',coalesce(previous,''),true);
|
|
999
|
+
IF note.id IS NULL THEN RAISE EXCEPTION 'Task note is expired, archived or outside this task tree' USING ERRCODE='42501'; END IF;
|
|
1000
|
+
RETURN jsonb_build_object('content',note.text,'source',jsonb_build_object('kind','task_note','noteId',note.id,
|
|
1001
|
+
'sessionId',note.created_by_session_id,'version',note.version::text));
|
|
1002
|
+
EXCEPTION WHEN OTHERS THEN
|
|
1003
|
+
PERFORM set_config('opengeni.task_note_write_capability',coalesce(previous,''),true);
|
|
1004
|
+
RAISE;
|
|
1005
|
+
END $$;
|
|
1006
|
+
|
|
1007
|
+
CREATE FUNCTION knowledge_entry_apply(p_account uuid,p_workspace uuid,p_actor jsonb,p_request jsonb)
|
|
1008
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT AS $$
|
|
1009
|
+
<<knowledge_entry_apply>>
|
|
1010
|
+
DECLARE actor jsonb; e knowledge_entries%ROWTYPE; r knowledge_entry_revisions%ROWTYPE;
|
|
1011
|
+
prior knowledge_entry_operations%ROWTYPE; body jsonb; codec integer; scope_value text;
|
|
1012
|
+
operation text:=p_request->>'operation'; operation_id uuid:=(p_request->>'operationId')::uuid;
|
|
1013
|
+
entry_id uuid:=(p_request->>'entryId')::uuid; revision_id uuid; old_revision uuid;
|
|
1014
|
+
batch_id uuid; owner_key text; disposition text; fingerprint text; result jsonb; next_number integer;
|
|
1015
|
+
mode text:='automatic'; restored_id uuid; search_text text;
|
|
1016
|
+
BEGIN
|
|
1017
|
+
IF operation IS NULL OR operation NOT IN ('save','promote_note','approve','approve_edit','reject','restore','archive')
|
|
1018
|
+
OR operation_id IS NULL OR entry_id IS NULL OR (p_request->>'expectedVersion')::integer<0 THEN
|
|
1019
|
+
RAISE EXCEPTION 'Invalid Knowledge operation' USING ERRCODE='22023';
|
|
1020
|
+
END IF;
|
|
1021
|
+
actor:=knowledge_resolve_actor(p_account,p_workspace,p_actor);
|
|
1022
|
+
IF actor->>'kind'='agent' AND operation IN ('approve','approve_edit','reject') THEN
|
|
1023
|
+
RAISE EXCEPTION 'Agents cannot approve their own Knowledge' USING ERRCODE='42501';
|
|
1024
|
+
END IF;
|
|
1025
|
+
IF operation IN ('approve','approve_edit','reject') AND actor->>'review' IS DISTINCT FROM 'true' THEN
|
|
1026
|
+
RAISE EXCEPTION 'Knowledge review permission required' USING ERRCODE='42501';
|
|
1027
|
+
END IF;
|
|
1028
|
+
-- Serialize publications and reference validation within an organization.
|
|
1029
|
+
-- No organization row is locked after a workspace: this key has no FK path.
|
|
1030
|
+
PERFORM pg_advisory_xact_lock(hashtextextended('knowledge-publication:'||p_account,0));
|
|
1031
|
+
-- Authenticate the live attempt above, but bind retries to logical work.
|
|
1032
|
+
-- Recovery changes the physical attempt, never the operation's session/turn.
|
|
1033
|
+
fingerprint:=encode(sha256(convert_to(jsonb_build_array(
|
|
1034
|
+
CASE WHEN p_actor->>'kind'='agent' THEN p_actor-'attemptId'-'executionGeneration' ELSE p_actor END,
|
|
1035
|
+
p_request)::text,'UTF8')),'hex');
|
|
1036
|
+
SELECT * INTO prior FROM knowledge_entry_operations o WHERE o.account_id=p_account AND o.operation_id=knowledge_entry_apply.operation_id;
|
|
1037
|
+
IF FOUND THEN
|
|
1038
|
+
IF prior.request_hash<>fingerprint OR prior.origin_workspace_id<>p_workspace THEN
|
|
1039
|
+
RAISE EXCEPTION 'Knowledge operation key reused' USING ERRCODE='23505';
|
|
1040
|
+
END IF;
|
|
1041
|
+
SELECT * INTO e FROM knowledge_entries h WHERE h.account_id=p_account AND h.id=prior.entry_id;
|
|
1042
|
+
IF NOT knowledge_scope_visible(e) THEN RAISE EXCEPTION 'Knowledge unavailable' USING ERRCODE='42501'; END IF;
|
|
1043
|
+
RETURN prior.receipt||jsonb_build_object('replayed',true);
|
|
1044
|
+
END IF;
|
|
1045
|
+
SELECT * INTO e FROM knowledge_entries h WHERE h.account_id=p_account AND h.id=entry_id FOR UPDATE;
|
|
1046
|
+
scope_value:=coalesce(e.scope,p_request->>'scope',actor->>'defaultScope');
|
|
1047
|
+
IF e.id IS NOT NULL AND (NOT knowledge_scope_visible(e)
|
|
1048
|
+
OR (p_request ? 'scope' AND p_request->>'scope'<>e.scope)) THEN
|
|
1049
|
+
RAISE EXCEPTION 'Knowledge scope is unavailable or immutable' USING ERRCODE='42501';
|
|
1050
|
+
END IF;
|
|
1051
|
+
IF actor->>'kind' IN ('agent','source_preparation') THEN
|
|
1052
|
+
IF actor->>'kind'='source_preparation' AND (operation<>'save' OR entry_id IS DISTINCT FROM (actor->>'entryId')::uuid
|
|
1053
|
+
OR p_request#>>'{entry,kind}' IS DISTINCT FROM 'source' OR p_request#>>'{entry,source,documentId}' IS DISTINCT FROM e.legacy_document_id::text) THEN
|
|
1054
|
+
RAISE EXCEPTION 'Source preparation is limited to its retained original' USING ERRCODE='42501'; END IF;
|
|
1055
|
+
IF scope_value IS DISTINCT FROM actor->>'defaultScope' OR (actor->>'retentionScope'='session' AND e.id IS NOT NULL
|
|
1056
|
+
AND (e.legacy_scope_type IS DISTINCT FROM 'session' OR e.legacy_scope_session_id IS DISTINCT FROM (actor->>'sessionId')::uuid)) THEN
|
|
1057
|
+
RAISE EXCEPTION 'Agent Knowledge stays in its accepted scope' USING ERRCODE='42501';
|
|
1058
|
+
END IF;
|
|
1059
|
+
mode:=actor#>>'{policy,effective,knowledge}';
|
|
1060
|
+
IF mode IS NULL OR mode NOT IN ('automatic','review_first') THEN
|
|
1061
|
+
RAISE EXCEPTION 'Knowledge learning is Off for this task' USING ERRCODE='42501';
|
|
1062
|
+
END IF;
|
|
1063
|
+
ELSIF NOT coalesce((actor->'writeScopes') ? scope_value,false) THEN
|
|
1064
|
+
RAISE EXCEPTION 'Knowledge write permission required for this scope' USING ERRCODE='42501';
|
|
1065
|
+
END IF;
|
|
1066
|
+
IF e.id IS NULL THEN
|
|
1067
|
+
IF operation NOT IN ('save','promote_note') OR (p_request->>'expectedVersion')::integer IS DISTINCT FROM 0 THEN
|
|
1068
|
+
RAISE EXCEPTION 'Knowledge entry changed or unavailable' USING ERRCODE='40001';
|
|
1069
|
+
END IF;
|
|
1070
|
+
INSERT INTO knowledge_entries(id,account_id,origin_workspace_id,scope,scope_workspace_id,scope_subject_id,legacy_scope_type,legacy_scope_session_id)
|
|
1071
|
+
VALUES(entry_id,p_account,p_workspace,scope_value,CASE WHEN scope_value='workspace' THEN p_workspace END,
|
|
1072
|
+
CASE WHEN scope_value='personal' THEN actor->>'subjectId' END,
|
|
1073
|
+
CASE WHEN actor->>'retentionScope'='session' THEN 'session' END,
|
|
1074
|
+
CASE WHEN actor->>'retentionScope'='session' THEN (actor->>'sessionId')::uuid END) RETURNING * INTO e;
|
|
1075
|
+
ELSIF e.version IS DISTINCT FROM (p_request->>'expectedVersion')::integer THEN
|
|
1076
|
+
RAISE EXCEPTION 'Knowledge entry changed; read its current revision' USING ERRCODE='40001';
|
|
1077
|
+
END IF;
|
|
1078
|
+
IF e.archived AND operation<>'restore' THEN
|
|
1079
|
+
RAISE EXCEPTION 'Restore archived Knowledge before editing it' USING ERRCODE='22023';
|
|
1080
|
+
END IF;
|
|
1081
|
+
old_revision:=e.published_revision_id;
|
|
1082
|
+
disposition:=CASE WHEN mode='review_first' THEN 'pending' ELSE 'published' END;
|
|
1083
|
+
IF operation IN ('approve','approve_edit','reject','restore','archive') THEN
|
|
1084
|
+
SELECT * INTO r FROM knowledge_entry_revisions v WHERE v.account_id=p_account AND v.entry_id=e.id
|
|
1085
|
+
AND v.id=CASE WHEN operation='archive' THEN e.latest_revision_id ELSE (p_request->>'revisionId')::uuid END;
|
|
1086
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'Knowledge revision unavailable' USING ERRCODE='42501'; END IF;
|
|
1087
|
+
IF operation IN ('approve','approve_edit','reject') AND (r.id IS DISTINCT FROM e.latest_revision_id
|
|
1088
|
+
OR NOT EXISTS(SELECT 1 FROM knowledge_entry_decisions d WHERE d.account_id=p_account AND d.entry_id=e.id
|
|
1089
|
+
AND d.revision_id=r.id AND d.version=e.version AND d.outcome='pending')) THEN
|
|
1090
|
+
RAISE EXCEPTION 'This Knowledge review is no longer current' USING ERRCODE='40001';
|
|
1091
|
+
END IF;
|
|
1092
|
+
body:=r.body; codec:=r.body_codec_version;
|
|
1093
|
+
revision_id:=r.id; batch_id:=r.review_batch_id;
|
|
1094
|
+
IF operation IN ('restore','archive') THEN restored_id:=r.id; END IF;
|
|
1095
|
+
IF operation='approve_edit' THEN
|
|
1096
|
+
IF r.change_kind='archive' THEN RAISE EXCEPTION 'An archive proposal has no editable content' USING ERRCODE='22023'; END IF;
|
|
1097
|
+
body:=p_request->'entry'; codec:=(p_request->>'codecVersion')::integer;
|
|
1098
|
+
END IF;
|
|
1099
|
+
ELSIF operation='save' THEN
|
|
1100
|
+
body:=p_request->'entry'; codec:=(p_request->>'codecVersion')::integer;
|
|
1101
|
+
ELSIF operation='promote_note' THEN
|
|
1102
|
+
body:=knowledge_task_note_source(p_account,p_workspace,p_actor,(p_request->>'noteId')::uuid,
|
|
1103
|
+
(p_request->>'expectedNoteVersion')::integer)||jsonb_build_object('kind','note','title',p_request->>'title',
|
|
1104
|
+
'evidence','[]'::jsonb,'relationships','[]'::jsonb,'groupIds',p_request->'groupIds');
|
|
1105
|
+
codec:=NULL;
|
|
1106
|
+
END IF;
|
|
1107
|
+
|
|
1108
|
+
IF operation IN ('save','promote_note','approve_edit','restore','archive') THEN
|
|
1109
|
+
IF jsonb_typeof(body) IS DISTINCT FROM 'object' OR jsonb_typeof(body->'title') IS DISTINCT FROM 'string'
|
|
1110
|
+
OR jsonb_typeof(body->'content') IS DISTINCT FROM 'string'
|
|
1111
|
+
OR body->>'kind' NOT IN ('source','fact','decision','requirement','incident','note','group')
|
|
1112
|
+
OR body->>'kind' IS NULL OR length(body->>'title') NOT BETWEEN 1 AND 8192
|
|
1113
|
+
OR (body->>'kind' NOT IN ('group','source') AND length(btrim(body->>'content'))=0)
|
|
1114
|
+
OR (body->>'kind'='source' AND jsonb_typeof(body->'source') IS DISTINCT FROM 'object')
|
|
1115
|
+
OR jsonb_typeof(body->'evidence') IS DISTINCT FROM 'array' OR (operation IN ('save','promote_note','approve_edit') AND jsonb_array_length(body->'evidence')>256)
|
|
1116
|
+
OR jsonb_typeof(body->'groupIds') IS DISTINCT FROM 'array' OR (operation IN ('save','promote_note','approve_edit') AND jsonb_array_length(body->'groupIds')>256)
|
|
1117
|
+
OR jsonb_typeof(body->'relationships') IS DISTINCT FROM 'array' OR (operation IN ('save','promote_note','approve_edit') AND jsonb_array_length(body->'relationships')>256)
|
|
1118
|
+
OR (codec IS NOT NULL AND codec<>1) THEN
|
|
1119
|
+
RAISE EXCEPTION 'Invalid Knowledge entry content' USING ERRCODE='22023';
|
|
1120
|
+
END IF;
|
|
1121
|
+
IF EXISTS(SELECT 1 FROM knowledge_entry_revisions v WHERE v.account_id=p_account AND v.id=e.latest_revision_id
|
|
1122
|
+
AND (v.body->>'kind'='group')<>(knowledge_entry_apply.body->>'kind'='group')) THEN
|
|
1123
|
+
RAISE EXCEPTION 'A Knowledge group cannot become a content entry' USING ERRCODE='22023';
|
|
1124
|
+
END IF;
|
|
1125
|
+
IF disposition='pending' THEN
|
|
1126
|
+
owner_key:=CASE scope_value WHEN 'personal' THEN 'personal:'||e.scope_subject_id
|
|
1127
|
+
WHEN 'organization' THEN 'organization:'||p_account ELSE 'workspace:'||e.scope_workspace_id END;
|
|
1128
|
+
SELECT id INTO batch_id FROM knowledge_review_batches b WHERE b.account_id=p_account AND b.owner_key=knowledge_entry_apply.owner_key
|
|
1129
|
+
AND ((actor->>'scheduledTaskRunId' IS NOT NULL AND b.scheduled_task_run_id=(actor->>'scheduledTaskRunId')::uuid)
|
|
1130
|
+
OR (actor->>'scheduledTaskRunId' IS NULL AND b.turn_id=(actor->>'turnId')::uuid));
|
|
1131
|
+
IF batch_id IS NULL THEN
|
|
1132
|
+
INSERT INTO knowledge_review_batches(account_id,origin_workspace_id,owner_key,session_id,turn_id,scheduled_task_run_id)
|
|
1133
|
+
VALUES(p_account,p_workspace,owner_key,(actor->>'sessionId')::uuid,(actor->>'turnId')::uuid,
|
|
1134
|
+
(actor->>'scheduledTaskRunId')::uuid) RETURNING id INTO batch_id;
|
|
1135
|
+
END IF;
|
|
1136
|
+
ELSE batch_id:=NULL; END IF;
|
|
1137
|
+
IF operation<>'archive' THEN
|
|
1138
|
+
PERFORM knowledge_validate_source(p_account,e.id,body);
|
|
1139
|
+
PERFORM knowledge_validate_links(p_account,e.id,body,batch_id);
|
|
1140
|
+
END IF;
|
|
1141
|
+
SELECT coalesce(max(v.number),0)+1 INTO next_number FROM knowledge_entry_revisions v WHERE v.account_id=p_account AND v.entry_id=e.id;
|
|
1142
|
+
INSERT INTO knowledge_entry_revisions(account_id,entry_id,number,body,body_codec_version,preview,preview_codec_version,
|
|
1143
|
+
previous_revision_id,restored_from_revision_id,actor,created_by_session_id,created_by_turn_id,review_batch_id,change_kind)
|
|
1144
|
+
VALUES(p_account,e.id,next_number,body,codec,coalesce(p_request->>'preview',r.preview,substring(body->>'content' FROM 1 FOR 512)),
|
|
1145
|
+
CASE WHEN p_request ? 'preview' THEN 1 ELSE r.preview_codec_version END,e.latest_revision_id,restored_id,p_actor,
|
|
1146
|
+
(actor->>'sessionId')::uuid,(actor->>'turnId')::uuid,batch_id,
|
|
1147
|
+
CASE WHEN operation='archive' THEN 'archive' ELSE 'upsert' END) RETURNING id INTO revision_id;
|
|
1148
|
+
INSERT INTO knowledge_entry_links(account_id,entry_id,revision_id,ordinal,target_entry_id,target_revision_id,relation)
|
|
1149
|
+
SELECT p_account,e.id,revision_id,(row_number() OVER ())::integer-1,links.target,links.rev,links.relation
|
|
1150
|
+
FROM (
|
|
1151
|
+
SELECT (x->>'entryId')::uuid AS target,(x->>'revisionId')::uuid AS rev,'evidence' AS relation
|
|
1152
|
+
FROM jsonb_array_elements(body->'evidence') x
|
|
1153
|
+
UNION ALL SELECT x::uuid,NULL,'group' FROM jsonb_array_elements_text(body->'groupIds') x
|
|
1154
|
+
UNION ALL SELECT (x->>'entryId')::uuid,NULL,x->>'relation' FROM jsonb_array_elements(body->'relationships') x
|
|
1155
|
+
) links;
|
|
1156
|
+
IF operation='archive' AND disposition='published' THEN disposition:='archived'; END IF;
|
|
1157
|
+
ELSIF operation='approve' THEN
|
|
1158
|
+
IF r.change_kind='archive' THEN disposition:='archived';
|
|
1159
|
+
ELSE
|
|
1160
|
+
PERFORM knowledge_validate_source(p_account,e.id,body);
|
|
1161
|
+
PERFORM knowledge_validate_links(p_account,e.id,body,NULL);
|
|
1162
|
+
disposition:='published';
|
|
1163
|
+
END IF;
|
|
1164
|
+
ELSIF operation='reject' THEN disposition:='rejected';
|
|
1165
|
+
END IF;
|
|
1166
|
+
UPDATE knowledge_entries h SET version=e.version+1,latest_revision_id=revision_id,
|
|
1167
|
+
published_revision_id=CASE WHEN disposition='published' THEN revision_id ELSE h.published_revision_id END,
|
|
1168
|
+
archived=CASE WHEN disposition='archived' THEN true WHEN operation='restore' AND disposition='published' THEN false ELSE h.archived END,
|
|
1169
|
+
updated_at=clock_timestamp() WHERE h.account_id=p_account AND h.id=e.id;
|
|
1170
|
+
INSERT INTO knowledge_entry_decisions(account_id,entry_id,revision_id,version,outcome,actor,policy_snapshot)
|
|
1171
|
+
VALUES(p_account,e.id,revision_id,e.version+1,disposition,p_actor,actor->'policy');
|
|
1172
|
+
IF operation IN ('save','promote_note','approve_edit','restore','archive') THEN
|
|
1173
|
+
-- The adapter supplies a lossless-text search projection; it is not evidence.
|
|
1174
|
+
search_text:=coalesce(p_request->>'searchText',(body->>'title')||' '||(body->>'content'));
|
|
1175
|
+
IF restored_id IS NOT NULL THEN
|
|
1176
|
+
INSERT INTO knowledge_entry_search(account_id,entry_id,revision_id,chunk_index,search_vector)
|
|
1177
|
+
SELECT p_account,e.id,knowledge_entry_apply.revision_id,v.chunk_index,v.search_vector FROM knowledge_entry_search v
|
|
1178
|
+
WHERE v.account_id=p_account AND v.entry_id=e.id AND v.revision_id=restored_id;
|
|
1179
|
+
ELSE
|
|
1180
|
+
PERFORM knowledge_index_revision(p_account,e.id,revision_id,search_text);
|
|
1181
|
+
END IF;
|
|
1182
|
+
END IF;
|
|
1183
|
+
result:=jsonb_build_object('operationId',operation_id,'entryId',e.id,'revisionId',revision_id,
|
|
1184
|
+
'version',e.version+1,'outcome',disposition,'reviewBatchId',batch_id,'replayed',false);
|
|
1185
|
+
INSERT INTO knowledge_entry_operations(account_id,origin_workspace_id,operation_id,entry_id,actor,request_hash,receipt)
|
|
1186
|
+
VALUES(p_account,p_workspace,operation_id,e.id,p_actor,fingerprint,result);
|
|
1187
|
+
RETURN result;
|
|
1188
|
+
END $$;
|
|
1189
|
+
|
|
1190
|
+
CREATE FUNCTION knowledge_entry_read(p_account uuid,p_workspace uuid,p_actor jsonb,p_request jsonb)
|
|
1191
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT AS $$
|
|
1192
|
+
<<knowledge_entry_read>>
|
|
1193
|
+
DECLARE actor jsonb; result jsonb; pending boolean:=p_request->>'view'='needs_review';
|
|
1194
|
+
operation text:=coalesce(p_request->>'operation','list'); take integer:=coalesce((p_request->>'limit')::integer,20);
|
|
1195
|
+
query_embedding vector:=CASE WHEN jsonb_typeof(p_request->'embedding')='array' THEN (p_request->'embedding')::text::vector END;
|
|
1196
|
+
BEGIN
|
|
1197
|
+
actor:=knowledge_resolve_actor(p_account,p_workspace,p_actor);
|
|
1198
|
+
pending:=coalesce(pending,false);
|
|
1199
|
+
IF operation='policy' AND actor->>'kind'='agent' THEN RETURN actor->'policy'; END IF;
|
|
1200
|
+
IF take NOT BETWEEN 1 AND 50 THEN RAISE EXCEPTION 'Invalid Knowledge page size' USING ERRCODE='22023'; END IF;
|
|
1201
|
+
IF operation IN ('source_list','source_read') THEN
|
|
1202
|
+
IF actor->>'kind'<>'agent' OR actor#>>'{policy,scheduledTaskRunId}' IS NULL THEN
|
|
1203
|
+
RAISE EXCEPTION 'Source content requires its accepted agent run' USING ERRCODE='42501'; END IF;
|
|
1204
|
+
SELECT coalesce(jsonb_agg(value ORDER BY id),'[]'::jsonb) INTO result FROM (
|
|
1205
|
+
SELECT e.id,jsonb_build_object('entryId',e.id,'revisionId',r.id,'title',r.body->'title',
|
|
1206
|
+
'codecVersion',r.body_codec_version,'pending',r.id IS DISTINCT FROM e.published_revision_id)
|
|
1207
|
+
|| CASE WHEN operation='source_read' THEN jsonb_build_object('content',r.body->'content') ELSE '{}'::jsonb END AS value
|
|
1208
|
+
FROM scheduled_task_runs run
|
|
1209
|
+
JOIN knowledge_document_versions v ON v.account_id=run.account_id
|
|
1210
|
+
AND v.source_id=(run.accepted_execution_snapshot#>>'{task,agentConfig,knowledgeSource,sourceId}')::uuid
|
|
1211
|
+
JOIN knowledge_entries e ON e.account_id=v.account_id AND e.legacy_document_version_id=v.id
|
|
1212
|
+
JOIN knowledge_entry_revisions r ON r.account_id=e.account_id AND r.entry_id=e.id
|
|
1213
|
+
AND r.id=coalesce(CASE WHEN EXISTS(SELECT 1 FROM knowledge_review_batches b
|
|
1214
|
+
JOIN knowledge_entry_revisions candidate ON candidate.account_id=b.account_id AND candidate.review_batch_id=b.id
|
|
1215
|
+
JOIN knowledge_entry_decisions d ON d.account_id=e.account_id AND d.entry_id=e.id
|
|
1216
|
+
AND d.revision_id=candidate.id AND d.version=e.version AND d.outcome='pending'
|
|
1217
|
+
WHERE b.account_id=e.account_id AND b.scheduled_task_run_id=run.id AND candidate.id=e.latest_revision_id)
|
|
1218
|
+
THEN e.latest_revision_id END,e.published_revision_id)
|
|
1219
|
+
WHERE run.account_id=p_account AND run.workspace_id=p_workspace
|
|
1220
|
+
AND run.id=(knowledge_entry_read.actor#>>'{policy,scheduledTaskRunId}')::uuid
|
|
1221
|
+
AND run.action_kind='agent_turn' AND NOT e.archived AND knowledge_scope_visible(e)
|
|
1222
|
+
AND (operation='source_read' OR (r.created_at>=run.created_at
|
|
1223
|
+
AND e.document_preparation->>'scheduledTaskRunId'=run.id::text))
|
|
1224
|
+
AND knowledge_revision_visible(p_account,e.id,r.id,true)
|
|
1225
|
+
AND (NOT(p_request ? 'afterId') OR e.id>(p_request->>'afterId')::uuid)
|
|
1226
|
+
AND (operation<>'source_read' OR e.id=(p_request->>'entryId')::uuid)
|
|
1227
|
+
ORDER BY e.id LIMIT CASE WHEN operation='source_read' THEN 1 ELSE take+1 END
|
|
1228
|
+
) page;
|
|
1229
|
+
RETURN result;
|
|
1230
|
+
END IF;
|
|
1231
|
+
|
|
1232
|
+
IF (operation='history' OR p_request->>'view'='rejected')
|
|
1233
|
+
AND (actor->>'kind'<>'human' OR actor->>'review' IS DISTINCT FROM 'true') THEN
|
|
1234
|
+
RAISE EXCEPTION 'Knowledge history requires human review access' USING ERRCODE='42501';
|
|
1235
|
+
END IF;
|
|
1236
|
+
IF pending AND actor->>'kind'<>'agent'
|
|
1237
|
+
AND (actor->>'kind'<>'human' OR actor->>'review' IS DISTINCT FROM 'true') THEN
|
|
1238
|
+
RAISE EXCEPTION 'Pending Knowledge requires a live agent or human reviewer' USING ERRCODE='42501';
|
|
1239
|
+
END IF;
|
|
1240
|
+
IF p_request->>'view'='archived' AND actor->>'kind'<>'human' THEN
|
|
1241
|
+
RAISE EXCEPTION 'Archived Knowledge is a human management view' USING ERRCODE='42501';
|
|
1242
|
+
END IF;
|
|
1243
|
+
IF operation='review_batches' THEN
|
|
1244
|
+
IF actor->>'kind'<>'human' OR actor->>'review' IS DISTINCT FROM 'true' THEN
|
|
1245
|
+
RAISE EXCEPTION 'Knowledge review groups require human review access' USING ERRCODE='42501'; END IF;
|
|
1246
|
+
WITH visible AS MATERIALIZED (
|
|
1247
|
+
SELECT b.id,e.scope,b.session_id,b.scheduled_task_run_id,b.created_at,e.id AS entry_id
|
|
1248
|
+
FROM knowledge_entries e JOIN knowledge_entry_revisions r
|
|
1249
|
+
ON r.account_id=e.account_id AND r.entry_id=e.id AND r.id=e.latest_revision_id
|
|
1250
|
+
JOIN knowledge_review_batches b ON b.account_id=e.account_id AND b.id=r.review_batch_id
|
|
1251
|
+
JOIN LATERAL (SELECT d.outcome FROM knowledge_entry_decisions d WHERE d.account_id=e.account_id
|
|
1252
|
+
AND d.entry_id=e.id AND d.revision_id=r.id ORDER BY d.version DESC LIMIT 1) decision ON true
|
|
1253
|
+
WHERE e.account_id=p_account AND decision.outcome='pending' AND knowledge_scope_visible(e)
|
|
1254
|
+
AND (knowledge_entry_read.actor->'writeScopes') ? e.scope AND knowledge_revision_visible(p_account,e.id,r.id,true)
|
|
1255
|
+
AND (NOT(p_request ? 'scope') OR e.scope=p_request->>'scope')
|
|
1256
|
+
), batches AS (
|
|
1257
|
+
SELECT id,scope,session_id,scheduled_task_run_id,created_at,count(*) AS pending_count FROM visible
|
|
1258
|
+
GROUP BY id,scope,session_id,scheduled_task_run_id,created_at
|
|
1259
|
+
HAVING NOT(p_request ? 'afterId') OR id>(p_request->>'afterId')::uuid
|
|
1260
|
+
ORDER BY id LIMIT take+1
|
|
1261
|
+
) SELECT coalesce(jsonb_agg(jsonb_build_object('id',b.id,'scope',b.scope,'pendingCount',b.pending_count,
|
|
1262
|
+
'createdAt',b.created_at,'sessionId',s.id,'scheduledTaskId',task.id,'scheduledTaskRunId',run.id,
|
|
1263
|
+
'title',coalesce(task.name,s.title)) ORDER BY b.id),'[]'::jsonb) INTO result
|
|
1264
|
+
FROM batches b LEFT JOIN sessions s ON s.account_id=p_account AND s.id=b.session_id
|
|
1265
|
+
LEFT JOIN scheduled_task_runs run ON run.account_id=p_account AND run.id=b.scheduled_task_run_id
|
|
1266
|
+
LEFT JOIN scheduled_tasks task ON task.account_id=p_account AND task.id=run.task_id;
|
|
1267
|
+
RETURN result;
|
|
1268
|
+
END IF;
|
|
1269
|
+
IF operation NOT IN ('list','get','history','file') OR (operation='file' AND NOT(p_request ? 'entryId')) THEN RAISE EXCEPTION 'Invalid Knowledge read' USING ERRCODE='22023'; END IF;
|
|
1270
|
+
-- Materialize access-filtered records before matching, ranking or pagination.
|
|
1271
|
+
WITH candidates AS MATERIALIZED (
|
|
1272
|
+
SELECT e.*,r.id AS revision_id,r.number,r.change_kind,r.body,r.body_codec_version,r.preview,r.preview_codec_version,
|
|
1273
|
+
r.previous_revision_id,r.restored_from_revision_id,r.created_by_session_id,r.review_batch_id,r.created_at AS revision_created_at,
|
|
1274
|
+
CASE WHEN r.id=e.published_revision_id THEN 'published'
|
|
1275
|
+
WHEN d.outcome='pending' AND r.id=e.latest_revision_id THEN 'pending'
|
|
1276
|
+
WHEN d.outcome='rejected' THEN 'rejected' ELSE 'superseded' END AS outcome
|
|
1277
|
+
FROM knowledge_entries e
|
|
1278
|
+
JOIN knowledge_entry_revisions r ON r.account_id=e.account_id AND r.entry_id=e.id
|
|
1279
|
+
AND (operation='history' OR r.id=CASE WHEN p_request ? 'revisionId' THEN (p_request->>'revisionId')::uuid
|
|
1280
|
+
WHEN pending THEN e.latest_revision_id
|
|
1281
|
+
WHEN p_request->>'view'='rejected' THEN e.latest_revision_id
|
|
1282
|
+
WHEN p_request->>'view'='archived' THEN coalesce(e.published_revision_id,e.latest_revision_id)
|
|
1283
|
+
ELSE e.published_revision_id END)
|
|
1284
|
+
LEFT JOIN LATERAL (SELECT outcome FROM knowledge_entry_decisions v WHERE v.account_id=e.account_id
|
|
1285
|
+
AND v.entry_id=e.id AND v.revision_id=r.id ORDER BY v.version DESC LIMIT 1) d ON true
|
|
1286
|
+
WHERE e.account_id=p_account AND knowledge_scope_visible(e)
|
|
1287
|
+
AND (operation='history' OR (p_request ? 'revisionId' AND knowledge_entry_read.actor->>'kind'='human' AND knowledge_entry_read.actor->>'review'='true')
|
|
1288
|
+
OR e.archived=coalesce(p_request->>'view'='archived',false))
|
|
1289
|
+
AND (NOT pending OR (d.outcome='pending' AND r.id=e.latest_revision_id
|
|
1290
|
+
AND (knowledge_entry_read.actor->>'kind'='agent' OR (knowledge_entry_read.actor->'writeScopes') ? e.scope)))
|
|
1291
|
+
AND (p_request->>'view' IS DISTINCT FROM 'rejected' OR d.outcome='rejected')
|
|
1292
|
+
AND (NOT(p_request ? 'scope') OR e.scope=p_request->>'scope')
|
|
1293
|
+
AND (NOT(p_request ? 'entryId') OR e.id=(p_request->>'entryId')::uuid)
|
|
1294
|
+
AND (NOT(p_request ? 'kind') OR r.body->>'kind'=p_request->>'kind')
|
|
1295
|
+
AND (NOT(p_request ? 'fileId') OR r.body#>>'{source,fileId}'=p_request->>'fileId'
|
|
1296
|
+
OR EXISTS(SELECT 1 FROM knowledge_entry_links l
|
|
1297
|
+
JOIN knowledge_entry_revisions source_revision ON source_revision.account_id=l.account_id
|
|
1298
|
+
AND source_revision.entry_id=l.target_entry_id AND source_revision.id=l.target_revision_id
|
|
1299
|
+
WHERE l.account_id=e.account_id AND l.entry_id=e.id AND l.revision_id=r.id
|
|
1300
|
+
AND l.relation='evidence' AND source_revision.body#>>'{source,fileId}'=p_request->>'fileId'))
|
|
1301
|
+
AND (NOT(p_request ? 'sessionId') OR r.created_by_session_id=(p_request->>'sessionId')::uuid)
|
|
1302
|
+
AND (NOT(p_request ? 'reviewBatchId') OR r.review_batch_id=(p_request->>'reviewBatchId')::uuid)
|
|
1303
|
+
AND (NOT(p_request ? 'groupId') OR EXISTS(SELECT 1 FROM knowledge_entry_links l
|
|
1304
|
+
WHERE l.account_id=e.account_id AND l.entry_id=e.id AND l.revision_id=r.id
|
|
1305
|
+
AND l.relation='group' AND l.target_entry_id=(p_request->>'groupId')::uuid
|
|
1306
|
+
AND EXISTS(SELECT 1 FROM knowledge_entries g WHERE g.account_id=p_account AND g.id=l.target_entry_id
|
|
1307
|
+
AND NOT g.archived AND knowledge_revision_visible(p_account,g.id,
|
|
1308
|
+
CASE WHEN pending THEN g.latest_revision_id ELSE g.published_revision_id END,pending))))
|
|
1309
|
+
-- Filter before pagination. A hidden, archived, or out-of-scope parent must
|
|
1310
|
+
-- never strand an otherwise readable entry outside the tree.
|
|
1311
|
+
AND (NOT coalesce((p_request->>'rootOnly')::boolean,false) OR NOT EXISTS(
|
|
1312
|
+
SELECT 1 FROM knowledge_entry_links l
|
|
1313
|
+
JOIN knowledge_entries g ON g.account_id=l.account_id AND g.id=l.target_entry_id
|
|
1314
|
+
JOIN knowledge_entry_revisions gr ON gr.account_id=g.account_id AND gr.entry_id=g.id
|
|
1315
|
+
AND gr.id=CASE WHEN pending THEN g.latest_revision_id ELSE g.published_revision_id END
|
|
1316
|
+
WHERE l.account_id=e.account_id AND l.entry_id=e.id AND l.revision_id=r.id AND l.relation='group'
|
|
1317
|
+
AND NOT g.archived AND gr.body->>'kind'='group'
|
|
1318
|
+
AND (NOT(p_request ? 'scope') OR g.scope=p_request->>'scope')
|
|
1319
|
+
AND knowledge_revision_visible(p_account,g.id,gr.id,pending)))
|
|
1320
|
+
AND (NOT(p_request ? 'beforeRevision') OR r.number<(p_request->>'beforeRevision')::integer)
|
|
1321
|
+
AND (knowledge_entry_read.actor->>'kind'='human' AND knowledge_entry_read.actor->>'review'='true'
|
|
1322
|
+
OR (pending AND knowledge_entry_read.actor->>'kind'='agent')
|
|
1323
|
+
OR EXISTS(SELECT 1 FROM knowledge_entry_decisions v WHERE v.account_id=e.account_id
|
|
1324
|
+
AND v.entry_id=e.id AND v.revision_id=r.id AND v.outcome='published'))
|
|
1325
|
+
AND knowledge_revision_visible(p_account,e.id,r.id,
|
|
1326
|
+
pending OR (knowledge_entry_read.actor->>'kind'='human' AND knowledge_entry_read.actor->>'review'='true'))
|
|
1327
|
+
), scored AS (
|
|
1328
|
+
SELECT c.*,coalesce(lex.score,0) AS keyword_score,coalesce(semantic.similarity,0) AS similarity,
|
|
1329
|
+
CASE WHEN coalesce(p_request->>'query','')='' THEN 0
|
|
1330
|
+
WHEN coalesce(p_request->>'mode','hybrid')='vector' THEN coalesce(semantic.similarity,0)
|
|
1331
|
+
WHEN coalesce(p_request->>'mode','hybrid')='keyword' THEN coalesce(lex.score/(1+lex.score),0)
|
|
1332
|
+
ELSE coalesce(lex.score/(1+lex.score),0)+coalesce(semantic.similarity,0) END AS score,
|
|
1333
|
+
CASE WHEN semantic.keyword_match OR semantic.similarity>=0.2 THEN
|
|
1334
|
+
jsonb_build_array(jsonb_build_object('field',semantic.field,'start',semantic.start_offset,'end',semantic.end_offset,
|
|
1335
|
+
'text',semantic.text,'codecVersion',semantic.text_codec_version)) ELSE '[]'::jsonb END AS excerpts
|
|
1336
|
+
FROM candidates c
|
|
1337
|
+
LEFT JOIN LATERAL (
|
|
1338
|
+
SELECT max(ts_rank_cd(s.search_vector,websearch_to_tsquery('simple',p_request->>'query'))) AS score
|
|
1339
|
+
FROM knowledge_entry_search s WHERE s.account_id=p_account AND s.entry_id=c.id AND s.revision_id=c.revision_id
|
|
1340
|
+
AND coalesce(p_request->>'query','')<>''
|
|
1341
|
+
) lex ON true
|
|
1342
|
+
LEFT JOIN LATERAL (
|
|
1343
|
+
SELECT v.field,v.start_offset,v.end_offset,v.text,v.text_codec_version,
|
|
1344
|
+
v.keyword_match,max(v.similarity) OVER () AS similarity FROM (
|
|
1345
|
+
SELECT cache.*,
|
|
1346
|
+
to_tsvector('simple',cache.text)@@websearch_to_tsquery('simple',p_request->>'query') AS keyword_match,
|
|
1347
|
+
CASE WHEN query_embedding IS NOT NULL AND cache.dimensions=vector_dims(query_embedding)
|
|
1348
|
+
AND cache.model=p_request->>'embeddingModel' AND vector_norm(query_embedding)>0
|
|
1349
|
+
THEN greatest(0,least(1,1-(cache.embedding<=>query_embedding))) ELSE 0 END AS similarity
|
|
1350
|
+
FROM knowledge_entry_vectors cache JOIN knowledge_index_jobs job ON job.account_id=cache.account_id
|
|
1351
|
+
AND job.revision_id=cache.revision_id AND job.completed_generation=cache.generation
|
|
1352
|
+
WHERE cache.account_id=p_account AND cache.entry_id=c.id AND cache.revision_id=c.revision_id
|
|
1353
|
+
AND coalesce(p_request->>'query','')<>''
|
|
1354
|
+
) v ORDER BY CASE WHEN p_request->>'mode'='vector' THEN false ELSE v.keyword_match END DESC,v.similarity DESC,v.chunk_index LIMIT 1
|
|
1355
|
+
) semantic ON true
|
|
1356
|
+
), page AS (
|
|
1357
|
+
SELECT * FROM scored s WHERE (coalesce(p_request->>'query','')=''
|
|
1358
|
+
OR (coalesce(p_request->>'mode','hybrid')<>'vector' AND s.keyword_score>0)
|
|
1359
|
+
OR (coalesce(p_request->>'mode','hybrid')<>'keyword' AND s.similarity>=0.2))
|
|
1360
|
+
AND (NOT(p_request ? 'afterId') OR s.score<(p_request->>'afterScore')::double precision
|
|
1361
|
+
OR (s.score=(p_request->>'afterScore')::double precision AND s.id>(p_request->>'afterId')::uuid))
|
|
1362
|
+
ORDER BY s.score DESC,s.id,s.number DESC LIMIT take+1
|
|
1363
|
+
) SELECT coalesce(jsonb_agg(jsonb_build_object(
|
|
1364
|
+
'score',c.score,'excerpts',c.excerpts,'id',c.id,'scope',c.scope,'version',c.version,'publishedRevisionId',c.published_revision_id,
|
|
1365
|
+
'latestRevisionId',c.latest_revision_id,'archived',c.archived,'createdAt',c.created_at,'updatedAt',c.updated_at,
|
|
1366
|
+
'revision',jsonb_build_object('id',c.revision_id,'entryId',c.id,'number',c.number,
|
|
1367
|
+
'entry',CASE WHEN operation IN ('get','file') THEN knowledge_entry_visible_body(p_account,c.body,pending) ELSE NULL END,'change',c.change_kind,
|
|
1368
|
+
'title',c.body->'title','kind',c.body->>'kind','preview',c.preview,'groupIds',knowledge_entry_visible_body(p_account,c.body,pending)->'groupIds',
|
|
1369
|
+
'sourceKind',c.body#>>'{source,kind}','bodyCodecVersion',c.body_codec_version,'previewCodecVersion',c.preview_codec_version,
|
|
1370
|
+
'previousRevisionId',c.previous_revision_id,'restoredFromRevisionId',c.restored_from_revision_id,
|
|
1371
|
+
'createdAt',c.revision_created_at,'createdBySessionId',c.created_by_session_id,
|
|
1372
|
+
'reviewBatchId',c.review_batch_id,'outcome',c.outcome)
|
|
1373
|
+
) ORDER BY c.score DESC,c.id,c.number DESC),'[]'::jsonb) INTO result FROM page c;
|
|
1374
|
+
IF operation='file' THEN
|
|
1375
|
+
IF result#>>'{0,revision,entry,source,documentId}' IS NOT NULL THEN
|
|
1376
|
+
SELECT to_jsonb(f) INTO result FROM resolve_document_original_file(p_account,p_workspace,
|
|
1377
|
+
nullif(current_setting('opengeni.subject_id',true),''),(result#>>'{0,revision,entry,source,documentId}')::uuid) f;
|
|
1378
|
+
ELSE
|
|
1379
|
+
SELECT to_jsonb(f) INTO result FROM files f WHERE f.account_id=p_account AND f.status='ready'
|
|
1380
|
+
AND f.id=(result#>>'{0,revision,entry,source,fileId}')::uuid;
|
|
1381
|
+
END IF;
|
|
1382
|
+
END IF;
|
|
1383
|
+
RETURN result;
|
|
1384
|
+
END $$;
|
|
1385
|
+
|
|
1386
|
+
-- Source preparation is an exact-attempt operation on an existing original.
|
|
1387
|
+
-- The stable owner/file identity makes retries, children and scheduled re-reads
|
|
1388
|
+
-- converge. A rejected or archived source is never silently re-published.
|
|
1389
|
+
CREATE FUNCTION knowledge_entry_prepare_file(p_account uuid,p_workspace uuid,p_actor jsonb,p_request jsonb)
|
|
1390
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT AS $$
|
|
1391
|
+
DECLARE actor jsonb; f files%ROWTYPE; e knowledge_entries%ROWTYPE;
|
|
1392
|
+
entry_id uuid; identity_hash text; receipt jsonb; operation text:=p_request->>'operation';
|
|
1393
|
+
file_id uuid:=(p_request->>'fileId')::uuid; body jsonb;
|
|
1394
|
+
BEGIN
|
|
1395
|
+
actor:=knowledge_resolve_actor(p_account,p_workspace,p_actor);
|
|
1396
|
+
IF actor->>'kind' IS DISTINCT FROM 'agent' OR file_id IS NULL OR operation IS NULL
|
|
1397
|
+
OR operation NOT IN ('inspect','complete') THEN
|
|
1398
|
+
RAISE EXCEPTION 'Source preparation requires an exact agent attempt' USING ERRCODE='42501'; END IF;
|
|
1399
|
+
IF actor#>>'{policy,effective,knowledge}'='off' THEN RETURN jsonb_build_object('status','disabled','fileId',file_id); END IF;
|
|
1400
|
+
SELECT * INTO f FROM files WHERE account_id=p_account AND id=file_id AND status='ready'
|
|
1401
|
+
AND (workspace_id=p_workspace OR (actor->>'defaultScope'='personal' AND actor->>'subjectId'=ANY(private_owner_subject_ids)));
|
|
1402
|
+
IF NOT FOUND OR NOT knowledge_source_visible(p_account,jsonb_build_object('source',jsonb_build_object('fileId',file_id))) THEN
|
|
1403
|
+
RAISE EXCEPTION 'Source file unavailable' USING ERRCODE='42501'; END IF;
|
|
1404
|
+
identity_hash:=md5('prepared-knowledge-file:'||p_account||':'||file_id||':'||
|
|
1405
|
+
CASE WHEN actor->>'defaultScope'='personal' THEN 'personal:'||(actor->>'subjectId') ELSE 'workspace:'||p_workspace END||
|
|
1406
|
+
CASE WHEN actor->>'retentionScope'='session' THEN ':session:'||(actor->>'sessionId') ELSE '' END);
|
|
1407
|
+
entry_id:=(substring(identity_hash,1,8)||'-'||substring(identity_hash,9,4)||'-5'||substring(identity_hash,14,3)||'-a'||substring(identity_hash,18,3)||'-'||substring(identity_hash,21,12))::uuid;
|
|
1408
|
+
-- Do not keep a transaction open while a parser or object store is running.
|
|
1409
|
+
-- Complete rechecks both authority and identity under the publication lock.
|
|
1410
|
+
IF operation='complete' THEN
|
|
1411
|
+
PERFORM pg_advisory_xact_lock(hashtextextended('knowledge-publication:'||p_account,0));
|
|
1412
|
+
END IF;
|
|
1413
|
+
SELECT * INTO e FROM knowledge_entries WHERE account_id=p_account AND id=entry_id;
|
|
1414
|
+
IF FOUND THEN
|
|
1415
|
+
IF e.prepared_file_id IS DISTINCT FROM file_id OR NOT knowledge_scope_visible(e) THEN
|
|
1416
|
+
RAISE EXCEPTION 'Prepared source identity is unavailable' USING ERRCODE='42501'; END IF;
|
|
1417
|
+
SELECT jsonb_build_object('operationId',o.operation_id,'entryId',e.id,'revisionId',e.latest_revision_id,
|
|
1418
|
+
'version',e.version,'outcome',CASE WHEN e.archived THEN 'archived' ELSE d.outcome END,
|
|
1419
|
+
'reviewBatchId',r.review_batch_id,'replayed',true) INTO receipt
|
|
1420
|
+
FROM knowledge_entry_decisions d JOIN knowledge_entry_revisions r ON r.account_id=d.account_id AND r.id=d.revision_id
|
|
1421
|
+
JOIN knowledge_entry_operations o ON o.account_id=d.account_id AND o.entry_id=d.entry_id AND o.receipt->>'revisionId'=d.revision_id::text
|
|
1422
|
+
WHERE d.account_id=p_account AND d.entry_id=e.id AND d.version=e.version ORDER BY o.created_at DESC LIMIT 1;
|
|
1423
|
+
IF receipt IS NULL THEN RAISE EXCEPTION 'Prepared source receipt unavailable' USING ERRCODE='55000'; END IF;
|
|
1424
|
+
RETURN jsonb_build_object('status','retained','fileId',file_id,'filename',f.filename,'receipt',receipt);
|
|
1425
|
+
END IF;
|
|
1426
|
+
IF operation='inspect' THEN
|
|
1427
|
+
RETURN jsonb_build_object('status','prepare','fileId',file_id,'file',to_jsonb(f));
|
|
1428
|
+
END IF;
|
|
1429
|
+
IF jsonb_typeof(p_request->'content') IS DISTINCT FROM 'string' OR p_request->>'codecVersion' IS DISTINCT FROM '1' THEN
|
|
1430
|
+
RAISE EXCEPTION 'Invalid prepared source content' USING ERRCODE='22023'; END IF;
|
|
1431
|
+
body:=jsonb_build_object('title',p_request->'title','kind','source','content',p_request->'content',
|
|
1432
|
+
'source',jsonb_build_object('kind','file','fileId',f.id,'version',coalesce(p_request->>'sourceVersion',f.sha256,f.id::text),
|
|
1433
|
+
'retention','full_text','capturedAt',clock_timestamp()),'evidence','[]'::jsonb,'groupIds','[]'::jsonb,'relationships','[]'::jsonb);
|
|
1434
|
+
receipt:=knowledge_entry_apply(p_account,p_workspace,p_actor,jsonb_build_object('operation','save','operationId',gen_random_uuid(),
|
|
1435
|
+
'entryId',entry_id,'expectedVersion',0,'entry',body,'codecVersion',1,'preview',p_request->'preview','searchText',p_request->'searchText'));
|
|
1436
|
+
UPDATE knowledge_entries SET prepared_file_id=file_id WHERE account_id=p_account AND id=entry_id;
|
|
1437
|
+
RETURN jsonb_build_object('status','retained','fileId',file_id,'filename',f.filename,'receipt',receipt);
|
|
1438
|
+
END $$;
|
|
1439
|
+
|
|
1440
|
+
-- Compatibility adapter for existing Document indexing obligations. New chat
|
|
1441
|
+
-- ingestion uses exact agent attempts. This mechanical adapter can prepare only
|
|
1442
|
+
-- its retained original, under its existing document ownership and frozen task
|
|
1443
|
+
-- policy; it never gains human authority or invents agent findings.
|
|
1444
|
+
CREATE FUNCTION knowledge_document_prepare(p_account uuid,p_workspace uuid,p_document uuid,p_request jsonb)
|
|
1445
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT AS $$
|
|
1446
|
+
DECLARE document documents%ROWTYPE; entry knowledge_entries%ROWTYPE; original files%ROWTYPE;
|
|
1447
|
+
obligation knowledge_source_sync_index_obligations%ROWTYPE; scheduled scheduled_task_runs%ROWTYPE;
|
|
1448
|
+
prior knowledge_entry_revisions%ROWTYPE; lease uuid; policy jsonb; body jsonb; result jsonb; actor jsonb;
|
|
1449
|
+
operation text:=p_request->>'operation'; accepted_at timestamptz; fingerprint text;
|
|
1450
|
+
BEGIN
|
|
1451
|
+
IF p_account IS DISTINCT FROM opengeni_private.current_account_id()
|
|
1452
|
+
OR p_workspace IS DISTINCT FROM opengeni_private.current_workspace_id()
|
|
1453
|
+
OR operation IS NULL OR operation NOT IN ('claim','complete') THEN
|
|
1454
|
+
RAISE EXCEPTION 'Invalid document preparation authority' USING ERRCODE='42501'; END IF;
|
|
1455
|
+
-- Same publication lock order as ordinary Knowledge writes.
|
|
1456
|
+
PERFORM pg_advisory_xact_lock(hashtextextended('knowledge-publication:'||p_account,0));
|
|
1457
|
+
SELECT * INTO document FROM documents d WHERE d.account_id=p_account AND d.workspace_id=p_workspace
|
|
1458
|
+
AND d.id=p_document FOR UPDATE;
|
|
1459
|
+
IF NOT FOUND OR document.status NOT IN ('indexing','ready') OR document.file_id IS DISTINCT FROM (p_request->>'fileId')::uuid THEN
|
|
1460
|
+
RAISE EXCEPTION 'Document original changed or is unavailable' USING ERRCODE='42501'; END IF;
|
|
1461
|
+
PERFORM set_config('opengeni.subject_id',coalesce(document.authority_subject_id,document.created_by,''),true);
|
|
1462
|
+
PERFORM set_config('opengeni.private_file_owner',coalesce(document.authority_subject_id,''),true);
|
|
1463
|
+
SELECT * INTO original FROM files f WHERE f.account_id=p_account AND f.id=document.file_id AND f.status='ready';
|
|
1464
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'Document original unavailable' USING ERRCODE='42501'; END IF;
|
|
1465
|
+
SELECT * INTO entry FROM knowledge_entries e WHERE e.account_id=p_account AND (e.legacy_document_id=p_document
|
|
1466
|
+
OR e.legacy_document_version_id IN (SELECT v.id FROM knowledge_document_versions v WHERE v.account_id=p_account AND v.document_id=p_document))
|
|
1467
|
+
ORDER BY (e.legacy_document_id=p_document) DESC NULLS LAST,e.created_at DESC LIMIT 1 FOR UPDATE;
|
|
1468
|
+
SELECT * INTO obligation FROM knowledge_source_sync_index_obligations o
|
|
1469
|
+
WHERE o.account_id=p_account AND o.workspace_id=p_workspace AND o.document_id=p_document
|
|
1470
|
+
ORDER BY o.object_version_generation DESC,o.created_at DESC LIMIT 1;
|
|
1471
|
+
IF obligation.id IS NOT NULL AND (obligation.status='invalidated' OR NOT EXISTS(
|
|
1472
|
+
SELECT 1 FROM knowledge_document_versions v
|
|
1473
|
+
JOIN knowledge_source_objects o ON o.account_id=v.account_id AND o.id=v.object_id AND o.lifecycle_state='active'
|
|
1474
|
+
AND o.current_version_id=v.id AND o.version_generation=obligation.object_version_generation
|
|
1475
|
+
AND o.lifecycle_generation=obligation.object_lifecycle_generation
|
|
1476
|
+
JOIN knowledge_sources s ON s.account_id=v.account_id AND s.id=v.source_id AND s.lifecycle_state='active'
|
|
1477
|
+
AND s.lifecycle_generation=obligation.source_lifecycle_generation AND s.current_acl_generation=v.acl_generation
|
|
1478
|
+
JOIN knowledge_providers p ON p.account_id=s.account_id AND p.id=s.provider_id AND p.lifecycle_state='active'
|
|
1479
|
+
WHERE v.account_id=p_account AND v.id=obligation.knowledge_document_version_id
|
|
1480
|
+
AND (v.document_id IS NULL OR v.document_id=p_document) AND (v.file_id IS NULL OR v.file_id=document.file_id)
|
|
1481
|
+
)) THEN RAISE EXCEPTION 'Source observation changed before preparation' USING ERRCODE='42501'; END IF;
|
|
1482
|
+
IF obligation.id IS NULL AND NOT google_drive_file_authorized(p_account,p_workspace,coalesce(document.authority_subject_id,document.created_by),document.file_id) THEN
|
|
1483
|
+
-- A normal upload cannot borrow a source-sync processing obligation.
|
|
1484
|
+
RAISE EXCEPTION 'Document original is no longer authorized' USING ERRCODE='42501'; END IF;
|
|
1485
|
+
IF operation='claim' THEN
|
|
1486
|
+
IF obligation.id IS NOT NULL THEN
|
|
1487
|
+
SELECT * INTO scheduled FROM scheduled_task_runs r WHERE r.account_id=p_account AND r.workspace_id=p_workspace
|
|
1488
|
+
AND r.id=obligation.scheduled_task_run_id;
|
|
1489
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'Source indexing run is no longer available' USING ERRCODE='42501'; END IF;
|
|
1490
|
+
accepted_at:=scheduled.created_at;
|
|
1491
|
+
SELECT snapshot.snapshot INTO policy FROM agent_learning_snapshots snapshot
|
|
1492
|
+
JOIN session_turns t ON t.account_id=snapshot.account_id AND t.id=snapshot.turn_id
|
|
1493
|
+
WHERE t.account_id=p_account AND t.workspace_id=p_workspace AND t.scheduled_task_run_id=scheduled.id
|
|
1494
|
+
ORDER BY t.created_at LIMIT 1;
|
|
1495
|
+
IF scheduled.action_kind<>'agent_turn' OR policy IS NULL THEN
|
|
1496
|
+
RAISE EXCEPTION 'Source preparation requires an accepted agent learning snapshot' USING ERRCODE='42501'; END IF;
|
|
1497
|
+
ELSE
|
|
1498
|
+
-- An explicit old Document upload is human/service-authored source input,
|
|
1499
|
+
-- not an automatic finding. Its source copy keeps that existing authority.
|
|
1500
|
+
policy:=jsonb_build_object('effective',jsonb_build_object('knowledge','automatic'));
|
|
1501
|
+
accepted_at:=document.created_at;
|
|
1502
|
+
END IF;
|
|
1503
|
+
IF policy#>>'{effective,knowledge}'='off' THEN
|
|
1504
|
+
RETURN jsonb_build_object('status','disabled','fileId',document.file_id);
|
|
1505
|
+
END IF;
|
|
1506
|
+
IF entry.id IS NULL THEN
|
|
1507
|
+
INSERT INTO knowledge_entries(account_id,origin_workspace_id,scope,scope_workspace_id,scope_subject_id,
|
|
1508
|
+
legacy_document_id,legacy_document_version_id,access_document_id)
|
|
1509
|
+
VALUES(p_account,p_workspace,document.authority_kind,
|
|
1510
|
+
CASE WHEN document.authority_kind='workspace' THEN document.authority_workspace_id END,
|
|
1511
|
+
CASE WHEN document.authority_kind='personal' THEN document.authority_subject_id END,
|
|
1512
|
+
p_document,obligation.knowledge_document_version_id,p_document) RETURNING * INTO entry;
|
|
1513
|
+
END IF;
|
|
1514
|
+
IF entry.scope IS DISTINCT FROM document.authority_kind
|
|
1515
|
+
OR entry.scope_subject_id IS DISTINCT FROM document.authority_subject_id
|
|
1516
|
+
OR (entry.scope='workspace' AND entry.scope_workspace_id IS DISTINCT FROM document.authority_workspace_id) THEN
|
|
1517
|
+
RAISE EXCEPTION 'Document ownership differs from retained Knowledge' USING ERRCODE='42501'; END IF;
|
|
1518
|
+
lease:=gen_random_uuid();
|
|
1519
|
+
UPDATE knowledge_entries SET legacy_document_id=p_document,document_preparation=jsonb_build_object('leaseId',lease,
|
|
1520
|
+
'leaseUntil',clock_timestamp()+interval '15 minutes','expectedVersion',entry.version,
|
|
1521
|
+
'fileId',document.file_id,'policy',policy,'acceptedAt',accepted_at,'scheduledTaskRunId',scheduled.id)
|
|
1522
|
+
WHERE id=entry.id;
|
|
1523
|
+
RETURN jsonb_build_object('status','prepare','entryId',entry.id,'leaseId',lease,'fileId',document.file_id,'file',to_jsonb(original));
|
|
1524
|
+
END IF;
|
|
1525
|
+
IF entry.id IS NULL OR entry.document_preparation->>'leaseId' IS DISTINCT FROM p_request->>'leaseId'
|
|
1526
|
+
OR entry.document_preparation->>'fileId' IS DISTINCT FROM document.file_id::text THEN
|
|
1527
|
+
RAISE EXCEPTION 'Document preparation lease changed' USING ERRCODE='40001'; END IF;
|
|
1528
|
+
fingerprint:=encode(sha256(convert_to(p_request::text,'UTF8')),'hex');
|
|
1529
|
+
IF entry.document_preparation ? 'completion' THEN
|
|
1530
|
+
IF entry.document_preparation->>'completionHash' IS DISTINCT FROM fingerprint THEN
|
|
1531
|
+
RAISE EXCEPTION 'Document preparation replay differs' USING ERRCODE='23505'; END IF;
|
|
1532
|
+
result:=entry.document_preparation->'completion';
|
|
1533
|
+
RETURN CASE WHEN result->>'status'='retained' THEN jsonb_set(result,'{receipt,replayed}','true'::jsonb) ELSE result END;
|
|
1534
|
+
END IF;
|
|
1535
|
+
IF (entry.document_preparation->>'leaseUntil')::timestamptz<=clock_timestamp() THEN
|
|
1536
|
+
RAISE EXCEPTION 'Document preparation lease expired' USING ERRCODE='40001'; END IF;
|
|
1537
|
+
IF p_request->>'codecVersion' IS DISTINCT FROM '1' OR jsonb_typeof(p_request->'content') IS DISTINCT FROM 'string'
|
|
1538
|
+
OR p_request->>'sourceVersion' !~ '^[0-9a-f]{64}$'
|
|
1539
|
+
OR (original.sha256 IS NOT NULL AND lower(original.sha256) IS DISTINCT FROM p_request->>'sourceVersion') THEN
|
|
1540
|
+
RAISE EXCEPTION 'Document extraction does not match its original' USING ERRCODE='22023'; END IF;
|
|
1541
|
+
IF entry.version IS DISTINCT FROM (entry.document_preparation->>'expectedVersion')::integer THEN
|
|
1542
|
+
RAISE EXCEPTION 'Knowledge changed during document processing' USING ERRCODE='40001'; END IF;
|
|
1543
|
+
SELECT * INTO prior FROM knowledge_entry_revisions r WHERE r.account_id=p_account AND r.id=entry.latest_revision_id;
|
|
1544
|
+
-- Re-reading an unchanged, rejected or archived source cannot publish it.
|
|
1545
|
+
IF prior.id IS NOT NULL AND (entry.archived OR (prior.body->'content'=p_request->'content'
|
|
1546
|
+
AND prior.body_codec_version=1 AND prior.body#>>'{source,version}'=p_request->>'sourceVersion')) THEN
|
|
1547
|
+
result:=jsonb_build_object('status','unchanged','entryId',entry.id,'revisionId',prior.id);
|
|
1548
|
+
UPDATE knowledge_entries SET document_preparation=document_preparation||jsonb_build_object('completionHash',fingerprint,'completion',result)
|
|
1549
|
+
WHERE id=entry.id;
|
|
1550
|
+
RETURN result;
|
|
1551
|
+
END IF;
|
|
1552
|
+
actor:=jsonb_build_object('kind','source_preparation','entryId',entry.id,'leaseId',p_request->'leaseId');
|
|
1553
|
+
UPDATE documents SET status='ready' WHERE id=p_document AND account_id=p_account;
|
|
1554
|
+
body:=jsonb_build_object('kind','source','title',p_request->'title','content',p_request->'content',
|
|
1555
|
+
'groupIds',coalesce(prior.body->'groupIds','[]'::jsonb),'relationships',coalesce(prior.body->'relationships','[]'::jsonb),
|
|
1556
|
+
'evidence',coalesce(prior.body->'evidence','[]'::jsonb),
|
|
1557
|
+
'source',jsonb_build_object('kind','file','documentId',p_document,'fileId',document.file_id,
|
|
1558
|
+
'version',p_request->'sourceVersion','retention','full_text'));
|
|
1559
|
+
result:=knowledge_entry_apply(p_account,p_workspace,actor,jsonb_build_object('operation','save',
|
|
1560
|
+
'operationId',p_request->'leaseId','entryId',entry.id,'expectedVersion',entry.version,
|
|
1561
|
+
'entry',body,'codecVersion',1,'preview',p_request->'preview','searchText',p_request->'searchText'));
|
|
1562
|
+
IF NOT knowledge_index_retention_active(p_account,entry.id,(result->>'revisionId')::uuid,p_document) THEN
|
|
1563
|
+
RAISE EXCEPTION 'Document source was withdrawn during processing' USING ERRCODE='42501'; END IF;
|
|
1564
|
+
result:=jsonb_build_object('status','retained','receipt',result);
|
|
1565
|
+
UPDATE knowledge_entries SET document_preparation=document_preparation||jsonb_build_object('completionHash',fingerprint,'completion',result) WHERE id=entry.id;
|
|
1566
|
+
RETURN result;
|
|
1567
|
+
END $$;
|
|
1568
|
+
|
|
1569
|
+
-- Configuration follows the same owner layer as accepted execution.
|
|
1570
|
+
CREATE FUNCTION knowledge_learning_context_visible(p_account uuid,p_workspace uuid,p_subject text,p_scope text,p_context text)
|
|
1571
|
+
RETURNS boolean LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
|
1572
|
+
DECLARE session sessions%ROWTYPE; task scheduled_tasks%ROWTYPE; target uuid;
|
|
1573
|
+
BEGIN
|
|
1574
|
+
IF p_context ~ '^chat:[0-9a-f-]{36}$' THEN
|
|
1575
|
+
SELECT * INTO session FROM sessions WHERE account_id=p_account AND workspace_id=p_workspace AND id=substring(p_context FROM 6)::uuid;
|
|
1576
|
+
IF NOT FOUND THEN RETURN false; END IF;
|
|
1577
|
+
IF get_workspace_kind(p_account,p_workspace)='personal' THEN RETURN p_scope='personal' AND p_subject IS NOT NULL; END IF;
|
|
1578
|
+
IF session.visibility='user_private' THEN RETURN coalesce(p_scope='personal' AND session.owner_subject_id=p_subject,false); END IF;
|
|
1579
|
+
IF session.memory_scope='user' THEN RETURN coalesce(p_scope='personal' AND session.scope_subject_id=p_subject,false); END IF;
|
|
1580
|
+
RETURN p_scope='workspace';
|
|
1581
|
+
ELSIF p_context ~ '^scheduled_task:[0-9a-f-]{36}$' THEN
|
|
1582
|
+
SELECT * INTO task FROM scheduled_tasks WHERE account_id=p_account AND workspace_id=p_workspace AND id=substring(p_context FROM 16)::uuid AND deleted_at IS NULL;
|
|
1583
|
+
IF NOT FOUND THEN RETURN false; END IF;
|
|
1584
|
+
IF task.agent_config#>>'{knowledgeSource,destination,kind}'='personal' THEN
|
|
1585
|
+
RETURN coalesce(p_scope='personal' AND p_subject=task.agent_config#>>'{knowledgeSource,destination,subjectId}',false); END IF;
|
|
1586
|
+
IF get_workspace_kind(p_account,p_workspace)='personal' THEN RETURN p_scope='personal' AND p_subject IS NOT NULL; END IF;
|
|
1587
|
+
target:=CASE WHEN task.run_mode IN ('existing_session','reusable_session') THEN task.reusable_session_id END;
|
|
1588
|
+
IF target IS NOT NULL THEN RETURN knowledge_learning_context_visible(p_account,p_workspace,p_subject,p_scope,'chat:'||target); END IF;
|
|
1589
|
+
IF task.run_mode='existing_session' THEN RETURN false; END IF;
|
|
1590
|
+
IF task.creator_session_policy->>'memoryScope'='user' THEN RETURN coalesce(p_scope='personal' AND task.creator_session_policy->>'scopeSubjectId'=p_subject,false); END IF;
|
|
1591
|
+
RETURN p_scope='workspace';
|
|
1592
|
+
END IF;
|
|
1593
|
+
RETURN false;
|
|
1594
|
+
END $$;
|
|
1595
|
+
|
|
1596
|
+
CREATE FUNCTION agent_learning_manage(p_account uuid,p_workspace uuid,p_actor jsonb,p_request jsonb)
|
|
1597
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT AS $$
|
|
1598
|
+
<<agent_learning_manage>>
|
|
1599
|
+
DECLARE actor jsonb; owner text; subject text; context_key text:=coalesce(p_request->>'contextKey','defaults');
|
|
1600
|
+
scope_value text:=coalesce(p_request->>'scope','workspace'); prior agent_learning_revisions%ROWTYPE;
|
|
1601
|
+
current_revision agent_learning_revisions%ROWTYPE; settings jsonb; patch jsonb; key text; val text;
|
|
1602
|
+
request_hash text; operation_id uuid:=(p_request->>'operationId')::uuid; result jsonb;
|
|
1603
|
+
BEGIN
|
|
1604
|
+
actor:=knowledge_resolve_actor(p_account,p_workspace,p_actor);
|
|
1605
|
+
-- Read-only contextual scope discovery uses the same exact authority check as
|
|
1606
|
+
-- writes. A bounded UI session picker is never the source of owner identity.
|
|
1607
|
+
IF actor->>'kind'='human' AND p_request->>'operation'='read' AND scope_value='context' THEN
|
|
1608
|
+
IF knowledge_learning_context_visible(p_account,p_workspace,actor->>'subjectId','workspace',context_key) THEN scope_value:='workspace';
|
|
1609
|
+
ELSIF knowledge_learning_context_visible(p_account,p_workspace,actor->>'subjectId','personal',context_key) THEN scope_value:='personal';
|
|
1610
|
+
ELSE RAISE EXCEPTION 'Agent learning context is unavailable' USING ERRCODE='42501'; END IF;
|
|
1611
|
+
END IF;
|
|
1612
|
+
IF actor->>'kind'<>'human' OR scope_value NOT IN ('workspace','personal') THEN
|
|
1613
|
+
RAISE EXCEPTION 'Only humans configure Agent learning' USING ERRCODE='42501';
|
|
1614
|
+
END IF;
|
|
1615
|
+
subject:=CASE WHEN scope_value='personal' THEN actor->>'subjectId' END;
|
|
1616
|
+
owner:=CASE WHEN subject IS NULL THEN 'workspace:'||p_workspace ELSE 'personal:'||subject END;
|
|
1617
|
+
IF context_key<>'defaults' AND NOT knowledge_learning_context_visible(p_account,p_workspace,actor->>'subjectId',scope_value,context_key) THEN
|
|
1618
|
+
RAISE EXCEPTION 'Agent learning context is unavailable in this owner scope' USING ERRCODE='42501';
|
|
1619
|
+
END IF;
|
|
1620
|
+
IF p_request->>'operation'='list' THEN
|
|
1621
|
+
SELECT coalesce(jsonb_agg(jsonb_build_object('contextKey',r.context_key,'version',r.version,
|
|
1622
|
+
'settings',r.settings,'updatedAt',r.created_at,'label',coalesce(s.title,t.name,'Untitled chat')) ORDER BY r.context_key),'[]'::jsonb) INTO result
|
|
1623
|
+
FROM (SELECT DISTINCT ON (v.context_key) v.* FROM agent_learning_revisions v
|
|
1624
|
+
WHERE v.account_id=p_account AND v.owner_key=owner AND v.origin_workspace_id=p_workspace AND v.context_key<>'defaults'
|
|
1625
|
+
ORDER BY v.context_key,v.version DESC) r
|
|
1626
|
+
LEFT JOIN sessions s ON s.account_id=p_account AND s.workspace_id=p_workspace AND r.context_key='chat:'||s.id
|
|
1627
|
+
|
|
1628
|
+
LEFT JOIN scheduled_tasks t ON t.account_id=p_account AND t.workspace_id=p_workspace AND r.context_key='scheduled_task:'||t.id
|
|
1629
|
+
AND t.deleted_at IS NULL
|
|
1630
|
+
WHERE r.settings<>'{}'::jsonb AND (s.id IS NOT NULL OR t.id IS NOT NULL)
|
|
1631
|
+
AND knowledge_learning_context_visible(p_account,p_workspace,agent_learning_manage.actor->>'subjectId',scope_value,r.context_key);
|
|
1632
|
+
RETURN result;
|
|
1633
|
+
END IF;
|
|
1634
|
+
SELECT * INTO current_revision FROM agent_learning_revisions r WHERE r.account_id=p_account
|
|
1635
|
+
AND r.owner_key=owner AND r.context_key=agent_learning_manage.context_key ORDER BY r.version DESC LIMIT 1;
|
|
1636
|
+
IF p_request->>'operation'='read' THEN
|
|
1637
|
+
RETURN jsonb_build_object('ownerKey',owner,'contextKey',context_key,'version',coalesce(current_revision.version,0),
|
|
1638
|
+
'settings',coalesce(current_revision.settings,CASE WHEN context_key='defaults' THEN
|
|
1639
|
+
'{"knowledge":"automatic","instructions":"review_first","skills":"review_first"}'::jsonb ELSE '{}'::jsonb END));
|
|
1640
|
+
END IF;
|
|
1641
|
+
IF p_request->>'operation' IS DISTINCT FROM 'save' OR operation_id IS NULL
|
|
1642
|
+
OR NOT coalesce((p_actor->'settingsScopes') ? scope_value,false) THEN
|
|
1643
|
+
RAISE EXCEPTION 'Agent learning settings permission required' USING ERRCODE='42501';
|
|
1644
|
+
END IF;
|
|
1645
|
+
PERFORM pg_advisory_xact_lock(hashtextextended('agent-learning:'||p_account||':'||owner,0));
|
|
1646
|
+
request_hash:=encode(sha256(convert_to(jsonb_build_array(p_actor,p_request)::text,'UTF8')),'hex');
|
|
1647
|
+
SELECT * INTO prior FROM agent_learning_revisions r WHERE r.account_id=p_account AND r.operation_id=agent_learning_manage.operation_id;
|
|
1648
|
+
IF FOUND THEN
|
|
1649
|
+
IF prior.request_hash<>request_hash THEN RAISE EXCEPTION 'Agent learning operation reused' USING ERRCODE='23505'; END IF;
|
|
1650
|
+
RETURN jsonb_build_object('ownerKey',owner,'contextKey',context_key,'version',prior.version,'settings',prior.settings);
|
|
1651
|
+
END IF;
|
|
1652
|
+
SELECT * INTO current_revision FROM agent_learning_revisions r WHERE r.account_id=p_account
|
|
1653
|
+
AND r.owner_key=owner AND r.context_key=agent_learning_manage.context_key ORDER BY r.version DESC LIMIT 1;
|
|
1654
|
+
IF coalesce(current_revision.version,0) IS DISTINCT FROM (p_request->>'expectedVersion')::integer THEN
|
|
1655
|
+
RAISE EXCEPTION 'Agent learning settings changed' USING ERRCODE='40001';
|
|
1656
|
+
END IF;
|
|
1657
|
+
IF context_key='defaults' THEN settings:=p_request->'settings';
|
|
1658
|
+
ELSE
|
|
1659
|
+
settings:=coalesce(current_revision.settings,'{}'::jsonb); patch:=p_request->'settings';
|
|
1660
|
+
IF jsonb_typeof(patch) IS DISTINCT FROM 'object' OR patch-'knowledge'-'instructions'-'skills'<>'{}'::jsonb THEN
|
|
1661
|
+
RAISE EXCEPTION 'Invalid Agent learning override' USING ERRCODE='22023';
|
|
1662
|
+
END IF;
|
|
1663
|
+
FOR key,val IN SELECT k,v#>>'{}' FROM jsonb_each(patch) p(k,v) LOOP
|
|
1664
|
+
IF val='inherit' THEN settings:=settings-key;
|
|
1665
|
+
ELSE settings:=jsonb_set(settings,ARRAY[key],to_jsonb(val),true); END IF;
|
|
1666
|
+
END LOOP;
|
|
1667
|
+
END IF;
|
|
1668
|
+
IF NOT knowledge_learning_settings_valid(settings,context_key='defaults') THEN
|
|
1669
|
+
RAISE EXCEPTION 'Invalid Agent learning settings' USING ERRCODE='22023';
|
|
1670
|
+
END IF;
|
|
1671
|
+
INSERT INTO agent_learning_revisions(account_id,origin_workspace_id,owner_key,subject_id,context_key,version,
|
|
1672
|
+
settings,operation_id,request_hash,actor_subject_id)
|
|
1673
|
+
VALUES(p_account,p_workspace,owner,subject,context_key,coalesce(current_revision.version,0)+1,
|
|
1674
|
+
settings,operation_id,request_hash,actor->>'subjectId') RETURNING * INTO prior;
|
|
1675
|
+
RETURN jsonb_build_object('ownerKey',owner,'contextKey',context_key,'version',prior.version,'settings',settings);
|
|
1676
|
+
END $$;
|
|
1677
|
+
|
|
1678
|
+
-- Evidence is checked transitively. A stored reference is never an authority
|
|
1679
|
+
-- shortcut, and a retired/restricted source withdraws dependent retrieval.
|
|
1680
|
+
CREATE FUNCTION knowledge_document_visible(p_document uuid) RETURNS boolean
|
|
1681
|
+
LANGUAGE sql VOLATILE SET search_path FROM CURRENT AS $$
|
|
1682
|
+
SELECT EXISTS(SELECT 1 FROM documents d
|
|
1683
|
+
WHERE d.id=p_document AND d.account_id=nullif(current_setting('opengeni.account_id',true),'')::uuid
|
|
1684
|
+
AND d.status='ready'
|
|
1685
|
+
AND ((current_setting('opengeni.knowledge_actor_kind',true)='human' OR (current_setting('opengeni.knowledge_actor_kind',true)='source_preparation' AND d.id::text=current_setting('opengeni.knowledge_prepared_document',true))) OR d.agent_access)
|
|
1686
|
+
AND (d.authority_kind='organization'
|
|
1687
|
+
OR (d.authority_kind='workspace' AND d.authority_workspace_id=nullif(current_setting('opengeni.workspace_id',true),'')::uuid)
|
|
1688
|
+
OR (d.authority_kind='personal' AND d.authority_subject_id=nullif(current_setting('opengeni.subject_id',true),'')
|
|
1689
|
+
AND ((d.authority_id IS NULL AND d.authority_workspace_id=nullif(current_setting('opengeni.workspace_id',true),'')::uuid)
|
|
1690
|
+
OR (d.authority_id IS NOT NULL AND ((current_setting('opengeni.knowledge_actor_kind',true)='human' OR (current_setting('opengeni.knowledge_actor_kind',true)='source_preparation' AND d.id::text=current_setting('opengeni.knowledge_prepared_document',true)))
|
|
1691
|
+
OR d.id IN (SELECT resolve_session_attempt_personal_document_reads(d.account_id,
|
|
1692
|
+
nullif(current_setting('opengeni.workspace_id',true),'')::uuid,
|
|
1693
|
+
nullif(current_setting('opengeni.knowledge_session_id',true),'')::uuid,
|
|
1694
|
+
nullif(current_setting('opengeni.knowledge_attempt_id',true),'')::uuid)))))))
|
|
1695
|
+
AND ((current_setting('opengeni.knowledge_actor_kind',true)='source_preparation'
|
|
1696
|
+
AND d.id::text=current_setting('opengeni.knowledge_prepared_document',true))
|
|
1697
|
+
OR google_drive_file_authorized(d.account_id,nullif(current_setting('opengeni.workspace_id',true),'')::uuid,
|
|
1698
|
+
nullif(current_setting('opengeni.subject_id',true),''),d.file_id)))
|
|
1699
|
+
$$;
|
|
1700
|
+
|
|
1701
|
+
CREATE FUNCTION knowledge_source_visible(p_account uuid,p_body jsonb) RETURNS boolean
|
|
1702
|
+
LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
|
1703
|
+
<<knowledge_source_visible>>
|
|
1704
|
+
DECLARE document_id uuid:=(p_body#>>'{source,documentId}')::uuid;
|
|
1705
|
+
file_id uuid:=(p_body#>>'{source,fileId}')::uuid;
|
|
1706
|
+
BEGIN
|
|
1707
|
+
IF document_id IS NOT NULL THEN
|
|
1708
|
+
RETURN knowledge_document_visible(document_id) AND (file_id IS NULL OR EXISTS(
|
|
1709
|
+
SELECT 1 FROM documents d WHERE d.account_id=p_account AND d.id=document_id AND d.file_id=knowledge_source_visible.file_id));
|
|
1710
|
+
END IF;
|
|
1711
|
+
IF file_id IS NOT NULL THEN
|
|
1712
|
+
RETURN EXISTS(SELECT 1 FROM files f WHERE f.account_id=p_account AND f.id=knowledge_source_visible.file_id
|
|
1713
|
+
AND f.status='ready') AND google_drive_file_authorized(p_account,nullif(current_setting('opengeni.workspace_id',true),'')::uuid,
|
|
1714
|
+
nullif(current_setting('opengeni.subject_id',true),''),file_id);
|
|
1715
|
+
END IF;
|
|
1716
|
+
RETURN true;
|
|
1717
|
+
END $$;
|
|
1718
|
+
|
|
1719
|
+
CREATE FUNCTION knowledge_validate_source(p_account uuid,p_entry uuid,p_body jsonb) RETURNS void
|
|
1720
|
+
LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
|
1721
|
+
DECLARE e knowledge_entries%ROWTYPE; d documents%ROWTYPE;
|
|
1722
|
+
BEGIN
|
|
1723
|
+
IF NOT knowledge_source_visible(p_account,p_body) THEN
|
|
1724
|
+
RAISE EXCEPTION 'Knowledge source unavailable' USING ERRCODE='42501'; END IF;
|
|
1725
|
+
SELECT * INTO STRICT e FROM knowledge_entries WHERE account_id=p_account AND id=p_entry;
|
|
1726
|
+
IF (e.access_document_id IS NOT NULL AND (p_body#>>'{source,documentId}')::uuid IS DISTINCT FROM e.access_document_id)
|
|
1727
|
+
OR (e.access_file_id IS NOT NULL AND (p_body#>>'{source,fileId}')::uuid IS DISTINCT FROM e.access_file_id) THEN
|
|
1728
|
+
RAISE EXCEPTION 'A source identity is immutable; retain a different source as a new entry' USING ERRCODE='22023'; END IF;
|
|
1729
|
+
IF p_body#>>'{source,documentId}' IS NOT NULL THEN
|
|
1730
|
+
SELECT * INTO STRICT d FROM documents WHERE account_id=p_account AND id=(p_body#>>'{source,documentId}')::uuid;
|
|
1731
|
+
IF NOT (d.authority_kind='organization'
|
|
1732
|
+
OR (d.authority_kind='workspace' AND e.scope IN ('workspace','personal') AND d.authority_workspace_id=e.origin_workspace_id)
|
|
1733
|
+
OR (d.authority_kind='personal' AND e.scope='personal' AND d.authority_subject_id=e.scope_subject_id)) THEN
|
|
1734
|
+
RAISE EXCEPTION 'Knowledge source would widen document access' USING ERRCODE='42501'; END IF;
|
|
1735
|
+
ELSIF p_body#>>'{source,fileId}' IS NOT NULL THEN
|
|
1736
|
+
IF NOT EXISTS(SELECT 1 FROM files f WHERE f.account_id=p_account
|
|
1737
|
+
AND (f.workspace_id=e.origin_workspace_id OR (e.scope='personal' AND e.scope_subject_id=ANY(f.private_owner_subject_ids)))
|
|
1738
|
+
AND f.id=(p_body#>>'{source,fileId}')::uuid AND f.status='ready'
|
|
1739
|
+
AND (f.private_owner_subject_ids IS NULL OR (e.scope='personal' AND e.scope_subject_id=ANY(f.private_owner_subject_ids)))) OR e.scope='organization' THEN
|
|
1740
|
+
RAISE EXCEPTION 'File source requires its authorized scope' USING ERRCODE='42501'; END IF;
|
|
1741
|
+
END IF;
|
|
1742
|
+
IF e.access_document_id IS NULL AND e.access_file_id IS NULL THEN
|
|
1743
|
+
UPDATE knowledge_entries SET access_document_id=(p_body#>>'{source,documentId}')::uuid,
|
|
1744
|
+
access_file_id=CASE WHEN p_body#>>'{source,documentId}' IS NULL THEN (p_body#>>'{source,fileId}')::uuid END
|
|
1745
|
+
WHERE account_id=p_account AND id=p_entry;
|
|
1746
|
+
END IF;
|
|
1747
|
+
END $$;
|
|
1748
|
+
|
|
1749
|
+
-- Imported provider evidence retains both the captured ACL and current source
|
|
1750
|
+
-- lifecycle. Migrating text into Knowledge never turns revoked input into a
|
|
1751
|
+
-- workspace-wide grant. These old rows remain provenance/access authority only.
|
|
1752
|
+
CREATE FUNCTION knowledge_legacy_version_visible(p_account uuid,p_version uuid) RETURNS boolean
|
|
1753
|
+
LANGUAGE sql VOLATILE SET search_path FROM CURRENT AS $$
|
|
1754
|
+
SELECT EXISTS(SELECT 1 FROM knowledge_document_versions v
|
|
1755
|
+
JOIN knowledge_source_objects o ON o.account_id=v.account_id AND o.id=v.object_id AND o.lifecycle_state='active'
|
|
1756
|
+
JOIN knowledge_sources s ON s.account_id=v.account_id AND s.id=v.source_id AND s.lifecycle_state='active'
|
|
1757
|
+
JOIN knowledge_providers p ON p.account_id=s.account_id AND p.id=s.provider_id AND p.lifecycle_state='active'
|
|
1758
|
+
JOIN knowledge_source_acl_versions captured ON captured.account_id=v.account_id AND captured.id=v.acl_version_id
|
|
1759
|
+
AND captured.source_id=v.source_id AND captured.generation=v.acl_generation
|
|
1760
|
+
JOIN knowledge_source_acl_versions current_acl ON current_acl.account_id=s.account_id AND current_acl.source_id=s.id
|
|
1761
|
+
AND current_acl.generation=s.current_acl_generation
|
|
1762
|
+
WHERE v.account_id=p_account AND v.id=p_version
|
|
1763
|
+
AND (current_setting('opengeni.knowledge_actor_kind',true)='human' OR (captured.agent_access AND current_acl.agent_access))
|
|
1764
|
+
AND (v.document_id IS NULL OR knowledge_document_visible(v.document_id))
|
|
1765
|
+
AND (v.file_id IS NULL OR google_drive_file_authorized(p_account,
|
|
1766
|
+
nullif(current_setting('opengeni.workspace_id',true),'')::uuid,nullif(current_setting('opengeni.subject_id',true),''),v.file_id)))
|
|
1767
|
+
$$;
|
|
1768
|
+
|
|
1769
|
+
CREATE FUNCTION knowledge_revision_visible(p_account uuid,p_entry uuid,p_revision uuid,p_pending boolean)
|
|
1770
|
+
RETURNS boolean LANGUAGE sql VOLATILE SET search_path FROM CURRENT AS $$
|
|
1771
|
+
WITH RECURSIVE dependencies(entry_id,revision_id) AS (
|
|
1772
|
+
SELECT p_entry,p_revision
|
|
1773
|
+
UNION
|
|
1774
|
+
SELECT l.target_entry_id,l.target_revision_id FROM dependencies d
|
|
1775
|
+
JOIN knowledge_entry_links l ON l.account_id=p_account AND l.entry_id=d.entry_id
|
|
1776
|
+
AND l.revision_id=d.revision_id AND l.relation='evidence'
|
|
1777
|
+
) SELECT EXISTS(SELECT 1 FROM knowledge_entry_revisions r WHERE r.account_id=p_account
|
|
1778
|
+
AND r.entry_id=p_entry AND r.id=p_revision)
|
|
1779
|
+
AND NOT EXISTS (
|
|
1780
|
+
SELECT 1 FROM dependencies d LEFT JOIN knowledge_entries e ON e.account_id=p_account AND e.id=d.entry_id
|
|
1781
|
+
LEFT JOIN knowledge_entry_revisions r ON r.account_id=p_account AND r.entry_id=d.entry_id AND r.id=d.revision_id
|
|
1782
|
+
WHERE e.id IS NULL OR NOT knowledge_scope_visible(e) OR (e.archived AND e.id<>p_entry)
|
|
1783
|
+
OR (NOT p_pending AND NOT EXISTS(SELECT 1 FROM knowledge_entry_decisions v
|
|
1784
|
+
WHERE v.account_id=p_account AND v.entry_id=d.entry_id AND v.revision_id=d.revision_id AND v.outcome='published'))
|
|
1785
|
+
OR (e.legacy_document_version_id IS NOT NULL AND NOT knowledge_legacy_version_visible(p_account,e.legacy_document_version_id))
|
|
1786
|
+
OR (e.legacy_document_id IS NOT NULL AND NOT knowledge_document_visible(e.legacy_document_id))
|
|
1787
|
+
OR (e.access_document_id IS NOT NULL AND NOT knowledge_document_visible(e.access_document_id))
|
|
1788
|
+
OR (e.access_file_id IS NOT NULL AND NOT google_drive_file_authorized(p_account,
|
|
1789
|
+
nullif(current_setting('opengeni.workspace_id',true),'')::uuid,
|
|
1790
|
+
nullif(current_setting('opengeni.subject_id',true),''),e.access_file_id))
|
|
1791
|
+
OR r.id IS NULL OR NOT knowledge_source_visible(p_account,r.body)
|
|
1792
|
+
)
|
|
1793
|
+
$$;
|
|
1794
|
+
|
|
1795
|
+
-- Relationship labels and group IDs are discovery metadata. Filter unavailable
|
|
1796
|
+
-- endpoints on every projection; their disappearance does not erase the retained
|
|
1797
|
+
-- entry or rewrite its immutable revision. Evidence remains a content dependency.
|
|
1798
|
+
CREATE FUNCTION knowledge_entry_visible_body(p_account uuid,p_body jsonb,p_pending boolean)
|
|
1799
|
+
RETURNS jsonb LANGUAGE sql VOLATILE SET search_path FROM CURRENT AS $$
|
|
1800
|
+
SELECT p_body || jsonb_build_object(
|
|
1801
|
+
'groupIds',coalesce((SELECT jsonb_agg(g.value ORDER BY g.ordinality)
|
|
1802
|
+
FROM jsonb_array_elements(p_body->'groupIds') WITH ORDINALITY g(value,ordinality)
|
|
1803
|
+
JOIN knowledge_entries e ON e.account_id=p_account AND e.id=(g.value#>>'{}')::uuid
|
|
1804
|
+
WHERE NOT e.archived AND knowledge_revision_visible(p_account,e.id,
|
|
1805
|
+
coalesce(e.published_revision_id,CASE WHEN p_pending THEN e.latest_revision_id END),p_pending)), '[]'::jsonb),
|
|
1806
|
+
'relationships',coalesce((SELECT jsonb_agg(l.value ORDER BY l.ordinality)
|
|
1807
|
+
FROM jsonb_array_elements(p_body->'relationships') WITH ORDINALITY l(value,ordinality)
|
|
1808
|
+
JOIN knowledge_entries e ON e.account_id=p_account AND e.id=(l.value->>'entryId')::uuid
|
|
1809
|
+
WHERE NOT e.archived AND knowledge_revision_visible(p_account,e.id,
|
|
1810
|
+
coalesce(e.published_revision_id,CASE WHEN p_pending THEN e.latest_revision_id END),p_pending)), '[]'::jsonb))
|
|
1811
|
+
$$;
|
|
1812
|
+
|
|
1813
|
+
CREATE FUNCTION knowledge_validate_links(p_account uuid,p_entry uuid,p_body jsonb,p_batch uuid)
|
|
1814
|
+
RETURNS void LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
|
1815
|
+
DECLARE source knowledge_entries%ROWTYPE; target knowledge_entries%ROWTYPE; link record; target_revision uuid;
|
|
1816
|
+
BEGIN
|
|
1817
|
+
SELECT * INTO source FROM knowledge_entries WHERE account_id=p_account AND id=p_entry;
|
|
1818
|
+
FOR link IN
|
|
1819
|
+
SELECT (x->>'entryId')::uuid AS id,(x->>'revisionId')::uuid AS revision,'evidence' AS relation
|
|
1820
|
+
FROM jsonb_array_elements(coalesce(p_body->'evidence','[]'::jsonb)) x
|
|
1821
|
+
UNION ALL SELECT x::uuid,NULL,'group' FROM jsonb_array_elements_text(coalesce(p_body->'groupIds','[]'::jsonb)) x
|
|
1822
|
+
UNION ALL SELECT (x->>'entryId')::uuid,NULL,x->>'relation'
|
|
1823
|
+
FROM jsonb_array_elements(coalesce(p_body->'relationships','[]'::jsonb)) x
|
|
1824
|
+
ORDER BY id,relation
|
|
1825
|
+
LOOP
|
|
1826
|
+
SELECT * INTO target FROM knowledge_entries WHERE account_id=p_account AND id=link.id;
|
|
1827
|
+
IF NOT FOUND OR target.id=source.id OR target.archived OR NOT knowledge_scope_visible(target) THEN
|
|
1828
|
+
RAISE EXCEPTION 'Knowledge reference unavailable' USING ERRCODE='42501';
|
|
1829
|
+
END IF;
|
|
1830
|
+
-- The destination's readers must all be eligible to see linked identities.
|
|
1831
|
+
IF NOT ((target.scope='organization')
|
|
1832
|
+
OR (target.scope='workspace' AND source.scope IN ('workspace','personal')
|
|
1833
|
+
AND target.scope_workspace_id=source.origin_workspace_id)
|
|
1834
|
+
OR (target.scope='personal' AND source.scope='personal' AND target.scope_subject_id=source.scope_subject_id))
|
|
1835
|
+
OR (target.legacy_scope_type IS NOT NULL AND (source.legacy_scope_type IS DISTINCT FROM target.legacy_scope_type
|
|
1836
|
+
OR source.legacy_scope_role_key IS DISTINCT FROM target.legacy_scope_role_key
|
|
1837
|
+
OR source.legacy_scope_session_id IS DISTINCT FROM target.legacy_scope_session_id)) THEN
|
|
1838
|
+
RAISE EXCEPTION 'Knowledge reference would widen source access' USING ERRCODE='42501';
|
|
1839
|
+
END IF;
|
|
1840
|
+
target_revision:=coalesce(link.revision,target.published_revision_id);
|
|
1841
|
+
IF target_revision IS NULL AND p_batch IS NOT NULL THEN
|
|
1842
|
+
SELECT id INTO target_revision FROM knowledge_entry_revisions r WHERE r.account_id=p_account
|
|
1843
|
+
AND r.entry_id=target.id AND r.id=target.latest_revision_id;
|
|
1844
|
+
END IF;
|
|
1845
|
+
IF NOT knowledge_revision_visible(p_account,target.id,target_revision,false) AND NOT (
|
|
1846
|
+
p_batch IS NOT NULL AND target_revision=target.latest_revision_id
|
|
1847
|
+
AND EXISTS(SELECT 1 FROM knowledge_entry_revisions r WHERE r.account_id=p_account
|
|
1848
|
+
AND r.entry_id=target.id AND r.id=target_revision)
|
|
1849
|
+
AND EXISTS(SELECT 1 FROM knowledge_entry_decisions d WHERE d.account_id=p_account
|
|
1850
|
+
AND d.entry_id=target.id AND d.revision_id=target_revision AND d.version=target.version AND d.outcome='pending')
|
|
1851
|
+
AND knowledge_revision_visible(p_account,target.id,target_revision,true)) THEN
|
|
1852
|
+
RAISE EXCEPTION 'Knowledge evidence has not been published' USING ERRCODE='42501';
|
|
1853
|
+
END IF;
|
|
1854
|
+
IF link.relation='group' AND NOT EXISTS(SELECT 1 FROM knowledge_entry_revisions r
|
|
1855
|
+
WHERE r.account_id=p_account AND r.entry_id=target.id AND r.id=target_revision AND r.body->>'kind'='group') THEN
|
|
1856
|
+
RAISE EXCEPTION 'Knowledge membership requires a group' USING ERRCODE='22023';
|
|
1857
|
+
END IF;
|
|
1858
|
+
-- Membership follows collection identities, not the revision originally
|
|
1859
|
+
-- linked. Check both published and outstanding proposed parents so that
|
|
1860
|
+
-- independently reviewed moves cannot later close a cycle. Publication
|
|
1861
|
+
-- and proposals share the account publication lock.
|
|
1862
|
+
IF link.relation='group' AND p_body->>'kind'='group' AND EXISTS(
|
|
1863
|
+
WITH RECURSIVE parents(id) AS (
|
|
1864
|
+
SELECT target.id UNION SELECT l.target_entry_id FROM parents
|
|
1865
|
+
JOIN knowledge_entries g ON g.account_id=p_account AND g.id=parents.id AND NOT g.archived
|
|
1866
|
+
JOIN knowledge_entry_links l ON l.account_id=g.account_id AND l.entry_id=g.id AND l.relation='group'
|
|
1867
|
+
AND (l.revision_id=g.published_revision_id OR (l.revision_id=g.latest_revision_id AND EXISTS(
|
|
1868
|
+
SELECT 1 FROM knowledge_entry_decisions d WHERE d.account_id=g.account_id AND d.entry_id=g.id
|
|
1869
|
+
AND d.revision_id=g.latest_revision_id AND d.version=g.version AND d.outcome='pending')))
|
|
1870
|
+
) SELECT 1 FROM parents WHERE id=source.id
|
|
1871
|
+
) THEN RAISE EXCEPTION 'Knowledge collections cannot contain a cycle' USING ERRCODE='22023'; END IF;
|
|
1872
|
+
IF link.relation='evidence' AND EXISTS(
|
|
1873
|
+
WITH RECURSIVE refs(id,rev) AS (
|
|
1874
|
+
SELECT target.id,target_revision UNION SELECT l.target_entry_id,l.target_revision_id FROM refs
|
|
1875
|
+
JOIN knowledge_entry_links l ON l.account_id=p_account AND l.entry_id=refs.id AND l.revision_id=refs.rev AND l.relation='evidence'
|
|
1876
|
+
) SELECT 1 FROM refs WHERE id=source.id
|
|
1877
|
+
) THEN RAISE EXCEPTION 'Knowledge evidence cannot contain a cycle' USING ERRCODE='22023'; END IF;
|
|
1878
|
+
END LOOP;
|
|
1879
|
+
END $$;
|
|
1880
|
+
|
|
1881
|
+
-- Batches are review presentation only. They grant no content or publication authority.
|
|
1882
|
+
CREATE FUNCTION knowledge_review_batch_for_actor(p_account uuid,p_workspace uuid,p_actor jsonb)
|
|
1883
|
+
RETURNS uuid LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
|
1884
|
+
DECLARE batch uuid; owner text:=CASE WHEN p_actor->>'defaultScope'='personal'
|
|
1885
|
+
THEN 'personal:'||(p_actor->>'subjectId') ELSE 'workspace:'||p_workspace END;
|
|
1886
|
+
BEGIN
|
|
1887
|
+
SELECT b.id INTO batch FROM knowledge_review_batches b WHERE b.account_id=p_account AND b.owner_key=owner
|
|
1888
|
+
AND ((p_actor->>'scheduledTaskRunId' IS NOT NULL AND b.scheduled_task_run_id=(p_actor->>'scheduledTaskRunId')::uuid)
|
|
1889
|
+
OR (p_actor->>'scheduledTaskRunId' IS NULL AND b.turn_id=(p_actor->>'turnId')::uuid));
|
|
1890
|
+
IF batch IS NULL THEN
|
|
1891
|
+
INSERT INTO knowledge_review_batches(account_id,origin_workspace_id,owner_key,session_id,turn_id,scheduled_task_run_id)
|
|
1892
|
+
VALUES(p_account,p_workspace,owner,(p_actor->>'sessionId')::uuid,(p_actor->>'turnId')::uuid,
|
|
1893
|
+
(p_actor->>'scheduledTaskRunId')::uuid) RETURNING id INTO batch;
|
|
1894
|
+
END IF;
|
|
1895
|
+
RETURN batch;
|
|
1896
|
+
END $$;
|
|
1897
|
+
|
|
1898
|
+
-- Historical proposals keep their native revision and immutable provenance.
|
|
1899
|
+
-- Only maintenance-imported receipts bridge them into the current review list;
|
|
1900
|
+
-- creating a new legacy proposal cannot become a second authoring path.
|
|
1901
|
+
CREATE FUNCTION knowledge_instruction_context(p_revision workspace_instruction_policy_revisions)
|
|
1902
|
+
RETURNS jsonb LANGUAGE sql STABLE SET search_path FROM CURRENT AS $$
|
|
1903
|
+
SELECT coalesce(p_revision.agent_learning_context,(SELECT op.actor->'legacyInstructionContext'
|
|
1904
|
+
FROM agent_instruction_operations op WHERE op.account_id=p_revision.account_id AND op.revision_id=p_revision.id
|
|
1905
|
+
AND op.request_hash='migration:0461:instruction-review' AND op.actor->>'kind'='migration' LIMIT 1))
|
|
1906
|
+
$$;
|
|
1907
|
+
|
|
1908
|
+
CREATE FUNCTION agent_instruction_apply(p_account uuid,p_workspace uuid,p_actor jsonb,p_request jsonb)
|
|
1909
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT AS $$
|
|
1910
|
+
<<instruction_change>>
|
|
1911
|
+
DECLARE actor jsonb; mode text; operation text:=p_request->>'operation'; fingerprint text; result jsonb;
|
|
1912
|
+
operation_id uuid:=(p_request->>'operationId')::uuid; revision_id uuid; batch uuid; actor_subject text;
|
|
1913
|
+
target jsonb:=p_request->'target'; context jsonb; expected_revision uuid; expected_version bigint;
|
|
1914
|
+
head workspace_instruction_policy_heads%ROWTYPE; revision workspace_instruction_policy_revisions%ROWTYPE;
|
|
1915
|
+
prior agent_instruction_operations%ROWTYPE; content_hash text; outcome text; link record; version bigint;
|
|
1916
|
+
BEGIN
|
|
1917
|
+
IF p_account IS DISTINCT FROM nullif(current_setting('opengeni.account_id',true),'')::uuid
|
|
1918
|
+
OR p_workspace IS DISTINCT FROM nullif(current_setting('opengeni.workspace_id',true),'')::uuid THEN
|
|
1919
|
+
RAISE EXCEPTION 'Instruction changes require exact tenant scope' USING ERRCODE='42501';
|
|
1920
|
+
END IF;
|
|
1921
|
+
-- Keep the workspace identity stable without excluding unrelated FK readers.
|
|
1922
|
+
-- Publication serializes on the account advisory lock below before the
|
|
1923
|
+
-- instruction head, matching other Knowledge writers without an advisory /
|
|
1924
|
+
-- workspace-row lock inversion.
|
|
1925
|
+
PERFORM 1 FROM workspaces WHERE id=p_workspace AND account_id=p_account FOR KEY SHARE;
|
|
1926
|
+
actor:=knowledge_resolve_actor(p_account,p_workspace,p_actor);
|
|
1927
|
+
IF operation='get' THEN
|
|
1928
|
+
SELECT * INTO head FROM workspace_instruction_policy_heads h WHERE h.account_id=p_account AND h.workspace_id=p_workspace
|
|
1929
|
+
AND h.kind=target->>'kind' AND h.scope=target->>'scope' AND h.role_key IS NOT DISTINCT FROM target->>'roleKey';
|
|
1930
|
+
SELECT * INTO revision FROM workspace_instruction_policy_revisions r WHERE r.account_id=p_account AND r.workspace_id=p_workspace
|
|
1931
|
+
AND r.id=head.revision_id;
|
|
1932
|
+
RETURN jsonb_build_object('target',target,'expectedCurrentRevisionId',head.revision_id,
|
|
1933
|
+
'expectedActivationVersion',coalesce(head.activation_version,(SELECT max(d.activation_version)
|
|
1934
|
+
FROM workspace_instruction_policy_deactivation_events d WHERE d.account_id=p_account AND d.workspace_id=p_workspace
|
|
1935
|
+
AND d.kind=target->>'kind' AND d.scope=target->>'scope' AND d.role_key IS NOT DISTINCT FROM target->>'roleKey'),0),
|
|
1936
|
+
'content',revision.content);
|
|
1937
|
+
END IF;
|
|
1938
|
+
IF operation='save' THEN
|
|
1939
|
+
IF actor->>'kind'<>'agent' OR (actor->>'defaultScope'='personal' AND get_workspace_kind(p_account,p_workspace)<>'personal') THEN
|
|
1940
|
+
RAISE EXCEPTION 'Workspace instructions require a workspace agent context' USING ERRCODE='42501';
|
|
1941
|
+
END IF;
|
|
1942
|
+
mode:=actor#>>'{policy,effective,instructions}';
|
|
1943
|
+
IF mode NOT IN ('automatic','review_first') OR mode IS NULL THEN
|
|
1944
|
+
RAISE EXCEPTION 'Instruction learning is Off' USING ERRCODE='42501';
|
|
1945
|
+
END IF;
|
|
1946
|
+
actor_subject:='service:agent-learning:'||(p_actor->>'attemptId');
|
|
1947
|
+
ELSE
|
|
1948
|
+
IF actor->>'kind'<>'human' OR NOT coalesce((p_actor->'settingsScopes') ? 'workspace',false) THEN
|
|
1949
|
+
RAISE EXCEPTION 'Instruction review requires workspace administration' USING ERRCODE='42501';
|
|
1950
|
+
END IF;
|
|
1951
|
+
actor_subject:=actor->>'subjectId';
|
|
1952
|
+
END IF;
|
|
1953
|
+
IF operation='list' THEN
|
|
1954
|
+
RETURN (SELECT coalesce(jsonb_agg(item ORDER BY identity),'[]'::jsonb) FROM (
|
|
1955
|
+
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),
|
|
1956
|
+
'content',r.content,'createdAt',r.created_at,'reviewBatchId',knowledge_instruction_context(r)->'reviewBatchId',
|
|
1957
|
+
'sessionId',knowledge_instruction_context(r)#>'{actor,sessionId}','reason',knowledge_instruction_context(r)->'reason') AS item
|
|
1958
|
+
FROM workspace_instruction_policy_revisions r WHERE r.account_id=p_account AND r.workspace_id=p_workspace
|
|
1959
|
+
AND knowledge_instruction_context(r) IS NOT NULL
|
|
1960
|
+
AND NOT EXISTS(SELECT 1 FROM workspace_instruction_policy_activation_events ev WHERE ev.account_id=p_account
|
|
1961
|
+
AND ev.workspace_id=p_workspace AND ev.new_revision_id=r.id)
|
|
1962
|
+
AND NOT EXISTS(SELECT 1 FROM agent_instruction_operations op WHERE op.account_id=p_account AND op.revision_id=r.id
|
|
1963
|
+
AND op.receipt->>'outcome' IN ('published','rejected'))
|
|
1964
|
+
AND NOT EXISTS(SELECT 1 FROM workspace_instruction_policy_revisions newer WHERE newer.account_id=p_account
|
|
1965
|
+
AND newer.workspace_id=p_workspace AND newer.kind=r.kind AND newer.scope=r.scope
|
|
1966
|
+
AND newer.role_key IS NOT DISTINCT FROM r.role_key AND newer.revision>r.revision)
|
|
1967
|
+
AND (NOT(p_request ? 'cursor') OR r.id>(p_request->>'cursor')::uuid)
|
|
1968
|
+
ORDER BY r.id LIMIT 51) visible);
|
|
1969
|
+
END IF;
|
|
1970
|
+
IF operation_id IS NULL OR operation NOT IN ('save','approve','reject') OR length(btrim(p_request->>'reason')) NOT BETWEEN 1 AND 4096 THEN
|
|
1971
|
+
RAISE EXCEPTION 'Invalid instruction change' USING ERRCODE='22023';
|
|
1972
|
+
END IF;
|
|
1973
|
+
fingerprint:=encode(sha256(convert_to(jsonb_build_array(
|
|
1974
|
+
CASE WHEN p_actor->>'kind'='agent' THEN p_actor-'attemptId'-'executionGeneration' ELSE p_actor END,
|
|
1975
|
+
p_request)::text,'UTF8')),'hex');
|
|
1976
|
+
SELECT * INTO prior FROM agent_instruction_operations op WHERE op.account_id=p_account AND op.operation_id=instruction_change.operation_id;
|
|
1977
|
+
IF FOUND THEN
|
|
1978
|
+
IF prior.request_hash<>fingerprint OR prior.origin_workspace_id<>p_workspace THEN
|
|
1979
|
+
RAISE EXCEPTION 'Instruction operation key reused' USING ERRCODE='23505'; END IF;
|
|
1980
|
+
RETURN prior.receipt||jsonb_build_object('replayed',true);
|
|
1981
|
+
END IF;
|
|
1982
|
+
IF operation<>'save' THEN
|
|
1983
|
+
SELECT * INTO revision FROM workspace_instruction_policy_revisions r WHERE r.account_id=p_account AND r.workspace_id=p_workspace
|
|
1984
|
+
AND r.id=(p_request->>'revisionId')::uuid AND knowledge_instruction_context(r) IS NOT NULL;
|
|
1985
|
+
IF NOT FOUND OR EXISTS(SELECT 1 FROM agent_instruction_operations op WHERE op.account_id=p_account AND op.revision_id=revision.id
|
|
1986
|
+
AND op.receipt->>'outcome' IN ('published','rejected')) OR EXISTS(SELECT 1 FROM workspace_instruction_policy_revisions newer
|
|
1987
|
+
WHERE newer.account_id=p_account AND newer.workspace_id=p_workspace AND newer.kind=revision.kind AND newer.scope=revision.scope
|
|
1988
|
+
AND newer.role_key IS NOT DISTINCT FROM revision.role_key AND newer.revision>revision.revision) THEN
|
|
1989
|
+
RAISE EXCEPTION 'Instruction review is no longer current' USING ERRCODE='40001'; END IF;
|
|
1990
|
+
revision_id:=revision.id; context:=knowledge_instruction_context(revision);
|
|
1991
|
+
target:=jsonb_build_object('kind',revision.kind,'scope',revision.scope,'roleKey',revision.role_key);
|
|
1992
|
+
expected_revision:=(context->>'expectedCurrentRevisionId')::uuid;
|
|
1993
|
+
expected_version:=(context->>'expectedActivationVersion')::bigint;
|
|
1994
|
+
batch:=(context->>'reviewBatchId')::uuid;
|
|
1995
|
+
outcome:=CASE WHEN operation='approve' THEN 'published' ELSE 'rejected' END;
|
|
1996
|
+
ELSE
|
|
1997
|
+
IF length(p_request->>'content') NOT BETWEEN 1 AND 600 OR length(btrim(p_request->>'content'))=0
|
|
1998
|
+
OR jsonb_typeof(p_request->'evidence') IS DISTINCT FROM 'array' OR jsonb_array_length(p_request->'evidence')>32 THEN
|
|
1999
|
+
RAISE EXCEPTION 'Invalid agent-authored instruction content' USING ERRCODE='22023'; END IF;
|
|
2000
|
+
expected_revision:=(p_request->>'expectedCurrentRevisionId')::uuid;
|
|
2001
|
+
expected_version:=(p_request->>'expectedActivationVersion')::bigint;
|
|
2002
|
+
outcome:=CASE WHEN mode='automatic' THEN 'published' ELSE 'pending' END;
|
|
2003
|
+
IF outcome='pending' THEN batch:=knowledge_review_batch_for_actor(p_account,p_workspace,actor); END IF;
|
|
2004
|
+
context:=jsonb_build_object('actor',p_actor,'policy',actor->'policy','evidence',p_request->'evidence','reason',p_request->'reason',
|
|
2005
|
+
'expectedCurrentRevisionId',expected_revision,'expectedActivationVersion',expected_version,'reviewBatchId',batch);
|
|
2006
|
+
END IF;
|
|
2007
|
+
IF operation<>'reject' THEN
|
|
2008
|
+
PERFORM pg_advisory_xact_lock(hashtextextended('knowledge-publication:'||p_account,0));
|
|
2009
|
+
END IF;
|
|
2010
|
+
SELECT * INTO head FROM workspace_instruction_policy_heads h WHERE h.account_id=p_account AND h.workspace_id=p_workspace
|
|
2011
|
+
AND h.kind=target->>'kind' AND h.scope=target->>'scope' AND h.role_key IS NOT DISTINCT FROM target->>'roleKey' FOR UPDATE;
|
|
2012
|
+
version:=coalesce(head.activation_version,(SELECT max(d.activation_version) FROM workspace_instruction_policy_deactivation_events d
|
|
2013
|
+
WHERE d.account_id=p_account AND d.workspace_id=p_workspace AND d.kind=target->>'kind' AND d.scope=target->>'scope'
|
|
2014
|
+
AND d.role_key IS NOT DISTINCT FROM target->>'roleKey'),0);
|
|
2015
|
+
IF operation<>'reject' AND (head.revision_id IS DISTINCT FROM expected_revision OR version IS DISTINCT FROM expected_version) THEN
|
|
2016
|
+
RAISE EXCEPTION 'Instruction head changed; read its current baseline' USING ERRCODE='40001'; END IF;
|
|
2017
|
+
IF operation<>'reject' THEN
|
|
2018
|
+
FOR link IN SELECT * FROM jsonb_array_elements(context->'evidence') LOOP
|
|
2019
|
+
IF NOT EXISTS(SELECT 1 FROM knowledge_entries e WHERE e.account_id=p_account AND e.id=(link.value->>'entryId')::uuid
|
|
2020
|
+
AND NOT e.archived AND e.scope IN ('workspace','organization') AND knowledge_scope_visible(e)
|
|
2021
|
+
AND knowledge_revision_visible(p_account,e.id,(link.value->>'revisionId')::uuid,false)) THEN
|
|
2022
|
+
RAISE EXCEPTION 'Instruction evidence is unavailable to workspace readers' USING ERRCODE='42501'; END IF;
|
|
2023
|
+
END LOOP;
|
|
2024
|
+
END IF;
|
|
2025
|
+
IF operation='save' THEN
|
|
2026
|
+
content_hash:=encode(sha256(convert_to(p_request->>'content','UTF8')),'hex');
|
|
2027
|
+
INSERT INTO workspace_instruction_policy_revisions(account_id,workspace_id,operation_id,request_fingerprint,kind,scope,role_key,
|
|
2028
|
+
content,content_hash,provenance_source,provenance_source_id,created_by_subject_id,supersedes_revision_id,agent_learning_context)
|
|
2029
|
+
VALUES(p_account,p_workspace,operation_id,fingerprint,target->>'kind',target->>'scope',target->>'roleKey',p_request->>'content',
|
|
2030
|
+
content_hash,'agent_learning',p_actor->>'attemptId',actor_subject,expected_revision,context) RETURNING * INTO revision;
|
|
2031
|
+
revision_id:=revision.id;
|
|
2032
|
+
END IF;
|
|
2033
|
+
IF outcome='published' THEN
|
|
2034
|
+
INSERT INTO workspace_instruction_policy_activation_events(account_id,workspace_id,operation_id,request_fingerprint,
|
|
2035
|
+
kind,scope,role_key,type,activation_version,old_revision_id,old_revision,old_content_hash,new_revision_id,new_revision,new_content_hash,
|
|
2036
|
+
actor_subject_id,reason)
|
|
2037
|
+
VALUES(p_account,p_workspace,operation_id,fingerprint,revision.kind,revision.scope,revision.role_key,'activate',version+1,
|
|
2038
|
+
head.revision_id,head.revision,head.content_hash,revision.id,revision.revision,revision.content_hash,actor_subject,p_request->>'reason');
|
|
2039
|
+
IF head.id IS NULL THEN
|
|
2040
|
+
INSERT INTO workspace_instruction_policy_heads(account_id,workspace_id,kind,scope,role_key,revision_id,revision,content_hash,activation_version)
|
|
2041
|
+
VALUES(p_account,p_workspace,revision.kind,revision.scope,revision.role_key,revision.id,revision.revision,revision.content_hash,version+1);
|
|
2042
|
+
ELSE
|
|
2043
|
+
UPDATE workspace_instruction_policy_heads h SET revision_id=revision.id,revision=revision.revision,content_hash=revision.content_hash,
|
|
2044
|
+
activation_version=version+1,activated_at=transaction_timestamp() WHERE h.account_id=p_account AND h.id=head.id;
|
|
2045
|
+
END IF;
|
|
2046
|
+
END IF;
|
|
2047
|
+
result:=jsonb_build_object('operationId',operation_id,'revisionId',revision_id,'outcome',outcome,'reviewBatchId',batch,'replayed',false);
|
|
2048
|
+
INSERT INTO agent_instruction_operations(account_id,origin_workspace_id,operation_id,revision_id,request_hash,actor,receipt)
|
|
2049
|
+
VALUES(p_account,p_workspace,operation_id,revision_id,fingerprint,p_actor,result);
|
|
2050
|
+
RETURN result;
|
|
2051
|
+
END $$;
|
|
2052
|
+
|
|
2053
|
+
-- Native Skill storage remains the destination; its policy is now unified.
|
|
2054
|
+
CREATE OR REPLACE FUNCTION skill_apply_lifecycle(p_account_id uuid, p_workspace_id uuid, p_actor jsonb, p_request jsonb)
|
|
2055
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT AS $body$
|
|
2056
|
+
<<lifecycle>>
|
|
2057
|
+
DECLARE
|
|
2058
|
+
actor_subject text; mode text := 'automatic'; operation text := p_request->>'operation';
|
|
2059
|
+
operation_id uuid := (p_request->>'operationId')::uuid;
|
|
2060
|
+
skill_id uuid := (p_request->>'skillId')::uuid;
|
|
2061
|
+
revision_id uuid; activation_event_id uuid; files jsonb := p_request->'files'; main_content text;
|
|
2062
|
+
head preference_registry_preferences%ROWTYPE; rev preference_registry_revisions%ROWTYPE;
|
|
2063
|
+
source record; binding skill_source_bindings%ROWTYPE;
|
|
2064
|
+
prior skill_write_receipts%ROWTYPE; result jsonb; fingerprint text;
|
|
2065
|
+
scope text := coalesce(p_request->>'scope','workspace'); next_event integer;
|
|
2066
|
+
title text := p_request->>'title'; description text := p_request->>'description';
|
|
2067
|
+
stable_key text := p_request->>'stableKey'; outcome text; source_id text;
|
|
2068
|
+
activation_mode text := 'workspace_managed';
|
|
2069
|
+
deferred_publication jsonb; source_effective boolean := true;
|
|
2070
|
+
confirmation_source skill_write_receipts%ROWTYPE;
|
|
2071
|
+
skill_review jsonb; initiating_human text; confirmation_generation integer;
|
|
2072
|
+
human_choice text; learning_actor jsonb; learning_policy jsonb;
|
|
2073
|
+
original_operation text := p_request->>'operation';
|
|
2074
|
+
BEGIN
|
|
2075
|
+
IF p_account_id IS DISTINCT FROM nullif(current_setting('opengeni.account_id',true),'')::uuid
|
|
2076
|
+
OR p_workspace_id IS DISTINCT FROM nullif(current_setting('opengeni.workspace_id',true),'')::uuid
|
|
2077
|
+
OR p_account_id IS NULL OR p_workspace_id IS NULL OR operation_id IS NULL
|
|
2078
|
+
OR operation NOT IN ('save','install','approve','reject','restore','confirm_response') OR operation IS NULL
|
|
2079
|
+
THEN RAISE EXCEPTION 'Skill lifecycle requires exact tenant context' USING ERRCODE='42501'; END IF;
|
|
2080
|
+
IF operation='confirm_response' THEN
|
|
2081
|
+
PERFORM pg_advisory_xact_lock(hashtextextended('organization-membership:'||p_account_id,0));
|
|
2082
|
+
END IF;
|
|
2083
|
+
PERFORM 1 FROM workspaces WHERE id=p_workspace_id AND account_id=p_account_id FOR KEY SHARE;
|
|
2084
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'Skill workspace unavailable' USING ERRCODE='42501'; END IF;
|
|
2085
|
+
PERFORM pg_advisory_xact_lock(hashtextextended('skill-publication:'||p_workspace_id,0));
|
|
2086
|
+
IF p_actor->>'kind' = 'agent' THEN
|
|
2087
|
+
learning_actor:=knowledge_resolve_actor(p_account_id,p_workspace_id,p_actor);
|
|
2088
|
+
learning_policy:=learning_actor->'policy';
|
|
2089
|
+
scope:=CASE WHEN learning_actor->>'defaultScope'='personal' THEN 'user' ELSE 'workspace' END;
|
|
2090
|
+
IF operation IN ('approve','reject') OR (scope='user' AND operation='install') THEN
|
|
2091
|
+
RAISE EXCEPTION 'Agents cannot review Skills or install workspace plugins from personal learning' USING ERRCODE='42501'; END IF;
|
|
2092
|
+
PERFORM 1 FROM sessions s JOIN session_turns t ON t.id=s.active_turn_id AND t.session_id=s.id
|
|
2093
|
+
JOIN session_turn_attempts a ON a.id=t.active_attempt_id AND a.turn_id=t.id
|
|
2094
|
+
WHERE s.id=(p_actor->>'sessionId')::uuid AND s.account_id=p_account_id AND s.workspace_id=p_workspace_id
|
|
2095
|
+
AND t.id=(p_actor->>'turnId')::uuid AND t.account_id=p_account_id AND t.workspace_id=p_workspace_id
|
|
2096
|
+
AND t.status IN ('running','requires_action','recovering','waiting_capacity')
|
|
2097
|
+
AND a.id=(p_actor->>'attemptId')::uuid AND a.account_id=p_account_id AND a.workspace_id=p_workspace_id
|
|
2098
|
+
AND a.session_id=s.id AND a.execution_generation=(p_actor->>'executionGeneration')::integer
|
|
2099
|
+
AND t.execution_generation=a.execution_generation AND a.state IN ('claimed','running')
|
|
2100
|
+
AND NOT EXISTS (SELECT 1 FROM session_attempt_interruptions i WHERE i.workspace_id=p_workspace_id
|
|
2101
|
+
AND i.attempt_id=a.id AND i.state IN ('pending','delivered','acknowledged'))
|
|
2102
|
+
FOR SHARE OF s,t,a;
|
|
2103
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'Skill write requires exact live attempt' USING ERRCODE='42501'; END IF;
|
|
2104
|
+
actor_subject := 'service:skill-attempt:' || (p_actor->>'attemptId');
|
|
2105
|
+
ELSIF p_actor->>'kind' = 'human' AND p_actor->>'principalKind' = 'human_session'
|
|
2106
|
+
AND p_actor->>'subjectId' = nullif(current_setting('opengeni.subject_id',true),'')
|
|
2107
|
+
AND current_setting('opengeni.principal_kind',true) = 'human_session' THEN
|
|
2108
|
+
actor_subject := p_actor->>'subjectId';
|
|
2109
|
+
ELSIF p_actor->>'kind' = 'service' AND operation = 'install' AND scope = 'workspace'
|
|
2110
|
+
AND p_actor->>'principalKind' IN ('service','api_key','configured_key')
|
|
2111
|
+
AND p_actor->>'subjectId' = nullif(current_setting('opengeni.subject_id',true),'')
|
|
2112
|
+
AND p_actor->>'principalKind' = current_setting('opengeni.principal_kind',true) THEN
|
|
2113
|
+
actor_subject := p_actor->>'subjectId';
|
|
2114
|
+
ELSE RAISE EXCEPTION 'Skill lifecycle actor is not authorized' USING ERRCODE='42501'; END IF;
|
|
2115
|
+
|
|
2116
|
+
IF operation='confirm_response' THEN
|
|
2117
|
+
IF p_actor->>'kind' <> 'human' THEN
|
|
2118
|
+
RAISE EXCEPTION 'Skill chat confirmation requires verified human response admission' USING ERRCODE='42501';
|
|
2119
|
+
END IF;
|
|
2120
|
+
SELECT * INTO confirmation_source FROM skill_write_receipts r
|
|
2121
|
+
WHERE r.account_id=p_account_id AND r.workspace_id=p_workspace_id
|
|
2122
|
+
AND r.operation_id=(p_request->>'sourceOperationId')::uuid;
|
|
2123
|
+
skill_review := confirmation_source.receipt->'skillReview';
|
|
2124
|
+
IF NOT FOUND OR skill_review IS NULL OR confirmation_source.actor->>'kind'<>'agent'
|
|
2125
|
+
THEN
|
|
2126
|
+
RAISE EXCEPTION 'Skill confirmation source unavailable' USING ERRCODE='42501';
|
|
2127
|
+
END IF;
|
|
2128
|
+
SELECT coalesce(t.initiating_human_subject_id, CASE WHEN t.initiator_kind='subject' THEN t.initiator_subject_id END),
|
|
2129
|
+
t.execution_generation INTO initiating_human,confirmation_generation
|
|
2130
|
+
FROM session_turns t JOIN sessions session ON session.active_turn_id=t.id AND session.id=t.session_id
|
|
2131
|
+
JOIN session_human_input_requests response_request ON response_request.session_id=t.session_id
|
|
2132
|
+
AND response_request.turn_id=t.id AND response_request.account_id=p_account_id
|
|
2133
|
+
AND response_request.workspace_id=p_workspace_id
|
|
2134
|
+
AND response_request.id=(p_request->>'humanInputRequestId')::uuid
|
|
2135
|
+
WHERE t.id=(confirmation_source.actor->>'turnId')::uuid
|
|
2136
|
+
AND t.session_id=(confirmation_source.actor->>'sessionId')::uuid AND t.workspace_id=p_workspace_id
|
|
2137
|
+
AND t.account_id=p_account_id AND session.workspace_id=p_workspace_id AND session.account_id=p_account_id
|
|
2138
|
+
AND t.status IN ('running','requires_action','recovering','waiting_capacity')
|
|
2139
|
+
FOR SHARE OF t,session;
|
|
2140
|
+
IF initiating_human IS NULL OR initiating_human IS DISTINCT FROM p_actor->>'subjectId' OR NOT (
|
|
2141
|
+
EXISTS(SELECT 1 FROM workspace_memberships m WHERE m.workspace_id=p_workspace_id AND m.subject_id=initiating_human)
|
|
2142
|
+
OR EXISTS(SELECT 1 FROM organization_memberships m WHERE m.account_id=p_account_id
|
|
2143
|
+
AND m.subject_id=initiating_human AND m.status='active' AND m.personal_workspace_id=p_workspace_id)
|
|
2144
|
+
) OR EXISTS(SELECT 1 FROM organization_memberships m WHERE m.account_id=p_account_id
|
|
2145
|
+
AND m.subject_id=initiating_human AND m.status<>'active') THEN
|
|
2146
|
+
RAISE EXCEPTION 'Skill confirming human no longer has workspace authority' USING ERRCODE='42501';
|
|
2147
|
+
END IF;
|
|
2148
|
+
SELECT answer->'values'->>0 INTO human_choice
|
|
2149
|
+
FROM session_human_input_requests h
|
|
2150
|
+
CROSS JOIN LATERAL jsonb_array_elements(h.response->'answers') answer
|
|
2151
|
+
WHERE h.id=(p_request->>'humanInputRequestId')::uuid AND h.account_id=p_account_id AND h.workspace_id=p_workspace_id
|
|
2152
|
+
AND h.session_id=(confirmation_source.actor->>'sessionId')::uuid AND h.turn_id=(confirmation_source.actor->>'turnId')::uuid
|
|
2153
|
+
AND h.turn_generation >= (confirmation_source.actor->>'executionGeneration')::integer
|
|
2154
|
+
AND h.turn_generation <= confirmation_generation AND h.status='answered' AND h.responded_by=initiating_human
|
|
2155
|
+
AND h.skill_review_human_authorized AND h.allow_skip=false
|
|
2156
|
+
AND h.response->>'outcome'='answered'
|
|
2157
|
+
AND jsonb_array_length(h.questions)=1 AND jsonb_array_length(h.response->'answers')=1
|
|
2158
|
+
AND h.questions->0->>'id'='skill:'||(skill_review->>'revisionId')
|
|
2159
|
+
AND h.questions->0->>'kind'='single_select'
|
|
2160
|
+
AND h.questions->0->>'label'='Save this Skill?'
|
|
2161
|
+
AND h.questions->0->>'helpText'='Review the complete files before saving. Saving activates this revision immediately.'
|
|
2162
|
+
AND h.questions->0->'required'='true'::jsonb
|
|
2163
|
+
AND coalesce(h.questions->0->'allowOther','false'::jsonb) IN ('false'::jsonb,'true'::jsonb)
|
|
2164
|
+
AND (answer->'other' IS NULL OR answer->'other'='null'::jsonb OR answer->'other'='""'::jsonb)
|
|
2165
|
+
AND h.responded_at IS NOT NULL AND h.created_at >= confirmation_source.created_at
|
|
2166
|
+
AND (h.expires_at IS NULL OR h.responded_at <= h.expires_at)
|
|
2167
|
+
AND h.questions->0->>'prompt'='Save this exact Skill revision for this workspace?'
|
|
2168
|
+
AND h.questions->0->'skillReview'=skill_review
|
|
2169
|
+
AND jsonb_array_length(h.questions->0->'options')=2
|
|
2170
|
+
AND ((h.questions->0->'options'->0) - 'description')='{"id":"save","label":"Save"}'::jsonb
|
|
2171
|
+
AND ((h.questions->0->'options'->1) - 'description')='{"id":"skip","label":"Don''t save"}'::jsonb
|
|
2172
|
+
AND coalesce(h.questions->0->'options'->0->'description','null'::jsonb)='null'::jsonb
|
|
2173
|
+
AND coalesce(h.questions->0->'options'->1->'description','null'::jsonb)='null'::jsonb
|
|
2174
|
+
AND answer->>'questionId'='skill:'||(skill_review->>'revisionId')
|
|
2175
|
+
AND answer->'values' IN ('["save"]'::jsonb,'["skip"]'::jsonb)
|
|
2176
|
+
FOR SHARE OF h;
|
|
2177
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'Exact human Skill confirmation unavailable' USING ERRCODE='42501'; END IF;
|
|
2178
|
+
skill_id := (skill_review->>'skillId')::uuid;
|
|
2179
|
+
p_request := p_request || skill_review || jsonb_build_object('operationId',operation_id,
|
|
2180
|
+
'reason','Approved once by the initiating human in chat');
|
|
2181
|
+
operation := CASE WHEN human_choice='skip' THEN 'reject' ELSE 'approve' END;
|
|
2182
|
+
actor_subject := initiating_human;
|
|
2183
|
+
END IF;
|
|
2184
|
+
fingerprint := encode(sha256(convert_to(jsonb_build_array(p_actor,p_request)::text,'UTF8')),'hex');
|
|
2185
|
+
PERFORM pg_advisory_xact_lock(hashtextextended('skill-operation:'||p_workspace_id||':'||operation_id,0));
|
|
2186
|
+
SELECT * INTO prior FROM skill_write_receipts r WHERE r.workspace_id=p_workspace_id AND r.operation_id=lifecycle.operation_id;
|
|
2187
|
+
IF FOUND THEN
|
|
2188
|
+
IF prior.fingerprint <> fingerprint OR prior.account_id <> p_account_id THEN
|
|
2189
|
+
RAISE EXCEPTION 'Skill operation key reused with different input' USING ERRCODE='23505'; END IF;
|
|
2190
|
+
RETURN (prior.receipt - 'portableInstall' - 'deferredPublication') || jsonb_build_object('replayed',true);
|
|
2191
|
+
END IF;
|
|
2192
|
+
IF p_actor->>'kind' IN ('agent','service') THEN
|
|
2193
|
+
learning_policy:=coalesce(learning_policy,knowledge_learning_resolve(p_account_id,p_workspace_id,NULL,'defaults',transaction_timestamp()));
|
|
2194
|
+
mode:=learning_policy#>>'{effective,skills}';
|
|
2195
|
+
IF mode='review_first' THEN mode:='suggest'; END IF;
|
|
2196
|
+
IF mode = 'off' THEN RAISE EXCEPTION 'Learning is Off; durable Skill changes are refused' USING ERRCODE='42501'; END IF;
|
|
2197
|
+
END IF;
|
|
2198
|
+
|
|
2199
|
+
IF original_operation='confirm_response' THEN mode := 'automatic'; END IF;
|
|
2200
|
+
|
|
2201
|
+
IF operation = 'install' THEN
|
|
2202
|
+
SELECT f.facet_key, f.activation_mode, v.plugin_id, sf.*, coalesce(jsonb_agg(jsonb_build_object('path',ff.path,'content',ff.content) ORDER BY ff.path COLLATE "C")
|
|
2203
|
+
FILTER (WHERE ff.id IS NOT NULL),'[]'::jsonb) AS files INTO source
|
|
2204
|
+
FROM capability_skill_facets sf JOIN capability_facets f ON f.id=sf.facet_id
|
|
2205
|
+
JOIN capability_plugin_versions v ON v.id=f.plugin_version_id
|
|
2206
|
+
JOIN capability_plugin_installations i ON i.plugin_version_id=v.id AND i.plugin_id=v.plugin_id
|
|
2207
|
+
JOIN capability_facet_installations fi ON fi.plugin_installation_id=i.id AND fi.facet_id=f.id AND fi.status='active'
|
|
2208
|
+
LEFT JOIN capability_skill_files ff ON ff.skill_facet_id=sf.facet_id
|
|
2209
|
+
WHERE sf.facet_id=(p_request->>'skillFacetId')::uuid AND i.account_id=p_account_id
|
|
2210
|
+
AND i.workspace_id=p_workspace_id AND i.status='active'
|
|
2211
|
+
GROUP BY f.facet_key,f.activation_mode,v.plugin_id,sf.facet_id;
|
|
2212
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'Skill source is not installed in this workspace' USING ERRCODE='42501'; END IF;
|
|
2213
|
+
source_effective := skill_source_has_effective_owner(p_account_id,p_workspace_id,source.facet_id);
|
|
2214
|
+
PERFORM pg_advisory_xact_lock(hashtextextended('skill-source:'||p_workspace_id||':'||source.plugin_id||':'||source.facet_key,0));
|
|
2215
|
+
SELECT * INTO binding FROM skill_source_bindings b WHERE b.workspace_id=p_workspace_id
|
|
2216
|
+
AND b.plugin_id=source.plugin_id AND b.facet_key=source.facet_key;
|
|
2217
|
+
skill_id := coalesce(binding.preference_id,gen_random_uuid());
|
|
2218
|
+
files := source.files; title := p_request->>'title'; description := p_request->>'description';
|
|
2219
|
+
stable_key := 'installed-'||replace(skill_id::text,'-',''); source_id := source.facet_id::text;
|
|
2220
|
+
activation_mode := source.activation_mode;
|
|
2221
|
+
END IF;
|
|
2222
|
+
SELECT * INTO head FROM preference_registry_preferences h WHERE h.id=skill_id AND h.account_id=p_account_id FOR UPDATE;
|
|
2223
|
+
IF FOUND THEN
|
|
2224
|
+
IF p_actor->>'kind'='agent' AND head.scope<>scope THEN
|
|
2225
|
+
RAISE EXCEPTION 'Skill outside accepted learning scope' USING ERRCODE='42501'; END IF;
|
|
2226
|
+
scope := head.scope;
|
|
2227
|
+
IF operation <> 'install' THEN
|
|
2228
|
+
SELECT coalesce(r.skill_activation_mode,'workspace_managed') INTO activation_mode
|
|
2229
|
+
FROM preference_registry_revisions r WHERE r.preference_id=head.id
|
|
2230
|
+
ORDER BY (r.id=head.active_revision_id) DESC NULLS LAST,r.revision DESC LIMIT 1;
|
|
2231
|
+
END IF;
|
|
2232
|
+
IF (p_actor->>'kind'='service' AND (head.scope<>'workspace' OR head.scope_workspace_id<>p_workspace_id))
|
|
2233
|
+
OR (p_actor->>'kind'='agent' AND ((head.scope='workspace' AND head.scope_workspace_id<>p_workspace_id)
|
|
2234
|
+
OR (head.scope='user' AND head.scope_subject_id IS DISTINCT FROM learning_actor->>'subjectId')))
|
|
2235
|
+
OR (p_actor->>'kind' = 'human' AND NOT opengeni_private.preference_registry_scope_visible(
|
|
2236
|
+
head.account_id,head.scope,head.scope_workspace_id,head.scope_subject_id)) THEN
|
|
2237
|
+
RAISE EXCEPTION 'Skill is outside authorized scope' USING ERRCODE='42501'; END IF;
|
|
2238
|
+
IF operation = 'install' THEN
|
|
2239
|
+
SELECT * INTO rev FROM preference_registry_revisions WHERE id=head.active_revision_id;
|
|
2240
|
+
IF head.active_revision_id IS NOT NULL AND (rev.provenance_source <> 'portable_skill' OR rev.provenance_source_id=source_id) THEN
|
|
2241
|
+
revision_id := head.active_revision_id; outcome := 'preserved';
|
|
2242
|
+
END IF;
|
|
2243
|
+
ELSIF head.active_revision_id IS DISTINCT FROM (p_request->>'expectedRevisionId')::uuid
|
|
2244
|
+
OR head.scope_version IS DISTINCT FROM (p_request->>'expectedScopeVersion')::integer THEN
|
|
2245
|
+
RAISE EXCEPTION 'Skill head changed' USING ERRCODE='40001';
|
|
2246
|
+
END IF;
|
|
2247
|
+
IF head.status IN ('rejected','superseded') THEN RAISE EXCEPTION 'Skill head is retired' USING ERRCODE='23514'; END IF;
|
|
2248
|
+
ELSE
|
|
2249
|
+
IF operation NOT IN ('save','install') OR (p_request->>'expectedRevisionId') IS NOT NULL
|
|
2250
|
+
OR (operation='save' AND (p_request->>'expectedScopeVersion')::integer IS DISTINCT FROM 1) THEN
|
|
2251
|
+
RAISE EXCEPTION 'Skill head missing or changed' USING ERRCODE='40001'; END IF;
|
|
2252
|
+
INSERT INTO preference_registry_preferences(id,account_id,stable_key,scope,scope_workspace_id,scope_subject_id,created_by_subject_id)
|
|
2253
|
+
VALUES(skill_id,p_account_id,stable_key,scope,CASE WHEN scope='workspace' THEN p_workspace_id END,
|
|
2254
|
+
CASE WHEN scope='user' THEN coalesce(learning_actor->>'subjectId',actor_subject) END,actor_subject) RETURNING * INTO head;
|
|
2255
|
+
END IF;
|
|
2256
|
+
|
|
2257
|
+
IF outcome IS NULL THEN
|
|
2258
|
+
IF operation IN ('approve','restore','reject') THEN
|
|
2259
|
+
SELECT * INTO rev FROM preference_registry_revisions r WHERE r.id=(p_request->>'revisionId')::uuid
|
|
2260
|
+
AND r.account_id=p_account_id AND r.preference_id=skill_id;
|
|
2261
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'Skill revision unavailable' USING ERRCODE='42501'; END IF;
|
|
2262
|
+
IF operation IN ('approve','reject') AND EXISTS(SELECT 1 FROM preference_registry_revisions newer
|
|
2263
|
+
WHERE newer.preference_id=skill_id AND newer.revision>rev.revision) THEN
|
|
2264
|
+
RAISE EXCEPTION 'Skill changed after chat proposal' USING ERRCODE='40001';
|
|
2265
|
+
END IF;
|
|
2266
|
+
IF operation IN ('approve','reject') AND (
|
|
2267
|
+
(operation='reject' AND rev.id=head.active_revision_id)
|
|
2268
|
+
OR EXISTS(SELECT 1 FROM preference_registry_events event
|
|
2269
|
+
WHERE event.preference_id=skill_id AND event.new_revision_id=rev.id AND event.type='rejected')
|
|
2270
|
+
) THEN RAISE EXCEPTION 'Skill proposal was already settled' USING ERRCODE='40001'; END IF;
|
|
2271
|
+
files := coalesce(rev.skill_files,jsonb_build_array(jsonb_build_object('path','SKILL.md','content',rev.content)));
|
|
2272
|
+
title := p_request->>'title'; description := p_request->>'description';
|
|
2273
|
+
activation_mode := coalesce(rev.skill_activation_mode,'workspace_managed');
|
|
2274
|
+
IF operation='approve' AND rev.provenance_source='portable_skill' AND EXISTS(
|
|
2275
|
+
SELECT 1 FROM skill_source_bindings b WHERE b.preference_id=skill_id AND b.account_id=p_account_id
|
|
2276
|
+
AND NOT skill_source_has_effective_owner(p_account_id,b.workspace_id,rev.provenance_source_id::uuid)) THEN
|
|
2277
|
+
RAISE EXCEPTION 'Skill approval requires a finalized source owner' USING ERRCODE='42501';
|
|
2278
|
+
END IF;
|
|
2279
|
+
END IF;
|
|
2280
|
+
IF NOT skill_files_valid(files) THEN RAISE EXCEPTION 'Invalid Skill text folder' USING ERRCODE='22023'; END IF;
|
|
2281
|
+
SELECT f->>'content' INTO main_content FROM jsonb_array_elements(files) f WHERE f->>'path'='SKILL.md';
|
|
2282
|
+
IF operation IN ('approve','reject') THEN revision_id := rev.id;
|
|
2283
|
+
ELSE
|
|
2284
|
+
INSERT INTO preference_registry_revisions(account_id,preference_id,title,description,content,content_hash,
|
|
2285
|
+
conflict_strategy,provenance_source,provenance_source_id,trust,created_by_subject_id,corrects_revision_id,skill_files,skill_activation_mode)
|
|
2286
|
+
VALUES(p_account_id,skill_id,title,description,main_content,encode(sha256(convert_to(main_content,'UTF8')),'hex'),
|
|
2287
|
+
'override',CASE WHEN operation='install' THEN 'portable_skill' WHEN p_actor->>'kind'='agent' THEN 'agent' ELSE 'human' END,
|
|
2288
|
+
CASE WHEN operation='install' THEN source_id WHEN p_actor->>'kind'='agent' THEN p_actor->>'attemptId' END,
|
|
2289
|
+
CASE WHEN p_actor->>'kind' IN ('agent','service') THEN 'untrusted_proposal' WHEN scope='user' THEN 'personal'
|
|
2290
|
+
WHEN scope='organization' THEN 'organization_managed' ELSE 'workspace_managed' END,
|
|
2291
|
+
actor_subject,head.active_revision_id,files,activation_mode) RETURNING id INTO revision_id;
|
|
2292
|
+
END IF;
|
|
2293
|
+
SELECT coalesce(max(e.version),0)+1 INTO next_event FROM preference_registry_events e WHERE e.preference_id=skill_id;
|
|
2294
|
+
IF next_event=1 THEN
|
|
2295
|
+
INSERT INTO preference_registry_events(account_id,preference_id,type,version,new_revision_id,new_scope,new_workspace_id,new_subject_id,actor_subject_id,reason)
|
|
2296
|
+
VALUES(p_account_id,skill_id,'proposal_created',next_event,revision_id,head.scope,head.scope_workspace_id,head.scope_subject_id,actor_subject,p_request->>'reason');
|
|
2297
|
+
next_event := next_event+1;
|
|
2298
|
+
END IF;
|
|
2299
|
+
IF mode='automatic' AND operation='install' AND NOT source_effective THEN
|
|
2300
|
+
deferred_publication := jsonb_build_object('expectedRevisionId',head.active_revision_id,
|
|
2301
|
+
'expectedScopeVersion',head.scope_version,'sourceFacetId',source.facet_id);
|
|
2302
|
+
END IF;
|
|
2303
|
+
IF operation='reject' THEN
|
|
2304
|
+
INSERT INTO preference_registry_events(account_id,preference_id,type,version,new_revision_id,actor_subject_id,reason)
|
|
2305
|
+
VALUES(p_account_id,skill_id,'rejected',next_event,revision_id,actor_subject,coalesce(p_request->>'reason','Declined during review'));
|
|
2306
|
+
outcome := 'preserved';
|
|
2307
|
+
ELSIF mode='automatic' AND deferred_publication IS NULL THEN
|
|
2308
|
+
PERFORM set_config('opengeni.preference_lifecycle_head_id',skill_id::text,true);
|
|
2309
|
+
PERFORM set_config('opengeni.preference_lifecycle_operation','activate',true);
|
|
2310
|
+
UPDATE preference_registry_preferences h SET status='active',active_revision_id=revision_id,
|
|
2311
|
+
active_revision=r.revision,active_content_hash=r.content_hash,activation_version=h.activation_version+1,updated_at=clock_timestamp()
|
|
2312
|
+
FROM preference_registry_revisions r WHERE h.id=skill_id AND r.id=revision_id;
|
|
2313
|
+
INSERT INTO preference_registry_events(account_id,preference_id,type,version,old_revision_id,new_revision_id,actor_subject_id,reason)
|
|
2314
|
+
VALUES(p_account_id,skill_id,'activated',next_event,head.active_revision_id,revision_id,actor_subject,p_request->>'reason') RETURNING id INTO activation_event_id;
|
|
2315
|
+
outcome := 'applied';
|
|
2316
|
+
ELSE outcome := 'pending'; END IF;
|
|
2317
|
+
IF operation='install' THEN
|
|
2318
|
+
INSERT INTO skill_source_bindings VALUES(p_account_id,p_workspace_id,source.plugin_id,source.facet_key,skill_id,source.facet_id)
|
|
2319
|
+
ON CONFLICT(workspace_id,plugin_id,facet_key) DO UPDATE SET skill_facet_id=excluded.skill_facet_id;
|
|
2320
|
+
END IF;
|
|
2321
|
+
END IF;
|
|
2322
|
+
IF operation='install' AND outcome='preserved' THEN
|
|
2323
|
+
UPDATE skill_source_bindings b SET skill_facet_id=source.facet_id
|
|
2324
|
+
WHERE b.workspace_id=p_workspace_id AND b.plugin_id=source.plugin_id AND b.facet_key=source.facet_key;
|
|
2325
|
+
END IF;
|
|
2326
|
+
result := jsonb_build_object('operationId',operation_id,'skillId',skill_id,'revisionId',revision_id,'outcome',outcome,'replayed',false);
|
|
2327
|
+
IF operation='reject' THEN result := result || jsonb_build_object('decision','rejected'); END IF;
|
|
2328
|
+
IF outcome='pending' AND deferred_publication IS NULL AND p_actor->>'kind'='agent' THEN
|
|
2329
|
+
result := result || jsonb_build_object('pendingReason','approval','skillReview',
|
|
2330
|
+
jsonb_build_object('sourceOperationId',operation_id,'skillId',skill_id,'revisionId',revision_id,
|
|
2331
|
+
'expectedRevisionId',head.active_revision_id,'expectedScopeVersion',head.scope_version));
|
|
2332
|
+
END IF;
|
|
2333
|
+
IF deferred_publication IS NOT NULL THEN result := result || jsonb_build_object('pendingReason','source_finalization'); END IF;
|
|
2334
|
+
IF learning_policy IS NOT NULL THEN result:=result||jsonb_build_object('learningPolicy',learning_policy); END IF;
|
|
2335
|
+
INSERT INTO skill_write_receipts(account_id,workspace_id,operation_id,fingerprint,actor,receipt,activation_event_id)
|
|
2336
|
+
VALUES(p_account_id,p_workspace_id,operation_id,fingerprint,p_actor,
|
|
2337
|
+
(CASE WHEN operation='install' AND p_request ? 'portableInstall'
|
|
2338
|
+
THEN result || jsonb_build_object('portableInstall',p_request->'portableInstall') ELSE result END)
|
|
2339
|
+
|| CASE WHEN deferred_publication IS NOT NULL THEN jsonb_build_object('deferredPublication',deferred_publication) ELSE '{}'::jsonb END,
|
|
2340
|
+
activation_event_id);
|
|
2341
|
+
RETURN result;
|
|
2342
|
+
END $body$;
|
|
2343
|
+
|
|
2344
|
+
CREATE OR REPLACE FUNCTION skill_publish_finalized_owner() RETURNS trigger
|
|
2345
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT AS $$
|
|
2346
|
+
DECLARE
|
|
2347
|
+
publication_owner_kind text := CASE TG_TABLE_NAME WHEN 'pack_installations' THEN 'pack' ELSE 'plugin' END;
|
|
2348
|
+
pending record; head preference_registry_preferences%ROWTYPE; revision preference_registry_revisions%ROWTYPE;
|
|
2349
|
+
publication_id uuid; publication_hash text; publication_operation uuid; expected_fingerprint text; event_id uuid;
|
|
2350
|
+
next_event integer; result jsonb; disposition text; current_mode text; parent_target text;
|
|
2351
|
+
publication_actor jsonb := jsonb_build_object('kind','service','subjectId','service:skill-publication','principalKind','service');
|
|
2352
|
+
BEGIN
|
|
2353
|
+
PERFORM pg_advisory_xact_lock(hashtextextended('skill-publication:'||NEW.workspace_id,0));
|
|
2354
|
+
FOR pending IN
|
|
2355
|
+
SELECT DISTINCT receipt.* FROM skill_write_receipts receipt
|
|
2356
|
+
JOIN skill_source_bindings binding ON binding.preference_id=(receipt.receipt->>'skillId')::uuid
|
|
2357
|
+
AND binding.account_id=receipt.account_id AND binding.workspace_id=receipt.workspace_id
|
|
2358
|
+
JOIN capability_facet_installations fi ON fi.facet_id=binding.skill_facet_id
|
|
2359
|
+
AND fi.account_id=binding.account_id AND fi.workspace_id=binding.workspace_id
|
|
2360
|
+
JOIN capability_component_owners owner ON owner.facet_installation_id=fi.id
|
|
2361
|
+
AND owner.account_id=fi.account_id AND owner.workspace_id=fi.workspace_id
|
|
2362
|
+
WHERE receipt.account_id=NEW.account_id AND receipt.workspace_id=NEW.workspace_id
|
|
2363
|
+
AND receipt.receipt ? 'deferredPublication' AND owner.owner_kind=publication_owner_kind AND owner.owner_id=NEW.id::text
|
|
2364
|
+
ORDER BY receipt.created_at,receipt.operation_id
|
|
2365
|
+
LOOP
|
|
2366
|
+
publication_hash := md5('skill-publication:'||pending.operation_id);
|
|
2367
|
+
publication_id := (substr(publication_hash,1,12)||'5'||substr(publication_hash,14,3)||'a'||substr(publication_hash,18,15))::uuid;
|
|
2368
|
+
expected_fingerprint := encode(sha256(convert_to('skill-publication:'||pending.fingerprint,'UTF8')),'hex');
|
|
2369
|
+
IF EXISTS(SELECT 1 FROM skill_write_receipts existing WHERE existing.workspace_id=NEW.workspace_id AND existing.operation_id=publication_id) THEN
|
|
2370
|
+
IF NOT EXISTS(SELECT 1 FROM skill_write_receipts existing WHERE existing.workspace_id=NEW.workspace_id AND existing.operation_id=publication_id
|
|
2371
|
+
AND existing.fingerprint=expected_fingerprint AND existing.receipt->>'sourceOperationId'=pending.operation_id::text) THEN
|
|
2372
|
+
RAISE EXCEPTION 'Skill publication operation identity collision' USING ERRCODE='23505';
|
|
2373
|
+
END IF;
|
|
2374
|
+
CONTINUE;
|
|
2375
|
+
END IF;
|
|
2376
|
+
publication_operation := nullif(current_setting('opengeni.skill_publication_operation_id',true),'')::uuid;
|
|
2377
|
+
IF publication_owner_kind='pack' THEN parent_target := NEW.pack_id;
|
|
2378
|
+
ELSE SELECT plugin_key INTO parent_target FROM capability_plugins WHERE id=NEW.plugin_id; END IF;
|
|
2379
|
+
IF publication_operation IS NULL OR NOT EXISTS(SELECT 1 FROM capability_operations operation
|
|
2380
|
+
WHERE operation.id=publication_operation AND operation.account_id=NEW.account_id AND operation.workspace_id=NEW.workspace_id
|
|
2381
|
+
AND operation.target_kind=publication_owner_kind AND operation.target_id=parent_target AND operation.status='running') THEN
|
|
2382
|
+
RAISE EXCEPTION 'Skill publication requires a claimed parent finalization' USING ERRCODE='42501';
|
|
2383
|
+
END IF;
|
|
2384
|
+
SELECT * INTO head FROM preference_registry_preferences WHERE id=(pending.receipt->>'skillId')::uuid AND account_id=NEW.account_id FOR UPDATE;
|
|
2385
|
+
SELECT * INTO revision FROM preference_registry_revisions WHERE id=(pending.receipt->>'revisionId')::uuid AND account_id=NEW.account_id;
|
|
2386
|
+
disposition := 'preserved'; event_id := NULL; current_mode := 'automatic';
|
|
2387
|
+
IF pending.actor->>'kind' <> 'human' THEN
|
|
2388
|
+
-- Deferred publication uses its original accepted policy. Historical
|
|
2389
|
+
-- receipts receive no new automatic authority during migration.
|
|
2390
|
+
current_mode:=coalesce(pending.receipt#>>'{learningPolicy,effective,skills}','review_first');
|
|
2391
|
+
IF current_mode='review_first' THEN current_mode:='suggest'; END IF;
|
|
2392
|
+
END IF;
|
|
2393
|
+
IF pending.actor->>'kind'='agent' THEN
|
|
2394
|
+
PERFORM 1 FROM sessions session JOIN session_turns turn ON turn.id=session.active_turn_id AND turn.session_id=session.id
|
|
2395
|
+
JOIN session_turn_attempts attempt ON attempt.id=turn.active_attempt_id AND attempt.turn_id=turn.id
|
|
2396
|
+
WHERE session.id=(pending.actor->>'sessionId')::uuid AND session.account_id=NEW.account_id AND session.workspace_id=NEW.workspace_id
|
|
2397
|
+
AND turn.id=(pending.actor->>'turnId')::uuid AND turn.account_id=NEW.account_id AND turn.workspace_id=NEW.workspace_id
|
|
2398
|
+
AND turn.status IN ('running','requires_action','recovering','waiting_capacity')
|
|
2399
|
+
AND attempt.id=(pending.actor->>'attemptId')::uuid AND attempt.account_id=NEW.account_id AND attempt.workspace_id=NEW.workspace_id
|
|
2400
|
+
AND attempt.session_id=session.id AND attempt.execution_generation=(pending.actor->>'executionGeneration')::integer
|
|
2401
|
+
AND turn.execution_generation=attempt.execution_generation AND attempt.state IN ('claimed','running')
|
|
2402
|
+
AND NOT EXISTS(SELECT 1 FROM session_attempt_interruptions interruption WHERE interruption.workspace_id=NEW.workspace_id
|
|
2403
|
+
AND interruption.attempt_id=attempt.id AND interruption.state IN ('pending','delivered','acknowledged'))
|
|
2404
|
+
FOR SHARE OF session,turn,attempt;
|
|
2405
|
+
IF NOT FOUND THEN current_mode := 'suggest'; END IF;
|
|
2406
|
+
END IF;
|
|
2407
|
+
IF head.status NOT IN ('rejected','superseded') AND head.scope='workspace' AND head.scope_workspace_id=NEW.workspace_id
|
|
2408
|
+
AND head.scope_version=(pending.receipt->'deferredPublication'->>'expectedScopeVersion')::integer
|
|
2409
|
+
AND head.active_revision_id IS NOT DISTINCT FROM (pending.receipt->'deferredPublication'->>'expectedRevisionId')::uuid
|
|
2410
|
+
AND revision.revision=(SELECT max(r.revision) FROM preference_registry_revisions r WHERE r.preference_id=head.id)
|
|
2411
|
+
AND (revision.expires_at IS NULL OR revision.expires_at>clock_timestamp())
|
|
2412
|
+
AND EXISTS(SELECT 1 FROM skill_source_bindings binding WHERE binding.preference_id=head.id
|
|
2413
|
+
AND binding.skill_facet_id=(pending.receipt->'deferredPublication'->>'sourceFacetId')::uuid)
|
|
2414
|
+
AND skill_source_has_effective_owner(NEW.account_id,NEW.workspace_id,(pending.receipt->'deferredPublication'->>'sourceFacetId')::uuid)
|
|
2415
|
+
THEN
|
|
2416
|
+
IF current_mode='automatic' THEN
|
|
2417
|
+
PERFORM set_config('opengeni.preference_lifecycle_head_id',head.id::text,true);
|
|
2418
|
+
PERFORM set_config('opengeni.preference_lifecycle_operation','activate',true);
|
|
2419
|
+
UPDATE preference_registry_preferences SET status='active',active_revision_id=revision.id,
|
|
2420
|
+
active_revision=revision.revision,active_content_hash=revision.content_hash,activation_version=activation_version+1,updated_at=clock_timestamp()
|
|
2421
|
+
WHERE id=head.id;
|
|
2422
|
+
SELECT coalesce(max(version),0)+1 INTO next_event FROM preference_registry_events WHERE preference_id=head.id;
|
|
2423
|
+
INSERT INTO preference_registry_events(account_id,preference_id,type,version,old_revision_id,new_revision_id,actor_subject_id,reason)
|
|
2424
|
+
VALUES(NEW.account_id,head.id,'activated',next_event,head.active_revision_id,revision.id,'service:skill-publication',
|
|
2425
|
+
'Publish previously authorized Skill after composite owner finalization') RETURNING id INTO event_id;
|
|
2426
|
+
disposition := 'applied';
|
|
2427
|
+
ELSE disposition := 'pending'; END IF;
|
|
2428
|
+
END IF;
|
|
2429
|
+
result := jsonb_build_object('operationId',publication_id,'sourceOperationId',pending.operation_id,
|
|
2430
|
+
'skillId',pending.receipt->>'skillId','revisionId',pending.receipt->>'revisionId','outcome',disposition,
|
|
2431
|
+
'activationEventId',event_id,'replayed',false,'publicationOperationId',publication_operation);
|
|
2432
|
+
IF disposition='pending' THEN result := result || jsonb_build_object('pendingReason','approval'); END IF;
|
|
2433
|
+
INSERT INTO skill_write_receipts(account_id,workspace_id,operation_id,fingerprint,actor,receipt,activation_event_id)
|
|
2434
|
+
VALUES(NEW.account_id,NEW.workspace_id,publication_id,expected_fingerprint,publication_actor,result,event_id);
|
|
2435
|
+
END LOOP;
|
|
2436
|
+
RETURN NEW;
|
|
2437
|
+
END $$;
|
|
2438
|
+
|
|
2439
|
+
-- Locking an immutable claim needs a narrowly scoped UPDATE policy even though
|
|
2440
|
+
-- no update is permitted. Only the native recovery definer's exact claim can be
|
|
2441
|
+
-- locked; runtime DML grants and the WITH CHECK false remain unchanged.
|
|
2442
|
+
DO $legacy_claim_lock$
|
|
2443
|
+
DECLARE table_name text; claim_column text;
|
|
2444
|
+
BEGIN
|
|
2445
|
+
FOREACH table_name IN ARRAY ARRAY['knowledge_claims','knowledge_claim_evidence','knowledge_change_proposals','knowledge_claim_reviews'] LOOP
|
|
2446
|
+
claim_column:=CASE WHEN table_name='knowledge_claims' THEN 'id' ELSE 'claim_id' END;
|
|
2447
|
+
EXECUTE format('CREATE POLICY knowledge_legacy_confirmation_lock ON %I FOR UPDATE '
|
|
2448
|
+
|| 'USING (current_user=%L AND scope_kind=''workspace'' AND account_id=current_setting(''opengeni.account_id'',true)::uuid '
|
|
2449
|
+
|| 'AND scope_workspace_id=current_setting(''opengeni.workspace_id'',true)::uuid '
|
|
2450
|
+
|| 'AND %I::text=current_setting(''opengeni.knowledge_legacy_lock_claim'',true)) WITH CHECK (false)',table_name,current_user,claim_column);
|
|
2451
|
+
END LOOP;
|
|
2452
|
+
END $legacy_claim_lock$;
|
|
2453
|
+
|
|
2454
|
+
-- Owner-only bridge for answers already bound to native instruction proposals
|
|
2455
|
+
-- at cutover. The original confirmation capability still verifies the exact
|
|
2456
|
+
-- question, initiating human, Task-note evidence and current destination CAS.
|
|
2457
|
+
-- No rebaseline, new draft, or new human authority is synthesized here.
|
|
2458
|
+
CREATE FUNCTION knowledge_instruction_confirm_legacy(p_account uuid,p_workspace uuid,p_actor jsonb,p_request jsonb)
|
|
2459
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT AS $$
|
|
2460
|
+
DECLARE actor jsonb; subject text; candidate record; result jsonb; recovered jsonb;
|
|
2461
|
+
revision workspace_instruction_policy_revisions%ROWTYPE; prior agent_instruction_operations%ROWTYPE;
|
|
2462
|
+
activation governed_learning_activation_receipts%ROWTYPE; unavailable integer:=0; receipts jsonb:='[]'::jsonb;
|
|
2463
|
+
legacy_operation_id uuid; fingerprint text; note_capability uuid:=gen_random_uuid();
|
|
2464
|
+
previous_note_capability text:=current_setting('opengeni.task_note_write_capability',true);
|
|
2465
|
+
previous_legacy_claim text:=current_setting('opengeni.knowledge_legacy_lock_claim',true);
|
|
2466
|
+
BEGIN
|
|
2467
|
+
IF p_actor->>'kind' IS DISTINCT FROM 'agent'
|
|
2468
|
+
OR p_account IS DISTINCT FROM nullif(current_setting('opengeni.account_id',true),'')::uuid
|
|
2469
|
+
OR p_workspace IS DISTINCT FROM nullif(current_setting('opengeni.workspace_id',true),'')::uuid THEN
|
|
2470
|
+
RAISE EXCEPTION 'Legacy instruction confirmation requires exact tenant and agent authority' USING ERRCODE='42501'; END IF;
|
|
2471
|
+
IF p_request->>'operation'='recover' THEN
|
|
2472
|
+
FOR candidate IN
|
|
2473
|
+
SELECT DISTINCT d.proposal_id,d.id AS decision_id,q.id AS request_id
|
|
2474
|
+
FROM agent_instruction_operations imported
|
|
2475
|
+
JOIN governed_learning_decision_receipts d ON d.account_id=imported.account_id AND d.workspace_id=imported.origin_workspace_id
|
|
2476
|
+
AND d.proposal_id::text=imported.actor#>>'{legacyInstructionContext,legacyProposalId}'
|
|
2477
|
+
JOIN session_human_input_requests q ON q.account_id=d.account_id AND q.workspace_id=d.workspace_id
|
|
2478
|
+
AND q.session_id=d.session_id AND q.turn_id=d.turn_id AND q.status='answered'
|
|
2479
|
+
WHERE imported.account_id=p_account AND imported.origin_workspace_id=p_workspace
|
|
2480
|
+
AND imported.request_hash='migration:0461:instruction-review' AND imported.actor->>'kind'='migration'
|
|
2481
|
+
AND d.session_id=(p_actor->>'sessionId')::uuid AND d.turn_id=(p_actor->>'turnId')::uuid
|
|
2482
|
+
AND q.responded_by=d.initiating_human_subject_id
|
|
2483
|
+
AND EXISTS(SELECT 1 FROM jsonb_array_elements(q.response->'answers') a
|
|
2484
|
+
WHERE a->>'questionId'='remember:'||d.proposal_id AND a->'values'='["save"]'::jsonb)
|
|
2485
|
+
ORDER BY d.proposal_id,d.id,q.id
|
|
2486
|
+
LOOP
|
|
2487
|
+
BEGIN
|
|
2488
|
+
recovered:=knowledge_instruction_confirm_legacy(p_account,p_workspace,p_actor,jsonb_build_object(
|
|
2489
|
+
'operationId',overlay(overlay(md5('0461:instruction-confirm:'||candidate.request_id||':'||candidate.decision_id) placing '5' from 13 for 1) placing '8' from 17 for 1)::uuid,
|
|
2490
|
+
'proposalId',candidate.proposal_id,'decisionReceiptId',candidate.decision_id,'humanInputRequestId',candidate.request_id));
|
|
2491
|
+
receipts:=receipts||jsonb_build_array(recovered);
|
|
2492
|
+
EXCEPTION WHEN insufficient_privilege OR serialization_failure OR unique_violation OR check_violation THEN unavailable:=unavailable+1;
|
|
2493
|
+
END;
|
|
2494
|
+
END LOOP;
|
|
2495
|
+
RETURN jsonb_build_object('receipts',receipts,'unavailable',unavailable);
|
|
2496
|
+
END IF;
|
|
2497
|
+
-- Match the native activation lock prefix before validating the live attempt.
|
|
2498
|
+
PERFORM 1 FROM workspaces WHERE account_id=p_account AND id=p_workspace FOR UPDATE;
|
|
2499
|
+
actor:=knowledge_resolve_actor(p_account,p_workspace,p_actor);
|
|
2500
|
+
subject:=nullif(current_setting('opengeni.subject_id',true),'');
|
|
2501
|
+
SELECT r.* INTO revision FROM workspace_instruction_policy_revisions r
|
|
2502
|
+
JOIN agent_instruction_operations imported ON imported.account_id=r.account_id AND imported.revision_id=r.id
|
|
2503
|
+
JOIN governed_learning_decision_receipts d ON d.account_id=r.account_id AND d.workspace_id=r.workspace_id
|
|
2504
|
+
AND d.proposal_id::text=imported.actor#>>'{legacyInstructionContext,legacyProposalId}'
|
|
2505
|
+
WHERE r.account_id=p_account AND r.workspace_id=p_workspace
|
|
2506
|
+
AND imported.request_hash='migration:0461:instruction-review' AND imported.actor->>'kind'='migration'
|
|
2507
|
+
AND d.id=(p_request->>'decisionReceiptId')::uuid AND d.proposal_id=(p_request->>'proposalId')::uuid
|
|
2508
|
+
AND d.initiating_human_subject_id=subject AND d.session_id=(p_actor->>'sessionId')::uuid
|
|
2509
|
+
AND d.turn_id=(p_actor->>'turnId')::uuid
|
|
2510
|
+
ORDER BY r.revision DESC,r.created_at DESC,r.id DESC LIMIT 1;
|
|
2511
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'No imported instruction matches this confirmation' USING ERRCODE='42501'; END IF;
|
|
2512
|
+
legacy_operation_id:=(p_request->>'operationId')::uuid;
|
|
2513
|
+
fingerprint:=encode(sha256(convert_to(jsonb_build_array('legacy-instruction-confirmation',p_request,subject)::text,'UTF8')),'hex');
|
|
2514
|
+
SELECT * INTO prior FROM agent_instruction_operations op WHERE op.account_id=p_account AND op.operation_id=legacy_operation_id;
|
|
2515
|
+
IF FOUND THEN
|
|
2516
|
+
IF prior.request_hash IS DISTINCT FROM fingerprint OR prior.revision_id<>revision.id OR prior.origin_workspace_id<>p_workspace THEN
|
|
2517
|
+
RAISE EXCEPTION 'Legacy instruction confirmation operation conflicts' USING ERRCODE='23505'; END IF;
|
|
2518
|
+
RETURN prior.receipt||jsonb_build_object('replayed',true);
|
|
2519
|
+
END IF;
|
|
2520
|
+
IF EXISTS(SELECT 1 FROM agent_instruction_operations op WHERE op.account_id=p_account AND op.revision_id=revision.id
|
|
2521
|
+
AND op.receipt->>'outcome' IN ('published','rejected'))
|
|
2522
|
+
OR EXISTS(SELECT 1 FROM workspace_instruction_policy_revisions newer WHERE newer.account_id=p_account AND newer.workspace_id=p_workspace
|
|
2523
|
+
AND newer.kind=revision.kind AND newer.scope=revision.scope AND newer.role_key IS NOT DISTINCT FROM revision.role_key
|
|
2524
|
+
AND newer.revision>revision.revision) THEN
|
|
2525
|
+
RAISE EXCEPTION 'Imported instruction review is no longer current' USING ERRCODE='40001'; END IF;
|
|
2526
|
+
PERFORM set_config('opengeni.knowledge_legacy_lock_claim',(SELECT d.claim_id::text FROM governed_learning_decision_receipts d
|
|
2527
|
+
WHERE d.account_id=p_account AND d.id=(p_request->>'decisionReceiptId')::uuid),true);
|
|
2528
|
+
-- The exact attempt was validated above. The existing native confirmation
|
|
2529
|
+
-- checks the bound note/root/version/hash; its read needs the note lifecycle lease.
|
|
2530
|
+
INSERT INTO task_note_write_capabilities(backend_pid,transaction_id,capability_id)
|
|
2531
|
+
VALUES(pg_backend_pid(),pg_current_xact_id(),note_capability);
|
|
2532
|
+
PERFORM set_config('opengeni.task_note_write_capability',note_capability::text,true);
|
|
2533
|
+
SELECT * INTO activation FROM activate_human_confirmed_learning_decision(p_account,p_workspace,
|
|
2534
|
+
legacy_operation_id,(p_request->>'decisionReceiptId')::uuid,(p_request->>'humanInputRequestId')::uuid);
|
|
2535
|
+
DELETE FROM task_note_write_capabilities WHERE backend_pid=pg_backend_pid()
|
|
2536
|
+
AND transaction_id=pg_current_xact_id() AND capability_id=note_capability;
|
|
2537
|
+
PERFORM set_config('opengeni.task_note_write_capability',coalesce(previous_note_capability,''),true);
|
|
2538
|
+
PERFORM set_config('opengeni.knowledge_legacy_lock_claim',coalesce(previous_legacy_claim,''),true);
|
|
2539
|
+
IF activation.id IS NULL OR activation.destination_revision_id IS DISTINCT FROM revision.id THEN
|
|
2540
|
+
RAISE EXCEPTION 'Confirmation must activate the exact imported instruction' USING ERRCODE='40001'; END IF;
|
|
2541
|
+
result:=jsonb_build_object('operationId',legacy_operation_id,'revisionId',revision.id,'outcome','published','reviewBatchId',NULL,'replayed',false);
|
|
2542
|
+
INSERT INTO agent_instruction_operations(account_id,origin_workspace_id,operation_id,revision_id,request_hash,actor,receipt)
|
|
2543
|
+
VALUES(p_account,p_workspace,legacy_operation_id,revision.id,fingerprint,p_actor||jsonb_build_object(
|
|
2544
|
+
'legacyHumanInputRequestId',p_request->'humanInputRequestId','activationReceiptId',activation.id,'initiatingHumanSubjectId',subject),result);
|
|
2545
|
+
RETURN result;
|
|
2546
|
+
END $$;
|
|
2547
|
+
|
|
2548
|
+
-- Recovery only. A migration-bound claim and the exact initiating human's
|
|
2549
|
+
-- canonical answer authorize publication; no legacy Memory or claim is written.
|
|
2550
|
+
CREATE FUNCTION knowledge_entry_confirm_legacy(p_account uuid,p_workspace uuid,p_actor jsonb,p_request jsonb)
|
|
2551
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path FROM CURRENT AS $$
|
|
2552
|
+
DECLARE actor jsonb; entry knowledge_entries%ROWTYPE; revision knowledge_entry_revisions%ROWTYPE;
|
|
2553
|
+
question session_human_input_requests%ROWTYPE; evidence knowledge_claim_evidence%ROWTYPE;
|
|
2554
|
+
prior knowledge_entry_operations%ROWTYPE; human_actor jsonb; note jsonb; subject text;
|
|
2555
|
+
candidate record; recovered jsonb; native_recovery jsonb; unavailable integer:=0;
|
|
2556
|
+
prior_principal text:=current_setting('opengeni.principal_kind',true); result jsonb; exact_text text;
|
|
2557
|
+
BEGIN
|
|
2558
|
+
IF p_actor->>'kind' IS DISTINCT FROM 'agent' THEN
|
|
2559
|
+
RAISE EXCEPTION 'Legacy confirmation requires an exact agent attempt' USING ERRCODE='42501'; END IF;
|
|
2560
|
+
IF p_request ? 'proposalId' THEN
|
|
2561
|
+
RETURN knowledge_instruction_confirm_legacy(p_account,p_workspace,p_actor,p_request);
|
|
2562
|
+
END IF;
|
|
2563
|
+
IF p_request->>'operation'='recover' THEN
|
|
2564
|
+
native_recovery:=knowledge_instruction_confirm_legacy(p_account,p_workspace,p_actor,p_request);
|
|
2565
|
+
END IF;
|
|
2566
|
+
actor:=knowledge_resolve_actor(p_account,p_workspace,p_actor);
|
|
2567
|
+
subject:=nullif(current_setting('opengeni.subject_id',true),'');
|
|
2568
|
+
IF p_request->>'operation'='recover' THEN
|
|
2569
|
+
result:='[]'::jsonb;
|
|
2570
|
+
FOR candidate IN
|
|
2571
|
+
SELECT DISTINCT e.legacy_claim_id AS claim_id,q.id AS request_id
|
|
2572
|
+
FROM session_human_input_requests q CROSS JOIN LATERAL jsonb_array_elements(q.questions) question_value
|
|
2573
|
+
JOIN knowledge_entries e ON e.legacy_claim_id::text=substring(question_value->>'id' FROM 10)
|
|
2574
|
+
AND e.account_id=q.account_id AND e.scope_workspace_id=q.workspace_id
|
|
2575
|
+
JOIN knowledge_entry_revisions r ON r.id=e.latest_revision_id AND r.account_id=e.account_id
|
|
2576
|
+
WHERE q.account_id=p_account AND q.workspace_id=p_workspace AND q.session_id=(p_actor->>'sessionId')::uuid
|
|
2577
|
+
AND q.turn_id=(p_actor->>'turnId')::uuid AND q.status='answered' AND q.responded_by=subject
|
|
2578
|
+
AND question_value->>'id' LIKE 'remember:%' AND r.number=1 AND r.legacy_snapshot IS NOT NULL
|
|
2579
|
+
ORDER BY e.legacy_claim_id,q.id
|
|
2580
|
+
LOOP
|
|
2581
|
+
BEGIN
|
|
2582
|
+
recovered:=knowledge_entry_confirm_legacy(p_account,p_workspace,p_actor,jsonb_build_object(
|
|
2583
|
+
'operationId',overlay(overlay(md5('opengeni:legacy-confirmation:'||candidate.request_id||':'||candidate.claim_id) placing '5' from 13 for 1) placing '8' from 17 for 1)::uuid,
|
|
2584
|
+
'claimId',candidate.claim_id,'humanInputRequestId',candidate.request_id));
|
|
2585
|
+
result:=result||jsonb_build_array(recovered);
|
|
2586
|
+
EXCEPTION WHEN insufficient_privilege OR serialization_failure THEN unavailable:=unavailable+1;
|
|
2587
|
+
END;
|
|
2588
|
+
END LOOP;
|
|
2589
|
+
RETURN jsonb_build_object('receipts',result,'instructionReceipts',native_recovery->'receipts',
|
|
2590
|
+
'unavailable',unavailable+coalesce((native_recovery->>'unavailable')::integer,0));
|
|
2591
|
+
END IF;
|
|
2592
|
+
|
|
2593
|
+
SELECT * INTO entry FROM knowledge_entries e WHERE e.account_id=p_account
|
|
2594
|
+
AND e.legacy_claim_id=(p_request->>'claimId')::uuid AND e.scope='workspace' AND e.scope_workspace_id=p_workspace;
|
|
2595
|
+
IF NOT FOUND OR NOT knowledge_scope_visible(entry) OR subject IS NULL THEN
|
|
2596
|
+
RAISE EXCEPTION 'No migrated Knowledge confirmation matches this claim' USING ERRCODE='42501'; END IF;
|
|
2597
|
+
SELECT f.object_value#>>'{}' INTO exact_text FROM knowledge_claims c JOIN knowledge_facts f ON f.id=c.fact_id AND f.account_id=c.account_id
|
|
2598
|
+
WHERE c.account_id=p_account AND c.id=entry.legacy_claim_id AND c.scope_kind='workspace'
|
|
2599
|
+
AND c.scope_workspace_id=p_workspace AND c.initiating_human_subject_id=subject
|
|
2600
|
+
AND c.extraction_method='task-note-promotion-v1' AND f.object_kind='text';
|
|
2601
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'Migrated confirmation content unavailable' USING ERRCODE='42501'; END IF;
|
|
2602
|
+
SELECT * INTO question FROM session_human_input_requests q WHERE q.account_id=p_account AND q.workspace_id=p_workspace
|
|
2603
|
+
AND q.id=(p_request->>'humanInputRequestId')::uuid AND q.session_id=(p_actor->>'sessionId')::uuid
|
|
2604
|
+
AND q.turn_id=(p_actor->>'turnId')::uuid AND q.turn_generation<=(p_actor->>'executionGeneration')::integer
|
|
2605
|
+
AND q.status='answered' AND q.responded_by=subject AND q.response->>'outcome'='answered'
|
|
2606
|
+
AND EXISTS(SELECT 1 FROM jsonb_array_elements(q.questions) value WHERE value->>'id'='remember:'||entry.legacy_claim_id
|
|
2607
|
+
AND value->>'kind'='single_select' AND value->>'prompt'='Save this as workspace knowledge for everyone in this workspace?'
|
|
2608
|
+
AND value->>'helpText'=left(exact_text,2000) AND value->'options'='[{"id":"save","label":"Save"},{"id":"skip","label":"Don''t save"}]'::jsonb
|
|
2609
|
+
AND coalesce((value->>'allowOther')::boolean,false)=false)
|
|
2610
|
+
AND EXISTS(SELECT 1 FROM jsonb_array_elements(q.response->'answers') value
|
|
2611
|
+
WHERE value->>'questionId'='remember:'||entry.legacy_claim_id AND value->'values'='["save"]'::jsonb);
|
|
2612
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'Exact human confirmation unavailable' USING ERRCODE='42501'; END IF;
|
|
2613
|
+
PERFORM pg_advisory_xact_lock(hashtextextended('knowledge-publication:'||p_account,0));
|
|
2614
|
+
SELECT * INTO prior FROM knowledge_entry_operations o WHERE o.account_id=p_account AND o.operation_id=(p_request->>'operationId')::uuid;
|
|
2615
|
+
IF FOUND THEN
|
|
2616
|
+
IF prior.entry_id<>entry.id OR prior.actor->>'legacyHumanInputRequestId' IS DISTINCT FROM question.id::text THEN
|
|
2617
|
+
RAISE EXCEPTION 'Legacy confirmation operation conflicts' USING ERRCODE='23505'; END IF;
|
|
2618
|
+
RETURN prior.receipt||jsonb_build_object('replayed',true);
|
|
2619
|
+
END IF;
|
|
2620
|
+
SELECT * INTO entry FROM knowledge_entries e WHERE e.id=entry.id AND e.account_id=p_account FOR UPDATE;
|
|
2621
|
+
SELECT * INTO revision FROM knowledge_entry_revisions r WHERE r.id=entry.latest_revision_id AND r.account_id=p_account;
|
|
2622
|
+
IF revision.legacy_snapshot IS NULL OR revision.number<>1 THEN
|
|
2623
|
+
RAISE EXCEPTION 'Migrated Knowledge changed; inspect its current review' USING ERRCODE='40001'; END IF;
|
|
2624
|
+
IF entry.published_revision_id=revision.id THEN
|
|
2625
|
+
RETURN jsonb_build_object('operationId',p_request->>'operationId','entryId',entry.id,'revisionId',revision.id,
|
|
2626
|
+
'version',entry.version,'outcome','published','reviewBatchId',NULL,'replayed',true);
|
|
2627
|
+
END IF;
|
|
2628
|
+
SELECT * INTO evidence FROM knowledge_claim_evidence e WHERE e.account_id=p_account AND e.claim_id=entry.legacy_claim_id
|
|
2629
|
+
AND e.scope_kind='workspace' AND e.scope_workspace_id=p_workspace AND e.polarity='supports'
|
|
2630
|
+
AND e.task_note_id IS NOT NULL AND e.initiating_human_subject_id=subject ORDER BY e.created_at,e.id LIMIT 1;
|
|
2631
|
+
IF NOT FOUND OR EXISTS(SELECT 1 FROM knowledge_claim_evidence e WHERE e.account_id=p_account
|
|
2632
|
+
AND e.claim_id=entry.legacy_claim_id AND e.polarity='contradicts') THEN
|
|
2633
|
+
RAISE EXCEPTION 'Migrated confirmation evidence unavailable' USING ERRCODE='42501'; END IF;
|
|
2634
|
+
note:=knowledge_task_note_source(p_account,p_workspace,p_actor,evidence.task_note_id,evidence.task_note_version);
|
|
2635
|
+
IF note->>'content' IS DISTINCT FROM exact_text OR encode(sha256(convert_to(exact_text,'UTF8')),'hex')<>evidence.content_hash THEN
|
|
2636
|
+
RAISE EXCEPTION 'Migrated confirmation evidence changed' USING ERRCODE='42501'; END IF;
|
|
2637
|
+
human_actor:=jsonb_build_object('kind','human','principalKind','human_session','subjectId',subject,
|
|
2638
|
+
'writeScopes',jsonb_build_array('workspace'),'settingsScopes','[]'::jsonb,'review',true,
|
|
2639
|
+
'legacyHumanInputRequestId',question.id,'confirmedFromAttempt',p_actor);
|
|
2640
|
+
PERFORM set_config('opengeni.principal_kind','human_session',true);
|
|
2641
|
+
result:=knowledge_entry_apply(p_account,p_workspace,human_actor,jsonb_build_object('operation','approve',
|
|
2642
|
+
'operationId',p_request->>'operationId','entryId',entry.id,'revisionId',revision.id,'expectedVersion',entry.version));
|
|
2643
|
+
PERFORM set_config('opengeni.principal_kind',coalesce(prior_principal,''),true);
|
|
2644
|
+
RETURN result;
|
|
2645
|
+
EXCEPTION WHEN OTHERS THEN
|
|
2646
|
+
PERFORM set_config('opengeni.principal_kind',coalesce(prior_principal,''),true);
|
|
2647
|
+
RAISE;
|
|
2648
|
+
END $$;
|
|
2649
|
+
|
|
2650
|
+
-- Only the named gateways are runtime capabilities. Helpers remain owner-only,
|
|
2651
|
+
-- with a pinned dedicated-schema search path and no inherited default grants.
|
|
2652
|
+
DO $secure$
|
|
2653
|
+
DECLARE f record; runtime_role text;
|
|
2654
|
+
BEGIN
|
|
2655
|
+
FOR f IN SELECT p.oid::regprocedure AS identity FROM pg_proc p
|
|
2656
|
+
WHERE p.pronamespace=current_schema()::regnamespace AND (p.proname LIKE 'knowledge_entry_%'
|
|
2657
|
+
OR p.proname IN ('knowledge_reject_history_mutation','knowledge_scope_visible','knowledge_learning_settings_valid',
|
|
2658
|
+
'knowledge_task_note_source','knowledge_instruction_confirm_legacy','knowledge_instruction_context','agent_instruction_apply','knowledge_review_batch_for_actor','knowledge_learning_context_visible','knowledge_learning_resolve','knowledge_learning_for_turn','knowledge_resolve_actor','knowledge_revision_visible','knowledge_document_visible','knowledge_index_revision',
|
|
2659
|
+
'knowledge_enqueue_index','knowledge_source_index_wake','knowledge_index_claim','knowledge_index_work','knowledge_index_retention_active','knowledge_document_prepare',
|
|
2660
|
+
'knowledge_file_owner_immutable','knowledge_media_file_owner_immutable','knowledge_legacy_memory_setting_retired','knowledge_legacy_version_visible','knowledge_validate_links','knowledge_validate_source','knowledge_source_visible','agent_learning_manage')) LOOP
|
|
2661
|
+
EXECUTE format('ALTER FUNCTION %s SET search_path = %I, pg_catalog, pg_temp',f.identity,current_schema());
|
|
2662
|
+
EXECUTE format('REVOKE ALL ON FUNCTION %s FROM PUBLIC',f.identity);
|
|
2663
|
+
FOR runtime_role IN SELECT jsonb_array_elements_text(current_setting('opengeni.migration_application_roles')::jsonb) LOOP
|
|
2664
|
+
IF runtime_role <> current_user AND EXISTS(SELECT 1 FROM pg_roles WHERE rolname=runtime_role) THEN
|
|
2665
|
+
EXECUTE format('REVOKE ALL ON FUNCTION %s FROM %I',f.identity,runtime_role);
|
|
2666
|
+
END IF;
|
|
2667
|
+
END LOOP;
|
|
2668
|
+
END LOOP;
|
|
2669
|
+
END $secure$;
|
|
2670
|
+
|
|
2671
|
+
-- The codec-aware conversion runs inside this same maintenance transaction.
|
|
2672
|
+
-- Published source/memory bytes and IDs are preserved; old tables remain audit
|
|
2673
|
+
-- evidence until their retention boundary, not active retrieval authorities.
|
|
2674
|
+
ALTER TABLE knowledge_review_batches NO FORCE ROW LEVEL SECURITY;
|
|
2675
|
+
ALTER TABLE knowledge_memory_lifecycle_events NO FORCE ROW LEVEL SECURITY;
|
|
2676
|
+
ALTER TABLE knowledge_entities NO FORCE ROW LEVEL SECURITY;
|
|
2677
|
+
ALTER TABLE knowledge_entity_aliases NO FORCE ROW LEVEL SECURITY;
|
|
2678
|
+
ALTER TABLE knowledge_facts NO FORCE ROW LEVEL SECURITY;
|
|
2679
|
+
ALTER TABLE knowledge_claims NO FORCE ROW LEVEL SECURITY;
|
|
2680
|
+
ALTER TABLE knowledge_claim_evidence NO FORCE ROW LEVEL SECURITY;
|
|
2681
|
+
ALTER TABLE knowledge_claim_reviews NO FORCE ROW LEVEL SECURITY;
|
|
2682
|
+
ALTER TABLE knowledge_claim_relations NO FORCE ROW LEVEL SECURITY;
|
|
2683
|
+
ALTER TABLE knowledge_change_proposals NO FORCE ROW LEVEL SECURITY;
|
|
2684
|
+
ALTER TABLE knowledge_document_versions NO FORCE ROW LEVEL SECURITY;
|
|
2685
|
+
ALTER TABLE knowledge_source_objects NO FORCE ROW LEVEL SECURITY;
|
|
2686
|
+
ALTER TABLE knowledge_sources NO FORCE ROW LEVEL SECURITY;
|
|
2687
|
+
ALTER TABLE knowledge_providers NO FORCE ROW LEVEL SECURITY;
|
|
2688
|
+
ALTER TABLE knowledge_source_acl_versions NO FORCE ROW LEVEL SECURITY;
|
|
2689
|
+
ALTER TABLE knowledge_memories NO FORCE ROW LEVEL SECURITY;
|
|
2690
|
+
ALTER TABLE knowledge_memory_relationships NO FORCE ROW LEVEL SECURITY;
|
|
2691
|
+
ALTER TABLE documents NO FORCE ROW LEVEL SECURITY;
|
|
2692
|
+
ALTER TABLE document_chunks NO FORCE ROW LEVEL SECURITY;
|
|
2693
|
+
ALTER TABLE document_bases NO FORCE ROW LEVEL SECURITY;
|
|
2694
|
+
ALTER TABLE knowledge_entries NO FORCE ROW LEVEL SECURITY;
|
|
2695
|
+
ALTER TABLE knowledge_entry_revisions NO FORCE ROW LEVEL SECURITY;
|
|
2696
|
+
ALTER TABLE knowledge_entry_decisions NO FORCE ROW LEVEL SECURITY;
|
|
2697
|
+
ALTER TABLE knowledge_entry_links NO FORCE ROW LEVEL SECURITY;
|
|
2698
|
+
ALTER TABLE knowledge_entry_search NO FORCE ROW LEVEL SECURITY;
|
|
2699
|
+
ALTER TABLE knowledge_index_jobs NO FORCE ROW LEVEL SECURITY;
|
|
2700
|
+
ALTER TABLE remember_knowledge_confirmation_receipts NO FORCE ROW LEVEL SECURITY;
|
|
2701
|
+
ALTER TABLE remember_knowledge_memory_materializations NO FORCE ROW LEVEL SECURITY;
|
|
2702
|
+
-- Keep pending instruction drafts in the one review surface without changing
|
|
2703
|
+
-- their text, provenance, original baseline, or native activation authority.
|
|
2704
|
+
ALTER TABLE workspace_instruction_policy_onboarding_proposals NO FORCE ROW LEVEL SECURITY;
|
|
2705
|
+
ALTER TABLE workspace_instruction_policy_revisions NO FORCE ROW LEVEL SECURITY;
|
|
2706
|
+
ALTER TABLE workspace_instruction_policy_activation_events NO FORCE ROW LEVEL SECURITY;
|
|
2707
|
+
ALTER TABLE governed_learning_decision_receipts NO FORCE ROW LEVEL SECURITY;
|
|
2708
|
+
ALTER TABLE agent_instruction_operations NO FORCE ROW LEVEL SECURITY;
|
|
2709
|
+
INSERT INTO agent_instruction_operations(account_id,origin_workspace_id,operation_id,revision_id,request_hash,actor,receipt,created_at)
|
|
2710
|
+
SELECT r.account_id,r.workspace_id,md5('0461:instruction-review:'||r.id)::uuid,r.id,'migration:0461:instruction-review',
|
|
2711
|
+
jsonb_build_object('kind','migration','subjectId','service:knowledge-migration:0461','legacyInstructionContext',
|
|
2712
|
+
jsonb_build_object('actor',jsonb_build_object('kind','migration','sessionId',decision.session_id),
|
|
2713
|
+
'reason','Pending instruction from the previous review system','reviewBatchId',NULL,'evidence','[]'::jsonb,
|
|
2714
|
+
'legacyProposalId',proposal.id,'legacyOnboardingId',onboarding.id,
|
|
2715
|
+
'expectedCurrentRevisionId',onboarding.baseline_revision_id,'expectedActivationVersion',onboarding.baseline_activation_version)),
|
|
2716
|
+
jsonb_build_object('operationId',md5('0461:instruction-review:'||r.id)::uuid,'revisionId',r.id,
|
|
2717
|
+
'outcome','pending','reviewBatchId',NULL,'replayed',false),r.created_at
|
|
2718
|
+
FROM workspace_instruction_policy_revisions r
|
|
2719
|
+
JOIN workspace_instruction_policy_onboarding_proposals onboarding ON onboarding.account_id=r.account_id
|
|
2720
|
+
AND onboarding.workspace_id=r.workspace_id AND onboarding.draft_revision_id=r.id AND onboarding.status='proposed'
|
|
2721
|
+
JOIN knowledge_change_proposals proposal ON proposal.account_id=r.account_id AND proposal.id::text=onboarding.source_id
|
|
2722
|
+
AND proposal.target_kind='instruction_policy' AND proposal.status='proposed' AND proposal.scope_kind='workspace'
|
|
2723
|
+
AND proposal.scope_workspace_id=r.workspace_id AND proposal.content_hash=onboarding.source_version
|
|
2724
|
+
LEFT JOIN LATERAL (SELECT d.session_id FROM governed_learning_decision_receipts d
|
|
2725
|
+
WHERE d.account_id=proposal.account_id AND d.workspace_id=r.workspace_id AND d.proposal_id=proposal.id
|
|
2726
|
+
ORDER BY d.created_at DESC,d.id DESC LIMIT 1) decision ON true
|
|
2727
|
+
WHERE NOT EXISTS(SELECT 1 FROM workspace_instruction_policy_activation_events ev
|
|
2728
|
+
WHERE ev.account_id=r.account_id AND ev.workspace_id=r.workspace_id AND ev.new_revision_id=r.id)
|
|
2729
|
+
AND coalesce((SELECT review.state FROM knowledge_claim_reviews review WHERE review.account_id=r.account_id
|
|
2730
|
+
AND review.claim_id=proposal.claim_id ORDER BY review.review_revision DESC LIMIT 1),'proposed') IN ('proposed','approved');
|
|
2731
|
+
ALTER TABLE agent_instruction_operations FORCE ROW LEVEL SECURITY;
|
|
2732
|
+
ALTER TABLE governed_learning_decision_receipts FORCE ROW LEVEL SECURITY;
|
|
2733
|
+
ALTER TABLE workspace_instruction_policy_activation_events FORCE ROW LEVEL SECURITY;
|
|
2734
|
+
ALTER TABLE workspace_instruction_policy_revisions FORCE ROW LEVEL SECURITY;
|
|
2735
|
+
ALTER TABLE workspace_instruction_policy_onboarding_proposals FORCE ROW LEVEL SECURITY;
|
|
2736
|
+
-- Native pending preference proposals become reviewable Skill folder revisions.
|
|
2737
|
+
ALTER TABLE preference_registry_preferences NO FORCE ROW LEVEL SECURITY;
|
|
2738
|
+
ALTER TABLE preference_registry_revisions NO FORCE ROW LEVEL SECURITY;
|
|
2739
|
+
ALTER TABLE preference_registry_events NO FORCE ROW LEVEL SECURITY;
|
|
2740
|
+
ALTER TABLE company_brain_preference_proposal_receipts NO FORCE ROW LEVEL SECURITY;
|
|
2741
|
+
ALTER TABLE skill_write_receipts NO FORCE ROW LEVEL SECURITY;
|
|
2742
|
+
-- opengeni:unified-knowledge-copy-v1
|
|
2743
|
+
SET CONSTRAINTS ALL IMMEDIATE;
|
|
2744
|
+
ALTER TABLE preference_registry_preferences FORCE ROW LEVEL SECURITY;
|
|
2745
|
+
ALTER TABLE preference_registry_revisions FORCE ROW LEVEL SECURITY;
|
|
2746
|
+
ALTER TABLE preference_registry_events FORCE ROW LEVEL SECURITY;
|
|
2747
|
+
ALTER TABLE company_brain_preference_proposal_receipts FORCE ROW LEVEL SECURITY;
|
|
2748
|
+
ALTER TABLE skill_write_receipts FORCE ROW LEVEL SECURITY;
|
|
2749
|
+
DO $copied$
|
|
2750
|
+
BEGIN
|
|
2751
|
+
IF (SELECT count(*) FROM pg_temp.knowledge_conversion_0461 WHERE completed)<>1 THEN
|
|
2752
|
+
RAISE EXCEPTION '0461 Knowledge conversion did not complete' USING ERRCODE='55000';
|
|
2753
|
+
END IF;
|
|
2754
|
+
END $copied$;
|
|
2755
|
+
-- Validate deferred head/revision references before restoring table posture.
|
|
2756
|
+
SET CONSTRAINTS ALL IMMEDIATE;
|
|
2757
|
+
ALTER TABLE knowledge_review_batches FORCE ROW LEVEL SECURITY;
|
|
2758
|
+
ALTER TABLE knowledge_memory_lifecycle_events FORCE ROW LEVEL SECURITY;
|
|
2759
|
+
ALTER TABLE knowledge_entities FORCE ROW LEVEL SECURITY;
|
|
2760
|
+
ALTER TABLE knowledge_entity_aliases FORCE ROW LEVEL SECURITY;
|
|
2761
|
+
ALTER TABLE knowledge_facts FORCE ROW LEVEL SECURITY;
|
|
2762
|
+
ALTER TABLE knowledge_claims FORCE ROW LEVEL SECURITY;
|
|
2763
|
+
ALTER TABLE knowledge_claim_evidence FORCE ROW LEVEL SECURITY;
|
|
2764
|
+
ALTER TABLE knowledge_claim_reviews FORCE ROW LEVEL SECURITY;
|
|
2765
|
+
ALTER TABLE knowledge_claim_relations FORCE ROW LEVEL SECURITY;
|
|
2766
|
+
ALTER TABLE knowledge_change_proposals FORCE ROW LEVEL SECURITY;
|
|
2767
|
+
ALTER TABLE knowledge_document_versions FORCE ROW LEVEL SECURITY;
|
|
2768
|
+
ALTER TABLE knowledge_source_objects FORCE ROW LEVEL SECURITY;
|
|
2769
|
+
ALTER TABLE knowledge_sources FORCE ROW LEVEL SECURITY;
|
|
2770
|
+
ALTER TABLE knowledge_providers FORCE ROW LEVEL SECURITY;
|
|
2771
|
+
ALTER TABLE knowledge_source_acl_versions FORCE ROW LEVEL SECURITY;
|
|
2772
|
+
ALTER TABLE knowledge_memories FORCE ROW LEVEL SECURITY;
|
|
2773
|
+
ALTER TABLE knowledge_memory_relationships FORCE ROW LEVEL SECURITY;
|
|
2774
|
+
ALTER TABLE documents FORCE ROW LEVEL SECURITY;
|
|
2775
|
+
ALTER TABLE document_chunks FORCE ROW LEVEL SECURITY;
|
|
2776
|
+
ALTER TABLE document_bases FORCE ROW LEVEL SECURITY;
|
|
2777
|
+
ALTER TABLE knowledge_entries FORCE ROW LEVEL SECURITY;
|
|
2778
|
+
ALTER TABLE knowledge_entry_revisions FORCE ROW LEVEL SECURITY;
|
|
2779
|
+
ALTER TABLE knowledge_entry_decisions FORCE ROW LEVEL SECURITY;
|
|
2780
|
+
ALTER TABLE knowledge_entry_links FORCE ROW LEVEL SECURITY;
|
|
2781
|
+
ALTER TABLE knowledge_entry_search FORCE ROW LEVEL SECURITY;
|
|
2782
|
+
ALTER TABLE knowledge_index_jobs FORCE ROW LEVEL SECURITY;
|
|
2783
|
+
|
|
2784
|
+
ALTER TABLE remember_knowledge_confirmation_receipts FORCE ROW LEVEL SECURITY;
|
|
2785
|
+
ALTER TABLE remember_knowledge_memory_materializations FORCE ROW LEVEL SECURITY;
|
|
2786
|
+
|
|
2787
|
+
-- Retired Memory is historical evidence only. Even an accidentally retained
|
|
2788
|
+
-- owner-definer helper cannot start a competing Memory writer after cutover.
|
|
2789
|
+
CREATE FUNCTION knowledge_retired_memory_read_only() RETURNS trigger
|
|
2790
|
+
LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
|
2791
|
+
BEGIN
|
|
2792
|
+
IF TG_OP='DELETE' AND pg_trigger_depth()>1 AND (
|
|
2793
|
+
NOT EXISTS(SELECT 1 FROM workspaces WHERE id=OLD.workspace_id AND account_id=OLD.account_id)
|
|
2794
|
+
OR NOT EXISTS(SELECT 1 FROM managed_accounts WHERE id=OLD.account_id)) THEN RETURN OLD; END IF;
|
|
2795
|
+
RAISE EXCEPTION 'Memory was replaced by Knowledge; historical Memory is read-only' USING ERRCODE='55000';
|
|
2796
|
+
END $$;
|
|
2797
|
+
CREATE TRIGGER knowledge_memories_retired BEFORE INSERT OR UPDATE OR DELETE ON knowledge_memories
|
|
2798
|
+
FOR EACH ROW EXECUTE FUNCTION knowledge_retired_memory_read_only();
|
|
2799
|
+
DO $retire_memory$
|
|
2800
|
+
DECLARE f record; grantee text;
|
|
2801
|
+
BEGIN
|
|
2802
|
+
FOR f IN SELECT p.oid,p.oid::regprocedure AS identity,p.proowner FROM pg_proc p
|
|
2803
|
+
WHERE p.pronamespace=current_schema()::regnamespace AND p.proname IN (
|
|
2804
|
+
'knowledge_memory_apply_operation','knowledge_memory_revert_operation',
|
|
2805
|
+
'materialize_remember_knowledge_memory','confirm_remember_knowledge_claim','knowledge_retired_memory_read_only',
|
|
2806
|
+
'evaluate_governed_learning_proposal','activate_governed_learning_decision','activate_human_confirmed_learning_decision') LOOP
|
|
2807
|
+
EXECUTE format('ALTER FUNCTION %s SET search_path = %I, pg_catalog, pg_temp',f.identity,current_schema());
|
|
2808
|
+
EXECUTE format('REVOKE ALL ON FUNCTION %s FROM PUBLIC',f.identity);
|
|
2809
|
+
FOR grantee IN SELECT DISTINCT r.rolname FROM pg_proc p,
|
|
2810
|
+
LATERAL aclexplode(coalesce(p.proacl,acldefault('f',p.proowner))) acl
|
|
2811
|
+
JOIN pg_roles r ON r.oid=acl.grantee WHERE p.oid=f.oid AND acl.grantee<>f.proowner LOOP
|
|
2812
|
+
EXECUTE format('REVOKE ALL ON FUNCTION %s FROM %I',f.identity,grantee);
|
|
2813
|
+
END LOOP;
|
|
2814
|
+
END LOOP;
|
|
2815
|
+
FOR grantee IN SELECT DISTINCT r.rolname FROM pg_class t,
|
|
2816
|
+
LATERAL aclexplode(coalesce(t.relacl,acldefault('r',t.relowner))) acl
|
|
2817
|
+
JOIN pg_roles r ON r.oid=acl.grantee WHERE t.oid='knowledge_memories'::regclass AND acl.grantee<>t.relowner LOOP
|
|
2818
|
+
EXECUTE format('REVOKE INSERT,UPDATE,DELETE,TRUNCATE,REFERENCES,TRIGGER ON knowledge_memories FROM %I',grantee);
|
|
2819
|
+
END LOOP;
|
|
2820
|
+
END $retire_memory$;
|
|
2821
|
+
|
|
2822
|
+
-- Historical learning snapshots still explain old decisions. Their policy
|
|
2823
|
+
-- authority is frozen: all new settings mutations use agent_learning_manage.
|
|
2824
|
+
CREATE FUNCTION knowledge_retired_learning_read_only() RETURNS trigger
|
|
2825
|
+
LANGUAGE plpgsql SET search_path FROM CURRENT AS $$
|
|
2826
|
+
BEGIN
|
|
2827
|
+
IF TG_OP='DELETE' AND pg_trigger_depth()>1 AND (
|
|
2828
|
+
NOT EXISTS(SELECT 1 FROM workspaces WHERE id=OLD.workspace_id AND account_id=OLD.account_id)
|
|
2829
|
+
OR NOT EXISTS(SELECT 1 FROM managed_accounts WHERE id=OLD.account_id)) THEN RETURN OLD; END IF;
|
|
2830
|
+
RAISE EXCEPTION 'Learning settings moved to Agent learning' USING ERRCODE='55000';
|
|
2831
|
+
END $$;
|
|
2832
|
+
CREATE TRIGGER knowledge_learning_revisions_retired BEFORE INSERT OR UPDATE OR DELETE ON workspace_learning_policy_revisions
|
|
2833
|
+
FOR EACH ROW EXECUTE FUNCTION knowledge_retired_learning_read_only();
|
|
2834
|
+
CREATE TRIGGER knowledge_learning_heads_retired BEFORE INSERT OR UPDATE OR DELETE ON workspace_learning_policy_heads
|
|
2835
|
+
FOR EACH ROW EXECUTE FUNCTION knowledge_retired_learning_read_only();
|
|
2836
|
+
CREATE TRIGGER knowledge_learning_events_retired BEFORE INSERT OR UPDATE OR DELETE ON workspace_learning_policy_activation_events
|
|
2837
|
+
FOR EACH ROW EXECUTE FUNCTION knowledge_retired_learning_read_only();
|
|
2838
|
+
DO $retire_learning$
|
|
2839
|
+
DECLARE f record; grantee text; t text;
|
|
2840
|
+
BEGIN
|
|
2841
|
+
FOR f IN SELECT p.oid,p.oid::regprocedure AS identity,p.proowner FROM pg_proc p
|
|
2842
|
+
WHERE p.pronamespace=current_schema()::regnamespace AND p.proname IN (
|
|
2843
|
+
'workspace_learning_policy_apply_activation','knowledge_retired_learning_read_only') LOOP
|
|
2844
|
+
EXECUTE format('ALTER FUNCTION %s SET search_path = %I, pg_catalog, pg_temp',f.identity,current_schema());
|
|
2845
|
+
EXECUTE format('REVOKE ALL ON FUNCTION %s FROM PUBLIC',f.identity);
|
|
2846
|
+
FOR grantee IN SELECT DISTINCT r.rolname FROM pg_proc p,
|
|
2847
|
+
LATERAL aclexplode(coalesce(p.proacl,acldefault('f',p.proowner))) acl
|
|
2848
|
+
JOIN pg_roles r ON r.oid=acl.grantee WHERE p.oid=f.oid AND acl.grantee<>f.proowner LOOP
|
|
2849
|
+
EXECUTE format('REVOKE ALL ON FUNCTION %s FROM %I',f.identity,grantee);
|
|
2850
|
+
END LOOP;
|
|
2851
|
+
END LOOP;
|
|
2852
|
+
FOREACH t IN ARRAY ARRAY['workspace_learning_policy_revisions','workspace_learning_policy_heads','workspace_learning_policy_activation_events'] LOOP
|
|
2853
|
+
FOR grantee IN SELECT DISTINCT r.rolname FROM pg_class c,
|
|
2854
|
+
LATERAL aclexplode(coalesce(c.relacl,acldefault('r',c.relowner))) acl
|
|
2855
|
+
JOIN pg_roles r ON r.oid=acl.grantee WHERE c.oid=t::regclass AND acl.grantee<>c.relowner LOOP
|
|
2856
|
+
EXECUTE format('REVOKE INSERT,UPDATE,DELETE,TRUNCATE,REFERENCES,TRIGGER ON %I FROM %I',t,grantee);
|
|
2857
|
+
END LOOP;
|
|
2858
|
+
END LOOP;
|
|
2859
|
+
END $retire_learning$;
|