@cohortapp/agent-sdk 2.17.0 → 2.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (526) hide show
  1. package/.claude/settings.json +18 -0
  2. package/.env.example +18 -5
  3. package/README.md +1 -0
  4. package/bin/maestro.mjs +62 -0
  5. package/docs/guides/billing-console-keys.md +60 -0
  6. package/docs/guides/front-door-session.md +54 -9
  7. package/docs/guides/mac-mini.md +20 -25
  8. package/docs/guides/setup-wizard.md +1 -1
  9. package/docs/runbooks/fleet-rollout.md +156 -0
  10. package/docs/runbooks/mac-mini-bootstrap.md +12 -14
  11. package/lib/action-executor.js +19 -3
  12. package/lib/budget-guard.mjs +279 -3
  13. package/lib/channels/base-adapter.mjs +3 -1
  14. package/lib/channels/contract.mjs +2 -1
  15. package/lib/channels/inbox-item.mjs +8 -0
  16. package/lib/claude-bin.mjs +5 -6
  17. package/lib/cli/doctor-checks.mjs +141 -10
  18. package/lib/cli/global-setup-extras.mjs +5 -1
  19. package/lib/cli/inbox.mjs +100 -15
  20. package/lib/cli/seat-auth.mjs +463 -0
  21. package/lib/cli/session.mjs +80 -12
  22. package/lib/collective/capture.mjs +8 -6
  23. package/lib/collective/global-config.mjs +63 -1
  24. package/lib/collective/presence.mjs +142 -5
  25. package/lib/comms/send-gate.mjs +559 -1
  26. package/lib/diagnostics/alerts.mjs +49 -0
  27. package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
  28. package/lib/engine/agents/definitions.mjs +343 -0
  29. package/lib/engine/agents/persist.mjs +275 -0
  30. package/lib/engine/agents/runtime.mjs +748 -0
  31. package/lib/engine/agents/usage.mjs +95 -0
  32. package/lib/engine/auth-status.mjs +139 -0
  33. package/lib/engine/budget.mjs +194 -0
  34. package/lib/engine/cli.mjs +1204 -0
  35. package/lib/engine/commands/index.mjs +269 -0
  36. package/lib/engine/context/budget.mjs +219 -0
  37. package/lib/engine/context/cache.mjs +125 -0
  38. package/lib/engine/context/child-env.mjs +215 -0
  39. package/lib/engine/context/compaction.mjs +342 -0
  40. package/lib/engine/context/images.mjs +90 -0
  41. package/lib/engine/context/instructions.mjs +327 -0
  42. package/lib/engine/context/lazy-instructions.mjs +169 -0
  43. package/lib/engine/context/manager.mjs +182 -0
  44. package/lib/engine/context/real-path.mjs +91 -0
  45. package/lib/engine/context/secret-values.mjs +163 -0
  46. package/lib/engine/context/settings.mjs +274 -0
  47. package/lib/engine/context/stream-input.mjs +159 -0
  48. package/lib/engine/guard.mjs +152 -0
  49. package/lib/engine/hooks.mjs +713 -0
  50. package/lib/engine/loop.mjs +560 -0
  51. package/lib/engine/mcp/client.mjs +254 -0
  52. package/lib/engine/mcp/config.mjs +301 -0
  53. package/lib/engine/mcp/http.mjs +201 -0
  54. package/lib/engine/mcp/index.mjs +146 -0
  55. package/lib/engine/mcp/jsonrpc.mjs +147 -0
  56. package/lib/engine/mcp/naming.mjs +66 -0
  57. package/lib/engine/mcp/resources.mjs +89 -0
  58. package/lib/engine/mcp/results.mjs +133 -0
  59. package/lib/engine/mcp/stdio.mjs +137 -0
  60. package/lib/engine/mcp/supervisor.mjs +116 -0
  61. package/lib/engine/messages.mjs +104 -0
  62. package/lib/engine/output/json.mjs +164 -0
  63. package/lib/engine/output/stream-json.mjs +266 -0
  64. package/lib/engine/permissions.mjs +845 -0
  65. package/lib/engine/process-identity.mjs +164 -0
  66. package/lib/engine/process-tree.mjs +551 -0
  67. package/lib/engine/prompt.mjs +60 -0
  68. package/lib/engine/session/store.mjs +299 -0
  69. package/lib/engine/session-runtime/args.mjs +97 -0
  70. package/lib/engine/session-runtime/host.mjs +143 -0
  71. package/lib/engine/session-runtime/inbox.mjs +122 -0
  72. package/lib/engine/session-runtime/notifications.mjs +129 -0
  73. package/lib/engine/session-runtime/registry.mjs +328 -0
  74. package/lib/engine/session-runtime/runner.mjs +344 -0
  75. package/lib/engine/session-runtime/socket.mjs +212 -0
  76. package/lib/engine/session-runtime/wakeup.mjs +115 -0
  77. package/lib/engine/skills/index.mjs +321 -0
  78. package/lib/engine/tools/bash-background.mjs +533 -0
  79. package/lib/engine/tools/bash.mjs +216 -0
  80. package/lib/engine/tools/edit.mjs +97 -0
  81. package/lib/engine/tools/glob.mjs +81 -0
  82. package/lib/engine/tools/grep.mjs +224 -0
  83. package/lib/engine/tools/index.mjs +84 -0
  84. package/lib/engine/tools/list-agents.mjs +32 -0
  85. package/lib/engine/tools/ls.mjs +127 -0
  86. package/lib/engine/tools/monitor.mjs +82 -0
  87. package/lib/engine/tools/notebook-edit.mjs +218 -0
  88. package/lib/engine/tools/read.mjs +103 -0
  89. package/lib/engine/tools/schedule-wakeup.mjs +45 -0
  90. package/lib/engine/tools/schema.mjs +144 -0
  91. package/lib/engine/tools/send-message.mjs +77 -0
  92. package/lib/engine/tools/session.mjs +70 -0
  93. package/lib/engine/tools/todo.mjs +144 -0
  94. package/lib/engine/tools/toolsearch.mjs +217 -0
  95. package/lib/engine/tools/walk.mjs +193 -0
  96. package/lib/engine/tools/web-switch.mjs +31 -0
  97. package/lib/engine/tools/webfetch-html.mjs +387 -0
  98. package/lib/engine/tools/webfetch-net.mjs +340 -0
  99. package/lib/engine/tools/webfetch.mjs +198 -0
  100. package/lib/engine/tools/websearch.mjs +91 -0
  101. package/lib/engine/tools/workflow.mjs +95 -0
  102. package/lib/engine/tools/write.mjs +76 -0
  103. package/lib/engine/tui/line-editor.mjs +137 -0
  104. package/lib/engine/tui/render.mjs +86 -0
  105. package/lib/engine/tui/tui.mjs +274 -0
  106. package/lib/engine/wire/anthropic-messages.mjs +263 -0
  107. package/lib/engine/wire/effort.mjs +36 -0
  108. package/lib/engine/wire/errors.mjs +496 -0
  109. package/lib/engine/wire/http.mjs +441 -0
  110. package/lib/engine/wire/index.mjs +76 -0
  111. package/lib/engine/wire/openai-chat.mjs +332 -0
  112. package/lib/engine/wire/prompt-cache.mjs +79 -0
  113. package/lib/engine/wire/search.mjs +140 -0
  114. package/lib/engine/wire/sse.mjs +114 -0
  115. package/lib/engine/wire/stall.mjs +349 -0
  116. package/lib/engine/wire/token-provider.mjs +175 -0
  117. package/lib/engine/wire/usage.mjs +192 -0
  118. package/lib/engine/workflow/host.mjs +524 -0
  119. package/lib/engine/workflow/journal.mjs +188 -0
  120. package/lib/engine/workflow/json-schema.mjs +171 -0
  121. package/lib/engine/workflow/meta.mjs +329 -0
  122. package/lib/engine/workflow/notifications.mjs +52 -0
  123. package/lib/engine/workflow/runtime.mjs +447 -0
  124. package/lib/engine/workflow/sandbox.mjs +534 -0
  125. package/lib/engine/workflow/worker.mjs +141 -0
  126. package/lib/engine/workflow/worktree.mjs +74 -0
  127. package/lib/execution/disposition.mjs +1 -1
  128. package/lib/execution/intake.mjs +10 -0
  129. package/lib/execution/surface-policy.mjs +15 -0
  130. package/lib/learning/curator.mjs +8 -6
  131. package/lib/learning/reflect.mjs +8 -6
  132. package/lib/model-router/catalog/cohort.yaml +137 -0
  133. package/lib/model-router/catalog.mjs +118 -1
  134. package/lib/model-router/failover.mjs +67 -16
  135. package/lib/model-router/llm-task.mjs +39 -3
  136. package/lib/model-router/resolve.mjs +89 -3
  137. package/lib/model-router/spawn.mjs +46 -47
  138. package/lib/model-router/taxonomy.mjs +126 -4
  139. package/lib/org/cost-sync.mjs +141 -11
  140. package/lib/org/inbound/broadcast.mjs +289 -0
  141. package/lib/org/inbound/collective.mjs +375 -0
  142. package/lib/org/inbound/directedness.mjs +96 -8
  143. package/lib/org/inbound/facts.mjs +78 -2
  144. package/lib/org/inbound/project.mjs +22 -0
  145. package/lib/org/inbound/surfaces.mjs +14 -0
  146. package/lib/org/llm-token.mjs +879 -0
  147. package/lib/org/mesh.mjs +61 -0
  148. package/lib/org/messaging.mjs +3 -1
  149. package/lib/org/protocol.checksum +1 -1
  150. package/lib/org/protocol.mjs +15 -0
  151. package/lib/org/quota.mjs +520 -0
  152. package/lib/org/tool-surface.mjs +104 -16
  153. package/lib/org/ui-parity.mjs +16 -1
  154. package/lib/org/work-ledger.mjs +37 -6
  155. package/lib/rate-guard.mjs +114 -1
  156. package/lib/resource-governor.mjs +41 -6
  157. package/lib/runtime/adapter.mjs +823 -0
  158. package/lib/runtime/child-env.mjs +191 -0
  159. package/lib/runtime/legacy-shell-guard.mjs +97 -0
  160. package/lib/runtime/seat-engine.mjs +162 -0
  161. package/lib/session/ask-ledger.mjs +271 -0
  162. package/lib/session/current-work.mjs +676 -0
  163. package/lib/session/feed-core.mjs +40 -3
  164. package/lib/session/launch-args.mjs +56 -4
  165. package/lib/session/status-summary.mjs +26 -9
  166. package/lib/session/upgrade-notice.mjs +42 -0
  167. package/lib/setup/claude-probe.mjs +117 -13
  168. package/lib/setup/enrich.mjs +13 -10
  169. package/lib/setup/sections/model.mjs +39 -13
  170. package/lib/telemetry/collect.mjs +208 -9
  171. package/lib/upgrade/ignored-drift.mjs +105 -0
  172. package/lib/voice/post-call-brief.mjs +30 -17
  173. package/package.json +13 -3
  174. package/plugins/maestro-skills/skills/board-work.md +5 -0
  175. package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
  176. package/plugins/maestro-skills/skills/main-session.md +18 -7
  177. package/scripts/ci/check-tarball-fidelity.mjs +126 -2
  178. package/scripts/ci/run-tests.mjs +47 -19
  179. package/scripts/cohort-llm/api-key-helper.mjs +92 -0
  180. package/scripts/collective/hook-runner.mjs +29 -2
  181. package/scripts/continuous-monitor.sh +13 -0
  182. package/scripts/cost/track-claude-usage.mjs +15 -0
  183. package/scripts/daemon/agent-daemon.mjs +408 -20
  184. package/scripts/daemon/assurance.mjs +48 -12
  185. package/scripts/daemon/cadence-consumer.mjs +218 -68
  186. package/scripts/daemon/cadence-handlers.mjs +73 -4
  187. package/scripts/daemon/classifier.mjs +75 -26
  188. package/scripts/daemon/context-compiler.mjs +51 -37
  189. package/scripts/daemon/deliver.mjs +30 -1
  190. package/scripts/daemon/dispatcher.mjs +595 -149
  191. package/scripts/daemon/health.mjs +14 -1
  192. package/scripts/daemon/maestro-daemon.mjs +11 -0
  193. package/scripts/daemon/prompt-builder.mjs +24 -0
  194. package/scripts/daemon/responder.mjs +246 -79
  195. package/scripts/daemon/sdk-version.mjs +98 -16
  196. package/scripts/eval/probe-gateway.mjs +635 -0
  197. package/scripts/eval/replay/extract.mjs +270 -0
  198. package/scripts/eval/replay/grade.mjs +260 -0
  199. package/scripts/eval/replay/lib/config.mjs +50 -0
  200. package/scripts/eval/replay/lib/effects.mjs +65 -0
  201. package/scripts/eval/replay/lib/fixture.mjs +188 -0
  202. package/scripts/eval/replay/lib/judge.mjs +72 -0
  203. package/scripts/eval/replay/lib/redact.mjs +136 -0
  204. package/scripts/eval/replay/lib/sandbox.mjs +170 -0
  205. package/scripts/eval/replay/lib/schema-check.mjs +63 -0
  206. package/scripts/eval/replay/lib/transcript.mjs +76 -0
  207. package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
  208. package/scripts/eval/replay/report.mjs +185 -0
  209. package/scripts/eval/replay/run.mjs +404 -0
  210. package/scripts/fleet/rollout.mjs +1094 -0
  211. package/scripts/hooks/pre-send-audit.sh +36 -245
  212. package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
  213. package/scripts/hooks/validate-state-yaml.sh +190 -0
  214. package/scripts/huddle/huddle-llm.mjs +361 -0
  215. package/scripts/huddle/huddle-server.mjs +46 -121
  216. package/scripts/local-triggers/autoupdate.sh +448 -78
  217. package/scripts/local-triggers/run-trigger.sh +13 -0
  218. package/scripts/maintenance/pin-integrity.mjs +364 -0
  219. package/scripts/poll-slack-events.sh +41 -9
  220. package/scripts/poller/slack-socket-mode.mjs +28 -3
  221. package/scripts/session/supervisor.mjs +80 -13
  222. package/scripts/spawn-session.sh +13 -0
  223. package/bin/maestro.test.mjs +0 -1574
  224. package/lib/action-executor.test.mjs +0 -871
  225. package/lib/archetype.test.mjs +0 -132
  226. package/lib/assurance/plan-note.test.mjs +0 -234
  227. package/lib/assurance/room-budget.test.mjs +0 -486
  228. package/lib/assurance/tier.test.mjs +0 -174
  229. package/lib/autonomy.test.mjs +0 -66
  230. package/lib/backlog.test.mjs +0 -302
  231. package/lib/backup/policy.test.mjs +0 -305
  232. package/lib/budget-escalate.test.mjs +0 -232
  233. package/lib/budget-guard.envelope.test.mjs +0 -476
  234. package/lib/budget-guard.test.mjs +0 -427
  235. package/lib/cadence-bus-requeue.test.mjs +0 -83
  236. package/lib/cadence-bus-schedule.test.mjs +0 -194
  237. package/lib/cadence-bus.test.mjs +0 -720
  238. package/lib/cadences.test.mjs +0 -230
  239. package/lib/capability/inventory.test.mjs +0 -232
  240. package/lib/capability.test.mjs +0 -78
  241. package/lib/channels/base-adapter.test.mjs +0 -590
  242. package/lib/channels/channels.test.mjs +0 -371
  243. package/lib/channels/contract.test.mjs +0 -162
  244. package/lib/channels/inbox-item.test.mjs +0 -368
  245. package/lib/channels/orgmail/adapter.test.mjs +0 -448
  246. package/lib/channels/pairing.test.mjs +0 -270
  247. package/lib/channels/repeat-suppressor.test.mjs +0 -134
  248. package/lib/channels/slack-adapter.test.mjs +0 -212
  249. package/lib/channels/telegram-adapter.test.mjs +0 -306
  250. package/lib/channels/voice/adapter.test.mjs +0 -278
  251. package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
  252. package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
  253. package/lib/charter.test.mjs +0 -89
  254. package/lib/claude-bin.test.mjs +0 -131
  255. package/lib/cli/board.test.mjs +0 -227
  256. package/lib/cli/design.test.mjs +0 -270
  257. package/lib/cli/doctor-checks.test.mjs +0 -336
  258. package/lib/cli/global-setup-extras.test.mjs +0 -462
  259. package/lib/cli/inbox.test.mjs +0 -230
  260. package/lib/cli/session-ack.test.mjs +0 -63
  261. package/lib/cli/session.test.mjs +0 -613
  262. package/lib/collective/capture.test.mjs +0 -121
  263. package/lib/collective/cards.test.mjs +0 -114
  264. package/lib/collective/config.test.mjs +0 -123
  265. package/lib/collective/global-config.test.mjs +0 -220
  266. package/lib/collective/global-skills.test.mjs +0 -126
  267. package/lib/collective/presence.test.mjs +0 -95
  268. package/lib/collective/recall.test.mjs +0 -116
  269. package/lib/collective/vendor-skills.test.mjs +0 -306
  270. package/lib/comms/send-gate.test.mjs +0 -770
  271. package/lib/comms.test.mjs +0 -41
  272. package/lib/context/budget.test.mjs +0 -252
  273. package/lib/context/history-scope.test.mjs +0 -79
  274. package/lib/cost/ledger-row.test.mjs +0 -183
  275. package/lib/design/design-md.test.mjs +0 -318
  276. package/lib/design/fixtures/DESIGN.golden.md +0 -238
  277. package/lib/design/fixtures/PRODUCT.golden.md +0 -67
  278. package/lib/design/fixtures/foundation.json +0 -133
  279. package/lib/design/refresh-gate.test.mjs +0 -144
  280. package/lib/design/write.test.mjs +0 -241
  281. package/lib/diagnostics/alerts.test.mjs +0 -318
  282. package/lib/diagnostics/backup-freshness.test.mjs +0 -185
  283. package/lib/diagnostics/counters.test.mjs +0 -206
  284. package/lib/diagnostics/events.test.mjs +0 -290
  285. package/lib/diagnostics/otel.test.mjs +0 -196
  286. package/lib/diagnostics/trace.test.mjs +0 -251
  287. package/lib/env-compat.test.mjs +0 -104
  288. package/lib/execution/disposition.test.mjs +0 -553
  289. package/lib/execution/drive.test.mjs +0 -270
  290. package/lib/execution/effects.test.mjs +0 -344
  291. package/lib/execution/intake.test.mjs +0 -389
  292. package/lib/execution/journal.test.mjs +0 -261
  293. package/lib/execution/match.test.mjs +0 -235
  294. package/lib/execution/pipeline.test.mjs +0 -392
  295. package/lib/execution/route.test.mjs +0 -186
  296. package/lib/execution/surface-policy.test.mjs +0 -162
  297. package/lib/fs-atomic.test.mjs +0 -72
  298. package/lib/fs-ownership.test.mjs +0 -158
  299. package/lib/goals/admission.test.mjs +0 -164
  300. package/lib/goals/classify.test.mjs +0 -167
  301. package/lib/goals/collaborate.test.mjs +0 -336
  302. package/lib/goals/gaps.test.mjs +0 -284
  303. package/lib/goals/loop.test.mjs +0 -845
  304. package/lib/hooks/bus.test.mjs +0 -387
  305. package/lib/identity/persona.test.mjs +0 -142
  306. package/lib/kpi-sensors.test.mjs +0 -278
  307. package/lib/kpi.test.mjs +0 -244
  308. package/lib/learning/config.test.mjs +0 -75
  309. package/lib/learning/counters.test.mjs +0 -69
  310. package/lib/learning/curator-consolidate.test.mjs +0 -238
  311. package/lib/learning/curator.test.mjs +0 -106
  312. package/lib/learning/reflect.test.mjs +0 -0
  313. package/lib/learning/session-index.test.mjs +0 -125
  314. package/lib/learning/skill-writer.test.mjs +0 -210
  315. package/lib/mandate/audit.test.mjs +0 -195
  316. package/lib/mandate/contract.test.mjs +0 -185
  317. package/lib/mandate/derive.test.mjs +0 -274
  318. package/lib/mandate/model.test.mjs +0 -164
  319. package/lib/mandate/refresh.test.mjs +0 -389
  320. package/lib/mcp/server.test.mjs +0 -426
  321. package/lib/model-router/auth-profiles.test.mjs +0 -580
  322. package/lib/model-router/catalog.test.mjs +0 -385
  323. package/lib/model-router/economics.test.mjs +0 -438
  324. package/lib/model-router/failover.test.mjs +0 -439
  325. package/lib/model-router/health.test.mjs +0 -338
  326. package/lib/model-router/integration-coverage.test.mjs +0 -831
  327. package/lib/model-router/integration.test.mjs +0 -564
  328. package/lib/model-router/ledger.test.mjs +0 -415
  329. package/lib/model-router/llm-task.test.mjs +0 -392
  330. package/lib/model-router/org-credentials.test.mjs +0 -265
  331. package/lib/model-router/pricing-refresh.test.mjs +0 -286
  332. package/lib/model-router/reconcile.test.mjs +0 -316
  333. package/lib/model-router/repair.test.mjs +0 -180
  334. package/lib/model-router/spawn.test.mjs +0 -446
  335. package/lib/model-router/taxonomy.test.mjs +0 -410
  336. package/lib/model-router.test.mjs +0 -1207
  337. package/lib/org/activity.test.mjs +0 -134
  338. package/lib/org/approvals.test.mjs +0 -216
  339. package/lib/org/awareness.test.mjs +0 -159
  340. package/lib/org/board-mine-cache.test.mjs +0 -53
  341. package/lib/org/board.test.mjs +0 -187
  342. package/lib/org/bootstrap-context.test.mjs +0 -153
  343. package/lib/org/client.test.mjs +0 -1206
  344. package/lib/org/cohort-client.test.mjs +0 -126
  345. package/lib/org/cost-sync.test.mjs +0 -153
  346. package/lib/org/doctor.test.mjs +0 -346
  347. package/lib/org/engagement-ledger.test.mjs +0 -112
  348. package/lib/org/engagement.test.mjs +0 -739
  349. package/lib/org/handoff.test.mjs +0 -269
  350. package/lib/org/inbound/directedness.test.mjs +0 -668
  351. package/lib/org/inbound/facts.test.mjs +0 -471
  352. package/lib/org/inbound/hydrate.test.mjs +0 -908
  353. package/lib/org/inbound/index.test.mjs +0 -429
  354. package/lib/org/inbound/project.test.mjs +0 -287
  355. package/lib/org/integration-tools.test.mjs +0 -160
  356. package/lib/org/keys.test.mjs +0 -92
  357. package/lib/org/knowledge.test.mjs +0 -326
  358. package/lib/org/leases.test.mjs +0 -235
  359. package/lib/org/mesh-directives.test.mjs +0 -110
  360. package/lib/org/mesh-integration.test.mjs +0 -127
  361. package/lib/org/mesh.test.mjs +0 -400
  362. package/lib/org/messaging.test.mjs +0 -471
  363. package/lib/org/param-contract.test.mjs +0 -477
  364. package/lib/org/policy.test.mjs +0 -237
  365. package/lib/org/protocol.checksum.test.mjs +0 -90
  366. package/lib/org/protocol.test.mjs +0 -323
  367. package/lib/org/push.test.mjs +0 -792
  368. package/lib/org/registry.test.mjs +0 -100
  369. package/lib/org/resource-tools.test.mjs +0 -361
  370. package/lib/org/tool-access.test.mjs +0 -144
  371. package/lib/org/tool-surface-integration.test.mjs +0 -120
  372. package/lib/org/tool-surface.test.mjs +0 -1268
  373. package/lib/org/typing.test.mjs +0 -291
  374. package/lib/org/ui-parity.test.mjs +0 -560
  375. package/lib/org/verify.test.mjs +0 -194
  376. package/lib/org/work-ledger.test.mjs +0 -273
  377. package/lib/plan/adoption-e2e.test.mjs +0 -366
  378. package/lib/plan/budget-enforcement.test.mjs +0 -400
  379. package/lib/plan/compile.test.mjs +0 -382
  380. package/lib/plan/emit.test.mjs +0 -269
  381. package/lib/plan/explain.test.mjs +0 -188
  382. package/lib/prompts/parallelism.test.mjs +0 -177
  383. package/lib/rag/rag.test.mjs +0 -505
  384. package/lib/rate-guard.test.mjs +0 -272
  385. package/lib/reactive-gate.test.mjs +0 -57
  386. package/lib/render.test.mjs +0 -68
  387. package/lib/resource-governor.test.mjs +0 -488
  388. package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
  389. package/lib/scheduling/jitter.test.mjs +0 -140
  390. package/lib/secrets/broker.test.mjs +0 -280
  391. package/lib/secrets/providers.test.mjs +0 -274
  392. package/lib/security/audit-engine.test.mjs +0 -424
  393. package/lib/security/coerce-args.test.mjs +0 -281
  394. package/lib/security/dangerous-tools.test.mjs +0 -68
  395. package/lib/security/external-content.test.mjs +0 -84
  396. package/lib/security/redact.test.mjs +0 -441
  397. package/lib/security/secret-equal.test.mjs +0 -55
  398. package/lib/session/config.test.mjs +0 -92
  399. package/lib/session/feed-core.test.mjs +0 -198
  400. package/lib/session/first-run.test.mjs +0 -121
  401. package/lib/session/frontdoor.test.mjs +0 -205
  402. package/lib/session/handoffs.test.mjs +0 -183
  403. package/lib/session/identity.test.mjs +0 -180
  404. package/lib/session/inbox-claims.test.mjs +0 -286
  405. package/lib/session/launch-args.test.mjs +0 -157
  406. package/lib/session/liveness.test.mjs +0 -100
  407. package/lib/session/status-summary.test.mjs +0 -118
  408. package/lib/session-permissions.test.mjs +0 -120
  409. package/lib/setup/claude-probe.test.mjs +0 -187
  410. package/lib/setup/completeness.test.mjs +0 -110
  411. package/lib/setup/context-pack.test.mjs +0 -89
  412. package/lib/setup/enrich.test.mjs +0 -115
  413. package/lib/setup/enroll-from-cohort.test.mjs +0 -300
  414. package/lib/setup/integration.test.mjs +0 -162
  415. package/lib/setup/io.test.mjs +0 -77
  416. package/lib/setup/runner.test.mjs +0 -132
  417. package/lib/setup/sections/identity.test.mjs +0 -234
  418. package/lib/setup/sections/inventory.test.mjs +0 -198
  419. package/lib/setup/sections/learning.test.mjs +0 -81
  420. package/lib/setup/sections/mandate.test.mjs +0 -388
  421. package/lib/setup/sections/messaging.test.mjs +0 -127
  422. package/lib/setup/sections/model.test.mjs +0 -240
  423. package/lib/setup/sections/org.test.mjs +0 -346
  424. package/lib/setup/sections/orgmail.test.mjs +0 -118
  425. package/lib/setup/sections/recovery.test.mjs +0 -98
  426. package/lib/setup/sections/subagents.test.mjs +0 -429
  427. package/lib/setup/sections/verify.test.mjs +0 -175
  428. package/lib/setup/sot.test.mjs +0 -81
  429. package/lib/setup/state.test.mjs +0 -115
  430. package/lib/singleton.test.mjs +0 -151
  431. package/lib/subagents/cli.test.mjs +0 -389
  432. package/lib/subagents/client.test.mjs +0 -309
  433. package/lib/subagents/gap.test.mjs +0 -234
  434. package/lib/subagents/lock.test.mjs +0 -248
  435. package/lib/subagents/manifest.test.mjs +0 -175
  436. package/lib/subagents/refs.test.mjs +0 -204
  437. package/lib/subagents/resolve.test.mjs +0 -422
  438. package/lib/subagents/schema.test.mjs +0 -328
  439. package/lib/telemetry/alerts.test.mjs +0 -109
  440. package/lib/telemetry/collect.test.mjs +0 -1274
  441. package/lib/tool-definitions-integration.test.mjs +0 -83
  442. package/lib/tool-definitions.test.mjs +0 -437
  443. package/lib/upgrade/global-refresh.test.mjs +0 -65
  444. package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
  445. package/lib/upgrade/post-steps.test.mjs +0 -200
  446. package/lib/upgrade/verify.test.mjs +0 -164
  447. package/lib/util/fetch-timeout.test.mjs +0 -202
  448. package/lib/util/reconnect.test.mjs +0 -369
  449. package/lib/util/unhandled.test.mjs +0 -216
  450. package/lib/voice/outbound.test.mjs +0 -69
  451. package/lib/voice/session-rotation.test.mjs +0 -114
  452. package/lib/voice/stt.test.mjs +0 -226
  453. package/lib/voice/voice.test.mjs +0 -990
  454. package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
  455. package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
  456. package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
  457. package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
  458. package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
  459. package/scripts/ci/check-skill-packs.test.mjs +0 -495
  460. package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
  461. package/scripts/ci/check.test.mjs +0 -194
  462. package/scripts/ci/conformance-org-api.test.mjs +0 -425
  463. package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
  464. package/scripts/collective/hook-runner.test.mjs +0 -173
  465. package/scripts/cost/fleet-digest.test.mjs +0 -207
  466. package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
  467. package/scripts/cost/track-claude-usage.test.mjs +0 -148
  468. package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
  469. package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
  470. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
  471. package/scripts/daemon/agent-daemon.test.mjs +0 -995
  472. package/scripts/daemon/assurance-e2e.test.mjs +0 -613
  473. package/scripts/daemon/assurance.test.mjs +0 -1791
  474. package/scripts/daemon/board-mirror.test.mjs +0 -165
  475. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
  476. package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
  477. package/scripts/daemon/cadence-consumer.test.mjs +0 -776
  478. package/scripts/daemon/cadence-handlers.test.mjs +0 -837
  479. package/scripts/daemon/classifier-identity.test.mjs +0 -137
  480. package/scripts/daemon/classifier.test.mjs +0 -266
  481. package/scripts/daemon/classify-kind.test.mjs +0 -40
  482. package/scripts/daemon/context-compiler.test.mjs +0 -406
  483. package/scripts/daemon/deliver.test.mjs +0 -564
  484. package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
  485. package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
  486. package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
  487. package/scripts/daemon/dispatcher-session-continuity.test.mjs +0 -365
  488. package/scripts/daemon/execution-ladder.test.mjs +0 -470
  489. package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
  490. package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
  491. package/scripts/daemon/inbox-deferral.test.mjs +0 -336
  492. package/scripts/daemon/inbox-wake.test.mjs +0 -199
  493. package/scripts/daemon/integration.test.mjs +0 -149
  494. package/scripts/daemon/lib/self-echo.test.mjs +0 -153
  495. package/scripts/daemon/lib/session-router.test.mjs +0 -554
  496. package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
  497. package/scripts/daemon/prompt-builder.test.mjs +0 -556
  498. package/scripts/daemon/responder-cost.test.mjs +0 -68
  499. package/scripts/daemon/responder-history.test.mjs +0 -221
  500. package/scripts/daemon/sdk-version.test.mjs +0 -31
  501. package/scripts/daemon/session-lock.test.mjs +0 -252
  502. package/scripts/daemon/session-outcomes.test.mjs +0 -533
  503. package/scripts/daemon/typing-registry.test.mjs +0 -102
  504. package/scripts/hooks/pre-send-audit.test.mjs +0 -354
  505. package/scripts/huddle/huddle-prompt.test.mjs +0 -176
  506. package/scripts/local-triggers/autoupdate.test.mjs +0 -518
  507. package/scripts/local-triggers/generate-plists.test.mjs +0 -456
  508. package/scripts/media-generation/brand-clause.test.mjs +0 -135
  509. package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
  510. package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
  511. package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
  512. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
  513. package/scripts/poller/slack-socket-mode.test.mjs +0 -805
  514. package/scripts/poller-launchd/install.test.mjs +0 -243
  515. package/scripts/restore-from-backup.test.mjs +0 -181
  516. package/scripts/session/feed.test.mjs +0 -196
  517. package/scripts/session/supervisor-sh.test.mjs +0 -218
  518. package/scripts/session/supervisor.test.mjs +0 -482
  519. package/scripts/setup/configure-macos.test.mjs +0 -306
  520. package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
  521. package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
  522. package/scripts/setup/generate-capability.test.mjs +0 -134
  523. package/scripts/setup/init-agent.test.mjs +0 -370
  524. package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
  525. package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
  526. package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
