pilotswarm-sdk 0.5.28 → 0.5.30

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 (69) hide show
  1. package/api/src/api-client.js +10 -1
  2. package/api/src/http-api-transport.js +22 -2
  3. package/api/src/protocol.js +25 -0
  4. package/dist/agent-loader.d.ts +8 -0
  5. package/dist/agent-loader.d.ts.map +1 -1
  6. package/dist/agent-loader.js +23 -0
  7. package/dist/agent-loader.js.map +1 -1
  8. package/dist/agent-package-format.d.ts +175 -0
  9. package/dist/agent-package-format.d.ts.map +1 -0
  10. package/dist/agent-package-format.js +812 -0
  11. package/dist/agent-package-format.js.map +1 -0
  12. package/dist/agent-package-installer.d.ts +64 -0
  13. package/dist/agent-package-installer.d.ts.map +1 -0
  14. package/dist/agent-package-installer.js +148 -0
  15. package/dist/agent-package-installer.js.map +1 -0
  16. package/dist/agent-package-service.d.ts +61 -0
  17. package/dist/agent-package-service.d.ts.map +1 -0
  18. package/dist/agent-package-service.js +181 -0
  19. package/dist/agent-package-service.js.map +1 -0
  20. package/dist/client.d.ts +6 -0
  21. package/dist/client.d.ts.map +1 -1
  22. package/dist/client.js +28 -0
  23. package/dist/client.js.map +1 -1
  24. package/dist/cms-migrations.d.ts.map +1 -1
  25. package/dist/cms-migrations.js +799 -0
  26. package/dist/cms-migrations.js.map +1 -1
  27. package/dist/cms.d.ts +224 -0
  28. package/dist/cms.d.ts.map +1 -1
  29. package/dist/cms.js +262 -0
  30. package/dist/cms.js.map +1 -1
  31. package/dist/index.d.ts +12 -2
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +11 -2
  34. package/dist/index.js.map +1 -1
  35. package/dist/management-client.d.ts +15 -0
  36. package/dist/management-client.d.ts.map +1 -1
  37. package/dist/management-client.js +38 -3
  38. package/dist/management-client.js.map +1 -1
  39. package/dist/mcp-loader.d.ts.map +1 -1
  40. package/dist/mcp-loader.js +17 -1
  41. package/dist/mcp-loader.js.map +1 -1
  42. package/dist/session-manager.d.ts +2 -0
  43. package/dist/session-manager.d.ts.map +1 -1
  44. package/dist/session-manager.js +11 -1
  45. package/dist/session-manager.js.map +1 -1
  46. package/dist/tool-pinning.d.ts +27 -0
  47. package/dist/tool-pinning.d.ts.map +1 -0
  48. package/dist/tool-pinning.js +31 -0
  49. package/dist/tool-pinning.js.map +1 -0
  50. package/dist/types.d.ts +27 -0
  51. package/dist/types.d.ts.map +1 -1
  52. package/dist/types.js.map +1 -1
  53. package/dist/web/api-connection.d.ts +8 -0
  54. package/dist/web/api-connection.d.ts.map +1 -1
  55. package/dist/web/api-connection.js +2 -0
  56. package/dist/web/api-connection.js.map +1 -1
  57. package/dist/web/generated-op-methods.d.ts +917 -0
  58. package/dist/web/generated-op-methods.d.ts.map +1 -0
  59. package/dist/web/generated-op-methods.js +255 -0
  60. package/dist/web/generated-op-methods.js.map +1 -0
  61. package/dist/web/web-management-client.d.ts +191 -8
  62. package/dist/web/web-management-client.d.ts.map +1 -1
  63. package/dist/web/web-management-client.js +164 -12
  64. package/dist/web/web-management-client.js.map +1 -1
  65. package/dist/worker.d.ts +69 -0
  66. package/dist/worker.d.ts.map +1 -1
  67. package/dist/worker.js +319 -11
  68. package/dist/worker.js.map +1 -1
  69. package/package.json +7 -6
@@ -197,8 +197,729 @@ export function CMS_MIGRATIONS(schema) {
197
197
  name: "service_sessions",
198
198
  sql: migration_0037_service_sessions(schema),
199
199
  },
200
+ {
201
+ version: "0038",
202
+ name: "agent_packages",
203
+ sql: migration_0038_agent_packages(schema),
204
+ },
205
+ {
206
+ version: "0039",
207
+ name: "agent_worker_heartbeat_prune",
208
+ sql: migration_0039_agent_worker_heartbeat_prune(schema),
209
+ },
210
+ {
211
+ version: "0040",
212
+ name: "worker_registry",
213
+ sql: migration_0040_worker_registry(schema),
214
+ },
215
+ {
216
+ version: "0041",
217
+ name: "agent_package_owner_identity",
218
+ sql: migration_0041_agent_package_owner_identity(schema),
219
+ },
200
220
  ];
201
221
  }
