@vellumai/assistant 0.8.12 → 0.9.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 (442) hide show
  1. package/AGENTS.md +0 -14
  2. package/ARCHITECTURE.md +45 -45
  3. package/README.md +1 -1
  4. package/bun.lock +200 -154
  5. package/docs/architecture/integrations.md +3 -3
  6. package/docs/architecture/memory.md +2 -2
  7. package/docs/architecture/security.md +10 -10
  8. package/docs/runbook-trusted-contacts.md +12 -12
  9. package/docs/skills.md +6 -6
  10. package/docs/workflows-testing.md +221 -0
  11. package/docs/workflows.md +510 -0
  12. package/examples/plugins/echo/README.md +5 -5
  13. package/knip.json +2 -0
  14. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +105 -0
  15. package/node_modules/@vellumai/gateway-client/src/index.ts +12 -0
  16. package/openapi.yaml +7197 -5708
  17. package/package.json +8 -4
  18. package/scripts/generate-openapi.ts +66 -114
  19. package/src/__tests__/access-request-seed-content-blocks.test.ts +213 -0
  20. package/src/__tests__/adaptive-thinking-repair.test.ts +32 -3
  21. package/src/__tests__/agent-loop-output-hooks.test.ts +183 -0
  22. package/src/__tests__/agent-loop-regrowth-guard.test.ts +506 -0
  23. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  24. package/src/__tests__/agent-wake-override-profile.test.ts +77 -0
  25. package/src/__tests__/app-compiler.test.ts +7 -1
  26. package/src/__tests__/app-dir-path-guard.test.ts +27 -3
  27. package/src/__tests__/app-executors.test.ts +43 -0
  28. package/src/__tests__/approval-cascade.test.ts +0 -5
  29. package/src/__tests__/approval-routes-http.test.ts +91 -0
  30. package/src/__tests__/assistant-stream-state.test.ts +107 -0
  31. package/src/__tests__/browser-fill-credential.test.ts +3 -3
  32. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -1
  33. package/src/__tests__/compaction-events.test.ts +63 -7
  34. package/src/__tests__/compaction-trail-store.test.ts +74 -1
  35. package/src/__tests__/compaction.benchmark.test.ts +63 -41
  36. package/src/__tests__/compactor-low-watermark-cut.test.ts +349 -0
  37. package/src/__tests__/context-window-manager-compact-retry.test.ts +64 -0
  38. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -5
  39. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -5
  40. package/src/__tests__/conversation-history-web-search.test.ts +7 -0
  41. package/src/__tests__/conversation-process-callsite.test.ts +0 -5
  42. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -5
  43. package/src/__tests__/conversation-queue.test.ts +0 -5
  44. package/src/__tests__/conversation-slash-queue.test.ts +0 -5
  45. package/src/__tests__/conversation-slash-unknown.test.ts +0 -5
  46. package/src/__tests__/conversation-speed-override.test.ts +0 -5
  47. package/src/__tests__/conversation-surfaces-data-persist.test.ts +97 -0
  48. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  49. package/src/__tests__/conversation-usage.test.ts +2 -0
  50. package/src/__tests__/conversation-workspace-injection.test.ts +0 -5
  51. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -5
  52. package/src/__tests__/credential-broker-browser-fill.test.ts +2 -2
  53. package/src/__tests__/credential-broker-server-use.test.ts +2 -2
  54. package/src/__tests__/credential-broker.test.ts +1 -1
  55. package/src/__tests__/credential-prompt-route.test.ts +417 -0
  56. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  57. package/src/__tests__/credential-vault.test.ts +37 -0
  58. package/src/__tests__/db-schedule-syntax-migration.test.ts +24 -0
  59. package/src/__tests__/dynamic-page-surface.test.ts +219 -0
  60. package/src/__tests__/empty-state-greeting-cache.test.ts +94 -0
  61. package/src/__tests__/gateway-flag-listener.test.ts +24 -7
  62. package/src/__tests__/guardian-action-sweep.test.ts +56 -219
  63. package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
  64. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  65. package/src/__tests__/list-messages-hidden-metadata.test.ts +99 -0
  66. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +87 -1
  67. package/src/__tests__/llm-resolver.test.ts +115 -0
  68. package/src/__tests__/managed-profile-guard.test.ts +6 -5
  69. package/src/__tests__/max-tokens-continue-hook.test.ts +184 -0
  70. package/src/__tests__/media-generate-image.test.ts +20 -9
  71. package/src/__tests__/mock-gateway-ipc.ts +23 -0
  72. package/src/__tests__/model-intents.test.ts +1 -1
  73. package/src/__tests__/normalize-onboarding.test.ts +26 -0
  74. package/src/__tests__/notification-decision-strategy.test.ts +4 -2
  75. package/src/__tests__/notification-telegram-adapter.test.ts +21 -3
  76. package/src/__tests__/pending-interactions-resolved-event.test.ts +62 -0
  77. package/src/__tests__/post-turn-tool-result-truncation.test.ts +72 -18
  78. package/src/__tests__/require-fresh-approval.test.ts +425 -1
  79. package/src/__tests__/resolve-app-id.test.ts +56 -0
  80. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -0
  81. package/src/__tests__/schedule-routes-workflow-validation.test.ts +408 -0
  82. package/src/__tests__/schedule-routes.test.ts +257 -4
  83. package/src/__tests__/schedule-store.test.ts +60 -0
  84. package/src/__tests__/schedule-tools.test.ts +247 -2
  85. package/src/__tests__/skill-execute-input.test.ts +85 -0
  86. package/src/__tests__/skill-secret-handling-guard.test.ts +21 -20
  87. package/src/__tests__/skills.test.ts +3 -3
  88. package/src/__tests__/slack-app-setup-skill-regression.test.ts +1 -1
  89. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  90. package/src/__tests__/subagent-tool-gate-mode.test.ts +547 -0
  91. package/src/__tests__/system-prompt.test.ts +1 -1
  92. package/src/__tests__/task-progress-nudge-hook.test.ts +372 -0
  93. package/src/__tests__/task-scheduler.test.ts +299 -0
  94. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +209 -0
  95. package/src/__tests__/tool-result-spool.test.ts +3 -1
  96. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +181 -0
  97. package/src/__tests__/workspace-migration-103-upgrade-quality-profile-to-opus-4-8.test.ts +174 -0
  98. package/src/agent/compaction-circuit.ts +11 -0
  99. package/src/agent/loop.ts +181 -12
  100. package/src/api/constants/call-sites.ts +12 -0
  101. package/src/api/events/assistant-thinking-delta.ts +10 -0
  102. package/src/api/events/usage-update.ts +7 -0
  103. package/src/api/index.ts +4 -1
  104. package/src/api/responses/memory-v3-selection-log.ts +18 -11
  105. package/src/approvals/approval-primitive.ts +2 -2
  106. package/src/background-wake/background-wake-routes.test.ts +5 -2
  107. package/src/bundler/compiler-tools.ts +1 -1
  108. package/src/bundler/package-resolver.ts +0 -1
  109. package/src/calls/call-domain.ts +1 -1
  110. package/src/calls/guardian-action-sweep.ts +16 -93
  111. package/src/cli/AGENTS.md +4 -0
  112. package/src/cli/commands/__tests__/schedules.test.ts +430 -1
  113. package/src/cli/commands/credentials.ts +28 -24
  114. package/src/cli/commands/image-generation.ts +23 -9
  115. package/src/cli/commands/notifications.ts +1 -1
  116. package/src/cli/commands/plugins.ts +89 -46
  117. package/src/cli/commands/schedules.ts +384 -11
  118. package/src/cli/lib/__tests__/inspect-plugin.test.ts +69 -5
  119. package/src/cli/lib/__tests__/install-from-github.test.ts +15 -0
  120. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +81 -4
  121. package/src/cli/lib/inspect-plugin.ts +62 -1
  122. package/src/cli/lib/install-from-github.ts +52 -5
  123. package/src/cli/lib/upgrade-plugin.ts +18 -0
  124. package/src/config/__tests__/workflows-schema.test.ts +60 -0
  125. package/src/config/bundled-skills/acp/SKILL.md +2 -2
  126. package/src/config/bundled-skills/app-builder/SKILL.md +1 -1
  127. package/src/config/bundled-skills/app-builder/tools/app-create.ts +6 -1
  128. package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +7 -1
  129. package/src/config/bundled-skills/app-builder/tools/app-refresh.ts +7 -1
  130. package/src/config/bundled-skills/app-builder/tools/app-update.ts +10 -1
  131. package/src/config/bundled-skills/image-studio/SKILL.md +66 -19
  132. package/src/config/bundled-skills/image-studio/TOOLS.json +1 -6
  133. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +22 -3
  134. package/src/config/bundled-skills/personal-page/SKILL.md +57 -0
  135. package/src/config/bundled-skills/personal-page/TOOLS.json +27 -0
  136. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +17 -0
  137. package/src/config/bundled-skills/schedule/SKILL.md +7 -2
  138. package/src/config/bundled-skills/schedule/TOOLS.json +48 -4
  139. package/src/config/bundled-skills/workflows/SKILL.md +214 -0
  140. package/src/config/bundled-skills/workflows/TOOLS.json +84 -0
  141. package/src/config/bundled-skills/workflows/tools/manage-workflows.ts +12 -0
  142. package/src/config/bundled-skills/workflows/tools/run-workflow.ts +12 -0
  143. package/src/config/bundled-tool-registry.ts +14 -2
  144. package/src/config/call-site-defaults.ts +5 -0
  145. package/src/config/feature-flag-registry.json +12 -4
  146. package/src/config/llm-context-resolution.ts +8 -0
  147. package/src/config/llm-resolver.ts +30 -0
  148. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +22 -0
  149. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +16 -0
  150. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +77 -0
  151. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +57 -0
  152. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +28 -0
  153. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +234 -0
  154. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +48 -0
  155. package/src/config/preloaded-apps/personal-page/src/components/media.ts +16 -0
  156. package/src/config/preloaded-apps/personal-page/src/index.html +20 -0
  157. package/src/config/preloaded-apps/personal-page/src/main.tsx +7 -0
  158. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +82 -0
  159. package/src/config/preloaded-apps/personal-page/src/styles.css +759 -0
  160. package/src/config/schema.ts +2 -0
  161. package/src/config/schemas/call-site-catalog.ts +7 -0
  162. package/src/config/schemas/heartbeat.ts +4 -1
  163. package/src/config/schemas/llm.ts +33 -26
  164. package/src/config/schemas/memory-retrospective.ts +19 -0
  165. package/src/config/schemas/platform.ts +8 -0
  166. package/src/config/schemas/services.ts +5 -2
  167. package/src/config/schemas/workflows.ts +42 -0
  168. package/src/config/skills.ts +3 -3
  169. package/src/context/compactor.ts +273 -39
  170. package/src/context/post-turn-tool-result-truncation.ts +23 -6
  171. package/src/context/tool-result-spool.ts +12 -17
  172. package/src/credential-execution/executable-discovery.ts +1 -1
  173. package/src/credential-execution/process-manager.ts +37 -3
  174. package/src/credential-execution/prompted-credential.ts +205 -0
  175. package/src/daemon/conversation-agent-loop-handlers.ts +14 -0
  176. package/src/daemon/conversation-process.ts +11 -2
  177. package/src/daemon/conversation-surfaces.ts +167 -3
  178. package/src/daemon/conversation-tool-setup.ts +103 -26
  179. package/src/daemon/conversation-usage.ts +2 -0
  180. package/src/daemon/conversation.ts +115 -11
  181. package/src/daemon/handlers/shared.ts +26 -14
  182. package/src/daemon/host-cu-proxy.ts +15 -12
  183. package/src/daemon/host-file-proxy.ts +15 -12
  184. package/src/daemon/host-transfer-proxy.ts +30 -24
  185. package/src/daemon/lifecycle.ts +40 -3
  186. package/src/daemon/message-protocol.ts +3 -0
  187. package/src/daemon/message-types/messages.ts +2 -10
  188. package/src/daemon/message-types/workflows.ts +49 -0
  189. package/src/daemon/parse-actual-tokens-from-error.test.ts +62 -1
  190. package/src/daemon/parse-actual-tokens-from-error.ts +43 -4
  191. package/src/daemon/process-message.ts +6 -0
  192. package/src/daemon/tool-setup-types.ts +57 -0
  193. package/src/daemon/wake-conversation-ops.ts +18 -0
  194. package/src/heartbeat/heartbeat-run-store.ts +8 -2
  195. package/src/home/feed-types.ts +1 -1
  196. package/src/ipc/gateway-flag-listener.ts +28 -6
  197. package/src/mcp/mcp-auth-state.ts +8 -20
  198. package/src/media/__tests__/image-models.test.ts +57 -0
  199. package/src/media/image-models.ts +66 -0
  200. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +38 -0
  201. package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +12 -2
  202. package/src/memory/__tests__/memory-retrospective-job.test.ts +911 -34
  203. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +227 -5
  204. package/src/memory/__tests__/memory-retrospective-state.test.ts +195 -0
  205. package/src/memory/__tests__/preloaded-apps.test.ts +85 -0
  206. package/src/memory/auto-analysis-enqueue.ts +14 -1
  207. package/src/memory/compaction-log-store-clickhouse.ts +6 -4
  208. package/src/memory/conversation-crud.ts +9 -2
  209. package/src/memory/conversation-disk-view.ts +1 -1
  210. package/src/memory/conversation-queries.ts +22 -7
  211. package/src/memory/db-init.ts +20 -0
  212. package/src/memory/db-maintenance.ts +16 -0
  213. package/src/memory/embedding-runtime-manager.ts +1 -1
  214. package/src/memory/llm-request-log-source-clickhouse.ts +112 -14
  215. package/src/memory/llm-request-log-source-local.ts +19 -1
  216. package/src/memory/llm-request-log-source.ts +35 -6
  217. package/src/memory/llm-request-log-store.ts +90 -2
  218. package/src/memory/memory-retrospective-constants.ts +9 -0
  219. package/src/memory/memory-retrospective-enqueue.ts +3 -6
  220. package/src/memory/memory-retrospective-fork-boundary.ts +94 -0
  221. package/src/memory/memory-retrospective-job.ts +500 -208
  222. package/src/memory/memory-retrospective-startup-cleanup.ts +97 -19
  223. package/src/memory/memory-retrospective-state.ts +85 -2
  224. package/src/memory/migrations/281-memory-retrospective-remembered-log.ts +40 -0
  225. package/src/memory/migrations/282-schedule-inference-profile.test.ts +77 -0
  226. package/src/memory/migrations/282-schedule-inference-profile.ts +26 -0
  227. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.test.ts +102 -0
  228. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.ts +53 -0
  229. package/src/memory/migrations/284-workflow-runs.ts +51 -0
  230. package/src/memory/migrations/285-schedule-workflow-mode.ts +26 -0
  231. package/src/memory/migrations/286-workflow-run-trust.ts +27 -0
  232. package/src/memory/migrations/287-conversation-origin-channel-index.ts +15 -0
  233. package/src/memory/migrations/288-backfill-origin-channel-from-bindings.ts +43 -0
  234. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +115 -0
  235. package/src/memory/migrations/290-schedule-capabilities.test.ts +77 -0
  236. package/src/memory/migrations/290-schedule-capabilities.ts +25 -0
  237. package/src/memory/migrations/__tests__/281-memory-retrospective-remembered-log.test.ts +96 -0
  238. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +571 -0
  239. package/src/memory/migrations/index.ts +10 -0
  240. package/src/memory/preloaded-apps.ts +116 -0
  241. package/src/memory/schema/infrastructure.ts +4 -0
  242. package/src/memory/schema/memory-core.ts +4 -0
  243. package/src/memory/v2/__tests__/concept-page-frontmatter-schema.test.ts +45 -0
  244. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +11 -7
  245. package/src/memory/v2/__tests__/page-store.test.ts +13 -2
  246. package/src/memory/v2/__tests__/qdrant.test.ts +24 -0
  247. package/src/memory/v2/frontmatter-sweep.ts +7 -6
  248. package/src/memory/v2/page-store.ts +4 -3
  249. package/src/memory/v2/qdrant.ts +42 -3
  250. package/src/memory/v2/types.ts +16 -10
  251. package/src/messaging/draft-store.ts +1 -1
  252. package/src/notifications/access-request-copy.ts +200 -113
  253. package/src/notifications/adapters/slack.ts +250 -111
  254. package/src/notifications/adapters/telegram.ts +7 -44
  255. package/src/notifications/approval-card-builder.ts +93 -0
  256. package/src/notifications/broadcaster.ts +74 -0
  257. package/src/notifications/conversation-pairing.ts +8 -6
  258. package/src/notifications/copy-composer.ts +32 -26
  259. package/src/notifications/decision-engine.ts +59 -7
  260. package/src/notifications/guardian-question-mode.ts +145 -155
  261. package/src/notifications/home-feed-side-effect.ts +28 -11
  262. package/src/notifications/notification-utils.ts +66 -0
  263. package/src/notifications/signal.ts +6 -0
  264. package/src/notifications/tool-approval-copy.ts +142 -0
  265. package/src/notifications/types.ts +19 -0
  266. package/src/permissions/threshold.ts +11 -0
  267. package/src/plugin-api/types.ts +16 -4
  268. package/src/plugins/defaults/compaction/window-manager.ts +44 -0
  269. package/src/plugins/defaults/index.ts +46 -0
  270. package/src/plugins/defaults/max-tokens-continue/continue-state-store.ts +53 -0
  271. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +80 -0
  272. package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +20 -0
  273. package/src/plugins/defaults/max-tokens-continue/package.json +14 -0
  274. package/src/plugins/defaults/memory-retrieval/hooks/__tests__/user-prompt-submit.test.ts +37 -0
  275. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +29 -1
  276. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +8 -3
  277. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +4 -2
  278. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +28 -18
  279. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +67 -0
  280. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +122 -22
  281. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +2 -0
  282. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +63 -1
  283. package/src/plugins/defaults/memory-v3-shadow/injector.ts +61 -18
  284. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +1 -1
  285. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +39 -10
  286. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +34 -1
  287. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +112 -47
  288. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +78 -15
  289. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +206 -0
  290. package/src/plugins/defaults/task-progress-nudge/package.json +15 -0
  291. package/src/prompts/__tests__/system-prompt.test.ts +100 -1
  292. package/src/prompts/__tests__/task-progress-hint-section.test.ts +5 -7
  293. package/src/prompts/normalize-onboarding.ts +2 -0
  294. package/src/prompts/persona-resolver.ts +3 -0
  295. package/src/prompts/system-prompt.ts +51 -2
  296. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +3 -1
  297. package/src/prompts/templates/system-sections.ts +8 -3
  298. package/src/providers/call-site-routing.ts +6 -3
  299. package/src/providers/fireworks/client.ts +3 -0
  300. package/src/providers/inference/auth.ts +52 -46
  301. package/src/providers/model-intents.ts +2 -2
  302. package/src/providers/openai/__tests__/coerce-object-args.test.ts +105 -0
  303. package/src/providers/openai/chat-completions-provider.ts +47 -9
  304. package/src/providers/openai/coerce-object-args.ts +104 -0
  305. package/src/providers/retry.ts +8 -5
  306. package/src/providers/types.ts +10 -0
  307. package/src/runtime/__tests__/agent-wake.test.ts +629 -7
  308. package/src/runtime/access-request-helper.ts +16 -9
  309. package/src/runtime/agent-wake.ts +302 -51
  310. package/src/runtime/assistant-stream-state.ts +141 -8
  311. package/src/runtime/background-job-runner.ts +9 -0
  312. package/src/runtime/channel-approval-types.ts +1 -0
  313. package/src/runtime/channel-invite-transports/telegram.ts +6 -5
  314. package/src/runtime/channel-invite-transports/voice.ts +2 -2
  315. package/src/runtime/channel-invite-types.ts +4 -2
  316. package/src/runtime/channel-retry-sweep.ts +19 -41
  317. package/src/runtime/finalize-event-delivery.ts +72 -0
  318. package/src/runtime/guardian-action-message-composer.ts +0 -54
  319. package/src/runtime/http-server.ts +6 -14
  320. package/src/runtime/http-types.ts +0 -1
  321. package/src/runtime/message-composer-types.ts +0 -9
  322. package/src/runtime/middleware/__tests__/rate-limiter.test.ts +63 -0
  323. package/src/runtime/middleware/auth.ts +27 -3
  324. package/src/runtime/middleware/rate-limiter.ts +28 -1
  325. package/src/runtime/migrations/vbundle-builder.ts +6 -5
  326. package/src/runtime/pending-interactions.ts +20 -1
  327. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +232 -173
  328. package/src/runtime/routes/__tests__/plugins-routes.test.ts +18 -0
  329. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +436 -0
  330. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +11 -0
  331. package/src/runtime/routes/approval-routes.ts +35 -8
  332. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -1
  333. package/src/runtime/routes/btw-routes.ts +37 -1
  334. package/src/runtime/routes/channel-delivery-routes.ts +11 -7
  335. package/src/runtime/routes/channel-route-definitions.ts +3 -0
  336. package/src/runtime/routes/channel-route-shared.ts +3 -1
  337. package/src/runtime/routes/consolidation-routes.ts +17 -13
  338. package/src/runtime/routes/conversation-compaction-routes.ts +159 -119
  339. package/src/runtime/routes/conversation-list-routes.ts +41 -4
  340. package/src/runtime/routes/conversation-query-routes.ts +196 -11
  341. package/src/runtime/routes/conversation-routes.ts +15 -1
  342. package/src/runtime/routes/credential-prompt-routes.ts +39 -17
  343. package/src/runtime/routes/empty-state-greeting-cache.ts +65 -0
  344. package/src/runtime/routes/heartbeat-routes.ts +18 -13
  345. package/src/runtime/routes/image-generation-routes.ts +20 -2
  346. package/src/runtime/routes/inbound-message-handler.ts +13 -12
  347. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +32 -31
  348. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +7 -5
  349. package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -21
  350. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +5 -5
  351. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +6 -15
  352. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +1 -1
  353. package/src/runtime/routes/index.ts +6 -0
  354. package/src/runtime/routes/log-export/AGENTS.md +1 -1
  355. package/src/runtime/routes/log-export/workspace-allowlist.ts +1 -1
  356. package/src/runtime/routes/migration-routes.ts +5 -9
  357. package/src/runtime/routes/plugins-routes.ts +26 -0
  358. package/src/runtime/routes/ps-routes.ts +10 -8
  359. package/src/runtime/routes/retrospective-routes.ts +235 -0
  360. package/src/runtime/routes/runs-pagination.ts +75 -0
  361. package/src/runtime/routes/schedule-routes.ts +367 -59
  362. package/src/runtime/routes/sounds-config-routes.ts +239 -0
  363. package/src/runtime/routes/surface-action-routes.ts +84 -4
  364. package/src/runtime/routes/workflow-routes.test.ts +372 -0
  365. package/src/runtime/routes/workflow-routes.ts +363 -0
  366. package/src/runtime/routes/workspace-routes.test.ts +61 -1
  367. package/src/runtime/routes/workspace-routes.ts +26 -1
  368. package/src/runtime/services/__tests__/analyze-conversation.test.ts +38 -0
  369. package/src/runtime/services/analyze-conversation.ts +26 -13
  370. package/src/schedule/inference-profile.ts +28 -0
  371. package/src/schedule/schedule-store.ts +152 -4
  372. package/src/schedule/scheduler-types.ts +6 -0
  373. package/src/schedule/scheduler.ts +96 -0
  374. package/src/security/secret-allowlist.ts +1 -1
  375. package/src/skills/path-classifier.ts +1 -1
  376. package/src/tools/apps/executors.ts +23 -0
  377. package/src/tools/apps/resolve-app-id.ts +42 -0
  378. package/src/tools/browser/browser-execution.ts +9 -11
  379. package/src/tools/credentials/broker.ts +4 -4
  380. package/src/tools/credentials/vault.ts +26 -137
  381. package/src/tools/executor.ts +69 -0
  382. package/src/tools/flag-gated-tools.test.ts +76 -0
  383. package/src/tools/permission-checker.ts +8 -1
  384. package/src/tools/registry.ts +51 -0
  385. package/src/tools/schedule/create.ts +77 -1
  386. package/src/tools/schedule/list.ts +1 -0
  387. package/src/tools/schedule/update.ts +73 -1
  388. package/src/tools/skills/execute.ts +56 -0
  389. package/src/tools/terminal/shell.ts +1 -1
  390. package/src/tools/ui-surface/definitions.ts +91 -2
  391. package/src/tools/workflows/manage-workflows.ts +183 -0
  392. package/src/tools/workflows/run-workflow.test.ts +442 -0
  393. package/src/tools/workflows/run-workflow.ts +88 -0
  394. package/src/types/onboarding-context.ts +2 -0
  395. package/src/usage/attribution.ts +24 -0
  396. package/src/util/canonicalize-identity.ts +12 -3
  397. package/src/util/platform.ts +17 -17
  398. package/src/watcher/__tests__/engine.test.ts +24 -0
  399. package/src/watcher/__tests__/telemetry.test.ts +135 -0
  400. package/src/watcher/engine.ts +7 -0
  401. package/src/watcher/telemetry.ts +74 -0
  402. package/src/workflows/capabilities.test.ts +365 -0
  403. package/src/workflows/capabilities.ts +359 -0
  404. package/src/workflows/deterministic-stringify.ts +27 -0
  405. package/src/workflows/engine-integration.test.ts +656 -0
  406. package/src/workflows/engine.test.ts +1144 -0
  407. package/src/workflows/engine.ts +1078 -0
  408. package/src/workflows/fanout-load.test.ts +168 -0
  409. package/src/workflows/journal-store.test.ts +369 -0
  410. package/src/workflows/journal-store.ts +470 -0
  411. package/src/workflows/leaf-runner.test.ts +704 -0
  412. package/src/workflows/leaf-runner.ts +589 -0
  413. package/src/workflows/library.test.ts +134 -0
  414. package/src/workflows/library.ts +124 -0
  415. package/src/workflows/run-manager.test.ts +711 -0
  416. package/src/workflows/run-manager.ts +593 -0
  417. package/src/workflows/sandbox-escape.test.ts +339 -0
  418. package/src/workflows/sandbox.test.ts +251 -0
  419. package/src/workflows/sandbox.ts +447 -0
  420. package/src/workspace/adaptive-thinking-repair.ts +33 -11
  421. package/src/workspace/migrations/021-move-signals-to-workspace.ts +1 -1
  422. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +1 -1
  423. package/src/workspace/migrations/026-backfill-install-meta.ts +1 -1
  424. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +2 -1
  425. package/src/workspace/migrations/031-drop-user-md.ts +1 -4
  426. package/src/workspace/migrations/048-remove-workspace-hooks.ts +1 -1
  427. package/src/workspace/migrations/056-release-notes-inference-profile-reordering.ts +5 -2
  428. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +1 -1
  429. package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +8 -2
  430. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +41 -14
  431. package/src/workspace/migrations/102-preserve-heartbeat-enabled-for-existing-workspaces.ts +69 -0
  432. package/src/workspace/migrations/103-upgrade-quality-profile-to-opus-4-8.ts +83 -0
  433. package/src/workspace/migrations/104-recheck-adaptive-thinking-model-implied-anthropic.ts +133 -0
  434. package/src/workspace/migrations/registry.ts +6 -0
  435. package/src/workspace/migrations/runner.ts +1 -1
  436. package/tsconfig.json +1 -1
  437. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -200
  438. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -579
  439. package/src/__tests__/guardian-action-store.test.ts +0 -106
  440. package/src/daemon/guardian-action-generators.ts +0 -71
  441. package/src/memory/guardian-action-store.ts +0 -484
  442. package/src/runtime/guardian-action-grant-minter.ts +0 -150
