@cohortapp/agent-sdk 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (731) hide show
  1. package/.claude/commands/init-agent.md +104 -0
  2. package/.claude/commands/init-maestro.md +1187 -0
  3. package/.claude/settings.json +161 -0
  4. package/.env.example +216 -0
  5. package/README.md +632 -0
  6. package/agents/browser-operator/agent.md +52 -0
  7. package/agents/calendar-ops/agent.md +50 -0
  8. package/agents/communications/agent.md +96 -0
  9. package/agents/decision-log/agent.md +65 -0
  10. package/agents/desktop-operator/agent.md +59 -0
  11. package/agents/gmail-operator/agent.md +62 -0
  12. package/agents/inbound-dispatcher/agent.md +66 -0
  13. package/agents/inbox-processor/agent.md +39 -0
  14. package/agents/pmo-execution/agent.md +60 -0
  15. package/agents/session-spawner/agent.md +64 -0
  16. package/agents/slack-operator/agent.md +60 -0
  17. package/agents/whatsapp-operator/agent.md +60 -0
  18. package/agents/workflow-automation/agent.md +61 -0
  19. package/archetypes/altitudes/c-suite.yaml +58 -0
  20. package/archetypes/altitudes/founder.yaml +68 -0
  21. package/archetypes/altitudes/senior-manager.yaml +63 -0
  22. package/archetypes/altitudes/svp.yaml +60 -0
  23. package/archetypes/altitudes/vp.yaml +50 -0
  24. package/archetypes/archetype.schema.json +77 -0
  25. package/archetypes/base.yaml +47 -0
  26. package/archetypes/capabilities/commercial-leader.yaml +159 -0
  27. package/archetypes/capabilities/compliance-officer.yaml +159 -0
  28. package/archetypes/capabilities/executive-operator.yaml +169 -0
  29. package/archetypes/capabilities/finance-leader.yaml +162 -0
  30. package/archetypes/capabilities/operations-leader.yaml +154 -0
  31. package/archetypes/capabilities/product-leader.yaml +148 -0
  32. package/archetypes/capabilities/technical-leader.yaml +146 -0
  33. package/archetypes/functions/commercial-leader.yaml +62 -0
  34. package/archetypes/functions/compliance-officer.yaml +64 -0
  35. package/archetypes/functions/executive-operator.yaml +70 -0
  36. package/archetypes/functions/finance-leader.yaml +70 -0
  37. package/archetypes/functions/operations-leader.yaml +62 -0
  38. package/archetypes/functions/product-leader.yaml +61 -0
  39. package/archetypes/functions/technical-leader.yaml +57 -0
  40. package/bin/cohort-mcp.mjs +81 -0
  41. package/bin/maestro.mjs +3516 -0
  42. package/bin/maestro.test.mjs +1015 -0
  43. package/desktop-control/README.md +56 -0
  44. package/desktop-control/app-profiles/gmail.yaml +120 -0
  45. package/desktop-control/app-profiles/slack.yaml +315 -0
  46. package/desktop-control/app-profiles/whatsapp.yaml +107 -0
  47. package/docs/architecture/agent-topology.md +2239 -0
  48. package/docs/architecture/archetype-agent-factory.md +110 -0
  49. package/docs/architecture/collective-memory-and-org-mesh.md +115 -0
  50. package/docs/architecture/continuous-monitoring.md +221 -0
  51. package/docs/architecture/mcp-capability-map.md +585 -0
  52. package/docs/architecture/system-architecture.md +1272 -0
  53. package/docs/company-context/README.md +40 -0
  54. package/docs/guides/agent-persona-setup.md +600 -0
  55. package/docs/guides/agents-observe-setup.md +64 -0
  56. package/docs/guides/billing-console-keys.md +88 -0
  57. package/docs/guides/ccxray-diagnostics.md +65 -0
  58. package/docs/guides/channel-bus.md +127 -0
  59. package/docs/guides/claude-mem-setup.md +79 -0
  60. package/docs/guides/claude-pace-setup.md +56 -0
  61. package/docs/guides/claudraband-sessions.md +98 -0
  62. package/docs/guides/clawteam-swarm.md +116 -0
  63. package/docs/guides/code-review-graph-setup.md +86 -0
  64. package/docs/guides/email-setup.md +431 -0
  65. package/docs/guides/mac-mini.md +119 -0
  66. package/docs/guides/media-generation-setup.md +349 -0
  67. package/docs/guides/model-routing.md +162 -0
  68. package/docs/guides/observability-otel.md +265 -0
  69. package/docs/guides/org-onboarding.md +132 -0
  70. package/docs/guides/outbound-governance-setup.md +437 -0
  71. package/docs/guides/pdf-generation-setup.md +315 -0
  72. package/docs/guides/poller-daemon-setup.md +563 -0
  73. package/docs/guides/rag-context-setup.md +459 -0
  74. package/docs/guides/self-optimization-pattern.md +82 -0
  75. package/docs/guides/setup-wizard.md +178 -0
  76. package/docs/guides/slack-setup.md +350 -0
  77. package/docs/guides/telegram-setup.md +227 -0
  78. package/docs/guides/twilio-subaccounts-setup.md +223 -0
  79. package/docs/guides/verification.md +128 -0
  80. package/docs/guides/voice-mode.md +188 -0
  81. package/docs/guides/voice-sms-setup.md +698 -0
  82. package/docs/guides/webhook-relay-setup.md +349 -0
  83. package/docs/guides/whatsapp-setup.md +288 -0
  84. package/docs/prompts/board-pack-cover-template.md +36 -0
  85. package/docs/prompts/decision-recommendation-template.md +88 -0
  86. package/docs/prompts/followup-message-template.md +141 -0
  87. package/docs/prompts/investor-letter-template.md +52 -0
  88. package/docs/prompts/morning-brief-template.md +82 -0
  89. package/docs/prompts/presentation-template.md +58 -0
  90. package/docs/prompts/weekly-strategic-memo-template.md +104 -0
  91. package/docs/research/hallucinated-tool-output-investigation.md +151 -0
  92. package/docs/runbooks/backup-restore.md +205 -0
  93. package/docs/runbooks/cohort-cutover.md +129 -0
  94. package/docs/runbooks/fleet-operations.md +200 -0
  95. package/docs/runbooks/incident-response.md +226 -0
  96. package/docs/runbooks/mac-mini-bootstrap.md +431 -0
  97. package/docs/runbooks/perpetual-operations.md +509 -0
  98. package/docs/runbooks/recovery-and-failover.md +260 -0
  99. package/framework-features.json +267 -0
  100. package/ingest/README.md +87 -0
  101. package/lib/action-executor.js +689 -0
  102. package/lib/action-executor.test.mjs +871 -0
  103. package/lib/agent-root.mjs +37 -0
  104. package/lib/archetype.mjs +236 -0
  105. package/lib/archetype.test.mjs +132 -0
  106. package/lib/autonomy.mjs +114 -0
  107. package/lib/autonomy.test.mjs +66 -0
  108. package/lib/backlog.mjs +358 -0
  109. package/lib/backlog.test.mjs +266 -0
  110. package/lib/budget-guard.mjs +279 -0
  111. package/lib/budget-guard.test.mjs +291 -0
  112. package/lib/cadence-bus-schedule.test.mjs +194 -0
  113. package/lib/cadence-bus.mjs +1120 -0
  114. package/lib/cadence-bus.test.mjs +720 -0
  115. package/lib/cadences.mjs +205 -0
  116. package/lib/cadences.test.mjs +125 -0
  117. package/lib/capability.mjs +154 -0
  118. package/lib/capability.test.mjs +78 -0
  119. package/lib/channels/base-adapter.mjs +719 -0
  120. package/lib/channels/base-adapter.test.mjs +590 -0
  121. package/lib/channels/channel.mjs +128 -0
  122. package/lib/channels/channels.test.mjs +371 -0
  123. package/lib/channels/contract.mjs +215 -0
  124. package/lib/channels/contract.test.mjs +137 -0
  125. package/lib/channels/conversation-resolver.mjs +95 -0
  126. package/lib/channels/gmail/adapter.mjs +87 -0
  127. package/lib/channels/inbox-item.mjs +255 -0
  128. package/lib/channels/inbox-item.test.mjs +335 -0
  129. package/lib/channels/index.mjs +94 -0
  130. package/lib/channels/orgmail/adapter.mjs +353 -0
  131. package/lib/channels/orgmail/adapter.test.mjs +311 -0
  132. package/lib/channels/pairing.mjs +363 -0
  133. package/lib/channels/pairing.test.mjs +270 -0
  134. package/lib/channels/registry.mjs +164 -0
  135. package/lib/channels/slack/adapter.mjs +317 -0
  136. package/lib/channels/slack-adapter.test.mjs +212 -0
  137. package/lib/channels/sms/adapter.mjs +43 -0
  138. package/lib/channels/telegram/adapter.mjs +432 -0
  139. package/lib/channels/telegram-adapter.test.mjs +306 -0
  140. package/lib/channels/voice/adapter.mjs +301 -0
  141. package/lib/channels/voice/adapter.test.mjs +278 -0
  142. package/lib/channels/whatsapp/adapter-baileys.mjs +587 -0
  143. package/lib/channels/whatsapp/adapter-baileys.test.mjs +359 -0
  144. package/lib/channels/whatsapp/adapter-twilio.mjs +65 -0
  145. package/lib/channels/whatsapp/baileys-typing.test.mjs +154 -0
  146. package/lib/charter.mjs +256 -0
  147. package/lib/charter.test.mjs +89 -0
  148. package/lib/claude-bin.mjs +134 -0
  149. package/lib/claude-bin.test.mjs +75 -0
  150. package/lib/collective/capture.mjs +185 -0
  151. package/lib/collective/capture.test.mjs +121 -0
  152. package/lib/collective/cards.mjs +201 -0
  153. package/lib/collective/cards.test.mjs +114 -0
  154. package/lib/collective/config.mjs +186 -0
  155. package/lib/collective/config.test.mjs +123 -0
  156. package/lib/collective/global-config.mjs +113 -0
  157. package/lib/collective/global-config.test.mjs +75 -0
  158. package/lib/collective/presence.mjs +201 -0
  159. package/lib/collective/presence.test.mjs +95 -0
  160. package/lib/collective/recall.mjs +215 -0
  161. package/lib/collective/recall.test.mjs +116 -0
  162. package/lib/comms/send-gate.mjs +554 -0
  163. package/lib/comms/send-gate.test.mjs +577 -0
  164. package/lib/comms.mjs +67 -0
  165. package/lib/comms.test.mjs +41 -0
  166. package/lib/diagnostics/alerts.mjs +424 -0
  167. package/lib/diagnostics/alerts.test.mjs +318 -0
  168. package/lib/diagnostics/backup-freshness.mjs +188 -0
  169. package/lib/diagnostics/backup-freshness.test.mjs +185 -0
  170. package/lib/diagnostics/counters.mjs +269 -0
  171. package/lib/diagnostics/counters.test.mjs +206 -0
  172. package/lib/diagnostics/events.mjs +188 -0
  173. package/lib/diagnostics/events.test.mjs +290 -0
  174. package/lib/diagnostics/otel.mjs +237 -0
  175. package/lib/diagnostics/otel.test.mjs +196 -0
  176. package/lib/diagnostics/trace.mjs +216 -0
  177. package/lib/diagnostics/trace.test.mjs +251 -0
  178. package/lib/env-compat.mjs +74 -0
  179. package/lib/env-compat.test.mjs +104 -0
  180. package/lib/feature-init.mjs +331 -0
  181. package/lib/fs-atomic.mjs +112 -0
  182. package/lib/fs-atomic.test.mjs +72 -0
  183. package/lib/fs-ownership.mjs +111 -0
  184. package/lib/fs-ownership.test.mjs +158 -0
  185. package/lib/hooks/bus.mjs +347 -0
  186. package/lib/hooks/bus.test.mjs +387 -0
  187. package/lib/index.js +16 -0
  188. package/lib/learning/config.mjs +106 -0
  189. package/lib/learning/config.test.mjs +75 -0
  190. package/lib/learning/counters.mjs +156 -0
  191. package/lib/learning/counters.test.mjs +69 -0
  192. package/lib/learning/curator-consolidate.test.mjs +238 -0
  193. package/lib/learning/curator.mjs +453 -0
  194. package/lib/learning/curator.test.mjs +106 -0
  195. package/lib/learning/log.mjs +40 -0
  196. package/lib/learning/reflect.mjs +534 -0
  197. package/lib/learning/reflect.test.mjs +0 -0
  198. package/lib/learning/session-index.mjs +352 -0
  199. package/lib/learning/session-index.test.mjs +125 -0
  200. package/lib/learning/skill-writer.mjs +474 -0
  201. package/lib/learning/skill-writer.test.mjs +210 -0
  202. package/lib/mcp/server.mjs +328 -0
  203. package/lib/mcp/server.test.mjs +400 -0
  204. package/lib/model-router/auth-profiles.mjs +758 -0
  205. package/lib/model-router/auth-profiles.test.mjs +580 -0
  206. package/lib/model-router/catalog/anthropic.yaml +153 -0
  207. package/lib/model-router/catalog/deepseek.yaml +86 -0
  208. package/lib/model-router/catalog/moonshot.yaml +81 -0
  209. package/lib/model-router/catalog/qwen.yaml +114 -0
  210. package/lib/model-router/catalog.mjs +925 -0
  211. package/lib/model-router/catalog.test.mjs +385 -0
  212. package/lib/model-router/economics.mjs +564 -0
  213. package/lib/model-router/economics.test.mjs +344 -0
  214. package/lib/model-router/failover.mjs +298 -0
  215. package/lib/model-router/failover.test.mjs +439 -0
  216. package/lib/model-router/health.mjs +453 -0
  217. package/lib/model-router/health.test.mjs +338 -0
  218. package/lib/model-router/integration-coverage.test.mjs +829 -0
  219. package/lib/model-router/integration.test.mjs +564 -0
  220. package/lib/model-router/ledger.mjs +402 -0
  221. package/lib/model-router/ledger.test.mjs +382 -0
  222. package/lib/model-router/llm-task.mjs +515 -0
  223. package/lib/model-router/llm-task.test.mjs +392 -0
  224. package/lib/model-router/org-credentials.mjs +260 -0
  225. package/lib/model-router/org-credentials.test.mjs +265 -0
  226. package/lib/model-router/pricing-refresh.mjs +463 -0
  227. package/lib/model-router/pricing-refresh.test.mjs +286 -0
  228. package/lib/model-router/reconcile.mjs +429 -0
  229. package/lib/model-router/reconcile.test.mjs +316 -0
  230. package/lib/model-router/repair.mjs +471 -0
  231. package/lib/model-router/repair.test.mjs +180 -0
  232. package/lib/model-router/resolve.mjs +1206 -0
  233. package/lib/model-router/spawn.mjs +497 -0
  234. package/lib/model-router/spawn.test.mjs +425 -0
  235. package/lib/model-router/taxonomy.mjs +893 -0
  236. package/lib/model-router/taxonomy.test.mjs +410 -0
  237. package/lib/model-router.mjs +677 -0
  238. package/lib/model-router.test.mjs +907 -0
  239. package/lib/org/activity.mjs +211 -0
  240. package/lib/org/activity.test.mjs +134 -0
  241. package/lib/org/approvals.mjs +448 -0
  242. package/lib/org/approvals.test.mjs +216 -0
  243. package/lib/org/awareness.mjs +222 -0
  244. package/lib/org/awareness.test.mjs +159 -0
  245. package/lib/org/board.mjs +229 -0
  246. package/lib/org/board.test.mjs +177 -0
  247. package/lib/org/bootstrap-context.mjs +169 -0
  248. package/lib/org/bootstrap-context.test.mjs +153 -0
  249. package/lib/org/client.mjs +1628 -0
  250. package/lib/org/client.test.mjs +1107 -0
  251. package/lib/org/cohort-client.mjs +67 -0
  252. package/lib/org/cohort-client.test.mjs +126 -0
  253. package/lib/org/cost-sync.mjs +227 -0
  254. package/lib/org/cost-sync.test.mjs +153 -0
  255. package/lib/org/doctor.mjs +212 -0
  256. package/lib/org/doctor.test.mjs +212 -0
  257. package/lib/org/handoff.mjs +293 -0
  258. package/lib/org/handoff.test.mjs +269 -0
  259. package/lib/org/integration-tools.mjs +182 -0
  260. package/lib/org/integration-tools.test.mjs +160 -0
  261. package/lib/org/keys.mjs +131 -0
  262. package/lib/org/keys.test.mjs +92 -0
  263. package/lib/org/knowledge.mjs +463 -0
  264. package/lib/org/knowledge.test.mjs +319 -0
  265. package/lib/org/leases.mjs +335 -0
  266. package/lib/org/leases.test.mjs +235 -0
  267. package/lib/org/mesh-integration.test.mjs +127 -0
  268. package/lib/org/mesh.mjs +459 -0
  269. package/lib/org/mesh.test.mjs +345 -0
  270. package/lib/org/messaging.mjs +503 -0
  271. package/lib/org/messaging.test.mjs +238 -0
  272. package/lib/org/policy.mjs +345 -0
  273. package/lib/org/policy.test.mjs +237 -0
  274. package/lib/org/protocol.checksum +1 -0
  275. package/lib/org/protocol.checksum.test.mjs +90 -0
  276. package/lib/org/protocol.mjs +967 -0
  277. package/lib/org/protocol.test.mjs +264 -0
  278. package/lib/org/registry.mjs +194 -0
  279. package/lib/org/registry.test.mjs +100 -0
  280. package/lib/org/tool-surface-integration.test.mjs +120 -0
  281. package/lib/org/tool-surface.mjs +2535 -0
  282. package/lib/org/tool-surface.test.mjs +589 -0
  283. package/lib/org/ui-parity.mjs +3236 -0
  284. package/lib/org/ui-parity.test.mjs +348 -0
  285. package/lib/org/verify.mjs +176 -0
  286. package/lib/org/verify.test.mjs +194 -0
  287. package/lib/rag/embed.mjs +188 -0
  288. package/lib/rag/indexer.mjs +425 -0
  289. package/lib/rag/rag.test.mjs +505 -0
  290. package/lib/rag/search.mjs +475 -0
  291. package/lib/rate-guard.mjs +246 -0
  292. package/lib/rate-guard.test.mjs +201 -0
  293. package/lib/render.mjs +112 -0
  294. package/lib/render.test.mjs +68 -0
  295. package/lib/resource-governor.mjs +297 -0
  296. package/lib/resource-governor.test.mjs +262 -0
  297. package/lib/scheduling/dynamic-jobs.mjs +675 -0
  298. package/lib/scheduling/dynamic-jobs.test.mjs +344 -0
  299. package/lib/scheduling/jitter.mjs +0 -0
  300. package/lib/scheduling/jitter.test.mjs +140 -0
  301. package/lib/secrets/broker.mjs +315 -0
  302. package/lib/secrets/broker.test.mjs +280 -0
  303. package/lib/secrets/providers.mjs +461 -0
  304. package/lib/secrets/providers.test.mjs +274 -0
  305. package/lib/security/audit-engine.mjs +684 -0
  306. package/lib/security/audit-engine.test.mjs +389 -0
  307. package/lib/security/coerce-args.mjs +552 -0
  308. package/lib/security/coerce-args.test.mjs +281 -0
  309. package/lib/security/dangerous-tools.mjs +97 -0
  310. package/lib/security/dangerous-tools.test.mjs +68 -0
  311. package/lib/security/external-content.mjs +145 -0
  312. package/lib/security/external-content.test.mjs +67 -0
  313. package/lib/security/redact.mjs +592 -0
  314. package/lib/security/redact.test.mjs +441 -0
  315. package/lib/security/secret-equal.mjs +73 -0
  316. package/lib/security/secret-equal.test.mjs +55 -0
  317. package/lib/session-permissions.mjs +101 -0
  318. package/lib/session-permissions.test.mjs +100 -0
  319. package/lib/setup/claude-probe.mjs +74 -0
  320. package/lib/setup/completeness.mjs +175 -0
  321. package/lib/setup/completeness.test.mjs +110 -0
  322. package/lib/setup/context-pack.mjs +173 -0
  323. package/lib/setup/context-pack.test.mjs +89 -0
  324. package/lib/setup/enrich.mjs +277 -0
  325. package/lib/setup/enrich.test.mjs +115 -0
  326. package/lib/setup/enroll-from-cohort.mjs +441 -0
  327. package/lib/setup/enroll-from-cohort.test.mjs +233 -0
  328. package/lib/setup/integration.test.mjs +162 -0
  329. package/lib/setup/io.mjs +360 -0
  330. package/lib/setup/io.test.mjs +77 -0
  331. package/lib/setup/run-generator.mjs +81 -0
  332. package/lib/setup/runner.mjs +244 -0
  333. package/lib/setup/runner.test.mjs +132 -0
  334. package/lib/setup/sections/comms.mjs +173 -0
  335. package/lib/setup/sections/company.mjs +120 -0
  336. package/lib/setup/sections/enrich.mjs +138 -0
  337. package/lib/setup/sections/identity.mjs +182 -0
  338. package/lib/setup/sections/identity.test.mjs +140 -0
  339. package/lib/setup/sections/learning.mjs +153 -0
  340. package/lib/setup/sections/learning.test.mjs +81 -0
  341. package/lib/setup/sections/messaging.mjs +219 -0
  342. package/lib/setup/sections/messaging.test.mjs +127 -0
  343. package/lib/setup/sections/model.mjs +102 -0
  344. package/lib/setup/sections/operating-model.mjs +78 -0
  345. package/lib/setup/sections/org.mjs +475 -0
  346. package/lib/setup/sections/org.test.mjs +313 -0
  347. package/lib/setup/sections/orgmail.mjs +173 -0
  348. package/lib/setup/sections/orgmail.test.mjs +118 -0
  349. package/lib/setup/sections/recovery.mjs +159 -0
  350. package/lib/setup/sections/recovery.test.mjs +98 -0
  351. package/lib/setup/sections/tools.mjs +132 -0
  352. package/lib/setup/sections/verify.mjs +97 -0
  353. package/lib/setup/sot.mjs +205 -0
  354. package/lib/setup/sot.test.mjs +81 -0
  355. package/lib/setup/state.mjs +151 -0
  356. package/lib/setup/state.test.mjs +92 -0
  357. package/lib/singleton.js +229 -0
  358. package/lib/singleton.test.mjs +135 -0
  359. package/lib/telemetry/alerts.mjs +216 -0
  360. package/lib/telemetry/alerts.test.mjs +109 -0
  361. package/lib/telemetry/collect.mjs +512 -0
  362. package/lib/telemetry/collect.test.mjs +202 -0
  363. package/lib/tool-definitions-integration.test.mjs +83 -0
  364. package/lib/tool-definitions.js +738 -0
  365. package/lib/tool-definitions.test.mjs +437 -0
  366. package/lib/util/fetch-timeout.mjs +136 -0
  367. package/lib/util/fetch-timeout.test.mjs +202 -0
  368. package/lib/util/reconnect.mjs +343 -0
  369. package/lib/util/reconnect.test.mjs +369 -0
  370. package/lib/util/unhandled.mjs +205 -0
  371. package/lib/util/unhandled.test.mjs +216 -0
  372. package/lib/voice/context-loader.mjs +466 -0
  373. package/lib/voice/index.mjs +100 -0
  374. package/lib/voice/openai-realtime.mjs +510 -0
  375. package/lib/voice/outbound.mjs +542 -0
  376. package/lib/voice/outbound.test.mjs +69 -0
  377. package/lib/voice/post-call-brief.mjs +428 -0
  378. package/lib/voice/provider.mjs +52 -0
  379. package/lib/voice/session-rotation.mjs +257 -0
  380. package/lib/voice/stt.mjs +161 -0
  381. package/lib/voice/stt.test.mjs +226 -0
  382. package/lib/voice/tool-bridge.mjs +370 -0
  383. package/lib/voice/tts.mjs +104 -0
  384. package/lib/voice/twilio-sip-bridge.mjs +288 -0
  385. package/lib/voice/voice.test.mjs +990 -0
  386. package/mcp/README.md +80 -0
  387. package/package.json +151 -0
  388. package/plugins/maestro-skills/plugin.json +139 -0
  389. package/plugins/maestro-skills/skills/agents-observe.md +110 -0
  390. package/plugins/maestro-skills/skills/board-deck.md +68 -0
  391. package/plugins/maestro-skills/skills/books-close.md +77 -0
  392. package/plugins/maestro-skills/skills/brand-steward.md +121 -0
  393. package/plugins/maestro-skills/skills/calendar-plan.md +57 -0
  394. package/plugins/maestro-skills/skills/call-working-sessions.md +124 -0
  395. package/plugins/maestro-skills/skills/ccxray-diagnostics.md +91 -0
  396. package/plugins/maestro-skills/skills/claude-pace.md +61 -0
  397. package/plugins/maestro-skills/skills/code-review-graph.md +99 -0
  398. package/plugins/maestro-skills/skills/crm-pipeline.md +65 -0
  399. package/plugins/maestro-skills/skills/decision-brief.md +89 -0
  400. package/plugins/maestro-skills/skills/directory-hygiene.md +125 -0
  401. package/plugins/maestro-skills/skills/draft-comms.md +84 -0
  402. package/plugins/maestro-skills/skills/evening-wrap.md +53 -0
  403. package/plugins/maestro-skills/skills/files-find.md +65 -0
  404. package/plugins/maestro-skills/skills/generative-ui.md +228 -0
  405. package/plugins/maestro-skills/skills/hiring-triage.md +74 -0
  406. package/plugins/maestro-skills/skills/inbox-triage.md +61 -0
  407. package/plugins/maestro-skills/skills/mail-triage.md +86 -0
  408. package/plugins/maestro-skills/skills/morning-brief.md +54 -0
  409. package/plugins/maestro-skills/skills/native-artifacts.md +157 -0
  410. package/plugins/maestro-skills/skills/org-board.md +133 -0
  411. package/plugins/maestro-skills/skills/org-credential.md +68 -0
  412. package/plugins/maestro-skills/skills/org-recall.md +81 -0
  413. package/plugins/maestro-skills/skills/pipeline-review.md +76 -0
  414. package/plugins/maestro-skills/skills/regulatory-status.md +81 -0
  415. package/plugins/maestro-skills/skills/router-why.md +78 -0
  416. package/plugins/maestro-skills/skills/schedule-meeting.md +91 -0
  417. package/plugins/maestro-skills/skills/session-search.md +71 -0
  418. package/plugins/maestro-skills/skills/set-reminder.md +93 -0
  419. package/plugins/maestro-skills/skills/slack-followup.md +64 -0
  420. package/plugins/maestro-skills/skills/team-activity.md +86 -0
  421. package/plugins/maestro-skills/skills/weekly-memo.md +70 -0
  422. package/policies/action-classification.yaml +114 -0
  423. package/policies/ai-disclosure.yaml +294 -0
  424. package/policies/communication-style.md +139 -0
  425. package/policies/information-barriers.yaml +118 -0
  426. package/policies/prompt-injection-defence.yaml +138 -0
  427. package/public/assets/icon-dark.png +0 -0
  428. package/public/assets/icon-dark.svg +9 -0
  429. package/public/assets/icon-light.svg +9 -0
  430. package/public/assets/logo-dark.svg +15 -0
  431. package/public/assets/logo-light.svg +15 -0
  432. package/scaffold/.mcp.json +7 -0
  433. package/scaffold/CLAUDE.md +368 -0
  434. package/scaffold/config/agent.json +55 -0
  435. package/scaffold/config/agent.ts +76 -0
  436. package/scaffold/config/agent.ts.example +89 -0
  437. package/scaffold/config/alerts.yaml +23 -0
  438. package/scaffold/config/allowlist.yaml.example +25 -0
  439. package/scaffold/config/caller-id-map.yaml +46 -0
  440. package/scaffold/config/collective.yaml +49 -0
  441. package/scaffold/config/company.json +20 -0
  442. package/scaffold/config/known-agents.json +6 -0
  443. package/scaffold/config/learning.yaml +55 -0
  444. package/scaffold/config/model-routing.yaml.example +104 -0
  445. package/scaffold/config/org.yaml +25 -0
  446. package/scaffold/config/orgmail.yaml.example +19 -0
  447. package/scaffold/config/recovery.yaml +72 -0
  448. package/scaffold/config/secrets.yaml +27 -0
  449. package/scaffold/config/slack.yaml.example +35 -0
  450. package/scaffold/config/telegram.yaml.example +38 -0
  451. package/scaffold/config/voice.yaml.example +89 -0
  452. package/scaffold/config/whatsapp.yaml.example +39 -0
  453. package/schedules/README.md +49 -0
  454. package/schedules/triggers/backlog-executor.md +102 -0
  455. package/schedules/triggers/brand-steward.md +72 -0
  456. package/schedules/triggers/daily-evening-wrap.md +159 -0
  457. package/schedules/triggers/daily-midday-sweep.md +58 -0
  458. package/schedules/triggers/daily-morning-brief.md +55 -0
  459. package/schedules/triggers/directory-hygiene.md +81 -0
  460. package/schedules/triggers/dynamic-jobs.md +40 -0
  461. package/schedules/triggers/inbox-processor.md +115 -0
  462. package/schedules/triggers/meeting-action-capture.md +60 -0
  463. package/schedules/triggers/meeting-prep.md +69 -0
  464. package/schedules/triggers/messaging-inbound.md +50 -0
  465. package/schedules/triggers/org-pulse.md +24 -0
  466. package/schedules/triggers/quarterly-self-assessment.md +54 -0
  467. package/schedules/triggers/weekly-engineering-health.md +37 -0
  468. package/schedules/triggers/weekly-execution.md +65 -0
  469. package/schedules/triggers/weekly-hiring.md +53 -0
  470. package/schedules/triggers/weekly-priorities.md +38 -0
  471. package/schedules/triggers/weekly-strategic-memo.md +124 -0
  472. package/scripts/archive-email.sh +55 -0
  473. package/scripts/cadence/cadence-status.mjs +36 -0
  474. package/scripts/cadence/enqueue-cadence-tick.mjs +174 -0
  475. package/scripts/cadence/enqueue-cadence-tick.test.mjs +187 -0
  476. package/scripts/cadence/launchd-cadence-wrapper.sh +85 -0
  477. package/scripts/cadence/launchd-cloud-relay-wrapper.sh +95 -0
  478. package/scripts/cadence/launchd-socket-mode-wrapper.sh +95 -0
  479. package/scripts/ci/check-docs-accuracy.mjs +493 -0
  480. package/scripts/ci/check-docs-accuracy.test.mjs +409 -0
  481. package/scripts/ci/check-exports-exist.mjs +140 -0
  482. package/scripts/ci/check-files-exist.mjs +107 -0
  483. package/scripts/ci/check-no-build-artifacts.mjs +111 -0
  484. package/scripts/ci/check-no-build-artifacts.test.mjs +71 -0
  485. package/scripts/ci/check-no-confidential.mjs +198 -0
  486. package/scripts/ci/check-no-conflict-markers.mjs +169 -0
  487. package/scripts/ci/check-no-residual-identity.mjs +163 -0
  488. package/scripts/ci/check-no-residual-identity.test.mjs +89 -0
  489. package/scripts/ci/check-tarball-fidelity.mjs +205 -0
  490. package/scripts/ci/check-unresolved-tokens.mjs +83 -0
  491. package/scripts/ci/check.mjs +109 -0
  492. package/scripts/ci/check.test.mjs +194 -0
  493. package/scripts/ci/run-coverage.mjs +82 -0
  494. package/scripts/ci/run-tests.mjs +71 -0
  495. package/scripts/cloud-relay/README.md +59 -0
  496. package/scripts/cloud-relay/index.mjs +233 -0
  497. package/scripts/cloud-relay/package.json +15 -0
  498. package/scripts/cloud-relay/railway.json +13 -0
  499. package/scripts/cloud-relay/voice/README.md +94 -0
  500. package/scripts/cloud-relay/voice/package-lock.json +39 -0
  501. package/scripts/cloud-relay/voice/package.json +16 -0
  502. package/scripts/cloud-relay/voice/railway.json +13 -0
  503. package/scripts/cloud-relay/voice/server.mjs +532 -0
  504. package/scripts/collective/hook-runner.mjs +211 -0
  505. package/scripts/collective/hook-runner.test.mjs +90 -0
  506. package/scripts/collective/org-pulse.mjs +72 -0
  507. package/scripts/collective/org-sync.mjs +61 -0
  508. package/scripts/collective/recall.mjs +45 -0
  509. package/scripts/collective/who.mjs +30 -0
  510. package/scripts/comms-monitor.sh +288 -0
  511. package/scripts/configure-whatsapp-sandbox.sh +201 -0
  512. package/scripts/continuous-monitor.sh +91 -0
  513. package/scripts/cost/fleet-digest.mjs +407 -0
  514. package/scripts/cost/fleet-digest.test.mjs +207 -0
  515. package/scripts/cost/track-claude-usage.mjs +169 -0
  516. package/scripts/daemon/agent-daemon.mjs +989 -0
  517. package/scripts/daemon/agent-daemon.test.mjs +525 -0
  518. package/scripts/daemon/cadence-consumer-governance.test.mjs +220 -0
  519. package/scripts/daemon/cadence-consumer.mjs +1080 -0
  520. package/scripts/daemon/cadence-consumer.test.mjs +770 -0
  521. package/scripts/daemon/cadence-handlers.mjs +1121 -0
  522. package/scripts/daemon/cadence-handlers.test.mjs +617 -0
  523. package/scripts/daemon/classifier.mjs +704 -0
  524. package/scripts/daemon/classifier.test.mjs +238 -0
  525. package/scripts/daemon/classify-kind.mjs +54 -0
  526. package/scripts/daemon/classify-kind.test.mjs +40 -0
  527. package/scripts/daemon/context-compiler.mjs +605 -0
  528. package/scripts/daemon/context-compiler.test.mjs +300 -0
  529. package/scripts/daemon/dispatcher-cooldown.test.mjs +122 -0
  530. package/scripts/daemon/dispatcher-governance.test.mjs +886 -0
  531. package/scripts/daemon/dispatcher.mjs +1516 -0
  532. package/scripts/daemon/health.mjs +72 -0
  533. package/scripts/daemon/inbox-deferral.mjs +210 -0
  534. package/scripts/daemon/inbox-deferral.test.mjs +242 -0
  535. package/scripts/daemon/integration.test.mjs +149 -0
  536. package/scripts/daemon/launchd-wrapper-generic.sh +96 -0
  537. package/scripts/daemon/launchd-wrapper-slack-events.sh +37 -0
  538. package/scripts/daemon/launchd-wrapper.sh +91 -0
  539. package/scripts/daemon/lib/session-router.mjs +274 -0
  540. package/scripts/daemon/lib/session-router.test.mjs +295 -0
  541. package/scripts/daemon/maestro-daemon.mjs +275 -0
  542. package/scripts/daemon/prompt-builder.mjs +685 -0
  543. package/scripts/daemon/prompt-builder.test.mjs +213 -0
  544. package/scripts/daemon/responder.mjs +854 -0
  545. package/scripts/daemon/session-lock.mjs +721 -0
  546. package/scripts/daemon/session-lock.test.mjs +252 -0
  547. package/scripts/daemon/session-outcomes.mjs +640 -0
  548. package/scripts/daemon/session-outcomes.test.mjs +533 -0
  549. package/scripts/daemon/typing-registry.mjs +90 -0
  550. package/scripts/daemon/typing-registry.test.mjs +77 -0
  551. package/scripts/daemon/voice-webhook-server.mjs +804 -0
  552. package/scripts/decisions/capture-decision.mjs +116 -0
  553. package/scripts/disclosure_assessment.py +873 -0
  554. package/scripts/disclosure_boundaries.py +562 -0
  555. package/scripts/email-signature-principal.html +52 -0
  556. package/scripts/email-signature.html +60 -0
  557. package/scripts/email_quote_thread.py +167 -0
  558. package/scripts/email_thread_dedup.py +362 -0
  559. package/scripts/emergency-stop.sh +81 -0
  560. package/scripts/healthcheck.sh +116 -0
  561. package/scripts/hooks/block-mcp-cohort-send.sh +15 -0
  562. package/scripts/hooks/block-mcp-slack-send.sh +7 -0
  563. package/scripts/hooks/post-action-log.sh +126 -0
  564. package/scripts/hooks/pre-send-audit.sh +174 -0
  565. package/scripts/hooks/pre-send-audit.test.mjs +215 -0
  566. package/scripts/hooks/session-end-log.sh +27 -0
  567. package/scripts/hooks/session-start-banner.sh +115 -0
  568. package/scripts/huddle/audio-bridge.mjs +664 -0
  569. package/scripts/huddle/boot-slack-cdp.sh +102 -0
  570. package/scripts/huddle/huddle-controller.mjs +942 -0
  571. package/scripts/huddle/huddle-server.mjs +1229 -0
  572. package/scripts/huddle/launch-slack.sh +232 -0
  573. package/scripts/huddle/openai-realtime-bridge.mjs +462 -0
  574. package/scripts/huddle/package-lock.json +62 -0
  575. package/scripts/huddle/package.json +22 -0
  576. package/scripts/huddle/setup-audio.sh +239 -0
  577. package/scripts/huddle/start-call.mjs +318 -0
  578. package/scripts/huddle/test-pipeline.mjs +263 -0
  579. package/scripts/learning/consolidate-skills.mjs +72 -0
  580. package/scripts/learning/session-search.mjs +125 -0
  581. package/scripts/llm_email_dedup.py +442 -0
  582. package/scripts/local-triggers/generate-plists.sh +432 -0
  583. package/scripts/local-triggers/generate-plists.test.mjs +413 -0
  584. package/scripts/local-triggers/install-all.sh +49 -0
  585. package/scripts/local-triggers/plists/.gitkeep +0 -0
  586. package/scripts/local-triggers/run-trigger.sh +63 -0
  587. package/scripts/local-triggers/templates/rag-reindex.plist.template +47 -0
  588. package/scripts/local-triggers/templates/voice-relay-poller.plist.template +54 -0
  589. package/scripts/local-triggers/templates/voice-tunnel.plist.template +55 -0
  590. package/scripts/local-triggers/templates/voice-webhook.plist.template +51 -0
  591. package/scripts/maintenance/backup-to-cloud.sh +124 -0
  592. package/scripts/maintenance/health-check.sh +377 -0
  593. package/scripts/media-generation/README.md +105 -0
  594. package/scripts/media-generation/gemini-image-client.mjs +173 -0
  595. package/scripts/media-generation/generate-assets.mjs +289 -0
  596. package/scripts/media-generation/veo-video-client.mjs +219 -0
  597. package/scripts/org/send-orgmail.mjs +227 -0
  598. package/scripts/outbound-dedup-cleanup.sh +43 -0
  599. package/scripts/outbound-dedup.sh +477 -0
  600. package/scripts/outbound_dedup.py +115 -0
  601. package/scripts/parse-voice-transcript.mjs +481 -0
  602. package/scripts/pdf-generation/README.md +63 -0
  603. package/scripts/pdf-generation/build-document.mjs +247 -0
  604. package/scripts/pdf-generation/templates/board-pack.latex +136 -0
  605. package/scripts/pdf-generation/templates/corporate-letter.latex +126 -0
  606. package/scripts/pdf-generation/templates/memo.latex +114 -0
  607. package/scripts/poll-slack-events.sh +35 -0
  608. package/scripts/poller/calendar-poller.mjs +12 -0
  609. package/scripts/poller/gmail-poller.mjs +192 -0
  610. package/scripts/poller/imap-client.mjs +289 -0
  611. package/scripts/poller/inbox-scan-poller.mjs +156 -0
  612. package/scripts/poller/inbox-scan-poller.test.mjs +231 -0
  613. package/scripts/poller/index.mjs +73 -0
  614. package/scripts/poller/intra-session-check.mjs +285 -0
  615. package/scripts/poller/lib/cloud-relay-dedup.mjs +88 -0
  616. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +133 -0
  617. package/scripts/poller/lib/slash-command-handlers.mjs +177 -0
  618. package/scripts/poller/secondary-gmail-poller.mjs +132 -0
  619. package/scripts/poller/slack-cloud-relay-client.mjs +368 -0
  620. package/scripts/poller/slack-poller.mjs +854 -0
  621. package/scripts/poller/slack-socket-mode.mjs +917 -0
  622. package/scripts/poller/slack-socket-mode.test.mjs +753 -0
  623. package/scripts/poller/trigger.mjs +75 -0
  624. package/scripts/poller/utils.mjs +371 -0
  625. package/scripts/poller/voice-cloud-relay-client.mjs +179 -0
  626. package/scripts/poller/voice-poller.mjs +236 -0
  627. package/scripts/poller-launchd/install.sh +66 -0
  628. package/scripts/poller-launchd/poller.plist.template +40 -0
  629. package/scripts/poller-launchd/whatsapp-handler.plist.template +39 -0
  630. package/scripts/post-interaction-indexer.py +1598 -0
  631. package/scripts/pre-draft-context.py +994 -0
  632. package/scripts/pre_draft_lookup.py +258 -0
  633. package/scripts/rag/build-index.mjs +47 -0
  634. package/scripts/rag/ingest.mjs +111 -0
  635. package/scripts/rag/search.mjs +119 -0
  636. package/scripts/rag-indexer.py +629 -0
  637. package/scripts/restore-from-backup.sh +248 -0
  638. package/scripts/restore-from-backup.test.mjs +178 -0
  639. package/scripts/resume-operations.sh +80 -0
  640. package/scripts/search-secondary-inbox.py +181 -0
  641. package/scripts/secondary-inbox-poller.py +437 -0
  642. package/scripts/self-optimization/compute-metrics.py +398 -0
  643. package/scripts/send-email-as-principal.py +369 -0
  644. package/scripts/send-email-threaded.py +392 -0
  645. package/scripts/send-email-with-attachment.py +377 -0
  646. package/scripts/send-email.sh +131 -0
  647. package/scripts/send-sms.sh +175 -0
  648. package/scripts/send-whatsapp.sh +292 -0
  649. package/scripts/session-start.sh +106 -0
  650. package/scripts/setup/boot-claude-session.sh +94 -0
  651. package/scripts/setup/configure-macos.sh +674 -0
  652. package/scripts/setup/configure-twilio-sip-trunk.mjs +207 -0
  653. package/scripts/setup/configure-voice-tunnel.mjs +182 -0
  654. package/scripts/setup/generate-agent-env.mjs +92 -0
  655. package/scripts/setup/generate-agent-package-json.mjs +222 -0
  656. package/scripts/setup/generate-agent-package-json.test.mjs +143 -0
  657. package/scripts/setup/generate-autonomy.mjs +60 -0
  658. package/scripts/setup/generate-backlog.mjs +101 -0
  659. package/scripts/setup/generate-cadences.mjs +92 -0
  660. package/scripts/setup/generate-capability.mjs +162 -0
  661. package/scripts/setup/generate-charter.mjs +76 -0
  662. package/scripts/setup/generate-comms.mjs +59 -0
  663. package/scripts/setup/generate-company.mjs +92 -0
  664. package/scripts/setup/init-agent.sh +547 -0
  665. package/scripts/setup/init-agent.test.mjs +151 -0
  666. package/scripts/setup/init-archetype.mjs +74 -0
  667. package/scripts/setup/init-backup.mjs +54 -0
  668. package/scripts/setup/init-cadence-bus.mjs +60 -0
  669. package/scripts/setup/init-channel-bus.mjs +46 -0
  670. package/scripts/setup/init-cost-tracking.mjs +45 -0
  671. package/scripts/setup/init-decision-capture.mjs +66 -0
  672. package/scripts/setup/init-known-agents.mjs +57 -0
  673. package/scripts/setup/init-learning.mjs +70 -0
  674. package/scripts/setup/init-memory-executive.mjs +45 -0
  675. package/scripts/setup/init-model-router.mjs +124 -0
  676. package/scripts/setup/init-rag.mjs +174 -0
  677. package/scripts/setup/init-session-router.mjs +38 -0
  678. package/scripts/setup/init-slack-socket-mode.mjs +260 -0
  679. package/scripts/setup/init-telegram.mjs +165 -0
  680. package/scripts/setup/init-voice-realtime.mjs +204 -0
  681. package/scripts/setup/init-whatsapp-baileys.mjs +77 -0
  682. package/scripts/setup/install-dev-tools.sh +150 -0
  683. package/scripts/setup/lib/install-plist.mjs +95 -0
  684. package/scripts/setup/migrate-agent-to-sot.mjs +192 -0
  685. package/scripts/setup/render-environment-yaml.mjs +133 -0
  686. package/scripts/slack-events-ctl.sh +177 -0
  687. package/scripts/slack-events-server.mjs +1045 -0
  688. package/scripts/slack-react.mjs +89 -0
  689. package/scripts/slack-responded.sh +232 -0
  690. package/scripts/slack-send.sh +287 -0
  691. package/scripts/slack-typing.mjs +196 -0
  692. package/scripts/slack-upload-v2.py +95 -0
  693. package/scripts/sms-handler.mjs +450 -0
  694. package/scripts/spawn-session.sh +120 -0
  695. package/scripts/sync-protocol.mjs +217 -0
  696. package/scripts/system-verify.sh +184 -0
  697. package/scripts/test-email-thread-dedup.py +239 -0
  698. package/scripts/test-information-barriers.py +484 -0
  699. package/scripts/test-llm-email-dedup.py +251 -0
  700. package/scripts/test-pre-draft-integration.py +203 -0
  701. package/scripts/test-rag-phase2.sh +442 -0
  702. package/scripts/test-rag-search.sh +251 -0
  703. package/scripts/test-voice-parser.mjs +316 -0
  704. package/scripts/user-context-search.py +659 -0
  705. package/scripts/validate_outbound.py +1504 -0
  706. package/scripts/watchdog/ai.maestro.memory-watchdog.plist +41 -0
  707. package/scripts/watchdog/force-reboot.sh +157 -0
  708. package/scripts/watchdog/memory-watchdog.sh +473 -0
  709. package/scripts/whatsapp-handler.mjs +538 -0
  710. package/teams/desktop-operations.yaml +34 -0
  711. package/teams/executive-office.yaml +27 -0
  712. package/teams/legal-and-regulatory.yaml +24 -0
  713. package/teams/platform-and-engineering.yaml +23 -0
  714. package/teams/strategy-and-growth.yaml +29 -0
  715. package/workflows/continuous/backlog-executor.yaml +141 -0
  716. package/workflows/continuous/inbound-monitor.yaml +168 -0
  717. package/workflows/daily/applicant-triage.yaml +197 -0
  718. package/workflows/daily/comms-triage.yaml +80 -0
  719. package/workflows/daily/evening-wrap.yaml +105 -0
  720. package/workflows/daily/morning-brief.yaml +164 -0
  721. package/workflows/daily/slack-followup-sweep.yaml +87 -0
  722. package/workflows/event-driven/README.md +50 -0
  723. package/workflows/event-driven/agent-failure-investigation.yaml +137 -0
  724. package/workflows/event-driven/pr-review.yaml +107 -0
  725. package/workflows/monthly/board-readiness.yaml +76 -0
  726. package/workflows/quarterly/strategic-scenario-analysis.yaml +85 -0
  727. package/workflows/session-protocol.md +171 -0
  728. package/workflows/weekly/engineering-health.yaml +154 -0
  729. package/workflows/weekly/hiring-review.yaml +169 -0
  730. package/workflows/weekly/rollup-pipeline-review.yaml +76 -0
  731. package/workflows/weekly/strategic-memo.yaml +79 -0
