@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
@@ -41,6 +41,10 @@ const parseCallSession = createRowMapper<
41
41
  inviteGuardianName: "inviteGuardianName",
42
42
  callerIdentityMode: "callerIdentityMode",
43
43
  callerIdentitySource: "callerIdentitySource",
44
+ skipDisclosure: {
45
+ from: "skipDisclosure",
46
+ transform: (v: unknown) => v === 1,
47
+ },
44
48
  initiatedFromConversationId: "initiatedFromConversationId",
45
49
  startedAt: "startedAt",
46
50
  endedAt: "endedAt",
@@ -87,11 +91,13 @@ export function createCallSession(opts: {
87
91
  inviteGuardianName?: string;
88
92
  callerIdentityMode?: string;
89
93
  callerIdentitySource?: string;
94
+ skipDisclosure?: boolean;
90
95
  initiatedFromConversationId?: string;
91
96
  }): CallSession {
92
97
  const db = getDb();
93
98
  const now = Date.now();
94
- const session = {
99
+ const skipDisclosure = opts.skipDisclosure ?? false;
100
+ const row = {
95
101
  id: uuid(),
96
102
  conversationId: opts.conversationId,
97
103
  provider: opts.provider,
@@ -106,6 +112,7 @@ export function createCallSession(opts: {
106
112
  inviteGuardianName: opts.inviteGuardianName ?? null,
107
113
  callerIdentityMode: opts.callerIdentityMode ?? null,
108
114
  callerIdentitySource: opts.callerIdentitySource ?? null,
115
+ skipDisclosure: skipDisclosure ? 1 : 0,
109
116
  initiatedFromConversationId: opts.initiatedFromConversationId ?? null,
110
117
  startedAt: null,
111
118
  endedAt: null,
@@ -113,8 +120,8 @@ export function createCallSession(opts: {
113
120
  createdAt: now,
114
121
  updatedAt: now,
115
122
  };
116
- db.insert(callSessions).values(session).run();
117
- return session;
123
+ db.insert(callSessions).values(row).run();
124
+ return { ...row, skipDisclosure };
118
125
  }
119
126
 
120
127
  export function getCallSession(id: string): CallSession | null {
@@ -0,0 +1,117 @@
1
+ import type { FishAudioConfig } from "../config/schemas/fish-audio.js";
2
+ import { credentialKey } from "../security/credential-key.js";
3
+ import { getSecureKeyAsync } from "../security/secure-keys.js";
4
+ import { getLogger } from "../util/logger.js";
5
+
6
+ const log = getLogger("fish-audio-client");
7
+
8
+ /** Timeout waiting for the first chunk from Fish Audio (ms). */
9
+ const FIRST_CHUNK_TIMEOUT_MS = 10_000;
10
+
11
+ /** Timeout waiting between consecutive chunks (ms). */
12
+ const IDLE_TIMEOUT_MS = 5_000;
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Fish Audio REST API (POST /v1/tts)
16
+ // ---------------------------------------------------------------------------
17
+
18
+ interface SynthesizeOptions {
19
+ onChunk?: (chunk: Uint8Array) => void;
20
+ signal?: AbortSignal;
21
+ }
22
+
23
+ /**
24
+ * Synthesize text to audio using the Fish Audio REST API with the s2-pro
25
+ * model. Streams audio chunks via the optional `onChunk` callback as they
26
+ * arrive from the server's chunked transfer-encoded response. Returns the
27
+ * complete audio buffer when the response finishes.
28
+ *
29
+ * Pass an `AbortSignal` to cancel in-flight synthesis (e.g. on barge-in).
30
+ */
31
+ export async function synthesizeWithFishAudio(
32
+ text: string,
33
+ config: FishAudioConfig,
34
+ options?: SynthesizeOptions,
35
+ ): Promise<Buffer> {
36
+ const apiKey = await getSecureKeyAsync(
37
+ credentialKey("fish-audio", "api_key"),
38
+ );
39
+ if (!apiKey) {
40
+ throw new Error(
41
+ "Fish Audio API key not configured. Store it via: assistant credentials set --service fish-audio --field api_key <key>",
42
+ );
43
+ }
44
+
45
+ const body = {
46
+ text,
47
+ reference_id: config.referenceId || undefined,
48
+ model: "s2-pro",
49
+ format: config.format,
50
+ mp3_bitrate: 192,
51
+ chunk_length: config.chunkLength,
52
+ normalize: true,
53
+ latency: config.latency,
54
+ temperature: 1.0,
55
+ prosody: config.speed !== 1.0 ? { speed: config.speed } : undefined,
56
+ };
57
+
58
+ log.info(
59
+ {
60
+ referenceId: config.referenceId,
61
+ format: config.format,
62
+ textLength: text.length,
63
+ },
64
+ "Starting Fish Audio synthesis",
65
+ );
66
+
67
+ const response = await fetch("https://api.fish.audio/v1/tts", {
68
+ method: "POST",
69
+ headers: {
70
+ Authorization: `Bearer ${apiKey}`,
71
+ "Content-Type": "application/json",
72
+ },
73
+ body: JSON.stringify(body),
74
+ signal: options?.signal,
75
+ });
76
+
77
+ if (!response.ok) {
78
+ const errorText = await response.text();
79
+ throw new Error(`Fish Audio API error (${response.status}): ${errorText}`);
80
+ }
81
+
82
+ if (!response.body) {
83
+ throw new Error("Fish Audio API returned no body");
84
+ }
85
+
86
+ const chunks: Uint8Array[] = [];
87
+ const reader = response.body.getReader();
88
+ let isFirstChunk = true;
89
+
90
+ while (true) {
91
+ const timeoutMs = isFirstChunk ? FIRST_CHUNK_TIMEOUT_MS : IDLE_TIMEOUT_MS;
92
+ const timeout = new Promise<never>((_, reject) =>
93
+ setTimeout(
94
+ () => reject(new Error(`Fish Audio read timed out after ${timeoutMs}ms`)),
95
+ timeoutMs,
96
+ ),
97
+ );
98
+ const { done, value } = await Promise.race([reader.read(), timeout]);
99
+ if (done) break;
100
+ if (value) {
101
+ isFirstChunk = false;
102
+ chunks.push(value);
103
+ options?.onChunk?.(value);
104
+ }
105
+ }
106
+
107
+ const totalLength = chunks.reduce((sum, c) => sum + c.byteLength, 0);
108
+ const merged = new Uint8Array(totalLength);
109
+ let offset = 0;
110
+ for (const chunk of chunks) {
111
+ merged.set(chunk, offset);
112
+ offset += chunk.byteLength;
113
+ }
114
+
115
+ log.debug({ bytes: totalLength }, "Fish Audio synthesis complete");
116
+ return Buffer.from(merged);
117
+ }
@@ -139,6 +139,12 @@ export interface RelayEndMessage {
139
139
  handoffData?: string;
140
140
  }
141
141
 
142
+ export interface RelayPlayMessage {
143
+ type: "play";
144
+ source: string;
145
+ interruptible: boolean;
146
+ }
147
+
142
148
  // ── WebSocket data type ──────────────────────────────────────────────
143
149
 
144
150
  export interface RelayWebSocketData {
@@ -323,6 +329,27 @@ export class RelayConnection {
323
329
  }
324
330
  }
325
331
 
332
+ /**
333
+ * Send a play-audio URL to the caller. Used when the assistant handles
334
+ * TTS synthesis itself (e.g. Fish Audio) instead of relying on
335
+ * ConversationRelay's built-in TTS.
336
+ */
337
+ sendPlayUrl(url: string): void {
338
+ const message: RelayPlayMessage = {
339
+ type: "play",
340
+ source: url,
341
+ interruptible: true,
342
+ };
343
+ try {
344
+ this.ws.send(JSON.stringify(message));
345
+ } catch (err) {
346
+ log.error(
347
+ { err, callSessionId: this.callSessionId },
348
+ "Failed to send play URL",
349
+ );
350
+ }
351
+ }
352
+
326
353
  /**
327
354
  * End the ConversationRelay session.
328
355
  */
@@ -49,6 +49,7 @@ export function generateTwiML(
49
49
  profile: {
50
50
  language: string;
51
51
  transcriptionProvider: string;
52
+ speechModel?: string;
52
53
  ttsProvider: string;
53
54
  voice: string;
54
55
  },
@@ -91,7 +92,7 @@ export function generateTwiML(
91
92
  ${greetingAttr}
92
93
  voice="${escapeXml(profile.voice)}"
93
94
  language="${escapeXml(profile.language)}"
94
- transcriptionProvider="${escapeXml(profile.transcriptionProvider)}"
95
+ transcriptionProvider="${escapeXml(profile.transcriptionProvider)}"${profile.speechModel ? `\n speechModel="${escapeXml(profile.speechModel)}"` : ""}
95
96
  ttsProvider="${escapeXml(profile.ttsProvider)}"
96
97
  interruptible="true"
97
98
  dtmfDetection="true"
@@ -75,6 +75,7 @@ export interface CallSession {
75
75
  inviteGuardianName: string | null;
76
76
  callerIdentityMode: string | null;
77
77
  callerIdentitySource: string | null;
78
+ skipDisclosure: boolean;
78
79
  initiatedFromConversationId?: string | null;
79
80
  startedAt: number | null;
80
81
  endedAt: number | null;
@@ -29,18 +29,6 @@ function fail(error: string): VoiceIngressPreflightFailure {
29
29
  };
30
30
  }
31
31
 
32
- function buildGatewayUnhealthyMessage(
33
- target: string,
34
- error: string | undefined,
35
- afterRecoveryAttempt: boolean,
36
- ): string {
37
- const detail = error ?? "Unknown gateway health check failure";
38
- if (afterRecoveryAttempt) {
39
- return `Voice callback gateway is still unhealthy at ${target} after a local recovery attempt: ${detail}`;
40
- }
41
- return `Voice callback gateway is unhealthy at ${target}: ${detail}`;
42
- }
43
-
44
32
  export async function preflightVoiceIngress(): Promise<VoiceIngressPreflightResult> {
45
33
  const ingressConfig = loadConfig();
46
34
 
@@ -65,36 +53,6 @@ export async function preflightVoiceIngress(): Promise<VoiceIngressPreflightResu
65
53
  );
66
54
  }
67
55
 
68
- const { ensureLocalGatewayReady, probeLocalGatewayHealth } =
69
- await import("../runtime/local-gateway-health.js");
70
-
71
- const initialHealth = await probeLocalGatewayHealth();
72
- if (!initialHealth.healthy && !initialHealth.localDeployment) {
73
- return fail(
74
- buildGatewayUnhealthyMessage(
75
- initialHealth.target,
76
- initialHealth.error,
77
- false,
78
- ),
79
- );
80
- }
81
-
82
- if (initialHealth.localDeployment) {
83
- const recovery = await ensureLocalGatewayReady();
84
- // Re-probe after the wake flow so the dial path only continues when the
85
- // current gateway process is demonstrably serving the callback stack.
86
- const confirmedHealth = await probeLocalGatewayHealth();
87
- if (!confirmedHealth.healthy) {
88
- return fail(
89
- buildGatewayUnhealthyMessage(
90
- confirmedHealth.target,
91
- confirmedHealth.error ?? recovery.error,
92
- recovery.recoveryAttempted,
93
- ),
94
- );
95
- }
96
- }
97
-
98
56
  return {
99
57
  ok: true,
100
58
  ingressConfig: {
@@ -3,6 +3,7 @@ import { loadConfig } from "../config/loader.js";
3
3
  export interface VoiceQualityProfile {
4
4
  language: string;
5
5
  transcriptionProvider: string;
6
+ speechModel?: string;
6
7
  ttsProvider: string;
7
8
  voice: string;
8
9
  }
@@ -42,19 +43,39 @@ export function buildElevenLabsVoiceSpec(config: {
42
43
  /**
43
44
  * Resolve the effective voice quality profile from config.
44
45
  *
45
- * Always uses ElevenLabs TTS via Twilio ConversationRelay.
46
- * The voice ID comes from the shared `elevenlabs.voiceId` config
47
- * (defaults to Amelia ZF6FPAbjXT4488VcRRnw).
46
+ * Supports ElevenLabs (default) and Fish Audio TTS providers.
47
+ * When Fish Audio is selected, `ttsProvider` is set to `"Google"` as a
48
+ * placeholder ConversationRelay requires a valid provider in TwiML, but
49
+ * actual audio is delivered via `play` messages from the call-controller.
50
+ * The voice string is left empty since it is unused in that mode.
51
+ *
52
+ * For ElevenLabs, the voice ID comes from the shared `elevenlabs.voiceId`
53
+ * config (defaults to Amelia — ZF6FPAbjXT4488VcRRnw).
48
54
  */
49
55
  export function resolveVoiceQualityProfile(
50
56
  config?: ReturnType<typeof loadConfig>,
51
57
  ): VoiceQualityProfile {
52
58
  const cfg = config ?? loadConfig();
53
59
  const voice = cfg.calls.voice;
60
+ const configuredTts = voice.ttsProvider ?? "elevenlabs";
61
+ const fishAudio = configuredTts === "fish-audio";
54
62
  return {
55
63
  language: voice.language,
56
64
  transcriptionProvider: voice.transcriptionProvider,
57
- ttsProvider: "ElevenLabs",
58
- voice: buildElevenLabsVoiceSpec(cfg.elevenlabs),
65
+ speechModel:
66
+ voice.speechModel ??
67
+ (voice.transcriptionProvider === "Google" ? undefined : "nova-3"),
68
+ ttsProvider: fishAudio ? "Google" : "ElevenLabs",
69
+ voice: fishAudio ? "" : buildElevenLabsVoiceSpec(cfg.elevenlabs),
59
70
  };
60
71
  }
72
+
73
+ /**
74
+ * Check whether Fish Audio TTS is configured for phone calls.
75
+ */
76
+ export function isFishAudioTts(
77
+ config?: ReturnType<typeof loadConfig>,
78
+ ): boolean {
79
+ const cfg = config ?? loadConfig();
80
+ return cfg.calls.voice?.ttsProvider === "fish-audio";
81
+ }
@@ -20,9 +20,7 @@ import type { ServerMessage } from "../daemon/message-protocol.js";
20
20
  import { buildAssistantEvent } from "../runtime/assistant-event.js";
21
21
  import { assistantEventHub } from "../runtime/assistant-event-hub.js";
22
22
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
23
- import { checkIngressForSecrets } from "../security/secret-ingress.js";
24
23
  import { computeToolApprovalDigest } from "../security/tool-approval-digest.js";
25
- import { IngressBlockedError } from "../util/errors.js";
26
24
  import { getLogger } from "../util/logger.js";
27
25
  import {
28
26
  CALL_OPENING_MARKER,
@@ -95,6 +93,8 @@ export interface VoiceTurnOptions {
95
93
  isInbound: boolean;
96
94
  /** The outbound call task, if any. */
97
95
  task?: string | null;
96
+ /** When true, skip the disclosure announcement for this call. */
97
+ skipDisclosure?: boolean;
98
98
  /** Called for each streaming text token from the agent loop. */
99
99
  onTextDelta: (text: string) => void;
100
100
  /** Called when the agent loop completes a full response. */
@@ -128,9 +128,11 @@ function buildVoiceCallControlPrompt(opts: {
128
128
  isInbound: boolean;
129
129
  task?: string | null;
130
130
  isCallerGuardian?: boolean;
131
+ skipDisclosure?: boolean;
131
132
  }): string {
132
133
  const config = getConfig();
133
- const disclosureEnabled = config.calls?.disclosure?.enabled === true;
134
+ const disclosureEnabled =
135
+ config.calls?.disclosure?.enabled === true && !opts.skipDisclosure;
134
136
  const disclosureText = config.calls?.disclosure?.text?.trim();
135
137
  const disclosureRule =
136
138
  disclosureEnabled && disclosureText
@@ -240,15 +242,6 @@ export async function startVoiceTurn(
240
242
  );
241
243
  }
242
244
 
243
- // Block inbound content that contains secrets
244
- const ingressCheck = checkIngressForSecrets(opts.content);
245
- if (ingressCheck.blocked) {
246
- throw new IngressBlockedError(
247
- ingressCheck.userNotice!,
248
- ingressCheck.detectedTypes,
249
- );
250
- }
251
-
252
245
  const eventSink: VoiceRunEventSink = {
253
246
  onTextDelta: opts.onTextDelta,
254
247
  onMessageComplete: opts.onComplete,
@@ -286,6 +279,7 @@ export async function startVoiceTurn(
286
279
  isInbound: opts.isInbound,
287
280
  task: opts.task,
288
281
  isCallerGuardian,
282
+ skipDisclosure: opts.skipDisclosure,
289
283
  });
290
284
 
291
285
  // Get or create the conversation
@@ -6,7 +6,6 @@ import {
6
6
  loadRawConfig,
7
7
  saveRawConfig,
8
8
  setNestedValue,
9
- syncConfigToLockfile,
10
9
  } from "../../config/loader.js";
11
10
  import { AssistantConfigSchema } from "../../config/schema.js";
12
11
  import { getSchemaAtPath } from "../../config/schema-utils.js";
@@ -73,8 +72,7 @@ Arguments:
73
72
  true, "42" becomes number 42). Falls back to plain string if JSON
74
73
  parsing fails.
75
74
 
76
- After writing the value to config.json, the lockfile is automatically synced
77
- to reflect the updated configuration.
75
+ After writing the value to config.json, the change takes effect immediately.
78
76
 
79
77
  To manage API keys, use "assistant keys set <provider> <key>" instead.
80
78
 
@@ -93,7 +91,6 @@ Examples:
93
91
  }
94
92
  setNestedValue(raw, key, parsed);
95
93
  saveRawConfig(raw);
96
- syncConfigToLockfile();
97
94
  log.info(`Set ${key} = ${JSON.stringify(parsed)}`);
98
95
  });
99
96
 
@@ -15,6 +15,7 @@ import { credentialKey } from "../../security/credential-key.js";
15
15
  import {
16
16
  deleteSecureKeyAsync,
17
17
  getSecureKeyAsync,
18
+ getSecureKeyResultAsync,
18
19
  setSecureKeyAsync,
19
20
  } from "../../security/secure-keys.js";
20
21
  import {
@@ -608,10 +609,19 @@ Examples:
608
609
  return;
609
610
  }
610
611
 
611
- const secret = await getSecureKeyAsync(storageKey);
612
+ const { value: secret, unreachable } =
613
+ await getSecureKeyResultAsync(storageKey);
612
614
 
613
615
  if (!metadata && (secret == null || secret.length === 0)) {
614
- writeOutput(cmd, { ok: false, error: "Credential not found" });
616
+ if (unreachable) {
617
+ writeOutput(cmd, {
618
+ ok: false,
619
+ error:
620
+ "Keychain broker is unreachable — restart the Vellum app and accept the macOS Keychain prompt",
621
+ });
622
+ } else {
623
+ writeOutput(cmd, { ok: false, error: "Credential not found" });
624
+ }
615
625
  process.exitCode = 1;
616
626
  return;
617
627
  }
@@ -646,10 +656,21 @@ Examples:
646
656
 
647
657
  const connection = safeGetConnectionByProvider(metadata.service);
648
658
  const output = buildCredentialOutput(metadata, secret, connection);
659
+
660
+ if (unreachable && (secret == null || secret.length === 0)) {
661
+ output.scrubbedValue = "(broker unreachable)";
662
+ output.brokerUnreachable = true;
663
+ }
664
+
649
665
  writeOutput(cmd, output);
650
666
 
651
667
  if (!shouldOutputJson(cmd)) {
652
668
  printCredentialHuman(output);
669
+ if (unreachable && (secret == null || secret.length === 0)) {
670
+ log.info(
671
+ " \u26A0 Keychain broker unreachable — restart the Vellum app and accept the macOS Keychain prompt to access credentials",
672
+ );
673
+ }
653
674
  }
654
675
  } catch (err) {
655
676
  const message = err instanceof Error ? err.message : String(err);
@@ -725,10 +746,19 @@ Examples:
725
746
  return;
726
747
  }
727
748
 
728
- const secret = await getSecureKeyAsync(storageKey);
749
+ const { value: secret, unreachable } =
750
+ await getSecureKeyResultAsync(storageKey);
729
751
 
730
752
  if (secret == null || secret.length === 0) {
731
- writeOutput(cmd, { ok: false, error: "Credential not found" });
753
+ if (unreachable) {
754
+ writeOutput(cmd, {
755
+ ok: false,
756
+ error:
757
+ "Keychain broker is unreachable — restart the Vellum app and accept the macOS Keychain prompt",
758
+ });
759
+ } else {
760
+ writeOutput(cmd, { ok: false, error: "Credential not found" });
761
+ }
732
762
  process.exitCode = 1;
733
763
  return;
734
764
  }
@@ -2,6 +2,7 @@ import type { Command } from "commander";
2
2
 
3
3
  import { registerAppCommands } from "./apps.js";
4
4
  import { registerConnectionCommands } from "./connections.js";
5
+ import { registerPlatformCommands } from "./platform.js";
5
6
  import { registerProviderCommands } from "./providers.js";
6
7
 
7
8
  export function registerOAuthCommand(program: Command): void {
@@ -49,4 +50,10 @@ Examples:
49
50
  // ---------------------------------------------------------------------------
50
51
 
51
52
  registerConnectionCommands(oauth);
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // platform — subcommand group
56
+ // ---------------------------------------------------------------------------
57
+
58
+ registerPlatformCommands(oauth);
52
59
  }
@@ -0,0 +1,179 @@
1
+ import type { Command } from "commander";
2
+
3
+ import { getConfig } from "../../../config/loader.js";
4
+ import {
5
+ type Services,
6
+ ServicesSchema,
7
+ } from "../../../config/schemas/services.js";
8
+ import { getProvider } from "../../../oauth/oauth-store.js";
9
+ import { VellumPlatformClient } from "../../../platform/client.js";
10
+ import { getCliLogger } from "../../logger.js";
11
+ import { shouldOutputJson, writeOutput } from "../../output.js";
12
+
13
+ const log = getCliLogger("cli");
14
+
15
+ /**
16
+ * Normalize a bare provider name (e.g. "google") into the canonical provider
17
+ * key used internally (e.g. "integration:google").
18
+ */
19
+ function toProviderKey(provider: string): string {
20
+ return provider.startsWith("integration:")
21
+ ? provider
22
+ : `integration:${provider}`;
23
+ }
24
+
25
+ export function registerPlatformCommands(oauth: Command): void {
26
+ const platform = oauth
27
+ .command("platform")
28
+ .description(
29
+ "Query platform-managed OAuth provider status and connections",
30
+ );
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // platform status <provider>
34
+ // ---------------------------------------------------------------------------
35
+
36
+ platform
37
+ .command("status <provider>")
38
+ .description(
39
+ "Check whether a provider supports managed OAuth and list the user's active connections",
40
+ )
41
+ .addHelpText(
42
+ "after",
43
+ `
44
+ Arguments:
45
+ provider Provider name (e.g. google, slack, twitter)
46
+
47
+ Checks whether the platform offers managed OAuth for the given provider,
48
+ whether managed mode is currently enabled, and lists any active connections
49
+ the user has set up on the platform.
50
+
51
+ Examples:
52
+ $ assistant oauth platform status google
53
+ $ assistant oauth platform status slack --json`,
54
+ )
55
+ .action(
56
+ async (
57
+ provider: string,
58
+ _opts: Record<string, unknown>,
59
+ cmd: Command,
60
+ ) => {
61
+ try {
62
+ const providerKey = toProviderKey(provider);
63
+ const providerRow = getProvider(providerKey);
64
+
65
+ // 1. Check if the provider even supports managed mode
66
+ const managedKey = providerRow?.managedServiceConfigKey;
67
+ if (!managedKey || !(managedKey in ServicesSchema.shape)) {
68
+ writeOutput(cmd, {
69
+ ok: true,
70
+ provider,
71
+ managedAvailable: false,
72
+ managedEnabled: false,
73
+ connections: [],
74
+ });
75
+ if (!shouldOutputJson(cmd)) {
76
+ log.info(
77
+ `Provider "${provider}" does not support platform-managed OAuth`,
78
+ );
79
+ }
80
+ return;
81
+ }
82
+
83
+ // 2. Check if managed mode is enabled in the services config
84
+ const services: Services = getConfig().services;
85
+ const managedEnabled =
86
+ services[managedKey as keyof Services].mode === "managed";
87
+
88
+ if (!managedEnabled) {
89
+ writeOutput(cmd, {
90
+ ok: true,
91
+ provider,
92
+ managedAvailable: true,
93
+ managedEnabled: false,
94
+ connections: [],
95
+ });
96
+ if (!shouldOutputJson(cmd)) {
97
+ log.info(
98
+ `Provider "${provider}" supports managed OAuth but is set to "your-own" mode`,
99
+ );
100
+ }
101
+ return;
102
+ }
103
+
104
+ // 3. Fetch active connections from the platform
105
+ const client = await VellumPlatformClient.create();
106
+ if (!client || !client.platformAssistantId) {
107
+ writeOutput(cmd, {
108
+ ok: false,
109
+ error:
110
+ "Platform prerequisites not met (not logged in or missing assistant ID)",
111
+ });
112
+ process.exitCode = 1;
113
+ return;
114
+ }
115
+
116
+ const params = new URLSearchParams();
117
+ params.set("provider", provider);
118
+ params.set("status", "ACTIVE");
119
+
120
+ const path = `/v1/assistants/${encodeURIComponent(client.platformAssistantId)}/oauth/connections/?${params.toString()}`;
121
+ const response = await client.fetch(path);
122
+
123
+ if (!response.ok) {
124
+ writeOutput(cmd, {
125
+ ok: false,
126
+ error: `Platform returned HTTP ${response.status}`,
127
+ });
128
+ process.exitCode = 1;
129
+ return;
130
+ }
131
+
132
+ const body = (await response.json()) as unknown;
133
+
134
+ // The platform returns either a flat array or a {results: [...]} wrapper.
135
+ const rawEntries = (
136
+ Array.isArray(body)
137
+ ? body
138
+ : ((body as Record<string, unknown>).results ?? [])
139
+ ) as Array<{
140
+ id: string;
141
+ account_label?: string;
142
+ scopes_granted?: string[];
143
+ status?: string;
144
+ }>;
145
+
146
+ const connections = rawEntries.map((c) => ({
147
+ id: c.id,
148
+ accountLabel: c.account_label ?? null,
149
+ scopesGranted: c.scopes_granted ?? [],
150
+ status: c.status ?? "ACTIVE",
151
+ }));
152
+
153
+ writeOutput(cmd, {
154
+ ok: true,
155
+ provider,
156
+ managedAvailable: true,
157
+ managedEnabled: true,
158
+ connections,
159
+ });
160
+
161
+ if (!shouldOutputJson(cmd)) {
162
+ if (connections.length === 0) {
163
+ log.info(
164
+ `Provider "${provider}" is managed but has no active connections`,
165
+ );
166
+ } else {
167
+ log.info(
168
+ `Provider "${provider}": ${connections.length} active connection(s)`,
169
+ );
170
+ }
171
+ }
172
+ } catch (err) {
173
+ const message = err instanceof Error ? err.message : String(err);
174
+ writeOutput(cmd, { ok: false, error: message });
175
+ process.exitCode = 1;
176
+ }
177
+ },
178
+ );
179
+ }