@pi-harness/pi-harness 0.1.22 → 0.1.24

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 (307) hide show
  1. package/README.md +12 -10
  2. package/apps/web/dist/assets/index-BcLXLKqO.js +24 -0
  3. package/apps/web/dist/assets/index-xF647pQJ.css +2 -0
  4. package/apps/web/dist/index.html +2 -2
  5. package/apps/web/package.json +6 -6
  6. package/apps/web/profile/cordis.yml +1 -0
  7. package/apps/web/server-dist/bin.js +45 -7
  8. package/apps/web/server-dist/bin.js.map +1 -1
  9. package/apps/web/src/server/bin.ts +45 -7
  10. package/apps/web/test/bin.test.ts +313 -0
  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 +295 -86
  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 -1
  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 -1
  22. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/browser-session.json +1 -1
  23. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/canvas-draw.json +1 -1
  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 -1
  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 -1
  30. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/context.json +1 -1
  31. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/cost-meter.json +1 -1
  32. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/dependency-checker.json +1 -1
  33. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/docker-sandbox.json +1 -1
  34. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/fail-logger.json +1 -1
  35. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/genui.json +1 -1
  36. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/git-time-capsule.json +1 -1
  37. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/graph-memory.json +1 -1
  38. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/history-compressor.json +1 -1
  39. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/hol-guard.json +3 -3
  40. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/i18n-pair.json +1 -1
  41. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/image-compressor.json +1 -1
  42. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/llm-verifier.json +1 -1
  43. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mcp-client.json +1 -1
  44. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mcp-panel.json +1 -1
  45. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/memory.json +1 -1
  46. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mirage-bridge.json +1 -1
  47. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mock-server.json +1 -1
  48. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/modlens.json +1 -1
  49. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/module-search.json +1 -1
  50. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/obsidian-sync.json +1 -1
  51. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/openpets.json +1 -1
  52. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plan-execute.json +1 -1
  53. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-check.json +1 -1
  54. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-dev.json +1 -1
  55. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-finder.json +1 -1
  56. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-radar.json +1 -1
  57. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-stars.json +1 -1
  58. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/prompt-guard.json +1 -1
  59. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/prompt-library.json +1 -1
  60. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/readme-gen.json +1 -1
  61. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/recall-unread.json +1 -1
  62. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/reverse-skill.json +1 -1
  63. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/reviewer-bot.json +1 -1
  64. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/runtime-doctor.json +1 -1
  65. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/secure-audit.json +1 -1
  66. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-bookmarks.json +1 -1
  67. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-bridge.json +1 -1
  68. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-compare.json +1 -1
  69. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-export.json +1 -1
  70. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-insights.json +1 -1
  71. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-search.json +1 -1
  72. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/skill-catalog.json +1 -1
  73. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/skill-guard.json +1 -1
  74. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/sql-lens.json +1 -1
  75. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/synapse.json +1 -1
  76. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/tab-manager.json +1 -1
  77. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/taskboard.json +1 -1
  78. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/telemetry-blocker.json +1 -1
  79. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/test-harness.json +1 -1
  80. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/theme-studio.json +1 -1
  81. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/token-guard.json +1 -1
  82. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/turn-rewind.json +1 -1
  83. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/undo-savepoint.json +1 -1
  84. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/vision-toolkit.json +1 -1
  85. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/web-research.json +1 -1
  86. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/workspace-navigator.json +1 -1
  87. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/workspace-search.json +1 -1
  88. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/yaml-validator.json +1 -1
  89. package/node_modules/@pi-harness/api-gateway/package.json +1 -1
  90. package/node_modules/@pi-harness/cli/dist/main.d.ts +1 -0
  91. package/node_modules/@pi-harness/cli/dist/main.d.ts.map +1 -1
  92. package/node_modules/@pi-harness/cli/dist/main.js +11 -0
  93. package/node_modules/@pi-harness/cli/dist/main.js.map +1 -1
  94. package/node_modules/@pi-harness/cli/dist/node-stdio.d.ts.map +1 -1
  95. package/node_modules/@pi-harness/cli/dist/node-stdio.js +3 -1
  96. package/node_modules/@pi-harness/cli/dist/node-stdio.js.map +1 -1
  97. package/node_modules/@pi-harness/cli/dist/relaunch.d.ts.map +1 -1
  98. package/node_modules/@pi-harness/cli/dist/relaunch.js +4 -1
  99. package/node_modules/@pi-harness/cli/dist/relaunch.js.map +1 -1
  100. package/node_modules/@pi-harness/cli/package.json +1 -1
  101. package/node_modules/@pi-harness/host-webserver/dist/index.d.ts +1 -0
  102. package/node_modules/@pi-harness/host-webserver/dist/index.d.ts.map +1 -1
  103. package/node_modules/@pi-harness/host-webserver/dist/index.js +86 -3
  104. package/node_modules/@pi-harness/host-webserver/dist/index.js.map +1 -1
  105. package/node_modules/@pi-harness/host-webserver/package.json +1 -1
  106. package/node_modules/@pi-harness/web-app/dist/index.d.ts +3 -0
  107. package/node_modules/@pi-harness/web-app/dist/index.d.ts.map +1 -1
  108. package/node_modules/@pi-harness/web-app/dist/index.js +21 -8
  109. package/node_modules/@pi-harness/web-app/dist/index.js.map +1 -1
  110. package/node_modules/@pi-harness/web-app/package.json +1 -1
  111. package/package.json +8 -8
  112. package/packages/api-gateway/dist/index.d.ts.map +1 -1
  113. package/packages/api-gateway/dist/index.js +295 -86
  114. package/packages/api-gateway/dist/index.js.map +1 -1
  115. package/packages/api-gateway/dist/marketplace-entries/official/agent-teams.json +1 -1
  116. package/packages/api-gateway/dist/marketplace-entries/official/anchored-standard.json +1 -1
  117. package/packages/api-gateway/dist/marketplace-entries/official/annotation.json +1 -1
  118. package/packages/api-gateway/dist/marketplace-entries/official/archify.json +1 -1
  119. package/packages/api-gateway/dist/marketplace-entries/official/at-file.json +1 -1
  120. package/packages/api-gateway/dist/marketplace-entries/official/auto-mode.json +1 -1
  121. package/packages/api-gateway/dist/marketplace-entries/official/better-sidebar.json +1 -1
  122. package/packages/api-gateway/dist/marketplace-entries/official/browser-fetch.json +1 -1
  123. package/packages/api-gateway/dist/marketplace-entries/official/browser-session.json +1 -1
  124. package/packages/api-gateway/dist/marketplace-entries/official/canvas-draw.json +1 -1
  125. package/packages/api-gateway/dist/marketplace-entries/official/change-verifier.json +1 -1
  126. package/packages/api-gateway/dist/marketplace-entries/official/cleaner.json +1 -1
  127. package/packages/api-gateway/dist/marketplace-entries/official/cli-notifier.json +1 -1
  128. package/packages/api-gateway/dist/marketplace-entries/official/code2skill.json +1 -1
  129. package/packages/api-gateway/dist/marketplace-entries/official/colleague-skill.json +1 -1
  130. package/packages/api-gateway/dist/marketplace-entries/official/context-doctor.json +1 -1
  131. package/packages/api-gateway/dist/marketplace-entries/official/context.json +1 -1
  132. package/packages/api-gateway/dist/marketplace-entries/official/cost-meter.json +1 -1
  133. package/packages/api-gateway/dist/marketplace-entries/official/dependency-checker.json +1 -1
  134. package/packages/api-gateway/dist/marketplace-entries/official/docker-sandbox.json +1 -1
  135. package/packages/api-gateway/dist/marketplace-entries/official/fail-logger.json +1 -1
  136. package/packages/api-gateway/dist/marketplace-entries/official/genui.json +1 -1
  137. package/packages/api-gateway/dist/marketplace-entries/official/git-time-capsule.json +1 -1
  138. package/packages/api-gateway/dist/marketplace-entries/official/graph-memory.json +1 -1
  139. package/packages/api-gateway/dist/marketplace-entries/official/history-compressor.json +1 -1
  140. package/packages/api-gateway/dist/marketplace-entries/official/hol-guard.json +3 -3
  141. package/packages/api-gateway/dist/marketplace-entries/official/i18n-pair.json +1 -1
  142. package/packages/api-gateway/dist/marketplace-entries/official/image-compressor.json +1 -1
  143. package/packages/api-gateway/dist/marketplace-entries/official/llm-verifier.json +1 -1
  144. package/packages/api-gateway/dist/marketplace-entries/official/mcp-client.json +1 -1
  145. package/packages/api-gateway/dist/marketplace-entries/official/mcp-panel.json +1 -1
  146. package/packages/api-gateway/dist/marketplace-entries/official/memory.json +1 -1
  147. package/packages/api-gateway/dist/marketplace-entries/official/mirage-bridge.json +1 -1
  148. package/packages/api-gateway/dist/marketplace-entries/official/mock-server.json +1 -1
  149. package/packages/api-gateway/dist/marketplace-entries/official/modlens.json +1 -1
  150. package/packages/api-gateway/dist/marketplace-entries/official/module-search.json +1 -1
  151. package/packages/api-gateway/dist/marketplace-entries/official/obsidian-sync.json +1 -1
  152. package/packages/api-gateway/dist/marketplace-entries/official/openpets.json +1 -1
  153. package/packages/api-gateway/dist/marketplace-entries/official/plan-execute.json +1 -1
  154. package/packages/api-gateway/dist/marketplace-entries/official/plugin-check.json +1 -1
  155. package/packages/api-gateway/dist/marketplace-entries/official/plugin-dev.json +1 -1
  156. package/packages/api-gateway/dist/marketplace-entries/official/plugin-finder.json +1 -1
  157. package/packages/api-gateway/dist/marketplace-entries/official/plugin-radar.json +1 -1
  158. package/packages/api-gateway/dist/marketplace-entries/official/plugin-stars.json +1 -1
  159. package/packages/api-gateway/dist/marketplace-entries/official/prompt-guard.json +1 -1
  160. package/packages/api-gateway/dist/marketplace-entries/official/prompt-library.json +1 -1
  161. package/packages/api-gateway/dist/marketplace-entries/official/readme-gen.json +1 -1
  162. package/packages/api-gateway/dist/marketplace-entries/official/recall-unread.json +1 -1
  163. package/packages/api-gateway/dist/marketplace-entries/official/reverse-skill.json +1 -1
  164. package/packages/api-gateway/dist/marketplace-entries/official/reviewer-bot.json +1 -1
  165. package/packages/api-gateway/dist/marketplace-entries/official/runtime-doctor.json +1 -1
  166. package/packages/api-gateway/dist/marketplace-entries/official/secure-audit.json +1 -1
  167. package/packages/api-gateway/dist/marketplace-entries/official/session-bookmarks.json +1 -1
  168. package/packages/api-gateway/dist/marketplace-entries/official/session-bridge.json +1 -1
  169. package/packages/api-gateway/dist/marketplace-entries/official/session-compare.json +1 -1
  170. package/packages/api-gateway/dist/marketplace-entries/official/session-export.json +1 -1
  171. package/packages/api-gateway/dist/marketplace-entries/official/session-insights.json +1 -1
  172. package/packages/api-gateway/dist/marketplace-entries/official/session-search.json +1 -1
  173. package/packages/api-gateway/dist/marketplace-entries/official/skill-catalog.json +1 -1
  174. package/packages/api-gateway/dist/marketplace-entries/official/skill-guard.json +1 -1
  175. package/packages/api-gateway/dist/marketplace-entries/official/sql-lens.json +1 -1
  176. package/packages/api-gateway/dist/marketplace-entries/official/synapse.json +1 -1
  177. package/packages/api-gateway/dist/marketplace-entries/official/tab-manager.json +1 -1
  178. package/packages/api-gateway/dist/marketplace-entries/official/taskboard.json +1 -1
  179. package/packages/api-gateway/dist/marketplace-entries/official/telemetry-blocker.json +1 -1
  180. package/packages/api-gateway/dist/marketplace-entries/official/test-harness.json +1 -1
  181. package/packages/api-gateway/dist/marketplace-entries/official/theme-studio.json +1 -1
  182. package/packages/api-gateway/dist/marketplace-entries/official/token-guard.json +1 -1
  183. package/packages/api-gateway/dist/marketplace-entries/official/turn-rewind.json +1 -1
  184. package/packages/api-gateway/dist/marketplace-entries/official/undo-savepoint.json +1 -1
  185. package/packages/api-gateway/dist/marketplace-entries/official/vision-toolkit.json +1 -1
  186. package/packages/api-gateway/dist/marketplace-entries/official/web-research.json +1 -1
  187. package/packages/api-gateway/dist/marketplace-entries/official/workspace-navigator.json +1 -1
  188. package/packages/api-gateway/dist/marketplace-entries/official/workspace-search.json +1 -1
  189. package/packages/api-gateway/dist/marketplace-entries/official/yaml-validator.json +1 -1
  190. package/packages/api-gateway/package.json +1 -1
  191. package/packages/api-gateway/src/index.ts +310 -87
  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 -1
  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 -1
  200. package/packages/api-gateway/src/marketplace-entries/official/browser-session.json +1 -1
  201. package/packages/api-gateway/src/marketplace-entries/official/canvas-draw.json +1 -1
  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 -1
  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 -1
  208. package/packages/api-gateway/src/marketplace-entries/official/context.json +1 -1
  209. package/packages/api-gateway/src/marketplace-entries/official/cost-meter.json +1 -1
  210. package/packages/api-gateway/src/marketplace-entries/official/dependency-checker.json +1 -1
  211. package/packages/api-gateway/src/marketplace-entries/official/docker-sandbox.json +1 -1
  212. package/packages/api-gateway/src/marketplace-entries/official/fail-logger.json +1 -1
  213. package/packages/api-gateway/src/marketplace-entries/official/genui.json +1 -1
  214. package/packages/api-gateway/src/marketplace-entries/official/git-time-capsule.json +1 -1
  215. package/packages/api-gateway/src/marketplace-entries/official/graph-memory.json +1 -1
  216. package/packages/api-gateway/src/marketplace-entries/official/history-compressor.json +1 -1
  217. package/packages/api-gateway/src/marketplace-entries/official/hol-guard.json +3 -3
  218. package/packages/api-gateway/src/marketplace-entries/official/i18n-pair.json +1 -1
  219. package/packages/api-gateway/src/marketplace-entries/official/image-compressor.json +1 -1
  220. package/packages/api-gateway/src/marketplace-entries/official/llm-verifier.json +1 -1
  221. package/packages/api-gateway/src/marketplace-entries/official/mcp-client.json +1 -1
  222. package/packages/api-gateway/src/marketplace-entries/official/mcp-panel.json +1 -1
  223. package/packages/api-gateway/src/marketplace-entries/official/memory.json +1 -1
  224. package/packages/api-gateway/src/marketplace-entries/official/mirage-bridge.json +1 -1
  225. package/packages/api-gateway/src/marketplace-entries/official/mock-server.json +1 -1
  226. package/packages/api-gateway/src/marketplace-entries/official/modlens.json +1 -1
  227. package/packages/api-gateway/src/marketplace-entries/official/module-search.json +1 -1
  228. package/packages/api-gateway/src/marketplace-entries/official/obsidian-sync.json +1 -1
  229. package/packages/api-gateway/src/marketplace-entries/official/openpets.json +1 -1
  230. package/packages/api-gateway/src/marketplace-entries/official/plan-execute.json +1 -1
  231. package/packages/api-gateway/src/marketplace-entries/official/plugin-check.json +1 -1
  232. package/packages/api-gateway/src/marketplace-entries/official/plugin-dev.json +1 -1
  233. package/packages/api-gateway/src/marketplace-entries/official/plugin-finder.json +1 -1
  234. package/packages/api-gateway/src/marketplace-entries/official/plugin-radar.json +1 -1
  235. package/packages/api-gateway/src/marketplace-entries/official/plugin-stars.json +1 -1
  236. package/packages/api-gateway/src/marketplace-entries/official/prompt-guard.json +1 -1
  237. package/packages/api-gateway/src/marketplace-entries/official/prompt-library.json +1 -1
  238. package/packages/api-gateway/src/marketplace-entries/official/readme-gen.json +1 -1
  239. package/packages/api-gateway/src/marketplace-entries/official/recall-unread.json +1 -1
  240. package/packages/api-gateway/src/marketplace-entries/official/reverse-skill.json +1 -1
  241. package/packages/api-gateway/src/marketplace-entries/official/reviewer-bot.json +1 -1
  242. package/packages/api-gateway/src/marketplace-entries/official/runtime-doctor.json +1 -1
  243. package/packages/api-gateway/src/marketplace-entries/official/secure-audit.json +1 -1
  244. package/packages/api-gateway/src/marketplace-entries/official/session-bookmarks.json +1 -1
  245. package/packages/api-gateway/src/marketplace-entries/official/session-bridge.json +1 -1
  246. package/packages/api-gateway/src/marketplace-entries/official/session-compare.json +1 -1
  247. package/packages/api-gateway/src/marketplace-entries/official/session-export.json +1 -1
  248. package/packages/api-gateway/src/marketplace-entries/official/session-insights.json +1 -1
  249. package/packages/api-gateway/src/marketplace-entries/official/session-search.json +1 -1
  250. package/packages/api-gateway/src/marketplace-entries/official/skill-catalog.json +1 -1
  251. package/packages/api-gateway/src/marketplace-entries/official/skill-guard.json +1 -1
  252. package/packages/api-gateway/src/marketplace-entries/official/sql-lens.json +1 -1
  253. package/packages/api-gateway/src/marketplace-entries/official/synapse.json +1 -1
  254. package/packages/api-gateway/src/marketplace-entries/official/tab-manager.json +1 -1
  255. package/packages/api-gateway/src/marketplace-entries/official/taskboard.json +1 -1
  256. package/packages/api-gateway/src/marketplace-entries/official/telemetry-blocker.json +1 -1
  257. package/packages/api-gateway/src/marketplace-entries/official/test-harness.json +1 -1
  258. package/packages/api-gateway/src/marketplace-entries/official/theme-studio.json +1 -1
  259. package/packages/api-gateway/src/marketplace-entries/official/token-guard.json +1 -1
  260. package/packages/api-gateway/src/marketplace-entries/official/turn-rewind.json +1 -1
  261. package/packages/api-gateway/src/marketplace-entries/official/undo-savepoint.json +1 -1
  262. package/packages/api-gateway/src/marketplace-entries/official/vision-toolkit.json +1 -1
  263. package/packages/api-gateway/src/marketplace-entries/official/web-research.json +1 -1
  264. package/packages/api-gateway/src/marketplace-entries/official/workspace-navigator.json +1 -1
  265. package/packages/api-gateway/src/marketplace-entries/official/workspace-search.json +1 -1
  266. package/packages/api-gateway/src/marketplace-entries/official/yaml-validator.json +1 -1
  267. package/packages/api-gateway/test/index.test.ts +799 -3
  268. package/packages/api-gateway/test/marketplace-schema.test.ts +139 -0
  269. package/packages/api-gateway/test/packed-package.test.ts +70 -0
  270. package/packages/api-gateway/test/reference-docs.test.ts +50 -0
  271. package/packages/api-gateway/test/release-contract.test.ts +77 -0
  272. package/packages/api-gateway/test/release-package.test.ts +33 -2
  273. package/packages/api-gateway/test/release-version.test.ts +46 -4
  274. package/packages/bundle-web-app/dist/index.d.ts +3 -0
  275. package/packages/bundle-web-app/dist/index.d.ts.map +1 -1
  276. package/packages/bundle-web-app/dist/index.js +21 -8
  277. package/packages/bundle-web-app/dist/index.js.map +1 -1
  278. package/packages/bundle-web-app/package.json +1 -1
  279. package/packages/bundle-web-app/src/index.ts +25 -8
  280. package/packages/bundle-web-app/test/index.test.ts +116 -4
  281. package/packages/cli/dist/main.d.ts +1 -0
  282. package/packages/cli/dist/main.d.ts.map +1 -1
  283. package/packages/cli/dist/main.js +11 -0
  284. package/packages/cli/dist/main.js.map +1 -1
  285. package/packages/cli/dist/node-stdio.d.ts.map +1 -1
  286. package/packages/cli/dist/node-stdio.js +3 -1
  287. package/packages/cli/dist/node-stdio.js.map +1 -1
  288. package/packages/cli/dist/relaunch.d.ts.map +1 -1
  289. package/packages/cli/dist/relaunch.js +4 -1
  290. package/packages/cli/dist/relaunch.js.map +1 -1
  291. package/packages/cli/package.json +1 -1
  292. package/packages/cli/src/main.ts +11 -0
  293. package/packages/cli/src/node-stdio.ts +6 -3
  294. package/packages/cli/src/relaunch.ts +4 -1
  295. package/packages/cli/test/cli.test.ts +3 -1
  296. package/packages/cli/test/main.test.ts +134 -0
  297. package/packages/cli/test/node-stdio.test.ts +43 -0
  298. package/packages/cli/test/relaunch.test.ts +110 -0
  299. package/packages/host-webserver/dist/index.d.ts +1 -0
  300. package/packages/host-webserver/dist/index.d.ts.map +1 -1
  301. package/packages/host-webserver/dist/index.js +86 -3
  302. package/packages/host-webserver/dist/index.js.map +1 -1
  303. package/packages/host-webserver/package.json +1 -1
  304. package/packages/host-webserver/src/index.ts +95 -3
  305. package/packages/host-webserver/test/index.test.ts +160 -1
  306. package/apps/web/dist/assets/index-BVI_IS-R.css +0 -2
  307. package/apps/web/dist/assets/index-w0KQgv8x.js +0 -22
