Skip to content

fix(probe): walk Windows service definition chains (Task Scheduler + WinSW) for ownership - #1154

Merged
Wibias merged 13 commits into
lidge-jun:devfrom
Wibias:fix/windows-ownership-probe
Aug 7, 2026
Merged

fix(probe): walk Windows service definition chains (Task Scheduler + WinSW) for ownership#1154
Wibias merged 13 commits into
lidge-jun:devfrom
Wibias:fix/windows-ownership-probe

Conversation

@Wibias

@Wibias Wibias commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes the Windows service-manager ownership probe, which previously hardcoded unknown ("the Windows definition chain is not inspected yet"). Because admission fails closed on unknown, every unattended Codex write on Windows — including the dashboard "Sync now" action — was refused with:

Sync failed: Refusing to write because ownership could not be proven: the Windows definition chain is not inspected yet.

The probe now walks the Windows service definition chains (Task Scheduler and WinSW) and reports real homes, so a genuine install resolves to owned and sync proceeds.

What changed

src/service-manager-probe.ts now walks the definition chains instead of refusing:

  1. Scheduled-task chain — task XML (opencodex-service-task.xml) → extract the launcher path from <Arguments>; VBS launcher → extract the batch wrapper path; batch wrapper → extract CODEX_HOME / OPENCODEX_HOME from set "NAME=..." lines (including %VAR% env-token decoding).
  2. WinSW chain — the native WinSW backend is now inspected too: XML env entries, SCM registration status, and the service exe. A missing exe or unverifiable SCM state is unknown, not absent.
  3. Registration — one bounded, read-only schtasks /query /tn opencodex-proxy /xml call. present is exit 0; absent only a definitive "task not found" message; everything else (access denied, timeout, spawn failure, null status) is unknown.
  4. Conflict handling — WinSW and a registered scheduler task together is a real conflict, and the staged scheduler claim carries its actual parsed homes rather than null placeholders. A broken/malformed scheduler chain is unknown — never fabricated into a claim.

Encoding

On-disk task XML and VBS launchers are UTF-16LE (often BOM-prefixed); they're now decoded before parsing so the on-disk and /query forms parse identically.

Fail-closed behavior preserved

  • Any broken link in a chain (missing/unreadable XML, launcher, wrapper, or exe) → unknown
  • schtasks / WinSW that cannot be asked → unknown
  • absent is only claimed when nothing is staged and the service is confirmed not registered
  • An omitted set line stays null (not ""), and the <env> value reader no longer falls back to a loose regex that could grab a wrong element's value
  • Registration is never inferred from a fabricated scheduler claim

Testing

49 tests covering: full chain extraction, env-token decoding, UTF-16 decoding, unregistered-but-staged tasks, omitted homes, broken links, missing XML, unaskable schtasks, WinSW exe-missing fail-closed, the WinSW+scheduler conflict, and ownership admission against every service-manager state. Two CI-specific test fixes:

  • The dangling-plist symlink test skips cleanly where symlinks are unavailable (Windows without Developer Mode/elevation) instead of failing with EPERM.
  • The Windows chain-walk suites wire the trusted-System32 resolver seam, so they run on the Linux CI shards that fake win32 (no kernel32).

Verified on Windows against a real ocx service install: the probe returns present with the task registered, ownership resolves to owned, and ocx sync --port 10100 completes.

