@opengeni/db 4.4.1 → 4.5.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 (82) hide show
  1. package/dist/canonical-human-identities.js +3 -3
  2. package/dist/capability-reconciliation.js +2 -2
  3. package/dist/{chunk-T7GDRZOH.js → chunk-4LVV4R4F.js} +2 -2
  4. package/dist/{chunk-NQWVBQFK.js → chunk-575FUJE2.js} +3 -3
  5. package/dist/{chunk-K2XOMTAG.js → chunk-7LFHUC35.js} +2 -2
  6. package/dist/{chunk-JC43DLHB.js → chunk-7P67EF5K.js} +5 -5
  7. package/dist/{chunk-XWANFAN4.js → chunk-BH2HBICR.js} +4 -4
  8. package/dist/{chunk-PB73JQOI.js → chunk-C3FI3DJ5.js} +2 -2
  9. package/dist/{chunk-LLQLCFCT.js → chunk-GSZZDYPV.js} +2 -2
  10. package/dist/{chunk-B4DWUU4Y.js → chunk-GYAQYECK.js} +2 -2
  11. package/dist/{chunk-GYHXV2LR.js → chunk-K2YN562I.js} +2 -2
  12. package/dist/{chunk-6UJX2OXO.js → chunk-KMBU4KDY.js} +25 -1
  13. package/dist/chunk-KMBU4KDY.js.map +1 -0
  14. package/dist/{chunk-JN2DIKGW.js → chunk-M6TNKUGE.js} +3 -1
  15. package/dist/chunk-M6TNKUGE.js.map +1 -0
  16. package/dist/{chunk-SSCUP23X.js → chunk-MSAWMGWJ.js} +6 -6
  17. package/dist/{chunk-T5RZ3HWS.js → chunk-OFQDECMZ.js} +4 -4
  18. package/dist/{chunk-7UA4WUGK.js → chunk-RRZO5XRA.js} +2671 -2335
  19. package/dist/{chunk-7UA4WUGK.js.map → chunk-RRZO5XRA.js.map} +1 -1
  20. package/dist/{chunk-72JNABPO.js → chunk-TIF52FMJ.js} +3 -3
  21. package/dist/{chunk-67VNEM5N.js → chunk-TLCTS7CZ.js} +4 -4
  22. package/dist/{chunk-VFQI3CDL.js → chunk-VXCIFQX7.js} +2 -2
  23. package/dist/{chunk-5H6FHFHD.js → chunk-XX5M3TJJ.js} +2 -2
  24. package/dist/{chunk-LUO43IZE.js → chunk-YOFYG5ZI.js} +2 -2
  25. package/dist/connect-attempts.js +3 -3
  26. package/dist/editable-artifact-durable-export.js +2 -2
  27. package/dist/editable-artifacts.js +3 -3
  28. package/dist/index.d.ts +21 -0
  29. package/dist/index.js +25 -19
  30. package/dist/managed-auth-session-sets.js +3 -3
  31. package/dist/mcp-operations.js +2 -2
  32. package/dist/migrate.js +19 -19
  33. package/dist/organization-integration-policy.js +3 -3
  34. package/dist/provision-roles.js +1 -1
  35. package/dist/retained-provider-commands.js +6 -6
  36. package/dist/runtime-posture.d.ts +2 -2
  37. package/dist/schema.d.ts +93 -0
  38. package/dist/schema.js +3 -1
  39. package/dist/session-background-commands.js +5 -5
  40. package/dist/session-command-output.js +5 -5
  41. package/dist/session-event-slices.js +3 -3
  42. package/dist/session-history-tool-identities.d.ts +11 -0
  43. package/dist/session-mcp-credential-rotation.js +6 -6
  44. package/dist/session-message-search.d.ts +21 -0
  45. package/dist/session-tenancy.js +6 -6
  46. package/dist/{subscription-core-codex-SUO2TISD.js → subscription-core-codex-L7NPFWAH.js} +6 -6
  47. package/dist/{subscription-core-codex-compat-SDHM4XG4.js → subscription-core-codex-compat-AK2UMLE3.js} +8 -8
  48. package/dist/subscription-usage-pool.d.ts +1 -1
  49. package/dist/video-generation.js +3 -3
  50. package/dist/workspace-artifacts.d.ts +13 -0
  51. package/dist/workspace-tool-defaults.js +5 -5
  52. package/drizzle/0907_subscription_connection_one_owner.sql +339 -209
  53. package/drizzle/0908_site_opens.sql +33 -0
  54. package/package.json +9 -9
  55. package/src/index.ts +304 -44
  56. package/src/runtime-posture.ts +2 -0
  57. package/src/schema.ts +24 -0
  58. package/src/session-history-tool-identities.ts +55 -0
  59. package/src/session-message-search.ts +188 -33
  60. package/src/subscription-core/administered-connections.ts +7 -5
  61. package/src/subscription-usage-pool.ts +7 -5
  62. package/src/workspace-artifacts.ts +45 -1
  63. package/dist/chunk-6UJX2OXO.js.map +0 -1
  64. package/dist/chunk-JN2DIKGW.js.map +0 -1
  65. /package/dist/{chunk-T7GDRZOH.js.map → chunk-4LVV4R4F.js.map} +0 -0
  66. /package/dist/{chunk-NQWVBQFK.js.map → chunk-575FUJE2.js.map} +0 -0
  67. /package/dist/{chunk-K2XOMTAG.js.map → chunk-7LFHUC35.js.map} +0 -0
  68. /package/dist/{chunk-JC43DLHB.js.map → chunk-7P67EF5K.js.map} +0 -0
  69. /package/dist/{chunk-XWANFAN4.js.map → chunk-BH2HBICR.js.map} +0 -0
  70. /package/dist/{chunk-PB73JQOI.js.map → chunk-C3FI3DJ5.js.map} +0 -0
  71. /package/dist/{chunk-LLQLCFCT.js.map → chunk-GSZZDYPV.js.map} +0 -0
  72. /package/dist/{chunk-B4DWUU4Y.js.map → chunk-GYAQYECK.js.map} +0 -0
  73. /package/dist/{chunk-GYHXV2LR.js.map → chunk-K2YN562I.js.map} +0 -0
  74. /package/dist/{chunk-SSCUP23X.js.map → chunk-MSAWMGWJ.js.map} +0 -0
  75. /package/dist/{chunk-T5RZ3HWS.js.map → chunk-OFQDECMZ.js.map} +0 -0
  76. /package/dist/{chunk-72JNABPO.js.map → chunk-TIF52FMJ.js.map} +0 -0
  77. /package/dist/{chunk-67VNEM5N.js.map → chunk-TLCTS7CZ.js.map} +0 -0
  78. /package/dist/{chunk-VFQI3CDL.js.map → chunk-VXCIFQX7.js.map} +0 -0
  79. /package/dist/{chunk-5H6FHFHD.js.map → chunk-XX5M3TJJ.js.map} +0 -0
  80. /package/dist/{chunk-LUO43IZE.js.map → chunk-YOFYG5ZI.js.map} +0 -0
  81. /package/dist/{subscription-core-codex-SUO2TISD.js.map → subscription-core-codex-L7NPFWAH.js.map} +0 -0
  82. /package/dist/{subscription-core-codex-compat-SDHM4XG4.js.map → subscription-core-codex-compat-AK2UMLE3.js.map} +0 -0
