@piagent/platform 1.3.1 → 1.4.0

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 (301) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +3 -3
  3. package/architecture/layers.json +96 -1
  4. package/docs/README.md +1 -0
  5. package/docs/architecture.md +1 -0
  6. package/docs/authority-manifest.md +1 -0
  7. package/docs/auto-delegation-policy.md +1 -0
  8. package/docs/capability-packs.md +1 -0
  9. package/docs/command-reference-vietnamese.md +5 -4
  10. package/docs/context-engine.md +1 -0
  11. package/docs/context-window-policy.md +1 -0
  12. package/docs/distribution-standard.md +8 -7
  13. package/docs/herdr-workflow.md +1 -0
  14. package/docs/mcp-and-tools.md +14 -9
  15. package/docs/memory-policy.md +1 -0
  16. package/docs/model-options.md +1 -0
  17. package/docs/oauth-providers.md +1 -0
  18. package/docs/operator-manual-vietnamese.md +15 -12
  19. package/docs/package-architecture-notes.md +1 -0
  20. package/docs/project-adapters.md +1 -0
  21. package/docs/project-onboarding.md +1 -0
  22. package/docs/publishing-for-teams.md +1 -0
  23. package/docs/quality-benchmark.md +1 -0
  24. package/docs/quickstart-vietnamese.md +3 -2
  25. package/docs/release-install-policy.md +7 -6
  26. package/docs/runtime-harness-standard.md +1 -0
  27. package/docs/runtime-policy-design.md +1 -0
  28. package/docs/runtime-quality-baseline.md +1 -0
  29. package/docs/security-redaction-benchmark.md +1 -0
  30. package/docs/security-threat-model.md +2 -1
  31. package/docs/subagent-orchestration-capabilities.md +1 -0
  32. package/docs/subagents-and-multiagent.md +1 -0
  33. package/docs/task-implementation-contract.md +1 -0
  34. package/docs/task-lifecycle-vietnamese.md +1 -0
  35. package/docs/team-onboarding.md +3 -2
  36. package/docs/third-party-neutrality.md +26 -0
  37. package/docs/usage-observability.md +1 -0
  38. package/docs/vercel-docs-site.md +1 -0
  39. package/docs/workflow-recipes.md +1 -0
  40. package/docs-site/architecture.html +1 -1
  41. package/docs-site/benchmark.html +1 -1
  42. package/docs-site/commands.html +1 -1
  43. package/docs-site/context-engine.html +1 -1
  44. package/docs-site/en/architecture.html +1 -1
  45. package/docs-site/en/benchmark.html +1 -1
  46. package/docs-site/en/commands.html +1 -1
  47. package/docs-site/en/context-engine.html +1 -1
  48. package/docs-site/en/index.html +1 -1
  49. package/docs-site/en/mcp-approval.html +1 -1
  50. package/docs-site/en/mcp-auth.html +1 -1
  51. package/docs-site/en/mcp-servers.html +1 -1
  52. package/docs-site/en/mcp.html +1 -1
  53. package/docs-site/en/permissions.html +1 -1
  54. package/docs-site/en/profiles.html +1 -1
  55. package/docs-site/en/quickstart.html +1 -1
  56. package/docs-site/en/runtime.html +1 -1
  57. package/docs-site/en/scope.html +1 -1
  58. package/docs-site/en/security.html +1 -1
  59. package/docs-site/en/team.html +1 -1
  60. package/docs-site/en/whats-new.html +1 -1
  61. package/docs-site/en/workflows.html +1 -1
  62. package/docs-site/index.html +1 -1
  63. package/docs-site/mcp-approval.html +1 -1
  64. package/docs-site/mcp-auth.html +1 -1
  65. package/docs-site/mcp-servers.html +1 -1
  66. package/docs-site/mcp.html +1 -1
  67. package/docs-site/permissions.html +1 -1
  68. package/docs-site/profiles.html +1 -1
  69. package/docs-site/quickstart.html +1 -1
  70. package/docs-site/runtime.html +1 -1
  71. package/docs-site/scope.html +1 -1
  72. package/docs-site/security.html +1 -1
  73. package/docs-site/team.html +1 -1
  74. package/docs-site/whats-new.html +1 -1
  75. package/docs-site/workflows.html +1 -1
  76. package/evals/fixtures/mutation-provenance-record.invalid.json +17 -0
  77. package/evals/fixtures/mutation-provenance-record.valid.json +27 -0
  78. package/evals/fixtures/piagent-webui/approval-v1.invalid.json +54 -0
  79. package/evals/fixtures/piagent-webui/approval-v1.valid.json +58 -0
  80. package/evals/fixtures/piagent-webui/attachment-v1.invalid.json +25 -0
  81. package/evals/fixtures/piagent-webui/attachment-v1.valid.json +25 -0
  82. package/evals/fixtures/piagent-webui/capabilities-v1.invalid.json +51 -0
  83. package/evals/fixtures/piagent-webui/capabilities-v1.valid.json +122 -0
  84. package/evals/fixtures/piagent-webui/commit-summary-v1.invalid.json +10 -0
  85. package/evals/fixtures/piagent-webui/commit-summary-v1.valid.json +25 -0
  86. package/evals/fixtures/piagent-webui/control-command-v1.invalid.json +37 -0
  87. package/evals/fixtures/piagent-webui/control-command-v1.valid.json +35 -0
  88. package/evals/fixtures/piagent-webui/diff-v1.invalid.json +13 -0
  89. package/evals/fixtures/piagent-webui/diff-v1.valid.json +141 -0
  90. package/evals/fixtures/piagent-webui/gateway-capabilities-v1.invalid.json +35 -0
  91. package/evals/fixtures/piagent-webui/gateway-capabilities-v1.valid.json +35 -0
  92. package/evals/fixtures/piagent-webui/gateway-protocol-v1.invalid.json +14 -0
  93. package/evals/fixtures/piagent-webui/gateway-protocol-v1.valid.json +14 -0
  94. package/evals/fixtures/piagent-webui/handoff-history-v1.invalid.json +16 -0
  95. package/evals/fixtures/piagent-webui/handoff-history-v1.valid.json +16 -0
  96. package/evals/fixtures/piagent-webui/model-catalog-v1.invalid.json +31 -0
  97. package/evals/fixtures/piagent-webui/model-catalog-v1.valid.json +43 -0
  98. package/evals/fixtures/piagent-webui/queue-v1.invalid.json +29 -0
  99. package/evals/fixtures/piagent-webui/queue-v1.valid.json +44 -0
  100. package/evals/fixtures/piagent-webui/recovery-history-v1.invalid.json +17 -0
  101. package/evals/fixtures/piagent-webui/recovery-history-v1.valid.json +20 -0
  102. package/evals/fixtures/piagent-webui/release-monitor-v1.invalid.json +12 -0
  103. package/evals/fixtures/piagent-webui/release-monitor-v1.valid.json +29 -0
  104. package/evals/fixtures/piagent-webui/review-state-v1.invalid.json +25 -0
  105. package/evals/fixtures/piagent-webui/review-state-v1.valid.json +38 -0
  106. package/evals/fixtures/piagent-webui/runtime-event-v2.invalid.json +49 -0
  107. package/evals/fixtures/piagent-webui/runtime-event-v2.valid.json +49 -0
  108. package/evals/fixtures/piagent-webui/session-catalog-v1.invalid.json +17 -0
  109. package/evals/fixtures/piagent-webui/session-catalog-v1.valid.json +58 -0
  110. package/evals/fixtures/piagent-webui/session-command-v1.invalid.json +19 -0
  111. package/evals/fixtures/piagent-webui/session-command-v1.valid.json +19 -0
  112. package/evals/fixtures/piagent-webui/snapshot-v1.invalid.json +5 -0
  113. package/evals/fixtures/piagent-webui/snapshot-v1.valid.json +191 -0
  114. package/evals/fixtures/piagent-webui/source-change-v1.invalid.json +57 -0
  115. package/evals/fixtures/piagent-webui/source-change-v1.valid.json +98 -0
  116. package/evals/fixtures/piagent-webui/source-mutation-v1.invalid.json +19 -0
  117. package/evals/fixtures/piagent-webui/source-mutation-v1.valid.json +38 -0
  118. package/evals/fixtures/piagent-webui/source-revert-v1.invalid.json +20 -0
  119. package/evals/fixtures/piagent-webui/source-revert-v1.valid.json +62 -0
  120. package/evals/fixtures/piagent-webui/subagent-tree-v1.invalid.json +26 -0
  121. package/evals/fixtures/piagent-webui/subagent-tree-v1.valid.json +39 -0
  122. package/evals/fixtures/piagent-webui/task-index-v1.invalid.json +15 -0
  123. package/evals/fixtures/piagent-webui/task-index-v1.valid.json +15 -0
  124. package/evals/fixtures/piagent-webui/task-timeline-v1.invalid.json +14 -0
  125. package/evals/fixtures/piagent-webui/task-timeline-v1.valid.json +16 -0
  126. package/evals/fixtures/piagent-webui/transcript-v1.invalid.json +35 -0
  127. package/evals/fixtures/piagent-webui/transcript-v1.valid.json +55 -0
  128. package/evals/fixtures/task-baseline-manifest.invalid.json +16 -0
  129. package/evals/fixtures/task-baseline-manifest.valid.json +41 -0
  130. package/evals/fixtures/verifier-file-snapshot.invalid.json +21 -0
  131. package/evals/fixtures/verifier-file-snapshot.valid.json +21 -0
  132. package/package.json +30 -3
  133. package/packages/piagent-core/benchmark/benchmark-candidate.d.ts +1 -0
  134. package/packages/piagent-core/benchmark/benchmark-forensics.d.ts +1 -0
  135. package/packages/piagent-core/benchmark/benchmark-ledger.d.ts +4 -0
  136. package/packages/piagent-core/benchmark/benchmark-record-validation.d.ts +1 -0
  137. package/packages/piagent-core/benchmark/benchmark-runtime.d.ts +1 -0
  138. package/packages/piagent-core/capabilities/capability-core.js +2 -2
  139. package/packages/piagent-core/capabilities/runtime-integrity.js +143 -0
  140. package/packages/piagent-core/extensions/piagent-guard.ts +62 -37
  141. package/packages/piagent-core/extensions/redaction-core.d.ts +2 -0
  142. package/packages/piagent-core/extensions/task-journal.js +30 -1
  143. package/packages/piagent-core/extensions/task-state.js +33 -36
  144. package/packages/piagent-core/mcp/mcp-server-catalog.js +5 -5
  145. package/packages/piagent-core/package.json +1 -1
  146. package/packages/piagent-core/runtime/hooks/session-start-hook.ts +2 -2
  147. package/packages/piagent-core/runtime/hooks/tool-call-hook.ts +5 -2
  148. package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +20 -19
  149. package/packages/piagent-core/runtime/hooks/tool-result-metadata.ts +13 -0
  150. package/packages/piagent-core/runtime/input/chat-images.ts +2 -2
  151. package/packages/piagent-core/runtime/inspection/activity-event-adapter.ts +94 -0
  152. package/packages/piagent-core/runtime/inspection/approval-broker.ts +318 -0
  153. package/packages/piagent-core/runtime/inspection/commit-summary-projection.ts +59 -0
  154. package/packages/piagent-core/runtime/inspection/context-telemetry-inspection.ts +37 -0
  155. package/packages/piagent-core/runtime/inspection/criteria-links.ts +295 -0
  156. package/packages/piagent-core/runtime/inspection/diff-projection.ts +499 -0
  157. package/packages/piagent-core/runtime/inspection/git-filter-safety.ts +38 -0
  158. package/packages/piagent-core/runtime/inspection/git-numstat-adapter.ts +71 -0
  159. package/packages/piagent-core/runtime/inspection/git-status-adapter.ts +493 -0
  160. package/packages/piagent-core/runtime/inspection/git-tree-adapter.ts +108 -0
  161. package/packages/piagent-core/runtime/inspection/mcp-control.ts +10 -0
  162. package/packages/piagent-core/runtime/inspection/mutation-provenance-contract.ts +131 -0
  163. package/packages/piagent-core/runtime/inspection/mutation-provenance-recorder.ts +48 -0
  164. package/packages/piagent-core/runtime/inspection/mutation-provenance-store.ts +161 -0
  165. package/packages/piagent-core/runtime/inspection/project-runtime-inspection.ts +36 -0
  166. package/packages/piagent-core/runtime/inspection/review-state-contract.ts +108 -0
  167. package/packages/piagent-core/runtime/inspection/review-state-projection.ts +93 -0
  168. package/packages/piagent-core/runtime/inspection/review-state-store.ts +112 -0
  169. package/packages/piagent-core/runtime/inspection/runtime-event-store.ts +262 -0
  170. package/packages/piagent-core/runtime/inspection/same-process-bridge-proof.ts +103 -0
  171. package/packages/piagent-core/runtime/inspection/source-change-projection.ts +471 -0
  172. package/packages/piagent-core/runtime/inspection/source-evidence-contract.ts +139 -0
  173. package/packages/piagent-core/runtime/inspection/source-evidence-store.ts +315 -0
  174. package/packages/piagent-core/runtime/inspection/source-handoff-store.ts +70 -0
  175. package/packages/piagent-core/runtime/inspection/source-inspection-plan.ts +34 -0
  176. package/packages/piagent-core/runtime/inspection/source-mutation-projection.ts +164 -0
  177. package/packages/piagent-core/runtime/inspection/source-mutation-store.ts +168 -0
  178. package/packages/piagent-core/runtime/inspection/source-open-target.ts +38 -0
  179. package/packages/piagent-core/runtime/inspection/source-revert-projection.ts +147 -0
  180. package/packages/piagent-core/runtime/inspection/task-baseline-start-capture.ts +22 -0
  181. package/packages/piagent-core/runtime/inspection/task-compaction-history.ts +160 -0
  182. package/packages/piagent-core/runtime/inspection/task-control-journal.ts +131 -0
  183. package/packages/piagent-core/runtime/inspection/task-handoff-history.ts +95 -0
  184. package/packages/piagent-core/runtime/inspection/task-provenance-projection.ts +60 -0
  185. package/packages/piagent-core/runtime/inspection/task-recovery-timeline.ts +88 -0
  186. package/packages/piagent-core/runtime/inspection/task-run-index.ts +87 -0
  187. package/packages/piagent-core/runtime/inspection/task-source-projection.ts +401 -0
  188. package/packages/piagent-core/runtime/inspection/task-subagent-tree.ts +101 -0
  189. package/packages/piagent-core/runtime/inspection/text-diff.ts +60 -0
  190. package/packages/piagent-core/runtime/inspection/verifier-snapshot-contract.ts +114 -0
  191. package/packages/piagent-core/runtime/inspection/verifier-snapshot-store.ts +189 -0
  192. package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +320 -0
  193. package/packages/piagent-core/runtime/inspection/workspace-file-reader.ts +132 -0
  194. package/packages/piagent-core/runtime/inspection/workspace-source-projection.ts +96 -0
  195. package/packages/piagent-core/runtime/inspection/zero-turn-conformance.ts +175 -0
  196. package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +68 -4
  197. package/packages/piagent-core/runtime/policy/source-index-transaction.ts +173 -0
  198. package/packages/piagent-core/runtime/policy/source-mutation-guard-binding.ts +42 -0
  199. package/packages/piagent-core/runtime/policy/source-mutation-guard.ts +184 -0
  200. package/packages/piagent-core/runtime/policy/source-worktree-transaction.ts +105 -0
  201. package/packages/piagent-core/runtime/product/activity-inspector-footer.ts +5 -3
  202. package/packages/piagent-core/runtime/product/activity-inspector.ts +85 -343
  203. package/packages/piagent-core/runtime/recovery/resume-state.ts +38 -1
  204. package/packages/piagent-core/runtime/registration/activity-inspector-command.ts +68 -14
  205. package/packages/piagent-core/runtime/registration/task-start-tool.ts +13 -14
  206. package/packages/piagent-core/runtime/session/model-authorship-state.ts +31 -3
  207. package/packages/piagent-core/runtime/session/runtime-state.ts +11 -1
  208. package/packages/piagent-webui/dist/client/assets/index-DjMntaK5.js +172 -0
  209. package/packages/piagent-webui/dist/client/assets/index-DwKCWbwd.css +1 -0
  210. package/packages/piagent-webui/dist/client/index.html +16 -0
  211. package/packages/piagent-webui/extension/attachment-store.ts +323 -0
  212. package/packages/piagent-webui/extension/held-message-queue.ts +311 -0
  213. package/packages/piagent-webui/extension/lifecycle-controller.ts +408 -0
  214. package/packages/piagent-webui/extension/lifecycle-event-adapter.ts +23 -0
  215. package/packages/piagent-webui/extension/piagent-webui.ts +470 -0
  216. package/packages/piagent-webui/extension/review-controller.ts +160 -0
  217. package/packages/piagent-webui/extension/same-session-bridge.ts +500 -0
  218. package/packages/piagent-webui/extension/session-options-controller.ts +281 -0
  219. package/packages/piagent-webui/extension/session-stream-adapter.ts +249 -0
  220. package/packages/piagent-webui/extension/source-mutation-controller.ts +215 -0
  221. package/packages/piagent-webui/extension/source-open-controller.ts +136 -0
  222. package/packages/piagent-webui/extension/source-revert-controller.ts +189 -0
  223. package/packages/piagent-webui/extension/terminal-session-adapter.ts +73 -0
  224. package/packages/piagent-webui/extension/vscode-handoff.ts +51 -0
  225. package/packages/piagent-webui/gateway/control-socket.ts +92 -0
  226. package/packages/piagent-webui/gateway/gateway-events.ts +60 -0
  227. package/packages/piagent-webui/gateway/gateway-protocol-service.ts +64 -0
  228. package/packages/piagent-webui/gateway/gateway-service.ts +215 -0
  229. package/packages/piagent-webui/gateway/gateway-session-stream.ts +110 -0
  230. package/packages/piagent-webui/gateway/mcp-auth-broker.ts +158 -0
  231. package/packages/piagent-webui/gateway/native-project-picker.ts +29 -0
  232. package/packages/piagent-webui/gateway/pi-host.ts +41 -0
  233. package/packages/piagent-webui/gateway/profile-state.ts +1 -0
  234. package/packages/piagent-webui/gateway/project-registry.ts +108 -0
  235. package/packages/piagent-webui/gateway/provider-auth-broker.ts +172 -0
  236. package/packages/piagent-webui/gateway/session-catalog.ts +128 -0
  237. package/packages/piagent-webui/gateway/session-command-controller.ts +237 -0
  238. package/packages/piagent-webui/gateway/session-command-store.ts +224 -0
  239. package/packages/piagent-webui/gateway/session-inspection-registry.ts +204 -0
  240. package/packages/piagent-webui/gateway/session-lease-store.ts +1 -0
  241. package/packages/piagent-webui/gateway/session-metadata-store.ts +152 -0
  242. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +485 -0
  243. package/packages/piagent-webui/ownership/gateway-runtime-context.ts +8 -0
  244. package/packages/piagent-webui/ownership/profile-state.ts +117 -0
  245. package/packages/piagent-webui/ownership/session-lease-store.ts +329 -0
  246. package/packages/piagent-webui/ownership/session-refs.ts +8 -0
  247. package/packages/piagent-webui/package.json +33 -0
  248. package/packages/piagent-webui/server/README.md +4 -0
  249. package/packages/piagent-webui/server/benchmark-release-monitor.ts +230 -0
  250. package/packages/piagent-webui/server/core-inspection-provider.ts +403 -0
  251. package/packages/piagent-webui/server/gateway-websocket.ts +143 -0
  252. package/packages/piagent-webui/server/http-security.ts +27 -0
  253. package/packages/piagent-webui/server/ipc-read-model-client.ts +89 -0
  254. package/packages/piagent-webui/server/loopback-server.ts +368 -0
  255. package/packages/piagent-webui/server/read-model-provider.ts +37 -0
  256. package/packages/piagent-webui/server/read-only-router.ts +105 -0
  257. package/packages/piagent-webui/server/session-auth.ts +88 -0
  258. package/packages/piagent-webui/server/sidecar-main.ts +86 -0
  259. package/packages/piagent-webui/server/sse-hub.ts +76 -0
  260. package/packages/piagent-webui/server/static-bundle.ts +40 -0
  261. package/packages/piagent-webui/server/transcript-projection.ts +110 -0
  262. package/schemas/mutation-provenance-record.schema.json +100 -0
  263. package/schemas/piagent-webui/approval-v1.schema.json +654 -0
  264. package/schemas/piagent-webui/attachment-v1.schema.json +267 -0
  265. package/schemas/piagent-webui/capabilities-v1.schema.json +729 -0
  266. package/schemas/piagent-webui/catalog-v1.json +240 -0
  267. package/schemas/piagent-webui/commit-summary-v1.schema.json +63 -0
  268. package/schemas/piagent-webui/common-v1.schema.json +479 -0
  269. package/schemas/piagent-webui/control-command-v1.schema.json +1780 -0
  270. package/schemas/piagent-webui/diff-v1.schema.json +826 -0
  271. package/schemas/piagent-webui/gateway-capabilities-v1.schema.json +136 -0
  272. package/schemas/piagent-webui/gateway-protocol-v1.schema.json +456 -0
  273. package/schemas/piagent-webui/handoff-history-v1.schema.json +92 -0
  274. package/schemas/piagent-webui/model-catalog-v1.schema.json +95 -0
  275. package/schemas/piagent-webui/queue-v1.schema.json +99 -0
  276. package/schemas/piagent-webui/recovery-history-v1.schema.json +144 -0
  277. package/schemas/piagent-webui/release-monitor-v1.schema.json +173 -0
  278. package/schemas/piagent-webui/review-state-v1.schema.json +206 -0
  279. package/schemas/piagent-webui/runtime-event-v2.schema.json +5206 -0
  280. package/schemas/piagent-webui/session-catalog-v1.schema.json +222 -0
  281. package/schemas/piagent-webui/session-command-v1.schema.json +390 -0
  282. package/schemas/piagent-webui/snapshot-v1.schema.json +2622 -0
  283. package/schemas/piagent-webui/source-change-v1.schema.json +763 -0
  284. package/schemas/piagent-webui/source-mutation-v1.schema.json +96 -0
  285. package/schemas/piagent-webui/source-revert-v1.schema.json +130 -0
  286. package/schemas/piagent-webui/subagent-tree-v1.schema.json +88 -0
  287. package/schemas/piagent-webui/task-index-v1.schema.json +74 -0
  288. package/schemas/piagent-webui/task-timeline-v1.schema.json +49 -0
  289. package/schemas/piagent-webui/transcript-v1.schema.json +243 -0
  290. package/schemas/task-baseline-manifest.schema.json +122 -0
  291. package/schemas/verifier-file-snapshot.schema.json +81 -0
  292. package/scripts/check-architecture.mjs +1 -0
  293. package/scripts/check-doc-languages.mjs +19 -2
  294. package/scripts/check-third-party-neutrality.mjs +43 -0
  295. package/scripts/install-global.sh +1 -1
  296. package/scripts/piagent-cli.mjs +17 -3
  297. package/scripts/piagent-dashboard.mjs +208 -0
  298. package/scripts/piagent-webui-launcher.mjs +121 -0
  299. package/scripts/typescript-loader.mjs +26 -1
  300. package/scripts/verify-local.sh +253 -0
  301. package/scripts/verify-release-identity.mjs +4 -1
