@cohortapp/agent-sdk 2.3.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 (731) hide show
  1. package/.claude/commands/init-agent.md +104 -0
  2. package/.claude/commands/init-maestro.md +1187 -0
  3. package/.claude/settings.json +161 -0
  4. package/.env.example +216 -0
  5. package/README.md +632 -0
  6. package/agents/browser-operator/agent.md +52 -0
  7. package/agents/calendar-ops/agent.md +50 -0
  8. package/agents/communications/agent.md +96 -0
  9. package/agents/decision-log/agent.md +65 -0
  10. package/agents/desktop-operator/agent.md +59 -0
  11. package/agents/gmail-operator/agent.md +62 -0
  12. package/agents/inbound-dispatcher/agent.md +66 -0
  13. package/agents/inbox-processor/agent.md +39 -0
  14. package/agents/pmo-execution/agent.md +60 -0
  15. package/agents/session-spawner/agent.md +64 -0
  16. package/agents/slack-operator/agent.md +60 -0
  17. package/agents/whatsapp-operator/agent.md +60 -0
  18. package/agents/workflow-automation/agent.md +61 -0
  19. package/archetypes/altitudes/c-suite.yaml +58 -0
  20. package/archetypes/altitudes/founder.yaml +68 -0
  21. package/archetypes/altitudes/senior-manager.yaml +63 -0
  22. package/archetypes/altitudes/svp.yaml +60 -0
  23. package/archetypes/altitudes/vp.yaml +50 -0
  24. package/archetypes/archetype.schema.json +77 -0
  25. package/archetypes/base.yaml +47 -0
  26. package/archetypes/capabilities/commercial-leader.yaml +159 -0
  27. package/archetypes/capabilities/compliance-officer.yaml +159 -0
  28. package/archetypes/capabilities/executive-operator.yaml +169 -0
  29. package/archetypes/capabilities/finance-leader.yaml +162 -0
  30. package/archetypes/capabilities/operations-leader.yaml +154 -0
  31. package/archetypes/capabilities/product-leader.yaml +148 -0
  32. package/archetypes/capabilities/technical-leader.yaml +146 -0
  33. package/archetypes/functions/commercial-leader.yaml +62 -0
  34. package/archetypes/functions/compliance-officer.yaml +64 -0
  35. package/archetypes/functions/executive-operator.yaml +70 -0
  36. package/archetypes/functions/finance-leader.yaml +70 -0
  37. package/archetypes/functions/operations-leader.yaml +62 -0
  38. package/archetypes/functions/product-leader.yaml +61 -0
  39. package/archetypes/functions/technical-leader.yaml +57 -0
  40. package/bin/cohort-mcp.mjs +81 -0
  41. package/bin/maestro.mjs +3516 -0
  42. package/bin/maestro.test.mjs +1015 -0
  43. package/desktop-control/README.md +56 -0
  44. package/desktop-control/app-profiles/gmail.yaml +120 -0
  45. package/desktop-control/app-profiles/slack.yaml +315 -0
  46. package/desktop-control/app-profiles/whatsapp.yaml +107 -0
  47. package/docs/architecture/agent-topology.md +2239 -0
  48. package/docs/architecture/archetype-agent-factory.md +110 -0
  49. package/docs/architecture/collective-memory-and-org-mesh.md +115 -0
  50. package/docs/architecture/continuous-monitoring.md +221 -0
  51. package/docs/architecture/mcp-capability-map.md +585 -0
  52. package/docs/architecture/system-architecture.md +1272 -0
  53. package/docs/company-context/README.md +40 -0
  54. package/docs/guides/agent-persona-setup.md +600 -0
  55. package/docs/guides/agents-observe-setup.md +64 -0
  56. package/docs/guides/billing-console-keys.md +88 -0
  57. package/docs/guides/ccxray-diagnostics.md +65 -0
  58. package/docs/guides/channel-bus.md +127 -0
  59. package/docs/guides/claude-mem-setup.md +79 -0
  60. package/docs/guides/claude-pace-setup.md +56 -0
  61. package/docs/guides/claudraband-sessions.md +98 -0
  62. package/docs/guides/clawteam-swarm.md +116 -0
  63. package/docs/guides/code-review-graph-setup.md +86 -0
  64. package/docs/guides/email-setup.md +431 -0
  65. package/docs/guides/mac-mini.md +119 -0
  66. package/docs/guides/media-generation-setup.md +349 -0
  67. package/docs/guides/model-routing.md +162 -0
  68. package/docs/guides/observability-otel.md +265 -0
  69. package/docs/guides/org-onboarding.md +132 -0
  70. package/docs/guides/outbound-governance-setup.md +437 -0
  71. package/docs/guides/pdf-generation-setup.md +315 -0
  72. package/docs/guides/poller-daemon-setup.md +563 -0
  73. package/docs/guides/rag-context-setup.md +459 -0
  74. package/docs/guides/self-optimization-pattern.md +82 -0
  75. package/docs/guides/setup-wizard.md +178 -0
  76. package/docs/guides/slack-setup.md +350 -0
  77. package/docs/guides/telegram-setup.md +227 -0
  78. package/docs/guides/twilio-subaccounts-setup.md +223 -0
  79. package/docs/guides/verification.md +128 -0
  80. package/docs/guides/voice-mode.md +188 -0
  81. package/docs/guides/voice-sms-setup.md +698 -0
  82. package/docs/guides/webhook-relay-setup.md +349 -0
  83. package/docs/guides/whatsapp-setup.md +288 -0
  84. package/docs/prompts/board-pack-cover-template.md +36 -0
  85. package/docs/prompts/decision-recommendation-template.md +88 -0
  86. package/docs/prompts/followup-message-template.md +141 -0
  87. package/docs/prompts/investor-letter-template.md +52 -0
  88. package/docs/prompts/morning-brief-template.md +82 -0
  89. package/docs/prompts/presentation-template.md +58 -0
  90. package/docs/prompts/weekly-strategic-memo-template.md +104 -0
  91. package/docs/research/hallucinated-tool-output-investigation.md +151 -0
  92. package/docs/runbooks/backup-restore.md +205 -0
  93. package/docs/runbooks/cohort-cutover.md +129 -0
  94. package/docs/runbooks/fleet-operations.md +200 -0
  95. package/docs/runbooks/incident-response.md +226 -0
  96. package/docs/runbooks/mac-mini-bootstrap.md +431 -0
  97. package/docs/runbooks/perpetual-operations.md +509 -0
  98. package/docs/runbooks/recovery-and-failover.md +260 -0
  99. package/framework-features.json +267 -0
  100. package/ingest/README.md +87 -0
  101. package/lib/action-executor.js +689 -0
  102. package/lib/action-executor.test.mjs +871 -0
  103. package/lib/agent-root.mjs +37 -0
  104. package/lib/archetype.mjs +236 -0
  105. package/lib/archetype.test.mjs +132 -0
  106. package/lib/autonomy.mjs +114 -0
  107. package/lib/autonomy.test.mjs +66 -0
  108. package/lib/backlog.mjs +358 -0
  109. package/lib/backlog.test.mjs +266 -0
  110. package/lib/budget-guard.mjs +279 -0
  111. package/lib/budget-guard.test.mjs +291 -0
  112. package/lib/cadence-bus-schedule.test.mjs +194 -0
  113. package/lib/cadence-bus.mjs +1120 -0
  114. package/lib/cadence-bus.test.mjs +720 -0
  115. package/lib/cadences.mjs +205 -0
  116. package/lib/cadences.test.mjs +125 -0
  117. package/lib/capability.mjs +154 -0
  118. package/lib/capability.test.mjs +78 -0
  119. package/lib/channels/base-adapter.mjs +719 -0
  120. package/lib/channels/base-adapter.test.mjs +590 -0
  121. package/lib/channels/channel.mjs +128 -0
  122. package/lib/channels/channels.test.mjs +371 -0
  123. package/lib/channels/contract.mjs +215 -0
  124. package/lib/channels/contract.test.mjs +137 -0
  125. package/lib/channels/conversation-resolver.mjs +95 -0
  126. package/lib/channels/gmail/adapter.mjs +87 -0
  127. package/lib/channels/inbox-item.mjs +255 -0
  128. package/lib/channels/inbox-item.test.mjs +335 -0
  129. package/lib/channels/index.mjs +94 -0
  130. package/lib/channels/orgmail/adapter.mjs +353 -0
  131. package/lib/channels/orgmail/adapter.test.mjs +311 -0
  132. package/lib/channels/pairing.mjs +363 -0
  133. package/lib/channels/pairing.test.mjs +270 -0
  134. package/lib/channels/registry.mjs +164 -0
  135. package/lib/channels/slack/adapter.mjs +317 -0
  136. package/lib/channels/slack-adapter.test.mjs +212 -0
  137. package/lib/channels/sms/adapter.mjs +43 -0
  138. package/lib/channels/telegram/adapter.mjs +432 -0
  139. package/lib/channels/telegram-adapter.test.mjs +306 -0
  140. package/lib/channels/voice/adapter.mjs +301 -0
  141. package/lib/channels/voice/adapter.test.mjs +278 -0
  142. package/lib/channels/whatsapp/adapter-baileys.mjs +587 -0
  143. package/lib/channels/whatsapp/adapter-baileys.test.mjs +359 -0
  144. package/lib/channels/whatsapp/adapter-twilio.mjs +65 -0
  145. package/lib/channels/whatsapp/baileys-typing.test.mjs +154 -0
  146. package/lib/charter.mjs +256 -0
  147. package/lib/charter.test.mjs +89 -0
  148. package/lib/claude-bin.mjs +134 -0
  149. package/lib/claude-bin.test.mjs +75 -0
  150. package/lib/collective/capture.mjs +185 -0
  151. package/lib/collective/capture.test.mjs +121 -0
  152. package/lib/collective/cards.mjs +201 -0
  153. package/lib/collective/cards.test.mjs +114 -0
  154. package/lib/collective/config.mjs +186 -0
  155. package/lib/collective/config.test.mjs +123 -0
  156. package/lib/collective/global-config.mjs +113 -0
  157. package/lib/collective/global-config.test.mjs +75 -0
  158. package/lib/collective/presence.mjs +201 -0
  159. package/lib/collective/presence.test.mjs +95 -0
  160. package/lib/collective/recall.mjs +215 -0
  161. package/lib/collective/recall.test.mjs +116 -0
  162. package/lib/comms/send-gate.mjs +554 -0
  163. package/lib/comms/send-gate.test.mjs +577 -0
  164. package/lib/comms.mjs +67 -0
  165. package/lib/comms.test.mjs +41 -0
  166. package/lib/diagnostics/alerts.mjs +424 -0
  167. package/lib/diagnostics/alerts.test.mjs +318 -0
  168. package/lib/diagnostics/backup-freshness.mjs +188 -0
  169. package/lib/diagnostics/backup-freshness.test.mjs +185 -0
  170. package/lib/diagnostics/counters.mjs +269 -0
  171. package/lib/diagnostics/counters.test.mjs +206 -0
  172. package/lib/diagnostics/events.mjs +188 -0
  173. package/lib/diagnostics/events.test.mjs +290 -0
  174. package/lib/diagnostics/otel.mjs +237 -0
  175. package/lib/diagnostics/otel.test.mjs +196 -0
  176. package/lib/diagnostics/trace.mjs +216 -0
  177. package/lib/diagnostics/trace.test.mjs +251 -0
  178. package/lib/env-compat.mjs +74 -0
  179. package/lib/env-compat.test.mjs +104 -0
  180. package/lib/feature-init.mjs +331 -0
  181. package/lib/fs-atomic.mjs +112 -0
  182. package/lib/fs-atomic.test.mjs +72 -0
  183. package/lib/fs-ownership.mjs +111 -0
  184. package/lib/fs-ownership.test.mjs +158 -0
  185. package/lib/hooks/bus.mjs +347 -0
  186. package/lib/hooks/bus.test.mjs +387 -0
  187. package/lib/index.js +16 -0
  188. package/lib/learning/config.mjs +106 -0
  189. package/lib/learning/config.test.mjs +75 -0
  190. package/lib/learning/counters.mjs +156 -0
  191. package/lib/learning/counters.test.mjs +69 -0
  192. package/lib/learning/curator-consolidate.test.mjs +238 -0
  193. package/lib/learning/curator.mjs +453 -0
  194. package/lib/learning/curator.test.mjs +106 -0
  195. package/lib/learning/log.mjs +40 -0
  196. package/lib/learning/reflect.mjs +534 -0
  197. package/lib/learning/reflect.test.mjs +0 -0
  198. package/lib/learning/session-index.mjs +352 -0
  199. package/lib/learning/session-index.test.mjs +125 -0
  200. package/lib/learning/skill-writer.mjs +474 -0
  201. package/lib/learning/skill-writer.test.mjs +210 -0
  202. package/lib/mcp/server.mjs +328 -0
  203. package/lib/mcp/server.test.mjs +400 -0
  204. package/lib/model-router/auth-profiles.mjs +758 -0
  205. package/lib/model-router/auth-profiles.test.mjs +580 -0
  206. package/lib/model-router/catalog/anthropic.yaml +153 -0
  207. package/lib/model-router/catalog/deepseek.yaml +86 -0
  208. package/lib/model-router/catalog/moonshot.yaml +81 -0
  209. package/lib/model-router/catalog/qwen.yaml +114 -0
  210. package/lib/model-router/catalog.mjs +925 -0
  211. package/lib/model-router/catalog.test.mjs +385 -0
  212. package/lib/model-router/economics.mjs +564 -0
  213. package/lib/model-router/economics.test.mjs +344 -0
  214. package/lib/model-router/failover.mjs +298 -0
  215. package/lib/model-router/failover.test.mjs +439 -0
  216. package/lib/model-router/health.mjs +453 -0
  217. package/lib/model-router/health.test.mjs +338 -0
  218. package/lib/model-router/integration-coverage.test.mjs +829 -0
  219. package/lib/model-router/integration.test.mjs +564 -0
  220. package/lib/model-router/ledger.mjs +402 -0
  221. package/lib/model-router/ledger.test.mjs +382 -0
  222. package/lib/model-router/llm-task.mjs +515 -0
  223. package/lib/model-router/llm-task.test.mjs +392 -0
  224. package/lib/model-router/org-credentials.mjs +260 -0
  225. package/lib/model-router/org-credentials.test.mjs +265 -0
  226. package/lib/model-router/pricing-refresh.mjs +463 -0
  227. package/lib/model-router/pricing-refresh.test.mjs +286 -0
  228. package/lib/model-router/reconcile.mjs +429 -0
  229. package/lib/model-router/reconcile.test.mjs +316 -0
  230. package/lib/model-router/repair.mjs +471 -0
  231. package/lib/model-router/repair.test.mjs +180 -0
  232. package/lib/model-router/resolve.mjs +1206 -0
  233. package/lib/model-router/spawn.mjs +497 -0
  234. package/lib/model-router/spawn.test.mjs +425 -0
  235. package/lib/model-router/taxonomy.mjs +893 -0
  236. package/lib/model-router/taxonomy.test.mjs +410 -0
  237. package/lib/model-router.mjs +677 -0
  238. package/lib/model-router.test.mjs +907 -0
  239. package/lib/org/activity.mjs +211 -0
  240. package/lib/org/activity.test.mjs +134 -0
  241. package/lib/org/approvals.mjs +448 -0
  242. package/lib/org/approvals.test.mjs +216 -0
  243. package/lib/org/awareness.mjs +222 -0
  244. package/lib/org/awareness.test.mjs +159 -0
  245. package/lib/org/board.mjs +229 -0
  246. package/lib/org/board.test.mjs +177 -0
  247. package/lib/org/bootstrap-context.mjs +169 -0
  248. package/lib/org/bootstrap-context.test.mjs +153 -0
  249. package/lib/org/client.mjs +1628 -0
  250. package/lib/org/client.test.mjs +1107 -0
  251. package/lib/org/cohort-client.mjs +67 -0
  252. package/lib/org/cohort-client.test.mjs +126 -0
  253. package/lib/org/cost-sync.mjs +227 -0
  254. package/lib/org/cost-sync.test.mjs +153 -0
  255. package/lib/org/doctor.mjs +212 -0
  256. package/lib/org/doctor.test.mjs +212 -0
  257. package/lib/org/handoff.mjs +293 -0
  258. package/lib/org/handoff.test.mjs +269 -0
  259. package/lib/org/integration-tools.mjs +182 -0
  260. package/lib/org/integration-tools.test.mjs +160 -0
  261. package/lib/org/keys.mjs +131 -0
  262. package/lib/org/keys.test.mjs +92 -0
  263. package/lib/org/knowledge.mjs +463 -0
  264. package/lib/org/knowledge.test.mjs +319 -0
  265. package/lib/org/leases.mjs +335 -0
  266. package/lib/org/leases.test.mjs +235 -0
  267. package/lib/org/mesh-integration.test.mjs +127 -0
  268. package/lib/org/mesh.mjs +459 -0
  269. package/lib/org/mesh.test.mjs +345 -0
  270. package/lib/org/messaging.mjs +503 -0
  271. package/lib/org/messaging.test.mjs +238 -0
  272. package/lib/org/policy.mjs +345 -0
  273. package/lib/org/policy.test.mjs +237 -0
  274. package/lib/org/protocol.checksum +1 -0
  275. package/lib/org/protocol.checksum.test.mjs +90 -0
  276. package/lib/org/protocol.mjs +967 -0
  277. package/lib/org/protocol.test.mjs +264 -0
  278. package/lib/org/registry.mjs +194 -0
  279. package/lib/org/registry.test.mjs +100 -0
  280. package/lib/org/tool-surface-integration.test.mjs +120 -0
  281. package/lib/org/tool-surface.mjs +2535 -0
  282. package/lib/org/tool-surface.test.mjs +589 -0
  283. package/lib/org/ui-parity.mjs +3236 -0
  284. package/lib/org/ui-parity.test.mjs +348 -0
  285. package/lib/org/verify.mjs +176 -0
  286. package/lib/org/verify.test.mjs +194 -0
  287. package/lib/rag/embed.mjs +188 -0
  288. package/lib/rag/indexer.mjs +425 -0
  289. package/lib/rag/rag.test.mjs +505 -0
  290. package/lib/rag/search.mjs +475 -0
  291. package/lib/rate-guard.mjs +246 -0
  292. package/lib/rate-guard.test.mjs +201 -0
  293. package/lib/render.mjs +112 -0
  294. package/lib/render.test.mjs +68 -0
  295. package/lib/resource-governor.mjs +297 -0
  296. package/lib/resource-governor.test.mjs +262 -0
  297. package/lib/scheduling/dynamic-jobs.mjs +675 -0
  298. package/lib/scheduling/dynamic-jobs.test.mjs +344 -0
  299. package/lib/scheduling/jitter.mjs +0 -0
  300. package/lib/scheduling/jitter.test.mjs +140 -0
  301. package/lib/secrets/broker.mjs +315 -0
  302. package/lib/secrets/broker.test.mjs +280 -0
  303. package/lib/secrets/providers.mjs +461 -0
  304. package/lib/secrets/providers.test.mjs +274 -0
  305. package/lib/security/audit-engine.mjs +684 -0
  306. package/lib/security/audit-engine.test.mjs +389 -0
  307. package/lib/security/coerce-args.mjs +552 -0
  308. package/lib/security/coerce-args.test.mjs +281 -0
  309. package/lib/security/dangerous-tools.mjs +97 -0
  310. package/lib/security/dangerous-tools.test.mjs +68 -0
  311. package/lib/security/external-content.mjs +145 -0
  312. package/lib/security/external-content.test.mjs +67 -0
  313. package/lib/security/redact.mjs +592 -0
  314. package/lib/security/redact.test.mjs +441 -0
  315. package/lib/security/secret-equal.mjs +73 -0
  316. package/lib/security/secret-equal.test.mjs +55 -0
  317. package/lib/session-permissions.mjs +101 -0
  318. package/lib/session-permissions.test.mjs +100 -0
  319. package/lib/setup/claude-probe.mjs +74 -0
  320. package/lib/setup/completeness.mjs +175 -0
  321. package/lib/setup/completeness.test.mjs +110 -0
  322. package/lib/setup/context-pack.mjs +173 -0
  323. package/lib/setup/context-pack.test.mjs +89 -0
  324. package/lib/setup/enrich.mjs +277 -0
  325. package/lib/setup/enrich.test.mjs +115 -0
  326. package/lib/setup/enroll-from-cohort.mjs +441 -0
  327. package/lib/setup/enroll-from-cohort.test.mjs +233 -0
  328. package/lib/setup/integration.test.mjs +162 -0
  329. package/lib/setup/io.mjs +360 -0
  330. package/lib/setup/io.test.mjs +77 -0
  331. package/lib/setup/run-generator.mjs +81 -0
  332. package/lib/setup/runner.mjs +244 -0
  333. package/lib/setup/runner.test.mjs +132 -0
  334. package/lib/setup/sections/comms.mjs +173 -0
  335. package/lib/setup/sections/company.mjs +120 -0
  336. package/lib/setup/sections/enrich.mjs +138 -0
  337. package/lib/setup/sections/identity.mjs +182 -0
  338. package/lib/setup/sections/identity.test.mjs +140 -0
  339. package/lib/setup/sections/learning.mjs +153 -0
  340. package/lib/setup/sections/learning.test.mjs +81 -0
  341. package/lib/setup/sections/messaging.mjs +219 -0
  342. package/lib/setup/sections/messaging.test.mjs +127 -0
  343. package/lib/setup/sections/model.mjs +102 -0
  344. package/lib/setup/sections/operating-model.mjs +78 -0
  345. package/lib/setup/sections/org.mjs +475 -0
  346. package/lib/setup/sections/org.test.mjs +313 -0
  347. package/lib/setup/sections/orgmail.mjs +173 -0
  348. package/lib/setup/sections/orgmail.test.mjs +118 -0
  349. package/lib/setup/sections/recovery.mjs +159 -0
  350. package/lib/setup/sections/recovery.test.mjs +98 -0
  351. package/lib/setup/sections/tools.mjs +132 -0
  352. package/lib/setup/sections/verify.mjs +97 -0
  353. package/lib/setup/sot.mjs +205 -0
  354. package/lib/setup/sot.test.mjs +81 -0
  355. package/lib/setup/state.mjs +151 -0
  356. package/lib/setup/state.test.mjs +92 -0
  357. package/lib/singleton.js +229 -0
  358. package/lib/singleton.test.mjs +135 -0
  359. package/lib/telemetry/alerts.mjs +216 -0
  360. package/lib/telemetry/alerts.test.mjs +109 -0
  361. package/lib/telemetry/collect.mjs +512 -0
  362. package/lib/telemetry/collect.test.mjs +202 -0
  363. package/lib/tool-definitions-integration.test.mjs +83 -0
  364. package/lib/tool-definitions.js +738 -0
  365. package/lib/tool-definitions.test.mjs +437 -0
  366. package/lib/util/fetch-timeout.mjs +136 -0
  367. package/lib/util/fetch-timeout.test.mjs +202 -0
  368. package/lib/util/reconnect.mjs +343 -0
  369. package/lib/util/reconnect.test.mjs +369 -0
  370. package/lib/util/unhandled.mjs +205 -0
  371. package/lib/util/unhandled.test.mjs +216 -0
  372. package/lib/voice/context-loader.mjs +466 -0
  373. package/lib/voice/index.mjs +100 -0
  374. package/lib/voice/openai-realtime.mjs +510 -0
  375. package/lib/voice/outbound.mjs +542 -0
  376. package/lib/voice/outbound.test.mjs +69 -0
  377. package/lib/voice/post-call-brief.mjs +428 -0
  378. package/lib/voice/provider.mjs +52 -0
  379. package/lib/voice/session-rotation.mjs +257 -0
  380. package/lib/voice/stt.mjs +161 -0
  381. package/lib/voice/stt.test.mjs +226 -0
  382. package/lib/voice/tool-bridge.mjs +370 -0
  383. package/lib/voice/tts.mjs +104 -0
  384. package/lib/voice/twilio-sip-bridge.mjs +288 -0
  385. package/lib/voice/voice.test.mjs +990 -0
  386. package/mcp/README.md +80 -0
  387. package/package.json +151 -0
  388. package/plugins/maestro-skills/plugin.json +139 -0
  389. package/plugins/maestro-skills/skills/agents-observe.md +110 -0
  390. package/plugins/maestro-skills/skills/board-deck.md +68 -0
  391. package/plugins/maestro-skills/skills/books-close.md +77 -0
  392. package/plugins/maestro-skills/skills/brand-steward.md +121 -0
  393. package/plugins/maestro-skills/skills/calendar-plan.md +57 -0
  394. package/plugins/maestro-skills/skills/call-working-sessions.md +124 -0
  395. package/plugins/maestro-skills/skills/ccxray-diagnostics.md +91 -0
  396. package/plugins/maestro-skills/skills/claude-pace.md +61 -0
  397. package/plugins/maestro-skills/skills/code-review-graph.md +99 -0
  398. package/plugins/maestro-skills/skills/crm-pipeline.md +65 -0
  399. package/plugins/maestro-skills/skills/decision-brief.md +89 -0
  400. package/plugins/maestro-skills/skills/directory-hygiene.md +125 -0
  401. package/plugins/maestro-skills/skills/draft-comms.md +84 -0
  402. package/plugins/maestro-skills/skills/evening-wrap.md +53 -0
  403. package/plugins/maestro-skills/skills/files-find.md +65 -0
  404. package/plugins/maestro-skills/skills/generative-ui.md +228 -0
  405. package/plugins/maestro-skills/skills/hiring-triage.md +74 -0
  406. package/plugins/maestro-skills/skills/inbox-triage.md +61 -0
  407. package/plugins/maestro-skills/skills/mail-triage.md +86 -0
  408. package/plugins/maestro-skills/skills/morning-brief.md +54 -0
  409. package/plugins/maestro-skills/skills/native-artifacts.md +157 -0
  410. package/plugins/maestro-skills/skills/org-board.md +133 -0
  411. package/plugins/maestro-skills/skills/org-credential.md +68 -0
  412. package/plugins/maestro-skills/skills/org-recall.md +81 -0
  413. package/plugins/maestro-skills/skills/pipeline-review.md +76 -0
  414. package/plugins/maestro-skills/skills/regulatory-status.md +81 -0
  415. package/plugins/maestro-skills/skills/router-why.md +78 -0
  416. package/plugins/maestro-skills/skills/schedule-meeting.md +91 -0
  417. package/plugins/maestro-skills/skills/session-search.md +71 -0
  418. package/plugins/maestro-skills/skills/set-reminder.md +93 -0
  419. package/plugins/maestro-skills/skills/slack-followup.md +64 -0
  420. package/plugins/maestro-skills/skills/team-activity.md +86 -0
  421. package/plugins/maestro-skills/skills/weekly-memo.md +70 -0
  422. package/policies/action-classification.yaml +114 -0
  423. package/policies/ai-disclosure.yaml +294 -0
  424. package/policies/communication-style.md +139 -0
  425. package/policies/information-barriers.yaml +118 -0
  426. package/policies/prompt-injection-defence.yaml +138 -0
  427. package/public/assets/icon-dark.png +0 -0
  428. package/public/assets/icon-dark.svg +9 -0
  429. package/public/assets/icon-light.svg +9 -0
  430. package/public/assets/logo-dark.svg +15 -0
  431. package/public/assets/logo-light.svg +15 -0
  432. package/scaffold/.mcp.json +7 -0
  433. package/scaffold/CLAUDE.md +368 -0
  434. package/scaffold/config/agent.json +55 -0
  435. package/scaffold/config/agent.ts +76 -0
  436. package/scaffold/config/agent.ts.example +89 -0
  437. package/scaffold/config/alerts.yaml +23 -0
  438. package/scaffold/config/allowlist.yaml.example +25 -0
  439. package/scaffold/config/caller-id-map.yaml +46 -0
  440. package/scaffold/config/collective.yaml +49 -0
  441. package/scaffold/config/company.json +20 -0
  442. package/scaffold/config/known-agents.json +6 -0
  443. package/scaffold/config/learning.yaml +55 -0
  444. package/scaffold/config/model-routing.yaml.example +104 -0
  445. package/scaffold/config/org.yaml +25 -0
  446. package/scaffold/config/orgmail.yaml.example +19 -0
  447. package/scaffold/config/recovery.yaml +72 -0
  448. package/scaffold/config/secrets.yaml +27 -0
  449. package/scaffold/config/slack.yaml.example +35 -0
  450. package/scaffold/config/telegram.yaml.example +38 -0
  451. package/scaffold/config/voice.yaml.example +89 -0
  452. package/scaffold/config/whatsapp.yaml.example +39 -0
  453. package/schedules/README.md +49 -0
  454. package/schedules/triggers/backlog-executor.md +102 -0
  455. package/schedules/triggers/brand-steward.md +72 -0
  456. package/schedules/triggers/daily-evening-wrap.md +159 -0
  457. package/schedules/triggers/daily-midday-sweep.md +58 -0
  458. package/schedules/triggers/daily-morning-brief.md +55 -0
  459. package/schedules/triggers/directory-hygiene.md +81 -0
  460. package/schedules/triggers/dynamic-jobs.md +40 -0
  461. package/schedules/triggers/inbox-processor.md +115 -0
  462. package/schedules/triggers/meeting-action-capture.md +60 -0
  463. package/schedules/triggers/meeting-prep.md +69 -0
  464. package/schedules/triggers/messaging-inbound.md +50 -0
  465. package/schedules/triggers/org-pulse.md +24 -0
  466. package/schedules/triggers/quarterly-self-assessment.md +54 -0
  467. package/schedules/triggers/weekly-engineering-health.md +37 -0
  468. package/schedules/triggers/weekly-execution.md +65 -0
  469. package/schedules/triggers/weekly-hiring.md +53 -0
  470. package/schedules/triggers/weekly-priorities.md +38 -0
  471. package/schedules/triggers/weekly-strategic-memo.md +124 -0
  472. package/scripts/archive-email.sh +55 -0
  473. package/scripts/cadence/cadence-status.mjs +36 -0
  474. package/scripts/cadence/enqueue-cadence-tick.mjs +174 -0
  475. package/scripts/cadence/enqueue-cadence-tick.test.mjs +187 -0
  476. package/scripts/cadence/launchd-cadence-wrapper.sh +85 -0
  477. package/scripts/cadence/launchd-cloud-relay-wrapper.sh +95 -0
  478. package/scripts/cadence/launchd-socket-mode-wrapper.sh +95 -0
  479. package/scripts/ci/check-docs-accuracy.mjs +493 -0
  480. package/scripts/ci/check-docs-accuracy.test.mjs +409 -0
  481. package/scripts/ci/check-exports-exist.mjs +140 -0
  482. package/scripts/ci/check-files-exist.mjs +107 -0
  483. package/scripts/ci/check-no-build-artifacts.mjs +111 -0
  484. package/scripts/ci/check-no-build-artifacts.test.mjs +71 -0
  485. package/scripts/ci/check-no-confidential.mjs +198 -0
  486. package/scripts/ci/check-no-conflict-markers.mjs +169 -0
  487. package/scripts/ci/check-no-residual-identity.mjs +163 -0
  488. package/scripts/ci/check-no-residual-identity.test.mjs +89 -0
  489. package/scripts/ci/check-tarball-fidelity.mjs +205 -0
  490. package/scripts/ci/check-unresolved-tokens.mjs +83 -0
  491. package/scripts/ci/check.mjs +109 -0
  492. package/scripts/ci/check.test.mjs +194 -0
  493. package/scripts/ci/run-coverage.mjs +82 -0
  494. package/scripts/ci/run-tests.mjs +71 -0
  495. package/scripts/cloud-relay/README.md +59 -0
  496. package/scripts/cloud-relay/index.mjs +233 -0
  497. package/scripts/cloud-relay/package.json +15 -0
  498. package/scripts/cloud-relay/railway.json +13 -0
  499. package/scripts/cloud-relay/voice/README.md +94 -0
  500. package/scripts/cloud-relay/voice/package-lock.json +39 -0
  501. package/scripts/cloud-relay/voice/package.json +16 -0
  502. package/scripts/cloud-relay/voice/railway.json +13 -0
  503. package/scripts/cloud-relay/voice/server.mjs +532 -0
  504. package/scripts/collective/hook-runner.mjs +211 -0
  505. package/scripts/collective/hook-runner.test.mjs +90 -0
  506. package/scripts/collective/org-pulse.mjs +72 -0
  507. package/scripts/collective/org-sync.mjs +61 -0
  508. package/scripts/collective/recall.mjs +45 -0
  509. package/scripts/collective/who.mjs +30 -0
  510. package/scripts/comms-monitor.sh +288 -0
  511. package/scripts/configure-whatsapp-sandbox.sh +201 -0
  512. package/scripts/continuous-monitor.sh +91 -0
  513. package/scripts/cost/fleet-digest.mjs +407 -0
  514. package/scripts/cost/fleet-digest.test.mjs +207 -0
  515. package/scripts/cost/track-claude-usage.mjs +169 -0
  516. package/scripts/daemon/agent-daemon.mjs +989 -0
  517. package/scripts/daemon/agent-daemon.test.mjs +525 -0
  518. package/scripts/daemon/cadence-consumer-governance.test.mjs +220 -0
  519. package/scripts/daemon/cadence-consumer.mjs +1080 -0
  520. package/scripts/daemon/cadence-consumer.test.mjs +770 -0
  521. package/scripts/daemon/cadence-handlers.mjs +1121 -0
  522. package/scripts/daemon/cadence-handlers.test.mjs +617 -0
  523. package/scripts/daemon/classifier.mjs +704 -0
  524. package/scripts/daemon/classifier.test.mjs +238 -0
  525. package/scripts/daemon/classify-kind.mjs +54 -0
  526. package/scripts/daemon/classify-kind.test.mjs +40 -0
  527. package/scripts/daemon/context-compiler.mjs +605 -0
  528. package/scripts/daemon/context-compiler.test.mjs +300 -0
  529. package/scripts/daemon/dispatcher-cooldown.test.mjs +122 -0
  530. package/scripts/daemon/dispatcher-governance.test.mjs +886 -0
  531. package/scripts/daemon/dispatcher.mjs +1516 -0
  532. package/scripts/daemon/health.mjs +72 -0
  533. package/scripts/daemon/inbox-deferral.mjs +210 -0
  534. package/scripts/daemon/inbox-deferral.test.mjs +242 -0
  535. package/scripts/daemon/integration.test.mjs +149 -0
  536. package/scripts/daemon/launchd-wrapper-generic.sh +96 -0
  537. package/scripts/daemon/launchd-wrapper-slack-events.sh +37 -0
  538. package/scripts/daemon/launchd-wrapper.sh +91 -0
  539. package/scripts/daemon/lib/session-router.mjs +274 -0
  540. package/scripts/daemon/lib/session-router.test.mjs +295 -0
  541. package/scripts/daemon/maestro-daemon.mjs +275 -0
  542. package/scripts/daemon/prompt-builder.mjs +685 -0
  543. package/scripts/daemon/prompt-builder.test.mjs +213 -0
  544. package/scripts/daemon/responder.mjs +854 -0
  545. package/scripts/daemon/session-lock.mjs +721 -0
  546. package/scripts/daemon/session-lock.test.mjs +252 -0
  547. package/scripts/daemon/session-outcomes.mjs +640 -0
  548. package/scripts/daemon/session-outcomes.test.mjs +533 -0
  549. package/scripts/daemon/typing-registry.mjs +90 -0
  550. package/scripts/daemon/typing-registry.test.mjs +77 -0
  551. package/scripts/daemon/voice-webhook-server.mjs +804 -0
  552. package/scripts/decisions/capture-decision.mjs +116 -0
  553. package/scripts/disclosure_assessment.py +873 -0
  554. package/scripts/disclosure_boundaries.py +562 -0
  555. package/scripts/email-signature-principal.html +52 -0
  556. package/scripts/email-signature.html +60 -0
  557. package/scripts/email_quote_thread.py +167 -0
  558. package/scripts/email_thread_dedup.py +362 -0
  559. package/scripts/emergency-stop.sh +81 -0
  560. package/scripts/healthcheck.sh +116 -0
  561. package/scripts/hooks/block-mcp-cohort-send.sh +15 -0
  562. package/scripts/hooks/block-mcp-slack-send.sh +7 -0
  563. package/scripts/hooks/post-action-log.sh +126 -0
  564. package/scripts/hooks/pre-send-audit.sh +174 -0
  565. package/scripts/hooks/pre-send-audit.test.mjs +215 -0
  566. package/scripts/hooks/session-end-log.sh +27 -0
  567. package/scripts/hooks/session-start-banner.sh +115 -0
  568. package/scripts/huddle/audio-bridge.mjs +664 -0
  569. package/scripts/huddle/boot-slack-cdp.sh +102 -0
  570. package/scripts/huddle/huddle-controller.mjs +942 -0
  571. package/scripts/huddle/huddle-server.mjs +1229 -0
  572. package/scripts/huddle/launch-slack.sh +232 -0
  573. package/scripts/huddle/openai-realtime-bridge.mjs +462 -0
  574. package/scripts/huddle/package-lock.json +62 -0
  575. package/scripts/huddle/package.json +22 -0
  576. package/scripts/huddle/setup-audio.sh +239 -0
  577. package/scripts/huddle/start-call.mjs +318 -0
  578. package/scripts/huddle/test-pipeline.mjs +263 -0
  579. package/scripts/learning/consolidate-skills.mjs +72 -0
  580. package/scripts/learning/session-search.mjs +125 -0
  581. package/scripts/llm_email_dedup.py +442 -0
  582. package/scripts/local-triggers/generate-plists.sh +432 -0
  583. package/scripts/local-triggers/generate-plists.test.mjs +413 -0
  584. package/scripts/local-triggers/install-all.sh +49 -0
  585. package/scripts/local-triggers/plists/.gitkeep +0 -0
  586. package/scripts/local-triggers/run-trigger.sh +63 -0
  587. package/scripts/local-triggers/templates/rag-reindex.plist.template +47 -0
  588. package/scripts/local-triggers/templates/voice-relay-poller.plist.template +54 -0
  589. package/scripts/local-triggers/templates/voice-tunnel.plist.template +55 -0
  590. package/scripts/local-triggers/templates/voice-webhook.plist.template +51 -0
  591. package/scripts/maintenance/backup-to-cloud.sh +124 -0
  592. package/scripts/maintenance/health-check.sh +377 -0
  593. package/scripts/media-generation/README.md +105 -0
  594. package/scripts/media-generation/gemini-image-client.mjs +173 -0
  595. package/scripts/media-generation/generate-assets.mjs +289 -0
  596. package/scripts/media-generation/veo-video-client.mjs +219 -0
  597. package/scripts/org/send-orgmail.mjs +227 -0
  598. package/scripts/outbound-dedup-cleanup.sh +43 -0
  599. package/scripts/outbound-dedup.sh +477 -0
  600. package/scripts/outbound_dedup.py +115 -0
  601. package/scripts/parse-voice-transcript.mjs +481 -0
  602. package/scripts/pdf-generation/README.md +63 -0
  603. package/scripts/pdf-generation/build-document.mjs +247 -0
  604. package/scripts/pdf-generation/templates/board-pack.latex +136 -0
  605. package/scripts/pdf-generation/templates/corporate-letter.latex +126 -0
  606. package/scripts/pdf-generation/templates/memo.latex +114 -0
  607. package/scripts/poll-slack-events.sh +35 -0
  608. package/scripts/poller/calendar-poller.mjs +12 -0
  609. package/scripts/poller/gmail-poller.mjs +192 -0
  610. package/scripts/poller/imap-client.mjs +289 -0
  611. package/scripts/poller/inbox-scan-poller.mjs +156 -0
  612. package/scripts/poller/inbox-scan-poller.test.mjs +231 -0
  613. package/scripts/poller/index.mjs +73 -0
  614. package/scripts/poller/intra-session-check.mjs +285 -0
  615. package/scripts/poller/lib/cloud-relay-dedup.mjs +88 -0
  616. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +133 -0
  617. package/scripts/poller/lib/slash-command-handlers.mjs +177 -0
  618. package/scripts/poller/secondary-gmail-poller.mjs +132 -0
  619. package/scripts/poller/slack-cloud-relay-client.mjs +368 -0
  620. package/scripts/poller/slack-poller.mjs +854 -0
  621. package/scripts/poller/slack-socket-mode.mjs +917 -0
  622. package/scripts/poller/slack-socket-mode.test.mjs +753 -0
  623. package/scripts/poller/trigger.mjs +75 -0
  624. package/scripts/poller/utils.mjs +371 -0
  625. package/scripts/poller/voice-cloud-relay-client.mjs +179 -0
  626. package/scripts/poller/voice-poller.mjs +236 -0
  627. package/scripts/poller-launchd/install.sh +66 -0
  628. package/scripts/poller-launchd/poller.plist.template +40 -0
  629. package/scripts/poller-launchd/whatsapp-handler.plist.template +39 -0
  630. package/scripts/post-interaction-indexer.py +1598 -0
  631. package/scripts/pre-draft-context.py +994 -0
  632. package/scripts/pre_draft_lookup.py +258 -0
  633. package/scripts/rag/build-index.mjs +47 -0
  634. package/scripts/rag/ingest.mjs +111 -0
  635. package/scripts/rag/search.mjs +119 -0
  636. package/scripts/rag-indexer.py +629 -0
  637. package/scripts/restore-from-backup.sh +248 -0
  638. package/scripts/restore-from-backup.test.mjs +178 -0
  639. package/scripts/resume-operations.sh +80 -0
  640. package/scripts/search-secondary-inbox.py +181 -0
  641. package/scripts/secondary-inbox-poller.py +437 -0
  642. package/scripts/self-optimization/compute-metrics.py +398 -0
  643. package/scripts/send-email-as-principal.py +369 -0
  644. package/scripts/send-email-threaded.py +392 -0
  645. package/scripts/send-email-with-attachment.py +377 -0
  646. package/scripts/send-email.sh +131 -0
  647. package/scripts/send-sms.sh +175 -0
  648. package/scripts/send-whatsapp.sh +292 -0
  649. package/scripts/session-start.sh +106 -0
  650. package/scripts/setup/boot-claude-session.sh +94 -0
  651. package/scripts/setup/configure-macos.sh +674 -0
  652. package/scripts/setup/configure-twilio-sip-trunk.mjs +207 -0
  653. package/scripts/setup/configure-voice-tunnel.mjs +182 -0
  654. package/scripts/setup/generate-agent-env.mjs +92 -0
  655. package/scripts/setup/generate-agent-package-json.mjs +222 -0
  656. package/scripts/setup/generate-agent-package-json.test.mjs +143 -0
  657. package/scripts/setup/generate-autonomy.mjs +60 -0
  658. package/scripts/setup/generate-backlog.mjs +101 -0
  659. package/scripts/setup/generate-cadences.mjs +92 -0
  660. package/scripts/setup/generate-capability.mjs +162 -0
  661. package/scripts/setup/generate-charter.mjs +76 -0
  662. package/scripts/setup/generate-comms.mjs +59 -0
  663. package/scripts/setup/generate-company.mjs +92 -0
  664. package/scripts/setup/init-agent.sh +547 -0
  665. package/scripts/setup/init-agent.test.mjs +151 -0
  666. package/scripts/setup/init-archetype.mjs +74 -0
  667. package/scripts/setup/init-backup.mjs +54 -0
  668. package/scripts/setup/init-cadence-bus.mjs +60 -0
  669. package/scripts/setup/init-channel-bus.mjs +46 -0
  670. package/scripts/setup/init-cost-tracking.mjs +45 -0
  671. package/scripts/setup/init-decision-capture.mjs +66 -0
  672. package/scripts/setup/init-known-agents.mjs +57 -0
  673. package/scripts/setup/init-learning.mjs +70 -0
  674. package/scripts/setup/init-memory-executive.mjs +45 -0
  675. package/scripts/setup/init-model-router.mjs +124 -0
  676. package/scripts/setup/init-rag.mjs +174 -0
  677. package/scripts/setup/init-session-router.mjs +38 -0
  678. package/scripts/setup/init-slack-socket-mode.mjs +260 -0
  679. package/scripts/setup/init-telegram.mjs +165 -0
  680. package/scripts/setup/init-voice-realtime.mjs +204 -0
  681. package/scripts/setup/init-whatsapp-baileys.mjs +77 -0
  682. package/scripts/setup/install-dev-tools.sh +150 -0
  683. package/scripts/setup/lib/install-plist.mjs +95 -0
  684. package/scripts/setup/migrate-agent-to-sot.mjs +192 -0
  685. package/scripts/setup/render-environment-yaml.mjs +133 -0
  686. package/scripts/slack-events-ctl.sh +177 -0
  687. package/scripts/slack-events-server.mjs +1045 -0
  688. package/scripts/slack-react.mjs +89 -0
  689. package/scripts/slack-responded.sh +232 -0
  690. package/scripts/slack-send.sh +287 -0
  691. package/scripts/slack-typing.mjs +196 -0
  692. package/scripts/slack-upload-v2.py +95 -0
  693. package/scripts/sms-handler.mjs +450 -0
  694. package/scripts/spawn-session.sh +120 -0
  695. package/scripts/sync-protocol.mjs +217 -0
  696. package/scripts/system-verify.sh +184 -0
  697. package/scripts/test-email-thread-dedup.py +239 -0
  698. package/scripts/test-information-barriers.py +484 -0
  699. package/scripts/test-llm-email-dedup.py +251 -0
  700. package/scripts/test-pre-draft-integration.py +203 -0
  701. package/scripts/test-rag-phase2.sh +442 -0
  702. package/scripts/test-rag-search.sh +251 -0
  703. package/scripts/test-voice-parser.mjs +316 -0
  704. package/scripts/user-context-search.py +659 -0
  705. package/scripts/validate_outbound.py +1504 -0
  706. package/scripts/watchdog/ai.maestro.memory-watchdog.plist +41 -0
  707. package/scripts/watchdog/force-reboot.sh +157 -0
  708. package/scripts/watchdog/memory-watchdog.sh +473 -0
  709. package/scripts/whatsapp-handler.mjs +538 -0
  710. package/teams/desktop-operations.yaml +34 -0
  711. package/teams/executive-office.yaml +27 -0
  712. package/teams/legal-and-regulatory.yaml +24 -0
  713. package/teams/platform-and-engineering.yaml +23 -0
  714. package/teams/strategy-and-growth.yaml +29 -0
  715. package/workflows/continuous/backlog-executor.yaml +141 -0
  716. package/workflows/continuous/inbound-monitor.yaml +168 -0
  717. package/workflows/daily/applicant-triage.yaml +197 -0
  718. package/workflows/daily/comms-triage.yaml +80 -0
  719. package/workflows/daily/evening-wrap.yaml +105 -0
  720. package/workflows/daily/morning-brief.yaml +164 -0
  721. package/workflows/daily/slack-followup-sweep.yaml +87 -0
  722. package/workflows/event-driven/README.md +50 -0
  723. package/workflows/event-driven/agent-failure-investigation.yaml +137 -0
  724. package/workflows/event-driven/pr-review.yaml +107 -0
  725. package/workflows/monthly/board-readiness.yaml +76 -0
  726. package/workflows/quarterly/strategic-scenario-analysis.yaml +85 -0
  727. package/workflows/session-protocol.md +171 -0
  728. package/workflows/weekly/engineering-health.yaml +154 -0
  729. package/workflows/weekly/hiring-review.yaml +169 -0
  730. package/workflows/weekly/rollup-pipeline-review.yaml +76 -0
  731. package/workflows/weekly/strategic-memo.yaml +79 -0
