Skip to content

Windows: install the background service without Administrator#1211

Open
RhysSullivan wants to merge 2 commits into
mainfrom
rhys/windows-no-admin-service-install
Open

Windows: install the background service without Administrator#1211
RhysSullivan wants to merge 2 commits into
mainfrom
rhys/windows-no-admin-service-install

Conversation

@RhysSullivan

Copy link
Copy Markdown
Owner

Windows: install the background service without Administrator

executor install / executor service install on Windows always required an
elevated PowerShell. It registered the daemon as a boot-triggered Scheduled Task
(AtStartup + S4U + RunLevel Highest), and Task Scheduler only lets an
Administrator create that shape. macOS (launchd RunAtLoad) and Linux
(systemd --user) both install per-user with no elevation, so Windows was the
odd one out.

What changed

  • Default to a per-user logon task: a LogonTrigger + InteractiveToken
    principal at LeastPrivilege, which a standard user is allowed to register for
    themselves. This matches the launchd/systemd behavior (start at login,
    unprivileged).
  • Boot-before-login is preserved behind --boot: executor service install --boot
    still installs the old BootTrigger/S4U/HighestAvailable task for true
    headless reboot survival. That genuinely needs Administrator, and the command
    says so when run unelevated.
  • Register via schtasks.exe, not the PowerShell *-ScheduledTask cmdlets.
    Those cmdlets reach Task Scheduler over CIM/DCOM, whose local-activation check
    fails with Access denied (0x80070005) when the compiled binary spawns the
    helper on a non-interactive window station. schtasks uses Task Scheduler RPC
    and has no such dependency. status / uninstall / restart and the
    orphaned-listener cleanup move off CIM too (schtasks /query + netstat/taskkill).
  • Hidden launcher shim. A logon task runs in the user's interactive session,
    so the daemon is launched through a hidden wscript shim to avoid flashing a
    console window on the desktop at every login. The shim waits on the wrapper, so
    the task stays Running and RestartOnFailure is preserved.

Verified on real Windows

Windows Server 2022, running as a standard, non-admin user (execstd, Medium
integrity, no UAC token):

  • executor service install --boot is correctly refused without Administrator.
  • executor service install (the new default) succeeds: registers a logon
    task (Logon Mode: Interactive only, Schedule Type: At logon time), the
    daemon comes up, and /api/health returns HTTP 200.

executor installing as a non-admin Windows user

Final state (still)

final state

Tests

apps/cli/src/service.test.ts updated: the default task XML asserts
LogonTrigger/InteractiveToken/LeastPrivilege (and no BootTrigger/S4U/
HighestAvailable), --boot asserts the boot/S4U shape, the wscript shim is
covered, and netstat listener parsing replaces the old CIM-script test.

The Windows `executor install` / `executor service install` always required
an elevated PowerShell. It registered the daemon as a boot-triggered Scheduled
Task (AtStartup + S4U + RunLevel Highest), and Task Scheduler only lets an
Administrator create that shape. macOS (launchd RunAtLoad) and Linux
(systemd --user) both install per-user with no elevation, so Windows was the
odd one out.

Default to a per-user logon task instead: a LogonTrigger + InteractiveToken
principal at LeastPrivilege, which a standard user may register for themselves.
This matches the launchd/systemd behavior (start at login, unprivileged). The
old boot-before-login behavior is preserved behind an explicit `--boot` flag,
which still needs an Administrator shell and says so.

Register via schtasks.exe rather than the PowerShell *-ScheduledTask cmdlets.
Those cmdlets reach Task Scheduler over CIM/DCOM, whose local-activation check
fails with Access denied (0x80070005) when the compiled binary spawns the
helper on a non-interactive window station; schtasks uses Task Scheduler RPC and
has no such dependency. status/uninstall/restart and the orphaned-listener
cleanup move off CIM too (schtasks query + netstat/taskkill).

A logon task runs in the user's interactive session, so launch the daemon
through a hidden wscript shim to avoid flashing a console window on the desktop
at every login; the shim waits on the wrapper so the task stays Running and
RestartOnFailure is preserved.

Verified on Windows Server 2022 as a standard, non-admin user: `--boot` is
correctly refused, the default install registers a logon task, the daemon comes
up, and /api/health returns 200.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 29, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
executor-marketing dac3e0a Commit Preview URL