@@ -1,7 +1,6 @@
1
- -- deployment-mode: rolling
2
- -- online-safety-exception: explicit-lock: the lock holds only the subscription connection, reach, policy and settings tables, which have a few rows per organization or workspace, so the owner rewrite cannot race a concurrent connect and finishes instantly
3
- -- online-safety-exception: unbatched-data: every rewritten table holds a handful of subscription connection, reach or policy rows per organization, so one pass finishes instantly
4
- -- online-safety-exception: constraint-validation: the validated table holds a handful of subscription connection rows per organization, so validating under the same lock finishes instantly
1
+ -- deployment-mode: maintenance
2
+ -- maintenance-justification: The fold holds ACCESS EXCLUSIVE on the four subscription tables until commit, measured at 6.1s for 30k connections with 100k workspaces on PostgreSQL 18, and serving clients would wait that long behind it; production counts are not yet known, so the release parks clients for it. An expand/contract split would need a binary that reads both the pool-row model and the one-owner model, which this release does not ship; shrinking this window is a follow-up.
3
+ -- online-safety-exception: maintenance-unbatched-data: every UPDATE, DELETE and INSERT here is one set-based statement over the subscription connection, reach, policy, auto-assignment and settings rows, never a per-connection loop; the whole migration measured 0.07s at 300 connections, 0.44s at 3k, 1.2s at 9k and 5.2s at 30k connections with 100k workspaces on PostgreSQL 18, growing linearly
5
4
  -- contract-step: nothing in this release reads or writes subscription_connection_assignment_policies; a later contract migration drops it. A previous binary that straddles the commit reads no policy rows, so it classifies every shared connection by its manager and the stripped settings (automatic source), which is exactly the reach written here.
6
5
  --
7
6
  -- Migration 0907: every subscription connection has one owner and serves
@@ -40,13 +39,31 @@
40
39
  -- owner window (docs/force-rls-migration-backfills.md); workspaces and
41
40
  -- memberships are read through the organization-lifecycle marker branch.
42
41
  -- No advisory fence is taken.
42
+ --
43
+ -- Locks: this is a maintenance migration, so the production release parks
44
+ -- database clients before it runs. Each LOCK statement still waits at most 5s
45
+ -- in total, by statement_timeout (not 5s per table), for anything left
46
+ -- running. First SHARE ROW EXCLUSIVE on the five tables: it waits only for
47
+ -- writers and never blocks readers. Then ACCESS EXCLUSIVE on the four whose
48
+ -- row security the owner window relaxes (ALTER ... NO FORCE needs it anyway):
49
+ -- with writers already excluded it waits only for reads in progress, and
50
+ -- readers that arrive meanwhile queue behind it for at most those 5s. Once
51
+ -- granted, the locks are held until commit, for as long as the set-based fold
52
+ -- below takes (6.1s measured at 30k connections and 100k workspaces), and any
53
+ -- reader or writer still connected waits for that too. SHARE ROW EXCLUSIVE
54
+ -- then ACCESS EXCLUSIVE can exclude writers for two acquisitions (up to 5s
55
+ -- each) plus that hold. The workspace-owner CHECK added below is validated by
56
+ -- one scan under the same lock, under 40ms at 30k connections.
43
57
  SET LOCAL lock_timeout = '5s';
44
- SET LOCAL statement_timeout = '5min';
45
-
58
+ SET LOCAL statement_timeout = '5s';
46
59
  LOCK TABLE subscription_connections, subscription_connection_workspaces,
47
60
  subscription_connection_assignment_policies, subscription_settings,
48
61
  opengeni_private.subscription_core_auto_assignments
49
62
  IN SHARE ROW EXCLUSIVE MODE;
63
+ LOCK TABLE subscription_connections, subscription_connection_workspaces,
64
+ subscription_connection_assignment_policies, subscription_settings
65
+ IN ACCESS EXCLUSIVE MODE;
66
+ SET LOCAL statement_timeout = '5min';
50
67
 
51
68
  ALTER TABLE subscription_connections NO FORCE ROW LEVEL SECURITY;
52
69
  ALTER TABLE subscription_connection_workspaces NO FORCE ROW LEVEL SECURITY;
@@ -59,226 +76,301 @@ ALTER TABLE subscription_connections DISABLE TRIGGER subscription_connections_sc
59
76
  SELECT pg_catalog.set_config('opengeni.organization_tenancy_lifecycle',
60
77
  'organization_membership_lifecycle', true);
61
78
 
