Skip to content

power-on account slice B1: host-generic boot attempt admission, wired into the boot run - #13230

Merged
gunbai-bot[bot] merged 7 commits into
mainfrom
session/calm-lynx-884-slice-b
Oct 4, 2026
Merged

gunbai-bot[bot] merged 7 commits into
mainfrom
session/calm-lynx-884-slice-b

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Slice B1 of docs/plans/power-on-sequence-model.md §12: admitting one boot attempt's configuration receipt, so the power-on account's attempt-configuration fields can be recorded from what a person saw on the machine. Before this, those fields were always NotRecorded.

Wired. mtcollins1_boot_under_live_unit_hold calls admit_boot_attempt(proof, revision) right after UnitHeld. That is after the boot subject and the unit hold, and before mtcollins1_boot_actuate, so before any pre-power read or write.

  • A refused receipt releases the hold, writes nothing, and fails the run with the typed cause.
  • The frozen configuration travels on the attempt record into the diagnostic bundle, and the power-on account reads it there.
  • Slice A's always-NotRecorded placeholder (mtcollins1_attempt_configuration_receipt) is deleted.

Still owed (slice B2): the pre-power-on hpm check firmware readback bound to the plan, and the controller population reading (PopulationControllerReading, FreshnessEstablished | CachePossible). Until B2 lands, firmware stays NotRecorded with that trigger.

What it adds

