@mycadet/assistant 0.12.10 → 0.13.0

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 (180) hide show
  1. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  2. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  3. package/node_modules/@mycadet/ces-client/src/http-credentials.ts +18 -1
  4. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  5. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  6. package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
  7. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  8. package/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  9. package/openapi.yaml +2 -708
  10. package/package.json +2 -3
  11. package/scripts/sync-web-search-catalog.ts +1 -2
  12. package/src/__tests__/cloud-profile-guard.test.ts +41 -5
  13. package/src/__tests__/config-schema.test.ts +6 -38
  14. package/src/__tests__/conversation-queue.test.ts +45 -6
  15. package/src/__tests__/db-readiness-http-gate.test.ts +0 -6
  16. package/src/__tests__/persist-onboarding-artifacts.test.ts +0 -1
  17. package/src/__tests__/persona-resolver.test.ts +0 -51
  18. package/src/__tests__/provider-cloud-proxy-integration.test.ts +2 -110
  19. package/src/__tests__/run-due-schedules.test.ts +0 -4
  20. package/src/__tests__/schedule-store.test.ts +0 -12
  21. package/src/__tests__/stt-stream-session.test.ts +4 -4
  22. package/src/__tests__/telephony-tts-guard.test.ts +2 -50
  23. package/src/__tests__/voice-config-update.test.ts +12 -167
  24. package/src/__tests__/web-search-catalog-parity.test.ts +0 -2
  25. package/src/__tests__/workspace-migration-130-speech-mode-to-provider.test.ts +9 -7
  26. package/src/__tests__/workspace-migration-132-web-search-mode-to-provider.test.ts +6 -6
  27. package/src/calls/__tests__/voice-session-bridge.test.ts +1 -4
  28. package/src/calls/resolve-call-tts-provider.ts +6 -25
  29. package/src/calls/telephony-credential-preflight.ts +0 -10
  30. package/src/calls/telephony-tts-capability.ts +1 -17
  31. package/src/cli/commands/tts.ts +1 -2
  32. package/src/cli/index.help.ts +0 -2
  33. package/src/cli/program.ts +0 -2
  34. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +4 -42
  35. package/src/config/schemas/__tests__/stt.test.ts +10 -14
  36. package/src/config/schemas/llm.ts +3 -5
  37. package/src/config/schemas/services.ts +5 -6
  38. package/src/config/schemas/stt.ts +1 -3
  39. package/src/config/schemas/tts.ts +1 -30
  40. package/src/daemon/daemon-readiness.ts +4 -29
  41. package/src/daemon/lifecycle.ts +0 -31
  42. package/src/daemon/providers-setup.ts +0 -8
  43. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  44. package/src/heartbeat/heartbeat-service.ts +1 -66
  45. package/src/ipc/__tests__/db-readiness-ipc-gate.test.ts +1 -9
  46. package/src/live-voice/__tests__/live-voice-credential-preflight.test.ts +0 -44
  47. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +4 -6
  48. package/src/live-voice/__tests__/live-voice-stt.test.ts +1 -1
  49. package/src/live-voice/live-voice-credential-preflight.ts +9 -33
  50. package/src/live-voice/live-voice-session.ts +10 -6
  51. package/src/live-voice/live-voice-tts.ts +1 -8
  52. package/src/plugins/defaults/memory/worker.ts +3 -3
  53. package/src/prompts/persona-resolver.ts +0 -35
  54. package/src/providers/__tests__/search-provider-catalog.test.ts +5 -8
  55. package/src/providers/__tests__/voice-error-copy.test.ts +9 -10
  56. package/src/providers/search-provider-catalog.ts +7 -39
  57. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +3 -47
  58. package/src/providers/speech-to-text/__tests__/resolve.test.ts +6 -372
  59. package/src/providers/speech-to-text/provider-catalog.ts +2 -73
  60. package/src/providers/speech-to-text/resolve.ts +3 -120
  61. package/src/runtime/http-router-types.ts +1 -3
  62. package/src/runtime/http-server.ts +1 -1
  63. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +13 -11
  64. package/src/runtime/routes/__tests__/tts-routes.test.ts +2 -6
  65. package/src/runtime/routes/conversation-query-routes.ts +0 -96
  66. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +7 -7
  67. package/src/runtime/routes/index.ts +0 -4
  68. package/src/runtime/routes/live-voice-routes.test.ts +4 -11
  69. package/src/runtime/routes/live-voice-routes.ts +8 -11
  70. package/src/runtime/routes/secret-routes.ts +0 -6
  71. package/src/runtime/routes/tts-routes.ts +0 -38
  72. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +1 -7
  73. package/src/schedule/__tests__/worker-mcp-readiness.test.ts +0 -4
  74. package/src/schedule/schedule-store.ts +0 -7
  75. package/src/schedule/scheduler.ts +0 -5
  76. package/src/schedule/worker.ts +1 -1
  77. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +2 -10
  78. package/src/stt/daemon-batch-transcriber.ts +4 -38
  79. package/src/stt/types.ts +1 -9
  80. package/src/tools/network/__tests__/web-search.test.ts +3 -250
  81. package/src/tools/network/web-search.ts +4 -165
  82. package/src/tts/__tests__/provider-resolution.test.ts +0 -5
  83. package/src/tts/provider-catalog.ts +0 -4
  84. package/src/tts/types.ts +0 -1
  85. package/src/__tests__/cloud-speech-defaults.test.ts +0 -330
  86. package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1708
  87. package/src/__tests__/migration-export-http.test.ts +0 -557
  88. package/src/__tests__/migration-export-streaming.test.ts +0 -350
  89. package/src/__tests__/migration-export-to-gcs.test.ts +0 -706
  90. package/src/__tests__/migration-import-commit-http.test.ts +0 -1176
  91. package/src/__tests__/migration-import-from-gcs.test.ts +0 -534
  92. package/src/__tests__/migration-import-from-path.test.ts +0 -324
  93. package/src/__tests__/migration-import-from-url.test.ts +0 -588
  94. package/src/__tests__/migration-import-preflight-http.test.ts +0 -739
  95. package/src/__tests__/migration-jobs-status.test.ts +0 -139
  96. package/src/__tests__/migration-parity-persistence.test.ts +0 -1891
  97. package/src/__tests__/migration-transport.test.ts +0 -1089
  98. package/src/__tests__/migration-validate-http.test.ts +0 -634
  99. package/src/__tests__/migration-wizard.test.ts +0 -1395
  100. package/src/__tests__/rebind-secrets-screen.test.ts +0 -876
  101. package/src/__tests__/sanitize-config-for-transfer.test.ts +0 -259
  102. package/src/__tests__/transfer-progress-screen.test.ts +0 -1217
  103. package/src/__tests__/validation-results-screen.test.ts +0 -1130
  104. package/src/__tests__/vbundle-pax-and-symlink.test.ts +0 -147
  105. package/src/background-wake/background-wake-routes.test.ts +0 -906
  106. package/src/background-wake/cloud-client.test.ts +0 -328
  107. package/src/background-wake/cloud-client.ts +0 -196
  108. package/src/background-wake/next-wake.test.ts +0 -335
  109. package/src/background-wake/next-wake.ts +0 -188
  110. package/src/background-wake/publisher.ts +0 -93
  111. package/src/background-wake/wake-intent-hooks.test.ts +0 -234
  112. package/src/cli/commands/__tests__/backup.test.ts +0 -363
  113. package/src/cli/commands/backup.help.ts +0 -180
  114. package/src/cli/commands/backup.ts +0 -398
  115. package/src/cloud/cloud-speech.test.ts +0 -335
  116. package/src/cloud/cloud-speech.ts +0 -300
  117. package/src/config/cloud-speech-defaults.ts +0 -300
  118. package/src/config/sanitize-for-transfer.ts +0 -111
  119. package/src/live-voice/__tests__/live-voice-cloud-speech-fallback.test.ts +0 -250
  120. package/src/providers/speech-to-text/__tests__/cadet-cloud-realtime.test.ts +0 -457
  121. package/src/providers/speech-to-text/__tests__/cadet-cloud.test.ts +0 -271
  122. package/src/providers/speech-to-text/__tests__/cadet-speech-relay-connection.test.ts +0 -126
  123. package/src/providers/speech-to-text/cadet-cloud-flux-realtime.ts +0 -309
  124. package/src/providers/speech-to-text/cadet-cloud-realtime.ts +0 -315
  125. package/src/providers/speech-to-text/cadet-cloud.ts +0 -108
  126. package/src/providers/speech-to-text/cadet-speech-relay-connection.ts +0 -176
  127. package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +0 -254
  128. package/src/runtime/migrations/__tests__/job-registry.test.ts +0 -346
  129. package/src/runtime/migrations/__tests__/origin-mode.test.ts +0 -62
  130. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +0 -184
  131. package/src/runtime/migrations/__tests__/staged-import-path.test.ts +0 -110
  132. package/src/runtime/migrations/__tests__/v1-test-helpers.ts +0 -112
  133. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +0 -285
  134. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +0 -126
  135. package/src/runtime/migrations/__tests__/vbundle-builder-v1-shape.test.ts +0 -253
  136. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +0 -208
  137. package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +0 -485
  138. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +0 -349
  139. package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +0 -196
  140. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +0 -551
  141. package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +0 -428
  142. package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +0 -220
  143. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +0 -1823
  144. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +0 -598
  145. package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +0 -463
  146. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -538
  147. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +0 -515
  148. package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +0 -453
  149. package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +0 -319
  150. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +0 -240
  151. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +0 -473
  152. package/src/runtime/migrations/gcs-signed-url.ts +0 -190
  153. package/src/runtime/migrations/job-registry.ts +0 -297
  154. package/src/runtime/migrations/migration-transport.ts +0 -612
  155. package/src/runtime/migrations/migration-wizard.ts +0 -747
  156. package/src/runtime/migrations/origin-mode.ts +0 -42
  157. package/src/runtime/migrations/rebind-secrets-screen.ts +0 -412
  158. package/src/runtime/migrations/staged-import-path.ts +0 -116
  159. package/src/runtime/migrations/transfer-progress-screen.ts +0 -323
  160. package/src/runtime/migrations/validation-results-screen.ts +0 -469
  161. package/src/runtime/migrations/vbundle-builder.ts +0 -1382
  162. package/src/runtime/migrations/vbundle-import-analyzer.ts +0 -405
  163. package/src/runtime/migrations/vbundle-import-policy.ts +0 -241
  164. package/src/runtime/migrations/vbundle-importer.ts +0 -826
  165. package/src/runtime/migrations/vbundle-metadata-merge.ts +0 -113
  166. package/src/runtime/migrations/vbundle-streaming-importer.ts +0 -2844
  167. package/src/runtime/migrations/vbundle-streaming-validator.ts +0 -426
  168. package/src/runtime/migrations/vbundle-tar-stream.ts +0 -244
  169. package/src/runtime/migrations/vbundle-validator.ts +0 -724
  170. package/src/runtime/routes/__tests__/migration-cadet-metadata-reconcile.test.ts +0 -227
  171. package/src/runtime/routes/__tests__/migration-export-secrets-redacted.test.ts +0 -54
  172. package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +0 -70
  173. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +0 -257
  174. package/src/runtime/routes/background-wake-routes.ts +0 -387
  175. package/src/runtime/routes/migration-routes.ts +0 -2584
  176. package/src/tools/network/__tests__/cloud-search-proxy.test.ts +0 -282
  177. package/src/tools/network/cloud-search-proxy.ts +0 -211
  178. package/src/tts/__tests__/cadet-provider.test.ts +0 -253
  179. package/src/tts/providers/cadet-provider.ts +0 -244
  180. package/src/types/tar-stream.d.ts +0 -66
