@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
@@ -193,13 +193,12 @@ describe("managed proxy integration — credential precedence", () => {
193
193
 
194
194
  const provider = getProvider("anthropic");
195
195
 
196
- // Unwrap RetryProvider → LogfireProvider → AnthropicProvider to inspect
197
- // the Anthropic SDK client's baseURL. The wrappers use private `inner`
198
- // and AnthropicProvider stores the SDK client as private `client`.
196
+ // Unwrap RetryProvider → AnthropicProvider to inspect the Anthropic
197
+ // SDK client's baseURL. RetryProvider stores the inner provider as
198
+ // private `inner` and AnthropicProvider stores the SDK client as
199
+ // private `client`.
199
200
  const retryInner = (provider as any).inner;
200
- // retryInner is the logfire wrapper; it also has an `inner` property
201
- const logfireInner = (retryInner as any).inner ?? retryInner;
202
- const anthropicClient = (logfireInner as any).client;
201
+ const anthropicClient = (retryInner as any).client;
203
202
 
204
203
  expect(anthropicClient).toBeDefined();
205
204
  const baseURL: string = anthropicClient.baseURL;
@@ -312,9 +312,10 @@ describe("QdrantManager", () => {
312
312
  expect(existsSync(pidPath)).toBe(false);
313
313
  }, 10_000);
314
314
 
315
- test("cleans up when process exits immediately", async () => {
315
+ test("fails fast with exit code when process exits immediately", async () => {
316
316
  const pidPath = join(testDataDir, "qdrant", "qdrant.pid");
317
317
 
318
+ // GIVEN a Qdrant binary that exits immediately with code 1
318
319
  placeFakeBinary("#!/bin/sh\nexit 1");
319
320
 
320
321
  const port = getTestPort();
@@ -323,9 +324,34 @@ describe("QdrantManager", () => {
323
324
  ...FAST_TIMEOUTS,
324
325
  });
325
326
 
326
- await expect(mgr.start()).rejects.toThrow("did not become ready");
327
+ // WHEN we start the manager
328
+ const startTime = Date.now();
329
+ await expect(mgr.start()).rejects.toThrow(
330
+ /exited with code \d+ before becoming ready/,
331
+ );
332
+ const elapsed = Date.now() - startTime;
333
+
334
+ // THEN it fails fast (well under the 100ms readyz timeout)
335
+ expect(elapsed).toBeLessThan(FAST_TIMEOUTS.readyzTimeoutMs);
336
+
337
+ // AND the PID file is cleaned up
327
338
  expect(existsSync(pidPath)).toBe(false);
328
339
  }, 10_000);
340
+
341
+ test("includes stderr in error when process crashes", async () => {
342
+ // GIVEN a Qdrant binary that writes to stderr before crashing
343
+ placeFakeBinary('#!/bin/sh\necho "fatal: storage corrupted" >&2\nexit 1');
344
+
345
+ const port = getTestPort();
346
+ const mgr = new QdrantManager({
347
+ url: `http://127.0.0.1:${port}`,
348
+ ...FAST_TIMEOUTS,
349
+ });
350
+
351
+ // WHEN we start the manager
352
+ // THEN the error includes the stderr output
353
+ await expect(mgr.start()).rejects.toThrow("storage corrupted");
354
+ }, 10_000);
329
355
  });
330
356
 
331
357
  // ── Binary Detection ─────────────────────────────────────────
@@ -173,12 +173,6 @@ describe("baseline characterization: hardcoded tool loading", () => {
173
173
  expect(eagerModuleToolNames).not.toContain(name);
174
174
  }
175
175
  });
176
-
177
- test("claude_code is NOT in global registry after initializeTools()", async () => {
178
- await initializeTools();
179
- const tool = getTool("claude_code");
180
- expect(tool).toBeUndefined();
181
- });
182
176
  });
183
177
 
184
178
  describe("baseline characterization: core app tool surface", () => {
@@ -47,10 +47,6 @@ mock.module("../config/loader.js", () => ({
47
47
  }),
48
48
  }));
