@librechat/agents 3.2.32 → 3.2.34

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 (597) hide show
  1. package/dist/cjs/_virtual/_rolldown/runtime.cjs +23 -0
  2. package/dist/cjs/agents/AgentContext.cjs +844 -1046
  3. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  4. package/dist/cjs/common/constants.cjs +13 -13
  5. package/dist/cjs/common/constants.cjs.map +1 -1
  6. package/dist/cjs/common/enum.cjs +233 -240
  7. package/dist/cjs/common/enum.cjs.map +1 -1
  8. package/dist/cjs/common/index.cjs +2 -0
  9. package/dist/cjs/events.cjs +121 -169
  10. package/dist/cjs/events.cjs.map +1 -1
  11. package/dist/cjs/graphs/Graph.cjs +1389 -1807
  12. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  13. package/dist/cjs/graphs/MultiAgentGraph.cjs +713 -945
  14. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  15. package/dist/cjs/graphs/index.cjs +2 -0
  16. package/dist/cjs/hitl/askUserQuestion.cjs +60 -62
  17. package/dist/cjs/hitl/askUserQuestion.cjs.map +1 -1
  18. package/dist/cjs/hitl/index.cjs +1 -0
  19. package/dist/cjs/hooks/HookRegistry.cjs +176 -202
  20. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  21. package/dist/cjs/hooks/createToolPolicyHook.cjs +71 -101
  22. package/dist/cjs/hooks/createToolPolicyHook.cjs.map +1 -1
  23. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +170 -273
  24. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs.map +1 -1
  25. package/dist/cjs/hooks/executeHooks.cjs +227 -282
  26. package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
  27. package/dist/cjs/hooks/index.cjs +6 -0
  28. package/dist/cjs/hooks/matchers.cjs +196 -230
  29. package/dist/cjs/hooks/matchers.cjs.map +1 -1
  30. package/dist/cjs/hooks/types.cjs +24 -24
  31. package/dist/cjs/hooks/types.cjs.map +1 -1
  32. package/dist/cjs/instrumentation.cjs +110 -137
  33. package/dist/cjs/instrumentation.cjs.map +1 -1
  34. package/dist/cjs/langchain/google-common.cjs +0 -3
  35. package/dist/cjs/langchain/index.cjs +80 -43
  36. package/dist/cjs/langchain/language_models/chat_models.cjs +0 -3
  37. package/dist/cjs/langchain/messages/tool.cjs +0 -3
  38. package/dist/cjs/langchain/messages.cjs +35 -18
  39. package/dist/cjs/langchain/openai.cjs +0 -3
  40. package/dist/cjs/langchain/prompts.cjs +5 -8
  41. package/dist/cjs/langchain/runnables.cjs +11 -10
  42. package/dist/cjs/langchain/tools.cjs +14 -11
  43. package/dist/cjs/langchain/utils/env.cjs +5 -8
  44. package/dist/cjs/langfuse.cjs +60 -79
  45. package/dist/cjs/langfuse.cjs.map +1 -1
  46. package/dist/cjs/langfuseToolOutputTracing.cjs +267 -399
  47. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
  48. package/dist/cjs/llm/anthropic/index.cjs +432 -562
  49. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  50. package/dist/cjs/llm/anthropic/types.cjs +23 -47
  51. package/dist/cjs/llm/anthropic/types.cjs.map +1 -1
  52. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +441 -731
  53. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  54. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +171 -256
  55. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
  56. package/dist/cjs/llm/anthropic/utils/output_parsers.cjs +2 -0
  57. package/dist/cjs/llm/anthropic/utils/tools.cjs +12 -26
  58. package/dist/cjs/llm/anthropic/utils/tools.cjs.map +1 -1
  59. package/dist/cjs/llm/bedrock/index.cjs +214 -240
  60. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  61. package/dist/cjs/llm/bedrock/toolCache.cjs +84 -106
  62. package/dist/cjs/llm/bedrock/toolCache.cjs.map +1 -1
  63. package/dist/cjs/llm/bedrock/utils/index.cjs +2 -0
  64. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +357 -620
  65. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  66. package/dist/cjs/llm/bedrock/utils/message_outputs.cjs +141 -149
  67. package/dist/cjs/llm/bedrock/utils/message_outputs.cjs.map +1 -1
  68. package/dist/cjs/llm/fake.cjs +86 -96
  69. package/dist/cjs/llm/fake.cjs.map +1 -1
  70. package/dist/cjs/llm/google/index.cjs +183 -237
  71. package/dist/cjs/llm/google/index.cjs.map +1 -1
  72. package/dist/cjs/llm/google/utils/common.cjs +404 -674
  73. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  74. package/dist/cjs/llm/google/utils/zod_to_genai_parameters.cjs +2 -0
  75. package/dist/cjs/llm/init.cjs +44 -53
  76. package/dist/cjs/llm/init.cjs.map +1 -1
  77. package/dist/cjs/llm/invoke.cjs +142 -182
  78. package/dist/cjs/llm/invoke.cjs.map +1 -1
  79. package/dist/cjs/llm/openai/index.cjs +1035 -1273
  80. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  81. package/dist/cjs/llm/openai/utils/index.cjs +189 -316
  82. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  83. package/dist/cjs/llm/openrouter/index.cjs +102 -153
  84. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  85. package/dist/cjs/llm/openrouter/toolCache.cjs +35 -44
  86. package/dist/cjs/llm/openrouter/toolCache.cjs.map +1 -1
  87. package/dist/cjs/llm/providers.cjs +29 -37
  88. package/dist/cjs/llm/providers.cjs.map +1 -1
  89. package/dist/cjs/llm/request.cjs +20 -33
  90. package/dist/cjs/llm/request.cjs.map +1 -1
  91. package/dist/cjs/llm/vertexai/index.cjs +446 -453
  92. package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
  93. package/dist/cjs/main.cjs +547 -528
  94. package/dist/cjs/messages/anthropicToolCache.cjs +68 -119
  95. package/dist/cjs/messages/anthropicToolCache.cjs.map +1 -1
  96. package/dist/cjs/messages/cache.cjs +305 -418
  97. package/dist/cjs/messages/cache.cjs.map +1 -1
  98. package/dist/cjs/messages/content.cjs +36 -49
  99. package/dist/cjs/messages/content.cjs.map +1 -1
  100. package/dist/cjs/messages/contextPruning.cjs +112 -145
  101. package/dist/cjs/messages/contextPruning.cjs.map +1 -1
  102. package/dist/cjs/messages/contextPruningSettings.cjs +36 -46
  103. package/dist/cjs/messages/contextPruningSettings.cjs.map +1 -1
  104. package/dist/cjs/messages/core.cjs +256 -397
  105. package/dist/cjs/messages/core.cjs.map +1 -1
  106. package/dist/cjs/messages/format.cjs +904 -1387
  107. package/dist/cjs/messages/format.cjs.map +1 -1
  108. package/dist/cjs/messages/ids.cjs +16 -20
  109. package/dist/cjs/messages/ids.cjs.map +1 -1
  110. package/dist/cjs/messages/index.cjs +12 -0
  111. package/dist/cjs/messages/langchain.cjs +18 -18
  112. package/dist/cjs/messages/langchain.cjs.map +1 -1
  113. package/dist/cjs/messages/prune.cjs +1054 -1517
  114. package/dist/cjs/messages/prune.cjs.map +1 -1
  115. package/dist/cjs/messages/recency.cjs +77 -95
  116. package/dist/cjs/messages/recency.cjs.map +1 -1
  117. package/dist/cjs/messages/reducer.cjs +63 -78
  118. package/dist/cjs/messages/reducer.cjs.map +1 -1
  119. package/dist/cjs/messages/tools.cjs +51 -79
  120. package/dist/cjs/messages/tools.cjs.map +1 -1
  121. package/dist/cjs/openai/index.cjs +171 -217
  122. package/dist/cjs/openai/index.cjs.map +1 -1
  123. package/dist/cjs/responses/index.cjs +302 -391
  124. package/dist/cjs/responses/index.cjs.map +1 -1
  125. package/dist/cjs/run.cjs +903 -1113
  126. package/dist/cjs/run.cjs.map +1 -1
  127. package/dist/cjs/session/AgentSession.cjs +805 -986
  128. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  129. package/dist/cjs/session/JsonlSessionStore.cjs +327 -410
  130. package/dist/cjs/session/JsonlSessionStore.cjs.map +1 -1
  131. package/dist/cjs/session/handlers.cjs +192 -208
  132. package/dist/cjs/session/handlers.cjs.map +1 -1
  133. package/dist/cjs/session/ids.cjs +9 -10
  134. package/dist/cjs/session/ids.cjs.map +1 -1
  135. package/dist/cjs/session/index.cjs +4 -0
  136. package/dist/cjs/session/messageSerialization.cjs +94 -156
  137. package/dist/cjs/session/messageSerialization.cjs.map +1 -1
  138. package/dist/cjs/splitStream.cjs +147 -206
  139. package/dist/cjs/splitStream.cjs.map +1 -1
  140. package/dist/cjs/stream.cjs +874 -1344
  141. package/dist/cjs/stream.cjs.map +1 -1
  142. package/dist/cjs/summarization/index.cjs +57 -101
  143. package/dist/cjs/summarization/index.cjs.map +1 -1
  144. package/dist/cjs/summarization/node.cjs +643 -796
  145. package/dist/cjs/summarization/node.cjs.map +1 -1
  146. package/dist/cjs/tools/BashExecutor.cjs +110 -136
  147. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  148. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +165 -245
  149. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  150. package/dist/cjs/tools/Calculator.cjs +36 -57
  151. package/dist/cjs/tools/Calculator.cjs.map +1 -1
  152. package/dist/cjs/tools/CodeExecutor.cjs +126 -168
  153. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  154. package/dist/cjs/tools/CodeSessionFileSummary.cjs +36 -46
  155. package/dist/cjs/tools/CodeSessionFileSummary.cjs.map +1 -1
  156. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +459 -649
  157. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  158. package/dist/cjs/tools/ReadFile.cjs +17 -20
  159. package/dist/cjs/tools/ReadFile.cjs.map +1 -1
  160. package/dist/cjs/tools/SkillTool.cjs +26 -27
  161. package/dist/cjs/tools/SkillTool.cjs.map +1 -1
  162. package/dist/cjs/tools/SubagentTool.cjs +59 -61
  163. package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
  164. package/dist/cjs/tools/ToolNode.cjs +2146 -2686
  165. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  166. package/dist/cjs/tools/ToolSearch.cjs +663 -825
  167. package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
  168. package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs +248 -340
  169. package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs.map +1 -1
  170. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs +170 -197
  171. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs.map +1 -1
  172. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs +425 -520
  173. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs.map +1 -1
  174. package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs +91 -124
  175. package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs.map +1 -1
  176. package/dist/cjs/tools/cloudflare/index.cjs +4 -0
  177. package/dist/cjs/tools/eagerEventExecution.cjs +75 -99
  178. package/dist/cjs/tools/eagerEventExecution.cjs.map +1 -1
  179. package/dist/cjs/tools/handlers.cjs +200 -262
  180. package/dist/cjs/tools/handlers.cjs.map +1 -1
  181. package/dist/cjs/tools/local/CompileCheckTool.cjs +150 -212
  182. package/dist/cjs/tools/local/CompileCheckTool.cjs.map +1 -1
  183. package/dist/cjs/tools/local/FileCheckpointer.cjs +77 -85
  184. package/dist/cjs/tools/local/FileCheckpointer.cjs.map +1 -1
  185. package/dist/cjs/tools/local/LocalCodingTools.cjs +763 -1022
  186. package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -1
  187. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +666 -941
  188. package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -1
  189. package/dist/cjs/tools/local/LocalExecutionTools.cjs +49 -92
  190. package/dist/cjs/tools/local/LocalExecutionTools.cjs.map +1 -1
  191. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +286 -354
  192. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs.map +1 -1
  193. package/dist/cjs/tools/local/attachments.cjs +108 -165
  194. package/dist/cjs/tools/local/attachments.cjs.map +1 -1
  195. package/dist/cjs/tools/local/bashAst.cjs +99 -113
  196. package/dist/cjs/tools/local/bashAst.cjs.map +1 -1
  197. package/dist/cjs/tools/local/editStrategies.cjs +126 -169
  198. package/dist/cjs/tools/local/editStrategies.cjs.map +1 -1
  199. package/dist/cjs/tools/local/index.cjs +12 -0
  200. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs +136 -218
  201. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs.map +1 -1
  202. package/dist/cjs/tools/local/syntaxCheck.cjs +142 -161
  203. package/dist/cjs/tools/local/syntaxCheck.cjs.map +1 -1
  204. package/dist/cjs/tools/local/textEncoding.cjs +25 -23
  205. package/dist/cjs/tools/local/textEncoding.cjs.map +1 -1
  206. package/dist/cjs/tools/local/workspaceFS.cjs +38 -46
  207. package/dist/cjs/tools/local/workspaceFS.cjs.map +1 -1
  208. package/dist/cjs/tools/ptcTimeout.cjs +27 -47
  209. package/dist/cjs/tools/ptcTimeout.cjs.map +1 -1
  210. package/dist/cjs/tools/schema.cjs +24 -23
  211. package/dist/cjs/tools/schema.cjs.map +1 -1
  212. package/dist/cjs/tools/search/anthropic.cjs +24 -33
  213. package/dist/cjs/tools/search/anthropic.cjs.map +1 -1
  214. package/dist/cjs/tools/search/content.cjs +95 -137
  215. package/dist/cjs/tools/search/content.cjs.map +1 -1
  216. package/dist/cjs/tools/search/firecrawl.cjs +141 -172
  217. package/dist/cjs/tools/search/firecrawl.cjs.map +1 -1
  218. package/dist/cjs/tools/search/format.cjs +128 -196
  219. package/dist/cjs/tools/search/format.cjs.map +1 -1
  220. package/dist/cjs/tools/search/highlights.cjs +165 -232
  221. package/dist/cjs/tools/search/highlights.cjs.map +1 -1
  222. package/dist/cjs/tools/search/index.cjs +2 -0
  223. package/dist/cjs/tools/search/rerankers.cjs +151 -174
  224. package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
  225. package/dist/cjs/tools/search/schema.cjs +40 -39
  226. package/dist/cjs/tools/search/schema.cjs.map +1 -1
  227. package/dist/cjs/tools/search/search.cjs +428 -530
  228. package/dist/cjs/tools/search/search.cjs.map +1 -1
  229. package/dist/cjs/tools/search/serper-scraper.cjs +106 -127
  230. package/dist/cjs/tools/search/serper-scraper.cjs.map +1 -1
  231. package/dist/cjs/tools/search/tavily-scraper.cjs +129 -181
  232. package/dist/cjs/tools/search/tavily-scraper.cjs.map +1 -1
  233. package/dist/cjs/tools/search/tavily-search.cjs +295 -359
  234. package/dist/cjs/tools/search/tavily-search.cjs.map +1 -1
  235. package/dist/cjs/tools/search/tool.cjs +260 -299
  236. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  237. package/dist/cjs/tools/search/utils.cjs +74 -117
  238. package/dist/cjs/tools/search/utils.cjs.map +1 -1
  239. package/dist/cjs/tools/skillCatalog.cjs +54 -72
  240. package/dist/cjs/tools/skillCatalog.cjs.map +1 -1
  241. package/dist/cjs/tools/streamedToolCallSeals.cjs +46 -34
  242. package/dist/cjs/tools/streamedToolCallSeals.cjs.map +1 -1
  243. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +612 -771
  244. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  245. package/dist/cjs/tools/subagent/index.cjs +1 -0
  246. package/dist/cjs/tools/toolOutputReferences.cjs +523 -630
  247. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  248. package/dist/cjs/utils/callbacks.cjs +11 -21
  249. package/dist/cjs/utils/callbacks.cjs.map +1 -1
  250. package/dist/cjs/utils/errors.cjs +70 -95
  251. package/dist/cjs/utils/errors.cjs.map +1 -1
  252. package/dist/cjs/utils/events.cjs +32 -42
  253. package/dist/cjs/utils/events.cjs.map +1 -1
  254. package/dist/cjs/utils/graph.cjs +8 -12
  255. package/dist/cjs/utils/graph.cjs.map +1 -1
  256. package/dist/cjs/utils/handlers.cjs +60 -82
  257. package/dist/cjs/utils/handlers.cjs.map +1 -1
  258. package/dist/cjs/utils/index.cjs +9 -0
  259. package/dist/cjs/utils/llm.cjs +19 -27
  260. package/dist/cjs/utils/llm.cjs.map +1 -1
  261. package/dist/cjs/utils/misc.cjs +30 -46
  262. package/dist/cjs/utils/misc.cjs.map +1 -1
  263. package/dist/cjs/utils/run.cjs +50 -66
  264. package/dist/cjs/utils/run.cjs.map +1 -1
  265. package/dist/cjs/utils/schema.cjs +11 -19
  266. package/dist/cjs/utils/schema.cjs.map +1 -1
  267. package/dist/cjs/utils/title.cjs +71 -106
  268. package/dist/cjs/utils/title.cjs.map +1 -1
  269. package/dist/cjs/utils/tokens.cjs +186 -283
  270. package/dist/cjs/utils/tokens.cjs.map +1 -1
  271. package/dist/cjs/utils/truncation.cjs +95 -114
  272. package/dist/cjs/utils/truncation.cjs.map +1 -1
  273. package/dist/esm/agents/AgentContext.mjs +844 -1044
  274. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  275. package/dist/esm/common/constants.mjs +13 -11
  276. package/dist/esm/common/constants.mjs.map +1 -1
  277. package/dist/esm/common/enum.mjs +221 -238
  278. package/dist/esm/common/enum.mjs.map +1 -1
  279. package/dist/esm/common/index.mjs +3 -0
  280. package/dist/esm/events.mjs +121 -167
  281. package/dist/esm/events.mjs.map +1 -1
  282. package/dist/esm/graphs/Graph.mjs +1388 -1804
  283. package/dist/esm/graphs/Graph.mjs.map +1 -1
  284. package/dist/esm/graphs/MultiAgentGraph.mjs +713 -943
  285. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  286. package/dist/esm/graphs/index.mjs +3 -0
  287. package/dist/esm/hitl/askUserQuestion.mjs +60 -60
  288. package/dist/esm/hitl/askUserQuestion.mjs.map +1 -1
  289. package/dist/esm/hitl/index.mjs +2 -0
  290. package/dist/esm/hooks/HookRegistry.mjs +176 -200
  291. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  292. package/dist/esm/hooks/createToolPolicyHook.mjs +71 -99
  293. package/dist/esm/hooks/createToolPolicyHook.mjs.map +1 -1
  294. package/dist/esm/hooks/createWorkspacePolicyHook.mjs +170 -271
  295. package/dist/esm/hooks/createWorkspacePolicyHook.mjs.map +1 -1
  296. package/dist/esm/hooks/executeHooks.mjs +227 -280
  297. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  298. package/dist/esm/hooks/index.mjs +7 -0
  299. package/dist/esm/hooks/matchers.mjs +196 -228
  300. package/dist/esm/hooks/matchers.mjs.map +1 -1
  301. package/dist/esm/hooks/types.mjs +24 -22
  302. package/dist/esm/hooks/types.mjs.map +1 -1
  303. package/dist/esm/instrumentation.mjs +109 -132
  304. package/dist/esm/instrumentation.mjs.map +1 -1
  305. package/dist/esm/langchain/google-common.mjs +1 -2
  306. package/dist/esm/langchain/index.mjs +5 -5
  307. package/dist/esm/langchain/language_models/chat_models.mjs +1 -2
  308. package/dist/esm/langchain/messages/tool.mjs +1 -2
  309. package/dist/esm/langchain/messages.mjs +2 -2
  310. package/dist/esm/langchain/openai.mjs +1 -2
  311. package/dist/esm/langchain/prompts.mjs +2 -2
  312. package/dist/esm/langchain/runnables.mjs +2 -2
  313. package/dist/esm/langchain/tools.mjs +2 -2
  314. package/dist/esm/langchain/utils/env.mjs +2 -2
  315. package/dist/esm/langfuse.mjs +60 -76
  316. package/dist/esm/langfuse.mjs.map +1 -1
  317. package/dist/esm/langfuseToolOutputTracing.mjs +267 -395
  318. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  319. package/dist/esm/llm/anthropic/index.mjs +432 -559
  320. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  321. package/dist/esm/llm/anthropic/types.mjs +23 -45
  322. package/dist/esm/llm/anthropic/types.mjs.map +1 -1
  323. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +439 -725
  324. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  325. package/dist/esm/llm/anthropic/utils/message_outputs.mjs +171 -253
  326. package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
  327. package/dist/esm/llm/anthropic/utils/output_parsers.mjs +3 -0
  328. package/dist/esm/llm/anthropic/utils/tools.mjs +12 -24
  329. package/dist/esm/llm/anthropic/utils/tools.mjs.map +1 -1
  330. package/dist/esm/llm/bedrock/index.mjs +214 -238
  331. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  332. package/dist/esm/llm/bedrock/toolCache.mjs +84 -104
  333. package/dist/esm/llm/bedrock/toolCache.mjs.map +1 -1
  334. package/dist/esm/llm/bedrock/utils/index.mjs +3 -0
  335. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +357 -618
  336. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  337. package/dist/esm/llm/bedrock/utils/message_outputs.mjs +140 -147
  338. package/dist/esm/llm/bedrock/utils/message_outputs.mjs.map +1 -1
  339. package/dist/esm/llm/fake.mjs +86 -94
  340. package/dist/esm/llm/fake.mjs.map +1 -1
  341. package/dist/esm/llm/google/index.mjs +183 -235
  342. package/dist/esm/llm/google/index.mjs.map +1 -1
  343. package/dist/esm/llm/google/utils/common.mjs +403 -666
  344. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  345. package/dist/esm/llm/google/utils/zod_to_genai_parameters.mjs +3 -0
  346. package/dist/esm/llm/init.mjs +44 -51
  347. package/dist/esm/llm/init.mjs.map +1 -1
  348. package/dist/esm/llm/invoke.mjs +142 -180
  349. package/dist/esm/llm/invoke.mjs.map +1 -1
  350. package/dist/esm/llm/openai/index.mjs +1035 -1268
  351. package/dist/esm/llm/openai/index.mjs.map +1 -1
  352. package/dist/esm/llm/openai/utils/index.mjs +188 -312
  353. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  354. package/dist/esm/llm/openrouter/index.mjs +102 -151
  355. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  356. package/dist/esm/llm/openrouter/toolCache.mjs +35 -42
  357. package/dist/esm/llm/openrouter/toolCache.mjs.map +1 -1
  358. package/dist/esm/llm/providers.mjs +29 -34
  359. package/dist/esm/llm/providers.mjs.map +1 -1
  360. package/dist/esm/llm/request.mjs +20 -31
  361. package/dist/esm/llm/request.mjs.map +1 -1
  362. package/dist/esm/llm/vertexai/index.mjs +446 -449
  363. package/dist/esm/llm/vertexai/index.mjs.map +1 -1
  364. package/dist/esm/main.mjs +99 -87
  365. package/dist/esm/messages/anthropicToolCache.mjs +68 -117
  366. package/dist/esm/messages/anthropicToolCache.mjs.map +1 -1
  367. package/dist/esm/messages/cache.mjs +305 -416
  368. package/dist/esm/messages/cache.mjs.map +1 -1
  369. package/dist/esm/messages/content.mjs +36 -47
  370. package/dist/esm/messages/content.mjs.map +1 -1
  371. package/dist/esm/messages/contextPruning.mjs +112 -143
  372. package/dist/esm/messages/contextPruning.mjs.map +1 -1
  373. package/dist/esm/messages/contextPruningSettings.mjs +36 -44
  374. package/dist/esm/messages/contextPruningSettings.mjs.map +1 -1
  375. package/dist/esm/messages/core.mjs +254 -393
  376. package/dist/esm/messages/core.mjs.map +1 -1
  377. package/dist/esm/messages/format.mjs +902 -1383
  378. package/dist/esm/messages/format.mjs.map +1 -1
  379. package/dist/esm/messages/ids.mjs +16 -18
  380. package/dist/esm/messages/ids.mjs.map +1 -1
  381. package/dist/esm/messages/index.mjs +13 -0
  382. package/dist/esm/messages/langchain.mjs +18 -16
  383. package/dist/esm/messages/langchain.mjs.map +1 -1
  384. package/dist/esm/messages/prune.mjs +1053 -1514
  385. package/dist/esm/messages/prune.mjs.map +1 -1
  386. package/dist/esm/messages/recency.mjs +77 -93
  387. package/dist/esm/messages/recency.mjs.map +1 -1
  388. package/dist/esm/messages/reducer.mjs +63 -76
  389. package/dist/esm/messages/reducer.mjs.map +1 -1
  390. package/dist/esm/messages/tools.mjs +49 -75
  391. package/dist/esm/messages/tools.mjs.map +1 -1
  392. package/dist/esm/openai/index.mjs +170 -215
  393. package/dist/esm/openai/index.mjs.map +1 -1
  394. package/dist/esm/responses/index.mjs +301 -389
  395. package/dist/esm/responses/index.mjs.map +1 -1
  396. package/dist/esm/run.mjs +903 -1111
  397. package/dist/esm/run.mjs.map +1 -1
  398. package/dist/esm/session/AgentSession.mjs +806 -985
  399. package/dist/esm/session/AgentSession.mjs.map +1 -1
  400. package/dist/esm/session/JsonlSessionStore.mjs +326 -407
  401. package/dist/esm/session/JsonlSessionStore.mjs.map +1 -1
  402. package/dist/esm/session/handlers.mjs +192 -206
  403. package/dist/esm/session/handlers.mjs.map +1 -1
  404. package/dist/esm/session/ids.mjs +9 -8
  405. package/dist/esm/session/ids.mjs.map +1 -1
  406. package/dist/esm/session/index.mjs +5 -0
  407. package/dist/esm/session/messageSerialization.mjs +94 -154
  408. package/dist/esm/session/messageSerialization.mjs.map +1 -1
  409. package/dist/esm/splitStream.mjs +147 -204
  410. package/dist/esm/splitStream.mjs.map +1 -1
  411. package/dist/esm/stream.mjs +872 -1341
  412. package/dist/esm/stream.mjs.map +1 -1
  413. package/dist/esm/summarization/index.mjs +57 -99
  414. package/dist/esm/summarization/index.mjs.map +1 -1
  415. package/dist/esm/summarization/node.mjs +640 -790
  416. package/dist/esm/summarization/node.mjs.map +1 -1
  417. package/dist/esm/tools/BashExecutor.mjs +103 -129
  418. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  419. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +162 -239
  420. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  421. package/dist/esm/tools/Calculator.mjs +34 -36
  422. package/dist/esm/tools/Calculator.mjs.map +1 -1
  423. package/dist/esm/tools/CodeExecutor.mjs +123 -164
  424. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  425. package/dist/esm/tools/CodeSessionFileSummary.mjs +36 -44
  426. package/dist/esm/tools/CodeSessionFileSummary.mjs.map +1 -1
  427. package/dist/esm/tools/ProgrammaticToolCalling.mjs +454 -644
  428. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  429. package/dist/esm/tools/ReadFile.mjs +17 -18
  430. package/dist/esm/tools/ReadFile.mjs.map +1 -1
  431. package/dist/esm/tools/SkillTool.mjs +26 -25
  432. package/dist/esm/tools/SkillTool.mjs.map +1 -1
  433. package/dist/esm/tools/SubagentTool.mjs +59 -59
  434. package/dist/esm/tools/SubagentTool.mjs.map +1 -1
  435. package/dist/esm/tools/ToolNode.mjs +2144 -2684
  436. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  437. package/dist/esm/tools/ToolSearch.mjs +659 -804
  438. package/dist/esm/tools/ToolSearch.mjs.map +1 -1
  439. package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs +248 -338
  440. package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs.map +1 -1
  441. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs +170 -195
  442. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs.map +1 -1
  443. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs +424 -517
  444. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs.map +1 -1
  445. package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs +91 -122
  446. package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs.map +1 -1
  447. package/dist/esm/tools/cloudflare/index.mjs +5 -0
  448. package/dist/esm/tools/eagerEventExecution.mjs +75 -96
  449. package/dist/esm/tools/eagerEventExecution.mjs.map +1 -1
  450. package/dist/esm/tools/handlers.mjs +200 -260
  451. package/dist/esm/tools/handlers.mjs.map +1 -1
  452. package/dist/esm/tools/local/CompileCheckTool.mjs +150 -210
  453. package/dist/esm/tools/local/CompileCheckTool.mjs.map +1 -1
  454. package/dist/esm/tools/local/FileCheckpointer.mjs +77 -83
  455. package/dist/esm/tools/local/FileCheckpointer.mjs.map +1 -1
  456. package/dist/esm/tools/local/LocalCodingTools.mjs +760 -1017
  457. package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -1
  458. package/dist/esm/tools/local/LocalExecutionEngine.mjs +663 -936
  459. package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -1
  460. package/dist/esm/tools/local/LocalExecutionTools.mjs +49 -90
  461. package/dist/esm/tools/local/LocalExecutionTools.mjs.map +1 -1
  462. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +283 -349
  463. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs.map +1 -1
  464. package/dist/esm/tools/local/attachments.mjs +108 -163
  465. package/dist/esm/tools/local/attachments.mjs.map +1 -1
  466. package/dist/esm/tools/local/bashAst.mjs +99 -111
  467. package/dist/esm/tools/local/bashAst.mjs.map +1 -1
  468. package/dist/esm/tools/local/editStrategies.mjs +126 -167
  469. package/dist/esm/tools/local/editStrategies.mjs.map +1 -1
  470. package/dist/esm/tools/local/index.mjs +13 -0
  471. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs +136 -216
  472. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs.map +1 -1
  473. package/dist/esm/tools/local/syntaxCheck.mjs +138 -155
  474. package/dist/esm/tools/local/syntaxCheck.mjs.map +1 -1
  475. package/dist/esm/tools/local/textEncoding.mjs +25 -21
  476. package/dist/esm/tools/local/textEncoding.mjs.map +1 -1
  477. package/dist/esm/tools/local/workspaceFS.mjs +38 -44
  478. package/dist/esm/tools/local/workspaceFS.mjs.map +1 -1
  479. package/dist/esm/tools/ptcTimeout.mjs +27 -42
  480. package/dist/esm/tools/ptcTimeout.mjs.map +1 -1
  481. package/dist/esm/tools/schema.mjs +24 -21
  482. package/dist/esm/tools/schema.mjs.map +1 -1
  483. package/dist/esm/tools/search/anthropic.mjs +24 -31
  484. package/dist/esm/tools/search/anthropic.mjs.map +1 -1
  485. package/dist/esm/tools/search/content.mjs +93 -116
  486. package/dist/esm/tools/search/content.mjs.map +1 -1
  487. package/dist/esm/tools/search/firecrawl.mjs +139 -169
  488. package/dist/esm/tools/search/firecrawl.mjs.map +1 -1
  489. package/dist/esm/tools/search/format.mjs +128 -194
  490. package/dist/esm/tools/search/format.mjs.map +1 -1
  491. package/dist/esm/tools/search/highlights.mjs +165 -230
  492. package/dist/esm/tools/search/highlights.mjs.map +1 -1
  493. package/dist/esm/tools/search/index.mjs +3 -0
  494. package/dist/esm/tools/search/rerankers.mjs +149 -168
  495. package/dist/esm/tools/search/rerankers.mjs.map +1 -1
  496. package/dist/esm/tools/search/schema.mjs +39 -37
  497. package/dist/esm/tools/search/schema.mjs.map +1 -1
  498. package/dist/esm/tools/search/search.mjs +426 -528
  499. package/dist/esm/tools/search/search.mjs.map +1 -1
  500. package/dist/esm/tools/search/serper-scraper.mjs +104 -124
  501. package/dist/esm/tools/search/serper-scraper.mjs.map +1 -1
  502. package/dist/esm/tools/search/tavily-scraper.mjs +127 -178
  503. package/dist/esm/tools/search/tavily-scraper.mjs.map +1 -1
  504. package/dist/esm/tools/search/tavily-search.mjs +293 -357
  505. package/dist/esm/tools/search/tavily-search.mjs.map +1 -1
  506. package/dist/esm/tools/search/tool.mjs +259 -297
  507. package/dist/esm/tools/search/tool.mjs.map +1 -1
  508. package/dist/esm/tools/search/utils.mjs +74 -115
  509. package/dist/esm/tools/search/utils.mjs.map +1 -1
  510. package/dist/esm/tools/skillCatalog.mjs +54 -70
  511. package/dist/esm/tools/skillCatalog.mjs.map +1 -1
  512. package/dist/esm/tools/streamedToolCallSeals.mjs +42 -31
  513. package/dist/esm/tools/streamedToolCallSeals.mjs.map +1 -1
  514. package/dist/esm/tools/subagent/SubagentExecutor.mjs +612 -768
  515. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  516. package/dist/esm/tools/subagent/index.mjs +2 -0
  517. package/dist/esm/tools/toolOutputReferences.mjs +523 -624
  518. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  519. package/dist/esm/utils/callbacks.mjs +11 -19
  520. package/dist/esm/utils/callbacks.mjs.map +1 -1
  521. package/dist/esm/utils/errors.mjs +70 -93
  522. package/dist/esm/utils/errors.mjs.map +1 -1
  523. package/dist/esm/utils/events.mjs +32 -40
  524. package/dist/esm/utils/events.mjs.map +1 -1
  525. package/dist/esm/utils/graph.mjs +8 -10
  526. package/dist/esm/utils/graph.mjs.map +1 -1
  527. package/dist/esm/utils/handlers.mjs +60 -80
  528. package/dist/esm/utils/handlers.mjs.map +1 -1
  529. package/dist/esm/utils/index.mjs +10 -0
  530. package/dist/esm/utils/llm.mjs +19 -25
  531. package/dist/esm/utils/llm.mjs.map +1 -1
  532. package/dist/esm/utils/misc.mjs +30 -44
  533. package/dist/esm/utils/misc.mjs.map +1 -1
  534. package/dist/esm/utils/run.mjs +50 -64
  535. package/dist/esm/utils/run.mjs.map +1 -1
  536. package/dist/esm/utils/schema.mjs +11 -17
  537. package/dist/esm/utils/schema.mjs.map +1 -1
  538. package/dist/esm/utils/title.mjs +71 -104
  539. package/dist/esm/utils/title.mjs.map +1 -1
  540. package/dist/esm/utils/tokens.mjs +186 -281
  541. package/dist/esm/utils/tokens.mjs.map +1 -1
  542. package/dist/esm/utils/truncation.mjs +95 -112
  543. package/dist/esm/utils/truncation.mjs.map +1 -1
  544. package/dist/types/llm/bedrock/utils/index.d.ts +1 -1
  545. package/dist/types/llm/bedrock/utils/message_outputs.d.ts +9 -0
  546. package/dist/types/llm/vertexai/index.d.ts +10 -0
  547. package/dist/types/tools/ToolNode.d.ts +8 -0
  548. package/dist/types/tools/search/tool.d.ts +17 -0
  549. package/dist/types/tools/search/types.d.ts +4 -0
  550. package/dist/types/tools/streamedToolCallSeals.d.ts +5 -1
  551. package/dist/types/types/tools.d.ts +10 -0
  552. package/package.json +4 -10
  553. package/src/__tests__/stream.eagerEventExecution.test.ts +703 -0
  554. package/src/llm/bedrock/index.ts +40 -0
  555. package/src/llm/bedrock/streamSealDispatch.test.ts +158 -0
  556. package/src/llm/bedrock/utils/index.ts +1 -0
  557. package/src/llm/bedrock/utils/message_outputs.test.ts +85 -0
  558. package/src/llm/bedrock/utils/message_outputs.ts +43 -0
  559. package/src/llm/google/utils/common.test.ts +64 -0
  560. package/src/llm/google/utils/common.ts +18 -0
  561. package/src/llm/openai/index.ts +95 -1
  562. package/src/llm/openai/sequentialToolCallSeals.test.ts +199 -0
  563. package/src/llm/vertexai/index.ts +31 -0
  564. package/src/llm/vertexai/sealStreamedToolCalls.test.ts +88 -0
  565. package/src/llm/vertexai/streamSealDispatch.test.ts +148 -0
  566. package/src/stream.ts +40 -6
  567. package/src/tools/ToolNode.ts +85 -3
  568. package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +368 -0
  569. package/src/tools/search/highlights.ts +9 -1
  570. package/src/tools/search/search.ts +41 -3
  571. package/src/tools/search/source-processing.test.ts +373 -0
  572. package/src/tools/search/tool.ts +22 -2
  573. package/src/tools/search/types.ts +4 -0
  574. package/src/tools/streamedToolCallSeals.ts +37 -9
  575. package/src/types/tools.ts +10 -0
  576. package/dist/cjs/langchain/google-common.cjs.map +0 -1
  577. package/dist/cjs/langchain/index.cjs.map +0 -1
  578. package/dist/cjs/langchain/language_models/chat_models.cjs.map +0 -1
  579. package/dist/cjs/langchain/messages/tool.cjs.map +0 -1
  580. package/dist/cjs/langchain/messages.cjs.map +0 -1
  581. package/dist/cjs/langchain/openai.cjs.map +0 -1
  582. package/dist/cjs/langchain/prompts.cjs.map +0 -1
  583. package/dist/cjs/langchain/runnables.cjs.map +0 -1
  584. package/dist/cjs/langchain/tools.cjs.map +0 -1
  585. package/dist/cjs/langchain/utils/env.cjs.map +0 -1
  586. package/dist/cjs/main.cjs.map +0 -1
  587. package/dist/esm/langchain/google-common.mjs.map +0 -1
  588. package/dist/esm/langchain/index.mjs.map +0 -1
  589. package/dist/esm/langchain/language_models/chat_models.mjs.map +0 -1
  590. package/dist/esm/langchain/messages/tool.mjs.map +0 -1
  591. package/dist/esm/langchain/messages.mjs.map +0 -1
  592. package/dist/esm/langchain/openai.mjs.map +0 -1
  593. package/dist/esm/langchain/prompts.mjs.map +0 -1
  594. package/dist/esm/langchain/runnables.mjs.map +0 -1
  595. package/dist/esm/langchain/tools.mjs.map +0 -1
  596. package/dist/esm/langchain/utils/env.mjs.map +0 -1
  597. package/dist/esm/main.mjs.map +0 -1
