Every Windows guard shared ONE daemon. test_daemon_stability.py said each
section "runs under its OWN cache directory: daemon coordination is
cache-scoped" -- it is not. The rendezvous key is FNV-1a over a compile-time
domain string (service.c: cbm_daemon_rendezvous_key) under a machine-global
parent (win_default_runtime_parent -> SHGetFolderPathW(CSIDL_LOCAL_APPDATA),
which ignores the LOCALAPPDATA variable). So the guard's own `daemon stop`
and kill -9 sections, the sibling guards, and anything else on the runner
all met the same daemon, and a daemon wedged anywhere failed every check
after it. That is the shape of this week's test-windows-guards red on
#1874: lifecycle's `daemon start` connected, the UI-config handshake never
answered, and stability / hook_augment / ui_drive / cli_non_ascii went red
in a row with an orphan daemon killed at cleanup -- on a diff and a base that
were each green elsewhere.
CBM_RUNTIME_DIR is the only relocation hook (bootstrap.c, resolved in
cbm_daemon_bootstrap_endpoint_new for every product endpoint). Each stability
section now gets a private runtime parent next to its cache, created before
the binary runs -- the endpoint validates the parent and never creates it --
and the daemon inherits it from the client that spawns it. Proven on macOS:
a daemon on the default rendezvous keeps its pid across the whole guard
(three stops and a kill -9 inside it).
The reporting was hiding the cause as well:
* every SETUP FAIL printed nothing at all, and every RED printed a 300-400
character HEAD slice, which is the allocator preamble, never the error.
Excerpts now show the tail (1500 chars); setup paths print the daemon's
full output.
* a subprocess timeout raised on a parallel-client worker thread was lost
and the client reported as "rc=none" with no output; it is now kept as a
failed result with its partial output. A section-level timeout is a
named RED with the command and output, not a bare traceback.
* the MCP session's stderr went to DEVNULL; it now goes to a file that is
printed when initialize never answers -- and that case is a RED against a
running daemon, not a "setup" skip.
* exit 2 was documented and never produced. Only `daemon start` failing
raises it now (test_daemon_lifecycle.py owns that surface as a
regression, so the board loses nothing); main() runs every section and
names all of the failing ones.
* test_hook_augment.py / test_cli_non_ascii_arg.py print full stdout AND
stderr on their index-setup failures instead of 300 chars of stdout.
On POSIX (local iteration only) the namespace lives under /tmp: sun_path
caps the socket at 104 bytes on macOS and TMPDIR there is ~50 bytes deep.
Out of scope, noted: test_daemon_lifecycle.py, test_non_ascii_path.py and
test_ui_drive_listing.py still use the machine-global rendezvous; a too-long
CBM_RUNTIME_DIR on POSIX is refused (ipc.c sun_path check) with no
validation detail.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Fixes surfaced while bringing the shared-coordination-daemon branch
green across macOS, the Linux containers, and the real Windows-ARM64 VM,
plus a new stability guard that caught one of them.
- daemon(win): the CLI teardown transition latched a failed release as
permanent, but a Windows participant-state release must briefly
try-hold the shared startup/legacy gates and legitimately collides
with a concurrent one-shot's teardown; the release is retriable by
contract (it always retains the transition), so parallel one-shot
commands no longer report "CLI coordination cleanup failed" despite
succeeding. Found by the new stability guard's churn section.
- cli/main(win): install/update/uninstall and the Augment hook-script
removal derive the managed launcher's identity from its plain
drive-form path (cli_windows_plain_utf8), not the \\?\ extended-length
form, so agent-config ownership matches on Windows and uninstall no
longer leaves the MCP entry or owned hook scripts behind.
- security: the `daemon start --open` browser launch is now shell-free
(ShellExecuteW on Windows, cbm_exec_no_shell elsewhere) instead of
system(); the CORS origin check spells out its two literal loopback
URLs so the static URL audit sees a complete value. Both clear the
Layer-1 allow-list audit.
- coordination: version-cohort lock retries now sleep a per-process
jittered interval — fixed-period retries can phase-lock two
participants so one starves. The activation-quiesce test's
observation window now covers worst-case candidate staging (three
tamper-defense hashes over a ~1 GB sanitizer binary exceed the old
30 s budget on container I/O).
- smoke/soak: the agent-config smoke matches Windows config paths
through their escaped-backslash quoted form; the soak parser skips the
update-available banner before the JSON summary and gained a one-shot
CLI admission-churn phase inside the RSS/FD leak window. The build-dir
safety fixture creates a real symlink/junction (MSYS2 ln -s otherwise
deep-copies) so its traversal-refusal contract is genuinely exercised,
and the symlinked-agent-roots test demotes its planted links to an
unprivileged owner when run as root so the refusal is really tested.
- tests(win): a new daemon-stability guard exercises the parameter
surface, hook fail-open with its rate-limited notice, start-twice and
occupied-port handling, busy-stop refusal against a live MCP session,
kill -9 crash recovery, and sequential/parallel one-shot churn.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>