@@ -210,3 +210,46 @@ describe("NodeStdio interactive prompt", () => {
210
210
  expect(input.listenerCount("error")).toBe(baseline.error);
211
211
  });
212
212
  });
213
+
214
+ describe("NodeStdio interactive prompt marker placement", () => {
215
+ function collect(stream: PassThrough): () => string {
216
+ const chunks: Buffer[] = [];
217
+ stream.on("data", (chunk: Buffer) => chunks.push(chunk));
218
+ return () => Buffer.concat(chunks).toString("utf8");
219
+ }
220
+
221
+ test("writes the marker to a TTY stderr instead of a redirected stdout", async () => {
222
+ const input: FakeTty = new PassThrough();
223
+ input.isTTY = true;
224
+ input.setRawMode = () => {};
225
+ const output: FakeTty = new PassThrough();
226
+ const error: FakeTty = new PassThrough();
227
+ error.isTTY = true;
228
+ const written = collect(output);
229
+ const shown = collect(error);
230
+ const stdio = new NodeStdio(input, output, error);
231
+ const read = stdio.readPrompt();
232
+ input.write("hello\n");
233
+
234
+ await expect(settle(read)).resolves.toBe("resolved:hello");
235
+ expect(written()).toBe("");
236
+ expect(shown()).toContain("> ");
237
+ });
238
+
239
+ test("writes no marker at all when neither stdout nor stderr is a terminal", async () => {
240
+ const input: FakeTty = new PassThrough();
241
+ input.isTTY = true;
242
+ input.setRawMode = () => {};
243
+ const output: FakeTty = new PassThrough();
244
+ const error: FakeTty = new PassThrough();
245
+ const written = collect(output);
246
+ const shown = collect(error);
247
+ const stdio = new NodeStdio(input, output, error);
248
+ const read = stdio.readPrompt();
249
+ input.write("hello\n");
250
+
251
+ await expect(settle(read)).resolves.toBe("resolved:hello");
252
+ expect(written()).toBe("");
253
+ expect(shown()).toBe("");
254
+ });
255
+ });
@@ -1,7 +1,9 @@
1
+ import { existsSync, readFileSync } from "node:fs";
1
2
  import { mkdtemp, readFile, writeFile } from "node:fs/promises";
