Repository navigation
HEALTH-0: an out-of-band, read-only fleet health check - #11681
Conversation
…ent password, typed custody-read refusal
srv1 and srv2 were brought up by hand on an operator-chosen BMC root password, so neither the
factory probe nor custody could authenticate to them. This adds the route that takes such a BMC
into custody, and ran it on both on 2026-09-18.
- gunbc.bmc_onboarding: srv1/srv2 onboarding plans, BMC endpoints read from the fleet intent.
- gunbc.secret_provision_actuator: create_secret_container split out of create_secret_for_attempt
(which now delegates), for flows whose payload is minted after custody is established.
- gunbc.tools.bmc_onboard:
- the custody container is observed, and created once on a pre-mutation 404, before the first
version is written and before the BMC is touched; an unknown create outcome stops for a human;
- bmc_takeover_credential proves an operator-supplied current password (file named by
GUNBC_BMC_TAKEOVER_PASSWORD_FILE, never source) against the BMC, then mints, stores, reads
back, rotates FROM that password and re-authenticates; rotation now takes the current password
as a parameter instead of assuming the factory one;
- a failed custody read in the credential probe is classified before its payload is decoded
(it crashed the interpreter on a null when no bmc-srv1-admin existed);
- srv1/srv2 entries select the run's token source (WIF or GUNBC_GCP_ACCESS_TOKEN_FILE).
Receipt (2026-09-18, operator token, run from srv1): both takeovers exited 0 with version 1
written and read back; independently, the stored bmc-srvN-admin secret authenticates (200) and the
old demo password is refused (401) on both BMCs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… in its own custody secret Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…atError, srv1) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…unreadable, never absent (review 67794) Members are read through gunbc.scm.json_member rather than a local optional-collapsing lookup; a skipped member used to shorten the membership and let an unread reader read as absent, authorizing a create. RoleId/Enabled go through the same authority. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e the reader login; per-host body path refused on failed removal (review 67817) - bmc_custody_refused / bmc_custody_refusal_reason deleted: the accessor fabricated "" for an unreachable arm. bmc_store_credential_verified, bmc_store_and_rotate and the reader create now match BmcCustodyContainer directly; the witness that locked the predicate pair in is deleted (the tool module stays typechecked through the reader witness's import). - The reader create is its own function: POST, then the administrator role read-back is matched, and only in its converged arm does the reader login repoint the shared netrc. - The create body path is per host, and a failed removal refuses before the POST outcome is read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…w 67836)
- bmc_custody_credential answers Held | Absent (404 only) | Unreadable { cause }; a refused
status or an unreached call no longer reads as a secret that is not in custody.
- bmc_login_attempt answers Accepted | Rejected { body } | NotAttempted { reason }; a netrc that
could not be written keeps netrc_failure_reason's located cause instead of reading as a
refused password.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sifier (review 67848) CustodyCredentialRead / custody_read_refusal_cause / bmc_custody_credential move down into gunbc.tools.bmc_onboard, and bmc_probe_credential_phase uses them: a 404 says custody holds no credential, every other failed read carries its cause, instead of one fixed sentence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- BmcOnboardingPlan carries the fleet HostIdentity; altra_onboarding_plan derives the admin secret name from it and takes the endpoint from that host's fleet_intent baseboard, for all four hosts. The reader converge takes only the plan: the reader secret and body path derive from plan.host, so one host's secret cannot be paired with another host's BMC. - bmc_stored_credential_read_succeeded is deleted; custody_read_refusal_cause answers none for a success and bmc_custody_credential matches once, with no unreachable arm. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…stake (review 67874) They land with the HEALTH-0 collector that produces HostHealthObserved, not before it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…for both BMC images Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d split out - product.host_health: pure host/fleet assessment over std.goal_assessment (signals: memory counters, processor/platform, fans via product.fan_tach_health, temperatures, BMC event window). - product.fan_tach fan_tach_window_of: the window's one producer from samples. - extdeps.bmc.http: readonly GetManager, GetChassisThermal, GetChassisSensor, GetSystemEventLogEntries. - extdeps.bmc.openbmc_message_registry: MemoryECCCorrectable/Uncorrectable ids. - gunbc.fleet_health_observe: closed FleetHealthRedfishRead vocabulary, surface derived from the live firmware through the board catalog, sampled fan/temperature windows, event log window with wrap/clear continuity, private per-run netrc shredded on every path. - gunbc.bmc_custody_read: the custody read, split out of gunbc.tools.bmc_onboard so a read-only consumer need not import the minting tool. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…roster; board secondary tach headers declared - gunbc.fleet_health: goals derived from fleet_intent_known_hosts (fan expectation rows, the board's 20-sensor temperature roster with no cited limit, NoPriorBaseline); reader credential from custody; one keyed observation per rostered host; assess_fleet_health; receipt rendered as the exit reason and written to GUNBC_FLEET_HEALTH_RECEIPT when set. - gunbc.fleet_fan_tach_expectation: FAN1_1..FAN5_1, which both images publish and no host wires, declared tach-not-expected on all four hosts; the two fan witnesses that count judged headers updated with the reason beside them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…wave admission)
The custody read moved out of gunbc.tools.bmc_onboard into its own read-only module; each
consumer's binding of CustodyCredential{Held,Absent,Unreadable}, bmc_custody_credential and
custody_read_refusal_cause now resolves to gunbc.bmc_custody_read. One TargetChanged row per
binding, owner gunbc#11681; the rows are consumed on landing and owe deletion then.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ws; delete the 40 consumed #11587 rows Touching the roster makes its consumed rows due: the 40 ACTION-USE admissions (owner #11587) were already satisfied at the base. The 13 custody-read rows now name gunbc#11696, which deletes them once #11681 lands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Transition-admission migration for gunbc#11704. No action until the operator sequences #11704's landing. gunbc#11704 moves namespace transition admissions out of the tree. Once it lands, any file under To migrate (paste + delete), after #11704 is on main and merged into this branch: git rm \
dag/gunbc/namespace/transition_admission/gunbc_tools_bmc_health_reader_converge_bmc_health_reader_converge_as_admin_bmc_custody_credential.dag \
dag/gunbc/namespace/transition_admission/gunbc_tools_bmc_health_reader_converge_bmc_health_reader_converge_as_admin_custodycredentialabsent.dag \
dag/gunbc/namespace/transition_admission/gunbc_tools_bmc_health_reader_converge_bmc_health_reader_converge_as_admin_custodycredentialheld.dag \
dag/gunbc/namespace/transition_admission/gunbc_tools_bmc_health_reader_converge_bmc_health_reader_converge_as_admin_custodycredentialunreadable.dag \
dag/gunbc/namespace/transition_admission/gunbc_tools_bmc_health_reader_converge_bmc_health_reader_converge_bmc_custody_credential.dag \
dag/gunbc/namespace/transition_admission/gunbc_tools_bmc_health_reader_converge_bmc_health_reader_converge_custodycredentialabsent.dag \
dag/gunbc/namespace/transition_admission/gunbc_tools_bmc_health_reader_converge_bmc_health_reader_converge_custodycredentialheld.dag \
dag/gunbc/namespace/transition_admission/gunbc_tools_bmc_health_reader_converge_bmc_health_reader_converge_custodycredentialunreadable.dag \
dag/gunbc/namespace/transition_admission/gunbc_tools_bmc_onboard_bmc_probe_credential_phase_bmc_custody_credential.dag \
dag/gunbc/namespace/transition_admission/gunbc_tools_bmc_onboard_bmc_probe_credential_phase_custodycredentialabsent.dag \
dag/gunbc/namespace/transition_admission/gunbc_tools_bmc_onboard_bmc_probe_credential_phase_custodycredentialheld.dag \
dag/gunbc/namespace/transition_admission/gunbc_tools_bmc_onboard_bmc_probe_credential_phase_custodycredentialunreadable.dag \
dag/gunbc/namespace/transition_admission/test_claim_bmc_health_reader_witness_only_a_404_reads_as_absent_custody_custody_read_refusal_cause.dag
git commit -F msg.txt # msg.txt = the text below, verbatim
msg.txt |
… disclose the qualified recompute realization - extdeps.bmc.openbmc_message_registry: MemoryExtendedECCCEData (detail of an error already counted), Ampere DIMM_HOT assert/deassert, phosphor-sel-logger threshold assert/deassert text. - product.host_health: asserts diverge, deasserts are recoveries, detail records add nothing; deviations aggregate per subject with a count (a flapping sensor is one condition). - gunbc.fleet_health: the route is qualified under the recompute realization (GUNBC_EVAL_MEMO=0, extdeps.realization.eval_memo) -- 2026-09-19 on the captured OOB responses, complete decoded output byte-identical memo-on/off, 1.08 MB log ~430 s -> ~27 s CPU. The receipt's first line discloses the realization in effect; the general memo-admission defect is work item adhoc-955f2d4f-274. Receipt lines are flattened to one per finding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ster touch makes due Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…try, with the service-call blind spot pinned as an executed absence call_reachable_decls from gunbc.fleet_health over the nine health module files: no reached body spells a mutating bare call. The enrolled fixture's bare-call mutations are found by identity (discriminating RED), and the walk is shown to reach the collector three levels down. The residual is measured, not promised: neither v2.std.fn_index's nor v2.std.decl_index's declaration skeleton carries a SERVICE call's operation identity, so a service-call mutation is invisible -- as are the collector's own five admitted Redfish GETs. That absence is asserted on the fixture's service-call SetAccountPassword so it FAILS when the instrument gains service-call identity, which is the trigger to extend the roster and delete the claim. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The health collector shreds its reader netrc on every path out and refuses the host when the shred fails; a run killed by SIGKILL reaches no path out. Receipt: the 2026-09-19 fleet run killed by its own timeout during srv4 left the srv4 reader password in /tmp on srv1. Ceiling and trigger are named: the structural answer needs a transport that reads a credential without a filesystem path; the attainable next rung is an execution binding whose teardown is owned by the supervisor rather than the process. A start-of-run sweep is explicitly refused as that capability. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s into (review 69184) call_reachable_decls cannot follow a callee whose module is absent from the pool, so enumerating nine modules while the route calls into gunbc.auth.netrc_binding (mktemp, owner-only write, remove) and gunbc.auth.access_token_source (token file read, gcloud token print) made the claim broader than its executed evidence -- with RecursiveForce on the forbidden roster and the module that makes filesystem calls outside the walk. Both modules are in the pool, and the positive control now asserts the walk reaches place_netrc_binding_privately, shred_netrc_binding and ensure_access_token by identity, so a pool that loses one fails there instead of silently narrowing the claim. 4/4 pass remotely: the closure is still clean of mutating bare calls with the wider pool. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Fixed in the latest push. Review 69184 was right and the finding was the sharp kind:
4/4 pass remotely with the wider pool: the closure is still clean of mutating bare calls. The service-call blind spot is unchanged and still pinned as an executed absence. — sent from swift-newt-222 |
The branch had already deleted these rows while they lived on its parent branch. #11681 then merged, so main carries them again -- which is the correct merge result, not a conflict artifact: they are new on main relative to this branch's base, so the merge re-adds them. The transition they admit is now consumed (the bindings moved to gunbc.bmc_custody_read when #11681 landed), so the rows owe deletion. They are deleted again here, against main. The six add/add conflicts in the health closure resolve to main's side in full: every branch-only line was the superseded earlier form of what #11681 finished landing (MemoryErrorEventsLogged and the DIMM_HOT classification), so main is strictly newer there and this branch had nothing unique to keep. The ten spark rows in the same directory belong to another lane and are untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rovenance The six transition_admission paths go back into v2.compiler.reference_conservation_census reference_conservation_stratified_sample_paths. The sample is pinned to fec339d and declared byte-for-byte rerunnable, with at least six paths per stratum. The census renders an unreadable path as its own `unreadable` line, never an omission, so a deleted path belongs in that rendering, not in a shrunken list. Recomputing the sample is not this change's work. CORRECTION to the first commit's message, which named the wrong landing PRs and a wrong count. The 56 surviving rows came from gunbc#11625 (32), #11565 (10), #11667 (10) and #11689 (4). #11681 contributed none: its 13 rows were already deleted by #11696. #11771 removed 23 rows, not 17. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
HEALTH-0: the first fleet health check. It is out-of-band, read-only, and goal-blind, and it reports what it cannot establish rather than passing over it.
Per the ruling on this work (2026-09-18): a separate protocol beside convergence, never an arm of
FleetConvergeRequest; the roster comes fromfleet_intent_known_hosts; no convergence lease; the observer takes no expectation; a signal the BMC does not expose is a typed gap and never an in-band fallback.What it does, per host
gunbc_healthReadOnly account, through a private per-run netrc it shreds on every path out.Assessment is
std.goal_assessment: deviations, unknowns and refusals stay apart, and the fleet is green only if every rostered host is. An unreachable host stays in the denominator as a located refusal.Modules
product.host_health— pure host and fleet assessment. Memory counters with an explicit baseline seam (NoPriorBaselinetoday; HEALTH-1 supplies admitted baselines), temperatures with availability and the BMC's own verdict kept apart from the reading, the event window, fans delegated whole toproduct.fan_tach_health.product.fan_tachfan_tach_window_of— the window's one producer from samples; until now everyFanTachWindowin the tree was hand-transcribed.extdeps.bmc.redfish_telemetry— Sensor, Thermal and LogEntry decoders for both BMC images. A reading is a value, a null or missing: three facts, never coerced.extdeps.bmc.openbmc_message_registry— MemoryECC correctable/uncorrectable, the ECC detail record, Ampere DIMM_HOT, and phosphor-sel-logger threshold text. Asserts diverge, deasserts are recoveries, detail records add nothing to the error they accompany.gunbc.fleet_health_observe— the collector. ClosedFleetHealthRedfishReadvocabulary (five GETs), surface derived from live firmware, sampled windows, event-window continuity.gunbc.fleet_health— goals derived from the roster, the receipt, and the entryfleet_health_check.gunbc.bmc_custody_read— the custody read, split out ofgunbc.tools.bmc_onboardso the health closure imports nothing that can mint or rotate a credential.gunbc.fleet_fan_tach_expectation— the board's secondary tach headersFAN1_1..FAN5_1, which both images publish and no host wires, declared not-expected on all four hosts.Status (wind-down, 2026-09-20)
The lane is winding down to free the system for v1 performance and the v2 migration. This PR is complete and green as it stands; the one remaining item — a live run whose receipt matches the current code (the receipt below predates the event classification and the realization disclosure) — is carried as the roadmap row
fleet-health-0-live-receiptin #11859 rather than held in a session. The live evidence below is real and unchanged in what it found.Evidence
/tmp/fleet-health-receipt.txton srv1.claim_batch): host health assessment 16/16 (the ruling's discriminating cases:0->0,4->5with residue kept, regression/reboot/endpoint-set change cannot construct a delta, no-baseline-with-residue, a baseline for another host refuses, a wrapped log is not an empty delta, one zero fan sample is not a failure, missing sensor unknown vs duplicate refusal, 0 °C vs unavailable vs disabled vs absent, uncited limit never green, one unreachable host blocks the fleet without hiding the others, empty roster refuses); collector 5/5; goal 1/1; registry 2/2; Redfish decoders 5/5 on captured srv1 and srv3 responses.call_reachable_declsfrom the entry over the nine health modules finds no mutating bare call; the enrolled forbidden-program fixture's mutations are found by identity; the walk demonstrably reaches the collector three levels down. Its limit is measured, not promised: neither declaration census carries a service call's operation identity, so a service-call mutation is invisible — as are the collector's own five GETs. That absence is asserted against the fixture, so the claim fails the day the instrument gains service-call identity, which is the trigger to extend it.Realization
The route is qualified under the recompute realization (
GUNBC_EVAL_MEMO=0,extdeps.realization.eval_memo): the seed's eval-frame memo keys every pure call before admission, and the decoders thread whole response bodies through recursive pure calls, so under the memo each call re-hashes the document — the 1.08 MB srv3 event log decoded in ~430 s of CPU against ~27 s. Qualified 2026-09-19 on the captured responses: complete decoded output byte-identical under both realizations (sha256a5c00479…), SipHash 79% → 4.6% of samples. The receipt's first line discloses the realization actually in effect. The general defect is its own lane (#11741 and its staged rewires).Known residuals, named
gunbc.recurring_failure_mode.a_killed_run_leaves_its_credential_file_behind, with ceiling and trigger named.🤖 Generated with Claude Code