@piagent/platform 1.3.1 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/README.md +10 -4
  3. package/README.vi.md +27 -0
  4. package/architecture/layers.json +96 -1
  5. package/docs/README.md +1 -0
  6. package/docs/architecture.md +1 -0
  7. package/docs/authority-manifest.md +1 -0
  8. package/docs/auto-delegation-policy.md +1 -0
  9. package/docs/capability-packs.md +1 -0
  10. package/docs/command-reference-vietnamese.md +5 -4
  11. package/docs/context-engine.md +1 -0
  12. package/docs/context-window-policy.md +1 -0
  13. package/docs/distribution-standard.md +8 -7
  14. package/docs/en/architecture.md +13 -0
  15. package/docs/herdr-workflow.md +1 -0
  16. package/docs/languages.json +20 -4
  17. package/docs/mcp-and-tools.md +125 -9
  18. package/docs/memory-policy.md +1 -0
  19. package/docs/model-options.md +1 -0
  20. package/docs/oauth-providers.md +1 -0
  21. package/docs/operator-manual-vietnamese.md +15 -12
  22. package/docs/package-architecture-notes.md +1 -0
  23. package/docs/project-adapters.md +1 -0
  24. package/docs/project-onboarding.md +1 -0
  25. package/docs/publishing-for-teams.md +37 -0
  26. package/docs/quality-benchmark.md +1 -0
  27. package/docs/quickstart-vietnamese.md +3 -2
  28. package/docs/release-install-policy.md +22 -8
  29. package/docs/runtime-harness-standard.md +2 -1
  30. package/docs/runtime-policy-design.md +1 -0
  31. package/docs/runtime-quality-baseline.md +1 -0
  32. package/docs/security-redaction-benchmark.md +1 -0
  33. package/docs/security-threat-model.md +2 -1
  34. package/docs/subagent-orchestration-capabilities.md +1 -0
  35. package/docs/subagents-and-multiagent.md +1 -0
  36. package/docs/task-implementation-contract.md +12 -4
  37. package/docs/task-lifecycle-vietnamese.md +1 -0
  38. package/docs/team-onboarding.md +3 -2
  39. package/docs/third-party-neutrality.md +26 -0
  40. package/docs/usage-observability.md +71 -0
  41. package/docs/vercel-docs-site.md +1 -0
  42. package/docs/vi/architecture.md +13 -0
  43. package/docs/workflow-recipes.md +1 -0
  44. package/docs-site/architecture.html +5 -1
  45. package/docs-site/benchmark.html +5 -1
  46. package/docs-site/commands.html +5 -1
  47. package/docs-site/content/en/index.html +49 -0
  48. package/docs-site/content/en/runtime.html +13 -0
  49. package/docs-site/content/index.html +49 -0
  50. package/docs-site/content/runtime.html +28 -0
  51. package/docs-site/context-engine.html +5 -1
  52. package/docs-site/en/architecture.html +5 -1
  53. package/docs-site/en/benchmark.html +5 -1
  54. package/docs-site/en/commands.html +5 -1
  55. package/docs-site/en/context-engine.html +5 -1
  56. package/docs-site/en/index.html +54 -1
  57. package/docs-site/en/mcp-approval.html +5 -1
  58. package/docs-site/en/mcp-auth.html +5 -1
  59. package/docs-site/en/mcp-servers.html +5 -1
  60. package/docs-site/en/mcp.html +5 -1
  61. package/docs-site/en/permissions.html +5 -1
  62. package/docs-site/en/profiles.html +5 -1
  63. package/docs-site/en/quickstart.html +5 -1
  64. package/docs-site/en/runtime.html +18 -1
  65. package/docs-site/en/scope.html +5 -1
  66. package/docs-site/en/security.html +5 -1
  67. package/docs-site/en/team.html +5 -1
  68. package/docs-site/en/whats-new.html +5 -1
  69. package/docs-site/en/workflows.html +5 -1
  70. package/docs-site/index.html +54 -1
  71. package/docs-site/mcp-approval.html +5 -1
  72. package/docs-site/mcp-auth.html +5 -1
  73. package/docs-site/mcp-servers.html +5 -1
  74. package/docs-site/mcp.html +5 -1
  75. package/docs-site/permissions.html +5 -1
  76. package/docs-site/profiles.html +5 -1
  77. package/docs-site/quickstart.html +5 -1
  78. package/docs-site/runtime.html +33 -1
  79. package/docs-site/scope.html +5 -1
  80. package/docs-site/security.html +5 -1
  81. package/docs-site/team.html +5 -1
  82. package/docs-site/whats-new.html +5 -1
  83. package/docs-site/workflows.html +5 -1
  84. package/evals/fixtures/mutation-provenance-record.invalid.json +17 -0
  85. package/evals/fixtures/mutation-provenance-record.valid.json +27 -0
  86. package/evals/fixtures/piagent-webui/approval-v1.invalid.json +54 -0
  87. package/evals/fixtures/piagent-webui/approval-v1.valid.json +58 -0
  88. package/evals/fixtures/piagent-webui/attachment-v1.invalid.json +25 -0
  89. package/evals/fixtures/piagent-webui/attachment-v1.valid.json +25 -0
  90. package/evals/fixtures/piagent-webui/capabilities-v1.invalid.json +51 -0
  91. package/evals/fixtures/piagent-webui/capabilities-v1.valid.json +122 -0
  92. package/evals/fixtures/piagent-webui/commit-summary-v1.invalid.json +10 -0
  93. package/evals/fixtures/piagent-webui/commit-summary-v1.valid.json +25 -0
  94. package/evals/fixtures/piagent-webui/control-command-v1.invalid.json +37 -0
  95. package/evals/fixtures/piagent-webui/control-command-v1.valid.json +35 -0
  96. package/evals/fixtures/piagent-webui/diff-v1.invalid.json +13 -0
  97. package/evals/fixtures/piagent-webui/diff-v1.valid.json +141 -0
  98. package/evals/fixtures/piagent-webui/gateway-capabilities-v1.invalid.json +35 -0
  99. package/evals/fixtures/piagent-webui/gateway-capabilities-v1.valid.json +35 -0
  100. package/evals/fixtures/piagent-webui/gateway-protocol-v1.invalid.json +14 -0
  101. package/evals/fixtures/piagent-webui/gateway-protocol-v1.valid.json +14 -0
  102. package/evals/fixtures/piagent-webui/handoff-history-v1.invalid.json +16 -0
  103. package/evals/fixtures/piagent-webui/handoff-history-v1.valid.json +16 -0
  104. package/evals/fixtures/piagent-webui/model-catalog-v1.invalid.json +31 -0
  105. package/evals/fixtures/piagent-webui/model-catalog-v1.valid.json +43 -0
  106. package/evals/fixtures/piagent-webui/queue-v1.invalid.json +29 -0
  107. package/evals/fixtures/piagent-webui/queue-v1.valid.json +44 -0
  108. package/evals/fixtures/piagent-webui/recovery-history-v1.invalid.json +17 -0
  109. package/evals/fixtures/piagent-webui/recovery-history-v1.valid.json +20 -0
  110. package/evals/fixtures/piagent-webui/release-monitor-v1.invalid.json +12 -0
  111. package/evals/fixtures/piagent-webui/release-monitor-v1.valid.json +29 -0
  112. package/evals/fixtures/piagent-webui/review-state-v1.invalid.json +25 -0
  113. package/evals/fixtures/piagent-webui/review-state-v1.valid.json +38 -0
  114. package/evals/fixtures/piagent-webui/runtime-event-v2.invalid.json +49 -0
  115. package/evals/fixtures/piagent-webui/runtime-event-v2.valid.json +49 -0
  116. package/evals/fixtures/piagent-webui/session-catalog-v1.invalid.json +17 -0
  117. package/evals/fixtures/piagent-webui/session-catalog-v1.valid.json +58 -0
  118. package/evals/fixtures/piagent-webui/session-command-v1.invalid.json +19 -0
  119. package/evals/fixtures/piagent-webui/session-command-v1.valid.json +19 -0
  120. package/evals/fixtures/piagent-webui/snapshot-v1.invalid.json +5 -0
  121. package/evals/fixtures/piagent-webui/snapshot-v1.valid.json +191 -0
  122. package/evals/fixtures/piagent-webui/source-change-v1.invalid.json +57 -0
  123. package/evals/fixtures/piagent-webui/source-change-v1.valid.json +98 -0
  124. package/evals/fixtures/piagent-webui/source-mutation-v1.invalid.json +19 -0
  125. package/evals/fixtures/piagent-webui/source-mutation-v1.valid.json +38 -0
  126. package/evals/fixtures/piagent-webui/source-revert-v1.invalid.json +20 -0
  127. package/evals/fixtures/piagent-webui/source-revert-v1.valid.json +62 -0
  128. package/evals/fixtures/piagent-webui/subagent-tree-v1.invalid.json +26 -0
  129. package/evals/fixtures/piagent-webui/subagent-tree-v1.valid.json +39 -0
  130. package/evals/fixtures/piagent-webui/task-index-v1.invalid.json +15 -0
  131. package/evals/fixtures/piagent-webui/task-index-v1.valid.json +15 -0
  132. package/evals/fixtures/piagent-webui/task-timeline-v1.invalid.json +14 -0
  133. package/evals/fixtures/piagent-webui/task-timeline-v1.valid.json +16 -0
  134. package/evals/fixtures/piagent-webui/transcript-v1.invalid.json +35 -0
  135. package/evals/fixtures/piagent-webui/transcript-v1.valid.json +55 -0
  136. package/evals/fixtures/task-baseline-manifest.invalid.json +16 -0
  137. package/evals/fixtures/task-baseline-manifest.valid.json +41 -0
  138. package/evals/fixtures/verifier-file-snapshot.invalid.json +21 -0
  139. package/evals/fixtures/verifier-file-snapshot.valid.json +21 -0
  140. package/package.json +43 -4
  141. package/packages/piagent-core/README.md +3 -2
  142. package/packages/piagent-core/benchmark/benchmark-candidate.d.ts +1 -0
  143. package/packages/piagent-core/benchmark/benchmark-forensics.d.ts +1 -0
  144. package/packages/piagent-core/benchmark/benchmark-ledger.d.ts +4 -0
  145. package/packages/piagent-core/benchmark/benchmark-record-validation.d.ts +1 -0
  146. package/packages/piagent-core/benchmark/benchmark-runtime.d.ts +1 -0
  147. package/packages/piagent-core/capabilities/capability-core.js +2 -2
  148. package/packages/piagent-core/capabilities/capability-verification-cache.js +17 -7
  149. package/packages/piagent-core/capabilities/runtime-integrity.js +190 -0
  150. package/packages/piagent-core/extensions/piagent-guard.ts +80 -272
  151. package/packages/piagent-core/extensions/redaction-core.d.ts +2 -0
  152. package/packages/piagent-core/extensions/runtime-evidence.js +12 -1
  153. package/packages/piagent-core/extensions/shell-reach.ts +250 -0
  154. package/packages/piagent-core/extensions/task-journal.js +30 -1
  155. package/packages/piagent-core/extensions/task-state.js +33 -36
  156. package/packages/piagent-core/mcp/mcp-command-actions.js +1 -1
  157. package/packages/piagent-core/mcp/mcp-server-catalog.js +139 -9
  158. package/packages/piagent-core/package.json +1 -1
  159. package/packages/piagent-core/runtime/hooks/session-start-hook.ts +2 -2
  160. package/packages/piagent-core/runtime/hooks/tool-call-hook.ts +12 -2
  161. package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +20 -19
  162. package/packages/piagent-core/runtime/hooks/tool-result-metadata.ts +13 -0
  163. package/packages/piagent-core/runtime/input/chat-images.ts +2 -2
  164. package/packages/piagent-core/runtime/inspection/activity-event-adapter.ts +94 -0
  165. package/packages/piagent-core/runtime/inspection/approval-broker.ts +318 -0
  166. package/packages/piagent-core/runtime/inspection/commit-summary-projection.ts +59 -0
  167. package/packages/piagent-core/runtime/inspection/context-telemetry-inspection.ts +37 -0
  168. package/packages/piagent-core/runtime/inspection/criteria-links.ts +295 -0
  169. package/packages/piagent-core/runtime/inspection/diff-projection.ts +499 -0
  170. package/packages/piagent-core/runtime/inspection/git-filter-safety.ts +38 -0
  171. package/packages/piagent-core/runtime/inspection/git-numstat-adapter.ts +71 -0
  172. package/packages/piagent-core/runtime/inspection/git-status-adapter.ts +493 -0
  173. package/packages/piagent-core/runtime/inspection/git-tree-adapter.ts +108 -0
  174. package/packages/piagent-core/runtime/inspection/mcp-control.ts +10 -0
  175. package/packages/piagent-core/runtime/inspection/mutation-provenance-contract.ts +131 -0
  176. package/packages/piagent-core/runtime/inspection/mutation-provenance-recorder.ts +48 -0
  177. package/packages/piagent-core/runtime/inspection/mutation-provenance-store.ts +161 -0
  178. package/packages/piagent-core/runtime/inspection/project-runtime-inspection.ts +36 -0
  179. package/packages/piagent-core/runtime/inspection/review-state-contract.ts +108 -0
  180. package/packages/piagent-core/runtime/inspection/review-state-projection.ts +93 -0
  181. package/packages/piagent-core/runtime/inspection/review-state-store.ts +112 -0
  182. package/packages/piagent-core/runtime/inspection/runtime-event-store.ts +262 -0
  183. package/packages/piagent-core/runtime/inspection/same-process-bridge-proof.ts +103 -0
  184. package/packages/piagent-core/runtime/inspection/source-change-projection.ts +471 -0
  185. package/packages/piagent-core/runtime/inspection/source-evidence-contract.ts +139 -0
  186. package/packages/piagent-core/runtime/inspection/source-evidence-store.ts +315 -0
  187. package/packages/piagent-core/runtime/inspection/source-handoff-store.ts +70 -0
  188. package/packages/piagent-core/runtime/inspection/source-inspection-plan.ts +34 -0
  189. package/packages/piagent-core/runtime/inspection/source-mutation-projection.ts +164 -0
  190. package/packages/piagent-core/runtime/inspection/source-mutation-store.ts +168 -0
  191. package/packages/piagent-core/runtime/inspection/source-open-target.ts +38 -0
  192. package/packages/piagent-core/runtime/inspection/source-revert-projection.ts +147 -0
  193. package/packages/piagent-core/runtime/inspection/task-baseline-start-capture.ts +22 -0
  194. package/packages/piagent-core/runtime/inspection/task-compaction-history.ts +160 -0
  195. package/packages/piagent-core/runtime/inspection/task-control-journal.ts +131 -0
  196. package/packages/piagent-core/runtime/inspection/task-handoff-history.ts +95 -0
  197. package/packages/piagent-core/runtime/inspection/task-provenance-projection.ts +60 -0
  198. package/packages/piagent-core/runtime/inspection/task-recovery-timeline.ts +88 -0
  199. package/packages/piagent-core/runtime/inspection/task-run-index.ts +87 -0
  200. package/packages/piagent-core/runtime/inspection/task-source-projection.ts +401 -0
  201. package/packages/piagent-core/runtime/inspection/task-subagent-tree.ts +101 -0
  202. package/packages/piagent-core/runtime/inspection/text-diff.ts +60 -0
  203. package/packages/piagent-core/runtime/inspection/verifier-snapshot-contract.ts +114 -0
  204. package/packages/piagent-core/runtime/inspection/verifier-snapshot-store.ts +189 -0
  205. package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +320 -0
  206. package/packages/piagent-core/runtime/inspection/workspace-file-reader.ts +132 -0
  207. package/packages/piagent-core/runtime/inspection/workspace-source-projection.ts +96 -0
  208. package/packages/piagent-core/runtime/inspection/zero-turn-conformance.ts +175 -0
  209. package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +93 -6
  210. package/packages/piagent-core/runtime/policy/block-remedy.ts +139 -0
  211. package/packages/piagent-core/runtime/policy/source-index-transaction.ts +173 -0
  212. package/packages/piagent-core/runtime/policy/source-mutation-guard-binding.ts +42 -0
  213. package/packages/piagent-core/runtime/policy/source-mutation-guard.ts +184 -0
  214. package/packages/piagent-core/runtime/policy/source-worktree-transaction.ts +105 -0
  215. package/packages/piagent-core/runtime/product/activity-inspector-footer.ts +5 -3
  216. package/packages/piagent-core/runtime/product/activity-inspector.ts +85 -343
  217. package/packages/piagent-core/runtime/recovery/resume-state.ts +38 -1
  218. package/packages/piagent-core/runtime/registration/activity-inspector-command.ts +68 -14
  219. package/packages/piagent-core/runtime/registration/task-start-tool.ts +22 -27
  220. package/packages/piagent-core/runtime/session/model-authorship-state.ts +31 -3
  221. package/packages/piagent-core/runtime/session/runtime-state.ts +11 -1
  222. package/packages/piagent-core/runtime/session/token-economics.js +34 -0
  223. package/packages/piagent-core/runtime/tools/phase-tool-runtime.ts +2 -1
  224. package/packages/piagent-core/runtime/workflows/task-intake.ts +5 -0
  225. package/packages/piagent-core/security/sensitive-data.js +7 -0
  226. package/packages/piagent-webui/dist/client/assets/index-DwKCWbwd.css +1 -0
  227. package/packages/piagent-webui/dist/client/assets/index-bu7XpD9Q.js +172 -0
  228. package/packages/piagent-webui/dist/client/index.html +16 -0
  229. package/packages/piagent-webui/extension/attachment-store.ts +323 -0
  230. package/packages/piagent-webui/extension/held-message-queue.ts +311 -0
  231. package/packages/piagent-webui/extension/lifecycle-controller.ts +423 -0
  232. package/packages/piagent-webui/extension/lifecycle-event-adapter.ts +23 -0
  233. package/packages/piagent-webui/extension/piagent-webui.ts +470 -0
  234. package/packages/piagent-webui/extension/review-controller.ts +160 -0
  235. package/packages/piagent-webui/extension/same-session-bridge.ts +500 -0
  236. package/packages/piagent-webui/extension/session-options-controller.ts +281 -0
  237. package/packages/piagent-webui/extension/session-stream-adapter.ts +249 -0
  238. package/packages/piagent-webui/extension/source-mutation-controller.ts +215 -0
  239. package/packages/piagent-webui/extension/source-open-controller.ts +136 -0
  240. package/packages/piagent-webui/extension/source-revert-controller.ts +189 -0
  241. package/packages/piagent-webui/extension/terminal-session-adapter.ts +73 -0
  242. package/packages/piagent-webui/extension/vscode-handoff.ts +51 -0
  243. package/packages/piagent-webui/gateway/control-socket.ts +92 -0
  244. package/packages/piagent-webui/gateway/extension-authority.ts +47 -0
  245. package/packages/piagent-webui/gateway/gateway-events.ts +60 -0
  246. package/packages/piagent-webui/gateway/gateway-protocol-service.ts +64 -0
  247. package/packages/piagent-webui/gateway/gateway-service.ts +215 -0
  248. package/packages/piagent-webui/gateway/gateway-session-stream.ts +110 -0
  249. package/packages/piagent-webui/gateway/mcp-auth-broker.ts +158 -0
  250. package/packages/piagent-webui/gateway/native-project-picker.ts +29 -0
  251. package/packages/piagent-webui/gateway/pi-host.ts +41 -0
  252. package/packages/piagent-webui/gateway/profile-state.ts +1 -0
  253. package/packages/piagent-webui/gateway/project-registry.ts +108 -0
  254. package/packages/piagent-webui/gateway/provider-auth-broker.ts +172 -0
  255. package/packages/piagent-webui/gateway/session-catalog.ts +128 -0
  256. package/packages/piagent-webui/gateway/session-command-controller.ts +237 -0
  257. package/packages/piagent-webui/gateway/session-command-store.ts +224 -0
  258. package/packages/piagent-webui/gateway/session-inspection-registry.ts +239 -0
  259. package/packages/piagent-webui/gateway/session-lease-store.ts +1 -0
  260. package/packages/piagent-webui/gateway/session-metadata-store.ts +152 -0
  261. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +489 -0
  262. package/packages/piagent-webui/ownership/gateway-runtime-context.ts +8 -0
  263. package/packages/piagent-webui/ownership/profile-state.ts +117 -0
  264. package/packages/piagent-webui/ownership/session-lease-store.ts +329 -0
  265. package/packages/piagent-webui/ownership/session-refs.ts +8 -0
  266. package/packages/piagent-webui/package.json +33 -0
  267. package/packages/piagent-webui/server/README.md +4 -0
  268. package/packages/piagent-webui/server/benchmark-release-monitor.ts +230 -0
  269. package/packages/piagent-webui/server/core-inspection-provider.ts +403 -0
  270. package/packages/piagent-webui/server/gateway-websocket.ts +143 -0
  271. package/packages/piagent-webui/server/http-security.ts +27 -0
  272. package/packages/piagent-webui/server/ipc-read-model-client.ts +89 -0
  273. package/packages/piagent-webui/server/loopback-server.ts +368 -0
  274. package/packages/piagent-webui/server/read-model-provider.ts +37 -0
  275. package/packages/piagent-webui/server/read-only-router.ts +105 -0
  276. package/packages/piagent-webui/server/session-auth.ts +88 -0
  277. package/packages/piagent-webui/server/sidecar-main.ts +86 -0
  278. package/packages/piagent-webui/server/sse-hub.ts +76 -0
  279. package/packages/piagent-webui/server/static-bundle.ts +40 -0
  280. package/packages/piagent-webui/server/transcript-projection.ts +125 -0
  281. package/schemas/mutation-provenance-record.schema.json +100 -0
  282. package/schemas/piagent-webui/approval-v1.schema.json +654 -0
  283. package/schemas/piagent-webui/attachment-v1.schema.json +267 -0
  284. package/schemas/piagent-webui/capabilities-v1.schema.json +729 -0
  285. package/schemas/piagent-webui/catalog-v1.json +240 -0
  286. package/schemas/piagent-webui/commit-summary-v1.schema.json +63 -0
  287. package/schemas/piagent-webui/common-v1.schema.json +479 -0
  288. package/schemas/piagent-webui/control-command-v1.schema.json +1780 -0
  289. package/schemas/piagent-webui/diff-v1.schema.json +826 -0
  290. package/schemas/piagent-webui/gateway-capabilities-v1.schema.json +136 -0
  291. package/schemas/piagent-webui/gateway-protocol-v1.schema.json +456 -0
  292. package/schemas/piagent-webui/handoff-history-v1.schema.json +92 -0
  293. package/schemas/piagent-webui/model-catalog-v1.schema.json +95 -0
  294. package/schemas/piagent-webui/queue-v1.schema.json +99 -0
  295. package/schemas/piagent-webui/recovery-history-v1.schema.json +144 -0
  296. package/schemas/piagent-webui/release-monitor-v1.schema.json +173 -0
  297. package/schemas/piagent-webui/review-state-v1.schema.json +206 -0
  298. package/schemas/piagent-webui/runtime-event-v2.schema.json +5206 -0
  299. package/schemas/piagent-webui/session-catalog-v1.schema.json +222 -0
  300. package/schemas/piagent-webui/session-command-v1.schema.json +390 -0
  301. package/schemas/piagent-webui/snapshot-v1.schema.json +2622 -0
  302. package/schemas/piagent-webui/source-change-v1.schema.json +763 -0
  303. package/schemas/piagent-webui/source-mutation-v1.schema.json +96 -0
  304. package/schemas/piagent-webui/source-revert-v1.schema.json +130 -0
  305. package/schemas/piagent-webui/subagent-tree-v1.schema.json +88 -0
  306. package/schemas/piagent-webui/task-index-v1.schema.json +74 -0
  307. package/schemas/piagent-webui/task-timeline-v1.schema.json +49 -0
  308. package/schemas/piagent-webui/transcript-v1.schema.json +243 -0
  309. package/schemas/task-baseline-manifest.schema.json +122 -0
  310. package/schemas/verifier-file-snapshot.schema.json +81 -0
  311. package/scripts/build-docs-site.mjs +21 -1
  312. package/scripts/check-architecture.mjs +1 -0
  313. package/scripts/check-doc-languages.mjs +19 -2
  314. package/scripts/check-third-party-neutrality.mjs +43 -0
  315. package/scripts/explain-command.mjs +240 -0
  316. package/scripts/install-global.sh +105 -4
  317. package/scripts/mcp-manage.mjs +17 -4
  318. package/scripts/pi-session-stats.sh +10 -2
  319. package/scripts/piagent-cli.mjs +30 -3
  320. package/scripts/piagent-dashboard.mjs +261 -0
  321. package/scripts/piagent-webui-launcher.mjs +121 -0
  322. package/scripts/setup.sh +69 -33
  323. package/scripts/traffic-snapshot.mjs +214 -0
  324. package/scripts/typescript-loader.mjs +42 -1
  325. package/scripts/uninstall-global.sh +53 -2
  326. package/scripts/update-global.mjs +33 -1
  327. package/scripts/verify-local.sh +257 -0
  328. package/scripts/verify-release-identity.mjs +4 -1
  329. package/scripts/verify-translation-pairing.mjs +121 -0