2
3
  import { tmpdir } from "node:os";
3
4
  import { join } from "node:path";
4
5
  import { describe, expect, test } from "vitest";
6
+ import { DUPLICATE_SIGNAL_WINDOW_MS } from "../src/main.js";
5
7
  import { PI_HARNESS_RESTART_EXIT_CODE, shouldRelaunchForDevelopmentProfile, superviseDevelopmentProcess } from "../src/relaunch.js";
6
8
 
7
9
  describe("development profile relaunch", () => {
@@ -59,3 +61,111 @@ describe("development profile relaunch", () => {
59
61
  await expect(superviseDevelopmentProcess(join(tmpdir(), "pi-harness-not-a-binary"), [], { stdio: "ignore" })).rejects.toThrow(/ENOENT/);
60
62
  });
61
63
  });
64
+
65
+ describe("development profile signal forwarding", () => {
66
+ async function startChild(label: string, body: string): Promise<{ markerPath: string; childPath: string }> {
67
+ const directory = await mkdtemp(join(tmpdir(), `pi-harness-supervisor-${label}-`));
68
+ const markerPath = join(directory, "started.txt");
69
+ const childPath = join(directory, "child.mjs");
70
+ // The marker is written last, after the body has installed its handlers. Written first, it announces a child that node would still kill outright with the default disposition, because a signal only becomes catchable once a listener for it exists - which is how this test was seen to observe exit 130 where it expected 31.
71
+ await writeFile(childPath, `import { writeFileSync } from "node:fs"; ${body} writeFileSync(process.argv[2], "started");`, "utf8");
72
+ return { markerPath, childPath };
73
+ }
74
+
75
+ test("relays SIGTERM to the child and reports its signal exit", async () => {
76
+ const { markerPath, childPath } = await startChild("term", "setInterval(() => {}, 1_000);");
77
+ const supervised = superviseDevelopmentProcess(process.execPath, [childPath, markerPath], { stdio: "ignore" });
78
+ await expect.poll(() => existsSync(markerPath), { interval: 20, timeout: 5_000 }).toBe(true);
79
+
80
+ process.emit("SIGTERM", "SIGTERM");
81
+
82
+ await expect(supervised).resolves.toBe(143);
83
+ });
84
+
85
+ // The vitest worker has no terminal on any of the three descriptors, so the tty state is faked here to pin that relaying does not depend on it.
86
+ function withTerminalStdio<T>(isTTY: boolean, spawnChild: () => T): T {
87
+ const streams = [process.stdin, process.stdout, process.stderr];
88
+ const original = streams.map((stream) => stream.isTTY);
89
+ for (const stream of streams) stream.isTTY = isTTY;
90
+ try {
91
+ return spawnChild();
92
+ } finally {
93
+ for (const [index, stream] of streams.entries()) stream.isTTY = original[index] ?? false;
94
+ }
95
+ }
96
+
97
+ test.skipIf(process.platform === "win32")(
98
+ "relays SIGINT and SIGHUP to the child whether or not the inherited stdio is a terminal",
99
+ async () => {
100
+ for (const isTTY of [true, false]) {
101
+ for (const [signal, code] of [
102
+ ["SIGINT", 31],
103
+ ["SIGHUP", 32],
104
+ ] as const) {
105
+ const { markerPath, childPath } = await startChild(
106
+ `${signal.toLowerCase()}-${String(isTTY)}`,
107
+ // Exiting with 21 when no signal arrives distinguishes a swallowed relay from a slow one.
108
+ `process.on("SIGINT", () => process.exit(31)); process.on("SIGHUP", () => process.exit(32)); setTimeout(() => process.exit(21), 2_000);`,
109
+ );
110
+ const supervised = withTerminalStdio(isTTY, () => superviseDevelopmentProcess(process.execPath, [childPath, markerPath], { stdio: "ignore" }));
111
+ await expect.poll(() => existsSync(markerPath), { interval: 20, timeout: 5_000 }).toBe(true);
112
+
113
+ expect(process.emit(signal, signal)).toBe(true);
114
+
115
+ await expect(supervised).resolves.toBe(code);
116
+ }
117
+ }
118
+ },
119
+ 30_000,
120
+ );
121
+
122
+ // A real `kill -INT -<pgid>` would also hit the vitest worker, so the group broadcast is simulated: the signal is delivered straight to the child (the terminal's copy) and emitted on this process (the copy the supervisor relays), 2ms apart, which matches the sub-millisecond spacing measured between the two copies and is well inside the duplicate window.
123
+ // How many deliveries the child sees is the kernel's decision, not this code's: two SIGINTs that are pending together are coalesced into one, which was observed on roughly one run in five and is why the count is asserted as at least one rather than exactly two. What this test pins is the outcome under a group broadcast - one graceful shutdown, exit 130, no force-quit - and it holds under either delivery count. The window logic itself is pinned deterministically and in-process against the real runCli in main.test.ts.
124
+ test.skipIf(process.platform === "win32")("a group broadcast that reaches the child directly and through the relay shuts the child down once", async () => {
125
+ const directory = await mkdtemp(join(tmpdir(), "pi-harness-supervisor-broadcast-"));
126
+ const markerPath = join(directory, "signals.txt");
127
+ const pidPath = join(directory, "pid.txt");
128
+ const childPath = join(directory, "child.mjs");
129
+ await writeFile(
130
+ childPath,
131
+ // The child mirrors runCli's handler: every delivery is recorded, but a repeat of the same signal inside DUPLICATE_SIGNAL_WINDOW_MS is ignored instead of counting toward the force-quit path.
132
+ `import { appendFileSync, writeFileSync } from "node:fs";
133
+ const marker = process.argv[2];
134
+ let lastSignal;
135
+ let lastSignalAt = 0;
136
+ let signalCount = 0;
137
+ process.on("SIGINT", () => {
138
+ appendFileSync(marker, "delivery\\n");
139
+ const receivedAt = performance.now();
140
+ if (lastSignal === "SIGINT" && receivedAt - lastSignalAt < ${DUPLICATE_SIGNAL_WINDOW_MS}) return;
141
+ lastSignal = "SIGINT";
142
+ lastSignalAt = receivedAt;
143
+ signalCount += 1;
144
+ if (signalCount > 1) {
145
+ appendFileSync(marker, "force-quit\\n");
146
+ process.exit(1);
147
+ }
148
+ setTimeout(() => {
149
+ appendFileSync(marker, "graceful\\n");
150
+ process.exit(130);
151
+ }, 25);
152
+ });
153
+ writeFileSync(process.argv[3], String(process.pid));
154
+ setInterval(() => {}, 1_000);`,
155
+ "utf8",
156
+ );
157
+ const supervised = superviseDevelopmentProcess(process.execPath, [childPath, markerPath, pidPath], { stdio: "ignore" });
158
+ await expect.poll(() => (existsSync(pidPath) ? readFileSync(pidPath, "utf8") : ""), { interval: 20, timeout: 5_000 }).not.toBe("");
159
+ const childPid = Number(readFileSync(pidPath, "utf8"));
160
+
161
+ process.kill(childPid, "SIGINT");
162
+ await new Promise((resolve) => setTimeout(resolve, 2));
163
+ expect(process.emit("SIGINT", "SIGINT")).toBe(true);
164
+
165
+ await expect(supervised).resolves.toBe(130);
166
+ const events = (await readFile(markerPath, "utf8")).split("\n").filter(Boolean);
167
+ expect(events.filter((event) => event === "delivery").length).toBeGreaterThanOrEqual(1);
168
+ expect(events).toContain("graceful");
169
+ expect(events).not.toContain("force-quit");
170
+ });
171
+ });
@@ -7,6 +7,7 @@ export interface WebRoute {
7
7
  export interface WebServerConfig {
8
8
  readonly host?: string;
9
9
  readonly port?: number;
10
+ readonly allowedHosts?: readonly string[];
10
11
  }
11
12
  export interface WebServer {
12
13
  readonly host: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AACpF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEnD,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,cAAc,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChG;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,IAAI,CAAC;IACtC,gBAAgB,CAAC,OAAO,EAAE,QAAQ,CAAC,SAAS,CAAC,GAAG,MAAM,IAAI,CAAC;IAC3D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAE5C,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,SAAS,EAAE,SAAS,CAAC;KACtB;CACF;;;mBAkBsB,OAAO,UAAU,eAAe;;AAFvD,wBAsDE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AAEpF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAEnD,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,cAAc,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChG;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAEvB,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,IAAI,CAAC;IACtC,gBAAgB,CAAC,OAAO,EAAE,QAAQ,CAAC,SAAS,CAAC,GAAG,MAAM,IAAI,CAAC;IAC3D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAE5C,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,SAAS,EAAE,SAAS,CAAC;KACtB;CACF;;;mBAqGsB,OAAO,UAAU,eAAe;;AAFvD,wBA4DE"}
@@ -1,32 +1,114 @@
1
1
  import { createServer } from "node:http";
2
+ import { hostname as machineHostname, networkInterfaces } from "node:os";
2
3
  export const DEFAULT_WEB_SERVER_PORT = 3141;
3
4
  function normalizePath(path) {
4
5
  if (!path.startsWith("/") || (path.length > 1 && path.endsWith("/")))
5
6
  throw new Error("Web route path must be absolute without a trailing slash: " + path);
6
7
  return path;
7
8
  }
9
+ function sendError(response, status, message) {
10
+ response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
11
+ response.end(JSON.stringify({ error: message }));
12
+ }
8
13
  function notFound(response) {
9
- response.writeHead(404, { "content-type": "application/json; charset=utf-8" });
10
- response.end(JSON.stringify({ error: "Not found" }));
14
+ sendError(response, 404, "Not found");
11
15
  }
12
16
  function hostForUrl(host) {
13
17
  return host.includes(":") && !host.startsWith("[") ? "[" + host + "]" : host;
14
18
  }
19
+ const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "localhost", "[::1]"]);
20
+ const WILDCARD_HOSTS = new Set(["", "0.0.0.0", "::", "[::]"]);
21
+ // Parses an http authority (Host header value, or an Origin with its "http://" prefix removed) into its lowercase hostname (IPv6 stays bracketed, as WHATWG URL reports it) and effective port. Returns undefined for anything that is not a bare authority.
22
+ function parseHttpAuthority(value) {
23
+ let url;
24
+ try {
25
+ url = new URL("http://" + value);
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ if (url.username !== "" || url.password !== "" || url.pathname !== "/" || url.search !== "" || url.hash !== "" || url.hostname === "")
31
+ return undefined;
32
+ if (value.endsWith("/") || value.includes("?") || value.includes("#"))
33
+ return undefined;
34
+ return { hostname: url.hostname, port: url.port === "" ? 80 : Number(url.port) };
35
+ }
36
+ // Normalizes a configured host or allowlist entry the same way parseHttpAuthority normalizes a header value, so IPv6 forms and letter case compare equal. Returns undefined for values that are not a valid host.
37
+ function canonicalHostname(host) {
38
+ try {
39
+ return new URL("http://" + hostForUrl(host)).hostname;
40
+ }
41
+ catch {
42
+ return undefined;
43
+ }
44
+ }
45
+ // A wildcard bind answers on every local address, so the names a browser may legitimately put in the Host header are this machine's own addresses and hostname. They are read per request because DHCP and interface changes rewrite them while the server runs. An interface-scoped IPv6 address ("fe80::1%en0") is compared without its zone id, which is what a URL host carries.
46
+ function localHostnames() {
47
+ const names = new Set();
48
+ const own = canonicalHostname(machineHostname());
49
+ if (own !== undefined)
50
+ names.add(own);
51
+ for (const entries of Object.values(networkInterfaces())) {
52
+ for (const entry of entries ?? []) {
53
+ const name = canonicalHostname(entry.address.split("%")[0] ?? entry.address);
54
+ if (name !== undefined)
55
+ names.add(name);
56
+ }
57
+ }
58
+ return names;
59
+ }
60
+ function isHostnameAllowed(hostname, configuredHost, allowedHosts) {
61
+ if (LOOPBACK_HOSTNAMES.has(hostname))
62
+ return true;
63
+ if (allowedHosts.some((entry) => canonicalHostname(entry) === hostname))
64
+ return true;
65
+ if (!WILDCARD_HOSTS.has(configuredHost))
66
+ return hostname === hostForUrl(configuredHost).toLowerCase();
67
+ return localHostnames().has(hostname);
68
+ }
69
+ // Browsers send the Origin header on every cross-origin request and on all non-GET/HEAD requests, so requiring it to match the (already validated) Host header blocks cross-site requests from pages the user visits. Requests without an Origin header (same-origin navigations, EventSource, curl, Node fetch) pass. The Host check defeats DNS rebinding: a page on attacker.example that resolves to one of this machine's addresses arrives with Host: attacker.example, which is neither loopback, nor the bind address, nor an address or hostname this machine owns. The check stays on for a wildcard bind, where only the machine's own addresses (or an operator-supplied allowedHosts entry) are accepted, because the Origin check cannot help: a rebinding page sends a matching Origin and Host pair.
70
+ function rejectForeignRequest(request, response, configuredHost, boundPort, allowedHosts) {
71
+ const hostHeader = request.headers.host;
72
+ const authority = hostHeader === undefined ? undefined : parseHttpAuthority(hostHeader);
73
+ if (authority === undefined) {
74
+ sendError(response, 400, "Invalid Host header");
75
+ return true;
76
+ }
77
+ if (!isHostnameAllowed(authority.hostname, configuredHost, allowedHosts) || authority.port !== boundPort) {
78
+ sendError(response, 400, "Host header does not match the web server address");
79
+ return true;
80
+ }
81
+ const origin = request.headers.origin;
82
+ if (origin === undefined)
83
+ return false;
84
+ const originAuthority = origin.startsWith("http://") ? parseHttpAuthority(origin.slice("http://".length)) : undefined;
85
+ if (originAuthority === undefined || originAuthority.hostname !== authority.hostname || originAuthority.port !== authority.port) {
86
+ sendError(response, 403, "Cross-origin requests are not allowed");
87
+ return true;
88
+ }
89
+ return false;
90
+ }
15
91
  export default {
16
92
  name: "pi-webserver",
17
93
  async apply(context, config) {
18
94
  const host = config.host ?? "127.0.0.1";
19
95
  const port = config.port ?? DEFAULT_WEB_SERVER_PORT;
96
+ const allowedHosts = config.allowedHosts ?? [];
20
97
  const routes = new Map();
21
98
  let fallback;
99
+ // Assigned once listen() completes; tests bind port 0 so the configured port is not the one clients address.
100
+ let boundPort = 0;
22
101
  const server = createServer((request, response) => {
102
+ if (rejectForeignRequest(request, response, host, boundPort, allowedHosts))
103
+ return;
23
104
  const path = new URL(request.url ?? "/", "http://" + hostForUrl(host)).pathname;
24
105
  const handler = routes.get(path) ?? fallback;
25
106
  if (handler === undefined) {
26
107
  notFound(response);
27
108
  return;
28
109
  }
29
- void Promise.resolve(handler(request, response)).catch((error) => {
110
+ // Invoke inside an async function so a synchronous throw is routed into the same 500 path as an async rejection instead of escaping the request listener as an uncaught exception.
111
+ void (async () => handler(request, response))().catch((error) => {
30
112
  if (response.headersSent) {
31
113
  response.destroy(error instanceof Error ? error : new Error(String(error)));
32
114
  return;
@@ -45,6 +127,7 @@ export default {
45
127
  const address = server.address();
46
128
  if (address === null || typeof address === "string")
47
129
  throw new Error("Web server did not expose a TCP address");
130
+ boundPort = address.port;
48
131
  const service = {
49
132
  host,
50
133
  port: address.port,
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/host-webserver",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "private": true,
5
5
  "description": "Cordis route-registration service for the Pi Harness web host",
6
6
  "homepage": "https://github.com/pi-harness/pi-harness#readme",
@@ -1,4 +1,5 @@
1
1
  import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
2
+ import { hostname as machineHostname, networkInterfaces } from "node:os";
2
3
  import type { Context } from "@deepseek-ai/cordis";
3
4
 
4
5
  export interface WebRoute {
@@ -9,6 +10,8 @@ export interface WebRoute {
9
10
  export interface WebServerConfig {
10
11
  readonly host?: string;
11
12
  readonly port?: number;
13
+ // Extra hostnames accepted in the Host header, on top of loopback names, the bound address and (for a wildcard bind) this machine's own addresses and hostname. Needed when the harness is reached through a proxy or a DNS name the machine itself does not know about.
14
+ readonly allowedHosts?: readonly string[];
12
15
  }
13
16
 
14
17
  export interface WebServer {
@@ -33,30 +36,118 @@ function normalizePath(path: string): string {
33
36
  return path;
34
37
  }
35
38
 
39
+ function sendError(response: ServerResponse, status: number, message: string): void {
40
+ response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
41
+ response.end(JSON.stringify({ error: message }));
42
+ }
43
+
36
44
  function notFound(response: ServerResponse): void {
37
- response.writeHead(404, { "content-type": "application/json; charset=utf-8" });
38
- response.end(JSON.stringify({ error: "Not found" }));
45
+ sendError(response, 404, "Not found");
39
46
  }
40
47
 
41
48
  function hostForUrl(host: string): string {
42
49
  return host.includes(":") && !host.startsWith("[") ? "[" + host + "]" : host;
43
50
  }
44
51
 
52
+ const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "localhost", "[::1]"]);
53
+ const WILDCARD_HOSTS = new Set(["", "0.0.0.0", "::", "[::]"]);
54
+
55
+ interface Authority {
56
+ readonly hostname: string;
57
+ readonly port: number;
58
+ }
59
+
60
+ // Parses an http authority (Host header value, or an Origin with its "http://" prefix removed) into its lowercase hostname (IPv6 stays bracketed, as WHATWG URL reports it) and effective port. Returns undefined for anything that is not a bare authority.
61
+ function parseHttpAuthority(value: string): Authority | undefined {
62
+ let url: URL;
63
+ try {
64
+ url = new URL("http://" + value);
65
+ } catch {
66
+ return undefined;
67
+ }
68
+ if (url.username !== "" || url.password !== "" || url.pathname !== "/" || url.search !== "" || url.hash !== "" || url.hostname === "") return undefined;
69
+ if (value.endsWith("/") || value.includes("?") || value.includes("#")) return undefined;
70
+ return { hostname: url.hostname, port: url.port === "" ? 80 : Number(url.port) };
71
+ }
72
+
73
+ // Normalizes a configured host or allowlist entry the same way parseHttpAuthority normalizes a header value, so IPv6 forms and letter case compare equal. Returns undefined for values that are not a valid host.
74
+ function canonicalHostname(host: string): string | undefined {
75
+ try {
76
+ return new URL("http://" + hostForUrl(host)).hostname;
77
+ } catch {
78
+ return undefined;
79
+ }
80
+ }
81
+
82
+ // A wildcard bind answers on every local address, so the names a browser may legitimately put in the Host header are this machine's own addresses and hostname. They are read per request because DHCP and interface changes rewrite them while the server runs. An interface-scoped IPv6 address ("fe80::1%en0") is compared without its zone id, which is what a URL host carries.
83
+ function localHostnames(): Set<string> {
84
+ const names = new Set<string>();
85
+ const own = canonicalHostname(machineHostname());
86
+ if (own !== undefined) names.add(own);
87
+ for (const entries of Object.values(networkInterfaces())) {
88
+ for (const entry of entries ?? []) {
89
+ const name = canonicalHostname(entry.address.split("%")[0] ?? entry.address);
90
+ if (name !== undefined) names.add(name);
91
+ }
92
+ }
93
+ return names;
94
+ }
95
+
96
+ function isHostnameAllowed(hostname: string, configuredHost: string, allowedHosts: readonly string[]): boolean {
97
+ if (LOOPBACK_HOSTNAMES.has(hostname)) return true;
98
+ if (allowedHosts.some((entry) => canonicalHostname(entry) === hostname)) return true;
99
+ if (!WILDCARD_HOSTS.has(configuredHost)) return hostname === hostForUrl(configuredHost).toLowerCase();
100
+ return localHostnames().has(hostname);
101
+ }
102
+
103
+ // Browsers send the Origin header on every cross-origin request and on all non-GET/HEAD requests, so requiring it to match the (already validated) Host header blocks cross-site requests from pages the user visits. Requests without an Origin header (same-origin navigations, EventSource, curl, Node fetch) pass. The Host check defeats DNS rebinding: a page on attacker.example that resolves to one of this machine's addresses arrives with Host: attacker.example, which is neither loopback, nor the bind address, nor an address or hostname this machine owns. The check stays on for a wildcard bind, where only the machine's own addresses (or an operator-supplied allowedHosts entry) are accepted, because the Origin check cannot help: a rebinding page sends a matching Origin and Host pair.
104
+ function rejectForeignRequest(
105
+ request: IncomingMessage,
106
+ response: ServerResponse,
107
+ configuredHost: string,
108
+ boundPort: number,
109
+ allowedHosts: readonly string[],
110
+ ): boolean {
111
+ const hostHeader = request.headers.host;
112
+ const authority = hostHeader === undefined ? undefined : parseHttpAuthority(hostHeader);
113
+ if (authority === undefined) {
114
+ sendError(response, 400, "Invalid Host header");
115
+ return true;
116
+ }
117
+ if (!isHostnameAllowed(authority.hostname, configuredHost, allowedHosts) || authority.port !== boundPort) {
118
+ sendError(response, 400, "Host header does not match the web server address");
119
+ return true;
120
+ }
121
+ const origin = request.headers.origin;
122
+ if (origin === undefined) return false;
123
+ const originAuthority = origin.startsWith("http://") ? parseHttpAuthority(origin.slice("http://".length)) : undefined;
124
+ if (originAuthority === undefined || originAuthority.hostname !== authority.hostname || originAuthority.port !== authority.port) {
125
+ sendError(response, 403, "Cross-origin requests are not allowed");
126
+ return true;
127
+ }
128
+ return false;
129
+ }
130
+
45
131
  export default {
46
132
  name: "pi-webserver",
47
133
  async apply(context: Context, config: WebServerConfig) {
48
134
  const host = config.host ?? "127.0.0.1";
49
135
  const port = config.port ?? DEFAULT_WEB_SERVER_PORT;
136
+ const allowedHosts = config.allowedHosts ?? [];
50
137
  const routes = new Map<string, WebRoute["handler"]>();
51
138
  let fallback: WebRoute["handler"] | undefined;
139
+ // Assigned once listen() completes; tests bind port 0 so the configured port is not the one clients address.
140
+ let boundPort = 0;
52
141
  const server = createServer((request, response) => {
142
+ if (rejectForeignRequest(request, response, host, boundPort, allowedHosts)) return;
53
143
  const path = new URL(request.url ?? "/", "http://" + hostForUrl(host)).pathname;
54
144
  const handler = routes.get(path) ?? fallback;
55
145
  if (handler === undefined) {
56
146
  notFound(response);
57
147
  return;
58
148
  }
59
- void Promise.resolve(handler(request, response)).catch((error: unknown) => {
149
+ // Invoke inside an async function so a synchronous throw is routed into the same 500 path as an async rejection instead of escaping the request listener as an uncaught exception.
150
+ void (async () => handler(request, response))().catch((error: unknown) => {
60
151
  if (response.headersSent) {
61
152
  response.destroy(error instanceof Error ? error : new Error(String(error)));
62
153
  return;
@@ -74,6 +165,7 @@ export default {
74
165
  });
75
166
  const address = server.address();
76
167
  if (address === null || typeof address === "string") throw new Error("Web server did not expose a TCP address");
168
+ boundPort = address.port;
77
169
  const service: WebServer = {
78
170
  host,
79
171
  port: address.port,