diff --git a/plugin/gateway-core/dist/hooks/primary-worktree-guard/index.js b/plugin/gateway-core/dist/hooks/primary-worktree-guard/index.js index b41bc71..36d224d 100644 --- a/plugin/gateway-core/dist/hooks/primary-worktree-guard/index.js +++ b/plugin/gateway-core/dist/hooks/primary-worktree-guard/index.js @@ -1,5 +1,6 @@ import { execFileSync } from "node:child_process"; import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; import { writeGatewayEventAudit } from "../../audit/event-audit.js"; import { hasDisallowedShellSyntax, isAllowedProtectedShellCommand } from "../protected-shell-policy.js"; import { effectiveToolDirectory } from "../shared/effective-tool-directory.js"; @@ -25,6 +26,10 @@ function stripQuotes(token) { function shellQuote(value) { return JSON.stringify(value); } +const MAINTENANCE_HELPER = fileURLToPath(new URL("../../../../../scripts/worktree_helper_command.py", import.meta.url)); +function maintenanceHelperCommand(directory, originalCommand) { + return `python3 ${shellQuote(MAINTENANCE_HELPER)} maintenance --directory ${shellQuote(directory)} --command ${shellQuote(originalCommand)} --json`; +} const GIT_PREFIX = String.raw `(?:^|&&|\|\||;)\s*(?:env\s+(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|\S+)\s+)*)?(?:(?:[^\s;&|]*/)?rtk\s+)?(?:[^\s;&|]*/)?git\s+`; function matchBranchTarget(command, pattern) { const match = command.match(pattern); @@ -60,7 +65,7 @@ export function createPrimaryWorktreeGuardHook(options) { if (!args || !originalCommand) { return false; } - args.command = `python3 scripts/worktree_helper_command.py maintenance --directory ${shellQuote(directory)} --command ${shellQuote(originalCommand)} --json`; + args.command = maintenanceHelperCommand(directory, originalCommand); writeGatewayEventAudit(directory, { hook: "primary-worktree-guard", stage: "state", diff --git a/plugin/gateway-core/dist/hooks/workflow-conformance-guard/index.js b/plugin/gateway-core/dist/hooks/workflow-conformance-guard/index.js index 3adc8d2..c562372 100644 --- a/plugin/gateway-core/dist/hooks/workflow-conformance-guard/index.js +++ b/plugin/gateway-core/dist/hooks/workflow-conformance-guard/index.js @@ -1,5 +1,6 @@ import { execSync } from "node:child_process"; import { basename, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; import { writeGatewayEventAudit } from "../../audit/event-audit.js"; import { isAllowedProtectedShellCommand } from "../protected-shell-policy.js"; import { effectiveToolDirectory } from "../shared/effective-tool-directory.js"; @@ -38,13 +39,17 @@ function protectedBranchWorktreeHint(directory) { function shellQuote(value) { return JSON.stringify(value); } +const MAINTENANCE_HELPER = fileURLToPath(new URL("../../../../../scripts/worktree_helper_command.py", import.meta.url)); +function maintenanceHelperCommand(directory, originalCommand) { + return `python3 ${shellQuote(MAINTENANCE_HELPER)} maintenance --directory ${shellQuote(directory)} --command ${shellQuote(originalCommand)} --json`; +} function rerouteToMaintenanceHelper(payload, directory, sessionId, reasonCode) { const args = payload.output?.args; const originalCommand = typeof args?.command === "string" ? args.command.trim() : ""; if (!args || !originalCommand) { return false; } - args.command = `python3 scripts/worktree_helper_command.py maintenance --directory ${shellQuote(directory)} --command ${shellQuote(originalCommand)} --json`; + args.command = maintenanceHelperCommand(directory, originalCommand); writeGatewayEventAudit(directory, { hook: "workflow-conformance-guard", stage: "state", diff --git a/plugin/gateway-core/src/hooks/primary-worktree-guard/index.ts b/plugin/gateway-core/src/hooks/primary-worktree-guard/index.ts index 5b8c29c..3e105e3 100644 --- a/plugin/gateway-core/src/hooks/primary-worktree-guard/index.ts +++ b/plugin/gateway-core/src/hooks/primary-worktree-guard/index.ts @@ -1,5 +1,6 @@ import { execFileSync } from "node:child_process" import { resolve } from "node:path" +import { fileURLToPath } from "node:url" import { writeGatewayEventAudit } from "../../audit/event-audit.js" import { hasDisallowedShellSyntax, isAllowedProtectedShellCommand } from "../protected-shell-policy.js" @@ -46,6 +47,12 @@ function shellQuote(value: string): string { return JSON.stringify(value) } +const MAINTENANCE_HELPER = fileURLToPath(new URL("../../../../../scripts/worktree_helper_command.py", import.meta.url)) + +function maintenanceHelperCommand(directory: string, originalCommand: string): string { + return `python3 ${shellQuote(MAINTENANCE_HELPER)} maintenance --directory ${shellQuote(directory)} --command ${shellQuote(originalCommand)} --json` +} + const GIT_PREFIX = String.raw`(?:^|&&|\|\||;)\s*(?:env\s+(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|\S+)\s+)*)?(?:(?:[^\s;&|]*/)?rtk\s+)?(?:[^\s;&|]*/)?git\s+` function matchBranchTarget(command: string, pattern: RegExp): string | null { @@ -104,7 +111,7 @@ export function createPrimaryWorktreeGuardHook(options: { if (!args || !originalCommand) { return false } - args.command = `python3 scripts/worktree_helper_command.py maintenance --directory ${shellQuote(directory)} --command ${shellQuote(originalCommand)} --json` + args.command = maintenanceHelperCommand(directory, originalCommand) writeGatewayEventAudit(directory, { hook: "primary-worktree-guard", stage: "state", diff --git a/plugin/gateway-core/src/hooks/workflow-conformance-guard/index.ts b/plugin/gateway-core/src/hooks/workflow-conformance-guard/index.ts index 935f47d..688d5bc 100644 --- a/plugin/gateway-core/src/hooks/workflow-conformance-guard/index.ts +++ b/plugin/gateway-core/src/hooks/workflow-conformance-guard/index.ts @@ -1,5 +1,6 @@ import { execSync } from "node:child_process" import { basename, resolve } from "node:path" +import { fileURLToPath } from "node:url" import { writeGatewayEventAudit } from "../../audit/event-audit.js" import { isAllowedProtectedShellCommand } from "../protected-shell-policy.js" @@ -65,13 +66,19 @@ function shellQuote(value: string): string { return JSON.stringify(value) } +const MAINTENANCE_HELPER = fileURLToPath(new URL("../../../../../scripts/worktree_helper_command.py", import.meta.url)) + +function maintenanceHelperCommand(directory: string, originalCommand: string): string { + return `python3 ${shellQuote(MAINTENANCE_HELPER)} maintenance --directory ${shellQuote(directory)} --command ${shellQuote(originalCommand)} --json` +} + function rerouteToMaintenanceHelper(payload: ToolBeforePayload, directory: string, sessionId: string, reasonCode: string): boolean { const args = payload.output?.args const originalCommand = typeof args?.command === "string" ? args.command.trim() : "" if (!args || !originalCommand) { return false } - args.command = `python3 scripts/worktree_helper_command.py maintenance --directory ${shellQuote(directory)} --command ${shellQuote(originalCommand)} --json` + args.command = maintenanceHelperCommand(directory, originalCommand) writeGatewayEventAudit(directory, { hook: "workflow-conformance-guard", stage: "state", diff --git a/plugin/gateway-core/test/primary-worktree-guard-hook.test.mjs b/plugin/gateway-core/test/primary-worktree-guard-hook.test.mjs index 931e208..3fbbe05 100644 --- a/plugin/gateway-core/test/primary-worktree-guard-hook.test.mjs +++ b/plugin/gateway-core/test/primary-worktree-guard-hook.test.mjs @@ -281,21 +281,21 @@ test("primary-worktree-guard reroutes mutating bash commands in the primary work { tool: "bash", sessionID: "session-primary-bash-mutate" }, mutatePayload ) - assert.match(mutatePayload.args.command, /python3 scripts\/worktree_helper_command\.py maintenance --directory/) + assert.match(mutatePayload.args.command, /python3 ".*scripts\/worktree_helper_command\.py" maintenance --directory/) const ghPayload = { args: { command: "gh api -X POST repos/foo/bar/issues" } } await plugin["tool.execute.before"]( { tool: "bash", sessionID: "session-primary-gh-api" }, ghPayload ) - assert.match(ghPayload.args.command, /python3 scripts\/worktree_helper_command\.py maintenance --directory/) + assert.match(ghPayload.args.command, /python3 ".*scripts\/worktree_helper_command\.py" maintenance --directory/) const chainPayload = { args: { command: "git status --short --branch && echo hi > file.txt" } } await plugin["tool.execute.before"]( { tool: "bash", sessionID: "session-primary-chain" }, chainPayload ) - assert.match(chainPayload.args.command, /python3 scripts\/worktree_helper_command\.py maintenance --directory/) + assert.match(chainPayload.args.command, /python3 ".*scripts\/worktree_helper_command\.py" maintenance --directory/) await assert.rejects( plugin["tool.execute.before"]( @@ -310,7 +310,7 @@ test("primary-worktree-guard reroutes mutating bash commands in the primary work { tool: "bash", sessionID: "session-primary-redirection" }, redirectPayload ) - assert.match(redirectPayload.args.command, /python3 scripts\/worktree_helper_command\.py maintenance --directory/) + assert.match(redirectPayload.args.command, /python3 ".*scripts\/worktree_helper_command\.py" maintenance --directory/) await plugin["tool.execute.before"]( { tool: "bash", sessionID: "session-primary-bash-safe" }, diff --git a/plugin/gateway-core/test/workflow-conformance-guard-hook.test.mjs b/plugin/gateway-core/test/workflow-conformance-guard-hook.test.mjs index 49ae22f..50040fb 100644 --- a/plugin/gateway-core/test/workflow-conformance-guard-hook.test.mjs +++ b/plugin/gateway-core/test/workflow-conformance-guard-hook.test.mjs @@ -36,7 +36,7 @@ test("workflow-conformance-guard reroutes git commit on protected branch", async { tool: "bash", sessionID: "session-workflow" }, payload, ) - assert.match(payload.args.command, /python3 scripts\/worktree_helper_command\.py maintenance --directory/) + assert.match(payload.args.command, /python3 ".*scripts\/worktree_helper_command\.py" maintenance --directory/) assert.match(payload.args.command, /--command "git commit -m \\\"msg\\\"" --json/) } finally { rmSync(directory, { recursive: true, force: true }) @@ -198,7 +198,7 @@ test("workflow-conformance-guard reroutes env-prefixed git mutation commands", a { tool: "bash", sessionID: "session-workflow-env" }, payload ) - assert.match(payload.args.command, /python3 scripts\/worktree_helper_command\.py maintenance --directory/) + assert.match(payload.args.command, /python3 ".*scripts\/worktree_helper_command\.py" maintenance --directory/) } finally { rmSync(directory, { recursive: true, force: true }) } @@ -225,7 +225,7 @@ test("workflow-conformance-guard reroutes wrapped rtk git commit on protected br { tool: "bash", sessionID: "session-workflow-rtk-commit" }, payload, ) - assert.match(payload.args.command, /python3 scripts\/worktree_helper_command\.py maintenance --directory/) + assert.match(payload.args.command, /python3 ".*scripts\/worktree_helper_command\.py" maintenance --directory/) assert.match(payload.args.command, /--command "rtk git commit -m \\\"msg\\\"" --json/) } finally { rmSync(directory, { recursive: true, force: true }) @@ -313,7 +313,7 @@ test("workflow-conformance-guard reroutes mutating bash commands on protected br { tool: "bash", sessionID: "session-workflow-bash-mutate" }, mutatePayload ) - assert.match(mutatePayload.args.command, /python3 scripts\/worktree_helper_command\.py maintenance --directory/) + assert.match(mutatePayload.args.command, /python3 ".*scripts\/worktree_helper_command\.py" maintenance --directory/) assert.match(mutatePayload.args.command, /--command "echo hi > file\.txt" --json/) const ghPayload = { args: { command: "gh api -X POST repos/foo/bar/issues" } } @@ -321,7 +321,7 @@ test("workflow-conformance-guard reroutes mutating bash commands on protected br { tool: "bash", sessionID: "session-workflow-gh-api" }, ghPayload ) - assert.match(ghPayload.args.command, /python3 scripts\/worktree_helper_command\.py maintenance --directory/) + assert.match(ghPayload.args.command, /python3 ".*scripts\/worktree_helper_command\.py" maintenance --directory/) const chainPayload = { args: { command: "git status --short --branch && echo hi > file.txt" } } await plugin["tool.execute.before"]( @@ -335,21 +335,21 @@ test("workflow-conformance-guard reroutes mutating bash commands on protected br { tool: "bash", sessionID: "session-workflow-refspec-pull" }, pullPayload ) - assert.match(pullPayload.args.command, /python3 scripts\/worktree_helper_command\.py maintenance --directory/) + assert.match(pullPayload.args.command, /python3 ".*scripts\/worktree_helper_command\.py" maintenance --directory/) const fetchPayload = { args: { command: "git fetch origin +feature/x:main" } } await plugin["tool.execute.before"]( { tool: "bash", sessionID: "session-workflow-fetch-refspec" }, fetchPayload ) - assert.match(fetchPayload.args.command, /python3 scripts\/worktree_helper_command\.py maintenance --directory/) + assert.match(fetchPayload.args.command, /python3 ".*scripts\/worktree_helper_command\.py" maintenance --directory/) const redirectPayload = { args: { command: "git status --short --branch > file.txt" } } await plugin["tool.execute.before"]( { tool: "bash", sessionID: "session-workflow-redirection" }, redirectPayload ) - assert.match(redirectPayload.args.command, /python3 scripts\/worktree_helper_command\.py maintenance --directory/) + assert.match(redirectPayload.args.command, /python3 ".*scripts\/worktree_helper_command\.py" maintenance --directory/) } finally { rmSync(directory, { recursive: true, force: true }) }