Skip to content

orchestrator: Confirm or revert a device's trial image - #586

Merged
chrysh merged 4 commits into
OpenPRoT:ocp-global-demo-wipfrom
9elements:wire-trial-boot
Oct 20, 2026
Merged

chrysh merged 4 commits into
OpenPRoT:ocp-global-demo-wipfrom
9elements:wire-trial-boot

Conversation

@chrysh

@chrysh chrysh commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

DeviceTrialBoot merged as a trait (#460) and nothing ever wired it: no
BoardCapabilities slot, no implementor, no call site. So an image a device
was given stayed on trial forever, whatever its boot did, and SOW P3's "ensure
known bootable partition" rested on the trait existing rather than on anything
running.

The gate is the eRoT's own ConfirmSelfTrial pattern, one chain position out.
Effect::ConfirmDeviceTrial is emitted where the commit window closes on
proof, which is the pending_commit guard in Ready's BootConfirmed arm,
the guard #507 added so a sibling's boot cannot consume another's window.
Effect::RevertDeviceTrial is emitted wherever that window closes on
suspicion instead, which is exactly the two places pending_commit is voided
without a boot behind it: a newer update superseding the image, and recovery
putting the running one under suspicion. Taking the id with take() rather
than clearing it means the emit and the void cannot drift apart.

BoardCapabilities::DeviceTrial is the seam. () is the no-op for a device
that keeps its own trial record, which is the usual case under
SvnFloorBinding::SelfManaged, where the eRoT moves no slot metadata.

What proves it

full_update closes the trial both ways, with exact counts rather than "at
least one": one confirm and no reverts when the device boots what it was
given, one revert and no confirms when it takes the update and never comes
back, neither when no update is ever offered. Exact because a no-op ()
behind the seam would sail through a looser check.

Four state-machine tests cover the arcs the scenarios cannot reach cheaply: a
proven boot confirms only the component that booted, a superseding update
reverts the image it replaces, recovery reverts an uncommitted trial, and an
update aimed at a sibling closes whatever trial was open. That last one is the
cross-component case, since the commit window is one and not one per
component.

Worth a look in review

is_pending has no caller. It exists for an orchestrator that rebooted
mid-update and has to read a device's open trial from stored state. Settling
that is the downstream twin of the eRoT's unclaimed-trial question and is
undecided the same way, so it is in GAPS.md rather than guessed at here.

The Sink capacity derivation in sm/src/sink.rs gained a paragraph. The
bound is unchanged and still has slack, but it is derived by enumerating
emission sites, and this adds three, so the comment now says why none of them
raises the worst case.

A sibling's update is now refused while a trial is open

Second commit, a behaviour change worth its own look. Ready accepted every
UpdateRequest, and Updating entry closes an open trial on the way in. So a
device that had activated an image and was still proving it lost that image
the moment any other component was offered an update: the trial reverted, the
BootConfirmed that followed found no window to close, and the device ran an
image its slot metadata calls unconfirmed. The next reset boots the old one.
A device slow to confirm is ordinary, so the race needed nothing to go wrong.

Ready now answers Super for a request naming a different component, which
falls through to the arm that already refuses an update the platform is too
busy to take. Superseding the open trial's own image stays allowed.

Two things reviewers should weigh:

  • A PLDM source now sees a refusal from Ready where it previously saw
    acceptance, so ReportUpdateDeferred reaches sources in a state it never
    did before.
  • One stage later the same shape survives and is not fixed here: after
    BootConfirmed and before the agent's 0x22, a sibling's update voids the
    outstanding floor advance, because Updating entry clears
    awaiting_floor_advance unconditionally. orchestrator: Move the SVN floor advance to the update agent's ask #568 sanctioned that for a
    same-target update, where the SVN belongs to the image being replaced; for
    a sibling the rationale does not hold. Recorded as a question rather
    than widened into this PR, and it touches orchestrator: Commit the eRoT's own floor through the same effect #581.

Tested: bazel test --config=virt_ast10x0 //target/ast10x0/tests/integration/...
passes, bazel test //services/... passes, ./pw presubmit is
green, and the commit builds standalone.

@chrysh
chrysh force-pushed the wire-trial-boot branch 6 times, most recently from aa93d43 to 3562f2f Compare October 20, 2026 11:00
@chrysh chrysh changed the title orchestrator: Wire the device trial gate and close it both ways orchestrator: Confirm or revert a device's trial image Oct 20, 2026
@chrysh
chrysh force-pushed the wire-trial-boot branch 6 times, most recently from a2f6d41 to 0bc41d3 Compare October 20, 2026 18:46
`DeviceTrialBoot` merged as a trait and nothing ever wired it: no
`BoardCapabilities` slot, no implementor, no call site. So a test image a
device was given stayed on trial forever, whatever the boot did, and the
SOW P3 row "ensure known bootable partition" rested on the trait existing.

The gate follows the `ConfirmSelfTrial` pattern the eRoT uses for itself,
one chain position out. `Effect::ConfirmDeviceTrial` fires where the
commit window closes on a successful boot, which is the `pending_commit`
guard in the `BootConfirmed` arm of `Ready`. `Effect::RevertDeviceTrial`
fires at the two sites that close the window with no boot behind it: a
newer update superseding the image, and recovery putting the running
image under suspicion. Both sites use `pending_commit.take()`, so the
component id reaches the effect in the same statement that clears the
field.

The board picks an implementor through `BoardCapabilities::DeviceTrial`.
A device that keeps its own trial record binds `()`, the usual case under
`SvnFloorBinding::SelfManaged`.

`full_update` covers confirm and revert with exact counts: one confirm
and no reverts when the device boots what it was given, one revert and no
confirms when the device takes the update and never comes back, neither
when no update is ever offered. An `assert!(confirms > 0)` would also pass
with `()` behind the seam.

`is_pending` has no caller: settling a trial left open by an orchestrator
that rebooted mid-update is undecided, the same way it is undecided for
an unclaimed image of the eRoT itself. In GAPS.md.

Assisted-by: Claude Opus 5
Signed-off-by: Christina Quast <christina.quast@9elements.com>
`Ready` accepted every `UpdateRequest`, and entry to `Updating` closes an
open trial on the way in. So a device that had activated a test image and
had not confirmed it lost that image as soon as any other component was
offered an update: the trial reverted, and the `BootConfirmed` arriving
afterwards found no window to close, leaving the device running an image
the slot metadata calls unconfirmed. The next reset boots the old image.
A device may confirm late, with nothing failing anywhere.

`Ready` now answers `Super` for a request naming a component other than
the one on trial, which falls through to the arm that already refuses an
update the platform is too busy to take. The source gets
`ReportUpdateDeferred` and the trial is untouched.

Superseding the image under trial stays allowed: the update replaces that
image, so the trial closes without keeping it.

One test asserted the opposite. `an_update_for_a_sibling_reverts_the_open_trial`
asserted the behaviour removed here, and is replaced by
`an_update_for_another_component_is_refused_while_a_trial_is_open`.

Assisted-by: Claude Opus 5
Signed-off-by: Christina Quast <christina.quast@9elements.com>
The components form a chain with depends_on edges, not a tree, so two of
them are not siblings in any structural sense. DSP0267 and the rest of the
tree say component.

Renames the three recovery re-walk tests and the boot-confirm window test,
and the prose in GAPS.md and orchestrator-model.md that reads the same way.
The sibling wording in the HAL and peripheral drivers means the analogous
other port and stays.

Assisted-by: Claude Opus 5
Signed-off-by: Christina Quast <christina.quast@9elements.com>
The orchestrator timer already uses arm and disarm in the watchdog sense.
What revert does to a device is clear the marker that makes the next reset
boot the test image.

Assisted-by: Claude Opus 5
Signed-off-by: Christina Quast <christina.quast@9elements.com>
@chrysh
chrysh merged commit 715c28d into OpenPRoT:ocp-global-demo-wip Oct 20, 2026
3 of 4 checks passed
@chrysh
chrysh deleted the wire-trial-boot branch October 20, 2026 18:55
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