Skip to content

Commit 229490a

Browse files
authored
fix: constrain Windows task script names [AI] (#85064)
* fix: validate windows task script file names * addressing ci * docs: add changelog entry for PR merge
1 parent 5f0bec4 commit 229490a

3 files changed

Lines changed: 100 additions & 57 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
2424

2525
### Fixes
2626

27+
- fix: constrain Windows task script names [AI]. (#85064) Thanks @pgondhi987.
2728
- 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.
2829
- Agents/hooks: wait for local one-shot CLI and Codex `agent_end` plugin hooks before process cleanup so terminal observability flushes reliably. (#85007)
2930
- CLI/agents: allow `openclaw agent --session-key` to target explicit session keys, including agent-scoped legacy keys. (#85121) Thanks @Kaspre.

src/daemon/schtasks.test.ts

Lines changed: 93 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
11
import fs from "node:fs/promises";
22
import os from "node:os";
33
import path from "node:path";
4-
import { describe, expect, it } from "vitest";
4+
import { beforeEach, describe, expect, it, vi } from "vitest";
55
import {
6-
deriveScheduledTaskRuntimeStatus,
76
parseSchtasksQuery,
87
readScheduledTaskCommand,
8+
readScheduledTaskRuntime,
99
resolveTaskScriptPath,
1010
} from "./schtasks.js";
1111

12+
const schtasksResponses = vi.hoisted(
13+
(): Array<{ code: number; stdout: string; stderr: string }> => [],
14+
);
15+
16+
vi.mock("./schtasks-exec.js", () => ({
17+
execSchtasks: async () => schtasksResponses.shift() ?? { code: 0, stdout: "", stderr: "" },
18+
}));
19+
20+
beforeEach(() => {
21+
schtasksResponses.length = 0;
22+
});
23+
1224
describe("schtasks runtime parsing", () => {
1325
it.each(["Ready", "Running"])("parses %s status", (status) => {
1426
const output = [
@@ -40,83 +52,85 @@ describe("schtasks runtime parsing", () => {
4052
});
4153

4254
describe("scheduled task runtime derivation", () => {
43-
it("treats Running + 0x41301 as running", () => {
44-
expect(
45-
deriveScheduledTaskRuntimeStatus({
46-
status: "Running",
47-
lastRunResult: "0x41301",
48-
}),
49-
).toEqual({ status: "running" });
55+
async function readRuntimeFromQueryOutput(output: string) {
56+
schtasksResponses.push(
57+
{ code: 0, stdout: "", stderr: "" },
58+
{ code: 0, stdout: output, stderr: "" },
59+
);
60+
return await readScheduledTaskRuntime({
61+
USERPROFILE: "C:\\Users\\test",
62+
OPENCLAW_PROFILE: "default",
63+
});
64+
}
65+
66+
function taskQueryOutput(lines: string[]): string {
67+
return [
68+
"TaskName: \\OpenClaw Gateway",
69+
"Last Run Time: 1/8/2026 1:23:45 AM",
70+
...lines,
71+
"",
72+
].join("\r\n");
73+
}
74+
75+
it("treats Running + 0x41301 as running", async () => {
76+
await expect(
77+
readRuntimeFromQueryOutput(taskQueryOutput(["Status: Running", "Last Run Result: 0x41301"])),
78+
).resolves.toMatchObject({ status: "running" });
5079
});
5180

52-
it("treats Running + decimal 267009 as running", () => {
53-
expect(
54-
deriveScheduledTaskRuntimeStatus({
55-
status: "Running",
56-
lastRunResult: "267009",
57-
}),
58-
).toEqual({ status: "running" });
81+
it("treats Running + decimal 267009 as running", async () => {
82+
await expect(
83+
readRuntimeFromQueryOutput(taskQueryOutput(["Status: Running", "Last Run Result: 267009"])),
84+
).resolves.toMatchObject({ status: "running" });
5985
});
6086

61-
it("treats Running without numeric result as unknown", () => {
62-
expect(
63-
deriveScheduledTaskRuntimeStatus({
64-
status: "Running",
65-
}),
66-
).toEqual({
87+
it("treats Running without numeric result as unknown", async () => {
88+
await expect(
89+
readRuntimeFromQueryOutput(taskQueryOutput(["Status: Running"])),
90+
).resolves.toMatchObject({
6791
status: "unknown",
6892
detail: "Task status is locale-dependent and no numeric Last Run Result was available.",
6993
});
7094
});
7195

72-
it("treats non-running result codes as stopped", () => {
73-
expect(
74-
deriveScheduledTaskRuntimeStatus({
75-
status: "Running",
76-
lastRunResult: "0x0",
77-
}),
78-
).toEqual({
96+
it("treats non-running result codes as stopped", async () => {
97+
await expect(
98+
readRuntimeFromQueryOutput(taskQueryOutput(["Status: Running", "Last Run Result: 0x0"])),
99+
).resolves.toMatchObject({
79100
status: "stopped",
80101
detail: "Task Last Run Result=0x0; treating as not running.",
81102
});
82103
});
83104

84-
it("detects running via result code when status is localized (German)", () => {
85-
expect(
86-
deriveScheduledTaskRuntimeStatus({
87-
status: "Wird ausgeführt",
88-
lastRunResult: "0x41301",
89-
}),
90-
).toEqual({ status: "running" });
105+
it("detects running via result code when status is localized (German)", async () => {
106+
await expect(
107+
readRuntimeFromQueryOutput(
108+
taskQueryOutput(["Status: Wird ausgeführt", "Last Run Result: 0x41301"]),
109+
),
110+
).resolves.toMatchObject({ status: "running" });
91111
});
92112

93-
it("detects running via result code when status is localized (French)", () => {
94-
expect(
95-
deriveScheduledTaskRuntimeStatus({
96-
status: "En cours",
97-
lastRunResult: "267009",
98-
}),
99-
).toEqual({ status: "running" });
113+
it("detects running via result code when status is localized (French)", async () => {
114+
await expect(
115+
readRuntimeFromQueryOutput(taskQueryOutput(["Status: En cours", "Last Run Result: 267009"])),
116+
).resolves.toMatchObject({ status: "running" });
100117
});
101118

102-
it("treats localized status as stopped when result code is not a running code", () => {
103-
expect(
104-
deriveScheduledTaskRuntimeStatus({
105-
status: "Wird ausgeführt",
106-
lastRunResult: "0x0",
107-
}),
108-
).toEqual({
119+
it("treats localized status as stopped when result code is not a running code", async () => {
120+
await expect(
121+
readRuntimeFromQueryOutput(
122+
taskQueryOutput(["Status: Wird ausgeführt", "Last Run Result: 0x0"]),
123+
),
124+
).resolves.toMatchObject({
109125
status: "stopped",
110126
detail: "Task Last Run Result=0x0; treating as not running.",
111127
});
112128
});
113129

114-
it("treats localized status without result code as unknown", () => {
115-
expect(
116-
deriveScheduledTaskRuntimeStatus({
117-
status: "Wird ausgeführt",
118-
}),
119-
).toEqual({
130+
it("treats localized status without result code as unknown", async () => {
131+
await expect(
132+
readRuntimeFromQueryOutput(taskQueryOutput(["Status: Wird ausgeführt"])),
133+
).resolves.toMatchObject({
120134
status: "unknown",
121135
detail: "Task status is locale-dependent and no numeric Last Run Result was available.",
122136
});
@@ -149,9 +163,32 @@ describe("resolveTaskScriptPath", () => {
149163
env: { HOME: "/home/test", OPENCLAW_PROFILE: "default" },
150164
expected: path.join("/home/test", ".openclaw", "gateway.cmd"),
151165
},
166+
{
167+
name: "uses a custom task script file name inside the state directory",
168+
env: {
169+
USERPROFILE: "C:\\Users\\test",
170+
OPENCLAW_TASK_SCRIPT_NAME: "gateway-node.cmd",
171+
},
172+
expected: path.join("C:\\Users\\test", ".openclaw", "gateway-node.cmd"),
173+
},
152174
])("$name", ({ env, expected }) => {
153175
expect(resolveTaskScriptPath(env)).toBe(expected);
154176
});
177+
178+
it.each([
179+
"../gateway.cmd",
180+
"..\\gateway.cmd",
181+
"nested/gateway.cmd",
182+
"nested\\gateway.cmd",
183+
"gateway..cmd",
184+
])("rejects non-file task script name %s", (scriptName) => {
185+
expect(() =>
186+
resolveTaskScriptPath({
187+
USERPROFILE: "C:\\Users\\test",
188+
OPENCLAW_TASK_SCRIPT_NAME: scriptName,
189+
}),
190+
).toThrow("OPENCLAW_TASK_SCRIPT_NAME must be a file name only");
191+
});
155192
});
156193

157194
describe("readScheduledTaskCommand", () => {

src/daemon/schtasks.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ export function resolveTaskScriptPath(env: GatewayServiceEnv): string {
5050
return override;
5151
}
5252
const scriptName = env.OPENCLAW_TASK_SCRIPT_NAME?.trim() || "gateway.cmd";
53+
if (/[/\\]|\.\./.test(scriptName)) {
54+
throw new Error(
55+
`OPENCLAW_TASK_SCRIPT_NAME must be a file name only, not a path: ${scriptName}`,
56+
);
57+
}
5358
const stateDir = resolveGatewayStateDir(env);
5459
return path.join(stateDir, scriptName);
5560
}
@@ -249,7 +254,7 @@ const UNKNOWN_STATUS_DETAIL =
249254
const SCHEDULED_TASK_FALLBACK_POLL_MS = 250;
250255
const SCHEDULED_TASK_FALLBACK_TIMEOUT_MS = 15_000;
251256

252-
export function deriveScheduledTaskRuntimeStatus(parsed: ScheduledTaskInfo): {
257+
function deriveScheduledTaskRuntimeStatus(parsed: ScheduledTaskInfo): {
253258
status: GatewayServiceRuntime["status"];
254259
detail?: string;
255260
} {

0 commit comments

Comments
 (0)