From 4bd81d2cdde52397c1be3b19e89f742a2fb7bd30 Mon Sep 17 00:00:00 2001 From: MoerAI Date: Wed, 13 May 2026 19:30:09 +0900 Subject: [PATCH 1/2] fix(delegate-task): exclude hidden agents from task delegation discovery (fixes #3957) OpenCode injects native execution agents like build (and a demoted plan in OMO mode) as { mode: 'subagent', hidden: true }. The dynamic agent discovery in subagent-discovery.ts only filtered by mode, so a hidden agent still resolved as a callable target via task(). This created a boundary leak: an OMO orchestrator (sisyphus, prometheus, etc.) could delegate work into the hidden native build/plan path instead of the OMO category/skill pipeline. Add hidden?: boolean to AgentInfo, plumb it through mergeWithClaudeCodeAgents, and skip hidden agents in both findCallableAgentMatch and listCallableAgentNames so hidden natives are neither matched nor advertised in 'Available agents' error messages. The OpenCode SDK Agent type already exposes hidden?: boolean, so no schema work is required. Verified by adding three regression tests in zauc-mocks-subagent-resolver/subagent-resolver.test.ts: hidden 'build' is rejected, hidden 'plan' is rejected, and hidden agents are excluded from the Available agents list. Full delegate-task suite (395 tests) and call-omo-agent suite (57 tests) pass; bun run typecheck is clean. --- src/tools/delegate-task/subagent-discovery.ts | 8 ++- .../subagent-resolver.test.ts | 55 +++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/tools/delegate-task/subagent-discovery.ts b/src/tools/delegate-task/subagent-discovery.ts index 340ef7c22..118d3adf8 100644 --- a/src/tools/delegate-task/subagent-discovery.ts +++ b/src/tools/delegate-task/subagent-discovery.ts @@ -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,10 +21,11 @@ export function mergeWithClaudeCodeAgents( const userAgentsRecord = loadUserAgents() const projectAgentsRecord = loadProjectAgents(directory) - const toAgentInfoList = (record: Record): AgentInfo[] => + const toAgentInfoList = (record: Record): AgentInfo[] => Object.entries(record).map(([name, config]) => ({ name, mode: config.mode as AgentInfo["mode"], + hidden: config.hidden, model: config.model, })) @@ -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(", ") diff --git a/src/tools/delegate-task/zauc-mocks-subagent-resolver/subagent-resolver.test.ts b/src/tools/delegate-task/zauc-mocks-subagent-resolver/subagent-resolver.test.ts index 05e988716..343177ace 100644 --- a/src/tools/delegate-task/zauc-mocks-subagent-resolver/subagent-resolver.test.ts +++ b/src/tools/delegate-task/zauc-mocks-subagent-resolver/subagent-resolver.test.ts @@ -248,6 +248,61 @@ 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("normalizes matched agent model string before returning categoryModel", async () => { //#given readProviderModelsCacheMock.mockReturnValue({ From 95cc9e2d28a917894d2d2cf8e940fd81bfb649c4 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 13 May 2026 20:31:17 +0900 Subject: [PATCH 2/2] [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. --- bun.lock | 44 ++++++------ src/tools/delegate-task/subagent-discovery.ts | 2 +- .../subagent-resolver.test.ts | 72 +++++++++++++++++++ 3 files changed, 95 insertions(+), 23 deletions(-) diff --git a/bun.lock b/bun.lock index 31ef61ee5..3b01308a5 100644 --- a/bun.lock +++ b/bun.lock @@ -30,17 +30,17 @@ "zod": "^4.3.0", }, "optionalDependencies": { - "oh-my-opencode-darwin-arm64": "4.1.0", - "oh-my-opencode-darwin-x64": "4.1.0", - "oh-my-opencode-darwin-x64-baseline": "4.1.0", - "oh-my-opencode-linux-arm64": "4.1.0", - "oh-my-opencode-linux-arm64-musl": "4.1.0", - "oh-my-opencode-linux-x64": "4.1.0", - "oh-my-opencode-linux-x64-baseline": "4.1.0", - "oh-my-opencode-linux-x64-musl": "4.1.0", - "oh-my-opencode-linux-x64-musl-baseline": "4.1.0", - "oh-my-opencode-windows-x64": "4.1.0", - "oh-my-opencode-windows-x64-baseline": "4.1.0", + "oh-my-opencode-darwin-arm64": "4.1.1", + "oh-my-opencode-darwin-x64": "4.1.1", + "oh-my-opencode-darwin-x64-baseline": "4.1.1", + "oh-my-opencode-linux-arm64": "4.1.1", + "oh-my-opencode-linux-arm64-musl": "4.1.1", + "oh-my-opencode-linux-x64": "4.1.1", + "oh-my-opencode-linux-x64-baseline": "4.1.1", + "oh-my-opencode-linux-x64-musl": "4.1.1", + "oh-my-opencode-linux-x64-musl-baseline": "4.1.1", + "oh-my-opencode-windows-x64": "4.1.1", + "oh-my-opencode-windows-x64-baseline": "4.1.1", }, "peerDependencies": { "zod": "^4.0.0", @@ -248,27 +248,27 @@ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@4.1.0", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-0GaAhgMLRdoftNs1OR0NCii6rhZUUSb5sURHWcVnlJ6Ndqcm1c9ftkPr6f6cDIg2F/vvjfxP14FULtEvAzLTMQ=="], + "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@4.1.1", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-nciXDvGDWRFvz3OsZo+6IVUp2GwtOOUxZdIlsZklLI+7KC9MMm5FAYbFq2rokk1hkggLCSRROeEd7A4Jm/cYSw=="], - "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@4.1.0", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-4Zm/oX2OECaKFxYX8VPryfChFW8lem2q3Tn4sCrn7aE6AB8xWq6VCe9+O/mN3EW1Z1WJC/s8/yqwX6lit7Qsxw=="], + "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@4.1.1", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-vk8w7NveZp1JLN0zBe2mcYFaBMPPZ9BvehqSJjYwGnhxzfrnAFopksan0v7N2yr1y0ccOaF31F2XNAglv4rXBA=="], - "oh-my-opencode-darwin-x64-baseline": ["oh-my-opencode-darwin-x64-baseline@4.1.0", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-VHC9Zh/fzIMmeVbHblaVictDUfiQGByY3KDC2rkojEazTFB9WYcSGIp6W9RhxA/fg1OCGYskDIbdhmmEz2KDLQ=="], + "oh-my-opencode-darwin-x64-baseline": ["oh-my-opencode-darwin-x64-baseline@4.1.1", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-0H5npMFOwrmfI6q1Sw+tlucp4VYlT5wAx6PgDrGV1WrF/g1c/f/lkTcXfJApEhmqmSv9uTX0mWy5JRb1FQZ+Nw=="], - "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@4.1.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-4ou8Z2J9dquzCBKIZQ/UnIRxqou7lU2yW9dYy1Soh4XwRKFqTXAEmGNRVr/2uKBIPxu4cm3sfw5ixhIPYly3Eg=="], + "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@4.1.1", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-b8kQbbuEYGOZbhtetS9O8xAHbmIZfLwBQVw7lxgloIPU1qietfBxMrRY2ebF3j/UNvK3tE817Xwv01KW0X71Kw=="], - "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@4.1.0", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-NG2x8CMOP6E0UAAuJhWwfsr4rmQuXNkp4sOHftfeiq0IcecNuj10zkb73TkNhGrKJg5CXnuqMpew1p80KuEl7w=="], + "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@4.1.1", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-ZD/4FCEca82NshK0nNLJrsSYrx55RWOn0nhu3ZKKVyj4hMKWroAqBRvCWde3HxM/e5xUlDHsMJsj7MwvyLpcLg=="], - "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@4.1.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-8ljrAKgweTc1XrdkSR+UluYasb8flJTVBfqyamKTVRc7I8tIskkrp43/QSLCjJRT6v7Ejx1PYe9MOGDPQBrlMQ=="], + "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@4.1.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-0tgDMdodXuuOomxPvcRFABLX2xte2J4Q2LL1SwTFPw2/JaRa1GYcIX0GzX3PDWfH19jrfO+0RVX/oeXUWbG79w=="], - "oh-my-opencode-linux-x64-baseline": ["oh-my-opencode-linux-x64-baseline@4.1.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-oPPHWAUiYvoykozC3JQl91PGilY7bN0GCdTN+Dq+oj8cdLnfXjULwtdUm9y3gpN9yWy9d/IB7McvM2pFCKy6Bw=="], + "oh-my-opencode-linux-x64-baseline": ["oh-my-opencode-linux-x64-baseline@4.1.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-Qb7pnsMU9tZYl/YKYnYOArRNhBDfX6PdkiLuL9onpMGWqxTCV1OsdIIG+LjSU0xJolKpDYMbJesew9XqUshG5w=="], - "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@4.1.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-FAiyeKOO+38B3XR0vb3MiBnCgEl9un42KClQ/3ggCPV4f5uMzu5tVa6rlPHMATxryl4uk6ysdFeWr4XXN3ci6A=="], + "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@4.1.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-2lbTzaBnlTeG5Y/pBzZT7KF82PXdxYcrwM0H03pboNZOZdDFf7KG7XXzeVV+9GDfDd7T3VfeRED0MHqcoTXipA=="], - "oh-my-opencode-linux-x64-musl-baseline": ["oh-my-opencode-linux-x64-musl-baseline@4.1.0", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-9DChW9j0iCx3xO/oL2LXZmKaR8L/sYH6Se0v9zv7RS9kU4gxvS9Gd4kzJoFnSKiw/2xUpjjkHk/SPxsoytvQ3A=="], + "oh-my-opencode-linux-x64-musl-baseline": ["oh-my-opencode-linux-x64-musl-baseline@4.1.1", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-QWmDD93TrPTD6DHnYmqeZWSjHel8oU3BxvN7XLk/elSqH/GUgC+ml/w6H02V7/59MU9fc557kgGzz6r/5g66IA=="], - "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@4.1.0", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-F6X1elfOA0rOSC8nB+Od4/9sHz0NNv6hIiZ6XkuqPBSWD5DymtjYjHIRKfumR9mYxkDOVp4ajU4hdLtO3tcxgA=="], + "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@4.1.1", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-zu8Qsb9k9u034tnrf5QABS/u9HdLPc15/n4BrKg4k06DLzqI2pVhK1olvATQsz9W8QdOO+eZ0NY1/Acp5V3Sog=="], - "oh-my-opencode-windows-x64-baseline": ["oh-my-opencode-windows-x64-baseline@4.1.0", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-91RFjv+sGJqQ/tgun0sV+xpkOcgoJ8Pv/mTN5qPyivJtIsTceSUqoJyHyz3tmpyckdKLl46NmzF1DLWCRUIwCA=="], + "oh-my-opencode-windows-x64-baseline": ["oh-my-opencode-windows-x64-baseline@4.1.1", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-dBeCsO8kLC8cUSfBcMvprKoxLaTGKHuSg+otDdgy53nSN+3pHRxszM9iIQ0YplZsWmTxgpRlyAoL6hMZzZfdsw=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], diff --git a/src/tools/delegate-task/subagent-discovery.ts b/src/tools/delegate-task/subagent-discovery.ts index 118d3adf8..fc67432e3 100644 --- a/src/tools/delegate-task/subagent-discovery.ts +++ b/src/tools/delegate-task/subagent-discovery.ts @@ -31,7 +31,7 @@ export function mergeWithClaudeCodeAgents( const mergedAgentMap = new Map() 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) } diff --git a/src/tools/delegate-task/zauc-mocks-subagent-resolver/subagent-resolver.test.ts b/src/tools/delegate-task/zauc-mocks-subagent-resolver/subagent-resolver.test.ts index 343177ace..39f3e3f16 100644 --- a/src/tools/delegate-task/zauc-mocks-subagent-resolver/subagent-resolver.test.ts +++ b/src/tools/delegate-task/zauc-mocks-subagent-resolver/subagent-resolver.test.ts @@ -303,6 +303,78 @@ describe("resolveSubagentExecution", () => { 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({