@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
@@ -2,19 +2,15 @@ import {
2
2
  existsSync,
3
3
  mkdirSync,
4
4
  readFileSync,
5
+ renameSync,
5
6
  statSync,
6
7
  writeFileSync,
7
8
  } from "node:fs";
8
- import { dirname } from "node:path";
9
+ import { dirname, join } from "node:path";
9
10
 
10
11
  import { ConfigError } from "../util/errors.js";
11
12
  import { getLogger } from "../util/logger.js";
12
- import {
13
- ensureDataDir,
14
- getWorkspaceConfigPath,
15
- readLockfile,
16
- writeLockfile,
17
- } from "../util/platform.js";
13
+ import { ensureDataDir, getWorkspaceConfigPath } from "../util/platform.js";
18
14
  import { AssistantConfigSchema } from "./schema.js";
19
15
  import type { AssistantConfig } from "./types.js";
20
16
 
@@ -214,6 +210,35 @@ export function deepMergeMissing(
214
210
  return changed;
215
211
  }
216
212
 
213
+ /**
214
+ * Deep-merge `overrides` into `target`, overwriting leaf values.
215
+ * Recursively merges nested objects; scalars and arrays from `overrides`
216
+ * replace corresponding values in `target`.
217
+ */
218
+ export function deepMergeOverwrite(
219
+ target: Record<string, unknown>,
220
+ overrides: Record<string, unknown>,
221
+ ): void {
222
+ for (const key of Object.keys(overrides)) {
223
+ const ov = overrides[key];
224
+ if (
225
+ ov != null &&
226
+ typeof ov === "object" &&
227
+ !Array.isArray(ov) &&
228
+ target[key] != null &&
229
+ typeof target[key] === "object" &&
230
+ !Array.isArray(target[key])
231
+ ) {
232
+ deepMergeOverwrite(
233
+ target[key] as Record<string, unknown>,
234
+ ov as Record<string, unknown>,
235
+ );
236
+ } else {
237
+ target[key] = ov;
238
+ }
239
+ }
240
+ }
241
+
217
242
  /**
218
243
  * Read the existing config.json from disk, merge any missing schema-default
219
244
  * keys, and rewrite only when there is an effective change.
@@ -248,6 +273,72 @@ function backfillConfigDefaults(
248
273
  }
249
274
  }
250
275
 
276
+ /**
277
+ * Merge default workspace config from the file referenced by
278
+ * VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH into the workspace config on disk.
279
+ *
280
+ * Called once at daemon startup (before the first loadConfig()) so the
281
+ * defaults are persisted to the workspace config file alongside any
282
+ * schema-level defaults that loadConfig() backfills.
283
+ */
284
+ export function mergeDefaultWorkspaceConfig(): void {
285
+ const defaultConfigPath = process.env.VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH;
286
+ if (!defaultConfigPath || !existsSync(defaultConfigPath)) return;
287
+
288
+ let defaults: unknown;
289
+ try {
290
+ defaults = JSON.parse(readFileSync(defaultConfigPath, "utf-8"));
291
+ } catch (err) {
292
+ log.warn(
293
+ { err },
294
+ "Failed to read default workspace config from %s",
295
+ defaultConfigPath,
296
+ );
297
+ return;
298
+ }
299
+
300
+ if (
301
+ defaults == null ||
302
+ typeof defaults !== "object" ||
303
+ Array.isArray(defaults)
304
+ ) {
305
+ return;
306
+ }
307
+
308
+ const configPath = getConfigPath();
309
+ let existing: Record<string, unknown> = {};
310
+ if (existsSync(configPath)) {
311
+ try {
312
+ existing = JSON.parse(readFileSync(configPath, "utf-8"));
313
+ } catch {
314
+ // If existing config is corrupt, start fresh
315
+ }
316
+ }
317
+
318
+ deepMergeOverwrite(existing, defaults as Record<string, unknown>);
319
+
320
+ const dir = dirname(configPath);
321
+ if (!existsSync(dir)) {
322
+ mkdirSync(dir, { recursive: true });
323
+ }
324
+ writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
325
+
326
+ // Move the temp file into the workspace directory as a permanent record.
327
+ // This prevents re-application on daemon restart (the env var still points
328
+ // at the old /tmp path which no longer exists).
329
+ try {
330
+ const dest = join(dir, "default-config.json");
331
+ renameSync(defaultConfigPath, dest);
332
+ log.info(
333
+ "Merged default workspace config from %s (archived to %s)",
334
+ defaultConfigPath,
335
+ dest,
336
+ );
337
+ } catch {
338
+ log.info("Merged default workspace config from %s", defaultConfigPath);
339
+ }
340
+ }
341
+
251
342
  export function loadConfig(): AssistantConfig {
252
343
  if (cached) return cached;
253
344
 
@@ -375,30 +466,6 @@ export function saveRawConfig(config: Record<string, unknown>): void {
375
466
  cached = null; // invalidate cache
376
467
  }
377
468
 
378
- /**
379
- * Sync client-relevant config values (e.g. platform.baseUrl) to the lockfile
380
- * so external tools (e.g. vel) can discover them without importing the full
381
- * config schema. Mirrors the behaviour of `syncConfigToLockfile` in the
382
- * lightweight CLI (`cli/src/lib/assistant-config.ts`).
383
- */
384
- export function syncConfigToLockfile(): void {
385
- const configPath = getWorkspaceConfigPath();
386
- if (!existsSync(configPath)) return;
387
-
388
- try {
389
- const raw = JSON.parse(readFileSync(configPath, "utf-8")) as Record<
390
- string,
391
- unknown
392
- >;
393
- const platform = raw.platform as Record<string, unknown> | undefined;
394
- const data = readLockfile() ?? {};
395
- data.platformBaseUrl = (platform?.baseUrl as string) || undefined;
396
- writeLockfile(data);
397
- } catch {
398
- // Config file unreadable — skip sync
399
- }
400
- }
401
-
402
469
  export function getNestedValue(
403
470
  obj: Record<string, unknown>,
404
471
  path: string,
@@ -40,6 +40,8 @@ export {
40
40
  ElevenLabsConfigSchema,
41
41
  VALID_CONVERSATION_TIMEOUTS,
42
42
  } from "./schemas/elevenlabs.js";
43
+ export type { FishAudioConfig } from "./schemas/fish-audio.js";
44
+ export { FishAudioConfigSchema } from "./schemas/fish-audio.js";
43
45
  export type { HeartbeatConfig } from "./schemas/heartbeat.js";
44
46
  export { HeartbeatConfigSchema } from "./schemas/heartbeat.js";
45
47
  export type {
@@ -173,8 +175,6 @@ export {
173
175
  SkillsInstallConfigSchema,
174
176
  SkillsLoadConfigSchema,
175
177
  } from "./schemas/skills.js";
176
- export type { SwarmConfig } from "./schemas/swarm.js";
177
- export { SwarmConfigSchema } from "./schemas/swarm.js";
178
178
  export type { RateLimitConfig, TimeoutConfig } from "./schemas/timeouts.js";
179
179
  export {
180
180
  RateLimitConfigSchema,
@@ -193,6 +193,7 @@ import {
193
193
  WhatsAppConfigSchema,
194
194
  } from "./schemas/channels.js";
195
195
  import { ElevenLabsConfigSchema } from "./schemas/elevenlabs.js";
196
+ import { FishAudioConfigSchema } from "./schemas/fish-audio.js";
196
197
  import { HeartbeatConfigSchema } from "./schemas/heartbeat.js";
197
198
  import {
198
199
  ContextWindowConfigSchema,
@@ -223,7 +224,6 @@ import {
223
224
  VALID_INFERENCE_PROVIDERS,
224
225
  } from "./schemas/services.js";
225
226
  import { SkillsConfigSchema } from "./schemas/skills.js";
226
- import { SwarmConfigSchema } from "./schemas/swarm.js";
227
227
  import {
228
228
  RateLimitConfigSchema,
229
229
  TimeoutConfigSchema,
@@ -281,7 +281,6 @@ export const AssistantConfigSchema = z
281
281
  "Custom pricing overrides for specific provider/model combinations",
282
282
  ),
283
283
  heartbeat: HeartbeatConfigSchema.default(HeartbeatConfigSchema.parse({})),
284
- swarm: SwarmConfigSchema.default(SwarmConfigSchema.parse({})),
285
284
  mcp: McpConfigSchema.default(McpConfigSchema.parse({})),
286
285
  acp: AcpConfigSchema.default(AcpConfigSchema.parse({})),
287
286
  skills: SkillsConfigSchema.default(SkillsConfigSchema.parse({})),
@@ -293,6 +292,7 @@ export const AssistantConfigSchema = z
293
292
  elevenlabs: ElevenLabsConfigSchema.default(
294
293
  ElevenLabsConfigSchema.parse({}),
295
294
  ),
295
+ fishAudio: FishAudioConfigSchema.default(FishAudioConfigSchema.parse({})),
296
296
  whatsapp: WhatsAppConfigSchema.default(WhatsAppConfigSchema.parse({})),
297
297
  telegram: TelegramConfigSchema.default(TelegramConfigSchema.parse({})),
298
298
  slack: SlackConfigSchema.default(SlackConfigSchema.parse({})),
@@ -303,15 +303,6 @@ export const AssistantConfigSchema = z
303
303
  NotificationsConfigSchema.parse({}),
304
304
  ),
305
305
  ui: UiConfigSchema.default(UiConfigSchema.parse({})),
306
- assistantFeatureFlagValues: z
307
- .record(
308
- z.string(),
309
- z.boolean({
310
- error: "assistantFeatureFlagValues values must be booleans",
311
- }),
312
- )
313
- .optional()
314
- .describe("Feature flag overrides — map of flag names to boolean values"),
315
306
  collectUsageData: z
316
307
  .boolean()
317
308
  .default(true)
@@ -6,6 +6,7 @@ export const VALID_CALLER_IDENTITY_MODES = [
6
6
  "user_number",
7
7
  ] as const;
8
8
  const VALID_CALL_TRANSCRIPTION_PROVIDERS = ["Deepgram", "Google"] as const;
9
+ export const VALID_TTS_PROVIDERS = ["elevenlabs", "fish-audio"] as const;
9
10
 
10
11
  export const CallsDisclosureConfigSchema = z
11
12
  .object({
@@ -57,6 +58,18 @@ export const CallsVoiceConfigSchema = z
57
58
  })
58
59
  .default("Deepgram")
59
60
  .describe("Speech-to-text provider used for call transcription"),
61
+ speechModel: z
62
+ .string({ error: "calls.voice.speechModel must be a string" })
63
+ .optional()
64
+ .describe(
65
+ "ASR model to use for speech recognition (e.g. nova-3, nova-2-phonecall for Deepgram; telephony, long for Google)",
66
+ ),
67
+ ttsProvider: z
68
+ .enum(VALID_TTS_PROVIDERS, {
69
+ error: `calls.voice.ttsProvider must be one of: ${VALID_TTS_PROVIDERS.join(", ")}`,
70
+ })
71
+ .default("elevenlabs")
72
+ .describe("Text-to-speech provider for phone calls"),
60
73
  })
61
74
  .describe("Voice and speech settings for phone calls");
62
75
 
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+
3
+ export const FishAudioConfigSchema = z
4
+ .object({
5
+ referenceId: z
6
+ .string({ error: "fishAudio.referenceId must be a string" })
7
+ .default("")
8
+ .describe("Fish Audio voice/clone reference ID"),
9
+ chunkLength: z
10
+ .number({ error: "fishAudio.chunkLength must be a number" })
11
+ .int("fishAudio.chunkLength must be an integer")
12
+ .min(100, "fishAudio.chunkLength must be >= 100")
13
+ .max(300, "fishAudio.chunkLength must be <= 300")
14
+ .default(200)
15
+ .describe("Text chunk size for streaming synthesis"),
16
+ format: z
17
+ .enum(["mp3", "wav", "opus"], {
18
+ error: "fishAudio.format must be one of: mp3, wav, opus",
19
+ })
20
+ .default("mp3")
21
+ .describe("Output audio format"),
22
+ latency: z
23
+ .enum(["normal", "balanced"], {
24
+ error: "fishAudio.latency must be one of: normal, balanced",
25
+ })
26
+ .default("normal")
27
+ .describe(
28
+ "Latency/quality tradeoff for Fish Audio S2 synthesis. 'normal' prioritizes lower latency; 'balanced' trades latency for higher quality.",
29
+ ),
30
+ speed: z
31
+ .number({ error: "fishAudio.speed must be a number" })
32
+ .min(0.5, "fishAudio.speed must be >= 0.5")
33
+ .max(2.0, "fishAudio.speed must be <= 2.0")
34
+ .default(1.0)
35
+ .describe("Playback speed multiplier (0.5 = slower, 2.0 = faster)"),
36
+ })
37
+ .describe("Fish Audio text-to-speech configuration");
38
+
39
+ export type FishAudioConfig = z.infer<typeof FishAudioConfigSchema>;
@@ -50,10 +50,6 @@ export const SecretDetectionConfigSchema = z
50
50
  .describe(
51
51
  "Whether to allow sending a detected secret once (with user confirmation) before redacting future occurrences",
52
52
  ),
53
- blockIngress: z
54
- .boolean({ error: "secretDetection.blockIngress must be a boolean" })
55
- .default(true)
56
- .describe("Whether to block secrets in incoming ingress messages"),
57
53
  customPatterns: z
58
54
  .array(CustomSecretPatternSchema)
59
55
  .optional()
@@ -35,7 +35,6 @@ export type {
35
35
  SkillsInstallConfig,
36
36
  SkillsLoadConfig,
37
37
  SlackConfig,
38
- SwarmConfig,
39
38
  ThinkingConfig,
40
39
  TimeoutConfig,
41
40
  UiConfig,
@@ -28,6 +28,36 @@ function escapeLike(value: string): string {
28
28
  return value.replace(/%/g, "").replace(/_/g, "");
29
29
  }
30
30
 
31
+ /**
32
+ * Generate a collision-free slugified filename for a contact's per-user persona file.
33
+ * Produces filenames like "alice.md", "alice-2.md", "alice-3.md", etc.
34
+ */
35
+ export function generateUserFileSlug(displayName: string): string {
36
+ const slug =
37
+ displayName
38
+ .toLowerCase()
39
+ .replace(/[^a-z0-9]+/g, "-")
40
+ .replace(/^-+|-+$/g, "")
41
+ .slice(0, 50) || "user";
42
+
43
+ const db = getDb();
44
+ const rows = db
45
+ .select({ userFile: contacts.userFile })
46
+ .from(contacts)
47
+ .where(like(contacts.userFile, `${escapeLike(slug)}%`))
48
+ .all();
49
+
50
+ const taken = new Set(rows.map((r) => r.userFile));
51
+
52
+ const base = `${slug}.md`;
53
+ if (!taken.has(base)) return base;
54
+
55
+ for (let i = 2; ; i++) {
56
+ const candidate = `${slug}-${i}.md`;
57
+ if (!taken.has(candidate)) return candidate;
58
+ }
59
+ }
60
+
31
61
  function parseContact(row: typeof contacts.$inferSelect): Contact {
32
62
  return {
33
63
  id: row.id,
@@ -40,6 +70,7 @@ function parseContact(row: typeof contacts.$inferSelect): Contact {
40
70
  role: row.role as Contact["role"],
41
71
  contactType: (row.contactType as Contact["contactType"]) ?? "human",
42
72
  principalId: row.principalId,
73
+ userFile: row.userFile ?? null,
43
74
  };
44
75
  }
45
76
 
@@ -148,6 +179,7 @@ export function upsertContact(params: {
148
179
  role?: ContactRole;
149
180
  contactType?: ContactType;
150
181
  principalId?: string | null;
182
+ userFile?: string | null;
151
183
  channels?: SyncChannelData[];
152
184
  /** When true, conflicting channels on other contacts are reassigned to this
153
185
  * contact instead of being skipped. Used by invite redemption to bind a
@@ -177,6 +209,7 @@ export function upsertContact(params: {
177
209
  updateSet.contactType = params.contactType;
178
210
  if (params.principalId !== undefined)
179
211
  updateSet.principalId = params.principalId;
212
+ if (params.userFile !== undefined) updateSet.userFile = params.userFile;
180
213
 
181
214
  db.update(contacts)
182
215
  .set(updateSet)
@@ -224,6 +257,7 @@ export function upsertContact(params: {
224
257
  updateSet.contactType = params.contactType;
225
258
  if (params.principalId !== undefined)
226
259
  updateSet.principalId = params.principalId;
260
+ if (params.userFile !== undefined) updateSet.userFile = params.userFile;
227
261
 
228
262
  db.update(contacts)
229
263
  .set(updateSet)
@@ -239,6 +273,10 @@ export function upsertContact(params: {
239
273
 
240
274
  // Create new contact
241
275
  contactId = contactId ?? uuid();
276
+ const userFileValue =
277
+ params.userFile !== undefined
278
+ ? params.userFile
279
+ : generateUserFileSlug(params.displayName);
242
280
  db.insert(contacts)
243
281
  .values({
244
282
  id: contactId,
@@ -247,6 +285,7 @@ export function upsertContact(params: {
247
285
  role: params.role ?? "contact",
248
286
  contactType: params.contactType ?? "human",
249
287
  principalId: params.principalId ?? null,
288
+ userFile: userFileValue ?? null,
250
289
  createdAt: now,
251
290
  updatedAt: now,
252
291
  })
@@ -44,6 +44,8 @@ export interface Contact {
44
44
  * identified by channel address instead.
45
45
  */
46
46
  principalId: string | null;
47
+ /** Workspace-relative path to a per-user persona file for this contact. */
48
+ userFile: string | null;
47
49
  }
48
50
 
49
51
  export type ChannelStatus =
@@ -103,6 +103,7 @@ function mapUserDecisionToCesDecision(
103
103
  userDecision: decision,
104
104
  };
105
105
  case "temporary_override":
106
+ case "dangerously_skip_permissions":
106
107
  return {
107
108
  grantDecision: "approved",
108
109
  ttl: undefined,
@@ -79,6 +79,11 @@ export interface LocalDiscoverySuccess {
79
79
  executablePath: string;
80
80
  }
81
81
 
82
+ export interface LocalSourceDiscoverySuccess {
83
+ mode: "local-source";
84
+ sourcePath: string;
85
+ }
86
+
82
87
  export interface ManagedDiscoverySuccess {
83
88
  mode: "managed";
84
89
  socketPath: string;
@@ -91,6 +96,7 @@ export interface DiscoveryFailure {
91
96
 
92
97
  export type DiscoveryResult =
93
98
  | LocalDiscoverySuccess
99
+ | LocalSourceDiscoverySuccess
94
100
  | ManagedDiscoverySuccess
95
101
  | DiscoveryFailure;
96
102
 
@@ -101,11 +107,16 @@ export type DiscoveryResult =
101
107
  /**
102
108
  * Discover the local CES executable.
103
109
  *
104
- * Searches well-known paths for the `credential-executor` binary. Returns
105
- * a structured result never throws. If the binary is not found, returns
110
+ * Searches well-known paths for the `credential-executor` binary. If the
111
+ * compiled binary is not found, falls back to the TypeScript source entry
112
+ * point in the monorepo. Returns a structured result — never throws. If
113
+ * neither the binary nor the source entry point is found, returns
106
114
  * `{ mode: "unavailable" }` so the caller can fail closed.
107
115
  */
108
- export function discoverLocalCes(): LocalDiscoverySuccess | DiscoveryFailure {
116
+ export function discoverLocalCes():
117
+ | LocalDiscoverySuccess
118
+ | LocalSourceDiscoverySuccess
119
+ | DiscoveryFailure {
109
120
  const searchPaths = getLocalBinarySearchPaths();
110
121
 
111
122
  for (const candidate of searchPaths) {
@@ -115,7 +126,20 @@ export function discoverLocalCes(): LocalDiscoverySuccess | DiscoveryFailure {
115
126
  }
116
127
  }
117
128
 
118
- const reason = `CES executable not found. Searched: ${searchPaths.join(", ")}`;
129
+ // Fallback: check for source entry point in the monorepo
130
+ const monorepoRoot = join(import.meta.dir, "..", "..", "..", "..");
131
+ const sourceEntry = join(
132
+ monorepoRoot,
133
+ "credential-executor",
134
+ "src",
135
+ "main.ts",
136
+ );
137
+ if (existsSync(sourceEntry)) {
138
+ log.info({ path: sourceEntry }, "Found local CES source entry point");
139
+ return { mode: "local-source", sourcePath: sourceEntry };
140
+ }
141
+
142
+ const reason = `CES executable not found. Searched: ${searchPaths.join(", ")}; also checked source at ${sourceEntry}`;
119
143
  log.warn(reason);
120
144
  return { mode: "unavailable", reason };
121
145
  }
@@ -35,6 +35,10 @@ export const CES_GRANT_AUDIT_FLAG_KEY =
35
35
  export const CES_MANAGED_SIDECAR_FLAG_KEY =
36
36
  "feature_flags.ces-managed-sidecar.enabled" as const;
37
37
 
38
+ /** Gate for routing credential reads/writes through the CES process. */
39
+ export const CES_CREDENTIAL_BACKEND_FLAG_KEY =
40
+ "feature_flags.ces-credential-backend.enabled" as const;
41
+
38
42
  // ---------------------------------------------------------------------------
39
43
  // Public API — predicate functions
40
44
  // ---------------------------------------------------------------------------
@@ -73,3 +77,15 @@ export function isCesGrantAuditEnabled(config: AssistantConfig): boolean {
73
77
  export function isCesManagedSidecarEnabled(config: AssistantConfig): boolean {
74
78
  return isAssistantFeatureFlagEnabled(CES_MANAGED_SIDECAR_FLAG_KEY, config);
75
79
  }
80
+
81
+ /**
82
+ * Whether credential reads and writes should be routed through the CES process.
83
+ */
84
+ export function isCesCredentialBackendEnabled(
85
+ config: AssistantConfig,
86
+ ): boolean {
87
+ return isAssistantFeatureFlagEnabled(
88
+ CES_CREDENTIAL_BACKEND_FLAG_KEY,
89
+ config,
90
+ );
91
+ }
@@ -38,6 +38,7 @@ import {
38
38
  discoverLocalCes,
39
39
  type DiscoveryResult,
40
40
  type LocalDiscoverySuccess,
41
+ type LocalSourceDiscoverySuccess,
41
42
  type ManagedDiscoverySuccess,
42
43
  } from "./executable-discovery.js";
43
44
  import { isCesManagedSidecarEnabled } from "./feature-gates.js";
@@ -156,6 +157,12 @@ export function createCesProcessManager(
156
157
  return transport;
157
158
  }
158
159
 
160
+ if (discoveryResult.mode === "local-source") {
161
+ const transport = await startLocalSourceProcess(discoveryResult);
162
+ running = true;
163
+ return transport;
164
+ }
165
+
159
166
  // managed mode
160
167
  const transport = await connectManagedSocket(discoveryResult);
161
168
  running = true;
@@ -235,6 +242,37 @@ export function createCesProcessManager(
235
242
  return createStdioTransport(proc);
236
243
  }
237
244
 
245
+ // -------------------------------------------------------------------------
246
+ // Local source mode — child process over stdio (bun run)
247
+ // -------------------------------------------------------------------------
248
+
249
+ async function startLocalSourceProcess(
250
+ discovery: LocalSourceDiscoverySuccess,
251
+ ): Promise<CesTransport> {
252
+ log.info(
253
+ { sourcePath: discovery.sourcePath },
254
+ "Spawning CES child process from source",
255
+ );
256
+
257
+ const proc = Bun.spawn({
258
+ cmd: ["bun", "run", discovery.sourcePath],
259
+ stdin: "pipe",
260
+ stdout: "pipe",
261
+ stderr: "ignore",
262
+ env: {
263
+ ...process.env,
264
+ // Signal to CES that it was launched by the assistant
265
+ CES_LAUNCHED_BY: "assistant",
266
+ },
267
+ });
268
+
269
+ childProcess = proc;
270
+
271
+ log.info({ pid: proc.pid }, "CES child process started (from source)");
272
+
273
+ return createStdioTransport(proc);
274
+ }
275
+
238
276
  // -------------------------------------------------------------------------
239
277
  // Managed mode — Unix socket connection
240
278
  // -------------------------------------------------------------------------
@@ -76,6 +76,15 @@ const EXTENSION_MIME_MAP: Record<string, string> = {
76
76
  js: "text/javascript",
77
77
  ts: "text/typescript",
78
78
 
79
+ // Audio
80
+ mp3: "audio/mpeg",
81
+ wav: "audio/wav",
82
+ ogg: "audio/ogg",
83
+ flac: "audio/flac",
84
+ aac: "audio/aac",
85
+ m4a: "audio/x-m4a",
86
+ opus: "audio/opus",
87
+
79
88
  // Video
80
89
  mp4: "video/mp4",
81
90
  webm: "video/webm",
@@ -12,6 +12,7 @@ import {
12
12
  } from "node:fs";
13
13
  import { join } from "node:path";
14
14
 
15
+ import { clearFeatureFlagOverridesCache } from "../config/assistant-feature-flags.js";
15
16
  import { getConfig, invalidateConfigCache } from "../config/loader.js";
16
17
  import { clearEmbeddingBackendCache } from "../memory/embedding-backend.js";
17
18
  import { clearCache as clearTrustCache } from "../permissions/trust-store.js";
@@ -153,6 +154,10 @@ export class ConfigWatcher {
153
154
  "trust.json": () => {
154
155
  clearTrustCache();
155
156
  },
157
+ "feature-flags.json": () => {
158
+ clearFeatureFlagOverridesCache();
159
+ onConversationEvict();
160
+ },
156
161
  "secret-allowlist.json": () => {
157
162
  resetAllowlist();
158
163
  try {