@@ -0,0 +1,403 @@
1
+ import { randomBytes } from "node:crypto";
2
+
3
+ import { redactSensitiveText } from "../../piagent-core/extensions/redaction-core.js";
4
+ import { ReadModelNotFound, type SourceView, type StreamEvent, type WebUiReadModelProvider } from "./read-model-provider.ts";
5
+ import { projectTranscript } from "./transcript-projection.ts";
6
+
7
+ const CORE_ROOT = "../../piagent-core";
8
+
9
+ type RuntimeEvent = { eventCursor: string; [key: string]: unknown };
10
+ type EventReplay = {
11
+ state: "current" | "truncated" | "resync-required";
12
+ events: RuntimeEvent[];
13
+ nextCursor: string;
14
+ latestCursor: string;
15
+ reasonCode: string | null;
16
+ };
17
+ type EventStore = {
18
+ retention(): unknown;
19
+ currentCursor(): string | null;
20
+ resyncRequired(): boolean;
21
+ replay(after: string | null, limit: number): EventReplay;
22
+ };
23
+
24
+ async function loadCoreInspection() {
25
+ const [{ collectFileDiff }, { buildWebUiInspectionProjection }, { matchesProtectedPath }, reviewProjection, reviewStore, mutationProjection, revertProjection, openProjection, commitSummaryProjection, taskIndexProjection, taskTimelineProjection, recoveryHistoryProjection, handoffHistoryProjection, subagentTreeProjection, releaseMonitorProjection] = await Promise.all([
26
+ import(`${CORE_ROOT}/runtime/inspection/diff-projection.ts`),
27
+ import(`${CORE_ROOT}/runtime/inspection/webui-snapshot.ts`),
28
+ import(`${CORE_ROOT}/extensions/policy-core.js`),
29
+ import(`${CORE_ROOT}/runtime/inspection/review-state-projection.ts`),
30
+ import(`${CORE_ROOT}/runtime/inspection/review-state-store.ts`),
31
+ import(`${CORE_ROOT}/runtime/inspection/source-mutation-projection.ts`),
32
+ import(`${CORE_ROOT}/runtime/inspection/source-revert-projection.ts`),
33
+ import(`${CORE_ROOT}/runtime/inspection/source-open-target.ts`),
34
+ import(`${CORE_ROOT}/runtime/inspection/commit-summary-projection.ts`),
35
+ import(`${CORE_ROOT}/runtime/inspection/task-run-index.ts`),
36
+ import(`${CORE_ROOT}/runtime/inspection/task-recovery-timeline.ts`),
37
+ import(`${CORE_ROOT}/runtime/inspection/task-compaction-history.ts`),
38
+ import(`${CORE_ROOT}/runtime/inspection/task-handoff-history.ts`),
39
+ import(`${CORE_ROOT}/runtime/inspection/task-subagent-tree.ts`),
40
+ import("./benchmark-release-monitor.ts")
41
+ ]);
42
+ return { collectFileDiff, buildWebUiInspectionProjection, matchesProtectedPath, ...reviewProjection, ...reviewStore, ...mutationProjection, ...revertProjection, ...openProjection, ...commitSummaryProjection, ...taskIndexProjection, ...taskTimelineProjection, ...recoveryHistoryProjection, ...handoffHistoryProjection, ...subagentTreeProjection, ...releaseMonitorProjection };
43
+ }
44
+
45
+ export type CoreInspectionInput = {
46
+ cwd: string;
47
+ sessionId: string;
48
+ runtimeInstanceId: string;
49
+ eventStore: EventStore;
50
+ task?: () => unknown;
51
+ activityEvents?: () => unknown[];
52
+ currentActivity?: () => unknown[];
53
+ sessionEntries?: () => unknown[];
54
+ protectedPaths?: () => string[];
55
+ contextUsage?: () => { tokens: number | null; contextWindow: number; percent: number | null } | undefined;
56
+ model?: () => { provider?: unknown; id?: unknown; name?: unknown; reasoning?: unknown; input?: unknown; thinkingLevelMap?: unknown; contextWindow?: unknown; maxTokens?: unknown } | undefined;
57
+ thinkingLevel?: () => unknown;
58
+ queueProjection?: () => unknown;
59
+ modelCatalog?: () => unknown;
60
+ attachmentCapability?: () => { kinds: Array<"file" | "image">; mimeTypes: string[] };
61
+ approvalProjection?: () => { revision: string | null; summary: Record<string, unknown> };
62
+ approvalDetail?: (approvalRef: string) => unknown | null;
63
+ sourceMutationGuardAvailable?: () => boolean;
64
+ sourceOpenAvailable?: () => boolean;
65
+ lifecycleControl?: () => { state: "active" | "pause-requested" | "paused" | "terminal" | "unknown"; controlRevision: string | null;
66
+ dispatchBlocked: boolean; currentPhase: string; pauseEpoch: number; actions: { stop: boolean; pause: boolean; resume: boolean } };
67
+ chatControl?: () => { state: "unbound" | "ready" | "replacement-pending" | "shutdown"; liveness: "idle" | "running" | "unknown";
68
+ taskState: "none" | "active" | "pause-requested" | "paused" | "terminal" | "unknown"; identity: Record<string, any> | null; revisions: Record<string, any> | null;
69
+ heldCount: number; queueRevision: string | null };
70
+ };
71
+
72
+ export class CoreInspectionProvider implements WebUiReadModelProvider {
73
+ readonly #input: CoreInspectionInput;
74
+ readonly #listeners = new Set<(event: StreamEvent) => void>();
75
+ #cachedProjection: { at: number; value: any } | null = null;
76
+ #invalidationEpoch = 0;
77
+ readonly #diffCache = new Map<string, { at: number; value: unknown }>();
78
+ readonly #sourceRevertConfirmationKey = randomBytes(32);
79
+
80
+ constructor(input: CoreInspectionInput) { this.#input = input; }
81
+
82
+ invalidate(): void {
83
+ this.#invalidationEpoch += 1;
84
+ this.#cachedProjection = null;
85
+ this.#diffCache.clear();
86
+ }
87
+
88
+ async #projection() {
89
+ if (this.#cachedProjection && Date.now() - this.#cachedProjection.at <= 200) return this.#cachedProjection.value;
90
+ const projectionEpoch = this.#invalidationEpoch;
91
+ const { buildWebUiInspectionProjection } = await loadCoreInspection();
92
+ const retention = this.#input.eventStore.retention();
93
+ const value = await buildWebUiInspectionProjection({
94
+ cwd: this.#input.cwd, sessionId: this.#input.sessionId, runtimeInstanceId: this.#input.runtimeInstanceId,
95
+ task: this.#input.task?.(), events: this.#input.activityEvents?.() ?? [], current: this.#input.currentActivity?.() ?? [],
96
+ sessionEntries: this.#input.sessionEntries?.() ?? [], protectedPaths: this.#input.protectedPaths?.() ?? [],
97
+ contextUsage: this.#input.contextUsage?.(), model: this.#input.model?.(), thinkingLevel: this.#input.thinkingLevel?.(),
98
+ eventCursor: this.#input.eventStore.currentCursor(), resyncRequired: this.#input.eventStore.resyncRequired(), eventReplay: retention
99
+ });
100
+ const control = this.#input.chatControl?.();
101
+ if (control?.state === "ready" && control.identity && control.revisions && !this.#input.eventStore.resyncRequired()) {
102
+ const available = { available: true, reasonCode: null }, disabled = (reasonCode: string) => ({ available: false, reasonCode });
103
+ const lifecycle = this.#input.lifecycleControl?.(), terminal = control.taskState === "terminal";
104
+ const dispatchBlocked = terminal || lifecycle?.dispatchBlocked === true;
105
+ value.snapshot.identity = structuredClone(control.identity);
106
+ value.snapshot.capabilities.identity = structuredClone(control.identity);
107
+ for (const name of ["runtimeRevision", "taskRevision", "controlRevision", "sessionOptionRevision", "queueRevision"])
108
+ value.snapshot.revision[name] = control.revisions[name];
109
+ value.snapshot.capabilities.mode = "control-enabled";
110
+ value.snapshot.capabilities.capabilities["control.chat"] = {
111
+ status: "available", version: 1, reason: null, queuePersistence: "runtime-lifetime",
112
+ actions: { send: dispatchBlocked ? disabled(terminal ? "task-terminal" : "task-control-gate-closed") : available, hold: available, editHeld: available,
113
+ deleteHeld: available, dispatchHeld: dispatchBlocked ? disabled(terminal ? "task-terminal" : "task-control-gate-closed") : available,
114
+ interruptAndSend: dispatchBlocked ? disabled(terminal ? "task-terminal" : "task-control-gate-closed") : control.liveness === "running" ? available : disabled("agent-not-running") }
115
+ };
116
+ if (!terminal && this.#input.attachmentCapability) {
117
+ const attachment = this.#input.attachmentCapability();
118
+ value.snapshot.capabilities.capabilities.attachments = { status: "available", version: 1, reason: null,
119
+ kinds: attachment.kinds, mimeTypes: attachment.mimeTypes };
120
+ Object.assign(value.snapshot.capabilities.limits, { maxRequestBodyBytes: 11_250_000, maxAttachmentCount: 4,
121
+ maxAttachmentFileBytes: 8_388_608, maxAttachmentTotalBytes: 16_777_216 });
122
+ }
123
+ let modelCatalog: any;
124
+ try { modelCatalog = this.#input.modelCatalog?.(); }
125
+ catch { modelCatalog = null; }
126
+ if (modelCatalog?.state === "ready" && modelCatalog.identity?.sessionRef === control.identity.sessionRef
127
+ && modelCatalog.revision?.sessionOptionRevision === control.revisions.sessionOptionRevision) {
128
+ const idle = control.liveness === "idle" && !dispatchBlocked, optionAvailability = idle ? available : disabled(dispatchBlocked ? "task-control-gate-closed" : "agent-not-idle");
129
+ value.snapshot.capabilities.capabilities["control.sessionOptions"] = {
130
+ status: "available", version: 1, reason: null, effectScope: "session-and-user-default",
131
+ allowedLifecyclePoints: ["idle", "pre-fresh-task"], actions: { setModel: optionAvailability, setThinking: optionAvailability }
132
+ };
133
+ }
134
+ value.snapshot.session.operation = { liveness: control.liveness, operationRef: control.identity.agentOperationId,
135
+ hostPhase: control.liveness === "unknown" ? { state: "unknown", value: null, evidence: null, reasonCode: "bridge-liveness-unavailable" }
136
+ : { state: "known", value: control.liveness === "idle" ? "idle" : "other", evidence: "observed", reasonCode: null },
137
+ startedAt: null, settledAt: null, reasonCode: control.liveness === "unknown" ? "bridge-liveness-unavailable" : null };
138
+ value.snapshot.session.queue = { state: "known", hasPending: control.heldCount > 0, heldCount: control.heldCount,
139
+ revision: control.queueRevision ?? control.revisions.queueRevision, reasonCode: null };
140
+ if (lifecycle) {
141
+ value.snapshot.session.controlState = lifecycle.state;
142
+ if (value.snapshot.task) value.snapshot.task.controlState = lifecycle.state;
143
+ value.snapshot.revision.controlRevision = lifecycle.controlRevision ?? control.revisions.controlRevision;
144
+ const phases = ["idle", "input-preflight", "model", "tool-preflight", "waiting-approval", "tool", "retry", "compaction", "branch-summary", "direct-bash", "settling", "other", "unknown"];
145
+ const stopPhaseSupport = Object.fromEntries(phases.map((phase) => [phase, phase === lifecycle.currentPhase && lifecycle.actions.stop
146
+ ? { stop: "supported", reasonCode: null } : { stop: phase === lifecycle.currentPhase ? "unsupported" : "unknown", reasonCode: phase === lifecycle.currentPhase ? "no-current-operation" : "phase-not-observed" }]));
147
+ value.snapshot.capabilities.capabilities["control.lifecycle"] = { status: "available", version: 1, reason: null,
148
+ currentPhase: phases.includes(lifecycle.currentPhase) ? lifecycle.currentPhase : "unknown",
149
+ actions: { pause: lifecycle.actions.pause ? available : disabled(lifecycle.state === "terminal" ? "task-terminal" : `control-state-${lifecycle.state}`),
150
+ resume: lifecycle.actions.resume ? available : disabled(lifecycle.state === "active" ? "already-active" : `control-state-${lifecycle.state}`) }, stopPhaseSupport };
151
+ value.snapshot.capabilities.capabilities["control.resumeAndContinue"] = lifecycle.actions.resume && control.liveness === "idle"
152
+ && Boolean(control.revisions.queueRevision) ? { status: "available", version: 1, reason: null, delivery: "new-operation",
153
+ requires: ["control.lifecycle", "control.chat"] }
154
+ : { status: "unavailable", version: null, reason: { code: lifecycle.actions.resume ? "agent-not-idle" : `control-state-${lifecycle.state}`,
155
+ message: "Resume & Continue requires a paused task and an idle Pi session." } };
156
+ }
157
+ const approval = this.#input.approvalProjection?.();
158
+ if (approval?.revision) {
159
+ value.snapshot.revision.approvalRevision = approval.revision;
160
+ value.snapshot.approvals = structuredClone(approval.summary);
161
+ value.snapshot.session.approvalState = (approval.summary as any).state;
162
+ value.snapshot.capabilities.capabilities.approve = { status: "available", version: 1, reason: null,
163
+ decisions: ["allow", "deny"], arbitration: "first-valid-cas" };
164
+ }
165
+ if (control.identity.taskId && control.identity.taskRunId && control.revisions.taskRevision && value.snapshot.revision.workspaceRevision) {
166
+ const mutationGuardAvailable = this.#input.sourceMutationGuardAvailable?.() === true;
167
+ const chatSend = value.snapshot.capabilities.capabilities["control.chat"]?.status === "available"
168
+ && value.snapshot.capabilities.capabilities["control.chat"].actions.send.available;
169
+ value.snapshot.capabilities.capabilities.reviewActions = { status: "available", version: 1, reason: null, actions: {
170
+ reviewMark: available,
171
+ stage: control.liveness === "idle" && !terminal && mutationGuardAvailable ? available
172
+ : disabled(terminal ? "task-terminal" : control.liveness !== "idle" ? "agent-not-idle" : "mutation-guard-unavailable"),
173
+ unstage: control.liveness === "idle" && !terminal && mutationGuardAvailable ? available
174
+ : disabled(terminal ? "task-terminal" : control.liveness !== "idle" ? "agent-not-idle" : "mutation-guard-unavailable"),
175
+ revert: control.liveness === "idle" && !terminal && mutationGuardAvailable ? available
176
+ : disabled(terminal ? "task-terminal" : control.liveness !== "idle" ? "agent-not-idle" : "mutation-guard-unavailable"),
177
+ openInVsCode: this.#input.sourceOpenAvailable?.() === true ? available : disabled("vscode-cli-unavailable"),
178
+ generateCommitSummaryDeterministic: available,
179
+ generateCommitSummaryModel: chatSend && control.liveness === "idle" && !terminal ? available
180
+ : disabled(terminal ? "task-terminal" : control.liveness !== "idle" ? "agent-not-idle" : "chat-dispatch-unavailable")
181
+ } };
182
+ }
183
+ }
184
+ if (projectionEpoch === this.#invalidationEpoch) this.#cachedProjection = { at: Date.now(), value };
185
+ return value;
186
+ }
187
+
188
+ async snapshot(): Promise<unknown> { return (await this.#projection()).snapshot; }
189
+ async sourceChanges(view: SourceView): Promise<unknown> {
190
+ const source = (await this.#projection()).sourceViews;
191
+ return view === "working-tree" ? source.workingTree : source[view];
192
+ }
193
+ async activity(): Promise<unknown> { return (await this.#projection()).snapshot.activity; }
194
+ async logPreview(activityRef: string): Promise<unknown> {
195
+ const activity = (await this.#projection()).snapshot.activity;
196
+ const item = [...activity.running, ...activity.recent].find((candidate: Record<string, unknown>) => candidate.activityRef === activityRef);
197
+ if (!item) throw new ReadModelNotFound();
198
+ return { activityRef, state: "available", preview: String((item as Record<string, unknown>).preview ?? "").slice(0, 65_536), truncated: false, reasonCode: null };
199
+ }
200
+ async transcript(beforeCursor: string | null, limit: number): Promise<unknown> {
201
+ const snapshot = await this.snapshot() as any;
202
+ return projectTranscript({ identity: { projectRef: snapshot.identity.projectRef, runtimeInstanceId: snapshot.identity.runtimeInstanceId,
203
+ sessionRef: snapshot.identity.sessionRef, taskId: snapshot.identity.taskId, taskRunId: snapshot.identity.taskRunId, agentOperationId: null, toolCallId: null },
204
+ revision: { runtimeRevision: snapshot.revision.runtimeRevision, taskRevision: snapshot.revision.taskRevision,
205
+ controlRevision: snapshot.revision.controlRevision, workspaceRevision: snapshot.revision.workspaceRevision,
206
+ indexRevision: snapshot.revision.indexRevision, approvalRevision: snapshot.revision.approvalRevision,
207
+ sessionOptionRevision: snapshot.revision.sessionOptionRevision, queueRevision: snapshot.revision.queueRevision },
208
+ eventCursor: snapshot.revision.eventCursor, entries: this.#input.sessionEntries?.() ?? [], beforeCursor, limit,
209
+ generatedAt: snapshot.generatedAt });
210
+ }
211
+ async queue(): Promise<unknown> {
212
+ if (!this.#input.queueProjection) throw new ReadModelNotFound();
213
+ return this.#input.queueProjection();
214
+ }
215
+ async modelCatalog(): Promise<unknown> {
216
+ if (!this.#input.modelCatalog) throw new ReadModelNotFound();
217
+ return this.#input.modelCatalog();
218
+ }
219
+ async approval(approvalRef: string): Promise<unknown> {
220
+ const value = this.#input.approvalDetail?.(approvalRef); if (!value) throw new ReadModelNotFound(); return value;
221
+ }
222
+ async diff(view: SourceView, fileRef: string): Promise<unknown> {
223
+ const cacheKey = `${view}:${fileRef}`;
224
+ const cached = this.#diffCache.get(cacheKey);
225
+ if (cached && Date.now() - cached.at <= 200) return cached.value;
226
+ const { collectFileDiff, matchesProtectedPath } = await loadCoreInspection();
227
+ const projection = await this.#projection();
228
+ const source = view === "working-tree" ? projection.sourceViews.workingTree : projection.sourceViews[view];
229
+ const file = source?.files.find((candidate: any) => candidate.fileRef === fileRef) as Record<string, any> | undefined;
230
+ if (!source || !file) throw new ReadModelNotFound();
231
+ const identity = projection.snapshot.identity;
232
+ const protectedPaths = this.#input.protectedPaths?.() ?? [];
233
+ const value = await collectFileDiff({
234
+ cwd: this.#input.cwd, identity, sourceView: source, fileRef,
235
+ precondition: { expectedViewRevision: source.viewRevision, expectedFileRevision: file.fileRevision,
236
+ expectedBaseDigest: file.baseDigest ?? null, expectedCurrentDigest: file.currentDigest ?? null },
237
+ taskRevision: projection.snapshot.revision.taskRevision,
238
+ redactLine: (text: string) => redactSensitiveText(text),
239
+ revalidationMode: "selected-file",
240
+ selectedRepoPaths: file.pathDisplay === "exact-safe" ? [file.oldPath, file.path].filter((value: unknown): value is string => typeof value === "string") : undefined,
241
+ isProtectedPath: (_root: string, repoPath: string) => Boolean(matchesProtectedPath(repoPath, protectedPaths))
242
+ });
243
+ this.#diffCache.set(cacheKey, { at: Date.now(), value });
244
+ if (this.#diffCache.size > 128) this.#diffCache.delete(this.#diffCache.keys().next().value as string);
245
+ return value;
246
+ }
247
+
248
+ async review(view: SourceView, fileRef: string): Promise<unknown> {
249
+ // Review is authority-bearing evidence. Never reuse the short UI projection
250
+ // cache when deriving its exact Git/task preimage.
251
+ this.invalidate();
252
+ const projection = await this.#projection(), snapshot = projection.snapshot;
253
+ const identity = { ...snapshot.identity, agentOperationId: null, toolCallId: null };
254
+ if (!identity.taskId || !identity.taskRunId || !snapshot.revision.taskRevision || !snapshot.revision.workspaceRevision) throw new ReadModelNotFound();
255
+ const diff = await this.diff(view, fileRef) as any;
256
+ const { deriveReviewTarget, projectReviewState, readReviewEvidence } = await loadCoreInspection();
257
+ const target = deriveReviewTarget({ diff, taskId: identity.taskId, taskRunId: identity.taskRunId,
258
+ taskRevision: snapshot.revision.taskRevision, workspaceRevision: snapshot.revision.workspaceRevision,
259
+ indexRevision: snapshot.revision.indexRevision });
260
+ const evidence = readReviewEvidence(this.#input.cwd, identity.taskRunId);
261
+ return projectReviewState({ identity, target, records: evidence.records, corruptions: evidence.corruptions,
262
+ unavailableReason: diff.availability?.reasonCode ?? diff.fallback?.reasonCode ?? "review-target-unavailable" });
263
+ }
264
+
265
+ async #sourceMutation(action: "source.stage" | "source.unstage", fileRef: string) {
266
+ this.invalidate();
267
+ const projection = await this.#projection(), snapshot = projection.snapshot;
268
+ const identity = { ...snapshot.identity, agentOperationId: null, toolCallId: null };
269
+ if (!identity.taskId || !identity.taskRunId || !snapshot.revision.taskRevision || !snapshot.revision.workspaceRevision || !snapshot.revision.indexRevision)
270
+ throw new ReadModelNotFound();
271
+ const view: SourceView = action === "source.stage" ? "working-tree" : "staged";
272
+ const source = view === "working-tree" ? projection.sourceViews.workingTree : projection.sourceViews.staged;
273
+ if (!source?.files.some((file: any) => file.fileRef === fileRef)) throw new ReadModelNotFound();
274
+ const diff = await this.diff(view, fileRef) as any;
275
+ const { collectSourceMutationPreview, matchesProtectedPath } = await loadCoreInspection();
276
+ const protectedPaths = this.#input.protectedPaths?.() ?? [];
277
+ return collectSourceMutationPreview({ cwd: this.#input.cwd, identity, action, sourceView: source, diff,
278
+ taskRevision: snapshot.revision.taskRevision, workspaceRevision: snapshot.revision.workspaceRevision,
279
+ indexRevision: snapshot.revision.indexRevision,
280
+ guardAvailable: this.#input.sourceMutationGuardAvailable?.() === true,
281
+ isProtectedPath: (_root: string, repoPath: string) => Boolean(matchesProtectedPath(repoPath, protectedPaths)) });
282
+ }
283
+
284
+ async sourceMutation(action: "source.stage" | "source.unstage", fileRef: string): Promise<unknown> {
285
+ return (await this.#sourceMutation(action, fileRef)).projection;
286
+ }
287
+
288
+ async sourceMutationAuthority(action: "source.stage" | "source.unstage", fileRef: string) {
289
+ return this.#sourceMutation(action, fileRef);
290
+ }
291
+
292
+ async #sourceRevert(fileRef: string, hunkRef: string | null) {
293
+ this.invalidate();
294
+ const projection = await this.#projection(), snapshot = projection.snapshot;
295
+ const identity = { ...snapshot.identity, agentOperationId: null, toolCallId: null };
296
+ if (!identity.taskId || !identity.taskRunId || !snapshot.revision.taskRevision || !snapshot.revision.workspaceRevision || !snapshot.revision.indexRevision)
297
+ throw new ReadModelNotFound();
298
+ const source = projection.sourceViews.workingTree, taskView = projection.sourceViews.task ?? null;
299
+ if (!source?.files.some((file: any) => file.fileRef === fileRef)) throw new ReadModelNotFound();
300
+ const diff = await this.diff("working-tree", fileRef) as any;
301
+ const { collectSourceRevertPreview, matchesProtectedPath } = await loadCoreInspection();
302
+ const protectedPaths = this.#input.protectedPaths?.() ?? [];
303
+ return collectSourceRevertPreview({ cwd: this.#input.cwd, identity, sourceView: source, taskView, diff,
304
+ taskRevision: snapshot.revision.taskRevision, workspaceRevision: snapshot.revision.workspaceRevision,
305
+ indexRevision: snapshot.revision.indexRevision, selectedHunkRefs: hunkRef ? [hunkRef] : [],
306
+ confirmationKey: this.#sourceRevertConfirmationKey,
307
+ guardAvailable: this.#input.sourceMutationGuardAvailable?.() === true,
308
+ isProtectedPath: (_root: string, repoPath: string) => Boolean(matchesProtectedPath(repoPath, protectedPaths)) });
309
+ }
310
+
311
+ async sourceRevert(fileRef: string, hunkRef: string | null): Promise<unknown> {
312
+ return (await this.#sourceRevert(fileRef, hunkRef)).projection;
313
+ }
314
+
315
+ async sourceRevertAuthority(fileRef: string, hunkRefs: string[]) {
316
+ if (hunkRefs.length > 1) throw new ReadModelNotFound();
317
+ return this.#sourceRevert(fileRef, hunkRefs[0] ?? null);
318
+ }
319
+
320
+ async sourceOpenAuthority(fileRef: string) {
321
+ this.invalidate(); const projection = await this.#projection(), snapshot = projection.snapshot;
322
+ const identity = { ...snapshot.identity, agentOperationId: null, toolCallId: null };
323
+ if (!identity.taskId || !identity.taskRunId || !snapshot.revision.taskRevision || !snapshot.revision.workspaceRevision) return null;
324
+ const { resolveSourceOpenTarget, matchesProtectedPath } = await loadCoreInspection(), protectedPaths = this.#input.protectedPaths?.() ?? [];
325
+ return resolveSourceOpenTarget({ cwd: this.#input.cwd, identity, sourceView: projection.sourceViews.workingTree, fileRef,
326
+ taskRevision: snapshot.revision.taskRevision, workspaceRevision: snapshot.revision.workspaceRevision,
327
+ isProtectedPath: (_root: string, repoPath: string) => Boolean(matchesProtectedPath(repoPath, protectedPaths)) });
328
+ }
329
+
330
+ async commitSummary(): Promise<unknown> {
331
+ this.invalidate(); const projection = await this.#projection(), snapshot = projection.snapshot;
332
+ const identity = { ...snapshot.identity, agentOperationId: null, toolCallId: null };
333
+ if (!identity.taskId || !identity.taskRunId || !snapshot.revision.taskRevision || !snapshot.revision.indexRevision) throw new ReadModelNotFound();
334
+ const { projectDeterministicCommitSummary } = await loadCoreInspection();
335
+ return projectDeterministicCommitSummary({ identity, sourceView: projection.sourceViews.staged,
336
+ taskRevision: snapshot.revision.taskRevision, indexRevision: snapshot.revision.indexRevision });
337
+ }
338
+
339
+ async taskIndex(): Promise<unknown> {
340
+ this.invalidate(); const snapshot = await this.snapshot() as any;
341
+ const identity = { ...snapshot.identity, agentOperationId: null, toolCallId: null };
342
+ const { projectTaskRunIndex } = await loadCoreInspection();
343
+ return projectTaskRunIndex({ cwd: this.#input.cwd, identity, currentSessionId: this.#input.sessionId });
344
+ }
345
+
346
+ async taskTimeline(runRef: string): Promise<unknown> {
347
+ this.invalidate(); const snapshot = await this.snapshot() as any;
348
+ const { projectTaskRecoveryTimeline, resolveTaskRunRef } = await loadCoreInspection();
349
+ const task = resolveTaskRunRef(this.#input.cwd, runRef); if (!task) throw new ReadModelNotFound();
350
+ const identity = { ...snapshot.identity, taskId: task.taskId, taskRunId: task.taskRunId, agentOperationId: null, toolCallId: null };
351
+ return projectTaskRecoveryTimeline({ cwd: this.#input.cwd, identity, task });
352
+ }
353
+
354
+ async recoveryHistory(runRef: string): Promise<unknown> {
355
+ this.invalidate(); const snapshot = await this.snapshot() as any;
356
+ const { projectTaskCompactionHistory, resolveTaskRunRef } = await loadCoreInspection();
357
+ const task = resolveTaskRunRef(this.#input.cwd, runRef); if (!task) throw new ReadModelNotFound();
358
+ const identity = { ...snapshot.identity, taskId: task.taskId, taskRunId: task.taskRunId, agentOperationId: null, toolCallId: null };
359
+ return projectTaskCompactionHistory({ cwd: this.#input.cwd, identity, task });
360
+ }
361
+
362
+ async handoffHistory(runRef: string): Promise<unknown> {
363
+ this.invalidate(); const snapshot = await this.snapshot() as any;
364
+ const { projectTaskHandoffHistory, resolveTaskRunRef } = await loadCoreInspection();
365
+ const task = resolveTaskRunRef(this.#input.cwd, runRef); if (!task) throw new ReadModelNotFound();
366
+ const identity = { ...snapshot.identity, taskId: task.taskId, taskRunId: task.taskRunId, agentOperationId: null, toolCallId: null };
367
+ return projectTaskHandoffHistory({ cwd: this.#input.cwd, identity, task });
368
+ }
369
+
370
+ async subagentTree(runRef: string): Promise<unknown> {
371
+ this.invalidate(); const snapshot = await this.snapshot() as any;
372
+ const { projectTaskSubagentTree, resolveTaskRunRef } = await loadCoreInspection();
373
+ const task = resolveTaskRunRef(this.#input.cwd, runRef); if (!task) throw new ReadModelNotFound();
374
+ const identity = { ...snapshot.identity, taskId: task.taskId, taskRunId: task.taskRunId, agentOperationId: null, toolCallId: null };
375
+ return projectTaskSubagentTree({ cwd: this.#input.cwd, identity, task });
376
+ }
377
+
378
+ async releaseMonitor(): Promise<unknown> {
379
+ const snapshot = await this.snapshot() as any;
380
+ const identity = { ...snapshot.identity, agentOperationId: null, toolCallId: null };
381
+ const { projectBenchmarkReleaseMonitor } = await loadCoreInspection();
382
+ return projectBenchmarkReleaseMonitor({ cwd: this.#input.cwd, identity });
383
+ }
384
+
385
+ async canonicalRevisions(): Promise<Record<string, string | null>> {
386
+ this.invalidate(); return structuredClone((await this.#projection()).snapshot.revision);
387
+ }
388
+
389
+ replay(after: string | null, limit: number) {
390
+ const replay = this.#input.eventStore.replay(after, limit);
391
+ if (replay.state !== "current") {
392
+ this.#cachedProjection = null;
393
+ this.#diffCache.clear();
394
+ }
395
+ return { ...replay, events: replay.events.map((event) => ({ cursor: event.eventCursor, value: event })) };
396
+ }
397
+ subscribe(listener: (event: StreamEvent) => void): () => void { this.#listeners.add(listener); return () => this.#listeners.delete(listener); }
398
+ publishObserved(event: RuntimeEvent): void {
399
+ this.invalidate();
400
+ const item = { cursor: event.eventCursor, value: event };
401
+ for (const listener of this.#listeners) listener(item);
402
+ }
403
+ }
@@ -0,0 +1,143 @@
1
+ import type { IncomingMessage, Server } from "node:http";
2
+ import { WebSocket, WebSocketServer } from "ws";
3
+
4
+ import type { SessionAuthority } from "./session-auth.ts";
5
+
6
+ const FRAME_LIMIT = 70_000;
7
+ const CLIENT_LIMIT = 8;
8
+ const BACKPRESSURE_LIMIT = 512 * 1024;
9
+ const REF = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,159}$/;
10
+ const REQUEST_ID = /^[A-Za-z0-9_-]{16,96}$/;
11
+
12
+ type GatewayProtocolEvent = {
13
+ sequence: number;
14
+ [key: string]: unknown;
15
+ };
16
+ export type GatewayRequest = { requestId: string; method: "gateway.health" | "sessions.list" | "sessions.get" | "sessions.command"; params: Record<string, unknown> };
17
+ export type GatewayProtocolHandler = {
18
+ capabilities(): unknown;
19
+ execute(request: GatewayRequest): Promise<unknown>;
20
+ events: {
21
+ readonly stateVersion: number;
22
+ subscribe(listener: (event: GatewayProtocolEvent) => void): () => void;
23
+ replay(after: number | null): { state: "current" | "resync-required"; events: GatewayProtocolEvent[]; earliestSequence: number; currentSequence: number };
24
+ };
25
+ };
26
+
27
+ function exactKeys(value: Record<string, unknown>, keys: string[]): boolean {
28
+ return Object.keys(value).length === keys.length && Object.keys(value).every((key) => keys.includes(key));
29
+ }
30
+
31
+ function record(value: unknown): value is Record<string, unknown> { return Boolean(value) && typeof value === "object" && !Array.isArray(value); }
32
+ function nullableRef(value: unknown): boolean { return value === null || typeof value === "string" && REF.test(value); }
33
+
34
+ function parseConnect(value: unknown): { lastEventSequence: number | null } | null {
35
+ if (!record(value) || !exactKeys(value, ["schemaVersion", "version", "messageType", "clientRef", "minimumProtocol", "maximumProtocol", "lastEventSequence", "catalogRevision"])) return null;
36
+ if (value.schemaVersion !== 1 || value.version !== "piagent-gateway-protocol-v1" || value.messageType !== "connect"
37
+ || typeof value.clientRef !== "string" || !REF.test(value.clientRef) || value.minimumProtocol !== 1 || value.maximumProtocol !== 1
38
+ || !nullableRef(value.catalogRevision)) return null;
39
+ const sequence = value.lastEventSequence;
40
+ if (sequence !== null && (!Number.isSafeInteger(sequence) || Number(sequence) < 0)) return null;
41
+ return { lastEventSequence: sequence as number | null };
42
+ }
43
+
44
+ function parseRequest(value: unknown): GatewayRequest | null {
45
+ if (!record(value) || !exactKeys(value, ["schemaVersion", "version", "messageType", "requestId", "method", "params"]) || value.schemaVersion !== 1
46
+ || value.version !== "piagent-gateway-protocol-v1" || value.messageType !== "request" || typeof value.requestId !== "string"
47
+ || !REQUEST_ID.test(value.requestId) || !["gateway.health", "sessions.list", "sessions.get", "sessions.command"].includes(String(value.method))
48
+ || !record(value.params)) return null;
49
+ const params = value.params;
50
+ if (value.method === "gateway.health" && !exactKeys(params, [])) return null;
51
+ if (value.method === "sessions.get" && (!exactKeys(params, ["sessionRef"]) || typeof params.sessionRef !== "string" || !REF.test(params.sessionRef))) return null;
52
+ if (value.method === "sessions.command" && (!exactKeys(params, ["command"]) || !record(params.command))) return null;
53
+ if (value.method === "sessions.list") {
54
+ if (!exactKeys(params, ["cursor", "limit", "filter", "query", "projectRef"]) || !nullableRef(params.cursor) || !nullableRef(params.projectRef)
55
+ || !Number.isInteger(params.limit) || Number(params.limit) < 1 || Number(params.limit) > 200
56
+ || !["active", "archived", "all"].includes(String(params.filter))
57
+ || !(params.query === null || typeof params.query === "string" && params.query.length >= 1 && params.query.length <= 200
58
+ && !/[\u0000-\u001f\u007f]/.test(params.query))) return null;
59
+ }
60
+ return value as GatewayRequest;
61
+ }
62
+
63
+ function send(socket: WebSocket, value: unknown): void {
64
+ if (socket.readyState !== WebSocket.OPEN) return;
65
+ if (socket.bufferedAmount > BACKPRESSURE_LIMIT) { socket.close(1013, "backpressure"); return; }
66
+ const body = JSON.stringify(value);
67
+ if (Buffer.byteLength(body) > FRAME_LIMIT) { socket.close(1009, "frame-limit"); return; }
68
+ socket.send(body);
69
+ }
70
+
71
+ function response(request: GatewayRequest, stateVersion: number, result: unknown) {
72
+ return { schemaVersion: 1, version: "piagent-gateway-protocol-v1", messageType: "response", requestId: request.requestId,
73
+ method: request.method, ok: true, stateVersion, result, error: null };
74
+ }
75
+
76
+ function errorResponse(request: GatewayRequest, stateVersion: number, error: unknown) {
77
+ const failure = error && typeof error === "object" && "code" in error && typeof error.code === "string"
78
+ ? { code: error.code, retryable: "retryable" in error && error.retryable === true } : { code: "gateway-request-failed", retryable: true };
79
+ return { schemaVersion: 1, version: "piagent-gateway-protocol-v1", messageType: "response", requestId: request.requestId,
80
+ method: request.method, ok: false, stateVersion, result: null,
81
+ error: { code: failure.code, message: failure.code, retryable: failure.retryable } };
82
+ }
83
+
84
+ export function attachGatewayWebSocket(options: {
85
+ server: Server;
86
+ origin(): string;
87
+ authority: SessionAuthority;
88
+ protocol: GatewayProtocolHandler;
89
+ }): { close(): Promise<void> } {
90
+ const sockets = new Set<WebSocket>();
91
+ const webSockets = new WebSocketServer({ noServer: true, clientTracking: false, perMessageDeflate: false, maxPayload: FRAME_LIMIT });
92
+ const reject = (socket: import("node:stream").Duplex, status: number) => socket.end(`HTTP/1.1 ${status} Rejected\r\nConnection: close\r\n\r\n`);
93
+ const upgrade = (request: IncomingMessage, socket: import("node:stream").Duplex, head: Buffer) => {
94
+ let url: URL;
95
+ try { url = new URL(request.url ?? "", options.origin()); } catch { reject(socket, 400); return; }
96
+ const protocol = String(request.headers["sec-websocket-protocol"] ?? "").split(",").map((item) => item.trim());
97
+ if (request.headers.origin !== options.origin() || request.headers.host !== options.origin().slice("http://".length)
98
+ || url.pathname !== "/api/v1/gateway" || url.search || !options.authority.authenticate(request)
99
+ || !protocol.includes("piagent.gateway.v1") || sockets.size >= CLIENT_LIMIT) { reject(socket, 403); return; }
100
+ webSockets.handleUpgrade(request, socket, head, (client) => webSockets.emit("connection", client, request));
101
+ };
102
+ options.server.on("upgrade", upgrade);
103
+ webSockets.on("connection", (socket) => {
104
+ sockets.add(socket); let connected = false, unsubscribe: (() => void) | null = null, chain = Promise.resolve();
105
+ const timeout = setTimeout(() => socket.close(1008, "connect-timeout"), 5_000);
106
+ socket.on("message", (data, binary) => {
107
+ if (binary) { socket.close(1003, "text-only"); return; }
108
+ chain = chain.then(async () => {
109
+ let value: unknown;
110
+ try { value = JSON.parse(data.toString()); } catch { socket.close(1007, "invalid-json"); return; }
111
+ if (!connected) {
112
+ const connect = parseConnect(value);
113
+ if (!connect) { socket.close(1008, "invalid-connect"); return; }
114
+ connected = true; clearTimeout(timeout);
115
+ const pending: GatewayProtocolEvent[] = [], seen = new Set<number>(); let replaying = true;
116
+ unsubscribe = options.protocol.events.subscribe((event) => replaying ? pending.push(event) : send(socket, event));
117
+ send(socket, { schemaVersion: 1, version: "piagent-gateway-protocol-v1", messageType: "hello", capabilities: options.protocol.capabilities() });
118
+ const replay = options.protocol.events.replay(connect.lastEventSequence);
119
+ if (replay.state === "resync-required") send(socket, {
120
+ schemaVersion: 1, version: "piagent-gateway-protocol-v1", messageType: "event", sequence: replay.currentSequence,
121
+ stateVersion: replay.currentSequence, generatedAt: new Date().toISOString(), kind: "resync.required",
122
+ payload: { reasonCode: "gateway-event-cursor-gap", earliestSequence: replay.earliestSequence, currentSequence: replay.currentSequence }
123
+ });
124
+ else for (const event of replay.events) { seen.add(event.sequence); send(socket, event); }
125
+ replaying = false;
126
+ for (const event of pending) if (!seen.has(event.sequence)) send(socket, event);
127
+ return;
128
+ }
129
+ const request = parseRequest(value);
130
+ if (!request) { socket.close(1008, "invalid-request"); return; }
131
+ try { send(socket, response(request, options.protocol.events.stateVersion, await options.protocol.execute(request))); }
132
+ catch (error) { send(socket, errorResponse(request, options.protocol.events.stateVersion, error)); }
133
+ }).catch(() => socket.close(1011, "gateway-failure"));
134
+ });
135
+ socket.once("close", () => { clearTimeout(timeout); unsubscribe?.(); sockets.delete(socket); });
136
+ socket.once("error", () => socket.terminate());
137
+ });
138
+ return { close: async () => {
139
+ options.server.off("upgrade", upgrade);
140
+ for (const socket of sockets) socket.terminate();
141
+ await new Promise<void>((resolve) => webSockets.close(() => resolve()));
142
+ } };
143
+ }
@@ -0,0 +1,27 @@
1
+ import type { ServerResponse } from "node:http";
2
+
3
+ export const SECURITY_HEADERS = Object.freeze({
4
+ "Cross-Origin-Opener-Policy": "same-origin",
5
+ "Permissions-Policy": "camera=(), microphone=(), geolocation=(), payment=(), usb=()",
6
+ "Referrer-Policy": "no-referrer",
7
+ "X-Content-Type-Options": "nosniff",
8
+ "X-Frame-Options": "DENY"
9
+ });
10
+
11
+ export function applySecurityHeaders(response: ServerResponse, styleNonce: string): void {
12
+ for (const [name, value] of Object.entries(SECURITY_HEADERS)) response.setHeader(name, value);
13
+ response.setHeader("Content-Security-Policy", `default-src 'self'; base-uri 'none'; object-src 'none'; frame-ancestors 'none'; img-src 'self' data:; style-src 'self'; style-src-elem 'self' 'nonce-${styleNonce}'; style-src-attr 'unsafe-inline'; script-src 'self'; connect-src 'self'`);
14
+ }
15
+
16
+ export function jsonResponse(response: ServerResponse, status: number, value: unknown): void {
17
+ const body = Buffer.from(JSON.stringify(value));
18
+ response.statusCode = status;
19
+ response.setHeader("Cache-Control", "no-store");
20
+ response.setHeader("Content-Type", "application/json; charset=utf-8");
21
+ response.setHeader("Content-Length", body.length);
22
+ response.end(body);
23
+ }
24
+
25
+ export function errorResponse(response: ServerResponse, status: number, code: string): void {
26
+ jsonResponse(response, status, { error: { code } });
27
+ }