@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.
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/package.json +1 -1
- package/node_modules/@mycadet/ces-client/package.json +1 -1
- package/node_modules/@mycadet/ces-client/src/__tests__/ces-client.test.ts +14 -14
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/package.json +1 -1
- package/node_modules/@mycadet/service-contracts/package.json +1 -1
- package/openapi.yaml +2 -18
- package/package.json +2 -2
- package/src/__tests__/cloud-profile-guard.test.ts +21 -19
- package/src/__tests__/config-set-route.test.ts +12 -8
- package/src/__tests__/credential-health-service.test.ts +4 -2
- package/src/__tests__/get-skill-detail-audit.test.ts +134 -289
- package/src/__tests__/mcp-abort-signal.test.ts +0 -1
- package/src/__tests__/mcp-client-auth.test.ts +0 -1
- package/src/__tests__/mcp-config-secret-boundary.test.ts +44 -40
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +0 -1
- package/src/__tests__/plugin-api-store-credential.test.ts +4 -1
- package/src/__tests__/skills-file-content-endpoint.test.ts +221 -535
- package/src/__tests__/skills-files-catalog-fallback.test.ts +258 -576
- package/src/api/events/show-cloud-login.ts +2 -2
- package/src/cli/commands/cloud/__tests__/callback-routes-list.test.ts +2 -2
- package/src/cli/commands/cloud/index.ts +1 -1
- package/src/cli/commands/config.ts +1 -1
- package/src/config/__tests__/substrate-twin-shadowing.test.ts +36 -26
- package/src/config/env-registry.ts +2 -9
- package/src/config/schemas/ingress.ts +30 -0
- package/src/config/schemas/llm.ts +0 -9
- package/src/config/seed-inference-profiles.ts +2 -2
- package/src/config/substrate-twin-shadowing.ts +12 -17
- package/src/config/sync-gated-profiles.ts +3 -4
- package/src/credential-execution/prompted-credential.ts +1 -10
- package/src/credential-health/credential-health-service.ts +8 -4
- package/src/daemon/credential-transcript-scrub.ts +11 -0
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +5 -0
- package/src/providers/inference/backfill.ts +2 -2
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +100 -57
- package/src/runtime/routes/cloud-routes.ts +5 -5
- package/src/runtime/routes/conversation-query-routes.ts +275 -397
- package/src/runtime/routes/secret-routes.ts +2 -17
- package/src/tools/credentials/store.ts +1 -10
- package/src/util/layout.ts +479 -0
- package/src/util/platform.ts +11 -467
- package/src/workspace/migrations/127-backfill-default-provider.ts +2 -2
|
@@ -1,241 +1,129 @@
|
|
|
1
|
-
|
|
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 {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
//
|
|
43
|
+
// Workspace seeding — the real loader resolves these
|
|
60
44
|
// ---------------------------------------------------------------------------
|
|
61
45
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
206
|
-
|
|
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
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
risk: "
|
|
232
|
-
|
|
233
|
-
|
|
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("
|
|
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("
|
|
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
|
|
267
|
-
|
|
268
|
-
|
|
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
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
-
//
|
|
317
|
-
expect(
|
|
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
|
|
322
|
-
//
|
|
323
|
-
|
|
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
|
|
381
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
224
|
-
await
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
)
|
|
241
|
-
|
|
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
|
|
267
|
-
await
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
)
|
|
282
|
-
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
expect(result).toEqual({ ok: true });
|
|
286
|
+
expect(committedRaw()?.mcp).toBeUndefined();
|
|
283
287
|
});
|
|
284
288
|
|
|
285
|
-
test("config_set
|
|
286
|
-
await
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
).
|
|
294
|
-
expect(committedRaw()).
|
|
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 () => {
|