222
+ // ─── Migration 0040: worker registry ─────────────────────────────
223
+ //
224
+ // docs/proposals/worker-registry.md (rev 4). Substrate-neutral, uniformly
225
+ // ephemeral workers (presence, not enrollment) + fleet_directives, the
226
+ // scoped desired-state channel (fleet/pool/worker, shallow-merge union,
227
+ // epoch = SUM of contributing rows). The heartbeat IS the convergence poll:
228
+ // one proc upserts the worker row, prunes hour-silent rows, and returns the
229
+ // effective directive set.
230
+ //
231
+ // Agent packages fold in as the first tenant VIA SHIMS with unchanged
232
+ // signatures: cms_agent_registry_bump/_epoch ride the
233
+ // ('agent-packages','*','*') directive row (seeded from
234
+ // agent_registry_state so epochs continue monotonically — old workers see
235
+ // no spurious change), and cms_list_agent_worker_state unions the legacy
236
+ // table with workers.state so mixed fleets display correctly during a
237
+ // rolling upgrade. Legacy tables are dropped later (0041) once the fleet
238
+ // is upgraded.
239
+ function migration_0040_worker_registry(schema) {
240
+ const s = `"${schema}"`;
241
+ return `
242
+ -- 0040_worker_registry: workers + fleet_directives + heartbeat + shims.
243
+
244
+ CREATE TABLE IF NOT EXISTS ${s}.workers (
245
+ worker_node_id TEXT PRIMARY KEY,
246
+ pool TEXT NOT NULL DEFAULT 'default',
247
+ phase TEXT NOT NULL DEFAULT 'starting' CHECK (phase IN ('starting', 'ready', 'draining')),
248
+ owner_provider TEXT,
249
+ owner_subject TEXT,
250
+ registered_at TIMESTAMPTZ NOT NULL DEFAULT now(),
251
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
252
+ info JSONB NOT NULL DEFAULT '{}'::jsonb,
253
+ health JSONB NOT NULL DEFAULT '{}'::jsonb,
254
+ state JSONB NOT NULL DEFAULT '{}'::jsonb
255
+ );
256
+
257
+ CREATE INDEX IF NOT EXISTS workers_pool_idx ON ${s}.workers (pool);
258
+
259
+ CREATE TABLE IF NOT EXISTS ${s}.fleet_directives (
260
+ domain TEXT NOT NULL,
261
+ pool TEXT NOT NULL DEFAULT '*',
262
+ worker_node_id TEXT NOT NULL DEFAULT '*',
263
+ epoch BIGINT NOT NULL DEFAULT 1,
264
+ actuation TEXT NOT NULL DEFAULT 'worker' CHECK (actuation IN ('worker', 'external')),
265
+ desired JSONB NOT NULL DEFAULT '{}'::jsonb,
266
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
267
+ updated_by TEXT,
268
+ -- Canonical form is structural: a row specializes pool OR worker, never
269
+ -- both, so even direct SQL cannot create an undefined-precedence row.
270
+ CONSTRAINT fleet_directives_canonical_scope CHECK (pool = '*' OR worker_node_id = '*'),
271
+ PRIMARY KEY (domain, pool, worker_node_id)
272
+ );
273
+
274
+ -- Seed the agent-packages directive from the legacy epoch so old and new
275
+ -- workers observe ONE continuous monotonic counter. Idempotent; the second
276
+ -- insert covers schemas where agent_registry_state was never populated.
277
+ INSERT INTO ${s}.fleet_directives (domain, pool, worker_node_id, epoch, actuation, desired, updated_by)
278
+ SELECT 'agent-packages', '*', '*', GREATEST(st.epoch, 1), 'worker', '{}'::jsonb, 'migration-0040'
279
+ FROM ${s}.agent_registry_state st WHERE st.id = 1
280
+ ON CONFLICT (domain, pool, worker_node_id) DO NOTHING;
281
+ INSERT INTO ${s}.fleet_directives (domain, pool, worker_node_id, epoch, actuation, desired, updated_by)
282
+ VALUES ('agent-packages', '*', '*', 1, 'worker', '{}'::jsonb, 'migration-0040')
283
+ ON CONFLICT (domain, pool, worker_node_id) DO NOTHING;
284
+
285
+ -- ── directive mutation ───────────────────────────────────────────
286
+
287
+ CREATE OR REPLACE FUNCTION ${s}.cms_fleet_directive_bump(
288
+ p_domain TEXT, p_pool TEXT, p_worker_node_id TEXT,
289
+ p_desired JSONB, p_actuation TEXT, p_updated_by TEXT
290
+ ) RETURNS BIGINT AS $$
291
+ DECLARE
292
+ v_pool TEXT := COALESCE(NULLIF(BTRIM(p_pool), ''), '*');
293
+ v_worker TEXT := COALESCE(NULLIF(BTRIM(p_worker_node_id), ''), '*');
294
+ v_actuation TEXT := COALESCE(NULLIF(p_actuation, ''), 'worker');
295
+ v_existing_actuation TEXT;
296
+ v_epoch BIGINT;
297
+ BEGIN
298
+ -- One writer per domain: the actuation-uniformity check below is
299
+ -- read-then-write, and two first-bumps racing could persist a mixed
300
+ -- domain that wedges every later bump. Directive writes are admin-rate.
301
+ PERFORM pg_advisory_xact_lock(hashtext('fleet_directive:' || p_domain));
302
+ -- Canonical worker-row form: worker-scoped rows use pool '*'; a triple
303
+ -- with BOTH pool and worker specialized has no defined precedence.
304
+ IF v_worker <> '*' AND v_pool <> '*' THEN
305
+ RAISE EXCEPTION 'FLEET_DIRECTIVE_BAD_SCOPE: worker-scoped directives use pool ''*'' (canonical form)';
306
+ END IF;
307
+ -- agent-packages converges through the legacy epoch shim (fleet row
308
+ -- only) until the registrar consumes the heartbeat's directive set; a
309
+ -- scoped row would be silently dead while skewing the summed epoch.
310
+ IF p_domain = 'agent-packages' AND (v_pool <> '*' OR v_worker <> '*') THEN
311
+ RAISE EXCEPTION 'FLEET_DIRECTIVE_BAD_SCOPE: agent-packages directives are fleet-wide for now (workers converge on the fleet row only)';
312
+ END IF;
313
+ -- Actuation is a property of the DOMAIN, uniform across its rows.
314
+ SELECT d.actuation INTO v_existing_actuation
315
+ FROM ${s}.fleet_directives d WHERE d.domain = p_domain LIMIT 1;
316
+ IF v_existing_actuation IS NOT NULL AND v_existing_actuation <> v_actuation THEN
317
+ RAISE EXCEPTION 'FLEET_DIRECTIVE_ACTUATION_MISMATCH: domain "%" is %-actuated; all rows of a domain share one actuation', p_domain, v_existing_actuation;
318
+ END IF;
319
+ INSERT INTO ${s}.fleet_directives (domain, pool, worker_node_id, epoch, actuation, desired, updated_at, updated_by)
320
+ VALUES (p_domain, v_pool, v_worker, 1, v_actuation, COALESCE(p_desired, '{}'::jsonb), now(), p_updated_by)
321
+ ON CONFLICT (domain, pool, worker_node_id) DO UPDATE
322
+ SET epoch = ${s}.fleet_directives.epoch + 1,
323
+ desired = COALESCE(p_desired, ${s}.fleet_directives.desired),
324
+ updated_at = now(),
325
+ updated_by = COALESCE(p_updated_by, ${s}.fleet_directives.updated_by)
326
+ RETURNING epoch INTO v_epoch;
327
+ RETURN v_epoch;
328
+ END;
329
+ $$ LANGUAGE plpgsql;
330
+
331
+ CREATE OR REPLACE FUNCTION ${s}.cms_get_fleet_directives()
332
+ RETURNS TABLE(domain TEXT, pool TEXT, worker_node_id TEXT, epoch BIGINT,
333
+ actuation TEXT, desired JSONB, updated_at TIMESTAMPTZ, updated_by TEXT) AS $$
334
+ SELECT domain, pool, worker_node_id, epoch, actuation, desired, updated_at, updated_by
335
+ FROM ${s}.fleet_directives
336
+ ORDER BY domain, pool, worker_node_id;
337
+ $$ LANGUAGE sql;
338
+
339
+ -- ── the one round-trip ───────────────────────────────────────────
340
+ --
341
+ -- Upsert the worker row (info/owner insert-only; pool/phase/health/state/
342
+ -- updated_at every beat), apply the uniform prune, and return the worker's
343
+ -- effective directive set: per domain, the shallow-merge union of the
344
+ -- fleet/pool/worker rows (worker > pool > fleet on conflicting keys) with
345
+ -- epoch = SUM of contributing epochs (monotonic under any single-row bump).
346
+
347
+ CREATE OR REPLACE FUNCTION ${s}.cms_worker_heartbeat(
348
+ p_worker_node_id TEXT, p_pool TEXT, p_phase TEXT,
349
+ p_owner_provider TEXT, p_owner_subject TEXT,
350
+ p_info JSONB, p_health JSONB, p_state JSONB
351
+ ) RETURNS TABLE(domain TEXT, epoch BIGINT, actuation TEXT, desired JSONB) AS $$
352
+ DECLARE
353
+ v_pool TEXT := COALESCE(NULLIF(BTRIM(p_pool), ''), 'default');
354
+ -- Unknown phases coerce to 'starting' (never advertise garbage as
355
+ -- healthy); '*' or blank ids would cross-match every worker-scoped
356
+ -- directive at top specificity, so they are rejected outright.
357
+ v_phase TEXT := CASE WHEN p_phase IN ('starting', 'ready', 'draining') THEN p_phase ELSE 'starting' END;
358
+ BEGIN
359
+ IF p_worker_node_id IS NULL OR BTRIM(p_worker_node_id) = '' OR p_worker_node_id = '*' THEN
360
+ RAISE EXCEPTION 'WORKER_ID_INVALID: worker_node_id must be a non-empty identifier';
361
+ END IF;
362
+ INSERT INTO ${s}.workers (worker_node_id, pool, phase, owner_provider, owner_subject,
363
+ registered_at, updated_at, info, health, state)
364
+ VALUES (p_worker_node_id, v_pool, v_phase,
365
+ NULLIF(BTRIM(p_owner_provider), ''), NULLIF(BTRIM(p_owner_subject), ''),
366
+ now(), now(),
367
+ COALESCE(p_info, '{}'::jsonb), COALESCE(p_health, '{}'::jsonb), COALESCE(p_state, '{}'::jsonb))
368
+ ON CONFLICT (worker_node_id) DO UPDATE
369
+ SET pool = EXCLUDED.pool,
370
+ phase = EXCLUDED.phase,
371
+ health = EXCLUDED.health,
372
+ state = EXCLUDED.state,
373
+ updated_at = now();
374
+
375
+ -- Uniform gone-ness: silent for an hour = gone, whatever the substrate.
376
+ DELETE FROM ${s}.workers w WHERE w.updated_at < now() - interval '1 hour';
377
+
378
+ RETURN QUERY
379
+ WITH contrib AS (
380
+ SELECT d.domain AS c_domain, d.epoch AS c_epoch,
381
+ d.actuation AS c_actuation, d.desired AS c_desired,
382
+ CASE WHEN d.worker_node_id = p_worker_node_id THEN 3
383
+ WHEN d.pool = v_pool THEN 2
384
+ ELSE 1 END AS specificity
385
+ FROM ${s}.fleet_directives d
386
+ WHERE (d.pool = '*' AND d.worker_node_id = '*')
387
+ OR (d.pool = v_pool AND d.worker_node_id = '*')
388
+ OR (d.worker_node_id = p_worker_node_id)
389
+ )
390
+ SELECT c.c_domain,
391
+ SUM(c.c_epoch)::BIGINT,
392
+ (array_agg(c.c_actuation ORDER BY c.specificity DESC))[1],
393
+ COALESCE((array_agg(c.c_desired ORDER BY c.specificity ASC))[1], '{}'::jsonb)
394
+ || COALESCE((array_agg(c.c_desired ORDER BY c.specificity ASC))[2], '{}'::jsonb)
395
+ || COALESCE((array_agg(c.c_desired ORDER BY c.specificity ASC))[3], '{}'::jsonb)
396
+ FROM contrib c
397
+ GROUP BY c.c_domain;
398
+ END;
399
+ $$ LANGUAGE plpgsql;
400
+
401
+ CREATE OR REPLACE FUNCTION ${s}.cms_list_workers()
402
+ RETURNS TABLE(worker_node_id TEXT, pool TEXT, phase TEXT,
403
+ owner_provider TEXT, owner_subject TEXT,
404
+ registered_at TIMESTAMPTZ, updated_at TIMESTAMPTZ,
405
+ info JSONB, health JSONB, state JSONB) AS $$
406
+ SELECT worker_node_id, pool, phase, owner_provider, owner_subject,
407
+ registered_at, updated_at, info, health, state
408
+ FROM ${s}.workers
409
+ ORDER BY pool, worker_node_id;
410
+ $$ LANGUAGE sql;
411
+
412
+ -- ── agent-packages fold-in shims (signatures unchanged) ──────────
413
+
414
+ CREATE OR REPLACE FUNCTION ${s}.cms_agent_registry_bump() RETURNS BIGINT AS $$
415
+ SELECT ${s}.cms_fleet_directive_bump('agent-packages', '*', '*', NULL, 'worker', 'agent-packages');
416
+ $$ LANGUAGE sql;
417
+
418
+ CREATE OR REPLACE FUNCTION ${s}.cms_agent_registry_epoch() RETURNS BIGINT AS $$
419
+ SELECT COALESCE((SELECT d.epoch FROM ${s}.fleet_directives d
420
+ WHERE d.domain = 'agent-packages' AND d.pool = '*' AND d.worker_node_id = '*'), 0);
421
+ $$ LANGUAGE sql;
422
+
423
+ -- Mixed-fleet display: union legacy agent_worker_state (old workers still
424
+ -- write it) with workers.state->'agent-packages' (new workers), newest row
425
+ -- per worker wins. Read paths (portal fleet adoption, MCP) stay untouched.
426
+ CREATE OR REPLACE FUNCTION ${s}.cms_list_agent_worker_state()
427
+ RETURNS TABLE(worker_node_id TEXT, epoch BIGINT, installed JSONB, updated_at TIMESTAMPTZ) AS $$
428
+ SELECT DISTINCT ON (u.worker_node_id)
429
+ u.worker_node_id, u.epoch, u.installed, u.updated_at
430
+ FROM (
431
+ SELECT w.worker_node_id,
432
+ CASE WHEN jsonb_typeof(w.state->'agent-packages'->'epoch') = 'number'
433
+ THEN (w.state->'agent-packages'->>'epoch')::BIGINT
434
+ ELSE 0 END AS epoch,
435
+ COALESCE(w.state->'agent-packages'->'installed', '{}'::jsonb) AS installed,
436
+ w.updated_at
437
+ FROM ${s}.workers w
438
+ UNION ALL
439
+ SELECT a.worker_node_id, a.epoch, a.installed, a.updated_at
440
+ FROM ${s}.agent_worker_state a
441
+ ) u
442
+ ORDER BY u.worker_node_id, u.updated_at DESC;
443
+ $$ LANGUAGE sql;
444
+ `;
445
+ }
446
+ // ─── Migration 0039: agent_worker_state self-prune ───────────────
447
+ //
448
+ // Workers are EPHEMERAL (K8s pods; names change every rollout), so
449
+ // agent_worker_state accumulated a row per pod that ever reported and
450
+ // fleet adoption read "8/16 workers". Workers now heartbeat updated_at on
451
+ // every poll; liveness is a display-side window (~90s), and this upsert
452
+ // prunes rows silent for over an hour so the table stays the live fleet
453
+ // plus a short tail. Same signature as 0038's version — CREATE OR REPLACE.
454
+ function migration_0039_agent_worker_heartbeat_prune(schema) {
455
+ const s = `"${schema}"`;
456
+ return `
457
+ CREATE OR REPLACE FUNCTION ${s}.cms_upsert_agent_worker_state(
458
+ p_worker_node_id TEXT, p_epoch BIGINT, p_installed JSONB
459
+ ) RETURNS VOID AS $$
460
+ BEGIN
461
+ INSERT INTO ${s}.agent_worker_state (worker_node_id, epoch, installed, updated_at)
462
+ VALUES (p_worker_node_id, p_epoch, COALESCE(p_installed, '{}'::jsonb), now())
463
+ ON CONFLICT (worker_node_id) DO UPDATE
464
+ SET epoch = EXCLUDED.epoch,
465
+ installed = EXCLUDED.installed,
466
+ updated_at = now();
467
+ -- Ephemeral-fleet hygiene: a worker silent for an hour is gone (pods
468
+ -- report every ~20s; rollouts retire names forever).
469
+ DELETE FROM ${s}.agent_worker_state
470
+ WHERE updated_at < now() - interval '1 hour';
471
+ END;
472
+ $$ LANGUAGE plpgsql;
473
+ `;
474
+ }
475
+ // ─── Migration 0038: agent packages registry ─────────────────────
476
+ //
477
+ // User-uploadable agent packages (docs/proposals/agent-packages.md).
478
+ // All-new tables, so a single transactional migration is safe — no hot-table
479
+ // ALTERs, no steps shape needed. Identity is (name, semver); sha256 is a
480
+ // verifier. agent_registry_state.epoch is bumped by EVERY mutating proc that
481
+ // changes what workers should run — workers poll it to converge without
482
+ // restarts. auth_token follows the users.github_copilot_key posture: status
483
+ // procs return a boolean, the raw read is internal-only.
484
+ function migration_0038_agent_packages(schema) {
485
+ const s = `"${schema}"`;
486
+ return `
487
+ -- 0038_agent_packages: registry for user-uploadable agent packages.
488
+
489
+ CREATE TABLE IF NOT EXISTS ${s}.agent_sources (
490
+ source_id TEXT PRIMARY KEY,
491
+ kind TEXT NOT NULL CHECK (kind IN ('github', 'ado', 'url', 'upload')),
492
+ scope TEXT NOT NULL DEFAULT 'user' CHECK (scope IN ('shared', 'user')),
493
+ repo_url TEXT,
494
+ ref TEXT,
495
+ path TEXT,
496
+ url TEXT,
497
+ auth_token TEXT,
498
+ auto_sync BOOLEAN NOT NULL DEFAULT FALSE,
499
+ last_sync_at TIMESTAMPTZ,
500
+ last_sync_status TEXT,
501
+ last_sync_error TEXT,
502
+ last_commit_sha TEXT,
503
+ owner_provider TEXT,
504
+ owner_subject TEXT,
505
+ created_by TEXT,
506
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
507
+ );
508
+
509
+ CREATE TABLE IF NOT EXISTS ${s}.agent_packages (
510
+ package_id TEXT PRIMARY KEY,
511
+ source_id TEXT REFERENCES ${s}.agent_sources(source_id) ON DELETE SET NULL,
512
+ name TEXT NOT NULL UNIQUE,
513
+ scope TEXT NOT NULL CHECK (scope IN ('shared', 'user')),
514
+ owner_provider TEXT,
515
+ owner_subject TEXT,
516
+ enabled BOOLEAN NOT NULL DEFAULT TRUE,
517
+ active_version_id TEXT,
518
+ created_by TEXT,
519
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
520
+ );
521
+
522
+ CREATE INDEX IF NOT EXISTS agent_packages_owner_idx ON ${s}.agent_packages (owner_provider, owner_subject);
523
+
524
+ CREATE TABLE IF NOT EXISTS ${s}.agent_package_versions (
525
+ version_id TEXT PRIMARY KEY,
526
+ package_id TEXT NOT NULL REFERENCES ${s}.agent_packages(package_id) ON DELETE CASCADE,
527
+ semver TEXT NOT NULL,
528
+ sha256 TEXT NOT NULL,
529
+ size_bytes BIGINT NOT NULL,
530
+ artifact_filename TEXT NOT NULL,
531
+ commit_sha TEXT,
532
+ manifest JSONB NOT NULL,
533
+ created_by TEXT,
534
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
535
+ UNIQUE (package_id, semver)
536
+ );
537
+
538
+ CREATE TABLE IF NOT EXISTS ${s}.agent_registry_state (
539
+ id INTEGER PRIMARY KEY CHECK (id = 1),
540
+ epoch BIGINT NOT NULL DEFAULT 1,
541
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
542
+ );
543
+ INSERT INTO ${s}.agent_registry_state (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
544
+
545
+ CREATE TABLE IF NOT EXISTS ${s}.agent_worker_state (
546
+ worker_node_id TEXT PRIMARY KEY,
547
+ epoch BIGINT NOT NULL DEFAULT 0,
548
+ installed JSONB NOT NULL DEFAULT '{}'::jsonb,
549
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
550
+ );
551
+
552
+ -- ── epoch ────────────────────────────────────────────────────────
553
+
554
+ CREATE OR REPLACE FUNCTION ${s}.cms_agent_registry_epoch() RETURNS BIGINT AS $$
555
+ SELECT epoch FROM ${s}.agent_registry_state WHERE id = 1;
556
+ $$ LANGUAGE sql;
557
+
558
+ CREATE OR REPLACE FUNCTION ${s}.cms_agent_registry_bump() RETURNS BIGINT AS $$
559
+ UPDATE ${s}.agent_registry_state
560
+ SET epoch = epoch + 1, updated_at = now()
561
+ WHERE id = 1
562
+ RETURNING epoch;
563
+ $$ LANGUAGE sql;
564
+
565
+ -- ── sources ──────────────────────────────────────────────────────
566
+
567
+ CREATE OR REPLACE FUNCTION ${s}.cms_register_agent_source(
568
+ p_source_id TEXT, p_kind TEXT, p_scope TEXT, p_repo_url TEXT, p_ref TEXT, p_path TEXT,
569
+ p_url TEXT, p_auth_token TEXT, p_auto_sync BOOLEAN,
570
+ p_owner_provider TEXT, p_owner_subject TEXT, p_created_by TEXT
571
+ ) RETURNS VOID AS $$
572
+ BEGIN
573
+ INSERT INTO ${s}.agent_sources
574
+ (source_id, kind, scope, repo_url, ref, path, url, auth_token, auto_sync,
575
+ owner_provider, owner_subject, created_by)
576
+ VALUES (p_source_id, p_kind, COALESCE(NULLIF(p_scope, ''), 'user'), p_repo_url, p_ref, p_path,
577
+ NULLIF(p_url, ''), NULLIF(p_auth_token, ''), COALESCE(p_auto_sync, FALSE),
578
+ NULLIF(BTRIM(p_owner_provider), ''), NULLIF(BTRIM(p_owner_subject), ''), p_created_by);
579
+ END;
580
+ $$ LANGUAGE plpgsql;
581
+
582
+ -- Listing never returns auth_token — only whether one is set.
583
+ CREATE OR REPLACE FUNCTION ${s}.cms_list_agent_sources(
584
+ p_viewer_provider TEXT, p_viewer_subject TEXT, p_is_admin BOOLEAN
585
+ ) RETURNS TABLE(
586
+ source_id TEXT, kind TEXT, scope TEXT, repo_url TEXT, ref TEXT, path TEXT, url TEXT,
587
+ auth_token_set BOOLEAN, auto_sync BOOLEAN, last_sync_at TIMESTAMPTZ,
588
+ last_sync_status TEXT, last_sync_error TEXT, last_commit_sha TEXT,
589
+ owner_provider TEXT, owner_subject TEXT, created_by TEXT, created_at TIMESTAMPTZ
590
+ ) AS $$
591
+ SELECT source_id, kind, scope, repo_url, ref, path, url,
592
+ (auth_token IS NOT NULL), auto_sync, last_sync_at,
593
+ last_sync_status, last_sync_error, last_commit_sha,
594
+ owner_provider, owner_subject, created_by, created_at
595
+ FROM ${s}.agent_sources s
596
+ WHERE p_is_admin
597
+ OR (s.owner_provider = BTRIM(p_viewer_provider) AND s.owner_subject = BTRIM(p_viewer_subject))
598
+ ORDER BY created_at DESC;
599
+ $$ LANGUAGE sql;
600
+
601
+ -- Internal-only raw token read for the sync fetchers. Never expose through
602
+ -- the public management API (github_copilot_key precedent, migration 0010).
603
+ CREATE OR REPLACE FUNCTION ${s}.cms_get_agent_source_token(p_source_id TEXT)
604
+ RETURNS TEXT AS $$
605
+ SELECT auth_token FROM ${s}.agent_sources WHERE source_id = p_source_id;
606
+ $$ LANGUAGE sql;
607
+
608
+ CREATE OR REPLACE FUNCTION ${s}.cms_get_agent_source(p_source_id TEXT)
609
+ RETURNS TABLE(
610
+ source_id TEXT, kind TEXT, scope TEXT, repo_url TEXT, ref TEXT, path TEXT, url TEXT,
611
+ auth_token_set BOOLEAN, auto_sync BOOLEAN, last_sync_at TIMESTAMPTZ,
612
+ last_sync_status TEXT, last_sync_error TEXT, last_commit_sha TEXT,
613
+ owner_provider TEXT, owner_subject TEXT, created_by TEXT, created_at TIMESTAMPTZ
614
+ ) AS $$
615
+ SELECT source_id, kind, scope, repo_url, ref, path, url,
616
+ (auth_token IS NOT NULL), auto_sync, last_sync_at,
617
+ last_sync_status, last_sync_error, last_commit_sha,
618
+ owner_provider, owner_subject, created_by, created_at
619
+ FROM ${s}.agent_sources
620
+ WHERE source_id = p_source_id;
621
+ $$ LANGUAGE sql;
622
+
623
+ CREATE OR REPLACE FUNCTION ${s}.cms_update_agent_source_sync(
624
+ p_source_id TEXT, p_status TEXT, p_error TEXT, p_commit_sha TEXT
625
+ ) RETURNS VOID AS $$
626
+ BEGIN
627
+ UPDATE ${s}.agent_sources
628
+ SET last_sync_at = now(),
629
+ last_sync_status = p_status,
630
+ last_sync_error = p_error,
631
+ last_commit_sha = COALESCE(p_commit_sha, last_commit_sha)
632
+ WHERE source_id = p_source_id;
633
+ END;
634
+ $$ LANGUAGE plpgsql;
635
+
636
+ CREATE OR REPLACE FUNCTION ${s}.cms_delete_agent_source(
637
+ p_source_id TEXT, p_actor_provider TEXT, p_actor_subject TEXT, p_is_admin BOOLEAN
638
+ ) RETURNS VOID AS $$
639
+ DECLARE
640
+ v_src RECORD;
641
+ BEGIN
642
+ SELECT * INTO v_src FROM ${s}.agent_sources WHERE source_id = p_source_id;
643
+ IF NOT FOUND THEN
644
+ RAISE EXCEPTION 'AGENT_SOURCE_NOT_FOUND: source % does not exist', p_source_id;
645
+ END IF;
646
+ IF NOT p_is_admin AND (
647
+ v_src.owner_provider IS NULL
648
+ OR v_src.owner_provider IS DISTINCT FROM NULLIF(BTRIM(p_actor_provider), '')
649
+ OR v_src.owner_subject IS DISTINCT FROM NULLIF(BTRIM(p_actor_subject), '')
650
+ ) THEN
651
+ RAISE EXCEPTION 'AGENT_PACKAGE_FORBIDDEN: only the source creator or an admin can delete it';
652
+ END IF;
653
+ DELETE FROM ${s}.agent_sources WHERE source_id = p_source_id;
654
+ END;
655
+ $$ LANGUAGE plpgsql;
656
+
657
+ -- ── publish (the atomic heart) ───────────────────────────────────
658
+ --
659
+ -- Row-locks the package by name, enforces creator-or-admin, treats an
660
+ -- identical (semver, sha256) republish as a no-op, rejects same-semver
661
+ -- different-content outright (bump the version — no replace), inserts the
662
+ -- version, activates it, and bumps the registry epoch. status is one of
663
+ -- 'published' | 'noop'.
664
+
665
+ CREATE OR REPLACE FUNCTION ${s}.cms_publish_agent_package(
666
+ p_package_id TEXT, p_version_id TEXT, p_name TEXT, p_scope TEXT,
667
+ p_owner_provider TEXT, p_owner_subject TEXT, p_source_id TEXT,
668
+ p_semver TEXT, p_sha256 TEXT, p_size_bytes BIGINT, p_artifact_filename TEXT,
669
+ p_commit_sha TEXT, p_manifest JSONB, p_created_by TEXT, p_is_admin BOOLEAN
670
+ ) RETURNS TABLE(status TEXT, package_id TEXT, version_id TEXT) AS $$
671
+ DECLARE
672
+ v_pkg RECORD;
673
+ v_ver RECORD;
674
+ v_owner_provider TEXT := NULLIF(BTRIM(p_owner_provider), '');
675
+ v_owner_subject TEXT := NULLIF(BTRIM(p_owner_subject), '');
676
+ BEGIN
677
+ -- An owner-less publish is admin-only: a NULL-owner package would be
678
+ -- unmanageable by its (anonymous) creator afterwards, so reject the
679
+ -- combination up front instead of minting an orphan.
680
+ IF NOT p_is_admin AND (v_owner_provider IS NULL OR v_owner_subject IS NULL) THEN
681
+ RAISE EXCEPTION 'AGENT_PACKAGE_FORBIDDEN: publishing without an owner identity requires the admin role';
682
+ END IF;
683
+
684
+ <<retry>>
685
+ LOOP
686
+ SELECT * INTO v_pkg FROM ${s}.agent_packages p WHERE p.name = p_name FOR UPDATE;
687
+ IF NOT FOUND THEN
688
+ -- First publish. FOR UPDATE on a missing row takes no lock, so a
689
+ -- concurrent first publish can beat this INSERT — catch the
690
+ -- unique violation and loop back to lock the winner's row.
691
+ BEGIN
692
+ INSERT INTO ${s}.agent_packages
693
+ (package_id, source_id, name, scope, owner_provider, owner_subject, created_by)
694
+ VALUES (p_package_id, p_source_id, p_name, p_scope,
695
+ v_owner_provider, v_owner_subject, p_created_by);
696
+ EXCEPTION WHEN unique_violation THEN
697
+ CONTINUE retry;
698
+ END;
699
+ INSERT INTO ${s}.agent_package_versions
700
+ (version_id, package_id, semver, sha256, size_bytes, artifact_filename, commit_sha, manifest, created_by)
701
+ VALUES (p_version_id, p_package_id, p_semver, p_sha256, p_size_bytes,
702
+ p_artifact_filename, p_commit_sha, p_manifest, p_created_by);
703
+ UPDATE ${s}.agent_packages SET active_version_id = p_version_id WHERE ${s}.agent_packages.package_id = p_package_id;
704
+ PERFORM ${s}.cms_agent_registry_bump();
705
+ RETURN QUERY SELECT 'published'::TEXT, p_package_id, p_version_id;
706
+ RETURN;
707
+ END IF;
708
+ EXIT retry;
709
+ END LOOP;
710
+
711
+ -- A NULL-owner package (no-auth or system provenance) is admin-managed:
712
+ -- publish must not be the one mutation left open to null principals.
713
+ IF NOT p_is_admin AND (
714
+ v_pkg.owner_provider IS NULL
715
+ OR v_pkg.owner_provider IS DISTINCT FROM v_owner_provider
716
+ OR v_pkg.owner_subject IS DISTINCT FROM v_owner_subject
717
+ ) THEN
718
+ RAISE EXCEPTION 'AGENT_PACKAGE_FORBIDDEN: only the package creator or an admin can publish new versions of "%"', p_name;
719
+ END IF;
720
+
721
+ IF p_scope IS DISTINCT FROM v_pkg.scope THEN
722
+ RAISE EXCEPTION 'AGENT_PACKAGE_SCOPE_MISMATCH: "%" is scope %, not %; change scope with promote/demote, not publish', p_name, v_pkg.scope, p_scope;
723
+ END IF;
724
+
725
+ SELECT * INTO v_ver FROM ${s}.agent_package_versions v
726
+ WHERE v.package_id = v_pkg.package_id AND v.semver = p_semver;
727
+ IF FOUND THEN
728
+ IF v_ver.sha256 = p_sha256 THEN
729
+ RETURN QUERY SELECT 'noop'::TEXT, v_pkg.package_id, v_ver.version_id;
730
+ RETURN;
731
+ END IF;
732
+ RAISE EXCEPTION 'AGENT_PACKAGE_SEMVER_CONFLICT: %@% is already published with different content — published versions are immutable, bump the version', p_name, p_semver;
733
+ END IF;
734
+
735
+ INSERT INTO ${s}.agent_package_versions
736
+ (version_id, package_id, semver, sha256, size_bytes, artifact_filename, commit_sha, manifest, created_by)
737
+ VALUES (p_version_id, v_pkg.package_id, p_semver, p_sha256, p_size_bytes,
738
+ p_artifact_filename, p_commit_sha, p_manifest, p_created_by);
739
+ UPDATE ${s}.agent_packages
740
+ SET active_version_id = p_version_id,
741
+ source_id = COALESCE(p_source_id, ${s}.agent_packages.source_id)
742
+ WHERE ${s}.agent_packages.package_id = v_pkg.package_id;
743
+ PERFORM ${s}.cms_agent_registry_bump();
744
+ RETURN QUERY SELECT 'published'::TEXT, v_pkg.package_id, p_version_id;
745
+ END;
746
+ $$ LANGUAGE plpgsql;
747
+
748
+ -- ── package reads ────────────────────────────────────────────────
749
+ --
750
+ -- Visibility: shared packages are visible to everyone; user packages only to
751
+ -- their owner (or an admin). This filter is THE user-scope privacy boundary —
752
+ -- every listing path goes through one of these two procs.
753
+
754
+ CREATE OR REPLACE FUNCTION ${s}.cms_list_agent_packages(
755
+ p_viewer_provider TEXT, p_viewer_subject TEXT, p_is_admin BOOLEAN
756
+ ) RETURNS TABLE(
757
+ package_id TEXT, source_id TEXT, name TEXT, scope TEXT,
758
+ owner_provider TEXT, owner_subject TEXT,
759
+ enabled BOOLEAN, created_by TEXT, created_at TIMESTAMPTZ,
760
+ active_version_id TEXT, semver TEXT, sha256 TEXT, size_bytes BIGINT,
761
+ artifact_filename TEXT, commit_sha TEXT, manifest JSONB,
762
+ version_created_at TIMESTAMPTZ, version_created_by TEXT
763
+ ) AS $$
764
+ SELECT p.package_id, p.source_id, p.name, p.scope,
765
+ p.owner_provider, p.owner_subject,
766
+ p.enabled, p.created_by, p.created_at,
767
+ v.version_id, v.semver, v.sha256, v.size_bytes,
768
+ v.artifact_filename, v.commit_sha, v.manifest, v.created_at, v.created_by
769
+ FROM ${s}.agent_packages p
770
+ LEFT JOIN ${s}.agent_package_versions v ON v.version_id = p.active_version_id
771
+ WHERE p.scope = 'shared'
772
+ OR p_is_admin
773
+ OR (p.owner_provider = BTRIM(p_viewer_provider) AND p.owner_subject = BTRIM(p_viewer_subject))
774
+ ORDER BY p.scope, p.name;
775
+ $$ LANGUAGE sql;
776
+
777
+ CREATE OR REPLACE FUNCTION ${s}.cms_get_agent_package(
778
+ p_name TEXT, p_viewer_provider TEXT, p_viewer_subject TEXT, p_is_admin BOOLEAN
779
+ ) RETURNS TABLE(
780
+ package_id TEXT, source_id TEXT, name TEXT, scope TEXT,
781
+ owner_provider TEXT, owner_subject TEXT,
782
+ enabled BOOLEAN, created_by TEXT, created_at TIMESTAMPTZ,
783
+ active_version_id TEXT, version_id TEXT, semver TEXT, sha256 TEXT,
784
+ size_bytes BIGINT, artifact_filename TEXT, commit_sha TEXT, manifest JSONB,
785
+ version_created_at TIMESTAMPTZ, version_created_by TEXT
786
+ ) AS $$
787
+ SELECT p.package_id, p.source_id, p.name, p.scope,
788
+ p.owner_provider, p.owner_subject,
789
+ p.enabled, p.created_by, p.created_at, p.active_version_id,
790
+ v.version_id, v.semver, v.sha256, v.size_bytes,
791
+ v.artifact_filename, v.commit_sha, v.manifest, v.created_at, v.created_by
792
+ FROM ${s}.agent_packages p
793
+ LEFT JOIN ${s}.agent_package_versions v ON v.package_id = p.package_id
794
+ WHERE p.name = p_name
795
+ AND (p.scope = 'shared' OR p_is_admin
796
+ OR (p.owner_provider = BTRIM(p_viewer_provider) AND p.owner_subject = BTRIM(p_viewer_subject)))
797
+ ORDER BY v.created_at DESC;
798
+ $$ LANGUAGE sql;
799
+
800
+ -- Worker-facing install manifest: every enabled package's active version.
801
+ -- Workers are trusted infrastructure — no viewer filter here by design.
802
+ CREATE OR REPLACE FUNCTION ${s}.cms_get_agent_packages_install_manifest()
803
+ RETURNS TABLE(
804
+ name TEXT, scope TEXT, owner_provider TEXT, owner_subject TEXT,
805
+ semver TEXT, sha256 TEXT, size_bytes BIGINT, artifact_filename TEXT, manifest JSONB
806
+ ) AS $$
807
+ SELECT p.name, p.scope, p.owner_provider, p.owner_subject, v.semver, v.sha256,
808
+ v.size_bytes, v.artifact_filename, v.manifest
809
+ FROM ${s}.agent_packages p
810
+ JOIN ${s}.agent_package_versions v ON v.version_id = p.active_version_id
811
+ WHERE p.enabled
812
+ ORDER BY p.name;
813
+ $$ LANGUAGE sql;
814
+
815
+ -- ── package mutations (all creator-or-admin, all epoch-bumping) ──
816
+
817
+ CREATE OR REPLACE FUNCTION ${s}.cms_agent_package_authz(
818
+ p_name TEXT, p_actor_provider TEXT, p_actor_subject TEXT, p_is_admin BOOLEAN
819
+ ) RETURNS ${s}.agent_packages AS $$
820
+ DECLARE
821
+ v_pkg ${s}.agent_packages;
822
+ BEGIN
823
+ SELECT * INTO v_pkg FROM ${s}.agent_packages WHERE name = p_name FOR UPDATE;
824
+ IF NOT FOUND THEN
825
+ RAISE EXCEPTION 'AGENT_PACKAGE_NOT_FOUND: package "%" does not exist', p_name;
826
+ END IF;
827
+ IF NOT p_is_admin AND (
828
+ v_pkg.owner_provider IS NULL
829
+ OR v_pkg.owner_provider IS DISTINCT FROM NULLIF(BTRIM(p_actor_provider), '')
830
+ OR v_pkg.owner_subject IS DISTINCT FROM NULLIF(BTRIM(p_actor_subject), '')
831
+ ) THEN
832
+ RAISE EXCEPTION 'AGENT_PACKAGE_FORBIDDEN: only the package creator or an admin can modify "%"', p_name;
833
+ END IF;
834
+ RETURN v_pkg;
835
+ END;
836
+ $$ LANGUAGE plpgsql;
837
+
838
+ CREATE OR REPLACE FUNCTION ${s}.cms_set_agent_package_scope(
839
+ p_name TEXT, p_scope TEXT, p_actor_provider TEXT, p_actor_subject TEXT, p_is_admin BOOLEAN
840
+ ) RETURNS VOID AS $$
841
+ DECLARE
842
+ v_pkg ${s}.agent_packages;
843
+ BEGIN
844
+ v_pkg := ${s}.cms_agent_package_authz(p_name, p_actor_provider, p_actor_subject, p_is_admin);
845
+ IF p_scope NOT IN ('shared', 'user') THEN
846
+ RAISE EXCEPTION 'AGENT_PACKAGE_BAD_SCOPE: scope must be shared or user, got "%"', p_scope;
847
+ END IF;
848
+ UPDATE ${s}.agent_packages SET scope = p_scope WHERE package_id = v_pkg.package_id;
849
+ PERFORM ${s}.cms_agent_registry_bump();
850
+ END;
851
+ $$ LANGUAGE plpgsql;
852
+
853
+ CREATE OR REPLACE FUNCTION ${s}.cms_set_agent_package_enabled(
854
+ p_name TEXT, p_enabled BOOLEAN, p_actor_provider TEXT, p_actor_subject TEXT, p_is_admin BOOLEAN
855
+ ) RETURNS VOID AS $$
856
+ DECLARE
857
+ v_pkg ${s}.agent_packages;
858
+ BEGIN
859
+ v_pkg := ${s}.cms_agent_package_authz(p_name, p_actor_provider, p_actor_subject, p_is_admin);
860
+ UPDATE ${s}.agent_packages SET enabled = p_enabled WHERE package_id = v_pkg.package_id;
861
+ PERFORM ${s}.cms_agent_registry_bump();
862
+ END;
863
+ $$ LANGUAGE plpgsql;
864
+
865
+ CREATE OR REPLACE FUNCTION ${s}.cms_pin_agent_package_version(
866
+ p_name TEXT, p_semver TEXT, p_actor_provider TEXT, p_actor_subject TEXT, p_is_admin BOOLEAN
867
+ ) RETURNS VOID AS $$
868
+ DECLARE
869
+ v_pkg ${s}.agent_packages;
870
+ v_version_id TEXT;
871
+ BEGIN
872
+ v_pkg := ${s}.cms_agent_package_authz(p_name, p_actor_provider, p_actor_subject, p_is_admin);
873
+ SELECT version_id INTO v_version_id FROM ${s}.agent_package_versions
874
+ WHERE package_id = v_pkg.package_id AND semver = p_semver;
875
+ IF NOT FOUND THEN
876
+ RAISE EXCEPTION 'AGENT_PACKAGE_VERSION_NOT_FOUND: %@% is not a published version', p_name, p_semver;
877
+ END IF;
878
+ UPDATE ${s}.agent_packages SET active_version_id = v_version_id WHERE package_id = v_pkg.package_id;
879
+ PERFORM ${s}.cms_agent_registry_bump();
880
+ END;
881
+ $$ LANGUAGE plpgsql;
882
+
883
+ -- Returns the artifact filenames of every deleted version so the caller can
884
+ -- clean up the artifact store after the transaction commits.
885
+ CREATE OR REPLACE FUNCTION ${s}.cms_delete_agent_package(
886
+ p_name TEXT, p_actor_provider TEXT, p_actor_subject TEXT, p_is_admin BOOLEAN
887
+ ) RETURNS TABLE(artifact_filename TEXT) AS $$
888
+ DECLARE
889
+ v_pkg ${s}.agent_packages;
890
+ BEGIN
891
+ v_pkg := ${s}.cms_agent_package_authz(p_name, p_actor_provider, p_actor_subject, p_is_admin);
892
+ RETURN QUERY
893
+ SELECT v.artifact_filename FROM ${s}.agent_package_versions v
894
+ WHERE v.package_id = v_pkg.package_id;
895
+ DELETE FROM ${s}.agent_packages WHERE package_id = v_pkg.package_id;
896
+ PERFORM ${s}.cms_agent_registry_bump();
897
+ END;
898
+ $$ LANGUAGE plpgsql;
899
+
900
+ -- ── worker fleet state ───────────────────────────────────────────
901
+
902
+ CREATE OR REPLACE FUNCTION ${s}.cms_upsert_agent_worker_state(
903
+ p_worker_node_id TEXT, p_epoch BIGINT, p_installed JSONB
904
+ ) RETURNS VOID AS $$
905
+ BEGIN
906
+ INSERT INTO ${s}.agent_worker_state (worker_node_id, epoch, installed, updated_at)
907
+ VALUES (p_worker_node_id, p_epoch, COALESCE(p_installed, '{}'::jsonb), now())
908
+ ON CONFLICT (worker_node_id) DO UPDATE
909
+ SET epoch = EXCLUDED.epoch,
910
+ installed = EXCLUDED.installed,
911
+ updated_at = now();
912
+ END;
913
+ $$ LANGUAGE plpgsql;
914
+
915
+ CREATE OR REPLACE FUNCTION ${s}.cms_list_agent_worker_state()
916
+ RETURNS TABLE(worker_node_id TEXT, epoch BIGINT, installed JSONB, updated_at TIMESTAMPTZ) AS $$
917
+ SELECT worker_node_id, epoch, installed, updated_at
918
+ FROM ${s}.agent_worker_state
919
+ ORDER BY worker_node_id;
920
+ $$ LANGUAGE sql;
921
+ `;
922
+ }
202
923
  // ─── Migration 0037: service sessions (tree-scoped system sessions) ─────
