@vellumai/assistant 0.5.6 → 0.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (442) hide show
  1. package/.env.example +16 -2
  2. package/ARCHITECTURE.md +6 -75
  3. package/Dockerfile +3 -2
  4. package/README.md +0 -2
  5. package/bun.lock +0 -414
  6. package/docker-entrypoint.sh +9 -0
  7. package/docs/architecture/keychain-broker.md +45 -240
  8. package/docs/architecture/memory.md +13 -11
  9. package/docs/architecture/security.md +0 -17
  10. package/docs/credential-execution-service.md +2 -2
  11. package/node_modules/@vellumai/ces-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
  13. package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
  14. package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
  15. package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
  16. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +120 -1
  17. package/node_modules/@vellumai/credential-storage/package.json +1 -0
  18. package/node_modules/@vellumai/egress-proxy/package.json +1 -0
  19. package/package.json +2 -3
  20. package/src/__tests__/actor-token-service.test.ts +0 -114
  21. package/src/__tests__/approval-cascade.test.ts +0 -1
  22. package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
  23. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  24. package/src/__tests__/browser-skill-endstate.test.ts +6 -5
  25. package/src/__tests__/btw-routes.test.ts +0 -39
  26. package/src/__tests__/call-controller.test.ts +0 -1
  27. package/src/__tests__/call-domain.test.ts +0 -128
  28. package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
  29. package/src/__tests__/ces-startup-timeout.test.ts +40 -0
  30. package/src/__tests__/channel-approval-routes.test.ts +0 -5
  31. package/src/__tests__/channel-readiness-service.test.ts +1 -60
  32. package/src/__tests__/checker.test.ts +4 -2
  33. package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
  34. package/src/__tests__/config-schema-cmd.test.ts +0 -2
  35. package/src/__tests__/config-schema.test.ts +3 -1
  36. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
  37. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
  38. package/src/__tests__/conversation-agent-loop.test.ts +2 -4
  39. package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
  40. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
  41. package/src/__tests__/conversation-error.test.ts +15 -1
  42. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  43. package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
  44. package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
  45. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
  46. package/src/__tests__/conversation-queue.test.ts +0 -1
  47. package/src/__tests__/conversation-skill-tools.test.ts +0 -54
  48. package/src/__tests__/conversation-slash-queue.test.ts +0 -1
  49. package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
  50. package/src/__tests__/conversation-title-service.test.ts +87 -0
  51. package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
  52. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
  53. package/src/__tests__/credential-execution-client.test.ts +5 -2
  54. package/src/__tests__/credential-execution-feature-gates.test.ts +59 -30
  55. package/src/__tests__/credential-execution-managed-contract.test.ts +35 -20
  56. package/src/__tests__/credential-security-e2e.test.ts +1 -67
  57. package/src/__tests__/credential-security-invariants.test.ts +6 -50
  58. package/src/__tests__/credentials-cli.test.ts +82 -3
  59. package/src/__tests__/daemon-credential-client.test.ts +123 -0
  60. package/src/__tests__/db-migration-rollback.test.ts +2015 -1
  61. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
  62. package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
  63. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
  64. package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
  65. package/src/__tests__/guardian-routing-state.test.ts +0 -5
  66. package/src/__tests__/host-shell-tool.test.ts +6 -7
  67. package/src/__tests__/http-user-message-parity.test.ts +3 -103
  68. package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
  69. package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
  70. package/src/__tests__/intent-routing.test.ts +0 -13
  71. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
  72. package/src/__tests__/journal-context.test.ts +335 -0
  73. package/src/__tests__/keychain-broker-client.test.ts +161 -22
  74. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
  75. package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
  76. package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
  77. package/src/__tests__/memory-recall-quality.test.ts +48 -17
  78. package/src/__tests__/memory-regressions.test.ts +408 -363
  79. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
  80. package/src/__tests__/migration-export-http.test.ts +2 -2
  81. package/src/__tests__/migration-import-commit-http.test.ts +2 -2
  82. package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
  83. package/src/__tests__/migration-validate-http.test.ts +2 -2
  84. package/src/__tests__/non-member-access-request.test.ts +2 -7
  85. package/src/__tests__/notification-decision-fallback.test.ts +4 -0
  86. package/src/__tests__/notification-decision-identity.test.ts +4 -0
  87. package/src/__tests__/notification-decision-strategy.test.ts +71 -0
  88. package/src/__tests__/oauth-cli.test.ts +5 -1
  89. package/src/__tests__/permission-types.test.ts +1 -0
  90. package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
  91. package/src/__tests__/provider-error-scenarios.test.ts +0 -267
  92. package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
  93. package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
  94. package/src/__tests__/qdrant-manager.test.ts +28 -2
  95. package/src/__tests__/registry.test.ts +0 -6
  96. package/src/__tests__/relay-server.test.ts +1 -2
  97. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
  98. package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
  99. package/src/__tests__/secret-onetime-send.test.ts +1 -1
  100. package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
  101. package/src/__tests__/secure-keys.test.ts +95 -272
  102. package/src/__tests__/shell-identity.test.ts +96 -6
  103. package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
  104. package/src/__tests__/skill-feature-flags.test.ts +46 -45
  105. package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
  106. package/src/__tests__/skill-load-inline-command.test.ts +8 -12
  107. package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
  108. package/src/__tests__/skill-load-tool.test.ts +0 -2
  109. package/src/__tests__/skill-memory.test.ts +17 -3
  110. package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
  111. package/src/__tests__/skills.test.ts +0 -2
  112. package/src/__tests__/slack-inbound-verification.test.ts +0 -4
  113. package/src/__tests__/stale-approval-dedup.test.ts +171 -0
  114. package/src/__tests__/stt-hints.test.ts +437 -0
  115. package/src/__tests__/suggestion-routes.test.ts +1 -32
  116. package/src/__tests__/system-prompt.test.ts +0 -1
  117. package/src/__tests__/task-memory-cleanup.test.ts +14 -0
  118. package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
  119. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
  120. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
  121. package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
  122. package/src/__tests__/update-bulletin.test.ts +0 -2
  123. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
  124. package/src/__tests__/voice-quality.test.ts +58 -0
  125. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -7
  126. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
  127. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +220 -0
  128. package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
  129. package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
  130. package/src/acp/agent-process.ts +9 -1
  131. package/src/agent/loop.ts +1 -1
  132. package/src/approvals/guardian-request-resolvers.ts +164 -38
  133. package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
  134. package/src/calls/audio-store.test.ts +97 -0
  135. package/src/calls/audio-store.ts +205 -0
  136. package/src/calls/call-controller.ts +90 -8
  137. package/src/calls/call-domain.ts +3 -0
  138. package/src/calls/call-store.ts +10 -3
  139. package/src/calls/fish-audio-client.ts +129 -0
  140. package/src/calls/relay-server.ts +27 -0
  141. package/src/calls/stt-hints.ts +189 -0
  142. package/src/calls/tts-text-sanitizer.ts +61 -0
  143. package/src/calls/twilio-routes.ts +34 -5
  144. package/src/calls/types.ts +1 -0
  145. package/src/calls/voice-ingress-preflight.ts +0 -42
  146. package/src/calls/voice-quality.ts +38 -5
  147. package/src/calls/voice-session-bridge.ts +7 -12
  148. package/src/cli/commands/avatar.ts +2 -2
  149. package/src/cli/commands/config.ts +1 -4
  150. package/src/cli/commands/credentials.ts +128 -82
  151. package/src/cli/commands/doctor.ts +2 -2
  152. package/src/cli/commands/keys.ts +7 -7
  153. package/src/cli/commands/memory.ts +1 -1
  154. package/src/cli/commands/oauth/connections.ts +11 -29
  155. package/src/cli/commands/oauth/index.ts +7 -0
  156. package/src/cli/commands/oauth/platform.ts +525 -0
  157. package/src/cli/commands/platform.ts +3 -3
  158. package/src/cli/lib/daemon-credential-client.ts +284 -0
  159. package/src/cli.ts +1 -1
  160. package/src/config/assistant-feature-flags.ts +186 -5
  161. package/src/config/bundled-skills/AGENTS.md +34 -0
  162. package/src/config/bundled-skills/acp/SKILL.md +10 -0
  163. package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
  164. package/src/config/bundled-skills/messaging/SKILL.md +5 -5
  165. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  166. package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
  167. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
  168. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
  169. package/src/config/bundled-skills/settings/SKILL.md +15 -2
  170. package/src/config/bundled-skills/settings/TOOLS.json +47 -2
  171. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
  172. package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
  173. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
  174. package/src/config/bundled-skills/slack/SKILL.md +1 -1
  175. package/src/config/bundled-tool-registry.ts +5 -11
  176. package/src/config/defaults.ts +0 -2
  177. package/src/config/env-registry.ts +5 -5
  178. package/src/config/env.ts +21 -14
  179. package/src/config/feature-flag-registry.json +49 -9
  180. package/src/config/loader.ts +106 -42
  181. package/src/config/schema.ts +9 -29
  182. package/src/config/schemas/calls.ts +30 -0
  183. package/src/config/schemas/fish-audio.ts +39 -0
  184. package/src/config/schemas/inference.ts +2 -2
  185. package/src/config/schemas/journal.ts +16 -0
  186. package/src/config/schemas/memory-processing.ts +2 -2
  187. package/src/config/schemas/security.ts +0 -4
  188. package/src/config/types.ts +1 -1
  189. package/src/contacts/contact-store.ts +39 -0
  190. package/src/contacts/types.ts +2 -0
  191. package/src/credential-execution/approval-bridge.ts +1 -0
  192. package/src/credential-execution/executable-discovery.ts +28 -4
  193. package/src/credential-execution/feature-gates.ts +16 -0
  194. package/src/credential-execution/process-manager.ts +38 -0
  195. package/src/credential-execution/startup-timeout.ts +36 -0
  196. package/src/daemon/approval-generators.ts +3 -9
  197. package/src/daemon/assistant-attachments.ts +9 -0
  198. package/src/daemon/config-watcher.ts +5 -0
  199. package/src/daemon/conversation-error.ts +13 -1
  200. package/src/daemon/conversation-memory.ts +1 -2
  201. package/src/daemon/conversation-process.ts +18 -1
  202. package/src/daemon/conversation-surfaces.ts +30 -1
  203. package/src/daemon/conversation-tool-setup.ts +0 -105
  204. package/src/daemon/conversation.ts +21 -1
  205. package/src/daemon/guardian-action-generators.ts +3 -9
  206. package/src/daemon/handlers/config-vercel.ts +92 -0
  207. package/src/daemon/handlers/skills.ts +2 -15
  208. package/src/daemon/install-symlink.ts +195 -0
  209. package/src/daemon/lifecycle.ts +234 -51
  210. package/src/daemon/message-types/conversations.ts +4 -4
  211. package/src/daemon/message-types/diagnostics.ts +3 -22
  212. package/src/daemon/message-types/messages.ts +0 -2
  213. package/src/daemon/message-types/upgrades.ts +8 -0
  214. package/src/daemon/server.ts +32 -95
  215. package/src/events/domain-events.ts +2 -1
  216. package/src/inbound/platform-callback-registration.ts +3 -3
  217. package/src/instrument.ts +8 -5
  218. package/src/memory/app-store.ts +31 -0
  219. package/src/memory/conversation-title-service.ts +50 -1
  220. package/src/memory/db-init.ts +16 -0
  221. package/src/memory/indexer.ts +19 -10
  222. package/src/memory/items-extractor.ts +328 -321
  223. package/src/memory/job-handlers/conversation-starters.ts +4 -1
  224. package/src/memory/job-handlers/summarization.ts +26 -16
  225. package/src/memory/jobs-store.ts +63 -6
  226. package/src/memory/jobs-worker.ts +31 -7
  227. package/src/memory/journal-memory.ts +214 -0
  228. package/src/memory/migrations/001-job-deferrals.ts +19 -0
  229. package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
  230. package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
  231. package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
  232. package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
  233. package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
  234. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
  235. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
  236. package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
  237. package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
  238. package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
  239. package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
  240. package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
  241. package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
  242. package/src/memory/migrations/116-messages-fts.ts +106 -1
  243. package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
  244. package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
  245. package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
  246. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
  247. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
  248. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
  249. package/src/memory/migrations/141-rename-verification-table.ts +54 -0
  250. package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
  251. package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
  252. package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
  253. package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
  254. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
  255. package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
  256. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
  257. package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
  258. package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
  259. package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
  260. package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
  261. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
  262. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
  263. package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
  264. package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
  265. package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
  266. package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
  267. package/src/memory/migrations/index.ts +5 -0
  268. package/src/memory/migrations/registry.ts +98 -0
  269. package/src/memory/migrations/validate-migration-state.ts +137 -11
  270. package/src/memory/qdrant-circuit-breaker.ts +9 -0
  271. package/src/memory/qdrant-manager.ts +64 -7
  272. package/src/memory/retriever.test.ts +37 -25
  273. package/src/memory/retriever.ts +24 -49
  274. package/src/memory/schema/calls.ts +1 -0
  275. package/src/memory/schema/contacts.ts +1 -0
  276. package/src/memory/schema/memory-core.ts +2 -0
  277. package/src/memory/search/formatting.ts +7 -44
  278. package/src/memory/search/staleness.ts +4 -0
  279. package/src/memory/search/tier-classifier.ts +10 -2
  280. package/src/memory/search/types.ts +2 -5
  281. package/src/memory/task-memory-cleanup.ts +4 -3
  282. package/src/notifications/adapters/slack.ts +168 -6
  283. package/src/notifications/broadcaster.ts +1 -0
  284. package/src/notifications/copy-composer.ts +59 -2
  285. package/src/notifications/decision-engine.ts +4 -1
  286. package/src/notifications/signal.ts +2 -0
  287. package/src/notifications/types.ts +2 -0
  288. package/src/oauth/connection-resolver.ts +6 -4
  289. package/src/permissions/checker.ts +0 -38
  290. package/src/permissions/shell-identity.ts +76 -22
  291. package/src/permissions/types.ts +4 -2
  292. package/src/platform/client.ts +35 -7
  293. package/src/prompts/journal-context.ts +133 -0
  294. package/src/prompts/persona-resolver.ts +194 -0
  295. package/src/prompts/system-prompt.ts +44 -4
  296. package/src/prompts/templates/SOUL.md +10 -0
  297. package/src/prompts/templates/users/default.md +1 -0
  298. package/src/providers/provider-send-message.ts +3 -32
  299. package/src/providers/registry.ts +29 -179
  300. package/src/providers/types.ts +1 -1
  301. package/src/runtime/access-request-helper.ts +4 -0
  302. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
  303. package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
  304. package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
  305. package/src/runtime/auth/external-assistant-id.ts +13 -59
  306. package/src/runtime/auth/route-policy.ts +17 -1
  307. package/src/runtime/auth/token-service.ts +43 -138
  308. package/src/runtime/channel-readiness-service.ts +1 -16
  309. package/src/runtime/gateway-client.ts +47 -4
  310. package/src/runtime/guardian-decision-types.ts +45 -4
  311. package/src/runtime/http-server.ts +31 -3
  312. package/src/runtime/middleware/error-handler.ts +1 -9
  313. package/src/runtime/routes/access-request-decision.ts +2 -2
  314. package/src/runtime/routes/app-management-routes.ts +2 -1
  315. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
  316. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
  317. package/src/runtime/routes/audio-routes.ts +40 -0
  318. package/src/runtime/routes/btw-routes.ts +0 -17
  319. package/src/runtime/routes/channel-readiness-routes.ts +9 -4
  320. package/src/runtime/routes/conversation-query-routes.ts +63 -1
  321. package/src/runtime/routes/conversation-routes.ts +4 -44
  322. package/src/runtime/routes/debug-routes.ts +12 -9
  323. package/src/runtime/routes/diagnostics-routes.ts +1 -477
  324. package/src/runtime/routes/guardian-approval-interception.ts +168 -11
  325. package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
  326. package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
  327. package/src/runtime/routes/identity-routes.ts +19 -30
  328. package/src/runtime/routes/inbound-message-handler.ts +31 -1
  329. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
  330. package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
  331. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
  332. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
  333. package/src/runtime/routes/integrations/twilio.ts +52 -10
  334. package/src/runtime/routes/integrations/vercel.ts +89 -0
  335. package/src/runtime/routes/log-export-routes.ts +5 -0
  336. package/src/runtime/routes/memory-item-routes.test.ts +3 -3
  337. package/src/runtime/routes/memory-item-routes.ts +46 -14
  338. package/src/runtime/routes/migration-rollback-routes.ts +209 -0
  339. package/src/runtime/routes/migration-routes.ts +17 -1
  340. package/src/runtime/routes/notification-routes.ts +58 -0
  341. package/src/runtime/routes/schedule-routes.ts +65 -0
  342. package/src/runtime/routes/secret-routes.ts +141 -10
  343. package/src/runtime/routes/settings-routes.ts +41 -1
  344. package/src/runtime/routes/tts-routes.ts +96 -0
  345. package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
  346. package/src/runtime/routes/workspace-commit-routes.ts +62 -0
  347. package/src/runtime/routes/workspace-routes.test.ts +22 -1
  348. package/src/runtime/routes/workspace-routes.ts +1 -1
  349. package/src/runtime/routes/workspace-utils.ts +86 -2
  350. package/src/security/ces-credential-client.ts +75 -29
  351. package/src/security/ces-rpc-credential-backend.ts +86 -0
  352. package/src/security/credential-backend.ts +22 -92
  353. package/src/security/keychain-broker-client.ts +10 -2
  354. package/src/security/secure-keys.ts +113 -115
  355. package/src/skills/catalog-install.ts +6 -32
  356. package/src/skills/skill-memory.ts +1 -0
  357. package/src/subagent/manager.ts +2 -5
  358. package/src/telemetry/usage-telemetry-reporter.ts +4 -2
  359. package/src/tools/acp/spawn.ts +78 -1
  360. package/src/tools/calls/call-start.ts +1 -0
  361. package/src/tools/credentials/vault.ts +5 -3
  362. package/src/tools/executor.ts +0 -4
  363. package/src/tools/memory/definitions.ts +3 -2
  364. package/src/tools/memory/handlers.ts +10 -7
  365. package/src/tools/network/script-proxy/session-manager.ts +19 -4
  366. package/src/tools/network/web-fetch.ts +3 -1
  367. package/src/tools/skills/execute.ts +1 -1
  368. package/src/tools/terminal/safe-env.ts +1 -0
  369. package/src/tools/types.ts +0 -8
  370. package/src/util/browser.ts +15 -0
  371. package/src/util/errors.ts +0 -12
  372. package/src/util/platform.ts +4 -51
  373. package/src/workspace/git-service.ts +5 -2
  374. package/src/workspace/migrations/001-avatar-rename.ts +15 -0
  375. package/src/workspace/migrations/003-seed-device-id.ts +17 -1
  376. package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
  377. package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
  378. package/src/workspace/migrations/006-services-config.ts +49 -0
  379. package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
  380. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
  381. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
  382. package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
  383. package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
  384. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
  385. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
  386. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
  387. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
  388. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
  389. package/src/workspace/migrations/017-seed-persona-dirs.ts +96 -0
  390. package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
  391. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
  392. package/src/workspace/migrations/migrate-to-workspace-volume.ts +27 -5
  393. package/src/workspace/migrations/registry.ts +12 -0
  394. package/src/workspace/migrations/runner.ts +106 -2
  395. package/src/workspace/migrations/types.ts +4 -0
  396. package/src/workspace/provider-commit-message-generator.ts +12 -21
  397. package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
  398. package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
  399. package/src/__tests__/diagnostics-export.test.ts +0 -288
  400. package/src/__tests__/local-gateway-health.test.ts +0 -209
  401. package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
  402. package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
  403. package/src/__tests__/secret-ingress-handler.test.ts +0 -120
  404. package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
  405. package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
  406. package/src/__tests__/swarm-orchestrator.test.ts +0 -463
  407. package/src/__tests__/swarm-plan-validator.test.ts +0 -384
  408. package/src/__tests__/swarm-recursion.test.ts +0 -197
  409. package/src/__tests__/swarm-router-planner.test.ts +0 -234
  410. package/src/__tests__/swarm-tool.test.ts +0 -185
  411. package/src/__tests__/swarm-worker-backend.test.ts +0 -144
  412. package/src/__tests__/swarm-worker-runner.test.ts +0 -288
  413. package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
  414. package/src/commands/cc-command-registry.ts +0 -248
  415. package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
  416. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
  417. package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
  418. package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
  419. package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
  420. package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
  421. package/src/config/schemas/swarm.ts +0 -82
  422. package/src/logfire.ts +0 -135
  423. package/src/memory/search/lexical.ts +0 -48
  424. package/src/providers/failover.ts +0 -186
  425. package/src/runtime/local-gateway-health.ts +0 -275
  426. package/src/security/secret-ingress.ts +0 -68
  427. package/src/swarm/backend-claude-code.ts +0 -225
  428. package/src/swarm/checkpoint.ts +0 -137
  429. package/src/swarm/graph-utils.ts +0 -53
  430. package/src/swarm/index.ts +0 -55
  431. package/src/swarm/limits.ts +0 -66
  432. package/src/swarm/orchestrator.ts +0 -424
  433. package/src/swarm/plan-validator.ts +0 -117
  434. package/src/swarm/router-planner.ts +0 -162
  435. package/src/swarm/router-prompts.ts +0 -39
  436. package/src/swarm/synthesizer.ts +0 -81
  437. package/src/swarm/types.ts +0 -72
  438. package/src/swarm/worker-backend.ts +0 -131
  439. package/src/swarm/worker-prompts.ts +0 -80
  440. package/src/swarm/worker-runner.ts +0 -170
  441. package/src/tools/claude-code/claude-code.ts +0 -610
  442. package/src/tools/swarm/delegate.ts +0 -205
