@vellumai/assistant 0.5.10 → 0.5.12

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 (395) hide show
  1. package/AGENTS.md +8 -0
  2. package/ARCHITECTURE.md +43 -43
  3. package/Dockerfile +3 -0
  4. package/docs/architecture/integrations.md +37 -42
  5. package/docs/architecture/memory.md +7 -12
  6. package/docs/credential-execution-service.md +9 -9
  7. package/docs/skills.md +1 -1
  8. package/node_modules/@vellumai/ces-contracts/src/__tests__/grants.test.ts +7 -7
  9. package/node_modules/@vellumai/ces-contracts/src/handles.ts +5 -4
  10. package/node_modules/@vellumai/credential-storage/src/index.ts +3 -3
  11. package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +1 -1
  12. package/openapi.yaml +7208 -0
  13. package/package.json +2 -1
  14. package/scripts/generate-openapi.ts +562 -0
  15. package/src/__tests__/acp-session.test.ts +239 -44
  16. package/src/__tests__/assistant-feature-flag-guard.test.ts +8 -8
  17. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +5 -86
  18. package/src/__tests__/assistant-feature-flags-integration.test.ts +7 -14
  19. package/src/__tests__/browser-skill-endstate.test.ts +1 -1
  20. package/src/__tests__/btw-routes.test.ts +8 -0
  21. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +10 -10
  22. package/src/__tests__/catalog-cache.test.ts +164 -0
  23. package/src/__tests__/catalog-search.test.ts +61 -0
  24. package/src/__tests__/channel-approvals.test.ts +7 -7
  25. package/src/__tests__/channel-readiness-service.test.ts +41 -0
  26. package/src/__tests__/cli-command-risk-guard.test.ts +181 -6
  27. package/src/__tests__/config-schema.test.ts +10 -2
  28. package/src/__tests__/context-memory-e2e.test.ts +2 -6
  29. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +396 -0
  30. package/src/__tests__/conversation-error.test.ts +3 -2
  31. package/src/__tests__/conversation-skill-tools.test.ts +1 -3
  32. package/src/__tests__/conversation-title-service.test.ts +2 -15
  33. package/src/__tests__/credential-execution-feature-gates.test.ts +4 -8
  34. package/src/__tests__/credential-execution-managed-contract.test.ts +8 -8
  35. package/src/__tests__/credential-security-e2e.test.ts +4 -4
  36. package/src/__tests__/credential-security-invariants.test.ts +12 -18
  37. package/src/__tests__/credential-vault-unit.test.ts +32 -34
  38. package/src/__tests__/credential-vault.test.ts +25 -33
  39. package/src/__tests__/credentials-cli.test.ts +3 -3
  40. package/src/__tests__/daemon-credential-client.test.ts +2 -2
  41. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -1
  42. package/src/__tests__/gateway-only-guard.test.ts +3 -0
  43. package/src/__tests__/heartbeat-service.test.ts +35 -0
  44. package/src/__tests__/host-bash-proxy.test.ts +79 -0
  45. package/src/__tests__/host-cu-proxy.test.ts +90 -0
  46. package/src/__tests__/host-file-proxy.test.ts +89 -0
  47. package/src/__tests__/host-shell-tool.test.ts +1 -1
  48. package/src/__tests__/inline-skill-load-permissions.test.ts +3 -3
  49. package/src/__tests__/integration-status.test.ts +5 -5
  50. package/src/__tests__/list-messages-attachments.test.ts +171 -0
  51. package/src/__tests__/llm-request-log-turn-query.test.ts +64 -0
  52. package/src/__tests__/log-export-workspace.test.ts +1 -1
  53. package/src/__tests__/mcp-abort-signal.test.ts +205 -0
  54. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  55. package/src/__tests__/memory-lifecycle-e2e.test.ts +2 -2
  56. package/src/__tests__/memory-recall-log-store.test.ts +182 -0
  57. package/src/__tests__/memory-recall-quality.test.ts +6 -8
  58. package/src/__tests__/memory-regressions.test.ts +53 -42
  59. package/src/__tests__/memory-retrieval.benchmark.test.ts +5 -9
  60. package/src/__tests__/messaging-send-tool.test.ts +5 -5
  61. package/src/__tests__/messaging-skill-split.test.ts +2 -17
  62. package/src/__tests__/notification-telegram-adapter.test.ts +125 -0
  63. package/src/__tests__/oauth-cli.test.ts +203 -649
  64. package/src/__tests__/oauth-provider-profiles.test.ts +55 -20
  65. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  66. package/src/__tests__/onboarding-template-contract.test.ts +2 -2
  67. package/src/__tests__/platform-callback-registration.test.ts +119 -0
  68. package/src/__tests__/secret-ingress-channel.test.ts +261 -0
  69. package/src/__tests__/secret-ingress-cli.test.ts +201 -0
  70. package/src/__tests__/secret-ingress-http.test.ts +312 -0
  71. package/src/__tests__/secret-ingress.test.ts +283 -0
  72. package/src/__tests__/secret-onetime-send.test.ts +4 -4
  73. package/src/__tests__/secret-routes-managed-proxy.test.ts +78 -0
  74. package/src/__tests__/secure-keys-managed-failover.test.ts +73 -0
  75. package/src/__tests__/skill-feature-flags-integration.test.ts +4 -4
  76. package/src/__tests__/skill-feature-flags.test.ts +11 -19
  77. package/src/__tests__/skill-load-feature-flag.test.ts +1 -1
  78. package/src/__tests__/skill-load-inline-command.test.ts +3 -3
  79. package/src/__tests__/skill-load-inline-includes.test.ts +2 -2
  80. package/src/__tests__/skill-memory.test.ts +2 -4
  81. package/src/__tests__/skill-projection-feature-flag.test.ts +2 -4
  82. package/src/__tests__/skill-projection.benchmark.test.ts +1 -3
  83. package/src/__tests__/skills-uninstall.test.ts +2 -2
  84. package/src/__tests__/skills.test.ts +16 -2
  85. package/src/__tests__/slack-channel-config.test.ts +1 -1
  86. package/src/__tests__/slack-messaging-token-resolution.test.ts +22 -24
  87. package/src/__tests__/slack-share-routes.test.ts +5 -5
  88. package/src/__tests__/slack-skill.test.ts +5 -69
  89. package/src/__tests__/system-prompt.test.ts +39 -0
  90. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -1
  91. package/src/__tests__/workspace-migration-018-rekey-compound-credential-keys.test.ts +181 -0
  92. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +5 -4
  93. package/src/acp/client-handler.ts +113 -31
  94. package/src/acp/session-manager.ts +29 -27
  95. package/src/approvals/guardian-request-resolvers.ts +1 -1
  96. package/src/cli/AGENTS.md +113 -0
  97. package/src/cli/commands/autonomy.ts +3 -5
  98. package/src/cli/commands/browser-relay.ts +2 -17
  99. package/src/cli/commands/contacts.ts +6 -4
  100. package/src/cli/commands/conversations.ts +13 -1
  101. package/src/cli/commands/credential-execution.ts +17 -3
  102. package/src/cli/commands/credentials.ts +2 -8
  103. package/src/cli/commands/memory.ts +2 -3
  104. package/src/cli/commands/oauth/__tests__/connect.test.ts +706 -0
  105. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +686 -0
  106. package/src/cli/commands/oauth/__tests__/mode.test.ts +625 -0
  107. package/src/cli/commands/oauth/__tests__/ping.test.ts +631 -0
  108. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +574 -0
  109. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +416 -0
  110. package/src/cli/commands/oauth/__tests__/status.test.ts +551 -0
  111. package/src/cli/commands/oauth/__tests__/token.test.ts +420 -0
  112. package/src/cli/commands/oauth/apps.ts +87 -50
  113. package/src/cli/commands/oauth/connect.ts +405 -0
  114. package/src/cli/commands/oauth/disconnect.ts +285 -0
  115. package/src/cli/commands/oauth/index.ts +62 -20
  116. package/src/cli/commands/oauth/mode.ts +251 -0
  117. package/src/cli/commands/oauth/ping.ts +196 -0
  118. package/src/cli/commands/oauth/providers.ts +589 -55
  119. package/src/cli/commands/oauth/request.ts +564 -0
  120. package/src/cli/commands/oauth/shared.ts +114 -0
  121. package/src/cli/commands/oauth/status.ts +191 -0
  122. package/src/cli/commands/oauth/token.ts +150 -0
  123. package/src/cli/commands/platform/connect.ts +104 -0
  124. package/src/cli/commands/platform/disconnect.ts +118 -0
  125. package/src/cli/commands/platform/index.ts +252 -0
  126. package/src/cli/commands/sequence.ts +5 -4
  127. package/src/cli/commands/shotgun.ts +16 -0
  128. package/src/cli/commands/skills.ts +173 -41
  129. package/src/cli/commands/usage.ts +5 -11
  130. package/src/cli/lib/daemon-credential-client.ts +22 -38
  131. package/src/cli/program.ts +1 -1
  132. package/src/cli.ts +82 -17
  133. package/src/config/assistant-feature-flags.ts +77 -18
  134. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  135. package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -1
  136. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
  137. package/src/config/bundled-skills/conversations/SKILL.md +20 -0
  138. package/src/config/bundled-skills/conversations/TOOLS.json +23 -0
  139. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +66 -0
  140. package/src/config/bundled-skills/gmail/SKILL.md +13 -13
  141. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +3 -3
  142. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +2 -2
  143. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +1 -1
  144. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +1 -1
  145. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +1 -1
  146. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +1 -1
  147. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +2 -2
  148. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +1 -1
  149. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +1 -1
  150. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +1 -1
  151. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +1 -1
  152. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +1 -1
  153. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +1 -1
  154. package/src/config/bundled-skills/google-calendar/SKILL.md +10 -4
  155. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  156. package/src/config/bundled-skills/messaging/SKILL.md +19 -42
  157. package/src/config/bundled-skills/messaging/TOOLS.json +9 -9
  158. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +1 -1
  159. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -2
  160. package/src/config/bundled-skills/messaging/tools/shared.ts +5 -6
  161. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  162. package/src/config/bundled-skills/schedule/SKILL.md +2 -2
  163. package/src/config/bundled-skills/settings/SKILL.md +5 -3
  164. package/src/config/bundled-skills/settings/TOOLS.json +17 -0
  165. package/src/config/bundled-skills/settings/tools/avatar-get.ts +50 -0
  166. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +7 -0
  167. package/src/config/bundled-skills/settings/tools/avatar-update.ts +6 -1
  168. package/src/config/bundled-skills/settings/tools/identity-avatar.ts +55 -0
  169. package/src/config/bundled-skills/skills-catalog/SKILL.md +3 -3
  170. package/src/config/bundled-skills/slack/SKILL.md +58 -44
  171. package/src/config/bundled-tool-registry.ts +7 -19
  172. package/src/config/env.ts +5 -1
  173. package/src/config/feature-flag-registry.json +58 -42
  174. package/src/config/loader.ts +4 -0
  175. package/src/config/schemas/platform.ts +0 -8
  176. package/src/config/schemas/security.ts +9 -1
  177. package/src/config/schemas/services.ts +1 -1
  178. package/src/config/skill-state.ts +1 -3
  179. package/src/config/skills.ts +2 -4
  180. package/src/credential-execution/client.ts +1 -1
  181. package/src/credential-execution/feature-gates.ts +9 -16
  182. package/src/credential-execution/process-manager.ts +12 -0
  183. package/src/daemon/config-watcher.ts +4 -0
  184. package/src/daemon/conversation-agent-loop-handlers.ts +10 -0
  185. package/src/daemon/conversation-agent-loop.ts +51 -2
  186. package/src/daemon/conversation-error.ts +36 -6
  187. package/src/daemon/conversation-memory.ts +0 -1
  188. package/src/daemon/conversation-messaging.ts +9 -0
  189. package/src/daemon/conversation-runtime-assembly.ts +33 -0
  190. package/src/daemon/conversation-surfaces.ts +120 -14
  191. package/src/daemon/conversation.ts +5 -0
  192. package/src/daemon/handlers/config-slack-channel.ts +43 -1
  193. package/src/daemon/handlers/conversations.ts +41 -33
  194. package/src/daemon/handlers/skills.ts +148 -3
  195. package/src/daemon/host-bash-proxy.ts +16 -0
  196. package/src/daemon/host-cu-proxy.ts +16 -0
  197. package/src/daemon/host-file-proxy.ts +16 -0
  198. package/src/daemon/lifecycle.ts +73 -3
  199. package/src/daemon/message-types/acp.ts +0 -15
  200. package/src/daemon/message-types/conversations.ts +1 -0
  201. package/src/daemon/message-types/guardian-actions.ts +2 -0
  202. package/src/daemon/message-types/host-bash.ts +6 -1
  203. package/src/daemon/message-types/host-cu.ts +6 -1
  204. package/src/daemon/message-types/host-file.ts +6 -1
  205. package/src/daemon/message-types/integrations.ts +0 -1
  206. package/src/daemon/message-types/memory.ts +0 -1
  207. package/src/daemon/message-types/messages.ts +9 -1
  208. package/src/daemon/message-types/schedules.ts +9 -0
  209. package/src/daemon/server.ts +48 -9
  210. package/src/email/feature-gate.ts +3 -3
  211. package/src/heartbeat/heartbeat-service.ts +48 -0
  212. package/src/hooks/cli.ts +74 -0
  213. package/src/inbound/platform-callback-registration.ts +68 -19
  214. package/src/mcp/client.ts +6 -1
  215. package/src/mcp/manager.ts +2 -1
  216. package/src/mcp/mcp-oauth-provider.ts +3 -3
  217. package/src/memory/app-store.ts +3 -3
  218. package/src/memory/conversation-crud.ts +213 -0
  219. package/src/memory/conversation-key-store.ts +26 -0
  220. package/src/memory/conversation-title-service.ts +7 -17
  221. package/src/memory/db-init.ts +24 -0
  222. package/src/memory/embedding-local.ts +47 -2
  223. package/src/memory/indexer.ts +13 -10
  224. package/src/memory/items-extractor.ts +12 -4
  225. package/src/memory/job-utils.ts +5 -0
  226. package/src/memory/jobs-store.ts +10 -2
  227. package/src/memory/journal-memory.ts +6 -2
  228. package/src/memory/llm-request-log-store.ts +88 -21
  229. package/src/memory/memory-recall-log-store.ts +128 -0
  230. package/src/memory/migrations/194-memory-recall-logs.ts +50 -0
  231. package/src/memory/migrations/195-oauth-providers-ping-config.ts +23 -0
  232. package/src/memory/migrations/196-messages-conversation-created-at-index.ts +9 -0
  233. package/src/memory/migrations/196-strip-integration-prefix-from-provider-keys.ts +186 -0
  234. package/src/memory/migrations/197-oauth-providers-behavior-columns.ts +29 -0
  235. package/src/memory/migrations/198-drop-setup-skill-id-column.ts +11 -0
  236. package/src/memory/migrations/index.ts +6 -0
  237. package/src/memory/migrations/registry.ts +8 -0
  238. package/src/memory/retriever.test.ts +4 -5
  239. package/src/memory/schema/infrastructure.ts +31 -0
  240. package/src/memory/schema/oauth.ts +14 -0
  241. package/src/messaging/provider.ts +13 -12
  242. package/src/messaging/providers/gmail/adapter.ts +44 -35
  243. package/src/messaging/providers/slack/adapter.ts +63 -33
  244. package/src/messaging/providers/telegram-bot/adapter.ts +7 -9
  245. package/src/messaging/providers/whatsapp/adapter.ts +6 -8
  246. package/src/notifications/adapters/telegram.ts +78 -2
  247. package/src/oauth/__tests__/identity-verifier.test.ts +464 -0
  248. package/src/oauth/byo-connection.test.ts +22 -24
  249. package/src/oauth/connect-orchestrator.ts +79 -64
  250. package/src/oauth/connect-types.ts +7 -65
  251. package/src/oauth/connection-resolver.test.ts +13 -13
  252. package/src/oauth/connection-resolver.ts +3 -4
  253. package/src/oauth/identity-verifier.ts +177 -0
  254. package/src/oauth/manual-token-connection.ts +5 -5
  255. package/src/oauth/oauth-store.ts +251 -5
  256. package/src/oauth/platform-connection.test.ts +56 -6
  257. package/src/oauth/platform-connection.ts +8 -1
  258. package/src/oauth/seed-providers.ts +256 -34
  259. package/src/permissions/checker.ts +129 -3
  260. package/src/permissions/trust-client.ts +2 -2
  261. package/src/platform/client.ts +2 -2
  262. package/src/prompts/journal-context.ts +6 -1
  263. package/src/prompts/system-prompt.ts +43 -9
  264. package/src/prompts/templates/BOOTSTRAP.md +16 -5
  265. package/src/providers/anthropic/client.ts +139 -28
  266. package/src/runtime/auth/__tests__/middleware.test.ts +19 -0
  267. package/src/runtime/auth/route-policy.ts +0 -1
  268. package/src/runtime/btw-sidechain.ts +7 -1
  269. package/src/runtime/channel-approvals.ts +2 -2
  270. package/src/runtime/channel-readiness-service.ts +30 -7
  271. package/src/runtime/guardian-action-service.ts +7 -2
  272. package/src/runtime/http-router.ts +31 -0
  273. package/src/runtime/http-server.ts +26 -7
  274. package/src/runtime/http-types.ts +9 -0
  275. package/src/runtime/pending-interactions.ts +21 -3
  276. package/src/runtime/routes/acp-routes.ts +46 -28
  277. package/src/runtime/routes/app-management-routes.ts +123 -0
  278. package/src/runtime/routes/app-routes.ts +31 -0
  279. package/src/runtime/routes/approval-routes.ts +108 -3
  280. package/src/runtime/routes/attachment-routes.ts +45 -0
  281. package/src/runtime/routes/avatar-routes.ts +16 -0
  282. package/src/runtime/routes/brain-graph-routes.ts +18 -0
  283. package/src/runtime/routes/btw-routes.ts +20 -0
  284. package/src/runtime/routes/call-routes.ts +81 -0
  285. package/src/runtime/routes/channel-readiness-routes.ts +48 -7
  286. package/src/runtime/routes/channel-routes.ts +18 -0
  287. package/src/runtime/routes/channel-verification-routes.ts +49 -1
  288. package/src/runtime/routes/contact-routes.ts +77 -0
  289. package/src/runtime/routes/conversation-attention-routes.ts +37 -0
  290. package/src/runtime/routes/conversation-management-routes.ts +125 -0
  291. package/src/runtime/routes/conversation-query-routes.ts +78 -0
  292. package/src/runtime/routes/conversation-routes.ts +191 -39
  293. package/src/runtime/routes/conversation-starter-routes.ts +29 -0
  294. package/src/runtime/routes/debug-routes.ts +23 -0
  295. package/src/runtime/routes/diagnostics-routes.ts +30 -0
  296. package/src/runtime/routes/documents-routes.ts +42 -0
  297. package/src/runtime/routes/events-routes.ts +10 -0
  298. package/src/runtime/routes/global-search-routes.ts +35 -0
  299. package/src/runtime/routes/guardian-action-routes.ts +61 -3
  300. package/src/runtime/routes/guardian-approval-prompt.ts +77 -2
  301. package/src/runtime/routes/heartbeat-routes.ts +278 -0
  302. package/src/runtime/routes/host-bash-routes.ts +16 -1
  303. package/src/runtime/routes/host-cu-routes.ts +23 -1
  304. package/src/runtime/routes/host-file-routes.ts +18 -1
  305. package/src/runtime/routes/identity-routes.ts +35 -0
  306. package/src/runtime/routes/inbound-message-handler.ts +46 -25
  307. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -8
  308. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +30 -2
  309. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -2
  310. package/src/runtime/routes/integrations/slack/share.ts +1 -1
  311. package/src/runtime/routes/integrations/twilio.ts +32 -22
  312. package/src/runtime/routes/invite-routes.ts +83 -0
  313. package/src/runtime/routes/log-export-routes.ts +14 -0
  314. package/src/runtime/routes/memory-item-routes.ts +99 -1
  315. package/src/runtime/routes/migration-rollback-routes.ts +25 -0
  316. package/src/runtime/routes/migration-routes.ts +40 -0
  317. package/src/runtime/routes/notification-routes.ts +20 -0
  318. package/src/runtime/routes/oauth-apps.ts +13 -4
  319. package/src/runtime/routes/pairing-routes.ts +15 -0
  320. package/src/runtime/routes/recording-routes.ts +72 -0
  321. package/src/runtime/routes/schedule-routes.ts +77 -5
  322. package/src/runtime/routes/secret-routes.ts +99 -14
  323. package/src/runtime/routes/settings-routes.ts +102 -19
  324. package/src/runtime/routes/skills-routes.ts +141 -18
  325. package/src/runtime/routes/subagents-routes.ts +38 -3
  326. package/src/runtime/routes/surface-action-routes.ts +66 -24
  327. package/src/runtime/routes/surface-content-routes.ts +20 -0
  328. package/src/runtime/routes/telemetry-routes.ts +12 -0
  329. package/src/runtime/routes/trace-event-routes.ts +25 -0
  330. package/src/runtime/routes/trust-rules-routes.ts +46 -0
  331. package/src/runtime/routes/tts-routes.ts +15 -4
  332. package/src/runtime/routes/upgrade-broadcast-routes.ts +38 -0
  333. package/src/runtime/routes/usage-routes.ts +59 -0
  334. package/src/runtime/routes/watch-routes.ts +28 -0
  335. package/src/runtime/routes/work-items-routes.ts +59 -0
  336. package/src/runtime/routes/workspace-commit-routes.ts +12 -0
  337. package/src/runtime/routes/workspace-routes.ts +102 -0
  338. package/src/schedule/integration-status.ts +2 -2
  339. package/src/schedule/scheduler.ts +7 -1
  340. package/src/security/AGENTS.md +7 -0
  341. package/src/security/ces-rpc-credential-backend.ts +19 -16
  342. package/src/security/credential-backend.ts +1 -1
  343. package/src/security/encrypted-store.ts +3 -3
  344. package/src/security/oauth-completion-page.ts +153 -0
  345. package/src/security/oauth2.ts +58 -17
  346. package/src/security/secret-ingress.ts +174 -0
  347. package/src/security/secret-patterns.ts +133 -0
  348. package/src/security/secret-scanner.ts +28 -117
  349. package/src/security/secure-keys.ts +207 -7
  350. package/src/security/token-manager.ts +3 -6
  351. package/src/signals/bash.ts +6 -1
  352. package/src/signals/confirm.ts +12 -8
  353. package/src/signals/user-message.ts +18 -3
  354. package/src/skills/catalog-cache.ts +44 -0
  355. package/src/skills/catalog-search.ts +18 -0
  356. package/src/skills/skill-memory.ts +1 -2
  357. package/src/tasks/task-runner.ts +7 -1
  358. package/src/tools/credentials/broker.ts +1 -1
  359. package/src/tools/credentials/metadata-store.ts +1 -1
  360. package/src/tools/credentials/post-connect-hooks.ts +1 -1
  361. package/src/tools/credentials/vault.ts +36 -48
  362. package/src/tools/host-terminal/host-shell.ts +16 -3
  363. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  364. package/src/tools/memory/definitions.ts +1 -1
  365. package/src/tools/memory/handlers.test.ts +2 -4
  366. package/src/tools/skills/load.ts +1 -1
  367. package/src/tools/skills/sandbox-runner.ts +16 -3
  368. package/src/tools/terminal/safe-env.ts +7 -0
  369. package/src/tools/terminal/shell.ts +16 -3
  370. package/src/tools/tool-manifest.ts +1 -1
  371. package/src/util/log-redact.ts +9 -34
  372. package/src/util/logger.ts +11 -1
  373. package/src/util/sentry-log-stream.ts +51 -0
  374. package/src/watcher/providers/github.ts +2 -2
  375. package/src/watcher/providers/gmail.ts +1 -1
  376. package/src/watcher/providers/google-calendar.ts +1 -1
  377. package/src/watcher/providers/linear.ts +2 -2
  378. package/src/workspace/migrations/011-backfill-installation-id.ts +5 -3
  379. package/src/workspace/migrations/020-rename-oauth-skill-dirs.ts +119 -0
  380. package/src/workspace/migrations/registry.ts +2 -0
  381. package/docs/architecture/keychain-broker.md +0 -68
  382. package/src/cli/commands/oauth/connections.ts +0 -734
  383. package/src/cli/commands/oauth/platform.ts +0 -525
  384. package/src/cli/commands/platform.ts +0 -176
  385. package/src/config/bundled-skills/slack/TOOLS.json +0 -272
  386. package/src/config/bundled-skills/slack/tools/shared.ts +0 -34
  387. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +0 -27
  388. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +0 -38
  389. package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +0 -146
  390. package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +0 -105
  391. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +0 -26
  392. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +0 -27
  393. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +0 -25
  394. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +0 -372
  395. package/src/oauth/provider-behaviors.ts +0 -634
