fix(prometheus-md-only): allow nested project paths with .sisyphus directory
Use regex /\.sisyphus[/\\]/i instead of checking first path segment. This fixes Windows paths where ctx.directory is parent of the actual project (e.g., project\.sisyphus\drafts\...). Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
1132be370c
commit
49b0b5e085
@ -373,8 +373,8 @@ describe("prometheus-md-only", () => {
|
|||||||
).rejects.toThrow("can only write/edit .md files inside .sisyphus/")
|
).rejects.toThrow("can only write/edit .md files inside .sisyphus/")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should block nested .sisyphus directories", async () => {
|
test("should allow nested .sisyphus directories (ctx.directory may be parent)", async () => {
|
||||||
// #given
|
// #given - when ctx.directory is parent of actual project, path includes project name
|
||||||
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
|
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
|
||||||
const input = {
|
const input = {
|
||||||
tool: "Write",
|
tool: "Write",
|
||||||
@ -385,10 +385,10 @@ describe("prometheus-md-only", () => {
|
|||||||
args: { filePath: "src/.sisyphus/plans/x.md" },
|
args: { filePath: "src/.sisyphus/plans/x.md" },
|
||||||
}
|
}
|
||||||
|
|
||||||
// #when / #then
|
// #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)
|
||||||
).rejects.toThrow("can only write/edit .md files inside .sisyphus/")
|
).resolves.toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should block path traversal attempts", async () => {
|
test("should block path traversal attempts", async () => {
|
||||||
@ -426,5 +426,60 @@ describe("prometheus-md-only", () => {
|
|||||||
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 () => {
|
||||||
|
// #given - simulates when ctx.directory is parent of actual project
|
||||||
|
// User reported: xauusd-dxy-plan\.sisyphus\drafts\supabase-email-templates.md
|
||||||
|
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
|
||||||
|
const input = {
|
||||||
|
tool: "Write",
|
||||||
|
sessionID: TEST_SESSION_ID,
|
||||||
|
callID: "call-1",
|
||||||
|
}
|
||||||
|
const output = {
|
||||||
|
args: { filePath: "xauusd-dxy-plan\\.sisyphus\\drafts\\supabase-email-templates.md" },
|
||||||
|
}
|
||||||
|
|
||||||
|
// #when / #then
|
||||||
|
await expect(
|
||||||
|
hook["tool.execute.before"](input, output)
|
||||||
|
).resolves.toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should allow nested project path with mixed separators", async () => {
|
||||||
|
// #given
|
||||||
|
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
|
||||||
|
const input = {
|
||||||
|
tool: "Write",
|
||||||
|
sessionID: TEST_SESSION_ID,
|
||||||
|
callID: "call-1",
|
||||||
|
}
|
||||||
|
const output = {
|
||||||
|
args: { filePath: "my-project/.sisyphus\\plans/task.md" },
|
||||||
|
}
|
||||||
|
|
||||||
|
// #when / #then
|
||||||
|
await expect(
|
||||||
|
hook["tool.execute.before"](input, output)
|
||||||
|
).resolves.toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should block nested project path without .sisyphus", async () => {
|
||||||
|
// #given
|
||||||
|
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
|
||||||
|
const input = {
|
||||||
|
tool: "Write",
|
||||||
|
sessionID: TEST_SESSION_ID,
|
||||||
|
callID: "call-1",
|
||||||
|
}
|
||||||
|
const output = {
|
||||||
|
args: { filePath: "my-project\\src\\code.ts" },
|
||||||
|
}
|
||||||
|
|
||||||
|
// #when / #then
|
||||||
|
await expect(
|
||||||
|
hook["tool.execute.before"](input, output)
|
||||||
|
).rejects.toThrow("can only write/edit .md files")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export * from "./constants"
|
|||||||
* - Mixed separators (e.g., .sisyphus\\plans/x.md)
|
* - Mixed separators (e.g., .sisyphus\\plans/x.md)
|
||||||
* - Case-insensitive directory/extension matching
|
* - Case-insensitive directory/extension matching
|
||||||
* - Workspace confinement (blocks paths outside root or via traversal)
|
* - Workspace confinement (blocks paths outside root or via traversal)
|
||||||
|
* - Nested project paths (e.g., parent/.sisyphus/... when ctx.directory is parent)
|
||||||
*/
|
*/
|
||||||
function isAllowedFile(filePath: string, workspaceRoot: string): boolean {
|
function isAllowedFile(filePath: string, workspaceRoot: string): boolean {
|
||||||
// 1. Resolve to absolute path
|
// 1. Resolve to absolute path
|
||||||
@ -27,10 +28,9 @@ function isAllowedFile(filePath: string, workspaceRoot: string): boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Split by both separators and check first segment matches ALLOWED_PATH_PREFIX (case-insensitive)
|
// 4. Check if .sisyphus/ or .sisyphus\ exists anywhere in the path (case-insensitive)
|
||||||
// Guard: if rel is empty (filePath === workspaceRoot), segments[0] would be "" — reject
|
// This handles both direct paths (.sisyphus/x.md) and nested paths (project/.sisyphus/x.md)
|
||||||
const segments = rel.split(/[/\\]/)
|
if (!/\.sisyphus[/\\]/i.test(rel)) {
|
||||||
if (!segments[0] || segments[0].toLowerCase() !== ALLOWED_PATH_PREFIX.toLowerCase()) {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user