fix(updater): thread job_id, relay child breadcrumbs, drop dead cosign_skipped - #1935
fix(updater): thread job_id, relay child breadcrumbs, drop dead cosign_skipped#1935thinmintdev wants to merge 2 commits into
Conversation
…n_skipped Three observability gaps in the updater's job/log correlation (#1901), found by rc-validate v1.0.0-rc.6: - The three background job runners (_run_apply_job, _run_prepare_job, _run_commit_job) built Updater(channel=channel) without the job_id the constructor already accepts, so every parent-side updater.* journal line logged job_id=None regardless of which job produced it. - The routed hal0-update stage/activate/discard child pins its structured log to stderr so stdout carries only the result envelope; the parent only folded that stderr into the raised error on failure, so no sha256_ok/cosign_verify_ok/prepare_ok breadcrumb ever reached the journal on a successful run. UpdateSeam._invoke now replays each captured stderr line into the parent's own log on success. - cosign_skipped was a job field with no producer (stage_release never emits it, and cosign verification has no bypass), so it always read null. Removed rather than given a fake producer. Verification logic itself is untouched — this is journal/logging only.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f71dcbca1a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "seam_bin": self._seam_bin, | ||
| }, | ||
| ) | ||
| _relay_child_log(stderr, verb=parts[0], job_id=self._job_id) |
There was a problem hiding this comment.
Relay the full successful child log
On hosts with several stale quarantine or staging directories, a successful extraction emits one cleanup log line per directory after the SHA-256 and cosign events. _invoke() truncates stderr to its final 4,000 characters before passing it here, so those cleanup lines can push the verification breadcrumbs out of the relayed data and leave the audit gap this change is intended to close. Keep a full stderr value for successful replay (or stream it), while retaining a bounded tail only for error details.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in b979d82 — _invoke() now only truncates stderr to the last 4000 chars on the error-details path; the success-path relay (_relay_child_log) sees the full, untruncated stderr, so a long cleanup-line tail can no longer push the sha256/cosign breadcrumbs out. Added test_successful_stage_relay_survives_a_long_cleanup_tail as a regression.
Codex review on #1935 (P2): _invoke() truncated stderr to its last 4000 chars before deciding what to relay, so a successful extraction that logs one cleanup line per stale quarantine/staging directory after the sha256/cosign breadcrumbs could push those breadcrumbs out of the relayed data — reopening the exact audit gap the previous commit closed. Truncation now only applies to the bounded error-details path; the success-path relay sees the full stderr.
Independent review — approve, 0 blockingReviewed as an outside pair of eyes (I did not write this), with extra scrutiny because this sits next to the sha256/cosign verification path. Everything below was executed, not read off the diff. Hard fences — respected
Gap 1 — job_id threading
Gap 2 — success-path relayConfirmed the mechanism: Gap 3 —
|
| Revert | Result |
|---|---|
Updater(channel=channel, job_id=job_id) → drop job_id |
test_job_runners_thread_job_id_into_updater — 3 failed (apply/prepare/commit) |
delete the _relay_child_log(...) call |
test_successful_stage_relays_the_childs_breadcrumbs_into_the_journal + the long-tail test — 2 failed |
relay full_stderr[-4000:] (the first-pass code) |
test_successful_stage_relay_survives_a_long_cleanup_tail — 1 failed, other two pass. The regression test genuinely pins commit 2. |
re-add job["cosign_skipped"] = ... |
test_prepare_job_never_reports_cosign_skipped — 1 failed |
Full suite on the head commit: 329 passed (tests/updater tests/api/test_updater_routes.py, HAL0_HOME=$(mktemp -d), FORCE_COLOR unset). ruff format --check and ruff check clean; PR checks green.
Non-blocking findings
-
The failure path still has the truncation hazard the success path just lost. On rc!=0 nothing is relayed and only
full_stderr[-4000:]reachesdetails. A failure after cosign passed (e.g. during activate) with a long cleanup/error tail loses thesha256_ok/cosign_verify_okevidence entirely — which is the run where you most want to know what was verified before things went sideways.test_failed_stage_does_not_double_relay_stderr_as_breadcrumbscements the asymmetry. Consider relaying the full stderr on the failure path too (journal) while keeping the bounded tail indetails(error envelope) — those are different sinks, so it isn't a true double-record. Follow-up, not a blocker for updater: job_id=None on every updater.* journal line, no prepare/verify breadcrumbs on success, cosign_skipped has no producer #1901's stated scope. -
Routed path: the child still logs
job_id=Noneinside the relayed text. On a real boxhal0-apiruns as the hal0 service user, sostage/activatego through the sudo seam and the child is not told the job id (_seam_argvpasses only verb/channel/version). The parent's relay event carries the realjob_id, but the child's own breadcrumbs insideline='...'still readjob_id=None— the issue's own repro grep will still reportjob_id=Nonehits. I'd not fix this here (extending the argv means touching the sudoers-pinned wrapper contract for an observability nicety), but it's worth a line in the issue close-out so the next validator doesn't re-file it. -
Informational: one
log.infoper child line and no cap. Realistic volume is small; only a pathological run (>10k lines/30s) would meet journald's default rate limit, at which point relayed lines could be dropped. Not worth guarding today, but a head+tail cap would be the shape if it ever bites — a plain "first N lines" cap would reintroduce the bug commit 2 fixed in mirror image.
Verdict: approve. The three gaps map cleanly onto the issue's constructs, the fences are intact, the self-caught truncation bug is genuinely pinned by its regression test, and the relay has no injection or double-record path I could construct.
Refs #1901
Summary
Fixes the three observability gaps named in #1901 (found by
rc-validatev1.0.0-rc.6,update-audit-trail-gaps). Verification logic itself is untouched — this is journal/logging only.job_id=Noneon everyupdater.*journal line. The three background job runners (_run_apply_job,_run_prepare_job,_run_commit_job) constructedUpdater(channel=channel)without thejob_idthe constructor already accepts and threads through to every log call. Now:Updater(channel=channel, job_id=job_id).hal0-update stage/activate/discardchild pins its structured log to stderr so stdout carries only the result envelope.UpdateSeam._invokeonly folded that stderr into the raised error on failure — on success it was captured bysubprocess.runand silently discarded, sosha256_ok/cosign_verify_ok/prepare_ok/etc. never reached the journal on a successful run. Added_relay_child_log, which replays each captured stderr line into the parent's own structured log (tagged withverb+job_id) once_invokesees a zero return code.cosign_skippedis a dead job field.stage_release()never emits it, and cosign verification has no bypass (_verify_cosign's own docstring: "Verification is always mandatory — there is no bypass"), so the field always readnull. Removed the producer-less consumer rather than inventing a fake producer.Explicitly out of scope (hard fences from the wave planner)
version:nullon a prepare job — that's the operator's pin, paired withresolved_version. Not touched.stage_release, steps 3–6 then cache write), consistent with the issue's own note. No new finding to escalate.Test plan
make lintuv run ruff format --check src testsenv -u FORCE_COLOR HAL0_HOME=$(mktemp -d) uv run --extra dev pytest tests/updater tests/api/test_updater_routes.py -q— 328 passedtest_job_runners_thread_job_id_into_updater(parametrized apply/prepare/commit) — regression for gap 1test_successful_stage_relays_the_childs_breadcrumbs_into_the_journal+test_failed_stage_does_not_double_relay_stderr_as_breadcrumbs— regression for gap 2test_prepare_job_never_reports_cosign_skipped— regression for gap 3