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,152 @@
1
+ /**
2
+ * credentials — oauth 刷新链(03 §10.9 oauth bullet 刷新/轮换三振细则;
3
+ * c-6 落码批)。
4
+ *
5
+ * 形态 = 件内自持挂钟(obs/service.ts 同形先例:setInterval + unref;
6
+ * interval 0 = 不自驱——测试经 tick() 手动驱动)。巡检面 = 流注册表 ×
7
+ * 存储行 meta:到期位(expiresAt)落入提前量(缺省 5 分钟)即经 refresh
8
+ * token 换新。
9
+ *
10
+ * 三律(保留上次有效值律细则——02 §7 张力细则同源):
11
+ * 1. **成功 rotate**:主行换新 token(source 'refresh'、failures 清零、
12
+ * expired 位随整列换消失);端点下发新 refresh token 才动刷新行
13
+ * (RFC 6749 §6 不下发即复用旧值);附加键保全(账号句柄等插件自记
14
+ * meta 键不因链写抹掉)。
15
+ * 2. **失败保留旧值续用**:值不动只记 failures++(durable——连续计数);
16
+ * 单败走 warn(日志面);invalid_grant(EXPIRED 码)= 授权态坏重试
17
+ * 无益,直落三振语义。
18
+ * 3. **三振 notify 不执法**:到阈值(缺省 3)置 meta.expired 告示位 +
19
+ * notify 用户(只通知——旧 token 可能仍有效,c-5 list 已呈现「已过期
20
+ * ——保留上次有效值」);notify 只在转换位发(已 expired 行继续失败
21
+ * 不重复告警);撤销唯一路径 = 人面 rm(链不清删)。
22
+ *
23
+ * 窄面注入律:存储四法(CredentialsCommandStore——commands.ts 同面)+
24
+ * 注册表 + fetch/now/notify/warn 全注入;本件零宿主依赖,纯逻辑可测。
25
+ */
26
+ import { BaseError } from '../contracts/index.js';
27
+ import { pluginNamespace } from './types.js';
28
+ import { refreshOAuthToken } from './oauth.js';
29
+ /** 剔除 failures/expired 两键的 meta 残余(链管键整列换、插件自记附加键保全) */
30
+ function stripChainKeys(meta) {
31
+ const { failures: _failures, expired: _expired, ...rest } = meta;
32
+ return rest;
33
+ }
34
+ /**
35
+ * 构造刷新链。巡检逐流隔离(单流失败不连坐他流);重入护栏(上一拍未收口
36
+ * 跳过本拍——慢端点下不叠拍)。
37
+ */
38
+ export function createRefreshChain(deps) {
39
+ const { store, registry, fetchFn } = deps;
40
+ const now = deps.now;
41
+ const notify = deps.notify;
42
+ const warn = deps.warn;
43
+ const aheadMs = deps.aheadMs ?? 5 * 60 * 1000;
44
+ const maxFailures = deps.maxFailures ?? 3;
45
+ let timer;
46
+ let running = false;
47
+ /** 单流巡检(读到写全包——单流异常折 warn 不炸整拍) */
48
+ const refreshOne = async (flow) => {
49
+ const ns = pluginNamespace(flow.pluginId);
50
+ const name = flow.def.name;
51
+ let row;
52
+ let meta;
53
+ try {
54
+ row = store.getCredential(ns, name);
55
+ if (row === undefined)
56
+ return; // 未授权过——不归链管
57
+ meta = (row.meta ?? {});
58
+ if (typeof meta.expiresAt !== 'number')
59
+ return; // 无到期位(manual/静态)不归链管
60
+ if (meta.expiresAt - now() > aheadMs)
61
+ return; // 未到提前量
62
+ }
63
+ catch (err) {
64
+ warn(`凭证 ${ns}/${name} 刷新巡检读侧异常:${err instanceof Error ? err.message : String(err)}`);
65
+ return;
66
+ }
67
+ const wasExpired = meta.expired === true;
68
+ const refreshName = typeof meta.refreshName === 'string' ? meta.refreshName : undefined;
69
+ if (refreshName === undefined)
70
+ return; // 单 token 形(无刷新行)——到期不自动续
71
+ try {
72
+ const refreshRow = store.getCredential(ns, refreshName);
73
+ if (refreshRow === undefined) {
74
+ throw new BaseError('CREDENTIALS_OAUTH_EXPIRED', `refresh 凭据行 ${refreshName} 缺席(已被人面 rm)——授权态不完整,重新发起授权流 /credentials oauth ${flow.pluginId} ${name}。`);
75
+ }
76
+ const grant = await refreshOAuthToken(flow.def, refreshRow.apiKey, { fetchFn, now });
77
+ // 成功 rotate:主行换新(source 'refresh'、failures/expired 随整列换消失;
78
+ // 端点未给 expires_in 则保留旧到期位)
79
+ store.setCredential(ns, name, {
80
+ apiKey: grant.accessToken,
81
+ meta: { ...stripChainKeys(meta), source: 'refresh', expiresAt: grant.expiresAt ?? meta.expiresAt },
82
+ });
83
+ // 新 refresh token 才动刷新行(不下发即复用——RFC 6749 §6)
84
+ if (grant.refreshToken !== undefined && grant.refreshToken !== refreshRow.apiKey) {
85
+ store.setCredential(ns, refreshName, { apiKey: grant.refreshToken, meta: { source: 'refresh' } });
86
+ }
87
+ // credentials/changed 审计 seam(值域 05 §1.1 单源:刷新轮换 = rotate/oauth-flow)
88
+ deps.onCredentialChanged?.({ namespace: ns, name, action: 'rotate', origin: 'oauth-flow' });
89
+ }
90
+ catch (err) {
91
+ // 失败保留旧值续用:值不动,failures 递增(durable)
92
+ const failures = (typeof meta.failures === 'number' ? meta.failures : 0) + 1;
93
+ // invalid_grant(EXPIRED 码)= 授权态坏——直落三振语义(重试无益不空转三拍)
94
+ const stateBroken = err instanceof BaseError && err.code === 'CREDENTIALS_OAUTH_EXPIRED';
95
+ const strike = stateBroken || failures >= maxFailures;
96
+ store.setCredential(ns, name, {
97
+ apiKey: row.apiKey, // 保留上次有效值(铁律——链不清删不清值)
98
+ meta: {
99
+ ...stripChainKeys(meta),
100
+ refreshName,
101
+ expiresAt: meta.expiresAt,
102
+ failures,
103
+ ...(strike ? { expired: true } : {}),
104
+ },
105
+ });
106
+ if (strike) {
107
+ // 三振 notify(只通知不执法——旧 token 可能仍有效;唯一转换位发,已 expired 行不重复告警)
108
+ if (!wasExpired) {
109
+ notify(`凭证 ${ns}/${name} 刷新${stateBroken ? '被拒(授权态坏——refresh 凭据失效或行不完整)' : `连续 ${failures} 次失败`}——已标记过期,保留上次有效值;重新授权:/credentials oauth ${flow.pluginId} ${name}`);
110
+ }
111
+ else {
112
+ warn(`凭证 ${ns}/${name} 刷新仍失败(已过期告示在案,第 ${failures} 次):${errText(err)}`);
113
+ }
114
+ }
115
+ else {
116
+ warn(`凭证 ${ns}/${name} 刷新失败(第 ${failures} 次,阈值 ${maxFailures}):${errText(err)}——保留旧值续用`);
117
+ }
118
+ }
119
+ };
120
+ return {
121
+ async tick() {
122
+ if (running)
123
+ return; // 重入护栏——上一拍未收口跳过本拍
124
+ running = true;
125
+ try {
126
+ for (const flow of registry.list()) {
127
+ await refreshOne(flow);
128
+ }
129
+ }
130
+ finally {
131
+ running = false;
132
+ }
133
+ },
134
+ start(intervalMs) {
135
+ this.stop(); // 幂等重启(重复 start 先清旧钟)
136
+ const ms = intervalMs ?? 60_000;
137
+ timer = setInterval(() => void this.tick(), ms);
138
+ // 不阻进程退出(obs/service.ts 同律——挂钟是后台巡检非存活语义)
139
+ timer.unref?.();
140
+ },
141
+ stop() {
142
+ if (timer !== undefined) {
143
+ clearInterval(timer);
144
+ timer = undefined;
145
+ }
146
+ },
147
+ };
148
+ }
149
+ /** 非 BaseError 错误折文案(oauth.ts errText 同形——本件零依赖不回流) */
150
+ function errText(err) {
151
+ return err instanceof Error ? err.message : String(err);
152
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * credentials — 读腿服务面工厂(03 §2.2 能力面第十面 + §10.9 读腿;c-3 落码批)。
3
+ *
4
+ * 消费形(§2.2 钉词面):**服务面**——宿主装配序逐插件 fork 作用域
5
+ * `provide('secrets', …本工厂产物…)`,插件经 `ctx.get("secrets")` 取自域
6
+ * 绑定版(服务闭包携带 pluginId——插件面永不自报身份,防冒名)。
7
+ *
8
+ * 三动词执法全景:
9
+ * - `get(name)`:自域读——物理键恒 `(plugin:<本插件 id>, name)`,越域名
10
+ * 恒解析为本域(不存在路径式逃逸:name 是 provider 作用名、非 namespace
11
+ * 载体);缺席拒 `CREDENTIALS_NOT_FOUND`。
12
+ * - `get(name, { namespace })`:显式越域读——值域好形判('host' | 好形
13
+ * plugin:<id>,坏形拒 `CREDENTIALS_NAMESPACE_DENIED` 同码分流)→ 高危面
14
+ * 门检 `credentials.read-cross`(§4.6 v1 首批第四枚——未开门拒同码,
15
+ * message 底稿 = 门检 verdict 原文指路 opens 授予位;core: 官方件直开
16
+ * 豁免——triggers.ts 同律:装配即用户意图)→ 开门后逐次 `capability/used`
17
+ * 审计 seam(缺省 no-op = 测试形;生产装配已接线 audit_events 真发射
18
+ * ——assembly U3 批 U3-5,2026-09-13 勘正旧挂账注记)。
19
+ * - `set(name, value, meta?)`:恒自域写——受理窗判定(宿主回调窗内可达,
20
+ * 窗外拒 `CREDENTIALS_WRITE_WINDOW_CLOSED`;缺省恒窗外 = fail-closed);
21
+ * 写成功落 `credentials/changed` 审计 seam(action 'rotate'/origin
22
+ * 'oauth-flow'——05 §1.1 值域单源:oauth 首写与刷新轮换均计 rotate;
23
+ * 人面 add/remove 的 'add'/'remove'·'human' 随 c-5)。
24
+ * - `registerOAuthFlow(spec)`(c-6 第十面第三动词):装载窗内注册 oauth
25
+ * 流(窗外拒 PLUGIN_WINDOW_CLOSED——12f-2a 装载窗执法码;回调窗内
26
+ * 动态注册不开放:流注册是装载期声明,注册面无运行时增量)。注册只入
27
+ * host-owned 注册表,不触存储——授权写入发生在流被用户发起时的回调窗
28
+ * handler 内(set 动词)。
29
+ *
30
+ * in-process TCB 诚实成文(§10.9):get 返回明文值给插件码面(它要外联
31
+ * 必然持有值);本腿防的是磁盘/env 明文落盘与跨插件串读,不防同进程内存
32
+ * 窥探。值恒不入 durable 面、恒不出模型面(注入腿 c-4 执法)。
33
+ *
34
+ * 窄面注入律:本件 DAG 无 host/context 边——窗判定/开门集/审计发射全经
35
+ * 构造注入(词面独立律:受局面结构兼容 host 装配桥真身,compat 互证归
36
+ * 本件测试)。
37
+ */
38
+ import { BaseError } from '../contracts/index.js';
39
+ // internal 桶机制符号深导(门检裁决核——03 §4.6;开门是宿主裁决面非插件
40
+ // API;DEEP_FACES 面册 sanctioned 位,triggers.ts 同律消费)
41
+ import { adjudicateCapabilityDoor } from '../contracts/api.js';
42
+ import { pluginNamespace, isPluginNamespace } from './types.js';
43
+ /** 高危面名(本件门检唯一消费位——§4.6 v1 首批第四枚) */
44
+ const READ_CROSS_CAPABILITY = 'credentials.read-cross';
45
+ /**
46
+ * 构造插件凭证面(host 装配序 createContext 位逐插件调用——fork 作用域
47
+ * provide('secrets', 产物))。
48
+ */
49
+ export function createSecretsFace(options) {
50
+ const { store, pluginId } = options;
51
+ const selfNamespace = pluginNamespace(pluginId);
52
+ const getOpens = options.getOpens ?? (() => new Set());
53
+ const inWriteWindow = options.inWriteWindow ?? (() => false);
54
+ return {
55
+ get(name, opts) {
56
+ const target = opts?.namespace;
57
+ // 缺省/显式自域:恒自域读(越域名不是载体——name 是 provider 作用名,
58
+ // 「显式指定自域」等价缺省形,不开门不审计)
59
+ if (target === undefined || target === selfNamespace) {
60
+ const entry = store.getCredential(selfNamespace, name);
61
+ if (entry === undefined) {
62
+ throw new BaseError('CREDENTIALS_NOT_FOUND', `凭证 ${name} 不在本插件域(${selfNamespace})——人面录入路径 /credentials add(03 §10.9 读腿)`);
63
+ }
64
+ return entry.apiKey;
65
+ }
66
+ // 越域读:先值域好形('host' | 好形 plugin:<id>——坏形同码分流拒;
67
+ // isPluginNamespace 单源判据,裸前缀/无前缀形全拒)
68
+ if (target !== 'host' && !isPluginNamespace(target)) {
69
+ throw new BaseError('CREDENTIALS_NAMESPACE_DENIED', `namespace「${target}」坏形——值域 = 'host' | 'plugin:<id>'(03 §10.9 namespace 归属列值域单源,types.ts 判据)`);
70
+ }
71
+ // 门检(§4.6——credentials.read-cross 默认关)。core: 官方件直开豁免
72
+ // (装配即用户意图——triggers.ts 同律;capability/used 审计照记)
73
+ if (!pluginId.startsWith('core:')) {
74
+ const verdict = adjudicateCapabilityDoor(getOpens(), READ_CROSS_CAPABILITY);
75
+ if (!verdict.ok) {
76
+ throw new BaseError('CREDENTIALS_NAMESPACE_DENIED', `${verdict.message}(插件 ${pluginId})`);
77
+ }
78
+ }
79
+ const entry = store.getCredential(target, name);
80
+ if (entry === undefined) {
81
+ throw new BaseError('CREDENTIALS_NOT_FOUND', `凭证 ${name} 不在目标域(${target})——越域读命中空名同响亮拒`);
82
+ }
83
+ // 开门后逐次审计(05 §1.1 capability/used——audit_events 真发射:生产
84
+ // 装配已接线 assembly U3 批 U3-5;core: 直开豁免同记审计——豁免免的是门不是账)
85
+ options.onCapabilityUsed?.({ pluginId, capability: READ_CROSS_CAPABILITY, namespace: target, name });
86
+ return entry.apiKey;
87
+ },
88
+ set(name, value, meta) {
89
+ // 受理窗判定(宿主回调窗内可达——唯一合法流 = 用户发起 oauth 授权流)
90
+ if (!inWriteWindow()) {
91
+ throw new BaseError('CREDENTIALS_WRITE_WINDOW_CLOSED', `凭证写在受理窗外被拒(插件 ${pluginId}——ctx.secrets.set 只在宿主回调窗内可达:用户发起授权流 → 宿主回调插件 handler → 窗内写,03 §10.9 写入面复合案)`);
92
+ }
93
+ // 恒自域写(无跨域写面——同读腿隔离律)
94
+ store.setCredential(selfNamespace, name, { apiKey: value, meta });
95
+ // credentials/changed 审计 seam(值恒不入载荷——05 §1.1 立词条款;action/
96
+ // origin 值域同源:oauth 首写与刷新轮换均计 rotate,流内写 = 'oauth-flow')
97
+ options.onCredentialChanged?.({ namespace: selfNamespace, name, action: 'rotate', origin: 'oauth-flow' });
98
+ },
99
+ registerOAuthFlow(spec) {
100
+ const wiring = options.oauth;
101
+ // 受局面缺位执法(装配缺陷响亮——plugin-context required() 同判据同码;
102
+ // 词面归 context 域,BaseError 携码不校验归属,本件 DAG 无 context 边
103
+ // 故字面直用,漂移由全链测试互证)
104
+ if (wiring === undefined) {
105
+ throw new BaseError('CONTEXT_SERVICE_MISSING', `注册动词 ctx.secrets.registerOAuthFlow 的受局面 oauth(流注册表)缺席(插件 ${pluginId}——装配根未接线;装配缺陷 fail-loud)`);
106
+ }
107
+ // 装载窗执法(12f-2a 窗执法码 PLUGIN_WINDOW_CLOSED——词面归 host 域,
108
+ // 同上字面直用;本面严于通律:回调窗内动态注册不开放——流注册是装载
109
+ // 期声明,注册面无运行时增量)
110
+ if (!wiring.inLoadWindow()) {
111
+ throw new BaseError('PLUGIN_WINDOW_CLOSED', `注册动词 ctx.secrets.registerOAuthFlow 在装载窗口外被拒(插件 ${pluginId}——流注册只在 apply 执行期间合法,03 §10.9 oauth 案)`);
112
+ }
113
+ // 入 host-owned 注册表((pluginId, name) 分键——同插件同名后写胜出,
114
+ // 跨插件结构性不撞;openWriteWindow 绑本插件 handle——invoke 时开窗)
115
+ wiring.registry.register(pluginId, spec, wiring.openWriteWindow);
116
+ },
117
+ };
118
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * credentials — 契约词面件(03 §10.9 凭证代管面)。
3
+ *
4
+ * namespace 值域与 meta 键约定的**单源**(物理载体在 persist 的 credentials
5
+ * 表——05 §9 扩形列;本件只立词面,值域执法在 c-3 读腿受理位):
6
+ * - namespace 列即归属列(立题档「namespace / 归属列」合取定形):'host'
7
+ * (宿主自用域——模型 API key 与静态人面凭证)| 'plugin:<id>'(插件域);
8
+ * - meta 列住条目来源(manual / oauth / refresh)与 expired 状态位
9
+ * (保留上次有效值律——条目不清删,撤销唯一路径 = 人面 rm)。
10
+ *
11
+ * persist 面签名用裸 string(DAG:credentials → persist 单向——persist 不
12
+ * import 本件,值域类型不回流物理层)。
13
+ */
14
+ /** 宿主域 namespace(模型 API key 射程界桩 + 静态人面凭证归属——03 §10.9) */
15
+ export const HOST_NAMESPACE = 'host';
16
+ /**
17
+ * 插件域 namespace 构造器(c-3 读腿「恒自域」的物理键合成位——受理面以
18
+ * 本插件 id 构造,插件面永不手拼字符串)。
19
+ */
20
+ export function pluginNamespace(pluginId) {
21
+ return `plugin:${pluginId}`;
22
+ }
23
+ /** 插件域判定(值域窄化——'host' 与坏形前缀均 false) */
24
+ export function isPluginNamespace(namespace) {
25
+ return namespace.startsWith('plugin:') && namespace.length > 'plugin:'.length;
26
+ }
27
+ /** 插件域反解(取插件 id;非插件域返回 null——跨域审计/列示面消费) */
28
+ export function parsePluginNamespace(namespace) {
29
+ return isPluginNamespace(namespace) ? namespace.slice('plugin:'.length) : null;
30
+ }
@@ -0,0 +1,279 @@
1
+ /**
2
+ * bash 工具件(04 §8 全规格:发现序/超时帽/后台化截获/沙箱消费/失败二分)。
3
+ *
4
+ * 参数面恰 {command, timeoutMs?, cwd?, sandbox_permissions?, justification?}——
5
+ * 无 stdin、无 env 注入、无后台化。升权闭包(会话内审批缓存/粘性)归驱动层,
6
+ * 本件纯逻辑腿只做 allowed-once 语义:审批产物 target 只用于本次 spawn,不写
7
+ * 回会话档(04 §8「只授予当次调用」)。
8
+ *
9
+ * 失败二分的工具面分报:spawn 阶段失败(EXEC_SPAWN_FAILED——bash 不在场/
10
+ * runner 没跑起来)与执行阶段失败(exitCode ≠ 0 → isError)走两条腿;沙箱
11
+ * 策略拒绝(denialSignatures 命中)单独成第三腿——拒绝标记 + 升权提示引导
12
+ * 模型走正道,拒绝是最终的不许投机。
13
+ */
14
+ import { accessSync, constants as fsConstants } from 'node:fs';
15
+ import { delimiter, join } from 'node:path';
16
+ import { BaseError } from '../contracts/index.js';
17
+ import { Type } from 'typebox';
18
+ import { canonicalPath, deriveWritableRoots, escalationHintMarker, requestEscalation, sandboxDenialMarker, validateEscalationArgs, } from '../safety/index.js';
19
+ import { BASH_TIMEOUT_DEFAULT_MS, BASH_TIMEOUT_MAX_MS } from './types.js';
20
+ import { findGitRedirectViolations, isGitMetadataExempt, worktreeGitDir } from './git-guard.js';
21
+ /**
22
+ * bash 发现序(04 §8):BERRY_AGENT_BASH_PATH > 系统 PATH 逐目录 X_OK 扫描。
23
+ * 候选命中 WSL launcher(win32 的 bash.exe 系 WSL 代跳板——非真 bash)视为
24
+ * 缺席继续扫;全链缺席 fail-loud 抛 EXEC_SPAWN_FAILED(不降级 cmd——降级会
25
+ * 制造半兼容 shell 的静默行为分叉)。
26
+ * @param env 环境源(BERRY_AGENT_BASH_PATH 与 PATH 的读面;缺省 process.env)
27
+ */
28
+ export function discoverBash(env = process.env) {
29
+ const override = env.BERRY_AGENT_BASH_PATH;
30
+ if (override !== undefined && override !== '') {
31
+ // 显式指名不可用 = 配置错:fail-loud 带修复提示(不静默回落 PATH——回落
32
+ // 会掩盖配置失效,用户以为在用指定 bash 实则不是)
33
+ assertExecutable(override);
34
+ return override;
35
+ }
36
+ const pathValue = env.PATH ?? '';
37
+ const launcherNames = new Set(['bash.exe', 'wsl.exe', 'wsl']);
38
+ for (const dir of pathValue.split(delimiter)) {
39
+ if (dir === '')
40
+ continue;
41
+ const candidate = join(dir, 'bash');
42
+ if (!assertExecutable(candidate, true))
43
+ continue;
44
+ // WSL launcher 防线:win32 下名为 bash 的实为 WSL 代跳板(basename 族)
45
+ const base = candidate.split(/[\\/]/).pop() ?? '';
46
+ if (process.platform === 'win32' && launcherNames.has(`${base}.exe`))
47
+ continue;
48
+ return candidate;
49
+ }
50
+ throw new BaseError('EXEC_SPAWN_FAILED', 'bash 不在场(PATH 全链无 X_OK 命中;不降级 cmd)——可经 BERRY_AGENT_BASH_PATH 显式指名');
51
+ }
52
+ /** X_OK 可执行判定(soft 形返回 false;hard 形抛 EXEC_SPAWN_FAILED 带路径提示) */
53
+ function assertExecutable(path, soft = false) {
54
+ try {
55
+ accessSync(path, fsConstants.X_OK);
56
+ return true;
57
+ }
58
+ catch (error) {
59
+ if (soft)
60
+ return false;
61
+ throw new BaseError('EXEC_SPAWN_FAILED', `BERRY_AGENT_BASH_PATH 指名的 bash 不可执行:${path}(${error instanceof Error ? error.message : String(error)})`, { cause: error });
62
+ }
63
+ }
64
+ /** 后台化截获(04 §8「无后台化」:模型不能脱管留后台进程) */
65
+ export function assertNoBackgroundCommand(command) {
66
+ const trimmed = command.trimEnd();
67
+ // 尾部单 &(不含 && 逻辑与——两字符形是串行算子)= 后台化
68
+ if (trimmed.endsWith('&') && !trimmed.endsWith('&&')) {
69
+ throw new BaseError('EXEC_BACKGROUND_REJECTED', '命令尾部单 &(后台化)被拒——模型不能脱管留后台进程;长任务用 timeoutMs 显式控预算');
70
+ }
71
+ // 命令位 nohup/disown(首 token):挂断免疫/脱管语义同为后台化面
72
+ const firstToken = command.trimStart().split(/\s+/)[0] ?? '';
73
+ if (firstToken === 'nohup' || firstToken === 'disown') {
74
+ throw new BaseError('EXEC_BACKGROUND_REJECTED', `命令位 ${firstToken}(脱管语义)被拒——模型不能脱管留后台进程`);
75
+ }
76
+ }
77
+ /**
78
+ * bash 工具定义工厂(conversation ExecToolService.bashTool 的直供体——装载
79
+ * 态 scope.provide('exec') 归批 12 装配面后装配批)。
80
+ */
81
+ export function createBashTool(deps) {
82
+ // 发现一次即缓存(PATH 扫描有成本;会话期 bash 在场性不变)
83
+ let bashPath;
84
+ const resolveBash = () => {
85
+ if (bashPath === undefined)
86
+ bashPath = discoverBash(deps.env);
87
+ return bashPath;
88
+ };
89
+ return {
90
+ name: 'bash',
91
+ description: '在工作区执行 bash 命令(login shell:profile 级工具链在场)。默认 120 秒' +
92
+ '超时(上限 600 秒,到点进程组树杀);输出保尾 60KiB 合计截断。工作目录' +
93
+ '缺省为工作区根。不支持后台化(尾部 & 与 nohup 被拒)。受限沙箱档下写' +
94
+ '工作区外会被拒;确需越档时同调用携带 sandbox_permissions(目标档)与' +
95
+ 'justification(理由)发起升权审批。',
96
+ parameters: Type.Object({
97
+ command: Type.String({ description: '要执行的 bash 命令串' }),
98
+ timeoutMs: Type.Optional(Type.Integer({
99
+ minimum: 1,
100
+ maximum: BASH_TIMEOUT_MAX_MS,
101
+ description: `超时预算毫秒(缺省 ${BASH_TIMEOUT_DEFAULT_MS},上限 ${BASH_TIMEOUT_MAX_MS})`,
102
+ })),
103
+ cwd: Type.Optional(Type.String({ description: '工作目录(缺省工作区根)' })),
104
+ sandbox_permissions: Type.Optional(Type.String({ description: '升权目标档(workspace-write / danger)——须与 justification 成对' })),
105
+ justification: Type.Optional(Type.String({ description: '升权理由——须与 sandbox_permissions 成对' })),
106
+ }, { additionalProperties: false }),
107
+ // bash 执行任意 shell 命令 = exec 档(04 §9 定形块①三值扩——v1 升档清单
108
+ // 恰一处即本件:任意进程执行类固有风险位最高,自 write 升 exec 档;批内
109
+ // 调度 write|exec 同串行屏障(03 §2.3 尾注)、审批对照走、write 档免问
110
+ // 授权不覆盖 bash 调用(偏序窄化自限))
111
+ effect: 'exec',
112
+ execute: async (args, toolCtx) => {
113
+ try {
114
+ const command = String(args.command);
115
+ assertNoBackgroundCommand(command);
116
+ const timeoutMs = typeof args.timeoutMs === 'number' ? args.timeoutMs : BASH_TIMEOUT_DEFAULT_MS;
117
+ const cwd = typeof args.cwd === 'string' ? args.cwd : deps.workspaceRoot();
118
+ // ---- 腿一(04 §252):.git 重定向目标扫描——硬拒前置(升权审批前,
119
+ // 不空耗审批对;任何档无升权出路、白名单不豁免——carve-out 路径级
120
+ // 直写恒不可写) ----
121
+ const gitViolations = findGitRedirectViolations(command, cwd);
122
+ if (gitViolations.length > 0) {
123
+ throw new BaseError('EXEC_GIT_REDIRECT_DENIED', `bash 重定向目标落在 .git 版本史内(${gitViolations.join('、')})——carve-out 平台底线:` +
124
+ '任何档恒不可写、无升权出路(04 §252);git 元数据操作请走 git 命令白名单形' +
125
+ '(add/commit/branch 等直陈命令,不带命令替换/子壳)');
126
+ }
127
+ const bash = resolveBash();
128
+ // ---- 三级解析本调用腿(04 §8):工具参数携带升权 → 校验 → 审批 ----
129
+ let mode = deps.currentMode();
130
+ if (args.sandbox_permissions !== undefined || args.justification !== undefined) {
131
+ const valid = validateEscalationArgs({
132
+ current: mode,
133
+ sandboxPermissions: typeof args.sandbox_permissions === 'string' ? args.sandbox_permissions : undefined,
134
+ justification: typeof args.justification === 'string' ? args.justification : undefined,
135
+ });
136
+ if (deps.approval === undefined) {
137
+ return {
138
+ content: [
139
+ {
140
+ type: 'text',
141
+ text: `[SANDBOX_UNAVAILABLE] 升权审批面缺席——拒绝无审批的升权执行(${mode} → ${valid.target})`,
142
+ },
143
+ ],
144
+ isError: true,
145
+ };
146
+ }
147
+ const decision = await requestEscalation(deps.approval, {
148
+ ...valid,
149
+ current: mode,
150
+ toolName: 'bash',
151
+ toolCallId: toolCtx.toolCallId,
152
+ ...(toolCtx.signal !== undefined ? { signal: toolCtx.signal } : {}),
153
+ });
154
+ if (decision.outcome !== 'allowed-once') {
155
+ // 用户拒绝/取消/审批面不可用:拒绝是最终的——不带升权提示(不许
156
+ // 投机重试;提示标记只随真实策略拒绝走)
157
+ return {
158
+ content: [
159
+ {
160
+ type: 'text',
161
+ text: `升权审批未通过(${mode} → ${valid.target},结果 ${decision.outcome})——本次调用拒绝执行`,
162
+ },
163
+ ],
164
+ isError: true,
165
+ };
166
+ }
167
+ // allowed-once:目标档只用于本次 spawn,不写回会话档
168
+ mode = valid.target;
169
+ }
170
+ // ---- argv 组装:三档一律 confine(04 §8 定形②「任何档一律」——
171
+ // 2026-09-08 P0①;danger 形 = 最小读 deny profile,后端件分支定形;
172
+ // 沙箱缺席恒 fail-closed 拒裸跑——换档不是绕后端的路) ----
173
+ // -lc login shell:profile 级工具链(nvm 等)在场——承 berry getShellArgs 语义
174
+ const rawArgv = [bash, '-lc', command];
175
+ if (deps.sandboxService === undefined) {
176
+ return {
177
+ content: [
178
+ {
179
+ type: 'text',
180
+ text: `[SANDBOX_UNAVAILABLE] 沙箱服务缺席,拒绝以 ${mode} 档裸跑(不静默无沙箱执行)`,
181
+ },
182
+ ],
183
+ isError: true,
184
+ };
185
+ }
186
+ // ---- 腿二(04 §252):静态洁净白名单分类 + 策略组装 ----
187
+ // 非豁免形恒携 workspace .git 写 deny(任何档含 danger——底线不交档位;
188
+ // 运行时兜底关 tee/python/sed -i/dd/变量间接等全部非重定向向量);
189
+ // 豁免形不携 deny,且 worktree 锚定时补 backing gitdir 可写根(修
190
+ // worktree 会话 git 命令沙箱断链——backing 在 worktree 根外、缺省
191
+ // 推导不可达;仅 workspace-write 档追加:danger 已全盘、read-only
192
+ // 空根不授予——豁免是 carve-out 面非写权授予)。
193
+ const wsRoot = deps.workspaceRoot();
194
+ const gitExempt = isGitMetadataExempt(command);
195
+ // worktree 授予腿:豁免形 + worktree 锚定 + workspace-write 档 →
196
+ // backing gitdir 入可写根(danger 已全盘不追加、read-only 空根不授予
197
+ // ——豁免是 carve-out 面非写权授予)
198
+ const backing = gitExempt && mode === 'workspace-write' ? worktreeGitDir(wsRoot) : undefined;
199
+ const policy = !gitExempt
200
+ ? { mode, workspaceRoot: wsRoot, denyWritePaths: [canonicalPath(join(wsRoot, '.git'))] }
201
+ : backing !== undefined
202
+ ? { mode, workspaceRoot: wsRoot, writableRoots: [...deriveWritableRoots(wsRoot, mode), backing] }
203
+ : { mode, workspaceRoot: wsRoot };
204
+ const confined = deps.sandboxService.confine(rawArgv, policy);
205
+ const result = await deps.pipeline.run({
206
+ argv: confined.argv,
207
+ cwd,
208
+ timeoutMs,
209
+ ...(toolCtx.signal !== undefined ? { signal: toolCtx.signal } : {}),
210
+ owner: toolCtx.toolCallId,
211
+ });
212
+ // ---- 结算分类(退出码非工具面异常——数据面分报) ----
213
+ if (result.outcome === 'timeout') {
214
+ return errorWithTail(`[EXEC_TIMEOUT] 命令超时(预算 ${timeoutMs}ms)——已进程组树杀;尾部输出保留下方`, result);
215
+ }
216
+ if (result.outcome === 'abort') {
217
+ return errorWithTail('[EXEC_ABORTED] 命令被打断(协作中止信号)', result);
218
+ }
219
+ // runner 自身失败(runner 没跑起来——区别于策略拒绝生效):EXEC_SPAWN_FAILED
220
+ // 分类(spawn 阶段失败同族:进程虽 spawn 了但沙箱 runner 未成活)
221
+ if (result.exitCode !== 0) {
222
+ const lower = result.stderr.toLowerCase();
223
+ const fatal = confined.runnerFailureRules.some((rule) => rule.fatalSignatures.some((sig) => lower.includes(sig.toLowerCase())));
224
+ if (fatal) {
225
+ return errorWithTail(`[EXEC_SPAWN_FAILED] 沙箱 runner 未跑起来(后端故障非策略拒绝)——安装对应沙箱后端`, result);
226
+ }
227
+ // 策略拒绝(denialSignatures 命中):拒绝标记 + 真实 stderr + 升权提示
228
+ const denied = confined.denialSignatures.some((sig) => lower.includes(sig.toLowerCase()));
229
+ if (denied) {
230
+ return {
231
+ content: [
232
+ {
233
+ type: 'text',
234
+ text: `${sandboxDenialMarker(mode)}\n${result.stderr.trim()}\n${escalationHintMarker()}`,
235
+ },
236
+ ],
237
+ isError: true,
238
+ };
239
+ }
240
+ }
241
+ // 自然退出:exitCode ≠ 0 = 执行阶段失败(失败二分后者——正常结算 isError)
242
+ const sections = [`Exit code: ${result.exitCode ?? 'null(信号终止)'}`];
243
+ if (result.stdout !== '')
244
+ sections.push(`--- stdout ---\n${result.stdout}`);
245
+ if (result.stderr !== '')
246
+ sections.push(`--- stderr ---\n${result.stderr}`);
247
+ if (result.truncated) {
248
+ sections.push(`(输出超 60KiB 已截尾:实收 ${result.bytes} 字节,保尾部)`);
249
+ }
250
+ const text = sections.join('\n');
251
+ return result.exitCode === 0
252
+ ? { content: [{ type: 'text', text }] }
253
+ : { content: [{ type: 'text', text }], isError: true };
254
+ }
255
+ catch (error) {
256
+ // 抛出面(EXEC_SPAWN_FAILED/EXEC_BACKGROUND_REJECTED/SANDBOX_* 校验族)
257
+ // 编码为 isError 数据面(03 §2.3)——BaseError 携码前置披露
258
+ const code = error instanceof BaseError ? error.code : undefined;
259
+ const message = error instanceof Error ? error.message : String(error);
260
+ return {
261
+ content: [{ type: 'text', text: code ? `[${code}] ${message}` : message }],
262
+ isError: true,
263
+ };
264
+ }
265
+ },
266
+ };
267
+ }
268
+ /** isError + 输出保尾段(超时/打断/runner 失败三形共用——尾部现场须保留) */
269
+ function errorWithTail(prefix, result) {
270
+ const sections = [prefix];
271
+ if (result.stdout !== '')
272
+ sections.push(`--- stdout ---\n${result.stdout}`);
273
+ if (result.stderr !== '')
274
+ sections.push(`--- stderr ---\n${result.stderr}`);
275
+ if (result.truncated) {
276
+ sections.push(`(输出超 60KiB 已截尾:实收 ${result.bytes} 字节,保尾部)`);
277
+ }
278
+ return { content: [{ type: 'text', text: sections.join('\n') }], isError: true };
279
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * exec 域错误码注册(04 §11 spawn 管道 + §8 bash 工具件——EXEC_ 前缀族)。
3
+ *
4
+ * 前缀族在 contracts/errors.ts ERROR_CODE_PREFIXES 已注册(02 §5.3 #1);
5
+ * 码名两枚为规范具名(EXEC_SPAWN_FAILED / EXEC_ENV_FORBIDDEN——04 §11/§8
6
+ * 原文),两枚为落码批定名(EXEC_TIMEOUT / EXEC_BACKGROUND_REJECTED——
7
+ * 04 §11 超时归因条与 §8「无后台化」截获条的拒执面,族清单随本批补明)。
8
+ * 本文件由模块公开面 index.ts 引入(注册纪律:import 发生才注册)。
9
+ */
10
+ import { registerErrorCodes } from '../contracts/index.js';
11
+ registerErrorCodes([
12
+ {
13
+ code: 'EXEC_SPAWN_FAILED',
14
+ module: 'exec',
15
+ description: 'spawn 阶段失败(可执行不存在/权限——进程从未存在)与执行阶段失败(exitCode ≠ 0)分报的前者(04 §11 失败二分条)',
16
+ },
17
+ {
18
+ code: 'EXEC_ENV_FORBIDDEN',
19
+ module: 'exec',
20
+ description: 'env 白名单 deny-by-default 拒静默继承——请求继承/注入 deny 覆盖变量的凭证泄漏面封死(04 §11 env 白名单条)',
21
+ },
22
+ {
23
+ code: 'EXEC_TIMEOUT',
24
+ module: 'exec',
25
+ description: '执行超时——已进程组树杀,归因 timeout(04 §11 超时归因先到先得条;04 §8 timeoutMs 上限 600s)',
26
+ },
27
+ {
28
+ code: 'EXEC_BACKGROUND_REJECTED',
29
+ module: 'exec',
30
+ description: '后台化命令截获拒——尾部单 & 或命令位 nohup/disown(04 §8「无后台化」:模型不能脱管留后台进程)',
31
+ },
32
+ {
33
+ code: 'EXEC_GIT_REDIRECT_DENIED',
34
+ module: 'exec',
35
+ description: 'bash 重定向目标落在 .git 版本史内——carve-out 路径级直写硬拒(04 §252 桥条款腿一):任何档无升权出路、白名单不豁免;git 元数据操作走命令白名单形(成熟度缺口 #9 落码批)',
36
+ },
37
+ ]);