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:
@@ -1,7 +1,6 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentMode, AgentPromptMetadata } from "../types"
|
||||
import { createAgentToolRestrictions } from "../../shared/permission-compat"
|
||||
import { applyModelThinkingConfig } from "./model-thinking-config"
|
||||
|
||||
const MODE: AgentMode = "primary"
|
||||
|
||||
@@ -326,6 +325,6 @@ export function createAthenaAgent(model: string): AgentConfig {
|
||||
color: "#1F8EFA",
|
||||
}
|
||||
|
||||
return applyModelThinkingConfig(base, model)
|
||||
return base
|
||||
}
|
||||
createAthenaAgent.mode = MODE
|
||||
|
||||
@@ -21,6 +21,14 @@ describe("Athena prompt config injection placeholders", () => {
|
||||
it("#then contains timeout reference with 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}")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentMode } from "../types"
|
||||
import { createAgentToolAllowlist } from "../../shared"
|
||||
import { applyModelThinkingConfig } from "./model-thinking-config"
|
||||
|
||||
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>
|
||||
\`\`\`
|
||||
|
||||
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`
|
||||
If you do not wrap your response in these tags, your analysis will not be included in the synthesis.`
|
||||
|
||||
export const COUNCIL_SOLO_ADDENDUM = `
|
||||
## Solo Analysis Mode
|
||||
@@ -129,6 +123,6 @@ export function createCouncilMemberAgent(model: string): AgentConfig {
|
||||
...restrictions,
|
||||
}
|
||||
|
||||
return applyModelThinkingConfig(base, model)
|
||||
return base
|
||||
}
|
||||
createCouncilMemberAgent.mode = MODE
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export { createAthenaAgent, ATHENA_PROMPT_METADATA } from "./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
|
||||
}
|
||||
@@ -61,36 +61,45 @@ Returns the file path to reference in subsequent task() calls.`
|
||||
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 tmpDir = join(directory, COUNCIL_TMP_DIR)
|
||||
await mkdir(tmpDir, { recursive: true })
|
||||
|
||||
const filename = `athena-council-${randomUUID().slice(0, 8)}.md`
|
||||
const filePath = join(tmpDir, filename)
|
||||
try {
|
||||
const tmpDir = join(directory, COUNCIL_TMP_DIR)
|
||||
await mkdir(tmpDir, { recursive: true })
|
||||
|
||||
const modeAddendum = mode === "delegation" ? COUNCIL_DELEGATION_ADDENDUM : COUNCIL_SOLO_ADDENDUM
|
||||
const content = `${modeAddendum}
|
||||
const filename = `athena-council-${randomUUID().slice(0, 8)}.md`
|
||||
const filePath = join(tmpDir, filename)
|
||||
|
||||
const modeAddendum = mode === "delegation" ? COUNCIL_DELEGATION_ADDENDUM : COUNCIL_SOLO_ADDENDUM
|
||||
const content = `${modeAddendum}
|
||||
|
||||
## Analysis Question
|
||||
|
||||
${args.prompt}`
|
||||
|
||||
await writeFile(filePath, content, "utf-8")
|
||||
await writeFile(filePath, content, "utf-8")
|
||||
|
||||
setTimeout(() => {
|
||||
unlink(filePath).catch((err) => {
|
||||
log("[prepare-council-prompt] Failed to clean up temp file", { filePath, error: String(err) })
|
||||
})
|
||||
}, CLEANUP_DELAY_MS)
|
||||
setTimeout(() => {
|
||||
unlink(filePath).catch((err) => {
|
||||
log("[prepare-council-prompt] Failed to clean up temp file", { filePath, error: String(err) })
|
||||
})
|
||||
}, 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:
|
||||
- prompt: "Read ${filePath} for your instructions."
|
||||
|
||||
The file auto-deletes after 30 minutes.`
|
||||
} catch (err) {
|
||||
return `Error saving council prompt: ${String(err)}`
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user