79
+ -- Every step below is one set-based statement over the whole chain, never a
80
+ -- per-connection or per-workspace loop, so its cost grows with the rows it
81
+ -- reads rather than with connections times workspaces.
82
+ --
83
+ -- The shared connections this folds, as they stood when the lock was taken.
84
+ CREATE TEMP TABLE subscription_one_owner_connection ON COMMIT DROP AS
85
+ SELECT connection.id, connection.account_id, connection.provider, connection.scope_kind,
86
+ connection.allow_personal_workspaces, connection.managed_by_workspace_id
87
+ FROM subscription_connections connection
88
+ WHERE connection.ownership = 'shared' AND connection.disconnected_at IS NULL
89
+ AND connection.scope_kind IN ('organization', 'workspaces');
90
+ CREATE UNIQUE INDEX ON subscription_one_owner_connection (id);
91
+ CREATE INDEX ON subscription_one_owner_connection (account_id);
92
+ ANALYZE subscription_one_owner_connection;
93
+
94
+ -- Every workspace of those organizations, and whether it is someone's Personal one.
95
+ CREATE TEMP TABLE subscription_one_owner_workspace ON COMMIT DROP AS
96
+ SELECT workspace.account_id, workspace.id, personal.workspace_id IS NOT NULL AS personal
97
+ FROM workspaces workspace
98
+ LEFT JOIN (SELECT DISTINCT membership.account_id, membership.personal_workspace_id AS workspace_id
99
+ FROM organization_memberships membership
100
+ WHERE membership.personal_workspace_id IS NOT NULL) personal
101
+ ON personal.account_id = workspace.account_id AND personal.workspace_id = workspace.id
102
+ WHERE workspace.account_id IN (SELECT account_id FROM subscription_one_owner_connection);
103
+ CREATE UNIQUE INDEX ON subscription_one_owner_workspace (account_id, id);
104
+ ANALYZE subscription_one_owner_workspace;
105
+
106
+ -- The source each workspace used per provider: exactly the inferenceSource
107
+ -- subscription_effective_settings() reported (or the useOrganizationAccounts
108
+ -- it derived), computed in one join instead of one call per workspace. With
109
+ -- no organization settings row that function returns nothing: automatic.
110
+ CREATE TEMP TABLE subscription_one_owner_source ON COMMIT DROP AS
111
+ SELECT workspace.account_id, workspace.id AS workspace_id, provider.provider,
112
+ CASE WHEN org.account_id IS NULL THEN 'automatic' ELSE coalesce(
113
+ CASE
114
+ WHEN entry.workspace_setting ? 'inferenceSource' THEN entry.workspace_setting->>'inferenceSource'
115
+ WHEN entry.workspace_setting ? 'useOrganizationAccounts'
116
+ THEN CASE WHEN entry.workspace_setting->>'useOrganizationAccounts' = 'false'
117
+ THEN 'workspace' ELSE 'automatic' END
118
+ WHEN entry.organization_setting ? 'inferenceSource'
119
+ THEN entry.organization_setting->>'inferenceSource'
120
+ WHEN entry.organization_setting ? 'useOrganizationAccounts'
121
+ THEN CASE WHEN entry.organization_setting->>'useOrganizationAccounts' = 'false'
122
+ THEN 'workspace' ELSE 'automatic' END
123
+ ELSE 'automatic'
124
+ END, 'automatic') END AS inference_source
125
+ FROM subscription_one_owner_workspace workspace
126
+ JOIN (SELECT DISTINCT account_id, provider FROM subscription_one_owner_connection) provider
127
+ ON provider.account_id = workspace.account_id
128
+ LEFT JOIN subscription_settings org
129
+ ON org.account_id = workspace.account_id AND org.workspace_id IS NULL
130
+ LEFT JOIN subscription_settings ws
131
+ ON ws.account_id = workspace.account_id AND ws.workspace_id = workspace.id
132
+ CROSS JOIN LATERAL (SELECT
133
+ coalesce(org.providers->provider.provider, '{}'::jsonb) AS organization_setting,
134
+ CASE WHEN ws.providers ? provider.provider
135
+ AND NOT ('providers' = ANY(coalesce(org.locked_settings, '{}')))
136
+ THEN ws.providers->provider.provider ELSE '{}'::jsonb END AS workspace_setting) entry;
137
+ CREATE UNIQUE INDEX ON subscription_one_owner_source (account_id, workspace_id, provider);
138
+ ANALYZE subscription_one_owner_source;
139
+
140
+ -- The pool rows per connection and workspace.
141
+ CREATE TEMP TABLE subscription_one_owner_policy ON COMMIT DROP AS
142
+ SELECT policy.connection_id, policy.workspace_id,
143
+ bool_or(policy.inference_pool = 'workspace') AS workspace_pool,
144
+ coalesce(array_agg(DISTINCT policy.inference_pool) FILTER (WHERE policy.allocator_enabled),
145
+ '{}') AS enabled_pools
146
+ FROM subscription_connection_assignment_policies policy
147
+ GROUP BY policy.connection_id, policy.workspace_id;
148
+ CREATE UNIQUE INDEX ON subscription_one_owner_policy (connection_id, workspace_id);
149
+ ANALYZE subscription_one_owner_policy;
150
+
151
+ -- 1. Where each connection is in scope, and whether it is used there.
62
152
  CREATE TEMP TABLE subscription_one_owner_use ON COMMIT DROP AS
