@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,88 @@
1
+ /**
2
+ * One-shot completion over nax-ai, with no session (S1 port 2). Package-owned
3
+ * options: the caller passes a `SessionModel` whose pricing is already in the
4
+ * standard vocabulary, and the per-adapter one-shot session key.
5
+ */
6
+ import { priceCall } from "#src/cost/core/index";
7
+ import { _adapterDeps, authFields, isProtocolStreamError } from "./adapter-deps.js";
8
+ import { getNativeClient } from "./client.js";
9
+ import { toAdapterFailure } from "./errors.js";
10
+ import { buildRateCard, parseNativeModel, toThinkingLevel } from "./models.js";
11
+ import { nativeSessionId } from "./session-affinity.js";
12
+ export async function nativeComplete(prompt, options, context) {
13
+ // modelDef.provider is deliberately ignored. resolveModel() INFERS it from
14
+ // the model name for string entries ("claude..." -> anthropic, else
15
+ // "unknown"), so it is a guess rather than configuration — and routing a
16
+ // billed call on a guess is what the protocol gate exists to prevent. The
17
+ // string is the only source of truth.
18
+ const { provider, model, effort } = parseNativeModel(options.model.model);
19
+ const thinking = toThinkingLevel(effort);
20
+ const client = await getNativeClient(context.catalogOverrides);
21
+ const resolved = await client.model(provider, model);
22
+ const controller = new AbortController();
23
+ const timer = options.timeoutMs !== undefined ? _adapterDeps.setTimeout(() => controller.abort(), options.timeoutMs) : undefined;
24
+ try {
25
+ const sessionId = nativeSessionId(context.sessionKey);
26
+ const result = await client.complete(resolved, {
27
+ messages: [{ role: "user", content: prompt }],
28
+ ...(options.maxTokens !== undefined ? { maxTokens: options.maxTokens } : {}),
29
+ sessionId,
30
+ signal: controller.signal,
31
+ ...(thinking !== undefined ? { thinking } : {}),
32
+ });
33
+ const tokenUsage = result.usage;
34
+ const catalog = client.pricing(resolved);
35
+ const { rates, source: pricingSource } = buildRateCard(catalog, options.model.pricing);
36
+ // US-002: stamp the effective `priceCall`-resolved rates so the cost
37
+ // subscriber can record the same numbers whose arithmetic reproduces
38
+ // `estimatedCostUsd`. The native path prices unconditionally — even
39
+ // a zero-token call carries `rates`, so a downstream cost row never
40
+ // has to guess what priced a no-spend dispatch.
41
+ //
42
+ // Single `priceCall` invocation: `costUsd` and `resolvedRates` come
43
+ // from the same call so they cannot diverge — the verifiability
44
+ // property the story names ("recorded rates reproduce recorded cost")
45
+ // would silently break if tier selection ever grew a side channel.
46
+ const { costUsd: estimatedCostUsd, resolvedRates } = priceCall(tokenUsage, rates);
47
+ return {
48
+ output: result.text,
49
+ tokenUsage,
50
+ estimatedCostUsd,
51
+ // exactCostUsd is deliberately unset: nax-ai supplies rates and
52
+ // computes no cost, so nothing here is exact.
53
+ // sessionId echoes the one we sent — US-002 lets downstream wiring
54
+ // (audit, dispatch) stamp it on artifacts without reaching into a
55
+ // private field.
56
+ sessionId,
57
+ // US-003: stamp the branch buildRateCard took so cost rows can tell a
58
+ // catalog-priced call from a config-overridden one. Single source of
59
+ // truth — the same override !== undefined predicate the rate card was
60
+ // chosen on, reported rather than re-derived by the cost subscriber.
61
+ pricingSource,
62
+ // US-002: the four per-1M rates that priced this call. See
63
+ // NativeCompleteResult.rates above.
64
+ rates: resolvedRates,
65
+ // US-006: the credential identity the store observed while serving
66
+ // this call. Absent (not undefined) when it observed nothing.
67
+ ...authFields(provider),
68
+ };
69
+ }
70
+ catch (err) {
71
+ // Returned, not rethrown: rethrowing routes through
72
+ // classifyCompleteException -> parseAgentError, which parses ACP strings
73
+ // and would discard the typed kind nax-ai just gave us.
74
+ if (isProtocolStreamError(err)) {
75
+ return {
76
+ output: "",
77
+ tokenUsage: { inputTokens: 0, outputTokens: 0 },
78
+ estimatedCostUsd: 0,
79
+ adapterFailure: toAdapterFailure(err.protocolError),
80
+ };
81
+ }
82
+ throw err;
83
+ }
84
+ finally {
85
+ if (timer !== undefined)
86
+ _adapterDeps.clearTimeout(timer);
87
+ }
88
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The chained credential store (US-004).
3
+ *
4
+ * nax-ai's file store re-reads `~/.nax/credentials` on every read, so a silent
5
+ * mid-run change can split billing across accounts. This is the seam that
6
+ * decides which source serves a provider — the exec helper first when one is
7
+ * configured, the file store otherwise — and remembers, per provider, which one
8
+ * answered.
9
+ *
10
+ * The memory matters because a provider the helper served is the helper's to
11
+ * manage: writing it to the file store would split one credential across two
12
+ * stores, and OAuth refresh must keep going through the file store so its
13
+ * cross-process lock and write-back still apply.
14
+ */
15
+ import type { CredentialStore, ProviderId } from "@nathapp/nax-ai";
16
+ import type { ExecCredentialSource } from "./exec-source.ts";
17
+ /** The store the run is assembled around: a chain, plus where each provider came from. */
18
+ export interface ChainedCredentialStore extends CredentialStore {
19
+ /** Which store last served `providerId`, or `undefined` if it was never read. */
20
+ sourceOf(providerId: ProviderId): "exec" | "file" | undefined;
21
+ /** The account label the exec source reported for `providerId`, if it served it. */
22
+ accountOf(providerId: ProviderId): string | undefined;
23
+ }
24
+ export interface ChainedCredentialStoreOptions {
25
+ /** Tried first, when configured. A decline falls through to the file store. */
26
+ readonly exec?: ExecCredentialSource;
27
+ /** The store `~/.nax/credentials` back, and the only one that can be written. */
28
+ readonly file: CredentialStore;
29
+ }
30
+ export declare function createChainedCredentialStore(options: ChainedCredentialStoreOptions): ChainedCredentialStore;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The chained credential store (US-004).
3
+ *
4
+ * nax-ai's file store re-reads `~/.nax/credentials` on every read, so a silent
5
+ * mid-run change can split billing across accounts. This is the seam that
6
+ * decides which source serves a provider — the exec helper first when one is
7
+ * configured, the file store otherwise — and remembers, per provider, which one
8
+ * answered.
9
+ *
10
+ * The memory matters because a provider the helper served is the helper's to
11
+ * manage: writing it to the file store would split one credential across two
12
+ * stores, and OAuth refresh must keep going through the file store so its
13
+ * cross-process lock and write-back still apply.
14
+ */
15
+ import { NaxError } from "#src/infra/index";
16
+ export function createChainedCredentialStore(options) {
17
+ /** Providers this process has read, and the source that answered. */
18
+ const served = new Map();
19
+ /** The refusal for a write against a provider the helper owns. */
20
+ function managedByHelper(providerId, operation) {
21
+ return new NaxError(`[credentials] Cannot ${operation} the credential for ${providerId}: the exec helper owns it.`, "CREDENTIAL_MANAGED_BY_HELPER", { stage: "credentials", providerId, operation });
22
+ }
23
+ /** Read the file store, reporting a failure under one code. */
24
+ async function readFileStore(providerId) {
25
+ try {
26
+ return await options.file.read(providerId);
27
+ }
28
+ catch (cause) {
29
+ const error = new NaxError(`[credentials] The credential file could not be read for ${providerId}.`, "CREDENTIAL_FILE_UNREADABLE", { stage: "credentials", providerId, cause });
30
+ // Also on the error itself: `context.cause` is where nax puts a cause, and
31
+ // `Error.cause` is what a generic cause-chain walk (nax-ai's ProtocolError,
32
+ // the credential-fault classifier) reads. Both spellings, one error.
33
+ error.cause = cause;
34
+ throw error;
35
+ }
36
+ }
37
+ return {
38
+ async read(providerId) {
39
+ if (options.exec !== undefined) {
40
+ const credential = await options.exec.read(providerId);
41
+ if (credential !== undefined) {
42
+ served.set(providerId, "exec");
43
+ return credential;
44
+ }
45
+ }
46
+ const credential = await readFileStore(providerId);
47
+ // Recorded even when the file store held nothing: the decline is what
48
+ // makes the file store this provider's source for the process's lifetime.
49
+ served.set(providerId, "file");
50
+ return credential;
51
+ },
52
+ // Both refusals are synchronous throws, not rejections: the store is asked
53
+ // to write and answers "no" without doing anything, and a caller that only
54
+ // holds the returned promise would otherwise see an unhandled rejection.
55
+ modify(providerId, fn) {
56
+ if (served.get(providerId) === "exec")
57
+ throw managedByHelper(providerId, "modify");
58
+ return options.file.modify(providerId, fn);
59
+ },
60
+ delete(providerId) {
61
+ if (served.get(providerId) === "exec")
62
+ throw managedByHelper(providerId, "delete");
63
+ return options.file.delete(providerId);
64
+ },
65
+ sourceOf: (providerId) => served.get(providerId),
66
+ accountOf: (providerId) => (served.get(providerId) === "exec" ? options.exec?.accountOf(providerId) : undefined),
67
+ };
68
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Credential change guard (US-002).
3
+ *
4
+ * A running nax re-reads `~/.nax/credentials` on every LLM request, so one
5
+ * story can be billed to two accounts with nothing in the run log to show it.
6
+ * The guard observes each read, compares the credential's identity with the one
7
+ * it last served, and either warns or refuses — never logging the credential
8
+ * itself, and never comparing the volatile parts of an OAuth credential.
9
+ *
10
+ * `read` is the only method wrapped. `modify` and `delete` pass straight
11
+ * through: a write is not a change of who is being billed.
12
+ */
13
+ import type { CredentialStore, ProviderId } from "@nathapp/nax-ai";
14
+ import type { CredentialAuthConfig } from "#src/infra/index";
15
+ import type { AuthStamp } from "#src/session/session-types";
16
+ /**
17
+ * Where a credential came from, as reported by `describe`. `AuthStamp` without
18
+ * the fingerprint — the guard computes that itself from the credential.
19
+ */
20
+ export interface CredentialOrigin {
21
+ source: "file" | "exec";
22
+ account?: string;
23
+ }
24
+ export interface ChangeGuardOptions {
25
+ /** `warn` adopts the new identity and serves it; `refuse` throws instead. */
26
+ onChange: CredentialAuthConfig["onChange"];
27
+ /**
28
+ * Where the credential read from `inner` came from. Called immediately after
29
+ * each successful read, because the credential itself carries no provenance.
30
+ * `undefined` means the source could not be described — recorded as `file`.
31
+ */
32
+ describe(providerId: ProviderId): CredentialOrigin | undefined;
33
+ }
34
+ /** The credential store, with the identity of whatever it last served. */
35
+ export interface GuardedCredentialStore extends CredentialStore {
36
+ /**
37
+ * The last identity observed for `providerId`, or `undefined` if it was never
38
+ * read. Updated on every read, including one that logged nothing because the
39
+ * fingerprint was unchanged.
40
+ */
41
+ servedAuth(providerId: ProviderId): AuthStamp | undefined;
42
+ }
43
+ /**
44
+ * Wrap `inner` in a change guard.
45
+ *
46
+ * The guard keeps one identity per provider for the life of the process. Under
47
+ * `onChange: "refuse"` a changed credential is never adopted, so the provider
48
+ * stays refused until the credential is restored — every later read refuses too,
49
+ * which is the point: the run must not quietly continue on a different account.
50
+ */
51
+ export declare function createChangeGuard(inner: CredentialStore, options: ChangeGuardOptions): GuardedCredentialStore;
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Credential change guard (US-002).
3
+ *
4
+ * A running nax re-reads `~/.nax/credentials` on every LLM request, so one
5
+ * story can be billed to two accounts with nothing in the run log to show it.
6
+ * The guard observes each read, compares the credential's identity with the one
7
+ * it last served, and either warns or refuses — never logging the credential
8
+ * itself, and never comparing the volatile parts of an OAuth credential.
9
+ *
10
+ * `read` is the only method wrapped. `modify` and `delete` pass straight
11
+ * through: a write is not a change of who is being billed.
12
+ */
13
+ import { getSafeLogger, NaxError } from "#src/infra/index";
14
+ import { fingerprintCredential } from "./fingerprint.js";
15
+ /**
16
+ * `api-key` → `api-key`, both carrying an account label, different labels: a
17
+ * different account is serving the same provider, whichever way the key moved.
18
+ */
19
+ function accountLabelChanged(previous, next) {
20
+ return (previous.kind === "api-key" &&
21
+ next.kind === "api-key" &&
22
+ previous.account !== undefined &&
23
+ next.account !== undefined &&
24
+ previous.account !== next.account);
25
+ }
26
+ /**
27
+ * Classify a read against the stored identity.
28
+ *
29
+ * `renewed` is the "this is routine" verdict: an OAuth refresh-token rotation,
30
+ * which any nax process on the machine produces, or a rotating `api-key` whose
31
+ * account label is unchanged (the exec helper hands out short-lived keys). It is
32
+ * deliberately not a change — refusing it would break normal operation.
33
+ */
34
+ function classify(previous, next) {
35
+ if (accountLabelChanged(previous, next))
36
+ return "changed";
37
+ if (previous.fingerprint === next.fingerprint)
38
+ return "same";
39
+ if (previous.kind !== next.kind)
40
+ return "changed";
41
+ if (next.kind === "oauth")
42
+ return "renewed";
43
+ if (previous.account !== undefined && next.account !== undefined)
44
+ return "renewed";
45
+ return "changed";
46
+ }
47
+ /** The identity fields every credential event carries. */
48
+ function identityData(providerId, identity) {
49
+ return {
50
+ providerId,
51
+ kind: identity.kind,
52
+ source: identity.source,
53
+ fingerprint: identity.fingerprint,
54
+ ...(identity.account !== undefined ? { account: identity.account } : {}),
55
+ };
56
+ }
57
+ /** The identity fields plus what it replaced — for `changed` and `renewed`. */
58
+ function replacementData(providerId, identity, previous) {
59
+ return {
60
+ ...identityData(providerId, identity),
61
+ previousFingerprint: previous.fingerprint,
62
+ ...(previous.account !== undefined ? { previousAccount: previous.account } : {}),
63
+ };
64
+ }
65
+ function toStamp(identity) {
66
+ return {
67
+ fingerprint: identity.fingerprint,
68
+ source: identity.source,
69
+ ...(identity.account !== undefined ? { account: identity.account } : {}),
70
+ };
71
+ }
72
+ /**
73
+ * Wrap `inner` in a change guard.
74
+ *
75
+ * The guard keeps one identity per provider for the life of the process. Under
76
+ * `onChange: "refuse"` a changed credential is never adopted, so the provider
77
+ * stays refused until the credential is restored — every later read refuses too,
78
+ * which is the point: the run must not quietly continue on a different account.
79
+ */
80
+ export function createChangeGuard(inner, options) {
81
+ const identities = new Map();
82
+ async function read(providerId) {
83
+ const credential = await inner.read(providerId);
84
+ // An absent credential says nothing about identity; leave the stored one be.
85
+ if (credential === undefined)
86
+ return undefined;
87
+ const described = options.describe(providerId);
88
+ const identity = {
89
+ kind: credential.kind,
90
+ fingerprint: await fingerprintCredential(credential),
91
+ source: described?.source ?? "file",
92
+ ...(described?.account !== undefined ? { account: described.account } : {}),
93
+ };
94
+ const previous = identities.get(providerId);
95
+ if (previous === undefined) {
96
+ getSafeLogger()?.info("credentials", "credential.resolved", identityData(providerId, identity));
97
+ identities.set(providerId, identity);
98
+ return credential;
99
+ }
100
+ const verdict = classify(previous, identity);
101
+ if (verdict === "renewed") {
102
+ getSafeLogger()?.info("credentials", "credential.renewed", replacementData(providerId, identity, previous));
103
+ }
104
+ else if (verdict === "changed") {
105
+ getSafeLogger()?.warn("credentials", "credential.changed", {
106
+ ...replacementData(providerId, identity, previous),
107
+ onChange: options.onChange,
108
+ });
109
+ if (options.onChange === "refuse") {
110
+ throw new NaxError(`[credentials] Credential for ${providerId} changed from ${previous.fingerprint} to ${identity.fingerprint}`, "CREDENTIAL_CHANGED", {
111
+ stage: "credentials",
112
+ providerId,
113
+ kind: identity.kind,
114
+ source: identity.source,
115
+ fingerprint: identity.fingerprint,
116
+ previousFingerprint: previous.fingerprint,
117
+ });
118
+ }
119
+ }
120
+ // Adopt what this read observed even when nothing was logged: an identical
121
+ // fingerprint can still arrive from a different source or account label, and
122
+ // servedAuth promises the *last observed* identity, not the first. A refused
123
+ // change never reaches here, which is what keeps the provider refused.
124
+ identities.set(providerId, identity);
125
+ return credential;
126
+ }
127
+ return {
128
+ read,
129
+ modify: (providerId, fn) => inner.modify(providerId, fn),
130
+ delete: (providerId) => inner.delete(providerId),
131
+ servedAuth: (providerId) => {
132
+ const identity = identities.get(providerId);
133
+ return identity === undefined ? undefined : toStamp(identity);
134
+ },
135
+ };
136
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Exec credential source (US-003).
3
+ *
4
+ * Credentials can come from a helper process instead of the credential file. The
5
+ * helper is a public contract, because koda depends on it:
6
+ *
7
+ * invocation argv `[...command, "get"]`, no shell, stdin a pipe, nax's
8
+ * environment inherited (as git credential helpers do)
9
+ * request one JSON line on stdin, then stdin is closed:
10
+ * `{"version":1,"providerId":"anthropic"}`
11
+ * credential exit 0, stdout `{"version":1,"kind":"api-key","key":"…",
12
+ * "expiresAt":…,"account":"…"}` — the last two optional
13
+ * decline exit 0, stdout `{"version":1,"decline":true}` — "not mine"
14
+ * failure non-zero exit, timeout, spawn error → CREDENTIAL_HELPER_FAILED
15
+ * a malformed reply → CREDENTIAL_HELPER_INVALID
16
+ *
17
+ * The wire contract lives in helper-protocol.ts, the process boundary (spawn,
18
+ * bounded streams, both kill switches) in helper-process.ts; this module is the
19
+ * part that caches leases, decides freshness, falls back to the last good lease,
20
+ * and never logs the key.
21
+ *
22
+ * `modify` and `delete` throw: the helper owns the credential. US-003 refuses a
23
+ * provider that switches source mid-process rather than supporting it, so a
24
+ * decline for a provider that already holds a lease is invalid, not a fallback.
25
+ */
26
+ import type { CredentialStore, ProviderId } from "@nathapp/nax-ai";
27
+ export { AUTH_HELPER_STDERR_MAX_BYTES, AUTH_HELPER_STDOUT_MAX_BYTES } from "./helper-process.ts";
28
+ /**
29
+ * A lease closer to expiry than this is accepted but never fresh: the next read
30
+ * spawns the helper again, so a run cannot be left holding a key that expires
31
+ * mid-call. Not configurable (US-003 scope).
32
+ */
33
+ export declare const LEASE_FRESHNESS_MS = 60000;
34
+ export interface ExecCredentialSourceOptions {
35
+ /** The helper executable, plus any leading argv. `"get"` is appended per call. */
36
+ readonly command: readonly string[];
37
+ /** Hard deadline for one helper call. Defaults to the config schema's 10s. */
38
+ readonly timeoutMs?: number;
39
+ /** Clock for lease expiry. Defaults to `Date.now`; tests inject one instead of waiting out a lease. */
40
+ readonly now?: () => number;
41
+ }
42
+ /**
43
+ * The exec source as the store seam sees it. `accountOf` exposes the current
44
+ * lease's non-secret account label separately, because `read` may only return
45
+ * the `{ kind, key }` shape nax-ai's `StoredCredential` allows.
46
+ */
47
+ export interface ExecCredentialSource extends CredentialStore {
48
+ accountOf(providerId: ProviderId): string | undefined;
49
+ }
50
+ export declare function createExecCredentialSource(options: ExecCredentialSourceOptions): ExecCredentialSource;
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Exec credential source (US-003).
3
+ *
4
+ * Credentials can come from a helper process instead of the credential file. The
5
+ * helper is a public contract, because koda depends on it:
6
+ *
7
+ * invocation argv `[...command, "get"]`, no shell, stdin a pipe, nax's
8
+ * environment inherited (as git credential helpers do)
9
+ * request one JSON line on stdin, then stdin is closed:
10
+ * `{"version":1,"providerId":"anthropic"}`
11
+ * credential exit 0, stdout `{"version":1,"kind":"api-key","key":"…",
12
+ * "expiresAt":…,"account":"…"}` — the last two optional
13
+ * decline exit 0, stdout `{"version":1,"decline":true}` — "not mine"
14
+ * failure non-zero exit, timeout, spawn error → CREDENTIAL_HELPER_FAILED
15
+ * a malformed reply → CREDENTIAL_HELPER_INVALID
16
+ *
17
+ * The wire contract lives in helper-protocol.ts, the process boundary (spawn,
18
+ * bounded streams, both kill switches) in helper-process.ts; this module is the
19
+ * part that caches leases, decides freshness, falls back to the last good lease,
20
+ * and never logs the key.
21
+ *
22
+ * `modify` and `delete` throw: the helper owns the credential. US-003 refuses a
23
+ * provider that switches source mid-process rather than supporting it, so a
24
+ * decline for a provider that already holds a lease is invalid, not a fallback.
25
+ */
26
+ import { getSafeLogger, NaxError } from "#src/infra/index";
27
+ import { runHelper } from "./helper-process.js";
28
+ import { parseReply } from "./helper-protocol.js";
29
+ export { AUTH_HELPER_STDERR_MAX_BYTES, AUTH_HELPER_STDOUT_MAX_BYTES } from "./helper-process.js";
30
+ /**
31
+ * A lease closer to expiry than this is accepted but never fresh: the next read
32
+ * spawns the helper again, so a run cannot be left holding a key that expires
33
+ * mid-call. Not configurable (US-003 scope).
34
+ */
35
+ export const LEASE_FRESHNESS_MS = 60_000;
36
+ /** Mirrors AuthConfigSchema's `auth.exec.timeoutMs` default; this module is not config-aware. */
37
+ const DEFAULT_HELPER_TIMEOUT_MS = 10_000;
38
+ /** A failure outcome, with the code it is reported under. */
39
+ function failureOf(code, parts) {
40
+ return { kind: "failure", failure: { code, ...parts } };
41
+ }
42
+ /**
43
+ * Translate what the process boundary saw into a helper outcome.
44
+ *
45
+ * The two kill switches do not share a code: a deadline that passed is a failed
46
+ * helper, while stdout past its cap is a malformed reply. Both killed the
47
+ * process, so its own exit code is not the verdict in either case.
48
+ */
49
+ function outcomeOf(result) {
50
+ switch (result.kind) {
51
+ case "spawn-failed":
52
+ return failureOf("CREDENTIAL_HELPER_FAILED", {
53
+ detail: result.detail,
54
+ timedOut: false,
55
+ stderr: result.stderr,
56
+ cause: result.cause,
57
+ });
58
+ case "no-answer":
59
+ return failureOf("CREDENTIAL_HELPER_FAILED", {
60
+ detail: result.detail,
61
+ timedOut: false,
62
+ stderr: result.stderr,
63
+ });
64
+ case "timed-out":
65
+ return failureOf("CREDENTIAL_HELPER_FAILED", { detail: result.detail, timedOut: true, stderr: result.stderr });
66
+ case "stdout-over-cap":
67
+ return failureOf("CREDENTIAL_HELPER_INVALID", {
68
+ detail: result.detail,
69
+ timedOut: false,
70
+ stderr: result.stderr,
71
+ });
72
+ case "exited": {
73
+ if (result.exitCode !== 0) {
74
+ return failureOf("CREDENTIAL_HELPER_FAILED", {
75
+ detail: `exited with code ${result.exitCode}`,
76
+ timedOut: false,
77
+ stderr: result.stderr,
78
+ exitCode: result.exitCode,
79
+ });
80
+ }
81
+ const reply = parseReply(result.stdout);
82
+ if (reply.kind === "invalid") {
83
+ return failureOf("CREDENTIAL_HELPER_INVALID", {
84
+ detail: reply.detail,
85
+ timedOut: false,
86
+ stderr: result.stderr,
87
+ });
88
+ }
89
+ return reply;
90
+ }
91
+ }
92
+ }
93
+ /** The message a thrown helper failure carries. */
94
+ function failureMessage(providerId, failure) {
95
+ return `[credentials] The credential helper for ${providerId} ${failure.detail}`;
96
+ }
97
+ export function createExecCredentialSource(options) {
98
+ const timeoutMs = options.timeoutMs ?? DEFAULT_HELPER_TIMEOUT_MS;
99
+ const now = options.now ?? Date.now;
100
+ /** The lease in force: what a fresh read is served from. */
101
+ const leases = new Map();
102
+ /** The last credential the helper handed out, kept for the failure branch. */
103
+ const lastGood = new Map();
104
+ /** Providers this process has already heard a decline for. */
105
+ const declined = new Set();
106
+ /** The helper call already running per provider — single-flight. */
107
+ const inFlight = new Map();
108
+ /** Providers whose current consecutive-failure streak has already been logged. */
109
+ const failureLogged = new Set();
110
+ /** Only the two fields nax-ai's StoredCredential allows; the account label travels separately. */
111
+ function credentialOf(lease) {
112
+ return { kind: "api-key", key: lease.key };
113
+ }
114
+ function isFresh(lease, now) {
115
+ return lease.expiresAt === undefined || lease.expiresAt - now > LEASE_FRESHNESS_MS;
116
+ }
117
+ /**
118
+ * The last good lease, only while it has not expired. A helper that has gone
119
+ * away mid-run must not leave every request using a key the provider rejects.
120
+ */
121
+ function servedLastGood(providerId, now) {
122
+ const lease = lastGood.get(providerId);
123
+ return lease?.expiresAt !== undefined && lease.expiresAt > now ? lease : undefined;
124
+ }
125
+ /**
126
+ * The failure branch. `credential.helper_failed` is logged once per
127
+ * consecutive-failure streak — a helper that keeps failing must not fill the
128
+ * run log, and the streak resets on the next credential it serves.
129
+ */
130
+ function handleFailure(providerId, failure) {
131
+ const fallback = servedLastGood(providerId, now());
132
+ if (!failureLogged.has(providerId)) {
133
+ failureLogged.add(providerId);
134
+ getSafeLogger()?.warn("credentials", "credential.helper_failed", {
135
+ providerId,
136
+ code: failure.code,
137
+ ...(failure.exitCode !== undefined ? { exitCode: failure.exitCode } : {}),
138
+ timedOut: failure.timedOut,
139
+ stderr: failure.stderr,
140
+ servedLastGood: fallback !== undefined,
141
+ });
142
+ }
143
+ if (fallback !== undefined)
144
+ return credentialOf(fallback);
145
+ throw new NaxError(failureMessage(providerId, failure), failure.code, {
146
+ stage: "credentials",
147
+ providerId,
148
+ timedOut: failure.timedOut,
149
+ ...(failure.exitCode !== undefined ? { exitCode: failure.exitCode } : {}),
150
+ ...(failure.cause !== undefined ? { cause: failure.cause } : {}),
151
+ });
152
+ }
153
+ /** Apply one helper outcome to the provider's state. */
154
+ function applyOutcome(providerId, outcome) {
155
+ if (outcome.kind === "failure")
156
+ return handleFailure(providerId, outcome.failure);
157
+ if (outcome.kind === "decline") {
158
+ // A provider never switches source mid-process: once it holds a lease, a
159
+ // decline is a contradiction, not a fallback to the file.
160
+ if (leases.has(providerId)) {
161
+ return handleFailure(providerId, {
162
+ code: "CREDENTIAL_HELPER_INVALID",
163
+ detail: "declined for a provider that already holds a lease",
164
+ timedOut: false,
165
+ stderr: "",
166
+ });
167
+ }
168
+ declined.add(providerId);
169
+ return undefined;
170
+ }
171
+ leases.set(providerId, outcome.lease);
172
+ lastGood.set(providerId, outcome.lease);
173
+ failureLogged.delete(providerId);
174
+ return credentialOf(outcome.lease);
175
+ }
176
+ async function callHelper(providerId) {
177
+ return applyOutcome(providerId, outcomeOf(await runHelper(options.command, providerId, timeoutMs)));
178
+ }
179
+ async function read(providerId) {
180
+ // 1. A decline this process already heard: the helper is not this
181
+ // provider's source, and asking again would only spawn it anew.
182
+ if (declined.has(providerId))
183
+ return undefined;
184
+ // 2. A fresh lease, without spawning. A lease with no expiry never goes
185
+ // stale, so the helper runs once per process for that provider.
186
+ const lease = leases.get(providerId);
187
+ if (lease !== undefined && isFresh(lease, now()))
188
+ return credentialOf(lease);
189
+ // 3. Single-flight: a second read for the same provider joins the call
190
+ // already running instead of spawning a second helper.
191
+ const running = inFlight.get(providerId);
192
+ if (running !== undefined)
193
+ return running;
194
+ // 4. Spawn. The map is set before the first await, so a concurrent read
195
+ // cannot miss it.
196
+ const pending = callHelper(providerId);
197
+ inFlight.set(providerId, pending);
198
+ try {
199
+ return await pending;
200
+ }
201
+ finally {
202
+ if (inFlight.get(providerId) === pending)
203
+ inFlight.delete(providerId);
204
+ }
205
+ }
206
+ function managedByHelper(providerId, operation) {
207
+ return new NaxError(`[credentials] Cannot ${operation} the credential for ${providerId}: the exec helper owns it.`, "CREDENTIAL_MANAGED_BY_HELPER", { stage: "credentials", providerId, operation });
208
+ }
209
+ return {
210
+ read,
211
+ // Synchronous throws, not rejections: the helper owns the credential, so the
212
+ // refusal is the whole answer and no work is done — a caller holding only
213
+ // the returned promise would otherwise see an unhandled rejection.
214
+ modify(providerId) {
215
+ throw managedByHelper(providerId, "modify");
216
+ },
217
+ delete(providerId) {
218
+ throw managedByHelper(providerId, "delete");
219
+ },
220
+ accountOf: (providerId) => leases.get(providerId)?.account,
221
+ };
222
+ }