The Windows service-manager probe hardcoded `unknown` ("the Windows
definition chain is not inspected yet"), so every unattended Codex
write — the dashboard "Sync now" path included — refused on Windows.

Implement the chain walk: task XML -> VBS launcher -> batch wrapper,
extracting CODEX_HOME/OPENCODEX_HOME from the wrapper's `set` lines,
and one bounded `schtasks /query /xml` call for registration. Decode
the UTF-16LE on-disk assets (task XML and VBS) so the on-disk and
registered forms parse the same. Any broken link stays `unknown`
(fail closed); absence is only claimed when nothing is staged.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Windows service-manager probing now inspects scheduled-task and WinSW definitions, follows launcher assets, extracts configured homes, decodes supported formats, classifies registration failures, and detects backend mismatches during ownership checks.

Changes

Windows service-manager inspection

Layer / File(s) Summary
Probe registration and asset decoding
src/service-manager-probe.ts, tests/codex-service-manager-probe.test.ts
The probe resolves Windows configuration paths, decodes task and launcher files, queries registration with bounded read-only schtasks calls, and distinguishes absent registrations from unknown failures.
Artifact chain validation and backend inspection
src/service-manager-probe.ts, tests/codex-service-manager-probe.test.ts
The probe follows task XML to the VBS launcher and batch wrapper, validates generated wrappers, expands environment tokens, extracts homes, and inspects WinSW definitions. Tests cover malformed, missing, unreadable, inconsistent, and UTF-16LE artifacts.
Ownership reconciliation and validation
src/integrations/native/ownership-preflight.ts, tests/codex-service-manager-probe.test.ts
Ownership inspection maps scheduler and WinSW claims to recorded state backends and returns unknown for mismatches. Tests cover matching and conflicting Windows ownership states and updated Darwin fixtures.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ServiceManagerProbe
  participant schtasks
  participant TaskXML
  participant LauncherVBS
  participant CommandWrapper
  participant WinSW
  participant OwnershipPreflight
  ServiceManagerProbe->>schtasks: query scheduled-task registration
  schtasks-->>ServiceManagerProbe: registration status
  ServiceManagerProbe->>TaskXML: read and decode task definition
  TaskXML-->>ServiceManagerProbe: launcher path
  ServiceManagerProbe->>LauncherVBS: read and decode launcher
  LauncherVBS-->>ServiceManagerProbe: wrapper path
  ServiceManagerProbe->>CommandWrapper: validate wrapper and extract homes
  CommandWrapper-->>ServiceManagerProbe: expanded home claims
  ServiceManagerProbe->>WinSW: inspect service definition and status
  WinSW-->>ServiceManagerProbe: WinSW claim
  ServiceManagerProbe->>OwnershipPreflight: compare detected and recorded backends
  OwnershipPreflight-->>ServiceManagerProbe: ownership status
Loading

Possibly related PRs

Suggested reviewers: ingwannu, lidge-jun

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: traversing Windows Task Scheduler and WinSW service-definition chains for ownership probing.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/service-manager-probe.ts`:
- Around line 369-372: Update windowsTaskRegistered to return "absent" only when
the schtasks result definitively indicates the task is not found; classify
access-denied, execution-error, signal-terminated, null-status, and all other
nonzero statuses as "unknown", while preserving "present" for status 0. Add a
regression test covering a nonzero access-denied response that expects
"unknown".
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 70fe5a5b-e1e3-4105-9be5-7c32c6a8ef22

📥 Commits

Reviewing files that changed from the base of the PR and between b39eecf and 153f8aa.

📒 Files selected for processing (2)
  • src/service-manager-probe.ts
  • tests/codex-service-manager-probe.test.ts

Comment thread src/service-manager-probe.ts Outdated
…sk probe

schtasks exits 1 for both "task not found" and "access denied"; only the
stderr message distinguishes them. Keying absence on the exit code alone
let a locked-down or wedged Task Scheduler read as a clean machine, so
`windowsTaskRegistered` now returns `absent` only when the stderr states
the task cannot be found, `present` only on exit 0, and `unknown` for
every other nonzero status, access denied, execution errors, signal
termination, null status, and spawn/timeout failures.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 153f8aab60

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +394 to +397
if (task === "absent") {
return registered === "present"
? unknown("Task Scheduler holds opencodex-proxy but its task XML is missing")
: { kind: "absent" };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Include the supported WinSW service backend in the probe

When the opt-in native backend is installed but its service-state mirror is missing or stale, this branch queries only Task Scheduler and returns absent. WinSW is a separate supported SCM registration (opencodex-proxy-native) whose ownership homes live in its XML (src/lib/winsw.ts:32-45, src/lib/winsw.ts:230-255); inspectNativeCodexOwnership then interprets no state plus this absent result as owned (src/integrations/native/ownership-preflight.ts:161-164), allowing unattended convergence to alter files still used by a foreign WinSW service. Query the SCM via the existing tri-state probe, parse the WinSW XML, and return conflict when both backends are present.

AGENTS.md reference: src/AGENTS.md:L10-L10

Useful? React with 👍 / 👎.

Comment thread src/service-manager-probe.ts Outdated

/** Registration state of the scheduled task. `unknown` when the query fails. */
function windowsTaskRegistered(deps: Required<Pick<ProbeDeps, "run">>): "present" | "absent" | "unknown" {
const queried = deps.run("schtasks", ["/query", "/tn", windowsTaskName(), "/xml"]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Resolve schtasks through the trusted system helper

When OpenCodex starts from an attacker-controlled project directory or search path on Windows, spawning the bare schtasks name permits executable search to select a planted binary instead of the system utility. This repository already provides and uses resolveTrustedWindowsSchtasksExe() specifically to obtain a verified absolute System32 path (src/lib/windows-elevation.ts:198-205, src/service.ts:735-748); use that resolver here as well so this startup/ownership probe cannot execute project-controlled code.

Useful? React with 👍 / 👎.

Comment thread src/service-manager-probe.ts Outdated
function windowsTaskRegistered(deps: Required<Pick<ProbeDeps, "run">>): "present" | "absent" | "unknown" {
const queried = deps.run("schtasks", ["/query", "/tn", windowsTaskName(), "/xml"]);
if (queried.spawnFailed || queried.timedOut) return "unknown";
return queried.status === 0 ? "present" : "absent";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Treat failed Task Scheduler queries as unknown

When schtasks exits nonzero because of access denial, an unavailable RPC/service, or another query failure, this maps the result to proven absence. The existing scheduler wrapper explicitly says to “never treat a failed query as proven absence” and falls back to a CSV listing before returning unknown (src/service.ts:764-768, src/service.ts:856-881); its tests cover access-denied and RPC failures (tests/windows-scheduler-install-verification.test.ts:123-134). With no staged XML, the new path instead returns kind: "absent", and ownership can become owned, so reuse that tri-state fallback or accept absence only after it is independently proven.

AGENTS.md reference: src/AGENTS.md:L17-L17

Useful? React with 👍 / 👎.

Comment thread src/service-manager-probe.ts Outdated
Comment on lines +370 to +372
const queried = deps.run("schtasks", ["/query", "/tn", windowsTaskName(), "/xml"]);
if (queried.spawnFailed || queried.timedOut) return "unknown";
return queried.status === 0 ? "present" : "absent";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Walk the registered XML instead of discarding it

When the registered task still points at an old or foreign launcher while a reinstall has staged a new on-disk XML, /query /xml returns the authoritative registered action in stdout, but this helper discards it and later walks only the staging copy. The resulting claim can pair the new homes with registration: "present" even though Task Scheduler will launch the old wrapper, causing the ownership preflight to accept the wrong definition. Preserve and correctly decode the queried XML, walk both definitions, and return unknown when the registered and staged chains disagree.

Useful? React with 👍 / 👎.

Comment thread src/service-manager-probe.ts Outdated
// The wrapper assets live under OPENCODEX_HOME. Defaulting to `~/.opencodex`
// mirrors service.ts defaultOpenCodexHome(); the caller can override `home`
// in tests.
return join(home, ".opencodex");

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 Locate scheduler assets under the effective OPENCODEX_HOME

When OPENCODEX_HOME is customized, service.ts writes the scheduler XML, launcher, and wrapper under getConfigDir() (src/service.ts:70-80, src/service.ts:1797-1805), while this probe always looks under <home>/.opencodex. A registered custom-home service is therefore reported as having missing XML, and an interrupted install staged only in the custom directory can be reported absent, defeating ownership detection for a supported configuration. Pass the effective/injected OpenCodex config directory into the probe and retain the default-home mirror only as an additional candidate.

AGENTS.md reference: src/AGENTS.md:L10-L10

Useful? React with 👍 / 👎.

/** Pull one `set "NAME=value"` out of a batch wrapper. */
function batchSetValue(body: string, name: string): string | null {
const match = new RegExp(`^\\s*set\\s+"${name}=([^"]*)"\\s*$`, "im").exec(body);
return match ? match[1]!.trim() : null;

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 Expand generated batch indirection before comparing homes

