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
This commit is contained in:
ismeth
2026-02-21 00:19:49 +01:00
committed by YeonGyu-Kim
parent 5da9337c7e
commit f9bb441644
8 changed files with 78 additions and 13 deletions

View File

@@ -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"
},

View File

@@ -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 = {

View File

@@ -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

View File

@@ -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 {

View File

@@ -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 = {

View File

@@ -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."
)
}
}
}

View File

@@ -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", () => {

View File

@@ -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<string, boolean> = {
write: false,
edit: false,
@@ -49,15 +51,26 @@ const AGENT_RESTRICTIONS: Record<string, Record<string, boolean>> = {
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<string, boolean> {
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]
?? {}