@piagent/platform 1.3.1 → 1.4.1

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 (329) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/README.md +10 -4
  3. package/README.vi.md +27 -0
  4. package/architecture/layers.json +96 -1
  5. package/docs/README.md +1 -0
  6. package/docs/architecture.md +1 -0
  7. package/docs/authority-manifest.md +1 -0
  8. package/docs/auto-delegation-policy.md +1 -0
  9. package/docs/capability-packs.md +1 -0
  10. package/docs/command-reference-vietnamese.md +5 -4
  11. package/docs/context-engine.md +1 -0
  12. package/docs/context-window-policy.md +1 -0
  13. package/docs/distribution-standard.md +8 -7
  14. package/docs/en/architecture.md +13 -0
  15. package/docs/herdr-workflow.md +1 -0
  16. package/docs/languages.json +20 -4
  17. package/docs/mcp-and-tools.md +125 -9
  18. package/docs/memory-policy.md +1 -0
  19. package/docs/model-options.md +1 -0
  20. package/docs/oauth-providers.md +1 -0
  21. package/docs/operator-manual-vietnamese.md +15 -12
  22. package/docs/package-architecture-notes.md +1 -0
  23. package/docs/project-adapters.md +1 -0
  24. package/docs/project-onboarding.md +1 -0
  25. package/docs/publishing-for-teams.md +37 -0
  26. package/docs/quality-benchmark.md +1 -0
  27. package/docs/quickstart-vietnamese.md +3 -2
  28. package/docs/release-install-policy.md +22 -8
  29. package/docs/runtime-harness-standard.md +2 -1
  30. package/docs/runtime-policy-design.md +1 -0
  31. package/docs/runtime-quality-baseline.md +1 -0
  32. package/docs/security-redaction-benchmark.md +1 -0
  33. package/docs/security-threat-model.md +2 -1
  34. package/docs/subagent-orchestration-capabilities.md +1 -0
  35. package/docs/subagents-and-multiagent.md +1 -0
  36. package/docs/task-implementation-contract.md +12 -4
  37. package/docs/task-lifecycle-vietnamese.md +1 -0
  38. package/docs/team-onboarding.md +3 -2
  39. package/docs/third-party-neutrality.md +26 -0
  40. package/docs/usage-observability.md +71 -0
  41. package/docs/vercel-docs-site.md +1 -0
  42. package/docs/vi/architecture.md +13 -0
  43. package/docs/workflow-recipes.md +1 -0
  44. package/docs-site/architecture.html +5 -1
  45. package/docs-site/benchmark.html +5 -1
  46. package/docs-site/commands.html +5 -1
  47. package/docs-site/content/en/index.html +49 -0
  48. package/docs-site/content/en/runtime.html +13 -0
  49. package/docs-site/content/index.html +49 -0
  50. package/docs-site/content/runtime.html +28 -0
  51. package/docs-site/context-engine.html +5 -1
  52. package/docs-site/en/architecture.html +5 -1
  53. package/docs-site/en/benchmark.html +5 -1
  54. package/docs-site/en/commands.html +5 -1
  55. package/docs-site/en/context-engine.html +5 -1
  56. package/docs-site/en/index.html +54 -1
  57. package/docs-site/en/mcp-approval.html +5 -1
  58. package/docs-site/en/mcp-auth.html +5 -1
  59. package/docs-site/en/mcp-servers.html +5 -1
  60. package/docs-site/en/mcp.html +5 -1
  61. package/docs-site/en/permissions.html +5 -1
  62. package/docs-site/en/profiles.html +5 -1
  63. package/docs-site/en/quickstart.html +5 -1
  64. package/docs-site/en/runtime.html +18 -1
  65. package/docs-site/en/scope.html +5 -1
  66. package/docs-site/en/security.html +5 -1
  67. package/docs-site/en/team.html +5 -1
  68. package/docs-site/en/whats-new.html +5 -1
  69. package/docs-site/en/workflows.html +5 -1
  70. package/docs-site/index.html +54 -1
  71. package/docs-site/mcp-approval.html +5 -1
  72. package/docs-site/mcp-auth.html +5 -1
  73. package/docs-site/mcp-servers.html +5 -1
  74. package/docs-site/mcp.html +5 -1
  75. package/docs-site/permissions.html +5 -1
  76. package/docs-site/profiles.html +5 -1
  77. package/docs-site/quickstart.html +5 -1
  78. package/docs-site/runtime.html +33 -1
  79. package/docs-site/scope.html +5 -1
  80. package/docs-site/security.html +5 -1
  81. package/docs-site/team.html +5 -1
  82. package/docs-site/whats-new.html +5 -1
  83. package/docs-site/workflows.html +5 -1
  84. package/evals/fixtures/mutation-provenance-record.invalid.json +17 -0
  85. package/evals/fixtures/mutation-provenance-record.valid.json +27 -0
  86. package/evals/fixtures/piagent-webui/approval-v1.invalid.json +54 -0
  87. package/evals/fixtures/piagent-webui/approval-v1.valid.json +58 -0
  88. package/evals/fixtures/piagent-webui/attachment-v1.invalid.json +25 -0
  89. package/evals/fixtures/piagent-webui/attachment-v1.valid.json +25 -0
  90. package/evals/fixtures/piagent-webui/capabilities-v1.invalid.json +51 -0
  91. package/evals/fixtures/piagent-webui/capabilities-v1.valid.json +122 -0
  92. package/evals/fixtures/piagent-webui/commit-summary-v1.invalid.json +10 -0
  93. package/evals/fixtures/piagent-webui/commit-summary-v1.valid.json +25 -0
  94. package/evals/fixtures/piagent-webui/control-command-v1.invalid.json +37 -0
  95. package/evals/fixtures/piagent-webui/control-command-v1.valid.json +35 -0
  96. package/evals/fixtures/piagent-webui/diff-v1.invalid.json +13 -0
  97. package/evals/fixtures/piagent-webui/diff-v1.valid.json +141 -0
  98. package/evals/fixtures/piagent-webui/gateway-capabilities-v1.invalid.json +35 -0
  99. package/evals/fixtures/piagent-webui/gateway-capabilities-v1.valid.json +35 -0
  100. package/evals/fixtures/piagent-webui/gateway-protocol-v1.invalid.json +14 -0
  101. package/evals/fixtures/piagent-webui/gateway-protocol-v1.valid.json +14 -0
  102. package/evals/fixtures/piagent-webui/handoff-history-v1.invalid.json +16 -0
  103. package/evals/fixtures/piagent-webui/handoff-history-v1.valid.json +16 -0
  104. package/evals/fixtures/piagent-webui/model-catalog-v1.invalid.json +31 -0
  105. package/evals/fixtures/piagent-webui/model-catalog-v1.valid.json +43 -0
  106. package/evals/fixtures/piagent-webui/queue-v1.invalid.json +29 -0
  107. package/evals/fixtures/piagent-webui/queue-v1.valid.json +44 -0
  108. package/evals/fixtures/piagent-webui/recovery-history-v1.invalid.json +17 -0
  109. package/evals/fixtures/piagent-webui/recovery-history-v1.valid.json +20 -0
  110. package/evals/fixtures/piagent-webui/release-monitor-v1.invalid.json +12 -0
  111. package/evals/fixtures/piagent-webui/release-monitor-v1.valid.json +29 -0
  112. package/evals/fixtures/piagent-webui/review-state-v1.invalid.json +25 -0
  113. package/evals/fixtures/piagent-webui/review-state-v1.valid.json +38 -0
  114. package/evals/fixtures/piagent-webui/runtime-event-v2.invalid.json +49 -0
  115. package/evals/fixtures/piagent-webui/runtime-event-v2.valid.json +49 -0
  116. package/evals/fixtures/piagent-webui/session-catalog-v1.invalid.json +17 -0
  117. package/evals/fixtures/piagent-webui/session-catalog-v1.valid.json +58 -0
  118. package/evals/fixtures/piagent-webui/session-command-v1.invalid.json +19 -0
  119. package/evals/fixtures/piagent-webui/session-command-v1.valid.json +19 -0
  120. package/evals/fixtures/piagent-webui/snapshot-v1.invalid.json +5 -0
  121. package/evals/fixtures/piagent-webui/snapshot-v1.valid.json +191 -0
  122. package/evals/fixtures/piagent-webui/source-change-v1.invalid.json +57 -0
  123. package/evals/fixtures/piagent-webui/source-change-v1.valid.json +98 -0
  124. package/evals/fixtures/piagent-webui/source-mutation-v1.invalid.json +19 -0
  125. package/evals/fixtures/piagent-webui/source-mutation-v1.valid.json +38 -0
  126. package/evals/fixtures/piagent-webui/source-revert-v1.invalid.json +20 -0
  127. package/evals/fixtures/piagent-webui/source-revert-v1.valid.json +62 -0
  128. package/evals/fixtures/piagent-webui/subagent-tree-v1.invalid.json +26 -0
  129. package/evals/fixtures/piagent-webui/subagent-tree-v1.valid.json +39 -0
  130. package/evals/fixtures/piagent-webui/task-index-v1.invalid.json +15 -0
  131. package/evals/fixtures/piagent-webui/task-index-v1.valid.json +15 -0
  132. package/evals/fixtures/piagent-webui/task-timeline-v1.invalid.json +14 -0
  133. package/evals/fixtures/piagent-webui/task-timeline-v1.valid.json +16 -0
  134. package/evals/fixtures/piagent-webui/transcript-v1.invalid.json +35 -0
  135. package/evals/fixtures/piagent-webui/transcript-v1.valid.json +55 -0
  136. package/evals/fixtures/task-baseline-manifest.invalid.json +16 -0
  137. package/evals/fixtures/task-baseline-manifest.valid.json +41 -0
  138. package/evals/fixtures/verifier-file-snapshot.invalid.json +21 -0
  139. package/evals/fixtures/verifier-file-snapshot.valid.json +21 -0
  140. package/package.json +43 -4
  141. package/packages/piagent-core/README.md +3 -2
  142. package/packages/piagent-core/benchmark/benchmark-candidate.d.ts +1 -0
  143. package/packages/piagent-core/benchmark/benchmark-forensics.d.ts +1 -0
  144. package/packages/piagent-core/benchmark/benchmark-ledger.d.ts +4 -0
  145. package/packages/piagent-core/benchmark/benchmark-record-validation.d.ts +1 -0
  146. package/packages/piagent-core/benchmark/benchmark-runtime.d.ts +1 -0
  147. package/packages/piagent-core/capabilities/capability-core.js +2 -2
  148. package/packages/piagent-core/capabilities/capability-verification-cache.js +17 -7
  149. package/packages/piagent-core/capabilities/runtime-integrity.js +190 -0
  150. package/packages/piagent-core/extensions/piagent-guard.ts +80 -272
  151. package/packages/piagent-core/extensions/redaction-core.d.ts +2 -0
  152. package/packages/piagent-core/extensions/runtime-evidence.js +12 -1
  153. package/packages/piagent-core/extensions/shell-reach.ts +250 -0
  154. package/packages/piagent-core/extensions/task-journal.js +30 -1
  155. package/packages/piagent-core/extensions/task-state.js +33 -36
  156. package/packages/piagent-core/mcp/mcp-command-actions.js +1 -1
  157. package/packages/piagent-core/mcp/mcp-server-catalog.js +139 -9
  158. package/packages/piagent-core/package.json +1 -1
  159. package/packages/piagent-core/runtime/hooks/session-start-hook.ts +2 -2
  160. package/packages/piagent-core/runtime/hooks/tool-call-hook.ts +12 -2
  161. package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +20 -19
  162. package/packages/piagent-core/runtime/hooks/tool-result-metadata.ts +13 -0
  163. package/packages/piagent-core/runtime/input/chat-images.ts +2 -2
  164. package/packages/piagent-core/runtime/inspection/activity-event-adapter.ts +94 -0
  165. package/packages/piagent-core/runtime/inspection/approval-broker.ts +318 -0
  166. package/packages/piagent-core/runtime/inspection/commit-summary-projection.ts +59 -0
  167. package/packages/piagent-core/runtime/inspection/context-telemetry-inspection.ts +37 -0
  168. package/packages/piagent-core/runtime/inspection/criteria-links.ts +295 -0
  169. package/packages/piagent-core/runtime/inspection/diff-projection.ts +499 -0
  170. package/packages/piagent-core/runtime/inspection/git-filter-safety.ts +38 -0
  171. package/packages/piagent-core/runtime/inspection/git-numstat-adapter.ts +71 -0
  172. package/packages/piagent-core/runtime/inspection/git-status-adapter.ts +493 -0
  173. package/packages/piagent-core/runtime/inspection/git-tree-adapter.ts +108 -0
  174. package/packages/piagent-core/runtime/inspection/mcp-control.ts +10 -0
  175. package/packages/piagent-core/runtime/inspection/mutation-provenance-contract.ts +131 -0
  176. package/packages/piagent-core/runtime/inspection/mutation-provenance-recorder.ts +48 -0
  177. package/packages/piagent-core/runtime/inspection/mutation-provenance-store.ts +161 -0
  178. package/packages/piagent-core/runtime/inspection/project-runtime-inspection.ts +36 -0
  179. package/packages/piagent-core/runtime/inspection/review-state-contract.ts +108 -0
  180. package/packages/piagent-core/runtime/inspection/review-state-projection.ts +93 -0
  181. package/packages/piagent-core/runtime/inspection/review-state-store.ts +112 -0
  182. package/packages/piagent-core/runtime/inspection/runtime-event-store.ts +262 -0
  183. package/packages/piagent-core/runtime/inspection/same-process-bridge-proof.ts +103 -0
  184. package/packages/piagent-core/runtime/inspection/source-change-projection.ts +471 -0
  185. package/packages/piagent-core/runtime/inspection/source-evidence-contract.ts +139 -0
  186. package/packages/piagent-core/runtime/inspection/source-evidence-store.ts +315 -0
  187. package/packages/piagent-core/runtime/inspection/source-handoff-store.ts +70 -0
  188. package/packages/piagent-core/runtime/inspection/source-inspection-plan.ts +34 -0
  189. package/packages/piagent-core/runtime/inspection/source-mutation-projection.ts +164 -0
  190. package/packages/piagent-core/runtime/inspection/source-mutation-store.ts +168 -0
  191. package/packages/piagent-core/runtime/inspection/source-open-target.ts +38 -0
  192. package/packages/piagent-core/runtime/inspection/source-revert-projection.ts +147 -0
  193. package/packages/piagent-core/runtime/inspection/task-baseline-start-capture.ts +22 -0
  194. package/packages/piagent-core/runtime/inspection/task-compaction-history.ts +160 -0
  195. package/packages/piagent-core/runtime/inspection/task-control-journal.ts +131 -0
  196. package/packages/piagent-core/runtime/inspection/task-handoff-history.ts +95 -0
  197. package/packages/piagent-core/runtime/inspection/task-provenance-projection.ts +60 -0
  198. package/packages/piagent-core/runtime/inspection/task-recovery-timeline.ts +88 -0
  199. package/packages/piagent-core/runtime/inspection/task-run-index.ts +87 -0
  200. package/packages/piagent-core/runtime/inspection/task-source-projection.ts +401 -0
  201. package/packages/piagent-core/runtime/inspection/task-subagent-tree.ts +101 -0
  202. package/packages/piagent-core/runtime/inspection/text-diff.ts +60 -0
  203. package/packages/piagent-core/runtime/inspection/verifier-snapshot-contract.ts +114 -0
  204. package/packages/piagent-core/runtime/inspection/verifier-snapshot-store.ts +189 -0
  205. package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +320 -0
  206. package/packages/piagent-core/runtime/inspection/workspace-file-reader.ts +132 -0
  207. package/packages/piagent-core/runtime/inspection/workspace-source-projection.ts +96 -0
  208. package/packages/piagent-core/runtime/inspection/zero-turn-conformance.ts +175 -0
  209. package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +93 -6
  210. package/packages/piagent-core/runtime/policy/block-remedy.ts +139 -0
  211. package/packages/piagent-core/runtime/policy/source-index-transaction.ts +173 -0
  212. package/packages/piagent-core/runtime/policy/source-mutation-guard-binding.ts +42 -0
  213. package/packages/piagent-core/runtime/policy/source-mutation-guard.ts +184 -0
  214. package/packages/piagent-core/runtime/policy/source-worktree-transaction.ts +105 -0
  215. package/packages/piagent-core/runtime/product/activity-inspector-footer.ts +5 -3
  216. package/packages/piagent-core/runtime/product/activity-inspector.ts +85 -343
  217. package/packages/piagent-core/runtime/recovery/resume-state.ts +38 -1
  218. package/packages/piagent-core/runtime/registration/activity-inspector-command.ts +68 -14
  219. package/packages/piagent-core/runtime/registration/task-start-tool.ts +22 -27
  220. package/packages/piagent-core/runtime/session/model-authorship-state.ts +31 -3
  221. package/packages/piagent-core/runtime/session/runtime-state.ts +11 -1
  222. package/packages/piagent-core/runtime/session/token-economics.js +34 -0
  223. package/packages/piagent-core/runtime/tools/phase-tool-runtime.ts +2 -1
  224. package/packages/piagent-core/runtime/workflows/task-intake.ts +5 -0
  225. package/packages/piagent-core/security/sensitive-data.js +7 -0
  226. package/packages/piagent-webui/dist/client/assets/index-DwKCWbwd.css +1 -0
  227. package/packages/piagent-webui/dist/client/assets/index-bu7XpD9Q.js +172 -0
  228. package/packages/piagent-webui/dist/client/index.html +16 -0
  229. package/packages/piagent-webui/extension/attachment-store.ts +323 -0
  230. package/packages/piagent-webui/extension/held-message-queue.ts +311 -0
  231. package/packages/piagent-webui/extension/lifecycle-controller.ts +423 -0
  232. package/packages/piagent-webui/extension/lifecycle-event-adapter.ts +23 -0
  233. package/packages/piagent-webui/extension/piagent-webui.ts +470 -0
  234. package/packages/piagent-webui/extension/review-controller.ts +160 -0
  235. package/packages/piagent-webui/extension/same-session-bridge.ts +500 -0
  236. package/packages/piagent-webui/extension/session-options-controller.ts +281 -0
  237. package/packages/piagent-webui/extension/session-stream-adapter.ts +249 -0
  238. package/packages/piagent-webui/extension/source-mutation-controller.ts +215 -0
  239. package/packages/piagent-webui/extension/source-open-controller.ts +136 -0
  240. package/packages/piagent-webui/extension/source-revert-controller.ts +189 -0
  241. package/packages/piagent-webui/extension/terminal-session-adapter.ts +73 -0
  242. package/packages/piagent-webui/extension/vscode-handoff.ts +51 -0
  243. package/packages/piagent-webui/gateway/control-socket.ts +92 -0
  244. package/packages/piagent-webui/gateway/extension-authority.ts +47 -0
  245. package/packages/piagent-webui/gateway/gateway-events.ts +60 -0
  246. package/packages/piagent-webui/gateway/gateway-protocol-service.ts +64 -0
  247. package/packages/piagent-webui/gateway/gateway-service.ts +215 -0
  248. package/packages/piagent-webui/gateway/gateway-session-stream.ts +110 -0
  249. package/packages/piagent-webui/gateway/mcp-auth-broker.ts +158 -0
  250. package/packages/piagent-webui/gateway/native-project-picker.ts +29 -0
  251. package/packages/piagent-webui/gateway/pi-host.ts +41 -0
  252. package/packages/piagent-webui/gateway/profile-state.ts +1 -0
  253. package/packages/piagent-webui/gateway/project-registry.ts +108 -0
  254. package/packages/piagent-webui/gateway/provider-auth-broker.ts +172 -0
  255. package/packages/piagent-webui/gateway/session-catalog.ts +128 -0
  256. package/packages/piagent-webui/gateway/session-command-controller.ts +237 -0
  257. package/packages/piagent-webui/gateway/session-command-store.ts +224 -0
  258. package/packages/piagent-webui/gateway/session-inspection-registry.ts +239 -0
  259. package/packages/piagent-webui/gateway/session-lease-store.ts +1 -0
  260. package/packages/piagent-webui/gateway/session-metadata-store.ts +152 -0
  261. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +489 -0
  262. package/packages/piagent-webui/ownership/gateway-runtime-context.ts +8 -0
  263. package/packages/piagent-webui/ownership/profile-state.ts +117 -0
  264. package/packages/piagent-webui/ownership/session-lease-store.ts +329 -0
  265. package/packages/piagent-webui/ownership/session-refs.ts +8 -0
  266. package/packages/piagent-webui/package.json +33 -0
  267. package/packages/piagent-webui/server/README.md +4 -0
  268. package/packages/piagent-webui/server/benchmark-release-monitor.ts +230 -0
  269. package/packages/piagent-webui/server/core-inspection-provider.ts +403 -0
  270. package/packages/piagent-webui/server/gateway-websocket.ts +143 -0
  271. package/packages/piagent-webui/server/http-security.ts +27 -0
  272. package/packages/piagent-webui/server/ipc-read-model-client.ts +89 -0
  273. package/packages/piagent-webui/server/loopback-server.ts +368 -0
  274. package/packages/piagent-webui/server/read-model-provider.ts +37 -0
  275. package/packages/piagent-webui/server/read-only-router.ts +105 -0
  276. package/packages/piagent-webui/server/session-auth.ts +88 -0
  277. package/packages/piagent-webui/server/sidecar-main.ts +86 -0
  278. package/packages/piagent-webui/server/sse-hub.ts +76 -0
  279. package/packages/piagent-webui/server/static-bundle.ts +40 -0
  280. package/packages/piagent-webui/server/transcript-projection.ts +125 -0
  281. package/schemas/mutation-provenance-record.schema.json +100 -0
  282. package/schemas/piagent-webui/approval-v1.schema.json +654 -0
  283. package/schemas/piagent-webui/attachment-v1.schema.json +267 -0
  284. package/schemas/piagent-webui/capabilities-v1.schema.json +729 -0
  285. package/schemas/piagent-webui/catalog-v1.json +240 -0
  286. package/schemas/piagent-webui/commit-summary-v1.schema.json +63 -0
  287. package/schemas/piagent-webui/common-v1.schema.json +479 -0
  288. package/schemas/piagent-webui/control-command-v1.schema.json +1780 -0
  289. package/schemas/piagent-webui/diff-v1.schema.json +826 -0
  290. package/schemas/piagent-webui/gateway-capabilities-v1.schema.json +136 -0
  291. package/schemas/piagent-webui/gateway-protocol-v1.schema.json +456 -0
  292. package/schemas/piagent-webui/handoff-history-v1.schema.json +92 -0
  293. package/schemas/piagent-webui/model-catalog-v1.schema.json +95 -0
  294. package/schemas/piagent-webui/queue-v1.schema.json +99 -0
  295. package/schemas/piagent-webui/recovery-history-v1.schema.json +144 -0
  296. package/schemas/piagent-webui/release-monitor-v1.schema.json +173 -0
  297. package/schemas/piagent-webui/review-state-v1.schema.json +206 -0
  298. package/schemas/piagent-webui/runtime-event-v2.schema.json +5206 -0
  299. package/schemas/piagent-webui/session-catalog-v1.schema.json +222 -0
  300. package/schemas/piagent-webui/session-command-v1.schema.json +390 -0
  301. package/schemas/piagent-webui/snapshot-v1.schema.json +2622 -0
  302. package/schemas/piagent-webui/source-change-v1.schema.json +763 -0
  303. package/schemas/piagent-webui/source-mutation-v1.schema.json +96 -0
  304. package/schemas/piagent-webui/source-revert-v1.schema.json +130 -0
  305. package/schemas/piagent-webui/subagent-tree-v1.schema.json +88 -0
  306. package/schemas/piagent-webui/task-index-v1.schema.json +74 -0
  307. package/schemas/piagent-webui/task-timeline-v1.schema.json +49 -0
  308. package/schemas/piagent-webui/transcript-v1.schema.json +243 -0
  309. package/schemas/task-baseline-manifest.schema.json +122 -0
  310. package/schemas/verifier-file-snapshot.schema.json +81 -0
  311. package/scripts/build-docs-site.mjs +21 -1
  312. package/scripts/check-architecture.mjs +1 -0
  313. package/scripts/check-doc-languages.mjs +19 -2
  314. package/scripts/check-third-party-neutrality.mjs +43 -0
  315. package/scripts/explain-command.mjs +240 -0
  316. package/scripts/install-global.sh +105 -4
  317. package/scripts/mcp-manage.mjs +17 -4
  318. package/scripts/pi-session-stats.sh +10 -2
  319. package/scripts/piagent-cli.mjs +30 -3
  320. package/scripts/piagent-dashboard.mjs +261 -0
  321. package/scripts/piagent-webui-launcher.mjs +121 -0
  322. package/scripts/setup.sh +69 -33
  323. package/scripts/traffic-snapshot.mjs +214 -0
  324. package/scripts/typescript-loader.mjs +42 -1
  325. package/scripts/uninstall-global.sh +53 -2
  326. package/scripts/update-global.mjs +33 -1
  327. package/scripts/verify-local.sh +257 -0
  328. package/scripts/verify-release-identity.mjs +4 -1
  329. package/scripts/verify-translation-pairing.mjs +121 -0
