From 8b662ae4bbd5d83c7ae8af3ec65ef03a01c9241f Mon Sep 17 00:00:00 2001 From: Sparkle <2705263765@qq.com> Date: Thu, 15 Oct 2026 11:43:26 +0800 Subject: [PATCH] fix(subagents): block both filename and declared name of a rejected agent type A role file whose `name` does not match its filename only blocked the declared name, so `implementer.md` with a misspelled `name: implementor` fell back to the write-capable built-in implementer. Reject both spellings at that precedence layer so a typo cannot widen a role meant to be narrowed. Co-Authored-By: Claude Opus 4.8 --- extensions/subagents/src/agent-types.ts | 15 ++++---- .../extensions/subagents/agent-types.test.ts | 36 +++++++++++++++++++ 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/extensions/subagents/src/agent-types.ts b/extensions/subagents/src/agent-types.ts index 7c52202aa..76b110ff9 100644 --- a/extensions/subagents/src/agent-types.ts +++ b/extensions/subagents/src/agent-types.ts @@ -281,8 +281,8 @@ export type ParseAgentTypeResult = } | { readonly agentType?: undefined; - /** A malformed higher-precedence file blocks fallback to this name. */ - readonly rejectedName: string; + /** A malformed higher-precedence file blocks fallback to these names. */ + readonly rejectedNames: readonly string[]; readonly diagnostics: AgentTypeDiagnostic[]; }; @@ -298,9 +298,9 @@ export function parseAgentType( source = stem, ): ParseAgentTypeResult { const diagnostics: AgentTypeDiagnostic[] = []; - let rejectedName = stem; + const rejectedNames = [stem]; const fail = (message: string) => ({ - rejectedName, + rejectedNames, diagnostics: [...diagnostics, { source, message }], }); if (Buffer.byteLength(content, "utf8") > AGENT_TYPE_LIMITS.fileBytes) { @@ -331,8 +331,9 @@ export function parseAgentType( ); } // Once a safe declared name is known, a malformed/misnamed file must block - // that role rather than only its filename spelling. - rejectedName = name; + // both that role and its filename spelling: either one may be the role the + // author meant to narrow, and the other would fall back to a broader one. + if (name !== stem) rejectedNames.push(name); // A renamed file with a stale `name` would otherwise shadow a different type // than the filename suggests. if (name !== stem) { @@ -477,7 +478,7 @@ function loadDirectory(directory: string) { const result = parseAgentType(content, file.slice(0, -3), filePath); diagnostics.push(...result.diagnostics); if (result.agentType) agentTypes.push(result.agentType); - else rejectedNames.add(result.rejectedName); + else for (const name of result.rejectedNames) rejectedNames.add(name); } return { agentTypes, rejectedNames, diagnostics, blockAllFallback }; diff --git a/tests/extensions/subagents/agent-types.test.ts b/tests/extensions/subagents/agent-types.test.ts index 38669c3e7..c367b7a7b 100644 --- a/tests/extensions/subagents/agent-types.test.ts +++ b/tests/extensions/subagents/agent-types.test.ts @@ -514,6 +514,42 @@ Do not write. }); }); +test("FAIL: a misspelled name in a role file still blocks the role its filename names", async () => { + for (const layer of ["global", "project"] as const) { + await withTempDir(async (root) => { + const { agentDir, cwd } = await seed(root, { + [layer]: { + "implementer.md": `--- +name: implementor +description: Intended read-only override. +tools: [read, grep] +--- +Do not write. +`, + }, + }); + const loaded = loadAgentTypes({ agentDir, cwd, projectTrusted: true }); + assert.equal( + loaded.agentTypes.has("implementer"), + false, + `${layer} implementer.md must not expose the write-capable builtin`, + ); + assert.equal(loaded.agentTypes.has("implementor"), false); + const messages = loaded.diagnostics.map((entry) => entry.message); + assert.ok( + messages.some((message) => + /does not match filename "implementer\.md"/.test(message), + ), + ); + assert.ok( + messages.some((message) => + /blocks fallback to built-in:implementer/.test(message), + ), + ); + }); + } +}); + test("every key the parser reads is accepted without a warning", () => { const parsed = parseAgentType(VALID, "explore", "explore.md"); assert.ok(parsed.agentType);