From f9bb4416444d360254b6043a4df36a4176f44bf3 Mon Sep 17 00:00:00 2001 From: ismeth Date: Sat, 21 Feb 2026 00:19:49 +0100 Subject: [PATCH] fix: sync council-member tool restrictions across all layers, optimize athena guards - Add switch_agent/background_wait to agent-tool-restrictions.ts (boolean format) - Add dynamic council member name matching via COUNCIL_MEMBER_KEY_PREFIX - Move athena question permission from hardcoded to tool-config-handler (CLI-mode aware) - Rename appendMissingCouncilPrompt -> applyMissingCouncilGuard - Optimize tool-execute-before: check hasPendingCouncilMembers before resolving session agent - Add fallback_models to council-member/athena in schema.json - Remove unused createAthenaAgent export from agents/index.ts - Add cross-reference comments for restriction sync points --- assets/oh-my-opencode.schema.json | 26 +++++++++++++++++++ src/agents/athena/agent.ts | 8 ++++-- src/agents/builtin-agents.ts | 6 ++--- .../builtin-agents/athena-council-guard.ts | 2 +- src/plugin-handlers/tool-config-handler.ts | 4 +++ src/plugin/tool-execute-before.ts | 14 +++++----- src/shared/agent-tool-restrictions.test.ts | 18 ++++++++++++- src/shared/agent-tool-restrictions.ts | 13 ++++++++++ 8 files changed, 78 insertions(+), 13 deletions(-) diff --git a/assets/oh-my-opencode.schema.json b/assets/oh-my-opencode.schema.json index f199c4ff..07ed9e1f 100644 --- a/assets/oh-my-opencode.schema.json +++ b/assets/oh-my-opencode.schema.json @@ -3162,6 +3162,19 @@ "model": { "type": "string" }, + "fallback_models": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, "variant": { "type": "string" }, @@ -3347,6 +3360,19 @@ "model": { "type": "string" }, + "fallback_models": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, "variant": { "type": "string" }, diff --git a/src/agents/athena/agent.ts b/src/agents/athena/agent.ts index c81c667c..002ead7c 100644 --- a/src/agents/athena/agent.ts +++ b/src/agents/athena/agent.ts @@ -203,17 +203,21 @@ The switch_agent tool switches the active agent. After you call it, end your res - Do NOT ask any post-synthesis questions until all selected member calls have finished. - Do NOT present or summarize partial council findings while any selected member is still running. - Do NOT write or edit files directly. -- Do NOT delegate without explicit user confirmation via Question tool. +- Do NOT delegate without explicit user confirmation via Question tool, unless in non-interactive mode (where auto-delegation applies per the non-interactive rules above). - Do NOT ignore solo finding false-positive warnings. - Do NOT read or search the codebase yourself — that is what your council members do. - When handing off to Atlas/Prometheus, include ONLY the selected findings in context — not all findings.` export function createAthenaAgent(model: string): AgentConfig { + // NOTE: Athena/council tool restrictions are also defined in: + // - src/shared/agent-tool-restrictions.ts (boolean format for session.prompt) + // - src/plugin-handlers/tool-config-handler.ts (allow/deny string format) + // Keep all three in sync when modifying. const restrictions = createAgentToolRestrictions(["write", "edit", "call_omo_agent"]) + // question permission is set by tool-config-handler.ts based on CLI mode (allow/deny) const permission = { ...restrictions.permission, - question: "allow", } as AgentConfig["permission"] const base = { diff --git a/src/agents/builtin-agents.ts b/src/agents/builtin-agents.ts index 8e90359c..1d96e5d9 100644 --- a/src/agents/builtin-agents.ts +++ b/src/agents/builtin-agents.ts @@ -29,7 +29,7 @@ import { maybeCreateHephaestusConfig } from "./builtin-agents/hephaestus-agent" import { maybeCreateAtlasConfig } from "./builtin-agents/atlas-agent" import { buildCustomAgentMetadata, parseRegisteredAgentSummaries } from "./custom-agent-summaries" import { registerCouncilMemberAgents } from "./builtin-agents/council-member-agents" -import { appendMissingCouncilPrompt } from "./builtin-agents/athena-council-guard" +import { applyMissingCouncilGuard } from "./builtin-agents/athena-council-guard" import type { CouncilConfig } from "../config/schema/athena" type AgentSource = AgentFactory | AgentConfig @@ -222,12 +222,12 @@ export async function createBuiltinAgents( prompt: (result["athena"].prompt ?? "") + councilTaskInstructions, } } else { - result["athena"] = appendMissingCouncilPrompt(result["athena"], skippedMembers) + result["athena"] = applyMissingCouncilGuard(result["athena"], skippedMembers) } } else if (councilConfig?.members && councilConfig.members.length >= 2 && !result["athena"]) { log("[builtin-agents] Skipping council member registration — Athena is disabled") } else if (result["athena"]) { - result["athena"] = appendMissingCouncilPrompt(result["athena"]) + result["athena"] = applyMissingCouncilGuard(result["athena"]) } return result diff --git a/src/agents/builtin-agents/athena-council-guard.ts b/src/agents/builtin-agents/athena-council-guard.ts index 149ebf3b..44ee8171 100644 --- a/src/agents/builtin-agents/athena-council-guard.ts +++ b/src/agents/builtin-agents/athena-council-guard.ts @@ -45,7 +45,7 @@ After informing the user, **end your turn**. Do NOT try to work around this by u * The original prompt is discarded to avoid contradictory instructions. * Used when Athena is registered but no valid council config exists. */ -export function appendMissingCouncilPrompt( +export function applyMissingCouncilGuard( athenaConfig: AgentConfig, skippedMembers?: Array<{ name: string; reason: string }>, ): AgentConfig { diff --git a/src/plugin-handlers/tool-config-handler.ts b/src/plugin-handlers/tool-config-handler.ts index 522253cd..b6de94de 100644 --- a/src/plugin-handlers/tool-config-handler.ts +++ b/src/plugin-handlers/tool-config-handler.ts @@ -97,6 +97,10 @@ export function applyToolConfig(params: { ...denyTodoTools, }; } + // NOTE: Athena/council tool restrictions are also defined in: + // - src/agents/athena/agent.ts (AgentConfig permission format) + // - src/shared/agent-tool-restrictions.ts (boolean format for session.prompt) + // Keep all three in sync when modifying. const athena = agentByKey(params.agentResult, "athena"); if (athena) { athena.permission = { diff --git a/src/plugin/tool-execute-before.ts b/src/plugin/tool-execute-before.ts index 6f1b71d9..f6d60974 100644 --- a/src/plugin/tool-execute-before.ts +++ b/src/plugin/tool-execute-before.ts @@ -37,13 +37,15 @@ export function createToolExecuteBeforeHandler(args: { const toolNameLower = input.tool?.toLowerCase() if (toolNameLower === "question" || toolNameLower === "askuserquestion" || toolNameLower === "ask_user_question" || toolNameLower === "switch_agent") { - const sessionAgent = await resolveSessionAgent(ctx.client, input.sessionID) - const sessionAgentKey = sessionAgent ? getAgentConfigKey(sessionAgent) : undefined + if (hasPendingCouncilMembers(input.sessionID)) { + const sessionAgent = await resolveSessionAgent(ctx.client, input.sessionID) + const sessionAgentKey = sessionAgent ? getAgentConfigKey(sessionAgent) : undefined - if (sessionAgentKey === "athena" && hasPendingCouncilMembers(input.sessionID)) { - throw new Error( - "Council members are still running. Wait for all launched members to finish and collect their outputs before asking next-step questions or switching agents." - ) + if (sessionAgentKey === "athena") { + throw new Error( + "Council members are still running. Wait for all launched members to finish and collect their outputs before asking next-step questions or switching agents." + ) + } } } diff --git a/src/shared/agent-tool-restrictions.test.ts b/src/shared/agent-tool-restrictions.test.ts index 96daad77..14151d3b 100644 --- a/src/shared/agent-tool-restrictions.test.ts +++ b/src/shared/agent-tool-restrictions.test.ts @@ -15,12 +15,28 @@ describe("agent-tool-restrictions", () => { expect(restrictions.call_omo_agent).toBe(false) }) - test("council-member restrictions include call_omo_agent", () => { + test("council-member restrictions include all denied tools", () => { //#given //#when const restrictions = getAgentToolRestrictions("council-member") //#then expect(restrictions.call_omo_agent).toBe(false) + expect(restrictions.switch_agent).toBe(false) + expect(restrictions.background_wait).toBe(false) + }) + + test("#given dynamic council member name #when getAgentToolRestrictions #then returns council-member restrictions", () => { + //#given + const dynamicName = "Council: Claude Opus 4.6" + //#when + const restrictions = getAgentToolRestrictions(dynamicName) + //#then + expect(restrictions.write).toBe(false) + expect(restrictions.edit).toBe(false) + expect(restrictions.task).toBe(false) + expect(restrictions.call_omo_agent).toBe(false) + expect(restrictions.switch_agent).toBe(false) + expect(restrictions.background_wait).toBe(false) }) test("hasAgentToolRestrictions returns true for athena", () => { diff --git a/src/shared/agent-tool-restrictions.ts b/src/shared/agent-tool-restrictions.ts index a4fc820a..bc4c35a9 100644 --- a/src/shared/agent-tool-restrictions.ts +++ b/src/shared/agent-tool-restrictions.ts @@ -4,6 +4,8 @@ * true = tool allowed, false = tool denied. */ +import { COUNCIL_MEMBER_KEY_PREFIX } from "../agents/builtin-agents/council-member-agents" + const EXPLORATION_AGENT_DENYLIST: Record = { write: false, edit: false, @@ -49,15 +51,26 @@ const AGENT_RESTRICTIONS: Record> = { call_omo_agent: false, }, + // NOTE: Athena/council tool restrictions are also defined in: + // - src/agents/athena/agent.ts (AgentConfig permission format) + // - src/agents/athena/council-member-agent.ts (AgentConfig permission format) + // - src/plugin-handlers/tool-config-handler.ts (allow/deny string format) + // Keep all three in sync when modifying. "council-member": { write: false, edit: false, task: false, call_omo_agent: false, + switch_agent: false, + background_wait: false, }, } export function getAgentToolRestrictions(agentName: string): Record { + if (agentName.startsWith(COUNCIL_MEMBER_KEY_PREFIX)) { + return AGENT_RESTRICTIONS["council-member"] ?? {} + } + return AGENT_RESTRICTIONS[agentName] ?? Object.entries(AGENT_RESTRICTIONS).find(([key]) => key.toLowerCase() === agentName.toLowerCase())?.[1] ?? {}