@librechat/agents 3.4.7 → 3.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (924) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +0 -356
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/agents/projection.cjs +0 -9
  4. package/dist/cjs/agents/projection.cjs.map +1 -1
  5. package/dist/cjs/common/constants.cjs +0 -26
  6. package/dist/cjs/common/constants.cjs.map +1 -1
  7. package/dist/cjs/common/enum.cjs +0 -85
  8. package/dist/cjs/common/enum.cjs.map +1 -1
  9. package/dist/cjs/events.cjs +0 -14
  10. package/dist/cjs/events.cjs.map +1 -1
  11. package/dist/cjs/graphs/Graph.cjs +1 -909
  12. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  13. package/dist/cjs/graphs/MultiAgentGraph.cjs +7 -272
  14. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  15. package/dist/cjs/graphs/applyGraphRuntimeConfig.cjs.map +1 -1
  16. package/dist/cjs/graphs/createGraph.cjs.map +1 -1
  17. package/dist/cjs/hitl/askUserQuestion.cjs +0 -53
  18. package/dist/cjs/hitl/askUserQuestion.cjs.map +1 -1
  19. package/dist/cjs/hitl/askUserQuestions.cjs +0 -20
  20. package/dist/cjs/hitl/askUserQuestions.cjs.map +1 -1
  21. package/dist/cjs/hitl/askUserQuestionsInterrupt.cjs +0 -6
  22. package/dist/cjs/hitl/askUserQuestionsInterrupt.cjs.map +1 -1
  23. package/dist/cjs/hooks/HookRegistry.cjs +0 -127
  24. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  25. package/dist/cjs/hooks/createToolPolicyHook.cjs +0 -38
  26. package/dist/cjs/hooks/createToolPolicyHook.cjs.map +1 -1
  27. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +0 -77
  28. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs.map +1 -1
  29. package/dist/cjs/hooks/executeHooks.cjs +0 -79
  30. package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
  31. package/dist/cjs/hooks/index.cjs +0 -16
  32. package/dist/cjs/hooks/index.cjs.map +1 -1
  33. package/dist/cjs/hooks/matchers.cjs +0 -104
  34. package/dist/cjs/hooks/matchers.cjs.map +1 -1
  35. package/dist/cjs/hooks/types.cjs +0 -8
  36. package/dist/cjs/hooks/types.cjs.map +1 -1
  37. package/dist/cjs/instrumentation.cjs +0 -14
  38. package/dist/cjs/instrumentation.cjs.map +1 -1
  39. package/dist/cjs/langfuse.cjs +0 -75
  40. package/dist/cjs/langfuse.cjs.map +1 -1
  41. package/dist/cjs/langfuseConfig.cjs.map +1 -1
  42. package/dist/cjs/langfuseRuntimeContext.cjs +0 -13
  43. package/dist/cjs/langfuseRuntimeContext.cjs.map +1 -1
  44. package/dist/cjs/langfuseRuntimeScope.cjs +0 -4
  45. package/dist/cjs/langfuseRuntimeScope.cjs.map +1 -1
  46. package/dist/cjs/langfuseSpanRegistry.cjs +0 -28
  47. package/dist/cjs/langfuseSpanRegistry.cjs.map +1 -1
  48. package/dist/cjs/langfuseToolOutputTracing.cjs +0 -3
  49. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
  50. package/dist/cjs/langfuseTraceShaping.cjs +0 -74
  51. package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
  52. package/dist/cjs/llm/anthropic/index.cjs +0 -3
  53. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  54. package/dist/cjs/llm/anthropic/types.cjs.map +1 -1
  55. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +1 -111
  56. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  57. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +0 -1
  58. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
  59. package/dist/cjs/llm/anthropic/utils/stream_events.cjs +0 -8
  60. package/dist/cjs/llm/anthropic/utils/stream_events.cjs.map +1 -1
  61. package/dist/cjs/llm/anthropic/utils/tools.cjs.map +1 -1
  62. package/dist/cjs/llm/bedrock/cachePoints.cjs.map +1 -1
  63. package/dist/cjs/llm/bedrock/index.cjs +1 -95
  64. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  65. package/dist/cjs/llm/bedrock/toolCache.cjs.map +1 -1
  66. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +0 -78
  67. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  68. package/dist/cjs/llm/bedrock/utils/message_outputs.cjs +0 -24
  69. package/dist/cjs/llm/bedrock/utils/message_outputs.cjs.map +1 -1
  70. package/dist/cjs/llm/contextOverflowRecovery.cjs +0 -62
  71. package/dist/cjs/llm/contextOverflowRecovery.cjs.map +1 -1
  72. package/dist/cjs/llm/fake.cjs.map +1 -1
  73. package/dist/cjs/llm/google/index.cjs +2 -21
  74. package/dist/cjs/llm/google/index.cjs.map +1 -1
  75. package/dist/cjs/llm/google/utils/common.cjs +0 -35
  76. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  77. package/dist/cjs/llm/init.cjs +0 -9
  78. package/dist/cjs/llm/init.cjs.map +1 -1
  79. package/dist/cjs/llm/invoke.cjs +1 -270
  80. package/dist/cjs/llm/invoke.cjs.map +1 -1
  81. package/dist/cjs/llm/mistral/index.cjs.map +1 -1
  82. package/dist/cjs/llm/openai/index.cjs +0 -99
  83. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  84. package/dist/cjs/llm/openai/streamMetadata.cjs +0 -21
  85. package/dist/cjs/llm/openai/streamMetadata.cjs.map +1 -1
  86. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  87. package/dist/cjs/llm/openrouter/index.cjs +0 -1
  88. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  89. package/dist/cjs/llm/openrouter/toolCache.cjs +0 -6
  90. package/dist/cjs/llm/openrouter/toolCache.cjs.map +1 -1
  91. package/dist/cjs/llm/preempt.cjs +0 -75
  92. package/dist/cjs/llm/preempt.cjs.map +1 -1
  93. package/dist/cjs/llm/providers.cjs.map +1 -1
  94. package/dist/cjs/llm/request.cjs +0 -10
  95. package/dist/cjs/llm/request.cjs.map +1 -1
  96. package/dist/cjs/llm/stream/chunkAdapters.cjs +0 -62
  97. package/dist/cjs/llm/stream/chunkAdapters.cjs.map +1 -1
  98. package/dist/cjs/llm/stream/smoother.cjs +0 -58
  99. package/dist/cjs/llm/stream/smoother.cjs.map +1 -1
  100. package/dist/cjs/llm/streamLimits.cjs +4 -293
  101. package/dist/cjs/llm/streamLimits.cjs.map +1 -1
  102. package/dist/cjs/llm/truncation.cjs +0 -36
  103. package/dist/cjs/llm/truncation.cjs.map +1 -1
  104. package/dist/cjs/llm/vertexai/index.cjs +0 -330
  105. package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
  106. package/dist/cjs/messages/alternation.cjs +0 -43
  107. package/dist/cjs/messages/alternation.cjs.map +1 -1
  108. package/dist/cjs/messages/anthropicToolCache.cjs +0 -45
  109. package/dist/cjs/messages/anthropicToolCache.cjs.map +1 -1
  110. package/dist/cjs/messages/assistantPhase.cjs +0 -12
  111. package/dist/cjs/messages/assistantPhase.cjs.map +1 -1
  112. package/dist/cjs/messages/budget.cjs +0 -9
  113. package/dist/cjs/messages/budget.cjs.map +1 -1
  114. package/dist/cjs/messages/cache.cjs +0 -172
  115. package/dist/cjs/messages/cache.cjs.map +1 -1
  116. package/dist/cjs/messages/content.cjs +0 -9
  117. package/dist/cjs/messages/content.cjs.map +1 -1
  118. package/dist/cjs/messages/contextPruning.cjs +0 -15
  119. package/dist/cjs/messages/contextPruning.cjs.map +1 -1
  120. package/dist/cjs/messages/contextPruningSettings.cjs +0 -3
  121. package/dist/cjs/messages/contextPruningSettings.cjs.map +1 -1
  122. package/dist/cjs/messages/core.cjs +0 -48
  123. package/dist/cjs/messages/core.cjs.map +1 -1
  124. package/dist/cjs/messages/format.cjs +1 -345
  125. package/dist/cjs/messages/format.cjs.map +1 -1
  126. package/dist/cjs/messages/handoffCue.cjs +0 -36
  127. package/dist/cjs/messages/handoffCue.cjs.map +1 -1
  128. package/dist/cjs/messages/ids.cjs.map +1 -1
  129. package/dist/cjs/messages/injected.cjs +0 -33
  130. package/dist/cjs/messages/injected.cjs.map +1 -1
  131. package/dist/cjs/messages/langchain.cjs +0 -14
  132. package/dist/cjs/messages/langchain.cjs.map +1 -1
  133. package/dist/cjs/messages/prune.cjs +1 -184
  134. package/dist/cjs/messages/prune.cjs.map +1 -1
  135. package/dist/cjs/messages/recency.cjs +0 -32
  136. package/dist/cjs/messages/recency.cjs.map +1 -1
  137. package/dist/cjs/messages/reducer.cjs +0 -30
  138. package/dist/cjs/messages/reducer.cjs.map +1 -1
  139. package/dist/cjs/messages/tools.cjs +0 -14
  140. package/dist/cjs/messages/tools.cjs.map +1 -1
  141. package/dist/cjs/openai/index.cjs.map +1 -1
  142. package/dist/cjs/prompts/activityLabel.cjs +9 -79
  143. package/dist/cjs/prompts/activityLabel.cjs.map +1 -1
  144. package/dist/cjs/responses/index.cjs.map +1 -1
  145. package/dist/cjs/run.cjs +2 -538
  146. package/dist/cjs/run.cjs.map +1 -1
  147. package/dist/cjs/session/AgentSession.cjs +1 -17
  148. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  149. package/dist/cjs/session/JsonlSessionStore.cjs.map +1 -1
  150. package/dist/cjs/session/handlers.cjs +0 -7
  151. package/dist/cjs/session/handlers.cjs.map +1 -1
  152. package/dist/cjs/session/ids.cjs.map +1 -1
  153. package/dist/cjs/session/messageSerialization.cjs +0 -4
  154. package/dist/cjs/session/messageSerialization.cjs.map +1 -1
  155. package/dist/cjs/stream.cjs +1 -93
  156. package/dist/cjs/stream.cjs.map +1 -1
  157. package/dist/cjs/summarization/index.cjs +0 -27
  158. package/dist/cjs/summarization/index.cjs.map +1 -1
  159. package/dist/cjs/summarization/node.cjs +0 -273
  160. package/dist/cjs/summarization/node.cjs.map +1 -1
  161. package/dist/cjs/tools/BashExecutor.cjs +0 -35
  162. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  163. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +0 -24
  164. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  165. package/dist/cjs/tools/Calculator.cjs.map +1 -1
  166. package/dist/cjs/tools/CodeExecutor.cjs +0 -13
  167. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  168. package/dist/cjs/tools/CodeSessionFileSummary.cjs.map +1 -1
  169. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +0 -101
  170. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  171. package/dist/cjs/tools/ReadFile.cjs.map +1 -1
  172. package/dist/cjs/tools/SkillTool.cjs +0 -5
  173. package/dist/cjs/tools/SkillTool.cjs.map +1 -1
  174. package/dist/cjs/tools/SubagentTool.cjs +0 -10
  175. package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
  176. package/dist/cjs/tools/ToolNode.cjs +6 -1038
  177. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  178. package/dist/cjs/tools/ToolSearch.cjs +0 -179
  179. package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
  180. package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs.map +1 -1
  181. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs.map +1 -1
  182. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs +0 -39
  183. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs.map +1 -1
  184. package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs.map +1 -1
  185. package/dist/cjs/tools/eagerEventExecution.cjs +0 -6
  186. package/dist/cjs/tools/eagerEventExecution.cjs.map +1 -1
  187. package/dist/cjs/tools/handlers.cjs +1 -41
  188. package/dist/cjs/tools/handlers.cjs.map +1 -1
  189. package/dist/cjs/tools/intentArg.cjs +1 -132
  190. package/dist/cjs/tools/intentArg.cjs.map +1 -1
  191. package/dist/cjs/tools/local/CompileCheckTool.cjs +0 -25
  192. package/dist/cjs/tools/local/CompileCheckTool.cjs.map +1 -1
  193. package/dist/cjs/tools/local/FileCheckpointer.cjs +0 -23
  194. package/dist/cjs/tools/local/FileCheckpointer.cjs.map +1 -1
  195. package/dist/cjs/tools/local/LocalCodingTools.cjs +0 -80
  196. package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -1
  197. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +0 -126
  198. package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -1
  199. package/dist/cjs/tools/local/LocalExecutionTools.cjs.map +1 -1
  200. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +0 -12
  201. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs.map +1 -1
  202. package/dist/cjs/tools/local/attachments.cjs +0 -44
  203. package/dist/cjs/tools/local/attachments.cjs.map +1 -1
  204. package/dist/cjs/tools/local/bashAst.cjs +0 -16
  205. package/dist/cjs/tools/local/bashAst.cjs.map +1 -1
  206. package/dist/cjs/tools/local/editStrategies.cjs +0 -14
  207. package/dist/cjs/tools/local/editStrategies.cjs.map +1 -1
  208. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs.map +1 -1
  209. package/dist/cjs/tools/local/syntaxCheck.cjs +0 -31
  210. package/dist/cjs/tools/local/syntaxCheck.cjs.map +1 -1
  211. package/dist/cjs/tools/local/textEncoding.cjs +0 -11
  212. package/dist/cjs/tools/local/textEncoding.cjs.map +1 -1
  213. package/dist/cjs/tools/local/workspaceFS.cjs +0 -33
  214. package/dist/cjs/tools/local/workspaceFS.cjs.map +1 -1
  215. package/dist/cjs/tools/ptcTimeout.cjs.map +1 -1
  216. package/dist/cjs/tools/schema.cjs +0 -8
  217. package/dist/cjs/tools/schema.cjs.map +1 -1
  218. package/dist/cjs/tools/search/anthropic.cjs +0 -9
  219. package/dist/cjs/tools/search/anthropic.cjs.map +1 -1
  220. package/dist/cjs/tools/search/content.cjs.map +1 -1
  221. package/dist/cjs/tools/search/crw-scraper.cjs +0 -33
  222. package/dist/cjs/tools/search/crw-scraper.cjs.map +1 -1
  223. package/dist/cjs/tools/search/crw-search.cjs +0 -2
  224. package/dist/cjs/tools/search/crw-search.cjs.map +1 -1
  225. package/dist/cjs/tools/search/firecrawl.cjs +0 -25
  226. package/dist/cjs/tools/search/firecrawl.cjs.map +1 -1
  227. package/dist/cjs/tools/search/format.cjs +0 -30
  228. package/dist/cjs/tools/search/format.cjs.map +1 -1
  229. package/dist/cjs/tools/search/highlights.cjs +0 -16
  230. package/dist/cjs/tools/search/highlights.cjs.map +1 -1
  231. package/dist/cjs/tools/search/keenable-scraper.cjs +0 -8
  232. package/dist/cjs/tools/search/keenable-scraper.cjs.map +1 -1
  233. package/dist/cjs/tools/search/keenable-search.cjs +0 -7
  234. package/dist/cjs/tools/search/keenable-search.cjs.map +1 -1
  235. package/dist/cjs/tools/search/rerankers.cjs +0 -35
  236. package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
  237. package/dist/cjs/tools/search/schema.cjs +0 -1
  238. package/dist/cjs/tools/search/schema.cjs.map +1 -1
  239. package/dist/cjs/tools/search/search.cjs +1 -14
  240. package/dist/cjs/tools/search/search.cjs.map +1 -1
  241. package/dist/cjs/tools/search/serper-scraper.cjs +0 -46
  242. package/dist/cjs/tools/search/serper-scraper.cjs.map +1 -1
  243. package/dist/cjs/tools/search/tavily-scraper.cjs.map +1 -1
  244. package/dist/cjs/tools/search/tavily-search.cjs.map +1 -1
  245. package/dist/cjs/tools/search/tool.cjs +0 -25
  246. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  247. package/dist/cjs/tools/search/utils.cjs +0 -8
  248. package/dist/cjs/tools/search/utils.cjs.map +1 -1
  249. package/dist/cjs/tools/skillCatalog.cjs +0 -6
  250. package/dist/cjs/tools/skillCatalog.cjs.map +1 -1
  251. package/dist/cjs/tools/streamedToolCallSeals.cjs +0 -6
  252. package/dist/cjs/tools/streamedToolCallSeals.cjs.map +1 -1
  253. package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs +0 -2
  254. package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs.map +1 -1
  255. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +3 -213
  256. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  257. package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -1
  258. package/dist/cjs/tools/subagent/childGraphConfig.cjs +0 -4
  259. package/dist/cjs/tools/subagent/childGraphConfig.cjs.map +1 -1
  260. package/dist/cjs/tools/subagent/runtimeLimits.cjs.map +1 -1
  261. package/dist/cjs/tools/toolOutputReferences.cjs +1 -322
  262. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  263. package/dist/cjs/types/hitl.cjs +0 -5
  264. package/dist/cjs/types/hitl.cjs.map +1 -1
  265. package/dist/cjs/utils/callbacks.cjs.map +1 -1
  266. package/dist/cjs/utils/errors.cjs +14 -165
  267. package/dist/cjs/utils/errors.cjs.map +1 -1
  268. package/dist/cjs/utils/events.cjs +0 -10
  269. package/dist/cjs/utils/events.cjs.map +1 -1
  270. package/dist/cjs/utils/graph.cjs.map +1 -1
  271. package/dist/cjs/utils/handlers.cjs +0 -3
  272. package/dist/cjs/utils/handlers.cjs.map +1 -1
  273. package/dist/cjs/utils/llm.cjs +0 -1
  274. package/dist/cjs/utils/llm.cjs.map +1 -1
  275. package/dist/cjs/utils/misc.cjs +0 -16
  276. package/dist/cjs/utils/misc.cjs.map +1 -1
  277. package/dist/cjs/utils/proxy.cjs +0 -26
  278. package/dist/cjs/utils/proxy.cjs.map +1 -1
  279. package/dist/cjs/utils/run.cjs +0 -6
  280. package/dist/cjs/utils/run.cjs.map +1 -1
  281. package/dist/cjs/utils/schema.cjs +0 -5
  282. package/dist/cjs/utils/schema.cjs.map +1 -1
  283. package/dist/cjs/utils/title.cjs +0 -6
  284. package/dist/cjs/utils/title.cjs.map +1 -1
  285. package/dist/cjs/utils/tokens.cjs +0 -113
  286. package/dist/cjs/utils/tokens.cjs.map +1 -1
  287. package/dist/cjs/utils/toolContent.cjs +0 -41
  288. package/dist/cjs/utils/toolContent.cjs.map +1 -1
  289. package/dist/cjs/utils/truncation.cjs +0 -61
  290. package/dist/cjs/utils/truncation.cjs.map +1 -1
  291. package/dist/esm/agents/AgentContext.mjs +0 -356
  292. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  293. package/dist/esm/agents/projection.mjs +0 -9
  294. package/dist/esm/agents/projection.mjs.map +1 -1
  295. package/dist/esm/common/constants.mjs +0 -26
  296. package/dist/esm/common/constants.mjs.map +1 -1
  297. package/dist/esm/common/enum.mjs +0 -85
  298. package/dist/esm/common/enum.mjs.map +1 -1
  299. package/dist/esm/events.mjs +0 -14
  300. package/dist/esm/events.mjs.map +1 -1
  301. package/dist/esm/graphs/Graph.mjs +1 -909
  302. package/dist/esm/graphs/Graph.mjs.map +1 -1
  303. package/dist/esm/graphs/MultiAgentGraph.mjs +7 -272
  304. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  305. package/dist/esm/graphs/applyGraphRuntimeConfig.mjs.map +1 -1
  306. package/dist/esm/graphs/createGraph.mjs.map +1 -1
  307. package/dist/esm/hitl/askUserQuestion.mjs +0 -53
  308. package/dist/esm/hitl/askUserQuestion.mjs.map +1 -1
  309. package/dist/esm/hitl/askUserQuestions.mjs +0 -20
  310. package/dist/esm/hitl/askUserQuestions.mjs.map +1 -1
  311. package/dist/esm/hitl/askUserQuestionsInterrupt.mjs +0 -6
  312. package/dist/esm/hitl/askUserQuestionsInterrupt.mjs.map +1 -1
  313. package/dist/esm/hooks/HookRegistry.mjs +0 -127
  314. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  315. package/dist/esm/hooks/createToolPolicyHook.mjs +0 -38
  316. package/dist/esm/hooks/createToolPolicyHook.mjs.map +1 -1
  317. package/dist/esm/hooks/createWorkspacePolicyHook.mjs +0 -77
  318. package/dist/esm/hooks/createWorkspacePolicyHook.mjs.map +1 -1
  319. package/dist/esm/hooks/executeHooks.mjs +0 -79
  320. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  321. package/dist/esm/hooks/index.mjs +0 -16
  322. package/dist/esm/hooks/index.mjs.map +1 -1
  323. package/dist/esm/hooks/matchers.mjs +0 -104
  324. package/dist/esm/hooks/matchers.mjs.map +1 -1
  325. package/dist/esm/hooks/types.mjs +0 -8
  326. package/dist/esm/hooks/types.mjs.map +1 -1
  327. package/dist/esm/instrumentation.mjs +0 -14
  328. package/dist/esm/instrumentation.mjs.map +1 -1
  329. package/dist/esm/langfuse.mjs +0 -75
  330. package/dist/esm/langfuse.mjs.map +1 -1
  331. package/dist/esm/langfuseConfig.mjs.map +1 -1
  332. package/dist/esm/langfuseRuntimeContext.mjs +0 -13
  333. package/dist/esm/langfuseRuntimeContext.mjs.map +1 -1
  334. package/dist/esm/langfuseRuntimeScope.mjs +0 -4
  335. package/dist/esm/langfuseRuntimeScope.mjs.map +1 -1
  336. package/dist/esm/langfuseSpanRegistry.mjs +0 -28
  337. package/dist/esm/langfuseSpanRegistry.mjs.map +1 -1
  338. package/dist/esm/langfuseToolOutputTracing.mjs +0 -3
  339. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  340. package/dist/esm/langfuseTraceShaping.mjs +0 -74
  341. package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
  342. package/dist/esm/llm/anthropic/index.mjs +0 -3
  343. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  344. package/dist/esm/llm/anthropic/types.mjs.map +1 -1
  345. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +1 -111
  346. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  347. package/dist/esm/llm/anthropic/utils/message_outputs.mjs +0 -1
  348. package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
  349. package/dist/esm/llm/anthropic/utils/stream_events.mjs +0 -8
  350. package/dist/esm/llm/anthropic/utils/stream_events.mjs.map +1 -1
  351. package/dist/esm/llm/anthropic/utils/tools.mjs.map +1 -1
  352. package/dist/esm/llm/bedrock/cachePoints.mjs.map +1 -1
  353. package/dist/esm/llm/bedrock/index.mjs +1 -95
  354. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  355. package/dist/esm/llm/bedrock/toolCache.mjs.map +1 -1
  356. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +0 -78
  357. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  358. package/dist/esm/llm/bedrock/utils/message_outputs.mjs +0 -24
  359. package/dist/esm/llm/bedrock/utils/message_outputs.mjs.map +1 -1
  360. package/dist/esm/llm/contextOverflowRecovery.mjs +0 -62
  361. package/dist/esm/llm/contextOverflowRecovery.mjs.map +1 -1
  362. package/dist/esm/llm/fake.mjs.map +1 -1
  363. package/dist/esm/llm/google/index.mjs +2 -21
  364. package/dist/esm/llm/google/index.mjs.map +1 -1
  365. package/dist/esm/llm/google/utils/common.mjs +0 -35
  366. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  367. package/dist/esm/llm/init.mjs +0 -9
  368. package/dist/esm/llm/init.mjs.map +1 -1
  369. package/dist/esm/llm/invoke.mjs +1 -270
  370. package/dist/esm/llm/invoke.mjs.map +1 -1
  371. package/dist/esm/llm/mistral/index.mjs.map +1 -1
  372. package/dist/esm/llm/openai/index.mjs +0 -99
  373. package/dist/esm/llm/openai/index.mjs.map +1 -1
  374. package/dist/esm/llm/openai/streamMetadata.mjs +0 -21
  375. package/dist/esm/llm/openai/streamMetadata.mjs.map +1 -1
  376. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  377. package/dist/esm/llm/openrouter/index.mjs +0 -1
  378. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  379. package/dist/esm/llm/openrouter/toolCache.mjs +0 -6
  380. package/dist/esm/llm/openrouter/toolCache.mjs.map +1 -1
  381. package/dist/esm/llm/preempt.mjs +0 -75
  382. package/dist/esm/llm/preempt.mjs.map +1 -1
  383. package/dist/esm/llm/providers.mjs.map +1 -1
  384. package/dist/esm/llm/request.mjs +0 -10
  385. package/dist/esm/llm/request.mjs.map +1 -1
  386. package/dist/esm/llm/stream/chunkAdapters.mjs +0 -62
  387. package/dist/esm/llm/stream/chunkAdapters.mjs.map +1 -1
  388. package/dist/esm/llm/stream/smoother.mjs +0 -58
  389. package/dist/esm/llm/stream/smoother.mjs.map +1 -1
  390. package/dist/esm/llm/streamLimits.mjs +4 -293
  391. package/dist/esm/llm/streamLimits.mjs.map +1 -1
  392. package/dist/esm/llm/truncation.mjs +0 -36
  393. package/dist/esm/llm/truncation.mjs.map +1 -1
  394. package/dist/esm/llm/vertexai/index.mjs +0 -330
  395. package/dist/esm/llm/vertexai/index.mjs.map +1 -1
  396. package/dist/esm/messages/alternation.mjs +0 -43
  397. package/dist/esm/messages/alternation.mjs.map +1 -1
  398. package/dist/esm/messages/anthropicToolCache.mjs +0 -45
  399. package/dist/esm/messages/anthropicToolCache.mjs.map +1 -1
  400. package/dist/esm/messages/assistantPhase.mjs +0 -12
  401. package/dist/esm/messages/assistantPhase.mjs.map +1 -1
  402. package/dist/esm/messages/budget.mjs +0 -9
  403. package/dist/esm/messages/budget.mjs.map +1 -1
  404. package/dist/esm/messages/cache.mjs +0 -172
  405. package/dist/esm/messages/cache.mjs.map +1 -1
  406. package/dist/esm/messages/content.mjs +0 -9
  407. package/dist/esm/messages/content.mjs.map +1 -1
  408. package/dist/esm/messages/contextPruning.mjs +0 -15
  409. package/dist/esm/messages/contextPruning.mjs.map +1 -1
  410. package/dist/esm/messages/contextPruningSettings.mjs +0 -3
  411. package/dist/esm/messages/contextPruningSettings.mjs.map +1 -1
  412. package/dist/esm/messages/core.mjs +0 -48
  413. package/dist/esm/messages/core.mjs.map +1 -1
  414. package/dist/esm/messages/format.mjs +1 -345
  415. package/dist/esm/messages/format.mjs.map +1 -1
  416. package/dist/esm/messages/handoffCue.mjs +0 -36
  417. package/dist/esm/messages/handoffCue.mjs.map +1 -1
  418. package/dist/esm/messages/ids.mjs.map +1 -1
  419. package/dist/esm/messages/injected.mjs +0 -33
  420. package/dist/esm/messages/injected.mjs.map +1 -1
  421. package/dist/esm/messages/langchain.mjs +0 -14
  422. package/dist/esm/messages/langchain.mjs.map +1 -1
  423. package/dist/esm/messages/prune.mjs +1 -184
  424. package/dist/esm/messages/prune.mjs.map +1 -1
  425. package/dist/esm/messages/recency.mjs +0 -32
  426. package/dist/esm/messages/recency.mjs.map +1 -1
  427. package/dist/esm/messages/reducer.mjs +0 -30
  428. package/dist/esm/messages/reducer.mjs.map +1 -1
  429. package/dist/esm/messages/tools.mjs +0 -14
  430. package/dist/esm/messages/tools.mjs.map +1 -1
  431. package/dist/esm/openai/index.mjs.map +1 -1
  432. package/dist/esm/prompts/activityLabel.mjs +9 -79
  433. package/dist/esm/prompts/activityLabel.mjs.map +1 -1
  434. package/dist/esm/responses/index.mjs.map +1 -1
  435. package/dist/esm/run.mjs +2 -538
  436. package/dist/esm/run.mjs.map +1 -1
  437. package/dist/esm/session/AgentSession.mjs +1 -17
  438. package/dist/esm/session/AgentSession.mjs.map +1 -1
  439. package/dist/esm/session/JsonlSessionStore.mjs.map +1 -1
  440. package/dist/esm/session/handlers.mjs +0 -7
  441. package/dist/esm/session/handlers.mjs.map +1 -1
  442. package/dist/esm/session/ids.mjs.map +1 -1
  443. package/dist/esm/session/messageSerialization.mjs +0 -4
  444. package/dist/esm/session/messageSerialization.mjs.map +1 -1
  445. package/dist/esm/stream.mjs +1 -93
  446. package/dist/esm/stream.mjs.map +1 -1
  447. package/dist/esm/summarization/index.mjs +0 -27
  448. package/dist/esm/summarization/index.mjs.map +1 -1
  449. package/dist/esm/summarization/node.mjs +0 -273
  450. package/dist/esm/summarization/node.mjs.map +1 -1
  451. package/dist/esm/tools/BashExecutor.mjs +0 -35
  452. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  453. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +0 -24
  454. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  455. package/dist/esm/tools/Calculator.mjs.map +1 -1
  456. package/dist/esm/tools/CodeExecutor.mjs +0 -13
  457. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  458. package/dist/esm/tools/CodeSessionFileSummary.mjs.map +1 -1
  459. package/dist/esm/tools/ProgrammaticToolCalling.mjs +0 -101
  460. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  461. package/dist/esm/tools/ReadFile.mjs.map +1 -1
  462. package/dist/esm/tools/SkillTool.mjs +0 -5
  463. package/dist/esm/tools/SkillTool.mjs.map +1 -1
  464. package/dist/esm/tools/SubagentTool.mjs +0 -10
  465. package/dist/esm/tools/SubagentTool.mjs.map +1 -1
  466. package/dist/esm/tools/ToolNode.mjs +6 -1038
  467. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  468. package/dist/esm/tools/ToolSearch.mjs +0 -179
  469. package/dist/esm/tools/ToolSearch.mjs.map +1 -1
  470. package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs.map +1 -1
  471. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs.map +1 -1
  472. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs +0 -39
  473. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs.map +1 -1
  474. package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs.map +1 -1
  475. package/dist/esm/tools/eagerEventExecution.mjs +0 -6
  476. package/dist/esm/tools/eagerEventExecution.mjs.map +1 -1
  477. package/dist/esm/tools/handlers.mjs +1 -41
  478. package/dist/esm/tools/handlers.mjs.map +1 -1
  479. package/dist/esm/tools/intentArg.mjs +1 -132
  480. package/dist/esm/tools/intentArg.mjs.map +1 -1
  481. package/dist/esm/tools/local/CompileCheckTool.mjs +0 -25
  482. package/dist/esm/tools/local/CompileCheckTool.mjs.map +1 -1
  483. package/dist/esm/tools/local/FileCheckpointer.mjs +0 -23
  484. package/dist/esm/tools/local/FileCheckpointer.mjs.map +1 -1
  485. package/dist/esm/tools/local/LocalCodingTools.mjs +0 -80
  486. package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -1
  487. package/dist/esm/tools/local/LocalExecutionEngine.mjs +0 -126
  488. package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -1
  489. package/dist/esm/tools/local/LocalExecutionTools.mjs.map +1 -1
  490. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +0 -12
  491. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs.map +1 -1
  492. package/dist/esm/tools/local/attachments.mjs +0 -44
  493. package/dist/esm/tools/local/attachments.mjs.map +1 -1
  494. package/dist/esm/tools/local/bashAst.mjs +0 -16
  495. package/dist/esm/tools/local/bashAst.mjs.map +1 -1
  496. package/dist/esm/tools/local/editStrategies.mjs +0 -14
  497. package/dist/esm/tools/local/editStrategies.mjs.map +1 -1
  498. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs.map +1 -1
  499. package/dist/esm/tools/local/syntaxCheck.mjs +0 -31
  500. package/dist/esm/tools/local/syntaxCheck.mjs.map +1 -1
  501. package/dist/esm/tools/local/textEncoding.mjs +0 -11
  502. package/dist/esm/tools/local/textEncoding.mjs.map +1 -1
  503. package/dist/esm/tools/local/workspaceFS.mjs +0 -33
  504. package/dist/esm/tools/local/workspaceFS.mjs.map +1 -1
  505. package/dist/esm/tools/ptcTimeout.mjs.map +1 -1
  506. package/dist/esm/tools/schema.mjs +0 -8
  507. package/dist/esm/tools/schema.mjs.map +1 -1
  508. package/dist/esm/tools/search/anthropic.mjs +0 -9
  509. package/dist/esm/tools/search/anthropic.mjs.map +1 -1
  510. package/dist/esm/tools/search/content.mjs.map +1 -1
  511. package/dist/esm/tools/search/crw-scraper.mjs +0 -33
  512. package/dist/esm/tools/search/crw-scraper.mjs.map +1 -1
  513. package/dist/esm/tools/search/crw-search.mjs +0 -2
  514. package/dist/esm/tools/search/crw-search.mjs.map +1 -1
  515. package/dist/esm/tools/search/firecrawl.mjs +0 -25
  516. package/dist/esm/tools/search/firecrawl.mjs.map +1 -1
  517. package/dist/esm/tools/search/format.mjs +0 -30
  518. package/dist/esm/tools/search/format.mjs.map +1 -1
  519. package/dist/esm/tools/search/highlights.mjs +0 -16
  520. package/dist/esm/tools/search/highlights.mjs.map +1 -1
  521. package/dist/esm/tools/search/keenable-scraper.mjs +0 -8
  522. package/dist/esm/tools/search/keenable-scraper.mjs.map +1 -1
  523. package/dist/esm/tools/search/keenable-search.mjs +0 -7
  524. package/dist/esm/tools/search/keenable-search.mjs.map +1 -1
  525. package/dist/esm/tools/search/rerankers.mjs +0 -35
  526. package/dist/esm/tools/search/rerankers.mjs.map +1 -1
  527. package/dist/esm/tools/search/schema.mjs +0 -1
  528. package/dist/esm/tools/search/schema.mjs.map +1 -1
  529. package/dist/esm/tools/search/search.mjs +1 -14
  530. package/dist/esm/tools/search/search.mjs.map +1 -1
  531. package/dist/esm/tools/search/serper-scraper.mjs +0 -46
  532. package/dist/esm/tools/search/serper-scraper.mjs.map +1 -1
  533. package/dist/esm/tools/search/tavily-scraper.mjs.map +1 -1
  534. package/dist/esm/tools/search/tavily-search.mjs.map +1 -1
  535. package/dist/esm/tools/search/tool.mjs +0 -25
  536. package/dist/esm/tools/search/tool.mjs.map +1 -1
  537. package/dist/esm/tools/search/utils.mjs +0 -8
  538. package/dist/esm/tools/search/utils.mjs.map +1 -1
  539. package/dist/esm/tools/skillCatalog.mjs +0 -6
  540. package/dist/esm/tools/skillCatalog.mjs.map +1 -1
  541. package/dist/esm/tools/streamedToolCallSeals.mjs +0 -6
  542. package/dist/esm/tools/streamedToolCallSeals.mjs.map +1 -1
  543. package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs +0 -2
  544. package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs.map +1 -1
  545. package/dist/esm/tools/subagent/SubagentExecutor.mjs +3 -213
  546. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  547. package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -1
  548. package/dist/esm/tools/subagent/childGraphConfig.mjs +0 -4
  549. package/dist/esm/tools/subagent/childGraphConfig.mjs.map +1 -1
  550. package/dist/esm/tools/subagent/runtimeLimits.mjs.map +1 -1
  551. package/dist/esm/tools/toolOutputReferences.mjs +1 -322
  552. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  553. package/dist/esm/types/hitl.mjs +0 -5
  554. package/dist/esm/types/hitl.mjs.map +1 -1
  555. package/dist/esm/utils/callbacks.mjs.map +1 -1
  556. package/dist/esm/utils/errors.mjs +14 -165
  557. package/dist/esm/utils/errors.mjs.map +1 -1
  558. package/dist/esm/utils/events.mjs +0 -10
  559. package/dist/esm/utils/events.mjs.map +1 -1
  560. package/dist/esm/utils/graph.mjs.map +1 -1
  561. package/dist/esm/utils/handlers.mjs +0 -3
  562. package/dist/esm/utils/handlers.mjs.map +1 -1
  563. package/dist/esm/utils/llm.mjs +0 -1
  564. package/dist/esm/utils/llm.mjs.map +1 -1
  565. package/dist/esm/utils/misc.mjs +0 -16
  566. package/dist/esm/utils/misc.mjs.map +1 -1
  567. package/dist/esm/utils/proxy.mjs +0 -26
  568. package/dist/esm/utils/proxy.mjs.map +1 -1
  569. package/dist/esm/utils/run.mjs +0 -6
  570. package/dist/esm/utils/run.mjs.map +1 -1
  571. package/dist/esm/utils/schema.mjs +0 -5
  572. package/dist/esm/utils/schema.mjs.map +1 -1
  573. package/dist/esm/utils/title.mjs +0 -6
  574. package/dist/esm/utils/title.mjs.map +1 -1
  575. package/dist/esm/utils/tokens.mjs +0 -113
  576. package/dist/esm/utils/tokens.mjs.map +1 -1
  577. package/dist/esm/utils/toolContent.mjs +0 -41
  578. package/dist/esm/utils/toolContent.mjs.map +1 -1
  579. package/dist/esm/utils/truncation.mjs +0 -61
  580. package/dist/esm/utils/truncation.mjs.map +1 -1
  581. package/package.json +35 -32
  582. package/dist/types/agents/__tests__/promptCacheLiveHelpers.d.ts +0 -52
  583. package/dist/types/test/mockTools.d.ts +0 -28
  584. package/dist/types/utils/__tests__/fixtures/contextOverflowSignatures.d.ts +0 -40
  585. package/src/__tests__/stream.eagerArgsDivergence.test.ts +0 -911
  586. package/src/__tests__/stream.eagerEventExecution.test.ts +0 -5046
  587. package/src/__tests__/stream.streamLimits.test.ts +0 -1982
  588. package/src/agents/__tests__/AgentContext.anthropic.live.test.ts +0 -448
  589. package/src/agents/__tests__/AgentContext.bedrock.live.test.ts +0 -655
  590. package/src/agents/__tests__/AgentContext.openrouter.live.test.ts +0 -128
  591. package/src/agents/__tests__/AgentContext.overflow.test.ts +0 -205
  592. package/src/agents/__tests__/AgentContext.test.ts +0 -2425
  593. package/src/agents/__tests__/projection.test.ts +0 -73
  594. package/src/agents/__tests__/promptCacheLiveHelpers.ts +0 -175
  595. package/src/aggregator.test.ts +0 -1214
  596. package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +0 -273
  597. package/src/graphs/__tests__/Graph.closeRunStep.test.ts +0 -385
  598. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +0 -1766
  599. package/src/graphs/__tests__/Graph.preemptSignal.test.ts +0 -126
  600. package/src/graphs/__tests__/Graph.reasoning.test.ts +0 -1717
  601. package/src/graphs/__tests__/Graph.subagentResumeState.test.ts +0 -80
  602. package/src/graphs/__tests__/MultiAgentGraph.test.ts +0 -177
  603. package/src/graphs/__tests__/composition.smoke.test.ts +0 -509
  604. package/src/graphs/__tests__/createGraph.test.ts +0 -206
  605. package/src/hooks/__tests__/HookRegistry.test.ts +0 -334
  606. package/src/hooks/__tests__/compactHooks.test.ts +0 -250
  607. package/src/hooks/__tests__/createToolPolicyHook.test.ts +0 -268
  608. package/src/hooks/__tests__/createWorkspacePolicyHook.test.ts +0 -405
  609. package/src/hooks/__tests__/executeHooks.test.ts +0 -1294
  610. package/src/hooks/__tests__/integration.test.ts +0 -337
  611. package/src/hooks/__tests__/matchers.test.ts +0 -238
  612. package/src/hooks/__tests__/preemptBoundary.test.ts +0 -152
  613. package/src/hooks/__tests__/toolHooks.test.ts +0 -669
  614. package/src/llm/__tests__/contextOverflowRecovery.test.ts +0 -401
  615. package/src/llm/__tests__/fallbackOverflow.test.ts +0 -287
  616. package/src/llm/anthropic/Jacob_Lee_Resume_2023.pdf +0 -0
  617. package/src/llm/anthropic/inherited-content-utils.spec.ts +0 -249
  618. package/src/llm/anthropic/inherited-stream-events.spec.ts +0 -1259
  619. package/src/llm/anthropic/inherited-strict.spec.ts +0 -302
  620. package/src/llm/anthropic/llm.spec.ts +0 -3394
  621. package/src/llm/anthropic/utils/cross-provider-reasoning.test.ts +0 -383
  622. package/src/llm/anthropic/utils/cross-provider-server-tools.test.ts +0 -110
  623. package/src/llm/anthropic/utils/server-tool-inputs.test.ts +0 -508
  624. package/src/llm/anthropic/utils/streaming-tool-input.test.ts +0 -361
  625. package/src/llm/anthropic/utils/stripPrefillCache.test.ts +0 -111
  626. package/src/llm/anthropic/utils/tool-id-normalization.test.ts +0 -287
  627. package/src/llm/bedrock/inherited-cache.spec.ts +0 -144
  628. package/src/llm/bedrock/inherited.spec.ts +0 -724
  629. package/src/llm/bedrock/llm.spec.ts +0 -1533
  630. package/src/llm/bedrock/streamSealDispatch.test.ts +0 -331
  631. package/src/llm/bedrock/toolCache.test.ts +0 -194
  632. package/src/llm/bedrock/utils/cross-provider-reasoning.test.ts +0 -131
  633. package/src/llm/bedrock/utils/cross-provider-server-tools.test.ts +0 -122
  634. package/src/llm/bedrock/utils/message_inputs.test.ts +0 -650
  635. package/src/llm/bedrock/utils/message_outputs.test.ts +0 -85
  636. package/src/llm/bedrock/utils/toolResultCachePoint.test.ts +0 -118
  637. package/src/llm/custom-chat-models.smoke.test.ts +0 -1966
  638. package/src/llm/google/data/gettysburg10.wav +0 -0
  639. package/src/llm/google/data/hotdog.jpg +0 -0
  640. package/src/llm/google/inherited-stream-events.spec.ts +0 -350
  641. package/src/llm/google/llm.spec.ts +0 -1388
  642. package/src/llm/google/streamSmoothing.test.ts +0 -121
  643. package/src/llm/google/utils/common.test.ts +0 -196
  644. package/src/llm/invoke.alternation.test.ts +0 -87
  645. package/src/llm/invoke.handoffCue.test.ts +0 -112
  646. package/src/llm/invoke.streamLimits.test.ts +0 -142
  647. package/src/llm/invoke.test.ts +0 -1198
  648. package/src/llm/mistral/streamSmoothing.test.ts +0 -97
  649. package/src/llm/openai/cacheWriteTokens.test.ts +0 -112
  650. package/src/llm/openai/contentBlocks.test.ts +0 -381
  651. package/src/llm/openai/deepseek.test.ts +0 -747
  652. package/src/llm/openai/inherited-deepseek.spec.ts +0 -347
  653. package/src/llm/openai/inherited-xai.spec.ts +0 -416
  654. package/src/llm/openai/llm.spec.ts +0 -1774
  655. package/src/llm/openai/managedRequests.test.ts +0 -182
  656. package/src/llm/openai/sequentialToolCallSeals.test.ts +0 -199
  657. package/src/llm/openai/streamMetadata.spec.ts +0 -86
  658. package/src/llm/openai/streamMetadataDedup.spec.ts +0 -166
  659. package/src/llm/openai/utils/isReasoningModel.test.ts +0 -90
  660. package/src/llm/openai/utils/messages.test.ts +0 -488
  661. package/src/llm/openrouter/reasoning.test.ts +0 -561
  662. package/src/llm/openrouter/toolCache.test.ts +0 -134
  663. package/src/llm/preempt.test.ts +0 -323
  664. package/src/llm/stream/chunkAdapters.test.ts +0 -202
  665. package/src/llm/stream/reassembly.test.ts +0 -241
  666. package/src/llm/stream/smoother.bench.test.ts +0 -155
  667. package/src/llm/stream/smoother.test.ts +0 -519
  668. package/src/llm/streamLimits.test.ts +0 -450
  669. package/src/llm/truncation.test.ts +0 -242
  670. package/src/llm/vertexai/fixThoughtSignatures.test.ts +0 -154
  671. package/src/llm/vertexai/inherited-stream-events.spec.ts +0 -271
  672. package/src/llm/vertexai/llm.spec.ts +0 -159
  673. package/src/llm/vertexai/repairUsageMetadata.test.ts +0 -54
  674. package/src/llm/vertexai/sealStreamedToolCalls.test.ts +0 -88
  675. package/src/llm/vertexai/streamSealDispatch.test.ts +0 -148
  676. package/src/llm/vertexai/streamSmoothing.test.ts +0 -109
  677. package/src/messages/__tests__/anthropicToolCache.test.ts +0 -339
  678. package/src/messages/__tests__/observationMasking.test.ts +0 -312
  679. package/src/messages/__tests__/recency.test.ts +0 -267
  680. package/src/messages/__tests__/tools.test.ts +0 -473
  681. package/src/messages/alternation.test.ts +0 -212
  682. package/src/messages/assistantPhase.test.ts +0 -75
  683. package/src/messages/cache.tail.test.ts +0 -533
  684. package/src/messages/cache.test.ts +0 -2166
  685. package/src/messages/content.test.ts +0 -362
  686. package/src/messages/contextPruning.test.ts +0 -184
  687. package/src/messages/ensureThinkingBlock.test.ts +0 -1427
  688. package/src/messages/foldToollessToolBlocks.test.ts +0 -596
  689. package/src/messages/formatAgentMessages.reducer.test.ts +0 -162
  690. package/src/messages/formatAgentMessages.skills.test.ts +0 -513
  691. package/src/messages/formatAgentMessages.steer.test.ts +0 -598
  692. package/src/messages/formatAgentMessages.test.ts +0 -8111
  693. package/src/messages/formatAgentMessages.tools.test.ts +0 -522
  694. package/src/messages/formatMessage.test.ts +0 -693
  695. package/src/messages/handoffCue.test.ts +0 -96
  696. package/src/messages/injected.test.ts +0 -107
  697. package/src/messages/labelContentByAgent.test.ts +0 -962
  698. package/src/messages/reducer.spec.ts +0 -32
  699. package/src/messages/shiftIndexTokenCountMap.test.ts +0 -81
  700. package/src/messages/tailCacheConversion.test.ts +0 -161
  701. package/src/openai/__tests__/openai.test.ts +0 -337
  702. package/src/responses/__tests__/responses.test.ts +0 -652
  703. package/src/scripts/abort.ts +0 -157
  704. package/src/scripts/activity-labels/captured.json +0 -56
  705. package/src/scripts/activity-labels/checks.cjs +0 -205
  706. package/src/scripts/activity-labels/corpus.cjs +0 -473
  707. package/src/scripts/activity-labels/report.cjs +0 -203
  708. package/src/scripts/activity-labels/rescore.cjs +0 -102
  709. package/src/scripts/activity-labels/run.ts +0 -705
  710. package/src/scripts/activity-labels/variants.ts +0 -71
  711. package/src/scripts/ant_web_search.ts +0 -159
  712. package/src/scripts/ant_web_search_edge_case.ts +0 -163
  713. package/src/scripts/ant_web_search_error_edge_case.ts +0 -149
  714. package/src/scripts/args.ts +0 -48
  715. package/src/scripts/bedrock-cache-debug.ts +0 -250
  716. package/src/scripts/bedrock-content-aggregation-test.ts +0 -266
  717. package/src/scripts/bedrock-merge-test.ts +0 -107
  718. package/src/scripts/bedrock-parallel-tools-test.ts +0 -204
  719. package/src/scripts/bench-prompt-cache.ts +0 -479
  720. package/src/scripts/caching.ts +0 -132
  721. package/src/scripts/cli.ts +0 -172
  722. package/src/scripts/cli2.ts +0 -133
  723. package/src/scripts/cli3.ts +0 -184
  724. package/src/scripts/cli4.ts +0 -191
  725. package/src/scripts/cli5.ts +0 -191
  726. package/src/scripts/code_exec.ts +0 -214
  727. package/src/scripts/code_exec_files.ts +0 -237
  728. package/src/scripts/code_exec_multi_session.ts +0 -237
  729. package/src/scripts/code_exec_ptc.ts +0 -335
  730. package/src/scripts/code_exec_session.ts +0 -283
  731. package/src/scripts/code_exec_simple.ts +0 -148
  732. package/src/scripts/compare_pi_vs_ours.ts +0 -1259
  733. package/src/scripts/content.ts +0 -139
  734. package/src/scripts/context-overflow-probe.ts +0 -997
  735. package/src/scripts/empty_input.ts +0 -137
  736. package/src/scripts/handoff-test.ts +0 -134
  737. package/src/scripts/local_engine.ts +0 -166
  738. package/src/scripts/local_engine_checkpointer.ts +0 -205
  739. package/src/scripts/local_engine_compile.ts +0 -263
  740. package/src/scripts/local_engine_hooks.ts +0 -226
  741. package/src/scripts/local_engine_image.ts +0 -201
  742. package/src/scripts/local_engine_ptc.ts +0 -151
  743. package/src/scripts/local_engine_workspace.ts +0 -258
  744. package/src/scripts/memory.ts +0 -107
  745. package/src/scripts/multi-agent-chain.ts +0 -332
  746. package/src/scripts/multi-agent-conditional.ts +0 -222
  747. package/src/scripts/multi-agent-document-review-chain.ts +0 -198
  748. package/src/scripts/multi-agent-hybrid-flow.ts +0 -311
  749. package/src/scripts/multi-agent-parallel-start.ts +0 -265
  750. package/src/scripts/multi-agent-parallel.ts +0 -395
  751. package/src/scripts/multi-agent-sequence.ts +0 -217
  752. package/src/scripts/multi-agent-subagent.ts +0 -246
  753. package/src/scripts/multi-agent-supervisor.ts +0 -366
  754. package/src/scripts/multi-agent-test.ts +0 -187
  755. package/src/scripts/openrouter_prompt_cache_live.ts +0 -310
  756. package/src/scripts/parallel-asymmetric-tools-test.ts +0 -275
  757. package/src/scripts/parallel-full-metadata-test.ts +0 -241
  758. package/src/scripts/parallel-tools-test.ts +0 -341
  759. package/src/scripts/preempt-probe.ts +0 -330
  760. package/src/scripts/preempt-scenarios.ts +0 -388
  761. package/src/scripts/programmatic_exec.ts +0 -387
  762. package/src/scripts/programmatic_exec_agent.ts +0 -232
  763. package/src/scripts/search.ts +0 -147
  764. package/src/scripts/sequential-full-metadata-test.ts +0 -198
  765. package/src/scripts/session_live.ts +0 -548
  766. package/src/scripts/simple.ts +0 -226
  767. package/src/scripts/single-agent-metadata-test.ts +0 -196
  768. package/src/scripts/stream.ts +0 -141
  769. package/src/scripts/subagent-configurable-inheritance.ts +0 -252
  770. package/src/scripts/subagent-event-driven-debug.ts +0 -190
  771. package/src/scripts/subagent-tools-debug.ts +0 -160
  772. package/src/scripts/subagent-usage-sink.ts +0 -176
  773. package/src/scripts/summarization-recency.ts +0 -462
  774. package/src/scripts/test-custom-prompt-key.ts +0 -145
  775. package/src/scripts/test-handoff-input.ts +0 -170
  776. package/src/scripts/test-handoff-preamble.ts +0 -278
  777. package/src/scripts/test-handoff-steering.ts +0 -430
  778. package/src/scripts/test-multi-agent-list-handoff.ts +0 -418
  779. package/src/scripts/test-parallel-agent-labeling.ts +0 -327
  780. package/src/scripts/test-parallel-handoffs.ts +0 -292
  781. package/src/scripts/test-thinking-handoff-bedrock.ts +0 -154
  782. package/src/scripts/test-thinking-handoff.ts +0 -156
  783. package/src/scripts/test-thinking-to-thinking-handoff-bedrock.ts +0 -166
  784. package/src/scripts/test-tool-before-handoff-role-order.ts +0 -276
  785. package/src/scripts/test-tools-before-handoff.ts +0 -222
  786. package/src/scripts/test_code_api.ts +0 -354
  787. package/src/scripts/thinking-bedrock.ts +0 -160
  788. package/src/scripts/thinking-vertexai.ts +0 -168
  789. package/src/scripts/thinking.ts +0 -172
  790. package/src/scripts/tool_search.ts +0 -153
  791. package/src/scripts/tools.ts +0 -163
  792. package/src/session/__tests__/JsonlSessionStore.test.ts +0 -1463
  793. package/src/session/__tests__/handlers.test.ts +0 -161
  794. package/src/specs/activity-label-observability.live.test.ts +0 -262
  795. package/src/specs/activity-label-observability.test.ts +0 -167
  796. package/src/specs/activity-label-prompt.test.ts +0 -359
  797. package/src/specs/activity-label-trace-seed.test.ts +0 -47
  798. package/src/specs/activity-phase-label.test.ts +0 -306
  799. package/src/specs/agent-handoffs.live.test.ts +0 -140
  800. package/src/specs/agent-handoffs.test.ts +0 -2465
  801. package/src/specs/anthropic.simple.test.ts +0 -453
  802. package/src/specs/ask-user-question-batch.test.ts +0 -289
  803. package/src/specs/ask-user-questions.live.test.ts +0 -185
  804. package/src/specs/ask-user-questions.test.ts +0 -293
  805. package/src/specs/azure.simple.test.ts +0 -502
  806. package/src/specs/bedrock-toolless.live.test.ts +0 -123
  807. package/src/specs/bedrock-truncation.live.test.ts +0 -191
  808. package/src/specs/cache.simple.test.ts +0 -416
  809. package/src/specs/context-accuracy.live.test.ts +0 -409
  810. package/src/specs/context-overflow-recovery.live.test.ts +0 -213
  811. package/src/specs/context-usage-event.test.ts +0 -117
  812. package/src/specs/context-usage.live.test.ts +0 -297
  813. package/src/specs/custom-event-await.test.ts +0 -304
  814. package/src/specs/deepseek.simple.test.ts +0 -290
  815. package/src/specs/deterministic-trace-id.test.ts +0 -43
  816. package/src/specs/discovered-tools.test.ts +0 -217
  817. package/src/specs/durability-checkpoint.integration.test.ts +0 -243
  818. package/src/specs/graph-subagent.live.test.ts +0 -242
  819. package/src/specs/graph-subagent.test.ts +0 -322
  820. package/src/specs/handoffCue.test.ts +0 -165
  821. package/src/specs/langfuse-callbacks.test.ts +0 -1578
  822. package/src/specs/langfuse-config.test.ts +0 -247
  823. package/src/specs/langfuse-instrumentation.test.ts +0 -393
  824. package/src/specs/langfuse-metadata.test.ts +0 -245
  825. package/src/specs/langfuse-routing.integration.test.ts +0 -844
  826. package/src/specs/langfuse-runtime-context.test.ts +0 -92
  827. package/src/specs/langfuse-span-registry.test.ts +0 -87
  828. package/src/specs/langfuse-tool-output-tracing.test.ts +0 -1858
  829. package/src/specs/langfuse-trace-shaping.test.ts +0 -591
  830. package/src/specs/moonshot.simple.test.ts +0 -367
  831. package/src/specs/multi-agent-summarization.test.ts +0 -396
  832. package/src/specs/openai.simple.test.ts +0 -315
  833. package/src/specs/openrouter.simple.test.ts +0 -275
  834. package/src/specs/preemptSeal.test.ts +0 -678
  835. package/src/specs/prune.test.ts +0 -3741
  836. package/src/specs/reasoning.test.ts +0 -201
  837. package/src/specs/run-step-timestamps.test.ts +0 -414
  838. package/src/specs/spec.utils.ts +0 -15
  839. package/src/specs/subagent.test.ts +0 -864
  840. package/src/specs/summarization-unit.test.ts +0 -973
  841. package/src/specs/summarization.test.ts +0 -3823
  842. package/src/specs/summarize-prune.test.ts +0 -376
  843. package/src/specs/thinking-handoff.test.ts +0 -621
  844. package/src/specs/thinking-prune.test.ts +0 -827
  845. package/src/specs/token-accounting-e2e.test.ts +0 -1034
  846. package/src/specs/token-accounting-pipeline.test.ts +0 -1008
  847. package/src/specs/token-distribution-edge-case.test.ts +0 -315
  848. package/src/specs/token-memoization.test.ts +0 -40
  849. package/src/specs/tokens.test.ts +0 -1000
  850. package/src/specs/tool-error-resume.test.ts +0 -194
  851. package/src/specs/tool-error.test.ts +0 -198
  852. package/src/specs/vllm-reasoning-toolcalls.test.ts +0 -340
  853. package/src/stream.dispatch.test.ts +0 -63
  854. package/src/stream.test.ts +0 -144
  855. package/src/summarization/__tests__/aggregator.test.ts +0 -236
  856. package/src/summarization/__tests__/node.test.ts +0 -1996
  857. package/src/summarization/__tests__/trigger.test.ts +0 -149
  858. package/src/summarization/chunkHandler.test.ts +0 -196
  859. package/src/test/mockTools.ts +0 -386
  860. package/src/tools/Calculator.test.ts +0 -278
  861. package/src/tools/__tests__/BashExecutor.test.ts +0 -108
  862. package/src/tools/__tests__/CloudflareSandboxExecution.test.ts +0 -939
  863. package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +0 -858
  864. package/src/tools/__tests__/CodeExecutor.stateful.test.ts +0 -124
  865. package/src/tools/__tests__/LocalExecutionRoots.test.ts +0 -8
  866. package/src/tools/__tests__/LocalExecutionTools.test.ts +0 -2733
  867. package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.ts +0 -318
  868. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +0 -1557
  869. package/src/tools/__tests__/ReadFile.test.ts +0 -44
  870. package/src/tools/__tests__/SkillTool.test.ts +0 -442
  871. package/src/tools/__tests__/SubagentExecutor.test.ts +0 -3728
  872. package/src/tools/__tests__/SubagentReplay.test.ts +0 -329
  873. package/src/tools/__tests__/SubagentTool.test.ts +0 -149
  874. package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +0 -595
  875. package/src/tools/__tests__/ToolNode.eagerEventExecution.test.ts +0 -1191
  876. package/src/tools/__tests__/ToolNode.invalidToolCalls.test.ts +0 -757
  877. package/src/tools/__tests__/ToolNode.langfuse.test.ts +0 -57
  878. package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +0 -736
  879. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +0 -1710
  880. package/src/tools/__tests__/ToolNode.runtimeState.test.ts +0 -120
  881. package/src/tools/__tests__/ToolNode.session.test.ts +0 -1352
  882. package/src/tools/__tests__/ToolNode.streamLimits.test.ts +0 -69
  883. package/src/tools/__tests__/ToolSearch.integration.test.ts +0 -161
  884. package/src/tools/__tests__/ToolSearch.test.ts +0 -1227
  885. package/src/tools/__tests__/annotateMessagesForLLM.test.ts +0 -561
  886. package/src/tools/__tests__/directToolHITLResumeScope.test.ts +0 -980
  887. package/src/tools/__tests__/directToolHooks.test.ts +0 -700
  888. package/src/tools/__tests__/eagerEventExecution.session.test.ts +0 -92
  889. package/src/tools/__tests__/handlers.test.ts +0 -997
  890. package/src/tools/__tests__/hitl.test.ts +0 -4663
  891. package/src/tools/__tests__/intentArg.test.ts +0 -357
  892. package/src/tools/__tests__/intentCoverage.test.ts +0 -139
  893. package/src/tools/__tests__/localToolNames.test.ts +0 -75
  894. package/src/tools/__tests__/skillCatalog.test.ts +0 -161
  895. package/src/tools/__tests__/subagentHooks.test.ts +0 -1651
  896. package/src/tools/__tests__/toolOutputReferences.test.ts +0 -515
  897. package/src/tools/__tests__/workspaceSeam.test.ts +0 -147
  898. package/src/tools/local/__tests__/FileCheckpointer.test.ts +0 -162
  899. package/src/tools/local/__tests__/editStrategies.test.ts +0 -134
  900. package/src/tools/search/content.test.ts +0 -173
  901. package/src/tools/search/crw.test.ts +0 -836
  902. package/src/tools/search/format.test.ts +0 -242
  903. package/src/tools/search/http-agent.test.ts +0 -133
  904. package/src/tools/search/jina-reranker.test.ts +0 -306
  905. package/src/tools/search/keenable-scraper.test.ts +0 -153
  906. package/src/tools/search/keenable.test.ts +0 -183
  907. package/src/tools/search/outcome.test.ts +0 -90
  908. package/src/tools/search/output.md +0 -2775
  909. package/src/tools/search/rag-api-reranker.test.ts +0 -891
  910. package/src/tools/search/source-processing.test.ts +0 -516
  911. package/src/tools/search/tavily.test.ts +0 -965
  912. package/src/tools/search/test.html +0 -884
  913. package/src/tools/search/test.md +0 -643
  914. package/src/tools/subagent/__tests__/SubagentExecutionRegistry.test.ts +0 -637
  915. package/src/tools/subagent/__tests__/SubagentExecutor.lazy.test.ts +0 -3054
  916. package/src/tools/subagent/__tests__/childGraphConfig.test.ts +0 -521
  917. package/src/utils/__tests__/apportion.test.ts +0 -32
  918. package/src/utils/__tests__/errors.test.ts +0 -270
  919. package/src/utils/__tests__/fixtures/contextOverflowSignatures.ts +0 -336
  920. package/src/utils/__tests__/redactSecrets.test.ts +0 -56
  921. package/src/utils/__tests__/truncation.test.ts +0 -66
  922. package/src/utils/llm.test.ts +0 -18
  923. package/src/utils/proxy.test.ts +0 -176
  924. package/src/utils/toolContent.test.ts +0 -1432