63
- WITH connection AS (
64
- SELECT connection.id, connection.account_id, connection.provider, connection.scope_kind,
65
- connection.allow_personal_workspaces, connection.managed_by_workspace_id
66
- FROM subscription_connections connection
67
- WHERE connection.ownership = 'shared' AND connection.disconnected_at IS NULL
68
- AND connection.scope_kind IN ('organization', 'workspaces')
69
- ), workspace AS (
70
- SELECT workspace.account_id, workspace.id,
71
- EXISTS (SELECT 1 FROM organization_memberships membership
72
- WHERE membership.account_id = workspace.account_id
73
- AND membership.personal_workspace_id = workspace.id) AS personal
74
- FROM workspaces workspace
75
- WHERE workspace.account_id IN (SELECT account_id FROM connection)
76
- ), source AS (
77
- SELECT workspace.account_id, workspace.id AS workspace_id, provider.provider,
78
- coalesce(effective->'values'->'providers'->provider.provider->>'inferenceSource',
79
- CASE WHEN effective->'values'->'providers'->provider.provider->>'useOrganizationAccounts' = 'false'
80
- THEN 'workspace' ELSE 'automatic' END) AS inference_source
81
- FROM workspace
82
- CROSS JOIN LATERAL (SELECT DISTINCT connection.provider FROM connection
83
- WHERE connection.account_id = workspace.account_id) provider
84
- CROSS JOIN LATERAL (SELECT subscription_effective_settings(workspace.account_id, workspace.id)
85
- AS effective) settings
86
- ), in_scope AS (
87
- SELECT connection.id AS connection_id, workspace.id AS workspace_id, workspace.personal,
88
- EXISTS (SELECT 1 FROM subscription_connection_workspaces assignment
89
- WHERE assignment.account_id = connection.account_id
90
- AND assignment.connection_id = connection.id
91
- AND assignment.workspace_id = workspace.id) AS assigned
92
- FROM connection JOIN workspace ON workspace.account_id = connection.account_id
153
+ WITH in_scope AS (
154
+ SELECT connection.id AS connection_id, workspace.id AS workspace_id, workspace.personal
155
+ FROM subscription_one_owner_connection connection
156
+ JOIN subscription_one_owner_workspace workspace ON workspace.account_id = connection.account_id
93
157
  WHERE connection.scope_kind = 'organization'
94
- OR EXISTS (SELECT 1 FROM subscription_connection_workspaces assignment
95
- WHERE assignment.account_id = connection.account_id
96
- AND assignment.connection_id = connection.id
97
- AND assignment.workspace_id = workspace.id)
98
- OR (workspace.personal AND connection.allow_personal_workspaces)
158
+ UNION
159
+ SELECT connection.id, workspace.id, workspace.personal
160
+ FROM subscription_one_owner_connection connection
161
+ JOIN subscription_connection_workspaces assignment
162
+ ON assignment.account_id = connection.account_id AND assignment.connection_id = connection.id
163
+ JOIN subscription_one_owner_workspace workspace
164
+ ON workspace.account_id = connection.account_id AND workspace.id = assignment.workspace_id
165
+ WHERE connection.scope_kind = 'workspaces'
166
+ UNION
167
+ SELECT connection.id, workspace.id, workspace.personal
168
+ FROM subscription_one_owner_connection connection
169
+ JOIN subscription_one_owner_workspace workspace
170
+ ON workspace.account_id = connection.account_id AND workspace.personal
171
+ WHERE connection.scope_kind = 'workspaces' AND connection.allow_personal_workspaces
99
172
  )
100
- SELECT in_scope.connection_id, in_scope.workspace_id, in_scope.personal, in_scope.assigned,
101
- source.inference_source,
173
+ SELECT in_scope.connection_id, in_scope.workspace_id, in_scope.personal,
174
+ assignment.connection_id IS NOT NULL AS assigned, source.inference_source,
102
175
  CASE
103
- WHEN EXISTS (SELECT 1 FROM subscription_connection_assignment_policies policy
104
- WHERE policy.connection_id = in_scope.connection_id
105
- AND policy.workspace_id = in_scope.workspace_id)
106
- THEN EXISTS (SELECT 1 FROM subscription_connection_assignment_policies policy
107
- WHERE policy.connection_id = in_scope.connection_id
108
- AND policy.workspace_id = in_scope.workspace_id
109
- AND policy.allocator_enabled
110
- AND (source.inference_source = 'automatic'
111
- OR policy.inference_pool = source.inference_source))
176
+ WHEN policy.connection_id IS NOT NULL
177
+ THEN coalesce((source.inference_source = 'automatic' AND cardinality(policy.enabled_pools) > 0)
178
+ OR source.inference_source = ANY(policy.enabled_pools), false)
112
179
  WHEN source.inference_source = 'workspace'
113
180
  THEN connection.managed_by_workspace_id IS NOT DISTINCT FROM in_scope.workspace_id
114
181
  WHEN source.inference_source = 'organization'
115
182
  THEN connection.managed_by_workspace_id IS DISTINCT FROM in_scope.workspace_id
116
183
  ELSE true
117
184
  END AS used,
118
- (EXISTS (SELECT 1 FROM subscription_connection_assignment_policies policy
119
- WHERE policy.connection_id = in_scope.connection_id
120
- AND policy.workspace_id = in_scope.workspace_id
121
- AND policy.inference_pool = 'workspace')
122
- OR (in_scope.assigned
185
+ coalesce(policy.workspace_pool, false)
186
+ OR (assignment.connection_id IS NOT NULL
123
187
  AND connection.managed_by_workspace_id IS NOT DISTINCT FROM in_scope.workspace_id
124
- AND NOT EXISTS (SELECT 1 FROM subscription_connection_assignment_policies policy
125
- WHERE policy.connection_id = in_scope.connection_id
126
- AND policy.workspace_id = in_scope.workspace_id))) AS local
188
+ AND policy.connection_id IS NULL) AS local
127
189
  FROM in_scope
128
- JOIN connection ON connection.id = in_scope.connection_id
129
- JOIN source ON source.account_id = connection.account_id
130
- AND source.workspace_id = in_scope.workspace_id AND source.provider = connection.provider;
190
+ JOIN subscription_one_owner_connection connection ON connection.id = in_scope.connection_id
191
+ JOIN subscription_one_owner_source source ON source.account_id = connection.account_id
192
+ AND source.workspace_id = in_scope.workspace_id AND source.provider = connection.provider
193
+ LEFT JOIN subscription_connection_workspaces assignment
194
+ ON assignment.account_id = connection.account_id AND assignment.connection_id = connection.id
195
+ AND assignment.workspace_id = in_scope.workspace_id
196
+ LEFT JOIN subscription_one_owner_policy policy
197
+ ON policy.connection_id = in_scope.connection_id AND policy.workspace_id = in_scope.workspace_id;
198
+ CREATE UNIQUE INDEX ON subscription_one_owner_use (connection_id, workspace_id);
199
+ ANALYZE subscription_one_owner_use;
131
200
 
132
- DO $one_owner$
133
- DECLARE
134
- target record;
135
- used_ids uuid[];
136
- reach_ids uuid[];
137
- assigned_ids uuid[];
138
- local_ids uuid[];
139
- personal_used boolean;
140
- models_unrestricted boolean;
141
- policy_models text[];
142
- personal_any boolean;
143
- everywhere boolean;
144
- owner_workspace uuid;
145
- BEGIN
146
- FOR target IN
147
- SELECT connection.id, connection.account_id, connection.provider, connection.scope_kind,
148
- connection.allow_personal_workspaces, connection.managed_by_workspace_id,
149
- connection.allocator_enabled
150
- FROM subscription_connections connection
151
- WHERE connection.ownership = 'shared' AND connection.disconnected_at IS NULL
152
- AND connection.scope_kind IN ('organization', 'workspaces')
153
- ORDER BY connection.id
154
- LOOP
155
- SELECT coalesce(array_agg(use.workspace_id ORDER BY use.workspace_id)
156
- FILTER (WHERE use.used), '{}'),
157
- coalesce(array_agg(use.workspace_id ORDER BY use.workspace_id)
158
- FILTER (WHERE use.assigned), '{}'),
159
- coalesce(array_agg(use.workspace_id ORDER BY use.workspace_id)
160
- FILTER (WHERE use.local), '{}'),
161
- coalesce(bool_and(use.used) FILTER (WHERE use.personal), true),
162
- coalesce(bool_or(use.personal), false),
163
- coalesce(bool_and(use.used), true)
164
- INTO used_ids, assigned_ids, local_ids, personal_used, personal_any, everywhere
165
- FROM subscription_one_owner_use use
166
- WHERE use.connection_id = target.id;
201
+ -- 2-4. Per connection: where it reaches, its owner, its model list and which
202
+ -- rewrite applies. A connection in scope nowhere keeps the defaults of an
203
+ -- empty aggregate (used nowhere, used everywhere it is in scope).
204
+ CREATE TEMP TABLE subscription_one_owner_plan ON COMMIT DROP AS
205
+ WITH aggregate AS (
206
+ SELECT use.connection_id,
207
+ array_agg(use.workspace_id ORDER BY use.workspace_id) FILTER (WHERE use.used) AS used_ids,
208
+ array_agg(use.workspace_id ORDER BY use.workspace_id) FILTER (WHERE use.assigned) AS assigned_ids,
209
+ array_agg(use.workspace_id ORDER BY use.workspace_id) FILTER (WHERE use.local) AS local_ids,
210
+ bool_and(use.used) FILTER (WHERE use.personal) AS personal_used,
211
+ bool_or(use.personal) AS personal_any,
212
+ bool_and(use.used) AS everywhere
213
+ FROM subscription_one_owner_use use
214
+ GROUP BY use.connection_id
215
+ ), models AS (
216
+ -- A model list the admitting policies narrowed in every workspace that
217
+ -- used the connection becomes the connection's own (their union, so no
218
+ -- workspace loses a model it had). Policies never stored exclusions.
219
+ SELECT use.connection_id,
220
+ bool_or(policy.connection_id IS NULL OR policy.allowed_model_ids IS NULL) AS unrestricted,
221
+ coalesce(array_agg(DISTINCT model.id ORDER BY model.id)
222
+ FILTER (WHERE model.id IS NOT NULL), '{}') AS policy_models
223
+ FROM subscription_one_owner_use use
224
+ LEFT JOIN subscription_connection_assignment_policies policy
225
+ ON policy.connection_id = use.connection_id AND policy.workspace_id = use.workspace_id
226
+ AND policy.allocator_enabled
227
+ AND (use.inference_source = 'automatic' OR policy.inference_pool = use.inference_source)
228
+ LEFT JOIN LATERAL unnest(policy.allowed_model_ids) AS model(id) ON true
229
+ WHERE use.used
230
+ GROUP BY use.connection_id
231
+ ), shaped AS (
232
+ SELECT connection.*,
233
+ coalesce(aggregate.used_ids, '{}') AS used_ids,
234
+ coalesce(aggregate.assigned_ids, '{}') AS assigned_ids,
235
+ coalesce(aggregate.local_ids, '{}') AS local_ids,
236
+ coalesce(aggregate.personal_used, true) AS personal_used,
237
+ coalesce(aggregate.personal_any, false) AS personal_any,
238
+ coalesce(aggregate.everywhere, true) AS everywhere,
239
+ coalesce(models.unrestricted, true) AS models_unrestricted,
240
+ coalesce(models.policy_models, '{}') AS policy_models
241
+ FROM subscription_one_owner_connection connection
242
+ LEFT JOIN aggregate ON aggregate.connection_id = connection.id
243
+ LEFT JOIN models ON models.connection_id = connection.id
244
+ ), reached AS (
245
+ -- 2. What it reaches: where it was used, or, used nowhere, where it is
246
+ -- listed (switched off below).
247
+ SELECT shaped.*,
248
+ CASE WHEN cardinality(shaped.used_ids) > 0 THEN shaped.used_ids
249
+ ELSE shaped.assigned_ids END AS reach_ids
250
+ FROM shaped
251
+ ), owned AS (
252
+ -- 3. Its owner.
253
+ SELECT reached.*,
254
+ CASE
255
+ WHEN reached.managed_by_workspace_id IS NOT NULL
256
+ AND reached.reach_ids <@ ARRAY[reached.managed_by_workspace_id]
257
+ THEN reached.managed_by_workspace_id
258
+ WHEN reached.managed_by_workspace_id IS NULL AND reached.scope_kind = 'workspaces'
259
+ AND cardinality(reached.local_ids) = 1 AND reached.reach_ids = reached.local_ids
260
+ AND NOT (reached.allow_personal_workspaces AND reached.personal_any)
261
+ THEN reached.local_ids[1]
262
+ END AS owner_workspace
263
+ FROM reached
264
+ )
265
+ SELECT owned.*,
266
+ cardinality(owned.used_ids) > 0 AS used_somewhere,
267
+ CASE
268
+ WHEN owned.owner_workspace IS NOT NULL THEN 'owner'
269
+ WHEN owned.scope_kind = 'organization' THEN CASE
270
+ WHEN owned.everywhere THEN 'unchanged'
271
+ WHEN cardinality(owned.used_ids) = 0 THEN 'switched_off'
272
+ ELSE 'listed' END
273
+ WHEN owned.managed_by_workspace_id IS NULL AND owned.everywhere THEN 'unchanged'
274
+ ELSE 'chosen'
275
+ END AS rewrite
276
+ FROM owned;
277
+ CREATE UNIQUE INDEX ON subscription_one_owner_plan (id);
167
278
 
