pilotswarm 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 (135) hide show
  1. package/mcp/dist/bin/pilotswarm-mcp.js +4 -0
  2. package/mcp/dist/bin/pilotswarm-mcp.js.map +1 -1
  3. package/mcp/dist/src/context.d.ts +43 -10
  4. package/mcp/dist/src/context.d.ts.map +1 -1
  5. package/mcp/dist/src/context.js +15 -7
  6. package/mcp/dist/src/context.js.map +1 -1
  7. package/mcp/dist/src/resources/artifacts.d.ts.map +1 -1
  8. package/mcp/dist/src/resources/artifacts.js +4 -3
  9. package/mcp/dist/src/resources/artifacts.js.map +1 -1
  10. package/mcp/dist/src/server.d.ts.map +1 -1
  11. package/mcp/dist/src/server.js +2 -0
  12. package/mcp/dist/src/server.js.map +1 -1
  13. package/mcp/dist/src/tools/agent-packages.d.ts +14 -0
  14. package/mcp/dist/src/tools/agent-packages.d.ts.map +1 -0
  15. package/mcp/dist/src/tools/agent-packages.js +126 -0
  16. package/mcp/dist/src/tools/agent-packages.js.map +1 -0
  17. package/mcp/dist/src/tools/agents.d.ts.map +1 -1
  18. package/mcp/dist/src/tools/agents.js +18 -0
  19. package/mcp/dist/src/tools/agents.js.map +1 -1
  20. package/mcp/dist/src/tools/artifacts.d.ts.map +1 -1
  21. package/mcp/dist/src/tools/artifacts.js +12 -11
  22. package/mcp/dist/src/tools/artifacts.js.map +1 -1
  23. package/mcp/dist/src/tools/capabilities.js +1 -1
  24. package/mcp/dist/src/tools/capabilities.js.map +1 -1
  25. package/mcp/dist/src/tools/observability.js +1 -1
  26. package/mcp/dist/src/tools/observability.js.map +1 -1
  27. package/mcp/dist/src/tools/sessions.js +2 -2
  28. package/mcp/dist/src/tools/sessions.js.map +1 -1
  29. package/mcp/dist/src/tools/system.d.ts.map +1 -1
  30. package/mcp/dist/src/tools/system.js +56 -5
  31. package/mcp/dist/src/tools/system.js.map +1 -1
  32. package/mcp/dist/src/tools/turn-control.js +1 -1
  33. package/mcp/dist/src/tools/turn-control.js.map +1 -1
  34. package/mcp/dist/src/util/command.d.ts +3 -3
  35. package/mcp/dist/src/util/command.d.ts.map +1 -1
  36. package/mcp/dist/src/util/command.js +1 -1
  37. package/mcp/dist/src/util/command.js.map +1 -1
  38. package/package.json +6 -6
  39. package/tui/bin/tui.js +6 -0
  40. package/tui/src/agents-cli.js +442 -0
  41. package/tui/src/app.js +44 -1
  42. package/tui/src/node-sdk-transport.js +386 -5
  43. package/tui/src/plugin-config.js +17 -0
  44. package/ui/core/src/commands.js +7 -0
  45. package/ui/core/src/controller.js +498 -33
  46. package/ui/core/src/formatting.js +60 -16
  47. package/ui/core/src/index.js +2 -0
  48. package/ui/core/src/reducer.js +421 -34
  49. package/ui/core/src/repo-import.js +298 -0
  50. package/ui/core/src/repo-links.js +54 -0
  51. package/ui/core/src/selectors.js +879 -91
  52. package/ui/core/src/session-tree.js +109 -8
  53. package/ui/core/src/state.js +108 -2
  54. package/ui/core/src/themes/helpers.js +4 -1
  55. package/ui/core/src/themes/index.js +4 -10
  56. package/ui/core/src/themes/workspace-dark-rich.js +65 -0
  57. package/ui/core/src/themes/workspace-dark.js +65 -0
  58. package/ui/react/src/components.js +181 -4
  59. package/ui/react/src/web-app.js +1410 -91
  60. package/web/api/router.js +20 -2
  61. package/web/authz.js +4 -10
  62. package/web/dist/assets/{abnfDiagram-VRR7QNED-DLV45wH4.js → abnfDiagram-VRR7QNED-Cimc0f-u.js} +1 -1
  63. package/web/dist/assets/{arc-CE4xAYdE.js → arc-D8-_Dx5J.js} +1 -1
  64. package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-DfzR8-9F.js → architectureDiagram-ZJ3FMSHR-lzMAuQHJ.js} +1 -1
  65. package/web/dist/assets/{blockDiagram-677ZJIJ3-C2pZdqvH.js → blockDiagram-677ZJIJ3-Dp2CWjVS.js} +1 -1
  66. package/web/dist/assets/{c4Diagram-LMCZKHZV-CKRspTan.js → c4Diagram-LMCZKHZV-DF93UkOy.js} +1 -1
  67. package/web/dist/assets/channel-OwWeiCox.js +1 -0
  68. package/web/dist/assets/{chunk-2Q5K7J3B-CNjLrfHT.js → chunk-2Q5K7J3B-pbsQRM-F.js} +1 -1
  69. package/web/dist/assets/{chunk-32BRIVSS-Bv5EwyaJ.js → chunk-32BRIVSS-B_f-gG1O.js} +1 -1
  70. package/web/dist/assets/{chunk-5VM5RSS4-NDBesEbJ.js → chunk-5VM5RSS4-gqlpJDmt.js} +1 -1
  71. package/web/dist/assets/{chunk-EX3LRPZG-DHQht9ty.js → chunk-EX3LRPZG-Cms7hPmM.js} +1 -1
  72. package/web/dist/assets/{chunk-JWPE2WC7-r0OQpJRF.js → chunk-JWPE2WC7-Boq5PtPI.js} +1 -1
  73. package/web/dist/assets/{chunk-MOJQB5TN-fcwIDgtl.js → chunk-MOJQB5TN-DVn9i8zt.js} +1 -1
  74. package/web/dist/assets/{chunk-RYQCIY6F-x7XQlDO1.js → chunk-RYQCIY6F-a1vKs3UX.js} +1 -1
  75. package/web/dist/assets/{chunk-V7JOEXUC-U8ZNVy1m.js → chunk-V7JOEXUC-i60p5fHd.js} +1 -1
  76. package/web/dist/assets/{chunk-VR4S4FIN-DLEdPLcN.js → chunk-VR4S4FIN-a8p0o-8B.js} +1 -1
  77. package/web/dist/assets/{chunk-XXDRQBXY-TeyAtB-i.js → chunk-XXDRQBXY-BwP1yBKd.js} +1 -1
  78. package/web/dist/assets/classDiagram-OUVF2IWQ-CDaP7nuC.js +1 -0
  79. package/web/dist/assets/classDiagram-v2-EOCWNBFH-CDaP7nuC.js +1 -0
  80. package/web/dist/assets/{cose-bilkent-JH36ORCC-B3bAl1-Z.js → cose-bilkent-JH36ORCC-HW6kGgze.js} +1 -1
  81. package/web/dist/assets/{cynefin-VYW2F7L2-DexP8nG-.js → cynefin-VYW2F7L2-YUFMn-vE.js} +1 -1
  82. package/web/dist/assets/{cynefinDiagram-TSTJHNR4-Doa2XnSs.js → cynefinDiagram-TSTJHNR4-Brdy17i4.js} +1 -1
  83. package/web/dist/assets/{dagre-VKFMJZFB-DZMBKQcJ.js → dagre-VKFMJZFB-CAiSJJj-.js} +1 -1
  84. package/web/dist/assets/{diagram-FQU43EPY-BkzibmSq.js → diagram-FQU43EPY-B63Cqe0J.js} +1 -1
  85. package/web/dist/assets/{diagram-G47NLZAW-CihyHR3a.js → diagram-G47NLZAW-ByktrDJ2.js} +1 -1
  86. package/web/dist/assets/{diagram-NH7WQ7WH-OMDUUxwW.js → diagram-NH7WQ7WH-BLSfRMoE.js} +1 -1
  87. package/web/dist/assets/{diagram-OA4YK3LP-Lc0imJba.js → diagram-OA4YK3LP-DNVu-zpk.js} +1 -1
  88. package/web/dist/assets/{diagram-WEI45ONY-Dlcy8hPe.js → diagram-WEI45ONY-CNf4Sz-I.js} +1 -1
  89. package/web/dist/assets/{ebnfDiagram-CCIWWBDH-Dh3u17Dg.js → ebnfDiagram-CCIWWBDH-9CT3ergx.js} +1 -1
  90. package/web/dist/assets/{erDiagram-Q63AITRT-C0OhlUC2.js → erDiagram-Q63AITRT-BPJHAMgl.js} +1 -1
  91. package/web/dist/assets/{flowDiagram-23GEKE2U-CADKxT-x.js → flowDiagram-23GEKE2U-BVtpr6Iu.js} +1 -1
  92. package/web/dist/assets/{ganttDiagram-NO4QXBWP-iVVdxWB7.js → ganttDiagram-NO4QXBWP-BTHnv9sV.js} +1 -1
  93. package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-BwDUec_a.js → gitGraphDiagram-IHSO6WYX-BfmtqbL5.js} +1 -1
  94. package/web/dist/assets/index-B37T1c2s.css +1 -0
  95. package/web/dist/assets/index-Bmb8qK39.js +24 -0
  96. package/web/dist/assets/{infoDiagram-FWYZ7A6U-CGPUif0s.js → infoDiagram-FWYZ7A6U-kU4i4HRu.js} +1 -1
  97. package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-QYnrl-KA.js → ishikawaDiagram-FXEZZL3T-Ch7i-E8O.js} +1 -1
  98. package/web/dist/assets/{journeyDiagram-5HDEW3XC-CeKixxL0.js → journeyDiagram-5HDEW3XC-CPQxp0ps.js} +1 -1
  99. package/web/dist/assets/{kanban-definition-HUTT4EX6-CeYokFBS.js → kanban-definition-HUTT4EX6-BBgMVqoJ.js} +1 -1
  100. package/web/dist/assets/{linear-BbA6_gnz.js → linear-rwksvzXJ.js} +1 -1
  101. package/web/dist/assets/{mermaid.core--xGRoQTa.js → mermaid.core-o7PszRUc.js} +4 -4
  102. package/web/dist/assets/{mindmap-definition-LN4V7U3C-D53u8h0z.js → mindmap-definition-LN4V7U3C-wgOTdzlq.js} +1 -1
  103. package/web/dist/assets/{papaparse.min-BU0o7jXn.js → papaparse.min-BRXtjEI6.js} +1 -1
  104. package/web/dist/assets/{pegDiagram-2B236MQR-B7QSyG-3.js → pegDiagram-2B236MQR-BkhQqqTm.js} +1 -1
  105. package/web/dist/assets/{pieDiagram-ENE6RG2P-XtSGL7GG.js → pieDiagram-ENE6RG2P-CcMnvxUz.js} +1 -1
  106. package/web/dist/assets/pilotswarm-DYXtzuUN.js +99 -0
  107. package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-LZoYSbTH.js → quadrantDiagram-ABIIQ3AL-_KD1QeEr.js} +1 -1
  108. package/web/dist/assets/{railroadDiagram-RFXS5EU6-B5FREx2U.js → railroadDiagram-RFXS5EU6-CqI0olIe.js} +1 -1
  109. package/web/dist/assets/{requirementDiagram-TGXJPOKE-1VRFkBQX.js → requirementDiagram-TGXJPOKE-1JxRwusF.js} +1 -1
  110. package/web/dist/assets/{sankeyDiagram-HTMAVEWB-J456gWvu.js → sankeyDiagram-HTMAVEWB-9IpFoKzi.js} +1 -1
  111. package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-CaCcw6KO.js → sequenceDiagram-DBY2YBRQ-teyObSQ9.js} +1 -1
  112. package/web/dist/assets/{sizeCapture-X5ZJPWSS-DClsP3_O.js → sizeCapture-X5ZJPWSS-DVhU2J_6.js} +1 -1
  113. package/web/dist/assets/{stateDiagram-2N3HPSRC-D6VyrKhd.js → stateDiagram-2N3HPSRC-DHPoWpIQ.js} +1 -1
  114. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-DJjZWYLS.js +1 -0
  115. package/web/dist/assets/{swimlanes-5IMT3BWC-q678iAuo.js → swimlanes-5IMT3BWC-BxTtDQoB.js} +2 -2
  116. package/web/dist/assets/swimlanesDiagram-G3AALYLV-CV12vDcK.js +8 -0
  117. package/web/dist/assets/{timeline-definition-FHXFAJF6-WnPK_zhe.js → timeline-definition-FHXFAJF6-D0wXZx-4.js} +1 -1
  118. package/web/dist/assets/{vennDiagram-L72KCM5P-WpumuLld.js → vennDiagram-L72KCM5P-BSFyI3iV.js} +1 -1
  119. package/web/dist/assets/{wardleyDiagram-EHGQE667-CBrYLUm_.js → wardleyDiagram-EHGQE667-BN-S7hh0.js} +1 -1
  120. package/web/dist/assets/{xychartDiagram-FW5EYKEG-DbX1rnDW.js → xychartDiagram-FW5EYKEG-nDNuPs7o.js} +1 -1
  121. package/web/dist/index.html +3 -3
  122. package/web/runtime.js +36 -4
  123. package/ui/core/src/themes/daylight.js +0 -62
  124. package/ui/core/src/themes/light-high-contrast.js +0 -65
  125. package/ui/core/src/themes/paper-ink.js +0 -62
  126. package/ui/core/src/themes/solarized-ops.js +0 -59
  127. package/ui/core/src/themes/tokyo-night.js +0 -56
  128. package/web/dist/assets/channel-D7p-E4El.js +0 -1
  129. package/web/dist/assets/classDiagram-OUVF2IWQ-yFZ-1yMb.js +0 -1
  130. package/web/dist/assets/classDiagram-v2-EOCWNBFH-yFZ-1yMb.js +0 -1
  131. package/web/dist/assets/index-BGWTu1KK.css +0 -1
  132. package/web/dist/assets/index-Djfdr7ts.js +0 -24
  133. package/web/dist/assets/pilotswarm-CtO819QH.js +0 -94
  134. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-D7eKFhHb.js +0 -1
  135. package/web/dist/assets/swimlanesDiagram-G3AALYLV-DlVHQBQx.js +0 -8
