@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
@@ -1,11 +1,9 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { createHash, randomUUID } from 'node:crypto';
4
- import { fileURLToPath } from 'node:url';
4
+ import { invokeReviewSnapshot } from './review-snapshot-request.mjs';
5
5
 
6
- const quote = value => "'" + value.replaceAll("'", "'\"'\"'") + "'";
7
6
  const textContent = result => result.content?.filter(p => p.type === 'text').map(p => p.text).join('\n') ?? '';
8
- const WORKER = fileURLToPath(new URL('./review-snapshot-worker.mjs', import.meta.url));
9
7
  const MAX_SNAPSHOT = 16 * 1024 * 1024;
10
8
 
11
9
  // The worker runs inside the tool boundary; what it prints is one short line.
@@ -20,8 +18,8 @@ async function worker(boundary, mode, scope = null, base = null) {
20
18
  fs.writeFileSync(path.join(boundary.temporary, scopeName), JSON.stringify([...scope]), { mode: 0o600, flag: 'wx' });
21
19
  }
22
20
  let result;
23
- const from = typeof base === 'string' && BASE.test(base) ? ' ' + base : '';
24
- try { result = await boundary.invoke('bash', { command: `${quote(process.execPath)} ${quote(WORKER)} ${mode}${scopeName ? ' ' + scopeName : ''}${from}`, timeout: 60 }); }
21
+ const extra = [...(scopeName ? [scopeName] : []), ...(typeof base === 'string' && BASE.test(base) ? [base] : [])];
22
+ try { result = await invokeReviewSnapshot(boundary, mode, extra); }
25
23
  finally { if (scopeName) fs.rmSync(path.join(boundary.temporary, scopeName), { force: true }); }
26
24
  const text = textContent(result).trim();
27
25
  if (result.details?.truncation?.truncated || text.includes('\n')) throw Error(/review-(patch|file|untracked)-limit/.exec(text)?.[0] ?? 'managed-review-patch-unreadable');
