@mycadet/assistant 0.12.7-dev.202610111049.d2b52a8 → 0.12.8

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 (40) hide show
  1. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +10 -4
  2. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +10 -4
  3. package/node_modules/@mycadet/service-contracts/src/ingress.ts +10 -4
  4. package/openapi.yaml +6679 -658
  5. package/package.json +1 -1
  6. package/src/__tests__/config-loader-cloud-defaults.test.ts +0 -233
  7. package/src/__tests__/credential-health-service.test.ts +0 -14
  8. package/src/__tests__/credential-routes.test.ts +0 -51
  9. package/src/__tests__/init-feature-flag-overrides.test.ts +40 -24
  10. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -2
  11. package/src/__tests__/llm-schema-fallback-profile.test.ts +9 -3
  12. package/src/__tests__/requests-mock.ts +231 -9
  13. package/src/__tests__/secret-routes-cloud-proxy.test.ts +8 -24
  14. package/src/__tests__/secret-routes-scrub.test.ts +2 -42
  15. package/src/__tests__/test-preload.ts +8 -5
  16. package/src/__tests__/web-search.test.ts +340 -859
  17. package/src/api/events/cloud-disconnected.ts +2 -2
  18. package/src/config/schemas/cloud.ts +2 -1
  19. package/src/config/schemas/host-browser.ts +1 -0
  20. package/src/config/schemas/ingress.ts +1 -12
  21. package/src/config/schemas/llm.ts +10 -1
  22. package/src/config/schemas/memory-lifecycle.ts +11 -1
  23. package/src/config/schemas/tts.ts +5 -0
  24. package/src/credential-execution/prompted-credential.ts +6 -18
  25. package/src/credential-health/credential-health-service.ts +6 -21
  26. package/src/daemon/credential-transcript-scrub.ts +21 -29
  27. package/src/ipc/gateway-client.test.ts +55 -59
  28. package/src/persistence/compaction-log-store-clickhouse.ts +1 -1
  29. package/src/persistence/llm-request-log-sink-clickhouse.ts +1 -1
  30. package/src/persistence/llm-request-log-source-clickhouse.ts +1 -1
  31. package/src/runtime/migrations/vbundle-streaming-importer.ts +3 -3
  32. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +10 -7
  33. package/src/runtime/routes/__tests__/migration-cadet-metadata-reconcile.test.ts +3 -9
  34. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +3 -3
  35. package/src/runtime/routes/cloud-routes.ts +14 -18
  36. package/src/runtime/routes/conversation-query-routes.ts +30 -230
  37. package/src/runtime/routes/migration-routes.ts +5 -6
  38. package/src/runtime/routes/secret-routes.ts +12 -31
  39. package/src/tools/credentials/store.ts +6 -15
  40. package/src/__tests__/mock-gateway-ipc.ts +0 -279
@@ -2,13 +2,12 @@
2
2
  * Tests for the post-import cadet metadata reconciliation helper.
3
3
  *
4
4
  * After every bundle import, `reconcileCadetMetadataFromCes` walks the
5
- * cloud-identity fields the gateway requires and, for each one that
5
+ * cadet credentials the gateway requires and, for each one that
6
6
  * CES already holds a value for, ensures `metadata.json` lists a
7
7
  * matching entry. This closes the race where a provisioning write to
8
8
  * CES completes successfully but its metadata upsert gets clobbered by
9
9
  * the import's in-place clear or atomic swap. The reconciled set covers
10
- * the Django-provisioned fields (assistant_api_key, platform_assistant_id,
11
- * webhook_secret).
10
+ * the Django-provisioned credentials (assistant_api_key, webhook_secret).
12
11
  *
13
12
  * We test the reconcile logic in isolation by mocking the secure-keys
14
13
  * and metadata-store modules — the real migration handler wires the
@@ -38,11 +37,7 @@ type MetadataRecord = {
38
37
  updatedAt: number;
39
38
  };
40
39
 