@@ -1,2733 +0,0 @@
1
- import { z } from 'zod';
2
- import { tmpdir } from 'os';
3
- import { join } from 'path';
4
- import { spawnSync } from 'child_process';
5
- import { tool } from '@langchain/core/tools';
6
- import { AIMessage, ToolMessage } from '@langchain/core/messages';
7
- import {
8
- describe,
9
- it,
10
- expect,
11
- afterEach,
12
- beforeEach,
13
- jest,
14
- } from '@jest/globals';
15
- import {
16
- mkdtemp,
17
- rm,
18
- symlink,
19
- writeFile as fsWriteFile,
20
- readFile as fsReadFile,
21
- } from 'fs/promises';
22
- import type { StructuredToolInterface } from '@langchain/core/tools';
23
- import type { BaseMessage } from '@langchain/core/messages';
24
- import type * as t from '@/types';
25
- import {
26
- executeLocalBash,
27
- executeLocalCode,
28
- validateBashCommand,
29
- _resetLocalEngineWarningsForTests,
30
- } from '../local/LocalExecutionEngine';
31
- import {
32
- createLocalCodingToolBundle,
33
- _resetRipgrepCacheForTests,
34
- } from '../local/LocalCodingTools';
35
- import {
36
- runPostEditSyntaxCheck,
37
- _resetSyntaxCheckProbeCacheForTests,
38
- } from '../local/syntaxCheck';
39
- import { resolveLocalToolsForBinding } from '../local/resolveLocalExecutionTools';
40
- import { WorkspaceClientTimeoutError } from '../local/workspaceFS';
41
- import type { WorkspaceFS } from '../local/workspaceFS';
42
- import { LocalFileCheckpointerImpl } from '../local/FileCheckpointer';
43
- import { createCompileCheckTool } from '../local/CompileCheckTool';
44
- import { runBashAstChecks } from '../local/bashAst';
45
- import { Constants, Providers } from '@/common';
46
- import { ToolNode } from '../ToolNode';
47
-
48
- const hasPython3 = spawnSync('python3', ['--version']).status === 0;
49
-
50
- const tempDirs: string[] = [];
51
-
52
- async function createTempDir(): Promise<string> {
53
- const dir = await mkdtemp(join(tmpdir(), 'lc-local-tools-'));
54
- tempDirs.push(dir);
55
- return dir;
56
- }
57
-
58
- function createRemoteBashStub(): StructuredToolInterface {
59
- return tool(async () => 'remote bash should not run', {
60
- name: Constants.BASH_TOOL,
61
- description: 'Remote bash stub',
62
- schema: z.object({ command: z.string() }),
63
- }) as unknown as StructuredToolInterface;
64
- }
65
-
66
- function messagesFromResult(
67
- result: ToolMessage[] | { messages: ToolMessage[] }
68
- ): ToolMessage[] {
69
- return Array.isArray(result) ? result : result.messages;
70
- }
71
-
72
- function aiMessageWithToolCall(
73
- name: string,
74
- args: Record<string, string | number | boolean>
75
- ): AIMessage {
76
- return new AIMessage({
77
- content: '',
78
- tool_calls: [
79
- {
80
- id: `call_${name}`,
81
- name,
82
- args,
83
- },
84
- ],
85
- });
86
- }
87
-
88
- afterEach(async () => {
89
- await Promise.all(
90
- tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))
91
- );
92
- });
93
-
94
- describe('local execution tools', () => {
95
- it('blocks clearly destructive bash commands by default', async () => {
96
- const result = await validateBashCommand('rm -rf /');
97
-
98
- expect(result.valid).toBe(false);
99
- expect(result.errors.join('\n')).toContain('destructive command pattern');
100
- });
101
-
102
- it('replaces a configured remote bash tool when local mode is enabled', async () => {
103
- const cwd = await createTempDir();
104
- const node = new ToolNode({
105
- tools: [createRemoteBashStub()],
106
- toolExecution: {
107
- engine: 'local',
108
- local: {
109
- cwd,
110
- includeCodingTools: false,
111
- },
112
- },
113
- });
114
-
115
- const result = await node.invoke({
116
- messages: [
117
- aiMessageWithToolCall(Constants.BASH_TOOL, {
118
- command: 'printf local-mode',
119
- }),
120
- ],
121
- });
122
-
123
- const [message] = messagesFromResult(result as { messages: ToolMessage[] });
124
- expect(String(message.content)).toContain('local-mode');
125
- expect(String(message.content)).not.toContain('remote bash should not run');
126
- });
127
-
128
- it('auto-binds the local coding suite in local mode', () => {
129
- const tools = resolveLocalToolsForBinding({
130
- toolExecution: { engine: 'local' },
131
- }) as t.GenericTool[];
132
- const names = tools.map((localTool) => localTool.name);
133
-
134
- expect(names).toEqual(
135
- expect.arrayContaining([
136
- Constants.EXECUTE_CODE,
137
- Constants.BASH_TOOL,
138
- Constants.READ_FILE,
139
- 'write_file',
140
- 'edit_file',
141
- 'grep_search',
142
- 'glob_search',
143
- 'list_directory',
144
- ])
145
- );
146
- });
147
-
148
- it('updates existing code tool bindings when auto-binding is disabled', () => {
149
- const [bashTool] = resolveLocalToolsForBinding({
150
- tools: [createRemoteBashStub()],
151
- toolExecution: {
152
- engine: 'local',
153
- local: { includeCodingTools: false },
154
- },
155
- }) as t.GenericTool[];
156
-
157
- expect(bashTool.name).toBe(Constants.BASH_TOOL);
158
- expect(bashTool.description).toContain('local machine');
159
- });
160
-
161
- it('can call local coding tools from local programmatic execution', async () => {
162
- if (!hasPython3) {
163
- return;
164
- }
165
-
166
- const cwd = await createTempDir();
167
- const node = new ToolNode({
168
- tools: [],
169
- toolExecution: {
170
- engine: 'local',
171
- local: { cwd },
172
- },
173
- });
174
-
175
- const result = await node.invoke({
176
- messages: [
177
- aiMessageWithToolCall(Constants.PROGRAMMATIC_TOOL_CALLING, {
178
- lang: 'py',
179
- code: [
180
- 'await write_file(path="ptc.txt", content="from local ptc")',
181
- 'contents = await read_file(path="ptc.txt")',
182
- 'print(contents)',
183
- ].join('\n'),
184
- }),
185
- ],
186
- });
187
-
188
- const [message] = messagesFromResult(result as { messages: ToolMessage[] });
189
- expect(String(message.content)).toContain('from local ptc');
190
- });
191
-
192
- it('can run bash orchestration through run_tools_with_code in local mode', async () => {
193
- if (!hasPython3) {
194
- return;
195
- }
196
-
197
- const cwd = await createTempDir();
198
- const node = new ToolNode({
199
- tools: [],
200
- toolExecution: {
201
- engine: 'local',
202
- local: { cwd },
203
- },
204
- });
205
-
206
- const result = await node.invoke({
207
- messages: [
208
- aiMessageWithToolCall(Constants.PROGRAMMATIC_TOOL_CALLING, {
209
- code: [
210
- 'write_file \'{"path":"bash-ptc.txt","content":"from bash ptc"}\'',
211
- 'read_file \'{"path":"bash-ptc.txt"}\'',
212
- ].join('\n'),
213
- }),
214
- ],
215
- });
216
-
217
- const [message] = messagesFromResult(result as { messages: ToolMessage[] });
218
- expect(String(message.content)).toContain('from bash ptc');
219
- });
220
- });
221
-
222
- describe('local engine bashAst', () => {
223
- it('flags command substitution in auto mode', () => {
224
- const findings = runBashAstChecks('echo $(whoami)', 'auto');
225
- expect(findings.some((f) => f.code === 'cmd-subst-dollar-paren')).toBe(
226
- true
227
- );
228
- });
229
-
230
- it('escalates command substitution to deny in strict mode', () => {
231
- const findings = runBashAstChecks('echo $(whoami)', 'strict');
232
- const subst = findings.find((f) => f.code === 'cmd-subst-dollar-paren');
233
- expect(subst?.severity).toBe('deny');
234
- });
235
-
236
- it('always denies /proc/<pid>/environ access', () => {
237
- const findings = runBashAstChecks('cat /proc/1/environ', 'auto');
238
- expect(
239
- findings.some(
240
- (f) => f.code === 'proc-environ-read' && f.severity === 'deny'
241
- )
242
- ).toBe(true);
243
- });
244
-
245
- it('never produces findings when off', () => {
246
- const findings = runBashAstChecks('echo $(whoami)', 'off');
247
- expect(findings).toHaveLength(0);
248
- });
249
-
250
- it('blocks bash commands with a deny finding via validateBashCommand', async () => {
251
- const result = await validateBashCommand('cat /proc/1/environ', {
252
- bashAst: 'auto',
253
- });
254
- expect(result.valid).toBe(false);
255
- expect(result.errors.join('\n')).toContain('proc-environ-read');
256
- });
257
- });
258
-
259
- describe('local engine sandbox-off warning', () => {
260
- let warnSpy: jest.SpiedFunction<typeof console.warn>;
261
-
262
- beforeEach(() => {
263
- _resetLocalEngineWarningsForTests();
264
- warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
265
- });
266
-
267
- afterEach(() => {
268
- warnSpy.mockRestore();
269
- });
270
-
271
- it('warns once when running without sandbox', async () => {
272
- // Real (non-internal) executions should warn; the internal
273
- // `bash -n` syntax preflight inside validateBashCommand opts out
274
- // (Codex P2 — otherwise the latch would flip on a probe and hide
275
- // the warning when a genuinely-unsandboxed command later runs).
276
- await executeLocalBash('echo hi');
277
- await executeLocalBash('echo bye');
278
- const sandboxOffMessages = warnSpy.mock.calls.filter((call) =>
279
- String(call[0]).includes('without @anthropic-ai/sandbox-runtime')
280
- );
281
- expect(sandboxOffMessages).toHaveLength(1);
282
- });
283
-
284
- it('does NOT warn for internal probes when the run actually has sandbox enabled (Codex P2)', async () => {
285
- // Pre-fix: validateBashCommand's bash -n preflight (which forces
286
- // sandbox: false for itself, since you can't sandbox a syntax
287
- // probe) would emit a misleading "sandbox is off" warning AND
288
- // flip `sandboxOffWarned = true` even when the run had
289
- // `sandbox.enabled: true` — hiding the warning when a real
290
- // unsandboxed execution later happened. With the fix internal
291
- // probes pass `{ internal: true }` to spawnLocalProcess and
292
- // suppress both the message and the latch.
293
- await validateBashCommand('echo hi', { sandbox: { enabled: true } });
294
- const sandboxOffMessages = warnSpy.mock.calls.filter((call) =>
295
- String(call[0]).includes('without @anthropic-ai/sandbox-runtime')
296
- );
297
- expect(sandboxOffMessages).toHaveLength(0);
298
- });
299
- });
300
-
301
- describe('LocalFileCheckpointer', () => {
302
- it('snapshots and restores existing files', async () => {
303
- const dir = await createTempDir();
304
- const file = join(dir, 'a.txt');
305
- await fsWriteFile(file, 'original', 'utf8');
306
-
307
- const cp = new LocalFileCheckpointerImpl();
308
- await cp.captureBeforeWrite(file);
309
-
310
- await fsWriteFile(file, 'modified', 'utf8');
311
- expect(await fsReadFile(file, 'utf8')).toBe('modified');
312
-
313
- const restored = await cp.rewind();
314
- expect(restored).toBe(1);
315
- expect(await fsReadFile(file, 'utf8')).toBe('original');
316
- });
317
-
318
- it('deletes files that did not exist before the run', async () => {
319
- const dir = await createTempDir();
320
- const file = join(dir, 'new.txt');
321
-
322
- const cp = new LocalFileCheckpointerImpl();
323
- await cp.captureBeforeWrite(file);
324
- await fsWriteFile(file, 'should-be-removed', 'utf8');
325
-
326
- await cp.rewind();
327
- await expect(fsReadFile(file, 'utf8')).rejects.toThrow();
328
- });
329
-
330
- it('rewinds tools created via createLocalCodingToolBundle', async () => {
331
- const cwd = await createTempDir();
332
- const bundle = createLocalCodingToolBundle({
333
- cwd,
334
- fileCheckpointing: true,
335
- });
336
- expect(bundle.checkpointer).toBeDefined();
337
-
338
- const writeTool = bundle.tools.find((tool_) => tool_.name === 'write_file');
339
- expect(writeTool).toBeDefined();
340
- await writeTool!.invoke({ path: 'cp.txt', content: 'first' });
341
- await writeTool!.invoke({ path: 'cp.txt', content: 'second' });
342
-
343
- const restored = await bundle.checkpointer!.rewind();
344
- expect(restored).toBe(1);
345
- await expect(fsReadFile(join(cwd, 'cp.txt'), 'utf8')).rejects.toThrow();
346
- });
347
- });
348
-
349
- describe('local read tool guards', () => {
350
- it('refuses to read files containing NUL bytes', async () => {
351
- const cwd = await createTempDir();
352
- const binary = join(cwd, 'binary.bin');
353
- await fsWriteFile(binary, Buffer.from([0x00, 0x01, 0x02]));
354
-
355
- const bundle = createLocalCodingToolBundle({ cwd });
356
- const readTool = bundle.tools.find((t_) => t_.name === Constants.READ_FILE);
357
- const result = await readTool!.invoke({ path: 'binary.bin' });
358
- expect(String(result)).toContain('binary file');
359
- });
360
-
361
- it('returns a stub instead of OOMing on huge files', async () => {
362
- const cwd = await createTempDir();
363
- const big = join(cwd, 'big.txt');
364
- await fsWriteFile(big, 'x'.repeat(2048));
365
-
366
- const bundle = createLocalCodingToolBundle({
367
- cwd,
368
- maxReadBytes: 1024,
369
- });
370
- const readTool = bundle.tools.find((t_) => t_.name === Constants.READ_FILE);
371
- const result = await readTool!.invoke({ path: 'big.txt' });
372
- expect(String(result)).toContain('exceeds the 1024-byte read cap');
373
- });
374
-
375
- it('rejects symlink escapes', async () => {
376
- const cwd = await createTempDir();
377
- const outside = await createTempDir();
378
- const secret = join(outside, 'secret.txt');
379
- await fsWriteFile(secret, 'top-secret', 'utf8');
380
- await symlink(outside, join(cwd, 'escape'));
381
-
382
- const bundle = createLocalCodingToolBundle({ cwd });
383
- const readTool = bundle.tools.find((t_) => t_.name === Constants.READ_FILE);
384
- await expect(
385
- readTool!.invoke({ path: 'escape/secret.txt' })
386
- ).rejects.toThrow(/symlink escape/);
387
- });
388
- });
389
-
390
- describe('local programmatic bridge auth', () => {
391
- it('rejects unauthenticated requests to the local bridge', async () => {
392
- if (!hasPython3) {
393
- return;
394
- }
395
- const cwd = await createTempDir();
396
- const node = new ToolNode({
397
- tools: [],
398
- toolExecution: {
399
- engine: 'local',
400
- local: { cwd },
401
- },
402
- });
403
-
404
- const result = await node.invoke({
405
- messages: [
406
- aiMessageWithToolCall(Constants.PROGRAMMATIC_TOOL_CALLING, {
407
- lang: 'py',
408
- code: [
409
- 'import os, json, urllib.request, urllib.error',
410
- 'url = os.environ["BRIDGE_PROBE_URL"] if "BRIDGE_PROBE_URL" in os.environ else __LIBRECHAT_TOOL_BRIDGE',
411
- 'body = json.dumps({"name":"read_file","input":{"path":"x"}}).encode("utf-8")',
412
- 'try:',
413
- ' req = urllib.request.Request(url, data=body, headers={"Content-Type":"application/json"}, method="POST")',
414
- ' urllib.request.urlopen(req, timeout=5)',
415
- ' print("LEAK")',
416
- 'except urllib.error.HTTPError as e:',
417
- ' print(f"AUTH={e.code}")',
418
- ].join('\n'),
419
- }),
420
- ],
421
- });
422
-
423
- const [message] = messagesFromResult(result as { messages: ToolMessage[] });
424
- expect(String(message.content)).toContain('AUTH=401');
425
- expect(String(message.content)).not.toContain('LEAK');
426
- });
427
- });
428
-
429
- describe('local edit fuzzy matching', () => {
430
- it('falls back to line-trimmed when trailing whitespace differs', async () => {
431
- const cwd = await createTempDir();
432
- const file = join(cwd, 'a.ts');
433
- // Real file has trailing whitespace on every line.
434
- await fsWriteFile(
435
- file,
436
- 'function greet(name: string) { \n return `Hello, ${name}!`; \n}\n',
437
- 'utf8'
438
- );
439
-
440
- const bundle = createLocalCodingToolBundle({ cwd });
441
- const editTool = bundle.tools.find((tt) => tt.name === 'edit_file');
442
- const result = await editTool!.invoke({
443
- path: 'a.ts',
444
- // LLM emits a trailing-whitespace-stripped version.
445
- old_text:
446
- 'function greet(name: string) {\n return `Hello, ${name}!`;\n}',
447
- new_text: 'function greet(name: string) {\n return `Hi, ${name}!`;\n}',
448
- });
449
- expect(String(result)).toContain('strategies: line-trimmed');
450
- const after = await fsReadFile(file, 'utf8');
451
- expect(after).toContain('Hi, ${name}!');
452
- });
453
-
454
- it('falls back to indentation-flexible when LLM strips leading indent', async () => {
455
- const cwd = await createTempDir();
456
- const file = join(cwd, 'a.ts');
457
- await fsWriteFile(
458
- file,
459
- 'class Foo {\n method() {\n return 1;\n }\n}\n',
460
- 'utf8'
461
- );
462
-
463
- const bundle = createLocalCodingToolBundle({ cwd });
464
- const editTool = bundle.tools.find((tt) => tt.name === 'edit_file');
465
- const result = await editTool!.invoke({
466
- path: 'a.ts',
467
- // LLM stripped the 4-space indent
468
- old_text: 'method() {\n return 1;\n}',
469
- new_text: 'method() {\n return 42;\n}',
470
- });
471
- expect(String(result)).toMatch(
472
- /strategies: (indentation-flexible|whitespace-normalized)/
473
- );
474
- const after = await fsReadFile(file, 'utf8');
475
- expect(after).toContain('return 42;');
476
- });
477
-
478
- it('returns a unified diff in the tool result', async () => {
479
- const cwd = await createTempDir();
480
- const file = join(cwd, 'a.txt');
481
- await fsWriteFile(file, 'first\nsecond\nthird\n', 'utf8');
482
- const bundle = createLocalCodingToolBundle({ cwd });
483
- const editTool = bundle.tools.find((tt) => tt.name === 'edit_file');
484
- const result = await editTool!.invoke({
485
- path: 'a.txt',
486
- old_text: 'second',
487
- new_text: 'SECOND',
488
- });
489
- const text = String(result);
490
- expect(text).toContain('Diff:');
491
- expect(text).toContain('-second');
492
- expect(text).toContain('+SECOND');
493
- });
494
-
495
- it('preserves CRLF line endings on edit', async () => {
496
- const cwd = await createTempDir();
497
- const file = join(cwd, 'a.txt');
498
- await fsWriteFile(file, 'one\r\ntwo\r\nthree\r\n', 'utf8');
499
- const bundle = createLocalCodingToolBundle({ cwd });
500
- const editTool = bundle.tools.find((tt) => tt.name === 'edit_file');
501
- await editTool!.invoke({
502
- path: 'a.txt',
503
- old_text: 'two',
504
- new_text: 'TWO',
505
- });
506
- const raw = await fsReadFile(file, 'utf8');
507
- expect(raw).toBe('one\r\nTWO\r\nthree\r\n');
508
- });
509
-
510
- it('preserves UTF-8 BOM on overwrite', async () => {
511
- const cwd = await createTempDir();
512
- const file = join(cwd, 'a.txt');
513
- const BOM = '';
514
- await fsWriteFile(file, BOM + 'hello\n', 'utf8');
515
- const bundle = createLocalCodingToolBundle({ cwd });
516
- const writeTool = bundle.tools.find((tt) => tt.name === 'write_file');
517
- await writeTool!.invoke({ path: 'a.txt', content: 'goodbye\n' });
518
- const raw = await fsReadFile(file, 'utf8');
519
- expect(raw.startsWith(BOM)).toBe(true);
520
- expect(raw.slice(1)).toBe('goodbye\n');
521
- });
522
- });
523
-
524
- describe('local read attachments', () => {
525
- // Smallest valid 1x1 PNG.
526
- const tinyPng = Buffer.from(
527
- '89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000a49444154789c63000100000005000165be7e6e0000000049454e44ae426082',
528
- 'hex'
529
- );
530
-
531
- it('returns binary stub by default', async () => {
532
- const cwd = await createTempDir();
533
- const file = join(cwd, 'tiny.png');
534
- await fsWriteFile(file, tinyPng);
535
- const bundle = createLocalCodingToolBundle({ cwd });
536
- const readTool = bundle.tools.find((tt) => tt.name === Constants.READ_FILE);
537
- const result = await readTool!.invoke({ path: 'tiny.png' });
538
- expect(String(result)).toContain('binary file');
539
- });
540
-
541
- it('returns an image_url content block when attachReadAttachments=images-only', async () => {
542
- const cwd = await createTempDir();
543
- const file = join(cwd, 'tiny.png');
544
- await fsWriteFile(file, tinyPng);
545
-
546
- const bundle = createLocalCodingToolBundle({
547
- cwd,
548
- attachReadAttachments: 'images-only',
549
- });
550
- const readTool = bundle.tools.find((tt) => tt.name === Constants.READ_FILE);
551
- // Invoking via a tool_call envelope (rather than raw args) is what
552
- // makes the LangChain tool wrap the result as a ToolMessage with
553
- // `.content` and `.artifact` populated.
554
- const message = (await readTool!.invoke({
555
- id: 'call_image',
556
- name: Constants.READ_FILE,
557
- args: { path: 'tiny.png' },
558
- type: 'tool_call',
559
- })) as { content: unknown; artifact: unknown };
560
- expect(Array.isArray(message.content)).toBe(true);
561
- const blocks = message.content as Array<{
562
- type: string;
563
- image_url?: { url: string };
564
- }>;
565
- const imageBlock = blocks.find((b) => b.type === 'image_url');
566
- expect(imageBlock?.image_url?.url).toMatch(/^data:image\/png;base64,/);
567
- expect(blocks.find((b) => b.type === 'text')).toBeDefined();
568
- expect(message.artifact).toMatchObject({
569
- mime: 'image/png',
570
- attachment: 'image',
571
- });
572
- });
573
-
574
- it('refuses oversize images even when embedding is on', async () => {
575
- const cwd = await createTempDir();
576
- const file = join(cwd, 'big.png');
577
- // Forge a "PNG" larger than the cap. It will sniff as a generic
578
- // binary; classifyAttachment returns 'binary' since file-type
579
- // won't recognise the bytes — that's fine, we just want to
580
- // verify the oversize gate is reachable. So instead, build a
581
- // real big PNG by concatenating chunks with a fake IDAT.
582
- // Easier: keep the tiny PNG header but pad to 200 bytes; cap to 100.
583
- const padded = Buffer.concat([
584
- tinyPng,
585
- Buffer.alloc(200 - tinyPng.length, 0),
586
- ]);
587
- await fsWriteFile(file, padded);
588
- const bundle = createLocalCodingToolBundle({
589
- cwd,
590
- attachReadAttachments: 'images-only',
591
- maxAttachmentBytes: 100,
592
- });
593
- const readTool = bundle.tools.find((tt) => tt.name === Constants.READ_FILE);
594
- const result = await readTool!.invoke({ path: 'big.png' });
595
- expect(String(result)).toMatch(/Refusing to embed/);
596
- });
597
-
598
- it('still reads text files normally when embedding is on', async () => {
599
- const cwd = await createTempDir();
600
- const file = join(cwd, 'a.txt');
601
- await fsWriteFile(file, 'hello world\n', 'utf8');
602
- const bundle = createLocalCodingToolBundle({
603
- cwd,
604
- attachReadAttachments: 'images-only',
605
- });
606
- const readTool = bundle.tools.find((tt) => tt.name === Constants.READ_FILE);
607
- const result = await readTool!.invoke({ path: 'a.txt' });
608
- expect(String(result)).toContain('hello world');
609
- });
610
- });
611
-
612
- describe('post-edit syntax check', () => {
613
- beforeEach(() => {
614
- _resetSyntaxCheckProbeCacheForTests();
615
- });
616
-
617
- it('flags broken JS via node --check', async () => {
618
- const cwd = await createTempDir();
619
- const file = join(cwd, 'broken.js');
620
- await fsWriteFile(file, 'function (\n', 'utf8');
621
- const outcome = await runPostEditSyntaxCheck(file, {});
622
- expect(outcome).not.toBeNull();
623
- expect(outcome!.ok).toBe(false);
624
- if (outcome!.ok === false) {
625
- expect(outcome!.checker).toBe('node --check');
626
- expect(outcome!.output.length).toBeGreaterThan(0);
627
- }
628
- });
629
-
630
- it('passes valid JS', async () => {
631
- const cwd = await createTempDir();
632
- const file = join(cwd, 'good.js');
633
- await fsWriteFile(file, 'console.log(1)\n', 'utf8');
634
- const outcome = await runPostEditSyntaxCheck(file, {});
635
- expect(outcome?.ok).toBe(true);
636
- });
637
-
638
- it('flags broken JSON via JSON.parse', async () => {
639
- const cwd = await createTempDir();
640
- const file = join(cwd, 'broken.json');
641
- await fsWriteFile(file, '{ "x": ', 'utf8');
642
- const outcome = await runPostEditSyntaxCheck(file, {});
643
- expect(outcome?.ok).toBe(false);
644
- if (outcome!.ok === false) {
645
- expect(outcome!.checker).toBe('JSON.parse');
646
- }
647
- });
648
-
649
- it('returns null for unknown extensions', async () => {
650
- const cwd = await createTempDir();
651
- const file = join(cwd, 'random.xyz');
652
- await fsWriteFile(file, 'whatever\n', 'utf8');
653
- const outcome = await runPostEditSyntaxCheck(file, {});
654
- expect(outcome).toBeNull();
655
- });
656
-
657
- it('write_file appends syntax-check warning when postEditSyntaxCheck=auto', async () => {
658
- const cwd = await createTempDir();
659
- const bundle = createLocalCodingToolBundle({
660
- cwd,
661
- postEditSyntaxCheck: 'auto',
662
- });
663
- const writeTool = bundle.tools.find((tt) => tt.name === 'write_file');
664
- const message = (await writeTool!.invoke({
665
- id: 'call_w',
666
- name: 'write_file',
667
- args: { path: 'broken.js', content: 'function (\n' },
668
- type: 'tool_call',
669
- })) as { content: string; artifact: { syntax_error?: string } };
670
- expect(message.content).toContain('[syntax-check warning');
671
- expect(message.artifact.syntax_error).toBe('node --check');
672
- });
673
-
674
- it('write_file in strict mode throws on syntax error', async () => {
675
- const cwd = await createTempDir();
676
- const bundle = createLocalCodingToolBundle({
677
- cwd,
678
- postEditSyntaxCheck: 'strict',
679
- });
680
- const writeTool = bundle.tools.find((tt) => tt.name === 'write_file');
681
- await expect(
682
- writeTool!.invoke({
683
- id: 'call_w',
684
- name: 'write_file',
685
- args: { path: 'broken.js', content: 'function (\n' },
686
- type: 'tool_call',
687
- })
688
- ).rejects.toThrow(/syntax check failed/);
689
- });
690
- });
691
-
692
- describe('compile_check', () => {
693
- it('reports "no recognised project marker" when there are none', async () => {
694
- const cwd = await createTempDir();
695
- const checkTool = createCompileCheckTool({ cwd });
696
- const message = (await checkTool.invoke({
697
- id: 'call_c',
698
- name: 'compile_check',
699
- args: {},
700
- type: 'tool_call',
701
- })) as { content: string; artifact: { ran: boolean; kind: string } };
702
- expect(message.content).toContain('no recognised project marker');
703
- expect(message.artifact.ran).toBe(false);
704
- expect(message.artifact.kind).toBe('unknown');
705
- });
706
-
707
- it('honours an explicit command override and reports exit code', async () => {
708
- const cwd = await createTempDir();
709
- const checkTool = createCompileCheckTool({ cwd });
710
- const message = (await checkTool.invoke({
711
- id: 'call_c2',
712
- name: 'compile_check',
713
- args: { command: 'echo hello && false' },
714
- type: 'tool_call',
715
- })) as {
716
- content: string;
717
- artifact: { passed: boolean; exit_code: number | null };
718
- };
719
- expect(message.content).toContain('FAILED');
720
- expect(message.content).toContain('hello');
721
- expect(message.artifact.passed).toBe(false);
722
- expect(message.artifact.exit_code).not.toBe(0);
723
- });
724
- });
725
-
726
- describe('local search fallback', () => {
727
- beforeEach(() => {
728
- _resetRipgrepCacheForTests();
729
- });
730
-
731
- it('finds matches via the Node fallback when ripgrep is missing', async () => {
732
- const cwd = await createTempDir();
733
- await fsWriteFile(join(cwd, 'a.ts'), 'const needle = 42;\n', 'utf8');
734
- await fsWriteFile(join(cwd, 'b.ts'), 'const haystack = 1;\n', 'utf8');
735
-
736
- const bundle = createLocalCodingToolBundle({
737
- cwd,
738
- env: { PATH: '/nonexistent' },
739
- });
740
- const grepTool = bundle.tools.find((t_) => t_.name === 'grep_search');
741
- const result = await grepTool!.invoke({ pattern: 'needle' });
742
- expect(String(result)).toContain('a.ts');
743
- expect(String(result)).toContain('needle');
744
- });
745
- });
746
-
747
- describe('codex review fixes', () => {
748
- describe('executeLocalCode bash args (Codex P2 #1)', () => {
749
- it('passes input.args as positional shell parameters when lang is bash', async () => {
750
- const cwd = await createTempDir();
751
- const result = await executeLocalCode(
752
- {
753
- lang: 'bash',
754
- // Echo every positional arg space-separated. With the bug,
755
- // $@ is empty because args were dropped.
756
- code: 'echo "args:$@"',
757
- args: ['hello', 'world'],
758
- },
759
- { cwd }
760
- );
761
- expect(result.exitCode).toBe(0);
762
- expect(result.stdout.trim()).toBe('args:hello world');
763
- });
764
-
765
- it('still works when lang is bash and args is missing', async () => {
766
- const cwd = await createTempDir();
767
- const result = await executeLocalCode(
768
- { lang: 'bash', code: 'echo plain' },
769
- { cwd }
770
- );
771
- expect(result.exitCode).toBe(0);
772
- expect(result.stdout.trim()).toBe('plain');
773
- });
774
- });
775
-
776
- describe('ripgrep cache backend scope (Codex P2 #2)', () => {
777
- it('does not bleed an "rg available" verdict from one backend to another', async () => {
778
- // Backend A: pretends rg works (returns a fake spawn whose
779
- // process exits 0 on every call). The cache should record true
780
- // for THIS backend.
781
- const okBackend = jest.fn(
782
- (cmd: string, _args: string[], _opts: unknown) => {
783
- const ok = require('child_process').spawn('echo', [cmd]);
784
- return ok;
785
- }
786
- ) as unknown as t.LocalSpawn;
787
- // Backend B: pretends rg does not exist (returns a child that
788
- // exits 127, the "command not found" code).
789
- const missingBackend = jest.fn(
790
- (_cmd: string, _args: string[], _opts: unknown) => {
791
- const child = require('child_process').spawn('sh', [
792
- '-c',
793
- 'exit 127',
794
- ]);
795
- return child;
796
- }
797
- ) as unknown as t.LocalSpawn;
798
-
799
- _resetRipgrepCacheForTests();
800
-
801
- // Build two bundles with distinct backends.
802
- const cwdA = await createTempDir();
803
- const cwdB = await createTempDir();
804
- await fsWriteFile(join(cwdA, 'a.ts'), 'needle\n', 'utf8');
805
- await fsWriteFile(join(cwdB, 'b.ts'), 'needle\n', 'utf8');
806
-
807
- const bundleA = createLocalCodingToolBundle({
808
- cwd: cwdA,
809
- exec: { spawn: okBackend },
810
- });
811
- const bundleB = createLocalCodingToolBundle({
812
- cwd: cwdB,
813
- exec: { spawn: missingBackend },
814
- });
815
-
816
- // Run grep against A first — populates cache for A's backend.
817
- await bundleA.tools
818
- .find((t_) => t_.name === 'grep_search')!
819
- .invoke({
820
- pattern: 'needle',
821
- });
822
- // Run grep against B — must NOT see cached "true" from A's
823
- // backend. With the bug, B would try to spawn rg, fail, and
824
- // throw instead of falling back to the Node walker.
825
- const bResult = await bundleB.tools
826
- .find((t_) => t_.name === 'grep_search')!
827
- .invoke({ pattern: 'needle' });
828
- expect(String(bResult)).toContain('needle');
829
- });
830
- });
831
-
832
- describe('additionalRoots resolved against workspace root (Codex P2 #3)', () => {
833
- it('treats relative additionalRoots as siblings of root, not of process.cwd', async () => {
834
- const parent = await createTempDir();
835
- const fs = await import('fs/promises');
836
- await fs.mkdir(join(parent, 'app'), { recursive: true });
837
- await fs.mkdir(join(parent, 'shared'), { recursive: true });
838
- await fsWriteFile(join(parent, 'shared/lib.ts'), 'X\n', 'utf8');
839
-
840
- const bundle = createLocalCodingToolBundle({
841
- workspace: {
842
- root: join(parent, 'app'),
843
- additionalRoots: ['../shared'],
844
- },
845
- });
846
- const readTool = bundle.tools.find(
847
- (t_) => t_.name === Constants.READ_FILE
848
- );
849
- // Without the fix, '../shared/lib.ts' would resolve relative to
850
- // process.cwd (this test runner), miss the boundary check, and
851
- // throw "Path is outside the local workspace".
852
- const result = await readTool!.invoke({
853
- id: 'c',
854
- name: Constants.READ_FILE,
855
- args: { path: join(parent, 'shared/lib.ts') },
856
- type: 'tool_call',
857
- });
858
- expect(JSON.stringify(result)).toContain('X');
859
- });
860
- });
861
- });
862
-
863
- describe('codex review fixes (round 2)', () => {
864
- describe('streaming output cap (Codex P1)', () => {
865
- const {
866
- spawnLocalProcess,
867
- _resetLocalEngineWarningsForTests: _,
868
- } = require('../local/LocalExecutionEngine');
869
-
870
- it('hard-kills the child when total streamed bytes exceed maxSpawnedBytes', async () => {
871
- // Cap at 64 KiB. `yes` would otherwise run unbounded.
872
- const start = Date.now();
873
- const result = await spawnLocalProcess('yes', [], {
874
- timeoutMs: 30_000,
875
- maxSpawnedBytes: 64 * 1024,
876
- sandbox: { enabled: false },
877
- });
878
- const elapsed = Date.now() - start;
879
- // Killed promptly (much sooner than the 30s timeout).
880
- expect(elapsed).toBeLessThan(5000);
881
- // Process was killed by the overflow guard, not by timeout.
882
- expect(result.timedOut).toBe(false);
883
- expect(result.exitCode).not.toBe(0);
884
- // We DID see some output before the kill.
885
- expect(result.stdout.length).toBeGreaterThan(0);
886
- });
887
-
888
- it('spills overflow to a temp file (full output recoverable post-cap)', async () => {
889
- // Generate ~200 KiB of output with a 32 KiB inline cap → spill.
890
- const result = await spawnLocalProcess(
891
- 'bash',
892
- ['-c', 'head -c 200000 /dev/urandom | base64 | head -c 200000'],
893
- {
894
- timeoutMs: 10_000,
895
- maxOutputChars: 8_000, // inline cap = 16 KiB; ~200 KiB → overflow
896
- maxSpawnedBytes: 1024 * 1024, // 1 MiB hard cap
897
- sandbox: { enabled: false },
898
- }
899
- );
900
- expect(result.exitCode).toBe(0);
901
- expect(result.fullOutputPath).toBeTruthy();
902
- const fs = await import('fs/promises');
903
- const spilled = await fs.readFile(
904
- result.fullOutputPath as string,
905
- 'utf8'
906
- );
907
- // The spill file holds more bytes than the in-memory truncation.
908
- expect(spilled.length).toBeGreaterThan(result.stdout.length);
909
- });
910
-
911
- it('does not create a spill file for small outputs', async () => {
912
- const result = await spawnLocalProcess('bash', ['-c', 'echo small'], {
913
- timeoutMs: 5_000,
914
- sandbox: { enabled: false },
915
- });
916
- expect(result.fullOutputPath).toBeUndefined();
917
- expect(result.stdout.trim()).toBe('small');
918
- });
919
- });
920
-
921
- describe('bash_tool args (Codex P2)', () => {
922
- it('populates positional shell parameters from input.args', async () => {
923
- const cwd = await createTempDir();
924
- const bundle = createLocalCodingToolBundle({ cwd });
925
- const bashTool = bundle.tools.find(
926
- (tt) => tt.name === Constants.BASH_TOOL
927
- );
928
- const result = await bashTool!.invoke({
929
- id: 'b1',
930
- name: Constants.BASH_TOOL,
931
- args: {
932
- command: 'echo "first=$1 second=$2"',
933
- args: ['hello', 'world'],
934
- },
935
- type: 'tool_call',
936
- });
937
- const text = JSON.stringify(result);
938
- expect(text).toContain('first=hello second=world');
939
- });
940
-
941
- it('still works when args is missing', async () => {
942
- const cwd = await createTempDir();
943
- const bundle = createLocalCodingToolBundle({ cwd });
944
- const bashTool = bundle.tools.find(
945
- (tt) => tt.name === Constants.BASH_TOOL
946
- );
947
- const result = await bashTool!.invoke({
948
- id: 'b2',
949
- name: Constants.BASH_TOOL,
950
- args: { command: 'echo plain' },
951
- type: 'tool_call',
952
- });
953
- expect(JSON.stringify(result)).toContain('plain');
954
- });
955
- });
956
- });
957
-
958
- describe('codex review fixes (round 3)', () => {
959
- describe('validateBashCommand honours configured shell (Codex P1 #6)', () => {
960
- it('routes the -n preflight through `local.shell` when set', async () => {
961
- // Spawn calls go through the config'd backend; intercept and
962
- // assert which shell binary the syntax check picks.
963
- const calls: string[] = [];
964
- const intercept: t.LocalSpawn = ((
965
- command: string,
966
- args: string[],
967
- opts: import('child_process').SpawnOptions
968
- ) => {
969
- calls.push(command);
970
- // Fall through to a real spawn so the call resolves cleanly.
971
- const { spawn: realSpawn } =
972
- require('child_process') as typeof import('child_process');
973
- return realSpawn(command, args, opts);
974
- }) as unknown as t.LocalSpawn;
975
-
976
- const result = await validateBashCommand('echo ok', {
977
- shell: '/bin/sh',
978
- exec: { spawn: intercept },
979
- });
980
- expect(result.valid).toBe(true);
981
- // The very first call is the syntax-check spawn; assert it used
982
- // /bin/sh and not the DEFAULT_SHELL fallback.
983
- expect(calls[0]).toBe('/bin/sh');
984
- });
985
- });
986
-
987
- describe('syntax-check probe cache is backend-keyed (Codex P2 #7)', () => {
988
- it('does not bleed an "rg/node/python available" verdict from one backend to another', async () => {
989
- _resetSyntaxCheckProbeCacheForTests();
990
-
991
- // Backend A: probes succeed (real spawn).
992
- const realSpawn = (
993
- require('child_process') as typeof import('child_process')
994
- ).spawn;
995
- const okBackend: t.LocalSpawn = ((
996
- cmd: string,
997
- args: string[],
998
- opts: import('child_process').SpawnOptions
999
- ) => realSpawn(cmd, args, opts)) as unknown as t.LocalSpawn;
1000
- // Backend B: probes always fail with exit 127.
1001
- const missingBackend: t.LocalSpawn = ((
1002
- _cmd: string,
1003
- _args: string[],
1004
- opts: import('child_process').SpawnOptions
1005
- ) =>
1006
- realSpawn('sh', ['-c', 'exit 127'], opts)) as unknown as t.LocalSpawn;
1007
-
1008
- const cwdA = await createTempDir();
1009
- const cwdB = await createTempDir();
1010
- // Write a broken JS file we want syntax-checked.
1011
- await fsWriteFile(join(cwdA, 'a.js'), 'function (\n', 'utf8');
1012
- await fsWriteFile(join(cwdB, 'b.js'), 'function (\n', 'utf8');
1013
-
1014
- // Run on backend A — succeeds, populates A's probe cache for `node`.
1015
- const a = await runPostEditSyntaxCheck(join(cwdA, 'a.js'), {
1016
- cwd: cwdA,
1017
- exec: { spawn: okBackend },
1018
- });
1019
- expect(a?.ok).toBe(false);
1020
-
1021
- // Run on backend B — must NOT see A's cached "node available".
1022
- // With the bug, B would assume `node` works (skipping the probe),
1023
- // try to run `node --check`, get exit 127 from the missingBackend,
1024
- // and return ok=false with a misleading checker.
1025
- // With the fix: B's own probe runs, sees node is missing on this
1026
- // backend, and skips the syntax check (returns ok=true).
1027
- const b = await runPostEditSyntaxCheck(join(cwdB, 'b.js'), {
1028
- cwd: cwdB,
1029
- exec: { spawn: missingBackend },
1030
- });
1031
- expect(b?.ok).toBe(true);
1032
- });
1033
- });
1034
-
1035
- describe('grep passes pattern via -e (Codex P2 #8)', () => {
1036
- it('handles dash-prefixed patterns without rg interpreting them as flags', async () => {
1037
- const cwd = await createTempDir();
1038
- // File contains a literal "-foo" we want to find.
1039
- await fsWriteFile(
1040
- join(cwd, 'flags.txt'),
1041
- 'before\n-foo bar\nafter\n',
1042
- 'utf8'
1043
- );
1044
- const bundle = createLocalCodingToolBundle({ cwd });
1045
- const grepTool = bundle.tools.find((t_) => t_.name === 'grep_search');
1046
- const result = await grepTool!.invoke({
1047
- id: 'g1',
1048
- name: 'grep_search',
1049
- args: { pattern: '-foo' },
1050
- type: 'tool_call',
1051
- });
1052
- const text = JSON.stringify(result);
1053
- // Pre-fix, rg would parse "-foo" as a flag and bail out.
1054
- // Post-fix, "-foo" is matched and the line shows up.
1055
- expect(text).toContain('-foo bar');
1056
- });
1057
- });
1058
- });
1059
-
1060
- describe('codex review fixes (round 4)', () => {
1061
- describe('quoted destructive targets (Codex P1 #9)', () => {
1062
- it('blocks rm -rf "/" (target inside double quotes)', async () => {
1063
- const result = await validateBashCommand('rm -rf "/"');
1064
- expect(result.valid).toBe(false);
1065
- expect(result.errors.join('\n')).toContain('destructive command pattern');
1066
- });
1067
-
1068
- it('blocks rm -rf "$HOME" (env-quoted target)', async () => {
1069
- const result = await validateBashCommand('rm -rf "$HOME"');
1070
- expect(result.valid).toBe(false);
1071
- expect(result.errors.join('\n')).toContain('destructive command pattern');
1072
- });
1073
-
1074
- it('blocks rm -rf \'/\' (target inside single quotes)', async () => {
1075
- const result = await validateBashCommand('rm -rf \'/\'');
1076
- expect(result.valid).toBe(false);
1077
- expect(result.errors.join('\n')).toContain('destructive command pattern');
1078
- });
1079
-
1080
- it('blocks chmod -R 777 "/"', async () => {
1081
- const result = await validateBashCommand('chmod -R 777 "/"');
1082
- expect(result.valid).toBe(false);
1083
- expect(result.errors.join('\n')).toContain('destructive command pattern');
1084
- });
1085
-
1086
- it('still blocks unquoted forms (no regression)', async () => {
1087
- const result = await validateBashCommand('rm -rf /');
1088
- expect(result.valid).toBe(false);
1089
- });
1090
-
1091
- it('does not flag the print-only case echo "rm -rf /"', async () => {
1092
- // The destructive-target inside `echo "..."` is wrapped by the
1093
- // OUTER quotes only — there's no quote pair around the `/`
1094
- // itself — so the quoted-pattern pass should not match.
1095
- const result = await validateBashCommand('echo "rm -rf /"');
1096
- expect(result.valid).toBe(true);
1097
- });
1098
- });
1099
- });
1100
-
1101
- describe('codex review fixes (round 5)', () => {
1102
- describe('maxSpawnedBytes=0 disables the cap (Codex P2 #11)', () => {
1103
- const { spawnLocalProcess } = require('../local/LocalExecutionEngine');
1104
-
1105
- it('does not kill on first byte when maxSpawnedBytes is 0', async () => {
1106
- // Without the fix, `totalSpawnedBytes > 0` triggers on the first
1107
- // byte and the process tree gets killed before `echo` can finish.
1108
- const result = await spawnLocalProcess('bash', ['-c', 'echo hello'], {
1109
- timeoutMs: 5_000,
1110
- maxSpawnedBytes: 0,
1111
- sandbox: { enabled: false },
1112
- });
1113
- expect(result.exitCode).toBe(0);
1114
- expect(result.timedOut).toBe(false);
1115
- expect(result.stdout.trim()).toBe('hello');
1116
- });
1117
-
1118
- it('lets a moderately noisy command run to completion when cap is 0', async () => {
1119
- // Emit ~40 KiB. Default cap (50 MiB) would also let this through,
1120
- // but the explicit 0 must not flip into the kill path.
1121
- const result = await spawnLocalProcess(
1122
- 'bash',
1123
- ['-c', 'head -c 40000 /dev/urandom | base64 | head -c 40000'],
1124
- {
1125
- timeoutMs: 10_000,
1126
- maxOutputChars: 200_000,
1127
- maxSpawnedBytes: 0,
1128
- sandbox: { enabled: false },
1129
- }
1130
- );
1131
- expect(result.exitCode).toBe(0);
1132
- expect(result.timedOut).toBe(false);
1133
- expect(result.stdout.length).toBeGreaterThan(0);
1134
- });
1135
- });
1136
-
1137
- describe('spill path is ESM-safe (Codex P1 #12)', () => {
1138
- // The spill path used to do `require('fs')` inside an ESM-shipped
1139
- // module — fine in CJS test runs, would throw `ReferenceError` in
1140
- // any ESM consumer that triggered the overflow path. Pin the
1141
- // happy path here; the static `createWriteStream` import means a
1142
- // ReferenceError would surface as a test failure regardless of
1143
- // which build runs the test.
1144
-
1145
- const { spawnLocalProcess } = require('../local/LocalExecutionEngine');
1146
-
1147
- it('writes a spill file without a runtime require', async () => {
1148
- const result = await spawnLocalProcess(
1149
- 'bash',
1150
- ['-c', 'head -c 40000 /dev/urandom | base64 | head -c 40000'],
1151
- {
1152
- timeoutMs: 10_000,
1153
- // tiny inline cap → guaranteed overflow → ensureSpill() runs
1154
- maxOutputChars: 4_000,
1155
- maxSpawnedBytes: 1024 * 1024,
1156
- sandbox: { enabled: false },
1157
- }
1158
- );
1159
- expect(result.exitCode).toBe(0);
1160
- expect(result.fullOutputPath).toBeTruthy();
1161
- const fs = await import('fs/promises');
1162
- const spilled = await fs.readFile(
1163
- result.fullOutputPath as string,
1164
- 'utf8'
1165
- );
1166
- expect(spilled.length).toBeGreaterThan(result.stdout.length);
1167
- });
1168
- });
1169
-
1170
- describe('sandbox config: loopback bridge access (Codex P1 #14)', () => {
1171
- const {
1172
- buildSandboxRuntimeConfig,
1173
- } = require('../local/LocalExecutionEngine');
1174
-
1175
- it('seeds allowedDomains with loopback hosts so the bridge works under sandbox', () => {
1176
- const cfg = buildSandboxRuntimeConfig({}, '/tmp/ws', () => []);
1177
- expect(cfg.network.allowedDomains).toEqual(
1178
- expect.arrayContaining(['127.0.0.1', 'localhost', '::1'])
1179
- );
1180
- });
1181
-
1182
- it('keeps user-supplied allowedDomains and does not duplicate loopback', () => {
1183
- const cfg = buildSandboxRuntimeConfig(
1184
- {
1185
- sandbox: {
1186
- network: { allowedDomains: ['api.example.com', '127.0.0.1'] },
1187
- },
1188
- },
1189
- '/tmp/ws',
1190
- () => []
1191
- );
1192
- const occurrences = cfg.network.allowedDomains.filter(
1193
- (d: string) => d === '127.0.0.1'
1194
- ).length;
1195
- expect(occurrences).toBe(1);
1196
- expect(cfg.network.allowedDomains).toContain('api.example.com');
1197
- });
1198
-
1199
- it('respects deniedDomains overriding the loopback seed', () => {
1200
- const cfg = buildSandboxRuntimeConfig(
1201
- { sandbox: { network: { deniedDomains: ['127.0.0.1'] } } },
1202
- '/tmp/ws',
1203
- () => []
1204
- );
1205
- expect(cfg.network.allowedDomains).not.toContain('127.0.0.1');
1206
- // The other loopback aliases still get seeded — the host opted
1207
- // out of just `127.0.0.1`, not all loopback.
1208
- expect(cfg.network.allowedDomains).toEqual(
1209
- expect.arrayContaining(['localhost', '::1'])
1210
- );
1211
- });
1212
- });
1213
-
1214
- describe('sandbox allowWrite includes additionalRoots (Codex P2 #15)', () => {
1215
- const {
1216
- buildSandboxRuntimeConfig,
1217
- } = require('../local/LocalExecutionEngine');
1218
-
1219
- it('adds workspace.additionalRoots to allowWrite alongside cwd', () => {
1220
- const cfg = buildSandboxRuntimeConfig(
1221
- {
1222
- cwd: '/tmp/repo/app',
1223
- workspace: {
1224
- root: '/tmp/repo/app',
1225
- additionalRoots: ['/tmp/repo/shared'],
1226
- },
1227
- },
1228
- '/tmp/repo/app',
1229
- () => ['/tmp/runtime-default']
1230
- );
1231
- expect(cfg.filesystem.allowWrite).toEqual(
1232
- expect.arrayContaining([
1233
- '/tmp/repo/app',
1234
- '/tmp/repo/shared',
1235
- '/tmp/runtime-default',
1236
- ])
1237
- );
1238
- });
1239
-
1240
- it('resolves relative additionalRoots against the workspace root', () => {
1241
- const cfg = buildSandboxRuntimeConfig(
1242
- {
1243
- cwd: '/tmp/repo/app',
1244
- workspace: {
1245
- root: '/tmp/repo/app',
1246
- additionalRoots: ['../shared'],
1247
- },
1248
- },
1249
- '/tmp/repo/app',
1250
- () => []
1251
- );
1252
- // ../shared anchored to root: /tmp/repo/app -> /tmp/repo/shared.
1253
- expect(cfg.filesystem.allowWrite).toContain('/tmp/repo/shared');
1254
- });
1255
-
1256
- it('falls back to cwd-only when no additionalRoots are configured', () => {
1257
- const cfg = buildSandboxRuntimeConfig(
1258
- { cwd: '/tmp/ws' },
1259
- '/tmp/ws',
1260
- () => ['/tmp/runtime-default']
1261
- );
1262
- expect(cfg.filesystem.allowWrite).toEqual([
1263
- '/tmp/ws',
1264
- '/tmp/runtime-default',
1265
- ]);
1266
- });
1267
-
1268
- it('honours an explicit allowWrite override (no auto-seeding)', () => {
1269
- const cfg = buildSandboxRuntimeConfig(
1270
- {
1271
- cwd: '/tmp/ws',
1272
- workspace: {
1273
- root: '/tmp/ws',
1274
- additionalRoots: ['/tmp/extra'],
1275
- },
1276
- sandbox: { filesystem: { allowWrite: ['/explicit/path'] } },
1277
- },
1278
- '/tmp/ws',
1279
- () => ['/tmp/runtime-default']
1280
- );
1281
- expect(cfg.filesystem.allowWrite).toEqual(['/explicit/path']);
1282
- });
1283
- });
1284
-
1285
- describe('glob_search surfaces ripgrep failures (Codex P2 #13)', () => {
1286
- it('returns an explicit error (not "No files found.") when rg exits non-zero', async () => {
1287
- _resetRipgrepCacheForTests();
1288
- // Inject a spawn backend that pretends rg exists for the
1289
- // availability probe but fails the actual `rg --files` call
1290
- // with exit 2 + stderr — the failure mode the codex comment
1291
- // flagged. Pre-fix, glob_search dropped exitCode/stderr on
1292
- // the floor and returned "No files found." regardless.
1293
- const realSpawn = (
1294
- require('child_process') as typeof import('child_process')
1295
- ).spawn;
1296
- const fakeRgBackend: t.LocalSpawn = ((
1297
- cmd: string,
1298
- args: string[],
1299
- opts: import('child_process').SpawnOptions
1300
- ) => {
1301
- if (cmd === 'rg' && args[0] === '--version') {
1302
- return realSpawn('sh', ['-c', 'exit 0'], opts);
1303
- }
1304
- if (cmd === 'rg') {
1305
- return realSpawn(
1306
- 'sh',
1307
- ['-c', 'printf \'rg: bad glob target\\n\' >&2; exit 2'],
1308
- opts
1309
- );
1310
- }
1311
- return realSpawn(cmd, args, opts);
1312
- }) as unknown as t.LocalSpawn;
1313
-
1314
- const cwd = await createTempDir();
1315
- const bundle = createLocalCodingToolBundle({
1316
- cwd,
1317
- exec: { spawn: fakeRgBackend },
1318
- });
1319
- const globTool = bundle.tools.find(
1320
- (tt) => tt.name === Constants.GLOB_SEARCH
1321
- );
1322
- const result = await globTool!.invoke({
1323
- id: 'g1',
1324
- name: Constants.GLOB_SEARCH,
1325
- args: { pattern: '**/*' },
1326
- type: 'tool_call',
1327
- });
1328
- const text = JSON.stringify(result);
1329
- expect(text).not.toContain('No files found.');
1330
- expect(text).toContain('glob_search failed');
1331
- expect(text).toContain('bad glob target');
1332
- });
1333
- });
1334
-
1335
- describe('grep_search surfaces ripgrep failures (Codex P2 #23)', () => {
1336
- it('returns an explicit error (not "No matches found.") when rg exits non-zero', async () => {
1337
- _resetRipgrepCacheForTests();
1338
- // Same shape as the glob_search test above. Pre-fix the
1339
- // grep_search rg branch dropped exitCode and reported
1340
- // matches: 0 on a real rg error (codex flagged that
1341
- // glob_search had this fix but grep_search hadn't been
1342
- // updated to match).
1343
- const realSpawn = (
1344
- require('child_process') as typeof import('child_process')
1345
- ).spawn;
1346
- const fakeRgBackend: t.LocalSpawn = ((
1347
- cmd: string,
1348
- args: string[],
1349
- opts: import('child_process').SpawnOptions
1350
- ) => {
1351
- if (cmd === 'rg' && args[0] === '--version') {
1352
- return realSpawn('sh', ['-c', 'exit 0'], opts);
1353
- }
1354
- if (cmd === 'rg') {
1355
- return realSpawn(
1356
- 'sh',
1357
- ['-c', 'printf \'rg: io error reading dir\\n\' >&2; exit 2'],
1358
- opts
1359
- );
1360
- }
1361
- return realSpawn(cmd, args, opts);
1362
- }) as unknown as t.LocalSpawn;
1363
-
1364
- const cwd = await createTempDir();
1365
- const bundle = createLocalCodingToolBundle({
1366
- cwd,
1367
- exec: { spawn: fakeRgBackend },
1368
- });
1369
- const grepTool = bundle.tools.find(
1370
- (tt) => tt.name === Constants.GREP_SEARCH
1371
- );
1372
- const result = await grepTool!.invoke({
1373
- id: 'gr1',
1374
- name: Constants.GREP_SEARCH,
1375
- args: { pattern: 'needle' },
1376
- type: 'tool_call',
1377
- });
1378
- const text = JSON.stringify(result);
1379
- expect(text).not.toContain('No matches found.');
1380
- expect(text).toContain('grep_search failed');
1381
- expect(text).toContain('io error reading dir');
1382
- });
1383
- });
1384
- });
1385
-
1386
- describe('codex review fixes (round 6)', () => {
1387
- describe('destructive guard handles `--` end-of-options (Codex P1 #20)', () => {
1388
- it('blocks rm -rf -- "/" (-- between flags and quoted target)', async () => {
1389
- const result = await validateBashCommand('rm -rf -- "/"');
1390
- expect(result.valid).toBe(false);
1391
- expect(result.errors.join('\n')).toContain('destructive command pattern');
1392
- });
1393
-
1394
- it('blocks rm -rf -- / (-- between flags and bare target)', async () => {
1395
- const result = await validateBashCommand('rm -rf -- /');
1396
- expect(result.valid).toBe(false);
1397
- expect(result.errors.join('\n')).toContain('destructive command pattern');
1398
- });
1399
-
1400
- it('blocks chmod -R 777 -- "/"', async () => {
1401
- const result = await validateBashCommand('chmod -R 777 -- "/"');
1402
- expect(result.valid).toBe(false);
1403
- expect(result.errors.join('\n')).toContain('destructive command pattern');
1404
- });
1405
-
1406
- it('blocks rm -rf -- "$HOME"', async () => {
1407
- const result = await validateBashCommand('rm -rf -- "$HOME"');
1408
- expect(result.valid).toBe(false);
1409
- expect(result.errors.join('\n')).toContain('destructive command pattern');
1410
- });
1411
-
1412
- it('still allows benign `--` usage (no destructive target)', async () => {
1413
- // `find` uses `--` to separate options from filenames; benign.
1414
- const result = await validateBashCommand('find . -- -name "*.ts"');
1415
- expect(result.valid).toBe(true);
1416
- });
1417
- });
1418
-
1419
- describe('compile_check enforces validateBashCommand + readOnly (Codex P1 #21)', () => {
1420
- it('refuses a destructive command override (rm -rf "/")', async () => {
1421
- const cwd = await createTempDir();
1422
- const compile = createCompileCheckTool({ cwd });
1423
- const result = await compile.invoke({
1424
- id: 'cc1',
1425
- name: Constants.COMPILE_CHECK,
1426
- args: { command: 'rm -rf "/"' },
1427
- type: 'tool_call',
1428
- });
1429
- const text = JSON.stringify(result);
1430
- expect(text).toContain('compile_check refused to run');
1431
- expect(text).toContain('destructive command pattern');
1432
- });
1433
-
1434
- it('refuses a mutating command override under readOnly: true', async () => {
1435
- const cwd = await createTempDir();
1436
- const compile = createCompileCheckTool({ cwd, readOnly: true });
1437
- const result = await compile.invoke({
1438
- id: 'cc2',
1439
- name: Constants.COMPILE_CHECK,
1440
- // `touch` is in mutatingCommandPattern — fine outside readOnly,
1441
- // blocked under readOnly.
1442
- args: { command: 'touch /tmp/lc-cc-should-not-create' },
1443
- type: 'tool_call',
1444
- });
1445
- const text = JSON.stringify(result);
1446
- expect(text).toContain('compile_check refused to run');
1447
- expect(text).toMatch(/read-only|mutate/i);
1448
- });
1449
-
1450
- it('still allows benign override commands (echo)', async () => {
1451
- const cwd = await createTempDir();
1452
- const compile = createCompileCheckTool({ cwd });
1453
- const result = await compile.invoke({
1454
- id: 'cc3',
1455
- name: Constants.COMPILE_CHECK,
1456
- args: { command: 'echo hello' },
1457
- type: 'tool_call',
1458
- });
1459
- const text = JSON.stringify(result);
1460
- expect(text).not.toContain('refused to run');
1461
- });
1462
- });
1463
- });
1464
-
1465
- describe('comprehensive review (round 7) — manual finding C', () => {
1466
- describe('nested-shell destructive payload (manual #C)', () => {
1467
- it('blocks bash -lc "rm -rf $HOME"', async () => {
1468
- const result = await validateBashCommand('bash -lc "rm -rf $HOME"');
1469
- expect(result.valid).toBe(false);
1470
- expect(result.errors.join('\n')).toMatch(/destructive command pattern/);
1471
- });
1472
-
1473
- it('blocks sh -c "chmod -R 777 /"', async () => {
1474
- const result = await validateBashCommand('sh -c \'chmod -R 777 /\'');
1475
- expect(result.valid).toBe(false);
1476
- expect(result.errors.join('\n')).toMatch(/destructive command pattern/);
1477
- });
1478
-
1479
- it('blocks eval "rm -rf /"', async () => {
1480
- const result = await validateBashCommand('eval \'rm -rf /\'');
1481
- expect(result.valid).toBe(false);
1482
- expect(result.errors.join('\n')).toMatch(/destructive command pattern/);
1483
- });
1484
-
1485
- it('still allows benign nested shell (echo)', async () => {
1486
- const result = await validateBashCommand('bash -lc "echo hello"');
1487
- expect(result.valid).toBe(true);
1488
- });
1489
- });
1490
- });
1491
-
1492
- describe('comprehensive review (round 7) — manual finding D', () => {
1493
- describe('fallback grep DoS guardrails', () => {
1494
- it('rejects oversize patterns before compile', async () => {
1495
- const cwd = await createTempDir();
1496
- const bundle = createLocalCodingToolBundle({ cwd });
1497
- const grepTool = bundle.tools.find(
1498
- (tt) => tt.name === Constants.GREP_SEARCH
1499
- );
1500
- const result = await grepTool!.invoke({
1501
- id: 'g-long',
1502
- name: Constants.GREP_SEARCH,
1503
- // 2 KiB pattern — over the 1 KiB cap.
1504
- args: { pattern: 'a'.repeat(2048) },
1505
- type: 'tool_call',
1506
- });
1507
- const text = JSON.stringify(result);
1508
- // Either the rg path runs (and matches nothing on an empty
1509
- // dir) or — when rg is unavailable — the fallback rejects via
1510
- // FallbackGrepError. We only assert the fallback shape when
1511
- // it triggers.
1512
- if (text.includes('node-fallback')) {
1513
- expect(text).toContain('grep_search refused the pattern');
1514
- expect(text).toContain('exceeds');
1515
- }
1516
- });
1517
-
1518
- it('rejects nested-quantifier patterns (catastrophic backtracking)', async () => {
1519
- const cwd = await createTempDir();
1520
- const bundle = createLocalCodingToolBundle({ cwd });
1521
- const grepTool = bundle.tools.find(
1522
- (tt) => tt.name === Constants.GREP_SEARCH
1523
- );
1524
- const result = await grepTool!.invoke({
1525
- id: 'g-evil',
1526
- name: Constants.GREP_SEARCH,
1527
- args: { pattern: '(a+)+$' },
1528
- type: 'tool_call',
1529
- });
1530
- const text = JSON.stringify(result);
1531
- if (text.includes('node-fallback')) {
1532
- expect(text).toContain('catastrophic backtracking');
1533
- }
1534
- });
1535
- });
1536
- });
1537
-
1538
- describe('comprehensive review (round 7) — manual finding E', () => {
1539
- describe('fileCheckpointer exposed via ToolNode auto-bind path', () => {
1540
- it('Run/ToolNode-style bind makes the checkpointer reachable when fileCheckpointing is true', () => {
1541
- const node = new ToolNode({
1542
- tools: [],
1543
- toolExecution: {
1544
- engine: 'local',
1545
- local: { fileCheckpointing: true },
1546
- },
1547
- });
1548
- const cp = node.getFileCheckpointer();
1549
- expect(cp).toBeDefined();
1550
- expect(typeof cp?.captureBeforeWrite).toBe('function');
1551
- expect(typeof cp?.rewind).toBe('function');
1552
- });
1553
-
1554
- it('returns undefined when fileCheckpointing is not enabled', () => {
1555
- const node = new ToolNode({
1556
- tools: [],
1557
- toolExecution: { engine: 'local' },
1558
- });
1559
- expect(node.getFileCheckpointer()).toBeUndefined();
1560
- });
1561
- });
1562
-
1563
- describe('fileCheckpointer reachable through Run.getFileCheckpointer / Run.rewindFiles (audit-of-audit follow-up)', () => {
1564
- // The round-7 fix exposed `getFileCheckpointer()` on ToolNode but
1565
- // the normal `Run.create(...)` path constructs the ToolNode inline
1566
- // inside StandardGraph and dropped the reference, so the public
1567
- // `RunConfig.toolExecution.local.fileCheckpointing` flag was still
1568
- // a no-op for Run callers (only direct `new ToolNode(...)` users
1569
- // could reach it). Pin the round-trip: a Run constructed through
1570
- // the standard config path must surface the same checkpointer the
1571
- // graph wired into its ToolNode, and `Run.rewindFiles()` must
1572
- // restore captured paths.
1573
- it('exposes the checkpointer via Run.getFileCheckpointer + restores through Run.rewindFiles', async () => {
1574
- const { Run } = await import('@/run');
1575
- const fs = await import('fs/promises');
1576
- const cwd = await createTempDir();
1577
- const file = join(cwd, 'tracked.txt');
1578
- await fs.writeFile(file, 'before\n');
1579
-
1580
- const run = await Run.create<t.IState>({
1581
- runId: 'run-checkpoint-roundtrip',
1582
- graphConfig: {
1583
- type: 'standard',
1584
- llmConfig: { provider: Providers.OPENAI, model: 'gpt-4o' },
1585
- },
1586
- toolExecution: {
1587
- engine: 'local',
1588
- local: { cwd, fileCheckpointing: true },
1589
- },
1590
- });
1591
-
1592
- // Reachable straight off Run — used to be undefined here even
1593
- // when the config flag was true.
1594
- const cp = run.getFileCheckpointer();
1595
- expect(cp).toBeDefined();
1596
-
1597
- // Capture, mutate, rewind via Run.rewindFiles() (the API the
1598
- // public JSDoc on `LocalExecutionConfig.fileCheckpointing`
1599
- // promises).
1600
- await cp!.captureBeforeWrite(file);
1601
- await fs.writeFile(file, 'mutated\n');
1602
- const restored = await run.rewindFiles();
1603
- expect(restored).toBeGreaterThanOrEqual(1);
1604
- expect(await fs.readFile(file, 'utf8')).toBe('before\n');
1605
- });
1606
-
1607
- it('Run.rewindFiles returns 0 when fileCheckpointing is disabled', async () => {
1608
- const { Run } = await import('@/run');
1609
- const run = await Run.create<t.IState>({
1610
- runId: 'run-no-checkpoint',
1611
- graphConfig: {
1612
- type: 'standard',
1613
- llmConfig: { provider: Providers.OPENAI, model: 'gpt-4o' },
1614
- },
1615
- toolExecution: { engine: 'local' },
1616
- });
1617
- expect(run.getFileCheckpointer()).toBeUndefined();
1618
- expect(await run.rewindFiles()).toBe(0);
1619
- });
1620
-
1621
- it('checkpointer survives Graph.clearHeavyState so post-completion rewind works (Codex P1 #32)', async () => {
1622
- // The original round-7 wiring nulled `_fileCheckpointer` in
1623
- // clearHeavyState — but processStream calls clearHeavyState
1624
- // in its finally block, so the host could never reach
1625
- // rewindFiles AFTER the run completed (which is exactly when
1626
- // rollback is most often needed). Pin that calling
1627
- // clearHeavyState directly DOES NOT drop the checkpointer.
1628
- const { Run } = await import('@/run');
1629
- const fs = await import('fs/promises');
1630
- const cwd = await createTempDir();
1631
- const file = join(cwd, 'after-completion.txt');
1632
- await fs.writeFile(file, 'pre-run\n');
1633
-
1634
- const run = await Run.create<t.IState>({
1635
- runId: 'run-cp-survives-clear',
1636
- graphConfig: {
1637
- type: 'standard',
1638
- llmConfig: { provider: Providers.OPENAI, model: 'gpt-4o' },
1639
- },
1640
- toolExecution: {
1641
- engine: 'local',
1642
- local: { cwd, fileCheckpointing: true },
1643
- },
1644
- });
1645
- const cp = run.getFileCheckpointer();
1646
- expect(cp).toBeDefined();
1647
-
1648
- await cp!.captureBeforeWrite(file);
1649
- await fs.writeFile(file, 'mutated-by-tool\n');
1650
-
1651
- // Simulate end-of-run cleanup (what processStream's finally
1652
- // block does). Pre-fix this nulled the checkpointer.
1653
- run.Graph?.clearHeavyState();
1654
-
1655
- // Same checkpointer instance must still be reachable AFTER
1656
- // clearHeavyState — that's the whole point of the fix.
1657
- expect(run.getFileCheckpointer()).toBe(cp);
1658
-
1659
- // Host calls rewindFiles after processStream returned.
1660
- const restored = await run.rewindFiles();
1661
- expect(restored).toBeGreaterThanOrEqual(1);
1662
- expect(await fs.readFile(file, 'utf8')).toBe('pre-run\n');
1663
- });
1664
- });
1665
- });
1666
-
1667
- describe('comprehensive review (round 8) — Codex P1 #24 / P1 #25', () => {
1668
- describe('JSON post-edit syntax check uses WorkspaceFS (Codex P1 #24)', () => {
1669
- it('routes the JSON read through `local.exec.fs` instead of host fs', async () => {
1670
- const { runPostEditSyntaxCheck } = require('../local/syntaxCheck');
1671
-
1672
- const reads: string[] = [];
1673
- // Custom WorkspaceFS that returns valid JSON for the path the
1674
- // syntax checker asks about. If the checker bypassed our fs and
1675
- // hit the host filesystem instead, `reads` would stay empty
1676
- // AND the validator would silently pass (host file doesn't
1677
- // exist → catch returns undefined → `ok: true`). The "ok: true"
1678
- // would be a FALSE pass, exactly the failure mode codex flagged.
1679
- const fakeFs = {
1680
- readFile: async (p: string, _enc?: 'utf8'): Promise<string> => {
1681
- reads.push(p);
1682
- return '{"valid": true}';
1683
- },
1684
- // unused stubs to satisfy the WorkspaceFS shape — never called
1685
- // by the JSON checker
1686
- writeFile: async () => undefined,
1687
- stat: async () => {
1688
- throw new Error('not implemented');
1689
- },
1690
- readdir: async () => [],
1691
- mkdir: async () => undefined,
1692
- realpath: async (p: string) => p,
1693
- unlink: async () => undefined,
1694
- open: async () => {
1695
- throw new Error('not implemented');
1696
- },
1697
- };
1698
-
1699
- const ok = await runPostEditSyntaxCheck('/virtual/file.json', {
1700
- exec: { fs: fakeFs as unknown as never },
1701
- });
1702
- expect(ok?.ok).toBe(true);
1703
- expect(reads).toEqual(['/virtual/file.json']);
1704
- });
1705
-
1706
- it('flags invalid JSON returned by the WorkspaceFS', async () => {
1707
- const { runPostEditSyntaxCheck } = require('../local/syntaxCheck');
1708
- const fakeFs = {
1709
- readFile: async () => '{ invalid: json',
1710
- writeFile: async () => undefined,
1711
- stat: async () => {
1712
- throw new Error('not implemented');
1713
- },
1714
- readdir: async () => [],
1715
- mkdir: async () => undefined,
1716
- realpath: async (p: string) => p,
1717
- unlink: async () => undefined,
1718
- open: async () => {
1719
- throw new Error('not implemented');
1720
- },
1721
- };
1722
- const result = await runPostEditSyntaxCheck('/virtual/bad.json', {
1723
- exec: { fs: fakeFs as unknown as never },
1724
- });
1725
- expect(result?.ok).toBe(false);
1726
- expect(result?.checker).toBe('JSON.parse');
1727
- });
1728
- });
1729
-
1730
- describe('compile_check detect uses WorkspaceFS (Codex P1 #25)', () => {
1731
- it('routes project-marker probes through `local.exec.fs`', async () => {
1732
- // Custom FS that pretends `tsconfig.json` exists at the cwd. If
1733
- // detect bypasses our fs and uses host fs/promises, the host
1734
- // path won't have a tsconfig.json and detection falls through
1735
- // to "unknown".
1736
- const stats: string[] = [];
1737
- const fakeFs = {
1738
- readFile: async () => '',
1739
- writeFile: async () => undefined,
1740
- stat: async (p: string) => {
1741
- stats.push(p);
1742
- if (p.endsWith('tsconfig.json')) {
1743
- return {
1744
- isFile: () => true,
1745
- isDirectory: () => false,
1746
- size: 0,
1747
- };
1748
- }
1749
- throw new Error('ENOENT');
1750
- },
1751
- readdir: async () => [],
1752
- mkdir: async () => undefined,
1753
- realpath: async (p: string) => p,
1754
- unlink: async () => undefined,
1755
- open: async () => {
1756
- throw new Error('not implemented');
1757
- },
1758
- };
1759
-
1760
- const compile = createCompileCheckTool({
1761
- cwd: '/virtual/repo',
1762
- exec: { fs: fakeFs as unknown as never },
1763
- });
1764
- // Don't actually run anything — we only care that detect()
1765
- // saw the tsconfig and picked typescript. The validateBashCommand
1766
- // call inside the tool will still try to spawn, but we don't
1767
- // need to assert on its outcome; the artifact carries the
1768
- // detection result.
1769
- const result = await compile.invoke({
1770
- id: 'cc',
1771
- name: Constants.COMPILE_CHECK,
1772
- args: { command: 'echo skip-spawn' },
1773
- type: 'tool_call',
1774
- });
1775
- // Just confirm at least one stat was made through our fake fs
1776
- // (auto-detect path). Even with the explicit override we use
1777
- // here, the tool path doesn't run detect — but the cwd-init
1778
- // and validateBashCommand still go through the right fs.
1779
- // For the actual detect() invocation, drop the override:
1780
- void result;
1781
- const compile2 = createCompileCheckTool({
1782
- cwd: '/virtual/repo',
1783
- exec: { fs: fakeFs as unknown as never },
1784
- });
1785
- await compile2.invoke({
1786
- id: 'cc2',
1787
- name: Constants.COMPILE_CHECK,
1788
- args: {},
1789
- type: 'tool_call',
1790
- });
1791
- // The tsconfig probe and the package.json probe (if it gets
1792
- // there) happen BEFORE the spawn, so even if spawn fails the
1793
- // stats list captures what detect saw.
1794
- expect(stats.some((p) => p.endsWith('tsconfig.json'))).toBe(true);
1795
- });
1796
- });
1797
- });
1798
-
1799
- describe('comprehensive review (round 9) — Codex P1 (overflow-killed) + audit findings', () => {
1800
- describe('overflow-killed processes report as failures (Codex P1)', () => {
1801
- const { spawnLocalProcess } = require('../local/LocalExecutionEngine');
1802
-
1803
- it('reports overflowKilled=true and a non-null exit code when maxSpawnedBytes is exceeded', async () => {
1804
- // `yes` produces unbounded output. Cap at 16 KiB so the
1805
- // overflow guard fires within milliseconds. Pre-fix the close
1806
- // handler returned `exitCode: null` (signal-killed) and no
1807
- // overflow flag, so callers couldn't tell the run had been
1808
- // force-killed.
1809
- const result = await spawnLocalProcess('yes', [], {
1810
- timeoutMs: 30_000,
1811
- maxSpawnedBytes: 16 * 1024,
1812
- sandbox: { enabled: false },
1813
- });
1814
- expect(result.overflowKilled).toBe(true);
1815
- // SIGKILL'd processes report exitCode=null from Node; we
1816
- // synthesize 137 (128 + SIGKILL) so callers see a non-zero
1817
- // status.
1818
- expect(result.exitCode).not.toBeNull();
1819
- expect(result.exitCode).not.toBe(0);
1820
- expect(result.timedOut).toBe(false);
1821
- });
1822
-
1823
- it('formatLocalOutput surfaces the killed flag', async () => {
1824
- const cwd = await createTempDir();
1825
- const bundle = createLocalCodingToolBundle({
1826
- cwd,
1827
- maxSpawnedBytes: 16 * 1024,
1828
- timeoutMs: 30_000,
1829
- sandbox: { enabled: false },
1830
- });
1831
- const bashTool = bundle.tools.find(
1832
- (tt) => tt.name === Constants.BASH_TOOL
1833
- );
1834
- const result = await bashTool!.invoke({
1835
- id: 'b1',
1836
- name: Constants.BASH_TOOL,
1837
- args: { command: 'yes' },
1838
- type: 'tool_call',
1839
- });
1840
- const text = JSON.stringify(result);
1841
- expect(text).toContain('killed: true');
1842
- expect(text).toContain('local.maxSpawnedBytes');
1843
- });
1844
- });
1845
-
1846
- describe('signal-killed processes report as failures (Codex P2 — generalizes the overflow fix)', () => {
1847
- const { spawnLocalProcess } = require('../local/LocalExecutionEngine');
1848
-
1849
- it('synthesizes a non-zero exit code and surfaces the signal name on `kill -9 $$`', async () => {
1850
- // Script kills its own pgroup with SIGKILL. Pre-fix the close
1851
- // handler dropped the `signal` argument and kept exitCode=null,
1852
- // so this looked like a clean run.
1853
- const result = await spawnLocalProcess(
1854
- 'bash',
1855
- ['-c', 'echo started; kill -9 $$'],
1856
- { timeoutMs: 5_000, sandbox: { enabled: false } }
1857
- );
1858
- // Node may report SIGKILL on the script process or the wrapper;
1859
- // either way exitCode must end up non-null and non-zero.
1860
- expect(result.exitCode).not.toBeNull();
1861
- expect(result.exitCode).not.toBe(0);
1862
- // Signal field is present and matches one of the expected
1863
- // POSIX kill signals.
1864
- expect(result.signal).toMatch(/^SIG/);
1865
- });
1866
-
1867
- it('formatLocalOutput surfaces the signal kill', async () => {
1868
- const cwd = await createTempDir();
1869
- const bundle = createLocalCodingToolBundle({
1870
- cwd,
1871
- timeoutMs: 5_000,
1872
- sandbox: { enabled: false },
1873
- });
1874
- const bashTool = bundle.tools.find(
1875
- (tt) => tt.name === Constants.BASH_TOOL
1876
- );
1877
- const result = await bashTool!.invoke({
1878
- id: 'sig1',
1879
- name: Constants.BASH_TOOL,
1880
- args: { command: 'echo started; kill -9 $$' },
1881
- type: 'tool_call',
1882
- });
1883
- const text = JSON.stringify(result);
1884
- expect(text).toContain('killed: true');
1885
- expect(text).toMatch(/signal=SIG/);
1886
- });
1887
- });
1888
-
1889
- describe('fallback-grep nested-quantifier heuristic catches double-nested groups (audit #1)', () => {
1890
- it('rejects `((a+)+)` (the textbook ReDoS pattern)', async () => {
1891
- _resetRipgrepCacheForTests();
1892
- // Force the fallback path by injecting a backend that says rg
1893
- // is unavailable (the rg --version probe always fails). This
1894
- // way the fallback compileFallbackRegex actually runs.
1895
- const realSpawn = (
1896
- require('child_process') as typeof import('child_process')
1897
- ).spawn;
1898
- const noRgBackend: t.LocalSpawn = ((
1899
- cmd: string,
1900
- args: string[],
1901
- opts: import('child_process').SpawnOptions
1902
- ) => {
1903
- if (cmd === 'rg') {
1904
- return realSpawn('sh', ['-c', 'exit 127'], opts);
1905
- }
1906
- return realSpawn(cmd, args, opts);
1907
- }) as unknown as t.LocalSpawn;
1908
-
1909
- const cwd = await createTempDir();
1910
- const bundle = createLocalCodingToolBundle({
1911
- cwd,
1912
- exec: { spawn: noRgBackend },
1913
- });
1914
- const grepTool = bundle.tools.find(
1915
- (tt) => tt.name === Constants.GREP_SEARCH
1916
- );
1917
- const result = await grepTool!.invoke({
1918
- id: 'gr-evil',
1919
- name: Constants.GREP_SEARCH,
1920
- args: { pattern: '((a+)+)' },
1921
- type: 'tool_call',
1922
- });
1923
- const text = JSON.stringify(result);
1924
- expect(text).toContain('grep_search refused the pattern');
1925
- expect(text).toContain('catastrophic backtracking');
1926
- });
1927
- });
1928
-
1929
- describe('resolveLocalExecutionTools no longer overwrites bundle tools (audit #4)', () => {
1930
- it('CODE_EXECUTION_TOOLS loop does not re-create tools when coding-tools bundle ran first', () => {
1931
- // The bundle path creates bash_tool/execute_code/etc. with a
1932
- // stable identity. Pre-fix the CODE_EXECUTION_TOOLS loop
1933
- // overwrote those instances with fresh ones — wasted work, and
1934
- // the fresh tools wouldn't share the bundle's checkpointer.
1935
- // Pin via tool identity comparison.
1936
- const node1 = new ToolNode({
1937
- tools: [],
1938
- toolExecution: { engine: 'local' },
1939
- });
1940
- // Capture the bash_tool instance
1941
-
1942
- const m1 = (node1 as unknown as { toolMap: Map<string, unknown> })
1943
- .toolMap;
1944
- expect(m1.has(Constants.BASH_TOOL)).toBe(true);
1945
- // Run the resolver again (simulating a fresh ToolNode); the
1946
- // bash_tool instance from the bundle should still be the only
1947
- // one (no overwrite step). Identity comparison would be
1948
- // brittle; assert tool count for the bundle members instead.
1949
- const bundleNames = [
1950
- Constants.BASH_TOOL,
1951
- Constants.EXECUTE_CODE,
1952
- Constants.PROGRAMMATIC_TOOL_CALLING,
1953
- Constants.BASH_PROGRAMMATIC_TOOL_CALLING,
1954
- ];
1955
- for (const name of bundleNames) {
1956
- expect(m1.has(name)).toBe(true);
1957
- }
1958
- });
1959
- });
1960
- });
1961
-
1962
- describe('comprehensive review (round 10) — Codex P1 #28 / P2 #29', () => {
1963
- describe('SIGKILL escalation defeats SIGTERM-trapping processes (Codex P1 #28)', () => {
1964
- const { spawnLocalProcess } = require('../local/LocalExecutionEngine');
1965
-
1966
- it('escalates to SIGKILL when timeoutMs elapses and the child traps SIGTERM', async () => {
1967
- // Trap SIGTERM and loop forever. Pre-fix killProcessTree only
1968
- // sent SIGTERM, so the child kept running, `close` never
1969
- // fired, and the spawn promise hung past timeoutMs. Now SIGKILL
1970
- // escalation kicks in 2s after the SIGTERM and the child dies
1971
- // unconditionally.
1972
- const start = Date.now();
1973
- const result = await spawnLocalProcess(
1974
- 'bash',
1975
- ['-c', 'trap \'\' TERM; while true; do sleep 0.1; done'],
1976
- { timeoutMs: 1500, sandbox: { enabled: false } }
1977
- );
1978
- const elapsed = Date.now() - start;
1979
- // Sanity: the test has to actually have terminated. With the
1980
- // bug the promise hangs and Jest times out after 5s default.
1981
- // Generous upper bound: timeout (1.5s) + escalation (2s) +
1982
- // spawn overhead. Assert under 6s.
1983
- expect(elapsed).toBeLessThan(6000);
1984
- expect(result.timedOut).toBe(true);
1985
- // signal field is populated (SIGKILL after escalation, or
1986
- // possibly SIGTERM if the trap didn't take effect on a
1987
- // particular host).
1988
- expect(result.signal).toMatch(/^SIG/);
1989
- }, 10_000);
1990
- });
1991
-
1992
- describe('ripgrep cache also keys on env (Codex P1 #34)', () => {
1993
- it('does not bleed an "rg available" verdict from one env to another on the same backend', async () => {
1994
- _resetRipgrepCacheForTests();
1995
- // Same backend instance for both Runs. Vary `local.env` between
1996
- // them — pre-fix the WeakMap cache was keyed on the spawn
1997
- // function alone, so the second Run inherited the first's
1998
- // verdict and tried to use rg under an env without it,
1999
- // failing with ENOENT.
2000
- const realSpawn = (
2001
- require('child_process') as typeof import('child_process')
2002
- ).spawn;
2003
-
2004
- // Backend that returns success for `rg --version` ONLY when
2005
- // the spawned process's env has PATH=/with/rg, and 127
2006
- // otherwise. This is the structural shape of "rg is on PATH
2007
- // for env A but not env B".
2008
- const envSensitive: t.LocalSpawn = ((
2009
- cmd: string,
2010
- args: string[],
2011
- opts: import('child_process').SpawnOptions
2012
- ) => {
2013
- if (cmd === 'rg' && args[0] === '--version') {
2014
- const env = (opts.env ?? {}) as NodeJS.ProcessEnv;
2015
- if (env.PATH === '/with/rg') {
2016
- return realSpawn('sh', ['-c', 'exit 0'], opts);
2017
- }
2018
- return realSpawn('sh', ['-c', 'exit 127'], opts);
2019
- }
2020
- return realSpawn(cmd, args, opts);
2021
- }) as unknown as t.LocalSpawn;
2022
-
2023
- const cwdA = await createTempDir();
2024
- const cwdB = await createTempDir();
2025
- await (
2026
- await import('fs/promises')
2027
- ).writeFile(join(cwdA, 'a.ts'), 'needle\n');
2028
- await (
2029
- await import('fs/promises')
2030
- ).writeFile(join(cwdB, 'b.ts'), 'needle\n');
2031
-
2032
- // Run A: env says rg is available → cache records `true` for
2033
- // (backend, env-A).
2034
- const bundleA = createLocalCodingToolBundle({
2035
- cwd: cwdA,
2036
- exec: { spawn: envSensitive },
2037
- env: { PATH: '/with/rg' },
2038
- });
2039
- await bundleA.tools
2040
- .find((t_) => t_.name === 'grep_search')!
2041
- .invoke({
2042
- id: 'gA',
2043
- name: 'grep_search',
2044
- args: { pattern: 'needle' },
2045
- type: 'tool_call',
2046
- });
2047
-
2048
- // Run B: same backend, DIFFERENT env (PATH excludes rg). Must
2049
- // run a fresh probe and fall back to the Node walker, NOT
2050
- // reuse Run A's cached "true". Pre-fix this would attempt to
2051
- // spawn rg with the wrong PATH and surface a tool failure.
2052
- const bundleB = createLocalCodingToolBundle({
2053
- cwd: cwdB,
2054
- exec: { spawn: envSensitive },
2055
- env: { PATH: '/without/rg' },
2056
- });
2057
- const bResult = await bundleB.tools
2058
- .find((t_) => t_.name === 'grep_search')!
2059
- .invoke({
2060
- id: 'gB',
2061
- name: 'grep_search',
2062
- args: { pattern: 'needle' },
2063
- type: 'tool_call',
2064
- });
2065
- const text = JSON.stringify(bResult);
2066
- // Result must show the match (Node fallback ran successfully)
2067
- // and indicate the fallback engine, not a ripgrep failure.
2068
- expect(text).toContain('needle');
2069
- expect(text).toContain('node-fallback');
2070
- });
2071
- });
2072
-
2073
- describe('compile-style runtimes honor local.shell (Codex P2 #29)', () => {
2074
- const { executeLocalCode } = require('../local/LocalExecutionEngine');
2075
-
2076
- it('routes the rust runtime through `local.shell` instead of bare `bash`', async () => {
2077
- // Intercept spawn — assert the configured shell is used for
2078
- // the rs runtime, not hardcoded `bash`.
2079
- const realSpawn = (
2080
- require('child_process') as typeof import('child_process')
2081
- ).spawn;
2082
- const calls: string[] = [];
2083
- const intercept: t.LocalSpawn = ((
2084
- cmd: string,
2085
- args: string[],
2086
- opts: import('child_process').SpawnOptions
2087
- ) => {
2088
- calls.push(cmd);
2089
- // Don't actually try to compile rust — short-circuit via sh.
2090
- return realSpawn('sh', ['-c', 'exit 0'], opts);
2091
- }) as unknown as t.LocalSpawn;
2092
-
2093
- await executeLocalCode(
2094
- { lang: 'rs', code: 'fn main() {}', args: [] },
2095
- {
2096
- shell: '/bin/sh',
2097
- exec: { spawn: intercept },
2098
- sandbox: { enabled: false },
2099
- }
2100
- );
2101
-
2102
- // The rust path's compile-and-run command should have been
2103
- // dispatched via `/bin/sh`, not `bash` / `bash.exe`.
2104
- expect(calls[0]).toBe('/bin/sh');
2105
- });
2106
- });
2107
- });
2108
-
2109
- describe('comprehensive review (round 12) — Codex P1 #36', () => {
2110
- describe('granular workspace flags override the legacy allowOutsideWorkspace', () => {
2111
- const {
2112
- getWriteRoots,
2113
- getReadRoots,
2114
- } = require('../local/LocalExecutionEngine');
2115
-
2116
- it('workspace.allowWriteOutside=false beats allowOutsideWorkspace=true (Codex P1 #36)', () => {
2117
- // Pre-fix the OR short-circuited on the legacy flag, returning
2118
- // null (skip clamp) even though the host explicitly tightened
2119
- // the granular flag during migration.
2120
- const roots = getWriteRoots({
2121
- cwd: '/tmp/ws',
2122
- workspace: { root: '/tmp/ws', allowWriteOutside: false },
2123
- allowOutsideWorkspace: true,
2124
- });
2125
- expect(roots).not.toBeNull();
2126
- expect(roots).toContain('/tmp/ws');
2127
- });
2128
-
2129
- it('workspace.allowReadOutside=false beats allowOutsideWorkspace=true', () => {
2130
- const roots = getReadRoots({
2131
- cwd: '/tmp/ws',
2132
- workspace: { root: '/tmp/ws', allowReadOutside: false },
2133
- allowOutsideWorkspace: true,
2134
- });
2135
- expect(roots).not.toBeNull();
2136
- expect(roots).toContain('/tmp/ws');
2137
- });
2138
-
2139
- it('workspace.allowWriteOutside=true still permits writes outside', () => {
2140
- const roots = getWriteRoots({
2141
- cwd: '/tmp/ws',
2142
- workspace: { root: '/tmp/ws', allowWriteOutside: true },
2143
- });
2144
- expect(roots).toBeNull();
2145
- });
2146
-
2147
- it('legacy allowOutsideWorkspace=true still works when granular flag is unset', () => {
2148
- const roots = getWriteRoots({
2149
- cwd: '/tmp/ws',
2150
- workspace: { root: '/tmp/ws' },
2151
- allowOutsideWorkspace: true,
2152
- });
2153
- expect(roots).toBeNull();
2154
- });
2155
-
2156
- it('default (no flags) returns the workspace boundary for both read and write', () => {
2157
- const cfg = { cwd: '/tmp/ws', workspace: { root: '/tmp/ws' } };
2158
- expect(getWriteRoots(cfg)).toEqual(['/tmp/ws']);
2159
- expect(getReadRoots(cfg)).toEqual(['/tmp/ws']);
2160
- });
2161
- });
2162
- });
2163
-
2164
- describe('comprehensive review (round 14) — Codex P1 #37 + P2 #38/#40/#41', () => {
2165
- describe('destructive path normalization (Codex P1 #37)', () => {
2166
- const cases: Array<[string, string]> = [
2167
- ['rm -rf $HOME/', 'trailing slash on $HOME'],
2168
- ['rm -rf ~/', 'trailing slash on ~'],
2169
- ['rm -rf ${HOME}/', 'trailing slash on ${HOME}'],
2170
- ['rm -rf "$HOME/"', 'quoted $HOME with trailing slash'],
2171
- ['rm -rf "~/"', 'quoted ~ with trailing slash'],
2172
- ['rm -rf "${HOME}/"', 'quoted ${HOME} with trailing slash'],
2173
- ['chmod -R 777 ~/', 'chmod with trailing slash'],
2174
- ['chmod -R 777 "$HOME/"', 'quoted chmod with trailing slash'],
2175
- ];
2176
- it.each(cases)('blocks %s (%s)', async (cmd) => {
2177
- const result = await validateBashCommand(cmd);
2178
- expect(result.valid).toBe(false);
2179
- expect(result.errors.join('\n')).toMatch(/destructive command pattern/);
2180
- });
2181
-
2182
- it('still allows benign trailing-slash commands', async () => {
2183
- const result = await validateBashCommand('ls $HOME/');
2184
- expect(result.valid).toBe(true);
2185
- });
2186
- });
2187
-
2188
- describe('destructive wildcard targets (Codex P1 [42])', () => {
2189
- const cases: Array<[string, string]> = [
2190
- ['rm -rf $HOME/*', 'glob over $HOME contents'],
2191
- ['rm -rf ~/*', 'glob over ~ contents'],
2192
- ['rm -rf ${HOME}/*', 'glob over ${HOME} contents'],
2193
- ['rm -rf ./*', 'glob over current dir contents'],
2194
- ['rm -rf .*', 'dotfile glob in current dir'],
2195
- ['rm -rf $HOME*', 'prefix glob against $HOME base'],
2196
- ['chmod -R 777 ~/*', 'chmod with glob'],
2197
- ];
2198
- it.each(cases)('blocks %s (%s)', async (cmd) => {
2199
- const result = await validateBashCommand(cmd);
2200
- expect(result.valid).toBe(false);
2201
- expect(result.errors.join('\n')).toMatch(/destructive command pattern/);
2202
- });
2203
-
2204
- it('does not flag benign glob commands (no rm/chmod/chown)', async () => {
2205
- const result = await validateBashCommand('ls $HOME/*');
2206
- expect(result.valid).toBe(true);
2207
- });
2208
- });
2209
-
2210
- describe('destructive dot-glob targets (Codex P1 [47])', () => {
2211
- const cases: Array<[string, string]> = [
2212
- ['rm -rf $HOME/.*', 'dotfile glob under $HOME'],
2213
- ['rm -rf ~/.*', 'dotfile glob under ~'],
2214
- ['rm -rf ${HOME}/.*', 'dotfile glob under ${HOME}'],
2215
- ['rm -rf /.*', 'dotfile glob under root'],
2216
- ['rm -rf "$HOME/.*"', 'quoted dotfile glob under $HOME'],
2217
- ['chmod -R 777 ~/.*', 'chmod dotfile glob'],
2218
- ];
2219
- it.each(cases)('blocks %s (%s)', async (cmd) => {
2220
- const result = await validateBashCommand(cmd);
2221
- expect(result.valid).toBe(false);
2222
- expect(result.errors.join('\n')).toMatch(/destructive command pattern/);
2223
- });
2224
-
2225
- it('blocks the positional-arg dot-glob form too', async () => {
2226
- const {
2227
- executeLocalBashWithArgs,
2228
- } = require('../local/LocalExecutionEngine');
2229
- await expect(
2230
- executeLocalBashWithArgs('rm -rf "$1"', ['/.*'], {
2231
- sandbox: { enabled: false },
2232
- timeoutMs: 5000,
2233
- })
2234
- ).rejects.toThrow(/destructive command pattern.*protected target/i);
2235
- });
2236
- });
2237
-
2238
- describe('strict postEditSyntaxCheck reverts the write on failure (Codex P2 [49])', () => {
2239
- it('write_file: reverts the file contents to pre-write state when strict check fails', async () => {
2240
- const cwd = await createTempDir();
2241
- const fsp = await import('fs/promises');
2242
- const file = join(cwd, 'a.js');
2243
- await fsp.writeFile(file, '// good\nconsole.log("ok");\n');
2244
-
2245
- const bundle = createLocalCodingToolBundle({
2246
- cwd,
2247
- postEditSyntaxCheck: 'strict',
2248
- });
2249
- const writeTool = bundle.tools.find(
2250
- (tt) => tt.name === Constants.WRITE_FILE
2251
- );
2252
- // Bad JS content (missing closing brace) — node --check will
2253
- // reject this and strict mode must throw AND restore the file.
2254
- await expect(
2255
- writeTool!.invoke({
2256
- id: 'wf-strict',
2257
- name: Constants.WRITE_FILE,
2258
- args: { path: file, content: 'function broken( {\n' },
2259
- type: 'tool_call',
2260
- })
2261
- ).rejects.toThrow(/syntax check failed.*reverted/i);
2262
- // Critical assertion: file on disk is restored to the
2263
- // pre-write content. Pre-fix it would still hold the broken
2264
- // content.
2265
- expect(await fsp.readFile(file, 'utf8')).toBe(
2266
- '// good\nconsole.log("ok");\n'
2267
- );
2268
- });
2269
-
2270
- it('write_file: deletes a brand-new file when strict check fails on first write', async () => {
2271
- const cwd = await createTempDir();
2272
- const fsp = await import('fs/promises');
2273
- const file = join(cwd, 'never-existed.js');
2274
-
2275
- const bundle = createLocalCodingToolBundle({
2276
- cwd,
2277
- postEditSyntaxCheck: 'strict',
2278
- });
2279
- const writeTool = bundle.tools.find(
2280
- (tt) => tt.name === Constants.WRITE_FILE
2281
- );
2282
- await expect(
2283
- writeTool!.invoke({
2284
- id: 'wf-strict-new',
2285
- name: Constants.WRITE_FILE,
2286
- args: { path: file, content: 'function broken( {\n' },
2287
- type: 'tool_call',
2288
- })
2289
- ).rejects.toThrow(/syntax check failed.*reverted/i);
2290
- // Brand-new file must be removed on revert.
2291
- await expect(fsp.stat(file)).rejects.toThrow();
2292
- });
2293
-
2294
- it('edit_file: reverts to pre-edit content when strict check fails', async () => {
2295
- const cwd = await createTempDir();
2296
- const fsp = await import('fs/promises');
2297
- const file = join(cwd, 'b.js');
2298
- const original = 'function ok() { return 1; }\n';
2299
- await fsp.writeFile(file, original);
2300
-
2301
- const bundle = createLocalCodingToolBundle({
2302
- cwd,
2303
- postEditSyntaxCheck: 'strict',
2304
- });
2305
- const editTool = bundle.tools.find(
2306
- (tt) => tt.name === Constants.EDIT_FILE
2307
- );
2308
- await expect(
2309
- editTool!.invoke({
2310
- id: 'ef-strict',
2311
- name: Constants.EDIT_FILE,
2312
- args: {
2313
- path: file,
2314
- old_text: 'return 1;',
2315
- new_text: 'return broken(',
2316
- },
2317
- type: 'tool_call',
2318
- })
2319
- ).rejects.toThrow(/syntax check failed.*reverted/i);
2320
- expect(await fsp.readFile(file, 'utf8')).toBe(original);
2321
- });
2322
-
2323
- it('write_file: still reverts when the strict validation READ times out', async () => {
2324
- // A stalled validation read must not leave the just-written bytes on disk —
2325
- // strict mode's revert contract still applies when validation is
2326
- // inconclusive due to a timeout (here a brand-new file -> unlink).
2327
- const cwd = await createTempDir();
2328
- const file = join(cwd, 'config.json');
2329
- const unlinked: string[] = [];
2330
- let written = false;
2331
- const fakeFs = {
2332
- readFile: async () => {
2333
- if (!written) {
2334
- // write_file pre-read: the file does not exist yet.
2335
- throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
2336
- }
2337
- // jsonCheck's post-write validation read stalls.
2338
- throw new WorkspaceClientTimeoutError(
2339
- 'cloudflare sandbox readFile exceeded 6000ms client-side timeout'
2340
- );
2341
- },
2342
- writeFile: async () => {
2343
- written = true;
2344
- },
2345
- mkdir: async () => undefined,
2346
- unlink: async (p: string) => {
2347
- unlinked.push(p);
2348
- },
2349
- stat: async () => ({ isFile: () => true, size: 1 }),
2350
- readdir: async () => [],
2351
- realpath: async (p: string) => p,
2352
- } as unknown as WorkspaceFS;
2353
-
2354
- const bundle = createLocalCodingToolBundle({
2355
- cwd,
2356
- postEditSyntaxCheck: 'strict',
2357
- exec: { fs: fakeFs },
2358
- });
2359
- const writeTool = bundle.tools.find(
2360
- (tt) => tt.name === Constants.WRITE_FILE
2361
- );
2362
- await expect(
2363
- writeTool!.invoke({
2364
- id: 'wf-strict-read-timeout',
2365
- name: Constants.WRITE_FILE,
2366
- args: { path: file, content: '{"ok": true}\n' },
2367
- type: 'tool_call',
2368
- })
2369
- ).rejects.toThrow(/client-side timeout/);
2370
- // strict mode attempted the revert (brand-new file -> unlink), rather than
2371
- // leaving the unvalidated write on disk.
2372
- expect(unlinked.length).toBeGreaterThan(0);
2373
- });
2374
- });
2375
-
2376
- describe('fallbackGrep skip sentinels do not count as matches (Codex P2 [43])', () => {
2377
- it('reports `matches: 0` when only oversize files are present', async () => {
2378
- _resetRipgrepCacheForTests();
2379
- const realSpawn = (
2380
- require('child_process') as typeof import('child_process')
2381
- ).spawn;
2382
- const noRgBackend: t.LocalSpawn = ((
2383
- cmd: string,
2384
- args: string[],
2385
- opts: import('child_process').SpawnOptions
2386
- ) => {
2387
- if (cmd === 'rg') return realSpawn('sh', ['-c', 'exit 127'], opts);
2388
- return realSpawn(cmd, args, opts);
2389
- }) as unknown as t.LocalSpawn;
2390
-
2391
- const cwd = await createTempDir();
2392
- const fsp = await import('fs/promises');
2393
- // Two oversize files, no real matches.
2394
- await fsp.writeFile(
2395
- join(cwd, 'big1.txt'),
2396
- Buffer.alloc(6 * 1024 * 1024, 'a')
2397
- );
2398
- await fsp.writeFile(
2399
- join(cwd, 'big2.txt'),
2400
- Buffer.alloc(6 * 1024 * 1024, 'a')
2401
- );
2402
-
2403
- const bundle = createLocalCodingToolBundle({
2404
- cwd,
2405
- exec: { spawn: noRgBackend },
2406
- });
2407
- const grepTool = bundle.tools.find(
2408
- (tt) => tt.name === Constants.GREP_SEARCH
2409
- );
2410
- const result = await grepTool!.invoke({
2411
- id: 'g43',
2412
- name: Constants.GREP_SEARCH,
2413
- args: { pattern: 'needle' },
2414
- type: 'tool_call',
2415
- });
2416
- // Result is [text, artifact]; pull the artifact off the
2417
- // ToolMessage shape.
2418
- const text = JSON.stringify(result);
2419
- // Artifact shape: { matches: 0, skipped: 2, engine: 'node-fallback' }
2420
- expect(text).toContain('"matches":0');
2421
- expect(text).toContain('"skipped":2');
2422
- });
2423
- });
2424
-
2425
- describe('Send-input direct path threads additionalContextsSink (Codex P2 [44])', () => {
2426
- it('materializes hook additionalContext as a HumanMessage on the Send branch', async () => {
2427
- // The Send-input branch dispatches a single direct tool. It
2428
- // had its own runDirectToolWithLifecycleHooks call site that
2429
- // didn't pass the sink, so PreToolUse additionalContext was
2430
- // dropped on this otherwise-supported input shape.
2431
- const { tool } = await import('@langchain/core/tools');
2432
- const { z } = await import('zod');
2433
- const { HookRegistry } = await import('@/hooks');
2434
- const { HumanMessage } = await import('@langchain/core/messages');
2435
-
2436
- const echo = tool(async () => 'ECHO', {
2437
- name: 'echo',
2438
- description: 'send-input echo',
2439
- schema: z.object({}).passthrough(),
2440
- });
2441
- const registry = new HookRegistry();
2442
- registry.register('PreToolUse', {
2443
- hooks: [
2444
- async () => ({
2445
- decision: 'allow',
2446
- additionalContext: 'SEND-CTX: policy note via Send branch',
2447
- }),
2448
- ],
2449
- });
2450
-
2451
- const node = new ToolNode({
2452
- tools: [echo],
2453
- eventDrivenMode: true,
2454
- hookRegistry: registry,
2455
- directToolNames: new Set(['echo']),
2456
- });
2457
- // Construct a Send-shaped input: { lg_tool_call: ToolCall }
2458
- const result = (await node.invoke({
2459
- lg_tool_call: { id: 'send_1', name: 'echo', args: {} },
2460
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2461
- } as any)) as { messages: BaseMessage[] } | BaseMessage[];
2462
- const messages = Array.isArray(result) ? result : result.messages;
2463
- const found = messages.find(
2464
- (m) =>
2465
- m instanceof HumanMessage &&
2466
- typeof m.content === 'string' &&
2467
- m.content.includes('SEND-CTX')
2468
- );
2469
- expect(found).toBeDefined();
2470
- });
2471
- });
2472
-
2473
- describe('bash args validated against destructive-target patterns (Codex P1 [45])', () => {
2474
- const {
2475
- executeLocalBashWithArgs,
2476
- } = require('../local/LocalExecutionEngine');
2477
-
2478
- it('blocks `rm -rf "$1"` + args=["/"]', async () => {
2479
- await expect(
2480
- executeLocalBashWithArgs('rm -rf "$1"', ['/'], {
2481
- sandbox: { enabled: false },
2482
- timeoutMs: 5000,
2483
- })
2484
- ).rejects.toThrow(/destructive command pattern.*protected target/i);
2485
- });
2486
-
2487
- it('blocks `chmod -R 777 "$1"` + args=["~/"]', async () => {
2488
- await expect(
2489
- executeLocalBashWithArgs('chmod -R 777 "$1"', ['~/'], {
2490
- sandbox: { enabled: false },
2491
- timeoutMs: 5000,
2492
- })
2493
- ).rejects.toThrow(/destructive command pattern.*protected target/i);
2494
- });
2495
-
2496
- it('blocks `rm -rf "$@"` + args=["$HOME"]', async () => {
2497
- await expect(
2498
- executeLocalBashWithArgs('rm -rf "$@"', ['$HOME'], {
2499
- sandbox: { enabled: false },
2500
- timeoutMs: 5000,
2501
- })
2502
- ).rejects.toThrow(/destructive command pattern.*protected target/i);
2503
- });
2504
-
2505
- it('allows benign positional arg use (echo + protected-shape arg)', async () => {
2506
- // `echo` is not in the destructive-op set so a "/" arg is fine.
2507
- const result = await executeLocalBashWithArgs('echo "$1"', ['/'], {
2508
- sandbox: { enabled: false },
2509
- timeoutMs: 5000,
2510
- });
2511
- expect(result.exitCode).toBe(0);
2512
- });
2513
-
2514
- it('allows destructive op with non-protected args', async () => {
2515
- // `rm` of a clearly non-protected path inside a tmpdir is fine.
2516
- const cwd = await createTempDir();
2517
- const fsp = await import('fs/promises');
2518
- const f = join(cwd, 'goner.txt');
2519
- await fsp.writeFile(f, 'bye\n');
2520
- const result = await executeLocalBashWithArgs('rm -f "$1"', [f], {
2521
- cwd,
2522
- sandbox: { enabled: false },
2523
- timeoutMs: 5000,
2524
- });
2525
- expect(result.exitCode).toBe(0);
2526
- });
2527
- });
2528
-
2529
- describe('direct-path additionalContext is marked as system metadata (Codex P2 [46])', () => {
2530
- it('attaches `additional_kwargs.role: "system"` to the materialized HumanMessage', async () => {
2531
- const { tool } = await import('@langchain/core/tools');
2532
- const { z } = await import('zod');
2533
- const { HookRegistry } = await import('@/hooks');
2534
- const { HumanMessage, AIMessage } = await import(
2535
- '@langchain/core/messages'
2536
- );
2537
-
2538
- const echo = tool(async () => 'OK', {
2539
- name: 'echo',
2540
- description: 'noop',
2541
- schema: z.object({}).passthrough(),
2542
- });
2543
- const registry = new HookRegistry();
2544
- registry.register('PreToolUse', {
2545
- hooks: [
2546
- async () => ({
2547
- decision: 'allow',
2548
- additionalContext: 'POLICY: be careful',
2549
- }),
2550
- ],
2551
- });
2552
- const node = new ToolNode({
2553
- tools: [echo],
2554
- eventDrivenMode: true,
2555
- hookRegistry: registry,
2556
- directToolNames: new Set(['echo']),
2557
- });
2558
- const ai = new AIMessage({
2559
- content: '',
2560
- tool_calls: [{ id: 'c46', name: 'echo', args: {} }],
2561
- });
2562
- const result = (await node.invoke({ messages: [ai] })) as
2563
- | { messages: BaseMessage[] }
2564
- | BaseMessage[];
2565
- const messages = Array.isArray(result) ? result : result.messages;
2566
- const human = messages.find(
2567
- (m): m is InstanceType<typeof HumanMessage> =>
2568
- m instanceof HumanMessage &&
2569
- typeof m.content === 'string' &&
2570
- m.content.includes('POLICY')
2571
- );
2572
- expect(human).toBeDefined();
2573
- // The marker the event-driven path sets — direct path now
2574
- // matches it.
2575
- expect(human?.additional_kwargs).toMatchObject({
2576
- role: 'system',
2577
- source: 'hook',
2578
- });
2579
- });
2580
- });
2581
-
2582
- describe('resolveWorkspacePathSafe routes through WorkspaceFS.realpath (Codex P2 #38)', () => {
2583
- it('honors a custom workspace fs realpath impl', async () => {
2584
- const {
2585
- resolveWorkspacePathSafe,
2586
- } = require('../local/LocalExecutionEngine');
2587
- const calls: string[] = [];
2588
- const fakeFs = {
2589
- readFile: async () => '',
2590
- writeFile: async () => undefined,
2591
- stat: async () => ({
2592
- isFile: () => true,
2593
- isDirectory: () => false,
2594
- size: 0,
2595
- }),
2596
- readdir: async () => [],
2597
- mkdir: async () => undefined,
2598
- // The custom realpath that the safe-path resolver MUST use.
2599
- // Returns paths unchanged so the lexical containment check
2600
- // succeeds for in-workspace targets.
2601
- realpath: async (p: string): Promise<string> => {
2602
- calls.push(p);
2603
- return p;
2604
- },
2605
- unlink: async () => undefined,
2606
- open: async () => {
2607
- throw new Error('not implemented');
2608
- },
2609
- };
2610
-
2611
- await resolveWorkspacePathSafe('/virtual/ws/file.ts', {
2612
- cwd: '/virtual/ws',
2613
- workspace: { root: '/virtual/ws' },
2614
- exec: { fs: fakeFs as unknown as never },
2615
- });
2616
-
2617
- // Must have called the WorkspaceFS realpath at least once
2618
- // (for either the root or the candidate path). Pre-fix the
2619
- // host fs/promises.realpath was used instead.
2620
- expect(calls.length).toBeGreaterThan(0);
2621
- expect(calls.every((p) => p.startsWith('/virtual/'))).toBe(true);
2622
- });
2623
- });
2624
-
2625
- describe('syntax-check probe cache also keys on env (Codex P2 #40)', () => {
2626
- it('does not bleed `hasNode` verdict from one env to another on the same backend', async () => {
2627
- _resetSyntaxCheckProbeCacheForTests();
2628
- const realSpawn = (
2629
- require('child_process') as typeof import('child_process')
2630
- ).spawn;
2631
- const calls: Array<{ cmd: string; env?: NodeJS.ProcessEnv }> = [];
2632
- // Backend that returns `node --version` success ONLY when
2633
- // env.PATH includes 'with-node'. Mirrors P1 #34's shape.
2634
- const envSensitive: t.LocalSpawn = ((
2635
- cmd: string,
2636
- args: string[],
2637
- opts: import('child_process').SpawnOptions
2638
- ) => {
2639
- calls.push({ cmd, env: opts.env as NodeJS.ProcessEnv });
2640
- if (cmd === 'node' && args[0] === '--version') {
2641
- const env = (opts.env ?? {}) as NodeJS.ProcessEnv;
2642
- if (env.PATH?.includes('with-node') === true) {
2643
- return realSpawn('sh', ['-c', 'exit 0'], opts);
2644
- }
2645
- return realSpawn('sh', ['-c', 'exit 127'], opts);
2646
- }
2647
- // Run all other spawns through a no-op so we don't hit
2648
- // real node/python/bash on the host.
2649
- return realSpawn('sh', ['-c', 'exit 0'], opts);
2650
- }) as unknown as t.LocalSpawn;
2651
-
2652
- const { runPostEditSyntaxCheck } = require('../local/syntaxCheck');
2653
- const cwd = await createTempDir();
2654
- const file = join(cwd, 'a.js');
2655
- await (await import('fs/promises')).writeFile(file, 'function (\n');
2656
-
2657
- // Run A: env says node IS available — probe records `true`
2658
- // for (backend, envA).
2659
- await runPostEditSyntaxCheck(file, {
2660
- exec: { spawn: envSensitive },
2661
- env: { PATH: '/with-node' },
2662
- });
2663
-
2664
- // Run B: env says node is NOT available. Pre-fix the cache
2665
- // would reuse the (backend) entry and try to actually
2666
- // syntax-check via the missing node. Now: separate cache slot
2667
- // for envB → its own probe → records `false` → skips check.
2668
- const probeCallsBefore = calls.filter(
2669
- (c) =>
2670
- c.cmd === 'node' && c.env?.PATH?.includes('without-node') === true
2671
- ).length;
2672
- await runPostEditSyntaxCheck(file, {
2673
- exec: { spawn: envSensitive },
2674
- env: { PATH: '/without-node' },
2675
- });
2676
- const probeCallsAfter = calls.filter(
2677
- (c) =>
2678
- c.cmd === 'node' && c.env?.PATH?.includes('without-node') === true
2679
- ).length;
2680
- // A fresh probe must have run for envB (count went up).
2681
- expect(probeCallsAfter).toBeGreaterThan(probeCallsBefore);
2682
- });
2683
- });
2684
-
2685
- describe('fallbackGrep skips files larger than the per-file cap (Codex P2 #41)', () => {
2686
- it('emits a sentinel and continues instead of reading multi-MB files into memory', async () => {
2687
- _resetRipgrepCacheForTests();
2688
- const realSpawn = (
2689
- require('child_process') as typeof import('child_process')
2690
- ).spawn;
2691
- // Force the Node fallback by making rg unavailable.
2692
- const noRgBackend: t.LocalSpawn = ((
2693
- cmd: string,
2694
- args: string[],
2695
- opts: import('child_process').SpawnOptions
2696
- ) => {
2697
- if (cmd === 'rg') {
2698
- return realSpawn('sh', ['-c', 'exit 127'], opts);
2699
- }
2700
- return realSpawn(cmd, args, opts);
2701
- }) as unknown as t.LocalSpawn;
2702
-
2703
- const cwd = await createTempDir();
2704
- const fsp = await import('fs/promises');
2705
- // Write a small file (matches the search) and a 6 MB file
2706
- // (over the 5 MB cap) — the fallback must skip the big one
2707
- // with a sentinel and still find the small-file match.
2708
- await fsp.writeFile(join(cwd, 'small.txt'), 'needle\n');
2709
- const big = Buffer.alloc(6 * 1024 * 1024, 'a');
2710
- await fsp.writeFile(join(cwd, 'big.txt'), big);
2711
-
2712
- const bundle = createLocalCodingToolBundle({
2713
- cwd,
2714
- exec: { spawn: noRgBackend },
2715
- });
2716
- const grepTool = bundle.tools.find(
2717
- (tt) => tt.name === Constants.GREP_SEARCH
2718
- );
2719
- const result = await grepTool!.invoke({
2720
- id: 'g41',
2721
- name: Constants.GREP_SEARCH,
2722
- args: { pattern: 'needle' },
2723
- type: 'tool_call',
2724
- });
2725
- const text = JSON.stringify(result);
2726
- // Small-file match landed.
2727
- expect(text).toContain('needle');
2728
- // Big-file got the skip sentinel (didn't OOM, didn't read
2729
- // into memory).
2730
- expect(text).toContain('skipped');
2731
- });
2732
- });
2733
- });