@@ -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,179 +188,107 @@ 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
210
+ // listSecureKeysAsync single-backend key listing
331
211
  // -----------------------------------------------------------------------
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
- });
212
+ describe("listSecureKeysAsync", () => {
213
+ test("returns encrypted store keys", async () => {
214
+ encryptedStore.setKey("enc-key-1", "val1");
215
+ encryptedStore.setKey("enc-key-2", "val2");
344
216
 
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);
217
+ const result = await listSecureKeysAsync();
218
+ expect(result.unreachable).toBe(false);
219
+ expect(result.accounts).toContain("enc-key-1");
220
+ expect(result.accounts).toContain("enc-key-2");
221
+ expect(result.accounts.length).toBe(2);
356
222
  });
357
- });
358
223
 
359
- // -----------------------------------------------------------------------
360
- // Stale-value prevention
361
- // -----------------------------------------------------------------------
362
- describe("stale-value prevention", () => {
363
- test("setSecureKeyAsync failure does not corrupt broker store", async () => {
364
- mockBrokerAvailable = true;
224
+ test("returns encrypted store keys with VELLUM_DEV=1", async () => {
225
+ process.env.VELLUM_DEV = "1";
365
226
  _resetBackend();
366
227
 
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);
228
+ encryptedStore.setKey("dev-key-1", "val2");
229
+ encryptedStore.setKey("dev-key-2", "val3");
374
230
 
375
- // Broker should still have original value
376
- expect(mockBrokerStore.get("api-key")).toBe("original-value");
231
+ const result = await listSecureKeysAsync();
232
+ expect(result.unreachable).toBe(false);
233
+ expect(result.accounts).toContain("dev-key-1");
234
+ expect(result.accounts).toContain("dev-key-2");
235
+ expect(result.accounts.length).toBe(2);
377
236
  });
378
- });
379
237
 
380
- // -----------------------------------------------------------------------
381
- // listSecureKeysAsync — merged/deduplicated key listing
382
- // -----------------------------------------------------------------------
383
- describe("listSecureKeysAsync", () => {
384
- test("returns merged, deduplicated keys when broker is primary and encrypted store has legacy keys", async () => {
385
- mockBrokerAvailable = true;
238
+ test("returns encrypted store keys with VELLUM_DESKTOP_APP=1", async () => {
239
+ process.env.VELLUM_DESKTOP_APP = "1";
386
240
  _resetBackend();
387
241
 
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");
242
+ encryptedStore.setKey("desktop-key-1", "val1");
243
+ encryptedStore.setKey("desktop-key-2", "val2");
395
244
 
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);
245
+ const result = await listSecureKeysAsync();
246
+ expect(result.unreachable).toBe(false);
247
+ expect(result.accounts).toContain("desktop-key-1");
248
+ expect(result.accounts).toContain("desktop-key-2");
249
+ expect(result.accounts.length).toBe(2);
402
250
  });
403
251
 
404
- test("returns only encrypted store keys when broker is unavailable", async () => {
405
- // Broker unavailable (default state) — primary backend is encrypted store
406
- encryptedStore.setKey("enc-key-1", "val1");
407
- encryptedStore.setKey("enc-key-2", "val2");
408
-
409
- const keys = await listSecureKeysAsync();
410
- expect(keys).toContain("enc-key-1");
411
- expect(keys).toContain("enc-key-2");
412
- expect(keys.length).toBe(2);
252
+ test("returns empty accounts when store is empty", async () => {
253
+ const result = await listSecureKeysAsync();
254
+ expect(result).toEqual({ accounts: [], unreachable: false });
413
255
  });
256
+ });
414
257
 
415
- test("returns only encrypted store keys when VELLUM_DEV=1 (even if broker available)", async () => {
416
- process.env.VELLUM_DEV = "1";
417
- mockBrokerAvailable = true;
418
- _resetBackend();
419
-
420
- // Broker has keys that should be ignored
421
- mockBrokerStore.set("broker-only", "val1");
422
-
423
- // Encrypted store has keys
424
- encryptedStore.setKey("dev-key-1", "val2");
425
- encryptedStore.setKey("dev-key-2", "val3");
258
+ // -----------------------------------------------------------------------
259
+ // getSecureKeyResultAsync — richer result with unreachable flag
260
+ // -----------------------------------------------------------------------
261
+ describe("getSecureKeyResultAsync", () => {
262
+ test("returns value and unreachable false on success", async () => {
263
+ encryptedStore.setKey("api-key", "stored-value");
426
264
 
427
- const keys = await listSecureKeysAsync();
428
- expect(keys).toContain("dev-key-1");
429
- 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
- expect(keys.length).toBe(2);
265
+ const result = await getSecureKeyResultAsync("api-key");
266
+ expect(result.value).toBe("stored-value");
267
+ expect(result.unreachable).toBe(false);
433
268
  });
434
269
 
435
- test("returns broker-only keys when encrypted store is empty", async () => {
436
- mockBrokerAvailable = true;
437
- _resetBackend();
438
-
439
- mockBrokerStore.set("broker-key-1", "val1");
440
- mockBrokerStore.set("broker-key-2", "val2");
441
-
442
- const keys = await listSecureKeysAsync();
443
- expect(keys).toContain("broker-key-1");
444
- expect(keys).toContain("broker-key-2");
445
- expect(keys.length).toBe(2);
270
+ test("returns unreachable false when key missing (encrypted store always reachable)", async () => {
271
+ const result = await getSecureKeyResultAsync("missing-key");
272
+ expect(result.value).toBeUndefined();
273
+ expect(result.unreachable).toBe(false);
446
274
  });
447
275
 
448
- test("deduplicates keys that exist in both stores", async () => {
449
- mockBrokerAvailable = true;
276
+ test("returns unreachable false in dev mode", async () => {
277
+ process.env.VELLUM_DEV = "1";
450
278
  _resetBackend();
451
279
 
452
- // Same key in both stores
453
- mockBrokerStore.set("api-key", "broker-val");
454
- encryptedStore.setKey("api-key", "enc-val");
455
-
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);
280
+ const result = await getSecureKeyResultAsync("missing-key");
281
+ expect(result.value).toBeUndefined();
282
+ expect(result.unreachable).toBe(false);
461
283
  });
462
284
 
463
- test("returns empty array when both stores are empty", async () => {
464
- mockBrokerAvailable = true;
285
+ test("returns unreachable false with VELLUM_DESKTOP_APP=1", async () => {
286
+ process.env.VELLUM_DESKTOP_APP = "1";
465
287
  _resetBackend();
466
288
 
467
- const keys = await listSecureKeysAsync();
468
- expect(keys).toEqual([]);
289
+ const result = await getSecureKeyResultAsync("missing-key");
290
+ expect(result.value).toBeUndefined();
291
+ expect(result.unreachable).toBe(false);
469
292
  });
470
293
  });
471
294
  });
@@ -82,12 +82,76 @@ describe("deriveShellActionKeys", () => {
82
82
  ]);
83
83
  });
84
84
 
85
- test("pipelines are marked non-simple", async () => {
85
+ test("pipelines are marked non-simple but produce action keys", async () => {
86
86
  const analysis = await analyzeShellCommand("git log | grep fix");
87
87
  const result = deriveShellActionKeys(analysis);
88
88
 
89
89
  expect(result.isSimpleAction).toBe(false);
90
- expect(result.keys).toHaveLength(0);
90
+ expect(result.keys).toEqual([
91
+ { key: "action:git log", depth: 2 },
92
+ { key: "action:git", depth: 1 },
93
+ ]);
94
+ });
95
+
96
+ test("pipeline extracts action keys from first segment", async () => {
97
+ const analysis = await analyzeShellCommand(
98
+ "pdftotext file.pdf | head -100",
99
+ );
100
+ const result = deriveShellActionKeys(analysis);
101
+
102
+ expect(result.isSimpleAction).toBe(false);
103
+ // file.pdf is treated as a subcommand token (doesn't start with . or contain /)
104
+ expect(result.keys).toEqual([
105
+ { key: "action:pdftotext file.pdf", depth: 2 },
106
+ { key: "action:pdftotext", depth: 1 },
107
+ ]);
108
+ });
109
+
110
+ test("setup-prefix + pipeline extracts action keys", async () => {
111
+ const analysis = await analyzeShellCommand(
112
+ "cd /tmp && pdftotext file.pdf | grep oil",
113
+ );
114
+ const result = deriveShellActionKeys(analysis);
115
+
116
+ expect(result.isSimpleAction).toBe(false);
117
+ expect(result.keys).toEqual([
118
+ { key: "action:pdftotext file.pdf", depth: 2 },
119
+ { key: "action:pdftotext", depth: 1 },
120
+ ]);
121
+ });
122
+
123
+ test("pipeline with subcommand extracts deeper keys", async () => {
124
+ const analysis = await analyzeShellCommand("cd repo && gh pr list | head");
125
+ const result = deriveShellActionKeys(analysis);
126
+
127
+ expect(result.isSimpleAction).toBe(false);
128
+ expect(result.keys).toEqual([
129
+ { key: "action:gh pr list", depth: 3 },
130
+ { key: "action:gh pr", depth: 2 },
131
+ { key: "action:gh", depth: 1 },
132
+ ]);
133
+ });
134
+
135
+ test("multi-pipe pipeline extracts first segment only", async () => {
136
+ const analysis = await analyzeShellCommand("cat file | grep error | wc -l");
137
+ const result = deriveShellActionKeys(analysis);
138
+
139
+ expect(result.isSimpleAction).toBe(false);
140
+ expect(result.keys).toEqual([
141
+ { key: "action:cat file", depth: 2 },
142
+ { key: "action:cat", depth: 1 },
143
+ ]);
144
+ });
145
+
146
+ test("dangerous pipe_to_shell still extracts keys", async () => {
147
+ const analysis = await analyzeShellCommand("curl url | bash");
148
+ const result = deriveShellActionKeys(analysis);
149
+
150
+ expect(result.isSimpleAction).toBe(false);
151
+ expect(result.keys).toEqual([
152
+ { key: "action:curl url", depth: 2 },
153
+ { key: "action:curl", depth: 1 },
154
+ ]);
91
155
  });
92
156
 
93
157
  test("complex chains with multiple actions are non-simple", async () => {
@@ -198,9 +262,19 @@ describe("buildShellCommandCandidates", () => {
198
262
  expect(candidates).toEqual(['git add . && git commit -m "fix"']);
199
263
  });
200
264
 
201
- test("pipeline returns raw-only", async () => {
265
+ test("pipeline returns raw and action key candidates", async () => {
202
266
  const candidates = await buildShellCommandCandidates("git log | grep fix");
203
- expect(candidates).toEqual(["git log | grep fix"]);
267
+ expect(candidates[0]).toBe("git log | grep fix");
268
+ expect(candidates).toContain("action:git log");
269
+ expect(candidates).toContain("action:git");
270
+ });
271
+
272
+ test("pipeline with setup prefix includes action candidates", async () => {
273
+ const candidates = await buildShellCommandCandidates(
274
+ "cd /tmp && pdftotext file.pdf | wc -c",
275
+ );
276
+ expect(candidates[0]).toBe("cd /tmp && pdftotext file.pdf | wc -c");
277
+ expect(candidates).toContain("action:pdftotext");
204
278
  });
205
279
 
206
280
  test("candidate order is stable", async () => {
@@ -241,13 +315,29 @@ describe("buildShellAllowlistOptions — complex command restrictions", () => {
241
315
  expect(options[0].description).toContain("compound");
242
316
  });
243
317
 
244
- test("pipeline offers exact only", async () => {
318
+ test("pipeline offers exact and action-key options", async () => {
245
319
  const options = await buildShellAllowlistOptions(
246
320
  "cat file.txt | grep error | wc -l",
247
321
  );
248
- expect(options).toHaveLength(1);
322
+ expect(options.length).toBeGreaterThanOrEqual(2);
249
323
  expect(options[0].pattern).toBe("cat file.txt | grep error | wc -l");
250
324
  expect(options[0].description).toContain("compound");
325
+ expect(options.some((o) => o.pattern.startsWith("action:"))).toBe(true);
326
+ });
327
+
328
+ test("pipeline offers action-key options from first segment", async () => {
329
+ const options = await buildShellAllowlistOptions(
330
+ "pdftotext file.pdf | head -100",
331
+ );
332
+ expect(options.length).toBeGreaterThanOrEqual(2);
333
+ expect(options[0].pattern).toBe("pdftotext file.pdf | head -100");
334
+ expect(
335
+ options.some(
336
+ (o) =>
337
+ o.pattern === "action:pdftotext" &&
338
+ o.description.includes('Any "pdftotext" command'),
339
+ ),
340
+ ).toBe(true);
251
341
  });
252
342
 
253
343
  test("semicolon chain offers exact only", async () => {