@opengeni/db 0.28.9 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/dist/attached-browser-devices.d.ts +42 -0
  2. package/dist/attempt-tool-catalogs.d.ts +16 -0
  3. package/dist/browser-auth.d.ts +299 -0
  4. package/dist/browser-downloads.d.ts +51 -0
  5. package/dist/browser-identities.d.ts +137 -0
  6. package/dist/browser-sessions.d.ts +354 -0
  7. package/dist/browser-state-artifacts.d.ts +91 -0
  8. package/dist/canonical-human-identities.d.ts +48 -0
  9. package/dist/canonical-human-identities.js +28 -0
  10. package/dist/canonical-human-identities.js.map +1 -0
  11. package/dist/capability-components.d.ts +37 -0
  12. package/dist/capability-integrations.d.ts +194 -0
  13. package/dist/chunk-5KTIEDX7.js +13200 -0
  14. package/dist/chunk-5KTIEDX7.js.map +1 -0
  15. package/dist/chunk-CQZ4QSP5.js +995 -0
  16. package/dist/chunk-CQZ4QSP5.js.map +1 -0
  17. package/dist/chunk-F32YT7MP.js +117 -0
  18. package/dist/chunk-F32YT7MP.js.map +1 -0
  19. package/dist/chunk-JC5QPLUM.js +666 -0
  20. package/dist/chunk-JC5QPLUM.js.map +1 -0
  21. package/dist/chunk-P3RE4D2B.js +3535 -0
  22. package/dist/chunk-P3RE4D2B.js.map +1 -0
  23. package/dist/chunk-TAUTWOS5.js +259 -0
  24. package/dist/chunk-TAUTWOS5.js.map +1 -0
  25. package/dist/chunk-VKLB2ZGI.js +2389 -0
  26. package/dist/chunk-VKLB2ZGI.js.map +1 -0
  27. package/dist/codemode-operations.d.ts +117 -0
  28. package/dist/codex-token-resolver.d.ts +2 -2
  29. package/dist/company-profile-schema.d.ts +798 -0
  30. package/dist/company-profile.d.ts +268 -0
  31. package/dist/computer-sessions.d.ts +109 -0
  32. package/dist/connection-token-resolver.d.ts +9 -4
  33. package/dist/database.d.ts +57 -3
  34. package/dist/editable-artifact-durable-export.d.ts +153 -0
  35. package/dist/editable-artifact-durable-export.js +788 -0
  36. package/dist/editable-artifact-durable-export.js.map +1 -0
  37. package/dist/editable-artifact-materialization.d.ts +123 -0
  38. package/dist/editable-artifacts-schema.d.ts +4152 -0
  39. package/dist/editable-artifacts.d.ts +610 -0
  40. package/dist/editable-artifacts.js +26 -0
  41. package/dist/editable-artifacts.js.map +1 -0
  42. package/dist/generated-images.d.ts +139 -0
  43. package/dist/index.d.ts +1122 -126
  44. package/dist/index.js +42329 -18407
  45. package/dist/index.js.map +1 -1
  46. package/dist/insights.d.ts +73 -10
  47. package/dist/integration-bindings.d.ts +70 -0
  48. package/dist/integration-facets.d.ts +99 -0
  49. package/dist/interaction-revisions.d.ts +14 -0
  50. package/dist/interaction-schema.d.ts +6036 -0
  51. package/dist/knowledge-source-sync-schema.d.ts +1679 -0
  52. package/dist/knowledge-source-sync.d.ts +274 -0
  53. package/dist/memory-governance.d.ts +2 -2
  54. package/dist/memory-slack-delivery.d.ts +101 -0
  55. package/dist/new-session-drafts.d.ts +2 -2
  56. package/dist/pack-components.d.ts +76 -0
  57. package/dist/pack-installations.d.ts +86 -0
  58. package/dist/persistence-errors.d.ts +3 -3
  59. package/dist/plugin-packages.d.ts +126 -0
  60. package/dist/preference-registry.d.ts +1 -1
  61. package/dist/provision-roles.d.ts +9 -1
  62. package/dist/provision-roles.js +1 -1
  63. package/dist/runtime-posture.d.ts +19 -6
  64. package/dist/schema.d.ts +22654 -9359
  65. package/dist/schema.js +209 -3
  66. package/dist/scoped-knowledge.d.ts +97 -1
  67. package/dist/session-control.d.ts +33 -7
  68. package/dist/session-queue-commands.d.ts +19 -11
  69. package/dist/session-realtime-context.d.ts +2 -2
  70. package/dist/session-realtime-ledger.d.ts +4 -4
  71. package/dist/session-realtime-mirror.d.ts +2 -2
  72. package/dist/session-realtime-state.d.ts +1 -1
  73. package/dist/session-realtime-terminal.d.ts +1 -1
  74. package/dist/session-realtime.d.ts +7 -7
  75. package/dist/session-tenancy.d.ts +44 -0
  76. package/dist/session-tenancy.js +16 -0
  77. package/dist/session-tenancy.js.map +1 -0
  78. package/dist/session-tool-call-settlement.d.ts +1 -1
  79. package/dist/slack-task-policy-schema.d.ts +847 -0
  80. package/dist/slack-task-policy.d.ts +65 -0
  81. package/dist/slack-user-link-access.d.ts +54 -0
  82. package/dist/task-notes-schema.d.ts +924 -0
  83. package/dist/task-notes.d.ts +85 -0
  84. package/dist/transcription-recordings.d.ts +2 -2
  85. package/dist/turn-initiator.d.ts +2 -2
  86. package/dist/video-generation.d.ts +225 -0
  87. package/dist/video-generation.js +68 -0
  88. package/dist/video-generation.js.map +1 -0
  89. package/dist/workspace-artifacts.d.ts +1 -1
  90. package/dist/workspace-instruction-policies.d.ts +1 -1
  91. package/dist/workspace-learning-policy-schema.d.ts +932 -0
  92. package/dist/workspace-learning-policy.d.ts +161 -0
  93. package/dist/xai-subscription.d.ts +326 -0
  94. package/drizzle/0183_model_call_provider_cost_estimates.sql +39 -0
  95. package/drizzle/0184_sandbox_drain_teardown_fence.sql +682 -0
  96. package/drizzle/0185_temporal_schedule_cleanup_outbox.sql +168 -0
  97. package/drizzle/0186_sandbox_capture_provider_contract.sql +400 -0
  98. package/drizzle/0187_generated_image_artifacts.sql +173 -0
  99. package/drizzle/0188_image_generation_retention_failure.sql +25 -0
  100. package/drizzle/0189_retained_session_image_formats.sql +13 -0
  101. package/drizzle/0190_timeline_annotations.sql +31 -0
  102. package/drizzle/0191_editable_artifact_engine.sql +3853 -0
  103. package/drizzle/0192_editable_artifact_live_tickets.sql +338 -0
  104. package/drizzle/0193_editable_artifact_authorization.sql +190 -0
  105. package/drizzle/0194_editable_artifact_durable_exports.sql +160 -0
  106. package/drizzle/0195_editable_artifact_import_authorization.sql +203 -0
  107. package/drizzle/0196_rig_provider_images.sql +152 -0
  108. package/drizzle/0197_knowledge_source_sync_schedules.sql +523 -0
  109. package/drizzle/0198_memory_slack_publication_delivery.sql +393 -0
  110. package/drizzle/0199_workspace_learning_policy.sql +980 -0
  111. package/drizzle/0201_company_profile_authority.sql +849 -0
  112. package/drizzle/0202_document_index_checkpoints.sql +85 -0
  113. package/drizzle/0203_durable_video_generation.sql +353 -0
  114. package/drizzle/0204_video_generation_funding.sql +44 -0
  115. package/drizzle/0205_attempt_tool_catalogs.sql +230 -0
  116. package/drizzle/0206_browser_sessions.sql +646 -0
  117. package/drizzle/0207_browser_identities.sql +379 -0
  118. package/drizzle/0208_attached_browser_devices.sql +165 -0
  119. package/drizzle/0209_computer_sessions.sql +575 -0
  120. package/drizzle/0210_browser_auth_network_interventions.sql +441 -0
  121. package/drizzle/0211_editable_artifact_session_links.sql +58 -0
  122. package/drizzle/0212_browser_state_transfer_hardening.sql +560 -0
  123. package/drizzle/0212_slack_installation_bindings.sql +348 -0
  124. package/drizzle/0213_browser_interaction_authority.sql +67 -0
  125. package/drizzle/0213_slack_user_link_access_requests.sql +177 -0
  126. package/drizzle/0214_browser_download_saves.sql +34 -0
  127. package/drizzle/0214_session_activity_commit_gate.sql +262 -0
  128. package/drizzle/0215_browser_controller_host.sql +385 -0
  129. package/drizzle/0215_capabilities_platform.sql +980 -0
  130. package/drizzle/0216_browser_auth_health_evidence.sql +137 -0
  131. package/drizzle/0216_pack_component_ownership.sql +235 -0
  132. package/drizzle/0217_capability_definition_delete_authority.sql +32 -0
  133. package/drizzle/0217_external_browser_auth_operations.sql +22 -0
  134. package/drizzle/0218_organization_tenancy_foundation.sql +299 -0
  135. package/drizzle/0219_organization_tenancy_managed_human_provisioning.sql +317 -0
  136. package/drizzle/0219_site_auth_maintenance_sessions.sql +234 -0
  137. package/drizzle/0220_memory_slack_append_only_cascade.sql +38 -0
  138. package/drizzle/0220_session_channels.sql +54 -0
  139. package/drizzle/0221_sessions_channel_index.sql +5 -0
  140. package/drizzle/0222_session_visibility_authority_epochs.sql +158 -0
  141. package/drizzle/0222_sessions_channel_fk.sql +89 -0
  142. package/drizzle/0223_pending_tool_event_output.sql +55 -0
  143. package/drizzle/0223_sessions_channel_fk_validate.sql +118 -0
  144. package/drizzle/0224_slack_post_outcome_reconciliation.sql +123 -0
  145. package/drizzle/0225_session_visibility_fork_activation.sql +1363 -0
  146. package/drizzle/0226_personal_codex_authority_foundation.sql +365 -0
  147. package/drizzle/0227_slack_native_actions.sql +145 -0
  148. package/drizzle/0228_interaction_controller_data_plane.sql +14 -0
  149. package/drizzle/0228_slack_task_policy.sql +400 -0
  150. package/drizzle/0229_slack_inbox_file_fact.sql +10 -0
  151. package/drizzle/0230_user_scoped_variable_sets_rigs.sql +84 -0
  152. package/drizzle/0231_integration_definition_identity_cutover.sql +271 -0
  153. package/drizzle/0232_integration_facet_authority_cutover.sql +610 -0
  154. package/drizzle/0233_skill_and_integration_authority_cutover.sql +1162 -0
  155. package/drizzle/0234_xai_subscription_authority.sql +1139 -0
  156. package/drizzle/0235_canonical_human_login_bindings.sql +920 -0
  157. package/drizzle/0236_browser_identity_lifecycle.sql +39 -0
  158. package/drizzle/0236_session_visibility_slack_policy.sql +72 -0
  159. package/drizzle/0237_interaction_transition_reaper.sql +263 -0
  160. package/drizzle/0238_goal_persistence_policy.sql +471 -0
  161. package/drizzle/0238_supergrok_realtime_model.sql +29 -0
  162. package/drizzle/0239_supergrok_video_funding.sql +50 -0
  163. package/drizzle/0239_task_tree_notes.sql +990 -0
  164. package/package.json +26 -5
  165. package/src/attached-browser-devices.ts +371 -0
  166. package/src/attempt-tool-catalogs.ts +131 -0
  167. package/src/browser-auth.ts +4373 -0
  168. package/src/browser-downloads.ts +517 -0
  169. package/src/browser-identities.ts +1385 -0
  170. package/src/browser-sessions.ts +2294 -0
  171. package/src/browser-state-artifacts.ts +449 -0
  172. package/src/canonical-human-identities.ts +317 -0
  173. package/src/capability-components.ts +166 -0
  174. package/src/capability-integrations.ts +1571 -0
  175. package/src/codemode-operations.ts +674 -0
  176. package/src/company-profile-schema.ts +143 -0
  177. package/src/company-profile.ts +968 -0
  178. package/src/computer-sessions.ts +1130 -0
  179. package/src/connection-token-resolver.ts +270 -25
  180. package/src/database.ts +468 -24
  181. package/src/editable-artifact-durable-export.ts +1201 -0
  182. package/src/editable-artifact-materialization.ts +893 -0
  183. package/src/editable-artifacts-schema.ts +879 -0
  184. package/src/editable-artifacts.ts +5404 -0
  185. package/src/generated-images.ts +740 -0
  186. package/src/index.ts +9963 -1465
  187. package/src/insights.ts +251 -23
  188. package/src/integration-bindings.ts +455 -0
  189. package/src/integration-facets.ts +862 -0
  190. package/src/interaction-revisions.ts +70 -0
  191. package/src/interaction-schema.ts +1606 -0
  192. package/src/knowledge-source-sync-schema.ts +192 -0
  193. package/src/knowledge-source-sync.ts +1922 -0
  194. package/src/memory-slack-delivery.ts +801 -0
  195. package/src/pack-components.ts +954 -0
  196. package/src/pack-installations.ts +886 -0
  197. package/src/persistence-errors.ts +4 -7
  198. package/src/plugin-packages.ts +1186 -0
  199. package/src/provision-roles.ts +534 -1
  200. package/src/runtime-posture-cli.ts +9 -0
  201. package/src/runtime-posture.ts +721 -5
  202. package/src/schema.ts +6270 -3159
  203. package/src/scoped-knowledge-schema.ts +1 -1
  204. package/src/scoped-knowledge.ts +655 -3
  205. package/src/session-control.ts +130 -42
  206. package/src/session-queue-commands.ts +265 -15
  207. package/src/session-realtime-context.ts +2 -2
  208. package/src/session-realtime-ledger.ts +3 -3
  209. package/src/session-realtime.ts +7 -7
  210. package/src/session-tenancy.ts +188 -0
  211. package/src/session-tool-call-settlement.ts +18 -3
  212. package/src/slack-task-policy-schema.ts +130 -0
  213. package/src/slack-task-policy.ts +341 -0
  214. package/src/slack-user-link-access.ts +702 -0
  215. package/src/task-notes-schema.ts +135 -0
  216. package/src/task-notes.ts +178 -0
  217. package/src/video-generation.ts +1530 -0
  218. package/src/workspace-artifacts.ts +1 -1
  219. package/src/workspace-learning-policy-schema.ts +139 -0
  220. package/src/workspace-learning-policy.ts +595 -0
  221. package/src/xai-subscription.ts +1887 -0
  222. package/dist/chunk-P6CUHXQZ.js +0 -7248
  223. package/dist/chunk-P6CUHXQZ.js.map +0 -1
  224. package/dist/chunk-VHBFHMPC.js +0 -1219
  225. package/dist/chunk-VHBFHMPC.js.map +0 -1
