@cohortapp/agent-sdk 2.17.0 → 2.18.4

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 (526) 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.mjs +8 -6
  23. package/lib/collective/global-config.mjs +63 -1
  24. package/lib/collective/presence.mjs +142 -5
  25. package/lib/comms/send-gate.mjs +559 -1
  26. package/lib/diagnostics/alerts.mjs +49 -0
  27. package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
  28. package/lib/engine/agents/definitions.mjs +343 -0
  29. package/lib/engine/agents/persist.mjs +275 -0
  30. package/lib/engine/agents/runtime.mjs +748 -0
  31. package/lib/engine/agents/usage.mjs +95 -0
  32. package/lib/engine/auth-status.mjs +139 -0
  33. package/lib/engine/budget.mjs +194 -0
  34. package/lib/engine/cli.mjs +1204 -0
  35. package/lib/engine/commands/index.mjs +269 -0
  36. package/lib/engine/context/budget.mjs +219 -0
  37. package/lib/engine/context/cache.mjs +125 -0
  38. package/lib/engine/context/child-env.mjs +215 -0
  39. package/lib/engine/context/compaction.mjs +342 -0
  40. package/lib/engine/context/images.mjs +90 -0
  41. package/lib/engine/context/instructions.mjs +327 -0
  42. package/lib/engine/context/lazy-instructions.mjs +169 -0
  43. package/lib/engine/context/manager.mjs +182 -0
  44. package/lib/engine/context/real-path.mjs +91 -0
  45. package/lib/engine/context/secret-values.mjs +163 -0
  46. package/lib/engine/context/settings.mjs +274 -0
  47. package/lib/engine/context/stream-input.mjs +159 -0
  48. package/lib/engine/guard.mjs +152 -0
  49. package/lib/engine/hooks.mjs +713 -0
  50. package/lib/engine/loop.mjs +560 -0
  51. package/lib/engine/mcp/client.mjs +254 -0
  52. package/lib/engine/mcp/config.mjs +301 -0
  53. package/lib/engine/mcp/http.mjs +201 -0
  54. package/lib/engine/mcp/index.mjs +146 -0
  55. package/lib/engine/mcp/jsonrpc.mjs +147 -0
  56. package/lib/engine/mcp/naming.mjs +66 -0
  57. package/lib/engine/mcp/resources.mjs +89 -0
  58. package/lib/engine/mcp/results.mjs +133 -0
  59. package/lib/engine/mcp/stdio.mjs +137 -0
  60. package/lib/engine/mcp/supervisor.mjs +116 -0
  61. package/lib/engine/messages.mjs +104 -0
  62. package/lib/engine/output/json.mjs +164 -0
  63. package/lib/engine/output/stream-json.mjs +266 -0
  64. package/lib/engine/permissions.mjs +845 -0
  65. package/lib/engine/process-identity.mjs +164 -0
  66. package/lib/engine/process-tree.mjs +551 -0
  67. package/lib/engine/prompt.mjs +60 -0
  68. package/lib/engine/session/store.mjs +299 -0
  69. package/lib/engine/session-runtime/args.mjs +97 -0
  70. package/lib/engine/session-runtime/host.mjs +143 -0
  71. package/lib/engine/session-runtime/inbox.mjs +122 -0
  72. package/lib/engine/session-runtime/notifications.mjs +129 -0
  73. package/lib/engine/session-runtime/registry.mjs +328 -0
  74. package/lib/engine/session-runtime/runner.mjs +344 -0
  75. package/lib/engine/session-runtime/socket.mjs +212 -0
  76. package/lib/engine/session-runtime/wakeup.mjs +115 -0
  77. package/lib/engine/skills/index.mjs +321 -0
  78. package/lib/engine/tools/bash-background.mjs +533 -0
  79. package/lib/engine/tools/bash.mjs +216 -0
  80. package/lib/engine/tools/edit.mjs +97 -0
  81. package/lib/engine/tools/glob.mjs +81 -0
  82. package/lib/engine/tools/grep.mjs +224 -0
  83. package/lib/engine/tools/index.mjs +84 -0
  84. package/lib/engine/tools/list-agents.mjs +32 -0
  85. package/lib/engine/tools/ls.mjs +127 -0
  86. package/lib/engine/tools/monitor.mjs +82 -0
  87. package/lib/engine/tools/notebook-edit.mjs +218 -0
  88. package/lib/engine/tools/read.mjs +103 -0
  89. package/lib/engine/tools/schedule-wakeup.mjs +45 -0
  90. package/lib/engine/tools/schema.mjs +144 -0
  91. package/lib/engine/tools/send-message.mjs +77 -0
  92. package/lib/engine/tools/session.mjs +70 -0
  93. package/lib/engine/tools/todo.mjs +144 -0
  94. package/lib/engine/tools/toolsearch.mjs +217 -0
  95. package/lib/engine/tools/walk.mjs +193 -0
  96. package/lib/engine/tools/web-switch.mjs +31 -0
  97. package/lib/engine/tools/webfetch-html.mjs +387 -0
  98. package/lib/engine/tools/webfetch-net.mjs +340 -0
  99. package/lib/engine/tools/webfetch.mjs +198 -0
  100. package/lib/engine/tools/websearch.mjs +91 -0
  101. package/lib/engine/tools/workflow.mjs +95 -0
  102. package/lib/engine/tools/write.mjs +76 -0
  103. package/lib/engine/tui/line-editor.mjs +137 -0
  104. package/lib/engine/tui/render.mjs +86 -0
  105. package/lib/engine/tui/tui.mjs +274 -0
  106. package/lib/engine/wire/anthropic-messages.mjs +263 -0
  107. package/lib/engine/wire/effort.mjs +36 -0
  108. package/lib/engine/wire/errors.mjs +496 -0
  109. package/lib/engine/wire/http.mjs +441 -0
  110. package/lib/engine/wire/index.mjs +76 -0
  111. package/lib/engine/wire/openai-chat.mjs +332 -0
  112. package/lib/engine/wire/prompt-cache.mjs +79 -0
  113. package/lib/engine/wire/search.mjs +140 -0
  114. package/lib/engine/wire/sse.mjs +114 -0
  115. package/lib/engine/wire/stall.mjs +349 -0
  116. package/lib/engine/wire/token-provider.mjs +175 -0
  117. package/lib/engine/wire/usage.mjs +192 -0
  118. package/lib/engine/workflow/host.mjs +524 -0
  119. package/lib/engine/workflow/journal.mjs +188 -0
  120. package/lib/engine/workflow/json-schema.mjs +171 -0
  121. package/lib/engine/workflow/meta.mjs +329 -0
  122. package/lib/engine/workflow/notifications.mjs +52 -0
  123. package/lib/engine/workflow/runtime.mjs +447 -0
  124. package/lib/engine/workflow/sandbox.mjs +534 -0
  125. package/lib/engine/workflow/worker.mjs +141 -0
  126. package/lib/engine/workflow/worktree.mjs +74 -0
  127. package/lib/execution/disposition.mjs +1 -1
  128. package/lib/execution/intake.mjs +10 -0
  129. package/lib/execution/surface-policy.mjs +15 -0
  130. package/lib/learning/curator.mjs +8 -6
  131. package/lib/learning/reflect.mjs +8 -6
  132. package/lib/model-router/catalog/cohort.yaml +137 -0
  133. package/lib/model-router/catalog.mjs +118 -1
  134. package/lib/model-router/failover.mjs +67 -16
  135. package/lib/model-router/llm-task.mjs +39 -3
  136. package/lib/model-router/resolve.mjs +89 -3
  137. package/lib/model-router/spawn.mjs +46 -47
  138. package/lib/model-router/taxonomy.mjs +126 -4
  139. package/lib/org/cost-sync.mjs +141 -11
  140. package/lib/org/inbound/broadcast.mjs +289 -0
  141. package/lib/org/inbound/collective.mjs +375 -0
  142. package/lib/org/inbound/directedness.mjs +96 -8
  143. package/lib/org/inbound/facts.mjs +78 -2
  144. package/lib/org/inbound/project.mjs +22 -0
  145. package/lib/org/inbound/surfaces.mjs +14 -0
  146. package/lib/org/llm-token.mjs +879 -0
  147. package/lib/org/mesh.mjs +61 -0
  148. package/lib/org/messaging.mjs +3 -1
  149. package/lib/org/protocol.checksum +1 -1
  150. package/lib/org/protocol.mjs +15 -0
  151. package/lib/org/quota.mjs +520 -0
  152. package/lib/org/tool-surface.mjs +104 -16
  153. package/lib/org/ui-parity.mjs +16 -1
  154. package/lib/org/work-ledger.mjs +37 -6
  155. package/lib/rate-guard.mjs +114 -1
  156. package/lib/resource-governor.mjs +41 -6
  157. package/lib/runtime/adapter.mjs +823 -0
  158. package/lib/runtime/child-env.mjs +191 -0
  159. package/lib/runtime/legacy-shell-guard.mjs +97 -0
  160. package/lib/runtime/seat-engine.mjs +162 -0
  161. package/lib/session/ask-ledger.mjs +271 -0
  162. package/lib/session/current-work.mjs +676 -0
  163. package/lib/session/feed-core.mjs +40 -3
  164. package/lib/session/launch-args.mjs +56 -4
  165. package/lib/session/status-summary.mjs +26 -9
  166. package/lib/session/upgrade-notice.mjs +42 -0
  167. package/lib/setup/claude-probe.mjs +117 -13
  168. package/lib/setup/enrich.mjs +13 -10
  169. package/lib/setup/sections/model.mjs +39 -13
  170. package/lib/telemetry/collect.mjs +208 -9
  171. package/lib/upgrade/ignored-drift.mjs +105 -0
  172. package/lib/voice/post-call-brief.mjs +30 -17
  173. package/package.json +13 -3
  174. package/plugins/maestro-skills/skills/board-work.md +5 -0
  175. package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
  176. package/plugins/maestro-skills/skills/main-session.md +18 -7
  177. package/scripts/ci/check-tarball-fidelity.mjs +126 -2
  178. package/scripts/ci/run-tests.mjs +47 -19
  179. package/scripts/cohort-llm/api-key-helper.mjs +92 -0
  180. package/scripts/collective/hook-runner.mjs +29 -2
  181. package/scripts/continuous-monitor.sh +13 -0
  182. package/scripts/cost/track-claude-usage.mjs +15 -0
  183. package/scripts/daemon/agent-daemon.mjs +408 -20
  184. package/scripts/daemon/assurance.mjs +48 -12
  185. package/scripts/daemon/cadence-consumer.mjs +218 -68
  186. package/scripts/daemon/cadence-handlers.mjs +73 -4
  187. package/scripts/daemon/classifier.mjs +75 -26
  188. package/scripts/daemon/context-compiler.mjs +51 -37
  189. package/scripts/daemon/deliver.mjs +30 -1
  190. package/scripts/daemon/dispatcher.mjs +595 -149
  191. package/scripts/daemon/health.mjs +14 -1
  192. package/scripts/daemon/maestro-daemon.mjs +11 -0
  193. package/scripts/daemon/prompt-builder.mjs +24 -0
  194. package/scripts/daemon/responder.mjs +246 -79
  195. package/scripts/daemon/sdk-version.mjs +98 -16
  196. package/scripts/eval/probe-gateway.mjs +635 -0
  197. package/scripts/eval/replay/extract.mjs +270 -0
  198. package/scripts/eval/replay/grade.mjs +260 -0
  199. package/scripts/eval/replay/lib/config.mjs +50 -0
  200. package/scripts/eval/replay/lib/effects.mjs +65 -0
  201. package/scripts/eval/replay/lib/fixture.mjs +188 -0
  202. package/scripts/eval/replay/lib/judge.mjs +72 -0
  203. package/scripts/eval/replay/lib/redact.mjs +136 -0
  204. package/scripts/eval/replay/lib/sandbox.mjs +170 -0
  205. package/scripts/eval/replay/lib/schema-check.mjs +63 -0
  206. package/scripts/eval/replay/lib/transcript.mjs +76 -0
  207. package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
  208. package/scripts/eval/replay/report.mjs +185 -0
  209. package/scripts/eval/replay/run.mjs +404 -0
  210. package/scripts/fleet/rollout.mjs +1094 -0
  211. package/scripts/hooks/pre-send-audit.sh +36 -245
  212. package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
  213. package/scripts/hooks/validate-state-yaml.sh +190 -0
  214. package/scripts/huddle/huddle-llm.mjs +361 -0
  215. package/scripts/huddle/huddle-server.mjs +46 -121
  216. package/scripts/local-triggers/autoupdate.sh +448 -78
  217. package/scripts/local-triggers/run-trigger.sh +13 -0
  218. package/scripts/maintenance/pin-integrity.mjs +364 -0
  219. package/scripts/poll-slack-events.sh +41 -9
  220. package/scripts/poller/slack-socket-mode.mjs +28 -3
  221. package/scripts/session/supervisor.mjs +80 -13
  222. package/scripts/spawn-session.sh +13 -0
  223. package/bin/maestro.test.mjs +0 -1574
  224. package/lib/action-executor.test.mjs +0 -871
  225. package/lib/archetype.test.mjs +0 -132
  226. package/lib/assurance/plan-note.test.mjs +0 -234
  227. package/lib/assurance/room-budget.test.mjs +0 -486
  228. package/lib/assurance/tier.test.mjs +0 -174
  229. package/lib/autonomy.test.mjs +0 -66
  230. package/lib/backlog.test.mjs +0 -302
  231. package/lib/backup/policy.test.mjs +0 -305
  232. package/lib/budget-escalate.test.mjs +0 -232
  233. package/lib/budget-guard.envelope.test.mjs +0 -476
  234. package/lib/budget-guard.test.mjs +0 -427
  235. package/lib/cadence-bus-requeue.test.mjs +0 -83
  236. package/lib/cadence-bus-schedule.test.mjs +0 -194
  237. package/lib/cadence-bus.test.mjs +0 -720
  238. package/lib/cadences.test.mjs +0 -230
  239. package/lib/capability/inventory.test.mjs +0 -232
  240. package/lib/capability.test.mjs +0 -78
  241. package/lib/channels/base-adapter.test.mjs +0 -590
  242. package/lib/channels/channels.test.mjs +0 -371
  243. package/lib/channels/contract.test.mjs +0 -162
  244. package/lib/channels/inbox-item.test.mjs +0 -368
  245. package/lib/channels/orgmail/adapter.test.mjs +0 -448
  246. package/lib/channels/pairing.test.mjs +0 -270
  247. package/lib/channels/repeat-suppressor.test.mjs +0 -134
  248. package/lib/channels/slack-adapter.test.mjs +0 -212
  249. package/lib/channels/telegram-adapter.test.mjs +0 -306
  250. package/lib/channels/voice/adapter.test.mjs +0 -278
  251. package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
  252. package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
  253. package/lib/charter.test.mjs +0 -89
  254. package/lib/claude-bin.test.mjs +0 -131
  255. package/lib/cli/board.test.mjs +0 -227
  256. package/lib/cli/design.test.mjs +0 -270
  257. package/lib/cli/doctor-checks.test.mjs +0 -336
  258. package/lib/cli/global-setup-extras.test.mjs +0 -462
  259. package/lib/cli/inbox.test.mjs +0 -230
  260. package/lib/cli/session-ack.test.mjs +0 -63
  261. package/lib/cli/session.test.mjs +0 -613
  262. package/lib/collective/capture.test.mjs +0 -121
  263. package/lib/collective/cards.test.mjs +0 -114
  264. package/lib/collective/config.test.mjs +0 -123
  265. package/lib/collective/global-config.test.mjs +0 -220
  266. package/lib/collective/global-skills.test.mjs +0 -126
  267. package/lib/collective/presence.test.mjs +0 -95
  268. package/lib/collective/recall.test.mjs +0 -116
  269. package/lib/collective/vendor-skills.test.mjs +0 -306
  270. package/lib/comms/send-gate.test.mjs +0 -770
  271. package/lib/comms.test.mjs +0 -41
  272. package/lib/context/budget.test.mjs +0 -252
  273. package/lib/context/history-scope.test.mjs +0 -79
  274. package/lib/cost/ledger-row.test.mjs +0 -183
  275. package/lib/design/design-md.test.mjs +0 -318
  276. package/lib/design/fixtures/DESIGN.golden.md +0 -238
  277. package/lib/design/fixtures/PRODUCT.golden.md +0 -67
  278. package/lib/design/fixtures/foundation.json +0 -133
  279. package/lib/design/refresh-gate.test.mjs +0 -144
  280. package/lib/design/write.test.mjs +0 -241
  281. package/lib/diagnostics/alerts.test.mjs +0 -318
  282. package/lib/diagnostics/backup-freshness.test.mjs +0 -185
  283. package/lib/diagnostics/counters.test.mjs +0 -206
  284. package/lib/diagnostics/events.test.mjs +0 -290
  285. package/lib/diagnostics/otel.test.mjs +0 -196
  286. package/lib/diagnostics/trace.test.mjs +0 -251
  287. package/lib/env-compat.test.mjs +0 -104
  288. package/lib/execution/disposition.test.mjs +0 -553
  289. package/lib/execution/drive.test.mjs +0 -270
  290. package/lib/execution/effects.test.mjs +0 -344
  291. package/lib/execution/intake.test.mjs +0 -389
  292. package/lib/execution/journal.test.mjs +0 -261
  293. package/lib/execution/match.test.mjs +0 -235
  294. package/lib/execution/pipeline.test.mjs +0 -392
  295. package/lib/execution/route.test.mjs +0 -186
  296. package/lib/execution/surface-policy.test.mjs +0 -162
  297. package/lib/fs-atomic.test.mjs +0 -72
  298. package/lib/fs-ownership.test.mjs +0 -158
  299. package/lib/goals/admission.test.mjs +0 -164
  300. package/lib/goals/classify.test.mjs +0 -167
  301. package/lib/goals/collaborate.test.mjs +0 -336
  302. package/lib/goals/gaps.test.mjs +0 -284
  303. package/lib/goals/loop.test.mjs +0 -845
  304. package/lib/hooks/bus.test.mjs +0 -387
  305. package/lib/identity/persona.test.mjs +0 -142
  306. package/lib/kpi-sensors.test.mjs +0 -278
  307. package/lib/kpi.test.mjs +0 -244
  308. package/lib/learning/config.test.mjs +0 -75
  309. package/lib/learning/counters.test.mjs +0 -69
  310. package/lib/learning/curator-consolidate.test.mjs +0 -238
  311. package/lib/learning/curator.test.mjs +0 -106
  312. package/lib/learning/reflect.test.mjs +0 -0
  313. package/lib/learning/session-index.test.mjs +0 -125
  314. package/lib/learning/skill-writer.test.mjs +0 -210
  315. package/lib/mandate/audit.test.mjs +0 -195
  316. package/lib/mandate/contract.test.mjs +0 -185
  317. package/lib/mandate/derive.test.mjs +0 -274
  318. package/lib/mandate/model.test.mjs +0 -164
  319. package/lib/mandate/refresh.test.mjs +0 -389
  320. package/lib/mcp/server.test.mjs +0 -426
  321. package/lib/model-router/auth-profiles.test.mjs +0 -580
  322. package/lib/model-router/catalog.test.mjs +0 -385
  323. package/lib/model-router/economics.test.mjs +0 -438
  324. package/lib/model-router/failover.test.mjs +0 -439
  325. package/lib/model-router/health.test.mjs +0 -338
  326. package/lib/model-router/integration-coverage.test.mjs +0 -831
  327. package/lib/model-router/integration.test.mjs +0 -564
  328. package/lib/model-router/ledger.test.mjs +0 -415
  329. package/lib/model-router/llm-task.test.mjs +0 -392
  330. package/lib/model-router/org-credentials.test.mjs +0 -265
  331. package/lib/model-router/pricing-refresh.test.mjs +0 -286
  332. package/lib/model-router/reconcile.test.mjs +0 -316
  333. package/lib/model-router/repair.test.mjs +0 -180
  334. package/lib/model-router/spawn.test.mjs +0 -446
  335. package/lib/model-router/taxonomy.test.mjs +0 -410
  336. package/lib/model-router.test.mjs +0 -1207
  337. package/lib/org/activity.test.mjs +0 -134
  338. package/lib/org/approvals.test.mjs +0 -216
  339. package/lib/org/awareness.test.mjs +0 -159
  340. package/lib/org/board-mine-cache.test.mjs +0 -53
  341. package/lib/org/board.test.mjs +0 -187
  342. package/lib/org/bootstrap-context.test.mjs +0 -153
  343. package/lib/org/client.test.mjs +0 -1206
  344. package/lib/org/cohort-client.test.mjs +0 -126
  345. package/lib/org/cost-sync.test.mjs +0 -153
  346. package/lib/org/doctor.test.mjs +0 -346
  347. package/lib/org/engagement-ledger.test.mjs +0 -112
  348. package/lib/org/engagement.test.mjs +0 -739
  349. package/lib/org/handoff.test.mjs +0 -269
  350. package/lib/org/inbound/directedness.test.mjs +0 -668
  351. package/lib/org/inbound/facts.test.mjs +0 -471
  352. package/lib/org/inbound/hydrate.test.mjs +0 -908
  353. package/lib/org/inbound/index.test.mjs +0 -429
  354. package/lib/org/inbound/project.test.mjs +0 -287
  355. package/lib/org/integration-tools.test.mjs +0 -160
  356. package/lib/org/keys.test.mjs +0 -92
  357. package/lib/org/knowledge.test.mjs +0 -326
  358. package/lib/org/leases.test.mjs +0 -235
  359. package/lib/org/mesh-directives.test.mjs +0 -110
  360. package/lib/org/mesh-integration.test.mjs +0 -127
  361. package/lib/org/mesh.test.mjs +0 -400
  362. package/lib/org/messaging.test.mjs +0 -471
  363. package/lib/org/param-contract.test.mjs +0 -477
  364. package/lib/org/policy.test.mjs +0 -237
  365. package/lib/org/protocol.checksum.test.mjs +0 -90
  366. package/lib/org/protocol.test.mjs +0 -323
  367. package/lib/org/push.test.mjs +0 -792
  368. package/lib/org/registry.test.mjs +0 -100
  369. package/lib/org/resource-tools.test.mjs +0 -361
  370. package/lib/org/tool-access.test.mjs +0 -144
  371. package/lib/org/tool-surface-integration.test.mjs +0 -120
  372. package/lib/org/tool-surface.test.mjs +0 -1268
  373. package/lib/org/typing.test.mjs +0 -291
  374. package/lib/org/ui-parity.test.mjs +0 -560
  375. package/lib/org/verify.test.mjs +0 -194
  376. package/lib/org/work-ledger.test.mjs +0 -273
  377. package/lib/plan/adoption-e2e.test.mjs +0 -366
  378. package/lib/plan/budget-enforcement.test.mjs +0 -400
  379. package/lib/plan/compile.test.mjs +0 -382
  380. package/lib/plan/emit.test.mjs +0 -269
  381. package/lib/plan/explain.test.mjs +0 -188
  382. package/lib/prompts/parallelism.test.mjs +0 -177
  383. package/lib/rag/rag.test.mjs +0 -505
  384. package/lib/rate-guard.test.mjs +0 -272
  385. package/lib/reactive-gate.test.mjs +0 -57
  386. package/lib/render.test.mjs +0 -68
  387. package/lib/resource-governor.test.mjs +0 -488
  388. package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
  389. package/lib/scheduling/jitter.test.mjs +0 -140
  390. package/lib/secrets/broker.test.mjs +0 -280
  391. package/lib/secrets/providers.test.mjs +0 -274
  392. package/lib/security/audit-engine.test.mjs +0 -424
  393. package/lib/security/coerce-args.test.mjs +0 -281
  394. package/lib/security/dangerous-tools.test.mjs +0 -68
  395. package/lib/security/external-content.test.mjs +0 -84
  396. package/lib/security/redact.test.mjs +0 -441
  397. package/lib/security/secret-equal.test.mjs +0 -55
  398. package/lib/session/config.test.mjs +0 -92
  399. package/lib/session/feed-core.test.mjs +0 -198
  400. package/lib/session/first-run.test.mjs +0 -121
  401. package/lib/session/frontdoor.test.mjs +0 -205
  402. package/lib/session/handoffs.test.mjs +0 -183
  403. package/lib/session/identity.test.mjs +0 -180
  404. package/lib/session/inbox-claims.test.mjs +0 -286
  405. package/lib/session/launch-args.test.mjs +0 -157
  406. package/lib/session/liveness.test.mjs +0 -100
  407. package/lib/session/status-summary.test.mjs +0 -118
  408. package/lib/session-permissions.test.mjs +0 -120
  409. package/lib/setup/claude-probe.test.mjs +0 -187
  410. package/lib/setup/completeness.test.mjs +0 -110
  411. package/lib/setup/context-pack.test.mjs +0 -89
  412. package/lib/setup/enrich.test.mjs +0 -115
  413. package/lib/setup/enroll-from-cohort.test.mjs +0 -300
  414. package/lib/setup/integration.test.mjs +0 -162
  415. package/lib/setup/io.test.mjs +0 -77
  416. package/lib/setup/runner.test.mjs +0 -132
  417. package/lib/setup/sections/identity.test.mjs +0 -234
  418. package/lib/setup/sections/inventory.test.mjs +0 -198
  419. package/lib/setup/sections/learning.test.mjs +0 -81
  420. package/lib/setup/sections/mandate.test.mjs +0 -388
  421. package/lib/setup/sections/messaging.test.mjs +0 -127
  422. package/lib/setup/sections/model.test.mjs +0 -240
  423. package/lib/setup/sections/org.test.mjs +0 -346
  424. package/lib/setup/sections/orgmail.test.mjs +0 -118
  425. package/lib/setup/sections/recovery.test.mjs +0 -98
  426. package/lib/setup/sections/subagents.test.mjs +0 -429
  427. package/lib/setup/sections/verify.test.mjs +0 -175
  428. package/lib/setup/sot.test.mjs +0 -81
  429. package/lib/setup/state.test.mjs +0 -115
  430. package/lib/singleton.test.mjs +0 -151
  431. package/lib/subagents/cli.test.mjs +0 -389
  432. package/lib/subagents/client.test.mjs +0 -309
  433. package/lib/subagents/gap.test.mjs +0 -234
  434. package/lib/subagents/lock.test.mjs +0 -248
  435. package/lib/subagents/manifest.test.mjs +0 -175
  436. package/lib/subagents/refs.test.mjs +0 -204
  437. package/lib/subagents/resolve.test.mjs +0 -422
  438. package/lib/subagents/schema.test.mjs +0 -328
  439. package/lib/telemetry/alerts.test.mjs +0 -109
  440. package/lib/telemetry/collect.test.mjs +0 -1274
  441. package/lib/tool-definitions-integration.test.mjs +0 -83
  442. package/lib/tool-definitions.test.mjs +0 -437
  443. package/lib/upgrade/global-refresh.test.mjs +0 -65
  444. package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
  445. package/lib/upgrade/post-steps.test.mjs +0 -200
  446. package/lib/upgrade/verify.test.mjs +0 -164
  447. package/lib/util/fetch-timeout.test.mjs +0 -202
  448. package/lib/util/reconnect.test.mjs +0 -369
  449. package/lib/util/unhandled.test.mjs +0 -216
  450. package/lib/voice/outbound.test.mjs +0 -69
  451. package/lib/voice/session-rotation.test.mjs +0 -114
  452. package/lib/voice/stt.test.mjs +0 -226
  453. package/lib/voice/voice.test.mjs +0 -990
  454. package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
  455. package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
  456. package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
  457. package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
  458. package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
  459. package/scripts/ci/check-skill-packs.test.mjs +0 -495
  460. package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
  461. package/scripts/ci/check.test.mjs +0 -194
  462. package/scripts/ci/conformance-org-api.test.mjs +0 -425
  463. package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
  464. package/scripts/collective/hook-runner.test.mjs +0 -173
  465. package/scripts/cost/fleet-digest.test.mjs +0 -207
  466. package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
  467. package/scripts/cost/track-claude-usage.test.mjs +0 -148
  468. package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
  469. package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
  470. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
  471. package/scripts/daemon/agent-daemon.test.mjs +0 -995
  472. package/scripts/daemon/assurance-e2e.test.mjs +0 -613
  473. package/scripts/daemon/assurance.test.mjs +0 -1791
  474. package/scripts/daemon/board-mirror.test.mjs +0 -165
  475. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
  476. package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
  477. package/scripts/daemon/cadence-consumer.test.mjs +0 -776
  478. package/scripts/daemon/cadence-handlers.test.mjs +0 -837
  479. package/scripts/daemon/classifier-identity.test.mjs +0 -137
  480. package/scripts/daemon/classifier.test.mjs +0 -266
  481. package/scripts/daemon/classify-kind.test.mjs +0 -40
  482. package/scripts/daemon/context-compiler.test.mjs +0 -406
  483. package/scripts/daemon/deliver.test.mjs +0 -564
  484. package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
  485. package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
  486. package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
  487. package/scripts/daemon/dispatcher-session-continuity.test.mjs +0 -365
  488. package/scripts/daemon/execution-ladder.test.mjs +0 -470
  489. package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
  490. package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
  491. package/scripts/daemon/inbox-deferral.test.mjs +0 -336
  492. package/scripts/daemon/inbox-wake.test.mjs +0 -199
  493. package/scripts/daemon/integration.test.mjs +0 -149
  494. package/scripts/daemon/lib/self-echo.test.mjs +0 -153
  495. package/scripts/daemon/lib/session-router.test.mjs +0 -554
  496. package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
  497. package/scripts/daemon/prompt-builder.test.mjs +0 -556
  498. package/scripts/daemon/responder-cost.test.mjs +0 -68
  499. package/scripts/daemon/responder-history.test.mjs +0 -221
  500. package/scripts/daemon/sdk-version.test.mjs +0 -31
  501. package/scripts/daemon/session-lock.test.mjs +0 -252
  502. package/scripts/daemon/session-outcomes.test.mjs +0 -533
  503. package/scripts/daemon/typing-registry.test.mjs +0 -102
  504. package/scripts/hooks/pre-send-audit.test.mjs +0 -354
  505. package/scripts/huddle/huddle-prompt.test.mjs +0 -176
  506. package/scripts/local-triggers/autoupdate.test.mjs +0 -518
  507. package/scripts/local-triggers/generate-plists.test.mjs +0 -456
  508. package/scripts/media-generation/brand-clause.test.mjs +0 -135
  509. package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
  510. package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
  511. package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
  512. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
  513. package/scripts/poller/slack-socket-mode.test.mjs +0 -805
  514. package/scripts/poller-launchd/install.test.mjs +0 -243
  515. package/scripts/restore-from-backup.test.mjs +0 -181
  516. package/scripts/session/feed.test.mjs +0 -196
  517. package/scripts/session/supervisor-sh.test.mjs +0 -218
  518. package/scripts/session/supervisor.test.mjs +0 -482
  519. package/scripts/setup/configure-macos.test.mjs +0 -306
  520. package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
  521. package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
  522. package/scripts/setup/generate-capability.test.mjs +0 -134
  523. package/scripts/setup/init-agent.test.mjs +0 -370
  524. package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
  525. package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
  526. 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
  }