@@ -1,300 +0,0 @@
1
- /**
2
- * Managed-speech defaulting.
3
- *
4
- * When the cloud connection is usable and a speech service has no working
5
- * BYOK credential, that service's effective provider is `"cadet"` so a fresh
6
- * Cadet connection gets voice features with zero configuration. A service
7
- * whose BYOK credential is configured is never redirected — connecting Cadet
8
- * must not silently reroute an existing voice setup — and a service already on
9
- * Cadet is left alone.
10
- *
11
- * {@link resolveEffectiveSpeechProviders} is the single definition of that
12
- * rule. It writes nothing, so runtime paths that only need to know which
13
- * provider they will actually use (live-voice readiness, transcription,
14
- * synthesis) resolve the same ids the scope-gated writer
15
- * {@link maybeDefaultSpeechToCloud} would persist, without needing write
16
- * access to the config.
17
- */
18
-
19
- import { ttsSecretResolves } from "../calls/telephony-tts-capability.js";
20
- import { managedSpeechAvailable } from "../cloud/cloud-speech.js";
21
- import { fluxModelForLanguage } from "../providers/speech-to-text/deepgram-flux-frames.js";
22
- import {
23
- baseModelFamilyFor,
24
- getProviderEntry,
25
- isCloudSttProvider,
26
- resolveSttCatalogKey,
27
- sttConfigForCatalogKey,
28
- supportsProviderTurnDetection,
29
- } from "../providers/speech-to-text/provider-catalog.js";
30
- import { sttProviderKeyResolves } from "../providers/speech-to-text/resolve.js";
31
- import {
32
- STT_ROLES,
33
- sttCatalogKeyForRole,
34
- type SttRole,
35
- sttRoleCapabilityGap,
36
- sttSelectionForRole,
37
- } from "../stt/roles.js";
38
- import type { SttProviderId } from "../stt/types.js";
39
- import { getCatalogProvider } from "../tts/provider-catalog.js";
40
- import type { TtsProviderId } from "../tts/types.js";
41
- import { getLogger } from "../util/logger.js";
42
- import {
43
- getConfig,
44
- invalidateConfigCache,
45
- loadRawConfig,
46
- saveRawConfig,
47
- setNestedValue,
48
- } from "./loader.js";
49
- import type { AssistantConfig } from "./types.js";
50
-
51
- const log = getLogger("cloud-speech-defaults");
52
-
53
- /** Whether the configured BYOK STT provider has a usable credential. */
54
- async function sttByokCredentialResolves(provider: string): Promise<boolean> {
55
- const entry = getProviderEntry(provider as SttProviderId);
56
- if (!entry) {
57
- return false;
58
- }
59
- return sttProviderKeyResolves(entry.credentialProvider);
60
- }
61
-
62
- /** Whether the configured BYOK TTS provider has all its secrets. */
63
- async function ttsByokCredentialsResolve(provider: string): Promise<boolean> {
64
- let entry;
65
- try {
66
- entry = getCatalogProvider(provider);
67
- } catch {
68
- return false;
69
- }
70
- for (const secret of entry.secretRequirements) {
71
- if (!(await ttsSecretResolves(secret.credentialStoreKey))) {
72
- return false;
73
- }
74
- }
75
- return true;
76
- }
77
-
78
- /**
79
- * The cloud provider that stands in for a configured one whose credential
80
- * did not resolve.
81
- *
82
- * Cloud speech has two STT entries, and they are not interchangeable:
83
- * `cadet-flux` decides end-of-turn itself, `cadet` leaves it to the
84
- * session's silence timer. Substituting the plain one for a provider that had
85
- * provider turn detection would keep live voice working while quietly
86
- * changing how it takes turns, which reads as "Flux is no better" rather than
87
- * as a missing credential. Match the capability instead.
88
- */
89
- function managedStandInFor(
90
- configured: SttProviderId,
91
- language: string | undefined,
92
- ): SttProviderId {
93
- if (!supportsProviderTurnDetection(configured)) {
94
- return "cadet";
95
- }
96
- // Flux has a model for ten languages. Outside them the relay rejects the
97
- // dial outright, so standing in with it would hand the speaker a mic that
98
- // does not work at all rather than one that detects turns less well. The
99
- // language is the one the resolver will actually send.
100
- return fluxModelForLanguage(language) === null ? "cadet" : "cadet-flux";
101
- }
102
-
103
- /** The speech providers a runtime path uses, after cloud-speech defaulting. */
104
- export interface EffectiveSpeechProviders {
105
- stt: SttProviderId;
106
- tts: TtsProviderId;
107
- }
108
-
109
- /**
110
- * Resolve the speech providers the runtime actually uses.
111
- *
112
- * A configured service whose BYOK credential does not resolve is reported as
113
- * its managed stand-in while cloud speech is available (see
114
- * {@link managedStandInFor}, which preserves provider turn detection); every
115
- * other service keeps its configured provider. Read-only: callers that hold no `settings.write`
116
- * scope (the live-voice WebSocket transport) resolve the same verdict the
117
- * preflight route does without persisting anything.
118
- *
119
- * `config` selects the configuration to read the configured providers from,
120
- * for callers already holding one (defaults to the loaded config). `role`
121
- * selects which consumer's STT override applies; omitting it reads the global
122
- * provider.
123
- */
124
- export async function resolveEffectiveSpeechProviders(
125
- config?: AssistantConfig,
126
- options: { role?: SttRole } = {},
127
- ): Promise<EffectiveSpeechProviders> {
128
- const services = (config ?? getConfig()).services;
129
- const configuredStt = sttCatalogKeyForRole(services.stt, options.role);
130
- const configuredTts = services.tts.provider as TtsProviderId;
131
-
132
- if (!(await managedSpeechAvailable())) {
133
- return { stt: configuredStt, tts: configuredTts };
134
- }
135
-
136
- const stt =
137
- !isCloudSttProvider(configuredStt) &&
138
- !(await sttByokCredentialResolves(configuredStt))
139
- ? managedStandInFor(configuredStt, services.stt.language)
140
- : configuredStt;
141
-
142
- const tts =
143
- configuredTts !== "cadet" &&
144
- !(await ttsByokCredentialsResolve(configuredTts))
145
- ? "cadet"
146
- : configuredTts;
147
-
148
- return {
149
- stt: managedLiveVoiceModelFamily(stt, options.role, services.stt),
150
- tts,
151
- };
152
- }
153
-
154
- /**
155
- * Managed live voice runs Flux.
156
- *
157
- * Turn detection is the reason to reach for Flux at all, and managed users
158
- * cannot ask for it: the provider picker offers no model family, so `cadet`
159
- * is as specific as they can be. Deciding it here rather than persisting a
160
- * `services.stt.roles.liveVoice` entry keeps managed opinionated without
161
- * writing config on anyone's behalf, and reaches installs already on `cadet`
162
- * that a substitution rule never sees.
163
- *
164
- * Two things still win over it. A family the user named is honoured, because
165
- * `services.stt.providers.cadet.model` accepts `nova-3` and quietly ignoring
166
- * a valid setting is the silent substitution roles exist to prevent. And a
167
- * language outside Flux's roster stays on nova-3, since the relay refuses the
168
- * dial rather than degrading (see `managedStandInFor`).
169
- *
170
- * Live voice only: Flux streams and nothing else, so every other consumer
171
- * would lose its transcriber.
172
- */
173
- function managedLiveVoiceModelFamily(
174
- resolved: SttProviderId,
175
- role: SttRole | undefined,
176
- stt: AssistantConfig["services"]["stt"],
177
- ): SttProviderId {
178
- if (role !== "liveVoice" || resolved !== "cadet") {
179
- return resolved;
180
- }
181
- if (sttSelectionForRole(stt, role).model !== undefined) {
182
- return resolved;
183
- }
184
- return fluxModelForLanguage(stt.language) === null ? "cadet" : "cadet-flux";
185
- }
186
-
187
- /**
188
- * Whether a provider can serve every consumer, or only the one that chose it.
189
- *
190
- * `cadet` covers every boundary, so substituting it is safe everywhere. A
191
- * narrowing row like `cadet-flux` streams and nothing else, and the roles it
192
- * fails are exactly the consumers that must not be moved onto it.
193
- */
194
- function servesEveryRole(provider: SttProviderId): boolean {
195
- const selection = sttConfigForCatalogKey(provider);
196
- return STT_ROLES.every(
197
- (role) => sttRoleCapabilityGap(role, selection) === null,
198
- );
199
- }
200
-
201
- /**
202
- * The writes that put `selection` in force as the global provider.
203
- *
204
- * The family is always written, including the base one. Substituting away
205
- * from a variant leaves its `model` behind otherwise, and the next load
206
- * resolves straight back to the variant this substitution just rejected: the
207
- * provider would read as plain cadet while running Flux, with batch and
208
- * telephony quietly unavailable. A provider with no families has no name to
209
- * write, and no variant to have left a stale value behind either.
210
- */
211
- function globalSttUpdates(selection: {
212
- provider: string;
213
- model?: string | undefined;
214
- }): { path: string; provider: string }[] {
215
- const updates = [
216
- { path: "services.stt.provider", provider: selection.provider },
217
- ];
218
- const family =
219
- selection.model ?? baseModelFamilyFor(selection.provider as SttProviderId);
220
- if (family !== undefined) {
221
- updates.push({
222
- path: `services.stt.providers.${selection.provider}.model`,
223
- provider: family,
224
- });
225
- }
226
- return updates;
227
- }
228
-
229
- /**
230
- * Persist the effective speech providers resolved by
231
- * {@link resolveEffectiveSpeechProviders} whenever they differ from the
232
- * configured ones.
233
- *
234
- * Safe to call repeatedly (idempotent) and safe to fire-and-forget. Callers
235
- * must hold the `settings.write` scope — this is the only path that writes
236
- * `services.stt/tts.provider` on behalf of cloud-speech defaulting.
237
- */
238
- export async function maybeDefaultSpeechToCloud(): Promise<void> {
239
- try {
240
- const services = getConfig().services;
241
- const effective = await resolveEffectiveSpeechProviders();
242
-
243
- const updates: { path: string; provider: string }[] = [];
244
- if (effective.stt !== resolveSttCatalogKey(services.stt)) {
245
- // A variant row is not a valid services.stt.provider value, so persist
246
- // the pair that selects it. Writing the key itself would put an id the
247
- // schema rejects on disk, and an unparseable services block is how the
248
- // loader's salvage ladder ends up resetting the whole section.
249
- const standIn = sttConfigForCatalogKey(effective.stt);
250
- if (servesEveryRole(effective.stt)) {
251
- updates.push(...globalSttUpdates(standIn));
252
- } else {
253
- // A stand-in that cannot serve every consumer belongs to the one that
254
- // chose it. Live voice is that consumer: provider turn detection is
255
- // the only reason managedStandInFor reaches for a narrow row, and
256
- // writing that row globally would take batch transcription and
257
- // telephony from every other consumer in a single write.
258
- updates.push({
259
- path: "services.stt.roles.liveVoice.provider",
260
- provider: standIn.provider,
261
- });
262
- if (standIn.model !== undefined) {
263
- updates.push({
264
- path: "services.stt.roles.liveVoice.model",
265
- provider: standIn.model,
266
- });
267
- }
268
- // The other consumers still need a provider that answers, and the
269
- // narrow stand-in's own base is the cloud row that serves them.
270
- if (servesEveryRole(standIn.provider as SttProviderId)) {
271
- updates.push(...globalSttUpdates({ provider: standIn.provider }));
272
- }
273
- }
274
- }
275
- if (effective.tts !== services.tts.provider) {
276
- updates.push({ path: "services.tts.provider", provider: effective.tts });
277
- }
278
-
279
- if (updates.length === 0) {
280
- return;
281
- }
282
-
283
- const raw = loadRawConfig();
284
- for (const { path, provider } of updates) {
285
- setNestedValue(raw, path, provider);
286
- }
287
- saveRawConfig(raw);
288
- invalidateConfigCache();
289
- log.info(
290
- { defaulted: updates.map(({ path }) => path) },
291
- "Defaulted unconfigured speech services to the Cadet provider after connection",
292
- );
293
- } catch (err) {
294
- // Convenience defaulting must never break credential storage.
295
- log.warn(
296
- { error: err instanceof Error ? err.message : String(err) },
297
- "Cloud speech defaulting failed (non-fatal)",
298
- );
299
- }
300
- }
@@ -1,111 +0,0 @@
1
- import {
2
- INGRESS_ASSISTANT_ID_KEY,
3
- INGRESS_LAST_TUNNEL_KEY,
4
- INGRESS_PAIRING_TUNNEL_KEY,
5
- } from "@mycadet/service-contracts/ingress";
6
-
7
- /**
8
- * Strips environment-specific fields from config JSON before transferring
9
- * between local and platform environments (teleport/restore).
10
- *
11
- * Fields removed or reset:
12
- * - `ingress.publicBaseUrl` → set to `""`
13
- * - `ingress.enabled` → deleted
14
- * - `ingress.publicBaseUrlManagedBy` → deleted
15
- * - `ingress.lastTunnel` → deleted
16
- * - `ingress.pairingTunnel` → deleted
17
- * - `ingress.assistantId` → deleted
18
- * - `telegram.registeredWebhookUrl` → deleted
19
- * - `daemon` → deleted entirely
20
- * - `skills.load.extraDirs` → set to `[]`
21
- * - `hostBrowser.cdpInspect.desktopAuto` → deleted **only when the source
22
- * either relies on the schema default or explicitly sets
23
- * `enabled: true`**. An explicit `enabled: false` is preserved so a
24
- * platform→local teleport doesn't silently re-enable auto-attach
25
- * against the user's opt-out.
26
- *
27
- * `logFile.dir` is intentionally *not* stripped: the logger's container
28
- * fallback (`util/logger.ts#resolveLogDir`) already redirects to the
29
- * default log dir with a warning when the configured path can't be
30
- * created, and stripping `dir` would disable rotating file logging
31
- * entirely because `lifecycle.ts` gates `initLogger` on a truthy
32
- * `config.logFile.dir`.
33
- */
34
- export function sanitizeConfigForTransfer(configJson: string): string {
35
- let config: Record<string, unknown>;
36
- try {
37
- const parsed = JSON.parse(configJson);
38
- if (
39
- typeof parsed !== "object" ||
40
- parsed === null ||
41
- Array.isArray(parsed)
42
- ) {
43
- return configJson;
44
- }
45
- config = parsed;
46
- } catch {
47
- return configJson;
48
- }
49
-
50
- // Strip ingress environment-specific fields
51
- if (config.ingress && typeof config.ingress === "object") {
52
- const ingress = config.ingress as Record<string, unknown>;
53
- ingress.publicBaseUrl = "";
54
- delete ingress.enabled;
55
- delete ingress.publicBaseUrlManagedBy;
56
- // The remembered tunnels and the assistant they fronted belong to the
57
- // source host; carrying them over makes tunnel-status UIs name the old
58
- // deployment, and the pairing record would have the destination advertise
59
- // an address on the source's tailnet.
60
- delete ingress[INGRESS_LAST_TUNNEL_KEY];
61
- delete ingress[INGRESS_PAIRING_TUNNEL_KEY];
62
- delete ingress[INGRESS_ASSISTANT_ID_KEY];
63
- }
64
-
65
- // Strip the recorded Telegram webhook URL. It records where *this*
66
- // deployment pointed Telegram, so carrying it across a teleport would have
67
- // the destination compare its own registration against the source's address
68
- // and report a mismatch on a channel that is fine. Absent, the health sweep
69
- // reports unverified until the destination reconciles and records its own.
70
- if (config.telegram && typeof config.telegram === "object") {
71
- const telegram = config.telegram as Record<string, unknown>;
72
- delete telegram.registeredWebhookUrl;
73
- }
74
-
75
- // Strip daemon entirely
76
- delete config.daemon;
77
-
78
- // Strip skills.load.extraDirs
79
- if (config.skills && typeof config.skills === "object") {
80
- const skills = config.skills as Record<string, unknown>;
81
- if (skills.load && typeof skills.load === "object") {
82
- const load = skills.load as Record<string, unknown>;
83
- load.extraDirs = [];
84
- }
85
- }
86
-
87
- // Strip hostBrowser.cdpInspect.desktopAuto — the auto-attach-to-Chrome
88
- // behavior is gated on a macOS-originated turn; preserving a
89
- // source-host-derived `enabled: true` inside a Linux managed pod's
90
- // config is misleading and brittle. Preserve an explicit
91
- // `enabled: false` opt-out, though — the schema default is `true`,
92
- // so unconditionally stripping this subobject would re-enable
93
- // auto-attach after a cloud→local teleport.
94
- if (config.hostBrowser && typeof config.hostBrowser === "object") {
95
- const hostBrowser = config.hostBrowser as Record<string, unknown>;
96
- if (hostBrowser.cdpInspect && typeof hostBrowser.cdpInspect === "object") {
97
- const cdpInspect = hostBrowser.cdpInspect as Record<string, unknown>;
98
- const desktopAuto = cdpInspect.desktopAuto;
99
- const isExplicitOptOut =
100
- desktopAuto !== null &&
101
- typeof desktopAuto === "object" &&
102
- !Array.isArray(desktopAuto) &&
103
- (desktopAuto as Record<string, unknown>).enabled === false;
104
- if (!isExplicitOptOut) {
105
- delete cdpInspect.desktopAuto;
106
- }
107
- }
108
- }
109
-
110
- return JSON.stringify(config, null, 2) + "\n";
111
- }
@@ -1,250 +0,0 @@
1
- /**
2
- * Live voice on cloud speech without a config write.
3
- *
4
- * `/v1/live-voice` validates guardian identity only — it does not enforce the
5
- * caller's `settings.write` scope — so the WebSocket transport must never
6
- * persist `services.stt/tts.provider`. Instead every live-voice leg resolves
7
- * the EFFECTIVE providers: while cloud speech is available, a configured
8
- * BYOK provider whose credential does not resolve is served by `"cadet"`.
9
- * These tests pin both halves of that contract — the readiness verdict and
10
- * the runtime legs agree, and `config.json` is byte-identical afterwards.
11
- *
12
- * `mock.module` is process-global in Bun and leaks into sibling files that run
13
- * later in the same `bun test` invocation, so each stub delegates to the real
14
- * implementation unless this file's tests are active (`fallbackMocksActive`,
15
- * toggled in beforeAll/afterAll). Run this file on its own.
16
- */
17
-
18
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
19
- import { join } from "node:path";
20
- import {
21
- afterAll,
22
- afterEach,
23
- beforeAll,
24
- beforeEach,
25
- describe,
26
- expect,
27
- mock,
28
- test,
29
- } from "bun:test";
30
-
31
- import type { ResolveStreamingTranscriberOptions } from "../../providers/speech-to-text/resolve.js";
32
- import type { StreamingTranscriber } from "../../stt/types.js";
33
-
34
- let fallbackMocksActive = false;
35
-
36
- const realSttResolveModule = {
37
- ...(await import("../../providers/speech-to-text/resolve.js")),
38
- };
39
- const realSecureKeysModule = {
40
- ...(await import("../../security/secure-keys.js")),
41
- };
42
- const realCloudSpeechModule = {
43
- ...(await import("../../cloud/cloud-speech.js")),
44
- };
45
-
46
- // -- Mutable stub state -------------------------------------------------------
47
-
48
- /** Whether the cloud connection can serve cloud speech. */
49
- let managedAvailable = false;
50
- /** Credential-store keys that resolve to a value. */
51
- let providerKeys: Record<string, string>;
52
- /** Provider ids `resolveStreamingTranscriber` was asked for, in call order. */
53
- let requestedSttProviders: (string | undefined)[];
54
-
55
- /** A provider yields a streaming transcriber when its credential resolves. */
56
- function transcriberFor(providerId: string | undefined): boolean {
57
- // Both managed rows ride the same platform connection, so neither has a
58
- // credential of its own. Live voice asks for the flux row specifically,
59
- // since managed live voice resolves to it when no family is named.
60
- return providerId === "cadet" || providerId === "cadet-flux"
61
- ? managedAvailable
62
- : providerId !== undefined && providerKeys[providerId] !== undefined;
63
- }
64
-
65
- mock.module("../../providers/speech-to-text/resolve.js", () => ({
66
- ...realSttResolveModule,
67
- resolveStreamingTranscriber: async (
68
- options: ResolveStreamingTranscriberOptions = {},
69
- ) => {
70
- if (!fallbackMocksActive) {
71
- return realSttResolveModule.resolveStreamingTranscriber(options);
72
- }
73
- requestedSttProviders.push(options.providerId);
74
- return transcriberFor(options.providerId)
75
- ? ({} as StreamingTranscriber)
76
- : null;
77
- },
78
- }));
79
-
80
- mock.module("../../security/secure-keys.js", () => ({
81
- ...realSecureKeysModule,
82
- getProviderKeyAsync: async (provider: string) =>
83
- fallbackMocksActive
84
- ? providerKeys[provider]
85
- : realSecureKeysModule.getProviderKeyAsync(provider),
86
- getSecureKeyAsync: async (account: string) =>
87
- fallbackMocksActive
88
- ? providerKeys[account]
89
- : realSecureKeysModule.getSecureKeyAsync(account),
90
- }));
91
-
92
- mock.module("../../cloud/cloud-speech.js", () => ({
93
- ...realCloudSpeechModule,
94
- managedSpeechAvailable: async () =>
95
- fallbackMocksActive
96
- ? managedAvailable
97
- : realCloudSpeechModule.managedSpeechAvailable(),
98
- }));
99
-
100
- import { resolveEffectiveSpeechProviders } from "../../config/cloud-speech-defaults.js";
101
- import { getConfig, invalidateConfigCache } from "../../config/loader.js";
102
- import {
103
- _resetTtsProviderOverridesForTests,
104
- _setTtsProviderForTests,
105
- } from "../../tts/provider-catalog.js";
106
- import type {
107
- TtsProvider,
108
- TtsSynthesisRequest,
109
- TtsSynthesisResult,
110
- } from "../../tts/types.js";
111
- import { resolveLiveVoiceCredentialReadiness } from "../live-voice-credential-preflight.js";
112
- import { streamLiveVoiceTtsAudio } from "../live-voice-tts.js";
113
-
114
- const WORKSPACE_DIR = process.env.CADET_WORKSPACE_DIR!;
115
- const CONFIG_PATH = join(WORKSPACE_DIR, "config.json");
116
-
117
- /** Config with both speech services pointed at BYOK providers. */
118
- const BYOK_CONFIG = {
119
- services: {
120
- stt: { provider: "deepgram", providers: {} },
121
- tts: { provider: "elevenlabs", providers: {} },
122
- },
123
- };
124
-
125
- function writeConfig(obj: unknown): void {
126
- if (!existsSync(WORKSPACE_DIR)) {
127
- mkdirSync(WORKSPACE_DIR, { recursive: true });
128
- }
129
- writeFileSync(CONFIG_PATH, JSON.stringify(obj));
130
- invalidateConfigCache();
131
- }
132
-
133
- function readConfigFile(): string {
134
- return readFileSync(CONFIG_PATH, "utf8");
135
- }
136
-
137
- /** A streaming TTS adapter standing in for the cloud `cadet` provider. */
138
- function registerManagedTtsProvider(): TtsProvider {
139
- const provider: TtsProvider = {
140
- id: "cadet",
141
- capabilities: { supportsStreaming: true, supportedFormats: ["pcm", "mp3"] },
142
- async synthesize(): Promise<TtsSynthesisResult> {
143
- throw new Error("buffered synthesis should not be used");
144
- },
145
- async synthesizeStream(
146
- _request: TtsSynthesisRequest,
147
- onChunk: (chunk: Uint8Array) => void,
148
- ): Promise<TtsSynthesisResult> {
149
- onChunk(Buffer.from("managed!"));
150
- return { audio: Buffer.from("managed!"), contentType: "audio/pcm" };
151
- },
152
- };
153
- _setTtsProviderForTests(provider);
154
- return provider;
155
- }
156
-
157
- beforeAll(() => {
158
- fallbackMocksActive = true;
159
- });
160
-
161
- afterAll(() => {
162
- fallbackMocksActive = false;
163
- });
164
-
165
- afterEach(() => {
166
- _resetTtsProviderOverridesForTests();
167
- });
168
-
169
- beforeEach(() => {
170
- writeConfig(BYOK_CONFIG);
171
- managedAvailable = false;
172
- providerKeys = {};
173
- requestedSttProviders = [];
174
- });
175
-
176
- describe("live voice on cloud speech", () => {
177
- test("unkeyed BYOK TTS resolves the cloud provider and reports ready", async () => {
178
- managedAvailable = true;
179
- providerKeys = { deepgram: "test-key" };
180
- const configBefore = readConfigFile();
181
-
182
- const effective = await resolveEffectiveSpeechProviders();
183
- expect(effective).toEqual({ stt: "deepgram", tts: "cadet" });
184
-
185
- const readiness = await resolveLiveVoiceCredentialReadiness();
186
- expect(readiness).toEqual({ status: "ready" });
187
-
188
- expect(readConfigFile()).toBe(configBefore);
189
- });
190
-
191
- test("synthesis runs on the cloud provider while the config still names the BYOK one", async () => {
192
- managedAvailable = true;
193
- providerKeys = { deepgram: "test-key" };
194
- registerManagedTtsProvider();
195
- const configBefore = readConfigFile();
196
-
197
- const chunks: string[] = [];
198
- const result = await streamLiveVoiceTtsAudio({
199
- text: "hello",
200
- config: getConfig(),
201
- onAudioChunk: (chunk) => {
202
- chunks.push(chunk.dataBase64);
203
- },
204
- });
205
-
206
- expect(result.provider).toBe("cadet");
207
- expect(chunks).toHaveLength(1);
208
- expect(getConfig().services.tts.provider).toBe("elevenlabs");
209
- expect(readConfigFile()).toBe(configBefore);
210
- });
211
-
212
- test("unkeyed BYOK STT resolves the cloud provider and reports ready", async () => {
213
- managedAvailable = true;
214
- providerKeys = { elevenlabs: "test-key" };
215
- const configBefore = readConfigFile();
216
-
217
- const effective = await resolveEffectiveSpeechProviders();
218
- expect(effective).toEqual({ stt: "cadet", tts: "elevenlabs" });
219
-
220
- const readiness = await resolveLiveVoiceCredentialReadiness();
221
- expect(readiness).toEqual({ status: "ready" });
222
-
223
- // The transcriber the session arms is asked for the same provider the
224
- // readiness verdict was earned on. Live voice resolves the flux row while
225
- // the roleless verdict above stays on the base one, which is the whole
226
- // point of resolving per role.
227
- expect(requestedSttProviders).toEqual(["cadet-flux"]);
228
- expect(readConfigFile()).toBe(configBefore);
229
- });
230
-
231
- test("cloud speech unavailable keeps the configured providers and reports not-ready", async () => {
232
- managedAvailable = false;
233
- const configBefore = readConfigFile();
234
-
235
- const effective = await resolveEffectiveSpeechProviders();
236
- expect(effective).toEqual({ stt: "deepgram", tts: "elevenlabs" });
237
-
238
- const readiness = await resolveLiveVoiceCredentialReadiness();
239
- expect(readiness.status).toBe("not-ready");
240
- if (readiness.status !== "not-ready") {
241
- throw new Error("expected not-ready");
242
- }
243
- expect(readiness.missing.map((gap) => gap.providerId)).toEqual([
244
- "deepgram",
245
- "elevenlabs",
246
- ]);
247
- expect(requestedSttProviders).toEqual(["deepgram"]);
248
- expect(readConfigFile()).toBe(configBefore);
249
- });
250
- });