@mycadet/assistant 0.12.8 → 0.12.9

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 (42) hide show
  1. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/package.json +1 -1
  2. package/node_modules/@mycadet/ces-client/package.json +1 -1
  3. package/node_modules/@mycadet/ces-client/src/__tests__/ces-client.test.ts +14 -14
  4. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/package.json +1 -1
  5. package/node_modules/@mycadet/service-contracts/package.json +1 -1
  6. package/openapi.yaml +2 -18
  7. package/package.json +2 -2
  8. package/src/__tests__/cloud-profile-guard.test.ts +21 -19
  9. package/src/__tests__/config-set-route.test.ts +12 -8
  10. package/src/__tests__/credential-health-service.test.ts +4 -2
  11. package/src/__tests__/get-skill-detail-audit.test.ts +134 -289
  12. package/src/__tests__/mcp-abort-signal.test.ts +0 -1
  13. package/src/__tests__/mcp-client-auth.test.ts +0 -1
  14. package/src/__tests__/mcp-config-secret-boundary.test.ts +44 -40
  15. package/src/__tests__/mcp-tool-annotations-risk.test.ts +0 -1
  16. package/src/__tests__/plugin-api-store-credential.test.ts +4 -1
  17. package/src/__tests__/skills-file-content-endpoint.test.ts +221 -535
  18. package/src/__tests__/skills-files-catalog-fallback.test.ts +258 -576
  19. package/src/api/events/show-cloud-login.ts +2 -2
  20. package/src/cli/commands/cloud/__tests__/callback-routes-list.test.ts +2 -2
  21. package/src/cli/commands/cloud/index.ts +1 -1
  22. package/src/cli/commands/config.ts +1 -1
  23. package/src/config/__tests__/substrate-twin-shadowing.test.ts +36 -26
  24. package/src/config/env-registry.ts +2 -9
  25. package/src/config/schemas/ingress.ts +30 -0
  26. package/src/config/schemas/llm.ts +0 -9
  27. package/src/config/seed-inference-profiles.ts +2 -2
  28. package/src/config/substrate-twin-shadowing.ts +12 -17
  29. package/src/config/sync-gated-profiles.ts +3 -4
  30. package/src/credential-execution/prompted-credential.ts +1 -10
  31. package/src/credential-health/credential-health-service.ts +8 -4
  32. package/src/daemon/credential-transcript-scrub.ts +11 -0
  33. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +5 -0
  34. package/src/providers/inference/backfill.ts +2 -2
  35. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +100 -57
  36. package/src/runtime/routes/cloud-routes.ts +5 -5
  37. package/src/runtime/routes/conversation-query-routes.ts +275 -397
  38. package/src/runtime/routes/secret-routes.ts +2 -17
  39. package/src/tools/credentials/store.ts +1 -10
  40. package/src/util/layout.ts +479 -0
  41. package/src/util/platform.ts +11 -467
  42. package/src/workspace/migrations/127-backfill-default-provider.ts +2 -2
@@ -1,241 +1,129 @@
1
- import { beforeEach, describe, expect, mock, test } from "bun:test";
1
+ /**
2
+ * Tests for `getSkill` — audit enrichment + lineage, zero-leak.
3
+ *
4
+ * Coverage:
5
+ * - Enriches skillssh skill detail with audit data on success.
6
+ * - Returns detail without audit data when the audit fetch fails (non-fatal).
7
+ * - Attributes a plugin-resident skill to its owning plugin.
8
+ * - Includes sourceConversationId when install-meta records it.
9
+ * - Omits sourceConversationId when install-meta has none.
10
+ * - Never exposes lineage on non-assistant-memory origins.
11
+ *
12
+ * Zero-leak: the REAL catalog loader resolves skills seeded into the
13
+ * workspace (a managed skills dir entry, a real plugin directory with a
14
+ * package.json manifest, each carrying a real `install-meta.json`), and the
15
+ * REAL `fetchSkillAudits` runs its HTTP fetch against the requests-mock
16
+ * transport. The former mock asserted the audit call args; these tests
17
+ * assert the real audit URL. The skillssh detail ids are one-segment
18
+ * workspace skill ids with the slash slug living in install-meta.json —
19
+ * through the real loader the id is the on-disk directory name, and the
20
+ * registry slug is install-meta state.
21
+ */
22
+
23
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
24
+ import { join } from "node:path";
25
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
26
+
27
+ import { setAllFlagsOnForTesting } from "./feature-flag-test-helpers.js";
28
+ import { setConfig } from "./helpers/set-config.js";
29
+ import { mockHttp } from "./requests-mock.js";
30
+
31
+ // Real env: the real getCloudBaseUrl() reads config.cloud.baseUrl — point it
32
+ // at a host mockHttp matches by path substring.
33
+ setConfig("cloud", { baseUrl: "https://test-cloud.mycadet.ai" });
2
34
 
