@cohortapp/agent-sdk 2.17.0 → 2.18.5

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 (531) hide show
  1. package/.claude/settings.json +18 -0
  2. package/.env.example +18 -5
  3. package/README.md +1 -0
  4. package/bin/maestro.mjs +62 -0
  5. package/docs/guides/billing-console-keys.md +60 -0
  6. package/docs/guides/front-door-session.md +54 -9
  7. package/docs/guides/mac-mini.md +20 -25
  8. package/docs/guides/setup-wizard.md +1 -1
  9. package/docs/runbooks/fleet-rollout.md +156 -0
  10. package/docs/runbooks/mac-mini-bootstrap.md +12 -14
  11. package/lib/action-executor.js +19 -3
  12. package/lib/budget-guard.mjs +279 -3
  13. package/lib/channels/base-adapter.mjs +3 -1
  14. package/lib/channels/contract.mjs +2 -1
  15. package/lib/channels/inbox-item.mjs +8 -0
  16. package/lib/claude-bin.mjs +5 -6
  17. package/lib/cli/doctor-checks.mjs +141 -10
  18. package/lib/cli/global-setup-extras.mjs +5 -1
  19. package/lib/cli/inbox.mjs +100 -15
  20. package/lib/cli/seat-auth.mjs +463 -0
  21. package/lib/cli/session.mjs +80 -12
  22. package/lib/collective/capture-slots.mjs +234 -0
  23. package/lib/collective/capture.mjs +8 -6
  24. package/lib/collective/config.mjs +2 -0
  25. package/lib/collective/global-config.mjs +63 -1
  26. package/lib/collective/loop-guard.mjs +155 -0
  27. package/lib/collective/presence.mjs +142 -5
  28. package/lib/comms/send-gate.mjs +559 -1
  29. package/lib/diagnostics/alerts.mjs +49 -0
  30. package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
  31. package/lib/engine/agents/definitions.mjs +343 -0
  32. package/lib/engine/agents/persist.mjs +275 -0
  33. package/lib/engine/agents/runtime.mjs +748 -0
  34. package/lib/engine/agents/usage.mjs +95 -0
  35. package/lib/engine/auth-status.mjs +139 -0
  36. package/lib/engine/budget.mjs +194 -0
  37. package/lib/engine/cli.mjs +1204 -0
  38. package/lib/engine/commands/index.mjs +269 -0
  39. package/lib/engine/context/budget.mjs +219 -0
  40. package/lib/engine/context/cache.mjs +125 -0
  41. package/lib/engine/context/child-env.mjs +215 -0
  42. package/lib/engine/context/compaction.mjs +342 -0
  43. package/lib/engine/context/images.mjs +90 -0
  44. package/lib/engine/context/instructions.mjs +327 -0
  45. package/lib/engine/context/lazy-instructions.mjs +169 -0
  46. package/lib/engine/context/manager.mjs +182 -0
  47. package/lib/engine/context/real-path.mjs +91 -0
  48. package/lib/engine/context/secret-values.mjs +163 -0
  49. package/lib/engine/context/settings.mjs +274 -0
  50. package/lib/engine/context/stream-input.mjs +159 -0
  51. package/lib/engine/guard.mjs +152 -0
  52. package/lib/engine/hooks.mjs +713 -0
  53. package/lib/engine/loop.mjs +560 -0
  54. package/lib/engine/mcp/client.mjs +254 -0
  55. package/lib/engine/mcp/config.mjs +301 -0
  56. package/lib/engine/mcp/http.mjs +201 -0
  57. package/lib/engine/mcp/index.mjs +146 -0
  58. package/lib/engine/mcp/jsonrpc.mjs +147 -0
  59. package/lib/engine/mcp/naming.mjs +66 -0
  60. package/lib/engine/mcp/resources.mjs +89 -0
  61. package/lib/engine/mcp/results.mjs +133 -0
  62. package/lib/engine/mcp/stdio.mjs +137 -0
  63. package/lib/engine/mcp/supervisor.mjs +116 -0
  64. package/lib/engine/messages.mjs +104 -0
  65. package/lib/engine/output/json.mjs +164 -0
  66. package/lib/engine/output/stream-json.mjs +266 -0
  67. package/lib/engine/permissions.mjs +845 -0
  68. package/lib/engine/process-identity.mjs +164 -0
  69. package/lib/engine/process-tree.mjs +551 -0
  70. package/lib/engine/prompt.mjs +60 -0
  71. package/lib/engine/session/store.mjs +299 -0
  72. package/lib/engine/session-runtime/args.mjs +97 -0
  73. package/lib/engine/session-runtime/host.mjs +143 -0
  74. package/lib/engine/session-runtime/inbox.mjs +122 -0
  75. package/lib/engine/session-runtime/notifications.mjs +129 -0
  76. package/lib/engine/session-runtime/registry.mjs +328 -0
  77. package/lib/engine/session-runtime/runner.mjs +344 -0
  78. package/lib/engine/session-runtime/socket.mjs +212 -0
  79. package/lib/engine/session-runtime/wakeup.mjs +115 -0
  80. package/lib/engine/skills/index.mjs +321 -0
  81. package/lib/engine/tools/bash-background.mjs +533 -0
  82. package/lib/engine/tools/bash.mjs +216 -0
  83. package/lib/engine/tools/edit.mjs +97 -0
  84. package/lib/engine/tools/glob.mjs +81 -0
  85. package/lib/engine/tools/grep.mjs +224 -0
  86. package/lib/engine/tools/index.mjs +84 -0
  87. package/lib/engine/tools/list-agents.mjs +32 -0
  88. package/lib/engine/tools/ls.mjs +127 -0
  89. package/lib/engine/tools/monitor.mjs +82 -0
  90. package/lib/engine/tools/notebook-edit.mjs +218 -0
  91. package/lib/engine/tools/read.mjs +103 -0
  92. package/lib/engine/tools/schedule-wakeup.mjs +45 -0
  93. package/lib/engine/tools/schema.mjs +144 -0
  94. package/lib/engine/tools/send-message.mjs +77 -0
  95. package/lib/engine/tools/session.mjs +70 -0
  96. package/lib/engine/tools/todo.mjs +144 -0
  97. package/lib/engine/tools/toolsearch.mjs +217 -0
  98. package/lib/engine/tools/walk.mjs +193 -0
  99. package/lib/engine/tools/web-switch.mjs +31 -0
  100. package/lib/engine/tools/webfetch-html.mjs +387 -0
  101. package/lib/engine/tools/webfetch-net.mjs +340 -0
  102. package/lib/engine/tools/webfetch.mjs +198 -0
  103. package/lib/engine/tools/websearch.mjs +91 -0
  104. package/lib/engine/tools/workflow.mjs +95 -0
  105. package/lib/engine/tools/write.mjs +76 -0
  106. package/lib/engine/tui/line-editor.mjs +137 -0
  107. package/lib/engine/tui/render.mjs +86 -0
  108. package/lib/engine/tui/tui.mjs +274 -0
  109. package/lib/engine/wire/anthropic-messages.mjs +263 -0
  110. package/lib/engine/wire/effort.mjs +36 -0
  111. package/lib/engine/wire/errors.mjs +496 -0
  112. package/lib/engine/wire/http.mjs +441 -0
  113. package/lib/engine/wire/index.mjs +76 -0
  114. package/lib/engine/wire/openai-chat.mjs +332 -0
  115. package/lib/engine/wire/prompt-cache.mjs +79 -0
  116. package/lib/engine/wire/search.mjs +140 -0
  117. package/lib/engine/wire/sse.mjs +114 -0
  118. package/lib/engine/wire/stall.mjs +349 -0
  119. package/lib/engine/wire/token-provider.mjs +175 -0
  120. package/lib/engine/wire/usage.mjs +192 -0
  121. package/lib/engine/workflow/host.mjs +524 -0
  122. package/lib/engine/workflow/journal.mjs +188 -0
  123. package/lib/engine/workflow/json-schema.mjs +171 -0
  124. package/lib/engine/workflow/meta.mjs +329 -0
  125. package/lib/engine/workflow/notifications.mjs +52 -0
  126. package/lib/engine/workflow/runtime.mjs +447 -0
  127. package/lib/engine/workflow/sandbox.mjs +534 -0
  128. package/lib/engine/workflow/worker.mjs +141 -0
  129. package/lib/engine/workflow/worktree.mjs +74 -0
  130. package/lib/execution/disposition.mjs +1 -1
  131. package/lib/execution/intake.mjs +10 -0
  132. package/lib/execution/surface-policy.mjs +15 -0
  133. package/lib/learning/curator.mjs +8 -6
  134. package/lib/learning/reflect.mjs +8 -6
  135. package/lib/model-router/catalog/cohort.yaml +137 -0
  136. package/lib/model-router/catalog.mjs +118 -1
  137. package/lib/model-router/failover.mjs +67 -16
  138. package/lib/model-router/llm-task.mjs +39 -3
  139. package/lib/model-router/resolve.mjs +89 -3
  140. package/lib/model-router/spawn.mjs +46 -47
  141. package/lib/model-router/taxonomy.mjs +126 -4
  142. package/lib/org/cost-sync.mjs +141 -11
  143. package/lib/org/inbound/broadcast.mjs +289 -0
  144. package/lib/org/inbound/collective.mjs +375 -0
  145. package/lib/org/inbound/directedness.mjs +96 -8
  146. package/lib/org/inbound/facts.mjs +78 -2
  147. package/lib/org/inbound/project.mjs +22 -0
  148. package/lib/org/inbound/surfaces.mjs +14 -0
  149. package/lib/org/llm-token.mjs +879 -0
  150. package/lib/org/mesh.mjs +61 -0
  151. package/lib/org/messaging.mjs +3 -1
  152. package/lib/org/protocol.checksum +1 -1
  153. package/lib/org/protocol.mjs +15 -0
  154. package/lib/org/quota.mjs +520 -0
  155. package/lib/org/tool-surface.mjs +104 -16
  156. package/lib/org/ui-parity.mjs +16 -1
  157. package/lib/org/work-ledger.mjs +37 -6
  158. package/lib/rate-guard.mjs +114 -1
  159. package/lib/resource-governor.mjs +41 -6
  160. package/lib/runtime/adapter.mjs +833 -0
  161. package/lib/runtime/child-env.mjs +191 -0
  162. package/lib/runtime/legacy-shell-guard.mjs +97 -0
  163. package/lib/runtime/seat-engine.mjs +162 -0
  164. package/lib/session/ask-ledger.mjs +271 -0
  165. package/lib/session/current-work.mjs +676 -0
  166. package/lib/session/feed-core.mjs +40 -3
  167. package/lib/session/launch-args.mjs +56 -4
  168. package/lib/session/status-summary.mjs +26 -9
  169. package/lib/session/upgrade-notice.mjs +42 -0
  170. package/lib/setup/claude-probe.mjs +117 -13
  171. package/lib/setup/enrich.mjs +13 -10
  172. package/lib/setup/sections/model.mjs +39 -13
  173. package/lib/telemetry/collect.mjs +229 -11
  174. package/lib/upgrade/ignored-drift.mjs +105 -0
  175. package/lib/voice/post-call-brief.mjs +30 -17
  176. package/package.json +13 -3
  177. package/plugins/maestro-skills/skills/board-work.md +5 -0
  178. package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
  179. package/plugins/maestro-skills/skills/main-session.md +18 -7
  180. package/scaffold/config/collective.yaml +7 -0
  181. package/scripts/ci/check-durable-write-seam.mjs +3 -1
  182. package/scripts/ci/check-tarball-fidelity.mjs +126 -2
  183. package/scripts/ci/run-tests.mjs +47 -19
  184. package/scripts/cohort-llm/api-key-helper.mjs +92 -0
  185. package/scripts/collective/hook-runner.mjs +142 -19
  186. package/scripts/continuous-monitor.sh +13 -0
  187. package/scripts/cost/track-claude-usage.mjs +15 -0
  188. package/scripts/daemon/agent-daemon.mjs +408 -20
  189. package/scripts/daemon/assurance.mjs +48 -12
  190. package/scripts/daemon/cadence-consumer.mjs +218 -68
  191. package/scripts/daemon/cadence-handlers.mjs +73 -4
  192. package/scripts/daemon/classifier.mjs +75 -26
  193. package/scripts/daemon/context-compiler.mjs +51 -37
  194. package/scripts/daemon/deliver.mjs +30 -1
  195. package/scripts/daemon/dispatcher.mjs +595 -149
  196. package/scripts/daemon/health.mjs +14 -1
  197. package/scripts/daemon/maestro-daemon.mjs +11 -0
  198. package/scripts/daemon/prompt-builder.mjs +24 -0
  199. package/scripts/daemon/responder.mjs +246 -79
  200. package/scripts/daemon/sdk-version.mjs +98 -16
  201. package/scripts/eval/probe-gateway.mjs +635 -0
  202. package/scripts/eval/replay/extract.mjs +270 -0
  203. package/scripts/eval/replay/grade.mjs +260 -0
  204. package/scripts/eval/replay/lib/config.mjs +50 -0
  205. package/scripts/eval/replay/lib/effects.mjs +65 -0
  206. package/scripts/eval/replay/lib/fixture.mjs +188 -0
  207. package/scripts/eval/replay/lib/judge.mjs +72 -0
  208. package/scripts/eval/replay/lib/redact.mjs +136 -0
  209. package/scripts/eval/replay/lib/sandbox.mjs +170 -0
  210. package/scripts/eval/replay/lib/schema-check.mjs +63 -0
  211. package/scripts/eval/replay/lib/transcript.mjs +76 -0
  212. package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
  213. package/scripts/eval/replay/report.mjs +185 -0
  214. package/scripts/eval/replay/run.mjs +404 -0
  215. package/scripts/fleet/rollout.mjs +1151 -0
  216. package/scripts/hooks/pre-send-audit.sh +36 -245
  217. package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
  218. package/scripts/hooks/validate-state-yaml.sh +190 -0
  219. package/scripts/huddle/huddle-llm.mjs +361 -0
  220. package/scripts/huddle/huddle-server.mjs +46 -121
  221. package/scripts/local-triggers/autoupdate.sh +465 -81
  222. package/scripts/local-triggers/run-trigger.sh +13 -0
  223. package/scripts/maintenance/pin-integrity.mjs +364 -0
  224. package/scripts/poll-slack-events.sh +41 -9
  225. package/scripts/poller/slack-socket-mode.mjs +28 -3
  226. package/scripts/session/supervisor.mjs +80 -13
  227. package/scripts/spawn-session.sh +13 -0
  228. package/bin/maestro.test.mjs +0 -1574
  229. package/lib/action-executor.test.mjs +0 -871
  230. package/lib/archetype.test.mjs +0 -132
  231. package/lib/assurance/plan-note.test.mjs +0 -234
  232. package/lib/assurance/room-budget.test.mjs +0 -486
  233. package/lib/assurance/tier.test.mjs +0 -174
  234. package/lib/autonomy.test.mjs +0 -66
  235. package/lib/backlog.test.mjs +0 -302
  236. package/lib/backup/policy.test.mjs +0 -305
  237. package/lib/budget-escalate.test.mjs +0 -232
  238. package/lib/budget-guard.envelope.test.mjs +0 -476
  239. package/lib/budget-guard.test.mjs +0 -427
  240. package/lib/cadence-bus-requeue.test.mjs +0 -83
  241. package/lib/cadence-bus-schedule.test.mjs +0 -194
  242. package/lib/cadence-bus.test.mjs +0 -720
  243. package/lib/cadences.test.mjs +0 -230
  244. package/lib/capability/inventory.test.mjs +0 -232
  245. package/lib/capability.test.mjs +0 -78
  246. package/lib/channels/base-adapter.test.mjs +0 -590
  247. package/lib/channels/channels.test.mjs +0 -371
  248. package/lib/channels/contract.test.mjs +0 -162
  249. package/lib/channels/inbox-item.test.mjs +0 -368
  250. package/lib/channels/orgmail/adapter.test.mjs +0 -448
  251. package/lib/channels/pairing.test.mjs +0 -270
  252. package/lib/channels/repeat-suppressor.test.mjs +0 -134
  253. package/lib/channels/slack-adapter.test.mjs +0 -212
  254. package/lib/channels/telegram-adapter.test.mjs +0 -306
  255. package/lib/channels/voice/adapter.test.mjs +0 -278
  256. package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
  257. package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
  258. package/lib/charter.test.mjs +0 -89
  259. package/lib/claude-bin.test.mjs +0 -131
  260. package/lib/cli/board.test.mjs +0 -227
  261. package/lib/cli/design.test.mjs +0 -270
  262. package/lib/cli/doctor-checks.test.mjs +0 -336
  263. package/lib/cli/global-setup-extras.test.mjs +0 -462
  264. package/lib/cli/inbox.test.mjs +0 -230
  265. package/lib/cli/session-ack.test.mjs +0 -63
  266. package/lib/cli/session.test.mjs +0 -613
  267. package/lib/collective/capture.test.mjs +0 -121
  268. package/lib/collective/cards.test.mjs +0 -114
  269. package/lib/collective/config.test.mjs +0 -123
  270. package/lib/collective/global-config.test.mjs +0 -220
  271. package/lib/collective/global-skills.test.mjs +0 -126
  272. package/lib/collective/presence.test.mjs +0 -95
  273. package/lib/collective/recall.test.mjs +0 -116
  274. package/lib/collective/vendor-skills.test.mjs +0 -306
  275. package/lib/comms/send-gate.test.mjs +0 -770
  276. package/lib/comms.test.mjs +0 -41
  277. package/lib/context/budget.test.mjs +0 -252
  278. package/lib/context/history-scope.test.mjs +0 -79
  279. package/lib/cost/ledger-row.test.mjs +0 -183
  280. package/lib/design/design-md.test.mjs +0 -318
  281. package/lib/design/fixtures/DESIGN.golden.md +0 -238
  282. package/lib/design/fixtures/PRODUCT.golden.md +0 -67
  283. package/lib/design/fixtures/foundation.json +0 -133
  284. package/lib/design/refresh-gate.test.mjs +0 -144
  285. package/lib/design/write.test.mjs +0 -241
  286. package/lib/diagnostics/alerts.test.mjs +0 -318
  287. package/lib/diagnostics/backup-freshness.test.mjs +0 -185
  288. package/lib/diagnostics/counters.test.mjs +0 -206
  289. package/lib/diagnostics/events.test.mjs +0 -290
  290. package/lib/diagnostics/otel.test.mjs +0 -196
  291. package/lib/diagnostics/trace.test.mjs +0 -251
  292. package/lib/env-compat.test.mjs +0 -104
  293. package/lib/execution/disposition.test.mjs +0 -553
  294. package/lib/execution/drive.test.mjs +0 -270
  295. package/lib/execution/effects.test.mjs +0 -344
  296. package/lib/execution/intake.test.mjs +0 -389
  297. package/lib/execution/journal.test.mjs +0 -261
  298. package/lib/execution/match.test.mjs +0 -235
  299. package/lib/execution/pipeline.test.mjs +0 -392
  300. package/lib/execution/route.test.mjs +0 -186
  301. package/lib/execution/surface-policy.test.mjs +0 -162
  302. package/lib/fs-atomic.test.mjs +0 -72
  303. package/lib/fs-ownership.test.mjs +0 -158
  304. package/lib/goals/admission.test.mjs +0 -164
  305. package/lib/goals/classify.test.mjs +0 -167
  306. package/lib/goals/collaborate.test.mjs +0 -336
  307. package/lib/goals/gaps.test.mjs +0 -284
  308. package/lib/goals/loop.test.mjs +0 -845
  309. package/lib/hooks/bus.test.mjs +0 -387
  310. package/lib/identity/persona.test.mjs +0 -142
  311. package/lib/kpi-sensors.test.mjs +0 -278
  312. package/lib/kpi.test.mjs +0 -244
  313. package/lib/learning/config.test.mjs +0 -75
  314. package/lib/learning/counters.test.mjs +0 -69
  315. package/lib/learning/curator-consolidate.test.mjs +0 -238
  316. package/lib/learning/curator.test.mjs +0 -106
  317. package/lib/learning/reflect.test.mjs +0 -0
  318. package/lib/learning/session-index.test.mjs +0 -125
  319. package/lib/learning/skill-writer.test.mjs +0 -210
  320. package/lib/mandate/audit.test.mjs +0 -195
  321. package/lib/mandate/contract.test.mjs +0 -185
  322. package/lib/mandate/derive.test.mjs +0 -274
  323. package/lib/mandate/model.test.mjs +0 -164
  324. package/lib/mandate/refresh.test.mjs +0 -389
  325. package/lib/mcp/server.test.mjs +0 -426
  326. package/lib/model-router/auth-profiles.test.mjs +0 -580
  327. package/lib/model-router/catalog.test.mjs +0 -385
  328. package/lib/model-router/economics.test.mjs +0 -438
  329. package/lib/model-router/failover.test.mjs +0 -439
  330. package/lib/model-router/health.test.mjs +0 -338
  331. package/lib/model-router/integration-coverage.test.mjs +0 -831
  332. package/lib/model-router/integration.test.mjs +0 -564
  333. package/lib/model-router/ledger.test.mjs +0 -415
  334. package/lib/model-router/llm-task.test.mjs +0 -392
  335. package/lib/model-router/org-credentials.test.mjs +0 -265
  336. package/lib/model-router/pricing-refresh.test.mjs +0 -286
  337. package/lib/model-router/reconcile.test.mjs +0 -316
  338. package/lib/model-router/repair.test.mjs +0 -180
  339. package/lib/model-router/spawn.test.mjs +0 -446
  340. package/lib/model-router/taxonomy.test.mjs +0 -410
  341. package/lib/model-router.test.mjs +0 -1207
  342. package/lib/org/activity.test.mjs +0 -134
  343. package/lib/org/approvals.test.mjs +0 -216
  344. package/lib/org/awareness.test.mjs +0 -159
  345. package/lib/org/board-mine-cache.test.mjs +0 -53
  346. package/lib/org/board.test.mjs +0 -187
  347. package/lib/org/bootstrap-context.test.mjs +0 -153
  348. package/lib/org/client.test.mjs +0 -1206
  349. package/lib/org/cohort-client.test.mjs +0 -126
  350. package/lib/org/cost-sync.test.mjs +0 -153
  351. package/lib/org/doctor.test.mjs +0 -346
  352. package/lib/org/engagement-ledger.test.mjs +0 -112
  353. package/lib/org/engagement.test.mjs +0 -739
  354. package/lib/org/handoff.test.mjs +0 -269
  355. package/lib/org/inbound/directedness.test.mjs +0 -668
  356. package/lib/org/inbound/facts.test.mjs +0 -471
  357. package/lib/org/inbound/hydrate.test.mjs +0 -908
  358. package/lib/org/inbound/index.test.mjs +0 -429
  359. package/lib/org/inbound/project.test.mjs +0 -287
  360. package/lib/org/integration-tools.test.mjs +0 -160
  361. package/lib/org/keys.test.mjs +0 -92
  362. package/lib/org/knowledge.test.mjs +0 -326
  363. package/lib/org/leases.test.mjs +0 -235
  364. package/lib/org/mesh-directives.test.mjs +0 -110
  365. package/lib/org/mesh-integration.test.mjs +0 -127
  366. package/lib/org/mesh.test.mjs +0 -400
  367. package/lib/org/messaging.test.mjs +0 -471
  368. package/lib/org/param-contract.test.mjs +0 -477
  369. package/lib/org/policy.test.mjs +0 -237
  370. package/lib/org/protocol.checksum.test.mjs +0 -90
  371. package/lib/org/protocol.test.mjs +0 -323
  372. package/lib/org/push.test.mjs +0 -792
  373. package/lib/org/registry.test.mjs +0 -100
  374. package/lib/org/resource-tools.test.mjs +0 -361
  375. package/lib/org/tool-access.test.mjs +0 -144
  376. package/lib/org/tool-surface-integration.test.mjs +0 -120
  377. package/lib/org/tool-surface.test.mjs +0 -1268
  378. package/lib/org/typing.test.mjs +0 -291
  379. package/lib/org/ui-parity.test.mjs +0 -560
  380. package/lib/org/verify.test.mjs +0 -194
  381. package/lib/org/work-ledger.test.mjs +0 -273
  382. package/lib/plan/adoption-e2e.test.mjs +0 -366
  383. package/lib/plan/budget-enforcement.test.mjs +0 -400
  384. package/lib/plan/compile.test.mjs +0 -382
  385. package/lib/plan/emit.test.mjs +0 -269
  386. package/lib/plan/explain.test.mjs +0 -188
  387. package/lib/prompts/parallelism.test.mjs +0 -177
  388. package/lib/rag/rag.test.mjs +0 -505
  389. package/lib/rate-guard.test.mjs +0 -272
  390. package/lib/reactive-gate.test.mjs +0 -57
  391. package/lib/render.test.mjs +0 -68
  392. package/lib/resource-governor.test.mjs +0 -488
  393. package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
  394. package/lib/scheduling/jitter.test.mjs +0 -140
  395. package/lib/secrets/broker.test.mjs +0 -280
  396. package/lib/secrets/providers.test.mjs +0 -274
  397. package/lib/security/audit-engine.test.mjs +0 -424
  398. package/lib/security/coerce-args.test.mjs +0 -281
  399. package/lib/security/dangerous-tools.test.mjs +0 -68
  400. package/lib/security/external-content.test.mjs +0 -84
  401. package/lib/security/redact.test.mjs +0 -441
  402. package/lib/security/secret-equal.test.mjs +0 -55
  403. package/lib/session/config.test.mjs +0 -92
  404. package/lib/session/feed-core.test.mjs +0 -198
  405. package/lib/session/first-run.test.mjs +0 -121
  406. package/lib/session/frontdoor.test.mjs +0 -205
  407. package/lib/session/handoffs.test.mjs +0 -183
  408. package/lib/session/identity.test.mjs +0 -180
  409. package/lib/session/inbox-claims.test.mjs +0 -286
  410. package/lib/session/launch-args.test.mjs +0 -157
  411. package/lib/session/liveness.test.mjs +0 -100
  412. package/lib/session/status-summary.test.mjs +0 -118
  413. package/lib/session-permissions.test.mjs +0 -120
  414. package/lib/setup/claude-probe.test.mjs +0 -187
  415. package/lib/setup/completeness.test.mjs +0 -110
  416. package/lib/setup/context-pack.test.mjs +0 -89
  417. package/lib/setup/enrich.test.mjs +0 -115
  418. package/lib/setup/enroll-from-cohort.test.mjs +0 -300
  419. package/lib/setup/integration.test.mjs +0 -162
  420. package/lib/setup/io.test.mjs +0 -77
  421. package/lib/setup/runner.test.mjs +0 -132
  422. package/lib/setup/sections/identity.test.mjs +0 -234
  423. package/lib/setup/sections/inventory.test.mjs +0 -198
  424. package/lib/setup/sections/learning.test.mjs +0 -81
  425. package/lib/setup/sections/mandate.test.mjs +0 -388
  426. package/lib/setup/sections/messaging.test.mjs +0 -127
  427. package/lib/setup/sections/model.test.mjs +0 -240
  428. package/lib/setup/sections/org.test.mjs +0 -346
  429. package/lib/setup/sections/orgmail.test.mjs +0 -118
  430. package/lib/setup/sections/recovery.test.mjs +0 -98
  431. package/lib/setup/sections/subagents.test.mjs +0 -429
  432. package/lib/setup/sections/verify.test.mjs +0 -175
  433. package/lib/setup/sot.test.mjs +0 -81
  434. package/lib/setup/state.test.mjs +0 -115
  435. package/lib/singleton.test.mjs +0 -151
  436. package/lib/subagents/cli.test.mjs +0 -389
  437. package/lib/subagents/client.test.mjs +0 -309
  438. package/lib/subagents/gap.test.mjs +0 -234
  439. package/lib/subagents/lock.test.mjs +0 -248
  440. package/lib/subagents/manifest.test.mjs +0 -175
  441. package/lib/subagents/refs.test.mjs +0 -204
  442. package/lib/subagents/resolve.test.mjs +0 -422
  443. package/lib/subagents/schema.test.mjs +0 -328
  444. package/lib/telemetry/alerts.test.mjs +0 -109
  445. package/lib/telemetry/collect.test.mjs +0 -1274
  446. package/lib/tool-definitions-integration.test.mjs +0 -83
  447. package/lib/tool-definitions.test.mjs +0 -437
  448. package/lib/upgrade/global-refresh.test.mjs +0 -65
  449. package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
  450. package/lib/upgrade/post-steps.test.mjs +0 -200
  451. package/lib/upgrade/verify.test.mjs +0 -164
  452. package/lib/util/fetch-timeout.test.mjs +0 -202
  453. package/lib/util/reconnect.test.mjs +0 -369
  454. package/lib/util/unhandled.test.mjs +0 -216
  455. package/lib/voice/outbound.test.mjs +0 -69
  456. package/lib/voice/session-rotation.test.mjs +0 -114
  457. package/lib/voice/stt.test.mjs +0 -226
  458. package/lib/voice/voice.test.mjs +0 -990
  459. package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
  460. package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
  461. package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
  462. package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
  463. package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
  464. package/scripts/ci/check-skill-packs.test.mjs +0 -495
  465. package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
  466. package/scripts/ci/check.test.mjs +0 -194
  467. package/scripts/ci/conformance-org-api.test.mjs +0 -425
  468. package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
  469. package/scripts/collective/hook-runner.test.mjs +0 -173
  470. package/scripts/cost/fleet-digest.test.mjs +0 -207
  471. package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
  472. package/scripts/cost/track-claude-usage.test.mjs +0 -148
  473. package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
  474. package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
  475. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
  476. package/scripts/daemon/agent-daemon.test.mjs +0 -995
  477. package/scripts/daemon/assurance-e2e.test.mjs +0 -613
  478. package/scripts/daemon/assurance.test.mjs +0 -1791
  479. package/scripts/daemon/board-mirror.test.mjs +0 -165
  480. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
  481. package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
  482. package/scripts/daemon/cadence-consumer.test.mjs +0 -776
  483. package/scripts/daemon/cadence-handlers.test.mjs +0 -837
  484. package/scripts/daemon/classifier-identity.test.mjs +0 -137
  485. package/scripts/daemon/classifier.test.mjs +0 -266
  486. package/scripts/daemon/classify-kind.test.mjs +0 -40
  487. package/scripts/daemon/context-compiler.test.mjs +0 -406
  488. package/scripts/daemon/deliver.test.mjs +0 -564
  489. package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
  490. package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
  491. package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
  492. package/scripts/daemon/dispatcher-session-continuity.test.mjs +0 -365
  493. package/scripts/daemon/execution-ladder.test.mjs +0 -470
  494. package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
  495. package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
  496. package/scripts/daemon/inbox-deferral.test.mjs +0 -336
  497. package/scripts/daemon/inbox-wake.test.mjs +0 -199
  498. package/scripts/daemon/integration.test.mjs +0 -149
  499. package/scripts/daemon/lib/self-echo.test.mjs +0 -153
  500. package/scripts/daemon/lib/session-router.test.mjs +0 -554
  501. package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
  502. package/scripts/daemon/prompt-builder.test.mjs +0 -556
  503. package/scripts/daemon/responder-cost.test.mjs +0 -68
  504. package/scripts/daemon/responder-history.test.mjs +0 -221
  505. package/scripts/daemon/sdk-version.test.mjs +0 -31
  506. package/scripts/daemon/session-lock.test.mjs +0 -252
  507. package/scripts/daemon/session-outcomes.test.mjs +0 -533
  508. package/scripts/daemon/typing-registry.test.mjs +0 -102
  509. package/scripts/hooks/pre-send-audit.test.mjs +0 -354
  510. package/scripts/huddle/huddle-prompt.test.mjs +0 -176
  511. package/scripts/local-triggers/autoupdate.test.mjs +0 -518
  512. package/scripts/local-triggers/generate-plists.test.mjs +0 -456
  513. package/scripts/media-generation/brand-clause.test.mjs +0 -135
  514. package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
  515. package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
  516. package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
  517. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
  518. package/scripts/poller/slack-socket-mode.test.mjs +0 -805
  519. package/scripts/poller-launchd/install.test.mjs +0 -243
  520. package/scripts/restore-from-backup.test.mjs +0 -181
  521. package/scripts/session/feed.test.mjs +0 -196
  522. package/scripts/session/supervisor-sh.test.mjs +0 -218
  523. package/scripts/session/supervisor.test.mjs +0 -482
  524. package/scripts/setup/configure-macos.test.mjs +0 -306
  525. package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
  526. package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
  527. package/scripts/setup/generate-capability.test.mjs +0 -134
  528. package/scripts/setup/init-agent.test.mjs +0 -370
  529. package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
  530. package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
  531. package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
