Skip to content

Windows: fix daemon liveness, crypto key corruption, and auth-token resilience#44

Open
danielhertz1999-bit wants to merge 6 commits into
CodeAbra:mainfrom
danielhertz1999-bit:windows-fixes-liveness-crypto
Open

Windows: fix daemon liveness, crypto key corruption, and auth-token resilience#44
danielhertz1999-bit wants to merge 6 commits into
CodeAbra:mainfrom
danielhertz1999-bit:windows-fixes-liveness-crypto

Conversation

@danielhertz1999-bit

@danielhertz1999-bit danielhertz1999-bit commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

A set of Windows daemon fixes discovered while running the memory engine on
Windows 11. Each is a POSIX-idiom-on-Windows bug that misbehaves silently.
Branch is main + these commits only.

1. os.kill(pid, 0) liveness probes fail on Windows (WinError 87)

os.kill(pid, 0) is the POSIX "is this PID alive?" idiom, but on Windows
os.kill rejects signal 0 with OSError [WinError 87] even for a live
process
. Three call sites treated that as "process dead", making a healthy,
ticking daemon report as down:

  • doctor check (a) "daemon process alive" → live daemon reported dead
  • doctor check (m) heartbeat scanner → aborted with WinError 87
  • maintenance pre-flight guard → always concluded "no daemon running" on
    Windows, which could let maintenance run against a store the live daemon holds

Each now uses psutil.pid_exists (cross-platform; psutil is already a hard
dep), falling back to os.kill only on POSIX — matching the existing pattern
in capture.py and lifecycle_lock._is_pid_alive.

2. Auth-token file is a single point of failure with no self-heal

On Windows the daemon authenticates clients with a token it writes to
~/.iai-mcp/.daemon.token at startup and holds in memory. If that file later
vanishes out-of-band (external cleanup, AV, a stale-file sweep) while the daemon
runs, the TCP port stays up but every new client fails the auth handshake
old connections survive, so the daemon looks alive while new sessions and
daemon status all report "not running". Observed in production.

Adds _ipc.reassert_token_if_missing() (idempotent; no-op on POSIX / in client
processes / when the file exists), called as the first daemon tick step, so a
vanished token self-heals within one tick instead of needing a manual restart.

doctor check (b) also now reports this precisely — the real endpoint
(127.0.0.1:<port>) and "auth token file missing (restart to regenerate)"
instead of the old misleading ".daemon.sock present but unreachable".

3. Crypto AES key corruption on Windows (missing O_BINARY)

crypto._try_file_set used os.open without os.O_BINARY; on Windows that
defaults to text mode and translated 0x0A bytes in the raw AES key to
0x0D0A, silently corrupting it. Adds os.O_BINARY (no-op on POSIX).

Tests

Test-portability changes so the suite runs green on Windows, plus a determinism
fix for a pre-existing centrality-bench flake (unseeded uuid4() let near-tied
top-K scores flip a str(id) tie-break between runs). New unit tests cover the
token self-heal. No behavior change on POSIX.

🤖 Generated with Claude Code

danielhertz1999-bit and others added 5 commits July 3, 2026 12:39
Rebased onto upstream/main (which carries its own Windows port); this
commit re-applies only the deltas upstream still lacks.

Product fix:
- crypto._try_file_set: os.open defaults to text mode on Windows, which
  translated 0x0A bytes in the raw AES key to 0x0D0A and silently corrupted
  it. Add os.O_BINARY (no-op on POSIX). Still unfixed upstream.
- bench/contradiction_longitudinal: write CSV/JSON/MD as UTF-8 (cp1252 default
  crashed on the delta glyph).

Test portability:
- conftest: make Path.home() honor $HOME on Windows and mirror HOME->USERPROFILE
  so the home-redirecting fixtures (and their subprocesses) stay hermetic.
- UTF-8 reads in source-scanning guards (constitutional_guards, bench_worktree,
  contradiction route columns).
- Skip POSIX-only checks on Windows (archive 0o700, crypto geteuid/uid,
  launchd getuid); the product already guards these behind hasattr(os,geteuid).