@@ -37,7 +35,7 @@ export async function readPatchDigest(boundary, scope = null, base = null) { ret
37
35
  // The commit HEAD names now, or null (no repository, no commit yet).
38
36
  export async function readHead(boundary) {
39
37
  try {
40
- const result = await boundary.invoke('bash', { command: `${quote(process.execPath)} ${quote(WORKER)} head`, timeout: 60 });
38
+ const result = await invokeReviewSnapshot(boundary, 'head');
41
39
  const text = textContent(result).trim();
42
40
  return BASE.test(text) ? text : null;
43
41
  } catch { return null; }
@@ -48,7 +46,7 @@ export async function readHead(boundary) {
48
46
  export async function resolveReviewBase(boundary, ref) {
49
47
  if (typeof ref !== 'string' || !/^[A-Za-z0-9._\/~^@{}-]{1,200}$/.test(ref) || ref.startsWith('-')) return null;
50
48
  try {
51
- const result = await boundary.invoke('bash', { command: `${quote(process.execPath)} ${quote(WORKER)} since ${quote(ref)}`, timeout: 60 });
49
+ const result = await invokeReviewSnapshot(boundary, 'since', [ref]);
52
50
  const text = textContent(result).trim();
53
51
  return BASE.test(text) ? text : null;
54
52
  } catch { return null; }
@@ -57,7 +55,7 @@ export async function resolveReviewBase(boundary, ref) {
57
55
  // came since: { base, ref }, or null.
58
56
  export async function readBranchBase(boundary) {
59
57
  try {
60
- const result = await boundary.invoke('bash', { command: `${quote(process.execPath)} ${quote(WORKER)} branch-base`, timeout: 60 });
58
+ const result = await invokeReviewSnapshot(boundary, 'branch-base');
61
59
  const [base, ref] = textContent(result).trim().split(' ');
62
60
  return BASE.test(base ?? '') && ref ? { base, ref } : null;
63
61
  } catch { return null; }
@@ -1,12 +1,22 @@
1
1
  // A company conversation's access, chosen by the member and kept with the
2
2
  // conversation: "workspace-write" asks before every command that needs the
3
- // internet; "trusted-full-access" (Bypass) asks only for what must be
4
- // confirmed (runtime/policy/bypass-policy.mjs).
3
+ // internet; "trusted-full-access" (Full access) asks only for what must be
4
+ // confirmed (runtime/policy/bypass-policy.mjs); "bypass" asks nothing, as
5
+ // Codex's bypass mode, where the team's Harness allows it
6
+ // (harness.configuration.bypass_mode, an administrator's switch in Studio).
7
+ // The sandbox itself stays: bypass removes the questions, not the boundary.
5
8
  import { mustConfirm } from '../runtime/policy/bypass-policy.mjs';
6
9
 
7
- export const PERMISSIONS = ['workspace-write', 'trusted-full-access'];
10
+ export const PERMISSIONS = ['workspace-write', 'trusted-full-access', 'bypass'];
8
11
  const ENTRY = 'agent-watch-permission';
9
12
 
13
+ // Only an explicit yes from Studio: an Agent Watch that does not carry the
14
+ // field, or a Harness an administrator switched off, gives no bypass.
15
+ export const bypassAllowed = manifest => manifest?.harness?.configuration?.bypass_mode === true;
16
+ // What applies now: a conversation left in bypass after the administrator
17
+ // switched it off falls back to Full access.
18
+ export const effectivePermission = owner => owner.permission === 'bypass' && !bypassAllowed(owner.manifest) ? 'trusted-full-access' : owner.permission;
19
+
10
20
  export function restorePermission(manager) {
11
21
  const chosen = manager.getEntries().filter(e => e.type === 'custom' && e.customType === ENTRY).at(-1)?.data?.mode;
12
22
  return PERMISSIONS.includes(chosen) ? chosen : 'workspace-write';
@@ -14,9 +24,22 @@ export function restorePermission(manager) {
14
24
  export function permissionSetter(owner, manager) {
15
25
  return mode => {
16
26
  if (!PERMISSIONS.includes(mode)) throw Error('managed-permission-unavailable');
27
+ if (mode === 'bypass' && !bypassAllowed(owner.manifest)) throw Error('managed-bypass-unavailable');
17
28
  manager.appendCustomEntry(ENTRY, { mode, at: new Date().toISOString() }); owner.permission = mode;
18
29
  };
19
30
  }
20
- // Why a network command must still be asked about: null runs it in Bypass,
21
- // 'ask' when the member asks first.
22
- export const networkConfirmation = (owner, command) => owner.permission === 'trusted-full-access' ? mustConfirm(command) : 'ask';
31
+ // The conversation records whether its Harness allows Bypass, so the
32
+ // dashboard offers it (or not) without the runtime.
33
+ const ALLOWANCE = 'agent-watch-bypass';
34
+ export function recordBypass(owner, manager) {
35
+ const allowed = bypassAllowed(owner.manifest);
36
+ const last = manager.getEntries().filter(e => e.type === 'custom' && e.customType === ALLOWANCE).at(-1)?.data;
37
+ if ((last?.allowed === true) !== allowed) manager.appendCustomEntry(ALLOWANCE, { allowed, at: new Date().toISOString() });
38
+ }
39
+ // Whether this conversation may skip the questions Full access still asks.
40
+ export const asksNothing = owner => effectivePermission(owner) === 'bypass';
41
+ export const broadAccess = owner => ['trusted-full-access', 'bypass'].includes(effectivePermission(owner));
42
+ // Why a network command must still be asked about: null runs it in Full
43
+ // access or Bypass, 'ask' when the member asks first.
44
+ export const networkConfirmation = async (owner, command) => asksNothing(owner) ? null
45
+ : effectivePermission(owner) === 'trusted-full-access' ? mustConfirm(command) : 'ask';
@@ -1,3 +1,4 @@
1
+ import { broadAccess } from './permission.mjs';
1
2
  import fs from 'node:fs';
2
3
  import os from 'node:os';
3
4
  import path from 'node:path';
@@ -112,7 +113,7 @@ export function fetchOriginTool(self) {
112
113
  parameters:{type:'object',properties:{path:{type:'string',maxLength:500,description:'Repository folder inside the conversation folder, such as "api"; omit when the conversation folder is the repository.'}},additionalProperties:false},
113
114
  execute:async(id,args,signal,_update,ctx)=>{
114
115
  const plan=await repositoryFetchPlan(self.boundary,args?.path);
115
- if(self.permission==='trusted-full-access'&&!signal?.aborted)return executeRepositoryFetch(self.boundary,plan,signal);
116
+ if(broadAccess(self)&&!signal?.aborted)return executeRepositoryFetch(self.boundary,plan,signal);
116
117
  const {piApprovalBroker}=await import('../runtime/inspection/approval-broker.ts');
117
118
  const decision=await piApprovalBroker.request({cwd:self.cwd,rawSessionId:self.session.sessionManager.getSessionId(),toolCallId:id,
118
119
  action:{kind:'external-provider-action',preconditionClass:'runtime-only',toolName:'fetch_origin',rawAction:plan,
@@ -0,0 +1,21 @@
1
+ import { fileURLToPath } from 'node:url';
2
+
3
+ const worker = fileURLToPath(new URL('./review-snapshot-worker.mjs', import.meta.url));
4
+ const requests = new WeakSet();
5
+ const quote = value => "'" + value.replaceAll("'", "'\"'\"'") + "'";
6
+
7
+ // Only fixed runtime snapshot reads preserve provenance. JSON tool arguments
8
+ // cannot forge this identity; freezing prevents substitution before dispatch.
9
+ export function invokeReviewSnapshot(boundary, mode, extra = []) {
10
+ if (!['head', 'since', 'branch-base', 'digest', 'file', 'files'].includes(mode)
11
+ || !Array.isArray(extra) || extra.some(value => typeof value !== 'string')) throw Error('review-request-invalid');
12
+ const args = Object.freeze({ command: [process.execPath, worker, mode, ...extra].map(quote).join(' '), timeout: 60 });
13
+ requests.add(args);
14
+ return boundary.invoke('bash', args);
15
+ }
16
+
17
+ export function consumeReviewSnapshot(args) {
18
+ const trusted = requests.has(args);
19
+ requests.delete(args);
20
+ return trusted;
21
+ }
@@ -1,3 +1,5 @@
1
+ export { companyTerminalDiagnostic } from './terminal-runtime.mjs';
2
+
1
3
  // What a failed command tells the agent. Every environment failure names the
2
4
  // way on (owner, 2026-10-13): a tool that asks the member's approval
3
5
  // (run_with_network, run_with_docker, start_docker, fetch_origin, run_on_host)
@@ -0,0 +1,37 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { freshnessPath } from './tool-freshness.mjs';
3
+
4
+ // Trusted sandbox metadata only. No project filesystem access in the parent.
5
+ // Every consumer must revalidate policy and digests inside the boundary.
6
+ export class SessionMutationObservations {
7
+ #sessionId = randomUUID();
8
+ #files = new Map();
9
+ get(file) { return structuredClone(this.#files.get(file) ?? null); }
10
+ snapshot() { return structuredClone([...this.#files.entries()]); }
11
+ clear() { this.#files.clear(); }
12
+ prepare(cwd, name, args, enabled) {
13
+ const file = freshnessPath(cwd, name, args);
14
+ const request = enabled ? { sessionId: this.#sessionId, toolCallId: randomUUID(), prior: this.get(file) } : undefined;
15
+ if (['edit', 'write'].includes(name)) this.#files.delete(file);
16
+ if (name === 'bash') this.clear();
17
+ if (name === 'workspace' && args?.kind === 'write') this.#files.delete(args.payload?.path);
18
+ return request;
19
+ }
20
+ acceptRevert(args, trusted, result, update) {
21
+ if (args?.kind !== 'source' || args.request?.kind !== 'execute') return;
22
+ const file = args.request.target?.path, prior = this.#files.get(file);
23
+ this.#files.delete(file);
24
+ const next = update?.observation;
25
+ if (!trusted || result?.state !== 'settled' || update?.path !== file || !prior || !next || next.path !== file
26
+ || next.toolCallId !== prior.toolCallId || JSON.stringify(next.baseline) !== JSON.stringify(prior.baseline)
27
+ || !/^[a-f0-9]{64}$/.test(next.currentDigest) || !/^session-mutation\.[a-f0-9]{64}$/.test(next.evidenceRef)) return;
28
+ this.#files.set(file, structuredClone(next));
29
+ }
30
+ accept(cwd, name, args, request, observation) {
31
+ const file = freshnessPath(cwd, name, args);
32
+ if (!request || !file || observation?.path !== file || observation.toolCallId !== request.toolCallId
33
+ || typeof observation.baseline?.content !== 'string' || Buffer.byteLength(observation.baseline.content) > 256 * 1024) return;
34
+ this.#files.set(file, observation);
35
+ while (this.#files.size > 32) this.#files.delete(this.#files.keys().next().value);
36
+ }
37
+ }
@@ -26,7 +26,7 @@ import { workflowPolicy, repositoryChecks, planTool, currentPlan, PLAN_ENTRY, wo
26
26
  import { beforeDelegate } from './objections.mjs';
27
27
  import { HELPER_CALLS, HELPER_ROLES, helperRoles, helperPrompt, webPrompt, delegateDescription } from './helper-roles.mjs'; import { delegateParameters, delegateArgs, SPEC_PROMPT } from './helper-settlement.mjs';
28
28
  import { loadAgentResources } from './agent-skills.mjs';
29
- import { restorePermission, permissionSetter } from './permission.mjs';
29
+ import { restorePermission, recordBypass, permissionSetter } from './permission.mjs';
30
30
  import { commandTools, servicesPrompt } from './command-tools.mjs'; import { attachMemory, memoryToolsFor, openRound, closeRound, prepareMemoryContext, trackMemoryLessons } from './memory.mjs';
31
31
 
32
32
  const PROVIDER = 'agent_watch_managed';
@@ -169,7 +169,7 @@ export class ManagedSession {
169
169
  self.session.managedExecution = true;
170
170
  self.turnRetry = enableCompanyTurnRetry(self.session, settings, self.studioLink);
171
171
  // The member's access for this conversation: ask first, or Bypass (permission.mjs).
172
- self.permission = restorePermission(manager); self.session.managedSetPermission = permissionSetter(self, manager);
172
+ self.permission = restorePermission(manager); recordBypass(self, manager); self.session.managedSetPermission = permissionSetter(self, manager);
173
173
  // Fast mode, where the Harness allows it (fast-mode.mjs): off until the member turns it on.
174
174
  self.fast = restoreFast(manager); recordFast(self, manager); self.session.managedSetFast = fastSetter(self, manager); self.session.managedFastState = () => fastState(self); // null where the Harness does not allow it
175
175
  self.session.managedFastServed = () => { const route = self.routes.get(self.modelRuntime); return route ? fastServed(route) : null; };
@@ -0,0 +1,21 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+
3
+ // Authorization lives outside JSON arguments. Model tools cannot manufacture
4
+ // the object identity needed to obtain the boundary's key or observations.
5
+ export class SourceRevertRequests {
6
+ #key = randomBytes(32).toString('hex');
7
+ #requests = new WeakMap();
8
+ get revision() { return `source-control.${createHash("sha256").update(this.#key).digest("hex")}`; }
9
+ request(request, binding) {
10
+ const args = { kind: 'source', request: structuredClone(request) };
11
+ this.#requests.set(args, binding);
12
+ return args;
13
+ }
14
+ consume(args, observations) {
15
+ if (args?.kind !== 'source') return undefined;
16
+ const binding = this.#requests.get(args); this.#requests.delete(args);
17
+ if (!binding || !binding.current() || !binding.idle()) throw new Error('source-session-unavailable');
18
+ return { identity: structuredClone(binding.identity), rawSessionId: binding.rawSessionId,
19
+ controlRevision: binding.controlRevision, confirmationKey: this.#key, observations: observations.snapshot() };
20
+ }
21
+ }
@@ -0,0 +1,88 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { spawn } from 'node:child_process';
5
+ import { createHash, randomUUID } from 'node:crypto';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { BWRAP, bubblewrapArgs, credentialFiles } from './linux-sandbox.mjs';
8
+ import { companyTerminalRuntime } from './terminal-runtime.mjs';
9
+ import { terminalChannel } from './terminal-channel.mjs';
10
+ import { SEATBELT_TERMINAL_RULES, seatbeltTerminal } from './terminal-seatbelt.mjs';
11
+
12
+ const worker = fileURLToPath(new URL('./terminal-worker.mjs', import.meta.url));
13
+ const hash = file => createHash('sha256').update(fs.readFileSync(file)).digest('hex');
14
+ const inside = (file, root) => file === root || file.startsWith(root + path.sep);
15
+ const dimensions = ({ cols, rows }) => Number.isInteger(cols) && cols >= 2 && cols <= 500 && Number.isInteger(rows) && rows >= 1 && rows <= 200;
16
+
17
+ export class BoundaryTerminals {
18
+ #boundary; #runtime; #workerDigest; #entries = new Map(); #closed = false;
19
+ reasonCode = null;
20
+ constructor(boundary) {
21
+ this.#boundary = boundary;
22
+ try {
23
+ this.#runtime = companyTerminalRuntime();
24
+ if (inside(this.#runtime.root, boundary.cwd) || inside(worker, boundary.cwd)) throw Error('managed-terminal-runtime-overlap');
25
+ this.#workerDigest = hash(worker);
26
+ } catch (error) { this.reasonCode = /^managed-terminal-/.test(error.message) ? error.message : 'managed-terminal-runtime-invalid'; }
27
+ }
28
+ async open(size, emit) {
29
+ if (this.#closed) throw Error('managed-terminal-boundary-closed');
30
+ if (this.reasonCode) throw Error(this.reasonCode);
31
+ const boundary = this.#boundary;
32
+ if (!boundary.allowed.includes('bash')) throw Error('managed-terminal-permission-denied');
33
+ if (!dimensions(size)) throw Error('managed-terminal-invalid-size');
34
+ if (this.#entries.size >= 4) throw Error('managed-terminal-limit');
35
+ this.#runtime.assertCurrent();
36
+ if (hash(worker) !== this.#workerDigest) throw Error('managed-terminal-runtime-changed');
37
+ for (const [file, digest] of boundary.pins) if (hash(file) !== digest) throw Error('managed-runtime-changed');
38
+ const temporary = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'piagent-terminal-')));
39
+ fs.chmodSync(temporary, 0o700);
40
+ let channel;
41
+ try {
42
+ const home = path.join(temporary, 'home'), empty = path.join(temporary, 'empty');
43
+ fs.mkdirSync(home, { mode: 0o700 }); fs.writeFileSync(empty, '', { mode: 0o400 });
44
+ const env = Object.fromEntries(Object.entries(boundary.environment.offline)
45
+ .filter(([key]) => !/proxy/i.test(key) && !['NODE_OPTIONS', 'NODE_PATH', 'BASH_ENV', 'ENV', 'PIAGENT_TOOL_SCOPE'].includes(key))
46
+ .map(([key, value]) => [key, String(value).split(boundary.temporary).join(temporary)]));
47
+ Object.assign(env, { HOME: home, TMPDIR: temporary, TERM: 'xterm-256color', BASH_SILENCE_DEPRECATION_WARNING: '1',
48
+ PIAGENT_PROCESS_SCOPE: `${boundary.scope}:${randomUUID()}` });
49
+ // A separate temporary root excludes the boundary's package-proxy socket.
50
+ // Writes reach the project, its Git directories and the project's
51
+ // package caches, as the agent's own offline commands do.
52
+ if (!boundary.linux) channel = seatbeltTerminal({ entry: this.#runtime.entry, cwd: boundary.cwd, env, size, emit,
53
+ profile: boundary.terminalProfile(temporary) + SEATBELT_TERMINAL_RULES });
54
+ else {
55
+ const base = boundary.linuxBase;
56
+ const protectedPaths = (base.denied ?? []).filter(file => fs.existsSync(file))
57
+ .map(file => ({ path: file, directory: fs.statSync(file).isDirectory() }));
58
+ const args = bubblewrapArgs({ ...base, temporary, empty, network: false, proxySocket: null, docker: null,
59
+ toolchains: [...base.toolchains, this.#runtime.root],
60
+ credentials: [...credentialFiles([boundary.cwd, ...base.repository, ...base.references]), ...protectedPaths] });
61
+ // There is intentionally no alternative spawn path or injected host shell.
62
+ const child = spawn(BWRAP, [...args, boundary.node, worker, this.#runtime.entry,
63
+ fs.readlinkSync('/proc/self/ns/pid'), fs.readlinkSync('/proc/self/ns/net'), String(size.cols), String(size.rows)],
64
+ { cwd: boundary.cwd, detached: true, stdio: ['pipe', 'pipe', 'pipe'], env });
65
+ channel = terminalChannel(child, emit);
66
+ }
67
+ const id = randomUUID();
68
+ this.#entries.set(id, channel);
69
+ void channel.completed.then(() => { this.#entries.delete(id); fs.rmSync(temporary, { recursive: true, force: true }); }).catch(() => {});
70
+ await channel.initialized;
71
+ return { id, input(data) {
72
+ if (typeof data !== 'string' || Buffer.byteLength(data) > 16384) return Promise.reject(Error('managed-terminal-input-limit'));
73
+ return channel.execute({ kind: 'input', data });
74
+ }, resize(size) {
75
+ if (!dimensions(size)) return Promise.reject(Error('managed-terminal-invalid-size'));
76
+ return channel.execute({ kind: 'resize', cols: size.cols, rows: size.rows });
77
+ }, close: channel.close };
78
+ } catch (error) {
79
+ if (channel) await channel.close();
80
+ else fs.rmSync(temporary, { recursive: true, force: true });
81
+ throw error;
82
+ }
83
+ }
84
+ async dispose() {
85
+ this.#closed = true;
86
+ await Promise.all([...this.#entries.values()].map(channel => channel.close()));
87
+ }
88
+ }
@@ -0,0 +1,66 @@
1
+ import { StringDecoder } from 'node:string_decoder';
2
+ import { stopProcessGroup } from './background-process.mjs';
3
+
4
+ // Native effects are acknowledged once. A lost response rejects as uncertain;
5
+ // the Gateway command ledger owns retries and must never redispatch the effect.
6
+ export function terminalChannel(child, emit) {
7
+ const requests = new Map(), decoder = new StringDecoder('utf8');
8
+ let pending = '', next = 0, ready = false, closed = false, closing, exited = false;
9
+ let resolveReady, rejectReady, resolveClosed;
10
+ const initialized = new Promise((resolve, reject) => { resolveReady = resolve; rejectReady = reject; });
11
+ const completed = new Promise(resolve => { resolveClosed = resolve; });
12
+ const timer = setTimeout(() => fail('managed-terminal-start-timeout'), 10000);
13
+ const notify = message => { try { emit(message); } catch { /* consumer never owns process lifetime */ } };
14
+ const rejectAll = reason => {
15
+ rejectReady(new Error(reason));
16
+ for (const request of requests.values()) { clearTimeout(request.timer); request.reject(new Error(reason)); }
17
+ requests.clear();
18
+ };
19
+ const close = () => closing ??= (async () => {
20
+ if (closed) return;
21
+ rejectAll('managed-terminal-effect-uncertain');
22
+ await stopProcessGroup(child);
23
+ let timeout;
24
+ try { await Promise.race([completed, new Promise((_, reject) => { timeout = setTimeout(() => reject(Error('managed-terminal-cleanup-unconfirmed')), 3000); })]); }
25
+ finally { clearTimeout(timeout); }
26
+ })();
27
+ const fail = reason => { rejectAll(reason); void close().catch(() => {}); };
28
+ child.once('error', () => fail('managed-terminal-sandbox-unavailable'));
29
+ child.once('close', () => {
30
+ closed = true; clearTimeout(timer); rejectAll('managed-terminal-effect-uncertain');
31
+ if (!exited) notify({ kind: 'exit', exitCode: null, signal: null });
32
+ resolveClosed();
33
+ });
34
+ child.stdin.on('error', () => fail('managed-terminal-effect-uncertain'));
35
+ child.stderr.resume(); // Sandbox startup errors are reported by code, not raw host paths.
36
+ child.stdout.on('data', chunk => {
37
+ pending += decoder.write(chunk);
38
+ if (Buffer.byteLength(pending) > 128 * 1024) return fail('managed-terminal-protocol-invalid');
39
+ for (;;) {
40
+ const end = pending.indexOf('\n'); if (end < 0) break;
41
+ const line = pending.slice(0, end); pending = pending.slice(end + 1);
42
+ try {
43
+ const value = JSON.parse(line), keys = Object.keys(value).sort().join(',');
44
+ if (value.kind === 'ready' && keys === 'kind' && !ready) { ready = true; clearTimeout(timer); resolveReady(); }
45
+ else if (value.kind === 'unavailable' && !ready) fail('managed-terminal-sandbox-unavailable');
46
+ else if (ready && value.kind === 'ack' && keys === 'id,kind' && requests.has(value.id)) {
47
+ const request = requests.get(value.id); clearTimeout(request.timer); requests.delete(value.id); request.resolve();
48
+ } else if (ready && value.kind === 'output' && keys === 'data,kind' && typeof value.data === 'string' && value.data.length <= 8192) notify(value);
49
+ else if (ready && value.kind === 'exit' && keys === 'exitCode,kind,signal' && (value.exitCode === null || Number.isInteger(value.exitCode))
50
+ && (value.signal === null || Number.isInteger(value.signal)) && !exited) { exited = true; notify(value); }
51
+ else throw Error();
52
+ } catch { return fail('managed-terminal-protocol-invalid'); }
53
+ }
54
+ });
55
+ return { initialized, completed, close, execute(payload) {
56
+ if (!ready || closed || closing || exited) return Promise.reject(Error('managed-terminal-exited'));
57
+ if (requests.size >= 128 || child.stdin.writableLength > 128 * 1024) return Promise.reject(Error('managed-terminal-capacity'));
58
+ const id = String(++next), line = JSON.stringify({ ...payload, id }) + '\n';
59
+ if (Buffer.byteLength(line) > 128 * 1024) return Promise.reject(Error('managed-terminal-input-limit'));
60
+ return new Promise((resolve, reject) => {
61
+ const timer = setTimeout(() => { requests.delete(id); reject(Error('managed-terminal-effect-uncertain')); void close().catch(() => {}); }, 10000);
62
+ requests.set(id, { resolve, reject, timer });
63
+ child.stdin.write(line, error => { if (error) fail('managed-terminal-effect-uncertain'); });
64
+ });
65
+ } };
66
+ }
@@ -0,0 +1,64 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createRequire } from 'node:module';
4
+ import { createHash } from 'node:crypto';
5
+ import { BWRAP } from './linux-sandbox.mjs';
6
+
7
+ const require = createRequire(import.meta.url);
8
+ const VERSION = '1.2.0-beta.15';
9
+ const fail = () => { throw new Error('managed-terminal-runtime-invalid'); };
10
+
11
+ // Inspect code without loading the native addon. The entire platform package
12
+ // is pinned, including its JS and prebuilt binary. Linux loads it inside the
13
+ // sandbox; macOS loads it on the company Gateway to make the PTY there (the
14
+ // shell itself runs under Seatbelt, terminal-boundary.mjs).
15
+ function inventory(root) {
16
+ const entries = [], queue = [root];
17
+ let bytes = 0, visited = 0;
18
+ while (queue.length) {
19
+ const directory = queue.shift();
20
+ if (!fs.lstatSync(directory).isDirectory() || fs.realpathSync(directory) !== directory) fail();
21
+ for (const name of fs.readdirSync(directory).sort()) {
22
+ if (++visited > 128) fail();
23
+ const file = path.join(directory, name), stat = fs.lstatSync(file);
24
+ if (stat.isSymbolicLink()) fail();
25
+ if (stat.isDirectory()) { queue.push(file); continue; }
26
+ if (!stat.isFile() || stat.size > 16 * 1024 * 1024 || (bytes += stat.size) > 32 * 1024 * 1024) fail();
27
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
28
+ try {
29
+ const opened = fs.fstatSync(fd);
30
+ if (!opened.isFile() || opened.dev !== stat.dev || opened.ino !== stat.ino || opened.size !== stat.size) fail();
31
+ const digest = createHash('sha256').update(fs.readFileSync(fd)).digest('hex');
32
+ entries.push([path.relative(root, file), digest, opened.mode & 0o777]);
33
+ } finally { fs.closeSync(fd); }
34
+ }
35
+ }
36
+ return JSON.stringify(entries.sort((a, b) => a[0].localeCompare(b[0])));
37
+ }
38
+
39
+ export function companyTerminalRuntime({ platform = process.platform, arch = process.arch,
40
+ bubblewrap = BWRAP, resolve = specifier => createRequire(require.resolve('@lydell/node-pty')).resolve(specifier) } = {}) {
41
+ if (!['linux', 'darwin'].includes(platform)) throw new Error('managed-terminal-platform-unavailable');
42
+ if (platform === 'linux' && !bubblewrap) throw new Error('managed-terminal-bubblewrap-unavailable');
43
+ if (!['arm64', 'x64'].includes(arch)) throw new Error('managed-terminal-platform-unavailable');
44
+ const name = `@lydell/node-pty-${platform}-${arch}`;
45
+ let entry;
46
+ try { entry = fs.realpathSync(resolve(name)); }
47
+ catch { throw new Error('managed-terminal-native-unavailable'); }
48
+ const root = path.dirname(path.dirname(entry));
49
+ if (entry !== path.join(root, 'lib/index.js')) fail();
50
+ const pinned = inventory(root);
51
+ const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
52
+ if (manifest.name !== name || manifest.version !== VERSION || manifest.dependencies && Object.keys(manifest.dependencies).length) fail();
53
+ if (!fs.statSync(path.join(root, `prebuilds/${platform}-${arch}/pty.node`)).isFile()) fail();
54
+ return Object.freeze({ entry, root, version: VERSION,
55
+ assertCurrent() { if (inventory(root) !== pinned) throw new Error('managed-terminal-runtime-changed'); } });
56
+ }
57
+
58
+ export function companyTerminalDiagnostic(options = {}) {
59
+ let reasonCode = null;
60
+ try { companyTerminalRuntime(options); }
61
+ catch (error) { reasonCode = /^managed-terminal-/.test(error.message) ? error.message : 'managed-terminal-runtime-invalid'; }
62
+ return { supported: reasonCode === null, reasonCode, platform: options.platform ?? process.platform,
63
+ scope: 'company', probe: 'runtime-files', ptySpawnVerified: false };
64
+ }
@@ -0,0 +1,48 @@
1
+ import { createRequire } from 'node:module';
2
+ import { stopProcessGroup } from './background-process.mjs';
3
+ import { ProcessTreeWatch } from '../runtime/process-tree-watch.mjs';
4
+
5
+ const watch = new ProcessTreeWatch();
6
+
7
+ // macOS: Seatbelt denies creating a PTY, so the company Gateway makes it and
8
+ // only the shell runs sandboxed, under the agent's offline profile plus these
9
+ // rules: ioctls on terminal devices (only the one this shell was handed can be
10
+ // reached, since opening any other stays denied) and /dev/tty, which always
11
+ // names the process's own terminal.
12
+ export const SEATBELT_TERMINAL_RULES = `
13
+ (allow file-ioctl (regex #"^/dev/ttys[0-9]+$"))
14
+ (allow file-read* file-write* file-ioctl (literal "/dev/tty"))
15
+ `;
16
+
17
+ export function seatbeltTerminal({ entry, profile, cwd, env, size, emit }) {
18
+ const pty = createRequire(import.meta.url)(entry);
19
+ const child = pty.spawn('/usr/bin/sandbox-exec', ['-p', profile, '/bin/bash', '--noprofile', '--norc'],
20
+ { cwd, env, cols: size.cols, rows: size.rows, name: 'xterm-256color' });
21
+ watch.watch(child.pid);
22
+ const notify = message => { try { emit(message); } catch { /* consumer never owns process lifetime */ } };
23
+ let exited = false, closing, resolveClosed;
24
+ const completed = new Promise(resolve => { resolveClosed = resolve; });
25
+ child.onData(text => {
26
+ for (let start = 0; start < text.length;) {
27
+ let end = Math.min(start + 8192, text.length);
28
+ if (end < text.length && /[\uD800-\uDBFF]/.test(text[end - 1])) end--;
29
+ notify({ kind: 'output', data: text.slice(start, end) }); start = end;
30
+ }
31
+ });
32
+ child.onExit(({ exitCode, signal }) => {
33
+ exited = true; notify({ kind: 'exit', exitCode, signal: signal || null });
34
+ void watch.stop(child.pid).finally(resolveClosed);
35
+ });
36
+ const close = () => closing ??= (async () => {
37
+ if (exited) return;
38
+ await stopProcessGroup({ pid: child.pid, exitCode: null, signalCode: null });
39
+ let timeout;
40
+ try { await Promise.race([completed, new Promise((_, reject) => { timeout = setTimeout(() => reject(Error('managed-terminal-cleanup-unconfirmed')), 3000); })]); }
41
+ finally { clearTimeout(timeout); }
42
+ })();
43
+ return { initialized: Promise.resolve(), completed, close, execute(payload) {
44
+ if (exited || closing) return Promise.reject(Error('managed-terminal-exited'));
45
+ if (payload.kind === 'input') child.write(payload.data); else child.resize(payload.cols, payload.rows);
46
+ return Promise.resolve();
47
+ } };
48
+ }
@@ -0,0 +1,70 @@
1
+ import fs from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { StringDecoder } from 'node:string_decoder';
4
+
5
+ // Only the boundary launches this entrypoint. Check both namespaces before
6
+ // loading native code: accidentally invoking it on the host must fail closed.
7
+ const [entry, hostPidNamespace, hostNetworkNamespace, columns, rows] = process.argv.slice(2);
8
+ const size = (cols, rows) => Number.isInteger(cols) && cols >= 2 && cols <= 500 && Number.isInteger(rows) && rows >= 1 && rows <= 200;
9
+ const send = message => {
10
+ if (process.stdout.writableLength > 512 * 1024) process.exit(1);
11
+ process.stdout.write(JSON.stringify(message) + '\n');
12
+ };
13
+ let terminal;
14
+ try {
15
+ if (process.platform !== 'linux' || !/^pid:\[\d+\]$/.test(hostPidNamespace ?? '') || !/^net:\[\d+\]$/.test(hostNetworkNamespace ?? '')
16
+ || fs.readlinkSync('/proc/self/ns/pid') === hostPidNamespace || fs.readlinkSync('/proc/self/ns/net') === hostNetworkNamespace
17
+ || !size(Number(columns), Number(rows))) throw Error('sandbox-required');
18
+ const pty = createRequire(import.meta.url)(entry);
19
+ terminal = pty.spawn('/bin/bash', ['--noprofile', '--norc'], { cols: Number(columns), rows: Number(rows),
20
+ cwd: process.cwd(), env: { ...process.env, TERM: 'xterm-256color' }, name: 'xterm-256color' });
21
+ } catch {
22
+ process.stdout.write(JSON.stringify({ kind: 'unavailable', reasonCode: 'managed-terminal-sandbox-unavailable' }) + '\n', () => process.exit(1));
23
+ }
24
+ if (terminal) {
25
+ let ending = false;
26
+ const finish = message => {
27
+ if (ending) return;
28
+ ending = true;
29
+ // Exiting the namespace's main process also stops detached shell children.
30
+ const timer = setTimeout(() => process.exit(0), 1000); timer.unref();
31
+ process.stdout.write(JSON.stringify(message) + '\n', () => process.exit(0));
32
+ };
33
+ // A writer faster than the Gateway reads (`yes`) waits on the terminal, as
34
+ // on a real one, instead of growing this pipe until the worker gives up.
35
+ let paused = false;
36
+ process.stdout.on('drain', () => { if (paused && !ending) { paused = false; terminal.resume(); } });
37
+ terminal.onData(text => {
38
+ for (let start = 0; start < text.length;) {
39
+ let end = Math.min(start + 8192, text.length);
40
+ if (end < text.length && /[\uD800-\uDBFF]/.test(text[end - 1])) end--;
41
+ send({ kind: 'output', data: text.slice(start, end) }); start = end;
42
+ }
43
+ if (!paused && process.stdout.writableLength > 128 * 1024) { paused = true; terminal.pause(); }
44
+ });
45
+ terminal.onExit(({ exitCode, signal }) => finish({ kind: 'exit', exitCode, signal: signal ?? null }));
46
+ send({ kind: 'ready' });
47
+ let pending = '';
48
+ const decoder = new StringDecoder('utf8');
49
+ process.stdin.on('data', chunk => {
50
+ pending += decoder.write(chunk);
51
+ if (Buffer.byteLength(pending) > 128 * 1024) return process.exit(1);
52
+ for (;;) {
53
+ const end = pending.indexOf('\n'); if (end < 0) break;
54
+ const line = pending.slice(0, end); pending = pending.slice(end + 1);
55
+ try {
56
+ const request = JSON.parse(line);
57
+ if (!request || typeof request !== 'object' || !/^[0-9]{1,16}$/.test(request.id) || ending) throw Error();
58
+ const keys = Object.keys(request).sort().join(',');
59
+ if (request.kind === 'input' && keys === 'data,id,kind' && typeof request.data === 'string' && Buffer.byteLength(request.data) <= 16384)
60
+ terminal.write(request.data);
61
+ else if (request.kind === 'resize' && keys === 'cols,id,kind,rows' && size(request.cols, request.rows)) terminal.resize(request.cols, request.rows);
62
+ else throw Error();
63
+ send({ kind: 'ack', id: request.id });
64
+ } catch { return process.exit(1); }
65
+ }
66
+ });
67
+ process.stdin.on('end', () => finish({ kind: 'exit', exitCode: null, signal: null }));
68
+ process.stdin.on('error', () => process.exit(1));
69
+ process.stdout.on('error', () => process.exit(1));
70
+ }