@@ -0,0 +1,489 @@
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
+ import { preferAuthoritativePiagentGuard } from "./extension-authority.ts";
16
+
17
+ const MAX_WARM_RUNTIMES = 10;
18
+
19
+ type RuntimeHandle = { dispose(): Promise<void>; session?: any };
20
+ type RuntimeFactory = (info: PiSessionInfo, runtimeInstanceRef: string, sessionManager?: any) => Promise<RuntimeHandle>;
21
+ type ActiveRuntime = { runtime: RuntimeHandle; lease: SessionLeaseSnapshot; info: PiSessionInfo; operationRef: string | null;
22
+ unsubscribe: (() => void) | null; completion: Promise<void> | null; settling: boolean; approvalWaiting: boolean;
23
+ unbindApproval: (() => void) | null; unsubscribeApproval: (() => void) | null };
24
+ type Projection = { sessionRevision: string; liveState: "offline" | "idle" | "running" | "paused" | "waiting-approval" | "uncertain" };
25
+
26
+ function rpcUiContext(): object {
27
+ const plain = (text: unknown): string => String(text ?? "");
28
+ const theme = Object.freeze({
29
+ fg: (_color: unknown, text: unknown) => plain(text),
30
+ bg: (_color: unknown, text: unknown) => plain(text),
31
+ bold: plain,
32
+ italic: plain,
33
+ underline: plain,
34
+ inverse: plain,
35
+ strikethrough: plain,
36
+ getFgAnsi: () => "",
37
+ getBgAnsi: () => "",
38
+ getColorMode: () => "truecolor",
39
+ getThinkingBorderColor: () => plain,
40
+ getBashModeBorderColor: () => plain
41
+ });
42
+ return new Proxy({}, {
43
+ get(_target, property) {
44
+ if (property === GATEWAY_RUNTIME_UI_MARKER) return true;
45
+ if (property === "theme") return theme;
46
+ if (property === "confirm") return () => new Promise<boolean>(() => undefined);
47
+ if (property === "select" || property === "input") return async () => undefined;
48
+ return () => undefined;
49
+ }
50
+ });
51
+ }
52
+
53
+ function productionRuntimeFactory(options: { host: any; agentDir: string; packageRoot: string; modelRuntime?: any }): RuntimeFactory {
54
+ return async (info, _runtimeInstanceRef, sessionManager) => {
55
+ const guard = path.join(options.packageRoot, "packages", "piagent-core", "extensions", "piagent-guard.ts");
56
+ const createRuntime = async ({ cwd, agentDir, sessionManager, sessionStartEvent }: any) => {
57
+ const services = await options.host.createAgentSessionServices({
58
+ cwd, agentDir, modelRuntime: options.modelRuntime,
59
+ resourceLoaderOptions: {
60
+ additionalExtensionPaths: [guard],
61
+ extensionsOverride: preferAuthoritativePiagentGuard(guard)
62
+ }
63
+ });
64
+ const extensionErrors = services.resourceLoader.getExtensions().errors;
65
+ if (extensionErrors.length) throw new Error("session-runtime-extension-load-failed");
66
+ const created = await options.host.createAgentSessionFromServices({ services, sessionManager, sessionStartEvent });
67
+ return { ...created, services, diagnostics: services.diagnostics };
68
+ };
69
+ const manager = sessionManager ?? options.host.SessionManager.open(info.path);
70
+ const runtime = await options.host.createAgentSessionRuntime(createRuntime, {
71
+ cwd: info.cwd,
72
+ agentDir: options.agentDir,
73
+ sessionManager: manager,
74
+ sessionStartEvent: { type: "session_start", reason: "resume", previousSessionFile: info.path }
75
+ });
76
+ const bind = async (session: any) => session.bindExtensions({ mode: "rpc", uiContext: rpcUiContext() });
77
+ runtime.setRebindSession(bind);
78
+ try { await bind(runtime.session); }
79
+ catch (error) { await runtime.dispose().catch(() => undefined); throw error; }
80
+ return runtime;
81
+ };
82
+ }
83
+
84
+ export class SessionRuntimeSupervisor {
85
+ readonly #gatewayInstanceRef: string;
86
+ readonly #key: Buffer;
87
+ readonly #leases: SessionLeaseStore;
88
+ readonly #listSessions: () => Promise<PiSessionInfo[]>;
89
+ readonly #runtimeFactory: RuntimeFactory;
90
+ readonly #events: GatewayEventStore;
91
+ readonly #host: any | null;
92
+ readonly #resolveProject: ((projectRef: string) => string | null) | null;
93
+ readonly #active = new Map<string, ActiveRuntime>();
94
+ readonly #opening = new Map<string, Promise<SessionLeaseSnapshot>>();
95
+ readonly #created = new Map<string, PiSessionInfo>();
96
+ #closed = false;
97
+ #readProjection: ((sessionRef: string) => Promise<Projection>) | null = null;
98
+
99
+ constructor(options: {
100
+ gatewayInstanceRef: string;
101
+ key: Buffer;
102
+ leases: SessionLeaseStore;
103
+ listSessions(): Promise<PiSessionInfo[]>;
104
+ runtimeFactory?: RuntimeFactory;
105
+ host?: any;
106
+ agentDir?: string;
107
+ packageRoot?: string;
108
+ modelRuntime?: any;
109
+ events?: GatewayEventStore;
110
+ resolveProject?(projectRef: string): string | null;
111
+ }) {
112
+ this.#gatewayInstanceRef = options.gatewayInstanceRef;
113
+ this.#key = options.key;
114
+ this.#leases = options.leases;
115
+ this.#listSessions = options.listSessions;
116
+ this.#events = options.events ?? new GatewayEventStore();
117
+ this.#host = options.host ?? null;
118
+ this.#resolveProject = options.resolveProject ?? null;
119
+ if (options.runtimeFactory) this.#runtimeFactory = options.runtimeFactory;
120
+ else {
121
+ if (!options.host || !options.agentDir || !options.packageRoot) throw new Error("session-runtime-factory-unavailable");
122
+ this.#runtimeFactory = productionRuntimeFactory({ host: options.host, agentDir: options.agentDir,
123
+ packageRoot: options.packageRoot, modelRuntime: options.modelRuntime });
124
+ }
125
+ }
126
+
127
+ get activeCount(): number { return this.#active.size; }
128
+
129
+ setProjectionReader(reader: (sessionRef: string) => Promise<Projection>): void { this.#readProjection = reader; }
130
+
131
+ async listSessions(): Promise<PiSessionInfo[]> {
132
+ const persisted = await this.#listSessions();
133
+ const paths = new Set(persisted.map((info) => info.path));
134
+ for (const [sessionRef, info] of this.#created) {
135
+ if (paths.has(info.path)) this.#created.delete(sessionRef);
136
+ }
137
+ return [...persisted, ...[...this.#created.values()].filter((info) => !paths.has(info.path))];
138
+ }
139
+
140
+ async create(projectRef: string, placeRef: string, modelRef: string | null, thinkingLevel: string): Promise<string> {
141
+ if (this.#closed || !this.#host) throw new Error("session-create-unavailable");
142
+ if (placeRef !== projectRef) throw new Error("session-place-mismatch");
143
+ const sessions = await this.#listSessions();
144
+ const imported = this.#resolveProject?.(projectRef) ?? null;
145
+ const candidates = [...new Set([...sessions.filter((info) => projectRefForCwd(this.#key, info.cwd) === projectRef).map((info) => info.cwd),
146
+ ...(imported ? [imported] : [])])];
147
+ if (candidates.length !== 1 || !candidates[0]) throw new Error(candidates.length ? "session-project-ambiguous" : "session-project-not-found");
148
+ const manager = this.#host.SessionManager.create(candidates[0]);
149
+ const sessionFile = manager.getSessionFile();
150
+ if (!sessionFile || typeof manager.getSessionId?.() !== "string") throw new Error("session-create-unavailable");
151
+ const createdAt = new Date();
152
+ const created: PiSessionInfo = { path: sessionFile, id: manager.getSessionId(), cwd: candidates[0], created: createdAt,
153
+ modified: createdAt, messageCount: 0, firstMessage: "(no messages)", allMessagesText: "" };
154
+ const sessionRef = sessionRefForPath(this.#key, sessionFile);
155
+ await this.#activate(sessionRef, created, manager);
156
+ const active = this.#active.get(sessionRef), session = active?.runtime.session;
157
+ if (!session) throw new Error("session-runtime-unavailable");
158
+ if (modelRef) {
159
+ const models = session.modelRuntime?.getAvailableSnapshot?.() ?? [];
160
+ const model = models.find((value: any) => webUiModelRef(String(value.provider ?? ""), String(value.id ?? "")) === modelRef);
161
+ if (!model) throw new Error("session-model-unavailable");
162
+ await session.setModel(model);
163
+ }
164
+ session.setThinkingLevel(thinkingLevel);
165
+ this.#created.set(sessionRef, created);
166
+ return sessionRef;
167
+ }
168
+
169
+ async acquire(sessionRef: string): Promise<SessionLeaseSnapshot> {
170
+ if (this.#closed) throw new Error("session-runtime-supervisor-closed");
171
+ const active = this.#active.get(sessionRef);
172
+ if (active) return active.lease;
173
+ const pending = this.#opening.get(sessionRef);
174
+ if (pending) return await pending;
175
+ const opening = this.#acquire(sessionRef).finally(() => this.#opening.delete(sessionRef));
176
+ this.#opening.set(sessionRef, opening);
177
+ return await opening;
178
+ }
179
+
180
+ async #acquire(sessionRef: string): Promise<SessionLeaseSnapshot> {
181
+ if (this.#active.size + this.#opening.size > MAX_WARM_RUNTIMES) throw new Error("session-runtime-capacity");
182
+ const found = (await this.#listSessions()).filter((info) => sessionRefForPath(this.#key, info.path) === sessionRef);
183
+ if (found.length !== 1) throw new Error(found.length ? "session-ref-ambiguous" : "session-not-found");
184
+ return await this.#activate(sessionRef, found[0]!);
185
+ }
186
+
187
+ async #activate(sessionRef: string, info: PiSessionInfo, sessionManager?: any): Promise<SessionLeaseSnapshot> {
188
+ if (this.#active.has(sessionRef)) throw new Error("session-owner-conflict");
189
+ const runtimeInstanceRef = `runtime_${randomBytes(24).toString("base64url")}`;
190
+ const prior = this.#leases.inspect(sessionRef);
191
+ if (prior.state === "gateway-owned" || prior.state === "terminal-owned") {
192
+ try { this.#leases.releaseDeadOwnerForExplicitRecovery(sessionRef); } catch { /* live or unprovable owners stay authoritative */ }
193
+ }
194
+ const lease = this.#leases.acquire(sessionRef, this.#gatewayInstanceRef, runtimeInstanceRef);
195
+ try {
196
+ const runtime = await this.#runtimeFactory(info, runtimeInstanceRef, sessionManager);
197
+ const current = this.#leases.inspect(sessionRef);
198
+ if (current.state !== "gateway-owned" || current.ownerEpoch !== lease.ownerEpoch
199
+ || current.gatewayInstanceRef !== this.#gatewayInstanceRef || current.runtimeInstanceRef !== runtimeInstanceRef) {
200
+ await runtime.dispose().catch(() => undefined);
201
+ throw new Error("session-owner-continuity-lost");
202
+ }
203
+ const active: ActiveRuntime = { runtime, lease: current, info, operationRef: null, unsubscribe: null, completion: null,
204
+ settling: false, approvalWaiting: false, unbindApproval: null, unsubscribeApproval: null };
205
+ this.#active.set(sessionRef, active);
206
+ this.#bindApproval(sessionRef, active);
207
+ return current;
208
+ } catch (error) {
209
+ try { this.#leases.requireRecovery(sessionRef, lease.ownerEpoch!, this.#gatewayInstanceRef, runtimeInstanceRef, "session-runtime-open-failed"); }
210
+ catch { /* Existing acquired record still fails closed on the next Gateway. */ }
211
+ throw error;
212
+ }
213
+ }
214
+
215
+ async release(sessionRef: string): Promise<SessionLeaseSnapshot> {
216
+ const active = this.#active.get(sessionRef);
217
+ if (!active) {
218
+ const current = this.#leases.inspect(sessionRef);
219
+ if (current.state === "released") return current;
220
+ throw new Error(current.state === "recovery-required" ? "session-recovery-required" : "session-owner-conflict");
221
+ }
222
+ if (active.operationRef) throw new Error("session-runtime-busy");
223
+ if (active.settling && active.completion) await active.completion;
224
+ if (active.operationRef || active.settling) throw new Error("session-runtime-busy");
225
+ this.#active.delete(sessionRef);
226
+ active.unsubscribeApproval?.(); active.unbindApproval?.();
227
+ try { await active.runtime.dispose(); }
228
+ catch (error) {
229
+ try { this.#leases.requireRecovery(sessionRef, active.lease.ownerEpoch!, this.#gatewayInstanceRef,
230
+ active.lease.runtimeInstanceRef!, "session-runtime-dispose-failed"); } catch { /* remains fail closed */ }
231
+ throw error;
232
+ }
233
+ const released = this.#leases.release(sessionRef, active.lease.ownerEpoch!, this.#gatewayInstanceRef, active.lease.runtimeInstanceRef!);
234
+ const transient = this.#created.get(sessionRef);
235
+ if (transient && !(await this.#listSessions()).some((info) => info.path === transient.path)) this.#created.delete(sessionRef);
236
+ return released;
237
+ }
238
+
239
+ ownership(sessionRef: string): SessionOwnerProjection {
240
+ const lease = this.#leases.inspect(sessionRef);
241
+ const active = this.#active.get(sessionRef);
242
+ if (lease.state === "released") return {
243
+ state: "offline", liveState: "offline", composerAvailable: true, needsAttention: false,
244
+ owner: { kind: "none", ownerEpoch: null, gatewayInstanceRef: null, runtimeInstanceRef: null, continuity: "released" },
245
+ reasonCode: null
246
+ };
247
+ if (lease.state === "gateway-owned" && active && active.lease.ownerEpoch === lease.ownerEpoch
248
+ && lease.gatewayInstanceRef === this.#gatewayInstanceRef) return {
249
+ state: "gateway-owned", liveState: active.approvalWaiting ? "waiting-approval" : active.operationRef ? "running" : "idle",
250
+ composerAvailable: true, needsAttention: active.approvalWaiting,
251
+ owner: { kind: "gateway", ownerEpoch: lease.ownerEpoch!, gatewayInstanceRef: lease.gatewayInstanceRef!,
252
+ runtimeInstanceRef: lease.runtimeInstanceRef!, continuity: "exact" }, reasonCode: null
253
+ };
254
+ if (lease.state === "unavailable") return {
255
+ state: "recovery-required", liveState: "uncertain", composerAvailable: false, needsAttention: true,
256
+ owner: { kind: "none", ownerEpoch: null, gatewayInstanceRef: null, runtimeInstanceRef: null, continuity: "unknown" },
257
+ reasonCode: lease.reasonCode ?? "session-lease-unavailable"
258
+ };
259
+ if (lease.state === "terminal-owned") return {
260
+ state: "terminal-owned", liveState: "uncertain", composerAvailable: false, needsAttention: false,
261
+ owner: { kind: "terminal", ownerEpoch: lease.ownerEpoch!,
262
+ gatewayInstanceRef: `terminal_${createHmac("sha256", this.#key).update(lease.gatewayInstanceRef!).digest("base64url").slice(0, 43)}`,
263
+ runtimeInstanceRef: lease.runtimeInstanceRef!, continuity: "exact" },
264
+ reasonCode: "terminal-owner-active"
265
+ };
266
+ return {
267
+ state: "recovery-required", liveState: "uncertain", composerAvailable: false, needsAttention: true,
268
+ owner: { kind: "gateway", ownerEpoch: lease.ownerEpoch!, gatewayInstanceRef: lease.gatewayInstanceRef!,
269
+ runtimeInstanceRef: lease.runtimeInstanceRef!, continuity: "uncertain" },
270
+ reasonCode: lease.reasonCode ?? "session-owner-continuity-unknown"
271
+ };
272
+ }
273
+
274
+ async send(sessionRef: string, payload: { delivery: "new-operation" | "follow-up" | "steer"; message: string;
275
+ expectedOperationRef: string | null }, sessionRevision: string): Promise<{ resultCode: "started" | "queued" | "steered"; operationRef: string }> {
276
+ await this.acquire(sessionRef);
277
+ const active = this.#active.get(sessionRef), session = active?.runtime.session;
278
+ if (!active || !session) throw new Error("session-runtime-unavailable");
279
+ if (payload.delivery !== "new-operation") {
280
+ if (!active.operationRef || active.operationRef !== payload.expectedOperationRef || !session.isStreaming) throw new Error("session-operation-conflict");
281
+ if (payload.delivery === "follow-up") { await session.followUp(payload.message); return { resultCode: "queued", operationRef: active.operationRef }; }
282
+ await session.steer(payload.message); return { resultCode: "steered", operationRef: active.operationRef };
283
+ }
284
+ if (payload.expectedOperationRef !== null || active.operationRef || active.settling || !session.isIdle) throw new Error("session-operation-conflict");
285
+ const operationRef = `operation_${randomBytes(24).toString("base64url")}`;
286
+ const stream = new GatewaySessionStream({ sessionRef, operationRef, events: this.#events });
287
+ active.operationRef = operationRef;
288
+ active.unsubscribe = session.subscribe((event: unknown) => stream.observe(event));
289
+ this.#events.publish("runtime.changed", { sessionRef, sessionRevision, liveState: "running", operationRef, reasonCode: null });
290
+ const created = this.#created.get(sessionRef);
291
+ if (created && created.firstMessage === "(no messages)") {
292
+ created.firstMessage = payload.message; created.allMessagesText = payload.message; created.messageCount = 1; created.modified = new Date();
293
+ }
294
+ let prompt: Promise<void>;
295
+ try { prompt = session.prompt(payload.message); }
296
+ catch (error) { await this.#finishOperation(sessionRef, operationRef, stream); throw error; }
297
+ active.completion = prompt.then(() => this.#finishOperation(sessionRef, operationRef, stream),
298
+ () => this.#finishOperation(sessionRef, operationRef, stream));
299
+ await Promise.race([
300
+ stream.started(),
301
+ prompt.then(() => { throw new Error("session-operation-start-unobserved"); }, (error: unknown) => { throw error; })
302
+ ]);
303
+ return { resultCode: "started", operationRef };
304
+ }
305
+
306
+ async abort(sessionRef: string, operationRef: string, clearQueued: boolean): Promise<void> {
307
+ const active = this.#active.get(sessionRef), session = active?.runtime.session;
308
+ if (!active || !session || active.operationRef !== operationRef) throw new Error("session-operation-conflict");
309
+ const completion = active.completion;
310
+ await session.abort();
311
+ if (clearQueued) session.clearQueue();
312
+ await completion;
313
+ }
314
+
315
+ #bindApproval(sessionRef: string, active: ActiveRuntime): void {
316
+ const authority = (): ApprovalAuthority => {
317
+ if (!active.operationRef || active.lease.state !== "gateway-owned" || !active.lease.revision) return null;
318
+ const task = activeSessionTask(active.info.cwd, active.info.id);
319
+ const synthetic = (namespace: string) => `${namespace}_${createHmac("sha256", this.#key)
320
+ .update(`${sessionRef}\0${namespace}`).digest("base64url").slice(0, 43)}`;
321
+ const control = task ? inspectTaskControlState(active.info.cwd, task) : null;
322
+ return {
323
+ identity: {
324
+ projectRef: projectRefForCwd(this.#key, active.info.cwd), runtimeInstanceId: active.lease.runtimeInstanceRef!, sessionRef,
325
+ taskId: task?.taskId ?? synthetic("task"), taskRunId: task?.taskRunId ?? synthetic("task_run"),
326
+ agentOperationId: active.operationRef
327
+ },
328
+ revisions: {
329
+ runtimeRevision: active.lease.revision,
330
+ taskRevision: task ? webUiTaskRevision(task) : synthetic("task_rev"),
331
+ controlRevision: control?.controlRevision ?? synthetic("control_rev")
332
+ },
333
+ taskState: control?.state === "terminal" ? "terminal" : "active"
334
+ };
335
+ };
336
+ active.unbindApproval = piApprovalBroker.bind({ cwd: active.info.cwd, rawSessionId: active.info.id,
337
+ runtimeInstanceId: active.lease.runtimeInstanceRef!, authority });
338
+ active.unsubscribeApproval = piApprovalBroker.subscribe(active.info.cwd, active.info.id, (event: ApprovalBrokerEvent) => {
339
+ const projection = piApprovalBroker.projection(active.info.cwd, active.info.id);
340
+ active.approvalWaiting = projection.summary.state === "waiting";
341
+ void this.#publishApprovalState(sessionRef, active, event);
342
+ });
343
+ }
344
+
345
+ async #publishApprovalState(sessionRef: string, active: ActiveRuntime, event: ApprovalBrokerEvent): Promise<void> {
346
+ try {
347
+ const projection = await this.#readProjection?.(sessionRef);
348
+ if (!projection || this.#active.get(sessionRef) !== active) return;
349
+ this.#events.publish("runtime.changed", { sessionRef, sessionRevision: projection.sessionRevision,
350
+ liveState: active.approvalWaiting ? "waiting-approval" : active.operationRef ? "running" : projection.liveState,
351
+ operationRef: active.operationRef, reasonCode: `approval-${event.kind}` });
352
+ } catch { /* approval truth remains available through the canonical inspection route */ }
353
+ }
354
+
355
+ async setModel(sessionRef: string, modelRef: string): Promise<"model-changed" | "no-change"> {
356
+ await this.acquire(sessionRef);
357
+ const active = this.#active.get(sessionRef), session = active?.runtime.session;
358
+ if (!active || !session) throw new Error("session-runtime-unavailable");
359
+ if (active.operationRef || !session.isIdle) throw new Error("session-runtime-busy");
360
+ const current = session.model && typeof session.model === "object"
361
+ ? webUiModelRef(String(session.model.provider ?? ""), String(session.model.id ?? session.model.modelId ?? "")) : null;
362
+ if (current === modelRef) return "no-change";
363
+ const models = session.modelRuntime?.getAvailableSnapshot?.() ?? [];
364
+ const model = models.find((value: any) => webUiModelRef(String(value.provider ?? ""), String(value.id ?? "")) === modelRef);
365
+ if (!model) throw new Error("session-model-unavailable");
366
+ await session.setModel(model);
367
+ this.#touchCreated(sessionRef);
368
+ return "model-changed";
369
+ }
370
+
371
+ async setThinking(sessionRef: string, thinkingLevel: string): Promise<"thinking-changed" | "no-change"> {
372
+ await this.acquire(sessionRef);
373
+ const active = this.#active.get(sessionRef), session = active?.runtime.session;
374
+ if (!active || !session) throw new Error("session-runtime-unavailable");
375
+ if (active.operationRef || !session.isIdle) throw new Error("session-runtime-busy");
376
+ const current = String(session.thinkingLevel ?? session.getThinkingLevel?.() ?? "unknown");
377
+ if (current === thinkingLevel) return "no-change";
378
+ session.setThinkingLevel(thinkingLevel);
379
+ this.#touchCreated(sessionRef);
380
+ return "thinking-changed";
381
+ }
382
+
383
+ async setPermission(sessionRef: string, permissionMode: "read-only" | "workspace-write" | "trusted-full-access"):
384
+ Promise<"permission-changed"> {
385
+ await this.acquire(sessionRef);
386
+ const active = this.#active.get(sessionRef), session = active?.runtime.session;
387
+ if (!active || !session) throw new Error("session-runtime-unavailable");
388
+ if (active.operationRef || !session.isIdle) throw new Error("session-runtime-busy");
389
+ const before = Array.isArray(session.messages) ? session.messages.length : 0;
390
+ await session.prompt(`/permission ${permissionMode}`);
391
+ const messages = Array.isArray(session.messages) ? session.messages.slice(before) : [];
392
+ const observed = messages.reverse().find((message: any) => message?.role === "custom"
393
+ && message.customType === "piagent-permission-profile");
394
+ const profile = observed?.details?.permissionProfile;
395
+ if (!profile || profile.mode !== permissionMode || profile.warning) throw new Error("session-permission-unavailable");
396
+ this.#touchCreated(sessionRef);
397
+ return "permission-changed";
398
+ }
399
+
400
+ approvalProjection(sessionRef: string): { revision: string | null; summary: Record<string, unknown> } {
401
+ const active = this.#active.get(sessionRef);
402
+ if (!active) return { revision: null, summary: { state: "unknown", pending: [], recent: [],
403
+ health: { state: "unavailable", reasonCode: "approval-broker-unavailable", message: "Approval broker is unavailable" } } };
404
+ return piApprovalBroker.projection(active.info.cwd, active.info.id);
405
+ }
406
+
407
+ approvalDetail(sessionRef: string, approvalRef: string): unknown | null {
408
+ const active = this.#active.get(sessionRef);
409
+ return active ? piApprovalBroker.detail(active.info.cwd, active.info.id, approvalRef) : null;
410
+ }
411
+
412
+ async decideApproval(approvalRef: string, decision: unknown): Promise<unknown> {
413
+ for (const active of this.#active.values()) {
414
+ if (piApprovalBroker.detail(active.info.cwd, active.info.id, approvalRef)) {
415
+ return await piApprovalBroker.decide(active.info.cwd, active.info.id, approvalRef, decision);
416
+ }
417
+ }
418
+ throw new Error("approval-not-pending");
419
+ }
420
+
421
+ async rename(sessionRef: string, title: string): Promise<"renamed" | "no-change"> {
422
+ await this.acquire(sessionRef);
423
+ const active = this.#active.get(sessionRef), session = active?.runtime.session;
424
+ if (!active || !session?.sessionManager) throw new Error("session-runtime-unavailable");
425
+ if (active.operationRef || !session.isIdle) throw new Error("session-runtime-busy");
426
+ const current = String(session.sessionManager.getSessionName?.() ?? "").trim();
427
+ if (current === title.trim()) return "no-change";
428
+ session.sessionManager.appendSessionInfo(title);
429
+ this.#touchCreated(sessionRef);
430
+ return "renamed";
431
+ }
432
+
433
+ async fork(sessionRef: string, entryRef: string | null, title: string | null): Promise<string> {
434
+ if (!this.#host) throw new Error("session-fork-unavailable");
435
+ await this.acquire(sessionRef);
436
+ const active = this.#active.get(sessionRef), manager = active?.runtime.session?.sessionManager;
437
+ if (!active || !manager) throw new Error("session-runtime-unavailable");
438
+ if (active.operationRef || !active.runtime.session?.isIdle) throw new Error("session-runtime-busy");
439
+ const source = (await this.listSessions()).find((value) => sessionRefForPath(this.#key, value.path) === sessionRef);
440
+ if (!source) throw new Error("session-not-found");
441
+ let forkManager: any;
442
+ if (entryRef) {
443
+ const file = manager.createBranchedSession(entryRef);
444
+ if (!file) throw new Error("session-fork-unavailable");
445
+ forkManager = this.#host.SessionManager.open(file);
446
+ } else {
447
+ forkManager = this.#host.SessionManager.forkFrom(source.path, source.cwd);
448
+ }
449
+ if (title) forkManager.appendSessionInfo(title);
450
+ const file = forkManager.getSessionFile(), id = forkManager.getSessionId?.();
451
+ if (!file || typeof id !== "string") throw new Error("session-fork-unavailable");
452
+ const persisted = (await this.#listSessions()).find((value) => value.path === file);
453
+ const now = new Date(), created = persisted ?? { ...source, path: file, id, name: title ?? source.name,
454
+ created: now, modified: now };
455
+ const forkRef = sessionRefForPath(this.#key, file);
456
+ this.#created.set(forkRef, created);
457
+ return forkRef;
458
+ }
459
+
460
+ #touchCreated(sessionRef: string): void {
461
+ const created = this.#created.get(sessionRef);
462
+ if (created) created.modified = new Date();
463
+ }
464
+
465
+ async #finishOperation(sessionRef: string, operationRef: string, stream: GatewaySessionStream): Promise<void> {
466
+ const active = this.#active.get(sessionRef);
467
+ if (!active || active.operationRef !== operationRef) return;
468
+ active.unsubscribe?.(); active.unsubscribe = null; active.operationRef = null; active.settling = true;
469
+ try {
470
+ const projection = await this.#readProjection?.(sessionRef);
471
+ if (!projection) return;
472
+ stream.complete(projection.sessionRevision);
473
+ this.#events.publish("runtime.changed", { sessionRef, sessionRevision: projection.sessionRevision,
474
+ liveState: projection.liveState, operationRef: null, reasonCode: null });
475
+ } catch { /* Canonical refresh failure cannot be replaced by an invented revision. */ }
476
+ finally {
477
+ if (this.#active.get(sessionRef) === active) { active.settling = false; active.completion = null; }
478
+ }
479
+ }
480
+
481
+ async close(): Promise<void> {
482
+ if (this.#closed) return;
483
+ this.#closed = true;
484
+ await Promise.allSettled([...this.#opening.values()]);
485
+ await Promise.allSettled([...this.#active.entries()].filter(([, active]) => active.operationRef)
486
+ .map(([sessionRef, active]) => this.abort(sessionRef, active.operationRef!, true)));
487
+ await Promise.allSettled([...this.#active.keys()].map((sessionRef) => this.release(sessionRef)));
488
+ }
489
+ }
@@ -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
+ }