168
- -- A model list the admitting policies narrowed in every workspace that
169
- -- used the connection becomes the connection's own (their union, so no
170
- -- workspace loses a model it had). Policies never stored exclusions.
171
- SELECT coalesce(bool_or(policy.connection_id IS NULL OR policy.allowed_model_ids IS NULL), true),
172
- coalesce(array_agg(DISTINCT model.id ORDER BY model.id)
173
- FILTER (WHERE model.id IS NOT NULL), '{}')
174
- INTO models_unrestricted, policy_models
175
- FROM subscription_one_owner_use use
176
- LEFT JOIN subscription_connection_assignment_policies policy
177
- ON policy.connection_id = use.connection_id AND policy.workspace_id = use.workspace_id
178
- AND policy.allocator_enabled
179
- AND (use.inference_source = 'automatic' OR policy.inference_pool = use.inference_source)
180
- LEFT JOIN LATERAL unnest(policy.allowed_model_ids) AS model(id) ON true
181
- WHERE use.connection_id = target.id AND use.used;
182
- IF NOT models_unrestricted THEN
183
- UPDATE subscription_connections SET allowed_model_ids = CASE
184
- WHEN allowed_model_ids IS NULL THEN policy_models
185
- ELSE ARRAY(SELECT unnest(allowed_model_ids) INTERSECT SELECT unnest(policy_models)
186
- ORDER BY 1) END,
187
- access_version = access_version + 1, updated_at = clock_timestamp()
188
- WHERE id = target.id;
189
- END IF;
279
+ -- 4. The narrowed model lists.
280
+ UPDATE subscription_connections connection SET allowed_model_ids = CASE
281
+ WHEN connection.allowed_model_ids IS NULL THEN plan.policy_models
282
+ ELSE ARRAY(SELECT unnest(connection.allowed_model_ids) INTERSECT SELECT unnest(plan.policy_models)
283
+ ORDER BY 1) END,
284
+ access_version = connection.access_version + 1, updated_at = clock_timestamp()
285
+ FROM subscription_one_owner_plan plan
286
+ WHERE connection.id = plan.id AND NOT plan.models_unrestricted;
190
287
 