@@ -0,0 +1,849 @@
1
+ -- deployment-mode: rolling
2
+ -- Canonical organization-wide company profile authority and exact-attempt
3
+ -- delivery. It does not use Documents/RAG, Memory, Preference Registry,
4
+ -- and workspace instruction-policy storage.
5
+
6
+ SET LOCAL lock_timeout = '5s';
7
+ SET LOCAL statement_timeout = '10min';
8
+
9
+ CREATE SEQUENCE "company_profile_revision_seq" AS bigint;
10
+
11
+ CREATE TABLE "company_profile_revisions" (
12
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
13
+ "operation_id" uuid NOT NULL,
14
+ "request_fingerprint" text NOT NULL,
15
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
16
+ "revision" bigint NOT NULL DEFAULT nextval('company_profile_revision_seq'),
17
+ "intent" text NOT NULL,
18
+ "content_json" text NOT NULL,
19
+ "content_hash" text NOT NULL,
20
+ "provenance_source" text NOT NULL,
21
+ "provenance_source_id" text,
22
+ "supersedes_revision_id" uuid REFERENCES "company_profile_revisions"("id") ON DELETE RESTRICT,
23
+ "created_by_subject_id" text NOT NULL,
24
+ "created_at" timestamptz NOT NULL DEFAULT now(),
25
+ CONSTRAINT "company_profile_revisions_revision_chk" CHECK ("revision" > 0),
26
+ CONSTRAINT "company_profile_revisions_intent_chk" CHECK ("intent" IN ('active', 'proposal')),
27
+ CONSTRAINT "company_profile_revisions_receipt_chk" CHECK (
28
+ "request_fingerprint" ~ '^[0-9a-f]{64}$'
29
+ ),
30
+ CONSTRAINT "company_profile_revisions_content_chk" CHECK (
31
+ octet_length(convert_to("content_json", 'UTF8')) BETWEEN 1 AND 28672
32
+ AND "content_hash" ~ '^[0-9a-f]{64}$'
33
+ AND "content_hash" = encode(sha256(convert_to("content_json", 'UTF8')), 'hex')
34
+ ),
35
+ CONSTRAINT "company_profile_revisions_provenance_chk" CHECK (
36
+ "provenance_source" IN ('human', 'durable_learning', 'migration')
37
+ AND (
38
+ "provenance_source_id" IS NULL
39
+ OR length("provenance_source_id") BETWEEN 1 AND 512
40
+ )
41
+ ),
42
+ CONSTRAINT "company_profile_revisions_actor_chk" CHECK (
43
+ length(btrim("created_by_subject_id")) BETWEEN 1 AND 1024
44
+ ),
45
+ CONSTRAINT "company_profile_revisions_account_operation_uq"
46
+ UNIQUE ("account_id", "operation_id"),
47
+ CONSTRAINT "company_profile_revisions_account_revision_uq"
48
+ UNIQUE ("account_id", "revision")
49
+ );
50
+
51
+ CREATE INDEX "company_profile_revisions_account_history_idx"
52
+ ON "company_profile_revisions" ("account_id", "revision" DESC);
53
+
54
+ CREATE TABLE "company_profile_heads" (
55
+ "account_id" uuid PRIMARY KEY REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
56
+ "revision_id" uuid NOT NULL REFERENCES "company_profile_revisions"("id") ON DELETE RESTRICT,
57
+ "revision" bigint NOT NULL,
58
+ "content_hash" text NOT NULL,
59
+ "activation_version" bigint NOT NULL,
60
+ "activated_at" timestamptz NOT NULL DEFAULT now(),
61
+ CONSTRAINT "company_profile_heads_identity_chk" CHECK (
62
+ "revision" > 0
63
+ AND "activation_version" > 0
64
+ AND "content_hash" ~ '^[0-9a-f]{64}$'
65
+ )
66
+ );
67
+
68
+ CREATE TABLE "company_profile_activation_events" (
69
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
70
+ "operation_id" uuid NOT NULL,
71
+ "request_fingerprint" text NOT NULL,
72
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
73
+ "type" text NOT NULL,
74
+ "activation_version" bigint NOT NULL,
75
+ "old_revision_id" uuid REFERENCES "company_profile_revisions"("id") ON DELETE RESTRICT,
76
+ "old_revision" bigint,
77
+ "old_content_hash" text,
78
+ "new_revision_id" uuid REFERENCES "company_profile_revisions"("id") ON DELETE RESTRICT,
79
+ "new_revision" bigint,
80
+ "new_content_hash" text,
81
+ "actor_subject_id" text NOT NULL,
82
+ "reason" text NOT NULL,
83
+ "created_at" timestamptz NOT NULL DEFAULT now(),
84
+ CONSTRAINT "company_profile_events_type_chk" CHECK ("type" IN ('activate', 'rollback')),
85
+ CONSTRAINT "company_profile_events_receipt_chk" CHECK (
86
+ "request_fingerprint" ~ '^[0-9a-f]{64}$'
87
+ ),
88
+ CONSTRAINT "company_profile_events_old_revision_chk" CHECK (
89
+ ("old_revision_id" IS NULL AND "old_revision" IS NULL AND "old_content_hash" IS NULL)
90
+ OR (
91
+ "old_revision_id" IS NOT NULL
92
+ AND "old_revision" > 0
93
+ AND "old_content_hash" ~ '^[0-9a-f]{64}$'
94
+ )
95
+ ),
96
+ CONSTRAINT "company_profile_events_new_revision_chk" CHECK (
97
+ ("new_revision_id" IS NULL AND "new_revision" IS NULL AND "new_content_hash" IS NULL)
98
+ OR (
99
+ "new_revision_id" IS NOT NULL
100
+ AND "new_revision" > 0
101
+ AND "new_content_hash" ~ '^[0-9a-f]{64}$'
102
+ )
103
+ ),
104
+ CONSTRAINT "company_profile_events_audit_chk" CHECK (
105
+ "activation_version" > 0
106
+ AND length(btrim("actor_subject_id")) BETWEEN 1 AND 1024
107
+ AND length(btrim("reason")) BETWEEN 1 AND 4096
108
+ ),
109
+ CONSTRAINT "company_profile_events_account_operation_uq"
110
+ UNIQUE ("account_id", "operation_id"),
111
+ CONSTRAINT "company_profile_events_account_version_uq"
112
+ UNIQUE ("account_id", "activation_version")
113
+ );
114
+
115
+ CREATE INDEX "company_profile_events_account_time_idx"
116
+ ON "company_profile_activation_events" ("account_id", "created_at" DESC, "id");
117
+
118
+ CREATE TABLE "company_profile_snapshots" (
119
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
120
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
121
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
122
+ "session_id" uuid NOT NULL REFERENCES "sessions"("id") ON DELETE CASCADE,
123
+ "turn_id" uuid NOT NULL REFERENCES "session_turns"("id") ON DELETE CASCADE,
124
+ "attempt_id" uuid NOT NULL REFERENCES "session_turn_attempts"("id") ON DELETE CASCADE,
125
+ "execution_generation" integer NOT NULL,
126
+ "profile" jsonb,
127
+ "snapshot_hash" text NOT NULL,
128
+ "created_at" timestamptz NOT NULL DEFAULT now(),
129
+ CONSTRAINT "company_profile_snapshots_generation_chk" CHECK ("execution_generation" > 0),
130
+ CONSTRAINT "company_profile_snapshots_profile_chk" CHECK (
131
+ "profile" IS NULL OR jsonb_typeof("profile") = 'object'
132
+ ),
133
+ CONSTRAINT "company_profile_snapshots_hash_chk" CHECK (
134
+ "snapshot_hash" ~ '^[0-9a-f]{64}$'
135
+ AND "snapshot_hash" = encode(
136
+ sha256(convert_to(coalesce("profile", 'null'::jsonb)::text, 'UTF8')),
137
+ 'hex'
138
+ )
139
+ ),
140
+ CONSTRAINT "company_profile_snapshots_attempt_uq"
141
+ UNIQUE ("account_id", "workspace_id", "attempt_id")
142
+ );
143
+
144
+ CREATE INDEX "company_profile_snapshots_workspace_time_idx"
145
+ ON "company_profile_snapshots" ("workspace_id", "created_at" DESC, "id");
146
+
147
+ CREATE OR REPLACE FUNCTION company_profile_validate_revision()
148
+ RETURNS trigger
149
+ LANGUAGE plpgsql
150
+ AS $$
151
+ DECLARE
152
+ profile jsonb;
153
+ field_name text;
154
+ entries jsonb;
155
+ BEGIN
156
+ BEGIN
157
+ profile := NEW.content_json::jsonb;
158
+ EXCEPTION WHEN others THEN
159
+ RAISE EXCEPTION 'company profile content must be canonical JSON'
160
+ USING ERRCODE = '23514';
161
+ END;
162
+ IF jsonb_typeof(profile) <> 'object'
163
+ OR NOT profile ?& ARRAY['identity', 'mission', 'products', 'customers', 'goals', 'constraints']
164
+ OR (profile - ARRAY['identity', 'mission', 'products', 'customers', 'goals', 'constraints']) <> '{}'::jsonb
165
+ THEN
166
+ RAISE EXCEPTION 'company profile must contain exactly the canonical fields'
167
+ USING ERRCODE = '23514';
168
+ END IF;
169
+ IF (jsonb_typeof(profile->'identity') NOT IN ('string', 'null'))
170
+ OR (jsonb_typeof(profile->'mission') NOT IN ('string', 'null'))
171
+ OR (
172
+ jsonb_typeof(profile->'identity') = 'string'
173
+ AND length(btrim(profile->>'identity')) NOT BETWEEN 1 AND 2048
174
+ )
175
+ OR (
176
+ jsonb_typeof(profile->'mission') = 'string'
177
+ AND length(btrim(profile->>'mission')) NOT BETWEEN 1 AND 2048
178
+ )
179
+ THEN
180
+ RAISE EXCEPTION 'company profile identity and mission are invalid'
181
+ USING ERRCODE = '23514';
182
+ END IF;
183
+ FOREACH field_name IN ARRAY ARRAY['products', 'customers', 'goals', 'constraints'] LOOP
184
+ entries := profile->field_name;
185
+ IF jsonb_typeof(entries) <> 'array' OR jsonb_array_length(entries) > 16 THEN
186
+ RAISE EXCEPTION 'company profile list field % is invalid', field_name
187
+ USING ERRCODE = '23514';
188
+ END IF;
189
+ IF EXISTS (
190
+ SELECT 1
191
+ FROM jsonb_array_elements(entries) item
192
+ WHERE jsonb_typeof(item) <> 'object'
193
+ OR NOT item ?& ARRAY['key', 'content']
194
+ OR (item - ARRAY['key', 'content']) <> '{}'::jsonb
195
+ OR jsonb_typeof(item->'key') <> 'string'
196
+ OR jsonb_typeof(item->'content') <> 'string'
197
+ OR length(item->>'key') NOT BETWEEN 1 AND 96
198
+ OR item->>'key' <> lower(btrim(item->>'key'))
199
+ OR item->>'key' !~ '^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$'
200
+ OR item->>'key' ~ '--'
201
+ OR length(btrim(item->>'content')) NOT BETWEEN 1 AND 1024
202
+ ) OR (
203
+ SELECT count(*) FROM jsonb_array_elements(entries)
204
+ ) <> (
205
+ SELECT count(DISTINCT item->>'key') FROM jsonb_array_elements(entries) item
206
+ ) THEN
207
+ RAISE EXCEPTION 'company profile list field % contains invalid entries', field_name
208
+ USING ERRCODE = '23514';
209
+ END IF;
210
+ END LOOP;
211
+ IF profile->'identity' = 'null'::jsonb
212
+ AND profile->'mission' = 'null'::jsonb
213
+ AND jsonb_array_length(profile->'products') = 0
214
+ AND jsonb_array_length(profile->'customers') = 0
215
+ AND jsonb_array_length(profile->'goals') = 0
216
+ AND jsonb_array_length(profile->'constraints') = 0
217
+ THEN
218
+ RAISE EXCEPTION 'company profile must contain at least one field'
219
+ USING ERRCODE = '23514';
220
+ END IF;
221
+ IF NEW.supersedes_revision_id IS NOT NULL AND NOT EXISTS (
222
+ SELECT 1 FROM company_profile_revisions prior
223
+ WHERE prior.id = NEW.supersedes_revision_id
224
+ AND prior.account_id = NEW.account_id
225
+ ) THEN
226
+ RAISE EXCEPTION 'superseded company-profile revision must belong to the same account'
227
+ USING ERRCODE = '23514';
228
+ END IF;
229
+ RETURN NEW;
230
+ END;
231
+ $$;
232
+
233
+ CREATE TRIGGER company_profile_revisions_validate
234
+ BEFORE INSERT OR UPDATE ON "company_profile_revisions"
235
+ FOR EACH ROW EXECUTE FUNCTION company_profile_validate_revision();
236
+
237
+ CREATE OR REPLACE FUNCTION company_profile_validate_head()
238
+ RETURNS trigger
239
+ LANGUAGE plpgsql
240
+ AS $$
241
+ BEGIN
242
+ IF NOT EXISTS (
243
+ SELECT 1 FROM company_profile_revisions revision
244
+ WHERE revision.id = NEW.revision_id
245
+ AND revision.account_id = NEW.account_id
246
+ AND revision.revision = NEW.revision
247
+ AND revision.content_hash = NEW.content_hash
248
+ ) THEN
249
+ RAISE EXCEPTION 'company-profile head must identify an exact account revision'
250
+ USING ERRCODE = '23514';
251
+ END IF;
252
+ RETURN NEW;
253
+ END;
254
+ $$;
255
+
256
+ CREATE TRIGGER company_profile_heads_validate
257
+ BEFORE INSERT OR UPDATE ON "company_profile_heads"
258
+ FOR EACH ROW EXECUTE FUNCTION company_profile_validate_head();
259
+
260
+ CREATE OR REPLACE FUNCTION company_profile_guard_head_mutation()
261
+ RETURNS trigger
262
+ LANGUAGE plpgsql
263
+ AS $$
264
+ DECLARE
265
+ lifecycle_account_id text;
266
+ BEGIN
267
+ IF TG_OP = 'DELETE'
268
+ AND pg_trigger_depth() > 1
269
+ AND NOT EXISTS (
270
+ SELECT 1 FROM managed_accounts account WHERE account.id = OLD.account_id
271
+ )
272
+ THEN
273
+ RETURN OLD;
274
+ END IF;
275
+ lifecycle_account_id := current_setting(
276
+ 'opengeni.company_profile_lifecycle_account_id',
277
+ true
278
+ );
279
+ IF lifecycle_account_id IS DISTINCT FROM coalesce(NEW.account_id, OLD.account_id)::text THEN
280
+ RAISE EXCEPTION 'company-profile heads change only through the lifecycle function'
281
+ USING ERRCODE = '55000';
282
+ END IF;
283
+ IF TG_OP = 'UPDATE' THEN
284
+ IF NEW.account_id IS DISTINCT FROM OLD.account_id
285
+ OR NEW.activation_version IS DISTINCT FROM OLD.activation_version + 1
286
+ THEN
287
+ RAISE EXCEPTION 'invalid company-profile head transition'
288
+ USING ERRCODE = '23514';
289
+ END IF;
290
+ END IF;
291
+ IF TG_OP = 'DELETE' THEN
292
+ RETURN OLD;
293
+ END IF;
294
+ RETURN NEW;
295
+ END;
296
+ $$;
297
+
298
+ CREATE TRIGGER company_profile_heads_lifecycle_only
299
+ BEFORE INSERT OR UPDATE OR DELETE ON "company_profile_heads"
300
+ FOR EACH ROW EXECUTE FUNCTION company_profile_guard_head_mutation();
301
+
302
+ CREATE OR REPLACE FUNCTION company_profile_validate_event()
303
+ RETURNS trigger
304
+ LANGUAGE plpgsql
305
+ AS $$
306
+ BEGIN
307
+ IF NEW.old_revision_id IS NOT NULL AND NOT EXISTS (
308
+ SELECT 1 FROM company_profile_revisions revision
309
+ WHERE revision.id = NEW.old_revision_id
310
+ AND revision.account_id = NEW.account_id
311
+ AND revision.revision = NEW.old_revision
312
+ AND revision.content_hash = NEW.old_content_hash
313
+ ) THEN
314
+ RAISE EXCEPTION 'company-profile event has an invalid old revision'
315
+ USING ERRCODE = '23514';
316
+ END IF;
317
+ IF NEW.new_revision_id IS NOT NULL AND NOT EXISTS (
318
+ SELECT 1 FROM company_profile_revisions revision
319
+ WHERE revision.id = NEW.new_revision_id
320
+ AND revision.account_id = NEW.account_id
321
+ AND revision.revision = NEW.new_revision
322
+ AND revision.content_hash = NEW.new_content_hash
323
+ ) THEN
324
+ RAISE EXCEPTION 'company-profile event has an invalid new revision'
325
+ USING ERRCODE = '23514';
326
+ END IF;
327
+ IF NEW.old_revision_id IS NULL AND NEW.new_revision_id IS NULL THEN
328
+ RAISE EXCEPTION 'company-profile event must change an active revision'
329
+ USING ERRCODE = '23514';
330
+ END IF;
331
+ RETURN NEW;
332
+ END;
333
+ $$;
334
+
335
+ CREATE TRIGGER company_profile_events_validate
336
+ BEFORE INSERT OR UPDATE ON "company_profile_activation_events"
337
+ FOR EACH ROW EXECUTE FUNCTION company_profile_validate_event();
338
+
339
+ CREATE OR REPLACE FUNCTION company_profile_reject_history_mutation()
340
+ RETURNS trigger
341
+ LANGUAGE plpgsql
342
+ AS $$
343
+ BEGIN
344
+ IF TG_OP = 'DELETE'
345
+ AND pg_trigger_depth() > 1
346
+ AND NOT EXISTS (SELECT 1 FROM managed_accounts account WHERE account.id = OLD.account_id)
347
+ THEN
348
+ RETURN OLD;
349
+ END IF;
350
+ RAISE EXCEPTION 'company-profile history is immutable' USING ERRCODE = '55000';
351
+ END;
352
+ $$;
353
+
354
+ CREATE TRIGGER company_profile_revisions_immutable
355
+ BEFORE UPDATE OR DELETE ON "company_profile_revisions"
356
+ FOR EACH ROW EXECUTE FUNCTION company_profile_reject_history_mutation();
357
+ CREATE TRIGGER company_profile_events_immutable
358
+ BEFORE UPDATE OR DELETE ON "company_profile_activation_events"
359
+ FOR EACH ROW EXECUTE FUNCTION company_profile_reject_history_mutation();
360
+
361
+ CREATE OR REPLACE FUNCTION company_profile_canonical_snapshot_at(
362
+ p_account_id uuid,
363
+ p_accepted_at timestamptz
364
+ ) RETURNS jsonb
365
+ LANGUAGE sql
366
+ STABLE
367
+ AS $$
368
+ SELECT CASE
369
+ WHEN event.new_revision_id IS NULL THEN NULL
370
+ ELSE jsonb_build_object(
371
+ 'id', revision.id::text,
372
+ 'revision', revision.revision,
373
+ 'contentHash', revision.content_hash,
374
+ 'activationVersion', event.activation_version,
375
+ 'activatedAt', to_char(event.created_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
376
+ 'provenance', jsonb_build_object(
377
+ 'source', revision.provenance_source,
378
+ 'sourceIdHash', CASE
379
+ WHEN revision.provenance_source_id IS NULL THEN NULL
380
+ ELSE encode(sha256(convert_to(revision.provenance_source_id, 'UTF8')), 'hex')
381
+ END
382
+ )
383
+ )
384
+ END
385
+ FROM (
386
+ SELECT activation.*
387
+ FROM company_profile_activation_events activation
388
+ WHERE activation.account_id = p_account_id
389
+ AND activation.created_at <= p_accepted_at
390
+ ORDER BY activation.created_at DESC, activation.activation_version DESC, activation.id DESC
391
+ LIMIT 1
392
+ ) event
393
+ LEFT JOIN company_profile_revisions revision
394
+ ON revision.account_id = event.account_id
395
+ AND revision.id = event.new_revision_id
396
+ AND revision.revision = event.new_revision
397
+ AND revision.content_hash = event.new_content_hash;
398
+ $$;
399
+
400
+ REVOKE ALL ON FUNCTION company_profile_canonical_snapshot_at(uuid, timestamptz) FROM PUBLIC;
401
+
402
+ CREATE OR REPLACE FUNCTION company_profile_validate_snapshot()
403
+ RETURNS trigger
404
+ LANGUAGE plpgsql
405
+ AS $$
406
+ DECLARE
407
+ accepted_at timestamptz;
408
+ canonical_profile jsonb;
409
+ BEGIN
410
+ SELECT turn.created_at INTO accepted_at
411
+ FROM session_turn_attempts attempt
412
+ JOIN session_turns turn
413
+ ON turn.account_id = attempt.account_id
414
+ AND turn.workspace_id = attempt.workspace_id
415
+ AND turn.session_id = attempt.session_id
416
+ AND turn.id = attempt.turn_id
417
+ JOIN sessions session
418
+ ON session.account_id = attempt.account_id
419
+ AND session.workspace_id = attempt.workspace_id
420
+ AND session.id = attempt.session_id
421
+ WHERE attempt.id = NEW.attempt_id
422
+ AND attempt.account_id = NEW.account_id
423
+ AND attempt.workspace_id = NEW.workspace_id
424
+ AND attempt.session_id = NEW.session_id
425
+ AND attempt.turn_id = NEW.turn_id
426
+ AND attempt.execution_generation = NEW.execution_generation
427
+ AND turn.execution_generation = NEW.execution_generation
428
+ AND attempt.state IN ('claimed', 'running')
429
+ AND turn.active_attempt_id = attempt.id
430
+ AND session.active_turn_id = turn.id
431
+ AND turn.status IN ('running', 'requires_action', 'recovering', 'waiting_capacity')
432
+ AND NOT EXISTS (
433
+ SELECT 1 FROM session_attempt_interruptions interruption
434
+ WHERE interruption.workspace_id = NEW.workspace_id
435
+ AND interruption.attempt_id = NEW.attempt_id
436
+ AND interruption.state IN ('pending', 'delivered', 'acknowledged')
437
+ );
438
+ IF NOT FOUND THEN
439
+ RAISE EXCEPTION 'company-profile snapshot requires an exact active attempt'
440
+ USING ERRCODE = '23514';
441
+ END IF;
442
+ canonical_profile := company_profile_canonical_snapshot_at(NEW.account_id, accepted_at);
443
+ IF NEW.profile IS DISTINCT FROM canonical_profile
444
+ OR NEW.snapshot_hash IS DISTINCT FROM encode(
445
+ sha256(convert_to(coalesce(canonical_profile, 'null'::jsonb)::text, 'UTF8')),
446
+ 'hex'
447
+ )
448
+ THEN
449
+ RAISE EXCEPTION 'company-profile snapshot is not canonical for the accepted turn'
450
+ USING ERRCODE = '23514';
451
+ END IF;
452
+ RETURN NEW;
453
+ END;
454
+ $$;
455
+
456
+ CREATE TRIGGER company_profile_snapshots_validate
457
+ BEFORE INSERT ON "company_profile_snapshots"
458
+ FOR EACH ROW EXECUTE FUNCTION company_profile_validate_snapshot();
459
+
460
+ CREATE OR REPLACE FUNCTION company_profile_reject_snapshot_mutation()
461
+ RETURNS trigger
462
+ LANGUAGE plpgsql
463
+ AS $$
464
+ BEGIN
465
+ IF TG_OP = 'DELETE'
466
+ AND pg_trigger_depth() > 1
467
+ AND (
468
+ NOT EXISTS (SELECT 1 FROM managed_accounts WHERE id = OLD.account_id)
469
+ OR NOT EXISTS (SELECT 1 FROM workspaces WHERE id = OLD.workspace_id)
470
+ OR NOT EXISTS (SELECT 1 FROM sessions WHERE id = OLD.session_id)
471
+ OR NOT EXISTS (SELECT 1 FROM session_turns WHERE id = OLD.turn_id)
472
+ OR NOT EXISTS (SELECT 1 FROM session_turn_attempts WHERE id = OLD.attempt_id)
473
+ )
474
+ THEN
475
+ RETURN OLD;
476
+ END IF;
477
+ RAISE EXCEPTION 'company-profile accepted-turn snapshots are immutable'
478
+ USING ERRCODE = '55000';
479
+ END;
480
+ $$;
481
+
482
+ CREATE TRIGGER company_profile_snapshots_immutable
483
+ BEFORE UPDATE OR DELETE ON "company_profile_snapshots"
484
+ FOR EACH ROW EXECUTE FUNCTION company_profile_reject_snapshot_mutation();
485
+
486
+ ALTER TABLE "company_profile_revisions" ENABLE ROW LEVEL SECURITY;
487
+ ALTER TABLE "company_profile_revisions" FORCE ROW LEVEL SECURITY;
488
+ ALTER TABLE "company_profile_heads" ENABLE ROW LEVEL SECURITY;
489
+ ALTER TABLE "company_profile_heads" FORCE ROW LEVEL SECURITY;
490
+ ALTER TABLE "company_profile_activation_events" ENABLE ROW LEVEL SECURITY;
491
+ ALTER TABLE "company_profile_activation_events" FORCE ROW LEVEL SECURITY;
492
+ ALTER TABLE "company_profile_snapshots" ENABLE ROW LEVEL SECURITY;
493
+ ALTER TABLE "company_profile_snapshots" FORCE ROW LEVEL SECURITY;
494
+
495
+ CREATE POLICY account_isolation ON "company_profile_revisions"
496
+ USING (opengeni_private.account_rls_visible("account_id"))
497
+ WITH CHECK (opengeni_private.account_rls_visible("account_id"));
498
+ CREATE POLICY account_isolation ON "company_profile_heads"
499
+ USING (opengeni_private.account_rls_visible("account_id"))
500
+ WITH CHECK (opengeni_private.account_rls_visible("account_id"));
501
+ CREATE POLICY account_isolation ON "company_profile_activation_events"
502
+ USING (opengeni_private.account_rls_visible("account_id"))
503
+ WITH CHECK (opengeni_private.account_rls_visible("account_id"));
504
+ CREATE POLICY workspace_isolation ON "company_profile_snapshots"
505
+ USING (opengeni_private.workspace_rls_visible("account_id", "workspace_id"))
506
+ WITH CHECK (opengeni_private.workspace_rls_visible("account_id", "workspace_id"));
507
+
508
+ DO $activation_function$
509
+ DECLARE target_schema text := current_schema();
510
+ BEGIN
511
+ EXECUTE format($ddl$
512
+ CREATE OR REPLACE FUNCTION %I.company_profile_apply_activation(
513
+ p_operation_id uuid,
514
+ p_request_fingerprint text,
515
+ p_account_id uuid,
516
+ p_workspace_id uuid,
517
+ p_target_revision_id uuid,
518
+ p_expected_current_revision_id uuid,
519
+ p_expected_activation_version bigint,
520
+ p_type text,
521
+ p_actor_subject_id text,
522
+ p_principal_kind text,
523
+ p_reason text
524
+ ) RETURNS TABLE (event_id uuid)
525
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = %I, pg_catalog
526
+ AS $body$
527
+ DECLARE
528
+ target_revision company_profile_revisions%%ROWTYPE;
529
+ current_head company_profile_heads%%ROWTYPE;
530
+ existing_event company_profile_activation_events%%ROWTYPE;
531
+ revision_operation company_profile_revisions%%ROWTYPE;
532
+ next_activation_version bigint;
533
+ event_created_at timestamptz;
534
+ context_account_id uuid;
535
+ context_workspace_id uuid;
536
+ context_subject_id text;
537
+ context_principal_kind text;
538
+ BEGIN
539
+ context_account_id := NULLIF(current_setting('opengeni.account_id', true), '')::uuid;
540
+ context_workspace_id := NULLIF(current_setting('opengeni.workspace_id', true), '')::uuid;
541
+ context_subject_id := NULLIF(current_setting('opengeni.subject_id', true), '');
542
+ context_principal_kind := NULLIF(current_setting('opengeni.principal_kind', true), '');
543
+ IF context_account_id IS DISTINCT FROM p_account_id
544
+ OR context_workspace_id IS DISTINCT FROM p_workspace_id
545
+ OR context_subject_id IS DISTINCT FROM p_actor_subject_id
546
+ OR context_principal_kind IS DISTINCT FROM p_principal_kind
547
+ OR p_principal_kind NOT IN ('human_session', 'agent_attempt', 'service')
548
+ THEN
549
+ RAISE EXCEPTION 'company-profile activation requires exact tenant and actor authority'
550
+ USING ERRCODE = '42501';
551
+ END IF;
552
+ IF p_request_fingerprint !~ '^[0-9a-f]{64}$'
553
+ OR p_expected_activation_version < 0
554
+ OR p_type NOT IN ('activate', 'rollback')
555
+ OR (p_target_revision_id IS NULL AND p_type <> 'rollback')
556
+ OR length(btrim(p_actor_subject_id)) NOT BETWEEN 1 AND 1024
557
+ OR length(btrim(p_reason)) NOT BETWEEN 1 AND 4096
558
+ THEN
559
+ RAISE EXCEPTION 'company-profile activation input is invalid'
560
+ USING ERRCODE = '22023';
561
+ END IF;
562
+
563
+ PERFORM 1
564
+ FROM managed_accounts account
565
+ JOIN workspaces workspace ON workspace.account_id = account.id
566
+ WHERE account.id = p_account_id AND workspace.id = p_workspace_id
567
+ FOR UPDATE OF account;
568
+ IF NOT FOUND THEN
569
+ RAISE EXCEPTION 'organization workspace was not found' USING ERRCODE = '42501';
570
+ END IF;
571
+
572
+ SELECT * INTO existing_event
573
+ FROM company_profile_activation_events event
574
+ WHERE event.account_id = p_account_id
575
+ AND event.operation_id = p_operation_id;
576
+ IF FOUND THEN
577
+ IF existing_event.request_fingerprint IS DISTINCT FROM p_request_fingerprint THEN
578
+ RAISE EXCEPTION 'company-profile operation id was reused'
579
+ USING ERRCODE = 'P1851';
580
+ END IF;
581
+ event_id := existing_event.id;
582
+ RETURN NEXT;
583
+ RETURN;
584
+ END IF;
585
+
586
+ SELECT * INTO revision_operation
587
+ FROM company_profile_revisions revision
588
+ WHERE revision.account_id = p_account_id
589
+ AND revision.operation_id = p_operation_id;
590
+ IF FOUND AND (
591
+ revision_operation.request_fingerprint IS DISTINCT FROM p_request_fingerprint
592
+ OR revision_operation.id IS DISTINCT FROM p_target_revision_id
593
+ ) THEN
594
+ RAISE EXCEPTION 'company-profile operation id was reused'
595
+ USING ERRCODE = 'P1851';
596
+ END IF;
597
+
598
+ IF p_target_revision_id IS NOT NULL THEN
599
+ SELECT * INTO target_revision
600
+ FROM company_profile_revisions revision
601
+ WHERE revision.id = p_target_revision_id
602
+ AND revision.account_id = p_account_id;
603
+ IF NOT FOUND THEN
604
+ RAISE EXCEPTION 'company-profile revision was not found' USING ERRCODE = '23503';
605
+ END IF;
606
+ END IF;
607
+
608
+ SELECT * INTO current_head
609
+ FROM company_profile_heads head
610
+ WHERE head.account_id = p_account_id
611
+ FOR UPDATE;
612
+ IF coalesce(current_head.activation_version, 0) IS DISTINCT FROM p_expected_activation_version
613
+ OR current_head.revision_id IS DISTINCT FROM p_expected_current_revision_id
614
+ THEN
615
+ RAISE EXCEPTION 'company-profile active revision changed'
616
+ USING ERRCODE = '40001';
617
+ END IF;
618
+ IF current_head.revision_id IS NOT NULL
619
+ AND current_head.revision_id IS NOT DISTINCT FROM p_target_revision_id
620
+ THEN
621
+ RAISE EXCEPTION 'company-profile revision is already active'
622
+ USING ERRCODE = '23514';
623
+ END IF;
624
+ IF current_head.revision_id IS NULL AND p_target_revision_id IS NULL THEN
625
+ RAISE EXCEPTION 'company-profile is already inactive'
626
+ USING ERRCODE = '23514';
627
+ END IF;
628
+ IF p_type = 'rollback' AND p_target_revision_id IS NOT NULL AND NOT EXISTS (
629
+ SELECT 1
630
+ FROM company_profile_activation_events event
631
+ WHERE event.account_id = p_account_id
632
+ AND event.new_revision_id = p_target_revision_id
633
+ ) THEN
634
+ RAISE EXCEPTION 'company-profile rollback target was not previously active'
635
+ USING ERRCODE = '23514';
636
+ END IF;
637
+
638
+ event_created_at := clock_timestamp();
639
+ next_activation_version := coalesce(current_head.activation_version, 0) + 1;
640
+ PERFORM set_config(
641
+ 'opengeni.company_profile_lifecycle_account_id',
642
+ p_account_id::text,
643
+ true
644
+ );
645
+ IF p_target_revision_id IS NULL THEN
646
+ DELETE FROM company_profile_heads WHERE account_id = p_account_id;
647
+ ELSIF current_head.account_id IS NULL THEN
648
+ INSERT INTO company_profile_heads (
649
+ account_id, revision_id, revision, content_hash, activation_version, activated_at
650
+ ) VALUES (
651
+ p_account_id, target_revision.id, target_revision.revision,
652
+ target_revision.content_hash, next_activation_version, event_created_at
653
+ );
654
+ ELSE
655
+ UPDATE company_profile_heads
656
+ SET revision_id = target_revision.id,
657
+ revision = target_revision.revision,
658
+ content_hash = target_revision.content_hash,
659
+ activation_version = next_activation_version,
660
+ activated_at = event_created_at
661
+ WHERE account_id = p_account_id;
662
+ END IF;
663
+ PERFORM set_config('opengeni.company_profile_lifecycle_account_id', '', true);
664
+
665
+ INSERT INTO company_profile_activation_events (
666
+ operation_id, request_fingerprint, account_id, type, activation_version,
667
+ old_revision_id, old_revision, old_content_hash,
668
+ new_revision_id, new_revision, new_content_hash,
669
+ actor_subject_id, reason, created_at
670
+ ) VALUES (
671
+ p_operation_id, p_request_fingerprint, p_account_id, p_type, next_activation_version,
672
+ current_head.revision_id, current_head.revision, current_head.content_hash,
673
+ target_revision.id, target_revision.revision, target_revision.content_hash,
674
+ p_actor_subject_id, p_reason, event_created_at
675
+ ) RETURNING id INTO event_id;
676
+ RETURN NEXT;
677
+ END
678
+ $body$
679
+ $ddl$, target_schema, target_schema);
680
+ EXECUTE format(
681
+ 'REVOKE ALL ON FUNCTION %I.company_profile_apply_activation(uuid,text,uuid,uuid,uuid,uuid,bigint,text,text,text,text) FROM PUBLIC',
682
+ target_schema
683
+ );
684
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
685
+ EXECUTE format(
686
+ 'GRANT EXECUTE ON FUNCTION %I.company_profile_apply_activation(uuid, text, uuid, uuid, uuid, uuid, bigint, text, text, text, text) TO opengeni_app',
687
+ target_schema
688
+ );
689
+ END IF;
690
+ END $activation_function$;
691
+
692
+ DO $snapshot_function$
693
+ DECLARE target_schema text := current_schema();
694
+ BEGIN
695
+ EXECUTE format($ddl$
696
+ CREATE OR REPLACE FUNCTION %I.company_profile_get_or_create_snapshot(
697
+ p_account_id uuid,
698
+ p_workspace_id uuid,
699
+ p_session_id uuid,
700
+ p_turn_id uuid,
701
+ p_attempt_id uuid,
702
+ p_execution_generation integer
703
+ ) RETURNS SETOF %I.company_profile_snapshots
704
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = %I, pg_catalog
705
+ AS $body$
706
+ DECLARE
707
+ context_account_id uuid;
708
+ context_workspace_id uuid;
709
+ accepted_at timestamptz;
710
+ canonical_profile jsonb;
711
+ canonical_hash text;
712
+ snapshot_time timestamptz := transaction_timestamp();
713
+ winner_id uuid;
714
+ BEGIN
715
+ context_account_id := NULLIF(current_setting('opengeni.account_id', true), '')::uuid;
716
+ context_workspace_id := NULLIF(current_setting('opengeni.workspace_id', true), '')::uuid;
717
+ IF context_account_id IS DISTINCT FROM p_account_id
718
+ OR context_workspace_id IS DISTINCT FROM p_workspace_id
719
+ OR p_execution_generation < 1
720
+ THEN
721
+ RAISE EXCEPTION 'company-profile snapshot requires exact tenant authority'
722
+ USING ERRCODE = '42501';
723
+ END IF;
724
+
725
+ SELECT turn.created_at INTO accepted_at
726
+ FROM workspaces workspace
727
+ JOIN sessions session
728
+ ON session.account_id = workspace.account_id AND session.workspace_id = workspace.id
729
+ JOIN session_turns turn
730
+ ON turn.account_id = session.account_id
731
+ AND turn.workspace_id = session.workspace_id
732
+ AND turn.session_id = session.id
733
+ JOIN session_turn_attempts attempt
734
+ ON attempt.account_id = turn.account_id
735
+ AND attempt.workspace_id = turn.workspace_id
736
+ AND attempt.session_id = turn.session_id
737
+ AND attempt.turn_id = turn.id
738
+ WHERE workspace.id = p_workspace_id
739
+ AND workspace.account_id = p_account_id
740
+ AND session.id = p_session_id
741
+ AND session.active_turn_id = p_turn_id
742
+ AND turn.id = p_turn_id
743
+ AND turn.active_attempt_id = p_attempt_id
744
+ AND turn.execution_generation = p_execution_generation
745
+ AND turn.status IN ('running', 'requires_action', 'recovering', 'waiting_capacity')
746
+ AND attempt.id = p_attempt_id
747
+ AND attempt.execution_generation = p_execution_generation
748
+ AND attempt.state IN ('claimed', 'running')
749
+ AND NOT EXISTS (
750
+ SELECT 1 FROM session_attempt_interruptions interruption
751
+ WHERE interruption.workspace_id = attempt.workspace_id
752
+ AND interruption.attempt_id = attempt.id
753
+ AND interruption.state IN ('pending', 'delivered', 'acknowledged')
754
+ )
755
+ FOR KEY SHARE OF workspace
756
+ FOR SHARE OF session, turn
757
+ FOR UPDATE OF attempt;
758
+ IF NOT FOUND THEN
759
+ RAISE EXCEPTION 'company-profile snapshot requires the exact current attempt'
760
+ USING ERRCODE = '42501';
761
+ END IF;
762
+
763
+ SELECT snapshot.id INTO winner_id
764
+ FROM company_profile_snapshots snapshot
765
+ WHERE snapshot.account_id = p_account_id
766
+ AND snapshot.workspace_id = p_workspace_id
767
+ AND snapshot.session_id = p_session_id
768
+ AND snapshot.turn_id = p_turn_id
769
+ AND snapshot.attempt_id = p_attempt_id
770
+ AND snapshot.execution_generation = p_execution_generation
771
+ FOR SHARE;
772
+ IF winner_id IS NOT NULL THEN
773
+ RETURN QUERY SELECT snapshot.* FROM company_profile_snapshots snapshot WHERE snapshot.id = winner_id;
774
+ RETURN;
775
+ END IF;
776
+
777
+ canonical_profile := company_profile_canonical_snapshot_at(p_account_id, accepted_at);
778
+ canonical_hash := encode(
779
+ sha256(convert_to(coalesce(canonical_profile, 'null'::jsonb)::text, 'UTF8')),
780
+ 'hex'
781
+ );
782
+ INSERT INTO company_profile_snapshots (
783
+ account_id, workspace_id, session_id, turn_id, attempt_id,
784
+ execution_generation, profile, snapshot_hash, created_at
785
+ ) VALUES (
786
+ p_account_id, p_workspace_id, p_session_id, p_turn_id, p_attempt_id,
787
+ p_execution_generation, canonical_profile, canonical_hash, snapshot_time
788
+ )
789
+ ON CONFLICT (account_id, workspace_id, attempt_id) DO NOTHING
790
+ RETURNING id INTO winner_id;
791
+ IF winner_id IS NULL THEN
792
+ SELECT snapshot.id INTO winner_id
793
+ FROM company_profile_snapshots snapshot
794
+ WHERE snapshot.account_id = p_account_id
795
+ AND snapshot.workspace_id = p_workspace_id
796
+ AND snapshot.session_id = p_session_id
797
+ AND snapshot.turn_id = p_turn_id
798
+ AND snapshot.attempt_id = p_attempt_id
799
+ AND snapshot.execution_generation = p_execution_generation
800
+ FOR SHARE;
801
+ END IF;
802
+ IF winner_id IS NULL THEN
803
+ RAISE EXCEPTION 'company-profile snapshot winner conflicts with exact attempt authority'
804
+ USING ERRCODE = '40001';
805
+ END IF;
806
+ IF EXISTS (
807
+ SELECT 1 FROM company_profile_snapshots snapshot
808
+ WHERE snapshot.id = winner_id
809
+ AND (
810
+ snapshot.profile IS DISTINCT FROM canonical_profile
811
+ OR snapshot.snapshot_hash IS DISTINCT FROM canonical_hash
812
+ OR snapshot.created_at IS DISTINCT FROM snapshot_time
813
+ )
814
+ ) THEN
815
+ RAISE EXCEPTION 'company-profile snapshot winner is not canonical'
816
+ USING ERRCODE = '40001';
817
+ END IF;
818
+ RETURN QUERY SELECT snapshot.* FROM company_profile_snapshots snapshot WHERE snapshot.id = winner_id;
819
+ END
820
+ $body$
821
+ $ddl$, target_schema, target_schema, target_schema);
822
+ EXECUTE format(
823
+ 'REVOKE ALL ON FUNCTION %I.company_profile_get_or_create_snapshot(uuid, uuid, uuid, uuid, uuid, integer) FROM PUBLIC',
824
+ target_schema
825
+ );
826
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
827
+ EXECUTE format(
828
+ 'GRANT EXECUTE ON FUNCTION %I.company_profile_get_or_create_snapshot(uuid, uuid, uuid, uuid, uuid, integer) TO opengeni_app',
829
+ target_schema
830
+ );
831
+ END IF;
832
+ END $snapshot_function$;
833
+
834
+ REVOKE ALL ON TABLE company_profile_revisions FROM PUBLIC;
835
+ REVOKE ALL ON TABLE company_profile_heads FROM PUBLIC;
836
+ REVOKE ALL ON TABLE company_profile_activation_events FROM PUBLIC;
837
+ REVOKE ALL ON TABLE company_profile_snapshots FROM PUBLIC;
838
+ REVOKE ALL ON SEQUENCE company_profile_revision_seq FROM PUBLIC;
839
+
840
+ DO $runtime_grants$
841
+ BEGIN
842
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
843
+ GRANT SELECT, INSERT ON TABLE company_profile_revisions TO opengeni_app;
844
+ GRANT SELECT ON TABLE company_profile_heads TO opengeni_app;
845
+ GRANT SELECT ON TABLE company_profile_activation_events TO opengeni_app;
846
+ GRANT SELECT ON TABLE company_profile_snapshots TO opengeni_app;
847
+ GRANT USAGE, SELECT ON SEQUENCE company_profile_revision_seq TO opengeni_app;
848
+ END IF;
849
+ END $runtime_grants$;