@piagent/platform 1.37.0 → 1.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/CHANGELOG.md +70 -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/helper-run.mjs +11 -1
  116. package/packages/piagent-core/managed/helper-settlement.mjs +17 -6
  117. package/packages/piagent-core/managed/ip-policy.mjs +50 -0
  118. package/packages/piagent-core/managed/memory-lessons.mjs +4 -1
  119. package/packages/piagent-core/managed/memory.mjs +1 -1
  120. package/packages/piagent-core/managed/package-proxy.mjs +12 -14
  121. package/packages/piagent-core/managed/patch-snapshot.mjs +6 -8
  122. package/packages/piagent-core/managed/permission.mjs +29 -6
  123. package/packages/piagent-core/managed/repository-operation.mjs +2 -1
  124. package/packages/piagent-core/managed/review-snapshot-request.mjs +21 -0
  125. package/packages/piagent-core/managed/sandbox-diagnostic.mjs +2 -0
  126. package/packages/piagent-core/managed/session-mutation-observations.mjs +37 -0
  127. package/packages/piagent-core/managed/session.mjs +6 -7
  128. package/packages/piagent-core/managed/skill-feedback.mjs +9 -1
  129. package/packages/piagent-core/managed/source-revert-requests.mjs +21 -0
  130. package/packages/piagent-core/managed/studio-link.mjs +16 -0
  131. package/packages/piagent-core/managed/terminal-boundary.mjs +88 -0
  132. package/packages/piagent-core/managed/terminal-channel.mjs +66 -0
  133. package/packages/piagent-core/managed/terminal-runtime.mjs +64 -0
  134. package/packages/piagent-core/managed/terminal-seatbelt.mjs +48 -0
  135. package/packages/piagent-core/managed/terminal-worker.mjs +70 -0
  136. package/packages/piagent-core/managed/tool-boundary.mjs +45 -35
  137. package/packages/piagent-core/managed/tool-freshness.mjs +47 -0
  138. package/packages/piagent-core/managed/tool-worker.mjs +18 -3
  139. package/packages/piagent-core/managed/web-fetch.mjs +3 -14
  140. package/packages/piagent-core/managed/workspace-worker-pins.mjs +26 -0
  141. package/packages/piagent-core/package.json +1 -1
  142. package/packages/piagent-core/policies/base-policy.json +2 -1
  143. package/packages/piagent-core/runtime/checkpoints/checkpoint-authority.ts +43 -0
  144. package/packages/piagent-core/runtime/checkpoints/checkpoint-refs.mjs +51 -0
  145. package/packages/piagent-core/runtime/checkpoints/checkpoint-store.mjs +179 -0
  146. package/packages/piagent-core/runtime/checkpoints/checkpoint-worker.ts +28 -0
  147. package/packages/piagent-core/runtime/checkpoints/git-adapter.mjs +36 -0
  148. package/packages/piagent-core/runtime/checkpoints/persistent-history.mjs +96 -0
  149. package/packages/piagent-core/runtime/checkpoints/personal-git.ts +272 -0
  150. package/packages/piagent-core/runtime/checkpoints/personal-runtime.ts +158 -0
  151. package/packages/piagent-core/runtime/checkpoints/prune.mjs +29 -0
  152. package/packages/piagent-core/runtime/checkpoints/restore-preview.mjs +46 -0
  153. package/packages/piagent-core/runtime/checkpoints/turn-checkpoints.mjs +167 -0
  154. package/packages/piagent-core/runtime/hooks/session-hooks.ts +19 -16
  155. package/packages/piagent-core/runtime/hooks/session-start-hook.ts +3 -0
  156. package/packages/piagent-core/runtime/hooks/tool-call-hook.ts +2 -2
  157. package/packages/piagent-core/runtime/inspection/diff-projection.ts +2 -1
  158. package/packages/piagent-core/runtime/inspection/managed-source-revert.ts +90 -0
  159. package/packages/piagent-core/runtime/inspection/review-state-projection.ts +19 -8
  160. package/packages/piagent-core/runtime/inspection/session-diff-projection.ts +47 -0
  161. package/packages/piagent-core/runtime/inspection/session-hunk-provenance.ts +56 -0
  162. package/packages/piagent-core/runtime/inspection/session-mutation-observer.ts +125 -0
  163. package/packages/piagent-core/runtime/inspection/source-action-contract.ts +123 -0
  164. package/packages/piagent-core/runtime/inspection/source-change-projection.ts +3 -9
  165. package/packages/piagent-core/runtime/inspection/source-mutation-projection.ts +3 -41
  166. package/packages/piagent-core/runtime/inspection/source-mutation-store.ts +6 -36
  167. package/packages/piagent-core/runtime/inspection/source-revert-projection.ts +36 -30
  168. package/packages/piagent-core/runtime/inspection/text-diff.ts +5 -4
  169. package/packages/piagent-core/runtime/inspection/workspace-worker.ts +79 -0
  170. package/packages/piagent-core/runtime/policy/source-index-transaction.ts +3 -10
  171. package/packages/piagent-core/runtime/policy/source-mutation-guard-binding.ts +27 -2
  172. package/packages/piagent-core/runtime/policy/source-mutation-guard.ts +42 -12
  173. package/packages/piagent-core/runtime/policy/source-worktree-transaction.ts +10 -5
  174. package/packages/piagent-core/runtime/policy/workspace-write-authority.ts +51 -0
  175. package/packages/piagent-core/runtime/process-owner.mjs +20 -0
  176. package/packages/piagent-core/runtime/process-tree-watch.mjs +46 -0
  177. package/packages/piagent-core/runtime/quality/model-mutation-proof.ts +8 -5
  178. package/packages/piagent-core/runtime/registration/permission-commands.ts +10 -2
  179. package/packages/piagent-core/runtime/registration/task-evidence-tools.ts +1 -1
  180. package/packages/piagent-core/runtime/session/workspace-member-edits.d.mts +7 -0
  181. package/packages/piagent-core/runtime/session/workspace-member-edits.mjs +41 -0
  182. package/packages/piagent-core/security/workspace-file-reader.ts +2 -1
  183. package/packages/piagent-webui/dist/client/assets/Alert-DXF8-XqE.js +1 -0
  184. package/packages/piagent-webui/dist/client/assets/Button-DZJosXQi.js +1 -0
  185. package/packages/piagent-webui/dist/client/assets/DialogTitle-CL-fzCnT.js +3 -0
  186. package/packages/piagent-webui/dist/client/assets/MarkdownMessage-k4tq1y8s.js +29 -0
  187. package/packages/piagent-webui/dist/client/assets/TerminalPanel-DvOPgrGI.js +2 -0
  188. package/packages/piagent-webui/dist/client/assets/TerminalView-B_a16moC.js +9 -0
  189. package/packages/piagent-webui/dist/client/assets/TerminalView-kHJ-D0s7.css +1 -0
  190. package/packages/piagent-webui/dist/client/assets/Typography-DMZL1kLS.js +1 -0
  191. package/packages/piagent-webui/dist/client/assets/WorkspaceCodeEditor-BCiYM9NA.js +2 -0
  192. package/packages/piagent-webui/dist/client/assets/WorkspaceExplorer-DttpQtpf.js +5 -0
  193. package/packages/piagent-webui/dist/client/assets/WorkspaceQuickOpen-fgXwQ46B.js +1 -0
  194. package/packages/piagent-webui/dist/client/assets/bootstrap-CedkNtRG.js +1 -0
  195. package/packages/piagent-webui/dist/client/assets/dist-B1M7IjfA.js +10 -0
  196. package/packages/piagent-webui/dist/client/assets/dist-BUYtGaUP.js +1 -0
  197. package/packages/piagent-webui/dist/client/assets/dist-C82AtDDT.js +6 -0
  198. package/packages/piagent-webui/dist/client/assets/dist-D9b8j6pJ.js +1 -0
  199. package/packages/piagent-webui/dist/client/assets/dist-DLfpWwLs.js +23 -0
  200. package/packages/piagent-webui/dist/client/assets/dist-DcEu_WYV.js +1 -0
  201. package/packages/piagent-webui/dist/client/assets/dist-_JsfplOG.js +9 -0
  202. package/packages/piagent-webui/dist/client/assets/dist-idN8f9Fc.js +1 -0
  203. package/packages/piagent-webui/dist/client/assets/dist-nvgpa2Us.js +1 -0
  204. package/packages/piagent-webui/dist/client/assets/index-BW8-BUMx.js +102 -0
  205. package/packages/piagent-webui/dist/client/assets/index-Brs_HjIo.css +1 -0
  206. package/packages/piagent-webui/dist/client/assets/rolldown-runtime-hePW80VL.js +1 -0
  207. package/packages/piagent-webui/dist/client/assets/tab-close-HE0XxoJf.js +1 -0
  208. package/packages/piagent-webui/dist/client/assets/ui-preferences-DiddStu0.js +124 -0
  209. package/packages/piagent-webui/dist/client/assets/useTheme-_ESOj7MI.js +10 -0
  210. package/packages/piagent-webui/dist/client/assets/workspace-api-O7auv_td.js +1 -0
  211. package/packages/piagent-webui/dist/client/index.html +10 -2
  212. package/packages/piagent-webui/extension/same-session-bridge.ts +2 -3
  213. package/packages/piagent-webui/extension/source-revert-controller.ts +14 -208
  214. package/packages/piagent-webui/gateway/checkpoint-sessions.ts +99 -0
  215. package/packages/piagent-webui/gateway/checkpoint-store.ts +6 -0
  216. package/packages/piagent-webui/gateway/company-relay.ts +5 -0
  217. package/packages/piagent-webui/gateway/company-terminal-cache.ts +54 -0
  218. package/packages/piagent-webui/gateway/company-terminal-channel.ts +117 -0
  219. package/packages/piagent-webui/gateway/company-terminal-relay.ts +75 -0
  220. package/packages/piagent-webui/gateway/gateway-protocol-service.ts +12 -0
  221. package/packages/piagent-webui/gateway/gateway-service.ts +48 -4
  222. package/packages/piagent-webui/gateway/managed-gateway.mjs +30 -3
  223. package/packages/piagent-webui/gateway/managed-projection.ts +5 -3
  224. package/packages/piagent-webui/gateway/managed-source-revert-controller.ts +45 -0
  225. package/packages/piagent-webui/gateway/managed-source-revert-sessions.ts +74 -0
  226. package/packages/piagent-webui/gateway/managed-terminal-binding.ts +30 -0
  227. package/packages/piagent-webui/gateway/managed-terminal-processes.ts +74 -0
  228. package/packages/piagent-webui/gateway/managed-terminal-sessions.ts +9 -0
  229. package/packages/piagent-webui/gateway/path-suggestions.ts +1 -124
  230. package/packages/piagent-webui/gateway/personal-terminal-binding.ts +36 -0
  231. package/packages/piagent-webui/gateway/personal-terminal-diagnostic.mjs +16 -0
  232. package/packages/piagent-webui/gateway/personal-terminal-processes.ts +119 -0
  233. package/packages/piagent-webui/gateway/personal-terminal-sessions.ts +22 -0
  234. package/packages/piagent-webui/gateway/runtime-session-controls.ts +2 -1
  235. package/packages/piagent-webui/gateway/session-catalog.ts +3 -2
  236. package/packages/piagent-webui/gateway/session-command-controller.ts +4 -4
  237. package/packages/piagent-webui/gateway/session-inspection-registry.ts +29 -5
  238. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +8 -8
  239. package/packages/piagent-webui/gateway/signed-session-journal.ts +77 -0
  240. package/packages/piagent-webui/gateway/source-revert-sessions.ts +74 -0
  241. package/packages/piagent-webui/gateway/source-revert-store.ts +47 -0
  242. package/packages/piagent-webui/gateway/terminal-command-ledger.ts +33 -0
  243. package/packages/piagent-webui/gateway/terminal-mentions.ts +29 -0
  244. package/packages/piagent-webui/gateway/terminal-output-buffer.ts +58 -0
  245. package/packages/piagent-webui/gateway/terminal-router.ts +12 -0
  246. package/packages/piagent-webui/gateway/terminal-sessions.ts +247 -0
  247. package/packages/piagent-webui/gateway/workspace-write-sessions.ts +69 -0
  248. package/packages/piagent-webui/gateway/workspace-write-store.ts +6 -0
  249. package/packages/piagent-webui/package.json +14 -2
  250. package/packages/piagent-webui/server/checkpoint-command.ts +30 -0
  251. package/packages/piagent-webui/server/checkpoint-controller.ts +87 -0
  252. package/packages/piagent-webui/server/checkpoint-routes.ts +38 -0
  253. package/packages/piagent-webui/server/control-command-digest.ts +5 -0
  254. package/packages/piagent-webui/server/core-inspection-provider.ts +16 -22
  255. package/packages/piagent-webui/server/gateway-websocket.ts +36 -8
  256. package/packages/piagent-webui/server/loopback-server.ts +9 -0
  257. package/packages/piagent-webui/server/read-model-provider.ts +3 -0
  258. package/packages/piagent-webui/server/read-only-router.ts +3 -0
  259. package/packages/piagent-webui/server/session-auth.ts +4 -1
  260. package/packages/piagent-webui/server/session-source-revert-routes.ts +54 -0
  261. package/packages/piagent-webui/server/source-projection-binding.ts +18 -0
  262. package/packages/piagent-webui/server/source-revert-controller.ts +225 -0
  263. package/packages/piagent-webui/server/terminal-command.ts +35 -0
  264. package/packages/piagent-webui/server/terminal-websocket-stream.ts +56 -0
  265. package/packages/piagent-webui/server/transcript-projection.ts +2 -1
  266. package/packages/piagent-webui/server/workspace-read-routes.ts +32 -0
  267. package/packages/piagent-webui/server/workspace-reader.ts +23 -0
  268. package/packages/piagent-webui/server/workspace-write-controller.ts +118 -0
  269. package/packages/piagent-webui/server/workspace-write-routes.ts +38 -0
  270. package/packages/piagent-webui/shared/terminal-message.ts +23 -0
  271. package/schemas/piagent-webui/catalog-v1.json +63 -0
  272. package/schemas/piagent-webui/checkpoint-v1.schema.json +466 -0
  273. package/schemas/piagent-webui/control-command-v1.schema.json +68 -3
  274. package/schemas/piagent-webui/diff-v1.schema.json +4 -0
  275. package/schemas/piagent-webui/gateway-protocol-v1.schema.json +123 -3
  276. package/schemas/piagent-webui/session-catalog-v1.schema.json +1 -1
  277. package/schemas/piagent-webui/session-command-v1.schema.json +1 -1
  278. package/schemas/piagent-webui/snapshot-v1.schema.json +2 -1
  279. package/schemas/piagent-webui/source-revert-v1.schema.json +22 -3
  280. package/schemas/piagent-webui/terminal-v1.schema.json +495 -0
  281. package/schemas/piagent-webui/workspace-file-v1.schema.json +79 -0
  282. package/schemas/piagent-webui/workspace-paths-v1.schema.json +71 -0
  283. package/schemas/piagent-webui/workspace-search-v1.schema.json +80 -0
  284. package/schemas/piagent-webui/workspace-tree-v1.schema.json +87 -0
  285. package/schemas/piagent-webui/workspace-write-v1.schema.json +276 -0
  286. package/schemas/project-profile.schema.json +8 -3
  287. package/scripts/company-terminal.mjs +16 -11
  288. package/scripts/pi-auto.sh +5 -1
  289. package/scripts/team-doctor.sh +8 -0
  290. package/packages/piagent-webui/dist/client/assets/MarkdownMessage-CfgyQQbW.js +0 -29
  291. package/packages/piagent-webui/dist/client/assets/index-HyzWKvDu.js +0 -234
  292. package/packages/piagent-webui/dist/client/assets/index-_0nGP2n3.css +0 -1
