@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,26 @@
1
+ /**
2
+ * Keyed credential fingerprints (US-002).
3
+ *
4
+ * A fingerprint lets a run report "the credential this call was billed to has
5
+ * changed" without ever logging the credential: the first 12 lowercase hex
6
+ * characters of HMAC-SHA-256 over the credential's identifying secret, keyed by
7
+ * a per-machine salt held in `<configDir>/auth-fingerprint-salt`.
8
+ *
9
+ * The salt is what makes the digest useless as a lookup key, and it is per
10
+ * machine by design — a fingerprint cannot be compared across machines. It is
11
+ * published atomically at mode 0600, so a writer that loses the creation race
12
+ * reads the winner's complete file instead of overwriting it.
13
+ */
14
+ import type { StoredCredential } from "@nathapp/nax-ai";
15
+ /** Clears the salt memo and the warn-once set. Tests only. */
16
+ export declare function _resetFingerprintSalt(): void;
17
+ /**
18
+ * The first 12 lowercase hex characters of HMAC-SHA-256 over the credential's
19
+ * identifying secret, keyed by the machine salt.
20
+ *
21
+ * `api-key` fingerprints its `key` as written — a `$VAR` or `!command` template
22
+ * is fingerprinted as the template, because nax-ai documents `key` as opaque.
23
+ * `oauth` fingerprints its `refresh` token, so access-token rotation is
24
+ * invisible while refresh-token rotation is not.
25
+ */
26
+ export declare function fingerprintCredential(credential: StoredCredential): Promise<string>;
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Keyed credential fingerprints (US-002).
3
+ *
4
+ * A fingerprint lets a run report "the credential this call was billed to has
5
+ * changed" without ever logging the credential: the first 12 lowercase hex
6
+ * characters of HMAC-SHA-256 over the credential's identifying secret, keyed by
7
+ * a per-machine salt held in `<configDir>/auth-fingerprint-salt`.
8
+ *
9
+ * The salt is what makes the digest useless as a lookup key, and it is per
10
+ * machine by design — a fingerprint cannot be compared across machines. It is
11
+ * published atomically at mode 0600, so a writer that loses the creation race
12
+ * reads the winner's complete file instead of overwriting it.
13
+ */
14
+ import { createHmac, randomBytes, randomUUID } from "node:crypto";
15
+ import { link, mkdir, readFile, rm, writeFile } from "node:fs/promises";
16
+ import { dirname, join } from "node:path";
17
+ import { errorMessage } from "#src/infra/errors";
18
+ import { credentialsConfig, getSafeLogger } from "#src/infra/index";
19
+ /** Exactly one HMAC key's worth. Anything shorter is a damaged file, not a salt. */
20
+ const SALT_BYTES = 32;
21
+ const SALT_FILENAME = "auth-fingerprint-salt";
22
+ /** Lowercase hex characters retained from the digest. */
23
+ const FINGERPRINT_CHARS = 12;
24
+ /**
25
+ * Salt memo, and the salt-file path it belongs to: `NAX_GLOBAL_CONFIG_DIR`
26
+ * moves between tests, and a salt cached for a stale path would make every
27
+ * fingerprint in the new directory wrong.
28
+ */
29
+ let memoPath;
30
+ let memoSalt;
31
+ /** Salt-file paths already reported invalid, so the event is emitted once per file. */
32
+ const warnedInvalidPaths = new Set();
33
+ /** Clears the salt memo and the warn-once set. Tests only. */
34
+ export function _resetFingerprintSalt() {
35
+ memoPath = undefined;
36
+ memoSalt = undefined;
37
+ warnedInvalidPaths.clear();
38
+ }
39
+ /** Absolute path of the per-machine salt file. */
40
+ function saltFilePath() {
41
+ return join(credentialsConfig().configDir(), SALT_FILENAME);
42
+ }
43
+ /** Read the salt file. `undefined` when it does not exist. */
44
+ async function readSaltFile(path) {
45
+ try {
46
+ return await readFile(path);
47
+ }
48
+ catch (error) {
49
+ if (error.code === "ENOENT")
50
+ return undefined;
51
+ throw error;
52
+ }
53
+ }
54
+ /**
55
+ * Create the salt file with its bytes already in place, or read the winner's
56
+ * when another writer beat us to it.
57
+ *
58
+ * `open(path, "wx")` publishes the name before the payload lands, so a process
59
+ * that lost the creation race could read a 0-byte file, call it invalid and
60
+ * fingerprint with an in-memory salt nobody else on the machine shares. Staging
61
+ * the bytes and hard-linking them into place publishes the name only once the
62
+ * content exists: `link` fails with EEXIST when another writer got there first,
63
+ * and a hard link shares the inode, mode 0600 included.
64
+ *
65
+ * The staging name is unique per attempt, so two concurrent callers inside one
66
+ * process cannot overwrite each other's bytes.
67
+ */
68
+ async function createSaltFile(path) {
69
+ // A fresh machine has no global config dir yet, and an exclusive create cannot
70
+ // make one: an absent parent surfaces as ENOENT, not EEXIST, so the first
71
+ // fingerprint would throw instead of creating the salt.
72
+ await mkdir(dirname(path), { recursive: true });
73
+ const candidate = randomBytes(SALT_BYTES);
74
+ const staged = `${path}.${randomUUID()}.staged`;
75
+ try {
76
+ await writeFile(staged, candidate, { mode: 0o600 });
77
+ try {
78
+ await link(staged, path);
79
+ }
80
+ catch (error) {
81
+ if (error.code === "EEXIST")
82
+ return readSaltFile(path);
83
+ throw error;
84
+ }
85
+ return candidate;
86
+ }
87
+ finally {
88
+ await rm(staged, { force: true });
89
+ }
90
+ }
91
+ /**
92
+ * Load the machine salt, creating it on first use.
93
+ *
94
+ * A file that is not exactly 32 bytes is left untouched: it is replaced by a
95
+ * random in-memory salt for this process, and `credential.salt_invalid` is
96
+ * logged once for that path. Rewriting the file would destroy whatever an
97
+ * operator put there, and reusing a short read would be worse than a fresh key.
98
+ *
99
+ * Any filesystem failure resolves the same way. This runs on the run-start probe
100
+ * (`providersWithoutCredentials`), where a raw `EACCES`/`EISDIR` would escape as a
101
+ * non-`NaxError` and take the whole precheck report down with it.
102
+ *
103
+ * The trade is deliberate and it is a real loss: a random salt breaks the
104
+ * cross-process comparison on THIS machine, which is the one thing a per-machine
105
+ * salt exists to provide (fingerprints still cannot cross machines either way —
106
+ * that is what the salt buys). So a run whose `~/.nax` is unwritable reports
107
+ * `credential.changed`/`credential.renewed` against a baseline no sibling process
108
+ * shares, and its `auth` fingerprints do not line up with the next run's. The
109
+ * errno rides along on the event so an operator can tell a permissions problem
110
+ * from a truncated file, which is the difference between a fixable machine and a
111
+ * mysterious one.
112
+ */
113
+ async function loadSalt(path) {
114
+ let stored;
115
+ let failure;
116
+ try {
117
+ stored = (await readSaltFile(path)) ?? (await createSaltFile(path));
118
+ }
119
+ catch (error) {
120
+ stored = undefined;
121
+ failure = error;
122
+ }
123
+ if (stored !== undefined && stored.byteLength === SALT_BYTES)
124
+ return stored;
125
+ if (!warnedInvalidPaths.has(path)) {
126
+ warnedInvalidPaths.add(path);
127
+ getSafeLogger()?.warn("credentials", "credential.salt_invalid", {
128
+ saltPath: path,
129
+ ...(failure !== undefined
130
+ ? { errno: failure.code, error: errorMessage(failure) }
131
+ : {}),
132
+ });
133
+ }
134
+ return randomBytes(SALT_BYTES);
135
+ }
136
+ /** The salt this process fingerprints with, memoised per salt-file path. */
137
+ async function resolveSalt() {
138
+ const path = saltFilePath();
139
+ if (memoSalt !== undefined && memoPath === path)
140
+ return memoSalt;
141
+ const salt = await loadSalt(path);
142
+ memoPath = path;
143
+ memoSalt = salt;
144
+ return salt;
145
+ }
146
+ /**
147
+ * The part of a credential that identifies it.
148
+ *
149
+ * Exhaustive over the union on purpose: a third `StoredCredential` kind must
150
+ * fail the build here, not silently hash whichever field the `else` branch
151
+ * happened to name.
152
+ */
153
+ function identifyingSecret(credential) {
154
+ switch (credential.kind) {
155
+ case "api-key":
156
+ return credential.key;
157
+ case "oauth":
158
+ return credential.refresh;
159
+ }
160
+ }
161
+ /**
162
+ * The first 12 lowercase hex characters of HMAC-SHA-256 over the credential's
163
+ * identifying secret, keyed by the machine salt.
164
+ *
165
+ * `api-key` fingerprints its `key` as written — a `$VAR` or `!command` template
166
+ * is fingerprinted as the template, because nax-ai documents `key` as opaque.
167
+ * `oauth` fingerprints its `refresh` token, so access-token rotation is
168
+ * invisible while refresh-token rotation is not.
169
+ */
170
+ export async function fingerprintCredential(credential) {
171
+ const salt = await resolveSalt();
172
+ const secret = identifyingSecret(credential);
173
+ return createHmac("sha256", salt).update(secret).digest("hex").slice(0, FINGERPRINT_CHARS);
174
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The exec helper's process boundary (US-003): spawn it, feed it one request
3
+ * line, bound both streams, and report what came back.
4
+ *
5
+ * Nothing here chooses a credential error code — that mapping belongs to
6
+ * exec-source.ts. What this module owns is the deadline, the stdout cap, the
7
+ * bounded drain after the process is gone, and the secrecy rule: stderr is
8
+ * redacted *before* it is truncated, so truncation can never cut a secret into a
9
+ * fragment the redaction patterns no longer recognise.
10
+ */
11
+ /**
12
+ * Hard cap on the helper's stdout. Over it the reply is malformed and the
13
+ * process is killed. Not configurable (US-003 scope).
14
+ */
15
+ export declare const AUTH_HELPER_STDOUT_MAX_BYTES = 65536;
16
+ /** Hard cap on the stderr excerpt that may reach a log line or an error message. */
17
+ export declare const AUTH_HELPER_STDERR_MAX_BYTES = 4096;
18
+ /** The slice of the runtime's spawn result this module uses. */
19
+ interface HelperProcess {
20
+ readonly stdin: {
21
+ write(data: string): unknown;
22
+ end(): unknown;
23
+ };
24
+ readonly stdout: ReadableStream<Uint8Array>;
25
+ readonly stderr: ReadableStream<Uint8Array>;
26
+ readonly exited: Promise<number>;
27
+ readonly exitCode: number | null;
28
+ /**
29
+ * Always SIGKILL. A deadline that a helper can ignore is not a deadline: the
30
+ * call would wait on a process that never exits, and `read` would never settle.
31
+ */
32
+ kill(signal: "SIGKILL"): void;
33
+ }
34
+ declare function spawnHelper(argv: readonly string[]): HelperProcess;
35
+ /**
36
+ * Injectable seam (the `_deps` pattern): every external call this module makes
37
+ * goes through it, so a test can drive the helper without a real process.
38
+ *
39
+ * The spawn deliberately passes no `env`: the child inherits nax's environment,
40
+ * which is what the helper contract promises.
41
+ */
42
+ export declare const _execSourceDeps: {
43
+ spawn: typeof spawnHelper;
44
+ /**
45
+ * Timer pair, wrapped so the global functions are resolved per call — a test
46
+ * that spies on `setTimeout` (the timer-leak helper) still sees these.
47
+ */
48
+ setTimeout: (fn: () => void, ms: number) => unknown;
49
+ clearTimeout: (id: unknown) => void;
50
+ };
51
+ /** What one helper invocation produced, as the process boundary saw it. */
52
+ export type HelperProcessResult = {
53
+ kind: "spawn-failed";
54
+ detail: string;
55
+ cause: unknown;
56
+ stderr: string;
57
+ } | {
58
+ kind: "timed-out";
59
+ detail: string;
60
+ stderr: string;
61
+ } | {
62
+ kind: "stdout-over-cap";
63
+ detail: string;
64
+ stderr: string;
65
+ }
66
+ /** No reply to judge: the process was killed, or its streams failed under us. */
67
+ | {
68
+ kind: "no-answer";
69
+ detail: string;
70
+ stderr: string;
71
+ } | {
72
+ kind: "exited";
73
+ exitCode: number;
74
+ stdout: string;
75
+ stderr: string;
76
+ };
77
+ /** Spawn the helper for `providerId` and report what it produced. Never throws for a helper failure. */
78
+ export declare function runHelper(command: readonly string[], providerId: string, timeoutMs: number): Promise<HelperProcessResult>;
79
+ export {};
@@ -0,0 +1,259 @@
1
+ /**
2
+ * The exec helper's process boundary (US-003): spawn it, feed it one request
3
+ * line, bound both streams, and report what came back.
4
+ *
5
+ * Nothing here chooses a credential error code — that mapping belongs to
6
+ * exec-source.ts. What this module owns is the deadline, the stdout cap, the
7
+ * bounded drain after the process is gone, and the secrecy rule: stderr is
8
+ * redacted *before* it is truncated, so truncation can never cut a secret into a
9
+ * fragment the redaction patterns no longer recognise.
10
+ */
11
+ import { errorMessage } from "#src/infra/errors";
12
+ import { getSafeLogger, NaxError } from "#src/infra/index";
13
+ import { redactSecrets } from "#src/internal/redact";
14
+ import { runtimeSpawn } from "#src/runtime/index";
15
+ import { HELPER_SUBCOMMAND, requestLine } from "./helper-protocol.js";
16
+ /**
17
+ * Hard cap on the helper's stdout. Over it the reply is malformed and the
18
+ * process is killed. Not configurable (US-003 scope).
19
+ */
20
+ export const AUTH_HELPER_STDOUT_MAX_BYTES = 65_536;
21
+ /** Hard cap on the stderr excerpt that may reach a log line or an error message. */
22
+ export const AUTH_HELPER_STDERR_MAX_BYTES = 4_096;
23
+ /**
24
+ * How much stderr is collected before further bytes are dropped. Redaction runs
25
+ * over the whole collected prefix and only AUTH_HELPER_STDERR_MAX_BYTES of the
26
+ * result is kept, so the 8× margin leaves a secret that straddles the
27
+ * truncation point fully visible to the redaction patterns.
28
+ */
29
+ const AUTH_HELPER_STDERR_COLLECT_MAX_BYTES = AUTH_HELPER_STDERR_MAX_BYTES * 8;
30
+ /**
31
+ * How long the readers may keep waiting once the process is gone. A helper's
32
+ * pipes close the instant it exits; only an inherited pipe — a descendant of a
33
+ * killed helper, such as the `sleep` or the network client it shelled out to —
34
+ * outlives it, and that one will never end on its own.
35
+ */
36
+ const DRAIN_GRACE_MS = 500;
37
+ /** A piped spawn always has stdin; a runtime that broke that is a spawn failure, caught by the caller. */
38
+ function hasStdin(proc) {
39
+ return proc.stdin !== undefined;
40
+ }
41
+ function spawnHelper(argv) {
42
+ // nax-git-env-allow: caller-supplied helper argv, not git; the child inherits nax's env by contract
43
+ const proc = runtimeSpawn(argv, { stdin: "pipe", stdout: "pipe", stderr: "pipe" });
44
+ if (!hasStdin(proc)) {
45
+ throw new NaxError("[credentials] the runtime returned no stdin for a piped spawn", "CREDENTIAL_HELPER_FAILED", {
46
+ stage: "credentials",
47
+ });
48
+ }
49
+ return proc;
50
+ }
51
+ /**
52
+ * Injectable seam (the `_deps` pattern): every external call this module makes
53
+ * goes through it, so a test can drive the helper without a real process.
54
+ *
55
+ * The spawn deliberately passes no `env`: the child inherits nax's environment,
56
+ * which is what the helper contract promises.
57
+ */
58
+ export const _execSourceDeps = {
59
+ spawn: spawnHelper,
60
+ /**
61
+ * Timer pair, wrapped so the global functions are resolved per call — a test
62
+ * that spies on `setTimeout` (the timer-leak helper) still sees these.
63
+ */
64
+ setTimeout: ((fn, ms) => setTimeout(fn, ms)),
65
+ clearTimeout: ((id) => clearTimeout(id)),
66
+ };
67
+ /**
68
+ * Read a stream to EOF, decoding UTF-8.
69
+ *
70
+ * Reads run concurrently with the exit wait: a child that fills the OS pipe
71
+ * buffer before anyone reads it would otherwise block on its own write and never
72
+ * reach `exited`, which would leave the timeout as the only way out.
73
+ *
74
+ * Past `cap` the two callers differ. With `onExceeded` the stream is abandoned —
75
+ * the callback kills the process, so there is nothing left to drain. Without it
76
+ * the stream is still drained (a full pipe must not wedge the child) but nothing
77
+ * more is kept.
78
+ */
79
+ async function readCapped(stream, cap, options = {}) {
80
+ const reader = stream.getReader();
81
+ const decoder = new TextDecoder();
82
+ let text = "";
83
+ let bytes = 0;
84
+ let exceeded = false;
85
+ /** Abort closes the stream, which resolves the pending `read()` with `done`. */
86
+ const onAbort = () => {
87
+ reader.cancel().catch(() => { });
88
+ };
89
+ options.signal?.addEventListener("abort", onAbort, { once: true });
90
+ try {
91
+ while (true) {
92
+ const { done, value } = await reader.read();
93
+ if (done === true)
94
+ break;
95
+ if (value === undefined)
96
+ continue;
97
+ bytes += value.byteLength;
98
+ if (bytes > cap && !exceeded) {
99
+ exceeded = true;
100
+ if (options.onExceeded !== undefined) {
101
+ options.onExceeded();
102
+ // The process is dying; do not wait for its pipe to notice.
103
+ await reader.cancel().catch(() => { });
104
+ break;
105
+ }
106
+ }
107
+ if (!exceeded)
108
+ text += decoder.decode(value, { stream: true });
109
+ }
110
+ }
111
+ catch {
112
+ // The pipe closed under us — a kill, an abort, or a child that exited
113
+ // mid-write. Whatever was collected before that still stands.
114
+ }
115
+ finally {
116
+ options.signal?.removeEventListener("abort", onAbort);
117
+ }
118
+ if (!exceeded)
119
+ text += decoder.decode();
120
+ return { text, exceeded };
121
+ }
122
+ /** Write the one-line request, then close stdin so the helper sees EOF. */
123
+ function writeRequest(proc, providerId) {
124
+ try {
125
+ proc.stdin.write(requestLine(providerId));
126
+ proc.stdin.end();
127
+ }
128
+ catch (cause) {
129
+ // A helper that exits without draining stdin surfaces EPIPE here. Its exit
130
+ // code is the verdict, so this is a diagnostic, not a failure of its own.
131
+ getSafeLogger()?.debug("credentials", "credential.helper_stdin_failed", {
132
+ providerId,
133
+ error: errorMessage(cause),
134
+ });
135
+ }
136
+ }
137
+ /**
138
+ * Collect both streams, bounded by DRAIN_GRACE_MS once the process has exited.
139
+ *
140
+ * A helper's pipes close as it exits — unless it left a descendant holding them,
141
+ * and a killed helper leaves exactly that whenever it shelled out. That pipe
142
+ * never closes on its own, so it must not be allowed to hold the read past the
143
+ * deadline the source promises: whatever arrived within the grace is kept, and
144
+ * the rest is abandoned.
145
+ */
146
+ async function drainStreams(streams) {
147
+ let graceTimer;
148
+ const grace = new Promise((resolve) => {
149
+ graceTimer = _execSourceDeps.setTimeout(() => resolve(null), DRAIN_GRACE_MS);
150
+ });
151
+ try {
152
+ const drained = await Promise.race([Promise.all([streams.stdout, streams.stderr]), grace]);
153
+ if (drained !== null)
154
+ return drained;
155
+ for (const controller of streams.controllers)
156
+ controller.abort();
157
+ return await Promise.all([streams.stdout, streams.stderr]);
158
+ }
159
+ finally {
160
+ // Cleared on both paths: an armed timer outliving the call keeps the event
161
+ // loop alive after the read has already resolved.
162
+ _execSourceDeps.clearTimeout(graceTimer);
163
+ }
164
+ }
165
+ /**
166
+ * SIGKILL a helper, tolerating a child that is already gone.
167
+ *
168
+ * A deadline that a helper can ignore is not a deadline: the call would wait on
169
+ * a process that never exits, and `read` would never settle. The `try` is for the
170
+ * race where the child is reaped between the decision to kill it and the signal.
171
+ */
172
+ function killHelper(proc) {
173
+ try {
174
+ proc.kill("SIGKILL");
175
+ }
176
+ catch {
177
+ // The child was already gone; its exit code decides the outcome either way.
178
+ }
179
+ }
180
+ /** Run one helper process to its end, killing it if either kill switch trips first. */
181
+ async function runHelperProcess(proc, providerId, timeoutMs) {
182
+ // Whichever trips first decides the outcome; both kill the process.
183
+ let trip;
184
+ const timer = _execSourceDeps.setTimeout(() => {
185
+ if (trip === undefined)
186
+ trip = "timeout";
187
+ killHelper(proc);
188
+ }, timeoutMs);
189
+ const stdoutController = new AbortController();
190
+ const stderrController = new AbortController();
191
+ try {
192
+ writeRequest(proc, providerId);
193
+ // Reading starts here, concurrently with the exit wait below.
194
+ const streams = {
195
+ stdout: readCapped(proc.stdout, AUTH_HELPER_STDOUT_MAX_BYTES, {
196
+ signal: stdoutController.signal,
197
+ onExceeded: () => {
198
+ if (trip === undefined)
199
+ trip = "cap";
200
+ killHelper(proc);
201
+ },
202
+ }),
203
+ stderr: readCapped(proc.stderr, AUTH_HELPER_STDERR_COLLECT_MAX_BYTES, { signal: stderrController.signal }),
204
+ controllers: [stdoutController, stderrController],
205
+ };
206
+ await proc.exited;
207
+ const [stdout, stderr] = await drainStreams(streams);
208
+ return { trip, exitCode: proc.exitCode, stdout, stderr: stderr.text };
209
+ }
210
+ finally {
211
+ // Cleared however the call ends, including the timeout: a timer left armed
212
+ // holds the event loop open after the read has already resolved.
213
+ _execSourceDeps.clearTimeout(timer);
214
+ }
215
+ }
216
+ /** Spawn the helper for `providerId` and report what it produced. Never throws for a helper failure. */
217
+ export async function runHelper(command, providerId, timeoutMs) {
218
+ let proc;
219
+ try {
220
+ // nax-git-env-allow: caller-supplied helper argv, not git; the child inherits nax's env by contract
221
+ proc = _execSourceDeps.spawn([...command, HELPER_SUBCOMMAND]);
222
+ }
223
+ catch (cause) {
224
+ // A missing binary never reaches `exited`: the runtime throws ENOENT from spawn.
225
+ return { kind: "spawn-failed", detail: `could not be started: ${errorMessage(cause)}`, cause, stderr: "" };
226
+ }
227
+ let run;
228
+ try {
229
+ run = await runHelperProcess(proc, providerId, timeoutMs);
230
+ }
231
+ catch (cause) {
232
+ // The streams or the exit wait failed under us, so nothing was learned about
233
+ // the provider. The child is NOT necessarily dead, though: the `finally` in
234
+ // runHelperProcess has already cleared the deadline timer that would have
235
+ // killed it, and that call's abort controllers died with the throw. Left
236
+ // alone it runs until it chooses to stop, holding two open pipes. Kill it
237
+ // and drop the readers here, where `proc` is still in scope.
238
+ killHelper(proc);
239
+ await Promise.allSettled([proc.stdout.cancel(), proc.stderr.cancel()]);
240
+ return { kind: "no-answer", detail: `could not be read: ${errorMessage(cause)}`, stderr: "" };
241
+ }
242
+ // Redact first, truncate second. The other order can cut a secret in half and
243
+ // leave a fragment no redaction pattern can recognise.
244
+ const stderr = redactSecrets(run.stderr).slice(0, AUTH_HELPER_STDERR_MAX_BYTES);
245
+ if (run.trip === "cap") {
246
+ return {
247
+ kind: "stdout-over-cap",
248
+ detail: `wrote more than ${AUTH_HELPER_STDOUT_MAX_BYTES} bytes to stdout`,
249
+ stderr,
250
+ };
251
+ }
252
+ if (run.trip === "timeout") {
253
+ return { kind: "timed-out", detail: `did not answer within ${timeoutMs}ms`, stderr };
254
+ }
255
+ if (run.exitCode === null) {
256
+ return { kind: "no-answer", detail: "was killed before it could answer", stderr };
257
+ }
258
+ return { kind: "exited", exitCode: run.exitCode, stdout: run.stdout.text, stderr };
259
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The exec helper's wire contract (US-003): one request line in, one reply line
3
+ * out.
4
+ *
5
+ * Held apart from the process boundary (helper-process.ts) and the source itself
6
+ * (exec-source.ts) because this is the part koda implements against, and because
7
+ * every rule here is fail-closed: anything this module cannot name with certainty
8
+ * is invalid, never a best-effort read of the nearest field.
9
+ */
10
+ /** The request/reply protocol version this source speaks. */
11
+ export declare const REQUEST_VERSION = 1;
12
+ /** The subcommand appended to the configured command. */
13
+ export declare const HELPER_SUBCOMMAND = "get";
14
+ /**
15
+ * One credential as the helper reported it. `expiresAt` absent means the lease
16
+ * is good for the life of the process; `account` is a label, never a secret.
17
+ */
18
+ export interface Lease {
19
+ key: string;
20
+ expiresAt?: number;
21
+ account?: string;
22
+ }
23
+ /** What a helper's stdout said. */
24
+ export type ParsedReply = {
25
+ kind: "credential";
26
+ lease: Lease;
27
+ } | {
28
+ kind: "decline";
29
+ } | {
30
+ kind: "invalid";
31
+ detail: string;
32
+ };
33
+ /** The one JSON line written to the helper's stdin. */
34
+ export declare function requestLine(providerId: string): string;
35
+ /** Validate one credential or decline reply, fail-closed. */
36
+ export declare function parseReply(stdout: string): ParsedReply;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The exec helper's wire contract (US-003): one request line in, one reply line
3
+ * out.
4
+ *
5
+ * Held apart from the process boundary (helper-process.ts) and the source itself
6
+ * (exec-source.ts) because this is the part koda implements against, and because
7
+ * every rule here is fail-closed: anything this module cannot name with certainty
8
+ * is invalid, never a best-effort read of the nearest field.
9
+ */
10
+ /** The request/reply protocol version this source speaks. */
11
+ export const REQUEST_VERSION = 1;
12
+ /** The subcommand appended to the configured command. */
13
+ export const HELPER_SUBCOMMAND = "get";
14
+ /** A longer credential is a malformed reply, not a key to try. */
15
+ const MAX_KEY_CHARS = 8_192;
16
+ /** The non-secret account label's length cap. */
17
+ const MAX_ACCOUNT_CHARS = 200;
18
+ /** The one JSON line written to the helper's stdin. */
19
+ export function requestLine(providerId) {
20
+ return `${JSON.stringify({ version: REQUEST_VERSION, providerId })}\n`;
21
+ }
22
+ /** Validate one credential or decline reply, fail-closed. */
23
+ export function parseReply(stdout) {
24
+ const invalid = (detail) => ({ kind: "invalid", detail });
25
+ let parsed;
26
+ try {
27
+ parsed = JSON.parse(stdout);
28
+ }
29
+ catch {
30
+ // The parser's message quotes the text it failed on, so it carries a fragment
31
+ // of stdout — which is where the key lives. The reply is malformed either
32
+ // way, and the caller has the code; naming the parser's text would put a
33
+ // secret in an error message, and from there in the CLI and the precheck
34
+ // report. See the Secrets rule in docs/specs/SPEC-credential-sources.md.
35
+ return invalid("stdout was not JSON");
36
+ }
37
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
38
+ return invalid("the reply was not a JSON object");
39
+ }
40
+ const reply = parsed;
41
+ // Neither `version` nor `kind` is echoed back: both are helper-controlled and
42
+ // unvalidated, so `JSON.stringify` on one would copy an arbitrarily large
43
+ // subtree of stdout — which is where the key is — into the message. Naming the
44
+ // field and the expected value is enough to debug a helper.
45
+ if (reply.version !== REQUEST_VERSION) {
46
+ return invalid(`version was not ${REQUEST_VERSION}`);
47
+ }
48
+ // The decline reply carries no kind, so it is recognised before the credential
49
+ // fields are required.
50
+ if (reply.decline === true)
51
+ return { kind: "decline" };
52
+ if (reply.kind !== "api-key")
53
+ return invalid('kind was not "api-key"');
54
+ const key = reply.key;
55
+ if (typeof key !== "string" || key.length === 0)
56
+ return invalid("the reply carried no key");
57
+ if (key.length > MAX_KEY_CHARS)
58
+ return invalid(`the key was longer than ${MAX_KEY_CHARS} characters`);
59
+ const account = reply.account;
60
+ if (account !== undefined && (typeof account !== "string" || account.length > MAX_ACCOUNT_CHARS)) {
61
+ return invalid(`the account label was not a string of at most ${MAX_ACCOUNT_CHARS} characters`);
62
+ }
63
+ const expiresAt = reply.expiresAt;
64
+ if (expiresAt !== undefined) {
65
+ if (typeof expiresAt !== "number" || !Number.isSafeInteger(expiresAt) || expiresAt <= 0) {
66
+ return invalid("expiresAt was not a positive integer");
67
+ }
68
+ if (expiresAt < Date.now())
69
+ return invalid("expiresAt was already past on receipt");
70
+ }
71
+ return {
72
+ kind: "credential",
73
+ lease: {
74
+ key,
75
+ ...(typeof account === "string" ? { account } : {}),
76
+ ...(typeof expiresAt === "number" ? { expiresAt } : {}),
77
+ },
78
+ };
79
+ }