Skip to content

fix(startup): avoid permission-sensitive service re-registration - #970

Closed
stephen-drew wants to merge 4 commits into
lidge-jun:devfrom
stephen-drew:codex/fix-windows-update-service-repair
Closed

fix(startup): avoid permission-sensitive service re-registration#970
stephen-drew wants to merge 4 commits into
lidge-jun:devfrom
stephen-drew:codex/fix-windows-update-service-repair

Conversation

@stephen-drew

Copy link
Copy Markdown

Summary

  • Refresh an existing background manager through ocx service repair during self-update instead of re-registering it.
  • Make Startup recovery, status, doctor and service diagnostics choose Start for a clean stopped service and Repair for an installed unhealthy service.
  • Add ocx repair as a documented alias of ocx service repair; reserve Install for a missing, disabled or conflicting registration and explain the Windows administrator requirement.
  • Keep the last known-good Startup health through routine cache revalidation and one transient probe failure.
  • Document the lifecycle and update behaviour in the canonical guide and maintained translations.

Root cause

On Windows, an existing Task Scheduler registration can be owned by Administrators while the signed-in user is still allowed to run and stop it. Recovery paths that unconditionally suggested ocx service install invoked schtasks /create /f, so a repairable service failed with Access denied even though schtasks /run and /end were permitted.

User impact

Updates and ordinary recovery no longer overwrite an existing registration. A stopped service can be started, stale generated assets can be repaired without elevation, and a genuine re-registration still follows the explicit administrator-approved install path.

Verification

  • Root suite: 7,949 passed and 6 skipped; the sole default-timeout failure was an unchanged native-model management test on this loaded Windows host. Its isolated rerun with a 15-second ceiling passed 8/8 in 2.74 seconds.
  • The eight earlier environment/stale-assertion failures reran 116/116 after adding the bundled Bun directory to child-process PATH and updating the obsolete repair assertion.
  • Dashboard package: 587 tests passed; lint, TypeScript and the production build passed.
  • Privacy scan passed; changed-surface React Doctor scan found no issues.
  • cd docs-site && bun install --frozen-lockfile && bun run build
  • Windows live canary: ocx stop, then ocx repair; replacement PID served on 127.0.0.1:10100 and Startup read back protected, protection=service, diagnosticStale=false.

Source commits before the current dev rebase: 52ef5469 and 70d2066d.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Documentation was updated.
  • No credential, authentication, global configuration or task-ACL boundary was widened.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e253a3db-2122-4371-aee4-ea07fe07b614

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

@lidge-jun

Copy link
Copy Markdown
Owner

