Files
oh-my-opencode/src/tools/delegate-task/subagent-discovery.ts
T
YeonGyu-Kim 95cc9e2d28 [sisyphus-dev] fix(delegate-task): canonicalize agent dedup key to close hidden filter bypass
mergeWithClaudeCodeAgents deduplicated by raw agent.name.toLowerCase() while
matchesRequestedAgent strips invisible characters, the numeric sort prefix,
and wrapper characters via stripAgentListSortPrefix. A project or user agent
named with a zero-width prefix, quote wrappers, or a sort prefix survived as
a visible duplicate of the hidden native build or demoted plan agent and
matched subagent_type="build" or "plan", which let an OMO orchestrator reach
the hidden execution agent the previous filter was meant to block.

Apply the same canonicalization to the dedup key so visible aliases of hidden
server agents collapse onto the hidden entry instead of bypassing the filter.

Adds three regression tests covering ZWSP, quote-wrapper, and sort-prefix
bypass paths.

bun.lock: refresh platform optionalDependencies to 4.1.1 so frozen-lockfile
install succeeds in CI.
2026-05-13 20:31:17 +09:00

88 lines
3.1 KiB
TypeScript

import { getAgentConfigKey, getAgentDisplayName, stripAgentListSortPrefix } from "../../shared/agent-display-names"
import { loadUserAgents, loadProjectAgents } from "../../features/claude-code-agent-loader"
export type AgentMode = "subagent" | "primary" | "all" | undefined
export type AgentInfo = {
name: string
mode?: "subagent" | "primary" | "all"
hidden?: boolean
model?: string | { providerID: string; modelID: string }
}
export function sanitizeSubagentType(subagentType: string): string {
return subagentType.trim().replace(/^[\\\/"']+|[\\\/"']+$/g, "").trim()
}
export function mergeWithClaudeCodeAgents(
serverAgents: AgentInfo[],
directory: string | undefined,
): AgentInfo[] {
const userAgentsRecord = loadUserAgents()
const projectAgentsRecord = loadProjectAgents(directory)
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 = stripAgentListSortPrefix(agent.name).trim().toLowerCase()
if (!mergedAgentMap.has(key)) {
mergedAgentMap.set(key, agent)
}
}
for (const agent of serverAgents) addIfAbsent(agent)
for (const agent of toAgentInfoList(projectAgentsRecord)) addIfAbsent(agent)
for (const agent of toAgentInfoList(userAgentsRecord)) addIfAbsent(agent)
return Array.from(mergedAgentMap.values())
}
function buildComparableNames(agentName: string): Set<string> {
return new Set([
agentName,
getAgentDisplayName(agentName),
getAgentConfigKey(agentName),
].map(name => stripAgentListSortPrefix(name).trim().toLowerCase()))
}
function matchesRequestedAgent(agent: AgentInfo, requestedAgentName: string): boolean {
const comparableNames = buildComparableNames(requestedAgentName)
const listedAgentName = stripAgentListSortPrefix(agent.name).trim().toLowerCase()
const listedAgentConfigKey = getAgentConfigKey(agent.name).trim().toLowerCase()
return comparableNames.has(listedAgentName) || comparableNames.has(listedAgentConfigKey)
}
export function isTaskCallableAgentMode(mode: AgentMode): boolean {
return mode === "all" || mode === "subagent"
}
export function findPrimaryAgentMatch(
agents: AgentInfo[],
requestedAgentName: string,
): AgentInfo | undefined {
return agents.find(agent => agent.mode === "primary" && matchesRequestedAgent(agent, requestedAgentName))
}
export function findCallableAgentMatch(
agents: AgentInfo[],
requestedAgentName: string,
): AgentInfo | undefined {
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) && agent.hidden !== true)
.map(agent => stripAgentListSortPrefix(agent.name))
.sort()
.join(", ")
}