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.
- package/api/src/api-client.js +10 -1
- package/api/src/http-api-transport.js +22 -2
- package/api/src/protocol.js +25 -0
- package/dist/agent-loader.d.ts +8 -0
- package/dist/agent-loader.d.ts.map +1 -1
- package/dist/agent-loader.js +23 -0
- package/dist/agent-loader.js.map +1 -1
- package/dist/agent-package-format.d.ts +175 -0
- package/dist/agent-package-format.d.ts.map +1 -0
- package/dist/agent-package-format.js +812 -0
- package/dist/agent-package-format.js.map +1 -0
- package/dist/agent-package-installer.d.ts +64 -0
- package/dist/agent-package-installer.d.ts.map +1 -0
- package/dist/agent-package-installer.js +148 -0
- package/dist/agent-package-installer.js.map +1 -0
- package/dist/agent-package-service.d.ts +61 -0
- package/dist/agent-package-service.d.ts.map +1 -0
- package/dist/agent-package-service.js +181 -0
- package/dist/agent-package-service.js.map +1 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +28 -0
- package/dist/client.js.map +1 -1
- package/dist/cms-migrations.d.ts.map +1 -1
- package/dist/cms-migrations.js +799 -0
- package/dist/cms-migrations.js.map +1 -1
- package/dist/cms.d.ts +224 -0
- package/dist/cms.d.ts.map +1 -1
- package/dist/cms.js +262 -0
- package/dist/cms.js.map +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -2
- package/dist/index.js.map +1 -1
- package/dist/management-client.d.ts +15 -0
- package/dist/management-client.d.ts.map +1 -1
- package/dist/management-client.js +38 -3
- package/dist/management-client.js.map +1 -1
- package/dist/mcp-loader.d.ts.map +1 -1
- package/dist/mcp-loader.js +17 -1
- package/dist/mcp-loader.js.map +1 -1
- package/dist/session-manager.d.ts +2 -0
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +11 -1
- package/dist/session-manager.js.map +1 -1
- package/dist/tool-pinning.d.ts +27 -0
- package/dist/tool-pinning.d.ts.map +1 -0
- package/dist/tool-pinning.js +31 -0
- package/dist/tool-pinning.js.map +1 -0
- package/dist/types.d.ts +27 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/web/api-connection.d.ts +8 -0
- package/dist/web/api-connection.d.ts.map +1 -1
- package/dist/web/api-connection.js +2 -0
- package/dist/web/api-connection.js.map +1 -1
- package/dist/web/generated-op-methods.d.ts +917 -0
- package/dist/web/generated-op-methods.d.ts.map +1 -0
- package/dist/web/generated-op-methods.js +255 -0
- package/dist/web/generated-op-methods.js.map +1 -0
- package/dist/web/web-management-client.d.ts +191 -8
- package/dist/web/web-management-client.d.ts.map +1 -1
- package/dist/web/web-management-client.js +164 -12
- package/dist/web/web-management-client.js.map +1 -1
- package/dist/worker.d.ts +69 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +319 -11
- package/dist/worker.js.map +1 -1
- package/package.json +7 -6
package/dist/cms-migrations.js
CHANGED
|
@@ -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
|