@@ -0,0 +1,137 @@
1
+ import { inspectWorkspaceEntry } from "../security/workspace-file-reader.ts";
2
+ import { execFile } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ // What an @ in the composer offers, as Pi's terminal does: files and folders
8
+ // of the project matched loosely by name (fd, which honours .gitignore), and,
9
+ // after a folder and a slash (src/, ~/Documents/, /Volumes/), the names under
10
+ // that folder anywhere on this Mac. Only names reach the browser; no file is
11
+ // opened.
12
+ export type PathSuggestion = { value: string; label: string; detail: string; kind: "file" | "directory" };
13
+ type Entry = { path: string; directory: boolean };
14
+
15
+ const LIMIT = 20;
16
+ const SEARCH_RESULTS = 100;
17
+ const LISTED_ENTRIES = 5_000;
18
+ // Outside the project a search stays shallow and skips app data and
19
+ // dependency folders: a home folder is far larger than a repository.
20
+ const OUTSIDE_DEPTH = 4;
21
+ const fold = (value: string) => value.normalize("NFC").toLowerCase();
22
+ const escape = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23
+ // A name typed composed (Dự án) may be stored decomposed on disk: either form matches.
24
+ const either = (value: string) => value.normalize("NFC") === value.normalize("NFD") ? escape(value)
25
+ : `(?:${escape(value.normalize("NFC"))}|${escape(value.normalize("NFD"))})`;
26
+ const inside = (file: string, root: string) => file === root || file.startsWith(root + path.sep);
27
+
28
+ // fd where it is commonly installed, then on PATH (Debian and Ubuntu name it
29
+ // fdfind; Windows adds .exe).
30
+ export function findFd(home = os.homedir()): string | null {
31
+ const names = process.platform === "win32" ? ["fd.exe"] : ["fd", "fdfind"];
32
+ const onPath = String(process.env.PATH ?? "").split(path.delimiter).filter(Boolean).flatMap((dir) => names.map((name) => path.join(dir, name)));
33
+ for (const candidate of ["/opt/homebrew/bin/fd", "/usr/local/bin/fd", path.join(home, ".pi/agent/bin", names[0]), ...onPath]) {
34
+ try { fs.accessSync(candidate, fs.constants.X_OK); return candidate; } catch { /* next */ }
35
+ }
36
+ return null;
37
+ }
38
+ // macOS: the system's git, always there; elsewhere git from PATH.
39
+ const GIT = process.platform === "darwin" ? "/usr/bin/git" : "git";
40
+
41
+ // Higher is better: the name itself, then its start, then anywhere in it,
42
+ // then anywhere in the path; a folder before a file with the same score.
43
+ function score(entry: string, query: string, directory: boolean): number {
44
+ const name = fold(path.basename(entry)), wanted = fold(query);
45
+ const value = name === wanted ? 100 : name.startsWith(wanted) ? 80 : name.includes(wanted) ? 50 : fold(entry).includes(wanted) ? 30 : 0;
46
+ return value && directory ? value + 10 : value;
47
+ }
48
+
49
+ function run(file: string, args: string[], signal?: AbortSignal, input?: string): Promise<string> {
50
+ return new Promise((resolve) => {
51
+ const child = execFile(file, args, { encoding: "utf8", timeout: 4_000, maxBuffer: 8 << 20, signal }, (error, stdout) => resolve(error ? "" : stdout));
52
+ child.stdin?.on("error", () => {}); child.stdin?.end(input ?? "");
53
+ });
54
+ }
55
+
56
+ // The folder's own names, without what its repository ignores (dist/,
57
+ // node_modules/), as fd lists them for Pi.
58
+ async function listed(folder: string, signal?: AbortSignal, projectOnly = false): Promise<Entry[]> {
59
+ const entries: Entry[] = [];
60
+ try {
61
+ for await (const entry of await fs.promises.opendir(folder)) {
62
+ let directory = entry.isDirectory();
63
+ if (entry.isSymbolicLink() && projectOnly) continue;
64
+ if (entry.isSymbolicLink()) try { directory = fs.statSync(path.join(folder, entry.name)).isDirectory(); } catch { /* broken link */ }
65
+ entries.push({ path: entry.name, directory });
66
+ if (entries.length >= LISTED_ENTRIES) break;
67
+ }
68
+ } catch { /* unreadable */ }
69
+ if (!entries.length) return entries;
70
+ const ignored = new Set((await run(GIT, ["-C", folder, "check-ignore", "-z", "--stdin"], signal, entries.map((entry) => `${entry.path}\0`).join("")))
71
+ .split("\0").filter(Boolean));
72
+ return ignored.size ? entries.filter((entry) => !ignored.has(entry.path)) : entries;
73
+ }
74
+
75
+ // Names deeper down that match: fd as Pi runs it in the project; shallow and
76
+ // without hidden folders elsewhere; without fd, the files git knows about.
77
+ async function searched(folder: string, query: string, options: { fd: string | null; project: boolean; atRoot: boolean; projectOnly?: boolean; signal?: AbortSignal }): Promise<Entry[]> {
78
+ if (options.fd) {
79
+ const args = ["--base-directory", folder, "--type", "f", "--type", "d", "--max-results", String(SEARCH_RESULTS), "--ignore-case", "--exclude", ".git",
80
+ ...(options.project ? [...(options.projectOnly ? [] : ["--follow"]), "--hidden"] : ["--max-depth", String(OUTSIDE_DEPTH), "--exclude", "Library", "--exclude", "node_modules", ...(query.startsWith(".") ? ["--hidden"] : [])]),
81
+ ...(query.includes("/") ? ["--full-path", query.split("/").filter(Boolean).map(either).join("/")] : [either(query)])];
82
+ return (await run(options.fd, args, options.signal)).split("\n").filter(Boolean)
83
+ .map((line) => ({ path: line.replace(/^\.\//, "").replace(/\/$/, ""), directory: line.endsWith("/") }));
84
+ }
85
+ if (!options.atRoot) return [];
86
+ const files = (await run(GIT, ["-C", folder, "ls-files", "--cached", "--others", "--exclude-standard", "-z"], options.signal))
87
+ .split("\0").filter(Boolean).slice(0, 50_000);
88
+ const folders = new Set<string>();
89
+ for (const file of files) for (let dir = path.posix.dirname(file); dir !== "."; dir = path.posix.dirname(dir)) folders.add(dir);
90
+ const wanted = fold(query);
91
+ return [...[...folders].map((dir) => ({ path: dir, directory: true })), ...files.map((file) => ({ path: file, directory: false }))]
92
+ .filter((entry) => fold(entry.path).includes(wanted));
93
+ }
94
+
95
+ export async function suggestPaths(options: { root: string; query: string; home?: string; fd?: string | null; projectOnly?: boolean; isAllowed?: (path: string) => boolean; signal?: AbortSignal }): Promise<{ suggestions: PathSuggestion[] }> {
96
+ const home = options.home ?? os.homedir(), fd = options.fd === undefined ? findFd(home) : options.fd;
97
+ const root = fs.realpathSync(options.root);
98
+ const typed = options.query.trim() === "~" ? "~/" : options.query.trim();
99
+ // Text up to the last slash names a folder: search under it (Pi's scoped
100
+ // search). When it is not a folder, the whole text is matched in the project.
101
+ if (options.projectOnly && (path.isAbsolute(typed) || typed.startsWith("~") || /[\\\x00-\x1f]/.test(typed)
102
+ || typed.split("/").includes(".."))) return { suggestions: [] };
103
+ const cut = typed.lastIndexOf("/") + 1;
104
+ let base = typed.slice(0, cut), name = typed.slice(cut), folder = root;
105
+ if (base) {
106
+ try {
107
+ const candidate = fs.realpathSync(base.startsWith("~/") ? path.join(home, base.slice(2)) : path.resolve(root, base));
108
+ if (fs.statSync(candidate).isDirectory()) folder = candidate; else { base = ""; name = typed; }
109
+ } catch { base = ""; name = typed; }
110
+ }
111
+ const project = inside(folder, root);
112
+ if (options.projectOnly && (!inside(folder, root) || (base && (() => {
113
+ try { return inspectWorkspaceEntry(root, base.replace(/\/$/, "")).isSymbolicLink(); } catch { return true; }
114
+ })()))) return { suggestions: [] };
115
+ const near = await listed(folder, options.signal, options.projectOnly);
116
+ const deep = name ? await searched(folder, name, { fd, project, atRoot: folder === root, projectOnly: options.projectOnly, signal: options.signal }) : [];
117
+ const seen = new Set<string>(), entries: Array<Entry & { score: number }> = [];
118
+ for (const entry of [...near, ...deep]) {
119
+ if (options.projectOnly) {
120
+ try {
121
+ const relative = `${base}${entry.path}`;
122
+ if (inspectWorkspaceEntry(root, relative).isSymbolicLink() || options.isAllowed && !options.isAllowed(relative)) continue;
123
+ } catch { continue; }
124
+ }
125
+ if (seen.has(entry.path)) continue; seen.add(entry.path);
126
+ // Hidden entries (.github, .ssh, .zshrc) for a name typed with a dot, and
127
+ // in the project for any name typed, as Pi's search finds them there.
128
+ if (entry.path.split("/").some((part) => part.startsWith(".")) && !name.startsWith(".") && !(project && name)) continue;
129
+ const value = name ? score(entry.path, name.split("/").filter(Boolean).at(-1) ?? name, entry.directory) : 1;
130
+ if (value > 0) entries.push({ ...entry, score: value });
131
+ }
132
+ const depth = (entry: Entry) => entry.path.split("/").length;
133
+ entries.sort((a, b) => b.score - a.score || depth(a) - depth(b)
134
+ || (name ? a.path.length - b.path.length : Number(b.directory) - Number(a.directory)) || a.path.localeCompare(b.path));
135
+ return { suggestions: entries.slice(0, LIMIT).map((entry) => ({ value: `${base}${entry.path}${entry.directory ? "/" : ""}`,
136
+ label: path.basename(entry.path) + (entry.directory ? "/" : ""), detail: `${base}${entry.path}`, kind: entry.directory ? "directory" : "file" })) };
137
+ }
@@ -1,3 +1,4 @@
1
+ import { sessionMutationObserver } from "../runtime/inspection/session-mutation-observer.ts";
1
2
  import { unresolvedFocusedVerification } from "./acceptance-focused-verification.js";
2
3
  import fs from "node:fs";
3
4
  import os from "node:os";
@@ -21,10 +22,10 @@ import {
21
22
  import { grantedSourceCheckoutRootForPath, piToolPathInput, referenceReadRootForPath, shellTouchesGrantedSourceCheckout } from "./source-checkout-paths.ts";
22
23
  import { experimentLoopDecision } from "./experiment-loop-policy.ts";
23
24
  import { extractShellWritePathCandidates } from "./shell-write-targets.js";
24
- import { isProjectMutatingShellCommand } from "./readonly-inline-inspection.ts";
25
+ import { isProjectMutatingShellCommand, isReadOnlyTaskShellCommand } from "./readonly-inline-inspection.ts";
25
26
  import { commandMatchesVerifyPlan, createBashResultLedger, findMatchingObservedBashResult, readObservedBashResults } from "./runtime-evidence.js";
26
27
  import { findPackageRoot, findPlatformRoot, readJsonFile } from "./guard-io.js";
27
- import { registerCompanySessionGuard } from "./company-session-guard.ts";
28
+ import { isCompanySession, registerCompanySessionGuard } from "./company-session-guard.ts";
28
29
  import {
29
30
  extractDocument,
30
31
  resolveDocumentPath,
@@ -93,6 +94,8 @@ import { ensurePrivateStateDirectory, resolveLocalStatePath } from "./local-stat
93
94
  import { hasDurableContextEvidence } from "./context-evidence.js";
94
95
  import { piApprovalBroker, type ApprovalActionDraft, type ApprovalUnavailableFallback } from "../runtime/inspection/approval-broker.ts";
95
96
  import { createSourceMutationGuardBindings } from "../runtime/policy/source-mutation-guard-binding.ts";
97
+ import { createWorkspaceWriteAuthorityBindings } from "../runtime/policy/workspace-write-authority.ts";
98
+ import { registerWorkspaceMemberEdits } from "../runtime/session/workspace-member-edits.mjs";
96
99
  import {
97
100
  TOOL_RESULT_CAPTURE_MAX_CHARS,
98
101
  TOOL_RESULT_COMPACT_CHAR_THRESHOLD,
@@ -136,6 +139,8 @@ import { registerAgentStartHook } from "../runtime/hooks/agent-start-hook.ts";
136
139
  import { registerSessionHooks } from "../runtime/hooks/session-hooks.ts";
137
140
  import { registerSessionStartHook } from "../runtime/hooks/session-start-hook.ts";
138
141
  import { registerToolCallHook } from "../runtime/hooks/tool-call-hook.ts";
142
+ import { PersonalCheckpointRuntime } from "../runtime/checkpoints/personal-runtime.ts";
143
+ import { checkpointOwnershipError } from "../runtime/checkpoints/turn-checkpoints.mjs";
139
144
  import { registerToolResultHook } from "../runtime/hooks/tool-result-hook.ts";
140
145
  import { readRuntimeVersionMetadata, RuntimeSnapshotCapture } from "../runtime/model/runtime-snapshot.ts";
141
146
  import { recordRuntimeSnapshotTelemetry } from "../runtime/model/snapshot-telemetry.ts";
@@ -339,7 +344,12 @@ const PERMISSION_PROFILE_ALIASES: Record<string, PermissionProfileMode> = {
339
344
  "trusted-full-access": "trusted-full-access",
340
345
  "trusted_full_access": "trusted-full-access",
341
346
  "danger-full-access": "trusted-full-access",
342
- "danger_full_access": "trusted-full-access"
347
+ "danger_full_access": "trusted-full-access",
348
+ bypass: "bypass",
349
+ yolo: "bypass",
350
+ "dangerously-bypass": "bypass",
351
+ "danger-bypass": "bypass",
352
+ "dangerously-bypass-approvals-and-sandbox": "bypass"
343
353
  };
344
354
  const READ_ONLY_TOOL_NAMES = new Set(["read", "grep", "find", "ls"]);
345
355
  const WRITE_TOOL_NAMES = new Set(["write", "edit"]);
@@ -1121,6 +1131,7 @@ function normalizePermissionProfileMode(value: unknown): PermissionProfileMode |
1121
1131
  function runtimeEquivalentForPermissionProfile(mode: PermissionProfileMode): string {
1122
1132
  if (mode === "read-only") return "sandbox_mode=read-only";
1123
1133
  if (mode === "trusted-full-access") return "sandbox_mode=danger-full-access + approval_policy=never, with Piagent protected-path and human-action gates still enforced";
1134
+ if (mode === "bypass") return "sandbox_mode=danger-full-access + approval_policy=never; every Piagent gate off (output redaction and turn checkpoints remain)";
1124
1135
  return "sandbox_mode=workspace-write + approval_policy=on-request";
1125
1136
  }
1126
1137
 
@@ -1198,6 +1209,9 @@ function isPiagentTool(toolName: string): boolean {
1198
1209
  return toolName.startsWith("piagent_");
1199
1210
  }
1200
1211
 
1212
+ // Full access or more: the profile does not narrow reads or the tool registry.
1213
+ const broadAccess = (mode: PermissionProfileMode) => mode === "trusted-full-access" || mode === "bypass";
1214
+
1201
1215
  function evaluatePermissionProfileToolAccess(
1202
1216
  toolName: string,
1203
1217
  permissionProfile: ResolvedPermissionProfile
@@ -3453,7 +3467,29 @@ export default function piagentGuard(pi: ExtensionAPI) {
3453
3467
  // task-contract retirement, so it uses one session-wide key; the guard itself
3454
3468
  // is already per session.
3455
3469
  const FREEFORM_EDIT_SCOPE = "freeform";
3456
- const sourceMutationGuardBindings = createSourceMutationGuardBindings(policy, loadProfileFromContext);
3470
+ const workspaceWritable = (ctx: ExtensionContext, profile: ProjectProfile) => {
3471
+ const permission = resolvePermissionProfile(profile, policy, permissionOverrideFromContext(ctx));
3472
+ return permission.mode !== "read-only" && !permission.warning;
3473
+ };
3474
+ const sourceMutationGuardBindings = createSourceMutationGuardBindings(policy, loadProfileFromContext, workspaceWritable);
3475
+ registerWorkspaceMemberEdits(pi);
3476
+ const workspaceWriteBindings = createWorkspaceWriteAuthorityBindings(policy, loadProfileFromContext, workspaceWritable);
3477
+ const personalCheckpoints = new PersonalCheckpointRuntime(ctx => ({
3478
+ policy: policy as unknown as Record<string, unknown>,
3479
+ profile: loadProfileFromContext(ctx) as unknown as Record<string, unknown>,
3480
+ readOnly: !workspaceWritable(ctx, loadProfileFromContext(ctx))
3481
+ }));
3482
+ registerRuntimeCommand(pi, "undo", {
3483
+ description: "Hoàn tác thay đổi của lượt trước; giữ nguyên commit.",
3484
+ handler: async (_args, ctx) => {
3485
+ try {
3486
+ const undo = await personalCheckpoints.undo(ctx);
3487
+ emitRuntimeMessage(ctx, "agent-watch-process", undo.text, { phase: "undo", undo });
3488
+ } catch (error) {
3489
+ emitRuntimeMessage(ctx, "agent-watch-process", `Không hoàn tác: ${redactText(String(error))}`, { phase: "undo", undo: { result: "failed" } });
3490
+ }
3491
+ }
3492
+ });
3457
3493
  const runtimeSnapshotCapture = new RuntimeSnapshotCapture(), runtimeVersions = readRuntimeVersionMetadata(PLATFORM_ROOT);
3458
3494
  const solverShadow = solverMode === "off" ? undefined : new SolverShadowRuntime(solverMode);
3459
3495
  const modelRouteRuntime = new ModelRouteRuntime(parentRoutingMode, routingObjective);
@@ -3699,6 +3735,8 @@ export default function piagentGuard(pi: ExtensionAPI) {
3699
3735
  ctx.ui.notify("Piagent Fast mode is enabled, but the active provider is not OpenAI Codex; no service tier was changed.", "warning");
3700
3736
  }
3701
3737
  sourceMutationGuardBindings.bind(ctx);
3738
+ workspaceWriteBindings.bind(ctx);
3739
+ personalCheckpoints.bind(ctx);
3702
3740
  await activityInspector.refresh(ctx);
3703
3741
  }
3704
3742
  });
@@ -3713,13 +3751,18 @@ export default function piagentGuard(pi: ExtensionAPI) {
3713
3751
  appendTrace,
3714
3752
  flushObservedTaskContext: noTask,
3715
3753
  onTurnEnd: activityInspector.refresh,
3716
- onAgentSettled: activityInspector.refresh,
3754
+ onAgentSettled: async ctx => { await personalCheckpoints.finish(ctx); await activityInspector.refresh(ctx); },
3717
3755
  beforeShutdown: async (ctx) => {
3756
+ try { await personalCheckpoints.close(ctx); }
3757
+ finally {
3718
3758
  serviceTierRuntime.forget(ctx);
3719
3759
  sourceMutationGuardBindings.unbind(ctx);
3760
+ workspaceWriteBindings.unbind(ctx);
3761
+ sessionMutationObserver.clear(ctx.cwd, ctx.sessionManager.getSessionId());
3720
3762
  editFreshnessGuard.clear(ctx);
3721
3763
  activityInspector.dispose(ctx);
3722
3764
  sessionCapabilityDigests.delete(`${ctx.cwd}\0${ctx.sessionManager.getSessionId()}`);
3765
+ }
3723
3766
  }
3724
3767
  });
3725
3768
  registerTaskUsageHooks(pi, { activeTask: noTask, telemetry });
@@ -3742,7 +3785,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
3742
3785
  }),
3743
3786
  readProtectedPaths: effectiveProtectedPaths(policy, profile).readProtectedPaths,
3744
3787
  filesystemRead: capabilityState.filesystemRead,
3745
- enforceFilesystemRead: permissionProfile.mode !== "trusted-full-access"
3788
+ enforceFilesystemRead: !broadAccess(permissionProfile.mode)
3746
3789
  };
3747
3790
  },
3748
3791
  activateToolGroups,
@@ -3795,6 +3838,8 @@ export default function piagentGuard(pi: ExtensionAPI) {
3795
3838
  disposition: result.disposition, reasonCode: result.reasonCode, calls: result.calls, tokens: result.tokens });