3
- import { setAllFlagsOnForTesting } from "../__tests__/feature-flag-test-helpers.js";
4
-
5
- // ---------------------------------------------------------------------------
6
- // Mock state
7
- // ---------------------------------------------------------------------------
8
-
9
- const mockResolveSkillStates = mock(
10
- (): Array<{
11
- summary: {
12
- id: string;
13
- displayName: string;
14
- description: string;
15
- emoji?: string;
16
- source:
17
- | "bundled"
18
- | "managed"
19
- | "workspace"
20
- | "extra"
21
- | "catalog"
22
- | "plugin";
23
- directoryPath: string;
24
- owner?: { kind: "skill" | "mcp" | "plugin" | "workspace"; id: string };
25
- };
26
- state: "enabled" | "disabled";
27
- }> => [],
28
- );
29
-
30
- const mockReadInstallMeta = mock(
31
- (
32
- _dir: string,
33
- ): {
34
- origin: string;
35
- slug?: string;
36
- sourceRepo?: string;
37
- installedAt?: string;
38
- author?: "assistant" | "user";
39
- sourceConversationId?: string;
40
- } | null => null,
41
- );
42
-
43
- const mockFetchSkillAudits = mock(
44
- async (
45
- _source: string,
46
- _skillSlugs: string[],
47
- ): Promise<
48
- Record<
49
- string,
50
- Record<
51
- string,
52
- { risk: string; alerts?: number; score?: number; analyzedAt: string }
53
- >
54
- >
55
- > => ({}),
56
- );
35
+ import { getSkill } from "../daemon/handlers/skills.js";
36
+ import { invalidateCatalogCache } from "../skills/catalog-cache.js";
37
+ import {
38
+ getWorkspacePluginsDir,
39
+ getWorkspaceSkillsDir,
40
+ } from "../util/layout.js";
57
41
 
58
42
  // ---------------------------------------------------------------------------
59
- // Mock modules — before importing module under test
43
+ // Workspace seeding — the real loader resolves these
60
44
  // ---------------------------------------------------------------------------
61
45
 