- cpu_features: match /proc/cpuinfo via as_posix() (Windows renders backslashes).
- capture_hooks_install: assert .ps1/powershell hooks on Windows, .sh/bash on POSIX.
- daemon_kill_mid_consolidation: SIGKILL->SIGTERM (TerminateProcess) fallback plus
  a bounded reopen-retry for Windows async byte-range lock release.
- bridge_socket_first: skip on Windows (npm/mcp-wrapper build path is POSIX-only),
  matching test_bridge_no_spawn_path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
os.kill(pid, 0) is the POSIX "is this PID alive?" idiom, but on Windows
os.kill rejects signal 0 with OSError [WinError 87] (invalid parameter)
even for a live process. Three call sites treated that exception as
"process is dead", which made a healthy, actively-ticking daemon report
as down:

  - doctor check (a) "daemon process alive" -> live daemon reported dead
  - doctor check (m) heartbeat scanner       -> scan aborted with WinError 87
  - maintenance pre-flight daemon guard       -> always concluded "no daemon
    running" on Windows, which could let maintenance run against a store the
    live daemon still holds

Each now uses psutil.pid_exists (correct and cross-platform; psutil is
already a hard dep), falling back to os.kill only on POSIX -- matching the
existing pattern in capture.py and lifecycle_lock._is_pid_alive. Verified:
doctor 4/25 -> 2/25 FAIL, checks (a) and (m) now pass and correctly
identify the live daemon PID. 149 relevant tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
uuid4() was unseeded, so near-tied scores at the top-K fidelity cutoff
could flip the str(id) tie-break between runs, making
test_centrality_for_runtime_approx_above_cutoff flaky independent of
the approximation algorithm.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
On Windows the daemon authenticates clients with a token it generates at
startup, writes to ~/.iai-mcp/.daemon.token, and holds in memory. If that
file later disappears out-of-band (external cleanup, AV quarantine, a
stale-file sweep) while the daemon keeps running, the TCP port stays up but
EVERY new client fails the auth handshake — existing long-lived connections
survive, so the daemon looks alive while new sessions and `daemon status`
all report "daemon not running". The only recovery was a manual restart.

The daemon still holds the token in memory, so it can restore the file
cheaply. Add _ipc.reassert_token_if_missing() (idempotent; no-op on POSIX,
in client processes, or when the file is present) and call it as the first
step of the daemon tick. _generate_token now stashes the token in a module
global (_CURRENT_TOKEN); _remove_token_file clears it so a deliberate
shutdown/restart can't be resurrected.

Observed in production on Windows 11: .daemon.token gone while PID was
listening and serving old clients; new connect raised "Daemon auth token
not found". Confirmed nothing in the daemon's own code removes the token
without also removing the port file (which was intact), so the deletion was
external — exactly the single-point-of-failure this heals.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
check (b) "socket file fresh" reported a missing-token connectivity failure
as "<...>.daemon.sock present but unreachable: FileNotFoundError" — doubly
misleading on Windows, where there is no .daemon.sock (the daemon listens on
TCP loopback) and the FileNotFoundError actually meant the auth token file
was gone, not the socket.

Report the real endpoint (127.0.0.1:<port>) instead of the nonexistent sock
path, and when the token file is missing while the port is listening, say so
explicitly with the remediation ("restart the daemon to regenerate it").
_socket_connect_probe now surfaces the underlying FileNotFoundError message
so a missing port vs a missing token are distinguishable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@danielhertz1999-bit danielhertz1999-bit changed the title Windows: fix crypto key corruption and os.kill(pid,0) liveness probes Windows: fix daemon liveness, crypto key corruption, and auth-token resilience Jul 4, 2026
The Node MCP wrapper's PythonCoreBridge never sent the Windows TCP
auth token before writing RPC requests, so the daemon's
_make_authenticated_handler (added same-day in 21fb055) silently
closed every connection. Add authenticateConnection() to ipc.ts and
call it at all three of bridge.ts's connection sites.

Also add episodes-recent/curiosity-pending/events-query/schema-list
CLI subcommands with the same daemon-first/direct-store-fallback
shape as the existing bank-recall command, reusing the daemon's own
dispatch functions (moved into core/_query_dispatch.py) so the two
paths can't drift apart.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

1 participant