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:
9
src/hooks/prometheus-md-only/constants.ts
Normal file
9
src/hooks/prometheus-md-only/constants.ts
Normal 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"]
|
||||
162
src/hooks/prometheus-md-only/index.test.ts
Normal file
162
src/hooks/prometheus-md-only/index.test.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
57
src/hooks/prometheus-md-only/index.ts
Normal file
57
src/hooks/prometheus-md-only/index.ts
Normal 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,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user