@@ -0,0 +1,89 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ import type { SourceView, StreamEvent, StreamReplay, WebUiReadModelProvider } from "./read-model-provider.ts";
4
+
5
+ type ResponseMessage = { channel: "piagent-webui"; type: "response"; requestId: string; ok: boolean; value?: unknown; error?: string };
6
+ type EventMessage = { channel: "piagent-webui"; type: "event"; event: StreamEvent };
7
+ type Pending = { resolve(value: unknown): void; reject(error: Error): void; timer: NodeJS.Timeout };
8
+
9
+ export class IpcReadModelClient implements WebUiReadModelProvider {
10
+ readonly #pending = new Map<string, Pending>();
11
+ readonly #listeners = new Set<(event: StreamEvent) => void>();
12
+ readonly #timeoutMs: number;
13
+
14
+ constructor(timeoutMs = 30_000) {
15
+ if (typeof process.send !== "function") throw new Error("webui-sidecar-ipc-unavailable");
16
+ this.#timeoutMs = timeoutMs;
17
+ process.on("message", this.#onMessage);
18
+ process.once("disconnect", this.close);
19
+ }
20
+
21
+ readonly #onMessage = (message: unknown) => {
22
+ if (!message || typeof message !== "object") return;
23
+ const value = message as Record<string, unknown>;
24
+ if (value.channel !== "piagent-webui") return;
25
+ if (value.type === "event" && value.event && typeof value.event === "object") {
26
+ for (const listener of this.#listeners) listener(value.event as StreamEvent);
27
+ return;
28
+ }
29
+ if (value.type !== "response" || typeof value.requestId !== "string") return;
30
+ const pending = this.#pending.get(value.requestId);
31
+ if (!pending) return;
32
+ clearTimeout(pending.timer);
33
+ this.#pending.delete(value.requestId);
34
+ if (value.ok) pending.resolve(value.value);
35
+ else pending.reject(new Error(typeof value.error === "string" ? value.error : "webui-parent-request-failed"));
36
+ };
37
+
38
+ #request(method: string, args: unknown[] = []): Promise<any> {
39
+ const requestId = `request.${randomUUID()}`;
40
+ return new Promise((resolve, reject) => {
41
+ const timer = setTimeout(() => {
42
+ this.#pending.delete(requestId);
43
+ reject(new Error("webui-parent-request-timeout"));
44
+ }, this.#timeoutMs);
45
+ timer.unref();
46
+ this.#pending.set(requestId, { resolve, reject, timer });
47
+ process.send?.({ channel: "piagent-webui", type: "request", requestId, method, args });
48
+ });
49
+ }
50
+
51
+ capabilities(): Promise<unknown> { return this.#request("capabilities"); }
52
+ snapshot(): Promise<unknown> { return this.#request("snapshot"); }
53
+ sourceChanges(view: SourceView): Promise<unknown> { return this.#request("sourceChanges", [view]); }
54
+ diff(view: SourceView, fileRef: string): Promise<unknown> { return this.#request("diff", [view, fileRef]); }
55
+ review(view: SourceView, fileRef: string): Promise<unknown> { return this.#request("review", [view, fileRef]); }
56
+ sourceMutation(action: "source.stage" | "source.unstage", fileRef: string): Promise<unknown> { return this.#request("sourceMutation", [action, fileRef]); }
57
+ sourceRevert(fileRef: string, hunkRef: string | null): Promise<unknown> { return this.#request("sourceRevert", [fileRef, hunkRef]); }
58
+ commitSummary(): Promise<unknown> { return this.#request("commitSummary"); }
59
+ taskIndex(): Promise<unknown> { return this.#request("taskIndex"); }
60
+ taskTimeline(runRef: string): Promise<unknown> { return this.#request("taskTimeline", [runRef]); }
61
+ recoveryHistory(runRef: string): Promise<unknown> { return this.#request("recoveryHistory", [runRef]); }
62
+ handoffHistory(runRef: string): Promise<unknown> { return this.#request("handoffHistory", [runRef]); }
63
+ subagentTree(runRef: string): Promise<unknown> { return this.#request("subagentTree", [runRef]); }
64
+ releaseMonitor(): Promise<unknown> { return this.#request("releaseMonitor"); }
65
+ activity(): Promise<unknown> { return this.#request("activity"); }
66
+ logPreview(activityRef: string): Promise<unknown> { return this.#request("logPreview", [activityRef]); }
67
+ transcript(beforeCursor: string | null, limit: number): Promise<unknown> { return this.#request("transcript", [beforeCursor, limit]); }
68
+ queue(): Promise<unknown> { return this.#request("queue"); }
69
+ modelCatalog(): Promise<unknown> { return this.#request("modelCatalog"); }
70
+ approval(approvalRef: string): Promise<unknown> { return this.#request("approval", [approvalRef]); }
71
+ executeControl(command: unknown): Promise<unknown> { return this.#request("control", [command]); }
72
+ executeAttachment(command: unknown): Promise<unknown> { return this.#request("attachment", [command]); }
73
+ executeApproval(approvalRef: string, decision: unknown): Promise<unknown> { return this.#request("approvalDecision", [approvalRef, decision]); }
74
+ replay(after: string | null, limit: number): Promise<StreamReplay> { return this.#request("replay", [after, limit]); }
75
+ subscribe(listener: (event: StreamEvent) => void): () => void {
76
+ this.#listeners.add(listener);
77
+ return () => this.#listeners.delete(listener);
78
+ }
79
+
80
+ close = (): void => {
81
+ process.off("message", this.#onMessage);
82
+ for (const pending of this.#pending.values()) {
83
+ clearTimeout(pending.timer);
84
+ pending.reject(new Error("webui-parent-disconnected"));
85
+ }
86
+ this.#pending.clear();
87
+ this.#listeners.clear();
88
+ };
89
+ }
@@ -0,0 +1,368 @@
1
+ import http, { type IncomingMessage, type ServerResponse } from "node:http";
2
+ import { randomBytes } from "node:crypto";
3
+
4
+ import { errorResponse, applySecurityHeaders, jsonResponse } from "./http-security.ts";
5
+ import { ReadModelNotFound, type WebUiReadModelProvider } from "./read-model-provider.ts";
6
+ import { routeReadOnlyRequest } from "./read-only-router.ts";
7
+ import { SessionAuthority } from "./session-auth.ts";
8
+ import { SseHub } from "./sse-hub.ts";
9
+ import { loadStaticBundle, type StaticAsset } from "./static-bundle.ts";
10
+ import { attachGatewayWebSocket, type GatewayProtocolHandler } from "./gateway-websocket.ts";
11
+
12
+ const MAX_BOOTSTRAP_BODY_BYTES = 4_096;
13
+ const MAX_CONTROL_BODY_BYTES = 70_000;
14
+ const MAX_ATTACHMENT_BODY_BYTES = 11_250_000;
15
+ const REQUESTS_PER_MINUTE = 120;
16
+ const BOOTSTRAPS_PER_MINUTE = 8;
17
+ const CONTROLS_PER_MINUTE = 60;
18
+ const MAX_JSON_RESPONSE_BYTES = 16 * 1024 * 1024;
19
+ const CURSOR = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,159}$/;
20
+
21
+ type ReadCapabilities = () => unknown | Promise<unknown>;
22
+ type RateState = { windowStart: number; requests: number; bootstraps: number; controls: number };
23
+
24
+ export type LoopbackServer = {
25
+ origin: string;
26
+ launchUrl: string;
27
+ issueLaunchUrl(): string;
28
+ close(): Promise<void>;
29
+ };
30
+
31
+ function requestBody(request: IncomingMessage, maximumBytes: number): Promise<Buffer> {
32
+ return new Promise((resolve, reject) => {
33
+ const chunks: Buffer[] = []; let bytes = 0, failed = false;
34
+ request.on("data", (chunk: Buffer) => {
35
+ if (failed) return;
36
+ bytes += chunk.length;
37
+ if (bytes > maximumBytes) { failed = true; reject(new Error("body-limit")); }
38
+ else chunks.push(chunk);
39
+ });
40
+ request.on("end", () => { if (!failed) resolve(Buffer.concat(chunks)); });
41
+ request.on("error", (error) => { if (!failed) reject(error); });
42
+ });
43
+ }
44
+
45
+ function serveAsset(response: ServerResponse, asset: StaticAsset): void {
46
+ response.statusCode = 200;
47
+ response.setHeader("Cache-Control", "no-store");
48
+ response.setHeader("Content-Type", asset.contentType);
49
+ response.setHeader("Content-Length", asset.body.length);
50
+ response.end(asset.body);
51
+ }
52
+
53
+ export async function startLoopbackServer(options: {
54
+ staticRoot: string;
55
+ mode?: "session" | "gateway";
56
+ readCapabilities: ReadCapabilities;
57
+ readSessionCatalog?: () => unknown | Promise<unknown>;
58
+ readSessionCreationOptions?: () => unknown | Promise<unknown>;
59
+ readSessionModel?: (sessionRef: string) => WebUiReadModelProvider | Promise<WebUiReadModelProvider>;
60
+ readSessionConnections?: (sessionRef: string) => unknown | Promise<unknown>;
61
+ executeSessionConnection?: (command: unknown) => unknown | Promise<unknown>;
62
+ readMcpAuthJob?: (jobRef: string) => unknown | Promise<unknown>;
63
+ cancelMcpAuthJob?: (jobRef: string) => unknown | Promise<unknown>;
64
+ readProviderAuthCatalog?: () => unknown | Promise<unknown>;
65
+ readProviderAuthJob?: (jobRef: string) => unknown | Promise<unknown>;
66
+ executeProviderAuth?: (command: unknown) => unknown | Promise<unknown>;
67
+ executeProjectImport?: () => unknown | Promise<unknown>;
68
+ gatewayProtocol?: GatewayProtocolHandler;
69
+ readModel?: WebUiReadModelProvider;
70
+ executeControl?: (command: unknown) => unknown | Promise<unknown>;
71
+ executeAttachment?: (command: unknown) => unknown | Promise<unknown>;
72
+ executeApproval?: (approvalRef: string, decision: unknown) => unknown | Promise<unknown>;
73
+ bootstrapTtlMs?: number;
74
+ sessionTtlMs?: number;
75
+ }): Promise<LoopbackServer> {
76
+ const assets = new Map(loadStaticBundle(options.staticRoot));
77
+ const styleNonce = randomBytes(16).toString("base64");
78
+ const indexAsset = assets.get("/");
79
+ if (indexAsset) assets.set("/", { ...indexAsset, body: Buffer.from(indexAsset.body.toString("utf8")
80
+ .replace("__PIAGENT_CSP_NONCE__", styleNonce)
81
+ .replace("__PIAGENT_WEBUI_MODE__", options.mode ?? "session")) });
82
+ const auth = new SessionAuthority({ bootstrapTtlMs: options.bootstrapTtlMs, sessionTtlMs: options.sessionTtlMs });
83
+ const sse = options.readModel ? new SseHub(options.readModel) : null;
84
+ const rates = new Map<string, RateState>();
85
+ let origin = "";
86
+ const server = http.createServer(async (request, response) => {
87
+ applySecurityHeaders(response, styleNonce);
88
+ const host = String(request.headers.host ?? "");
89
+ if (!origin || host !== origin.slice("http://".length)) return errorResponse(response, 421, "host-mismatch");
90
+ const requestOrigin = request.headers.origin;
91
+ if (requestOrigin && requestOrigin !== origin) return errorResponse(response, 403, "origin-mismatch");
92
+ const remote = request.socket.remoteAddress ?? "unknown", now = Date.now();
93
+ const rate = rates.get(remote) ?? { windowStart: now, requests: 0, bootstraps: 0, controls: 0 };
94
+ if (now - rate.windowStart >= 60_000) Object.assign(rate, { windowStart: now, requests: 0, bootstraps: 0, controls: 0 });
95
+ rate.requests += 1; rates.set(remote, rate);
96
+ if (rate.requests > REQUESTS_PER_MINUTE) return errorResponse(response, 429, "rate-limit");
97
+ let url: URL;
98
+ try { url = new URL(request.url ?? "/", origin); }
99
+ catch { return errorResponse(response, 400, "invalid-url"); }
100
+ if (url.hash || url.username || url.password || url.origin !== origin) return errorResponse(response, 400, "invalid-url");
101
+
102
+ if (request.method === "POST" && url.pathname === "/api/v1/bootstrap") {
103
+ rate.bootstraps += 1;
104
+ if (rate.bootstraps > BOOTSTRAPS_PER_MINUTE) return errorResponse(response, 429, "bootstrap-rate-limit");
105
+ if (requestOrigin !== origin) return errorResponse(response, 403, "origin-required");
106
+ if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return errorResponse(response, 415, "content-type");
107
+ let value: unknown;
108
+ try { value = JSON.parse((await requestBody(request, MAX_BOOTSTRAP_BODY_BYTES)).toString("utf8")); }
109
+ catch (error) { return errorResponse(response, (error as Error).message === "body-limit" ? 413 : 400, "invalid-bootstrap"); }
110
+ const capability = value && typeof value === "object" ? (value as Record<string, unknown>).capability : null;
111
+ const session = typeof capability === "string" ? auth.exchange(capability) : null;
112
+ if (!session) return errorResponse(response, 403, "bootstrap-rejected");
113
+ response.setHeader("Set-Cookie", auth.cookieHeader(session));
114
+ return jsonResponse(response, 200, { authenticated: true, csrfToken: session.csrf, expiresAt: new Date(session.expiresAt).toISOString() });
115
+ }
116
+
117
+ if (request.method === "GET" && url.pathname === "/api/v1/browser-session") {
118
+ const session = auth.authenticate(request);
119
+ if (!session) return errorResponse(response, 401, "authentication-required");
120
+ return jsonResponse(response, 200, { authenticated: true, csrfToken: session.csrf, expiresAt: new Date(session.expiresAt).toISOString() });
121
+ }
122
+
123
+ if (request.method === "POST" && ["/api/v1/chat/messages", "/api/v1/session-options", "/api/v1/lifecycle",
124
+ "/api/v1/control/resume-and-continue", "/api/v1/reviews", "/api/v1/source-mutations", "/api/v1/source-handoffs"].includes(url.pathname) && options.executeControl) {
125
+ rate.controls += 1;
126
+ if (rate.controls > CONTROLS_PER_MINUTE) return errorResponse(response, 429, "control-rate-limit");
127
+ if (requestOrigin !== origin) return errorResponse(response, 403, "origin-required");
128
+ if (!auth.authorizeMutation(request)) return errorResponse(response, 403, "mutation-authority-rejected");
129
+ if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return errorResponse(response, 415, "content-type");
130
+ let command: unknown;
131
+ try { command = JSON.parse((await requestBody(request, MAX_CONTROL_BODY_BYTES)).toString("utf8")); }
132
+ catch (error) { return errorResponse(response, (error as Error).message === "body-limit" ? 413 : 400, "invalid-control-command"); }
133
+ try { return jsonResponse(response, 200, await options.executeControl(command)); }
134
+ catch { return errorResponse(response, 503, "control-runtime-unavailable"); }
135
+ }
136
+
137
+ if (request.method === "POST" && url.pathname === "/api/v1/projects/import" && options.executeProjectImport) {
138
+ rate.controls += 1;
139
+ if (rate.controls > CONTROLS_PER_MINUTE) return errorResponse(response, 429, "control-rate-limit");
140
+ if (requestOrigin !== origin) return errorResponse(response, 403, "origin-required");
141
+ if (!auth.authorizeMutation(request)) return errorResponse(response, 403, "mutation-authority-rejected");
142
+ if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return errorResponse(response, 415, "content-type");
143
+ let command: unknown;
144
+ try { command = JSON.parse((await requestBody(request, MAX_BOOTSTRAP_BODY_BYTES)).toString("utf8")); }
145
+ catch (error) { return errorResponse(response, (error as Error).message === "body-limit" ? 413 : 400, "invalid-project-import"); }
146
+ if (!command || typeof command !== "object" || Array.isArray(command)
147
+ || Object.keys(command).length !== 1 || (command as Record<string, unknown>).action !== "project.import") {
148
+ return errorResponse(response, 400, "invalid-project-import");
149
+ }
150
+ try { return jsonResponse(response, 200, await options.executeProjectImport()); }
151
+ catch (error) {
152
+ const code = error instanceof Error ? error.message : "project-import-unavailable";
153
+ return errorResponse(response, code === "project-import-cancelled" ? 409 : code.includes("invalid") ? 400 : 503, code);
154
+ }
155
+ }
156
+
157
+ if (request.method === "POST" && url.pathname === "/api/v1/provider-auth" && options.executeProviderAuth) {
158
+ rate.controls += 1;
159
+ if (rate.controls > CONTROLS_PER_MINUTE) return errorResponse(response, 429, "control-rate-limit");
160
+ if (requestOrigin !== origin) return errorResponse(response, 403, "origin-required");
161
+ if (!auth.authorizeMutation(request)) return errorResponse(response, 403, "mutation-authority-rejected");
162
+ if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return errorResponse(response, 415, "content-type");
163
+ let command: unknown;
164
+ try { command = JSON.parse((await requestBody(request, MAX_CONTROL_BODY_BYTES)).toString("utf8")); }
165
+ catch (error) { return errorResponse(response, (error as Error).message === "body-limit" ? 413 : 400, "invalid-provider-auth-command"); }
166
+ try { return jsonResponse(response, 200, await options.executeProviderAuth(command)); }
167
+ catch (error) {
168
+ const code = error instanceof Error ? error.message : "provider-auth-unavailable";
169
+ return errorResponse(response, code.includes("not-found") ? 404 : code.includes("stale") || code.includes("conflict") ? 409
170
+ : code.includes("invalid") ? 400 : 503, code);
171
+ }
172
+ }
173
+
174
+ if (request.method === "POST" && url.pathname === "/api/v1/session-connections" && options.executeSessionConnection) {
175
+ rate.controls += 1;
176
+ if (rate.controls > CONTROLS_PER_MINUTE) return errorResponse(response, 429, "control-rate-limit");
177
+ if (requestOrigin !== origin) return errorResponse(response, 403, "origin-required");
178
+ if (!auth.authorizeMutation(request)) return errorResponse(response, 403, "mutation-authority-rejected");
179
+ if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return errorResponse(response, 415, "content-type");
180
+ let command: unknown;
181
+ try { command = JSON.parse((await requestBody(request, MAX_CONTROL_BODY_BYTES)).toString("utf8")); }
182
+ catch (error) { return errorResponse(response, (error as Error).message === "body-limit" ? 413 : 400, "invalid-session-connection-command"); }
183
+ try { return jsonResponse(response, 200, await options.executeSessionConnection(command)); }
184
+ catch (error) {
185
+ const code = error instanceof Error ? error.message : "session-connection-unavailable";
186
+ return errorResponse(response, code.includes("not-found") ? 404 : code.includes("invalid") ? 400 : code.includes("supported") ? 409 : 503, code);
187
+ }
188
+ }
189
+
190
+ if (request.method === "POST" && url.pathname.startsWith("/api/v1/mcp-auth/jobs/") && url.pathname.endsWith("/cancel") && options.cancelMcpAuthJob) {
191
+ rate.controls += 1;
192
+ if (rate.controls > CONTROLS_PER_MINUTE) return errorResponse(response, 429, "control-rate-limit");
193
+ if (requestOrigin !== origin || !auth.authorizeMutation(request)) return errorResponse(response, 403, "mutation-authority-rejected");
194
+ const jobRef = url.pathname.slice("/api/v1/mcp-auth/jobs/".length, -"/cancel".length);
195
+ if (!CURSOR.test(jobRef) || jobRef.includes("/") || url.search) return errorResponse(response, 400, "invalid-mcp-auth-job-ref");
196
+ try { return jsonResponse(response, 200, await options.cancelMcpAuthJob(jobRef)); }
197
+ catch (error) { return errorResponse(response, error instanceof Error && error.message.includes("not-found") ? 404 : 503, "mcp-auth-job-unavailable"); }
198
+ }
199
+
200
+ if (request.method === "POST" && url.pathname === "/api/v1/attachments" && options.executeAttachment) {
201
+ rate.controls += 1;
202
+ if (rate.controls > CONTROLS_PER_MINUTE) return errorResponse(response, 429, "control-rate-limit");
203
+ if (requestOrigin !== origin) return errorResponse(response, 403, "origin-required");
204
+ if (!auth.authorizeMutation(request)) return errorResponse(response, 403, "mutation-authority-rejected");
205
+ if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return errorResponse(response, 415, "content-type");
206
+ let command: unknown;
207
+ try { command = JSON.parse((await requestBody(request, MAX_ATTACHMENT_BODY_BYTES)).toString("utf8")); }
208
+ catch (error) { return errorResponse(response, (error as Error).message === "body-limit" ? 413 : 400, "invalid-attachment-command"); }
209
+ try { return jsonResponse(response, 200, await options.executeAttachment(command)); }
210
+ catch { return errorResponse(response, 503, "attachment-runtime-unavailable"); }
211
+ }
212
+
213
+ if (request.method === "POST" && url.pathname.startsWith("/api/v1/approvals/") && url.pathname.endsWith("/decision") && options.executeApproval) {
214
+ rate.controls += 1;
215
+ if (rate.controls > CONTROLS_PER_MINUTE) return errorResponse(response, 429, "control-rate-limit");
216
+ if (requestOrigin !== origin) return errorResponse(response, 403, "origin-required");
217
+ if (!auth.authorizeMutation(request)) return errorResponse(response, 403, "mutation-authority-rejected");
218
+ if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return errorResponse(response, 415, "content-type");
219
+ const approvalRef = url.pathname.slice("/api/v1/approvals/".length, -"/decision".length);
220
+ if (!CURSOR.test(approvalRef) || approvalRef.includes("/")) return errorResponse(response, 400, "invalid-approval-ref");
221
+ let decision: unknown;
222
+ try { decision = JSON.parse((await requestBody(request, MAX_CONTROL_BODY_BYTES)).toString("utf8")); }
223
+ catch (error) { return errorResponse(response, (error as Error).message === "body-limit" ? 413 : 400, "invalid-approval-decision"); }
224
+ try { return jsonResponse(response, 200, await options.executeApproval(approvalRef, decision)); }
225
+ catch (error) {
226
+ const code = error instanceof Error ? error.message : "approval-runtime-unavailable";
227
+ return errorResponse(response, code.includes("not-pending") || code.includes("resolved") || code.includes("expired") ? 409 : code.includes("invalid") ? 400 : 503, code);
228
+ }
229
+ }
230
+
231
+ if (request.method === "GET" && url.pathname === "/api/v1/capabilities") {
232
+ if (!auth.authenticate(request)) return errorResponse(response, 401, "authentication-required");
233
+ try { return jsonResponse(response, 200, await options.readCapabilities()); }
234
+ catch { return errorResponse(response, 503, "capabilities-unavailable"); }
235
+ }
236
+
237
+ if (request.method === "GET" && url.pathname === "/api/v1/session-catalog" && options.readSessionCatalog) {
238
+ if (!auth.authenticate(request)) return errorResponse(response, 401, "authentication-required");
239
+ if (url.search) return errorResponse(response, 400, "invalid-catalog-query");
240
+ try { return jsonResponse(response, 200, await options.readSessionCatalog()); }
241
+ catch { return errorResponse(response, 503, "session-catalog-unavailable"); }
242
+ }
243
+
244
+ if (request.method === "GET" && url.pathname === "/api/v1/session-creation-options" && options.readSessionCreationOptions) {
245
+ if (!auth.authenticate(request)) return errorResponse(response, 401, "authentication-required");
246
+ if (url.search) return errorResponse(response, 400, "invalid-session-creation-query");
247
+ try {
248
+ const value = await options.readSessionCreationOptions();
249
+ if (Buffer.byteLength(JSON.stringify(value)) > MAX_JSON_RESPONSE_BYTES) return errorResponse(response, 503, "projection-response-limit");
250
+ return jsonResponse(response, 200, value);
251
+ }
252
+ catch { return errorResponse(response, 503, "session-creation-options-unavailable"); }
253
+ }
254
+
255
+ if (request.method === "GET" && url.pathname === "/api/v1/provider-auth" && options.readProviderAuthCatalog) {
256
+ if (!auth.authenticate(request)) return errorResponse(response, 401, "authentication-required");
257
+ if (url.search) return errorResponse(response, 400, "invalid-provider-auth-query");
258
+ try { return jsonResponse(response, 200, await options.readProviderAuthCatalog()); }
259
+ catch { return errorResponse(response, 503, "provider-auth-catalog-unavailable"); }
260
+ }
261
+
262
+ if (request.method === "GET" && url.pathname.startsWith("/api/v1/provider-auth/jobs/") && options.readProviderAuthJob) {
263
+ if (!auth.authenticate(request)) return errorResponse(response, 401, "authentication-required");
264
+ if (url.search) return errorResponse(response, 400, "invalid-provider-auth-job-query");
265
+ const jobRef = url.pathname.slice("/api/v1/provider-auth/jobs/".length);
266
+ if (!CURSOR.test(jobRef) || jobRef.includes("/")) return errorResponse(response, 400, "invalid-provider-auth-job-ref");
267
+ try { return jsonResponse(response, 200, await options.readProviderAuthJob(jobRef)); }
268
+ catch (error) { return errorResponse(response, error instanceof Error && error.message.includes("not-found") ? 404 : 503, "provider-auth-job-unavailable"); }
269
+ }
270
+
271
+ if (request.method === "GET" && url.pathname.startsWith("/api/v1/mcp-auth/jobs/") && options.readMcpAuthJob) {
272
+ if (!auth.authenticate(request)) return errorResponse(response, 401, "authentication-required");
273
+ if (url.search) return errorResponse(response, 400, "invalid-mcp-auth-job-query");
274
+ const jobRef = url.pathname.slice("/api/v1/mcp-auth/jobs/".length);
275
+ if (!CURSOR.test(jobRef) || jobRef.includes("/")) return errorResponse(response, 400, "invalid-mcp-auth-job-ref");
276
+ try { return jsonResponse(response, 200, await options.readMcpAuthJob(jobRef)); }
277
+ catch (error) { return errorResponse(response, error instanceof Error && error.message.includes("not-found") ? 404 : 503, "mcp-auth-job-unavailable"); }
278
+ }
279
+
280
+ if (request.method === "GET" && url.pathname.startsWith("/api/v1/sessions/")
281
+ && (options.readSessionModel || options.readSessionConnections)) {
282
+ if (!auth.authenticate(request)) return errorResponse(response, 401, "authentication-required");
283
+ const rest = url.pathname.slice("/api/v1/sessions/".length);
284
+ const separator = rest.indexOf("/inspection/");
285
+ const sessionRef = separator >= 0 ? rest.slice(0, separator) : "";
286
+ const inspectionPath = separator >= 0 ? rest.slice(separator + "/inspection".length) : "";
287
+ if (!CURSOR.test(sessionRef) || sessionRef.includes("/") || !inspectionPath.startsWith("/")) {
288
+ return errorResponse(response, 400, "invalid-session-inspection-ref");
289
+ }
290
+ try {
291
+ if (inspectionPath === "/connections") {
292
+ if (url.search || !options.readSessionConnections) return errorResponse(response, 404, "inspection-route-not-found");
293
+ const value = await options.readSessionConnections(sessionRef);
294
+ const bytes = Buffer.byteLength(JSON.stringify(value));
295
+ if (bytes > MAX_JSON_RESPONSE_BYTES) return errorResponse(response, 503, "projection-response-limit");
296
+ return jsonResponse(response, 200, value);
297
+ }
298
+ if (!options.readSessionModel) return errorResponse(response, 404, "inspection-route-not-found");
299
+ const provider = await options.readSessionModel(sessionRef);
300
+ const scoped = new URL(url.href);
301
+ scoped.pathname = `/api/v1${inspectionPath}`;
302
+ const result = await routeReadOnlyRequest(scoped, provider);
303
+ if (!result.handled) return errorResponse(response, 404, "inspection-route-not-found");
304
+ let bytes: number;
305
+ try { bytes = Buffer.byteLength(JSON.stringify(result.value)); }
306
+ catch { return errorResponse(response, 503, "projection-unavailable"); }
307
+ if (bytes > MAX_JSON_RESPONSE_BYTES) return errorResponse(response, 503, "projection-response-limit");
308
+ return jsonResponse(response, result.status, result.value);
309
+ } catch (error) {
310
+ return errorResponse(response, error instanceof ReadModelNotFound ? 404 : 503,
311
+ error instanceof ReadModelNotFound ? "opaque-ref-not-found" : "projection-unavailable");
312
+ }
313
+ }
314
+
315
+ if (request.method === "GET" && url.pathname.startsWith("/api/v1/") && options.readModel) {
316
+ if (!auth.authenticate(request)) return errorResponse(response, 401, "authentication-required");
317
+ if (url.pathname === "/api/v1/events") {
318
+ if ([...url.searchParams.keys()].some((key) => key !== "after") || url.searchParams.getAll("after").length > 1) return errorResponse(response, 400, "invalid-event-cursor");
319
+ const hasQuery = url.searchParams.has("after"), query = url.searchParams.get("after"), header = String(request.headers["last-event-id"] ?? "") || null;
320
+ if ((hasQuery && (!query || !CURSOR.test(query))) || (header && !CURSOR.test(header)) || (query && header && query !== header)) return errorResponse(response, 400, "invalid-event-cursor");
321
+ try { await sse?.open(request, response, query ?? header); }
322
+ catch { if (!response.headersSent) errorResponse(response, 503, "event-replay-unavailable"); else response.destroy(); }
323
+ return;
324
+ }
325
+ try {
326
+ const result = await routeReadOnlyRequest(url, options.readModel);
327
+ if (result.handled) {
328
+ let bytes: number;
329
+ try { bytes = Buffer.byteLength(JSON.stringify(result.value)); } catch { return errorResponse(response, 503, "projection-unavailable"); }
330
+ if (bytes > MAX_JSON_RESPONSE_BYTES) return errorResponse(response, 503, "projection-response-limit");
331
+ return jsonResponse(response, result.status, result.value);
332
+ }
333
+ } catch (error) {
334
+ return errorResponse(response, error instanceof ReadModelNotFound ? 404 : 503,
335
+ error instanceof ReadModelNotFound ? "opaque-ref-not-found" : "projection-unavailable");
336
+ }
337
+ }
338
+
339
+ if (request.method !== "GET" && request.method !== "HEAD") return errorResponse(response, 405, "method-not-allowed");
340
+ const asset = assets.get(url.pathname);
341
+ if (!asset || url.search) return errorResponse(response, 404, "not-found");
342
+ if (request.method === "HEAD") { response.statusCode = 200; response.setHeader("Content-Type", asset.contentType); return response.end(); }
343
+ serveAsset(response, asset);
344
+ });
345
+ const gatewaySocket = options.gatewayProtocol ? attachGatewayWebSocket({
346
+ server, origin: () => origin, authority: auth, protocol: options.gatewayProtocol
347
+ }) : null;
348
+ server.requestTimeout = 5_000; server.headersTimeout = 5_000; server.keepAliveTimeout = 5_000; server.maxHeadersCount = 64;
349
+ await new Promise<void>((resolve, reject) => {
350
+ server.once("error", reject);
351
+ server.listen({ host: "127.0.0.1", port: 0, exclusive: true }, resolve);
352
+ });
353
+ const address = server.address();
354
+ if (!address || typeof address === "string" || address.address !== "127.0.0.1") { server.close(); throw new Error("loopback-bind-failed"); }
355
+ origin = `http://127.0.0.1:${address.port}`;
356
+ return {
357
+ origin,
358
+ launchUrl: `${origin}/#bootstrap=${auth.bootstrapCapability}`,
359
+ issueLaunchUrl: () => `${origin}/#bootstrap=${auth.issueBootstrapCapability()}`,
360
+ close: async () => {
361
+ auth.invalidate();
362
+ sse?.close();
363
+ await gatewaySocket?.close();
364
+ server.closeAllConnections();
365
+ await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
366
+ }
367
+ };
368
+ }
@@ -0,0 +1,37 @@
1
+ export type SourceView = "task" | "working-tree" | "staged";
2
+ export type StreamEvent = { cursor: string; value: unknown };
3
+ export type StreamReplay = {
4
+ state: "current" | "truncated" | "resync-required";
5
+ events: StreamEvent[];
6
+ nextCursor: string;
7
+ latestCursor: string;
8
+ reasonCode: string | null;
9
+ };
10
+
11
+ export type WebUiReadModelProvider = {
12
+ snapshot(): unknown | Promise<unknown>;
13
+ sourceChanges(view: SourceView): unknown | Promise<unknown>;
14
+ diff(view: SourceView, fileRef: string): unknown | Promise<unknown>;
15
+ review(view: SourceView, fileRef: string): unknown | Promise<unknown>;
16
+ sourceMutation(action: "source.stage" | "source.unstage", fileRef: string): unknown | Promise<unknown>;
17
+ sourceRevert(fileRef: string, hunkRef: string | null): unknown | Promise<unknown>;
18
+ commitSummary(): unknown | Promise<unknown>;
19
+ taskIndex(): unknown | Promise<unknown>;
20
+ taskTimeline(runRef: string): unknown | Promise<unknown>;
21
+ recoveryHistory(runRef: string): unknown | Promise<unknown>;
22
+ handoffHistory(runRef: string): unknown | Promise<unknown>;
23
+ subagentTree(runRef: string): unknown | Promise<unknown>;
24
+ releaseMonitor(): unknown | Promise<unknown>;
25
+ activity(): unknown | Promise<unknown>;
26
+ logPreview(activityRef: string): unknown | Promise<unknown>;
27
+ transcript(beforeCursor: string | null, limit: number): unknown | Promise<unknown>;
28
+ queue(): unknown | Promise<unknown>;
29
+ modelCatalog(): unknown | Promise<unknown>;
30
+ approval(approvalRef: string): unknown | Promise<unknown>;
31
+ replay(after: string | null, limit: number): StreamReplay | Promise<StreamReplay>;
32
+ subscribe(listener: (event: StreamEvent) => void): () => void;
33
+ };
34
+
35
+ export class ReadModelNotFound extends Error {
36
+ constructor() { super("read-model-ref-not-found"); }
37
+ }
@@ -0,0 +1,105 @@
1
+ import type { WebUiReadModelProvider, SourceView } from "./read-model-provider.ts";
2
+
3
+ const OPAQUE_REF = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,159}$/;
4
+
5
+ export type ReadRouteResult = { handled: false } | { handled: true; status: number; value: unknown };
6
+
7
+ export async function routeReadOnlyRequest(url: URL, provider: WebUiReadModelProvider): Promise<ReadRouteResult> {
8
+ if (url.pathname === "/api/v1/snapshot" && !url.search) return { handled: true, status: 200, value: await provider.snapshot() };
9
+ if (url.pathname === "/api/v1/activity" && !url.search) return { handled: true, status: 200, value: await provider.activity() };
10
+ if (url.pathname === "/api/v1/tasks" && !url.search) return { handled: true, status: 200, value: await provider.taskIndex() };
11
+ if (url.pathname === "/api/v1/monitoring/release" && !url.search) return { handled: true, status: 200, value: await provider.releaseMonitor() };
12
+ if (url.pathname.startsWith("/api/v1/tasks/") && url.pathname.endsWith("/timeline")) {
13
+ const ref = url.pathname.slice("/api/v1/tasks/".length, -"/timeline".length);
14
+ if (url.search || !OPAQUE_REF.test(ref) || ref.includes("/")) return { handled: true, status: 400, value: { error: { code: "invalid-task-run-ref" } } };
15
+ return { handled: true, status: 200, value: await provider.taskTimeline(ref) };
16
+ }
17
+ if (url.pathname.startsWith("/api/v1/tasks/") && url.pathname.endsWith("/recovery-history")) {
18
+ const ref = url.pathname.slice("/api/v1/tasks/".length, -"/recovery-history".length);
19
+ if (url.search || !OPAQUE_REF.test(ref) || ref.includes("/")) return { handled: true, status: 400, value: { error: { code: "invalid-task-run-ref" } } };
20
+ return { handled: true, status: 200, value: await provider.recoveryHistory(ref) };
21
+ }
22
+ if (url.pathname.startsWith("/api/v1/tasks/") && url.pathname.endsWith("/handoff-history")) {
23
+ const ref = url.pathname.slice("/api/v1/tasks/".length, -"/handoff-history".length);
24
+ if (url.search || !OPAQUE_REF.test(ref) || ref.includes("/")) return { handled: true, status: 400, value: { error: { code: "invalid-task-run-ref" } } };
25
+ return { handled: true, status: 200, value: await provider.handoffHistory(ref) };
26
+ }
27
+ if (url.pathname.startsWith("/api/v1/tasks/") && url.pathname.endsWith("/subagent-tree")) {
28
+ const ref = url.pathname.slice("/api/v1/tasks/".length, -"/subagent-tree".length);
29
+ if (url.search || !OPAQUE_REF.test(ref) || ref.includes("/")) return { handled: true, status: 400, value: { error: { code: "invalid-task-run-ref" } } };
30
+ return { handled: true, status: 200, value: await provider.subagentTree(ref) };
31
+ }
32
+ if (url.pathname === "/api/v1/chat/queue" && !url.search) return { handled: true, status: 200, value: await provider.queue() };
33
+ if (url.pathname === "/api/v1/session-options/models" && !url.search) return { handled: true, status: 200, value: await provider.modelCatalog() };
34
+ if (url.pathname.startsWith("/api/v1/approvals/")) {
35
+ const ref = url.pathname.slice("/api/v1/approvals/".length);
36
+ if (url.search || !OPAQUE_REF.test(ref) || ref.includes("/")) return { handled: true, status: 400, value: { error: { code: "invalid-approval-ref" } } };
37
+ return { handled: true, status: 200, value: await provider.approval(ref) };
38
+ }
39
+ if (url.pathname === "/api/v1/transcript") {
40
+ if ([...url.searchParams.keys()].some((key) => key !== "before" && key !== "limit")
41
+ || url.searchParams.getAll("before").length > 1 || url.searchParams.getAll("limit").length > 1) {
42
+ return { handled: true, status: 400, value: { error: { code: "invalid-transcript-page" } } };
43
+ }
44
+ const before = url.searchParams.get("before"), rawLimit = url.searchParams.get("limit"), limit = rawLimit === null ? 50 : Number(rawLimit);
45
+ if ((before !== null && !OPAQUE_REF.test(before)) || !Number.isInteger(limit) || limit < 1 || limit > 200) {
46
+ return { handled: true, status: 400, value: { error: { code: "invalid-transcript-page" } } };
47
+ }
48
+ return { handled: true, status: 200, value: await provider.transcript(before, limit) };
49
+ }
50
+ if (url.pathname === "/api/v1/source-changes") {
51
+ if ([...url.searchParams.keys()].some((key) => key !== "view") || url.searchParams.getAll("view").length !== 1) return { handled: true, status: 400, value: { error: { code: "invalid-source-view" } } };
52
+ const view = url.searchParams.get("view");
53
+ if (!(["task", "working-tree", "staged"] as Array<string | null>).includes(view)) return { handled: true, status: 400, value: { error: { code: "invalid-source-view" } } };
54
+ return { handled: true, status: 200, value: await provider.sourceChanges(view as SourceView) };
55
+ }
56
+ if (url.pathname.startsWith("/api/v1/diffs/")) {
57
+ const ref = url.pathname.slice("/api/v1/diffs/".length);
58
+ const view = url.searchParams.get("view");
59
+ if (!OPAQUE_REF.test(ref) || url.searchParams.getAll("view").length !== 1
60
+ || [...url.searchParams.keys()].some((key) => key !== "view")
61
+ || !(view && (["task", "working-tree", "staged"] as string[]).includes(view))) {
62
+ return { handled: true, status: 400, value: { error: { code: "invalid-diff-authority" } } };
63
+ }
64
+ return { handled: true, status: 200, value: await provider.diff(view as SourceView, ref) };
65
+ }
66
+ if (url.pathname.startsWith("/api/v1/reviews/")) {
67
+ const ref = url.pathname.slice("/api/v1/reviews/".length);
68
+ const view = url.searchParams.get("view");
69
+ if (!OPAQUE_REF.test(ref) || url.searchParams.getAll("view").length !== 1
70
+ || [...url.searchParams.keys()].some((key) => key !== "view")
71
+ || !(view && (["task", "working-tree", "staged"] as string[]).includes(view))) {
72
+ return { handled: true, status: 400, value: { error: { code: "invalid-review-authority" } } };
73
+ }
74
+ return { handled: true, status: 200, value: await provider.review(view as SourceView, ref) };
75
+ }
76
+ if (url.pathname.startsWith("/api/v1/source-mutations/")) {
77
+ const ref = url.pathname.slice("/api/v1/source-mutations/".length), action = url.searchParams.get("action");
78
+ if (!OPAQUE_REF.test(ref) || url.searchParams.getAll("action").length !== 1
79
+ || [...url.searchParams.keys()].some((key) => key !== "action") || !["source.stage", "source.unstage"].includes(String(action))) {
80
+ return { handled: true, status: 400, value: { error: { code: "invalid-source-mutation-authority" } } };
81
+ }
82
+ return { handled: true, status: 200, value: await provider.sourceMutation(action as "source.stage" | "source.unstage", ref) };
83
+ }
84
+ if (url.pathname.startsWith("/api/v1/source-reverts/")) {
85
+ const ref = url.pathname.slice("/api/v1/source-reverts/".length), hunkRef = url.searchParams.get("hunkRef");
86
+ if (!OPAQUE_REF.test(ref) || url.searchParams.getAll("hunkRef").length > 1
87
+ || [...url.searchParams.keys()].some((key) => key !== "hunkRef") || hunkRef !== null && !OPAQUE_REF.test(hunkRef)) {
88
+ return { handled: true, status: 400, value: { error: { code: "invalid-source-revert-authority" } } };
89
+ }
90
+ return { handled: true, status: 200, value: await provider.sourceRevert(ref, hunkRef) };
91
+ }
92
+ if (url.pathname === "/api/v1/commit-summary") {
93
+ if (url.search) return { handled: true, status: 400, value: { error: { code: "invalid-commit-summary-request" } } };
94
+ return { handled: true, status: 200, value: await provider.commitSummary() };
95
+ }
96
+ for (const [prefix, read] of [
97
+ ["/api/v1/log-previews/", (ref: string) => provider.logPreview(ref)]
98
+ ] as const) {
99
+ if (!url.pathname.startsWith(prefix)) continue;
100
+ const ref = url.pathname.slice(prefix.length);
101
+ if (url.search || !OPAQUE_REF.test(ref)) return { handled: true, status: 400, value: { error: { code: "invalid-opaque-ref" } } };
102
+ return { handled: true, status: 200, value: await read(ref) };
103
+ }
104
+ return { handled: false };
105
+ }