@@ -17,6 +17,14 @@ import {
17
17
  ATTACHMENT_MAX_BYTES,
18
18
  ATTACHMENTS_MAX_COUNT,
19
19
  ATTACHMENTS_MAX_TOTAL_BYTES,
20
+ PgSessionCatalog,
21
+ publishAgentPackageDir,
22
+ deleteAgentPackageEverywhere,
23
+ fetchAgentPackageTarGz,
24
+ readAgentPackageTarGz,
25
+ listBundledAgentNames,
26
+ normalizeAgentName,
27
+ AgentPackageValidationError,
20
28
  } from "pilotswarm-sdk";
21
29
  import { startEmbeddedWorkers, stopEmbeddedWorkers } from "./embedded-workers.js";
22
30
  import { getPluginDirsFromEnv } from "./plugin-config.js";
@@ -604,7 +612,10 @@ export class NodeSdkTransport {
604
612
  this.client ? this.client.stop() : Promise.resolve(),
605
613
  this.mgmt.stop(),
606
614
  stopEmbeddedWorkers(this.workers),
615
+ this._agentPkgCatalog ? this._agentPkgCatalog.close() : Promise.resolve(),
607
616
  ]);
617
+ this._agentPkgCatalog = null;
618
+ this._agentPkgCatalogPromise = null;
608
619
  this.client = null;
