Skip to content

HEALTH-0: an out-of-band, read-only fleet health check - #11681

Merged
briansrls merged 21 commits into
mainfrom
fleet-health-0
Sep 20, 2026
Merged

briansrls merged 21 commits into
mainfrom
fleet-health-0

Conversation

@briansrls

@briansrls briansrls commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

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 from fleet_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

  1. Reads the BMC's live firmware version and derives its telemetry surface from the board catalog, so a flash changes the route rather than leaving a stale row behind.
  2. Reads the event log at the window's opening and closing, and refuses the window if the log wrapped past the opening's newest entry or was cleared in between.
  3. Samples fans and temperatures 5 times between those reads.
  4. Authenticates only as the gunbc_health ReadOnly 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 (NoPriorBaseline today; 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 to product.fan_tach_health.
  • product.fan_tach fan_tach_window_of — the window's one producer from samples; until now every FanTachWindow in 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. Closed FleetHealthRedfishRead vocabulary (five GETs), surface derived from live firmware, sampled windows, event-window continuity.
  • gunbc.fleet_health — goals derived from the roster, the receipt, and the entry fleet_health_check.
  • gunbc.bmc_custody_read — the custody read, split out of gunbc.tools.bmc_onboard so the health closure imports nothing that can mint or rotate a credential.
  • gunbc.fleet_fan_tach_expectation — the board's secondary tach headers FAN1_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-receipt in #11859 rather than held in a session. The live evidence below is real and unchanged in what it found.

Evidence

  • Live run, 2026-09-19 13:51Z, all four BMCs, read-only. Fleet INDETERMINATE, which is the honest answer while no temperature limit is cited and memory counters are not exposed out-of-band. Findings: srv1 FAN4 rotating in 1 of 5 samples and FAN5 in 2 of 5; srv2 FAN5 in 2 of 5; srv3 logged 14 corrected memory-error events during the ~30-minute window plus DIMM_HOT on channel 2; srv4 DIMM_HOT on channels 2 and 6. Receipt kept at /tmp/fleet-health-receipt.txt on srv1.
  • Witnesses (remote claim_batch): host health assessment 16/16 (the ruling's discriminating cases: 0->0, 4->5 with 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.
  • No mutation reachable (witness 12): call_reachable_decls from 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 (sha256 a5c00479…), 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

  • Memory error counters are not exposed out-of-band on either image, so every host is unknown on that axis; srv1/srv2 additionally have empty event logs on firmware 2.07. Flashing them to 3.22 is what would expose it.
  • No temperature limit is cited yet, so every sensor reports its limit as unestablished. The BMC's own threshold verdict is still carried.
  • srv3/srv4 event logs wrap at ~1200 entries and are flooded by fan-threshold flapping, so continuity holds only over short windows.
  • A killed run can leave its reader credential file behind: recorded as gunbc.recurring_failure_mode.a_killed_run_leaves_its_credential_file_behind, with ceiling and trigger named.

🤖 Generated with Claude Code

Brian Searls and others added 14 commits September 18, 2026 15:02
…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>
@gunbai-bot

gunbai-bot Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

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 dag/gunbc/namespace/transition_admission/ refuses at this PR's own gate, and the admission has to be carried in a commit message on this branch instead. The block below was derived mechanically from this PR's 13 row file(s) at its current head. The only edits: deletion_follow_up, owner_pull_request and their now-unused imports are dropped, because those fields no longer exist. All 13 blocks load through the production fold (carried_admissions_from_messages) with no refusal.

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

-F keeps the lines exactly as they are. The squash merge carries the message into the queue run, and nothing lands in the tree. If a row is wrong later, a later block with the same stem supersedes it.

msg.txt
Move transition admissions into the commit message (gunbc#11704)

```transition-admission
module gunbc.namespace.transition_admission.gunbc_tools_bmc_health_reader_converge_bmc_health_reader_converge_as_admin_bmc_custody_credential

import std.types { NonEmptyStr, List }
import std.decl_ref { decl_ref }
import gunbc.compiler_frontend_program_interlock { TargetChanged }
import gunbc.namespace.transition_admission { TransitionAdmission, Binding }

data gunbc_tools_bmc_health_reader_converge_bmc_health_reader_converge_as_admin_bmc_custody_credential: TransitionAdmission = TransitionAdmission {
  label: "BMC custody read moves to its read-only module gunbc.bmc_custody_read (gunbc#11681)" as NonEmptyStr,
  subject: Binding {
    enclosing: decl_ref("gunbc.tools.bmc_health_reader_converge", "bmc_health_reader_converge_as_admin"),
    spelling: "bmc_custody_credential" as NonEmptyStr,
    expected_candidates: [decl_ref("gunbc.bmc_custody_read", "bmc_custody_credential")],
  },
  disposition: TargetChanged,
}
```

```transition-admission
module gunbc.namespace.transition_admission.gunbc_tools_bmc_health_reader_converge_bmc_health_reader_converge_as_admin_custodycredentialabsent

import std.types { NonEmptyStr, List }
import std.decl_ref { decl_ref }
import gunbc.compiler_frontend_program_interlock { TargetChanged }
import gunbc.namespace.transition_admission { TransitionAdmission, Binding }

data gunbc_tools_bmc_health_reader_converge_bmc_health_reader_converge_as_admin_custodycredentialabsent: TransitionAdmission = TransitionAdmission {
  label: "BMC custody read moves to its read-only module gunbc.bmc_custody_read (gunbc#11681)" as NonEmptyStr,
  subject: Binding {
    enclosing: decl_ref("gunbc.tools.bmc_health_reader_converge", "bmc_health_reader_converge_as_admin"),
    spelling: "CustodyCredentialAbsent" as NonEmptyStr,
    expected_candidates: [decl_ref("gunbc.bmc_custody_read", "CustodyCredentialAbsent")],
  },
  disposition: TargetChanged,
}
```

```transition-admission
module gunbc.namespace.transition_admission.gunbc_tools_bmc_health_reader_converge_bmc_health_reader_converge_as_admin_custodycredentialheld

import std.types { NonEmptyStr, List }
import std.decl_ref { decl_ref }
import gunbc.compiler_frontend_program_interlock { TargetChanged }
import gunbc.namespace.transition_admission { TransitionAdmission, Binding }

data gunbc_tools_bmc_health_reader_converge_bmc_health_reader_converge_as_admin_custodycredentialheld: TransitionAdmission = TransitionAdmission {
  label: "BMC custody read moves to its read-only module gunbc.bmc_custody_read (gunbc#11681)" as NonEmptyStr,
  subject: Binding {
    enclosing: decl_ref("gunbc.tools.bmc_health_reader_converge", "bmc_health_reader_converge_as_admin"),
    spelling: "CustodyCredentialHeld" as NonEmptyStr,
    expected_candidates: [decl_ref("gunbc.bmc_custody_read", "CustodyCredentialHeld")],
  },
  disposition: TargetChanged,
}
```

```transition-admission
module gunbc.namespace.transition_admission.gunbc_tools_bmc_health_reader_converge_bmc_health_reader_converge_as_admin_custodycredentialunreadable

import std.types { NonEmptyStr, List }
import std.decl_ref { decl_ref }
import gunbc.compiler_frontend_program_interlock { TargetChanged }
import gunbc.namespace.transition_admission { TransitionAdmission, Binding }

data gunbc_tools_bmc_health_reader_converge_bmc_health_reader_converge_as_admin_custodycredentialunreadable: TransitionAdmission = TransitionAdmission {
  label: "BMC custody read moves to its read-only module gunbc.bmc_custody_read (gunbc#11681)" as NonEmptyStr,
  subject: Binding {
    enclosing: decl_ref("gunbc.tools.bmc_health_reader_converge", "bmc_health_reader_converge_as_admin"),
    spelling: "CustodyCredentialUnreadable" as NonEmptyStr,
    expected_candidates: [decl_ref("gunbc.bmc_custody_read", "CustodyCredentialUnreadable")],
  },
  disposition: TargetChanged,
}
```

```transition-admission
module gunbc.namespace.transition_admission.gunbc_tools_bmc_health_reader_converge_bmc_health_reader_converge_bmc_custody_credential

import std.types { NonEmptyStr, List }
import std.decl_ref { decl_ref }
import gunbc.compiler_frontend_program_interlock { TargetChanged }
import gunbc.namespace.transition_admission { TransitionAdmission, Binding }

data gunbc_tools_bmc_health_reader_converge_bmc_health_reader_converge_bmc_custody_credential: TransitionAdmission = TransitionAdmission {
  label: "BMC custody read moves to its read-only module gunbc.bmc_custody_read (gunbc#11681)" as NonEmptyStr,
  subject: Binding {
    enclosing: decl_ref("gunbc.tools.bmc_health_reader_converge", "bmc_health_reader_converge"),
    spelling: "bmc_custody_credential" as NonEmptyStr,
    expected_candidates: [decl_ref("gunbc.bmc_custody_read", "bmc_custody_credential")],
  },
  disposition: TargetChanged,
}
```

```transition-admission
module gunbc.namespace.transition_admission.gunbc_tools_bmc_health_reader_converge_bmc_health_reader_converge_custodycredentialabsent

import std.types { NonEmptyStr, List }
import std.decl_ref { decl_ref }
import gunbc.compiler_frontend_program_interlock { TargetChanged }
import gunbc.namespace.transition_admission { TransitionAdmission, Binding }

data gunbc_tools_bmc_health_reader_converge_bmc_health_reader_converge_custodycredentialabsent: TransitionAdmission = TransitionAdmission {
  label: "BMC custody read moves to its read-only module gunbc.bmc_custody_read (gunbc#11681)" as NonEmptyStr,
  subject: Binding {
    enclosing: decl_ref("gunbc.tools.bmc_health_reader_converge", "bmc_health_reader_converge"),
    spelling: "CustodyCredentialAbsent" as NonEmptyStr,
    expected_candidates: [decl_ref("gunbc.bmc_custody_read", "CustodyCredentialAbsent")],
  },
  disposition: TargetChanged,
}
```

```transition-admission
module gunbc.namespace.transition_admission.gunbc_tools_bmc_health_reader_converge_bmc_health_reader_converge_custodycredentialheld

import std.types { NonEmptyStr, List }
import std.decl_ref { decl_ref }
import gunbc.compiler_frontend_program_interlock { TargetChanged }
import gunbc.namespace.transition_admission { TransitionAdmission, Binding }

data gunbc_tools_bmc_health_reader_converge_bmc_health_reader_converge_custodycredentialheld: TransitionAdmission = TransitionAdmission {
  label: "BMC custody read moves to its read-only module gunbc.bmc_custody_read (gunbc#11681)" as NonEmptyStr,
  subject: Binding {
    enclosing: decl_ref("gunbc.tools.bmc_health_reader_converge", "bmc_health_reader_converge"),
    spelling: "CustodyCredentialHeld" as NonEmptyStr,
    expected_candidates: [decl_ref("gunbc.bmc_custody_read", "CustodyCredentialHeld")],
  },
  disposition: TargetChanged,
}
```

```transition-admission
module gunbc.namespace.transition_admission.gunbc_tools_bmc_health_reader_converge_bmc_health_reader_converge_custodycredentialunreadable

import std.types { NonEmptyStr, List }
import std.decl_ref { decl_ref }
import gunbc.compiler_frontend_program_interlock { TargetChanged }
import gunbc.namespace.transition_admission { TransitionAdmission, Binding }

data gunbc_tools_bmc_health_reader_converge_bmc_health_reader_converge_custodycredentialunreadable: TransitionAdmission = TransitionAdmission {
  label: "BMC custody read moves to its read-only module gunbc.bmc_custody_read (gunbc#11681)" as NonEmptyStr,
  subject: Binding {
    enclosing: decl_ref("gunbc.tools.bmc_health_reader_converge", "bmc_health_reader_converge"),
    spelling: "CustodyCredentialUnreadable" as NonEmptyStr,
    expected_candidates: [decl_ref("gunbc.bmc_custody_read", "CustodyCredentialUnreadable")],
  },
  disposition: TargetChanged,
}
```

```transition-admission
module gunbc.namespace.transition_admission.gunbc_tools_bmc_onboard_bmc_probe_credential_phase_bmc_custody_credential

import std.types { NonEmptyStr, List }
import std.decl_ref { decl_ref }
import gunbc.compiler_frontend_program_interlock { TargetChanged }
import gunbc.namespace.transition_admission { TransitionAdmission, Binding }

data gunbc_tools_bmc_onboard_bmc_probe_credential_phase_bmc_custody_credential: TransitionAdmission = TransitionAdmission {
  label: "BMC custody read moves to its read-only module gunbc.bmc_custody_read (gunbc#11681)" as NonEmptyStr,
  subject: Binding {
    enclosing: decl_ref("gunbc.tools.bmc_onboard", "bmc_probe_credential_phase"),
    spelling: "bmc_custody_credential" as NonEmptyStr,
    expected_candidates: [decl_ref("gunbc.bmc_custody_read", "bmc_custody_credential")],
  },
  disposition: TargetChanged,
}
```

```transition-admission
module gunbc.namespace.transition_admission.gunbc_tools_bmc_onboard_bmc_probe_credential_phase_custodycredentialabsent

import std.types { NonEmptyStr, List }
import std.decl_ref { decl_ref }
import gunbc.compiler_frontend_program_interlock { TargetChanged }
import gunbc.namespace.transition_admission { TransitionAdmission, Binding }

data gunbc_tools_bmc_onboard_bmc_probe_credential_phase_custodycredentialabsent: TransitionAdmission = TransitionAdmission {
  label: "BMC custody read moves to its read-only module gunbc.bmc_custody_read (gunbc#11681)" as NonEmptyStr,
  subject: Binding {
    enclosing: decl_ref("gunbc.tools.bmc_onboard", "bmc_probe_credential_phase"),
    spelling: "CustodyCredentialAbsent" as NonEmptyStr,
    expected_candidates: [decl_ref("gunbc.bmc_custody_read", "CustodyCredentialAbsent")],
  },
  disposition: TargetChanged,
}
```

```transition-admission
module gunbc.namespace.transition_admission.gunbc_tools_bmc_onboard_bmc_probe_credential_phase_custodycredentialheld

import std.types { NonEmptyStr, List }
import std.decl_ref { decl_ref }
import gunbc.compiler_frontend_program_interlock { TargetChanged }
import gunbc.namespace.transition_admission { TransitionAdmission, Binding }

data gunbc_tools_bmc_onboard_bmc_probe_credential_phase_custodycredentialheld: TransitionAdmission = TransitionAdmission {
  label: "BMC custody read moves to its read-only module gunbc.bmc_custody_read (gunbc#11681)" as NonEmptyStr,
  subject: Binding {
    enclosing: decl_ref("gunbc.tools.bmc_onboard", "bmc_probe_credential_phase"),
    spelling: "CustodyCredentialHeld" as NonEmptyStr,
    expected_candidates: [decl_ref("gunbc.bmc_custody_read", "CustodyCredentialHeld")],
  },
  disposition: TargetChanged,
}
```

```transition-admission
module gunbc.namespace.transition_admission.gunbc_tools_bmc_onboard_bmc_probe_credential_phase_custodycredentialunreadable

import std.types { NonEmptyStr, List }
import std.decl_ref { decl_ref }
import gunbc.compiler_frontend_program_interlock { TargetChanged }
import gunbc.namespace.transition_admission { TransitionAdmission, Binding }

data gunbc_tools_bmc_onboard_bmc_probe_credential_phase_custodycredentialunreadable: TransitionAdmission = TransitionAdmission {
  label: "BMC custody read moves to its read-only module gunbc.bmc_custody_read (gunbc#11681)" as NonEmptyStr,
  subject: Binding {
    enclosing: decl_ref("gunbc.tools.bmc_onboard", "bmc_probe_credential_phase"),
    spelling: "CustodyCredentialUnreadable" as NonEmptyStr,
    expected_candidates: [decl_ref("gunbc.bmc_custody_read", "CustodyCredentialUnreadable")],
  },
  disposition: TargetChanged,
}
```

```transition-admission
module gunbc.namespace.transition_admission.test_claim_bmc_health_reader_witness_only_a_404_reads_as_absent_custody_custody_read_refusal_cause

import std.types { NonEmptyStr, List }
import std.decl_ref { decl_ref }
import gunbc.compiler_frontend_program_interlock { TargetChanged }
import gunbc.namespace.transition_admission { TransitionAdmission, Binding }

data test_claim_bmc_health_reader_witness_only_a_404_reads_as_absent_custody_custody_read_refusal_cause: TransitionAdmission = TransitionAdmission {
  label: "BMC custody read moves to its read-only module gunbc.bmc_custody_read (gunbc#11681)" as NonEmptyStr,
  subject: Binding {
    enclosing: decl_ref("test.claim.bmc_health_reader_witness", "only_a_404_reads_as_absent_custody"),
    spelling: "custody_read_refusal_cause" as NonEmptyStr,
    expected_candidates: [decl_ref("gunbc.bmc_custody_read", "custody_read_refusal_cause")],
  },
  disposition: TargetChanged,
}
```

Brian Searls and others added 3 commits September 19, 2026 18:18
… 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>
Brian Searls and others added 2 commits September 20, 2026 01:13
…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>
@gunbai-bot gunbai-bot Bot changed the title fleet health scout HEALTH-0: an out-of-band, read-only fleet health check Sep 20, 2026
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review September 20, 2026 14:47
…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>
@gunbai-bot

gunbai-bot Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Fixed in the latest push. Review 69184 was right and the finding was the sharp kind: call_reachable_decls cannot follow a callee whose module is absent from the pool, so a nine-module pool while the route calls into gunbc.auth.netrc_binding and gunbc.auth.access_token_source meant the walk never entered the two modules that actually touch the filesystem — with RecursiveForce sitting on the forbidden roster the whole time. The claim was broader than its executed evidence.

  • Both modules are now in health_closure_modules and health_pool_files.
  • The positive control is renamed and strengthened: it asserts the walk reaches place_netrc_binding_privately, shred_netrc_binding and ensure_access_token by identity, so a pool that loses one of them fails there rather than quietly narrowing the headline claim. The old control checked only two in-pool functions and could not have caught this.
  • The module doc now states the pool rule and cites this review as the reason.

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

@briansrls
briansrls added this pull request to the merge queue Sep 20, 2026
Merged via the queue into main with commit 8fb7e52 Sep 20, 2026
4 checks passed
@briansrls
briansrls deleted the fleet-health-0 branch September 20, 2026 16:34
@briansrls
briansrls restored the fleet-health-0 branch September 20, 2026 16:39
gunbai-bot Bot pushed a commit that referenced this pull request Sep 20, 2026
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>
gunbai-bot Bot pushed a commit that referenced this pull request Sep 24, 2026
…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>
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