@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,392 @@
1
+ /**
2
+ * lib/model-router/llm-task.test.mjs — the governed cheap one-shot path.
3
+ *
4
+ * Drives llmTask with an injected fetchImpl (no network) over a hermetic catalog,
5
+ * asserting the component-brief contract:
6
+ * - routes to the CHEAPEST direct-harness model meeting minGrade;
7
+ * - writes ONE ledger row (real writeLedgerRow into a tmp dir);
8
+ * - tool-less by construction (no tools[] in the request body);
9
+ * - non-Anthropic payloads pass through the injected PII mask (§7.4);
10
+ * - HTTP/transport faults classify onto the taxonomy and fail-open (no throw);
11
+ * - the kill switch forces an Anthropic direct row;
12
+ * - missing credential / open breaker / grade floor are honored.
13
+ *
14
+ * House style: node:test, node:assert/strict, hermetic.
15
+ */
16
+
17
+ import { test } from "node:test";
18
+ import assert from "node:assert/strict";
19
+ import { mkdtempSync, rmSync, existsSync, readFileSync, mkdirSync } from "node:fs";
20
+ import { tmpdir } from "node:os";
21
+ import { join } from "node:path";
22
+
23
+ import { llmTask, pickDirectModel, _internals } from "./llm-task.mjs";
24
+ import { loadCatalog } from "./catalog.mjs";
25
+ import { sumLedgerToday } from "./ledger.mjs";
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // A hermetic catalog: a cheap grade-B direct row, a cheaper grade-C row, and an
29
+ // anthropic direct row. We inject raw YAML via catalogText so no FS is touched.
30
+ // ---------------------------------------------------------------------------
31
+
32
+ const CHEAP_YAML = `
33
+ provider: cheapco
34
+ auth_env: CHEAPCO_API_KEY
35
+ endpoints:
36
+ openai: https://api.cheapco.test
37
+ models:
38
+ - id: cheap-b
39
+ status: available
40
+ context_tokens: 200000
41
+ cost: { input: 0.20, output: 0.40 }
42
+ cost_provenance: { source: official, fetched: "2026-06-10", volatile: false }
43
+ compat: { cache_control: none, max_tokens_field: max_tokens }
44
+ tool_reliability: B
45
+ harness: { session: false, direct: true, batch: false }
46
+ - id: cheap-c
47
+ status: available
48
+ context_tokens: 100000
49
+ cost: { input: 0.05, output: 0.10 }
50
+ cost_provenance: { source: official, fetched: "2026-06-10", volatile: false }
51
+ compat: { cache_control: none, max_tokens_field: max_tokens }
52
+ tool_reliability: C
53
+ harness: { session: false, direct: true, batch: false }
54
+ `;
55
+
56
+ const ANTH_YAML = `
57
+ provider: anthropic
58
+ auth_env: ANTHROPIC_API_KEY
59
+ endpoints:
60
+ anthropic: https://api.anthropic.com
61
+ models:
62
+ - id: claude-haiku-4-5
63
+ status: available
64
+ context_tokens: 200000
65
+ cost: { input: 1.0, output: 5.0, cache_read: 0.1, cache_write: 1.25 }
66
+ cost_provenance: { source: official, fetched: "2026-06-10", volatile: false }
67
+ compat: { cache_control: anthropic }
68
+ tool_reliability: A
69
+ harness: { session: true, direct: true, batch: true }
70
+ `;
71
+
72
+ function hermeticCatalog() {
73
+ const res = loadCatalog("/tmp/nope", {
74
+ catalogText: { cheapco: CHEAP_YAML, anthropic: ANTH_YAML },
75
+ });
76
+ assert.ok(res.byRef.size >= 3, "catalog parsed three rows");
77
+ return res;
78
+ }
79
+
80
+ /** A config that permits cheapco for public data (otherwise deny-default). */
81
+ function publicConfig() {
82
+ return {
83
+ schema_version: 2,
84
+ backends: {
85
+ anthropic: { allowed_data_classes: ["public", "internal", "sensitive"] },
86
+ cheapco: { allowed_data_classes: ["public"] },
87
+ },
88
+ defaults: { data_class: "public" },
89
+ };
90
+ }
91
+
92
+ /** A fake OpenAI-compatible response. */
93
+ function openaiResponse(text, usage = {}) {
94
+ return {
95
+ ok: true,
96
+ status: 200,
97
+ async json() {
98
+ return {
99
+ choices: [{ message: { role: "assistant", content: text } }],
100
+ usage: { prompt_tokens: usage.in ?? 100, completion_tokens: usage.out ?? 20 },
101
+ };
102
+ },
103
+ };
104
+ }
105
+
106
+ /** A fake anthropic-messages response. */
107
+ function anthropicResponse(text, usage = {}) {
108
+ return {
109
+ ok: true,
110
+ status: 200,
111
+ async json() {
112
+ return {
113
+ content: [{ type: "text", text }],
114
+ usage: { input_tokens: usage.in ?? 100, output_tokens: usage.out ?? 20 },
115
+ };
116
+ },
117
+ };
118
+ }
119
+
120
+ const baseDeps = (over = {}) => ({
121
+ catalog: hermeticCatalog(),
122
+ config: publicConfig(),
123
+ env: { CHEAPCO_API_KEY: "cheap-key", ANTHROPIC_API_KEY: "anth-key" },
124
+ now: () => 1_700_000_000_000,
125
+ isOpen: () => ({ open: false, until: 0, reason: null, strikes: 0 }),
126
+ ...over,
127
+ });
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // pickDirectModel
131
+ // ---------------------------------------------------------------------------
132
+
133
+ test("pickDirectModel returns the cheapest qualifying direct row for minGrade C", () => {
134
+ const catalog = hermeticCatalog();
135
+ const r = pickDirectModel(catalog, { data_class: "public" }, { ...baseDeps({ catalog }), minGrade: "C" });
136
+ assert.equal(r.row.ref, "cheapco/cheap-c", "cheapest (C) wins at minGrade C");
137
+ });
138
+
139
+ test("pickDirectModel skips below-grade rows for minGrade B", () => {
140
+ const catalog = hermeticCatalog();
141
+ const r = pickDirectModel(catalog, { data_class: "public" }, { ...baseDeps({ catalog }), minGrade: "B" });
142
+ assert.equal(r.row.ref, "cheapco/cheap-b", "cheap-c is grade C, excluded; cheap-b wins");
143
+ });
144
+
145
+ test("pickDirectModel denies cheapco for non-public data_class (deny-default)", () => {
146
+ const catalog = hermeticCatalog();
147
+ const r = pickDirectModel(
148
+ catalog,
149
+ { data_class: "sensitive" },
150
+ { ...baseDeps({ catalog }), minGrade: "C" }
151
+ );
152
+ // cheapco rows denied → anthropic haiku is the only allowed direct row.
153
+ assert.equal(r.row.ref, "anthropic/claude-haiku-4-5");
154
+ });
155
+
156
+ test("pickDirectModel skips a row whose credential is absent", () => {
157
+ const catalog = hermeticCatalog();
158
+ const r = pickDirectModel(
159
+ catalog,
160
+ { data_class: "public" },
161
+ { ...baseDeps({ catalog, env: { ANTHROPIC_API_KEY: "anth-key" } }), minGrade: "C" }
162
+ );
163
+ // No CHEAPCO_API_KEY → cheapco skipped → anthropic haiku.
164
+ assert.equal(r.row.ref, "anthropic/claude-haiku-4-5");
165
+ });
166
+
167
+ test("pickDirectModel skips a row with an open breaker", () => {
168
+ const catalog = hermeticCatalog();
169
+ const r = pickDirectModel(catalog, { data_class: "public" }, {
170
+ ...baseDeps({ catalog }),
171
+ minGrade: "C",
172
+ isOpen: (key) => ({ open: key.startsWith("cheapco"), until: 9e15, reason: "rate_limit", strikes: 1 }),
173
+ });
174
+ assert.equal(r.row.ref, "anthropic/claude-haiku-4-5", "cheapco breaker open → anthropic");
175
+ });
176
+
177
+ // ---------------------------------------------------------------------------
178
+ // llmTask happy path
179
+ // ---------------------------------------------------------------------------
180
+
181
+ test("routes to the cheapest direct model, returns text + usage + estCostUSD, writes a ledger row", async () => {
182
+ const rows = [];
183
+ const out = await llmTask("classify this", {
184
+ ...baseDeps(),
185
+ minGrade: "C",
186
+ request: { task_class: "classify.inbox", data_class: "public", agent: "alex" },
187
+ fetchImpl: async (url, init) => {
188
+ // tool-less invariant: no tools[] ever sent.
189
+ const body = JSON.parse(init.body);
190
+ assert.equal(body.tools, undefined, "no tools[] in the request");
191
+ assert.match(url, /cheapco/, "cheapest cheapco model selected");
192
+ return openaiResponse("spam", { in: 80, out: 4 });
193
+ },
194
+ writeLedger: (catalog, entry) => rows.push(entry),
195
+ });
196
+
197
+ assert.equal(out.ok, true);
198
+ assert.equal(out.ref, "cheapco/cheap-c");
199
+ assert.equal(out.text, "spam");
200
+ assert.equal(out.usage.inputTokens, 80);
201
+ assert.equal(out.usage.outputTokens, 4);
202
+ assert.ok(out.estCostUSD != null);
203
+ // Exactly one ledger row, joined to the task + harness + exit reason.
204
+ assert.equal(rows.length, 1);
205
+ assert.equal(rows[0].ref, "cheapco/cheap-c");
206
+ assert.equal(rows[0].harness, "direct");
207
+ assert.equal(rows[0].task_class, "classify.inbox");
208
+ assert.equal(rows[0].exitReason, "ok");
209
+ assert.equal(rows[0].agent, "alex");
210
+ });
211
+
212
+ test("writes a REAL ledger row to state/cost-tracking via writeLedgerRow", async () => {
213
+ const root = mkdtempSync(join(tmpdir(), "maestro-llmtask-"));
214
+ mkdirSync(join(root, "state", "cost-tracking"), { recursive: true });
215
+ try {
216
+ const out = await llmTask("hi", {
217
+ ...baseDeps(),
218
+ minGrade: "C",
219
+ request: { task_class: "lookup.gmail", data_class: "public" },
220
+ ledgerDeps: { agentRoot: root, now: () => 1_700_000_000_000 },
221
+ fetchImpl: async () => openaiResponse("ok", { in: 50, out: 10 }),
222
+ });
223
+ assert.equal(out.ok, true);
224
+ const sum = sumLedgerToday({ agentRoot: root, now: () => 1_700_000_000_000 });
225
+ assert.equal(sum.sessions, 1, "one ledger row written to disk");
226
+ assert.ok(sum.spentUSD >= 0);
227
+ } finally {
228
+ rmSync(root, { recursive: true, force: true });
229
+ }
230
+ });
231
+
232
+ test("non-Anthropic payload passes through the injected PII mask (§7.4)", async () => {
233
+ let masked = false;
234
+ let sentBody = null;
235
+ const out = await llmTask("email me at a@b.com", {
236
+ ...baseDeps(),
237
+ minGrade: "C",
238
+ request: { data_class: "public" },
239
+ maskInput: (s) => {
240
+ masked = true;
241
+ return s.replace(/[\w.]+@[\w.]+/g, "***");
242
+ },
243
+ fetchImpl: async (url, init) => {
244
+ sentBody = JSON.parse(init.body);
245
+ return openaiResponse("done");
246
+ },
247
+ writeLedger: () => {},
248
+ });
249
+ assert.equal(out.ok, true);
250
+ assert.equal(masked, true, "mask invoked for the cheapco (non-anthropic) route");
251
+ assert.match(sentBody.messages.at(-1).content, /\*\*\*/, "PII masked in the sent prompt");
252
+ assert.ok(!/a@b\.com/.test(sentBody.messages.at(-1).content));
253
+ });
254
+
255
+ test("kill switch forces an Anthropic direct row (mask NOT applied)", async () => {
256
+ let masked = false;
257
+ let url = null;
258
+ const out = await llmTask("anything", {
259
+ ...baseDeps({ env: { CHEAPCO_API_KEY: "k", ANTHROPIC_API_KEY: "a", MAESTRO_ROUTER_FORCE_ANTHROPIC: "1" } }),
260
+ minGrade: "C",
261
+ request: { data_class: "public" },
262
+ maskInput: () => {
263
+ masked = true;
264
+ return "x";
265
+ },
266
+ fetchImpl: async (u) => {
267
+ url = u;
268
+ return anthropicResponse("hello");
269
+ },
270
+ writeLedger: () => {},
271
+ });
272
+ assert.equal(out.ok, true);
273
+ assert.equal(out.provider, "anthropic");
274
+ assert.match(url, /anthropic/);
275
+ assert.equal(masked, false, "anthropic route is not PII-masked");
276
+ });
277
+
278
+ // ---------------------------------------------------------------------------
279
+ // fault classification (fail-open, never throws)
280
+ // ---------------------------------------------------------------------------
281
+
282
+ test("HTTP 429 → rate_limit reason, fail-open (no throw), ledger row written", async () => {
283
+ const rows = [];
284
+ const out = await llmTask("x", {
285
+ ...baseDeps(),
286
+ minGrade: "C",
287
+ request: { data_class: "public" },
288
+ fetchImpl: async () => ({
289
+ ok: false,
290
+ status: 429,
291
+ async json() {
292
+ return { error: { message: "rate limit exceeded" } };
293
+ },
294
+ }),
295
+ writeLedger: (c, e) => rows.push(e),
296
+ });
297
+ assert.equal(out.ok, false);
298
+ assert.equal(out.reason, "rate_limit");
299
+ assert.equal(rows.length, 1);
300
+ assert.equal(rows[0].exitReason, "rate_limit");
301
+ });
302
+
303
+ test("transport throw → classified network, fail-open", async () => {
304
+ const out = await llmTask("x", {
305
+ ...baseDeps(),
306
+ minGrade: "C",
307
+ request: { data_class: "public" },
308
+ fetchImpl: async () => {
309
+ const e = new Error("getaddrinfo ENOTFOUND api.cheapco.test");
310
+ e.code = "ENOTFOUND";
311
+ throw e;
312
+ },
313
+ writeLedger: () => {},
314
+ });
315
+ assert.equal(out.ok, false);
316
+ assert.equal(out.reason, "network");
317
+ });
318
+
319
+ test("2xx with empty content → empty_response soft-fail", async () => {
320
+ const out = await llmTask("x", {
321
+ ...baseDeps(),
322
+ minGrade: "C",
323
+ request: { data_class: "public" },
324
+ fetchImpl: async () => openaiResponse(" "),
325
+ writeLedger: () => {},
326
+ });
327
+ assert.equal(out.ok, false);
328
+ assert.equal(out.reason, "empty_response");
329
+ });
330
+
331
+ test("no fetchImpl → no_fetch_impl, does not throw", async () => {
332
+ const out = await llmTask("x", { ...baseDeps(), request: { data_class: "public" } });
333
+ assert.equal(out.ok, false);
334
+ assert.equal(out.reason, "no_fetch_impl");
335
+ });
336
+
337
+ test("no qualifying candidate (no creds anywhere) → no_direct_candidate", async () => {
338
+ const out = await llmTask("x", {
339
+ ...baseDeps({ env: {} }),
340
+ minGrade: "C",
341
+ request: { data_class: "public" },
342
+ fetchImpl: async () => openaiResponse("y"),
343
+ });
344
+ assert.equal(out.ok, false);
345
+ assert.ok(["missing_credential", "no_direct_candidate"].includes(out.reason));
346
+ });
347
+
348
+ test("opts.repair promotes a leaked tool call in the response text", async () => {
349
+ const out = await llmTask("call the tool", {
350
+ ...baseDeps(),
351
+ minGrade: "C",
352
+ request: { data_class: "public" },
353
+ repair: true,
354
+ repairOpts: { idFor: (n) => `id_${n}` },
355
+ fetchImpl: async () => openaiResponse('```json\n{"name":"do","input":{"k":1}}\n```'),
356
+ writeLedger: () => {},
357
+ });
358
+ assert.equal(out.ok, true);
359
+ assert.equal(out.repaired, true);
360
+ assert.equal(out.toolCalls[0].name, "do");
361
+ assert.deepEqual(out.toolCalls[0].input, { k: 1 });
362
+ });
363
+
364
+ test("anthropic wire sends x-api-key + anthropic-version, parses content[]", async () => {
365
+ let headers = null;
366
+ const out = await llmTask("x", {
367
+ ...baseDeps(),
368
+ minGrade: "A", // forces the anthropic grade-A row
369
+ request: { data_class: "public" },
370
+ fetchImpl: async (url, init) => {
371
+ headers = init.headers;
372
+ assert.match(url, /\/v1\/messages$/);
373
+ return anthropicResponse("hi", { in: 10, out: 2 });
374
+ },
375
+ writeLedger: () => {},
376
+ });
377
+ assert.equal(out.ok, true);
378
+ assert.equal(out.provider, "anthropic");
379
+ assert.equal(headers["x-api-key"], "anth-key");
380
+ assert.equal(headers["anthropic-version"], "2023-06-01");
381
+ assert.equal(out.text, "hi");
382
+ });
383
+
384
+ test("_internals.wireFor + endpointFor pick the right wire per provider", () => {
385
+ const catalog = hermeticCatalog();
386
+ const cheap = catalog.byRef.get("cheapco/cheap-c");
387
+ const anth = catalog.byRef.get("anthropic/claude-haiku-4-5");
388
+ assert.equal(_internals.wireFor(cheap), "openai-completions");
389
+ assert.equal(_internals.wireFor(anth), "anthropic-messages");
390
+ assert.match(_internals.endpointFor(cheap, "openai-completions"), /cheapco.*chat\/completions/);
391
+ assert.match(_internals.endpointFor(anth, "anthropic-messages"), /\/v1\/messages$/);
392
+ });
@@ -0,0 +1,260 @@
1
+ /**
2
+ * lib/model-router/org-credentials.mjs — make the router CONSUME org credentials.
3
+ *
4
+ * THE single-source-of-truth payoff. An org runs 50-100 agents on separate
5
+ * machines; provisioning a DeepSeek/Kimi/OpenAI key onto every one is
6
+ * unmanageable and a breach surface. Instead an admin sets each provider key
7
+ * ONCE in Cohort (credential.put); this module LEASES it short-lived,
8
+ * role-scoped, and audited (credential.lease) at spawn time — the org key never
9
+ * persists on the agent machine.
10
+ *
11
+ * resolveOrgCredentialEnv({catalog, cfg, roles, fetchImpl, now}) walks the
12
+ * router catalog's third-party providers; for each one whose `auth_env` is NOT
13
+ * already set in the local env AND which the org has configured, it leases the
14
+ * value from Cohort and maps provider → auth_env → value into an `envOverlay`.
15
+ * The daemon merges that overlay into the spawn child env BEFORE resolveChain
16
+ * (see the INTEGRATION POINT note below), so the router sees the leased key the
17
+ * same way it would a locally-provisioned one — and a genuinely missing key still
18
+ * surfaces as the router's existing `missing_credential`, never as a crash.
19
+ *
20
+ * DESIGN INVARIANTS:
21
+ * - LOCAL WINS. If `env[auth_env]` is already a non-empty string, we never
22
+ * lease that provider (the agent's own key beats the org broker).
23
+ * - FAIL-OPEN. Cohort unreachable / disabled / errors ⇒ we return whatever
24
+ * local env already has. We NEVER throw and NEVER block a spawn.
25
+ * - NEVER PERSIST. Leased plaintext lives ONLY in this process's in-memory
26
+ * cache (keyed by provider, expiring at the lease TTL) and in the returned
27
+ * overlay the caller merges into the child env. It is NEVER written to disk
28
+ * and NEVER logged.
29
+ * - CACHE. A successful lease is cached for its TTL; a second resolve within
30
+ * the TTL reuses the cached value WITHOUT a second lease call. Expiry
31
+ * triggers a re-lease.
32
+ *
33
+ * INTEGRATION POINT (one-line follow-up, OUT of this lane's files): the spawn
34
+ * site (scripts/daemon/* / lib/model-router/spawn.mjs) must, BEFORE calling
35
+ * resolveChain, do roughly:
36
+ * const { envOverlay } = await resolveOrgCredentialEnv({ catalog, cfg, roles });
37
+ * childEnv = { ...childEnv, ...envOverlay }; // leased keys, local-wins already applied
38
+ * resolveChain then reads childEnv[row.auth_env] exactly as today.
39
+ *
40
+ * Node builtins only. ESM. Pure-core + injectable IO (fetchImpl, env, now, a
41
+ * private cache) so tests are deterministic and never touch the network.
42
+ *
43
+ * @module lib/model-router/org-credentials
44
+ */
45
+
46
+ "use strict";
47
+
48
+ import { credentialLease, isEnabled, configFromAgent } from "../org/client.mjs";
49
+
50
+ /** Providers that ride the stock claude CLI / keychain OAuth — never brokered. */
51
+ const NEVER_BROKER = new Set(["anthropic"]);
52
+
53
+ /** Default lease TTL (ms) when the server's response omits one. 15 minutes. */
54
+ const DEFAULT_LEASE_TTL_MS = 15 * 60 * 1000;
55
+
56
+ /**
57
+ * Process-lifetime in-memory cache of leased credentials, keyed by provider.
58
+ * Value: { authEnv, value, expiresAtMs }. NEVER serialised to disk. A module
59
+ * singleton so successive spawns within a TTL share one lease; tests inject
60
+ * their own cache via opts.cache for isolation.
61
+ * @type {Map<string,{authEnv:string,value:string,expiresAtMs:number}>}
62
+ */
63
+ const GLOBAL_CACHE = new Map();
64
+
65
+ /**
66
+ * Enumerate the catalog's third-party providers and their auth_env. Accepts the
67
+ * loadCatalog result OR a bare catalog ({providers} Map / providers array /
68
+ * models array) — tolerant because the caller passes whatever it has on hand.
69
+ * Returns a de-duped list of { provider, authEnv }; providers we never broker
70
+ * (anthropic) and rows without an auth_env are dropped.
71
+ * @param {object} catalog
72
+ * @returns {{provider:string, authEnv:string}[]}
73
+ */
74
+ function thirdPartyProviders(catalog) {
75
+ // Unwrap a loadCatalog result ({catalog, models, byRef}) to the bare catalog:
76
+ // prefer a top-level that actually carries a providers Map or non-empty models,
77
+ // else fall through to the nested `.catalog`. (An empty `models:[]` on the
78
+ // wrapper must NOT mask the real catalog underneath.)
79
+ let cat = catalog;
80
+ if (catalog && typeof catalog === "object") {
81
+ const topHasProviders = !!catalog.providers;
82
+ const topHasModels = Array.isArray(catalog.models) && catalog.models.length > 0;
83
+ if (!topHasProviders && !topHasModels && catalog.catalog) cat = catalog.catalog;
84
+ }
85
+ const out = new Map(); // provider -> authEnv (first non-null wins)
86
+
87
+ const consider = (provider, authEnv) => {
88
+ const p = typeof provider === "string" ? provider.trim() : "";
89
+ const a = typeof authEnv === "string" ? authEnv.trim() : "";
90
+ if (!p || !a) return;
91
+ if (NEVER_BROKER.has(p)) return;
92
+ if (!out.has(p)) out.set(p, a);
93
+ };
94
+
95
+ // Preferred source: the providers map (provider -> { auth_env, ... }).
96
+ const providers = cat && cat.providers;
97
+ if (providers && typeof providers.forEach === "function" && !(providers instanceof Array)) {
98
+ // Map<string, {auth_env}>
99
+ providers.forEach((meta, provider) => consider(provider, meta && meta.auth_env));
100
+ } else if (Array.isArray(providers)) {
101
+ for (const meta of providers) consider(meta && meta.provider, meta && meta.auth_env);
102
+ }
103
+
104
+ // Fallback / supplement: model rows each carry provider + auth_env.
105
+ const models = (cat && cat.models) || [];
106
+ if (Array.isArray(models)) {
107
+ for (const row of models) consider(row && row.provider, row && row.auth_env);
108
+ }
109
+
110
+ return [...out.entries()].map(([provider, authEnv]) => ({ provider, authEnv }));
111
+ }
112
+
113
+ /**
114
+ * Is a local env var already a usable (non-empty) credential? Local wins, so
115
+ * such a provider is NEVER leased.
116
+ * @param {object} env @param {string} authEnv @returns {boolean}
117
+ */
118
+ function localHas(env, authEnv) {
119
+ const v = env && env[authEnv];
120
+ return typeof v === "string" && v.trim() !== "";
121
+ }
122
+
123
+ /**
124
+ * Extract the leased value + TTL from a credential.lease res frame, tolerant of
125
+ * where the server lands the fields (result.value | result.credential.value;
126
+ * ttlMs | ttl_ms | expiresAt). Returns null when there's no usable plaintext.
127
+ * @param {object} frame res frame {ok,result}
128
+ * @param {number} nowMs
129
+ * @returns {{value:string, expiresAtMs:number}|null}
130
+ */
131
+ function parseLease(frame, nowMs) {
132
+ if (!frame || frame.ok !== true) return null;
133
+ const r = frame.result || {};
134
+ const inner = (r.credential && typeof r.credential === "object") ? r.credential : r;
135
+ const value = inner.value;
136
+ if (typeof value !== "string" || value === "") return null;
137
+
138
+ // Derive expiry: explicit expiresAt wins; else now + ttlMs; else default.
139
+ let expiresAtMs;
140
+ const exp = inner.expiresAt != null ? inner.expiresAt : r.expiresAt;
141
+ const ttl = pickTtlMs(inner, r);
142
+ const parsed = exp != null ? Date.parse(String(exp)) : NaN;
143
+ if (Number.isFinite(parsed)) expiresAtMs = parsed;
144
+ else if (Number.isFinite(ttl) && ttl > 0) expiresAtMs = nowMs + ttl;
145
+ else expiresAtMs = nowMs + DEFAULT_LEASE_TTL_MS;
146
+
147
+ return { value, expiresAtMs };
148
+ }
149
+
150
+ function pickTtlMs(inner, r) {
151
+ for (const v of [inner.ttlMs, inner.ttl_ms, r.ttlMs, r.ttl_ms]) {
152
+ const n = Number(v);
153
+ if (Number.isFinite(n) && n > 0) return n;
154
+ }
155
+ return NaN;
156
+ }
157
+
158
+ /**
159
+ * Resolve org-brokered third-party credentials into an env overlay the spawn
160
+ * site merges into the child env BEFORE resolveChain.
161
+ *
162
+ * For each third-party provider in the catalog whose `auth_env` is NOT already
163
+ * set in the local env: serve it from the in-memory cache if a non-expired lease
164
+ * exists, else lease it from Cohort (credential.lease) and cache the value at
165
+ * its TTL. Providers the org hasn't configured (lease ⇒ NOT_FOUND / error frame)
166
+ * are recorded in `errors` and simply omitted from the overlay — the router then
167
+ * skips that backend with its existing `missing_credential`, exactly as if no
168
+ * key were provisioned. Local-set providers are never leased (local wins).
169
+ *
170
+ * Fail-open everywhere: a disabled org plane or an unreachable Cohort yields an
171
+ * empty overlay (or whatever the cache already holds) and never throws.
172
+ *
173
+ * @param {object} args
174
+ * @param {object} args.catalog loadCatalog result OR a bare catalog
175
+ * @param {object} args.cfg agent config (org.cohort.* drives the client)
176
+ * @param {string[]} [args.roles] the agent's roles (informational; scope is
177
+ * enforced server-side from the verified token)
178
+ * @param {object} [args.env] env to read local keys from (default process.env)
179
+ * @param {function} [args.fetchImpl] injected fetch (tests / determinism)
180
+ * @param {()=>number} [args.now] clock (default Date.now)
181
+ * @param {Map} [args.cache] injectable cache (default the module singleton)
182
+ * @returns {Promise<{envOverlay:Object<string,string>, leased:string[], errors:Array<{provider:string,reason:string}>}>}
183
+ */
184
+ export async function resolveOrgCredentialEnv(args = {}) {
185
+ const catalog = args.catalog;
186
+ const cfg = args.cfg;
187
+ const env = args.env || process.env;
188
+ const fetchImpl = args.fetchImpl;
189
+ const nowFn = typeof args.now === "function" ? args.now : Date.now;
190
+ const cache = args.cache instanceof Map ? args.cache : GLOBAL_CACHE;
191
+
192
+ const envOverlay = {};
193
+ const leased = [];
194
+ const errors = [];
195
+
196
+ // Fail-open: org plane disabled or unconfigured ⇒ nothing to broker, return
197
+ // an empty overlay so the router falls through to whatever local env has.
198
+ if (!isEnabled(cfg)) {
199
+ return { envOverlay, leased, errors };
200
+ }
201
+
202
+ const conn = configFromAgent(cfg);
203
+ const o = { base: conn.base, token: conn.token, fetchImpl };
204
+
205
+ const providers = thirdPartyProviders(catalog);
206
+ if (providers.length === 0) {
207
+ return { envOverlay, leased, errors };
208
+ }
209
+
210
+ const nowMs = nowFn();
211
+
212
+ for (const { provider, authEnv } of providers) {
213
+ // LOCAL WINS — never lease a provider whose key the agent already holds.
214
+ if (localHas(env, authEnv)) continue;
215
+
216
+ // CACHE — reuse a non-expired lease without a second credential.lease call.
217
+ const cached = cache.get(provider);
218
+ if (cached && cached.value && cached.expiresAtMs > nowMs) {
219
+ envOverlay[cached.authEnv || authEnv] = cached.value;
220
+ leased.push(provider);
221
+ continue;
222
+ }
223
+ if (cached && cached.expiresAtMs <= nowMs) cache.delete(provider); // expired
224
+
225
+ // LEASE — short-lived, role-scoped, audited. Fail-open per provider: an
226
+ // error frame (provider not configured / Cohort down / FORBIDDEN_SCOPE)
227
+ // is recorded and the provider is left out of the overlay.
228
+ let frame;
229
+ try {
230
+ frame = await credentialLease({ provider }, o);
231
+ } catch {
232
+ // The client is fail-open and shouldn't throw, but belt-and-braces: a
233
+ // thrown fetchImpl must never crash the spawn path.
234
+ frame = null;
235
+ }
236
+
237
+ const parsed = parseLease(frame, nowMs);
238
+ if (!parsed) {
239
+ const reason =
240
+ (frame && frame.error && frame.error.code) ||
241
+ (frame && frame.ok === false ? "lease_failed" : "unreachable");
242
+ errors.push({ provider, reason: String(reason) });
243
+ continue;
244
+ }
245
+
246
+ cache.set(provider, { authEnv, value: parsed.value, expiresAtMs: parsed.expiresAtMs });
247
+ envOverlay[authEnv] = parsed.value;
248
+ leased.push(provider);
249
+ }
250
+
251
+ return { envOverlay, leased, errors };
252
+ }
253
+
254
+ /**
255
+ * Clear the module-singleton lease cache (test hygiene / forced re-lease, e.g.
256
+ * after a credential.revoke). Does NOT touch an injected cache.
257
+ */
258
+ export function clearOrgCredentialCache() {
259
+ GLOBAL_CACHE.clear();
260
+ }