@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
@@ -24,9 +24,10 @@ import { getRootDir } from "../util/platform.js";
24
24
  const log = getLogger("keychain-broker-client");
25
25
 
26
26
  const REQUEST_TIMEOUT_MS = 5_000;
27
+ const CONNECT_TIMEOUT_MS = 3_000;
27
28
 
28
- /** Cooldown periods (ms) after consecutive connection failures: 30s, 60s, 2min, 5min, then cap at 5min. */
29
- const RECONNECT_COOLDOWN_MS = [30_000, 60_000, 120_000, 300_000];
29
+ /** Cooldown periods (ms) after consecutive connection failures: 5s, 15s, 30s, 60s, 5min, then cap at 5min. */
30
+ const RECONNECT_COOLDOWN_MS = [5_000, 15_000, 30_000, 60_000, 300_000];
30
31
 
31
32
  // ---------------------------------------------------------------------------
32
33
  // Types
@@ -191,7 +192,13 @@ export function createBrokerClient(): KeychainBrokerClient {
191
192
 
192
193
  const sock = createConnection({ path: socketPath });
193
194
 
195
+ const connectTimer = setTimeout(() => {
196
+ sock.destroy();
197
+ reject(new Error("Connect timeout"));
198
+ }, CONNECT_TIMEOUT_MS);
199
+
194
200
  sock.on("connect", () => {
201
+ clearTimeout(connectTimer);
195
202
  socket = sock;
196
203
  consecutiveFailures = 0;
197
204
  unavailableSince = null;
@@ -199,6 +206,7 @@ export function createBrokerClient(): KeychainBrokerClient {
199
206
  });
200
207
 
201
208
  sock.on("error", (err) => {
209
+ clearTimeout(connectTimer);
202
210
  log.warn({ err }, "Keychain broker socket error");
203
211
  cleanupSocket();
204
212
  reject(err);
@@ -1,15 +1,17 @@
1
1
  /**
2
- * Unified secure key storage — single-writer routing through CredentialBackend
2
+ * Unified secure key storage — single-backend routing through CredentialBackend
3
3
  * adapters.
4
4
  *
5
- * Backend selection (`resolveBackend`) is the single decision point:
6
- * - Containerized (IS_CONTAINERIZED + CES_CREDENTIAL_URL set): CES HTTP client.
7
- * - Production (VELLUM_DEV unset or "0"): keychain backend when available.
8
- * - Dev mode (VELLUM_DEV=1): encrypted file store always.
5
+ * Backend selection (`resolveBackendAsync`) is the single async decision point:
6
+ * 1. CES RPC (primary) injected via `setCesClient()`: delegates credential
7
+ * operations to the CES process over stdio RPC. This is the default path
8
+ * for all local modes (desktop app, dev, CLI).
9
+ * 2. CES HTTP — containerized mode (IS_CONTAINERIZED + CES_CREDENTIAL_URL):
10
+ * delegates to the CES sidecar over HTTP. Used in Docker/managed mode.
11
+ * 3. Encrypted file store (fallback) — used when CES is unavailable.
9
12
  *
10
- * Writes go to exactly one backend (no dual-writing). Reads in keychain mode
11
- * fall back to the encrypted store for keys that haven't been migrated yet.
12
- * Deletes clean up both stores regardless of mode.
13
+ * All operations (reads, writes, lists, deletes) go to exactly one backend.
14
+ * There are no cross-backend fallbacks or merges.
13
15
  */
14
16
 
15
17
  import type {
@@ -19,13 +21,12 @@ import type {
19
21
 
20
22
  import providerEnvVarsRegistry from "../../../meta/provider-env-vars.json" with { type: "json" };
21
23
  import { getIsContainerized } from "../config/env-registry.js";
24
+ import type { CesClient } from "../credential-execution/client.js";
22
25
  import { getLogger } from "../util/logger.js";
23
26
  import { createCesCredentialBackend } from "./ces-credential-client.js";
27
+ import { CesRpcCredentialBackend } from "./ces-rpc-credential-backend.js";
24
28
  import type { CredentialBackend, DeleteResult } from "./credential-backend.js";
25
- import {
26
- createEncryptedStoreBackend,
27
- createKeychainBackend,
28
- } from "./credential-backend.js";
29
+ import { createEncryptedStoreBackend } from "./credential-backend.js";
29
30
 
30
31
  export type { DeleteResult } from "./credential-backend.js";
31
32
 
@@ -36,15 +37,24 @@ export type { DeleteResult } from "./credential-backend.js";
36
37
  */
37
38
  export type { SecureKeyBackend, SecureKeyDeleteResult };
38
39
 
40
+ export interface SecureKeyResult {
41
+ value: string | undefined;
42
+ unreachable: boolean;
43
+ }
44
+
39
45
  const log = getLogger("secure-keys");
40
46
 
41
- let _keychain: CredentialBackend | undefined;
47
+ let _cesClient: CesClient | undefined;
42
48
  let _encryptedStore: CredentialBackend | undefined;
43
49
  let _resolvedBackend: CredentialBackend | undefined;
50
+ let _resolvePromise: Promise<CredentialBackend> | undefined;
44
51
 
45
- function getKeychainBackend(): CredentialBackend {
46
- if (!_keychain) _keychain = createKeychainBackend();
47
- return _keychain;
52
+ /** Inject a CES RPC client for credential routing. Resets the resolved backend. */
53
+ export function setCesClient(client: CesClient | undefined): void {
54
+ _cesClient = client;
55
+ // Reset resolved backend so next call picks up CES
56
+ _resolvedBackend = undefined;
57
+ _resolvePromise = undefined;
48
58
  }
49
59
 
50
60
  function getEncryptedStoreBackend(): CredentialBackend {
@@ -53,101 +63,96 @@ function getEncryptedStoreBackend(): CredentialBackend {
53
63
  }
54
64
 
55
65
  /**
56
- * Resolve the primary credential backend for this process.
66
+ * Resolve the primary credential backend for this process (async).
57
67
  *
58
68
  * Priority:
59
- * 1. Containerized + CES_CREDENTIAL_URLCES HTTP client (skip keychain
60
- * and encrypted store entirely the sidecar owns credential storage).
61
- * 2. Production (VELLUM_DEV unset or "0") keychain when available.
62
- * 3. Dev mode (VELLUM_DEV=1) → encrypted file store always.
69
+ * 1. CES RPC clientprimary path for all local modes.
70
+ * 2. Containerized + CES_CREDENTIAL_URL CES HTTP client (Docker/managed).
71
+ * 3. Encrypted file storefallback when CES is unavailable.
63
72
  *
64
73
  * Once resolved, the backend does not change during the process lifetime.
65
74
  * Call `_resetBackend()` in tests to clear the cached resolution.
66
75
  */
67
- function resolveBackend(): CredentialBackend {
68
- if (!_resolvedBackend) {
69
- if (getIsContainerized() && process.env.CES_CREDENTIAL_URL) {
70
- const ces = createCesCredentialBackend();
71
- if (ces.isAvailable()) {
72
- _resolvedBackend = ces;
73
- } else {
74
- log.warn(
75
- "CES_CREDENTIAL_URL is set but CES backend is not available — " +
76
- "falling back to local credential store",
77
- );
78
- }
76
+ async function resolveBackendAsync(): Promise<CredentialBackend> {
77
+ if (_resolvedBackend) return _resolvedBackend;
78
+ if (!_resolvePromise) {
79
+ _resolvePromise = doResolveBackend();
80
+ }
81
+ return _resolvePromise;
82
+ }
83
+
84
+ async function doResolveBackend(): Promise<CredentialBackend> {
85
+ // 1. CES RPC — primary credential backend for all local modes
86
+ if (_cesClient) {
87
+ const cesRpc = new CesRpcCredentialBackend(_cesClient);
88
+ if (cesRpc.isAvailable()) {
89
+ _resolvedBackend = cesRpc;
90
+ return cesRpc;
79
91
  }
92
+ log.warn("CES RPC client is set but not ready — falling back to local credential store");
93
+ }
80
94
 
81
- if (!_resolvedBackend) {
82
- if (
83
- process.env.VELLUM_DEV !== "1" &&
84
- getKeychainBackend().isAvailable()
85
- ) {
86
- _resolvedBackend = getKeychainBackend();
87
- } else {
88
- _resolvedBackend = getEncryptedStoreBackend();
89
- }
95
+ // 2. CES HTTP — containerized / Docker / managed mode
96
+ if (getIsContainerized() && process.env.CES_CREDENTIAL_URL) {
97
+ const ces = createCesCredentialBackend();
98
+ if (ces.isAvailable()) {
99
+ _resolvedBackend = ces;
100
+ return ces;
90
101
  }
102
+ log.warn(
103
+ "CES_CREDENTIAL_URL is set but CES backend is not available — " +
104
+ "falling back to local credential store",
105
+ );
91
106
  }
107
+
108
+ // 3. Encrypted file store — fallback when CES is unavailable
109
+ _resolvedBackend = getEncryptedStoreBackend();
92
110
  return _resolvedBackend;
93
111
  }
94
112
 
95
113
  /**
96
- * List all account names across both backends (async).
97
- *
98
- * In CES mode, only the CES backend is queried — there are no local stores.
114
+ * List all account names from the resolved backend (async).
99
115
  *
100
- * When the primary backend is the keychain, this merges keys from the keychain
101
- * and the encrypted store (for legacy keys that haven't been migrated). The
102
- * result is deduplicated. When the primary backend is already the encrypted
103
- * store, only that store is queried.
116
+ * Queries exactly one backend no cross-store merge.
104
117
  */
105
118
  export async function listSecureKeysAsync(): Promise<string[]> {
106
- const backend = resolveBackend();
107
- const primaryKeys = await backend.list();
108
-
109
- // CES mode — the sidecar is the single source of truth, no local merge.
110
- if (backend.name === "ces-http") return primaryKeys;
111
-
112
- // If primary backend is NOT the encrypted store, also check
113
- // the encrypted store for legacy keys that haven't been migrated.
114
- if (backend !== getEncryptedStoreBackend()) {
115
- const encKeys = await getEncryptedStoreBackend().list();
116
- const merged = new Set([...primaryKeys, ...encKeys]);
117
- return Array.from(merged);
118
- }
119
-
120
- return primaryKeys;
119
+ const backend = await resolveBackendAsync();
120
+ return backend.list();
121
121
  }
122
122
 
123
123
  // ---------------------------------------------------------------------------
124
- // Async CRUD — single-writer routing
124
+ // Async CRUD — single-backend routing
125
125
  // ---------------------------------------------------------------------------
126
126
 
127
127
  /**
128
- * Retrieve a secret from secure storage. Reads from the primary backend
129
- * first. In CES mode, the sidecar is the single source of truth — no
130
- * local fallback. In local mode, if the primary backend is the keychain,
131
- * falls back to the encrypted store for legacy keys that haven't been
132
- * migrated.
128
+ * Retrieve a secret from secure storage with richer result metadata.
129
+ *
130
+ * Returns both the value (if found) and whether the backend was
131
+ * unreachable. Callers that need to distinguish "not found" from
132
+ * "backend down" should use this instead of `getSecureKeyAsync`.
133
+ *
134
+ * Reads from exactly one backend — no cross-store fallback.
133
135
  */
134
- export async function getSecureKeyAsync(
136
+ export async function getSecureKeyResultAsync(
135
137
  account: string,
136
- ): Promise<string | undefined> {
137
- const backend = resolveBackend();
138
+ ): Promise<SecureKeyResult> {
139
+ const backend = await resolveBackendAsync();
138
140
  const result = await backend.get(account);
139
- if (result != null) return result;
140
-
141
- // CES mode — no local fallback.
142
- if (backend.name === "ces-http") return undefined;
143
-
144
- // Legacy fallback: if primary backend is NOT the encrypted store,
145
- // check the encrypted store for keys that haven't been migrated.
146
- if (backend !== getEncryptedStoreBackend()) {
147
- return await getEncryptedStoreBackend().get(account);
141
+ if (result.value != null) {
142
+ return { value: result.value, unreachable: false };
148
143
  }
144
+ return { value: undefined, unreachable: result.unreachable };
145
+ }
149
146
 
150
- return undefined;
147
+ /**
148
+ * Retrieve a secret from secure storage. Convenience wrapper over
149
+ * `getSecureKeyResultAsync` that returns only the value.
150
+ */
151
+ export async function getSecureKeyAsync(
152
+ account: string,
153
+ ): Promise<string | undefined> {
154
+ const result = await getSecureKeyResultAsync(account);
155
+ return result.value;
151
156
  }
152
157
 
153
158
  /**
@@ -158,7 +163,7 @@ export async function setSecureKeyAsync(
158
163
  account: string,
159
164
  value: string,
160
165
  ): Promise<boolean> {
161
- const backend = resolveBackend();
166
+ const backend = await resolveBackendAsync();
162
167
  const ok = await backend.set(account, value);
163
168
  if (!ok) {
164
169
  log.warn(
@@ -172,38 +177,13 @@ export async function setSecureKeyAsync(
172
177
  /**
173
178
  * Delete a secret from secure storage.
174
179
  *
175
- * In containerized mode with CES, deletion is routed exclusively through the
176
- * CES backend — there are no local stores to clean up.
177
- *
178
- * In local mode, always attempts deletion on both the keychain backend (if
179
- * available) and the encrypted store backend, regardless of routing mode.
180
- * This cleans up legacy data from both stores.
180
+ * Deletes from exactly one backend no cross-store cleanup.
181
181
  */
182
182
  export async function deleteSecureKeyAsync(
183
183
  account: string,
184
184
  ): Promise<DeleteResult> {
185
- const backend = resolveBackend();
186
-
187
- // In CES mode, the sidecar is the only store — no local cleanup needed.
188
- if (backend.name === "ces-http") {
189
- return backend.delete(account);
190
- }
191
-
192
- const keychain = getKeychainBackend();
193
- const enc = getEncryptedStoreBackend();
194
-
195
- let keychainResult: DeleteResult = "not-found";
196
- if (keychain.isAvailable()) {
197
- keychainResult = await keychain.delete(account);
198
- }
199
-
200
- const encResult = await enc.delete(account);
201
-
202
- // Return "error" if either errored
203
- if (keychainResult === "error" || encResult === "error") return "error";
204
- // Return "deleted" if either deleted
205
- if (keychainResult === "deleted" || encResult === "deleted") return "deleted";
206
- return "not-found";
185
+ const backend = await resolveBackendAsync();
186
+ return backend.delete(account);
207
187
  }
208
188
 
209
189
  // ---------------------------------------------------------------------------
@@ -263,7 +243,8 @@ export async function getMaskedProviderKey(
263
243
 
264
244
  /** @internal Test-only: reset the cached backends so they're re-created. */
265
245
  export function _resetBackend(): void {
266
- _keychain = undefined;
246
+ _cesClient = undefined;
267
247
  _encryptedStore = undefined;
268
248
  _resolvedBackend = undefined;
249
+ _resolvePromise = undefined;
269
250
  }
@@ -88,11 +88,7 @@ function getConfigPlatformUrl(): string | undefined {
88
88
  }
89
89
 
90
90
  function getPlatformUrl(): string {
91
- return (
92
- process.env.VELLUM_PLATFORM_URL ??
93
- getConfigPlatformUrl() ??
94
- "https://platform.vellum.ai"
95
- );
91
+ return process.env.VELLUM_PLATFORM_URL ?? getConfigPlatformUrl() ?? "";
96
92
  }
97
93
 
98
94
  function buildHeaders(): Record<string, string> {
@@ -107,7 +103,11 @@ function buildHeaders(): Record<string, string> {
107
103
  // ─── Catalog operations ──────────────────────────────────────────────────────
108
104
 
109
105
  export async function fetchCatalog(): Promise<CatalogSkill[]> {
110
- const url = `${getPlatformUrl()}/v1/skills/`;
106
+ const platformUrl = getPlatformUrl();
107
+ if (!platformUrl) {
108
+ return [];
109
+ }
110
+ const url = `${platformUrl}/v1/skills/`;
111
111
  const response = await fetch(url, {
112
112
  headers: buildHeaders(),
113
113
  signal: AbortSignal.timeout(10000),
@@ -214,7 +214,13 @@ export async function fetchAndExtractSkill(
214
214
  skillId: string,
215
215
  destDir: string,
216
216
  ): Promise<void> {
217
- const url = `${getPlatformUrl()}/v1/skills/${encodeURIComponent(skillId)}/`;
217
+ const platformUrl = getPlatformUrl();
218
+ if (!platformUrl) {
219
+ throw new Error(
220
+ `Cannot fetch skill "${skillId}": VELLUM_PLATFORM_URL is not configured.`,
221
+ );
222
+ }
223
+ const url = `${platformUrl}/v1/skills/${encodeURIComponent(skillId)}/`;
218
224
  const response = await fetch(url, {
219
225
  headers: buildHeaders(),
220
226
  signal: AbortSignal.timeout(15000),
@@ -140,7 +140,7 @@ export class UsageTelemetryReporter {
140
140
 
141
141
  // Resolve auth context — skip flush when neither auth mode is viable
142
142
  const client = await VellumPlatformClient.create();
143
- if (!client && !getTelemetryAppToken()) {
143
+ if (!client && (!getTelemetryAppToken() || !getTelemetryPlatformUrl())) {
144
144
  return;
145
145
  }
146
146
 
@@ -203,7 +203,9 @@ export class UsageTelemetryReporter {
203
203
  if (client) {
204
204
  resp = await client.fetch(TELEMETRY_PATH, fetchInit);
205
205
  } else {
206
- const url = `${getTelemetryPlatformUrl()}${TELEMETRY_PATH}`;
206
+ const platformUrl = getTelemetryPlatformUrl();
207
+ if (!platformUrl) return;
208
+ const url = `${platformUrl}${TELEMETRY_PATH}`;
207
209
  resp = await fetch(url, {
208
210
  ...fetchInit,
209
211
  headers: {
@@ -46,6 +46,7 @@ export async function executeCallStart(
46
46
  | "assistant_number"
47
47
  | "user_number"
48
48
  | undefined,
49
+ skipDisclosure: input.skip_disclosure === true,
49
50
  });
50
51
 
51
52
  if (!result.ok) {
@@ -183,10 +183,6 @@ export class ToolExecutor {
183
183
  );
184
184
  // Buffer so the shell's own timeout fires first and handles cleanup
185
185
  toolTimeoutMs = (shellTimeoutSec + 5) * 1000;
186
- } else if (name === "claude_code") {
187
- // Claude Code spawns a subprocess that manages its own turn limits
188
- // (maxTurns). Give it a generous timeout so it isn't killed mid-task.
189
- toolTimeoutMs = 10 * 60 * 1000; // 10 minutes
190
186
  } else {
191
187
  const rawTimeoutSec = getConfig().timeouts.toolExecutionTimeoutSec;
192
188
  toolTimeoutMs = safeTimeoutMs(rawTimeoutSec);
@@ -61,10 +61,25 @@ const store = new SessionStore();
61
61
  /**
62
62
  * Host patterns that are allowed by default through the proxy policy engine,
63
63
  * regardless of session configuration. Supports exact matches (e.g.
64
- * `"localhost"`) and wildcard subdomain patterns (e.g. `"*.vellum.ai"`
65
- * matches `platform.vellum.ai`, `dev-platform.vellum.ai`, etc.).
64
+ * `"localhost"`) and wildcard subdomain patterns (e.g. `"*.example.com"`
65
+ * matches `api.example.com`, `dev.example.com`, etc.).
66
+ *
67
+ * Additional patterns can be added via the `PROXY_ALLOWED_HOSTS` env var
68
+ * (comma-separated, e.g. `"*.example.com,api.foo.bar"`).
66
69
  */
67
- const ALLOWED_HOST_PATTERNS: readonly string[] = ["*.vellum.ai", "localhost"];
70
+ const ALLOWED_HOST_PATTERNS: readonly string[] = (() => {
71
+ const extra = process.env.PROXY_ALLOWED_HOSTS?.trim();
72
+ const defaults = ["localhost"];
73
+ if (extra) {
74
+ defaults.push(
75
+ ...extra
76
+ .split(",")
77
+ .map((h) => h.trim())
78
+ .filter(Boolean),
79
+ );
80
+ }
81
+ return defaults;
82
+ })();
68
83
 
69
84
  /**
70
85
  * Returns `true` when `hostname` matches any entry in
@@ -73,7 +88,7 @@ const ALLOWED_HOST_PATTERNS: readonly string[] = ["*.vellum.ai", "localhost"];
73
88
  function isAllowedHost(hostname: string): boolean {
74
89
  for (const pattern of ALLOWED_HOST_PATTERNS) {
75
90
  if (pattern.startsWith("*.")) {
76
- const suffix = pattern.slice(1); // e.g. ".vellum.ai"
91
+ const suffix = pattern.slice(1); // e.g. ".example.com"
77
92
  if (hostname.endsWith(suffix) || hostname === pattern.slice(2)) {
78
93
  return true;
79
94
  }
@@ -573,7 +573,9 @@ export async function executeWebFetch(
573
573
  Accept:
574
574
  "text/markdown, text/html;q=0.9, application/xhtml+xml;q=0.9, text/plain;q=0.8, application/json;q=0.7, */*;q=0.6",
575
575
  "Accept-Encoding": "identity",
576
- "User-Agent": "VellumAssistant/1.0 (+https://vellum.ai)",
576
+ "User-Agent":
577
+ process.env.HTTP_USER_AGENT ||
578
+ "VellumAssistant/1.0 (+https://vellum.ai)",
577
579
  };
578
580
 
579
581
  let currentUrl = new URL(requestedUrl);
@@ -30,7 +30,7 @@ export class SkillExecuteTool implements Tool {
30
30
  activity: {
31
31
  type: "string",
32
32
  description:
33
- "Brief non-technical explanation of what you are doing and why, shown to the user as a status update.",
33
+ "Brief non-technical explanation of what you are doing and why, shown to the user as a progress update.",
34
34
  },
35
35
  },
36
36
  required: ["tool", "input", "activity"],
@@ -118,14 +118,6 @@ export interface ToolContext {
118
118
  proxyToolResolver?: ProxyToolResolver;
119
119
  /** When set, only tools in this set may execute. Tools outside the set are blocked with an error. */
120
120
  allowedToolNames?: Set<string>;
121
- /** Request user confirmation for a sub-tool operation (used by claude_code tool). */
122
- requestConfirmation?: (req: {
123
- toolName: string;
124
- input: Record<string, unknown>;
125
- riskLevel: string;
126
- executionTarget?: ExecutionTarget;
127
- principal?: string;
128
- }) => Promise<{ decision: "allow" | "deny" }>;
129
121
  /** Prompt the user for a secret value via native SecureField UI. */
130
122
  requestSecret?: (params: {
131
123
  service: string;
@@ -20,9 +20,6 @@ export enum ErrorCode {
20
20
  // WASM integrity check failures
21
21
  INTEGRITY_ERROR = "INTEGRITY_ERROR",
22
22
 
23
- // Secret detected in inbound content
24
- INGRESS_BLOCKED = "INGRESS_BLOCKED",
25
-
26
23
  // Internal/unexpected errors
27
24
  INTERNAL_ERROR = "INTERNAL_ERROR",
28
25
  }
@@ -178,12 +175,3 @@ export class IntegrityError extends AssistantError {
178
175
  }
179
176
  }
180
177
 
181
- export class IngressBlockedError extends AssistantError {
182
- constructor(
183
- message: string,
184
- public readonly detectedTypes: string[],
185
- ) {
186
- super(message, ErrorCode.INGRESS_BLOCKED);
187
- this.name = "IngressBlockedError";
188
- }
189
- }
@@ -3,7 +3,6 @@ import {
3
3
  existsSync,
4
4
  mkdirSync,
5
5
  readFileSync,
6
- writeFileSync,
7
6
  } from "node:fs";
8
7
  import { homedir } from "node:os";
9
8
  import { join } from "node:path";
@@ -45,25 +44,6 @@ export function getClipboardCommand(): string | null {
45
44
  return null;
46
45
  }
47
46
 
48
- /**
49
- * Read and parse the lockfile (~/.vellum.lock.json).
50
- * Respects BASE_DATA_DIR for non-standard home directories.
51
- * Returns null if the file doesn't exist or is malformed.
52
- */
53
- export function readLockfile(): Record<string, unknown> | null {
54
- const base = getBaseDataDir() || homedir();
55
- const lockPath = join(base, ".vellum.lock.json");
56
- if (!existsSync(lockPath)) return null;
57
- try {
58
- const raw = JSON.parse(readFileSync(lockPath, "utf-8"));
59
- if (raw && typeof raw === "object" && !Array.isArray(raw)) {
60
- return raw as Record<string, unknown>;
61
- }
62
- } catch {
63
- // malformed JSON
64
- }
65
- return null;
66
- }
67
47
 
68
48
  /**
69
49
  * Resolve the instance data directory from the lockfile.
@@ -155,45 +135,18 @@ export function resolveInstanceDataDir(): string | undefined {
155
135
  * (see migration 007-assistant-id-to-self). However, the desktop UI
156
136
  * sends the real assistant ID (e.g., "vellum-true-eel") while the
157
137
  * inbound call path resolves phone numbers to config keys (typically
158
- * "self"). This function maps any known lockfile assistant ID to "self"
138
+ * "self"). This function maps the current assistant's ID to "self"
159
139
  * so both sides use a consistent DB key.
160
- *
161
- * Multi-instance safety: each daemon process runs with a scoped
162
- * BASE_DATA_DIR, so readLockfile() only sees the lockfile for this
163
- * instance. The mapping to "self" is correct because each daemon is
164
- * single-tenant — it only manages its own instance's data.
165
140
  */
166
141
  export function normalizeAssistantId(assistantId: string): string {
167
142
  if (assistantId === "self") return "self";
168
143
 
169
- try {
170
- const lockData = readLockfile();
171
- const assistants = lockData?.assistants as
172
- | Array<Record<string, unknown>>
173
- | undefined;
174
- if (assistants) {
175
- for (const entry of assistants) {
176
- if (entry.assistantId === assistantId) return "self";
177
- }
178
- }
179
- } catch {
180
- // lockfile unreadable — return as-is
181
- }
144
+ const ownName = process.env.VELLUM_ASSISTANT_NAME;
145
+ if (ownName && assistantId === ownName) return "self";
182
146
 
183
147
  return assistantId;
184
148
  }
185
149
 
186
- /**
187
- * Write data to the primary lockfile (~/.vellum.lock.json).
188
- * Respects BASE_DATA_DIR for non-standard home directories.
189
- */
190
- export function writeLockfile(data: Record<string, unknown>): void {
191
- const base = getBaseDataDir() || homedir();
192
- writeFileSync(
193
- join(base, ".vellum.lock.json"),
194
- JSON.stringify(data, null, 2) + "\n",
195
- );
196
- }
197
150
 
198
151
  /**
199
152
  * Returns the root ~/.vellum directory. User-facing files (config, prompt
@@ -753,8 +753,11 @@ export class WorkspaceGitService {
753
753
  * Must be called with the mutex lock held.
754
754
  */
755
755
  private async ensureCommitIdentityLocked(): Promise<void> {
756
- await this.execGit(["config", "user.name", "Vellum Assistant"]);
757
- await this.execGit(["config", "user.email", "assistant@vellum.ai"]);
756
+ const gitName = process.env.ASSISTANT_GIT_USER_NAME || "Vellum Assistant";
757
+ const gitEmail =
758
+ process.env.ASSISTANT_GIT_USER_EMAIL || "assistant@vellum.ai";
759
+ await this.execGit(["config", "user.name", gitName]);
760
+ await this.execGit(["config", "user.email", gitEmail]);
758
761
  }
759
762
 
760
763
  /**
@@ -22,4 +22,19 @@ export const avatarRenameMigration: WorkspaceMigration = {
22
22
  renameSync(oldTraits, newTraits);
23
23
  }
24
24
  },
25
+ down(workspaceDir: string): void {
26
+ const avatarDir = join(workspaceDir, "data", "avatar");
27
+
28
+ const newImage = join(avatarDir, "avatar-image.png");
29
+ const oldImage = join(avatarDir, "custom-avatar.png");
30
+ if (existsSync(newImage) && !existsSync(oldImage)) {
31
+ renameSync(newImage, oldImage);
32
+ }
33
+
34
+ const newTraits = join(avatarDir, "character-traits.json");
35
+ const oldTraits = join(avatarDir, "avatar-components.json");
36
+ if (existsSync(newTraits) && !existsSync(oldTraits)) {
37
+ renameSync(newTraits, oldTraits);
38
+ }
39
+ },
25
40
  };
@@ -1,4 +1,10 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ unlinkSync,
6
+ writeFileSync,
7
+ } from "node:fs";
2
8
  import { join } from "node:path";
3
9
 
4
10
  import { getDeviceIdBaseDir } from "../../util/device-id.js";
@@ -97,4 +103,14 @@ export const seedDeviceIdMigration: WorkspaceMigration = {
97
103
  // Best-effort — getDeviceId() will generate a new one if this fails.
98
104
  }
99
105
  },
106
+ down(_workspaceDir: string): void {
107
+ // The forward migration seeds deviceId in ~/.vellum/device.json from the
108
+ // lockfile. Reverse by removing device.json entirely — getDeviceId() will
109
+ // generate a fresh one on next startup if needed.
110
+ const base = getDeviceIdBaseDir();
111
+ const devicePath = join(base, ".vellum", "device.json");
112
+ if (existsSync(devicePath)) {
113
+ unlinkSync(devicePath);
114
+ }
115
+ },
100
116
  };