@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
@@ -0,0 +1,442 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // Silence the logger everywhere this graph reaches.
4
+ const realLogger = await import("../../util/logger.js");
5
+ mock.module("../../util/logger.js", () => ({
6
+ ...realLogger,
7
+ getLogger: () =>
8
+ new Proxy({} as Record<string, unknown>, {
9
+ get: () => () => {},
10
+ }),
11
+ }));
12
+
13
+ // ── Mutable mock state ────────────────────────────────────────────────
14
+ // `flagEnabled` toggles the `workflows` feature flag; `configThrows`
15
+ // simulates config not yet loaded (test-setup race). The run-manager mock
16
+ // records the args of the last `start()` call for assertion.
17
+
18
+ let flagEnabled = true;
19
+ let configThrows = false;
20
+
21
+ const realLoader = await import("../../config/loader.js");
22
+ mock.module("../../config/loader.js", () => ({
23
+ ...realLoader,
24
+ getConfig: () => {
25
+ if (configThrows) throw new Error("config not loaded");
26
+ return {} as ReturnType<typeof realLoader.getConfig>;
27
+ },
28
+ // manage_workflows `list_profiles` reads profiles via loadConfig().
29
+ loadConfig: () =>
30
+ ({
31
+ llm: {
32
+ profiles: { "cost-optimized": {}, balanced: {} },
33
+ activeProfile: "balanced",
34
+ },
35
+ }) as unknown as ReturnType<typeof realLoader.loadConfig>,
36
+ }));
37
+
38
+ const realFlags = await import("../../config/assistant-feature-flags.js");
39
+ mock.module("../../config/assistant-feature-flags.js", () => ({
40
+ ...realFlags,
41
+ isAssistantFeatureFlagEnabled: (key: string) =>
42
+ key === "workflows" ? flagEnabled : false,
43
+ }));
44
+
45
+ // No live conversation in tests — the tool falls back to a synthetic trust
46
+ // context built from the tool context's trustClass.
47
+ const realRegistry = await import("../../daemon/conversation-registry.js");
48
+ mock.module("../../daemon/conversation-registry.js", () => ({
49
+ ...realRegistry,
50
+ findConversation: () => undefined,
51
+ }));
52
+
53
+ let lastStartArgs: Record<string, unknown> | null = null;
54
+ let startThrows: Error | null = null;
55
+ const startMock = mock((opts: Record<string, unknown>) => {
56
+ if (startThrows) throw startThrows;
57
+ lastStartArgs = opts;
58
+ return { runId: "run-123" };
59
+ });
60
+ const statusMock = mock(() => null as unknown);
61
+ const abortMock = mock(() => {});
62
+ const listMock = mock(() => [] as unknown[]);
63
+ let resumeThrows: Error | null = null;
64
+ const resumeMock = mock((runId: string) => {
65
+ if (resumeThrows) throw resumeThrows;
66
+ return { runId };
67
+ });
68
+
69
+ mock.module("../../workflows/run-manager.js", () => ({
70
+ getWorkflowRunManager: () => ({
71
+ start: startMock,
72
+ status: statusMock,
73
+ abort: abortMock,
74
+ list: listMock,
75
+ resume: resumeMock,
76
+ }),
77
+ }));
78
+
79
+ // Imports AFTER mocks so the mocked modules are picked up.
80
+ const { executeRunWorkflow } = await import("./run-workflow.js");
81
+ const { executeManageWorkflows } = await import("./manage-workflows.js");
82
+ const { WORKFLOW_READONLY_BASELINE } =
83
+ await import("../../workflows/capabilities.js");
84
+
85
+ // Minimal tool context — only the fields the workflow tools read.
86
+ function makeContext(): Parameters<typeof executeRunWorkflow>[1] {
87
+ return {
88
+ conversationId: "conv-1",
89
+ workingDir: "/tmp",
90
+ trustClass: "guardian",
91
+ } as Parameters<typeof executeRunWorkflow>[1];
92
+ }
93
+
94
+ beforeEach(() => {
95
+ flagEnabled = true;
96
+ configThrows = false;
97
+ startThrows = null;
98
+ lastStartArgs = null;
99
+ resumeThrows = null;
100
+ startMock.mockClear();
101
+ statusMock.mockClear();
102
+ abortMock.mockClear();
103
+ listMock.mockClear();
104
+ resumeMock.mockClear();
105
+ });
106
+
107
+ describe("workflow tools are served by the workflows skill", () => {
108
+ // The cutover moved run_workflow / manage_workflows out of the always-on tool
109
+ // manifest into the flag-gated `workflows` bundled skill (loaded via
110
+ // skill_load, invoked via skill_execute). The skill manifest is the
111
+ // source of truth; assert it declares both as in-process host tools.
112
+ test("the workflows skill TOOLS.json declares both host-executed tools", async () => {
113
+ const { readFileSync } = await import("node:fs");
114
+ const { join } = await import("node:path");
115
+ const manifest = JSON.parse(
116
+ readFileSync(
117
+ join(
118
+ import.meta.dir,
119
+ "../../config/bundled-skills/workflows/TOOLS.json",
120
+ ),
121
+ "utf-8",
122
+ ),
123
+ ) as {
124
+ tools: Array<{ name: string; execution_target: string }>;
125
+ };
126
+ const byName = new Map(manifest.tools.map((t) => [t.name, t]));
127
+ expect(byName.get("run_workflow")?.execution_target).toBe("host");
128
+ expect(byName.get("manage_workflows")?.execution_target).toBe("host");
129
+ });
130
+ });
131
+
132
+ describe("run_workflow capability contract", () => {
133
+ test("SKILL.md advertises the actual read-only baseline (no drift from the code)", async () => {
134
+ // The workflows skill's SKILL.md is the authoring contract the model reads
135
+ // when generating scripts. Guard it against the code: every
136
+ // WORKFLOW_READONLY_BASELINE entry must appear, so the documented baseline
137
+ // never drifts from what resolveCapabilities actually grants (e.g.
138
+ // re-advertising web_fetch as a default).
139
+ const { readFileSync } = await import("node:fs");
140
+ const { join } = await import("node:path");
141
+ const skillMd = readFileSync(
142
+ join(import.meta.dir, "../../config/bundled-skills/workflows/SKILL.md"),
143
+ "utf-8",
144
+ );
145
+ for (const name of WORKFLOW_READONLY_BASELINE) {
146
+ expect(skillMd).toContain(name);
147
+ }
148
+ // web_fetch is side-effecting, so it is NOT a default — the contract must
149
+ // present it as a tool to DECLARE, never as part of the baseline.
150
+ expect(WORKFLOW_READONLY_BASELINE).not.toContain("web_fetch");
151
+ });
152
+ });
153
+
154
+ describe("run_workflow input validation", () => {
155
+ test("rejects when neither script nor name is provided", async () => {
156
+ const res = await executeRunWorkflow({}, makeContext());
157
+ expect(res.isError).toBe(true);
158
+ expect(startMock).not.toHaveBeenCalled();
159
+ });
160
+
161
+ test("rejects when BOTH script and name are provided", async () => {
162
+ const res = await executeRunWorkflow(
163
+ { script: "export const meta = {};", name: "saved" },
164
+ makeContext(),
165
+ );
166
+ expect(res.isError).toBe(true);
167
+ expect(startMock).not.toHaveBeenCalled();
168
+ });
169
+ });
170
+
171
+ describe("run_workflow launch", () => {
172
+ test("starts an inline-script run with the built manifest and returns runId", async () => {
173
+ const res = await executeRunWorkflow(
174
+ {
175
+ script: "export const meta = { name: 'x', description: 'y' };",
176
+ args: { foo: 1 },
177
+ capabilities: { tools: ["file_write"], persona: true },
178
+ label: "My Run",
179
+ },
180
+ makeContext(),
181
+ );
182
+
183
+ expect(res.isError).toBe(false);
184
+ expect(JSON.parse(res.content).runId).toBe("run-123");
185
+
186
+ expect(startMock).toHaveBeenCalledTimes(1);
187
+ expect(lastStartArgs).toMatchObject({
188
+ scriptSource: "export const meta = { name: 'x', description: 'y' };",
189
+ args: { foo: 1 },
190
+ label: "My Run",
191
+ conversationId: "conv-1",
192
+ manifest: { tools: ["file_write"], hostFunctions: [], persona: true },
193
+ });
194
+ // Trust context falls back to the tool context's trust class.
195
+ expect(
196
+ (lastStartArgs as Record<string, unknown>).trustContext,
197
+ ).toMatchObject({
198
+ trustClass: "guardian",
199
+ });
200
+ });
201
+
202
+ test("starts a saved-name run and defaults the manifest to empties", async () => {
203
+ await executeRunWorkflow({ name: "saved-flow" }, makeContext());
204
+ expect(lastStartArgs).toMatchObject({
205
+ name: "saved-flow",
206
+ manifest: { tools: [], hostFunctions: [], persona: false },
207
+ });
208
+ expect(
209
+ (lastStartArgs as Record<string, unknown>).scriptSource,
210
+ ).toBeUndefined();
211
+ });
212
+
213
+ test("surfaces a run-manager start error as a tool error", async () => {
214
+ startThrows = new Error("Workflows are not enabled.");
215
+ const res = await executeRunWorkflow(
216
+ { script: "export const meta = {};" },
217
+ makeContext(),
218
+ );
219
+ expect(res.isError).toBe(true);
220
+ expect(res.content).toContain("Workflows are not enabled.");
221
+ });
222
+ });
223
+
224
+ describe("manage_workflows", () => {
225
+ test("list_runs delegates to list()", async () => {
226
+ listMock.mockReturnValueOnce([
227
+ {
228
+ id: "r1",
229
+ name: "flow",
230
+ status: "completed",
231
+ agentsSpawned: 3,
232
+ },
233
+ ] as unknown[]);
234
+ const res = await executeManageWorkflows(
235
+ { action: "list_runs" },
236
+ makeContext(),
237
+ );
238
+ expect(res.isError).toBe(false);
239
+ expect(listMock).toHaveBeenCalledTimes(1);
240
+ expect(JSON.parse(res.content).runs[0].runId).toBe("r1");
241
+ });
242
+
243
+ test("abort requires run_id and delegates to abort()", async () => {
244
+ const missing = await executeManageWorkflows(
245
+ { action: "abort" },
246
+ makeContext(),
247
+ );
248
+ expect(missing.isError).toBe(true);
249
+ expect(abortMock).not.toHaveBeenCalled();
250
+
251
+ // abort now fetches the run first to enforce ownership; seed an owned run.
252
+ statusMock.mockReturnValueOnce({
253
+ id: "r9",
254
+ conversationId: "conv-1",
255
+ } as unknown);
256
+ const ok = await executeManageWorkflows(
257
+ { action: "abort", run_id: "r9" },
258
+ makeContext(),
259
+ );
260
+ expect(ok.isError).toBe(false);
261
+ expect(abortMock).toHaveBeenCalledWith("r9");
262
+ });
263
+
264
+ test("status requires run_id and reports not-found cleanly", async () => {
265
+ const missing = await executeManageWorkflows(
266
+ { action: "status" },
267
+ makeContext(),
268
+ );
269
+ expect(missing.isError).toBe(true);
270
+
271
+ const res = await executeManageWorkflows(
272
+ { action: "status", run_id: "nope" },
273
+ makeContext(),
274
+ );
275
+ expect(res.isError).toBe(false);
276
+ expect(JSON.parse(res.content).found).toBe(false);
277
+ });
278
+
279
+ test("get_result requires run_id and returns the full result", async () => {
280
+ const missing = await executeManageWorkflows(
281
+ { action: "get_result" },
282
+ makeContext(),
283
+ );
284
+ expect(missing.isError).toBe(true);
285
+
286
+ // not-found / not-owned reports cleanly, without leaking a result
287
+ const notFound = await executeManageWorkflows(
288
+ { action: "get_result", run_id: "nope" },
289
+ makeContext(),
290
+ );
291
+ expect(notFound.isError).toBe(false);
292
+ expect(JSON.parse(notFound.content).found).toBe(false);
293
+
294
+ // an owned, finished run returns the full (untruncated) result payload
295
+ statusMock.mockReturnValueOnce({
296
+ id: "r5",
297
+ conversationId: "conv-1",
298
+ status: "completed",
299
+ result: { scores: [1, 2, 3], note: "the full result lives here" },
300
+ error: null,
301
+ } as unknown);
302
+ const ok = await executeManageWorkflows(
303
+ { action: "get_result", run_id: "r5" },
304
+ makeContext(),
305
+ );
306
+ expect(ok.isError).toBe(false);
307
+ const parsed = JSON.parse(ok.content);
308
+ expect(parsed.runId).toBe("r5");
309
+ expect(parsed.status).toBe("completed");
310
+ expect(parsed.result).toEqual({
311
+ scores: [1, 2, 3],
312
+ note: "the full result lives here",
313
+ });
314
+ });
315
+
316
+ test("resume requires run_id and delegates to resume()", async () => {
317
+ const missing = await executeManageWorkflows(
318
+ { action: "resume" },
319
+ makeContext(),
320
+ );
321
+ expect(missing.isError).toBe(true);
322
+ expect(resumeMock).not.toHaveBeenCalled();
323
+
324
+ // resume now fetches the run first to enforce ownership; seed an owned run.
325
+ statusMock.mockReturnValueOnce({
326
+ id: "r9",
327
+ conversationId: "conv-1",
328
+ } as unknown);
329
+ const ok = await executeManageWorkflows(
330
+ { action: "resume", run_id: "r9" },
331
+ makeContext(),
332
+ );
333
+ expect(ok.isError).toBe(false);
334
+ expect(resumeMock).toHaveBeenCalledWith("r9");
335
+ expect(JSON.parse(ok.content).runId).toBe("r9");
336
+ });
337
+
338
+ test("resume surfaces a run-manager error as a tool error", async () => {
339
+ statusMock.mockReturnValueOnce({
340
+ id: "r9",
341
+ conversationId: "conv-1",
342
+ } as unknown);
343
+ resumeThrows = new Error(
344
+ "Workflow run r9 is not resumable (status: completed).",
345
+ );
346
+ const res = await executeManageWorkflows(
347
+ { action: "resume", run_id: "r9" },
348
+ makeContext(),
349
+ );
350
+ expect(res.isError).toBe(true);
351
+ expect(res.content).toContain("not resumable");
352
+ });
353
+
354
+ test("list_profiles returns sorted profile names and the active profile", async () => {
355
+ const res = await executeManageWorkflows(
356
+ { action: "list_profiles" },
357
+ makeContext(),
358
+ );
359
+ expect(res.isError).toBe(false);
360
+ const parsed = JSON.parse(res.content);
361
+ expect(parsed.profiles).toEqual(["balanced", "cost-optimized"]);
362
+ expect(parsed.activeProfile).toBe("balanced");
363
+ });
364
+
365
+ test("rejects an unknown action", async () => {
366
+ const res = await executeManageWorkflows(
367
+ { action: "bogus" },
368
+ makeContext(),
369
+ );
370
+ expect(res.isError).toBe(true);
371
+ });
372
+
373
+ // Non-guardian authorization scope: a contact conversation may only see and
374
+ // control runs it originated, never another conversation's (or a guardian's).
375
+ const contactContext = () =>
376
+ ({
377
+ conversationId: "conv-1",
378
+ workingDir: "/tmp",
379
+ trustClass: "trusted_contact",
380
+ }) as Parameters<typeof executeManageWorkflows>[1];
381
+
382
+ test("list_runs is scoped to the caller's conversation for a non-guardian", async () => {
383
+ listMock.mockReturnValueOnce([
384
+ { id: "mine", name: "a", status: "running", conversationId: "conv-1" },
385
+ { id: "theirs", name: "b", status: "running", conversationId: "conv-2" },
386
+ { id: "nullconv", name: "c", status: "running", conversationId: null },
387
+ ] as unknown[]);
388
+ const res = await executeManageWorkflows(
389
+ { action: "list_runs" },
390
+ contactContext(),
391
+ );
392
+ const ids = JSON.parse(res.content).runs.map(
393
+ (r: { runId: string }) => r.runId,
394
+ );
395
+ expect(ids).toEqual(["mine"]);
396
+ });
397
+
398
+ test("a non-guardian cannot status/abort/resume another conversation's run", async () => {
399
+ const foreign = {
400
+ id: "theirs",
401
+ name: "b",
402
+ status: "interrupted",
403
+ conversationId: "conv-2",
404
+ } as unknown;
405
+
406
+ statusMock.mockReturnValueOnce(foreign);
407
+ const status = await executeManageWorkflows(
408
+ { action: "status", run_id: "theirs" },
409
+ contactContext(),
410
+ );
411
+ // Reads as not-found — never reveals the foreign run's details.
412
+ expect(JSON.parse(status.content).found).toBe(false);
413
+
414
+ statusMock.mockReturnValueOnce(foreign);
415
+ await executeManageWorkflows(
416
+ { action: "abort", run_id: "theirs" },
417
+ contactContext(),
418
+ );
419
+ expect(abortMock).not.toHaveBeenCalled();
420
+
421
+ statusMock.mockReturnValueOnce(foreign);
422
+ const resume = await executeManageWorkflows(
423
+ { action: "resume", run_id: "theirs" },
424
+ contactContext(),
425
+ );
426
+ expect(resume.isError).toBe(true);
427
+ expect(resume.content).toContain("not found");
428
+ expect(resumeMock).not.toHaveBeenCalled();
429
+ });
430
+
431
+ test("a non-guardian CAN control a run it originated", async () => {
432
+ statusMock.mockReturnValueOnce({
433
+ id: "mine",
434
+ conversationId: "conv-1",
435
+ } as unknown);
436
+ await executeManageWorkflows(
437
+ { action: "abort", run_id: "mine" },
438
+ contactContext(),
439
+ );
440
+ expect(abortMock).toHaveBeenCalledWith("mine");
441
+ });
442
+ });
@@ -0,0 +1,88 @@
1
+ /**
2
+ * `run_workflow` core implementation — a thin launcher over
3
+ * {@link WorkflowRunManager}.
4
+ *
5
+ * The assistant calls this to kick off an autonomous, multi-agent workflow from
6
+ * either an inline script or a saved workflow name. All orchestration logic
7
+ * lives in the run manager and engine; this implementation only validates
8
+ * input, builds the per-run capability manifest, resolves the originating
9
+ * conversation's trust context, and fires `start()` (which returns immediately —
10
+ * the run is asynchronous; its completion is surfaced later as a conversation
11
+ * injection).
12
+ *
13
+ * The script-authoring contract the assistant follows when writing workflow
14
+ * scripts lives in the `workflows` bundled skill's SKILL.md, the single source
15
+ * of truth the model reads when generating the `script`.
16
+ */
17
+
18
+ import { findConversation } from "../../daemon/conversation-registry.js";
19
+ import {
20
+ FALLBACK_TURN_TRUST,
21
+ type TrustContext,
22
+ } from "../../daemon/trust-context.js";
23
+ import { CapabilityManifestSchema } from "../../workflows/capabilities.js";
24
+ import { getWorkflowRunManager } from "../../workflows/run-manager.js";
25
+ import type { ToolContext, ToolExecutionResult } from "../types.js";
26
+
27
+ /**
28
+ * Resolve the {@link TrustContext} to forward to the run's leaves. Prefer the
29
+ * originating conversation's per-turn snapshot (the same context the live turn
30
+ * is running under), then its resolved trust context, and finally a synthetic
31
+ * fallback built from the tool context's `trustClass`. We never elevate beyond
32
+ * the tool context's own trust class.
33
+ */
34
+ function resolveTrustContext(context: ToolContext): TrustContext {
35
+ const conversation = findConversation(context.conversationId);
36
+ const fromConversation =
37
+ conversation?.currentTurnTrustContext ?? conversation?.trustContext;
38
+ if (fromConversation) return fromConversation;
39
+ return { ...FALLBACK_TURN_TRUST, trustClass: context.trustClass };
40
+ }
41
+
42
+ export async function executeRunWorkflow(
43
+ input: Record<string, unknown>,
44
+ context: ToolContext,
45
+ ): Promise<ToolExecutionResult> {
46
+ const script = input.script as string | undefined;
47
+ const name = input.name as string | undefined;
48
+
49
+ // Exactly one of script/name is required.
50
+ if ((script == null) === (name == null)) {
51
+ return {
52
+ content:
53
+ 'Provide exactly one of "script" (inline workflow source) or "name" (saved workflow name).',
54
+ isError: true,
55
+ };
56
+ }
57
+
58
+ const args = (input.args as Record<string, unknown> | undefined) ?? {};
59
+ const label = input.label as string | undefined;
60
+ const trustContext = resolveTrustContext(context);
61
+
62
+ try {
63
+ // The schema fills defaults (empty arrays, persona:false) for omitted
64
+ // fields and throws (caught below) on a malformed `capabilities` object.
65
+ const manifest = CapabilityManifestSchema.parse(input.capabilities ?? {});
66
+ const { runId } = getWorkflowRunManager().start({
67
+ ...(script != null ? { scriptSource: script } : { name: name as string }),
68
+ args,
69
+ manifest,
70
+ conversationId: context.conversationId,
71
+ ...(label ? { label } : {}),
72
+ trustContext,
73
+ });
74
+
75
+ return {
76
+ content: JSON.stringify({
77
+ runId,
78
+ status: "running",
79
+ message:
80
+ "Workflow started. You will be notified in this conversation when it completes — do NOT poll. Use manage_workflows to check status or abort if needed.",
81
+ }),
82
+ isError: false,
83
+ };
84
+ } catch (err) {
85
+ const msg = err instanceof Error ? err.message : String(err);
86
+ return { content: `Failed to start workflow: ${msg}`, isError: true };
87
+ }
88
+ }
@@ -3,6 +3,8 @@ export interface OnboardingContext {
3
3
  tasks: string[];
4
4
  tone: string;
5
5
  userName?: string;
6
+ /** The user's role / occupation, e.g. "Software Engineer". */
7
+ occupation?: string;
6
8
  assistantName?: string;
7
9
  priorAssistants?: string[];
8
10
  googleConnected?: boolean;
@@ -15,6 +15,12 @@ export type UsageAttributionProfileSource =
15
15
  export interface UsageAttributionInput {
16
16
  callSite: LLMCallSite | null;
17
17
  overrideProfile?: string | null;
18
+ /**
19
+ * Mirrors `ResolveCallSiteOpts.forceOverrideProfile`: the override profile
20
+ * was floated above the call-site layers for this request, so attribution
21
+ * must credit it ahead of the call-site profile too.
22
+ */
23
+ forceOverrideProfile?: boolean;
18
24
  /**
19
25
  * Per-conversation seed for `mix`-profile expansion (the conversation id).
20
26
  * When the applied profile is a mix, threading the same seed the dispatch
@@ -116,6 +122,9 @@ export function resolveUsageAttribution(
116
122
  const mixSelections = new Map<string, string>();
117
123
  const resolved = resolveCallSiteConfig(callSite, llm, {
118
124
  ...(overrideProfile != null ? { overrideProfile } : {}),
125
+ ...(input.forceOverrideProfile === true
126
+ ? { forceOverrideProfile: true }
127
+ : {}),
119
128
  ...(input.selectionSeed != null
120
129
  ? { selectionSeed: input.selectionSeed }
121
130
  : {}),
@@ -131,6 +140,7 @@ export function resolveUsageAttribution(
131
140
  profiles: llm.profiles ?? {},
132
141
  activeProfile,
133
142
  overrideProfile,
143
+ forceOverrideProfile: input.forceOverrideProfile === true,
134
144
  callSiteProfile,
135
145
  });
136
146
 
@@ -155,6 +165,7 @@ function resolveAppliedProfile(input: {
155
165
  profiles: Record<string, unknown>;
156
166
  activeProfile: string | null;
157
167
  overrideProfile: string | null;
168
+ forceOverrideProfile: boolean;
158
169
  callSiteProfile: string | null;
159
170
  }): Pick<UsageAttributionSnapshot, "appliedProfile" | "profileSource"> {
160
171
  if (input.callSite === "mainAgent") {
@@ -194,6 +205,19 @@ function resolveAppliedProfile(input: {
194
205
  };
195
206
  }
196
207
 
208
+ // Forced override floats above the call-site profile (the resolver's
209
+ // `forceOverrideProfile` escape hatch), so it wins attribution too.
210
+ if (
211
+ input.forceOverrideProfile &&
212
+ input.overrideProfile != null &&
213
+ input.profiles[input.overrideProfile] != null
214
+ ) {
215
+ return {
216
+ appliedProfile: input.overrideProfile,
217
+ profileSource: "conversation",
218
+ };
219
+ }
220
+
197
221
  if (
198
222
  input.callSiteProfile != null &&
199
223
  input.profiles[input.callSiteProfile] != null
@@ -6,8 +6,9 @@
6
6
  * immune to formatting variance across channels.
7
7
  *
8
8
  * Phone-like channels (voice, whatsapp) normalize to E.164 using the
9
- * existing phone utilities. Non-phone channels (telegram, slack, etc.)
10
- * pass through the platform-stable ID as-is after whitespace trimming.
9
+ * existing phone utilities. Email channels are lowercased (RFC 5321).
10
+ * Other channels (telegram, slack, etc.) pass through the platform-stable
11
+ * ID as-is after whitespace trimming.
11
12
  */
12
13
 
13
14
  import type { ChannelId } from "../channels/types.js";
@@ -16,6 +17,9 @@ import { normalizePhoneNumber } from "./phone.js";
16
17
  /** Channels whose raw sender IDs are phone numbers. */
17
18
  const PHONE_CHANNELS: ReadonlySet<ChannelId> = new Set(["phone", "whatsapp"]);
18
19
 
20
+ /** Channels whose raw sender IDs are email addresses (case-insensitive per RFC 5321). */
21
+ const EMAIL_CHANNELS: ReadonlySet<ChannelId> = new Set(["email"]);
22
+
19
23
  /**
20
24
  * Canonicalize a raw inbound sender identity for the given channel.
21
25
  *
@@ -23,7 +27,8 @@ const PHONE_CHANNELS: ReadonlySet<ChannelId> = new Set(["phone", "whatsapp"]);
23
27
  * normalized E.164 string on success, or the trimmed raw ID if
24
28
  * normalization fails (defensive: don't discard an identity just because
25
29
  * it doesn't parse as a phone number).
26
- * - For non-phone channels: returns the trimmed raw ID unchanged (these
30
+ * - For email channels: lowercases the address (case-insensitive per RFC 5321).
31
+ * - For other channels: returns the trimmed raw ID unchanged (these
27
32
  * platforms provide stable, unique identifiers that don't need normalization).
28
33
  *
29
34
  * Returns `null` only when `rawId` is empty/whitespace-only.
@@ -42,5 +47,9 @@ export function canonicalizeInboundIdentity(
42
47
  return e164 ?? trimmed;
43
48
  }
44
49
 
50
+ if (EMAIL_CHANNELS.has(channel)) {
51
+ return trimmed.toLowerCase();
52
+ }
53
+
45
54
  return trimmed;
46
55
  }