49
49
 
50
- mock.module("../security/secret-ingress.js", () => ({
51
- checkIngressForSecrets: () => ({ blocked: false }),
52
- }));
53
-
54
50
  import { upsertContact } from "../contacts/contact-store.js";
55
51
  import {
56
52
  linkAttachmentToMessage,
@@ -75,10 +75,6 @@ mock.module("../config/loader.js", () => ({
75
75
  invalidateConfigCache: () => {},
76
76
  }));
77
77
 
78
- mock.module("../logfire.js", () => ({
79
- wrapWithLogfire: (provider: unknown) => provider,
80
- }));
81
-
82
78
  mock.module("../security/secure-keys.js", () => ({
83
79
  getSecureKeyAsync: async (key: string) => secureKeyStore[key],
84
80
  setSecureKeyAsync: async (key: string, value: string) => {
@@ -23,58 +23,13 @@ mock.module("../util/logger.js", () => ({
23
23
  }),
24
24
  }));
25
25
 
26
- // ---------------------------------------------------------------------------
27
- // Broker client mock — set up before importing secure-keys so the
28
- // module-level `createBrokerClient()` call picks up our mock.
29
- // ---------------------------------------------------------------------------
30
-
31
- let mockBrokerAvailable = false;
32
- let mockBrokerStore: Map<string, string> = new Map();
33
- let mockBrokerGetError = false;
34
- let mockBrokerSetError = false;
35
- let mockBrokerDelError = false;
36
- let mockBrokerGetCalled = false;
37
- let mockBrokerSetCalled = false;
38
-
39
- mock.module("../security/keychain-broker-client.js", () => ({
40
- createBrokerClient: () => ({
41
- isAvailable: () => mockBrokerAvailable,
42
- ping: async () => (mockBrokerAvailable ? { pong: true } : null),
43
- get: async (account: string) => {
44
- mockBrokerGetCalled = true;
45
- // null = broker error (fall back to encrypted store)
46
- if (mockBrokerGetError) return null;
47
- const value = mockBrokerStore.get(account);
48
- if (value !== undefined) return { found: true, value };
49
- return { found: false };
50
- },
51
- set: async (account: string, value: string) => {
52
- mockBrokerSetCalled = true;
53
- if (mockBrokerSetError)
54
- return {
55
- status: "rejected" as const,
56
- code: "KEYCHAIN_ERROR",
57
- message: "mock error",
58
- };
59
- mockBrokerStore.set(account, value);
60
- return { status: "ok" as const };
61
- },
62
- del: async (account: string) => {
63
- if (mockBrokerDelError) return false;
64
- const existed = mockBrokerStore.has(account);
65
- mockBrokerStore.delete(account);
66
- return existed;
67
- },
68
- list: async () => Array.from(mockBrokerStore.keys()),
69
- }),
70
- }));
71
-
72
26
  import * as encryptedStore from "../security/encrypted-store.js";
73
27
  import { _setStorePath } from "../security/encrypted-store.js";
74
28
  import {
75
29
  _resetBackend,
76
30
  deleteSecureKeyAsync,
77
31
  getSecureKeyAsync,
32
+ getSecureKeyResultAsync,
78
33
  listSecureKeysAsync,
79
34
  setSecureKeyAsync,
80
35
  } from "../security/secure-keys.js";
@@ -93,17 +48,9 @@ describe("secure-keys", () => {
93
48
  beforeEach(() => {
94
49
  _resetBackend();
95
50
 
96
- // Reset broker mock state
97
- mockBrokerAvailable = false;
98
- mockBrokerStore = new Map();
99
- mockBrokerGetError = false;
100
- mockBrokerSetError = false;
101
- mockBrokerDelError = false;
102
- mockBrokerGetCalled = false;
103
- mockBrokerSetCalled = false;
104
-
105
- // Ensure VELLUM_DEV is NOT set so broker tests work by default
51
+ // Ensure VELLUM_DEV and VELLUM_DESKTOP_APP are NOT set
106
52
  delete process.env.VELLUM_DEV;
53
+ delete process.env.VELLUM_DESKTOP_APP;
107
54
 
108
55
  if (existsSync(TEST_DIR)) {
109
56
  rmSync(TEST_DIR, { recursive: true });
@@ -116,6 +63,7 @@ describe("secure-keys", () => {
116
63
  _setStorePath(null);
117
64
  _resetBackend();
118
65
  delete process.env.VELLUM_DEV;
66
+ delete process.env.VELLUM_DESKTOP_APP;
119
67
  });
120
68
 
121
69
  afterAll(() => {
@@ -125,9 +73,9 @@ describe("secure-keys", () => {
125
73
  });
126
74
 
127
75
  // -----------------------------------------------------------------------
128
- // CRUD operations (via encrypted store backend — broker unavailable)
76
+ // CRUD operations (encrypted store backend)
129
77
  // -----------------------------------------------------------------------
130
- describe("CRUD with encrypted backend (broker unavailable)", () => {
78
+ describe("CRUD with encrypted backend", () => {
131
79
  test("set and get a key", async () => {
132
80
  await setSecureKeyAsync("openai", "sk-openai-789");
133
81
  expect(await getSecureKeyAsync("openai")).toBe("sk-openai-789");
@@ -149,143 +97,90 @@ describe("secure-keys", () => {
149
97
  });
150
98
 
151
99
  // -----------------------------------------------------------------------
152
- // Single-writer: writes go to keychain only when broker available
100
+ // Desktop app uses encrypted store (same as dev/CLI)
153
101
  // -----------------------------------------------------------------------
154
- describe("single-writer with broker available", () => {
155
- test("setSecureKeyAsync writes to broker only (not encrypted store)", async () => {
156
- mockBrokerAvailable = true;
102
+ describe("desktop app uses encrypted store", () => {
103
+ test("VELLUM_DESKTOP_APP=1 writes to encrypted store", async () => {
104
+ process.env.VELLUM_DESKTOP_APP = "1";
157
105
  _resetBackend();
158
106
 
159
107
  const result = await setSecureKeyAsync("api-key", "new-value");
160
108
  expect(result).toBe(true);
161
- // Value is in the broker store
162
- expect(mockBrokerStore.get("api-key")).toBe("new-value");
163
- // Value should NOT be in the encrypted store (single-writer)
164
- expect(encryptedStore.getKey("api-key")).toBeUndefined();
109
+ expect(encryptedStore.getKey("api-key")).toBe("new-value");
165
110
  });
166
111
 
167
- test("setSecureKeyAsync returns false on broker set error", async () => {
168
- mockBrokerAvailable = true;
169
- mockBrokerSetError = true;
112
+ test("VELLUM_DESKTOP_APP=1 reads from encrypted store", async () => {
113
+ process.env.VELLUM_DESKTOP_APP = "1";
170
114
  _resetBackend();
171
115
 
172
- const result = await setSecureKeyAsync("api-key", "new-value");
173
- expect(result).toBe(false);
174
- expect(mockBrokerStore.has("api-key")).toBe(false);
175
- });
176
- });
177
-
178
- // -----------------------------------------------------------------------
179
- // Reads: primary backend first, legacy fallback to encrypted store
180
- // -----------------------------------------------------------------------
181
- describe("reads with broker available", () => {
182
- test("getSecureKeyAsync reads from broker (primary backend)", async () => {
183
- mockBrokerAvailable = true;
184
- _resetBackend();
116
+ encryptedStore.setKey("api-key", "encrypted-value");
185
117
 
186
- mockBrokerStore.set("api-key", "broker-value");
187
118
  const result = await getSecureKeyAsync("api-key");
188
- expect(result).toBe("broker-value");
189
- expect(mockBrokerGetCalled).toBe(true);
190
- });
191
-
192
- test("getSecureKeyAsync falls back to encrypted store for legacy keys", async () => {
193
- mockBrokerAvailable = true;
194
- _resetBackend();
195
-
196
- // Pre-populate encrypted store directly (legacy key not in broker)
197
- encryptedStore.setKey("legacy-key", "legacy-value");
198
-
199
- const result = await getSecureKeyAsync("legacy-key");
200
- expect(result).toBe("legacy-value");
201
- // Broker was checked first (returned nothing), then encrypted store
202
- expect(mockBrokerGetCalled).toBe(true);
203
- });
204
-
205
- test("getSecureKeyAsync returns undefined when neither store has the key", async () => {
206
- mockBrokerAvailable = true;
207
- _resetBackend();
208
-
209
- expect(await getSecureKeyAsync("missing-key")).toBeUndefined();
119
+ expect(result).toBe("encrypted-value");
210
120
  });
211
121
 
212
- test("getSecureKeyAsync returns broker value even when encrypted store also has a value", async () => {
213
- mockBrokerAvailable = true;
122
+ test("VELLUM_DESKTOP_APP=1 deletes from encrypted store", async () => {
123
+ process.env.VELLUM_DESKTOP_APP = "1";
214
124
  _resetBackend();
215
125
 
216
- // Both stores have a value — broker (primary) should win
217
- mockBrokerStore.set("api-key", "broker-value");
218
126
  encryptedStore.setKey("api-key", "encrypted-value");
219
127
 
220
- const result = await getSecureKeyAsync("api-key");
221
- expect(result).toBe("broker-value");
128
+ const result = await deleteSecureKeyAsync("api-key");
129
+ expect(result).toBe("deleted");
130
+ expect(encryptedStore.getKey("api-key")).toBeUndefined();
222
131
  });
223
132
  });
224
133
 
225
134
  // -----------------------------------------------------------------------
226
- // Dev mode bypass — VELLUM_DEV=1 uses encrypted store only
135
+ // Dev mode — VELLUM_DEV=1 uses encrypted store
227
136
  // -----------------------------------------------------------------------
228
- describe("dev mode bypass (VELLUM_DEV=1)", () => {
229
- test("setSecureKeyAsync writes to encrypted store only, ignoring broker", async () => {
137
+ describe("dev mode (VELLUM_DEV=1)", () => {
138
+ test("setSecureKeyAsync writes to encrypted store", async () => {
230
139
  process.env.VELLUM_DEV = "1";
231
- mockBrokerAvailable = true;
232
140
  _resetBackend();
233
141
 
234
142
  const result = await setSecureKeyAsync("api-key", "dev-value");
235
143
  expect(result).toBe(true);
236
- // Written to encrypted store
237
144
  expect(encryptedStore.getKey("api-key")).toBe("dev-value");
238
- // NOT written to broker
239
- expect(mockBrokerStore.has("api-key")).toBe(false);
240
- expect(mockBrokerSetCalled).toBe(false);
241
145
  });
242
146
 
243
- test("getSecureKeyAsync reads from encrypted store only, ignoring broker", async () => {
147
+ test("getSecureKeyAsync reads from encrypted store", async () => {
244
148
  process.env.VELLUM_DEV = "1";
245
- mockBrokerAvailable = true;
246
149
  _resetBackend();
247
150
 
248
- mockBrokerStore.set("api-key", "broker-value");
249
151
  encryptedStore.setKey("api-key", "encrypted-value");
250
152
 
251
153
  const result = await getSecureKeyAsync("api-key");
252
154
  expect(result).toBe("encrypted-value");
253
- // Broker should not have been contacted
254
- expect(mockBrokerGetCalled).toBe(false);
255
155
  });
256
156
 
257
- test("getSecureKeyAsync returns undefined when encrypted store is empty (does not check broker)", async () => {
157
+ test("getSecureKeyAsync returns undefined when encrypted store is empty", async () => {
258
158
  process.env.VELLUM_DEV = "1";
259
- mockBrokerAvailable = true;
260
159
  _resetBackend();
261
160
 
262
- mockBrokerStore.set("api-key", "broker-value");
263
-
264
161
  const result = await getSecureKeyAsync("api-key");
265
162
  expect(result).toBeUndefined();
266
- expect(mockBrokerGetCalled).toBe(false);
267
163
  });
268
164
  });
269
165
 
270
166
  // -----------------------------------------------------------------------
271
- // Delete always attempts both stores
167
+ // Non-desktop topology uses encrypted store
272
168
  // -----------------------------------------------------------------------
273
- describe("delete attempts both stores", () => {
274
- test("deleteSecureKeyAsync removes from both stores when broker available", async () => {
275
- mockBrokerAvailable = true;
169
+ describe("non-desktop topology", () => {
170
+ test("uses encrypted store", async () => {
276
171
  _resetBackend();
277
172
 
278
- mockBrokerStore.set("api-key", "broker-value");
279
- encryptedStore.setKey("api-key", "encrypted-value");
280
-
281
- const result = await deleteSecureKeyAsync("api-key");
282
- expect(result).toBe("deleted");
283
- expect(mockBrokerStore.has("api-key")).toBe(false);
284
- expect(encryptedStore.getKey("api-key")).toBeUndefined();
173
+ const result = await setSecureKeyAsync("api-key", "new-value");
174
+ expect(result).toBe(true);
175
+ expect(encryptedStore.getKey("api-key")).toBe("new-value");
285
176
  });
177
+ });
286
178
 
287
- test("deleteSecureKeyAsync returns deleted when only encrypted store has key", async () => {
288
- // Broker unavailable only encrypted store
179
+ // -----------------------------------------------------------------------
180
+ // Deletesingle backend
181
+ // -----------------------------------------------------------------------
182
+ describe("delete from encrypted store", () => {
183
+ test("deleteSecureKeyAsync removes key from encrypted store", async () => {
289
184
  encryptedStore.setKey("api-key", "encrypted-value");
290
185
 
291
186
  const result = await deleteSecureKeyAsync("api-key");
@@ -293,116 +188,29 @@ describe("secure-keys", () => {
293
188
  expect(encryptedStore.getKey("api-key")).toBeUndefined();
294
189
  });
295
190
 
296
- test("deleteSecureKeyAsync returns error when broker delete fails", async () => {
297
- mockBrokerAvailable = true;
298
- mockBrokerDelError = true;
299
- _resetBackend();
300
-
301
- mockBrokerStore.set("api-key", "broker-value");
302
- encryptedStore.setKey("api-key", "encrypted-value");
303
-
304
- const result = await deleteSecureKeyAsync("api-key");
305
- expect(result).toBe("error");
306
- });
307
-
308
- test("deleteSecureKeyAsync in dev mode still attempts both stores", async () => {
191
+ test("deleteSecureKeyAsync in dev mode deletes from encrypted store", async () => {
309
192
  process.env.VELLUM_DEV = "1";
310
- mockBrokerAvailable = true;
193
+ process.env.VELLUM_DESKTOP_APP = "1";
311
194
  _resetBackend();
312
195
 
313
- mockBrokerStore.set("api-key", "broker-value");
314
196
  encryptedStore.setKey("api-key", "encrypted-value");
315
197
 
316
198
  const result = await deleteSecureKeyAsync("api-key");
317
199
  expect(result).toBe("deleted");
318
- expect(mockBrokerStore.has("api-key")).toBe(false);
319
200
  expect(encryptedStore.getKey("api-key")).toBeUndefined();
320
201
  });
321
202
 
322
- test("deleteSecureKeyAsync returns not-found when key missing from both stores", async () => {
323
- // Broker unavailable, encrypted store empty
203
+ test("deleteSecureKeyAsync returns not-found when key missing", async () => {
324
204
  const result = await deleteSecureKeyAsync("missing-key");
325
205
  expect(result).toBe("not-found");
326
206
  });
327
207
  });
328
208
 
329
209
  // -----------------------------------------------------------------------
330
- // Legacy read fallback
331
- // -----------------------------------------------------------------------
332
- describe("legacy read fallback", () => {
333
- test("returns encrypted store value when broker has no key (legacy migration)", async () => {
334
- mockBrokerAvailable = true;
335
- _resetBackend();
336
-
337
- // Simulate a legacy key that was written to encrypted store before
338
- // the single-writer migration — broker doesn't have it.
339
- encryptedStore.setKey("legacy-account", "legacy-secret");
340
-
341
- const result = await getSecureKeyAsync("legacy-account");
342
- expect(result).toBe("legacy-secret");
343
- });
344
-
345
- test("does not fall back to encrypted store when already using encrypted store backend", async () => {
346
- // Broker unavailable — primary backend IS the encrypted store.
347
- // No fallback needed.
348
- encryptedStore.setKey("account", "value");
349
- encryptedStore.setKey("other-account", "other-value");
350
-
351
- // Should read directly from encrypted store (primary)
352
- const result = await getSecureKeyAsync("account");
353
- expect(result).toBe("value");
354
- // Broker should not have been contacted
355
- expect(mockBrokerGetCalled).toBe(false);
356
- });
357
- });
358
-
359
- // -----------------------------------------------------------------------
360
- // Stale-value prevention
361
- // -----------------------------------------------------------------------
362
- describe("stale-value prevention", () => {
363
- test("setSecureKeyAsync failure does not corrupt broker store", async () => {
364
- mockBrokerAvailable = true;
365
- _resetBackend();
366
-
367
- // Pre-seed broker with original value
368
- mockBrokerStore.set("api-key", "original-value");
369
-
370
- // Now fail the next set
371
- mockBrokerSetError = true;
372
- const ok = await setSecureKeyAsync("api-key", "new-value");
373
- expect(ok).toBe(false);
374
-
375
- // Broker should still have original value
376
- expect(mockBrokerStore.get("api-key")).toBe("original-value");
377
- });
378
- });
379
-
380
- // -----------------------------------------------------------------------
381
- // listSecureKeysAsync — merged/deduplicated key listing
210
+ // listSecureKeysAsync single-backend key listing
382
211
  // -----------------------------------------------------------------------
383
212
  describe("listSecureKeysAsync", () => {
384
- test("returns merged, deduplicated keys when broker is primary and encrypted store has legacy keys", async () => {
385
- mockBrokerAvailable = true;
386
- _resetBackend();
387
-
388
- // Broker has some keys
389
- mockBrokerStore.set("broker-key-1", "val1");
390
- mockBrokerStore.set("shared-key", "broker-val");
391
-
392
- // Encrypted store has legacy keys (some overlapping)
393
- encryptedStore.setKey("legacy-key-1", "val2");
394
- encryptedStore.setKey("shared-key", "enc-val");
395
-
396
- const keys = await listSecureKeysAsync();
397
- expect(keys).toContain("broker-key-1");
398
- expect(keys).toContain("legacy-key-1");
399
- expect(keys).toContain("shared-key");
400
- // Should be exactly 3 unique keys (no duplicates)
401
- expect(keys.length).toBe(3);
402
- });
403
-
404
- test("returns only encrypted store keys when broker is unavailable", async () => {
405
- // Broker unavailable (default state) — primary backend is encrypted store
213
+ test("returns encrypted store keys", async () => {
406
214
  encryptedStore.setKey("enc-key-1", "val1");
407
215
  encryptedStore.setKey("enc-key-2", "val2");
408
216
 
@@ -412,60 +220,72 @@ describe("secure-keys", () => {
412
220
  expect(keys.length).toBe(2);
413
221
  });
414
222
 
415
- test("returns only encrypted store keys when VELLUM_DEV=1 (even if broker available)", async () => {
223
+ test("returns encrypted store keys with VELLUM_DEV=1", async () => {
416
224
  process.env.VELLUM_DEV = "1";
417
- mockBrokerAvailable = true;
418
225
  _resetBackend();
419
226
 
420
- // Broker has keys that should be ignored
421
- mockBrokerStore.set("broker-only", "val1");
422
-
423
- // Encrypted store has keys
424
227
  encryptedStore.setKey("dev-key-1", "val2");
425
228
  encryptedStore.setKey("dev-key-2", "val3");
426
229
 
427
230
  const keys = await listSecureKeysAsync();
428
231
  expect(keys).toContain("dev-key-1");
429
232
  expect(keys).toContain("dev-key-2");
430
- // broker-only key should NOT appear since primary backend is encrypted store
431
- expect(keys).not.toContain("broker-only");
432
233
  expect(keys.length).toBe(2);
433
234
  });
434
235
 
435
- test("returns broker-only keys when encrypted store is empty", async () => {
436
- mockBrokerAvailable = true;
236
+ test("returns encrypted store keys with VELLUM_DESKTOP_APP=1", async () => {
237
+ process.env.VELLUM_DESKTOP_APP = "1";
437
238
  _resetBackend();
438
239
 
439
- mockBrokerStore.set("broker-key-1", "val1");
440
- mockBrokerStore.set("broker-key-2", "val2");
240
+ encryptedStore.setKey("desktop-key-1", "val1");
241
+ encryptedStore.setKey("desktop-key-2", "val2");
441
242
 
442
243
  const keys = await listSecureKeysAsync();
443
- expect(keys).toContain("broker-key-1");
444
- expect(keys).toContain("broker-key-2");
244
+ expect(keys).toContain("desktop-key-1");
245
+ expect(keys).toContain("desktop-key-2");
445
246
  expect(keys.length).toBe(2);
446
247
  });
447
248
 
448
- test("deduplicates keys that exist in both stores", async () => {
449
- mockBrokerAvailable = true;
450
- _resetBackend();
249
+ test("returns empty array when store is empty", async () => {
250
+ const keys = await listSecureKeysAsync();
251
+ expect(keys).toEqual([]);
252
+ });
253
+ });
254
+
255
+ // -----------------------------------------------------------------------
256
+ // getSecureKeyResultAsync — richer result with unreachable flag
257
+ // -----------------------------------------------------------------------
258
+ describe("getSecureKeyResultAsync", () => {
259
+ test("returns value and unreachable false on success", async () => {
260
+ encryptedStore.setKey("api-key", "stored-value");
451
261
 
452
- // Same key in both stores
453
- mockBrokerStore.set("api-key", "broker-val");
454
- encryptedStore.setKey("api-key", "enc-val");
262
+ const result = await getSecureKeyResultAsync("api-key");
263
+ expect(result.value).toBe("stored-value");
264
+ expect(result.unreachable).toBe(false);
265
+ });
455
266
 
456
- const keys = await listSecureKeysAsync();
457
- expect(keys).toContain("api-key");
458
- // Only one copy, not two
459
- expect(keys.length).toBe(1);
460
- expect(keys.filter((k) => k === "api-key").length).toBe(1);
267
+ test("returns unreachable false when key missing (encrypted store always reachable)", async () => {
268
+ const result = await getSecureKeyResultAsync("missing-key");
269
+ expect(result.value).toBeUndefined();
270
+ expect(result.unreachable).toBe(false);
461
271
  });
462
272
 
463
- test("returns empty array when both stores are empty", async () => {
464
- mockBrokerAvailable = true;
273
+ test("returns unreachable false in dev mode", async () => {
274
+ process.env.VELLUM_DEV = "1";
465
275
  _resetBackend();
466
276
 
467
- const keys = await listSecureKeysAsync();
468
- expect(keys).toEqual([]);
277
+ const result = await getSecureKeyResultAsync("missing-key");
278
+ expect(result.value).toBeUndefined();
279
+ expect(result.unreachable).toBe(false);
280
+ });
281
+
282
+ test("returns unreachable false with VELLUM_DESKTOP_APP=1", async () => {
283
+ process.env.VELLUM_DESKTOP_APP = "1";
284
+ _resetBackend();
285
+
286
+ const result = await getSecureKeyResultAsync("missing-key");
287
+ expect(result.value).toBeUndefined();
288
+ expect(result.unreachable).toBe(false);
469
289
  });
470
290
  });
471
291
  });