gunbc.host_boot_attempt_admission. It is host-generic: it takes a ManagedHostBinding and the unit hold's UnitHoldStoreExecutor, and names no unit except in its route row.

  • Records. AttemptConfigurationPlan and AttemptInspectionReceipt (both sole_constructor), OperatorAttestedTime, and ReceiptEvidence{path, digest, commit}.
  • Fail-closed reader. It reads a committed artifacts/receipts/*.json. Every refusal is a typed AttemptReceiptRefusal arm: wrong schema, missing field, subject, attempt or plan mismatch, attested ordering, malformed or absent file.
  • Attempt identity from the dispatch. The identity is transaction_nonce, joined to the receipt's attempt, then consumed in a create-once slot.
    • The slot lives in its own store root, /var/lib/gunbc/boot-attempts, through the file CAS with ExpectSlotAbsent.
    • Every check on the file finishes before the slot is touched.
    • The key is an injective, length-prefixed encoding over [A-Za-z0-9_-]. It is not a hash, because the corpus hash is 64-bit FNV and not collision-safe.
    • The only write is admit_callers-restricted: admit_boot_attempt → admit_named_receipt → consume_attempt_slot.
  • Absent vs refused. When no receipt is named, the result is a BootAttemptClearance with the configuration NotRecorded; the reason names the HumanIntervention step that would record it. A named receipt that fails any check gives BootAttemptRefused before any pre-power read or actuation.
  • Projection. attempt_configuration_receipt fills the account's expected topology, requested and applied stimulus, and operator-attested CPU and DIMM population. Firmware stays NotRecorded until slice B2 adds the pre-power-on hpm check read and the controller population reading.
  • Workflow (Q6: operator escalation msg_1b57e749, approved by default). A new attempt_receipt dispatch input. The boot step's environment maps it and transaction_nonce into GUNBC_BOOT_ATTEMPT_RECEIPT and GUNBC_BOOT_ATTEMPT_NONCE. The transaction_nonce description no longer says "not consumed by any step". fleet-converge.yml was regenerated via generated_artifact_gate main_wet.

Evidence

  • Existing witnesses still pass under claim_batch Hermetic after the wiring: the diagnostic bundle (71/71), the boot run (35/35) and the acceptance matrix (28/28).
  • test.claim.host_boot_attempt_admission_witness: 10/10 under claim_batch Hermetic. These are the validation fold's arms with supplied reads: the positive projection, explicit null vs missing request, attempt A dispatched as B, another host, a crossed plan, attested ordering, time shape, malformed and absent file, nonce/path refused before any read, key injectivity, and not-named giving every field NotRecorded.
  • test.claim.host_boot_attempt_admission_wet_witness: PASS under --wet against a real temp directory. In one ordered run: first A consumed; repeated A refused as already consumed; B consumed; A still refused after B; and A still refused after a unit hold for the same host is taken and released in the same directory.
    • Discriminating control: with the nonce dropped from the slot key, this claim FAILS.
    • It is enrolled on the local-repo wet lane: a ci_layer_roots exclusion row, a local_repo_wet_schedule row, and a floor_route_gap DirWithTemplate expectation.
  • Plan §11 records Q6 as decided. §12a records where the build differs from the plan, with reasons.

Rework for the side-chat review at 064622b

  1. Admission before any controller read. The SDR cache and SEL baseline (mtcollins1_boot_baseline) now run only under the unit hold and after BootAttemptCleared; a hold or receipt refusal records them as not taken, with the reason.
    • Matrix control: a_refused_named_receipt_reads_no_baseline_and_writes_nothing. A named receipt outside artifacts/receipts/ refuses with no SDR or SEL read and no power action. The matrix passes 29/29.
  2. The receipt is the tracked regular blob at the bound revision.
    • The read: extdeps.git.inspect ListTreeEntryAtPath (new, path-scoped ls-tree -z), decoded by the existing gunbc.namespace_step0_subject_collector reader, then git show <rev>:<path>.
    • Refusals: no entry is ReceiptNotTracked; a symlink, gitlink or directory is ReceiptNotRegularFile.
    • Evidence: ReceiptEvidence.digest: Sha256FileDigest is the SHA-256 of exactly those bytes, via extdeps.tools.sha256sum sha256sum_stdin_digest_via_shell.
    • Controls: w_only_a_regular_tracked_blob_at_the_revision_is_a_receipt (regular admits, symlink and untracked refuse), and the wet one_changed_byte_changes_the_receipt_digest_by_real_execution.
    • Follow-up: the decoder's home should be extdeps.git; extracting it is left to the namespace lane.
  3. Times. rfc3339_utc_seconds is deleted; times now use gunbc.auth.approval_capability utc_instant_is_canonical and utc_instant_before. Control: w_an_impossible_calendar_time_refuses_and_equal_instants_are_admitted (month 99, Feb 30 and hour 24 refuse; equal instants are admitted).
  4. Provisioned store. gunbc.runner_host_grants unit_hold_store_operations now also ensures /var/lib/gunbc/boot-attempts, with the same hosts, owner and mode as the unit-hold store, as a separate directory.
    • Privileged grant: the regenerated provisioning/srv1/gunbc-ghrunner.sudoers gains exactly that install -d line.
    • Owed: the executed control on srv1 (grant convergence, then a receipt-carrying boot) needs the BMC and a dispatch.
  5. Exact population joins.
    • CPUs: the CPU rows must name exactly plan.expected.
    • DIMMs: the DIMM rows must name exactly the host's slot roster. For Mt. Collins that is the Getting Started Guide's 32 connectors (dimm_figure_banks), labelled by connector number, on their bank's socket.
    • Refusal causes: ReceiptCpusNotTheExpectedSockets, ReceiptDimmUnknownSlot, ReceiptDimmOnWrongSocket, ReceiptDimmSlotOmitted.
    • Controls: w_a_population_that_does_not_join_its_roster_refuses (all four REDs, plus a positive) and w_the_mt_collins_roster_is_the_guides_32_connectors_by_socket.

Local evidence at this head (claim_batch):

Witness Result
admission, hermetic 14/14
wet controls 2/2
acceptance matrix 29/29
bundle 71/71
boot_run 35/35
account 28/28
executor privileged ops 14/14
fleet_converge_plan 87/87

🤖 Generated with Claude Code

gunbc-ci-auto-heal and others added 5 commits October 4, 2026 06:12
…rollment, dispatch input

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d; bundle carries the frozen configuration; delete slice A placeholder

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@gunbai-bot gunbai-bot Bot changed the title power-on account slice B1: host-generic boot attempt admission (receipt + create-once attempt slot) power-on account slice B1: host-generic boot attempt admission, wired into the boot run Oct 4, 2026
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review October 4, 2026 09:41

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 064622b08ace28e833ff63fda50d59692305704a.

The three implementation guardrails from the plan review are substantially present:

  • the slot key is an injective encoding of stable host identity plus nonce, not a serialization of ManagedHost or a 64-bit hash;
  • admit_named_receipt reaches the create-once CAS only from ReceiptValidated, after the file, schema, subject, attempt, plan and attested-order checks;
  • production derives the store-host executor from the UnitHoldProof and admits no independently supplied host label;
  • the absent-input arm remains NotRecorded, while a named invalid receipt refuses and releases the unit hold before actuation.

Five blockers remain.

1. Admission is not before the attempt's pre-power reads

The approved placement is stronger than “after UnitHeld and before actuation”: clearance must exist before any pre-power read. In the real BMC-user branch, mtcollins1_boot_sdr_cache and mtcollins1_boot_sel_snapshot execute before mtcollins1_boot_under_live_unit_hold, which is where the hold and admit_boot_attempt occur.

So a malformed, mismatched or replayed receipt still causes BMC reads before it is refused. It also means those baseline readings are taken outside the unit hold that defines this attempt.

Move the unit-hold acquisition and attempt admission ahead of those reads, then take the SDR/SEL baseline only under the resulting clearance. Required control: a named receipt that refuses must dispatch neither the BMC baseline reads nor an actuation.

2. ReceiptEvidence is neither the approved SHA-256 evidence nor actually joined to the named commit

The approved carrier is ReceiptEvidence { path, digest: Sha256FileDigest, commit }. This cut instead stores digest: NonEmptyStr produced by content_hash_of_value, which is this corpus's 64-bit FNV structural hash. That is computation identity, not byte-integrity evidence.

Separately, revision is copied beside a normal Filesystem.Read(path). Nothing proves the bytes came from that revision. The lexical artifacts/receipts/*.json check also follows a committed symlink, so a path inside the directory can read bytes outside the checkout and still be rendered as path@GITHUB_SHA.

Read the regular tracked blob at the bound revision, or establish an equivalent typed tracked-file/regular-file join, and digest the exact admitted bytes through the existing SHA-256 file-digest authority. Required controls: a symlink or untracked/worktree-only file cannot mint a receipt; changing one byte changes the SHA-256 evidence.

3. The new time parser repeats an existing authority and accepts impossible dates

rfc3339_utc_seconds checks only width, punctuation and digit positions. It accepts values such as 2026-99-99T99:99:99Z and 2026-02-30T12:00:00Z, then the plan orders them lexicographically as though they were instants.

The repository already has the exact authority in gunbc.auth.approval_capability: utc_instant_is_canonical validates field ranges and the Gregorian calendar, and utc_instant_before names the comparison over canonical inputs. Reuse/extract that authority rather than creating a second, weaker timestamp contract here.

Required REDs: invalid month, invalid day-for-month and hour 24 each refuse; equal canonical instants remain admitted because the plan says fixed-at is at or before completed-at.

4. The production attempt-slot directory has no provisioned standing

This cut writes /var/lib/gunbc/boot-attempts, but no changed provisioning/grant row creates that directory or establishes its owner and mode. file_compare_and_set creates the generation file, not its parent. The wet witness passes because mktemp hands it an already-existing directory; it does not exercise the production root.

The neighbouring unit-hold store has an explicit EnsureOwnedDirectory route in gunbc.runner_host_grants. Give the attempt-admission namespace the same provisioned standing, or place it in an already admitted durable namespace without letting hold cleanup reach its keys. Add an executed control on the real store host showing the admitted process reaches the production store.

5. Partial population lists are promoted to complete operator attestations

cpu_rows enforces only a non-empty list with unique socket numbers. It does not require coverage of plan.expected; [socket 0] is accepted beside expected sockets [0,1] despite the refusal text saying “one per socket.”

dimm_rows likewise enforces only a non-empty list with unique labels. A one-row list, an unknown label, or a label paired with the wrong socket becomes PopulationOperatorAttested. Downstream, recorded_slot_rows treats that arm as the recorded population and compares the BMC only with the populated rows supplied, so an omitted populated DIMM silently lowers the expected count instead of remaining an open question.

Join CPU rows exactly to the plan's expected socket roster. Join DIMM rows to the host's admitted static slot topology, including label-to-socket identity and complete coverage, or introduce an explicit partial standing that cannot become PopulationOperatorAttested.

Required REDs: missing expected CPU socket; unknown DIMM label; known label on another socket; and an omitted slot.

The B1/B2 split itself is honest: leaving firmware and controller-freshness corroboration NotRecorded for B2 is fine. Exact-head required CI was still queued at review time; the findings above are semantic and independent of its result.

…ead; receipt is the tracked regular blob at the bound revision with SHA-256 evidence; canonical UTC instants from approval_capability; provisioned attempt store; exact CPU/DIMM roster joins

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head b1d4bab18dfc865239713a8c7e967164ce691a82.

Four and a half of the five prior blockers are closed:

  • The unit hold and BootAttemptCleared now precede the SDR/SEL baseline; a refused named receipt releases the hold and carries a not-taken baseline, so it neither reads the controller nor actuates it.
  • The receipt is read as the regular tracked blob at the bound revision (ls-tree subject check, then git show ref:path), and ReceiptEvidence carries the SHA-256 of those exact bytes. Symlink and untracked controls, plus the one-byte digest control, discriminate the old path.
  • Attested times now reuse utc_instant_is_canonical / utc_instant_before, including the impossible-calendar REDs and equal-instant positive control.
  • /var/lib/gunbc/boot-attempts has a desired EnsureOwnedDirectory standing and the generated srv1 sudoers line.
  • CPU rows join exactly to the expected socket set; DIMM rows join for completeness, known identity and socket placement rather than becoming a partial PopulationOperatorAttested.

One semantic blocker remains in the DIMM identity spelling.

The production roster drops the authority's J prefix

mt_collins_slot_roster currently mints labels with to_string(connector), so the accepted roster is "1" through "32". The Getting Started Guide authority it consumes declares dimm_figure_connector_label_prefix = "J", describes these as J-number keys, and the existing gunbc.mt_collins_dimm_physical_identity authority derives the diagram label as J + connector number.

That means an operator receipt using the machine vocabulary already used throughout this investigation—J1, J17, J25, etc.—will refuse as ReceiptDimmUnknownSlot, while the non-authoritative bare spelling "25" is admitted. The current real-roster witness bakes in that wrong spelling.

Please construct each roster label through the existing prefix authority and update the controls. Required discrimination:

J25 on socket 1  -> admitted
25                -> ReceiptDimmUnknownSlot
J25 on socket 0  -> ReceiptDimmOnWrongSocket
omitted J25       -> ReceiptDimmSlotOmitted

Ruling on the unexecuted srv1 control

The missing real-host execution does not block B1 by itself. The model establishes the desired directory grant, and runtime remains fail-closed: until grant convergence creates the directory, the create-once CAS yields a typed store refusal before the BMC baseline and before actuation. This PR does not claim that the directory has already been observed on srv1.

It is, however, presently an owed wet execution stated in prose, not a completed control and not a typed frontier row. Keep it explicitly open; a countable wet-execution/frontier row would be preferable, but I am not making that a second blocker while mtcollins1's BMC is unavailable.

Exact-head required CI is green. GitHub currently reports the branch as needing reconciliation; that is operational, separate from the label blocker above.

…5 controls

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 6fdab372cc438900e0a272212ed69919f46de7ff.

The remaining DIMM-identity blocker is closed. mt_collins_slot_roster now derives every label through dimm_figure_connector_label_prefix plus the connector number, matching both the Getting Started Guide authority and gunbc.mt_collins_dimm_physical_identity; the production roster is therefore J1–J32, not bare numerals.

The controls discriminate the intended wall on the real validation path:

  • J25 on socket 1 is admitted;
  • bare 25 is ReceiptDimmUnknownSlot;
  • J25 on socket 0 is ReceiptDimmOnWrongSocket;
  • omitting J25 is ReceiptDimmSlotOmitted;
  • the full roster establishes 32 connectors, J16 on socket 0, J25 on socket 1, and no bare 25 identity.

The five earlier findings remain repaired: clearance precedes all BMC baseline reads, receipt bytes are the regular tracked blob at the bound revision with SHA-256 evidence, canonical UTC validation is reused, the attempt namespace has a provisioned desired state, and CPU/DIMM populations join exactly.

The unexecuted srv1 control remains an explicit wet obligation, not a claimed receipt. It is non-blocking here because the production path fails closed with a typed store refusal before baseline reads or actuation until grant convergence creates the directory.

No semantic blocker remains. Exact-head required CI is queued; land after it is green.

@gunbai-bot
gunbai-bot Bot added this pull request to the merge queue Oct 4, 2026
Merged via the queue into main with commit e885d93 Oct 4, 2026
4 checks passed
@gunbai-bot
gunbai-bot Bot deleted the session/calm-lynx-884-slice-b branch October 4, 2026 18:55
gunbai-bot Bot pushed a commit that referenced this pull request Oct 4, 2026
… actuation takes AdmittedBootSubject

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