@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.
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +10 -4
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +10 -4
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +10 -4
- package/openapi.yaml +6679 -658
- package/package.json +1 -1
- package/src/__tests__/config-loader-cloud-defaults.test.ts +0 -233
- package/src/__tests__/credential-health-service.test.ts +0 -14
- package/src/__tests__/credential-routes.test.ts +0 -51
- package/src/__tests__/init-feature-flag-overrides.test.ts +40 -24
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -2
- package/src/__tests__/llm-schema-fallback-profile.test.ts +9 -3
- package/src/__tests__/requests-mock.ts +231 -9
- package/src/__tests__/secret-routes-cloud-proxy.test.ts +8 -24
- package/src/__tests__/secret-routes-scrub.test.ts +2 -42
- package/src/__tests__/test-preload.ts +8 -5
- package/src/__tests__/web-search.test.ts +340 -859
- package/src/api/events/cloud-disconnected.ts +2 -2
- package/src/config/schemas/cloud.ts +2 -1
- package/src/config/schemas/host-browser.ts +1 -0
- package/src/config/schemas/ingress.ts +1 -12
- package/src/config/schemas/llm.ts +10 -1
- package/src/config/schemas/memory-lifecycle.ts +11 -1
- package/src/config/schemas/tts.ts +5 -0
- package/src/credential-execution/prompted-credential.ts +6 -18
- package/src/credential-health/credential-health-service.ts +6 -21
- package/src/daemon/credential-transcript-scrub.ts +21 -29
- package/src/ipc/gateway-client.test.ts +55 -59
- package/src/persistence/compaction-log-store-clickhouse.ts +1 -1
- package/src/persistence/llm-request-log-sink-clickhouse.ts +1 -1
- package/src/persistence/llm-request-log-source-clickhouse.ts +1 -1
- package/src/runtime/migrations/vbundle-streaming-importer.ts +3 -3
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +10 -7
- package/src/runtime/routes/__tests__/migration-cadet-metadata-reconcile.test.ts +3 -9
- package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +3 -3
- package/src/runtime/routes/cloud-routes.ts +14 -18
- package/src/runtime/routes/conversation-query-routes.ts +30 -230
- package/src/runtime/routes/migration-routes.ts +5 -6
- package/src/runtime/routes/secret-routes.ts +12 -31
- package/src/tools/credentials/store.ts +6 -15
- 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
|
-
*
|
|
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
|
|
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
|
-
"
|
|
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", "
|
|
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", "
|
|
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
|
|
12
|
-
* credentials and emits
|
|
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
|
|
118
|
+
const CloudDisconnectResponseSchema = z.object({
|
|
120
119
|
disconnected: z.literal(true),
|
|
121
120
|
previousBaseUrl: z.string().nullable(),
|
|
122
121
|
});
|
|
123
|
-
type
|
|
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
|
|
398
|
+
async function handleCloudDisconnect(
|
|
402
399
|
_args: RouteHandlerArgs,
|
|
403
|
-
): Promise<
|
|
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
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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: "
|
|
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
|
|
944
|
+
"Deletes stored cloud credentials and emits cloud_disconnected signal to connected clients.",
|
|
949
945
|
tags: ["platform"],
|
|
950
|
-
handler:
|
|
951
|
-
responseBody:
|
|
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 {
|
|
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 `
|
|
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 =
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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:
|
|
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
|
-
*
|
|
123
|
-
* On a cloud assistant the cloud provisions these after hatch
|
|
124
|
-
* `POST /v1/secrets
|
|
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
|
|
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
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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 {
|
|
140
|
+
const { scrubStoredCredentialFromTranscripts } =
|
|
141
141
|
await import("../../daemon/credential-transcript-scrub.js");
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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, {
|