fix(athena): address 5 audit findings — remove harmful thinking config, add error handling, improve tests

- Remove applyModelThinkingConfig and all callers (OpenCode handles thinking natively)

- Add try/catch error handling to prepare_council_prompt filesystem operations

- Add mode input validation to prepare_council_prompt tool

- Add 2 missing placeholder test cases (RETRY_FAILED_IF_OTHERS_FINISHED, CANCEL_RETRYING_ON_QUORUM)

- Remove redundant 'Do NOT use TodoWrite' prompt section from council member agent
This commit is contained in:
ismeth
2026-02-26 15:12:57 +01:00
committed by YeonGyu-Kim
parent 4cd27f620e
commit 5a819d0914
7 changed files with 34 additions and 126 deletions
+1 -2
View File
@@ -1,7 +1,6 @@
import type { AgentConfig } from "@opencode-ai/sdk" import type { AgentConfig } from "@opencode-ai/sdk"
import type { AgentMode, AgentPromptMetadata } from "../types" import type { AgentMode, AgentPromptMetadata } from "../types"
import { createAgentToolRestrictions } from "../../shared/permission-compat" import { createAgentToolRestrictions } from "../../shared/permission-compat"
import { applyModelThinkingConfig } from "./model-thinking-config"
const MODE: AgentMode = "primary" const MODE: AgentMode = "primary"
@@ -326,6 +325,6 @@ export function createAthenaAgent(model: string): AgentConfig {
color: "#1F8EFA", color: "#1F8EFA",
} }
return applyModelThinkingConfig(base, model) return base
} }
createAthenaAgent.mode = MODE createAthenaAgent.mode = MODE
@@ -21,6 +21,14 @@ describe("Athena prompt config injection placeholders", () => {
it("#then contains timeout reference with 30000", () => { it("#then contains timeout reference with 30000", () => {
expect(athenaConfig.prompt).toContain("30000") expect(athenaConfig.prompt).toContain("30000")
}) })
it("#then contains RETRY_FAILED_IF_OTHERS_FINISHED placeholder", () => {
expect(athenaConfig.prompt).toContain("{RETRY_FAILED_IF_OTHERS_FINISHED}")
})
it("#then contains CANCEL_RETRYING_ON_QUORUM placeholder", () => {
expect(athenaConfig.prompt).toContain("{CANCEL_RETRYING_ON_QUORUM}")
})
}) })
}) })
}) })
+2 -8
View File
@@ -1,7 +1,6 @@
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 { createAgentToolAllowlist } from "../../shared" import { createAgentToolAllowlist } from "../../shared"
import { applyModelThinkingConfig } from "./model-thinking-config"
const MODE: AgentMode = "subagent" const MODE: AgentMode = "subagent"
@@ -57,12 +56,7 @@ You MUST wrap your final analysis in <COUNCIL_MEMBER_RESPONSE> tags. This is how
</COUNCIL_MEMBER_RESPONSE> </COUNCIL_MEMBER_RESPONSE>
\`\`\` \`\`\`
If you do not wrap your response in these tags, your analysis will not be included in the synthesis. If you do not wrap your response in these tags, your analysis will not be included in the synthesis.`
## CRITICAL: Do NOT use TodoWrite
- Do NOT create todos or task lists
- Do NOT use the TodoWrite tool under any circumstances
- Simply report your findings directly in your response`
export const COUNCIL_SOLO_ADDENDUM = ` export const COUNCIL_SOLO_ADDENDUM = `
## Solo Analysis Mode ## Solo Analysis Mode
@@ -129,6 +123,6 @@ export function createCouncilMemberAgent(model: string): AgentConfig {
...restrictions, ...restrictions,
} }
return applyModelThinkingConfig(base, model) return base
} }
createCouncilMemberAgent.mode = MODE createCouncilMemberAgent.mode = MODE
-1
View File
@@ -1,3 +1,2 @@
export { createAthenaAgent, ATHENA_PROMPT_METADATA } from "./agent" export { createAthenaAgent, ATHENA_PROMPT_METADATA } from "./agent"
export { createCouncilMemberAgent, COUNCIL_MEMBER_PROMPT, COUNCIL_SOLO_ADDENDUM, COUNCIL_DELEGATION_ADDENDUM } from "./council-member-agent" export { createCouncilMemberAgent, COUNCIL_MEMBER_PROMPT, COUNCIL_SOLO_ADDENDUM, COUNCIL_DELEGATION_ADDENDUM } from "./council-member-agent"
export { applyModelThinkingConfig } from "./model-thinking-config"
@@ -1,81 +0,0 @@
import { describe, expect, test } from "bun:test"
import type { AgentConfig } from "@opencode-ai/sdk"
import { applyModelThinkingConfig } from "./model-thinking-config"
const BASE_CONFIG: AgentConfig = {
name: "test-agent",
description: "test",
model: "anthropic/claude-opus-4-6",
temperature: 0.1,
}
describe("applyModelThinkingConfig", () => {
describe("#given a GPT model", () => {
test("#then returns reasoningEffort medium", () => {
const result = applyModelThinkingConfig(BASE_CONFIG, "gpt-5.2")
expect(result).toEqual({ ...BASE_CONFIG, reasoningEffort: "medium" })
})
test("#then returns reasoningEffort medium for openai-prefixed model", () => {
const result = applyModelThinkingConfig(BASE_CONFIG, "openai/gpt-5.2")
expect(result).toEqual({ ...BASE_CONFIG, reasoningEffort: "medium" })
})
})
describe("#given an Anthropic model", () => {
test("#then returns thinking config with budgetTokens 32000", () => {
const result = applyModelThinkingConfig(BASE_CONFIG, "anthropic/claude-opus-4-6")
expect(result).toEqual({
...BASE_CONFIG,
thinking: { type: "enabled", budgetTokens: 32000 },
})
})
})
describe("#given a Google model", () => {
test("#then returns base config unchanged", () => {
const result = applyModelThinkingConfig(BASE_CONFIG, "google/gemini-3-pro")
expect(result).toBe(BASE_CONFIG)
})
})
describe("#given a Kimi model", () => {
test("#then returns base config unchanged", () => {
const result = applyModelThinkingConfig(BASE_CONFIG, "kimi/kimi-k2.5")
expect(result).toBe(BASE_CONFIG)
})
})
describe("#given a model with no provider prefix", () => {
test("#then returns base config unchanged for non-GPT model", () => {
const result = applyModelThinkingConfig(BASE_CONFIG, "gemini-3-pro")
expect(result).toBe(BASE_CONFIG)
})
})
describe("#given a Claude model through a non-Anthropic provider", () => {
test("#then returns thinking config for github-copilot/claude-opus-4-6", () => {
const result = applyModelThinkingConfig(BASE_CONFIG, "github-copilot/claude-opus-4-6")
expect(result).toEqual({
...BASE_CONFIG,
thinking: { type: "enabled", budgetTokens: 32000 },
})
})
test("#then returns thinking config for opencode/claude-opus-4-6", () => {
const result = applyModelThinkingConfig(BASE_CONFIG, "opencode/claude-opus-4-6")
expect(result).toEqual({
...BASE_CONFIG,
thinking: { type: "enabled", budgetTokens: 32000 },
})
})
test("#then returns thinking config for opencode/claude-sonnet-4-6", () => {
const result = applyModelThinkingConfig(BASE_CONFIG, "opencode/claude-sonnet-4-6")
expect(result).toEqual({
...BASE_CONFIG,
thinking: { type: "enabled", budgetTokens: 32000 },
})
})
})
})
@@ -1,20 +0,0 @@
import type { AgentConfig } from "@opencode-ai/sdk"
import { parseModelString } from "../../tools/delegate-task/model-string-parser"
import { isGptModel } from "../types"
export function applyModelThinkingConfig(base: AgentConfig, model: string): AgentConfig {
if (isGptModel(model)) {
return { ...base, reasoningEffort: "medium" }
}
const parsed = parseModelString(model)
if (!parsed) {
return base
}
if (parsed.providerID.toLowerCase() === "anthropic" || parsed.modelID.startsWith("claude")) {
return { ...base, thinking: { type: "enabled", budgetTokens: 32000 } }
}
return base
}
+23 -14
View File
@@ -61,36 +61,45 @@ Returns the file path to reference in subsequent task() calls.`
return "Prompt cannot be empty." return "Prompt cannot be empty."
} }
if (args.mode !== undefined && args.mode !== "solo" && args.mode !== "delegation") {
return `Invalid mode: "${args.mode}". Valid modes: "solo", "delegation".`
}
const mode = args.mode === "delegation" ? "delegation" : "solo" const mode = args.mode === "delegation" ? "delegation" : "solo"
const tmpDir = join(directory, COUNCIL_TMP_DIR)
await mkdir(tmpDir, { recursive: true })
const filename = `athena-council-${randomUUID().slice(0, 8)}.md` try {
const filePath = join(tmpDir, filename) const tmpDir = join(directory, COUNCIL_TMP_DIR)
await mkdir(tmpDir, { recursive: true })
const modeAddendum = mode === "delegation" ? COUNCIL_DELEGATION_ADDENDUM : COUNCIL_SOLO_ADDENDUM const filename = `athena-council-${randomUUID().slice(0, 8)}.md`
const content = `${modeAddendum} const filePath = join(tmpDir, filename)
const modeAddendum = mode === "delegation" ? COUNCIL_DELEGATION_ADDENDUM : COUNCIL_SOLO_ADDENDUM
const content = `${modeAddendum}
## Analysis Question ## Analysis Question
${args.prompt}` ${args.prompt}`
await writeFile(filePath, content, "utf-8") await writeFile(filePath, content, "utf-8")
setTimeout(() => { setTimeout(() => {
unlink(filePath).catch((err) => { unlink(filePath).catch((err) => {
log("[prepare-council-prompt] Failed to clean up temp file", { filePath, error: String(err) }) log("[prepare-council-prompt] Failed to clean up temp file", { filePath, error: String(err) })
}) })
}, CLEANUP_DELAY_MS) }, CLEANUP_DELAY_MS)
log("[prepare-council-prompt] Saved prompt", { filePath, length: args.prompt.length, mode }) log("[prepare-council-prompt] Saved prompt", { filePath, length: args.prompt.length, mode })
return `Council prompt saved to: ${filePath} (mode: ${mode}) return `Council prompt saved to: ${filePath} (mode: ${mode})
Use this path in each council member's task() call: Use this path in each council member's task() call:
- prompt: "Read ${filePath} for your instructions." - prompt: "Read ${filePath} for your instructions."
The file auto-deletes after 30 minutes.` The file auto-deletes after 30 minutes.`
} catch (err) {
return `Error saving council prompt: ${String(err)}`
}
}, },
}) })
} }