Windows worker first-run onboarding: packaged setup dialog + working scarcity-worker.exe pair (issue #113) #114
No reviewers
Labels
No labels
accessibility
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
P1
P2
P3
question
wontfix
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Creatidy/scarcity-router!114
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/windows-first-run-onboarding"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #113 (stays open for human review; do not merge before live Windows acceptance).
Problem
The v0.1.0 Windows ZIP (
scarcity-worker.exe, candidate run35780744775) dead-ended on first launch: it instructed users to runscarcity-router-worker pair …— a Python console script the standalone package does not contain.docs/m10-acceptance.mdalready claimed the Windows package asks for pairing information "in its first-run dialog"; this PR makes that statement true.What this changes
scarcity_router/worker_setup.py(new, GUI-independent core): typed/versioned non-secret local settings (WorkerLocalSettings: optional control-UI origin + optional loopback Ollama resource/host/port) persisted as one strictly-parsed JSON value in the existingWorkerLocalStore(schema v1, exact key set, bounded, atomic, fail-closed); pairing orchestration (pair_worker→ the EXISTINGWorkerRuntime.pair); deterministic error mapping (safe, actionable, code-never-echoed);complete_setupwith defined partial-failure semantics (failed pairing persists nothing; settings-save failure after pairing stays recoverable); documented precedence (effective_arguments: explicit CLI--allow-*flags replace the stored selection, otherwise stored settings drive); launch routing (resolve_launch).windows_tray.py(packaged entry router):pair …→ packaged CLI pairing (same path as the dialog; best-effort parent-console attach via CreateFileW + fd so output is visible from PowerShell/cmd despite the windowed build); no-arg launch → first-run dialog when unpaired, tray when paired; explicit run flags keep pre-#113 CLI semantics exactly. The tray runtime factory rebuilds the adapter registry from stored settings on every (re)connect through the EXISTINGbuild_local_adapter_registry; a new "Worker settings…" tray action reopens the SAME dialog and a save triggers the controlled restart machinery. Remediation text namesscarcity-worker.exe, never the console script.scarcity_worker_setup_view.py(new, compact tkinter dialog — first-run and settings modes in ONE window; lazy imports; no wizard, no second application); tray view gains the settings item; launcher wires both seams;worker.specpins thetkinterhidden import (PyInstaller's hook bundles the tcl/tk runtime — verified present in the produced ZIP).LoopbackOllamaAdapter); server can never widen the allowlist; pairing code never persisted/logged/echoed; verified TLS unchanged; settings dialog holds no admin credentials and mutates no server configuration; Windows-native Codex NOT advertised (the dialog source contains no codex token — asserted by test; the Linux/WSL CLI path stays fail-closed). The library writes no files — the console attach uses a Win32 handle, keeping the M08 write-surface guardrail green.scarcity-worker.exe), M10 acceptance record (worker sequence + new platform-table row), decision D-051 indocs/decisions.md.Tests
New
tests/test_worker_first_run.py(44 tests, all failing on pre-#113 code where applicable) drives the production seams — the packaged entry router, the real pairing protocol over in-memory transports, the real store — with fake views (no GUI pixels): entry routing, cancel/partial-failure semantics, code non-persistence (raw DB bytes), safe error mapping, strict settings parsing/secret smuggling, restart persistence, registry reuse, pair-only validity, packagedpaircommand, non-loopback refusal, no-Codex advertising. Updatedtests/test_windows_packaging.py(spec shape incl. tkinter; launcher delegation; setup-view adapter lazy-import discipline).Local gates:
make check(1930 tests) green,uv run basedpyright0/0/0,make package-checkgreen,make guardrailsgreen,git diff --checkclean.Windows candidate build (exact PR HEAD
91982ae)GitHub manual release-candidate run
35791518999(workflow_dispatchonfix/windows-first-run-onboarding@91982aecdb4120ac96a9a014f29cd67ea3001209):verify-release-contractsuccess,build-and-verify-artifactssuccess,build-windows-worker-packagesuccess,finalize-release-bundlesuccess;publish-github-releaseskipped,publish-pypiskipped. New ZIP downloaded and inspected:scarcity-worker-0.1.0-windows-x64.zip, 20,775,270 bytes, SHA2565b977a39f1782b7830824ce8d138722c276ebeab3e393ae6ea77b4a89ee431c5, containsscarcity-worker.exe+_internal/incl._tkinter.pyd,tcl86t.dll,tk86t.dlland the tcl/tk data tree (927 entries).Review
A fresh independent GLM-5.3-Flash (thinking MAX) review runs against this PR per the bounded review lifecycle; findings will be remediated (≤ 2 rounds) here. Live Windows acceptance remains with the owner (checklist in the task report).
Independent review verdict: APPROVE
Fresh independent GLM-5.3-Flash (thinking MAX) review session over the full diff
a7ea86d..91982ae, the governing docs (AGENTS.md, docs/security.md, docs/decisions.md D-042/D-044/D-049/D-050/D-051) and targeted test re-runs (61 tests green). No MERGE_BLOCKER finding stands; per the bounded review lifecycle no remediation round is triggered and the reviewed head stays91982ae(the exact head of candidate run35791518999).All 15 required review questions answered YES/SUFFICIENT with file:line evidence, notably: complete ZIP-only first-run path; one pairing implementation across dialog + packaged CLI + console script; no-arg launch routing; allowlist stays worker-local (non-loopback Ollama refused before pairing, server cannot expand it); secrets out of the settings document (strict schema, raw-DB-byte assertion); pairing code never persisted/logged/echoed; TLS verification unchanged (grep-verified no bypass); deterministic persist/reload + documented CLI-vs-settings precedence; fail-closed malformed-config recovery; one compact dialog, not an application; Windows Codex honestly absent (dialog source contains no codex token, adapter gate fail-closed); docs match the artifact; windows-latest evidence correctly tied to this HEAD; nothing added as release theatre.
Recorded DEFER items (non-blocking follow-ups, not fixed in this PR to keep the reviewed head and the candidate-build evidence identical):
_packaged_paircloses the store only on the success path (scarcity_router/windows_tray.py:677-700) — try/finally would match the zero-leak discipline; harmless today (process exits immediately after the failure).srw://origins (packaging/windows/scarcity_worker_setup_view.py:381) — cosmetic; transport selection is correct.scarcity_router/windows_tray.py:357-358) — full remediation text is visible in the Worker settings dialog, the documented recovery path.packaging/windows/scarcity_worker_setup_view.py:433-452) — not a dead end; the origin is displayed in the dialog header.pair --code CODEpasses the transient one-time code as an argv — mirrors the frozen pre-existing console-script contract; future hardening (prompt fallback) needs its own interface decision.is_setup_available()is exported without a production caller — test seam only.Remaining gates are all external/owner-side:
EXTERNAL_ACCEPTANCE_GATE: LIVE_WINDOWS_ACCEPTANCE(double-click flow on Windows 11).Lifecycle remediation: deterministic
WorkerLocalStoreownership (commit3aea980)Defect.
_packaged_pair()returned on every typed pairing failure (invalid/expired/used code, TLS failure, transportOSError, origin validation) without closing theWorkerLocalStoreit opened — the success path was the only close. A failed first-run pairing is a normal user path, not an exotic edge. The same inspection proved the identical ownership defect in the directly analogous paths, fixed in the same commit:tray_main()tray-runtime store: leaked onWorkerOrigin.parse/control_ui_origin/TrayNotAvailableError/build_runtimefailures and was never closed on the success path;worker_client.main():pairfailures,runconfiguration errors (WorkerOrigin.parse,build_registry), a raisingruntime.run()(past the diagnosticsfinally) all leaked past the outerexcept.Remediation. Explicit ownership at all three sites:
open → try → finally: store.close()— exactly once on success, normal failure and exceptional failure; never via GC, interpreter shutdown or process exit; never before the runtime that uses the store finishes (run_tray_workerjoins the session thread before returning;runtime.run()completes before the close, including when it raises). Pairing semantics, user-facing messages, exit codes (0/1/2/3), code redaction and TLS verification are unchanged.Tests. New
CloseCountingStorefixture (recordsclose()calls) drives 21 new ownership tests: packagedpairsuccess / invalid / expired / used / TLS / transport / config-after-open / unexpected-exception / code-never-persisted; tray paired launch, config failure and runtime failure (each also asserts the store is still open when the runtime starts — no early close);worker_client.mainpair success, origin failure, transport failure, run not-paired, run config error, run fatal, budget-exhausted, run exception. 16 of the 21 tests fail against the pre-remediation code (independently re-verified by the reviewer against an extracted HEAD tree). Focused suites pass withPYTHONWARNINGS=error::ResourceWarning— zero project-owned resource warnings, no suppression added.Gates.
make check(1951 tests OK; basedpyright 0 errors / 0 warnings / 0 notes),make package-checkPASS,make guardrailsPASS,git diff --checkclean. Forgejo CI run #1910 SUCCESS on3aea980.Review. One fresh independent review on GLM-5.3-Flash (thinking MAX): APPROVE, 0
MERGE_BLOCKER, 2DEFER(a pre-existing 10 s join cap inrun_tray_worker— residual, out of scope for this remediation; and a wording imprecision in the new fixture docstring, corrected in3aea980).New Windows candidate — run
35791518999is superseded. GitHub manual run 35881326806 (workflow_dispatchonfix/windows-first-run-onboarding@3aea980efb8c90db0fa720623f157d052b0288d7, mirror SHA verified equal to this PR head):verify-release-contractsuccess,build-and-verify-artifactssuccess,build-windows-worker-packagesuccess,finalize-release-bundlesuccess;publish-github-releaseskipped,publish-pypiskipped. New ZIP downloaded and inspected:scarcity-worker-0.1.0-windows-x64.zip, 20,775,846 bytes, SHA256bedb490ff1f6b916c2c40ae1334afa19d3b4f9dcda608f7f12f18ccdba320897, containsscarcity-worker.exe+_internal/(1043 entries incl._tkinter.pyd,tcl86t.dll,tk86t.dll, tcl/tk tree). Nothing was published.The old candidate run
35791518999(built from91982ae) is obsolete and MUST NOT be used for final live acceptance. EXTERNAL_ACCEPTANCE_GATE (LIVE_WINDOWS_ACCEPTANCE: unzip → double-click → pair → tray → relaunch → settings) remains with the owner; the PR stays open and unmerged.Windows GUI ownership rework: D-052 (commit
bc2cb15)Live defect. On the Windows 11 acceptance host, the "Worker settings..." dialog froze, failed focus/foreground transitions, and occupied the tray callback. Root cause (architecture, not the Ollama fields):
_open_settings_dialogran the dialog's whole tkinter mainloop synchronously inside the pystray menu callback. pystray invokes menu handlers on its Win32 message-loop thread, so the tray loop was blocked for the dialog's lifetime — no menu action could run, focus behavior broke, and Quit could not deterministically tear the process down.New model — D-052 (
docs/decisions.md): one deliberate owner per long-lived event loop.windows_tray.UiDispatcher,scarcity-router-ui), lazy-started, strictly serial; the first-run dialog runs there too while the coordination thread waits on the result future (a plain sequential wait, no nesting).WorkerLocalStore(locked, atomic single-row writes).dispatcher.close_request+ the view'sarm_close_requestcapability (packaged tkinter view polls it viaafter); Quit unwinds an open dialog, drains and joins the UI thread bounded beforetray_mainreturns. No orphan GUI threads, no daemon-kill reliance.No new framework, process or IPC layer; the library stays GUI-toolkit-free; onboarding semantics, pairing, messages, exit codes, redaction and TLS verification unchanged.
Tests. New
TrayUiOwnershipTests(6): the settings action returns immediately while the dialog blocks on the UI-owner thread (different thread asserted); 5 rapid requests open exactly 1 window and a post-close request opens the next; Quit with the dialog open unwinds it via the close signal (armed, no restart, exit 0, noscarcity-router-ui/scarcity-router-workerthread left); Reconnect under an open dialog rebuilds the runtime deterministically; Settings→Cancel→Settings→Save→restart; "Open server control UI" never blocks its caller. Focused suites green, zero ResourceWarnings withPYTHONWARNINGS=error::ResourceWarning.Gates.
make check(1957 tests OK; basedpyright 0/0/0),make package-checkPASS,make guardrailsPASS,git diff --checkclean. Forgejo CI run #1913 SUCCESS onbc2cb15.Review. Fresh independent GLM-5.3-Flash (thinking MAX) review with the explicit instruction that an unchanged unsafe event-loop ownership is CHANGES_REQUESTED: APPROVE, 0
MERGE_BLOCKER, 5DEFER. Two hardening defers were applied in the reviewed commit (slot acquire/release ordering on the view-less path; enqueue moved under the dispatcher lock so no task can land behind the shutdown pill). Remaining defers are documented semantics (Quit during an in-flight save discards the dialog; the atomic row applies at next restart — added to the live retest checklist), a cosmetic message tense change, and the live-Windows gate itself.New Windows candidate — runs
35791518999(old) and35881326806(store-ownership build) are both superseded. GitHub manual run 35996303779 (workflow_dispatch@bc2cb156397aca658395f4bdf0bfd6ab28b7ed0a, mirror SHA verified equal to this PR head):verify-release-contract,build-and-verify-artifacts,build-windows-worker-package,finalize-release-bundleall success;publish-github-releaseandpublish-pypiskipped. ZIP downloaded and inspected:scarcity-worker-0.1.0-windows-x64.zip, SHA2561fc592f6c5a96bc8c9e74a0c220330272892e7d319d7bb93c7e82ce1916c549c(matches bundleSHA256SUMS), 1044 entries incl.scarcity-worker.exe,_internal/_tkinter.pyd,tcl86t.dll,tk86t.dll. Nothing published.The live retest must exercise the tray as a whole (D-052 checklist in
docs/m10-acceptance.md), not only Ollama field enablement: settings open → tray responsive (status / control UI / diagnostics / reconnect all work), second settings request opens no second window, Settings→Cancel→Settings, Settings→Save→restart→Settings, Settings open→Quit, Settings open→Reconnect, rapid repeated Settings, no freeze/focus loss, clean process exit. PR stays open and unmerged for EXTERNAL_ACCEPTANCE_GATE: LIVE_WINDOWS_ACCEPTANCE.