@@ -0,0 +1,315 @@
1
+ /**
2
+ * lib/secrets/broker.mjs — the secret lifecycle broker (gaps-enterprise-ops
3
+ * G12 + G16 DR-config).
4
+ *
5
+ * Sits above lib/secrets/providers.mjs and turns "where does this secret live"
6
+ * into a resolution policy plus two fleet operations: pull (sync) and rotate.
7
+ *
8
+ * • resolveSecret(name, {cfg, …})
9
+ * Chain precedence: explicit override > broker > file > env.
10
+ * The first provider that returns a non-null value wins; every read in
11
+ * the chain fails OPEN (a provider error is treated as "not here, keep
12
+ * going"), matching the project audit's fail-open posture.
13
+ *
14
+ * • syncSecrets({cfg, names, …})
15
+ * Pull a set of secrets from the broker into the local provider (the
16
+ * backing store of `maestro secrets sync`). Each pull is a `set` on the
17
+ * local provider, so it fails CLOSED per secret — a secret that the
18
+ * broker can't supply is reported, not silently skipped.
19
+ *
20
+ * • rotateSecret(name, newValue, {cfg, …})
21
+ * Write the new value to the AUTHORITATIVE provider (broker if a broker
22
+ * is configured, else the local provider) and record names-only rotation
23
+ * metadata. Fail-closed on the write.
24
+ *
25
+ * Audit: every rotation appends one line to logs/audit/secrets.jsonl recording
26
+ * WHO (the agent identity), WHAT (the secret name + a name fingerprint), WHEN,
27
+ * and the target provider — and NEVER the value. One identity per agent: the
28
+ * agent token, surfaced via getAgentIdentity().
29
+ *
30
+ * Pure node:* + the providers module. The broker's network identity (fetch,
31
+ * baseUrl, token) is injected through cfg/ctx so the whole module is testable
32
+ * with a stub fetch and no live calls.
33
+ *
34
+ * @module lib/secrets/broker
35
+ */
36
+
37
+ "use strict";
38
+
39
+ import { join } from "node:path";
40
+
41
+ import { appendJsonl } from "../fs-atomic.mjs";
42
+ import {
43
+ selectProvider,
44
+ BrokerProvider,
45
+ nameFingerprint,
46
+ } from "./providers.mjs";
47
+
48
+ /** Relative path of the names-only rotation audit log under the agent root. */
49
+ const AUDIT_REL = join("logs", "audit", "secrets.jsonl");
50
+
51
+ /**
52
+ * The single agent identity used for broker auth + audit attribution. One
53
+ * identity per agent (the agent token). Resolution order, all injectable:
54
+ * ctx.identity > ctx.env.MAESTRO_AGENT_ID > cfg.broker.identity > "agent".
55
+ *
56
+ * @param {object} cfg
57
+ * @param {object} ctx
58
+ * @returns {string}
59
+ */
60
+ export function getAgentIdentity(cfg, ctx = {}) {
61
+ const env = ctx.env || process.env;
62
+ return (
63
+ ctx.identity ||
64
+ env.MAESTRO_AGENT_ID ||
65
+ (cfg && cfg.broker && cfg.broker.identity) ||
66
+ "agent"
67
+ );
68
+ }
69
+
70
+ /**
71
+ * The agent token used as the broker Bearer credential. Injectable so tests
72
+ * never read a real secret: ctx.token > ctx.env.MAESTRO_AGENT_TOKEN >
73
+ * cfg.broker.token_env-named env var.
74
+ *
75
+ * @param {object} cfg
76
+ * @param {object} ctx
77
+ * @returns {string}
78
+ */
79
+ export function getAgentToken(cfg, ctx = {}) {
80
+ const env = ctx.env || process.env;
81
+ if (ctx.token) return ctx.token;
82
+ const tokenEnv = cfg && cfg.broker && cfg.broker.token_env;
83
+ if (tokenEnv && env[tokenEnv]) return env[tokenEnv];
84
+ return env.MAESTRO_AGENT_TOKEN || "";
85
+ }
86
+
87
+ /**
88
+ * Construct the broker provider from config, or null when no broker is
89
+ * configured / no fetch is available. The fetch is INJECTED (ctx.fetch) — we
90
+ * never reach for the global so tests stay offline by construction.
91
+ *
92
+ * @param {object} cfg parsed config/secrets.yaml
93
+ * @param {object} ctx @param {typeof fetch} [ctx.fetch] @param {string} [ctx.token]
94
+ * @returns {BrokerProvider|null}
95
+ */
96
+ export function makeBroker(cfg, ctx = {}) {
97
+ const bcfg = (cfg && cfg.broker) || {};
98
+ const enabled = bcfg.enabled !== false; // default-on when a broker block exists
99
+ const baseUrl = ctx.baseUrl || bcfg.base_url || bcfg.baseUrl;
100
+ const fetchImpl = ctx.fetch; // required — no implicit global fetch
101
+ if (!enabled || !baseUrl || !fetchImpl) return null;
102
+ return new BrokerProvider({
103
+ baseUrl,
104
+ token: getAgentToken(cfg, ctx),
105
+ fetch: fetchImpl,
106
+ });
107
+ }
108
+
109
+ /**
110
+ * Whether a broker is the authoritative writer for this agent. A broker is
111
+ * authoritative when one is configured AND constructible (fetch present).
112
+ *
113
+ * @param {object} cfg
114
+ * @param {object} ctx
115
+ * @returns {boolean}
116
+ */
117
+ export function brokerIsAuthoritative(cfg, ctx = {}) {
118
+ return makeBroker(cfg, ctx) != null;
119
+ }
120
+
121
+ /**
122
+ * Resolve one secret by walking the provider chain in precedence order:
123
+ * explicit override > broker > file (local provider) > env.
124
+ *
125
+ * `explicit` lets a caller pin one provider (used by tooling that already
126
+ * knows where a value lives). Every read fails open: a provider that throws
127
+ * or returns null is skipped and the chain continues. Returns the first
128
+ * non-null value, or null if nothing in the chain has it.
129
+ *
130
+ * @param {string} name
131
+ * @param {object} args
132
+ * @param {object} args.cfg parsed config/secrets.yaml
133
+ * @param {string} [args.agentRoot]
134
+ * @param {Record<string,string>} [args.env]
135
+ * @param {typeof fetch} [args.fetch] injected fetch for the broker link
136
+ * @param {string} [args.token] injected agent token
137
+ * @param {{get:(n:string)=>any}} [args.explicit] an explicit provider to try first
138
+ * @returns {Promise<string|null>}
139
+ */
140
+ export async function resolveSecret(name, args = {}) {
141
+ const { cfg, agentRoot = ".", env, explicit } = args;
142
+ const ctx = { agentRoot, env, fetch: args.fetch, token: args.token, baseUrl: args.baseUrl };
143
+
144
+ const chain = [];
145
+ if (explicit) chain.push(explicit);
146
+
147
+ const broker = makeBroker(cfg, ctx);
148
+ if (broker) chain.push(broker);
149
+
150
+ // Local provider (file by default). If config explicitly selects `env`, the
151
+ // local provider IS env — we still add an env provider below, but a duplicate
152
+ // read is harmless (both fail open) and keeps the precedence comment honest.
153
+ const local = selectProvider(cfg, { agentRoot, env });
154
+ chain.push(local);
155
+
156
+ // Env is the final fallback so process-injected secrets always resolve even
157
+ // when nothing else is configured.
158
+ if (local.name !== "env") {
159
+ const { EnvProvider } = await import("./providers.mjs");
160
+ chain.push(new EnvProvider({ env }));
161
+ }
162
+
163
+ for (const provider of chain) {
164
+ try {
165
+ const v = await provider.get(name);
166
+ if (v != null && v !== "") return v;
167
+ } catch {
168
+ // fail open — try the next link
169
+ }
170
+ }
171
+ return null;
172
+ }
173
+
174
+ /**
175
+ * Pull a set of secrets from the broker into the local provider. This is the
176
+ * engine behind `maestro secrets sync`: it makes the local encrypted store the
177
+ * cached, offline-readable mirror of the org's broker.
178
+ *
179
+ * Per-secret semantics: fetch from the broker (fail-open read), then `set` on
180
+ * the local provider (fail-CLOSED write). A secret the broker doesn't have is
181
+ * recorded as `missing`; a local write that throws is recorded as `failed`.
182
+ * The caller (CLI) decides how loudly to surface those.
183
+ *
184
+ * @param {object} args
185
+ * @param {object} args.cfg
186
+ * @param {string[]} args.names secret names to pull (required)
187
+ * @param {string} [args.agentRoot]
188
+ * @param {Record<string,string>} [args.env]
189
+ * @param {typeof fetch} args.fetch injected broker fetch (required for a real pull)
190
+ * @param {string} [args.token]
191
+ * @returns {Promise<{synced:string[], missing:string[], failed:{name:string,error:string}[]}>}
192
+ */
193
+ export async function syncSecrets(args = {}) {
194
+ const { cfg, agentRoot = ".", env, names } = args;
195
+ if (!Array.isArray(names)) {
196
+ throw new Error("syncSecrets: { names: string[] } is required");
197
+ }
198
+ const ctx = { agentRoot, env, fetch: args.fetch, token: args.token, baseUrl: args.baseUrl };
199
+ const broker = makeBroker(cfg, ctx);
200
+ if (!broker) {
201
+ throw new Error("syncSecrets: no broker configured (need config.broker + an injected fetch)");
202
+ }
203
+ const local = selectProvider(cfg, { agentRoot, env });
204
+ if (local.readonly) {
205
+ throw new Error(`syncSecrets: local provider "${local.name}" is read-only — cannot cache pulled secrets`);
206
+ }
207
+
208
+ const result = { synced: [], missing: [], failed: [] };
209
+ for (const name of names) {
210
+ let value = null;
211
+ try {
212
+ value = await broker.get(name); // fail-open read
213
+ } catch {
214
+ value = null;
215
+ }
216
+ if (value == null) {
217
+ result.missing.push(name);
218
+ continue;
219
+ }
220
+ try {
221
+ await local.set(name, value); // fail-closed write
222
+ result.synced.push(name);
223
+ } catch (err) {
224
+ result.failed.push({ name, error: err.message });
225
+ }
226
+ }
227
+ return result;
228
+ }
229
+
230
+ /**
231
+ * Rotate a secret: write `newValue` to the authoritative provider and record
232
+ * names-only rotation metadata. Authoritative = broker when configured, else
233
+ * the local provider. The write is fail-CLOSED (a throw propagates) so a
234
+ * botched rotation never looks like success.
235
+ *
236
+ * The audit line records who/what/when/where — never the value. On a broker
237
+ * rotation we also `set` the local mirror best-effort so the agent can keep
238
+ * reading the new value offline; a mirror failure does NOT fail the rotation
239
+ * (the authoritative write already succeeded) but is noted in the return.
240
+ *
241
+ * @param {string} name
242
+ * @param {string} newValue
243
+ * @param {object} args
244
+ * @param {object} args.cfg
245
+ * @param {string} [args.agentRoot]
246
+ * @param {Record<string,string>} [args.env]
247
+ * @param {typeof fetch} [args.fetch]
248
+ * @param {string} [args.token]
249
+ * @param {string} [args.identity] override the audited agent identity
250
+ * @param {(p:string,r:object)=>boolean} [args.appendAudit] injected audit sink (tests)
251
+ * @returns {Promise<{name:string, provider:string, identity:string, at:string, mirrored:boolean}>}
252
+ */
253
+ export async function rotateSecret(name, newValue, args = {}) {
254
+ const { cfg, agentRoot = ".", env } = args;
255
+ if (!name || typeof name !== "string") {
256
+ throw new Error("rotateSecret: a secret name is required");
257
+ }
258
+ if (newValue == null || newValue === "") {
259
+ throw new Error(`rotateSecret: a non-empty value is required for "${name}"`);
260
+ }
261
+ const ctx = { agentRoot, env, fetch: args.fetch, token: args.token, baseUrl: args.baseUrl };
262
+ const broker = makeBroker(cfg, ctx);
263
+ const local = selectProvider(cfg, { agentRoot, env });
264
+
265
+ let authoritative;
266
+ if (broker) authoritative = broker;
267
+ else if (!local.readonly) authoritative = local;
268
+ else {
269
+ throw new Error(
270
+ `rotateSecret: no writable authoritative provider (local "${local.name}" is read-only and no broker is configured)`
271
+ );
272
+ }
273
+
274
+ // Fail-closed authoritative write.
275
+ await authoritative.set(name, newValue);
276
+
277
+ // Best-effort local mirror when the broker is authoritative so reads stay
278
+ // offline-fast. Never overrides the success of the authoritative write.
279
+ let mirrored = false;
280
+ if (authoritative === broker && !local.readonly) {
281
+ try {
282
+ await local.set(name, newValue);
283
+ mirrored = true;
284
+ } catch {
285
+ mirrored = false;
286
+ }
287
+ }
288
+
289
+ const identity = getAgentIdentity(cfg, { ...ctx, identity: args.identity });
290
+ const at = new Date().toISOString();
291
+ const record = {
292
+ event: "secret-rotated",
293
+ at,
294
+ identity,
295
+ name, // the NAME is operational metadata; the VALUE is never logged
296
+ name_fingerprint: nameFingerprint(name),
297
+ provider: authoritative.name,
298
+ mirrored,
299
+ };
300
+ const sink = args.appendAudit || appendJsonl;
301
+ sink(join(String(agentRoot), AUDIT_REL), record);
302
+
303
+ return { name, provider: authoritative.name, identity, at, mirrored };
304
+ }
305
+
306
+ /**
307
+ * Resolve the path of the names-only rotation audit log for an agent root.
308
+ * Exported so the CLI / doctor can point at it without duplicating the const.
309
+ *
310
+ * @param {string} agentRoot
311
+ * @returns {string}
312
+ */
313
+ export function auditLogPath(agentRoot) {
314
+ return join(String(agentRoot || "."), AUDIT_REL);
315
+ }
@@ -0,0 +1,280 @@
1
+ /**
2
+ * lib/secrets/broker.test.mjs — resolution chain, sync, rotate, names-only audit.
3
+ *
4
+ * Covers:
5
+ * • resolveSecret chain precedence: explicit > broker > file > env
6
+ * • resolveSecret fails open through a throwing provider
7
+ * • syncSecrets pulls a set from an INJECTED broker fetch into the local store
8
+ * • syncSecrets reports missing + write failures
9
+ * • rotateSecret writes to the authoritative provider (broker > local)
10
+ * • rotateSecret audits names-only (never the value), with identity + fingerprint
11
+ * • getAgentIdentity / getAgentToken resolution
12
+ */
13
+
14
+ import { test } from "node:test";
15
+ import assert from "node:assert/strict";
16
+ import { promises as fsp, readFileSync, existsSync } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import { join } from "node:path";
19
+
20
+ import {
21
+ resolveSecret,
22
+ syncSecrets,
23
+ rotateSecret,
24
+ getAgentIdentity,
25
+ getAgentToken,
26
+ makeBroker,
27
+ brokerIsAuthoritative,
28
+ auditLogPath,
29
+ } from "./broker.mjs";
30
+ import { FileProvider, nameFingerprint } from "./providers.mjs";
31
+
32
+ async function tmpRoot(prefix = "secrets-broker") {
33
+ const path = join(tmpdir(), `${prefix}-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
34
+ await fsp.mkdir(path, { recursive: true });
35
+ return path;
36
+ }
37
+ async function rmRoot(p) { try { await fsp.rm(p, { recursive: true, force: true }); } catch { /* */ } }
38
+
39
+ /** A fetch stub backed by an in-memory secrets object (offline). */
40
+ function brokerFetchStub(secrets) {
41
+ return async (url, opts = {}) => {
42
+ const u = new URL(url);
43
+ const m = u.pathname.match(/\/secrets\/(.+)$/);
44
+ if (opts.method === "GET" && u.pathname === "/secrets") {
45
+ return { ok: true, status: 200, json: async () => ({ secrets: Object.keys(secrets).map((name) => ({ name })) }) };
46
+ }
47
+ if (opts.method === "GET" && m) {
48
+ const name = decodeURIComponent(m[1]);
49
+ if (!(name in secrets)) return { ok: false, status: 404, json: async () => ({}) };
50
+ return { ok: true, status: 200, json: async () => ({ value: secrets[name] }) };
51
+ }
52
+ if (opts.method === "PUT" && m) {
53
+ secrets[decodeURIComponent(m[1])] = JSON.parse(opts.body).value;
54
+ return { ok: true, status: 200, json: async () => ({}) };
55
+ }
56
+ return { ok: false, status: 400, json: async () => ({}) };
57
+ };
58
+ }
59
+
60
+ const BROKER_CFG = { provider: "file", broker: { base_url: "https://broker.example", token_env: "MAESTRO_AGENT_TOKEN" } };
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // identity + token
64
+ // ---------------------------------------------------------------------------
65
+
66
+ test("getAgentIdentity / getAgentToken resolve from ctx then env then cfg", () => {
67
+ assert.equal(getAgentIdentity({}, { identity: "explicit" }), "explicit");
68
+ assert.equal(getAgentIdentity({}, { env: { MAESTRO_AGENT_ID: "robin" } }), "robin");
69
+ assert.equal(getAgentIdentity({ broker: { identity: "cfg-id" } }, { env: {} }), "cfg-id");
70
+ assert.equal(getAgentIdentity({}, { env: {} }), "agent");
71
+
72
+ assert.equal(getAgentToken({}, { token: "tok" }), "tok");
73
+ assert.equal(getAgentToken(BROKER_CFG, { env: { MAESTRO_AGENT_TOKEN: "from-env" } }), "from-env");
74
+ assert.equal(getAgentToken({}, { env: {} }), "");
75
+ });
76
+
77
+ test("makeBroker / brokerIsAuthoritative require config + an injected fetch", () => {
78
+ assert.equal(makeBroker({}, {}), null, "no broker block → null");
79
+ assert.equal(makeBroker(BROKER_CFG, {}), null, "no fetch → null (stay offline)");
80
+ const b = makeBroker(BROKER_CFG, { fetch: async () => ({ ok: true, status: 200, json: async () => ({}) }), token: "t" });
81
+ assert.ok(b && b.name === "broker");
82
+ assert.equal(brokerIsAuthoritative(BROKER_CFG, { fetch: b._fetch }), true);
83
+ assert.equal(brokerIsAuthoritative({}, {}), false);
84
+ });
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // resolveSecret — chain precedence
88
+ // ---------------------------------------------------------------------------
89
+
90
+ test("resolveSecret precedence: explicit > broker > file > env", async () => {
91
+ const root = await tmpRoot();
92
+ try {
93
+ // Seed the file store with FILE_KEY; broker has BROKER_KEY + a shadowed key.
94
+ const file = new FileProvider({ agentRoot: root });
95
+ file.set("FILE_KEY", "from-file");
96
+ file.set("SHADOW", "file-shadow");
97
+ const secrets = { BROKER_KEY: "from-broker", SHADOW: "broker-shadow" };
98
+ const fetch = brokerFetchStub(secrets);
99
+ const env = { ENV_KEY: "from-env", SHADOW: "env-shadow" };
100
+
101
+ const base = { cfg: BROKER_CFG, agentRoot: root, env, fetch, token: "t" };
102
+
103
+ // explicit wins over everything
104
+ const explicit = { get: () => "from-explicit" };
105
+ assert.equal(await resolveSecret("SHADOW", { ...base, explicit }), "from-explicit");
106
+
107
+ // broker beats file beats env on the same name
108
+ assert.equal(await resolveSecret("SHADOW", base), "broker-shadow");
109
+
110
+ // broker-only key resolves from the broker
111
+ assert.equal(await resolveSecret("BROKER_KEY", base), "from-broker");
112
+ // file-only key resolves from the file store
113
+ assert.equal(await resolveSecret("FILE_KEY", base), "from-file");
114
+ // env-only key resolves from env (final fallback)
115
+ assert.equal(await resolveSecret("ENV_KEY", base), "from-env");
116
+ // nothing has it
117
+ assert.equal(await resolveSecret("ABSENT", base), null);
118
+ } finally { await rmRoot(root); }
119
+ });
120
+
121
+ test("resolveSecret fails open through a throwing provider in the chain", async () => {
122
+ const root = await tmpRoot();
123
+ try {
124
+ const file = new FileProvider({ agentRoot: root });
125
+ file.set("K", "from-file");
126
+ const explicit = { get: () => { throw new Error("boom"); } };
127
+ // Explicit throws → chain continues to the file store.
128
+ assert.equal(await resolveSecret("K", { cfg: { provider: "file" }, agentRoot: root, env: {}, explicit }), "from-file");
129
+ } finally { await rmRoot(root); }
130
+ });
131
+
132
+ test("resolveSecret with no broker config still reads file then env", async () => {
133
+ const root = await tmpRoot();
134
+ try {
135
+ const file = new FileProvider({ agentRoot: root });
136
+ file.set("ONLY_FILE", "f");
137
+ const args = { cfg: { provider: "file" }, agentRoot: root, env: { ONLY_ENV: "e" } };
138
+ assert.equal(await resolveSecret("ONLY_FILE", args), "f");
139
+ assert.equal(await resolveSecret("ONLY_ENV", args), "e");
140
+ } finally { await rmRoot(root); }
141
+ });
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // syncSecrets — pull from injected broker into local store
145
+ // ---------------------------------------------------------------------------
146
+
147
+ test("syncSecrets pulls a set from the broker into the local file store", async () => {
148
+ const root = await tmpRoot();
149
+ try {
150
+ const secrets = { A: "alpha", B: "bravo", C: "charlie" };
151
+ const fetch = brokerFetchStub(secrets);
152
+ const res = await syncSecrets({
153
+ cfg: BROKER_CFG, agentRoot: root, env: {}, fetch, token: "t",
154
+ names: ["A", "B", "MISSING"],
155
+ });
156
+ assert.deepEqual(res.synced.sort(), ["A", "B"]);
157
+ assert.deepEqual(res.missing, ["MISSING"]);
158
+ assert.deepEqual(res.failed, []);
159
+
160
+ // The local store now holds the pulled values, sealed.
161
+ const file = new FileProvider({ agentRoot: root });
162
+ assert.equal(file.get("A"), "alpha");
163
+ assert.equal(file.get("B"), "bravo");
164
+ assert.equal(file.get("MISSING"), null);
165
+
166
+ // And the value never appears in plaintext on disk.
167
+ const raw = readFileSync(join(root, "state/secrets/.secrets.json"), "utf-8");
168
+ assert.ok(!raw.includes("alpha") && !raw.includes("bravo"), "sealed at rest");
169
+ } finally { await rmRoot(root); }
170
+ });
171
+
172
+ test("syncSecrets throws when no broker is configured", async () => {
173
+ const root = await tmpRoot();
174
+ try {
175
+ await assert.rejects(
176
+ () => syncSecrets({ cfg: { provider: "file" }, agentRoot: root, names: ["A"] }),
177
+ /no broker configured/
178
+ );
179
+ } finally { await rmRoot(root); }
180
+ });
181
+
182
+ test("syncSecrets requires names array", async () => {
183
+ await assert.rejects(() => syncSecrets({ cfg: BROKER_CFG }), /names: string\[\] \} is required/);
184
+ });
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // rotateSecret — authoritative write + names-only audit
188
+ // ---------------------------------------------------------------------------
189
+
190
+ test("rotateSecret writes to the local provider and audits names-only", async () => {
191
+ const root = await tmpRoot();
192
+ try {
193
+ const res = await rotateSecret("DB_PASSWORD", "p@ss-NEW", {
194
+ cfg: { provider: "file" }, agentRoot: root, env: { MAESTRO_AGENT_ID: "alex" },
195
+ });
196
+ assert.equal(res.provider, "file");
197
+ assert.equal(res.identity, "alex");
198
+ assert.match(res.at, /^\d{4}-\d{2}-\d{2}T/);
199
+
200
+ // The new value is readable.
201
+ const file = new FileProvider({ agentRoot: root });
202
+ assert.equal(file.get("DB_PASSWORD"), "p@ss-NEW");
203
+
204
+ // Audit line: names + identity + fingerprint, NEVER the value.
205
+ const logPath = auditLogPath(root);
206
+ assert.ok(existsSync(logPath), "audit log written");
207
+ const raw = readFileSync(logPath, "utf-8").trim();
208
+ assert.ok(!raw.includes("p@ss-NEW"), "audit must never contain the secret value");
209
+ const rec = JSON.parse(raw.split("\n").pop());
210
+ assert.equal(rec.event, "secret-rotated");
211
+ assert.equal(rec.name, "DB_PASSWORD");
212
+ assert.equal(rec.identity, "alex");
213
+ assert.equal(rec.provider, "file");
214
+ assert.equal(rec.name_fingerprint, nameFingerprint("DB_PASSWORD"));
215
+ } finally { await rmRoot(root); }
216
+ });
217
+
218
+ test("rotateSecret writes to the broker (authoritative) and mirrors locally", async () => {
219
+ const root = await tmpRoot();
220
+ try {
221
+ const secrets = {};
222
+ const fetch = brokerFetchStub(secrets);
223
+ const res = await rotateSecret("API_TOKEN", "tok-rotated", {
224
+ cfg: BROKER_CFG, agentRoot: root, env: {}, fetch, token: "agent-tok", identity: "robin",
225
+ });
226
+ assert.equal(res.provider, "broker", "broker is authoritative when configured");
227
+ assert.equal(res.mirrored, true, "local mirror written for offline reads");
228
+ assert.equal(secrets.API_TOKEN, "tok-rotated", "broker received the rotation");
229
+ // Local mirror holds it too.
230
+ assert.equal(new FileProvider({ agentRoot: root }).get("API_TOKEN"), "tok-rotated");
231
+ } finally { await rmRoot(root); }
232
+ });
233
+
234
+ test("rotateSecret fails closed on an empty value or missing name", async () => {
235
+ const root = await tmpRoot();
236
+ try {
237
+ await assert.rejects(() => rotateSecret("", "v", { cfg: { provider: "file" }, agentRoot: root }), /name is required/);
238
+ await assert.rejects(() => rotateSecret("K", "", { cfg: { provider: "file" }, agentRoot: root }), /non-empty value/);
239
+ } finally { await rmRoot(root); }
240
+ });
241
+
242
+ test("rotateSecret fails closed when the broker write rejects", async () => {
243
+ const root = await tmpRoot();
244
+ try {
245
+ const fetch = async (url, opts) => {
246
+ if (opts.method === "PUT") return { ok: false, status: 503, json: async () => ({}) };
247
+ return { ok: false, status: 404, json: async () => ({}) };
248
+ };
249
+ await assert.rejects(
250
+ () => rotateSecret("K", "v", { cfg: BROKER_CFG, agentRoot: root, env: {}, fetch, token: "t" }),
251
+ /failed \(status 503\)/
252
+ );
253
+ // No audit line on a failed rotation.
254
+ assert.ok(!existsSync(auditLogPath(root)), "no audit on failed authoritative write");
255
+ } finally { await rmRoot(root); }
256
+ });
257
+
258
+ test("rotateSecret fails closed when the local provider is read-only (env) and no broker", async () => {
259
+ const root = await tmpRoot();
260
+ try {
261
+ await assert.rejects(
262
+ () => rotateSecret("K", "v", { cfg: { provider: "env" }, agentRoot: root, env: {} }),
263
+ /no writable authoritative provider/
264
+ );
265
+ } finally { await rmRoot(root); }
266
+ });
267
+
268
+ test("rotateSecret uses an injected audit sink when provided", async () => {
269
+ const root = await tmpRoot();
270
+ try {
271
+ const lines = [];
272
+ await rotateSecret("K", "v", {
273
+ cfg: { provider: "file" }, agentRoot: root, env: {},
274
+ appendAudit: (p, rec) => { lines.push({ p, rec }); return true; },
275
+ });
276
+ assert.equal(lines.length, 1);
277
+ assert.equal(lines[0].rec.name, "K");
278
+ assert.ok(lines[0].p.endsWith("logs/audit/secrets.jsonl"));
279
+ } finally { await rmRoot(root); }
280
+ });