Skip to content

fix(fm-watch): read certsync health from host files - #78

Merged
Freudator86 merged 3 commits into
mainfrom
fm/fm-certsync-health-file-read
Aug 6, 2026
Merged

fix(fm-watch): read certsync health from host files#78
Freudator86 merged 3 commits into
mainfrom
fm/fm-certsync-health-file-read

Conversation

@Freudator86

Copy link
Copy Markdown
Owner

Intent

Land the certsync health check's host-file read (bin/fm-watch.sh) that was already built on branch fm/certsync-health-file-read but could not land because that worker was already driving a no-mistakes run for a different repo. This is the clean second attempt, based on that existing branch. The check now reads certsync's heartbeat JSON and sqlite state DB directly off a host bind-mount path (confirmed matching the container-side contract landed in hlr-certsync PR 14, which opened that path read-only and exposed it to an unprivileged monitoring account) instead of shelling into the container via docker compose exec, so it needs no docker access or docker-group membership at all. The state DB is opened read-only (mode=ro) so a live WAL database is never disturbed. A heartbeat-freshness gate (FM_CERTSYNC_HEARTBEAT_MAX_AGE, default 7200s) was deliberately added on top of the inherited branch: reading frozen host files cannot tell a live healthy daemon from a stopped container the way the old docker-exec check could (exec itself used to fail when the container was down), so a healthy:true reading whose heartbeat has gone stale is now reported as unhealthy rather than silently absorbed - this preserves the existing 'cannot confirm well never collapses into is well' contract. docs/configuration.md was updated in the inherited branch to match. On top of the inherited branch I fixed a real, pre-existing shellcheck failure (SC2046 unquoted word-splitting on 7 unquoted env $(certsync_health_env ...) call sites, plus one SC2015 'A && B || C is not if-then-else' finding) in tests/fm-watch-triage.test.sh - rewrote the env expansion to use a mapfile-populated array and the chained boolean check as an explicit if/fi. I did not touch the actual health-check logic in bin/fm-watch.sh or the docs beyond what the inherited branch already did. PR 75 (which raised this same branch manually, bypassing the no-mistakes channel) should be closed as superseded by this PR once it opens, with a one-line comment pointing at the new PR.

What Changed

  • Certsync watcher health now reads heartbeat and SQLite state from host-mounted files, including stale-heartbeat handling via FM_CERTSYNC_HEARTBEAT_MAX_AGE, instead of using Docker exec access.
  • Updated certsync monitoring docs to describe the host-path contract, heartbeat freshness setting, and read-only database access expectations.
  • Expanded watcher triage tests for host-file health behavior and adjusted the test env setup to satisfy shellcheck.

Risk Assessment

🚨 High: Captain, the change is small, but it touches a live health monitor and appears to miss the critical read-only DB guarantee for the new host-file access path.

Testing

Captain, I exercised the changed certsync heartbeat path via the full watcher triage script, then manually verified the end-user CLI behavior: fresh healthy host files are absorbed with no wake, while a stale healthy host-file read produces and drains a durable check certsync-health wake without invoking docker; the working tree remained clean.

