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:
parent
166fd20a4f
commit
ee2eb2174f
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,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user