fix(athena): write council prompt to .sisyphus/tmp/, switch to allow-list permissions

Council members now use an allow-list (read, grep, glob, lsp_*, ast_grep_search)
instead of a deny-list. Prompt file moved from /tmp/ to .sisyphus/tmp/ so no
external_directory permission is needed. COUNCIL_MEMBER_PROMPT is included in
the temp file for self-contained council member instructions.
This commit is contained in:
ismeth 2026-02-23 12:46:44 +01:00 committed by YeonGyu-Kim
parent 1e0229226e
commit 92e9cbea5c
5 changed files with 42 additions and 26 deletions

View File

@ -35,7 +35,7 @@ Agent factories following `createXXXAgent(model) → AgentConfig` pattern. Each
| Atlas | task, call_omo_agent | | Atlas | task, call_omo_agent |
| Momus | write, edit, task | | Momus | write, edit, task |
| Athena | write, edit, call_omo_agent | | 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 ## STRUCTURE

View File

@ -1,11 +1,11 @@
import type { AgentConfig } from "@opencode-ai/sdk" import type { AgentConfig } from "@opencode-ai/sdk"
import type { AgentMode } from "../types" import type { AgentMode } from "../types"
import { createAgentToolRestrictions } from "../../shared/permission-compat" import { createAgentToolAllowlist } from "../../shared"
import { applyModelThinkingConfig } from "./model-thinking-config" import { applyModelThinkingConfig } from "./model-thinking-config"
const MODE: AgentMode = "subagent" 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 ## Your Role
- You are one of several AI models analyzing the same question independently - 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` 5. Be concise but thorough quality over quantity`
export function createCouncilMemberAgent(model: string): AgentConfig { export function createCouncilMemberAgent(model: string): AgentConfig {
const restrictions = createAgentToolRestrictions([ // Allow-list: only read-only analysis tools. Everything else is denied via `*: deny`.
"write", const restrictions = createAgentToolAllowlist([
"edit", "read",
"task", "grep",
"call_omo_agent", "glob",
"switch_agent", "lsp_goto_definition",
"background_wait", "lsp_find_references",
"prepare_council_prompt", "lsp_symbols",
"lsp_diagnostics",
"ast_grep_search",
]) ])
const base = { const base = {

View File

@ -139,7 +139,7 @@ export function createToolRegistry(args: {
interactive_bash, interactive_bash,
...taskToolsRecord, ...taskToolsRecord,
...hashlineToolsRecord, ...hashlineToolsRecord,
prepare_council_prompt: createPrepareCouncilPromptTool(), prepare_council_prompt: createPrepareCouncilPromptTool(ctx.directory),
} }
const filteredTools = filterDisabledTools(allTools, pluginConfig.disabled_tools) const filteredTools = filterDisabledTools(allTools, pluginConfig.disabled_tools)

View File

@ -53,17 +53,21 @@ const AGENT_RESTRICTIONS: Record<string, Record<string, boolean>> = {
// NOTE: Athena/council tool restrictions are also defined in: // NOTE: Athena/council tool restrictions are also defined in:
// - src/agents/athena/agent.ts (AgentConfig permission format) // - 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) // - src/plugin-handlers/tool-config-handler.ts (allow/deny string format)
// Keep all three in sync when modifying. // 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": { "council-member": {
write: false, "*": false,
edit: false, read: true,
task: false, grep: true,
call_omo_agent: false, glob: true,
switch_agent: false, lsp_goto_definition: true,
background_wait: false, lsp_find_references: true,
prepare_council_prompt: false, lsp_symbols: true,
lsp_diagnostics: true,
ast_grep_search: true,
}, },
} }

View File

@ -1,13 +1,14 @@
import { tool, type ToolDefinition } from "@opencode-ai/plugin" import { tool, type ToolDefinition } from "@opencode-ai/plugin"
import { randomUUID } from "node:crypto" 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 { join } from "node:path"
import { tmpdir } from "node:os"
import { log } from "../../shared/logger" import { log } from "../../shared/logger"
import { COUNCIL_MEMBER_PROMPT } from "../../agents/athena/council-member-agent"
const CLEANUP_DELAY_MS = 30 * 60 * 1000 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. 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 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." return "Prompt cannot be empty."
} }
const filename = `athena-council-${randomUUID().slice(0, 8)}.md` const tmpDir = join(directory, COUNCIL_TMP_DIR)
const filePath = join(tmpdir(), filename) 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(() => { setTimeout(() => {
unlink(filePath).catch(() => {}) unlink(filePath).catch(() => {})