@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,485 @@
1
+ import { createHmac, randomBytes } from "node:crypto";
2
+ import path from "node:path";
3
+
4
+ import { webUiModelRef } from "../../piagent-core/runtime/inspection/webui-snapshot.ts";
5
+ import { webUiTaskRevision } from "../../piagent-core/runtime/inspection/webui-snapshot.ts";
6
+ import { activeSessionTask } from "../../piagent-core/extensions/task-state.js";
7
+ import { inspectTaskControlState } from "../../piagent-core/runtime/inspection/task-control-journal.ts";
8
+ import { piApprovalBroker, type ApprovalAuthority, type ApprovalBrokerEvent } from "../../piagent-core/runtime/inspection/approval-broker.ts";
9
+ import type { SessionOwnerProjection, PiSessionInfo } from "./session-catalog.ts";
10
+ import { projectRefForCwd, sessionRefForPath } from "./session-catalog.ts";
11
+ import { SessionLeaseStore, type SessionLeaseSnapshot } from "./session-lease-store.ts";
12
+ import { GatewayEventStore } from "./gateway-events.ts";
13
+ import { GatewaySessionStream } from "./gateway-session-stream.ts";
14
+ import { GATEWAY_RUNTIME_UI_MARKER } from "../ownership/gateway-runtime-context.ts";
15
+
16
+ const MAX_WARM_RUNTIMES = 10;
17
+
18
+ type RuntimeHandle = { dispose(): Promise<void>; session?: any };
19
+ type RuntimeFactory = (info: PiSessionInfo, runtimeInstanceRef: string, sessionManager?: any) => Promise<RuntimeHandle>;
20
+ type ActiveRuntime = { runtime: RuntimeHandle; lease: SessionLeaseSnapshot; info: PiSessionInfo; operationRef: string | null;
21
+ unsubscribe: (() => void) | null; completion: Promise<void> | null; settling: boolean; approvalWaiting: boolean;
22
+ unbindApproval: (() => void) | null; unsubscribeApproval: (() => void) | null };
23
+ type Projection = { sessionRevision: string; liveState: "offline" | "idle" | "running" | "paused" | "waiting-approval" | "uncertain" };
24
+
25
+ function rpcUiContext(): object {
26
+ const plain = (text: unknown): string => String(text ?? "");
27
+ const theme = Object.freeze({
28
+ fg: (_color: unknown, text: unknown) => plain(text),
29
+ bg: (_color: unknown, text: unknown) => plain(text),
30
+ bold: plain,
31
+ italic: plain,
32
+ underline: plain,
33
+ inverse: plain,
34
+ strikethrough: plain,
35
+ getFgAnsi: () => "",
36
+ getBgAnsi: () => "",
37
+ getColorMode: () => "truecolor",
38
+ getThinkingBorderColor: () => plain,
39
+ getBashModeBorderColor: () => plain
40
+ });
41
+ return new Proxy({}, {
42
+ get(_target, property) {
43
+ if (property === GATEWAY_RUNTIME_UI_MARKER) return true;
44
+ if (property === "theme") return theme;
45
+ if (property === "confirm") return () => new Promise<boolean>(() => undefined);
46
+ if (property === "select" || property === "input") return async () => undefined;
47
+ return () => undefined;
48
+ }
49
+ });
50
+ }
51
+
52
+ function productionRuntimeFactory(options: { host: any; agentDir: string; packageRoot: string; modelRuntime?: any }): RuntimeFactory {
53
+ return async (info, _runtimeInstanceRef, sessionManager) => {
54
+ const guard = path.join(options.packageRoot, "packages", "piagent-core", "extensions", "piagent-guard.ts");
55
+ const createRuntime = async ({ cwd, agentDir, sessionManager, sessionStartEvent }: any) => {
56
+ const services = await options.host.createAgentSessionServices({
57
+ cwd, agentDir, modelRuntime: options.modelRuntime,
58
+ resourceLoaderOptions: { additionalExtensionPaths: [guard] }
59
+ });
60
+ const extensionErrors = services.resourceLoader.getExtensions().errors;
61
+ if (extensionErrors.length) throw new Error("session-runtime-extension-load-failed");
62
+ const created = await options.host.createAgentSessionFromServices({ services, sessionManager, sessionStartEvent });
63
+ return { ...created, services, diagnostics: services.diagnostics };
64
+ };
65
+ const manager = sessionManager ?? options.host.SessionManager.open(info.path);
66
+ const runtime = await options.host.createAgentSessionRuntime(createRuntime, {
67
+ cwd: info.cwd,
68
+ agentDir: options.agentDir,
69
+ sessionManager: manager,
70
+ sessionStartEvent: { type: "session_start", reason: "resume", previousSessionFile: info.path }
71
+ });
72
+ const bind = async (session: any) => session.bindExtensions({ mode: "rpc", uiContext: rpcUiContext() });
73
+ runtime.setRebindSession(bind);
74
+ try { await bind(runtime.session); }
75
+ catch (error) { await runtime.dispose().catch(() => undefined); throw error; }
76
+ return runtime;
77
+ };
78
+ }
79
+
80
+ export class SessionRuntimeSupervisor {
81
+ readonly #gatewayInstanceRef: string;
82
+ readonly #key: Buffer;
83
+ readonly #leases: SessionLeaseStore;
84
+ readonly #listSessions: () => Promise<PiSessionInfo[]>;
85
+ readonly #runtimeFactory: RuntimeFactory;
86
+ readonly #events: GatewayEventStore;
87
+ readonly #host: any | null;
88
+ readonly #resolveProject: ((projectRef: string) => string | null) | null;
89
+ readonly #active = new Map<string, ActiveRuntime>();
90
+ readonly #opening = new Map<string, Promise<SessionLeaseSnapshot>>();
91
+ readonly #created = new Map<string, PiSessionInfo>();
92
+ #closed = false;
93
+ #readProjection: ((sessionRef: string) => Promise<Projection>) | null = null;
94
+
95
+ constructor(options: {
96
+ gatewayInstanceRef: string;
97
+ key: Buffer;
98
+ leases: SessionLeaseStore;
99
+ listSessions(): Promise<PiSessionInfo[]>;
100
+ runtimeFactory?: RuntimeFactory;
101
+ host?: any;
102
+ agentDir?: string;
103
+ packageRoot?: string;
104
+ modelRuntime?: any;
105
+ events?: GatewayEventStore;
106
+ resolveProject?(projectRef: string): string | null;
107
+ }) {
108
+ this.#gatewayInstanceRef = options.gatewayInstanceRef;
109
+ this.#key = options.key;
110
+ this.#leases = options.leases;
111
+ this.#listSessions = options.listSessions;
112
+ this.#events = options.events ?? new GatewayEventStore();
113
+ this.#host = options.host ?? null;
114
+ this.#resolveProject = options.resolveProject ?? null;
115
+ if (options.runtimeFactory) this.#runtimeFactory = options.runtimeFactory;
116
+ else {
117
+ if (!options.host || !options.agentDir || !options.packageRoot) throw new Error("session-runtime-factory-unavailable");
118
+ this.#runtimeFactory = productionRuntimeFactory({ host: options.host, agentDir: options.agentDir,
119
+ packageRoot: options.packageRoot, modelRuntime: options.modelRuntime });
120
+ }
121
+ }
122
+
123
+ get activeCount(): number { return this.#active.size; }
124
+
125
+ setProjectionReader(reader: (sessionRef: string) => Promise<Projection>): void { this.#readProjection = reader; }
126
+
127
+ async listSessions(): Promise<PiSessionInfo[]> {
128
+ const persisted = await this.#listSessions();
129
+ const paths = new Set(persisted.map((info) => info.path));
130
+ for (const [sessionRef, info] of this.#created) {
131
+ if (paths.has(info.path)) this.#created.delete(sessionRef);
132
+ }
133
+ return [...persisted, ...[...this.#created.values()].filter((info) => !paths.has(info.path))];
134
+ }
135
+
136
+ async create(projectRef: string, placeRef: string, modelRef: string | null, thinkingLevel: string): Promise<string> {
137
+ if (this.#closed || !this.#host) throw new Error("session-create-unavailable");
138
+ if (placeRef !== projectRef) throw new Error("session-place-mismatch");
139
+ const sessions = await this.#listSessions();
140
+ const imported = this.#resolveProject?.(projectRef) ?? null;
141
+ const candidates = [...new Set([...sessions.filter((info) => projectRefForCwd(this.#key, info.cwd) === projectRef).map((info) => info.cwd),
142
+ ...(imported ? [imported] : [])])];
143
+ if (candidates.length !== 1 || !candidates[0]) throw new Error(candidates.length ? "session-project-ambiguous" : "session-project-not-found");
144
+ const manager = this.#host.SessionManager.create(candidates[0]);
145
+ const sessionFile = manager.getSessionFile();
146
+ if (!sessionFile || typeof manager.getSessionId?.() !== "string") throw new Error("session-create-unavailable");
147
+ const createdAt = new Date();
148
+ const created: PiSessionInfo = { path: sessionFile, id: manager.getSessionId(), cwd: candidates[0], created: createdAt,
149
+ modified: createdAt, messageCount: 0, firstMessage: "(no messages)", allMessagesText: "" };
150
+ const sessionRef = sessionRefForPath(this.#key, sessionFile);
151
+ await this.#activate(sessionRef, created, manager);
152
+ const active = this.#active.get(sessionRef), session = active?.runtime.session;
153
+ if (!session) throw new Error("session-runtime-unavailable");
154
+ if (modelRef) {
155
+ const models = session.modelRuntime?.getAvailableSnapshot?.() ?? [];
156
+ const model = models.find((value: any) => webUiModelRef(String(value.provider ?? ""), String(value.id ?? "")) === modelRef);
157
+ if (!model) throw new Error("session-model-unavailable");
158
+ await session.setModel(model);
159
+ }
160
+ session.setThinkingLevel(thinkingLevel);
161
+ this.#created.set(sessionRef, created);
162
+ return sessionRef;
163
+ }
164
+
165
+ async acquire(sessionRef: string): Promise<SessionLeaseSnapshot> {
166
+ if (this.#closed) throw new Error("session-runtime-supervisor-closed");
167
+ const active = this.#active.get(sessionRef);
168
+ if (active) return active.lease;
169
+ const pending = this.#opening.get(sessionRef);
170
+ if (pending) return await pending;
171
+ const opening = this.#acquire(sessionRef).finally(() => this.#opening.delete(sessionRef));
172
+ this.#opening.set(sessionRef, opening);
173
+ return await opening;
174
+ }
175
+
176
+ async #acquire(sessionRef: string): Promise<SessionLeaseSnapshot> {
177
+ if (this.#active.size + this.#opening.size > MAX_WARM_RUNTIMES) throw new Error("session-runtime-capacity");
178
+ const found = (await this.#listSessions()).filter((info) => sessionRefForPath(this.#key, info.path) === sessionRef);
179
+ if (found.length !== 1) throw new Error(found.length ? "session-ref-ambiguous" : "session-not-found");
180
+ return await this.#activate(sessionRef, found[0]!);
181
+ }
182
+
183
+ async #activate(sessionRef: string, info: PiSessionInfo, sessionManager?: any): Promise<SessionLeaseSnapshot> {
184
+ if (this.#active.has(sessionRef)) throw new Error("session-owner-conflict");
185
+ const runtimeInstanceRef = `runtime_${randomBytes(24).toString("base64url")}`;
186
+ const prior = this.#leases.inspect(sessionRef);
187
+ if (prior.state === "gateway-owned" || prior.state === "terminal-owned") {
188
+ try { this.#leases.releaseDeadOwnerForExplicitRecovery(sessionRef); } catch { /* live or unprovable owners stay authoritative */ }
189
+ }
190
+ const lease = this.#leases.acquire(sessionRef, this.#gatewayInstanceRef, runtimeInstanceRef);
191
+ try {
192
+ const runtime = await this.#runtimeFactory(info, runtimeInstanceRef, sessionManager);
193
+ const current = this.#leases.inspect(sessionRef);
194
+ if (current.state !== "gateway-owned" || current.ownerEpoch !== lease.ownerEpoch
195
+ || current.gatewayInstanceRef !== this.#gatewayInstanceRef || current.runtimeInstanceRef !== runtimeInstanceRef) {
196
+ await runtime.dispose().catch(() => undefined);
197
+ throw new Error("session-owner-continuity-lost");
198
+ }
199
+ const active: ActiveRuntime = { runtime, lease: current, info, operationRef: null, unsubscribe: null, completion: null,
200
+ settling: false, approvalWaiting: false, unbindApproval: null, unsubscribeApproval: null };
201
+ this.#active.set(sessionRef, active);
202
+ this.#bindApproval(sessionRef, active);
203
+ return current;
204
+ } catch (error) {
205
+ try { this.#leases.requireRecovery(sessionRef, lease.ownerEpoch!, this.#gatewayInstanceRef, runtimeInstanceRef, "session-runtime-open-failed"); }
206
+ catch { /* Existing acquired record still fails closed on the next Gateway. */ }
207
+ throw error;
208
+ }
209
+ }
210
+
211
+ async release(sessionRef: string): Promise<SessionLeaseSnapshot> {
212
+ const active = this.#active.get(sessionRef);
213
+ if (!active) {
214
+ const current = this.#leases.inspect(sessionRef);
215
+ if (current.state === "released") return current;
216
+ throw new Error(current.state === "recovery-required" ? "session-recovery-required" : "session-owner-conflict");
217
+ }
218
+ if (active.operationRef) throw new Error("session-runtime-busy");
219
+ if (active.settling && active.completion) await active.completion;
220
+ if (active.operationRef || active.settling) throw new Error("session-runtime-busy");
221
+ this.#active.delete(sessionRef);
222
+ active.unsubscribeApproval?.(); active.unbindApproval?.();
223
+ try { await active.runtime.dispose(); }
224
+ catch (error) {
225
+ try { this.#leases.requireRecovery(sessionRef, active.lease.ownerEpoch!, this.#gatewayInstanceRef,
226
+ active.lease.runtimeInstanceRef!, "session-runtime-dispose-failed"); } catch { /* remains fail closed */ }
227
+ throw error;
228
+ }
229
+ const released = this.#leases.release(sessionRef, active.lease.ownerEpoch!, this.#gatewayInstanceRef, active.lease.runtimeInstanceRef!);
230
+ const transient = this.#created.get(sessionRef);
231
+ if (transient && !(await this.#listSessions()).some((info) => info.path === transient.path)) this.#created.delete(sessionRef);
232
+ return released;
233
+ }
234
+
235
+ ownership(sessionRef: string): SessionOwnerProjection {
236
+ const lease = this.#leases.inspect(sessionRef);
237
+ const active = this.#active.get(sessionRef);
238
+ if (lease.state === "released") return {
239
+ state: "offline", liveState: "offline", composerAvailable: true, needsAttention: false,
240
+ owner: { kind: "none", ownerEpoch: null, gatewayInstanceRef: null, runtimeInstanceRef: null, continuity: "released" },
241
+ reasonCode: null
242
+ };
243
+ if (lease.state === "gateway-owned" && active && active.lease.ownerEpoch === lease.ownerEpoch
244
+ && lease.gatewayInstanceRef === this.#gatewayInstanceRef) return {
245
+ state: "gateway-owned", liveState: active.approvalWaiting ? "waiting-approval" : active.operationRef ? "running" : "idle",
246
+ composerAvailable: true, needsAttention: active.approvalWaiting,
247
+ owner: { kind: "gateway", ownerEpoch: lease.ownerEpoch!, gatewayInstanceRef: lease.gatewayInstanceRef!,
248
+ runtimeInstanceRef: lease.runtimeInstanceRef!, continuity: "exact" }, reasonCode: null
249
+ };
250
+ if (lease.state === "unavailable") return {
251
+ state: "recovery-required", liveState: "uncertain", composerAvailable: false, needsAttention: true,
252
+ owner: { kind: "none", ownerEpoch: null, gatewayInstanceRef: null, runtimeInstanceRef: null, continuity: "unknown" },
253
+ reasonCode: lease.reasonCode ?? "session-lease-unavailable"
254
+ };
255
+ if (lease.state === "terminal-owned") return {
256
+ state: "terminal-owned", liveState: "uncertain", composerAvailable: false, needsAttention: false,
257
+ owner: { kind: "terminal", ownerEpoch: lease.ownerEpoch!,
258
+ gatewayInstanceRef: `terminal_${createHmac("sha256", this.#key).update(lease.gatewayInstanceRef!).digest("base64url").slice(0, 43)}`,
259
+ runtimeInstanceRef: lease.runtimeInstanceRef!, continuity: "exact" },
260
+ reasonCode: "terminal-owner-active"
261
+ };
262
+ return {
263
+ state: "recovery-required", liveState: "uncertain", composerAvailable: false, needsAttention: true,
264
+ owner: { kind: "gateway", ownerEpoch: lease.ownerEpoch!, gatewayInstanceRef: lease.gatewayInstanceRef!,
265
+ runtimeInstanceRef: lease.runtimeInstanceRef!, continuity: "uncertain" },
266
+ reasonCode: lease.reasonCode ?? "session-owner-continuity-unknown"
267
+ };
268
+ }
269
+
270
+ async send(sessionRef: string, payload: { delivery: "new-operation" | "follow-up" | "steer"; message: string;
271
+ expectedOperationRef: string | null }, sessionRevision: string): Promise<{ resultCode: "started" | "queued" | "steered"; operationRef: string }> {
272
+ await this.acquire(sessionRef);
273
+ const active = this.#active.get(sessionRef), session = active?.runtime.session;
274
+ if (!active || !session) throw new Error("session-runtime-unavailable");
275
+ if (payload.delivery !== "new-operation") {
276
+ if (!active.operationRef || active.operationRef !== payload.expectedOperationRef || !session.isStreaming) throw new Error("session-operation-conflict");
277
+ if (payload.delivery === "follow-up") { await session.followUp(payload.message); return { resultCode: "queued", operationRef: active.operationRef }; }
278
+ await session.steer(payload.message); return { resultCode: "steered", operationRef: active.operationRef };
279
+ }
280
+ if (payload.expectedOperationRef !== null || active.operationRef || active.settling || !session.isIdle) throw new Error("session-operation-conflict");
281
+ const operationRef = `operation_${randomBytes(24).toString("base64url")}`;
282
+ const stream = new GatewaySessionStream({ sessionRef, operationRef, events: this.#events });
283
+ active.operationRef = operationRef;
284
+ active.unsubscribe = session.subscribe((event: unknown) => stream.observe(event));
285
+ this.#events.publish("runtime.changed", { sessionRef, sessionRevision, liveState: "running", operationRef, reasonCode: null });
286
+ const created = this.#created.get(sessionRef);
287
+ if (created && created.firstMessage === "(no messages)") {
288
+ created.firstMessage = payload.message; created.allMessagesText = payload.message; created.messageCount = 1; created.modified = new Date();
289
+ }
290
+ let prompt: Promise<void>;
291
+ try { prompt = session.prompt(payload.message); }
292
+ catch (error) { await this.#finishOperation(sessionRef, operationRef, stream); throw error; }
293
+ active.completion = prompt.then(() => this.#finishOperation(sessionRef, operationRef, stream),
294
+ () => this.#finishOperation(sessionRef, operationRef, stream));
295
+ await Promise.race([
296
+ stream.started(),
297
+ prompt.then(() => { throw new Error("session-operation-start-unobserved"); }, (error: unknown) => { throw error; })
298
+ ]);
299
+ return { resultCode: "started", operationRef };
300
+ }
301
+
302
+ async abort(sessionRef: string, operationRef: string, clearQueued: boolean): Promise<void> {
303
+ const active = this.#active.get(sessionRef), session = active?.runtime.session;
304
+ if (!active || !session || active.operationRef !== operationRef) throw new Error("session-operation-conflict");
305
+ const completion = active.completion;
306
+ await session.abort();
307
+ if (clearQueued) session.clearQueue();
308
+ await completion;
309
+ }
310
+
311
+ #bindApproval(sessionRef: string, active: ActiveRuntime): void {
312
+ const authority = (): ApprovalAuthority => {
313
+ if (!active.operationRef || active.lease.state !== "gateway-owned" || !active.lease.revision) return null;
314
+ const task = activeSessionTask(active.info.cwd, active.info.id);
315
+ const synthetic = (namespace: string) => `${namespace}_${createHmac("sha256", this.#key)
316
+ .update(`${sessionRef}\0${namespace}`).digest("base64url").slice(0, 43)}`;
317
+ const control = task ? inspectTaskControlState(active.info.cwd, task) : null;
318
+ return {
319
+ identity: {
320
+ projectRef: projectRefForCwd(this.#key, active.info.cwd), runtimeInstanceId: active.lease.runtimeInstanceRef!, sessionRef,
321
+ taskId: task?.taskId ?? synthetic("task"), taskRunId: task?.taskRunId ?? synthetic("task_run"),
322
+ agentOperationId: active.operationRef
323
+ },
324
+ revisions: {
325
+ runtimeRevision: active.lease.revision,
326
+ taskRevision: task ? webUiTaskRevision(task) : synthetic("task_rev"),
327
+ controlRevision: control?.controlRevision ?? synthetic("control_rev")
328
+ },
329
+ taskState: control?.state === "terminal" ? "terminal" : "active"
330
+ };
331
+ };
332
+ active.unbindApproval = piApprovalBroker.bind({ cwd: active.info.cwd, rawSessionId: active.info.id,
333
+ runtimeInstanceId: active.lease.runtimeInstanceRef!, authority });
334
+ active.unsubscribeApproval = piApprovalBroker.subscribe(active.info.cwd, active.info.id, (event: ApprovalBrokerEvent) => {
335
+ const projection = piApprovalBroker.projection(active.info.cwd, active.info.id);
336
+ active.approvalWaiting = projection.summary.state === "waiting";
337
+ void this.#publishApprovalState(sessionRef, active, event);
338
+ });
339
+ }
340
+
341
+ async #publishApprovalState(sessionRef: string, active: ActiveRuntime, event: ApprovalBrokerEvent): Promise<void> {
342
+ try {
343
+ const projection = await this.#readProjection?.(sessionRef);
344
+ if (!projection || this.#active.get(sessionRef) !== active) return;
345
+ this.#events.publish("runtime.changed", { sessionRef, sessionRevision: projection.sessionRevision,
346
+ liveState: active.approvalWaiting ? "waiting-approval" : active.operationRef ? "running" : projection.liveState,
347
+ operationRef: active.operationRef, reasonCode: `approval-${event.kind}` });
348
+ } catch { /* approval truth remains available through the canonical inspection route */ }
349
+ }
350
+
351
+ async setModel(sessionRef: string, modelRef: string): Promise<"model-changed" | "no-change"> {
352
+ await this.acquire(sessionRef);
353
+ const active = this.#active.get(sessionRef), session = active?.runtime.session;
354
+ if (!active || !session) throw new Error("session-runtime-unavailable");
355
+ if (active.operationRef || !session.isIdle) throw new Error("session-runtime-busy");
356
+ const current = session.model && typeof session.model === "object"
357
+ ? webUiModelRef(String(session.model.provider ?? ""), String(session.model.id ?? session.model.modelId ?? "")) : null;
358
+ if (current === modelRef) return "no-change";
359
+ const models = session.modelRuntime?.getAvailableSnapshot?.() ?? [];
360
+ const model = models.find((value: any) => webUiModelRef(String(value.provider ?? ""), String(value.id ?? "")) === modelRef);
361
+ if (!model) throw new Error("session-model-unavailable");
362
+ await session.setModel(model);
363
+ this.#touchCreated(sessionRef);
364
+ return "model-changed";
365
+ }
366
+
367
+ async setThinking(sessionRef: string, thinkingLevel: string): Promise<"thinking-changed" | "no-change"> {
368
+ await this.acquire(sessionRef);
369
+ const active = this.#active.get(sessionRef), session = active?.runtime.session;
370
+ if (!active || !session) throw new Error("session-runtime-unavailable");
371
+ if (active.operationRef || !session.isIdle) throw new Error("session-runtime-busy");
372
+ const current = String(session.thinkingLevel ?? session.getThinkingLevel?.() ?? "unknown");
373
+ if (current === thinkingLevel) return "no-change";
374
+ session.setThinkingLevel(thinkingLevel);
375
+ this.#touchCreated(sessionRef);
376
+ return "thinking-changed";
377
+ }
378
+
379
+ async setPermission(sessionRef: string, permissionMode: "read-only" | "workspace-write" | "trusted-full-access"):
380
+ Promise<"permission-changed"> {
381
+ await this.acquire(sessionRef);
382
+ const active = this.#active.get(sessionRef), session = active?.runtime.session;
383
+ if (!active || !session) throw new Error("session-runtime-unavailable");
384
+ if (active.operationRef || !session.isIdle) throw new Error("session-runtime-busy");
385
+ const before = Array.isArray(session.messages) ? session.messages.length : 0;
386
+ await session.prompt(`/permission ${permissionMode}`);
387
+ const messages = Array.isArray(session.messages) ? session.messages.slice(before) : [];
388
+ const observed = messages.reverse().find((message: any) => message?.role === "custom"
389
+ && message.customType === "piagent-permission-profile");
390
+ const profile = observed?.details?.permissionProfile;
391
+ if (!profile || profile.mode !== permissionMode || profile.warning) throw new Error("session-permission-unavailable");
392
+ this.#touchCreated(sessionRef);
393
+ return "permission-changed";
394
+ }
395
+
396
+ approvalProjection(sessionRef: string): { revision: string | null; summary: Record<string, unknown> } {
397
+ const active = this.#active.get(sessionRef);
398
+ if (!active) return { revision: null, summary: { state: "unknown", pending: [], recent: [],
399
+ health: { state: "unavailable", reasonCode: "approval-broker-unavailable", message: "Approval broker is unavailable" } } };
400
+ return piApprovalBroker.projection(active.info.cwd, active.info.id);
401
+ }
402
+
403
+ approvalDetail(sessionRef: string, approvalRef: string): unknown | null {
404
+ const active = this.#active.get(sessionRef);
405
+ return active ? piApprovalBroker.detail(active.info.cwd, active.info.id, approvalRef) : null;
406
+ }
407
+
408
+ async decideApproval(approvalRef: string, decision: unknown): Promise<unknown> {
409
+ for (const active of this.#active.values()) {
410
+ if (piApprovalBroker.detail(active.info.cwd, active.info.id, approvalRef)) {
411
+ return await piApprovalBroker.decide(active.info.cwd, active.info.id, approvalRef, decision);
412
+ }
413
+ }
414
+ throw new Error("approval-not-pending");
415
+ }
416
+
417
+ async rename(sessionRef: string, title: string): Promise<"renamed" | "no-change"> {
418
+ await this.acquire(sessionRef);
419
+ const active = this.#active.get(sessionRef), session = active?.runtime.session;
420
+ if (!active || !session?.sessionManager) throw new Error("session-runtime-unavailable");
421
+ if (active.operationRef || !session.isIdle) throw new Error("session-runtime-busy");
422
+ const current = String(session.sessionManager.getSessionName?.() ?? "").trim();
423
+ if (current === title.trim()) return "no-change";
424
+ session.sessionManager.appendSessionInfo(title);
425
+ this.#touchCreated(sessionRef);
426
+ return "renamed";
427
+ }
428
+
429
+ async fork(sessionRef: string, entryRef: string | null, title: string | null): Promise<string> {
430
+ if (!this.#host) throw new Error("session-fork-unavailable");
431
+ await this.acquire(sessionRef);
432
+ const active = this.#active.get(sessionRef), manager = active?.runtime.session?.sessionManager;
433
+ if (!active || !manager) throw new Error("session-runtime-unavailable");
434
+ if (active.operationRef || !active.runtime.session?.isIdle) throw new Error("session-runtime-busy");
435
+ const source = (await this.listSessions()).find((value) => sessionRefForPath(this.#key, value.path) === sessionRef);
436
+ if (!source) throw new Error("session-not-found");
437
+ let forkManager: any;
438
+ if (entryRef) {
439
+ const file = manager.createBranchedSession(entryRef);
440
+ if (!file) throw new Error("session-fork-unavailable");
441
+ forkManager = this.#host.SessionManager.open(file);
442
+ } else {
443
+ forkManager = this.#host.SessionManager.forkFrom(source.path, source.cwd);
444
+ }
445
+ if (title) forkManager.appendSessionInfo(title);
446
+ const file = forkManager.getSessionFile(), id = forkManager.getSessionId?.();
447
+ if (!file || typeof id !== "string") throw new Error("session-fork-unavailable");
448
+ const persisted = (await this.#listSessions()).find((value) => value.path === file);
449
+ const now = new Date(), created = persisted ?? { ...source, path: file, id, name: title ?? source.name,
450
+ created: now, modified: now };
451
+ const forkRef = sessionRefForPath(this.#key, file);
452
+ this.#created.set(forkRef, created);
453
+ return forkRef;
454
+ }
455
+
456
+ #touchCreated(sessionRef: string): void {
457
+ const created = this.#created.get(sessionRef);
458
+ if (created) created.modified = new Date();
459
+ }
460
+
461
+ async #finishOperation(sessionRef: string, operationRef: string, stream: GatewaySessionStream): Promise<void> {
462
+ const active = this.#active.get(sessionRef);
463
+ if (!active || active.operationRef !== operationRef) return;
464
+ active.unsubscribe?.(); active.unsubscribe = null; active.operationRef = null; active.settling = true;
465
+ try {
466
+ const projection = await this.#readProjection?.(sessionRef);
467
+ if (!projection) return;
468
+ stream.complete(projection.sessionRevision);
469
+ this.#events.publish("runtime.changed", { sessionRef, sessionRevision: projection.sessionRevision,
470
+ liveState: projection.liveState, operationRef: null, reasonCode: null });
471
+ } catch { /* Canonical refresh failure cannot be replaced by an invented revision. */ }
472
+ finally {
473
+ if (this.#active.get(sessionRef) === active) { active.settling = false; active.completion = null; }
474
+ }
475
+ }
476
+
477
+ async close(): Promise<void> {
478
+ if (this.#closed) return;
479
+ this.#closed = true;
480
+ await Promise.allSettled([...this.#opening.values()]);
481
+ await Promise.allSettled([...this.#active.entries()].filter(([, active]) => active.operationRef)
482
+ .map(([sessionRef, active]) => this.abort(sessionRef, active.operationRef!, true)));
483
+ await Promise.allSettled([...this.#active.keys()].map((sessionRef) => this.release(sessionRef)));
484
+ }
485
+ }
@@ -0,0 +1,8 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ export const GATEWAY_RUNTIME_UI_MARKER = Symbol.for("piagent.webui.gateway-runtime-ui.v1");
4
+
5
+ export function isGatewayRuntimeContext(ctx: Pick<ExtensionContext, "ui">): boolean {
6
+ try { return Reflect.get(ctx.ui as object, GATEWAY_RUNTIME_UI_MARKER) === true; }
7
+ catch { return false; }
8
+ }
@@ -0,0 +1,117 @@
1
+ import { createHash, createHmac, randomBytes } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ const DESCRIPTOR_MAX_BYTES = 16 * 1024; // Shared by Gateway and terminal ownership adapters.
7
+
8
+ export type GatewayDescriptor = {
9
+ version: "piagent-gateway-descriptor-v1";
10
+ gatewayInstanceRef: string;
11
+ pid: number;
12
+ startedAt: string;
13
+ origin: string;
14
+ controlSocket: string;
15
+ profileRef: string;
16
+ };
17
+
18
+ export type GatewayProfileState = {
19
+ agentDir: string;
20
+ root: string;
21
+ descriptorFile: string;
22
+ controlSocket: string;
23
+ catalogKeyFile: string;
24
+ };
25
+
26
+ function assertOwnerOnlyDirectory(target: string): void {
27
+ const stat = fs.lstatSync(target);
28
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("gateway-state-root-invalid");
29
+ fs.chmodSync(target, 0o700);
30
+ }
31
+
32
+ function shortControlSocket(agentDir: string): string {
33
+ if (process.platform === "win32") {
34
+ const digest = createHash("sha256").update(agentDir).digest("hex").slice(0, 24);
35
+ return `\\\\.\\pipe\\piagent-${digest}`;
36
+ }
37
+ const uid = typeof process.getuid === "function" ? process.getuid() : "user";
38
+ const socketRoot = path.join("/tmp", `piagent-${uid}`);
39
+ fs.mkdirSync(socketRoot, { recursive: true, mode: 0o700 });
40
+ const stat = fs.lstatSync(socketRoot);
41
+ if (!stat.isDirectory() || stat.isSymbolicLink()
42
+ || (typeof process.getuid === "function" && stat.uid !== process.getuid())) throw new Error("gateway-control-root-invalid");
43
+ fs.chmodSync(socketRoot, 0o700);
44
+ const digest = createHash("sha256").update(agentDir).digest("hex").slice(0, 32);
45
+ return path.join(socketRoot, `${digest}.sock`);
46
+ }
47
+
48
+ export function gatewayProfileState(agentDirInput?: string): GatewayProfileState {
49
+ const agentDir = path.resolve(agentDirInput || process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent"));
50
+ fs.mkdirSync(agentDir, { recursive: true, mode: 0o700 });
51
+ const agentStat = fs.lstatSync(agentDir);
52
+ if (!agentStat.isDirectory() || agentStat.isSymbolicLink()
53
+ || (typeof process.getuid === "function" && agentStat.uid !== process.getuid())) throw new Error("gateway-agent-dir-invalid");
54
+ const canonicalAgentDir = fs.realpathSync(agentDir);
55
+ const root = path.join(canonicalAgentDir, "piagent-gateway");
56
+ fs.mkdirSync(root, { recursive: true, mode: 0o700 });
57
+ assertOwnerOnlyDirectory(root);
58
+ return {
59
+ agentDir: canonicalAgentDir,
60
+ root,
61
+ descriptorFile: path.join(root, "descriptor.json"),
62
+ controlSocket: shortControlSocket(canonicalAgentDir),
63
+ catalogKeyFile: path.join(root, "catalog.key")
64
+ };
65
+ }
66
+
67
+ export function profileRef(state: GatewayProfileState, key: Buffer): string {
68
+ return `profile_${createHmac("sha256", key).update(state.agentDir).digest("base64url").slice(0, 43)}`;
69
+ }
70
+
71
+ export function readOrCreateCatalogKey(state: GatewayProfileState): Buffer {
72
+ try {
73
+ const descriptor = fs.openSync(state.catalogKeyFile, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o600);
74
+ try { fs.writeFileSync(descriptor, randomBytes(32)); fs.fsyncSync(descriptor); }
75
+ finally { fs.closeSync(descriptor); }
76
+ } catch (error) {
77
+ if (!(error && typeof error === "object" && "code" in error && error.code === "EEXIST")) throw error;
78
+ }
79
+ const stat = fs.lstatSync(state.catalogKeyFile);
80
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size !== 32) throw new Error("gateway-catalog-key-invalid");
81
+ fs.chmodSync(state.catalogKeyFile, 0o600);
82
+ const descriptor = fs.openSync(state.catalogKeyFile, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
83
+ try {
84
+ const opened = fs.fstatSync(descriptor);
85
+ if (opened.dev !== stat.dev || opened.ino !== stat.ino || opened.size !== 32) throw new Error("gateway-catalog-key-race");
86
+ return fs.readFileSync(descriptor);
87
+ } finally { fs.closeSync(descriptor); }
88
+ }
89
+
90
+ export function writeGatewayDescriptor(state: GatewayProfileState, value: GatewayDescriptor): void {
91
+ const temporary = path.join(state.root, `.descriptor-${process.pid}-${randomBytes(8).toString("hex")}.tmp`);
92
+ const body = Buffer.from(`${JSON.stringify(value)}\n`);
93
+ if (body.length > DESCRIPTOR_MAX_BYTES) throw new Error("gateway-descriptor-limit");
94
+ const descriptor = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o600);
95
+ try { fs.writeFileSync(descriptor, body); fs.fsyncSync(descriptor); }
96
+ finally { fs.closeSync(descriptor); }
97
+ fs.renameSync(temporary, state.descriptorFile);
98
+ fs.chmodSync(state.descriptorFile, 0o600);
99
+ }
100
+
101
+ export function readGatewayDescriptor(state: GatewayProfileState): GatewayDescriptor | null {
102
+ try {
103
+ const stat = fs.lstatSync(state.descriptorFile);
104
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > DESCRIPTOR_MAX_BYTES) return null;
105
+ const value = JSON.parse(fs.readFileSync(state.descriptorFile, "utf8")) as Partial<GatewayDescriptor>;
106
+ if (value.version !== "piagent-gateway-descriptor-v1" || typeof value.gatewayInstanceRef !== "string"
107
+ || typeof value.pid !== "number" || typeof value.startedAt !== "string" || typeof value.origin !== "string"
108
+ || value.controlSocket !== state.controlSocket || typeof value.profileRef !== "string") return null;
109
+ return value as GatewayDescriptor;
110
+ } catch { return null; }
111
+ }
112
+
113
+ export function removeGatewayDescriptor(state: GatewayProfileState, instanceRef: string): void {
114
+ const current = readGatewayDescriptor(state);
115
+ if (current?.gatewayInstanceRef !== instanceRef) return;
116
+ try { fs.unlinkSync(state.descriptorFile); } catch { /* already removed */ }
117
+ }