mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-29 09:03:35 +00:00
Summary: - The PR updates background-session command tokenization to avoid catastrophic regex backtracking and adds `deriveSessionName` regression tests for quoted and backslash-heavy commands. - PR surface: Source 0, Tests +26. Total +26 across 2 files. - Reproducibility: yes. with high confidence from source inspection and supplied terminal proof: current `main ... shows before/after timing for the production helper. I did not run tests because this review is read-only. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(agents): treat backslash as literal inside single-quoted session … - PR branch already contained follow-up commit before automerge: fix(agents): prevent ReDoS in background-session name derivation Validation: - ClawSweeper review passed for head0a38952fc8. - Required merge gates passed before the squash merge. Prepared head SHA:0a38952fc8Review: https://github.com/openclaw/openclaw/pull/91233#issuecomment-4643821335 Co-authored-by: yetval <yetvald@gmail.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
147 lines
4.9 KiB
TypeScript
147 lines
4.9 KiB
TypeScript
/**
|
|
* Shared bash-tool helper tests.
|
|
* Covers strict env parsing and sandbox workdir mapping between container and
|
|
* host workspace paths.
|
|
*/
|
|
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { deriveSessionName, readEnvInt, resolveSandboxWorkdir } from "./bash-tools.shared.js";
|
|
|
|
async function withTempDir(run: (dir: string) => Promise<void>) {
|
|
const dir = await mkdtemp(path.join(os.tmpdir(), "openclaw-bash-workdir-"));
|
|
try {
|
|
await run(dir);
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
describe("resolveSandboxWorkdir", () => {
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
it("reads deprecated PI env integer aliases behind OPENCLAW env names", () => {
|
|
vi.stubEnv("PI_BASH_YIELD_MS", "250");
|
|
|
|
expect(readEnvInt("OPENCLAW_BASH_YIELD_MS", "PI_BASH_YIELD_MS")).toBe(250);
|
|
|
|
vi.stubEnv("OPENCLAW_BASH_YIELD_MS", "500");
|
|
|
|
expect(readEnvInt("OPENCLAW_BASH_YIELD_MS", "PI_BASH_YIELD_MS")).toBe(500);
|
|
});
|
|
|
|
it("ignores partial environment integers", () => {
|
|
vi.stubEnv("OPENCLAW_BASH_YIELD_MS", "250ms");
|
|
vi.stubEnv("PI_BASH_YIELD_MS", "500");
|
|
|
|
expect(readEnvInt("OPENCLAW_BASH_YIELD_MS", "PI_BASH_YIELD_MS")).toBeUndefined();
|
|
});
|
|
|
|
it("reads only strict signed decimal environment integers", () => {
|
|
vi.stubEnv("OPENCLAW_BASH_YIELD_MS", "+250");
|
|
expect(readEnvInt("OPENCLAW_BASH_YIELD_MS", "PI_BASH_YIELD_MS")).toBe(250);
|
|
|
|
vi.stubEnv("OPENCLAW_BASH_YIELD_MS", "0x10");
|
|
expect(readEnvInt("OPENCLAW_BASH_YIELD_MS", "PI_BASH_YIELD_MS")).toBeUndefined();
|
|
|
|
vi.stubEnv("OPENCLAW_BASH_YIELD_MS", "1e2");
|
|
expect(readEnvInt("OPENCLAW_BASH_YIELD_MS", "PI_BASH_YIELD_MS")).toBeUndefined();
|
|
});
|
|
|
|
it("ignores unsafe environment integers", () => {
|
|
vi.stubEnv("OPENCLAW_BASH_YIELD_MS", "9007199254740993");
|
|
|
|
expect(readEnvInt("OPENCLAW_BASH_YIELD_MS", "PI_BASH_YIELD_MS")).toBeUndefined();
|
|
});
|
|
|
|
it("maps container root workdir to host workspace", async () => {
|
|
await withTempDir(async (workspaceDir) => {
|
|
const warnings: string[] = [];
|
|
const resolved = await resolveSandboxWorkdir({
|
|
workdir: "/workspace",
|
|
sandbox: {
|
|
containerName: "sandbox-1",
|
|
workspaceDir,
|
|
containerWorkdir: "/workspace",
|
|
},
|
|
warnings,
|
|
});
|
|
|
|
expect(resolved.hostWorkdir).toBe(workspaceDir);
|
|
expect(resolved.containerWorkdir).toBe("/workspace");
|
|
expect(warnings).toStrictEqual([]);
|
|
});
|
|
});
|
|
|
|
it("maps nested container workdir under the container workspace", async () => {
|
|
await withTempDir(async (workspaceDir) => {
|
|
const nested = path.join(workspaceDir, "scripts", "runner");
|
|
await mkdir(nested, { recursive: true });
|
|
const warnings: string[] = [];
|
|
const resolved = await resolveSandboxWorkdir({
|
|
workdir: "/workspace/scripts/runner",
|
|
sandbox: {
|
|
containerName: "sandbox-2",
|
|
workspaceDir,
|
|
containerWorkdir: "/workspace",
|
|
},
|
|
warnings,
|
|
});
|
|
|
|
expect(resolved.hostWorkdir).toBe(nested);
|
|
expect(resolved.containerWorkdir).toBe("/workspace/scripts/runner");
|
|
expect(warnings).toStrictEqual([]);
|
|
});
|
|
});
|
|
|
|
it("supports custom container workdir prefixes", async () => {
|
|
await withTempDir(async (workspaceDir) => {
|
|
const nested = path.join(workspaceDir, "project");
|
|
await mkdir(nested, { recursive: true });
|
|
const warnings: string[] = [];
|
|
const resolved = await resolveSandboxWorkdir({
|
|
workdir: "/sandbox-root/project",
|
|
sandbox: {
|
|
containerName: "sandbox-3",
|
|
workspaceDir,
|
|
containerWorkdir: "/sandbox-root",
|
|
},
|
|
warnings,
|
|
});
|
|
|
|
expect(resolved.hostWorkdir).toBe(nested);
|
|
expect(resolved.containerWorkdir).toBe("/sandbox-root/project");
|
|
expect(warnings).toStrictEqual([]);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("deriveSessionName", () => {
|
|
it("labels well-formed quoted commands", () => {
|
|
expect(deriveSessionName('node "my server.js" --port 8080')).toBe("node my server.js");
|
|
expect(deriveSessionName("git commit -m 'fix bug'")).toBe("git commit");
|
|
});
|
|
|
|
it("keeps grouping backslash-bearing quoted spans into one token", () => {
|
|
expect(deriveSessionName('tar "a\\b c"')).toBe("tar a\\b c");
|
|
});
|
|
|
|
it("treats backslash as literal inside single-quoted spans", () => {
|
|
expect(deriveSessionName("cmd 'a b\\' next")).toBe("cmd a b\\");
|
|
});
|
|
|
|
it("returns a label without catastrophic backtracking on unterminated quoted backslash runs", () => {
|
|
for (const quote of [`"`, `'`]) {
|
|
const malicious = `node ${quote}${"\\".repeat(50000)}`;
|
|
const start = process.hrtime.bigint();
|
|
const label = deriveSessionName(malicious);
|
|
const elapsedMs = Number(process.hrtime.bigint() - start) / 1e6;
|
|
expect(typeof label).toBe("string");
|
|
expect(elapsedMs).toBeLessThan(100);
|
|
}
|
|
});
|
|
});
|