Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1050,6 +1050,29 @@ function taskXmlOptionalValueEquals(xml: string, tag: string, expected: string):
return value?.trim().toLowerCase() === expected.toLowerCase();
}

function taskXmlDecodeEntities(value: string): string {
return value
.replace(/"/g, "\"")
.replace(/'/g, "'")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&amp;/g, "&");
}

/**
* Exact element-text match after one-pass decoding of XML's five predefined entities.
* Task Scheduler exports element text unescaped (`"`), while we emit `&quot;` (#608).
* Rejects zero/multiple occurrences and nested markup (no `<` inside the value).
*/
function taskXmlElementTextEquals(xml: string, tag: string, expectedDecoded: string): boolean {
if (taskXmlHasPrefixedTag(xml, tag)) return false;
const count = taskXmlElementCount(xml, tag);
if (count !== 1) return false;
const raw = new RegExp(`<${tag}(?:\\s[^>]*?)?>\\s*([^<]*?)\\s*<\\/${tag}>`, "i").exec(xml)?.[1];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve whitespace when comparing task action text

If an externally modified task contains leading or trailing whitespace in <Command> or <Arguments>, the surrounding \s* consumes that whitespace before comparison, so windowsTaskRegistrationHealthy() reports an exact match even though the registered command text differs and a whitespace-prefixed executable path may not launch. Capture the complete element text, decode it without trimming, and add a focused regression case for whitespace around the action value.

AGENTS.md reference: AGENTS.md:L93-L95

Useful? React with 👍 / 👎.

if (raw === undefined || raw.includes("<")) return false;
return taskXmlDecodeEntities(raw).trim() === expectedDecoded.trim();
}

/** Validate the security/lifecycle-critical fields of the registered scheduler task. */
export function windowsTaskRegistrationHealthy(
xml: string,
Expand All @@ -1076,8 +1099,8 @@ export function windowsTaskRegistrationHealthy(
&& taskXmlOptionalValueEquals(settings, "Enabled", "true")
&& /<MultipleInstancesPolicy>\s*IgnoreNew\s*<\/MultipleInstancesPolicy>/i.test(settings)
&& /<ExecutionTimeLimit>\s*PT0S\s*<\/ExecutionTimeLimit>/i.test(settings)
&& action.includes(`<Command>${taskXmlString(wscript)}</Command>`)
&& action.includes(`<Arguments>${taskXmlString(`/b /nologo "${launcher}"`)}</Arguments>`);
&& taskXmlElementTextEquals(action, "Command", wscript)
&& taskXmlElementTextEquals(action, "Arguments", `/b /nologo "${launcher}"`);
}

export interface WindowsSchedulerXmlState {
Expand Down
15 changes: 15 additions & 0 deletions tests/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,21 @@ describe("Windows service task", () => {
expect(xml).not.toContain("<Command>C:\\Users\\a&amp;b\\.opencodex\\opencodex-service.cmd</Command>");
});

test("accepts Task Scheduler Arguments with unescaped quotes (#608)", () => {
const wscript = "C:\\Windows\\System32\\wscript.exe";
const launcher = "C:\\Users\\Test\\.opencodex\\service-launcher.vbs";
const xml = buildWindowsTaskXml("ignored.cmd", launcher)
.replace(/<Command>.*?<\/Command>/, `<Command>${wscript}</Command>`)
// Windows exports element text unescaped — literal " not &quot;
.replace(
`<Arguments>/b /nologo &quot;${launcher}&quot;</Arguments>`,
`<Arguments>/b /nologo "${launcher}"</Arguments>`,
);
expect(xml).toContain(`<Arguments>/b /nologo "${launcher}"</Arguments>`);
expect(xml).not.toContain("&quot;");
expect(windowsTaskRegistrationHealthy(xml, wscript, launcher)).toBe(true);
});

test("validates the registered scheduler action, trigger, principal, and settings", () => {
const wscript = "C:\\Windows\\System32\\wscript.exe";
const launcher = "C:\\Users\\Test\\.opencodex\\service-launcher.vbs";
Expand Down
Loading