Confirmed as real and left open — not folded into the current review stack (#951#973).

Two reasons. It touches a permission-sensitive path (service registration during self-update) across 30 files spanning CLI, GUI, and five docs locales, which is its own review with its own risk profile. And per AGENTS.md, install/service-lifecycle changes want deliberate maintainer attention rather than a ride on a bug stack.

The direction reads right to me — service repair for an installed-but-unhealthy registration, Install reserved for missing/disabled/conflicting — since re-registering to fix a health problem is what makes the permission prompt fire. Thanks for the writeup; the state table in the description is the part that makes it reviewable.

lidge-jun added a commit that referenced this pull request Aug 4, 2026
…it (#970)

`ocx update` stops the proxy before replacing package files, then brought the
service back with `ocx service install`. The Windows scheduler installer always
reaches `schtasks /create`, which requires elevation the updater does not have,
so an ordinary non-elevated update stopped a working proxy and could not restore
its managed service.

serviceReinstallArgs() now returns ["service", "repair"], which rewrites the
wrapper assets and restarts the EXISTING registration without /create. The
export name is kept for out-of-module callers; serviceInstallArgs() is split out
for the paths that genuinely need to register.

Safety of the substitution: repairService() throws when the service is not
installed, and the update path runs after `ocx stop` — but stop never
deregisters on any platform. macOS unloads the plist, Windows calls /end, Linux
calls systemctl stop; deletion lives only in the uninstall paths. Verified
across all three (evidence: devlog 020).

Two things a straight argv change would have missed:

The Windows GUI worker skipped the refresh entirely (update/job.ts) because its
own comment said /create would UAC-fail. That reason does not survive repair, so
the skip is narrowed to callers still passing install argv — otherwise the
dashboard-triggered update, the most common Windows path, keeps the bug while
the CLI gets fixed.

bin/ocx.mjs infers 'a service manages this proxy' from service-state.json
existing, which can be stale. Repair correctly refuses that case, but its thrown
Error is indistinguishable from any other failure there (plain Error, inherited
stdio, generic exit status), so message-matching was unimplementable and a
blanket install-on-failure would resurrect the elevation prompt. It now reads
startup.serviceInstalled from the `status --json` subprocess it already spawns —
the file is plain Node ESM and cannot import diagnoseService() directly.

Advice strings that fire only for an INSTALLED service now say repair: cli/status,
winsw missing-binary, stale baked paths, stale scheduler assets, the launchd
older-plist and not-loaded hints. First-install and missing-unit guidance stays
install.

Red-green: restoring the unconditional Windows skip fails the new guard. Six
existing tests pinned the install argv and were updated with reasons.

245 pass / 0 fail across the service, update, winsw, doctor, status, startup and
Windows-deploy suites.
@lidge-jun

Copy link
Copy Markdown
Owner

Superseded by #980 (stack 7/7), commit e9c0bb3d1. Closing this one — reopening is one click.

Your diagnosis is correct and it is now fixed. ocx update stopped the proxy and then brought the service back with ocx service install, whose Windows scheduler path always reaches schtasks /create — elevation a normal updater does not have. Switching the post-update refresh to repair is the right fix and it shipped.

The safety question your PR did not answer, now verified. repairService() throws when the service is not installed, and the update path runs after ocx stop. If stopping deregistered anything, the substitution would throw exactly where install used to succeed. It does not, on any platform: macOS unloads the plist, Windows calls schtasks /end, Linux calls systemctl stop, and deletion lives only in the uninstall paths (src/service.ts:2204-2225).

Two things the reconstruction adds.

The Windows GUI worker never reached the command at all. src/update/job.ts:775 sets skipServiceInstall = true unconditionally when OCX_SERVICE=1 on Windows, and its own comment gives the reason: schtasks /create will UAC-fail. Repair never calls /create, so that justification does not survive the change — the skip is now narrowed to callers still passing install argv. Without this, the dashboard-triggered update (the most common Windows path, and the one closest to your report) keeps the bug while the CLI gets fixed. A pure argv swap would have missed it.

The stale-marker hole. bin/ocx.mjs infers "a service manages this proxy" from service-state.json merely existing, which can be stale — present while the registration is gone. Today install recreates the service from that marker; a blind repair substitution throws and the user silently loses their managed service. The fix could not be "catch the not-installed error", because repairService() throws plain Error for every failure mode and the launcher sees only an exit status. It now reads startup.serviceInstalled from the status --json subprocess it already spawns, and installs only when the service is genuinely absent. Installing after any repair failure would have resurrected the elevation prompt this change exists to remove.

Also carried: the advice strings that fire only for an already-installed service now say ocx service repair (cli/status, stale baked paths, stale scheduler assets, the WinSW missing-binary hint, and the launchd older-plist and not-loaded hints). First-install and missing-unit guidance stays install, since repair would be wrong advice there.

Six existing tests pinned the old install argv and were updated with reasons; a new guard proves the non-elevated Windows worker now receives service repair, and restoring the unconditional skip turns it red.

Thank you — the elevation analysis was the hard part and you got it right.

@lidge-jun lidge-jun closed this Aug 4, 2026
chrisae9 pushed a commit to chrisae9/opencodex that referenced this pull request Aug 4, 2026
…ice repair

Two overnight contributor PRs describe real defects the lidge-jun#951-lidge-jun#973 stack does
not touch. This unit plans layer 7 as their reconstruction.

lidge-jun#964 cannot be carried: five ids in its hand-written text-only list are
natively image-capable per NVIDIA's own docs (inkling, minimax-m3, kimi-k2.6,
step-3.7-flash, mistral-medium-3.5-128b). A false positive there is silent —
the model can read the image, but the proxy substitutes another model's text
description. Issue lidge-jun#956's own body carries two of the same errors, so reporter
and author shared the premise. 010 inverts the design: maintain the 15 verified
vision-capable ids and derive text-only as the complement, so an unclassified
new model defaults to sidecar-on rather than to the bug being fixed.

lidge-jun#970's premise is right but its diff is oversized: repairService() and
'ocx service repair' already exist here. 020 records the safety proof that
matters — repair throws when not installed and the update path runs after
'ocx stop', but stop never deregisters on any of the three platforms. It also
closes a hole lidge-jun#970 leaves: bin/ocx.mjs infers service presence from a
possibly-stale marker, where repair would throw and lose the managed service.

030 sequences the bottom-up merge and issue closure, including the lidge-jun#954
security-review gate that can legitimately stop the queue.
chrisae9 pushed a commit to chrisae9/opencodex that referenced this pull request Aug 4, 2026
The A-gate reviewer returned FAIL. Every blocker was reproduced before being
accepted; none was rebutted. 001 records the synthesis.

B1 killed my own design. I proposed maintaining the 15 vision-capable ids and
deriving text-only as their complement, and claimed an unclassified model would
default to sidecar-on. It does not — a complement over a static chat-model list
leaves an unknown id in neither list, so modelInList returns false and lidge-jun#956
survives verbatim:

  deepseek-ai/deepseek-v4-flash       sidecarWouldRun=true
  moonshotai/kimi-k2.6                sidecarWouldRun=false
  brandnew/model-nobody-classified    sidecarWouldRun=false

I had inverted which list is maintained while keeping the closed world — the
same lesson as the three earlier allowlist failures, reproduced while writing
the document that cites them. 010 now changes the predicate instead: default-on
for the nvidia entry with the vision list as its exception set, so a stale
exception list costs one description hop rather than reproducing the bug.

B2: removing a native-vision id from noVisionModels is not sufficient. The
catalog advertises image input only for list members, so those models would be
blocked client-side instead. They need explicit modelInputModalities.

B3: src/update/job.ts:775 skips the service refresh entirely on non-elevated
Windows — the dashboard path. Its stated reason is that schtasks /create needs
UAC, which repair does not call, so the skip must be narrowed or the reporter's
own surface stays broken.

B4: repairService throws plain Errors and bin/ocx.mjs sees only an exit status,
so 'fall back on not-installed' was unimplementable. Re-run diagnoseService()
after a failed repair instead of parsing messages.

B5: retargeting emits 'edited', which ci.yml does not listen for, so a green
check on the same head sha proves nothing about the new merge base.

030 also moves the lidge-jun#964/lidge-jun#970 closure from 'when stack 7 opens' to 'open and
green'. The earlier text borrowed a policy from the six carried PRs, which had
verified replacement commits already on a branch; this replacement does not
exist yet and its first design just failed audit.
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.

2 participants