pilotswarm-sdk 0.5.30 → 0.5.32

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 (81) hide show
  1. package/api/index.d.ts +40 -0
  2. package/api/index.js +8 -0
  3. package/api/src/session-authz.d.ts +76 -0
  4. package/api/src/session-authz.js +240 -0
  5. package/dist/agent-fqn.d.ts +72 -0
  6. package/dist/agent-fqn.d.ts.map +1 -0
  7. package/dist/agent-fqn.js +137 -0
  8. package/dist/agent-fqn.js.map +1 -0
  9. package/dist/agent-loader.d.ts.map +1 -1
  10. package/dist/agent-loader.js +22 -6
  11. package/dist/agent-loader.js.map +1 -1
  12. package/dist/agent-manager-tools.d.ts +135 -0
  13. package/dist/agent-manager-tools.d.ts.map +1 -0
  14. package/dist/agent-manager-tools.js +813 -0
  15. package/dist/agent-manager-tools.js.map +1 -0
  16. package/dist/agent-package-diff.d.ts +97 -0
  17. package/dist/agent-package-diff.d.ts.map +1 -0
  18. package/dist/agent-package-diff.js +203 -0
  19. package/dist/agent-package-diff.js.map +1 -0
  20. package/dist/agent-package-format.d.ts +2 -0
  21. package/dist/agent-package-format.d.ts.map +1 -1
  22. package/dist/agent-package-format.js +15 -0
  23. package/dist/agent-package-format.js.map +1 -1
  24. package/dist/agent-package-import-fetch.d.ts +68 -0
  25. package/dist/agent-package-import-fetch.d.ts.map +1 -0
  26. package/dist/agent-package-import-fetch.js +156 -0
  27. package/dist/agent-package-import-fetch.js.map +1 -0
  28. package/dist/agent-package-import-policy.d.ts +133 -0
  29. package/dist/agent-package-import-policy.d.ts.map +1 -0
  30. package/dist/agent-package-import-policy.js +347 -0
  31. package/dist/agent-package-import-policy.js.map +1 -0
  32. package/dist/agent-package-installer.d.ts.map +1 -1
  33. package/dist/agent-package-installer.js +30 -4
  34. package/dist/agent-package-installer.js.map +1 -1
  35. package/dist/cms-migrations.d.ts.map +1 -1
  36. package/dist/cms-migrations.js +653 -1
  37. package/dist/cms-migrations.js.map +1 -1
  38. package/dist/cms.d.ts +103 -10
  39. package/dist/cms.d.ts.map +1 -1
  40. package/dist/cms.js +85 -10
  41. package/dist/cms.js.map +1 -1
  42. package/dist/index.d.ts +2 -1
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +1 -0
  45. package/dist/index.js.map +1 -1
  46. package/dist/inspect-tools.d.ts +68 -8
  47. package/dist/inspect-tools.d.ts.map +1 -1
  48. package/dist/inspect-tools.js +282 -21
  49. package/dist/inspect-tools.js.map +1 -1
  50. package/dist/managed-session.d.ts.map +1 -1
  51. package/dist/managed-session.js +87 -0
  52. package/dist/managed-session.js.map +1 -1
  53. package/dist/management-client.d.ts +20 -1
  54. package/dist/management-client.d.ts.map +1 -1
  55. package/dist/management-client.js +25 -0
  56. package/dist/management-client.js.map +1 -1
  57. package/dist/session-manager.d.ts +61 -0
  58. package/dist/session-manager.d.ts.map +1 -1
  59. package/dist/session-manager.js +143 -1
  60. package/dist/session-manager.js.map +1 -1
  61. package/dist/session-proxy.d.ts +9 -1
  62. package/dist/session-proxy.d.ts.map +1 -1
  63. package/dist/session-proxy.js +345 -12
  64. package/dist/session-proxy.js.map +1 -1
  65. package/dist/types.d.ts +31 -0
  66. package/dist/types.d.ts.map +1 -1
  67. package/dist/types.js.map +1 -1
  68. package/dist/web/web-management-client.d.ts +2 -0
  69. package/dist/web/web-management-client.d.ts.map +1 -1
  70. package/dist/web/web-management-client.js +6 -0
  71. package/dist/web/web-management-client.js.map +1 -1
  72. package/dist/worker.d.ts +5 -0
  73. package/dist/worker.d.ts.map +1 -1
  74. package/dist/worker.js +28 -0
  75. package/dist/worker.js.map +1 -1
  76. package/package.json +3 -3
  77. package/plugins/mgmt/agents/pilotswarm.agent.md +2 -2
  78. package/plugins/mgmt/skills/cost-latency-analysis/SKILL.md +1 -1
  79. package/plugins/mgmt/skills/graph-debug/SKILL.md +4 -4
  80. package/plugins/mgmt/skills/orchestration-session-lifecycle/SKILL.md +1 -1
  81. package/plugins/mgmt/agents/agent-tuner.agent.md +0 -316
@@ -217,6 +217,16 @@ export function CMS_MIGRATIONS(schema) {
217
217
  name: "agent_package_owner_identity",
218
218
  sql: migration_0041_agent_package_owner_identity(schema),
219
219
  },
220
+ {
221
+ version: "0042",
222
+ name: "user_role_from_signin",
223
+ sql: migration_0042_user_role_from_signin(schema),
224
+ },
225
+ {
226
+ version: "0043",
227
+ name: "agent_package_namespaces",
228
+ sql: migration_0043_agent_package_namespaces(schema),
229
+ },
220
230
  ];
221
231
  }
222
232
  // ─── Migration 0040: worker registry ─────────────────────────────
@@ -8122,7 +8132,7 @@ $$ LANGUAGE plpgsql;
8122
8132
  //
8123
8133
  // An agent package stored only its owner PRINCIPAL (provider + subject —
8124
8134
  // 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@…"
8135
+ // name to render and fell back to the email's local part: "ada@…"
8126
8136
  // became "DA" where the same person's sessions correctly showed "AD" for