609
620
  }
610
621
 
@@ -623,6 +634,374 @@ export class NodeSdkTransport {
623
634
  return loadSessionCreationMetadataFromPluginDirs(getPluginDirsFromEnv());
624
635
  }
625
636
 
637
+ async listCreatableAgents(viewer = null, isAdmin = true) {
638
+ const baked = this.creatableAgents.map((agent) => ({ ...agent, source: "builtin" }));
639
+ const registry = await this._listRegistryCreatableAgents(viewer, isAdmin);
640
+ // Baked agents are deployment code — they win a (normalized) name
641
+ // collision; the colliding registry entry is dropped, matching the
642
+ // validator that refuses such packages going forward.
643
+ const taken = new Set(baked.map((agent) => normalizeAgentName(agent.name)));
644
+ const merged = [...baked];
645
+ for (const entry of registry) {
646
+ if (taken.has(normalizeAgentName(entry.name))) {
647
+ // Baked wins by design — but never silently: the package's
648
+ // agent is unreachable until it renames.
649
+ console.warn(`[transport] package agent "${entry.name}" (${entry.packageName}) collides with a baked agent and is hidden from the catalog`);
650
+ continue;
651
+ }
652
+ taken.add(normalizeAgentName(entry.name));
653
+ merged.push(entry);
654
+ }
655
+ return merged;
656
+ }
657
+
658
+ /** Registry package agents as creatable-agent entries, viewer-scoped. */
659
+ async _listRegistryCreatableAgents(viewer, isAdmin) {
660
+ const ctx = await this._agentPackagesContext();
661
+ if (!ctx) return [];
662
+ try {
663
+ const packages = await ctx.catalog.listAgentPackages(
664
+ viewer ? { provider: viewer.provider, subject: viewer.subject } : null,
665
+ Boolean(isAdmin),
666
+ );
667
+ // Ownership is decided HERE, where the viewer's identity is known.
668
+ // An ADMIN sees every user's user-scoped packages, so "scope is
669
+ // user" does not mean "mine" — grouping on scope alone filed other
670
+ // people's agents onto the caller's shelf. Sending a boolean rather
671
+ // than the owner's identifiers keeps the catalog from telling every
672
+ // caller who installed what. No viewer at all (single-user or
673
+ // auth-disabled) means any user-scoped package is the caller's.
674
+ const principalKey = (principal) => (principal?.provider && principal?.subject
675
+ ? `${principal.provider}:${principal.subject}`.toLowerCase()
676
+ : null);
677
+ const viewerKey = principalKey(viewer);
678
+ const entries = [];
679
+ for (const pkg of packages) {
680
+ if (!pkg.enabled || !pkg.active) continue;
681
+ const ownerKey = principalKey(pkg.owner);
682
+ const mine = pkg.scope === "user" && (viewerKey === null || ownerKey === viewerKey);
683
+ const agents = Array.isArray(pkg.active.manifest?.agents) ? pkg.active.manifest.agents : [];
684
+ for (const agent of agents) {
685
+ if (!agent?.name) continue;
686
+ entries.push({
687
+ name: agent.name,
688
+ title: agent.title || undefined,
689
+ description: agent.description || undefined,
690
+ tools: Array.isArray(agent.tools) ? agent.tools : undefined,
691
+ // Carried so a PACKAGE agent presents itself like a
692
+ // baked one: the picker sends these with the create
693
+ // call, so without them a package agent could neither
694
+ // self-start (initialPrompt) nor show its own splash.
695
+ splash: typeof agent.splash === "string" && agent.splash.trim() ? agent.splash : undefined,
696
+ splashMobile: typeof agent.splashMobile === "string" && agent.splashMobile.trim() ? agent.splashMobile : undefined,
697
+ initialPrompt: typeof agent.initialPrompt === "string" && agent.initialPrompt.trim() ? agent.initialPrompt : undefined,
698
+ source: "package",
699
+ packageName: pkg.name,
700
+ packageSemver: pkg.active.semver,
701
+ scope: pkg.scope,
702
+ mine,
703
+ ownerLabel: pkg.createdBy || pkg.owner?.subject || null,
704
+ });
705
+ }
706
+ }
707
+ return entries;
708
+ } catch (err) {
709
+ console.warn(`[transport] agent-package catalog unavailable: ${err?.message || err}`);
710
+ return [];
711
+ }
712
+ }
713
+
714
+ /**
715
+ * Enforce user-scope visibility at create time and admit package agents
716
+ * into the client's live allowlist. Baked agents pass through untouched.
717
+ */
718
+ async _authorizePackageAgentCreate(agentName, owner, isAdmin) {
719
+ const normalized = normalizeAgentName(agentName);
720
+ const baked = this.creatableAgents.find((agent) => normalizeAgentName(agent.name) === normalized);
721
+ if (baked) return baked.name;
722
+ const registry = await this._listRegistryCreatableAgents(owner, isAdmin);
723
+ const match = registry.find((entry) => normalizeAgentName(entry.name) === normalized);
724
+ if (!match) {
725
+ // Either the agent doesn't exist or it is a user-scope package the
726
+ // caller can't see — same answer either way (no existence oracle).
727
+ const err = new Error(`agent "${agentName}" is not available to you`);
728
+ err.code = "FORBIDDEN";
729
+ throw err;
730
+ }
731
+ // Client holds this.allowedAgentNames BY REFERENCE (live getter) —
732
+ // pushing here makes the delegated create pass client validation.
733
+ if (this.allowedAgentNames.length > 0 && !this.allowedAgentNames.includes(match.name)) {
734
+ this.allowedAgentNames.push(match.name);
735
+ }
736
+ return match.name;
737
+ }
738
+
739
+ /**
740
+ * Lazy direct-store registry context; null in pure web/client setups AND
741
+ * on init failure. A failed init is NEVER cached — the portal boots
742
+ * alongside 8 workers all polling the migration advisory lock, and one
743
+ * transient contention loss must not permanently reduce the catalog to
744
+ * baked agents (observed live: a cached rejection made every later
745
+ * listCreatableAgents union silently registry-less until pod restart).
746
+ */
747
+ async _agentPackagesContext() {
748
+ if (!this.store || !this.artifactStore) return null;
749
+ if (this._agentPkgCatalog) {
750
+ return { catalog: this._agentPkgCatalog, artifactStore: this.artifactStore };
751
+ }
752
+ try {
753
+ this._agentPkgCatalogPromise ??= (async () => {
754
+ const catalog = await PgSessionCatalog.create(this.store, process.env.PILOTSWARM_CMS_SCHEMA || undefined, {
755
+ ...(this.useManagedIdentity !== undefined ? { useManagedIdentity: this.useManagedIdentity } : {}),
756
+ ...(this.aadDbUser ? { aadUser: this.aadDbUser } : {}),
757
+ });
758
+ await catalog.initialize();
759
+ this._agentPkgCatalog = catalog;
760
+ return catalog;
761
+ })();
762
+ await this._agentPkgCatalogPromise;
763
+ } catch (err) {
764
+ this._agentPkgCatalogPromise = null;
765
+ console.warn(`[transport] agent-package registry init failed (will retry on next call): ${err?.message || err}`);
766
+ return null;
767
+ }
768
+ return { catalog: this._agentPkgCatalog, artifactStore: this.artifactStore };
769
+ }
770
+
771
+ _requirePrincipal(owner, isAdmin) {
772
+ if (owner) return { provider: owner.provider, subject: owner.subject };
773
+ if (isAdmin) return null; // anonymous deployments: admin-equivalent
774
+ const err = new Error("agent-package operations require an authenticated principal");
775
+ err.code = "FORBIDDEN";
776
+ throw err;
777
+ }
778
+
779
+ _createdByLabel(owner) {
780
+ return owner?.email || owner?.displayName || (owner ? `${owner.provider}:${owner.subject}` : "system");
781
+ }
782
+
783
+ /**
784
+ * The registry raises typed failures as MESSAGE PREFIXES
785
+ * (AGENT_PACKAGE_FORBIDDEN: …). Attach the matching error.code so the
786
+ * router maps them to 403/404/409 instead of a generic 500 — without
787
+ * this, "you don't own this package" reaches the browser as
788
+ * "Internal server error".
789
+ */
790
+ async _mapAgentPackageErrors(run) {
791
+ try {
792
+ return await run();
793
+ } catch (error) {
794
+ const message = String(error?.message || "");
795
+ if (!error?.code) {
796
+ if (/AGENT_(?:PACKAGE|SOURCE)_[A-Z_]*FORBIDDEN/.test(message)) error.code = "FORBIDDEN";
797
+ else if (/AGENT_(?:PACKAGE|SOURCE)_[A-Z_]*NOT_FOUND/.test(message)) error.code = "NOT_FOUND";
798
+ else if (/AGENT_PACKAGE_(?:SEMVER_CONFLICT|SCOPE_MISMATCH)/.test(message)) error.code = "CONFLICT";
799
+ else if (/AGENT_PACKAGE_BAD_SCOPE/.test(message)) error.code = "INVALID_REQUEST";
800
+ }
801
+ throw error;
802
+ }
803
+ }
804
+
805
+ // ── Agent packages: registry CRUD (portal ops surface) ──────
806
+
807
+ async listAgentPackages(owner, isAdmin) {
808
+ const ctx = await this._agentPackagesContext();
809
+ if (!ctx) return [];
810
+ return ctx.catalog.listAgentPackages(this._requirePrincipal(owner, isAdmin), Boolean(isAdmin));
811
+ }
812
+
813
+ async getAgentPackage(name, owner, isAdmin) {
814
+ const ctx = await this._agentPackagesContext();
815
+ if (!ctx) return null;
816
+ return ctx.catalog.getAgentPackage(name, this._requirePrincipal(owner, isAdmin), Boolean(isAdmin));
817
+ }
818
+
819
+
820
+
821
+
822
+
823
+ async listAgentWorkerState() {
824
+ const ctx = await this._agentPackagesContext();
825
+ if (!ctx) return [];
826
+ return ctx.catalog.listAgentWorkerState();
827
+ }
828
+
829
+ async listWorkers() {
830
+ const ctx = await this._agentPackagesContext();
831
+ if (!ctx) return [];
832
+ return ctx.catalog.listWorkers();
833
+ }
834
+
835
+ async setAgentPackageScope(name, scope, owner, isAdmin) {
836
+ const ctx = await this._agentPackagesContext();
837
+ if (!ctx) throw new Error("agent packages are not available on this deployment");
838
+ await this._mapAgentPackageErrors(() => ctx.catalog.setAgentPackageScope(name, scope === "shared" ? "shared" : "user",
839
+ this._requirePrincipal(owner, isAdmin), Boolean(isAdmin)));
840
+ return { ok: true };
841
+ }
842
+
843
+ async setAgentPackageEnabled(name, enabled, owner, isAdmin) {
844
+ const ctx = await this._agentPackagesContext();
845
+ if (!ctx) throw new Error("agent packages are not available on this deployment");
846
+ await this._mapAgentPackageErrors(() => ctx.catalog.setAgentPackageEnabled(name, Boolean(enabled),
847
+ this._requirePrincipal(owner, isAdmin), Boolean(isAdmin)));
848
+ return { ok: true };
849
+ }
850
+
851
+ async pinAgentPackageVersion(name, semver, owner, isAdmin) {
852
+ const ctx = await this._agentPackagesContext();
853
+ if (!ctx) throw new Error("agent packages are not available on this deployment");
854
+ await this._mapAgentPackageErrors(() => ctx.catalog.pinAgentPackageVersion(name, String(semver || ""),
855
+ this._requirePrincipal(owner, isAdmin), Boolean(isAdmin)));
856
+ return { ok: true };
857
+ }
858
+
859
+ async deleteAgentPackage(name, owner, isAdmin) {
860
+ const ctx = await this._agentPackagesContext();
861
+ if (!ctx) throw new Error("agent packages are not available on this deployment");
862
+ await this._mapAgentPackageErrors(() => deleteAgentPackageEverywhere(ctx, name, this._requirePrincipal(owner, isAdmin), Boolean(isAdmin)));
863
+ this._agentPkgTarCache?.clear?.();
864
+ return { ok: true };
865
+ }
866
+
867
+ /** Inline-files upload → the one publish pipeline. ≤ 2 MB decoded. */
868
+ async uploadAgentPackage(files, scope, owner, isAdmin) {
869
+ const ctx = await this._agentPackagesContext();
870
+ if (!ctx) throw new Error("agent packages are not available on this deployment");
871
+ const principal = this._requirePrincipal(owner, isAdmin);
872
+ if (!Array.isArray(files) || files.length === 0) {
873
+ throw new Error("upload requires files: [{ path, contentBase64 }]");
874
+ }
875
+ if (files.length > 2000) throw new Error("upload exceeds 2000 files");
876
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "ps-agent-upload-"));
877
+ try {
878
+ let total = 0;
879
+ for (const file of files) {
880
+ const rel = String(file?.path || "");
881
+ if (!rel || rel.startsWith("/") || rel.includes("\\") || rel.split("/").some((p) => p === "..")) {
882
+ throw new Error(`upload file path is not a safe relative path: ${rel}`);
883
+ }
884
+ const bytes = Buffer.from(String(file?.contentBase64 || ""), "base64");
885
+ total += bytes.length;
886
+ if (total > 2 * 1024 * 1024) throw new Error("upload exceeds the 2 MB envelope — use a repo/url source or the CLI for larger packages");
887
+ const target = path.resolve(tmp, rel);
888
+ if (target !== tmp && !target.startsWith(tmp + path.sep)) {
889
+ throw new Error(`upload file path escapes the package root: ${rel}`);
890
+ }
891
+ fs.mkdirSync(path.dirname(target), { recursive: true });
892
+ fs.writeFileSync(target, bytes);
893
+ }
894
+ const outcome = await this._mapAgentPackageErrors(() => publishAgentPackageDir(ctx, {
895
+ dir: tmp,
896
+ scope: scope === "shared" ? "shared" : "user",
897
+ owner: principal,
898
+ createdBy: this._createdByLabel(owner),
899
+ isAdmin: Boolean(isAdmin),
900
+ validate: { reservedAgentNames: this._reservedAgentNames() },
901
+ }));
902
+ const { manifest: _m, ...summary } = outcome;
903
+ return summary;
904
+ } catch (err) {
905
+ if (err instanceof AgentPackageValidationError) {
906
+ const wrapped = new Error(err.message);
907
+ wrapped.code = "VALIDATION_FAILED";
908
+ wrapped.validation = err.validation;
909
+ throw wrapped;
910
+ }
911
+ throw err;
912
+ } finally {
913
+ fs.rmSync(tmp, { recursive: true, force: true });
914
+ }
915
+ }
916
+
917
+ _reservedAgentNames() {
918
+ if (!this._reservedNamesCache) {
919
+ const bundled = (() => { try { return listBundledAgentNames(); } catch { return []; } })();
920
+ if (bundled.length === 0) {
921
+ // System agent names (sweeper, resourcemgr, …) live ONLY here —
922
+ // an empty list means packages could shadow them silently.
923
+ console.warn("[transport] listBundledAgentNames returned nothing — bundled plugins/ missing from the SDK install? Reserved-name validation is degraded.");
924
+ }
925
+ this._reservedNamesCache = [
926
+ ...bundled,
927
+ ...this.creatableAgents.map((agent) => agent.name),
928
+ ];
929
+ }
930
+ return this._reservedNamesCache;
931
+ }
932
+
933
+ // ── Agent packages: workspace viewer (tree + file preview) ──
934
+
935
+ async _agentPackageEntries(name, semver, owner, isAdmin) {
936
+ const ctx = await this._agentPackagesContext();
937
+ if (!ctx) throw new Error("agent packages are not available on this deployment");
938
+ const detail = await ctx.catalog.getAgentPackage(name, this._requirePrincipal(owner, isAdmin), Boolean(isAdmin));
939
+ if (!detail) throw Object.assign(new Error(`package "${name}" not found`), { code: "NOT_FOUND" });
940
+ const version = semver
941
+ ? detail.versions.find((v) => v.semver === semver)
942
+ : detail.versions.find((v) => v.versionId === detail.activeVersionId) ?? detail.versions[0];
943
+ if (!version) throw Object.assign(new Error(`package "${name}" has no ${semver ? `version ${semver}` : "versions"}`), { code: "NOT_FOUND" });
944
+ const cacheKey = `${name}@${version.semver}.${version.sha256.slice(0, 12)}`;
945
+ this._agentPkgTarCache ??= new Map();
946
+ let entries = this._agentPkgTarCache.get(cacheKey);
947
+ if (!entries) {
948
+ const targz = await this._mapAgentPackageErrors(
949
+ () => fetchAgentPackageTarGz(ctx.artifactStore, version.artifactFilename, version.sha256),
950
+ );
951
+ entries = readAgentPackageTarGz(targz);
952
+ this._agentPkgTarCache.set(cacheKey, entries);
953
+ // Bound by BYTES (the real resource), not just entry count.
954
+ const bytesOf = (list) => list.reduce((sum, e) => sum + e.body.length, 0);
955
+ let totalBytes = 0;
956
+ for (const cached of this._agentPkgTarCache.values()) totalBytes += bytesOf(cached);
957
+ while ((totalBytes > 64 * 1024 * 1024 || this._agentPkgTarCache.size > 6)
958
+ && this._agentPkgTarCache.size > 1) {
959
+ const oldest = this._agentPkgTarCache.keys().next().value;
960
+ totalBytes -= bytesOf(this._agentPkgTarCache.get(oldest));
961
+ this._agentPkgTarCache.delete(oldest);
962
+ }
963
+ }
964
+ return { entries, version };
965
+ }
966
+
967
+ async getAgentPackageTree(name, semver, owner, isAdmin) {
968
+ const { entries, version } = await this._agentPackageEntries(name, semver, owner, isAdmin);
969
+ const dirs = new Set();
970
+ const files = [];
971
+ for (const entry of entries) {
972
+ files.push({ path: entry.name, size: entry.body.length });
973
+ const parts = entry.name.split("/");
974
+ for (let i = 1; i < parts.length; i++) dirs.add(parts.slice(0, i).join("/"));
975
+ }
976
+ files.sort((a, b) => (a.path < b.path ? -1 : 1));
977
+ return {
978
+ name,
979
+ semver: version.semver,
980
+ sha256: version.sha256,
981
+ dirs: [...dirs].sort(),
982
+ files,
983
+ };
984
+ }
985
+
986
+ async getAgentPackageFile(name, semver, filePath, owner, isAdmin) {
987
+ const { entries, version } = await this._agentPackageEntries(name, semver, owner, isAdmin);
988
+ const rel = String(filePath || "");
989
+ const entry = entries.find((candidate) => candidate.name === rel);
990
+ if (!entry) throw Object.assign(new Error(`"${rel}" is not in ${name}@${version.semver}`), { code: "NOT_FOUND" });
991
+ const MAX_PREVIEW = 256 * 1024;
992
+ const body = entry.body.subarray(0, MAX_PREVIEW);
993
+ const isBinary = body.includes(0);
994
+ return {
995
+ name,
996
+ semver: version.semver,
997
+ path: rel,
998
+ size: entry.body.length,
999
+ truncated: entry.body.length > MAX_PREVIEW,
1000
+ encoding: isBinary ? "base64" : "utf8",
1001
+ content: isBinary ? body.toString("base64") : body.toString("utf8"),
1002
+ };
1003
+ }
1004
+
626
1005
  getWorkerCount() {
627
1006
  return this.workers.length || (this.mode === "remote" ? 0 : parseInt(process.env.WORKERS || "4", 10));
628
1007
  }