@@ -0,0 +1,758 @@
1
+ /**
2
+ * lib/model-router/auth-profiles.mjs — the OpenClaw auth-profile rotation store.
3
+ *
4
+ * A 40-60 agent org runs MULTIPLE keys per provider (several Anthropic / DeepSeek
5
+ * accounts) for throughput isolation. This module turns that pile of keys into a
6
+ * self-healing POOLED quota with per-credential circuit-breaker semantics:
7
+ *
8
+ * loadAuthProfiles(cfg) → per-provider key lists, drawn from
9
+ * config/auth-profiles.yaml AND from
10
+ * env lists (ANTHROPIC_API_KEY,
11
+ * ANTHROPIC_API_KEY_2, …).
12
+ * nextCredential(provider, {now,...}) → round-robin pick that SKIPS keys
13
+ * currently cooling down.
14
+ * markCredentialCoolingDown(provider,key,…) → a 429 / auth error cools THAT key,
15
+ * NOT the whole provider (the
16
+ * provider-level breaker is
17
+ * rate-guard.mjs / health.mjs's job).
18
+ * available(provider, {now}) → how many keys can serve right now.
19
+ *
20
+ * This is the per-CREDENTIAL layer; it composes UNDER the per-BACKEND breaker
21
+ * (health.mjs) and the shared 429 breaker (rate-guard.mjs). One Anthropic key
22
+ * hitting a 429 cools only that key — the pool keeps serving on the siblings, and
23
+ * only when EVERY key in the pool is cooling does the backend look exhausted.
24
+ *
25
+ * State lives at `state/rate-limits/auth-profiles.json` (same dir as the breakers
26
+ * so `maestro doctor` dumps them together):
27
+ * {
28
+ * "<provider>": {
29
+ * "rrIndex": <number — round-robin cursor>,
30
+ * "creds": {
31
+ * "<fingerprint>": { "coolUntil": <epoch ms>, "reason": <str>, "strikes": <n>, "updatedAt": <epoch ms> }
32
+ * }
33
+ * }
34
+ * }
35
+ *
36
+ * Credentials are keyed by a STABLE FINGERPRINT (sha256[:16] of the plaintext),
37
+ * never the plaintext itself — the cooldown ledger NEVER persists a secret to
38
+ * disk. The fingerprint is deterministic across processes so two agents (or two
39
+ * lanes) cooling the same key converge on the same ledger entry.
40
+ *
41
+ * Concurrency: read-modify-write goes through the same O_EXCL lock discipline as
42
+ * health.mjs (openSync(..,"wx"), spin-with-stale-break, atomic tmp+rename). Two
43
+ * concurrent marks on the same key converge to ONE coherent file with monotonic
44
+ * `strikes` instead of clobbering each other.
45
+ *
46
+ * House style (CLAUDE.md): ESM .mjs, Node 20, near-zero-dep (js-yaml is the one
47
+ * parse dep, imported defensively). EVERYTHING injectable so tests are hermetic —
48
+ * `deps.now`, `deps.stateDir`, `deps.agentRoot`, `deps.env` all bypass the real
49
+ * clock/fs/env. NEVER throws on the read path: a missing/corrupt ledger degrades
50
+ * to "no cooldowns" (fail-open-for-work — better one extra request on a key we
51
+ * can't prove is cooling than blocking the whole pool).
52
+ *
53
+ * @module lib/model-router/auth-profiles
54
+ */
55
+
56
+ import {
57
+ existsSync,
58
+ mkdirSync,
59
+ readFileSync,
60
+ writeFileSync,
61
+ renameSync,
62
+ unlinkSync,
63
+ openSync,
64
+ closeSync,
65
+ } from "node:fs";
66
+ import { join, resolve, dirname } from "node:path";
67
+ import { createHash, randomBytes } from "node:crypto";
68
+
69
+ import { sanitizeProvider } from "../rate-guard.mjs";
70
+
71
+ // js-yaml is a hard dep of @cohortapp/agent-sdk, imported defensively so the read
72
+ // path degrades rather than crashes if the dep tree is broken on a machine.
73
+ let yamlParse = null;
74
+ try {
75
+ const y = await import("js-yaml");
76
+ yamlParse = (text) => y.load(text);
77
+ } catch {
78
+ yamlParse = null;
79
+ }
80
+
81
+ const SECOND = 1000;
82
+ const MINUTE = 60 * SECOND;
83
+
84
+ /** Stepped cooldown ladder for a cooled credential: 30s → 1m → 5m, then sticks. */
85
+ const COOLDOWN_LADDER_MS = Object.freeze([30 * SECOND, MINUTE, 5 * MINUTE]);
86
+ /** Default cooldown when a caller passes no explicit ms (uses the ladder). */
87
+ function ladderMsForStrike(strike) {
88
+ const i = Math.max(0, Math.min(COOLDOWN_LADDER_MS.length - 1, strike));
89
+ return COOLDOWN_LADDER_MS[i];
90
+ }
91
+
92
+ /** How long we busy-spin trying to grab the O_EXCL lock before giving up. */
93
+ const LOCK_TIMEOUT_MS = 2 * SECOND;
94
+ const LOCK_SPIN_MS = 5;
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Injected primitives + paths
98
+ // ---------------------------------------------------------------------------
99
+
100
+ function clock(deps) {
101
+ return deps && typeof deps.now === "function" ? deps.now : Date.now;
102
+ }
103
+
104
+ function envOf(deps) {
105
+ return (deps && deps.env) || process.env || {};
106
+ }
107
+
108
+ function resolveStateDir(deps) {
109
+ if (deps && deps.stateDir) return resolve(deps.stateDir);
110
+ const root =
111
+ (deps && deps.agentRoot) ||
112
+ process.env.AGENT_ROOT ||
113
+ process.env.AGENT_DIR ||
114
+ process.cwd();
115
+ return join(resolve(root), "state", "rate-limits");
116
+ }
117
+
118
+ function ledgerPath(deps) {
119
+ return join(resolveStateDir(deps), "auth-profiles.json");
120
+ }
121
+
122
+ function agentRootOf(deps) {
123
+ return resolve(
124
+ (deps && deps.agentRoot) ||
125
+ process.env.AGENT_ROOT ||
126
+ process.env.AGENT_DIR ||
127
+ process.cwd()
128
+ );
129
+ }
130
+
131
+ function authProfilesConfigPath(root) {
132
+ return join(root, "config", "auth-profiles.yaml");
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // Fingerprinting (never persist plaintext)
137
+ // ---------------------------------------------------------------------------
138
+
139
+ /**
140
+ * Stable, deterministic fingerprint of a credential value. We hash the plaintext
141
+ * so the cooldown ledger references a key WITHOUT storing it, and so two
142
+ * processes cooling the same key converge on the same ledger entry. 16 hex chars
143
+ * (64 bits) is collision-resistant for a per-provider key pool.
144
+ *
145
+ * @param {string} value
146
+ * @returns {string}
147
+ */
148
+ export function fingerprint(value) {
149
+ if (typeof value !== "string" || value === "") return "";
150
+ return createHash("sha256").update(value, "utf8").digest("hex").slice(0, 16);
151
+ }
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // loadAuthProfiles — per-provider key list from config + env
155
+ // ---------------------------------------------------------------------------
156
+
157
+ function readTextSafe(path) {
158
+ try {
159
+ if (!existsSync(path)) return null;
160
+ return readFileSync(path, "utf-8");
161
+ } catch {
162
+ return null;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Parse the config/auth-profiles.yaml document into a provider → key[] map. The
168
+ * file shape is intentionally simple and forgiving:
169
+ *
170
+ * providers:
171
+ * deepseek:
172
+ * keys: ["sk-a", "sk-b"] # explicit pool
173
+ * anthropic:
174
+ * env: [ANTHROPIC_API_KEY, ANTHROPIC_API_KEY_2] # names of env vars to pool
175
+ *
176
+ * Either `keys` (literal values) or `env` (env var NAMES to read) may appear; both
177
+ * merge. Bare `provider: [k1, k2]` and `provider: "k1"` are also accepted.
178
+ *
179
+ * @param {object|string|null} doc parsed YAML doc (or raw string, or null)
180
+ * @param {object} env
181
+ * @returns {Map<string,string[]>}
182
+ */
183
+ function profilesFromConfigDoc(doc, env) {
184
+ const out = new Map();
185
+ if (typeof doc === "string") {
186
+ if (!yamlParse) return out;
187
+ try {
188
+ doc = yamlParse(doc);
189
+ } catch {
190
+ return out;
191
+ }
192
+ }
193
+ if (!doc || typeof doc !== "object") return out;
194
+
195
+ const providers = doc.providers && typeof doc.providers === "object" ? doc.providers : doc;
196
+ if (!providers || typeof providers !== "object") return out;
197
+
198
+ for (const [rawName, spec] of Object.entries(providers)) {
199
+ const provider = sanitizeProvider(rawName);
200
+ const keys = [];
201
+ if (Array.isArray(spec)) {
202
+ for (const v of spec) if (typeof v === "string" && v.trim()) keys.push(v.trim());
203
+ } else if (typeof spec === "string") {
204
+ if (spec.trim()) keys.push(spec.trim());
205
+ } else if (spec && typeof spec === "object") {
206
+ const kk = spec.keys;
207
+ if (Array.isArray(kk)) {
208
+ for (const v of kk) if (typeof v === "string" && v.trim()) keys.push(v.trim());
209
+ } else if (typeof kk === "string" && kk.trim()) {
210
+ keys.push(kk.trim());
211
+ }
212
+ const ee = spec.env;
213
+ const envNames = Array.isArray(ee) ? ee : typeof ee === "string" ? [ee] : [];
214
+ for (const name of envNames) {
215
+ if (typeof name !== "string") continue;
216
+ const v = env[name];
217
+ if (typeof v === "string" && v.trim()) keys.push(v.trim());
218
+ }
219
+ }
220
+ if (keys.length) mergeKeys(out, provider, keys);
221
+ }
222
+ return out;
223
+ }
224
+
225
+ /**
226
+ * Pool keys from env-var lists. Convention (OpenClaw api-key-rotation): a base
227
+ * var plus numbered siblings — ANTHROPIC_API_KEY, ANTHROPIC_API_KEY_2,
228
+ * ANTHROPIC_API_KEY_3, … and also ANTHROPIC_API_KEYS="k1,k2,k3" (comma list).
229
+ *
230
+ * The mapping provider → base env var name comes from the catalog's auth_env
231
+ * (passed via opts.authEnvByProvider) so we don't hardcode provider names. We
232
+ * also accept an explicit opts.envBases map. As a convenience, the well-known
233
+ * defaults below are always probed.
234
+ *
235
+ * @param {object} env
236
+ * @param {Map<string,string>} authEnvByProvider provider -> base env var name
237
+ * @returns {Map<string,string[]>}
238
+ */
239
+ function profilesFromEnv(env, authEnvByProvider) {
240
+ const out = new Map();
241
+ for (const [rawProvider, baseVar] of authEnvByProvider) {
242
+ if (typeof baseVar !== "string" || !baseVar) continue;
243
+ const provider = sanitizeProvider(rawProvider);
244
+ const keys = [];
245
+
246
+ // Comma-list form: <BASE>S="k1,k2" (e.g. ANTHROPIC_API_KEYS).
247
+ const listVar = env[`${baseVar}S`] ?? env[`${baseVar}_LIST`];
248
+ if (typeof listVar === "string" && listVar.includes(",")) {
249
+ for (const part of listVar.split(",")) {
250
+ const v = part.trim();
251
+ if (v) keys.push(v);
252
+ }
253
+ }
254
+
255
+ // Base var.
256
+ const base = env[baseVar];
257
+ if (typeof base === "string" && base.trim()) keys.push(base.trim());
258
+
259
+ // Numbered siblings: <BASE>_2, <BASE>_3, … (stop at the first gap, cap 32).
260
+ for (let n = 2; n <= 32; n++) {
261
+ const v = env[`${baseVar}_${n}`];
262
+ if (typeof v === "string" && v.trim()) keys.push(v.trim());
263
+ else break;
264
+ }
265
+
266
+ if (keys.length) mergeKeys(out, provider, keys);
267
+ }
268
+ return out;
269
+ }
270
+
271
+ /** Append keys to a provider's pool, de-duped, preserving first-seen order. */
272
+ function mergeKeys(map, provider, keys) {
273
+ const list = map.get(provider) || [];
274
+ const seen = new Set(list);
275
+ for (const k of keys) {
276
+ if (typeof k !== "string" || !k.trim()) continue;
277
+ const v = k.trim();
278
+ if (seen.has(v)) continue;
279
+ seen.add(v);
280
+ list.push(v);
281
+ }
282
+ map.set(provider, list);
283
+ }
284
+
285
+ /**
286
+ * @typedef {object} AuthProfiles
287
+ * @property {Map<string,string[]>} byProvider provider -> ordered key pool (plaintext)
288
+ * @property {(provider:string)=>string[]} keysFor
289
+ * @property {(provider:string)=>boolean} isPooled true ⇒ >1 key (rotation matters)
290
+ * @property {string[]} providers
291
+ */
292
+
293
+ /**
294
+ * Load the auth-profile key pools. Merges config/auth-profiles.yaml with env-var
295
+ * lists. NEVER throws — a missing config or env yields an empty pool map.
296
+ *
297
+ * @param {object} [cfg] optional agent config (currently informational; the
298
+ * pools come from the dedicated auth-profiles.yaml + env)
299
+ * @param {object} [opts]
300
+ * @param {object} [opts.env] env to read (default process.env)
301
+ * @param {string} [opts.agentRoot]
302
+ * @param {object|string} [opts.configDoc] bypass-FS config doc (tests)
303
+ * @param {Map<string,string>|object} [opts.authEnvByProvider] provider -> base env var
304
+ * @returns {AuthProfiles}
305
+ */
306
+ export function loadAuthProfiles(cfg, opts = {}) {
307
+ const env = envOf(opts);
308
+ const root = agentRootOf(opts);
309
+
310
+ // 1. config/auth-profiles.yaml (or injected configDoc).
311
+ let configDoc = opts.configDoc;
312
+ if (configDoc === undefined) {
313
+ const text = readTextSafe(authProfilesConfigPath(root));
314
+ configDoc = text;
315
+ }
316
+ const fromConfig = profilesFromConfigDoc(configDoc, env);
317
+
318
+ // 2. env-var lists, keyed by the catalog's provider → auth_env mapping.
319
+ const authEnvByProvider = normaliseAuthEnvMap(opts.authEnvByProvider);
320
+ const fromEnv = profilesFromEnv(env, authEnvByProvider);
321
+
322
+ // Merge: config keys first (explicit intent wins ordering), then env siblings.
323
+ const byProvider = new Map();
324
+ for (const [provider, keys] of fromConfig) mergeKeys(byProvider, provider, keys);
325
+ for (const [provider, keys] of fromEnv) mergeKeys(byProvider, provider, keys);
326
+
327
+ return {
328
+ byProvider,
329
+ keysFor(provider) {
330
+ return byProvider.get(sanitizeProvider(provider)) || [];
331
+ },
332
+ isPooled(provider) {
333
+ const k = byProvider.get(sanitizeProvider(provider));
334
+ return Array.isArray(k) && k.length > 1;
335
+ },
336
+ get providers() {
337
+ return [...byProvider.keys()];
338
+ },
339
+ };
340
+ }
341
+
342
+ /** Coerce a Map | plain object | undefined into a Map<provider, baseVar>. */
343
+ function normaliseAuthEnvMap(m) {
344
+ const out = new Map();
345
+ if (m instanceof Map) {
346
+ for (const [p, v] of m) if (typeof v === "string") out.set(sanitizeProvider(p), v);
347
+ } else if (m && typeof m === "object") {
348
+ for (const [p, v] of Object.entries(m)) if (typeof v === "string") out.set(sanitizeProvider(p), v);
349
+ }
350
+ return out;
351
+ }
352
+
353
+ /**
354
+ * Convenience: build the provider → auth_env map loadAuthProfiles wants from a
355
+ * loaded catalog (its `providers` Map / `models` array each carry auth_env).
356
+ * NEVER throws.
357
+ *
358
+ * @param {object} catalog loadCatalog result OR bare catalog
359
+ * @returns {Map<string,string>}
360
+ */
361
+ export function authEnvMapFromCatalog(catalog) {
362
+ const out = new Map();
363
+ let cat = catalog;
364
+ if (catalog && typeof catalog === "object" && !catalog.providers && !Array.isArray(catalog.models) && catalog.catalog) {
365
+ cat = catalog.catalog;
366
+ }
367
+ const consider = (provider, authEnv) => {
368
+ const p = typeof provider === "string" ? provider.trim() : "";
369
+ const a = typeof authEnv === "string" ? authEnv.trim() : "";
370
+ if (!p || !a) return;
371
+ if (!out.has(sanitizeProvider(p))) out.set(sanitizeProvider(p), a);
372
+ };
373
+ const providers = cat && cat.providers;
374
+ if (providers && typeof providers.forEach === "function" && !(providers instanceof Array)) {
375
+ providers.forEach((meta, provider) => consider(provider, meta && meta.auth_env));
376
+ } else if (Array.isArray(providers)) {
377
+ for (const meta of providers) consider(meta && meta.provider, meta && meta.auth_env);
378
+ }
379
+ const models = (cat && cat.models) || [];
380
+ if (Array.isArray(models)) {
381
+ for (const row of models) consider(row && row.provider, row && row.auth_env);
382
+ }
383
+ return out;
384
+ }
385
+
386
+ // ---------------------------------------------------------------------------
387
+ // Cooldown ledger (read/write under O_EXCL lock)
388
+ // ---------------------------------------------------------------------------
389
+
390
+ function readLedgerSafe(deps) {
391
+ const p = ledgerPath(deps);
392
+ if (!existsSync(p)) return {};
393
+ try {
394
+ const raw = JSON.parse(readFileSync(p, "utf-8"));
395
+ return raw && typeof raw === "object" ? raw : {};
396
+ } catch {
397
+ return {};
398
+ }
399
+ }
400
+
401
+ function providerEntry(ledger, provider) {
402
+ const e = ledger[provider];
403
+ if (e && typeof e === "object") {
404
+ return {
405
+ rrIndex: Number.isFinite(Number(e.rrIndex)) ? Number(e.rrIndex) : 0,
406
+ creds: e.creds && typeof e.creds === "object" ? e.creds : {},
407
+ };
408
+ }
409
+ return { rrIndex: 0, creds: {} };
410
+ }
411
+
412
+ function credState(entry, fp) {
413
+ const c = entry.creds[fp];
414
+ if (c && typeof c === "object") {
415
+ return {
416
+ coolUntil: Number.isFinite(Number(c.coolUntil)) ? Number(c.coolUntil) : 0,
417
+ reason: typeof c.reason === "string" ? c.reason : null,
418
+ strikes: Number.isFinite(Number(c.strikes)) ? Number(c.strikes) : 0,
419
+ updatedAt: Number.isFinite(Number(c.updatedAt)) ? Number(c.updatedAt) : 0,
420
+ };
421
+ }
422
+ return { coolUntil: 0, reason: null, strikes: 0, updatedAt: 0 };
423
+ }
424
+
425
+ function isCooling(entry, fp, now) {
426
+ return credState(entry, fp).coolUntil > now;
427
+ }
428
+
429
+ // O_EXCL lock — mirrors health.mjs's withLock so concurrent marks converge.
430
+
431
+ function acquireLock(lockPath, deps) {
432
+ const now = clock(deps);
433
+ const deadline = now() + LOCK_TIMEOUT_MS;
434
+ for (;;) {
435
+ try {
436
+ return openSync(lockPath, "wx"); // O_CREAT | O_EXCL | O_WRONLY
437
+ } catch (err) {
438
+ if (err && err.code === "EEXIST") {
439
+ try {
440
+ const raw = readFileSync(lockPath, "utf-8");
441
+ const heldAt = Number(JSON.parse(raw).at);
442
+ if (Number.isFinite(heldAt) && now() - heldAt > LOCK_TIMEOUT_MS) {
443
+ try { unlinkSync(lockPath); } catch { /* */ }
444
+ continue;
445
+ }
446
+ } catch { /* unreadable lock — spin/timeout */ }
447
+ if (now() >= deadline) return null;
448
+ busyWaitMs(LOCK_SPIN_MS);
449
+ continue;
450
+ }
451
+ return undefined; // e.g. ENOENT on missing dir — caller mkdir+retries
452
+ }
453
+ }
454
+ }
455
+
456
+ function busyWaitMs(ms) {
457
+ const end = Date.now() + ms;
458
+ while (Date.now() < end) { /* spin */ }
459
+ }
460
+
461
+ /**
462
+ * Run `mutator(ledger) → ledger` under an exclusive lock over the whole ledger
463
+ * file and atomically persist it. Returns the persisted ledger. On unrecoverable
464
+ * I/O failure applies the mutation in-memory and returns it (fail-open).
465
+ */
466
+ function withLock(deps, mutator) {
467
+ const file = ledgerPath(deps);
468
+ const lockPath = `${file}.lock`;
469
+ const now = clock(deps);
470
+ try { mkdirSync(dirname(file), { recursive: true }); } catch { /* */ }
471
+
472
+ let fd = acquireLock(lockPath, deps);
473
+ if (fd === undefined) {
474
+ try { mkdirSync(dirname(file), { recursive: true }); } catch { /* */ }
475
+ fd = acquireLock(lockPath, deps);
476
+ }
477
+ if (fd == null) {
478
+ const cur = readLedgerSafe(deps);
479
+ const next = mutator(cur) || cur;
480
+ bestEffortWrite(file, next, deps);
481
+ return next;
482
+ }
483
+ try {
484
+ try { writeFileSync(fd, JSON.stringify({ at: now(), pid: process.pid })); } catch { /* */ }
485
+ const cur = readLedgerSafe(deps);
486
+ const next = mutator(cur) || cur;
487
+ bestEffortWrite(file, next, deps);
488
+ return next;
489
+ } finally {
490
+ try { closeSync(fd); } catch { /* */ }
491
+ try { unlinkSync(lockPath); } catch { /* */ }
492
+ }
493
+ }
494
+
495
+ function bestEffortWrite(file, ledger, deps) {
496
+ try {
497
+ mkdirSync(dirname(file), { recursive: true });
498
+ const tmp = `${file}.tmp.${process.pid}.${Date.now()}.${randomBytes(2).toString("hex")}`;
499
+ writeFileSync(tmp, JSON.stringify(ledger, null, 2) + "\n");
500
+ try {
501
+ renameSync(tmp, file);
502
+ } catch (err) {
503
+ try { unlinkSync(tmp); } catch { /* */ }
504
+ throw err;
505
+ }
506
+ } catch {
507
+ /* fail-open: in-memory only */
508
+ }
509
+ }
510
+
511
+ // ---------------------------------------------------------------------------
512
+ // Public API: nextCredential / markCredentialCoolingDown / available
513
+ // ---------------------------------------------------------------------------
514
+
515
+ /**
516
+ * Pick the next credential for `provider` round-robin, SKIPPING any key whose
517
+ * cooldown has not expired. Advances and persists the round-robin cursor under
518
+ * the lock so concurrent pickers fairly share the pool. NEVER throws.
519
+ *
520
+ * The key pool comes from `opts.profiles` (a loadAuthProfiles result) or
521
+ * `opts.keys` (an explicit string[]). When the pool has 0 or 1 usable key, the
522
+ * single-key behavior is preserved exactly (returns that key, or null).
523
+ *
524
+ * @param {string} provider
525
+ * @param {object} [opts]
526
+ * @param {AuthProfiles} [opts.profiles] from loadAuthProfiles
527
+ * @param {string[]} [opts.keys] explicit pool (overrides profiles)
528
+ * @param {number} [opts.now]
529
+ * @param {object} [opts.deps] { now?, stateDir?, agentRoot? } for the ledger
530
+ * @returns {{ key:string|null, fingerprint:string|null, index:number,
531
+ * poolSize:number, available:number, allCooling:boolean,
532
+ * soonestCoolUntil:number|null }}
533
+ */
534
+ export function nextCredential(provider, opts = {}) {
535
+ const prov = sanitizeProvider(provider);
536
+ const deps = opts.deps || opts; // allow flat opts.now / opts.stateDir too
537
+ const now = Number.isFinite(opts.now) ? opts.now : clock(deps)();
538
+
539
+ const keys = poolKeys(prov, opts);
540
+ const poolSize = keys.length;
541
+
542
+ // Single-key (or empty) pool: preserve today's behavior exactly. No ledger
543
+ // write, no rotation — just return the one key (respecting its cooldown only
544
+ // insofar as we still surface allCooling so the caller can skip if it wants).
545
+ if (poolSize === 0) {
546
+ return { key: null, fingerprint: null, index: -1, poolSize: 0, available: 0, allCooling: false, soonestCoolUntil: null };
547
+ }
548
+ if (poolSize === 1) {
549
+ const key = keys[0];
550
+ const fp = fingerprint(key);
551
+ const led = readLedgerSafe(deps);
552
+ const entry = providerEntry(led, prov);
553
+ const cooling = isCooling(entry, fp, now);
554
+ const cs = credState(entry, fp);
555
+ return {
556
+ key,
557
+ fingerprint: fp,
558
+ index: 0,
559
+ poolSize: 1,
560
+ available: cooling ? 0 : 1,
561
+ allCooling: cooling,
562
+ soonestCoolUntil: cooling ? cs.coolUntil : null,
563
+ };
564
+ }
565
+
566
+ // Multi-key pool: round-robin over the keys, skipping cooled ones. The cursor
567
+ // is advanced + persisted under the lock so two concurrent pickers don't both
568
+ // hand out the same key.
569
+ let chosenIdx = -1;
570
+ let chosenKey = null;
571
+ let chosenFp = null;
572
+ let availableCount = 0;
573
+ let soonest = null;
574
+
575
+ withLock(deps, (ledger) => {
576
+ const entry = providerEntry(ledger, prov);
577
+ const start = ((entry.rrIndex % poolSize) + poolSize) % poolSize;
578
+ // First pass: find the next non-cooling key starting at the cursor.
579
+ for (let step = 0; step < poolSize; step++) {
580
+ const idx = (start + step) % poolSize;
581
+ const key = keys[idx];
582
+ const fp = fingerprint(key);
583
+ if (!isCooling(entry, fp, now)) {
584
+ if (chosenIdx === -1) {
585
+ chosenIdx = idx;
586
+ chosenKey = key;
587
+ chosenFp = fp;
588
+ }
589
+ }
590
+ }
591
+ // Count availability + soonest cooldown across the whole pool (for the caller).
592
+ for (let i = 0; i < poolSize; i++) {
593
+ const fp = fingerprint(keys[i]);
594
+ const cs = credState(entry, fp);
595
+ if (cs.coolUntil > now) {
596
+ if (soonest == null || cs.coolUntil < soonest) soonest = cs.coolUntil;
597
+ } else {
598
+ availableCount++;
599
+ }
600
+ }
601
+ // Advance the cursor to just past the chosen key so the NEXT call rotates on.
602
+ // If everything is cooling, advance by one so we don't pin a single key.
603
+ const advanceTo = chosenIdx === -1 ? (start + 1) % poolSize : (chosenIdx + 1) % poolSize;
604
+ ledger[prov] = { ...entry, rrIndex: advanceTo };
605
+ return ledger;
606
+ });
607
+
608
+ return {
609
+ key: chosenKey,
610
+ fingerprint: chosenFp,
611
+ index: chosenIdx,
612
+ poolSize,
613
+ available: availableCount,
614
+ allCooling: chosenIdx === -1,
615
+ soonestCoolUntil: soonest,
616
+ };
617
+ }
618
+
619
+ /**
620
+ * Cool down ONE credential (the one that just 429'd / auth-failed), NOT the whole
621
+ * provider. Stepped backoff via the ladder unless an explicit `ms` is given.
622
+ * Locked read-modify-write so concurrent marks converge with monotonic strikes
623
+ * and the longer cooldown wins. NEVER throws.
624
+ *
625
+ * The credential may be passed as the plaintext `key` OR a precomputed
626
+ * `{ fingerprint }`; either way only the fingerprint is persisted.
627
+ *
628
+ * @param {string} provider
629
+ * @param {string} key the plaintext credential (fingerprinted internally)
630
+ * @param {string} [reason] FailoverReason-ish label (e.g. "rate_limit", "auth")
631
+ * @param {number|null} [ms] explicit cooldown ms; null/undefined → ladder
632
+ * @param {object} [opts]
633
+ * @param {number} [opts.now]
634
+ * @param {object} [opts.deps]
635
+ * @returns {{ fingerprint:string, coolUntil:number, strikes:number, reason:string|null }}
636
+ */
637
+ export function markCredentialCoolingDown(provider, key, reason, ms, opts = {}) {
638
+ const prov = sanitizeProvider(provider);
639
+ const deps = opts.deps || opts;
640
+ const now = Number.isFinite(opts.now) ? opts.now : clock(deps)();
641
+ const fp = typeof key === "string" ? fingerprint(key) : (key && key.fingerprint) || "";
642
+ if (!fp) {
643
+ return { fingerprint: "", coolUntil: 0, strikes: 0, reason: null };
644
+ }
645
+
646
+ let result = { fingerprint: fp, coolUntil: 0, strikes: 0, reason: reason || null };
647
+ withLock(deps, (ledger) => {
648
+ const entry = providerEntry(ledger, prov);
649
+ const cur = credState(entry, fp);
650
+ const strikes = cur.strikes + 1;
651
+ // strike-1 indexes the ladder from 0 (first hit → 30s).
652
+ const cooldownMs = ms == null ? ladderMsForStrike(strikes - 1) : Math.max(0, Number(ms) || 0);
653
+ const until = cooldownMs === Infinity ? Infinity : now + cooldownMs;
654
+ // Never shorten an existing longer cooldown (a racing shorter mark can't undo it).
655
+ const coolUntil =
656
+ cur.coolUntil === Infinity ? Infinity : Math.max(cur.coolUntil, until);
657
+ const next = {
658
+ coolUntil,
659
+ reason: reason || cur.reason || null,
660
+ strikes,
661
+ updatedAt: now,
662
+ };
663
+ entry.creds[fp] = next;
664
+ ledger[prov] = entry;
665
+ result = { fingerprint: fp, coolUntil, strikes, reason: next.reason };
666
+ return ledger;
667
+ });
668
+ return result;
669
+ }
670
+
671
+ /**
672
+ * Clear a credential's cooldown (clean success). Idempotent + best-effort.
673
+ * NEVER throws.
674
+ *
675
+ * @param {string} provider
676
+ * @param {string} key
677
+ * @param {object} [opts]
678
+ */
679
+ export function markCredentialHealthy(provider, key, opts = {}) {
680
+ const prov = sanitizeProvider(provider);
681
+ const deps = opts.deps || opts;
682
+ const fp = typeof key === "string" ? fingerprint(key) : (key && key.fingerprint) || "";
683
+ if (!fp) return;
684
+ const led = readLedgerSafe(deps);
685
+ const entry = providerEntry(led, prov);
686
+ // No-op if there's nothing to clear (don't churn the file on every success).
687
+ if (!entry.creds[fp]) return;
688
+ withLock(deps, (ledger) => {
689
+ const e = providerEntry(ledger, prov);
690
+ if (e.creds[fp]) delete e.creds[fp];
691
+ ledger[prov] = e;
692
+ return ledger;
693
+ });
694
+ }
695
+
696
+ /**
697
+ * How many credentials in `provider`'s pool can serve right now (not cooling).
698
+ * NEVER throws.
699
+ *
700
+ * @param {string} provider
701
+ * @param {object} [opts] same pool/clock/deps shape as nextCredential
702
+ * @returns {{ count:number, poolSize:number, soonestCoolUntil:number|null }}
703
+ */
704
+ export function available(provider, opts = {}) {
705
+ const prov = sanitizeProvider(provider);
706
+ const deps = opts.deps || opts;
707
+ const now = Number.isFinite(opts.now) ? opts.now : clock(deps)();
708
+ const keys = poolKeys(prov, opts);
709
+ const poolSize = keys.length;
710
+ if (poolSize === 0) return { count: 0, poolSize: 0, soonestCoolUntil: null };
711
+ const led = readLedgerSafe(deps);
712
+ const entry = providerEntry(led, prov);
713
+ let count = 0;
714
+ let soonest = null;
715
+ for (const k of keys) {
716
+ const cs = credState(entry, fingerprint(k));
717
+ if (cs.coolUntil > now) {
718
+ if (soonest == null || cs.coolUntil < soonest) soonest = cs.coolUntil;
719
+ } else {
720
+ count++;
721
+ }
722
+ }
723
+ return { count, poolSize, soonestCoolUntil: soonest };
724
+ }
725
+
726
+ /** Resolve the candidate key pool for a provider from opts.keys | opts.profiles. */
727
+ function poolKeys(provider, opts) {
728
+ if (Array.isArray(opts.keys)) {
729
+ return opts.keys.filter((k) => typeof k === "string" && k.trim()).map((k) => k.trim());
730
+ }
731
+ if (opts.profiles && typeof opts.profiles.keysFor === "function") {
732
+ return opts.profiles.keysFor(provider);
733
+ }
734
+ return [];
735
+ }
736
+
737
+ export const _internals = {
738
+ profilesFromConfigDoc,
739
+ profilesFromEnv,
740
+ mergeKeys,
741
+ ladderMsForStrike,
742
+ COOLDOWN_LADDER_MS,
743
+ readLedgerSafe,
744
+ providerEntry,
745
+ credState,
746
+ poolKeys,
747
+ ledgerPath,
748
+ };
749
+
750
+ export default {
751
+ loadAuthProfiles,
752
+ nextCredential,
753
+ markCredentialCoolingDown,
754
+ markCredentialHealthy,
755
+ available,
756
+ fingerprint,
757
+ authEnvMapFromCatalog,
758
+ };