8127
8137
  // "Affan Dar".
8128
8138
  //
@@ -8196,4 +8206,646 @@ CREATE OR REPLACE FUNCTION ${s}.cms_get_agent_package(
8196
8206
  $$ LANGUAGE sql;
8197
8207
  `;
8198
8208
  }
8209
+ // ─── Migration 0042: persist the authorization role at sign-in ───
8210
+ //
8211
+ // The worker had no way to know whether a session's owner is an admin.
8212
+ // The portal resolves the role from the request JWT (Entra app roles, or the
8213
+ // email allowlist) and never persisted it, so a worker — which holds an
8214
+ // owner, not a token, and runs turns on the far side of a durable queue where
8215
+ // no request exists at all (cron, sub-agent turns, crash recovery, replay) —
8216
+ // had nothing to read. Agent inspect tools were therefore user-scoped for
8217
+ // everyone, admins included.
8218
+ //
8219
+ // This stores the LAST OBSERVED role for a principal, refreshed on every
8220
+ // authenticated portal request. Two properties make that safe to rely on:
8221
+ //
8222
+ // - `role` is OVERWRITTEN, never COALESCE'd. A demotion (admin → user) must
8223
+ // land, and a COALESCE-style merge like cms_register_user's display-field
8224
+ // rule would silently preserve the higher privilege forever. This is why
8225
+ // the role does NOT ride along on cms_register_user: that function is
8226
+ // called by sightings which carry no role at all (share grants, session
8227
+ // creates), and any of them would either wipe a good role or force the
8228
+ // merge semantics we must not have here.
8229
+ //
8230
+ // - `role_seen_at` records when the value was last CONFIRMED, not when it
8231
+ // last changed, so readers can fail closed on a stale row. It is bumped
8232
+ // on every write even when the role is unchanged.
8233
+ //
8234
+ // Unrecognized role text normalizes to NULL rather than being stored verbatim:
8235
+ // an unknown role must read as "no privilege", never as an opaque value some
8236
+ // later caller might compare loosely.
8237
+ function migration_0042_user_role_from_signin(schema) {
8238
+ const s = `"${schema}"`;
8239
+ return `
8240
+ -- 0042_user_role_from_signin: last-observed authorization role per user.
8241
+
8242
+ ALTER TABLE ${s}.users ADD COLUMN IF NOT EXISTS role TEXT;
8243
+ ALTER TABLE ${s}.users ADD COLUMN IF NOT EXISTS role_seen_at TIMESTAMPTZ;
8244
+
8245
+ -- ── cms_set_user_role ────────────────────────────────────────────
8246
+ -- Upserts the user (reusing the standard sighting path so email/display_name
8247
+ -- stay fresh) and then REPLACES the role outright.
8248
+ --
8249
+ -- Returns the stored role so the caller can cheaply detect a change without a
8250
+ -- second round-trip.
8251
+ CREATE OR REPLACE FUNCTION ${s}.cms_set_user_role(
8252
+ p_provider TEXT,
8253
+ p_subject TEXT,
8254
+ p_email TEXT,
8255
+ p_display_name TEXT,
8256
+ p_role TEXT
8257
+ ) RETURNS TEXT AS $$
8258
+ DECLARE
8259
+ v_user_id BIGINT;
8260
+ v_role TEXT := LOWER(NULLIF(BTRIM(p_role), ''));
8261
+ BEGIN
8262
+ -- Anything outside the known vocabulary is stored as NULL (= no
8263
+ -- privilege). 'anonymous' is a real value: it is what an auth-disabled
8264
+ -- deployment issues, and the portal treats it as full access, so the
8265
+ -- worker must be able to see the same thing rather than guess.
8266
+ IF v_role IS NOT NULL AND v_role NOT IN ('admin', 'user', 'anonymous') THEN
8267
+ v_role := NULL;
8268
+ END IF;
8269
+
8270
+ v_user_id := ${s}.cms_register_user(p_provider, p_subject, p_email, p_display_name);
8271
+
8272
+ UPDATE ${s}.users
8273
+ SET role = v_role,
8274
+ role_seen_at = now(),
8275
+ updated_at = now()
8276
+ WHERE user_id = v_user_id;
8277
+
8278
+ RETURN v_role;
8279
+ END;
8280
+ $$ LANGUAGE plpgsql;
8281
+
8282
+ -- ── cms_get_user_role ────────────────────────────────────────────
8283
+ -- Narrow read for the worker's viewer resolver. Deliberately NOT folded into
8284
+ -- cms_get_user_profile: that would change an existing return type (requiring
8285
+ -- a DROP) and would hand the role to every profile reader, including the
8286
+ -- management surface, where it has no business being.
8287
+ --
8288
+ -- Returns no row for an unknown principal — the caller must treat "no row"
8289
+ -- and "NULL role" alike, as no privilege.
8290
+ CREATE OR REPLACE FUNCTION ${s}.cms_get_user_role(
8291
+ p_provider TEXT,
8292
+ p_subject TEXT
8293
+ ) RETURNS TABLE (
8294
+ role TEXT,
8295
+ role_seen_at TIMESTAMPTZ
8296
+ ) AS $$
8297
+ DECLARE
8298
+ v_provider TEXT := NULLIF(BTRIM(p_provider), '');
8299
+ v_subject TEXT := NULLIF(BTRIM(p_subject), '');
8300
+ BEGIN
8301
+ IF v_provider IS NULL OR v_subject IS NULL THEN
8302
+ RETURN;
8303
+ END IF;
8304
+
8305
+ RETURN QUERY
8306
+ SELECT u.role, u.role_seen_at
8307
+ FROM ${s}.users u
8308
+ WHERE u.provider = v_provider AND u.subject = v_subject;
8309
+ END;
8310
+ $$ LANGUAGE plpgsql;
8311
+ `;
8312
+ }
8313
+ // ─── Migration 0043: per-user agent package namespaces ───────────
8314
+ //
8315
+ // Package identity was globally unique on `name` alone, so the first person
8316
+ // to publish "triager" owned that word for the whole deployment. Identity
8317
+ // becomes the triple (scope, owner, name): every user gets their own
8318
+ // namespace, and `shared` is the deployment-wide one.
8319
+ //
8320
+ // Two rules carry the whole design:
8321
+ //
8322
+ // 1. RESOLUTION — a viewer's own ENABLED copy shadows the shared copy.
8323
+ // That single rule delivers download-modify-independently, and doubles
8324
+ // as recovery: disable a broken personal copy and the shared one takes
8325
+ // over again with no other action.
8326
+ //
8327
+ // 2. PROMOTION IS EXCLUSIVE — user→shared refuses when a shared package
8328
+ // already holds the name, because `shared` is the one namespace that
8329
+ // still has to be globally unique.
8330
+ //
8331
+ // Every mutation proc previously keyed off `p_name` alone, which is now
8332
+ // ambiguous (your "triager" and mine). They all gain an explicit SELECTOR —
8333
+ // (scope, owner_provider, owner_subject) — where a NULL scope means "resolve
8334
+ // it for the actor the same way resolution works everywhere else". Adding
8335
+ // parameters creates an OVERLOAD rather than replacing the function, so each
8336
+ // old signature is dropped explicitly first.
8337
+ //
8338
+ // The install manifest also starts returning `package_id`. Workers install
8339
+ // every enabled package to disk, and with namespaces two packages can share
8340
+ // a name — a stable per-row key is what keeps them from colliding in the
8341
+ // cache directory.
8342
+ function migration_0043_agent_package_namespaces(schema) {
8343
+ const s = `"${schema}"`;
8344
+ return `
8345
+ -- 0043_agent_package_namespaces: identity becomes (scope, owner, name).
8346
+
8347
+ -- ── The uniqueness swap ──────────────────────────────────────────
8348
+ --
8349
+ -- The old constraint came from \`name TEXT NOT NULL UNIQUE\` in the 0038
8350
+ -- CREATE TABLE, so PostgreSQL named it. Look it up by DEFINITION rather than
8351
+ -- by guessing the generated name: a schema restored from a dump, or created
8352
+ -- by a different PostgreSQL version, may not have named it identically.
8353
+ DO $mig$
8354
+ DECLARE
8355
+ v_con TEXT;
8356
+ BEGIN
8357
+ SELECT c.conname INTO v_con
8358
+ FROM pg_constraint c
8359
+ WHERE c.conrelid = '${s}.agent_packages'::regclass
8360
+ AND c.contype = 'u'
8361
+ AND pg_get_constraintdef(c.oid) = 'UNIQUE (name)';
8362
+ IF v_con IS NOT NULL THEN
8363
+ EXECUTE format('ALTER TABLE ${s}.agent_packages DROP CONSTRAINT %I', v_con);
8364
+ END IF;
8365
+ END
8366
+ $mig$;
8367
+
8368
+ -- Shared is still globally unique — it is the deployment's own namespace.
8369
+ CREATE UNIQUE INDEX IF NOT EXISTS agent_packages_shared_name_uniq
8370
+ ON ${s}.agent_packages (name) WHERE scope = 'shared';
8371
+
8372
+ -- User scope is unique PER OWNER. Existing rows were globally unique, so this
8373
+ -- index cannot conflict on the way in — there is nothing to reconcile.
8374
+ CREATE UNIQUE INDEX IF NOT EXISTS agent_packages_user_name_uniq
8375
+ ON ${s}.agent_packages (owner_provider, owner_subject, name) WHERE scope = 'user';
8376
+
8377
+ -- ── The resolver ─────────────────────────────────────────────────
8378
+ --
8379
+ -- One place decides which copy a name refers to, so reads, writes and the
8380
+ -- worker's agent lookup can never disagree about it.
8381
+ --
8382
+ -- p_sel_scope NULL → resolve: the viewer's own copy, else shared.
8383
+ -- p_sel_scope given → pin exactly that copy (this is what an FQN compiles to).
8384
+ --
8385
+ -- p_require_enabled exists because SHADOWING IS ENABLE-SENSITIVE: a disabled
8386
+ -- personal copy must fall through to shared (that is the recovery path),
8387
+ -- while a mutation has to find the disabled row in order to re-enable it.
8388
+ CREATE OR REPLACE FUNCTION ${s}.cms_resolve_agent_package_id(
8389
+ p_name TEXT,
8390
+ p_viewer_provider TEXT,
8391
+ p_viewer_subject TEXT,
8392
+ p_sel_scope TEXT,
8393
+ p_sel_owner_provider TEXT,
8394
+ p_sel_owner_subject TEXT,
8395
+ p_require_enabled BOOLEAN
8396
+ ) RETURNS TEXT AS $$
8397
+ DECLARE
8398
+ v_viewer_p TEXT := NULLIF(BTRIM(p_viewer_provider), '');
8399
+ v_viewer_s TEXT := NULLIF(BTRIM(p_viewer_subject), '');
8400
+ v_sel_p TEXT := NULLIF(BTRIM(p_sel_owner_provider), '');
8401
+ v_sel_s TEXT := NULLIF(BTRIM(p_sel_owner_subject), '');
8402
+ v_id TEXT;
8403
+ BEGIN
8404
+ IF p_sel_scope = 'shared' THEN
8405
+ SELECT p.package_id INTO v_id FROM ${s}.agent_packages p
8406
+ WHERE p.name = p_name AND p.scope = 'shared'
8407
+ AND (NOT p_require_enabled OR p.enabled);
8408
+ RETURN v_id;
8409
+ END IF;
8410
+
8411
+ IF p_sel_scope = 'user' THEN
8412
+ -- An explicit user-scope selection with no owner named means "mine".
8413
+ IF v_sel_p IS NULL OR v_sel_s IS NULL THEN
8414
+ v_sel_p := v_viewer_p;
8415
+ v_sel_s := v_viewer_s;
8416
+ END IF;
8417
+ IF v_sel_p IS NULL OR v_sel_s IS NULL THEN
8418
+ RETURN NULL;
8419
+ END IF;
8420
+ SELECT p.package_id INTO v_id FROM ${s}.agent_packages p
8421
+ WHERE p.name = p_name AND p.scope = 'user'
8422
+ AND p.owner_provider = v_sel_p AND p.owner_subject = v_sel_s
8423
+ AND (NOT p_require_enabled OR p.enabled);
8424
+ RETURN v_id;
8425
+ END IF;
8426
+
8427
+ -- Unpinned: own copy shadows shared.
8428
+ IF v_viewer_p IS NOT NULL AND v_viewer_s IS NOT NULL THEN
8429
+ SELECT p.package_id INTO v_id FROM ${s}.agent_packages p
8430
+ WHERE p.name = p_name AND p.scope = 'user'
8431
+ AND p.owner_provider = v_viewer_p AND p.owner_subject = v_viewer_s
8432
+ AND (NOT p_require_enabled OR p.enabled);
8433
+ IF v_id IS NOT NULL THEN
8434
+ RETURN v_id;
8435
+ END IF;
8436
+ END IF;
8437
+
8438
+ SELECT p.package_id INTO v_id FROM ${s}.agent_packages p
8439
+ WHERE p.name = p_name AND p.scope = 'shared'
8440
+ AND (NOT p_require_enabled OR p.enabled);
8441
+ RETURN v_id;
8442
+ END;
8443
+ $$ LANGUAGE plpgsql;
8444
+
8445
+ -- ── Mutations: every one now takes a selector ────────────────────
8446
+ --
8447
+ -- Dropped first because the argument lists grew; CREATE OR REPLACE would
8448
+ -- leave the old arity behind as a live overload, and a caller that missed
8449
+ -- the update would silently keep hitting the name-only version.
8450
+ DROP FUNCTION IF EXISTS ${s}.cms_agent_package_authz(TEXT, TEXT, TEXT, BOOLEAN);
8451
+ DROP FUNCTION IF EXISTS ${s}.cms_set_agent_package_scope(TEXT, TEXT, TEXT, TEXT, BOOLEAN);
8452
+ DROP FUNCTION IF EXISTS ${s}.cms_set_agent_package_enabled(TEXT, BOOLEAN, TEXT, TEXT, BOOLEAN);
8453
+ DROP FUNCTION IF EXISTS ${s}.cms_pin_agent_package_version(TEXT, TEXT, TEXT, TEXT, BOOLEAN);
8454
+ DROP FUNCTION IF EXISTS ${s}.cms_delete_agent_package(TEXT, TEXT, TEXT, BOOLEAN);
8455
+
8456
+ CREATE OR REPLACE FUNCTION ${s}.cms_agent_package_authz(
8457
+ p_name TEXT, p_actor_provider TEXT, p_actor_subject TEXT, p_is_admin BOOLEAN,
8458
+ p_sel_scope TEXT, p_sel_owner_provider TEXT, p_sel_owner_subject TEXT
8459
+ ) RETURNS ${s}.agent_packages AS $$
8460
+ DECLARE
8461
+ v_pkg ${s}.agent_packages;
8462
+ v_id TEXT;
8463
+ v_count INT;
8464
+ BEGIN
8465
+ -- require_enabled = FALSE: a disabled copy must still be reachable, or
8466
+ -- disabling one would be irreversible.
8467
+ v_id := ${s}.cms_resolve_agent_package_id(
8468
+ p_name, p_actor_provider, p_actor_subject,
8469
+ p_sel_scope, p_sel_owner_provider, p_sel_owner_subject, FALSE);
8470
+
8471
+ -- ADMIN FALLBACK, and it is load-bearing rather than a convenience.
8472
+ --
8473
+ -- Ordinary resolution walks "my copy, then shared", which is right for a
8474
+ -- user: a name they do not own and that is not shared genuinely does not
8475
+ -- exist in their namespace. But that rule would strand two real classes
8476
+ -- of row with no owner triple to select them by:
8477
+ --
8478
+ -- * NULL-owner packages, which pre-date owner stamping or were minted
8479
+ -- by an admin in a no-auth deployment;
8480
+ -- * another user's private package, which an admin must still be able
8481
+ -- to disable or delete during an incident.
8482
+ --
8483
+ -- Without this they would be invisible AND undeletable after 0043 — a
8484
+ -- migration that quietly orphans existing data. Ambiguity is refused
8485
+ -- rather than guessed: if several copies share the name, the admin has
8486
+ -- to say which one.
8487
+ IF v_id IS NULL AND p_is_admin AND p_sel_scope IS NULL THEN
8488
+ SELECT count(*) INTO v_count FROM ${s}.agent_packages p WHERE p.name = p_name;
8489
+ IF v_count > 1 THEN
8490
+ RAISE EXCEPTION 'AGENT_PACKAGE_AMBIGUOUS: % copies of "%" exist; name an owner or scope to pick one', v_count, p_name;
8491
+ END IF;
8492
+ SELECT p.package_id INTO v_id FROM ${s}.agent_packages p WHERE p.name = p_name;
8493
+ END IF;
8494
+
8495
+ IF v_id IS NULL THEN
8496
+ RAISE EXCEPTION 'AGENT_PACKAGE_NOT_FOUND: package "%" does not exist', p_name;
8497
+ END IF;
8498
+
8499
+ SELECT * INTO v_pkg FROM ${s}.agent_packages WHERE package_id = v_id FOR UPDATE;
8500
+ IF NOT FOUND THEN
8501
+ RAISE EXCEPTION 'AGENT_PACKAGE_NOT_FOUND: package "%" does not exist', p_name;
8502
+ END IF;
8503
+
8504
+ IF NOT p_is_admin AND (
8505
+ v_pkg.owner_provider IS NULL
8506
+ OR v_pkg.owner_provider IS DISTINCT FROM NULLIF(BTRIM(p_actor_provider), '')
8507
+ OR v_pkg.owner_subject IS DISTINCT FROM NULLIF(BTRIM(p_actor_subject), '')
8508
+ ) THEN
8509
+ RAISE EXCEPTION 'AGENT_PACKAGE_FORBIDDEN: only the package creator or an admin can modify "%"', p_name;
8510
+ END IF;
8511
+ RETURN v_pkg;
8512
+ END;
8513
+ $$ LANGUAGE plpgsql;
8514
+
8515
+ CREATE OR REPLACE FUNCTION ${s}.cms_set_agent_package_scope(
8516
+ p_name TEXT, p_scope TEXT, p_actor_provider TEXT, p_actor_subject TEXT, p_is_admin BOOLEAN,
8517
+ p_sel_scope TEXT, p_sel_owner_provider TEXT, p_sel_owner_subject TEXT
8518
+ ) RETURNS VOID AS $$
8519
+ DECLARE
8520
+ v_pkg ${s}.agent_packages;
8521
+ v_clash TEXT;
8522
+ BEGIN
8523
+ IF p_scope NOT IN ('shared', 'user') THEN
8524
+ RAISE EXCEPTION 'AGENT_PACKAGE_BAD_SCOPE: scope must be shared or user, got "%"', p_scope;
8525
+ END IF;
8526
+ v_pkg := ${s}.cms_agent_package_authz(
8527
+ p_name, p_actor_provider, p_actor_subject, p_is_admin,
8528
+ p_sel_scope, p_sel_owner_provider, p_sel_owner_subject);
8529
+
8530
+ IF p_scope = 'shared' AND v_pkg.scope <> 'shared' THEN
8531
+ -- Promotion is exclusive: shared is the one namespace still globally
8532
+ -- unique, so refuse with a legible error rather than letting the
8533
+ -- partial unique index raise a raw constraint violation.
8534
+ SELECT p.package_id INTO v_clash FROM ${s}.agent_packages p
8535
+ WHERE p.name = p_name AND p.scope = 'shared';
8536
+ IF v_clash IS NOT NULL THEN
8537
+ RAISE EXCEPTION 'AGENT_PACKAGE_NAME_TAKEN: a shared package named "%" already exists; rename before promoting', p_name;
8538
+ END IF;
8539
+ END IF;
8540
+
8541
+ IF p_scope = 'user' AND v_pkg.scope <> 'user' THEN
8542
+ -- Demotion needs an owner to land on, and must not collide with a
8543
+ -- copy that owner already has.
8544
+ IF v_pkg.owner_provider IS NULL OR v_pkg.owner_subject IS NULL THEN
8545
+ RAISE EXCEPTION 'AGENT_PACKAGE_NO_OWNER: "%" has no owner identity to demote to', p_name;
8546
+ END IF;
8547
+ SELECT p.package_id INTO v_clash FROM ${s}.agent_packages p
8548
+ WHERE p.name = p_name AND p.scope = 'user'
8549
+ AND p.owner_provider = v_pkg.owner_provider
8550
+ AND p.owner_subject = v_pkg.owner_subject;
8551
+ IF v_clash IS NOT NULL THEN
8552
+ RAISE EXCEPTION 'AGENT_PACKAGE_NAME_TAKEN: that owner already has a user-scope package named "%"', p_name;
8553
+ END IF;
8554
+ END IF;
8555
+
8556
+ UPDATE ${s}.agent_packages SET scope = p_scope WHERE package_id = v_pkg.package_id;
8557
+ PERFORM ${s}.cms_agent_registry_bump();
8558
+ END;
8559
+ $$ LANGUAGE plpgsql;
8560
+
8561
+ CREATE OR REPLACE FUNCTION ${s}.cms_set_agent_package_enabled(
8562
+ p_name TEXT, p_enabled BOOLEAN, p_actor_provider TEXT, p_actor_subject TEXT, p_is_admin BOOLEAN,
8563
+ p_sel_scope TEXT, p_sel_owner_provider TEXT, p_sel_owner_subject TEXT
8564
+ ) RETURNS VOID AS $$
8565
+ DECLARE
8566
+ v_pkg ${s}.agent_packages;
8567
+ BEGIN
8568
+ v_pkg := ${s}.cms_agent_package_authz(
8569
+ p_name, p_actor_provider, p_actor_subject, p_is_admin,
8570
+ p_sel_scope, p_sel_owner_provider, p_sel_owner_subject);
8571
+ UPDATE ${s}.agent_packages SET enabled = p_enabled WHERE package_id = v_pkg.package_id;
8572
+ PERFORM ${s}.cms_agent_registry_bump();
8573
+ END;
8574
+ $$ LANGUAGE plpgsql;
8575
+
8576
+ CREATE OR REPLACE FUNCTION ${s}.cms_pin_agent_package_version(
8577
+ p_name TEXT, p_semver TEXT, p_actor_provider TEXT, p_actor_subject TEXT, p_is_admin BOOLEAN,
8578
+ p_sel_scope TEXT, p_sel_owner_provider TEXT, p_sel_owner_subject TEXT
8579
+ ) RETURNS VOID AS $$
8580
+ DECLARE
8581
+ v_pkg ${s}.agent_packages;
8582
+ v_version_id TEXT;
8583
+ BEGIN
8584
+ v_pkg := ${s}.cms_agent_package_authz(
8585
+ p_name, p_actor_provider, p_actor_subject, p_is_admin,
8586
+ p_sel_scope, p_sel_owner_provider, p_sel_owner_subject);
8587
+ SELECT version_id INTO v_version_id FROM ${s}.agent_package_versions
8588
+ WHERE package_id = v_pkg.package_id AND semver = p_semver;
8589
+ IF NOT FOUND THEN
8590
+ RAISE EXCEPTION 'AGENT_PACKAGE_VERSION_NOT_FOUND: %@% is not a published version', p_name, p_semver;
8591
+ END IF;
8592
+ UPDATE ${s}.agent_packages SET active_version_id = v_version_id WHERE package_id = v_pkg.package_id;
8593
+ PERFORM ${s}.cms_agent_registry_bump();
8594
+ END;
8595
+ $$ LANGUAGE plpgsql;
8596
+
8597
+ CREATE OR REPLACE FUNCTION ${s}.cms_delete_agent_package(
8598
+ p_name TEXT, p_actor_provider TEXT, p_actor_subject TEXT, p_is_admin BOOLEAN,
8599
+ p_sel_scope TEXT, p_sel_owner_provider TEXT, p_sel_owner_subject TEXT
8600
+ ) RETURNS TABLE(artifact_filename TEXT) AS $$
8601
+ DECLARE
8602
+ v_pkg ${s}.agent_packages;
8603
+ BEGIN
8604
+ v_pkg := ${s}.cms_agent_package_authz(
8605
+ p_name, p_actor_provider, p_actor_subject, p_is_admin,
8606
+ p_sel_scope, p_sel_owner_provider, p_sel_owner_subject);
8607
+ RETURN QUERY
8608
+ SELECT v.artifact_filename FROM ${s}.agent_package_versions v
8609
+ WHERE v.package_id = v_pkg.package_id;
8610
+ DELETE FROM ${s}.agent_packages WHERE package_id = v_pkg.package_id;
8611
+ PERFORM ${s}.cms_agent_registry_bump();
8612
+ END;
8613
+ $$ LANGUAGE plpgsql;
8614
+
8615
+ -- ── Publish: locks the (name, scope, owner) row, not the name ────
8616
+ --
8617
+ -- The scope-mismatch error from 0038 is deliberately GONE. Publishing
8618
+ -- \`user\` when a \`shared\` package holds the name is no longer a conflict —
8619
+ -- it is exactly how a user takes a personal copy of a shared package, which
8620
+ -- is the headline feature of this migration.
8621
+ DROP FUNCTION IF EXISTS ${s}.cms_publish_agent_package(
8622
+ TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, BIGINT, TEXT, TEXT, JSONB, TEXT, BOOLEAN);
8623
+
8624
+ CREATE OR REPLACE FUNCTION ${s}.cms_publish_agent_package(
8625
+ p_package_id TEXT, p_version_id TEXT, p_name TEXT, p_scope TEXT,
8626
+ p_owner_provider TEXT, p_owner_subject TEXT, p_source_id TEXT,
8627
+ p_semver TEXT, p_sha256 TEXT, p_size_bytes BIGINT, p_artifact_filename TEXT,
8628
+ p_commit_sha TEXT, p_manifest JSONB, p_created_by TEXT, p_is_admin BOOLEAN
8629
+ ) RETURNS TABLE(status TEXT, package_id TEXT, version_id TEXT) AS $$
8630
+ DECLARE
8631
+ v_pkg RECORD;
8632
+ v_ver RECORD;
8633
+ v_owner_provider TEXT := NULLIF(BTRIM(p_owner_provider), '');
8634
+ v_owner_subject TEXT := NULLIF(BTRIM(p_owner_subject), '');
8635
+
8636
+ BEGIN
8637
+ IF p_scope NOT IN ('shared', 'user') THEN
8638
+ RAISE EXCEPTION 'AGENT_PACKAGE_BAD_SCOPE: scope must be shared or user, got "%"', p_scope;
8639
+ END IF;
8640
+
8641
+ -- The "__" prefix is reserved for platform sentinels (\`__shared\`, and
8642
+ -- whatever comes later). Enforced HERE, in the database, because it is the
8643
+ -- one gate every publish path goes through — CLI push, portal upload and
8644
+ -- the manager's own import all land on this function. A TypeScript-only
8645
+ -- check would be one forgotten caller away from letting somebody mint a
8646
+ -- package that captures every reference meaning "the deployment's copy".
8647
+ IF LOWER(BTRIM(p_name)) LIKE '\\_\\_%' THEN
8648
+ RAISE EXCEPTION 'AGENT_PACKAGE_RESERVED_NAME: "%" uses the reserved "__" prefix', p_name;
8649
+ END IF;
8650
+
8651
+ -- An owner-less publish is admin-only: a NULL-owner package would be
8652
+ -- unmanageable by its (anonymous) creator afterwards.
8653
+ IF NOT p_is_admin AND (v_owner_provider IS NULL OR v_owner_subject IS NULL) THEN
8654
+ RAISE EXCEPTION 'AGENT_PACKAGE_FORBIDDEN: publishing without an owner identity requires the admin role';
8655
+ END IF;
8656
+
8657
+ <<retry>>
8658
+ LOOP
8659
+ SELECT * INTO v_pkg FROM ${s}.agent_packages p
8660
+ WHERE p.name = p_name
8661
+ AND p.scope = p_scope
8662
+ AND (p_scope = 'shared'
8663
+ OR (p.owner_provider IS NOT DISTINCT FROM v_owner_provider
8664
+ AND p.owner_subject IS NOT DISTINCT FROM v_owner_subject))
8665
+ FOR UPDATE;
8666
+ IF NOT FOUND THEN
8667
+ -- FOR UPDATE on a missing row takes no lock, so a concurrent first
8668
+ -- publish can beat this INSERT — catch the unique violation and
8669
+ -- loop back to lock the winner's row.
8670
+ BEGIN
8671
+ INSERT INTO ${s}.agent_packages
8672
+ (package_id, source_id, name, scope, owner_provider, owner_subject, created_by)
8673
+ VALUES (p_package_id, p_source_id, p_name, p_scope,
8674
+ v_owner_provider, v_owner_subject, p_created_by);
8675
+ EXCEPTION WHEN unique_violation THEN
8676
+ CONTINUE retry;
8677
+ END;
8678
+ INSERT INTO ${s}.agent_package_versions
8679
+ (version_id, package_id, semver, sha256, size_bytes, artifact_filename, commit_sha, manifest, created_by)
8680
+ VALUES (p_version_id, p_package_id, p_semver, p_sha256, p_size_bytes,
8681
+ p_artifact_filename, p_commit_sha, p_manifest, p_created_by);
8682
+ UPDATE ${s}.agent_packages SET active_version_id = p_version_id WHERE ${s}.agent_packages.package_id = p_package_id;
8683
+ PERFORM ${s}.cms_agent_registry_bump();
8684
+ RETURN QUERY SELECT 'published'::TEXT, p_package_id, p_version_id;
8685
+ RETURN;
8686
+ END IF;
8687
+ EXIT retry;
8688
+ END LOOP;
8689
+
8690
+ IF NOT p_is_admin AND (
8691
+ v_pkg.owner_provider IS NULL
8692
+ OR v_pkg.owner_provider IS DISTINCT FROM v_owner_provider
8693
+ OR v_pkg.owner_subject IS DISTINCT FROM v_owner_subject
8694
+ ) THEN
8695
+ RAISE EXCEPTION 'AGENT_PACKAGE_FORBIDDEN: only the package creator or an admin can publish new versions of "%"', p_name;
8696
+ END IF;
8697
+
8698
+ SELECT * INTO v_ver FROM ${s}.agent_package_versions v
8699
+ WHERE v.package_id = v_pkg.package_id AND v.semver = p_semver;
8700
+ IF FOUND THEN
8701
+ IF v_ver.sha256 = p_sha256 THEN
8702
+ RETURN QUERY SELECT 'noop'::TEXT, v_pkg.package_id, v_ver.version_id;
8703
+ RETURN;
8704
+ END IF;
8705
+ RAISE EXCEPTION 'AGENT_PACKAGE_SEMVER_CONFLICT: %@% is already published with different content — published versions are immutable, bump the version', p_name, p_semver;
8706
+ END IF;
8707
+
8708
+ INSERT INTO ${s}.agent_package_versions
8709
+ (version_id, package_id, semver, sha256, size_bytes, artifact_filename, commit_sha, manifest, created_by)
8710
+ VALUES (p_version_id, v_pkg.package_id, p_semver, p_sha256, p_size_bytes,
8711
+ p_artifact_filename, p_commit_sha, p_manifest, p_created_by);
8712
+ UPDATE ${s}.agent_packages
8713
+ SET active_version_id = p_version_id,
8714
+ source_id = COALESCE(p_source_id, ${s}.agent_packages.source_id)
8715
+ WHERE ${s}.agent_packages.package_id = v_pkg.package_id;
8716
+ PERFORM ${s}.cms_agent_registry_bump();
8717
+ RETURN QUERY SELECT 'published'::TEXT, v_pkg.package_id, p_version_id;
8718
+ END;
8719
+ $$ LANGUAGE plpgsql;
8720
+
8721
+ -- ── Reads: a name can now legitimately return two rows ───────────
8722
+ --
8723
+ -- \`shadowed\` tells a UI that this shared package is currently overridden by
8724
+ -- the viewer's own copy, which is the difference between "you have two
8725
+ -- packages" and "you have one package with a fallback".
8726
+ DROP FUNCTION IF EXISTS ${s}.cms_list_agent_packages(TEXT, TEXT, BOOLEAN);
8727
+ DROP FUNCTION IF EXISTS ${s}.cms_get_agent_package(TEXT, TEXT, TEXT, BOOLEAN);
8728
+
8729
+ CREATE OR REPLACE FUNCTION ${s}.cms_list_agent_packages(
8730
+ p_viewer_provider TEXT, p_viewer_subject TEXT, p_is_admin BOOLEAN
8731
+ ) RETURNS TABLE(
8732
+ package_id TEXT, source_id TEXT, name TEXT, scope TEXT,
8733
+ owner_provider TEXT, owner_subject TEXT,
8734
+ owner_email TEXT, owner_display_name TEXT,
8735
+ enabled BOOLEAN, created_by TEXT, created_at TIMESTAMPTZ,
8736
+ active_version_id TEXT, semver TEXT, sha256 TEXT, size_bytes BIGINT,
8737
+ artifact_filename TEXT, commit_sha TEXT, manifest JSONB,
8738
+ version_created_at TIMESTAMPTZ, version_created_by TEXT,
8739
+ shadowed BOOLEAN
8740
+ ) AS $$
8741
+ SELECT p.package_id, p.source_id, p.name, p.scope,
8742
+ p.owner_provider, p.owner_subject,
8743
+ u.email, u.display_name,
8744
+ p.enabled, p.created_by, p.created_at,
8745
+ v.version_id, v.semver, v.sha256, v.size_bytes,
8746
+ v.artifact_filename, v.commit_sha, v.manifest, v.created_at, v.created_by,
8747
+ (p.scope = 'shared' AND EXISTS (
8748
+ SELECT 1 FROM ${s}.agent_packages o
8749
+ WHERE o.name = p.name AND o.scope = 'user' AND o.enabled
8750
+ AND o.owner_provider = BTRIM(p_viewer_provider)
8751
+ AND o.owner_subject = BTRIM(p_viewer_subject)
8752
+ )) AS shadowed
8753
+ FROM ${s}.agent_packages p
8754
+ LEFT JOIN ${s}.agent_package_versions v ON v.version_id = p.active_version_id
8755
+ LEFT JOIN ${s}.users u
8756
+ ON u.provider = p.owner_provider AND u.subject = p.owner_subject
8757
+ WHERE p.scope = 'shared'
8758
+ OR p_is_admin
8759
+ OR (p.owner_provider = BTRIM(p_viewer_provider) AND p.owner_subject = BTRIM(p_viewer_subject))
8760
+ ORDER BY p.name, p.scope, p.owner_provider, p.owner_subject;
8761
+ $$ LANGUAGE sql;
8762
+
8763
+ -- Selector-aware single read. With no selector it returns the copy the viewer
8764
+ -- would actually GET, which is what makes "show me package X" agree with
8765
+ -- "run agent from package X".
8766
+ CREATE OR REPLACE FUNCTION ${s}.cms_get_agent_package(
8767
+ p_name TEXT, p_viewer_provider TEXT, p_viewer_subject TEXT, p_is_admin BOOLEAN,
8768
+ p_sel_scope TEXT, p_sel_owner_provider TEXT, p_sel_owner_subject TEXT
8769
+ ) RETURNS TABLE(
8770
+ package_id TEXT, source_id TEXT, name TEXT, scope TEXT,
8771
+ owner_provider TEXT, owner_subject TEXT,
8772
+ owner_email TEXT, owner_display_name TEXT,
8773
+ enabled BOOLEAN, created_by TEXT, created_at TIMESTAMPTZ,
8774
+ active_version_id TEXT, version_id TEXT, semver TEXT, sha256 TEXT,
8775
+ size_bytes BIGINT, artifact_filename TEXT, commit_sha TEXT, manifest JSONB,
8776
+ version_created_at TIMESTAMPTZ, version_created_by TEXT
8777
+ ) AS $$
8778
+ DECLARE
8779
+ v_id TEXT;
8780
+ v_count INT;
8781
+ BEGIN
8782
+ -- Two-pass on purpose. A read should answer "which copy do I actually
8783
+ -- GET", and shadowing is enable-sensitive — so try the live-resolution
8784
+ -- rule first (enabled only). Falling back to the disabled row second
8785
+ -- means a package whose ONLY copy is disabled is still inspectable;
8786
+ -- without that, disabling your one copy would make it invisible as well
8787
+ -- as inert, and there would be no way back through this API.
8788
+ v_id := ${s}.cms_resolve_agent_package_id(
8789
+ p_name, p_viewer_provider, p_viewer_subject,
8790
+ p_sel_scope, p_sel_owner_provider, p_sel_owner_subject, TRUE);
8791
+ IF v_id IS NULL THEN
8792
+ v_id := ${s}.cms_resolve_agent_package_id(
8793
+ p_name, p_viewer_provider, p_viewer_subject,
8794
+ p_sel_scope, p_sel_owner_provider, p_sel_owner_subject, FALSE);
8795
+ END IF;
8796
+
8797
+ -- Same admin fallback the mutation path uses, and for the same reason:
8798
+ -- an admin must be able to READ the row they are allowed to delete.
8799
+ -- Keeping the two in step matters more than the duplication — a package
8800
+ -- an admin can destroy but cannot inspect is the worse outcome.
8801
+ IF v_id IS NULL AND p_is_admin AND p_sel_scope IS NULL THEN
8802
+ SELECT count(*) INTO v_count FROM ${s}.agent_packages p WHERE p.name = p_name;
8803
+ IF v_count = 1 THEN
8804
+ SELECT p.package_id INTO v_id FROM ${s}.agent_packages p WHERE p.name = p_name;
8805
+ END IF;
8806
+ END IF;
8807
+
8808
+ IF v_id IS NULL THEN
8809
+ RETURN;
8810
+ END IF;
8811
+
8812
+ -- Re-apply the visibility rule against the RESOLVED row. The resolver is
8813
+ -- about which copy a name means, never about whether you may see it —
8814
+ -- keeping those separate is what stops a selector from being a way to
8815
+ -- read someone else's private package by naming their owner triple.
8816
+ RETURN QUERY
8817
+ SELECT p.package_id, p.source_id, p.name, p.scope,
8818
+ p.owner_provider, p.owner_subject,
8819
+ u.email, u.display_name,
8820
+ p.enabled, p.created_by, p.created_at, p.active_version_id,
8821
+ v.version_id, v.semver, v.sha256, v.size_bytes,
8822
+ v.artifact_filename, v.commit_sha, v.manifest, v.created_at, v.created_by
8823
+ FROM ${s}.agent_packages p
8824
+ LEFT JOIN ${s}.agent_package_versions v ON v.package_id = p.package_id
8825
+ LEFT JOIN ${s}.users u
8826
+ ON u.provider = p.owner_provider AND u.subject = p.owner_subject
8827
+ WHERE p.package_id = v_id
8828
+ AND (p.scope = 'shared' OR p_is_admin
8829
+ OR (p.owner_provider = BTRIM(p_viewer_provider) AND p.owner_subject = BTRIM(p_viewer_subject)))
8830
+ ORDER BY v.created_at DESC;
8831
+ END;
8832
+ $$ LANGUAGE plpgsql;
8833
+
8834
+ -- ── Install manifest: carries package_id so disk keys stay unique ─
8835
+ DROP FUNCTION IF EXISTS ${s}.cms_get_agent_packages_install_manifest();
8836
+
8837
+ CREATE OR REPLACE FUNCTION ${s}.cms_get_agent_packages_install_manifest()
8838
+ RETURNS TABLE(
8839
+ package_id TEXT, name TEXT, scope TEXT, owner_provider TEXT, owner_subject TEXT,
8840
+ semver TEXT, sha256 TEXT, size_bytes BIGINT, artifact_filename TEXT, manifest JSONB
8841
+ ) AS $$
8842
+ SELECT p.package_id, p.name, p.scope, p.owner_provider, p.owner_subject,
8843
+ v.semver, v.sha256, v.size_bytes, v.artifact_filename, v.manifest
8844
+ FROM ${s}.agent_packages p
8845
+ JOIN ${s}.agent_package_versions v ON v.version_id = p.active_version_id
8846
+ WHERE p.enabled
8847
+ ORDER BY p.name, p.scope, p.owner_provider, p.owner_subject;
8848
+ $$ LANGUAGE sql;
8849
+ `;
8850
+ }
8199
8851
  //# sourceMappingURL=cms-migrations.js.map