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:
@@ -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")
|
||||
|
||||
@@ -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)}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user