@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,441 @@
1
+ /**
2
+ * lib/security/redact.test.mjs
3
+ *
4
+ * node:test coverage for redact.mjs:
5
+ * - user:pass@ userinfo is stripped
6
+ * - sensitive query params (token/key/secret/password/apikey/...) are masked
7
+ * - benign URLs are untouched
8
+ * - malformed URLs do not throw
9
+ * - private vs public IP classification, incl. IPv4-mapped-IPv6 bypass
10
+ * (::ffff:10.0.0.1) and loopback 127.0.0.1
11
+ *
12
+ * Run: cd /Users/wong/maestro && node --test lib/security/redact.test.mjs
13
+ */
14
+
15
+ import test from "node:test";
16
+ import assert from "node:assert/strict";
17
+
18
+ import {
19
+ redactSensitiveUrl,
20
+ isPrivateOrReservedIp,
21
+ isSensitiveParamName,
22
+ REDACTED,
23
+ maskText,
24
+ maskTextString,
25
+ redactObject,
26
+ REDACTED_LABEL_PREFIX,
27
+ } from "./redact.mjs";
28
+
29
+ /* ----------------------------- userinfo ---------------------------------- */
30
+
31
+ test("strips user:pass@ userinfo from a URL", () => {
32
+ const out = redactSensitiveUrl("https://alice:hunter2@example.com/path?x=1");
33
+ assert.ok(!out.includes("alice"), `username leaked: ${out}`);
34
+ assert.ok(!out.includes("hunter2"), `password leaked: ${out}`);
35
+ assert.ok(!out.includes("@example.com"), `@ host leaked: ${out}`);
36
+ assert.ok(out.startsWith("https://example.com/"), `host changed: ${out}`);
37
+ // Non-sensitive param preserved.
38
+ assert.ok(out.includes("x=1"), `benign param dropped: ${out}`);
39
+ });
40
+
41
+ test("strips userinfo with username only (no password)", () => {
42
+ const out = redactSensitiveUrl("https://tokenuser@api.example.com/");
43
+ assert.ok(!out.includes("tokenuser"), `username leaked: ${out}`);
44
+ assert.ok(out.startsWith("https://api.example.com/"), out);
45
+ });
46
+
47
+ /* --------------------------- secret params ------------------------------- */
48
+
49
+ test("masks sensitive query params, keeps benign ones", () => {
50
+ const out = redactSensitiveUrl(
51
+ "https://example.com/api?token=abc123&page=2&api_key=zzz&q=hello"
52
+ );
53
+ assert.ok(out.includes(`token=${encodeURIComponent(REDACTED)}`) || out.includes("token=***"), out);
54
+ assert.ok(!out.includes("abc123"), `token value leaked: ${out}`);
55
+ assert.ok(!out.includes("zzz"), `api_key value leaked: ${out}`);
56
+ // Benign params untouched.
57
+ assert.ok(out.includes("page=2"), out);
58
+ assert.ok(out.includes("q=hello"), out);
59
+ });
60
+
61
+ test("masks each sensitive keyword variant", () => {
62
+ const cases = [
63
+ "token",
64
+ "access_token",
65
+ "refresh_token",
66
+ "key",
67
+ "apiKey",
68
+ "api_key",
69
+ "secret",
70
+ "client_secret",
71
+ "password",
72
+ "passwd",
73
+ "pwd",
74
+ "auth",
75
+ "signature",
76
+ "sig",
77
+ ];
78
+ for (const k of cases) {
79
+ const out = redactSensitiveUrl(`https://e.com/?${k}=SUPERSECRETVALUE`);
80
+ assert.ok(
81
+ !out.includes("SUPERSECRETVALUE"),
82
+ `param "${k}" not masked: ${out}`
83
+ );
84
+ }
85
+ });
86
+
87
+ test("masks repeated sensitive params (multiplicity preserved)", () => {
88
+ const out = redactSensitiveUrl("https://e.com/?key=a&key=b&n=1");
89
+ assert.ok(!out.includes("=a"), out);
90
+ assert.ok(!out.includes("=b"), out);
91
+ // two key= occurrences remain
92
+ const count = (out.match(/key=/g) || []).length;
93
+ assert.equal(count, 2, `expected 2 key= occurrences: ${out}`);
94
+ assert.ok(out.includes("n=1"), out);
95
+ });
96
+
97
+ /* ----------------------------- benign ------------------------------------ */
98
+
99
+ test("leaves a benign URL effectively untouched", () => {
100
+ const url = "https://example.com/docs/page?lang=en&page=2";
101
+ const out = redactSensitiveUrl(url);
102
+ assert.ok(out.includes("example.com/docs/page"), out);
103
+ assert.ok(out.includes("lang=en"), out);
104
+ assert.ok(out.includes("page=2"), out);
105
+ assert.ok(!out.includes(REDACTED), `nothing should be masked: ${out}`);
106
+ });
107
+
108
+ test("benign URL with no query is unchanged", () => {
109
+ const url = "https://example.com/";
110
+ assert.equal(redactSensitiveUrl(url), url);
111
+ });
112
+
113
+ /* ---------------------------- malformed ---------------------------------- */
114
+
115
+ test("malformed / non-URL inputs do not throw", () => {
116
+ const inputs = [
117
+ "",
118
+ "not a url",
119
+ "http://",
120
+ "://missing-scheme",
121
+ "ht!tp://bad",
122
+ "https://alice:pw@", // truncated authority
123
+ "token=secret&password=hunter2", // bare query-ish string
124
+ "javascript:alert(1)",
125
+ " ",
126
+ "%%%not-encoded%%%",
127
+ ];
128
+ for (const inp of inputs) {
129
+ assert.doesNotThrow(() => redactSensitiveUrl(inp), `threw on: ${JSON.stringify(inp)}`);
130
+ }
131
+ });
132
+
133
+ test("non-string inputs are handled without throwing", () => {
134
+ assert.doesNotThrow(() => redactSensitiveUrl(null));
135
+ assert.doesNotThrow(() => redactSensitiveUrl(undefined));
136
+ assert.doesNotThrow(() => redactSensitiveUrl(12345));
137
+ assert.equal(redactSensitiveUrl(null), "");
138
+ assert.equal(redactSensitiveUrl(undefined), "");
139
+ });
140
+
141
+ test("regex fallback masks secrets in unparseable query strings", () => {
142
+ const out = redactSensitiveUrl("token=abc123&page=2&password=hunter2");
143
+ assert.ok(!out.includes("abc123"), out);
144
+ assert.ok(!out.includes("hunter2"), out);
145
+ assert.ok(out.includes("page=2"), out);
146
+ });
147
+
148
+ /* ------------------------ isSensitiveParamName --------------------------- */
149
+
150
+ test("isSensitiveParamName matches case-insensitively", () => {
151
+ assert.equal(isSensitiveParamName("ApiKey"), true);
152
+ assert.equal(isSensitiveParamName("X-Auth-Token"), true);
153
+ assert.equal(isSensitiveParamName("PASSWORD"), true);
154
+ assert.equal(isSensitiveParamName("page"), false);
155
+ assert.equal(isSensitiveParamName("q"), false);
156
+ assert.equal(isSensitiveParamName(""), false);
157
+ assert.equal(isSensitiveParamName(null), false);
158
+ });
159
+
160
+ /* --------------------------- IP: private --------------------------------- */
161
+
162
+ test("classifies private IPv4 ranges as private/reserved", () => {
163
+ const priv = [
164
+ "10.0.0.1",
165
+ "10.255.255.255",
166
+ "172.16.0.1",
167
+ "172.31.255.255",
168
+ "192.168.0.1",
169
+ "192.168.1.1",
170
+ "127.0.0.1", // loopback
171
+ "127.1.2.3",
172
+ "169.254.0.1", // link-local
173
+ "0.0.0.0", // unspecified
174
+ "100.64.0.1", // CGNAT
175
+ "255.255.255.255", // broadcast / reserved
176
+ "224.0.0.1", // multicast
177
+ "198.18.0.1", // benchmarking
178
+ "192.0.2.5", // TEST-NET-1
179
+ "203.0.113.9", // TEST-NET-3
180
+ ];
181
+ for (const ip of priv) {
182
+ assert.equal(isPrivateOrReservedIp(ip), true, `${ip} should be private/reserved`);
183
+ }
184
+ });
185
+
186
+ test("127.0.0.1 loopback is private/reserved", () => {
187
+ assert.equal(isPrivateOrReservedIp("127.0.0.1"), true);
188
+ });
189
+
190
+ /* ---------------------------- IP: public --------------------------------- */
191
+
192
+ test("classifies public IPv4 as NOT private/reserved", () => {
193
+ const pub = ["8.8.8.8", "1.1.1.1", "93.184.216.34", "172.15.0.1", "172.32.0.1", "11.0.0.1"];
194
+ for (const ip of pub) {
195
+ assert.equal(isPrivateOrReservedIp(ip), false, `${ip} should be public`);
196
+ }
197
+ });
198
+
199
+ /* --------------------------- IP: IPv6 ------------------------------------ */
200
+
201
+ test("IPv4-mapped-IPv6 private addresses are caught (bypass protection)", () => {
202
+ assert.equal(isPrivateOrReservedIp("::ffff:10.0.0.1"), true);
203
+ assert.equal(isPrivateOrReservedIp("::ffff:127.0.0.1"), true);
204
+ assert.equal(isPrivateOrReservedIp("::ffff:192.168.1.1"), true);
205
+ assert.equal(isPrivateOrReservedIp("[::ffff:10.0.0.1]"), true);
206
+ });
207
+
208
+ test("IPv4-mapped-IPv6 public address stays public", () => {
209
+ assert.equal(isPrivateOrReservedIp("::ffff:8.8.8.8"), false);
210
+ });
211
+
212
+ test("classifies IPv6 loopback / ULA / link-local / multicast as private", () => {
213
+ assert.equal(isPrivateOrReservedIp("::1"), true); // loopback
214
+ assert.equal(isPrivateOrReservedIp("[::1]"), true);
215
+ assert.equal(isPrivateOrReservedIp("::"), true); // unspecified
216
+ assert.equal(isPrivateOrReservedIp("fc00::1"), true); // ULA
217
+ assert.equal(isPrivateOrReservedIp("fd12:3456:789a::1"), true); // ULA
218
+ assert.equal(isPrivateOrReservedIp("fe80::1"), true); // link-local
219
+ assert.equal(isPrivateOrReservedIp("fe80::1%eth0"), true); // link-local + zone
220
+ assert.equal(isPrivateOrReservedIp("ff02::1"), true); // multicast
221
+ });
222
+
223
+ test("classifies public IPv6 as NOT private", () => {
224
+ assert.equal(isPrivateOrReservedIp("2606:4700:4700::1111"), false); // cloudflare
225
+ assert.equal(isPrivateOrReservedIp("2001:4860:4860::8888"), false); // google
226
+ });
227
+
228
+ /* --------------------------- IP: edges ----------------------------------- */
229
+
230
+ test("non-IP hosts and junk return false (classifies addresses, not names)", () => {
231
+ assert.equal(isPrivateOrReservedIp("example.com"), false);
232
+ assert.equal(isPrivateOrReservedIp("localhost"), false);
233
+ assert.equal(isPrivateOrReservedIp(""), false);
234
+ assert.equal(isPrivateOrReservedIp(" "), false);
235
+ assert.equal(isPrivateOrReservedIp("999.999.999.999"), false);
236
+ assert.equal(isPrivateOrReservedIp("not-an-ip"), false);
237
+ assert.equal(isPrivateOrReservedIp(null), false);
238
+ assert.equal(isPrivateOrReservedIp(undefined), false);
239
+ });
240
+
241
+ test("isPrivateOrReservedIp never throws on hostile input", () => {
242
+ for (const v of ["", "::::", "[", "]", "1.2.3", "::ffff:", "fe80::%", null, 42, {}]) {
243
+ assert.doesNotThrow(() => isPrivateOrReservedIp(/** @type {any} */ (v)));
244
+ }
245
+ });
246
+
247
+ /* ========================================================================= *
248
+ * maskText — free-text PII / secret masking
249
+ * ========================================================================= */
250
+
251
+ /** Assert a single secret class is fully masked and counted. */
252
+ function assertMasked(input, secret, label) {
253
+ const { text, count } = maskText(input);
254
+ assert.ok(!text.includes(secret), `${label} value leaked: ${text}`);
255
+ assert.ok(count >= 1, `${label} not counted: ${JSON.stringify({ text, count })}`);
256
+ assert.ok(
257
+ text.includes(`${REDACTED_LABEL_PREFIX}`),
258
+ `${label} placeholder missing: ${text}`
259
+ );
260
+ }
261
+
262
+ test("maskText redacts Anthropic keys (sk-ant-) without leaking sk- generic", () => {
263
+ const key = "sk-ant-api03-AbCdEf0123456789_GhIjKlMnOpQrStUvWx";
264
+ const { text, count } = maskText(`my key is ${key} ok`);
265
+ assert.ok(!text.includes(key), text);
266
+ assert.equal(count, 1);
267
+ assert.ok(text.includes("[REDACTED:ANTHROPIC_KEY]"), text);
268
+ });
269
+
270
+ test("maskText redacts OpenAI-style sk- and sk-proj- keys", () => {
271
+ assertMasked("token sk-proj-0123456789ABCDEFabcdef0123 here", "sk-proj-0123456789ABCDEFabcdef0123", "API_KEY");
272
+ assertMasked("token sk-0123456789ABCDEFabcdef0123 here", "sk-0123456789ABCDEFabcdef0123", "API_KEY");
273
+ });
274
+
275
+ test("maskText redacts AWS access key ids (AKIA/ASIA)", () => {
276
+ assertMasked("creds AKIAIOSFODNN7EXAMPLE done", "AKIAIOSFODNN7EXAMPLE", "AWS_KEY");
277
+ assertMasked("creds ASIAIOSFODNN7EXAMPLE done", "ASIAIOSFODNN7EXAMPLE", "AWS_KEY");
278
+ });
279
+
280
+ test("maskText redacts GitHub tokens (ghp_ / github_pat_)", () => {
281
+ assertMasked(
282
+ "auth ghp_0123456789abcdefABCDEF0123456789abcdef tail",
283
+ "ghp_0123456789abcdefABCDEF0123456789abcdef",
284
+ "GITHUB_TOKEN"
285
+ );
286
+ assertMasked(
287
+ "auth github_pat_0123456789abcdefABCD_extra tail",
288
+ "github_pat_0123456789abcdefABCD_extra",
289
+ "GITHUB_TOKEN"
290
+ );
291
+ });
292
+
293
+ test("maskText redacts bearer tokens", () => {
294
+ const { text, count } = maskText("Authorization: Bearer abc.DEF-123_xyz=+/ rest");
295
+ assert.ok(!text.includes("abc.DEF-123_xyz"), text);
296
+ assert.ok(count >= 1, `count=${count}`);
297
+ assert.ok(text.includes("[REDACTED:BEARER_TOKEN]"), text);
298
+ });
299
+
300
+ test("maskText redacts JWTs", () => {
301
+ const jwt =
302
+ "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U";
303
+ const { text, count } = maskText(`session=${jwt};`);
304
+ assert.ok(!text.includes(jwt), text);
305
+ assert.ok(count >= 1);
306
+ });
307
+
308
+ test("maskText redacts PEM private key blocks", () => {
309
+ const pem =
310
+ "-----BEGIN RSA PRIVATE KEY-----\nMIIBVAIBADANBgkqh\nkiG9w0BAQEFA\n-----END RSA PRIVATE KEY-----";
311
+ const { text, count } = maskText(`key:\n${pem}\nend`);
312
+ assert.ok(!text.includes("MIIBVAIBADAN"), text);
313
+ assert.ok(count >= 1);
314
+ assert.ok(text.includes("[REDACTED:PRIVATE_KEY]"), text);
315
+ });
316
+
317
+ test("maskText redacts email addresses", () => {
318
+ assertMasked("contact alice.smith+tag@example.co.uk please", "alice.smith+tag@example.co.uk", "EMAIL");
319
+ });
320
+
321
+ test("maskText redacts phone numbers", () => {
322
+ const { text, count } = maskText("call me at +1 (415) 555-0132 tomorrow");
323
+ assert.ok(!text.includes("555-0132"), text);
324
+ assert.ok(count >= 1);
325
+ assert.ok(text.includes("[REDACTED:PHONE]"), text);
326
+ });
327
+
328
+ test("maskText redacts long hex secrets", () => {
329
+ const hex = "deadbeefcafef00d0123456789abcdef0123456789abcdef"; // 48 hex chars
330
+ assertMasked(`hash=${hex} ok`, hex, "HEX_SECRET");
331
+ });
332
+
333
+ test("maskText redacts long base64-shaped secrets", () => {
334
+ const b64 = "QWxhZGRpbjpvcGVuIHNlc2FtZQ123456789abcDEF_xyz-987654321";
335
+ const { text, count } = maskText(`blob ${b64} end`);
336
+ assert.ok(!text.includes(b64), text);
337
+ assert.ok(count >= 1);
338
+ });
339
+
340
+ test("maskText leaves benign prose untouched and counts zero", () => {
341
+ const benign =
342
+ "The quarterly report is due Friday. Page 2 covers revenue; total was 1234 units across 3 regions.";
343
+ const { text, count } = maskText(benign);
344
+ assert.equal(text, benign);
345
+ assert.equal(count, 0);
346
+ assert.ok(!text.includes(REDACTED_LABEL_PREFIX), text);
347
+ });
348
+
349
+ test("maskText counts multiple distinct secrets in one string", () => {
350
+ const input =
351
+ "email bob@corp.com key sk-ant-api03-ZZZZZZZZ_aaaaaaaa11112222 and aws AKIAIOSFODNN7EXAMPLE";
352
+ const { text, count } = maskText(input);
353
+ assert.ok(!text.includes("bob@corp.com"), text);
354
+ assert.ok(!text.includes("AKIAIOSFODNN7EXAMPLE"), text);
355
+ assert.equal(count, 3, `expected 3 redactions, got ${count}: ${text}`);
356
+ });
357
+
358
+ test("maskText never throws and coerces non-strings", () => {
359
+ assert.deepEqual(maskText(null), { text: "", count: 0 });
360
+ assert.deepEqual(maskText(undefined), { text: "", count: 0 });
361
+ assert.equal(maskText(12345).count, 0);
362
+ assert.doesNotThrow(() => maskText({}));
363
+ assert.deepEqual(maskText(""), { text: "", count: 0 });
364
+ });
365
+
366
+ test("maskText honors a custom placeholder factory", () => {
367
+ const { text } = maskText("mail me at x@y.com", {
368
+ placeholder: (label) => `<${label}>`,
369
+ });
370
+ assert.ok(text.includes("<EMAIL>"), text);
371
+ assert.ok(!text.includes("x@y.com"), text);
372
+ });
373
+
374
+ test("maskTextString returns only the cleaned string (masker-hook shape)", () => {
375
+ const out = maskTextString("token sk-0123456789ABCDEFabcdef0123 done");
376
+ assert.equal(typeof out, "string");
377
+ assert.ok(!out.includes("sk-0123456789ABCDEFabcdef0123"), out);
378
+ });
379
+
380
+ /* ========================================================================= *
381
+ * redactObject — deep masking
382
+ * ========================================================================= */
383
+
384
+ test("redactObject deep-masks nested string values, preserving structure", () => {
385
+ const input = {
386
+ user: "alice@example.com",
387
+ meta: {
388
+ note: "all good",
389
+ creds: ["AKIAIOSFODNN7EXAMPLE", "harmless"],
390
+ depth: { token: "sk-ant-api03-QQQQQQQQ_bbbbbbbb33334444" },
391
+ },
392
+ count: 7,
393
+ enabled: true,
394
+ nothing: null,
395
+ };
396
+ const { value, count } = redactObject(input);
397
+
398
+ assert.ok(!value.user.includes("alice@example.com"), value.user);
399
+ assert.equal(value.meta.note, "all good"); // benign untouched
400
+ assert.ok(!value.meta.creds[0].includes("AKIAIOSFODNN7EXAMPLE"), value.meta.creds[0]);
401
+ assert.equal(value.meta.creds[1], "harmless");
402
+ assert.ok(!value.meta.depth.token.includes("sk-ant-"), value.meta.depth.token);
403
+ // Non-strings pass through unchanged.
404
+ assert.equal(value.count, 7);
405
+ assert.equal(value.enabled, true);
406
+ assert.equal(value.nothing, null);
407
+ // Keys preserved.
408
+ assert.deepEqual(Object.keys(value).sort(), ["count", "enabled", "meta", "nothing", "user"]);
409
+ assert.equal(count, 3);
410
+ });
411
+
412
+ test("redactObject does not mutate the input", () => {
413
+ const input = { a: "ping me at z@z.com" };
414
+ const before = input.a;
415
+ redactObject(input);
416
+ assert.equal(input.a, before, "input was mutated");
417
+ });
418
+
419
+ test("redactObject masks keys' values only, not the keys themselves", () => {
420
+ const input = { "token-field": "sk-0123456789ABCDEFabcdef0123" };
421
+ const { value } = redactObject(input);
422
+ assert.ok(Object.prototype.hasOwnProperty.call(value, "token-field"), "key dropped");
423
+ assert.ok(!value["token-field"].includes("sk-0123456789"), value["token-field"]);
424
+ });
425
+
426
+ test("redactObject handles cycles and never throws", () => {
427
+ const a = { name: "leak: secret@host.com" };
428
+ a.self = a; // cycle
429
+ let result;
430
+ assert.doesNotThrow(() => {
431
+ result = redactObject(a);
432
+ });
433
+ assert.ok(!result.value.name.includes("secret@host.com"), result.value.name);
434
+ assert.equal(result.count, 1);
435
+ });
436
+
437
+ test("redactObject passes scalars through untouched", () => {
438
+ assert.deepEqual(redactObject(42), { value: 42, count: 0 });
439
+ assert.deepEqual(redactObject(true), { value: true, count: 0 });
440
+ assert.deepEqual(redactObject(null), { value: null, count: 0 });
441
+ });
@@ -0,0 +1,73 @@
1
+ /**
2
+ * secret-equal.mjs — constant-time comparison of secrets (tokens, signatures,
3
+ * HMACs, shared keys) without leaking length or content through timing.
4
+ *
5
+ * A naive `a === b` short-circuits on the first differing byte and on length
6
+ * mismatch, which gives an attacker a timing oracle. node:crypto's
7
+ * timingSafeEqual fixes the byte-by-byte leak but THROWS when the two buffers
8
+ * differ in length — so calling it directly re-introduces an early-return path.
9
+ *
10
+ * safeEqualSecret() closes both holes: it always runs the constant-time
11
+ * compare over equal-length buffers (padded to the longer of the two) and only
12
+ * afterwards folds in the original length equality, so a length mismatch costs
13
+ * the same as a content mismatch. Non-string inputs return false instead of
14
+ * throwing.
15
+ *
16
+ * Ported and adapted from OpenClaw (src/security/secret-equal.ts) for the
17
+ * maestro autonomy model: any place that checks an inbound webhook signature,
18
+ * relay token, or shared secret should compare it with safeEqualSecret().
19
+ *
20
+ * Pure function; the only dependency is node:crypto.
21
+ */
22
+
23
+ import { timingSafeEqual } from "node:crypto";
24
+
25
+ /**
26
+ * Copy `bytes` into a fresh buffer of exactly `length` bytes, zero-padding the
27
+ * tail when the input is shorter. Returns the input untouched when it already
28
+ * matches `length`.
29
+ * @param {Buffer} bytes
30
+ * @param {number} length
31
+ * @returns {Buffer}
32
+ */
33
+ function padSecretBytes(bytes, length) {
34
+ if (bytes.length === length) {
35
+ return bytes;
36
+ }
37
+ const padded = Buffer.alloc(length);
38
+ bytes.copy(padded);
39
+ return padded;
40
+ }
41
+
42
+ /**
43
+ * Timing-safe secret comparison.
44
+ *
45
+ * Returns true iff `provided` and `expected` are both strings with identical
46
+ * UTF-8 bytes. The comparison runs in time independent of where the values
47
+ * differ and of their lengths, and never throws on length mismatch or
48
+ * non-string input.
49
+ *
50
+ * @param {string|undefined|null} provided
51
+ * @param {string|undefined|null} expected
52
+ * @returns {boolean}
53
+ */
54
+ export function safeEqualSecret(provided, expected) {
55
+ if (typeof provided !== "string" || typeof expected !== "string") {
56
+ return false;
57
+ }
58
+ const providedBytes = Buffer.from(provided, "utf8");
59
+ const expectedBytes = Buffer.from(expected, "utf8");
60
+ const byteLength = Math.max(providedBytes.length, expectedBytes.length);
61
+ if (byteLength === 0) {
62
+ // Both strings are empty: equal, and there is nothing to compare.
63
+ return true;
64
+ }
65
+ // Always perform the constant-time compare, then fold in the length check so
66
+ // a length mismatch is indistinguishable (timing-wise) from a content one.
67
+ return (
68
+ timingSafeEqual(
69
+ padSecretBytes(providedBytes, byteLength),
70
+ padSecretBytes(expectedBytes, byteLength),
71
+ ) && providedBytes.length === expectedBytes.length
72
+ );
73
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * secret-equal.test.mjs — node:test coverage for safeEqualSecret().
3
+ *
4
+ * Verifies: equal strings, differing strings, length mismatch (no throw),
5
+ * non-string inputs (no throw), and empty-string handling.
6
+ */
7
+
8
+ import test from "node:test";
9
+ import assert from "node:assert/strict";
10
+
11
+ import { safeEqualSecret } from "./secret-equal.mjs";
12
+
13
+ test("equal strings return true", () => {
14
+ assert.equal(safeEqualSecret("s3cr3t-token", "s3cr3t-token"), true);
15
+ });
16
+
17
+ test("different strings of same length return false", () => {
18
+ assert.equal(safeEqualSecret("abcdef", "abcxef"), false);
19
+ });
20
+
21
+ test("different lengths return false without throwing", () => {
22
+ assert.doesNotThrow(() => safeEqualSecret("short", "shorter-secret"));
23
+ assert.equal(safeEqualSecret("short", "shorter-secret"), false);
24
+ assert.equal(safeEqualSecret("shorter-secret", "short"), false);
25
+ // A string that is a strict prefix of the other must not compare equal.
26
+ assert.equal(safeEqualSecret("abc", "abcd"), false);
27
+ });
28
+
29
+ test("non-string inputs return false without throwing", () => {
30
+ assert.doesNotThrow(() => safeEqualSecret(undefined, "x"));
31
+ assert.equal(safeEqualSecret(undefined, "x"), false);
32
+ assert.equal(safeEqualSecret("x", undefined), false);
33
+ assert.equal(safeEqualSecret(null, "x"), false);
34
+ assert.equal(safeEqualSecret("x", null), false);
35
+ assert.equal(safeEqualSecret(123, "123"), false);
36
+ assert.equal(safeEqualSecret("123", 123), false);
37
+ assert.equal(safeEqualSecret(Buffer.from("x"), "x"), false);
38
+ assert.equal(safeEqualSecret({}, {}), false);
39
+ assert.equal(safeEqualSecret(undefined, undefined), false);
40
+ assert.equal(safeEqualSecret(null, null), false);
41
+ });
42
+
43
+ test("two empty strings are equal", () => {
44
+ assert.equal(safeEqualSecret("", ""), true);
45
+ });
46
+
47
+ test("empty string vs non-empty string returns false", () => {
48
+ assert.equal(safeEqualSecret("", "x"), false);
49
+ assert.equal(safeEqualSecret("x", ""), false);
50
+ });
51
+
52
+ test("multibyte/unicode strings compare on UTF-8 bytes", () => {
53
+ assert.equal(safeEqualSecret("séçret-🔑", "séçret-🔑"), true);
54
+ assert.equal(safeEqualSecret("séçret-🔑", "secret-key"), false);
55
+ });
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Maestro — Session permission scoping (audit H1).
3
+ *
4
+ * Every autonomous sub-session the daemon spawns (cadence ticks, and — by
5
+ * extension — reactive dispatcher/responder/classifier paths) historically ran
6
+ * `claude --print` with `--dangerously-skip-permissions`. That means an
7
+ * inbound message containing a prompt-injection payload can drive ARBITRARY
8
+ * tool use with no human in the loop, which on an unattended 24/7 Mac mini is
9
+ * the single highest-impact realistic compromise path (audit/AUDIT-REPORT.md
10
+ * finding H1).
11
+ *
12
+ * This module centralizes the decision of WHICH permission CLI args a spawned
13
+ * session gets, so the dangerous default becomes explicit and operators get a
14
+ * real gate. It is deliberately OPT-IN: with no env flag set, behaviour is
15
+ * BYTE-FOR-BYTE UNCHANGED (still `--dangerously-skip-permissions`). Operators
16
+ * who want the scoped behaviour set `MAESTRO_SCOPED_PERMISSIONS=1`.
17
+ *
18
+ * Pure function; zero dependencies beyond the cadence registry. Returns ONLY
19
+ * the permission flags — callers keep appending `daemonClaudeArgs()`
20
+ * (`--strict-mcp-config` / `--bare`) exactly as before, so `--strict-mcp-config`
21
+ * is preserved in BOTH modes.
22
+ *
23
+ * Caller sites (today): scripts/daemon/cadence-consumer.mjs#realSpawnSession.
24
+ */
25
+
26
+ import { getCadenceDef } from "../scripts/daemon/cadence-handlers.mjs";
27
+
28
+ /**
29
+ * Conservative fallback allowlist used in scoped mode when a cadence has no
30
+ * explicit `allowedTools` field in the registry. Deliberately READ-ONLY /
31
+ * search-only: an unknown or untrusted cadence should never get a write,
32
+ * shell, send, or control-plane tool by default. Tightening here is safe;
33
+ * widening should be a per-cadence opt-in via the registry `allowedTools`.
34
+ *
35
+ * Frozen so a caller can't accidentally mutate the shared default.
36
+ * @type {ReadonlyArray<string>}
37
+ */
38
+ export const CONSERVATIVE_DEFAULT_ALLOWLIST = Object.freeze([
39
+ "Read",
40
+ "Grep",
41
+ "Glob",
42
+ "LS",
43
+ ]);
44
+
45
+ /**
46
+ * Resolve the effective allowlist for a cadence in scoped mode. Prefers an
47
+ * explicit `allowedTools` array on the cadence registry entry; otherwise falls
48
+ * back to {@link CONSERVATIVE_DEFAULT_ALLOWLIST}. A registry entry whose
49
+ * `allowedTools` is absent, non-array, or empty also falls back, so a
50
+ * mis-specified entry fails CLOSED (to the conservative default) rather than
51
+ * open.
52
+ *
53
+ * @param {string} [cadence] cadence name (registry key)
54
+ * @returns {string[]} non-empty list of allowed tool names
55
+ */
56
+ function resolveAllowlist(cadence) {
57
+ const def = cadence ? getCadenceDef(cadence) : null;
58
+ const allow = def && def.allowedTools;
59
+ if (Array.isArray(allow) && allow.length > 0) {
60
+ // Drop any non-string / blank entries defensively.
61
+ const cleaned = allow.filter((t) => typeof t === "string" && t.trim());
62
+ if (cleaned.length > 0) return cleaned;
63
+ }
64
+ return [...CONSERVATIVE_DEFAULT_ALLOWLIST];
65
+ }
66
+
67
+ /**
68
+ * Build the `claude` CLI permission args for a spawned sub-session.
69
+ *
70
+ * Behaviour is gated on the `MAESTRO_SCOPED_PERMISSIONS` env var:
71
+ *
72
+ * - UNSET (or any value other than "1") — back-compat / default. Returns
73
+ * `["--dangerously-skip-permissions"]`, identical to the pre-H1 hardcoded
74
+ * literal. NOTHING changes for operators who have not opted in.
75
+ *
76
+ * - "1" — scoped mode. Returns `["--allowedTools", "<comma-list>"]`, where
77
+ * the list is the cadence's registry `allowedTools` field if present, else
78
+ * {@link CONSERVATIVE_DEFAULT_ALLOWLIST}. `--strict-mcp-config` is NOT
79
+ * added here — it stays single-sourced in `daemonClaudeArgs()` which the
80
+ * caller appends.
81
+ *
82
+ * The flag is read at call time (not import time) so an operator can flip it
83
+ * via `.env` and have a long-lived daemon pick it up on the next spawn without
84
+ * a code change.
85
+ *
86
+ * @param {Object} [opts]
87
+ * @param {string} [opts.cadence] cadence name; used to look up a per-cadence allowlist in scoped mode.
88
+ * @param {string} [opts.mode] cadence mode ("inline"|"guarded"|"escalate"); accepted for forward-compat (future per-mode tightening), currently unused.
89
+ * @returns {string[]} permission CLI args to splice into the `claude --print` argv.
90
+ */
91
+ export function sessionPermissionArgs({ cadence, mode } = {}) {
92
+ // `mode` is reserved for future per-mode tightening; reference it so linters
93
+ // don't flag it as unused while keeping the signature stable for callers.
94
+ void mode;
95
+ if (process.env.MAESTRO_SCOPED_PERMISSIONS !== "1") {
96
+ // Default / back-compat: UNCHANGED behaviour.
97
+ return ["--dangerously-skip-permissions"];
98
+ }
99
+ const allow = resolveAllowlist(cadence);
100
+ return ["--allowedTools", allow.join(",")];
101
+ }