@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,168 @@
1
+ export interface ApprovalEntry {
2
+ readonly stage: string;
3
+ readonly command: string;
4
+ readonly root: string;
5
+ readonly origin: "escalate" | "askRule";
6
+ readonly matchedRule: string | null;
7
+ readonly approvedAt: string;
8
+ readonly approvedBy: string;
9
+ readonly naxCommit: string;
10
+ }
11
+ /**
12
+ * Written by nax itself when a FORGE-CAPABLE run uses the store (#2199): every
13
+ * entry beside it is untrusted, and the next run that trusts the cache
14
+ * discards them. See approvals-taint.ts for the trust argument.
15
+ */
16
+ export interface ApprovalsTaint {
17
+ readonly since: string;
18
+ readonly runId: string;
19
+ /** The tainting nax process; `undefined` when absent or malformed on disk. */
20
+ readonly pid: number | undefined;
21
+ }
22
+ export interface ApprovalsFile {
23
+ readonly entries: readonly ApprovalEntry[];
24
+ readonly taint: ApprovalsTaint | undefined;
25
+ }
26
+ /**
27
+ * The classification a `readApprovalsFileDetailed` call reports. `missing` is a
28
+ * path that resolves to no file at all (ENOENT, or a parent dir that does not
29
+ * exist); `unparseable` is a file whose contents the read could not turn into
30
+ * the entries+taint shape the cache needs; `ok` is a file the read accepts.
31
+ */
32
+ export type ApprovalsFileState = "ok" | "missing" | "unparseable";
33
+ /**
34
+ * What `readApprovalsFileDetailed` returns: the same `ApprovalsFile` shape
35
+ * `readApprovalsFile` always has, plus a classification and the count of array
36
+ * elements the read dropped for not being approval entries. `droppedMalformed`
37
+ * is `0` unless `state === "ok"` -- an unparseable file has no entries to
38
+ * drop, and a missing file has no file at all.
39
+ */
40
+ export interface ApprovalsFileRead {
41
+ readonly file: ApprovalsFile;
42
+ readonly state: ApprovalsFileState;
43
+ readonly droppedMalformed: number;
44
+ }
45
+ /**
46
+ * The run's output dir, NOT the tool root. `root` is storyExecRoot -- the repo
47
+ * OR WORKTREE root -- so a root-relative file is deleted by the
48
+ * `git worktree remove --force` that ends the run. That is the exact defect
49
+ * recorded in toolAuditDir's docblock (src/config/paths/index.ts:138-150).
50
+ */
51
+ export declare function approvalsPath(outputDir: string): string;
52
+ /**
53
+ * Stable, derived id for an entry. The id is a function of (stage, command,
54
+ * approvedAt) alone -- NOT of the entry's position in the file, or of any
55
+ * sibling entry -- so it survives other entries being added or removed and
56
+ * two entries recorded for the same triple share an id and are removed
57
+ * together (US-001).
58
+ *
59
+ * Computed on every read and never stored. A missing or non-string
60
+ * `approvedAt` reads as the empty string so the id is still a well-defined
61
+ * 8-character hex value for the malformed-on-disk entries the cache admits.
62
+ */
63
+ export declare function approvalId(entry: ApprovalEntry): string;
64
+ /**
65
+ * Classify a store read. `missing` means the file is absent (or its parent
66
+ * directory is); `unparseable` means a file is present but its contents are
67
+ * not a `{ entries, taint }` object the cache can trust; `ok` means the read
68
+ * accepted the file and `file.entries` lists every element `isApprovalEntry`
69
+ * accepted, with `droppedMalformed` counting the rejections.
70
+ *
71
+ * A present `taint` is still parsed when the rest of the file is unparseable:
72
+ * a forged `entries` array must not strip the marker.
73
+ *
74
+ * A read failure (EACCES/EIO) is not a parse failure and is not classified
75
+ * here — it propagates so a caller that must surface it (the `nax approvals`
76
+ * CLI) can report it instead of mislabelling it `unparseable`. Callers that
77
+ * must stay fail-closed use `readApprovalsFile`, which catches it.
78
+ */
79
+ export declare function readApprovalsFileDetailed(path: string): Promise<ApprovalsFileRead>;
80
+ /**
81
+ * Missing, malformed OR unreadable reads as empty: the CACHE fails, the chain
82
+ * does not. A permissions/IO error is caught here (rather than propagating
83
+ * from `readApprovalsFileDetailed`) so it cannot abort an approval lookup; a
84
+ * caller that must distinguish it calls the detailed read directly.
85
+ */
86
+ export declare function readApprovalsFile(path: string): Promise<ApprovalsFile>;
87
+ export declare function readApprovals(path: string): Promise<readonly ApprovalEntry[]>;
88
+ /** Serialize the whole store. Callers hold the path lock. */
89
+ export declare function writeApprovalsFile(path: string, file: ApprovalsFile): Promise<void>;
90
+ /**
91
+ * Lock the read-modify-write. Worktree-isolated parallel stories share one
92
+ * project-scoped file BY DESIGN (that is why `root` is not part of the key), so
93
+ * two "Allow + remember" taps can race and drop one. `withPathFileLock` is the
94
+ * shared primitive for exactly this shape; it fails closed on timeout rather
95
+ * than entering over a possible holder.
96
+ *
97
+ * The taint marker is PRESERVED: a "remember" tapped during a forge-capable run
98
+ * lands in a store that stays untrusted.
99
+ */
100
+ export declare function appendApproval(path: string, entry: ApprovalEntry): Promise<void>;
101
+ /**
102
+ * The decision `removeApprovals` asks the caller for, once the read has
103
+ * classified the file. The caller picks which entries to drop with `remove` --
104
+ * a predicate over the kept entries from the read -- or refuses the operation
105
+ * with `refuse`. The two arms are mutually exclusive: a single decision is
106
+ * either "remove these" or "I will not act, because: ...".
107
+ */
108
+ export type RemovalDecision = {
109
+ readonly remove: (entry: ApprovalEntry) => boolean;
110
+ } | {
111
+ readonly refuse: string;
112
+ };
113
+ /**
114
+ * What `removeApprovals` reports back. `refused` covers both the file being
115
+ * unparseable (so the read refuses the call to `decide`) and the caller's own
116
+ * `refuse` decision: the caller cannot distinguish the two from the result
117
+ * alone, only from the message. `removed` reports exactly the entries the
118
+ * caller chose to drop plus the count of malformed array elements the read
119
+ * dropped for being non-entries. `unchanged` is returned for both a missing
120
+ * file and a present file whose predicate selected nothing.
121
+ */
122
+ export type RemovalResult = {
123
+ readonly outcome: "removed";
124
+ readonly removed: readonly ApprovalEntry[];
125
+ readonly droppedMalformed: number;
126
+ } | {
127
+ readonly outcome: "unchanged";
128
+ } | {
129
+ readonly outcome: "refused";
130
+ readonly reason: string;
131
+ };
132
+ /**
133
+ * Locked, taint-preserving removal. Holds the same path-scoped lock
134
+ * `appendApproval` holds, so a removal racing an append cannot drop one of the
135
+ * two writes; whichever runs first inside the lock, the other sees the
136
+ * post-write state on its next read.
137
+ *
138
+ * The order of the guards matters and is documented in the story:
139
+ * 1. `state === "unparseable"` -> `refused`, no `decide` call, no write.
140
+ * Rewriting would erase whatever the file holds, including a taint
141
+ * marker we did not write.
142
+ * 2. `decide(read)` returns `{ refuse }` -> `refused`, no write.
143
+ * 3. The predicate selected no entry (or the file was missing) ->
144
+ * `unchanged`, no write, and neither the data file nor its parent
145
+ * directory is created.
146
+ * 4. Otherwise write `{ taint: read.file.taint, entries: kept }` with the
147
+ * SAME taint that was read. Nothing in this function clears a taint;
148
+ * only `clearApprovalsTaint` (approvals-taint.ts) does, and only from a
149
+ * trusted run.
150
+ *
151
+ * The lock file must live next to the target, so the lock is only taken when
152
+ * that parent directory already exists. A path whose parent is missing is by
153
+ * definition a missing store: the read-decide path runs without the lock and
154
+ * creates nothing -- taking the lock there would require `mkdir`, which rule 3
155
+ * forbids. `decide` is still invoked exactly once in that arm, so the missing
156
+ * read reaches the caller as it does under the lock.
157
+ */
158
+ export declare function removeApprovals(path: string, decide: (read: ApprovalsFileRead) => RemovalDecision): Promise<RemovalResult>;
159
+ /**
160
+ * BYTE-EXACT. No normalization. See the module docblock.
161
+ *
162
+ * `projectRoot`, when given, also requires the entry's `root` to be that root
163
+ * or lie inside it. Worktrees (`<projectRoot>/.nax-wt/<id>`) and monorepo
164
+ * package dirs both do, so parallel stories still share entries. This is
165
+ * HYGIENE, not authentication: it drops entries another project wrote into a
166
+ * shared outputDir, but a forger can write any `root` it likes (#2199).
167
+ */
168
+ export declare function findApproval(entries: readonly ApprovalEntry[], stage: string, command: string, projectRoot?: string): ApprovalEntry | undefined;
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Durable store for remembered approvals (P2 design section 6.6).
3
+ *
4
+ * A remembered approval is a CACHED HUMAN DECISION, not a rule. Lookup is
5
+ * BYTE-EXACT on (stage, command) with no normalization of any kind -- not
6
+ * trimming, not whitespace collapsing, not quote folding. Any normalization is
7
+ * a place where the string that was approved and the string that runs can
8
+ * diverge, and the whole value of the cache is that they cannot.
9
+ *
10
+ * Synthesizing a `Bash(...)` rule instead would be materially broader than what
11
+ * the operator approved: rule matching is a token-wise PREFIX match with no
12
+ * length ceiling (src/tools/policy-bash.ts:72-83), so a rule made from
13
+ * `bun run test` would also grant `bun run test --reporter=./x`.
14
+ *
15
+ * The file may also carry a `taint` marker (#2199) -- see approvals-taint.ts.
16
+ *
17
+ * Revocation goes through `src/cli/approvals.ts`, the surface D20 relies on:
18
+ * `removeApprovals` here is the locked, taint-preserving read-decide-write
19
+ * primitive that surface calls into.
20
+ */
21
+ import { createHash } from "node:crypto";
22
+ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
23
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
24
+ import { withPathFileLock } from "#src/internal/path-file-lock";
25
+ const EMPTY_FILE = { entries: [], taint: undefined };
26
+ /**
27
+ * The run's output dir, NOT the tool root. `root` is storyExecRoot -- the repo
28
+ * OR WORKTREE root -- so a root-relative file is deleted by the
29
+ * `git worktree remove --force` that ends the run. That is the exact defect
30
+ * recorded in toolAuditDir's docblock (src/config/paths/index.ts:138-150).
31
+ */
32
+ export function approvalsPath(outputDir) {
33
+ return join(outputDir, "approvals.json");
34
+ }
35
+ /**
36
+ * The shape findApproval relies on. A malformed element (null, a string, an
37
+ * object missing `stage`/`command`) is DROPPED rather than returned: an
38
+ * unvalidated array makes `findApproval`'s `e.stage` throw, and a poisoned
39
+ * cache read would fail the whole ask chain instead of just this entry.
40
+ */
41
+ function isApprovalEntry(value) {
42
+ if (typeof value !== "object" || value === null)
43
+ return false;
44
+ if (!("stage" in value) || !("command" in value))
45
+ return false;
46
+ return typeof value.stage === "string" && typeof value.command === "string";
47
+ }
48
+ /**
49
+ * ANY present `taint` value taints, however malformed: the marker exists to
50
+ * withhold trust, so an unparseable one must not grant it.
51
+ */
52
+ function parseTaint(value) {
53
+ if (value === undefined || value === null)
54
+ return undefined;
55
+ const raw = typeof value === "object" ? { ...value } : {};
56
+ return {
57
+ since: typeof raw.since === "string" ? raw.since : "",
58
+ runId: typeof raw.runId === "string" ? raw.runId : "",
59
+ pid: typeof raw.pid === "number" && Number.isInteger(raw.pid) ? raw.pid : undefined,
60
+ };
61
+ }
62
+ /**
63
+ * Stable, derived id for an entry. The id is a function of (stage, command,
64
+ * approvedAt) alone -- NOT of the entry's position in the file, or of any
65
+ * sibling entry -- so it survives other entries being added or removed and
66
+ * two entries recorded for the same triple share an id and are removed
67
+ * together (US-001).
68
+ *
69
+ * Computed on every read and never stored. A missing or non-string
70
+ * `approvedAt` reads as the empty string so the id is still a well-defined
71
+ * 8-character hex value for the malformed-on-disk entries the cache admits.
72
+ */
73
+ export function approvalId(entry) {
74
+ const approvedAt = typeof entry.approvedAt === "string" ? entry.approvedAt : "";
75
+ const digest = createHash("sha256");
76
+ digest.update(`${entry.stage}\0${entry.command}\0${approvedAt}`);
77
+ return digest.digest("hex").slice(0, 8);
78
+ }
79
+ /**
80
+ * Classify a store read. `missing` means the file is absent (or its parent
81
+ * directory is); `unparseable` means a file is present but its contents are
82
+ * not a `{ entries, taint }` object the cache can trust; `ok` means the read
83
+ * accepted the file and `file.entries` lists every element `isApprovalEntry`
84
+ * accepted, with `droppedMalformed` counting the rejections.
85
+ *
86
+ * A present `taint` is still parsed when the rest of the file is unparseable:
87
+ * a forged `entries` array must not strip the marker.
88
+ *
89
+ * A read failure (EACCES/EIO) is not a parse failure and is not classified
90
+ * here — it propagates so a caller that must surface it (the `nax approvals`
91
+ * CLI) can report it instead of mislabelling it `unparseable`. Callers that
92
+ * must stay fail-closed use `readApprovalsFile`, which catches it.
93
+ */
94
+ export async function readApprovalsFileDetailed(path) {
95
+ let contents;
96
+ try {
97
+ contents = await readFile(path, "utf8");
98
+ }
99
+ catch (error) {
100
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
101
+ return { state: "missing", file: EMPTY_FILE, droppedMalformed: 0 };
102
+ }
103
+ throw error;
104
+ }
105
+ let parsed;
106
+ try {
107
+ parsed = JSON.parse(contents);
108
+ }
109
+ catch {
110
+ return { state: "unparseable", file: EMPTY_FILE, droppedMalformed: 0 };
111
+ }
112
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
113
+ return { state: "unparseable", file: EMPTY_FILE, droppedMalformed: 0 };
114
+ }
115
+ const { entries, taint } = parsed;
116
+ if ("entries" in parsed && !Array.isArray(entries)) {
117
+ return { state: "unparseable", file: { entries: [], taint: parseTaint(taint) }, droppedMalformed: 0 };
118
+ }
119
+ const array = Array.isArray(entries) ? entries : [];
120
+ const kept = [];
121
+ let dropped = 0;
122
+ for (const element of array) {
123
+ if (isApprovalEntry(element)) {
124
+ kept.push(element);
125
+ }
126
+ else {
127
+ dropped += 1;
128
+ }
129
+ }
130
+ return { state: "ok", file: { entries: kept, taint: parseTaint(taint) }, droppedMalformed: dropped };
131
+ }
132
+ /**
133
+ * Missing, malformed OR unreadable reads as empty: the CACHE fails, the chain
134
+ * does not. A permissions/IO error is caught here (rather than propagating
135
+ * from `readApprovalsFileDetailed`) so it cannot abort an approval lookup; a
136
+ * caller that must distinguish it calls the detailed read directly.
137
+ */
138
+ export async function readApprovalsFile(path) {
139
+ try {
140
+ return (await readApprovalsFileDetailed(path)).file;
141
+ }
142
+ catch {
143
+ return EMPTY_FILE;
144
+ }
145
+ }
146
+ export async function readApprovals(path) {
147
+ return (await readApprovalsFile(path)).entries;
148
+ }
149
+ /** Serialize the whole store. Callers hold the path lock. */
150
+ export async function writeApprovalsFile(path, file) {
151
+ const body = file.taint === undefined ? { entries: file.entries } : { taint: file.taint, entries: file.entries };
152
+ await mkdir(dirname(path), { recursive: true });
153
+ await writeFile(path, `${JSON.stringify(body, null, 2)}\n`, "utf8");
154
+ }
155
+ /**
156
+ * Lock the read-modify-write. Worktree-isolated parallel stories share one
157
+ * project-scoped file BY DESIGN (that is why `root` is not part of the key), so
158
+ * two "Allow + remember" taps can race and drop one. `withPathFileLock` is the
159
+ * shared primitive for exactly this shape; it fails closed on timeout rather
160
+ * than entering over a possible holder.
161
+ *
162
+ * The taint marker is PRESERVED: a "remember" tapped during a forge-capable run
163
+ * lands in a store that stays untrusted.
164
+ */
165
+ export async function appendApproval(path, entry) {
166
+ await mkdir(dirname(path), { recursive: true });
167
+ await withPathFileLock(path, async () => {
168
+ const existing = await readApprovalsFile(path);
169
+ await writeApprovalsFile(path, { taint: existing.taint, entries: [...existing.entries, entry] });
170
+ });
171
+ }
172
+ /**
173
+ * The locked read-decide-write body shared by the two `removeApprovals` arms.
174
+ * Pure read -> guard -> decide -> filter -> write: no mkdir, no lock handling.
175
+ */
176
+ async function applyRemoval(path, decide) {
177
+ const read = await readApprovalsFileDetailed(path);
178
+ if (read.state === "unparseable") {
179
+ return { outcome: "refused", reason: "approvals.json could not be parsed; not rewriting it" };
180
+ }
181
+ const decision = decide(read);
182
+ if ("refuse" in decision) {
183
+ return { outcome: "refused", reason: decision.refuse };
184
+ }
185
+ const removed = [];
186
+ const kept = [];
187
+ for (const entry of read.file.entries) {
188
+ if (decision.remove(entry)) {
189
+ removed.push(entry);
190
+ }
191
+ else {
192
+ kept.push(entry);
193
+ }
194
+ }
195
+ if (removed.length === 0) {
196
+ return { outcome: "unchanged" };
197
+ }
198
+ await writeApprovalsFile(path, { taint: read.file.taint, entries: kept });
199
+ return { outcome: "removed", removed, droppedMalformed: read.droppedMalformed };
200
+ }
201
+ /** `true` iff `dir` exists and is a directory (symlinks followed). */
202
+ async function directoryExists(dir) {
203
+ try {
204
+ return (await stat(dir)).isDirectory();
205
+ }
206
+ catch {
207
+ return false;
208
+ }
209
+ }
210
+ /**
211
+ * Locked, taint-preserving removal. Holds the same path-scoped lock
212
+ * `appendApproval` holds, so a removal racing an append cannot drop one of the
213
+ * two writes; whichever runs first inside the lock, the other sees the
214
+ * post-write state on its next read.
215
+ *
216
+ * The order of the guards matters and is documented in the story:
217
+ * 1. `state === "unparseable"` -> `refused`, no `decide` call, no write.
218
+ * Rewriting would erase whatever the file holds, including a taint
219
+ * marker we did not write.
220
+ * 2. `decide(read)` returns `{ refuse }` -> `refused`, no write.
221
+ * 3. The predicate selected no entry (or the file was missing) ->
222
+ * `unchanged`, no write, and neither the data file nor its parent
223
+ * directory is created.
224
+ * 4. Otherwise write `{ taint: read.file.taint, entries: kept }` with the
225
+ * SAME taint that was read. Nothing in this function clears a taint;
226
+ * only `clearApprovalsTaint` (approvals-taint.ts) does, and only from a
227
+ * trusted run.
228
+ *
229
+ * The lock file must live next to the target, so the lock is only taken when
230
+ * that parent directory already exists. A path whose parent is missing is by
231
+ * definition a missing store: the read-decide path runs without the lock and
232
+ * creates nothing -- taking the lock there would require `mkdir`, which rule 3
233
+ * forbids. `decide` is still invoked exactly once in that arm, so the missing
234
+ * read reaches the caller as it does under the lock.
235
+ */
236
+ export async function removeApprovals(path, decide) {
237
+ if (!(await directoryExists(dirname(path)))) {
238
+ return applyRemoval(path, decide);
239
+ }
240
+ return withPathFileLock(path, () => applyRemoval(path, decide));
241
+ }
242
+ /**
243
+ * BYTE-EXACT. No normalization. See the module docblock.
244
+ *
245
+ * `projectRoot`, when given, also requires the entry's `root` to be that root
246
+ * or lie inside it. Worktrees (`<projectRoot>/.nax-wt/<id>`) and monorepo
247
+ * package dirs both do, so parallel stories still share entries. This is
248
+ * HYGIENE, not authentication: it drops entries another project wrote into a
249
+ * shared outputDir, but a forger can write any `root` it likes (#2199).
250
+ */
251
+ export function findApproval(entries, stage, command, projectRoot) {
252
+ return entries.find((e) => e.stage === stage && e.command === command && (projectRoot === undefined || isWithin(projectRoot, e.root)));
253
+ }
254
+ function isWithin(projectRoot, root) {
255
+ if (typeof root !== "string" || root === "")
256
+ return false;
257
+ const rel = relative(resolve(projectRoot), resolve(root));
258
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
259
+ }
@@ -0,0 +1,33 @@
1
+ export declare const _approvalsTaintDeps: {
2
+ pid: () => number;
3
+ now: () => Date;
4
+ isProcessAlive: (pid: number) => boolean;
5
+ };
6
+ /**
7
+ * A run can forge the approvals file when any stage runs a raw shell outside
8
+ * the sandbox. With the sandbox enabled the file is always in `denyWrite`, and
9
+ * a raw stage is either wrapped or refused. Config-only: no probe dependency.
10
+ */
11
+ export declare function isForgeCapable(stageModes: readonly string[], sandboxEnabled: boolean): boolean;
12
+ /** Drop every entry and record who tainted the store. */
13
+ export declare function taintApprovals(path: string, runId: string): Promise<void>;
14
+ /** `held`: a still-running forge-capable nax process owns the taint. */
15
+ export type ClearTaintOutcome = "clean" | "cleared" | "held";
16
+ /**
17
+ * Start a trusted epoch. Entries that sat beside a taint are dropped, never
18
+ * promoted: nothing distinguishes a forged one from a human's.
19
+ */
20
+ export declare function clearApprovalsTaint(path: string, runId: string): Promise<ClearTaintOutcome>;
21
+ export interface PrepareApprovalsStoreOptions {
22
+ readonly approvalsFile: string;
23
+ readonly runId: string;
24
+ /** Absent for a run-scoped (post-run) dispatch scope. */
25
+ readonly storyId?: string;
26
+ readonly forgeCapable: boolean;
27
+ }
28
+ /**
29
+ * Taint (forge-capable) or clear (trusted) the store. Never throws: a failed
30
+ * clear leaves the taint in place, so the link abstains and the cost is
31
+ * prompts; a failed taint is logged, since a later run may then trust entries.
32
+ */
33
+ export declare function prepareApprovalsStore(opts: PrepareApprovalsStoreOptions): Promise<void>;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Cross-run provenance for the approvals cache (#2199).
3
+ *
4
+ * THE PROBLEM. `approvals.json` is project-scoped and outlives the run. A run
5
+ * with any `bashApproval: raw` stage and no sandbox ("forge-capable") has a
6
+ * shell that can append arbitrary entries, and a LATER run that trusts the
7
+ * cache would auto-allow them as `decidedBy: "cache"`. Checking only the
8
+ * current run's stage modes cannot see that history.
9
+ *
10
+ * WHY NOT A SIGNATURE. An unsandboxed raw shell runs as the same OS user as
11
+ * nax. Any key nax can read from disk -- under ~/.nax or anywhere else -- that
12
+ * shell can read too, and then mint valid signatures. Signing would look
13
+ * stronger than it is, so it is deliberately not done (ADR-030 said the same).
14
+ *
15
+ * THE DESIGN: a taint marker that only nax writes, at points no agent runs.
16
+ * 1. A forge-capable run TAINTS the store before a dispatch scope's agents
17
+ * start (a story, or a post-run fix loop) and again after they finish: every entry is dropped and a `taint` record is
18
+ * written. The end-of-story re-taint wipes whatever the story's agents
19
+ * wrote, even an agent that knew to strip the first marker. One final seal
20
+ * runs at the end of the run (`buildApprovalsSeal`, awaited by `cleanupRun`
21
+ * and by the signal teardown), covering agents dispatched outside any
22
+ * dispatch-ask scope after the last per-scope re-taint.
23
+ * 2. The cache link ABSTAINS on a tainted store, whatever it contains.
24
+ * 3. A run that trusts the cache clears a taint before building its link by
25
+ * dropping every entry with it, so a trusted epoch starts empty. It leaves
26
+ * the taint alone while the tainting run may still be writing: the same
27
+ * run (a sibling story in a forge-capable package), or another nax process
28
+ * that is still alive.
29
+ *
30
+ * RESIDUAL RISK, stated so it is not overclaimed. The guarantee is against an
31
+ * agent that forges entries during its turn, not against a hostile one that
32
+ * understands this scheme. Out of reach of any on-disk mechanism:
33
+ * - a process the agent leaves running past its story (e.g. a detached
34
+ * `sleep; rewrite`) that strips the marker and forges after the last
35
+ * re-taint;
36
+ * - a forge-capable run killed by a death no handler sees (SIGKILL, power
37
+ * loss) between an agent stripping the marker and the run-end seal. Normal
38
+ * exits and handled signals both seal: `cleanupRun` awaits the seal in the
39
+ * runner's finally, and `performTeardown` awaits it after the PID sweep.
40
+ * An unsandboxed raw shell can equally edit shell rc files or git hooks, so
41
+ * this sits inside the posture ADR-030 already discloses; the sandbox, which
42
+ * always write-denies this file, is the real boundary.
43
+ */
44
+ import { getSafeLogger } from "#src/infra/index";
45
+ import { withPathFileLock } from "#src/internal/path-file-lock";
46
+ import { readApprovalsFile, writeApprovalsFile } from "./approvals-store.js";
47
+ /** `kill(pid, 0)` failing with EPERM means the process exists but is not ours. */
48
+ const ERRNO_NO_PERMISSION = "EPERM";
49
+ export const _approvalsTaintDeps = {
50
+ pid: () => process.pid,
51
+ now: () => new Date(),
52
+ isProcessAlive: (pid) => {
53
+ try {
54
+ process.kill(pid, 0);
55
+ return true;
56
+ }
57
+ catch (err) {
58
+ return err.code === ERRNO_NO_PERMISSION;
59
+ }
60
+ },
61
+ };
62
+ /**
63
+ * A run can forge the approvals file when any stage runs a raw shell outside
64
+ * the sandbox. With the sandbox enabled the file is always in `denyWrite`, and
65
+ * a raw stage is either wrapped or refused. Config-only: no probe dependency.
66
+ */
67
+ export function isForgeCapable(stageModes, sandboxEnabled) {
68
+ return stageModes.includes("raw") && !sandboxEnabled;
69
+ }
70
+ /** Drop every entry and record who tainted the store. */
71
+ export async function taintApprovals(path, runId) {
72
+ const taint = { since: _approvalsTaintDeps.now().toISOString(), runId, pid: _approvalsTaintDeps.pid() };
73
+ await withPathFileLock(path, async () => writeApprovalsFile(path, { taint, entries: [] }));
74
+ }
75
+ /**
76
+ * Whether the tainting run may still be writing. A taint from THIS run is held:
77
+ * stories can resolve different sandbox settings per package, so a trusted
78
+ * story must not clear the marker a forge-capable sibling story just wrote. A
79
+ * taint from an earlier run in this same process is not held.
80
+ */
81
+ function taintIsHeld(taint, runId) {
82
+ if (taint.runId === runId)
83
+ return true;
84
+ const owner = taint.pid;
85
+ if (owner === undefined || owner === _approvalsTaintDeps.pid())
86
+ return false;
87
+ return _approvalsTaintDeps.isProcessAlive(owner);
88
+ }
89
+ /**
90
+ * Start a trusted epoch. Entries that sat beside a taint are dropped, never
91
+ * promoted: nothing distinguishes a forged one from a human's.
92
+ */
93
+ export async function clearApprovalsTaint(path, runId) {
94
+ return withPathFileLock(path, async () => {
95
+ const { taint } = await readApprovalsFile(path);
96
+ if (taint === undefined)
97
+ return "clean";
98
+ if (taintIsHeld(taint, runId))
99
+ return "held";
100
+ await writeApprovalsFile(path, { taint: undefined, entries: [] });
101
+ return "cleared";
102
+ });
103
+ }
104
+ /**
105
+ * Taint (forge-capable) or clear (trusted) the store. Never throws: a failed
106
+ * clear leaves the taint in place, so the link abstains and the cost is
107
+ * prompts; a failed taint is logged, since a later run may then trust entries.
108
+ */
109
+ export async function prepareApprovalsStore(opts) {
110
+ const logger = getSafeLogger();
111
+ try {
112
+ if (opts.forgeCapable) {
113
+ await taintApprovals(opts.approvalsFile, opts.runId);
114
+ return;
115
+ }
116
+ const outcome = await clearApprovalsTaint(opts.approvalsFile, opts.runId);
117
+ if (outcome === "held") {
118
+ logger?.warn("permissions", "[approvals] store tainted by a forge-capable run still in progress; cache stays off", {
119
+ storyId: opts.storyId,
120
+ approvalsFile: opts.approvalsFile,
121
+ });
122
+ }
123
+ else if (outcome === "cleared") {
124
+ logger?.info("permissions", "[approvals] discarded entries written under a forge-capable run", {
125
+ storyId: opts.storyId,
126
+ approvalsFile: opts.approvalsFile,
127
+ });
128
+ }
129
+ }
130
+ catch (error) {
131
+ logger?.warn("permissions", "[approvals] could not update the store's taint marker", {
132
+ storyId: opts.storyId,
133
+ approvalsFile: opts.approvalsFile,
134
+ forgeCapable: opts.forgeCapable,
135
+ error,
136
+ });
137
+ }
138
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The ask-tier resolver chain (P2 design section 4).
3
+ *
4
+ * One decision point for every permission adjudicator: the approvals cache,
5
+ * a future classifier (P5) and the human gate are all LINKS here. The channel
6
+ * that asks a human lives in src/interaction/ and is not a peer of this chain
7
+ * -- it is what the human link talks to.
8
+ *
9
+ * Fail-closed by construction: the chain appends its own terminal deny, so an
10
+ * exhausted or all-abstaining chain denies whether or not the last link is
11
+ * total. A link that throws is treated as no answer, never as permission.
12
+ */
13
+ import type { AskRequest } from "./types.ts";
14
+ /** A link's answer. `abstain` means "no opinion, try the next link". */
15
+ export type AskDecision = "allow" | "deny" | "abstain";
16
+ /** Who actually decided. Carried into the ledger so it never has to be inferred. */
17
+ export type AskDecidedBy = "cache" | "model" | "human" | "timeout" | "unavailable" | "cancelled" | "unshowable";
18
+ /**
19
+ * Per-ask control (US-003), carried SEPARATELY from the `AskRequest` so the
20
+ * request the ledger records stays the pure request and a resolver can react
21
+ * to the orchestrating turn being cancelled.
22
+ */
23
+ export interface AskControl {
24
+ /** The turn's abort signal; aborted while a waiter sits on-screen or queued. */
25
+ readonly signal?: AbortSignal;
26
+ /** Notifies the turn loop that this ask is waiting on a human. */
27
+ readonly onWaiting?: () => void;
28
+ }
29
+ export interface AskLinkOutcome {
30
+ readonly decision: AskDecision;
31
+ readonly decidedBy: AskDecidedBy;
32
+ }
33
+ /**
34
+ * One adjudicator. A link ALWAYS names who decided, because a single link can
35
+ * answer for more than one reason -- the human link resolves as `human` when
36
+ * someone taps and as `timeout` when nobody does.
37
+ */
38
+ export interface AskLink {
39
+ readonly name: string;
40
+ resolve(req: AskRequest, control?: AskControl): Promise<AskLinkOutcome>;
41
+ }
42
+ /** What the runtime consumes. Never `abstain`. */
43
+ export interface AskVerdict {
44
+ readonly decision: "allow" | "deny";
45
+ readonly decidedBy: AskDecidedBy;
46
+ readonly latencyMs: number;
47
+ }
48
+ export interface AskResolver {
49
+ /**
50
+ * Whether a human can answer at all (ADR-030, amended for P4): an interaction
51
+ * channel exists for this run. Absent means false. Only the escalate Bash
52
+ * DESCRIPTION reads it; the verdict never depends on it.
53
+ */
54
+ readonly humanReachable?: boolean;
55
+ resolve(req: AskRequest, control?: AskControl): Promise<AskVerdict>;
56
+ }
57
+ /**
58
+ * Compose links into a resolver. First non-abstain wins.
59
+ *
60
+ * A throwing link abstains rather than propagating: `runtime.callTool` turns an
61
+ * exception out of the resolver into a TOOL ERROR surfaced to the model, not a
62
+ * denial, which would lose the `denied:ask` ledger row and hand the agent
63
+ * something it may retry around.
64
+ *
65
+ * US-003: forwards the same `control` object to every link so a turn-cancel
66
+ * signal reaches whichever link would have answered, not just the first one
67
+ * that asks.
68
+ */
69
+ export declare function chainAskLinks(links: readonly AskLink[]): AskResolver;