@vellumai/assistant 0.9.1-staging.1 → 0.10.0-staging.2

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 (433) hide show
  1. package/docs/activation-funnel-telemetry.md +24 -18
  2. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +97 -0
  3. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
  4. package/node_modules/@vellumai/gateway-client/src/index.ts +15 -0
  5. package/openapi.yaml +852 -15
  6. package/package.json +1 -1
  7. package/src/__tests__/access-request-card-view.test.ts +98 -0
  8. package/src/__tests__/access-request-seed-content-blocks.test.ts +2 -4
  9. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +59 -7
  10. package/src/__tests__/agent-loop-compaction-strip.test.ts +17 -16
  11. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +16 -13
  12. package/src/__tests__/app-compiler.test.ts +15 -1
  13. package/src/__tests__/auth-fallback-events-store.test.ts +6 -14
  14. package/src/__tests__/call-pointer-messages.test.ts +28 -0
  15. package/src/__tests__/cancel-clears-processing.test.ts +89 -0
  16. package/src/__tests__/channel-approval-routes.test.ts +0 -4
  17. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -15
  18. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +3 -4
  19. package/src/__tests__/compactor-image-manifest-trust.test.ts +21 -1
  20. package/src/__tests__/compactor-summary-call-truncation.test.ts +223 -0
  21. package/src/__tests__/config-loader-backfill.test.ts +174 -30
  22. package/src/__tests__/config-schema.test.ts +35 -0
  23. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -2
  24. package/src/__tests__/contact-store-user-file.test.ts +0 -6
  25. package/src/__tests__/contacts-tools.test.ts +29 -0
  26. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  27. package/src/__tests__/conversation-agent-loop.test.ts +58 -0
  28. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  29. package/src/__tests__/conversation-lifecycle.test.ts +7 -9
  30. package/src/__tests__/conversation-load-history-repair.test.ts +101 -0
  31. package/src/__tests__/conversation-routes-guardian-reply.test.ts +15 -12
  32. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -3
  33. package/src/__tests__/conversation-title-service.test.ts +62 -0
  34. package/src/__tests__/credential-execution-shell-lockdown.test.ts +18 -11
  35. package/src/__tests__/credential-prompt-route.test.ts +1 -0
  36. package/src/__tests__/credential-security-invariants.test.ts +2 -0
  37. package/src/__tests__/disk-pressure-policy.test.ts +12 -0
  38. package/src/__tests__/disk-usage.test.ts +65 -0
  39. package/src/__tests__/dynamic-page-surface.test.ts +51 -0
  40. package/src/__tests__/gateway-flag-listener.test.ts +110 -1
  41. package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
  42. package/src/__tests__/guardian-card-withdrawal.test.ts +403 -0
  43. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  44. package/src/__tests__/guardian-grant-minting.test.ts +3 -35
  45. package/src/__tests__/guardian-routing-invariants.test.ts +64 -26
  46. package/src/__tests__/guardian-routing-state.test.ts +0 -1
  47. package/src/__tests__/headless-browser-mode.test.ts +10 -0
  48. package/src/__tests__/headless-browser-navigate.test.ts +8 -3
  49. package/src/__tests__/helpers/create-guardian-binding.ts +0 -1
  50. package/src/__tests__/host-browser-proxy.test.ts +87 -0
  51. package/src/__tests__/injector-v3-suppression.test.ts +27 -20
  52. package/src/__tests__/internal-telemetry-routes.test.ts +6 -14
  53. package/src/__tests__/invite-redemption-service.test.ts +0 -3
  54. package/src/__tests__/llm-catalog-parity.test.ts +30 -1
  55. package/src/__tests__/llm-resolver.test.ts +21 -0
  56. package/src/__tests__/llm-schema.test.ts +1 -0
  57. package/src/__tests__/managed-profile-guard.test.ts +163 -4
  58. package/src/__tests__/mcp-health-check.test.ts +6 -7
  59. package/src/__tests__/media-stream-server-integration.test.ts +317 -13
  60. package/src/__tests__/path-policy.test.ts +34 -0
  61. package/src/__tests__/persona-resolver.test.ts +38 -0
  62. package/src/__tests__/plugin-api-provider.test.ts +24 -0
  63. package/src/__tests__/plugin-tool-contribution.test.ts +6 -3
  64. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +214 -0
  65. package/src/__tests__/provider-send-message-override-profile.test.ts +76 -0
  66. package/src/__tests__/reaction-persistence.test.ts +150 -29
  67. package/src/__tests__/relay-server.test.ts +285 -0
  68. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  69. package/src/__tests__/scheduler-reuse-conversation.test.ts +8 -5
  70. package/src/__tests__/skill-execute-input.test.ts +5 -0
  71. package/src/__tests__/skills.test.ts +51 -0
  72. package/src/__tests__/slack-notification-approval-card.test.ts +176 -0
  73. package/src/__tests__/slack-reaction-canonical-approval.test.ts +285 -0
  74. package/src/__tests__/subagent-tools.test.ts +150 -0
  75. package/src/__tests__/task-progress-nudge-hook.test.ts +1 -1
  76. package/src/__tests__/title-generate-hook.test.ts +100 -3
  77. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +1 -1
  78. package/src/__tests__/tool-audit-listener.test.ts +7 -7
  79. package/src/__tests__/tool-executor-lifecycle-events.test.ts +6 -3
  80. package/src/__tests__/trusted-contact-approval-notifier.test.ts +4 -2
  81. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +220 -3
  82. package/src/__tests__/trusted-contact-verification.test.ts +2 -4
  83. package/src/__tests__/twilio-routes.test.ts +81 -1
  84. package/src/__tests__/voice-invite-redemption.test.ts +0 -1
  85. package/src/__tests__/weak-open-model.test.ts +30 -0
  86. package/src/__tests__/workspace-migration-105-enable-memory-v3-live-for-new-workspaces.test.ts +149 -0
  87. package/src/__tests__/workspace-migration-108-drop-balanced-economy-profile.test.ts +285 -0
  88. package/src/__tests__/workspace-migration-add-send-diagnostics.test.ts +1 -1
  89. package/src/__tests__/workspace-migration-drop-collect-usage-data.test.ts +118 -0
  90. package/src/__tests__/workspace-migration-drop-send-diagnostics.test.ts +118 -0
  91. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +0 -4
  92. package/src/agent/loop.ts +33 -33
  93. package/src/api/events/tool-result.ts +6 -0
  94. package/src/api/events/workflow-completed.ts +53 -0
  95. package/src/api/events/workflow-leaf-finished.ts +38 -0
  96. package/src/api/events/workflow-leaf-started.ts +35 -0
  97. package/src/api/events/workflow-progress.ts +32 -0
  98. package/src/api/events/workflow-started.ts +31 -0
  99. package/src/api/index.ts +40 -0
  100. package/src/api/responses/conversation-message.ts +26 -0
  101. package/src/api/responses/home.ts +26 -0
  102. package/src/api/responses/workflow-journal.ts +53 -0
  103. package/src/approvals/guardian-card-withdrawal.ts +145 -0
  104. package/src/approvals/guardian-decision-primitive.ts +26 -3
  105. package/src/approvals/guardian-request-resolvers.ts +181 -78
  106. package/src/calls/__tests__/channel-admission-reader.test.ts +132 -0
  107. package/src/calls/__tests__/relay-setup-router.test.ts +350 -0
  108. package/src/calls/call-pointer-messages.ts +10 -4
  109. package/src/calls/channel-admission-reader.ts +104 -0
  110. package/src/calls/guardian-dispatch.ts +17 -45
  111. package/src/calls/media-stream-server.ts +84 -2
  112. package/src/calls/relay-server.ts +66 -0
  113. package/src/calls/relay-setup-router.ts +82 -1
  114. package/src/calls/twilio-routes.ts +17 -8
  115. package/src/cli/commands/clients.ts +3 -0
  116. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2-compare-render.test.ts +1 -1
  117. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2.test.ts +8 -7
  118. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v3.test.ts +5 -4
  119. package/src/cli/commands/memory/index.ts +30 -0
  120. package/src/cli/commands/{memory-v2-compare-render.ts → memory/memory-v2-compare-render.ts} +1 -1
  121. package/src/cli/commands/{memory-v2.ts → memory/memory-v2.ts} +6 -15
  122. package/src/cli/commands/{memory-v3.ts → memory/memory-v3.ts} +97 -11
  123. package/src/cli/commands/oauth/status.test.ts +36 -0
  124. package/src/cli/commands/oauth/status.ts +23 -3
  125. package/src/cli/commands/plugins.ts +57 -5
  126. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  127. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +134 -4
  128. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +111 -0
  129. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +53 -11
  130. package/src/cli/lib/inspect-plugin.ts +12 -1
  131. package/src/cli/lib/merge-plugin-tree.ts +149 -49
  132. package/src/cli/lib/plugin-surfaces.ts +104 -0
  133. package/src/cli/lib/upgrade-plugin.ts +64 -36
  134. package/src/cli/program.ts +2 -4
  135. package/src/config/__tests__/sync-gated-profiles.test.ts +368 -0
  136. package/src/config/assistant-feature-flags.ts +22 -7
  137. package/src/config/bundled-skills/contacts/tools/contact-search.ts +0 -1
  138. package/src/config/bundled-skills/messaging/SKILL.md +6 -4
  139. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +9 -8
  140. package/src/config/bundled-skills/workflows/SKILL.md +14 -7
  141. package/src/config/call-site-defaults.ts +3 -0
  142. package/src/config/feature-flag-registry.json +49 -18
  143. package/src/config/llm-resolver.ts +3 -0
  144. package/src/config/memory-v3-gate.ts +11 -0
  145. package/src/config/schema.ts +8 -6
  146. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  147. package/src/config/schemas/call-site-catalog.ts +7 -0
  148. package/src/config/schemas/channels.ts +11 -0
  149. package/src/config/schemas/llm.ts +31 -0
  150. package/src/config/schemas/memory-lifecycle.ts +3 -7
  151. package/src/config/schemas/memory-v3.ts +6 -0
  152. package/src/config/schemas/services.ts +18 -0
  153. package/src/config/seed-inference-profiles.ts +94 -34
  154. package/src/config/skills.ts +21 -0
  155. package/src/config/sync-gated-profiles.ts +220 -0
  156. package/src/contacts/contact-store.ts +2 -10
  157. package/src/contacts/contacts-write.ts +1 -2
  158. package/src/contacts/types.ts +0 -1
  159. package/src/context/compactor.ts +86 -52
  160. package/src/context/strip-injections.ts +58 -10
  161. package/src/context/token-estimator.ts +1 -1
  162. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -2
  163. package/src/daemon/conversation-agent-loop-handlers.ts +2 -0
  164. package/src/daemon/conversation-agent-loop.ts +100 -19
  165. package/src/daemon/conversation-history.ts +1 -1
  166. package/src/daemon/conversation-lifecycle.ts +3 -5
  167. package/src/daemon/conversation-process.ts +13 -5
  168. package/src/daemon/conversation-runtime-assembly.ts +13 -15
  169. package/src/daemon/conversation-surfaces.ts +26 -0
  170. package/src/daemon/conversation-tool-setup.ts +16 -11
  171. package/src/daemon/conversation.ts +64 -14
  172. package/src/daemon/disk-pressure-policy.ts +5 -3
  173. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -1
  174. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -1
  175. package/src/daemon/handlers/config-a2a.ts +0 -2
  176. package/src/daemon/handlers/config-channels.ts +5 -10
  177. package/src/daemon/handlers/config-slack-channel.ts +20 -0
  178. package/src/daemon/handlers/conversations.ts +107 -0
  179. package/src/daemon/host-browser-proxy.ts +41 -0
  180. package/src/daemon/lifecycle.ts +55 -20
  181. package/src/daemon/message-provenance.ts +2 -0
  182. package/src/daemon/message-types/contacts.ts +0 -1
  183. package/src/daemon/message-types/web-activity.ts +7 -1
  184. package/src/daemon/message-types/workflows.ts +83 -1
  185. package/src/daemon/tool-setup-types.ts +4 -0
  186. package/src/daemon/trust-context.ts +1 -1
  187. package/src/events/tool-audit-listener.ts +2 -2
  188. package/src/home/feed-source-enrichment.test.ts +151 -0
  189. package/src/home/feed-source-enrichment.ts +176 -0
  190. package/src/instrument.ts +18 -6
  191. package/src/ipc/__tests__/binary-result-ipc.test.ts +81 -0
  192. package/src/ipc/__tests__/clients-list-ipc.test.ts +20 -0
  193. package/src/ipc/assistant-server.ts +37 -4
  194. package/src/ipc/gateway-flag-listener.ts +18 -2
  195. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +5 -16
  196. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +7 -11
  197. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +37 -7
  198. package/src/memory/__tests__/memory-retrospective-job.test.ts +34 -0
  199. package/src/memory/__tests__/onboarding-events-store.test.ts +7 -7
  200. package/src/memory/auth-fallback-events-store.ts +2 -2
  201. package/src/memory/auto-analysis-enqueue.ts +3 -5
  202. package/src/memory/canonical-guardian-store.ts +39 -1
  203. package/src/memory/conversation-crud.ts +9 -4
  204. package/src/memory/conversation-key-store.ts +17 -2
  205. package/src/memory/conversation-title-service.ts +64 -7
  206. package/src/memory/db-init.ts +10 -0
  207. package/src/memory/embedding-backend.ts +15 -1
  208. package/src/memory/jobs-worker.ts +2 -1
  209. package/src/memory/lifecycle-events-store.ts +2 -2
  210. package/src/memory/memory-retrospective-enqueue.ts +31 -6
  211. package/src/memory/memory-retrospective-job.ts +9 -0
  212. package/src/memory/migrations/129-contact-channels-access-fields.ts +18 -9
  213. package/src/memory/migrations/131-drop-legacy-member-guardian-tables.ts +14 -2
  214. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +10 -0
  215. package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +10 -0
  216. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.test.ts +67 -0
  217. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.ts +25 -0
  218. package/src/memory/migrations/293-workflow-journal-leaf-tokens.ts +32 -0
  219. package/src/memory/migrations/294-drop-external-user-id.ts +31 -0
  220. package/src/memory/migrations/295-drop-approval-prompt-ts-tracker.ts +20 -0
  221. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.test.ts +110 -0
  222. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.ts +68 -0
  223. package/src/memory/migrations/__tests__/131-drop-legacy-member-guardian-tables.test.ts +154 -0
  224. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +31 -0
  225. package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +30 -0
  226. package/src/memory/migrations/index.ts +5 -0
  227. package/src/memory/onboarding-events-store.ts +3 -3
  228. package/src/memory/schema/contacts.ts +0 -1
  229. package/src/memory/skill-loaded-events-store.test.ts +7 -15
  230. package/src/memory/skill-loaded-events-store.ts +2 -2
  231. package/src/memory/tool-executed-events-store.test.ts +7 -7
  232. package/src/memory/turn-trace-store.test.ts +736 -0
  233. package/src/memory/turn-trace-store.ts +364 -0
  234. package/src/memory/v2/__tests__/consolidation-job.test.ts +8 -0
  235. package/src/memory/v2/__tests__/skill-content.test.ts +30 -0
  236. package/src/memory/v2/consolidation-job.ts +2 -2
  237. package/src/memory/v2/skill-content.ts +25 -7
  238. package/src/memory/v2/skill-store.ts +7 -1
  239. package/src/memory/v3-eval/__tests__/eval-packets.test.ts +248 -0
  240. package/src/memory/v3-eval/eval-packets.ts +546 -0
  241. package/src/messaging/providers/slack/api.ts +31 -0
  242. package/src/messaging/providers/slack/send.test.ts +114 -2
  243. package/src/messaging/providers/slack/send.ts +30 -7
  244. package/src/messaging/providers/slack/withdraw.test.ts +200 -0
  245. package/src/messaging/providers/slack/withdraw.ts +161 -0
  246. package/src/notifications/AGENTS.md +2 -0
  247. package/src/notifications/access-request-copy.ts +72 -59
  248. package/src/notifications/adapters/slack.ts +55 -73
  249. package/src/notifications/approval-card-data.ts +333 -0
  250. package/src/notifications/broadcaster.ts +6 -2
  251. package/src/notifications/canonical-delivery-recorder.ts +139 -0
  252. package/src/notifications/copy-composer.ts +3 -3
  253. package/src/notifications/decision-engine.ts +4 -2
  254. package/src/notifications/destination-resolver.ts +4 -6
  255. package/src/notifications/guardian-question-mode.ts +10 -0
  256. package/src/notifications/home-feed-side-effect.ts +3 -13
  257. package/src/notifications/notification-utils.ts +2 -1
  258. package/src/notifications/signal.ts +79 -43
  259. package/src/notifications/types.ts +98 -128
  260. package/src/permissions/checker.test.ts +51 -0
  261. package/src/permissions/checker.ts +185 -26
  262. package/src/permissions/ipc-risk-types.ts +24 -0
  263. package/src/permissions/question-prompter.test.ts +27 -0
  264. package/src/permissions/question-prompter.ts +4 -0
  265. package/src/platform/client.test.ts +119 -0
  266. package/src/platform/client.ts +66 -0
  267. package/src/platform/consent-cache.test.ts +267 -0
  268. package/src/platform/consent-cache.ts +174 -0
  269. package/src/plugin-api/index.ts +27 -0
  270. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +56 -0
  271. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +43 -0
  272. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +137 -0
  273. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +153 -0
  274. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +138 -0
  275. package/src/plugins/defaults/advisor/__tests__/transcript.test.ts +147 -0
  276. package/src/plugins/defaults/advisor/advisor-gate.ts +29 -0
  277. package/src/plugins/defaults/advisor/advisor-state-store.ts +94 -0
  278. package/src/plugins/defaults/advisor/config.ts +21 -0
  279. package/src/plugins/defaults/advisor/consult.ts +93 -0
  280. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +34 -0
  281. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +30 -0
  282. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +19 -0
  283. package/src/plugins/defaults/advisor/package.json +14 -0
  284. package/src/plugins/defaults/advisor/steering.ts +67 -0
  285. package/src/plugins/defaults/advisor/tools/advisor.ts +65 -0
  286. package/src/plugins/defaults/advisor/transcript.ts +76 -0
  287. package/src/plugins/defaults/index.ts +35 -0
  288. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +22 -9
  289. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
  290. package/src/plugins/defaults/memory-retrieval/tail-reinjection-strip.ts +64 -0
  291. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +29 -21
  292. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1 -0
  293. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  294. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +75 -7
  295. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +31 -4
  296. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +77 -2
  297. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +1 -0
  298. package/src/plugins/defaults/memory-v3-shadow/injector.ts +7 -10
  299. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +37 -4
  300. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +32 -20
  301. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +56 -3
  302. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +23 -2
  303. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +2 -12
  304. package/src/plugins/defaults/title-generate/hooks/stop.ts +56 -21
  305. package/src/prompts/persona-resolver.ts +12 -2
  306. package/src/prompts/templates/system-sections.ts +7 -2
  307. package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
  308. package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
  309. package/src/providers/__tests__/retry-callsite.test.ts +176 -0
  310. package/src/providers/atlascloud/client.ts +85 -0
  311. package/src/providers/fetch-provider-catalog.ts +85 -0
  312. package/src/providers/inference/adapter-factory.ts +3 -0
  313. package/src/providers/model-catalog.ts +58 -0
  314. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +33 -0
  315. package/src/providers/openai/chat-completions-provider.ts +7 -0
  316. package/src/providers/openai/responses-provider.ts +10 -0
  317. package/src/providers/provider-send-message.ts +11 -3
  318. package/src/providers/retry.ts +53 -12
  319. package/src/providers/search-provider-catalog.ts +10 -0
  320. package/src/providers/weak-open-model.ts +22 -0
  321. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  322. package/src/runtime/__tests__/client-health.test.ts +44 -0
  323. package/src/runtime/access-request-helper.ts +21 -53
  324. package/src/runtime/actor-trust-resolver.ts +49 -21
  325. package/src/runtime/agent-wake.ts +52 -0
  326. package/src/runtime/assistant-event-hub.ts +18 -4
  327. package/src/runtime/auth/__tests__/route-policy.test.ts +12 -0
  328. package/src/runtime/auth/require-bound-guardian.ts +1 -4
  329. package/src/runtime/capabilities.test.ts +120 -0
  330. package/src/runtime/capabilities.ts +197 -0
  331. package/src/runtime/channel-approval-types.ts +5 -1
  332. package/src/runtime/channel-retry-sweep.ts +1 -0
  333. package/src/runtime/channel-verification-service.ts +1 -2
  334. package/src/runtime/client-health.ts +26 -0
  335. package/src/runtime/confirmation-request-guardian-bridge.ts +38 -29
  336. package/src/runtime/effective-capabilities.test.ts +128 -0
  337. package/src/runtime/effective-capabilities.ts +84 -0
  338. package/src/runtime/guardian-reply-router.ts +106 -21
  339. package/src/runtime/invite-redemption-service.ts +6 -22
  340. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +123 -0
  341. package/src/runtime/migrations/vbundle-builder.ts +49 -20
  342. package/src/runtime/pending-interactions.ts +15 -0
  343. package/src/runtime/routes/__tests__/client-routes.test.ts +13 -0
  344. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +67 -0
  345. package/src/runtime/routes/__tests__/plugins-routes.test.ts +35 -13
  346. package/src/runtime/routes/browser-tabs-routes.ts +9 -0
  347. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +17 -8
  348. package/src/runtime/routes/client-routes.ts +10 -0
  349. package/src/runtime/routes/contact-routes.ts +31 -8
  350. package/src/runtime/routes/conversation-management-routes.ts +80 -1
  351. package/src/runtime/routes/conversation-query-routes.ts +68 -22
  352. package/src/runtime/routes/conversation-routes.ts +37 -12
  353. package/src/runtime/routes/events-routes.ts +1 -3
  354. package/src/runtime/routes/guardian-approval-interception.ts +14 -73
  355. package/src/runtime/routes/guardian-approval-prompt.ts +22 -4
  356. package/src/runtime/routes/home-feed-routes.ts +8 -3
  357. package/src/runtime/routes/inbound-message-handler.ts +214 -228
  358. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +88 -6
  359. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +154 -0
  360. package/src/runtime/routes/inbound-stages/admission-policy.ts +140 -0
  361. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +3 -3
  362. package/src/runtime/routes/inbound-stages/background-dispatch.ts +11 -6
  363. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +1 -2
  364. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +1 -2
  365. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +7 -7
  366. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +47 -28
  367. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +358 -0
  368. package/src/runtime/routes/index.ts +2 -0
  369. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +8 -0
  370. package/src/runtime/routes/integrations/slack/channel.ts +36 -0
  371. package/src/runtime/routes/internal-telemetry-routes.ts +1 -1
  372. package/src/runtime/routes/mcp-auth-routes.ts +233 -41
  373. package/src/runtime/routes/memory-eval-routes.ts +87 -0
  374. package/src/runtime/routes/notification-routes.ts +122 -133
  375. package/src/runtime/routes/platform-routes.ts +2 -2
  376. package/src/runtime/routes/plugins-routes.ts +40 -7
  377. package/src/runtime/routes/secret-routes.ts +10 -0
  378. package/src/runtime/routes/surface-action-routes.ts +2 -1
  379. package/src/runtime/routes/tool-call-question-enrichment.test.ts +146 -0
  380. package/src/runtime/routes/tool-call-question-enrichment.ts +66 -0
  381. package/src/runtime/routes/workflow-routes.test.ts +225 -1
  382. package/src/runtime/routes/workflow-routes.ts +131 -1
  383. package/src/runtime/tool-grant-request-helper.ts +18 -16
  384. package/src/runtime/trust-context-resolver.ts +8 -5
  385. package/src/schedule/schedule-store.ts +1 -1
  386. package/src/schedule/scheduler-types.ts +5 -1
  387. package/src/security/__tests__/provider-key-env-fallback.test.ts +6 -0
  388. package/src/security/secret-patterns.ts +3 -0
  389. package/src/subagent/manager.ts +11 -4
  390. package/src/telemetry/trace-collection-policy.test.ts +28 -0
  391. package/src/telemetry/trace-collection-policy.ts +30 -0
  392. package/src/telemetry/types.ts +89 -0
  393. package/src/telemetry/usage-telemetry-reporter.test.ts +586 -36
  394. package/src/telemetry/usage-telemetry-reporter.ts +148 -41
  395. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +31 -0
  396. package/src/tools/browser/browser-execution.ts +29 -18
  397. package/src/tools/document/document-tool.ts +2 -3
  398. package/src/tools/executor.ts +5 -3
  399. package/src/tools/host-terminal/host-shell.ts +5 -4
  400. package/src/tools/memory/register.ts +2 -2
  401. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +360 -0
  402. package/src/tools/network/__tests__/web-search.test.ts +143 -0
  403. package/src/tools/network/web-fetch.ts +372 -1
  404. package/src/tools/network/web-search.ts +213 -10
  405. package/src/tools/permission-checker.ts +3 -2
  406. package/src/tools/registry.ts +20 -0
  407. package/src/tools/schedule/create.ts +4 -3
  408. package/src/tools/schedule/update.ts +2 -1
  409. package/src/tools/shared/filesystem/path-policy.ts +39 -13
  410. package/src/tools/skills/execute.ts +1 -2
  411. package/src/tools/subagent/spawn.ts +37 -13
  412. package/src/tools/terminal/shell.ts +10 -4
  413. package/src/tools/tool-approval-handler.ts +17 -10
  414. package/src/tools/types.ts +9 -0
  415. package/src/tools/ui-surface/definitions.ts +25 -2
  416. package/src/tools/verification-control-plane-policy.ts +3 -1
  417. package/src/tools/workflows/run-workflow.ts +1 -0
  418. package/src/util/disk-usage.ts +78 -23
  419. package/src/util/platform.ts +8 -1
  420. package/src/watcher/telemetry.ts +2 -2
  421. package/src/workflows/engine.test.ts +175 -1
  422. package/src/workflows/engine.ts +82 -0
  423. package/src/workflows/journal-store.test.ts +70 -0
  424. package/src/workflows/journal-store.ts +18 -3
  425. package/src/workflows/run-manager.test.ts +171 -3
  426. package/src/workflows/run-manager.ts +64 -0
  427. package/src/workspace/migrations/105-enable-memory-v3-live-for-new-workspaces.ts +63 -0
  428. package/src/workspace/migrations/106-drop-collect-usage-data.ts +47 -0
  429. package/src/workspace/migrations/107-drop-send-diagnostics.ts +47 -0
  430. package/src/workspace/migrations/108-drop-balanced-economy-profile.ts +129 -0
  431. package/src/workspace/migrations/registry.ts +8 -0
  432. package/src/notifications/tool-approval-copy.ts +0 -142
  433. package/src/runtime/routes/approval-prompt-ts-tracker.ts +0 -78
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Cross-surface withdrawal of guardian approval cards.
3
+ *
4
+ * A single guardian request is projected onto every surface it was delivered
5
+ * to — the in-app Vellum card, a Slack message, etc. When the request reaches a
6
+ * terminal status its cards must stop offering live actions on *all* surfaces,
7
+ * not just the one the guardian acted on. This is the withdrawal counterpart to
8
+ * the unified card *rendering* dispatcher (`notifications/approval-card-data.ts`):
9
+ * rendering projects `pending` onto each surface, withdrawal projects the
10
+ * terminal status back onto each surface.
11
+ *
12
+ * Withdrawal preserves the card's information and removes only the live
13
+ * affordances (it does not delete the message/card) so each surface keeps an
14
+ * audit trail of what was decided.
15
+ *
16
+ * It is driven off `canonical_guardian_deliveries` — the per-request registry of
17
+ * where each card was sent — so it stays correct as surfaces are added and is
18
+ * agnostic to which surface originated the decision.
19
+ *
20
+ * Best-effort by contract: the request is already resolved (CAS committed)
21
+ * before this runs, so a failed edit must never surface as a decision failure.
22
+ * Every surface is attempted independently; one failure never blocks the rest.
23
+ */
24
+
25
+ import { completeSurfaceAndNotify } from "../daemon/conversation-surfaces.js";
26
+ import {
27
+ type CanonicalGuardianDelivery,
28
+ type CanonicalGuardianRequest,
29
+ type CanonicalRequestStatus,
30
+ listCanonicalGuardianDeliveries,
31
+ } from "../memory/canonical-guardian-store.js";
32
+ import { withdrawSlackApprovalCard } from "../messaging/providers/slack/withdraw.js";
33
+ import { approvalCardSurfaceId } from "../notifications/approval-card-data.js";
34
+ import { getLogger } from "../util/logger.js";
35
+
36
+ const log = getLogger("guardian-card-withdrawal");
37
+
38
+ /** Completion-summary label shown on an in-app card for a resolved request. */
39
+ const SURFACE_STATUS_LABELS: Partial<Record<CanonicalRequestStatus, string>> = {
40
+ approved: "Approved",
41
+ denied: "Denied",
42
+ expired: "Expired",
43
+ cancelled: "Cancelled",
44
+ };
45
+
46
+ export interface WithdrawGuardianCardsParams {
47
+ /** The request, already transitioned to its terminal status. */
48
+ request: CanonicalGuardianRequest;
49
+ /** Terminal status to reflect on each card. */
50
+ status: CanonicalRequestStatus;
51
+ /**
52
+ * Channel the decision originated on, when applicable.
53
+ *
54
+ * The acting in-app client completes its own card optimistically, so the
55
+ * in-app card is skipped when the decision originated in-app; it is withdrawn
56
+ * here only when the decision came from another surface. Omit (e.g. the expiry
57
+ * sweep) to withdraw every surface.
58
+ */
59
+ originChannel?: string;
60
+ }
61
+
62
+ /**
63
+ * Withdraw a resolved request's approval cards across all delivery surfaces.
64
+ * Never throws.
65
+ */
66
+ export async function withdrawGuardianRequestCards(
67
+ params: WithdrawGuardianCardsParams,
68
+ ): Promise<void> {
69
+ const { request, status, originChannel } = params;
70
+
71
+ let deliveries: CanonicalGuardianDelivery[];
72
+ try {
73
+ deliveries = listCanonicalGuardianDeliveries(request.id);
74
+ } catch (err) {
75
+ log.warn(
76
+ { err, requestId: request.id },
77
+ "Failed to list deliveries for card withdrawal",
78
+ );
79
+ return;
80
+ }
81
+
82
+ for (const delivery of deliveries) {
83
+ try {
84
+ if (delivery.destinationChannel === "vellum") {
85
+ withdrawVellumCard(request, delivery, status, originChannel);
86
+ } else if (delivery.destinationChannel === "slack") {
87
+ await withdrawSlackCard(request, delivery, status);
88
+ }
89
+ // Telegram/WhatsApp direct delivery can't edit a message in place (it
90
+ // would post a new one), so their stale clicks are left to the existing
91
+ // "already resolved" reply until in-place edit support lands.
92
+ } catch (err) {
93
+ log.warn(
94
+ {
95
+ err,
96
+ requestId: request.id,
97
+ channel: delivery.destinationChannel,
98
+ },
99
+ "Failed to withdraw guardian card on surface (non-fatal)",
100
+ );
101
+ }
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Withdraw the in-app approval card so it stops offering live actions while
107
+ * keeping its content. Skipped when the decision originated in-app — the acting
108
+ * client already completed the card itself.
109
+ */
110
+ function withdrawVellumCard(
111
+ request: CanonicalGuardianRequest,
112
+ delivery: CanonicalGuardianDelivery,
113
+ status: CanonicalRequestStatus,
114
+ originChannel: string | undefined,
115
+ ): void {
116
+ if (originChannel === "vellum") return;
117
+ if (!delivery.destinationConversationId) return;
118
+ const surfaceId = approvalCardSurfaceId(request.kind, request.id);
119
+ if (!surfaceId) return;
120
+ completeSurfaceAndNotify(
121
+ delivery.destinationConversationId,
122
+ surfaceId,
123
+ SURFACE_STATUS_LABELS[status] ?? "Resolved",
124
+ );
125
+ }
126
+
127
+ /**
128
+ * Edit the Slack message in place to its resolved state — original card content
129
+ * preserved, action buttons removed, an outcome/decider/time line appended.
130
+ * No-ops when the channel-native message id was not captured at delivery time.
131
+ */
132
+ async function withdrawSlackCard(
133
+ request: CanonicalGuardianRequest,
134
+ delivery: CanonicalGuardianDelivery,
135
+ status: CanonicalRequestStatus,
136
+ ): Promise<void> {
137
+ if (!delivery.destinationChatId || !delivery.destinationMessageId) return;
138
+ await withdrawSlackApprovalCard({
139
+ channel: delivery.destinationChatId,
140
+ messageTs: delivery.destinationMessageId,
141
+ status,
142
+ decidedByExternalUserId: request.decidedByExternalUserId ?? undefined,
143
+ decidedAtMs: request.updatedAt,
144
+ });
145
+ }
@@ -48,6 +48,7 @@ import type { ApplyGuardianDecisionResult } from "../runtime/guardian-decision-t
48
48
  import { computeToolApprovalDigest } from "../security/tool-approval-digest.js";
49
49
  import { getLogger } from "../util/logger.js";
50
50
  import { mintGrantFromDecision } from "./approval-primitive.js";
51
+ import { withdrawGuardianRequestCards } from "./guardian-card-withdrawal.js";
51
52
  import {
52
53
  type ActorContext,
53
54
  type ChannelDeliveryContext,
@@ -354,6 +355,7 @@ export type CanonicalDecisionResult =
354
355
  | "not_found"
355
356
  | "already_resolved"
356
357
  | "identity_mismatch"
358
+ | "request_misconfigured"
357
359
  | "invalid_action"
358
360
  | "expired";
359
361
  detail?: string;
@@ -426,18 +428,22 @@ export async function applyCanonicalGuardianDecision(
426
428
  // authorization gate — principal identity must always match.
427
429
 
428
430
  if (!request.guardianPrincipalId) {
429
- log.warn(
431
+ // A decisionable request with no bound principal can never be authorized
432
+ // by anyone — it is stuck. This is a data-integrity fault (creation guards
433
+ // against it for decisionable kinds), not an authorization denial, so it
434
+ // must NOT be reported to the actor as "you don't have permission".
435
+ log.error(
430
436
  {
431
437
  event: "canonical_decision_missing_request_principal",
432
438
  requestId,
433
439
  kind: request.kind,
434
440
  sourceType: request.sourceType,
435
441
  },
436
- "Canonical request missing guardianPrincipalId; rejecting decision",
442
+ "Canonical request missing guardianPrincipalId; request is undecidable",
437
443
  );
438
444
  return {
439
445
  applied: false,
440
- reason: "identity_mismatch",
446
+ reason: "request_misconfigured",
441
447
  detail: "request missing guardianPrincipalId",
442
448
  };
443
449
  }
@@ -571,6 +577,23 @@ export async function applyCanonicalGuardianDecision(
571
577
  grantMinted = grantResult.minted;
572
578
  }
573
579
 
580
+ // 6. Project the terminal status onto the request's approval cards on every
581
+ // surface it was delivered to (in-app, Slack, ...). Fire-and-forget: the
582
+ // decision is already committed via CAS and withdrawal is a best-effort
583
+ // cosmetic projection, so awaiting its Slack round-trips would only add
584
+ // latency to the decision response that interactive callers wait on. The
585
+ // projector never throws; the `.catch` is a defensive backstop.
586
+ void withdrawGuardianRequestCards({
587
+ request: resolved,
588
+ status: targetStatus,
589
+ originChannel: actorContext.channel,
590
+ }).catch((err) => {
591
+ log.warn(
592
+ { err, requestId },
593
+ "Cross-surface card withdrawal failed (non-fatal)",
594
+ );
595
+ });
596
+
574
597
  log.info(
575
598
  {
576
599
  event: "canonical_decision_applied",
@@ -55,6 +55,87 @@ function shouldUseEphemeral(sourceChannel: string, chatId: string): boolean {
55
55
  return sourceChannel === "slack" && chatId.startsWith("C");
56
56
  }
57
57
 
58
+ /**
59
+ * Deliver the verification code straight to the requester's Slack DM so the
60
+ * guardian is never an out-of-band courier for the secret.
61
+ *
62
+ * Slack is the only channel with a guaranteed private path to the requester:
63
+ * posting to their user ID (`U…`) opens a 1:1 DM. The `threadTs` query param is
64
+ * dropped because it points at the guardian's channel thread and would raise
65
+ * `thread_not_found` in the DM. The code is sent as a durable (non-ephemeral)
66
+ * message the requester can refer back to when verifying.
67
+ *
68
+ * The verification session is identity-bound to the requester, so delivering
69
+ * the code to them directly does not widen who can consume it — it only removes
70
+ * the guardian relay step.
71
+ *
72
+ * Returns whether the code was delivered.
73
+ */
74
+ async function deliverVerificationCodeToSlackRequester(params: {
75
+ replyCallbackUrl: string;
76
+ requesterExternalUserId: string;
77
+ verificationCode: string;
78
+ assistantId: string;
79
+ }): Promise<boolean> {
80
+ let callbackUrl = params.replyCallbackUrl;
81
+ try {
82
+ const url = new URL(params.replyCallbackUrl);
83
+ url.searchParams.delete("threadTs");
84
+ callbackUrl = url.toString();
85
+ } catch {
86
+ // Relative path (e.g. the desktop "/deliver/slack" target) — use as-is;
87
+ // it carries no threadTs to strip.
88
+ }
89
+
90
+ try {
91
+ await deliverChannelReply(callbackUrl, {
92
+ chatId: params.requesterExternalUserId,
93
+ text:
94
+ "Great news — your access request was approved! " +
95
+ `Your verification code is: \`${params.verificationCode}\`. ` +
96
+ "Reply with it here to complete verification. The code expires in 10 minutes.",
97
+ assistantId: params.assistantId,
98
+ });
99
+ return true;
100
+ } catch (err) {
101
+ log.error(
102
+ { err, requesterExternalUserId: params.requesterExternalUserId },
103
+ "Failed to auto-deliver verification code to Slack requester",
104
+ );
105
+ return false;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Build a requester-facing channel notice (approval/denial/courier text).
111
+ *
112
+ * Posts to the originating chat. On a Slack shared channel it goes out as an
113
+ * ephemeral message visible only to the requester — and because
114
+ * `chat.postEphemeral` needs a channel ID, `chatId` stays the channel
115
+ * (`requesterChatId`), never the requester's `U…` user ID.
116
+ */
117
+ function buildRequesterChannelNotice(params: {
118
+ channel: string;
119
+ requesterChatId: string;
120
+ requesterExternalUserId: string;
121
+ text: string;
122
+ assistantId: string;
123
+ }): Parameters<typeof deliverChannelReply>[1] {
124
+ const payload: Parameters<typeof deliverChannelReply>[1] = {
125
+ chatId: params.requesterChatId,
126
+ text: params.text,
127
+ assistantId: params.assistantId,
128
+ };
129
+ if (
130
+ shouldUseEphemeral(params.channel, params.requesterChatId) &&
131
+ params.requesterExternalUserId
132
+ ) {
133
+ payload.ephemeral = true;
134
+ payload.user = params.requesterExternalUserId;
135
+ }
136
+ return payload;
137
+ }
138
+
58
139
  // ---------------------------------------------------------------------------
59
140
  // Types
60
141
  // ---------------------------------------------------------------------------
@@ -363,8 +444,6 @@ const accessRequestResolver: GuardianRequestResolver = {
363
444
  const requesterExternalUserId = request.requesterExternalUserId ?? "";
364
445
  const requesterChatId =
365
446
  request.requesterChatId ?? request.requesterExternalUserId ?? "";
366
- const requesterLabel =
367
- requesterExternalUserId || requesterChatId || "the requester";
368
447
  const decidedByExternalUserId = ctx.actor.actorExternalUserId ?? "";
369
448
  const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
370
449
  const desktopDeliverUrl = resolveDeliverCallbackUrlForChannel(channel);
@@ -379,6 +458,13 @@ const accessRequestResolver: GuardianRequestResolver = {
379
458
  const requesterDisplayName =
380
459
  requesterContactResult?.contact.displayName ?? null;
381
460
 
461
+ // Guardian-facing label prefers the contact display name over the raw ID.
462
+ const requesterLabel =
463
+ requesterDisplayName ||
464
+ requesterExternalUserId ||
465
+ requesterChatId ||
466
+ "the requester";
467
+
382
468
  const decidedByContactResult = decidedByExternalUserId
383
469
  ? findContactChannel({
384
470
  channelType: channel,
@@ -397,22 +483,15 @@ const accessRequestResolver: GuardianRequestResolver = {
397
483
  // Deliver denial notification and lifecycle signals when channel context is available
398
484
  if (channelDeliveryContext) {
399
485
  try {
400
- const denialPayload: Parameters<typeof deliverChannelReply>[1] = {
401
- chatId: requesterChatId,
402
- text: "Your access request has been denied.",
403
- assistantId,
404
- };
405
- // On Slack shared channels, deliver as ephemeral so only the requester sees the denial
406
- if (
407
- shouldUseEphemeral(channel, requesterChatId) &&
408
- requesterExternalUserId
409
- ) {
410
- denialPayload.ephemeral = true;
411
- denialPayload.user = requesterExternalUserId;
412
- }
413
486
  await deliverChannelReply(
414
487
  channelDeliveryContext.replyCallbackUrl,
415
- denialPayload,
488
+ buildRequesterChannelNotice({
489
+ channel,
490
+ requesterChatId,
491
+ requesterExternalUserId,
492
+ text: "Your access request has been denied.",
493
+ assistantId,
494
+ }),
416
495
  );
417
496
  } catch (err) {
418
497
  log.error(
@@ -563,7 +642,7 @@ const accessRequestResolver: GuardianRequestResolver = {
563
642
 
564
643
  // Deliver verification code to guardian
565
644
  const codeText =
566
- `You approved access for ${requesterExternalUserId}. ` +
645
+ `You approved access for ${requesterLabel}. ` +
567
646
  `Give them this verification code: \`${session.secret}\`. ` +
568
647
  `The code expires in 10 minutes.`;
569
648
  try {
@@ -629,12 +708,8 @@ const accessRequestResolver: GuardianRequestResolver = {
629
708
  }
630
709
  }
631
710
 
632
- // Notify the requester. For Slack, route to DM via the user ID and
633
- // strip threadTs (which belongs to the guardian's channel thread).
634
- const requesterTargetChatId =
635
- channel === "slack" && requesterExternalUserId
636
- ? requesterExternalUserId
637
- : requesterChatId;
711
+ // Strip threadTs from the requester reply URL it belongs to the
712
+ // guardian's channel thread and would cause thread_not_found in a DM.
638
713
  let requesterCallbackUrl = channelDeliveryContext.replyCallbackUrl;
639
714
  if (channel === "slack" && requesterExternalUserId) {
640
715
  try {
@@ -647,47 +722,58 @@ const accessRequestResolver: GuardianRequestResolver = {
647
722
  }
648
723
 
649
724
  if (codeDelivered) {
650
- try {
651
- const approvalPayload: Parameters<typeof deliverChannelReply>[1] = {
652
- chatId: requesterTargetChatId,
653
- text:
654
- "Your access request has been approved! " +
655
- "Please enter the 6-digit verification code you receive from the guardian.",
656
- assistantId,
657
- };
658
- // On Slack shared channels, deliver as ephemeral so only the requester sees
659
- if (
660
- shouldUseEphemeral(channel, requesterChatId) &&
661
- requesterExternalUserId
662
- ) {
663
- approvalPayload.ephemeral = true;
664
- approvalPayload.user = requesterExternalUserId;
665
- }
666
- await deliverChannelReply(requesterCallbackUrl, approvalPayload);
725
+ // On Slack, deliver the code straight to the requester's DM so the
726
+ // guardian doesn't have to relay it. Other channels (and a failed Slack
727
+ // delivery) fall back to the courier notice — there is no guaranteed
728
+ // private path to the requester elsewhere (e.g. group chats).
729
+ const requesterCodeDelivered =
730
+ channel === "slack" && requesterExternalUserId
731
+ ? await deliverVerificationCodeToSlackRequester({
732
+ replyCallbackUrl: channelDeliveryContext.replyCallbackUrl,
733
+ requesterExternalUserId,
734
+ verificationCode: session.secret,
735
+ assistantId,
736
+ })
737
+ : false;
738
+
739
+ if (requesterCodeDelivered) {
667
740
  requesterNotified = true;
668
- } catch (err) {
669
- log.error(
670
- { err, requesterChatId },
671
- "Failed to notify requester of access request approval",
672
- );
741
+ } else {
742
+ try {
743
+ await deliverChannelReply(
744
+ requesterCallbackUrl,
745
+ buildRequesterChannelNotice({
746
+ channel,
747
+ requesterChatId,
748
+ requesterExternalUserId,
749
+ text:
750
+ "Your access request has been approved! " +
751
+ "Please enter the 6-digit verification code you receive from the guardian.",
752
+ assistantId,
753
+ }),
754
+ );
755
+ requesterNotified = true;
756
+ } catch (err) {
757
+ log.error(
758
+ { err, requesterChatId },
759
+ "Failed to notify requester of access request approval",
760
+ );
761
+ }
673
762
  }
674
763
  } else {
675
764
  try {
676
- const failurePayload: Parameters<typeof deliverChannelReply>[1] = {
677
- chatId: requesterTargetChatId,
678
- text:
679
- "Your access request was approved, but we were unable to " +
680
- "deliver the verification code. Please try again later.",
681
- assistantId,
682
- };
683
- if (
684
- shouldUseEphemeral(channel, requesterChatId) &&
685
- requesterExternalUserId
686
- ) {
687
- failurePayload.ephemeral = true;
688
- failurePayload.user = requesterExternalUserId;
689
- }
690
- await deliverChannelReply(requesterCallbackUrl, failurePayload);
765
+ await deliverChannelReply(
766
+ requesterCallbackUrl,
767
+ buildRequesterChannelNotice({
768
+ channel,
769
+ requesterChatId,
770
+ requesterExternalUserId,
771
+ text:
772
+ "Your access request was approved, but we were unable to " +
773
+ "deliver the verification code. Please try again later.",
774
+ assistantId,
775
+ }),
776
+ );
691
777
  } catch (err) {
692
778
  log.error(
693
779
  { err, requesterChatId },
@@ -721,26 +807,43 @@ const accessRequestResolver: GuardianRequestResolver = {
721
807
  });
722
808
  }
723
809
  } else if (desktopDeliverUrl && requesterChatId) {
724
- // For Slack, route to DM via requesterExternalUserId (user ID) instead
725
- // of requesterChatId (channel ID) to avoid posting in public channels.
726
- const targetChatId =
810
+ // Guardian decided off-channel (e.g. desktop) but the requester is on a
811
+ // deliverable channel. On Slack, DM the code directly (parity with the
812
+ // on-channel path); otherwise fall back to the courier notice.
813
+ const requesterCodeDelivered =
727
814
  channel === "slack" && requesterExternalUserId
728
- ? requesterExternalUserId
729
- : requesterChatId;
730
- try {
731
- await deliverChannelReply(desktopDeliverUrl, {
732
- chatId: targetChatId,
733
- text:
734
- "Your access request has been approved! " +
735
- "Please enter the 6-digit verification code you receive from the guardian.",
736
- assistantId,
737
- });
815
+ ? await deliverVerificationCodeToSlackRequester({
816
+ replyCallbackUrl: desktopDeliverUrl,
817
+ requesterExternalUserId,
818
+ verificationCode: session.secret,
819
+ assistantId,
820
+ })
821
+ : false;
822
+
823
+ if (requesterCodeDelivered) {
738
824
  requesterNotified = true;
739
- } catch (err) {
740
- log.error(
741
- { err, requesterChatId },
742
- "Failed to notify requester of access request approval (desktop decision path)",
743
- );
825
+ } else {
826
+ // For Slack, route to DM via requesterExternalUserId (user ID) instead
827
+ // of requesterChatId (channel ID) to avoid posting in public channels.
828
+ const targetChatId =
829
+ channel === "slack" && requesterExternalUserId
830
+ ? requesterExternalUserId
831
+ : requesterChatId;
832
+ try {
833
+ await deliverChannelReply(desktopDeliverUrl, {
834
+ chatId: targetChatId,
835
+ text:
836
+ "Your access request has been approved! " +
837
+ "Please enter the 6-digit verification code you receive from the guardian.",
838
+ assistantId,
839
+ });
840
+ requesterNotified = true;
841
+ } catch (err) {
842
+ log.error(
843
+ { err, requesterChatId },
844
+ "Failed to notify requester of access request approval (desktop decision path)",
845
+ );
846
+ }
744
847
  }
745
848
  }
746
849
 
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Tests for the gateway-backed channel admission policy reader.
3
+ *
4
+ * The reader fails open (returns null = admit) so a gateway IPC failure can
5
+ * never block a live call. These tests pin that contract plus the TTL cache.
6
+ */
7
+
8
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
9
+
10
+ // ── Controllable IPC mock ────────────────────────────────────────────────────
11
+
12
+ type IpcHandler = (params?: Record<string, unknown>) => unknown;
13
+
14
+ const ipcHandlers = new Map<string, IpcHandler>();
15
+ const ipcCallLog: Array<{
16
+ method: string;
17
+ params?: Record<string, unknown>;
18
+ timeoutMs?: number;
19
+ }> = [];
20
+
21
+ mock.module("../../ipc/gateway-client.js", () => ({
22
+ ipcCall: async (
23
+ method: string,
24
+ params?: Record<string, unknown>,
25
+ timeoutMs?: number,
26
+ ) => {
27
+ ipcCallLog.push({ method, params, timeoutMs });
28
+ const handler = ipcHandlers.get(method);
29
+ return handler ? handler(params) : undefined;
30
+ },
31
+ ipcCallPersistent: async () => undefined,
32
+ resetPersistentClient: () => {},
33
+ }));
34
+
35
+ import {
36
+ _clearCacheForTesting,
37
+ getChannelAdmissionPolicy,
38
+ } from "../channel-admission-reader.js";
39
+
40
+ const METHOD = "get_channel_admission_policy";
41
+
42
+ function countCalls(method: string): number {
43
+ return ipcCallLog.filter((c) => c.method === method).length;
44
+ }
45
+
46
+ describe("getChannelAdmissionPolicy", () => {
47
+ beforeEach(() => {
48
+ _clearCacheForTesting();
49
+ ipcHandlers.clear();
50
+ ipcCallLog.length = 0;
51
+ });
52
+
53
+ test("returns the gateway-resolved policy and caches it within the TTL", async () => {
54
+ ipcHandlers.set(METHOD, () => ({ policy: "guardian_only" }));
55
+
56
+ expect(await getChannelAdmissionPolicy("telegram")).toBe("guardian_only");
57
+ // Second call within TTL is served from cache — no extra IPC round-trip.
58
+ expect(await getChannelAdmissionPolicy("telegram")).toBe("guardian_only");
59
+ expect(countCalls(METHOD)).toBe(1);
60
+ });
61
+
62
+ test("bounds the IPC read with a short timeout so it fails open promptly", async () => {
63
+ ipcHandlers.set(METHOD, () => ({ policy: "guardian_only" }));
64
+
65
+ await getChannelAdmissionPolicy("telegram");
66
+
67
+ const call = ipcCallLog.find((c) => c.method === METHOD);
68
+ expect(call?.timeoutMs).toBe(1_000);
69
+ });
70
+
71
+ test("returns null when IPC transport fails (undefined)", async () => {
72
+ ipcHandlers.set(METHOD, () => undefined);
73
+ expect(await getChannelAdmissionPolicy("telegram")).toBeNull();
74
+ });
75
+
76
+ test("returns null when the gateway explicitly resolves no policy", async () => {
77
+ ipcHandlers.set(METHOD, () => ({ policy: null }));
78
+ expect(await getChannelAdmissionPolicy("telegram")).toBeNull();
79
+ });
80
+
81
+ test("returns null when the IPC call throws", async () => {
82
+ ipcHandlers.set(METHOD, () => {
83
+ throw new Error("socket exploded");
84
+ });
85
+ expect(await getChannelAdmissionPolicy("telegram")).toBeNull();
86
+ });
87
+
88
+ test("returns null for an invalid policy string", async () => {
89
+ ipcHandlers.set(METHOD, () => ({ policy: "definitely-not-a-policy" }));
90
+ expect(await getChannelAdmissionPolicy("telegram")).toBeNull();
91
+ });
92
+
93
+ test("caches the explicit no-enforcement (null) answer within the TTL", async () => {
94
+ // The gateway successfully said "no enforcement" — a real, cacheable answer.
95
+ ipcHandlers.set(METHOD, () => ({ policy: null }));
96
+ expect(await getChannelAdmissionPolicy("telegram")).toBeNull();
97
+ expect(await getChannelAdmissionPolicy("telegram")).toBeNull();
98
+ expect(countCalls(METHOD)).toBe(1);
99
+ });
100
+
101
+ test("does NOT cache a transport failure (undefined) — re-consults the gateway", async () => {
102
+ // First setup: gateway hiccup → fail open to null, NOT cached.
103
+ ipcHandlers.set(METHOD, () => undefined);
104
+ expect(await getChannelAdmissionPolicy("telegram")).toBeNull();
105
+
106
+ // Gateway recovers with a restrictive policy: the next setup must re-attempt
107
+ // the IPC (not serve a stale fail-open null) and honor the now-recovered floor.
108
+ ipcHandlers.set(METHOD, () => ({ policy: "guardian_only" }));
109
+ expect(await getChannelAdmissionPolicy("telegram")).toBe("guardian_only");
110
+ expect(countCalls(METHOD)).toBe(2);
111
+ });
112
+
113
+ test("does NOT cache a thrown IPC error — re-consults the gateway", async () => {
114
+ ipcHandlers.set(METHOD, () => {
115
+ throw new Error("socket exploded");
116
+ });
117
+ expect(await getChannelAdmissionPolicy("telegram")).toBeNull();
118
+
119
+ ipcHandlers.set(METHOD, () => ({ policy: "no_one" }));
120
+ expect(await getChannelAdmissionPolicy("telegram")).toBe("no_one");
121
+ expect(countCalls(METHOD)).toBe(2);
122
+ });
123
+
124
+ test("does NOT cache a malformed shape — re-consults the gateway", async () => {
125
+ ipcHandlers.set(METHOD, () => ({ policy: "definitely-not-a-policy" }));
126
+ expect(await getChannelAdmissionPolicy("telegram")).toBeNull();
127
+
128
+ ipcHandlers.set(METHOD, () => ({ policy: "guardian_only" }));
129
+ expect(await getChannelAdmissionPolicy("telegram")).toBe("guardian_only");
130
+ expect(countCalls(METHOD)).toBe(2);
131
+ });
132
+ });