@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,154 @@
1
+ /**
2
+ * Delete a file whose path the policy already resolved and approved.
3
+ *
4
+ * nax#1972: this used to be tracked-only, on the theory that a tracked
5
+ * file's content is in git history (an undo away) while an untracked file
6
+ * exists only on disk and is therefore unrecoverable. That rationale is
7
+ * retired -- it made the guard unsatisfiable for a file the agent itself
8
+ * wrote this session, which is untracked by construction. Paired with the
9
+ * #1937 denial redirect (Exec `rm` bounces back to Delete), the agent had no
10
+ * legal way to remove its own scratch output, and its fallback was to blank
11
+ * files to 0 bytes with Write -- which the auto-commit sweep then carried
12
+ * into history anyway. Refusing the deletion did not make anything more
13
+ * recoverable; it just moved the damage to a different tool.
14
+ *
15
+ * The new rule follows the three states git actually puts a path in:
16
+ *
17
+ * - tracked -> allow (unchanged)
18
+ * - untracked AND gitignored -> refuse (unchanged verdict, new reason)
19
+ * - untracked and NOT gitignored -> allow (the fix)
20
+ *
21
+ * Both untracked classes are equally unrecoverable -- neither is in git
22
+ * history -- so recoverability alone no longer explains the split. What
23
+ * distinguishes them is declared intent. A gitignored path has a rule
24
+ * somebody wrote on purpose, saying "this lives only on this machine"
25
+ * (`.env`, `.nax-pids`, `.claude/settings.local.json`, the fragments and
26
+ * plan-log directories under `.nax` are real examples in this repo) --
27
+ * deleting it is
28
+ * unrecoverable AND was never meant to be sent anywhere else, so the refusal
29
+ * stays, but the message now says why instead of telling the agent to commit
30
+ * a file its own author chose to keep out of git. An untracked-and-not-
31
+ * ignored path has no such rule attached to it at all: nobody has said
32
+ * anything about it one way or the other. In practice that is agent scratch
33
+ * or work in progress, so it is allowed.
34
+ *
35
+ * Accepted cost: a genuine new source file the agent wrote and has not yet
36
+ * committed is untracked-and-not-ignored too, so this permission also makes
37
+ * IT deletable -- there is no signal in git alone that separates "my own
38
+ * throwaway probe" from "a real file I just haven't committed yet". That is
39
+ * deliberate, not an oversight: it is the same permission that lets an agent
40
+ * clean up after itself, and the completion-phase auto-commit sweep
41
+ * (`autoCommitIfDirty`, `src/utils/git.ts`) is the backstop if something
42
+ * real gets removed before it was staged.
43
+ *
44
+ * `.git/` never reaches this point at all -- `resolveWithin`
45
+ * (`src/tools/policy.ts`) excludes it for every path-bearing tool, so
46
+ * `ctx.resolvedPaths` cannot contain a `.git/` path in the first place
47
+ * (nax#1943). Re-checking it here would be exactly the per-tool
48
+ * re-remembering that issue exists to prevent.
49
+ *
50
+ * Not `git rm`: gitTool documents a deliberate read/write split, and `git rm`
51
+ * both deletes and stages, folding two capabilities into one call. Plain
52
+ * unlink plus the existing GitCommit keeps each seam doing one thing --
53
+ * `git add -- <deleted path>` stages a deletion, so no change to GitCommit is
54
+ * needed.
55
+ */
56
+ import { lstat, unlink } from "node:fs/promises";
57
+ import { relative, resolve, sep } from "node:path";
58
+ import { gitWithTimeout } from "#src/internal/git-exec";
59
+ import { matchesDenyPaths } from "./deny-paths.js";
60
+ const GIT_TIMEOUT_MS = 30_000;
61
+ export const deleteTool = {
62
+ name: "Delete",
63
+ description: "Delete a file. A tracked file is always removable. An untracked file is removable unless it is gitignored -- a gitignored path was deliberately kept out of git, so removing it is unrecoverable and is refused. Directories are refused. The removal still has to be staged -- pass the same path to GitCommit afterwards.",
64
+ inputSchema: {
65
+ type: "object",
66
+ properties: {
67
+ path: { type: "string", description: "Path relative to the repository root" },
68
+ },
69
+ required: ["path"],
70
+ },
71
+ scope: { pathFields: ["path"] },
72
+ async run(input, ctx) {
73
+ if (ctx.resolvedPaths[0] === undefined)
74
+ return { content: "no path supplied", isError: true };
75
+ const shown = String(input.path);
76
+ // The policy uses a realpath to prove containment, which dereferences a
77
+ // symlink. Deletion must instead act on the requested directory entry: a
78
+ // tracked symlink is itself recoverable, while its target may be a
79
+ // different tracked file. The policy has already validated `shown`.
80
+ const target = resolve(ctx.root, shown);
81
+ // denyPaths (nax#1972) is a repo-authored narrowing of Delete, and is
82
+ // checked before anything else this function decides -- including
83
+ // existence -- because it is a blanket refusal on the path itself, not a
84
+ // judgment about the path's current state on disk.
85
+ //
86
+ // Matched against the CANONICAL entry, never the raw `input.path`. The
87
+ // policy resolves before it approves, so a denylist consulted on the
88
+ // caller's own spelling is a different parser than the one that granted
89
+ // the call -- and every alternate spelling of the same file ("./x",
90
+ // "x//y", "a/../x", an absolute path) walks straight past it. Deriving
91
+ // the relative form from `target` collapses all of them to one string.
92
+ // Deliberately NOT realpath: a symlink's entry is what Delete removes,
93
+ // and removing a link cannot destroy what it points at, so resolving
94
+ // through it would refuse deletions the denylist never meant to cover.
95
+ const denyCandidate = relative(ctx.root, target).split(sep).join("/");
96
+ if (matchesDenyPaths(denyCandidate, ctx.denyPaths)) {
97
+ return {
98
+ content: `"${shown}" matches this repository's denyPaths configuration, so Delete refuses it regardless of tracked status.`,
99
+ isError: true,
100
+ };
101
+ }
102
+ // Existence and directory come first so each refusal says the true thing.
103
+ // Tracked-first would report a mistyped path as "not tracked", which is
104
+ // technically true and diagnostically useless.
105
+ let isDirectory;
106
+ try {
107
+ isDirectory = (await lstat(target)).isDirectory();
108
+ }
109
+ catch {
110
+ return { content: `"${shown}" does not exist`, isError: true };
111
+ }
112
+ if (isDirectory) {
113
+ // Reachable, not defensive: `ls-files --error-unmatch` exits 0 for a
114
+ // directory holding tracked files, so "tracked" does not imply "file".
115
+ return { content: `"${shown}" is a directory; Delete removes one file at a time`, isError: true };
116
+ }
117
+ const tracked = await gitWithTimeout(["ls-files", "--error-unmatch", "--", target], ctx.root, GIT_TIMEOUT_MS);
118
+ if (tracked.exitCode !== 0) {
119
+ // Untracked. Whether it stays refused now turns on the SECOND git
120
+ // question -- gitignored or not -- rather than stopping here the way
121
+ // the tracked-only rule used to.
122
+ //
123
+ // `check-ignore` has three outcomes and they must NOT be collapsed into
124
+ // two: 0 means ignored, 1 means definitively not ignored, and anything
125
+ // else (128) means git could not answer -- no repository, or a broken
126
+ // one. Only the definitive 1 is an allow. Folding 128 in with 1 would
127
+ // make a repository-less directory the most permissive environment this
128
+ // tool has, which inverts the guard exactly where it is least
129
+ // observable. The pre-nax#1972 code refused "untracked" and "git
130
+ // failed" through one branch on purpose, and that caution survives the
131
+ // three-class rewrite even though its original rationale does not.
132
+ const ignored = await gitWithTimeout(["check-ignore", "-q", "--", target], ctx.root, GIT_TIMEOUT_MS);
133
+ if (ignored.exitCode === 0) {
134
+ return {
135
+ content: `"${shown}" is gitignored, so it exists only on this machine and is not recoverable once deleted. Delete refuses it for that reason.`,
136
+ isError: true,
137
+ };
138
+ }
139
+ if (ignored.exitCode !== 1) {
140
+ return {
141
+ content: `"${shown}" is not tracked, and git could not determine whether it is ignored, so Delete cannot establish that removing it is safe. Check that this is a git repository.`,
142
+ isError: true,
143
+ };
144
+ }
145
+ }
146
+ try {
147
+ await unlink(target);
148
+ return { content: `deleted ${shown} -- stage the removal by passing the same path to GitCommit` };
149
+ }
150
+ catch (err) {
151
+ return { content: err instanceof Error ? err.message : String(err), isError: true };
152
+ }
153
+ },
154
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Name the tool that serves the intent behind a denied call.
3
+ *
4
+ * `policy.ts` already names the granted argv FORMS, which is the right answer
5
+ * when the model wanted a different install. It is the wrong answer when the
6
+ * model wanted `git status`: across three runs, 8 of 32 denied argv calls were
7
+ * for capabilities the session already held as a first-class tool (nax#1937).
8
+ *
9
+ * Lives here rather than in policy.ts because the policy knows the grants but
10
+ * not which tools were ADVERTISED to this session. Naming a tool the session
11
+ * never received would reproduce the defect this exists to fix, so every row
12
+ * is gated on the caller's `available` set.
13
+ *
14
+ * ONE table answers both entry points. There used to be two -- one for argv
15
+ * command lines, one for bare verbs -- covering disjoint command sets, while
16
+ * `redirectForVerb` routed anything multi-token into the argv table. So which
17
+ * table answered depended on whether the model typed a flag, and coverage came
18
+ * out inverted: `ls -la` redirected but `ls` did not, `grep` redirected but
19
+ * `grep -n x y` did not. 20 of 34 denials in the two runs after #1971 carried
20
+ * no redirect (nax#1999). A single table cannot drift against itself.
21
+ */
22
+ export declare function redirectForArgv(argv: readonly string[], available: ReadonlySet<string>, declaredCommands: ReadonlySet<string>): string | undefined;
23
+ /**
24
+ * Name the tool that serves the intent behind a denied Bash COMMAND.
25
+ *
26
+ * Reads the first segment only: that is what the model reached for, and a
27
+ * later segment's intent is not what to teach when the call is refused. The
28
+ * split is deliberately its own crude one rather than the lexer's
29
+ * (`lexBashCommand`): a command REFUSED for an unanalysable construct must
30
+ * still get a redirect, and the lexer returns nothing to read in that case.
31
+ */
32
+ export declare function redirectForCommand(command: string, available: ReadonlySet<string>, declaredCommands: ReadonlySet<string>): string | undefined;
33
+ /**
34
+ * Name the tool that serves the intent behind a denied VERB call.
35
+ *
36
+ * `redirectForArgv` is unreachable for RunCommand and Git: they deny through
37
+ * `verbField`, where the policy sees no argv at all, so every such denial was a
38
+ * bare refusal (nax#1971). A verb slot carries either a mini command line the
39
+ * model stuffed there ("ls -la") or a bare word ("grep"); both are tokenized
40
+ * and answered by the same table, which is what keeps them consistent.
41
+ */
42
+ export declare function redirectForVerb(deniedTool: string, verb: string, available: ReadonlySet<string>, declaredCommands: ReadonlySet<string>): string | undefined;
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Name the tool that serves the intent behind a denied call.
3
+ *
4
+ * `policy.ts` already names the granted argv FORMS, which is the right answer
5
+ * when the model wanted a different install. It is the wrong answer when the
6
+ * model wanted `git status`: across three runs, 8 of 32 denied argv calls were
7
+ * for capabilities the session already held as a first-class tool (nax#1937).
8
+ *
9
+ * Lives here rather than in policy.ts because the policy knows the grants but
10
+ * not which tools were ADVERTISED to this session. Naming a tool the session
11
+ * never received would reproduce the defect this exists to fix, so every row
12
+ * is gated on the caller's `available` set.
13
+ *
14
+ * ONE table answers both entry points. There used to be two -- one for argv
15
+ * command lines, one for bare verbs -- covering disjoint command sets, while
16
+ * `redirectForVerb` routed anything multi-token into the argv table. So which
17
+ * table answered depended on whether the model typed a flag, and coverage came
18
+ * out inverted: `ls -la` redirected but `ls` did not, `grep` redirected but
19
+ * `grep -n x y` did not. 20 of 34 denials in the two runs after #1971 carried
20
+ * no redirect (nax#1999). A single table cannot drift against itself.
21
+ */
22
+ const GIT_READ_VERBS = new Set(["diff", "log", "show", "status", "blame"]);
23
+ /**
24
+ * Git verbs that stage or record a commit. Deliberately NOT every write verb:
25
+ * `git restore` reverts, which `GitCommit` does not do, and naming it would be
26
+ * the same defect as naming a tool the session never received.
27
+ */
28
+ const GIT_COMMIT_VERBS = new Set(["add", "commit"]);
29
+ /**
30
+ * Git verbs that revert tracked paths. Checked separately from the intent
31
+ * table: when the paths are nax's own `.nax/` run state, no tool serves the
32
+ * intent, so the refusal teaches instead of naming one (nax#2007).
33
+ */
34
+ const GIT_REVERT_VERBS = new Set(["checkout", "restore"]);
35
+ const NAX_OWNED_RUN_STATE_EXPLANATION = "`.nax/` is nax's own run state, written by the harness during this run. It is not part of your diff and must not be reverted.";
36
+ /**
37
+ * The sentence explaining a revert of nax's own run state, or undefined.
38
+ *
39
+ * Deliberately NOT an `Intent`: `render()` wraps every hit as "this session
40
+ * already has `X`", which is false here -- no tool reverts `.nax/`, and the
41
+ * module's header forbids naming one it does not hold. So this predicate is
42
+ * checked BEFORE `intentFor` in both entry points and returns its own sentence.
43
+ *
44
+ * Verb scope is `checkout` and `restore` only; `git stash` is the agent's own
45
+ * WIP, not nax's state, and has no operands to gate on. The gate is a `.nax`
46
+ * path SEGMENT (split on `/`), never a substring, so `src/nax-helpers.ts` and
47
+ * `docs/.naxignore` stay unanswered. `--` is a separator, not an operand, and a
48
+ * `timeout N` wrapper is stripped the same way `intentFor` strips it.
49
+ */
50
+ function naxOwnedRunStateExplanation(tokens) {
51
+ const av = withoutTimeoutPrefix(tokens);
52
+ const hasGitPrefix = av[0] === "git";
53
+ const verb = hasGitPrefix ? av[1] : av[0];
54
+ if (verb === undefined || !GIT_REVERT_VERBS.has(verb))
55
+ return undefined;
56
+ for (const operand of av.slice(hasGitPrefix ? 2 : 1)) {
57
+ if (operand === "--")
58
+ continue;
59
+ if (operand.split("/").includes(".nax"))
60
+ return NAX_OWNED_RUN_STATE_EXPLANATION;
61
+ }
62
+ return undefined;
63
+ }
64
+ const GLOB = { tool: "Glob", how: "Glob lists repository paths by pattern" };
65
+ const GREP = { tool: "Grep", how: "Grep searches file contents" };
66
+ const READ = { tool: "Read", how: "Read returns file contents, by line range with offset/limit" };
67
+ const DELETE = { tool: "Delete", how: "Delete removes one tracked file at a time" };
68
+ const GIT = { tool: "Git", how: `Git runs read-only git (${[...GIT_READ_VERBS].join(", ")})` };
69
+ const GIT_COMMIT = {
70
+ tool: "GitCommit",
71
+ how: "GitCommit stages and commits named files -- supply `message` and a `paths` array",
72
+ };
73
+ const TEST_SCOPED = {
74
+ tool: "RunCommand:testScoped",
75
+ how: 'RunCommand {"command":"testScoped"} runs the project test command on named files',
76
+ };
77
+ const BASH = {
78
+ tool: "Bash",
79
+ how: "Bash runs one shell command string, when a Bash(...) allow rule covers every segment of it",
80
+ };
81
+ /**
82
+ * Commands whose intent the HEAD token alone decides.
83
+ *
84
+ * `rm` and `git` are absent on purpose: their intent is decided by the SECOND
85
+ * token (`rm a.ts` is a Delete, `rm -r dir` is nothing Delete can do; `git log`
86
+ * is Git, `git add` is GitCommit), so they are resolved by shape below.
87
+ */
88
+ const HEAD_INTENTS = new Map([
89
+ ["ls", GLOB],
90
+ ["find", GLOB],
91
+ ["grep", GREP],
92
+ ["cat", READ],
93
+ ["bash", BASH],
94
+ ["sh", BASH],
95
+ ["zsh", BASH],
96
+ // No tool counts lines, and Read with no offset/limit returns a byte-bounded
97
+ // PREFIX -- so counting what it returns under-reports exactly the large files
98
+ // the question gets asked about. Naming Read beats a bare refusal the model
99
+ // retries verbatim, but only if the ceiling is stated rather than implied.
100
+ ["wc", { tool: "Read", how: "Read returns file contents, truncated past a size ceiling -- no tool counts lines" }],
101
+ // nax-git-env-allow: a [command, redirect] table entry, not an argv
102
+ ["git", GIT],
103
+ ...[...GIT_READ_VERBS].map((verb) => [verb, GIT]),
104
+ // `git.ts` declares `allowedVerbs: GIT_READ_VERBS`, so every Git verb-slot
105
+ // denial carries a write verb and arrives BARE -- `commit`, never
106
+ // `git commit`. Reaching these only through the shape branch below would
107
+ // leave the row unreachable from the one slot Git denials come through.
108
+ // Safe as head tokens: `add` is an install verb only in TASK_RUNNERS
109
+ // position (`bun add`), where the head is the runner, not `add`.
110
+ ...[...GIT_COMMIT_VERBS].map((verb) => [verb, GIT_COMMIT]),
111
+ ]);
112
+ /** Task-runner binaries. A detection heuristic ONLY: what gets named comes
113
+ * entirely from the project's declared commands, never from this list. A runner
114
+ * missing here degrades to the pre-#1971 message rather than to a wrong one.
115
+ */
116
+ const TASK_RUNNERS = new Set([
117
+ "bun",
118
+ "npm",
119
+ "pnpm",
120
+ "yarn",
121
+ "deno",
122
+ "npx",
123
+ "make",
124
+ "just",
125
+ "task",
126
+ "go",
127
+ "cargo",
128
+ "uv",
129
+ "poetry",
130
+ "pipenv",
131
+ "tox",
132
+ "gradle",
133
+ "mvn",
134
+ ]);
135
+ /**
136
+ * Install subcommands. These runners double as package managers, and an install
137
+ * attempt wants the granted install FORMS policy.ts already printed -- not a
138
+ * list of project gates that cannot install anything.
139
+ */
140
+ const INSTALL_VERBS = new Set(["add", "install", "i", "ci", "get", "sync", "fetch", "mod", "download"]);
141
+ function isSingleFileDelete(tokens, from) {
142
+ return tokens.length === from + 1 && !tokens[from]?.startsWith("-");
143
+ }
144
+ /** A `timeout N ...` prefix wraps the real command; look past it. */
145
+ function withoutTimeoutPrefix(tokens) {
146
+ return tokens[0] === "timeout" ? tokens.slice(2) : tokens;
147
+ }
148
+ /**
149
+ * The one intent table. Shape-dependent heads are resolved first, then the
150
+ * head-only map -- so a bare verb and the same verb with arguments can never
151
+ * be answered differently.
152
+ */
153
+ function intentFor(tokens) {
154
+ const av = withoutTimeoutPrefix(tokens);
155
+ const [head, second] = av;
156
+ if (head === undefined)
157
+ return undefined;
158
+ if (head === "rm")
159
+ return isSingleFileDelete(av, 1) ? DELETE : undefined;
160
+ if (head === "git" && second !== undefined) {
161
+ if (second === "rm")
162
+ return isSingleFileDelete(av, 2) ? DELETE : undefined;
163
+ if (GIT_READ_VERBS.has(second))
164
+ return GIT;
165
+ if (GIT_COMMIT_VERBS.has(second))
166
+ return GIT_COMMIT;
167
+ return undefined;
168
+ }
169
+ if (head === "bun" && second === "test")
170
+ return TEST_SCOPED;
171
+ return HEAD_INTENTS.get(head);
172
+ }
173
+ /**
174
+ * Name the gates this project declared, for a model that reached for a task
175
+ * runner and wanted to run one of them. What gets named comes from the
176
+ * project's declared commands, never from a hardcoded list (nax#1971).
177
+ */
178
+ function taskRunnerFallback(tokens, available, declaredCommands) {
179
+ const av = withoutTimeoutPrefix(tokens);
180
+ const head = av[0];
181
+ if (head === undefined || !TASK_RUNNERS.has(head))
182
+ return undefined;
183
+ const sub = av[1];
184
+ if (sub !== undefined && INSTALL_VERBS.has(sub))
185
+ return undefined;
186
+ if (!available.has("RunCommand") || declaredCommands.size === 0)
187
+ return undefined;
188
+ return `this session already has RunCommand with declared commands: ${[...declaredCommands].join(", ")}`;
189
+ }
190
+ function render(hit, available, declaredCommands) {
191
+ if (hit.tool === TEST_SCOPED.tool) {
192
+ // Conditioned on the project actually declaring the command, not hardcoded:
193
+ // naming a command this project never declared is the same defect again.
194
+ if (!available.has("RunCommand") || !declaredCommands.has("testScoped"))
195
+ return undefined;
196
+ return `this session already has ${hit.how}`;
197
+ }
198
+ if (!available.has(hit.tool))
199
+ return undefined;
200
+ return `this session already has \`${hit.tool}\` -- ${hit.how}`;
201
+ }
202
+ export function redirectForArgv(argv, available, declaredCommands) {
203
+ const ownedRunState = naxOwnedRunStateExplanation(argv);
204
+ if (ownedRunState !== undefined)
205
+ return ownedRunState;
206
+ const hit = intentFor(argv);
207
+ if (hit === undefined)
208
+ return taskRunnerFallback(argv, available, declaredCommands);
209
+ return render(hit, available, declaredCommands);
210
+ }
211
+ /**
212
+ * Name the tool that serves the intent behind a denied Bash COMMAND.
213
+ *
214
+ * Reads the first segment only: that is what the model reached for, and a
215
+ * later segment's intent is not what to teach when the call is refused. The
216
+ * split is deliberately its own crude one rather than the lexer's
217
+ * (`lexBashCommand`): a command REFUSED for an unanalysable construct must
218
+ * still get a redirect, and the lexer returns nothing to read in that case.
219
+ */
220
+ export function redirectForCommand(command, available, declaredCommands) {
221
+ const [first = ""] = command.split(/&&|\|\||;|\||\n/);
222
+ const tokens = first
223
+ .trim()
224
+ .split(/\s+/)
225
+ .filter((token) => token.length > 0);
226
+ if (tokens.length === 0)
227
+ return undefined;
228
+ const ownedRunState = naxOwnedRunStateExplanation(tokens);
229
+ if (ownedRunState !== undefined)
230
+ return ownedRunState;
231
+ const hit = intentFor(tokens);
232
+ if (hit === undefined)
233
+ return taskRunnerFallback(tokens, available, declaredCommands);
234
+ // Telling Bash it already has Bash reads as a contradiction of the denial —
235
+ // the same suppression redirectForVerb applies.
236
+ if (hit.tool === "Bash")
237
+ return undefined;
238
+ return render(hit, available, declaredCommands);
239
+ }
240
+ /**
241
+ * Name the tool that serves the intent behind a denied VERB call.
242
+ *
243
+ * `redirectForArgv` is unreachable for RunCommand and Git: they deny through
244
+ * `verbField`, where the policy sees no argv at all, so every such denial was a
245
+ * bare refusal (nax#1971). A verb slot carries either a mini command line the
246
+ * model stuffed there ("ls -la") or a bare word ("grep"); both are tokenized
247
+ * and answered by the same table, which is what keeps them consistent.
248
+ */
249
+ export function redirectForVerb(deniedTool, verb, available, declaredCommands) {
250
+ const tokens = verb
251
+ .trim()
252
+ .split(/\s+/)
253
+ .filter((token) => token.length > 0);
254
+ if (tokens.length === 0)
255
+ return undefined;
256
+ const ownedRunState = naxOwnedRunStateExplanation(tokens);
257
+ if (ownedRunState !== undefined)
258
+ return ownedRunState;
259
+ const hit = intentFor(tokens);
260
+ if (hit === undefined)
261
+ return taskRunnerFallback(tokens, available, declaredCommands);
262
+ // Telling Git it already has Git reads as a contradiction of the denial.
263
+ if (hit.tool === deniedTool)
264
+ return undefined;
265
+ return render(hit, available, declaredCommands);
266
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Match a Delete candidate against the repo-configurable `denyPaths`
3
+ * denylist (nax#1972, `execution.denyPaths` in config).
4
+ *
5
+ * A separate file rather than an addition to src/tools/policy.ts on purpose:
6
+ * that file already carries the containment seam and is close to the
7
+ * project's file-size ratchet, and denyPaths is a narrower, single-tool
8
+ * concern -- it has nothing to do with resolving or containing a path, only
9
+ * with refusing one the policy already approved.
10
+ *
11
+ * The glob syntax deliberately matches globToRegExp in policy.ts (`**` spans
12
+ * separators, `*` does not) so a `denyPaths` entry reads the same way a
13
+ * ToolGrant pattern does elsewhere in config -- one mental model for "what
14
+ * does a glob mean in nax config", not two subtly different ones. The
15
+ * implementation is a small, independent copy rather than an export from
16
+ * policy.ts: that keeps this file free to be read (and reasoned about) on
17
+ * its own, and keeps policy.ts's containment logic untouched by a change
18
+ * that has nothing to do with containment.
19
+ */
20
+ /**
21
+ * Does `relativePath` match any pattern in `denyPaths`?
22
+ *
23
+ * `relativePath` MUST already be canonical and repo-relative -- the caller
24
+ * derives it from the resolved entry, never from `input.path`. This is not a
25
+ * stylistic preference: matching the caller's own spelling means the denylist
26
+ * and the policy that approved the call are reading different strings, and
27
+ * every alternate spelling of one file ("./x", "x//y", "a/../x") walks past
28
+ * the denylist (nax#1972, reproduced). Repo-relative rather than absolute so
29
+ * a pattern is rooted at the repo, not at the filesystem.
30
+ *
31
+ * Both sides are NFC-normalized before comparison. macOS stores filenames
32
+ * decomposed (NFD), so a pattern typed in a config file and the path that
33
+ * comes back off disk can be byte-different while naming one file.
34
+ */
35
+ export declare function matchesDenyPaths(relativePath: string, denyPaths: readonly string[] | undefined): boolean;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Match a Delete candidate against the repo-configurable `denyPaths`
3
+ * denylist (nax#1972, `execution.denyPaths` in config).
4
+ *
5
+ * A separate file rather than an addition to src/tools/policy.ts on purpose:
6
+ * that file already carries the containment seam and is close to the
7
+ * project's file-size ratchet, and denyPaths is a narrower, single-tool
8
+ * concern -- it has nothing to do with resolving or containing a path, only
9
+ * with refusing one the policy already approved.
10
+ *
11
+ * The glob syntax deliberately matches globToRegExp in policy.ts (`**` spans
12
+ * separators, `*` does not) so a `denyPaths` entry reads the same way a
13
+ * ToolGrant pattern does elsewhere in config -- one mental model for "what
14
+ * does a glob mean in nax config", not two subtly different ones. The
15
+ * implementation is a small, independent copy rather than an export from
16
+ * policy.ts: that keeps this file free to be read (and reasoned about) on
17
+ * its own, and keeps policy.ts's containment logic untouched by a change
18
+ * that has nothing to do with containment.
19
+ */
20
+ function globToRegExp(pattern) {
21
+ let out = "";
22
+ for (let i = 0; i < pattern.length; i += 1) {
23
+ const char = pattern[i];
24
+ if (char === "*") {
25
+ if (pattern[i + 1] === "*") {
26
+ i += 1;
27
+ if (pattern[i + 1] === "/") {
28
+ out += "(?:.*/)?";
29
+ i += 1;
30
+ }
31
+ else {
32
+ out += ".*";
33
+ }
34
+ }
35
+ else {
36
+ out += "[^/]*";
37
+ }
38
+ continue;
39
+ }
40
+ if (char === "?") {
41
+ out += "[^/]";
42
+ continue;
43
+ }
44
+ out += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
45
+ }
46
+ // Case-insensitive, and both sides NFC-normalized at the call site.
47
+ // macOS and Windows default to case-insensitive filesystems, where ".ENV"
48
+ // and ".env" are one file -- a case-sensitive compare there lets a denied
49
+ // path be deleted by respelling it (nax#1972, reproduced). Applying the
50
+ // flag unconditionally rather than probing the filesystem keeps the rule
51
+ // one predictable thing: on a case-sensitive filesystem this over-refuses
52
+ // a genuinely distinct ".ENV", which is the direction a denylist should
53
+ // fail in.
54
+ return new RegExp(`^${out}$`, "i");
55
+ }
56
+ /**
57
+ * Does `relativePath` match any pattern in `denyPaths`?
58
+ *
59
+ * `relativePath` MUST already be canonical and repo-relative -- the caller
60
+ * derives it from the resolved entry, never from `input.path`. This is not a
61
+ * stylistic preference: matching the caller's own spelling means the denylist
62
+ * and the policy that approved the call are reading different strings, and
63
+ * every alternate spelling of one file ("./x", "x//y", "a/../x") walks past
64
+ * the denylist (nax#1972, reproduced). Repo-relative rather than absolute so
65
+ * a pattern is rooted at the repo, not at the filesystem.
66
+ *
67
+ * Both sides are NFC-normalized before comparison. macOS stores filenames
68
+ * decomposed (NFD), so a pattern typed in a config file and the path that
69
+ * comes back off disk can be byte-different while naming one file.
70
+ */
71
+ export function matchesDenyPaths(relativePath, denyPaths) {
72
+ if (denyPaths === undefined || denyPaths.length === 0)
73
+ return false;
74
+ const candidate = relativePath.normalize("NFC");
75
+ return denyPaths.some((pattern) => globToRegExp(pattern.normalize("NFC")).test(candidate));
76
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Edit's literal replacement (US-001).
3
+ *
4
+ * `String.prototype.replace` reads a string replacement as a template and
5
+ * expands `$$`, `$&`, `` $` `` and `$'` inside it, so
6
+ * `source.replace(oldString, newString)` writes something other than the
7
+ * characters the model supplied whenever `new_string` contains one of those
8
+ * patterns — `"a".replace("a", "x$$y$&z")` returns `x$yaz`. Composing the
9
+ * result by hand keeps every character literal.
10
+ *
11
+ * File-local on purpose, matching `src/tools/read-continuation.ts`: the
12
+ * helper is a property of the `Edit` composition, nothing else composes it,
13
+ * and `src/tools` does not export it from its barrel — `edit.ts` imports it
14
+ * by relative path.
15
+ *
16
+ * US-002 extends the module with `composeEditRegion`, the bounded
17
+ * Read-compatible view of the region an Edit just wrote: the `[lines a-b of
18
+ * N]` header and the selected lines, with elision when the replacement spans
19
+ * more than `MAX_REGION_LINES_SHOWN` lines. Deterministic string composition
20
+ * — no LLM, no config, no new tool arguments.
21
+ */
22
+ /**
23
+ * Inputs to `composeEditRegion`.
24
+ *
25
+ * `matchIndex` is the offset `source.indexOf(oldString)` found — the caller
26
+ * wrote the new text there, so `updated.slice(0, matchIndex)` is identical to
27
+ * the source prefix and the start line can be counted on either.
28
+ */
29
+ export interface EditRegionInput {
30
+ readonly updated: string;
31
+ readonly matchIndex: number;
32
+ readonly newStringLength: number;
33
+ }
34
+ /**
35
+ * The bounded, Read-compatible view of the region an Edit wrote: a
36
+ * `[lines a-b of N]` header and the selected lines, or `[file is now empty]`
37
+ * for an emptied file. Has no trailing newline, and cannot throw on any
38
+ * string input.
39
+ */
40
+ export declare function composeEditRegion({ updated, matchIndex, newStringLength }: EditRegionInput): string;
41
+ /**
42
+ * Replace the single occurrence of `oldString` at `matchIndex` with
43
+ * `newString`, literally.
44
+ *
45
+ * The caller has already established uniqueness (`countOccurrences` in
46
+ * `edit.ts`) and passes the index `source.indexOf(oldString)` found, so this
47
+ * is pure slicing: everything before the match, the new text verbatim, and
48
+ * everything after it. No `replace`, and therefore no substitution patterns.
49
+ */
50
+ export declare function replaceUniqueLiteral(source: string, oldString: string, newString: string, matchIndex: number): string;