62
- mock.module("../config/skills.js", () => ({
63
- loadSkillCatalog: () => [],
64
- }));
65
-
66
- mock.module("../config/skill-state.js", () => ({
67
- resolveSkillStates: mockResolveSkillStates,
68
- skillFlagKey: () => null,
69
- }));
70
-
71
- mock.module("../skills/install-meta.js", () => ({
72
- readInstallMeta: mockReadInstallMeta,
73
- }));
74
-
75
- mock.module("../skills/skillssh-registry.js", () => ({
76
- searchSkillsRegistry: mock(async () => []),
77
- fetchSkillAudits: mockFetchSkillAudits,
78
- // resolveSkillSource uses the real implementation — its pure parsing
79
- // logic is reliable and doesn't need mocking. The handler calls it to
80
- // derive owner/repo/skillSlug from the slug string before calling
81
- // fetchSkillAudits, so as long as fetchSkillAudits receives the right
82
- // args (asserted below), we know resolveSkillSource did its job.
83
- resolveSkillSource: (source: string) => {
84
- const parts = source.split("/");
85
- return { owner: parts[0], repo: parts[1], skillSlug: parts[2] };
86
- },
87
- installExternalSkill: mock(async () => {}),
88
- }));
89
-
90
- mock.module("../skills/clawhub.js", () => ({
91
- clawhubSearch: mock(async () => ({ skills: [] })),
92
- clawhubCheckUpdates: mock(async () => []),
93
- clawhubInspect: mock(async () => ({})),
94
- clawhubInstall: mock(async () => ({ success: true })),
95
- clawhubUpdate: mock(async () => ({ success: true })),
96
- }));
97
-
98
- mock.module("../skills/catalog-search.js", () => ({
99
- filterByQuery: () => [],
100
- }));
101
-
102
- mock.module("../providers/provider-send-message.js", () => ({
103
- createTimeout: () => ({
104
- signal: AbortSignal.timeout(1000),
105
- cleanup: () => {},
106
- }),
107
- extractText: () => "",
108
- getConfiguredProvider: async () => null,
109
- userMessage: () => ({}),
110
- }));
111
-
112
- mock.module("../runtime/routes/workspace-utils.js", () => ({
113
- isTextMimeType: () => true,
114
- }));
115
-
116
- mock.module("../skills/catalog-cache.js", () => ({
117
- getCatalog: async () => [],
118
- }));
119
-
120
- mock.module("../skills/catalog-files.js", () => ({
121
- catalogSkillToSlim: () => ({}),
122
- createCadetCatalogProvider: () => ({
123
- canHandle: () => false,
124
- listFiles: async () => null,
125
- toSlimSkill: async () => null,
126
- readFileContent: async () => null,
127
- }),
128
- hasHiddenOrSkippedSegment: () => false,
129
- sanitizeRelativePath: (p: string) => p,
130
- SKIP_DIRS: new Set(),
131
- }));
132
-
133
- mock.module("../skills/skillssh-files.js", () => ({
134
- createSkillsShProvider: () => ({
135
- canHandle: () => false,
136
- listFiles: async () => null,
137
- toSlimSkill: async () => null,
138
- readFileContent: async () => null,
139
- }),
140
- }));
141
-
142
- mock.module("../skills/clawhub-files.js", () => ({
143
- createClawhubProvider: () => ({
144
- canHandle: () => false,
145
- listFiles: async () => null,
146
- toSlimSkill: async () => null,
147
- readFileContent: async () => null,
148
- }),
149
- }));
150
-
151
- mock.module("../skills/catalog-install.js", () => ({
152
- installSkillLocally: async () => {},
153
- }));
154
-
155
- mock.module("../skills/cloud-store.js", () => ({
156
- createCloudSkill: () => ({ created: true }),
157
- deleteCloudSkill: () => ({ deleted: true }),
158
- validateCloudSkillId: () => null,
159
- }));
160
-
161
- mock.module("../plugins/defaults/memory/graph/capability-seed.js", () => ({
162
- deleteSkillCapabilityNode: () => {},
163
- seedSkillGraphNodes: () => {},
164
- seedUninstalledCatalogSkillMemories: async () => {},
165
- }));
166
-
167
- mock.module("../daemon/handlers/shared.js", () => ({
168
- CONFIG_RELOAD_DEBOUNCE_MS: 100,
169
- ensureSkillEntry: () => ({}),
170
- log: {
171
- info: () => {},
172
- warn: () => {},
173
- error: () => {},
174
- debug: () => {},
175
- },
176
- }));
177
-
178
- // Import after mocking
179
- import { getSkill } from "../daemon/handlers/skills.js";
46
+ const seededDirs: string[] = [];
47
+
48
+ /** Seed a managed skill (workspace skills dir entry) with an install-meta.json. */
49
+ function seedManagedSkill(
50
+ id: string,
51
+ installMeta: Record<string, unknown> | null,
52
+ ): string {
53
+ const dir = join(getWorkspaceSkillsDir(), id);
54
+ mkdirSync(dir, { recursive: true });
55
+ writeFileSync(
56
+ join(dir, "SKILL.md"),
57
+ `---\nname: ${id}\ndescription: seeded test skill\n---\n# ${id}\n`,
58
+ );
59
+ if (installMeta !== null) {
60
+ writeFileSync(
61
+ join(dir, "install-meta.json"),
62
+ JSON.stringify(installMeta, null, 2),
63
+ );
64
+ }
65
+ seededDirs.push(dir);
66
+ return dir;
67
+ }
68
+
69
+ /** Seed a real plugin directory (package.json manifest + one resident skill). */
70
+ function seedPluginWithSkill(pluginId: string, skillId: string): string {
71
+ const pluginDir = join(getWorkspacePluginsDir(), pluginId);
72
+ const skillDir = join(pluginDir, "skills", skillId);
73
+ mkdirSync(skillDir, { recursive: true });
74
+ writeFileSync(
75
+ join(pluginDir, "package.json"),
76
+ JSON.stringify({ name: pluginId, version: "1.0.0" }),
77
+ );
78
+ writeFileSync(
79
+ join(skillDir, "SKILL.md"),
80
+ `---\nname: ${skillId}\ndescription: seeded plugin skill\n---\n# ${skillId}\n`,
81
+ );
82
+ seededDirs.push(pluginDir);
83
+ return pluginDir;
84
+ }
85
+
86
+ beforeEach(() => {
87
+ setAllFlagsOnForTesting();
88
+ invalidateCatalogCache();
89
+ });
180
90
 
