@mycadet/assistant 0.12.11 → 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 (176) hide show
  1. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  2. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  3. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  4. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  5. package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
  6. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  7. package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  8. package/openapi.yaml +2 -708
  9. package/package.json +2 -3
  10. package/scripts/sync-web-search-catalog.ts +1 -2
  11. package/src/__tests__/cloud-profile-guard.test.ts +2 -2
  12. package/src/__tests__/config-schema.test.ts +6 -38
  13. package/src/__tests__/conversation-queue.test.ts +45 -6
  14. package/src/__tests__/db-readiness-http-gate.test.ts +0 -6
  15. package/src/__tests__/persist-onboarding-artifacts.test.ts +0 -1
  16. package/src/__tests__/persona-resolver.test.ts +0 -51
  17. package/src/__tests__/provider-cloud-proxy-integration.test.ts +2 -110
  18. package/src/__tests__/run-due-schedules.test.ts +0 -4
  19. package/src/__tests__/schedule-store.test.ts +0 -12
  20. package/src/__tests__/stt-stream-session.test.ts +4 -4
  21. package/src/__tests__/telephony-tts-guard.test.ts +2 -50
  22. package/src/__tests__/voice-config-update.test.ts +12 -167
  23. package/src/__tests__/web-search-catalog-parity.test.ts +0 -2
  24. package/src/__tests__/workspace-migration-130-speech-mode-to-provider.test.ts +9 -7
  25. package/src/__tests__/workspace-migration-132-web-search-mode-to-provider.test.ts +6 -6
  26. package/src/calls/__tests__/voice-session-bridge.test.ts +1 -4
  27. package/src/calls/resolve-call-tts-provider.ts +6 -25
  28. package/src/calls/telephony-credential-preflight.ts +0 -10
  29. package/src/calls/telephony-tts-capability.ts +1 -17
  30. package/src/cli/commands/tts.ts +1 -2
  31. package/src/cli/index.help.ts +0 -2
  32. package/src/cli/program.ts +0 -2
  33. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +4 -42
  34. package/src/config/schemas/__tests__/stt.test.ts +10 -14
  35. package/src/config/schemas/services.ts +5 -6
  36. package/src/config/schemas/stt.ts +1 -3
  37. package/src/config/schemas/tts.ts +1 -30
  38. package/src/daemon/daemon-readiness.ts +4 -29
  39. package/src/daemon/lifecycle.ts +0 -31
  40. package/src/daemon/providers-setup.ts +0 -8
  41. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  42. package/src/heartbeat/heartbeat-service.ts +1 -66
  43. package/src/ipc/__tests__/db-readiness-ipc-gate.test.ts +1 -9
  44. package/src/live-voice/__tests__/live-voice-credential-preflight.test.ts +0 -44
  45. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +4 -6
  46. package/src/live-voice/__tests__/live-voice-stt.test.ts +1 -1
  47. package/src/live-voice/live-voice-credential-preflight.ts +9 -33
  48. package/src/live-voice/live-voice-session.ts +10 -6
  49. package/src/live-voice/live-voice-tts.ts +1 -8
  50. package/src/plugins/defaults/memory/worker.ts +3 -3
  51. package/src/prompts/persona-resolver.ts +0 -35
  52. package/src/providers/__tests__/search-provider-catalog.test.ts +5 -8
  53. package/src/providers/__tests__/voice-error-copy.test.ts +9 -10
  54. package/src/providers/search-provider-catalog.ts +7 -39
  55. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +3 -47
  56. package/src/providers/speech-to-text/__tests__/resolve.test.ts +6 -372
  57. package/src/providers/speech-to-text/provider-catalog.ts +2 -73
  58. package/src/providers/speech-to-text/resolve.ts +3 -120
  59. package/src/runtime/http-router-types.ts +1 -3
  60. package/src/runtime/http-server.ts +1 -1
  61. package/src/runtime/routes/__tests__/tts-routes.test.ts +2 -6
  62. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +7 -7
  63. package/src/runtime/routes/index.ts +0 -4
  64. package/src/runtime/routes/live-voice-routes.test.ts +4 -11
  65. package/src/runtime/routes/live-voice-routes.ts +8 -11
  66. package/src/runtime/routes/secret-routes.ts +0 -6
  67. package/src/runtime/routes/tts-routes.ts +0 -38
  68. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +1 -7
  69. package/src/schedule/__tests__/worker-mcp-readiness.test.ts +0 -4
  70. package/src/schedule/schedule-store.ts +0 -7
  71. package/src/schedule/scheduler.ts +0 -5
  72. package/src/schedule/worker.ts +1 -1
  73. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +2 -10
  74. package/src/stt/daemon-batch-transcriber.ts +4 -38
  75. package/src/stt/types.ts +1 -9
  76. package/src/tools/network/__tests__/web-search.test.ts +3 -250
  77. package/src/tools/network/web-search.ts +4 -165
  78. package/src/tts/__tests__/provider-resolution.test.ts +0 -5
  79. package/src/tts/provider-catalog.ts +0 -4
  80. package/src/tts/types.ts +0 -1
  81. package/src/__tests__/cloud-speech-defaults.test.ts +0 -330
  82. package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1708
  83. package/src/__tests__/migration-export-http.test.ts +0 -557
  84. package/src/__tests__/migration-export-streaming.test.ts +0 -350
  85. package/src/__tests__/migration-export-to-gcs.test.ts +0 -706
  86. package/src/__tests__/migration-import-commit-http.test.ts +0 -1176
  87. package/src/__tests__/migration-import-from-gcs.test.ts +0 -534
  88. package/src/__tests__/migration-import-from-path.test.ts +0 -324
  89. package/src/__tests__/migration-import-from-url.test.ts +0 -588
  90. package/src/__tests__/migration-import-preflight-http.test.ts +0 -739
  91. package/src/__tests__/migration-jobs-status.test.ts +0 -139
  92. package/src/__tests__/migration-parity-persistence.test.ts +0 -1891
  93. package/src/__tests__/migration-transport.test.ts +0 -1089
  94. package/src/__tests__/migration-validate-http.test.ts +0 -634
  95. package/src/__tests__/migration-wizard.test.ts +0 -1395
  96. package/src/__tests__/rebind-secrets-screen.test.ts +0 -876
  97. package/src/__tests__/sanitize-config-for-transfer.test.ts +0 -259
  98. package/src/__tests__/transfer-progress-screen.test.ts +0 -1217
  99. package/src/__tests__/validation-results-screen.test.ts +0 -1130
  100. package/src/__tests__/vbundle-pax-and-symlink.test.ts +0 -147
  101. package/src/background-wake/background-wake-routes.test.ts +0 -906
  102. package/src/background-wake/cloud-client.test.ts +0 -328
  103. package/src/background-wake/cloud-client.ts +0 -196
  104. package/src/background-wake/next-wake.test.ts +0 -335
  105. package/src/background-wake/next-wake.ts +0 -188
  106. package/src/background-wake/publisher.ts +0 -93
  107. package/src/background-wake/wake-intent-hooks.test.ts +0 -234
  108. package/src/cli/commands/__tests__/backup.test.ts +0 -363
  109. package/src/cli/commands/backup.help.ts +0 -180
  110. package/src/cli/commands/backup.ts +0 -398
  111. package/src/cloud/cloud-speech.test.ts +0 -335
  112. package/src/cloud/cloud-speech.ts +0 -300
  113. package/src/config/cloud-speech-defaults.ts +0 -300
  114. package/src/config/sanitize-for-transfer.ts +0 -111
  115. package/src/live-voice/__tests__/live-voice-cloud-speech-fallback.test.ts +0 -250
  116. package/src/providers/speech-to-text/__tests__/cadet-cloud-realtime.test.ts +0 -457
  117. package/src/providers/speech-to-text/__tests__/cadet-cloud.test.ts +0 -271
  118. package/src/providers/speech-to-text/__tests__/cadet-speech-relay-connection.test.ts +0 -126
  119. package/src/providers/speech-to-text/cadet-cloud-flux-realtime.ts +0 -309
  120. package/src/providers/speech-to-text/cadet-cloud-realtime.ts +0 -315
  121. package/src/providers/speech-to-text/cadet-cloud.ts +0 -108
  122. package/src/providers/speech-to-text/cadet-speech-relay-connection.ts +0 -176
  123. package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +0 -254
  124. package/src/runtime/migrations/__tests__/job-registry.test.ts +0 -346
  125. package/src/runtime/migrations/__tests__/origin-mode.test.ts +0 -62
  126. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +0 -184
  127. package/src/runtime/migrations/__tests__/staged-import-path.test.ts +0 -110
  128. package/src/runtime/migrations/__tests__/v1-test-helpers.ts +0 -112
  129. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +0 -285
  130. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +0 -126
  131. package/src/runtime/migrations/__tests__/vbundle-builder-v1-shape.test.ts +0 -253
  132. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +0 -208
  133. package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +0 -485
  134. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +0 -349
  135. package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +0 -196
  136. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +0 -551
  137. package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +0 -428
  138. package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +0 -220
  139. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +0 -1823
  140. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +0 -598
  141. package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +0 -463
  142. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -538
  143. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +0 -515
  144. package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +0 -453
  145. package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +0 -319
  146. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +0 -240
  147. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +0 -473
  148. package/src/runtime/migrations/gcs-signed-url.ts +0 -190
  149. package/src/runtime/migrations/job-registry.ts +0 -297
  150. package/src/runtime/migrations/migration-transport.ts +0 -612
  151. package/src/runtime/migrations/migration-wizard.ts +0 -747
  152. package/src/runtime/migrations/origin-mode.ts +0 -42
  153. package/src/runtime/migrations/rebind-secrets-screen.ts +0 -412
  154. package/src/runtime/migrations/staged-import-path.ts +0 -116
  155. package/src/runtime/migrations/transfer-progress-screen.ts +0 -323
  156. package/src/runtime/migrations/validation-results-screen.ts +0 -469
  157. package/src/runtime/migrations/vbundle-builder.ts +0 -1382
  158. package/src/runtime/migrations/vbundle-import-analyzer.ts +0 -405
  159. package/src/runtime/migrations/vbundle-import-policy.ts +0 -241
  160. package/src/runtime/migrations/vbundle-importer.ts +0 -826
  161. package/src/runtime/migrations/vbundle-metadata-merge.ts +0 -113
  162. package/src/runtime/migrations/vbundle-streaming-importer.ts +0 -2844
  163. package/src/runtime/migrations/vbundle-streaming-validator.ts +0 -426
  164. package/src/runtime/migrations/vbundle-tar-stream.ts +0 -244
  165. package/src/runtime/migrations/vbundle-validator.ts +0 -724
  166. package/src/runtime/routes/__tests__/migration-cadet-metadata-reconcile.test.ts +0 -227
  167. package/src/runtime/routes/__tests__/migration-export-secrets-redacted.test.ts +0 -54
  168. package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +0 -70
  169. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +0 -257
  170. package/src/runtime/routes/background-wake-routes.ts +0 -387
  171. package/src/runtime/routes/migration-routes.ts +0 -2584
  172. package/src/tools/network/__tests__/cloud-search-proxy.test.ts +0 -282
  173. package/src/tools/network/cloud-search-proxy.ts +0 -211
  174. package/src/tts/__tests__/cadet-provider.test.ts +0 -253
  175. package/src/tts/providers/cadet-provider.ts +0 -244
  176. 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
- });