@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,161 @@
1
+ /**
2
+ * The I/O half of the sandbox's git guards (#2198): the git-read files that
3
+ * redirect where git finds its config and hooks, as they exist on disk now.
4
+ *
5
+ * A sandboxed command that repoints one of these at an agent-authored
6
+ * directory (its own `config` with core.hooksPath / core.fsmonitor) makes
7
+ * nax's next UNSANDBOXED git run agent code. The redirecting files are:
8
+ * `<common>/commondir`, and for every linked worktree `worktrees/<id>/`
9
+ * `{gitdir, commondir, config.worktree}` plus the worktree's `.git` pointer.
10
+ *
11
+ * Only files that EXIST are denied here: srt on Linux stubs an absent deny
12
+ * with an empty file (host-side too, while the command runs), and git dies
13
+ * reading an empty `commondir` / `gitdir` -- inside the sandbox and in any
14
+ * concurrent nax git. An absent `<common>/commondir` is therefore guarded by
15
+ * a tripwire instead (strayCommonDirTripwire). `config.worktree` is the
16
+ * exception: an empty one is valid config, so it is always denied.
17
+ *
18
+ * The tripwire is what protects a main checkout, whose git dir sits inside the
19
+ * root write root. In a linked worktree the common dir is not a write root at
20
+ * all (WORKTREE_COMMON_WRITE_DIRS, #2211), so the file cannot be created there
21
+ * and the tripwire is only a second line.
22
+ */
23
+ import { lstat, readdir, readFile, rm } from "node:fs/promises";
24
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
25
+ import { errorMessage } from "#src/infra/errors";
26
+ import { getSafeLogger } from "#src/infra/index";
27
+ import { realOrRaw } from "#src/internal/realpath";
28
+ import { SANDBOX_GLOB_CHARS } from "../config/schemas-sandbox.js";
29
+ const WORKTREES_DIR = "worktrees";
30
+ const COMMONDIR_FILE = "commondir";
31
+ const GITDIR_FILE = "gitdir";
32
+ export const WORKTREE_CONFIG_FILE = "config.worktree";
33
+ /**
34
+ * What a linked worktree's git writes in the SHARED common dir (#2211): loose
35
+ * and packed objects, refs and their reflogs (`reftable/` holds both when
36
+ * `extensions.refStorage=reftable`, git >= 2.45), and git-lfs's object store.
37
+ * Only these are write roots, never the common dir itself, so the redirecting
38
+ * files at its top level (`commondir`, `config`, `hooks/`) and every sibling
39
+ * worktree's admin dir lie outside the sandbox's writes. `modules/` is left
40
+ * out on purpose: it holds submodule git dirs, each with its own config.
41
+ *
42
+ * Cost: a lock file at the common dir's top level cannot be created, so a
43
+ * sandboxed command cannot rewrite `packed-refs` (deleting a packed branch or
44
+ * tag) or take the `gc --auto` lock; commits, loose ref updates and fetches
45
+ * into loose objects are unaffected. (A reftable repo locks inside
46
+ * `reftable/`, so it keeps ref deletion too.)
47
+ */
48
+ export const WORKTREE_COMMON_WRITE_DIRS = ["objects", "refs", "logs", "reftable", "lfs"];
49
+ const DOT_GIT = ".git";
50
+ export const _gitGuardDeps = {
51
+ readdir,
52
+ exists: async (p) => {
53
+ try {
54
+ await lstat(p);
55
+ return true;
56
+ }
57
+ catch {
58
+ return false;
59
+ }
60
+ },
61
+ readText: (p) => readFile(p, "utf8"),
62
+ remove: (p) => rm(p, { recursive: true, force: true }),
63
+ };
64
+ /** The directory holding the shared config/hooks: the git dir itself for a main checkout. */
65
+ export function commonDirOf(git) {
66
+ if (git.kind === "none")
67
+ return undefined;
68
+ return git.kind === "worktree" ? git.commonDir : git.gitDir;
69
+ }
70
+ /** A glob-named path would make every policy build throw (F1), so it is skipped, not emitted. */
71
+ function isLiteral(p) {
72
+ return !SANDBOX_GLOB_CHARS.test(realOrRaw(p));
73
+ }
74
+ /**
75
+ * The `<wt>/.git` pointer a `gitdir` file names -- only a literal `.git` path.
76
+ *
77
+ * git >= 2.48 with `worktree.useRelativePaths` (or `worktree add
78
+ * --relative-paths`) writes it relative to the admin dir `<common>/worktrees/<id>`
79
+ * -- computed from that dir's realpath, and read back by joining onto it -- so a
80
+ * relative target resolves against the admin dir's realpath. Resolving against
81
+ * the unresolved spelling would walk `..` lexically and miss a symlinked
82
+ * common dir.
83
+ */
84
+ async function pointerOf(gitdirFile) {
85
+ try {
86
+ const raw = (await _gitGuardDeps.readText(gitdirFile)).trim();
87
+ if (raw === "")
88
+ return [];
89
+ const target = isAbsolute(raw) ? raw : resolve(realOrRaw(dirname(gitdirFile)), raw);
90
+ return basename(target) === DOT_GIT && isLiteral(target) ? [target] : [];
91
+ }
92
+ catch {
93
+ return [];
94
+ }
95
+ }
96
+ async function existing(paths) {
97
+ const present = await Promise.all(paths.map((p) => _gitGuardDeps.exists(p)));
98
+ return paths.filter((_, i) => present[i]);
99
+ }
100
+ async function worktreeEntryFiles(adminDir) {
101
+ const gitdirFile = join(adminDir, GITDIR_FILE);
102
+ const pointers = await existing([gitdirFile, join(adminDir, COMMONDIR_FILE), ...(await pointerOf(gitdirFile))]);
103
+ return [...pointers, join(adminDir, WORKTREE_CONFIG_FILE)];
104
+ }
105
+ /**
106
+ * Every redirecting git file for the repo that `git` describes, for every
107
+ * worktree registered when the policy is built (literal paths: srt on Linux
108
+ * drops glob denies).
109
+ */
110
+ export async function listGitGuardFiles(git) {
111
+ const common = commonDirOf(git);
112
+ if (common === undefined)
113
+ return [];
114
+ const worktrees = join(common, WORKTREES_DIR);
115
+ let names = [];
116
+ try {
117
+ const entries = await _gitGuardDeps.readdir(worktrees, { withFileTypes: true });
118
+ names = entries.filter((e) => e.isDirectory() && isLiteral(join(worktrees, e.name))).map((e) => e.name);
119
+ }
120
+ catch {
121
+ // no linked worktrees
122
+ }
123
+ const perWorktree = await Promise.all(names.map((name) => worktreeEntryFiles(join(worktrees, name))));
124
+ // Filtered again as a whole: an agent-made symlink can resolve a literal name to a glob path.
125
+ return [...(await existing([join(common, COMMONDIR_FILE)])), ...perWorktree.flat()].filter(isLiteral);
126
+ }
127
+ /**
128
+ * `<common>/commondir` never legitimately exists (git reads it only from a
129
+ * linked worktree's own git dir), and an absent one cannot be denied on Linux
130
+ * (see the header). So when it is absent at session start, one created by a
131
+ * sandboxed command is removed after that command, before nax's next git.
132
+ * Returns undefined when there is nothing to guard (no repo, or the file
133
+ * already exists -- listGitGuardFiles then denies it outright).
134
+ */
135
+ export async function strayCommonDirTripwire(git, storyId) {
136
+ const common = commonDirOf(git);
137
+ if (common === undefined)
138
+ return undefined;
139
+ const target = join(common, COMMONDIR_FILE);
140
+ if (await _gitGuardDeps.exists(target))
141
+ return undefined;
142
+ return async () => {
143
+ if (!(await _gitGuardDeps.exists(target)))
144
+ return;
145
+ const logger = getSafeLogger();
146
+ try {
147
+ await _gitGuardDeps.remove(target);
148
+ logger?.error("sandbox", "A sandboxed command created a git commondir redirect; removed it", {
149
+ storyId,
150
+ path: target,
151
+ });
152
+ }
153
+ catch (err) {
154
+ logger?.error("sandbox", "A sandboxed command created a git commondir redirect that could not be removed", {
155
+ storyId,
156
+ path: target,
157
+ error: errorMessage(err),
158
+ });
159
+ }
160
+ };
161
+ }
@@ -0,0 +1,11 @@
1
+ export { quoteArgvForShell } from "./argv-quote.ts";
2
+ export { _gitGuardDeps, listGitGuardFiles, strayCommonDirTripwire } from "./git-guards.ts";
3
+ export { _launcherDeps, type CommandLauncherOptions, createCommandLauncher, DISABLED_SANDBOX_STATE, } from "./launcher.ts";
4
+ export { denialHintLine, LIKELY_SANDBOX_DENIAL, rawBashRefusalReason, sandboxSentence, unsandboxedSentence, } from "./messages.ts";
5
+ export { buildSandboxPolicy, type SandboxPolicyInput } from "./policy-builder.ts";
6
+ export { _policyInputDeps, defaultTempRoots, type GitLayout, listCredentialFiles, listNaxEntries, resolveGitLayout, runTempRoots, } from "./policy-inputs.ts";
7
+ export { _probeDeps, probeSandbox } from "./probe.ts";
8
+ export { _resetSandboxRegistryForTests, _sandboxRegistryDeps, probeSandboxOnce, resetSandboxBackend, sandboxBackendFor, warnSandboxUnavailableOnce, } from "./registry.ts";
9
+ export { _sessionTmpDeps, runTmpRoot, sessionTmpDir, sessionTmpDirUnder } from "./session-tmp.ts";
10
+ export { _srtBackendDeps, createSrtBackend } from "./srt-backend.ts";
11
+ export type { CommandLauncher, LaunchRequest, LaunchResult, LaunchSpec, ProbeResult, SandboxBackend, SandboxBackendName, SandboxNetworkPolicy, SandboxPolicy, SandboxRecord, SandboxState, SandboxWrapRequest, } from "./types.ts";
@@ -0,0 +1,10 @@
1
+ export { quoteArgvForShell } from "./argv-quote.js";
2
+ export { _gitGuardDeps, listGitGuardFiles, strayCommonDirTripwire } from "./git-guards.js";
3
+ export { _launcherDeps, createCommandLauncher, DISABLED_SANDBOX_STATE, } from "./launcher.js";
4
+ export { denialHintLine, LIKELY_SANDBOX_DENIAL, rawBashRefusalReason, sandboxSentence, unsandboxedSentence, } from "./messages.js";
5
+ export { buildSandboxPolicy } from "./policy-builder.js";
6
+ export { _policyInputDeps, defaultTempRoots, listCredentialFiles, listNaxEntries, resolveGitLayout, runTempRoots, } from "./policy-inputs.js";
7
+ export { _probeDeps, probeSandbox } from "./probe.js";
8
+ export { _resetSandboxRegistryForTests, _sandboxRegistryDeps, probeSandboxOnce, resetSandboxBackend, sandboxBackendFor, warnSandboxUnavailableOnce, } from "./registry.js";
9
+ export { _sessionTmpDeps, runTmpRoot, sessionTmpDir, sessionTmpDirUnder } from "./session-tmp.js";
10
+ export { _srtBackendDeps, createSrtBackend } from "./srt-backend.js";
@@ -0,0 +1,37 @@
1
+ import { runArgv } from "#src/internal/argv-exec";
2
+ import type { CommandLauncher, SandboxBackend, SandboxPolicy, SandboxState } from "./types.ts";
3
+ export declare const DISABLED_SANDBOX_STATE: SandboxState;
4
+ export declare const _launcherDeps: {
5
+ runArgv: typeof runArgv;
6
+ newCommandId: () => string;
7
+ mkdir: (path: string) => Promise<void>;
8
+ };
9
+ export interface CommandLauncherOptions {
10
+ readonly state: SandboxState;
11
+ readonly backend?: SandboxBackend;
12
+ /**
13
+ * Build the policy for one command.
14
+ *
15
+ * `tmpDirInForce` says whether THIS command gets the session's own `TMPDIR`
16
+ * override: the launcher declared a `tmpDir` and the per-run `ensureTmpDir`
17
+ * recreated it, so `runWrapped` will apply the `export TMPDIR=…` prefix. It is
18
+ * false when the override is dropped, and srt's own forced `TMPDIR` applies
19
+ * instead — a builder that denies that directory would then hand the session a
20
+ * `TMPDIR` its own sandbox refuses to write (#2301, SPEC-tmp-confinement.md:132).
21
+ *
22
+ * Reported separately from `state` on purpose: `state.sharedTmp === false` is
23
+ * the construction-time answer (did the session RESOLVE as confined) and never
24
+ * changes, while this is re-decided on every run. A session dir that recreates
25
+ * for one run and not the next is confined by the first and not the second.
26
+ */
27
+ readonly policyFor?: (root: string, tmpDirInForce: boolean) => Promise<SandboxPolicy>;
28
+ /** Runs after every wrapped command, before control returns to nax (e.g. a git tripwire, #2198). */
29
+ readonly afterWrapped?: () => Promise<void>;
30
+ /**
31
+ * US-004 — the session's temp directory. Created before every run, then handed
32
+ * to the child as `TMPDIR`/`TMP`/`TEMP` so stray files land under the run's own
33
+ * root instead of the shared `/tmp`.
34
+ */
35
+ readonly tmpDir?: string;
36
+ }
37
+ export declare function createCommandLauncher(opts: CommandLauncherOptions): CommandLauncher;
@@ -0,0 +1,160 @@
1
+ /**
2
+ * How an agent-authored command runs (spec 5.1). Never WHETHER -- that is the
3
+ * policy's call (single-gate rule). Handed only to Bash and RunCommand's Exec
4
+ * branch, so a user-authored command can never be wrapped (D14).
5
+ *
6
+ * Two rules with teeth:
7
+ * - F6: the backend returns argv only; runArgv receives the CALLER's env
8
+ * overlay and strips from process.env exactly as today.
9
+ * - A wrap that throws is an error. The command never runs unwrapped: what
10
+ * stops is the command, never the sandbox.
11
+ */
12
+ import { randomUUID } from "node:crypto";
13
+ import { mkdir } from "node:fs/promises";
14
+ import { errorMessage } from "#src/infra/errors";
15
+ import { getSafeLogger, NaxError } from "#src/infra/index";
16
+ import { runArgv } from "#src/internal/argv-exec";
17
+ import { quoteArgvForShell } from "./argv-quote.js";
18
+ import { denialHintLine, LIKELY_SANDBOX_DENIAL } from "./messages.js";
19
+ export const DISABLED_SANDBOX_STATE = { kind: "disabled" };
20
+ export const _launcherDeps = {
21
+ runArgv,
22
+ newCommandId: () => randomUUID(),
23
+ mkdir: (path) => mkdir(path, { recursive: true }).then(() => undefined),
24
+ };
25
+ function logicalArgv(req) {
26
+ return req.spec.kind === "shell" ? [req.spec.shell, "-c", req.spec.command] : req.spec.argv;
27
+ }
28
+ /** POSIX single-quoting: wrap in `'`, turning an embedded `'` into `'\''`. */
29
+ function shellSingleQuote(value) {
30
+ return `'${value.replaceAll("'", "'\\''")}'`;
31
+ }
32
+ /** `export TMPDIR=… TMP=… TEMP=…; ` — the prefix a wrapped command carries, since srt replaces the child env. */
33
+ function tmpEnvPrefix(tmpDir) {
34
+ const q = shellSingleQuote(tmpDir);
35
+ return `export TMPDIR=${q} TMP=${q} TEMP=${q}; `;
36
+ }
37
+ /** The TMPDIR/TMP/TEMP overlay for an unwrapped run; the request's own env wins per key. */
38
+ function withTmpEnv(req, tmpDir) {
39
+ if (tmpDir === undefined)
40
+ return req.env;
41
+ return { TMPDIR: tmpDir, TMP: tmpDir, TEMP: tmpDir, ...(req.env ?? {}) };
42
+ }
43
+ async function ensureTmpDir(tmpDir) {
44
+ try {
45
+ await _launcherDeps.mkdir(tmpDir);
46
+ return true;
47
+ }
48
+ catch (err) {
49
+ getSafeLogger()?.warn("sandbox", "could not create session temp dir — running without TMPDIR override", {
50
+ tmpDir,
51
+ error: errorMessage(err),
52
+ });
53
+ return false;
54
+ }
55
+ }
56
+ async function runUnwrapped(req, sandbox, env) {
57
+ const argv = logicalArgv(req);
58
+ const overlay = env.tmpDir !== undefined ? withTmpEnv(req, env.tmpDir) : req.env;
59
+ const result = await _launcherDeps.runArgv({
60
+ argv,
61
+ cwd: req.cwd,
62
+ timeoutMs: req.timeoutMs,
63
+ stripEnvVars: [...req.stripEnvVars],
64
+ ...(overlay !== undefined ? { env: overlay } : {}),
65
+ ...(req.signal !== undefined ? { signal: req.signal } : {}),
66
+ });
67
+ return { ...result, executed: argv, sandbox };
68
+ }
69
+ async function runWrapped(req, backend, policy, env) {
70
+ const rawCommand = req.spec.kind === "shell" ? req.spec.command : quoteArgvForShell(req.spec.argv);
71
+ // srt replaces the child environment wholesale, so a TMPDIR override cannot
72
+ // ride an `env` overlay — it has to be part of the shell command itself.
73
+ // Other overlay keys DO survive: the live suite proves it (`AGENT=1` reaches
74
+ // a wrapped child — see test/integration/sandbox/sandbox-live.test.ts
75
+ // "US-004 — the AGENT=1 overlay reaches a wrapped child"). `executed` below
76
+ // stays the unprefixed logical argv, so the ledger records what the agent
77
+ // wrote, not this shim.
78
+ const command = env.tmpDir !== undefined ? `${tmpEnvPrefix(env.tmpDir)}${rawCommand}` : rawCommand;
79
+ const shell = req.spec.kind === "shell" ? req.spec.shell : "/bin/sh";
80
+ const commandId = _launcherDeps.newCommandId();
81
+ let argv;
82
+ try {
83
+ argv = await backend.wrap({ command, shell, policy, cwd: req.cwd, commandId });
84
+ }
85
+ catch (err) {
86
+ throw new NaxError(`[sandbox] could not wrap the command: ${errorMessage(err)}`, "SANDBOX_WRAP_FAILED", {
87
+ stage: "sandbox",
88
+ backend: backend.name,
89
+ commandId,
90
+ cause: err,
91
+ });
92
+ }
93
+ try {
94
+ const result = await _launcherDeps.runArgv({
95
+ argv,
96
+ cwd: req.cwd,
97
+ timeoutMs: req.timeoutMs,
98
+ stripEnvVars: [...req.stripEnvVars],
99
+ ...(req.env !== undefined ? { env: req.env } : {}),
100
+ ...(req.signal !== undefined ? { signal: req.signal } : {}),
101
+ });
102
+ const denied = result.exitCode !== 0 && LIKELY_SANDBOX_DENIAL.test(result.stderr);
103
+ const violations = backend.annotate(commandId, result.stderr);
104
+ const extra = [
105
+ ...(denied ? [denialHintLine(policy.writeRoots, env.sharedTmp)] : []),
106
+ ...(violations !== "" ? [violations] : []),
107
+ ];
108
+ return {
109
+ ...result,
110
+ stderr: extra.length > 0 ? `${result.stderr}\n${extra.join("\n")}` : result.stderr,
111
+ executed: logicalArgv(req),
112
+ sandbox: {
113
+ backend: backend.name,
114
+ wrapped: true,
115
+ argv,
116
+ ...(denied ? { denialHint: true } : {}),
117
+ },
118
+ };
119
+ }
120
+ finally {
121
+ backend.commandFinished();
122
+ }
123
+ }
124
+ export function createCommandLauncher(opts) {
125
+ const { state } = opts;
126
+ const env = {
127
+ tmpDir: opts.tmpDir,
128
+ // A confined session's denial hint points at $TMPDIR instead of /tmp; a
129
+ // shared-temp session's hint must not contradict the policy it runs under.
130
+ sharedTmp: state.kind !== "available" || state.sharedTmp !== false,
131
+ };
132
+ return {
133
+ state,
134
+ async run(req) {
135
+ const tmpDir = env.tmpDir !== undefined && (await ensureTmpDir(env.tmpDir)) ? env.tmpDir : undefined;
136
+ if (state.kind === "disabled")
137
+ return runUnwrapped(req, { backend: "none", wrapped: false }, { ...env, tmpDir });
138
+ if (state.kind === "unavailable") {
139
+ return runUnwrapped(req, { backend: state.backend, wrapped: false, reason: state.reason }, { ...env, tmpDir });
140
+ }
141
+ if (opts.backend === undefined || opts.policyFor === undefined) {
142
+ throw new NaxError("[sandbox] an available launcher needs a backend and a policy", "SANDBOX_NOT_CONFIGURED", {
143
+ stage: "sandbox",
144
+ });
145
+ }
146
+ try {
147
+ // `runWrapped` is handed this same `tmpDir` and applies the
148
+ // `export TMPDIR=…` prefix only when it is defined, so `tmpDir !== undefined`
149
+ // is exactly the "the override is in force for this command" fact.
150
+ return await runWrapped(req, opts.backend, await opts.policyFor(req.root, tmpDir !== undefined), {
151
+ ...env,
152
+ tmpDir,
153
+ });
154
+ }
155
+ finally {
156
+ await opts.afterWrapped?.();
157
+ }
158
+ },
159
+ };
160
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Every sentence the agent reads about the sandbox (spec 5.5), in one place so
3
+ * descriptions, refusals and result notes cannot drift apart. The agent must
4
+ * always know whether it is sandboxed and what it may write (spec S5).
5
+ */
6
+ export declare const LIKELY_SANDBOX_DENIAL: RegExp;
7
+ export declare function sandboxSentence(network: "open" | readonly string[], sharedTmp?: boolean): string;
8
+ export declare function unsandboxedSentence(reason: string): string;
9
+ export declare function rawBashRefusalReason(reason: string): string;
10
+ /**
11
+ * US-003: the denial path is exactly where an agent discovers the sandbox
12
+ * refuses a `/tmp` write, so the hint names the two roots that do work -- the
13
+ * session temp dir and the scratchpad -- rather than only listing the policy's
14
+ * write roots. A shared-temp session (`allowSharedTmp: true`) can write `/tmp`,
15
+ * so its hint omits the "not /tmp" clause that would contradict its policy.
16
+ */
17
+ export declare function denialHintLine(writeRoots: readonly string[], sharedTmp?: boolean): string;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Every sentence the agent reads about the sandbox (spec 5.5), in one place so
3
+ * descriptions, refusals and result notes cannot drift apart. The agent must
4
+ * always know whether it is sandboxed and what it may write (spec S5).
5
+ */
6
+ export const LIKELY_SANDBOX_DENIAL = /Operation not permitted|Read-only file system/;
7
+ function describeNetwork(network) {
8
+ if (network === "open")
9
+ return "network access is unrestricted";
10
+ if (network.length === 0)
11
+ return "network access is disabled";
12
+ return `network access is limited to ${network.join(", ")}`;
13
+ }
14
+ /**
15
+ * US-003: the writable temp root the sentence advertises. A confined session
16
+ * (`sharedTmp: false`) can only write under the run's own temp directory, so
17
+ * promising "the system temp directories" would send the agent at a denied
18
+ * write; a shared-temp session keeps the wording it had before.
19
+ */
20
+ const tempRootsPhrase = (sharedTmp) => sharedTmp ? "the system temp directories" : "this run's temp directory ($TMPDIR)";
21
+ export function sandboxSentence(network, sharedTmp = true) {
22
+ return (`inside an OS sandbox: writes are allowed only under the repository root, ${tempRootsPhrase(sharedTmp)} and ` +
23
+ "package-manager caches -- and nothing under .nax/ except .nax/scratchpad/, since .nax/ is nax's own state " +
24
+ "(change a feature's acceptance test with the Edit tool); credential files (~/.ssh, ~/.aws, ~/.npmrc, nax credentials and similar) are unreadable; " +
25
+ `${describeNetwork(network)}. A write anywhere else fails with "Operation not permitted" or "Read-only file system" ` +
26
+ "-- that is the sandbox, not a bug in your command.");
27
+ }
28
+ export function unsandboxedSentence(reason) {
29
+ return `Commands are NOT sandboxed on this machine (${reason}).`;
30
+ }
31
+ export function rawBashRefusalReason(reason) {
32
+ return (`sandbox unavailable (${reason}): raw bash requires the sandbox when execution.sandbox.enabled is true -- ` +
33
+ "set this stage's bashApproval to gated or escalate, or disable the sandbox.");
34
+ }
35
+ /**
36
+ * US-003: the denial path is exactly where an agent discovers the sandbox
37
+ * refuses a `/tmp` write, so the hint names the two roots that do work -- the
38
+ * session temp dir and the scratchpad -- rather than only listing the policy's
39
+ * write roots. A shared-temp session (`allowSharedTmp: true`) can write `/tmp`,
40
+ * so its hint omits the "not /tmp" clause that would contradict its policy.
41
+ */
42
+ export function denialHintLine(writeRoots, sharedTmp = false) {
43
+ const tempClause = sharedTmp ? "" : " For temporary files use $TMPDIR or .nax/scratchpad/, not /tmp.";
44
+ return (`note: this command ran in the nax sandbox; that failure may be a sandbox denial -- writable roots: ${writeRoots.join(", ")}.` +
45
+ tempClause);
46
+ }
@@ -0,0 +1,44 @@
1
+ import { type SandboxConfig } from "../config/schemas-sandbox.ts";
2
+ import type { GitLayout } from "./policy-inputs.ts";
3
+ import type { SandboxPolicy } from "./types.ts";
4
+ export interface SandboxPolicyInput {
5
+ readonly root: string;
6
+ readonly git: GitLayout;
7
+ /** Redirecting git files present now, every worktree included (listGitGuardFiles, #2198). */
8
+ readonly gitGuardFiles: readonly string[];
9
+ /** Top-level entry names under `<root>/.nax` right now (listNaxEntries). */
10
+ readonly naxEntries: readonly string[];
11
+ readonly credentialFiles: readonly string[];
12
+ readonly approvalsFile?: string;
13
+ /** US-006 — the trust store (`trustStorePath()`), denied so a command cannot rewrite its own trust. */
14
+ readonly trustStoreFile?: string;
15
+ readonly home: string;
16
+ readonly tempRoots: readonly string[];
17
+ /**
18
+ * #2301: whether THIS command's temp writes stay confined — the RESOLVE-time
19
+ * answer ANDed with the fact that this launch's `TMPDIR` override is in force.
20
+ *
21
+ * It governs srt's forced TMPDIR on BOTH sides, not only the deny. On darwin,
22
+ * `true` drops `/tmp/claude` from `writeRoots` and adds both of its spellings
23
+ * to `denyWrite`; `false` does the reverse, so a command that did not confine
24
+ * keeps `/tmp/claude` writable as a plain grant. `input.tempRoots` is passed
25
+ * through either way: the flag moves srt's `/tmp/claude` grant, never the
26
+ * session's own temp roots.
27
+ *
28
+ * Deliberately NOT the value behind `SandboxState.sharedTmp === false` or
29
+ * `isTempConfined`. Those are the resolve-time answer, fixed when the launcher
30
+ * is built, while this is re-decided on every launch by ANDing that answer with
31
+ * whether the session temp dir was re-created. The two are MEANT to disagree: a
32
+ * session whose dir fails to re-create keeps `sharedTmp: false` and
33
+ * `isTempConfined() === true` while this reads `false` for that one command. Do
34
+ * not reconcile them by making either one follow the other.
35
+ *
36
+ * Absent means it confined neither way, so every construction outside
37
+ * `resolveSessionSandbox` keeps today's behaviour; the only other builder in
38
+ * the tree, the probe, writes its policy as a literal and never reaches here.
39
+ */
40
+ readonly confined?: boolean;
41
+ readonly platform: NodeJS.Platform;
42
+ readonly config: SandboxConfig;
43
+ }
44
+ export declare function buildSandboxPolicy(input: SandboxPolicyInput): SandboxPolicy;
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Pure: everything the sandbox allows and denies for one command (spec 5.3).
3
+ *
4
+ * Rebuilt per call so a `.nax/` entry created mid-run gets its deny (nax#2260).
5
+ * Every emitted path goes through realOrRaw -- srt realpaths only paths
6
+ * that exist, so a deny for a not-yet-created file under a symlinked temp dir
7
+ * would otherwise keep a spelling the kernel never sees (spec 12, finding 3).
8
+ */
9
+ import { isAbsolute, join, resolve } from "node:path";
10
+ import { NaxError } from "#src/infra/index";
11
+ import { realOrRaw } from "#src/internal/realpath";
12
+ import { SANDBOX_GLOB_CHARS } from "../config/schemas-sandbox.js";
13
+ import { NAX_ALWAYS_DENIED_ENTRIES, NAX_SCRATCHPAD_ENTRY, naxWriteOptIns, QUEUE_CONTROL_FILES, } from "../tools/nax-owned-writes.js";
14
+ import { BUILTIN_CACHE_WRITE_ROOTS, BUILTIN_CREDENTIAL_READ_DENIES, MACOS_CACHE_WRITE_ROOT, SRT_MACOS_TMPDIR, SRT_MACOS_TMPDIR_DENIES, } from "./defaults.js";
15
+ import { WORKTREE_COMMON_WRITE_DIRS, WORKTREE_CONFIG_FILE } from "./git-guards.js";
16
+ function expandHome(p, home) {
17
+ if (p === "~")
18
+ return home;
19
+ return p.startsWith("~/") ? join(home, p.slice(2)) : p;
20
+ }
21
+ /** Throws on a glob: a Linux backend would drop it silently (F1), so fail closed instead. */
22
+ function literal(paths) {
23
+ const resolved = [...new Set(paths.map((p) => realOrRaw(p)))];
24
+ const glob = resolved.find((p) => SANDBOX_GLOB_CHARS.test(p));
25
+ if (glob !== undefined) {
26
+ throw new NaxError(`[sandbox] policy path is not literal (glob character): ${glob}`, "SANDBOX_POLICY_NOT_LITERAL", {
27
+ stage: "sandbox",
28
+ path: glob,
29
+ });
30
+ }
31
+ return resolved;
32
+ }
33
+ function gitDenies(root, git) {
34
+ if (git.kind === "none")
35
+ return [];
36
+ const common = git.kind === "worktree" ? git.commonDir : git.gitDir;
37
+ // config.worktree is read when extensions.worktreeConfig is on; an absent one
38
+ // is safe to deny (srt stubs it empty, and empty is valid config) (#2198).
39
+ const shared = [join(common, "hooks"), join(common, "config"), join(common, WORKTREE_CONFIG_FILE)];
40
+ if (git.kind === "main")
41
+ return shared;
42
+ // A worktree's `.git` is a pointer FILE, and gitdir/commondir point back;
43
+ // repointing any of them at an agent-written config (core.hooksPath) would
44
+ // make nax's own unsandboxed git run hooks (spec 12, finding 4).
45
+ return [
46
+ ...shared,
47
+ join(root, ".git"),
48
+ join(git.gitDir, "gitdir"),
49
+ join(git.gitDir, "commondir"),
50
+ join(git.gitDir, WORKTREE_CONFIG_FILE),
51
+ ];
52
+ }
53
+ /**
54
+ * A worktree's own admin dir plus the shared subdirs its git writes -- never
55
+ * the whole common dir, whose top-level `commondir` would otherwise be a live
56
+ * redirect for concurrent unsandboxed git while the command runs (#2211).
57
+ * srt skips a write root that does not exist yet.
58
+ */
59
+ function worktreeGitWriteRoots(git) {
60
+ if (git.kind !== "worktree")
61
+ return [];
62
+ return [git.gitDir, ...WORKTREE_COMMON_WRITE_DIRS.map((dir) => join(git.commonDir, dir))];
63
+ }
64
+ /**
65
+ * nax#2260: every top-level `.nax/` entry, whole, plus the ones nax reads even
66
+ * when absent -- except the scratchpad and entries opted in via allowWrite.
67
+ * srt applies deny over allow and on Linux turns every literal deny into a
68
+ * bind mount, so whole entries (one `features` deny, not one per PRD) keep
69
+ * both the protection and the per-command cost bounded.
70
+ *
71
+ * Only `<root>/.nax` is covered. A monorepo package's own `.nax/`
72
+ * (`packages/<pkg>/.nax/`) is deliberately excluded: what lives there --
73
+ * cache/, scratchpad/, status.json, the features' acceptance tests and run
74
+ * artifacts -- is gitignored and rebuilt or regenerated, so deleting it is
75
+ * harmless, and the package's config sits under the root's protected `mono/`.
76
+ * The exception is an optional `packages/<pkg>/.nax/rules/`, which nax loads
77
+ * into prompts; covering it needs package discovery, because a `.nax/` at any
78
+ * depth also matches test-fixture projects (`test/fixtures/<p>/.nax/`).
79
+ */
80
+ function naxDenies(root, entries, allowWrite) {
81
+ const optIns = naxWriteOptIns(root, allowWrite);
82
+ return [...new Set([...entries, ...NAX_ALWAYS_DENIED_ENTRIES])]
83
+ .filter((name) => name !== NAX_SCRATCHPAD_ENTRY && !optIns.has(name))
84
+ .map((name) => join(root, ".nax", name));
85
+ }
86
+ export function buildSandboxPolicy(input) {
87
+ const { root, home, config } = input;
88
+ const darwin = input.platform === "darwin";
89
+ // #2301: a CONFINED session does not need srt's forced TMPDIR. srt always
90
+ // allows `/tmp/claude` (`SANDBOX_OWN_WRITE_PATHS`) and hands the child a
91
+ // `TMPDIR` of `/tmp/claude` (sandbox-utils.js:630), and in a macOS Seatbelt
92
+ // profile it renders the allow rules before the deny rules, where the later deny
93
+ // wins — so the only way to take the write back is an explicit deny, which is
94
+ // what a confined session gets.
95
+ //
96
+ // Both spellings, because `realOrRaw` resolves the nearest EXISTING ancestor and
97
+ // `/tmp` exists: on a stock macOS host `/tmp` is a symlink to `/private/tmp`, so
98
+ // `literal()` collapses the pair to one entry and it costs nothing there. The
99
+ // pair still matters on a host where `/tmp` is a real directory, or a symlink
100
+ // somewhere other than `/private/tmp`: there the two resolve apart and a single
101
+ // spelling would leave srt's other one writable.
102
+ //
103
+ // Nothing needs the grant instead: a session is confined only when it HAS a temp
104
+ // dir, and `runWrapped` prefixes the command with
105
+ // `export TMPDIR=<that dir> TMP=… TEMP=…` (src/sandbox/launcher.ts:138),
106
+ // replacing srt's value before the agent's command runs. That prefix rides on
107
+ // `createCommandLauncher`'s per-run mkdir (src/sandbox/launcher.ts:194), so it is
108
+ // conditional — when the mkdir fails the override is dropped and the agent's
109
+ // `TMPDIR` is srt's `/tmp/claude` again, which is why that case needs deciding
110
+ // before a deny can be relied on.
111
+ //
112
+ // A session that did NOT confine keeps the grant: its temp roots are
113
+ // `defaultTempRoots`, which allows `/tmp` outright, so `/tmp/claude` is inside an
114
+ // allowed root anyway and a deny would contradict the session's own denial hint
115
+ // (`denialHintLine`).
116
+ //
117
+ // darwin only, deliberately: srt's `SANDBOX_OWN_WRITE_PATHS` is
118
+ // platform-independent, so a confined LINUX session still gets `/tmp/claude`.
119
+ // Left open on purpose — on bwrap a literal deny becomes a bind mount, a
120
+ // different mechanism that needs its own check before a deny is added there.
121
+ const confined = input.confined === true;
122
+ const writeRoots = literal([
123
+ root,
124
+ ...worktreeGitWriteRoots(input.git),
125
+ ...input.tempRoots,
126
+ ...(darwin ? [...(confined ? [] : [SRT_MACOS_TMPDIR]), join(home, MACOS_CACHE_WRITE_ROOT)] : []),
127
+ ...BUILTIN_CACHE_WRITE_ROOTS.map((rel) => join(home, rel)),
128
+ ...config.filesystem.allowWrite.map((p) => {
129
+ const expanded = expandHome(p, home);
130
+ return isAbsolute(expanded) ? expanded : resolve(root, expanded);
131
+ }),
132
+ ]);
133
+ const denyWrite = literal([
134
+ ...naxDenies(root, input.naxEntries, config.filesystem.allowWrite),
135
+ ...[...QUEUE_CONTROL_FILES].map((name) => join(root, name)),
136
+ ...gitDenies(root, input.git),
137
+ ...input.gitGuardFiles,
138
+ ...(input.approvalsFile !== undefined ? [input.approvalsFile] : []),
139
+ ...(input.trustStoreFile !== undefined ? [input.trustStoreFile] : []),
140
+ ...(darwin && confined ? SRT_MACOS_TMPDIR_DENIES : []),
141
+ ]);
142
+ const denyRead = literal([
143
+ ...BUILTIN_CREDENTIAL_READ_DENIES.map((rel) => join(home, rel)),
144
+ ...input.credentialFiles,
145
+ ...config.filesystem.denyRead.map((p) => resolve(root, expandHome(p, home))),
146
+ ]);
147
+ const allowed = config.network.allowedDomains;
148
+ return { writeRoots, denyWrite, denyRead, network: allowed === undefined ? {} : { allowedDomains: [...allowed] } };
149
+ }