@nathapp/nax-agent 0.1.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 (398) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +49 -0
  4. package/dist/coding-tools/coding-tool-bash.d.ts +46 -0
  5. package/dist/coding-tools/coding-tool-bash.js +44 -0
  6. package/dist/coding-tools/coding-tool-extras.d.ts +35 -0
  7. package/dist/coding-tools/coding-tool-extras.js +56 -0
  8. package/dist/coding-tools/coding-tool-sandbox.d.ts +74 -0
  9. package/dist/coding-tools/coding-tool-sandbox.js +215 -0
  10. package/dist/coding-tools/coding-tool-support.d.ts +132 -0
  11. package/dist/coding-tools/coding-tool-support.js +159 -0
  12. package/dist/coding-tools/universal-coding-tools.d.ts +11 -0
  13. package/dist/coding-tools/universal-coding-tools.js +5 -0
  14. package/dist/command-interceptor/index.d.ts +55 -0
  15. package/dist/command-interceptor/index.js +60 -0
  16. package/dist/command-interceptor/shell.d.ts +36 -0
  17. package/dist/command-interceptor/shell.js +137 -0
  18. package/dist/command-safety/build.d.ts +19 -0
  19. package/dist/command-safety/build.js +47 -0
  20. package/dist/command-safety/guard.d.ts +32 -0
  21. package/dist/command-safety/guard.js +179 -0
  22. package/dist/command-safety/identifiers.d.ts +24 -0
  23. package/dist/command-safety/identifiers.js +20 -0
  24. package/dist/command-safety/index.d.ts +17 -0
  25. package/dist/command-safety/index.js +11 -0
  26. package/dist/command-safety/questions.d.ts +19 -0
  27. package/dist/command-safety/questions.js +60 -0
  28. package/dist/command-safety/row.d.ts +2 -0
  29. package/dist/command-safety/row.js +14 -0
  30. package/dist/command-safety/rule-scorer.d.ts +9 -0
  31. package/dist/command-safety/rule-scorer.js +391 -0
  32. package/dist/command-safety/shadow.d.ts +33 -0
  33. package/dist/command-safety/shadow.js +188 -0
  34. package/dist/command-safety/systemone-client.d.ts +16 -0
  35. package/dist/command-safety/systemone-client.js +92 -0
  36. package/dist/command-safety/tap.d.ts +37 -0
  37. package/dist/command-safety/tap.js +77 -0
  38. package/dist/command-safety/temp-only.d.ts +6 -0
  39. package/dist/command-safety/temp-only.js +215 -0
  40. package/dist/command-safety/tmp-write.d.ts +2 -0
  41. package/dist/command-safety/tmp-write.js +112 -0
  42. package/dist/command-safety/types.d.ts +185 -0
  43. package/dist/command-safety/types.js +21 -0
  44. package/dist/config/bash-approval.d.ts +33 -0
  45. package/dist/config/bash-approval.js +30 -0
  46. package/dist/config/catalog-overrides.d.ts +94 -0
  47. package/dist/config/catalog-overrides.js +7 -0
  48. package/dist/config/native-agent/index.d.ts +8 -0
  49. package/dist/config/native-agent/index.js +8 -0
  50. package/dist/config/schemas-sandbox.d.ts +31 -0
  51. package/dist/config/schemas-sandbox.js +45 -0
  52. package/dist/cost/core/index.d.ts +8 -0
  53. package/dist/cost/core/index.js +7 -0
  54. package/dist/cost/estimate.d.ts +34 -0
  55. package/dist/cost/estimate.js +65 -0
  56. package/dist/cost/model-spec.d.ts +28 -0
  57. package/dist/cost/model-spec.js +29 -0
  58. package/dist/cost/standard-types.d.ts +9 -0
  59. package/dist/cost/standard-types.js +1 -0
  60. package/dist/cost/usage-math.d.ts +18 -0
  61. package/dist/cost/usage-math.js +46 -0
  62. package/dist/index.d.ts +26 -0
  63. package/dist/index.js +21 -0
  64. package/dist/infra/agent-logger.d.ts +22 -0
  65. package/dist/infra/agent-logger.js +19 -0
  66. package/dist/infra/credentials-config.d.ts +20 -0
  67. package/dist/infra/credentials-config.js +15 -0
  68. package/dist/infra/errors.d.ts +9 -0
  69. package/dist/infra/errors.js +11 -0
  70. package/dist/infra/index.d.ts +7 -0
  71. package/dist/infra/index.js +7 -0
  72. package/dist/infra/nax-error.d.ts +10 -0
  73. package/dist/infra/nax-error.js +16 -0
  74. package/dist/infra/spin-breaker/hash.d.ts +2 -0
  75. package/dist/infra/spin-breaker/hash.js +5 -0
  76. package/dist/infra/spin-breaker/index.d.ts +120 -0
  77. package/dist/infra/spin-breaker/index.js +340 -0
  78. package/dist/internal/agent-output-env.d.ts +44 -0
  79. package/dist/internal/agent-output-env.js +57 -0
  80. package/dist/internal/argv-exec.d.ts +58 -0
  81. package/dist/internal/argv-exec.js +224 -0
  82. package/dist/internal/bounded-io.d.ts +24 -0
  83. package/dist/internal/bounded-io.js +51 -0
  84. package/dist/internal/command-spec/index.d.ts +29 -0
  85. package/dist/internal/command-spec/index.js +36 -0
  86. package/dist/internal/describe-value-type.d.ts +8 -0
  87. package/dist/internal/describe-value-type.js +14 -0
  88. package/dist/internal/exec-framing.d.ts +18 -0
  89. package/dist/internal/exec-framing.js +22 -0
  90. package/dist/internal/file-lock.d.ts +91 -0
  91. package/dist/internal/file-lock.js +212 -0
  92. package/dist/internal/file-size.d.ts +2 -0
  93. package/dist/internal/file-size.js +12 -0
  94. package/dist/internal/git-add.d.ts +43 -0
  95. package/dist/internal/git-add.js +59 -0
  96. package/dist/internal/git-env.d.ts +59 -0
  97. package/dist/internal/git-env.js +93 -0
  98. package/dist/internal/git-exec.d.ts +58 -0
  99. package/dist/internal/git-exec.js +115 -0
  100. package/dist/internal/path-file-lock.d.ts +23 -0
  101. package/dist/internal/path-file-lock.js +25 -0
  102. package/dist/internal/process-alive.d.ts +42 -0
  103. package/dist/internal/process-alive.js +56 -0
  104. package/dist/internal/process-kill.d.ts +27 -0
  105. package/dist/internal/process-kill.js +54 -0
  106. package/dist/internal/realpath.d.ts +29 -0
  107. package/dist/internal/realpath.js +52 -0
  108. package/dist/internal/redact.d.ts +40 -0
  109. package/dist/internal/redact.js +161 -0
  110. package/dist/internal/shell-quote.d.ts +2 -0
  111. package/dist/internal/shell-quote.js +4 -0
  112. package/dist/internal/sort.d.ts +20 -0
  113. package/dist/internal/sort.js +20 -0
  114. package/dist/internal/strip-control-chars.d.ts +19 -0
  115. package/dist/internal/strip-control-chars.js +38 -0
  116. package/dist/internal/thenable.d.ts +10 -0
  117. package/dist/internal/thenable.js +15 -0
  118. package/dist/internal.d.ts +91 -0
  119. package/dist/internal.js +91 -0
  120. package/dist/native/adapter-deps.d.ts +46 -0
  121. package/dist/native/adapter-deps.js +43 -0
  122. package/dist/native/auth-types.d.ts +70 -0
  123. package/dist/native/auth-types.js +13 -0
  124. package/dist/native/auth.d.ts +133 -0
  125. package/dist/native/auth.js +362 -0
  126. package/dist/native/client.d.ts +50 -0
  127. package/dist/native/client.js +174 -0
  128. package/dist/native/complete.d.ts +33 -0
  129. package/dist/native/complete.js +88 -0
  130. package/dist/native/credentials/chained-store.d.ts +30 -0
  131. package/dist/native/credentials/chained-store.js +68 -0
  132. package/dist/native/credentials/change-guard.d.ts +51 -0
  133. package/dist/native/credentials/change-guard.js +136 -0
  134. package/dist/native/credentials/exec-source.d.ts +50 -0
  135. package/dist/native/credentials/exec-source.js +222 -0
  136. package/dist/native/credentials/fingerprint.d.ts +26 -0
  137. package/dist/native/credentials/fingerprint.js +174 -0
  138. package/dist/native/credentials/helper-process.d.ts +79 -0
  139. package/dist/native/credentials/helper-process.js +259 -0
  140. package/dist/native/credentials/helper-protocol.d.ts +36 -0
  141. package/dist/native/credentials/helper-protocol.js +79 -0
  142. package/dist/native/credentials/index.d.ts +60 -0
  143. package/dist/native/credentials/index.js +184 -0
  144. package/dist/native/errors.d.ts +34 -0
  145. package/dist/native/errors.js +109 -0
  146. package/dist/native/index.d.ts +25 -0
  147. package/dist/native/index.js +23 -0
  148. package/dist/native/model-resolver.d.ts +57 -0
  149. package/dist/native/model-resolver.js +73 -0
  150. package/dist/native/models.d.ts +116 -0
  151. package/dist/native/models.js +183 -0
  152. package/dist/native/session/ask-human.d.ts +15 -0
  153. package/dist/native/session/ask-human.js +22 -0
  154. package/dist/native/session/compaction.d.ts +94 -0
  155. package/dist/native/session/compaction.js +170 -0
  156. package/dist/native/session/handle-invalid-tool-call.d.ts +111 -0
  157. package/dist/native/session/handle-invalid-tool-call.js +159 -0
  158. package/dist/native/session/loop-events/cache-boundary.d.ts +11 -0
  159. package/dist/native/session/loop-events/cache-boundary.js +29 -0
  160. package/dist/native/session/loop-events/external-handler.d.ts +38 -0
  161. package/dist/native/session/loop-events/external-handler.js +193 -0
  162. package/dist/native/session/loop-events/index.d.ts +16 -0
  163. package/dist/native/session/loop-events/index.js +15 -0
  164. package/dist/native/session/loop-events/payload-guard.d.ts +9 -0
  165. package/dist/native/session/loop-events/payload-guard.js +31 -0
  166. package/dist/native/session/loop-events/registry.d.ts +84 -0
  167. package/dist/native/session/loop-events/registry.js +305 -0
  168. package/dist/native/session/loop-events/types.d.ts +251 -0
  169. package/dist/native/session/loop-events/types.js +14 -0
  170. package/dist/native/session/loop-handlers.d.ts +73 -0
  171. package/dist/native/session/loop-handlers.js +137 -0
  172. package/dist/native/session/nudge.d.ts +29 -0
  173. package/dist/native/session/nudge.js +35 -0
  174. package/dist/native/session/rate-provenance.d.ts +17 -0
  175. package/dist/native/session/rate-provenance.js +39 -0
  176. package/dist/native/session/session.d.ts +139 -0
  177. package/dist/native/session/session.js +208 -0
  178. package/dist/native/session/tool-input-exemplar.d.ts +2 -0
  179. package/dist/native/session/tool-input-exemplar.js +66 -0
  180. package/dist/native/session/tool-input-validation.d.ts +15 -0
  181. package/dist/native/session/tool-input-validation.js +146 -0
  182. package/dist/native/session/tool-mapping.d.ts +18 -0
  183. package/dist/native/session/tool-mapping.js +27 -0
  184. package/dist/native/session/tool-result.d.ts +37 -0
  185. package/dist/native/session/tool-result.js +25 -0
  186. package/dist/native/session/transcript-store.d.ts +77 -0
  187. package/dist/native/session/transcript-store.js +200 -0
  188. package/dist/native/session/truncation-handler.d.ts +47 -0
  189. package/dist/native/session/truncation-handler.js +83 -0
  190. package/dist/native/session/turn-accumulator.d.ts +45 -0
  191. package/dist/native/session/turn-accumulator.js +67 -0
  192. package/dist/native/session/turn-ask-human.d.ts +23 -0
  193. package/dist/native/session/turn-ask-human.js +41 -0
  194. package/dist/native/session/turn-compaction-step.d.ts +74 -0
  195. package/dist/native/session/turn-compaction-step.js +128 -0
  196. package/dist/native/session/turn-complete-step.d.ts +77 -0
  197. package/dist/native/session/turn-complete-step.js +159 -0
  198. package/dist/native/session/turn-end-event.d.ts +3 -0
  199. package/dist/native/session/turn-end-event.js +17 -0
  200. package/dist/native/session/turn-events.d.ts +63 -0
  201. package/dist/native/session/turn-events.js +48 -0
  202. package/dist/native/session/turn-loop-round-trip.d.ts +116 -0
  203. package/dist/native/session/turn-loop-round-trip.js +318 -0
  204. package/dist/native/session/turn-loop.d.ts +23 -0
  205. package/dist/native/session/turn-loop.js +250 -0
  206. package/dist/native/session/turn-result.d.ts +39 -0
  207. package/dist/native/session/turn-result.js +64 -0
  208. package/dist/native/session/turn-retry.d.ts +120 -0
  209. package/dist/native/session/turn-retry.js +182 -0
  210. package/dist/native/session/turn-tool-batch.d.ts +61 -0
  211. package/dist/native/session/turn-tool-batch.js +259 -0
  212. package/dist/native/session/turn-types.d.ts +164 -0
  213. package/dist/native/session/turn-types.js +36 -0
  214. package/dist/native/session-adapter.d.ts +56 -0
  215. package/dist/native/session-adapter.js +369 -0
  216. package/dist/native/session-affinity.d.ts +36 -0
  217. package/dist/native/session-affinity.js +43 -0
  218. package/dist/native/tier-providers.d.ts +18 -0
  219. package/dist/native/tier-providers.js +20 -0
  220. package/dist/permissions/approval-audit.d.ts +10 -0
  221. package/dist/permissions/approval-audit.js +22 -0
  222. package/dist/permissions/approvals-link.d.ts +20 -0
  223. package/dist/permissions/approvals-link.js +62 -0
  224. package/dist/permissions/approvals-store.d.ts +168 -0
  225. package/dist/permissions/approvals-store.js +259 -0
  226. package/dist/permissions/approvals-taint.d.ts +33 -0
  227. package/dist/permissions/approvals-taint.js +138 -0
  228. package/dist/permissions/ask-chain.d.ts +69 -0
  229. package/dist/permissions/ask-chain.js +32 -0
  230. package/dist/permissions/ask.d.ts +23 -0
  231. package/dist/permissions/ask.js +25 -0
  232. package/dist/permissions/bash-lex.d.ts +59 -0
  233. package/dist/permissions/bash-lex.js +279 -0
  234. package/dist/permissions/grammar.d.ts +12 -0
  235. package/dist/permissions/grammar.js +24 -0
  236. package/dist/permissions/index.d.ts +11 -0
  237. package/dist/permissions/index.js +9 -0
  238. package/dist/permissions/secret-spans.d.ts +19 -0
  239. package/dist/permissions/secret-spans.js +88 -0
  240. package/dist/permissions/types.d.ts +85 -0
  241. package/dist/permissions/types.js +1 -0
  242. package/dist/runtime/index.d.ts +3 -0
  243. package/dist/runtime/index.js +2 -0
  244. package/dist/runtime/node-glob.d.ts +3 -0
  245. package/dist/runtime/node-glob.js +83 -0
  246. package/dist/runtime/node-runtime.d.ts +3 -0
  247. package/dist/runtime/node-runtime.js +126 -0
  248. package/dist/runtime/slot.d.ts +11 -0
  249. package/dist/runtime/slot.js +17 -0
  250. package/dist/runtime/types.d.ts +51 -0
  251. package/dist/runtime/types.js +1 -0
  252. package/dist/runtime/which.d.ts +8 -0
  253. package/dist/runtime/which.js +32 -0
  254. package/dist/sandbox/argv-quote.d.ts +11 -0
  255. package/dist/sandbox/argv-quote.js +13 -0
  256. package/dist/sandbox/defaults.d.ts +27 -0
  257. package/dist/sandbox/defaults.js +47 -0
  258. package/dist/sandbox/git-guards.d.ts +64 -0
  259. package/dist/sandbox/git-guards.js +161 -0
  260. package/dist/sandbox/index.d.ts +11 -0
  261. package/dist/sandbox/index.js +10 -0
  262. package/dist/sandbox/launcher.d.ts +37 -0
  263. package/dist/sandbox/launcher.js +160 -0
  264. package/dist/sandbox/messages.d.ts +17 -0
  265. package/dist/sandbox/messages.js +46 -0
  266. package/dist/sandbox/policy-builder.d.ts +44 -0
  267. package/dist/sandbox/policy-builder.js +149 -0
  268. package/dist/sandbox/policy-inputs.d.ts +53 -0
  269. package/dist/sandbox/policy-inputs.js +76 -0
  270. package/dist/sandbox/probe.d.ts +8 -0
  271. package/dist/sandbox/probe.js +66 -0
  272. package/dist/sandbox/registry.d.ts +17 -0
  273. package/dist/sandbox/registry.js +57 -0
  274. package/dist/sandbox/session-tmp.d.ts +20 -0
  275. package/dist/sandbox/session-tmp.js +95 -0
  276. package/dist/sandbox/srt-backend.d.ts +21 -0
  277. package/dist/sandbox/srt-backend.js +99 -0
  278. package/dist/sandbox/types.d.ts +102 -0
  279. package/dist/sandbox/types.js +1 -0
  280. package/dist/session/adapter-failure.d.ts +60 -0
  281. package/dist/session/adapter-failure.js +11 -0
  282. package/dist/session/agent-stream-events.d.ts +103 -0
  283. package/dist/session/agent-stream-events.js +6 -0
  284. package/dist/session/interaction-handler.d.ts +53 -0
  285. package/dist/session/interaction-handler.js +2 -0
  286. package/dist/session/no-op-interaction-handler.d.ts +9 -0
  287. package/dist/session/no-op-interaction-handler.js +11 -0
  288. package/dist/session/protocol-types.d.ts +12 -0
  289. package/dist/session/protocol-types.js +1 -0
  290. package/dist/session/session-types.d.ts +456 -0
  291. package/dist/session/session-types.js +64 -0
  292. package/dist/session/tool-descriptor.d.ts +23 -0
  293. package/dist/session/tool-descriptor.js +5 -0
  294. package/dist/session/turn-deadline.d.ts +17 -0
  295. package/dist/session/turn-deadline.js +23 -0
  296. package/dist/tools/ask-request.d.ts +35 -0
  297. package/dist/tools/ask-request.js +57 -0
  298. package/dist/tools/bash-cwd.d.ts +82 -0
  299. package/dist/tools/bash-cwd.js +55 -0
  300. package/dist/tools/bash.d.ts +69 -0
  301. package/dist/tools/bash.js +283 -0
  302. package/dist/tools/delete.d.ts +57 -0
  303. package/dist/tools/delete.js +154 -0
  304. package/dist/tools/denial-redirect.d.ts +42 -0
  305. package/dist/tools/denial-redirect.js +266 -0
  306. package/dist/tools/deny-paths.d.ts +35 -0
  307. package/dist/tools/deny-paths.js +76 -0
  308. package/dist/tools/edit-region.d.ts +50 -0
  309. package/dist/tools/edit-region.js +90 -0
  310. package/dist/tools/edit.d.ts +25 -0
  311. package/dist/tools/edit.js +114 -0
  312. package/dist/tools/exec-allowlist-text.d.ts +18 -0
  313. package/dist/tools/exec-allowlist-text.js +24 -0
  314. package/dist/tools/exec-guard.d.ts +69 -0
  315. package/dist/tools/exec-guard.js +151 -0
  316. package/dist/tools/git-commit.d.ts +59 -0
  317. package/dist/tools/git-commit.js +220 -0
  318. package/dist/tools/git-flags/index.d.ts +10 -0
  319. package/dist/tools/git-flags/index.js +10 -0
  320. package/dist/tools/git.d.ts +88 -0
  321. package/dist/tools/git.js +397 -0
  322. package/dist/tools/glob.d.ts +29 -0
  323. package/dist/tools/glob.js +153 -0
  324. package/dist/tools/grep.d.ts +21 -0
  325. package/dist/tools/grep.js +162 -0
  326. package/dist/tools/index.d.ts +36 -0
  327. package/dist/tools/index.js +28 -0
  328. package/dist/tools/narrow-grants.d.ts +19 -0
  329. package/dist/tools/narrow-grants.js +54 -0
  330. package/dist/tools/nax-owned-writes.d.ts +135 -0
  331. package/dist/tools/nax-owned-writes.js +259 -0
  332. package/dist/tools/package-managers-table.d.ts +146 -0
  333. package/dist/tools/package-managers-table.js +325 -0
  334. package/dist/tools/package-managers-types.d.ts +36 -0
  335. package/dist/tools/package-managers-types.js +6 -0
  336. package/dist/tools/package-managers.d.ts +85 -0
  337. package/dist/tools/package-managers.js +391 -0
  338. package/dist/tools/path-list.d.ts +38 -0
  339. package/dist/tools/path-list.js +62 -0
  340. package/dist/tools/policy-bash-raw.d.ts +19 -0
  341. package/dist/tools/policy-bash-raw.js +210 -0
  342. package/dist/tools/policy-bash.d.ts +64 -0
  343. package/dist/tools/policy-bash.js +232 -0
  344. package/dist/tools/policy-command-branch.d.ts +32 -0
  345. package/dist/tools/policy-command-branch.js +63 -0
  346. package/dist/tools/policy-input.d.ts +2 -0
  347. package/dist/tools/policy-input.js +10 -0
  348. package/dist/tools/policy-match.d.ts +75 -0
  349. package/dist/tools/policy-match.js +117 -0
  350. package/dist/tools/policy-paths-branch.d.ts +97 -0
  351. package/dist/tools/policy-paths-branch.js +314 -0
  352. package/dist/tools/policy.d.ts +65 -0
  353. package/dist/tools/policy.js +232 -0
  354. package/dist/tools/protected-paths.d.ts +26 -0
  355. package/dist/tools/protected-paths.js +8 -0
  356. package/dist/tools/provider-adapt.d.ts +4 -0
  357. package/dist/tools/provider-adapt.js +32 -0
  358. package/dist/tools/provider-advertise.d.ts +39 -0
  359. package/dist/tools/provider-advertise.js +50 -0
  360. package/dist/tools/provider-grants.d.ts +36 -0
  361. package/dist/tools/provider-grants.js +72 -0
  362. package/dist/tools/provider-sanitize.d.ts +19 -0
  363. package/dist/tools/provider-sanitize.js +53 -0
  364. package/dist/tools/provider-types.d.ts +41 -0
  365. package/dist/tools/provider-types.js +26 -0
  366. package/dist/tools/read-continuation.d.ts +147 -0
  367. package/dist/tools/read-continuation.js +176 -0
  368. package/dist/tools/read-file.d.ts +32 -0
  369. package/dist/tools/read-file.js +85 -0
  370. package/dist/tools/read.d.ts +25 -0
  371. package/dist/tools/read.js +182 -0
  372. package/dist/tools/registry.d.ts +125 -0
  373. package/dist/tools/registry.js +67 -0
  374. package/dist/tools/request-capability.d.ts +13 -0
  375. package/dist/tools/request-capability.js +23 -0
  376. package/dist/tools/run-command-exec.d.ts +14 -0
  377. package/dist/tools/run-command-exec.js +161 -0
  378. package/dist/tools/run-command.d.ts +107 -0
  379. package/dist/tools/run-command.js +398 -0
  380. package/dist/tools/runtime-calltool.d.ts +146 -0
  381. package/dist/tools/runtime-calltool.js +223 -0
  382. package/dist/tools/runtime.d.ts +149 -0
  383. package/dist/tools/runtime.js +345 -0
  384. package/dist/tools/scratchpad.d.ts +20 -0
  385. package/dist/tools/scratchpad.js +197 -0
  386. package/dist/tools/spill.d.ts +81 -0
  387. package/dist/tools/spill.js +251 -0
  388. package/dist/tools/tool-audit.d.ts +171 -0
  389. package/dist/tools/tool-audit.js +183 -0
  390. package/dist/tools/truncate.d.ts +98 -0
  391. package/dist/tools/truncate.js +297 -0
  392. package/dist/tools/types.d.ts +145 -0
  393. package/dist/tools/types.js +24 -0
  394. package/dist/tools/verb-denial-argv-hint.d.ts +35 -0
  395. package/dist/tools/verb-denial-argv-hint.js +40 -0
  396. package/dist/tools/write.d.ts +9 -0
  397. package/dist/tools/write.js +45 -0
  398. package/package.json +54 -0
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Detect an invalid tool-call input at the transport boundary (nax#2047).
3
+ *
4
+ * The model returned RunCommand calls with `values:""` 69 times in a row —
5
+ * the empty string produced no rejected keys, the loop never named "values"
6
+ * as the wrong type, so the spin breaker never saw the repetition as
7
+ * repetition. This module is the gate the loop was missing: validate the
8
+ * call against the tool's JSON Schema before any handler runs. On violation,
9
+ * name the property and offer a schema-conforming exemplar so the next round
10
+ * trip sees what the tool would have accepted.
11
+ *
12
+ * Where the exemplar goes (nax#2200): #2047 wrote it over the model's call in
13
+ * the assistant message, so the malformed value would not persist as a
14
+ * few-shot example. That kept the purpose but attributed a call the model never
15
+ * made — `refs: ["<FILL IN: refs>"]` — to the assistant. The recorded call is
16
+ * now the model's own input with ONLY the rejected property removed: the
17
+ * malformed value still never persists, nothing fabricated is attributed to
18
+ * the model, and the exemplar lives in the tool-result's error text, where it
19
+ * is plainly guidance from the harness.
20
+ *
21
+ * Since nax#2151 this decision is produced by a built-in `before_tool`
22
+ * handler (`loop-handlers.ts`) instead of an inline branch of the loop: the
23
+ * repair is a tool-call decision — answer without invoking — which is exactly
24
+ * what the seam's `block` outcome is for. What stays here is the decision and
25
+ * its per-turn budget; the transcript rewrite is the loop's, because that is
26
+ * where the message array lives.
27
+ *
28
+ * Returns `undefined` when the call is valid, or when the tool is not in the
29
+ * local catalogue (no schema → no validation, success path). The success path
30
+ * costs one function call and no allocation.
31
+ *
32
+ * The per-turn budget (Task 4) lives in `createInvalidCallBudget()` — three
33
+ * identical invalid calls (same tool, same `stableStringify`'d input) end the
34
+ * turn with `{kind: "stopped"}` and NO tool-result, matching the spin
35
+ * breaker's stop branch ("a result nobody reads only grows the transcript").
36
+ * The counter is per-key, cumulative across interleaved valid calls, and lives
37
+ * in the factory's closure.
38
+ */
39
+ import type { ToolCall, ToolDefinition } from "@nathapp/nax-ai";
40
+ import type { InvalidToolCallDetail } from "#src/session/session-types";
41
+ import type { TranscriptMessage as NativeTranscriptMessage } from "./compaction.ts";
42
+ /** Three identical invalid calls in a turn ends it (Task 4). */
43
+ export declare const INVALID_CALL_BUDGET_THRESHOLD = 3;
44
+ export type InvalidCallOutcome =
45
+ /**
46
+ * Answer the call with `content` instead of running it. `input` is what the
47
+ * transcript records for the call: the model's own input minus the rejected
48
+ * property (nax#2200), never a fabricated exemplar.
49
+ */
50
+ {
51
+ readonly kind: "repair";
52
+ readonly input: Record<string, unknown>;
53
+ readonly content: string;
54
+ }
55
+ /** The per-key budget is spent: end the turn with no answer at all. */
56
+ | {
57
+ readonly kind: "stopped";
58
+ readonly detail: InvalidToolCallDetail;
59
+ };
60
+ export declare function decideInvalidToolCall(call: ToolCall, tools: readonly ToolDefinition[], counters: Map<string, number>): InvalidCallOutcome | undefined;
61
+ /**
62
+ * The call's input with its `null`-valued optional properties removed, or
63
+ * `undefined` when the tool is not in the catalogue or nothing was removed
64
+ * (nax#2200). The validator already reads such a `null` as absent; this is
65
+ * what makes the tool's handler see the same thing.
66
+ */
67
+ export declare function normalizeNullOptionals(call: ToolCall, tools: readonly ToolDefinition[]): Record<string, unknown> | undefined;
68
+ /**
69
+ * Replace one `toolCalls` entry's input on the LAST assistant message.
70
+ *
71
+ * Composes onto the last assistant message rather than the last array entry:
72
+ * after iteration N of a multi-call assistant message, iteration N+1 sees a
73
+ * tool-result for iteration N at the tail, and a naive implementation would
74
+ * rewrite only the last of two invalid calls.
75
+ *
76
+ * Never mutates: the returned array and the rewritten message are fresh values,
77
+ * and thinking blocks survive byte-for-byte (ADR-028 §8 — Anthropic needs the
78
+ * exact thinking block back to continue a thinking conversation).
79
+ */
80
+ export declare function rewriteToolCallInput(messages: readonly NativeTranscriptMessage[], callId: string, input: Record<string, unknown>): NativeTranscriptMessage[];
81
+ /**
82
+ * Per-turn budget for repeated invalid tool calls (nax#2047, Task 4).
83
+ *
84
+ * The spin breaker's 50-call budget is for REPEATED *valid-shape* calls; a
85
+ * malformed shape is a stronger signal — the model will keep sending the
86
+ * same wrong shape for as long as it ignores the error result. This budget
87
+ * hard-stops a turn after three identical invalid calls (same tool, same
88
+ * `stableStringify`'d input) so the transcript never grows past the second
89
+ * error result.
90
+ *
91
+ * Per-key, cumulative across interleaved valid calls: a valid call does
92
+ * not reset the counter, and the same key returning later in the same turn
93
+ * keeps accumulating. Different keys do not stop each other — three distinct
94
+ * invalid calls (the model is exploring) are not a budget violation.
95
+ *
96
+ * On exceed, `observe()` returns `{kind: "stopped"}` and `exceeded` flips to
97
+ * `true`. The caller checks `exceeded` after the dispatch to break the batch
98
+ * with no tool-result and to set `TurnResult.invalidCallBudgetExceeded`,
99
+ * mirroring `spinStopped`'s reporting pattern.
100
+ */
101
+ export interface InvalidCallBudget {
102
+ /** Whether the budget was tripped on any call so far. Read after the dispatch. */
103
+ readonly exceeded: boolean;
104
+ /**
105
+ * The call that tripped the budget — set together with `exceeded`, so the
106
+ * halt can be logged and classified by what was rejected (nax#2200).
107
+ */
108
+ readonly halt: InvalidToolCallDetail | undefined;
109
+ observe(call: ToolCall, tools: readonly ToolDefinition[]): InvalidCallOutcome | undefined;
110
+ }
111
+ export declare function createInvalidCallBudget(): InvalidCallBudget;
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Detect an invalid tool-call input at the transport boundary (nax#2047).
3
+ *
4
+ * The model returned RunCommand calls with `values:""` 69 times in a row —
5
+ * the empty string produced no rejected keys, the loop never named "values"
6
+ * as the wrong type, so the spin breaker never saw the repetition as
7
+ * repetition. This module is the gate the loop was missing: validate the
8
+ * call against the tool's JSON Schema before any handler runs. On violation,
9
+ * name the property and offer a schema-conforming exemplar so the next round
10
+ * trip sees what the tool would have accepted.
11
+ *
12
+ * Where the exemplar goes (nax#2200): #2047 wrote it over the model's call in
13
+ * the assistant message, so the malformed value would not persist as a
14
+ * few-shot example. That kept the purpose but attributed a call the model never
15
+ * made — `refs: ["<FILL IN: refs>"]` — to the assistant. The recorded call is
16
+ * now the model's own input with ONLY the rejected property removed: the
17
+ * malformed value still never persists, nothing fabricated is attributed to
18
+ * the model, and the exemplar lives in the tool-result's error text, where it
19
+ * is plainly guidance from the harness.
20
+ *
21
+ * Since nax#2151 this decision is produced by a built-in `before_tool`
22
+ * handler (`loop-handlers.ts`) instead of an inline branch of the loop: the
23
+ * repair is a tool-call decision — answer without invoking — which is exactly
24
+ * what the seam's `block` outcome is for. What stays here is the decision and
25
+ * its per-turn budget; the transcript rewrite is the loop's, because that is
26
+ * where the message array lives.
27
+ *
28
+ * Returns `undefined` when the call is valid, or when the tool is not in the
29
+ * local catalogue (no schema → no validation, success path). The success path
30
+ * costs one function call and no allocation.
31
+ *
32
+ * The per-turn budget (Task 4) lives in `createInvalidCallBudget()` — three
33
+ * identical invalid calls (same tool, same `stableStringify`'d input) end the
34
+ * turn with `{kind: "stopped"}` and NO tool-result, matching the spin
35
+ * breaker's stop branch ("a result nobody reads only grows the transcript").
36
+ * The counter is per-key, cumulative across interleaved valid calls, and lives
37
+ * in the factory's closure.
38
+ */
39
+ import { byCodePoint } from "#src/internal/sort";
40
+ import { exemplarFor } from "./tool-input-exemplar.js";
41
+ import { stripNullOptionals, validateToolInput } from "./tool-input-validation.js";
42
+ /** Three identical invalid calls in a turn ends it (Task 4). */
43
+ export const INVALID_CALL_BUDGET_THRESHOLD = 3;
44
+ export function decideInvalidToolCall(call, tools, counters) {
45
+ const tool = tools.find((t) => t.name === call.name);
46
+ if (tool === undefined)
47
+ return undefined;
48
+ const violation = validateToolInput(tool.inputSchema, call.input);
49
+ if (violation === undefined)
50
+ return undefined;
51
+ // Per-key counter: tool name + stableStringify(input). Mutated in place
52
+ // so the next iteration sees the updated count without us having to
53
+ // thread the new map back through.
54
+ const counterKey = `${call.name}\u0000${stableStringify(call.input)}`;
55
+ const nextCount = (counters.get(counterKey) ?? 0) + 1;
56
+ counters.set(counterKey, nextCount);
57
+ if (nextCount >= INVALID_CALL_BUDGET_THRESHOLD) {
58
+ const { property, expected, actual } = violation;
59
+ return { kind: "stopped", detail: { tool: call.name, property, expected, actual } };
60
+ }
61
+ const original = call.input;
62
+ const exemplar = exemplarFor(tool.inputSchema, original, violation);
63
+ const recorded = withoutProperty(original, violation.property);
64
+ return { kind: "repair", input: recorded, content: invalidCallMessage(violation, exemplar, recorded !== original) };
65
+ }
66
+ /**
67
+ * The call's input with its `null`-valued optional properties removed, or
68
+ * `undefined` when the tool is not in the catalogue or nothing was removed
69
+ * (nax#2200). The validator already reads such a `null` as absent; this is
70
+ * what makes the tool's handler see the same thing.
71
+ */
72
+ export function normalizeNullOptionals(call, tools) {
73
+ const tool = tools.find((t) => t.name === call.name);
74
+ if (tool === undefined)
75
+ return undefined;
76
+ return stripNullOptionals(tool.inputSchema, call.input);
77
+ }
78
+ /** `input` without `property`; the same object when the property is not there. */
79
+ function withoutProperty(input, property) {
80
+ if (property === "" || !(property in input))
81
+ return input;
82
+ const { [property]: _rejected, ...rest } = input;
83
+ return rest;
84
+ }
85
+ /**
86
+ * Replace one `toolCalls` entry's input on the LAST assistant message.
87
+ *
88
+ * Composes onto the last assistant message rather than the last array entry:
89
+ * after iteration N of a multi-call assistant message, iteration N+1 sees a
90
+ * tool-result for iteration N at the tail, and a naive implementation would
91
+ * rewrite only the last of two invalid calls.
92
+ *
93
+ * Never mutates: the returned array and the rewritten message are fresh values,
94
+ * and thinking blocks survive byte-for-byte (ADR-028 §8 — Anthropic needs the
95
+ * exact thinking block back to continue a thinking conversation).
96
+ */
97
+ export function rewriteToolCallInput(messages, callId, input) {
98
+ let lastAssistantIndex = -1;
99
+ for (let i = messages.length - 1; i >= 0; i--) {
100
+ if (messages[i]?.role === "assistant") {
101
+ lastAssistantIndex = i;
102
+ break;
103
+ }
104
+ }
105
+ const lastAssistant = messages[lastAssistantIndex];
106
+ if (lastAssistant === undefined || lastAssistant.role !== "assistant")
107
+ return [...messages];
108
+ const toolCalls = (lastAssistant.toolCalls ?? []).map((tc) => (tc.id === callId ? { ...tc, input } : tc));
109
+ return [
110
+ ...messages.slice(0, lastAssistantIndex),
111
+ {
112
+ role: "assistant",
113
+ content: lastAssistant.content,
114
+ toolCalls,
115
+ ...(lastAssistant.thinking !== undefined ? { thinking: lastAssistant.thinking } : {}),
116
+ },
117
+ ...messages.slice(lastAssistantIndex + 1),
118
+ ];
119
+ }
120
+ function invalidCallMessage(violation, exemplar, removedFromRecord) {
121
+ return (`invalid tool call: property "${violation.property}" expected ${violation.expected}, ` +
122
+ `got ${violation.actual}. The call was not executed. ` +
123
+ (removedFromRecord ? `The rejected "${violation.property}" value was removed from the recorded call. ` : "") +
124
+ `An input of the expected shape looks like ${JSON.stringify(exemplar)} — ` +
125
+ "replace any <FILL IN> placeholder with a real value, and omit an optional property you have no value for.");
126
+ }
127
+ export function createInvalidCallBudget() {
128
+ const counters = new Map();
129
+ let halt;
130
+ return {
131
+ get exceeded() {
132
+ return halt !== undefined;
133
+ },
134
+ get halt() {
135
+ return halt;
136
+ },
137
+ observe(call, tools) {
138
+ const outcome = decideInvalidToolCall(call, tools, counters);
139
+ if (outcome?.kind === "stopped" && halt === undefined)
140
+ halt = outcome.detail;
141
+ return outcome;
142
+ },
143
+ };
144
+ }
145
+ /**
146
+ * Deterministic serializer independent of property insertion order, so
147
+ * `{a:1,b:2}` and `{b:2,a:1}` collide on the same counter key. Locally
148
+ * scoped on purpose — `src/runtime/spin-breaker` has its own private copy
149
+ * of the same algorithm. The two implementations are deliberately small and
150
+ * identical so a future extraction is a one-line move.
151
+ */
152
+ function stableStringify(value) {
153
+ if (value === null || typeof value !== "object")
154
+ return JSON.stringify(value) ?? "null";
155
+ if (Array.isArray(value))
156
+ return `[${value.map(stableStringify).join(",")}]`;
157
+ const entries = Object.entries(value).sort((a, b) => byCodePoint(a[0], b[0]));
158
+ return `{${entries.map(([key, val]) => `${JSON.stringify(key)}:${stableStringify(val)}`).join(",")}}`;
159
+ }
@@ -0,0 +1,11 @@
1
+ export declare function checkPrefixStable(before: readonly unknown[], after: readonly unknown[], anchorIndex: number | undefined): boolean;
2
+ export declare function applyHistoryPatch<T>(args: {
3
+ readonly before: readonly T[];
4
+ readonly patched: readonly T[] | undefined;
5
+ readonly anchorIndex: number | undefined;
6
+ readonly boundary: boolean;
7
+ readonly event: string;
8
+ }): {
9
+ readonly messages: readonly T[];
10
+ readonly honoured: boolean;
11
+ };
@@ -0,0 +1,29 @@
1
+ import { getSafeLogger } from "#src/infra/index";
2
+ export function checkPrefixStable(before, after, anchorIndex) {
3
+ // No anchor means no cached prefix to protect (spec 3.5).
4
+ if (anchorIndex === undefined)
5
+ return true;
6
+ const end = Math.min(anchorIndex, before.length - 1);
7
+ if (after.length <= end)
8
+ return false;
9
+ for (let i = 0; i <= end; i += 1) {
10
+ // Reference identity: an equal-valued rebuild still breaks the wire cache.
11
+ if (before[i] !== after[i])
12
+ return false;
13
+ }
14
+ return true;
15
+ }
16
+ export function applyHistoryPatch(args) {
17
+ const { before, patched, anchorIndex, boundary, event } = args;
18
+ if (patched === undefined)
19
+ return { messages: before, honoured: false };
20
+ if (boundary || checkPrefixStable(before, patched, anchorIndex)) {
21
+ return { messages: patched, honoured: true };
22
+ }
23
+ getSafeLogger()?.warn("native-loop-events", "history patch rejected: prefix rewritten off-boundary", {
24
+ event,
25
+ ...(anchorIndex !== undefined ? { anchorIndex } : {}),
26
+ });
27
+ // What stops is the PATCH, never the turn (spec 3.7).
28
+ return { messages: before, honoured: false };
29
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * US-002 — `wrapExternalHandler()`: bounded lifetime, context and attributed
3
+ * failure for plugin-contributed loop handlers.
4
+ *
5
+ * A `src/hooks/` handler is a child process with a 5s timeout; a plugin loop
6
+ * handler is an IN-PROCESS function called from inside `runNativeTurn`, so
7
+ * nothing bounds it and nothing catches it. A handler that never settles would
8
+ * hang the turn, and a handler that throws must not be able to silently pass a
9
+ * tool guard. This module answers both, for one staged entry at a time, and it
10
+ * never rethrows: a plugin's defect must not fail the turn.
11
+ *
12
+ * The payload is handed over untouched — including for `before_tool`, whose
13
+ * answer is a DECISION (`allow`/`nudge`/`block`/`terminate`) rather than a
14
+ * field patch. That answer is therefore validated rather than field-picked: a
15
+ * plugin module is plain JavaScript, is loaded from disk, and can return
16
+ * anything at all.
17
+ */
18
+ import type { HandlerOf, LoopEvent, LoopHandlerContext, LoopHandlerEntry } from "./types.ts";
19
+ /**
20
+ * How long a plugin handler may run before the wrapper answers for it. A
21
+ * constant by ruling, not a `NaxConfig` key: it bounds the turn's liveness, it
22
+ * is not a policy a project tunes per package.
23
+ */
24
+ export declare const LOOP_HANDLER_TIMEOUT_MS = 10000;
25
+ /**
26
+ * Test seam for the deadline only. Read at DISPATCH time, never captured when
27
+ * the handler is wrapped — a wrapped handler is installed once and dispatched
28
+ * many times.
29
+ */
30
+ export declare const _externalHandlerDeps: {
31
+ timeoutMs: number;
32
+ };
33
+ /**
34
+ * Wrap one staged plugin registration into a built-in-shaped `HandlerOf<E>`:
35
+ * same payload, same patch, but with a bounded lifetime, the turn's context,
36
+ * and a failure that is attributed and never rethrown.
37
+ */
38
+ export declare function wrapExternalHandler<E extends LoopEvent>(entry: LoopHandlerEntry, getCtx: () => LoopHandlerContext, signal: AbortSignal): HandlerOf<E>;
@@ -0,0 +1,193 @@
1
+ /**
2
+ * US-002 — `wrapExternalHandler()`: bounded lifetime, context and attributed
3
+ * failure for plugin-contributed loop handlers.
4
+ *
5
+ * A `src/hooks/` handler is a child process with a 5s timeout; a plugin loop
6
+ * handler is an IN-PROCESS function called from inside `runNativeTurn`, so
7
+ * nothing bounds it and nothing catches it. A handler that never settles would
8
+ * hang the turn, and a handler that throws must not be able to silently pass a
9
+ * tool guard. This module answers both, for one staged entry at a time, and it
10
+ * never rethrows: a plugin's defect must not fail the turn.
11
+ *
12
+ * The payload is handed over untouched — including for `before_tool`, whose
13
+ * answer is a DECISION (`allow`/`nudge`/`block`/`terminate`) rather than a
14
+ * field patch. That answer is therefore validated rather than field-picked: a
15
+ * plugin module is plain JavaScript, is loaded from disk, and can return
16
+ * anything at all.
17
+ */
18
+ import { errorMessage } from "#src/infra/errors";
19
+ import { getSafeLogger } from "#src/infra/index";
20
+ import { isCompleteBeforeToolOutcome } from "./registry.js";
21
+ /**
22
+ * How long a plugin handler may run before the wrapper answers for it. A
23
+ * constant by ruling, not a `NaxConfig` key: it bounds the turn's liveness, it
24
+ * is not a policy a project tunes per package.
25
+ */
26
+ export const LOOP_HANDLER_TIMEOUT_MS = 10_000;
27
+ /**
28
+ * Test seam for the deadline only. Read at DISPATCH time, never captured when
29
+ * the handler is wrapped — a wrapped handler is installed once and dispatched
30
+ * many times.
31
+ */
32
+ export const _externalHandlerDeps = {
33
+ timeoutMs: LOOP_HANDLER_TIMEOUT_MS,
34
+ };
35
+ /**
36
+ * The patch fields whose presence is worth tracing: the ones a later
37
+ * `applyHistoryPatch` can reject, so a rejection is attributable to a plugin.
38
+ */
39
+ const TRACED_PATCH_FIELDS = ["seed", "history", "messages", "summary"];
40
+ /**
41
+ * Race the handler's settlement against the deadline and the turn's abort. The
42
+ * first to settle wins, every later settlement is discarded, and the timer is
43
+ * cleared the moment the race settles so an instant handler never holds the
44
+ * event loop open for the full deadline.
45
+ *
46
+ * The abort is honoured for the lifetime of THIS dispatch: the listener is
47
+ * wired synchronously, before the handler is called. A signal that was already
48
+ * aborted when the dispatch began is deliberately NOT honoured — one registry
49
+ * is installed once and repointed per turn (`./loop-handlers`), so a signal
50
+ * carried over from an earlier turn would otherwise fail-close every later
51
+ * `before_tool` dispatch to `block` without ever calling the handler. The
52
+ * deadline still bounds such a dispatch, so nothing hangs.
53
+ *
54
+ * `setTimeout` and not `Bun.sleep()`: the handle has to be cancelled mid-flight
55
+ * (`clearTimeout`), which is the sanctioned exception to the Bun-native delay
56
+ * rule.
57
+ */
58
+ function raceSettlement(entry, payload, deps) {
59
+ const timeoutMs = _externalHandlerDeps.timeoutMs;
60
+ return new Promise((resolve) => {
61
+ let settled = false;
62
+ let timer;
63
+ const finish = (result) => {
64
+ if (settled)
65
+ return;
66
+ settled = true;
67
+ if (timer !== undefined)
68
+ clearTimeout(timer);
69
+ deps.signal.removeEventListener("abort", onAbort);
70
+ resolve(result);
71
+ };
72
+ const onAbort = () => finish({ ok: false, reason: "aborted before it settled" });
73
+ timer = setTimeout(() => finish({ ok: false, reason: `did not settle within ${timeoutMs}ms` }), timeoutMs);
74
+ deps.signal.addEventListener("abort", onAbort, { once: true });
75
+ // The context is read here, at dispatch, so a handler always sees the facts
76
+ // of the turn it is running in. Read in its OWN try: a `getCtx` throw is
77
+ // nax's own defect, and it must not be reported as the plugin's handler
78
+ // failing (US-002 review).
79
+ let ctx;
80
+ try {
81
+ ctx = deps.getCtx();
82
+ }
83
+ catch (err) {
84
+ finish({ ok: false, reason: `reading the handler context failed: ${errorMessage(err)}` });
85
+ return;
86
+ }
87
+ let returned;
88
+ try {
89
+ returned = entry.handler(payload, ctx);
90
+ }
91
+ catch (err) {
92
+ finish({ ok: false, reason: errorMessage(err) });
93
+ return;
94
+ }
95
+ Promise.resolve(returned).then((value) => finish({ ok: true, value }), (err) => finish({ ok: false, reason: errorMessage(err) }));
96
+ });
97
+ }
98
+ /**
99
+ * The attributed failure. On `before_tool` the call is answered with an error
100
+ * block — a failed guard must never read as a silent allow — and every other
101
+ * event answers `{}` (no patch). One warn per failure either way, naming the
102
+ * plugin the failure belongs to.
103
+ *
104
+ * The tool name is read defensively: this runs on the failure path, outside any
105
+ * try, and a wrapper whose contract is that it never rethrows must not throw
106
+ * from its own diagnosis of a malformed payload.
107
+ */
108
+ function failure(entry, payload, reason) {
109
+ const logger = getSafeLogger();
110
+ if (entry.event === "before_tool") {
111
+ const tool = payload?.call?.name;
112
+ logger?.warn("native-loop-events", `plugin '${entry.plugin}' failed on before_tool; blocking the call`, {
113
+ plugin: entry.plugin,
114
+ event: entry.event,
115
+ ...(tool !== undefined ? { tool } : {}),
116
+ error: reason,
117
+ });
118
+ return {
119
+ kind: "block",
120
+ isError: true,
121
+ content: `Blocked: loop handler from plugin '${entry.plugin}' failed (${reason}).`,
122
+ };
123
+ }
124
+ logger?.warn("native-loop-events", `plugin '${entry.plugin}' failed on ${entry.event}`, {
125
+ plugin: entry.plugin,
126
+ event: entry.event,
127
+ error: reason,
128
+ });
129
+ return {};
130
+ }
131
+ /**
132
+ * Log which patch fields a handler carried, so a later `applyHistoryPatch`
133
+ * rejection can be traced back to the plugin that caused it. An empty patch
134
+ * traces nothing — there is no field to attribute.
135
+ */
136
+ function tracePatch(entry, value) {
137
+ // A plain-JavaScript plugin can answer a non-object on any event; there is
138
+ // then no field to attribute, and reading one must not throw.
139
+ if (typeof value !== "object" || value === null)
140
+ return;
141
+ const patch = value;
142
+ const fields = TRACED_PATCH_FIELDS.filter((field) => patch[field] !== undefined);
143
+ if (fields.length === 0)
144
+ return;
145
+ getSafeLogger()?.debug("native-loop-events", `plugin '${entry.plugin}' patched ${entry.event}`, {
146
+ plugin: entry.plugin,
147
+ event: entry.event,
148
+ fields,
149
+ });
150
+ }
151
+ function describeValue(value) {
152
+ if (value === null)
153
+ return "null";
154
+ if (Array.isArray(value))
155
+ return "an array";
156
+ if (typeof value === "object")
157
+ return "an object whose kind is not a before_tool outcome";
158
+ return `a ${typeof value} value`;
159
+ }
160
+ /** What a settled (non-failure) handler answer becomes. */
161
+ function settled(entry, payload, value) {
162
+ if (entry.event === "before_tool") {
163
+ // `undefined` is the handler declining to decide, which is an allow.
164
+ if (value === undefined)
165
+ return { kind: "allow" };
166
+ // The plugin boundary, so the answer must be COMPLETE: a `kind` without
167
+ // the payload it promises (`block`/`terminate` content, `nudge` text) is
168
+ // malformed, not a decision.
169
+ if (!isCompleteBeforeToolOutcome(value))
170
+ return failure(entry, payload, `returned ${describeValue(value)}`);
171
+ return value;
172
+ }
173
+ if (value === undefined)
174
+ return {};
175
+ tracePatch(entry, value);
176
+ return value;
177
+ }
178
+ /**
179
+ * Wrap one staged plugin registration into a built-in-shaped `HandlerOf<E>`:
180
+ * same payload, same patch, but with a bounded lifetime, the turn's context,
181
+ * and a failure that is attributed and never rethrown.
182
+ */
183
+ export function wrapExternalHandler(entry, getCtx, signal) {
184
+ return async (payload) => {
185
+ const settlement = await raceSettlement(entry, payload, { getCtx, signal });
186
+ // `entry.event` carries the event at runtime; the erased patch is the union
187
+ // the dispatcher re-narrows, because it registered this handler per event.
188
+ const answer = settlement.ok
189
+ ? settled(entry, payload, settlement.value)
190
+ : failure(entry, payload, settlement.reason);
191
+ return answer;
192
+ };
193
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Barrel for the loop-event seam (spec 4.5): the single-file `loop-events.ts`
3
+ * module became a directory, and this re-exports every name it exported, so
4
+ * no importer churns.
5
+ */
6
+ export { type BuildToolResultArgs, buildToolResult, type DenialInfo, type ToolResultMessage } from "../tool-result.ts";
7
+ /**
8
+ * The external-handler surface (US-002). It is re-exported rather than reached
9
+ * through `./external-handler` because `check:alias-internals` rejects a VALUE
10
+ * import of `@/<dir>/<internal>` from `src/`, so the barrel is the only route
11
+ * US-003's installer and US-004's delivery code can take. Identity matters:
12
+ * `_externalHandlerDeps` is the object the wrapper reads at dispatch time.
13
+ */
14
+ export { _externalHandlerDeps, LOOP_HANDLER_TIMEOUT_MS, wrapExternalHandler } from "./external-handler.ts";
15
+ export { createLoopEventRegistry, LOOP_EVENTS, type LoopEventRegistry } from "./registry.ts";
16
+ export type { AfterToolPatch, AfterToolPayload, BeforeToolOutcome, CompleteCallOptions, ExternalHandlerOf, LoopHandlerContext, LoopHandlerEntry, LoopHandlerSet, } from "./types.ts";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Barrel for the loop-event seam (spec 4.5): the single-file `loop-events.ts`
3
+ * module became a directory, and this re-exports every name it exported, so
4
+ * no importer churns.
5
+ */
6
+ export { buildToolResult } from "../tool-result.js";
7
+ /**
8
+ * The external-handler surface (US-002). It is re-exported rather than reached
9
+ * through `./external-handler` because `check:alias-internals` rejects a VALUE
10
+ * import of `@/<dir>/<internal>` from `src/`, so the barrel is the only route
11
+ * US-003's installer and US-004's delivery code can take. Identity matters:
12
+ * `_externalHandlerDeps` is the object the wrapper reads at dispatch time.
13
+ */
14
+ export { _externalHandlerDeps, LOOP_HANDLER_TIMEOUT_MS, wrapExternalHandler } from "./external-handler.js";
15
+ export { createLoopEventRegistry, LOOP_EVENTS } from "./registry.js";
@@ -0,0 +1,9 @@
1
+ interface ArrayRecord {
2
+ readonly field: string;
3
+ readonly array: unknown[];
4
+ readonly saved: readonly unknown[];
5
+ }
6
+ export type ArraySnapshot = readonly ArrayRecord[];
7
+ export declare function snapshotArrays(payload: object): ArraySnapshot;
8
+ export declare function restoreMutated(snapshot: ArraySnapshot, event: string, handlerIndex: number): void;
9
+ export {};
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Review #19: the cache boundary judges only RETURNED patches, but handlers
3
+ * receive the loop's live arrays (history, messages, tools). A handler that
4
+ * edits one in place would rewrite the transcript and the prompt-cache
5
+ * prefix unseen. Dispatch snapshots every array field before each handler and
6
+ * undoes an in-place change afterwards, keeping the SAME array object with
7
+ * its original elements.
8
+ *
9
+ * Not detected: edits INSIDE an element (a message's fields). Deep-freezing
10
+ * is not an option: the payload is live, and the loop itself pushes onto
11
+ * `messages` after a follow-up; freezing a clone would break the reference
12
+ * identity checkPrefixStable relies on.
13
+ */
14
+ import { getSafeLogger } from "#src/infra/index";
15
+ export function snapshotArrays(payload) {
16
+ return Object.entries(payload).flatMap(([field, value]) => Array.isArray(value) ? [{ field, array: value, saved: [...value] }] : []);
17
+ }
18
+ export function restoreMutated(snapshot, event, handlerIndex) {
19
+ for (const { field, array, saved } of snapshot) {
20
+ const changed = array.length !== saved.length || saved.some((item, i) => array[i] !== item);
21
+ if (!changed)
22
+ continue;
23
+ getSafeLogger()?.warn("native-loop-events", "handler mutated payload in place; restored", {
24
+ event,
25
+ handler: handlerIndex,
26
+ field,
27
+ });
28
+ // Deliberate in-place restore: the caller holds this exact array object.
29
+ array.splice(0, array.length, ...saved);
30
+ }
31
+ }