Skip to content

Claude/daemon startup install yyuf77#51

Merged
SteveWeed79 merged 4 commits into
mainfrom
claude/daemon-startup-install-yyuf77
Jul 19, 2026
Merged

Claude/daemon startup install yyuf77#51
SteveWeed79 merged 4 commits into
mainfrom
claude/daemon-startup-install-yyuf77

Conversation

@SteveWeed79

Copy link
Copy Markdown
Owner

What & why

How was it tested?

Checklist

  • pytest passes
  • ruff check palctl tests passes
  • Commits are signed off (git commit -s) — opening this PR agrees to the
    CLA for its
    contributions (see
    CONTRIBUTING.md)
  • User-visible changes have a line under [Unreleased] in
    CHANGELOG.md

claude added 4 commits July 19, 2026 06:22
… is one switch

The field failure: with both palctl and the GUI running as the same user
(Path A), every GUI call answered "the daemon rejected the token." Root
cause: Windows builds a service's environment from the SYSTEM block —
%APPDATA% is an interactive-shell variable the SCM never sets, even for a
service logging on as that user. The --as-user service therefore fell into
config_dir()'s ~/.config fallback and read a DIFFERENT config folder (and
token) than the GUI. Latent since the wrapper swap: winsw_config_xml
deliberately skipped the APPDATA redirect when a service account was set
("user wins over the redirect") — and a test pinned that wrong behavior.
The redirect is now injected for user-account services too, it survives
the password scrub, and the GUI's 401 message explains the real cause and
fix instead of suggesting a re-registration that wouldn't have helped.

And per the design direction: the wizard's background section is ONE
switch, not a menu. The login-startup option is REMOVED from the wizard —
not disabled, not hidden. Every alternative to "service under your
account" either couldn't watch the server or couldn't run the Discord
bot; offering it was how users ended up in the broken split. The group is
checkable: background on (service as you, password field inline) or off.
A legacy "login" config value maps to service on the next run. PIN-only
accounts keep `palctl-daemon install-startup` as a console escape hatch.

Verified: 474 tests + ruff clean; offscreen Qt construction confirms the
login option is gone, on/off maps to service/none, the password field
follows the group state, and a legacy "login" config restores as service.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GSzfBCYS7ZrnGD7creTJaK
Signed-off-by: Claude <noreply@anthropic.com>
….config

The APPDATA env injection fixed the service definition — but the deeper
defect was the fallback itself: with %APPDATA% absent (every Windows
service; the SCM never sets it), config_dir() fell back to ~/.config, a
Linux convention that on Windows lands in a folder no GUI ever reads. Two
config folders, two tokens, 401 on every GUI call — and the only cure was
re-registering the service or hand-editing its XML.

The fallback is now platform-correct: on Windows, <profile>\AppData\
Roaming — exactly where the GUI lives; ~/.config stays the non-Windows
path. This makes the fix self-healing: even under an OLD service
registration with no redirect, an updated daemon computes the same folder
as the GUI. Updating palctl alone heals an affected install — no wizard
re-run, no re-registration, no hand-edits. Belt (env injection) and
suspenders (correct fallback), because a config split is a 401 the user
cannot diagnose.

Verified: 477 tests (3 new pinning all branches: APPDATA wins; Windows
fallback = AppData\Roaming; Linux fallback = .config), plus an end-to-end
simulation of the field scenario proving GUI and no-APPDATA service now
resolve the identical directory where the old code diverged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GSzfBCYS7ZrnGD7creTJaK
Signed-off-by: Claude <noreply@anthropic.com>
Every failure that shipped this cycle lived in one seam: an interactive
user's GUI talking to a service under that same account. The unit suite
fakes the SCM (even on windows-latest) and install-lifecycle registers
LocalSystem only — so the --as-user path and the GUI<->service token
handshake were never executed by any machine before a user's.

New as-user-lifecycle job, on a real Windows runner:
- config_dir with no APPDATA must resolve to AppData\Roaming (the exact
  Linux-ism fallback that split the config and produced the same-user 401)
- give the runner account a password, register the daemon as a
  user-account service the way the wizard does (non-interactive password)
- THE HANDSHAKE: a client reading the token from the interactive %APPDATA%
  must get 200 from the service daemon's /state — the precise scenario
  that failed in the field as "the daemon rejected the token"
- the account password must not persist in the WinSW XML (scrub proof on
  a real registration, not a faked one)
- the health task is registered with the service and removed with it

From here, "the as-user install works" is a claim CI makes by executing
it, not a claim made in a PR description.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GSzfBCYS7ZrnGD7creTJaK
Signed-off-by: Claude <noreply@anthropic.com>
…gnored

Bug #7, caught by the as-user-lifecycle CI job on its FIRST execution: the
sc qc output in the green run read SERVICE_START_NAME : LocalSystem. palctl
wrote <username> inside <serviceaccount>, but WinSW v2's schema is <domain>
plus <user> — the unknown element is ignored without error and the
registration silently falls back to LocalSystem while --as-user claims
success. Broken since the NSSM->WinSW swap; invisible to every unit test
(they assert the XML palctl writes, not what WinSW reads) and to the
handshake probe (the APPDATA injection made even the LocalSystem daemon
read the right config, so /state answered 200).

winsw_config_xml now emits <domain> + <user> (".\name" and "DOMAIN\name"
split; a bare name gets the local-machine domain "."), and the CI job now
ASSERTS the logon account from sc qc instead of just printing it — the
silent-fallback class is machine-checked from here on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GSzfBCYS7ZrnGD7creTJaK
Signed-off-by: Claude <noreply@anthropic.com>
@SteveWeed79
SteveWeed79 merged commit dfd1558 into main Jul 19, 2026
20 checks passed
@SteveWeed79
SteveWeed79 deleted the claude/daemon-startup-install-yyuf77 branch July 19, 2026 14:45
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.

2 participants