diff --git a/src/agents/AGENTS.md b/src/agents/AGENTS.md index 65c02e15..ad78363a 100644 --- a/src/agents/AGENTS.md +++ b/src/agents/AGENTS.md @@ -35,7 +35,7 @@ Agent factories following `createXXXAgent(model) → AgentConfig` pattern. Each | Atlas | task, call_omo_agent | | Momus | write, edit, task | | Athena | write, edit, call_omo_agent | -| Council-Member | write, edit, task, call_omo_agent, switch_agent, background_wait, prepare_council_prompt | +| Council-Member | ALL except read, grep, glob, lsp_*, ast_grep_search (allow-list) | ## STRUCTURE diff --git a/src/agents/athena/council-member-agent.ts b/src/agents/athena/council-member-agent.ts index fa53f2ca..16fafc25 100644 --- a/src/agents/athena/council-member-agent.ts +++ b/src/agents/athena/council-member-agent.ts @@ -1,11 +1,11 @@ import type { AgentConfig } from "@opencode-ai/sdk" import type { AgentMode } from "../types" -import { createAgentToolRestrictions } from "../../shared/permission-compat" +import { createAgentToolAllowlist } from "../../shared" import { applyModelThinkingConfig } from "./model-thinking-config" const MODE: AgentMode = "subagent" -const COUNCIL_MEMBER_PROMPT = `You are an independent code analyst in a multi-model analysis council. Your role is to provide thorough, evidence-based analysis. +export const COUNCIL_MEMBER_PROMPT = `You are an independent code analyst in a multi-model analysis council. Your role is to provide thorough, evidence-based analysis. ## Your Role - You are one of several AI models analyzing the same question independently @@ -25,14 +25,16 @@ const COUNCIL_MEMBER_PROMPT = `You are an independent code analyst in a multi-mo 5. Be concise but thorough — quality over quantity` export function createCouncilMemberAgent(model: string): AgentConfig { - const restrictions = createAgentToolRestrictions([ - "write", - "edit", - "task", - "call_omo_agent", - "switch_agent", - "background_wait", - "prepare_council_prompt", + // Allow-list: only read-only analysis tools. Everything else is denied via `*: deny`. + const restrictions = createAgentToolAllowlist([ + "read", + "grep", + "glob", + "lsp_goto_definition", + "lsp_find_references", + "lsp_symbols", + "lsp_diagnostics", + "ast_grep_search", ]) const base = { diff --git a/src/plugin/tool-registry.ts b/src/plugin/tool-registry.ts index dad7d2f5..020a5121 100644 --- a/src/plugin/tool-registry.ts +++ b/src/plugin/tool-registry.ts @@ -139,7 +139,7 @@ export function createToolRegistry(args: { interactive_bash, ...taskToolsRecord, ...hashlineToolsRecord, - prepare_council_prompt: createPrepareCouncilPromptTool(), + prepare_council_prompt: createPrepareCouncilPromptTool(ctx.directory), } const filteredTools = filterDisabledTools(allTools, pluginConfig.disabled_tools) diff --git a/src/shared/agent-tool-restrictions.ts b/src/shared/agent-tool-restrictions.ts index 37192b35..19ff8459 100644 --- a/src/shared/agent-tool-restrictions.ts +++ b/src/shared/agent-tool-restrictions.ts @@ -53,17 +53,21 @@ const AGENT_RESTRICTIONS: Record> = { // 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/agents/athena/council-member-agent.ts (AgentConfig permission format — allow-list) // - src/plugin-handlers/tool-config-handler.ts (allow/deny string format) // Keep all three in sync when modifying. + // Council members use an allow-list: only read-only analysis tools are permitted. + // Prompt file lives in .sisyphus/tmp/ (inside project) so no external_directory needed. "council-member": { - write: false, - edit: false, - task: false, - call_omo_agent: false, - switch_agent: false, - background_wait: false, - prepare_council_prompt: false, + "*": false, + read: true, + grep: true, + glob: true, + lsp_goto_definition: true, + lsp_find_references: true, + lsp_symbols: true, + lsp_diagnostics: true, + ast_grep_search: true, }, } diff --git a/src/tools/prepare-council-prompt/tools.ts b/src/tools/prepare-council-prompt/tools.ts index 6117fea7..f587f972 100644 --- a/src/tools/prepare-council-prompt/tools.ts +++ b/src/tools/prepare-council-prompt/tools.ts @@ -1,13 +1,14 @@ import { tool, type ToolDefinition } from "@opencode-ai/plugin" import { randomUUID } from "node:crypto" -import { writeFile, unlink } from "node:fs/promises" +import { writeFile, unlink, mkdir } from "node:fs/promises" import { join } from "node:path" -import { tmpdir } from "node:os" import { log } from "../../shared/logger" +import { COUNCIL_MEMBER_PROMPT } from "../../agents/athena/council-member-agent" const CLEANUP_DELAY_MS = 30 * 60 * 1000 +const COUNCIL_TMP_DIR = ".sisyphus/tmp" -export function createPrepareCouncilPromptTool(): ToolDefinition { +export function createPrepareCouncilPromptTool(directory: string): ToolDefinition { const description = `Save a council analysis prompt to a temp file so council members can read it. Athena-only tool. Saves the prompt once, then each council member task() call uses a short @@ -26,10 +27,19 @@ Returns the file path to reference in subsequent task() calls.` return "Prompt cannot be empty." } - const filename = `athena-council-${randomUUID().slice(0, 8)}.md` - const filePath = join(tmpdir(), filename) + const tmpDir = join(directory, COUNCIL_TMP_DIR) + await mkdir(tmpDir, { recursive: true }) - await writeFile(filePath, args.prompt, "utf-8") + const filename = `athena-council-${randomUUID().slice(0, 8)}.md` + const filePath = join(tmpDir, filename) + + const content = `${COUNCIL_MEMBER_PROMPT} + +## Analysis Question + +${args.prompt}` + + await writeFile(filePath, content, "utf-8") setTimeout(() => { unlink(filePath).catch(() => {})