203
924
  //
204
925
  // A service session is machinery that serves ONE session tree (first kind:
@@ -7397,4 +8118,82 @@ $$ LANGUAGE plpgsql;
7397
8118
  `;
7398
8119
  return [step_columns, step_backfill, step_drop_invalid_index, step_index, step_tables_and_functions];
7399
8120
  }
8121
+ // ─── Migration 0041: agent-package owner identity ────────────────
8122
+ //
8123
+ // An agent package stored only its owner PRINCIPAL (provider + subject —
8124
+ // an opaque directory id) plus a created_by email, so the UI had no human
8125
+ // name to render and fell back to the email's local part: "daraffan@…"
8126
+ // became "DA" where the same person's sessions correctly showed "AD" for
8127
+ // "Affan Dar".
8128
+ //
8129
+ // The identity was never missing — it lives in the users table, which the
8130
+ // session view has always joined (u.display_name AS owner_display_name).
8131
+ // These procs simply never did. Joining it here means packages and
8132
+ // sessions resolve the same person the same way, for every row that
8133
+ // already exists, with no new columns and nothing to backfill.
8134
+ function migration_0041_agent_package_owner_identity(schema) {
8135
+ const s = schema;
8136
+ return `
8137
+ -- Adding OUT columns CHANGES the return type, and CREATE OR REPLACE cannot
8138
+ -- do that ("cannot change return type of existing function"). Both functions
8139
+ -- must be dropped first. The migrator runs this in a transaction, so callers
8140
+ -- never observe the gap.
8141
+ DROP FUNCTION IF EXISTS ${s}.cms_list_agent_packages(TEXT, TEXT, BOOLEAN);
8142
+ DROP FUNCTION IF EXISTS ${s}.cms_get_agent_package(TEXT, TEXT, TEXT, BOOLEAN);
8143
+
8144
+ CREATE OR REPLACE FUNCTION ${s}.cms_list_agent_packages(
8145
+ p_viewer_provider TEXT, p_viewer_subject TEXT, p_is_admin BOOLEAN
8146
+ ) RETURNS TABLE(
8147
+ package_id TEXT, source_id TEXT, name TEXT, scope TEXT,
8148
+ owner_provider TEXT, owner_subject TEXT,
8149
+ owner_email TEXT, owner_display_name TEXT,
8150
+ enabled BOOLEAN, created_by TEXT, created_at TIMESTAMPTZ,
8151
+ active_version_id TEXT, semver TEXT, sha256 TEXT, size_bytes BIGINT,
8152
+ artifact_filename TEXT, commit_sha TEXT, manifest JSONB,
8153
+ version_created_at TIMESTAMPTZ, version_created_by TEXT
8154
+ ) AS $$
8155
+ SELECT p.package_id, p.source_id, p.name, p.scope,
8156
+ p.owner_provider, p.owner_subject,
8157
+ u.email, u.display_name,
8158
+ p.enabled, p.created_by, p.created_at,
8159
+ v.version_id, v.semver, v.sha256, v.size_bytes,
8160
+ v.artifact_filename, v.commit_sha, v.manifest, v.created_at, v.created_by
8161
+ FROM ${s}.agent_packages p
8162
+ LEFT JOIN ${s}.agent_package_versions v ON v.version_id = p.active_version_id
8163
+ LEFT JOIN ${s}.users u
8164
+ ON u.provider = p.owner_provider AND u.subject = p.owner_subject
8165
+ WHERE p.scope = 'shared'
8166
+ OR p_is_admin
8167
+ OR (p.owner_provider = BTRIM(p_viewer_provider) AND p.owner_subject = BTRIM(p_viewer_subject))
8168
+ ORDER BY p.scope, p.name;
8169
+ $$ LANGUAGE sql;
8170
+
8171
+ CREATE OR REPLACE FUNCTION ${s}.cms_get_agent_package(
8172
+ p_name TEXT, p_viewer_provider TEXT, p_viewer_subject TEXT, p_is_admin BOOLEAN
8173
+ ) RETURNS TABLE(
8174
+ package_id TEXT, source_id TEXT, name TEXT, scope TEXT,
8175
+ owner_provider TEXT, owner_subject TEXT,
8176
+ owner_email TEXT, owner_display_name TEXT,
8177
+ enabled BOOLEAN, created_by TEXT, created_at TIMESTAMPTZ,
8178
+ active_version_id TEXT, version_id TEXT, semver TEXT, sha256 TEXT,
8179
+ size_bytes BIGINT, artifact_filename TEXT, commit_sha TEXT, manifest JSONB,
8180
+ version_created_at TIMESTAMPTZ, version_created_by TEXT
8181
+ ) AS $$
8182
+ SELECT p.package_id, p.source_id, p.name, p.scope,
8183
+ p.owner_provider, p.owner_subject,
8184
+ u.email, u.display_name,
8185
+ p.enabled, p.created_by, p.created_at, p.active_version_id,
8186
+ v.version_id, v.semver, v.sha256, v.size_bytes,
8187
+ v.artifact_filename, v.commit_sha, v.manifest, v.created_at, v.created_by
8188
+ FROM ${s}.agent_packages p
8189
+ LEFT JOIN ${s}.agent_package_versions v ON v.package_id = p.package_id
8190
+ LEFT JOIN ${s}.users u
8191
+ ON u.provider = p.owner_provider AND u.subject = p.owner_subject
8192
+ WHERE p.name = p_name
8193
+ AND (p.scope = 'shared' OR p_is_admin
8194
+ OR (p.owner_provider = BTRIM(p_viewer_provider) AND p.owner_subject = BTRIM(p_viewer_subject)))
8195
+ ORDER BY v.created_at DESC;
8196
+ $$ LANGUAGE sql;
8197
+ `;
8198
+ }
7400
8199
  //# sourceMappingURL=cms-migrations.js.map