Merge pull request #3992 from MoerAI/fix/omo-block-native-execution-delegation
fix(delegate-task): exclude hidden agents from task delegation discovery (fixes #3957)
This commit is contained in:
@@ -6,6 +6,7 @@ export type AgentMode = "subagent" | "primary" | "all" | undefined
|
||||
export type AgentInfo = {
|
||||
name: string
|
||||
mode?: "subagent" | "primary" | "all"
|
||||
hidden?: boolean
|
||||
model?: string | { providerID: string; modelID: string }
|
||||
}
|
||||
|
||||
@@ -20,16 +21,17 @@ export function mergeWithClaudeCodeAgents(
|
||||
const userAgentsRecord = loadUserAgents()
|
||||
const projectAgentsRecord = loadProjectAgents(directory)
|
||||
|
||||
const toAgentInfoList = (record: Record<string, { mode?: string; model?: AgentInfo["model"] }>): AgentInfo[] =>
|
||||
const toAgentInfoList = (record: Record<string, { mode?: string; hidden?: boolean; model?: AgentInfo["model"] }>): AgentInfo[] =>
|
||||
Object.entries(record).map(([name, config]) => ({
|
||||
name,
|
||||
mode: config.mode as AgentInfo["mode"],
|
||||
hidden: config.hidden,
|
||||
model: config.model,
|
||||
}))
|
||||
|
||||
const mergedAgentMap = new Map<string, AgentInfo>()
|
||||
const addIfAbsent = (agent: AgentInfo): void => {
|
||||
const key = agent.name.toLowerCase()
|
||||
const key = stripAgentListSortPrefix(agent.name).trim().toLowerCase()
|
||||
if (!mergedAgentMap.has(key)) {
|
||||
mergedAgentMap.set(key, agent)
|
||||
}
|
||||
@@ -73,12 +75,12 @@ export function findCallableAgentMatch(
|
||||
agents: AgentInfo[],
|
||||
requestedAgentName: string,
|
||||
): AgentInfo | undefined {
|
||||
return agents.find(agent => isTaskCallableAgentMode(agent.mode) && matchesRequestedAgent(agent, requestedAgentName))
|
||||
return agents.find(agent => isTaskCallableAgentMode(agent.mode) && agent.hidden !== true && matchesRequestedAgent(agent, requestedAgentName))
|
||||
}
|
||||
|
||||
export function listCallableAgentNames(agents: AgentInfo[]): string {
|
||||
return agents
|
||||
.filter(agent => isTaskCallableAgentMode(agent.mode))
|
||||
.filter(agent => isTaskCallableAgentMode(agent.mode) && agent.hidden !== true)
|
||||
.map(agent => stripAgentListSortPrefix(agent.name))
|
||||
.sort()
|
||||
.join(", ")
|
||||
|
||||
@@ -248,6 +248,133 @@ describe("resolveSubagentExecution", () => {
|
||||
expect(result.error).toBe('Unknown agent: "custom-worker". Available agents: oracle')
|
||||
})
|
||||
|
||||
test("rejects delegation to hidden native execution agents (regression #3957)", async () => {
|
||||
//#given
|
||||
const args = createBaseArgs({ subagent_type: "build" })
|
||||
const executorCtx = createExecutorContext(async () => ([
|
||||
{ name: "build", mode: "subagent", hidden: true },
|
||||
{ name: "oracle", mode: "subagent" },
|
||||
]))
|
||||
|
||||
//#when
|
||||
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
||||
|
||||
//#then
|
||||
expect(result.agentToUse).toBe("")
|
||||
expect(result.categoryModel).toBeUndefined()
|
||||
expect(result.error).toBe('Unknown agent: "build". Available agents: oracle')
|
||||
})
|
||||
|
||||
test("rejects delegation to hidden plan agent demoted to subagent (regression #3957)", async () => {
|
||||
//#given
|
||||
const args = createBaseArgs({ subagent_type: "plan" })
|
||||
const executorCtx = createExecutorContext(async () => ([
|
||||
{ name: "plan", mode: "subagent", hidden: true },
|
||||
{ name: "oracle", mode: "subagent" },
|
||||
]))
|
||||
|
||||
//#when
|
||||
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
||||
|
||||
//#then
|
||||
expect(result.agentToUse).toBe("")
|
||||
expect(result.categoryModel).toBeUndefined()
|
||||
expect(result.error).toBe('Unknown agent: "plan". Available agents: oracle')
|
||||
})
|
||||
|
||||
test("hidden agents are excluded from listCallableAgentNames in error messages (regression #3957)", async () => {
|
||||
//#given
|
||||
const args = createBaseArgs({ subagent_type: "nonexistent" })
|
||||
const executorCtx = createExecutorContext(async () => ([
|
||||
{ name: "build", mode: "subagent", hidden: true },
|
||||
{ name: "plan", mode: "subagent", hidden: true },
|
||||
{ name: "oracle", mode: "subagent" },
|
||||
{ name: "explore", mode: "subagent" },
|
||||
]))
|
||||
|
||||
//#when
|
||||
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
||||
|
||||
//#then
|
||||
expect(result.agentToUse).toBe("")
|
||||
expect(result.error).toBeDefined()
|
||||
expect(result.error).toContain('Available agents: explore, oracle')
|
||||
expect(result.error).not.toContain("build")
|
||||
expect(result.error).not.toContain("plan")
|
||||
})
|
||||
|
||||
test("rejects ZWSP-prefixed project agent that canonicalizes to hidden build (regression #3957 canonical-key bypass)", async () => {
|
||||
//#given
|
||||
loadProjectAgentsMock.mockImplementation(() => ({
|
||||
"\u200Bbuild": {
|
||||
description: "Aliases hidden build via zero-width prefix",
|
||||
mode: "subagent",
|
||||
prompt: "rogue",
|
||||
},
|
||||
}))
|
||||
const args = createBaseArgs({ subagent_type: "build" })
|
||||
const executorCtx = createExecutorContext(async () => ([
|
||||
{ name: "build", mode: "subagent", hidden: true },
|
||||
{ name: "oracle", mode: "subagent" },
|
||||
]))
|
||||
|
||||
//#when
|
||||
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
||||
|
||||
//#then
|
||||
expect(result.agentToUse).toBe("")
|
||||
expect(result.categoryModel).toBeUndefined()
|
||||
expect(result.error).toBe('Unknown agent: "build". Available agents: oracle')
|
||||
})
|
||||
|
||||
test("rejects quoted user agent that canonicalizes to hidden plan (regression #3957 canonical-key bypass)", async () => {
|
||||
//#given
|
||||
loadUserAgentsMock.mockImplementation(() => ({
|
||||
'"plan"': {
|
||||
description: "Aliases hidden plan via quote wrappers",
|
||||
mode: "subagent",
|
||||
prompt: "rogue",
|
||||
},
|
||||
}))
|
||||
const args = createBaseArgs({ subagent_type: "plan" })
|
||||
const executorCtx = createExecutorContext(async () => ([
|
||||
{ name: "plan", mode: "subagent", hidden: true },
|
||||
{ name: "oracle", mode: "subagent" },
|
||||
]))
|
||||
|
||||
//#when
|
||||
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
||||
|
||||
//#then
|
||||
expect(result.agentToUse).toBe("")
|
||||
expect(result.categoryModel).toBeUndefined()
|
||||
expect(result.error).toBe('Unknown agent: "plan". Available agents: oracle')
|
||||
})
|
||||
|
||||
test("rejects sort-prefixed project agent that canonicalizes to hidden build (regression #3957 canonical-key bypass)", async () => {
|
||||
//#given
|
||||
loadProjectAgentsMock.mockImplementation(() => ({
|
||||
"1|build": {
|
||||
description: "Aliases hidden build via sort prefix",
|
||||
mode: "subagent",
|
||||
prompt: "rogue",
|
||||
},
|
||||
}))
|
||||
const args = createBaseArgs({ subagent_type: "build" })
|
||||
const executorCtx = createExecutorContext(async () => ([
|
||||
{ name: "build", mode: "subagent", hidden: true },
|
||||
{ name: "oracle", mode: "subagent" },
|
||||
]))
|
||||
|
||||
//#when
|
||||
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
||||
|
||||
//#then
|
||||
expect(result.agentToUse).toBe("")
|
||||
expect(result.categoryModel).toBeUndefined()
|
||||
expect(result.error).toBe('Unknown agent: "build". Available agents: oracle')
|
||||
})
|
||||
|
||||
test("normalizes matched agent model string before returning categoryModel", async () => {
|
||||
//#given
|
||||
readProviderModelsCacheMock.mockReturnValue({
|
||||
|
||||
Reference in New Issue
Block a user