Evidence: Full watcher triage test transcript
ok - signal_reason_is_actionable: benign absorbed, captain verbs and coalesced batches surfaced
ok - stale_is_terminal: terminal status surfaces, non-terminal and no-status are benign
ok - scan_captain_relevant_statuses lists only captain-relevant statuses
ok - classifier primitives: keyed decisions and activity phases, captain relevance, window-to-task, and overrides
ok - crew_is_provably_working: only working+run-step/pane is provable; idle/finished/parked/failed/unknown surface
ok - status_is_paused: only the leading paused verb matches, and paused is not captain-relevant
ok - crew_absorb_class: working/paused/none from one read; crew_is_paused and crew_is_provably_working agree
ok - signal_crew_provably_working: benign only when every referenced crew is provably working
ok - a no-verb signal whose crew is provably working is absorbed (no exit, no queue, suppressor advanced, beacon present)
ok - a bare turn-end whose crew is provably working (busy pane) is absorbed
ok - a bare turn-end whose crew is not provably working is surfaced (the swallowed-finish fix)
ok - a no-verb working: note whose crew is idle with no running pipeline is surfaced
ok - captain-relevant signal is surfaced (queue + exit) and marked surfaced
ok - a two-file single crewmate turn enqueues exactly one wake while both markers advance
ok - away mode collapses a two-file crewmate turn to one wake while both markers advance
ok - two crewmates signalling in one poll still produce one wake each (dedup never crosses tasks)
ok - a turn-end marker moving alone still keys its own wake
ok - a stale pane sitting on a terminal status is surfaced (queue + exit)
ok - a relayed terminal task absorbs parked pane churn and re-surfaces on the bounded cadence
ok - a real status write wakes immediately and clears parked tracking
ok - a metadata change clears parked tracking before stale classification
ok - mark-parked wrapper: creates the marker for a recorded window, refuses an unrecognized one, never engages the watcher lock
ok - mark-parked wrapper: refuses a kind=secondmate window, leaving the pause-tracking path untouched
ok - a stale terminal-looking status is absorbed while its run works, holds the ladder, and escalates once the run stops
ok - provably-working non-terminal stale is absorbed, holds the ladder while its run is active, and escalates once the run stops
ok - a healthy static-pane codex worker with an alive process is absorbed (agent-liveness backstop), and the wedge timer still arms
ok - a codex pane whose process is dead surfaces immediately (the backstop never masks a crash)
ok - the agent-liveness absorb is scoped to codex: a claude worker with an alive process still surfaces immediately
ok - a follow-on stale repeating an already-surfaced terminal line is absorbed, and a real wedge behind it still escalates
ok - a stale whose captain-relevant line has not been surfaced yet still wakes firstmate at once
ok - consecutive wedge escalations on the same pane accumulate and demand deep inspection at the threshold
ok - the ladder holds for a provably-working crew under a stripped environment, and an unreadable read is reported instead
ok - a ladder hold that lasts a full recheck window surfaces one bounded recheck without climbing the ladder
ok - a pane becoming active again resets the consecutive wedge-escalation counter
ok - a not-provably-working non-terminal stale is surfaced immediately (never left to wait out the timer)
ok - a declared pause is absorbed on first sight, then re-surfaced as a recheck past the threshold, never wedge-escalated
ok - exited declared-pause and captain-held panes use bounded pause cadence while a live decision gate still surfaces once
ok - a declared paused secondmate re-surfaces on the bounded normal-mode cadence
ok - a non-paused secondmate retains normal stale suppression
ok - a resumed secondmate clears pause and stale tracking before stale exemption
ok - unchanged stale hashes reclassify when a crew enters or leaves pause
ok - a declared pause is periodically rechecked against authoritative active-run state
ok - a paused status overridden by authoritative working keeps its wedge timer and holds the ladder while the run is active
ok - a ladder hold on the paused-then-working path surfaces one bounded recheck without climbing the ladder
ok - matching non-terminal stale suppressors repair missing or corrupt stale-since timers
ok - triage log capping handles wc byte counts with leading spaces
ok - a heartbeat with no captain-relevant change is absorbed and backs off the cadence
ok - heartbeat backstop fail-safe surfaces a captain-relevant status the per-wake path missed
ok - heartbeat absorbs healthy certsync status read directly off the host files
ok - a healthy payload with a stale heartbeat surfaces as unhealthy instead of reading as healthy off frozen files
ok - heartbeat surfaces confirmed unhealthy certsync through the check wake path
ok - afk heartbeat surfaces confirmed unhealthy certsync through the check wake path
ok - heartbeat surfaces an unreadable certsync status as its own cannot-run check wake, distinct from healthy
ok - heartbeat surfaces a failing certsync status read as its own cannot-run check wake, not as healthy
ok - heartbeat surfaces a certsync deployment with no readable source as its own cannot-run check wake
ok - the liveness beacon stays fresh while the watcher absorbs benign wakes (fm-guard never false-alarms)
ok - with .afk present the watcher reverts to one-shot so the daemon owns triage (no double-triage)
ok - AFK changed paused panes hand off plain stale identities for daemon-owned pause triage
Evidence: End-to-end certsync host-file behavior transcript

Fresh healthy host files: watcher stayed quiet, stdout bytes 0, no wake queue. Stale healthy host files: watcher printed check: certsync health: unhealthy: heartbeat stale ...; drain showed a durable check certsync-health wake. A failing fake docker was first on PATH for both scenarios.

