@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,345 @@
1
+ /**
2
+ * Pairs the compiled policy with the tool registry and answers one call.
3
+ *
4
+ * nax-permission-mode-allow: consumes grants resolved by resolvePermissions;
5
+ * makes no permission decision of its own.
6
+ *
7
+ * The three outcomes are kept structurally distinct. Reusing one channel for a
8
+ * refusal and a crash would make a denied permission look like a recoverable
9
+ * tool error, which ADR-029 section 5 forbids.
10
+ */
11
+ import { randomUUID } from "node:crypto";
12
+ import { getSafeLogger } from "#src/infra/index";
13
+ import { headlessAskResolver } from "#src/permissions/index";
14
+ import { deleteTool } from "./delete.js";
15
+ import { editTool } from "./edit.js";
16
+ import { gitTool } from "./git.js";
17
+ import { gitCommitTool } from "./git-commit.js";
18
+ import { globTool } from "./glob.js";
19
+ import { grepTool } from "./grep.js";
20
+ import { readTool } from "./read.js";
21
+ import { getCodingTool, registerBuiltinTool } from "./registry.js";
22
+ import { requestCapabilityTool } from "./request-capability.js";
23
+ import { openCallShadowTap, resolveAskOutcome, resolveDenialOutcome, resolveGuardDenial, resolvePolicyIdentity, } from "./runtime-calltool.js";
24
+ import { scratchpadListTool, scratchpadReadTool, scratchpadWriteTool } from "./scratchpad.js";
25
+ import { applyModelTruncationPolicy } from "./spill.js";
26
+ import { createNoOpToolAuditSink } from "./tool-audit.js";
27
+ import { READ_CEILING } from "./truncate.js";
28
+ import { writeTool } from "./write.js";
29
+ /** Per-call output ceiling, mirroring ToolDescriptor.maxTokensPerCall in spirit. */
30
+ export const DEFAULT_TOOL_MAX_BYTES = 40_000;
31
+ /**
32
+ * Largest file a tool will read whole or write at all.
33
+ *
34
+ * Distinct from DEFAULT_TOOL_MAX_BYTES because they answer different questions:
35
+ * that one is how much the model may be told, this one is how much file a tool
36
+ * will handle. Edit must read and rewrite a file entirely, so it cannot use the
37
+ * output ceiling without refusing ordinary source files.
38
+ *
39
+ * Generous on purpose. Nothing an LLM legitimately edits approaches it, and a
40
+ * limit that fires during normal work would be worked around rather than
41
+ * respected.
42
+ */
43
+ export const DEFAULT_TOOL_MAX_FILE_BYTES = 2_000_000;
44
+ /** Injectable logger seam, mirroring _pullToolsDeps.getLogger. */
45
+ export const _codingToolDeps = { getLogger: getSafeLogger };
46
+ let builtinsRegistered = false;
47
+ /** Idempotent: the registry is process-global, the runtime is per-session. */
48
+ export function registerBuiltinCodingTools() {
49
+ if (builtinsRegistered)
50
+ return;
51
+ for (const tool of [
52
+ readTool,
53
+ globTool,
54
+ grepTool,
55
+ writeTool,
56
+ editTool,
57
+ deleteTool,
58
+ gitTool,
59
+ gitCommitTool,
60
+ requestCapabilityTool,
61
+ scratchpadWriteTool,
62
+ scratchpadReadTool,
63
+ scratchpadListTool,
64
+ ]) {
65
+ if (getCodingTool(tool.name) === undefined)
66
+ registerBuiltinTool(tool);
67
+ }
68
+ builtinsRegistered = true;
69
+ }
70
+ /** @internal Test-only: pairs with _resetRegistryForTest. */
71
+ export function _resetBuiltinsForTest() {
72
+ builtinsRegistered = false;
73
+ }
74
+ /**
75
+ * The run-scoped ports every ToolRunContext carries, present-field only.
76
+ * Built once per runtime and kept out of `runTool`, which sits at its
77
+ * complexity baseline.
78
+ */
79
+ function contextPorts(opts) {
80
+ return {
81
+ ...(opts.interceptor !== undefined ? { interceptor: opts.interceptor } : {}),
82
+ ...(opts.protectedPaths !== undefined ? { protectedPaths: opts.protectedPaths } : {}),
83
+ };
84
+ }
85
+ export function createCodingToolRuntime(opts) {
86
+ registerBuiltinCodingTools();
87
+ // The global registry cannot hold session-local tools like RunCommand (its
88
+ // declared commands are per-project). Consult this layer before the registry.
89
+ const extra = new Map((opts.extraTools ?? []).map((t) => [t.name, t]));
90
+ const lookup = (name) => extra.get(name) ?? getCodingTool(name);
91
+ const sink = opts.sink ?? createNoOpToolAuditSink();
92
+ const ports = contextPorts(opts);
93
+ const maxBytes = opts.maxBytes ?? DEFAULT_TOOL_MAX_BYTES;
94
+ const maxFileBytes = opts.maxFileBytes ?? DEFAULT_TOOL_MAX_FILE_BYTES;
95
+ const readCeiling = opts.readCeiling ?? READ_CEILING;
96
+ const askResolver = opts.askResolver ?? headlessAskResolver();
97
+ const granted = new Set(opts.policy.grantedTools());
98
+ // Captured so the `ToolRunContext` constructed inside `runTool` carries the
99
+ // session-wide abort signal; tools (Bash, Exec's argv branch) forward it
100
+ // into their launcher / runArgv call so turn cancellation reaches the
101
+ // process group. US-001.
102
+ const signal = opts.signal;
103
+ // What `advertised()` actually returned, so a denial can name only tools the
104
+ // session really received. Recomputing from `granted` would be wrong: an op
105
+ // narrows the set by declaring fewer tools than it was granted.
106
+ let advertisedNames = new Set();
107
+ /**
108
+ * One line per call, mirroring the pull-tool subsystem's `invoked` record.
109
+ *
110
+ * Every outcome is logged, denials included: a refused call that leaves no
111
+ * trace is indistinguishable from a call never made, and telling those two
112
+ * apart is the whole reason this exists.
113
+ *
114
+ * The logger keeps its calls for operator visibility. The audit sink is the
115
+ * durable copy a later decision reads from; the two are not interchangeable.
116
+ */
117
+ function log(tool, outcome, resultBytes, input, context, breach, reason, routineErrors, audit, resultBytesPreTruncation) {
118
+ // The level is the console filter: `normal` mode drops debug, and the file
119
+ // sink writes every level regardless, so demoting keeps the record without
120
+ // spending an operator's attention on it. A breach is the one outcome that
121
+ // can indicate prompt injection, so it alone reaches `error`.
122
+ const level = outcome === "denied" || outcome === "denied:ask"
123
+ ? breach === true
124
+ ? "error"
125
+ : "warn"
126
+ : outcome === "error" && !routineErrors
127
+ ? "warn"
128
+ : "debug";
129
+ // Never parse the namespaced name apart — the provider id is carried
130
+ // explicitly precisely so a naming-convention change cannot break
131
+ // telemetry silently. `tool` is the identity the ledger records: the
132
+ // namespaced name for a provider tool, `Exec` for the argv branch (which
133
+ // is not provider-supplied, so the lookup misses and the field is absent).
134
+ const provider = opts.providerIdByTool?.get(tool);
135
+ // `reason` rides under the `error` key because the formatter's
136
+ // readFailureReason() renders exactly that key on warn/error lines. The
137
+ // message names the tool so the line is legible without the JSONL: these
138
+ // used to print as a bare "coding-tool invoked" with neither.
139
+ _codingToolDeps.getLogger()?.[level]("coding-tool", `${tool} ${outcome}`, {
140
+ storyId: opts.storyId,
141
+ tool,
142
+ outcome,
143
+ resultBytes,
144
+ ...(reason !== undefined && reason.length > 0 ? { error: reason } : {}),
145
+ });
146
+ sink.record({
147
+ tool,
148
+ outcome,
149
+ breach,
150
+ input,
151
+ resultBytes,
152
+ storyId: opts.storyId,
153
+ at: new Date().toISOString(),
154
+ ...(reason !== undefined && reason.length > 0 ? { reason } : {}),
155
+ ...(audit?.executed !== undefined ? { executed: audit.executed } : {}),
156
+ ...(audit?.target !== undefined ? { target: audit.target } : {}),
157
+ ...(audit?.approval !== undefined ? { approval: audit.approval } : {}),
158
+ ...(audit?.sandbox !== undefined ? { sandbox: audit.sandbox } : {}),
159
+ ...(audit?.exitCode !== undefined ? { exitCode: audit.exitCode } : {}),
160
+ ...(provider !== undefined ? { provider } : {}),
161
+ ...(resultBytesPreTruncation !== undefined ? { resultBytesPreTruncation } : {}),
162
+ ...(opts.callId !== undefined ? { callId: opts.callId } : {}),
163
+ ...(opts.scopeId !== undefined ? { scopeId: opts.scopeId } : {}),
164
+ ...(context?.turnId !== undefined ? { turnId: context.turnId } : {}),
165
+ ...(context?.roundTrips !== undefined ? { roundTrips: context.roundTrips } : {}),
166
+ ...(context?.toolCallId !== undefined ? { toolCallId: context.toolCallId } : {}),
167
+ });
168
+ }
169
+ async function shapeToolResult(body, toolName, context) {
170
+ if (context?.deferModelTruncation === true)
171
+ return body;
172
+ return applyModelTruncationPolicy(body, {
173
+ toolName,
174
+ callId: context?.toolCallId ?? randomUUID(),
175
+ root: opts.policy.root,
176
+ maxBytes,
177
+ });
178
+ }
179
+ return {
180
+ advertised(declared) {
181
+ const out = [];
182
+ for (const name of declared) {
183
+ if (!granted.has(name))
184
+ continue;
185
+ const tool = lookup(name);
186
+ if (tool !== undefined)
187
+ out.push(tool);
188
+ }
189
+ advertisedNames = new Set(out.map((t) => t.name));
190
+ return out;
191
+ },
192
+ async callTool(name, input, context) {
193
+ const tool = lookup(name);
194
+ if (tool === undefined) {
195
+ const reason = `unknown tool "${name}"`;
196
+ log(name, "denied", 0, input, context, false, reason);
197
+ return { kind: "denied", reason, breach: false };
198
+ }
199
+ const { policyIdentity, argvField, hasArgv } = resolvePolicyIdentity(name, tool, input);
200
+ const verdict = opts.policy.check(policyIdentity, tool.scope, input);
201
+ // P5: observe the command now (not awaited), settle from logCall below.
202
+ const tap = openCallShadowTap({
203
+ shadow: opts.commandShadow,
204
+ identity: policyIdentity,
205
+ tool,
206
+ input,
207
+ hasArgv,
208
+ argvField,
209
+ root: opts.policy.root,
210
+ stage: opts.pipelineStage,
211
+ storyId: opts.storyId,
212
+ callId: opts.callId,
213
+ scopeId: opts.scopeId,
214
+ context,
215
+ verdict,
216
+ });
217
+ // Every ledger outcome of this call settles the tap exactly once, with
218
+ // `denied:ask` and `decidedBy` intact -- which CodingToolOutcome.kind
219
+ // alone would lose (spec 4.2). The audit is a named parameter, not a
220
+ // rest-array index: `log`'s positional shape can then change without
221
+ // silently shifting what the tap settles with.
222
+ const logCall = (tool, outcome, resultBytes, input, context, breach, reason, routineErrors, audit, resultBytesPreTruncation) => {
223
+ log(tool, outcome, resultBytes, input, context, breach, reason, routineErrors, audit, resultBytesPreTruncation);
224
+ tap?.settle(outcome, audit?.approval?.decidedBy, audit?.executed === undefined
225
+ ? undefined
226
+ : { executed: audit.executed, ...(audit.cwd !== undefined ? { cwd: audit.cwd } : {}) });
227
+ };
228
+ /**
229
+ * Executes a permitted call and records its outcome. Shared by the
230
+ * ordinary allow path and an ask verdict an AskResolver approved, so an
231
+ * approved call behaves exactly as a grant would have.
232
+ *
233
+ * The result is shaped by `applyModelTruncationPolicy` before it is
234
+ * returned, which is what makes the model-facing cap one policy rather
235
+ * than one per tool: the tools bound their own I/O at `ctx.readCeiling`,
236
+ * and this is where the byte/line ceilings the model experiences — and
237
+ * the spill of whatever they cut — are applied. The native session
238
+ * applies the same policy at its `after_tool` chokepoint, so a call made
239
+ * through the loop and one made directly see the same shaping.
240
+ */
241
+ async function runTool(target, callInput, resolvedPaths, approval) {
242
+ // US-002 AC15: a per-call `ToolCallContext.signal` (the native batch's
243
+ // turn signal) takes priority over the runtime-level signal. Both
244
+ // describe "abort this in-flight tool"; the per-call one is the one
245
+ // the batch explicitly put in scope, so it wins when both are
246
+ // present, falling back to the runtime-level signal otherwise so the
247
+ // session-wide abort keeps working when no per-call signal is in
248
+ // scope.
249
+ const callSignal = context?.signal ?? signal;
250
+ try {
251
+ const result = await target.run(callInput, {
252
+ root: opts.policy.root,
253
+ resolvedPaths,
254
+ maxBytes,
255
+ maxFileBytes,
256
+ readCeiling,
257
+ ...(opts.denyPaths !== undefined ? { denyPaths: opts.denyPaths } : {}),
258
+ ...ports,
259
+ ...(callSignal !== undefined ? { signal: callSignal } : {}),
260
+ });
261
+ const kind = result.isError === true ? "error" : "ok";
262
+ const content = await shapeToolResult(result.content, policyIdentity, context);
263
+ const record = (finalContent) => logCall(policyIdentity, kind, finalContent.length, callInput, context, false, kind === "error" ? result.content : undefined, target.routineErrors, { ...result.audit, ...(approval ? { approval } : {}) }, result.resultBytesPreTruncation);
264
+ if (context?.deferModelTruncation === true)
265
+ return { kind, content, finalizeAudit: record };
266
+ record(content);
267
+ return { kind, content };
268
+ }
269
+ catch (err) {
270
+ const rawContent = err instanceof Error ? err.message : String(err);
271
+ const content = await shapeToolResult(rawContent, policyIdentity, context);
272
+ const record = (finalContent) => logCall(policyIdentity, "error", finalContent.length, callInput, context, false, rawContent, target.routineErrors, approval ? { approval } : undefined);
273
+ if (context?.deferModelTruncation === true) {
274
+ return { kind: "error", content, finalizeAudit: record };
275
+ }
276
+ record(content);
277
+ return { kind: "error", content };
278
+ }
279
+ }
280
+ if (!verdict.allowed && verdict.outcome === "ask") {
281
+ return resolveAskOutcome({
282
+ identity: policyIdentity,
283
+ tool,
284
+ input,
285
+ context,
286
+ verdict,
287
+ runtimeSignal: signal,
288
+ askResolver,
289
+ stage: opts.pipelineStage,
290
+ storyId: opts.storyId,
291
+ root: opts.policy.root,
292
+ logCall,
293
+ runTool,
294
+ });
295
+ }
296
+ if (!verdict.allowed) {
297
+ return resolveDenialOutcome({
298
+ name,
299
+ identity: policyIdentity,
300
+ tool,
301
+ input,
302
+ context,
303
+ verdict,
304
+ argvField,
305
+ advertisedNames,
306
+ declaredCommands: opts.declaredCommands,
307
+ root: opts.policy.root,
308
+ logCall,
309
+ });
310
+ }
311
+ // US-004: an ALLOWED `Bash`/`Exec` call the configured guard flags is
312
+ // routed through the existing ask channel; every other call (no guard,
313
+ // unflagged, any other identity) runs exactly as before. The tap above
314
+ // was opened BEFORE this await, so the shadow's observation starts the
315
+ // shared classification the guard then reuses.
316
+ const guardDenial = await resolveGuardDenial({
317
+ identity: policyIdentity,
318
+ tool,
319
+ input,
320
+ argvField,
321
+ guard: opts.commandShadow?.guard,
322
+ root: opts.policy.root,
323
+ tempConfined: opts.tempConfined ?? false,
324
+ resolvedPaths: verdict.resolvedPaths,
325
+ });
326
+ if (guardDenial !== undefined) {
327
+ return resolveAskOutcome({
328
+ identity: policyIdentity,
329
+ tool,
330
+ input,
331
+ context,
332
+ verdict: guardDenial,
333
+ runtimeSignal: signal,
334
+ askResolver,
335
+ stage: opts.pipelineStage,
336
+ storyId: opts.storyId,
337
+ root: opts.policy.root,
338
+ logCall,
339
+ runTool,
340
+ });
341
+ }
342
+ return runTool(tool, input, verdict.resolvedPaths);
343
+ },
344
+ };
345
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Confined write/read/list tools for an agent-owned throwaway directory.
3
+ *
4
+ * Three small tools (ScratchpadWrite / ScratchpadRead / ScratchpadList) that
5
+ * let a session keep notes, command output and intermediate lists between
6
+ * calls without ever writing anything the policy could later attribute to
7
+ * the run. Containment is the load-bearing seam: every tool declares
8
+ * `scope.confineTo = SCRATCHPAD_DIR`, so the policy runs `resolveWithin`
9
+ * against `<root>/<confineTo>` instead of the repository root. A `..`
10
+ * traversal leaves the confined directory, returns null from `resolveWithin`,
11
+ * and the policy reports `breach: true` (US-001). The tools themselves never
12
+ * resolve a path -- they consume `ctx.resolvedPaths[0]` only, so the policy
13
+ * stays the single seam.
14
+ */
15
+ import type { CodingTool } from "./registry.ts";
16
+ /** The canonical scratchpad path, written into the policy as `scope.confineTo`. */
17
+ export declare const SCRATCHPAD_DIR = ".nax/scratchpad";
18
+ export declare const scratchpadWriteTool: CodingTool;
19
+ export declare const scratchpadReadTool: CodingTool;
20
+ export declare const scratchpadListTool: CodingTool;
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Confined write/read/list tools for an agent-owned throwaway directory.
3
+ *
4
+ * Three small tools (ScratchpadWrite / ScratchpadRead / ScratchpadList) that
5
+ * let a session keep notes, command output and intermediate lists between
6
+ * calls without ever writing anything the policy could later attribute to
7
+ * the run. Containment is the load-bearing seam: every tool declares
8
+ * `scope.confineTo = SCRATCHPAD_DIR`, so the policy runs `resolveWithin`
9
+ * against `<root>/<confineTo>` instead of the repository root. A `..`
10
+ * traversal leaves the confined directory, returns null from `resolveWithin`,
11
+ * and the policy reports `breach: true` (US-001). The tools themselves never
12
+ * resolve a path -- they consume `ctx.resolvedPaths[0]` only, so the policy
13
+ * stays the single seam.
14
+ */
15
+ import { existsSync } from "node:fs";
16
+ import { mkdir, writeFile } from "node:fs/promises";
17
+ import { dirname, join, sep } from "node:path";
18
+ import { fileSizeOrZero } from "#src/internal/file-size";
19
+ import { getAgentRuntime } from "#src/runtime/index";
20
+ import { resolveWithin } from "./policy.js";
21
+ import { readFileSlice } from "./read-file.js";
22
+ import { READ_CEILING } from "./truncate.js";
23
+ /** The canonical scratchpad path, written into the policy as `scope.confineTo`. */
24
+ export const SCRATCHPAD_DIR = ".nax/scratchpad";
25
+ /**
26
+ * Match cap for ScratchpadList. Mirrors `glob.ts`'s `MAX_MATCHES` -- a
27
+ * long-running session accumulates throwaway files by design, so an
28
+ * unbounded scan would feed an arbitrarily large listing back into model
29
+ * context, bypassing the model-facing ceiling the after_tool policy
30
+ * applies to every other content-returning tool.
31
+ */
32
+ const MAX_MATCHES = 500;
33
+ export const scratchpadWriteTool = {
34
+ name: "ScratchpadWrite",
35
+ description: "Write a throwaway file to your scratchpad at .nax/scratchpad/. Use it for notes to yourself, command output you want to re-read, intermediate lists, or a probe script to run against the project's code. It is never committed and is wiped when a run finishes (a failed run's scratchpad is retained for inspection until the next run starts and clears it). Paths are relative to the scratchpad and cannot reach the repository.",
36
+ inputSchema: {
37
+ type: "object",
38
+ properties: {
39
+ path: { type: "string", description: "Path relative to the scratchpad directory" },
40
+ content: { type: "string", description: "Full file contents to write" },
41
+ },
42
+ required: ["path", "content"],
43
+ },
44
+ scope: { pathFields: ["path"], confineTo: SCRATCHPAD_DIR },
45
+ async run(input, ctx) {
46
+ const [target] = ctx.resolvedPaths;
47
+ if (target === undefined)
48
+ return { content: "no path supplied", isError: true };
49
+ const content = input.content;
50
+ if (typeof content !== "string")
51
+ return { content: "content must be a string", isError: true };
52
+ // Check the ceiling BEFORE any directory or file creation, so a refused
53
+ // write leaves no empty file at the target. This is the load-bearing
54
+ // property the AC11 boundary pins.
55
+ const size = Buffer.byteLength(content, "utf8");
56
+ if (size > ctx.maxFileBytes) {
57
+ return {
58
+ content: `content is ${size} bytes, which exceeds the ${ctx.maxFileBytes}-byte file ceiling`,
59
+ isError: true,
60
+ };
61
+ }
62
+ try {
63
+ await mkdir(dirname(target), { recursive: true });
64
+ await writeFile(target, content, "utf8");
65
+ return { content: `wrote ${size} bytes` };
66
+ }
67
+ catch (err) {
68
+ return { content: err instanceof Error ? err.message : String(err), isError: true };
69
+ }
70
+ },
71
+ };
72
+ export const scratchpadReadTool = {
73
+ name: "ScratchpadRead",
74
+ description: "Read a throwaway file from your scratchpad at .nax/scratchpad/. Paths are relative to the scratchpad. Use it to re-read notes, command output and intermediate lists you wrote with ScratchpadWrite -- including the output a truncated tool result spilled there. Optionally pass offset (1-based line number to start from) and/or limit (maximum number of lines to return) to page through a body too large to read at once.",
75
+ inputSchema: {
76
+ type: "object",
77
+ properties: {
78
+ path: { type: "string", description: "Path relative to the scratchpad directory" },
79
+ offset: { type: "integer", minimum: 1, description: "1-based line number to start reading from" },
80
+ limit: { type: "integer", minimum: 1, description: "Maximum number of lines to return" },
81
+ },
82
+ required: ["path"],
83
+ },
84
+ scope: { pathFields: ["path"], confineTo: SCRATCHPAD_DIR },
85
+ async run(input, ctx) {
86
+ const [target] = ctx.resolvedPaths;
87
+ if (target === undefined)
88
+ return { content: "no path supplied", isError: true };
89
+ const requestedPath = typeof input.path === "string" ? input.path : "<path>";
90
+ const rawOffset = input.offset;
91
+ if (rawOffset !== undefined && typeof rawOffset !== "number") {
92
+ return { content: "offset must be an integer", isError: true };
93
+ }
94
+ const rawLimit = input.limit;
95
+ if (rawLimit !== undefined && typeof rawLimit !== "number") {
96
+ return { content: "limit must be an integer", isError: true };
97
+ }
98
+ const offset = rawOffset;
99
+ const limit = rawLimit;
100
+ try {
101
+ // The tool bounds its own I/O at `readCeiling`, NOT at `maxBytes`: the
102
+ // model-facing cap belongs to the session's after_tool policy, which
103
+ // also owns the spill of whatever it cuts. Reading up to the ceiling is
104
+ // what lets a body in (maxBytes, readCeiling) reach that policy whole.
105
+ //
106
+ // `resultBytesPreTruncation` reports the file's FULL byte length, before
107
+ // any of this, so the ledger can answer "how much did we discard" for a
108
+ // result that was shaped after it left here.
109
+ const slice = await readFileSlice(target, {
110
+ readCeiling: ctx.readCeiling ?? READ_CEILING,
111
+ ...(offset !== undefined ? { offset } : {}),
112
+ ...(limit !== undefined ? { limit } : {}),
113
+ });
114
+ const fullBytes = fileSizeOrZero(target);
115
+ const { content, bounded, totalLines } = slice;
116
+ // The header leads EVERY read that returns file content, the paged one
117
+ // included: it reports the FILE's line count, which is what a model
118
+ // paging a spilled body needs in order to know how many pages remain —
119
+ // the page it asked for is not a substitute for that number. Precedent
120
+ // from readTool's whole-file read, marked with `+` when the read stopped
121
+ // at the I/O ceiling and the count is therefore a floor rather than the
122
+ // file's true total.
123
+ const header = `[${bounded ? `${totalLines}+` : `${totalLines}`} lines]`;
124
+ if (offset !== undefined || limit !== undefined) {
125
+ // An offset past the last line is answered with the line count rather
126
+ // than an empty result, because "there is nothing here" is
127
+ // indistinguishable from "the file is empty" and the model can act on
128
+ // the number -- and there the count IS the message, so no header leads
129
+ // it. The condition is the OFFSET against the file, never the emptiness
130
+ // of the slice: a range that selects only blank lines is a perfectly
131
+ // valid page, and reporting it as past-the-end would hide content the
132
+ // caller asked for and that the file really holds.
133
+ if (offset !== undefined && offset > totalLines) {
134
+ const pastEnd = `offset ${String(offset)} is past the end of the file -- it has ${totalLines} lines`;
135
+ return { content: pastEnd, resultBytesPreTruncation: fullBytes };
136
+ }
137
+ // The header goes on its own line and the requested range follows it,
138
+ // verbatim -- possibly empty, when the page holds blank lines.
139
+ // readTool's ranged read composes it the same way.
140
+ return { content: `${header}\n${content}`, resultBytesPreTruncation: fullBytes };
141
+ }
142
+ return { content: content === "" ? header : `${header}\n${content}`, resultBytesPreTruncation: fullBytes };
143
+ }
144
+ catch (err) {
145
+ // A missing file is a tool ERROR the model can react to, never a
146
+ // denial -- the policy already said yes. Naming the requested path is
147
+ // what AC7 pins: the model should see what it asked for, not "ENOENT".
148
+ const message = err instanceof Error ? err.message : String(err);
149
+ return { content: `cannot read "${requestedPath}": ${message}`, isError: true };
150
+ }
151
+ },
152
+ };
153
+ export const scratchpadListTool = {
154
+ name: "ScratchpadList",
155
+ description: "List files in your scratchpad at .nax/scratchpad/. Returns paths relative to the scratchpad. Reports no entries when the scratchpad is empty or has not been written to this run.",
156
+ inputSchema: {
157
+ type: "object",
158
+ properties: {},
159
+ },
160
+ // No path field: there is nothing for the policy to gate per call. The
161
+ // directory the tool reads is the confined directory, declared at registration.
162
+ scope: { pathFields: [], confineTo: SCRATCHPAD_DIR },
163
+ async run(_input, ctx) {
164
+ const scratchpad = join(ctx.root, SCRATCHPAD_DIR);
165
+ // A missing scratchpad is not an error -- a model that has not written
166
+ // anything yet (or one whose prior writes have been wiped at run start)
167
+ // should see a clean empty listing, not an ENOENT to recover from.
168
+ if (!existsSync(scratchpad))
169
+ return { content: "(no entries)" };
170
+ const entries = getAgentRuntime().globSync("**/*", { cwd: scratchpad, absolute: false });
171
+ // Mirror glob.ts: every match is re-checked through resolveWithin before
172
+ // it is emitted, so a directory symlink planted inside the scratchpad
173
+ // cannot leak external paths the policy never saw. resolveWithin runs
174
+ // the symlink through isInside(realOrRaw(...)), so a symlink to outside
175
+ // the policy root yields null and the entry is skipped.
176
+ //
177
+ // Sort first, then cap. Capping first would leak the scan's enumeration
178
+ // order into the model, which is not what any caller expects from a
179
+ // listing -- a deterministic alphabetical prefix is the useful signal
180
+ // when a scratchpad has grown past the cap. The caller materializes the
181
+ // full list before we see it, so the sort/cap are bounded by what the
182
+ // scan produced, not by anything the tool itself decided to walk.
183
+ const sorted = [...entries].sort();
184
+ const files = [];
185
+ for (const hit of sorted) {
186
+ if (files.length >= MAX_MATCHES)
187
+ break;
188
+ if (resolveWithin(ctx.root, hit) === null)
189
+ continue;
190
+ files.push(hit.split(sep).join("/"));
191
+ }
192
+ if (files.length === 0)
193
+ return { content: "(no entries)" };
194
+ const rendered = files.join("\n");
195
+ return { content: rendered };
196
+ },
197
+ };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Spill writer and the model-facing composition of the truncation policy
3
+ * (US-003).
4
+ *
5
+ * The policy itself (`truncateForModel`) decides *what* the model may see; this
6
+ * module decides what happens to the rest. On truncation only, the untruncated
7
+ * body — up to `READ_CEILING` — is written under the scratchpad at
8
+ * `spill/<toolName>-<callId>.txt`, and the marker naming that path and both byte
9
+ * counts is appended to the content the model receives. `writeSpill` keeps
10
+ * returning the scratchpad-relative spelling; `applyModelTruncationPolicy`
11
+ * turns it into the path the marker names — root-relative
12
+ * (`.nax/scratchpad/spill/...`) by default, so the session's own tools and
13
+ * shell can open it verbatim, or absolute when the root is not the session's
14
+ * shell root. The spilled body stays recoverable with `offset`/`limit`.
15
+ *
16
+ * The composition lives here, beside the writer, because the marker and the
17
+ * spill are one decision: the path may only appear in the marker when the write
18
+ * actually succeeded. Both transports reach it — the coding-tool runtime applies
19
+ * it to every genuine tool result, and the native session applies it at the
20
+ * `after_tool` chokepoint — so there is exactly one implementation of the
21
+ * model-facing cap rather than one per tool.
22
+ *
23
+ * Filesystem calls are injectable (`_spillDeps`), following `_scratchpadWipeDeps`
24
+ * in src/execution/lifecycle/scratchpad-wipe.ts, and writes go through
25
+ * asynchronous Node filesystem APIs.
26
+ */
27
+ /** Directory under the scratchpad that holds spilled tool output. */
28
+ export declare const SPILL_DIR = "spill";
29
+ /** Injectable filesystem seam (see docs/architecture/conventions.md §2). */
30
+ export declare const _spillDeps: {
31
+ mkdir: (path: string) => Promise<string | undefined>;
32
+ writeFile: (path: string, data: string) => Promise<number>;
33
+ };
34
+ export interface SpillRequest {
35
+ /** Absolute root holding `.nax/scratchpad/` — the same root the tools are confined to. */
36
+ readonly root: string;
37
+ readonly toolName: string;
38
+ readonly callId: string;
39
+ /** The untruncated body, exactly as the tool returned it. */
40
+ readonly body: string;
41
+ }
42
+ /** `spill/<toolName>-<callId>.txt`, relative to the scratchpad directory. */
43
+ export declare function spillRelativePath(toolName: string, callId: string): string;
44
+ /**
45
+ * Write the untruncated body under the scratchpad and return its path relative
46
+ * to the scratchpad, or `undefined` when the write failed.
47
+ *
48
+ * Failure is fail-open by contract: the caller still delivers the truncated
49
+ * result. What it must not do is name a file that is not there, which is why
50
+ * this returns the path rather than a boolean.
51
+ */
52
+ export declare function writeSpill(req: SpillRequest): Promise<string | undefined>;
53
+ export interface ModelTruncationOptions {
54
+ readonly toolName: string;
55
+ /** Identifies the spill file; the model's own tool-call id when it has one. */
56
+ readonly callId: string;
57
+ /** Absolute root to spill under. Omitted means no spill, and no path in the marker. */
58
+ readonly root?: string;
59
+ /** Model-facing byte ceiling. Defaults to `MODEL_MAX_BYTES`. */
60
+ readonly maxBytes?: number;
61
+ /**
62
+ * How the marker spells the spill path. `"root-relative"` (the default)
63
+ * names it from the directory the session's tools and shell start in
64
+ * (`.nax/scratchpad/spill/...`), so the model can open it verbatim;
65
+ * `"absolute"` names the written file's absolute path, for a caller whose
66
+ * root is not that directory (the native transcript fallback).
67
+ */
68
+ readonly spillPathStyle?: "root-relative" | "absolute";
69
+ }
70
+ /**
71
+ * Apply the model-facing truncation policy to one tool result.
72
+ *
73
+ * Returns the body unchanged when it is within every cap — no spill, no marker.
74
+ * Otherwise the shared policy shapes the content, the untruncated body is
75
+ * spilled when a root is known, and the marker naming both byte counts and the
76
+ * spill path is composed inside the same ceilings the body obeys.
77
+ *
78
+ * The byte ceiling is the caller's `maxBytes`, defaulting to `MODEL_MAX_BYTES`;
79
+ * the line ceilings always come from the shared policy's constants.
80
+ */
81
+ export declare function applyModelTruncationPolicy(body: string, opts: ModelTruncationOptions): Promise<string>;