@vellumai/assistant 0.5.6 → 0.5.7

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 (305) hide show
  1. package/.env.example +16 -2
  2. package/ARCHITECTURE.md +6 -75
  3. package/Dockerfile +1 -1
  4. package/README.md +0 -2
  5. package/bun.lock +0 -414
  6. package/docs/architecture/keychain-broker.md +45 -240
  7. package/docs/architecture/security.md +0 -17
  8. package/docs/credential-execution-service.md +2 -2
  9. package/node_modules/@vellumai/ces-contracts/package.json +1 -0
  10. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +119 -0
  11. package/node_modules/@vellumai/credential-storage/package.json +1 -0
  12. package/node_modules/@vellumai/egress-proxy/package.json +1 -0
  13. package/package.json +2 -3
  14. package/src/__tests__/actor-token-service.test.ts +0 -114
  15. package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
  16. package/src/__tests__/browser-skill-endstate.test.ts +6 -5
  17. package/src/__tests__/btw-routes.test.ts +0 -39
  18. package/src/__tests__/call-domain.test.ts +0 -128
  19. package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
  20. package/src/__tests__/channel-approval-routes.test.ts +0 -5
  21. package/src/__tests__/channel-readiness-service.test.ts +1 -60
  22. package/src/__tests__/checker.test.ts +4 -2
  23. package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
  24. package/src/__tests__/config-schema-cmd.test.ts +0 -1
  25. package/src/__tests__/config-schema.test.ts +1 -1
  26. package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
  27. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  28. package/src/__tests__/conversation-skill-tools.test.ts +0 -54
  29. package/src/__tests__/conversation-title-service.test.ts +87 -0
  30. package/src/__tests__/credential-execution-feature-gates.test.ts +28 -14
  31. package/src/__tests__/credential-execution-managed-contract.test.ts +33 -18
  32. package/src/__tests__/credential-security-e2e.test.ts +0 -66
  33. package/src/__tests__/credential-security-invariants.test.ts +4 -45
  34. package/src/__tests__/credentials-cli.test.ts +78 -0
  35. package/src/__tests__/db-migration-rollback.test.ts +2015 -1
  36. package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
  37. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
  38. package/src/__tests__/guardian-routing-state.test.ts +0 -5
  39. package/src/__tests__/host-shell-tool.test.ts +6 -7
  40. package/src/__tests__/http-user-message-parity.test.ts +3 -103
  41. package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
  42. package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
  43. package/src/__tests__/intent-routing.test.ts +0 -13
  44. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
  45. package/src/__tests__/keychain-broker-client.test.ts +161 -22
  46. package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
  47. package/src/__tests__/migration-export-http.test.ts +2 -2
  48. package/src/__tests__/migration-import-commit-http.test.ts +2 -2
  49. package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
  50. package/src/__tests__/migration-validate-http.test.ts +2 -2
  51. package/src/__tests__/non-member-access-request.test.ts +0 -5
  52. package/src/__tests__/notification-decision-fallback.test.ts +4 -0
  53. package/src/__tests__/notification-decision-identity.test.ts +4 -0
  54. package/src/__tests__/permission-types.test.ts +1 -0
  55. package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
  56. package/src/__tests__/qdrant-manager.test.ts +28 -2
  57. package/src/__tests__/registry.test.ts +0 -6
  58. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
  59. package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
  60. package/src/__tests__/secure-keys.test.ts +83 -263
  61. package/src/__tests__/shell-identity.test.ts +96 -6
  62. package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
  63. package/src/__tests__/skill-feature-flags.test.ts +46 -45
  64. package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
  65. package/src/__tests__/skill-load-inline-command.test.ts +8 -12
  66. package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
  67. package/src/__tests__/skill-load-tool.test.ts +0 -2
  68. package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
  69. package/src/__tests__/skills.test.ts +0 -2
  70. package/src/__tests__/slack-inbound-verification.test.ts +0 -4
  71. package/src/__tests__/suggestion-routes.test.ts +1 -32
  72. package/src/__tests__/system-prompt.test.ts +0 -1
  73. package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
  74. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
  75. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
  76. package/src/__tests__/update-bulletin.test.ts +0 -2
  77. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
  78. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -6
  79. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
  80. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +218 -0
  81. package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
  82. package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
  83. package/src/calls/audio-store.test.ts +97 -0
  84. package/src/calls/audio-store.ts +205 -0
  85. package/src/calls/call-controller.ts +85 -7
  86. package/src/calls/call-domain.ts +3 -0
  87. package/src/calls/call-store.ts +10 -3
  88. package/src/calls/fish-audio-client.ts +117 -0
  89. package/src/calls/relay-server.ts +27 -0
  90. package/src/calls/twilio-routes.ts +2 -1
  91. package/src/calls/types.ts +1 -0
  92. package/src/calls/voice-ingress-preflight.ts +0 -42
  93. package/src/calls/voice-quality.ts +26 -5
  94. package/src/calls/voice-session-bridge.ts +6 -12
  95. package/src/cli/commands/config.ts +1 -4
  96. package/src/cli/commands/credentials.ts +34 -4
  97. package/src/cli/commands/oauth/index.ts +7 -0
  98. package/src/cli/commands/oauth/platform.ts +179 -0
  99. package/src/cli/commands/platform.ts +3 -3
  100. package/src/config/assistant-feature-flags.ts +186 -5
  101. package/src/config/bundled-skills/messaging/SKILL.md +5 -5
  102. package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
  103. package/src/config/bundled-skills/settings/TOOLS.json +2 -2
  104. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
  105. package/src/config/bundled-tool-registry.ts +1 -11
  106. package/src/config/env-registry.ts +1 -1
  107. package/src/config/env.ts +8 -14
  108. package/src/config/feature-flag-registry.json +48 -8
  109. package/src/config/loader.ts +98 -31
  110. package/src/config/schema.ts +4 -13
  111. package/src/config/schemas/calls.ts +13 -0
  112. package/src/config/schemas/fish-audio.ts +39 -0
  113. package/src/config/schemas/security.ts +0 -4
  114. package/src/config/types.ts +0 -1
  115. package/src/contacts/contact-store.ts +39 -0
  116. package/src/contacts/types.ts +2 -0
  117. package/src/credential-execution/approval-bridge.ts +1 -0
  118. package/src/credential-execution/executable-discovery.ts +28 -4
  119. package/src/credential-execution/feature-gates.ts +16 -0
  120. package/src/credential-execution/process-manager.ts +38 -0
  121. package/src/daemon/assistant-attachments.ts +9 -0
  122. package/src/daemon/config-watcher.ts +5 -0
  123. package/src/daemon/conversation-tool-setup.ts +0 -105
  124. package/src/daemon/conversation.ts +10 -1
  125. package/src/daemon/handlers/config-vercel.ts +92 -0
  126. package/src/daemon/handlers/skills.ts +2 -15
  127. package/src/daemon/install-symlink.ts +195 -0
  128. package/src/daemon/lifecycle.ts +227 -51
  129. package/src/daemon/message-types/conversations.ts +3 -4
  130. package/src/daemon/message-types/diagnostics.ts +3 -22
  131. package/src/daemon/message-types/messages.ts +0 -2
  132. package/src/daemon/message-types/upgrades.ts +8 -0
  133. package/src/daemon/server.ts +30 -92
  134. package/src/events/domain-events.ts +2 -1
  135. package/src/inbound/platform-callback-registration.ts +3 -3
  136. package/src/instrument.ts +8 -5
  137. package/src/memory/conversation-title-service.ts +50 -1
  138. package/src/memory/db-init.ts +12 -0
  139. package/src/memory/items-extractor.ts +15 -1
  140. package/src/memory/job-handlers/conversation-starters.ts +4 -1
  141. package/src/memory/jobs-store.ts +30 -5
  142. package/src/memory/jobs-worker.ts +31 -7
  143. package/src/memory/migrations/001-job-deferrals.ts +19 -0
  144. package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
  145. package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
  146. package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
  147. package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
  148. package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
  149. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
  150. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
  151. package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
  152. package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
  153. package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
  154. package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
  155. package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
  156. package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
  157. package/src/memory/migrations/116-messages-fts.ts +106 -1
  158. package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
  159. package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
  160. package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
  161. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
  162. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
  163. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
  164. package/src/memory/migrations/141-rename-verification-table.ts +54 -0
  165. package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
  166. package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
  167. package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
  168. package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
  169. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
  170. package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
  171. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
  172. package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
  173. package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
  174. package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
  175. package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
  176. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
  177. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
  178. package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
  179. package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
  180. package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
  181. package/src/memory/migrations/index.ts +4 -0
  182. package/src/memory/migrations/registry.ts +90 -0
  183. package/src/memory/migrations/validate-migration-state.ts +137 -11
  184. package/src/memory/qdrant-circuit-breaker.ts +9 -0
  185. package/src/memory/qdrant-manager.ts +64 -7
  186. package/src/memory/schema/calls.ts +1 -0
  187. package/src/memory/schema/contacts.ts +1 -0
  188. package/src/notifications/decision-engine.ts +4 -1
  189. package/src/oauth/connection-resolver.ts +6 -4
  190. package/src/permissions/checker.ts +0 -38
  191. package/src/permissions/shell-identity.ts +76 -22
  192. package/src/permissions/types.ts +4 -2
  193. package/src/platform/client.ts +35 -7
  194. package/src/prompts/persona-resolver.ts +138 -0
  195. package/src/prompts/system-prompt.ts +36 -4
  196. package/src/prompts/templates/users/default.md +1 -0
  197. package/src/providers/registry.ts +27 -40
  198. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
  199. package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
  200. package/src/runtime/auth/external-assistant-id.ts +13 -59
  201. package/src/runtime/auth/route-policy.ts +15 -1
  202. package/src/runtime/auth/token-service.ts +43 -138
  203. package/src/runtime/channel-readiness-service.ts +1 -16
  204. package/src/runtime/http-server.ts +27 -2
  205. package/src/runtime/middleware/error-handler.ts +1 -9
  206. package/src/runtime/routes/audio-routes.ts +40 -0
  207. package/src/runtime/routes/btw-routes.ts +0 -17
  208. package/src/runtime/routes/conversation-query-routes.ts +63 -1
  209. package/src/runtime/routes/conversation-routes.ts +4 -44
  210. package/src/runtime/routes/diagnostics-routes.ts +1 -477
  211. package/src/runtime/routes/identity-routes.ts +18 -29
  212. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
  213. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
  214. package/src/runtime/routes/integrations/vercel.ts +89 -0
  215. package/src/runtime/routes/log-export-routes.ts +5 -0
  216. package/src/runtime/routes/memory-item-routes.ts +24 -6
  217. package/src/runtime/routes/migration-rollback-routes.ts +209 -0
  218. package/src/runtime/routes/migration-routes.ts +17 -1
  219. package/src/runtime/routes/notification-routes.ts +58 -0
  220. package/src/runtime/routes/schedule-routes.ts +65 -0
  221. package/src/runtime/routes/settings-routes.ts +41 -1
  222. package/src/runtime/routes/tts-routes.ts +86 -0
  223. package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
  224. package/src/runtime/routes/workspace-commit-routes.ts +62 -0
  225. package/src/runtime/routes/workspace-routes.test.ts +22 -1
  226. package/src/runtime/routes/workspace-routes.ts +1 -1
  227. package/src/runtime/routes/workspace-utils.ts +86 -2
  228. package/src/security/ces-credential-client.ts +59 -22
  229. package/src/security/ces-rpc-credential-backend.ts +85 -0
  230. package/src/security/credential-backend.ts +12 -88
  231. package/src/security/keychain-broker-client.ts +10 -2
  232. package/src/security/secure-keys.ts +94 -113
  233. package/src/skills/catalog-install.ts +13 -7
  234. package/src/telemetry/usage-telemetry-reporter.ts +4 -2
  235. package/src/tools/calls/call-start.ts +1 -0
  236. package/src/tools/executor.ts +0 -4
  237. package/src/tools/network/script-proxy/session-manager.ts +19 -4
  238. package/src/tools/network/web-fetch.ts +3 -1
  239. package/src/tools/skills/execute.ts +1 -1
  240. package/src/tools/types.ts +0 -8
  241. package/src/util/errors.ts +0 -12
  242. package/src/util/platform.ts +3 -50
  243. package/src/workspace/git-service.ts +5 -2
  244. package/src/workspace/migrations/001-avatar-rename.ts +15 -0
  245. package/src/workspace/migrations/003-seed-device-id.ts +17 -1
  246. package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
  247. package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
  248. package/src/workspace/migrations/006-services-config.ts +49 -0
  249. package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
  250. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
  251. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
  252. package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
  253. package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
  254. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
  255. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
  256. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
  257. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
  258. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
  259. package/src/workspace/migrations/017-seed-persona-dirs.ts +95 -0
  260. package/src/workspace/migrations/migrate-to-workspace-volume.ts +23 -1
  261. package/src/workspace/migrations/registry.ts +8 -0
  262. package/src/workspace/migrations/runner.ts +106 -2
  263. package/src/workspace/migrations/types.ts +4 -0
  264. package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
  265. package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
  266. package/src/__tests__/diagnostics-export.test.ts +0 -288
  267. package/src/__tests__/local-gateway-health.test.ts +0 -209
  268. package/src/__tests__/secret-ingress-handler.test.ts +0 -120
  269. package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
  270. package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
  271. package/src/__tests__/swarm-orchestrator.test.ts +0 -463
  272. package/src/__tests__/swarm-plan-validator.test.ts +0 -384
  273. package/src/__tests__/swarm-recursion.test.ts +0 -197
  274. package/src/__tests__/swarm-router-planner.test.ts +0 -234
  275. package/src/__tests__/swarm-tool.test.ts +0 -185
  276. package/src/__tests__/swarm-worker-backend.test.ts +0 -144
  277. package/src/__tests__/swarm-worker-runner.test.ts +0 -288
  278. package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
  279. package/src/commands/cc-command-registry.ts +0 -248
  280. package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
  281. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
  282. package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
  283. package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
  284. package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
  285. package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
  286. package/src/config/schemas/swarm.ts +0 -82
  287. package/src/logfire.ts +0 -135
  288. package/src/runtime/local-gateway-health.ts +0 -275
  289. package/src/security/secret-ingress.ts +0 -68
  290. package/src/swarm/backend-claude-code.ts +0 -225
  291. package/src/swarm/checkpoint.ts +0 -137
  292. package/src/swarm/graph-utils.ts +0 -53
  293. package/src/swarm/index.ts +0 -55
  294. package/src/swarm/limits.ts +0 -66
  295. package/src/swarm/orchestrator.ts +0 -424
  296. package/src/swarm/plan-validator.ts +0 -117
  297. package/src/swarm/router-planner.ts +0 -162
  298. package/src/swarm/router-prompts.ts +0 -39
  299. package/src/swarm/synthesizer.ts +0 -81
  300. package/src/swarm/types.ts +0 -72
  301. package/src/swarm/worker-backend.ts +0 -131
  302. package/src/swarm/worker-prompts.ts +0 -80
  303. package/src/swarm/worker-runner.ts +0 -170
  304. package/src/tools/claude-code/claude-code.ts +0 -610
  305. package/src/tools/swarm/delegate.ts +0 -205
