fix: resolve 11 council-audited violations across athena subsystem

- Add switch_agent to athena-junior deny list (P1 defense-in-depth)
- Add terminal status check to waitForSessionIds polling loop
- Add athena-junior to OverridableAgentNameSchema and AgentOverridesSchema
- Add explicit retry workflow instructions to non-interactive prompt Step 9
- Fix JSON schema generation to exclude defaulted fields from required arrays
- Fix docs example for non_interactive_member_list (remove Council: prefix)
- Use segment-aware regex in path-policy.ts to block fake.sisyphus/ paths
- Add defensive path resolution and contract validation for prompt_file
- Add explicit .optional() to AthenaOverrideConfigSchema fields for clarity
- Fix traversal check precision for .. prefixed directory names
This commit is contained in:
ismeth
2026-03-03 22:37:26 +01:00
committed by YeonGyu-Kim
parent 6db83b937f
commit 62cf707502
9 changed files with 97 additions and 20 deletions
+17 -6
View File
@@ -17,7 +17,8 @@ import type { BackgroundManager } from "../../features/background-agent"
import type { CouncilConfig } from "../../config/schema/athena"
const TEST_TMP_DIR = join(tmpdir(), "athena-council-test")
const PROMPT_FILE = join(TEST_TMP_DIR, "test-prompt.md")
const SISYPHUS_TMP_DIR = join(TEST_TMP_DIR, ".sisyphus", "tmp")
const PROMPT_FILE = join(SISYPHUS_TMP_DIR, "test-prompt.md")
const makeManager = (): BackgroundManager =>
({
@@ -46,7 +47,7 @@ const makeCouncilConfig = (members?: Array<{ name: string; model: string; varian
describe("createAthenaCouncilTool", () => {
beforeEach(async () => {
await mkdir(TEST_TMP_DIR, { recursive: true })
await mkdir(SISYPHUS_TMP_DIR, { recursive: true })
await writeFile(PROMPT_FILE, "prompt file content", "utf-8")
mockLaunchCouncilMember.mockImplementation(async (member: { name: string; model: string }) => ({
member,
@@ -61,7 +62,7 @@ describe("createAthenaCouncilTool", () => {
describe("#given council is not configured (undefined)", () => {
describe("#when execute is called", () => {
it("#then returns error message about council not configured", async () => {
const tool = createAthenaCouncilTool({ backgroundManager: makeManager(), councilConfig: undefined })
const tool = createAthenaCouncilTool({ backgroundManager: makeManager(), councilConfig: undefined, directory: TEST_TMP_DIR })
const result = await tool.execute({ prompt_file: PROMPT_FILE }, makeToolContext())
expect(result).toContain("Council not configured")
})
@@ -72,7 +73,7 @@ describe("createAthenaCouncilTool", () => {
describe("#when execute is called", () => {
it("#then returns error message about council not configured", async () => {
const config = makeCouncilConfig([])
const tool = createAthenaCouncilTool({ backgroundManager: makeManager(), councilConfig: config })
const tool = createAthenaCouncilTool({ backgroundManager: makeManager(), councilConfig: config, directory: TEST_TMP_DIR })
const result = await tool.execute({ prompt_file: PROMPT_FILE }, makeToolContext())
expect(result).toContain("Council not configured")
})
@@ -85,10 +86,12 @@ describe("createAthenaCouncilTool", () => {
const tool = createAthenaCouncilTool({
backgroundManager: makeManager(),
councilConfig: makeCouncilConfig(),
directory: TEST_TMP_DIR,
})
const result = await tool.execute({ prompt_file: "/nonexistent/prompt.md" }, makeToolContext())
const nonExistentFile = join(SISYPHUS_TMP_DIR, "nonexistent.md")
const result = await tool.execute({ prompt_file: nonExistentFile }, makeToolContext())
expect(result).toContain("Failed to read prompt file")
expect(result).toContain("/nonexistent/prompt.md")
expect(result).toContain("nonexistent.md")
})
})
})
@@ -99,6 +102,7 @@ describe("createAthenaCouncilTool", () => {
const tool = createAthenaCouncilTool({
backgroundManager: makeManager(),
councilConfig: makeCouncilConfig(),
directory: TEST_TMP_DIR,
})
const result = await tool.execute(
{ prompt_file: PROMPT_FILE, members: ["NonExistentMember"] },
@@ -116,6 +120,7 @@ describe("createAthenaCouncilTool", () => {
const tool = createAthenaCouncilTool({
backgroundManager: makeManager(),
councilConfig: makeCouncilConfig(),
directory: TEST_TMP_DIR,
})
const result = await tool.execute(
{ prompt_file: PROMPT_FILE, members: ["Claude Opus"] },
@@ -137,6 +142,7 @@ describe("createAthenaCouncilTool", () => {
const tool = createAthenaCouncilTool({
backgroundManager: makeManager(),
councilConfig: makeCouncilConfig(),
directory: TEST_TMP_DIR,
})
const result = await tool.execute({ prompt_file: PROMPT_FILE }, makeToolContext())
const jsonMatch = result.match(/\{[\s\S]*\}/)
@@ -151,6 +157,7 @@ describe("createAthenaCouncilTool", () => {
const tool = createAthenaCouncilTool({
backgroundManager: makeManager(),
councilConfig: makeCouncilConfig(),
directory: TEST_TMP_DIR,
})
const result = await tool.execute({ prompt_file: PROMPT_FILE }, makeToolContext())
expect(result).toContain("background_wait")
@@ -176,6 +183,7 @@ describe("createAthenaCouncilTool", () => {
const tool = createAthenaCouncilTool({
backgroundManager: makeManager(),
councilConfig: makeCouncilConfig(),
directory: TEST_TMP_DIR,
})
const result = await tool.execute({ prompt_file: PROMPT_FILE }, makeToolContext())
const jsonMatch = result.match(/\{[\s\S]*\}/)
@@ -188,6 +196,7 @@ describe("createAthenaCouncilTool", () => {
const tool = createAthenaCouncilTool({
backgroundManager: makeManager(),
councilConfig: makeCouncilConfig(),
directory: TEST_TMP_DIR,
})
const result = await tool.execute({ prompt_file: PROMPT_FILE }, makeToolContext())
const jsonMatch = result.match(/\{[\s\S]*\}/)
@@ -211,6 +220,7 @@ describe("createAthenaCouncilTool", () => {
const tool = createAthenaCouncilTool({
backgroundManager: makeManager(),
councilConfig: makeCouncilConfig(),
directory: TEST_TMP_DIR,
})
const result = await tool.execute({ prompt_file: PROMPT_FILE }, makeToolContext())
expect(result).toContain("All council member launches failed")
@@ -221,6 +231,7 @@ describe("createAthenaCouncilTool", () => {
const tool = createAthenaCouncilTool({
backgroundManager: makeManager(),
councilConfig: makeCouncilConfig(),
directory: TEST_TMP_DIR,
})
const result = await tool.execute({ prompt_file: PROMPT_FILE }, makeToolContext())
expect(result).toContain("Claude Opus")
+11 -3
View File
@@ -1,5 +1,6 @@
import { tool, type ToolDefinition } from "@opencode-ai/plugin"
import { readFile } from "node:fs/promises"
import { resolve } from "node:path"
import type { BackgroundManager } from "../../features/background-agent"
import type { CouncilConfig, CouncilMemberConfig } from "../../config/schema/athena"
import { launchCouncilMember, type CouncilLaunchContext } from "./council-launcher"
@@ -86,9 +87,10 @@ async function waitForSessionIds(
if (task?.sessionID) {
result.set(taskId, task.sessionID)
pending.delete(taskId)
} else if (task?.status === "error" || task?.status === "cancelled" || task?.status === "interrupt") {
pending.delete(taskId)
}
}
if (pending.size > 0) {
await new Promise((resolve) => setTimeout(resolve, SESSION_WAIT_INTERVAL_MS))
}
@@ -100,8 +102,9 @@ async function waitForSessionIds(
export function createAthenaCouncilTool(args: {
backgroundManager: BackgroundManager
councilConfig: CouncilConfig | undefined
directory: string
}): ToolDefinition {
const { backgroundManager, councilConfig } = args
const { backgroundManager, councilConfig, directory } = args
const description = buildToolDescription(councilConfig)
return tool({
@@ -124,7 +127,12 @@ export function createAthenaCouncilTool(args: {
let promptContent: string
try {
promptContent = await readFile(toolArgs.prompt_file, "utf-8")
const resolvedPath = resolve(directory, toolArgs.prompt_file)
const expectedPrefix = resolve(directory, ".sisyphus/tmp")
if (!resolvedPath.startsWith(expectedPrefix)) {
return `Invalid prompt_file path: expected path within .sisyphus/tmp/, got: ${toolArgs.prompt_file}`
}
promptContent = await readFile(resolvedPath, "utf-8")
} catch (err) {
return `Failed to read prompt file: ${toolArgs.prompt_file}. Error: ${String(err)}`
}