### Scenario 1: fresh healthy host files are absorbed without docker
watcher exit code: 124 (124 means timeout kept running quietly)
watcher stdout bytes: 0
wake queue present: no
docker fake was first on PATH: /tmp/no-mistakes-evidence/01KZ8YMWBADRJE8A3Z0KCHH9W2/certsync-host-file-e2e-1557717/bin/docker

### Scenario 2: stale healthy host files surface a certsync-health check wake
watcher stdout:
check: certsync health: unhealthy: heartbeat stale (75933767s > 7200s); daemon may be stopped or syncs failing
drained durable wake:
1785933767	1	check	certsync-health	check: certsync health: unhealthy: heartbeat stale (75933767s > 7200s); daemon may be stopped or syncs failing

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 error
  • 🚨 bin/fm-watch.sh:1080 - The new host-file path instantiates StateStore with the raw DB path, so the watcher no longer enforces the intended read-only SQLite access at this call boundary. For a live WAL DB on a read-only bind mount, a default read/write open can fail or create lock/journal side effects; pass a read-only URI/flag through the certsync API instead of relying on StateStore defaults.
✅ **Test** - passed

✅ No issues found.

  • bin/fm-session-start.sh startup baseline and repository context read
  • git diff 7778ca3246868064e4e33531174a5032f705fb76..da88c88d298bcd47dc5a6bb24948e0b945d316f1 -- bin/fm-watch.sh tests/fm-watch-triage.test.sh to align tests with intent
  • tests/fm-watch-triage.test.sh | tee /tmp/no-mistakes-evidence/01KZ8YMWBADRJE8A3Z0KCHH9W2/fm-watch-triage-certsync-transcript.txt
  • Manual evidence check: created a fake hlr_certsync source tree and a fake failing docker first on PATH, then ran bin/fm-watch.sh against fresh and stale host heartbeat files and drained the durable wake queue into /tmp/no-mistakes-evidence/01KZ8YMWBADRJE8A3Z0KCHH9W2/certsync-host-file-e2e-transcript.txt
  • git status --short to confirm no working-tree artifacts were left behind
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Crew Agent and others added 3 commits August 5, 2026 12:29
The certsync health check ran `docker compose exec -T certsync certsync
status`. On this fleet's own accounts - deliberately not in the docker group -
that exec is denied at unix:///var/run/docker.sock, so since the honesty fix
(PR #73) the check honestly but permanently reports "cannot run: status command
failed", giving no read at all on whether certsync is well.

Read certsync's status directly off the host instead. certsync now exposes its
heartbeat JSON and sqlite state DB under a readable host bind mount (see the
certsync repo's docs/deploy.md, "State host path"); `certsync status` computes
healthy/reason purely from those two files plus the daemon-state argument, so
certsync_health_reason runs certsync's own build_status via python3 against them
(PYTHONPATH=$FM_CERTSYNC_SRC, default $FM_CERTSYNC_PROJECT/src). No docker
socket, no exec, no docker-group membership, no socket proxy - the read needs no
docker access at all, and reproduces the exact healthy/reason JSON the exec
produced.

Reading frozen files loses the one liveness signal exec gave for free: exec
failed when the container was down. Reinstate it as a heartbeat-freshness bound
(FM_CERTSYNC_HEARTBEAT_MAX_AGE, default 7200s = 2x the 3600s max sync interval,
0 disables): a healthy:true reading whose heartbeat has gone stale reads as
unhealthy, never quiet, so a stopped container or a run of failing syncs can
never read as healthy off stale files. This also closes a pre-existing blind
spot where failing syncs read healthy under the exec path.

Preflight now checks python3/jq and the certsync source tree; each failure keeps
its own distinct "cannot run: ..." reason. Tests rewritten to the file-read
model with added coverage for the freshness gate and the source-unavailable
preflight. docs/configuration.md updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Word-split env $(...) (SC2046) into an array via mapfile, and split an
&& ... || fail chain (SC2015) into an explicit if, so the PR carrying
the host-file certsync health read passes fm-lint.sh.
@Freudator86
Freudator86 merged commit 17c8fbf into main Aug 6, 2026
10 checks passed
@Freudator86
Freudator86 deleted the fm/fm-certsync-health-file-read branch August 6, 2026 03:16
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