berry-agent 0.1.0-alpha.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 (401) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -0
  3. package/dist/.api-emit.stamp +1 -0
  4. package/dist/.build-meta.json +6 -0
  5. package/dist/agent/events.js +1 -0
  6. package/dist/agent/index.js +2 -0
  7. package/dist/agent/loop.js +139 -0
  8. package/dist/agent/queue.js +119 -0
  9. package/dist/agent/stream.js +62 -0
  10. package/dist/agent/tools-batch.js +221 -0
  11. package/dist/agent/types.js +8 -0
  12. package/dist/api/berry-agent-llm.d.ts +18 -0
  13. package/dist/api/berry-agent.d.ts +9 -0
  14. package/dist/api/surface.json +1632 -0
  15. package/dist/api/tsconfig.paths.json +12 -0
  16. package/dist/api/typebox-compile.d.ts +2 -0
  17. package/dist/api/typebox-value.d.ts +2 -0
  18. package/dist/api/typebox.d.ts +2 -0
  19. package/dist/browser/cdp.js +256 -0
  20. package/dist/browser/codes.js +31 -0
  21. package/dist/browser/discover.js +107 -0
  22. package/dist/browser/engine.js +169 -0
  23. package/dist/browser/index.js +17 -0
  24. package/dist/browser/install.js +342 -0
  25. package/dist/browser/page.js +321 -0
  26. package/dist/browser/service.js +158 -0
  27. package/dist/browser/tools.js +198 -0
  28. package/dist/browser/types.js +81 -0
  29. package/dist/channels/ask-queue.js +82 -0
  30. package/dist/channels/commands.js +129 -0
  31. package/dist/channels/engine/cell.js +143 -0
  32. package/dist/channels/engine/diff.js +143 -0
  33. package/dist/channels/engine/engine.js +416 -0
  34. package/dist/channels/engine/index.js +8 -0
  35. package/dist/channels/engine/input-keys.js +86 -0
  36. package/dist/channels/engine/input.js +596 -0
  37. package/dist/channels/engine/memory-io.js +71 -0
  38. package/dist/channels/engine/process-io.js +38 -0
  39. package/dist/channels/engine/types.js +17 -0
  40. package/dist/channels/engine/width.js +122 -0
  41. package/dist/channels/index.js +15 -0
  42. package/dist/channels/registry.js +56 -0
  43. package/dist/channels/sdk/admit.js +29 -0
  44. package/dist/channels/sdk/backend.js +102 -0
  45. package/dist/channels/sdk/cursor.js +56 -0
  46. package/dist/channels/sdk/index.js +20 -0
  47. package/dist/channels/sdk/jsonl.js +208 -0
  48. package/dist/channels/sdk/protocol.js +24 -0
  49. package/dist/channels/sdk/schema.js +91 -0
  50. package/dist/channels/sdk/wire-core.js +446 -0
  51. package/dist/channels/service.js +169 -0
  52. package/dist/channels/tui/autocomplete/autocomplete.js +42 -0
  53. package/dist/channels/tui/autocomplete/file-mentions.js +106 -0
  54. package/dist/channels/tui/autocomplete/popup.js +148 -0
  55. package/dist/channels/tui/autocomplete/provider.js +7 -0
  56. package/dist/channels/tui/autocomplete/token.js +68 -0
  57. package/dist/channels/tui/backend/ansi-rows.js +211 -0
  58. package/dist/channels/tui/backend/main-screen.js +207 -0
  59. package/dist/channels/tui/backend/osc.js +91 -0
  60. package/dist/channels/tui/backend/transcript.js +230 -0
  61. package/dist/channels/tui/backend/tui-backend.js +915 -0
  62. package/dist/channels/tui/editor/editor-model.js +543 -0
  63. package/dist/channels/tui/editor/editor-view.js +140 -0
  64. package/dist/channels/tui/editor/editor.js +275 -0
  65. package/dist/channels/tui/editor/undo-stack.js +35 -0
  66. package/dist/channels/tui/editor/visual-lines.js +126 -0
  67. package/dist/channels/tui/editor/word-nav.js +106 -0
  68. package/dist/channels/tui/history/history-viewer.js +403 -0
  69. package/dist/channels/tui/index.js +3 -0
  70. package/dist/channels/tui/layout.js +167 -0
  71. package/dist/channels/tui/markdown/blocks.js +111 -0
  72. package/dist/channels/tui/markdown/inline.js +75 -0
  73. package/dist/channels/tui/markdown/markdown.js +182 -0
  74. package/dist/channels/tui/memory/memory-viewer.js +540 -0
  75. package/dist/channels/tui/overlay/alt-screen.js +105 -0
  76. package/dist/channels/tui/overlay/overlay.js +57 -0
  77. package/dist/channels/tui/overlay/select-confirm.js +156 -0
  78. package/dist/channels/tui/panels/todo-panel.js +54 -0
  79. package/dist/channels/tui/panels/tool-progress-panel.js +93 -0
  80. package/dist/channels/tui/scroll/scroll-view.js +254 -0
  81. package/dist/channels/tui/status/status-line.js +76 -0
  82. package/dist/channels/tui/text.js +38 -0
  83. package/dist/channels/tui/theme.js +41 -0
  84. package/dist/channels/types.js +11 -0
  85. package/dist/channels/ui-core.js +209 -0
  86. package/dist/checkpoint/capture.js +45 -0
  87. package/dist/checkpoint/codes.js +33 -0
  88. package/dist/checkpoint/command.js +82 -0
  89. package/dist/checkpoint/gate.js +48 -0
  90. package/dist/checkpoint/index.js +20 -0
  91. package/dist/checkpoint/restore.js +172 -0
  92. package/dist/checkpoint/store.js +247 -0
  93. package/dist/checkpoint/types.js +4 -0
  94. package/dist/checkpoint/walk.js +146 -0
  95. package/dist/compaction/ccr-tools.js +112 -0
  96. package/dist/compaction/ccr.js +84 -0
  97. package/dist/compaction/codes.js +21 -0
  98. package/dist/compaction/index.js +18 -0
  99. package/dist/compaction/policy.js +200 -0
  100. package/dist/compaction/service.js +422 -0
  101. package/dist/compaction/slots.js +121 -0
  102. package/dist/compaction/types.js +10 -0
  103. package/dist/context/codes.js +40 -0
  104. package/dist/context/events.js +160 -0
  105. package/dist/context/index.js +13 -0
  106. package/dist/context/logger.js +136 -0
  107. package/dist/context/scope.js +182 -0
  108. package/dist/context/workspace.js +104 -0
  109. package/dist/contracts/agent-events.d.ts +73 -0
  110. package/dist/contracts/agent-events.js +16 -0
  111. package/dist/contracts/api.d.ts +249 -0
  112. package/dist/contracts/api.js +299 -0
  113. package/dist/contracts/approval.d.ts +37 -0
  114. package/dist/contracts/approval.js +9 -0
  115. package/dist/contracts/env-ref.d.ts +41 -0
  116. package/dist/contracts/env-ref.js +32 -0
  117. package/dist/contracts/errors.d.ts +56 -0
  118. package/dist/contracts/errors.js +266 -0
  119. package/dist/contracts/events.d.ts +70 -0
  120. package/dist/contracts/events.js +329 -0
  121. package/dist/contracts/index.d.ts +32 -0
  122. package/dist/contracts/index.js +26 -0
  123. package/dist/contracts/llm.d.ts +275 -0
  124. package/dist/contracts/llm.js +12 -0
  125. package/dist/contracts/messages.d.ts +63 -0
  126. package/dist/contracts/messages.js +65 -0
  127. package/dist/contracts/redact.d.ts +46 -0
  128. package/dist/contracts/redact.js +223 -0
  129. package/dist/contracts/tools.d.ts +197 -0
  130. package/dist/contracts/tools.js +33 -0
  131. package/dist/contracts/types.d.ts +302 -0
  132. package/dist/contracts/types.js +50 -0
  133. package/dist/contracts/ui.d.ts +122 -0
  134. package/dist/contracts/ui.js +15 -0
  135. package/dist/conversation/agent-service.js +61 -0
  136. package/dist/conversation/approval-wiring.js +82 -0
  137. package/dist/conversation/backoff.js +38 -0
  138. package/dist/conversation/codes.js +47 -0
  139. package/dist/conversation/control-tools.js +102 -0
  140. package/dist/conversation/control.js +210 -0
  141. package/dist/conversation/driver.js +922 -0
  142. package/dist/conversation/index.js +12 -0
  143. package/dist/conversation/model-visible.js +169 -0
  144. package/dist/conversation/open-tools.js +126 -0
  145. package/dist/conversation/reseed.js +132 -0
  146. package/dist/conversation/sessions.js +215 -0
  147. package/dist/conversation/todo.js +203 -0
  148. package/dist/conversation/types.js +35 -0
  149. package/dist/conversation/wiring.js +204 -0
  150. package/dist/credentials/codes.js +59 -0
  151. package/dist/credentials/commands.js +186 -0
  152. package/dist/credentials/env-ref.js +34 -0
  153. package/dist/credentials/index.js +28 -0
  154. package/dist/credentials/migration.js +18 -0
  155. package/dist/credentials/oauth.js +266 -0
  156. package/dist/credentials/refresh.js +152 -0
  157. package/dist/credentials/secrets.js +118 -0
  158. package/dist/credentials/types.js +30 -0
  159. package/dist/exec/bash.js +279 -0
  160. package/dist/exec/codes.js +37 -0
  161. package/dist/exec/env.js +99 -0
  162. package/dist/exec/environment.js +39 -0
  163. package/dist/exec/git-guard.js +623 -0
  164. package/dist/exec/index.js +24 -0
  165. package/dist/exec/registry.js +120 -0
  166. package/dist/exec/spawn.js +281 -0
  167. package/dist/exec/tail.js +77 -0
  168. package/dist/exec/types.js +5 -0
  169. package/dist/goal/codes.js +33 -0
  170. package/dist/goal/command.js +175 -0
  171. package/dist/goal/fold.js +121 -0
  172. package/dist/goal/gates.js +122 -0
  173. package/dist/goal/index.js +22 -0
  174. package/dist/goal/migration.js +48 -0
  175. package/dist/goal/service.js +501 -0
  176. package/dist/goal/todo-tool.js +174 -0
  177. package/dist/goal/types.js +1 -0
  178. package/dist/goal/update-tool.js +46 -0
  179. package/dist/host/approval-cmd.js +276 -0
  180. package/dist/host/assembly.js +1146 -0
  181. package/dist/host/boot-failures.js +104 -0
  182. package/dist/host/budget-advisory.js +74 -0
  183. package/dist/host/budget-broadcast.js +50 -0
  184. package/dist/host/builtins.js +54 -0
  185. package/dist/host/cli.js +544 -0
  186. package/dist/host/codes.js +120 -0
  187. package/dist/host/config-schema.js +195 -0
  188. package/dist/host/conversation-stack.js +654 -0
  189. package/dist/host/core-plugins.js +1685 -0
  190. package/dist/host/credentials-cmd.js +42 -0
  191. package/dist/host/disclosure.js +30 -0
  192. package/dist/host/dispatch.js +105 -0
  193. package/dist/host/doors-cmd.js +154 -0
  194. package/dist/host/dump-config.js +44 -0
  195. package/dist/host/hook-dispatch-guard.js +27 -0
  196. package/dist/host/import-gate.js +153 -0
  197. package/dist/host/index.js +54 -0
  198. package/dist/host/issue-session.js +241 -0
  199. package/dist/host/loader.js +372 -0
  200. package/dist/host/main.js +161 -0
  201. package/dist/host/manifest.js +381 -0
  202. package/dist/host/mcp-entry.js +53 -0
  203. package/dist/host/plugin-boot.js +952 -0
  204. package/dist/host/plugin-context.js +672 -0
  205. package/dist/host/plugin-install.js +480 -0
  206. package/dist/host/plugin-reload.js +124 -0
  207. package/dist/host/plugin-store.js +462 -0
  208. package/dist/host/plugin-tools.js +359 -0
  209. package/dist/host/plugin-uninstall.js +257 -0
  210. package/dist/host/plugins-cmd.js +302 -0
  211. package/dist/host/plugins-command.js +130 -0
  212. package/dist/host/plugins-config.js +195 -0
  213. package/dist/host/prompt-sections.js +133 -0
  214. package/dist/host/run-entry.js +544 -0
  215. package/dist/host/runtime.js +209 -0
  216. package/dist/host/scheduler-tick.js +294 -0
  217. package/dist/host/serve-daemon.js +501 -0
  218. package/dist/host/serve-entry.js +279 -0
  219. package/dist/host/session-anchor.js +41 -0
  220. package/dist/host/sessions-cmd.js +219 -0
  221. package/dist/host/sessions-face.js +47 -0
  222. package/dist/host/settings-store.js +112 -0
  223. package/dist/host/signals.js +69 -0
  224. package/dist/host/single-instance.js +105 -0
  225. package/dist/host/subagent-factory.js +291 -0
  226. package/dist/host/testkit/harness.js +283 -0
  227. package/dist/host/testkit/index.js +14 -0
  228. package/dist/host/testkit/matrix.js +302 -0
  229. package/dist/host/tool-policy-store.js +177 -0
  230. package/dist/host/triggers.js +212 -0
  231. package/dist/host/tui-entry.js +216 -0
  232. package/dist/host/webui-bridge.js +165 -0
  233. package/dist/issue/codes.js +38 -0
  234. package/dist/issue/filter.js +146 -0
  235. package/dist/issue/github.js +116 -0
  236. package/dist/issue/index.js +17 -0
  237. package/dist/issue/mount.js +70 -0
  238. package/dist/issue/poll.js +58 -0
  239. package/dist/issue/service.js +341 -0
  240. package/dist/issue/tools.js +70 -0
  241. package/dist/issue/types.js +34 -0
  242. package/dist/issue/webhook.js +126 -0
  243. package/dist/llm/codes.js +77 -0
  244. package/dist/llm/complete.js +196 -0
  245. package/dist/llm/events.js +15 -0
  246. package/dist/llm/index.js +24 -0
  247. package/dist/llm/inflight.js +65 -0
  248. package/dist/llm/model-id.js +58 -0
  249. package/dist/llm/provider-face.d.ts +32 -0
  250. package/dist/llm/provider-face.js +34 -0
  251. package/dist/llm/recovery.js +146 -0
  252. package/dist/llm/runtime.js +46 -0
  253. package/dist/llm/stream-fn.js +147 -0
  254. package/dist/lsp/codes.js +31 -0
  255. package/dist/lsp/connection.js +147 -0
  256. package/dist/lsp/frame.js +110 -0
  257. package/dist/lsp/index.js +16 -0
  258. package/dist/lsp/inject.js +58 -0
  259. package/dist/lsp/instance.js +264 -0
  260. package/dist/lsp/service.js +522 -0
  261. package/dist/lsp/tools.js +53 -0
  262. package/dist/lsp/types.js +201 -0
  263. package/dist/mcp/bridge.js +222 -0
  264. package/dist/mcp/codes.js +21 -0
  265. package/dist/mcp/index.js +19 -0
  266. package/dist/mcp/jsonrpc.js +230 -0
  267. package/dist/mcp/service.js +125 -0
  268. package/dist/mcp/tools.js +145 -0
  269. package/dist/mcp/types.js +108 -0
  270. package/dist/memory/cite.js +131 -0
  271. package/dist/memory/codes.js +50 -0
  272. package/dist/memory/command.js +71 -0
  273. package/dist/memory/consolidate.js +219 -0
  274. package/dist/memory/cycle.js +109 -0
  275. package/dist/memory/dao.js +878 -0
  276. package/dist/memory/diff.js +238 -0
  277. package/dist/memory/extract.js +181 -0
  278. package/dist/memory/fts.js +58 -0
  279. package/dist/memory/index.js +42 -0
  280. package/dist/memory/inject.js +354 -0
  281. package/dist/memory/merge.js +178 -0
  282. package/dist/memory/migration.js +132 -0
  283. package/dist/memory/pollution.js +59 -0
  284. package/dist/memory/port.js +385 -0
  285. package/dist/memory/review.js +201 -0
  286. package/dist/memory/scan.js +89 -0
  287. package/dist/memory/tools.js +508 -0
  288. package/dist/memory/types.js +140 -0
  289. package/dist/obs/codes.js +38 -0
  290. package/dist/obs/db.js +115 -0
  291. package/dist/obs/index.js +20 -0
  292. package/dist/obs/rollup.js +61 -0
  293. package/dist/obs/service.js +363 -0
  294. package/dist/obs/session-tools.js +282 -0
  295. package/dist/obs/session-view.js +225 -0
  296. package/dist/obs/tool.js +97 -0
  297. package/dist/obs/types.js +1 -0
  298. package/dist/persist/audit.js +63 -0
  299. package/dist/persist/aux.js +39 -0
  300. package/dist/persist/codes.js +35 -0
  301. package/dist/persist/index.js +30 -0
  302. package/dist/persist/load-history.js +50 -0
  303. package/dist/persist/migrations.js +37 -0
  304. package/dist/persist/paths.js +108 -0
  305. package/dist/persist/persistence.js +229 -0
  306. package/dist/persist/schema.js +99 -0
  307. package/dist/persist/secret-box.js +95 -0
  308. package/dist/persist/store.js +706 -0
  309. package/dist/persist/write-behind.js +238 -0
  310. package/dist/safety/approval.js +171 -0
  311. package/dist/safety/bwrap.js +123 -0
  312. package/dist/safety/codes.js +57 -0
  313. package/dist/safety/danger.js +505 -0
  314. package/dist/safety/gate.js +226 -0
  315. package/dist/safety/index.js +24 -0
  316. package/dist/safety/presets.js +73 -0
  317. package/dist/safety/roots.js +139 -0
  318. package/dist/safety/sandbox.js +214 -0
  319. package/dist/safety/seatbelt.js +88 -0
  320. package/dist/safety/sensitive.js +34 -0
  321. package/dist/safety/tool-policy.js +203 -0
  322. package/dist/safety/types.js +13 -0
  323. package/dist/scheduler/codes.js +54 -0
  324. package/dist/scheduler/cron-backend.js +135 -0
  325. package/dist/scheduler/engine.js +280 -0
  326. package/dist/scheduler/gates.js +60 -0
  327. package/dist/scheduler/index.js +25 -0
  328. package/dist/scheduler/migration.js +22 -0
  329. package/dist/scheduler/runner.js +171 -0
  330. package/dist/scheduler/schedule.js +193 -0
  331. package/dist/scheduler/service.js +283 -0
  332. package/dist/scheduler/tick.js +139 -0
  333. package/dist/scheduler/types.js +1 -0
  334. package/dist/sdk/http.js +865 -0
  335. package/dist/sdk/index.js +22 -0
  336. package/dist/sdk/mcp.js +289 -0
  337. package/dist/sdk/plugin-route-registry.js +126 -0
  338. package/dist/sdk/plugin-routes.js +149 -0
  339. package/dist/sdk/security.js +116 -0
  340. package/dist/sdk/types.js +32 -0
  341. package/dist/session/budget.js +159 -0
  342. package/dist/session/codes.js +40 -0
  343. package/dist/session/derive.js +180 -0
  344. package/dist/session/event-data.js +1 -0
  345. package/dist/session/fork.js +72 -0
  346. package/dist/session/import-gates.js +148 -0
  347. package/dist/session/index.js +18 -0
  348. package/dist/session/recover.js +103 -0
  349. package/dist/session/session.js +279 -0
  350. package/dist/session/snapshot.js +107 -0
  351. package/dist/skills/agents.js +242 -0
  352. package/dist/skills/codes.js +51 -0
  353. package/dist/skills/discovery.js +266 -0
  354. package/dist/skills/frontmatter.js +215 -0
  355. package/dist/skills/index.js +28 -0
  356. package/dist/skills/load.js +118 -0
  357. package/dist/skills/manage.js +201 -0
  358. package/dist/skills/registry.js +138 -0
  359. package/dist/skills/render.js +124 -0
  360. package/dist/skills/sections.js +171 -0
  361. package/dist/skills/types.js +21 -0
  362. package/dist/subagent/codes.js +50 -0
  363. package/dist/subagent/declarative.js +55 -0
  364. package/dist/subagent/index.js +19 -0
  365. package/dist/subagent/notify.js +51 -0
  366. package/dist/subagent/provide.js +7 -0
  367. package/dist/subagent/registry.js +149 -0
  368. package/dist/subagent/service.js +338 -0
  369. package/dist/subagent/surface.js +44 -0
  370. package/dist/subagent/tool.js +153 -0
  371. package/dist/subagent/types.js +25 -0
  372. package/dist/tools/apply-patch.js +164 -0
  373. package/dist/tools/codes.js +109 -0
  374. package/dist/tools/fs.js +501 -0
  375. package/dist/tools/index.js +17 -0
  376. package/dist/tools/observed.js +99 -0
  377. package/dist/tools/pipeline.js +176 -0
  378. package/dist/tools/protected-read.js +54 -0
  379. package/dist/tools/registry.js +250 -0
  380. package/dist/tools/search.js +451 -0
  381. package/dist/tools/worktree.js +327 -0
  382. package/dist/web/codes.js +37 -0
  383. package/dist/web/gate.js +43 -0
  384. package/dist/web/hygiene.js +195 -0
  385. package/dist/web/index.js +16 -0
  386. package/dist/web/service.js +207 -0
  387. package/dist/web/ssrf-guard.js +44 -0
  388. package/dist/web/tool.js +76 -0
  389. package/dist/web/types.js +10 -0
  390. package/dist/webui/assets/index-B-t2O8gh.js +22 -0
  391. package/dist/webui/assets/index-DOlgSZ9Q.css +2 -0
  392. package/dist/webui/index.html +14 -0
  393. package/dist/webui/index.js +14 -0
  394. package/dist/webui/server.js +549 -0
  395. package/dist/webui/types.js +38 -0
  396. package/examples/README.md +26 -0
  397. package/examples/minimal-code-plugin/entry.js +43 -0
  398. package/examples/minimal-code-plugin/package.json +12 -0
  399. package/examples/pure-skill-pack/package.json +14 -0
  400. package/examples/pure-skill-pack/skills/markdown-table/SKILL.md +32 -0
  401. package/package.json +86 -0
