diff --git a/src/sandboxes/__tests__/appendNoCommitInstruction.test.ts b/src/sandboxes/__tests__/appendNoCommitInstruction.test.ts new file mode 100644 index 0000000..54d9925 --- /dev/null +++ b/src/sandboxes/__tests__/appendNoCommitInstruction.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect } from "vitest"; +import { appendNoCommitInstruction, NO_COMMIT_INSTRUCTION } from "../appendNoCommitInstruction"; + +describe("appendNoCommitInstruction", () => { + it("appends the instruction to the value after --message", () => { + const result = appendNoCommitInstruction([ + "agent", + "--agent", + "main", + "--message", + "fix the bug", + ]); + + expect(result).toEqual([ + "agent", + "--agent", + "main", + "--message", + `fix the bug ${NO_COMMIT_INSTRUCTION}`, + ]); + }); + + it("is idempotent — does not re-append when the instruction is already present", () => { + const original = [ + "agent", + "--message", + `do something ${NO_COMMIT_INSTRUCTION}`, + ]; + + expect(appendNoCommitInstruction(original)).toEqual(original); + }); + + it("leaves args unchanged when there is no --message flag", () => { + const original = ["status"]; + expect(appendNoCommitInstruction(original)).toEqual(original); + }); + + it("leaves args unchanged when --message is the last arg (no value)", () => { + const original = ["agent", "--message"]; + expect(appendNoCommitInstruction(original)).toEqual(original); + }); + + it("returns a new array rather than mutating the input", () => { + const original = ["--message", "hi"]; + const result = appendNoCommitInstruction(original); + expect(result).not.toBe(original); + expect(original).toEqual(["--message", "hi"]); + }); +}); diff --git a/src/sandboxes/appendNoCommitInstruction.ts b/src/sandboxes/appendNoCommitInstruction.ts new file mode 100644 index 0000000..8700ddf --- /dev/null +++ b/src/sandboxes/appendNoCommitInstruction.ts @@ -0,0 +1,28 @@ +export const NO_COMMIT_INSTRUCTION = "do not commit your changes."; + +/** + * Hotfix guardrail: ensure the `--message` value passed to `openclaw agent` + * (or any tool that takes `--message `) ends with the + * {@link NO_COMMIT_INSTRUCTION} text. The task runs in a sandbox that is + * git-aware, and we push sandbox changes to GitHub after the command — we + * do NOT want the agent itself creating commits during the run. + * + * @param args - The `args` payload passed to `sandbox.runCommand`. + * @returns A new args array with the instruction appended to the `--message` + * value if one is present and the instruction isn't already there. + * Otherwise the input is returned unchanged (as a copy). + */ +export function appendNoCommitInstruction(args: readonly string[]): string[] { + const out = [...args]; + const messageIdx = out.indexOf("--message"); + if (messageIdx === -1) return out; + + const valueIdx = messageIdx + 1; + if (valueIdx >= out.length) return out; + + const current = out[valueIdx]; + if (current.includes(NO_COMMIT_INSTRUCTION)) return out; + + out[valueIdx] = `${current} ${NO_COMMIT_INSTRUCTION}`; + return out; +} diff --git a/src/tasks/runSandboxCommandTask.ts b/src/tasks/runSandboxCommandTask.ts index 857df73..d873545 100644 --- a/src/tasks/runSandboxCommandTask.ts +++ b/src/tasks/runSandboxCommandTask.ts @@ -12,6 +12,7 @@ import { ensureOrgRepos } from "../sandboxes/ensureOrgRepos"; import { ensureSetupSandbox } from "../sandboxes/ensureSetupSandbox"; import { pushSandboxToGithub } from "../sandboxes/pushSandboxToGithub"; import { syncOrgRepos } from "../sandboxes/git/syncOrgRepos"; +import { appendNoCommitInstruction } from "../sandboxes/appendNoCommitInstruction"; import { runSandboxCommandPayloadSchema, type SandboxResult, @@ -76,7 +77,7 @@ export const runSandboxCommandTask = schemaTask({ const commandResult = await sandbox.runCommand({ cmd: command, - args: args || [], + args: appendNoCommitInstruction(args || []), cwd, env: getSandboxEnv(accountId), });