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,238 @@
1
+ /**
2
+ * persist — write-behind 批落链(05 篇 §6.3)。
3
+ *
4
+ * append 热路径零 I/O(session 侧 onAppend 直通 enqueue——纯内存入队);
5
+ * 落库编舞全在本文件:微任务节流 + 批尺寸帽 64、事务序 = 入队序(单全局
6
+ * 队列——多会话事件按全局入队序交织落库,同会话序天然保真)、批级失败
7
+ * 退避重试(指数退避,耗尽 3 次 fail-loud)、毒丸分类(约束违例逐行诊断:
8
+ * 单条隔离不重试 + durable 标记 + 会话 durability 切断)、flush 屏障。
9
+ *
10
+ * 毒丸切断语义(§6.3 终态条款的实现推论):被隔离事件不落库,其会话日志
11
+ * 即现空洞——seq 连续律(恢复重放的前缀语义)下,洞后事件重放不可达;故
12
+ * 毒丸一旦确认,该会话后续事件一并不再落库(每会话一笔 incident 自述,
13
+ * 不逐条刷账)。其他会话不受牵连(「不阻塞整队列」的另一半)。
14
+ */
15
+ import { BaseError } from '../contracts/index.js';
16
+ /**
17
+ * 判别是否 SQLite 约束违例类(确定性失败——重试必再败,毒丸分类入口)。
18
+ * 结构化判别(code 字符串前缀)而非 instanceof:better-sqlite3 的 SqliteError
19
+ * 在 @types 里是构造器类型属性,instanceof 收窄面失真;code 前缀是稳定契约。
20
+ */
21
+ function isConstraintError(err) {
22
+ return (typeof err === 'object' &&
23
+ err !== null &&
24
+ 'code' in err &&
25
+ typeof err.code === 'string' &&
26
+ err.code.startsWith('SQLITE_CONSTRAINT'));
27
+ }
28
+ /**
29
+ * WriteBehind——进程级单写队列(Persistence 构造并持有;跨会话共用,
30
+ * 05 §6.1 律 2「单进程内所有打开的会话共享同一 write-behind 队列」)。
31
+ *
32
+ * 生命周期态三枚:queued(队列本体)/ draining(在飞 drain 任务——enqueue
33
+ * 只入队 + 按需点火)/ broken(fatal 熔断——此后 enqueue 即抛,诚实拒写)。
34
+ */
35
+ export class WriteBehind {
36
+ target;
37
+ maxBatchSize;
38
+ retryLimit;
39
+ backoffBaseMs;
40
+ sleep;
41
+ onFatal;
42
+ warn;
43
+ clock;
44
+ /** 待写队列(全局序 = 事务序) */
45
+ queue = [];
46
+ /** 在飞 drain 任务(null = 空闲;finally 补射防「drain 收尾与 enqueue 竞速」漏项) */
47
+ drainTask = null;
48
+ /** 微任务节流旗(已排队未点火——同 tick 内连续 enqueue 合一批) */
49
+ drainScheduled = false;
50
+ /** 毒丸诊断模式旗(本 drain 周期内逐行写——批路径在毒丸后不可回用:前段已逐行落账) */
51
+ rowMode = false;
52
+ /** 连续批级失败计数(成功清零;达 retryLimit 熔断) */
53
+ consecutiveFailures = 0;
54
+ /** 熔断旗(fatal 后诚实拒写——写链已死,再入队 = 谎报持久化) */
55
+ broken = false;
56
+ /** 已切断 durability 的会话(毒丸后——后续事件静默丢弃,不再入队落库) */
57
+ severed = new Set();
58
+ constructor(options) {
59
+ this.target = options.target;
60
+ this.maxBatchSize = options.maxBatchSize ?? 64;
61
+ this.retryLimit = options.retryLimit ?? 3;
62
+ this.backoffBaseMs = options.backoffBaseMs ?? 50;
63
+ this.sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
64
+ this.onFatal =
65
+ options.onFatal ??
66
+ ((err) => {
67
+ throw err;
68
+ });
69
+ this.warn = options.warn ?? ((message) => console.error(message));
70
+ this.clock = options.clock ?? (() => Date.now());
71
+ }
72
+ /**
73
+ * 入队(append 热路径直通——O(1) 内存操作 + 按需点火微任务)。
74
+ * 熔断后抛 PERSIST_WRITE_EXHAUSTED(诚实拒写:调用方立即知道持久化已死)。
75
+ * 切断会话静默丢弃(incident 已自述——毒丸后该会话 durability 已终)。
76
+ */
77
+ enqueue(write) {
78
+ if (this.broken) {
79
+ throw new BaseError('PERSIST_WRITE_EXHAUSTED', '写链已熔断(批级重试耗尽)——进程应退出,拒绝继续假写');
80
+ }
81
+ if (this.severed.has(write.sessionId))
82
+ return;
83
+ this.queue.push(write);
84
+ this.scheduleDrain();
85
+ }
86
+ /** 队列深度(诊断面/测试断言) */
87
+ get pending() {
88
+ return this.queue.length;
89
+ }
90
+ /** 毒丸切断会话清单(诊断只读面) */
91
+ get severedSessions() {
92
+ return [...this.severed];
93
+ }
94
+ /**
95
+ * flush 屏障(05 §4/§6.3#5):等待队列写尽并确认全部落库(含在飞 drain 与
96
+ * 退避等待——屏障后崩溃 = 已落库)。失败原样抛(毒丸不视为失败——其终态
97
+ * 在 incident 账,队列照常清空)。
98
+ */
99
+ async flush() {
100
+ if (this.broken) {
101
+ throw new BaseError('PERSIST_WRITE_EXHAUSTED', '写链已熔断——flush 无从屏障(进程应退出)');
102
+ }
103
+ this.scheduleDrain();
104
+ while (this.drainTask !== null || this.queue.length > 0 || this.drainScheduled) {
105
+ if (this.broken) {
106
+ throw new BaseError('PERSIST_WRITE_EXHAUSTED', 'flush 期间写链熔断——屏障失败(进程应退出)');
107
+ }
108
+ const task = this.drainTask;
109
+ if (task) {
110
+ await task;
111
+ }
112
+ else {
113
+ // 节流旗在排但 drain 未点火——让一个微任务(点火位先行)后再查
114
+ await new Promise((resolve) => queueMicrotask(resolve));
115
+ }
116
+ }
117
+ }
118
+ /**
119
+ * 点火 drain——微任务节流(05 §6.3):enqueue 只入队不写(append 热路径零
120
+ * I/O 的兑现位),同一 tick 内的连续 append 合一批;已排队或在飞则不重复点火。
121
+ */
122
+ scheduleDrain() {
123
+ if (this.drainScheduled || this.drainTask !== null || this.broken)
124
+ return;
125
+ this.drainScheduled = true;
126
+ queueMicrotask(() => {
127
+ this.drainScheduled = false;
128
+ // 点火时队列已空(flush 消费完)或已熔断——本次空转
129
+ if (this.broken || this.queue.length === 0)
130
+ return;
131
+ this.drainTask = this.drain()
132
+ .catch((err) => {
133
+ // fatal 熔断:标记后交 onFatal(缺省重抛 → 未捕获拒绝 → 进程 fail-loud)
134
+ this.broken = true;
135
+ this.onFatal(err instanceof BaseError ? err : new BaseError('PERSIST_WRITE_EXHAUSTED', String(err), { cause: err }));
136
+ })
137
+ .finally(() => {
138
+ this.drainTask = null;
139
+ if (!this.broken && this.queue.length > 0)
140
+ this.scheduleDrain();
141
+ });
142
+ });
143
+ }
144
+ /**
145
+ * 落库主循环(微任务链上跑——非真后台线程;节流 = 每轮微任务一批):
146
+ * 批模式(快路径)→ 约束违例翻行模式逐行诊断 → 非约束错走退避重试。
147
+ */
148
+ async drain() {
149
+ for (;;) {
150
+ if (this.queue.length === 0) {
151
+ this.rowMode = false; // 队列清空——下一周期恢复批模式
152
+ this.consecutiveFailures = 0;
153
+ return;
154
+ }
155
+ if (this.rowMode) {
156
+ const write = this.queue.shift();
157
+ if (this.severed.has(write.sessionId))
158
+ continue; // 切断会话:静默丢弃(incident 已记)
159
+ try {
160
+ this.target.writeEventSingle(write);
161
+ this.consecutiveFailures = 0;
162
+ }
163
+ catch (err) {
164
+ if (isConstraintError(err)) {
165
+ this.isolatePoison(write, err);
166
+ }
167
+ else {
168
+ this.queue.unshift(write); // 放回队首——事务序保持
169
+ if (!(await this.failOrBackoff(err)))
170
+ return;
171
+ }
172
+ }
173
+ }
174
+ else {
175
+ const batch = this.queue.splice(0, this.maxBatchSize);
176
+ try {
177
+ this.target.writeEvents(batch);
178
+ this.consecutiveFailures = 0;
179
+ }
180
+ catch (err) {
181
+ if (isConstraintError(err)) {
182
+ // 批内含毒丸:整批放回 + 翻行模式逐行定位(批事务已回滚,无损)
183
+ this.queue.unshift(...batch);
184
+ this.rowMode = true;
185
+ }
186
+ else {
187
+ this.queue.unshift(...batch);
188
+ if (!(await this.failOrBackoff(err)))
189
+ return;
190
+ }
191
+ }
192
+ }
193
+ }
194
+ }
195
+ /**
196
+ * 批级失败处置:计数 → 达帽熔断(抛出 = drain 链拒绝 → onFatal);
197
+ * 未达帽则指数退避后继续。
198
+ * @returns false = 已熔断(调用方即刻收场)
199
+ */
200
+ async failOrBackoff(err) {
201
+ this.consecutiveFailures++;
202
+ if (this.consecutiveFailures >= this.retryLimit) {
203
+ throw new BaseError('PERSIST_WRITE_EXHAUSTED', `write-behind 批级重试耗尽(连续 ${this.consecutiveFailures} 次失败)——写不进库的会话继续跑 = 谎报持久化,进程 fail-loud`, { cause: err });
204
+ }
205
+ const delay = this.backoffBaseMs * 2 ** (this.consecutiveFailures - 1);
206
+ await this.sleep(delay);
207
+ return true;
208
+ }
209
+ /**
210
+ * 毒丸隔离(§6.3 终态条款):单条不重试 + durable 标记(persist_incidents)
211
+ * + 进程级告警 + 会话 durability 切断(后续事件不再落库——seq 连续律推论,
212
+ * 见文件头注)。游标跳记账(accountDropped)让其他会话的连续性断言不受牵连。
213
+ */
214
+ isolatePoison(write, err) {
215
+ const { sessionId, event } = write;
216
+ const code = typeof err === 'object' && err !== null && 'code' in err
217
+ ? String(err.code)
218
+ : 'SQLITE_CONSTRAINT';
219
+ const message = err instanceof Error ? err.message : String(err);
220
+ this.severed.add(sessionId);
221
+ this.target.accountDropped(sessionId, event.seq);
222
+ const entry = {
223
+ time: this.clock(),
224
+ sessionId,
225
+ seq: event.seq,
226
+ type: event.type,
227
+ reason: `${code}: ${message};毒丸隔离不落库丢弃,该会话后续事件一并停写(seq 连续律破——durable 前缀止于 seq<${event.seq})`,
228
+ };
229
+ try {
230
+ this.target.recordIncident(entry);
231
+ }
232
+ catch (recordErr) {
233
+ // 标记面自身失败:告警面兜底(进程还活着就让人看得见——审计优先于静默)
234
+ this.warn(`[persist] 毒丸标记落账失败(审计缺口):${String(recordErr)}`);
235
+ }
236
+ this.warn(`[persist] 毒丸隔离:会话 ${sessionId} seq#${event.seq} ${event.type} 约束违例不落库(${message})——durable 标记已落 persist_incidents,该会话后续事件停写`);
237
+ }
238
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * L3 safety — ApprovalService(04 §9 审批与动作安全)。
3
+ *
4
+ * 机制全景:
5
+ * - 粘性短路(04 §9 粘性段定形①):ask 前先查会话粘性表——同指纹(粘性
6
+ * 第 1 款)或子集预批(第 2 款:同目标更严档位请求被既有放行包含)命中
7
+ * 即免问放行,**不产生审批对**(没有问就没有 asked/decided——审计诚实);
8
+ * - ask 档:经 approval/answer waterfall 派发给已注册 answerer(通道面短路
9
+ * 返回四值答案);无人应答 fail-closed → unavailable(source=timeout——
10
+ * headless 无人挂与超时同源);answerer 自身抛错先落 decided 闭合审计对
11
+ * 再上抛(功能面 fail-closed 由管道守门段承担,本 catch 只护审计对完整);
12
+ * - never 档:确定性拒绝回执(不发起问询——策略面定死「问都不问」;落
13
+ * decided 单腿带 source=policy-never,审计可分辨)。
14
+ *
15
+ * 审批对 durable 落账(05 篇词汇 approval/asked + approval/decided):经注入
16
+ * sink 接 session.append(装配层接线)——回放时决策已是既成事实,不重问。
17
+ *
18
+ * 升权审批(04 §8 严格变宽条)与守门审批共用本服务,reason 区分。
19
+ */
20
+ import { randomUUID } from 'node:crypto';
21
+ /** answerer 监听的活体事件名(waterfall 语义:能答者置 envelope.answer 且不调 next 短路;调 next() = 本行不接) */
22
+ export const APPROVAL_ANSWER_EVENT = 'approval/answer';
23
+ /**
24
+ * run 信号桥接(04 §9 run 信号透传条款——answerer 实现共用单源工具):把
25
+ * ApprovalRequest.signal(发起 run 的取消信号)桥进 answerer 的 per-request
26
+ * controller——abort 同 reason,run abort 与竞速败腿收束汇入同一撤销面。
27
+ * 已 abort 的 signal 同步触发(abort 事件只发一次,只挂监听则死路)。
28
+ * @returns 监听摘除函数(ask 结算时调用——迟到 abort 落在已结算提问上
29
+ * no-op 无害,摘除是「任何结算路径摘监听」不变式纪律非正确性依赖)
30
+ */
31
+ export function bridgeApprovalSignal(req, controller) {
32
+ const runSignal = req.signal;
33
+ if (runSignal === undefined)
34
+ return () => { };
35
+ const relay = () => controller.abort(runSignal.reason);
36
+ if (runSignal.aborted)
37
+ relay();
38
+ else
39
+ runSignal.addEventListener('abort', relay, { once: true });
40
+ return () => runSignal.removeEventListener('abort', relay);
41
+ }
42
+ /** 沙箱档宽度偏序秩(read-only ⊂ workspace-write ⊂ danger)——子集预批判定用 */
43
+ const TIER_RANK = {
44
+ 'read-only': 0,
45
+ 'workspace-write': 1,
46
+ danger: 2,
47
+ };
48
+ /**
49
+ * outcome → durable 决议值映射。allowed-once 落日志记 approve——「批了这
50
+ * 一次」;always 单列(第二参数,条目真实写入时的决议值——与 approve 审计
51
+ * 语义不同)。
52
+ */
53
+ function outcomeToDecision(outcome, alwaysWritten) {
54
+ if (outcome === 'allowed-once')
55
+ return alwaysWritten ? 'always' : 'approve';
56
+ switch (outcome) {
57
+ case 'rejected':
58
+ return 'reject';
59
+ case 'cancelled':
60
+ return 'cancel';
61
+ case 'unavailable':
62
+ return 'unavailable';
63
+ }
64
+ }
65
+ /**
66
+ * 组装审批服务。每次 ask 独立 approvalId(randomUUID)——审批对以此为关联
67
+ * 键落日志。构造时注册 approval/answer 事件词汇(幂等跳过——批 19c-1 修正:
68
+ * 「同 dispatch 重复组装是装配 bug」前提随多会话同栈装配废止〔in-process
69
+ * 子代理真工厂首例——每会话各装配一份审批服务共享词汇〕;归属路由恰一键
70
+ * 在 answerer 侧隔离他会话 ask,词汇层不承担装配检测)。
71
+ */
72
+ export function createApprovalService(dispatch, opts = {}) {
73
+ const policy = opts.policy ?? 'ask';
74
+ const sink = opts.sink ?? { asked: () => { }, decided: () => { } };
75
+ if (!dispatch.isRegistered(APPROVAL_ANSWER_EVENT)) {
76
+ dispatch.registerEventNames([APPROVAL_ANSWER_EVENT]);
77
+ }
78
+ /** 会话粘性表(粘性第 1/2 款内存面——随服务生命周期即会话生命周期) */
79
+ const stickyGrants = new Map(); // 键 = target
80
+ const service = {
81
+ policyMode: policy,
82
+ async ask(req) {
83
+ // 粘性短路(04 §9 粘性段定形①——ask 之前):同目标命中且请求档宽
84
+ // ≤ 已授档宽(第 1 款同指纹 = 恰等;第 2 款子集预批 = 请求更严被
85
+ // 宽批包含)→ 免问放行。不产生审批对(没有问就没有 asked/decided)。
86
+ const key = req.stickyKey;
87
+ if (key !== undefined) {
88
+ const grant = stickyGrants.get(key.target);
89
+ if (grant !== undefined && TIER_RANK[key.tier] <= TIER_RANK[grant.grantedTier]) {
90
+ return {
91
+ outcome: 'allowed-once',
92
+ source: key.tier === grant.grantedTier ? 'sticky' : 'subset',
93
+ };
94
+ }
95
+ }
96
+ const approvalId = randomUUID();
97
+ // 审批对第一腿:请求落日志(问询真实发生的开头;never 档不问不落)
98
+ if (policy !== 'never') {
99
+ sink.asked({ approvalId, summary: req.summary });
100
+ }
101
+ // 归属织入(恰一键 {sessionId})+ approvalId——answerer 的消费面载荷
102
+ const enriched = {
103
+ ...req,
104
+ approvalId,
105
+ ...(opts.ownership !== undefined ? { ownership: opts.ownership } : {}),
106
+ };
107
+ let outcome;
108
+ let source;
109
+ // always 是否真实写入条目(决议落账值分流:真写入才落 'always')
110
+ let alwaysWritten = false;
111
+ if (policy === 'never') {
112
+ // never:确定性拒绝,不派发 answerer(无人值守姿态没有「问谁」);
113
+ // decided 单腿落账(source=policy-never——审计可分辨策略拒与人拒)
114
+ outcome = 'rejected';
115
+ source = 'policy-never';
116
+ }
117
+ else {
118
+ try {
119
+ // ask:waterfall 派发——answerer 能答者写 envelope.answer 且短路
120
+ // (不调 next 即已答);全链无人写 = answer 字段 undefined
121
+ const envelope = await dispatch.waterfall(APPROVAL_ANSWER_EVENT, { req: enriched });
122
+ const answer = envelope.answer;
123
+ if (answer === 'always') {
124
+ // 「始终允许」:批准本次 + 授权写跨会话条目 + 会话粘性表入账。
125
+ // 载荷无草案或写入回调未装配 = answerer 面本不该呈现该选项,防御
126
+ // 收口视同 approve(零草案零副作用——含粘性表:一次性批准不产生
127
+ // 会话免问面,与「decided 落 approve 而非 always」同口径)
128
+ if (enriched.suggestedEntry !== undefined && opts.persistToolPolicy !== undefined) {
129
+ opts.persistToolPolicy(enriched.suggestedEntry);
130
+ alwaysWritten = true;
131
+ // 粘性表入账(stickyKey 在场时)——会话内同指纹/子集后续免问
132
+ if (key !== undefined) {
133
+ stickyGrants.set(key.target, { target: key.target, grantedTier: key.tier });
134
+ }
135
+ }
136
+ outcome = 'allowed-once';
137
+ source = 'user';
138
+ }
139
+ else {
140
+ outcome =
141
+ answer === 'approve'
142
+ ? 'allowed-once'
143
+ : answer === 'reject'
144
+ ? 'rejected'
145
+ : answer === 'cancel'
146
+ ? 'cancelled'
147
+ : // 无人应答 fail-closed:unavailable 不是「稍后再试」,是本次
148
+ // 不放行(超时/无应答者同归 timeout 源——04 §9 无静默审批条)
149
+ 'unavailable';
150
+ source = 'user';
151
+ if (outcome === 'unavailable')
152
+ source = 'timeout';
153
+ }
154
+ }
155
+ catch (cause) {
156
+ // answerer 自身缺陷的错误路收口:先落 decided 闭合审批对
157
+ // (unavailable = 确定收场,不留 asked 无 decided 的悬空审计记录)
158
+ // 再原样上抛——功能面 fail-closed 由管道守门段承担(TOOL_GATE_FAILED
159
+ // block),本 catch 只护审计对完整
160
+ sink.decided({ approvalId, decision: 'unavailable', source: 'timeout' });
161
+ throw cause;
162
+ }
163
+ }
164
+ // 审批对第二腿:决议落日志(never 档 asked 未落但 decided 单腿在——
165
+ // 审计面据 source=policy-never 分辨「问都没问」)
166
+ sink.decided({ approvalId, decision: outcomeToDecision(outcome, alwaysWritten), source });
167
+ return { outcome, source };
168
+ },
169
+ };
170
+ return service;
171
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * L3 safety — Linux bwrap(bubblewrap)后端(04 §8 执行 seam 条)。
3
+ *
4
+ * 形态:拼 mount 参数。基础面 --ro-bind / /(全系统只读视图)+ 必要的虚拟
5
+ * 设备与隔离挂载;逐根追加 --bind(真实读写)——/tmp 恒 tmpfs(临时面
6
+ * 「能用不留痕」)。argv 生成纯函数可单测。
7
+ */
8
+ import { spawnSync } from 'node:child_process';
9
+ import { resolvePolicyRoots } from './sandbox.js';
10
+ /**
11
+ * bwrap argv 前缀(不含策略差异):全系统只读 + 虚拟 /dev /proc + 隔离 PID
12
+ * 命名空间。/tmp tmpfs 恒前置为第一条挂载:bwrap 按参数序建挂载,若 tmpfs
13
+ * /tmp 出现在某 /tmp 子路径根的 bind 之后,会整树遮蔽该 bind(berry 刀四
14
+ * CI 首跑红根因:bind 先、tmpfs 后 → 挂载点不可见;tmpfs 先、bind 后 →
15
+ * bwrap 在 tmpfs 上自动造出 dest 挂载点,根正确露出——顺序即正确性)。
16
+ */
17
+ function bwrapBaseArgs() {
18
+ return [
19
+ '--tmpfs',
20
+ '/tmp',
21
+ '--ro-bind',
22
+ '/',
23
+ '/',
24
+ '--dev',
25
+ '/dev',
26
+ '--proc',
27
+ '/proc',
28
+ '--unshare-pid',
29
+ '--die-with-parent',
30
+ ];
31
+ }
32
+ /**
33
+ * 敏感件读 deny 遮蔽参数(04 §7 读侧 carve-out + 2026-09-08 P0① 定形):
34
+ * `--ro-bind-try /dev/null <canonical 路径>` 逐件一对。要点:
35
+ * - SRC 恒在场(/dev/null 必在)、DEST 由 bwrap 自建(不必预先存在);
36
+ * `-try` 形 = DEST 缺席跳过不报错(保护面不含「目录不存在即失败」语义);
37
+ * - 后位遮蔽:mount 点后建遮蔽前挂载——必须排在全部既有 bind 之后(末位
38
+ * 追加),否则被后续 bind 整树覆盖(顺序即正确性,与 base 的 tmpfs 前置
39
+ * 同一律);
40
+ * - 遮蔽形下硬链攻击链结构性失败:link() 跨 mount 点对只读遮蔽源操作不可
41
+ * 达(内核 errno 多形不钉死——实机核验定形)。
42
+ */
43
+ function bwrapDenyArgs(policy) {
44
+ const args = [];
45
+ for (const p of policy.denyReadFiles ?? [])
46
+ args.push('--ro-bind-try', '/dev/null', p);
47
+ return args;
48
+ }
49
+ /**
50
+ * 写 deny 遮蔽参数(04 §252 腿二——成熟度缺口 #9):`--ro-bind-try <path>
51
+ * <path>` 逐件一对——把宿主该路径**只读**挂载遮蔽沙箱内同名路径(写即
52
+ * read-only file system 拒)。与读 deny 同律末位追加(后位遮蔽——排在全部
53
+ * 既有 bind 之后);`-try` 形 DEST 缺席跳过(.git 未建的会话新建可写——
54
+ * 已文档化边界:新建仓非篡改既有版本史)。danger 档 `--bind / /` 后追加
55
+ * 同律(底线不交档位)。
56
+ */
57
+ function bwrapDenyWriteArgs(policy) {
58
+ const args = [];
59
+ for (const p of policy.denyWritePaths ?? [])
60
+ args.push('--ro-bind-try', p, p);
61
+ return args;
62
+ }
63
+ /**
64
+ * 按策略生成 bwrap 参数前缀(纯函数)。两档统一消费 resolvePolicyRoots——
65
+ * 缺省按档位推导(read-only 空根 = 无 rw bind、workspace-write 工作区根族),
66
+ * 显式 writableRoots 覆盖在两档同等生效(与 seatbeltProfile 同律)。不变式:
67
+ * /tmp 恒 tmpfs 由 base 前缀承接;根恰为 /tmp 时跳过重复挂载;/tmp 子路径
68
+ * 根在 tmpfs 之上 bind 露出(见 base 前缀注记)。读 deny 遮蔽行末位追加
69
+ * (后位遮蔽——见 bwrapDenyArgs 注记)。
70
+ *
71
+ * danger 档形(04 §8 定形②「任何档一律」):`--bind / /` 全盘真读写 +
72
+ * /dev /proc + 遮蔽行——跳过 tmpfs /tmp 与 --ro-bind /(danger 不走只读基
73
+ * 座);--unshare-pid / --die-with-parent 卫生旗恒保留(/proc 同 uid 进程
74
+ * 面结构性不可见——c-4 泄漏面③同判收口)。
75
+ */
76
+ export function bwrapArgs(policy) {
77
+ if (policy.mode === 'danger') {
78
+ return [
79
+ '--bind',
80
+ '/',
81
+ '/',
82
+ '--dev',
83
+ '/dev',
84
+ '--proc',
85
+ '/proc',
86
+ '--unshare-pid',
87
+ '--die-with-parent',
88
+ ...bwrapDenyArgs(policy),
89
+ ...bwrapDenyWriteArgs(policy),
90
+ ];
91
+ }
92
+ const args = [...bwrapBaseArgs()];
93
+ for (const root of resolvePolicyRoots(policy)) {
94
+ // 可写根与 fs fence 同源;/tmp 已由 base tmpfs 覆盖,其余根真实 bind
95
+ if (root !== '/tmp')
96
+ args.push('--bind', root, root);
97
+ }
98
+ args.push(...bwrapDenyArgs(policy), ...bwrapDenyWriteArgs(policy));
99
+ return args;
100
+ }
101
+ /**
102
+ * 组装 Linux bwrap 后端。
103
+ * 后端差异数据化下发:
104
+ * - 策略拒绝 → stderr 含 "read-only file system"(只读 bind 上的写标准 errno 文案);
105
+ * - runner 自身失败(bwrap 未装/参数错/权限不够)→ stderr 前缀 "bwrap: "。
106
+ */
107
+ export function createBwrapBackend() {
108
+ return {
109
+ id: 'bwrap',
110
+ enforcement: 'full',
111
+ denialSignatures: ['read-only file system'],
112
+ runnerFailureRules: [{ fatalSignatures: ['bwrap: '] }],
113
+ wrap(argv, policy) {
114
+ return ['bwrap', ...bwrapArgs(policy), '--', ...argv];
115
+ },
116
+ probe(timeoutMs) {
117
+ // 功能性探测:真跑一次 read-only 包装的 /bin/true——status 0 才算后端可用
118
+ const confined = ['bwrap', ...bwrapArgs({ mode: 'read-only', workspaceRoot: '/' }), '--', '/bin/true'];
119
+ const result = spawnSync(confined[0], confined.slice(1), { timeout: timeoutMs });
120
+ return result.status === 0;
121
+ },
122
+ };
123
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * safety 域错误码注册(04 §8 沙箱栈——SANDBOX_ 前缀族首批)。
3
+ *
4
+ * 前缀族明列见 contracts/errors.ts ERROR_CODE_PREFIXES(02 §5.3 #1);码名清
5
+ * 单与语义真源 = 02 篇 §5.3 SANDBOX_ 族明列段(2026-09-06 safety 纵切批)。
6
+ * 本文件由模块公开面 index.ts 引入(注册纪律:写入点文件必须实际 import 本
7
+ * 文件注册才发生——与 tools/llm/session 的 codes.ts 同款)。
8
+ */
9
+ import { registerErrorCodes } from '../contracts/index.js';
10
+ registerErrorCodes([
11
+ {
12
+ code: 'SANDBOX_UNAVAILABLE',
13
+ module: 'safety',
14
+ description: '策略要求沙箱而后端链空/全部探测失败——fail-closed 拒执行绝不裸跑(04 §8 执行 seam 条)',
15
+ },
16
+ {
17
+ code: 'SANDBOX_MODE_INVALID',
18
+ module: 'safety',
19
+ description: 'sandbox/mode 事件折叠遇非三档词汇——静默跳过坏事件会沿用旧档属 fail-open,宁响亮失败(04 §8 策略三级解析条)',
20
+ },
21
+ {
22
+ code: 'SANDBOX_ESCALATION_INVALID',
23
+ module: 'safety',
24
+ description: '升权参数残缺:sandbox_permissions 与 justification 不成对非空 / 目标档非法 / 非严格变宽(04 §8 严格变宽升权条)',
25
+ },
26
+ // DANGER_ 六码(04 §13 危险工具闸——02 §5.3 族明细 2026-09-09 #4 规范先行批同笔入册)
27
+ {
28
+ code: 'DANGER_CONSENT_ABSENT',
29
+ module: 'safety',
30
+ description: '危险闸 consent 缺席(fail-closed 缺省——缺席 = 闸落地零配置零行为变化,deny 路径即原「阻塞转人审」语义;指路 /danger approve 签发)',
31
+ },
32
+ {
33
+ code: 'DANGER_CONSENT_INVALID',
34
+ module: 'safety',
35
+ description: '危险闸 consent 过期或 mandate 漂移(哈希不符——批了 A 契约不等于批了 B 契约;两形同码:过期不自动续不静默宽限、漂移即死——重签唯一出路)',
36
+ },
37
+ {
38
+ code: 'DANGER_HALTED',
39
+ module: 'safety',
40
+ description: '危险闸 HALT 哨兵在场(存在性判内容不解析、stat 不可达按在场——touch 即全停;指路删除数据目录下 HALT 文件恢复)',
41
+ },
42
+ {
43
+ code: 'DANGER_TARGET_DENIED',
44
+ module: 'safety',
45
+ description: '危险闸动作/目标不在 mandate 值域(action 越闭集或 target 不匹配 targets——04 §13 闸序步 4)',
46
+ },
47
+ {
48
+ code: 'DANGER_CAP_EXCEEDED',
49
+ module: 'safety',
50
+ description: '危险闸当日成功帽已满(allow-succeeded 计数 UTC 日界恢复——账本派生无第二状态;提帽须重签 consent)',
51
+ },
52
+ {
53
+ code: 'DANGER_LEDGER_CORRUPT',
54
+ module: 'safety',
55
+ description: '危险闸审计账本链坏拒续写(改/删任何历史行则哈希传播破坏其后全部——账本完整性先于可用性;修复归人手,截断/重建是人工决策)',
56
+ },
57
+ ]);