@@ -18,7 +18,26 @@
18
18
  * House style: ESM, Node built-ins only, mirrors rate-guard's deps-injection.
19
19
  */
20
20
 
21
- import { DISPOSITION, asFailoverReason, cooldownMsFor } from "./taxonomy.mjs";
21
+ import { DISPOSITION, asFailoverReason, cooldownMsFor, MAX_SAME_RETRY_AFTER_MS } from "./taxonomy.mjs";
22
+
23
+ function defaultSleep(ms) {
24
+ return new Promise((resolve) => setTimeout(resolve, ms));
25
+ }
26
+
27
+ /**
28
+ * The cohort refusal an attempt reported (a normalised §4.8 object, as
29
+ * lib/org/quota.mjs `parseRefusalBody` returns), or null.
30
+ * @param {object|undefined} meta
31
+ */
32
+ function refusalOf(meta) {
33
+ const r = meta && meta.refusal;
34
+ return r && typeof r === "object" && typeof r.code === "string" && r.code.startsWith("cohort_") ? r : null;
35
+ }
36
+
37
+ /** A finite, non-negative retry-after (ms) an attempt reported, or null. */
38
+ function retryAfterOf(meta) {
39
+ return meta && Number.isFinite(meta.retryAfterMs) && meta.retryAfterMs >= 0 ? meta.retryAfterMs : null;
40
+ }
22
41
 
