fix: constrain Windows task script names [AI] (#85064)

* fix: validate windows task script file names

* addressing ci

* docs: add changelog entry for PR merge
This commit is contained in:
Pavan Kumar Gondhi
2026-05-22 09:46:52 +05:30
committed by GitHub
parent 5f0bec4479
commit 229490a489
3 changed files with 100 additions and 57 deletions
+1
View File
@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- fix: constrain Windows task script names [AI]. (#85064) Thanks @pgondhi987.
- Agents/Anthropic: preserve unsafe integer tool-call input values in streamed Anthropic tool-use JSON, preventing Discord-style IDs from being rounded before dispatch. Fixes #47229. (#83063) Thanks @leno23.
- Agents/hooks: wait for local one-shot CLI and Codex `agent_end` plugin hooks before process cleanup so terminal observability flushes reliably. (#85007)
- CLI/agents: allow `openclaw agent --session-key` to target explicit session keys, including agent-scoped legacy keys. (#85121) Thanks @Kaspre.
+93 -56
View File
@@ -1,14 +1,26 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
deriveScheduledTaskRuntimeStatus,
parseSchtasksQuery,
readScheduledTaskCommand,
readScheduledTaskRuntime,
resolveTaskScriptPath,
} from "./schtasks.js";
const schtasksResponses = vi.hoisted(
(): Array<{ code: number; stdout: string; stderr: string }> => [],
);
vi.mock("./schtasks-exec.js", () => ({
execSchtasks: async () => schtasksResponses.shift() ?? { code: 0, stdout: "", stderr: "" },
}));
beforeEach(() => {
schtasksResponses.length = 0;
});
describe("schtasks runtime parsing", () => {
it.each(["Ready", "Running"])("parses %s status", (status) => {
const output = [
@@ -40,83 +52,85 @@ describe("schtasks runtime parsing", () => {
});
describe("scheduled task runtime derivation", () => {
it("treats Running + 0x41301 as running", () => {
expect(
deriveScheduledTaskRuntimeStatus({
status: "Running",
lastRunResult: "0x41301",
}),
).toEqual({ status: "running" });
async function readRuntimeFromQueryOutput(output: string) {
schtasksResponses.push(
{ code: 0, stdout: "", stderr: "" },
{ code: 0, stdout: output, stderr: "" },
);
return await readScheduledTaskRuntime({
USERPROFILE: "C:\\Users\\test",
OPENCLAW_PROFILE: "default",
});
}
function taskQueryOutput(lines: string[]): string {
return [
"TaskName: \\OpenClaw Gateway",
"Last Run Time: 1/8/2026 1:23:45 AM",
...lines,
"",
].join("\r\n");
}
it("treats Running + 0x41301 as running", async () => {
await expect(
readRuntimeFromQueryOutput(taskQueryOutput(["Status: Running", "Last Run Result: 0x41301"])),
).resolves.toMatchObject({ status: "running" });
});
it("treats Running + decimal 267009 as running", () => {
expect(
deriveScheduledTaskRuntimeStatus({
status: "Running",
lastRunResult: "267009",
}),
).toEqual({ status: "running" });
it("treats Running + decimal 267009 as running", async () => {
await expect(
readRuntimeFromQueryOutput(taskQueryOutput(["Status: Running", "Last Run Result: 267009"])),
).resolves.toMatchObject({ status: "running" });
});
it("treats Running without numeric result as unknown", () => {
expect(
deriveScheduledTaskRuntimeStatus({
status: "Running",
}),
).toEqual({
it("treats Running without numeric result as unknown", async () => {
await expect(
readRuntimeFromQueryOutput(taskQueryOutput(["Status: Running"])),
).resolves.toMatchObject({
status: "unknown",
detail: "Task status is locale-dependent and no numeric Last Run Result was available.",
});
});
it("treats non-running result codes as stopped", () => {
expect(
deriveScheduledTaskRuntimeStatus({
status: "Running",
lastRunResult: "0x0",
}),
).toEqual({
it("treats non-running result codes as stopped", async () => {
await expect(
readRuntimeFromQueryOutput(taskQueryOutput(["Status: Running", "Last Run Result: 0x0"])),
).resolves.toMatchObject({
status: "stopped",
detail: "Task Last Run Result=0x0; treating as not running.",
});
});
it("detects running via result code when status is localized (German)", () => {
expect(
deriveScheduledTaskRuntimeStatus({
status: "Wird ausgeführt",
lastRunResult: "0x41301",
}),
).toEqual({ status: "running" });
it("detects running via result code when status is localized (German)", async () => {
await expect(
readRuntimeFromQueryOutput(
taskQueryOutput(["Status: Wird ausgeführt", "Last Run Result: 0x41301"]),
),
).resolves.toMatchObject({ status: "running" });
});
it("detects running via result code when status is localized (French)", () => {
expect(
deriveScheduledTaskRuntimeStatus({
status: "En cours",
lastRunResult: "267009",
}),
).toEqual({ status: "running" });
it("detects running via result code when status is localized (French)", async () => {
await expect(
readRuntimeFromQueryOutput(taskQueryOutput(["Status: En cours", "Last Run Result: 267009"])),
).resolves.toMatchObject({ status: "running" });
});
it("treats localized status as stopped when result code is not a running code", () => {
expect(
deriveScheduledTaskRuntimeStatus({
status: "Wird ausgeführt",
lastRunResult: "0x0",
}),
).toEqual({
it("treats localized status as stopped when result code is not a running code", async () => {
await expect(
readRuntimeFromQueryOutput(
taskQueryOutput(["Status: Wird ausgeführt", "Last Run Result: 0x0"]),
),
).resolves.toMatchObject({
status: "stopped",
detail: "Task Last Run Result=0x0; treating as not running.",
});
});
it("treats localized status without result code as unknown", () => {
expect(
deriveScheduledTaskRuntimeStatus({
status: "Wird ausgeführt",
}),
).toEqual({
it("treats localized status without result code as unknown", async () => {
await expect(
readRuntimeFromQueryOutput(taskQueryOutput(["Status: Wird ausgeführt"])),
).resolves.toMatchObject({
status: "unknown",
detail: "Task status is locale-dependent and no numeric Last Run Result was available.",
});
@@ -149,9 +163,32 @@ describe("resolveTaskScriptPath", () => {
env: { HOME: "/home/test", OPENCLAW_PROFILE: "default" },
expected: path.join("/home/test", ".openclaw", "gateway.cmd"),
},
{
name: "uses a custom task script file name inside the state directory",
env: {
USERPROFILE: "C:\\Users\\test",
OPENCLAW_TASK_SCRIPT_NAME: "gateway-node.cmd",
},
expected: path.join("C:\\Users\\test", ".openclaw", "gateway-node.cmd"),
},
])("$name", ({ env, expected }) => {
expect(resolveTaskScriptPath(env)).toBe(expected);
});
it.each([
"../gateway.cmd",
"..\\gateway.cmd",
"nested/gateway.cmd",
"nested\\gateway.cmd",
"gateway..cmd",
])("rejects non-file task script name %s", (scriptName) => {
expect(() =>
resolveTaskScriptPath({
USERPROFILE: "C:\\Users\\test",
OPENCLAW_TASK_SCRIPT_NAME: scriptName,
}),
).toThrow("OPENCLAW_TASK_SCRIPT_NAME must be a file name only");
});
});
describe("readScheduledTaskCommand", () => {
+6 -1
View File
@@ -50,6 +50,11 @@ export function resolveTaskScriptPath(env: GatewayServiceEnv): string {
return override;
}
const scriptName = env.OPENCLAW_TASK_SCRIPT_NAME?.trim() || "gateway.cmd";
if (/[/\\]|\.\./.test(scriptName)) {
throw new Error(
`OPENCLAW_TASK_SCRIPT_NAME must be a file name only, not a path: ${scriptName}`,
);
}
const stateDir = resolveGatewayStateDir(env);
return path.join(stateDir, scriptName);
}
@@ -249,7 +254,7 @@ const UNKNOWN_STATUS_DETAIL =
const SCHEDULED_TASK_FALLBACK_POLL_MS = 250;
const SCHEDULED_TASK_FALLBACK_TIMEOUT_MS = 15_000;
export function deriveScheduledTaskRuntimeStatus(parsed: ScheduledTaskInfo): {
function deriveScheduledTaskRuntimeStatus(parsed: ScheduledTaskInfo): {
status: GatewayServiceRuntime["status"];
detail?: string;
} {