fwmanager: table-declared boot checkpoints replace BootMonitor - #397
fwmanager: table-declared boot checkpoints replace BootMonitor#397chrysh wants to merge 13 commits into
Conversation
There was a problem hiding this comment.
The CommitPolicy type models a redundant setting. It asks "must this device attest before its update commits?" — but whether a device can attest is already decided by if it is an Active or Passive device (the orchestrator-sm::ComponentKind).
The CSA defines only the iRoT-vs-symbiont distinction and lets attestation follow from it; CommitPolicy adds a separate attestation setting the CSA never defines, so it contradicts the source architecture it's meant to reflect.
This violates DRY (don't repeat yourself).
There was a problem hiding this comment.
Agreed. Dropped in 1eab413. Commit gating follows from ComponentKind (passive or active).
| /// Intentionally exhaustive (not `#[non_exhaustive]`): adding a verdict is | ||
| /// a breaking change, so the compiler forces every consumer — in particular | ||
| /// the orchestrator's event mapping — to handle it explicitly. | ||
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
There was a problem hiding this comment.
Retry & terminal decisions belong to the Orchestrator State Machine
WalkVerdict's retries_left, Retry, and Dead re-implement what orchestrator-sm already owns: ComponentStatus.retry/max_retry and the Recovering→RecoveryFailed path. uplicating the retry count means one side can think retries remain while the other says it's exhausted.
There was a problem hiding this comment.
Agreed, a retry re-resets the device, which restarts the whole walk — you can't retry "kernel" without re-passing bl1 and bl2 — so retry budgets are inherently per boot attempt, and boot attempts are owned by the orchestrator state machine. Fixed in e550a20: WalkVerdict is observation-only now (Waiting / Complete / Failed{checkpoint, cause}), retries_left/Retry/Dead are gone, and max_retries left the device table. The device's own judgment is still signalled by FailureCause::{TimedOut, DeviceRetriable, DeviceFatal}. DeviceFatal says no remaining budget can overturn the verdict.
|
@rusty1968 three design notes worth discussing: Timeouts stay per-checkpoint, and the SM never sees them. The device table declares a window per checkpoint (bl1 can get 200 ms while the service checkpoint gets 30 s); the boot walk consumes those windows and reports expiry as a failed attempt. I think Lou mentioned something about per checkpoint timeouts. Open question 1 — Open question 2 — where does |
There was a problem hiding this comment.
Do we really need an entirely new file for one enum definition? Or are we planning to extend this in the near future?
| @@ -92,15 +85,44 @@ pub const fn validate<R, G>(devices: &[DeviceConfig<R, G>]) { | |||
There was a problem hiding this comment.
Can we check this already in the Checkpoint creation?
5dc453a to
4d6e72b
Compare
…hecks A BootCheckpoint is timing policy plus its own evidence check: a capture-less fn handed the board's device context, so the channel underneath never leaks past the check and an unobservable checkpoint is unrepresentable. config.rs defines the schema (BootSignal is gone); the board table declares the checkpoints against its own context and error types. BootStatus stays as the shared vocabulary and absorbs the latch-cleared-by-reset contract; GpioBootMonitor keeps its behavior as a plain reader. BootWatch/WalkVerdict is the erased seam the orchestrator polls — timeout and retry-budget judgment lands with the walker that implements it. Assisted-by: Claude:claude-fable-5 Signed-off-by: Christina Quast <christina.quast@9elements.com>
…al ids The embedded fn was the more general shape, but the generality went unused while its costs did not: the table stopped being pure data (unprintable, unvalidatable on mechanisms, never generatable), every check shared one &mut board context, and dispatch went indirect. A signal id is the same check defunctionalized: data in the table, an exhaustive match in the board's EvidenceReader — typically one per device, so each walk borrows only its own reader. Boot-evidence mechanisms per board are a closed set; when one can't be named, that is a new variant in that board's enum, not an API change. Assisted-by: Claude:claude-fable-5 Signed-off-by: Christina Quast <christina.quast@9elements.com>
…ence A device that knows it failed should end the wait early, and one that knows a retry is pointless should say so, instead of the orchestrator burning its window and budget to find out. BootStatus::Failed splits into FailedRetriable (consumes budget immediately) and FailedFatal (ends the boot regardless of budget). Timeouts stay the orchestrator's own judgment — hung devices report nothing — and channel trouble stays in the reader's Error, distinct from a device-reported verdict. Assisted-by: Claude:claude-fable-5 Signed-off-by: Christina Quast <christina.quast@9elements.com>
A timeout is never on the wire: a hung endpoint reads Booting forever, and only the orchestrator's clock turns silence into a verdict. The message path carries the active verdicts (device failure codes) and channel trouble, each on its own channel. Assisted-by: Claude:claude-fable-5 Signed-off-by: Christina Quast <christina.quast@9elements.com>
Retry re-arms the window, but the caller had no way to know until when — it would have had to reach into the checkpoint's timeout and do the walker's arithmetic itself. Retry now carries deadline_millis exactly like Waiting: one scheduling rule for both verdicts. Assisted-by: Claude:claude-fable-5 Signed-off-by: Christina Quast <christina.quast@9elements.com>
Adapter crates cannot implement EvidenceReader themselves — a board's signal vocabulary G is not theirs to know. Show the intended shape on the trait: the board impl owns the match, the hardware binding is made once at construction, the signal id proves the right reader was wired. Assisted-by: Claude:claude-fable-5 Signed-off-by: Christina Quast <christina.quast@9elements.com>
…aning Failure reports identify a checkpoint by name, so a duplicate within a device would make them ambiguous — validate now rejects it at build time (str comparison by hand: == on &str is not const). Also state explicitly that max_retries=0 means the one attempt is all the device gets. Assisted-by: Claude:claude-fable-5 Signed-off-by: Christina Quast <christina.quast@9elements.com>
The signal field now says on the spot why it is an id and who resolves it, and validate points at the mock table, which demonstrates the board-local const fence for checks the generic validate cannot do (gpio line within the bank). Assisted-by: Claude:claude-fable-5 Signed-off-by: Christina Quast <christina.quast@9elements.com>
Whether a device must attest before its update commits follows from what kind of device it is (iRoT-backed or symbiont — the orchestrator's ComponentKind); the CSA defines only that distinction. A second table knob could only agree with the kind or contradict it. Assisted-by: Claude:claude-fable-5 Signed-off-by: Christina Quast <christina.quast@9elements.com>
WalkVerdict now reports observation only: Failed{checkpoint, cause}
replaces Retry/Dead/retries_left — the state machine's
ComponentStatus.retry and Recovering→RecoveryFailed path already own
those decisions, and a second counter could only agree or disagree
with the first. max_retries leaves the table for the same reason: a
retry re-resets the device and re-runs the whole walk, so budgets are
per boot attempt, owned where boot attempts are owned. The device's
own judgment still flows up as FailureCause::{TimedOut,
DeviceRetriable, DeviceFatal} — the one input the retry decision
needs.
Assisted-by: Claude:claude-fable-5
Signed-off-by: Christina Quast <christina.quast@9elements.com>
Pin three facts the docs left implicit: checkpoint timeouts are table data the walk consumes — the clockless state machine never sees a duration, a component's boot timeout is just its walk over the windows; the device table is the authority the chain is built from; and Complete maps to ComponentReady or Booted by component kind, in the shell. Assisted-by: Claude:claude-fable-5 Signed-off-by: Christina Quast <christina.quast@9elements.com>
Every schema check is per-device, so the constructors can run them all. BootCheckpoint::new and DeviceConfig::new are const fn -- board tables still build in const context, so a bad table is still a build error -- but the fields are private now, and a checkpoint or device entry that violates the schema cannot be constructed at all. The free validate() is gone with the loophole it carried: it had to be remembered, and a board table that dropped the const fence compiled fine while broken. Construction is the one gate every entry passes. Board-local checks keep the const-fence pattern (validate_signals in the mock table), reading through the new accessors. Assisted-by: Claude:claude-fable-5 Signed-off-by: Christina Quast <christina.quast@9elements.com>
Within the crate only EvidenceReader consumes BootStatus — it is the trait's return vocabulary — so the enum does not earn a module of its own. The crate-root re-export is unchanged; no import anywhere moves. Assisted-by: Claude:claude-fable-5 Signed-off-by: Christina Quast <christina.quast@9elements.com>
Traits and schema only — the walker implementing them follows in a separate PR, so this one stays reviewable on the contract alone.
What
BootMonitoris removed. ABootCheckpointnames its evidence as a board-defined signal id (signal: G, timeout, retry budget) — the schema names no signal kinds, and everything that is config is declared in the board device table (target/mock/devices.rs);config.rsonly defines what that configuration has to look like.EvidenceReader<G>resolves a signal id toBootStatus— board wiring, typically one per managed device, exhaustive match, direct dispatch.BootStatusgrows device-reported verdicts:FailedRetriable(consumes retry budget immediately instead of waiting out the window) andFailedFatal(ends the boot regardless of budget). Timeouts stay the observer's own judgment — hung devices report nothing; the reader tests pin this down with an MCTP-shaped mock where silence readsBootingforever.BootWatch/WalkVerdictis the erased seam the orchestrator polls:Waiting/Complete/Retry/Deadper device, free of device and error types.GpioBootMonitorkeeps its behavior as a plain reader (inherent method, no trait).Reading order
The first commit is the checkpoint-embedded-
fnvariant; the second defunctionalizes it into signal ids and records why (table stays pure data, borrows stay per-device, dispatch stays direct). The pair is kept deliberately as the design record — review the combined diff if you only want the endpoint. Commits three and four add the failure verdicts and the message-path reader tests.Supersedes
BootWalk/MonitorMapoverBootMonitor.await_bootwill need rework once this lands.Test
bazel test //services/fwmanager/...(13 api tests),bazel build //target/mock/...; device-tablevalidateruns in const context, so a bad table is a build error.Migration (for integrators)
BootMonitoris replaced byEvidenceReader<G>+BootStatus; the checkpoint schema changed shape:max_retries: 0= the single attempt is all the device gets.BootStatus::Failedsplit intoFailedRetriable/FailedFatal(device-reported verdicts; timeouts remain the observer's judgment).validate).WalkVerdict::Retrycarriesdeadline_millisof the re-armed window — schedule against it exactly likeWaiting.