@chances-ai/engine 24.0.0

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 (389) hide show
  1. package/dist/agents/discover.d.ts +30 -0
  2. package/dist/agents/discover.d.ts.map +1 -0
  3. package/dist/agents/discover.js +183 -0
  4. package/dist/agents/discover.js.map +1 -0
  5. package/dist/agents/index.d.ts +20 -0
  6. package/dist/agents/index.d.ts.map +1 -0
  7. package/dist/agents/index.js +52 -0
  8. package/dist/agents/index.js.map +1 -0
  9. package/dist/agents/parse.d.ts +61 -0
  10. package/dist/agents/parse.d.ts.map +1 -0
  11. package/dist/agents/parse.js +527 -0
  12. package/dist/agents/parse.js.map +1 -0
  13. package/dist/agents/types.d.ts +52 -0
  14. package/dist/agents/types.d.ts.map +1 -0
  15. package/dist/agents/types.js +8 -0
  16. package/dist/agents/types.js.map +1 -0
  17. package/dist/ai/adapters/ai-sdk-stream.d.ts +19 -0
  18. package/dist/ai/adapters/ai-sdk-stream.d.ts.map +1 -0
  19. package/dist/ai/adapters/ai-sdk-stream.js +125 -0
  20. package/dist/ai/adapters/ai-sdk-stream.js.map +1 -0
  21. package/dist/ai/adapters/ai-sdk.d.ts +56 -0
  22. package/dist/ai/adapters/ai-sdk.d.ts.map +1 -0
  23. package/dist/ai/adapters/ai-sdk.js +112 -0
  24. package/dist/ai/adapters/ai-sdk.js.map +1 -0
  25. package/dist/ai/adapters/mock.d.ts +13 -0
  26. package/dist/ai/adapters/mock.d.ts.map +1 -0
  27. package/dist/ai/adapters/mock.js +54 -0
  28. package/dist/ai/adapters/mock.js.map +1 -0
  29. package/dist/ai/adapters/openai-compatible.d.ts +23 -0
  30. package/dist/ai/adapters/openai-compatible.d.ts.map +1 -0
  31. package/dist/ai/adapters/openai-compatible.js +45 -0
  32. package/dist/ai/adapters/openai-compatible.js.map +1 -0
  33. package/dist/ai/cost.d.ts +3 -0
  34. package/dist/ai/cost.d.ts.map +1 -0
  35. package/dist/ai/cost.js +5 -0
  36. package/dist/ai/cost.js.map +1 -0
  37. package/dist/ai/index.d.ts +12 -0
  38. package/dist/ai/index.d.ts.map +1 -0
  39. package/dist/ai/index.js +11 -0
  40. package/dist/ai/index.js.map +1 -0
  41. package/dist/ai/known-models.d.ts +20 -0
  42. package/dist/ai/known-models.d.ts.map +1 -0
  43. package/dist/ai/known-models.js +129 -0
  44. package/dist/ai/known-models.js.map +1 -0
  45. package/dist/ai/registry.d.ts +12 -0
  46. package/dist/ai/registry.d.ts.map +1 -0
  47. package/dist/ai/registry.js +24 -0
  48. package/dist/ai/registry.js.map +1 -0
  49. package/dist/ai/retry.d.ts +11 -0
  50. package/dist/ai/retry.d.ts.map +1 -0
  51. package/dist/ai/retry.js +14 -0
  52. package/dist/ai/retry.js.map +1 -0
  53. package/dist/ai/router.d.ts +25 -0
  54. package/dist/ai/router.d.ts.map +1 -0
  55. package/dist/ai/router.js +36 -0
  56. package/dist/ai/router.js.map +1 -0
  57. package/dist/ai/setup.d.ts +23 -0
  58. package/dist/ai/setup.d.ts.map +1 -0
  59. package/dist/ai/setup.js +47 -0
  60. package/dist/ai/setup.js.map +1 -0
  61. package/dist/ai/summarizer.d.ts +24 -0
  62. package/dist/ai/summarizer.d.ts.map +1 -0
  63. package/dist/ai/summarizer.js +56 -0
  64. package/dist/ai/summarizer.js.map +1 -0
  65. package/dist/ai/types.d.ts +83 -0
  66. package/dist/ai/types.d.ts.map +1 -0
  67. package/dist/ai/types.js +2 -0
  68. package/dist/ai/types.js.map +1 -0
  69. package/dist/core/compaction/circuit-breaker.d.ts +32 -0
  70. package/dist/core/compaction/circuit-breaker.d.ts.map +1 -0
  71. package/dist/core/compaction/circuit-breaker.js +42 -0
  72. package/dist/core/compaction/circuit-breaker.js.map +1 -0
  73. package/dist/core/compaction/compactor.d.ts +75 -0
  74. package/dist/core/compaction/compactor.d.ts.map +1 -0
  75. package/dist/core/compaction/compactor.js +261 -0
  76. package/dist/core/compaction/compactor.js.map +1 -0
  77. package/dist/core/compaction/estimate.d.ts +39 -0
  78. package/dist/core/compaction/estimate.d.ts.map +1 -0
  79. package/dist/core/compaction/estimate.js +74 -0
  80. package/dist/core/compaction/estimate.js.map +1 -0
  81. package/dist/core/compaction/index.d.ts +5 -0
  82. package/dist/core/compaction/index.d.ts.map +1 -0
  83. package/dist/core/compaction/index.js +5 -0
  84. package/dist/core/compaction/index.js.map +1 -0
  85. package/dist/core/compaction/prune.d.ts +43 -0
  86. package/dist/core/compaction/prune.d.ts.map +1 -0
  87. package/dist/core/compaction/prune.js +51 -0
  88. package/dist/core/compaction/prune.js.map +1 -0
  89. package/dist/core/engine.d.ts +268 -0
  90. package/dist/core/engine.d.ts.map +1 -0
  91. package/dist/core/engine.js +767 -0
  92. package/dist/core/engine.js.map +1 -0
  93. package/dist/core/index.d.ts +6 -0
  94. package/dist/core/index.d.ts.map +1 -0
  95. package/dist/core/index.js +6 -0
  96. package/dist/core/index.js.map +1 -0
  97. package/dist/core/task-tool.d.ts +175 -0
  98. package/dist/core/task-tool.d.ts.map +1 -0
  99. package/dist/core/task-tool.js +901 -0
  100. package/dist/core/task-tool.js.map +1 -0
  101. package/dist/core/workspace-query.d.ts +83 -0
  102. package/dist/core/workspace-query.d.ts.map +1 -0
  103. package/dist/core/workspace-query.js +217 -0
  104. package/dist/core/workspace-query.js.map +1 -0
  105. package/dist/core/worktree/active-marker.d.ts +31 -0
  106. package/dist/core/worktree/active-marker.d.ts.map +1 -0
  107. package/dist/core/worktree/active-marker.js +109 -0
  108. package/dist/core/worktree/active-marker.js.map +1 -0
  109. package/dist/core/worktree/create.d.ts +40 -0
  110. package/dist/core/worktree/create.d.ts.map +1 -0
  111. package/dist/core/worktree/create.js +121 -0
  112. package/dist/core/worktree/create.js.map +1 -0
  113. package/dist/core/worktree/errors.d.ts +7 -0
  114. package/dist/core/worktree/errors.d.ts.map +1 -0
  115. package/dist/core/worktree/errors.js +11 -0
  116. package/dist/core/worktree/errors.js.map +1 -0
  117. package/dist/core/worktree/gc.d.ts +39 -0
  118. package/dist/core/worktree/gc.d.ts.map +1 -0
  119. package/dist/core/worktree/gc.js +146 -0
  120. package/dist/core/worktree/gc.js.map +1 -0
  121. package/dist/core/worktree/git.d.ts +53 -0
  122. package/dist/core/worktree/git.d.ts.map +1 -0
  123. package/dist/core/worktree/git.js +166 -0
  124. package/dist/core/worktree/git.js.map +1 -0
  125. package/dist/core/worktree/index.d.ts +8 -0
  126. package/dist/core/worktree/index.d.ts.map +1 -0
  127. package/dist/core/worktree/index.js +8 -0
  128. package/dist/core/worktree/index.js.map +1 -0
  129. package/dist/core/worktree/paths.d.ts +26 -0
  130. package/dist/core/worktree/paths.d.ts.map +1 -0
  131. package/dist/core/worktree/paths.js +57 -0
  132. package/dist/core/worktree/paths.js.map +1 -0
  133. package/dist/core/worktree/slug.d.ts +6 -0
  134. package/dist/core/worktree/slug.d.ts.map +1 -0
  135. package/dist/core/worktree/slug.js +21 -0
  136. package/dist/core/worktree/slug.js.map +1 -0
  137. package/dist/local-vault/file-store.d.ts +64 -0
  138. package/dist/local-vault/file-store.d.ts.map +1 -0
  139. package/dist/local-vault/file-store.js +225 -0
  140. package/dist/local-vault/file-store.js.map +1 -0
  141. package/dist/local-vault/index.d.ts +57 -0
  142. package/dist/local-vault/index.d.ts.map +1 -0
  143. package/dist/local-vault/index.js +68 -0
  144. package/dist/local-vault/index.js.map +1 -0
  145. package/dist/local-vault/keychain.d.ts +58 -0
  146. package/dist/local-vault/keychain.d.ts.map +1 -0
  147. package/dist/local-vault/keychain.js +200 -0
  148. package/dist/local-vault/keychain.js.map +1 -0
  149. package/dist/local-vault/mutex.d.ts +20 -0
  150. package/dist/local-vault/mutex.d.ts.map +1 -0
  151. package/dist/local-vault/mutex.js +44 -0
  152. package/dist/local-vault/mutex.js.map +1 -0
  153. package/dist/local-vault/passphrase.d.ts +30 -0
  154. package/dist/local-vault/passphrase.d.ts.map +1 -0
  155. package/dist/local-vault/passphrase.js +72 -0
  156. package/dist/local-vault/passphrase.js.map +1 -0
  157. package/dist/lsp/config.d.ts +34 -0
  158. package/dist/lsp/config.d.ts.map +1 -0
  159. package/dist/lsp/config.js +68 -0
  160. package/dist/lsp/config.js.map +1 -0
  161. package/dist/lsp/detect.d.ts +7 -0
  162. package/dist/lsp/detect.d.ts.map +1 -0
  163. package/dist/lsp/detect.js +78 -0
  164. package/dist/lsp/detect.js.map +1 -0
  165. package/dist/lsp/errors.d.ts +11 -0
  166. package/dist/lsp/errors.d.ts.map +1 -0
  167. package/dist/lsp/errors.js +11 -0
  168. package/dist/lsp/errors.js.map +1 -0
  169. package/dist/lsp/formatters.d.ts +147 -0
  170. package/dist/lsp/formatters.d.ts.map +1 -0
  171. package/dist/lsp/formatters.js +259 -0
  172. package/dist/lsp/formatters.js.map +1 -0
  173. package/dist/lsp/index.d.ts +31 -0
  174. package/dist/lsp/index.d.ts.map +1 -0
  175. package/dist/lsp/index.js +31 -0
  176. package/dist/lsp/index.js.map +1 -0
  177. package/dist/lsp/instance.d.ts +72 -0
  178. package/dist/lsp/instance.d.ts.map +1 -0
  179. package/dist/lsp/instance.js +489 -0
  180. package/dist/lsp/instance.js.map +1 -0
  181. package/dist/lsp/lazy-load.d.ts +27 -0
  182. package/dist/lsp/lazy-load.d.ts.map +1 -0
  183. package/dist/lsp/lazy-load.js +57 -0
  184. package/dist/lsp/lazy-load.js.map +1 -0
  185. package/dist/lsp/manager.d.ts +59 -0
  186. package/dist/lsp/manager.d.ts.map +1 -0
  187. package/dist/lsp/manager.js +242 -0
  188. package/dist/lsp/manager.js.map +1 -0
  189. package/dist/lsp/ops.d.ts +13 -0
  190. package/dist/lsp/ops.d.ts.map +1 -0
  191. package/dist/lsp/ops.js +225 -0
  192. package/dist/lsp/ops.js.map +1 -0
  193. package/dist/lsp/rpc.d.ts +47 -0
  194. package/dist/lsp/rpc.d.ts.map +1 -0
  195. package/dist/lsp/rpc.js +134 -0
  196. package/dist/lsp/rpc.js.map +1 -0
  197. package/dist/lsp/safe-uri.d.ts +18 -0
  198. package/dist/lsp/safe-uri.d.ts.map +1 -0
  199. package/dist/lsp/safe-uri.js +96 -0
  200. package/dist/lsp/safe-uri.js.map +1 -0
  201. package/dist/lsp/types.d.ts +70 -0
  202. package/dist/lsp/types.d.ts.map +1 -0
  203. package/dist/lsp/types.js +16 -0
  204. package/dist/lsp/types.js.map +1 -0
  205. package/dist/mcp/bridge.d.ts +57 -0
  206. package/dist/mcp/bridge.d.ts.map +1 -0
  207. package/dist/mcp/bridge.js +98 -0
  208. package/dist/mcp/bridge.js.map +1 -0
  209. package/dist/mcp/category.d.ts +22 -0
  210. package/dist/mcp/category.d.ts.map +1 -0
  211. package/dist/mcp/category.js +11 -0
  212. package/dist/mcp/category.js.map +1 -0
  213. package/dist/mcp/client.d.ts +228 -0
  214. package/dist/mcp/client.d.ts.map +1 -0
  215. package/dist/mcp/client.js +352 -0
  216. package/dist/mcp/client.js.map +1 -0
  217. package/dist/mcp/content.d.ts +86 -0
  218. package/dist/mcp/content.d.ts.map +1 -0
  219. package/dist/mcp/content.js +147 -0
  220. package/dist/mcp/content.js.map +1 -0
  221. package/dist/mcp/env.d.ts +19 -0
  222. package/dist/mcp/env.d.ts.map +1 -0
  223. package/dist/mcp/env.js +50 -0
  224. package/dist/mcp/env.js.map +1 -0
  225. package/dist/mcp/host.d.ts +199 -0
  226. package/dist/mcp/host.d.ts.map +1 -0
  227. package/dist/mcp/host.js +530 -0
  228. package/dist/mcp/host.js.map +1 -0
  229. package/dist/mcp/index.d.ts +18 -0
  230. package/dist/mcp/index.d.ts.map +1 -0
  231. package/dist/mcp/index.js +17 -0
  232. package/dist/mcp/index.js.map +1 -0
  233. package/dist/mcp/load-mcp-host.d.ts +32 -0
  234. package/dist/mcp/load-mcp-host.d.ts.map +1 -0
  235. package/dist/mcp/load-mcp-host.js +49 -0
  236. package/dist/mcp/load-mcp-host.js.map +1 -0
  237. package/dist/mcp/oauth/callback-server.d.ts +73 -0
  238. package/dist/mcp/oauth/callback-server.d.ts.map +1 -0
  239. package/dist/mcp/oauth/callback-server.js +280 -0
  240. package/dist/mcp/oauth/callback-server.js.map +1 -0
  241. package/dist/mcp/oauth/config-hash.d.ts +24 -0
  242. package/dist/mcp/oauth/config-hash.d.ts.map +1 -0
  243. package/dist/mcp/oauth/config-hash.js +55 -0
  244. package/dist/mcp/oauth/config-hash.js.map +1 -0
  245. package/dist/mcp/oauth/error-normalize.d.ts +39 -0
  246. package/dist/mcp/oauth/error-normalize.d.ts.map +1 -0
  247. package/dist/mcp/oauth/error-normalize.js +91 -0
  248. package/dist/mcp/oauth/error-normalize.js.map +1 -0
  249. package/dist/mcp/oauth/provider.d.ts +190 -0
  250. package/dist/mcp/oauth/provider.d.ts.map +1 -0
  251. package/dist/mcp/oauth/provider.js +305 -0
  252. package/dist/mcp/oauth/provider.js.map +1 -0
  253. package/dist/mcp/oauth/refresh-coalescer.d.ts +46 -0
  254. package/dist/mcp/oauth/refresh-coalescer.d.ts.map +1 -0
  255. package/dist/mcp/oauth/refresh-coalescer.js +77 -0
  256. package/dist/mcp/oauth/refresh-coalescer.js.map +1 -0
  257. package/dist/mcp/oauth/sdk-shapes.d.ts +77 -0
  258. package/dist/mcp/oauth/sdk-shapes.d.ts.map +1 -0
  259. package/dist/mcp/oauth/sdk-shapes.js +21 -0
  260. package/dist/mcp/oauth/sdk-shapes.js.map +1 -0
  261. package/dist/mcp/parse.d.ts +28 -0
  262. package/dist/mcp/parse.d.ts.map +1 -0
  263. package/dist/mcp/parse.js +209 -0
  264. package/dist/mcp/parse.js.map +1 -0
  265. package/dist/mcp/prompts.d.ts +31 -0
  266. package/dist/mcp/prompts.d.ts.map +1 -0
  267. package/dist/mcp/prompts.js +71 -0
  268. package/dist/mcp/prompts.js.map +1 -0
  269. package/dist/mcp/redact.d.ts +62 -0
  270. package/dist/mcp/redact.d.ts.map +1 -0
  271. package/dist/mcp/redact.js +87 -0
  272. package/dist/mcp/redact.js.map +1 -0
  273. package/dist/mcp/resources.d.ts +70 -0
  274. package/dist/mcp/resources.d.ts.map +1 -0
  275. package/dist/mcp/resources.js +170 -0
  276. package/dist/mcp/resources.js.map +1 -0
  277. package/dist/mcp/types.d.ts +123 -0
  278. package/dist/mcp/types.d.ts.map +1 -0
  279. package/dist/mcp/types.js +2 -0
  280. package/dist/mcp/types.js.map +1 -0
  281. package/dist/memory/frontmatter.d.ts +18 -0
  282. package/dist/memory/frontmatter.d.ts.map +1 -0
  283. package/dist/memory/frontmatter.js +81 -0
  284. package/dist/memory/frontmatter.js.map +1 -0
  285. package/dist/memory/index.d.ts +5 -0
  286. package/dist/memory/index.d.ts.map +1 -0
  287. package/dist/memory/index.js +5 -0
  288. package/dist/memory/index.js.map +1 -0
  289. package/dist/memory/store.d.ts +44 -0
  290. package/dist/memory/store.d.ts.map +1 -0
  291. package/dist/memory/store.js +237 -0
  292. package/dist/memory/store.js.map +1 -0
  293. package/dist/memory/tools.d.ts +11 -0
  294. package/dist/memory/tools.d.ts.map +1 -0
  295. package/dist/memory/tools.js +159 -0
  296. package/dist/memory/tools.js.map +1 -0
  297. package/dist/memory/types.d.ts +32 -0
  298. package/dist/memory/types.d.ts.map +1 -0
  299. package/dist/memory/types.js +20 -0
  300. package/dist/memory/types.js.map +1 -0
  301. package/dist/plugin-api/index.d.ts +167 -0
  302. package/dist/plugin-api/index.d.ts.map +1 -0
  303. package/dist/plugin-api/index.js +151 -0
  304. package/dist/plugin-api/index.js.map +1 -0
  305. package/dist/plugin-logger/index.d.ts +21 -0
  306. package/dist/plugin-logger/index.d.ts.map +1 -0
  307. package/dist/plugin-logger/index.js +59 -0
  308. package/dist/plugin-logger/index.js.map +1 -0
  309. package/dist/session/index.d.ts +125 -0
  310. package/dist/session/index.d.ts.map +1 -0
  311. package/dist/session/index.js +202 -0
  312. package/dist/session/index.js.map +1 -0
  313. package/dist/tools/approval.d.ts +33 -0
  314. package/dist/tools/approval.d.ts.map +1 -0
  315. package/dist/tools/approval.js +53 -0
  316. package/dist/tools/approval.js.map +1 -0
  317. package/dist/tools/builtins/_shared.d.ts +94 -0
  318. package/dist/tools/builtins/_shared.d.ts.map +1 -0
  319. package/dist/tools/builtins/_shared.js +246 -0
  320. package/dist/tools/builtins/_shared.js.map +1 -0
  321. package/dist/tools/builtins/ask-user-question.d.ts +27 -0
  322. package/dist/tools/builtins/ask-user-question.d.ts.map +1 -0
  323. package/dist/tools/builtins/ask-user-question.js +191 -0
  324. package/dist/tools/builtins/ask-user-question.js.map +1 -0
  325. package/dist/tools/builtins/bash.d.ts +3 -0
  326. package/dist/tools/builtins/bash.d.ts.map +1 -0
  327. package/dist/tools/builtins/bash.js +158 -0
  328. package/dist/tools/builtins/bash.js.map +1 -0
  329. package/dist/tools/builtins/diff.d.ts +3 -0
  330. package/dist/tools/builtins/diff.d.ts.map +1 -0
  331. package/dist/tools/builtins/diff.js +83 -0
  332. package/dist/tools/builtins/diff.js.map +1 -0
  333. package/dist/tools/builtins/edit.d.ts +3 -0
  334. package/dist/tools/builtins/edit.d.ts.map +1 -0
  335. package/dist/tools/builtins/edit.js +40 -0
  336. package/dist/tools/builtins/edit.js.map +1 -0
  337. package/dist/tools/builtins/glob.d.ts +3 -0
  338. package/dist/tools/builtins/glob.d.ts.map +1 -0
  339. package/dist/tools/builtins/glob.js +37 -0
  340. package/dist/tools/builtins/glob.js.map +1 -0
  341. package/dist/tools/builtins/grep.d.ts +3 -0
  342. package/dist/tools/builtins/grep.d.ts.map +1 -0
  343. package/dist/tools/builtins/grep.js +81 -0
  344. package/dist/tools/builtins/grep.js.map +1 -0
  345. package/dist/tools/builtins/lsp.d.ts +3 -0
  346. package/dist/tools/builtins/lsp.d.ts.map +1 -0
  347. package/dist/tools/builtins/lsp.js +102 -0
  348. package/dist/tools/builtins/lsp.js.map +1 -0
  349. package/dist/tools/builtins/pty.d.ts +64 -0
  350. package/dist/tools/builtins/pty.d.ts.map +1 -0
  351. package/dist/tools/builtins/pty.js +536 -0
  352. package/dist/tools/builtins/pty.js.map +1 -0
  353. package/dist/tools/builtins/read.d.ts +3 -0
  354. package/dist/tools/builtins/read.d.ts.map +1 -0
  355. package/dist/tools/builtins/read.js +18 -0
  356. package/dist/tools/builtins/read.js.map +1 -0
  357. package/dist/tools/builtins/web-fetch.d.ts +4 -0
  358. package/dist/tools/builtins/web-fetch.d.ts.map +1 -0
  359. package/dist/tools/builtins/web-fetch.js +353 -0
  360. package/dist/tools/builtins/web-fetch.js.map +1 -0
  361. package/dist/tools/builtins/write.d.ts +3 -0
  362. package/dist/tools/builtins/write.d.ts.map +1 -0
  363. package/dist/tools/builtins/write.js +48 -0
  364. package/dist/tools/builtins/write.js.map +1 -0
  365. package/dist/tools/builtins.d.ts +9 -0
  366. package/dist/tools/builtins.d.ts.map +1 -0
  367. package/dist/tools/builtins.js +29 -0
  368. package/dist/tools/builtins.js.map +1 -0
  369. package/dist/tools/diff.d.ts +18 -0
  370. package/dist/tools/diff.d.ts.map +1 -0
  371. package/dist/tools/diff.js +57 -0
  372. package/dist/tools/diff.js.map +1 -0
  373. package/dist/tools/index.d.ts +10 -0
  374. package/dist/tools/index.d.ts.map +1 -0
  375. package/dist/tools/index.js +9 -0
  376. package/dist/tools/index.js.map +1 -0
  377. package/dist/tools/permission.d.ts +120 -0
  378. package/dist/tools/permission.d.ts.map +1 -0
  379. package/dist/tools/permission.js +208 -0
  380. package/dist/tools/permission.js.map +1 -0
  381. package/dist/tools/registry.d.ts +12 -0
  382. package/dist/tools/registry.d.ts.map +1 -0
  383. package/dist/tools/registry.js +19 -0
  384. package/dist/tools/registry.js.map +1 -0
  385. package/dist/tools/types.d.ts +244 -0
  386. package/dist/tools/types.d.ts.map +1 -0
  387. package/dist/tools/types.js +15 -0
  388. package/dist/tools/types.js.map +1 -0
  389. package/package.json +109 -0
