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,214 @@
1
+ /**
2
+ * L3 safety — 沙箱 seam(04 §8 执行 seam 与 fail-closed 条)。
3
+ *
4
+ * confine(argv, policy) → ConfinedArgv 是纯包装接口:后端无状态、策略逐调用
5
+ * 携带、消费方(bash 工具等)自行 spawn——沙箱不造进程,只约束进程。后端
6
+ * 差异数据化下发(denialSignatures / runnerFailureRules),消费方零后端知识。
7
+ * 无可用后端抛 SANDBOX_UNAVAILABLE——fail-closed,绝不静默裸跑。
8
+ *
9
+ * 本文件同时持有:effective mode 三级解析的 fold 半边(显式档是调用方的
10
+ * 事,会话 override 事件与部署缺省在此折叠)+ 升权词汇(严格变宽阶梯 /
11
+ * 成对必填校验 / 统一拒绝标记)——bash 与 fs 两族工具共享同一 home,防文
12
+ * 案与顺序漂移。
13
+ *
14
+ * 与 berry 分叉注记:①第三档定名 `danger`(短名——04 §8 拍板);②折叠
15
+ * 缺省档 = workspace-write(04 §8「个人开发机形态的实用缺省」;berry 的
16
+ * read-only 兜底不承)。
17
+ */
18
+ import { BaseError } from '../contracts/index.js';
19
+ import { canonicalPath, deriveWritableRoots } from './roots.js';
20
+ import { sensitiveReadFiles } from './sensitive.js';
21
+ // 平台链引用(函数体内才调用,无顶层互调——与后端文件的双向引用安全)
22
+ import { createSeatbeltBackend } from './seatbelt.js';
23
+ import { createBwrapBackend } from './bwrap.js';
24
+ /**
25
+ * 策略实际生效的可写根列表(缺省按档位推导;两个后端与 fs fence 共用同一
26
+ * 来源。read-only 档空根——Seatbelt read-only profile 本就全拒写(不吃根列
27
+ * 表)、Bwrap 空根即无 rw bind)。
28
+ */
29
+ export function resolvePolicyRoots(policy) {
30
+ return policy.writableRoots ?? deriveWritableRoots(policy.workspaceRoot, policy.mode);
31
+ }
32
+ /* ------------------------------------------------------------------ */
33
+ /* 三级解析的 fold 半边(04 §8 策略三级解析条)。 */
34
+ /* 实况注记(2026-09-11 遗漏扫描批定形):本函数 = 词先锚定的读位半边—— */
35
+ /* sandbox/mode 落账词在册(05 §1.1)而 v1 无会话中切档面(TUI/webui/CLI */
36
+ /* 均无切档命令),零 append 写点;生产生效档 = boot 闭包 CLI 旗标形 */
37
+ /* (conversation-stack options.sandboxMode——run-entry --read-only 唯一 */
38
+ /* 注入源)。会话策略中间档随切档面立题接线,届时本函数即 fold 读点。 */
39
+ /* ------------------------------------------------------------------ */
40
+ /** 三档词汇守卫(fold 与配置解析共用;拼错档位必须响亮失败) */
41
+ export function isSandboxMode(value) {
42
+ return value === 'read-only' || value === 'workspace-write' || value === 'danger';
43
+ }
44
+ /**
45
+ * 折叠会话 sandbox/mode 事件序列 → 生效档(最后一条胜出)。缺省档 =
46
+ * workspace-write(04 §8「个人开发机形态的实用缺省」——与 berry 的
47
+ * read-only 兜底分叉)。载荷不在三档词汇内直接抛 SANDBOX_MODE_INVALID——
48
+ * 静默跳过坏事件会沿用旧档,是 fail-open。
49
+ */
50
+ export function resolveEffectiveMode(events, fallback = 'workspace-write') {
51
+ let mode = fallback;
52
+ for (const event of events) {
53
+ if (!isSandboxMode(event.mode)) {
54
+ throw new BaseError('SANDBOX_MODE_INVALID', `sandbox/mode 事件档位非法:${JSON.stringify(event.mode)}(三档词汇:read-only / workspace-write / danger)`);
55
+ }
56
+ mode = event.mode;
57
+ }
58
+ return mode;
59
+ }
60
+ /**
61
+ * 组装沙箱服务。选后端规则(单候选链不预探测):
62
+ * - 空链 → confine 抛 SANDBOX_UNAVAILABLE(fail-closed);
63
+ * - 单候选 → 直接使用不预探测(探测有 spawn 开销,且坏了 spawn 会响亮
64
+ * 失败,runnerFailureRules 负责分类);
65
+ * - 多候选 → 按注册序找首个 probe 通过者(无 probe 视为可用)——探测结果
66
+ * 按后端缓存一次,探测失败的候选不重试。
67
+ */
68
+ export function createSandboxService(opts = {}) {
69
+ /** 后端链(注册序即优先序) */
70
+ const chain = [...(opts.backends ?? createDefaultBackends())];
71
+ /** probe 结果缓存(backend.id → 布尔;注销即清除) */
72
+ const probeCache = new Map();
73
+ /**
74
+ * 策略 enrich:读 deny 单源补位 + 数据目录写 deny 恒并入。
75
+ * - 读侧(04 §7):未显式携带 denyReadFiles 且装配持有 dataDir 时,以敏
76
+ * 感件集单源补位。显式携带(含空数组)恒胜出——测试/宿主覆盖位不与
77
+ * 单源打架。
78
+ * - 写侧(04 §7/§14「模型不可自授」的 bash 腿):dataDir 在场即无条件
79
+ * 并入 denyWritePaths——danger 档 allow-default 下 `> ~/.berry-agent/
80
+ * tool-policy.json`〔原 allowlist.json——更名批同笔〕自授面此前敞门,本条封死;恒 = 平台底线不交装配
81
+ * 裁量,调用方携带值与 enrich 取并集去重。
82
+ */
83
+ const enrichPolicy = (policy) => {
84
+ let enriched = policy;
85
+ if (enriched.denyReadFiles === undefined && opts.dataDir != null) {
86
+ enriched = { ...enriched, denyReadFiles: sensitiveReadFiles(opts.dataDir) };
87
+ }
88
+ if (opts.dataDir != null) {
89
+ const dataDirCanonical = canonicalPath(opts.dataDir);
90
+ const existing = enriched.denyWritePaths ?? [];
91
+ if (!existing.includes(dataDirCanonical)) {
92
+ enriched = { ...enriched, denyWritePaths: [...existing, dataDirCanonical] };
93
+ }
94
+ }
95
+ return enriched;
96
+ };
97
+ /** 单个后端的可用性判定(无 probe = 视为可用;结果缓存一次) */
98
+ const isAvailable = (backend) => {
99
+ if (!backend.probe)
100
+ return true;
101
+ if (!probeCache.has(backend.id)) {
102
+ // 功能性探测:真跑一次 read-only 包装(后端实现负责),status 0 才证明内核确实执行
103
+ probeCache.set(backend.id, backend.probe(PROBE_TIMEOUT_MS));
104
+ }
105
+ return probeCache.get(backend.id);
106
+ };
107
+ const service = {
108
+ confine(argv, policy) {
109
+ if (chain.length === 0) {
110
+ // fail-closed:没有后端就拒绝执行,绝不静默裸跑(04 §8——任何档一律,
111
+ // danger 不豁免:换档不是绕后端的路)
112
+ throw new BaseError('SANDBOX_UNAVAILABLE', `无可用沙箱后端,拒绝裸跑(后端链为空;${policy.mode} 档一律经沙箱——可安装沙箱后端)`);
113
+ }
114
+ // 单候选直接用;多候选按 probe 仲裁
115
+ const backend = chain.length === 1 ? chain[0] : (chain.find((b) => isAvailable(b)) ?? undefined);
116
+ if (!backend) {
117
+ throw new BaseError('SANDBOX_UNAVAILABLE', `后端链全部探测失败(${chain.map((b) => b.id).join(' → ')}),拒绝以 ${policy.mode} 档裸跑`);
118
+ }
119
+ return {
120
+ argv: backend.wrap(argv, enrichPolicy(policy)),
121
+ enforcement: backend.enforcement,
122
+ denialSignatures: backend.denialSignatures,
123
+ runnerFailureRules: backend.runnerFailureRules,
124
+ };
125
+ },
126
+ registerBackend(backend) {
127
+ chain.push(backend);
128
+ let done = false;
129
+ return () => {
130
+ if (done)
131
+ return;
132
+ done = true;
133
+ // 同位注销护栏:仅当链尾仍是本后端时弹出(防误撤他者注册的同 id 后端)
134
+ if (chain[chain.length - 1] === backend) {
135
+ chain.pop();
136
+ probeCache.delete(backend.id);
137
+ }
138
+ };
139
+ },
140
+ listBackends() {
141
+ return [...chain];
142
+ },
143
+ };
144
+ return service;
145
+ }
146
+ /** 功能性探测超时(毫秒):真跑一次包装不该超过这个时长,超过视为不可用 */
147
+ const PROBE_TIMEOUT_MS = 5_000;
148
+ /** 平台默认后端链(04 §8:macOS Seatbelt / Linux bwrap;其余平台无后端——confine fail-closed,如实空链不假装覆盖) */
149
+ export function createDefaultBackends() {
150
+ if (process.platform === 'darwin')
151
+ return [createSeatbeltBackend()];
152
+ if (process.platform === 'linux')
153
+ return [createBwrapBackend()];
154
+ return [];
155
+ }
156
+ /* ------------------------------------------------------------------ */
157
+ /* 升权词汇(04 §8 严格变宽升权条;bash 与 fs 共享同一 home) */
158
+ /* ------------------------------------------------------------------ */
159
+ /** 严格变宽阶梯:只许变宽不许变窄绕行(read-only→两档 / workspace-write→danger / danger 无更宽) */
160
+ export const WIDER_MODES = {
161
+ 'read-only': ['workspace-write', 'danger'],
162
+ 'workspace-write': ['danger'],
163
+ danger: [],
164
+ };
165
+ /** 可请求的升权目标档(= 比受限两档更宽的「两档受限/全放」词汇中更宽者) */
166
+ export const ESCALATION_TARGETS = ['workspace-write', 'danger'];
167
+ /**
168
+ * 升权参数校验(04 §8 严格变宽条全在此):sandbox_permissions 与
169
+ * justification 强制成对非空(空句同非法);目标必须是合法升权档;必须
170
+ * 严格变宽(非变宽请求不弹窗直接拒,防窄绕行与同档重试噪音)。
171
+ */
172
+ export function validateEscalationArgs(args) {
173
+ const perm = args.sandboxPermissions?.trim();
174
+ const just = args.justification?.trim();
175
+ // 成对非空:一边有一边无 = 参数残缺,不给问询机会(04 §8「拒在 schema 段」)
176
+ if (!perm && !just) {
177
+ throw new BaseError('SANDBOX_ESCALATION_INVALID', '升权请求缺 sandbox_permissions 与 justification(两者必须成对提供)');
178
+ }
179
+ if (!perm || !just) {
180
+ throw new BaseError('SANDBOX_ESCALATION_INVALID', `升权参数不成对:sandbox_permissions=${JSON.stringify(perm ?? '')} justification=${JSON.stringify(just ?? '')}(两者必须成对非空)`);
181
+ }
182
+ if (!isSandboxMode(perm) || !ESCALATION_TARGETS.includes(perm)) {
183
+ throw new BaseError('SANDBOX_ESCALATION_INVALID', `升权目标档非法:${perm}(合法目标:${ESCALATION_TARGETS.join(' / ')})`);
184
+ }
185
+ if (!WIDER_MODES[args.current].includes(perm)) {
186
+ // 非严格变宽(变窄或同档):不问询直接拒——变窄绕行与无意义重试都不进审批
187
+ throw new BaseError('SANDBOX_ESCALATION_INVALID', `升权请求非严格变宽:${args.current} → ${perm}(当前档只可升至 ${WIDER_MODES[args.current].join(' / ') || '(已是最高档)'})`);
188
+ }
189
+ return { target: perm, justification: just };
190
+ }
191
+ /** 统一拒绝标记:被策略拒绝时回给模型的固定句式(工具结果与守门 block 共用) */
192
+ export function sandboxDenialMarker(mode) {
193
+ return `[sandbox: file access denied under ${mode}]`;
194
+ }
195
+ /** 升权提示标记:拒绝后引导模型基于真实 denial 走正道(明确不许投机) */
196
+ export function escalationHintMarker() {
197
+ return '[sandbox hint: 若确需本次访问,在同一工具调用中同时提供 sandbox_permissions(workspace-write 或 danger)与 justification 发起升权;拒绝是最终的——不许绕路、不许无真实拒绝依据的投机升权]';
198
+ }
199
+ /**
200
+ * 走审批的升权序列:校验产物 → approval.ask(reason 注明目标档与理由)→
201
+ * 原样返回结果元数据。allowed-once 的「只授予当次调用」由调用方保证(把
202
+ * 返回的 target 仅用于本次 spawn,不写回会话 override)。
203
+ */
204
+ export function requestEscalation(approval, input) {
205
+ return approval.ask({
206
+ summary: `沙箱升权 ${input.current} → ${input.target}`,
207
+ reason: `目标档 ${input.target};理由:${input.justification}`,
208
+ toolName: input.toolName,
209
+ toolCallId: input.toolCallId,
210
+ // run 取消信号随 ask 载荷透传(undefined 不携带)
211
+ ...(input.signal !== undefined ? { signal: input.signal } : {}),
212
+ ...(input.suggestedEntry !== undefined ? { suggestedEntry: input.suggestedEntry } : {}),
213
+ });
214
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * L3 safety — macOS Seatbelt 后端(04 §8 执行 seam 条:后端 = 数据 + 包装)。
3
+ *
4
+ * 形态:拼 SBPL profile 字符串,argv = ['sandbox-exec','-p',profile,'--',...argv]。
5
+ * profile 生成是纯函数(可单测不 spawn);probe 用功能性探测——真跑一次
6
+ * read-only 包装(版本检查会漏「有 syscall 但拒绝执行」的内核形态)。
7
+ */
8
+ import { spawnSync } from 'node:child_process';
9
+ import { resolvePolicyRoots } from './sandbox.js';
10
+ /** SBPL 字符串字面量转义(SBPL 语法内 " 与 \ 需反斜杠转义) */
11
+ function sbplString(value) {
12
+ return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
13
+ }
14
+ /** read-only 基座 SBPL:全默认放行 + 拒写 + /dev/null 例外(大量 CLI 静默写 /dev/null) */
15
+ export function seatbeltReadOnlyProfile() {
16
+ return ['(version 1)', '(allow default)', '(deny file-write*)', '(allow file-write* (literal "/dev/null"))'].join('\n');
17
+ }
18
+ /**
19
+ * 敏感件读 deny 行(04 §7 读侧 carve-out + 2026-09-08 P0① 实机核验定形):
20
+ * `(deny file-read* (literal "<canonical 路径>"))` 逐件一行。要点:
21
+ * - 末位追加——SBPL last-match-wins,末位 deny 压过此前一切 allow;
22
+ * - literal 必须是符号链解析后的真实路径(/tmp 拼写字面 miss /private/tmp
23
+ * 实体——denyReadFiles 数据源 sensitiveReadFiles 已 canonical 派生);
24
+ * - 单行 deny 结构性覆盖硬链攻击链:link() 对源路径即触发 file-read* 判定
25
+ * ——「链时即拦」,无需独立 link 算子(实机核验:ln 直接 Operation not
26
+ * permitted)。
27
+ */
28
+ function seatbeltDenyLines(policy) {
29
+ return (policy.denyReadFiles ?? []).map((p) => `(deny file-read* (literal ${sbplString(p)}))`);
30
+ }
31
+ /**
32
+ * 写 deny 行(04 §252 腿二——成熟度缺口 #9):`(deny file-write* (subpath
33
+ * "<canonical 路径>"))` 逐件一行,恒末位追加(SBPL last-match-wins——压过
34
+ * 此前一切 allow,含 workspace-write 档的逐根 allow 与 danger 档的
35
+ * allow default;subpath 覆盖整树)。read-only 档与全局 `(deny file-write*)`
36
+ * 并存冗余无害——三档统一形。
37
+ */
38
+ function seatbeltDenyWriteLines(policy) {
39
+ return (policy.denyWritePaths ?? []).map((p) => `(deny file-write* (subpath ${sbplString(p)}))`);
40
+ }
41
+ /**
42
+ * 按策略生成 SBPL profile(纯函数)。两档统一消费 resolvePolicyRoots——缺省
43
+ * 按档位推导(read-only 空根 = 纯拒写、workspace-write 工作区根族),显式
44
+ * writableRoots 覆盖在两档同等生效(字段契约本义:e1 式宿主只读档携刚需根
45
+ * 即走此路——berry 真机冒烟实证原 mode 分支吃不到显式根的教训照搬防御)。
46
+ *
47
+ * danger 档形(04 §8 定形②「任何档一律」):(version 1) + (allow default)
48
+ * + 读 deny 行——无拒写、无逐根 allow;danger 同过最小读 deny profile。
49
+ * 写 deny 行两档同律恒末位(含 danger——04 §252 底线不交档位)。
50
+ */
51
+ export function seatbeltProfile(policy) {
52
+ const denies = seatbeltDenyLines(policy);
53
+ const writeDenies = seatbeltDenyWriteLines(policy);
54
+ if (policy.mode === 'danger') {
55
+ return ['(version 1)', '(allow default)', ...denies, ...writeDenies].join('\n');
56
+ }
57
+ const allows = resolvePolicyRoots(policy)
58
+ .map((root) => `(allow file-write* (subpath ${sbplString(root)}))`)
59
+ .join('\n');
60
+ return allows
61
+ ? [seatbeltReadOnlyProfile(), allows, ...denies, ...writeDenies].join('\n')
62
+ : [seatbeltReadOnlyProfile(), ...denies, ...writeDenies].join('\n');
63
+ }
64
+ /**
65
+ * 组装 macOS Seatbelt 后端。
66
+ * 后端差异数据化下发(消费方零后端知识):
67
+ * - 策略拒绝 → stderr 含 "operation not permitted"(seatbelt 拒写标准句);
68
+ * - runner 自身失败(profile 语法错/内核拒绝加载)→ stderr 前缀 "sandbox-exec: "。
69
+ */
70
+ export function createSeatbeltBackend() {
71
+ return {
72
+ id: 'seatbelt',
73
+ enforcement: 'full',
74
+ denialSignatures: ['operation not permitted'],
75
+ runnerFailureRules: [{ fatalSignatures: ['sandbox-exec: '] }],
76
+ wrap(argv, policy) {
77
+ const profile = seatbeltProfile(policy);
78
+ return ['sandbox-exec', '-p', profile, '--', ...argv];
79
+ },
80
+ probe(timeoutMs) {
81
+ // 功能性探测:真跑一次 read-only 包装的 /usr/bin/true——status 0 才证明
82
+ // 内核确实执行了 profile(不是「命令存在」而是「策略真的生效」)
83
+ const confined = ['sandbox-exec', '-p', seatbeltReadOnlyProfile(), '--', '/usr/bin/true'];
84
+ const result = spawnSync(confined[0], confined.slice(1), { timeout: timeoutMs });
85
+ return result.status === 0;
86
+ },
87
+ };
88
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * L3 safety — 敏感件读集单源(04 §7 读侧 carve-out + 03 §10.9 凭证代管 + 04 §9 allowlist)。
3
+ *
4
+ * SENSITIVE_READ_BASENAMES = dataDir 直下「恒不可读」件名的唯一字面源,消费
5
+ * 面 = fs 工具族读脸(装配层注入 tools)与沙箱 profile 读 deny(confine
6
+ * enrich)两条执法腿的同源数据。
7
+ *
8
+ * 对拍锚(字面镜像):persist/secret-box.ts 的 SECRET_KEY_BASENAME 与
9
+ * host/tool-policy-store.ts 的 TOOL_POLICY_BASENAME。safety 不 import
10
+ * persist/host(DAG 边表——与 gate.dataDir 必填注入同律),字面同步由对拍
11
+ * 测试互证(漂移即红)。
12
+ */
13
+ import { join } from 'node:path';
14
+ import { canonicalPath } from './roots.js';
15
+ /**
16
+ * 敏感件 basename 集(canonical dataDir 直下)。
17
+ * - secret.key:会话密钥(03 §10.9 / persist secret-box——密文与凭据的根钥);
18
+ * - tool-policy.json:策略表面(04 §9——免问/拒绝条目本体泄出即审批面测绘;
19
+ * 2026-09-11 审批分档批更名,原 allowlist.json);
20
+ * - allowlist.json:旧载体名留置保护(更名批共存序「旧文件留置不删」——
21
+ * 旧文件在数据目录滞留期间同不可读,防升格遗存读敞门)。
22
+ * canonical 派生不可省:literal deny(seatbelt)与 ro-bind 遮蔽(bwrap)
23
+ * 都必须落在符号链解析后的真实路径上才生效(/tmp → /private/tmp 字面
24
+ * miss——2026-09-08 实机核验定形)。
25
+ */
26
+ export const SENSITIVE_READ_BASENAMES = ['secret.key', 'tool-policy.json', 'allowlist.json'];
27
+ /**
28
+ * 敏感件 canonical 绝对路径集(路径判与 inode 判的比对基准;沙箱读 deny 行
29
+ * 的数据源)。逐件 canonical 派生而非整目录——canonicalPath(dataDir) 目录
30
+ * 存在时两者等价,目录缺席(首启未建)时逐件回退最近在场祖先仍稳定。
31
+ */
32
+ export function sensitiveReadFiles(dataDir) {
33
+ return SENSITIVE_READ_BASENAMES.map((name) => canonicalPath(join(dataDir, name)));
34
+ }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * L3 safety — 跨会话工具策略表匹配引擎(04 §9 粘性第 3/4 款 + 2026-09-11
3
+ * 审批分档批定形块③④——纯函数半边;原「allowlist 匹配引擎」随文件更名
4
+ * tool-policy.json 同步升格「工具策略表」概念)。
5
+ *
6
+ * 定位:条目双面——**allow = advisory 免问面**(只影响「问不问」,不折算
7
+ * 执行权:fence / 可写根 / carve-out 硬拒面 / 执行段照走);**deny = 用户
8
+ * 主权硬拒**(不可被 sticky / allow / policy-never / sandbox danger 任何
9
+ * 后续面翻转——④deny 优先律:首个 deny 命中恒最先且硬拒,reason
10
+ * `policy-deny:<条目序>`)。deny 永远最高,本引擎 deny 命中即终局返回。
11
+ * 条目落用户配置层(`~/.berry-agent/tool-policy.json`,非模型可写面——
12
+ * §7 fence;存储读写接线在 host 件,本文件定形状与判定语义)。
13
+ *
14
+ * 三族匹配器(04 §9 粘性段定形③,族分派沿旧):
15
+ * - fs 族(write / edit):条目 pattern = 路径前缀(相对则锚 workspace)——
16
+ * 本次调用**全部**写目标都落在前缀内才命中(多路径 all-or-nothing,保守);
17
+ * - bash 族:条目 pattern = 命令词干(≤2 词:命令 [子命令])——剥壳语义全
18
+ * 集见 commandStem(环境变量前缀剥除 / shell 包装穿透一层 / 管道与串接
19
+ * 即不可判定 / 任何 flag 即不可判定——v1 无「已知 flag 白名单」,白名单
20
+ * 随真实命令谱定稿;git -C 换仓走私被「flag 即 miss」自然覆盖);
21
+ * - 其余工具族:工具名整匹配(pattern 字段忽略,留位后续扩族)。
22
+ *
23
+ * TTL(粘性第 4 款):**仅 allow 条目**——`expiresAt`(Unix 毫秒)过期 =
24
+ * 未命中(回落 ask),缺省 = 用户显式选择永久;deny 条目**无 TTL**(带
25
+ * `expiresAt` 的 deny 条目属坏形——deny 自动过期 = 自动变宽松,方向性不
26
+ * 允许;引擎按逐条剔除语义跳过,与读侧坏形剔除同形)。
27
+ *
28
+ * 档位限定(定形块③偏序包含判):条目 `effect?` 缺席 = 全档;在场时——
29
+ * **allow 条目窄化自限**(只作用于该档及以下:write 档 allow 条目不覆盖
30
+ * exec 调用)、**deny 条目覆写扩面**(作用于该档及以上:write 档 deny
31
+ * 条目覆盖 write+exec 调用)——方向性同律:allow 不自动放宽、deny 不自动
32
+ * 收紧。
33
+ */
34
+ import { basename, resolve as resolvePath, sep } from 'node:path';
35
+ import { TOOL_TIER_RANK } from '../contracts/index.js';
36
+ import { canonicalPath } from './roots.js';
37
+ /** fs 写路径工具族(与 gate 守门行认知的写意图工具同源;导出供守门行判定收窄共用) */
38
+ export const FS_WRITE_TOOLS = new Set(['write', 'edit']);
39
+ /**
40
+ * 命中来源标注串(ap-3 三面同源单源):守门行 deny 理由 / allow 免问
41
+ * allowReason、/approval explain 干跑呈现、session_status 整名族干跑呈现
42
+ * ——三面共用本函数,执法与呈现漂移在结构上不可能(同函数同条目同串)。
43
+ */
44
+ export function policyHitNote(match) {
45
+ return `policy-${match.entry.decision}:${match.index}`;
46
+ }
47
+ /**
48
+ * 判定入口(04 §9 定形块④评估序的纯函数半边):**deny 命中恒最先且终局**
49
+ * ——全表扫描中首个 deny 命中即返回(**序在 allow 之前恒胜**:条目序不构成
50
+ * deny 与 allow 的优先级,deny 是不同维度的主权裁决);无 deny 命中时返回
51
+ * 首个未过期 allow 命中;无命中返回 undefined。
52
+ * 保守原则:任何不可判定(管道/串接/引号/flag/条目形状非法/deny 带 TTL)
53
+ * 都按未命中处理——照问照审,不做假静态分析(deny 坏形剔除同向:被剔除的
54
+ * deny 条目不参与评估,其余条目照常)。
55
+ */
56
+ export function matchToolPolicy(entries, input, now) {
57
+ let firstAllow;
58
+ for (let index = 0; index < entries.length; index++) {
59
+ const entry = entries[index];
60
+ if (entry.tool !== input.tool)
61
+ continue;
62
+ // 偏序包含判(③):缺席全档;allow 及以下 / deny 及以上
63
+ if (!tierIncluded(entry, input.effect))
64
+ continue;
65
+ // deny 条目带 expiresAt = 坏形 → 逐条剔除(跳过继续扫,非整档失效)。
66
+ // decision 缺席防御:仅精确 'deny' 走硬拒腿(旧形行经读侧归一为 allow)
67
+ if (entry.decision === 'deny') {
68
+ if (entry.expiresAt !== undefined)
69
+ continue;
70
+ if (entryMatches(entry, input))
71
+ return { index, entry };
72
+ continue;
73
+ }
74
+ // TTL(粘性第 4 款,仅 allow):过期 = 未命中(回落 ask;过期条目由配置面提示清理)
75
+ if (entry.expiresAt !== undefined && now >= entry.expiresAt)
76
+ continue;
77
+ if (firstAllow === undefined && entryMatches(entry, input))
78
+ firstAllow = { index, entry };
79
+ }
80
+ return firstAllow;
81
+ }
82
+ /** 档位偏序包含判(③):allow 条目只作用于该档及以下(窄化自限)、deny 条目作用于该档及以上(覆写扩面) */
83
+ function tierIncluded(entry, callEffect) {
84
+ if (entry.effect === undefined)
85
+ return true;
86
+ const callRank = TOOL_TIER_RANK[callEffect];
87
+ const entryRank = TOOL_TIER_RANK[entry.effect];
88
+ return entry.decision === 'deny' ? callRank >= entryRank : callRank <= entryRank;
89
+ }
90
+ /** 三族匹配分派(fs 前缀 all-or-nothing / bash 词干 / 整名族) */
91
+ function entryMatches(entry, input) {
92
+ if (FS_WRITE_TOOLS.has(entry.tool)) {
93
+ return fsPrefixHit(entry.pattern, input);
94
+ }
95
+ if (entry.tool === 'bash') {
96
+ const command = input.bashCommand ?? '';
97
+ return command.trim().length > 0 && matchesCommandStem(entry.pattern ?? '', command);
98
+ }
99
+ // 整名族:工具名相等即命中(TTL/档位判已在前)
100
+ return true;
101
+ }
102
+ /** fs 族判定:全部写目标落在条目前缀内(前缀到路径分隔边界——/app 不匹配 /apple);pattern 缺席/不可解析 = 条目无效 = 不命中 */
103
+ function fsPrefixHit(pattern, input) {
104
+ if (typeof pattern !== 'string' || pattern.length === 0)
105
+ return false;
106
+ const paths = input.writePaths ?? [];
107
+ if (paths.length === 0)
108
+ return false; // 无写意图不归本族(整名语义由他族条目表达)
109
+ let prefix;
110
+ try {
111
+ // 相对 pattern 锚 workspace;canonical 化与 fence/根推导同源
112
+ prefix = canonicalPath(resolvePath(input.workspace ?? process.cwd(), pattern));
113
+ }
114
+ catch {
115
+ return false; // pattern 不可解析 = 条目无效 = 不命中
116
+ }
117
+ return paths.every((p) => p === prefix || p.startsWith(prefix + sep));
118
+ }
119
+ /** 不可判定字符集:管道 / 串接 / 重定向 / 命令替换 / 换行——任一出现即照问 */
120
+ const INDETERMINATE = /[|;&<>$(`)\n]/;
121
+ /** 引号字符(剥离 shell 包装引号后仍残留即视为不可判定) */
122
+ const QUOTES = /["'`]/;
123
+ /** shell 包装器名(穿透一层取内层命令) */
124
+ const WRAPPERS = new Set(['sh', 'bash', 'zsh', 'dash', 'ksh']);
125
+ /** 包装器选项中含 c(-c / -lc / -ec——payload 在下一个词) */
126
+ const WRAPPER_C_OPTION = /^-[a-z]*c[a-z]*$/;
127
+ /**
128
+ * 剥壳取主命令词干(≤2 词:命令 [子命令])——「始终允许」bash 草案生成器
129
+ * (04 §9 粘性段定形③:fs = 精确路径、bash = 剥壳词干——与 matchesCommandStem
130
+ * 同源同实现,剥壳全集共享,双实现漂移结构性不可能)。不可判定(管道/串接/
131
+ * 重定向/命令替换/换行/残留引号/flag——全局无害三件除外)返回 undefined:
132
+ * 剥不出干净词干即无草案,「始终允许」选项不呈现。
133
+ */
134
+ export function commandStem(command) {
135
+ // 换行(含 \r)是命令分隔符,且会被下方 \s+ 分词吞掉——必须先于分词对原文
136
+ // 检查(INDETERMINATE 其余字符均非空白、分词后仍存活;唯换行需要前置)
137
+ if (/[\r\n]/.test(command))
138
+ return undefined;
139
+ // 剥壳:环境变量前缀赋值 + shell 包装穿透一层
140
+ let tokens = command.trim().split(/\s+/).filter(Boolean);
141
+ while (tokens.length > 0 && /^[A-Za-z_][A-Za-z0-9_]*=\S*$/.test(tokens[0])) {
142
+ tokens = tokens.slice(1);
143
+ }
144
+ if (tokens.length === 0)
145
+ return undefined;
146
+ let head = basename(tokens[0]);
147
+ if (WRAPPERS.has(head)) {
148
+ // 找 -c 类选项后的 payload 词(选项序列之后的第一个非选项词)
149
+ let i = 1;
150
+ let sawC = false;
151
+ while (i < tokens.length && tokens[i].startsWith('-') && WRAPPER_C_OPTION.test(tokens[i])) {
152
+ sawC = true;
153
+ i += 1;
154
+ }
155
+ if (sawC && i < tokens.length) {
156
+ // payload = 选项后全部剩余词拼接(引号内空格已被分词拆散——拼接还原后再剥引号)
157
+ let payload = tokens.slice(i).join(' ');
158
+ // 剥整段包裹引号;引号内含空白则重新分词(只此一层,不递归)
159
+ if (payload.length >= 2 &&
160
+ ((payload.startsWith("'") && payload.endsWith("'")) || (payload.startsWith('"') && payload.endsWith('"')))) {
161
+ payload = payload.slice(1, -1);
162
+ }
163
+ tokens = payload.split(/\s+/).filter(Boolean);
164
+ head = tokens.length > 0 ? basename(tokens[0]) : '';
165
+ }
166
+ }
167
+ if (tokens.length === 0 || head === '')
168
+ return undefined;
169
+ // 不可判定:命令全文任一危险字符 / 残留引号 → 无词干(草案不生成)
170
+ const peeled = tokens.join(' ');
171
+ if (INDETERMINATE.test(peeled) || QUOTES.test(peeled))
172
+ return undefined;
173
+ // 剩余参数保守判定:flag 即无词干(--help/-h/--version 三件无害除外)
174
+ const rest = tokens.slice(1);
175
+ for (const arg of rest) {
176
+ if (arg.startsWith('-') && arg !== '--help' && arg !== '-h' && arg !== '--version')
177
+ return undefined;
178
+ }
179
+ // 词干 = 主命令 [子命令](≤2 词;子命令存在且非 flag 才纳入)
180
+ const cmd = basename(tokens[0]);
181
+ const sub = tokens[1];
182
+ return sub !== undefined && !sub.startsWith('-') ? `${cmd} ${sub}` : cmd;
183
+ }
184
+ /**
185
+ * bash 族判定:剥壳与不可判定全权委托 commandStem(同源同实现——草案生成
186
+ * 与本判定共用同一套剥壳全集);本函数只做 pattern(≤2 词)对词干的**逐词
187
+ * 前缀对齐**——pattern 单词(如 `git`)匹配「该命令 + 任意非 flag 形参」
188
+ * (`git status` 命中 `git` 条目),双词条目(`git push`)要求词干恰好两词
189
+ * 且逐词相等。
190
+ */
191
+ function matchesCommandStem(pattern, command) {
192
+ // pattern 解析:≤2 词(命令 [子命令]);超长/空 = 条目无效
193
+ const patternTokens = pattern.trim().split(/\s+/).filter(Boolean);
194
+ if (patternTokens.length === 0 || patternTokens.length > 2)
195
+ return false;
196
+ const stem = commandStem(command);
197
+ if (stem === undefined)
198
+ return false; // 剥不出干净词干(不可判定)即 miss
199
+ const stemTokens = stem.split(' ');
200
+ if (patternTokens.length > stemTokens.length)
201
+ return false;
202
+ return patternTokens.every((token, i) => token === stemTokens[i]);
203
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * L3 safety — 公共类型词汇(04 §8 沙箱栈 / §9 审批与动作安全)。
3
+ *
4
+ * 两个正交旋钮(04 §9):sandbox 档(三档文件效果)× 审批策略档(ask /
5
+ * never)——两闸独立,不合成一个「安全级别」糊面。预设只是用户面打包,
6
+ * 组合逻辑不进执行路径。
7
+ *
8
+ * 与 berry 分叉注记:三档第三席本仓定名 `danger`(04 §8 拍板句「机器面
9
+ * 枚举字面以短名为准」——berry 的 `danger-full-access` 不承,两名并存已
10
+ * 于 2026-09-06 终结);审批请求归属恰一键 `{sessionId}`(立项改造裁决:
11
+ * 承 berry 删插件位)。
12
+ */
13
+ export {};
@@ -0,0 +1,54 @@
1
+ /**
2
+ * scheduler 域错误码注册(04 §12 调度条——SCHEDULER_ 前缀族;02 §5.3 批 15a
3
+ * 明列八码)。
4
+ *
5
+ * 码语义分层:四码守 /tick 六动词与程序面的行管理(NAME_INVALID/NAME_EXISTS/
6
+ * NOT_FOUND/JOB_INVALID);一码管 schedule 串词法(SCHEDULE_INVALID——
7
+ * GoalJobsFace register 坏串响亮拒不炸装配时 message 即引此码);三码管
8
+ * OS cron 可选后端(CRON_UNSUPPORTED/CRON_UNAUTHORIZED/CRON_WRITE_FAILED
9
+ * ——04 §12「OS 定时注册态」已裁乙案条的跨平台处置与人面授权链执法面)。
10
+ * 本文件由模块公开面 index.ts 引入(注册纪律:import 发生才注册)。
11
+ */
12
+ import { registerErrorCodes } from '../contracts/index.js';
13
+ registerErrorCodes([
14
+ {
15
+ code: 'SCHEDULER_NAME_INVALID',
16
+ module: 'scheduler',
17
+ description: '任务名词法违例——须匹配 ^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$(goal 挂钟行名 goal-<goalId> 同型合法)',
18
+ },
19
+ {
20
+ code: 'SCHEDULER_NAME_EXISTS',
21
+ module: 'scheduler',
22
+ description: '名冲突守卫——add/挂钟 register 撞既有行拒(jobs 表行即唯一事实源)',
23
+ },
24
+ {
25
+ code: 'SCHEDULER_NOT_FOUND',
26
+ module: 'scheduler',
27
+ description: '幽灵名零行守卫——enable/disable/rm 作用行缺席响亮拒(04 §12 /tick 瘦身条 2026-09-04 同款守卫)',
28
+ },
29
+ {
30
+ code: 'SCHEDULER_SCHEDULE_INVALID',
31
+ module: 'scheduler',
32
+ description: 'schedule 串词法/语义坏形——四形 every:/once@+相对/once@绝对/daily@/weekly@ 之外,含 interval 下限 5s 违反与 cron 后端 sub-minute/非 60 倍数拒',
33
+ },
34
+ {
35
+ code: 'SCHEDULER_JOB_INVALID',
36
+ module: 'scheduler',
37
+ description: '行载荷坏——prompt 空/超 16KiB、cwd 非绝对路径或建行时不存在',
38
+ },
39
+ {
40
+ code: 'SCHEDULER_CRON_UNSUPPORTED',
41
+ module: 'scheduler',
42
+ description: 'cron 可选后端诚实拒——win32 无用户 crontab 形态、后端缺席形态下启用 OS 注册面(04 §12 OS 定时注册态已裁条)',
43
+ },
44
+ {
45
+ code: 'SCHEDULER_CRON_UNAUTHORIZED',
46
+ module: 'scheduler',
47
+ description: '人面授权链未过——写系统 crontab 需一次人面授权,授权拒即拒写(04 §12 OS 定时注册态已裁条)',
48
+ },
49
+ {
50
+ code: 'SCHEDULER_CRON_WRITE_FAILED',
51
+ module: 'scheduler',
52
+ description: 'crontab 读改写链失败——保留他人行的写回失败亮拒不静默(以 # berry-agent:<名> 标记行定位自有条目)',
53
+ },
54
+ ]);