191
- -- 2. What it reaches: where it was used, or, used nowhere, where it is
192
- -- listed (switched off below).
193
- IF cardinality(used_ids) > 0 THEN
194
- reach_ids := used_ids;
195
- ELSE
196
- reach_ids := assigned_ids;
197
- END IF;
288
+ -- One workspace's own: it reaches only that workspace.
289
+ UPDATE subscription_connections connection SET managed_by_workspace_id = plan.owner_workspace,
290
+ scope_kind = 'workspaces', allow_personal_workspaces = false,
291
+ allocator_enabled = connection.allocator_enabled AND plan.used_somewhere,
292
+ access_version = connection.access_version + 1, updated_at = clock_timestamp()
293
+ FROM subscription_one_owner_plan plan
294
+ WHERE connection.id = plan.id AND plan.rewrite = 'owner';
295
+ DELETE FROM subscription_connection_workspaces assignment
296
+ USING subscription_one_owner_plan plan
297
+ WHERE plan.rewrite = 'owner' AND assignment.connection_id = plan.id
298
+ AND assignment.workspace_id <> ALL(plan.reach_ids);
299
+ INSERT INTO subscription_connection_workspaces (account_id, connection_id, workspace_id)
300
+ SELECT plan.account_id, plan.id, plan.owner_workspace
301
+ FROM subscription_one_owner_plan plan
302
+ WHERE plan.rewrite = 'owner' AND plan.used_somewhere
303
+ ON CONFLICT DO NOTHING;
304
+ DELETE FROM opengeni_private.subscription_core_auto_assignments auto
305
+ USING subscription_one_owner_plan plan
306
+ WHERE plan.rewrite = 'owner' AND auto.connection_id = plan.id;
198
307
 
199
- -- 3. Its owner.
200
- owner_workspace := NULL;
201
- IF target.managed_by_workspace_id IS NOT NULL
202
- AND reach_ids <@ ARRAY[target.managed_by_workspace_id] THEN
203
- owner_workspace := target.managed_by_workspace_id;
204
- ELSIF target.managed_by_workspace_id IS NULL AND target.scope_kind = 'workspaces'
205
- AND cardinality(local_ids) = 1 AND reach_ids = local_ids
206
- AND NOT (target.allow_personal_workspaces AND personal_any) THEN
207
- owner_workspace := local_ids[1];
208
- END IF;
308
+ -- Organization-wide and used nowhere: keep reaching everyone, switched off.
309
+ UPDATE subscription_connections connection SET allocator_enabled = false,
310
+ access_version = connection.access_version + 1, updated_at = clock_timestamp()
311
+ FROM subscription_one_owner_plan plan
312
+ WHERE connection.id = plan.id AND plan.rewrite = 'switched_off' AND connection.allocator_enabled;
209
313
 