@@ -0,0 +1,202 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { createId } from "@chances-ai/runtime";
4
+ /** Persists sessions as JSON under <workspaceRoot>/.chances/sessions. */
5
+ export class SessionStore {
6
+ dir;
7
+ constructor(workspaceRoot) {
8
+ this.dir = join(workspaceRoot, ".chances", "sessions");
9
+ mkdirSync(this.dir, { recursive: true });
10
+ }
11
+ save(state) {
12
+ writeFileSync(join(this.dir, `${state.id}.json`), JSON.stringify(state, null, 2));
13
+ }
14
+ load(id) {
15
+ const path = join(this.dir, `${id}.json`);
16
+ if (!existsSync(path))
17
+ return undefined;
18
+ try {
19
+ return JSON.parse(readFileSync(path, "utf8"));
20
+ }
21
+ catch {
22
+ return undefined;
23
+ }
24
+ }
25
+ list() {
26
+ return readdirSync(this.dir)
27
+ .filter((f) => f.endsWith(".json"))
28
+ .flatMap((f) => {
29
+ try {
30
+ return [JSON.parse(readFileSync(join(this.dir, f), "utf8"))];
31
+ }
32
+ catch {
33
+ return [];
34
+ }
35
+ })
36
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
37
+ }
38
+ }
39
+ /**
40
+ * Owns the session lifecycle (state machine): create/resume, append turns,
41
+ * flatten to a provider message list, checkpoint and restore.
42
+ */
43
+ export class SessionManager {
44
+ store;
45
+ state;
46
+ constructor(state, store) {
47
+ this.store = store;
48
+ this.state = state;
49
+ }
50
+ static create(title = "session", store) {
51
+ const now = new Date().toISOString();
52
+ return new SessionManager({ id: createId("ses"), title, createdAt: now, updatedAt: now, turns: [] }, store);
53
+ }
54
+ static resume(id, store) {
55
+ const state = store.load(id);
56
+ return state ? new SessionManager(state, store) : undefined;
57
+ }
58
+ /**
59
+ * (5.5) Creates a NEW session pre-seeded with a deep clone of `parent`'s
60
+ * completed turns — the seam behind the `task` tool's `context: "fork"` mode.
61
+ *
62
+ * Three invariants make this safe:
63
+ * - **New id, no `SessionStore`.** The child is a distinct session that can
64
+ * never `save()` over the parent's `<id>.json` on disk (mirrors
65
+ * `create("subagent")`, which also omits the store). All mutation stays in
66
+ * the child's own in-memory state.
67
+ * - **Whole turns, never a mid-turn slice.** chances-cli persists an
68
+ * assistant message carrying `tool-call` parts and its `tool` results in
69
+ * one atomic turn (see `engine.ts` end-of-turn `appendTurn`), so copying at
70
+ * turn granularity keeps every `tool-call` paired with its result — the
71
+ * provider's no-orphan-`tool_use` invariant holds for free.
72
+ * - **The session `model` field is NOT carried.** The forked child's model is
73
+ * chosen by the caller (persona override or inherited parent selection),
74
+ * not by the parent session's last `/model` choice. `snapshot()` deep-clones
75
+ * the whole state; we keep only `turns`.
76
+ */
77
+ static forkFrom(parent, title = "subagent") {
78
+ const now = new Date().toISOString();
79
+ return new SessionManager({ id: createId("ses"), title, createdAt: now, updatedAt: now, turns: parent.snapshot().turns }, undefined);
80
+ }
81
+ get id() {
82
+ return this.state.id;
83
+ }
84
+ /** All messages across turns, ready to send to a provider. */
85
+ messages() {
86
+ return this.state.turns.flatMap((t) => t.messages);
87
+ }
88
+ appendTurn(messages) {
89
+ const turn = { turnId: createId("turn"), at: new Date().toISOString(), messages };
90
+ this.state.turns.push(turn);
91
+ this.state.updatedAt = turn.at;
92
+ this.store?.save(this.state);
93
+ return turn;
94
+ }
95
+ /** Records the user's current model choice so `/resume` can restore it.
96
+ * Persisted immediately; not gated behind the next `appendTurn`. */
97
+ setModel(model) {
98
+ this.state.model = model;
99
+ this.state.updatedAt = new Date().toISOString();
100
+ this.store?.save(this.state);
101
+ }
102
+ /** Returns the persisted model choice for this session, if any. */
103
+ modelChoice() {
104
+ return this.state.model ? { ...this.state.model } : undefined;
105
+ }
106
+ /**
107
+ * Empties this session's turn list. Used by `/clear` when the user wants a
108
+ * fresh conversation without losing the session metadata (id/title). The
109
+ * underlying file is rewritten so a `/resume` of this id will land on an
110
+ * empty conversation.
111
+ */
112
+ clearTurns() {
113
+ this.state.turns = [];
114
+ this.state.updatedAt = new Date().toISOString();
115
+ this.store?.save(this.state);
116
+ }
117
+ /**
118
+ * (3.5) Applies a pure transform to every turn in the prefix
119
+ * (all but the last `keepRecentTurns` turns). Used by the Stage 1
120
+ * pruner — the transform receives one turn's messages, returns
121
+ * the rewritten messages. The turn's `turnId` / `at` are
122
+ * unchanged so persistence keeps its identity; the synthetic
123
+ * `compact-*` turn (from `compactTurns`) is treated like any
124
+ * other turn for indexing purposes.
125
+ *
126
+ * `keepRecentTurns` is clamped to `[0, turns.length]`. When
127
+ * `keepRecentTurns >= turns.length`, this is a no-op (nothing to
128
+ * prune). When 0, every turn including the most recent is fed to
129
+ * the transform.
130
+ *
131
+ * Idempotent if the transform is idempotent (the pruner's marker
132
+ * replacement is — second pass sees the marker, fails the
133
+ * delta>0 check, leaves it alone).
134
+ */
135
+ pruneTurns(keepRecentTurns, transform) {
136
+ const total = this.state.turns.length;
137
+ const cutoff = Math.max(0, total - Math.max(0, keepRecentTurns));
138
+ if (cutoff === 0)
139
+ return;
140
+ let mutated = false;
141
+ for (let i = 0; i < cutoff; i++) {
142
+ const t = this.state.turns[i];
143
+ const newMessages = transform(t.messages);
144
+ if (newMessages !== t.messages) {
145
+ this.state.turns[i] = { ...t, messages: newMessages };
146
+ mutated = true;
147
+ }
148
+ }
149
+ if (mutated) {
150
+ this.state.updatedAt = new Date().toISOString();
151
+ this.store?.save(this.state);
152
+ }
153
+ }
154
+ /**
155
+ * Replaces all but the last `keepLastN` turns with a single synthetic
156
+ * "system context" turn carrying `summary` as a user-role message. Why
157
+ * user-role rather than system-role: providers vary in how they handle
158
+ * mid-conversation system messages, but every provider accepts a long-form
159
+ * user message as historical context. The synthetic turn's `turnId` is
160
+ * tagged `compact-*` so debuggers can spot it.
161
+ *
162
+ * `keepLastN` is clamped to `[0, turns.length]` — passing a larger N keeps
163
+ * everything (compaction is a no-op), passing 0 collapses the whole
164
+ * conversation into the summary. The summary itself must be non-empty;
165
+ * callers should bail before invoking this when the summarizer returned
166
+ * nothing.
167
+ */
168
+ compactTurns(summary, keepLastN) {
169
+ if (!summary)
170
+ throw new Error("compactTurns: summary must be a non-empty string");
171
+ if (keepLastN < 0)
172
+ throw new Error("compactTurns: keepLastN must be >= 0");
173
+ const total = this.state.turns.length;
174
+ if (keepLastN >= total)
175
+ return; // nothing to compact
176
+ const recent = this.state.turns.slice(total - keepLastN);
177
+ const synthetic = {
178
+ turnId: `compact-${createId("turn")}`,
179
+ at: new Date().toISOString(),
180
+ messages: [
181
+ {
182
+ role: "user",
183
+ content: [{ type: "text", text: `PREVIOUS_CONVERSATION_SUMMARY:\n${summary}` }],
184
+ },
185
+ ],
186
+ };
187
+ this.state.turns = [synthetic, ...recent];
188
+ this.state.updatedAt = synthetic.at;
189
+ this.store?.save(this.state);
190
+ }
191
+ checkpoint() {
192
+ return { turnIndex: this.state.turns.length, state: structuredClone(this.state) };
193
+ }
194
+ restore(checkpoint) {
195
+ this.state = structuredClone(checkpoint.state);
196
+ this.store?.save(this.state);
197
+ }
198
+ snapshot() {
199
+ return structuredClone(this.state);
200
+ }
201
+ }
202
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/session/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import type { ToolCategory } from "@chances-ai/runtime/config";
2
+ import type { ApprovalMode, ToolTier } from "@chances-ai/runtime";
3
+ /**
4
+ * (5.3) Pure tier logic backing the session approval modes. Lives in
5
+ * `@chances-ai/tools` (next to the gate, its only consumer) rather than in
6
+ * config/runtime so the tables sit beside the code that reads them; the bare
7
+ * `ApprovalMode`/`ToolTier` type unions live in `@chances-ai/runtime` to avoid
8
+ * a config↔runtime import cycle (codex 5.3 Round-1 MUST-FIX #3). See
9
+ * docs/5.3-design.md § 3 (D1/D2/D3).
10
+ */
11
+ /**
12
+ * Each tool category's capability tier. `network-read` (web_fetch) and
13
+ * `integration` (MCP-bridged tools) are **exec**: the first is per-URL-sensitive
14
+ * egress (see `defaultPolicy` rationale in config/types.ts), the second can do
15
+ * anything the server implements. So `auto-edit` prompts both, and `plan`
16
+ * blocks both.
17
+ */
18
+ export declare const CATEGORY_TIER: Record<ToolCategory, ToolTier>;
19
+ /**
20
+ * True when `mode` blocks the entire `category` outright (plan's read-only
21
+ * floor). The single source of truth shared by `PermissionGate.evaluate()` AND
22
+ * the engine's pre-hook `blockByMode` check — so a `Tool.permission()` that
23
+ * returns `null` to bypass the gate (e.g. `pty.read`) can't sneak past plan
24
+ * mode (codex 5.3 Round-1 MUST-FIX #1).
25
+ */
26
+ export declare function isBlockedByMode(category: ToolCategory, mode: ApprovalMode): boolean;
27
+ /** True when `mode` auto-approves the entire `category` (yolo / auto-edit),
28
+ * i.e. the interactive prompt is skipped and the call runs. */
29
+ export declare function isAutoApprovedByMode(category: ToolCategory, mode: ApprovalMode): boolean;
30
+ /** Human-readable reason for a mode-driven block, surfaced in the tool result
31
+ * so the model (and user) sees why a call was refused. */
32
+ export declare function modeBlockReason(mode: ApprovalMode): string;
33
+ //# sourceMappingURL=approval.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approval.d.ts","sourceRoot":"","sources":["../../src/tools/approval.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAElE;;;;;;;GAOG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,YAAY,EAAE,QAAQ,CASxD,CAAC;AAmBF;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAEnF;AAED;gEACgE;AAChE,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAExF;AAED;2DAC2D;AAC3D,wBAAgB,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CAE1D"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * (5.3) Pure tier logic backing the session approval modes. Lives in
3
+ * `@chances-ai/tools` (next to the gate, its only consumer) rather than in
4
+ * config/runtime so the tables sit beside the code that reads them; the bare
5
+ * `ApprovalMode`/`ToolTier` type unions live in `@chances-ai/runtime` to avoid
6
+ * a config↔runtime import cycle (codex 5.3 Round-1 MUST-FIX #3). See
7
+ * docs/5.3-design.md § 3 (D1/D2/D3).
8
+ */
9
+ /**
10
+ * Each tool category's capability tier. `network-read` (web_fetch) and
11
+ * `integration` (MCP-bridged tools) are **exec**: the first is per-URL-sensitive
12
+ * egress (see `defaultPolicy` rationale in config/types.ts), the second can do
13
+ * anything the server implements. So `auto-edit` prompts both, and `plan`
14
+ * blocks both.
15
+ */
16
+ export const CATEGORY_TIER = {
17
+ "file-read": "read",
18
+ search: "read",
19
+ "memory-read": "read",
20
+ "file-write": "write",
21
+ "memory-write": "write",
22
+ shell: "exec",
23
+ integration: "exec",
24
+ "network-read": "exec",
25
+ };
26
+ const TIER_RANK = { read: 0, write: 1, exec: 2 };
27
+ const MODE_PROFILE = {
28
+ default: { autoApproveMaxTier: -1, blockAboveTier: 2 }, // pure passthrough to policy
29
+ "auto-edit": { autoApproveMaxTier: 1, blockAboveTier: 2 }, // auto read+write, prompt exec
30
+ plan: { autoApproveMaxTier: -1, blockAboveTier: 0 }, // read-only: block write+exec
31
+ yolo: { autoApproveMaxTier: 2, blockAboveTier: 2 }, // auto-approve every tier
32
+ };
33
+ /**
34
+ * True when `mode` blocks the entire `category` outright (plan's read-only
35
+ * floor). The single source of truth shared by `PermissionGate.evaluate()` AND
36
+ * the engine's pre-hook `blockByMode` check — so a `Tool.permission()` that
37
+ * returns `null` to bypass the gate (e.g. `pty.read`) can't sneak past plan
38
+ * mode (codex 5.3 Round-1 MUST-FIX #1).
39
+ */
40
+ export function isBlockedByMode(category, mode) {
41
+ return TIER_RANK[CATEGORY_TIER[category]] > MODE_PROFILE[mode].blockAboveTier;
42
+ }
43
+ /** True when `mode` auto-approves the entire `category` (yolo / auto-edit),
44
+ * i.e. the interactive prompt is skipped and the call runs. */
45
+ export function isAutoApprovedByMode(category, mode) {
46
+ return TIER_RANK[CATEGORY_TIER[category]] <= MODE_PROFILE[mode].autoApproveMaxTier;
47
+ }
48
+ /** Human-readable reason for a mode-driven block, surfaced in the tool result
49
+ * so the model (and user) sees why a call was refused. */
50
+ export function modeBlockReason(mode) {
51
+ return `Blocked: '${mode}' approval mode is read-only — exit ${mode} mode (shift+tab) to run mutating tools.`;
52
+ }
53
+ //# sourceMappingURL=approval.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approval.js","sourceRoot":"","sources":["../../src/tools/approval.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,aAAa,GAAmC;IAC3D,WAAW,EAAE,MAAM;IACnB,MAAM,EAAE,MAAM;IACd,aAAa,EAAE,MAAM;IACrB,YAAY,EAAE,OAAO;IACrB,cAAc,EAAE,OAAO;IACvB,KAAK,EAAE,MAAM;IACb,WAAW,EAAE,MAAM;IACnB,cAAc,EAAE,MAAM;CACvB,CAAC;AAEF,MAAM,SAAS,GAA6B,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;AAU3E,MAAM,YAAY,GAAsC;IACtD,OAAO,EAAE,EAAE,kBAAkB,EAAE,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,EAAE,6BAA6B;IACrF,WAAW,EAAE,EAAE,kBAAkB,EAAE,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,EAAE,+BAA+B;IAC1F,IAAI,EAAE,EAAE,kBAAkB,EAAE,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,EAAE,8BAA8B;IACnF,IAAI,EAAE,EAAE,kBAAkB,EAAE,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,EAAE,0BAA0B;CAC/E,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,QAAsB,EAAE,IAAkB;IACxE,OAAO,SAAS,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,cAAc,CAAC;AAChF,CAAC;AAED;gEACgE;AAChE,MAAM,UAAU,oBAAoB,CAAC,QAAsB,EAAE,IAAkB;IAC7E,OAAO,SAAS,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC,kBAAkB,CAAC;AACrF,CAAC;AAED;2DAC2D;AAC3D,MAAM,UAAU,eAAe,CAAC,IAAkB;IAChD,OAAO,aAAa,IAAI,uCAAuC,IAAI,0CAA0C,CAAC;AAChH,CAAC"}
@@ -0,0 +1,94 @@
1
+ import type { JSONValue } from "@chances-ai/runtime";
2
+ import type { ToolContext } from "../types.js";
3
+ /** Diff/write-preview cap: refuses to read either side of a comparison when it
4
+ * exceeds this many bytes, so a `write` against a generated 50 MB file doesn't
5
+ * stall the approval prompt on a synchronous read. */
6
+ export declare const PREVIEW_BYTE_CAP: number;
7
+ export declare function str(args: JSONValue, key: string): string;
8
+ export declare function optStr(args: JSONValue, key: string): string | undefined;
9
+ export declare function optNum(args: JSONValue, key: string): number | undefined;
10
+ export declare function optBool(args: JSONValue, key: string): boolean | undefined;
11
+ /** Resolves a path and refuses to escape the workspace root.
12
+ *
13
+ * Symlinks are resolved through `realpathSync` *before* the containment
14
+ * check whenever EITHER condition holds:
15
+ *
16
+ * - **Isolated context** (`runWithCwd` active — i.e. an
17
+ * `isolation: 'worktree'` subagent): always, so a tracked symlink
18
+ * inside the worktree that points outside it is rejected
19
+ * (4.1 codex Round-1 MUST-FIX #5).
20
+ * - **`opts.resolveSymlinks`** set by the caller: the file-write tools
21
+ * (`write`/`edit`) pass it so that, even in the non-isolated parent
22
+ * context, a workspace-internal symlink pointing OUTSIDE can't be
23
+ * *written through* to plant/modify a file outside the workspace
24
+ * (e.g. a git hook, build script, or dotfile). This closes the
25
+ * write-escape hole that lexical-only containment leaves open — for the
26
+ * full symlink chain (`realpathExtant` walks every hop and fails closed
27
+ * past its depth cap), and `write`/`edit` write to the *resolved* path so
28
+ * the symlink isn't re-traversed after the check.
29
+ *
30
+ * **Residual (out of the single-agent threat model, not closed here):** a
31
+ * classic TOCTOU still exists if a *concurrent* actor swaps a parent dir
32
+ * for an outbound symlink in the window between this check and the
33
+ * `writeFileSync`. Fully closing it needs an `openat`/`O_NOFOLLOW` path
34
+ * walk (no clean Node API); documented as a future-hardening pointer.
35
+ *
36
+ * In the resolving branch, both `workspaceRoot` and the target are
37
+ * realpath'd so the comparison is between fully-resolved trees; for
38
+ * non-existent leaves (the standard `write` case) and dangling symlinks,
39
+ * `realpathExtant` walks the symlink chain up to the nearest existing
40
+ * ancestor and re-joins the missing tail.
41
+ *
42
+ * **Windows junctions.** An NTFS junction (a directory reparse point) is
43
+ * NOT reported as a symlink by `lstat().isSymbolicLink()`, so it skips the
44
+ * manual `readlink` branch in `realpathExtant` — but `realpathSync` DOES
45
+ * traverse it, so a write target under a junction that points outside the
46
+ * workspace still resolves outside and is rejected by the containment check.
47
+ * Covered by the win32-only junction tests in `_shared.symlink.test.ts` (D-C).
48
+ *
49
+ * **Read-class tools** (`read`/`grep`/`glob`/`diff`/`lsp`) deliberately
50
+ * stay lexical in the non-isolated context: reading *through* an inbound
51
+ * symlink (a config/fixture symlinked into a repo) is a common, legitimate
52
+ * pattern, and the high-severity risk is *writing* outside the workspace,
53
+ * not reading a file the caller's own UID can already read. (claude-code
54
+ * resolves reads too; we intentionally keep them lexical here to avoid
55
+ * breaking legit symlinked-file reads — revisit if a read-escape threat
56
+ * emerges.)
57
+ */
58
+ export declare function safePath(ctx: ToolContext, p: string, opts?: {
59
+ resolveSymlinks?: boolean;
60
+ }): string;
61
+ /** Render a string as a short JSON-quoted preview for permission prompts. */
62
+ export declare function preview(s: string): string;
63
+ export { cleanOutput, normaliseLineEndings, stripAnsi } from "@chances-ai/runtime";
64
+ /** Map an engine call id to a filesystem-safe artifact-dir name (5.7 / codex
65
+ * R1 M2). A clean id (`toolu_…`, `call_…`) is used **verbatim** — that branch is
66
+ * injective and the dir name stays human-readable, joining 1:1 to the
67
+ * `tool_result` the client saw. Anything else — a hostile/unusual provider id
68
+ * containing `/`, `..`, or other bytes, OR an empty string — is replaced
69
+ * WHOLESALE by `id-<sha256[:16]>` rather than lossily mapped (`a/b` and `a_b`
70
+ * must NOT collide into one dir and overwrite each other). That hash branch is
71
+ * **collision-resistant, not strictly injective** (a 64-bit truncation), but
72
+ * for the volume of tool calls in a session the collision probability is
73
+ * negligible and a collision is never a security issue (worst case: two
74
+ * unrelated calls share a dir — never a traversal). Traversal is impossible in
75
+ * both branches: verbatim only admits `[A-Za-z0-9_-]`, the hash only emits hex. */
76
+ export declare function safeArtifactId(raw: string): string;
77
+ /** Best-effort persistence under `<root>/.chances/tool-results/<id>/<file>`
78
+ * with restrictive POSIX permissions (0o700 dir, 0o600 file). Returns the
79
+ * absolute path on success, `null` on filesystem failure (the caller's
80
+ * banner still renders an inline preview either way, so persistence
81
+ * failure degrades to "model only sees the head"; not a fatal tool
82
+ * error).
83
+ *
84
+ * `id` is the engine's `ctx.callId` (5.7) when available — the artifact dir is
85
+ * then named for the real call so a client correlates the saved file with the
86
+ * `tool_result`. Falls back to a fresh `createId(prefix)` for non-engine
87
+ * callers (tests/plugins) — fully backward compatible.
88
+ *
89
+ * **Windows caveat.** Node's `fs` `mode` flag only affects the read-only
90
+ * bit on Windows; ACL-based access control is not enforced via this
91
+ * argument. POSIX-first audience; explicit DACL hardening is a follow-up.
92
+ */
93
+ export declare function persistFullOutput(workspaceRoot: string, body: string, prefix?: string, filename?: string, id?: string): string | null;
94
+ //# sourceMappingURL=_shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_shared.d.ts","sourceRoot":"","sources":["../../../src/tools/builtins/_shared.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C;;sDAEsD;AACtD,eAAO,MAAM,gBAAgB,QAAa,CAAC;AAE3C,wBAAgB,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAIxD;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAKvE;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOvE;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAKzE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,wBAAgB,QAAQ,CACtB,GAAG,EAAE,WAAW,EAChB,CAAC,EAAE,MAAM,EACT,IAAI,CAAC,EAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAA;CAAE,GACnC,MAAM,CAqBR;AA0ED,6EAA6E;AAC7E,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAGzC;AAaD,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAEnF;;;;;;;;;;;mFAWmF;AACnF,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAGlD;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAC/B,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,MAAM,EACZ,MAAM,SAAS,EACf,QAAQ,SAAe,EACvB,EAAE,CAAC,EAAE,MAAM,GACV,MAAM,GAAG,IAAI,CAef"}
@@ -0,0 +1,246 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstatSync, mkdirSync, readlinkSync, realpathSync, writeFileSync } from "node:fs";
3
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
+ import { AppError, ErrorCode, createId, currentCwdOverride } from "@chances-ai/runtime";
5
+ /** Diff/write-preview cap: refuses to read either side of a comparison when it
6
+ * exceeds this many bytes, so a `write` against a generated 50 MB file doesn't
7
+ * stall the approval prompt on a synchronous read. */
8
+ export const PREVIEW_BYTE_CAP = 256 * 1024;
9
+ export function str(args, key) {
10
+ const v = args[key];
11
+ if (typeof v !== "string")
12
+ throw new AppError(ErrorCode.Tool, `Expected string arg "${key}"`);
13
+ return v;
14
+ }
15
+ export function optStr(args, key) {
16
+ const v = args[key];
17
+ if (v === undefined || v === null)
18
+ return undefined;
19
+ if (typeof v !== "string")
20
+ throw new AppError(ErrorCode.Tool, `Expected string arg "${key}"`);
21
+ return v;
22
+ }
23
+ export function optNum(args, key) {
24
+ const v = args[key];
25
+ if (v === undefined || v === null)
26
+ return undefined;
27
+ if (typeof v !== "number" || !Number.isFinite(v)) {
28
+ throw new AppError(ErrorCode.Tool, `Expected number arg "${key}"`);
29
+ }
30
+ return v;
31
+ }
32
+ export function optBool(args, key) {
33
+ const v = args[key];
34
+ if (v === undefined || v === null)
35
+ return undefined;
36
+ if (typeof v !== "boolean")
37
+ throw new AppError(ErrorCode.Tool, `Expected boolean arg "${key}"`);
38
+ return v;
39
+ }
40
+ /** Resolves a path and refuses to escape the workspace root.
41
+ *
42
+ * Symlinks are resolved through `realpathSync` *before* the containment
43
+ * check whenever EITHER condition holds:
44
+ *
45
+ * - **Isolated context** (`runWithCwd` active — i.e. an
46
+ * `isolation: 'worktree'` subagent): always, so a tracked symlink
47
+ * inside the worktree that points outside it is rejected
48
+ * (4.1 codex Round-1 MUST-FIX #5).
49
+ * - **`opts.resolveSymlinks`** set by the caller: the file-write tools
50
+ * (`write`/`edit`) pass it so that, even in the non-isolated parent
51
+ * context, a workspace-internal symlink pointing OUTSIDE can't be
52
+ * *written through* to plant/modify a file outside the workspace
53
+ * (e.g. a git hook, build script, or dotfile). This closes the
54
+ * write-escape hole that lexical-only containment leaves open — for the
55
+ * full symlink chain (`realpathExtant` walks every hop and fails closed
56
+ * past its depth cap), and `write`/`edit` write to the *resolved* path so
57
+ * the symlink isn't re-traversed after the check.
58
+ *
59
+ * **Residual (out of the single-agent threat model, not closed here):** a
60
+ * classic TOCTOU still exists if a *concurrent* actor swaps a parent dir
61
+ * for an outbound symlink in the window between this check and the
62
+ * `writeFileSync`. Fully closing it needs an `openat`/`O_NOFOLLOW` path
63
+ * walk (no clean Node API); documented as a future-hardening pointer.
64
+ *
65
+ * In the resolving branch, both `workspaceRoot` and the target are
66
+ * realpath'd so the comparison is between fully-resolved trees; for
67
+ * non-existent leaves (the standard `write` case) and dangling symlinks,
68
+ * `realpathExtant` walks the symlink chain up to the nearest existing
69
+ * ancestor and re-joins the missing tail.
70
+ *
71
+ * **Windows junctions.** An NTFS junction (a directory reparse point) is
72
+ * NOT reported as a symlink by `lstat().isSymbolicLink()`, so it skips the
73
+ * manual `readlink` branch in `realpathExtant` — but `realpathSync` DOES
74
+ * traverse it, so a write target under a junction that points outside the
75
+ * workspace still resolves outside and is rejected by the containment check.
76
+ * Covered by the win32-only junction tests in `_shared.symlink.test.ts` (D-C).
77
+ *
78
+ * **Read-class tools** (`read`/`grep`/`glob`/`diff`/`lsp`) deliberately
79
+ * stay lexical in the non-isolated context: reading *through* an inbound
80
+ * symlink (a config/fixture symlinked into a repo) is a common, legitimate
81
+ * pattern, and the high-severity risk is *writing* outside the workspace,
82
+ * not reading a file the caller's own UID can already read. (claude-code
83
+ * resolves reads too; we intentionally keep them lexical here to avoid
84
+ * breaking legit symlinked-file reads — revisit if a read-escape threat
85
+ * emerges.)
86
+ */
87
+ export function safePath(ctx, p, opts) {
88
+ const abs = isAbsolute(p) ? p : resolve(ctx.cwd, p);
89
+ const isolated = currentCwdOverride() !== undefined;
90
+ if (isolated || opts?.resolveSymlinks) {
91
+ // Realpath both sides, then re-check against the fully-resolved trees.
92
+ const rootReal = realpathSafe(ctx.workspaceRoot);
93
+ const absReal = realpathExtant(abs);
94
+ const rel = relative(rootReal, absReal);
95
+ if (rel.startsWith("..") || isAbsolute(rel)) {
96
+ throw new AppError(ErrorCode.Permission, `Path escapes workspace: ${p}`);
97
+ }
98
+ return absReal;
99
+ }
100
+ // Non-isolated read-class path: lexical containment only, unchanged since 1.x.
101
+ const rel = relative(ctx.workspaceRoot, abs);
102
+ if (rel.startsWith("..") || isAbsolute(rel)) {
103
+ throw new AppError(ErrorCode.Permission, `Path escapes workspace: ${p}`);
104
+ }
105
+ return abs;
106
+ }
107
+ /** Realpath an existing path. We always expect `workspaceRoot` to exist
108
+ * when invoked through the engine; if it doesn't we fall back to the
109
+ * original (lexical containment is then a no-op safety net). */
110
+ function realpathSafe(p) {
111
+ try {
112
+ return realpathSync(p);
113
+ }
114
+ catch {
115
+ return p;
116
+ }
117
+ }
118
+ /** Realpath a path that may or may not exist, resolving any symlinks
119
+ * at the leaf — including DANGLING symlinks (Round-2 MUST-FIX #3). A
120
+ * symlink whose target doesn't exist would otherwise be accepted by the
121
+ * lexical containment check; a subsequent `write` would then follow the
122
+ * symlink and create the target OUTSIDE the worktree. We catch this by
123
+ * `lstat`-ing the leaf first; if it's a symlink, we resolve the link
124
+ * target manually (relative-to-link-dir if not absolute) and recurse.
125
+ * Bounded by `SYMLINK_RESOLVE_MAX_DEPTH` to defeat link cycles.
126
+ *
127
+ * On depth exhaustion we **fail closed** (throw) rather than returning the
128
+ * unresolved path: returning the lexically-in-workspace symlink would let a
129
+ * `write` follow a >8-hop chain OUTSIDE the workspace — the very escape this
130
+ * resolution prevents (5.6-era symlink-hardening codex MUST-FIX). A legit
131
+ * path practically never chains >8 symlinks, so rejecting is the safe choice. */
132
+ const SYMLINK_RESOLVE_MAX_DEPTH = 8;
133
+ function realpathExtant(p, depth = 0) {
134
+ // Detect symlinks (live OR dangling) at the leaf BEFORE the
135
+ // `realpathSync` happy path — dangling symlinks make `realpathSync`
136
+ // throw ENOENT, which we previously swallowed and rejoined the leaf
137
+ // verbatim (the bug).
138
+ try {
139
+ const lst = lstatSync(p);
140
+ if (lst.isSymbolicLink()) {
141
+ if (depth >= SYMLINK_RESOLVE_MAX_DEPTH) {
142
+ throw new AppError(ErrorCode.Permission, `Symlink chain too deep (possible workspace escape): ${p}`);
143
+ }
144
+ const target = readlinkSync(p);
145
+ const resolved = isAbsolute(target) ? target : resolve(dirname(p), target);
146
+ return realpathExtant(resolved, depth + 1);
147
+ }
148
+ }
149
+ catch (e) {
150
+ // Propagate our own fail-closed throw (and any deeper one from the
151
+ // recursion); only swallow the expected fs errors (lstat ENOENT on a
152
+ // non-existent leaf), which fall through to the ancestor walk below.
153
+ if (e instanceof AppError)
154
+ throw e;
155
+ }
156
+ try {
157
+ return realpathSync(p);
158
+ }
159
+ catch {
160
+ /* fall through */
161
+ }
162
+ let current = dirname(p);
163
+ let tail = p.slice(current.length);
164
+ while (current !== dirname(current)) {
165
+ try {
166
+ const real = realpathSync(current);
167
+ return join(real, tail);
168
+ }
169
+ catch {
170
+ tail = current.slice(dirname(current).length) + tail;
171
+ current = dirname(current);
172
+ }
173
+ }
174
+ // Reached the root without finding an existing ancestor; return the
175
+ // path lexically resolved (this is the standard cwd-relative form, so
176
+ // containment falls through to the lexical check above).
177
+ return p;
178
+ }
179
+ /** Render a string as a short JSON-quoted preview for permission prompts. */
180
+ export function preview(s) {
181
+ const one = s.replace(/\s+/g, " ").trim();
182
+ return JSON.stringify(one.length > 40 ? one.slice(0, 40) + "…" : one);
183
+ }
184
+ // ---------------------------------------------------------------------------
185
+ // (5.1) Shared output-cleaning + persistence helpers used by `bash` and
186
+ // the new `pty` interactive tool. Both tools strip ANSI before showing
187
+ // the model the body, and persist oversized bodies to
188
+ // `.chances/tool-results/<id>/<file>` under restrictive POSIX perms.
189
+ //
190
+ // (5.7) The ANSI / control-char strippers moved to `@chances-ai/runtime`
191
+ // (`text-sanitize.ts`) so the bash tool, the pty registry, and the MCP /
192
+ // compaction / agents sanitizers all share ONE audited implementation
193
+ // instead of byte-copied regexes. Re-exported here so existing
194
+ // `./_shared.js` importers (bash, pty) are unchanged.
195
+ export { cleanOutput, normaliseLineEndings, stripAnsi } from "@chances-ai/runtime";
196
+ /** Map an engine call id to a filesystem-safe artifact-dir name (5.7 / codex
197
+ * R1 M2). A clean id (`toolu_…`, `call_…`) is used **verbatim** — that branch is
198
+ * injective and the dir name stays human-readable, joining 1:1 to the
199
+ * `tool_result` the client saw. Anything else — a hostile/unusual provider id
200
+ * containing `/`, `..`, or other bytes, OR an empty string — is replaced
201
+ * WHOLESALE by `id-<sha256[:16]>` rather than lossily mapped (`a/b` and `a_b`
202
+ * must NOT collide into one dir and overwrite each other). That hash branch is
203
+ * **collision-resistant, not strictly injective** (a 64-bit truncation), but
204
+ * for the volume of tool calls in a session the collision probability is
205
+ * negligible and a collision is never a security issue (worst case: two
206
+ * unrelated calls share a dir — never a traversal). Traversal is impossible in
207
+ * both branches: verbatim only admits `[A-Za-z0-9_-]`, the hash only emits hex. */
208
+ export function safeArtifactId(raw) {
209
+ if (/^[A-Za-z0-9_-]{1,128}$/.test(raw))
210
+ return raw;
211
+ return `id-${createHash("sha256").update(raw).digest("hex").slice(0, 16)}`;
212
+ }
213
+ /** Best-effort persistence under `<root>/.chances/tool-results/<id>/<file>`
214
+ * with restrictive POSIX permissions (0o700 dir, 0o600 file). Returns the
215
+ * absolute path on success, `null` on filesystem failure (the caller's
216
+ * banner still renders an inline preview either way, so persistence
217
+ * failure degrades to "model only sees the head"; not a fatal tool
218
+ * error).
219
+ *
220
+ * `id` is the engine's `ctx.callId` (5.7) when available — the artifact dir is
221
+ * then named for the real call so a client correlates the saved file with the
222
+ * `tool_result`. Falls back to a fresh `createId(prefix)` for non-engine
223
+ * callers (tests/plugins) — fully backward compatible.
224
+ *
225
+ * **Windows caveat.** Node's `fs` `mode` flag only affects the read-only
226
+ * bit on Windows; ACL-based access control is not enforced via this
227
+ * argument. POSIX-first audience; explicit DACL hardening is a follow-up.
228
+ */
229
+ export function persistFullOutput(workspaceRoot, body, prefix = "tool", filename = "output.txt", id) {
230
+ try {
231
+ // `id !== undefined` (not truthiness) so an empty-string call id still
232
+ // routes through `safeArtifactId` (→ a hash dir) instead of silently
233
+ // falling back to a random `createId`, which would break correlation
234
+ // with `tool_result.callId === ""` (codex R2 Q2).
235
+ const dirName = id !== undefined ? safeArtifactId(id) : createId(prefix);
236
+ const dir = join(workspaceRoot, ".chances", "tool-results", dirName);
237
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
238
+ const path = join(dir, filename);
239
+ writeFileSync(path, body, { mode: 0o600 });
240
+ return path;
241
+ }
242
+ catch {
243
+ return null;
244
+ }
245
+ }
246
+ //# sourceMappingURL=_shared.js.map