feat(hooks): add prometheus-md-only write restriction hook

Add hook that restricts Prometheus planner to writing only .md files
in the .sisyphus/ directory. Prevents planners from implementing.
Includes test coverage.

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
YeonGyu-Kim
2026-01-05 13:50:59 +09:00
parent 166fd20a4f
commit ee2eb2174f
3 changed files with 228 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
export const HOOK_NAME = "prometheus-md-only"
export const PROMETHEUS_AGENTS = ["Prometheus (Planner)"]
export const ALLOWED_EXTENSIONS = [".md"]
export const ALLOWED_PATH_PREFIX = ".sisyphus/"
export const BLOCKED_TOOLS = ["Write", "Edit", "write", "edit"]

View File

@@ -0,0 +1,162 @@
import { describe, expect, test } from "bun:test"
import { createPrometheusMdOnlyHook } from "./index"
describe("prometheus-md-only", () => {
function createMockPluginInput() {
return {
client: {},
directory: "/tmp/test",
} as any
}
test("should block Prometheus from writing non-.md files", async () => {
// #given
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
const input = {
tool: "Write",
sessionID: "test-session",
callID: "call-1",
agent: "Prometheus (Planner)",
}
const output = {
args: { filePath: "/path/to/file.ts" },
}
// #when / #then
await expect(
hook["tool.execute.before"](input, output)
).rejects.toThrow("can only write/edit .md files")
})
test("should allow Prometheus to write .md files inside .sisyphus/", async () => {
// #given
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
const input = {
tool: "Write",
sessionID: "test-session",
callID: "call-1",
agent: "Prometheus (Planner)",
}
const output = {
args: { filePath: "/project/.sisyphus/plans/work-plan.md" },
}
// #when / #then
await expect(
hook["tool.execute.before"](input, output)
).resolves.toBeUndefined()
})
test("should block Prometheus from writing .md files outside .sisyphus/", async () => {
// #given
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
const input = {
tool: "Write",
sessionID: "test-session",
callID: "call-1",
agent: "Prometheus (Planner)",
}
const output = {
args: { filePath: "/path/to/README.md" },
}
// #when / #then
await expect(
hook["tool.execute.before"](input, output)
).rejects.toThrow("can only write/edit .md files inside .sisyphus/")
})
test("should not affect non-Prometheus agents", async () => {
// #given
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
const input = {
tool: "Write",
sessionID: "test-session",
callID: "call-1",
agent: "Sisyphus",
}
const output = {
args: { filePath: "/path/to/file.ts" },
}
// #when / #then
await expect(
hook["tool.execute.before"](input, output)
).resolves.toBeUndefined()
})
test("should not affect non-Write/Edit tools", async () => {
// #given
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
const input = {
tool: "Read",
sessionID: "test-session",
callID: "call-1",
agent: "Prometheus (Planner)",
}
const output = {
args: { filePath: "/path/to/file.ts" },
}
// #when / #then
await expect(
hook["tool.execute.before"](input, output)
).resolves.toBeUndefined()
})
test("should block Edit tool for non-.md files", async () => {
// #given
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
const input = {
tool: "Edit",
sessionID: "test-session",
callID: "call-1",
agent: "Prometheus (Planner)",
}
const output = {
args: { filePath: "/path/to/code.py" },
}
// #when / #then
await expect(
hook["tool.execute.before"](input, output)
).rejects.toThrow("can only write/edit .md files")
})
test("should handle missing filePath gracefully", async () => {
// #given
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
const input = {
tool: "Write",
sessionID: "test-session",
callID: "call-1",
agent: "Prometheus (Planner)",
}
const output = {
args: {},
}
// #when / #then
await expect(
hook["tool.execute.before"](input, output)
).resolves.toBeUndefined()
})
test("should handle missing agent gracefully", async () => {
// #given
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
const input = {
tool: "Write",
sessionID: "test-session",
callID: "call-1",
}
const output = {
args: { filePath: "/path/to/file.ts" },
}
// #when / #then
await expect(
hook["tool.execute.before"](input, output)
).resolves.toBeUndefined()
})
})

View File

@@ -0,0 +1,57 @@
import type { PluginInput } from "@opencode-ai/plugin"
import { HOOK_NAME, PROMETHEUS_AGENTS, ALLOWED_EXTENSIONS, ALLOWED_PATH_PREFIX, BLOCKED_TOOLS } from "./constants"
import { log } from "../../shared/logger"
export * from "./constants"
function isAllowedFile(filePath: string): boolean {
const hasAllowedExtension = ALLOWED_EXTENSIONS.some(ext => filePath.endsWith(ext))
const isInAllowedPath = filePath.includes(ALLOWED_PATH_PREFIX)
return hasAllowedExtension && isInAllowedPath
}
export function createPrometheusMdOnlyHook(_ctx: PluginInput) {
return {
"tool.execute.before": async (
input: { tool: string; sessionID: string; callID: string; agent?: string },
output: { args: Record<string, unknown>; message?: string }
): Promise<void> => {
const agentName = input.agent
if (!agentName || !PROMETHEUS_AGENTS.includes(agentName)) {
return
}
const toolName = input.tool
if (!BLOCKED_TOOLS.includes(toolName)) {
return
}
const filePath = (output.args.filePath ?? output.args.path ?? output.args.file) as string | undefined
if (!filePath) {
return
}
if (!isAllowedFile(filePath)) {
log(`[${HOOK_NAME}] Blocked: Prometheus can only write to .sisyphus/*.md`, {
sessionID: input.sessionID,
tool: toolName,
filePath,
agent: agentName,
})
throw new Error(
`[${HOOK_NAME}] Prometheus (Planner) can only write/edit .md files inside .sisyphus/ directory. ` +
`Attempted to modify: ${filePath}. ` +
`Prometheus is a READ-ONLY planner. Use /start-work to execute the plan.`
)
}
log(`[${HOOK_NAME}] Allowed: .sisyphus/*.md write permitted`, {
sessionID: input.sessionID,
tool: toolName,
filePath,
agent: agentName,
})
},
}
}