210
- IF owner_workspace IS NOT NULL THEN
211
- UPDATE subscription_connections SET managed_by_workspace_id = owner_workspace,
212
- scope_kind = 'workspaces', allow_personal_workspaces = false,
213
- allocator_enabled = allocator_enabled AND cardinality(used_ids) > 0,
214
- access_version = access_version + 1, updated_at = clock_timestamp()
215
- WHERE id = target.id;
216
- DELETE FROM subscription_connection_workspaces
217
- WHERE connection_id = target.id AND workspace_id <> ALL(reach_ids);
218
- IF cardinality(used_ids) > 0 THEN
219
- INSERT INTO subscription_connection_workspaces (account_id, connection_id, workspace_id)
220
- VALUES (target.account_id, target.id, owner_workspace)
221
- ON CONFLICT DO NOTHING;
222
- END IF;
223
- DELETE FROM opengeni_private.subscription_core_auto_assignments
224
- WHERE connection_id = target.id;
225
- ELSIF target.scope_kind = 'organization' THEN
226
- IF everywhere THEN
227
- CONTINUE;
228
- END IF;
229
- IF cardinality(used_ids) = 0 THEN
230
- -- Used nowhere: keep reaching everyone, switched off.
231
- UPDATE subscription_connections SET allocator_enabled = false,
232
- access_version = access_version + 1, updated_at = clock_timestamp()
233
- WHERE id = target.id AND allocator_enabled;
234
- CONTINUE;
235
- END IF;
236
- -- Every workspace that used it, still reaching shared workspaces
237
- -- created later (and Personal ones when every Personal one used it).
238
- UPDATE subscription_connections SET managed_by_workspace_id = NULL,
239
- scope_kind = 'workspaces', allow_personal_workspaces = personal_used,
240
- access_version = access_version + 1, updated_at = clock_timestamp()
241
- WHERE id = target.id;
242
- DELETE FROM subscription_connection_workspaces WHERE connection_id = target.id;
243
- INSERT INTO subscription_connection_workspaces (account_id, connection_id, workspace_id)
244
- SELECT target.account_id, target.id, use.workspace_id
245
- FROM subscription_one_owner_use use
246
- WHERE use.connection_id = target.id AND use.used AND NOT (use.personal AND personal_used);
247
- INSERT INTO opengeni_private.subscription_core_auto_assignments AS auto (
248
- account_id, provider, connection_id, shared_workspaces, personal_workspaces,
249
- allocator_enabled, allowed_model_ids
250
- )
251
- SELECT target.account_id, target.provider, target.id, true, personal_used,
252
- true, connection.allowed_model_ids
253
- FROM subscription_connections connection WHERE connection.id = target.id
254
- ON CONFLICT (connection_id) DO UPDATE SET shared_workspaces = true,
255
- personal_workspaces = excluded.personal_workspaces;
256
- ELSE
257
- -- Chosen workspaces, owned by the organization.
258
- IF target.managed_by_workspace_id IS NULL AND everywhere THEN
259
- CONTINUE;
260
- END IF;
261
- UPDATE subscription_connections SET managed_by_workspace_id = NULL,
262
- allow_personal_workspaces = allow_personal_workspaces
263
- AND (cardinality(used_ids) = 0 OR personal_used),
264
- allocator_enabled = allocator_enabled AND cardinality(used_ids) > 0,
265
- access_version = access_version + 1, updated_at = clock_timestamp()
266
- WHERE id = target.id;
267
- IF cardinality(used_ids) > 0 THEN
268
- DELETE FROM subscription_connection_workspaces
269
- WHERE connection_id = target.id AND workspace_id <> ALL(used_ids);
270
- IF target.allow_personal_workspaces AND NOT personal_used THEN
271
- INSERT INTO subscription_connection_workspaces (account_id, connection_id, workspace_id)
272
- SELECT target.account_id, target.id, use.workspace_id
273
- FROM subscription_one_owner_use use
274
- WHERE use.connection_id = target.id AND use.used AND use.personal
275
- ON CONFLICT DO NOTHING;
276
- END IF;
277
- END IF;
278
- END IF;
279
- END LOOP;
280
- END
281
- $one_owner$;
314
+ -- Organization-wide but not used everywhere: every workspace that used it,
315
+ -- still reaching shared workspaces created later (and Personal ones when
316
+ -- every Personal one used it).
317
+ UPDATE subscription_connections connection SET managed_by_workspace_id = NULL,
318
+ scope_kind = 'workspaces', allow_personal_workspaces = plan.personal_used,
319
+ access_version = connection.access_version + 1, updated_at = clock_timestamp()
320
+ FROM subscription_one_owner_plan plan
321
+ WHERE connection.id = plan.id AND plan.rewrite = 'listed';
322
+ DELETE FROM subscription_connection_workspaces assignment
323
+ USING subscription_one_owner_plan plan
324
+ WHERE plan.rewrite = 'listed' AND assignment.connection_id = plan.id;
325
+ INSERT INTO subscription_connection_workspaces (account_id, connection_id, workspace_id)
326
+ SELECT plan.account_id, plan.id, use.workspace_id
327
+ FROM subscription_one_owner_plan plan
328
+ JOIN subscription_one_owner_use use ON use.connection_id = plan.id
329
+ WHERE plan.rewrite = 'listed' AND use.used AND NOT (use.personal AND plan.personal_used);
330
+ INSERT INTO opengeni_private.subscription_core_auto_assignments AS auto (
331
+ account_id, provider, connection_id, shared_workspaces, personal_workspaces,
332
+ allocator_enabled, allowed_model_ids
333
+ )
334
+ SELECT plan.account_id, plan.provider, plan.id, true, plan.personal_used,
335
+ true, connection.allowed_model_ids
336
+ FROM subscription_one_owner_plan plan
337
+ JOIN subscription_connections connection ON connection.id = plan.id
338
+ WHERE plan.rewrite = 'listed'
339
+ ON CONFLICT (connection_id) DO UPDATE SET shared_workspaces = true,
340
+ personal_workspaces = excluded.personal_workspaces;
341
+
342
+ -- Chosen workspaces, owned by the organization.
343
+ UPDATE subscription_connections connection SET managed_by_workspace_id = NULL,
344
+ allow_personal_workspaces = connection.allow_personal_workspaces
345
+ AND (NOT plan.used_somewhere OR plan.personal_used),
346
+ allocator_enabled = connection.allocator_enabled AND plan.used_somewhere,
347
+ access_version = connection.access_version + 1, updated_at = clock_timestamp()
348
+ FROM subscription_one_owner_plan plan
349
+ WHERE connection.id = plan.id AND plan.rewrite = 'chosen';
350
+ DELETE FROM subscription_connection_workspaces assignment
351
+ USING subscription_one_owner_plan plan
352
+ WHERE plan.rewrite = 'chosen' AND plan.used_somewhere AND assignment.connection_id = plan.id
353
+ AND assignment.workspace_id <> ALL(plan.used_ids);
354
+ INSERT INTO subscription_connection_workspaces (account_id, connection_id, workspace_id)
355
+ SELECT plan.account_id, plan.id, use.workspace_id
356
+ FROM subscription_one_owner_plan plan
357
+ JOIN subscription_one_owner_use use ON use.connection_id = plan.id
358
+ WHERE plan.rewrite = 'chosen' AND plan.used_somewhere
359
+ AND plan.allow_personal_workspaces AND NOT plan.personal_used
360
+ AND use.used AND use.personal
361
+ ON CONFLICT DO NOTHING;
362
+
363
+ -- A disconnected connection serves nowhere and is folded nowhere above, but
364
+ -- it keeps its reach rows. One a workspace manages that still lists another
365
+ -- workspace becomes the organization's (as in 3), so the reach check below
366
+ -- holds instead of failing the migration.
367
+ UPDATE subscription_connections connection SET managed_by_workspace_id = NULL,
368
+ access_version = connection.access_version + 1, updated_at = clock_timestamp()
369
+ WHERE connection.disconnected_at IS NOT NULL AND connection.managed_by_workspace_id IS NOT NULL
370
+ AND EXISTS (SELECT 1 FROM subscription_connection_workspaces assignment
371
+ WHERE assignment.account_id = connection.account_id
372
+ AND assignment.connection_id = connection.id
373
+ AND assignment.workspace_id <> connection.managed_by_workspace_id);
282
374
 
