pilotswarm-sdk 0.5.13 → 0.5.14
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/api/src/api-client.js +11 -7
- package/api/src/http-api-transport.js +29 -0
- package/api/src/protocol.js +121 -94
- package/dist/client.d.ts +7 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +7 -0
- package/dist/client.js.map +1 -1
- package/dist/cms-migrations.d.ts.map +1 -1
- package/dist/cms-migrations.js +1129 -0
- package/dist/cms-migrations.js.map +1 -1
- package/dist/cms.d.ts +151 -0
- package/dist/cms.d.ts.map +1 -1
- package/dist/cms.js +125 -4
- package/dist/cms.js.map +1 -1
- package/dist/management-client.d.ts +74 -2
- package/dist/management-client.d.ts.map +1 -1
- package/dist/management-client.js +70 -2
- package/dist/management-client.js.map +1 -1
- package/dist/message-sender.d.ts +38 -0
- package/dist/message-sender.d.ts.map +1 -0
- package/dist/message-sender.js +63 -0
- package/dist/message-sender.js.map +1 -0
- package/dist/orchestration/index.d.ts +2 -2
- package/dist/orchestration/index.js +1 -1
- package/dist/orchestration/lifecycle.d.ts.map +1 -1
- package/dist/orchestration/lifecycle.js +8 -0
- package/dist/orchestration/lifecycle.js.map +1 -1
- package/dist/orchestration/queue.d.ts.map +1 -1
- package/dist/orchestration/queue.js +27 -8
- package/dist/orchestration/queue.js.map +1 -1
- package/dist/orchestration/runtime.d.ts +1 -1
- package/dist/orchestration/state.d.ts +7 -0
- package/dist/orchestration/state.d.ts.map +1 -1
- package/dist/orchestration/state.js +7 -0
- package/dist/orchestration/state.js.map +1 -1
- package/dist/orchestration/turn.d.ts +2 -1
- package/dist/orchestration/turn.d.ts.map +1 -1
- package/dist/orchestration/turn.js +2 -1
- package/dist/orchestration/turn.js.map +1 -1
- package/dist/orchestration/utils.d.ts +17 -0
- package/dist/orchestration/utils.d.ts.map +1 -1
- package/dist/orchestration/utils.js +90 -0
- package/dist/orchestration/utils.js.map +1 -1
- package/dist/orchestration-registry.d.ts.map +1 -1
- package/dist/orchestration-registry.js +4 -2
- package/dist/orchestration-registry.js.map +1 -1
- package/dist/orchestration-version.d.ts +1 -1
- package/dist/orchestration-version.js +1 -1
- package/dist/orchestration.d.ts +2 -2
- package/dist/orchestration.js +1 -1
- package/dist/orchestration_1_0_46.d.ts +1 -1
- package/dist/orchestration_1_0_47.d.ts +1 -1
- package/dist/orchestration_1_0_48.d.ts +1 -1
- package/dist/orchestration_1_0_49.d.ts +1 -1
- package/dist/orchestration_1_0_50.d.ts +1 -1
- package/dist/orchestration_1_0_60/agents.d.ts +41 -0
- package/dist/orchestration_1_0_60/agents.d.ts.map +1 -0
- package/dist/orchestration_1_0_60/agents.js +773 -0
- package/dist/orchestration_1_0_60/agents.js.map +1 -0
- package/dist/orchestration_1_0_60/index.d.ts +24 -0
- package/dist/orchestration_1_0_60/index.d.ts.map +1 -0
- package/dist/orchestration_1_0_60/index.js +13 -0
- package/dist/orchestration_1_0_60/index.js.map +1 -0
- package/dist/orchestration_1_0_60/lifecycle.d.ts +48 -0
- package/dist/orchestration_1_0_60/lifecycle.d.ts.map +1 -0
- package/dist/orchestration_1_0_60/lifecycle.js +557 -0
- package/dist/orchestration_1_0_60/lifecycle.js.map +1 -0
- package/dist/orchestration_1_0_60/queue.d.ts +7 -0
- package/dist/orchestration_1_0_60/queue.d.ts.map +1 -0
- package/dist/orchestration_1_0_60/queue.js +665 -0
- package/dist/orchestration_1_0_60/queue.js.map +1 -0
- package/dist/orchestration_1_0_60/runtime.d.ts +29 -0
- package/dist/orchestration_1_0_60/runtime.d.ts.map +1 -0
- package/dist/orchestration_1_0_60/runtime.js +196 -0
- package/dist/orchestration_1_0_60/runtime.js.map +1 -0
- package/dist/orchestration_1_0_60/state.d.ts +134 -0
- package/dist/orchestration_1_0_60/state.d.ts.map +1 -0
- package/dist/orchestration_1_0_60/state.js +116 -0
- package/dist/orchestration_1_0_60/state.js.map +1 -0
- package/dist/orchestration_1_0_60/turn.d.ts +32 -0
- package/dist/orchestration_1_0_60/turn.d.ts.map +1 -0
- package/dist/orchestration_1_0_60/turn.js +1154 -0
- package/dist/orchestration_1_0_60/turn.js.map +1 -0
- package/dist/orchestration_1_0_60/utils.d.ts +22 -0
- package/dist/orchestration_1_0_60/utils.d.ts.map +1 -0
- package/dist/orchestration_1_0_60/utils.js +226 -0
- package/dist/orchestration_1_0_60/utils.js.map +1 -0
- package/dist/pg-migrator.d.ts +19 -3
- package/dist/pg-migrator.d.ts.map +1 -1
- package/dist/pg-migrator.js +71 -2
- package/dist/pg-migrator.js.map +1 -1
- package/dist/session-messages.d.ts.map +1 -1
- package/dist/session-messages.js +49 -5
- package/dist/session-messages.js.map +1 -1
- package/dist/session-proxy.d.ts +1 -0
- package/dist/session-proxy.d.ts.map +1 -1
- package/dist/session-proxy.js +17 -6
- package/dist/session-proxy.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/plugins/system/agents/default.agent.md +2 -2
package/dist/cms-migrations.js
CHANGED
|
@@ -152,8 +152,293 @@ export function CMS_MIGRATIONS(schema) {
|
|
|
152
152
|
name: "list_sessions_page_owner",
|
|
153
153
|
sql: migration_0028_list_sessions_page_owner(schema),
|
|
154
154
|
},
|
|
155
|
+
{
|
|
156
|
+
version: "0029",
|
|
157
|
+
name: "session_visibility_shares",
|
|
158
|
+
steps: migration_0029_session_visibility_shares(schema),
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
version: "0030",
|
|
162
|
+
name: "register_user_update_on_sighting",
|
|
163
|
+
sql: migration_0030_register_user_update_on_sighting(schema),
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
version: "0031",
|
|
167
|
+
name: "list_users_directory",
|
|
168
|
+
sql: migration_0031_list_users_directory(schema),
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
version: "0032",
|
|
172
|
+
name: "adopt_email_keyed_grants",
|
|
173
|
+
sql: migration_0032_adopt_email_keyed_grants(schema),
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
version: "0033",
|
|
177
|
+
name: "grant_share_create_only_grantee",
|
|
178
|
+
sql: migration_0033_grant_share_create_only_grantee(schema),
|
|
179
|
+
},
|
|
155
180
|
];
|
|
156
181
|
}
|
|
182
|
+
// ─── Migration 0033: grant path never overwrites directory identity ──
|
|
183
|
+
function migration_0033_grant_share_create_only_grantee(schema) {
|
|
184
|
+
const s = `"${schema}"`;
|
|
185
|
+
return `
|
|
186
|
+
-- 0033_grant_share_create_only_grantee: close a cross-user directory-tampering
|
|
187
|
+
-- hole. cms_grant_session_share resolved the grantee via cms_register_user,
|
|
188
|
+
-- which does UPDATE-on-sighting: a caller-supplied email/display_name
|
|
189
|
+
-- OVERWROTE an existing user's stored identity. Since a grant only needs the
|
|
190
|
+
-- caller to own the (throwaway) session being shared, ANY user could rewrite
|
|
191
|
+
-- ANY other user's display name / email as it appears in session lists, the
|
|
192
|
+
-- member directory, and share dialogs — impersonation, not an access bypass.
|
|
193
|
+
--
|
|
194
|
+
-- Fix: the grant path resolves-or-CREATES the grantee keyed ONLY on
|
|
195
|
+
-- (provider, subject), with NULL display fields — never touching an existing
|
|
196
|
+
-- row (this restores migration 0031's stated invariant that grants carry no
|
|
197
|
+
-- display_name). Names/emails enter the directory ONLY from a principal's own
|
|
198
|
+
-- sightings (login / session-create → cms_register_user). A brand-new grantee
|
|
199
|
+
-- placeholder therefore has display_name NULL, so it is correctly excluded
|
|
200
|
+
-- from cms_list_users until that person actually signs in (and email-keyed
|
|
201
|
+
-- placeholders still fold into the real principal via 0032 adoption).
|
|
202
|
+
--
|
|
203
|
+
-- Same 8-arg signature — CREATE OR REPLACE, rolling-deploy safe. The
|
|
204
|
+
-- p_email/p_display_name params are retained for signature stability but are
|
|
205
|
+
-- no longer written to the directory.
|
|
206
|
+
|
|
207
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_grant_session_share(
|
|
208
|
+
p_session_id TEXT,
|
|
209
|
+
p_provider TEXT,
|
|
210
|
+
p_subject TEXT,
|
|
211
|
+
p_email TEXT,
|
|
212
|
+
p_display_name TEXT,
|
|
213
|
+
p_access TEXT,
|
|
214
|
+
p_granted_by_provider TEXT,
|
|
215
|
+
p_granted_by_subject TEXT
|
|
216
|
+
) RETURNS VOID AS $$
|
|
217
|
+
DECLARE
|
|
218
|
+
v_root TEXT;
|
|
219
|
+
v_is_system BOOLEAN;
|
|
220
|
+
v_provider TEXT := NULLIF(BTRIM(p_provider), '');
|
|
221
|
+
v_subject TEXT := NULLIF(BTRIM(p_subject), '');
|
|
222
|
+
v_user_id BIGINT;
|
|
223
|
+
v_granted_by BIGINT;
|
|
224
|
+
BEGIN
|
|
225
|
+
IF v_provider IS NULL OR v_subject IS NULL THEN
|
|
226
|
+
RAISE EXCEPTION 'Grantee provider and subject are required';
|
|
227
|
+
END IF;
|
|
228
|
+
IF p_access NOT IN ('read', 'write') THEN
|
|
229
|
+
RAISE EXCEPTION 'Invalid share access "%" (expected read|write)', p_access;
|
|
230
|
+
END IF;
|
|
231
|
+
v_root := ${s}.cms_resolve_root_session(p_session_id);
|
|
232
|
+
IF v_root IS NULL THEN
|
|
233
|
+
RAISE EXCEPTION 'Session not found';
|
|
234
|
+
END IF;
|
|
235
|
+
SELECT sess.is_system INTO v_is_system FROM ${s}.sessions sess WHERE sess.session_id = v_root;
|
|
236
|
+
IF COALESCE(v_is_system, FALSE) THEN
|
|
237
|
+
RAISE EXCEPTION 'Cannot share a system session';
|
|
238
|
+
END IF;
|
|
239
|
+
|
|
240
|
+
-- Create-only: never overwrite a sighted row's identity. Display fields
|
|
241
|
+
-- come only from the grantee's own sightings, never from a grant.
|
|
242
|
+
INSERT INTO ${s}.users (provider, subject)
|
|
243
|
+
VALUES (v_provider, v_subject)
|
|
244
|
+
ON CONFLICT (provider, subject) DO NOTHING;
|
|
245
|
+
SELECT u.user_id INTO v_user_id
|
|
246
|
+
FROM ${s}.users u
|
|
247
|
+
WHERE u.provider = v_provider AND u.subject = v_subject;
|
|
248
|
+
|
|
249
|
+
IF p_granted_by_provider IS NOT NULL AND p_granted_by_subject IS NOT NULL THEN
|
|
250
|
+
SELECT u.user_id INTO v_granted_by FROM ${s}.users u
|
|
251
|
+
WHERE u.provider = BTRIM(p_granted_by_provider) AND u.subject = BTRIM(p_granted_by_subject);
|
|
252
|
+
END IF;
|
|
253
|
+
|
|
254
|
+
INSERT INTO ${s}.session_shares (session_id, user_id, access, granted_by)
|
|
255
|
+
VALUES (v_root, v_user_id, p_access, v_granted_by)
|
|
256
|
+
ON CONFLICT (session_id, user_id) DO UPDATE
|
|
257
|
+
SET access = EXCLUDED.access,
|
|
258
|
+
granted_by = EXCLUDED.granted_by,
|
|
259
|
+
granted_at = now();
|
|
260
|
+
END;
|
|
261
|
+
$$ LANGUAGE plpgsql;
|
|
262
|
+
`;
|
|
263
|
+
}
|
|
264
|
+
// ─── Migration 0032: adopt email-keyed grants on first sign-in ───
|
|
265
|
+
function migration_0032_adopt_email_keyed_grants(schema) {
|
|
266
|
+
const s = `"${schema}"`;
|
|
267
|
+
return `
|
|
268
|
+
-- 0032_adopt_email_keyed_grants: make "share with someone who has never
|
|
269
|
+
-- signed in" actually bind for providers with opaque subjects.
|
|
270
|
+
--
|
|
271
|
+
-- A share grant may target a user the granter identifies only by EMAIL —
|
|
272
|
+
-- someone who has never signed in. The grant path stores what it was given,
|
|
273
|
+
-- so that placeholder user row is keyed (provider, subject = typed email).
|
|
274
|
+
-- But real Entra principals are keyed by OID: on first sign-in the user
|
|
275
|
+
-- upserts a DIFFERENT row and the email-keyed grant would never match.
|
|
276
|
+
--
|
|
277
|
+
-- Fix: on every sighting that carries an email, cms_register_user adopts any
|
|
278
|
+
-- placeholder rows keyed (same provider, subject = this email, other user_id):
|
|
279
|
+
-- their shares move to the real user (keeping the stronger access where both
|
|
280
|
+
-- have one), residual references re-point, and the placeholder is deleted.
|
|
281
|
+
-- Same 4-arg signature as 0030 — CREATE OR REPLACE, rolling-deploy safe.
|
|
282
|
+
|
|
283
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_register_user(
|
|
284
|
+
p_provider TEXT,
|
|
285
|
+
p_subject TEXT,
|
|
286
|
+
p_email TEXT,
|
|
287
|
+
p_display_name TEXT
|
|
288
|
+
) RETURNS BIGINT AS $$
|
|
289
|
+
DECLARE
|
|
290
|
+
v_provider TEXT := NULLIF(BTRIM(p_provider), '');
|
|
291
|
+
v_subject TEXT := NULLIF(BTRIM(p_subject), '');
|
|
292
|
+
v_email TEXT := NULLIF(BTRIM(p_email), '');
|
|
293
|
+
v_display TEXT := NULLIF(BTRIM(p_display_name), '');
|
|
294
|
+
v_user_id BIGINT;
|
|
295
|
+
v_ghost BIGINT;
|
|
296
|
+
BEGIN
|
|
297
|
+
IF v_provider IS NULL OR v_subject IS NULL THEN
|
|
298
|
+
RAISE EXCEPTION 'User provider and subject are required';
|
|
299
|
+
END IF;
|
|
300
|
+
|
|
301
|
+
INSERT INTO ${s}.users (provider, subject, email, display_name)
|
|
302
|
+
VALUES (v_provider, v_subject, v_email, v_display)
|
|
303
|
+
ON CONFLICT (provider, subject) DO UPDATE
|
|
304
|
+
SET email = COALESCE(EXCLUDED.email, ${s}.users.email),
|
|
305
|
+
display_name = COALESCE(EXCLUDED.display_name, ${s}.users.display_name),
|
|
306
|
+
updated_at = now()
|
|
307
|
+
WHERE COALESCE(EXCLUDED.email, ${s}.users.email) IS DISTINCT FROM ${s}.users.email
|
|
308
|
+
OR COALESCE(EXCLUDED.display_name, ${s}.users.display_name) IS DISTINCT FROM ${s}.users.display_name;
|
|
309
|
+
|
|
310
|
+
SELECT user_id INTO v_user_id
|
|
311
|
+
FROM ${s}.users
|
|
312
|
+
WHERE provider = v_provider AND subject = v_subject;
|
|
313
|
+
|
|
314
|
+
-- Adopt placeholder rows keyed by this sighting's email (grants made
|
|
315
|
+
-- before the grantee ever signed in). The users table is a small
|
|
316
|
+
-- directory, so the case-insensitive subject scan is cheap.
|
|
317
|
+
IF v_email IS NOT NULL THEN
|
|
318
|
+
FOR v_ghost IN
|
|
319
|
+
SELECT u.user_id FROM ${s}.users u
|
|
320
|
+
WHERE u.provider = v_provider
|
|
321
|
+
AND LOWER(u.subject) = LOWER(v_email)
|
|
322
|
+
AND u.user_id <> v_user_id
|
|
323
|
+
LOOP
|
|
324
|
+
-- Move shares where the real user has none on that session.
|
|
325
|
+
UPDATE ${s}.session_shares ss SET user_id = v_user_id
|
|
326
|
+
WHERE ss.user_id = v_ghost
|
|
327
|
+
AND NOT EXISTS (
|
|
328
|
+
SELECT 1 FROM ${s}.session_shares e
|
|
329
|
+
WHERE e.session_id = ss.session_id AND e.user_id = v_user_id
|
|
330
|
+
);
|
|
331
|
+
-- Where both have a grant, keep the stronger access.
|
|
332
|
+
UPDATE ${s}.session_shares e SET access = 'write'
|
|
333
|
+
FROM ${s}.session_shares g
|
|
334
|
+
WHERE g.user_id = v_ghost AND g.session_id = e.session_id
|
|
335
|
+
AND e.user_id = v_user_id AND g.access = 'write' AND e.access <> 'write';
|
|
336
|
+
DELETE FROM ${s}.session_shares WHERE user_id = v_ghost;
|
|
337
|
+
-- Defensive: placeholders never sign in, so they shouldn't own or
|
|
338
|
+
-- grant anything — but re-point rather than break the FK delete.
|
|
339
|
+
UPDATE ${s}.session_shares SET granted_by = v_user_id WHERE granted_by = v_ghost;
|
|
340
|
+
UPDATE ${s}.session_owners SET user_id = v_user_id WHERE user_id = v_ghost;
|
|
341
|
+
UPDATE ${s}.session_group_owners SET user_id = v_user_id WHERE user_id = v_ghost;
|
|
342
|
+
DELETE FROM ${s}.users WHERE user_id = v_ghost;
|
|
343
|
+
END LOOP;
|
|
344
|
+
END IF;
|
|
345
|
+
|
|
346
|
+
RETURN v_user_id;
|
|
347
|
+
END;
|
|
348
|
+
$$ LANGUAGE plpgsql;
|
|
349
|
+
`;
|
|
350
|
+
}
|
|
351
|
+
// ─── Migration 0031: user directory for share autocomplete ───────
|
|
352
|
+
function migration_0031_list_users_directory(schema) {
|
|
353
|
+
const s = `"${schema}"`;
|
|
354
|
+
return `
|
|
355
|
+
-- 0031_list_users_directory: a read-only member directory so the share UI can
|
|
356
|
+
-- autocomplete a grantee by NAME (resolving to the stable provider/subject the
|
|
357
|
+
-- grant is keyed on) instead of asking for an internal id. Excludes the
|
|
358
|
+
-- synthetic system/local principals. Newest-updated first, capped.
|
|
359
|
+
|
|
360
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_list_users(
|
|
361
|
+
p_limit INT
|
|
362
|
+
) RETURNS TABLE (
|
|
363
|
+
provider TEXT,
|
|
364
|
+
subject TEXT,
|
|
365
|
+
email TEXT,
|
|
366
|
+
display_name TEXT
|
|
367
|
+
) AS $$
|
|
368
|
+
DECLARE
|
|
369
|
+
v_limit INT := GREATEST(1, LEAST(COALESCE(p_limit, 500), 2000));
|
|
370
|
+
BEGIN
|
|
371
|
+
RETURN QUERY
|
|
372
|
+
SELECT u.provider, u.subject, u.email, u.display_name
|
|
373
|
+
FROM ${s}.users u
|
|
374
|
+
WHERE NOT (u.provider = 'system' AND u.subject = 'system')
|
|
375
|
+
AND NOT (u.provider = 'local' AND u.subject = 'default')
|
|
376
|
+
-- Only sighted members (a real login/session-create sets display_name).
|
|
377
|
+
-- Excludes rows created solely by a mistyped raw-id grant.
|
|
378
|
+
AND u.display_name IS NOT NULL
|
|
379
|
+
ORDER BY u.updated_at DESC, u.user_id DESC
|
|
380
|
+
LIMIT v_limit;
|
|
381
|
+
END;
|
|
382
|
+
$$ LANGUAGE plpgsql;
|
|
383
|
+
`;
|
|
384
|
+
}
|
|
385
|
+
// ─── Migration 0030: user profile update-on-sighting ─────────────
|
|
386
|
+
function migration_0030_register_user_update_on_sighting(schema) {
|
|
387
|
+
const s = `"${schema}"`;
|
|
388
|
+
return `
|
|
389
|
+
-- 0030_register_user_update_on_sighting: flip cms_register_user from
|
|
390
|
+
-- first-seen-write-wins to update-on-sighting for the display fields.
|
|
391
|
+
--
|
|
392
|
+
-- Why: a user row is created the first time a principal is *seen*, which can
|
|
393
|
+
-- be a share grant (cms_grant_session_share) carrying only (provider, subject)
|
|
394
|
+
-- — no email/display_name, because the granter rarely knows them. Under the
|
|
395
|
+
-- old first-seen rule that null-display row was frozen, so when the grantee
|
|
396
|
+
-- later signed in and created sessions, the owner-initials UI still rendered
|
|
397
|
+
-- "?" for their own sessions. The security-model share dialog and audit views
|
|
398
|
+
-- make stale/missing display names a real cost (see the user-lifecycle section
|
|
399
|
+
-- of docs/proposals/user-admin-security-model.md).
|
|
400
|
+
--
|
|
401
|
+
-- New rule: on every sighting, COALESCE a non-empty incoming email/display_name
|
|
402
|
+
-- over the stored value. A sighting that carries the fields (a real login /
|
|
403
|
+
-- session create) backfills or refreshes them; a sighting that carries nulls
|
|
404
|
+
-- (a share grant) leaves existing values untouched. The identity key
|
|
405
|
+
-- (provider, subject) is never changed. Same signature — CREATE OR REPLACE.
|
|
406
|
+
|
|
407
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_register_user(
|
|
408
|
+
p_provider TEXT,
|
|
409
|
+
p_subject TEXT,
|
|
410
|
+
p_email TEXT,
|
|
411
|
+
p_display_name TEXT
|
|
412
|
+
) RETURNS BIGINT AS $$
|
|
413
|
+
DECLARE
|
|
414
|
+
v_provider TEXT := NULLIF(BTRIM(p_provider), '');
|
|
415
|
+
v_subject TEXT := NULLIF(BTRIM(p_subject), '');
|
|
416
|
+
v_email TEXT := NULLIF(BTRIM(p_email), '');
|
|
417
|
+
v_display TEXT := NULLIF(BTRIM(p_display_name), '');
|
|
418
|
+
v_user_id BIGINT;
|
|
419
|
+
BEGIN
|
|
420
|
+
IF v_provider IS NULL OR v_subject IS NULL THEN
|
|
421
|
+
RAISE EXCEPTION 'User provider and subject are required';
|
|
422
|
+
END IF;
|
|
423
|
+
|
|
424
|
+
INSERT INTO ${s}.users (provider, subject, email, display_name)
|
|
425
|
+
VALUES (v_provider, v_subject, v_email, v_display)
|
|
426
|
+
ON CONFLICT (provider, subject) DO UPDATE
|
|
427
|
+
SET email = COALESCE(EXCLUDED.email, ${s}.users.email),
|
|
428
|
+
display_name = COALESCE(EXCLUDED.display_name, ${s}.users.display_name),
|
|
429
|
+
updated_at = now()
|
|
430
|
+
WHERE COALESCE(EXCLUDED.email, ${s}.users.email) IS DISTINCT FROM ${s}.users.email
|
|
431
|
+
OR COALESCE(EXCLUDED.display_name, ${s}.users.display_name) IS DISTINCT FROM ${s}.users.display_name;
|
|
432
|
+
|
|
433
|
+
SELECT user_id INTO v_user_id
|
|
434
|
+
FROM ${s}.users
|
|
435
|
+
WHERE provider = v_provider AND subject = v_subject;
|
|
436
|
+
|
|
437
|
+
RETURN v_user_id;
|
|
438
|
+
END;
|
|
439
|
+
$$ LANGUAGE plpgsql;
|
|
440
|
+
`;
|
|
441
|
+
}
|
|
157
442
|
// ─── Migration 0001: Baseline ────────────────────────────────────
|
|
158
443
|
function migration_0001_baseline(schema) {
|
|
159
444
|
const s = `"${schema}"`;
|
|
@@ -5130,4 +5415,848 @@ END;
|
|
|
5130
5415
|
$$ LANGUAGE plpgsql;
|
|
5131
5416
|
`;
|
|
5132
5417
|
}
|
|
5418
|
+
// ─── Migration 0029: session visibility, shares, root stamping, authz audit ─
|
|
5419
|
+
function migration_0029_session_visibility_shares(schema) {
|
|
5420
|
+
const s = `"${schema}"`;
|
|
5421
|
+
// Non-transactional steps (see MigrationEntry.steps). The original
|
|
5422
|
+
// single-transaction shape held the ALTER's ACCESS EXCLUSIVE lock on
|
|
5423
|
+
// `sessions` through a full-table backfill and a non-CONCURRENT index
|
|
5424
|
+
// build — on a large, busy table that freezes every live reader/writer
|
|
5425
|
+
// for the whole migration. Hardened shape:
|
|
5426
|
+
// 1. The two ALTERs commit alone under a 5s lock_timeout (metadata-only:
|
|
5427
|
+
// constant default, no rewrite). A blocked ALTER fails fast and the
|
|
5428
|
+
// worker's CMS-init retry re-attempts, instead of queueing the fleet
|
|
5429
|
+
// behind the lock request.
|
|
5430
|
+
// 2. The backfill runs level-by-level in a DO block, COMMITting between
|
|
5431
|
+
// passes (PG11+), so it takes only brief row locks — never a table
|
|
5432
|
+
// lock. Cycles self-exhaust (their members never gain a root);
|
|
5433
|
+
// orphans and cycle members fall back to self-rooted, matching the
|
|
5434
|
+
// original recursive-CTE semantics.
|
|
5435
|
+
// 3. The sessions index builds with CREATE INDEX CONCURRENTLY (writes
|
|
5436
|
+
// proceed during the build), with an invalid-leftover sweep first —
|
|
5437
|
+
// a died CIC leaves an INVALID index that IF NOT EXISTS would
|
|
5438
|
+
// otherwise treat as present.
|
|
5439
|
+
// 4. The new tables + all function DDL stay one atomic step, so
|
|
5440
|
+
// DROP+CREATE proc swaps expose no missing-function window.
|
|
5441
|
+
// Every step is idempotent: a mid-way failure re-runs all steps.
|
|
5442
|
+
const step_columns = `
|
|
5443
|
+
BEGIN;
|
|
5444
|
+
SET LOCAL lock_timeout = '5s';
|
|
5445
|
+
ALTER TABLE ${s}.sessions ADD COLUMN IF NOT EXISTS visibility TEXT NOT NULL DEFAULT 'private';
|
|
5446
|
+
ALTER TABLE ${s}.sessions ADD COLUMN IF NOT EXISTS root_session_id TEXT;
|
|
5447
|
+
COMMIT;
|
|
5448
|
+
`;
|
|
5449
|
+
const step_backfill = `
|
|
5450
|
+
DO $harden$
|
|
5451
|
+
DECLARE
|
|
5452
|
+
affected BIGINT;
|
|
5453
|
+
passes INT := 0;
|
|
5454
|
+
BEGIN
|
|
5455
|
+
-- Roots: parentless rows own their tree.
|
|
5456
|
+
UPDATE ${s}.sessions
|
|
5457
|
+
SET root_session_id = session_id
|
|
5458
|
+
WHERE parent_session_id IS NULL AND root_session_id IS NULL;
|
|
5459
|
+
COMMIT;
|
|
5460
|
+
|
|
5461
|
+
-- Propagate one tree level per pass, committing between passes.
|
|
5462
|
+
LOOP
|
|
5463
|
+
UPDATE ${s}.sessions AS child
|
|
5464
|
+
SET root_session_id = parent.root_session_id
|
|
5465
|
+
FROM ${s}.sessions AS parent
|
|
5466
|
+
WHERE child.parent_session_id = parent.session_id
|
|
5467
|
+
AND child.root_session_id IS NULL
|
|
5468
|
+
AND parent.root_session_id IS NOT NULL;
|
|
5469
|
+
GET DIAGNOSTICS affected = ROW_COUNT;
|
|
5470
|
+
EXIT WHEN affected = 0;
|
|
5471
|
+
COMMIT;
|
|
5472
|
+
passes := passes + 1;
|
|
5473
|
+
EXIT WHEN passes >= 128;
|
|
5474
|
+
END LOOP;
|
|
5475
|
+
|
|
5476
|
+
-- Orphans (hard-deleted parents) and cycle members fall back to self.
|
|
5477
|
+
UPDATE ${s}.sessions
|
|
5478
|
+
SET root_session_id = session_id
|
|
5479
|
+
WHERE root_session_id IS NULL;
|
|
5480
|
+
END
|
|
5481
|
+
$harden$;
|
|
5482
|
+
`;
|
|
5483
|
+
const step_drop_invalid_index = `
|
|
5484
|
+
DO $harden$
|
|
5485
|
+
BEGIN
|
|
5486
|
+
IF EXISTS (
|
|
5487
|
+
SELECT 1
|
|
5488
|
+
FROM pg_index i
|
|
5489
|
+
JOIN pg_class c ON c.oid = i.indexrelid
|
|
5490
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
5491
|
+
WHERE n.nspname = '${schema}'
|
|
5492
|
+
AND c.relname = 'idx_${schema}_sessions_root'
|
|
5493
|
+
AND NOT i.indisvalid
|
|
5494
|
+
) THEN
|
|
5495
|
+
EXECUTE 'DROP INDEX ${s}.idx_${schema}_sessions_root';
|
|
5496
|
+
END IF;
|
|
5497
|
+
END
|
|
5498
|
+
$harden$;
|
|
5499
|
+
`;
|
|
5500
|
+
const step_index = `
|
|
5501
|
+
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_${schema}_sessions_root
|
|
5502
|
+
ON ${s}.sessions(root_session_id);
|
|
5503
|
+
`;
|
|
5504
|
+
const step_tables_and_functions = `
|
|
5505
|
+
-- 0029_session_visibility_shares: the security-model schema
|
|
5506
|
+
-- (docs/proposals/user-admin-security-model.md).
|
|
5507
|
+
--
|
|
5508
|
+
-- Adds:
|
|
5509
|
+
-- sessions.visibility 'private' | 'shared_read' | 'shared_write'.
|
|
5510
|
+
-- Meaningful on ROOT sessions only — access for a
|
|
5511
|
+
-- child always resolves through its root.
|
|
5512
|
+
-- sessions.root_session_id Denormalized tree root, stamped at create so
|
|
5513
|
+
-- list filtering is one join, not a recursive
|
|
5514
|
+
-- walk. Backfilled here for existing rows.
|
|
5515
|
+
-- session_shares Targeted per-user grants (read|write), keyed on
|
|
5516
|
+
-- the ROOT session id.
|
|
5517
|
+
-- authz_audit Denials, break-glass reads, share changes.
|
|
5518
|
+
--
|
|
5519
|
+
-- Access predicate (evaluated in cms_get_session_access / list filtering):
|
|
5520
|
+
-- canRead := admin || root.owner == viewer || root.visibility in
|
|
5521
|
+
-- (shared_read, shared_write) || share(root, viewer) is set
|
|
5522
|
+
-- canWrite := admin || root.owner == viewer || root.visibility ==
|
|
5523
|
+
-- shared_write || share(root, viewer) == write
|
|
5524
|
+
-- Role (admin or not) is the caller's concern; these procs only report facts.
|
|
5525
|
+
|
|
5526
|
+
CREATE TABLE IF NOT EXISTS ${s}.session_shares (
|
|
5527
|
+
session_id TEXT NOT NULL REFERENCES ${s}.sessions(session_id) ON DELETE CASCADE,
|
|
5528
|
+
user_id BIGINT NOT NULL REFERENCES ${s}.users(user_id),
|
|
5529
|
+
access TEXT NOT NULL,
|
|
5530
|
+
granted_by BIGINT REFERENCES ${s}.users(user_id),
|
|
5531
|
+
granted_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
5532
|
+
PRIMARY KEY (session_id, user_id)
|
|
5533
|
+
);
|
|
5534
|
+
|
|
5535
|
+
CREATE INDEX IF NOT EXISTS idx_${schema}_session_shares_user
|
|
5536
|
+
ON ${s}.session_shares(user_id);
|
|
5537
|
+
|
|
5538
|
+
CREATE TABLE IF NOT EXISTS ${s}.authz_audit (
|
|
5539
|
+
audit_id BIGSERIAL PRIMARY KEY,
|
|
5540
|
+
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
5541
|
+
actor_provider TEXT,
|
|
5542
|
+
actor_subject TEXT,
|
|
5543
|
+
actor_display TEXT,
|
|
5544
|
+
action TEXT NOT NULL,
|
|
5545
|
+
session_id TEXT,
|
|
5546
|
+
target TEXT,
|
|
5547
|
+
decision TEXT NOT NULL,
|
|
5548
|
+
reason TEXT,
|
|
5549
|
+
details JSONB NOT NULL DEFAULT '{}'
|
|
5550
|
+
);
|
|
5551
|
+
|
|
5552
|
+
CREATE INDEX IF NOT EXISTS idx_${schema}_authz_audit_session
|
|
5553
|
+
ON ${s}.authz_audit(session_id, occurred_at DESC);
|
|
5554
|
+
CREATE INDEX IF NOT EXISTS idx_${schema}_authz_audit_occurred
|
|
5555
|
+
ON ${s}.authz_audit(occurred_at DESC);
|
|
5556
|
+
|
|
5557
|
+
-- ── cms_resolve_root_session ─────────────────────────────────────
|
|
5558
|
+
-- Root id for a live session; falls back to the session itself when the
|
|
5559
|
+
-- recorded root row is gone (hard-delete edge). NULL when the session does
|
|
5560
|
+
-- not exist or is soft-deleted.
|
|
5561
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_resolve_root_session(
|
|
5562
|
+
p_session_id TEXT
|
|
5563
|
+
) RETURNS TEXT AS $$
|
|
5564
|
+
DECLARE
|
|
5565
|
+
v_root TEXT;
|
|
5566
|
+
BEGIN
|
|
5567
|
+
SELECT COALESCE(sess.root_session_id, sess.session_id) INTO v_root
|
|
5568
|
+
FROM ${s}.sessions sess
|
|
5569
|
+
WHERE sess.session_id = p_session_id AND sess.deleted_at IS NULL;
|
|
5570
|
+
IF v_root IS NULL THEN
|
|
5571
|
+
RETURN NULL;
|
|
5572
|
+
END IF;
|
|
5573
|
+
IF NOT EXISTS (SELECT 1 FROM ${s}.sessions r WHERE r.session_id = v_root) THEN
|
|
5574
|
+
RETURN p_session_id;
|
|
5575
|
+
END IF;
|
|
5576
|
+
RETURN v_root;
|
|
5577
|
+
END;
|
|
5578
|
+
$$ LANGUAGE plpgsql;
|
|
5579
|
+
|
|
5580
|
+
-- ── cms_create_session (visibility + root overload) ──────────────
|
|
5581
|
+
-- 10-arg overload: 0026's 9-arg body plus p_visibility, root stamping.
|
|
5582
|
+
-- No parameter defaults — defaults would make 9-arg calls ambiguous against
|
|
5583
|
+
-- the 0026 overload, which stays for older writers during rolling deploys.
|
|
5584
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_create_session(
|
|
5585
|
+
p_session_id TEXT,
|
|
5586
|
+
p_model TEXT,
|
|
5587
|
+
p_reasoning_effort TEXT,
|
|
5588
|
+
p_parent_session_id TEXT,
|
|
5589
|
+
p_is_system BOOLEAN,
|
|
5590
|
+
p_agent_id TEXT,
|
|
5591
|
+
p_splash TEXT,
|
|
5592
|
+
p_group_id TEXT,
|
|
5593
|
+
p_splash_mobile TEXT,
|
|
5594
|
+
p_visibility TEXT
|
|
5595
|
+
) RETURNS VOID AS $$
|
|
5596
|
+
DECLARE
|
|
5597
|
+
v_reasoning_effort TEXT := NULLIF(BTRIM(p_reasoning_effort), '');
|
|
5598
|
+
v_group_id TEXT := NULLIF(BTRIM(p_group_id), '');
|
|
5599
|
+
v_root TEXT;
|
|
5600
|
+
v_visibility TEXT := CASE
|
|
5601
|
+
WHEN p_visibility IN ('private', 'shared_read', 'shared_write') THEN p_visibility
|
|
5602
|
+
ELSE 'private'
|
|
5603
|
+
END;
|
|
5604
|
+
BEGIN
|
|
5605
|
+
IF p_parent_session_id IS NOT NULL THEN
|
|
5606
|
+
SELECT COALESCE(parent.root_session_id, parent.session_id), COALESCE(v_group_id, parent.group_id)
|
|
5607
|
+
INTO v_root, v_group_id
|
|
5608
|
+
FROM ${s}.sessions parent
|
|
5609
|
+
WHERE parent.session_id = p_parent_session_id;
|
|
5610
|
+
END IF;
|
|
5611
|
+
v_root := COALESCE(v_root, p_session_id);
|
|
5612
|
+
|
|
5613
|
+
INSERT INTO ${s}.sessions
|
|
5614
|
+
(session_id, model, reasoning_effort, parent_session_id, is_system, agent_id, splash, splash_mobile, group_id, root_session_id, visibility)
|
|
5615
|
+
VALUES
|
|
5616
|
+
(p_session_id, p_model, v_reasoning_effort, p_parent_session_id, p_is_system, p_agent_id, p_splash, p_splash_mobile, v_group_id, v_root, v_visibility)
|
|
5617
|
+
ON CONFLICT (session_id) DO UPDATE
|
|
5618
|
+
SET model = EXCLUDED.model,
|
|
5619
|
+
reasoning_effort = EXCLUDED.reasoning_effort,
|
|
5620
|
+
parent_session_id = EXCLUDED.parent_session_id,
|
|
5621
|
+
is_system = EXCLUDED.is_system,
|
|
5622
|
+
agent_id = EXCLUDED.agent_id,
|
|
5623
|
+
splash = EXCLUDED.splash,
|
|
5624
|
+
splash_mobile = EXCLUDED.splash_mobile,
|
|
5625
|
+
group_id = EXCLUDED.group_id,
|
|
5626
|
+
root_session_id = EXCLUDED.root_session_id,
|
|
5627
|
+
visibility = EXCLUDED.visibility,
|
|
5628
|
+
deleted_at = NULL,
|
|
5629
|
+
updated_at = now(),
|
|
5630
|
+
state = 'pending',
|
|
5631
|
+
orchestration_id = NULL,
|
|
5632
|
+
last_error = NULL,
|
|
5633
|
+
last_active_at = NULL,
|
|
5634
|
+
current_iteration = 0,
|
|
5635
|
+
wait_reason = NULL,
|
|
5636
|
+
title_locked = FALSE
|
|
5637
|
+
WHERE ${s}.sessions.deleted_at IS NOT NULL;
|
|
5638
|
+
|
|
5639
|
+
INSERT INTO ${s}.session_metric_summaries
|
|
5640
|
+
(session_id, agent_id, model, reasoning_effort, parent_session_id)
|
|
5641
|
+
VALUES
|
|
5642
|
+
(p_session_id, p_agent_id, p_model, v_reasoning_effort, p_parent_session_id)
|
|
5643
|
+
ON CONFLICT (session_id) DO UPDATE
|
|
5644
|
+
SET agent_id = COALESCE(${s}.session_metric_summaries.agent_id, EXCLUDED.agent_id),
|
|
5645
|
+
model = COALESCE(${s}.session_metric_summaries.model, EXCLUDED.model),
|
|
5646
|
+
reasoning_effort = COALESCE(${s}.session_metric_summaries.reasoning_effort, EXCLUDED.reasoning_effort),
|
|
5647
|
+
parent_session_id = COALESCE(${s}.session_metric_summaries.parent_session_id, EXCLUDED.parent_session_id),
|
|
5648
|
+
updated_at = now();
|
|
5649
|
+
END;
|
|
5650
|
+
$$ LANGUAGE plpgsql;
|
|
5651
|
+
|
|
5652
|
+
-- ── cms_set_session_visibility ───────────────────────────────────
|
|
5653
|
+
-- Applies to the ROOT of the given session; refuses system trees.
|
|
5654
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_set_session_visibility(
|
|
5655
|
+
p_session_id TEXT,
|
|
5656
|
+
p_visibility TEXT
|
|
5657
|
+
) RETURNS VOID AS $$
|
|
5658
|
+
DECLARE
|
|
5659
|
+
v_root TEXT;
|
|
5660
|
+
v_is_system BOOLEAN;
|
|
5661
|
+
BEGIN
|
|
5662
|
+
IF p_visibility NOT IN ('private', 'shared_read', 'shared_write') THEN
|
|
5663
|
+
RAISE EXCEPTION 'Invalid visibility "%" (expected private|shared_read|shared_write)', p_visibility;
|
|
5664
|
+
END IF;
|
|
5665
|
+
v_root := ${s}.cms_resolve_root_session(p_session_id);
|
|
5666
|
+
IF v_root IS NULL THEN
|
|
5667
|
+
RAISE EXCEPTION 'Session not found';
|
|
5668
|
+
END IF;
|
|
5669
|
+
SELECT sess.is_system INTO v_is_system FROM ${s}.sessions sess WHERE sess.session_id = v_root;
|
|
5670
|
+
IF COALESCE(v_is_system, FALSE) THEN
|
|
5671
|
+
RAISE EXCEPTION 'Cannot change visibility of a system session';
|
|
5672
|
+
END IF;
|
|
5673
|
+
UPDATE ${s}.sessions SET visibility = p_visibility, updated_at = now()
|
|
5674
|
+
WHERE session_id = v_root;
|
|
5675
|
+
END;
|
|
5676
|
+
$$ LANGUAGE plpgsql;
|
|
5677
|
+
|
|
5678
|
+
-- ── cms_grant_session_share ──────────────────────────────────────
|
|
5679
|
+
-- Grants (or updates) a targeted share on the ROOT of the given session.
|
|
5680
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_grant_session_share(
|
|
5681
|
+
p_session_id TEXT,
|
|
5682
|
+
p_provider TEXT,
|
|
5683
|
+
p_subject TEXT,
|
|
5684
|
+
p_email TEXT,
|
|
5685
|
+
p_display_name TEXT,
|
|
5686
|
+
p_access TEXT,
|
|
5687
|
+
p_granted_by_provider TEXT,
|
|
5688
|
+
p_granted_by_subject TEXT
|
|
5689
|
+
) RETURNS VOID AS $$
|
|
5690
|
+
DECLARE
|
|
5691
|
+
v_root TEXT;
|
|
5692
|
+
v_is_system BOOLEAN;
|
|
5693
|
+
v_user_id BIGINT;
|
|
5694
|
+
v_granted_by BIGINT;
|
|
5695
|
+
BEGIN
|
|
5696
|
+
IF p_access NOT IN ('read', 'write') THEN
|
|
5697
|
+
RAISE EXCEPTION 'Invalid share access "%" (expected read|write)', p_access;
|
|
5698
|
+
END IF;
|
|
5699
|
+
v_root := ${s}.cms_resolve_root_session(p_session_id);
|
|
5700
|
+
IF v_root IS NULL THEN
|
|
5701
|
+
RAISE EXCEPTION 'Session not found';
|
|
5702
|
+
END IF;
|
|
5703
|
+
SELECT sess.is_system INTO v_is_system FROM ${s}.sessions sess WHERE sess.session_id = v_root;
|
|
5704
|
+
IF COALESCE(v_is_system, FALSE) THEN
|
|
5705
|
+
RAISE EXCEPTION 'Cannot share a system session';
|
|
5706
|
+
END IF;
|
|
5707
|
+
|
|
5708
|
+
v_user_id := ${s}.cms_register_user(p_provider, p_subject, p_email, p_display_name);
|
|
5709
|
+
IF p_granted_by_provider IS NOT NULL AND p_granted_by_subject IS NOT NULL THEN
|
|
5710
|
+
SELECT u.user_id INTO v_granted_by FROM ${s}.users u
|
|
5711
|
+
WHERE u.provider = BTRIM(p_granted_by_provider) AND u.subject = BTRIM(p_granted_by_subject);
|
|
5712
|
+
END IF;
|
|
5713
|
+
|
|
5714
|
+
INSERT INTO ${s}.session_shares (session_id, user_id, access, granted_by)
|
|
5715
|
+
VALUES (v_root, v_user_id, p_access, v_granted_by)
|
|
5716
|
+
ON CONFLICT (session_id, user_id) DO UPDATE
|
|
5717
|
+
SET access = EXCLUDED.access,
|
|
5718
|
+
granted_by = EXCLUDED.granted_by,
|
|
5719
|
+
granted_at = now();
|
|
5720
|
+
END;
|
|
5721
|
+
$$ LANGUAGE plpgsql;
|
|
5722
|
+
|
|
5723
|
+
-- ── cms_revoke_session_share ─────────────────────────────────────
|
|
5724
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_revoke_session_share(
|
|
5725
|
+
p_session_id TEXT,
|
|
5726
|
+
p_provider TEXT,
|
|
5727
|
+
p_subject TEXT
|
|
5728
|
+
) RETURNS VOID AS $$
|
|
5729
|
+
DECLARE
|
|
5730
|
+
v_root TEXT;
|
|
5731
|
+
BEGIN
|
|
5732
|
+
v_root := ${s}.cms_resolve_root_session(p_session_id);
|
|
5733
|
+
IF v_root IS NULL THEN
|
|
5734
|
+
RAISE EXCEPTION 'Session not found';
|
|
5735
|
+
END IF;
|
|
5736
|
+
DELETE FROM ${s}.session_shares sh
|
|
5737
|
+
USING ${s}.users u
|
|
5738
|
+
WHERE sh.session_id = v_root
|
|
5739
|
+
AND u.user_id = sh.user_id
|
|
5740
|
+
AND u.provider = BTRIM(p_provider)
|
|
5741
|
+
AND u.subject = BTRIM(p_subject);
|
|
5742
|
+
END;
|
|
5743
|
+
$$ LANGUAGE plpgsql;
|
|
5744
|
+
|
|
5745
|
+
-- ── cms_list_session_shares ──────────────────────────────────────
|
|
5746
|
+
DROP FUNCTION IF EXISTS ${s}.cms_list_session_shares(TEXT);
|
|
5747
|
+
CREATE FUNCTION ${s}.cms_list_session_shares(
|
|
5748
|
+
p_session_id TEXT
|
|
5749
|
+
) RETURNS TABLE (
|
|
5750
|
+
provider TEXT,
|
|
5751
|
+
subject TEXT,
|
|
5752
|
+
email TEXT,
|
|
5753
|
+
display_name TEXT,
|
|
5754
|
+
access TEXT,
|
|
5755
|
+
granted_at TIMESTAMPTZ,
|
|
5756
|
+
granted_by_display TEXT
|
|
5757
|
+
) AS $$
|
|
5758
|
+
DECLARE
|
|
5759
|
+
v_root TEXT;
|
|
5760
|
+
BEGIN
|
|
5761
|
+
v_root := ${s}.cms_resolve_root_session(p_session_id);
|
|
5762
|
+
IF v_root IS NULL THEN
|
|
5763
|
+
RETURN;
|
|
5764
|
+
END IF;
|
|
5765
|
+
RETURN QUERY
|
|
5766
|
+
SELECT u.provider, u.subject, u.email, u.display_name, sh.access, sh.granted_at,
|
|
5767
|
+
gb.display_name AS granted_by_display
|
|
5768
|
+
FROM ${s}.session_shares sh
|
|
5769
|
+
JOIN ${s}.users u ON u.user_id = sh.user_id
|
|
5770
|
+
LEFT JOIN ${s}.users gb ON gb.user_id = sh.granted_by
|
|
5771
|
+
WHERE sh.session_id = v_root
|
|
5772
|
+
ORDER BY sh.granted_at ASC;
|
|
5773
|
+
END;
|
|
5774
|
+
$$ LANGUAGE plpgsql;
|
|
5775
|
+
|
|
5776
|
+
-- ── cms_get_session_access ───────────────────────────────────────
|
|
5777
|
+
-- One round-trip access snapshot for the enforcement predicate: the root's
|
|
5778
|
+
-- system flag, visibility, owner, and the viewer's targeted share. Reports
|
|
5779
|
+
-- facts only; the caller combines them with the caller's role.
|
|
5780
|
+
DROP FUNCTION IF EXISTS ${s}.cms_get_session_access(TEXT, TEXT, TEXT);
|
|
5781
|
+
CREATE FUNCTION ${s}.cms_get_session_access(
|
|
5782
|
+
p_session_id TEXT,
|
|
5783
|
+
p_viewer_provider TEXT,
|
|
5784
|
+
p_viewer_subject TEXT
|
|
5785
|
+
) RETURNS TABLE (
|
|
5786
|
+
root_session_id TEXT,
|
|
5787
|
+
is_system BOOLEAN,
|
|
5788
|
+
visibility TEXT,
|
|
5789
|
+
owner_provider TEXT,
|
|
5790
|
+
owner_subject TEXT,
|
|
5791
|
+
owner_email TEXT,
|
|
5792
|
+
owner_display_name TEXT,
|
|
5793
|
+
viewer_is_owner BOOLEAN,
|
|
5794
|
+
viewer_share_access TEXT
|
|
5795
|
+
) AS $$
|
|
5796
|
+
DECLARE
|
|
5797
|
+
v_root TEXT;
|
|
5798
|
+
BEGIN
|
|
5799
|
+
v_root := ${s}.cms_resolve_root_session(p_session_id);
|
|
5800
|
+
IF v_root IS NULL THEN
|
|
5801
|
+
RETURN; -- zero rows: session missing or soft-deleted
|
|
5802
|
+
END IF;
|
|
5803
|
+
RETURN QUERY
|
|
5804
|
+
SELECT
|
|
5805
|
+
v_root,
|
|
5806
|
+
COALESCE(r.is_system, FALSE),
|
|
5807
|
+
COALESCE(r.visibility, 'private'),
|
|
5808
|
+
u.provider,
|
|
5809
|
+
u.subject,
|
|
5810
|
+
u.email,
|
|
5811
|
+
u.display_name,
|
|
5812
|
+
(u.provider IS NOT NULL
|
|
5813
|
+
AND u.provider = BTRIM(p_viewer_provider)
|
|
5814
|
+
AND u.subject = BTRIM(p_viewer_subject)),
|
|
5815
|
+
(SELECT sh.access
|
|
5816
|
+
FROM ${s}.session_shares sh
|
|
5817
|
+
JOIN ${s}.users vu ON vu.user_id = sh.user_id
|
|
5818
|
+
WHERE sh.session_id = v_root
|
|
5819
|
+
AND vu.provider = BTRIM(p_viewer_provider)
|
|
5820
|
+
AND vu.subject = BTRIM(p_viewer_subject))
|
|
5821
|
+
FROM ${s}.sessions r
|
|
5822
|
+
LEFT JOIN ${s}.session_owners so ON so.session_id = r.session_id
|
|
5823
|
+
LEFT JOIN ${s}.users u ON u.user_id = so.user_id
|
|
5824
|
+
WHERE r.session_id = v_root;
|
|
5825
|
+
END;
|
|
5826
|
+
$$ LANGUAGE plpgsql;
|
|
5827
|
+
|
|
5828
|
+
-- ── cms_record_authz_audit / cms_list_authz_audit ────────────────
|
|
5829
|
+
CREATE OR REPLACE FUNCTION ${s}.cms_record_authz_audit(
|
|
5830
|
+
p_actor_provider TEXT,
|
|
5831
|
+
p_actor_subject TEXT,
|
|
5832
|
+
p_actor_display TEXT,
|
|
5833
|
+
p_action TEXT,
|
|
5834
|
+
p_session_id TEXT,
|
|
5835
|
+
p_target TEXT,
|
|
5836
|
+
p_decision TEXT,
|
|
5837
|
+
p_reason TEXT,
|
|
5838
|
+
p_details JSONB
|
|
5839
|
+
) RETURNS VOID AS $$
|
|
5840
|
+
BEGIN
|
|
5841
|
+
INSERT INTO ${s}.authz_audit
|
|
5842
|
+
(actor_provider, actor_subject, actor_display, action, session_id, target, decision, reason, details)
|
|
5843
|
+
VALUES
|
|
5844
|
+
(p_actor_provider, p_actor_subject, p_actor_display, p_action, p_session_id, p_target, p_decision, p_reason, COALESCE(p_details, '{}'::JSONB));
|
|
5845
|
+
END;
|
|
5846
|
+
$$ LANGUAGE plpgsql;
|
|
5847
|
+
|
|
5848
|
+
DROP FUNCTION IF EXISTS ${s}.cms_list_authz_audit(INT, TEXT);
|
|
5849
|
+
CREATE FUNCTION ${s}.cms_list_authz_audit(
|
|
5850
|
+
p_limit INT,
|
|
5851
|
+
p_session_id TEXT
|
|
5852
|
+
) RETURNS TABLE (
|
|
5853
|
+
audit_id BIGINT,
|
|
5854
|
+
occurred_at TIMESTAMPTZ,
|
|
5855
|
+
actor_provider TEXT,
|
|
5856
|
+
actor_subject TEXT,
|
|
5857
|
+
actor_display TEXT,
|
|
5858
|
+
action TEXT,
|
|
5859
|
+
session_id TEXT,
|
|
5860
|
+
target TEXT,
|
|
5861
|
+
decision TEXT,
|
|
5862
|
+
reason TEXT,
|
|
5863
|
+
details JSONB
|
|
5864
|
+
) AS $$
|
|
5865
|
+
DECLARE
|
|
5866
|
+
v_limit INT := GREATEST(1, LEAST(COALESCE(p_limit, 100), 500));
|
|
5867
|
+
BEGIN
|
|
5868
|
+
RETURN QUERY
|
|
5869
|
+
SELECT a.audit_id, a.occurred_at, a.actor_provider, a.actor_subject, a.actor_display,
|
|
5870
|
+
a.action, a.session_id, a.target, a.decision, a.reason, a.details
|
|
5871
|
+
FROM ${s}.authz_audit a
|
|
5872
|
+
WHERE p_session_id IS NULL
|
|
5873
|
+
OR a.session_id = p_session_id
|
|
5874
|
+
OR a.session_id IN (
|
|
5875
|
+
SELECT sess.session_id FROM ${s}.sessions sess
|
|
5876
|
+
WHERE COALESCE(sess.root_session_id, sess.session_id) = ${s}.cms_resolve_root_session(p_session_id)
|
|
5877
|
+
)
|
|
5878
|
+
ORDER BY a.occurred_at DESC, a.audit_id DESC
|
|
5879
|
+
LIMIT v_limit;
|
|
5880
|
+
END;
|
|
5881
|
+
$$ LANGUAGE plpgsql;
|
|
5882
|
+
|
|
5883
|
+
-- ── Read procs: append visibility + root_session_id ──────────────
|
|
5884
|
+
-- All four session read procs gain the two new columns AT THE END of the
|
|
5885
|
+
-- return shape (after splash_mobile) so rowToSessionRow handles every path
|
|
5886
|
+
-- identically. Return-shape changes require drop-then-create.
|
|
5887
|
+
|
|
5888
|
+
-- cms_get_session
|
|
5889
|
+
DROP FUNCTION IF EXISTS ${s}.cms_get_session(TEXT);
|
|
5890
|
+
CREATE FUNCTION ${s}.cms_get_session(
|
|
5891
|
+
p_session_id TEXT
|
|
5892
|
+
) RETURNS TABLE (
|
|
5893
|
+
session_id TEXT,
|
|
5894
|
+
orchestration_id TEXT,
|
|
5895
|
+
title TEXT,
|
|
5896
|
+
title_locked BOOLEAN,
|
|
5897
|
+
state TEXT,
|
|
5898
|
+
model TEXT,
|
|
5899
|
+
reasoning_effort TEXT,
|
|
5900
|
+
group_id TEXT,
|
|
5901
|
+
short_summary TEXT,
|
|
5902
|
+
summary_state JSONB,
|
|
5903
|
+
summary_updated_at TIMESTAMPTZ,
|
|
5904
|
+
created_at TIMESTAMPTZ,
|
|
5905
|
+
updated_at TIMESTAMPTZ,
|
|
5906
|
+
last_active_at TIMESTAMPTZ,
|
|
5907
|
+
deleted_at TIMESTAMPTZ,
|
|
5908
|
+
current_iteration INTEGER,
|
|
5909
|
+
last_error TEXT,
|
|
5910
|
+
parent_session_id TEXT,
|
|
5911
|
+
wait_reason TEXT,
|
|
5912
|
+
is_system BOOLEAN,
|
|
5913
|
+
agent_id TEXT,
|
|
5914
|
+
splash TEXT,
|
|
5915
|
+
owner_provider TEXT,
|
|
5916
|
+
owner_subject TEXT,
|
|
5917
|
+
owner_email TEXT,
|
|
5918
|
+
owner_display_name TEXT,
|
|
5919
|
+
active_turn_index INTEGER,
|
|
5920
|
+
splash_mobile TEXT,
|
|
5921
|
+
visibility TEXT,
|
|
5922
|
+
root_session_id TEXT
|
|
5923
|
+
) AS $$
|
|
5924
|
+
BEGIN
|
|
5925
|
+
RETURN QUERY
|
|
5926
|
+
SELECT
|
|
5927
|
+
sess.session_id,
|
|
5928
|
+
sess.orchestration_id,
|
|
5929
|
+
sess.title,
|
|
5930
|
+
sess.title_locked,
|
|
5931
|
+
sess.state,
|
|
5932
|
+
sess.model,
|
|
5933
|
+
sess.reasoning_effort,
|
|
5934
|
+
sess.group_id,
|
|
5935
|
+
sess.short_summary,
|
|
5936
|
+
sess.summary_state,
|
|
5937
|
+
sess.summary_updated_at,
|
|
5938
|
+
sess.created_at,
|
|
5939
|
+
sess.updated_at,
|
|
5940
|
+
sess.last_active_at,
|
|
5941
|
+
sess.deleted_at,
|
|
5942
|
+
sess.current_iteration,
|
|
5943
|
+
sess.last_error,
|
|
5944
|
+
sess.parent_session_id,
|
|
5945
|
+
sess.wait_reason,
|
|
5946
|
+
sess.is_system,
|
|
5947
|
+
sess.agent_id,
|
|
5948
|
+
sess.splash,
|
|
5949
|
+
u.provider AS owner_provider,
|
|
5950
|
+
u.subject AS owner_subject,
|
|
5951
|
+
u.email AS owner_email,
|
|
5952
|
+
u.display_name AS owner_display_name,
|
|
5953
|
+
sess.active_turn_index,
|
|
5954
|
+
sess.splash_mobile,
|
|
5955
|
+
sess.visibility,
|
|
5956
|
+
sess.root_session_id
|
|
5957
|
+
FROM ${s}.sessions sess
|
|
5958
|
+
LEFT JOIN ${s}.session_owners so ON so.session_id = sess.session_id
|
|
5959
|
+
LEFT JOIN ${s}.users u ON u.user_id = so.user_id
|
|
5960
|
+
WHERE sess.session_id = p_session_id AND sess.deleted_at IS NULL;
|
|
5961
|
+
END;
|
|
5962
|
+
$$ LANGUAGE plpgsql;
|
|
5963
|
+
|
|
5964
|
+
-- cms_list_sessions / cms_list_group_sessions (shapes must stay identical)
|
|
5965
|
+
DROP FUNCTION IF EXISTS ${s}.cms_list_group_sessions(TEXT);
|
|
5966
|
+
DROP FUNCTION IF EXISTS ${s}.cms_list_sessions();
|
|
5967
|
+
CREATE FUNCTION ${s}.cms_list_sessions()
|
|
5968
|
+
RETURNS TABLE (
|
|
5969
|
+
session_id TEXT,
|
|
5970
|
+
orchestration_id TEXT,
|
|
5971
|
+
title TEXT,
|
|
5972
|
+
title_locked BOOLEAN,
|
|
5973
|
+
state TEXT,
|
|
5974
|
+
model TEXT,
|
|
5975
|
+
reasoning_effort TEXT,
|
|
5976
|
+
group_id TEXT,
|
|
5977
|
+
short_summary TEXT,
|
|
5978
|
+
summary_state JSONB,
|
|
5979
|
+
summary_updated_at TIMESTAMPTZ,
|
|
5980
|
+
created_at TIMESTAMPTZ,
|
|
5981
|
+
updated_at TIMESTAMPTZ,
|
|
5982
|
+
last_active_at TIMESTAMPTZ,
|
|
5983
|
+
deleted_at TIMESTAMPTZ,
|
|
5984
|
+
current_iteration INTEGER,
|
|
5985
|
+
last_error TEXT,
|
|
5986
|
+
parent_session_id TEXT,
|
|
5987
|
+
wait_reason TEXT,
|
|
5988
|
+
is_system BOOLEAN,
|
|
5989
|
+
agent_id TEXT,
|
|
5990
|
+
splash TEXT,
|
|
5991
|
+
owner_provider TEXT,
|
|
5992
|
+
owner_subject TEXT,
|
|
5993
|
+
owner_email TEXT,
|
|
5994
|
+
owner_display_name TEXT,
|
|
5995
|
+
splash_mobile TEXT,
|
|
5996
|
+
visibility TEXT,
|
|
5997
|
+
root_session_id TEXT
|
|
5998
|
+
) AS $$
|
|
5999
|
+
BEGIN
|
|
6000
|
+
RETURN QUERY
|
|
6001
|
+
SELECT
|
|
6002
|
+
sess.session_id,
|
|
6003
|
+
sess.orchestration_id,
|
|
6004
|
+
sess.title,
|
|
6005
|
+
sess.title_locked,
|
|
6006
|
+
sess.state,
|
|
6007
|
+
sess.model,
|
|
6008
|
+
sess.reasoning_effort,
|
|
6009
|
+
sess.group_id,
|
|
6010
|
+
sess.short_summary,
|
|
6011
|
+
sess.summary_state,
|
|
6012
|
+
sess.summary_updated_at,
|
|
6013
|
+
sess.created_at,
|
|
6014
|
+
sess.updated_at,
|
|
6015
|
+
sess.last_active_at,
|
|
6016
|
+
sess.deleted_at,
|
|
6017
|
+
sess.current_iteration,
|
|
6018
|
+
sess.last_error,
|
|
6019
|
+
sess.parent_session_id,
|
|
6020
|
+
sess.wait_reason,
|
|
6021
|
+
sess.is_system,
|
|
6022
|
+
sess.agent_id,
|
|
6023
|
+
sess.splash,
|
|
6024
|
+
u.provider AS owner_provider,
|
|
6025
|
+
u.subject AS owner_subject,
|
|
6026
|
+
u.email AS owner_email,
|
|
6027
|
+
u.display_name AS owner_display_name,
|
|
6028
|
+
sess.splash_mobile,
|
|
6029
|
+
sess.visibility,
|
|
6030
|
+
sess.root_session_id
|
|
6031
|
+
FROM ${s}.sessions sess
|
|
6032
|
+
LEFT JOIN ${s}.session_owners so ON so.session_id = sess.session_id
|
|
6033
|
+
LEFT JOIN ${s}.users u ON u.user_id = so.user_id
|
|
6034
|
+
WHERE sess.deleted_at IS NULL
|
|
6035
|
+
ORDER BY sess.updated_at DESC, sess.session_id DESC;
|
|
6036
|
+
END;
|
|
6037
|
+
$$ LANGUAGE plpgsql;
|
|
6038
|
+
|
|
6039
|
+
CREATE FUNCTION ${s}.cms_list_group_sessions(
|
|
6040
|
+
p_group_id TEXT
|
|
6041
|
+
) RETURNS TABLE (
|
|
6042
|
+
session_id TEXT,
|
|
6043
|
+
orchestration_id TEXT,
|
|
6044
|
+
title TEXT,
|
|
6045
|
+
title_locked BOOLEAN,
|
|
6046
|
+
state TEXT,
|
|
6047
|
+
model TEXT,
|
|
6048
|
+
reasoning_effort TEXT,
|
|
6049
|
+
group_id TEXT,
|
|
6050
|
+
short_summary TEXT,
|
|
6051
|
+
summary_state JSONB,
|
|
6052
|
+
summary_updated_at TIMESTAMPTZ,
|
|
6053
|
+
created_at TIMESTAMPTZ,
|
|
6054
|
+
updated_at TIMESTAMPTZ,
|
|
6055
|
+
last_active_at TIMESTAMPTZ,
|
|
6056
|
+
deleted_at TIMESTAMPTZ,
|
|
6057
|
+
current_iteration INTEGER,
|
|
6058
|
+
last_error TEXT,
|
|
6059
|
+
parent_session_id TEXT,
|
|
6060
|
+
wait_reason TEXT,
|
|
6061
|
+
is_system BOOLEAN,
|
|
6062
|
+
agent_id TEXT,
|
|
6063
|
+
splash TEXT,
|
|
6064
|
+
owner_provider TEXT,
|
|
6065
|
+
owner_subject TEXT,
|
|
6066
|
+
owner_email TEXT,
|
|
6067
|
+
owner_display_name TEXT,
|
|
6068
|
+
splash_mobile TEXT,
|
|
6069
|
+
visibility TEXT,
|
|
6070
|
+
root_session_id TEXT
|
|
6071
|
+
) AS $$
|
|
6072
|
+
BEGIN
|
|
6073
|
+
RETURN QUERY
|
|
6074
|
+
SELECT * FROM ${s}.cms_list_sessions() sess
|
|
6075
|
+
WHERE sess.group_id = p_group_id
|
|
6076
|
+
ORDER BY sess.updated_at DESC, sess.session_id DESC;
|
|
6077
|
+
END;
|
|
6078
|
+
$$ LANGUAGE plpgsql;
|
|
6079
|
+
|
|
6080
|
+
-- cms_list_sessions_page: viewer-aware. p_viewer_provider NULL = unfiltered
|
|
6081
|
+
-- (admin / internal callers). Non-NULL viewer sees: own trees, trees shared
|
|
6082
|
+
-- deployment-wide or granted to them, and system sessions when
|
|
6083
|
+
-- p_viewer_system_visible.
|
|
6084
|
+
DROP FUNCTION IF EXISTS ${s}.cms_list_sessions_page(INT, TIMESTAMPTZ, TEXT, BOOL);
|
|
6085
|
+
CREATE FUNCTION ${s}.cms_list_sessions_page(
|
|
6086
|
+
p_limit INT DEFAULT 51,
|
|
6087
|
+
p_cursor_updated_at TIMESTAMPTZ DEFAULT NULL,
|
|
6088
|
+
p_cursor_session_id TEXT DEFAULT NULL,
|
|
6089
|
+
p_include_deleted BOOL DEFAULT FALSE,
|
|
6090
|
+
p_viewer_provider TEXT DEFAULT NULL,
|
|
6091
|
+
p_viewer_subject TEXT DEFAULT NULL,
|
|
6092
|
+
p_viewer_system_visible BOOL DEFAULT TRUE
|
|
6093
|
+
) RETURNS TABLE (
|
|
6094
|
+
session_id TEXT,
|
|
6095
|
+
orchestration_id TEXT,
|
|
6096
|
+
title TEXT,
|
|
6097
|
+
title_locked BOOLEAN,
|
|
6098
|
+
state TEXT,
|
|
6099
|
+
model TEXT,
|
|
6100
|
+
reasoning_effort TEXT,
|
|
6101
|
+
group_id TEXT,
|
|
6102
|
+
short_summary TEXT,
|
|
6103
|
+
summary_state JSONB,
|
|
6104
|
+
summary_updated_at TIMESTAMPTZ,
|
|
6105
|
+
created_at TIMESTAMPTZ,
|
|
6106
|
+
updated_at TIMESTAMPTZ,
|
|
6107
|
+
last_active_at TIMESTAMPTZ,
|
|
6108
|
+
deleted_at TIMESTAMPTZ,
|
|
6109
|
+
current_iteration INTEGER,
|
|
6110
|
+
last_error TEXT,
|
|
6111
|
+
parent_session_id TEXT,
|
|
6112
|
+
wait_reason TEXT,
|
|
6113
|
+
is_system BOOLEAN,
|
|
6114
|
+
agent_id TEXT,
|
|
6115
|
+
splash TEXT,
|
|
6116
|
+
owner_provider TEXT,
|
|
6117
|
+
owner_subject TEXT,
|
|
6118
|
+
owner_email TEXT,
|
|
6119
|
+
owner_display_name TEXT,
|
|
6120
|
+
splash_mobile TEXT,
|
|
6121
|
+
visibility TEXT,
|
|
6122
|
+
root_session_id TEXT
|
|
6123
|
+
) AS $$
|
|
6124
|
+
DECLARE
|
|
6125
|
+
v_limit INT := GREATEST(1, LEAST(COALESCE(p_limit, 51), 201));
|
|
6126
|
+
BEGIN
|
|
6127
|
+
RETURN QUERY
|
|
6128
|
+
SELECT
|
|
6129
|
+
sess.session_id,
|
|
6130
|
+
sess.orchestration_id,
|
|
6131
|
+
sess.title,
|
|
6132
|
+
sess.title_locked,
|
|
6133
|
+
sess.state,
|
|
6134
|
+
sess.model,
|
|
6135
|
+
sess.reasoning_effort,
|
|
6136
|
+
sess.group_id,
|
|
6137
|
+
sess.short_summary,
|
|
6138
|
+
sess.summary_state,
|
|
6139
|
+
sess.summary_updated_at,
|
|
6140
|
+
sess.created_at,
|
|
6141
|
+
sess.updated_at,
|
|
6142
|
+
sess.last_active_at,
|
|
6143
|
+
sess.deleted_at,
|
|
6144
|
+
sess.current_iteration,
|
|
6145
|
+
sess.last_error,
|
|
6146
|
+
sess.parent_session_id,
|
|
6147
|
+
sess.wait_reason,
|
|
6148
|
+
sess.is_system,
|
|
6149
|
+
sess.agent_id,
|
|
6150
|
+
sess.splash,
|
|
6151
|
+
u.provider AS owner_provider,
|
|
6152
|
+
u.subject AS owner_subject,
|
|
6153
|
+
u.email AS owner_email,
|
|
6154
|
+
u.display_name AS owner_display_name,
|
|
6155
|
+
sess.splash_mobile,
|
|
6156
|
+
sess.visibility,
|
|
6157
|
+
sess.root_session_id
|
|
6158
|
+
FROM ${s}.sessions sess
|
|
6159
|
+
LEFT JOIN ${s}.session_owners so ON so.session_id = sess.session_id
|
|
6160
|
+
LEFT JOIN ${s}.users u ON u.user_id = so.user_id
|
|
6161
|
+
WHERE
|
|
6162
|
+
(p_include_deleted OR sess.deleted_at IS NULL)
|
|
6163
|
+
AND (
|
|
6164
|
+
p_cursor_updated_at IS NULL
|
|
6165
|
+
OR sess.updated_at < p_cursor_updated_at
|
|
6166
|
+
OR (sess.updated_at = p_cursor_updated_at AND sess.session_id < p_cursor_session_id)
|
|
6167
|
+
)
|
|
6168
|
+
AND (
|
|
6169
|
+
p_viewer_provider IS NULL
|
|
6170
|
+
OR EXISTS (
|
|
6171
|
+
SELECT 1
|
|
6172
|
+
FROM ${s}.sessions r
|
|
6173
|
+
LEFT JOIN ${s}.session_owners rso ON rso.session_id = r.session_id
|
|
6174
|
+
LEFT JOIN ${s}.users ru ON ru.user_id = rso.user_id
|
|
6175
|
+
WHERE r.session_id = COALESCE(sess.root_session_id, sess.session_id)
|
|
6176
|
+
AND (
|
|
6177
|
+
(r.is_system AND p_viewer_system_visible)
|
|
6178
|
+
OR (ru.provider = BTRIM(p_viewer_provider) AND ru.subject = BTRIM(p_viewer_subject))
|
|
6179
|
+
OR COALESCE(r.visibility, 'private') IN ('shared_read', 'shared_write')
|
|
6180
|
+
OR EXISTS (
|
|
6181
|
+
SELECT 1 FROM ${s}.session_shares sh
|
|
6182
|
+
JOIN ${s}.users vu ON vu.user_id = sh.user_id
|
|
6183
|
+
WHERE sh.session_id = r.session_id
|
|
6184
|
+
AND vu.provider = BTRIM(p_viewer_provider)
|
|
6185
|
+
AND vu.subject = BTRIM(p_viewer_subject)
|
|
6186
|
+
)
|
|
6187
|
+
)
|
|
6188
|
+
)
|
|
6189
|
+
)
|
|
6190
|
+
ORDER BY sess.updated_at DESC, sess.session_id DESC
|
|
6191
|
+
LIMIT v_limit;
|
|
6192
|
+
END;
|
|
6193
|
+
$$ LANGUAGE plpgsql;
|
|
6194
|
+
|
|
6195
|
+
-- cms_list_sessions_visible: non-paged viewer-scoped listing, same shape and
|
|
6196
|
+
-- predicate as the paged variant. Used by the plain listSessions op for
|
|
6197
|
+
-- non-admin callers.
|
|
6198
|
+
DROP FUNCTION IF EXISTS ${s}.cms_list_sessions_visible(TEXT, TEXT, BOOL);
|
|
6199
|
+
CREATE FUNCTION ${s}.cms_list_sessions_visible(
|
|
6200
|
+
p_viewer_provider TEXT,
|
|
6201
|
+
p_viewer_subject TEXT,
|
|
6202
|
+
p_viewer_system_visible BOOL
|
|
6203
|
+
) RETURNS TABLE (
|
|
6204
|
+
session_id TEXT,
|
|
6205
|
+
orchestration_id TEXT,
|
|
6206
|
+
title TEXT,
|
|
6207
|
+
title_locked BOOLEAN,
|
|
6208
|
+
state TEXT,
|
|
6209
|
+
model TEXT,
|
|
6210
|
+
reasoning_effort TEXT,
|
|
6211
|
+
group_id TEXT,
|
|
6212
|
+
short_summary TEXT,
|
|
6213
|
+
summary_state JSONB,
|
|
6214
|
+
summary_updated_at TIMESTAMPTZ,
|
|
6215
|
+
created_at TIMESTAMPTZ,
|
|
6216
|
+
updated_at TIMESTAMPTZ,
|
|
6217
|
+
last_active_at TIMESTAMPTZ,
|
|
6218
|
+
deleted_at TIMESTAMPTZ,
|
|
6219
|
+
current_iteration INTEGER,
|
|
6220
|
+
last_error TEXT,
|
|
6221
|
+
parent_session_id TEXT,
|
|
6222
|
+
wait_reason TEXT,
|
|
6223
|
+
is_system BOOLEAN,
|
|
6224
|
+
agent_id TEXT,
|
|
6225
|
+
splash TEXT,
|
|
6226
|
+
owner_provider TEXT,
|
|
6227
|
+
owner_subject TEXT,
|
|
6228
|
+
owner_email TEXT,
|
|
6229
|
+
owner_display_name TEXT,
|
|
6230
|
+
splash_mobile TEXT,
|
|
6231
|
+
visibility TEXT,
|
|
6232
|
+
root_session_id TEXT
|
|
6233
|
+
) AS $$
|
|
6234
|
+
BEGIN
|
|
6235
|
+
RETURN QUERY
|
|
6236
|
+
SELECT * FROM ${s}.cms_list_sessions() sess
|
|
6237
|
+
WHERE EXISTS (
|
|
6238
|
+
SELECT 1
|
|
6239
|
+
FROM ${s}.sessions r
|
|
6240
|
+
LEFT JOIN ${s}.session_owners rso ON rso.session_id = r.session_id
|
|
6241
|
+
LEFT JOIN ${s}.users ru ON ru.user_id = rso.user_id
|
|
6242
|
+
WHERE r.session_id = COALESCE(sess.root_session_id, sess.session_id)
|
|
6243
|
+
AND (
|
|
6244
|
+
(r.is_system AND p_viewer_system_visible)
|
|
6245
|
+
OR (ru.provider = BTRIM(p_viewer_provider) AND ru.subject = BTRIM(p_viewer_subject))
|
|
6246
|
+
OR COALESCE(r.visibility, 'private') IN ('shared_read', 'shared_write')
|
|
6247
|
+
OR EXISTS (
|
|
6248
|
+
SELECT 1 FROM ${s}.session_shares sh
|
|
6249
|
+
JOIN ${s}.users vu ON vu.user_id = sh.user_id
|
|
6250
|
+
WHERE sh.session_id = r.session_id
|
|
6251
|
+
AND vu.provider = BTRIM(p_viewer_provider)
|
|
6252
|
+
AND vu.subject = BTRIM(p_viewer_subject)
|
|
6253
|
+
)
|
|
6254
|
+
)
|
|
6255
|
+
)
|
|
6256
|
+
ORDER BY sess.updated_at DESC, sess.session_id DESC;
|
|
6257
|
+
END;
|
|
6258
|
+
$$ LANGUAGE plpgsql;
|
|
6259
|
+
`;
|
|
6260
|
+
return [step_columns, step_backfill, step_drop_invalid_index, step_index, step_tables_and_functions];
|
|
6261
|
+
}
|
|
5133
6262
|
//# sourceMappingURL=cms-migrations.js.map
|