orchestrator: Device table is the single source of the chain of trust - #26
Draft
chrysh wants to merge 16 commits into
Draft
orchestrator: Device table is the single source of the chain of trust#26chrysh wants to merge 16 commits into
chrysh wants to merge 16 commits into
Conversation
…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>
Kind and failure policy are per-device board policy, so their vocabulary belongs to the device-table schema crate, where boards declare the rest of their boot behavior; the state machine depends on the schema crate and re-exports both types, keeping the reducer's API unchanged while giving the classification a single home next to the table that will declare it. Assisted-by: Claude:claude-fable-5
One fact, one home: what a device is (ComponentKind), what its failure means for the platform (FailurePolicy), and which earlier device it cascades with (depends_on, by name) are board facts, so they belong in the board's device table next to its reset wiring and checkpoints. The new const DeviceTable wrapper is the single place cross-entry invariants are checked — non-empty, within the orchestrator's cursor bound, unique device names, and every dependency naming a strictly earlier entry — so an invalid table is unconstructible and fails the build; per-entry constructors cannot see the whole table, which is why the wrapper owns these checks. The mock board declares its archetypes accordingly: the direct-flash bmc is Passive and Required, the self-updating nic is Active and Isolable. Assisted-by: Claude:claude-fable-5
Holding a DeviceTable is proof of the chain invariants — its const constructor already rejected every malformed table at build time — so Chain::from_table is infallible and ChainError with its TryFrom impl, a second copy of the same rules that could drift into disagreement, is deleted; each invariant now has exactly one checker and the type system carries the proof across the crate boundary. The conversion derives everything: table index becomes the component id (declaration order is walk order), kind and policy are copied, and depends_on names resolve to the ids of their earlier entries. The mock board derives its chain capacity and effect-buffer size from the table and declares the one underivable board fact, the retry budget; its new host test drives table, chain, and orchestrator end to end, checking that the passive bmc release advances the walk speculatively. Reducer tests keep building ad-hoc chains through a cfg(test)-only constructor, so no unchecked door exists in production builds. Assisted-by: Claude:claude-fable-5
Collaborator
Author
|
Same work as OpenPRoT#404 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR devices.rs config addition to provide chain information to orchestrator-sm
Stacked on
add-boot-walk(PR OpenPRoT#397's branch).Makes the board device table the single source of truth for the orchestrator's chain of trust, and gives every invariant exactly one checker:
ComponentKind/FailurePolicymove intoorchestrator-config; the SM re-exports them (API unchanged).DeviceConfigdeclares kind, failure policy, anddepends_on(by device name); the new constDeviceTablewrapper is the only place cross-entry invariants are checked, so an invalid table fails the build.Chain::from_tablederives the chain from the table (index = id, declaration order = walk order) and is infallible — holding aDeviceTableis the proof — soChainErrorand theTryFromimpl are deleted.DEVICE_COUNT/EFFECT_CAPfrom its table, declaresMAX_RETRY, and gains a host test driving table → chain → orchestrator end to end.https://claude.ai/code/session_014on9N5gJLKpARXtNh4Wzib