@@ -472,8 +472,7 @@ describe("seedCatalogSkillMemories", () => {
472
472
  mockResolveCatalog = async () => skills;
473
473
 
474
474
  // Disable the feature flag for the flagged skill
475
- mockIsFeatureFlagEnabled = (key: string) =>
476
- key !== "feature_flags.my_gated_feature.enabled";
475
+ mockIsFeatureFlagEnabled = (key: string) => key !== "my_gated_feature";
477
476
 
478
477
  await seedCatalogSkillMemories();
479
478
 
@@ -519,8 +518,7 @@ describe("seedCatalogSkillMemories", () => {
519
518
  expect(beforeItems.every((i) => i.status === "active")).toBe(true);
520
519
 
521
520
  // Now disable the flag — the flagged skill should be pruned
522
- mockIsFeatureFlagEnabled = (key: string) =>
523
- key !== "feature_flags.my_gated_feature.enabled";
521
+ mockIsFeatureFlagEnabled = (key: string) => key !== "my_gated_feature";
524
522
  await seedCatalogSkillMemories();
525
523
 
526
524
  const afterItems = db
@@ -22,7 +22,7 @@ let mockSkillRefCount: Map<string, number> = new Map();
22
22
 
23
23
  let currentConfig: Record<string, unknown> = {};
24
24
  const DECLARED_SKILL_ID = "contacts";
25
- const DECLARED_FLAG_KEY = "feature_flags.contacts.enabled";
25
+ const DECLARED_FLAG_KEY = "contacts";
26
26
 
27
27
  // ---------------------------------------------------------------------------
28
28
  // Mocks
@@ -40,9 +40,7 @@ mock.module("../config/loader.js", () => ({
40
40
 
41
41
  mock.module("../config/skill-state.js", () => ({
42
42
  skillFlagKey: (skill: { featureFlag?: string }) =>
43
- skill.featureFlag
44
- ? `feature_flags.${skill.featureFlag}.enabled`
45
- : undefined,
43
+ skill.featureFlag || undefined,
46
44
  }));
47
45
 
48
46
  // Mock assistant-feature-flags to avoid loading the real module (which
@@ -66,9 +66,7 @@ mock.module("../config/skills.js", () => ({
66
66
  // only needs skillFlagKey and doesn't exercise resolveSkillStates.
67
67
  mock.module("../config/skill-state.js", () => ({
68
68
  skillFlagKey: (skill: { featureFlag?: string }) =>
69
- skill.featureFlag
70
- ? `feature_flags.${skill.featureFlag}.enabled`
71
- : undefined,
69
+ skill.featureFlag || undefined,
72
70
  resolveSkillStates: () => [],
73
71
  }));
74
72
 
@@ -58,7 +58,7 @@ describe("assistant skills uninstall", () => {
58
58
 
59
59
  // GIVEN a skill is installed locally
60
60
  installFakeSkill("weather");
61
- writeSkillsIndex("- weather\n- google-oauth-applescript\n");
61
+ writeSkillsIndex("- weather\n- google-oauth-app-setup\n");
62
62
 
63
63
  // WHEN we uninstall the skill
64
64
  uninstallSkillLocally("weather");
@@ -71,7 +71,7 @@ describe("assistant skills uninstall", () => {
71
71
  expect(index).not.toContain("weather");
72
72
 
73
73
  // AND other skills should remain in the index
74
- expect(index).toContain("google-oauth-applescript");
74
+ expect(index).toContain("google-oauth-app-setup");
75
75
  });
76
76
 
77
77
  test("errors when skill is not installed", () => {
@@ -603,7 +603,7 @@ describe("bundled browser skill", () => {
603
603
  });
604
604
  });
605
605
 
606
- describe("ingress-dependent setup skills declare public-ingress", () => {
606
+ describe("ingress-dependent setup skills declare public-ingress intentionally", () => {
607
607
  const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
608
608
  const FIRST_PARTY_SKILLS_DIR = join(
609
609
  import.meta.dir,
@@ -649,14 +649,28 @@ describe("ingress-dependent setup skills declare public-ingress", () => {
649
649
  return undefined;
650
650
  }
651
651
 
652
- test("telegram-setup includes public-ingress", () => {
652
+ test("telegram-setup does not hard-depend on public-ingress", () => {
653
653
  const includes = readSkillIncludes(
654
654
  FIRST_PARTY_SKILLS_DIR,
655
655
  "telegram-setup",
656
656
  );
657
+ expect(includes ?? []).not.toContain("public-ingress");
658
+ });
659
+
660
+ test("twilio-setup includes public-ingress", () => {
661
+ const includes = readSkillIncludes(FIRST_PARTY_SKILLS_DIR, "twilio-setup");
657
662
  expect(includes).toBeDefined();
658
663
  expect(includes).toContain("public-ingress");
659
664
  });
665
+
666
+ test("public-ingress frontmatter advertises managed-mode avoidance", () => {
667
+ const content = readFileSync(
668
+ join(FIRST_PARTY_SKILLS_DIR, "public-ingress", "SKILL.md"),
669
+ "utf-8",
670
+ );
671
+ expect(content).toContain("avoid-when:");
672
+ expect(content.toLowerCase()).toContain("managed/containerized");
673
+ });
660
674
  });
661
675
 
662
676
  describe("bundled computer-use skill", () => {
@@ -252,7 +252,7 @@ describe("Slack channel config handler", () => {
252
252
  });
253
253
 
254
254
  test("GET reports per-field token presence independently of connection row", async () => {
255
- // Only bot_token in keychain, no app_token, but connection row exists
255
+ // Only bot_token in credential store, no app_token, but connection row exists
256
256
  oauthConnectionStore["slack_channel"] = {
257
257
  id: "conn-slack",
258
258
  status: "active",
@@ -117,12 +117,12 @@ describe("Slack messaging token resolution", () => {
117
117
  expect(await slackProvider.isConnected!()).toBe(true);
118
118
  });
119
119
 
120
- test("returns true when only integration:slack has active OAuth connection (backwards compat)", async () => {
120
+ test("returns true when only slack has active OAuth connection (backwards compat)", async () => {
121
121
  // No bot token
122
122
  getSecureKeyAsyncMock.mockImplementation(async () => null);
123
123
  // But OAuth provider is connected
124
124
  isProviderConnectedMock.mockImplementation(async (service: string) =>
125
- service === "integration:slack" ? true : false,
125
+ service === "slack" ? true : false,
126
126
  );
127
127
 
128
128
  expect(await slackProvider.isConnected!()).toBe(true);
@@ -139,7 +139,7 @@ describe("Slack messaging token resolution", () => {
139
139
  // ── slackProvider.resolveConnection() ───────────────────────────────────
140
140
 
141
141
  describe("slackProvider.resolveConnection()", () => {
142
- test("returns bot token string when Socket Mode credentials exist", async () => {
142
+ test("returns undefined when Socket Mode credentials exist (token cached internally)", async () => {
143
143
  getSecureKeyAsyncMock.mockImplementation(async (key: string) =>
144
144
  key === "credential/slack_channel/bot_token"
145
145
  ? "xoxb-socket-token"
@@ -147,20 +147,20 @@ describe("Slack messaging token resolution", () => {
147
147
  );
148
148
 
149
149
  const result = await slackProvider.resolveConnection!();
150
- expect(result).toBe("xoxb-socket-token");
150
+ expect(result).toBeUndefined();
151
151
  });
152
152
 
153
- test("returns bot token string even without a slack_channel connection row (token-only resilience)", async () => {
153
+ test("returns undefined even without a slack_channel connection row (token-only resilience)", async () => {
154
154
  getSecureKeyAsyncMock.mockImplementation(async (key: string) =>
155
155
  key === "credential/slack_channel/bot_token" ? "xoxb-token-only" : null,
156
156
  );
157
- // No connection row — resolveConnection should still return the token
157
+ // No connection row — resolveConnection should still return undefined (token cached internally)
158
158
 
159
159
  const result = await slackProvider.resolveConnection!();
160
- expect(result).toBe("xoxb-token-only");
160
+ expect(result).toBeUndefined();
161
161
  });
162
162
 
163
- test("returns OAuthConnection when only OAuth integration:slack credentials exist (backwards compat)", async () => {
163
+ test("returns OAuthConnection when only OAuth slack credentials exist (backwards compat)", async () => {
164
164
  getSecureKeyAsyncMock.mockImplementation(async () => null);
165
165
  const oauthConn = {
166
166
  accessToken: "xoxp-oauth-token",
@@ -169,16 +169,15 @@ describe("Slack messaging token resolution", () => {
169
169
 
170
170
  const result = await slackProvider.resolveConnection!();
171
171
  expect(result).toBe(oauthConn);
172
- expect(resolveOAuthConnectionMock).toHaveBeenCalledWith(
173
- "integration:slack",
174
- { account: undefined },
175
- );
172
+ expect(resolveOAuthConnectionMock).toHaveBeenCalledWith("slack", {
173
+ account: undefined,
174
+ });
176
175
  });
177
176
 
178
177
  test("throws when no credentials exist at all (no Socket Mode, no OAuth)", async () => {
179
178
  getSecureKeyAsyncMock.mockImplementation(async () => null);
180
179
  resolveOAuthConnectionMock.mockImplementation(async () => {
181
- throw new Error("No OAuth connection found for integration:slack");
180
+ throw new Error("No OAuth connection found for slack");
182
181
  });
183
182
 
184
183
  await expect(slackProvider.resolveConnection!()).rejects.toThrow(
@@ -190,13 +189,13 @@ describe("Slack messaging token resolution", () => {
190
189
  // ── getProviderConnection() integration ─────────────────────────────────
191
190
 
192
191
  describe("getProviderConnection()", () => {
193
- test("returns bot token string for Slack when Socket Mode credentials exist", async () => {
192
+ test("returns undefined for Slack when Socket Mode credentials exist (token cached internally)", async () => {
194
193
  getSecureKeyAsyncMock.mockImplementation(async (key: string) =>
195
194
  key === "credential/slack_channel/bot_token" ? "xoxb-conn-token" : null,
196
195
  );
197
196
 
198
197
  const result = await getProviderConnection(slackProvider);
199
- expect(result).toBe("xoxb-conn-token");
198
+ expect(result).toBeUndefined();
200
199
  });
201
200
 
202
201
  test("returns OAuthConnection for Slack when only OAuth credentials exist (backwards compat)", async () => {
@@ -221,9 +220,9 @@ describe("Slack messaging token resolution", () => {
221
220
  );
222
221
  });
223
222
 
224
- test('Telegram still returns "" (no resolveConnection, uses isConnected path — regression check)', async () => {
223
+ test("Telegram returns undefined (no resolveConnection, uses isConnected path — regression check)", async () => {
225
224
  // Telegram has isConnected but no resolveConnection.
226
- // When isConnected returns true, getProviderConnection returns ""
225
+ // When isConnected returns true, getProviderConnection returns undefined
227
226
  getSecureKeyAsyncMock.mockImplementation(async (key: string) => {
228
227
  if (key === "credential/telegram/bot_token") return "bot-token";
229
228
  if (key === "credential/telegram/webhook_secret") return "secret";
@@ -234,7 +233,7 @@ describe("Slack messaging token resolution", () => {
234
233
  );
235
234
 
236
235
  const result = await getProviderConnection(telegramBotMessagingProvider);
237
- expect(result).toBe("");
236
+ expect(result).toBeUndefined();
238
237
  });
239
238
 
240
239
  test("Gmail still calls resolveOAuthConnection (no resolveConnection, no isConnected — regression check)", async () => {
@@ -247,10 +246,9 @@ describe("Slack messaging token resolution", () => {
247
246
 
248
247
  const result = await getProviderConnection(gmailMessagingProvider);
249
248
  expect(result).toBe(oauthConn);
250
- expect(resolveOAuthConnectionMock).toHaveBeenCalledWith(
251
- "integration:google",
252
- { account: undefined },
253
- );
249
+ expect(resolveOAuthConnectionMock).toHaveBeenCalledWith("google", {
250
+ account: undefined,
251
+ });
254
252
  });
255
253
  });
256
254
 
@@ -264,7 +262,7 @@ describe("Slack messaging token resolution", () => {
264
262
  );
265
263
  // Gmail connected via OAuth
266
264
  isProviderConnectedMock.mockImplementation(async (service: string) =>
267
- service === "integration:google" ? true : false,
265
+ service === "google" ? true : false,
268
266
  );
269
267
 
270
268
  await expect(resolveProvider()).rejects.toThrow(
@@ -285,7 +283,7 @@ describe("Slack messaging token resolution", () => {
285
283
  test("auto-selects Gmail when it is the only connected provider (no Slack credentials)", async () => {
286
284
  getSecureKeyAsyncMock.mockImplementation(async () => null);
287
285
  isProviderConnectedMock.mockImplementation(async (service: string) =>
288
- service === "integration:google" ? true : false,
286
+ service === "google" ? true : false,
289
287
  );
290
288
 
291
289
  const provider = await resolveProvider();
@@ -111,7 +111,7 @@ describe("handleListSlackChannels", () => {
111
111
  });
112
112
 
113
113
  test("returns channels sorted by type then name", async () => {
114
- connectionByProvider["integration:slack"] = { id: "conn-slack-1" };
114
+ connectionByProvider["slack"] = { id: "conn-slack-1" };
115
115
  secureKeyValues.set(
116
116
  "oauth_connection/conn-slack-1/access_token",
117
117
  "xoxb-test",
@@ -192,7 +192,7 @@ describe("handleShareToSlackChannel", () => {
192
192
  });
193
193
 
194
194
  test("returns 400 for malformed JSON", async () => {
195
- connectionByProvider["integration:slack"] = { id: "conn-slack-1" };
195
+ connectionByProvider["slack"] = { id: "conn-slack-1" };
196
196
  secureKeyValues.set(
197
197
  "oauth_connection/conn-slack-1/access_token",
198
198
  "xoxb-test",
@@ -207,7 +207,7 @@ describe("handleShareToSlackChannel", () => {
207
207
  });
208
208
 
209
209
  test("returns 400 when missing required fields", async () => {
210
- connectionByProvider["integration:slack"] = { id: "conn-slack-1" };
210
+ connectionByProvider["slack"] = { id: "conn-slack-1" };
211
211
  secureKeyValues.set(
212
212
  "oauth_connection/conn-slack-1/access_token",
213
213
  "xoxb-test",
@@ -220,7 +220,7 @@ describe("handleShareToSlackChannel", () => {
220
220
  });
221
221
 
222
222
  test("returns 404 when app not found", async () => {
223
- connectionByProvider["integration:slack"] = { id: "conn-slack-1" };
223
+ connectionByProvider["slack"] = { id: "conn-slack-1" };
224
224
  secureKeyValues.set(
225
225
  "oauth_connection/conn-slack-1/access_token",
226
226
  "xoxb-test",
@@ -232,7 +232,7 @@ describe("handleShareToSlackChannel", () => {
232
232
  });
233
233
 
234
234
  test("posts message and returns success", async () => {
235
- connectionByProvider["integration:slack"] = { id: "conn-slack-1" };
235
+ connectionByProvider["slack"] = { id: "conn-slack-1" };
236
236
  secureKeyValues.set(
237
237
  "oauth_connection/conn-slack-1/access_token",
238
238
  "xoxb-test",
@@ -46,75 +46,11 @@ describe("slack adapter isPrivate mapping", () => {
46
46
  });
47
47
  });
48
48
 
49
- describe("slack skill TOOLS.json", () => {
49
+ describe("slack skill has no TOOLS.json (uses Web API via CLI)", () => {
50
50
  const toolsPath = join(BUNDLED_SKILLS_DIR, "slack", "TOOLS.json");
51
- const toolsJson = JSON.parse(readFileSync(toolsPath, "utf-8"));
52
51
 
53
- test("is valid JSON with correct version", () => {
54
- expect(toolsJson.version).toBe(1);
55
- expect(Array.isArray(toolsJson.tools)).toBe(true);
56
- });
57
-
58
- test("has expected tools", () => {
59
- const names = toolsJson.tools.map((t: { name: string }) => t.name);
60
- expect(names).toContain("slack_scan_digest");
61
- expect(names).toContain("slack_channel_details");
62
- expect(names).toContain("slack_configure_channels");
63
- expect(names).toContain("slack_add_reaction");
64
- expect(names).toContain("slack_edit_message");
65
- expect(names).toContain("slack_delete_message");
66
- expect(names).toContain("slack_leave_channel");
67
- expect(names).toContain("slack_channel_permissions");
68
- });
69
-
70
- test("has 8 tools total", () => {
71
- expect(toolsJson.tools.length).toBe(8);
72
- });
73
-
74
- test("all tools have required fields", () => {
75
- for (const tool of toolsJson.tools) {
76
- expect(tool.name).toBeDefined();
77
- expect(tool.description).toBeDefined();
78
- expect(tool.category).toBeDefined();
79
- expect(tool.risk).toBeDefined();
80
- expect(tool.input_schema).toBeDefined();
81
- expect(tool.executor).toBeDefined();
82
- expect(tool.execution_target).toBeDefined();
83
- }
84
- });
85
-
86
- test("all executor files exist", () => {
87
- const slackSkillDir = join(BUNDLED_SKILLS_DIR, "slack");
88
- for (const tool of toolsJson.tools) {
89
- const executorPath = join(slackSkillDir, tool.executor);
90
- expect(() => readFileSync(executorPath)).not.toThrow();
91
- }
92
- });
93
- });
94
-
95
- describe("messaging skill no longer has Slack tools", () => {
96
- const messagingToolsPath = join(
97
- BUNDLED_SKILLS_DIR,
98
- "messaging",
99
- "TOOLS.json",
100
- );
101
- const messagingToolsJson = JSON.parse(
102
- readFileSync(messagingToolsPath, "utf-8"),
103
- );
104
-
105
- test("slack_add_reaction not in messaging TOOLS.json", () => {
106
- const names = messagingToolsJson.tools.map((t: { name: string }) => t.name);
107
- expect(names).not.toContain("slack_add_reaction");
108
- });
109
-
110
- test("slack_delete_message not in messaging TOOLS.json", () => {
111
- const names = messagingToolsJson.tools.map((t: { name: string }) => t.name);
112
- expect(names).not.toContain("slack_delete_message");
113
- });
114
-
115
- test("slack_leave_channel not in messaging TOOLS.json", () => {
116
- const names = messagingToolsJson.tools.map((t: { name: string }) => t.name);
117
- expect(names).not.toContain("slack_leave_channel");
52
+ test("TOOLS.json does not exist", () => {
53
+ expect(() => readFileSync(toolsPath)).toThrow();
118
54
  });
119
55
  });
120
56
 
@@ -129,7 +65,7 @@ describe("slack skill SKILL.md", () => {
129
65
  });
130
66
 
131
67
  test("mentions privacy rules", () => {
132
- expect(skillMd).toContain("isPrivate");
133
- expect(skillMd).toContain("MUST NEVER be shared");
68
+ expect(skillMd).toContain("is_private");
69
+ expect(skillMd).toContain("must NEVER be shared");
134
70
  });
135
71
  });
@@ -24,6 +24,7 @@ mock.module("../util/platform.js", () => ({
24
24
  getWorkspaceConfigPath: () => join(TEST_DIR, "config.json"),
25
25
  getWorkspaceSkillsDir: () => join(TEST_DIR, "skills"),
26
26
  getWorkspaceHooksDir: () => join(TEST_DIR, "hooks"),
27
+ getConversationsDir: () => join(TEST_DIR, "conversations"),
27
28
  getWorkspacePromptPath: (file: string) => join(TEST_DIR, file),
28
29
  ensureDataDir: () => {},
29
30
  getPidPath: () => join(TEST_DIR, "vellum.pid"),
@@ -560,4 +561,42 @@ describe("ensurePromptFiles", () => {
560
561
  const bootstrapPath = join(TEST_DIR, "BOOTSTRAP.md");
561
562
  expect(existsSync(bootstrapPath)).toBe(false);
562
563
  });
564
+
565
+ test("auto-deletes stale BOOTSTRAP.md when prior conversations exist", () => {
566
+ // Simulate a non-first-run workspace: core files + BOOTSTRAP.md still present
567
+ writeFileSync(join(TEST_DIR, "IDENTITY.md"), "My identity");
568
+ writeFileSync(join(TEST_DIR, "SOUL.md"), "My soul");
569
+ writeFileSync(join(TEST_DIR, "USER.md"), "My user");
570
+ writeFileSync(join(TEST_DIR, "BOOTSTRAP.md"), "# Stale bootstrap");
571
+
572
+ // Create a conversations directory with at least one entry
573
+ const convDir = join(TEST_DIR, "conversations");
574
+ mkdirSync(convDir, { recursive: true });
575
+ writeFileSync(join(convDir, "conv-001.json"), "{}");
576
+
577
+ ensurePromptFiles();
578
+
579
+ expect(existsSync(join(TEST_DIR, "BOOTSTRAP.md"))).toBe(false);
580
+ });
581
+
582
+ test("keeps BOOTSTRAP.md on first run even if conversations dir exists", () => {
583
+ // First run: no core files exist, BOOTSTRAP.md should be created and kept
584
+ const convDir = join(TEST_DIR, "conversations");
585
+ mkdirSync(convDir, { recursive: true });
586
+ writeFileSync(join(convDir, "conv-001.json"), "{}");
587
+
588
+ ensurePromptFiles();
589
+
590
+ expect(existsSync(join(TEST_DIR, "BOOTSTRAP.md"))).toBe(true);
591
+ });
592
+
593
+ test("keeps BOOTSTRAP.md when no conversations exist yet", () => {
594
+ // Non-first-run but no conversations — user hasn't chatted yet
595
+ writeFileSync(join(TEST_DIR, "IDENTITY.md"), "My identity");
596
+ writeFileSync(join(TEST_DIR, "BOOTSTRAP.md"), "# Bootstrap");
597
+
598
+ ensurePromptFiles();
599
+
600
+ expect(existsSync(join(TEST_DIR, "BOOTSTRAP.md"))).toBe(true);
601
+ });
563
602
  });
@@ -226,7 +226,7 @@ describe("vellum-self-knowledge inline command expansion", () => {
226
226
 
227
227
  // Enable the feature flag via protected directory override
228
228
  _setOverridesForTesting({
229
- "feature_flags.inline-skill-commands.enabled": true,
229
+ "inline-skill-commands": true,
230
230
  });
231
231
  testConfig.skills = { load: { extraDirs: [] } };
232
232
 
@@ -0,0 +1,181 @@
1
+ import { describe, expect, mock, test } from "bun:test";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Mocks — must precede migration import
5
+ // ---------------------------------------------------------------------------
6
+
7
+ // In-memory credential store. Using `let` so tests can reset between runs.
8
+ let store = new Map<string, string>();
9
+ let storeUnreachable = false;
10
+
11
+ mock.module("../security/secure-keys.js", () => ({
12
+ listSecureKeysAsync: async () => ({
13
+ accounts: [...store.keys()],
14
+ unreachable: storeUnreachable,
15
+ }),
16
+ getSecureKeyAsync: async (key: string) => store.get(key),
17
+ setSecureKeyAsync: async (key: string, value: string) => {
18
+ store.set(key, value);
19
+ return true;
20
+ },
21
+ deleteSecureKeyAsync: async (key: string) => {
22
+ store.delete(key);
23
+ },
24
+ }));
25
+
26
+ import { rekeyCompoundCredentialKeysMigration } from "../workspace/migrations/018-rekey-compound-credential-keys.js";
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Helpers
30
+ // ---------------------------------------------------------------------------
31
+
32
+ function resetStore(entries: Record<string, string> = {}): void {
33
+ store = new Map(Object.entries(entries));
34
+ storeUnreachable = false;
35
+ }
36
+
37
+ function storeEntries(): Record<string, string> {
38
+ return Object.fromEntries(store);
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Tests
43
+ // ---------------------------------------------------------------------------
44
+
45
+ describe("018-rekey-compound-credential-keys migration", () => {
46
+ test("has correct migration id", () => {
47
+ expect(rekeyCompoundCredentialKeysMigration.id).toBe(
48
+ "018-rekey-compound-credential-keys",
49
+ );
50
+ });
51
+
52
+ test("run() re-keys compound credential from indexOf to lastIndexOf format", async () => {
53
+ resetStore({
54
+ "credential/integration/google:access_token": "my-token",
55
+ });
56
+
57
+ await rekeyCompoundCredentialKeysMigration.run("/fake");
58
+
59
+ expect(storeEntries()).toEqual({
60
+ "credential/integration:google/access_token": "my-token",
61
+ });
62
+ });
63
+
64
+ test("run() leaves simple single-colon keys unchanged", async () => {
65
+ resetStore({
66
+ "credential/github/token": "gh-token",
67
+ });
68
+
69
+ await rekeyCompoundCredentialKeysMigration.run("/fake");
70
+
71
+ expect(storeEntries()).toEqual({
72
+ "credential/github/token": "gh-token",
73
+ });
74
+ });
75
+
76
+ test("run() ignores non-credential keys", async () => {
77
+ resetStore({
78
+ "other/integration/google:access_token": "my-token",
79
+ });
80
+
81
+ await rekeyCompoundCredentialKeysMigration.run("/fake");
82
+
83
+ expect(storeEntries()).toEqual({
84
+ "other/integration/google:access_token": "my-token",
85
+ });
86
+ });
87
+
88
+ test("run() is idempotent — second run is a no-op", async () => {
89
+ resetStore({
90
+ "credential/integration/google:access_token": "my-token",
91
+ });
92
+
93
+ await rekeyCompoundCredentialKeysMigration.run("/fake");
94
+ const afterFirst = storeEntries();
95
+
96
+ await rekeyCompoundCredentialKeysMigration.run("/fake");
97
+
98
+ expect(storeEntries()).toEqual(afterFirst);
99
+ });
100
+
101
+ test("run() deletes orphaned old key when new key already exists", async () => {
102
+ resetStore({
103
+ "credential/integration/google:access_token": "old-token",
104
+ "credential/integration:google/access_token": "new-token",
105
+ });
106
+
107
+ await rekeyCompoundCredentialKeysMigration.run("/fake");
108
+
109
+ // Old key removed; new key (already present) wins
110
+ expect(storeEntries()).toEqual({
111
+ "credential/integration:google/access_token": "new-token",
112
+ });
113
+ });
114
+
115
+ test("run() throws when credential store is unreachable", async () => {
116
+ resetStore();
117
+ storeUnreachable = true;
118
+
119
+ await expect(
120
+ rekeyCompoundCredentialKeysMigration.run("/fake"),
121
+ ).rejects.toThrow("Credential store unreachable");
122
+ });
123
+
124
+ test("down() reverses run() — re-keys from lastIndexOf back to indexOf format", async () => {
125
+ resetStore({
126
+ "credential/integration:google/access_token": "my-token",
127
+ });
128
+
129
+ await rekeyCompoundCredentialKeysMigration.down("/fake");
130
+
131
+ expect(storeEntries()).toEqual({
132
+ "credential/integration/google:access_token": "my-token",
133
+ });
134
+ });
135
+
136
+ test("down() leaves simple keys unchanged", async () => {
137
+ resetStore({
138
+ "credential/github/token": "gh-token",
139
+ });
140
+
141
+ await rekeyCompoundCredentialKeysMigration.down("/fake");
142
+
143
+ expect(storeEntries()).toEqual({
144
+ "credential/github/token": "gh-token",
145
+ });
146
+ });
147
+
148
+ test("down() is idempotent — second down() is a no-op", async () => {
149
+ resetStore({
150
+ "credential/integration:google/access_token": "my-token",
151
+ });
152
+
153
+ await rekeyCompoundCredentialKeysMigration.down("/fake");
154
+ const afterFirst = storeEntries();
155
+
156
+ await rekeyCompoundCredentialKeysMigration.down("/fake");
157
+
158
+ expect(storeEntries()).toEqual(afterFirst);
159
+ });
160
+
161
+ test("run() then down() restores original state", async () => {
162
+ const original = {
163
+ "credential/integration/google:access_token": "my-token",
164
+ };
165
+ resetStore(original);
166
+
167
+ await rekeyCompoundCredentialKeysMigration.run("/fake");
168
+ await rekeyCompoundCredentialKeysMigration.down("/fake");
169
+
170
+ expect(storeEntries()).toEqual(original);
171
+ });
172
+
173
+ test("down() throws when credential store is unreachable", async () => {
174
+ resetStore();
175
+ storeUnreachable = true;
176
+
177
+ await expect(
178
+ rekeyCompoundCredentialKeysMigration.down("/fake"),
179
+ ).rejects.toThrow("Credential store unreachable");
180
+ });
181
+ });
@@ -284,20 +284,21 @@ describe("011-backfill-installation-id migration", () => {
284
284
  expect(parsed.assistants[0].installationId).toBe("sqlite-id");
285
285
  });
286
286
 
287
- test("respects BASE_DATA_DIR environment variable", () => {
287
+ test("ignores BASE_DATA_DIR and always reads lockfile from homedir", () => {
288
288
  process.env.BASE_DATA_DIR = "/custom-base";
289
289
  getMemoryCheckpointFn.mockReturnValue("sqlite-id");
290
290
 
291
- const customLockPath = "/custom-base/.vellum.lock.json";
291
+ // Lockfile under BASE_DATA_DIR should be ignored — the migration
292
+ // always reads from homedir() (per-user, not per-instance).
292
293
  setupFs({
293
- [customLockPath]: makeLockfile([{ assistantId: "my-assistant" }]),
294
+ [LOCK_PATH]: makeLockfile([{ assistantId: "my-assistant" }]),
294
295
  });
295
296
 
296
297
  backfillInstallationIdMigration.run(WORKSPACE_DIR);
297
298
 
298
299
  expect(writeFileSyncFn).toHaveBeenCalledTimes(1);
299
300
  const [path] = writeFileSyncFn.mock.calls[0] as [string, string];
300
- expect(path).toBe(customLockPath);
301
+ expect(path).toBe(LOCK_PATH);
301
302
  });
302
303
 
303
304
  test("preserves other assistants in lockfile when writing", () => {