@@ -1,17 +1,34 @@
1
1
  /**
2
2
  * Slack channel adapter — delivers notifications to Slack DMs
3
- * by calling the Slack Web API directly.
3
+ * using native Card blocks for approval notifications.
4
+ *
5
+ * Approval notifications (access requests, tool approvals) render as a
6
+ * single Slack Card block with Approve/Reject action buttons, plus
7
+ * optional companion context blocks for details that exceed the card's
8
+ * character limits. Non-approval notifications use standard Block Kit
9
+ * text sections.
10
+ *
11
+ * Card block reference:
12
+ * https://docs.slack.dev/reference/block-kit/blocks/card-block
4
13
  */
5
14
 
6
15
  import { sendSlackReply } from "../../messaging/providers/slack/send.js";
16
+ import type { ApprovalUIMetadata } from "../../runtime/channel-approval-types.js";
7
17
  import { getLogger } from "../../util/logger.js";
8
18
  import {
9
- buildAccessRequestIdentityLine,
10
19
  buildAccessRequestInviteDirective,
11
- sanitizeIdentityField,
20
+ buildAccessRequestWarnings,
21
+ buildSlackMessagePermalink,
22
+ isSlackDmConversation,
23
+ parseAccessRequestPayload,
24
+ type ParsedAccessRequestPayload,
12
25
  } from "../access-request-copy.js";
13
26
  import { isConversationSeedSane } from "../conversation-seed-composer.js";
14
- import { nonEmpty } from "../copy-composer.js";
27
+ import {
28
+ nonEmpty,
29
+ sanitizeIdentityField,
30
+ sanitizeMessagePreview,
31
+ } from "../notification-utils.js";
15
32
  import type {
16
33
  ChannelAdapter,
17
34
  ChannelDeliveryPayload,
@@ -24,6 +41,10 @@ import type {
24
41
 
25
42
  const log = getLogger("notif-adapter-slack");
26
43
 
44
+ // ---------------------------------------------------------------------------
45
+ // Text resolution
46
+ // ---------------------------------------------------------------------------
47
+
27
48
  function resolveSlackMessageText(payload: ChannelDeliveryPayload): string {
28
49
  const deliveryText = nonEmpty(payload.copy.deliveryText);
29
50
  if (deliveryText) return deliveryText;
@@ -41,147 +62,270 @@ function resolveSlackMessageText(payload: ChannelDeliveryPayload): string {
41
62
  return payload.sourceEventName.replace(/[._]/g, " ");
42
63
  }
43
64
 
65
+ /** Truncate to `maxLength`, appending "…" when exceeded. */
66
+ function truncate(text: string, maxLength: number): string {
67
+ if (text.length <= maxLength) return text;
68
+ return text.slice(0, maxLength - 1) + "…";
69
+ }
70
+
44
71
  // ---------------------------------------------------------------------------
45
- // Block Kit helpers for access request notifications
72
+ // Slack Card block builders for approval notifications
46
73
  // ---------------------------------------------------------------------------
47
74
 
48
- /**
49
- * Build Block Kit blocks for an access request notification.
50
- *
51
- * Returns an array of Slack Block Kit block objects with structured layout:
52
- * - Header: "New access request"
53
- * - Section: requester identity details
54
- * - Optional context: message preview
55
- * - Context: approval code instructions + invite directive
56
- */
57
- function buildAccessRequestBlocks(payload: Record<string, unknown>): unknown[] {
58
- const blocks: unknown[] = [];
75
+ /** Build action buttons for a Slack Card block from approval metadata. */
76
+ function buildCardActions(approval: ApprovalUIMetadata): unknown[] {
77
+ return approval.actions.map((action) => ({
78
+ type: "button",
79
+ text: { type: "plain_text", text: action.label, emoji: true },
80
+ action_id: `apr:${approval.requestId}:${action.id}`,
81
+ value: `apr:${approval.requestId}:${action.id}`,
82
+ ...(action.id === "reject" ? { style: "danger" } : { style: "primary" }),
83
+ }));
84
+ }
59
85
 
60
- // Header
61
- blocks.push({
62
- type: "header",
63
- text: { type: "plain_text", text: "New access request", emoji: true },
64
- });
86
+ // ---------------------------------------------------------------------------
87
+ // Access request card
88
+ // ---------------------------------------------------------------------------
65
89
 
66
- // Requester identity section
67
- const identityLine = buildAccessRequestIdentityLine(payload);
68
- blocks.push({
69
- type: "section",
70
- text: { type: "mrkdwn", text: identityLine },
71
- });
90
+ /** Concise requester identity for the card subtitle (≤150 chars). */
91
+ function buildAccessRequestSubtitle(p: ParsedAccessRequestPayload): string {
92
+ const rawName = nonEmpty(p.actorDisplayName) ?? nonEmpty(p.senderIdentifier);
93
+ const displayName = rawName ? sanitizeIdentityField(rawName) : "Someone";
94
+ const parts = [displayName];
95
+
96
+ const username = nonEmpty(p.actorUsername);
97
+ if (username) {
98
+ const safe = sanitizeIdentityField(username);
99
+ if (safe !== displayName) parts.push(`(@${safe})`);
100
+ }
72
101
 
73
- // Build fields for structured requester details
74
- const fields: Array<{ type: "mrkdwn"; text: string }> = [];
102
+ if (p.sourceChannel) parts.push(`via ${p.sourceChannel}`);
75
103
 
76
- const senderIdentifier = nonEmpty(
77
- typeof payload.senderIdentifier === "string"
78
- ? sanitizeIdentityField(payload.senderIdentifier)
79
- : undefined,
80
- );
81
- if (senderIdentifier) {
82
- fields.push({ type: "mrkdwn", text: `*Name:*\n${senderIdentifier}` });
104
+ return truncate(parts.join(" "), 150);
105
+ }
106
+
107
+ /** Card body: message preview when available, otherwise a default label. */
108
+ function buildAccessRequestBody(p: ParsedAccessRequestPayload): string {
109
+ if (p.messagePreview) {
110
+ const sanitized = sanitizeMessagePreview(p.messagePreview);
111
+ if (sanitized) {
112
+ // Truncate content before wrapping so formatting chars stay balanced.
113
+ // Wrapper `> _"..."_` is 6 chars; reserve space for them.
114
+ const trimmed = truncate(sanitized, 200 - 6);
115
+ return `> _"${trimmed}"_`;
116
+ }
83
117
  }
118
+ return "Requesting access to the assistant";
119
+ }
84
120
 
85
- const actorUsername = nonEmpty(
86
- typeof payload.actorUsername === "string"
87
- ? sanitizeIdentityField(payload.actorUsername)
88
- : undefined,
89
- );
90
- if (actorUsername) {
91
- fields.push({ type: "mrkdwn", text: `*Username:*\n@${actorUsername}` });
121
+ /** Source-channel context block with Slack permalink when available. */
122
+ function buildSourceContextBlock(
123
+ p: ParsedAccessRequestPayload,
124
+ ): unknown | undefined {
125
+ if (p.sourceChannel !== "slack" || !p.conversationExternalId) {
126
+ return undefined;
92
127
  }
93
128
 
94
- const sourceChannel = nonEmpty(
95
- typeof payload.sourceChannel === "string"
96
- ? payload.sourceChannel
97
- : undefined,
98
- );
99
- if (sourceChannel) {
100
- fields.push({ type: "mrkdwn", text: `*Channel:*\n${sourceChannel}` });
129
+ const permalink = p.messageTs
130
+ ? buildSlackMessagePermalink(p.conversationExternalId, p.messageTs)
131
+ : undefined;
132
+
133
+ let sourceText: string;
134
+ if (isSlackDmConversation(p.conversationExternalId)) {
135
+ sourceText = permalink
136
+ ? `Source: Slack — Direct message · <${permalink}|View message>`
137
+ : "Source: Slack — Direct message";
138
+ } else {
139
+ sourceText = permalink
140
+ ? `Source: Slack — <#${p.conversationExternalId}> · <${permalink}|View message>`
141
+ : `Source: Slack — <#${p.conversationExternalId}>`;
101
142
  }
102
143
 
103
- const actorExternalId = nonEmpty(
104
- typeof payload.actorExternalId === "string"
105
- ? sanitizeIdentityField(payload.actorExternalId)
106
- : undefined,
144
+ return {
145
+ type: "context",
146
+ elements: [{ type: "mrkdwn", text: sourceText }],
147
+ };
148
+ }
149
+
150
+ /** Stable requester identifier context block (external ID when it adds info). */
151
+ function buildRequesterIdBlock(
152
+ p: ParsedAccessRequestPayload,
153
+ ): unknown | undefined {
154
+ const safeExternalId = nonEmpty(
155
+ p.actorExternalId ? sanitizeIdentityField(p.actorExternalId) : undefined,
107
156
  );
108
- if (actorExternalId && actorExternalId !== senderIdentifier) {
109
- fields.push({ type: "mrkdwn", text: `*ID:*\n${actorExternalId}` });
110
- }
157
+ if (!safeExternalId) return undefined;
111
158
 
112
- if (fields.length > 0) {
113
- blocks.push({
114
- type: "section",
115
- fields,
116
- });
159
+ const displayedName = nonEmpty(
160
+ p.actorDisplayName
161
+ ? sanitizeIdentityField(p.actorDisplayName)
162
+ : p.senderIdentifier
163
+ ? sanitizeIdentityField(p.senderIdentifier)
164
+ : undefined,
165
+ );
166
+ const displayedUsername = nonEmpty(
167
+ p.actorUsername ? sanitizeIdentityField(p.actorUsername) : undefined,
168
+ );
169
+ if (
170
+ safeExternalId === displayedName ||
171
+ safeExternalId === displayedUsername
172
+ ) {
173
+ return undefined;
117
174
  }
118
175
 
119
- // Previously revoked warning
120
- const previousMemberStatus =
121
- typeof payload.previousMemberStatus === "string"
122
- ? payload.previousMemberStatus
176
+ return {
177
+ type: "context",
178
+ elements: [{ type: "mrkdwn", text: `ID: ${safeExternalId}` }],
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Build Slack blocks for an access request using a native Card block.
184
+ *
185
+ * Layout:
186
+ * Card — title + subtitle (identity) + body (preview) + actions + subtext (warnings)
187
+ * Context — source permalink (when the request is from Slack)
188
+ * Context — stable requester ID (when it adds info beyond subtitle)
189
+ * Context — invite directive
190
+ * Context — guardian verification note (conditional)
191
+ */
192
+ function buildAccessRequestCardBlocks(
193
+ payload: ChannelDeliveryPayload,
194
+ ): unknown[] {
195
+ const approval = payload.approvalContext!;
196
+ const p = parseAccessRequestPayload(payload.contextPayload!);
197
+ const blocks: unknown[] = [];
198
+
199
+ const subtitle = buildAccessRequestSubtitle(p);
200
+ const body = buildAccessRequestBody(p);
201
+
202
+ const warnings = buildAccessRequestWarnings(p);
203
+ const subtext =
204
+ warnings.length > 0
205
+ ? truncate(warnings.map((w) => `:warning: ${w}`).join(" · "), 200)
123
206
  : undefined;
124
- if (previousMemberStatus === "revoked") {
207
+
208
+ const card: Record<string, unknown> = {
209
+ type: "card",
210
+ title: { type: "plain_text", text: "Access Request" },
211
+ subtitle: { type: "mrkdwn", text: subtitle },
212
+ body: { type: "mrkdwn", text: body },
213
+ actions: buildCardActions(approval),
214
+ };
215
+ if (subtext) card.subtext = { type: "mrkdwn", text: subtext };
216
+ blocks.push(card);
217
+
218
+ const sourceContext = buildSourceContextBlock(p);
219
+ if (sourceContext) blocks.push(sourceContext);
220
+
221
+ const idBlock = buildRequesterIdBlock(p);
222
+ if (idBlock) blocks.push(idBlock);
223
+
224
+ blocks.push({
225
+ type: "context",
226
+ elements: [{ type: "mrkdwn", text: buildAccessRequestInviteDirective() }],
227
+ });
228
+
229
+ if (
230
+ (p.guardianResolutionSource === "vellum-anchor" ||
231
+ p.guardianResolutionSource === "none") &&
232
+ p.sourceChannel
233
+ ) {
125
234
  blocks.push({
126
235
  type: "context",
127
236
  elements: [
128
237
  {
129
238
  type: "mrkdwn",
130
- text: ":warning: This user was previously revoked.",
239
+ text: `_You haven't verified your identity on ${p.sourceChannel} yet. If this was you trying to message your assistant, say "help me verify as guardian on ${p.sourceChannel}" to set up direct access._`,
131
240
  },
132
241
  ],
133
242
  });
134
243
  }
135
244
 
136
- // Divider before instructions
137
- blocks.push({ type: "divider" });
245
+ return blocks;
246
+ }
138
247
 
139
- // Approval code instructions
140
- const requestCode = nonEmpty(
141
- typeof payload.requestCode === "string" ? payload.requestCode : undefined,
142
- );
143
- if (requestCode) {
144
- const code = requestCode.toUpperCase();
248
+ // ---------------------------------------------------------------------------
249
+ // Tool approval card
250
+ // ---------------------------------------------------------------------------
251
+
252
+ /**
253
+ * Build Slack blocks for a tool approval notification using a native Card block.
254
+ *
255
+ * Layout:
256
+ * Card — title + subtitle (tool + requester) + body (notification text) + actions
257
+ */
258
+ function buildToolApprovalCardBlocks(
259
+ payload: ChannelDeliveryPayload,
260
+ messageText: string,
261
+ ): unknown[] {
262
+ const approval = payload.approvalContext!;
263
+ const blocks: unknown[] = [];
264
+
265
+ const details = approval.permissionDetails;
266
+ const toolName = details?.toolName;
267
+ const requester = details?.requesterIdentifier;
268
+ let subtitle: string | undefined;
269
+ if (toolName && requester) {
270
+ subtitle = truncate(`${toolName} — requested by ${requester}`, 150);
271
+ } else if (toolName) {
272
+ subtitle = truncate(toolName, 150);
273
+ }
274
+
275
+ const needsOverflow = messageText.length > 200;
276
+ const card: Record<string, unknown> = {
277
+ type: "card",
278
+ title: {
279
+ type: "plain_text",
280
+ text: details ? "Tool Approval" : "Approval Request",
281
+ },
282
+ body: {
283
+ type: "mrkdwn",
284
+ text: needsOverflow ? truncate(messageText, 197) + " ↓" : messageText,
285
+ },
286
+ actions: buildCardActions(approval),
287
+ };
288
+ if (subtitle) card.subtitle = { type: "mrkdwn", text: subtitle };
289
+ blocks.push(card);
290
+
291
+ // When the message exceeds the card body limit, show the full text in a
292
+ // companion section so the approver can see the complete command/context.
293
+ if (needsOverflow) {
145
294
  blocks.push({
146
295
  type: "section",
147
- text: {
148
- type: "mrkdwn",
149
- text: `Reply *${code} approve* to grant access or *${code} reject* to deny.`,
150
- },
296
+ text: { type: "mrkdwn", text: truncate(messageText, 3000) },
151
297
  });
152
298
  }
153
299
 
154
- // Invite directive
155
- const inviteDirective = buildAccessRequestInviteDirective();
156
- blocks.push({
157
- type: "context",
158
- elements: [{ type: "mrkdwn", text: inviteDirective }],
159
- });
300
+ return blocks;
301
+ }
160
302
 
161
- // Guardian verification note
162
- const guardianResolutionSource =
163
- typeof payload.guardianResolutionSource === "string"
164
- ? payload.guardianResolutionSource
165
- : undefined;
303
+ // ---------------------------------------------------------------------------
304
+ // Unified approval block dispatcher
305
+ // ---------------------------------------------------------------------------
306
+
307
+ /**
308
+ * Build Slack blocks for any notification carrying approval context.
309
+ * Dispatches to the appropriate card builder based on source event type.
310
+ */
311
+ function buildApprovalNotificationBlocks(
312
+ payload: ChannelDeliveryPayload,
313
+ messageText: string,
314
+ ): unknown[] {
166
315
  if (
167
- (guardianResolutionSource === "vellum-anchor" ||
168
- guardianResolutionSource === "none") &&
169
- sourceChannel
316
+ payload.sourceEventName === "ingress.access_request" &&
317
+ payload.contextPayload != null
170
318
  ) {
171
- blocks.push({
172
- type: "context",
173
- elements: [
174
- {
175
- type: "mrkdwn",
176
- text: `_You haven't verified your identity on ${sourceChannel} yet. If this was you trying to message your assistant, say "help me verify as guardian on ${sourceChannel}" to set up direct access._`,
177
- },
178
- ],
179
- });
319
+ return buildAccessRequestCardBlocks(payload);
180
320
  }
181
321
 
182
- return blocks;
322
+ return buildToolApprovalCardBlocks(payload, messageText);
183
323
  }
184
324
 
325
+ // ---------------------------------------------------------------------------
326
+ // Adapter
327
+ // ---------------------------------------------------------------------------
328
+
185
329
  export class SlackAdapter implements ChannelAdapter {
186
330
  readonly channel: NotificationChannel = "slack";
187
331
 
@@ -203,15 +347,10 @@ export class SlackAdapter implements ChannelAdapter {
203
347
 
204
348
  const messageText = resolveSlackMessageText(payload);
205
349
 
206
- // Build Block Kit blocks for access request notifications
207
- const isAccessRequest =
208
- payload.sourceEventName === "ingress.access_request" &&
209
- payload.contextPayload != null;
210
-
211
350
  try {
212
- const result = isAccessRequest
351
+ const result = payload.approvalContext
213
352
  ? await sendSlackReply(chatId, messageText, {
214
- blocks: buildAccessRequestBlocks(payload.contextPayload!),
353
+ blocks: buildApprovalNotificationBlocks(payload, messageText),
215
354
  })
216
355
  : await sendSlackReply(chatId, messageText, { useBlocks: true });
217
356
 
@@ -2,19 +2,17 @@
2
2
  * Telegram channel adapter — delivers notifications to Telegram chats
3
3
  * by calling the Telegram Bot API directly.
4
4
  *
5
- * For access request notifications, inline keyboard buttons ("Approve once",
6
- * "Reject") are attached so the guardian can act without typing a command.
7
- * If the rich delivery fails, the adapter falls back to plain text with
8
- * typed-command instructions.
5
+ * When the delivery payload carries an `approvalContext` (built centrally
6
+ * by the broadcaster), inline keyboard buttons ("Approve once", "Reject")
7
+ * are attached. If the rich delivery fails, the adapter falls back to
8
+ * plain text with typed-command instructions.
9
9
  */
10
10
 
11
11
  import { sendTelegramReply } from "../../messaging/providers/telegram-bot/send.js";
12
- import type { ApprovalUIMetadata } from "../../runtime/channel-approval-types.js";
13
12
  import { ConfigError } from "../../util/errors.js";
14
13
  import { getLogger } from "../../util/logger.js";
15
- import { buildAccessRequestContractText } from "../access-request-copy.js";
16
14
  import { isConversationSeedSane } from "../conversation-seed-composer.js";
17
- import { nonEmpty } from "../copy-composer.js";
15
+ import { nonEmpty } from "../notification-utils.js";
18
16
  import type {
19
17
  ChannelAdapter,
20
18
  ChannelDeliveryPayload,
@@ -42,34 +40,6 @@ function resolveTelegramMessageText(payload: ChannelDeliveryPayload): string {
42
40
  return payload.sourceEventName.replace(/[._]/g, " ");
43
41
  }
44
42
 
45
- /**
46
- * Build an {@link ApprovalUIMetadata} for an access request so the delivery
47
- * renders inline keyboard buttons in the Telegram message.
48
- *
49
- * Returns `undefined` when the context payload is missing the required
50
- * `requestId`, in which case the caller should fall back to plain text.
51
- */
52
- function buildAccessRequestApproval(
53
- contextPayload: Record<string, unknown>,
54
- ): ApprovalUIMetadata | undefined {
55
- const requestId =
56
- typeof contextPayload.requestId === "string"
57
- ? contextPayload.requestId
58
- : undefined;
59
- if (!requestId) return undefined;
60
-
61
- const plainTextFallback = buildAccessRequestContractText(contextPayload);
62
-
63
- return {
64
- requestId,
65
- actions: [
66
- { id: "approve_once", label: "Approve once" },
67
- { id: "reject", label: "Reject" },
68
- ],
69
- plainTextFallback,
70
- };
71
- }
72
-
73
43
  export class TelegramAdapter implements ChannelAdapter {
74
44
  readonly channel: NotificationChannel = "telegram";
75
45
 
@@ -90,14 +60,7 @@ export class TelegramAdapter implements ChannelAdapter {
90
60
  }
91
61
 
92
62
  const messageText = resolveTelegramMessageText(payload);
93
-
94
- const isAccessRequest =
95
- payload.sourceEventName === "ingress.access_request" &&
96
- payload.contextPayload != null;
97
-
98
- const approval = isAccessRequest
99
- ? buildAccessRequestApproval(payload.contextPayload!)
100
- : undefined;
63
+ const approval = payload.approvalContext;
101
64
 
102
65
  try {
103
66
  if (approval) {
@@ -108,7 +71,7 @@ export class TelegramAdapter implements ChannelAdapter {
108
71
 
109
72
  log.info(
110
73
  { sourceEventName: payload.sourceEventName, chatId },
111
- "Telegram access request notification delivered with inline buttons",
74
+ "Telegram approval notification delivered with inline buttons",
112
75
  );
113
76
 
114
77
  return { success: true };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Shared builder for guardian approval card seed content blocks.
3
+ *
4
+ * Both access-request and tool-approval notifications render a Surface card
5
+ * with Approve/Reject buttons in the Vellum in-app channel. This module
6
+ * provides the shared construction logic so each notification type only
7
+ * supplies its domain-specific data (title, subtitle, metadata, etc.)
8
+ * without duplicating the card structure or action wiring.
9
+ *
10
+ * The card data shape matches `CardSurfaceData` from
11
+ * `daemon/message-types/surfaces.ts`: `{ title, subtitle, body, metadata }`.
12
+ * Actions use the canonical `apr:<requestId>:<action>` callback format
13
+ * consumed by `surface-action-routes.ts` → `processGuardianDecision`.
14
+ */
15
+
16
+ // ── Public types ────────────────────────────────────────────────────────────
17
+
18
+ export interface ApprovalCardParams {
19
+ /** Prefix for `surfaceId` — combined with `requestId` to form e.g. "access-request-abc123". */
20
+ surfaceIdPrefix: string;
21
+ /** Top-level card title shown in the surface header (e.g. "Access Request", "Tool Approval"). */
22
+ cardTitle: string;
23
+ /** Primary line inside the card (typically the requester's display name). */
24
+ requesterName: string;
25
+ /** Secondary line below the requester name (e.g. "Requesting access to the assistant"). */
26
+ subtitle: string;
27
+ /** Markdown body — blockquotes, warnings, links, etc. */
28
+ body: string;
29
+ /** Key-value metadata grid rendered below the body. */
30
+ metadata: Array<{ label: string; value: string }>;
31
+ /** When present, Approve/Reject buttons are wired with `apr:<requestId>:*` action IDs. */
32
+ requestId?: string;
33
+ /** Plain-text content for the fallback `text` block. */
34
+ fallbackText: string;
35
+ }
36
+
37
+ // ── Builder ─────────────────────────────────────────────────────────────────
38
+
39
+ /**
40
+ * Build a `[ui_surface, text]` block pair for a guardian approval notification.
41
+ *
42
+ * The `ui_surface` block renders as a card with Approve/Reject buttons in
43
+ * clients that support Surface rendering. The `text` block provides a
44
+ * plain-text fallback for older clients, search indexing, and CLI display.
45
+ */
46
+ export function buildApprovalCardBlocks(params: ApprovalCardParams): unknown[] {
47
+ const {
48
+ surfaceIdPrefix,
49
+ cardTitle,
50
+ requesterName,
51
+ subtitle,
52
+ body,
53
+ metadata,
54
+ requestId,
55
+ fallbackText,
56
+ } = params;
57
+
58
+ const actions = requestId
59
+ ? [
60
+ {
61
+ id: `apr:${requestId}:approve_once`,
62
+ label: "Approve",
63
+ style: "primary",
64
+ },
65
+ {
66
+ id: `apr:${requestId}:reject`,
67
+ label: "Reject",
68
+ style: "destructive",
69
+ },
70
+ ]
71
+ : undefined;
72
+
73
+ const surfaceBlock = {
74
+ type: "ui_surface" as const,
75
+ surfaceId: `${surfaceIdPrefix}-${requestId ?? "unknown"}`,
76
+ surfaceType: "card" as const,
77
+ title: cardTitle,
78
+ data: {
79
+ title: requesterName,
80
+ subtitle,
81
+ body,
82
+ metadata,
83
+ },
84
+ ...(actions ? { actions } : {}),
85
+ };
86
+
87
+ const textBlock = {
88
+ type: "text" as const,
89
+ text: fallbackText,
90
+ };
91
+
92
+ return [surfaceBlock, textBlock];
93
+ }