181
- // ---------------------------------------------------------------------------
182
- // Helpers
183
- // ---------------------------------------------------------------------------
91
+ afterEach(() => {
92
+ for (const dir of seededDirs) {
93
+ rmSync(dir, { recursive: true, force: true });
94
+ }
95
+ seededDirs.length = 0;
96
+ });
184
97
 
185
98
  // ---------------------------------------------------------------------------
186
99
  // Tests
187
100
  // ---------------------------------------------------------------------------
188
101
 
189
102
  describe("getSkill — skillssh audit enrichment", () => {
190
- beforeEach(() => {
191
- // Real flag resolution replaces the former blanket module mock.
192
- setAllFlagsOnForTesting();
193
-
194
- mockResolveSkillStates.mockReset();
195
- mockReadInstallMeta.mockReset();
196
- mockFetchSkillAudits.mockReset();
197
-
198
- // Default: no skills resolved
199
- mockResolveSkillStates.mockReturnValue([]);
200
- mockReadInstallMeta.mockReturnValue(null);
201
- mockFetchSkillAudits.mockResolvedValue({});
202
- });
203
-
204
103
  test("enriches skillssh skill detail with audit data on success", async () => {
205
- // Set up a skillssh installed skill
206
- mockResolveSkillStates.mockReturnValue([
207
- {
208
- summary: {
209
- id: "acme/tools/lint",
210
- displayName: "Lint",
211
- description: "A lint skill",
212
- source: "extra" as const,
213
- directoryPath: "/tmp/test-skills/lint",
214
- },
215
- state: "enabled" as const,
216
- },
217
- ]);
218
-
219
- // Return skillssh origin from install-meta
220
- mockReadInstallMeta.mockReturnValue({
104
+ const http = mockHttp();
105
+ seedManagedSkill("lint-skill", {
221
106
  origin: "skillssh",
222
107
  slug: "acme/tools/lint",
223
108
  sourceRepo: "acme/tools",
224
109
  });
225
-
226
- // fetchSkillAudits returns audit data keyed by skill slug
227
- mockFetchSkillAudits.mockResolvedValue({
228
- lint: {
229
- ath: { risk: "safe", score: 100, analyzedAt: "2025-06-01T00:00:00Z" },
230
- socket: {
231
- risk: "low",
232
- alerts: 1,
233
- analyzedAt: "2025-06-01T00:00:00Z",
110
+ // The real fetchSkillAudits runs its HTTP fetch; the audit service
111
+ // response is keyed by skill slug.
112
+ http.on("GET", "add-skill.vercel.sh/audit", {
113
+ status: 200,
114
+ body: {
115
+ lint: {
116
+ ath: { risk: "safe", score: 100, analyzedAt: "2025-06-01T00:00:00Z" },
117
+ socket: {
118
+ risk: "low",
119
+ alerts: 1,
120
+ analyzedAt: "2025-06-01T00:00:00Z",
121
+ },
234
122
  },
235
123
  },
236
124
  });
237
125
 
238
- const result = await getSkill("acme/tools/lint");
126
+ const result = await getSkill("lint-skill");
239
127
 
240
128
  // Should succeed
241
129
  expect("skill" in result).toBe(true);
@@ -245,8 +133,7 @@ describe("getSkill — skillssh audit enrichment", () => {
245
133
 
246
134
  const detail = result.skill;
247
135
  expect(detail.origin).toBe("skillssh");
248
- expect(detail.id).toBe("acme/tools/lint");
249
- expect(detail.name).toBe("Lint");
136
+ expect(detail.id).toBe("lint-skill");
250
137
 
251
138
  // Verify audit data is attached
252
139
  if (detail.origin === "skillssh") {
@@ -263,39 +150,29 @@ describe("getSkill — skillssh audit enrichment", () => {
263
150
  });
264
151
  }
265
152
 
266
- // Verify fetchSkillAudits was called with the correct source repo and slug
267
- expect(mockFetchSkillAudits).toHaveBeenCalledTimes(1);
268
- expect(mockFetchSkillAudits).toHaveBeenCalledWith("acme/tools", ["lint"]);
153
+ // Verify the real audit fetch targeted the install-meta source repo and
154
+ // the registry slug's last segment.
155
+ const auditCall = http.lastCall("GET");
156
+ expect(auditCall).toBeDefined();
157
+ expect(auditCall!.url).toContain("add-skill.vercel.sh/audit");
158
+ expect(auditCall!.url).toContain("source=acme%2Ftools");
159
+ expect(auditCall!.url).toContain("skills=lint");
269
160
  });
270
161
 
271
- test("returns detail without audit data when fetchSkillAudits throws", async () => {
272
- // Set up a skillssh installed skill
273
- mockResolveSkillStates.mockReturnValue([
274
- {
275
- summary: {
276
- id: "org/repo/my-tool",
277
- displayName: "My Tool",
278
- description: "A community tool",
279
- source: "extra" as const,
280
- directoryPath: "/tmp/test-skills/my-tool",
281
- },
282
- state: "enabled" as const,
283
- },
284
- ]);
285
-
286
- // Return skillssh origin from install-meta
287
- mockReadInstallMeta.mockReturnValue({
162
+ test("returns detail without audit data when the audit fetch fails", async () => {
163
+ const http = mockHttp();
164
+ seedManagedSkill("my-tool", {
288
165
  origin: "skillssh",
289
166
  slug: "org/repo/my-tool",
290
167
  sourceRepo: "org/repo",
291
168
  });
169
+ // The real fetchSkillAudits throws on a non-2xx; the handler treats
170
+ // audit failure as non-fatal.
171
+ http.on("GET", "add-skill.vercel.sh/audit", {
172
+ status: 500,
173
+ });
292
174
 
293
- // fetchSkillAudits throws an error
294
- mockFetchSkillAudits.mockRejectedValue(
295
- new Error("audit service unavailable"),
296
- );
297
-
298
- const result = await getSkill("org/repo/my-tool");
175
+ const result = await getSkill("my-tool");
299
176
 
300
177
  // Should still succeed — audit failure is non-fatal
301
178
  expect("skill" in result).toBe(true);
@@ -305,41 +182,28 @@ describe("getSkill — skillssh audit enrichment", () => {
305
182
 
306
183
  const detail = result.skill;
307
184
  expect(detail.origin).toBe("skillssh");
308
- expect(detail.id).toBe("org/repo/my-tool");
309
- expect(detail.name).toBe("My Tool");
185
+ expect(detail.id).toBe("my-tool");
310
186
 
311
187
  // Audit data should not be present
312
188
  if (detail.origin === "skillssh") {
313
189
  expect(detail.audit).toBeUndefined();
314
190
  }
315
191
 
316
- // fetchSkillAudits was still called (just failed)
317
- expect(mockFetchSkillAudits).toHaveBeenCalledTimes(1);
192
+ // The audit fetch was still attempted (and failed)
193
+ expect(http.callCount("GET")).toBe(1);
318
194
  });
319
195
 
320
196
  test("attributes a plugin-resident skill to its owning plugin on the detail response", async () => {
321
- // GIVEN a skill shipped inside an installed plugin, resolved with a
322
- // `plugin` source and a plugin owner descriptor
323
- mockResolveSkillStates.mockReturnValue([
324
- {
325
- summary: {
326
- id: "caveman",
327
- displayName: "Caveman",
328
- description: "Ultra-compressed communication mode",
329
- source: "plugin" as const,
330
- directoryPath: "/tmp/test-skills/plugins/caveman/skills/caveman",
331
- owner: { kind: "plugin", id: "caveman" },
332
- },
333
- state: "enabled" as const,
334
- },
335
- ]);
197
+ // GIVEN a skill shipped inside a real plugin directory (package.json
198
+ // manifest + skills/<id>/SKILL.md), discovered by the real loader
199
+ seedPluginWithSkill("caveman", "caveman");
336
200
 
337
201
  // WHEN we fetch its detail
338
202
  const result = await getSkill("caveman");
339
203
 
340
204
  // THEN it resolves to the cadet origin (plugin skills are mapped to
341
205
  // bundled/cadet for older clients) while preserving plugin attribution
342
- // on the owner descriptor
206
+ // on the owner descriptor the real loader attached
343
207
  expect("skill" in result).toBe(true);
344
208
  if (!("skill" in result)) {
345
209
  throw new Error("Expected skill response");
@@ -356,29 +220,10 @@ describe("getSkill — skillssh audit enrichment", () => {
356
220
  });
357
221
 
358
222
  describe("getSkill — assistant-memory conversation lineage", () => {
359
- beforeEach(() => {
360
- mockResolveSkillStates.mockReset();
361
- mockReadInstallMeta.mockReset();
362
- mockFetchSkillAudits.mockReset();
363
-
364
- mockResolveSkillStates.mockReturnValue([
365
- {
366
- summary: {
367
- id: "triage-inbox",
368
- displayName: "Triage Inbox",
369
- description: "Distilled inbox triage procedure",
370
- source: "managed" as const,
371
- directoryPath: "/tmp/test-skills/triage-inbox",
372
- },
373
- state: "enabled" as const,
374
- },
375
- ]);
376
- mockFetchSkillAudits.mockResolvedValue({});
377
- });
378
-
379
223
  test("includes sourceConversationId when install-meta records it", async () => {
380
- // GIVEN a retrospective-authored skill whose install-meta carries lineage
381
- mockReadInstallMeta.mockReturnValue({
224
+ // GIVEN a retrospective-authored skill whose real install-meta.json
225
+ // carries lineage
226
+ seedManagedSkill("triage-inbox", {
382
227
  origin: "custom",
383
228
  author: "assistant",
384
229
  sourceConversationId: "conv-123",
@@ -401,7 +246,7 @@ describe("getSkill — assistant-memory conversation lineage", () => {
401
246
 
402
247
  test("omits sourceConversationId when install-meta has none", async () => {
403
248
  // GIVEN a retrospective-authored skill without recorded lineage
404
- mockReadInstallMeta.mockReturnValue({
249
+ seedManagedSkill("triage-inbox", {
405
250
  origin: "custom",
406
251
  author: "assistant",
407
252
  installedAt: "2026-01-01T00:00:00Z",
@@ -422,7 +267,7 @@ describe("getSkill — assistant-memory conversation lineage", () => {
422
267
  test("never exposes lineage on non-assistant-memory origins", async () => {
423
268
  // GIVEN a user-installed custom skill that (unexpectedly) carries a
424
269
  // sourceConversationId in install-meta
425
- mockReadInstallMeta.mockReturnValue({
270
+ seedManagedSkill("triage-inbox", {
426
271
  origin: "custom",
427
272
  sourceConversationId: "conv-123",
428
273
  installedAt: "2026-01-01T00:00:00Z",
@@ -11,7 +11,6 @@ mock.module("../config/env-registry.js", () => ({
11
11
  getDebugStdoutLogs: () => false,
12
12
  getIsContainerized: () => false,
13
13
  getIsCloud: () => false,
14
- isCloudRemote: () => false,
15
14
  getCpuLimit: () => undefined,
16
15
  getMinikubeStorageSize: () => undefined,
17
16
  getProfilerRunId: () => undefined,
@@ -11,7 +11,6 @@ mock.module("../config/env-registry.js", () => ({
11
11
  getDebugStdoutLogs: () => false,
12
12
  getIsContainerized: () => false,
13
13
  getIsCloud: () => false,
14
- isCloudRemote: () => false,
15
14
  getCpuLimit: () => undefined,
16
15
  getMinikubeStorageSize: () => undefined,
17
16
  getProfilerRunId: () => undefined,
@@ -88,7 +88,6 @@ mock.module("../persistence/embeddings/embedding-backend.js", () => ({
88
88
 
89
89
  const { ROUTES } =
90
90
  await import("../runtime/routes/conversation-query-routes.js");
91
- const { BadRequestError } = await import("../runtime/routes/errors.js");
92
91
 
93
92
  function findRoute(operationId: string) {
94
93
  const route = ROUTES.find((r) => r.operationId === operationId);
@@ -220,25 +219,30 @@ describe("MCP config secret boundary", () => {
220
219
  });
221
220
  });
222
221
 
223
- test("config_patch rejects MCP transport headers so generic writes cannot reintroduce plaintext credentials", async () => {
224
- await expect(
225
- configPatchRoute.handler({
226
- body: {
227
- mcp: {
228
- servers: {
229
- remote: {
230
- transport: {
231
- type: "streamable-http",
232
- url: "https://mcp.example.com",
233
- headers: { Authorization: "Bearer mcp-secret" },
234
- },
222
+ test("config_patch strips MCP transport headers so generic writes cannot persist plaintext credentials", async () => {
223
+ const result = await configPatchRoute.handler({
224
+ body: {
225
+ mcp: {
226
+ servers: {
227
+ remote: {
228
+ transport: {
229
+ type: "streamable-http",
230
+ url: "https://mcp.example.com",
231
+ headers: { Authorization: "Bearer mcp-secret" },
235
232
  },
236
233
  },
237
234
  },
238
235
  },
239
- }),
240
- ).rejects.toThrow(BadRequestError);
241
- expect(committedRaw()).toBeNull();
236
+ },
237
+ });
238
+
239
+ // `mcp` is not an AssistantConfigSchema key: the write's schema parse
240
+ // strips it, so the secret never reaches disk — and the response's
241
+ // projected workspace view carries no such server either.
242
+ expect(committedRaw()?.mcp).toBeUndefined();
243
+ expect(
244
+ (result as { mcp: { servers: Record<string, unknown> } }).mcp.servers,
245
+ ).toEqual({});
242
246
  });
243
247
 
244
248
  test("config_patch strips mcp so a full-config round trip cannot write it back", async () => {
@@ -263,34 +267,34 @@ describe("MCP config secret boundary", () => {
263
267
  ).toEqual({});
264
268
  });
265
269
 
266
- test("config_set rejects MCP server writes", async () => {
267
- await expect(
268
- configSetRoute.handler({
269
- body: {
270
- path: "mcp.servers",
271
- value: {
272
- remote: {
273
- transport: {
274
- type: "streamable-http",
275
- url: "https://mcp.example.com",
276
- },
270
+ test("config_set drops MCP server writes", async () => {
271
+ const result = await configSetRoute.handler({
272
+ body: {
273
+ path: "mcp.servers",
274
+ value: {
275
+ remote: {
276
+ transport: {
277
+ type: "streamable-http",
278
+ url: "https://mcp.example.com",
277
279
  },
278
280
  },
279
281
  },
280
- }),
281
- ).rejects.toThrow(BadRequestError);
282
- expect(committedRaw()).toBeNull();
282
+ },
283
+ });
284
+
285
+ expect(result).toEqual({ ok: true });
286
+ expect(committedRaw()?.mcp).toBeUndefined();
283
287
  });
284
288
 
285
- test("config_set rejects direct MCP transport header paths", async () => {
286
- await expect(
287
- configSetRoute.handler({
288
- body: {
289
- path: "mcp.servers.remote.transport.headers.Authorization",
290
- value: "Bearer mcp-secret",
291
- },
292
- }),
293
- ).rejects.toThrow(BadRequestError);
294
- expect(committedRaw()).toBeNull();
289
+ test("config_set drops direct MCP transport header paths", async () => {
290
+ const result = await configSetRoute.handler({
291
+ body: {
292
+ path: "mcp.servers.remote.transport.headers.Authorization",
293
+ value: "Bearer mcp-secret",
294
+ },
295
+ });
296
+
297
+ expect(result).toEqual({ ok: true });
298
+ expect(committedRaw()?.mcp).toBeUndefined();
295
299
  });
296
300
  });
@@ -11,7 +11,6 @@ mock.module("../config/env-registry.js", () => ({
11
11
  getDebugStdoutLogs: () => false,
12
12
  getIsContainerized: () => false,
13
13
  getIsCloud: () => false,
14
- isCloudRemote: () => false,
15
14
  getCpuLimit: () => undefined,
16
15
  getMinikubeStorageSize: () => undefined,
17
16
  getProfilerRunId: () => undefined,
@@ -198,7 +198,10 @@ describe("storeCredential", () => {
198
198
 
199
199
  test("scrubs the stored value from recent transcripts", async () => {
200
200
  await asPlugin(() => storeCredential("acme/api_key", "sk-secret"));
201
- expect(scrubSpy).toHaveBeenCalledWith("sk-secret");
201
+ expect(scrubSpy).toHaveBeenCalledWith("sk-secret", {
202
+ service: "acme",
203
+ field: "api_key",
204
+ });
202
205
  });
203
206
 
204
207
  test("skips the transcript scrub when asked", async () => {