@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,212 @@
1
+ /**
2
+ * Shared single-file lock used by `withPathFileLock` and `withQueueFileLock`.
3
+ *
4
+ * Mutual exclusion derives from the atomicity of an exclusive create: the
5
+ * lock is ONE file at `<target>.lock`, and the writer whose
6
+ * `writeFile(path, pid, { flag: "wx" })` succeeds holds the lock. There is
7
+ * no candidate ordering, no filesystem birthtime, and no polling race.
8
+ *
9
+ * #1731: the previous design created timestamped candidate files and polled
10
+ * until it saw itself as the "oldest live" candidate. That protocol is only
11
+ * sound if candidate order matches creation order, which it did not: the
12
+ * sort key was `(stat().birthtimeMs, name.localeCompare)` — on Linux CI
13
+ * filesystems birthtimeMs is degenerate so order collapsed to the random
14
+ * uuid tail of the name, and a poll whose readdir completed before a
15
+ * competitor's create landed missed that competitor entirely. A
16
+ * later-created candidate could therefore sort before the holder, also
17
+ * conclude it was oldest, and enter — two writers inside the critical
18
+ * section, one write silently lost. An exclusive create cannot exhibit any
19
+ * of those failure modes: exactly one create ever succeeds.
20
+ *
21
+ * Staleness (BUG-10 / BUG-25, carried over from the candidate design):
22
+ * - The holder's pid is stored in the lock file. A waiter that loses the
23
+ * create race reads the pid; a PROVEN-dead holder (ESRCH) means the lock
24
+ * is abandoned and may be reclaimed.
25
+ * - Content that cannot be parsed (e.g. the creator crashed between the
26
+ * create and the pid write) is respected until it ages past
27
+ * `EMPTY_LOCK_EVICT_AGE_MS` — a live holder writes its pid within the
28
+ * same tick as the create, so old unparseable content proves abandonment.
29
+ * Inconclusive liveness fails closed: the waiter times out and throws
30
+ * rather than entering over a possible holder.
31
+ *
32
+ * Reclamation is serialized by a claim file (`<target>.lock.claim`), whose
33
+ * exclusive create elects a single "gravedigger" at a time. This closes the
34
+ * steal race: without it, two waiters that both observed the dead holder
35
+ * could interleave (stealer A's create landing between stealer B's
36
+ * re-verification and its unlink), letting B unlink A's fresh live lock and
37
+ * enter over it. Under the claim:
38
+ * - Only the claim holder may unlink the lock file, so no unlink can make
39
+ * the path absent during another gravedigger's verify→unlink window.
40
+ * - The gravedigger RE-VERIFIES abandonment after winning the claim (a
41
+ * stale observation must not kill a lock that a new live holder has
42
+ * since created) and only then unlinks.
43
+ * - If two gravediggers ever do overlap (only possible via the claim's own
44
+ * age-based steal), both re-verify the same dead file, both unlink (the
45
+ * second gets ENOENT), and the exclusive create on the fixed lock path
46
+ * still admits exactly one holder.
47
+ * The claim itself is held for microseconds, so it is stolen purely on age
48
+ * (`CLAIM_MAX_AGE_MS`): a claim older than that proves its gravedigger died
49
+ * mid-reclaim. A crashed gravedigger otherwise pauses reclamation until
50
+ * then — fail closed, never fail into someone else's live lock.
51
+ *
52
+ * Release unlinks the lock file in the `finally` block. The unlink is
53
+ * swallowed because release runs from the same process that created the
54
+ * file — a double-cleanup on retry paths must not throw. A crashed holder
55
+ * leaks the file, which the dead-pid reclamation above reclaims.
56
+ *
57
+ * Mixed versions: the pre-#1731 design wrote `<target>.lock.<time>.<pid>.<uuid>`
58
+ * candidate files, which this design deliberately does NOT scan for — they
59
+ * are inert leftovers here (and legacy candidate files are never GC'd). An
60
+ * old-version process and a new-version process must therefore not run
61
+ * concurrently against the same target during a one-release transition
62
+ * window: each would see only its own lock format and could enter
63
+ * concurrently. This self-heals once old-version processes exit.
64
+ */
65
+ import { readFile, stat, unlink, writeFile } from "node:fs/promises";
66
+ import { NaxError } from "#src/infra/index";
67
+ import { isProcessAlive } from "./process-alive.js";
68
+ const DEFAULT_RETRY_MS = 10;
69
+ const DEFAULT_TIMEOUT_MS = 5_000;
70
+ /**
71
+ * A live holder writes its pid within the same tick as the create, so
72
+ * unparseable content older than this bound proves the creator died
73
+ * between creating the file and writing its pid (crash, kill -9).
74
+ */
75
+ const EMPTY_LOCK_EVICT_AGE_MS = 10_000;
76
+ /**
77
+ * A gravedigger holds the claim file for microseconds (verify + one
78
+ * unlink). A claim older than this bound proves its process died before
79
+ * releasing it. Kept comfortably above any scheduling delay a live
80
+ * reclaim could take.
81
+ */
82
+ const CLAIM_MAX_AGE_MS = 10_000;
83
+ export const _fileLockDeps = {
84
+ writeFile,
85
+ readFile,
86
+ stat,
87
+ unlink,
88
+ now: () => Date.now(),
89
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
90
+ isPidAlive: isProcessAlive,
91
+ };
92
+ function unlinkQuiet(path) {
93
+ return _fileLockDeps.unlink(path).catch(() => { });
94
+ }
95
+ async function tryCreateExclusive(path) {
96
+ try {
97
+ await _fileLockDeps.writeFile(path, `${process.pid}\n`, { flag: "wx" });
98
+ return true;
99
+ }
100
+ catch (error) {
101
+ if (error.code === "EEXIST")
102
+ return false;
103
+ // Any other create failure (EACCES, ENOSPC, …) is environmental, not
104
+ // contention — fail fast instead of burning the whole timeout as a
105
+ // misleading "Timed out".
106
+ throw error;
107
+ }
108
+ }
109
+ async function readHolderPid(path) {
110
+ // ENOENT/EACCES on read and empty/garbage content are deliberately
111
+ // conflated: both mean "no judgable holder", and both fall to the
112
+ // age-based reclamation bound (BUG-25 conservative rule).
113
+ const raw = await _fileLockDeps.readFile(path, "utf8").catch(() => null);
114
+ if (raw === null)
115
+ return null;
116
+ const pid = Number.parseInt(raw.trim(), 10);
117
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
118
+ }
119
+ async function ageMs(path) {
120
+ const stats = await _fileLockDeps.stat(path).catch(() => null);
121
+ return stats ? _fileLockDeps.now() - stats.mtimeMs : null;
122
+ }
123
+ /**
124
+ * Judge whether the lock at `path` is provably abandoned: its holder pid is
125
+ * dead, or its content is unparseable and older than the empty-lock bound.
126
+ * Returns the observed pid (null when unparseable) so the reclamation step
127
+ * can detect a changed file before unlinking.
128
+ */
129
+ async function judgeAbandoned(path) {
130
+ const pid = await readHolderPid(path);
131
+ if (pid !== null)
132
+ return { abandoned: !_fileLockDeps.isPidAlive(pid), observedPid: pid };
133
+ const age = await ageMs(path);
134
+ return { abandoned: age !== null && age >= EMPTY_LOCK_EVICT_AGE_MS, observedPid: null };
135
+ }
136
+ /**
137
+ * Elect one gravedigger via the claim file's exclusive create. A fresh
138
+ * claim held by someone else means reclamation is already in progress —
139
+ * back off. An aged claim proves its gravedigger died mid-reclaim and is
140
+ * stolen (age-based, so no content check is needed: a live gravedigger
141
+ * releases within microseconds).
142
+ */
143
+ async function tryClaim(claimPath) {
144
+ if (await tryCreateExclusive(claimPath))
145
+ return true;
146
+ const age = await ageMs(claimPath);
147
+ if (age === null || age < CLAIM_MAX_AGE_MS)
148
+ return false;
149
+ await unlinkQuiet(claimPath);
150
+ return tryCreateExclusive(claimPath);
151
+ }
152
+ /**
153
+ * Reclaim `path` if it is STILL the abandoned lock that was observed — the
154
+ * observation can be stale (a new live holder may have been created since,
155
+ * or another gravedigger may have already removed the file). Runs under
156
+ * the claim, so no concurrent unlink can make the path flip between absent
157
+ * and present during this verification.
158
+ */
159
+ async function reclaimIfStillAbandoned(path, observedPid) {
160
+ const pid = await readHolderPid(path);
161
+ if (pid !== null) {
162
+ if (pid === observedPid && !_fileLockDeps.isPidAlive(pid))
163
+ await unlinkQuiet(path);
164
+ return;
165
+ }
166
+ if (observedPid !== null)
167
+ return;
168
+ const age = await ageMs(path);
169
+ if (age !== null && age >= EMPTY_LOCK_EVICT_AGE_MS)
170
+ await unlinkQuiet(path);
171
+ }
172
+ async function acquire(lockPath, options) {
173
+ const retryMs = options.retryMs ?? DEFAULT_RETRY_MS;
174
+ const deadline = _fileLockDeps.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
175
+ const claimPath = `${lockPath}.claim`;
176
+ for (;;) {
177
+ if (await tryCreateExclusive(lockPath)) {
178
+ return () => unlinkQuiet(lockPath);
179
+ }
180
+ if (_fileLockDeps.now() >= deadline)
181
+ break;
182
+ const { abandoned, observedPid } = await judgeAbandoned(lockPath);
183
+ if (abandoned && (await tryClaim(claimPath))) {
184
+ try {
185
+ await reclaimIfStillAbandoned(lockPath, observedPid);
186
+ }
187
+ finally {
188
+ await unlinkQuiet(claimPath);
189
+ }
190
+ }
191
+ await _fileLockDeps.sleep(retryMs);
192
+ }
193
+ throw new NaxError(`${options.errorPrefix} Timed out acquiring ${options.lockName} lock: ${lockPath}`, "FILE_LOCK_TIMEOUT", {
194
+ stage: "file-lock",
195
+ lockName: options.lockName,
196
+ lockPath,
197
+ });
198
+ }
199
+ /**
200
+ * Serialize `operation` against any other process holding the lock at
201
+ * `lockPath`. See the module doc for the acquisition, staleness, and
202
+ * release semantics.
203
+ */
204
+ export async function withFileLock(lockPath, operation, options) {
205
+ const release = await acquire(lockPath, options);
206
+ try {
207
+ return await operation();
208
+ }
209
+ finally {
210
+ await release();
211
+ }
212
+ }
@@ -0,0 +1,2 @@
1
+ /** Preserve missing-file size probes without suppressing other I/O errors. */
2
+ export declare function fileSizeOrZero(path: string): number;
@@ -0,0 +1,12 @@
1
+ import { statSync } from "node:fs";
2
+ /** Preserve missing-file size probes without suppressing other I/O errors. */
3
+ export function fileSizeOrZero(path) {
4
+ try {
5
+ return statSync(path).size;
6
+ }
7
+ catch (error) {
8
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
9
+ return 0;
10
+ throw error;
11
+ }
12
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * `git add` that never makes git recurse into a gitlink (#2210).
3
+ *
4
+ * For every gitlink (submodule entry) in the index, `git add` runs
5
+ * `git status` inside the nested repository to learn whether it is dirty,
6
+ * and that child git reads the nested repo's own `.git/config` and
7
+ * `.gitattributes`. A nested repo the agent created inside the sandbox write
8
+ * root can name a filter driver there (`filter.<x>.clean`), which nax's
9
+ * unsandboxed `git add` would then run. `add` ignores `diff.ignoreSubmodules`
10
+ * (it overrides submodule config), so the env hardening in `./git-env` cannot
11
+ * reach it. Instead, gitlinks are excluded from the `add` pathspec and
12
+ * restaged with `git update-index`, which records the nested HEAD without
13
+ * spawning git inside it. The staged result is the same as a plain `git add`.
14
+ *
15
+ * `git commit` with nothing staged prints a status, and that status honours a
16
+ * `submodule.<name>.ignore` from `.gitmodules` -- so it can recurse too. The
17
+ * commit sites therefore ask `hasStagedChanges` first and never run a commit
18
+ * that would only report "nothing to commit".
19
+ */
20
+ export interface GitRunResult {
21
+ stdout: string;
22
+ stderr: string;
23
+ exitCode: number;
24
+ timedOut?: boolean;
25
+ }
26
+ /** `gitWithTimeout`'s shape; injected so this module does not import `./git` (which imports it). */
27
+ export type GitRunner = (args: string[], cwd: string, timeoutMs?: number) => Promise<GitRunResult>;
28
+ export interface GitlinkSafeAddOptions {
29
+ /** Flags placed before `--`, e.g. `["-A"]`. */
30
+ readonly flags?: readonly string[];
31
+ /** Pathspecs to stage; empty means the whole tree (`:/`), as a bare `git add -A` does. */
32
+ readonly pathspecs?: readonly string[];
33
+ readonly timeoutMs?: number;
34
+ }
35
+ /** Gitlink paths from `git ls-files --stage -z` output (paths as git printed them: relative to its cwd). */
36
+ export declare function parseGitlinks(lsFilesStageZ: string): string[];
37
+ export declare function gitlinkSafeAdd(git: GitRunner, cwd: string, opts: GitlinkSafeAddOptions): Promise<GitRunResult>;
38
+ /**
39
+ * Whether the index differs from HEAD: `diff --cached` compares index to HEAD
40
+ * only, so it never looks at the worktree or inside a gitlink. Undefined when
41
+ * git could not answer (fail closed: callers do not commit).
42
+ */
43
+ export declare function hasStagedChanges(git: GitRunner, cwd: string, timeoutMs?: number): Promise<boolean | undefined>;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * `git add` that never makes git recurse into a gitlink (#2210).
3
+ *
4
+ * For every gitlink (submodule entry) in the index, `git add` runs
5
+ * `git status` inside the nested repository to learn whether it is dirty,
6
+ * and that child git reads the nested repo's own `.git/config` and
7
+ * `.gitattributes`. A nested repo the agent created inside the sandbox write
8
+ * root can name a filter driver there (`filter.<x>.clean`), which nax's
9
+ * unsandboxed `git add` would then run. `add` ignores `diff.ignoreSubmodules`
10
+ * (it overrides submodule config), so the env hardening in `./git-env` cannot
11
+ * reach it. Instead, gitlinks are excluded from the `add` pathspec and
12
+ * restaged with `git update-index`, which records the nested HEAD without
13
+ * spawning git inside it. The staged result is the same as a plain `git add`.
14
+ *
15
+ * `git commit` with nothing staged prints a status, and that status honours a
16
+ * `submodule.<name>.ignore` from `.gitmodules` -- so it can recurse too. The
17
+ * commit sites therefore ask `hasStagedChanges` first and never run a commit
18
+ * that would only report "nothing to commit".
19
+ */
20
+ const GITLINK_MODE = "160000";
21
+ const WHOLE_TREE_PATHSPEC = ":/";
22
+ /** Gitlink paths from `git ls-files --stage -z` output (paths as git printed them: relative to its cwd). */
23
+ export function parseGitlinks(lsFilesStageZ) {
24
+ const paths = lsFilesStageZ.split("\0").flatMap((record) => {
25
+ const tab = record.indexOf("\t");
26
+ if (tab < 0 || !record.startsWith(`${GITLINK_MODE} `))
27
+ return [];
28
+ return [record.slice(tab + 1)];
29
+ });
30
+ // An unmerged gitlink has one entry per stage.
31
+ return [...new Set(paths)];
32
+ }
33
+ export async function gitlinkSafeAdd(git, cwd, opts) {
34
+ const pathspecs = opts.pathspecs !== undefined && opts.pathspecs.length > 0 ? opts.pathspecs : [WHOLE_TREE_PATHSPEC];
35
+ const listed = await git(["ls-files", "--stage", "-z", "--", ...pathspecs], cwd, opts.timeoutMs);
36
+ if (listed.exitCode !== 0 || listed.timedOut) {
37
+ // Fail closed: staging without knowing the gitlinks would recurse into them.
38
+ return { ...listed, exitCode: listed.exitCode === 0 ? 1 : listed.exitCode };
39
+ }
40
+ const gitlinks = parseGitlinks(listed.stdout);
41
+ const excludes = gitlinks.map((p) => `:(exclude,literal)${p}`);
42
+ const added = await git(["add", ...(opts.flags ?? []), "--", ...pathspecs, ...excludes], cwd, opts.timeoutMs);
43
+ if (added.exitCode !== 0 || gitlinks.length === 0)
44
+ return added;
45
+ return git(["update-index", "--add", "--remove", "--", ...gitlinks], cwd, opts.timeoutMs);
46
+ }
47
+ /**
48
+ * Whether the index differs from HEAD: `diff --cached` compares index to HEAD
49
+ * only, so it never looks at the worktree or inside a gitlink. Undefined when
50
+ * git could not answer (fail closed: callers do not commit).
51
+ */
52
+ export async function hasStagedChanges(git, cwd, timeoutMs) {
53
+ const r = await git(["diff", "--cached", "--quiet"], cwd, timeoutMs);
54
+ if (r.timedOut)
55
+ return undefined;
56
+ if (r.exitCode === 0)
57
+ return false;
58
+ return r.exitCode === 1 ? true : undefined;
59
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Config overrides every nax-spawned git carries (#2198, defence in depth).
3
+ *
4
+ * `core.fsmonitor` names a program git runs on every status/diff/add; were an
5
+ * agent ever to get a value into a config nax's git reads (a redirected
6
+ * commondir, a nested repo git recurses into), nax would run it unsandboxed.
7
+ * nax never needs an fsmonitor, so it is always off.
8
+ *
9
+ * Submodules (#2210): for every gitlink, `git status` / `diff` / `commit`
10
+ * run `git status` inside the nested repo to learn whether it is dirty, and
11
+ * that child git reads the nested repo's own config and `.gitattributes` --
12
+ * files an agent can write when it creates the repo inside its write root.
13
+ * A filter driver (`filter.<x>.clean`) named there would run unsandboxed, and
14
+ * unlike fsmonitor it cannot be switched off by name. `diff.ignoreSubmodules
15
+ * =dirty` skips that dirty check (a moved gitlink HEAD is still reported,
16
+ * without spawning git in it), and `submodule.recurse=false` keeps checkout /
17
+ * merge from recursing. Config is only a default, though: a
18
+ * `submodule.<name>.ignore` in the agent-writable `.gitmodules` overrides it,
19
+ * so status / diff also get `--ignore-submodules=dirty` on the command line
20
+ * (`hardenedGitArgv`), the one form that beats `.gitmodules`. `git add`
21
+ * ignores both, so nax stages through `gitlinkSafeAdd` (`./git-add`), which
22
+ * also refuses to run a commit that would only print status.
23
+ *
24
+ * Passed through GIT_CONFIG_COUNT/KEY/VALUE (git >= 2.31; older git ignores
25
+ * them) rather than `-c` so argv stays unchanged, and because git forwards
26
+ * these to the child git processes it spawns (submodules) as well.
27
+ *
28
+ * Coverage: `gitWithTimeout` applies it, and every other site in `src/` that
29
+ * spawns git itself passes `env: gitSpawnEnv(...)`. The generic forge and
30
+ * auto-pr runners harden every command they spawn, since `gh` / `glab` run git
31
+ * underneath. `packages/repo-tooling/scripts/check-git-spawn-env.ts` (in `lint:checks`) fails on a
32
+ * `["git", ...]` argv literal that is neither inside a spawn call carrying one
33
+ * of these helpers nor marked `// nax-git-env-allow: <reason>` (argv handed to
34
+ * a runner that hardens it itself). An argv held in a variable and spawned
35
+ * elsewhere is beyond a textual gate; keep git argv literal at the spawn.
36
+ */
37
+ /**
38
+ * `base` plus the hardened entries, appended after any GIT_CONFIG_COUNT
39
+ * entries the caller's environment already carries. A malformed existing
40
+ * count is left alone: git rejects it either way, and rewriting it would
41
+ * change which of the caller's entries apply.
42
+ */
43
+ export declare function hardenedGitEnv(base: Readonly<Record<string, string | undefined>>): Record<string, string | undefined>;
44
+ /**
45
+ * The environment for a git nax spawns directly: `process.env`, then the
46
+ * caller's `overlay` (e.g. GIT_INDEX_FILE), then the hardened entries. Pass
47
+ * the overlay here rather than spreading the result, so a GIT_CONFIG_COUNT
48
+ * the overlay sets is appended to instead of overwritten.
49
+ */
50
+ export declare function gitSpawnEnv(overlay?: Readonly<Record<string, string | undefined>>): Record<string, string | undefined>;
51
+ export declare const IGNORE_DIRTY_SUBMODULES_FLAG = "--ignore-submodules=dirty";
52
+ /**
53
+ * `argv` (argv[0] is the git program) with `--ignore-submodules=dirty` placed
54
+ * right after a `status` / `diff` subcommand. Unlike the config key, the flag
55
+ * overrides a `submodule.<name>.ignore` from `.gitmodules`. A later explicit
56
+ * `--ignore-submodules` in `argv` still wins (git takes the last), which the
57
+ * agent-facing git tool cannot supply: it refuses every `-`-leading element.
58
+ */
59
+ export declare function hardenedGitArgv(argv: readonly string[]): string[];
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Config overrides every nax-spawned git carries (#2198, defence in depth).
3
+ *
4
+ * `core.fsmonitor` names a program git runs on every status/diff/add; were an
5
+ * agent ever to get a value into a config nax's git reads (a redirected
6
+ * commondir, a nested repo git recurses into), nax would run it unsandboxed.
7
+ * nax never needs an fsmonitor, so it is always off.
8
+ *
9
+ * Submodules (#2210): for every gitlink, `git status` / `diff` / `commit`
10
+ * run `git status` inside the nested repo to learn whether it is dirty, and
11
+ * that child git reads the nested repo's own config and `.gitattributes` --
12
+ * files an agent can write when it creates the repo inside its write root.
13
+ * A filter driver (`filter.<x>.clean`) named there would run unsandboxed, and
14
+ * unlike fsmonitor it cannot be switched off by name. `diff.ignoreSubmodules
15
+ * =dirty` skips that dirty check (a moved gitlink HEAD is still reported,
16
+ * without spawning git in it), and `submodule.recurse=false` keeps checkout /
17
+ * merge from recursing. Config is only a default, though: a
18
+ * `submodule.<name>.ignore` in the agent-writable `.gitmodules` overrides it,
19
+ * so status / diff also get `--ignore-submodules=dirty` on the command line
20
+ * (`hardenedGitArgv`), the one form that beats `.gitmodules`. `git add`
21
+ * ignores both, so nax stages through `gitlinkSafeAdd` (`./git-add`), which
22
+ * also refuses to run a commit that would only print status.
23
+ *
24
+ * Passed through GIT_CONFIG_COUNT/KEY/VALUE (git >= 2.31; older git ignores
25
+ * them) rather than `-c` so argv stays unchanged, and because git forwards
26
+ * these to the child git processes it spawns (submodules) as well.
27
+ *
28
+ * Coverage: `gitWithTimeout` applies it, and every other site in `src/` that
29
+ * spawns git itself passes `env: gitSpawnEnv(...)`. The generic forge and
30
+ * auto-pr runners harden every command they spawn, since `gh` / `glab` run git
31
+ * underneath. `packages/repo-tooling/scripts/check-git-spawn-env.ts` (in `lint:checks`) fails on a
32
+ * `["git", ...]` argv literal that is neither inside a spawn call carrying one
33
+ * of these helpers nor marked `// nax-git-env-allow: <reason>` (argv handed to
34
+ * a runner that hardens it itself). An argv held in a variable and spawned
35
+ * elsewhere is beyond a textual gate; keep git argv literal at the spawn.
36
+ */
37
+ const HARDENED_GIT_CONFIG = [
38
+ ["core.fsmonitor", "false"],
39
+ ["diff.ignoreSubmodules", "dirty"],
40
+ ["submodule.recurse", "false"],
41
+ ];
42
+ const NON_NEGATIVE_INT = /^\d+$/;
43
+ /**
44
+ * `base` plus the hardened entries, appended after any GIT_CONFIG_COUNT
45
+ * entries the caller's environment already carries. A malformed existing
46
+ * count is left alone: git rejects it either way, and rewriting it would
47
+ * change which of the caller's entries apply.
48
+ */
49
+ export function hardenedGitEnv(base) {
50
+ const existing = base.GIT_CONFIG_COUNT;
51
+ if (existing !== undefined && existing !== "" && !NON_NEGATIVE_INT.test(existing))
52
+ return { ...base };
53
+ const start = existing === undefined || existing === "" ? 0 : Number.parseInt(existing, 10);
54
+ const env = { ...base };
55
+ HARDENED_GIT_CONFIG.forEach(([key, value], i) => {
56
+ env[`GIT_CONFIG_KEY_${start + i}`] = key;
57
+ env[`GIT_CONFIG_VALUE_${start + i}`] = value;
58
+ });
59
+ env.GIT_CONFIG_COUNT = String(start + HARDENED_GIT_CONFIG.length);
60
+ return env;
61
+ }
62
+ /**
63
+ * The environment for a git nax spawns directly: `process.env`, then the
64
+ * caller's `overlay` (e.g. GIT_INDEX_FILE), then the hardened entries. Pass
65
+ * the overlay here rather than spreading the result, so a GIT_CONFIG_COUNT
66
+ * the overlay sets is appended to instead of overwritten.
67
+ */
68
+ export function gitSpawnEnv(overlay) {
69
+ return hardenedGitEnv(overlay === undefined ? process.env : { ...process.env, ...overlay });
70
+ }
71
+ /** Subcommands that dirty-check gitlinks, and so run git inside them unless told not to (#2210). */
72
+ const SUBMODULE_DIRTY_CHECK_VERBS = new Set(["status", "diff"]);
73
+ /** Global options that consume the next argument (`-C <dir>`, `-c <k=v>`). */
74
+ const GLOBAL_OPTIONS_WITH_VALUE = new Set(["-C", "-c"]);
75
+ export const IGNORE_DIRTY_SUBMODULES_FLAG = "--ignore-submodules=dirty";
76
+ /**
77
+ * `argv` (argv[0] is the git program) with `--ignore-submodules=dirty` placed
78
+ * right after a `status` / `diff` subcommand. Unlike the config key, the flag
79
+ * overrides a `submodule.<name>.ignore` from `.gitmodules`. A later explicit
80
+ * `--ignore-submodules` in `argv` still wins (git takes the last), which the
81
+ * agent-facing git tool cannot supply: it refuses every `-`-leading element.
82
+ */
83
+ export function hardenedGitArgv(argv) {
84
+ const out = [...argv];
85
+ let i = 1;
86
+ while (i < out.length && out[i].startsWith("-")) {
87
+ i += GLOBAL_OPTIONS_WITH_VALUE.has(out[i]) ? 2 : 1;
88
+ }
89
+ const verb = out[i];
90
+ if (verb !== undefined && SUBMODULE_DIRTY_CHECK_VERBS.has(verb))
91
+ out.splice(i + 1, 0, IGNORE_DIRTY_SUBMODULES_FLAG);
92
+ return out;
93
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Generic git subprocess execution, shared by nax and the move set. `_gitDeps`
3
+ * is defined here once; `utils/git.ts` re-exports the same object (spec section
4
+ * 4.3), so a test patch through either path affects both halves.
5
+ */
6
+ import { getSafeLogger } from "#src/infra/index";
7
+ /**
8
+ * Default timeout for git subprocess calls.
9
+ * Prevents git from hanging indefinitely on locked repos or network mounts.
10
+ */
11
+ export declare const GIT_TIMEOUT_MS = 10000;
12
+ /**
13
+ * Injectable dependencies for git subprocess calls — allows tests to intercept
14
+ * Bun.spawn without mock.module().
15
+ *
16
+ * `gitTimeoutMs` and `timeoutRetryGitTimeoutMs` are injectable so the hang-path
17
+ * tests can assert the SIGKILL contract without burning the full production
18
+ * timeout in wall-clock.
19
+ *
20
+ * @internal
21
+ */
22
+ export declare const _gitDeps: {
23
+ spawn: (cmd: readonly string[], opts: import("#src/runtime/index").AgentSpawnOptions) => import("#src/runtime/index").AgentSpawnResult;
24
+ getSafeLogger: typeof getSafeLogger;
25
+ gitTimeoutMs: number;
26
+ timeoutRetryGitTimeoutMs: number;
27
+ };
28
+ /**
29
+ * Return the absolute path of the git repository root for the given workdir.
30
+ * Returns null if workdir is not inside a git repo or the command fails.
31
+ */
32
+ export declare function getGitRoot(workdir: string): Promise<string | null>;
33
+ /**
34
+ * Spawn a git command with a hard timeout.
35
+ *
36
+ * Kills the process with SIGKILL after GIT_TIMEOUT_MS if it hasn't exited.
37
+ * Returns empty stdout and exit code 1 on timeout.
38
+ *
39
+ * `timedOut` is ADDITIVE and OMITTED on every non-timeout return -- every
40
+ * existing caller that destructures `{ stdout, stderr, exitCode }` or asserts
41
+ * on that exact shape is unaffected. It exists because `exitCode === 1` is
42
+ * ambiguous on its own: a timeout collapses to exit code 1 (see below), which
43
+ * is indistinguishable from a real "git said no" exit 1 unless a caller can
44
+ * also see `timedOut`. A caller that treats "not exit 0" as a real, trustworthy
45
+ * answer (e.g. `git check-ignore`'s "not ignored") must check `timedOut` first
46
+ * -- see `partitionNaxOwnedPaths` in `src/tools/git-commit.ts`.
47
+ *
48
+ * @internal
49
+ */
50
+ export declare function gitWithTimeout(args: string[], workdir: string, timeoutMs?: number, maxBytes?: number,
51
+ /** Full argv INCLUDING argv[0]. Callers that must control argv[0] pass this;
52
+ * everyone else gets ["git", ...args] as before. */
53
+ argvOverride?: readonly string[]): Promise<{
54
+ stdout: string;
55
+ stderr: string;
56
+ exitCode: number;
57
+ timedOut?: boolean;
58
+ }>;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Generic git subprocess execution, shared by nax and the move set. `_gitDeps`
3
+ * is defined here once; `utils/git.ts` re-exports the same object (spec section
4
+ * 4.3), so a test patch through either path affects both halves.
5
+ */
6
+ import { getSafeLogger } from "#src/infra/index";
7
+ import { runtimeSpawn } from "#src/runtime/index";
8
+ import { drainBounded } from "./bounded-io.js";
9
+ import { hardenedGitArgv, hardenedGitEnv } from "./git-env.js";
10
+ /**
11
+ * Default timeout for git subprocess calls.
12
+ * Prevents git from hanging indefinitely on locked repos or network mounts.
13
+ */
14
+ export const GIT_TIMEOUT_MS = 10_000;
15
+ /**
16
+ * Timeout for the git subprocesses captureWorkingTreeChanges spawns during
17
+ * timeout-retry recovery. Scoped separately from GIT_TIMEOUT_MS so it doesn't
18
+ * shrink the timeout for unrelated callers (captureOutputFiles, findMergeBase,
19
+ * etc.) — a hung git here must not stall the already-timed-out agent turn.
20
+ */
21
+ const TIMEOUT_RETRY_GIT_TIMEOUT_MS = 3_000;
22
+ /**
23
+ * Injectable dependencies for git subprocess calls — allows tests to intercept
24
+ * Bun.spawn without mock.module().
25
+ *
26
+ * `gitTimeoutMs` and `timeoutRetryGitTimeoutMs` are injectable so the hang-path
27
+ * tests can assert the SIGKILL contract without burning the full production
28
+ * timeout in wall-clock.
29
+ *
30
+ * @internal
31
+ */
32
+ export const _gitDeps = {
33
+ spawn: runtimeSpawn,
34
+ getSafeLogger,
35
+ gitTimeoutMs: GIT_TIMEOUT_MS,
36
+ timeoutRetryGitTimeoutMs: TIMEOUT_RETRY_GIT_TIMEOUT_MS,
37
+ };
38
+ /**
39
+ * Return the absolute path of the git repository root for the given workdir.
40
+ * Returns null if workdir is not inside a git repo or the command fails.
41
+ */
42
+ export async function getGitRoot(workdir) {
43
+ try {
44
+ const { stdout, exitCode } = await gitWithTimeout(["rev-parse", "--show-toplevel"], workdir);
45
+ if (exitCode !== 0)
46
+ return null;
47
+ const trimmed = stdout.trim();
48
+ return trimmed || null;
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ /**
55
+ * Spawn a git command with a hard timeout.
56
+ *
57
+ * Kills the process with SIGKILL after GIT_TIMEOUT_MS if it hasn't exited.
58
+ * Returns empty stdout and exit code 1 on timeout.
59
+ *
60
+ * `timedOut` is ADDITIVE and OMITTED on every non-timeout return -- every
61
+ * existing caller that destructures `{ stdout, stderr, exitCode }` or asserts
62
+ * on that exact shape is unaffected. It exists because `exitCode === 1` is
63
+ * ambiguous on its own: a timeout collapses to exit code 1 (see below), which
64
+ * is indistinguishable from a real "git said no" exit 1 unless a caller can
65
+ * also see `timedOut`. A caller that treats "not exit 0" as a real, trustworthy
66
+ * answer (e.g. `git check-ignore`'s "not ignored") must check `timedOut` first
67
+ * -- see `partitionNaxOwnedPaths` in `src/tools/git-commit.ts`.
68
+ *
69
+ * @internal
70
+ */
71
+ export async function gitWithTimeout(args, workdir, timeoutMs = _gitDeps.gitTimeoutMs, maxBytes,
72
+ /** Full argv INCLUDING argv[0]. Callers that must control argv[0] pass this;
73
+ * everyone else gets ["git", ...args] as before. */
74
+ argvOverride) {
75
+ const proc = _gitDeps.spawn(hardenedGitArgv(argvOverride ?? ["git", ...args]), {
76
+ cwd: workdir,
77
+ env: hardenedGitEnv(process.env),
78
+ stdout: "pipe",
79
+ stderr: "pipe",
80
+ });
81
+ let timedOut = false;
82
+ const timerId = setTimeout(() => {
83
+ timedOut = true;
84
+ try {
85
+ proc.kill("SIGKILL");
86
+ }
87
+ catch {
88
+ // Process may have already exited
89
+ }
90
+ }, timeoutMs);
91
+ // Drain stdout/stderr concurrently with awaiting exit — a process that fills
92
+ // either pipe's OS buffer (>64KB) before being read would otherwise block on
93
+ // the write and never reach `exited`, defeating the timeout's own SIGKILL.
94
+ // `.catch()` is attached eagerly: on the timeout path below we return without
95
+ // awaiting these, and an unawaited rejection (a SIGKILLed process can error its
96
+ // pipes) would surface as an unhandled rejection and take the process down.
97
+ // `maxBytes` bounds the WORK, not just the answer: the timeout caps wall
98
+ // clock, so without it `git log -p` on a large repository can accumulate for
99
+ // as long as the timeout allows. Optional, and unbounded when absent, so the
100
+ // callers that read a single ref keep exactly their current behaviour.
101
+ const drain = (stream) => (maxBytes === undefined ? new Response(stream).text() : drainBounded(stream, maxBytes)).catch(() => "");
102
+ const stdoutPromise = drain(proc.stdout);
103
+ const stderrPromise = drain(proc.stderr);
104
+ const exitCode = await proc.exited;
105
+ clearTimeout(timerId);
106
+ if (timedOut) {
107
+ // Don't await the drain promises here — a SIGKILL'd process's pipes may
108
+ // never close in test mocks (and are irrelevant either way since the
109
+ // output is discarded), so awaiting them could re-introduce a hang on
110
+ // the very path this timeout exists to bound.
111
+ return { stdout: "", stderr: "", exitCode: 1, timedOut: true };
112
+ }
113
+ const [stdout, stderr] = await Promise.all([stdoutPromise, stderrPromise]);
114
+ return { stdout, stderr, exitCode };
115
+ }