@pi-harness/pi-harness 0.1.30 → 0.1.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 (288) hide show
  1. package/README.md +1 -1
  2. package/apps/web/dist/assets/index-BqOM0elx.css +2 -0
  3. package/apps/web/dist/assets/index-x2UhIAWm.js +25 -0
  4. package/apps/web/dist/index.html +2 -2
  5. package/apps/web/package.json +1 -1
  6. package/apps/web/profile/cordis.yml +0 -221
  7. package/apps/web/server-dist/bin.js +11 -3
  8. package/apps/web/server-dist/bin.js.map +1 -1
  9. package/apps/web/src/server/bin.ts +10 -2
  10. package/apps/web/src/style.css +30 -1
  11. package/node_modules/@pi-harness/api-gateway/dist/index.d.ts.map +1 -1
  12. package/node_modules/@pi-harness/api-gateway/dist/index.js +70 -18
  13. package/node_modules/@pi-harness/api-gateway/dist/index.js.map +1 -1
  14. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/agent-teams.json +1 -1
  15. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/anchored-standard.json +1 -1
  16. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/annotation.json +1 -1
  17. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/archify.json +1 -1
  18. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/at-file.json +1 -9
  19. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/auto-mode.json +1 -1
  20. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/better-sidebar.json +1 -1
  21. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/browser-fetch.json +1 -8
  22. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/browser-session.json +1 -8
  23. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/canvas-draw.json +2 -2
  24. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/change-verifier.json +1 -1
  25. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/cleaner.json +1 -9
  26. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/cli-notifier.json +1 -1
  27. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/code2skill.json +1 -1
  28. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/colleague-skill.json +1 -1
  29. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/context-doctor.json +1 -8
  30. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/context.json +1 -7
  31. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/cordis-group.json +1 -1
  32. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/cordis-logger-console.json +1 -1
  33. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/cordis-timer.json +1 -1
  34. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/cost-meter.json +1 -1
  35. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/dependency-checker.json +2 -10
  36. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/docker-sandbox.json +1 -9
  37. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/fail-logger.json +1 -8
  38. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/genui.json +1 -1
  39. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/git-time-capsule.json +1 -8
  40. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/graph-memory.json +1 -1
  41. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/history-compressor.json +2 -2
  42. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/hol-guard.json +2 -2
  43. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/i18n-pair.json +1 -7
  44. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/image-compressor.json +2 -2
  45. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/llm-verifier.json +1 -1
  46. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mcp-client.json +2 -8
  47. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mcp-panel.json +1 -1
  48. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/memory.json +1 -1
  49. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mirage-bridge.json +1 -1
  50. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mock-server.json +1 -1
  51. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/modlens.json +1 -1
  52. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/module-search.json +1 -1
  53. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/obsidian-sync.json +1 -1
  54. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/openpets.json +2 -8
  55. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plan-execute.json +1 -1
  56. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-check.json +1 -1
  57. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-dev.json +1 -1
  58. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-finder.json +1 -1
  59. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-radar.json +1 -1
  60. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-stars.json +1 -8
  61. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/prompt-guard.json +2 -2
  62. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/prompt-library.json +1 -1
  63. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/readme-gen.json +1 -9
  64. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/recall-unread.json +1 -8
  65. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/reverse-skill.json +2 -2
  66. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/reviewer-bot.json +1 -1
  67. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/runtime-doctor.json +1 -1
  68. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/secure-audit.json +1 -1
  69. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-bookmarks.json +1 -1
  70. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-bridge.json +1 -9
  71. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-compare.json +1 -1
  72. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-export.json +1 -1
  73. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-insights.json +1 -9
  74. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-search.json +1 -1
  75. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/skill-catalog.json +1 -1
  76. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/skill-guard.json +2 -9
  77. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/sql-lens.json +1 -8
  78. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/synapse.json +1 -1
  79. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/tab-manager.json +1 -1
  80. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/taskboard.json +1 -1
  81. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/telemetry-blocker.json +1 -1
  82. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/test-harness.json +2 -10
  83. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/theme-studio.json +1 -1
  84. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/token-guard.json +1 -9
  85. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/turn-rewind.json +1 -8
  86. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/undo-savepoint.json +1 -1
  87. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/vision-toolkit.json +1 -7
  88. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/web-research.json +1 -1
  89. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/workspace-navigator.json +1 -1
  90. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/workspace-search.json +2 -2
  91. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/yaml-validator.json +1 -7
  92. package/node_modules/@pi-harness/api-gateway/dist/marketplace.d.ts +18 -2
  93. package/node_modules/@pi-harness/api-gateway/dist/marketplace.d.ts.map +1 -1
  94. package/node_modules/@pi-harness/api-gateway/dist/marketplace.js +93 -10
  95. package/node_modules/@pi-harness/api-gateway/dist/marketplace.js.map +1 -1
  96. package/node_modules/@pi-harness/api-gateway/package.json +1 -27
  97. package/node_modules/@pi-harness/cli/dist/main.d.ts.map +1 -1
  98. package/node_modules/@pi-harness/cli/dist/main.js +19 -3
  99. package/node_modules/@pi-harness/cli/dist/main.js.map +1 -1
  100. package/node_modules/@pi-harness/cli/package.json +1 -1
  101. package/node_modules/@pi-harness/cli/profiles/default/cordis.yml +0 -87
  102. package/node_modules/@pi-harness/host-webserver/package.json +1 -1
  103. package/node_modules/@pi-harness/web-app/package.json +1 -1
  104. package/package.json +6 -79
  105. package/packages/api-gateway/dist/index.d.ts.map +1 -1
  106. package/packages/api-gateway/dist/index.js +70 -18
  107. package/packages/api-gateway/dist/index.js.map +1 -1
  108. package/packages/api-gateway/dist/marketplace-entries/official/agent-teams.json +1 -1
  109. package/packages/api-gateway/dist/marketplace-entries/official/anchored-standard.json +1 -1
  110. package/packages/api-gateway/dist/marketplace-entries/official/annotation.json +1 -1
  111. package/packages/api-gateway/dist/marketplace-entries/official/archify.json +1 -1
  112. package/packages/api-gateway/dist/marketplace-entries/official/at-file.json +1 -9
  113. package/packages/api-gateway/dist/marketplace-entries/official/auto-mode.json +1 -1
  114. package/packages/api-gateway/dist/marketplace-entries/official/better-sidebar.json +1 -1
  115. package/packages/api-gateway/dist/marketplace-entries/official/browser-fetch.json +1 -8
  116. package/packages/api-gateway/dist/marketplace-entries/official/browser-session.json +1 -8
  117. package/packages/api-gateway/dist/marketplace-entries/official/canvas-draw.json +2 -2
  118. package/packages/api-gateway/dist/marketplace-entries/official/change-verifier.json +1 -1
  119. package/packages/api-gateway/dist/marketplace-entries/official/cleaner.json +1 -9
  120. package/packages/api-gateway/dist/marketplace-entries/official/cli-notifier.json +1 -1
  121. package/packages/api-gateway/dist/marketplace-entries/official/code2skill.json +1 -1
  122. package/packages/api-gateway/dist/marketplace-entries/official/colleague-skill.json +1 -1
  123. package/packages/api-gateway/dist/marketplace-entries/official/context-doctor.json +1 -8
  124. package/packages/api-gateway/dist/marketplace-entries/official/context.json +1 -7
  125. package/packages/api-gateway/dist/marketplace-entries/official/cordis-group.json +1 -1
  126. package/packages/api-gateway/dist/marketplace-entries/official/cordis-logger-console.json +1 -1
  127. package/packages/api-gateway/dist/marketplace-entries/official/cordis-timer.json +1 -1
  128. package/packages/api-gateway/dist/marketplace-entries/official/cost-meter.json +1 -1
  129. package/packages/api-gateway/dist/marketplace-entries/official/dependency-checker.json +2 -10
  130. package/packages/api-gateway/dist/marketplace-entries/official/docker-sandbox.json +1 -9
  131. package/packages/api-gateway/dist/marketplace-entries/official/fail-logger.json +1 -8
  132. package/packages/api-gateway/dist/marketplace-entries/official/genui.json +1 -1
  133. package/packages/api-gateway/dist/marketplace-entries/official/git-time-capsule.json +1 -8
  134. package/packages/api-gateway/dist/marketplace-entries/official/graph-memory.json +1 -1
  135. package/packages/api-gateway/dist/marketplace-entries/official/history-compressor.json +2 -2
  136. package/packages/api-gateway/dist/marketplace-entries/official/hol-guard.json +2 -2
  137. package/packages/api-gateway/dist/marketplace-entries/official/i18n-pair.json +1 -7
  138. package/packages/api-gateway/dist/marketplace-entries/official/image-compressor.json +2 -2
  139. package/packages/api-gateway/dist/marketplace-entries/official/llm-verifier.json +1 -1
  140. package/packages/api-gateway/dist/marketplace-entries/official/mcp-client.json +2 -8
  141. package/packages/api-gateway/dist/marketplace-entries/official/mcp-panel.json +1 -1
  142. package/packages/api-gateway/dist/marketplace-entries/official/memory.json +1 -1
  143. package/packages/api-gateway/dist/marketplace-entries/official/mirage-bridge.json +1 -1
  144. package/packages/api-gateway/dist/marketplace-entries/official/mock-server.json +1 -1
  145. package/packages/api-gateway/dist/marketplace-entries/official/modlens.json +1 -1
  146. package/packages/api-gateway/dist/marketplace-entries/official/module-search.json +1 -1
  147. package/packages/api-gateway/dist/marketplace-entries/official/obsidian-sync.json +1 -1
  148. package/packages/api-gateway/dist/marketplace-entries/official/openpets.json +2 -8
  149. package/packages/api-gateway/dist/marketplace-entries/official/plan-execute.json +1 -1
  150. package/packages/api-gateway/dist/marketplace-entries/official/plugin-check.json +1 -1
  151. package/packages/api-gateway/dist/marketplace-entries/official/plugin-dev.json +1 -1
  152. package/packages/api-gateway/dist/marketplace-entries/official/plugin-finder.json +1 -1
  153. package/packages/api-gateway/dist/marketplace-entries/official/plugin-radar.json +1 -1
  154. package/packages/api-gateway/dist/marketplace-entries/official/plugin-stars.json +1 -8
  155. package/packages/api-gateway/dist/marketplace-entries/official/prompt-guard.json +2 -2
  156. package/packages/api-gateway/dist/marketplace-entries/official/prompt-library.json +1 -1
  157. package/packages/api-gateway/dist/marketplace-entries/official/readme-gen.json +1 -9
  158. package/packages/api-gateway/dist/marketplace-entries/official/recall-unread.json +1 -8
  159. package/packages/api-gateway/dist/marketplace-entries/official/reverse-skill.json +2 -2
  160. package/packages/api-gateway/dist/marketplace-entries/official/reviewer-bot.json +1 -1
  161. package/packages/api-gateway/dist/marketplace-entries/official/runtime-doctor.json +1 -1
  162. package/packages/api-gateway/dist/marketplace-entries/official/secure-audit.json +1 -1
  163. package/packages/api-gateway/dist/marketplace-entries/official/session-bookmarks.json +1 -1
  164. package/packages/api-gateway/dist/marketplace-entries/official/session-bridge.json +1 -9
  165. package/packages/api-gateway/dist/marketplace-entries/official/session-compare.json +1 -1
  166. package/packages/api-gateway/dist/marketplace-entries/official/session-export.json +1 -1
  167. package/packages/api-gateway/dist/marketplace-entries/official/session-insights.json +1 -9
  168. package/packages/api-gateway/dist/marketplace-entries/official/session-search.json +1 -1
  169. package/packages/api-gateway/dist/marketplace-entries/official/skill-catalog.json +1 -1
  170. package/packages/api-gateway/dist/marketplace-entries/official/skill-guard.json +2 -9
  171. package/packages/api-gateway/dist/marketplace-entries/official/sql-lens.json +1 -8
  172. package/packages/api-gateway/dist/marketplace-entries/official/synapse.json +1 -1
  173. package/packages/api-gateway/dist/marketplace-entries/official/tab-manager.json +1 -1
  174. package/packages/api-gateway/dist/marketplace-entries/official/taskboard.json +1 -1
  175. package/packages/api-gateway/dist/marketplace-entries/official/telemetry-blocker.json +1 -1
  176. package/packages/api-gateway/dist/marketplace-entries/official/test-harness.json +2 -10
  177. package/packages/api-gateway/dist/marketplace-entries/official/theme-studio.json +1 -1
  178. package/packages/api-gateway/dist/marketplace-entries/official/token-guard.json +1 -9
  179. package/packages/api-gateway/dist/marketplace-entries/official/turn-rewind.json +1 -8
  180. package/packages/api-gateway/dist/marketplace-entries/official/undo-savepoint.json +1 -1
  181. package/packages/api-gateway/dist/marketplace-entries/official/vision-toolkit.json +1 -7
  182. package/packages/api-gateway/dist/marketplace-entries/official/web-research.json +1 -1
  183. package/packages/api-gateway/dist/marketplace-entries/official/workspace-navigator.json +1 -1
  184. package/packages/api-gateway/dist/marketplace-entries/official/workspace-search.json +2 -2
  185. package/packages/api-gateway/dist/marketplace-entries/official/yaml-validator.json +1 -7
  186. package/packages/api-gateway/dist/marketplace.d.ts +18 -2
  187. package/packages/api-gateway/dist/marketplace.d.ts.map +1 -1
  188. package/packages/api-gateway/dist/marketplace.js +93 -10
  189. package/packages/api-gateway/dist/marketplace.js.map +1 -1
  190. package/packages/api-gateway/package.json +1 -27
  191. package/packages/api-gateway/src/index.ts +85 -23
  192. package/packages/api-gateway/src/marketplace-entries/official/agent-teams.json +1 -1
  193. package/packages/api-gateway/src/marketplace-entries/official/anchored-standard.json +1 -1
  194. package/packages/api-gateway/src/marketplace-entries/official/annotation.json +1 -1
  195. package/packages/api-gateway/src/marketplace-entries/official/archify.json +1 -1
  196. package/packages/api-gateway/src/marketplace-entries/official/at-file.json +1 -9
  197. package/packages/api-gateway/src/marketplace-entries/official/auto-mode.json +1 -1
  198. package/packages/api-gateway/src/marketplace-entries/official/better-sidebar.json +1 -1
  199. package/packages/api-gateway/src/marketplace-entries/official/browser-fetch.json +1 -8
  200. package/packages/api-gateway/src/marketplace-entries/official/browser-session.json +1 -8
  201. package/packages/api-gateway/src/marketplace-entries/official/canvas-draw.json +2 -2
  202. package/packages/api-gateway/src/marketplace-entries/official/change-verifier.json +1 -1
  203. package/packages/api-gateway/src/marketplace-entries/official/cleaner.json +1 -9
  204. package/packages/api-gateway/src/marketplace-entries/official/cli-notifier.json +1 -1
  205. package/packages/api-gateway/src/marketplace-entries/official/code2skill.json +1 -1
  206. package/packages/api-gateway/src/marketplace-entries/official/colleague-skill.json +1 -1
  207. package/packages/api-gateway/src/marketplace-entries/official/context-doctor.json +1 -8
  208. package/packages/api-gateway/src/marketplace-entries/official/context.json +1 -7
  209. package/packages/api-gateway/src/marketplace-entries/official/cordis-group.json +1 -1
  210. package/packages/api-gateway/src/marketplace-entries/official/cordis-logger-console.json +1 -1
  211. package/packages/api-gateway/src/marketplace-entries/official/cordis-timer.json +1 -1
  212. package/packages/api-gateway/src/marketplace-entries/official/cost-meter.json +1 -1
  213. package/packages/api-gateway/src/marketplace-entries/official/dependency-checker.json +2 -10
  214. package/packages/api-gateway/src/marketplace-entries/official/docker-sandbox.json +1 -9
  215. package/packages/api-gateway/src/marketplace-entries/official/fail-logger.json +1 -8
  216. package/packages/api-gateway/src/marketplace-entries/official/genui.json +1 -1
  217. package/packages/api-gateway/src/marketplace-entries/official/git-time-capsule.json +1 -8
  218. package/packages/api-gateway/src/marketplace-entries/official/graph-memory.json +1 -1
  219. package/packages/api-gateway/src/marketplace-entries/official/history-compressor.json +2 -2
  220. package/packages/api-gateway/src/marketplace-entries/official/hol-guard.json +2 -2
  221. package/packages/api-gateway/src/marketplace-entries/official/i18n-pair.json +1 -7
  222. package/packages/api-gateway/src/marketplace-entries/official/image-compressor.json +2 -2
  223. package/packages/api-gateway/src/marketplace-entries/official/llm-verifier.json +1 -1
  224. package/packages/api-gateway/src/marketplace-entries/official/mcp-client.json +2 -8
  225. package/packages/api-gateway/src/marketplace-entries/official/mcp-panel.json +1 -1
  226. package/packages/api-gateway/src/marketplace-entries/official/memory.json +1 -1
  227. package/packages/api-gateway/src/marketplace-entries/official/mirage-bridge.json +1 -1
  228. package/packages/api-gateway/src/marketplace-entries/official/mock-server.json +1 -1
  229. package/packages/api-gateway/src/marketplace-entries/official/modlens.json +1 -1
  230. package/packages/api-gateway/src/marketplace-entries/official/module-search.json +1 -1
  231. package/packages/api-gateway/src/marketplace-entries/official/obsidian-sync.json +1 -1
  232. package/packages/api-gateway/src/marketplace-entries/official/openpets.json +2 -8
  233. package/packages/api-gateway/src/marketplace-entries/official/plan-execute.json +1 -1
  234. package/packages/api-gateway/src/marketplace-entries/official/plugin-check.json +1 -1
  235. package/packages/api-gateway/src/marketplace-entries/official/plugin-dev.json +1 -1
  236. package/packages/api-gateway/src/marketplace-entries/official/plugin-finder.json +1 -1
  237. package/packages/api-gateway/src/marketplace-entries/official/plugin-radar.json +1 -1
  238. package/packages/api-gateway/src/marketplace-entries/official/plugin-stars.json +1 -8
  239. package/packages/api-gateway/src/marketplace-entries/official/prompt-guard.json +2 -2
  240. package/packages/api-gateway/src/marketplace-entries/official/prompt-library.json +1 -1
  241. package/packages/api-gateway/src/marketplace-entries/official/readme-gen.json +1 -9
  242. package/packages/api-gateway/src/marketplace-entries/official/recall-unread.json +1 -8
  243. package/packages/api-gateway/src/marketplace-entries/official/reverse-skill.json +2 -2
  244. package/packages/api-gateway/src/marketplace-entries/official/reviewer-bot.json +1 -1
  245. package/packages/api-gateway/src/marketplace-entries/official/runtime-doctor.json +1 -1
  246. package/packages/api-gateway/src/marketplace-entries/official/secure-audit.json +1 -1
  247. package/packages/api-gateway/src/marketplace-entries/official/session-bookmarks.json +1 -1
  248. package/packages/api-gateway/src/marketplace-entries/official/session-bridge.json +1 -9
  249. package/packages/api-gateway/src/marketplace-entries/official/session-compare.json +1 -1
  250. package/packages/api-gateway/src/marketplace-entries/official/session-export.json +1 -1
  251. package/packages/api-gateway/src/marketplace-entries/official/session-insights.json +1 -9
  252. package/packages/api-gateway/src/marketplace-entries/official/session-search.json +1 -1
  253. package/packages/api-gateway/src/marketplace-entries/official/skill-catalog.json +1 -1
  254. package/packages/api-gateway/src/marketplace-entries/official/skill-guard.json +2 -9
  255. package/packages/api-gateway/src/marketplace-entries/official/sql-lens.json +1 -8
  256. package/packages/api-gateway/src/marketplace-entries/official/synapse.json +1 -1
  257. package/packages/api-gateway/src/marketplace-entries/official/tab-manager.json +1 -1
  258. package/packages/api-gateway/src/marketplace-entries/official/taskboard.json +1 -1
  259. package/packages/api-gateway/src/marketplace-entries/official/telemetry-blocker.json +1 -1
  260. package/packages/api-gateway/src/marketplace-entries/official/test-harness.json +2 -10
  261. package/packages/api-gateway/src/marketplace-entries/official/theme-studio.json +1 -1
  262. package/packages/api-gateway/src/marketplace-entries/official/token-guard.json +1 -9
  263. package/packages/api-gateway/src/marketplace-entries/official/turn-rewind.json +1 -8
  264. package/packages/api-gateway/src/marketplace-entries/official/undo-savepoint.json +1 -1
  265. package/packages/api-gateway/src/marketplace-entries/official/vision-toolkit.json +1 -7
  266. package/packages/api-gateway/src/marketplace-entries/official/web-research.json +1 -1
  267. package/packages/api-gateway/src/marketplace-entries/official/workspace-navigator.json +1 -1
  268. package/packages/api-gateway/src/marketplace-entries/official/workspace-search.json +2 -2
  269. package/packages/api-gateway/src/marketplace-entries/official/yaml-validator.json +1 -7
  270. package/packages/api-gateway/src/marketplace.ts +113 -12
  271. package/packages/api-gateway/test/index.test.ts +297 -5
  272. package/packages/api-gateway/test/marketplace-schema.test.ts +12 -0
  273. package/packages/api-gateway/test/marketplace.test.ts +172 -240
  274. package/packages/api-gateway/test/packed-package.test.ts +18 -6
  275. package/packages/api-gateway/test/release-contract.test.ts +8 -4
  276. package/packages/bundle-web-app/package.json +1 -1
  277. package/packages/cli/dist/main.d.ts.map +1 -1
  278. package/packages/cli/dist/main.js +19 -3
  279. package/packages/cli/dist/main.js.map +1 -1
  280. package/packages/cli/package.json +1 -1
  281. package/packages/cli/profiles/default/cordis.yml +0 -87
  282. package/packages/cli/src/main.ts +18 -3
  283. package/packages/cli/test/cli.test.ts +20 -0
  284. package/packages/cli/test/profiles/plugins/cordis.yml +122 -0
  285. package/packages/cli/test/profiles.integration.test.ts +40 -23
  286. package/packages/host-webserver/package.json +1 -1
  287. package/apps/web/dist/assets/index-B7pwZ76Z.js +0 -25
  288. package/apps/web/dist/assets/index-xF647pQJ.css +0 -2
