@aitne/daemon 0.1.3 → 0.1.6

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 (421) hide show
  1. package/dist/adapters/notification-manager.d.ts +12 -0
  2. package/dist/adapters/notification-manager.d.ts.map +1 -1
  3. package/dist/adapters/notification-manager.js +39 -1
  4. package/dist/adapters/notification-manager.js.map +1 -1
  5. package/dist/adapters/whatsapp-adapter.d.ts.map +1 -1
  6. package/dist/adapters/whatsapp-adapter.js +0 -1
  7. package/dist/adapters/whatsapp-adapter.js.map +1 -1
  8. package/dist/api/integration-route-gate.d.ts +15 -11
  9. package/dist/api/integration-route-gate.d.ts.map +1 -1
  10. package/dist/api/integration-route-gate.js +60 -23
  11. package/dist/api/integration-route-gate.js.map +1 -1
  12. package/dist/api/json-body.d.ts +22 -7
  13. package/dist/api/json-body.d.ts.map +1 -1
  14. package/dist/api/json-body.js +27 -8
  15. package/dist/api/json-body.js.map +1 -1
  16. package/dist/api/routes/agent.d.ts.map +1 -1
  17. package/dist/api/routes/agent.js +25 -0
  18. package/dist/api/routes/agent.js.map +1 -1
  19. package/dist/api/routes/backends.d.ts.map +1 -1
  20. package/dist/api/routes/backends.js +96 -1
  21. package/dist/api/routes/backends.js.map +1 -1
  22. package/dist/api/routes/books.js +1 -1
  23. package/dist/api/routes/books.js.map +1 -1
  24. package/dist/api/routes/commands.d.ts.map +1 -1
  25. package/dist/api/routes/commands.js +16 -13
  26. package/dist/api/routes/commands.js.map +1 -1
  27. package/dist/api/routes/context.d.ts.map +1 -1
  28. package/dist/api/routes/context.js +26 -3
  29. package/dist/api/routes/context.js.map +1 -1
  30. package/dist/api/routes/dashboard.d.ts.map +1 -1
  31. package/dist/api/routes/dashboard.js +103 -5
  32. package/dist/api/routes/dashboard.js.map +1 -1
  33. package/dist/api/routes/fs.d.ts +23 -0
  34. package/dist/api/routes/fs.d.ts.map +1 -0
  35. package/dist/api/routes/fs.js +156 -0
  36. package/dist/api/routes/fs.js.map +1 -0
  37. package/dist/api/routes/fs.logic.d.ts +62 -0
  38. package/dist/api/routes/fs.logic.d.ts.map +1 -0
  39. package/dist/api/routes/fs.logic.js +137 -0
  40. package/dist/api/routes/fs.logic.js.map +1 -0
  41. package/dist/api/routes/github.d.ts.map +1 -1
  42. package/dist/api/routes/github.js +38 -5
  43. package/dist/api/routes/github.js.map +1 -1
  44. package/dist/api/routes/health.d.ts.map +1 -1
  45. package/dist/api/routes/health.js +4 -2
  46. package/dist/api/routes/health.js.map +1 -1
  47. package/dist/api/routes/integrations.d.ts +35 -6
  48. package/dist/api/routes/integrations.d.ts.map +1 -1
  49. package/dist/api/routes/integrations.js +192 -15
  50. package/dist/api/routes/integrations.js.map +1 -1
  51. package/dist/api/routes/mail.d.ts.map +1 -1
  52. package/dist/api/routes/mail.js +112 -46
  53. package/dist/api/routes/mail.js.map +1 -1
  54. package/dist/api/routes/metrics.d.ts +1 -0
  55. package/dist/api/routes/metrics.d.ts.map +1 -1
  56. package/dist/api/routes/metrics.js +24 -0
  57. package/dist/api/routes/metrics.js.map +1 -1
  58. package/dist/api/routes/observations.d.ts.map +1 -1
  59. package/dist/api/routes/observations.js +696 -30
  60. package/dist/api/routes/observations.js.map +1 -1
  61. package/dist/api/routes/setup-migrate.d.ts +9 -1
  62. package/dist/api/routes/setup-migrate.d.ts.map +1 -1
  63. package/dist/api/routes/setup-migrate.js +4 -2
  64. package/dist/api/routes/setup-migrate.js.map +1 -1
  65. package/dist/api/routes/skills.d.ts +9 -1
  66. package/dist/api/routes/skills.d.ts.map +1 -1
  67. package/dist/api/routes/skills.js +77 -17
  68. package/dist/api/routes/skills.js.map +1 -1
  69. package/dist/api/routes/voice.d.ts.map +1 -1
  70. package/dist/api/routes/voice.js +62 -4
  71. package/dist/api/routes/voice.js.map +1 -1
  72. package/dist/api/routes/wiki.d.ts +4 -0
  73. package/dist/api/routes/wiki.d.ts.map +1 -0
  74. package/dist/api/routes/wiki.js +1075 -0
  75. package/dist/api/routes/wiki.js.map +1 -0
  76. package/dist/api/server.d.ts +13 -0
  77. package/dist/api/server.d.ts.map +1 -1
  78. package/dist/api/server.js +27 -1
  79. package/dist/api/server.js.map +1 -1
  80. package/dist/bootstrap/adapters.d.ts +109 -0
  81. package/dist/bootstrap/adapters.d.ts.map +1 -0
  82. package/dist/bootstrap/adapters.js +237 -0
  83. package/dist/bootstrap/adapters.js.map +1 -0
  84. package/dist/bootstrap/catchup.d.ts +23 -0
  85. package/dist/bootstrap/catchup.d.ts.map +1 -0
  86. package/dist/bootstrap/catchup.js +124 -0
  87. package/dist/bootstrap/catchup.js.map +1 -0
  88. package/dist/bootstrap/schedule-helpers.d.ts +18 -0
  89. package/dist/bootstrap/schedule-helpers.d.ts.map +1 -0
  90. package/dist/bootstrap/schedule-helpers.js +96 -0
  91. package/dist/bootstrap/schedule-helpers.js.map +1 -0
  92. package/dist/bootstrap/services.d.ts +60 -0
  93. package/dist/bootstrap/services.d.ts.map +1 -0
  94. package/dist/bootstrap/services.js +209 -0
  95. package/dist/bootstrap/services.js.map +1 -0
  96. package/dist/config.d.ts.map +1 -1
  97. package/dist/config.js +26 -0
  98. package/dist/config.js.map +1 -1
  99. package/dist/core/agent-core.d.ts +25 -0
  100. package/dist/core/agent-core.d.ts.map +1 -1
  101. package/dist/core/agent-core.js.map +1 -1
  102. package/dist/core/backends/backend-router.d.ts +28 -1
  103. package/dist/core/backends/backend-router.d.ts.map +1 -1
  104. package/dist/core/backends/backend-router.js +58 -4
  105. package/dist/core/backends/backend-router.js.map +1 -1
  106. package/dist/core/backends/claude-auth.d.ts +70 -0
  107. package/dist/core/backends/claude-auth.d.ts.map +1 -0
  108. package/dist/core/backends/claude-auth.js +198 -0
  109. package/dist/core/backends/claude-auth.js.map +1 -0
  110. package/dist/core/backends/claude-code-core.d.ts +47 -119
  111. package/dist/core/backends/claude-code-core.d.ts.map +1 -1
  112. package/dist/core/backends/claude-code-core.js +166 -1561
  113. package/dist/core/backends/claude-code-core.js.map +1 -1
  114. package/dist/core/backends/claude-delegated.d.ts +86 -0
  115. package/dist/core/backends/claude-delegated.d.ts.map +1 -0
  116. package/dist/core/backends/claude-delegated.js +801 -0
  117. package/dist/core/backends/claude-delegated.js.map +1 -0
  118. package/dist/core/backends/claude-errors.d.ts +39 -0
  119. package/dist/core/backends/claude-errors.d.ts.map +1 -0
  120. package/dist/core/backends/claude-errors.js +71 -0
  121. package/dist/core/backends/claude-errors.js.map +1 -0
  122. package/dist/core/backends/claude-probe.d.ts +103 -0
  123. package/dist/core/backends/claude-probe.d.ts.map +1 -0
  124. package/dist/core/backends/claude-probe.js +336 -0
  125. package/dist/core/backends/claude-probe.js.map +1 -0
  126. package/dist/core/backends/claude-tool-collection.d.ts +135 -0
  127. package/dist/core/backends/claude-tool-collection.d.ts.map +1 -0
  128. package/dist/core/backends/claude-tool-collection.js +1093 -0
  129. package/dist/core/backends/claude-tool-collection.js.map +1 -0
  130. package/dist/core/backends/codex-core.d.ts.map +1 -1
  131. package/dist/core/backends/codex-core.js +36 -0
  132. package/dist/core/backends/codex-core.js.map +1 -1
  133. package/dist/core/backends/gemini-cli-core.d.ts +45 -5
  134. package/dist/core/backends/gemini-cli-core.d.ts.map +1 -1
  135. package/dist/core/backends/gemini-cli-core.js +146 -36
  136. package/dist/core/backends/gemini-cli-core.js.map +1 -1
  137. package/dist/core/backends/plan-presets.d.ts +3 -1
  138. package/dist/core/backends/plan-presets.d.ts.map +1 -1
  139. package/dist/core/backends/plan-presets.js +42 -2
  140. package/dist/core/backends/plan-presets.js.map +1 -1
  141. package/dist/core/backends/prompt-utils.d.ts +1 -0
  142. package/dist/core/backends/prompt-utils.d.ts.map +1 -1
  143. package/dist/core/backends/prompt-utils.js +60 -3
  144. package/dist/core/backends/prompt-utils.js.map +1 -1
  145. package/dist/core/bang-commands/commands-help.d.ts +5 -0
  146. package/dist/core/bang-commands/commands-help.d.ts.map +1 -0
  147. package/dist/core/bang-commands/commands-help.js +69 -0
  148. package/dist/core/bang-commands/commands-help.js.map +1 -0
  149. package/dist/core/bang-commands/commands-wiki.d.ts +75 -0
  150. package/dist/core/bang-commands/commands-wiki.d.ts.map +1 -0
  151. package/dist/core/bang-commands/commands-wiki.js +574 -0
  152. package/dist/core/bang-commands/commands-wiki.js.map +1 -0
  153. package/dist/core/bang-commands/index.d.ts +4 -2
  154. package/dist/core/bang-commands/index.d.ts.map +1 -1
  155. package/dist/core/bang-commands/index.js +15 -1
  156. package/dist/core/bang-commands/index.js.map +1 -1
  157. package/dist/core/bang-commands/registry.d.ts +47 -4
  158. package/dist/core/bang-commands/registry.d.ts.map +1 -1
  159. package/dist/core/bang-commands/registry.js +85 -15
  160. package/dist/core/bang-commands/registry.js.map +1 -1
  161. package/dist/core/context-builder.d.ts +53 -12
  162. package/dist/core/context-builder.d.ts.map +1 -1
  163. package/dist/core/context-builder.js +240 -92
  164. package/dist/core/context-builder.js.map +1 -1
  165. package/dist/core/daemon-api-cli.d.ts.map +1 -1
  166. package/dist/core/daemon-api-cli.js +50 -2
  167. package/dist/core/daemon-api-cli.js.map +1 -1
  168. package/dist/core/dispatcher-date-utils.d.ts +49 -0
  169. package/dist/core/dispatcher-date-utils.d.ts.map +1 -0
  170. package/dist/core/dispatcher-date-utils.js +132 -0
  171. package/dist/core/dispatcher-date-utils.js.map +1 -0
  172. package/dist/core/dispatcher-error-handling.d.ts +159 -0
  173. package/dist/core/dispatcher-error-handling.d.ts.map +1 -0
  174. package/dist/core/dispatcher-error-handling.js +393 -0
  175. package/dist/core/dispatcher-error-handling.js.map +1 -0
  176. package/dist/core/dispatcher-hourly-check.d.ts +150 -0
  177. package/dist/core/dispatcher-hourly-check.d.ts.map +1 -0
  178. package/dist/core/dispatcher-hourly-check.js +665 -0
  179. package/dist/core/dispatcher-hourly-check.js.map +1 -0
  180. package/dist/core/dispatcher-message-handler.d.ts +170 -0
  181. package/dist/core/dispatcher-message-handler.d.ts.map +1 -0
  182. package/dist/core/dispatcher-message-handler.js +1064 -0
  183. package/dist/core/dispatcher-message-handler.js.map +1 -0
  184. package/dist/core/dispatcher-morning-routine.d.ts +169 -0
  185. package/dist/core/dispatcher-morning-routine.d.ts.map +1 -0
  186. package/dist/core/dispatcher-morning-routine.js +449 -0
  187. package/dist/core/dispatcher-morning-routine.js.map +1 -0
  188. package/dist/core/dispatcher-prompt.d.ts +107 -0
  189. package/dist/core/dispatcher-prompt.d.ts.map +1 -0
  190. package/dist/core/dispatcher-prompt.js +227 -0
  191. package/dist/core/dispatcher-prompt.js.map +1 -0
  192. package/dist/core/dispatcher-repository-helpers.d.ts +39 -0
  193. package/dist/core/dispatcher-repository-helpers.d.ts.map +1 -0
  194. package/dist/core/dispatcher-repository-helpers.js +86 -0
  195. package/dist/core/dispatcher-repository-helpers.js.map +1 -0
  196. package/dist/core/dispatcher-result-processor.d.ts +168 -0
  197. package/dist/core/dispatcher-result-processor.d.ts.map +1 -0
  198. package/dist/core/dispatcher-result-processor.js +533 -0
  199. package/dist/core/dispatcher-result-processor.js.map +1 -0
  200. package/dist/core/dispatcher-scheduled-tasks.d.ts +406 -0
  201. package/dist/core/dispatcher-scheduled-tasks.d.ts.map +1 -0
  202. package/dist/core/dispatcher-scheduled-tasks.js +1032 -0
  203. package/dist/core/dispatcher-scheduled-tasks.js.map +1 -0
  204. package/dist/core/dispatcher-types.d.ts +411 -0
  205. package/dist/core/dispatcher-types.d.ts.map +1 -0
  206. package/dist/core/dispatcher-types.js +106 -0
  207. package/dist/core/dispatcher-types.js.map +1 -0
  208. package/dist/core/dispatcher.d.ts +122 -610
  209. package/dist/core/dispatcher.d.ts.map +1 -1
  210. package/dist/core/dispatcher.js +365 -3521
  211. package/dist/core/dispatcher.js.map +1 -1
  212. package/dist/core/integration-health.d.ts +18 -10
  213. package/dist/core/integration-health.d.ts.map +1 -1
  214. package/dist/core/integration-health.js +31 -1
  215. package/dist/core/integration-health.js.map +1 -1
  216. package/dist/core/integration-lifecycle.d.ts +65 -0
  217. package/dist/core/integration-lifecycle.d.ts.map +1 -1
  218. package/dist/core/integration-lifecycle.js +163 -14
  219. package/dist/core/integration-lifecycle.js.map +1 -1
  220. package/dist/core/integration-main-backend.d.ts +40 -0
  221. package/dist/core/integration-main-backend.d.ts.map +1 -1
  222. package/dist/core/integration-main-backend.js +89 -2
  223. package/dist/core/integration-main-backend.js.map +1 -1
  224. package/dist/core/management-md.d.ts +51 -17
  225. package/dist/core/management-md.d.ts.map +1 -1
  226. package/dist/core/management-md.js +233 -56
  227. package/dist/core/management-md.js.map +1 -1
  228. package/dist/core/metrics.d.ts +127 -0
  229. package/dist/core/metrics.d.ts.map +1 -1
  230. package/dist/core/metrics.js +256 -1
  231. package/dist/core/metrics.js.map +1 -1
  232. package/dist/core/output-language-policy.d.ts +74 -0
  233. package/dist/core/output-language-policy.d.ts.map +1 -0
  234. package/dist/core/output-language-policy.js +194 -0
  235. package/dist/core/output-language-policy.js.map +1 -0
  236. package/dist/core/prompts.d.ts +3 -1
  237. package/dist/core/prompts.d.ts.map +1 -1
  238. package/dist/core/prompts.js +161 -3
  239. package/dist/core/prompts.js.map +1 -1
  240. package/dist/core/repository-management-docs.d.ts +24 -0
  241. package/dist/core/repository-management-docs.d.ts.map +1 -1
  242. package/dist/core/repository-management-docs.js +210 -26
  243. package/dist/core/repository-management-docs.js.map +1 -1
  244. package/dist/core/roadmap-validate.js +13 -1
  245. package/dist/core/roadmap-validate.js.map +1 -1
  246. package/dist/core/routine-acquisition-plan.d.ts +182 -0
  247. package/dist/core/routine-acquisition-plan.d.ts.map +1 -0
  248. package/dist/core/routine-acquisition-plan.js +367 -0
  249. package/dist/core/routine-acquisition-plan.js.map +1 -0
  250. package/dist/core/routine-fetch-window-retry.d.ts +109 -0
  251. package/dist/core/routine-fetch-window-retry.d.ts.map +1 -0
  252. package/dist/core/routine-fetch-window-retry.js +210 -0
  253. package/dist/core/routine-fetch-window-retry.js.map +1 -0
  254. package/dist/core/routine-fetch-window-runner.d.ts +427 -0
  255. package/dist/core/routine-fetch-window-runner.d.ts.map +1 -0
  256. package/dist/core/routine-fetch-window-runner.js +1591 -0
  257. package/dist/core/routine-fetch-window-runner.js.map +1 -0
  258. package/dist/core/routine-windows.d.ts +171 -0
  259. package/dist/core/routine-windows.d.ts.map +1 -0
  260. package/dist/core/routine-windows.js +377 -0
  261. package/dist/core/routine-windows.js.map +1 -0
  262. package/dist/core/scheduler.d.ts +50 -2
  263. package/dist/core/scheduler.d.ts.map +1 -1
  264. package/dist/core/scheduler.js +88 -7
  265. package/dist/core/scheduler.js.map +1 -1
  266. package/dist/core/skill-curation/declarations.d.ts.map +1 -1
  267. package/dist/core/skill-curation/declarations.js +11 -12
  268. package/dist/core/skill-curation/declarations.js.map +1 -1
  269. package/dist/core/skill-source-paths.d.ts +14 -0
  270. package/dist/core/skill-source-paths.d.ts.map +1 -0
  271. package/dist/core/skill-source-paths.js +82 -0
  272. package/dist/core/skill-source-paths.js.map +1 -0
  273. package/dist/core/skills-compiler.d.ts +29 -0
  274. package/dist/core/skills-compiler.d.ts.map +1 -1
  275. package/dist/core/skills-compiler.js +166 -30
  276. package/dist/core/skills-compiler.js.map +1 -1
  277. package/dist/core/skills-manifest.d.ts.map +1 -1
  278. package/dist/core/skills-manifest.js +72 -0
  279. package/dist/core/skills-manifest.js.map +1 -1
  280. package/dist/core/system-reset.d.ts +25 -0
  281. package/dist/core/system-reset.d.ts.map +1 -1
  282. package/dist/core/system-reset.js +72 -2
  283. package/dist/core/system-reset.js.map +1 -1
  284. package/dist/core/wiki/approval-queue.d.ts +31 -0
  285. package/dist/core/wiki/approval-queue.d.ts.map +1 -0
  286. package/dist/core/wiki/approval-queue.js +44 -0
  287. package/dist/core/wiki/approval-queue.js.map +1 -0
  288. package/dist/core/wiki/bridge.d.ts +74 -0
  289. package/dist/core/wiki/bridge.d.ts.map +1 -0
  290. package/dist/core/wiki/bridge.js +405 -0
  291. package/dist/core/wiki/bridge.js.map +1 -0
  292. package/dist/core/wiki/compile-lock.d.ts +42 -0
  293. package/dist/core/wiki/compile-lock.d.ts.map +1 -0
  294. package/dist/core/wiki/compile-lock.js +55 -0
  295. package/dist/core/wiki/compile-lock.js.map +1 -0
  296. package/dist/core/wiki/compile-preview.d.ts +8 -0
  297. package/dist/core/wiki/compile-preview.d.ts.map +1 -0
  298. package/dist/core/wiki/compile-preview.js +200 -0
  299. package/dist/core/wiki/compile-preview.js.map +1 -0
  300. package/dist/core/wiki/cost-estimate.d.ts +30 -0
  301. package/dist/core/wiki/cost-estimate.d.ts.map +1 -0
  302. package/dist/core/wiki/cost-estimate.js +243 -0
  303. package/dist/core/wiki/cost-estimate.js.map +1 -0
  304. package/dist/core/wiki/dispatcher.d.ts +48 -0
  305. package/dist/core/wiki/dispatcher.d.ts.map +1 -0
  306. package/dist/core/wiki/dispatcher.js +92 -0
  307. package/dist/core/wiki/dispatcher.js.map +1 -0
  308. package/dist/core/wiki/git-precompile.d.ts +86 -0
  309. package/dist/core/wiki/git-precompile.d.ts.map +1 -0
  310. package/dist/core/wiki/git-precompile.js +96 -0
  311. package/dist/core/wiki/git-precompile.js.map +1 -0
  312. package/dist/core/wiki/import-migrate.d.ts +38 -0
  313. package/dist/core/wiki/import-migrate.d.ts.map +1 -0
  314. package/dist/core/wiki/import-migrate.js +310 -0
  315. package/dist/core/wiki/import-migrate.js.map +1 -0
  316. package/dist/core/wiki/import-probe.d.ts +76 -0
  317. package/dist/core/wiki/import-probe.d.ts.map +1 -0
  318. package/dist/core/wiki/import-probe.js +245 -0
  319. package/dist/core/wiki/import-probe.js.map +1 -0
  320. package/dist/core/wiki/index-cache.d.ts +39 -0
  321. package/dist/core/wiki/index-cache.d.ts.map +1 -0
  322. package/dist/core/wiki/index-cache.js +152 -0
  323. package/dist/core/wiki/index-cache.js.map +1 -0
  324. package/dist/core/wiki/multi-url-dispatch.d.ts +52 -0
  325. package/dist/core/wiki/multi-url-dispatch.d.ts.map +1 -0
  326. package/dist/core/wiki/multi-url-dispatch.js +72 -0
  327. package/dist/core/wiki/multi-url-dispatch.js.map +1 -0
  328. package/dist/core/wiki/wiki-fts.d.ts +75 -0
  329. package/dist/core/wiki/wiki-fts.d.ts.map +1 -0
  330. package/dist/core/wiki/wiki-fts.js +265 -0
  331. package/dist/core/wiki/wiki-fts.js.map +1 -0
  332. package/dist/core/wiki/workspaces.d.ts +101 -0
  333. package/dist/core/wiki/workspaces.d.ts.map +1 -0
  334. package/dist/core/wiki/workspaces.js +352 -0
  335. package/dist/core/wiki/workspaces.js.map +1 -0
  336. package/dist/core/wiki/write-strategy.d.ts +70 -0
  337. package/dist/core/wiki/write-strategy.d.ts.map +1 -0
  338. package/dist/core/wiki/write-strategy.js +112 -0
  339. package/dist/core/wiki/write-strategy.js.map +1 -0
  340. package/dist/core/workdir.d.ts +8 -1
  341. package/dist/core/workdir.d.ts.map +1 -1
  342. package/dist/core/workdir.js +4 -1
  343. package/dist/core/workdir.js.map +1 -1
  344. package/dist/db/observations.d.ts +45 -2
  345. package/dist/db/observations.d.ts.map +1 -1
  346. package/dist/db/observations.js +112 -14
  347. package/dist/db/observations.js.map +1 -1
  348. package/dist/db/schema.d.ts.map +1 -1
  349. package/dist/db/schema.js +135 -25
  350. package/dist/db/schema.js.map +1 -1
  351. package/dist/db/wiki-store.d.ts +3 -0
  352. package/dist/db/wiki-store.d.ts.map +1 -0
  353. package/dist/db/wiki-store.js +7 -0
  354. package/dist/db/wiki-store.js.map +1 -0
  355. package/dist/index.js +159 -610
  356. package/dist/index.js.map +1 -1
  357. package/dist/messaging/url-extract.d.ts +8 -0
  358. package/dist/messaging/url-extract.d.ts.map +1 -0
  359. package/dist/messaging/url-extract.js +41 -0
  360. package/dist/messaging/url-extract.js.map +1 -0
  361. package/dist/observers/delegated-sync-worker.d.ts +52 -1
  362. package/dist/observers/delegated-sync-worker.d.ts.map +1 -1
  363. package/dist/observers/delegated-sync-worker.js +75 -18
  364. package/dist/observers/delegated-sync-worker.js.map +1 -1
  365. package/dist/observers/imminent-event-scheduler.d.ts +20 -7
  366. package/dist/observers/imminent-event-scheduler.d.ts.map +1 -1
  367. package/dist/observers/imminent-event-scheduler.js +134 -29
  368. package/dist/observers/imminent-event-scheduler.js.map +1 -1
  369. package/dist/observers/mail-poller.d.ts +12 -5
  370. package/dist/observers/mail-poller.d.ts.map +1 -1
  371. package/dist/observers/mail-poller.js +36 -14
  372. package/dist/observers/mail-poller.js.map +1 -1
  373. package/dist/observers/manager.d.ts +37 -5
  374. package/dist/observers/manager.d.ts.map +1 -1
  375. package/dist/observers/manager.js +28 -10
  376. package/dist/observers/manager.js.map +1 -1
  377. package/dist/safety/always-disallowed.d.ts +65 -0
  378. package/dist/safety/always-disallowed.d.ts.map +1 -1
  379. package/dist/safety/always-disallowed.js +106 -10
  380. package/dist/safety/always-disallowed.js.map +1 -1
  381. package/dist/safety/audit.d.ts +46 -1
  382. package/dist/safety/audit.d.ts.map +1 -1
  383. package/dist/safety/audit.js +79 -16
  384. package/dist/safety/audit.js.map +1 -1
  385. package/dist/safety/risk-classifier.d.ts.map +1 -1
  386. package/dist/safety/risk-classifier.js +29 -0
  387. package/dist/safety/risk-classifier.js.map +1 -1
  388. package/dist/services/delegated-backend-invoker.d.ts +1 -51
  389. package/dist/services/delegated-backend-invoker.d.ts.map +1 -1
  390. package/dist/services/delegated-backend-invoker.js +41 -480
  391. package/dist/services/delegated-backend-invoker.js.map +1 -1
  392. package/dist/services/delegated-invoker-audit.d.ts +94 -0
  393. package/dist/services/delegated-invoker-audit.d.ts.map +1 -0
  394. package/dist/services/delegated-invoker-audit.js +238 -0
  395. package/dist/services/delegated-invoker-audit.js.map +1 -0
  396. package/dist/services/delegated-invoker-cache-hits.d.ts +34 -0
  397. package/dist/services/delegated-invoker-cache-hits.d.ts.map +1 -0
  398. package/dist/services/delegated-invoker-cache-hits.js +104 -0
  399. package/dist/services/delegated-invoker-cache-hits.js.map +1 -0
  400. package/dist/services/delegated-invoker-janitors.d.ts +28 -0
  401. package/dist/services/delegated-invoker-janitors.d.ts.map +1 -0
  402. package/dist/services/delegated-invoker-janitors.js +104 -0
  403. package/dist/services/delegated-invoker-janitors.js.map +1 -0
  404. package/dist/services/delegated-invoker-utils.d.ts +42 -0
  405. package/dist/services/delegated-invoker-utils.d.ts.map +1 -0
  406. package/dist/services/delegated-invoker-utils.js +100 -0
  407. package/dist/services/delegated-invoker-utils.js.map +1 -0
  408. package/dist/services/delegated-task-runtime.d.ts +1 -1
  409. package/dist/services/delegated-task-runtime.js +1 -1
  410. package/dist/services/integrations/snapshot-partitions.d.ts +5 -0
  411. package/dist/services/integrations/snapshot-partitions.d.ts.map +1 -1
  412. package/dist/services/integrations/snapshot-partitions.js +12 -0
  413. package/dist/services/integrations/snapshot-partitions.js.map +1 -1
  414. package/dist/services/voice/transcriber-impl.d.ts.map +1 -1
  415. package/dist/services/voice/transcriber-impl.js +7 -8
  416. package/dist/services/voice/transcriber-impl.js.map +1 -1
  417. package/dist/settings/runtime-settings.d.ts +12 -1
  418. package/dist/settings/runtime-settings.d.ts.map +1 -1
  419. package/dist/settings/runtime-settings.js +59 -1
  420. package/dist/settings/runtime-settings.js.map +1 -1
  421. package/package.json +2 -2