@@ -1080,9 +1459,14 @@ export class NodeSdkTransport {
1080
1459
  return { sessionId: session.sessionId, model: effectiveModel, reasoningEffort: reasoningEffort || undefined, contextTier: contextTier || undefined };
1081
1460
  }
1082
1461
 
1083
- async createSessionForAgent(agentName, { model, reasoningEffort, contextTier, title, splash, splashMobile, initialPrompt, owner, groupId, visibility } = {}) {
1462
+ async createSessionForAgent(agentName, { model, reasoningEffort, contextTier, title, splash, splashMobile, initialPrompt, owner, isAdmin, groupId, visibility } = {}) {
1463
+ // Registry (package) agents are not in the static baked allowlist —
1464
+ // resolve the union, enforce user-scope ownership, then delegate the
1465
+ // CANONICAL catalog name (the client's allowlist and the CMS row use
1466
+ // exact names; only the worker resolver is fuzzy).
1467
+ const canonicalName = await this._authorizePackageAgentCreate(agentName, owner ?? null, isAdmin ?? false);
1084
1468
  const effectiveModel = await this.assertSessionModelCreatable({ model, owner });
1085
- const session = await this.client.createSessionForAgent(agentName, {
1469
+ const session = await this.client.createSessionForAgent(canonicalName, {
1086
1470
  ...(effectiveModel ? { model: effectiveModel } : {}),
1087
1471
  ...(reasoningEffort ? { reasoningEffort } : {}),
1088
1472
  ...(contextTier ? { contextTier } : {}),
@@ -1103,9 +1487,6 @@ export class NodeSdkTransport {
1103
1487
  };
1104
1488
  }
1105
1489
 
1106
- listCreatableAgents() {
1107
- return [...this.creatableAgents];
1108
- }
1109
1490
 
1110
1491
  getSessionCreationPolicy() {
1111
1492
  return this.sessionPolicy;
@@ -176,6 +176,15 @@ export function resolvePortalConfigBundleFromPluginDirs(pluginDirs = []) {
176
176
  loadingMessage: "Preparing your workspace",
177
177
  loadingCopy: "Connecting the shared workspace and live session feeds...",
178
178
  },
179
+ // Where "how do I build an agent package?" points. A layered
180
+ // deployment ships its OWN guide - the base instructions plus the
181
+ // skills, tools and MCP servers that exist only on that fleet - so
182
+ // this is config, not a constant. Public repo by default because the
183
+ // link is meant to be handed to a coding assistant, which cannot
184
+ // reach an intranet-only portal.
185
+ docs: {
186
+ agentPackageGuideUrl: "https://github.com/affandar/PilotSwarm/blob/main/docs/building-agent-packages.md",
187
+ },
179
188
  auth: {
180
189
  provider: null,
181
190
  providers: {},
@@ -193,6 +202,7 @@ export function resolvePortalConfigBundleFromPluginDirs(pluginDirs = []) {
193
202
  const portal = getObject(pluginMeta?.portal);
194
203
  const portalBranding = getObject(portal.branding);
195
204
  const portalUi = getObject(portal.ui);
205
+ const portalDocs = getObject(portal.docs);
196
206
  const portalAuth = getObject(portal.auth);
197
207
  const tui = getObject(pluginMeta?.tui);
198
208
 
@@ -250,6 +260,13 @@ export function resolvePortalConfigBundleFromPluginDirs(pluginDirs = []) {
250
260
  loadingMessage: firstNonEmptyString(portalUi.loadingMessage, portal.loadingMessage, defaults.ui.loadingMessage) || defaults.ui.loadingMessage,
251
261
  loadingCopy: firstNonEmptyString(portalUi.loadingCopy, portal.loadingCopy, defaults.ui.loadingCopy) || defaults.ui.loadingCopy,
252
262
  },
263
+ docs: {
264
+ agentPackageGuideUrl: firstNonEmptyString(
265
+ portalDocs.agentPackageGuideUrl,
266
+ portal.agentPackageGuideUrl,
267
+ defaults.docs.agentPackageGuideUrl,
268
+ ) || defaults.docs.agentPackageGuideUrl,
269
+ },
253
270
  auth: {
254
271
  provider: firstNonEmptyString(portalAuth.provider, portal.provider),
255
272
  providers: getObject(portalAuth.providers),
@@ -89,6 +89,13 @@ export const UI_COMMANDS = {
89
89
  OPEN_ADMIN_CONSOLE: "openAdminConsole",
90
90
  CLOSE_ADMIN_CONSOLE: "closeAdminConsole",
91
91
  ADMIN_REFRESH_PROFILE: "adminRefreshProfile",
92
+ ADMIN_SHOW_GHCP: "adminShowGhcp",
93
+ ADMIN_SHOW_PACKAGES: "adminShowPackages",
94
+ ADMIN_SHOW_WORKERS: "adminShowWorkers",
95
+ ADMIN_WORKERS_REFRESH: "adminWorkersRefresh",
96
+ ADMIN_PACKAGES_REFRESH: "adminPackagesRefresh",
97
+ ADMIN_PACKAGES_NEXT: "adminPackagesNext",
98
+ ADMIN_PACKAGES_PREV: "adminPackagesPrev",
92
99
  ADMIN_BEGIN_EDIT_GHCP_KEY: "adminBeginEditGhcpKey",
93
100
  ADMIN_CANCEL_EDIT_GHCP_KEY: "adminCancelEditGhcpKey",
94
101
  ADMIN_SAVE_GHCP_KEY: "adminSaveGhcpKey",