refactor(prometheus-hook): use lowercase config key

This commit is contained in:
justsisyphus 2026-01-23 20:49:17 +09:00
parent cc4deed8ee
commit 90292db4c4
3 changed files with 193 additions and 181 deletions

View File

@ -1,8 +1,9 @@
import { createSystemDirective, SystemDirectiveTypes } from "../../shared/system-directive" import { createSystemDirective, SystemDirectiveTypes } from "../../shared/system-directive"
import { getAgentDisplayName } from "../../shared/agent-display-names"
export const HOOK_NAME = "prometheus-md-only" export const HOOK_NAME = "prometheus-md-only"
export const PROMETHEUS_AGENTS = ["Prometheus (Planner)"] export const PROMETHEUS_AGENTS = ["prometheus"]
export const ALLOWED_EXTENSIONS = [".md"] export const ALLOWED_EXTENSIONS = [".md"]
@ -16,7 +17,7 @@ export const PLANNING_CONSULT_WARNING = `
${createSystemDirective(SystemDirectiveTypes.PROMETHEUS_READ_ONLY)} ${createSystemDirective(SystemDirectiveTypes.PROMETHEUS_READ_ONLY)}
You are being invoked by Prometheus (Planner), a READ-ONLY planning agent. You are being invoked by ${getAgentDisplayName("prometheus")}, a READ-ONLY planning agent.
**CRITICAL CONSTRAINTS:** **CRITICAL CONSTRAINTS:**
- DO NOT modify any files (no Write, Edit, or any file mutations) - DO NOT modify any files (no Write, Edit, or any file mutations)

View File

@ -41,10 +41,10 @@ describe("prometheus-md-only", () => {
} }
}) })
describe("with Prometheus agent in message storage", () => { describe("with Prometheus agent in message storage", () => {
beforeEach(() => { beforeEach(() => {
setupMessageStorage(TEST_SESSION_ID, "Prometheus (Planner)") setupMessageStorage(TEST_SESSION_ID, "prometheus")
}) })
test("should block Prometheus from writing non-.md files", async () => { test("should block Prometheus from writing non-.md files", async () => {
// #given // #given
@ -345,185 +345,195 @@ describe("prometheus-md-only", () => {
setupMessageStorage(TEST_SESSION_ID, "Prometheus (Planner)") setupMessageStorage(TEST_SESSION_ID, "Prometheus (Planner)")
}) })
test("should allow Windows-style backslash paths under .sisyphus/", async () => { test("should allow Windows-style backslash paths under .sisyphus/", async () => {
// #given // #given
const hook = createPrometheusMdOnlyHook(createMockPluginInput()) setupMessageStorage(TEST_SESSION_ID, "prometheus")
const input = { const hook = createPrometheusMdOnlyHook(createMockPluginInput())
tool: "Write", const input = {
sessionID: TEST_SESSION_ID, tool: "Write",
callID: "call-1", sessionID: TEST_SESSION_ID,
} callID: "call-1",
const output = { }
args: { filePath: ".sisyphus\\plans\\work-plan.md" }, const output = {
} args: { filePath: ".sisyphus\\plans\\work-plan.md" },
}
// #when / #then // #when / #then
await expect( await expect(
hook["tool.execute.before"](input, output) hook["tool.execute.before"](input, output)
).resolves.toBeUndefined() ).resolves.toBeUndefined()
}) })
test("should allow mixed separator paths under .sisyphus/", async () => { test("should allow mixed separator paths under .sisyphus/", async () => {
// #given // #given
const hook = createPrometheusMdOnlyHook(createMockPluginInput()) setupMessageStorage(TEST_SESSION_ID, "prometheus")
const input = { const hook = createPrometheusMdOnlyHook(createMockPluginInput())
tool: "Write", const input = {
sessionID: TEST_SESSION_ID, tool: "Write",
callID: "call-1", sessionID: TEST_SESSION_ID,
} callID: "call-1",
const output = { }
args: { filePath: ".sisyphus\\plans/work-plan.MD" }, const output = {
} args: { filePath: ".sisyphus\\plans/work-plan.MD" },
}
// #when / #then // #when / #then
await expect( await expect(
hook["tool.execute.before"](input, output) hook["tool.execute.before"](input, output)
).resolves.toBeUndefined() ).resolves.toBeUndefined()
}) })
test("should allow uppercase .MD extension", async () => { test("should allow uppercase .MD extension", async () => {
// #given // #given
const hook = createPrometheusMdOnlyHook(createMockPluginInput()) setupMessageStorage(TEST_SESSION_ID, "prometheus")
const input = { const hook = createPrometheusMdOnlyHook(createMockPluginInput())
tool: "Write", const input = {
sessionID: TEST_SESSION_ID, tool: "Write",
callID: "call-1", sessionID: TEST_SESSION_ID,
} callID: "call-1",
const output = { }
args: { filePath: ".sisyphus/plans/work-plan.MD" }, const output = {
} args: { filePath: ".sisyphus/plans/work-plan.MD" },
}
// #when / #then // #when / #then
await expect( await expect(
hook["tool.execute.before"](input, output) hook["tool.execute.before"](input, output)
).resolves.toBeUndefined() ).resolves.toBeUndefined()
}) })
test("should block paths outside workspace root even if containing .sisyphus", async () => { test("should block paths outside workspace root even if containing .sisyphus", async () => {
// #given // #given
const hook = createPrometheusMdOnlyHook(createMockPluginInput()) setupMessageStorage(TEST_SESSION_ID, "prometheus")
const input = { const hook = createPrometheusMdOnlyHook(createMockPluginInput())
tool: "Write", const input = {
sessionID: TEST_SESSION_ID, tool: "Write",
callID: "call-1", sessionID: TEST_SESSION_ID,
} callID: "call-1",
const output = { }
args: { filePath: "/other/project/.sisyphus/plans/x.md" }, const output = {
} args: { filePath: "/other/project/.sisyphus/plans/x.md" },
}
// #when / #then // #when / #then
await expect( await expect(
hook["tool.execute.before"](input, output) hook["tool.execute.before"](input, output)
).rejects.toThrow("can only write/edit .md files inside .sisyphus/") ).rejects.toThrow("can only write/edit .md files inside .sisyphus/")
}) })
test("should allow nested .sisyphus directories (ctx.directory may be parent)", async () => { test("should allow nested .sisyphus directories (ctx.directory may be parent)", async () => {
// #given - when ctx.directory is parent of actual project, path includes project name // #given - when ctx.directory is parent of actual project, path includes project name
const hook = createPrometheusMdOnlyHook(createMockPluginInput()) setupMessageStorage(TEST_SESSION_ID, "prometheus")
const input = { const hook = createPrometheusMdOnlyHook(createMockPluginInput())
tool: "Write", const input = {
sessionID: TEST_SESSION_ID, tool: "Write",
callID: "call-1", sessionID: TEST_SESSION_ID,
} callID: "call-1",
const output = { }
args: { filePath: "src/.sisyphus/plans/x.md" }, const output = {
} args: { filePath: "src/.sisyphus/plans/x.md" },
}
// #when / #then - should allow because .sisyphus is in path // #when / #then - should allow because .sisyphus is in path
await expect( await expect(
hook["tool.execute.before"](input, output) hook["tool.execute.before"](input, output)
).resolves.toBeUndefined() ).resolves.toBeUndefined()
}) })
test("should block path traversal attempts", async () => { test("should block path traversal attempts", async () => {
// #given // #given
const hook = createPrometheusMdOnlyHook(createMockPluginInput()) setupMessageStorage(TEST_SESSION_ID, "prometheus")
const input = { const hook = createPrometheusMdOnlyHook(createMockPluginInput())
tool: "Write", const input = {
sessionID: TEST_SESSION_ID, tool: "Write",
callID: "call-1", sessionID: TEST_SESSION_ID,
} callID: "call-1",
const output = { }
args: { filePath: ".sisyphus/../secrets.md" }, const output = {
} args: { filePath: ".sisyphus/../secrets.md" },
}
// #when / #then // #when / #then
await expect( await expect(
hook["tool.execute.before"](input, output) hook["tool.execute.before"](input, output)
).rejects.toThrow("can only write/edit .md files inside .sisyphus/") ).rejects.toThrow("can only write/edit .md files inside .sisyphus/")
}) })
test("should allow case-insensitive .SISYPHUS directory", async () => { test("should allow case-insensitive .SISYPHUS directory", async () => {
// #given // #given
const hook = createPrometheusMdOnlyHook(createMockPluginInput()) setupMessageStorage(TEST_SESSION_ID, "prometheus")
const input = { const hook = createPrometheusMdOnlyHook(createMockPluginInput())
tool: "Write", const input = {
sessionID: TEST_SESSION_ID, tool: "Write",
callID: "call-1", sessionID: TEST_SESSION_ID,
} callID: "call-1",
const output = { }
args: { filePath: ".SISYPHUS/plans/work-plan.md" }, const output = {
} args: { filePath: ".SISYPHUS/plans/work-plan.md" },
}
// #when / #then // #when / #then
await expect( await expect(
hook["tool.execute.before"](input, output) hook["tool.execute.before"](input, output)
).resolves.toBeUndefined() ).resolves.toBeUndefined()
}) })
test("should allow nested project path with .sisyphus (Windows real-world case)", async () => { test("should allow nested project path with .sisyphus (Windows real-world case)", async () => {
// #given - simulates when ctx.directory is parent of actual project // #given - simulates when ctx.directory is parent of actual project
// User reported: xauusd-dxy-plan\.sisyphus\drafts\supabase-email-templates.md // User reported: xauusd-dxy-plan\.sisyphus\drafts\supabase-email-templates.md
const hook = createPrometheusMdOnlyHook(createMockPluginInput()) setupMessageStorage(TEST_SESSION_ID, "prometheus")
const input = { const hook = createPrometheusMdOnlyHook(createMockPluginInput())
tool: "Write", const input = {
sessionID: TEST_SESSION_ID, tool: "Write",
callID: "call-1", sessionID: TEST_SESSION_ID,
} callID: "call-1",
const output = { }
args: { filePath: "xauusd-dxy-plan\\.sisyphus\\drafts\\supabase-email-templates.md" }, const output = {
} args: { filePath: "xauusd-dxy-plan\\.sisyphus\\drafts\\supabase-email-templates.md" },
}
// #when / #then // #when / #then
await expect( await expect(
hook["tool.execute.before"](input, output) hook["tool.execute.before"](input, output)
).resolves.toBeUndefined() ).resolves.toBeUndefined()
}) })
test("should allow nested project path with mixed separators", async () => { test("should allow nested project path with mixed separators", async () => {
// #given // #given
const hook = createPrometheusMdOnlyHook(createMockPluginInput()) setupMessageStorage(TEST_SESSION_ID, "prometheus")
const input = { const hook = createPrometheusMdOnlyHook(createMockPluginInput())
tool: "Write", const input = {
sessionID: TEST_SESSION_ID, tool: "Write",
callID: "call-1", sessionID: TEST_SESSION_ID,
} callID: "call-1",
const output = { }
args: { filePath: "my-project/.sisyphus\\plans/task.md" }, const output = {
} args: { filePath: "my-project/.sisyphus\\plans/task.md" },
}
// #when / #then // #when / #then
await expect( await expect(
hook["tool.execute.before"](input, output) hook["tool.execute.before"](input, output)
).resolves.toBeUndefined() ).resolves.toBeUndefined()
}) })
test("should block nested project path without .sisyphus", async () => { test("should block nested project path without .sisyphus", async () => {
// #given // #given
const hook = createPrometheusMdOnlyHook(createMockPluginInput()) setupMessageStorage(TEST_SESSION_ID, "prometheus")
const input = { const hook = createPrometheusMdOnlyHook(createMockPluginInput())
tool: "Write", const input = {
sessionID: TEST_SESSION_ID, tool: "Write",
callID: "call-1", sessionID: TEST_SESSION_ID,
} callID: "call-1",
const output = { }
args: { filePath: "my-project\\src\\code.ts" }, const output = {
} args: { filePath: "my-project\\src\\code.ts" },
}
// #when / #then // #when / #then
await expect( await expect(
hook["tool.execute.before"](input, output) hook["tool.execute.before"](input, output)
).rejects.toThrow("can only write/edit .md files") ).rejects.toThrow("can only write/edit .md files")
}) })
}) })
}) })

View File

@ -6,6 +6,7 @@ import { findNearestMessageWithFields, findFirstMessageWithAgent, MESSAGE_STORAG
import { getSessionAgent } from "../../features/claude-code-session-state" import { getSessionAgent } from "../../features/claude-code-session-state"
import { log } from "../../shared/logger" import { log } from "../../shared/logger"
import { SYSTEM_DIRECTIVE_PREFIX } from "../../shared/system-directive" import { SYSTEM_DIRECTIVE_PREFIX } from "../../shared/system-directive"
import { getAgentDisplayName } from "../../shared/agent-display-names"
export * from "./constants" export * from "./constants"
@ -110,20 +111,20 @@ export function createPrometheusMdOnlyHook(ctx: PluginInput) {
return return
} }
if (!isAllowedFile(filePath, ctx.directory)) { if (!isAllowedFile(filePath, ctx.directory)) {
log(`[${HOOK_NAME}] Blocked: Prometheus can only write to .sisyphus/*.md`, { log(`[${HOOK_NAME}] Blocked: Prometheus can only write to .sisyphus/*.md`, {
sessionID: input.sessionID, sessionID: input.sessionID,
tool: toolName, tool: toolName,
filePath, filePath,
agent: agentName, agent: agentName,
}) })
throw new Error( throw new Error(
`[${HOOK_NAME}] Prometheus (Planner) can only write/edit .md files inside .sisyphus/ directory. ` + `[${HOOK_NAME}] ${getAgentDisplayName("prometheus")} can only write/edit .md files inside .sisyphus/ directory. ` +
`Attempted to modify: ${filePath}. ` + `Attempted to modify: ${filePath}. ` +
`Prometheus is a READ-ONLY planner. Use /start-work to execute the plan. ` + `${getAgentDisplayName("prometheus")} is a READ-ONLY planner. Use /start-work to execute the plan. ` +
`APOLOGIZE TO THE USER, REMIND OF YOUR PLAN WRITING PROCESSES, TELL USER WHAT YOU WILL GOING TO DO AS THE PROCESS, WRITE THE PLAN` `APOLOGIZE TO THE USER, REMIND OF YOUR PLAN WRITING PROCESSES, TELL USER WHAT YOU WILL GOING TO DO AS THE PROCESS, WRITE THE PLAN`
) )
} }
const normalizedPath = filePath.toLowerCase().replace(/\\/g, "/") const normalizedPath = filePath.toLowerCase().replace(/\\/g, "/")
if (normalizedPath.includes(".sisyphus/plans/") || normalizedPath.includes(".sisyphus\\plans\\")) { if (normalizedPath.includes(".sisyphus/plans/") || normalizedPath.includes(".sisyphus\\plans\\")) {