diff --git a/src/daemon/schtasks.install.test.ts b/src/daemon/schtasks.install.test.ts
index 59e9cc8305a..b8a83c7f304 100644
--- a/src/daemon/schtasks.install.test.ts
+++ b/src/daemon/schtasks.install.test.ts
@@ -271,6 +271,27 @@ describe("installScheduledTask", () => {
});
});
+ it("omits /RU for workgroup accounts so schtasks can use the current local user", async () => {
+ await withUserProfileDir(async (_tmpDir, env) => {
+ schtasksResponses.push(okSchtasksResponse, missingTaskResponse);
+
+ await installDefaultGatewayTask({
+ ...env,
+ USERDOMAIN: "WORKGROUP",
+ USERNAME: "alice",
+ });
+
+ expectInitialTaskQueries();
+ const createCall = schtasksCalls[2];
+ expect(createCall?.slice(0, 5)).toEqual(["/Create", "/F", "/TN", "OpenClaw Gateway", "/XML"]);
+ expect(createCall).not.toContain("/RU");
+ const captured = xmlPayloadCaptures.find((entry) => entry.index === 2);
+ expect(captured?.xml).toContain("alice");
+ expect(captured?.xml).not.toContain("S-1-5-32-545");
+ expectTaskRunCall(3);
+ });
+ });
+
it("re-applies the XML on /Change so upgraded tasks adopt battery flags (#59299)", async () => {
await withUserProfileDir(async (_tmpDir, env) => {
// /Query yes, /Query /TN yes, /Change ok, /Create /XML ok (upgrade), /Run ok.
@@ -296,7 +317,9 @@ describe("installScheduledTask", () => {
const upgradeCapture = xmlPayloadCaptures.find((entry) => entry.index === 3);
expect(upgradeCapture).toBeDefined();
const upgradeXml = upgradeCapture?.xml ?? "";
- expect(upgradeXml).toContain("false");
+ expect(upgradeXml).toContain(
+ "false",
+ );
expect(upgradeXml).toContain("false");
expectTaskRunCall(4);
});
diff --git a/src/daemon/schtasks.ts b/src/daemon/schtasks.ts
index c65d7cf4933..ebf3d6c41ab 100644
--- a/src/daemon/schtasks.ts
+++ b/src/daemon/schtasks.ts
@@ -200,12 +200,25 @@ function resolveTaskUser(env: GatewayServiceEnv): string | null {
return username;
}
const domain = env.USERDOMAIN;
+ if (normalizeLowercaseStringOrEmpty(domain) === "workgroup") {
+ return username;
+ }
if (domain) {
return `${domain}\\${username}`;
}
return username;
}
+function resolveSchtasksCreateUser(env: GatewayServiceEnv, taskUser: string | null): string | null {
+ // Workgroup hosts can report USERDOMAIN=WORKGROUP even though schtasks wants
+ // the current local account. Keep the XML user-scoped, but omit /RU so
+ // Task Scheduler binds the task to the caller instead of prompting.
+ if (normalizeLowercaseStringOrEmpty(env.USERDOMAIN) === "workgroup") {
+ return null;
+ }
+ return taskUser;
+}
+
function shouldUseHiddenWindowsTaskLauncher(env: GatewayServiceEnv): boolean {
const value = normalizeLowercaseStringOrEmpty(env.OPENCLAW_WINDOWS_TASK_HIDDEN_LAUNCHER);
return value === "1" || value === "true" || value === "yes";
@@ -1170,9 +1183,10 @@ async function activateScheduledTask(params: {
let create: Awaited>;
try {
const xmlArgs = ["/Create", "/F", "/TN", taskName, "/XML", xmlPath];
- const xmlArgsWithUser = taskUser ? [...xmlArgs, "/RU", taskUser, "/NP"] : xmlArgs;
+ const createUser = resolveSchtasksCreateUser(params.env, taskUser);
+ const xmlArgsWithUser = createUser ? [...xmlArgs, "/RU", createUser, "/NP"] : xmlArgs;
create = await execSchtasks(xmlArgsWithUser);
- if (create.code !== 0 && taskUser) {
+ if (create.code !== 0 && createUser) {
// Retry without the elevated `/RU` form, matching the pre-XML behavior
// for accounts whose service password cannot be stored.
create = await execSchtasks(xmlArgs);