23
42
  /**
24
43
  * @typedef {object} Candidate
@@ -74,6 +93,15 @@ function cooldownKey(candidate, scope) {
74
93
  * may advance ONCE (the "one Anthropic-only re-route
75
94
  * permitted" rule). Default false (sticky refusal).
76
95
  * maxAttempts → hard cap on total attempts (safety net; default 32)
96
+ * sleep(ms) → injected sleep for a disposition's retryBackoffMs
97
+ * ladder (request_timeout). Default: a real timer.
98
+ *
99
+ * A cohort refusal is handed back, never scheduled here. When the final attempt
100
+ * reported `meta.refusal` (budget_refusal, gateway_retry) the result carries it
101
+ * as `refusal`, with `retryAfterMs` when the response named one. The daemon's
102
+ * one scheduler for cohort holds is `rateGuard.recordCohortRefusal(refusal)`;
103
+ * stepChain computes no wake time for these reasons (CF-143: the earlier
104
+ * gateway_retry wake had no production caller — see docs/engine/README.md).
77
105
  *
78
106
  * @param {Candidate[]} chain
79
107
  * @param {object} opts
@@ -83,7 +111,9 @@ function cooldownKey(candidate, scope) {
83
111
  * chosen?: Candidate,
84
112
  * reason: string|null, // final reason (null on success)
85
113
  * tried: Array<{ref, reason, retry?:boolean, meta?:object}>,
86
- * soonestCooldownExpiry: number|null, // epoch ms to re-queue at, or null
114
+ * soonestCooldownExpiry: number|null, // epoch ms a BREAKER cooldown ends, or null
115
+ * refusal?: object|null, // the final attempt's cohort refusal (resetsAt, window, scope)
116
+ * retryAfterMs?: number|null, // the final attempt's retry-after
87
117
  * attempts: number
88
118
  * }>}
89
119
  */