@@ -29,6 +29,8 @@ export interface MarketplaceStatistics {
29
29
  export type MarketplaceStatisticsFetcher = (url: string, init?: RequestInit) => Promise<Response>;
30
30
 
31
31
  export interface MarketplaceStatisticsLoaderOptions {
32
+ readonly concurrency?: number;
33
+ readonly failureMaxTtlMs?: number;
32
34
  readonly failureTtlMs?: number;
33
35
  readonly fetcher?: MarketplaceStatisticsFetcher;
34
36
  readonly now?: () => number;
@@ -36,6 +38,18 @@ export interface MarketplaceStatisticsLoaderOptions {
36
38
  readonly ttlMs?: number;
37
39
  }
38
40
 
41
+ export interface MarketplaceCachedStatistics {
42
+ /** False until every catalogued package has been looked up once. A caller that reorders on statistics uses this to reorder once, when the set is complete, rather than on every poll while the background warm-up trickles in. */
43
+ readonly ready: boolean;
44
+ readonly statistics: ReadonlyMap<string, MarketplaceStatistics>;
45
+ }
46
+
47
+ export interface MarketplaceStatisticsLoader {
48
+ (plugins: readonly MarketplacePlugin[]): Promise<ReadonlyMap<string, MarketplaceStatistics>>;
49
+ // Returns only what is already cached and warms the misses in the background, so a request never waits on the npm registry.
50
+ readonly readCached: (plugins: readonly MarketplacePlugin[]) => MarketplaceCachedStatistics;
51
+ }
52
+
39
53
  interface NpmPackageStatisticsResult {
40
54
  readonly failed: boolean;
41
55
  readonly statistics?: MarketplaceStatistics;
@@ -55,6 +69,36 @@ export interface MarketplaceCategory {
55
69
  readonly count: number;
56
70
  }
57
71
 
72
+ export interface MarketplaceCapability {
73
+ readonly id: string;
74
+ readonly label: string;
75
+ readonly count: number;
76
+ }
77
+
78
+ /** What a plugin does to the machine it is installed on, ordered from the least invasive to the most, so a reader can decide from the badges alone. This replaced free text, which produced 299 values across 78 entries with 284 of them held by a single plugin: the filter listed nearly one option per plugin and nothing could be compared against anything else. Every value here is derived from the plugin's own imports, including those it inherits from a plugin it depends on. */
79
+ const MARKETPLACE_CAPABILITY_VOCABULARY: readonly { readonly id: string; readonly label: string }[] = [
80
+ { id: "read-only", label: "只读运行" },
81
+ { id: "session-data", label: "读取会话内容" },
82
+ { id: "reads-files", label: "读取本机文件" },
83
+ { id: "writes-files", label: "写入本机文件" },
84
+ { id: "runs-commands", label: "执行本机命令" },
85
+ { id: "local-server", label: "监听本地端口" },
86
+ { id: "network-access", label: "访问网络" },
87
+ { id: "model-calls", label: "额外调用模型" },
88
+ ];
89
+
90
+ /** A badge that says a plugin changes nothing is a claim about trust, so an entry that also carries one of these is rejected rather than shown. */
91
+ const MARKETPLACE_EFFECT_CAPABILITIES: readonly string[] = ["writes-files", "runs-commands", "local-server", "network-access", "model-calls"];
92
+
93
+ const marketplaceCapabilityLabels = new Map(MARKETPLACE_CAPABILITY_VOCABULARY.map((capability) => [capability.id, capability.label]));
94
+
95
+ function isMarketplaceCapabilityList(value: unknown): value is readonly string[] {
96
+ if (!Array.isArray(value) || value.length === 0) return false;
97
+ const capabilities = value.filter((entry): entry is string => typeof entry === "string" && marketplaceCapabilityLabels.has(entry));
98
+ if (capabilities.length !== value.length || new Set(capabilities).size !== capabilities.length) return false;
99
+ return !capabilities.includes("read-only") || !capabilities.some((entry) => MARKETPLACE_EFFECT_CAPABILITIES.includes(entry));
100
+ }
101
+
58
102
  const npmPackagePattern = /^(?:@[a-z0-9._~-]+\/)?[a-z0-9._~-]+(?:\/[a-z0-9._~-]+)*$/;
59
103
  const entryIdPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
60
104
  const versionPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
@@ -92,9 +136,7 @@ function isMarketplacePlugin(value: unknown): value is MarketplacePlugin {
92
136
  categoryIdPattern.test(category.id) &&
93
137
  typeof category.label === "string" &&
94
138
  category.label.trim() !== "" &&
95
- Array.isArray(value.capabilities) &&
96
- value.capabilities.length > 0 &&
97
- value.capabilities.every((entry) => typeof entry === "string" && entry.trim() !== "") &&
139
+ isMarketplaceCapabilityList(value.capabilities) &&
98
140
  Array.isArray(value.hooks) &&
99
141
  value.hooks.length > 0 &&
100
142
  value.hooks.every((entry) => typeof entry === "string" && entry.trim() !== "") &&
@@ -129,11 +171,17 @@ function loadMarketplacePlugins(): readonly MarketplacePlugin[] {
129
171
  });
130
172
  const ids = new Set<string>();
131
173
  const packages = new Set<string>();
174
+ // The category tabs are built from whichever entry is read first, so two entries that spell the same category differently would make the label depend on file order.
175
+ const categoryLabels = new Map<string, string>();
132
176
  for (const plugin of plugins) {
133
177
  if (ids.has(plugin.id)) throw new Error(`Duplicate marketplace id: ${plugin.id}`);
134
178
  if (packages.has(plugin.packageName)) throw new Error(`Duplicate marketplace package: ${plugin.packageName}`);
179
+ const categoryLabel = categoryLabels.get(plugin.category.id);
180
+ if (categoryLabel !== undefined && categoryLabel !== plugin.category.label)
181
+ throw new Error(`Conflicting marketplace category label for ${plugin.category.id}: ${categoryLabel} and ${plugin.category.label}`);
135
182
  ids.add(plugin.id);
136
183
  packages.add(plugin.packageName);
184
+ categoryLabels.set(plugin.category.id, plugin.category.label);
137
185
  }
138
186
  return plugins;
139
187
  }
@@ -199,35 +247,82 @@ async function fetchNpmPackageStatistics(packageName: string, fetcher: Marketpla
199
247
  return { failed, statistics };
200
248
  }
201
249
 
202
- export function createMarketplaceStatisticsLoader(options: MarketplaceStatisticsLoaderOptions = {}) {
250
+ export function createMarketplaceStatisticsLoader(options: MarketplaceStatisticsLoaderOptions = {}): MarketplaceStatisticsLoader {
203
251
  const fetcher = options.fetcher ?? ((url: string, init?: RequestInit) => globalThis.fetch(url, init));
204
252
  const failureTtlMs = options.failureTtlMs ?? 60_000;
253
+ // Consecutive failures double the retry window up to this ceiling: an open console polls the catalog every few seconds, so a flat failure window would send one registry lookup per catalogued package every minute for as long as the tab stays open.
254
+ const failureMaxTtlMs = options.failureMaxTtlMs ?? 30 * 60_000;
205
255
  const now = options.now ?? Date.now;
206
256
  const timeoutMs = options.timeoutMs ?? 3_000;
207
257
  const ttlMs = options.ttlMs ?? 6 * 60 * 60 * 1_000;
208
- const cache = new Map<string, { readonly expiresAt: number; readonly result: NpmPackageStatisticsResult }>();
258
+ // The registry answers an unthrottled fan-out over the whole catalogue with 429s, so lookups queue behind a small number of slots instead of leaving at once.
259
+ const concurrency = Math.max(1, Math.trunc(options.concurrency ?? 4));
260
+ const cache = new Map<string, { readonly expiresAt: number; readonly failures: number; readonly result: NpmPackageStatisticsResult }>();
209
261
  const inFlight = new Map<string, Promise<NpmPackageStatisticsResult>>();
262
+ const waiting: (() => void)[] = [];
263
+ let active = 0;
264
+ const acquireSlot = async (): Promise<void> => {
265
+ if (active < concurrency) {
266
+ active += 1;
267
+ return;
268
+ }
269
+ await new Promise<void>((resolveSlot) => waiting.push(resolveSlot));
270
+ };
271
+ const releaseSlot = (): void => {
272
+ const next = waiting.shift();
273
+ if (next === undefined) active -= 1;
274
+ else next();
275
+ };
276
+ const failureTtl = (failures: number): number => Math.min(failureTtlMs * 2 ** Math.min(failures - 1, 20), failureMaxTtlMs);
210
277
  const loadPackage = async (packageName: string): Promise<NpmPackageStatisticsResult> => {
211
278
  const timestamp = now();
212
279
  const cached = cache.get(packageName);
213
280
  if (cached !== undefined && cached.expiresAt > timestamp) return cached.result;
214
281
  const pending = inFlight.get(packageName);
215
282
  if (pending !== undefined) return pending;
216
- const request = fetchNpmPackageStatistics(packageName, fetcher, timeoutMs);
283
+ // The slot is taken inside the promise so the in-flight entry is still registered synchronously and concurrent callers keep sharing one request.
284
+ const request = (async () => {
285
+ await acquireSlot();
286
+ try {
287
+ return await fetchNpmPackageStatistics(packageName, fetcher, timeoutMs);
288
+ } finally {
289
+ releaseSlot();
290
+ }
291
+ })();
217
292
  inFlight.set(packageName, request);
218
293
  try {
219
294
  const result = await request;
220
- cache.set(packageName, { expiresAt: now() + (result.failed ? failureTtlMs : ttlMs), result });
295
+ const failures = result.failed ? (cached?.failures ?? 0) + 1 : 0;
296
+ cache.set(packageName, { expiresAt: now() + (result.failed ? failureTtl(failures) : ttlMs), failures, result });
221
297
  return result;
222
298
  } finally {
223
299
  if (inFlight.get(packageName) === request) inFlight.delete(packageName);
224
300
  }
225
301
  };
226
- return async (plugins: readonly MarketplacePlugin[]): Promise<ReadonlyMap<string, MarketplaceStatistics>> => {
227
- const packageNames = [...new Set(plugins.map((plugin) => marketplaceNpmPackageName(plugin.packageName)))].sort();
228
- const entries = await Promise.all(packageNames.map(async (packageName) => [packageName, await loadPackage(packageName)] as const));
302
+ const packageNames = (plugins: readonly MarketplacePlugin[]): readonly string[] =>
303
+ [...new Set(plugins.map((plugin) => marketplaceNpmPackageName(plugin.packageName)))].sort();
304
+ const load = async (plugins: readonly MarketplacePlugin[]): Promise<ReadonlyMap<string, MarketplaceStatistics>> => {
305
+ const entries = await Promise.all(packageNames(plugins).map(async (packageName) => [packageName, await loadPackage(packageName)] as const));
229
306
  return new Map(entries.flatMap(([packageName, result]) => (result.statistics === undefined ? [] : [[packageName, result.statistics] as const])));
230
307
  };
308
+ const readCached = (plugins: readonly MarketplacePlugin[]): MarketplaceCachedStatistics => {
309
+ const timestamp = now();
310
+ const statistics = new Map<string, MarketplaceStatistics>();
311
+ // Readiness tracks whether a package has ever been looked up, not whether its entry is fresh, so an expiring entry refreshes in the background without dropping the catalogue back to an unsorted order.
312
+ let ready = true;
313
+ for (const packageName of packageNames(plugins)) {
314
+ const cached = cache.get(packageName);
315
+ if (cached === undefined) ready = false;
316
+ if (cached !== undefined && cached.expiresAt > timestamp) {
317
+ if (cached.result.statistics !== undefined) statistics.set(packageName, cached.result.statistics);
318
+ continue;
319
+ }
320
+ // A prewarm has nobody to report to, and a catalogue sorted without npm statistics is not worth crashing the process over an unhandled rejection.
321
+ void loadPackage(packageName).catch(() => {});
322
+ }
323
+ return { ready, statistics };
324
+ };
325
+ return Object.assign(load, { readCached });
231
326
  }
232
327
 
233
328
  export function attachMarketplaceStatistics(
@@ -259,7 +354,9 @@ export function searchMarketplace(query = "", capability = "", category = ""): r
259
354
  const normalizedCapability = capability.trim().toLowerCase();
260
355
  const normalizedCategory = category.trim().toLowerCase();
261
356
  return MARKETPLACE_PLUGINS.filter((plugin) => {
262
- const searchable = [plugin.name, plugin.packageName, plugin.description, plugin.author, ...plugin.capabilities, ...plugin.hooks].join(" ").toLowerCase();
357
+ // The capability labels are searched alongside their ids: the ids are what the filter and the URL carry, and the labels are what the reader sees on the card.
358
+ const capabilities = plugin.capabilities.flatMap((item) => [item, marketplaceCapabilityLabels.get(item) ?? item]);
359
+ const searchable = [plugin.name, plugin.packageName, plugin.description, plugin.author, ...capabilities, ...plugin.hooks].join(" ").toLowerCase();
263
360
  return (
264
361
  (normalizedQuery === "" || searchable.includes(normalizedQuery)) &&
265
362
  (normalizedCapability === "" || plugin.capabilities.some((item) => item.toLowerCase() === normalizedCapability)) &&
@@ -281,7 +378,11 @@ export function paginateMarketplace(items: readonly MarketplacePlugin[], page =
281
378
  };
282
379
  }
283
380
 
284
- export const MARKETPLACE_CAPABILITIES = [...new Set(MARKETPLACE_PLUGINS.flatMap((plugin) => plugin.capabilities))].sort();
381
+ // Kept in vocabulary order rather than sorted, so the filter reads from the least invasive option to the most, and an unused capability is left out rather than offering a filter that returns nothing.
382
+ export const MARKETPLACE_CAPABILITIES: readonly MarketplaceCapability[] = MARKETPLACE_CAPABILITY_VOCABULARY.map((capability) => ({
383
+ ...capability,
384
+ count: MARKETPLACE_PLUGINS.filter((plugin) => plugin.capabilities.includes(capability.id)).length,
385
+ })).filter((capability) => capability.count > 0);
285
386
  export const MARKETPLACE_CATEGORIES: readonly MarketplaceCategory[] = [
286
387
  ...new Map(MARKETPLACE_PLUGINS.map((plugin) => [plugin.category.id, { id: plugin.category.id, label: plugin.category.label, count: 0 }])).values(),
287
388
  ]
@@ -10,7 +10,7 @@ import timerPlugin from "@deepseek-ai/cordis-plugin-timer";
10
10
  import { SessionManager } from "@earendil-works/pi-coding-agent";
11
11
  import { afterEach, describe, expect, test, vi } from "vitest";
12
12
  import webServerPlugin from "@pi-harness/host-webserver";
13
- import { PiPluginUiRegistry, PiToolRegistry } from "@pi-harness/core";
13
+ import { PiPluginUiRegistry, PiToolRegistry, PiToolRegistryLeasedError } from "@pi-harness/core";
14
14
  import type { MarketplacePlugin } from "../src/marketplace.js";
15
15
  import apiPlugin from "../src/index.js";
16
16
  import type * as FsPromises from "node:fs/promises";
@@ -2122,11 +2122,11 @@ describe("API gateway plugin", () => {
2122
2122
  context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
2123
2123
  await context.plugin(apiPlugin);
2124
2124
 
2125
- const response = await fetch(context.webServer.url + "/api/marketplace?q=timer&capability=scheduling&category=runtime&page=0&pageSize=1");
2125
+ const response = await fetch(context.webServer.url + "/api/marketplace?q=timer&capability=read-only&category=runtime&page=0&pageSize=1");
2126
2126
  expect(response.status).toBe(200);
2127
2127
  const payload = (await response.json()) as {
2128
2128
  items?: readonly { packageName?: unknown; status?: unknown }[];
2129
- capabilities?: readonly unknown[];
2129
+ capabilities?: readonly { id?: unknown; label?: unknown; count?: unknown }[];
2130
2130
  categories?: readonly { id?: unknown; label?: unknown; count?: unknown }[];
2131
2131
  total?: number;
2132
2132
  page?: number;
@@ -2136,7 +2136,7 @@ describe("API gateway plugin", () => {
2136
2136
  expect(payload.items).toHaveLength(1);
2137
2137
  expect(payload.items?.[0]).toMatchObject({ packageName: "@deepseek-ai/cordis-plugin-timer", status: "verified" });
2138
2138
  expect(payload).toMatchObject({ total: 1, page: 0, pageSize: 1, hasNext: false });
2139
- expect(payload.capabilities).toContain("scheduling");
2139
+ expect(payload.capabilities).toEqual(expect.arrayContaining([expect.objectContaining({ id: "read-only", label: "只读运行" })]));
2140
2140
  expect(payload.categories).toEqual(expect.arrayContaining([expect.objectContaining({ id: "runtime", label: "运行时", count: 1 })]));
2141
2141
  const tooLong = await fetch(context.webServer.url + "/api/marketplace?q=" + "x".repeat(121));
2142
2142
  expect(tooLong.status).toBe(400);
@@ -2146,6 +2146,89 @@ describe("API gateway plugin", () => {
2146
2146
  expect(invalidSort.status).toBe(400);
2147
2147
  });
2148
2148
 
2149
+ // The console renders nothing until every one of its startup requests has answered, so the recommended sort is served from the npm statistics already cached and the misses are warmed behind the response. A registry that never answers must cost the page nothing.
2150
+ test("answers the recommended marketplace sort while the npm registry never replies", async () => {
2151
+ const context = new Context();
2152
+ contexts.push(context);
2153
+ await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
2154
+ const session = { sessionId: "marketplace-sort-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
2155
+ context.provide("piRuntime", { session, prompt: () => Promise.resolve(), abort: () => Promise.resolve(), dispose: () => Promise.resolve() } as never);
2156
+ context.provide("piModels", { model: { provider: "test", id: "model" }, runtime: { getModels: () => [], getModel: () => undefined } } as never);
2157
+ context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
2158
+ await context.plugin(apiPlugin);
2159
+ const originalFetch = globalThis.fetch;
2160
+ const registryRequests: string[] = [];
2161
+ globalThis.fetch = (input: RequestInfo | URL, init?: RequestInit) => {
2162
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
2163
+ if (url.startsWith("https://registry.npmjs.org/") || url.startsWith("https://api.npmjs.org/")) {
2164
+ registryRequests.push(url);
2165
+ return new Promise<Response>(() => {});
2166
+ }
2167
+ return originalFetch(input, init);
2168
+ };
2169
+ try {
2170
+ const response = await fetch(context.webServer.url + "/api/marketplace?sort=recommended&pageSize=2");
2171
+
2172
+ expect(response.status).toBe(200);
2173
+ const payload = (await response.json()) as { items?: readonly unknown[]; page?: number; pageSize?: number };
2174
+ expect(payload.items).toHaveLength(2);
2175
+ expect(payload).toMatchObject({ page: 0, pageSize: 2 });
2176
+ // The background prewarm queues behind a handful of slots instead of asking the registry about every catalogued package at once.
2177
+ await vi.waitFor(() => expect(registryRequests.length).toBeGreaterThan(0));
2178
+ expect(registryRequests.length).toBeLessThanOrEqual(4);
2179
+ } finally {
2180
+ globalThis.fetch = originalFetch;
2181
+ }
2182
+ });
2183
+
2184
+ // The message goes straight to a console user who clicked a button, so it reads as npm's own report; the command line stays on the error cause for the log.
2185
+ test("reports a failed plugin install in npm's own words without the command line", async () => {
2186
+ const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-plugin-install-failure-"));
2187
+ temporaryDirectories.push(directory);
2188
+ const shimDirectory = join(directory, "bin");
2189
+ await mkdir(shimDirectory);
2190
+ await writeFile(join(directory, "package.json"), '{ "name": "harness" }\n', "utf8");
2191
+ const configPath = join(directory, "profile.yml");
2192
+ const profileBefore = '- id: runtime\n name: "@pi-harness/core/plugins/runtime"\n config: {}\n';
2193
+ await writeFile(configPath, profileBefore, "utf8");
2194
+ await writeFile(
2195
+ join(shimDirectory, "npm"),
2196
+ "#!/bin/sh\nprintf 'npm error code E404\\nnpm error 404 Not Found - GET https://registry.npmjs.org/@pi-harness/plugin-skill-guard\\nnpm error 404 The package is not in the npm registry\\nnpm error A complete log of this run can be found in: /tmp/npm-debug.log\\n' >&2\nexit 1\n",
2197
+ { mode: 0o755 },
2198
+ );
2199
+ const context = new Context();
2200
+ contexts.push(context);
2201
+ await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
2202
+ const session = { sessionId: "install-failure-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
2203
+ context.provide("piRuntime", { session, prompt: () => Promise.resolve(), abort: () => Promise.resolve(), dispose: () => Promise.resolve() } as never);
2204
+ context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
2205
+ context.provide("piHarnessLaunch", { cwd: directory, agentDir: directory, configPath, args: [], requestExit() {} });
2206
+ context.reflect.provide("loader", { entries: () => [], create: () => Promise.resolve("marketplace-entry"), remove: () => Promise.resolve() });
2207
+ await context.plugin(apiPlugin);
2208
+
2209
+ const originalPath = process.env.PATH ?? "";
2210
+ process.env.PATH = `${shimDirectory}:${originalPath}`;
2211
+ let response: Response;
2212
+ try {
2213
+ response = await fetch(context.webServer.url + "/api/marketplace/install", {
2214
+ method: "POST",
2215
+ headers: { "content-type": "application/json" },
2216
+ body: JSON.stringify({ id: "skill-guard" }),
2217
+ });
2218
+ } finally {
2219
+ process.env.PATH = originalPath;
2220
+ }
2221
+
2222
+ expect(response.status).toBe(502);
2223
+ const payload = (await response.json()) as { error?: string };
2224
+ expect(payload.error).toBe(
2225
+ "npm error code E404 npm error 404 Not Found - GET https://registry.npmjs.org/@pi-harness/plugin-skill-guard npm error 404 The package is not in the npm registry",
2226
+ );
2227
+ expect(payload.error).not.toContain("--save-exact");
2228
+ expect(payload.error).not.toContain("npm-debug.log");
2229
+ await expect(readFile(configPath, "utf8")).resolves.toBe(profileBefore);
2230
+ });
2231
+
2149
2232
  // An official plugin is its own npm package, so it takes the same install path a community package takes: npm first, then the profile row, then the loader entry. Nothing about `source: "official"` shortcuts any of it.
2150
2233
  test("installs an official plugin through npm exactly like a community package", async () => {
2151
2234
  const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-plugin-install-"));
@@ -2190,12 +2273,167 @@ describe("API gateway plugin", () => {
2190
2273
  }
2191
2274
 
2192
2275
  expect(response.status).toBe(200);
2193
- await expect(response.json()).resolves.toMatchObject({ installed: true, plugin: { id: "skill-guard" } });
2276
+ await expect(response.json()).resolves.toMatchObject({ installed: true, restartRequired: false, plugin: { id: "skill-guard" } });
2194
2277
  expect((await readFile(npmLog, "utf8")).trim()).toMatch(/^install --save-exact --package-lock=false @pi-harness\/plugin-skill-guard@\d/u);
2195
2278
  expect(created).toEqual([expect.objectContaining({ name: "@pi-harness/plugin-skill-guard" })]);
2196
2279
  await expect(readFile(configPath, "utf8")).resolves.toContain('name: "@pi-harness/plugin-skill-guard"');
2197
2280
  });
2198
2281
 
2282
+ // A plugin registers its tools while it activates, and pi-runtime takes the tool registry when it activates, so an installed entry only works if it sits ahead of the runtime inside the runtime's own group.
2283
+ test("installs a marketplace plugin into the runtime's group ahead of the runtime", async () => {
2284
+ const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-plugin-placement-"));
2285
+ temporaryDirectories.push(directory);
2286
+ const shimDirectory = join(directory, "bin");
2287
+ await mkdir(shimDirectory);
2288
+ await writeFile(join(directory, "package.json"), '{ "name": "harness" }\n', "utf8");
2289
+ const configPath = join(directory, "profile.yml");
2290
+ await writeFile(
2291
+ configPath,
2292
+ '- id: agent\n name: cordis:group\n group: true\n config:\n - id: tools\n name: "@pi-harness/core/plugins/tools"\n config: {}\n - id: runtime\n name: "@pi-harness/core/plugins/runtime"\n config: {}\n',
2293
+ "utf8",
2294
+ );
2295
+ await writeFile(join(shimDirectory, "npm"), "#!/bin/sh\nexit 0\n", { mode: 0o755 });
2296
+ const context = new Context();
2297
+ contexts.push(context);
2298
+ await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
2299
+ const session = { sessionId: "marketplace-placement-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
2300
+ context.provide("piRuntime", { session, prompt: () => Promise.resolve(), abort: () => Promise.resolve(), dispose: () => Promise.resolve() } as never);
2301
+ context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
2302
+ context.provide("piHarnessLaunch", { cwd: directory, agentDir: directory, configPath, args: [], requestExit() {} });
2303
+ const runtimeOptions = { id: "runtime", name: "@pi-harness/core/plugins/runtime", config: {} };
2304
+ const runtimeGroup = { data: [{ id: "tools", name: "@pi-harness/core/plugins/tools", config: {} }, runtimeOptions] };
2305
+ const loaderEntries = [
2306
+ // The loader addresses a nested group by its qualified id, while the profile file names the same entry by its own id.
2307
+ { id: "profile:agent", options: { id: "agent", name: "cordis:group" }, subgroup: runtimeGroup },
2308
+ { id: "profile:runtime", options: runtimeOptions, parent: runtimeGroup },
2309
+ ];
2310
+ const created: Array<[unknown, unknown, unknown]> = [];
2311
+ context.reflect.provide("loader", {
2312
+ entries: () => loaderEntries,
2313
+ create: (options: unknown, parent: unknown, position: unknown) => {
2314
+ created.push([options, parent, position]);
2315
+ return Promise.resolve("marketplace-entry");
2316
+ },
2317
+ resolve: () => ({ fiber: { await: () => Promise.resolve() } }),
2318
+ remove: () => Promise.resolve(),
2319
+ });
2320
+ await context.plugin(apiPlugin);
2321
+
2322
+ const originalPath = process.env.PATH ?? "";
2323
+ process.env.PATH = `${shimDirectory}:${originalPath}`;
2324
+ let response: Response;
2325
+ try {
2326
+ response = await fetch(context.webServer.url + "/api/marketplace/install", {
2327
+ method: "POST",
2328
+ headers: { "content-type": "application/json" },
2329
+ body: JSON.stringify({ id: "skill-guard" }),
2330
+ });
2331
+ } finally {
2332
+ process.env.PATH = originalPath;
2333
+ }
2334
+
2335
+ expect(response.status).toBe(200);
2336
+ await expect(response.json()).resolves.toMatchObject({ installed: true, restartRequired: false });
2337
+ expect(created).toEqual([[expect.objectContaining({ id: "marketplace-skill-guard", name: "@pi-harness/plugin-skill-guard" }), "profile:agent", 1]]);
2338
+ await expect(readFile(configPath, "utf8")).resolves.toBe(
2339
+ '- id: agent\n name: cordis:group\n group: true\n config:\n - id: tools\n name: "@pi-harness/core/plugins/tools"\n config: {}\n - id: marketplace-skill-guard\n name: "@pi-harness/plugin-skill-guard"\n config: {}\n - id: runtime\n name: "@pi-harness/core/plugins/runtime"\n config: {}\n',
2340
+ );
2341
+ });
2342
+
2343
+ // The runtime snapshots the tool set when it takes the registry, so a plugin that contributes tools cannot join a harness that is already running. Rolling the install back would leave the user unable to install it at all, so the package and the profile row stay and the console asks for a restart.
2344
+ test("keeps an installed plugin in place when the runtime already leased the tool registry", async () => {
2345
+ const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-plugin-leased-"));
2346
+ temporaryDirectories.push(directory);
2347
+ const shimDirectory = join(directory, "bin");
2348
+ await mkdir(shimDirectory);
2349
+ await writeFile(join(directory, "package.json"), '{ "name": "harness", "dependencies": {} }\n', "utf8");
2350
+ const configPath = join(directory, "profile.yml");
2351
+ await writeFile(configPath, '- id: runtime\n name: "@pi-harness/core/plugins/runtime"\n config: {}\n', "utf8");
2352
+ await writeFile(join(shimDirectory, "npm"), "#!/bin/sh\nexit 0\n", { mode: 0o755 });
2353
+ const context = new Context();
2354
+ contexts.push(context);
2355
+ await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
2356
+ const session = { sessionId: "marketplace-leased-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
2357
+ context.provide("piRuntime", { session, prompt: () => Promise.resolve(), abort: () => Promise.resolve(), dispose: () => Promise.resolve() } as never);
2358
+ context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
2359
+ context.provide("piHarnessLaunch", { cwd: directory, agentDir: directory, configPath, args: [], requestExit() {} });
2360
+ const removed: string[] = [];
2361
+ context.reflect.provide("loader", {
2362
+ entries: () => [],
2363
+ create: () => Promise.resolve("marketplace-entry"),
2364
+ // The loader wraps an activation failure in its own error chain, which is what the gateway has to see through.
2365
+ resolve: () => ({
2366
+ fiber: {
2367
+ await: () => Promise.reject(new Error("failed to apply loader entry", { cause: new PiToolRegistryLeasedError("skill_scan") })),
2368
+ },
2369
+ }),
2370
+ remove: (id: string) => {
2371
+ removed.push(id);
2372
+ return Promise.resolve();
2373
+ },
2374
+ });
2375
+ await context.plugin(apiPlugin);
2376
+
2377
+ const originalPath = process.env.PATH ?? "";
2378
+ process.env.PATH = `${shimDirectory}:${originalPath}`;
2379
+ let response: Response;
2380
+ try {
2381
+ response = await fetch(context.webServer.url + "/api/marketplace/install", {
2382
+ method: "POST",
2383
+ headers: { "content-type": "application/json" },
2384
+ body: JSON.stringify({ id: "skill-guard" }),
2385
+ });
2386
+ } finally {
2387
+ process.env.PATH = originalPath;
2388
+ }
2389
+
2390
+ expect(response.status).toBe(200);
2391
+ await expect(response.json()).resolves.toMatchObject({ installed: true, restartRequired: true, plugin: { id: "skill-guard" } });
2392
+ expect(removed).toEqual(["marketplace-entry"]);
2393
+ await expect(readFile(configPath, "utf8")).resolves.toContain('name: "@pi-harness/plugin-skill-guard"');
2394
+ await expect(readFile(join(directory, "package.json"), "utf8")).resolves.toBe('{ "name": "harness", "dependencies": {} }\n');
2395
+ });
2396
+
2397
+ // Reverting the profile here would leave a plugin that can be switched off but never on again, so the change stays and the plugin comes back on the next start.
2398
+ test("keeps a re-enabled plugin enabled in the profile when the runtime already leased the tool registry", async () => {
2399
+ const context = new Context();
2400
+ contexts.push(context);
2401
+ const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-plugin-toggle-leased-"));
2402
+ temporaryDirectories.push(directory);
2403
+ const configPath = join(directory, "profile.yml");
2404
+ const loaderEntry = {
2405
+ id: "profile:marketplace-skill-guard",
2406
+ options: { id: "marketplace-skill-guard", name: "@pi-harness/plugin-skill-guard", config: {} },
2407
+ update: () => Promise.reject(new Error("failed to apply loader entry", { cause: new PiToolRegistryLeasedError("skill_scan") })),
2408
+ };
2409
+ await writeFile(
2410
+ configPath,
2411
+ '- id: marketplace-skill-guard\n name: "@pi-harness/plugin-skill-guard"\n disabled: true\n config: {}\n- id: runtime\n name: "@pi-harness/core/plugins/runtime"\n config: {}\n',
2412
+ "utf8",
2413
+ );
2414
+ await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
2415
+ const session = { sessionId: "plugin-toggle-leased-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
2416
+ context.provide("piRuntime", { session, prompt: () => Promise.resolve(), abort: () => Promise.resolve(), dispose: () => Promise.resolve() } as never);
2417
+ context.provide("piModels", { model: { provider: "test", id: "model" }, runtime: { getModels: () => [], getModel: () => undefined } } as never);
2418
+ context.provide("piHarnessLaunch", { cwd: directory, agentDir: directory, configPath, args: [], requestExit() {} });
2419
+ context.reflect.provide("loader", {
2420
+ *entries() {
2421
+ yield loaderEntry;
2422
+ },
2423
+ });
2424
+ await context.plugin(apiPlugin);
2425
+
2426
+ const response = await fetch(context.webServer.url + "/api/plugins/toggle", {
2427
+ method: "POST",
2428
+ headers: { "content-type": "application/json" },
2429
+ body: JSON.stringify({ id: "marketplace-skill-guard", enabled: true }),
2430
+ });
2431
+
2432
+ expect(response.status).toBe(200);
2433
+ await expect(response.json()).resolves.toMatchObject({ restartRequired: true, plugin: { id: "marketplace-skill-guard" } });
2434
+ await expect(readFile(configPath, "utf8")).resolves.not.toContain("disabled: true");
2435
+ });
2436
+
2199
2437
  test("locks marketplace installation before reading a request body", async () => {
2200
2438
  const cwd = await mkdtemp(join(tmpdir(), "pi-harness-api-plugin-install-lock-"));
2201
2439
  temporaryDirectories.push(cwd);
@@ -2455,6 +2693,60 @@ describe("API gateway plugin", () => {
2455
2693
  await expect(readFile(configPath, "utf8")).resolves.toContain("- id: sibling");
2456
2694
  });
2457
2695
 
2696
+ test("reports a failed plugin uninstall in npm's own words without the command line", async () => {
2697
+ const context = new Context();
2698
+ contexts.push(context);
2699
+ const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-uninstall-failure-"));
2700
+ temporaryDirectories.push(directory);
2701
+ const shimDirectory = join(directory, "bin");
2702
+ await mkdir(shimDirectory);
2703
+ await writeFile(join(directory, "package.json"), '{ "name": "harness" }\n', "utf8");
2704
+ await writeFile(join(shimDirectory, "npm"), "#!/bin/sh\nprintf 'npm error code EACCES\\nnpm error syscall unlink\\n' >&2\nexit 1\n", { mode: 0o755 });
2705
+ const configPath = join(directory, "profile.yml");
2706
+ const entryId = "skill-guard";
2707
+ const loaderEntry = {
2708
+ id: `profile:${entryId}`,
2709
+ options: { id: entryId, name: "@pi-harness/plugin-skill-guard", config: {} },
2710
+ parent: { tree: { write() {} }, remove: () => Promise.resolve() },
2711
+ };
2712
+ const profileBefore = `- id: ${entryId}\n name: ${JSON.stringify(loaderEntry.options.name)}\n config: {}\n- id: sibling\n name: "@pi-harness/core/plugins/runtime"\n config: {}\n`;
2713
+ await writeFile(configPath, profileBefore, "utf8");
2714
+ await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
2715
+ const session = { sessionId: "uninstall-failure-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
2716
+ context.provide("piRuntime", { session, prompt: () => Promise.resolve(), abort: () => Promise.resolve(), dispose: () => Promise.resolve() } as never);
2717
+ context.provide("piModels", { model: { provider: "test", id: "model" }, runtime: { getModels: () => [], getModel: () => undefined } } as never);
2718
+ context.provide("piHarnessLaunch", { cwd: directory, agentDir: directory, configPath, args: [], requestExit() {} });
2719
+ const restored: unknown[] = [];
2720
+ context.reflect.provide("loader", {
2721
+ entries: () => [loaderEntry],
2722
+ create: (options: unknown) => {
2723
+ restored.push(options);
2724
+ return Promise.resolve(`profile:${entryId}`);
2725
+ },
2726
+ });
2727
+ await context.plugin(apiPlugin);
2728
+
2729
+ const originalPath = process.env.PATH ?? "";
2730
+ process.env.PATH = `${shimDirectory}:${originalPath}`;
2731
+ let response: Response;
2732
+ try {
2733
+ response = await fetch(context.webServer.url + "/api/plugins/uninstall", {
2734
+ method: "POST",
2735
+ headers: { "content-type": "application/json" },
2736
+ body: JSON.stringify({ id: entryId }),
2737
+ });
2738
+ } finally {
2739
+ process.env.PATH = originalPath;
2740
+ }
2741
+
2742
+ expect(response.status).toBe(502);
2743
+ const payload = (await response.json()) as { error?: string };
2744
+ expect(payload.error).toBe("npm error code EACCES npm error syscall unlink");
2745
+ expect(payload.error).not.toContain("--package-lock=false");
2746
+ expect(restored).toEqual([expect.objectContaining({ id: entryId, name: "@pi-harness/plugin-skill-guard" })]);
2747
+ await expect(readFile(configPath, "utf8")).resolves.toBe(profileBefore);
2748
+ });
2749
+
2458
2750
  test("commits selected workspace files only after an explicit message", async () => {
2459
2751
  const context = new Context();
2460
2752
  contexts.push(context);
@@ -31,6 +31,9 @@ const supportedKeywords = new Set([
31
31
  "if",
32
32
  "then",
33
33
  "else",
34
+ "allOf",
35
+ "contains",
36
+ "not",
34
37
  ]);
35
38
 
36
39
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -75,6 +78,8 @@ function validate(schema: Schema, value: unknown, path = "$"): string[] {
75
78
  if (typeof schema.minItems === "number" && value.length < schema.minItems) errors.push(`${path}: fewer than ${schema.minItems} items`);
76
79
  if (schema.uniqueItems === true && new Set(value.map((item) => JSON.stringify(item))).size !== value.length) errors.push(`${path}: items are not unique`);
77
80
  if (isRecord(schema.items)) value.forEach((item, index) => errors.push(...validate(schema.items as Schema, item, `${path}[${index}]`)));
81
+ if (isRecord(schema.contains) && !value.some((item) => validate(schema.contains as Schema, item, path).length === 0))
82
+ errors.push(`${path}: contains no item matching ${JSON.stringify(schema.contains)}`);
78
83
  }
79
84
  if (isRecord(value)) {
80
85
  const properties = isRecord(schema.properties) ? schema.properties : {};
@@ -91,6 +96,8 @@ function validate(schema: Schema, value: unknown, path = "$"): string[] {
91
96
  const branch = validate(schema.if, value, path).length === 0 ? schema.then : schema.else;
92
97
  if (isRecord(branch)) errors.push(...validate(branch, value, path));
93
98
  }
99
+ if (isRecord(schema.not) && validate(schema.not, value, path).length === 0) errors.push(`${path}: matches ${JSON.stringify(schema.not)}`);
100
+ if (Array.isArray(schema.allOf)) for (const branch of schema.allOf) if (isRecord(branch)) errors.push(...validate(branch, value, path));
94
101
  return errors;
95
102
  }
96
103
 
@@ -133,6 +140,11 @@ describe("marketplace entry schema", () => {
133
140
  expect(validate(schema, { ...validEntry(), profile: { name: "@pi-harness/plugin-agent-teams", config: [] } })).not.toEqual([]);
134
141
  expect(validate(schema, { ...groupEntry(), profile: { name: "@deepseek-ai/cordis-plugin-group", group: true, config: {} } })).not.toEqual([]);
135
142
  expect(validate(schema, { ...validEntry(), capabilities: [] })).not.toEqual([]);
143
+ expect(validate(schema, { ...validEntry(), capabilities: ["bounded skill packaging"] })).not.toEqual([]);
144
+ expect(validate(schema, { ...validEntry(), capabilities: ["read-only", "read-only"] })).not.toEqual([]);
145
+ // A plugin cannot claim it changes nothing and also declare an effect; the console shows that claim as a badge next to the Install button.
146
+ expect(validate(schema, { ...validEntry(), capabilities: ["read-only", "runs-commands"] })).not.toEqual([]);
147
+ expect(validate(schema, { ...validEntry(), capabilities: ["session-data", "runs-commands"] })).toEqual([]);
136
148
  expect(validate(schema, { ...validEntry(), repository: "http://example.com" })).not.toEqual([]);
137
149
  expect(validate(schema, { ...validEntry(), unexpected: true })).not.toEqual([]);
138
150
  });