@@ -1,1268 +0,0 @@
1
- /**
2
- * tool-surface.test.mjs — the curated org tool table + shared executor.
3
- *
4
- * Hermetic: every network call rides an injected fetchImpl; the send-gate is an
5
- * injected stub; env is snapshotted/cleared so a developer's COHORT_* vars
6
- * can't leak in. Run: node --test lib/org/tool-surface.test.mjs
7
- */
8
-
9
- "use strict";
10
-
11
- import { test, before, after } from "node:test";
12
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
13
- import { tmpdir } from "node:os";
14
- import { join } from "node:path";
15
- import assert from "node:assert/strict";
16
-
17
- import {
18
- ORG_TOOLS,
19
- getOrgTools,
20
- isOrgTool,
21
- orgToolDef,
22
- OUTBOUND_METHODS,
23
- emailFamilyAvailable,
24
- artifactFamilyAvailable,
25
- deskFamilyAvailable,
26
- resolveOrgToolConfig,
27
- executeOrgTool,
28
- DEFAULT_COHORT_BASE,
29
- } from "./tool-surface.mjs";
30
- import { methodDef, PROTOCOL_VERSION, READS } from "./protocol.mjs";
31
- import { ADMIN_TOOLS, expectedAccessFor, normalizeToolAccess } from "./tool-access.mjs";
32
-
33
- // ── env hygiene ────────────────────────────────────────────────────────────
34
- const ENV_KEYS = [
35
- "COHORT_API_TOKEN", "COHORT_TOKEN", "COHORT_API_KEY", "COHORT_ORG_ID",
36
- "COHORT_BASE", "COHORT_API_URL", "COHORT_AGENT_ROOT", "AGENT_ROOT",
37
- "COHORT_AGENT_ID", "COHORT_AGENT_EMAIL",
38
- ];
39
- const saved = {};
40
- before(() => {
41
- for (const k of ENV_KEYS) { saved[k] = process.env[k]; delete process.env[k]; }
42
- });
43
- after(() => {
44
- for (const k of ENV_KEYS) {
45
- if (saved[k] === undefined) delete process.env[k];
46
- else process.env[k] = saved[k];
47
- }
48
- });
49
-
50
- // ── helpers ────────────────────────────────────────────────────────────────
51
- const CFG = { org: { cohort: { enabled: true, base: "https://org.example", orgId: "acme", token: "nlk_secret" } } };
52
-
53
- /** fetch stub: records calls, returns a res-frame body. */
54
- function fakeFetch(body = { ok: true, result: { fine: true } }, { status = 200, ok = true } = {}) {
55
- const calls = [];
56
- const fn = async (url, init) => {
57
- calls.push({ url: String(url), init: init || {} });
58
- return { ok, status, json: async () => body, headers: { get: () => undefined } };
59
- };
60
- fn.calls = calls;
61
- return fn;
62
- }
63
-
64
- // ── table shape ────────────────────────────────────────────────────────────
65
-
66
- test("table: curated 54 + 5 email + 5 artifact + 77 desk tools, snake_case names, valid access + schemas", () => {
67
- // 2026-08 mobile-parity delta: +5 mail (report_spam/react/move_mailbox/
68
- // summarise/ask) and +3 asks (files/calendar/crm) → desk 56 → 64; the
69
- // call-working-sessions delta adds the 5 calls-desk tools → 69.
70
- // The wire-contract fix adds task_assign: hq's editTaskSchema (board.updateTask)
71
- // has NO assignee field, so reassignment needs board.assignTask — curated 28 → 29.
72
- // The conversation-parity delta adds the per-message actions the human
73
- // long-press sheet ships (react/bookmark/delete/mark_unread_from) and the two
74
- // huddle verbs the share tools presuppose (start/join) — curated 29 → 35.
75
- // They are ALWAYS-ON, not desk-gated: the base messaging/channel/calling
76
- // families have been vendored since SP3.
77
- // The macro-parity delta adds org_pulse (GET ops?pulse=1 — the founder's
78
- // attention surface, previously human-only), org_huddle_end (which closes the
79
- // loop org_huddle_start opened) and memory_recall — hq built memory.recall
80
- // explicitly because "an SDK-driven seat has been reasoning off a strictly
81
- // smaller memory than the chat lane", then shipped no tool for it, so the
82
- // reverse-parity fix never reached the plane it was built for.
83
- // Curated 35 → 38.
84
- // The ENGAGEMENT delta adds the four verbs an SDK seat needs to bring somebody
85
- // in and had no binding for, despite all four sitting in the frozen protocol
86
- // table since it was written: messaging_open_group (channel.createConversation),
87
- // messaging_create_channel (channel.create), messaging_add_to_channel
88
- // (channel.addMember), and engage_colleagues — the judged path that decides
89
- // WHETHER anyone needs involving before it opens anything at all.
90
- // Curated 38 → 42, +1 for work_track → 43.
91
- // The VOICE delta adds messaging_send_voice_note — hq could synthesise a
92
- // colleague's voice note only as a reflex (answering a human's spoken note in
93
- // kind), so an agent ASKED in text for one had no verb and truthfully said it
94
- // could not. A `local` binding: it records (messaging.synthesizeVoiceNote)
95
- // then posts (plain messaging.send with the file attached). Curated 43 → 44.
96
- // The MANDATE + REGISTRY + PREFERENCE delta (+10 → 54). Three families that
97
- // were protocol-declared and curated NOWHERE, so a session could reach them
98
- // only through `org_rpc` — access:"admin", i.e. not at all below the CEO tier:
99
- // mandate (5) — hq's own chat colleague has carried the whole belt
100
- // since the spine landed, under a docblock naming SDK
101
- // parity as its reason; the SDK plane had none of it, so
102
- // the daemon knew the seat's objectives and the session it
103
- // spawned did not. adopt/retire are deliberately absent —
104
- // `mandate.write` is not in DEFAULT_AGENT_SCOPES.
105
- // subagent (3) — READS only. hq keeps the registry, maestro generates
106
- // sub-agents locally, and the two halves never met. The
107
- // writes stay with lib/subagents/client.mjs, which owns
108
- // the offline outbox and the lock file a second writer
109
- // would silently bypass.
110
- // preference (2) — hq built this family FOR this plane and said so in the
111
- // handler; no tool was ever added, so the same workspace
112
- // remembered a standing preference when hq answered and
113
- // forgot it when the seat's own daemon did.
114
- // The RESOURCE delta (+8 desk → 77, table 133 → 141). A venture's artifacts
115
- // cross from the portal and are filed as real WorkspaceFile rows, and the
116
- // OrgResource catalogue that indexes them was reachable from NO agent code
117
- // path in EITHER plane — the venture's own colleagues could not list, add,
118
- // correct, reorder or remove one of their own deliverables. Unlike every
119
- // block above it, this one is GENERATED from the vendored protocol
120
- // declaration (lib/org/resource-tools.mjs) rather than transcribed from hq's
121
- // desk, so it cannot acquire the 239-vs-69 drift the hand-copied desks
122
- // already carry; resource-tools.test.mjs is the parity that holds it there.
123
- // The FRONT-DOOR delta (+3 curated → 57, table 141 → 144; design spec
124
- // 2026-09-08 §3.5/§3.7): board_mine (the agent's own items across every
125
- // board — board.ready is unassigned-only, so "my tasks" was unreadable from
126
- // any plane), board_track (the sanctioned inbound→board seam with the
127
- // accepted/done ends of the ladder and title/why passthrough) and
128
- // session_status (the main session's liveness + peers from local state,
129
- // offline-capable).
130
- assert.equal(ORG_TOOLS.length, 144, "57 curated + 5 email + 5 artifact + 77 desk");
131
- assert.equal(ORG_TOOLS.filter((t) => t.desk).length, 77, "exactly seventy-seven desk tools");
132
- assert.equal(ORG_TOOLS.filter((t) => t.email).length, 5, "exactly five email tools");
133
- assert.equal(ORG_TOOLS.filter((t) => t.artifact).length, 5, "exactly five artifact tools");
134
- for (const t of ORG_TOOLS) {
135
- assert.match(t.name, /^[a-z0-9_]+$/, `${t.name} snake_case`);
136
- assert.ok(["read", "write", "admin"].includes(t.access), `${t.name} access`);
137
- assert.ok(t.description && t.description.length > 10, `${t.name} described`);
138
- assert.equal(t.input_schema.type, "object", `${t.name} schema object`);
139
- assert.ok(t.binding && ["rpc", "read", "local"].includes(t.binding.kind), `${t.name} binding`);
140
- if (t.binding.kind === "rpc") {
141
- assert.ok(methodDef(t.binding.method), `${t.name} binds a real protocol method (${t.binding.method})`);
142
- }
143
- // The rpc side was already pinned to the frozen table; the READ side was
144
- // not, so a curated tool could name a read path the server does not serve
145
- // and only fail at runtime, as a bare 404 the model cannot interpret.
146
- if (t.binding.kind === "read") {
147
- assert.ok(
148
- Object.prototype.hasOwnProperty.call(READS, t.binding.path),
149
- `${t.name} binds a real protocol read (${t.binding.path})`,
150
- );
151
- if (t.binding.query !== undefined) {
152
- assert.equal(typeof t.binding.query, "object", `${t.name} binding.query is an object`);
153
- for (const [k, v] of Object.entries(t.binding.query)) {
154
- assert.equal(typeof v, "string", `${t.name} binding.query.${k} is a string`);
155
- }
156
- }
157
- }
158
- }
159
- });
160
-
161
- test("OUTBOUND_METHODS is DERIVED from outbound:true tools (messaging/email/mail-desk/share-step sends)", () => {
162
- assert.deepEqual([...OUTBOUND_METHODS].sort(), ["calling.appShareAct", "email.draftSend", "email.send", "messaging.send"]);
163
- // messaging_send_voice_note is outbound-flagged but adds NOTHING to
164
- // OUTBOUND_METHODS: it is a `local` binding, and the derivation reads rpc
165
- // bindings only. Its POST leg is messaging.send, which is already in the set,
166
- // so the escape hatch stays covered too — the flag here buys the screen on
167
- // the SPOKEN words, before a billable character is synthesised.
168
- const outboundTools = ORG_TOOLS.filter((t) => t.outbound).map((t) => t.name).sort();
169
- assert.deepEqual(outboundTools, [
170
- "email_draft_send",
171
- "email_send",
172
- "messaging_send",
173
- "messaging_send_voice_note",
174
- "org_call_share_step",
175
- ]);
176
- });
177
-
178
- test("email gating: family present in the vendored protocol → tools active; override excludes", () => {
179
- // The email family landed in the vendored protocol (sync-protocol phase 1).
180
- assert.equal(emailFamilyAvailable(), true, "vendored protocol carries email.send");
181
- assert.equal(getOrgTools().length, 144);
182
- const without = getOrgTools({ emailAvailable: false });
183
- assert.equal(without.length, 139);
184
- assert.ok(!without.some((t) => t.email), "email tools excluded when family absent");
185
- });
186
-
187
- test("artifact gating: family present → tools active; override excludes (email precedent)", () => {
188
- assert.equal(artifactFamilyAvailable(), true, "vendored protocol carries artifact.act");
189
- const without = getOrgTools({ artifactAvailable: false });
190
- assert.equal(without.length, 139);
191
- assert.ok(!without.some((t) => t.artifact), "artifact tools excluded when family absent");
192
- const neither = getOrgTools({ emailAvailable: false, artifactAvailable: false, desksAvailable: false });
193
- assert.equal(neither.length, 57, "all additive families off → the 57 always-on tools");
194
- });
195
-
196
- test("isOrgTool / orgToolDef cover the full table; unknown names rejected", () => {
197
- for (const t of ORG_TOOLS) assert.equal(isOrgTool(t.name), true);
198
- assert.equal(isOrgTool("slack_send"), false);
199
- assert.equal(isOrgTool(""), false);
200
- assert.equal(orgToolDef("org_rpc").access, "admin");
201
- });
202
-
203
- // ── config resolution (§1.4) ───────────────────────────────────────────────
204
-
205
- test("resolveOrgToolConfig: config base/token honoured; default base when nothing names one", () => {
206
- const cfg = resolveOrgToolConfig({ orgConfig: CFG, agentRoot: "/tmp/none" });
207
- assert.equal(cfg.base, "https://org.example");
208
- assert.equal(cfg.token, "nlk_secret");
209
- assert.equal(cfg.orgId, "acme");
210
- const bare = resolveOrgToolConfig({ orgConfig: {}, agentRoot: "/tmp/none" });
211
- assert.equal(bare.base, DEFAULT_COHORT_BASE);
212
- assert.equal(bare.token, "");
213
- });
214
-
215
- test("resolveOrgToolConfig: COHORT_BASE env wins over config base (env-first)", () => {
216
- process.env.COHORT_BASE = "https://env.example/";
217
- try {
218
- const cfg = resolveOrgToolConfig({ orgConfig: CFG, agentRoot: "/tmp/none" });
219
- assert.equal(cfg.base, "https://env.example", "env base wins, trailing slash stripped");
220
- } finally {
221
- delete process.env.COHORT_BASE;
222
- }
223
- });
224
-
225
- // ── executor: basics ───────────────────────────────────────────────────────
226
-
227
- test("executeOrgTool: unknown tool → NOT_FOUND frame, never a throw", async () => {
228
- const frame = await executeOrgTool("nope_tool", {}, { orgConfig: CFG, agentRoot: "/tmp/none" });
229
- assert.equal(frame.ok, false);
230
- assert.equal(frame.error.code, "NOT_FOUND");
231
- });
232
-
233
- test("executeOrgTool: missing token → clear UNAUTHORIZED frame for network tools", async () => {
234
- const fetchImpl = fakeFetch();
235
- const frame = await executeOrgTool("org_directory", {}, { orgConfig: {}, agentRoot: "/tmp/none", fetchImpl });
236
- assert.equal(frame.ok, false);
237
- assert.equal(frame.error.code, "UNAUTHORIZED");
238
- assert.match(frame.error.message, /COHORT_API_TOKEN/);
239
- assert.equal(fetchImpl.calls.length, 0, "no network attempted");
240
- });
241
-
242
- test("org_describe: offline, no network, family filter works", async () => {
243
- const fetchImpl = fakeFetch();
244
- const frame = await executeOrgTool("org_describe", {}, { orgConfig: {}, agentRoot: "/tmp/none", fetchImpl });
245
- assert.equal(frame.ok, true);
246
- assert.equal(frame.result.protocolVersion, PROTOCOL_VERSION);
247
- assert.ok(frame.result.methods.length >= 200, "full method table listed");
248
- assert.ok(frame.result.reads.length >= 13, "reads listed");
249
- const fam = await executeOrgTool("org_describe", { family: "board" }, { orgConfig: {}, agentRoot: "/tmp/none" });
250
- assert.ok(fam.result.methods.every((m) => m.family === "board"));
251
- assert.deepEqual(fam.result.families, ["board"]);
252
- assert.equal(fetchImpl.calls.length, 0);
253
- });
254
-
255
- // ── executor: rpc + read bindings ──────────────────────────────────────────
256
-
257
- test("rpc binding: messaging_history POSTs /api/v1/messaging.history with Bearer + org pin", async () => {
258
- const fetchImpl = fakeFetch({ ok: true, result: { messages: [] } });
259
- const frame = await executeOrgTool("messaging_history", { channelId: "C1", limit: 5 }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
260
- assert.equal(frame.ok, true);
261
- assert.equal(fetchImpl.calls.length, 1);
262
- const { url, init } = fetchImpl.calls[0];
263
- assert.equal(url, "https://org.example/api/v1/messaging.history");
264
- assert.equal(init.method, "POST");
265
- assert.equal(init.headers.authorization, "Bearer nlk_secret");
266
- assert.equal(init.headers["x-org-id"], "acme");
267
- assert.deepEqual(JSON.parse(init.body), { channelId: "C1", limit: 5 });
268
- });
269
-
270
- test("read binding: board_ready GETs /api/v1/board.ready and normalises the bare payload", async () => {
271
- const fetchImpl = fakeFetch({ items: [{ id: "w1" }] });
272
- const frame = await executeOrgTool("board_ready", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
273
- assert.equal(frame.ok, true);
274
- assert.deepEqual(frame.result, { items: [{ id: "w1" }] });
275
- assert.equal(fetchImpl.calls[0].init.method, "GET");
276
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/board\.ready$/);
277
- });
278
-
279
- // ── macro pulse (the founder's attention surface, previously human-only) ────
280
-
281
- test("org_pulse: the binding PINS ?pulse=1 — the agent never has to know the param", async () => {
282
- const fetchImpl = fakeFetch({
283
- chainHead: { seq: 42, rowHash: "abc" },
284
- members: 9,
285
- openAlerts: 2,
286
- readyTasks: 5,
287
- pulse: {
288
- stats: { agentsRunning: 3, blocked: 1, decisionsToday: 2, needs: 2 },
289
- needs: [{ id: "esc_1", severity: "high", what: "Blocked on pricing" }],
290
- activity: [{ id: "ev_0", sentence: "Assigned a card in Growth" }],
291
- },
292
- });
293
- const frame = await executeOrgTool("org_pulse", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
294
- assert.equal(frame.ok, true);
295
- assert.equal(fetchImpl.calls[0].init.method, "GET");
296
- // Without a pinned query this GETs the bare probe and the pulse is null — the
297
- // whole capability turns on this one param riding the binding.
298
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/ops\?pulse=1$/);
299
- assert.equal(frame.result.pulse.stats.blocked, 1);
300
- assert.equal(frame.result.pulse.needs[0].severity, "high");
301
- });
302
-
303
- test("org_pulse is reachable at the READ tier — not another admin-only escape hatch", () => {
304
- const pulse = orgToolDef("org_pulse");
305
- assert.equal(pulse.access, "read", "an ordinary agent must be able to ask what needs attention");
306
- assert.equal(pulse.desk, undefined, "always-on: `ops` has been in READS since the first protocol");
307
- // The failure mode the description must pre-empt: reporting "nothing needs
308
- // attention" when the derivation actually failed.
309
- assert.match(pulse.description, /pulse: null/);
310
- assert.match(pulse.description, /do not report zero/);
311
- });
312
-
313
- test("huddle lifecycle is CLOSED: a room an agent can open is a room it can enter and shut", async () => {
314
- const names = ORG_TOOLS.filter((t) => t.name.startsWith("org_huddle_")).map((t) => t.name).sort();
315
- assert.deepEqual(names, ["org_huddle_end", "org_huddle_join", "org_huddle_start"]);
316
- for (const n of names) {
317
- const def = orgToolDef(n);
318
- assert.equal(def.desk, undefined, `${n} is always-on (the base calling family predates the desks)`);
319
- assert.equal(def.access, "write");
320
- assert.ok(def.binding.method.startsWith("calling."), `${n} rides the calling family`);
321
- }
322
- const fetchImpl = fakeFetch({ ok: true, result: { callId: "call_1", endedAt: "2026-08-12T10:00:00.000Z" } });
323
- const frame = await executeOrgTool("org_huddle_end", { callId: "call_1" }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
324
- assert.equal(frame.ok, true);
325
- assert.equal(fetchImpl.calls[0].url, "https://org.example/api/v1/calling.end");
326
- assert.deepEqual(JSON.parse(fetchImpl.calls[0].init.body), { callId: "call_1" });
327
- // CONFLICT means "already closed", not "you failed" — the description must
328
- // say so, or a model retries a call it successfully ended.
329
- assert.match(orgToolDef("org_huddle_end").description, /CONFLICT/);
330
- });
331
-
332
- test("org_directory advertises the derived presence lane (a field nobody is told about is unreachable)", () => {
333
- const dir = orgToolDef("org_directory");
334
- for (const needle of ["presence", "lane", "detail", "indisposed"]) {
335
- assert.match(dir.description, new RegExp(needle), `org_directory names ${needle}`);
336
- }
337
- // The two honest-reading rules: a human seat's null is not "offline", and the
338
- // raw client-stamped `status.ts` is not the dot.
339
- assert.match(dir.description, /presence: null/);
340
- assert.match(dir.description, /client-stamped/);
341
- });
342
-
343
- test("memory_recall: the SDK seat gets the same memory the chat lane reasons off", async () => {
344
- const recall = orgToolDef("memory_recall");
345
- assert.equal(recall.access, "read", "recall is read-only — it must not need a write tier");
346
- assert.equal(recall.binding.method, "memory.recall");
347
- // knowledge.search is a NARROWED PROJECTION of the same index; if the model is
348
- // not told that, it keeps reaching for the smaller one out of habit.
349
- assert.match(orgToolDef("knowledge_search").description, /memory_recall/);
350
- // The two contracts a model gets wrong without being told: the entity pin is
351
- // a PAIR, and an empty index is a fact, not a failure to retry.
352
- assert.match(recall.description, /TOGETHER/);
353
- assert.match(recall.description, /found:false/);
354
-
355
- const fetchImpl = fakeFetch({ ok: true, result: { found: true, items: [{ id: "m1" }] } });
356
- const frame = await executeOrgTool(
357
- "memory_recall",
358
- { query: "what did we decide about pricing", scope: "self", depth: "specific", k: 5 },
359
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl },
360
- );
361
- assert.equal(frame.ok, true);
362
- assert.equal(fetchImpl.calls[0].url, "https://org.example/api/v1/memory.recall");
363
- assert.deepEqual(JSON.parse(fetchImpl.calls[0].init.body), {
364
- query: "what did we decide about pricing", scope: "self", depth: "specific", k: 5,
365
- });
366
- // A read carries no dispatcher idempotency key.
367
- assert.equal(fetchImpl.calls[0].init.headers["x-idempotency-key"], undefined);
368
- });
369
-
370
- test("email_triage exposes every facet the server accepts (mark-unread-from, muted, done)", () => {
371
- const triage = orgToolDef("email_triage");
372
- const props = Object.keys(triage.input_schema.properties).sort();
373
- assert.deepEqual(props, [
374
- "assigneeMemberId", "category", "done", "muted", "priority",
375
- "read", "starred", "tags", "threadId", "unreadFromMessageId",
376
- ], "the schema matches email.triage's real param set — an undocumented param is an unreachable one");
377
- // The exclusivity rules are the server's; a model that does not know them
378
- // burns a turn on a BAD_REQUEST it cannot diagnose.
379
- assert.match(triage.description, /mutually exclusive/);
380
- assert.match(triage.description, /one-verb-per-call/);
381
- });
382
-
383
- test("org_events_tail: cursor + clamped limit ride the query string", async () => {
384
- const fetchImpl = fakeFetch({ events: [] });
385
- await executeOrgTool("org_events_tail", { cursor: 7, limit: 9999 }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
386
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/events\?cursor=7&limit=500$/, "limit clamped to 500");
387
- });
388
-
389
- test("approval_wait: requires approvalId; dispatches the bounded long-poll", async () => {
390
- const missing = await executeOrgTool("approval_wait", {}, { orgConfig: CFG, agentRoot: "/tmp/none" });
391
- assert.equal(missing.error.code, "BAD_REQUEST");
392
- const fetchImpl = fakeFetch({ id: "a1", status: "approved" });
393
- const frame = await executeOrgTool("approval_wait", { approvalId: "a1" }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
394
- assert.equal(frame.ok, true);
395
- assert.match(fetchImpl.calls[0].url, /approval\.wait\?id=a1&timeoutMs=55000/);
396
- });
397
-
398
- // ── executor: send-gate parity (§1.5a) ─────────────────────────────────────
399
-
400
- test("messaging_send: screened through the send-gate, idempotencyId auto-minted", async () => {
401
- const screened = [];
402
- const fetchImpl = fakeFetch({ ok: true, result: { messageId: "m1" } });
403
- const frame = await executeOrgTool(
404
- "messaging_send",
405
- { channelId: "C9", body: "hello org" },
406
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async (o) => { screened.push(o); return { allow: true }; } },
407
- );
408
- assert.equal(frame.ok, true);
409
- assert.equal(screened.length, 1, "screenOutbound ran");
410
- assert.equal(screened[0].recipient, "C9");
411
- assert.equal(screened[0].text, "hello org");
412
- const body = JSON.parse(fetchImpl.calls[0].init.body);
413
- // hq's sendMessageSchemaV1 REQUIRES `idempotencyId`. Minting `clientMsgId`
414
- // (the column name, not the wire name) 400'd every message this plane sent.
415
- assert.ok(body.idempotencyId && body.idempotencyId.length >= 8, "idempotencyId auto-minted");
416
- assert.equal(body.clientMsgId, undefined, "the column name is NOT the wire name");
417
- assert.ok(fetchImpl.calls[0].init.headers["x-idempotency-key"], "dispatcher idempotency key present");
418
- });
419
-
420
- test("messaging_send: a vetoing gate blocks BEFORE any network", async () => {
421
- const fetchImpl = fakeFetch();
422
- const frame = await executeOrgTool(
423
- "messaging_send",
424
- { channelId: "C9", body: "As an AI I love this" },
425
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async () => ({ allow: false, reason: "banned-phrase" }) },
426
- );
427
- assert.equal(frame.ok, false);
428
- assert.equal(frame.error.code, "FORBIDDEN_SCOPE");
429
- assert.match(frame.error.message, /banned-phrase/);
430
- assert.equal(fetchImpl.calls.length, 0, "blocked before dispatch");
431
- });
432
-
433
- test("a THROWING gate fails open with a counted diagnostic (adapter parity)", async () => {
434
- const counted = [];
435
- const fetchImpl = fakeFetch({ ok: true, result: {} });
436
- const frame = await executeOrgTool(
437
- "messaging_send",
438
- { channelId: "C9", body: "hi" },
439
- {
440
- orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl,
441
- screenImpl: async () => { throw new Error("gate exploded"); },
442
- bumpImpl: (name, attrs) => counted.push({ name, attrs }),
443
- },
444
- );
445
- assert.equal(frame.ok, true, "fail-open");
446
- assert.equal(counted[0].name, "channel.send_gate.fail_open");
447
- });
448
-
449
- // ── executor: escape hatches ───────────────────────────────────────────────
450
-
451
- test("org_rpc: unknown method rejected against the protocol table, NO network", async () => {
452
- const fetchImpl = fakeFetch();
453
- const frame = await executeOrgTool("org_rpc", { method: "nope.nothing" }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
454
- assert.equal(frame.ok, false);
455
- assert.equal(frame.error.code, "NOT_FOUND");
456
- assert.equal(fetchImpl.calls.length, 0, "validated before any network");
457
- });
458
-
459
- test("org_rpc: escape-hatch parity — messaging.send through org_rpc is ALSO screened", async () => {
460
- const screened = [];
461
- const fetchImpl = fakeFetch({ ok: true, result: {} });
462
- await executeOrgTool(
463
- "org_rpc",
464
- { method: "messaging.send", params: { channelId: "C2", body: "raw lane" } },
465
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async (o) => { screened.push(o); return { allow: true }; } },
466
- );
467
- assert.equal(screened.length, 1, "escape hatch screened");
468
- assert.match(screened[0].text, /raw lane/, "params blob screened");
469
- // and a blocked verdict stops the dispatch
470
- const fetch2 = fakeFetch();
471
- const blocked = await executeOrgTool(
472
- "org_rpc",
473
- { method: "email.send", params: { to: ["x@y.z"], text: "leak" } },
474
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: fetch2, screenImpl: async () => ({ allow: false, reason: "no" }) },
475
- );
476
- assert.equal(blocked.ok, false);
477
- assert.equal(fetch2.calls.length, 0);
478
- });
479
-
480
- test("org_rpc: non-outbound method dispatches unscreened with optional idempotencyKey", async () => {
481
- const screened = [];
482
- const fetchImpl = fakeFetch({ ok: true, result: { got: 1 } });
483
- const frame = await executeOrgTool(
484
- "org_rpc",
485
- { method: "member.get", params: { memberId: "m-1" }, idempotencyKey: "k-1" },
486
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async (o) => { screened.push(o); return { allow: true }; } },
487
- );
488
- assert.equal(frame.ok, true);
489
- assert.equal(screened.length, 0, "reads/non-outbound never screened");
490
- assert.match(fetchImpl.calls[0].url, /member\.get$/);
491
- });
492
-
493
- test("org_read: validated against READS; query params encoded; unknown path rejected", async () => {
494
- const fetchImpl = fakeFetch({ events: [] });
495
- const frame = await executeOrgTool("org_read", { path: "events", query: { cursor: 3, limit: 10 } }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
496
- assert.equal(frame.ok, true);
497
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/events\?cursor=3&limit=10$/);
498
- const bad = await executeOrgTool("org_read", { path: "not-a-read" }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
499
- assert.equal(bad.error.code, "NOT_FOUND");
500
- assert.equal(fetchImpl.calls.length, 1, "unknown path never dispatched");
501
- });
502
-
503
- // ── executor: email tools ──────────────────────────────────────────────────
504
-
505
- test("email_send: idempotencyId auto-minted; screened; rides email.send", async () => {
506
- const screened = [];
507
- const fetchImpl = fakeFetch({ ok: true, result: { status: "queued", messageId: "e1" } });
508
- const frame = await executeOrgTool(
509
- "email_send",
510
- { to: ["a@ext.com"], subject: "Hi", text: "Body" },
511
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async (o) => { screened.push(o); return { allow: true }; } },
512
- );
513
- assert.equal(frame.ok, true);
514
- assert.equal(screened[0].recipient, "a@ext.com");
515
- assert.match(screened[0].text, /Subject: Hi/);
516
- const body = JSON.parse(fetchImpl.calls[0].init.body);
517
- assert.ok(body.idempotencyId && body.idempotencyId.length >= 8, "idempotencyId auto-minted");
518
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/email\.send$/);
519
- });
520
-
521
- test("email tools degrade to NOT_FOUND when the family is gated off (pre-sync install)", async () => {
522
- const fetchImpl = fakeFetch();
523
- const frame = await executeOrgTool("email_inbox", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, emailAvailable: false });
524
- assert.equal(frame.ok, false);
525
- assert.equal(frame.error.code, "NOT_FOUND");
526
- assert.match(frame.error.message, /upgrade/i);
527
- assert.equal(fetchImpl.calls.length, 0);
528
- });
529
-
530
- // ── executor: artifact tools ───────────────────────────────────────────────
531
-
532
- test("artifact_create: clientMsgId auto-minted; unscreened (in-thread, not outbound); rides artifact.create", async () => {
533
- const screened = [];
534
- const envelope = { genui: "v2", artifact: { class: "ops.status", version: 1, state: "complete", source: { kind: "native" } }, actions: [], root: { type: "card", tone: "neutral", blocks: [] } };
535
- const fetchImpl = fakeFetch({ ok: true, result: { artifactId: "a1", messageId: "m1" } });
536
- const frame = await executeOrgTool(
537
- "artifact_create",
538
- { channelId: "C1", envelope },
539
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async (o) => { screened.push(o); return { allow: true }; } },
540
- );
541
- assert.equal(frame.ok, true);
542
- assert.deepEqual(frame.result, { artifactId: "a1", messageId: "m1" });
543
- assert.equal(screened.length, 0, "artifact posts are in-thread — never send-gate screened");
544
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/artifact\.create$/);
545
- const body = JSON.parse(fetchImpl.calls[0].init.body);
546
- assert.ok(body.clientMsgId && body.clientMsgId.length >= 8, "clientMsgId auto-minted");
547
- assert.deepEqual(body.envelope, envelope, "envelope posted verbatim");
548
- assert.equal(fetchImpl.calls[0].init.headers["x-idempotency-key"], undefined, "sideEffecting:false — no dispatcher key; the service dedupes on clientMsgId");
549
- });
550
-
551
- test("artifact_act: idempotencyKey auto-minted when absent, caller's key preserved; held result is a NORMAL frame", async () => {
552
- const fetchImpl = fakeFetch({ ok: true, result: { held: true, approvalId: "ap-1" } });
553
- const frame = await executeOrgTool(
554
- "artifact_act",
555
- { artifactId: "a1", action: "approve" },
556
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl },
557
- );
558
- assert.equal(frame.ok, true, "approval-held is a result, not an error");
559
- assert.equal(frame.result.held, true);
560
- assert.equal(frame.result.approvalId, "ap-1");
561
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/artifact\.act$/);
562
- const minted = JSON.parse(fetchImpl.calls[0].init.body);
563
- assert.ok(minted.idempotencyKey && minted.idempotencyKey.length >= 8, "idempotencyKey auto-minted");
564
- // A caller-supplied key (retry / post-approval re-invoke) rides through untouched.
565
- await executeOrgTool(
566
- "artifact_act",
567
- { artifactId: "a1", action: "approve", idempotencyKey: "approve-a1" },
568
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl },
569
- );
570
- assert.equal(JSON.parse(fetchImpl.calls[1].init.body).idempotencyKey, "approve-a1");
571
- });
572
-
573
- test("artifact reads ride their rpc methods; tools degrade to NOT_FOUND when the family is gated off", async () => {
574
- const fetchImpl = fakeFetch({ ok: true, result: { items: [] } });
575
- await executeOrgTool("artifact_list", { channelId: "C1", state: "complete" }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
576
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/artifact\.list$/);
577
- await executeOrgTool("artifact_catalog", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
578
- assert.match(fetchImpl.calls[1].url, /\/api\/v1\/artifact\.catalog$/);
579
- const gated = fakeFetch();
580
- const frame = await executeOrgTool("artifact_get", { artifactId: "a1" }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: gated, artifactAvailable: false });
581
- assert.equal(frame.ok, false);
582
- assert.equal(frame.error.code, "NOT_FOUND");
583
- assert.match(frame.error.message, /upgrade/i);
584
- assert.equal(gated.calls.length, 0);
585
- });
586
-
587
- // ── executor: directory + design desks (2026-07) ───────────────────────────
588
-
589
- test("directory/design desks gate on their OWN probe, independently of the other desks", () => {
590
- // One DESK_PROBES line lights a desk up on BOTH planes; a desk whose probe
591
- // method is absent from the vendored protocol must not register.
592
- assert.equal(deskFamilyAvailable("directory"), true, "vendored protocol carries directory.search");
593
- assert.equal(deskFamilyAvailable("design"), true, "vendored protocol carries branding.getFoundation");
594
- assert.equal(deskFamilyAvailable("nope"), false, "unknown desk → default-deny");
595
- assert.equal(ORG_TOOLS.filter((t) => t.desk === "directory").length, 14);
596
- assert.equal(ORG_TOOLS.filter((t) => t.desk === "design").length, 8);
597
- // Neither desk smuggles an outbound lane in: no directory/design tool sends.
598
- assert.equal(ORG_TOOLS.filter((t) => (t.desk === "directory" || t.desk === "design") && t.outbound).length, 0);
599
- });
600
-
601
- test("directory reads/writes ride their rpc methods; a side-effecting write carries a dispatcher key", async () => {
602
- const fetchImpl = fakeFetch({ ok: true, result: { people: [] } });
603
- const o = { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl };
604
- await executeOrgTool("directory_search", { q: "hartmann" }, o);
605
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/directory\.search$/);
606
- assert.deepEqual(JSON.parse(fetchImpl.calls[0].init.body), { q: "hartmann" });
607
- assert.equal(fetchImpl.calls[0].init.headers["x-idempotency-key"], undefined, "sideEffecting:false read sends no key");
608
-
609
- await executeOrgTool("directory_list_people", { relationship: "client", sort: "touch" }, o);
610
- assert.match(fetchImpl.calls[1].url, /\/api\/v1\/directory\.listPeople$/);
611
-
612
- await executeOrgTool(
613
- "directory_propose_capture",
614
- { kind: "PERSON", payload: { person: { displayName: "Dr. Lena Hartmann" } }, source: "MAIL", sourceFingerprint: "sig:abc" },
615
- o,
616
- );
617
- assert.match(fetchImpl.calls[2].url, /\/api\/v1\/directory\.proposeCapture$/);
618
- assert.ok(
619
- fetchImpl.calls[2].init.headers["x-idempotency-key"],
620
- "sideEffecting write gets a fresh dispatcher key",
621
- );
622
- assert.equal(JSON.parse(fetchImpl.calls[2].init.body).sourceFingerprint, "sig:abc");
623
- });
624
-
625
- test("design tools ride branding.*; the costed quote call posts budgetCents:0 verbatim", async () => {
626
- const fetchImpl = fakeFetch({ ok: true, result: { slots: [], costCents: 0 } });
627
- const o = { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl };
628
- await executeOrgTool("design_foundation", { historyLimit: 5 }, o);
629
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/branding\.getFoundation$/);
630
- await executeOrgTool("design_voice", {}, o);
631
- assert.match(fetchImpl.calls[1].url, /\/api\/v1\/branding\.getVoice$/);
632
- await executeOrgTool("design_generate_image", { budgetCents: 0 }, o);
633
- assert.match(fetchImpl.calls[2].url, /\/api\/v1\/branding\.generateImage$/);
634
- assert.equal(JSON.parse(fetchImpl.calls[2].init.body).budgetCents, 0, "0 is the QUOTE call, not a falsy drop");
635
- });
636
-
637
- test("design_rewrite_in_voice produces copy and is NEVER send-gate screened", async () => {
638
- // It returns a rewrite; it sends nothing. Whatever the caller does with the
639
- // result is screened at the send verb, not here.
640
- const screened = [];
641
- const fetchImpl = fakeFetch({ ok: true, result: { text: "Rewritten.", costCents: 3 } });
642
- const frame = await executeOrgTool(
643
- "design_rewrite_in_voice",
644
- { text: "we are excited to announce" },
645
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async (x) => { screened.push(x); return { allow: true }; } },
646
- );
647
- assert.equal(frame.ok, true);
648
- assert.equal(screened.length, 0);
649
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/branding\.rewriteInVoice$/);
650
- });
651
-
652
- test("directory/design tools degrade to NOT_FOUND when their desk is gated off (pre-sync install)", async () => {
653
- for (const name of ["directory_search", "design_foundation"]) {
654
- const gated = fakeFetch();
655
- const frame = await executeOrgTool(name, { q: "x" }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: gated, desksAvailable: false });
656
- assert.equal(frame.ok, false, `${name} gated`);
657
- assert.equal(frame.error.code, "NOT_FOUND");
658
- assert.match(frame.error.message, /upgrade/i);
659
- assert.equal(gated.calls.length, 0, `${name} never dispatched`);
660
- }
661
- });
662
-
663
- // ── executor: calls desk (call working sessions, 2026-08) ──────────────────
664
-
665
- test("calls desk gates on calling.appShareStart; five tools, one outbound verb", () => {
666
- assert.equal(deskFamilyAvailable("calls"), true, "vendored protocol carries calling.appShareStart");
667
- const calls = ORG_TOOLS.filter((t) => t.desk === "calls");
668
- assert.equal(calls.length, 5);
669
- // The share-step verb is the block's ONE outbound lane: its highlight notes
670
- // and typed text are read by every human on the call.
671
- assert.deepEqual(calls.filter((t) => t.outbound).map((t) => t.name), ["org_call_share_step"]);
672
- assert.deepEqual(
673
- calls.filter((t) => t.access === "read").map((t) => t.name).sort(),
674
- ["org_call_events", "org_call_visual_context"],
675
- );
676
- });
677
-
678
- test("org_call_share_start/end ride their rpc methods with a fresh dispatcher key", async () => {
679
- const fetchImpl = fakeFetch({ ok: true, result: { sessionId: "s1" } });
680
- const o = { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl };
681
- const frame = await executeOrgTool(
682
- "org_call_share_start",
683
- { callId: "call-1", surface: { kind: "doc", ref: "f-1", title: "Q3 plan" } },
684
- o,
685
- );
686
- assert.equal(frame.ok, true);
687
- assert.equal(frame.result.sessionId, "s1");
688
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/calling\.appShareStart$/);
689
- assert.ok(fetchImpl.calls[0].init.headers["x-idempotency-key"], "sideEffecting write gets a fresh dispatcher key");
690
- assert.deepEqual(JSON.parse(fetchImpl.calls[0].init.body).surface, { kind: "doc", ref: "f-1", title: "Q3 plan" });
691
-
692
- await executeOrgTool("org_call_share_end", { callId: "call-1", sessionId: "s1" }, o);
693
- assert.match(fetchImpl.calls[1].url, /\/api\/v1\/calling\.appShareEnd$/);
694
- });
695
-
696
- test("org_call_share_step: screened through the send-gate (notes + typed text), then rides calling.appShareAct", async () => {
697
- const screened = [];
698
- const fetchImpl = fakeFetch({ ok: true, result: { seq: 4 } });
699
- const steps = [
700
- { k: "scroll", anchor: "block:3" },
701
- { k: "highlight", anchor: "block:3", note: "revenue line" },
702
- { k: "type", anchor: "block:4", text: "Draft summary" },
703
- ];
704
- const frame = await executeOrgTool(
705
- "org_call_share_step",
706
- { callId: "call-1", sessionId: "s1", steps },
707
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async (x) => { screened.push(x); return { allow: true }; } },
708
- );
709
- assert.equal(frame.ok, true);
710
- assert.equal(screened.length, 1, "screenOutbound ran");
711
- assert.equal(screened[0].recipient, "call-1");
712
- assert.match(screened[0].text, /revenue line/);
713
- assert.match(screened[0].text, /Draft summary/);
714
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/calling\.appShareAct$/);
715
- assert.deepEqual(JSON.parse(fetchImpl.calls[0].init.body).steps, steps);
716
-
717
- // A vetoing gate blocks BEFORE any network — messaging_send parity.
718
- const fetch2 = fakeFetch();
719
- const blocked = await executeOrgTool(
720
- "org_call_share_step",
721
- { callId: "call-1", sessionId: "s1", steps: [{ k: "highlight", anchor: "block:1", note: "As an AI" }] },
722
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: fetch2, screenImpl: async () => ({ allow: false, reason: "banned-phrase" }) },
723
- );
724
- assert.equal(blocked.ok, false);
725
- assert.equal(blocked.error.code, "FORBIDDEN_SCOPE");
726
- assert.equal(fetch2.calls.length, 0, "blocked before dispatch");
727
- });
728
-
729
- test("calls reads ride their rpc methods without a dispatcher key", async () => {
730
- const fetchImpl = fakeFetch({ ok: true, result: { events: [] } });
731
- const o = { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl };
732
- await executeOrgTool("org_call_visual_context", { callId: "call-1", limit: 10 }, o);
733
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/calling\.getVisualContext$/);
734
- assert.equal(fetchImpl.calls[0].init.headers["x-idempotency-key"], undefined, "sideEffecting:false read sends no key");
735
- await executeOrgTool("org_call_events", { callId: "call-1" }, o);
736
- assert.match(fetchImpl.calls[1].url, /\/api\/v1\/calling\.getCallEvents$/);
737
- });
738
-
739
- test("calls tools degrade to NOT_FOUND when the desk is gated off (pre-sync install)", async () => {
740
- const gated = fakeFetch();
741
- const frame = await executeOrgTool(
742
- "org_call_share_start",
743
- { callId: "call-1", surface: { kind: "doc", ref: "f-1" } },
744
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: gated, desksAvailable: false },
745
- );
746
- assert.equal(frame.ok, false);
747
- assert.equal(frame.error.code, "NOT_FOUND");
748
- assert.match(frame.error.message, /upgrade/i);
749
- assert.equal(gated.calls.length, 0, "never dispatched");
750
- });
751
-
752
- // ── executor: org_whoami ───────────────────────────────────────────────────
753
-
754
- test("org_whoami: unenrolled → config summary with member:null + note, never an error", async () => {
755
- const frame = await executeOrgTool("org_whoami", {}, { orgConfig: {}, agentRoot: "/tmp/none" });
756
- assert.equal(frame.ok, true);
757
- assert.equal(frame.result.member, null);
758
- assert.equal(frame.result.tokenPresent, false);
759
- assert.match(frame.result.note, /setup --only org|COHORT_API_TOKEN|COHORT_ORG_ID/);
760
- });
761
-
762
- test("org_whoami: resolves the member via the org-data whoami lane; token never in the result", async () => {
763
- const fetchImpl = fakeFetch({ slug: "astra", displayName: "Astra", agentEmail: "astra@agents.example" });
764
- const frame = await executeOrgTool("org_whoami", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
765
- assert.equal(frame.ok, true);
766
- assert.equal(frame.result.member.slug, "astra");
767
- assert.equal(frame.result.tokenPresent, true);
768
- assert.equal(frame.result.protocolVersion, PROTOCOL_VERSION);
769
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/org\/whoami\?orgId=acme/);
770
- assert.ok(!JSON.stringify(frame).includes("nlk_secret"), "token never surfaces");
771
- });
772
-
773
- test("org_whoami: unresolvable member degrades to summary + hint note (no error frame)", async () => {
774
- const fetchImpl = fakeFetch({ error: { code: "NOT_FOUND", message: "ambiguous" } }, { status: 404, ok: false });
775
- const frame = await executeOrgTool("org_whoami", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
776
- assert.equal(frame.ok, true);
777
- assert.equal(frame.result.member, null);
778
- assert.match(frame.result.note, /COHORT_AGENT_ID/);
779
- });
780
-
781
- test("org_whoami: COHORT_AGENT_ID hint routes to the direct member GET", async () => {
782
- const fetchImpl = fakeFetch({ slug: "astra", displayName: "Astra" });
783
- const frame = await executeOrgTool("org_whoami", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, env: { COHORT_AGENT_ID: "astra" } });
784
- assert.equal(frame.ok, true);
785
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/org\/members\/astra\?orgId=acme/);
786
- });
787
-
788
- // ── engage_colleagues: the judged path through the tool plane ──────────────
789
-
790
- test("engage_colleagues returns an OK frame when it decides NOBODY needs involving", async () => {
791
- // "Nobody needed involving" is an answer. Returning it as an error would teach
792
- // the model to retry until it manages to interrupt somebody.
793
- const f = fakeFetch({ ok: true, result: { members: [] } });
794
- const res = await executeOrgTool(
795
- "engage_colleagues",
796
- { id: "W-1", title: "Refresh the digest", state: "in_progress", interestedParties: ["M-2"] },
797
- { orgConfig: CFG, agentRoot: "/tmp/engage-tool-test", fetchImpl: f, env: {} },
798
- );
799
- assert.equal(res.ok, true);
800
- assert.equal(res.result.engaged, false);
801
- assert.equal(res.result.reasonCode, "self_contained");
802
- assert.deepEqual(res.result.consideredNotEngaged, ["M-2"]);
803
- assert.ok(res.result.why.length, "the reasoning comes back to the model, not just the verdict");
804
- assert.ok(
805
- !f.calls.some((c) => /messaging\.send/.test(c.url)),
806
- "a self-contained decision must not put a message on the wire",
807
- );
808
- });
809
-
810
- test("engage_colleagues refuses an unidentified piece of work rather than guessing", async () => {
811
- const res = await executeOrgTool(
812
- "engage_colleagues",
813
- { title: "no id" },
814
- { orgConfig: CFG, agentRoot: "/tmp/engage-tool-test", fetchImpl: fakeFetch(), env: {} },
815
- );
816
- assert.equal(res.ok, false);
817
- assert.equal(res.error.code, "BAD_REQUEST");
818
- });
819
-
820
- test("messaging_send passes mentions straight through to the wire", async () => {
821
- const f = fakeFetch({ ok: true, result: { id: "m1" } });
822
- await executeOrgTool(
823
- "messaging_send",
824
- { channelId: "C-1", body: "Need a decision here.", mentions: ["M-2"] },
825
- { orgConfig: CFG, agentRoot: "/tmp", fetchImpl: f, env: {}, screenImpl: async () => ({ allow: true }) },
826
- );
827
- const body = JSON.parse(f.calls[0].init.body);
828
- // The model writes the obvious thing — an array of id strings — and the param
829
- // contract coerces it at the one chokepoint. Sending the string form verbatim
830
- // fails hq's zod and 400s the WHOLE message, tags and body alike.
831
- assert.deepEqual(body.mentions, [{ memberId: "M-2" }]);
832
- });
833
-
834
- // ── speaking: messaging_send_voice_note ────────────────────────────────────
835
- //
836
- // The verb that closes "I can't send audio voice notes". hq could always
837
- // synthesise an agent's voice note, but only as a REFLEX answering a human's
838
- // spoken note in kind — asked in text, an agent had nothing to call.
839
- //
840
- // What is pinned here is the composition: record, then post the recording as an
841
- // ORDINARY message attachment. There is no audio-only send path, so the post
842
- // leg carries the same ACL, audit and dedup as any typed message; and a refused
843
- // recording must never be followed by a post that implies one was made.
844
-
845
- test("messaging_send_voice_note: records, then posts the file as an ordinary attachment", async () => {
846
- let n = 0;
847
- const calls = [];
848
- const fetchImpl = async (url, init) => {
849
- calls.push({ url: String(url), body: JSON.parse(init.body) });
850
- n += 1;
851
- return {
852
- ok: true,
853
- status: 200,
854
- headers: { get: () => undefined },
855
- json: async () =>
856
- n === 1
857
- ? { ok: true, result: { fileId: "file_v1", durationMs: 4200 } }
858
- : { ok: true, result: { messageId: "m1" } },
859
- };
860
- };
861
- const frame = await executeOrgTool(
862
- "messaging_send_voice_note",
863
- { channelId: "C1", text: "Shipping Friday." },
864
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async () => ({ allow: true }) },
865
- );
866
- assert.equal(frame.ok, true);
867
- assert.match(calls[0].url, /messaging\.synthesizeVoiceNote$/);
868
- assert.equal(calls[0].body.text, "Shipping Friday.");
869
- assert.match(calls[1].url, /messaging\.send$/);
870
- assert.deepEqual(calls[1].body.attachments, [{ fileId: "file_v1" }]);
871
- // The written body defaults to the spoken words, never an empty message.
872
- assert.equal(calls[1].body.body, "Shipping Friday.");
873
- assert.ok(calls[1].body.idempotencyId, "the post is dedup-keyed like any send");
874
- assert.equal(frame.result.fileId, "file_v1");
875
- });
876
-
877
- test("messaging_send_voice_note: a refused recording is NOT followed by a post", async () => {
878
- const calls = [];
879
- const fetchImpl = async (url, init) => {
880
- calls.push(String(url));
881
- return {
882
- ok: false,
883
- status: 403,
884
- headers: { get: () => undefined },
885
- json: async () => ({
886
- ok: false,
887
- error: { code: "FORBIDDEN_SCOPE", message: "voice note not recorded (no-key): ..." },
888
- }),
889
- };
890
- };
891
- const frame = await executeOrgTool(
892
- "messaging_send_voice_note",
893
- { channelId: "C1", text: "hi" },
894
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async () => ({ allow: true }) },
895
- );
896
- assert.equal(frame.ok, false);
897
- assert.match(frame.error.message, /no-key/);
898
- assert.equal(calls.length, 1, "no post claiming a voice note that was never recorded");
899
- });
900
-
901
- test("messaging_send_voice_note: the send-gate screens the SPOKEN words before synthesis", async () => {
902
- const fetchImpl = fakeFetch();
903
- const seen = [];
904
- const frame = await executeOrgTool(
905
- "messaging_send_voice_note",
906
- { channelId: "C1", text: "the secret is hunter2", body: "see note" },
907
- {
908
- orgConfig: CFG,
909
- agentRoot: "/tmp/none",
910
- fetchImpl,
911
- screenImpl: async (args) => {
912
- seen.push(args);
913
- return args.text.includes("hunter2") ? { allow: false, reason: "credential" } : { allow: true };
914
- },
915
- },
916
- );
917
- assert.equal(frame.ok, false);
918
- assert.equal(frame.error.code, "FORBIDDEN_SCOPE");
919
- // The gate sees the PROSE — the spoken words and the written body — not a
920
- // stringified params blob. A gate judging JSON would pass sentences it never
921
- // actually read.
922
- assert.equal(seen[0].text, "the secret is hunter2\nsee note");
923
- assert.equal(seen[0].recipient, "C1");
924
- // Blocked BEFORE the network: audio is the hardest content to retract, and a
925
- // blocked note must not cost a billable synthesis either.
926
- assert.equal(fetchImpl.calls.length, 0);
927
- });
928
-
929
- test("messaging_send_voice_note: channelId and text are both required", async () => {
930
- const fetchImpl = fakeFetch();
931
- const frame = await executeOrgTool(
932
- "messaging_send_voice_note",
933
- { channelId: "C1" },
934
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async () => ({ allow: true }) },
935
- );
936
- assert.equal(frame.ok, false);
937
- assert.equal(frame.error.code, "BAD_REQUEST");
938
- assert.equal(fetchImpl.calls.length, 0);
939
- });
940
-
941
- // ── mandate / sub-agent registry / preferences (2026-08 parity delta) ───────
942
-
943
- test("mandate tools ride their mandate.* methods and POST the params verbatim", async () => {
944
- const seen = [];
945
- const fetchImpl = async (url, init) => {
946
- seen.push({ url: String(url), body: JSON.parse(init.body) });
947
- return { ok: true, status: 200, headers: { get: () => undefined }, json: async () => ({ ok: true, result: {} }) };
948
- };
949
- fetchImpl.calls = seen;
950
- const o = { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl };
951
- await executeOrgTool("mandate_read", {}, o);
952
- await executeOrgTool("mandate_objectives", { state: "active", includeRetired: false }, o);
953
- await executeOrgTool("mandate_kpi_series", { objectiveId: "ob-1", limit: 20 }, o);
954
- await executeOrgTool("mandate_propose_objective", { key: "pipeline-coverage", text: "3x cover", target: 3 }, o);
955
- await executeOrgTool(
956
- "mandate_record_kpi_sample",
957
- { objectiveId: "ob-1", value: 2.4, window: "2026-W33", evidence: { method: "crm.listDeals" } },
958
- o,
959
- );
960
- assert.deepEqual(seen.map((c) => c.url), [
961
- "https://org.example/api/v1/mandate.get",
962
- "https://org.example/api/v1/mandate.tree",
963
- "https://org.example/api/v1/mandate.series",
964
- "https://org.example/api/v1/mandate.propose",
965
- "https://org.example/api/v1/mandate.sample",
966
- ]);
967
- // Pass-through, byte for byte: hq's mandate schemas read these names, and the
968
- // evidence OBJECT is what the server's honesty gate inspects — flattening it
969
- // to a string here would silently downgrade every sample to `llm`.
970
- assert.deepEqual(seen[3].body, { key: "pipeline-coverage", text: "3x cover", target: 3 });
971
- assert.deepEqual(seen[4].body.evidence, { method: "crm.listDeals" });
972
- });
973
-
974
- test("the mandate belt stops at PROPOSE — no adopt/retire tool exists at any tier", () => {
975
- const names = new Set(ORG_TOOLS.map((t) => t.name));
976
- for (const forbidden of ["mandate_adopt", "mandate_retire"]) {
977
- assert.equal(names.has(forbidden), false, `${forbidden} must not exist`);
978
- }
979
- // The absence is structural, not incidental: no curated tool may bind a
980
- // method whose scope is missing from DEFAULT_AGENT_SCOPES, because a tool the
981
- // seat's own credential can never execute is a promise the model will make
982
- // and fail to keep. `mandate.write` is the anti-Goodhart scope.
983
- const bound = ORG_TOOLS.filter((t) => t.binding.kind === "rpc").map((t) => t.binding.method);
984
- for (const m of bound) {
985
- assert.notEqual(methodDef(m).scope, "mandate.write", `${m} needs mandate.write — not an agent's to hold`);
986
- }
987
- });
988
-
989
- test("subagent tools are READS ONLY — the writes stay with the outbox-owning client", () => {
990
- const subagentTools = ORG_TOOLS.filter(
991
- (t) => t.binding.kind === "rpc" && t.binding.method.startsWith("subagent."),
992
- );
993
- assert.deepEqual(
994
- subagentTools.map((t) => t.name).sort(),
995
- ["subagent_get", "subagent_list", "subagent_resolve"],
996
- );
997
- for (const t of subagentTools) {
998
- assert.equal(t.access, "read", `${t.name} is a read`);
999
- assert.equal(methodDef(t.binding.method).sideEffecting, false, `${t.binding.method} mutates nothing`);
1000
- }
1001
- });
1002
-
1003
- test("preference tools ship as a PAIR — a write-only memory is not parity", async () => {
1004
- const names = ORG_TOOLS.filter(
1005
- (t) => t.binding.kind === "rpc" && t.binding.method.startsWith("preference."),
1006
- );
1007
- assert.deepEqual(names.map((t) => t.name).sort(), ["preference_list", "preference_remember"]);
1008
- assert.equal(orgToolDef("preference_list").access, "read");
1009
- assert.equal(orgToolDef("preference_remember").access, "write");
1010
- // `why` is required in the SCHEMA, not just server-side: the evidence rule
1011
- // has to reach the model, or it learns the rule by BAD_REQUEST.
1012
- assert.ok(orgToolDef("preference_remember").input_schema.required.includes("why"));
1013
-
1014
- const fetchImpl = fakeFetch({ ok: true, result: { id: "pref-1" } });
1015
- const frame = await executeOrgTool(
1016
- "preference_remember",
1017
- { domain: "travel", key: "seat", value: "aisle", why: "said so in #travel" },
1018
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl },
1019
- );
1020
- assert.equal(frame.ok, true);
1021
- assert.equal(fetchImpl.calls[0].url, "https://org.example/api/v1/preference.upsert");
1022
- // sideEffecting:true at the dispatcher → a fresh header key per invocation.
1023
- assert.ok(fetchImpl.calls[0].init.headers["x-idempotency-key"], "dispatcher key attached");
1024
- });
1025
-
1026
- // ── the access mechanism, TABLE-WIDE ───────────────────────────────────────
1027
- //
1028
- // The per-delta assertion below ("the new tools reach the tiers that need
1029
- // them") lists ten names by hand, so it polices the tools whose author
1030
- // remembered to add them and nothing else — every family that ships after it is
1031
- // unguarded by default. These three assertions are the table-wide version:
1032
- // they iterate ORG_TOOLS, so a capability cannot be parked at the CEO-only tier
1033
- // or given a tier its protocol scope contradicts without one of them going red,
1034
- // whether or not anyone updates a list.
1035
-
1036
- test("ACCESS MECHANISM: `admin` is a CLOSED set of two escape hatches, not a parking space", () => {
1037
- const admins = ORG_TOOLS.filter((t) => t.access === "admin").map((t) => t.name).sort();
1038
- assert.deepEqual(
1039
- admins,
1040
- [...ADMIN_TOOLS].sort(),
1041
- "only org_rpc/org_read may be admin-tier — anything else is a CAPABILITY, and a capability " +
1042
- "at admin is CEO-only, i.e. exactly as unreachable for a leadership or default agent as " +
1043
- "the org_rpc route a curated tool exists to replace",
1044
- );
1045
- for (const name of ADMIN_TOOLS) {
1046
- const def = orgToolDef(name);
1047
- assert.ok(def, `${name} is in the table`);
1048
- assert.equal(def.access, "admin", `${name} is the escape hatch it claims to be`);
1049
- // Both hatches are `local`: they take a method/path NAME and validate it
1050
- // against the frozen table before any I/O. A desk verb can never be one.
1051
- assert.equal(def.binding.kind, "local", `${name} is a console, not a capability`);
1052
- }
1053
- });
1054
-
1055
- test("ACCESS MECHANISM: every rpc-bound tool's tier is DERIVED from its protocol scope", () => {
1056
- // hq gates on SCOPE, so the native clamp has to mirror the scope lane or it
1057
- // either withholds a tool hq would have allowed or offers one hq will refuse.
1058
- // Note the rule this is NOT: `sideEffecting ? "write" : "read"` is wrong for
1059
- // seven tools already in the table — knowledge.search, directory.ask and the
1060
- // three branding renders/exports are AUDITED READS (side-effecting, `.read`
1061
- // scope, runnable by a VIEWER seat), and artifact.create/act are the mirror
1062
- // case. Deriving from sideEffecting would demote all five audited reads out
1063
- // of the lookup set every tier gets.
1064
- let checked = 0;
1065
- for (const t of ORG_TOOLS) {
1066
- if (!t.binding || t.binding.kind !== "rpc") continue;
1067
- checked += 1;
1068
- const want = expectedAccessFor(t.binding.method);
1069
- assert.ok(want, `${t.name} binds a vendored method`);
1070
- assert.equal(
1071
- t.access,
1072
- want,
1073
- `${t.name} (${t.binding.method}, scope ${methodDef(t.binding.method).scope}) must be access:"${want}"`,
1074
- );
1075
- }
1076
- assert.ok(checked >= 120, "the whole rpc-bound belt was checked, not a sample");
1077
- });
1078
-
1079
- test("ACCESS MECHANISM: no tool can fall out of every tier — the partition is exhaustive", () => {
1080
- // Exact string equality used to bucket the table, so a missing or mis-cased
1081
- // `access` matched NO bucket and reached NO native access level, while the
1082
- // MCP plane kept publishing it. normalizeToolAccess is the tool-side twin of
1083
- // normalizeAccessLevel: it normalises casing and sends a genuinely
1084
- // unrecognised tier to the most-restricted bucket, loudly.
1085
- for (const t of ORG_TOOLS) {
1086
- assert.ok(
1087
- ["read", "write", "admin"].includes(t.access),
1088
- `${t.name} declares a known tier verbatim (the normaliser is a safety net, not a licence)`,
1089
- );
1090
- const warnings = [];
1091
- assert.equal(normalizeToolAccess(t.access, t.name, (m) => warnings.push(m)), t.access);
1092
- assert.equal(warnings.length, 0, `${t.name} normalises silently`);
1093
- }
1094
- // And the safety net itself: a typo fails CLOSED and NOISY, never invisible.
1095
- const warnings = [];
1096
- assert.equal(normalizeToolAccess("Read", "x_tool", (m) => warnings.push(m)), "read", "casing is forgiven");
1097
- assert.equal(normalizeToolAccess(undefined, "x_tool", (m) => warnings.push(m)), "admin", "a missing tier fails closed");
1098
- assert.equal(warnings.length, 1, "and says so out loud");
1099
- assert.match(warnings[0], /x_tool/);
1100
- });
1101
-
1102
- test("the new tools reach the tiers that need them — reads everywhere, no new admin surface", () => {
1103
- const added = [
1104
- "mandate_read", "mandate_objectives", "mandate_kpi_series",
1105
- "mandate_propose_objective", "mandate_record_kpi_sample",
1106
- "subagent_list", "subagent_get", "subagent_resolve",
1107
- "preference_remember", "preference_list",
1108
- ];
1109
- for (const n of added) {
1110
- const def = orgToolDef(n);
1111
- assert.ok(def, `${n} is in the table`);
1112
- // The whole point of the delta: NONE of these may land at access:"admin",
1113
- // which is the CEO-only escape-hatch tier. A capability parked there is
1114
- // exactly as unreachable as the org_rpc route it was meant to replace.
1115
- assert.notEqual(def.access, "admin", `${n} must not be admin-tier`);
1116
- assert.equal(def.outbound, undefined, `${n} sends nothing a human reads as a message`);
1117
- }
1118
- assert.equal(
1119
- added.filter((n) => orgToolDef(n).access === "read").length,
1120
- 7,
1121
- "seven reads (3 mandate, 3 subagent, 1 preference) land in EVERY tier's lookup set",
1122
- );
1123
- });
1124
-
1125
- test("the new tools FAIL CLOSED with no credential — refusal, never a silent success", async () => {
1126
- const fetchImpl = fakeFetch();
1127
- for (const n of ["mandate_read", "subagent_list", "preference_remember"]) {
1128
- const frame = await executeOrgTool(n, {}, { orgConfig: {}, agentRoot: "/tmp/none", fetchImpl });
1129
- assert.equal(frame.ok, false, `${n} refuses`);
1130
- assert.equal(frame.error.code, "UNAUTHORIZED");
1131
- assert.match(frame.error.message, /COHORT_API_TOKEN/);
1132
- }
1133
- assert.equal(fetchImpl.calls.length, 0, "nothing dispatched without a credential");
1134
- });
1135
-
1136
- test("a 401/403 from hq surfaces VERBATIM on the new tools (a refusal is an answer)", async () => {
1137
- const denied = fakeFetch(
1138
- { ok: false, error: { code: "FORBIDDEN_SCOPE", message: "resolving another member's sub-agents requires admin" } },
1139
- { ok: false, status: 403 },
1140
- );
1141
- const frame = await executeOrgTool(
1142
- "subagent_resolve",
1143
- { memberId: "someone-else" },
1144
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: denied },
1145
- );
1146
- assert.equal(frame.ok, false);
1147
- assert.equal(frame.error.code, "FORBIDDEN_SCOPE");
1148
- assert.match(frame.error.message, /admin/);
1149
-
1150
- const unauth = fakeFetch({ ok: false, error: { code: "UNAUTHORIZED", message: "bad key" } }, { ok: false, status: 401 });
1151
- const f2 = await executeOrgTool("mandate_read", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: unauth });
1152
- assert.equal(f2.ok, false);
1153
- assert.equal(f2.error.code, "UNAUTHORIZED");
1154
- });
1155
-
1156
- test("transport failure on a new tool fails open into an INTERNAL frame, never a throw", async () => {
1157
- const thrower = async () => { throw new Error("econnrefused"); };
1158
- const frame = await executeOrgTool(
1159
- "mandate_objectives",
1160
- {},
1161
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: thrower },
1162
- );
1163
- assert.equal(frame.ok, false);
1164
- assert.equal(frame.error.code, "INTERNAL");
1165
- });
1166
-
1167
- // ── WP-M4: front-door tools (board_mine / board_track / session_status) ────
1168
-
1169
- test("board_mine: read access, GET /v1/board.mine, items normalised, [] when hq lacks the read (fail-open)", async () => {
1170
- const def = orgToolDef("board_mine");
1171
- assert.ok(def, "board_mine curated");
1172
- assert.equal(def.access, "read");
1173
- assert.equal(def.binding.kind, "local", "local so an hq without board.mine yet degrades to [] instead of a bare 404");
1174
- const fetchImpl = fakeFetch({ items: [{ itemId: "i1", title: "Deck", col: "doing" }] });
1175
- const frame = await executeOrgTool("board_mine", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
1176
- assert.equal(frame.ok, true);
1177
- assert.deepEqual(frame.result.items, [{ itemId: "i1", title: "Deck", col: "doing" }]);
1178
- assert.equal(frame.result.count, 1);
1179
- assert.match(fetchImpl.calls[0].url, /\/v1\/board\.mine$/);
1180
- assert.equal(fetchImpl.calls[0].init.method, "GET");
1181
- const gone = fakeFetch({ error: { code: "NOT_FOUND", message: "no such read" } }, { status: 404, ok: false });
1182
- const f2 = await executeOrgTool("board_mine", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: gone });
1183
- assert.equal(f2.ok, true, "fail-open: an older hq is an empty list, not an error the model must interpret");
1184
- assert.deepEqual(f2.result.items, []);
1185
- // No token → UNAUTHORIZED like every other network tool.
1186
- const noTok = await executeOrgTool("board_mine", {}, { orgConfig: { org: { cohort: { enabled: true, base: "https://org.example" } } }, agentRoot: "/tmp/none", fetchImpl });
1187
- assert.equal(noTok.ok, false);
1188
- assert.equal(noTok.error.code, "UNAUTHORIZED");
1189
- });
1190
-
1191
- test("board_track: write access on board.track; title/why/stage pass through; whole ladder incl. accepted|done; bad stage rejected offline", async () => {
1192
- const def = orgToolDef("board_track");
1193
- assert.ok(def, "board_track curated");
1194
- assert.equal(def.access, "write");
1195
- assert.equal(def.binding.method, "board.track");
1196
- assert.deepEqual(def.input_schema.properties.stage.enum, ["accepted", "working", "blocked", "review", "done", "failed"]);
1197
- assert.ok(def.input_schema.properties.title, "title passthrough");
1198
- assert.ok(def.input_schema.properties.why, "why passthrough");
1199
- const fetchImpl = fakeFetch({ ok: true, result: { tracked: true, taskId: "t1", col: "todo", created: true } });
1200
- const frame = await executeOrgTool(
1201
- "board_track",
1202
- { channelId: "c1", messageId: "m1", stage: "accepted", title: "Build the deck", why: "asked in DM", note: "on it" },
1203
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl },
1204
- );
1205
- assert.equal(frame.ok, true);
1206
- assert.equal(frame.result.taskId, "t1");
1207
- assert.match(fetchImpl.calls[0].url, /\/v1\/board\.track$/);
1208
- const body = JSON.parse(fetchImpl.calls[0].init.body);
1209
- assert.equal(body.stage, "accepted");
1210
- assert.equal(body.title, "Build the deck");
1211
- assert.equal(body.why, "asked in DM");
1212
- assert.equal(body.service, "cohort", "service defaults to cohort");
1213
- assert.equal(body.channelId, "c1");
1214
- assert.equal(body.messageId, "m1");
1215
- assert.equal(fetchImpl.calls[0].init.headers["x-idempotency-key"], "board.track:c1:m1:accepted", "stable per (ask, stage)");
1216
- const bad = fakeFetch();
1217
- const rej = await executeOrgTool("board_track", { channelId: "c1", messageId: "m1", stage: "later" }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: bad });
1218
- assert.equal(rej.ok, false);
1219
- assert.equal(rej.error.code, "BAD_REQUEST");
1220
- assert.equal(bad.calls.length, 0, "validated before any network");
1221
- });
1222
-
1223
- test("session_status: offline local read of state/session + state/org/board-mine; works with no token", async () => {
1224
- const def = orgToolDef("session_status");
1225
- assert.ok(def, "session_status curated");
1226
- assert.equal(def.access, "read");
1227
- assert.equal(def.binding.kind, "local");
1228
- const root = mkdtempSync(join(tmpdir(), "ts-session-"));
1229
- try {
1230
- mkdirSync(join(root, "state", "session", "handoffs"), { recursive: true });
1231
- mkdirSync(join(root, "state", "org"), { recursive: true });
1232
- writeFileSync(join(root, "state", "session", "heartbeat.json"), JSON.stringify({ pid: 1, name: "alex-main", sessionId: "s1", ts: Date.now() }));
1233
- writeFileSync(join(root, "state", "session", "peers.json"), JSON.stringify([{ name: "alex-deck", purpose: "board deck" }]));
1234
- writeFileSync(join(root, "state", "session", "handoffs", "t1.json"), "{}");
1235
- writeFileSync(join(root, "state", "org", "board-mine.json"), JSON.stringify({ items: [{ itemId: "i1" }, { itemId: "i2" }], fetchedAt: new Date().toISOString() }));
1236
- const fetchImpl = fakeFetch();
1237
- const frame = await executeOrgTool("session_status", {}, { orgConfig: { org: { cohort: { enabled: true, base: "https://org.example" } } }, agentRoot: root, fetchImpl });
1238
- assert.equal(frame.ok, true, "no token is fine — this never touches the network");
1239
- assert.equal(fetchImpl.calls.length, 0);
1240
- assert.equal(frame.result.live, true);
1241
- assert.equal(frame.result.name, "alex-main");
1242
- assert.equal(frame.result.peers[0].name, "alex-deck");
1243
- assert.equal(frame.result.handoffsOpen, 1);
1244
- assert.equal(frame.result.boardMineCount, 2);
1245
- assert.equal(typeof frame.result.summary, "string");
1246
- // An empty root is "absent", not an error.
1247
- const empty = await executeOrgTool("session_status", {}, { orgConfig: CFG, agentRoot: join(root, "nope"), fetchImpl });
1248
- assert.equal(empty.ok, true);
1249
- assert.equal(empty.result.state, "absent");
1250
- } finally { rmSync(root, { recursive: true, force: true }); }
1251
- });
1252
-
1253
- test("board_track: the model cannot override service or smuggle extra keys — only the schema's passthrough fields reach hq", async () => {
1254
- const fetchImpl = fakeFetch({ ok: true, result: { tracked: true, taskId: "t2", col: "doing" } });
1255
- const frame = await executeOrgTool(
1256
- "board_track",
1257
- { channelId: "c1", messageId: "m1", stage: "working", service: "slack", junk: "x", note: "halfway", priority: "P1", notify: ["u1"] },
1258
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl },
1259
- );
1260
- assert.equal(frame.ok, true);
1261
- const body = JSON.parse(fetchImpl.calls[0].init.body);
1262
- assert.equal(body.service, "cohort", "service is pinned, not model-supplied");
1263
- assert.equal(body.junk, undefined, "unknown keys are dropped");
1264
- assert.equal(body.note, "halfway");
1265
- assert.equal(body.priority, "P1");
1266
- assert.deepEqual(body.notify, ["u1"]);
1267
- assert.deepEqual(Object.keys(body).sort(), ["channelId", "messageId", "note", "notify", "priority", "service", "stage"]);
1268
- });