@@ -3,7 +3,7 @@
3
3
  * tools, even when conversation history contains old markers for those skills.
4
4
  */
5
5
  import * as realFs from "node:fs";
6
- import { beforeEach, describe, expect, mock, test } from "bun:test";
6
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
7
7
 
8
8
  import type { SkillSummary, SkillToolManifest } from "../config/skills.js";
9
9
  import { RiskLevel } from "../permissions/types.js";
@@ -38,23 +38,6 @@ mock.module("../config/loader.js", () => ({
38
38
  invalidateConfigCache: () => {},
39
39
  }));
40
40
 
41
- mock.module("../config/assistant-feature-flags.js", () => ({
42
- isAssistantFeatureFlagEnabled: (
43
- key: string,
44
- config: Record<string, unknown>,
45
- ) => {
46
- const vals = (
47
- config as {
48
- assistantFeatureFlagValues?: Record<string, boolean>;
49
- }
50
- ).assistantFeatureFlagValues;
51
- if (vals && typeof vals[key] === "boolean") return vals[key];
52
- return true; // default enabled
53
- },
54
- loadDefaultsRegistry: () => ({}),
55
- getAssistantFeatureFlagDefaults: () => ({}),
56
- }));
57
-
58
41
  mock.module("../config/skill-state.js", () => ({
59
42
  skillFlagKey: (skill: { featureFlag?: string }) =>
60
43
  skill.featureFlag
@@ -62,6 +45,24 @@ mock.module("../config/skill-state.js", () => ({
62
45
  : undefined,
63
46
  }));
64
47
 
48
+ // Mock assistant-feature-flags to avoid loading the real module (which
49
+ // triggers file I/O and env-registry imports that hang in test context).
50
+ let _mockOverrides: Record<string, boolean> = {};
51
+ mock.module("../config/assistant-feature-flags.js", () => ({
52
+ isAssistantFeatureFlagEnabled: (key: string, _config: unknown): boolean => {
53
+ const explicit = _mockOverrides[key];
54
+ if (typeof explicit === "boolean") return explicit;
55
+ return true; // undeclared flags default to enabled
56
+ },
57
+ clearFeatureFlagOverridesCache: () => {
58
+ _mockOverrides = {};
59
+ },
60
+ _setOverridesForTesting: (overrides: Record<string, boolean>) => {
61
+ _mockOverrides = { ...overrides };
62
+ },
63
+ getAssistantFeatureFlagDefaults: () => ({}),
64
+ }));
65
+
65
66
  mock.module("../skills/active-skill-tools.js", () => {
66
67
  const parseMarkers = (messages: Message[]) => {
67
68
  const skillLoadUseIds = new Set<string>();
@@ -216,6 +217,10 @@ mock.module("../util/logger.js", () => ({
216
217
 
217
218
  const { projectSkillTools, resetSkillToolProjection } =
218
219
  await import("../daemon/conversation-skill-tools.js");
220
+ const { _setOverridesForTesting } =
221
+ (await import("../config/assistant-feature-flags.js")) as {
222
+ _setOverridesForTesting: (o: Record<string, boolean>) => void;
223
+ };
219
224
 
220
225
  // ---------------------------------------------------------------------------
221
226
  // Helpers
@@ -289,9 +294,14 @@ describe("projectSkillTools feature flag enforcement", () => {
289
294
  mockUnregisteredSkillIds = [];
290
295
  mockSkillRefCount = new Map();
291
296
  currentConfig = {};
297
+ _setOverridesForTesting({});
292
298
  resetSkillToolProjection();
293
299
  });
294
300
 
301
+ afterEach(() => {
302
+ _setOverridesForTesting({});
303
+ });
304
+
295
305
  test("no skill tools projected for flag OFF skill even with old markers", () => {
296
306
  mockCatalog = [makeSkill(DECLARED_SKILL_ID, DECLARED_SKILL_ID)];
297
307
  mockManifests = {
@@ -302,10 +312,8 @@ describe("projectSkillTools feature flag enforcement", () => {
302
312
  const history = buildHistoryWithMarker(DECLARED_SKILL_ID);
303
313
  const prevActive = new Map<string, string>();
304
314
 
305
- // Feature flag is OFF
306
- currentConfig = {
307
- assistantFeatureFlagValues: { [DECLARED_FLAG_KEY]: false },
308
- };
315
+ // Feature flag is OFF — use protected directory override
316
+ _setOverridesForTesting({ [DECLARED_FLAG_KEY]: false });
309
317
 
310
318
  const result = projectSkillTools(history, {
311
319
  previouslyActiveSkillIds: prevActive,
@@ -325,10 +333,8 @@ describe("projectSkillTools feature flag enforcement", () => {
325
333
  const history = buildHistoryWithMarker(DECLARED_SKILL_ID);
326
334
  const prevActive = new Map<string, string>();
327
335
 
328
- // Feature flag is ON
329
- currentConfig = {
330
- assistantFeatureFlagValues: { [DECLARED_FLAG_KEY]: true },
331
- };
336
+ // Feature flag is ON — use protected directory override
337
+ _setOverridesForTesting({ [DECLARED_FLAG_KEY]: true });
332
338
 
333
339
  const result = projectSkillTools(history, {
334
340
  previouslyActiveSkillIds: prevActive,
@@ -419,9 +425,7 @@ describe("projectSkillTools feature flag enforcement", () => {
419
425
  const prevActive = new Map<string, string>();
420
426
 
421
427
  // Declared skill is OFF, plain-skill is undeclared with no persisted override so remains ON.
422
- currentConfig = {
423
- assistantFeatureFlagValues: { [DECLARED_FLAG_KEY]: false },
424
- };
428
+ _setOverridesForTesting({ [DECLARED_FLAG_KEY]: false });
425
429
 
426
430
  const result = projectSkillTools(history, {
427
431
  previouslyActiveSkillIds: prevActive,
@@ -40,8 +40,6 @@ mock.module("../util/platform.js", () => ({
40
40
  getWorkspacePromptPath: (file: string) => join(TEST_DIR, file),
41
41
  readSessionToken: () => null,
42
42
  normalizeAssistantId: (id: string) => id,
43
- readLockfile: () => null,
44
- writeLockfile: () => {},
45
43
  }));
46
44
 
47
45
  const noopLogger = {
@@ -38,10 +38,6 @@ mock.module("../util/logger.js", () => ({
38
38
  }),
39
39
  }));
40
40
 
41
- mock.module("../security/secret-ingress.js", () => ({
42
- checkIngressForSecrets: () => ({ blocked: false }),
43
- }));
44
-
45
41
  mock.module("../config/env.js", () => ({
46
42
  isHttpAuthDisabled: () => true,
47
43
  getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
@@ -2,8 +2,7 @@
2
2
  * Unit tests for the GET /v1/suggestion endpoint (handleGetSuggestion).
3
3
  *
4
4
  * Validates happy path, all null-return paths, caching, staleness check,
5
- * quote stripping, word-boundary truncation, empty response rejection,
6
- * and modelIntent verification.
5
+ * quote stripping, empty response rejection, and modelIntent verification.
7
6
  */
8
7
 
9
8
  import { describe, expect, mock, test } from "bun:test";
@@ -293,36 +292,6 @@ describe("GET /v1/suggestion", () => {
293
292
  expect(body.suggestion).toBe("Sure, let's go!");
294
293
  });
295
294
 
296
- test("truncates long suggestions at word boundary", async () => {
297
- // A 60-char string that will exceed the 50-char limit
298
- const longText =
299
- "This is a really long suggestion that goes well beyond fifty chars";
300
- const provider = makeMockProvider(longText);
301
- mockGetConfiguredProvider.mockImplementation(async () => provider);
302
- mockGetConversationByKey.mockImplementation(() => ({
303
- conversationId: "conv-test",
304
- }));
305
- mockGetMessages.mockImplementation(() => [
306
- {
307
- id: "msg-asst-1",
308
- conversationId: "conv-test",
309
- role: "assistant",
310
- content: JSON.stringify([{ type: "text", text: "Hello there" }]),
311
- createdAt: Date.now(),
312
- metadata: null,
313
- },
314
- ]);
315
-
316
- const url = makeUrl({ conversationKey: "test-key" });
317
- const deps = makeDeps();
318
- const res = await handleGetSuggestion(url, deps);
319
- const body = (await res.json()) as { suggestion: string };
320
-
321
- expect(body.suggestion.length).toBeLessThanOrEqual(50);
322
- // Should end at a word boundary (no partial words)
323
- expect(body.suggestion).not.toMatch(/\s$/);
324
- });
325
-
326
295
  test("rejects empty LLM response", async () => {
327
296
  const provider = makeMockProvider("");
328
297
  mockGetConfiguredProvider.mockImplementation(async () => provider);
@@ -86,7 +86,6 @@ mock.module("../config/loader.js", () => ({
86
86
  invalidateConfigCache: () => {},
87
87
  getNestedValue: () => undefined,
88
88
  setNestedValue: () => {},
89
- syncConfigToLockfile: () => {},
90
89
  }));
91
90
 
92
91
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -330,7 +330,7 @@ describe("ToolExecutor → real shell allowlist integration", () => {
330
330
  expect(patterns).toContain("action:git");
331
331
  });
332
332
 
333
- test("pipeline command produces only exact option", async () => {
333
+ test("pipeline command produces exact + action-key options", async () => {
334
334
  const { prompter, getAllowlist } = makeCapturingPrompter();
335
335
  const executor = new ToolExecutor(prompter);
336
336
 
@@ -343,9 +343,11 @@ describe("ToolExecutor → real shell allowlist integration", () => {
343
343
  const allowlist = getAllowlist();
344
344
  expect(allowlist).toBeDefined();
345
345
 
346
- // Pipelines are complex commands — only exact option, no action keys
347
- expect(allowlist!.length).toBe(1);
346
+ // Pipelines now produce exact option + action key options
347
+ expect(allowlist!.length).toBeGreaterThanOrEqual(2);
348
348
  expect(allowlist![0].pattern).toBe("cat file.txt | grep error");
349
349
  expect(allowlist![0].description).toContain("compound");
350
+ // Action keys from the first segment before the pipe
351
+ expect(allowlist!.some((o) => o.pattern.startsWith("action:"))).toBe(true);
350
352
  });
351
353
  });
@@ -41,11 +41,6 @@ mock.module("../util/logger.js", () => ({
41
41
  }),
42
42
  }));
43
43
 
44
- // Mock security check to always pass
45
- mock.module("../security/secret-ingress.js", () => ({
46
- checkIngressForSecrets: () => ({ blocked: false }),
47
- }));
48
-
49
44
  mock.module("../config/env.js", () => ({
50
45
  isHttpAuthDisabled: () => true,
51
46
  getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
@@ -36,10 +36,6 @@ mock.module("../util/logger.js", () => ({
36
36
  }),
37
37
  }));
38
38
 
39
- mock.module("../security/secret-ingress.js", () => ({
40
- checkIngressForSecrets: () => ({ blocked: false }),
41
- }));
42
-
43
39
  mock.module("../config/env.js", () => ({
44
40
  isHttpAuthDisabled: () => true,
45
41
  getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
@@ -66,8 +66,6 @@ mock.module("../util/platform.js", () => ({
66
66
  getSandboxWorkingDir: () => "",
67
67
  getInterfacesDir: () => "",
68
68
  getClipboardCommand: () => null,
69
- readLockfile: () => null,
70
- writeLockfile: () => {},
71
69
  readPlatformToken: () => null,
72
70
  readSessionToken: () => null,
73
71
  getTCPPort: () => 8765,
@@ -61,9 +61,7 @@ const platformOverrides: Record<string, (...args: unknown[]) => unknown> = {
61
61
  getSessionTokenPath: () => join(TEST_DIR, "session-token"),
62
62
  readSessionToken: () => null,
63
63
  getClipboardCommand: () => null,
64
- readLockfile: () => null,
65
64
  normalizeAssistantId: (id: unknown) => String(id),
66
- writeLockfile: () => {},
67
65
  getEmbeddingModelsDir: () => join(TEST_DIR, "embedding-models"),
68
66
  getTCPPort: () => 8765,
69
67
  isTCPEnabled: () => false,
@@ -141,7 +139,6 @@ interface TestConfig {
141
139
  permissions: { mode: "strict" | "workspace" };
142
140
  skills: { load: { extraDirs: string[] } };
143
141
  sandbox: { enabled: boolean };
144
- assistantFeatureFlagValues?: Record<string, boolean>;
145
142
  [key: string]: unknown;
146
143
  }
147
144
 
@@ -149,9 +146,6 @@ const testConfig: TestConfig = {
149
146
  permissions: { mode: "workspace" },
150
147
  skills: { load: { extraDirs: [] } },
151
148
  sandbox: { enabled: true },
152
- assistantFeatureFlagValues: {
153
- "feature_flags.inline-skill-commands.enabled": true,
154
- },
155
149
  };
156
150
 
157
151
  mock.module("../config/loader.js", () => ({
@@ -169,6 +163,8 @@ mock.module("../config/loader.js", () => ({
169
163
 
170
164
  await import("../tools/skills/load.js");
171
165
  const { getTool } = await import("../tools/registry.js");
166
+ const { _setOverridesForTesting } =
167
+ await import("../config/assistant-feature-flags.js");
172
168
 
173
169
  // ── Helpers ──────────────────────────────────────────────────────────────
174
170
 
@@ -228,16 +224,17 @@ describe("vellum-self-knowledge inline command expansion", () => {
228
224
  ) => mockRunInlineCommand(command, workingDir),
229
225
  }));
230
226
 
231
- // Enable the feature flag
232
- testConfig.assistantFeatureFlagValues = {
227
+ // Enable the feature flag via protected directory override
228
+ _setOverridesForTesting({
233
229
  "feature_flags.inline-skill-commands.enabled": true,
234
- };
230
+ });
235
231
  testConfig.skills = { load: { extraDirs: [] } };
236
232
 
237
233
  installSelfKnowledgeSkill();
238
234
  });
239
235
 
240
236
  afterEach(() => {
237
+ _setOverridesForTesting({});
241
238
  if (existsSync(TEST_DIR)) {
242
239
  rmSync(TEST_DIR, { recursive: true, force: true });
243
240
  }
@@ -70,12 +70,6 @@ mock.module("../config/loader.js", () => ({
70
70
  }),
71
71
  }));
72
72
 
73
- // ── Secret ingress mock ────────────────────────────────────────────
74
-
75
- mock.module("../security/secret-ingress.js", () => ({
76
- checkIngressForSecrets: () => ({ blocked: false }),
77
- }));
78
-
79
73
  // ── Assistant event hub mock ───────────────────────────────────────
80
74
 
81
75
  mock.module("../runtime/assistant-event-hub.js", () => ({
@@ -0,0 +1,252 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Mock state
5
+ // ---------------------------------------------------------------------------
6
+
7
+ const isAvailableFn = mock((): boolean => true);
8
+ const brokerSetFn = mock(
9
+ async (
10
+ _account: string,
11
+ _value: string,
12
+ ): Promise<{ status: string; code?: string; message?: string }> => ({
13
+ status: "ok",
14
+ }),
15
+ );
16
+ const createBrokerClientFn = mock(() => ({
17
+ isAvailable: isAvailableFn,
18
+ set: brokerSetFn,
19
+ }));
20
+
21
+ const listKeysFn = mock((): string[] => []);
22
+ const getKeyFn = mock((_account: string): string | undefined => undefined);
23
+ const deleteKeyFn = mock(
24
+ (_account: string): "deleted" | "not-found" | "error" => "deleted",
25
+ );
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Mock modules — before importing module under test
29
+ //
30
+ // The logger is mocked with a silent Proxy to suppress pino output in tests.
31
+ // The broker client and encrypted store are mocked to control migration
32
+ // behavior without touching real keychain or filesystem state.
33
+ // ---------------------------------------------------------------------------
34
+
35
+ mock.module("../util/logger.js", () => ({
36
+ getLogger: () =>
37
+ new Proxy({} as Record<string, unknown>, {
38
+ get: () => () => {},
39
+ }),
40
+ }));
41
+
42
+ mock.module("../security/keychain-broker-client.js", () => ({
43
+ createBrokerClient: createBrokerClientFn,
44
+ }));
45
+
46
+ mock.module("../security/encrypted-store.js", () => ({
47
+ listKeys: listKeysFn,
48
+ getKey: getKeyFn,
49
+ deleteKey: deleteKeyFn,
50
+ }));
51
+
52
+ // Import after mocking
53
+ import { migrateCredentialsToKeychainMigration } from "../workspace/migrations/015-migrate-credentials-to-keychain.js";
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Helpers
57
+ // ---------------------------------------------------------------------------
58
+
59
+ const WORKSPACE_DIR = "/mock-home/.vellum/workspace";
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Tests
63
+ // ---------------------------------------------------------------------------
64
+
65
+ describe("015-migrate-credentials-to-keychain migration", () => {
66
+ beforeEach(() => {
67
+ isAvailableFn.mockClear();
68
+ brokerSetFn.mockClear();
69
+ createBrokerClientFn.mockClear();
70
+ listKeysFn.mockClear();
71
+ getKeyFn.mockClear();
72
+ deleteKeyFn.mockClear();
73
+
74
+ // Defaults: mac production build
75
+ process.env.VELLUM_DESKTOP_APP = "1";
76
+ delete process.env.VELLUM_DEV;
77
+
78
+ isAvailableFn.mockReturnValue(true);
79
+ brokerSetFn.mockResolvedValue({ status: "ok" });
80
+ listKeysFn.mockReturnValue([]);
81
+ getKeyFn.mockReturnValue(undefined);
82
+ deleteKeyFn.mockReturnValue("deleted");
83
+ });
84
+
85
+ test("has correct migration id", () => {
86
+ expect(migrateCredentialsToKeychainMigration.id).toBe(
87
+ "015-migrate-credentials-to-keychain",
88
+ );
89
+ });
90
+
91
+ test("skips when VELLUM_DESKTOP_APP is not set", async () => {
92
+ delete process.env.VELLUM_DESKTOP_APP;
93
+
94
+ await migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR);
95
+
96
+ expect(createBrokerClientFn).not.toHaveBeenCalled();
97
+ expect(listKeysFn).not.toHaveBeenCalled();
98
+ });
99
+
100
+ test("skips when VELLUM_DESKTOP_APP is not '1'", async () => {
101
+ process.env.VELLUM_DESKTOP_APP = "0";
102
+
103
+ await migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR);
104
+
105
+ expect(createBrokerClientFn).not.toHaveBeenCalled();
106
+ });
107
+
108
+ test("skips when VELLUM_DEV=1", async () => {
109
+ process.env.VELLUM_DEV = "1";
110
+
111
+ await migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR);
112
+
113
+ expect(createBrokerClientFn).not.toHaveBeenCalled();
114
+ expect(listKeysFn).not.toHaveBeenCalled();
115
+ });
116
+
117
+ test(
118
+ "throws when broker is not available after max retry attempts",
119
+ async () => {
120
+ isAvailableFn.mockReturnValue(false);
121
+
122
+ await expect(
123
+ migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR),
124
+ ).rejects.toThrow(
125
+ "Keychain broker not available after waiting — credential migration will be retried on next startup",
126
+ );
127
+
128
+ // Should have retried isAvailable multiple times
129
+ expect(isAvailableFn.mock.calls.length).toBeGreaterThan(1);
130
+
131
+ // Should not proceed to list or migrate keys
132
+ expect(listKeysFn).not.toHaveBeenCalled();
133
+ expect(brokerSetFn).not.toHaveBeenCalled();
134
+ },
135
+ { timeout: 10_000 },
136
+ );
137
+
138
+ test("succeeds when broker becomes available after retry", async () => {
139
+ // Broker unavailable for first 3 calls, then available
140
+ let callCount = 0;
141
+ isAvailableFn.mockImplementation(() => {
142
+ callCount++;
143
+ return callCount > 3;
144
+ });
145
+ listKeysFn.mockReturnValue(["retry-key"]);
146
+ getKeyFn.mockReturnValue("retry-secret");
147
+ brokerSetFn.mockResolvedValue({ status: "ok" });
148
+
149
+ await migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR);
150
+
151
+ // Should have called isAvailable 4 times (3 false + 1 true)
152
+ expect(isAvailableFn).toHaveBeenCalledTimes(4);
153
+
154
+ // Should have proceeded with migration
155
+ expect(brokerSetFn).toHaveBeenCalledWith("retry-key", "retry-secret");
156
+ expect(deleteKeyFn).toHaveBeenCalledWith("retry-key");
157
+ });
158
+
159
+ test("no-ops when encrypted store has no keys", async () => {
160
+ listKeysFn.mockReturnValue([]);
161
+
162
+ await migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR);
163
+
164
+ expect(brokerSetFn).not.toHaveBeenCalled();
165
+ expect(deleteKeyFn).not.toHaveBeenCalled();
166
+ });
167
+
168
+ test("successfully migrates keys from encrypted store to keychain", async () => {
169
+ listKeysFn.mockReturnValue(["account-a", "account-b"]);
170
+ getKeyFn.mockImplementation((account: string) => {
171
+ if (account === "account-a") return "secret-a";
172
+ if (account === "account-b") return "secret-b";
173
+ return undefined;
174
+ });
175
+ brokerSetFn.mockResolvedValue({ status: "ok" });
176
+
177
+ await migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR);
178
+
179
+ // Should have called broker.set for each key
180
+ expect(brokerSetFn).toHaveBeenCalledTimes(2);
181
+ expect(brokerSetFn).toHaveBeenCalledWith("account-a", "secret-a");
182
+ expect(brokerSetFn).toHaveBeenCalledWith("account-b", "secret-b");
183
+
184
+ // Should have deleted each key from encrypted store after successful migration
185
+ expect(deleteKeyFn).toHaveBeenCalledTimes(2);
186
+ expect(deleteKeyFn).toHaveBeenCalledWith("account-a");
187
+ expect(deleteKeyFn).toHaveBeenCalledWith("account-b");
188
+ });
189
+
190
+ test("continues on individual key failure and migrates others", async () => {
191
+ listKeysFn.mockReturnValue(["fail-key", "ok-key"]);
192
+ getKeyFn.mockImplementation((account: string) => {
193
+ if (account === "fail-key") return "fail-secret";
194
+ if (account === "ok-key") return "ok-secret";
195
+ return undefined;
196
+ });
197
+ brokerSetFn.mockImplementation(async (account: string) => {
198
+ if (account === "fail-key") {
199
+ return {
200
+ status: "rejected" as const,
201
+ code: "UNKNOWN",
202
+ message: "broker rejected",
203
+ };
204
+ }
205
+ return { status: "ok" as const };
206
+ });
207
+
208
+ await migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR);
209
+
210
+ // fail-key should NOT have been deleted (broker rejected it)
211
+ expect(deleteKeyFn).not.toHaveBeenCalledWith("fail-key");
212
+
213
+ // ok-key should have been migrated and deleted
214
+ expect(brokerSetFn).toHaveBeenCalledWith("ok-key", "ok-secret");
215
+ expect(deleteKeyFn).toHaveBeenCalledWith("ok-key");
216
+ expect(deleteKeyFn).toHaveBeenCalledTimes(1);
217
+ });
218
+
219
+ test("handles getKey returning undefined for a listed key", async () => {
220
+ listKeysFn.mockReturnValue(["ghost-key", "real-key"]);
221
+ getKeyFn.mockImplementation((account: string) => {
222
+ if (account === "ghost-key") return undefined;
223
+ if (account === "real-key") return "real-secret";
224
+ return undefined;
225
+ });
226
+ brokerSetFn.mockResolvedValue({ status: "ok" });
227
+
228
+ await migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR);
229
+
230
+ // ghost-key should not be sent to broker or deleted
231
+ expect(brokerSetFn).not.toHaveBeenCalledWith(
232
+ "ghost-key",
233
+ expect.anything(),
234
+ );
235
+ expect(deleteKeyFn).not.toHaveBeenCalledWith("ghost-key");
236
+
237
+ // real-key should be migrated
238
+ expect(brokerSetFn).toHaveBeenCalledWith("real-key", "real-secret");
239
+ expect(deleteKeyFn).toHaveBeenCalledWith("real-key");
240
+ });
241
+
242
+ test("handles broker unreachable status for individual keys", async () => {
243
+ listKeysFn.mockReturnValue(["key-1"]);
244
+ getKeyFn.mockReturnValue("secret-1");
245
+ brokerSetFn.mockResolvedValue({ status: "unreachable" });
246
+
247
+ await migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR);
248
+
249
+ // Should not delete when broker is unreachable
250
+ expect(deleteKeyFn).not.toHaveBeenCalled();
251
+ });
252
+ });