@@ -94,6 +124,7 @@ export async function stepChain(chain, opts = {}) {
94
124
  const onAttempt = typeof opts.onAttempt === "function" ? opts.onAttempt : null;
95
125
  const allowContentReroute = opts.allowContentReroute === true;
96
126
  const maxAttempts = Number.isFinite(opts.maxAttempts) ? opts.maxAttempts : 32;
127
+ const sleep = typeof opts.sleep === "function" ? opts.sleep : defaultSleep;
97
128
 
98
129
  const tried = [];
99
130
  const candidates = Array.isArray(chain) ? chain.filter(Boolean) : [];
@@ -142,14 +173,14 @@ export async function stepChain(chain, opts = {}) {
142
173
 
143
174
  // ── Attempt this candidate, with same-candidate retry where allowed ──
144
175
  let advanced = false;
145
- let retriedThisCandidate = false;
176
+ let sameRetries = 0;
146
177
  while (attempts < maxAttempts) {
147
178
  attempts++;
148
179
  let res;
149
180
  try {
150
181
  res = await attempt(candidate, {
151
182
  attemptIndex: attempts - 1,
152
- retryOfRef: retriedThisCandidate ? candidate.ref : null,
183
+ retryOfRef: sameRetries > 0 ? candidate.ref : null,
153
184
  now: now(),
154
185
  });
155
186
  } catch (err) {
@@ -182,7 +213,7 @@ export async function stepChain(chain, opts = {}) {
182
213
  const record = {
183
214
  ref: candidate.ref,
184
215
  reason,
185
- retry: retriedThisCandidate || undefined,
216
+ retry: sameRetries > 0 || undefined,
186
217
  meta: res.meta,
187
218
  };
188
219
  tried.push(record);
@@ -212,13 +243,25 @@ export async function stepChain(chain, opts = {}) {
212
243
  }
213
244
 
214
245
  // ── retry-same? ──────────────────────────────────────────────────
215
- const canRetrySame =
216
- !retriedThisCandidate &&
217
- d.retrySame &&
218
- d.retrySame !== "cli"; // "cli" means the CLI already retried internally
219
- if (canRetrySame) {
220
- retriedThisCandidate = true;
221
- continue; // retry the SAME candidate once
246
+ // A row with a retryBackoffMs ladder retries once per entry, sleeping
247
+ // first (request_timeout, CF-143); any other retrying row retries once,
248
+ // immediately. "cli" means the CLI already retried internally.
249
+ const ladder = Array.isArray(d.retryBackoffMs) ? d.retryBackoffMs : null;
250
+ const sameBudget = ladder ? ladder.length : d.retrySame && d.retrySame !== "cli" ? 1 : 0;
251
+ if (sameRetries < sameBudget) {
252
+ if (ladder) {
253
+ const ra = retryAfterOf(res.meta);
254
+ // A retry-after longer than the ceiling is not slept through: fail over.
255
+ if (ra != null && ra > MAX_SAME_RETRY_AFTER_MS) {
256
+ sameRetries = sameBudget;
257
+ } else {
258
+ await sleep(ra != null ? ra : ladder[sameRetries]);
259
+ }
260
+ }
261
+ if (sameRetries < sameBudget) {
262
+ sameRetries++;
263
+ continue; // retry the SAME candidate
264
+ }
222
265
  }
223
266
 
224
267
  // ── failover-next? ───────────────────────────────────────────────
@@ -232,12 +275,15 @@ export async function stepChain(chain, opts = {}) {
232
275
  break; // advance to next candidate
233
276
  }
234
277
  // No failover for this reason (context_overflow / payload_too_large /
235
- // network / sticky content_policy). Terminate the whole walk.
278
+ // network / sticky content_policy / budget_refusal / gateway_retry).
279
+ // Terminate the whole walk; a cohort refusal rides back to the caller.
236
280
  return {
237
281
  ok: false,
238
282
  reason,
239
283
  tried,
240
284
  soonestCooldownExpiry, // null for network/overflow (no cooldown)
285
+ refusal: refusalOf(res.meta),
286
+ retryAfterMs: retryAfterOf(res.meta),
241
287
  attempts,
242
288
  chosen: undefined,
243
289
  };
@@ -251,11 +297,14 @@ export async function stepChain(chain, opts = {}) {
251
297
  }
252
298
 
253
299
  // ── Chain exhausted ──────────────────────────────────────────────────
300
+ const lastMeta = tried.length ? tried[tried.length - 1].meta : undefined;
254
301
  return {
255
302
  ok: false,
256
303
  reason: lastReason || "unknown",
257
304
  tried,
258
305
  soonestCooldownExpiry,
306
+ refusal: refusalOf(lastMeta),
307
+ retryAfterMs: retryAfterOf(lastMeta),
259
308
  attempts,
260
309
  chosen: undefined,
261
310
  };
@@ -288,11 +337,13 @@ export function summarizeExhaustion(summary) {
288
337
  const trail = (summary.tried || [])
289
338
  .map((t) => `${t.ref ?? "?"}=${t.reason}`)
290
339
  .join(" → ");
291
- const wake =
292
- summary.soonestCooldownExpiry == null
340
+ const r = summary.refusal;
341
+ const wake = r
342
+ ? `cohort refusal ${r.code}${r.resetsAt ? ` (resets ${r.resetsAt})` : ""}; hold the seat via rate-guard, do not re-queue on this summary`
343
+ : summary.soonestCooldownExpiry == null
293
344
  ? "no cooldown (re-queue immediately / DEFER)"
294
345
  : `wake at ${new Date(summary.soonestCooldownExpiry).toISOString()}`;
295
346
  return `chain exhausted (final=${summary.reason}): ${trail}; ${wake}`;
296
347
  }
297
348
 
298
- export const _internals = { cooldownKey, tableCooldownUntil, noopBreaker };
349
+ export const _internals = { cooldownKey, tableCooldownUntil, noopBreaker, refusalOf, retryAfterOf };
@@ -36,8 +36,9 @@
36
36
  import { loadCatalogCached, listModels, lookupModel } from "./catalog.mjs";
37
37
  import { estimateCost, writeLedgerRow } from "./ledger.mjs";
38
38
  import { keyFor, isOpen as healthIsOpen } from "./health.mjs";
39
- import { classifyHttp, classifyTransportError } from "./taxonomy.mjs";
39
+ import { classifyHttp, classifyTransportError, DISPOSITION } from "./taxonomy.mjs";
40
40
  import { repairToolCalls } from "./repair.mjs";
41
+ import { parseRefusalBody } from "../org/quota.mjs";
41
42
 
42
43
  const GRADE_RANK = { A: 3, B: 2, C: 1 };
43
44
  const DATA_CLASSES = new Set(["public", "internal", "sensitive"]);
@@ -389,7 +390,21 @@ export async function llmTask(prompt, opts = {}) {
389
390
  if (!ok) {
390
391
  const reason = classifyHttp(status, parsedBody ?? rawText ?? null);
391
392
  writeRow(catalog, row, wire, {}, opts, request, reason);
392
- return fail(reason, { model: row.id, provider: row.provider, ref: row.ref, wire, status });
393
+ // retry-after (seconds) rides along so a caller can honour it.
394
+ let retryAfterMs = null;
395
+ try {
396
+ const ra = res && res.headers && typeof res.headers.get === "function" ? res.headers.get("retry-after") : null;
397
+ const secs = ra == null || ra === "" ? NaN : Number(ra);
398
+ if (Number.isFinite(secs) && secs >= 0) retryAfterMs = Math.round(secs * 1000);
399
+ } catch { /* advisory */ }
400
+ // A cohort refusal object (§4.8: code, window, scope, resetsAt, requestId)
401
+ // is preserved verbatim, so a scheduler can hold until resetsAt
402
+ // (rateGuard.recordCohortRefusal takes exactly this shape).
403
+ const refusal = safe(() => parseRefusalBody(parsedBody ?? rawText ?? null), null);
404
+ const extra = { model: row.id, provider: row.provider, ref: row.ref, wire, status, retryAfterMs, refusal };
405
+ // A budget refusal is typed non-retryable (x-should-retry: false, CF-143).
406
+ if (reason === "budget_refusal") extra.retryable = false;
407
+ return fail(reason, extra);
393
408
  }
394
409
 
395
410
  const { text, usage } = parseResponse(parsedBody, wire);
@@ -453,6 +468,27 @@ export async function llmTask(prompt, opts = {}) {
453
468
  return out;
454
469
  }
455
470
 
471
+ /**
472
+ * An llmTask result as a stepChain AttemptResult: the reason drives the
473
+ * disposition table, and `meta` carries what the table and the caller need —
474
+ * the status, retry-after (a request_timeout ladder honours it) and the cohort
475
+ * refusal (stepChain hands it back as `outcome.refusal`). PURE.
476
+ *
477
+ * @param {object} res an llmTask result
478
+ * @returns {{ok:boolean, reason?:string, value?:object, meta:{status:number|null, retryAfterMs:number|null, refusal:object|null, retryable?:boolean}}}
479
+ */
480
+ export function attemptResultOf(res) {
481
+ if (!res || typeof res !== "object") return { ok: false, reason: "unknown", meta: { status: null, retryAfterMs: null, refusal: null } };
482
+ const meta = {
483
+ status: Number.isFinite(res.status) ? res.status : null,
484
+ retryAfterMs: Number.isFinite(res.retryAfterMs) ? res.retryAfterMs : null,
485
+ refusal: res.refusal || null,
486
+ };
487
+ if (typeof res.retryable === "boolean") meta.retryable = res.retryable;
488
+ if (res.ok === true) return { ok: true, value: res, meta };
489
+ return { ok: false, reason: DISPOSITION[res.reason] ? res.reason : "unknown", meta };
490
+ }
491
+
456
492
  /** Pick the cheapest anthropic direct row (kill-switch path). */
457
493
  function pickAnthropicDirect(catalog, request, opts) {
458
494
  const minRank = GRADE_RANK[opts.minGrade || request.minGrade || "C"] || GRADE_RANK.C;
@@ -512,4 +548,4 @@ export const _internals = {
512
548
  credentialEnvFor,
513
549
  };
514
550
 
515
- export default { llmTask, pickDirectModel };
551
+ export default { llmTask, pickDirectModel, attemptResultOf };
@@ -34,6 +34,7 @@
34
34
  */
35
35
 
36
36
  import { loadCatalogCached, lookupModel } from "./catalog.mjs";
37
+ import { hasSeatCredential, apiKeyHelperCommand } from "../org/llm-token.mjs";
37
38
  import { estimateCost } from "./ledger.mjs";
38
39
  import { keyFor, isOpen as breakerIsOpen } from "./health.mjs";
39
40
  import { nextCredential as authNextCredential, available as authAvailable } from "./auth-profiles.mjs";
@@ -364,7 +365,10 @@ function gateCandidate(row, req, ctx) {
364
365
 
365
366
  // 5. data_class — deny-by-default. The provider's allowed_data_classes
366
367
  // (config backends ∩ overlay max) must include the request's data_class.
367
- const allowed = ctx.allowedDataClasses[row.provider];
368
+ // A FIRST-PARTY provider (Cohort's own gateway) declares its allowance in
369
+ // the catalog; a config backends entry or overlay cap still narrows it.
370
+ const allowed = ctx.allowedDataClasses[row.provider] ||
371
+ (row.first_party === true && Array.isArray(row.data_classes) ? new Set(row.data_classes) : null);
368
372
  if (allowed && !allowed.has(req.data_class)) {
369
373
  return { ok: false, reason: "data_class_denied" };
370
374
  }
@@ -381,12 +385,26 @@ function gateCandidate(row, req, ctx) {
381
385
  // anthropic-cli the same way. Direct-api Anthropic and ALL third-party rows
382
386
  // require their credential or they are structurally unreachable.
383
387
  const sessionAnthropic = row.provider === "anthropic" && harness === "session";
384
- if (row.auth_env && !sessionAnthropic) {
388
+ // A seat-token row (cohort-llm) has no static key: it is reachable when the
389
+ // seat can mint a token — it holds an OrgApiKey or a live cached token.
390
+ if (row.auth === "seat_token") {
391
+ if (!(typeof ctx.seatCredential === "function" && ctx.seatCredential())) {
392
+ return { ok: false, reason: "missing_credential" };
393
+ }
394
+ } else if (row.auth_env && !sessionAnthropic) {
385
395
  const cred = resolveRowCredential(row, ctx);
386
396
  if (!cred.token) {
387
397
  return { ok: false, reason: "missing_credential" };
388
398
  }
389
399
  }
400
+ // A third-party SESSION row with no auth_env at all used to pass this gate
401
+ // (the check above only runs when auth_env is set) and was then retargeted
402
+ // with a base URL and no token — leaving the seat's Claude subscription token
403
+ // as the child's only credential, sent to that backend (F1 §5.3). Such a row
404
+ // is unreachable: it cannot authenticate as itself.
405
+ if (row.auth !== "seat_token" && !row.auth_env && harness === "session" && row.provider !== "anthropic") {
406
+ return { ok: false, reason: "missing_credential" };
407
+ }
390
408
 
391
409
  // 7. breaker — the per-backend AND per-backend:model breaker must be closed.
392
410
  const backendKey = keyFor(row.provider);
@@ -421,6 +439,9 @@ function modelClassOf(row) {
421
439
  function buildEnvForSpawn(row, harness, env, config, credCtx) {
422
440
  if (harness !== "session") return {};
423
441
  if (row.provider === "anthropic") return {};
442
+ if (row.auth === "seat_token") {
443
+ return cohortRetargetEnv(row, { agentRoot: (credCtx && credCtx.agentRoot) || env.AGENT_ROOT || env.AGENT_DIR });
444
+ }
424
445
 
425
446
  const baseUrl = row.endpoints && row.endpoints.anthropic;
426
447
  if (!baseUrl) return {}; // not session-capable on the anthropic wire; caller falls back
@@ -434,14 +455,23 @@ function buildEnvForSpawn(row, harness, env, config, credCtx) {
434
455
  // Auth token: a pooled auth-profile set picks the next non-cooled credential
435
456
  // (additive — a single-key setup falls through to env[auth_env] exactly as
436
457
  // before). credCtx carries { authProfiles, authProfileDeps, now, env }.
458
+ // Only a NON-EMPTY token is set. An absent one leaves ANTHROPIC_AUTH_TOKEN
459
+ // out of the retarget set, and the runtime env builder
460
+ // (lib/runtime/child-env.mjs) refuses to spawn a retarget without it — so the
461
+ // seat's own Claude credential can never be the token that reaches this
462
+ // backend.
437
463
  if (row.auth_env) {
438
464
  const cred = credCtx ? resolveRowCredential(row, credCtx) : null;
439
465
  if (cred && cred.pooled && cred.token) {
440
466
  out.ANTHROPIC_AUTH_TOKEN = cred.token;
441
- } else if (typeof env[row.auth_env] === "string") {
467
+ } else if (typeof env[row.auth_env] === "string" && env[row.auth_env].trim() !== "") {
442
468
  out.ANTHROPIC_AUTH_TOKEN = env[row.auth_env];
443
469
  }
444
470
  }
471
+ // `strip_attribution_header: false` still shapes the DECISION record, but the
472
+ // runtime env builder forces CLAUDE_CODE_ATTRIBUTION_HEADER=0 on every
473
+ // retarget: a per-request attribution header poisons a third-party prompt
474
+ // cache, and nothing on this path wants that.
445
475
  if (!config || config.strip_attribution_header !== false) {
446
476
  out.CLAUDE_CODE_ATTRIBUTION_HEADER = "0";
447
477
  }
@@ -451,6 +481,47 @@ function buildEnvForSpawn(row, harness, env, config, credCtx) {
451
481
  return out;
452
482
  }
453
483
 
484
+ /** How often Claude Code re-runs the cohort key helper (well inside a 60-min token). */
485
+ export const COHORT_HELPER_TTL_MS = 15 * 60 * 1000;
486
+
487
+ /**
488
+ * The retarget env for a seat-token (cohort-llm) session row — the internal
489
+ * Path B testbed, where the `claude` binary is pointed at Cohort's gateway
490
+ * (design C1 / §4.7):
491
+ * - ANTHROPIC_BASE_URL = the gateway; NO ANTHROPIC_AUTH_TOKEN. The credential
492
+ * is the apiKeyHelper (COHORT_LLM_API_KEY_HELPER, which the runtime adapter
493
+ * passes as `--settings`), because a static token outranks the helper and
494
+ * would expire mid-session (lib/runtime/child-env.mjs COHORT_HELPER_KEY);
495
+ * - every Anthropic slot alias maps to a Cohort tier through the provider's
496
+ * tier_map (ANTHROPIC_DEFAULT_{OPUS,SONNET,HAIKU,FABLE}_MODEL), so a
497
+ * `--model sonnet` or a subagent's `haiku` never names an Anthropic model at
498
+ * the gateway.
499
+ * Exported for spawnRouted's per-candidate failover env. PURE.
500
+ * @param {object} row a catalog row with auth: "seat_token"
501
+ * @param {{agentRoot?:string, nodeBin?:string}} [o]
502
+ */
503
+ export function cohortRetargetEnv(row, o = {}) {
504
+ const baseUrl = row && row.endpoints && row.endpoints.anthropic;
505
+ if (!baseUrl) return {};
506
+ const tm = (row.tier_map && typeof row.tier_map === "object") ? row.tier_map : {};
507
+ return {
508
+ ANTHROPIC_BASE_URL: baseUrl,
509
+ ANTHROPIC_API_KEY: "",
510
+ ANTHROPIC_MODEL: row.id,
511
+ ANTHROPIC_SMALL_FAST_MODEL: tm.haiku || row.id,
512
+ ANTHROPIC_DEFAULT_FABLE_MODEL: tm.fable || row.id,
513
+ ANTHROPIC_DEFAULT_OPUS_MODEL: tm.opus || row.id,
514
+ ANTHROPIC_DEFAULT_SONNET_MODEL: tm.sonnet || row.id,
515
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: tm.haiku || row.id,
516
+ CLAUDE_CODE_SUBAGENT_MODEL: row.id,
517
+ COHORT_LLM_BASE_URL: baseUrl,
518
+ COHORT_LLM_API_KEY_HELPER: apiKeyHelperCommand({ agentRoot: o.agentRoot, nodeBin: o.nodeBin }),
519
+ CLAUDE_CODE_API_KEY_HELPER_TTL_MS: String(COHORT_HELPER_TTL_MS),
520
+ CLAUDE_CODE_ATTRIBUTION_HEADER: "0",
521
+ CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: "1",
522
+ };
523
+ }
524
+
454
525
  function transportFor(row, harness) {
455
526
  if (harness === "session") {
456
527
  if (row.provider === "anthropic") return TRANSPORT.anthropicApi;
@@ -630,6 +701,21 @@ export function resolveChain(request = {}, opts = {}) {
630
701
  authProfiles: opts.authProfiles || null,
631
702
  authProfileDeps: opts.authProfileDeps || (agentRoot ? { agentRoot } : undefined),
632
703
  now,
704
+ agentRoot: agentRoot || env.AGENT_ROOT || env.AGENT_DIR || null,
705
+ // Seat-token (cohort-llm) reachability, asked at most once per resolve.
706
+ seatCredential: (() => {
707
+ let memo;
708
+ return () => {
709
+ if (memo === undefined) {
710
+ memo = typeof opts.seatCredential === "function"
711
+ ? !!opts.seatCredential()
712
+ // helperReadable: a Path B child authenticates through the
713
+ // apiKeyHelper, which cannot see an env-only key (it is scrubbed).
714
+ : hasSeatCredential({ agentRoot: agentRoot || env.AGENT_ROOT || env.AGENT_DIR, env, now, helperReadable: true });
715
+ }
716
+ return memo;
717
+ };
718
+ })(),
633
719
  };
634
720
 
635
721
  let chosenRow = null;
@@ -39,7 +39,7 @@
39
39
  * @module lib/model-router/spawn
40
40
  */
41
41
 
42
- import { resolveChain } from "./resolve.mjs";
42
+ import { resolveChain, cohortRetargetEnv } from "./resolve.mjs";
43
43
  import { stepChain } from "./failover.mjs";
44
44
  import { keyFor, isOpen as healthIsOpen, open as healthOpen, close as healthClose } from "./health.mjs";
45
45
  import { lookupModel } from "./catalog.mjs";
@@ -47,43 +47,38 @@ import {
47
47
  nextCredential as authNextCredential,
48
48
  markCredentialCoolingDown as authMarkCooling,
49
49
  } from "./auth-profiles.mjs";
50
+ import { buildRuntimeEnv, isCredentialKey, RETARGET_KEYS } from "../runtime/child-env.mjs";
50
51
 
51
52
  // Failover reasons that indicate the CREDENTIAL itself is the problem (a 429 /
52
53
  // auth failure on this key) — these cool THAT credential in the pool so the next
53
54
  // attempt rotates to a sibling. A backend/model-class reason (overloaded,
54
- // server_error, …) cools the breaker, not the key.
55
+ // server_error, …) cools the breaker, not the key. A cohort budget refusal
56
+ // (`budget_refusal`, CF-143) and a gateway transient (`gateway_retry`) are never
57
+ // here: the credential is healthy, and a sibling key of the same tenant would be
58
+ // refused again or would slip past an operator limit.
55
59
  const CRED_COOL_REASONS = new Set(["rate_limit", "auth", "auth_permanent", "billing"]);
56
60
 
57
- // Env keys that the session-retarget set is allowed to introduce. Everything
58
- // else credential-shaped is scrubbed (§7.3) before injection.
59
- const RETARGET_KEYS = new Set([
60
- "ANTHROPIC_BASE_URL",
61
- "ANTHROPIC_AUTH_TOKEN",
62
- "ANTHROPIC_API_KEY",
63
- "ANTHROPIC_MODEL",
64
- "ANTHROPIC_SMALL_FAST_MODEL",
65
- "CLAUDE_CODE_ATTRIBUTION_HEADER",
66
- "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS",
67
- ]);
68
-
69
61
  // ---------------------------------------------------------------------------
70
62
  // Child-env scrubbing (graft #3 / §7.3)
71
63
  // ---------------------------------------------------------------------------
72
64
 
73
65
  /**
74
- * Build the child env by allowlist: start from a scrubbed copy of `baseEnv`
75
- * with every credential-shaped var (`*_API_KEY` / `*_AUTH_TOKEN`, including the
76
- * break-glass key) removed, then inject ONLY the resolved candidate's
77
- * session-retarget pair (decision.envForSpawn). A session routed to a third
78
- * party never sees Anthropic credentials, and vice versa.
66
+ * Build the child env by allowlist. A thin compatibility wrapper over the one
67
+ * runtime env builder, `lib/runtime/child-env.mjs#buildRuntimeEnv`, which owns
68
+ * the rules:
69
+ *
70
+ * - stock (envForSpawn has no ANTHROPIC_BASE_URL): every foreign
71
+ * credential-shaped var is scrubbed; the seat's CLAUDE_CODE_OAUTH_TOKEN is
72
+ * kept; ANTHROPIC_API_KEY is kept only with opts.keepAnthropicApiKey.
73
+ * - retarget: CLAUDE_CODE_OAUTH_TOKEN and every inherited ANTHROPIC_* var are
74
+ * scrubbed too, ANTHROPIC_API_KEY="" and CLAUDE_CODE_ATTRIBUTION_HEADER=0
75
+ * are forced, and a retarget without a third-party token is REFUSED.
79
76
  *
80
- * The Anthropic SESSION path (envForSpawn === {}) rides the stock CLI / keychain
81
- * OAuth or a Console key the spawner owns; we keep an explicitly-injected
82
- * ANTHROPIC_API_KEY from the caller's base env in that case ONLY if the caller
83
- * opted in via opts.keepAnthropicApiKey (default: scrub for hygiene). When we do
84
- * retarget (envForSpawn has ANTHROPIC_BASE_URL), ANTHROPIC_API_KEY="" is part of
85
- * the retarget set and is injected verbatim (empty-string discipline, W:
86
- * keychain fallthrough).
77
+ * A refusal is an expected failure, so callers that can meet one (every spawn
78
+ * site) call buildRuntimeEnv and branch on `ok`. This wrapper exists for the
79
+ * callers that hand it a decision the resolver already credential-gated; for
80
+ * them a refusal means the decision was built wrong, which is a defect — so it
81
+ * throws rather than return an env that could be spawned.
87
82
  *
88
83
  * @param {object} baseEnv usually process.env
89
84
  * @param {object} envForSpawn decision.envForSpawn (the retarget pair) or {}
@@ -92,27 +87,13 @@ const RETARGET_KEYS = new Set([
92
87
  * @returns {object} the child env
93
88
  */
94
89
  export function buildChildEnv(baseEnv, envForSpawn = {}, opts = {}) {
95
- const out = {};
96
- const src = baseEnv && typeof baseEnv === "object" ? baseEnv : {};
97
- for (const [k, v] of Object.entries(src)) {
98
- if (isCredentialKey(k)) continue; // scrub all credential-shaped vars
99
- out[k] = v;
100
- }
101
- // For a retarget (third-party session), force the empty-string ANTHROPIC_API_KEY
102
- // so Claude Code does NOT fall back to keychain OAuth.
103
- const retargeting = !!(envForSpawn && envForSpawn.ANTHROPIC_BASE_URL);
104
- if (!retargeting && opts.keepAnthropicApiKey && typeof src.ANTHROPIC_API_KEY === "string") {
105
- out.ANTHROPIC_API_KEY = src.ANTHROPIC_API_KEY;
106
- }
107
- // Inject only the retarget set (whitelisted keys).
108
- for (const [k, v] of Object.entries(envForSpawn || {})) {
109
- if (RETARGET_KEYS.has(k)) out[k] = v;
90
+ const r = buildRuntimeEnv({ baseEnv, retarget: envForSpawn, keepAnthropicApiKey: !!opts.keepAnthropicApiKey });
91
+ if (!r.ok) {
92
+ const err = new Error(r.error.message);
93
+ err.code = r.error.code;
94
+ throw err;
110
95
  }
111
- return out;
112
- }
113
-
114
- function isCredentialKey(k) {
115
- return /(_API_KEY|_AUTH_TOKEN|_SECRET|_ACCESS_KEY|_PRIVATE_KEY)$/i.test(k) || k === "ANTHROPIC_API_KEY";
96
+ return r.env;
116
97
  }
117
98
 
118
99
  // ---------------------------------------------------------------------------
@@ -193,6 +174,9 @@ function envForCandidate(catalog, ref, harness, baseEnv, decision, authCtx) {
193
174
  }
194
175
  if (!row || harness !== "session") return isChosen ? decision.envForSpawn || {} : {};
195
176
  if (row.provider === "anthropic") return isChosen ? decision.envForSpawn || {} : {};
177
+ // A seat-token (cohort-llm) candidate authenticates through the key helper,
178
+ // never a pooled/static token.
179
+ if (row.auth === "seat_token") return cohortRetargetEnv(row, { agentRoot: baseEnv && (baseEnv.AGENT_ROOT || baseEnv.AGENT_DIR) });
196
180
  const baseUrl = row.endpoints && row.endpoints.anthropic;
197
181
  if (!baseUrl) return isChosen ? decision.envForSpawn || {} : {};
198
182
  const out = {
@@ -393,8 +377,20 @@ export async function spawnRouted(request = {}, opts = {}) {
393
377
  // strip it from the retarget set so it can never leak into the child env.
394
378
  const credFingerprint = retarget.__credFingerprint || null;
395
379
  if (retarget.__credFingerprint !== undefined) delete retarget.__credFingerprint;
396
- const childEnv = buildChildEnv(env, retarget, { keepAnthropicApiKey: opts.keepAnthropicApiKey });
380
+ const built = buildRuntimeEnv({ baseEnv: env, retarget, keepAnthropicApiKey: !!opts.keepAnthropicApiKey });
397
381
  lastArgs = args;
382
+ if (!built.ok) {
383
+ // Refused retarget (no third-party token, a subscription token as the
384
+ // token, a helper aimed at another gateway): this candidate cannot be
385
+ // spawned without handing it the seat's own credential. Nothing left the
386
+ // machine, so nothing is known about the backend: fail over as a
387
+ // LOCAL refusal, which opens no breaker (CF-11 — reporting it as `auth`
388
+ // cooled a real provider for 5 minutes on a local config gap) and cools
389
+ // no credential.
390
+ lastEnv = null;
391
+ return { ok: false, reason: "local_refusal", meta: { refused: built.error.code } };
392
+ }
393
+ const childEnv = built.env;
398
394
  lastEnv = childEnv;
399
395
 
400
396
  let res;
@@ -442,6 +438,9 @@ export async function spawnRouted(request = {}, opts = {}) {
442
438
  env: lastEnv,
443
439
  outcome,
444
440
  tried: outcome.tried || [],
441
+ // A cohort refusal the walk stopped on (budget_refusal / gateway_retry),
442
+ // with resetsAt: hand it to rateGuard.recordCohortRefusal to hold the seat.
443
+ refusal: outcome.refusal || null,
445
444
  ok: outcome.ok === true,
446
445
  };
447
446
  }