Branch Preview URL
Jun 29 2026, 05:01 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 29, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
executor-cloud dac3e0a Jun 29 2026, 05:02 PM

@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Cloudflare preview

Console https://executor-preview-pr-1211.executor-e2e.workers.dev
MCP https://executor-preview-pr-1211.executor-e2e.workers.dev/mcp
Deployed commit dac3e0a

Sign-in is Cloudflare Access (one-time PIN to an allowed email). The preview has its own database and encryption key; it is destroyed when this PR closes.

@pkg-pr-new

pkg-pr-new Bot commented Jun 29, 2026

Copy link
Copy Markdown

Open in StackBlitz

@executor-js/cli

npm i https://pkg.pr.new/@executor-js/cli@1211

@executor-js/config

npm i https://pkg.pr.new/@executor-js/config@1211

@executor-js/execution

npm i https://pkg.pr.new/@executor-js/execution@1211

@executor-js/sdk

npm i https://pkg.pr.new/@executor-js/sdk@1211

@executor-js/codemode-core

npm i https://pkg.pr.new/@executor-js/codemode-core@1211

@executor-js/runtime-quickjs

npm i https://pkg.pr.new/@executor-js/runtime-quickjs@1211

@executor-js/plugin-file-secrets

npm i https://pkg.pr.new/@executor-js/plugin-file-secrets@1211

@executor-js/plugin-graphql

npm i https://pkg.pr.new/@executor-js/plugin-graphql@1211

@executor-js/plugin-keychain

npm i https://pkg.pr.new/@executor-js/plugin-keychain@1211

@executor-js/plugin-mcp

npm i https://pkg.pr.new/@executor-js/plugin-mcp@1211

@executor-js/plugin-onepassword

npm i https://pkg.pr.new/@executor-js/plugin-onepassword@1211

@executor-js/plugin-openapi

npm i https://pkg.pr.new/@executor-js/plugin-openapi@1211

executor

npm i https://pkg.pr.new/executor@1211

commit: dac3e0a

@greptile-apps

greptile-apps Bot commented Jun 29, 2026

Copy link
Copy Markdown

Greptile Summary

This PR changes the Windows service installation path so that executor service install (the default) no longer requires an Administrator shell. Instead of a boot-triggered S4U Scheduled Task registered via PowerShell CIM cmdlets, it registers a per-user logon task via schtasks.exe XML import, matching the unprivileged behavior of the macOS/Linux backends. A --boot flag preserves the old boot/S4U shape for headless servers.

  • schtasks.exe replaces PowerShell *-ScheduledTask cmdlets for all Task Scheduler operations (install, status, restart, uninstall) and netstat/tasklist/taskkill replace the CIM Get-NetTCPConnection path to avoid DCOM local-activation failures on non-interactive window stations.
  • Locale-invariant status and listener detection: running status uses the SCHED_S_TASK_RUNNING HRESULT code (267009/0x41301) rather than a localized "Status: Running" string; listener detection matches on the wildcard remote endpoint rather than the translated "LISTENING" state word.
  • VBScript shim (run-daemon.vbs) wraps the .cmd wrapper to suppress the console window that would otherwise flash at every login, while keeping the task in Running state so RestartOnFailure is preserved.

Confidence Score: 5/5

Safe to merge — the core install path works correctly and the non-admin logon-task approach is well-validated on real Windows Server 2022.

The change rewrites the Windows service backend from PowerShell CIM cmdlets to schtasks.exe XML import, adding locale-invariant status and listener detection. The locale concerns raised in previous review threads are all addressed. The only open question is whether schtasks /end blocks until the task fully terminates before the restart's /run fires — a mismatch with IgnoreNew policy would cause a silent no-op on restart, but this doesn't affect the install or status paths and is an edge case during restart.

The restart implementation in service.ts deserves a second look for the /end/run ordering guarantee.

Important Files Changed