@@ -0,0 +1,226 @@
1
+ /**
2
+ * L3 safety — 守门固定行(04 §7 守门段 + §8 carve-out 条 + §9 审批对)。
3
+ *
4
+ * 本行装在 tools_pre_execute 守门段首位,评估序六步(04 §9 定形块④——
5
+ * 2026-09-11 审批分档批重排定形,read-only 档让棒沿旧保持一切判定之前):
6
+ * 1. **(1) 工具策略表 deny 面**——命中即 block **硬拒**最先短路(reason
7
+ * `policy-deny:<条目序>`,**不可被任何后续面翻转**:sticky always /
8
+ * allow 命中 / policy never / sandbox danger 全不豁免——deny 是用户
9
+ * 主权裁决;deny 可拦 read 档工具,(1) 先于 (3));
10
+ * 2. **(2) carve-out 判定**(04 §8 2026-09-06 定形):fs 写路径在根内但命中
11
+ * carve-out 例外(.git / .env 族 + 数据目录族)→ block **硬拒**——denial
12
+ * marker 回执、无升权出路(不存在「升到哪档能写 .git」,任何档含 danger
13
+ * 恒不可写——底线不交模型裁决。与 berry 分叉:berry 把命中做成升权审批面
14
+ * 不承);数据目录条(04 §7 宿主状态根)无条件恒追加、不可经 entries=[]
15
+ * 关闭(恒 = 平台底线,不交装配裁量——2026-09-06 遗漏审计批补钉);
16
+ * 3. **(3) read 档放行**(显式声明 read 才有宽档;未声明经注册面归一 exec
17
+ * 不经此位——04 §9 ②未知缺省最危律);
18
+ * 4. **(4) allow 命中免问**(策略表 allow 条目 + 偏序档位判——advisory:只
19
+ * 影响问不问,fence/执行段照走;carve-out 硬拒面与之无交叉命中面);
20
+ * 5. **(5) fence 前核**(§7 outside-roots 不问直接拒,沿旧);
21
+ * 6. **(6) write/exec 审批对 ask**(03 §2.3「write|exec 触发审批对」——粘性
22
+ * 短路在 ApprovalService 内;policy never → 直接拒沿旧)。
23
+ *
24
+ * 分工(防重复拦截):粗粒度根 containment 在 tools/fs 的 fence——路径在可
25
+ * 写根外(outside-roots)是 fence 的拒绝面,本行不拦不问(问了 fence 也会
26
+ * 拒——审批对为可执行动作而设);read-only 档 fence 拒全量写(空根),本行
27
+ * 跳过不产生审批交互。
28
+ *
29
+ * 链续约定:放行必须 return next(input)(waterfall 委托——不调 next 即短路
30
+ * 会吞掉链上后续守门者);block 置 input.outcome 后返回(不调 next)。
31
+ * 装配纪律:本仓 EventDispatch 无 prepend 选项、链序 = 注册序——装配根必须
32
+ * **先装本行**再装其余守门者(安全栈固定占守门段首位)。
33
+ */
34
+ import { TOOL_PRE_EXECUTE_EVENT } from '../contracts/index.js';
35
+ import { parseApplyPatch } from '../tools/index.js';
36
+ import { absolutize, buildCarveOutTable, canonicalPath, deriveWritableRoots, resolveWritability, } from './roots.js';
37
+ import { FS_WRITE_TOOLS, matchToolPolicy, policyHitNote } from './tool-policy.js';
38
+ import { sandboxDenialMarker } from './sandbox.js';
39
+ /** 内置默认 carve-out 条目(04 §8 例示:.git 转只读 + .env 族遮罩——含单层 glob 两形) */
40
+ export const DEFAULT_CARVE_OUT_ENTRIES = [
41
+ { pattern: '.git', effect: 'deny', note: '版本库元数据默认只读' },
42
+ { pattern: '.env', effect: 'deny', note: '环境变量敏感文件' },
43
+ { pattern: '*.env', effect: 'deny', note: '环境变量敏感文件(glob)' },
44
+ { pattern: '.env.*', effect: 'deny', note: '环境变量敏感文件(glob)' },
45
+ ];
46
+ /**
47
+ * 从写类工具参数提取目标路径集合(相对路径保持原样,统一由本行锚 workspace)。
48
+ * 未知工具返回空(本行的路径语义只覆盖 fs 写族;其余 write-effect 工具走
49
+ * 整名审批,无路径可提取)。
50
+ */
51
+ function extractWritePaths(toolName, args) {
52
+ if (toolName === 'write') {
53
+ // write 工具:单目标 path 参数
54
+ return typeof args.path === 'string' ? [args.path] : [];
55
+ }
56
+ if (toolName === 'edit') {
57
+ // edit 工具:apply_patch 补丁文本(多文件操作段,逐段提取路径);
58
+ // 补丁格式错不在本行的管辖面——放行给 edit 工具自身报 FS_PATCH_FAILED
59
+ const patch = typeof args.patch === 'string' ? args.patch : '';
60
+ try {
61
+ return parseApplyPatch(patch).map((op) => op.path);
62
+ }
63
+ catch {
64
+ return [];
65
+ }
66
+ }
67
+ return [];
68
+ }
69
+ /**
70
+ * 安装安全守门行(tools_pre_execute 守门段首位——装配根先于其余守门者注册)。
71
+ * @returns 退订闭包(装配层经 scope.effect 组合生命周期)
72
+ *
73
+ * carve-out 判定表在装配期钉死(glob「先展开再遮罩」的展开时刻在此)——
74
+ * 会话中途新建的 .env 新文件不追溯遮罩(诚实语义;要再遮罩须换条目重装)。
75
+ */
76
+ export function installSafetyGate(dispatch, opts) {
77
+ const workspace = canonicalPath(opts.workspace);
78
+ // 数据目录恒排除条(04 §7):无条件追加于用户 entries 之后——entries=[] 关
79
+ // 闭的是 .git/.env 例示面,本条不随装配裁量关闭(恒 = 平台底线)。pattern
80
+ // 为绝对路径(resolveDataDir 产物),buildCarveOutTable 内展开即其自身
81
+ const entries = [
82
+ ...(opts.entries ?? DEFAULT_CARVE_OUT_ENTRIES),
83
+ { pattern: opts.dataDir, effect: 'deny', note: '数据目录(04 §7——宿主状态根,任何档含 danger 恒不可写)' },
84
+ ];
85
+ const carveTable = buildCarveOutTable(workspace, entries);
86
+ const approval = opts.approval;
87
+ const handler = async (input, next) => {
88
+ // 会话归属过滤(批 19c-1):归属位与调用位双在场且相异 = 他会话的工具
89
+ // 调用——本行让棒(不执法不产生审批交互,交棒给归属行)。双缺席任一 =
90
+ // 无会话判据语境(单会话测试形 / 无会话管道调用),不过滤保持旧行为。
91
+ if (opts.sessionId !== undefined && input.sessionId !== undefined && input.sessionId !== opts.sessionId) {
92
+ return next(input);
93
+ }
94
+ const mode = opts.mode();
95
+ const tool = input.tool;
96
+ // read-only 档让棒沿旧(04 §9 定形块④:skip 位保持一切判定之前——fence
97
+ // 拒全量写(空根),本行跳过不产生审批交互;策略表在 read-only 会话无
98
+ // 执法位〔冷读闸 m1 定位〕,deny 亦不因 read-only 档而前置执法)
99
+ if (mode === 'read-only')
100
+ return next(input);
101
+ const isFsFamily = FS_WRITE_TOOLS.has(tool.name);
102
+ // canonical 写目标集一次提取(fs 族):deny 匹配 / fence 前核 / 粘性指纹
103
+ // 三消费点共用(相对路径锚 workspace、符号链解析与 fence 同源)
104
+ const canonicalWritePaths = isFsFamily
105
+ ? extractWritePaths(tool.name, input.args).map((p) => absolutize(workspace, p))
106
+ : [];
107
+ /* ---- (1) 工具策略表 deny 面(04 §9 定形块④评估序①——最先短路) ---- */
108
+ // 匹配一次全表持有:引擎 deny 优先律(首个 deny 命中即终局返回)——deny
109
+ // 序位在 carve-out / read 放行 / allow 免问 / ask 全部之前,**不可被任何
110
+ // 后续面翻转**(sticky always / allow 命中 / policy never / sandbox danger
111
+ // 全不豁免——deny 是用户主权裁决,其余面无权翻案)。deny 可拦 read 档
112
+ // 工具((1) 先于 (3):「此工具永拒」的用户主权语义自然覆盖只读工具——
113
+ // read 档免审 ≠ 免策略表)。deny 拒不产生 approval 对(短路在 ask 之前
114
+ // ——粘性命中无审批对同律),reason policy-deny:<条目序> 落 gate/decision。
115
+ const hit = opts.toolPolicy !== undefined && opts.toolPolicy.length > 0
116
+ ? matchToolPolicy(opts.toolPolicy, isFsFamily
117
+ ? {
118
+ tool: tool.name,
119
+ // 档位取值:注册面归一(04 §9 定形块②反转)后应恒在场;类型
120
+ // 位缺省兜底 = exec(未知缺省最危律同律——守门行自身防御与
121
+ // 注册面归一同向,双保险)
122
+ effect: tool.effect ?? 'exec',
123
+ writePaths: canonicalWritePaths,
124
+ workspace,
125
+ }
126
+ : { tool: tool.name, effect: tool.effect ?? 'exec' }, Date.now())
127
+ : undefined;
128
+ if (hit !== undefined && hit.entry.decision === 'deny') {
129
+ input.outcome = {
130
+ action: 'block',
131
+ reason: `${sandboxDenialMarker(mode)} ${tool.name} 命中工具策略表 deny 条目 ${policyHitNote(hit)}${hit.entry.reason !== undefined ? `(${hit.entry.reason})` : ''}——用户主权硬拒,任何面不可翻转(解除唯手删条目)。`,
132
+ };
133
+ return input; // 不调 next:短路整链(deny 优先律——硬拒)
134
+ }
135
+ /* ---- (2) carve-out 判定(fs 族写路径;任何档含 danger 照走——硬拒) ---- */
136
+ if (isFsFamily) {
137
+ const roots = deriveWritableRoots(workspace, mode);
138
+ // 逐路径独立判定:任一 deny 命中即整调用硬拒(多文件补丁不部分放行)
139
+ for (const rawPath of extractWritePaths(tool.name, input.args)) {
140
+ // 与 fence 同源的 canonical 化(相对锚 workspace、最近存在祖先解析符号链)
141
+ const absPath = absolutize(workspace, rawPath);
142
+ const verdict = resolveWritability(absPath, roots, carveTable);
143
+ if (!verdict.allowed && verdict.kind === 'carve-out') {
144
+ const node = verdict.matched;
145
+ // 硬拒回执(04 §8 定形):denial marker + 命中条目——无升权 hint
146
+ // (不存在「升到哪档能写」,任何档恒不可写——引导升权即引导徒劳)
147
+ input.outcome = {
148
+ action: 'block',
149
+ reason: `${sandboxDenialMarker(mode)} ${rawPath} 命中 carve-out 恒排除条目 ${node.entry.pattern}${node.entry.note ? `(${node.entry.note})` : ''}——任何档(含 danger)恒不可写,无升权出路。`,
150
+ };
151
+ return input; // 不调 next:短路整链(守门段 block 语义)
152
+ }
153
+ }
154
+ }
155
+ /* ---- (3) read 档放行(04 §9 定形块④评估序③:显式声明 read 才有宽档) ---- */
156
+ // 本行只管写/执行意图(03 §2.3 effect 面):write/exec 同走审批链——
157
+ // exec 档不留审批旁路;未声明效果面工具经注册面归一(04 §9 ②缺省反转
158
+ // ?? 'exec')已恒为 exec 档,不经本位放行
159
+ if (tool.effect === 'read')
160
+ return next(input);
161
+ /* ---- (4) allow 命中免问(评估序④:策略表 allow 条目 + 偏序档位判) ---- */
162
+ // 命中审计(04 §9 ④):放行来源标注 policy-allow:<条目序> 落 GateInput
163
+ // ——管道 recordGate 承接进 gate/decision 的 reason 位(免问放行仍可审计
164
+ // ——不产生 approval 事件对,来源在此标注)
165
+ if (hit !== undefined) {
166
+ input.allowReason = policyHitNote(hit);
167
+ return next(input);
168
+ }
169
+ /* ---- (5)(6) write/exec 审批对(03 §2.3;粘性短路在 ApprovalService 内) ---- */
170
+ // 审批前先核 fence 面:任一写目标在可写根外 = fence 必拒(本行不问——
171
+ // 审批对为可执行动作而设,防「批了又被 fence 拒」的空转交互)
172
+ if (isFsFamily) {
173
+ const roots = deriveWritableRoots(workspace, mode);
174
+ const outside = canonicalWritePaths.some((p) => {
175
+ const verdict = resolveWritability(p, roots, carveTable);
176
+ return !verdict.allowed && verdict.kind === 'outside-roots';
177
+ });
178
+ if (outside)
179
+ return next(input); // fence 的拒绝面,本行不重复拦
180
+ }
181
+ // 粘性指纹目标摘要(04 §9 粘性第 1/2 款):fs 族 = 写目标路径(单目标 =
182
+ // 该路径;多目标 = 排序去重拼接——同集合才谈得上免问);其余 write/exec
183
+ // 工具 = 工具名(整名族语义——同工具的后续调用免问)
184
+ const targets = isFsFamily ? [...new Set(canonicalWritePaths)].sort() : [tool.name];
185
+ // 草案(04 §9 定形③):fs 族仅在单目标时携带(精确 canonical 路径——批
186
+ // 这一次不升格批全仓);多目标无单一路径可代表即无草案;非 fs 族无路径
187
+ // 语义亦无草案(整名草案随真实消费者出现再裁)
188
+ const draft = isFsFamily && targets.length === 1 ? { tool: tool.name, pattern: targets[0] } : undefined;
189
+ const summary = isFsFamily
190
+ ? targets.length === 1
191
+ ? `写入 ${targets[0]}`
192
+ : `写入 ${targets.length} 个目标(${targets[0]} 等)`
193
+ : `${tool.name} 写动作`;
194
+ const { outcome } = await approval.ask({
195
+ summary,
196
+ toolName: tool.name,
197
+ toolCallId: input.toolCallId,
198
+ // 粘性指纹(tier = 当前档——子集偏序维度:更严档的重复请求被既有宽批包含)
199
+ stickyKey: { target: targets.join('\n'), tier: mode },
200
+ ...(draft !== undefined ? { suggestedEntry: draft } : {}),
201
+ // run 取消信号随 ask 载荷透传(interrupt 链——answerer 桥接撤销;
202
+ // undefined = 无 run 语境不携带)
203
+ ...(input.signal !== undefined ? { signal: input.signal } : {}),
204
+ });
205
+ if (outcome === 'allowed-once')
206
+ return next(input); // 放行交棒(仅本次调用)
207
+ // 拒绝族 block:cancelled 是打断非拒绝(run 已 abort 时模型看不见——
208
+ // loop break 在先,收的是 durable 审计面与 resume 回放语境的诚实);
209
+ // rejected/unavailable 不带升权 hint(fs 写审批无升权出路——用户已当面
210
+ // 说否,引导绕路违背 04 §8「拒绝是最终的」)
211
+ if (outcome === 'cancelled') {
212
+ input.outcome = {
213
+ action: 'block',
214
+ reason: `${sandboxDenialMarker(mode)} ${summary}未执行(审批已取消——run 已打断)。`,
215
+ };
216
+ return input;
217
+ }
218
+ input.outcome = {
219
+ action: 'block',
220
+ reason: `${sandboxDenialMarker(mode)} ${summary}被拒(审批结果:${outcome}${outcome === 'unavailable' ? '——无人应答,fail-closed 拒执行' : ''})。`,
221
+ };
222
+ return input;
223
+ };
224
+ // 无 prepend 选项的装配纪律见文件头注——调用方(装配根)保证本行先注册
225
+ return dispatch.onWaterfall(TOOL_PRE_EXECUTE_EVENT, handler);
226
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * L3 safety 公开面(02 篇席 8:审批对 + 沙箱三档 + carve-out + 可写根推导 +
3
+ * 权限预设 + 升权编舞)。
4
+ *
5
+ * 消费方:tools/fs 的 fence(createRootsProvider 数据源)、exec 的 bash 工具
6
+ * 件(confine + 升权)、host 装配根(installSafetyGate + createApprovalService
7
+ * + 持久/策略表接线——审批分档批起载体名 tool-policy.json)。权限预设(presets
8
+ * 纯数据件 + host 人面/装配消费腿)2026-09-11 审批分档批 ap-3 落码。
9
+ *
10
+ * 引入 './codes.js' 触发 SANDBOX_ 三码注册(注册纪律:import 发生才注册——
11
+ * 与 tools/llm/session 的 codes.ts 同款)。
12
+ */
13
+ import './codes.js';
14
+ export * from './types.js';
15
+ export * from './roots.js';
16
+ export * from './sensitive.js';
17
+ export * from './tool-policy.js';
18
+ export * from './presets.js';
19
+ export * from './approval.js';
20
+ export * from './sandbox.js';
21
+ export * from './seatbelt.js';
22
+ export * from './bwrap.js';
23
+ export * from './gate.js';
24
+ export * from './danger.js';
@@ -0,0 +1,73 @@
1
+ /**
2
+ * L3 safety — 权限预设打包(04 §9 定形块⑥·RP5——纯数据半边,2026-09-11
3
+ * 审批分档批 ap-3)。
4
+ *
5
+ * 预设 = sandbox 档 × 审批 policy × 策略表建议集的**打包缺省**,三档定名:
6
+ * - `conservative`:read-only + ask + 空建议集;
7
+ * - `balanced`:workspace-write + ask + 空建议集——**缺省档 = 现状打包**,零行为变化;
8
+ * - `open`:workspace-write + ask + 建议集 = 工作区根 fs 写前缀两件 +
9
+ * git 本地只读词干五件(04 §8 GIT_METADATA_COMMANDS 闭集取本地只读子集
10
+ * ——**push/pull/fetch 网络族不入集**:§13「此类词干不经 allowlist 放行」
11
+ * 不因预设翻转,git push 恒走危险闸〔冷读闸 M5 定形〕)。
12
+ *
13
+ * 展开式非引用式(不留预设名第二真相源):本件只产**展开值**——两旋钮
14
+ * (sandboxMode/approvalPolicy)与 allow 草案清单;草案经写侧唯一正门
15
+ * `appendToolPolicyEntry` 落盘(host 消费腿),机器条目无 reason/effect/
16
+ * expiresAt(与审批服务 suggestedEntry 同形——ToolPolicyDraft 两字段)。
17
+ * 无静默审批不破:open 档不含任何 yolo 形自动放行——policy 闭集 ask/never
18
+ * 本就无「不问放行」档,预设只打包既有合法态组合。
19
+ */
20
+ /** 预设名闭集(三档——04 §9 ⑥定名;CLI --preset 值域同源本表) */
21
+ export const APPROVAL_PRESET_NAMES = ['conservative', 'balanced', 'open'];
22
+ /**
23
+ * 三档打包表(静态单源)。balanced = 现状(workspace-write + ask)零变化;
24
+ * conservative 只收紧沙箱档(policy 仍 ask——never 档缺省不给:无人应答
25
+ * fail-closed 已是缺省语义,无需用户选「直接拒」档防身)。
26
+ */
27
+ export const APPROVAL_PRESETS = [
28
+ {
29
+ name: 'conservative',
30
+ sandboxMode: 'read-only',
31
+ approvalPolicy: 'ask',
32
+ description: '只读沙箱——文件写/执行全走审批(最严档)',
33
+ },
34
+ {
35
+ name: 'balanced',
36
+ sandboxMode: 'workspace-write',
37
+ approvalPolicy: 'ask',
38
+ description: '工作区可写 + 审批如常(缺省档 = 现状打包,零行为变化)',
39
+ },
40
+ {
41
+ name: 'open',
42
+ sandboxMode: 'workspace-write',
43
+ approvalPolicy: 'ask',
44
+ description: '工作区可写 + fs 写与 git 本地只读命令免问建议集(不含网络写动词族)',
45
+ },
46
+ ];
47
+ /** 取预设(未知名返 undefined——调用方自定错误面,本件不抛) */
48
+ export function approvalPresetOf(name) {
49
+ return APPROVAL_PRESETS.find((preset) => preset.name === name);
50
+ }
51
+ /**
52
+ * open 档 bash 建议词干五件(04 §8 GIT_METADATA_COMMANDS 本地只读子集——
53
+ * 网络写动词 push/pull/fetch 恒不入集,见文件头注)。
54
+ */
55
+ const OPEN_GIT_STEMS = ['git status', 'git log', 'git diff', 'git show', 'git branch'];
56
+ /**
57
+ * 预设建议集展开(写盘腿专用——CLI 逐次形不产条目,04 §9 ⑥ ap-3 定形补充):
58
+ * - conservative/balanced:空清单(建议集为空零写入——切档不清不删既有条目);
59
+ * - open:fs 两件(write/edit × 工作区根前缀)+ bash 五词干,共七条 allow 草案。
60
+ * pattern = 工作区根 canonical 绝对路径(fs 前缀语义)或命令词干(bash 语义)
61
+ * ——与审批服务 suggestedEntry 同源同形(ToolPolicyDraft 两字段),机器条目
62
+ * 无 reason/effect/expiresAt。
63
+ */
64
+ export function presetSuggestedEntries(preset, workspaceRoot) {
65
+ if (preset !== 'open')
66
+ return [];
67
+ const root = workspaceRoot.length > 0 ? workspaceRoot : '.';
68
+ return [
69
+ { tool: 'write', pattern: root },
70
+ { tool: 'edit', pattern: root },
71
+ ...OPEN_GIT_STEMS.map((stem) => ({ tool: 'bash', pattern: stem })),
72
+ ];
73
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * L3 safety — 可写根唯一推导函数 + carve-out 层叠例外(04 §8 deriveWritableRoots 表 + carve-out 条款)。
3
+ *
4
+ * deriveWritableRoots() 是「某档是什么意思」的唯一 home:沙箱 profile
5
+ * (seatbelt/bwrap)与进程内 fs fence(tools/fs.ts 的 writableRoots
6
+ * provider)都从这里取根列表——两套防线同源生成、永不漂移。carve-out
7
+ * 在根列表之上叠加按路径层叠的例外条目(.git / .env 族)。
8
+ *
9
+ * 与 berry 分叉注记:carve-out 命中 = 守门行 block **硬拒**(04 §8
10
+ * 2026-09-06 定形——「恒不可写=平台底线」字面从硬,无升权出路);berry
11
+ * 把 carve-out 命中做成升权审批面(用户可裁放行)不承。external 分域
12
+ * 变体(externalWritableRoots 族)不落——本仓插件进程隔离批随需再裁。
13
+ */
14
+ import { readdirSync, realpathSync } from 'node:fs';
15
+ import { tmpdir } from 'node:os';
16
+ import { basename, dirname, isAbsolute, join, resolve as resolvePath, sep } from 'node:path';
17
+ /**
18
+ * 路径 canonical 化(符号链解析到真实位置)。
19
+ * 路径或前缀不存在时:回退**最近存在祖先**解析符号链再拼回尾部段——与写侧
20
+ * fs.ts canonicalize 同律(别名工作区下新建文件的绝对别名路径若原样返回,
21
+ * 与 canonical 化的根比较恒 miss → 守门判根外跳过而写侧父目录递归判在根内
22
+ * 放行——两层 canonical 化分歧乘出遮罩绕过,蓝本真缺陷教训照搬防御)。
23
+ */
24
+ export function canonicalPath(path) {
25
+ try {
26
+ // native 实现按文件系统逐组件查找(与 spawn 及各强制层一致);JS 实现
27
+ // 在部分平台会先做词法折叠再解析符号链,与强制层判定不一致
28
+ return realpathSync.native(path);
29
+ }
30
+ catch {
31
+ // 不存在:父目录递归解析(父到达文件系统根仍失败即回退原样——绝对病理
32
+ // 形态不虚构路径)
33
+ const parent = dirname(path);
34
+ if (parent === path)
35
+ return path;
36
+ return join(canonicalPath(parent), basename(path));
37
+ }
38
+ }
39
+ /**
40
+ * 按档位推导可写根列表(canonical 化去重)——04 §8 表的唯一落码:
41
+ * - read-only:空列表(fence 拒全量写——mode 是一等输入,不是装饰参数);
42
+ * - workspace-write:workspace + /tmp + os.tmpdir();
43
+ * - danger:文件系统根 [sep](全盘可写——配合 isInsideRoot 的根分隔符特判,
44
+ * 任意绝对路径皆命中)。
45
+ * 这是 fs fence 与沙箱 profile(seatbelt/bwrap)的共同数据源。
46
+ */
47
+ export function deriveWritableRoots(workspace, mode) {
48
+ if (mode === 'read-only')
49
+ return [];
50
+ if (mode === 'danger')
51
+ return [sep];
52
+ return [...new Set([workspace, '/tmp', tmpdir()].map(canonicalPath))];
53
+ }
54
+ /** child 是否位于 root 内(相等或隔分隔符的前缀——防 /root 与 /root-evil 误判;root 为 sep 时任意绝对路径皆命中) */
55
+ export function isInsideRoot(child, root) {
56
+ const prefix = root === sep ? sep : root + sep;
57
+ return child === root || child.startsWith(prefix);
58
+ }
59
+ /**
60
+ * carve-out 条目展开:pattern → canonical 绝对路径集合。
61
+ * 字面 pattern 直接转绝对(不检查存在性——尚未存在的敏感路径也预先遮罩,
62
+ * .git 在 init 前就该挡);含 `*` 的 pattern 扫描其所在目录层(单层不递归)
63
+ * 把实际存在的匹配项展开——「glob 先展开再遮罩」。展开结果同时用于判定表
64
+ * 构建与审计输出。
65
+ */
66
+ export function expandCarveOutEntry(workspace, entry) {
67
+ const dir = entry.pattern.includes('/')
68
+ ? resolvePath(workspace, entry.pattern.slice(0, entry.pattern.lastIndexOf('/')))
69
+ : workspace;
70
+ const leaf = entry.pattern.includes('/') ? entry.pattern.slice(entry.pattern.lastIndexOf('/') + 1) : entry.pattern;
71
+ if (!leaf.includes('*')) {
72
+ return [absolutize(workspace, entry.pattern)];
73
+ }
74
+ // 顶层单层 glob:`*` 不跨分隔符,扫描目录层取实际存在的匹配
75
+ const pattern = new RegExp(`^${leaf.replace(/[.+^${}()|[\]\\]/g, String.raw `\$&`).replaceAll('*', '[^/]*')}$`);
76
+ try {
77
+ return readdirSync(dir, { withFileTypes: true })
78
+ .filter((e) => pattern.test(e.name))
79
+ .map((e) => canonicalPath(join(dir, e.name)));
80
+ }
81
+ catch {
82
+ // 目录不存在:glob 无可展开(字面条目不同——见上)
83
+ return [];
84
+ }
85
+ }
86
+ /**
87
+ * 构建 carve-out 判定表:全部条目展开后按「最具体路径胜出」排序(路径段数
88
+ * 多者先;同深按 deny 优先——保守)。层叠语义由排序后的首个前缀匹配实现:
89
+ * 父条目宽、子条目窄,孙条目比子条目更窄时赢回(allow 效果的用途)。
90
+ */
91
+ export function buildCarveOutTable(workspace, entries) {
92
+ const nodes = [];
93
+ for (const entry of entries) {
94
+ for (const path of expandCarveOutEntry(workspace, entry)) {
95
+ nodes.push({ path, effect: entry.effect, entry });
96
+ }
97
+ }
98
+ // 最长路径(最深)优先;同路径 deny 胜 allow(保守);再按 pattern 字典序稳定排序
99
+ return nodes.sort((a, b) => {
100
+ const depth = b.path.split(sep).length - a.path.split(sep).length;
101
+ if (depth !== 0)
102
+ return depth;
103
+ if (a.effect !== b.effect)
104
+ return a.effect === 'deny' ? -1 : 1;
105
+ return a.entry.pattern.localeCompare(b.entry.pattern);
106
+ });
107
+ }
108
+ /**
109
+ * 判定一个 canonical 绝对路径在当前策略下的可写性:carve-out 判定表首个
110
+ * 前缀命中即为其效果(无命中再看根 containment)。顺序保证「根内但被遮罩」
111
+ * 判为 carve-out(守门硬拒面),「根外」判为 outside-roots(fence 的面)——
112
+ * 两个拒绝面词汇不同、去处不同。
113
+ */
114
+ export function resolveWritability(absPath, roots, carveOut) {
115
+ // carve-out 先判:表按最具体优先排序,首个(最深的)前缀命中即生效
116
+ for (const node of carveOut) {
117
+ const inside = absPath === node.path || absPath.startsWith(node.path + sep);
118
+ if (inside) {
119
+ return node.effect === 'deny' ? { allowed: false, kind: 'carve-out', matched: node } : { allowed: true };
120
+ }
121
+ }
122
+ // 根 containment:相等或隔分隔符前缀(防 /root-evil 误判;全盘根见 isInsideRoot)
123
+ const inRoots = roots.some((root) => isInsideRoot(absPath, root));
124
+ return inRoots ? { allowed: true } : { allowed: false, kind: 'outside-roots' };
125
+ }
126
+ /**
127
+ * 组装 fs 工具族的 writableRoots provider(装配层接线位:替换 tools/fs 的
128
+ * 过渡缺省——批 8 挂账兑现)。返回的根列表按当前档位推导(mode getter 每
129
+ * 次 fence 检查取最新——read-only 空根 / danger 全盘根 / workspace-write
130
+ * 三根),已 canonical 化,与沙箱 profile 同源。
131
+ */
132
+ export function createRootsProvider(input) {
133
+ const workspace = canonicalPath(input.workspace);
134
+ return () => deriveWritableRoots(workspace, input.mode());
135
+ }
136
+ /** 绝对化工具:workspace 锚定 canonical 化(相对锚 workspace、绝对原样——守门行预检单源) */
137
+ export function absolutize(workspace, p) {
138
+ return canonicalPath(isAbsolute(p) ? resolvePath(p) : resolvePath(workspace, p));
139
+ }