@@ -0,0 +1,1093 @@
1
+ /**
2
+ * Claude tool surface — pure helpers split out of `claude-code-core.ts` as
3
+ * part of the file-split plan (Tier 2, §8). Owns five responsibilities:
4
+ *
5
+ * - `getAllowedTools` — assemble the SDK `allowedTools` list from the
6
+ * configured default + the runtime override + any delegated- and native-
7
+ * integration tools the registry exposes.
8
+ * - `getDelegatedClaudeTools` — read the current `integrations` registry
9
+ * state and project it through `computeDelegatedClaudeTools`. Returns
10
+ * `[]` when the MCP context is not yet wired or on DB read failure.
11
+ * - `getNativeClaudeTools` — same shape as `getDelegatedClaudeTools` but
12
+ * projects through `computeNativeClaudeTools` (native-mode parallel).
13
+ * - `getSessionDeniedTools` — DELEGATED-MODE-V2-DESIGN.md §4.3.3 — expand
14
+ * per-integration `deniedTools` into namespaced tool names that the SDK
15
+ * rejects via `disallowedTools` regardless of the allow list.
16
+ * - `buildSecurityHooks` — build the PreToolUse hook record that enforces
17
+ * curl localhost-only, jq env/file-flag denials, context-dir chokepoint,
18
+ * vault write attribution, and the absolute-block audit layer.
19
+ *
20
+ * Pattern A (file-split-plan §5): each function reads its dependencies via
21
+ * an explicit argument record rather than `this.<field>`. The pure shape
22
+ * means these can be unit tested without instantiating `ClaudeCodeCore`,
23
+ * and lets tests inspect the hook closures directly. Thin shims on
24
+ * `ClaudeCodeCore` (`private getAllowedTools(...) { return ... }`) remain
25
+ * for the transitional period (file-split-plan §15).
26
+ */
27
+ import { collectSessionDeniedTools } from "@aitne/shared";
28
+ import { realpathSync } from "node:fs";
29
+ import { homedir } from "node:os";
30
+ import { dirname, resolve as resolvePath, isAbsolute } from "node:path";
31
+ import { getContextDir } from "../../config.js";
32
+ import { readIntegrations } from "../../db/integrations-store.js";
33
+ import { recordAbsoluteBlockAudit } from "../../safety/absolute-block-audit.js";
34
+ import { classifyAbsoluteBlock, stripBashHeredocs, stripBashStringContent, } from "../../safety/always-disallowed.js";
35
+ import { createLogger } from "../../logging.js";
36
+ import { computeDelegatedClaudeTools, computeNativeClaudeTools } from "./claude-probe.js";
37
+ import { isPathInsideOrEqual, shellPathForms } from "../path-compat.js";
38
+ /**
39
+ * Resolve a path through symlinks, even when the leaf does not yet exist.
40
+ *
41
+ * `fs.realpathSync` throws ENOENT on a non-existent leaf, which is the
42
+ * common case for a Write hook (the target file is the *next* write).
43
+ * Walk upwards until an existing ancestor is found, realpath that, then
44
+ * rejoin the missing suffix. Used by both `fileWriteHook` and
45
+ * `bashContextWriteHook` to defeat symlink-based bypasses that point
46
+ * back into the context dir.
47
+ */
48
+ function realpathLenient(absPath) {
49
+ const segments = [];
50
+ let current = absPath;
51
+ // Hard ceiling on iterations so a pathological path never spins forever.
52
+ for (let i = 0; i < 64; i++) {
53
+ try {
54
+ const real = realpathSync(current);
55
+ return segments.length === 0
56
+ ? real
57
+ : resolvePath(real, ...segments.reverse());
58
+ }
59
+ catch {
60
+ const parent = dirname(current);
61
+ if (parent === current)
62
+ return absPath;
63
+ segments.push(current.slice(parent.length).replace(/^[/\\]+/, ""));
64
+ current = parent;
65
+ }
66
+ }
67
+ return absPath;
68
+ }
69
+ /**
70
+ * Best-effort shell tokenizer for path-token scanning. Splits on
71
+ * whitespace while honouring single, double, and back-tick quotes; ignores
72
+ * shell operators (`|`, `;`, `&`, `<`, `>`, parentheses). Returns tokens
73
+ * with their quote wrappers stripped.
74
+ *
75
+ * Not a full shell parser — it cannot resolve variable expansions,
76
+ * subshells, or function definitions. Exists to surface *literal* path
77
+ * arguments so that an obvious form like
78
+ * `echo > /Users/shuto/.personal-agent/context/today.md` is caught. The
79
+ * absolute-block layer is the authoritative defence for the things this
80
+ * heuristic misses.
81
+ */
82
+ function tokenizeShellCommand(cmd) {
83
+ const tokens = [];
84
+ const re = /"([^"]*)"|'([^']*)'|`([^`]*)`|\$\(([^)]*)\)|([^\s|;&<>()]+)/g;
85
+ let match;
86
+ while ((match = re.exec(cmd)) !== null) {
87
+ const tok = match[1] ?? match[2] ?? match[3] ?? match[4] ?? match[5] ?? "";
88
+ if (tok.length > 0)
89
+ tokens.push(tok);
90
+ }
91
+ return tokens;
92
+ }
93
+ /**
94
+ * Expand the leading `~`, `$HOME`, and `${HOME}` segments of a token
95
+ * to the supplied home directory. No other shell expansion is performed.
96
+ */
97
+ function expandHomeForms(token, home) {
98
+ if (token === "~")
99
+ return home;
100
+ if (token.startsWith("~/"))
101
+ return home + token.slice(1);
102
+ if (token.startsWith("$HOME/"))
103
+ return home + token.slice(5);
104
+ if (token.startsWith("${HOME}/"))
105
+ return home + token.slice(7);
106
+ if (token === "$HOME" || token === "${HOME}")
107
+ return home;
108
+ return token;
109
+ }
110
+ /**
111
+ * Decide whether a shell token (after `expandHomeForms` normalisation)
112
+ * resembles a filesystem path argument. Replaces the older "contains
113
+ * `/` or `\`" filter, which false-positived on quoted JSON bodies and
114
+ * HTTP header values whenever the session cwd was inside the data dir
115
+ * (production cwd is `<dataDir>/agent-sessions/<id>`):
116
+ *
117
+ * - `Content-Type: application/json` → `/` inside `application/json`
118
+ * was treated as a path separator, the token was resolved relative
119
+ * to the (data-dir-internal) cwd, the resulting candidate landed
120
+ * inside `absDataDir`, and the hook blocked an otherwise benign
121
+ * `curl -X PATCH -H '...'` invocation.
122
+ * - `'{"content":"line1\nline2"}'` → the literal `\` in `\n` triggered
123
+ * the same data-dir resolution path even though the token is a JSON
124
+ * payload, not a filename.
125
+ *
126
+ * The shape rules below are deliberately positive (a token must look
127
+ * like a path) rather than negative (skip if it contains JSON chars):
128
+ *
129
+ * 1. Absolute on POSIX (`/foo`) — also catches tokens that
130
+ * `expandHomeForms` rewrote from `~` / `$HOME` / `${HOME}` forms.
131
+ * 2. Explicit relative anchor (`./foo`, `../foo`, exactly `.` / `..`).
132
+ * 3. Unresolved home / env-var prefix that survived `expandHomeForms`
133
+ * (e.g. `~user/foo`, `$OTHER/foo` when the variable is unknown).
134
+ * Treating these as path candidates is a defensive belt — the
135
+ * static analysis can't know what they expand to at runtime, so
136
+ * err on the side of forwarding them through the data-dir check.
137
+ * 4. Bare multi-segment path made of filename-safe characters
138
+ * (`context/today.md`, `agent-sessions/foo/bar`). The character
139
+ * class deliberately excludes whitespace, `:`, `=`, `{`, `}`,
140
+ * `"`, `'`, `` ` ``, `?`, `*`, `<`, `>` — all of which appear in
141
+ * header values, JSON bodies, and query strings but never in
142
+ * well-formed filename segments.
143
+ *
144
+ * URL-shaped tokens (`http://...`) are filtered by the caller before
145
+ * this helper runs, so rule 1 / rule 4 cannot misfire on them.
146
+ */
147
+ function looksLikePathArg(token) {
148
+ if (token.length === 0)
149
+ return false;
150
+ // Rules 1 + 2 — POSIX-absolute or anchored-relative.
151
+ if (token.startsWith("/"))
152
+ return true;
153
+ if (token === "." || token === ".." || token.startsWith("./") || token.startsWith("../")) {
154
+ return true;
155
+ }
156
+ // Rule 3 — unresolved home / env-var prefix.
157
+ if (token.startsWith("~") || token.startsWith("$"))
158
+ return true;
159
+ // Rule 4 — bare relative path with filename-safe segments only.
160
+ // `[A-Za-z0-9_.\-+@]` is the segment alphabet — broad enough to
161
+ // cover typical project filenames (dashes, underscores, dots,
162
+ // version suffixes, `@scope/pkg` style) without admitting tokens
163
+ // that came from a JSON body or header value. The trailing `/?`
164
+ // tolerates a directory-shape suffix (`context/`).
165
+ if (/^[A-Za-z0-9_.\-+@]+(?:\/[A-Za-z0-9_.\-+@]+)+\/?$/.test(token)) {
166
+ return true;
167
+ }
168
+ return false;
169
+ }
170
+ const logger = createLogger("claude-tool-collection");
171
+ /** Default allowed-tools list when the dashboard override is unset. */
172
+ export const CLAUDE_DEFAULT_ALLOWED_TOOLS = [
173
+ "Read",
174
+ "Glob",
175
+ "Grep",
176
+ "Write",
177
+ "Edit",
178
+ "Skill", // user skills (external-services, obsidian-*, observations, ...)
179
+ "Bash(curl *)", // curl broadly allowed; hooks restrict to localhost
180
+ "Bash(git *)", // Git operations
181
+ "Bash(jq *)", // safe JSON post-processor for curl pipelines
182
+ ];
183
+ /**
184
+ * Allowed tools whitelist for dontAsk permission mode.
185
+ *
186
+ * `delegatedTools` and `nativeTools` are UNION'd onto the returned list —
187
+ * even when `allowedToolsOverride` is set. This is a deliberate deviation
188
+ * from the override's otherwise-absolute "replace everything" contract (see
189
+ * `CRITICAL_OVERRIDE_TOOLS` in `claude-code-core.ts`, which warns but does
190
+ * not union). Rationale: delegated / native modes are runtime-configurable
191
+ * axes orthogonal to the dashboard's tool-customization override. If a user
192
+ * set the override before flipping an integration, silently dropping the
193
+ * registry-declared connector tools would break mail/calendar with a
194
+ * misleading "permission denied" DM. Union semantics keep the override's
195
+ * curation intent while letting either mode widen the surface to whatever
196
+ * the registry already advertised.
197
+ *
198
+ * Native and delegated lists are accepted separately (rather than a single
199
+ * `extraMcpTools` parameter) so callers — and tests — surface the
200
+ * provenance of every widening: an audit log entry with
201
+ * `delegatedToolCount` and `nativeToolCount` makes a misconfigured flip
202
+ * diagnosable without re-running the resolver.
203
+ */
204
+ export function getAllowedTools(config, webSearchEnabled, delegatedTools = [], nativeTools = [],
205
+ // WIKI_BUILDER_DESIGN.md §4.3 — wiki.ingest_url turns need WebFetch on
206
+ // top of the default surface to read external pages (the `Bash(curl *)`
207
+ // PreToolUse hook keeps curl restricted to localhost). Gated on the
208
+ // same `!allowedToolsOverride` clause as `webSearchEnabled` so a user
209
+ // who configured a custom override gets the override verbatim — they
210
+ // are expected to add `WebFetch` themselves if they need it (matches
211
+ // the WebSearch contract; documented in /settings/wiki).
212
+ wikiUrlFetchEnabled = false,
213
+ // Wiki sessions must write only through the daemon Wiki API
214
+ // (`POST /api/wiki/<ws>/files/...`) — every wiki.* process key has a
215
+ // skill body and the wiki-agent profile both stating "no `Write` /
216
+ // `Edit` against the vault." Skill frontmatter `allowed-tools` is
217
+ // human-facing metadata and does NOT propagate into the SDK's
218
+ // session-level allowlist, so without this hard strip a wiki turn
219
+ // can bypass the API path-classifier, the agent_actions audit row,
220
+ // and the result-processor's write-verifier by Writing a vault
221
+ // path directly. Pass true for any `processKey.startsWith("wiki.")`.
222
+ wikiApiOnlyWrites = false) {
223
+ const base = config.allowedToolsOverride ?? [...CLAUDE_DEFAULT_ALLOWED_TOOLS];
224
+ const merged = new Set(base);
225
+ if (!config.allowedToolsOverride && webSearchEnabled) {
226
+ merged.add("WebSearch");
227
+ }
228
+ if (!config.allowedToolsOverride && wikiUrlFetchEnabled) {
229
+ merged.add("WebFetch");
230
+ }
231
+ for (const tool of delegatedTools)
232
+ merged.add(tool);
233
+ for (const tool of nativeTools)
234
+ merged.add(tool);
235
+ // Claude Code 2.1+ defers large MCP manifests (`mcp__claude_ai_*`) behind
236
+ // `ToolSearch` — the tools appear by name but their schemas are not
237
+ // loaded until the agent calls `ToolSearch select:<name>`. Without
238
+ // ToolSearch allowed, the model cannot invoke any unioned MCP tool and
239
+ // silently falls back to denied surfaces (raw Bash, WebFetch), surfacing
240
+ // as "Bash and WebFetch denied" failure DMs from native/delegated-same
241
+ // routines. Mirrors the same widening already applied by
242
+ // `composePrePassAllowedTools` (pre-pass), `CLAUDE_PROBE_TOOLS_PROMPT`
243
+ // (probe), and `claude-delegated.ts` (cross-backend proxy). Unioned even
244
+ // under `allowedToolsOverride` for the same orthogonality reason the
245
+ // MCP tools themselves bypass the override above — silently dropping
246
+ // ToolSearch while keeping the MCP names defeats the widening.
247
+ if (delegatedTools.length > 0 || nativeTools.length > 0) {
248
+ merged.add("ToolSearch");
249
+ }
250
+ if (wikiApiOnlyWrites) {
251
+ merged.delete("Write");
252
+ merged.delete("Edit");
253
+ }
254
+ return Array.from(merged);
255
+ }
256
+ /**
257
+ * Read the integrations record from the wired MCP context and project it
258
+ * through the `computeDelegatedClaudeTools` allowlist computation. Returns
259
+ * `[]` when the context is not yet wired (tests / startup ordering) or on
260
+ * DB read failure — the latter is logged as a warning so a corrupt
261
+ * integrations table is visible without halting the session.
262
+ */
263
+ export function getDelegatedClaudeTools(mcpContext) {
264
+ if (!mcpContext)
265
+ return [];
266
+ try {
267
+ const integrations = readIntegrations(mcpContext.db);
268
+ return computeDelegatedClaudeTools(integrations);
269
+ }
270
+ catch (err) {
271
+ logger.warn({ err }, "Failed to read integrations for delegated-tool allowlist — proceeding without delegated tools");
272
+ return [];
273
+ }
274
+ }
275
+ /**
276
+ * Sibling of `getDelegatedClaudeTools` — projects integrations record
277
+ * through `computeNativeClaudeTools`. Returns `[]` when the context is
278
+ * not yet wired or on DB read failure, matching the conservative pattern
279
+ * used by the delegated counterpart.
280
+ *
281
+ * Required because the SDK's `dontAsk` permission mode silently denies
282
+ * tools not in `allowedTools`. Native-mode skill bodies instruct the
283
+ * agent to call connector MCP tools directly (e.g.
284
+ * `mcp__claude_ai_Gmail__search_threads`), so the registry-declared tool
285
+ * names for every `mode === "native" && nativeBackend === "claude"` row
286
+ * must be pre-authorized.
287
+ */
288
+ export function getNativeClaudeTools(mcpContext) {
289
+ if (!mcpContext)
290
+ return [];
291
+ try {
292
+ const integrations = readIntegrations(mcpContext.db);
293
+ return computeNativeClaudeTools(integrations);
294
+ }
295
+ catch (err) {
296
+ logger.warn({ err }, "Failed to read integrations for native-tool allowlist — proceeding without native tools");
297
+ return [];
298
+ }
299
+ }
300
+ /**
301
+ * DELEGATED-MODE-V2-DESIGN.md §4.3.3 — same-backend deny enforcement at
302
+ * the SDK boundary. For every integration whose `delegatedBackend === "claude"`,
303
+ * expand `state.deniedTools` against the connector's known tools and emit
304
+ * the namespaced names (`mcp__claude_ai_<X>__<tool>`). The SDK refuses any
305
+ * tool listed in `disallowedTools` regardless of `allowedTools` — hard
306
+ * enforcement.
307
+ *
308
+ * Returns `[]` when context isn't wired (tests / pre-startup) and on read
309
+ * failures, matching the conservative pattern used by
310
+ * `getDelegatedClaudeTools`.
311
+ */
312
+ export function getSessionDeniedTools(mcpContext) {
313
+ if (!mcpContext)
314
+ return [];
315
+ try {
316
+ const integrations = readIntegrations(mcpContext.db);
317
+ const map = collectSessionDeniedTools(integrations, "claude");
318
+ const out = [];
319
+ for (const names of map.values()) {
320
+ for (const n of names)
321
+ out.push(n);
322
+ }
323
+ return out;
324
+ }
325
+ catch (err) {
326
+ logger.warn({ err }, "Failed to read integrations for same-backend denied-tools — proceeding without per-integration deny");
327
+ return [];
328
+ }
329
+ }
330
+ /**
331
+ * Security hooks:
332
+ * 1. Bash(curl *) — restrict to localhost Daemon API, block connection-override flags. (strict only)
333
+ * 2. Bash(jq *) — block file-access flags and the `env` filter (process env exfiltration). (strict only)
334
+ * 3. Write/Edit — block writes into the session helper dir and context dir, mark vault writes.
335
+ *
336
+ * In allow mode the curl and jq hooks are dropped, but the Write/Edit hook
337
+ * stays: the context-dir chokepoint exists for memory integrity (today-write
338
+ * lock, md_file_snapshots, CONTEXT_WRITE_PERMISSIONS), not permissions.
339
+ */
340
+ export function buildSecurityHooks(deps, allowMode = false) {
341
+ const { config, writeTracker, getMcpContext } = deps;
342
+ // Per-Bash-hook block logging. The SDK's `dontAsk` mode silently
343
+ // denies any Bash command that doesn't match an allowed prefix —
344
+ // no tool_result, no error feedback — and PreToolUse hooks that
345
+ // return `block` emit a generic reason that the agent often
346
+ // misinterprets as "Bash is blocked entirely." Without this log,
347
+ // diagnosing a failed wiki / context update means guessing at the
348
+ // command the model produced. The line is logged at warn level
349
+ // (one per actual block, not per call) so steady-state cost is
350
+ // negligible; the cmd is truncated to 400 chars to keep secrets
351
+ // out of logs and the entry parseable.
352
+ const wrapBashHook = (hookName, inner) => async (input) => {
353
+ const result = await inner(input);
354
+ if (result && result.decision === "block") {
355
+ const toolInput = input.tool_input;
356
+ const cmd = toolInput?.command ?? "";
357
+ logger.warn({
358
+ hook: hookName,
359
+ reason: result.reason,
360
+ cmd: cmd.slice(0, 400),
361
+ }, "Bash hook block");
362
+ }
363
+ return result;
364
+ };
365
+ const bashCurlHook = async (input) => {
366
+ const toolInput = input.tool_input;
367
+ const cmd = toolInput?.command ?? "";
368
+ // Three views of the command, each used by a different class of check:
369
+ //
370
+ // - `cmd` (raw) — the initial `\bcurl\b` keyword presence test.
371
+ // Must see literal token text so a `-d
372
+ // '{"text":"see curl docs"}'` body doesn't
373
+ // suppress the hook entirely.
374
+ // - `scan` — substring scans for flag PRESENCE (chained
375
+ // curl, --next, --proxy, -L, -o, -c, -b, etc.).
376
+ // Strips single-quoted strings AND heredoc
377
+ // bodies so prose inside a JSON payload like
378
+ // "set -o pipefail in scripts" cannot trip
379
+ // the flag detectors.
380
+ // - `tokenizable` — tokenizer walks and value extractors
381
+ // (top-level URL collection, `-d @file` arg
382
+ // walker, `-o <file>` path capture). Strips
383
+ // ONLY heredoc bodies (which are stdin
384
+ // payload, never shell argv) and PRESERVES
385
+ // quoted strings so the value extractors can
386
+ // still recognise quoted URL targets and
387
+ // quoted file paths.
388
+ //
389
+ // The wiki.ingest_url skill is the canonical case where this matters:
390
+ // it POSTs an article body via `-d @- <<'JSON' … JSON`, and the body
391
+ // routinely contains the source URL ("Source: https://news.example.com/…").
392
+ // Before this layered design the URL extractor scanned `cmd`, found
393
+ // the body URL, and falsely blocked with "Multiple URL targets".
394
+ const scan = stripBashStringContent(cmd);
395
+ const tokenizable = stripBashHeredocs(cmd);
396
+ if (/\bcurl\b/.test(cmd)) {
397
+ // ── Multi-request defenses (run BEFORE host/port loop) ─────────
398
+ // The SDK `allowedTools` glob is a prefix match against the full
399
+ // command, so a permitted `Bash(curl http://localhost:<port>/api/x/*)`
400
+ // entry still matches a chained `curl http://localhost/api/x/y ;
401
+ // curl http://localhost/api/notify -d @evil`. The URL host/port
402
+ // loop below validates every URL but does NOT count invocations
403
+ // or request transactions, so a second HTTP request slips through.
404
+ // The three rules below cap a curl-bearing command to a single
405
+ // HTTP request.
406
+ //
407
+ // 1. Chained curl invocations — mirrors the `cmdStart` anchor
408
+ // pattern in `safety/always-disallowed.ts`. Count `curl`
409
+ // tokens at start-of-string / after `;` / `&&` / `||` / `|` /
410
+ // newline / backtick / `$(`. A single `jq -n '…' | curl URL`
411
+ // pipeline counts as ONE curl (only the `curl` token itself
412
+ // is matched; the leading `jq` is not). Two or more anchored
413
+ // `curl` tokens → chained invocation → block.
414
+ const chainedCurlMatches = scan.match(/(?:^|[;&|`\n]|\$\()\s*curl\b/g) ?? [];
415
+ if (chainedCurlMatches.length > 1) {
416
+ return {
417
+ decision: "block",
418
+ reason: `Chained curl invocations are not allowed `
419
+ + `(detected ${chainedCurlMatches.length} curl commands; `
420
+ + `one curl per Bash invocation).`,
421
+ };
422
+ }
423
+ // 2. `--next` / `-:` URL multiplexing — curl's `--next` (short
424
+ // form `-:`) starts a new transaction with reset option state
425
+ // inside the same invocation. The URL loop below still passes
426
+ // because both URLs hit the same host:port, but curl issues
427
+ // one HTTP request per `--next` separator. Same exfil shape
428
+ // as chained curl, different syntax.
429
+ if (/(?:^|\s)--next(?:[\s=]|$)/.test(scan)
430
+ || /(?:^|\s)-:(?:\s|$)/.test(scan)) {
431
+ return {
432
+ decision: "block",
433
+ reason: "curl --next / -: (URL multiplexing) is not allowed "
434
+ + "— one HTTP request per Bash invocation.",
435
+ };
436
+ }
437
+ // 3. Multi-positional URL targets — `curl URL1 URL2 -X PUT -d
438
+ // @body` sends the same options to BOTH URLs sequentially,
439
+ // which `--next` blocking above does not catch. Tokenize the
440
+ // heredoc-stripped command and collect tokens that are URLs:
441
+ //
442
+ // - Bare URL token: `curl http://localhost:8321/api/x`
443
+ // - Fully single-quoted URL: `curl 'http://localhost:8321/api/x'`
444
+ // - Fully double-quoted URL: `curl "http://localhost:8321/api/x"`
445
+ //
446
+ // URLs that appear INSIDE a quoted body / header value
447
+ // (e.g. `-d '{"link":"https://example.com"}'` or
448
+ // `-H "X-Source: https://example.com"`) are NOT counted: the
449
+ // surrounding quoted token carries other characters, so the
450
+ // "entire content is the URL" patterns below do not match.
451
+ //
452
+ // Heredoc bodies (`<<'JSON' … JSON`) are stripped from
453
+ // `tokenizable` above because they are stdin payload, never
454
+ // shell argv — without that strip, the routine wiki.ingest_url
455
+ // shape of "store an article body that mentions other URLs"
456
+ // would trip this multi-URL rule on the body URL.
457
+ const topLevelTokenRe = /'[^']*'|"[^"]*"|[^'"\s]+/g;
458
+ const topLevelUrls = [];
459
+ let tokenMatch;
460
+ while ((tokenMatch = topLevelTokenRe.exec(tokenizable)) !== null) {
461
+ const token = tokenMatch[0];
462
+ if (/^https?:\/\//.test(token)) {
463
+ topLevelUrls.push(token);
464
+ continue;
465
+ }
466
+ // Fully-quoted URL token: the WHOLE content between matching
467
+ // single or double quotes must be the URL — anything else
468
+ // (JSON body, header value) starts with non-URL characters
469
+ // after the quote.
470
+ const quoted = /^(['"])(https?:\/\/[^'"\s]+)\1$/.exec(token);
471
+ if (quoted) {
472
+ topLevelUrls.push(quoted[2]);
473
+ }
474
+ }
475
+ if (topLevelUrls.length > 1) {
476
+ return {
477
+ decision: "block",
478
+ reason: `Multiple URL targets in a single curl invocation are not allowed `
479
+ + `(detected ${topLevelUrls.length} top-level URL tokens; quote `
480
+ + `body URLs inside -d/-H string args).`,
481
+ };
482
+ }
483
+ if (topLevelUrls.length === 0) {
484
+ return {
485
+ decision: "block",
486
+ reason: "curl command must contain an explicit localhost URL",
487
+ };
488
+ }
489
+ for (const url of topLevelUrls) {
490
+ try {
491
+ const parsed = new URL(url);
492
+ if (parsed.hostname !== "localhost" && parsed.hostname !== "127.0.0.1") {
493
+ return {
494
+ decision: "block",
495
+ reason: `curl target not allowed: ${url} (host: ${parsed.hostname})`,
496
+ };
497
+ }
498
+ const effectivePort = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
499
+ if (effectivePort !== String(config.apiPort)) {
500
+ return {
501
+ decision: "block",
502
+ reason: `curl target port not allowed: ${effectivePort}`,
503
+ };
504
+ }
505
+ }
506
+ catch {
507
+ return {
508
+ decision: "block",
509
+ reason: `curl target URL is malformed: ${url}`,
510
+ };
511
+ }
512
+ }
513
+ // Connection-override flags — host/proxy/socket redirection that
514
+ // would let curl reach something other than the configured loopback
515
+ // HTTP endpoint.
516
+ if (/--connect-to|--resolve|--config\b|(?:^|\s)-[a-zA-Z]*K|--proxy\b|(?:^|\s)-[a-zA-Z]*x|--socks|--unix-socket|--abstract-unix-socket|--interface\b|--local-port\b/.test(scan)) {
517
+ return {
518
+ decision: "block",
519
+ reason: "curl connection override flags not allowed " +
520
+ "(--connect-to, --resolve, --config, --proxy, " +
521
+ "--unix-socket, --abstract-unix-socket, " +
522
+ "--interface, --local-port)",
523
+ };
524
+ }
525
+ // File-read exfil flags. curl can read arbitrary files into the
526
+ // request body via `@<path>` in -d / --data / --form, or via the
527
+ // upload-file flag. The daemon API is loopback so the request
528
+ // body would land in `agent_actions` / notification surfaces that
529
+ // the agent reads back — a confused-deputy exfil.
530
+ //
531
+ // --upload-file / -T — PUT a local file as the body
532
+ // -d @path / --data @path — body literal from file
533
+ // --data-binary @path — same, raw bytes
534
+ // --data-raw @path — same, no escape
535
+ // --data-urlencode @path — same, urlencoded
536
+ // --data-ascii @path — same, ascii
537
+ // -F name=@path / --form …=@ — multipart file part
538
+ // -F name=<path / --form …=< — multipart text from file
539
+ // Short-flag combined forms (`curl -fsT /etc/passwd`) must be
540
+ // caught alongside the single-flag form (`curl -T /etc/passwd`).
541
+ // The leading `-[a-zA-Z]*` permits zero-or-more other short flags
542
+ // before the dangerous letter, mirroring the pattern proven for
543
+ // `-L`. Same shape applied to every short-flag below — without it
544
+ // an attacker can stuff the dangerous letter into a benign-looking
545
+ // flag bundle like `-fs<X>` and bypass the deny rule entirely.
546
+ if (/(?:^|\s)(?:--upload-file\b|-[a-zA-Z]*T(?:\s|=|$))/.test(scan)) {
547
+ return {
548
+ decision: "block",
549
+ reason: "curl --upload-file / -T not allowed — would read arbitrary files",
550
+ };
551
+ }
552
+ // `@-` is curl's stdin marker (canonical: `-d @-` reads the body
553
+ // from stdin, used by pipelines like `echo $body | curl ... -d @-`).
554
+ // Block `@<anything-other-than-stdin-marker>`. The lookahead
555
+ // `(?!-["']?(?:\s|$))` lets `@-`, `@-"`, `@-'`, `@- ` through.
556
+ // `-d / --data* / -F / --form` value-content checks. The previous
557
+ // regex `(?:^|\s)…\s+["']?@(?!-…)` matched the @-file syntax with
558
+ // the value attached, which meant a JSON BODY containing literal
559
+ // text like ` -d @<chars>` (an agent journal entry that quotes a
560
+ // shell example) also tripped it. Conversely, switching to the
561
+ // `scan` form alone loses single-quoted attack content (the
562
+ // legitimate `-d '@/etc/passwd'` form): scan strips the body and
563
+ // the regex no longer sees the `@`.
564
+ //
565
+ // The walker below is value-aware: tokenize the command (already
566
+ // quote-aware via the same regex used for URL extraction), find
567
+ // every `-d` / `--data*` / `-F` / `--form` flag token, recover the
568
+ // unquoted value (either after `=` in the same token or in the
569
+ // adjacent token), and reject if the value's FIRST CHARACTER is
570
+ // `@` (with the canonical stdin marker `@-` excluded). That
571
+ // discriminates:
572
+ // - `-d '@/etc/passwd'` → value starts with `@` and is not `@-`
573
+ // → block (matches the original protection).
574
+ // - `-d '{"content":"a -d @x b"}'` → value starts with `{` →
575
+ // allow (the body contains @ but is not an @-file argument).
576
+ //
577
+ // For `-F`/`--form`, the file-read syntax is `name=@file` /
578
+ // `name=<file` (first `=` in the value followed by `@` or `<`),
579
+ // which the same walker can test in the value once recovered.
580
+ // Adjacent-token merge: bash treats `-d='value'` as the SINGLE
581
+ // argument `-d=value` (the quote is stripped, the bare prefix and
582
+ // the quoted body are joined when there is no whitespace between
583
+ // them). The regex pass below splits the two pieces — track each
584
+ // match's start vs. the previous match's end and concatenate any
585
+ // pair with no whitespace gap. A composite token is treated as
586
+ // "effectively bare" if either constituent was bare, so the flag
587
+ // walker still recognises `-d='@/path'` as a `-d=` flag carrying
588
+ // the value `@/path` (which the regex form `["']?@` used to catch).
589
+ const argRe = /'([^']*)'|"([^"]*)"|`([^`]*)`|([^\s'"`]+)/g;
590
+ const argList = [];
591
+ let am;
592
+ let lastEnd = -1;
593
+ // Walks `tokenizable` (heredoc-stripped) so a body line like
594
+ // `prose mentioning -d @/etc/passwd` cannot be parsed as a real
595
+ // `-d` flag carrying an `@file` value. Single / double quotes are
596
+ // preserved so the `quoted` discriminator still tracks user intent
597
+ // correctly for the dataFlag / formFlag checks below.
598
+ while ((am = argRe.exec(tokenizable)) !== null) {
599
+ const value = am[1] ?? am[2] ?? am[3] ?? am[4] ?? "";
600
+ const quoted = am[4] === undefined;
601
+ if (am.index === lastEnd && argList.length > 0) {
602
+ const prev = argList[argList.length - 1];
603
+ prev.value = prev.value + value;
604
+ prev.quoted = prev.quoted && quoted;
605
+ }
606
+ else {
607
+ argList.push({ value, quoted });
608
+ }
609
+ lastEnd = argRe.lastIndex;
610
+ }
611
+ const dataFlag = /^(?:--data(?:-binary|-raw|-urlencode|-ascii)?|--data|-d)(?:=(.*))?$/;
612
+ const formFlag = /^(?:--form|-F)(?:=(.*))?$/;
613
+ for (let i = 0; i < argList.length; i++) {
614
+ const tok = argList[i];
615
+ if (!tok || tok.quoted)
616
+ continue;
617
+ const dm = tok.value.match(dataFlag);
618
+ if (dm) {
619
+ const value = dm[1] ?? argList[i + 1]?.value ?? "";
620
+ if (value.length > 0 && value[0] === "@" && value !== "@-") {
621
+ return {
622
+ decision: "block",
623
+ reason: "curl -d/--data with `@file` syntax not allowed — reads local files",
624
+ };
625
+ }
626
+ continue;
627
+ }
628
+ const fm = tok.value.match(formFlag);
629
+ if (fm) {
630
+ const value = fm[1] ?? argList[i + 1]?.value ?? "";
631
+ // `name=@path` / `name=<path`: first `=` then `@` or `<`.
632
+ if (/^[^=\s]*=[@<]/.test(value)) {
633
+ return {
634
+ decision: "block",
635
+ reason: "curl -F/--form with `=@file` or `=<file` syntax not allowed — reads local files",
636
+ };
637
+ }
638
+ }
639
+ }
640
+ // File-write flags. The agent can land bytes anywhere on disk —
641
+ // overwriting shims, ssh keys, shell rc files, etc. Daemon API is
642
+ // the sole sanctioned write path; Bash curl writes are denied.
643
+ //
644
+ // -o / --output FILE — write response to FILE
645
+ // -O / --remote-name — write to basename-of-URL
646
+ // --remote-name-all — same, for every URL
647
+ // -D / --dump-header FILE — write response headers
648
+ // -c / --cookie-jar FILE — write Set-Cookie state
649
+ // --trace / --trace-ascii F — write protocol trace
650
+ // -w / --write-out FORMAT — format-string output
651
+ // (`%{stderr}` writes to stderr;
652
+ // combined with shell redirect
653
+ // it's another write channel)
654
+ // `-o <file>` / `--output <file>` — used to download binary
655
+ // payloads from the daemon API (e.g. `curl -o receipt.pdf
656
+ // /api/receipts/1/download`). Permit only simple relative
657
+ // filenames so absolute (`-o /etc/passwd`) and parent-escape
658
+ // (`-o ../../foo`) forms are still blocked. Tilde / env-var
659
+ // prefixes are likewise refused because they bypass cwd
660
+ // containment. Quoted paths with spaces (`-o "my file"`) are
661
+ // ALSO rejected so a denylist regex that stops at the space
662
+ // inside the quotes cannot be smuggled past.
663
+ // Flag PRESENCE detection runs on the scan (quote-stripped) command
664
+ // so a body containing prose like "set -o pipefail" does not falsely
665
+ // claim there is an output flag. The subsequent VALUE extraction
666
+ // reads `tokenizable` (heredoc-stripped, quotes preserved) so an
667
+ // earlier heredoc-body occurrence of `-o /etc/passwd` cannot be
668
+ // captured ahead of the real flag — while quoted paths like
669
+ // `-o "my file.pdf"` are still readable.
670
+ const hasOutputFlag = /(?:^|\s)(?:--output(?:\b|=)|-[a-zA-Z]*o(?:\s|=|$))/.test(scan);
671
+ if (hasOutputFlag) {
672
+ // Three capture-group alternatives so quoted paths with spaces
673
+ // are caught — `[^\s'"]+` alone fails on `"my file"`.
674
+ const valueMatch = tokenizable.match(/(?:^|\s)(?:--output(?:\s+|=)|-o(?:\s+|=))(?:"([^"]*)"|'([^']*)'|([^\s'"]+))/);
675
+ const target = valueMatch?.[1] ?? valueMatch?.[2] ?? valueMatch?.[3] ?? "";
676
+ const isSafeRelative = target.length > 0 &&
677
+ !target.startsWith("/") &&
678
+ !target.startsWith("~") &&
679
+ !target.startsWith("$") &&
680
+ !target.split("/").includes("..") &&
681
+ !target.split("\\").includes("..");
682
+ if (!isSafeRelative) {
683
+ return {
684
+ decision: "block",
685
+ reason: `curl --output/-o target must be a simple relative path; ` +
686
+ `got: ${target || "<unparseable>"} ` +
687
+ `(no absolute paths, parent-dir escapes, or shell expansions).`,
688
+ };
689
+ }
690
+ }
691
+ if (/(?:^|\s)(?:--remote-name(?:-all)?\b|-[a-zA-Z]*O(?:\s|=|$))/.test(scan)) {
692
+ return {
693
+ decision: "block",
694
+ reason: "curl --remote-name/-O not allowed — would write to URL-derived path",
695
+ };
696
+ }
697
+ if (/(?:^|\s)(?:--dump-header\b|-[a-zA-Z]*D(?:\s|=|$))/.test(scan)) {
698
+ return {
699
+ decision: "block",
700
+ reason: "curl --dump-header/-D not allowed — writes response headers to disk",
701
+ };
702
+ }
703
+ if (/(?:^|\s)(?:--cookie-jar\b|-[a-zA-Z]*c(?:\s|=|$))/.test(scan)) {
704
+ return {
705
+ decision: "block",
706
+ reason: "curl --cookie-jar/-c not allowed — writes cookie state to disk",
707
+ };
708
+ }
709
+ // `--cookie` / `-b` reads cookies from a file when the value
710
+ // is a filename (curl's documented semantics: `-b "FILE"` if
711
+ // the value has no `=`). Same exfil shape as `-d @file` — the
712
+ // file content is sent in the request header. Allowing
713
+ // `-b name=value` would require parsing the value; the simpler
714
+ // safe stance is to refuse the flag outright since the daemon
715
+ // API uses bearer tokens, not cookies.
716
+ if (/(?:^|\s)(?:--cookie\b|-[a-zA-Z]*b(?:\s|=|$))/.test(scan)) {
717
+ return {
718
+ decision: "block",
719
+ reason: "curl --cookie/-b not allowed — when the value is a path, " +
720
+ "the file contents are sent as the Cookie header (file read).",
721
+ };
722
+ }
723
+ if (/(?:^|\s)--trace(?:-ascii)?\b/.test(scan)) {
724
+ return {
725
+ decision: "block",
726
+ reason: "curl --trace / --trace-ascii not allowed — writes protocol trace to disk",
727
+ };
728
+ }
729
+ if (/(?:^|\s)(?:--write-out\b|-[a-zA-Z]*w(?:\s|=|$))/.test(scan)) {
730
+ return {
731
+ decision: "block",
732
+ reason: "curl --write-out/-w not allowed — format strings include file/stderr sinks",
733
+ };
734
+ }
735
+ // Cert / key file references. The daemon API is plain HTTP on
736
+ // loopback; none of these flags are needed for legitimate
737
+ // operation and they all read arbitrary files from disk.
738
+ if (/(?:^|\s)(?:--cert\b|--key\b|--cacert\b|--capath\b|-[a-zA-Z]*E(?:\s|=|$))/.test(scan)) {
739
+ return {
740
+ decision: "block",
741
+ reason: "curl --cert/--key/--cacert/--capath/-E not allowed — read arbitrary files",
742
+ };
743
+ }
744
+ // Follow-redirect flags. The localhost URL check above is
745
+ // bypass-able if curl follows a 3xx off-localhost. The daemon
746
+ // never emits redirects so this flag has no legitimate use.
747
+ //
748
+ // Combined-short-flag forms (`-fsSL`, `-vL`) are caught by the
749
+ // `[a-zA-Z]*L` alternation; the literal `--location` and
750
+ // `--location-trusted` long forms are matched explicitly.
751
+ if (/(?:^|\s)(?:-[a-zA-Z]*L(?:\s|=|$)|--location(?:-trusted)?\b)/.test(scan)) {
752
+ return {
753
+ decision: "block",
754
+ reason: "curl -L / --location not allowed — would follow redirects off localhost",
755
+ };
756
+ }
757
+ }
758
+ return { continue: true };
759
+ };
760
+ const bashJqHook = async (input) => {
761
+ const toolInput = input.tool_input;
762
+ const cmd = toolInput?.command ?? "";
763
+ if (!/\bjq\b/.test(cmd))
764
+ return { continue: true };
765
+ // Narrow to THIS jq invocation's own args (up to the next pipe / chain op)
766
+ // so that later pipeline stages are not inspected by the jq rules.
767
+ //
768
+ // The match runs against `stripBashHeredocs(cmd)` so that prose inside
769
+ // a heredoc body (e.g. a wiki article that mentions "the jq env
770
+ // filter") cannot trip the env / -L / --slurpfile checks below.
771
+ // Quoted strings remain intact because the env-filter detector
772
+ // intentionally peers inside the single-quoted jq filter argument
773
+ // (jq syntax lives inside shell quotes, so blanket quote-stripping
774
+ // would lose the very thing we need to inspect).
775
+ //
776
+ // Known approximation: `[^|;&]*` does not respect shell quoting, so a
777
+ // jq filter with a `|` INSIDE a quoted expression (e.g. `jq 'env | keys'`)
778
+ // will truncate `jqPart` at the first `|` regardless of whether that `|`
779
+ // is a jq pipe inside quotes or an actual shell pipeline break. This is
780
+ // intentionally conservative on the safe side: the env-filter check
781
+ // below still fires on the truncated left half (`jq 'env `), so attack
782
+ // payloads are still blocked. The downside is slightly reduced precision
783
+ // on benign expressions containing the jq `|` operator — those get
784
+ // scanned only up to the first pipe, not their full extent.
785
+ const jqMatch = stripBashHeredocs(cmd).match(/\bjq\b([^|;&]*)/);
786
+ if (!jqMatch)
787
+ return { continue: true };
788
+ const jqPart = jqMatch[0];
789
+ // (a) Block file-access flags — --slurpfile / --rawfile read arbitrary
790
+ // files, which would bypass the Read deny list (~/.ssh/**, .env, etc.).
791
+ if (/(?:^|\s)--slurpfile\b/.test(jqPart) || /(?:^|\s)--rawfile\b/.test(jqPart)) {
792
+ return {
793
+ decision: "block",
794
+ reason: "jq --slurpfile and --rawfile are not allowed " +
795
+ "(would bypass Read(.env) / Read(~/.ssh/**) disallow rules).",
796
+ };
797
+ }
798
+ // (b) Block module loading — -L <dir> + import can load filter code from
799
+ // the filesystem, effectively RCE inside the jq process.
800
+ if (/(?:^|\s)-L(?:\s|=|$)/.test(jqPart)) {
801
+ return {
802
+ decision: "block",
803
+ reason: "jq -L (module load path) is not allowed.",
804
+ };
805
+ }
806
+ // (c) Block the `env` filter. `jq env`, `jq -n env`, `jq 'env.FOO'`,
807
+ // `jq '. , env'` all dump the daemon's process.env to stdout. Process.env
808
+ // on this daemon is expected to be clean (secrets live in the keychain),
809
+ // but defense-in-depth: if OPENAI_API_KEY or similar is ever exported at
810
+ // launch, the env filter is the shortest exfil path.
811
+ //
812
+ // Heuristic: match bare `env` NOT preceded by a field-access dot or word
813
+ // char, and NOT followed by a word char. This matches jq's env filter
814
+ // (`env`, `env.HOME`, `(env)`, `env|keys`) while leaving field access
815
+ // like `.env`, `.env_var`, `.data.environments` untouched.
816
+ if (/(?:^|[^\w.])env(?!\w)/.test(jqPart)) {
817
+ return {
818
+ decision: "block",
819
+ reason: "jq env filter is not allowed — it dumps the daemon process " +
820
+ "environment, which is a known exfiltration vector for any " +
821
+ "secrets loaded via .env at startup.",
822
+ };
823
+ }
824
+ return { continue: true };
825
+ };
826
+ /**
827
+ * Block any Bash command that references the context-directory path.
828
+ *
829
+ * Rationale: the daemon API is the ONLY sanctioned write channel for
830
+ * context files — it enforces today-write-lock, md_file_snapshots,
831
+ * CONTEXT_WRITE_PERMISSIONS, and onPromptContextChanged. In strict mode,
832
+ * the allowlist (Bash narrowed to curl/git/jq) + fileWriteHook keeps
833
+ * this chokepoint intact. In allow mode Bash is unrestricted, so an
834
+ * agent could bypass via `echo > today.md`, `tee`, `python -c 'open…'`,
835
+ * `git log … > context/…`, etc. The defence here is layered:
836
+ *
837
+ * 1. Original substring match against `shellPathForms`. Cheap and
838
+ * catches the obvious literal form an honest model would emit.
839
+ * 2. Best-effort shell tokenizer + `~`/`$HOME` expansion + symlink
840
+ * realpath. Catches `cd ~/.personal-agent && echo > ./context/X`
841
+ * (the `./context/X` token, once joined to the cwd or after a
842
+ * separate `cd` token is detected, lands in the context dir),
843
+ * `ln -s ~/.personal-agent/context /tmp/x` followed by writes
844
+ * to `/tmp/x/today.md`, and `~/.personal-agent/./context/X`.
845
+ * 3. Hard block on interpreter escape hatches (`python -c`, `node
846
+ * -e`, `bash -c`, etc.). Static analysis cannot see what these
847
+ * will do; in allow-mode Bash they are the most direct route
848
+ * around the chokepoint.
849
+ *
850
+ * Defence-in-depth, not authoritative: a prompt-injection-driven
851
+ * variable-construction attack (`P=context; D=today; cd ~/.personal-agent;
852
+ * echo > "$P/$D.md"`) can still slip past static analysis. The static
853
+ * absolute-block layer covers the highest-risk patterns; if a new
854
+ * shape of bypass is observed in audit, codify it here.
855
+ */
856
+ const bashContextWriteHook = async (input) => {
857
+ const hookInput = input;
858
+ const toolInput = hookInput.tool_input;
859
+ const cmd = toolInput?.command ?? "";
860
+ if (typeof cmd !== "string" || cmd.length === 0)
861
+ return { continue: true };
862
+ const absContextDir = resolvePath(getContextDir(config));
863
+ const home = homedir();
864
+ const realContextDir = realpathLenient(absContextDir);
865
+ // The data dir is the context dir's parent. `cd ~/.personal-agent`
866
+ // followed by `echo > context/today.md` lands in context via a
867
+ // post-cd relative path that Layer 2 cannot resolve (the hook only
868
+ // sees the *initial* cwd). Treating any reference to the data dir
869
+ // as out-of-bounds preempts that bypass — the agent has no
870
+ // legitimate reason to touch the data dir directly when the daemon
871
+ // API is the sanctioned write channel.
872
+ const absDataDir = resolvePath(config.dataDir);
873
+ const realDataDir = realpathLenient(absDataDir);
874
+ // Use the quote/heredoc-stripped form for Layer 1 (substring) and
875
+ // Layer 3 (interpreter regex) so a JSON body or heredoc payload that
876
+ // legitimately contains the absolute context-dir path string, or the
877
+ // literal text `bash -c …`, does not trip these layers. Layer 2
878
+ // still uses `cmd` because its tokenizer is already quote-aware via
879
+ // `looksLikePathArg`.
880
+ const scan = stripBashStringContent(cmd);
881
+ // ── Layer 1: substring match against well-known path forms ──
882
+ const pathForms = shellPathForms(absContextDir, home);
883
+ for (const form of pathForms) {
884
+ if (scan.includes(form)) {
885
+ return blockContextWrite(absContextDir, `substring match: ${form}`);
886
+ }
887
+ }
888
+ // ── Layer 2: tokenized realpath check ──
889
+ //
890
+ // Resolve every path-looking token to its absolute form (relative
891
+ // to the hook-provided cwd) and to its realpath. If either lands
892
+ // inside the context dir OR the data dir, block.
893
+ const cwd = hookInput.cwd ?? "/";
894
+ const tokens = tokenizeShellCommand(cmd);
895
+ for (const rawTok of tokens) {
896
+ const tok = expandHomeForms(rawTok, home);
897
+ // Skip URL-shaped tokens; they are not filesystem paths. Must
898
+ // come before `looksLikePathArg` because `http://localhost/...`
899
+ // satisfies the "starts with `/`" rule once the scheme prefix
900
+ // is removed — and the bare-path rule too — but is never a
901
+ // filesystem reference.
902
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(tok))
903
+ continue;
904
+ // The old filter (`!tok.includes("/") && !tok.includes("\\")`)
905
+ // forwarded any quoted token with a `/` or `\` into the data-dir
906
+ // resolution branch, which produced false positives on JSON
907
+ // bodies (`{"content":"a\nb"}`) and header values (`Content-Type:
908
+ // application/json`) whenever cwd lived under the data dir. See
909
+ // `looksLikePathArg` for the replacement rules.
910
+ if (!looksLikePathArg(tok))
911
+ continue;
912
+ const candidate = isAbsolute(tok) ? tok : resolvePath(cwd, tok);
913
+ const real = realpathLenient(candidate);
914
+ const landsInsideContext = isPathInsideOrEqual(absContextDir, candidate) ||
915
+ isPathInsideOrEqual(realContextDir, real);
916
+ const landsInsideData = isPathInsideOrEqual(absDataDir, candidate) ||
917
+ isPathInsideOrEqual(realDataDir, real);
918
+ if (landsInsideContext || landsInsideData) {
919
+ return blockContextWrite(absContextDir, landsInsideContext
920
+ ? `path token resolves into context dir: ${rawTok} → ${real}`
921
+ : `path token resolves into the data dir (${absDataDir}); ` +
922
+ `the agent should never reference the data dir directly: ${rawTok} → ${real}`);
923
+ }
924
+ }
925
+ // ── Layer 3: interpreter escape hatches ──
926
+ //
927
+ // `bash -c "..."`, `python -c "..."`, etc. tunnel arbitrary code
928
+ // through an opaque argument that static analysis cannot see into.
929
+ // Even in allow-mode Bash the agent should never need these — the
930
+ // SDK Write/Edit tools and the daemon API cover legitimate
931
+ // file-touching use cases. Blocking the patterns themselves is the
932
+ // only way to keep this hook's guarantees meaningful.
933
+ if (/(?:^|[\s|;&])(?:bash|sh|zsh|ksh|dash|busybox)\s+-c\b/.test(scan) ||
934
+ /(?:^|[\s|;&])(?:python3?|node|ruby|perl|php|deno|bun)\s+-[ce]\b/.test(scan)) {
935
+ return {
936
+ decision: "block",
937
+ reason: `Bash commands that invoke an interpreter with -c / -e are not ` +
938
+ `allowed. Their argument is opaque to static analysis, which ` +
939
+ `defeats the context-write chokepoint. Use the Write/Edit tools ` +
940
+ `or the daemon API at http://localhost:${config.apiPort}/api/context/.`,
941
+ };
942
+ }
943
+ return { continue: true };
944
+ };
945
+ function blockContextWrite(absContextDir, reasonDetail) {
946
+ return {
947
+ decision: "block",
948
+ reason: `Bash commands that reference the context directory (${absContextDir}) are ` +
949
+ `not allowed. Use the daemon API: ` +
950
+ `GET/PUT/PATCH http://localhost:${config.apiPort}/api/context/<path>. ` +
951
+ `The API enforces today-write-lock, md_file_snapshots, CONTEXT_WRITE_PERMISSIONS, ` +
952
+ `and onPromptContextChanged — bypassing it via shell redirects or script ` +
953
+ `engines leaves the memory layer inconsistent. ${reasonDetail}.`,
954
+ };
955
+ }
956
+ const fileWriteHook = async (input) => {
957
+ const hookInput = input;
958
+ const toolInput = hookInput.tool_input;
959
+ const rawFilePath = toolInput?.file_path;
960
+ if (typeof rawFilePath !== "string" || rawFilePath.length === 0) {
961
+ return { continue: true };
962
+ }
963
+ const filePath = rawFilePath;
964
+ const cwd = hookInput.cwd;
965
+ if (!cwd && !isAbsolute(filePath))
966
+ return { continue: true };
967
+ const absFile = resolvePath(cwd ?? "/", filePath);
968
+ // Resolve symlinks. A lexical containment check accepts a symlink
969
+ // whose target lives inside a forbidden dir, because the link
970
+ // itself sits outside. The kernel write follows the link, so the
971
+ // forbidden bytes land anyway. Realpath both sides of every
972
+ // comparison closes that bypass.
973
+ const realFile = realpathLenient(absFile);
974
+ // (a) Block writes into the session-local helper dir. The `curl` shim in
975
+ // `.pa/bin/` carries daemon-auth env at execution time; letting the model
976
+ // rewrite it would turn the helper into a secret exfiltration vector.
977
+ const absHelperDir = resolvePath(cwd ?? "/", ".pa");
978
+ const realHelperDir = realpathLenient(absHelperDir);
979
+ const withinHelperDir = isPathInsideOrEqual(absHelperDir, absFile) ||
980
+ isPathInsideOrEqual(realHelperDir, realFile);
981
+ if (withinHelperDir) {
982
+ return {
983
+ decision: "block",
984
+ reason: "Direct Write/Edit to .pa is forbidden. " +
985
+ "Session helper binaries are managed by the daemon.",
986
+ };
987
+ }
988
+ // (b) Block writes into the context dir.
989
+ const contextDir = getContextDir(config);
990
+ const absContextDir = resolvePath(contextDir);
991
+ const realContextDir = realpathLenient(absContextDir);
992
+ const withinContext = isPathInsideOrEqual(absContextDir, absFile) ||
993
+ isPathInsideOrEqual(realContextDir, realFile);
994
+ if (withinContext) {
995
+ return {
996
+ decision: "block",
997
+ reason: `Direct Write/Edit to context dir is forbidden. ` +
998
+ `Use the daemon API instead: ` +
999
+ `PUT http://localhost:${config.apiPort}/api/context/<path> (full replace) or ` +
1000
+ `PATCH http://localhost:${config.apiPort}/api/context/<path> (section op). ` +
1001
+ `The API enforces CONTEXT_WRITE_PERMISSIONS, morningRoutineLock, md_file_snapshots, ` +
1002
+ `onPromptContextChanged, and expectedMtime concurrency. Path: ${absFile}` +
1003
+ (realFile !== absFile ? ` (realpath: ${realFile})` : ""),
1004
+ };
1005
+ }
1006
+ // (c) Mark vault-scoped writes for observer attribution.
1007
+ // Targets the EXTERNAL Obsidian vault; the ObsidianWatcher observer
1008
+ // watches that path and would otherwise misattribute agent writes
1009
+ // as user writes.
1010
+ if (!writeTracker)
1011
+ return { continue: true };
1012
+ const vaultPath = config.externalObsidianVaultPath;
1013
+ if (!vaultPath)
1014
+ return { continue: true };
1015
+ const absVault = resolvePath(vaultPath);
1016
+ const realVault = realpathLenient(absVault);
1017
+ const withinVault = isPathInsideOrEqual(absVault, absFile) ||
1018
+ isPathInsideOrEqual(realVault, realFile);
1019
+ if (!withinVault)
1020
+ return { continue: true };
1021
+ // Mark BOTH paths so the observer can match whichever form the
1022
+ // ObsidianWatcher emits. Most filesystems report the lexical path;
1023
+ // the realpath form is belt-and-braces.
1024
+ writeTracker.markWriting(absFile);
1025
+ if (realFile !== absFile)
1026
+ writeTracker.markWriting(realFile);
1027
+ logger.debug({ filePath: absFile, realPath: realFile }, "vault write pre-marked for observer attribution");
1028
+ return { continue: true };
1029
+ };
1030
+ // EXECUTION-MODE-DESIGN.md §6 — absolute-block audit hook. Runs ahead
1031
+ // of every other Bash/Read/Write/Edit hook in both modes. The SDK-level
1032
+ // `disallowedTools` rejection is the authoritative block; this hook is
1033
+ // redundant defense-in-depth that also writes the `blocked_absolute`
1034
+ // audit row so the owner can see the layer is active.
1035
+ const makeAbsoluteBlockHook = (toolName, argField) => async (input) => {
1036
+ const toolInput = input.tool_input;
1037
+ const raw = toolInput?.[argField];
1038
+ if (typeof raw !== "string")
1039
+ return { continue: true };
1040
+ const match = classifyAbsoluteBlock(toolName, raw);
1041
+ if (!match)
1042
+ return { continue: true };
1043
+ recordAbsoluteBlockAudit({
1044
+ db: getMcpContext?.()?.db,
1045
+ backend: "claude",
1046
+ mode: config.claudeExecutionPermissionMode,
1047
+ match,
1048
+ toolName,
1049
+ });
1050
+ return {
1051
+ decision: "block",
1052
+ reason: `Absolute-block layer denied this ${toolName} call ` +
1053
+ `(category: ${match.category}). This rule holds in both Safe ` +
1054
+ `and Allow modes — see EXECUTION-MODE-DESIGN.md §6.`,
1055
+ };
1056
+ };
1057
+ const bashAbsoluteBlockHook = makeAbsoluteBlockHook("Bash", "command");
1058
+ const readAbsoluteBlockHook = makeAbsoluteBlockHook("Read", "file_path");
1059
+ const writeAbsoluteBlockHook = makeAbsoluteBlockHook("Write", "file_path");
1060
+ const editAbsoluteBlockHook = makeAbsoluteBlockHook("Edit", "file_path");
1061
+ // The context-write hook is always attached to Bash — it is the only
1062
+ // guarantee that the daemon-API chokepoint for memory files survives
1063
+ // allow mode (where curl/jq restrictions are dropped and Bash can
1064
+ // otherwise redirect into context/*.md freely).
1065
+ //
1066
+ // The absolute-block audit hook is appended LAST on every matcher
1067
+ // (§6.3). Appended rather than prepended so existing per-index hook
1068
+ // tests keep pointing at the same functions; semantically it is a
1069
+ // fallback defense whose practical effect is duplicating the SDK's
1070
+ // `disallowedTools` rejection into an `agent_actions` row.
1071
+ return {
1072
+ PreToolUse: [
1073
+ {
1074
+ matcher: "Bash",
1075
+ hooks: allowMode
1076
+ ? [
1077
+ wrapBashHook("bashContextWriteHook", bashContextWriteHook),
1078
+ wrapBashHook("bashAbsoluteBlockHook", bashAbsoluteBlockHook),
1079
+ ]
1080
+ : [
1081
+ wrapBashHook("bashCurlHook", bashCurlHook),
1082
+ wrapBashHook("bashJqHook", bashJqHook),
1083
+ wrapBashHook("bashContextWriteHook", bashContextWriteHook),
1084
+ wrapBashHook("bashAbsoluteBlockHook", bashAbsoluteBlockHook),
1085
+ ],
1086
+ },
1087
+ { matcher: "Write", hooks: [fileWriteHook, writeAbsoluteBlockHook] },
1088
+ { matcher: "Edit", hooks: [fileWriteHook, editAbsoluteBlockHook] },
1089
+ { matcher: "Read", hooks: [readAbsoluteBlockHook] },
1090
+ ],
1091
+ };
1092
+ }
1093
+ //# sourceMappingURL=claude-tool-collection.js.map