@@ -1,295 +1,194 @@
1
- import { homedir } from 'os';
2
- import { realpath } from 'fs/promises';
3
- import { resolve, isAbsolute, relative } from 'path';
4
- import { Constants } from '../common/enum.mjs';
5
-
1
+ import "../common/enum.mjs";
2
+ import "../common/index.mjs";
3
+ import { isAbsolute, relative, resolve } from "path";
4
+ import { homedir } from "os";
5
+ import { realpath } from "fs/promises";
6
+ //#region src/hooks/createWorkspacePolicyHook.ts
6
7
  /**
7
- * Workspace boundary policy as a `PreToolUse` hook.
8
- *
9
- * Local-engine file tools enforce a hard workspace boundary at the
10
- * tool implementation layer (`resolveWorkspacePathSafe`). This hook
11
- * adds a complementary, host-controlled layer on top that uses the
12
- * standard PreToolUse / HITL machinery to *negotiate* access to
13
- * paths outside the workspace — instead of just throwing.
14
- *
15
- * The host opts in by registering this hook on a `HookRegistry`; the
16
- * hook inspects each tool call's input, extracts the file paths it
17
- * mentions via per-tool extractors, and returns:
18
- *
19
- * - `allow` — every path is inside `workspace.root`
20
- * (or `additionalRoots`)
21
- * - `deny` — at least one path is outside, and the
22
- * configured outside-policy is `'deny'`
23
- * - `ask` — at least one path is outside, and the
24
- * outside-policy is `'ask'` (default).
25
- * When `humanInTheLoop.enabled` is true,
26
- * the existing PreToolUse `'ask'` flow
27
- * raises a tool_approval interrupt the
28
- * host UI can render. When HITL is off,
29
- * `'ask'` collapses to `deny` (matches
30
- * the rest of the SDK's default).
31
- *
32
- * Default per-tool path extractors cover the local-engine coding
33
- * suite (`read_file`, `write_file`, `edit_file`, `grep_search`,
34
- * `glob_search`, `list_directory`, `compile_check`). The host can
35
- * override or extend via `pathExtractors`. Bash/code paths are not
36
- * extracted by default — bash command parsing is its own concern, and
37
- * the existing `bashAst` validator + sandbox-runtime fs allowlist are
38
- * the right gates for those.
39
- *
40
- * Important: this hook does NOT replace `resolveWorkspacePathSafe`.
41
- * Even if the hook returns `allow`, the file tool still enforces its
42
- * own clamp unless `workspace.allowReadOutside` /
43
- * `workspace.allowWriteOutside` (or the legacy
44
- * `allowOutsideWorkspace`) is set. The recommended composition for
45
- * "ask the user" semantics is:
46
- *
47
- * workspace: {
48
- * root,
49
- * allowReadOutside: true,
50
- * allowWriteOutside: true,
51
- * },
52
- * // …with the hook installed and humanInTheLoop.enabled = true.
53
- */
8
+ * Workspace boundary policy as a `PreToolUse` hook.
9
+ *
10
+ * Local-engine file tools enforce a hard workspace boundary at the
11
+ * tool implementation layer (`resolveWorkspacePathSafe`). This hook
12
+ * adds a complementary, host-controlled layer on top that uses the
13
+ * standard PreToolUse / HITL machinery to *negotiate* access to
14
+ * paths outside the workspace — instead of just throwing.
15
+ *
16
+ * The host opts in by registering this hook on a `HookRegistry`; the
17
+ * hook inspects each tool call's input, extracts the file paths it
18
+ * mentions via per-tool extractors, and returns:
19
+ *
20
+ * - `allow` — every path is inside `workspace.root`
21
+ * (or `additionalRoots`)
22
+ * - `deny` — at least one path is outside, and the
23
+ * configured outside-policy is `'deny'`
24
+ * - `ask` — at least one path is outside, and the
25
+ * outside-policy is `'ask'` (default).
26
+ * When `humanInTheLoop.enabled` is true,
27
+ * the existing PreToolUse `'ask'` flow
28
+ * raises a tool_approval interrupt the
29
+ * host UI can render. When HITL is off,
30
+ * `'ask'` collapses to `deny` (matches
31
+ * the rest of the SDK's default).
32
+ *
33
+ * Default per-tool path extractors cover the local-engine coding
34
+ * suite (`read_file`, `write_file`, `edit_file`, `grep_search`,
35
+ * `glob_search`, `list_directory`, `compile_check`). The host can
36
+ * override or extend via `pathExtractors`. Bash/code paths are not
37
+ * extracted by default — bash command parsing is its own concern, and
38
+ * the existing `bashAst` validator + sandbox-runtime fs allowlist are
39
+ * the right gates for those.
40
+ *
41
+ * Important: this hook does NOT replace `resolveWorkspacePathSafe`.
42
+ * Even if the hook returns `allow`, the file tool still enforces its
43
+ * own clamp unless `workspace.allowReadOutside` /
44
+ * `workspace.allowWriteOutside` (or the legacy
45
+ * `allowOutsideWorkspace`) is set. The recommended composition for
46
+ * "ask the user" semantics is:
47
+ *
48
+ * workspace: {
49
+ * root,
50
+ * allowReadOutside: true,
51
+ * allowWriteOutside: true,
52
+ * },
53
+ * // …with the hook installed and humanInTheLoop.enabled = true.
54
+ */
54
55
  const READ_TOOLS = new Set([
55
- Constants.READ_FILE,
56
- Constants.GREP_SEARCH,
57
- Constants.GLOB_SEARCH,
58
- Constants.LIST_DIRECTORY,
59
- Constants.COMPILE_CHECK,
60
- ]);
61
- const WRITE_TOOLS = new Set([
62
- Constants.WRITE_FILE,
63
- Constants.EDIT_FILE,
56
+ "read_file",
57
+ "grep_search",
58
+ "glob_search",
59
+ "list_directory",
60
+ "compile_check"
64
61
  ]);
65
- /**
66
- * Best-effort extractor for `compile_check` — pulls absolute and `~/`
67
- * path tokens out of the `command` string so the workspace boundary
68
- * sees them. Without this, a model could ship `command: 'cat
69
- * /etc/passwd'` and the policy hook would short-circuit to `allow`
70
- * (Codex P1 #26 — the prior `() => []` made the hook a no-op for
71
- * compile_check). Conservative by design:
72
- *
73
- * - Matches `/foo`, `~/foo`, `$HOME/foo`, `${HOME}/foo` followed by
74
- * non-shell-special chars. Stops at whitespace, quotes, redirect
75
- * operators, pipes, semicolons.
76
- * - Strips a leading `--flag=` so `--out=/etc/foo` extracts as
77
- * `/etc/foo` (the path the agent's actually trying to write).
78
- * - Misses relative paths (intended — those resolve under cwd
79
- * anyway), and shell-substituted paths whose final form isn't
80
- * visible at extract time. Hosts that need bulletproof gating
81
- * should pair this with a `bash_tool`-level policy.
82
- */
83
- // `["']?` slots before AND after the captured path cover quoted
84
- // forms like `cat "/etc/passwd"` and `--out='/tmp/x'`. Codex P1 #31
85
- // — the previous regex only matched unquoted tokens, so a model
86
- // could trivially bypass the workspace policy by quoting any
87
- // destination path. The path content character class still excludes
88
- // quotes/whitespace/shell-specials so we don't over-extract; that's
89
- // the defensive trade we want for fallback-grep style matching.
90
- //
91
- // The `\.\.(?:\/[^…]*)?` alternation covers parent-traversal forms
92
- // (`..`, `../secrets.txt`, `../foo/bar`). Without it, a model could
93
- // exfiltrate parent-directory files via `cat ../secrets` and the
94
- // hook would short-circuit to `allow` because the extractor saw no
95
- // "absolute" token. The boundary check at the call site resolves
96
- // non-absolute extracted tokens against `root`, so `../secrets`
97
- // becomes `<parent-of-workspace>/secrets` which the boundary then
98
- // correctly flags as outside. Codex P2 #35.
99
- const PATH_TOKEN = /(?:^|[\s=])(?:--[^\s=]+=)?["']?(\/[^\s'"|;&<>()`]+|~\/[^\s'"|;&<>()`]+|\$\{?HOME\}?\/[^\s'"|;&<>()`]+|\.\.(?:\/[^\s'"|;&<>()`]*)?)["']?/g;
100
- // Back-compat alias kept for any downstream import.
101
- const ABSOLUTE_PATH_TOKEN = PATH_TOKEN;
62
+ const WRITE_TOOLS = new Set(["write_file", "edit_file"]);
63
+ const ABSOLUTE_PATH_TOKEN = /(?:^|[\s=])(?:--[^\s=]+=)?["']?(\/[^\s'"|;&<>()`]+|~\/[^\s'"|;&<>()`]+|\$\{?HOME\}?\/[^\s'"|;&<>()`]+|\.\.(?:\/[^\s'"|;&<>()`]*)?)["']?/g;
102
64
  function expandHomeRelative(token) {
103
- // Expand ~/foo and $HOME/foo and ${HOME}/foo to absolute. The
104
- // workspace boundary check resolves non-absolute paths against the
105
- // workspace root, which would silently treat `~/secret` as
106
- // `<workspace>/~/secret` exactly the bypass the codex flagged.
107
- const home = homedir();
108
- if (token.startsWith('~/'))
109
- return `${home}/${token.slice(2)}`;
110
- if (token.startsWith('${HOME}/'))
111
- return `${home}/${token.slice(8)}`;
112
- if (token.startsWith('$HOME/'))
113
- return `${home}/${token.slice(6)}`;
114
- return token;
65
+ const home = homedir();
66
+ if (token.startsWith("~/")) return `${home}/${token.slice(2)}`;
67
+ if (token.startsWith("${HOME}/")) return `${home}/${token.slice(8)}`;
68
+ if (token.startsWith("$HOME/")) return `${home}/${token.slice(6)}`;
69
+ return token;
115
70
  }
116
71
  function extractCompileCheckPaths(input) {
117
- const command = typeof input.command === 'string' ? input.command : '';
118
- if (command === '')
119
- return [];
120
- const out = [];
121
- for (const match of command.matchAll(ABSOLUTE_PATH_TOKEN)) {
122
- out.push(expandHomeRelative(match[1]));
123
- }
124
- return out;
72
+ const command = typeof input.command === "string" ? input.command : "";
73
+ if (command === "") return [];
74
+ const out = [];
75
+ for (const match of command.matchAll(ABSOLUTE_PATH_TOKEN)) out.push(expandHomeRelative(match[1]));
76
+ return out;
125
77
  }
126
78
  const DEFAULT_EXTRACTORS = {
127
- [Constants.READ_FILE]: (i) => typeof i.file_path === 'string' ? [i.file_path] : [],
128
- [Constants.WRITE_FILE]: (i) => typeof i.file_path === 'string' ? [i.file_path] : [],
129
- [Constants.EDIT_FILE]: (i) => typeof i.file_path === 'string' ? [i.file_path] : [],
130
- [Constants.GREP_SEARCH]: (i) => typeof i.path === 'string' && i.path !== '' ? [i.path] : [],
131
- [Constants.GLOB_SEARCH]: (i) => typeof i.path === 'string' && i.path !== '' ? [i.path] : [],
132
- [Constants.LIST_DIRECTORY]: (i) => typeof i.path === 'string' && i.path !== '' ? [i.path] : [],
133
- [Constants.COMPILE_CHECK]: extractCompileCheckPaths,
79
+ ["read_file"]: (i) => typeof i.file_path === "string" ? [i.file_path] : [],
80
+ ["write_file"]: (i) => typeof i.file_path === "string" ? [i.file_path] : [],
81
+ ["edit_file"]: (i) => typeof i.file_path === "string" ? [i.file_path] : [],
82
+ ["grep_search"]: (i) => typeof i.path === "string" && i.path !== "" ? [i.path] : [],
83
+ ["glob_search"]: (i) => typeof i.path === "string" && i.path !== "" ? [i.path] : [],
84
+ ["list_directory"]: (i) => typeof i.path === "string" && i.path !== "" ? [i.path] : [],
85
+ ["compile_check"]: extractCompileCheckPaths
134
86
  };
135
87
  function isInsideAnyRoot(absolutePath, roots) {
136
- for (const root of roots) {
137
- if (absolutePath === root)
138
- return true;
139
- const rel = relative(root, absolutePath);
140
- if (!rel.startsWith('..') && !isAbsolute(rel))
141
- return true;
142
- }
143
- return false;
88
+ for (const root of roots) {
89
+ if (absolutePath === root) return true;
90
+ const rel = relative(root, absolutePath);
91
+ if (!rel.startsWith("..") && !isAbsolute(rel)) return true;
92
+ }
93
+ return false;
144
94
  }
145
95
  /**
146
- * Symlink-aware variant: realpaths the candidate AND the roots before
147
- * comparing. Without this, a symlink inside the workspace pointing
148
- * outside (e.g. `workspace/link → /etc/passwd`) compares as
149
- * "in-workspace" lexically, but actually grants the agent reach
150
- * outside the boundary. Critical when this hook is the primary gate
151
- * (i.e. the host opted into `workspace.allowReadOutside: true` /
152
- * `allowWriteOutside: true` so the file tools' own clamp is off).
153
- *
154
- * Handles paths that don't yet exist (e.g. `write_file` to a brand
155
- * new path) by walking up to the nearest existing ancestor and
156
- * realpathing that, then re-attaching the unresolved suffix. Mirrors
157
- * `resolveWorkspacePathSafe`'s approach in LocalExecutionEngine.
158
- */
96
+ * Symlink-aware variant: realpaths the candidate AND the roots before
97
+ * comparing. Without this, a symlink inside the workspace pointing
98
+ * outside (e.g. `workspace/link → /etc/passwd`) compares as
99
+ * "in-workspace" lexically, but actually grants the agent reach
100
+ * outside the boundary. Critical when this hook is the primary gate
101
+ * (i.e. the host opted into `workspace.allowReadOutside: true` /
102
+ * `allowWriteOutside: true` so the file tools' own clamp is off).
103
+ *
104
+ * Handles paths that don't yet exist (e.g. `write_file` to a brand
105
+ * new path) by walking up to the nearest existing ancestor and
106
+ * realpathing that, then re-attaching the unresolved suffix. Mirrors
107
+ * `resolveWorkspacePathSafe`'s approach in LocalExecutionEngine.
108
+ */
159
109
  async function realpathOrSelf(absolutePath) {
160
- try {
161
- return await realpath(absolutePath);
162
- }
163
- catch {
164
- return absolutePath;
165
- }
110
+ try {
111
+ return await realpath(absolutePath);
112
+ } catch {
113
+ return absolutePath;
114
+ }
166
115
  }
167
116
  async function realpathOfPathOrAncestor(absolutePath) {
168
- let current = absolutePath;
169
- let suffix = '';
170
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
171
- while (true) {
172
- try {
173
- const real = await realpath(current);
174
- return suffix === '' ? real : resolve(real, suffix);
175
- }
176
- catch {
177
- const parent = resolve(current, '..');
178
- if (parent === current) {
179
- return absolutePath;
180
- }
181
- const base = current.slice(parent.length + 1);
182
- suffix = suffix === '' ? base : `${base}/${suffix}`;
183
- current = parent;
184
- }
185
- }
117
+ let current = absolutePath;
118
+ let suffix = "";
119
+ while (true) try {
120
+ const real = await realpath(current);
121
+ return suffix === "" ? real : resolve(real, suffix);
122
+ } catch {
123
+ const parent = resolve(current, "..");
124
+ if (parent === current) return absolutePath;
125
+ const base = current.slice(parent.length + 1);
126
+ suffix = suffix === "" ? base : `${base}/${suffix}`;
127
+ current = parent;
128
+ }
186
129
  }
187
130
  async function isInsideAnyRootRealpath(absolutePath, realRoots) {
188
- const real = await realpathOfPathOrAncestor(absolutePath);
189
- return isInsideAnyRoot(real, [...realRoots]);
131
+ return isInsideAnyRoot(await realpathOfPathOrAncestor(absolutePath), [...realRoots]);
190
132
  }
191
133
  function formatReason(template, toolName, outsidePaths) {
192
- const fallback = `Tool "${toolName}" wants to touch ${outsidePaths.length} path(s) outside the workspace: ${outsidePaths.join(', ')}`;
193
- if (template == null)
194
- return fallback;
195
- return template
196
- .replace(/\{tool\}/g, toolName)
197
- .replace(/\{paths\}/g, outsidePaths.join(', '));
134
+ const fallback = `Tool "${toolName}" wants to touch ${outsidePaths.length} path(s) outside the workspace: ${outsidePaths.join(", ")}`;
135
+ if (template == null) return fallback;
136
+ return template.replace(/\{tool\}/g, toolName).replace(/\{paths\}/g, outsidePaths.join(", "));
198
137
  }
199
138
  /**
200
- * Build a `PreToolUse` callback that enforces the workspace policy.
201
- * Register it on a `HookRegistry`:
202
- *
203
- * ```ts
204
- * registry.register('PreToolUse', {
205
- * hooks: [createWorkspacePolicyHook({ root, outsideWrite: 'ask' })],
206
- * });
207
- * ```
208
- *
209
- * The hook is composable with `createToolPolicyHook` — register both;
210
- * `executeHooks` precedence (`deny > ask > allow`) sorts out which
211
- * decision wins per call.
212
- */
139
+ * Build a `PreToolUse` callback that enforces the workspace policy.
140
+ * Register it on a `HookRegistry`:
141
+ *
142
+ * ```ts
143
+ * registry.register('PreToolUse', {
144
+ * hooks: [createWorkspacePolicyHook({ root, outsideWrite: 'ask' })],
145
+ * });
146
+ * ```
147
+ *
148
+ * The hook is composable with `createToolPolicyHook` — register both;
149
+ * `executeHooks` precedence (`deny > ask > allow`) sorts out which
150
+ * decision wins per call.
151
+ */
213
152
  function createWorkspacePolicyHook(config) {
214
- const root = resolve(config.root);
215
- // Relative `additionalRoots` entries are anchored to `root` so a
216
- // monorepo config like `additionalRoots: ['../shared']` resolves
217
- // to a sibling of `root`, not of process.cwd. Matches
218
- // `getWorkspaceRoots` in LocalExecutionEngine.
219
- const additionalRoots = (config.additionalRoots ?? []).map((p) => isAbsolute(p) ? resolve(p) : resolve(root, p));
220
- const allRoots = [root, ...additionalRoots];
221
- // Pre-realpath the roots once at construction — these are stable
222
- // per Run. The candidate paths get realpath'd lazily inside the
223
- // hook callback. Cached so the per-call cost is just one realpath.
224
- let realRootsPromise;
225
- const getRealRoots = () => {
226
- if (realRootsPromise == null) {
227
- realRootsPromise = Promise.all(allRoots.map(realpathOrSelf));
228
- }
229
- return realRootsPromise;
230
- };
231
- const readPolicy = config.outsideRead ?? 'ask';
232
- const writePolicy = config.outsideWrite ?? 'ask';
233
- const extractors = {
234
- ...DEFAULT_EXTRACTORS,
235
- ...(config.pathExtractors ?? {}),
236
- };
237
- /** Unknown tools are treated as writes (the stricter policy). */
238
- const resolvePolicy = (toolName) => {
239
- if (WRITE_TOOLS.has(toolName)) {
240
- return writePolicy;
241
- }
242
- if (READ_TOOLS.has(toolName)) {
243
- return readPolicy;
244
- }
245
- return writePolicy;
246
- };
247
- return async (input) => {
248
- const extractor = extractors[input.toolName];
249
- if (extractor == null)
250
- return { decision: 'allow' };
251
- const paths = extractor(input.toolInput);
252
- if (paths.length === 0)
253
- return { decision: 'allow' };
254
- // Two-stage check:
255
- // 1. Lexical fast path — anything that's lexically inside the
256
- // workspace AND doesn't get redirected by realpath stays
257
- // allow-able without paying the realpath cost on every call.
258
- // 2. For paths that look outside lexically OR look inside but
259
- // may have been routed through a symlink, realpath both the
260
- // candidate and the roots and compare. This catches the
261
- // `workspace/link → /etc/passwd` escape that lexical-only
262
- // checks miss.
263
- const outside = [];
264
- const realRoots = await getRealRoots();
265
- for (const p of paths) {
266
- const abs = isAbsolute(p) ? resolve(p) : resolve(root, p);
267
- // Realpath is the source of truth — it catches both the
268
- // symlink-escape case (lexically-inside path that resolves
269
- // outside) and the alternate-mount case (lexically-outside
270
- // path that resolves back inside the workspace). The lexical
271
- // check alone gives the wrong answer for either, so we don't
272
- // bother computing it.
273
- const realInside = await isInsideAnyRootRealpath(abs, realRoots);
274
- if (!realInside) {
275
- outside.push(p);
276
- }
277
- }
278
- if (outside.length === 0)
279
- return { decision: 'allow' };
280
- const policy = resolvePolicy(input.toolName);
281
- if (policy === 'allow')
282
- return { decision: 'allow' };
283
- const decision = policy === 'deny' ? 'deny' : 'ask';
284
- return {
285
- decision,
286
- reason: formatReason(config.reason, input.toolName, outside),
287
- ...(decision === 'ask'
288
- ? { allowedDecisions: ['approve', 'reject'] }
289
- : {}),
290
- };
291
- };
153
+ const root = resolve(config.root);
154
+ const allRoots = [root, ...(config.additionalRoots ?? []).map((p) => isAbsolute(p) ? resolve(p) : resolve(root, p))];
155
+ let realRootsPromise;
156
+ const getRealRoots = () => {
157
+ if (realRootsPromise == null) realRootsPromise = Promise.all(allRoots.map(realpathOrSelf));
158
+ return realRootsPromise;
159
+ };
160
+ const readPolicy = config.outsideRead ?? "ask";
161
+ const writePolicy = config.outsideWrite ?? "ask";
162
+ const extractors = {
163
+ ...DEFAULT_EXTRACTORS,
164
+ ...config.pathExtractors ?? {}
165
+ };
166
+ /** Unknown tools are treated as writes (the stricter policy). */
167
+ const resolvePolicy = (toolName) => {
168
+ if (WRITE_TOOLS.has(toolName)) return writePolicy;
169
+ if (READ_TOOLS.has(toolName)) return readPolicy;
170
+ return writePolicy;
171
+ };
172
+ return async (input) => {
173
+ const extractor = extractors[input.toolName];
174
+ if (extractor == null) return { decision: "allow" };
175
+ const paths = extractor(input.toolInput);
176
+ if (paths.length === 0) return { decision: "allow" };
177
+ const outside = [];
178
+ const realRoots = await getRealRoots();
179
+ for (const p of paths) if (!await isInsideAnyRootRealpath(isAbsolute(p) ? resolve(p) : resolve(root, p), realRoots)) outside.push(p);
180
+ if (outside.length === 0) return { decision: "allow" };
181
+ const policy = resolvePolicy(input.toolName);
182
+ if (policy === "allow") return { decision: "allow" };
183
+ const decision = policy === "deny" ? "deny" : "ask";
184
+ return {
185
+ decision,
186
+ reason: formatReason(config.reason, input.toolName, outside),
187
+ ...decision === "ask" ? { allowedDecisions: ["approve", "reject"] } : {}
188
+ };
189
+ };
292
190
  }
293
-
191
+ //#endregion
294
192
  export { createWorkspacePolicyHook };
295
- //# sourceMappingURL=createWorkspacePolicyHook.mjs.map
193
+
194
+ //# sourceMappingURL=createWorkspacePolicyHook.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"createWorkspacePolicyHook.mjs","sources":["../../../src/hooks/createWorkspacePolicyHook.ts"],"sourcesContent":["/**\n * Workspace boundary policy as a `PreToolUse` hook.\n *\n * Local-engine file tools enforce a hard workspace boundary at the\n * tool implementation layer (`resolveWorkspacePathSafe`). This hook\n * adds a complementary, host-controlled layer on top that uses the\n * standard PreToolUse / HITL machinery to *negotiate* access to\n * paths outside the workspace — instead of just throwing.\n *\n * The host opts in by registering this hook on a `HookRegistry`; the\n * hook inspects each tool call's input, extracts the file paths it\n * mentions via per-tool extractors, and returns:\n *\n * - `allow` — every path is inside `workspace.root`\n * (or `additionalRoots`)\n * - `deny` — at least one path is outside, and the\n * configured outside-policy is `'deny'`\n * - `ask` — at least one path is outside, and the\n * outside-policy is `'ask'` (default).\n * When `humanInTheLoop.enabled` is true,\n * the existing PreToolUse `'ask'` flow\n * raises a tool_approval interrupt the\n * host UI can render. When HITL is off,\n * `'ask'` collapses to `deny` (matches\n * the rest of the SDK's default).\n *\n * Default per-tool path extractors cover the local-engine coding\n * suite (`read_file`, `write_file`, `edit_file`, `grep_search`,\n * `glob_search`, `list_directory`, `compile_check`). The host can\n * override or extend via `pathExtractors`. Bash/code paths are not\n * extracted by default — bash command parsing is its own concern, and\n * the existing `bashAst` validator + sandbox-runtime fs allowlist are\n * the right gates for those.\n *\n * Important: this hook does NOT replace `resolveWorkspacePathSafe`.\n * Even if the hook returns `allow`, the file tool still enforces its\n * own clamp unless `workspace.allowReadOutside` /\n * `workspace.allowWriteOutside` (or the legacy\n * `allowOutsideWorkspace`) is set. The recommended composition for\n * \"ask the user\" semantics is:\n *\n * workspace: {\n * root,\n * allowReadOutside: true,\n * allowWriteOutside: true,\n * },\n * // …with the hook installed and humanInTheLoop.enabled = true.\n */\n\nimport { homedir } from 'os';\nimport { realpath } from 'fs/promises';\nimport { isAbsolute, relative, resolve } from 'path';\nimport type {\n HookCallback,\n PreToolUseHookInput,\n PreToolUseHookOutput,\n ToolDecision,\n} from './types';\nimport { Constants } from '@/common';\n\n/**\n * What to do when a tool call references a path outside the workspace.\n *\n * - `'ask'` : default. Raise a PreToolUse `ask` (host UI prompts\n * via the HITL interrupt path).\n * - `'allow'` : let the call through (use the existing tool clamp\n * to actually enforce — the hook is purely advisory).\n * - `'deny'` : block the call with an error ToolMessage.\n */\nexport type OutsideAccessPolicy = 'ask' | 'allow' | 'deny';\n\nexport interface WorkspacePolicyConfig {\n /** Canonical workspace root. Required. */\n root: string;\n /** Sibling roots that count as inside-workspace. */\n additionalRoots?: readonly string[];\n /** Policy applied to read-only file tools. Defaults to `'ask'`. */\n outsideRead?: OutsideAccessPolicy;\n /** Policy applied to write-shaped file tools. Defaults to `'ask'`. */\n outsideWrite?: OutsideAccessPolicy;\n /**\n * Optional reason template surfaced in the `ask`/`deny` decision.\n * Supports `{tool}` and `{paths}` substitution.\n */\n reason?: string;\n /**\n * Per-tool path extractors. Defaults cover the local-engine coding\n * suite. Returning an empty array opts that tool out of policy.\n */\n pathExtractors?: Record<string, PathExtractor>;\n}\n\nexport type PathExtractor = (\n toolInput: Record<string, unknown>\n) => readonly string[];\n\nconst READ_TOOLS = new Set<string>([\n Constants.READ_FILE,\n Constants.GREP_SEARCH,\n Constants.GLOB_SEARCH,\n Constants.LIST_DIRECTORY,\n Constants.COMPILE_CHECK,\n]);\n\nconst WRITE_TOOLS = new Set<string>([\n Constants.WRITE_FILE,\n Constants.EDIT_FILE,\n]);\n\n/**\n * Best-effort extractor for `compile_check` — pulls absolute and `~/`\n * path tokens out of the `command` string so the workspace boundary\n * sees them. Without this, a model could ship `command: 'cat\n * /etc/passwd'` and the policy hook would short-circuit to `allow`\n * (Codex P1 #26 — the prior `() => []` made the hook a no-op for\n * compile_check). Conservative by design:\n *\n * - Matches `/foo`, `~/foo`, `$HOME/foo`, `${HOME}/foo` followed by\n * non-shell-special chars. Stops at whitespace, quotes, redirect\n * operators, pipes, semicolons.\n * - Strips a leading `--flag=` so `--out=/etc/foo` extracts as\n * `/etc/foo` (the path the agent's actually trying to write).\n * - Misses relative paths (intended — those resolve under cwd\n * anyway), and shell-substituted paths whose final form isn't\n * visible at extract time. Hosts that need bulletproof gating\n * should pair this with a `bash_tool`-level policy.\n */\n// `[\"']?` slots before AND after the captured path cover quoted\n// forms like `cat \"/etc/passwd\"` and `--out='/tmp/x'`. Codex P1 #31\n// — the previous regex only matched unquoted tokens, so a model\n// could trivially bypass the workspace policy by quoting any\n// destination path. The path content character class still excludes\n// quotes/whitespace/shell-specials so we don't over-extract; that's\n// the defensive trade we want for fallback-grep style matching.\n//\n// The `\\.\\.(?:\\/[^…]*)?` alternation covers parent-traversal forms\n// (`..`, `../secrets.txt`, `../foo/bar`). Without it, a model could\n// exfiltrate parent-directory files via `cat ../secrets` and the\n// hook would short-circuit to `allow` because the extractor saw no\n// \"absolute\" token. The boundary check at the call site resolves\n// non-absolute extracted tokens against `root`, so `../secrets`\n// becomes `<parent-of-workspace>/secrets` which the boundary then\n// correctly flags as outside. Codex P2 #35.\nconst PATH_TOKEN =\n /(?:^|[\\s=])(?:--[^\\s=]+=)?[\"']?(\\/[^\\s'\"|;&<>()`]+|~\\/[^\\s'\"|;&<>()`]+|\\$\\{?HOME\\}?\\/[^\\s'\"|;&<>()`]+|\\.\\.(?:\\/[^\\s'\"|;&<>()`]*)?)[\"']?/g;\n// Back-compat alias kept for any downstream import.\nconst ABSOLUTE_PATH_TOKEN = PATH_TOKEN;\nfunction expandHomeRelative(token: string): string {\n // Expand ~/foo and $HOME/foo and ${HOME}/foo to absolute. The\n // workspace boundary check resolves non-absolute paths against the\n // workspace root, which would silently treat `~/secret` as\n // `<workspace>/~/secret` — exactly the bypass the codex flagged.\n const home = homedir();\n if (token.startsWith('~/')) return `${home}/${token.slice(2)}`;\n if (token.startsWith('${HOME}/')) return `${home}/${token.slice(8)}`;\n if (token.startsWith('$HOME/')) return `${home}/${token.slice(6)}`;\n return token;\n}\nfunction extractCompileCheckPaths(input: Record<string, unknown>): string[] {\n const command = typeof input.command === 'string' ? input.command : '';\n if (command === '') return [];\n const out: string[] = [];\n for (const match of command.matchAll(ABSOLUTE_PATH_TOKEN)) {\n out.push(expandHomeRelative(match[1]));\n }\n return out;\n}\n\nconst DEFAULT_EXTRACTORS: Record<string, PathExtractor> = {\n [Constants.READ_FILE]: (i) =>\n typeof i.file_path === 'string' ? [i.file_path] : [],\n [Constants.WRITE_FILE]: (i) =>\n typeof i.file_path === 'string' ? [i.file_path] : [],\n [Constants.EDIT_FILE]: (i) =>\n typeof i.file_path === 'string' ? [i.file_path] : [],\n [Constants.GREP_SEARCH]: (i) =>\n typeof i.path === 'string' && i.path !== '' ? [i.path] : [],\n [Constants.GLOB_SEARCH]: (i) =>\n typeof i.path === 'string' && i.path !== '' ? [i.path] : [],\n [Constants.LIST_DIRECTORY]: (i) =>\n typeof i.path === 'string' && i.path !== '' ? [i.path] : [],\n [Constants.COMPILE_CHECK]: extractCompileCheckPaths,\n};\n\nfunction isInsideAnyRoot(absolutePath: string, roots: string[]): boolean {\n for (const root of roots) {\n if (absolutePath === root) return true;\n const rel = relative(root, absolutePath);\n if (!rel.startsWith('..') && !isAbsolute(rel)) return true;\n }\n return false;\n}\n\n/**\n * Symlink-aware variant: realpaths the candidate AND the roots before\n * comparing. Without this, a symlink inside the workspace pointing\n * outside (e.g. `workspace/link → /etc/passwd`) compares as\n * \"in-workspace\" lexically, but actually grants the agent reach\n * outside the boundary. Critical when this hook is the primary gate\n * (i.e. the host opted into `workspace.allowReadOutside: true` /\n * `allowWriteOutside: true` so the file tools' own clamp is off).\n *\n * Handles paths that don't yet exist (e.g. `write_file` to a brand\n * new path) by walking up to the nearest existing ancestor and\n * realpathing that, then re-attaching the unresolved suffix. Mirrors\n * `resolveWorkspacePathSafe`'s approach in LocalExecutionEngine.\n */\nasync function realpathOrSelf(absolutePath: string): Promise<string> {\n try {\n return await realpath(absolutePath);\n } catch {\n return absolutePath;\n }\n}\n\nasync function realpathOfPathOrAncestor(absolutePath: string): Promise<string> {\n let current = absolutePath;\n let suffix = '';\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n while (true) {\n try {\n const real = await realpath(current);\n return suffix === '' ? real : resolve(real, suffix);\n } catch {\n const parent = resolve(current, '..');\n if (parent === current) {\n return absolutePath;\n }\n const base = current.slice(parent.length + 1);\n suffix = suffix === '' ? base : `${base}/${suffix}`;\n current = parent;\n }\n }\n}\n\nasync function isInsideAnyRootRealpath(\n absolutePath: string,\n realRoots: readonly string[]\n): Promise<boolean> {\n const real = await realpathOfPathOrAncestor(absolutePath);\n return isInsideAnyRoot(real, [...realRoots]);\n}\n\nfunction formatReason(\n template: string | undefined,\n toolName: string,\n outsidePaths: readonly string[]\n): string {\n const fallback = `Tool \"${toolName}\" wants to touch ${outsidePaths.length} path(s) outside the workspace: ${outsidePaths.join(', ')}`;\n if (template == null) return fallback;\n return template\n .replace(/\\{tool\\}/g, toolName)\n .replace(/\\{paths\\}/g, outsidePaths.join(', '));\n}\n\n/**\n * Build a `PreToolUse` callback that enforces the workspace policy.\n * Register it on a `HookRegistry`:\n *\n * ```ts\n * registry.register('PreToolUse', {\n * hooks: [createWorkspacePolicyHook({ root, outsideWrite: 'ask' })],\n * });\n * ```\n *\n * The hook is composable with `createToolPolicyHook` — register both;\n * `executeHooks` precedence (`deny > ask > allow`) sorts out which\n * decision wins per call.\n */\nexport function createWorkspacePolicyHook(\n config: WorkspacePolicyConfig\n): HookCallback<'PreToolUse'> {\n const root = resolve(config.root);\n // Relative `additionalRoots` entries are anchored to `root` so a\n // monorepo config like `additionalRoots: ['../shared']` resolves\n // to a sibling of `root`, not of process.cwd. Matches\n // `getWorkspaceRoots` in LocalExecutionEngine.\n const additionalRoots = (config.additionalRoots ?? []).map((p) =>\n isAbsolute(p) ? resolve(p) : resolve(root, p)\n );\n const allRoots = [root, ...additionalRoots];\n\n // Pre-realpath the roots once at construction — these are stable\n // per Run. The candidate paths get realpath'd lazily inside the\n // hook callback. Cached so the per-call cost is just one realpath.\n let realRootsPromise: Promise<string[]> | undefined;\n const getRealRoots = (): Promise<string[]> => {\n if (realRootsPromise == null) {\n realRootsPromise = Promise.all(allRoots.map(realpathOrSelf));\n }\n return realRootsPromise;\n };\n\n const readPolicy: OutsideAccessPolicy = config.outsideRead ?? 'ask';\n const writePolicy: OutsideAccessPolicy = config.outsideWrite ?? 'ask';\n\n const extractors: Record<string, PathExtractor | undefined> = {\n ...DEFAULT_EXTRACTORS,\n ...(config.pathExtractors ?? {}),\n };\n\n /** Unknown tools are treated as writes (the stricter policy). */\n const resolvePolicy = (toolName: string): OutsideAccessPolicy => {\n if (WRITE_TOOLS.has(toolName)) {\n return writePolicy;\n }\n if (READ_TOOLS.has(toolName)) {\n return readPolicy;\n }\n return writePolicy;\n };\n\n return async (input: PreToolUseHookInput): Promise<PreToolUseHookOutput> => {\n const extractor = extractors[input.toolName];\n if (extractor == null) return { decision: 'allow' };\n\n const paths = extractor(input.toolInput);\n if (paths.length === 0) return { decision: 'allow' };\n\n // Two-stage check:\n // 1. Lexical fast path — anything that's lexically inside the\n // workspace AND doesn't get redirected by realpath stays\n // allow-able without paying the realpath cost on every call.\n // 2. For paths that look outside lexically OR look inside but\n // may have been routed through a symlink, realpath both the\n // candidate and the roots and compare. This catches the\n // `workspace/link → /etc/passwd` escape that lexical-only\n // checks miss.\n const outside: string[] = [];\n const realRoots = await getRealRoots();\n for (const p of paths) {\n const abs = isAbsolute(p) ? resolve(p) : resolve(root, p);\n // Realpath is the source of truth — it catches both the\n // symlink-escape case (lexically-inside path that resolves\n // outside) and the alternate-mount case (lexically-outside\n // path that resolves back inside the workspace). The lexical\n // check alone gives the wrong answer for either, so we don't\n // bother computing it.\n const realInside = await isInsideAnyRootRealpath(abs, realRoots);\n if (!realInside) {\n outside.push(p);\n }\n }\n if (outside.length === 0) return { decision: 'allow' };\n\n const policy = resolvePolicy(input.toolName);\n if (policy === 'allow') return { decision: 'allow' };\n\n const decision: ToolDecision = policy === 'deny' ? 'deny' : 'ask';\n return {\n decision,\n reason: formatReason(config.reason, input.toolName, outside),\n ...(decision === 'ask'\n ? { allowedDecisions: ['approve', 'reject'] as const }\n : {}),\n };\n };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"createWorkspacePolicyHook.mjs","names":[],"sources":["../../../src/hooks/createWorkspacePolicyHook.ts"],"sourcesContent":["/**\n * Workspace boundary policy as a `PreToolUse` hook.\n *\n * Local-engine file tools enforce a hard workspace boundary at the\n * tool implementation layer (`resolveWorkspacePathSafe`). This hook\n * adds a complementary, host-controlled layer on top that uses the\n * standard PreToolUse / HITL machinery to *negotiate* access to\n * paths outside the workspace — instead of just throwing.\n *\n * The host opts in by registering this hook on a `HookRegistry`; the\n * hook inspects each tool call's input, extracts the file paths it\n * mentions via per-tool extractors, and returns:\n *\n * - `allow` — every path is inside `workspace.root`\n * (or `additionalRoots`)\n * - `deny` — at least one path is outside, and the\n * configured outside-policy is `'deny'`\n * - `ask` — at least one path is outside, and the\n * outside-policy is `'ask'` (default).\n * When `humanInTheLoop.enabled` is true,\n * the existing PreToolUse `'ask'` flow\n * raises a tool_approval interrupt the\n * host UI can render. When HITL is off,\n * `'ask'` collapses to `deny` (matches\n * the rest of the SDK's default).\n *\n * Default per-tool path extractors cover the local-engine coding\n * suite (`read_file`, `write_file`, `edit_file`, `grep_search`,\n * `glob_search`, `list_directory`, `compile_check`). The host can\n * override or extend via `pathExtractors`. Bash/code paths are not\n * extracted by default — bash command parsing is its own concern, and\n * the existing `bashAst` validator + sandbox-runtime fs allowlist are\n * the right gates for those.\n *\n * Important: this hook does NOT replace `resolveWorkspacePathSafe`.\n * Even if the hook returns `allow`, the file tool still enforces its\n * own clamp unless `workspace.allowReadOutside` /\n * `workspace.allowWriteOutside` (or the legacy\n * `allowOutsideWorkspace`) is set. The recommended composition for\n * \"ask the user\" semantics is:\n *\n * workspace: {\n * root,\n * allowReadOutside: true,\n * allowWriteOutside: true,\n * },\n * // …with the hook installed and humanInTheLoop.enabled = true.\n */\n\nimport { homedir } from 'os';\nimport { realpath } from 'fs/promises';\nimport { isAbsolute, relative, resolve } from 'path';\nimport type {\n HookCallback,\n PreToolUseHookInput,\n PreToolUseHookOutput,\n ToolDecision,\n} from './types';\nimport { Constants } from '@/common';\n\n/**\n * What to do when a tool call references a path outside the workspace.\n *\n * - `'ask'` : default. Raise a PreToolUse `ask` (host UI prompts\n * via the HITL interrupt path).\n * - `'allow'` : let the call through (use the existing tool clamp\n * to actually enforce — the hook is purely advisory).\n * - `'deny'` : block the call with an error ToolMessage.\n */\nexport type OutsideAccessPolicy = 'ask' | 'allow' | 'deny';\n\nexport interface WorkspacePolicyConfig {\n /** Canonical workspace root. Required. */\n root: string;\n /** Sibling roots that count as inside-workspace. */\n additionalRoots?: readonly string[];\n /** Policy applied to read-only file tools. Defaults to `'ask'`. */\n outsideRead?: OutsideAccessPolicy;\n /** Policy applied to write-shaped file tools. Defaults to `'ask'`. */\n outsideWrite?: OutsideAccessPolicy;\n /**\n * Optional reason template surfaced in the `ask`/`deny` decision.\n * Supports `{tool}` and `{paths}` substitution.\n */\n reason?: string;\n /**\n * Per-tool path extractors. Defaults cover the local-engine coding\n * suite. Returning an empty array opts that tool out of policy.\n */\n pathExtractors?: Record<string, PathExtractor>;\n}\n\nexport type PathExtractor = (\n toolInput: Record<string, unknown>\n) => readonly string[];\n\nconst READ_TOOLS = new Set<string>([\n Constants.READ_FILE,\n Constants.GREP_SEARCH,\n Constants.GLOB_SEARCH,\n Constants.LIST_DIRECTORY,\n Constants.COMPILE_CHECK,\n]);\n\nconst WRITE_TOOLS = new Set<string>([\n Constants.WRITE_FILE,\n Constants.EDIT_FILE,\n]);\n\n/**\n * Best-effort extractor for `compile_check` — pulls absolute and `~/`\n * path tokens out of the `command` string so the workspace boundary\n * sees them. Without this, a model could ship `command: 'cat\n * /etc/passwd'` and the policy hook would short-circuit to `allow`\n * (Codex P1 #26 — the prior `() => []` made the hook a no-op for\n * compile_check). Conservative by design:\n *\n * - Matches `/foo`, `~/foo`, `$HOME/foo`, `${HOME}/foo` followed by\n * non-shell-special chars. Stops at whitespace, quotes, redirect\n * operators, pipes, semicolons.\n * - Strips a leading `--flag=` so `--out=/etc/foo` extracts as\n * `/etc/foo` (the path the agent's actually trying to write).\n * - Misses relative paths (intended — those resolve under cwd\n * anyway), and shell-substituted paths whose final form isn't\n * visible at extract time. Hosts that need bulletproof gating\n * should pair this with a `bash_tool`-level policy.\n */\n// `[\"']?` slots before AND after the captured path cover quoted\n// forms like `cat \"/etc/passwd\"` and `--out='/tmp/x'`. Codex P1 #31\n// — the previous regex only matched unquoted tokens, so a model\n// could trivially bypass the workspace policy by quoting any\n// destination path. The path content character class still excludes\n// quotes/whitespace/shell-specials so we don't over-extract; that's\n// the defensive trade we want for fallback-grep style matching.\n//\n// The `\\.\\.(?:\\/[^…]*)?` alternation covers parent-traversal forms\n// (`..`, `../secrets.txt`, `../foo/bar`). Without it, a model could\n// exfiltrate parent-directory files via `cat ../secrets` and the\n// hook would short-circuit to `allow` because the extractor saw no\n// \"absolute\" token. The boundary check at the call site resolves\n// non-absolute extracted tokens against `root`, so `../secrets`\n// becomes `<parent-of-workspace>/secrets` which the boundary then\n// correctly flags as outside. Codex P2 #35.\nconst PATH_TOKEN =\n /(?:^|[\\s=])(?:--[^\\s=]+=)?[\"']?(\\/[^\\s'\"|;&<>()`]+|~\\/[^\\s'\"|;&<>()`]+|\\$\\{?HOME\\}?\\/[^\\s'\"|;&<>()`]+|\\.\\.(?:\\/[^\\s'\"|;&<>()`]*)?)[\"']?/g;\n// Back-compat alias kept for any downstream import.\nconst ABSOLUTE_PATH_TOKEN = PATH_TOKEN;\nfunction expandHomeRelative(token: string): string {\n // Expand ~/foo and $HOME/foo and ${HOME}/foo to absolute. The\n // workspace boundary check resolves non-absolute paths against the\n // workspace root, which would silently treat `~/secret` as\n // `<workspace>/~/secret` — exactly the bypass the codex flagged.\n const home = homedir();\n if (token.startsWith('~/')) return `${home}/${token.slice(2)}`;\n if (token.startsWith('${HOME}/')) return `${home}/${token.slice(8)}`;\n if (token.startsWith('$HOME/')) return `${home}/${token.slice(6)}`;\n return token;\n}\nfunction extractCompileCheckPaths(input: Record<string, unknown>): string[] {\n const command = typeof input.command === 'string' ? input.command : '';\n if (command === '') return [];\n const out: string[] = [];\n for (const match of command.matchAll(ABSOLUTE_PATH_TOKEN)) {\n out.push(expandHomeRelative(match[1]));\n }\n return out;\n}\n\nconst DEFAULT_EXTRACTORS: Record<string, PathExtractor> = {\n [Constants.READ_FILE]: (i) =>\n typeof i.file_path === 'string' ? [i.file_path] : [],\n [Constants.WRITE_FILE]: (i) =>\n typeof i.file_path === 'string' ? [i.file_path] : [],\n [Constants.EDIT_FILE]: (i) =>\n typeof i.file_path === 'string' ? [i.file_path] : [],\n [Constants.GREP_SEARCH]: (i) =>\n typeof i.path === 'string' && i.path !== '' ? [i.path] : [],\n [Constants.GLOB_SEARCH]: (i) =>\n typeof i.path === 'string' && i.path !== '' ? [i.path] : [],\n [Constants.LIST_DIRECTORY]: (i) =>\n typeof i.path === 'string' && i.path !== '' ? [i.path] : [],\n [Constants.COMPILE_CHECK]: extractCompileCheckPaths,\n};\n\nfunction isInsideAnyRoot(absolutePath: string, roots: string[]): boolean {\n for (const root of roots) {\n if (absolutePath === root) return true;\n const rel = relative(root, absolutePath);\n if (!rel.startsWith('..') && !isAbsolute(rel)) return true;\n }\n return false;\n}\n\n/**\n * Symlink-aware variant: realpaths the candidate AND the roots before\n * comparing. Without this, a symlink inside the workspace pointing\n * outside (e.g. `workspace/link → /etc/passwd`) compares as\n * \"in-workspace\" lexically, but actually grants the agent reach\n * outside the boundary. Critical when this hook is the primary gate\n * (i.e. the host opted into `workspace.allowReadOutside: true` /\n * `allowWriteOutside: true` so the file tools' own clamp is off).\n *\n * Handles paths that don't yet exist (e.g. `write_file` to a brand\n * new path) by walking up to the nearest existing ancestor and\n * realpathing that, then re-attaching the unresolved suffix. Mirrors\n * `resolveWorkspacePathSafe`'s approach in LocalExecutionEngine.\n */\nasync function realpathOrSelf(absolutePath: string): Promise<string> {\n try {\n return await realpath(absolutePath);\n } catch {\n return absolutePath;\n }\n}\n\nasync function realpathOfPathOrAncestor(absolutePath: string): Promise<string> {\n let current = absolutePath;\n let suffix = '';\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n while (true) {\n try {\n const real = await realpath(current);\n return suffix === '' ? real : resolve(real, suffix);\n } catch {\n const parent = resolve(current, '..');\n if (parent === current) {\n return absolutePath;\n }\n const base = current.slice(parent.length + 1);\n suffix = suffix === '' ? base : `${base}/${suffix}`;\n current = parent;\n }\n }\n}\n\nasync function isInsideAnyRootRealpath(\n absolutePath: string,\n realRoots: readonly string[]\n): Promise<boolean> {\n const real = await realpathOfPathOrAncestor(absolutePath);\n return isInsideAnyRoot(real, [...realRoots]);\n}\n\nfunction formatReason(\n template: string | undefined,\n toolName: string,\n outsidePaths: readonly string[]\n): string {\n const fallback = `Tool \"${toolName}\" wants to touch ${outsidePaths.length} path(s) outside the workspace: ${outsidePaths.join(', ')}`;\n if (template == null) return fallback;\n return template\n .replace(/\\{tool\\}/g, toolName)\n .replace(/\\{paths\\}/g, outsidePaths.join(', '));\n}\n\n/**\n * Build a `PreToolUse` callback that enforces the workspace policy.\n * Register it on a `HookRegistry`:\n *\n * ```ts\n * registry.register('PreToolUse', {\n * hooks: [createWorkspacePolicyHook({ root, outsideWrite: 'ask' })],\n * });\n * ```\n *\n * The hook is composable with `createToolPolicyHook` — register both;\n * `executeHooks` precedence (`deny > ask > allow`) sorts out which\n * decision wins per call.\n */\nexport function createWorkspacePolicyHook(\n config: WorkspacePolicyConfig\n): HookCallback<'PreToolUse'> {\n const root = resolve(config.root);\n // Relative `additionalRoots` entries are anchored to `root` so a\n // monorepo config like `additionalRoots: ['../shared']` resolves\n // to a sibling of `root`, not of process.cwd. Matches\n // `getWorkspaceRoots` in LocalExecutionEngine.\n const additionalRoots = (config.additionalRoots ?? []).map((p) =>\n isAbsolute(p) ? resolve(p) : resolve(root, p)\n );\n const allRoots = [root, ...additionalRoots];\n\n // Pre-realpath the roots once at construction — these are stable\n // per Run. The candidate paths get realpath'd lazily inside the\n // hook callback. Cached so the per-call cost is just one realpath.\n let realRootsPromise: Promise<string[]> | undefined;\n const getRealRoots = (): Promise<string[]> => {\n if (realRootsPromise == null) {\n realRootsPromise = Promise.all(allRoots.map(realpathOrSelf));\n }\n return realRootsPromise;\n };\n\n const readPolicy: OutsideAccessPolicy = config.outsideRead ?? 'ask';\n const writePolicy: OutsideAccessPolicy = config.outsideWrite ?? 'ask';\n\n const extractors: Record<string, PathExtractor | undefined> = {\n ...DEFAULT_EXTRACTORS,\n ...(config.pathExtractors ?? {}),\n };\n\n /** Unknown tools are treated as writes (the stricter policy). */\n const resolvePolicy = (toolName: string): OutsideAccessPolicy => {\n if (WRITE_TOOLS.has(toolName)) {\n return writePolicy;\n }\n if (READ_TOOLS.has(toolName)) {\n return readPolicy;\n }\n return writePolicy;\n };\n\n return async (input: PreToolUseHookInput): Promise<PreToolUseHookOutput> => {\n const extractor = extractors[input.toolName];\n if (extractor == null) return { decision: 'allow' };\n\n const paths = extractor(input.toolInput);\n if (paths.length === 0) return { decision: 'allow' };\n\n // Two-stage check:\n // 1. Lexical fast path — anything that's lexically inside the\n // workspace AND doesn't get redirected by realpath stays\n // allow-able without paying the realpath cost on every call.\n // 2. For paths that look outside lexically OR look inside but\n // may have been routed through a symlink, realpath both the\n // candidate and the roots and compare. This catches the\n // `workspace/link → /etc/passwd` escape that lexical-only\n // checks miss.\n const outside: string[] = [];\n const realRoots = await getRealRoots();\n for (const p of paths) {\n const abs = isAbsolute(p) ? resolve(p) : resolve(root, p);\n // Realpath is the source of truth — it catches both the\n // symlink-escape case (lexically-inside path that resolves\n // outside) and the alternate-mount case (lexically-outside\n // path that resolves back inside the workspace). The lexical\n // check alone gives the wrong answer for either, so we don't\n // bother computing it.\n const realInside = await isInsideAnyRootRealpath(abs, realRoots);\n if (!realInside) {\n outside.push(p);\n }\n }\n if (outside.length === 0) return { decision: 'allow' };\n\n const policy = resolvePolicy(input.toolName);\n if (policy === 'allow') return { decision: 'allow' };\n\n const decision: ToolDecision = policy === 'deny' ? 'deny' : 'ask';\n return {\n decision,\n reason: formatReason(config.reason, input.toolName, outside),\n ...(decision === 'ask'\n ? { allowedDecisions: ['approve', 'reject'] as const }\n : {}),\n };\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgGA,MAAM,aAAa,IAAI,IAAY;;;;;;AAMnC,CAAC;AAED,MAAM,cAAc,IAAI,IAAY,CAAA,cAAA,WAGpC,CAAC;AAuCD,MAAM,sBAAsB;AAC5B,SAAS,mBAAmB,OAAuB;CAKjD,MAAM,OAAO,QAAQ;CACrB,IAAI,MAAM,WAAW,IAAI,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,MAAM,CAAC;CAC3D,IAAI,MAAM,WAAW,UAAU,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,MAAM,CAAC;CACjE,IAAI,MAAM,WAAW,QAAQ,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,MAAM,CAAC;CAC/D,OAAO;AACT;AACA,SAAS,yBAAyB,OAA0C;CAC1E,MAAM,UAAU,OAAO,MAAM,YAAY,WAAW,MAAM,UAAU;CACpE,IAAI,YAAY,IAAI,OAAO,CAAC;CAC5B,MAAM,MAAgB,CAAC;CACvB,KAAK,MAAM,SAAS,QAAQ,SAAS,mBAAmB,GACtD,IAAI,KAAK,mBAAmB,MAAM,EAAE,CAAC;CAEvC,OAAO;AACT;AAEA,MAAM,qBAAoD;iBAChC,MACtB,OAAO,EAAE,cAAc,WAAW,CAAC,EAAE,SAAS,IAAI,CAAC;kBAC5B,MACvB,OAAO,EAAE,cAAc,WAAW,CAAC,EAAE,SAAS,IAAI,CAAC;iBAC7B,MACtB,OAAO,EAAE,cAAc,WAAW,CAAC,EAAE,SAAS,IAAI,CAAC;mBAC3B,MACxB,OAAO,EAAE,SAAS,YAAY,EAAE,SAAS,KAAK,CAAC,EAAE,IAAI,IAAI,CAAC;mBAClC,MACxB,OAAO,EAAE,SAAS,YAAY,EAAE,SAAS,KAAK,CAAC,EAAE,IAAI,IAAI,CAAC;sBAC/B,MAC3B,OAAO,EAAE,SAAS,YAAY,EAAE,SAAS,KAAK,CAAC,EAAE,IAAI,IAAI,CAAC;oBACjC;AAC7B;AAEA,SAAS,gBAAgB,cAAsB,OAA0B;CACvE,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,iBAAiB,MAAM,OAAO;EAClC,MAAM,MAAM,SAAS,MAAM,YAAY;EACvC,IAAI,CAAC,IAAI,WAAW,IAAI,KAAK,CAAC,WAAW,GAAG,GAAG,OAAO;CACxD;CACA,OAAO;AACT;;;;;;;;;;;;;;;AAgBA,eAAe,eAAe,cAAuC;CACnE,IAAI;EACF,OAAO,MAAM,SAAS,YAAY;CACpC,QAAQ;EACN,OAAO;CACT;AACF;AAEA,eAAe,yBAAyB,cAAuC;CAC7E,IAAI,UAAU;CACd,IAAI,SAAS;CAEb,OAAO,MACL,IAAI;EACF,MAAM,OAAO,MAAM,SAAS,OAAO;EACnC,OAAO,WAAW,KAAK,OAAO,QAAQ,MAAM,MAAM;CACpD,QAAQ;EACN,MAAM,SAAS,QAAQ,SAAS,IAAI;EACpC,IAAI,WAAW,SACb,OAAO;EAET,MAAM,OAAO,QAAQ,MAAM,OAAO,SAAS,CAAC;EAC5C,SAAS,WAAW,KAAK,OAAO,GAAG,KAAK,GAAG;EAC3C,UAAU;CACZ;AAEJ;AAEA,eAAe,wBACb,cACA,WACkB;CAElB,OAAO,gBAAgB,MADJ,yBAAyB,YAAY,GAC3B,CAAC,GAAG,SAAS,CAAC;AAC7C;AAEA,SAAS,aACP,UACA,UACA,cACQ;CACR,MAAM,WAAW,SAAS,SAAS,mBAAmB,aAAa,OAAO,kCAAkC,aAAa,KAAK,IAAI;CAClI,IAAI,YAAY,MAAM,OAAO;CAC7B,OAAO,SACJ,QAAQ,aAAa,QAAQ,CAAC,CAC9B,QAAQ,cAAc,aAAa,KAAK,IAAI,CAAC;AAClD;;;;;;;;;;;;;;;AAgBA,SAAgB,0BACd,QAC4B;CAC5B,MAAM,OAAO,QAAQ,OAAO,IAAI;CAQhC,MAAM,WAAW,CAAC,MAAM,IAHC,OAAO,mBAAmB,CAAC,EAAA,CAAG,KAAK,MAC1D,WAAW,CAAC,IAAI,QAAQ,CAAC,IAAI,QAAQ,MAAM,CAAC,CAEL,CAAC;CAK1C,IAAI;CACJ,MAAM,qBAAwC;EAC5C,IAAI,oBAAoB,MACtB,mBAAmB,QAAQ,IAAI,SAAS,IAAI,cAAc,CAAC;EAE7D,OAAO;CACT;CAEA,MAAM,aAAkC,OAAO,eAAe;CAC9D,MAAM,cAAmC,OAAO,gBAAgB;CAEhE,MAAM,aAAwD;EAC5D,GAAG;EACH,GAAI,OAAO,kBAAkB,CAAC;CAChC;;CAGA,MAAM,iBAAiB,aAA0C;EAC/D,IAAI,YAAY,IAAI,QAAQ,GAC1B,OAAO;EAET,IAAI,WAAW,IAAI,QAAQ,GACzB,OAAO;EAET,OAAO;CACT;CAEA,OAAO,OAAO,UAA8D;EAC1E,MAAM,YAAY,WAAW,MAAM;EACnC,IAAI,aAAa,MAAM,OAAO,EAAE,UAAU,QAAQ;EAElD,MAAM,QAAQ,UAAU,MAAM,SAAS;EACvC,IAAI,MAAM,WAAW,GAAG,OAAO,EAAE,UAAU,QAAQ;EAWnD,MAAM,UAAoB,CAAC;EAC3B,MAAM,YAAY,MAAM,aAAa;EACrC,KAAK,MAAM,KAAK,OASd,IAAI,CAAC,MADoB,wBAPb,WAAW,CAAC,IAAI,QAAQ,CAAC,IAAI,QAAQ,MAAM,CAAC,GAOF,SAAS,GAE7D,QAAQ,KAAK,CAAC;EAGlB,IAAI,QAAQ,WAAW,GAAG,OAAO,EAAE,UAAU,QAAQ;EAErD,MAAM,SAAS,cAAc,MAAM,QAAQ;EAC3C,IAAI,WAAW,SAAS,OAAO,EAAE,UAAU,QAAQ;EAEnD,MAAM,WAAyB,WAAW,SAAS,SAAS;EAC5D,OAAO;GACL;GACA,QAAQ,aAAa,OAAO,QAAQ,MAAM,UAAU,OAAO;GAC3D,GAAI,aAAa,QACb,EAAE,kBAAkB,CAAC,WAAW,QAAQ,EAAW,IACnD,CAAC;EACP;CACF;AACF"}