mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 15:40:44 +00:00
test: collapse exec preflight parser cases
This commit is contained in:
@@ -4,12 +4,13 @@ import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { __setFsSafeTestHooksForTest } from "../infra/fs-safe.js";
|
||||
import { withTempDir } from "../test-utils/temp-dir.js";
|
||||
import { createExecTool } from "./bash-tools.exec.js";
|
||||
import { __testing, createExecTool } from "./bash-tools.exec.js";
|
||||
|
||||
const isWin = process.platform === "win32";
|
||||
|
||||
const describeNonWin = isWin ? describe.skip : describe;
|
||||
const describeWin = isWin ? describe : describe.skip;
|
||||
const validateExecScriptPreflight = __testing.validateScriptFileForShellBleed;
|
||||
|
||||
afterEach(() => {
|
||||
__setFsSafeTestHooksForTest();
|
||||
@@ -17,7 +18,6 @@ afterEach(() => {
|
||||
|
||||
async function expectSymlinkSwapDuringPreflightToAvoidErrors(params: {
|
||||
hookName: "afterPreOpenLstat" | "beforeOpen";
|
||||
callId: string;
|
||||
}) {
|
||||
await withTempDir("openclaw-exec-preflight-open-race-", async (parent) => {
|
||||
const workdir = path.join(parent, "workdir");
|
||||
@@ -40,14 +40,13 @@ async function expectSymlinkSwapDuringPreflightToAvoidErrors(params: {
|
||||
},
|
||||
});
|
||||
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
const result = await tool.execute(params.callId, {
|
||||
command: "node script.js",
|
||||
workdir,
|
||||
});
|
||||
const text = result.content.find((block) => block.type === "text")?.text ?? "";
|
||||
await expect(
|
||||
validateExecScriptPreflight({
|
||||
command: "node script.js",
|
||||
workdir,
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
expect(swapped).toBe(true);
|
||||
expect(text).not.toMatch(/exec preflight:/);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -347,28 +346,24 @@ describeNonWin("exec script preflight", () => {
|
||||
await fs.mkdir(workdir, { recursive: true });
|
||||
await fs.writeFile(outsidePath, "const value = $DM_JSON;", "utf-8");
|
||||
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
const result = await tool.execute("call-outside", {
|
||||
command: "node ../outside.js",
|
||||
workdir,
|
||||
});
|
||||
const text = result.content.find((block) => block.type === "text")?.text ?? "";
|
||||
expect(text).not.toMatch(/exec preflight:/);
|
||||
await expect(
|
||||
validateExecScriptPreflight({
|
||||
command: "node ../outside.js",
|
||||
workdir,
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("does not trust a swapped script pathname between validation and read", async () => {
|
||||
await expectSymlinkSwapDuringPreflightToAvoidErrors({
|
||||
hookName: "afterPreOpenLstat",
|
||||
callId: "call-swapped-pathname",
|
||||
});
|
||||
});
|
||||
|
||||
it("handles pre-open symlink swaps without surfacing preflight errors", async () => {
|
||||
await expectSymlinkSwapDuringPreflightToAvoidErrors({
|
||||
hookName: "beforeOpen",
|
||||
callId: "call-pre-open-swapped-pathname",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -387,470 +382,75 @@ describeNonWin("exec script preflight", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
const result = await tool.execute("call-nonblocking-preflight-open", {
|
||||
command: "node script.js",
|
||||
workdir: tmp,
|
||||
});
|
||||
const text = result.content.find((block) => block.type === "text")?.text ?? "";
|
||||
await expect(
|
||||
validateExecScriptPreflight({
|
||||
command: "node script.js",
|
||||
workdir: tmp,
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
expect(scriptOpenFlags.length).toBeGreaterThan(0);
|
||||
expect(scriptOpenFlags.some((flags) => (flags & fsConstants.O_NONBLOCK) !== 0)).toBe(true);
|
||||
expect(text).not.toMatch(/exec preflight:/);
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed for piped interpreter commands that bypass direct script parsing", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const pyPath = path.join(tmp, "bad.py");
|
||||
await fs.writeFile(pyPath, "payload = $DM_JSON", "utf-8");
|
||||
const failClosedCases = [
|
||||
["piped interpreter command", "cat bad.py | python"],
|
||||
["top-level control-flow", "if true; then python bad.py; fi"],
|
||||
["multiline top-level control-flow", "if true; then\npython bad.py\nfi"],
|
||||
["shell-wrapped quoted script path", `bash -c "python 'bad.py'"`],
|
||||
["top-level control-flow with quoted script path", 'if true; then python "bad.py"; fi'],
|
||||
["shell-wrapped interpreter", 'bash -c "python bad.py"'],
|
||||
["shell-wrapped control-flow payload", 'bash -c "if true; then python bad.py; fi"'],
|
||||
["env-prefixed shell wrapper", 'env bash -c "python bad.py"'],
|
||||
["absolute shell path", '/bin/bash -c "python bad.py"'],
|
||||
["long option with separate value", 'bash --rcfile shell.rc -c "python bad.py"'],
|
||||
["leading long options", 'bash --noprofile --norc -c "python bad.py"'],
|
||||
["combined shell flags", 'bash -xc "python bad.py"'],
|
||||
["-O option value", 'bash -O extglob -c "python bad.py"'],
|
||||
["-o option value", 'bash -o errexit -c "python bad.py"'],
|
||||
["-c not trailing short flag", 'bash -ceu "python bad.py"'],
|
||||
["process substitution", "python <(cat bad.py)"],
|
||||
] as const;
|
||||
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
await expect(
|
||||
tool.execute("call-pipe", {
|
||||
command: "cat bad.py | python",
|
||||
workdir: tmp,
|
||||
}),
|
||||
).rejects.toThrow(/exec preflight: complex interpreter invocation detected/);
|
||||
});
|
||||
it.each(failClosedCases)("fails closed for %s", async (_name, command) => {
|
||||
await expect(
|
||||
validateExecScriptPreflight({
|
||||
command,
|
||||
workdir: process.cwd(),
|
||||
}),
|
||||
).rejects.toThrow(/exec preflight: complex interpreter invocation detected/);
|
||||
});
|
||||
|
||||
it("fails closed for top-level interpreter invocations inside shell control-flow", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const pyPath = path.join(tmp, "bad.py");
|
||||
await fs.writeFile(pyPath, "payload = $DM_JSON", "utf-8");
|
||||
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
await expect(
|
||||
tool.execute("call-top-level-control-flow", {
|
||||
command: "if true; then python bad.py; fi",
|
||||
workdir: tmp,
|
||||
}),
|
||||
).rejects.toThrow(/exec preflight: complex interpreter invocation detected/);
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed for multiline top-level control-flow interpreter invocations", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const pyPath = path.join(tmp, "bad.py");
|
||||
await fs.writeFile(pyPath, "payload = $DM_JSON", "utf-8");
|
||||
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
await expect(
|
||||
tool.execute("call-top-level-control-flow-multiline", {
|
||||
command: "if true; then\npython bad.py\nfi",
|
||||
workdir: tmp,
|
||||
}),
|
||||
).rejects.toThrow(/exec preflight: complex interpreter invocation detected/);
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed for shell-wrapped interpreter invocations with quoted script paths", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const pyPath = path.join(tmp, "bad.py");
|
||||
await fs.writeFile(pyPath, "payload = $DM_JSON", "utf-8");
|
||||
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
await expect(
|
||||
tool.execute("call-shell-wrap-quoted-script", {
|
||||
command: `bash -c "python '${path.basename(pyPath)}'"`,
|
||||
workdir: tmp,
|
||||
}),
|
||||
).rejects.toThrow(/exec preflight: complex interpreter invocation detected/);
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed for top-level control-flow with quoted interpreter script paths", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const pyPath = path.join(tmp, "bad.py");
|
||||
await fs.writeFile(pyPath, "payload = $DM_JSON", "utf-8");
|
||||
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
await expect(
|
||||
tool.execute("call-top-level-control-flow-quoted-script", {
|
||||
command: 'if true; then python "bad.py"; fi',
|
||||
workdir: tmp,
|
||||
}),
|
||||
).rejects.toThrow(/exec preflight: complex interpreter invocation detected/);
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed for shell-wrapped interpreter invocations", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const pyPath = path.join(tmp, "bad.py");
|
||||
await fs.writeFile(pyPath, "payload = $DM_JSON", "utf-8");
|
||||
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
await expect(
|
||||
tool.execute("call-shell-wrap", {
|
||||
command: 'bash -c "python bad.py"',
|
||||
workdir: tmp,
|
||||
}),
|
||||
).rejects.toThrow(/exec preflight: complex interpreter invocation detected/);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not fail closed for shell-wrapped payloads that only echo interpreter words", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
const result = await tool.execute("call-shell-wrap-echo-text", {
|
||||
command: 'bash -c "echo python"',
|
||||
workdir: tmp,
|
||||
});
|
||||
const text = result.content.find((block) => block.type === "text")?.text ?? "";
|
||||
expect(text).toContain("python");
|
||||
expect(text).not.toMatch(/exec preflight:/);
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed for shell-wrapped interpreter invocations inside control-flow payloads", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const pyPath = path.join(tmp, "bad.py");
|
||||
await fs.writeFile(pyPath, "payload = $DM_JSON", "utf-8");
|
||||
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
await expect(
|
||||
tool.execute("call-shell-wrap-control-flow", {
|
||||
command: 'bash -c "if true; then python bad.py; fi"',
|
||||
workdir: tmp,
|
||||
}),
|
||||
).rejects.toThrow(/exec preflight: complex interpreter invocation detected/);
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed for env-prefixed shell-wrapped interpreter invocations", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const pyPath = path.join(tmp, "bad.py");
|
||||
await fs.writeFile(pyPath, "payload = $DM_JSON", "utf-8");
|
||||
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
await expect(
|
||||
tool.execute("call-env-shell-wrap", {
|
||||
command: 'env bash -c "python bad.py"',
|
||||
workdir: tmp,
|
||||
}),
|
||||
).rejects.toThrow(/exec preflight: complex interpreter invocation detected/);
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed for shell-wrapped interpreter invocations via absolute shell paths", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const pyPath = path.join(tmp, "bad.py");
|
||||
await fs.writeFile(pyPath, "payload = $DM_JSON", "utf-8");
|
||||
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
await expect(
|
||||
tool.execute("call-shell-wrap-abs-path", {
|
||||
command: '/bin/bash -c "python bad.py"',
|
||||
workdir: tmp,
|
||||
}),
|
||||
).rejects.toThrow(/exec preflight: complex interpreter invocation detected/);
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed for shell-wrapped interpreter invocations when long options take separate values", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const pyPath = path.join(tmp, "bad.py");
|
||||
await fs.writeFile(pyPath, "payload = $DM_JSON", "utf-8");
|
||||
await fs.writeFile(path.join(tmp, "shell.rc"), "# rc", "utf-8");
|
||||
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
await expect(
|
||||
tool.execute("call-shell-wrap-long-option-value", {
|
||||
command: 'bash --rcfile shell.rc -c "python bad.py"',
|
||||
workdir: tmp,
|
||||
}),
|
||||
).rejects.toThrow(/exec preflight: complex interpreter invocation detected/);
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed for shell-wrapped interpreter invocations with leading long options", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const pyPath = path.join(tmp, "bad.py");
|
||||
await fs.writeFile(pyPath, "payload = $DM_JSON", "utf-8");
|
||||
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
await expect(
|
||||
tool.execute("call-shell-wrap-long-options", {
|
||||
command: 'bash --noprofile --norc -c "python bad.py"',
|
||||
workdir: tmp,
|
||||
}),
|
||||
).rejects.toThrow(/exec preflight: complex interpreter invocation detected/);
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed for shell-wrapped interpreter invocations with combined shell flags", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const pyPath = path.join(tmp, "bad.py");
|
||||
await fs.writeFile(pyPath, "payload = $DM_JSON", "utf-8");
|
||||
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
await expect(
|
||||
tool.execute("call-shell-wrap-combined", {
|
||||
command: 'bash -xc "python bad.py"',
|
||||
workdir: tmp,
|
||||
}),
|
||||
).rejects.toThrow(/exec preflight: complex interpreter invocation detected/);
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed for shell-wrapped interpreter invocations when -O consumes a separate value", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const pyPath = path.join(tmp, "bad.py");
|
||||
await fs.writeFile(pyPath, "payload = $DM_JSON", "utf-8");
|
||||
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
await expect(
|
||||
tool.execute("call-shell-wrap-short-option-O-value", {
|
||||
command: 'bash -O extglob -c "python bad.py"',
|
||||
workdir: tmp,
|
||||
}),
|
||||
).rejects.toThrow(/exec preflight: complex interpreter invocation detected/);
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed for shell-wrapped interpreter invocations when -o consumes a separate value", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const pyPath = path.join(tmp, "bad.py");
|
||||
await fs.writeFile(pyPath, "payload = $DM_JSON", "utf-8");
|
||||
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
await expect(
|
||||
tool.execute("call-shell-wrap-short-option-o-value", {
|
||||
command: 'bash -o errexit -c "python bad.py"',
|
||||
workdir: tmp,
|
||||
}),
|
||||
).rejects.toThrow(/exec preflight: complex interpreter invocation detected/);
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed for shell-wrapped interpreter invocations when -c is not the trailing short flag", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const pyPath = path.join(tmp, "bad.py");
|
||||
await fs.writeFile(pyPath, "payload = $DM_JSON", "utf-8");
|
||||
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
await expect(
|
||||
tool.execute("call-shell-wrap-short-flags", {
|
||||
command: 'bash -ceu "python bad.py"',
|
||||
workdir: tmp,
|
||||
}),
|
||||
).rejects.toThrow(/exec preflight: complex interpreter invocation detected/);
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed for process-substitution interpreter invocations", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const pyPath = path.join(tmp, "bad.py");
|
||||
await fs.writeFile(pyPath, "payload = $DM_JSON", "utf-8");
|
||||
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
await expect(
|
||||
tool.execute("call-process-substitution", {
|
||||
command: "python <(cat bad.py)",
|
||||
workdir: tmp,
|
||||
}),
|
||||
).rejects.toThrow(/exec preflight: complex interpreter invocation detected/);
|
||||
});
|
||||
});
|
||||
|
||||
it("allows direct inline interpreter commands with no script file hint", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
const result = await tool.execute("call-inline", {
|
||||
command: 'node -e "console.log(123)"',
|
||||
workdir: tmp,
|
||||
});
|
||||
const text = result.content.find((block) => block.type === "text")?.text ?? "";
|
||||
expect(text).toContain("123");
|
||||
expect(text).not.toMatch(/exec preflight:/);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not fail closed when interpreter and script hints only appear in echoed text", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
const result = await tool.execute("call-echo-text", {
|
||||
command: "echo 'python bad.py | python'",
|
||||
workdir: tmp,
|
||||
});
|
||||
const text = result.content.find((block) => block.type === "text")?.text ?? "";
|
||||
expect(text).toContain("python bad.py | python");
|
||||
expect(text).not.toMatch(/exec preflight:/);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not fail closed when shell keyword-like text appears only as echo arguments", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
const result = await tool.execute("call-echo-keyword-like-text", {
|
||||
command: "echo time python bad.py; cat",
|
||||
workdir: tmp,
|
||||
});
|
||||
const text = result.content.find((block) => block.type === "text")?.text ?? "";
|
||||
expect(text).toContain("time python bad.py");
|
||||
expect(text).not.toMatch(/exec preflight:/);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not fail closed for pipelines that only contain interpreter words as plain text", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
const result = await tool.execute("call-echo-pipe-text", {
|
||||
command: "echo python | cat",
|
||||
workdir: tmp,
|
||||
});
|
||||
const text = result.content.find((block) => block.type === "text")?.text ?? "";
|
||||
expect(text).toContain("python");
|
||||
expect(text).not.toMatch(/exec preflight:/);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not fail closed for non-executing pipelines that only print interpreter words", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
const result = await tool.execute("call-printf-pipe-text", {
|
||||
command: "printf node | wc -c",
|
||||
workdir: tmp,
|
||||
});
|
||||
const text = result.content.find((block) => block.type === "text")?.text ?? "";
|
||||
expect(text).toContain("4");
|
||||
expect(text).not.toMatch(/exec preflight:/);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not fail closed when script-like text is in a separate command segment", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
const result = await tool.execute("call-separate-script-hint-segment", {
|
||||
command: "echo bad.py; python --version",
|
||||
workdir: tmp,
|
||||
});
|
||||
const text = result.content.find((block) => block.type === "text")?.text ?? "";
|
||||
expect(text).toContain("bad.py");
|
||||
expect(text).not.toMatch(/exec preflight:/);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not fail closed when script hints appear outside the interpreter segment with &&", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
await fs.writeFile(path.join(tmp, "sample.py"), "print('ok')", "utf-8");
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
const result = await tool.execute("call-interpreter-version-and-list", {
|
||||
command: "node --version && ls *.py",
|
||||
workdir: tmp,
|
||||
});
|
||||
const text = result.content.find((block) => block.type === "text")?.text ?? "";
|
||||
expect(text).toContain("sample.py");
|
||||
expect(text).not.toMatch(/exec preflight:/);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not fail closed for piped interpreter version commands with script-like upstream text", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
const result = await tool.execute("call-piped-interpreter-version", {
|
||||
command: "echo bad.py | node --version",
|
||||
workdir: tmp,
|
||||
});
|
||||
const text = result.content.find((block) => block.type === "text")?.text ?? "";
|
||||
expect(text).toMatch(/v\d+/);
|
||||
expect(text).not.toMatch(/exec preflight:/);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not fail closed for piped node -c syntax-check commands with script-like upstream text", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
await fs.writeFile(path.join(tmp, "ok.js"), "console.log('ok')", "utf-8");
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
const result = await tool.execute("call-piped-node-check", {
|
||||
command: "echo bad.py | node -c ok.js",
|
||||
workdir: tmp,
|
||||
});
|
||||
const text = result.content.find((block) => block.type === "text")?.text ?? "";
|
||||
expect(text).not.toMatch(/exec preflight:/);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not fail closed for piped node -e commands when inline code contains script-like text", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
const result = await tool.execute("call-piped-node-e-inline-script-hint", {
|
||||
command: "node -e \"console.log('bad.py')\" | cat",
|
||||
workdir: tmp,
|
||||
});
|
||||
const text = result.content.find((block) => block.type === "text")?.text ?? "";
|
||||
expect(text).toContain("bad.py");
|
||||
expect(text).not.toMatch(/exec preflight:/);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not fail closed when shell operator characters are escaped", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
const result = await tool.execute("call-echo-escaped-operator", {
|
||||
command: "echo python bad.py \\| node",
|
||||
workdir: tmp,
|
||||
});
|
||||
const text = result.content.find((block) => block.type === "text")?.text ?? "";
|
||||
expect(text).toContain("python bad.py | node");
|
||||
expect(text).not.toMatch(/exec preflight:/);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not fail closed when escaped semicolons appear with interpreter hints", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
const result = await tool.execute("call-echo-escaped-semicolon", {
|
||||
command: "echo python bad.py \\; node",
|
||||
workdir: tmp,
|
||||
});
|
||||
const text = result.content.find((block) => block.type === "text")?.text ?? "";
|
||||
expect(text).toContain("python bad.py ; node");
|
||||
expect(text).not.toMatch(/exec preflight:/);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not fail closed for node -e when .py appears inside quoted inline code", async () => {
|
||||
await withTempDir("openclaw-exec-preflight-", async (tmp) => {
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
|
||||
const result = await tool.execute("call-inline-script-hint", {
|
||||
command: "node -e \"console.log('bad.py')\"",
|
||||
workdir: tmp,
|
||||
});
|
||||
const text = result.content.find((block) => block.type === "text")?.text ?? "";
|
||||
expect(text).toContain("bad.py");
|
||||
expect(text).not.toMatch(/exec preflight:/);
|
||||
});
|
||||
const passCases = [
|
||||
["shell-wrapped echoed interpreter words", 'bash -c "echo python"'],
|
||||
["direct inline interpreter command", 'node -e "console.log(123)"'],
|
||||
["interpreter and script hints only in echoed text", "echo 'python bad.py | python'"],
|
||||
["shell keyword-like text only as echo arguments", "echo time python bad.py; cat"],
|
||||
["pipeline containing only interpreter words as plain text", "echo python | cat"],
|
||||
["non-executing pipeline that only prints interpreter words", "printf node | wc -c"],
|
||||
["script-like text in a separate command segment", "echo bad.py; python --version"],
|
||||
["script hints outside interpreter segment with &&", "node --version && ls *.py"],
|
||||
[
|
||||
"piped interpreter version command with script-like upstream text",
|
||||
"echo bad.py | node --version",
|
||||
],
|
||||
["piped node -c command with script-like upstream text", "echo bad.py | node -c ok.js"],
|
||||
[
|
||||
"piped node -e command with inline script-like text",
|
||||
"node -e \"console.log('bad.py')\" | cat",
|
||||
],
|
||||
["escaped shell operator characters", "echo python bad.py \\| node"],
|
||||
["escaped semicolons with interpreter hints", "echo python bad.py \\; node"],
|
||||
["node -e with .py inside quoted inline code", "node -e \"console.log('bad.py')\""],
|
||||
] as const;
|
||||
|
||||
it.each(passCases)("does not fail closed for %s", async (_name, command) => {
|
||||
await expect(
|
||||
validateExecScriptPreflight({
|
||||
command,
|
||||
workdir: process.cwd(),
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -917,29 +517,13 @@ describeWin("exec script preflight on windows path syntax", () => {
|
||||
|
||||
describe("exec interpreter heuristics ReDoS guard", () => {
|
||||
it("does not hang on long commands with VAR=value assignments and whitespace-heavy text", async () => {
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
// Simulate a heredoc with HTML content after a VAR= assignment. Keep the
|
||||
// command-substitution failure local so the test measures parser behavior,
|
||||
// not external network timing.
|
||||
// command parser check direct so no shell process timing hides regex cost.
|
||||
const htmlBlock = '<section style="padding: 30px 20px; font-family: Arial;">'.repeat(50);
|
||||
const command = `ACCESS_TOKEN=$(__openclaw_missing_redos_guard__)\ncat > /tmp/out.html << 'EOF'\n${htmlBlock}\nEOF`;
|
||||
|
||||
const start = Date.now();
|
||||
// The command itself will fail — we only care that the interpreter
|
||||
// heuristics analysis completes without hanging.
|
||||
try {
|
||||
await Promise.race([
|
||||
tool.execute("redos-guard", { command }),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error("ReDoS: regex hung for >5s")), 5000),
|
||||
),
|
||||
]);
|
||||
} catch (e) {
|
||||
// Any error EXCEPT the timeout is acceptable — it means the regex finished
|
||||
if (e instanceof Error && e.message.includes("ReDoS")) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
await validateExecScriptPreflight({ command, workdir: process.cwd() });
|
||||
const elapsed = Date.now() - start;
|
||||
expect(elapsed).toBeLessThan(5000);
|
||||
});
|
||||
|
||||
@@ -1784,3 +1784,7 @@ export function createExecTool(
|
||||
}
|
||||
|
||||
export const execTool = createExecTool();
|
||||
|
||||
export const __testing = {
|
||||
validateScriptFileForShellBleed,
|
||||
};
|
||||
|
||||
@@ -12,8 +12,7 @@ import {
|
||||
import type { ProviderPlugin } from "../plugins/types.js";
|
||||
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
||||
import { OLLAMA_LOCAL_AUTH_MARKER } from "./model-auth-markers.js";
|
||||
import { resolveImplicitProviders } from "./models-config.providers.js";
|
||||
import type { ProviderConfig } from "./models-config.providers.js";
|
||||
import type { ProviderConfig } from "./models-config.providers.secrets.js";
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
@@ -49,15 +48,6 @@ describe("Ollama provider", () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveProvidersWithOllamaKey(agentDir: string) {
|
||||
return withOllamaApiKey(() =>
|
||||
resolveProvidersWithOllamaOnly({
|
||||
agentDir,
|
||||
env: { VITEST: "", NODE_ENV: "development" },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function resolveProvidersWithOllamaOnly(params: {
|
||||
agentDir: string;
|
||||
explicitProviders?: Record<string, ProviderConfig>;
|
||||
@@ -71,6 +61,7 @@ describe("Ollama provider", () => {
|
||||
...params.env,
|
||||
} satisfies NodeJS.ProcessEnv;
|
||||
|
||||
const { resolveImplicitProviders } = await import("./models-config.providers.implicit.js");
|
||||
return resolveImplicitProviders({
|
||||
agentDir: params.agentDir,
|
||||
explicitProviders: params.explicitProviders,
|
||||
@@ -202,7 +193,6 @@ describe("Ollama provider", () => {
|
||||
});
|
||||
|
||||
it("discovers per-model context windows from /api/show", async () => {
|
||||
const agentDir = createAgentDir();
|
||||
enableDiscoveryEnv();
|
||||
const fetchMock = vi.fn(async (input: unknown, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
@@ -230,8 +220,10 @@ describe("Ollama provider", () => {
|
||||
});
|
||||
vi.stubGlobal("fetch", withFetchPreconnect(fetchMock));
|
||||
|
||||
const providers = await resolveProvidersWithOllamaKey(agentDir);
|
||||
const models = providers?.ollama?.models ?? [];
|
||||
const provider = await runOllamaCatalog({
|
||||
env: { OLLAMA_API_KEY: "test-key", VITEST: "", NODE_ENV: "development" },
|
||||
});
|
||||
const models = provider?.models ?? [];
|
||||
const qwen = models.find((model) => model.id === "qwen3:32b");
|
||||
const llama = models.find((model) => model.id === "llama3.3:70b");
|
||||
expect(qwen?.contextWindow).toBe(131072);
|
||||
@@ -325,7 +317,6 @@ describe("Ollama provider", () => {
|
||||
});
|
||||
|
||||
it("falls back to default context window when /api/show fails", async () => {
|
||||
const agentDir = createAgentDir();
|
||||
enableDiscoveryEnv();
|
||||
const fetchMock = vi.fn(async (input: unknown) => {
|
||||
const url = String(input);
|
||||
@@ -342,14 +333,15 @@ describe("Ollama provider", () => {
|
||||
});
|
||||
vi.stubGlobal("fetch", withFetchPreconnect(fetchMock));
|
||||
|
||||
const providers = await resolveProvidersWithOllamaKey(agentDir);
|
||||
const model = providers?.ollama?.models?.find((entry) => entry.id === "qwen3:32b");
|
||||
const provider = await runOllamaCatalog({
|
||||
env: { OLLAMA_API_KEY: "test-key", VITEST: "", NODE_ENV: "development" },
|
||||
});
|
||||
const model = provider?.models?.find((entry) => entry.id === "qwen3:32b");
|
||||
expect(model?.contextWindow).toBe(128000);
|
||||
expectDiscoveryCallCounts(fetchMock, { tags: 1, show: 1 });
|
||||
});
|
||||
|
||||
it("caps /api/show requests when /api/tags returns a very large model list", async () => {
|
||||
const agentDir = createAgentDir();
|
||||
enableDiscoveryEnv();
|
||||
const manyModels = Array.from({ length: 250 }, (_, idx) => ({
|
||||
name: `model-${idx}`,
|
||||
@@ -372,8 +364,10 @@ describe("Ollama provider", () => {
|
||||
});
|
||||
vi.stubGlobal("fetch", withFetchPreconnect(fetchMock));
|
||||
|
||||
const providers = await resolveProvidersWithOllamaKey(agentDir);
|
||||
const models = providers?.ollama?.models ?? [];
|
||||
const provider = await runOllamaCatalog({
|
||||
env: { OLLAMA_API_KEY: "test-key", VITEST: "", NODE_ENV: "development" },
|
||||
});
|
||||
const models = provider?.models ?? [];
|
||||
// 1 call for /api/tags + 200 capped /api/show calls.
|
||||
expectDiscoveryCallCounts(fetchMock, { tags: 1, show: 200 });
|
||||
expect(models).toHaveLength(200);
|
||||
|
||||
Reference in New Issue
Block a user