@vellumai/assistant 0.10.2-dev.202606250202.4960321 → 0.10.2-dev.202606250318.5e7cfb0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.10.2-dev.202606250202.4960321",
3
+ "version": "0.10.2-dev.202606250318.5e7cfb0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1703,8 +1703,8 @@ describe("seedInferenceProfiles BYOK-mode managed profile labels", () => {
1703
1703
  // ---------------------------------------------------------------------------
1704
1704
  // Tests: OS Beta flag-gated managed profile. The template is defined but
1705
1705
  // intentionally NOT part of MANAGED_PROFILE_TEMPLATES, so seedInferenceProfiles
1706
- // must never create it. A later PR reconciles it in/out based on the `os-beta`
1707
- // feature flag.
1706
+ // must never create it. The flag-gated reconcile creates or removes it based on
1707
+ // the `os-beta` feature flag.
1708
1708
  // ---------------------------------------------------------------------------
1709
1709
 
1710
1710
  describe("OS Beta managed profile template", () => {
@@ -1751,20 +1751,21 @@ describe("OS Beta managed profile template", () => {
1751
1751
  expect(MANAGED_PROFILE_NAMES.has("os-beta")).toBe(true);
1752
1752
  });
1753
1753
 
1754
- test("materializeProfile honors the explicit OS Beta model", () => {
1754
+ test("materializeProfile resolves OS Beta to the Balanced model with low effort", () => {
1755
1755
  const entry = materializeProfile(
1756
1756
  OS_BETA_PROFILE_TEMPLATE,
1757
- "fireworks",
1758
- "fireworks-managed",
1757
+ "together",
1758
+ "together-managed",
1759
1759
  );
1760
1760
 
1761
- expect(entry.model).toBe("accounts/fireworks/models/glm-5p2");
1762
- expect(entry.provider_connection).toBe("fireworks-managed");
1763
- expect(entry.provider).toBe("fireworks");
1761
+ expect(entry.model).toBe("MiniMaxAI/MiniMax-M3");
1762
+ expect(entry.provider_connection).toBe("together-managed");
1763
+ expect(entry.provider).toBe("together");
1764
1764
  expect(entry.label).toBe("OS Beta");
1765
1765
  expect(entry.source).toBe("managed");
1766
1766
  expect(entry.maxTokens).toBe(32000);
1767
- expect(entry.effort).toBe("high");
1767
+ expect(entry.effort).toBe("low");
1768
1768
  expect(entry.thinking?.enabled).toBe(true);
1769
+ expect(entry.topP).toBe(0.95);
1769
1770
  });
1770
1771
  });
@@ -0,0 +1,390 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import { makeMockLogger } from "./helpers/mock-logger.js";
4
+
5
+ mock.module("../util/logger.js", () => ({
6
+ LOG_FILE_PATTERN: /^assistant-(\d{4}-\d{2}-\d{2})\.log$/,
7
+ getCliLogger: () => makeMockLogger(),
8
+ getLogger: () => makeMockLogger(),
9
+ initLogger: () => {},
10
+ pruneOldLogFiles: () => 0,
11
+ truncateForLog: (value: string, maxLen = 500) => value.slice(0, maxLen),
12
+ }));
13
+
14
+ let rawConfig: Record<string, unknown> = {};
15
+ let savedRawConfig: Record<string, unknown> | null = null;
16
+
17
+ function deepMerge(
18
+ target: Record<string, unknown>,
19
+ patch: Record<string, unknown>,
20
+ ): void {
21
+ for (const [key, value] of Object.entries(patch)) {
22
+ if (
23
+ value !== null &&
24
+ typeof value === "object" &&
25
+ !Array.isArray(value) &&
26
+ target[key] !== null &&
27
+ typeof target[key] === "object" &&
28
+ !Array.isArray(target[key])
29
+ ) {
30
+ deepMerge(
31
+ target[key] as Record<string, unknown>,
32
+ value as Record<string, unknown>,
33
+ );
34
+ } else {
35
+ target[key] = value;
36
+ }
37
+ }
38
+ }
39
+
40
+ function setNestedValue(
41
+ obj: Record<string, unknown>,
42
+ path: string,
43
+ value: unknown,
44
+ ): void {
45
+ const keys = path.split(".");
46
+ let current = obj;
47
+ for (const key of keys.slice(0, -1)) {
48
+ if (
49
+ current[key] === null ||
50
+ typeof current[key] !== "object" ||
51
+ Array.isArray(current[key])
52
+ ) {
53
+ current[key] = {};
54
+ }
55
+ current = current[key] as Record<string, unknown>;
56
+ }
57
+ current[keys[keys.length - 1]!] = value;
58
+ }
59
+
60
+ mock.module("../config/loader.js", () => ({
61
+ API_KEY_PROVIDERS: [],
62
+ applyNestedDefaults: (config: unknown) => config,
63
+ loadRawConfig: () => structuredClone(savedRawConfig ?? rawConfig),
64
+ saveRawConfig: (raw: Record<string, unknown>) => {
65
+ savedRawConfig = structuredClone(raw);
66
+ },
67
+ deepMergeOverwrite: deepMerge,
68
+ fillContextDefaultsForMissingKeys: () => {},
69
+ loadConfig: () => structuredClone(savedRawConfig ?? rawConfig),
70
+ getConfig: () => structuredClone(savedRawConfig ?? rawConfig),
71
+ getConfigReadOnly: () => structuredClone(savedRawConfig ?? rawConfig),
72
+ getDeploymentContextDefaults: () => ({}),
73
+ getNestedValue: (obj: Record<string, unknown>, path: string) =>
74
+ path.split(".").reduce<unknown>((current, key) => {
75
+ if (
76
+ current === null ||
77
+ typeof current !== "object" ||
78
+ Array.isArray(current)
79
+ ) {
80
+ return undefined;
81
+ }
82
+ return (current as Record<string, unknown>)[key];
83
+ }, obj),
84
+ invalidateConfigCache: () => {},
85
+ mergeDefaultWorkspaceConfig: () => ({
86
+ merged: false,
87
+ config: structuredClone(savedRawConfig ?? rawConfig),
88
+ }),
89
+ setNestedValue,
90
+ withSuppressedConfigDiskWrites: async (fn: () => unknown) => fn(),
91
+ withSuppressedConfigDiskWritesSync: (fn: () => unknown) => fn(),
92
+ _writeQuarantineNotice: () => {},
93
+ }));
94
+
95
+ mock.module("../daemon/config-watcher.js", () => ({
96
+ getConfigWatcher: () => ({
97
+ suppressConfigReload: false,
98
+ timers: { schedule: () => {} },
99
+ updateFingerprint: () => {},
100
+ }),
101
+ }));
102
+
103
+ mock.module("../providers/registry.js", () => ({
104
+ clearConnectionProviderCache: () => {},
105
+ getProvider: () => {
106
+ throw new Error("provider registry mock not implemented");
107
+ },
108
+ getProviderRoutingSource: () => null,
109
+ initializeProviders: async () => {},
110
+ isNativeWebSearchCapableProvider: () => false,
111
+ listProviders: () => [],
112
+ resolveProviderFromConnection: async () => null,
113
+ }));
114
+
115
+ mock.module("../memory/embedding-backend.js", () => ({
116
+ EmbeddingBackendUnavailableError: class EmbeddingBackendUnavailableError extends Error {},
117
+ SPARSE_EMBEDDING_VERSION: 4,
118
+ clearEmbeddingBackendCache: () => {},
119
+ embedWithBackend: async () => ({
120
+ provider: "local",
121
+ model: "test",
122
+ vectors: [],
123
+ }),
124
+ generateSparseEmbedding: () => ({ indices: [], values: [] }),
125
+ getMemoryBackendStatus: async () => ({
126
+ enabled: false,
127
+ provider: null,
128
+ model: null,
129
+ }),
130
+ resetLocalEmbeddingFailureState: () => {},
131
+ selectEmbeddingBackend: async () => null,
132
+ selectedBackendSupportsMultimodal: async () => false,
133
+ }));
134
+
135
+ mock.module("../security/secret-allowlist.js", () => ({
136
+ isAllowlisted: () => false,
137
+ loadAllowlist: () => {},
138
+ resetAllowlist: () => {},
139
+ validateAllowlistFile: () => null,
140
+ }));
141
+
142
+ const { ROUTES } =
143
+ await import("../runtime/routes/conversation-query-routes.js");
144
+ const { BadRequestError } = await import("../runtime/routes/errors.js");
145
+
146
+ function findRoute(operationId: string) {
147
+ const route = ROUTES.find((r) => r.operationId === operationId);
148
+ if (!route) throw new Error(`Route not found: ${operationId}`);
149
+ return route;
150
+ }
151
+
152
+ const configGetRoute = findRoute("config_get");
153
+ const configPatchRoute = findRoute("config_patch");
154
+ const configSetRoute = findRoute("config_set");
155
+
156
+ describe("MCP config secret boundary", () => {
157
+ beforeEach(() => {
158
+ rawConfig = {};
159
+ savedRawConfig = null;
160
+ });
161
+
162
+ test("config_get omits legacy MCP transport headers from settings-read responses", () => {
163
+ rawConfig = {
164
+ mcp: {
165
+ servers: {
166
+ remote: {
167
+ transport: {
168
+ type: "streamable-http",
169
+ url: "https://mcp.example.com",
170
+ headers: {
171
+ Authorization: "Bearer mcp-secret",
172
+ "X-API-Key": "mcp-api-secret",
173
+ },
174
+ },
175
+ },
176
+ },
177
+ },
178
+ };
179
+
180
+ const result = configGetRoute.handler({}) as Record<string, unknown>;
181
+
182
+ expect(JSON.stringify(result)).not.toContain("mcp-secret");
183
+ expect(JSON.stringify(result)).not.toContain("mcp-api-secret");
184
+ const mcp = result.mcp as {
185
+ servers: { remote: { transport: Record<string, unknown> } };
186
+ };
187
+ expect(mcp.servers.remote.transport).toEqual({
188
+ type: "streamable-http",
189
+ url: "https://mcp.example.com",
190
+ });
191
+ });
192
+
193
+ test("config_get omits headers inside malformed MCP server trees", () => {
194
+ rawConfig = {
195
+ mcp: {
196
+ servers: [
197
+ {
198
+ transport: {
199
+ headers: { Authorization: "Bearer malformed-secret" },
200
+ },
201
+ },
202
+ ],
203
+ },
204
+ };
205
+
206
+ const result = configGetRoute.handler({}) as Record<string, unknown>;
207
+
208
+ expect(JSON.stringify(result)).not.toContain("malformed-secret");
209
+ expect(result).toEqual({
210
+ mcp: {
211
+ servers: [
212
+ {
213
+ transport: {},
214
+ },
215
+ ],
216
+ },
217
+ });
218
+ });
219
+
220
+ test("config_get preserves an MCP server named headers", () => {
221
+ rawConfig = {
222
+ mcp: {
223
+ servers: {
224
+ headers: {
225
+ transport: {
226
+ type: "streamable-http",
227
+ url: "https://mcp.example.com",
228
+ },
229
+ },
230
+ },
231
+ },
232
+ };
233
+
234
+ const result = configGetRoute.handler({}) as Record<string, unknown>;
235
+
236
+ expect(result).toEqual(rawConfig);
237
+ });
238
+
239
+ test("config_get preserves non-credential headers env vars", () => {
240
+ rawConfig = {
241
+ mcp: {
242
+ servers: {
243
+ local: {
244
+ transport: {
245
+ type: "stdio",
246
+ command: "npx",
247
+ env: {
248
+ headers: "not-a-transport-header",
249
+ },
250
+ },
251
+ },
252
+ },
253
+ },
254
+ };
255
+
256
+ const result = configGetRoute.handler({}) as Record<string, unknown>;
257
+
258
+ expect(result).toEqual(rawConfig);
259
+ });
260
+
261
+ test("config_patch rejects MCP transport headers so generic writes cannot reintroduce plaintext credentials", async () => {
262
+ await expect(
263
+ configPatchRoute.handler({
264
+ body: {
265
+ mcp: {
266
+ servers: {
267
+ remote: {
268
+ transport: {
269
+ type: "streamable-http",
270
+ url: "https://mcp.example.com",
271
+ headers: { Authorization: "Bearer mcp-secret" },
272
+ },
273
+ },
274
+ },
275
+ },
276
+ },
277
+ }),
278
+ ).rejects.toThrow(BadRequestError);
279
+ expect(savedRawConfig).toBeNull();
280
+ });
281
+
282
+ test("config_patch allows an MCP server named headers when its value has no header credentials", async () => {
283
+ const result = await configPatchRoute.handler({
284
+ body: {
285
+ mcp: {
286
+ servers: {
287
+ headers: {
288
+ transport: {
289
+ type: "streamable-http",
290
+ url: "https://mcp.example.com",
291
+ },
292
+ },
293
+ },
294
+ },
295
+ },
296
+ });
297
+
298
+ expect(result).toEqual({
299
+ mcp: {
300
+ servers: {
301
+ headers: {
302
+ transport: {
303
+ type: "streamable-http",
304
+ url: "https://mcp.example.com",
305
+ },
306
+ },
307
+ },
308
+ },
309
+ });
310
+ });
311
+
312
+ test("config_patch allows non-credential headers env vars", async () => {
313
+ const result = await configPatchRoute.handler({
314
+ body: {
315
+ mcp: {
316
+ servers: {
317
+ local: {
318
+ transport: {
319
+ type: "stdio",
320
+ command: "npx",
321
+ env: {
322
+ headers: "not-a-transport-header",
323
+ },
324
+ },
325
+ },
326
+ },
327
+ },
328
+ },
329
+ });
330
+
331
+ expect(result).toEqual({
332
+ mcp: {
333
+ servers: {
334
+ local: {
335
+ transport: {
336
+ type: "stdio",
337
+ command: "npx",
338
+ env: {
339
+ headers: "not-a-transport-header",
340
+ },
341
+ },
342
+ },
343
+ },
344
+ },
345
+ });
346
+ });
347
+
348
+ test("config_set rejects malformed MCP server trees containing headers", async () => {
349
+ await expect(
350
+ configSetRoute.handler({
351
+ body: {
352
+ path: "mcp.servers",
353
+ value: [
354
+ {
355
+ transport: {
356
+ headers: { Authorization: "Bearer malformed-secret" },
357
+ },
358
+ },
359
+ ],
360
+ },
361
+ }),
362
+ ).rejects.toThrow(BadRequestError);
363
+ expect(savedRawConfig).toBeNull();
364
+ });
365
+
366
+ test("config_set rejects direct MCP transport header paths", async () => {
367
+ rawConfig = {
368
+ mcp: {
369
+ servers: {
370
+ remote: {
371
+ transport: {
372
+ type: "streamable-http",
373
+ url: "https://mcp.example.com",
374
+ },
375
+ },
376
+ },
377
+ },
378
+ };
379
+
380
+ await expect(
381
+ configSetRoute.handler({
382
+ body: {
383
+ path: "mcp.servers.remote.transport.headers.Authorization",
384
+ value: "Bearer mcp-secret",
385
+ },
386
+ }),
387
+ ).rejects.toThrow(BadRequestError);
388
+ expect(savedRawConfig).toBeNull();
389
+ });
390
+ });
@@ -109,10 +109,13 @@ describe("reconcileFlagGatedProfiles", () => {
109
109
 
110
110
  const raw = readConfig();
111
111
  const osBeta = raw.llm.profiles["os-beta"]!;
112
- expect(osBeta.model).toBe("accounts/fireworks/models/glm-5p2");
113
- expect(osBeta.provider_connection).toBe("fireworks-managed");
112
+ expect(osBeta.model).toBe("MiniMaxAI/MiniMax-M3");
113
+ expect(osBeta.provider_connection).toBe("together-managed");
114
+ expect(osBeta.provider).toBe("together");
114
115
  expect(osBeta.source).toBe("managed");
115
116
  expect(osBeta.label).toBe("OS Beta");
117
+ expect(osBeta.effort).toBe("low");
118
+ expect(osBeta.topP).toBe(0.95);
116
119
 
117
120
  const order = raw.llm.profileOrder;
118
121
  expect(order.indexOf("os-beta")).toBe(order.indexOf("balanced") + 1);
@@ -128,6 +131,7 @@ describe("reconcileFlagGatedProfiles", () => {
128
131
  expect(osBeta.status).toBe("disabled");
129
132
  expect(osBeta.label).toBe("OS Beta (Managed)");
130
133
  expect(osBeta.source).toBe("managed");
134
+ expect(osBeta.effort).toBe("low");
131
135
  });
132
136
 
133
137
  test("flag on is idempotent across repeated runs", () => {
@@ -150,6 +154,7 @@ describe("reconcileFlagGatedProfiles", () => {
150
154
  raw.llm.profiles["os-beta"]!.label = "My OS Beta";
151
155
  raw.llm.profiles["os-beta"]!.status = "disabled";
152
156
  raw.llm.profiles["os-beta"]!.advisorEnabled = true;
157
+ raw.llm.profiles["os-beta"]!.topP = 0.8;
153
158
  writeConfig(raw);
154
159
  invalidateConfigCache();
155
160
 
@@ -159,7 +164,10 @@ describe("reconcileFlagGatedProfiles", () => {
159
164
  expect(after.label).toBe("My OS Beta");
160
165
  expect(after.status).toBe("disabled");
161
166
  expect(after.advisorEnabled).toBe(true);
162
- expect(after.model).toBe("accounts/fireworks/models/glm-5p2");
167
+ expect(after.topP).toBe(0.8);
168
+ expect(after.model).toBe("MiniMaxAI/MiniMax-M3");
169
+ expect(after.provider_connection).toBe("together-managed");
170
+ expect(after.effort).toBe("low");
163
171
  });
164
172
 
165
173
  test("flag off removes a managed os-beta and applies fallbacks", () => {
@@ -415,7 +415,7 @@
415
415
  "scope": "assistant",
416
416
  "key": "os-beta",
417
417
  "label": "OS Beta",
418
- "description": "Enable the OS Beta model profile (GLM 5.2 / Fireworks) in the assistant's model profile selection.",
418
+ "description": "Enable the OS Beta model profile (MiniMax M3 / Together) in the assistant's model profile selection.",
419
419
  "defaultEnabled": false
420
420
  }
421
421
  ]
@@ -164,19 +164,20 @@ export const OS_BETA_FEATURE_FLAG_KEY = "os-beta";
164
164
  * Flag-gated managed profile. NOT in MANAGED_PROFILE_TEMPLATES, so the
165
165
  * unconditional boot seed never creates it. Reconciled in/out by
166
166
  * the flag-gated profile reconcile based on the `os-beta` feature flag.
167
- * Balanced-parity defaults; GLM 5.2 pinned explicitly via `model`.
167
+ * Balanced defaults, with lower reasoning effort while the profile is in beta.
168
168
  */
169
169
  export const OS_BETA_PROFILE_TEMPLATE: ManagedProfileTemplate = {
170
- model: "accounts/fireworks/models/glm-5p2",
171
- provider: "fireworks",
172
- connectionName: "fireworks-managed",
170
+ intent: "balanced",
171
+ provider: "together",
172
+ connectionName: "together-managed",
173
173
  source: "managed",
174
174
  label: "OS Beta",
175
- description: "Open-source frontier model (GLM 5.2), in beta",
175
+ description: "Good balance of quality, cost, and speed, in beta",
176
176
  maxTokens: 32000,
177
- effort: "high",
177
+ effort: "low",
178
178
  thinking: { enabled: true, streamThinking: true },
179
179
  contextWindow: { maxInputTokens: DEFAULT_CONTEXT_WINDOW_MAX_INPUT_TOKENS },
180
+ topP: 0.95,
180
181
  };
181
182
 
182
183
  // Membership here marks a name as managed. The route layer applies managed
@@ -23,12 +23,12 @@ const log = getLogger("sync-gated-profiles");
23
23
  * Reconcile flag-gated managed profiles against the current feature-flag state.
24
24
  *
25
25
  * `seedInferenceProfiles()` runs synchronously at boot before feature flags are
26
- * available, so the OS Beta profile (GLM 5.2 / fireworks-managed) is materialized
27
- * here once flags have loaded. When the `os-beta` flag is on, the managed profile
28
- * is created (ordered right after `balanced`); when it is off, a previously
29
- * managed entry is removed with `profileOrder` / `activeProfile` / `advisorProfile`
30
- * fallbacks. The reconcile is idempotent and never touches a user-owned profile of
31
- * the same name.
26
+ * available, so the OS Beta profile (MiniMax M3 / together-managed) is
27
+ * materialized here once flags have loaded. When the `os-beta` flag is on, the
28
+ * managed profile is created (ordered right after `balanced`); when it is off, a
29
+ * previously managed entry is removed with `profileOrder` / `activeProfile` /
30
+ * `advisorProfile` fallbacks. The reconcile is idempotent and never touches a
31
+ * user-owned profile of the same name.
32
32
  *
33
33
  * Returns whether the on-disk config changed.
34
34
  */
@@ -105,23 +105,22 @@ function enableProfile(
105
105
  OS_BETA_PROFILE_TEMPLATE.connectionName,
106
106
  ) as Record<string, unknown>;
107
107
 
108
- // BYOK installs seed managed profiles disabled: the platform-auth
109
- // `fireworks-managed` connection backing this profile isn't usable until the
110
- // user enables it, so a fresh OS Beta entry starts disabled to avoid offering
111
- // an unusable route. A user's own status override (preserved below) wins on
112
- // later reconciles.
108
+ // BYOK installs seed managed profiles disabled: the managed inference
109
+ // connection backing this profile isn't usable until the user enables it, so a
110
+ // fresh OS Beta entry starts disabled to avoid offering an unusable route. A
111
+ // user's own status override (preserved below) wins on later reconciles.
113
112
  if (isByokMode && !previous) {
114
113
  next.status = "disabled";
115
114
  }
116
115
 
117
116
  if (previous) {
118
- // The only fields a user may override on a managed profile. Carry `label`
119
- // by key-presence so an explicit null (user cleared it) survives too.
117
+ // Preserve user-owned overrides across reconciles.
120
118
  if ("label" in previous) next.label = previous.label;
121
119
  if ("status" in previous) next.status = previous.status;
122
120
  if ("advisorEnabled" in previous) {
123
121
  next.advisorEnabled = previous.advisorEnabled;
124
122
  }
123
+ if ("topP" in previous) next.topP = previous.topP;
125
124
  }
126
125
 
127
126
  let changed = false;
@@ -486,6 +486,74 @@ function readPlainObject(value: unknown): Record<string, unknown> | undefined {
486
486
  return value as Record<string, unknown>;
487
487
  }
488
488
 
489
+ function stripTransportHeadersRecursively(value: unknown): void {
490
+ if (Array.isArray(value)) {
491
+ for (const item of value) {
492
+ stripTransportHeadersRecursively(item);
493
+ }
494
+ return;
495
+ }
496
+
497
+ const object = readPlainObject(value);
498
+ if (!object) return;
499
+ const transport = readPlainObject(object.transport);
500
+ if (transport) delete transport.headers;
501
+ for (const child of Object.values(object)) {
502
+ stripTransportHeadersRecursively(child);
503
+ }
504
+ }
505
+
506
+ function containsTransportHeadersRecursively(value: unknown): boolean {
507
+ if (Array.isArray(value)) {
508
+ return value.some((item) => containsTransportHeadersRecursively(item));
509
+ }
510
+
511
+ const object = readPlainObject(value);
512
+ if (!object) return false;
513
+ const transport = readPlainObject(object.transport);
514
+ if (transport && Object.hasOwn(transport, "headers")) return true;
515
+ return Object.values(object).some((child) =>
516
+ containsTransportHeadersRecursively(child),
517
+ );
518
+ }
519
+
520
+ function sanitizeMcpTransportHeadersForSettingsRead(config: unknown): void {
521
+ const root = readPlainObject(config);
522
+ if (!root) return;
523
+ const mcp = readPlainObject(root.mcp);
524
+ if (!mcp || !Object.hasOwn(mcp, "servers")) return;
525
+ if (Array.isArray(mcp.servers)) {
526
+ stripTransportHeadersRecursively(mcp.servers);
527
+ return;
528
+ }
529
+ const servers = readPlainObject(mcp.servers);
530
+ if (!servers) return;
531
+ for (const server of Object.values(servers)) {
532
+ stripTransportHeadersRecursively(server);
533
+ }
534
+ }
535
+
536
+ function patchContainsMcpTransportHeaders(patch: unknown): boolean {
537
+ const root = readPlainObject(patch);
538
+ const mcp = readPlainObject(root?.mcp);
539
+ if (!mcp || !Object.hasOwn(mcp, "servers")) return false;
540
+ if (Array.isArray(mcp.servers)) {
541
+ return containsTransportHeadersRecursively(mcp.servers);
542
+ }
543
+ const servers = readPlainObject(mcp.servers);
544
+ if (!servers) return false;
545
+ return Object.values(servers).some((server) =>
546
+ containsTransportHeadersRecursively(server),
547
+ );
548
+ }
549
+
550
+ function rejectMcpTransportHeaderWrite(patch: unknown): void {
551
+ if (!patchContainsMcpTransportHeaders(patch)) return;
552
+ throw new BadRequestError(
553
+ "MCP authentication headers must be managed through MCP server add/update APIs, not generic config writes.",
554
+ );
555
+ }
556
+
489
557
  const WireProfileEntry = ProfileEntry.extend({
490
558
  supportsVision: z.boolean().optional(),
491
559
  })
@@ -688,6 +756,7 @@ const ConfigPatchRequestSchema = z
688
756
  function handleGetConfig() {
689
757
  try {
690
758
  const config = applyContextDefaultsToRawConfig(loadRawConfig());
759
+ sanitizeMcpTransportHeadersForSettingsRead(config);
691
760
  enrichProfilesWithVisionFlag(config);
692
761
  return config;
693
762
  } catch (err) {
@@ -840,6 +909,7 @@ async function handlePatchConfig({ body }: RouteHandlerArgs) {
840
909
  throw new BadRequestError("Body must be a non-empty JSON object");
841
910
  }
842
911
  rejectManagedProfileDeletion(body as Record<string, unknown>);
912
+ rejectMcpTransportHeaderWrite(body);
843
913
 
844
914
  const raw = loadRawConfig();
845
915
  const patch = body as Record<string, unknown>;
@@ -848,6 +918,7 @@ async function handlePatchConfig({ body }: RouteHandlerArgs) {
848
918
  await commitConfigWrite(raw, "patch");
849
919
 
850
920
  const merged = applyContextDefaultsToRawConfig(loadRawConfig());
921
+ sanitizeMcpTransportHeadersForSettingsRead(merged);
851
922
  enrichProfilesWithVisionFlag(merged);
852
923
  return merged;
853
924
  }
@@ -892,6 +963,7 @@ async function handleSetConfig({ body }: RouteHandlerArgs) {
892
963
  const patchShape: Record<string, unknown> = {};
893
964
  setNestedValue(patchShape, path, value);
894
965
  rejectManagedProfileDeletion(patchShape);
966
+ rejectMcpTransportHeaderWrite(patchShape);
895
967
 
896
968
  const raw = loadRawConfig();
897
969
  setNestedValue(raw, path, value);