3796
3839
  },
3797
3840
  observeEditFreshness: (ctx, event, metadata) => {
3841
+ sessionMutationObserver.finish(ctx.cwd, ctx.sessionManager.getSessionId(), event.toolCallId ?? "", event.toolName, metadata.successful,
3842
+ { policy: policy as unknown as Record<string, unknown>, profile: loadProfileFromContext(ctx) as unknown as Record<string, unknown> });
3798
3843
  if (!metadata.successful) return;
3799
3844
  const protectedPaths = effectiveProtectedPaths(policy, loadProfileFromContext(ctx)).readProtectedPaths;
3800
3845
  const source = event.toolName === "read" ? "read" : ["edit", "write", "apply_patch"].includes(event.toolName) ? "mutation" : undefined;
@@ -3805,11 +3850,35 @@ export default function piagentGuard(pi: ExtensionAPI) {
3805
3850
  },
3806
3851
  });
3807
3852
 
3853
+ // Bypass is for personal sessions only: a company session keeps Studio's rules.
3854
+ const bypassActive = (ctx: ExtensionContext) => !isCompanySession(ctx) && ctx.model?.provider !== "agent_watch_managed"
3855
+ && resolvePermissionProfile(loadProfileFromContext(ctx), policy, permissionOverrideFromContext(ctx)).mode === "bypass";
3808
3856
  registerToolCallHook(pi, {
3809
3857
  extractLikelyPath: extractLikelyPathFromInput,
3810
3858
  redactText,
3811
3859
  telemetry,
3812
3860
  activity: recordActivity,
3861
+ beforeStart: async (event, ctx) => {
3862
+ const input = isPlainRecord(event.input) ? event.input : {};
3863
+ const target = authorizationMutationTargetInspection(ctx.cwd, event.toolName, input);
3864
+ let mutating = ["write", "edit", "apply_patch"].includes(event.toolName) || target.exactLocalPatchExecutor;
3865
+ if (SHELL_TOOL_NAMES.has(event.toolName)) {
3866
+ const shell = extractShellCommandInput(input);
3867
+ if (shell.command) {
3868
+ // Anything not provably read-only may change files (`prettier --write`,
3869
+ // `git stash`, a generator): it gets the turn's checkpoint first.
3870
+ const command = normalizeShellCommandForPolicy(shell.command), segments = evaluateExecPolicy(command, loadProfileFromContext(ctx), policy).segments;
3871
+ mutating ||= isProjectMutatingShellCommand(command, segments, ctx.cwd) || !isReadOnlyTaskShellCommand(command, segments, ctx.cwd);
3872
+ }
3873
+ }
3874
+ if (!mutating) return;
3875
+ // A missing checkpoint costs this turn its undo, never the edit itself;
3876
+ // only another live owner of the session's history makes the edit wait.
3877
+ try { await personalCheckpoints.beforeMutation(ctx); }
3878
+ catch (error) {
3879
+ if (checkpointOwnershipError(error)) return { block: true, reason: `Checkpoint failed before tool start: ${redactText(String(error))}` };
3880
+ }
3881
+ },
3813
3882
  beforeAuthorize: (event, ctx) => {
3814
3883
  const toolInput = isPlainRecord(event.input) ? event.input : {};
3815
3884
  const helperPreflight = evaluateDirectSubagentDispatch({ toolName: event.toolName, toolInput, cwd: ctx.cwd, helpersMode: helpersMode(),
@@ -3819,9 +3888,10 @@ export default function piagentGuard(pi: ExtensionAPI) {
3819
3888
  role: helperPreflight.role, requestCount: helperPreflight.requestCount });
3820
3889
  return { block: true, reason: `Blocked ${event.toolName}: ${helperPreflight.reason} Do not retry this helper; continue in the parent.` };
3821
3890
  }
3891
+ if (bypassActive(ctx)) return;
3822
3892
  const targetInspection = authorizationMutationTargetInspection(ctx.cwd, event.toolName, toolInput);
3823
3893
  if (targetInspection.reason) return { block: true, reason: `Blocked ${event.toolName}: ${targetInspection.reason}.` };
3824
- if (event.toolName === "edit" || targetInspection.exactLocalPatchExecutor) {
3894
+ if (event.toolName === "edit" || event.toolName === "write" || targetInspection.exactLocalPatchExecutor) {
3825
3895
  const freshness = editFreshnessGuard.evaluate(ctx, FREEFORM_EDIT_SCOPE, targetInspection.targets);
3826
3896
  if (freshness.decision === "stale") {
3827
3897
  telemetry(ctx, { event: "edit_freshness_stale", toolCallId: event.toolCallId, enforce: freshness.enforce, paths: freshness.stalePaths.map(redactText) });
@@ -3831,6 +3901,10 @@ export default function piagentGuard(pi: ExtensionAPI) {
3831
3901
  return;
3832
3902
  },
3833
3903
  afterAuthorized: (event, ctx, metadata) => {
3904
+ const input = isPlainRecord(event.input) ? event.input : {};
3905
+ sessionMutationObserver.begin(ctx.cwd, ctx.sessionManager.getSessionId(), metadata.toolCallId, event.toolName, input,
3906
+ authorizationMutationTargetInspection(ctx.cwd, event.toolName, input).targets,
3907
+ { policy: policy as unknown as Record<string, unknown>, profile: loadProfileFromContext(ctx) as unknown as Record<string, unknown> });
3834
3908
  const helperPreflight = evaluateDirectSubagentDispatch({ toolName: event.toolName, toolInput: event.input, cwd: ctx.cwd, helpersMode: helpersMode(),
3835
3909
  taskPending: false, dispatchAuthority: false, durableSubagentState: undefined });
3836
3910
  if (helperPreflight.applicable && helperPreflight.dispatch && helperPreflight.allowed) {
@@ -3840,6 +3914,10 @@ export default function piagentGuard(pi: ExtensionAPI) {
3840
3914
  return undefined;
3841
3915
  },
3842
3916
  authorize: async (event, ctx) => {
3917
+ // Bypass: no Piagent gate asks or blocks, as with Codex's
3918
+ // --dangerously-bypass-approvals-and-sandbox. Tool output is still
3919
+ // redacted and the turn still gets its checkpoint; neither stops a command.
3920
+ if (bypassActive(ctx)) return;
3843
3921
  const preInput = isPlainRecord(event.input) ? event.input : {};
3844
3922
  const authorizationTargetInspection = authorizationMutationTargetInspection(ctx.cwd, event.toolName, preInput);
3845
3923
  if (authorizationTargetInspection.reason) {
@@ -0,0 +1,145 @@
1
+ import { suggestPaths } from "./path-suggestions.ts";
2
+ import { createHash } from "node:crypto";
3
+ import { execFileSync } from "node:child_process";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+
7
+ import { effectiveProtectedPaths, matchesProtectedPath } from "./workspace-policy.js";
8
+ import { inspectWorkspaceEntry, readWorkspaceFile, WorkspaceReadError } from "../security/workspace-file-reader.ts";
9
+
10
+ export type WorkspacePolicy = { policy: Record<string, unknown>; profile: Record<string, unknown>; readOnly?: boolean };
11
+ export type WorkspaceEntry = { name: string; path: string; kind: "file" | "directory" | "symlink" | "other";
12
+ protected: boolean; readOnly: boolean; ignored: boolean };
13
+ const MAX_ENTRIES = 1_000, MAX_FILE_BYTES = 1 << 20;
14
+ const git = process.platform === "darwin" ? "/usr/bin/git" : "git";
15
+
16
+ function validPath(value: string, rootAllowed = false): void {
17
+ if (rootAllowed && value === "") return;
18
+ if (!value || value.length > 4096 || /[\\\x00-\x1f\x7f]/.test(value) || path.isAbsolute(value)
19
+ || value.split("/").some(part => !part || part === "." || part === "..")) {
20
+ throw new WorkspaceReadError("outside-root", "Workspace path is not a safe relative path");
21
+ }
22
+ }
23
+
24
+ // Ancestors inherit restrictions too: a protected folder cannot be opened via
25
+ // a direct file URL that avoids the explorer's locked entry.
26
+ export function workspacePathFlags(value: string, options: WorkspacePolicy): { protected: boolean; readOnly: boolean } {
27
+ const effective = effectiveProtectedPaths(options.policy, options.profile);
28
+ const parts = value.split("/");
29
+ const candidates = parts.map((_, index) => parts.slice(0, index + 1).join("/"));
30
+ return {
31
+ protected: candidates.some(candidate => !!matchesProtectedPath(candidate, effective.readProtectedPaths)),
32
+ readOnly: !!options.readOnly || candidates.some(candidate => !!matchesProtectedPath(candidate, effective.writeProtectedPaths))
33
+ };
34
+ }
35
+
36
+ function directory(root: string, relative: string): { absolute: string; stat: fs.Stats } {
37
+ validPath(relative, true);
38
+ const absolute = relative ? path.join(root, relative) : root;
39
+ const stat = relative ? inspectWorkspaceEntry(root, relative) : fs.lstatSync(root);
40
+ if (stat.isSymbolicLink()) throw new WorkspaceReadError("symlink-ancestor", "Workspace directory is a symbolic link");
41
+ if (!stat.isDirectory()) throw new WorkspaceReadError("not-file", "Workspace entry is not a directory");
42
+ return { absolute, stat };
43
+ }
44
+
45
+ export function inspectWorkspaceTree(rootPath: string, relative: string, options: WorkspacePolicy, limit = MAX_ENTRIES) {
46
+ const root = fs.realpathSync(rootPath);
47
+ const folder = directory(root, relative);
48
+ if (relative && workspacePathFlags(relative, options).protected) throw new Error("workspace-path-protected");
49
+ const cap = Math.max(1, Math.min(MAX_ENTRIES, Number.isFinite(limit) ? Math.floor(limit) : MAX_ENTRIES));
50
+ const names = new Set<string>(), directories = new Set<string>();
51
+ let truncated = false, source: "git" | "directory" = "git";
52
+ let output: string | null = null;
53
+ try {
54
+ output = execFileSync(git, ["-C", folder.absolute, "ls-files", "--cached", "--others", "--exclude-standard", "-z", "--", "."],
55
+ { encoding: "utf8", timeout: 3000, maxBuffer: 4 << 20, stdio: ["ignore", "pipe", "ignore"] });
56
+ } catch (error) {
57
+ const failure = error as NodeJS.ErrnoException & { stdout?: string | Buffer };
58
+ if (failure.code === "ENOBUFS" && failure.stdout) {
59
+ const partial = String(failure.stdout);
60
+ output = partial.slice(0, partial.lastIndexOf("\0") + 1);
61
+ truncated = true;
62
+ } else {
63
+ let repository = false;
64
+ try { repository = execFileSync(git, ["-C", root, "rev-parse", "--is-inside-work-tree"],
65
+ { encoding: "utf8", timeout: 1000, stdio: ["ignore", "pipe", "ignore"] }).trim() === "true"; } catch { /* no repository */ }
66
+ // A Git timeout must not turn ignored paths into visible fallback entries.
67
+ if (repository) throw new Error("workspace-tree-unavailable");
68
+ }
69
+ }
70
+ if (output !== null) {
71
+ for (const entry of output.split("\0")) {
72
+ const name = entry.split("/")[0];
73
+ if (!name || name === ".git") continue;
74
+ names.add(name);
75
+ if (entry.includes("/")) directories.add(name);
76
+ if (names.size > cap) { truncated = true; break; }
77
+ }
78
+ } else {
79
+ // Even without Git, never materialize an unbounded directory listing.
80
+ source = "directory";
81
+ const handle = fs.opendirSync(folder.absolute);
82
+ try {
83
+ let entry: fs.Dirent | null;
84
+ while ((entry = handle.readSync())) {
85
+ if (entry.name === ".git") continue;
86
+ names.add(entry.name);
87
+ if (entry.isDirectory()) directories.add(entry.name);
88
+ if (names.size > cap) { truncated = true; break; }
89
+ }
90
+ } finally { handle.closeSync(); }
91
+ }
92
+ const entries: WorkspaceEntry[] = [];
93
+ for (const name of [...names].slice(0, cap)) {
94
+ const entryPath = relative ? `${relative}/${name}` : name;
95
+ try {
96
+ validPath(entryPath);
97
+ const flags = workspacePathFlags(entryPath, options);
98
+ if (flags.protected) {
99
+ entries.push({ name, path: entryPath, kind: directories.has(name) ? "directory" : "file", ...flags, ignored: false });
100
+ continue;
101
+ }
102
+ const stat = inspectWorkspaceEntry(root, entryPath);
103
+ const kind = stat.isSymbolicLink() ? "symlink" : stat.isDirectory() ? "directory" : stat.isFile() ? "file" : "other";
104
+ entries.push({ name, path: entryPath, kind, ...workspacePathFlags(entryPath, options), ignored: false });
105
+ } catch (error) {
106
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
107
+ throw error;
108
+ }
109
+ }
110
+ const after = directory(root, relative).stat;
111
+ if (after.dev !== folder.stat.dev || after.ino !== folder.stat.ino || after.mtimeMs !== folder.stat.mtimeMs) {
112
+ throw new WorkspaceReadError("changed-during-read", "Workspace directory changed during inspection");
113
+ }
114
+ entries.sort((a, b) => Number(b.kind === "directory") - Number(a.kind === "directory") || a.name.localeCompare(b.name));
115
+ return { path: relative, entries, truncated, source };
116
+ }
117
+
118
+ const LANGUAGES: Record<string, string> = { ts: "typescript", tsx: "tsx", js: "javascript", jsx: "jsx", mjs: "javascript", cjs: "javascript",
119
+ json: "json", md: "markdown", css: "css", html: "html", py: "python", rs: "rust", go: "go", sh: "shell", yaml: "yaml", yml: "yaml" };
120
+
121
+ export function inspectWorkspaceFile(root: string, relative: string, options: WorkspacePolicy) {
122
+ validPath(relative);
123
+ const flags = workspacePathFlags(relative, options);
124
+ const base = { path: relative, language: LANGUAGES[relative.split(".").at(-1)?.toLowerCase() ?? ""] ?? "text", ...flags,
125
+ binary: false, oversized: false, content: null as string | null, digest: null as string | null, revision: null as string | null };
126
+ // Do not stat or hash protected files: even their digest would expose data.
127
+ if (flags.protected) return base;
128
+ let bytes: Buffer;
129
+ try { bytes = readWorkspaceFile(root, relative, MAX_FILE_BYTES); }
130
+ catch (error) { if (error instanceof WorkspaceReadError && error.code === "oversized") return { ...base, oversized: true }; throw error; }
131
+ const digest = createHash("sha256").update(bytes).digest("hex");
132
+ let content: string;
133
+ try {
134
+ if (bytes.includes(0)) return { ...base, binary: true, digest, revision: `sha256:${digest}` };
135
+ content = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
136
+ } catch { return { ...base, binary: true, digest, revision: `sha256:${digest}` }; }
137
+ return { ...base, content, digest, revision: `sha256:${digest}` };
138
+ }
139
+
140
+ export async function inspectWorkspacePaths(root: string, query: string, options: WorkspacePolicy) {
141
+ if (query.length > 512 || /[\x00-\x1f\x7f]/.test(query)) throw new Error("workspace-query-invalid");
142
+ const result = await suggestPaths({ root, query, projectOnly: true,
143
+ isAllowed: relative => !workspacePathFlags(relative, options).protected });
144
+ return { query, ...result, truncated: result.suggestions.length >= 20 };
145
+ }
@@ -0,0 +1,7 @@
1
+ export function effectiveProtectedPaths(policy?: Record<string, unknown>, profile?: Record<string, unknown>): {
2
+ readProtectedPaths: string[]; writeProtectedPaths: string[]; shellProtectedPaths: string[]; readOnlyPaths: string[];
3
+ };
4
+ export function matchesProtectedPath(candidate: string, patterns: string[]): string | undefined;
5
+ export function loadProjectContextIndexPolicy(platformRoot: string, projectPath: string, options?: { profilePath?: string }): {
6
+ policy: Record<string, unknown>; profile: Record<string, unknown>; profilePath: string; profileSource: string; excludePatterns: string[];
7
+ };
@@ -0,0 +1,3 @@
1
+ // The typed workspace API shares the existing policy implementation.
2
+ export { effectiveProtectedPaths, loadProjectContextIndexPolicy } from "./context-index-policy.js";
3
+ export { matchesProtectedPath } from "./policy-core.js";
@@ -0,0 +1,8 @@
1
+ import { parentPort, workerData } from 'node:worker_threads';
2
+ import { searchContextIndexV2 } from './context-engine.js';
3
+ // Isolate synchronous SQLite work so the caller can enforce its deadline.
4
+ try {
5
+ const result = await searchContextIndexV2(workerData.root, workerData.query,
6
+ { excludePatterns: workerData.excludePatterns, limit: 50 });
7
+ parentPort.postMessage({ paths: result.results.map(row => row.path), available: result.status.exists });
8
+ } catch { parentPort.postMessage({ paths: [], available: false }); }
@@ -0,0 +1,74 @@
1
+ import { execFile } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { Worker } from "node:worker_threads";
5
+ import { inspectWorkspaceFile, workspacePathFlags, type WorkspacePolicy } from "./workspace-inspection.ts";
6
+ import { effectiveProtectedPaths } from "./workspace-policy.js";
7
+
8
+ export type WorkspaceMatch = { path: string; line: number; column: number; preview: string };
9
+ const TIMEOUT = 3000, LIMIT = 100;
10
+ function findRipgrep(): string | null {
11
+ for (const dir of String(process.env.PATH ?? "").split(path.delimiter)) {
12
+ const file = path.join(dir, process.platform === "win32" ? "rg.exe" : "rg");
13
+ try { fs.accessSync(file, fs.constants.X_OK); return file; } catch { /* next */ }
14
+ }
15
+ return null;
16
+ }
17
+
18
+ function ftsCandidates(root: string, query: string, excludePatterns: string[], timeout: number) {
19
+ return new Promise<{ paths: string[]; available: boolean; timedOut: boolean }>((resolve) => {
20
+ const worker = new Worker(new URL("./workspace-search-index.mjs", import.meta.url), { workerData: { root, query, excludePatterns } });
21
+ let settled = false;
22
+ const finish = (value: { paths: string[]; available: boolean; timedOut: boolean }) => {
23
+ if (settled) return; settled = true; clearTimeout(timer); void worker.terminate(); resolve(value);
24
+ };
25
+ const timer = setTimeout(() => finish({ paths: [], available: true, timedOut: true }), timeout);
26
+ worker.once("message", value => finish({ ...value, timedOut: false }));
27
+ worker.once("error", () => finish({ paths: [], available: false, timedOut: false }));
28
+ worker.once("exit", () => finish({ paths: [], available: false, timedOut: false }));
29
+ });
30
+ }
31
+
32
+ export async function searchWorkspace(root: string, query: string, options: WorkspacePolicy, config: { rg?: string | null; timeout?: number } = {}) {
33
+ if (!query.trim() || query.length > 512 || /[\x00-\x1f\x7f]/.test(query)) throw new Error("workspace-query-invalid");
34
+ const deadline = Date.now() + Math.max(1, Math.min(TIMEOUT, config.timeout ?? TIMEOUT));
35
+ const effective = effectiveProtectedPaths(options.policy, options.profile);
36
+ const excludePatterns = [...new Set([...effective.readProtectedPaths, ".git/**"])];
37
+ const rg = config.rg === undefined ? findRipgrep() : config.rg;
38
+ let paths: string[] = [], truncated = false, available = true;
39
+ const backend = rg ? "ripgrep" : "fts";
40
+ if (rg) {
41
+ const result = await new Promise<{ output: string; limited: boolean }>((resolve) => {
42
+ execFile(rg, ["--files-with-matches", "--null", "--hidden", "--fixed-strings", "--max-filesize", "1M", "--no-messages",
43
+ ...excludePatterns.flatMap(pattern => ["--iglob", `!${pattern}`, "--iglob", `!${pattern}/**`]), "--", query, "."],
44
+ { cwd: root, timeout: Math.max(1, deadline - Date.now()), maxBuffer: 1 << 20, encoding: "utf8" }, (error, stdout) => {
45
+ resolve({ output: stdout, limited: !!error && (error as { code?: unknown }).code !== 1 });
46
+ });
47
+ });
48
+ truncated = result.limited;
49
+ paths = result.output.slice(0, result.output.lastIndexOf("\0") + 1).split("\0").filter(Boolean).map(file => file.replace(/^\.\//, ""));
50
+ } else {
51
+ const result = await ftsCandidates(root, query, excludePatterns, Math.max(1, deadline - Date.now()));
52
+ paths = result.paths; available = result.available; truncated = result.timedOut || paths.length >= 50;
53
+ }
54
+ const matches: WorkspaceMatch[] = [];
55
+ for (const relative of [...new Set(paths)]) {
56
+ if (Date.now() >= deadline || matches.length >= LIMIT) { truncated = true; break; }
57
+ if (workspacePathFlags(relative, options).protected) continue;
58
+ try {
59
+ // Never return stale index text or rg output; apply the file reader's
60
+ // symlink/policy checks and calculate positions from current bytes.
61
+ const file = inspectWorkspaceFile(root, relative, options);
62
+ if (file.content === null) continue;
63
+ const lines = file.content.split(/\r?\n/);
64
+ for (let index = 0; index < lines.length; index++) {
65
+ if (Date.now() >= deadline || matches.length >= LIMIT) { truncated = true; break; }
66
+ const column = lines[index].indexOf(query);
67
+ if (column < 0) continue;
68
+ const start = Math.max(0, column - 80);
69
+ matches.push({ path: relative, line: index + 1, column: column + 1, preview: lines[index].slice(start, start + 400) });
70
+ }
71
+ } catch { /* moved, unsafe, or unreadable candidate */ }
72
+ }
73
+ return { query, matches, truncated, backend, available };
74
+ }