When an explicit CODEX_HOME or OPENCODEX_HOME lies under USERPROFILE, APPDATA, or LOCALAPPDATA, the real builder intentionally emits values such as %USERPROFILE%\.codex (src/lib/win-paths.ts:44-55), but this parser returns that batch syntax verbatim. claimNamesDifferentHome then compares it with the resolved absolute current home and reports a false disagreement; literal percent signs are similarly returned doubled. Decode the known generated indirection and batch escaping before returning the home values, and cover the probe with a wrapper produced by buildWindowsServiceScript rather than only raw-path fixtures.

AGENTS.md reference: src/AGENTS.md:L10-L10

Useful? React with 👍 / 👎.

return {
kind: "present",
claims: [{
backend: "scheduler",

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 Reject service-state backend mismatches

When a v2 service-state file records backend: "native" but the new Windows probe finds a scheduler definition whose homes agree, this claim is passed to inspectNativeCodexOwnership, which compares only the homes and returns owned. That state occurs during an interrupted or failed backend switch and does not prove which manager owns the installation; the repository's completed ownership design explicitly requires unknown when the manager backend disagrees with state.backend (devlog/_fin/260804_codex_write_substrate/041_wp12_closeout.md:763-769). Correlate each Windows manager claim with the recorded backend before admitting the write.

Useful? React with 👍 / 👎.

Comment thread src/service-manager-probe.ts Outdated
Comment on lines +440 to +441
codexHome: batchSetValue(wrapperBody, "CODEX_HOME"),
opencodexHome: batchSetValue(wrapperBody, "OPENCODEX_HOME"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject malformed wrappers instead of treating them as omissions

When the wrapper is empty, truncated, or replaced with an unrelated readable file, both calls here return null, which is indistinguishable from a legitimate install that deliberately omitted both variables. If a valid service-state file exists, inspectNativeCodexOwnership finds no disagreeing home and returns owned, so an interrupted non-atomic wrapper rewrite can authorize unattended writes despite the definition being malformed. Validate the generated wrapper's required structure and execution line before interpreting absent optional set lines; malformed content must use the existing unknown failure representation.

AGENTS.md reference: src/AGENTS.md:L17-L17

Useful? React with 👍 / 👎.

Three findings from the Codex connector review:

- P1: a malformed wrapper (empty, truncated, or an unrelated readable file)
  previously read as a legitimate install that omitted both homes. The probe
  now validates the generated wrapper structure (`:loop` + `%OCX_BUN%`/
  `%OCX_CLI%` launch tail) and returns `unknown` for anything that does not
  look generated.

- P2: home values baked by `buildWindowsServiceScript` use `%USERPROFILE%`-
  style env tokens and double literal `%` as `%%`; the probe now decodes
  that indirection against the live environment before comparing, so an
  install under the user profile no longer reports a false home disagreement.

- P2: a v2 service-state file recording backend `native` (WinSW) beside a
  scheduler task claim is an interrupted backend switch; ownership now
  returns `unknown` instead of `owned` when the manager backend disagrees
  with the recorded state.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/service-manager-probe.ts`:
- Around line 404-405: Update wrapperLooksGenerated to validate an actual
line-anchored %OCX_BUN% or %OCX_CLI% invocation using the start command after
the :loop label, rather than accepting any bun substring. Add a regression test
covering valid set lines followed by :loop and rem bun, asserting the probe
result remains unknown.
- Around line 379-395: Update decodeBatchPathValue to protect escaped percent
sequences with a sentinel before expanding environment tokens, then restore the
literal percent signs afterward so %%USERPROFILE%% remains %USERPROFILE%.
Normalize lookup names to uppercase for case-insensitive matching and use
resolved === undefined so defined empty variables expand correctly. Add
regression coverage using a controlled USERPROFILE value for both escaped and
lowercase token forms.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d5f3ad16-6c91-48d4-bc25-3fc70a7620c0

📥 Commits

Reviewing files that changed from the base of the PR and between ca015f7 and c4fb80b.

📒 Files selected for processing (3)
  • src/integrations/native/ownership-preflight.ts
  • src/service-manager-probe.ts
  • tests/codex-service-manager-probe.test.ts

Comment thread src/service-manager-probe.ts
Comment thread src/service-manager-probe.ts Outdated
CodeRabbit:
- decodeBatchPathValue: protect `%%` with a sentinel before expanding env
  tokens (%%USERPROFILE%% stays literal), normalize token names to
  uppercase for case-insensitive lookup, and use `resolved === undefined`
  so defined empty vars expand.
- wrapperLooksGenerated: require a line-anchored `"%OCX_BUN%" "%OCX_CLI%"
  start` invocation after `:loop` instead of any `bun` substring, so a
  `:loop` + `rem bun` shell is rejected as malformed.

Codex connector:
- Resolve schtasks through resolveTrustedWindowsSchtasksExe() so a planted
  binary on PATH cannot be executed.
- Walk the REGISTERED task XML (from /query /xml stdout) in addition to the
  staged on-disk definition; return unknown when the registered and staged
  chains disagree (interrupted reinstall).
- Locate scheduler assets under the effective OPENCODEX_HOME (new configDir
  probe dep) instead of always the default-home mirror.
- Include the WinSW native backend: probe the SCM registration + parse the
  WinSW XML homes, and report a conflict when both the scheduler task and
  the native service are present.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
@github-actions github-actions Bot added the bug Something isn't working label Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/service-manager-probe.ts`:
- Around line 464-467: Update walkWinswChain and its caller to route the WinSW
status check through the injected ProbeDeps.run, or introduce and use an
injectable winswStatus dependency instead of calling statusWinswRaw directly.
Ensure every Windows probe uses the bounded, observable dependency path, then
update the bounded-query test assertion to expect and verify both the schtasks
and WinSW queries.
- Around line 620-623: Update src/service-manager-probe.ts lines 620-623 to
validate the WinSW XML with the same generated-artifact check used by
wrapperLooksGenerated, returning unknown when validation fails; broaden envValue
to support single-quoted values and either attribute order. At lines 484-486,
call walkWindowsChain(deps, xml, taskXmlPath) before constructing the conflict
result and reuse its scheduler claim instead of hardcoded null homes.
- Around line 624-635: Update inspectWindows to treat a WinSW claim as installed
only when its registration is "present", rather than relying solely on
winsw.kind === "present". Apply this registration check to both the conflict
detection and the early return that selects winsw, so claims with registration
"absent" do not block ownership or count as an installed backend.
- Around line 442-446: Wrap the resolveTrustedWindowsSchtasksExe() call in the
inspectWindows probe with exception handling, returning the existing fail-closed
{ registered: "unknown", registeredXml: "" } result when resolution throws. Keep
the deps.run query and its existing spawnFailed/timedOut handling unchanged.
- Around line 484-486: Update the early conflict branch in the service-manager
probe to call walkWindowsChain before returning, then reuse its resulting
scheduler claim in the conflict result instead of constructing homes with null
values. Preserve the existing conflict kind and WinSW claim while ensuring the
staged definition’s actual home fields are retained.
- Around line 505-516: Update the registered-task validation around
walkWindowsChain so any result other than kind "present" returns unknown instead
of skipping the comparison; retain the existing homesDisagree check for
successfully walked chains. Add a regression test in the service-manager probe
tests using a registered task whose launcher path does not exist, asserting the
probe returns unknown.
- Around line 509-511: Normalize both registered and staged home paths before
the comparisons in the homesDisagree calculation: lowercase them, convert
forward slashes to backslashes, and remove trailing separators. Apply the same
normalization to codexHome and opencodexHome while preserving null handling and
the existing disagreement behavior.
- Around line 447-451: Update ProbeRunner/defaultProbeRunner to preserve
schtasks stdout and stderr as raw buffers instead of decoding them as UTF-8,
with an explicit UTF-16LE contract for Windows task output. In the
queried.status === 0 path, decode the raw schtasks /xml output using utf16le
before the existing registeredXml fallback and return behavior; do not pass
UTF-8-reconstructed strings through decodeWindowsText.
- Around line 487-496: Update the Windows probe around the winsw status handling
to return { kind: "unknown", reason: "cannot confirm native WinSW status" }
whenever winsw.kind is "unknown" and task is not "absent", before the scheduler
claim comparison can return present. Preserve the existing absent-task handling
and document the intentional tradeoff near the surrounding probe logic if
appropriate.

In `@tests/codex-service-manager-probe.test.ts`:
- Around line 465-492: Add a distinct decoy service chain under the default
<home>/.opencodex location in the scheduler asset test, including a different
CODEX_HOME value. Keep the existing custom chain and assert that
inspectServiceManagerInstallation with configDir: custom reports the custom
chain’s codexHome, proving customized OPENCODEX_HOME takes precedence over the
default mirror.
- Around line 356-359: Update the executable-path assertion in the schtasks
query test to verify the resolved path is absolute and located under the trusted
System32 directory, rather than merely containing “schtasks.” Reuse the trusted
system-directory expectation established by resolveTrustedWindowsSchtasksExe,
while preserving the existing call count and argument assertions.
- Around line 461-463: Update the test around inspectServiceManagerInstallation
to assert result.reason equals the specific reason returned by
wrapperLooksGenerated at service-manager-probe.ts, in addition to result.kind
being "unknown", so the test verifies the intended failure path rather than
another unknown outcome.
- Around line 488-492: Add focused tests in the service-manager probe test suite
covering the WinSW backend: assert kind "conflict" when both Task Scheduler and
WinSW are present, verify WinSW home paths parsed from <env .../> entries, and
assert kind "unknown" when statusWinswRaw() cannot verify SCM state. Reuse the
existing probe fixtures and helpers, and keep the current Task Scheduler
coverage unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2bdfdd34-b371-46e9-8a89-b67795128c94

📥 Commits

Reviewing files that changed from the base of the PR and between c4fb80b and ca9618a.

📒 Files selected for processing (2)
  • src/service-manager-probe.ts
  • tests/codex-service-manager-probe.test.ts

Comment thread src/service-manager-probe.ts Outdated
Comment thread src/service-manager-probe.ts Outdated
Comment thread src/service-manager-probe.ts Outdated
Comment thread src/service-manager-probe.ts Outdated
Comment thread src/service-manager-probe.ts Outdated
Comment thread src/service-manager-probe.ts
Comment thread tests/codex-service-manager-probe.test.ts
Comment thread tests/codex-service-manager-probe.test.ts Outdated
Comment thread tests/codex-service-manager-probe.test.ts
Comment thread tests/codex-service-manager-probe.test.ts Outdated
…hains

- Route the WinSW status check through an injectable winswStatus dep
  (defaults to statusWinswRaw) so every Windows probe uses the bounded,
  observable dependency path.
- Validate the WinSW XML with a generated-artifact check
  (winswXmlLooksGenerated) and broaden envValue to single/double quotes
  and either attribute order.
- Treat a WinSW claim as installed only when registration is "present",
  in both conflict detection and the early return.
- Wrap resolveTrustedWindowsSchtasksExe() in exception handling; a
  resolution failure fails closed to unknown.
- The conflict branch walks the staged scheduler definition first and
  reuses its real claim instead of null homes.
- A registered-task walk that yields anything other than "present" now
  returns unknown (not silently skipped), with a regression test for a
  registered task whose launcher is missing.
- Normalize registered vs staged homes (case, slash, trailing separator)
  before the disagreement comparison.
- Preserve schtasks stdout/stderr as raw buffers (UTF-16LE contract) via a
  new raw probe runner; decode the registered XML from the raw bytes.
- Return unknown when WinSW state is unverifiable while a scheduler task
  is also present.

Tests: decoy default-home chain in the OPENCODEX_HOME test, absolute
System32 schtasks path assertion, exact wrapper reason assertion, and new
WinSW tests (conflict, homes parsed, unverifiable SCM).

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/service-manager-probe.ts (1)

690-697: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not use an unrelated <env> element when the requested home is omitted.

If a WinSW XML contains only CODEX_HOME, envValue("OPENCODEX_HOME") fails at Line 693 and Line 694 selects the first <env> element. The function then reports the CODEX_HOME value for both homes instead of reporting opencodexHome: null.

Remove the fallback. The first expression already accepts either attribute order and both quote styles. Add a fixture with only one home variable and assert that the other home remains null.

Proposed fix
-    const tag = new RegExp(`<env\\b[^>]*\\bname=["']${name}["'][^>]*>`, "i").exec(body)
-      ?? new RegExp(`<env\\b[^>]*>[^<]*`, "i").exec(body);
+    const tag = new RegExp(`<env\\b[^>]*\\bname=["']${name}["'][^>]*>`, "i").exec(body);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/service-manager-probe.ts` around lines 690 - 697, Remove the fallback
RegExp in envValue so a missing requested name returns null instead of matching
an unrelated env element; retain the existing name-matching expression for both
attribute orders and quote styles, and add a fixture containing only one home
variable that verifies the other home remains null.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/service-manager-probe.ts`:
- Around line 524-535: Update the winswInstalled && schedulerPresent branch to
return staged unchanged whenever walkWindowsChain returns a non-present result,
including unknown. For a present staged claim, build the conflict using that
claim with registration set to "present" rather than fabricating null homes;
preserve the WinSW claim. Add coverage for a broken scheduler chain and verify
registration on a valid conflict.

---

Duplicate comments:
In `@src/service-manager-probe.ts`:
- Around line 690-697: Remove the fallback RegExp in envValue so a missing
requested name returns null instead of matching an unrelated env element; retain
the existing name-matching expression for both attribute orders and quote
styles, and add a fixture containing only one home variable that verifies the
other home remains null.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ffeeca59-b7f8-4d83-b0c4-e4975c40bcb7

📥 Commits

Reviewing files that changed from the base of the PR and between ca9618a and 83a21cc.

📒 Files selected for processing (2)
  • src/service-manager-probe.ts
  • tests/codex-service-manager-probe.test.ts

Comment thread src/service-manager-probe.ts Outdated
Wibias and others added 4 commits August 7, 2026 03:21
The WinSW XML without its paired exe cannot prove a verifiable SCM claim;
add a regression test asserting the probe returns unknown rather than an
owned WinSW backend.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- When WinSW and a scheduler task are both present, a broken/malformed
  staged scheduler chain now returns unknown instead of fabricating a
  null-homes claim; a valid staged claim is a conflict with registration
  present (no invented placeholder homes).
- WinSW absence now requires both the XML and the service exe to be
  missing, so a missing binary is unknown, not "absent".
- The <env> value reader drops its loose fallback regex: an unmatched
  name stays null rather than grabbing a wrong element's value.
- Tests: conflict asserts real (non-fabricated) scheduler claim homes +
  registration; new case covers a broken scheduler chain alongside WinSW
  failing closed as unknown.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
…ilable

Windows without Developer Mode or elevated privileges cannot create
symlinks (EPERM), so the dangling-plist test always failed there even
though the code under test is fine. Probe symlink capability once and
gate the test with test.skipIf, mirroring claude-agents-inject; the
machine without symlinks now reports a visible skip instead of a
spurious failure.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
The Windows chain-walk suites pass `platform: "win32"` as a parameter, but
the trusted schtasks resolver keys off process.platform, which is "linux"
on the ubuntu-latest test shards. It threw on every Windows test, so the
probe returned "Task Scheduler could not be asked" and 18 tests failed.

Set the trusted-system-directory resolver seam in beforeEach, pointing at
a per-test fake System32 containing schtasks.exe (mirroring
windows-elevation.test.ts), and reset it in afterEach. Reproduced the CI
failure locally under a linux-platform simulation: 18 failed without the
seam, 49 pass / 0 fail with it.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
@Wibias Wibias changed the title fix(probe): walk the Windows service definition chain for ownership fix(probe): walk Windows service definition chains (Task Scheduler + WinSW) for ownership Aug 7, 2026
@Wibias
Wibias merged commit 1fc24f0 into lidge-jun:dev Aug 7, 2026
21 checks passed
@Wibias
Wibias deleted the fix/windows-ownership-probe branch August 7, 2026 03:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant