@piagent/platform 1.37.1 → 1.38.2

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 (283) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +5 -3
  3. package/README.vi.md +2 -2
  4. package/catalog/capabilities.json +2 -2
  5. package/docs/command-reference-vietnamese.md +4 -3
  6. package/docs/distribution-standard.md +6 -6
  7. package/docs/en/maintainer-guide.md +8 -0
  8. package/docs/en/workspace-editor.md +222 -0
  9. package/docs/languages.json +13 -4
  10. package/docs/operator-manual-vietnamese.md +7 -7
  11. package/docs/publishing-for-teams.md +3 -0
  12. package/docs/release-install-policy.md +2 -2
  13. package/docs/research/ide-m0-spike.md +101 -0
  14. package/docs/team-onboarding.md +2 -2
  15. package/docs/vi/maintainer-guide.md +8 -0
  16. package/docs/vi/workspace-editor.md +217 -0
  17. package/docs-site/architecture.html +1 -1
  18. package/docs-site/benchmark.html +1 -1
  19. package/docs-site/commands.html +1 -1
  20. package/docs-site/content/en/index.html +4 -4
  21. package/docs-site/content/en/whats-new.html +23 -0
  22. package/docs-site/content/index.html +4 -4
  23. package/docs-site/content/whats-new.html +23 -0
  24. package/docs-site/context-engine.html +1 -1
  25. package/docs-site/ecosystem.html +1 -1
  26. package/docs-site/en/architecture.html +1 -1
  27. package/docs-site/en/benchmark.html +1 -1
  28. package/docs-site/en/commands.html +1 -1
  29. package/docs-site/en/context-engine.html +1 -1
  30. package/docs-site/en/ecosystem.html +1 -1
  31. package/docs-site/en/index.html +5 -5
  32. package/docs-site/en/mcp-approval.html +1 -1
  33. package/docs-site/en/mcp-auth.html +1 -1
  34. package/docs-site/en/mcp-servers.html +1 -1
  35. package/docs-site/en/mcp.html +1 -1
  36. package/docs-site/en/permissions.html +1 -1
  37. package/docs-site/en/profiles.html +1 -1
  38. package/docs-site/en/quickstart.html +1 -1
  39. package/docs-site/en/runtime.html +1 -1
  40. package/docs-site/en/scope.html +1 -1
  41. package/docs-site/en/security.html +1 -1
  42. package/docs-site/en/team.html +1 -1
  43. package/docs-site/en/whats-new.html +24 -1
  44. package/docs-site/en/windows.html +1 -1
  45. package/docs-site/en/workflows.html +1 -1
  46. package/docs-site/index.html +5 -5
  47. package/docs-site/mcp-approval.html +1 -1
  48. package/docs-site/mcp-auth.html +1 -1
  49. package/docs-site/mcp-servers.html +1 -1
  50. package/docs-site/mcp.html +1 -1
  51. package/docs-site/permissions.html +1 -1
  52. package/docs-site/profiles.html +1 -1
  53. package/docs-site/quickstart.html +1 -1
  54. package/docs-site/runtime.html +1 -1
  55. package/docs-site/scope.html +1 -1
  56. package/docs-site/security.html +1 -1
  57. package/docs-site/team.html +1 -1
  58. package/docs-site/whats-new.html +24 -1
  59. package/docs-site/windows.html +1 -1
  60. package/docs-site/workflows.html +1 -1
  61. package/evals/fixtures/piagent-webui/checkpoint-v1.invalid.json +21 -0
  62. package/evals/fixtures/piagent-webui/checkpoint-v1.valid.json +20 -0
  63. package/evals/fixtures/piagent-webui/terminal-v1.invalid.json +17 -0
  64. package/evals/fixtures/piagent-webui/terminal-v1.valid.json +16 -0
  65. package/evals/fixtures/piagent-webui/workspace-file-v1.invalid.json +24 -0
  66. package/evals/fixtures/piagent-webui/workspace-file-v1.valid.json +23 -0
  67. package/evals/fixtures/piagent-webui/workspace-paths-v1.invalid.json +24 -0
  68. package/evals/fixtures/piagent-webui/workspace-paths-v1.valid.json +24 -0
  69. package/evals/fixtures/piagent-webui/workspace-search-v1.invalid.json +26 -0
  70. package/evals/fixtures/piagent-webui/workspace-search-v1.valid.json +26 -0
  71. package/evals/fixtures/piagent-webui/workspace-tree-v1.invalid.json +28 -0
  72. package/evals/fixtures/piagent-webui/workspace-tree-v1.valid.json +27 -0
  73. package/evals/fixtures/piagent-webui/workspace-write-v1.invalid.json +19 -0
  74. package/evals/fixtures/piagent-webui/workspace-write-v1.valid.json +19 -0
  75. package/evals/long-task-soak-v1/README.md +17 -0
  76. package/evals/long-task-soak-v1/lib/session-loop.mjs +19 -3
  77. package/evals/long-task-soak-v1/scenarios/ide-editor-terminal.json +19 -0
  78. package/evals/long-task-soak-v1/surfaces/webui-gateway.mjs +17 -2
  79. package/evals/long-task-soak-v1/surfaces/workspace-actions.mjs +95 -0
  80. package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
  81. package/package.json +7 -3
  82. package/packages/piagent-core/extensions/fallback-base-policy.ts +1 -1
  83. package/packages/piagent-core/extensions/guard-types.ts +3 -1
  84. package/packages/piagent-core/extensions/path-suggestions.ts +137 -0
  85. package/packages/piagent-core/extensions/piagent-guard.ts +85 -7
  86. package/packages/piagent-core/extensions/workspace-inspection.ts +145 -0
  87. package/packages/piagent-core/extensions/workspace-policy.d.ts +7 -0
  88. package/packages/piagent-core/extensions/workspace-policy.js +3 -0
  89. package/packages/piagent-core/extensions/workspace-search-index.mjs +8 -0
  90. package/packages/piagent-core/extensions/workspace-search.ts +74 -0
  91. package/packages/piagent-core/extensions/workspace-write.ts +116 -0
  92. package/packages/piagent-core/managed/background-process.mjs +9 -4
  93. package/packages/piagent-core/managed/boundary-package-proxy.mjs +21 -0
  94. package/packages/piagent-core/managed/boundary-queue.mjs +24 -0
  95. package/packages/piagent-core/managed/checkpoint-git.mjs +16 -0
  96. package/packages/piagent-core/managed/checkpoints.mjs +17 -64
  97. package/packages/piagent-core/managed/command-tools.mjs +6 -6
  98. package/packages/piagent-core/managed/continuation.mjs +3 -0
  99. package/packages/piagent-core/managed/dist/workspace/assets/adapters/backend-api/profile.json +88 -0
  100. package/packages/piagent-core/managed/dist/workspace/assets/adapters/be-readonly-fe/profile.json +130 -0
  101. package/packages/piagent-core/managed/dist/workspace/assets/adapters/data/profile.json +88 -0
  102. package/packages/piagent-core/managed/dist/workspace/assets/adapters/devops/profile.json +90 -0
  103. package/packages/piagent-core/managed/dist/workspace/assets/adapters/docs/profile.json +74 -0
  104. package/packages/piagent-core/managed/dist/workspace/assets/adapters/fullstack/profile.json +92 -0
  105. package/packages/piagent-core/managed/dist/workspace/assets/adapters/generic/profile.json +95 -0
  106. package/packages/piagent-core/managed/dist/workspace/assets/adapters/mobile/profile.json +86 -0
  107. package/packages/piagent-core/managed/dist/workspace/assets/adapters/node-typescript/profile.json +74 -0
  108. package/packages/piagent-core/managed/dist/workspace/assets/adapters/python/profile.json +75 -0
  109. package/packages/piagent-core/managed/dist/workspace/assets/adapters/web-frontend/profile.json +99 -0
  110. package/packages/piagent-core/managed/dist/workspace/assets/local-state-path-BrmghxEZ.js +284 -0
  111. package/packages/piagent-core/managed/dist/workspace/assets/packages/piagent-core/policies/base-policy.json +351 -0
  112. package/packages/piagent-core/managed/dist/workspace/manifest.json +65 -0
  113. package/packages/piagent-core/managed/dist/workspace/worker.mjs +7374 -0
  114. package/packages/piagent-core/managed/dist/workspace/workspace-search-index.mjs +1007 -0
  115. package/packages/piagent-core/managed/patch-snapshot.mjs +6 -8
  116. package/packages/piagent-core/managed/permission.mjs +29 -6
  117. package/packages/piagent-core/managed/repository-operation.mjs +2 -1
  118. package/packages/piagent-core/managed/review-snapshot-request.mjs +21 -0
  119. package/packages/piagent-core/managed/sandbox-diagnostic.mjs +2 -0
  120. package/packages/piagent-core/managed/session-mutation-observations.mjs +37 -0
  121. package/packages/piagent-core/managed/session.mjs +2 -2
  122. package/packages/piagent-core/managed/source-revert-requests.mjs +21 -0
  123. package/packages/piagent-core/managed/terminal-boundary.mjs +88 -0
  124. package/packages/piagent-core/managed/terminal-channel.mjs +66 -0
  125. package/packages/piagent-core/managed/terminal-runtime.mjs +64 -0
  126. package/packages/piagent-core/managed/terminal-seatbelt.mjs +48 -0
  127. package/packages/piagent-core/managed/terminal-worker.mjs +70 -0
  128. package/packages/piagent-core/managed/tool-boundary.mjs +45 -35
  129. package/packages/piagent-core/managed/tool-freshness.mjs +47 -0
  130. package/packages/piagent-core/managed/tool-worker.mjs +18 -3
  131. package/packages/piagent-core/managed/workspace-worker-pins.mjs +26 -0
  132. package/packages/piagent-core/package.json +1 -1
  133. package/packages/piagent-core/policies/base-policy.json +2 -1
  134. package/packages/piagent-core/runtime/checkpoints/checkpoint-authority.ts +43 -0
  135. package/packages/piagent-core/runtime/checkpoints/checkpoint-refs.mjs +51 -0
  136. package/packages/piagent-core/runtime/checkpoints/checkpoint-store.mjs +179 -0
  137. package/packages/piagent-core/runtime/checkpoints/checkpoint-worker.ts +28 -0
  138. package/packages/piagent-core/runtime/checkpoints/git-adapter.mjs +36 -0
  139. package/packages/piagent-core/runtime/checkpoints/persistent-history.mjs +96 -0
  140. package/packages/piagent-core/runtime/checkpoints/personal-git.ts +272 -0
  141. package/packages/piagent-core/runtime/checkpoints/personal-runtime.ts +158 -0
  142. package/packages/piagent-core/runtime/checkpoints/prune.mjs +29 -0
  143. package/packages/piagent-core/runtime/checkpoints/restore-preview.mjs +46 -0
  144. package/packages/piagent-core/runtime/checkpoints/turn-checkpoints.mjs +167 -0
  145. package/packages/piagent-core/runtime/hooks/session-hooks.ts +19 -16
  146. package/packages/piagent-core/runtime/hooks/session-start-hook.ts +3 -0
  147. package/packages/piagent-core/runtime/hooks/tool-call-hook.ts +2 -2
  148. package/packages/piagent-core/runtime/inspection/diff-projection.ts +2 -1
  149. package/packages/piagent-core/runtime/inspection/managed-source-revert.ts +90 -0
  150. package/packages/piagent-core/runtime/inspection/review-state-projection.ts +19 -8
  151. package/packages/piagent-core/runtime/inspection/session-diff-projection.ts +47 -0
  152. package/packages/piagent-core/runtime/inspection/session-hunk-provenance.ts +56 -0
  153. package/packages/piagent-core/runtime/inspection/session-mutation-observer.ts +125 -0
  154. package/packages/piagent-core/runtime/inspection/source-action-contract.ts +123 -0
  155. package/packages/piagent-core/runtime/inspection/source-change-projection.ts +3 -9
  156. package/packages/piagent-core/runtime/inspection/source-mutation-projection.ts +3 -41
  157. package/packages/piagent-core/runtime/inspection/source-mutation-store.ts +6 -36
  158. package/packages/piagent-core/runtime/inspection/source-revert-projection.ts +36 -30
  159. package/packages/piagent-core/runtime/inspection/text-diff.ts +5 -4
  160. package/packages/piagent-core/runtime/inspection/workspace-worker.ts +79 -0
  161. package/packages/piagent-core/runtime/policy/source-index-transaction.ts +3 -10
  162. package/packages/piagent-core/runtime/policy/source-mutation-guard-binding.ts +27 -2
  163. package/packages/piagent-core/runtime/policy/source-mutation-guard.ts +42 -12
  164. package/packages/piagent-core/runtime/policy/source-worktree-transaction.ts +10 -5
  165. package/packages/piagent-core/runtime/policy/workspace-write-authority.ts +51 -0
  166. package/packages/piagent-core/runtime/process-owner.mjs +20 -0
  167. package/packages/piagent-core/runtime/process-tree-watch.mjs +46 -0
  168. package/packages/piagent-core/runtime/quality/model-mutation-proof.ts +8 -5
  169. package/packages/piagent-core/runtime/registration/permission-commands.ts +10 -2
  170. package/packages/piagent-core/runtime/registration/task-evidence-tools.ts +1 -1
  171. package/packages/piagent-core/runtime/session/workspace-member-edits.d.mts +7 -0
  172. package/packages/piagent-core/runtime/session/workspace-member-edits.mjs +41 -0
  173. package/packages/piagent-core/security/workspace-file-reader.ts +2 -1
  174. package/packages/piagent-webui/dist/client/assets/Alert-DGWhP2MM.js +1 -0
  175. package/packages/piagent-webui/dist/client/assets/Button-QItfZUNj.js +1 -0
  176. package/packages/piagent-webui/dist/client/assets/DialogTitle-CyaFPaiW.js +3 -0
  177. package/packages/piagent-webui/dist/client/assets/MarkdownMessage-DtEwqme0.js +29 -0
  178. package/packages/piagent-webui/dist/client/assets/TerminalPanel-C3vkzjQ6.js +2 -0
  179. package/packages/piagent-webui/dist/client/assets/TerminalView-TNjhBGfa.js +9 -0
  180. package/packages/piagent-webui/dist/client/assets/TerminalView-kHJ-D0s7.css +1 -0
  181. package/packages/piagent-webui/dist/client/assets/Typography-B9om7Evq.js +1 -0
  182. package/packages/piagent-webui/dist/client/assets/WorkspaceCodeEditor-BAhYwQNM.js +2 -0
  183. package/packages/piagent-webui/dist/client/assets/WorkspaceExplorer-Coh_GWCO.js +5 -0
  184. package/packages/piagent-webui/dist/client/assets/WorkspaceQuickOpen-Bn4miZq4.js +1 -0
  185. package/packages/piagent-webui/dist/client/assets/bootstrap-CedkNtRG.js +1 -0
  186. package/packages/piagent-webui/dist/client/assets/dist-B5MCHZv5.js +9 -0
  187. package/packages/piagent-webui/dist/client/assets/dist-BM_LL1o9.js +1 -0
  188. package/packages/piagent-webui/dist/client/assets/dist-C2TMLUTo.js +23 -0
  189. package/packages/piagent-webui/dist/client/assets/dist-CqL3udNu.js +1 -0
  190. package/packages/piagent-webui/dist/client/assets/dist-D4df0jLQ.js +1 -0
  191. package/packages/piagent-webui/dist/client/assets/dist-D6i5SpQu.js +10 -0
  192. package/packages/piagent-webui/dist/client/assets/dist-DfjUS8LS.js +6 -0
  193. package/packages/piagent-webui/dist/client/assets/dist-IRUjhd1P.js +1 -0
  194. package/packages/piagent-webui/dist/client/assets/dist-sNhoSseW.js +1 -0
  195. package/packages/piagent-webui/dist/client/assets/index-Brs_HjIo.css +1 -0
  196. package/packages/piagent-webui/dist/client/assets/index-a-perpOX.js +102 -0
  197. package/packages/piagent-webui/dist/client/assets/rolldown-runtime-hePW80VL.js +1 -0
  198. package/packages/piagent-webui/dist/client/assets/tab-close-nAxrK7Lq.js +1 -0
  199. package/packages/piagent-webui/dist/client/assets/ui-preferences-BO3AV7QE.js +10 -0
  200. package/packages/piagent-webui/dist/client/assets/useId-DEefKUAT.js +124 -0
  201. package/packages/piagent-webui/dist/client/assets/workspace-api-O7auv_td.js +1 -0
  202. package/packages/piagent-webui/dist/client/index.html +10 -2
  203. package/packages/piagent-webui/extension/same-session-bridge.ts +2 -3
  204. package/packages/piagent-webui/extension/source-revert-controller.ts +14 -208
  205. package/packages/piagent-webui/gateway/checkpoint-sessions.ts +99 -0
  206. package/packages/piagent-webui/gateway/checkpoint-store.ts +6 -0
  207. package/packages/piagent-webui/gateway/company-relay.ts +5 -0
  208. package/packages/piagent-webui/gateway/company-terminal-cache.ts +54 -0
  209. package/packages/piagent-webui/gateway/company-terminal-channel.ts +117 -0
  210. package/packages/piagent-webui/gateway/company-terminal-relay.ts +75 -0
  211. package/packages/piagent-webui/gateway/gateway-protocol-service.ts +12 -0
  212. package/packages/piagent-webui/gateway/gateway-service.ts +48 -4
  213. package/packages/piagent-webui/gateway/managed-gateway.mjs +30 -3
  214. package/packages/piagent-webui/gateway/managed-projection.ts +5 -3
  215. package/packages/piagent-webui/gateway/managed-source-revert-controller.ts +45 -0
  216. package/packages/piagent-webui/gateway/managed-source-revert-sessions.ts +74 -0
  217. package/packages/piagent-webui/gateway/managed-terminal-binding.ts +30 -0
  218. package/packages/piagent-webui/gateway/managed-terminal-processes.ts +74 -0
  219. package/packages/piagent-webui/gateway/managed-terminal-sessions.ts +9 -0
  220. package/packages/piagent-webui/gateway/path-suggestions.ts +1 -124
  221. package/packages/piagent-webui/gateway/personal-terminal-binding.ts +36 -0
  222. package/packages/piagent-webui/gateway/personal-terminal-diagnostic.mjs +16 -0
  223. package/packages/piagent-webui/gateway/personal-terminal-processes.ts +119 -0
  224. package/packages/piagent-webui/gateway/personal-terminal-sessions.ts +22 -0
  225. package/packages/piagent-webui/gateway/runtime-session-controls.ts +2 -1
  226. package/packages/piagent-webui/gateway/session-catalog.ts +3 -2
  227. package/packages/piagent-webui/gateway/session-command-controller.ts +4 -4
  228. package/packages/piagent-webui/gateway/session-inspection-registry.ts +29 -5
  229. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +8 -8
  230. package/packages/piagent-webui/gateway/signed-session-journal.ts +77 -0
  231. package/packages/piagent-webui/gateway/source-revert-sessions.ts +74 -0
  232. package/packages/piagent-webui/gateway/source-revert-store.ts +47 -0
  233. package/packages/piagent-webui/gateway/terminal-command-ledger.ts +33 -0
  234. package/packages/piagent-webui/gateway/terminal-mentions.ts +29 -0
  235. package/packages/piagent-webui/gateway/terminal-output-buffer.ts +58 -0
  236. package/packages/piagent-webui/gateway/terminal-router.ts +12 -0
  237. package/packages/piagent-webui/gateway/terminal-sessions.ts +247 -0
  238. package/packages/piagent-webui/gateway/workspace-write-sessions.ts +69 -0
  239. package/packages/piagent-webui/gateway/workspace-write-store.ts +6 -0
  240. package/packages/piagent-webui/package.json +15 -2
  241. package/packages/piagent-webui/server/checkpoint-command.ts +30 -0
  242. package/packages/piagent-webui/server/checkpoint-controller.ts +87 -0
  243. package/packages/piagent-webui/server/checkpoint-routes.ts +38 -0
  244. package/packages/piagent-webui/server/control-command-digest.ts +5 -0
  245. package/packages/piagent-webui/server/core-inspection-provider.ts +16 -22
  246. package/packages/piagent-webui/server/gateway-websocket.ts +36 -8
  247. package/packages/piagent-webui/server/loopback-server.ts +9 -0
  248. package/packages/piagent-webui/server/read-model-provider.ts +3 -0
  249. package/packages/piagent-webui/server/read-only-router.ts +3 -0
  250. package/packages/piagent-webui/server/session-auth.ts +4 -1
  251. package/packages/piagent-webui/server/session-source-revert-routes.ts +54 -0
  252. package/packages/piagent-webui/server/source-projection-binding.ts +18 -0
  253. package/packages/piagent-webui/server/source-revert-controller.ts +225 -0
  254. package/packages/piagent-webui/server/terminal-command.ts +35 -0
  255. package/packages/piagent-webui/server/terminal-websocket-stream.ts +56 -0
  256. package/packages/piagent-webui/server/transcript-projection.ts +2 -1
  257. package/packages/piagent-webui/server/workspace-read-routes.ts +32 -0
  258. package/packages/piagent-webui/server/workspace-reader.ts +23 -0
  259. package/packages/piagent-webui/server/workspace-write-controller.ts +118 -0
  260. package/packages/piagent-webui/server/workspace-write-routes.ts +38 -0
  261. package/packages/piagent-webui/shared/terminal-message.ts +23 -0
  262. package/schemas/piagent-webui/catalog-v1.json +63 -0
  263. package/schemas/piagent-webui/checkpoint-v1.schema.json +466 -0
  264. package/schemas/piagent-webui/control-command-v1.schema.json +68 -3
  265. package/schemas/piagent-webui/diff-v1.schema.json +4 -0
  266. package/schemas/piagent-webui/gateway-protocol-v1.schema.json +123 -3
  267. package/schemas/piagent-webui/session-catalog-v1.schema.json +1 -1
  268. package/schemas/piagent-webui/session-command-v1.schema.json +1 -1
  269. package/schemas/piagent-webui/snapshot-v1.schema.json +2 -1
  270. package/schemas/piagent-webui/source-revert-v1.schema.json +22 -3
  271. package/schemas/piagent-webui/terminal-v1.schema.json +495 -0
  272. package/schemas/piagent-webui/workspace-file-v1.schema.json +79 -0
  273. package/schemas/piagent-webui/workspace-paths-v1.schema.json +71 -0
  274. package/schemas/piagent-webui/workspace-search-v1.schema.json +80 -0
  275. package/schemas/piagent-webui/workspace-tree-v1.schema.json +87 -0
  276. package/schemas/piagent-webui/workspace-write-v1.schema.json +276 -0
  277. package/schemas/project-profile.schema.json +8 -3
  278. package/scripts/company-terminal.mjs +16 -11
  279. package/scripts/pi-auto.sh +5 -1
  280. package/scripts/team-doctor.sh +8 -0
  281. package/packages/piagent-webui/dist/client/assets/MarkdownMessage-CfgyQQbW.js +0 -29
  282. package/packages/piagent-webui/dist/client/assets/index-HyzWKvDu.js +0 -234
  283. package/packages/piagent-webui/dist/client/assets/index-_0nGP2n3.css +0 -1
@@ -0,0 +1,36 @@
1
+ import { sessionRefForPath } from "./session-catalog.ts";
2
+ import type { SessionRuntimeSupervisor } from "./session-runtime-supervisor.ts";
3
+
4
+ type Options = {
5
+ key: Buffer;
6
+ managed: boolean;
7
+ gatewayInstanceRef: string;
8
+ runtimes: Pick<SessionRuntimeSupervisor, "listSessions" | "acquire" | "liveSessionManager" | "ownership">;
9
+ };
10
+
11
+ // Browser refs select catalog entries, never host paths or shell environments.
12
+ // Keep a synchronous authority check for the instant before each native effect.
13
+ export async function personalTerminalBinding(options: Options, sessionRef: string) {
14
+ if (options.managed) throw new Error("company-boundary-required");
15
+ const { runtimes } = options;
16
+ const matches = (await runtimes.listSessions()).filter(info => sessionRefForPath(options.key, info.path) === sessionRef);
17
+ if (matches.length !== 1) throw new Error("terminal-session-unavailable");
18
+ const info = matches[0]!;
19
+ const lease = await runtimes.acquire(sessionRef);
20
+ const manager = runtimes.liveSessionManager(sessionRef);
21
+ const runtimeInstanceRef = lease.runtimeInstanceRef;
22
+ if (!runtimeInstanceRef || !manager || manager.getSessionId() !== info.id)
23
+ throw new Error("terminal-session-unavailable");
24
+ const assertCurrent = () => {
25
+ const current = runtimes.liveSessionManager(sessionRef), ownership = runtimes.ownership(sessionRef);
26
+ if (current !== manager || current.getSessionId() !== info.id || ownership.state !== "gateway-owned"
27
+ || ownership.owner.continuity !== "exact" || ownership.owner.gatewayInstanceRef !== options.gatewayInstanceRef
28
+ || ownership.owner.runtimeInstanceRef !== runtimeInstanceRef || ownership.owner.ownerEpoch !== lease.ownerEpoch)
29
+ throw new Error("terminal-runtime-replaced");
30
+ if (current.buildSessionContext().model?.provider === "agent_watch_managed")
31
+ throw new Error("company-boundary-required");
32
+ };
33
+ assertCurrent();
34
+ return { scope: { sessionRef, mode: "personal" as const, cwd: info.cwd },
35
+ identity: { gatewayInstanceRef: options.gatewayInstanceRef, sessionRef, runtimeInstanceRef }, assertCurrent };
36
+ }
@@ -0,0 +1,16 @@
1
+ import { createRequire } from 'node:module';
2
+ const require = createRequire(import.meta.url);
3
+ export function loadPersonalPty(load = () => require('@lydell/node-pty'), platform = process.platform) {
4
+ if (platform === 'win32') return { pty: null, reason: 'terminal-platform-unavailable' };
5
+ try {
6
+ const pty = load();
7
+ if (typeof pty?.spawn !== 'function') throw Error('invalid-pty-module');
8
+ return { pty, reason: null };
9
+ } catch { return { pty: null, reason: 'terminal-native-module-unavailable' }; }
10
+ }
11
+ export function personalTerminalDiagnostic(load, platform = process.platform) {
12
+ const { reason } = loadPersonalPty(load, platform);
13
+ return { available: reason === null, reasonCode: reason, platform,
14
+ scope: 'personal', nativeModule: '@lydell/node-pty', probe: 'module-load',
15
+ shellSpawnVerified: false };
16
+ }
@@ -0,0 +1,119 @@
1
+ import { loadPersonalPty } from "./personal-terminal-diagnostic.mjs";
2
+ import { randomUUID } from "node:crypto";
3
+ import { userInfo } from "node:os";
4
+ import { statSync } from "node:fs";
5
+ import { isAbsolute } from "node:path";
6
+ import { execFileSync } from "node:child_process";
7
+ import type { IPty } from "@lydell/node-pty";
8
+ import { ProcessTreeWatch } from "../../piagent-core/runtime/process-tree-watch.mjs";
9
+
10
+ type PtyModule = { spawn: typeof import("@lydell/node-pty").spawn };
11
+ type Scope = { sessionRef: string; mode: "personal" | "company"; cwd: string };
12
+ type Size = { cols: number; rows: number };
13
+ export type PersonalTerminalEvent = { sessionRef: string; terminalRef: string } &
14
+ ({ kind: "output"; data: string } | { kind: "exit"; exitCode: number; signal: number | null });
15
+ type Entry = { sessionRef: string; terminalRef: string; process: IPty; exited: boolean; cleanup?: Promise<void>; closing?: Promise<void> };
16
+ const dimensions = ({ cols, rows }: Size) => {
17
+ if (!Number.isInteger(cols) || cols < 2 || cols > 500 || !Number.isInteger(rows) || rows < 1 || rows > 200)
18
+ throw new Error("terminal-invalid-size");
19
+ };
20
+ function descendants(pid: number): number[] {
21
+ const rows = execFileSync("/bin/ps", ["-axo", "pid=,ppid="], { encoding: "utf8", timeout: 2000, maxBuffer: 4 * 1024 * 1024 })
22
+ .trim().split("\n").map(line => line.trim().split(/\s+/).map(Number));
23
+ const found = new Set<number>(), queue = [pid];
24
+ for (const parent of queue) for (const [child, owner] of rows) {
25
+ if (owner === parent && child > 1 && child !== process.pid && !found.has(child)) { found.add(child); queue.push(child); }
26
+ }
27
+ return [...found].reverse();
28
+ }
29
+ const signal = (pid: number, name: NodeJS.Signals) => { try { process.kill(pid, name); } catch { /* already exited */ } };
30
+
31
+ // This owns personal native processes only. The protocol/controller must resolve
32
+ // the trusted session scope before calling it; company never falls back here.
33
+ export class PersonalTerminalProcesses {
34
+ readonly #entries = new Map<string, Entry>();
35
+ readonly #closingSessions = new Set<string>();
36
+ readonly #emit: (event: PersonalTerminalEvent) => void;
37
+ readonly #pty: PtyModule | null;
38
+ readonly #watch = new ProcessTreeWatch();
39
+ readonly unavailableReason: string | null;
40
+ #closed = false;
41
+ constructor(emit: (event: PersonalTerminalEvent) => void, load?: () => PtyModule) {
42
+ this.#emit = emit;
43
+ const { pty, reason } = loadPersonalPty(load);
44
+ this.#pty = pty; this.unavailableReason = reason;
45
+ }
46
+ open(scope: Scope, size: Size, shell = userInfo().shell || process.env.SHELL || "/bin/sh") {
47
+ if (scope.mode !== "personal") throw new Error("company-boundary-required");
48
+ if (this.#closed || this.#closingSessions.has(scope.sessionRef)) throw new Error("terminal-session-closed");
49
+ if (!this.#pty) throw new Error(this.unavailableReason!);
50
+ dimensions(size);
51
+ if (!isAbsolute(scope.cwd) || !statSync(scope.cwd, { throwIfNoEntry: false })?.isDirectory()) throw new Error("terminal-project-unavailable");
52
+ if (!isAbsolute(shell) || shell.includes("\0")) throw new Error("terminal-shell-unavailable");
53
+ if (this.list(scope.sessionRef).length >= 4 || this.#entries.size >= 256) throw new Error("terminal-limit");
54
+ const terminalRef = `terminal.${randomUUID()}`;
55
+ // The member's own environment, without what only this Gateway uses
56
+ // (its agent folder, transform root, update switches).
57
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !/^(PI|PIAGENT)_/.test(key)));
58
+ let child: IPty;
59
+ try { child = this.#pty.spawn(shell, [], { ...size, cwd: scope.cwd, env: { ...env, TERM: "xterm-256color" }, name: "xterm-256color" }); }
60
+ catch { throw new Error("terminal-spawn-failed"); }
61
+ this.#watch.watch(child.pid);
62
+ const entry: Entry = { sessionRef: scope.sessionRef, terminalRef, process: child, exited: false };
63
+ this.#entries.set(terminalRef, entry);
64
+ child.onData(data => { if (!entry.closing) this.#notify({ sessionRef: scope.sessionRef, terminalRef, kind: "output", data }); });
65
+ child.onExit(({ exitCode, signal: exitSignal }) => {
66
+ entry.exited = true;
67
+ entry.cleanup ??= this.#stop(entry);
68
+ this.#notify({ sessionRef: scope.sessionRef, terminalRef, kind: "exit", exitCode, signal: exitSignal ?? null });
69
+ });
70
+ return { terminalRef, pid: child.pid };
71
+ }
72
+ list(sessionRef: string) { return [...this.#entries.values()].filter(entry => entry.sessionRef === sessionRef)
73
+ .map(entry => ({ terminalRef: entry.terminalRef, exited: entry.exited })); }
74
+ input(sessionRef: string, terminalRef: string, data: string) {
75
+ if (typeof data !== "string" || Buffer.byteLength(data) > 16 * 1024) throw new Error("terminal-input-limit");
76
+ const entry = this.#get(sessionRef, terminalRef);
77
+ if (entry.exited || entry.closing) throw new Error("terminal-exited");
78
+ entry.process.write(data);
79
+ }
80
+ resize(sessionRef: string, terminalRef: string, size: Size) {
81
+ dimensions(size); const entry = this.#get(sessionRef, terminalRef);
82
+ if (entry.exited || entry.closing) throw new Error("terminal-exited");
83
+ entry.process.resize(size.cols, size.rows);
84
+ }
85
+ pause(sessionRef: string, terminalRef: string) { const entry = this.#get(sessionRef, terminalRef); if (!entry.exited) entry.process.pause(); }
86
+ resume(sessionRef: string, terminalRef: string) { const entry = this.#get(sessionRef, terminalRef); if (!entry.exited) entry.process.resume(); }
87
+ async close(sessionRef: string, terminalRef: string) {
88
+ const entry = this.#get(sessionRef, terminalRef);
89
+ if (!entry.closing) { entry.cleanup ??= this.#stop(entry); entry.closing = entry.cleanup.finally(() => this.#entries.delete(terminalRef)); }
90
+ await entry.closing;
91
+ }
92
+ async closeSession(sessionRef: string) {
93
+ this.#closingSessions.add(sessionRef);
94
+ try { await Promise.all(this.list(sessionRef).map(entry => this.close(sessionRef, entry.terminalRef))); }
95
+ finally { this.#closingSessions.delete(sessionRef); }
96
+ }
97
+ async dispose() {
98
+ this.#closed = true;
99
+ await Promise.all([...new Set([...this.#entries.values()].map(entry => entry.sessionRef))].map(ref => this.closeSession(ref)));
100
+ }
101
+ #get(sessionRef: string, terminalRef: string) {
102
+ const entry = this.#entries.get(terminalRef);
103
+ if (!entry || entry.sessionRef !== sessionRef) throw new Error("terminal-not-found");
104
+ return entry;
105
+ }
106
+ #notify(event: PersonalTerminalEvent) { try { this.#emit(event); } catch { /* a disconnected consumer cannot own process lifetime */ } }
107
+ async #stop(entry: Entry) {
108
+ const pid = entry.process.pid;
109
+ let tree: number[] = [];
110
+ try { if (!entry.exited) tree = descendants(pid); } catch { /* still signal the PTY process group */ }
111
+ for (const child of tree) signal(child, "SIGTERM");
112
+ try { if (!entry.exited) entry.process.kill("SIGHUP"); } catch { /* already exited */ }
113
+ signal(-pid, "SIGTERM");
114
+ await new Promise(resolve => setTimeout(resolve, 100));
115
+ for (const child of tree) signal(child, "SIGKILL");
116
+ signal(-pid, "SIGKILL");
117
+ await this.#watch.stop(pid);
118
+ }
119
+ }
@@ -0,0 +1,22 @@
1
+ import { personalTerminalBinding } from "./personal-terminal-binding.ts";
2
+ import { PersonalTerminalProcesses, type PersonalTerminalEvent } from "./personal-terminal-processes.ts";
3
+ import { TerminalSessions } from "./terminal-sessions.ts";
4
+ export type { TerminalMessage as PersonalTerminalMessage } from "./terminal-sessions.ts";
5
+ type Binding = Awaited<ReturnType<typeof personalTerminalBinding>>;
6
+ type Options = Parameters<typeof personalTerminalBinding>[0];
7
+
8
+ // Personal authority is resolved here. This adapter never accepts a company boundary.
9
+ export class PersonalTerminalSessions extends TerminalSessions<Binding> {
10
+ constructor(options: Options, createProcesses = (emit: (event: PersonalTerminalEvent) => void) => new PersonalTerminalProcesses(emit)) {
11
+ super(ref => personalTerminalBinding(options, ref), emit => {
12
+ const processes = createProcesses(emit);
13
+ return { reason: () => processes.unavailableReason,
14
+ open: (binding, size) => processes.open(binding.scope, size),
15
+ input: (ref, terminal, data) => processes.input(ref, terminal, data),
16
+ resize: (ref, terminal, size) => processes.resize(ref, terminal, size),
17
+ close: (ref, terminal) => processes.close(ref, terminal),
18
+ pause: (ref, terminal) => processes.pause(ref, terminal), resume: (ref, terminal) => processes.resume(ref, terminal),
19
+ closeSession: ref => processes.closeSession(ref), dispose: () => processes.dispose() };
20
+ });
21
+ }
22
+ }
@@ -14,11 +14,12 @@ type RuntimeCommandOutput = {
14
14
  };
15
15
 
16
16
  export async function executePermissionCommand(session: any,
17
- permissionMode: "read-only" | "workspace-write" | "trusted-full-access"): Promise<void> {
17
+ permissionMode: "read-only" | "workspace-write" | "trusted-full-access" | "bypass"): Promise<void> {
18
18
  // A company conversation: "Ask first" or "Bypass" (no read-only mode); the
19
19
  // runtime keeps the choice in the conversation and reads it at each command,
20
20
  // so it may change while a turn runs (the supervisor lets it).
21
21
  if (session.managedExecution) {
22
+ // Bypass where the team's Harness allows it; the runtime refuses otherwise.
22
23
  if (typeof session.managedSetPermission !== "function" || permissionMode === "read-only") throw new Error("managed-permission-unavailable");
23
24
  session.managedSetPermission(permissionMode);
24
25
  return;
@@ -49,7 +49,7 @@ function catalogRef(key: Buffer, namespace: string, value: string): string {
49
49
  }
50
50
 
51
51
  export type SessionOwnerProjection = Pick<SessionRow, "state" | "liveState" | "composerAvailable" | "needsAttention" | "owner" | "reasonCode">;
52
- export type SessionOptionProjection = Pick<SessionRow, "modelLabel" | "thinkingLevel" | "managedHelpers" | "managedThinkingLevels" | "managedPermission" | "managedFast" | "managedPlan" | "managedProcess" | "managedTurnEnd" | "managedReview">;
52
+ export type SessionOptionProjection = Pick<SessionRow, "modelLabel" | "thinkingLevel" | "managedHelpers" | "managedThinkingLevels" | "managedPermission" | "managedBypass" | "managedFast" | "managedPlan" | "managedProcess" | "managedTurnEnd" | "managedReview">;
53
53
 
54
54
  function display(value: unknown, maximum: number, fallback: string): string {
55
55
  const clean = redactSensitiveText(String(value ?? "")).text
@@ -136,6 +136,7 @@ function row(key: Buffer, info: PiSessionInfo, metadata: SessionMetadata | undef
136
136
  ...(sessionOptions?.managedThinkingLevels ? {managedThinkingLevels:sessionOptions.managedThinkingLevels} : {}),
137
137
  ...(sessionOptions?.managedPermission ? {managedPermission:sessionOptions.managedPermission} : {}),
138
138
  ...(sessionOptions?.managedFast ? {managedFast:sessionOptions.managedFast} : {}),
139
+ ...(sessionOptions?.managedBypass ? {managedBypass:true as const} : {}),
139
140
  ...(sessionOptions?.managedPlan ? {managedPlan:sessionOptions.managedPlan} : {}),
140
141
  ...(sessionOptions?.managedProcess ? {managedProcess:sessionOptions.managedProcess} : {}),
141
142
  ...(sessionOptions?.managedTurnEnd ? {managedTurnEnd:sessionOptions.managedTurnEnd} : {}),
@@ -148,7 +149,7 @@ function row(key: Buffer, info: PiSessionInfo, metadata: SessionMetadata | undef
148
149
  ? { kind: "none", ownerEpoch: null, gatewayInstanceRef: null, runtimeInstanceRef: null, continuity: "released" }
149
150
  : ownership?.owner ?? { kind: "none", ownerEpoch: null, gatewayInstanceRef: null, runtimeInstanceRef: null, continuity: "unknown" },
150
151
  sessionRevision: revision(key, [info.path, info.modified.toISOString(), info.messageCount, info.name ?? null, metadata?.revision ?? null,
151
- sessionOptions?.modelLabel ?? null, sessionOptions?.thinkingLevel ?? "unknown", sessionOptions?.managedHelpers, sessionOptions?.managedThinkingLevels, sessionOptions?.managedPermission ?? null, sessionOptions?.managedFast ?? null, sessionOptions?.managedPlan, sessionOptions?.managedProcess, sessionOptions?.managedTurnEnd, sessionOptions?.managedReview,
152
+ sessionOptions?.modelLabel ?? null, sessionOptions?.thinkingLevel ?? "unknown", sessionOptions?.managedHelpers, sessionOptions?.managedThinkingLevels, sessionOptions?.managedPermission ?? null, sessionOptions?.managedBypass ?? null, sessionOptions?.managedFast ?? null, sessionOptions?.managedPlan, sessionOptions?.managedProcess, sessionOptions?.managedTurnEnd, sessionOptions?.managedReview,
152
153
  gitBranch, gitRepositories,
153
154
  metadata?.archived ? "archived" : ownership ? [ownership.state, ownership.liveState, ownership.owner.kind,
154
155
  ownership.owner.ownerEpoch, ownership.owner.runtimeInstanceRef, ownership.reasonCode] : "offline"]),
@@ -57,7 +57,7 @@ function validPayload(action: SessionAction, payload: Record<string, unknown>):
57
57
  if (action === "session.set-thinking") return exactKeys(payload, ["thinkingLevel"])
58
58
  && ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(String(payload.thinkingLevel));
59
59
  if (action === "session.set-permission") return exactKeys(payload, ["permissionMode"])
60
- && ["read-only", "workspace-write", "trusted-full-access"].includes(String(payload.permissionMode));
60
+ && ["read-only", "workspace-write", "trusted-full-access", "bypass"].includes(String(payload.permissionMode));
61
61
  if (action === "session.set-fast") return exactKeys(payload, ["fast"]) && typeof payload.fast === "boolean";
62
62
  if (action === "session.rename") return exactKeys(payload, ["title"]) && typeof payload.title === "string"
63
63
  && payload.title.length >= 1 && payload.title.length <= 500 && !/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(payload.title);
@@ -71,7 +71,7 @@ function validPayload(action: SessionAction, payload: Record<string, unknown>):
71
71
  && ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(String(payload.thinkingLevel))
72
72
  && typeof payload.message === "string" && payload.message.length >= 1 && payload.message.length <= 32_768 && !payload.message.includes("\0")
73
73
  && REF.test(String(payload.messageRequestId)) && (payload.deferInitialMessage === undefined || typeof payload.deferInitialMessage === "boolean")
74
- && (payload.permissionMode === undefined || ["read-only", "workspace-write", "trusted-full-access"].includes(String(payload.permissionMode)))
74
+ && (payload.permissionMode === undefined || ["read-only", "workspace-write", "trusted-full-access", "bypass"].includes(String(payload.permissionMode)))
75
75
  && (payload.fast === undefined || typeof payload.fast === "boolean")
76
76
  && (payload.workflow === undefined || isWebUiWorkflowId(payload.workflow));
77
77
  }
@@ -180,7 +180,7 @@ export class SessionCommandController {
180
180
  }
181
181
  if (command.payload.permissionMode !== undefined) {
182
182
  await this.#runtimes.setPermission(targetSessionRef, String(command.payload.permissionMode) as
183
- "read-only" | "workspace-write" | "trusted-full-access");
183
+ "read-only" | "workspace-write" | "trusted-full-access" | "bypass");
184
184
  }
185
185
  // Fast mode chosen on the new chat page is on before the first message
186
186
  // runs; a Harness that does not allow it refuses the create.
@@ -232,7 +232,7 @@ export class SessionCommandController {
232
232
  resultCode = await this.#runtimes.setThinking(command.sessionRef!, String(command.payload.thinkingLevel));
233
233
  } else if (command.action === "session.set-permission") {
234
234
  resultCode = await this.#runtimes.setPermission(command.sessionRef!, String(command.payload.permissionMode) as
235
- "read-only" | "workspace-write" | "trusted-full-access");
235
+ "read-only" | "workspace-write" | "trusted-full-access" | "bypass");
236
236
  } else if (command.action === "session.set-fast") {
237
237
  resultCode = await this.#runtimes.setFast(command.sessionRef!, command.payload.fast === true);
238
238
  } else if (command.action === "session.rename") {
@@ -1,3 +1,5 @@
1
+ import { piSourceMutationGuard } from "../../piagent-core/runtime/policy/source-mutation-guard.ts";
2
+ import { personalWorkspaceReader } from "../server/workspace-reader.ts";
1
3
  import { createHash } from "node:crypto";
2
4
  import fs from "node:fs";
3
5
  import path from "node:path";
@@ -49,7 +51,7 @@ const EMPTY_REPLAY = {
49
51
  reasonCode: null
50
52
  };
51
53
 
52
- function inspectionRuntimeRef(gatewayInstanceRef: string, sessionRef: string): string {
54
+ export function inspectionRuntimeRef(gatewayInstanceRef: string, sessionRef: string): string {
53
55
  return `runtime.${createHash("sha256").update(`${gatewayInstanceRef}\0${sessionRef}`).digest("hex")}`;
54
56
  }
55
57
 
@@ -118,13 +120,16 @@ export class SessionInspectionRegistry {
118
120
  readonly #packageRoot: string;
119
121
  readonly #agentDir?: string;
120
122
  readonly #models?: ModelRuntime;
123
+ readonly #managed: boolean;
124
+ readonly #workspaceRead?: (sessionRef: string, id: string, kind: string, path: string) => Promise<unknown>;
121
125
  readonly #projects?: ProjectSource;
122
126
  readonly #mcpAuth?: McpAuthBroker;
123
127
  readonly #operationLiveness: ((sessionRef: string) => "idle" | "running" | "unknown") | null;
124
128
  readonly #providers = new Map<string, { modifiedAt: string; provider: CoreInspectionProvider }>();
125
129
 
126
130
  constructor(options: { gatewayInstanceRef: string; host: PiHost; key: Buffer; packageRoot: string; agentDir?: string; models?: ModelRuntime; projects?: ProjectSource;
127
- mcpAuth?: McpAuthBroker; listSessions?: () => Promise<PiSessionInfo[]>;
131
+ workspaceRead?: (sessionRef: string, id: string, kind: string, path: string) => Promise<unknown>;
132
+ managed?: boolean; mcpAuth?: McpAuthBroker; listSessions?: () => Promise<PiSessionInfo[]>;
128
133
  openLiveSession?: (sessionRef: string) => ReturnType<PiHost["SessionManager"]["open"]> | null;
129
134
  operationLiveness?: (sessionRef: string) => "idle" | "running" | "unknown" }) {
130
135
  this.#gatewayInstanceRef = options.gatewayInstanceRef;
@@ -135,6 +140,8 @@ export class SessionInspectionRegistry {
135
140
  this.#packageRoot = options.packageRoot;
136
141
  this.#agentDir = options.agentDir;
137
142
  this.#models = options.models;
143
+ this.#managed = options.managed === true;
144
+ this.#workspaceRead = options.workspaceRead;
138
145
  this.#projects = options.projects;
139
146
  this.#mcpAuth = options.mcpAuth;
140
147
  this.#operationLiveness = options.operationLiveness ?? null;
@@ -225,7 +232,7 @@ export class SessionInspectionRegistry {
225
232
  if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1_048_576) return [];
226
233
  const value = JSON.parse(fs.readFileSync(file, "utf8")) as Record<string, unknown>;
227
234
  return [{ id: entry, displayName: safeName(value.displayName ?? entry),
228
- permissionMode: ["read-only", "workspace-write", "trusted-full-access"].includes(String(value.permissionProfile))
235
+ permissionMode: ["read-only", "workspace-write", "trusted-full-access", "bypass"].includes(String(value.permissionProfile))
229
236
  ? String(value.permissionProfile) : null }];
230
237
  }).slice(0, 100), []);
231
238
  const models = availableModels.flatMap((value) => {
@@ -239,7 +246,8 @@ export class SessionInspectionRegistry {
239
246
  const inputs = Array.isArray(value.input) ? value.input.filter((item): item is string => typeof item === "string") : null;
240
247
  return [{ modelRef: webUiModelRef(provider, modelId), provider, modelId, displayName: safeName(value.name ?? modelId),
241
248
  reasoning: value.reasoning, imageInput: inputs ? inputs.includes("image") : null, thinkingLevels,
242
- ...(provider === "agent_watch_managed" && value.managedFast === true ? { managedFast: true } : {}) }];
249
+ ...(provider === "agent_watch_managed" && value.managedFast === true ? { managedFast: true } : {}),
250
+ ...(provider === "agent_watch_managed" && value.managedBypass === true ? { managedBypass: true } : {}) }];
243
251
  });
244
252
  // Only offer a default present in the authenticated host catalog. Retain
245
253
  // the previous Sol baseline for accounts that have not received GPT-6 yet.
@@ -256,7 +264,7 @@ export class SessionInspectionRegistry {
256
264
  reasonCode: projects.size ? null : "no-known-project" };
257
265
  }
258
266
 
259
- async provider(sessionRef: string): Promise<WebUiReadModelProvider> {
267
+ async provider(sessionRef: string): Promise<CoreInspectionProvider> {
260
268
  const info = await this.#sessionInfo(sessionRef);
261
269
  const modifiedAt = info.modified.toISOString();
262
270
  const cached = this.#providers.get(sessionRef);
@@ -288,6 +296,11 @@ export class SessionInspectionRegistry {
288
296
  runtimeInstanceId: inspectionRuntimeRef(this.#gatewayInstanceRef, sessionRef),
289
297
  eventStore,
290
298
  task: () => safeRead(() => activeSessionTask(info.cwd, info.id), undefined),
299
+ ...(!this.#managed && context.model?.provider !== "agent_watch_managed" ? {
300
+ sourceMutationGuardAvailable: () => piSourceMutationGuard.available(info.cwd, info.id),
301
+ sourceSessionControlRevision: () => piSourceMutationGuard.sessionControlRevision(info.cwd, info.id),
302
+ sourceSessionObservation: (file: string) => piSourceMutationGuard.sessionObservation(info.cwd, info.id, file)
303
+ } : {}),
291
304
  activityEvents: () => safeRead(() => readContextTelemetry(info.cwd, { limit: 5_000 }), []),
292
305
  // A provider can outlive the turn that created it (notably while a new
293
306
  // session has not reached the host index yet). Read the branch at request
@@ -312,6 +325,17 @@ export class SessionInspectionRegistry {
312
325
  approvalProjection: () => piApprovalBroker.projection(info.cwd, info.id),
313
326
  approvalDetail: (approvalRef) => piApprovalBroker.detail(info.cwd, info.id, approvalRef)
314
327
  });
328
+ if (this.#managed && this.#workspaceRead) {
329
+ Object.assign(provider, { workspace: async (kind: string, relative: string) => {
330
+ const value = await this.#workspaceRead!(sessionRef, info.id, kind, relative);
331
+ return { schemaVersion: 1, version: `piagent-webui-workspace-${kind}-v1`, generatedAt: new Date().toISOString(),
332
+ identity: (await provider.snapshot() as { identity: unknown }).identity, ...(value as object) };
333
+ } });
334
+ }
335
+ if (!this.#managed && context.model?.provider !== "agent_watch_managed") {
336
+ Object.assign(provider, { workspace: personalWorkspaceReader({ cwd: info.cwd, packageRoot: this.#packageRoot,
337
+ identity: async () => (await provider.snapshot() as { identity: Record<string, unknown> }).identity }) });
338
+ }
315
339
  this.#providers.delete(sessionRef);
316
340
  this.#providers.set(sessionRef, { modifiedAt, provider });
317
341
  while (this.#providers.size > CACHE_LIMIT) this.#providers.delete(this.#providers.keys().next().value as string);
@@ -32,22 +32,21 @@ export class SessionRuntimeSupervisor {
32
32
  readonly #runtimeFactory: RuntimeFactory; readonly #events: GatewayEventStore; readonly #settledOwners = new Set<string>();
33
33
  readonly #host: any | null; readonly #operationDeadlinePolicy: SessionOperationDeadlinePolicy;
34
34
  readonly #resolveProject: ((projectRef: string) => string | null) | null;
35
- readonly #sessionDirectory?: string;
35
+ readonly #sessionDirectory?: string; readonly #closeSessionResources: (sessionRef: string) => Promise<void>;
36
36
  readonly #settleOperation: ReturnType<typeof createSessionOperationSettlement>;
37
37
  readonly #active = new Map<string, ActiveRuntime>(); readonly #opening = new Map<string, Promise<SessionLeaseSnapshot>>();
38
38
  readonly #created = new Map<string, PiSessionInfo>(); #closed = false; #closing: Promise<void> | null = null; #readProjection: ((sessionRef: string) => Promise<Projection>) | null = null;
39
39
  constructor(options: {
40
- gatewayInstanceRef: string;
41
- key: Buffer;
40
+ gatewayInstanceRef: string; key: Buffer;
42
41
  leases: SessionLeaseStore;
43
42
  listSessions(): Promise<PiSessionInfo[]>;
44
43
  runtimeFactory?: RuntimeFactory;
45
44
  host?: any;
46
- agentDir?: string;
47
- packageRoot?: string;
45
+ agentDir?: string; packageRoot?: string;
48
46
  modelRuntime?: any;
49
47
  scopedBrokerRouter?: ScopedBrokerRouter;
50
48
  sessionDirectory?: string;
49
+ closeSessionResources?(sessionRef: string): Promise<void>;
51
50
  events?: GatewayEventStore;
52
51
  operationWatchdog?: SessionOperationWatchdogOptions;
53
52
  resolveProject?(projectRef: string): string | null;
@@ -61,7 +60,7 @@ export class SessionRuntimeSupervisor {
61
60
  this.#host = options.host ?? null;
62
61
  this.#operationDeadlinePolicy = sessionOperationDeadlinePolicy(options.operationWatchdog);
63
62
  this.#resolveProject = options.resolveProject ?? null;
64
- this.#sessionDirectory = options.sessionDirectory;
63
+ this.#sessionDirectory = options.sessionDirectory; this.#closeSessionResources = options.closeSessionResources ?? (async () => {});
65
64
  this.#settleOperation = createSessionOperationSettlement(options);
66
65
  if (options.runtimeFactory) this.#runtimeFactory = options.runtimeFactory;
67
66
  else {
@@ -169,7 +168,7 @@ export class SessionRuntimeSupervisor {
169
168
  if (active.operationRef || active.settling) throw new Error("session-runtime-busy");
170
169
  this.#active.delete(sessionRef);
171
170
  bestEffortUnsubscribe(active.unsubscribeApproval); bestEffortUnsubscribe(active.unbindApproval);
172
- try { await active.runtime.dispose(); }
171
+ try { await this.#closeSessionResources(sessionRef); await active.runtime.dispose(); }
173
172
  catch (error) {
174
173
  try { this.#leases.requireRecovery(sessionRef, active.lease.ownerEpoch!, this.#gatewayInstanceRef,
175
174
  active.lease.runtimeInstanceRef!, "session-runtime-dispose-failed"); } catch { /* remains fail closed */ }
@@ -308,6 +307,7 @@ export class SessionRuntimeSupervisor {
308
307
  if (active.lastSessionRevision) this.#events.publish("runtime.changed", { sessionRef,
309
308
  sessionRevision: active.lastSessionRevision, liveState: "uncertain", operationRef: null,
310
309
  ...(messageRequestId ? { messageRequestId } : {}), reasonCode });
310
+ await this.#closeSessionResources(sessionRef);
311
311
  await boundedResult(Promise.resolve().then(() => active.runtime.dispose()), this.#operationDeadlinePolicy.terminationTimeoutMs);
312
312
  }
313
313
  #bindApproval(sessionRef: string, active: ActiveRuntime): void {
@@ -362,7 +362,7 @@ export class SessionRuntimeSupervisor {
362
362
  if (effective !== thinkingLevel) throw new Error("session-thinking-mismatch");
363
363
  this.#touchCreated(sessionRef); return "thinking-changed";
364
364
  }
365
- async setPermission(sessionRef: string, permissionMode: "read-only" | "workspace-write" | "trusted-full-access"): Promise<"permission-changed"> {
365
+ async setPermission(sessionRef: string, permissionMode: "read-only" | "workspace-write" | "trusted-full-access" | "bypass"): Promise<"permission-changed"> {
366
366
  await this.acquire(sessionRef);
367
367
  const active = this.#active.get(sessionRef), session = active?.runtime.session;
368
368
  if (!active || !session) throw new Error("session-runtime-unavailable");
@@ -0,0 +1,77 @@
1
+ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ const MAX_BYTES = 8 * 1024 * 1024, MAX_RECORDS = 4000;
6
+ type SessionRecord = { identity: { sessionRef: string }; idempotencyKeyDigest: string };
7
+ type Journal<T> = { version: 1; sequence: number; records: Array<{ sequence: number; receipt: T }> };
8
+
9
+ // Gateway-owned receipts survive a runtime eviction or an empty conversation
10
+ // that Pi has not persisted yet. No source contents or raw idempotency keys.
11
+ export class SignedSessionJournal<T extends SessionRecord> {
12
+ readonly #namespace: string;
13
+ readonly #root: string;
14
+ readonly #key: Buffer;
15
+ readonly #directory: fs.Stats;
16
+ constructor(root: string, key: Buffer, namespace: string) {
17
+ if (!/^[a-z][a-z0-9-]{0,63}$/.test(namespace) || key.length < 32) throw new Error("session-journal-config-invalid");
18
+ this.#namespace = namespace;
19
+ this.#root = path.join(fs.realpathSync(root), namespace); this.#key = Buffer.from(key);
20
+ try { fs.mkdirSync(this.#root, { mode: 0o700 }); }
21
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error; }
22
+ const stat = fs.lstatSync(this.#root);
23
+ this.#directory = stat; this.#checkDirectory();
24
+ }
25
+ #checkDirectory(): void {
26
+ const stat = fs.lstatSync(this.#root);
27
+ if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o077)
28
+ || typeof process.getuid === "function" && stat.uid !== process.getuid()
29
+ || stat.dev !== this.#directory.dev || stat.ino !== this.#directory.ino) throw new Error("workspace-journal-unsafe");
30
+ }
31
+ #file(sessionRef: string): string {
32
+ if (!/^[A-Za-z0-9][A-Za-z0-9._~-]{0,159}$/.test(sessionRef)) throw new Error("workspace-session-invalid");
33
+ return path.join(this.#root, `${sessionRef}.json`);
34
+ }
35
+ #sign(sessionRef: string, value: Journal<T>): string {
36
+ return createHmac("sha256", this.#key).update(`${this.#namespace}-v1\0${sessionRef}\0`).update(JSON.stringify(value)).digest("hex");
37
+ }
38
+ #read(sessionRef: string): Journal<T> {
39
+ this.#checkDirectory();
40
+ let descriptor: number;
41
+ try { descriptor = fs.openSync(this.#file(sessionRef), fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK); }
42
+ catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return { version: 1, sequence: 0, records: [] }; throw error; }
43
+ try {
44
+ const stat = fs.fstatSync(descriptor);
45
+ if (!stat.isFile() || stat.nlink !== 1 || stat.size > MAX_BYTES || (stat.mode & 0o077)
46
+ || typeof process.getuid === "function" && stat.uid !== process.getuid()) throw new Error("workspace-journal-unsafe");
47
+ const { value, mac } = JSON.parse(fs.readFileSync(descriptor, "utf8")) as { value: Journal<T>; mac: string };
48
+ if (!value || typeof mac !== "string" || !/^[a-f0-9]{64}$/.test(mac) || !timingSafeEqual(Buffer.from(mac, "hex"), Buffer.from(this.#sign(sessionRef, value), "hex"))
49
+ || value.version !== 1 || !Number.isSafeInteger(value.sequence) || value.sequence < 0 || !Array.isArray(value.records) || value.records.length > MAX_RECORDS
50
+ || value.records.some(record => !record || !Number.isSafeInteger(record.sequence) || record.sequence < 1 || record.sequence > value.sequence
51
+ || !record.receipt || record.receipt.identity?.sessionRef !== sessionRef || !/^sha256:[a-f0-9]{64}$/.test(record.receipt.idempotencyKeyDigest)))
52
+ throw new Error("workspace-journal-invalid");
53
+ return value;
54
+ } finally { fs.closeSync(descriptor); }
55
+ }
56
+ history(sessionRef: string): T[] {
57
+ return this.#read(sessionRef).records.sort((a, b) => a.sequence - b.sequence).map(record => record.receipt);
58
+ }
59
+ append(sessionRef: string, receipt: T): void {
60
+ if (receipt.identity.sessionRef !== sessionRef) throw new Error("workspace-session-invalid");
61
+ const value = this.#read(sessionRef), sequence = value.sequence + 1;
62
+ const records = value.records.filter(record => record.receipt.idempotencyKeyDigest !== receipt.idempotencyKeyDigest);
63
+ records.push({ sequence, receipt });
64
+ if (records.length > MAX_RECORDS || !Number.isSafeInteger(sequence)) throw new Error("workspace-journal-full");
65
+ const next: Journal<T> = { version: 1, sequence, records }, bytes = JSON.stringify({ value: next, mac: this.#sign(sessionRef, next) }) + "\n";
66
+ if (Buffer.byteLength(bytes) > MAX_BYTES) throw new Error("workspace-journal-full");
67
+ const temporary = path.join(this.#root, `.write-${randomBytes(16).toString("hex")}`);
68
+ let descriptor: number | undefined;
69
+ try {
70
+ descriptor = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600);
71
+ fs.writeFileSync(descriptor, bytes); fs.fsyncSync(descriptor); fs.closeSync(descriptor); descriptor = undefined;
72
+ this.#checkDirectory(); fs.renameSync(temporary, this.#file(sessionRef));
73
+ const directory = fs.openSync(this.#root, fs.constants.O_RDONLY);
74
+ try { fs.fsyncSync(directory); } finally { fs.closeSync(directory); }
75
+ } finally { if (descriptor !== undefined) fs.closeSync(descriptor); fs.rmSync(temporary, { force: true }); }
76
+ }
77
+ }
@@ -0,0 +1,74 @@
1
+ import type { SourceRevertProjection } from "../../piagent-core/runtime/inspection/source-action-contract.ts";
2
+ import { piSourceMutationGuard } from "../../piagent-core/runtime/policy/source-mutation-guard.ts";
3
+ import type { BridgeSnapshot } from "../../piagent-core/runtime/inspection/session-identity.ts";
4
+ import { SourceRevertController } from "../server/source-revert-controller.ts";
5
+ import type { CoreInspectionProvider } from "../server/core-inspection-provider.ts";
6
+ import { SourceRevertStore } from "./source-revert-store.ts";
7
+ import { sessionRefForPath } from "./session-catalog.ts";
8
+ import type { SessionRuntimeSupervisor } from "./session-runtime-supervisor.ts";
9
+
10
+ type Options = { root: string; key: Buffer; managed: boolean;
11
+ runtimes: Pick<SessionRuntimeSupervisor, "listSessions" | "acquire" | "liveSessionManager" | "currentOperation">;
12
+ provider(sessionRef: string): Promise<CoreInspectionProvider>; changed(sessionRef: string): void };
13
+
14
+ // The catalog ref selects a runtime. The inspection identity inside commands
15
+ // remains independently bound to that runtime; the two refs need not be equal.
16
+ export class SourceRevertSessions {
17
+ readonly #options: Options; readonly #store: SourceRevertStore;
18
+ readonly #pending = new Map<string, Promise<unknown>>();
19
+ constructor(options: Options) { this.#options = options; this.#store = new SourceRevertStore(options.root, options.key); }
20
+ #serial<T>(sessionRef: string, action: () => Promise<T>): Promise<T> {
21
+ const run = (this.#pending.get(sessionRef) ?? Promise.resolve()).then(action, action);
22
+ this.#pending.set(sessionRef, run);
23
+ void run.finally(() => { if (this.#pending.get(sessionRef) === run) this.#pending.delete(sessionRef); }).catch(() => {});
24
+ return run;
25
+ }
26
+ async #live(sessionRef: string) {
27
+ const { runtimes, key } = this.#options;
28
+ if (this.#options.managed) throw new Error("source-company-boundary-required");
29
+ const matches = (await runtimes.listSessions()).filter(info => sessionRefForPath(key, info.path) === sessionRef);
30
+ if (matches.length !== 1) throw new Error("source-session-unavailable");
31
+ const info = matches[0]; await runtimes.acquire(sessionRef);
32
+ const manager = runtimes.liveSessionManager(sessionRef);
33
+ if (!manager || manager.getSessionId() !== info.id) throw new Error("source-session-unavailable");
34
+ if (manager.buildSessionContext().model?.provider === "agent_watch_managed") throw new Error("source-company-boundary-required");
35
+ const provider = await this.#options.provider(sessionRef); provider.invalidate();
36
+ const snapshot = await provider.snapshot() as { identity: NonNullable<BridgeSnapshot["identity"]>; revision: NonNullable<BridgeSnapshot["revisions"]> };
37
+ if (snapshot.identity.taskId !== null || snapshot.identity.taskRunId !== null) throw new Error("source-session-binding-unavailable");
38
+ const binding = (): BridgeSnapshot => {
39
+ const current = runtimes.liveSessionManager(sessionRef), same = current === manager && current?.getSessionId() === info.id;
40
+ return { state: same ? "ready" : "replacement-pending", identity: snapshot.identity,
41
+ revisions: { ...snapshot.revision, controlRevision: piSourceMutationGuard.sessionControlRevision(info.cwd, info.id) },
42
+ taskState: "none", liveness: same ? runtimes.currentOperation(sessionRef) ? "running" : "idle" : "unknown", eventSequence: 0 };
43
+ };
44
+ return { info, provider, snapshot, binding };
45
+ }
46
+ sources(sessionRef: string) {
47
+ return this.#serial(sessionRef, async () => (await this.#live(sessionRef)).provider.sourceChanges("working-tree"));
48
+ }
49
+ diff(sessionRef: string, fileRef: string) {
50
+ return this.#serial(sessionRef, async () => (await this.#live(sessionRef)).provider.diff("working-tree", fileRef));
51
+ }
52
+ preview(sessionRef: string, fileRef: string, hunkRef: string | null) {
53
+ return this.#serial(sessionRef, async () => {
54
+ const { provider } = await this.#live(sessionRef);
55
+ const preview = await provider.sourceRevert(fileRef, hunkRef) as SourceRevertProjection;
56
+ const snapshot = await provider.snapshot() as { revision: { runtimeRevision: string; controlRevision: string | null } };
57
+ return { ...preview, ...(snapshot.revision.controlRevision ? { commandBinding: {
58
+ runtimeRevision: snapshot.revision.runtimeRevision, controlRevision: snapshot.revision.controlRevision } } : {}) };
59
+ });
60
+ }
61
+ execute(sessionRef: string, command: unknown) {
62
+ return this.#serial(sessionRef, async () => {
63
+ const { info, provider, snapshot, binding } = await this.#live(sessionRef);
64
+ const controller = new SourceRevertController({ allowSessionReverts: true, projectRoot: info.cwd, bridge: { snapshot: binding },
65
+ evidence: this.#store.forSession(info.cwd, snapshot.identity.projectRef, snapshot.identity.sessionRef),
66
+ resolve: (file, hunks) => provider.sourceRevertAuthority(file, hunks),
67
+ revisions: async () => { provider.invalidate(); return (await provider.snapshot() as typeof snapshot).revision; },
68
+ mutate: request => piSourceMutationGuard.executeRevert({ cwd: info.cwd, rawSessionId: info.id, ...request }) });
69
+ const receipt = await controller.execute(command);
70
+ if (receipt.phase !== "rejected") { provider.invalidate(); this.#options.changed(sessionRef); }
71
+ return receipt;
72
+ });
73
+ }
74
+ }