283
375
  -- Only a connection serving chosen workspaces can be one workspace's own.
284
376
  UPDATE subscription_connections SET managed_by_workspace_id = NULL,
@@ -293,6 +385,35 @@ DELETE FROM subscription_connection_assignment_policies;
293
385
 
294
386
  -- 5. The per-workspace preference is gone; an entry left empty is no
295
387
  -- override at all.
388
+ --
389
+ -- The 0724 lock guard would check every stripped workspace row one at a
390
+ -- time. It can object to exactly one thing here: a provider entry that keeps
391
+ -- other values after the strip, in a workspace whose organization locks
392
+ -- `providers` (an entry left empty is removed, which only clears). That is
393
+ -- checked once for the whole table with the guard's own error, and the guard
394
+ -- is off for this one statement only. Where the guard is not installed there
395
+ -- is nothing to check, exactly as a row-by-row strip would find.
396
+ DO $locked_providers$
397
+ BEGIN
398
+ IF NOT EXISTS (SELECT 1 FROM pg_trigger
399
+ WHERE tgrelid = 'subscription_settings'::regclass
400
+ AND tgname = 'subscription_settings_locked_overrides_guard') THEN
401
+ RETURN;
402
+ END IF;
403
+ IF EXISTS (SELECT 1 FROM subscription_settings settings
404
+ JOIN subscription_settings organization ON organization.account_id = settings.account_id
405
+ AND organization.workspace_id IS NULL
406
+ WHERE settings.workspace_id IS NOT NULL
407
+ AND 'providers' = ANY(organization.locked_settings)
408
+ AND EXISTS (SELECT 1 FROM jsonb_each(settings.providers) entry
409
+ WHERE (entry.value ? 'inferenceSource' OR entry.value ? 'useOrganizationAccounts')
410
+ AND entry.value - 'inferenceSource' - 'useOrganizationAccounts' <> '{}'::jsonb)) THEN
411
+ RAISE EXCEPTION 'subscription setting % is locked by the organization', 'providers'
412
+ USING ERRCODE = '42501', DETAIL = 'subscription_setting_locked:providers';
413
+ END IF;
414
+ ALTER TABLE subscription_settings DISABLE TRIGGER subscription_settings_locked_overrides_guard;
415
+ END
416
+ $locked_providers$;
296
417
  UPDATE subscription_settings settings SET providers = coalesce((
297
418
  SELECT jsonb_object_agg(entry.key, entry.value - 'inferenceSource' - 'useOrganizationAccounts')
298
419
  FROM jsonb_each(settings.providers) entry
@@ -300,6 +421,15 @@ UPDATE subscription_settings settings SET providers = coalesce((
300
421
  ), '{}'::jsonb)
301
422
  WHERE EXISTS (SELECT 1 FROM jsonb_each(settings.providers) entry
302
423
  WHERE entry.value ? 'inferenceSource' OR entry.value ? 'useOrganizationAccounts');
424
+ DO $locked_providers_on$
425
+ BEGIN
426
+ IF EXISTS (SELECT 1 FROM pg_trigger
427
+ WHERE tgrelid = 'subscription_settings'::regclass
428
+ AND tgname = 'subscription_settings_locked_overrides_guard') THEN
429
+ ALTER TABLE subscription_settings ENABLE TRIGGER subscription_settings_locked_overrides_guard;
430
+ END IF;
431
+ END
432
+ $locked_providers_on$;
303
433
 
304
434
  -- A workspace's own connection reaches only that workspace.
305
435
  ALTER TABLE subscription_connections