@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
@@ -0,0 +1,86 @@
1
+ /**
2
+ * HTTP route definitions for message text-to-speech synthesis.
3
+ *
4
+ * POST /v1/messages/:id/tts?conversationId=... — synthesize message text to audio
5
+ *
6
+ * Gated behind the `feature_flags.message-tts.enabled` assistant feature flag.
7
+ * Uses Fish Audio for synthesis when configured.
8
+ */
9
+
10
+ import { synthesizeWithFishAudio } from "../../calls/fish-audio-client.js";
11
+ import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
12
+ import { getConfig } from "../../config/loader.js";
13
+ import { getMessageContent } from "../../daemon/handlers/conversation-history.js";
14
+ import { getLogger } from "../../util/logger.js";
15
+ import { httpError } from "../http-errors.js";
16
+ import type { RouteDefinition } from "../http-router.js";
17
+
18
+ const log = getLogger("tts-routes");
19
+
20
+ const MESSAGE_TTS_FLAG = "feature_flags.message-tts.enabled" as const;
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Route definitions
24
+ // ---------------------------------------------------------------------------
25
+
26
+ export function ttsRouteDefinitions(): RouteDefinition[] {
27
+ return [
28
+ {
29
+ endpoint: "messages/:id/tts",
30
+ method: "POST",
31
+ policyKey: "messages/tts",
32
+ handler: async ({ url, params }) => {
33
+ const config = getConfig();
34
+
35
+ if (!isAssistantFeatureFlagEnabled(MESSAGE_TTS_FLAG, config)) {
36
+ return httpError("FORBIDDEN", "Message TTS is not enabled", 403);
37
+ }
38
+
39
+ const messageId = params.id;
40
+ const conversationId =
41
+ url.searchParams.get("conversationId") ?? undefined;
42
+
43
+ const result = getMessageContent(messageId, conversationId);
44
+ if (!result) {
45
+ return httpError("NOT_FOUND", `Message ${messageId} not found`, 404);
46
+ }
47
+
48
+ if (!result.text) {
49
+ return httpError("BAD_REQUEST", "Message has no text content", 400);
50
+ }
51
+
52
+ const { fishAudio } = config;
53
+ if (!fishAudio?.referenceId) {
54
+ return httpError(
55
+ "SERVICE_UNAVAILABLE",
56
+ "Fish Audio TTS is not configured",
57
+ 503,
58
+ );
59
+ }
60
+
61
+ try {
62
+ const audioBuffer = await synthesizeWithFishAudio(
63
+ result.text,
64
+ fishAudio,
65
+ );
66
+
67
+ const format = fishAudio.format ?? "mp3";
68
+ const contentType =
69
+ format === "wav"
70
+ ? "audio/wav"
71
+ : format === "opus"
72
+ ? "audio/opus"
73
+ : "audio/mpeg";
74
+
75
+ return new Response(new Uint8Array(audioBuffer), {
76
+ status: 200,
77
+ headers: { "Content-Type": contentType },
78
+ });
79
+ } catch (err) {
80
+ log.error({ err, messageId }, "TTS synthesis failed");
81
+ return httpError("INTERNAL_ERROR", "TTS synthesis failed", 502);
82
+ }
83
+ },
84
+ },
85
+ ];
86
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Upgrade broadcast endpoint — publishes service group update lifecycle
3
- * events (starting / complete) to all connected SSE clients.
3
+ * events (starting / progress / complete) to all connected SSE clients.
4
4
  *
5
5
  * Protected by a route policy restricting access to gateway service
6
6
  * principals only (`svc_gateway` with `internal.write` scope), following
@@ -11,6 +11,7 @@
11
11
 
12
12
  import type {
13
13
  ServiceGroupUpdateComplete,
14
+ ServiceGroupUpdateProgress,
14
15
  ServiceGroupUpdateStarting,
15
16
  } from "../../daemon/message-types/upgrades.js";
16
17
  import { buildAssistantEvent } from "../assistant-event.js";
@@ -86,6 +87,29 @@ export function upgradeBroadcastRouteDefinitions(): RouteDefinition[] {
86
87
  return Response.json({ ok: true });
87
88
  }
88
89
 
90
+ if (type === "progress") {
91
+ const { statusMessage } = body as { statusMessage?: unknown };
92
+
93
+ if (typeof statusMessage !== "string" || statusMessage.length === 0) {
94
+ return httpError(
95
+ "BAD_REQUEST",
96
+ "statusMessage is required and must be a non-empty string",
97
+ 400,
98
+ );
99
+ }
100
+
101
+ const message: ServiceGroupUpdateProgress = {
102
+ type: "service_group_update_progress",
103
+ statusMessage,
104
+ };
105
+
106
+ await assistantEventHub.publish(
107
+ buildAssistantEvent(DAEMON_INTERNAL_ASSISTANT_ID, message),
108
+ );
109
+
110
+ return Response.json({ ok: true });
111
+ }
112
+
89
113
  if (type === "complete") {
90
114
  const { installedVersion, success, rolledBackToVersion } = body as {
91
115
  installedVersion?: unknown;
@@ -142,7 +166,7 @@ export function upgradeBroadcastRouteDefinitions(): RouteDefinition[] {
142
166
 
143
167
  return httpError(
144
168
  "BAD_REQUEST",
145
- 'type must be "starting" or "complete"',
169
+ 'type must be "starting", "progress", or "complete"',
146
170
  400,
147
171
  );
148
172
  },
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Workspace commit endpoint — creates a git commit in the workspace
3
+ * directory with all pending changes.
4
+ *
5
+ * Protected by a route policy restricting access to gateway service
6
+ * principals only (`svc_gateway` with `internal.write` scope), following
7
+ * the same pattern as other gateway-forwarded control-plane endpoints.
8
+ */
9
+
10
+ import { getWorkspaceDir } from "../../util/platform.js";
11
+ import { getWorkspaceGitService } from "../../workspace/git-service.js";
12
+ import { httpError } from "../http-errors.js";
13
+ import type { RouteDefinition } from "../http-router.js";
14
+
15
+ export function workspaceCommitRouteDefinitions(): RouteDefinition[] {
16
+ return [
17
+ {
18
+ endpoint: "admin/workspace-commit",
19
+ method: "POST",
20
+ handler: async ({ req }) => {
21
+ let body: unknown;
22
+ try {
23
+ body = await req.json();
24
+ } catch {
25
+ return httpError("BAD_REQUEST", "Invalid JSON body", 400);
26
+ }
27
+
28
+ if (!body || typeof body !== "object") {
29
+ return httpError(
30
+ "BAD_REQUEST",
31
+ "Request body must be a JSON object",
32
+ 400,
33
+ );
34
+ }
35
+
36
+ const { message } = body as { message?: unknown };
37
+
38
+ if (typeof message !== "string" || message.length === 0) {
39
+ return httpError(
40
+ "BAD_REQUEST",
41
+ "message is required and must be a non-empty string",
42
+ 400,
43
+ );
44
+ }
45
+
46
+ try {
47
+ await getWorkspaceGitService(getWorkspaceDir()).commitChanges(
48
+ message,
49
+ );
50
+ return Response.json({ ok: true });
51
+ } catch (err) {
52
+ const detail = err instanceof Error ? err.message : "Unknown error";
53
+ return httpError(
54
+ "INTERNAL_ERROR",
55
+ `Workspace commit failed: ${detail}`,
56
+ 500,
57
+ );
58
+ }
59
+ },
60
+ },
61
+ ];
62
+ }
@@ -190,9 +190,30 @@ describe("isTextMimeType", () => {
190
190
  expect(isTextMimeType("video/mp4")).toBe(false);
191
191
  });
192
192
 
193
- test("application/octet-stream is not text", () => {
193
+ test("application/octet-stream is not text without filename", () => {
194
194
  expect(isTextMimeType("application/octet-stream")).toBe(false);
195
195
  });
196
+
197
+ test("application/octet-stream with .py filename is text", () => {
198
+ expect(isTextMimeType("application/octet-stream", "script.py")).toBe(true);
199
+ });
200
+
201
+ test("application/octet-stream with .go filename is text", () => {
202
+ expect(isTextMimeType("application/octet-stream", "main.go")).toBe(true);
203
+ });
204
+
205
+ test("application/octet-stream with .rs filename is text", () => {
206
+ expect(isTextMimeType("application/octet-stream", "lib.rs")).toBe(true);
207
+ });
208
+
209
+ test("application/octet-stream with unknown extension is not text", () => {
210
+ expect(isTextMimeType("application/octet-stream", "data.bin")).toBe(false);
211
+ });
212
+
213
+ test("extension fallback only applies to application/octet-stream", () => {
214
+ // A binary plist has a specific MIME type — extension should not override it
215
+ expect(isTextMimeType("application/x-plist", "Info.plist")).toBe(false);
216
+ });
196
217
  });
197
218
 
198
219
  // ===========================================================================
@@ -119,7 +119,7 @@ function handleWorkspaceFile(ctx: RouteContext): Response {
119
119
  }
120
120
 
121
121
  const mimeType = Bun.file(resolved).type;
122
- const isText = isTextMimeType(mimeType);
122
+ const isText = isTextMimeType(mimeType, basename(resolved));
123
123
  const isBinary = !isText;
124
124
 
125
125
  let content: string | undefined = undefined;
@@ -85,10 +85,94 @@ const TEXT_MIME_PREFIXES = [
85
85
  "application/x-yaml",
86
86
  "application/toml",
87
87
  "application/x-sh",
88
+ "application/x-httpd-php",
89
+ "application/x-perl",
90
+ "application/x-sql",
91
+ "application/x-tex",
92
+ "application/vnd.dart",
88
93
  ];
89
94
 
90
- export function isTextMimeType(mimeType: string): boolean {
91
- return TEXT_MIME_PREFIXES.some((prefix) => mimeType.startsWith(prefix));
95
+ /**
96
+ * File extensions that are known text/code files but that Bun's MIME
97
+ * detection reports as `application/octet-stream`.
98
+ */
99
+ const TEXT_FILE_EXTENSIONS = new Set([
100
+ // Programming languages
101
+ "py",
102
+ "rb",
103
+ "go",
104
+ "rs",
105
+ "swift",
106
+ "kt",
107
+ "kts",
108
+ "cs",
109
+ "scala",
110
+ "ex",
111
+ "exs",
112
+ "erl",
113
+ "hs",
114
+ "clj",
115
+ "cljs",
116
+ "jl",
117
+ "zig",
118
+ "nim",
119
+ "v",
120
+ "sol",
121
+ "r",
122
+ "java",
123
+ "lua",
124
+ // Shell / scripting
125
+ "bash",
126
+ "zsh",
127
+ "fish",
128
+ "ps1",
129
+ "bat",
130
+ "cmd",
131
+ "awk",
132
+ // Web frameworks
133
+ "vue",
134
+ "svelte",
135
+ "scss",
136
+ "sass",
137
+ "less",
138
+ // Config / data
139
+ "cfg",
140
+ "conf",
141
+ "ini",
142
+ "properties",
143
+ "env",
144
+ "gradle",
145
+ "cmake",
146
+ // Markup / docs
147
+ "rst",
148
+ "adoc",
149
+ "org",
150
+ "tex",
151
+ "latex",
152
+ // Other text formats
153
+ "graphql",
154
+ "gql",
155
+ "proto",
156
+ "tf",
157
+ "hcl",
158
+ "diff",
159
+ "patch",
160
+ "log",
161
+ "lock",
162
+ ]);
163
+
164
+ export function isTextMimeType(mimeType: string, fileName?: string): boolean {
165
+ if (TEXT_MIME_PREFIXES.some((prefix) => mimeType.startsWith(prefix))) {
166
+ return true;
167
+ }
168
+ // Only fall back to extension check when the MIME type is genuinely unknown.
169
+ // Specific MIME types (e.g. application/x-plist for binary plists) should be
170
+ // trusted over the extension — overriding them risks corrupting binary files.
171
+ if (fileName && mimeType === "application/octet-stream") {
172
+ const ext = fileName.split(".").pop()?.toLowerCase();
173
+ if (ext && TEXT_FILE_EXTENSIONS.has(ext)) return true;
174
+ }
175
+ return false;
92
176
  }
93
177
 
94
178
  export const MAX_INLINE_TEXT_SIZE = 2 * 1024 * 1024; // 2 MB
@@ -16,11 +16,17 @@
16
16
  */
17
17
 
18
18
  import { getLogger } from "../util/logger.js";
19
- import type { CredentialBackend, DeleteResult } from "./credential-backend.js";
19
+ import type {
20
+ CredentialBackend,
21
+ CredentialGetResult,
22
+ DeleteResult,
23
+ } from "./credential-backend.js";
20
24
 
21
25
  const log = getLogger("ces-credential-client");
22
26
 
23
27
  const REQUEST_TIMEOUT_MS = 10_000;
28
+ const SET_MAX_RETRIES = 3;
29
+ const SET_RETRY_DELAY_MS = 2_000;
24
30
 
25
31
  // ---------------------------------------------------------------------------
26
32
  // Env helpers
@@ -77,49 +83,80 @@ export class CesCredentialBackend implements CredentialBackend {
77
83
  return !!getBaseUrl() && !!getServiceToken();
78
84
  }
79
85
 
80
- async get(account: string): Promise<string | undefined> {
86
+ async get(account: string): Promise<CredentialGetResult> {
81
87
  try {
82
88
  const res = await cesRequest(
83
89
  "GET",
84
90
  `/v1/credentials/${encodeURIComponent(account)}`,
85
91
  );
86
- if (!res) return undefined;
87
- if (res.status === 404) return undefined;
92
+ if (!res) return { value: undefined, unreachable: true };
93
+ if (res.status === 404) return { value: undefined, unreachable: false };
88
94
  if (!res.ok) {
89
95
  log.warn(
90
96
  { account, status: res.status },
91
97
  "CES credential get returned non-OK status",
92
98
  );
93
- return undefined;
99
+ return { value: undefined, unreachable: true };
94
100
  }
95
101
  const data = (await res.json()) as { value?: string };
96
- return data.value;
102
+ return { value: data.value, unreachable: false };
97
103
  } catch (err) {
98
104
  log.warn({ err, account }, "CES credential get threw unexpectedly");
99
- return undefined;
105
+ return { value: undefined, unreachable: true };
100
106
  }
101
107
  }
102
108
 
103
109
  async set(account: string, value: string): Promise<boolean> {
104
- try {
105
- const res = await cesRequest(
106
- "POST",
107
- `/v1/credentials/${encodeURIComponent(account)}`,
108
- { value },
109
- );
110
- if (!res) return false;
111
- if (!res.ok) {
112
- log.warn(
113
- { account, status: res.status },
114
- "CES credential set returned non-OK status",
110
+ for (let attempt = 0; attempt < SET_MAX_RETRIES; attempt++) {
111
+ try {
112
+ const res = await cesRequest(
113
+ "POST",
114
+ `/v1/credentials/${encodeURIComponent(account)}`,
115
+ { value },
115
116
  );
117
+ if (!res) {
118
+ // CES not reachable or env vars missing — retry in case sidecar
119
+ // is still starting up after pod creation.
120
+ if (attempt < SET_MAX_RETRIES - 1) {
121
+ log.warn(
122
+ { account, attempt },
123
+ "CES credential set got no response, retrying",
124
+ );
125
+ await new Promise((r) => setTimeout(r, SET_RETRY_DELAY_MS));
126
+ continue;
127
+ }
128
+ return false;
129
+ }
130
+ if (!res.ok) {
131
+ if (attempt < SET_MAX_RETRIES - 1) {
132
+ log.warn(
133
+ { account, status: res.status, attempt },
134
+ "CES credential set returned non-OK status, retrying",
135
+ );
136
+ await new Promise((r) => setTimeout(r, SET_RETRY_DELAY_MS));
137
+ continue;
138
+ }
139
+ log.warn(
140
+ { account, status: res.status },
141
+ "CES credential set returned non-OK status",
142
+ );
143
+ return false;
144
+ }
145
+ return true;
146
+ } catch (err) {
147
+ if (attempt < SET_MAX_RETRIES - 1) {
148
+ log.warn(
149
+ { err, account, attempt },
150
+ "CES credential set threw, retrying",
151
+ );
152
+ await new Promise((r) => setTimeout(r, SET_RETRY_DELAY_MS));
153
+ continue;
154
+ }
155
+ log.warn({ err, account }, "CES credential set threw unexpectedly");
116
156
  return false;
117
157
  }
118
- return true;
119
- } catch (err) {
120
- log.warn({ err, account }, "CES credential set threw unexpectedly");
121
- return false;
122
158
  }
159
+ return false;
123
160
  }
124
161
 
125
162
  async delete(account: string): Promise<DeleteResult> {
@@ -0,0 +1,85 @@
1
+ /**
2
+ * CesRpcCredentialBackend — a CredentialBackend that delegates all credential
3
+ * operations to the Credential Execution Service (CES) via stdio RPC.
4
+ *
5
+ * Maps RPC responses to the existing CredentialGetResult and DeleteResult
6
+ * types. Errors are caught and mapped to unreachable/error states for
7
+ * graceful fallback.
8
+ */
9
+
10
+ import { CesRpcMethod } from "@vellumai/ces-contracts";
11
+
12
+ import type { CesClient } from "../credential-execution/client.js";
13
+ import { getLogger } from "../util/logger.js";
14
+ import type {
15
+ CredentialBackend,
16
+ CredentialGetResult,
17
+ DeleteResult,
18
+ } from "./credential-backend.js";
19
+
20
+ const log = getLogger("ces-rpc-credential-backend");
21
+
22
+ export class CesRpcCredentialBackend implements CredentialBackend {
23
+ readonly name = "ces-rpc";
24
+
25
+ constructor(private readonly client: CesClient) {}
26
+
27
+ isAvailable(): boolean {
28
+ return this.client.isReady();
29
+ }
30
+
31
+ async get(account: string): Promise<CredentialGetResult> {
32
+ try {
33
+ const result = await this.client.call(
34
+ CesRpcMethod.GetCredential,
35
+ { account },
36
+ );
37
+ return {
38
+ value: result.found ? result.value : undefined,
39
+ unreachable: false,
40
+ };
41
+ } catch (err) {
42
+ log.warn({ err, account }, "CES RPC credential get failed");
43
+ return { value: undefined, unreachable: true };
44
+ }
45
+ }
46
+
47
+ async set(account: string, value: string): Promise<boolean> {
48
+ try {
49
+ const result = await this.client.call(
50
+ CesRpcMethod.SetCredential,
51
+ { account, value },
52
+ );
53
+ return result.ok;
54
+ } catch (err) {
55
+ log.warn({ err, account }, "CES RPC credential set failed");
56
+ return false;
57
+ }
58
+ }
59
+
60
+ async delete(account: string): Promise<DeleteResult> {
61
+ try {
62
+ const result = await this.client.call(
63
+ CesRpcMethod.DeleteCredential,
64
+ { account },
65
+ );
66
+ return result.result;
67
+ } catch (err) {
68
+ log.warn({ err, account }, "CES RPC credential delete failed");
69
+ return "error";
70
+ }
71
+ }
72
+
73
+ async list(): Promise<string[]> {
74
+ try {
75
+ const result = await this.client.call(
76
+ CesRpcMethod.ListCredentials,
77
+ {},
78
+ );
79
+ return result.accounts;
80
+ } catch (err) {
81
+ log.warn({ err }, "CES RPC credential list failed");
82
+ return [];
83
+ }
84
+ }
85
+ }
@@ -1,15 +1,10 @@
1
1
  /**
2
2
  * CredentialBackend interface and adapters — abstracts credential storage
3
3
  * behind a unified async API so callers don't need to know which backend
4
- * (macOS Keychain, encrypted file store, etc.) is in use.
4
+ * is in use.
5
5
  */
6
6
 
7
- import { getLogger } from "../util/logger.js";
8
7
  import * as encryptedStore from "./encrypted-store.js";
9
- import type { KeychainBrokerClient } from "./keychain-broker-client.js";
10
- import { createBrokerClient } from "./keychain-broker-client.js";
11
-
12
- const log = getLogger("credential-backend");
13
8
 
14
9
  // ---------------------------------------------------------------------------
15
10
  // Types
@@ -18,6 +13,12 @@ const log = getLogger("credential-backend");
18
13
  /** Result of a delete operation — distinguishes success, not-found, and error. */
19
14
  export type DeleteResult = "deleted" | "not-found" | "error";
20
15
 
16
+ /** Result of a get operation — distinguishes unreachable from not-found. */
17
+ export interface CredentialGetResult {
18
+ value: string | undefined;
19
+ unreachable: boolean;
20
+ }
21
+
21
22
  // ---------------------------------------------------------------------------
22
23
  // Interface
23
24
  // ---------------------------------------------------------------------------
@@ -29,8 +30,8 @@ export interface CredentialBackend {
29
30
  /** Whether this backend is currently reachable. Sync and cheap. */
30
31
  isAvailable(): boolean;
31
32
 
32
- /** Retrieve a secret. Returns undefined if not found or on error. */
33
- get(account: string): Promise<string | undefined>;
33
+ /** Retrieve a secret. Returns a result distinguishing unreachable from not-found. */
34
+ get(account: string): Promise<CredentialGetResult>;
34
35
 
35
36
  /** Store a secret. Returns true on success. */
36
37
  set(account: string, value: string): Promise<boolean>;
@@ -42,79 +43,6 @@ export interface CredentialBackend {
42
43
  list(): Promise<string[]>;
43
44
  }
44
45
 
45
- // ---------------------------------------------------------------------------
46
- // KeychainBackend
47
- // ---------------------------------------------------------------------------
48
-
49
- export class KeychainBackend implements CredentialBackend {
50
- readonly name = "keychain";
51
-
52
- constructor(private readonly client: KeychainBrokerClient) {}
53
-
54
- isAvailable(): boolean {
55
- return this.client.isAvailable();
56
- }
57
-
58
- async get(account: string): Promise<string | undefined> {
59
- try {
60
- const result = await this.client.get(account);
61
- if (result == null) {
62
- log.warn(
63
- { account },
64
- "Keychain broker unreachable during get — falling back",
65
- );
66
- return undefined;
67
- }
68
- if (!result.found) return undefined;
69
- return result.value;
70
- } catch (err) {
71
- log.warn({ err, account }, "Keychain get threw unexpectedly");
72
- return undefined;
73
- }
74
- }
75
-
76
- async set(account: string, value: string): Promise<boolean> {
77
- try {
78
- const result = await this.client.set(account, value);
79
- if (result.status === "ok") return true;
80
- log.warn(
81
- {
82
- account,
83
- status: result.status,
84
- ...(result.status === "rejected"
85
- ? { code: result.code, message: result.message }
86
- : {}),
87
- },
88
- "Keychain broker set failed",
89
- );
90
- return false;
91
- } catch (err) {
92
- log.warn({ err, account }, "Keychain set threw unexpectedly");
93
- return false;
94
- }
95
- }
96
-
97
- async delete(account: string): Promise<DeleteResult> {
98
- try {
99
- const ok = await this.client.del(account);
100
- // The keychain broker returns a boolean — it does not distinguish
101
- // "not found" from a genuine error, so we map false → "error".
102
- return ok ? "deleted" : "error";
103
- } catch {
104
- return "error";
105
- }
106
- }
107
-
108
- async list(): Promise<string[]> {
109
- try {
110
- return await this.client.list();
111
- } catch (err) {
112
- log.warn({ err }, "Keychain list threw unexpectedly");
113
- return [];
114
- }
115
- }
116
- }
117
-
118
46
  // ---------------------------------------------------------------------------
119
47
  // EncryptedStoreBackend
120
48
  // ---------------------------------------------------------------------------
@@ -126,11 +54,11 @@ export class EncryptedStoreBackend implements CredentialBackend {
126
54
  return true;
127
55
  }
128
56
 
129
- async get(account: string): Promise<string | undefined> {
57
+ async get(account: string): Promise<CredentialGetResult> {
130
58
  try {
131
- return encryptedStore.getKey(account);
59
+ return { value: encryptedStore.getKey(account), unreachable: false };
132
60
  } catch {
133
- return undefined;
61
+ return { value: undefined, unreachable: false };
134
62
  }
135
63
  }
136
64
 
@@ -163,10 +91,6 @@ export class EncryptedStoreBackend implements CredentialBackend {
163
91
  // Factory functions
164
92
  // ---------------------------------------------------------------------------
165
93
 
166
- export function createKeychainBackend(): KeychainBackend {
167
- return new KeychainBackend(createBrokerClient());
168
- }
169
-
170
94
  export function createEncryptedStoreBackend(): EncryptedStoreBackend {
171
95
  return new EncryptedStoreBackend();
172
96
  }