41
- const CADET_FIELDS = [
42
- "assistant_api_key",
43
- "platform_assistant_id",
44
- "webhook_secret",
45
- ] as const;
40
+ const CADET_FIELDS = ["assistant_api_key", "webhook_secret"] as const;
46
41
 
47
42
  const upsertCalls: Array<{ service: string; field: string }> = [];
48
43
  let metadataStore: Map<string, MetadataRecord> = new Map();
@@ -171,7 +166,6 @@ describe("reconcileCadetMetadataFromCes", () => {
171
166
  const reconciled = new Set(upsertCalls.map((c) => c.field));
172
167
  // Django-provisioned trio.
173
168
  expect(reconciled).toContain("assistant_api_key");
174
- expect(reconciled).toContain("platform_assistant_id");
175
169
  expect(reconciled).toContain("webhook_secret");
176
170
  });
177
171
 
@@ -110,7 +110,7 @@ describe("migration import credential filtering", () => {
110
110
  test("platform (cadet:*) credentials are excluded", () => {
111
111
  const cadetFields = [
112
112
  "assistant_api_key",
113
- "platform_assistant_id",
113
+ "cloud_assistant_id",
114
114
  "webhook_secret",
115
115
  ] as const;
116
116
  const cadetPaths = cadetFields.map((f) =>
@@ -163,7 +163,7 @@ describe("migration import credential filtering", () => {
163
163
 
164
164
  test("mixed bundle with both platform and user credentials correctly splits", () => {
165
165
  const cadetApiKey = credentialKey("cadet", "assistant_api_key");
166
- const cadetUserId = credentialKey("cadet", "platform_assistant_id");
166
+ const cadetUserId = credentialKey("cadet", "cloud_assistant_id");
167
167
  const openaiKey = credentialKey("openai", "api_key");
168
168
  const anthropicKey = credentialKey("anthropic", "api_key");
169
169
  const githubToken = credentialKey("github", "api_token");
@@ -203,7 +203,7 @@ describe("migration import credential filtering", () => {
203
203
 
204
204
  test("skippedCloud count is accurate with mixed credentials", () => {
205
205
  const cadetApiKey = credentialKey("cadet", "assistant_api_key");
206
- const cadetAssistantId = credentialKey("cadet", "platform_assistant_id");
206
+ const cadetAssistantId = credentialKey("cadet", "cloud_assistant_id");
207
207
  const cadetWebhook = credentialKey("cadet", "webhook_secret");
208
208
  const userKey = credentialKey("github", "api_token");
209
209
 
@@ -8,8 +8,8 @@
8
8
  * - platform_connect (POST platform/connect): reports whether the stored
9
9
  * credentials still connect, and when the stored key has been rejected,
10
10
  * says so, so the caller can tell the user what replaces it.
11
- * - platform_disconnect (POST platform/disconnect): deletes stored platform
12
- * credentials and emits platform_disconnected signal.
11
+ * - platform_disconnect (POST platform/disconnect): deletes stored cloud
12
+ * credentials and emits cloud_disconnected signal.
13
13
  * - platform_callback_routes_register (POST platform/callback-routes/register):
14
14
  * registers a callback route with the cloud gateway.
15
15
  * - platform_callback_routes_list (GET platform/callback-routes): lists
@@ -76,7 +76,6 @@ import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
76
76
 
77
77
  const CREDENTIAL_KEYS = {
78
78
  apiKey: { service: "cadet", field: "assistant_api_key" },
79
- assistantId: { service: "cadet", field: "platform_assistant_id" },
80
79
  } as const;
81
80
 
82
81
  // ---------------------------------------------------------------------------
@@ -116,13 +115,11 @@ export type PlatformConnectResponse = z.infer<
116
115
  typeof PlatformConnectResponseSchema
117
116
  >;
118
117
 
119
- const PlatformDisconnectResponseSchema = z.object({
118
+ const CloudDisconnectResponseSchema = z.object({
120
119
  disconnected: z.literal(true),
121
120
  previousBaseUrl: z.string().nullable(),
122
121
  });
123
- type PlatformDisconnectResponse = z.infer<
124
- typeof PlatformDisconnectResponseSchema
125
- >;
122
+ type CloudDisconnectResponse = z.infer<typeof CloudDisconnectResponseSchema>;
126
123
 
127
124
  const CallbackRouteRegisterRequestSchema = z.object({
128
125
  path: z.string(),
@@ -398,9 +395,9 @@ async function handlePlatformVerifyCredential(
398
395
  return { status: await verifyStoredCredential() };
399
396
  }
400
397
 
401
- async function handlePlatformDisconnect(
398
+ async function handleCloudDisconnect(
402
399
  _args: RouteHandlerArgs,
403
- ): Promise<PlatformDisconnectResponse> {
400
+ ): Promise<CloudDisconnectResponse> {
404
401
  // Reject if running inside a cloud host
405
402
  if (isCloudRemote()) {
406
403
  throw new UnprocessableEntityError(
@@ -420,11 +417,10 @@ async function handlePlatformDisconnect(
420
417
  );
421
418
  }
422
419
 
423
- // Delete all cloud credentials
424
- const keysToDelete = [
425
- CREDENTIAL_KEYS.apiKey,
426
- CREDENTIAL_KEYS.assistantId,
427
- ] as const;
420
+ // Delete the stored cloud credential. The cloud_*_id fields are not
421
+ // credentials — they resolve from the api key via validate and live only
422
+ // in the in-memory identity, cleared right below.
423
+ const keysToDelete = [CREDENTIAL_KEYS.apiKey] as const;
428
424
 
429
425
  const failedKeys: string[] = [];
430
426
  for (const key of keysToDelete) {
@@ -447,7 +443,7 @@ async function handlePlatformDisconnect(
447
443
  setCloudUserId(undefined);
448
444
 
449
445
  // Notify connected clients
450
- broadcastMessage({ type: "platform_disconnected" });
446
+ broadcastMessage({ type: "cloud_disconnected" });
451
447
 
452
448
  return {
453
449
  disconnected: true,
@@ -945,10 +941,10 @@ export const ROUTES: RouteDefinition[] = [
945
941
  },
946
942
  summary: "Disconnect from Cadet Cloud",
947
943
  description:
948
- "Deletes stored cloud credentials and emits platform_disconnected signal to connected clients.",
944
+ "Deletes stored cloud credentials and emits cloud_disconnected signal to connected clients.",
949
945
  tags: ["platform"],
950
- handler: handlePlatformDisconnect,
951
- responseBody: PlatformDisconnectResponseSchema,
946
+ handler: handleCloudDisconnect,
947
+ responseBody: CloudDisconnectResponseSchema,
952
948
  },
953
949
  {
954
950
  operationId: "platform_callback_routes_register",
@@ -35,7 +35,6 @@ import {
35
35
  scrubNulledChatSettings,
36
36
  validateChatSettingsWrite,
37
37
  } from "../../config/chat-settings.js";
38
- import { DefaultProviderSelectionSchema } from "../../config/default-provider.js";
39
38
  import {
40
39
  catalogEntryFor,
41
40
  type InputModalities,
@@ -44,10 +43,9 @@ import {
44
43
  } from "../../config/input-modalities.js";
45
44
  import {
46
45
  deepMergeOverwrite,
47
- fillContextDefaultsForMissingKeys,
48
46
  getConfig,
49
- getDeploymentContextDefaults,
50
47
  invalidateConfigCache,
48
+ loadConfig,
51
49
  loadRawConfig,
52
50
  saveRawConfig,
53
51
  setNestedValue,
@@ -58,22 +56,21 @@ import {
58
56
  completeCustomProfile,
59
57
  mergePreservingUnknownKeys,
60
58
  } from "../../config/profile-materialization.js";
61
- import { AssistantConfigSchema } from "../../config/schema.js";
59
+ import {
60
+ type AssistantConfig,
61
+ AssistantConfigSchema,
62
+ } from "../../config/schema.js";
62
63
  import { getSchemaAtPath } from "../../config/schema-utils.js";
63
- import { AutoArchiveConfigSchema } from "../../config/schemas/conversations.js";
64
64
  import {
65
65
  collectFallbackProfileIssues,
66
- isDefaultProviderChoice,
67
66
  LLMConfigBase,
68
67
  LLMConfigFragment,
69
- LLMDefaultConfigSchema,
70
68
  LLMSchema,
71
69
  ProfileEntry,
72
70
  routingIdentityModelIssue,
73
71
  unknownLlmProviderIssue,
74
72
  } from "../../config/schemas/llm.js";
75
73
  import { VALID_MEMORY_EMBEDDING_PROVIDERS } from "../../config/schemas/memory-storage.js";
76
- import { NotificationsConfigSchema } from "../../config/schemas/notifications.js";
77
74
  import { ServiceModeSchema } from "../../config/schemas/services.js";
78
75
  import { isSidebarDoneEnabled } from "../../config/sidebar-done-gate.js";
79
76
  import {
@@ -526,85 +523,6 @@ async function handleSetEmbeddingConfig({ body }: RouteHandlerArgs) {
526
523
  }
527
524
  }
528
525
 
529
- /**
530
- * Apply deployment-context defaults to a raw config payload before it goes
531
- * out over the wire from `GET /v1/config`. The in-memory `loadConfig()`
532
- * already layers these defaults for daemon-internal consumers; the GET
533
- * response needs the same treatment so external clients (macOS, web, CLI)
534
- * see the effective value rather than `undefined` when the daemon hasn't
535
- * persisted an explicit choice yet. For example, on a freshly-hatched
536
- * cloud assistant, `services.image-generation.provider` may be
537
- * absent from disk (only `llm.profiles` was written by
538
- * `seedInferenceProfiles`); the fill pass ensures clients receive `"cadet"`
539
- * rather than falling back to their own defaults.
540
- *
541
- * Guards against `loadRawConfig()` handing us a value that is technically
542
- * valid JSON but not a plain object (e.g. literal `null`, a number, or an
543
- * array). `loadRawConfig` is typed `Record<string, unknown>` but `JSON.parse`
544
- * itself doesn't enforce that — a malformed-but-parseable `config.json`
545
- * would blow up `fillContextDefaultsForMissingKeys` on its `target[key]` /
546
- * `fileConfig[key]` accesses, turning `GET /v1/config` into a 500 where it
547
- * used to succeed (returning the malformed payload as-is). When `raw` is
548
- * not a plain object, we return it unchanged.
549
- *
550
- * Exported for direct unit testing.
551
- */
552
- export function applyContextDefaultsToRawConfig(raw: unknown): unknown {
553
- const contextDefaults = getDeploymentContextDefaults();
554
- if (
555
- Object.keys(contextDefaults).length === 0 ||
556
- raw === null ||
557
- typeof raw !== "object" ||
558
- Array.isArray(raw)
559
- ) {
560
- return raw;
561
- }
562
- fillContextDefaultsForMissingKeys(
563
- raw as Record<string, unknown>,
564
- raw as Record<string, unknown>,
565
- contextDefaults,
566
- );
567
- synthesizeLegacyInferenceModeForPlatform(raw as Record<string, unknown>);
568
- return raw;
569
- }
570
-
571
- /**
572
- * Backwards-compat wire field for `GET /v1/config`. PR removed
573
- * `services.inference.mode` from the typed schema (routing is governed by
574
- * `provider_connections` rows + per-profile `provider_connection`), but
575
- * the macOS settings client (`SettingsStore.swift:loadServiceModes`) still
576
- * reads this field and falls back to its `@Published` default of "your-own"
577
- * when absent. On a cloud assistant served by a newer daemon and
578
- * an older macOS client, that fallback would show the wrong mode in the UI
579
- * until the user explicitly saved. Synthesize the value here so the wire
580
- * shape stays compatible during the rollout window. Remove once the macOS
581
- * Providers UI (the follow-up PR that retires this field on the client) has
582
- * shipped to the majority of installs.
583
- *
584
- * The synthesis is wire-only: it never persists to disk and never reaches
585
- * the typed `AssistantConfig` consumed by daemon-internal code. The on-disk
586
- * config is stripped of `mode` by workspace migration 076.
587
- *
588
- * Only runs when this function is reached, which is guarded by
589
- * `getDeploymentContextDefaults()` returning non-empty (IS_CLOUD=true).
590
- */
591
- function synthesizeLegacyInferenceModeForPlatform(
592
- root: Record<string, unknown>,
593
- ): void {
594
- const services = readPlainObject(root.services);
595
- if (!services) {
596
- return;
597
- }
598
- let inference = readPlainObject(services.inference);
599
- if (!inference) {
600
- inference = {};
601
- services.inference = inference;
602
- }
603
- if (inference.mode === undefined) {
604
- inference.mode = "managed";
605
- }
606
- }
607
-
608
526
  function readPlainObject(value: unknown): Record<string, unknown> | undefined {
609
527
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
610
528
  return undefined;
@@ -732,13 +650,6 @@ function nestPath(path: string, value: unknown): unknown {
732
650
  return current;
733
651
  }
734
652
 
735
- const WireProfileEntry = ProfileEntry.extend({
736
- supportsVision: z.boolean().optional(),
737
- invariant: z.boolean().optional(),
738
- })
739
- .passthrough()
740
- .meta({ id: "ProfileEntry" });
741
-
742
653
  /**
743
654
  * Wire shape of the `memory` section in config responses. Passthrough
744
655
  * preserves fields beyond `enabled` and `v2` so the client doesn't strip
@@ -752,97 +663,6 @@ const MemoryWireConfigSchema = z
752
663
  .passthrough()
753
664
  .meta({ id: "MemoryConfig" });
754
665
 
755
- /**
756
- * Response schema for `GET /v1/config`.
757
- *
758
- * Describes the wire shape of the raw `settings.json` response after
759
- * context-default filling and vision-flag enrichment. Chat settings include
760
- * effective defaults. Fields remain optional for older assistants and sparse
761
- * on-disk config. Additional top-level sections are preserved via
762
- * passthrough. This schema types the fields that web/macOS clients consume
763
- * without restricting the full config surface.
764
- */
765
- const ConfigGetResponseSchema = z
766
- .object({
767
- // A malformed or absent stored chat-settings subtree falls back to its
768
- // schema defaults so one bad leaf can't fail the whole wire parse (which
769
- // would strip every derived field from the response); the chat-settings
770
- // read overlay refills both from the effective config.
771
- conversations: z
772
- .object({
773
- autoArchive: AutoArchiveConfigSchema.passthrough().catch(
774
- AutoArchiveConfigSchema.parse({}),
775
- ),
776
- })
777
- .passthrough()
778
- .optional(),
779
- notifications: NotificationsConfigSchema.passthrough()
780
- .optional()
781
- .catch(undefined),
782
- llm: z
783
- .object({
784
- default: LLMDefaultConfigSchema.optional(),
785
- // Wire-compat view of the default provider, derived from `default`
786
- // by the read overlay (the key is never stored anymore).
787
- defaultProvider: DefaultProviderSelectionSchema.optional(),
788
- profiles: z.record(z.string(), WireProfileEntry).optional(),
789
- profileOrder: z.array(z.string()).optional(),
790
- activeProfile: z.string().optional(),
791
- // The profile the advisor consults; excluded from chat-profile pickers.
792
- advisorProfile: z.string().optional(),
793
- callSites: z
794
- .record(
795
- z.string(),
796
- LLMConfigFragment.extend({
797
- profile: z.string().optional(),
798
- }).nullable(),
799
- )
800
- .optional(),
801
- profileSession: z
802
- .object({
803
- defaultTtlSeconds: z.number().optional(),
804
- maxTtlSeconds: z.number().optional(),
805
- })
806
- .optional(),
807
- pricingOverrides: z.array(z.unknown()).optional(),
808
- })
809
- .passthrough()
810
- .optional(),
811
- memory: MemoryWireConfigSchema.optional(),
812
- services: z
813
- .object({
814
- "web-search": z
815
- .object({
816
- provider: z.string().optional(),
817
- })
818
- .passthrough()
819
- .optional(),
820
- "web-fetch": z
821
- .object({
822
- provider: z.string().optional(),
823
- })
824
- .passthrough()
825
- .optional(),
826
- "image-generation": z
827
- .object({
828
- provider: z.string().optional(),
829
- model: z.string().optional(),
830
- })
831
- .passthrough()
832
- .optional(),
833
- inference: z
834
- .object({ mode: ServiceModeSchema.optional() })
835
- .passthrough()
836
- .optional(),
837
- })
838
- .passthrough()
839
- .optional(),
840
- })
841
- .passthrough()
842
- .meta({ id: "ConfigGetResponse" });
843
-
844
- type ConfigGetResponse = z.infer<typeof ConfigGetResponseSchema>;
845
-
846
666
  /**
847
667
  * Given a `z.object(...)` schema, returns a new schema where every property
848
668
  * is `.nullable().optional()`. Used to express PATCH body semantics where any
@@ -880,7 +700,7 @@ const CallSiteOverrideDraftSchema = nullablePartial(
880
700
  *
881
701
  * Mirrors the response shape but every field is `.nullable().optional()`:
882
702
  * omitted keys are left unchanged by the daemon's deep-merge, `null` values
883
- * delete the key. Uses the same Zod enums as `ConfigGetResponseSchema` so
703
+ * delete the key. Uses the same Zod enums as `AssistantConfigSchema` so
884
704
  * the generated SDK produces literal-union types — no hand-written patch
885
705
  * types needed downstream.
886
706
  */
@@ -953,54 +773,37 @@ const ConfigPatchRequestSchema = z
953
773
  .passthrough()
954
774
  .meta({ id: "ConfigPatchRequest" });
955
775
 
776
+ /**
777
+ * The response contract for `GET`/`PATCH /v1/config` — the five subtrees
778
+ * external clients actually read, picked out of the full graph. Rendering
779
+ * the whole `AssistantConfigSchema` into the spec blasted openapi.yaml past
780
+ * 12K lines; we're not ready to trim the response schema itself yet, so the
781
+ * runtime still returns the full config and the spec documents these.
782
+ */
783
+ const ConfigGetResponseSchema = AssistantConfigSchema.pick({
784
+ conversations: true,
785
+ notifications: true,
786
+ llm: true,
787
+ memory: true,
788
+ services: true,
789
+ });
790
+
956
791
  function handleGetConfig() {
957
792
  try {
958
- const config = applyContextDefaultsToRawConfig(loadRawConfig());
959
- // One zod pass shapes the response: ConfigGetResponseSchema is this
960
- // route's wire contract, so the overlays stamp their derived fields on
961
- // the parsed view instead of digging handles out of raw objects. A
962
- // config the schema rejects gets no overlays and is served raw.
963
- const parsed = ConfigGetResponseSchema.safeParse(config);
964
- if (!parsed.success) {
965
- return config;
966
- }
967
- const wire = parsed.data;
968
- overlayWorkspaceMcpForConfigRead(wire);
969
- sanitizeMcpTransportHeadersForSettingsRead(wire);
970
- overlayEffectiveProfilesForWire(wire);
971
- enrichProfilesForWire(wire);
972
- overlayDefaultProviderForWire(wire);
973
- overlayChatSettingsForWire(wire);
974
- return wire;
793
+ const config = loadConfig();
794
+ overlayWorkspaceMcpForConfigRead(config);
795
+ sanitizeMcpTransportHeadersForSettingsRead(config);
796
+ overlayEffectiveProfilesForWire(config);
797
+ enrichProfilesForWire(config);
798
+ overlayChatSettingsForWire(config);
799
+ return config;
975
800
  } catch (err) {
976
801
  const message = err instanceof Error ? err.message : String(err);
977
802
  throw new InternalError(`Failed to read config: ${message}`);
978
803
  }
979
804
  }
980
805
 
981
- /**
982
- * Surface the default-provider selection on config reads. The selection lives
983
- * in the `llm.default` blob; the dedicated `defaultProvider` response field is
984
- * a derived wire-compat view (readers of the config GET predate the blob) and
985
- * is never stored on disk — it is stamped here from the blob on every read.
986
- */
987
- function overlayDefaultProviderForWire(config: ConfigGetResponse): void {
988
- if (!config.llm) {
989
- return;
990
- }
991
- const blob = config.llm.default;
992
- const provider = blob?.provider;
993
- if (provider != null && isDefaultProviderChoice(provider)) {
994
- config.llm.defaultProvider = {
995
- provider,
996
- connectionName: blob?.provider_connection,
997
- };
998
- } else {
999
- delete config.llm.defaultProvider;
1000
- }
1001
- }
1002
-
1003
- function overlayChatSettingsForWire(config: ConfigGetResponse): void {
806
+ function overlayChatSettingsForWire(config: AssistantConfig): void {
1004
807
  const effective = getConfig();
1005
808
  config.conversations = {
1006
809
  ...config.conversations,
@@ -1033,10 +836,7 @@ function overlayChatSettingsForWire(config: ConfigGetResponse): void {
1033
836
  * shields the matrix lookup from a persisted value the matrix cannot
1034
837
  * implement, which a raw read would feed straight into resolution.
1035
838
  */
1036
- function overlayEffectiveProfilesForWire(config: ConfigGetResponse): void {
1037
- if (!config.llm) {
1038
- config.llm = {};
1039
- }
839
+ function overlayEffectiveProfilesForWire(config: AssistantConfig): void {
1040
840
  const parsedLlm = LLMSchema.safeParse(config.llm);
1041
841
  config.llm.profiles = getUserSelectableProfilesForProvider(
1042
842
  parsedLlm.success ? parsedLlm.data : undefined,
@@ -119,18 +119,17 @@ import { RouteResponse } from "./types.js";
119
119
  const PLATFORM_CREDENTIAL_PREFIX = credentialKey("cadet", "");
120
120
 
121
121
  /**
122
- * Platform-identity fields that the cloud runtime expects to see in CES.
123
- * On a cloud assistant the cloud provisions these after hatch (most via
124
- * `POST /v1/secrets`). A signed-in client may additionally (re)assert some of
122
+ * Cadet credentials that the cloud runtime expects to see in CES.
123
+ * On a cloud assistant the cloud provisions these after hatch via
124
+ * `POST /v1/secrets`. A signed-in client may additionally (re)assert some of
125
125
  * them on teleport / local→managed transfer. Either set of writes can race with
126
126
  * the import — the CES write survives (separate volume), but the metadata
127
127
  * upsert may be clobbered by the in-place clear / atomic swap. After every
128
128
  * import we reconcile metadata.json against CES so any field CES already holds
129
129
  * a value for gets a matching metadata entry.
130
130
  */
131
- const CADET_PLATFORM_IDENTITY_FIELDS = [
131
+ const CADET_CREDENTIAL_FIELDS = [
132
132
  "assistant_api_key",
133
- "platform_assistant_id",
134
133
  "webhook_secret",
135
134
  ] as const;
136
135
 
@@ -145,7 +144,7 @@ const CADET_PLATFORM_IDENTITY_FIELDS = [
145
144
  export async function reconcileCadetMetadataFromCes(warningSink: {
146
145
  warnings: string[];
147
146
  }): Promise<void> {
148
- for (const field of CADET_PLATFORM_IDENTITY_FIELDS) {
147
+ for (const field of CADET_CREDENTIAL_FIELDS) {
149
148
  try {
150
149
  const value = await getSecureKeyAsync(credentialKey("cadet", field));
151
150
  if (!value) {
@@ -21,10 +21,7 @@ import { getCloudAssistantId } from "../../config/env.js";
21
21
  import { getConfig } from "../../config/loader.js";
22
22
  import { getCesClient } from "../../credential-execution/ces-runtime.js";
23
23
  import type { CesClient } from "../../credential-execution/client.js";
24
- import {
25
- isNonSecretPlatformField,
26
- scrubStoredCredentialFromTranscripts,
27
- } from "../../daemon/credential-transcript-scrub.js";
24
+ import { scrubStoredCredentialFromTranscripts } from "../../daemon/credential-transcript-scrub.js";
28
25
  import { syncManualTokenConnection } from "../../oauth/manual-token-connection.js";
29
26
  import { maybeReseedCapabilitiesAfterCloudCredential } from "../../plugins/defaults/memory/substrate/boot-maintenance.js";
30
27
  import { validateAnthropicApiKey } from "../../providers/anthropic/client.js";
@@ -97,20 +94,11 @@ async function persistCredential(
97
94
  `Failed to store credential in secure storage (backend: ${getActiveBackendName()})`,
98
95
  );
99
96
  }
100
- if (!isNonSecretPlatformField(service, field)) {
101
- try {
102
- const scrubbed = await scrubStoredCredentialFromTranscripts(value);
103
- log.info(
104
- { service, field, ...scrubbed },
105
- "Credential stored; scrubbed value from recent transcripts",
106
- );
107
- } catch (err) {
108
- log.warn(
109
- { err, service, field },
110
- "Credential stored, but transcript scrub failed",
111
- );
112
- }
113
- }
97
+ const scrubbed = await scrubStoredCredentialFromTranscripts(value);
98
+ log.info(
99
+ { service, field, ...scrubbed },
100
+ "Credential stored; scrubbed value from recent transcripts",
101
+ );
114
102
  upsertCredentialMetadata(service, field, {});
115
103
  await syncManualTokenConnection(service);
116
104
  }
@@ -326,19 +314,12 @@ async function handleAddSecret({ body }: RouteHandlerArgs) {
326
314
  // refresh: that side effect can throw, and a stored-but-unscrubbed key
327
315
  // must not depend on it succeeding. The key IS stored at this point;
328
316
  // the scrub is best-effort hygiene and must stay invisible to the
329
- // caller. Counts only — never the value.
330
- try {
331
- const scrubbed = await scrubStoredCredentialFromTranscripts(value);
332
- log.info(
333
- { provider: name, ...scrubbed },
334
- "API key stored; scrubbed value from recent transcripts",
335
- );
336
- } catch (err) {
337
- log.warn(
338
- { err, provider: name },
339
- "API key stored, but transcript scrub failed",
340
- );
341
- }
317
+ // Counts only — never the value.
318
+ const scrubbed = await scrubStoredCredentialFromTranscripts(value);
319
+ log.info(
320
+ { provider: name, ...scrubbed },
321
+ "API key stored; scrubbed value from recent transcripts",
322
+ );
342
323
 
343
324
  await refreshProvidersAfterSecretChange();
344
325
  log.info({ provider: name }, "API key updated via HTTP");
@@ -137,22 +137,13 @@ export async function storeCredentialValue(
137
137
  // also fires on OAuth refresh rotations and MCP header writes whose values
138
138
  // never transited a transcript.
139
139
  if (!input.skipTranscriptScrub) {
140
- const { isNonSecretPlatformField, scrubStoredCredentialFromTranscripts } =
140
+ const { scrubStoredCredentialFromTranscripts } =
141
141
  await import("../../daemon/credential-transcript-scrub.js");
142
- if (!isNonSecretPlatformField(service, field)) {
143
- try {
144
- const scrubbed = await scrubStoredCredentialFromTranscripts(value);
145
- log.info(
146
- { service, field, ...scrubbed },
147
- "Credential stored; scrubbed value from recent transcripts",
148
- );
149
- } catch (err) {
150
- log.warn(
151
- { err, service, field },
152
- "Credential stored, but transcript scrub failed",
153
- );
154
- }
155
- }
142
+ const scrubbed = await scrubStoredCredentialFromTranscripts(value);
143
+ log.info(
144
+ { service, field, ...scrubbed },
145
+ "Credential stored; scrubbed value from recent transcripts",
146
+ );
156
147
  }
157
148
 
158
149
  const metadata = upsertCredentialMetadata(service, field, {