Filename Overview
apps/cli/src/service.ts Core Windows backend rewrite: replaces PowerShell CIM cmdlets with schtasks.exe XML import, adds VBScript shim, and introduces locale-invariant status and listener parsing. Well-structured and documented; a minor timing concern exists in the restart flow.
apps/cli/src/service.test.ts Tests updated to cover the new XML task shapes (default logon and --boot), the VBScript shim, netstat listener parsing, and locale-invariant schtasks running detection including German and hex-form fixtures.
apps/cli/src/main.ts Adds --boot option to both install and service install commands, threads it through to the backend descriptor, and emits a console.warn on non-Windows so the flag isn't silently ignored.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant U as User (non-admin)
    participant CLI as executor CLI
    participant FS as Filesystem
    participant ST as schtasks.exe
    participant WS as wscript.exe (shim)
    participant D as executor daemon

    U->>CLI: executor service install
    CLI->>FS: write run-daemon.cmd (env + exec)
    CLI->>FS: write run-daemon.vbs (hidden launcher)
    CLI->>FS: write run-daemon.xml (UTF-16LE, LogonTrigger/LeastPrivilege)
    CLI->>ST: schtasks /create /tn ExecutorDaemon /xml run-daemon.xml /f
    ST-->>CLI: exit 0 (task registered)
    CLI->>ST: schtasks /run /tn ExecutorDaemon
    ST->>WS: launch wscript.exe run-daemon.vbs (hidden window)
    WS->>D: Run(run-daemon.cmd, 0, True)
    D-->>WS: daemon running (wscript blocks)
    CLI->>CLI: waitForReachable(/api/health)
    D-->>CLI: HTTP 200

    Note over U,D: On next login, Task Scheduler fires LogonTrigger → same wscript/daemon chain

    U->>CLI: executor service install --boot (needs Admin)
    CLI->>FS: write run-daemon.xml (BootTrigger/S4U/HighestAvailable)
    CLI->>ST: schtasks /create /tn ExecutorDaemon /xml ...
    alt Not elevated
        ST-->>CLI: exit non-0 (Access denied)
        CLI-->>U: error + hint to re-run elevated
    else Elevated
        ST-->>CLI: exit 0
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant U as User (non-admin)
    participant CLI as executor CLI
    participant FS as Filesystem
    participant ST as schtasks.exe
    participant WS as wscript.exe (shim)
    participant D as executor daemon

    U->>CLI: executor service install
    CLI->>FS: write run-daemon.cmd (env + exec)
    CLI->>FS: write run-daemon.vbs (hidden launcher)
    CLI->>FS: write run-daemon.xml (UTF-16LE, LogonTrigger/LeastPrivilege)
    CLI->>ST: schtasks /create /tn ExecutorDaemon /xml run-daemon.xml /f
    ST-->>CLI: exit 0 (task registered)
    CLI->>ST: schtasks /run /tn ExecutorDaemon
    ST->>WS: launch wscript.exe run-daemon.vbs (hidden window)
    WS->>D: Run(run-daemon.cmd, 0, True)
    D-->>WS: daemon running (wscript blocks)
    CLI->>CLI: waitForReachable(/api/health)
    D-->>CLI: HTTP 200

    Note over U,D: On next login, Task Scheduler fires LogonTrigger → same wscript/daemon chain

    U->>CLI: executor service install --boot (needs Admin)
    CLI->>FS: write run-daemon.xml (BootTrigger/S4U/HighestAvailable)
    CLI->>ST: schtasks /create /tn ExecutorDaemon /xml ...
    alt Not elevated
        ST-->>CLI: exit non-0 (Access denied)
        CLI-->>U: error + hint to re-run elevated
    else Elevated
        ST-->>CLI: exit 0
    end
Loading

Reviews (2): Last reviewed commit: "Windows service: locale-invariant status..." | Re-trigger Greptile

Comment thread apps/cli/src/service.ts Outdated
Comment thread apps/cli/src/main.ts
Address review on non-English Windows:

- status() derived "running" from the schtasks `Status: Running` line, but
  both the label and the word are localized (fr: `État : En cours d'exécution`),
  so it always read as not-running on a non-English install. Detect via the
  locale-invariant SCHED_S_TASK_RUNNING result code (267009 / 0x41301) instead,
  extracted into the pure, tested parseSchtasksRunning().

- parseNetstatListenerPids matched the localized `LISTENING` state word. Identify
  a listener by its wildcard/zero remote endpoint (0.0.0.0:0 / [::]:0) instead;
  TCP and addresses are not localized.

- `--boot` is documented Windows-only but was silently ignored on macOS/Linux.
  Warn when it is passed on a non-Windows platform.

Tests add fr-FR / de-DE fixtures for both parsers. Verified on the Windows guest
against a real running task's schtasks output: the old Status/Running regex
regresses to false under fr-FR rendering while the 267009-based check stays
correct.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant