@vellumai/assistant 0.8.10 → 0.8.11-staging.1

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 (400) hide show
  1. package/bun.lock +62 -1
  2. package/docs/workspace-tools.md +196 -0
  3. package/examples/plugins/echo/README.md +3 -3
  4. package/knip.json +1 -0
  5. package/openapi.yaml +460 -128
  6. package/package.json +2 -1
  7. package/scripts/build-plugin-api.ts +299 -0
  8. package/src/__tests__/agent-loop-callsite-precedence.test.ts +7 -0
  9. package/src/__tests__/agent-loop-compaction-events.test.ts +197 -0
  10. package/src/__tests__/agent-loop-exit-reason.test.ts +93 -96
  11. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +2 -0
  12. package/src/__tests__/agent-loop-output-hooks.test.ts +274 -1
  13. package/src/__tests__/agent-loop-override-profile.test.ts +3 -0
  14. package/src/__tests__/agent-loop-provider-error-recording.test.ts +4 -0
  15. package/src/__tests__/agent-loop-thinking.test.ts +4 -0
  16. package/src/__tests__/agent-loop.test.ts +578 -5
  17. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +0 -1
  18. package/src/__tests__/approval-cascade.test.ts +1 -0
  19. package/src/__tests__/background-workers-disk-pressure.test.ts +0 -2
  20. package/src/__tests__/btw-routes.test.ts +0 -1
  21. package/src/__tests__/build-persisted-content.test.ts +75 -1
  22. package/src/__tests__/catalog-install-normalize.test.ts +141 -0
  23. package/src/__tests__/ces-startup-timeout.test.ts +60 -0
  24. package/src/__tests__/compaction-events.test.ts +1 -0
  25. package/src/__tests__/config-managed-gemini-defaults.test.ts +2 -46
  26. package/src/__tests__/context-overflow-reducer.test.ts +264 -124
  27. package/src/__tests__/context-window-manager-overflow-rung.test.ts +351 -0
  28. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  29. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +13 -5
  30. package/src/__tests__/conversation-agent-loop-overflow.test.ts +284 -455
  31. package/src/__tests__/conversation-agent-loop.test.ts +131 -551
  32. package/src/__tests__/conversation-app-control-instantiation.test.ts +13 -0
  33. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -0
  34. package/src/__tests__/conversation-fork-crud.test.ts +259 -0
  35. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  36. package/src/__tests__/conversation-lifecycle.test.ts +257 -1
  37. package/src/__tests__/conversation-process-callsite.test.ts +1 -0
  38. package/src/__tests__/conversation-provider-retry-repair.test.ts +38 -377
  39. package/src/__tests__/conversation-queue.test.ts +1 -39
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +119 -8
  41. package/src/__tests__/conversation-skill-tools.test.ts +491 -5
  42. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  43. package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
  44. package/src/__tests__/conversation-speed-override.test.ts +1 -0
  45. package/src/__tests__/conversation-store.test.ts +74 -0
  46. package/src/__tests__/conversation-surfaces-app-control.test.ts +4 -1
  47. package/src/__tests__/conversation-tool-setup-attribution.test.ts +323 -0
  48. package/src/__tests__/conversation-tool-setup-tools-disabled.test.ts +34 -0
  49. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
  50. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  51. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -0
  52. package/src/__tests__/corrected-target.test.ts +93 -0
  53. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -5
  54. package/src/__tests__/credential-execution-tools.test.ts +23 -11
  55. package/src/__tests__/credential-security-invariants.test.ts +6 -1
  56. package/src/__tests__/db-schedule-syntax-migration.test.ts +80 -0
  57. package/src/__tests__/device-id.test.ts +70 -1
  58. package/src/__tests__/embedding-managed-proxy-selection.test.ts +6 -40
  59. package/src/__tests__/empty-response-hook.test.ts +242 -66
  60. package/src/__tests__/external-plugin-loader.test.ts +0 -31
  61. package/src/__tests__/get-skill-detail-audit.test.ts +43 -1
  62. package/src/__tests__/guardian-routing-invariants.test.ts +91 -0
  63. package/src/__tests__/history-repair-hook.test.ts +228 -3
  64. package/src/__tests__/host-app-control-proxy.test.ts +45 -0
  65. package/src/__tests__/host-browser-proxy.test.ts +254 -9
  66. package/src/__tests__/identity-routes.test.ts +1 -0
  67. package/src/__tests__/image-recovery-hook.test.ts +387 -0
  68. package/src/__tests__/injector-chain.test.ts +5 -4
  69. package/src/__tests__/injector-v3-suppression.test.ts +373 -47
  70. package/src/__tests__/intent-routing.test.ts +7 -0
  71. package/src/__tests__/memory-retrieval-hook.test.ts +117 -15
  72. package/src/__tests__/notification-decision-strategy.test.ts +3 -3
  73. package/src/__tests__/oauth-store.test.ts +0 -85
  74. package/src/__tests__/{context-overflow-policy.test.ts → overflow-policy.test.ts} +1 -1
  75. package/src/__tests__/parallel-tool.benchmark.test.ts +4 -0
  76. package/src/__tests__/persist-unsendable-image-downscale.test.ts +29 -9
  77. package/src/__tests__/persist-unsendable-image.test.ts +4 -4
  78. package/src/__tests__/persistence-secret-redaction.test.ts +78 -0
  79. package/src/__tests__/plugin-bootstrap.test.ts +82 -73
  80. package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
  81. package/src/__tests__/plugin-types.test.ts +0 -8
  82. package/src/__tests__/provider-catalog-visibility.test.ts +1 -9
  83. package/src/__tests__/prune-old-conversations-job.test.ts +99 -0
  84. package/src/__tests__/registry.test.ts +240 -1
  85. package/src/__tests__/require-fresh-approval.test.ts +3 -0
  86. package/src/__tests__/schedule-routes.test.ts +116 -1
  87. package/src/__tests__/schedule-store.test.ts +28 -0
  88. package/src/__tests__/schedule-tools.test.ts +94 -1
  89. package/src/__tests__/server-history-render.test.ts +39 -0
  90. package/src/__tests__/skill-projection-feature-flag.test.ts +13 -0
  91. package/src/__tests__/skill-projection.benchmark.test.ts +25 -7
  92. package/src/__tests__/skills.test.ts +202 -0
  93. package/src/__tests__/slim-skill-category.test.ts +195 -0
  94. package/src/__tests__/strip-memory-injections.test.ts +33 -39
  95. package/src/__tests__/test-support/tool-invocation-seed.ts +79 -0
  96. package/src/__tests__/title-generate-hook.test.ts +9 -7
  97. package/src/__tests__/tool-audit-listener.test.ts +264 -1
  98. package/src/__tests__/tool-error-hook.test.ts +4 -3
  99. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
  100. package/src/__tests__/tool-executor-lifecycle-events.test.ts +273 -0
  101. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  102. package/src/__tests__/tool-start-timestamp.test.ts +218 -0
  103. package/src/__tests__/tools-get-route.test.ts +202 -0
  104. package/src/__tests__/workspace-tool-loader.test.ts +319 -0
  105. package/src/__tests__/workspace-tools-watcher-flag.test.ts +70 -0
  106. package/src/agent/loop.ts +569 -319
  107. package/src/api/events/tool-result.ts +9 -0
  108. package/src/api/events/tool-use-start.ts +7 -0
  109. package/src/api/index.ts +10 -0
  110. package/src/api/responses/conversation-message.ts +135 -27
  111. package/src/api/responses/memory-v3-selection-log.ts +4 -4
  112. package/src/approvals/guardian-request-resolvers.ts +26 -0
  113. package/src/browser-session/backends/host-bridge.ts +29 -0
  114. package/src/browser-session/index.ts +1 -0
  115. package/src/browser-session/types.ts +5 -1
  116. package/src/cli/commands/__tests__/schedules.test.ts +62 -4
  117. package/src/cli/commands/__tests__/skills.test.ts +53 -0
  118. package/src/cli/commands/channel-verification-sessions.ts +6 -6
  119. package/src/cli/commands/inference-providers.ts +0 -8
  120. package/src/cli/commands/plugins.ts +2 -2
  121. package/src/cli/commands/schedules.ts +27 -4
  122. package/src/cli/commands/skills.ts +187 -146
  123. package/src/cli/commands/tools.ts +106 -0
  124. package/src/cli/lib/__tests__/install-from-github.test.ts +256 -328
  125. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +6 -2
  126. package/src/cli/lib/__tests__/plugin-details.test.ts +10 -16
  127. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +2 -2
  128. package/src/cli/lib/__tests__/search-plugins.test.ts +145 -240
  129. package/src/cli/lib/install-from-github.ts +187 -117
  130. package/src/cli/lib/plugin-catalog-cache.ts +9 -9
  131. package/src/cli/lib/plugin-details.ts +38 -68
  132. package/src/cli/lib/plugin-marketplace.ts +42 -14
  133. package/src/cli/lib/search-plugins.ts +29 -129
  134. package/src/cli/program.ts +2 -0
  135. package/src/config/bundled-skills/acp/SKILL.md +1 -0
  136. package/src/config/bundled-skills/app-builder/SKILL.md +1 -0
  137. package/src/config/bundled-skills/app-control/SKILL.md +1 -0
  138. package/src/config/bundled-skills/computer-use/SKILL.md +1 -0
  139. package/src/config/bundled-skills/contacts/SKILL.md +1 -0
  140. package/src/config/bundled-skills/document-editor/SKILL.md +1 -0
  141. package/src/config/bundled-skills/followups/SKILL.md +1 -0
  142. package/src/config/bundled-skills/image-studio/SKILL.md +1 -0
  143. package/src/config/bundled-skills/media-processing/SKILL.md +1 -0
  144. package/src/config/bundled-skills/messaging/SKILL.md +1 -0
  145. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -0
  146. package/src/config/bundled-skills/playbooks/SKILL.md +1 -0
  147. package/src/config/bundled-skills/schedule/SKILL.md +1 -0
  148. package/src/config/bundled-skills/schedule/TOOLS.json +11 -3
  149. package/src/config/bundled-skills/sequences/SKILL.md +1 -0
  150. package/src/config/bundled-skills/settings/SKILL.md +1 -0
  151. package/src/config/bundled-skills/skill-management/SKILL.md +101 -1
  152. package/src/config/bundled-skills/subagent/SKILL.md +1 -0
  153. package/src/config/bundled-skills/transcribe/SKILL.md +1 -0
  154. package/src/config/env-registry.ts +23 -0
  155. package/src/config/feature-flag-registry.json +17 -81
  156. package/src/config/loader.ts +5 -22
  157. package/src/config/schema.ts +2 -0
  158. package/src/config/schemas/__tests__/compaction-logs.test.ts +56 -0
  159. package/src/config/schemas/__tests__/memory-v2.test.ts +0 -1
  160. package/src/config/schemas/__tests__/memory-v3.test.ts +61 -1
  161. package/src/config/schemas/compaction-logs.ts +79 -0
  162. package/src/config/schemas/memory-v2.ts +0 -8
  163. package/src/config/schemas/memory-v3.ts +104 -33
  164. package/src/config/seed-inference-profiles.ts +1 -1
  165. package/src/config/skills.ts +117 -47
  166. package/src/context/compactor.ts +11 -0
  167. package/src/context/strip-injections.ts +38 -4
  168. package/src/credential-execution/feature-gates.ts +0 -21
  169. package/src/credential-execution/startup-timeout.ts +32 -4
  170. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +18 -0
  171. package/src/daemon/conversation-agent-loop-handlers.ts +140 -91
  172. package/src/daemon/conversation-agent-loop.ts +123 -658
  173. package/src/daemon/conversation-error.ts +6 -33
  174. package/src/daemon/conversation-lifecycle.ts +1 -1
  175. package/src/daemon/conversation-runtime-assembly.ts +183 -22
  176. package/src/daemon/conversation-skill-tools.ts +137 -8
  177. package/src/daemon/conversation-slash.ts +0 -14
  178. package/src/daemon/conversation-store.ts +2 -19
  179. package/src/daemon/conversation-tool-setup.ts +87 -1
  180. package/src/daemon/conversation.ts +119 -50
  181. package/src/daemon/external-plugins-bootstrap.ts +36 -95
  182. package/src/daemon/handlers/config-channels.ts +11 -2
  183. package/src/daemon/handlers/shared.ts +9 -1
  184. package/src/daemon/handlers/skills.ts +10 -4
  185. package/src/daemon/host-app-control-proxy.ts +72 -57
  186. package/src/daemon/host-browser-proxy.ts +117 -22
  187. package/src/daemon/lifecycle.ts +34 -5
  188. package/src/daemon/message-protocol.ts +0 -7
  189. package/src/daemon/message-types/schedules.ts +1 -0
  190. package/src/daemon/message-types/skills.ts +17 -0
  191. package/src/daemon/providers-setup.ts +3 -0
  192. package/src/daemon/server.ts +3 -3
  193. package/src/daemon/tool-setup-types.ts +9 -3
  194. package/src/daemon/trust-context.ts +23 -0
  195. package/src/daemon/workspace-tools-watcher.ts +324 -0
  196. package/src/events/tool-audit-listener.ts +78 -16
  197. package/src/events/tool-metrics-listener.ts +2 -5
  198. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +227 -0
  199. package/src/memory/__tests__/conversation-queries.test.ts +176 -0
  200. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +20 -32
  201. package/src/memory/compaction-log-writer-clickhouse.ts +418 -0
  202. package/src/memory/conversation-crud.ts +134 -3
  203. package/src/memory/conversation-queries.ts +64 -7
  204. package/src/memory/db-init.ts +14 -0
  205. package/src/memory/embedding-backend.test.ts +130 -1
  206. package/src/memory/embedding-backend.ts +79 -106
  207. package/src/memory/embedding-gemini.ts +5 -0
  208. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +12 -0
  209. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +19 -0
  210. package/src/memory/graph/conversation-graph-memory.ts +36 -25
  211. package/src/memory/graph/tool-handlers.ts +3 -0
  212. package/src/memory/job-handlers/cleanup.ts +3 -1
  213. package/src/memory/jobs-store.ts +0 -28
  214. package/src/memory/jobs-worker.ts +10 -22
  215. package/src/memory/memory-marker.ts +29 -0
  216. package/src/memory/memory-retrospective-startup-cleanup.ts +1 -1
  217. package/src/memory/migrations/268-add-memory-v3-selections.ts +6 -0
  218. package/src/memory/migrations/270-schedule-description.ts +36 -0
  219. package/src/memory/migrations/275-tool-invocations-add-skill-id.test.ts +81 -0
  220. package/src/memory/migrations/275-tool-invocations-add-skill-id.ts +20 -0
  221. package/src/memory/migrations/276-tool-invocations-created-at-id-index.test.ts +68 -0
  222. package/src/memory/migrations/276-tool-invocations-created-at-id-index.ts +20 -0
  223. package/src/memory/migrations/277-add-memory-v3-ever-injected.ts +29 -0
  224. package/src/memory/migrations/278-tool-invocations-telemetry-columns.test.ts +96 -0
  225. package/src/memory/migrations/278-tool-invocations-telemetry-columns.ts +39 -0
  226. package/src/memory/migrations/279-create-skill-loaded-events.test.ts +84 -0
  227. package/src/memory/migrations/279-create-skill-loaded-events.ts +26 -0
  228. package/src/memory/migrations/280-conversations-surfaced-at.test.ts +88 -0
  229. package/src/memory/migrations/280-conversations-surfaced-at.ts +24 -0
  230. package/src/memory/migrations/index.ts +10 -0
  231. package/src/memory/migrations/registry.ts +8 -0
  232. package/src/memory/schema/conversations.ts +16 -0
  233. package/src/memory/schema/infrastructure.ts +26 -0
  234. package/src/memory/skill-loaded-events-store.test.ts +160 -0
  235. package/src/memory/skill-loaded-events-store.ts +95 -0
  236. package/src/memory/tool-executed-events-store.test.ts +219 -0
  237. package/src/memory/tool-executed-events-store.ts +102 -0
  238. package/src/memory/tool-usage-store.ts +15 -3
  239. package/src/memory/v2/__tests__/consolidation-job.test.ts +117 -12
  240. package/src/memory/v2/__tests__/consolidation-prompt-flag-gating-guard.test.ts +189 -0
  241. package/src/memory/v2/__tests__/injected-block-slugs.test.ts +90 -0
  242. package/src/memory/v2/__tests__/page-store.test.ts +33 -0
  243. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +88 -15
  244. package/src/memory/v2/activation-store.ts +50 -1
  245. package/src/memory/v2/consolidation-job.ts +113 -29
  246. package/src/memory/v2/injected-block-slugs.ts +79 -0
  247. package/src/memory/v2/injection.ts +6 -1
  248. package/src/memory/v2/prompts/consolidation.ts +414 -13
  249. package/src/memory/v2/router.ts +2 -28
  250. package/src/memory/v2/static-context.ts +1 -1
  251. package/src/memory/v2/types.ts +16 -0
  252. package/src/notifications/__tests__/copy-composer.test.ts +244 -0
  253. package/src/notifications/access-request-copy.ts +298 -0
  254. package/src/notifications/adapters/slack.ts +3 -3
  255. package/src/notifications/adapters/telegram.ts +2 -1
  256. package/src/notifications/copy-composer.ts +49 -267
  257. package/src/notifications/decision-engine.ts +16 -35
  258. package/src/notifications/home-feed-side-effect.ts +1 -6
  259. package/src/oauth/oauth-store.ts +0 -9
  260. package/src/permissions/checker.test.ts +83 -1
  261. package/src/permissions/checker.ts +25 -2
  262. package/src/permissions/gateway-threshold-reader.test.ts +182 -0
  263. package/src/permissions/gateway-threshold-reader.ts +80 -0
  264. package/src/platform/client.ts +1 -3
  265. package/src/platform/feature-gate.ts +3 -12
  266. package/src/plugin-api/constants.ts +4 -2
  267. package/src/plugin-api/index.ts +63 -11
  268. package/src/plugin-api/types.ts +236 -71
  269. package/src/plugins/defaults/compaction/compact.ts +66 -2
  270. package/src/plugins/defaults/compaction/context-overflow-reducer.ts +240 -32
  271. package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
  272. package/src/{daemon/context-overflow-policy.ts → plugins/defaults/compaction/overflow-policy.ts} +1 -1
  273. package/src/plugins/defaults/compaction/window-manager.ts +303 -1
  274. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +173 -0
  275. package/src/plugins/defaults/empty-response/hooks/stop.ts +11 -115
  276. package/src/plugins/defaults/empty-response/nudge-state-store.ts +46 -0
  277. package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +50 -0
  278. package/src/plugins/defaults/history-repair/hooks/stop.ts +22 -0
  279. package/src/plugins/defaults/history-repair/repair-state-store.ts +51 -0
  280. package/src/plugins/defaults/history-repair/terminal.ts +39 -2
  281. package/src/plugins/defaults/image-recovery/detect.ts +25 -0
  282. package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +73 -0
  283. package/src/plugins/defaults/image-recovery/hooks/stop.ts +22 -0
  284. package/src/plugins/defaults/image-recovery/image-recovery-state-store.ts +48 -0
  285. package/src/plugins/defaults/image-recovery/package.json +14 -0
  286. package/src/{daemon/persist-unsendable-image.ts → plugins/defaults/image-recovery/recover.ts} +67 -14
  287. package/src/plugins/defaults/index.ts +71 -5
  288. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +76 -112
  289. package/src/plugins/defaults/memory-retrieval/hooks/{user-prompt-submit-temp.ts → user-prompt-submit.ts} +83 -74
  290. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +14 -8
  291. package/src/plugins/defaults/memory-retrieval/injectors.ts +2 -18
  292. package/src/plugins/defaults/memory-retrieval/package.json +14 -0
  293. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1157 -0
  294. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +683 -0
  295. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +161 -140
  296. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +160 -0
  297. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +335 -316
  298. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +145 -53
  299. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +38 -1
  300. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +18 -8
  301. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +112 -71
  302. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +192 -51
  303. package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +4 -16
  304. package/src/plugins/defaults/memory-v3-shadow/card.test.ts +173 -0
  305. package/src/plugins/defaults/memory-v3-shadow/card.ts +116 -0
  306. package/src/plugins/defaults/memory-v3-shadow/core-set.test.ts +104 -0
  307. package/src/plugins/defaults/memory-v3-shadow/core-set.ts +59 -0
  308. package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.test.ts +305 -0
  309. package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.ts +278 -0
  310. package/src/plugins/defaults/memory-v3-shadow/hot-set.test.ts +138 -0
  311. package/src/plugins/defaults/memory-v3-shadow/hot-set.ts +85 -0
  312. package/src/plugins/defaults/memory-v3-shadow/injector.ts +331 -24
  313. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +119 -13
  314. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +169 -114
  315. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +47 -16
  316. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +144 -66
  317. package/src/plugins/defaults/memory-v3-shadow/prune.test.ts +758 -0
  318. package/src/plugins/defaults/memory-v3-shadow/prune.ts +471 -0
  319. package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +68 -16
  320. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +19 -12
  321. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +96 -43
  322. package/src/plugins/defaults/memory-v3-shadow/types.ts +34 -17
  323. package/src/plugins/defaults/title-generate/hooks/stop.ts +9 -11
  324. package/src/plugins/pipeline.ts +8 -5
  325. package/src/plugins/types.ts +5 -51
  326. package/src/providers/cache-control.ts +26 -0
  327. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +1 -2
  328. package/src/providers/model-catalog.ts +13 -1
  329. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +147 -0
  330. package/src/providers/openai/chat-completions-provider.ts +46 -0
  331. package/src/providers/openai/responses-provider.ts +45 -0
  332. package/src/providers/registry.ts +0 -8
  333. package/src/runtime/__tests__/agent-wake.test.ts +0 -1
  334. package/src/runtime/agent-wake.ts +13 -0
  335. package/src/runtime/routes/__tests__/acp-routes.test.ts +151 -0
  336. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +12 -50
  337. package/src/runtime/routes/__tests__/conversation-surface-routes.test.ts +322 -0
  338. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +0 -62
  339. package/src/runtime/routes/__tests__/plugins-routes.test.ts +31 -11
  340. package/src/runtime/routes/acp-routes.test.ts +106 -0
  341. package/src/runtime/routes/acp-routes.ts +248 -2
  342. package/src/runtime/routes/browser-tabs-routes.ts +1 -1
  343. package/src/runtime/routes/channel-verification-routes.ts +14 -5
  344. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +0 -14
  345. package/src/runtime/routes/consolidation-routes.ts +6 -82
  346. package/src/runtime/routes/conversation-list-routes.ts +6 -0
  347. package/src/runtime/routes/conversation-management-routes.ts +71 -0
  348. package/src/runtime/routes/identity-routes.ts +8 -0
  349. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
  350. package/src/runtime/routes/inference-provider-connection-routes.ts +0 -45
  351. package/src/runtime/routes/plugins-routes.ts +34 -45
  352. package/src/runtime/routes/schedule-routes.ts +43 -5
  353. package/src/runtime/routes/settings-routes.ts +140 -15
  354. package/src/runtime/routes/skills-routes.ts +18 -6
  355. package/src/runtime/services/__tests__/conversation-serializer.test.ts +140 -0
  356. package/src/runtime/services/conversation-serializer.ts +38 -1
  357. package/src/runtime/verification-outbound-actions.ts +147 -2
  358. package/src/runtime/verification-templates.ts +29 -3
  359. package/src/schedule/schedule-store.ts +19 -0
  360. package/src/skills/catalog-install.ts +77 -13
  361. package/src/tasks/task-scheduler.ts +1 -0
  362. package/src/telemetry/types.ts +66 -1
  363. package/src/telemetry/usage-telemetry-reporter.test.ts +542 -13
  364. package/src/telemetry/usage-telemetry-reporter.ts +213 -20
  365. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +49 -2
  366. package/src/tools/browser/__tests__/browser-status.test.ts +29 -5
  367. package/src/tools/browser/browser-execution.ts +27 -9
  368. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +380 -4
  369. package/src/tools/browser/cdp-client/__tests__/host-bridge-cdp-client.test.ts +107 -0
  370. package/src/tools/browser/cdp-client/__tests__/types.test.ts +6 -1
  371. package/src/tools/browser/cdp-client/factory.ts +217 -17
  372. package/src/tools/browser/cdp-client/host-bridge-cdp-client.ts +67 -0
  373. package/src/tools/browser/cdp-client/types.ts +22 -2
  374. package/src/tools/credential-execution/make-authenticated-request.ts +2 -1
  375. package/src/tools/credential-execution/manage-secure-command-tool.ts +169 -164
  376. package/src/tools/credential-execution/run-authenticated-command.ts +2 -1
  377. package/src/tools/executor.ts +39 -7
  378. package/src/tools/registry.ts +387 -5
  379. package/src/tools/schedule/create.ts +16 -0
  380. package/src/tools/schedule/list.ts +12 -4
  381. package/src/tools/schedule/update.ts +12 -0
  382. package/src/tools/skills/load.ts +11 -6
  383. package/src/tools/terminal/safe-env.ts +2 -0
  384. package/src/tools/types.ts +65 -9
  385. package/src/tools/workspace-tools/loader.ts +673 -0
  386. package/src/usage/attribution.ts +28 -0
  387. package/src/util/device-id.ts +17 -3
  388. package/src/util/platform.ts +16 -0
  389. package/tsconfig.plugin-api.json +13 -0
  390. package/src/__tests__/plugin-external-api.test.ts +0 -68
  391. package/src/__tests__/plugin-skill-contribution.test.ts +0 -355
  392. package/src/daemon/message-types/browser.ts +0 -10
  393. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
  394. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/eval-turns.json +0 -36
  395. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/live-turns.json +0 -37
  396. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-eviction.test.ts +0 -106
  397. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-skeleton.test.ts +0 -44
  398. package/src/plugins/defaults/memory-v3-shadow/working-set.ts +0 -91
  399. package/src/plugins/external-api.ts +0 -114
  400. package/src/plugins/plugin-skill-contributions.ts +0 -292
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Tests for the notification copy-composer — specifically the fallback
3
+ * path that composeFallbackCopy uses when the LLM is unavailable, and
4
+ * the shared deriveTitle utility.
5
+ */
6
+
7
+ import { describe, expect, test } from "bun:test";
8
+
9
+ import { composeFallbackCopy, deriveTitle } from "../copy-composer.js";
10
+ import type { NotificationSignal } from "../signal.js";
11
+ import type { NotificationChannel } from "../types.js";
12
+
13
+ // ── Helpers ───────────────────────────────────────────────────────────
14
+
15
+ function makeSignal(
16
+ overrides?: Partial<NotificationSignal>,
17
+ ): NotificationSignal {
18
+ return {
19
+ signalId: "sig-copy-test-1",
20
+ createdAt: Date.now(),
21
+ sourceChannel: "scheduler",
22
+ sourceContextId: "ctx-1",
23
+ sourceEventName: "user.send_notification",
24
+ contextPayload: {},
25
+ attentionHints: {
26
+ requiresAction: false,
27
+ urgency: "low",
28
+ isAsyncBackground: false,
29
+ visibleInSourceNow: false,
30
+ },
31
+ ...overrides,
32
+ };
33
+ }
34
+
35
+ const CHANNELS: NotificationChannel[] = ["vellum" as NotificationChannel];
36
+
37
+ // ── composeFallbackCopy with requestedMessage ─────────────────────────
38
+
39
+ describe("composeFallbackCopy honors requestedMessage / requestedTitle", () => {
40
+ test("uses requestedMessage as body and requestedTitle as title", () => {
41
+ const signal = makeSignal({
42
+ contextPayload: {
43
+ requestedMessage: "Take out the trash",
44
+ requestedTitle: "Household Reminder",
45
+ },
46
+ });
47
+ const copy = composeFallbackCopy(signal, CHANNELS);
48
+
49
+ expect(copy.vellum?.body).toBe("Take out the trash");
50
+ expect(copy.vellum?.title).toBe("Household Reminder");
51
+ expect(copy.vellum?.conversationSeedMessage).toBe("Take out the trash");
52
+ });
53
+
54
+ test("derives title from body when requestedTitle is absent", () => {
55
+ const signal = makeSignal({
56
+ contextPayload: {
57
+ requestedMessage: "First sentence. Second sentence follows.",
58
+ },
59
+ });
60
+ const copy = composeFallbackCopy(signal, CHANNELS);
61
+
62
+ expect(copy.vellum?.body).toBe("First sentence. Second sentence follows.");
63
+ expect(copy.vellum?.title).toBe("First sentence.");
64
+ });
65
+
66
+ test("derives title from body when requestedTitle is empty string", () => {
67
+ const signal = makeSignal({
68
+ contextPayload: {
69
+ requestedMessage: "Some message body here",
70
+ requestedTitle: "",
71
+ },
72
+ });
73
+ const copy = composeFallbackCopy(signal, CHANNELS);
74
+
75
+ expect(copy.vellum?.body).toBe("Some message body here");
76
+ expect(copy.vellum?.title).toBe("Some message body here");
77
+ });
78
+
79
+ test("derives title from body when requestedTitle is whitespace", () => {
80
+ const signal = makeSignal({
81
+ contextPayload: {
82
+ requestedMessage: "Whitespace title test",
83
+ requestedTitle: " ",
84
+ },
85
+ });
86
+ const copy = composeFallbackCopy(signal, CHANNELS);
87
+
88
+ expect(copy.vellum?.body).toBe("Whitespace title test");
89
+ expect(copy.vellum?.title).toBe("Whitespace title test");
90
+ });
91
+
92
+ test("trims whitespace from requestedMessage", () => {
93
+ const signal = makeSignal({
94
+ contextPayload: {
95
+ requestedMessage: " padded message ",
96
+ requestedTitle: " padded title ",
97
+ },
98
+ });
99
+ const copy = composeFallbackCopy(signal, CHANNELS);
100
+
101
+ expect(copy.vellum?.body).toBe("padded message");
102
+ expect(copy.vellum?.title).toBe("padded title");
103
+ });
104
+
105
+ test("populates copy for all requested channels", () => {
106
+ const channels = [
107
+ "vellum" as NotificationChannel,
108
+ "telegram" as NotificationChannel,
109
+ ];
110
+ const signal = makeSignal({
111
+ contextPayload: {
112
+ requestedMessage: "Multi-channel test",
113
+ requestedTitle: "Multi Title",
114
+ },
115
+ });
116
+ const copy = composeFallbackCopy(signal, channels);
117
+
118
+ expect(copy.vellum?.body).toBe("Multi-channel test");
119
+ expect(copy.vellum?.title).toBe("Multi Title");
120
+ expect(copy.telegram?.body).toBe("Multi-channel test");
121
+ expect(copy.telegram?.title).toBe("Multi Title");
122
+ });
123
+
124
+ test("falls through to template when requestedMessage is absent", () => {
125
+ const signal = makeSignal({
126
+ sourceEventName: "schedule.notify",
127
+ contextPayload: {
128
+ message: "Standup time",
129
+ label: "Daily Standup",
130
+ },
131
+ });
132
+ const copy = composeFallbackCopy(signal, CHANNELS);
133
+
134
+ expect(copy.vellum?.title).toBe("Daily Standup");
135
+ expect(copy.vellum?.body).toBe("Standup time");
136
+ });
137
+
138
+ test("falls through to template when requestedMessage is empty string", () => {
139
+ const signal = makeSignal({
140
+ sourceEventName: "schedule.notify",
141
+ contextPayload: {
142
+ requestedMessage: "",
143
+ message: "Standup time",
144
+ },
145
+ });
146
+ const copy = composeFallbackCopy(signal, CHANNELS);
147
+
148
+ expect(copy.vellum?.title).toBe("Reminder");
149
+ expect(copy.vellum?.body).toBe("Standup time");
150
+ });
151
+
152
+ test("falls through to template when requestedMessage is whitespace", () => {
153
+ const signal = makeSignal({
154
+ sourceEventName: "schedule.notify",
155
+ contextPayload: {
156
+ requestedMessage: " ",
157
+ message: "Standup time",
158
+ },
159
+ });
160
+ const copy = composeFallbackCopy(signal, CHANNELS);
161
+
162
+ expect(copy.vellum?.title).toBe("Reminder");
163
+ expect(copy.vellum?.body).toBe("Standup time");
164
+ });
165
+
166
+ test("falls through to generic copy when no requestedMessage and no template match", () => {
167
+ const signal = makeSignal({
168
+ sourceEventName: "unknown.event",
169
+ contextPayload: {},
170
+ });
171
+ const copy = composeFallbackCopy(signal, CHANNELS);
172
+
173
+ expect(copy.vellum?.title).toBe("Notification");
174
+ expect(copy.vellum?.body).toBe("");
175
+ });
176
+
177
+ test("requestedMessage takes priority over event-name template", () => {
178
+ const signal = makeSignal({
179
+ sourceEventName: "schedule.notify",
180
+ contextPayload: {
181
+ requestedMessage: "User-supplied content",
182
+ requestedTitle: "User Title",
183
+ message: "Template message field",
184
+ label: "Template label field",
185
+ },
186
+ });
187
+ const copy = composeFallbackCopy(signal, CHANNELS);
188
+
189
+ expect(copy.vellum?.body).toBe("User-supplied content");
190
+ expect(copy.vellum?.title).toBe("User Title");
191
+ });
192
+
193
+ test("works with non-assistant_tool source channels", () => {
194
+ for (const sourceChannel of ["scheduler", "watcher", "slack"] as const) {
195
+ const signal = makeSignal({
196
+ sourceChannel,
197
+ contextPayload: {
198
+ requestedMessage: `Message from ${sourceChannel}`,
199
+ },
200
+ });
201
+ const copy = composeFallbackCopy(signal, CHANNELS);
202
+
203
+ expect(copy.vellum?.body).toBe(`Message from ${sourceChannel}`);
204
+ }
205
+ });
206
+ });
207
+
208
+ // ── deriveTitle ───────────────────────────────────────────────────────
209
+
210
+ describe("deriveTitle", () => {
211
+ test("extracts first sentence when period is present", () => {
212
+ expect(deriveTitle("First sentence. Second sentence.")).toBe(
213
+ "First sentence.",
214
+ );
215
+ });
216
+
217
+ test("extracts first sentence on exclamation mark", () => {
218
+ expect(deriveTitle("Alert! More details follow.")).toBe("Alert!");
219
+ });
220
+
221
+ test("extracts first sentence on question mark", () => {
222
+ expect(deriveTitle("Ready? Let me know.")).toBe("Ready?");
223
+ });
224
+
225
+ test("uses full body when no sentence terminator", () => {
226
+ expect(deriveTitle("No terminator here")).toBe("No terminator here");
227
+ });
228
+
229
+ test("truncates to 60 characters with ellipsis", () => {
230
+ const long = "A".repeat(80);
231
+ const result = deriveTitle(long);
232
+ expect(result.length).toBeLessThanOrEqual(61); // 60 + ellipsis char
233
+ expect(result.endsWith("\u2026")).toBe(true);
234
+ });
235
+
236
+ test("does not truncate at exactly 60 characters", () => {
237
+ const exact = "A".repeat(60);
238
+ expect(deriveTitle(exact)).toBe(exact);
239
+ });
240
+
241
+ test("trims whitespace", () => {
242
+ expect(deriveTitle(" trimmed ")).toBe("trimmed");
243
+ });
244
+ });
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Deterministic helpers for building guardian-facing access-request copy.
3
+ *
4
+ * Used by the fallback template in copy-composer and the decision-engine
5
+ * post-generation enforcement to ensure required directives always appear.
6
+ */
7
+
8
+ // ── Local string utilities ──────────────────────────────────────────────────
9
+ //
10
+ // Tiny helpers duplicated from copy-composer to keep this module
11
+ // dependency-free (avoiding a circular import with copy-composer, which
12
+ // imports access-request helpers for its templates).
13
+
14
+ function str(value: unknown, fallback: string): string {
15
+ if (typeof value === "string" && value.length > 0) return value;
16
+ return fallback;
17
+ }
18
+
19
+ function nonEmpty(value: string | undefined): string | undefined {
20
+ if (typeof value !== "string") return undefined;
21
+ const trimmed = value.trim();
22
+ return trimmed.length > 0 ? trimmed : undefined;
23
+ }
24
+
25
+ // ── Identity sanitization ───────────────────────────────────────────────────
26
+
27
+ const IDENTITY_FIELD_MAX_LENGTH = 120;
28
+
29
+ /**
30
+ * Sanitize an untrusted identity field for inclusion in notification copy.
31
+ *
32
+ * - Strips control characters (U+0000–U+001F, U+007F–U+009F) and newlines.
33
+ * - Clamps to IDENTITY_FIELD_MAX_LENGTH characters.
34
+ * - Wraps in quotes to neutralize instruction-like payload text.
35
+ */
36
+ export function sanitizeIdentityField(value: string): string {
37
+ const stripped = value.replace(/[\x00-\x1f\x7f-\x9f\r\n]+/g, " ").trim();
38
+ const clamped =
39
+ stripped.length > IDENTITY_FIELD_MAX_LENGTH
40
+ ? stripped.slice(0, IDENTITY_FIELD_MAX_LENGTH) + "…"
41
+ : stripped;
42
+ return clamped;
43
+ }
44
+
45
+ export function buildAccessRequestIdentityLine(
46
+ payload: Record<string, unknown>,
47
+ ): string {
48
+ const requester = sanitizeIdentityField(
49
+ str(payload.senderIdentifier, "Someone"),
50
+ );
51
+ const sourceChannel =
52
+ typeof payload.sourceChannel === "string"
53
+ ? payload.sourceChannel
54
+ : undefined;
55
+ const callerName = nonEmpty(
56
+ typeof payload.actorDisplayName === "string"
57
+ ? payload.actorDisplayName
58
+ : undefined,
59
+ );
60
+ const actorUsername = nonEmpty(
61
+ typeof payload.actorUsername === "string"
62
+ ? payload.actorUsername
63
+ : undefined,
64
+ );
65
+ const actorExternalId = nonEmpty(
66
+ typeof payload.actorExternalId === "string"
67
+ ? payload.actorExternalId
68
+ : undefined,
69
+ );
70
+
71
+ if (sourceChannel === "phone" && callerName) {
72
+ const safeName = sanitizeIdentityField(callerName);
73
+ const safeId = sanitizeIdentityField(
74
+ str(payload.actorExternalId, requester),
75
+ );
76
+ return `${safeName} (${safeId}) is calling and requesting access to the assistant.`;
77
+ }
78
+
79
+ // Sanitize before comparing to avoid deduplication failures when identity
80
+ // fields contain control characters that are stripped from `requester`.
81
+ const sanitizedUsername = actorUsername
82
+ ? sanitizeIdentityField(actorUsername)
83
+ : undefined;
84
+ const sanitizedExternalId = actorExternalId
85
+ ? sanitizeIdentityField(actorExternalId)
86
+ : undefined;
87
+ // When the requester is a raw Slack user ID (e.g. the fallback path in
88
+ // access-request-helper sets senderIdentifier to the raw actorExternalId),
89
+ // format it as a Slack mention so it renders as a clickable display name.
90
+ const formattedRequester =
91
+ sourceChannel === "slack" && /^U[A-Z0-9]+$/i.test(requester)
92
+ ? `<@${requester}>`
93
+ : requester;
94
+ const parts = [formattedRequester];
95
+ if (sanitizedUsername && sanitizedUsername !== requester) {
96
+ parts.push(`@${sanitizedUsername}`);
97
+ }
98
+ if (
99
+ sanitizedExternalId &&
100
+ sanitizedExternalId !== requester &&
101
+ sanitizedExternalId !== sanitizedUsername
102
+ ) {
103
+ // For Slack, use the <@U...> mention format so Slack auto-renders
104
+ // the user ID as a clickable display name.
105
+ const formattedId =
106
+ sourceChannel === "slack" && /^U[A-Z0-9]+$/i.test(sanitizedExternalId)
107
+ ? `<@${sanitizedExternalId}>`
108
+ : `[${sanitizedExternalId}]`;
109
+ parts.push(formattedId);
110
+ }
111
+ if (sourceChannel) {
112
+ parts.push(`via ${sourceChannel}`);
113
+ }
114
+
115
+ return `${parts.join(" ")} is requesting access to the assistant.`;
116
+ }
117
+
118
+ // ── Message preview ─────────────────────────────────────────────────────────
119
+
120
+ export const MESSAGE_PREVIEW_MAX_LENGTH = 200;
121
+
122
+ /**
123
+ * Sanitize an untrusted message preview for inclusion in notification copy.
124
+ *
125
+ * Like {@link sanitizeIdentityField} but uses the higher
126
+ * MESSAGE_PREVIEW_MAX_LENGTH limit (200 chars) instead of the identity
127
+ * field limit (120 chars).
128
+ */
129
+ export function sanitizeMessagePreview(value: string): string {
130
+ const stripped = value.replace(/[\x00-\x1f\x7f-\x9f\r\n]+/g, " ").trim();
131
+ const clamped =
132
+ stripped.length > MESSAGE_PREVIEW_MAX_LENGTH
133
+ ? stripped.slice(0, MESSAGE_PREVIEW_MAX_LENGTH) + "…"
134
+ : stripped;
135
+ return clamped;
136
+ }
137
+
138
+ /**
139
+ * Build a quoted preview of the requester's original message for inclusion
140
+ * in guardian-facing access-request copy. Sanitizes and truncates to keep
141
+ * the notification concise.
142
+ *
143
+ * Returns `undefined` when no usable preview is available.
144
+ */
145
+ function buildAccessRequestMessagePreview(
146
+ payload: Record<string, unknown>,
147
+ ): string | undefined {
148
+ const raw =
149
+ typeof payload.messagePreview === "string"
150
+ ? payload.messagePreview
151
+ : undefined;
152
+ if (!raw) return undefined;
153
+
154
+ const sanitized = sanitizeMessagePreview(raw);
155
+ if (sanitized.length === 0) return undefined;
156
+
157
+ return `> Their message: "${sanitized}"`;
158
+ }
159
+
160
+ // ── Directives ──────────────────────────────────────────────────────────────
161
+
162
+ export function buildAccessRequestInviteDirective(): string {
163
+ return 'Reply "open invite flow" to start Trusted Contacts invite flow.';
164
+ }
165
+
166
+ /**
167
+ * Normalize text before running directive-matching regexes.
168
+ *
169
+ * - Replaces smart/curly apostrophes (\u2018, \u2019, \u201B) with ASCII `'`
170
+ * so contractions like "Don\u2019t" are matched by the `n't` lookbehind.
171
+ * - Collapses runs of whitespace into a single space so "Do not reply"
172
+ * is matched by the single-space negative lookbehind.
173
+ * - Trims leading/trailing whitespace.
174
+ */
175
+ export function normalizeForDirectiveMatching(text: string): string {
176
+ return text
177
+ .replace(/[\u2018\u2019\u201B]/g, "'")
178
+ .replace(/\s+/g, " ")
179
+ .trim();
180
+ }
181
+
182
+ /**
183
+ * Check whether a text contains the required access-request instruction elements:
184
+ * 1. Approve directive: Reply "CODE approve"
185
+ * 2. Reject directive: Reply "CODE reject"
186
+ * 3. Invite directive: Reply "open invite flow"
187
+ *
188
+ * Each directive is matched independently using negative lookbehind to reject
189
+ * matches preceded by negation words ("not", "n't", "never"). This prevents
190
+ * contradictory copy like `Do not reply "CODE reject"` from satisfying the
191
+ * check even when a positive approve directive exists nearby.
192
+ *
193
+ * The text is normalized before matching to handle smart apostrophes and
194
+ * multiple whitespace characters that would otherwise bypass negation detection.
195
+ */
196
+ export function hasAccessRequestInstructions(
197
+ text: string | undefined,
198
+ requestCode: string,
199
+ ): boolean {
200
+ if (typeof text !== "string") return false;
201
+ const normalized = normalizeForDirectiveMatching(text);
202
+ const escapedCode = requestCode.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
203
+ // Each directive must follow "reply" without a preceding negation word.
204
+ // Negative lookbehinds reject "do not reply", "don't reply", "never reply".
205
+ const approveRe = new RegExp(
206
+ `(?<!not\\s)(?<!n't\\s)(?<!never\\s)reply\\b[^.!?\\n]*?"${escapedCode}\\s+approve"`,
207
+ "i",
208
+ );
209
+ const rejectRe = new RegExp(
210
+ `(?<!not\\s)(?<!n't\\s)(?<!never\\s)reply\\b[^.!?\\n]*?"${escapedCode}\\s+reject"`,
211
+ "i",
212
+ );
213
+ const inviteRe =
214
+ /(?<!not\s)(?<!n't\s)(?<!never\s)reply\b[^.!?\n]*?"open invite flow"/i;
215
+
216
+ return (
217
+ approveRe.test(normalized) &&
218
+ rejectRe.test(normalized) &&
219
+ inviteRe.test(normalized)
220
+ );
221
+ }
222
+
223
+ /**
224
+ * Check whether text contains the invite-flow directive ("open invite flow")
225
+ * using the same normalized negative-lookbehind pattern as the full check.
226
+ * This is used for enforcement when requestCode is absent but the invite
227
+ * directive should still be present.
228
+ */
229
+ export function hasInviteFlowDirective(text: string | undefined): boolean {
230
+ if (typeof text !== "string") return false;
231
+ const normalized = normalizeForDirectiveMatching(text);
232
+ const inviteRe =
233
+ /(?<!not\s)(?<!n't\s)(?<!never\s)reply\b[^.!?\n]*?"open invite flow"/i;
234
+ return inviteRe.test(normalized);
235
+ }
236
+
237
+ // ── Contract text builder ───────────────────────────────────────────────────
238
+
239
+ /**
240
+ * Build the deterministic access-request contract text from payload fields.
241
+ * This is the canonical baseline that enforcement can append when generated
242
+ * copy is missing required elements.
243
+ *
244
+ * Channel-agnostic by design: this function reads from the generic
245
+ * `contextPayload` and works identically regardless of which channel
246
+ * (Slack, Telegram, desktop, etc.) the notification is delivered to.
247
+ * When `guardianResolutionSource` is present and not `"source-channel-contact"`,
248
+ * the guardian was resolved via fallback (e.g. vellum anchor) rather than
249
+ * a verified same-channel contact — downstream copy or routing can use
250
+ * this to append verification CTAs like "Was this you?".
251
+ */
252
+ export function buildAccessRequestContractText(
253
+ payload: Record<string, unknown>,
254
+ ): string {
255
+ const requestCode = nonEmpty(
256
+ typeof payload.requestCode === "string" ? payload.requestCode : undefined,
257
+ );
258
+ const previousMemberStatus =
259
+ typeof payload.previousMemberStatus === "string"
260
+ ? payload.previousMemberStatus
261
+ : undefined;
262
+
263
+ const guardianResolutionSource =
264
+ typeof payload.guardianResolutionSource === "string"
265
+ ? payload.guardianResolutionSource
266
+ : undefined;
267
+ const sourceChannel =
268
+ typeof payload.sourceChannel === "string"
269
+ ? payload.sourceChannel
270
+ : undefined;
271
+
272
+ const lines: string[] = [];
273
+ lines.push(buildAccessRequestIdentityLine(payload));
274
+ const preview = buildAccessRequestMessagePreview(payload);
275
+ if (preview) {
276
+ lines.push(preview);
277
+ }
278
+ if (previousMemberStatus === "revoked") {
279
+ lines.push("Note: this user was previously revoked.");
280
+ }
281
+ if (requestCode) {
282
+ const code = requestCode.toUpperCase();
283
+ lines.push(
284
+ `Reply "${code} approve" to grant access or "${code} reject" to deny.`,
285
+ );
286
+ }
287
+ lines.push(buildAccessRequestInviteDirective());
288
+ if (
289
+ (guardianResolutionSource === "vellum-anchor" ||
290
+ guardianResolutionSource === "none") &&
291
+ sourceChannel
292
+ ) {
293
+ lines.push(
294
+ `Note: 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.`,
295
+ );
296
+ }
297
+ return lines.join("\n");
298
+ }
@@ -5,13 +5,13 @@
5
5
 
6
6
  import { sendSlackReply } from "../../messaging/providers/slack/send.js";
7
7
  import { getLogger } from "../../util/logger.js";
8
- import { isConversationSeedSane } from "../conversation-seed-composer.js";
9
8
  import {
10
9
  buildAccessRequestIdentityLine,
11
10
  buildAccessRequestInviteDirective,
12
- nonEmpty,
13
11
  sanitizeIdentityField,
14
- } from "../copy-composer.js";
12
+ } from "../access-request-copy.js";
13
+ import { isConversationSeedSane } from "../conversation-seed-composer.js";
14
+ import { nonEmpty } from "../copy-composer.js";
15
15
  import type {
16
16
  ChannelAdapter,
17
17
  ChannelDeliveryPayload,
@@ -12,8 +12,9 @@ import { sendTelegramReply } from "../../messaging/providers/telegram-bot/send.j
12
12
  import type { ApprovalUIMetadata } from "../../runtime/channel-approval-types.js";
13
13
  import { ConfigError } from "../../util/errors.js";
14
14
  import { getLogger } from "../../util/logger.js";
15
+ import { buildAccessRequestContractText } from "../access-request-copy.js";
15
16
  import { isConversationSeedSane } from "../conversation-seed-composer.js";
16
- import { buildAccessRequestContractText, nonEmpty } from "../copy-composer.js";
17
+ import { nonEmpty } from "../copy-composer.js";
17
18
  import type {
18
19
  ChannelAdapter,
19
20
  ChannelDeliveryPayload,