carlos / platform Public

moves: M11, a revert's render, release and spawn bind its recorded inputs (Q11)

MovesClaimPolicy's reverting row now binds the held config, the held
release and the spawn to Move.RecoveryInputs, which the revert's probe
records before it renders (MoveRecordRevertInputs), as restoring binds
Rollback.RecoveryInputs. Nothing recorded refuses; the move's pins never
stand in. TestMovesClaimPolicyRevertBindsRecordedInputs, and the earlier
by-shape revert cells now expect the binding.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 800205565adc9cc7bb9a6f72cbba094f58b1d645 parent 76eb992
2 files changed, +63 −28
  • internal/host/movespolicy.go +9 −10
  • internal/host/movespolicy_test.go +54 −18
diff --git a/internal/host/movespolicy.go b/internal/host/movespolicy.go
index 0d0df30fb..8eed6d740 100644
--- a/internal/host/movespolicy.go
+++ b/internal/host/movespolicy.go
@@ -88,9 +88,10 @@ import (
// they are recorded the render is granted on the rows above: configured
// is the render the record takes them from, and a clean return renders
// before its probe with no configured. A dirty return's probe, and every
-// spawn, need them recorded. A revert records none: its render, its
-// release and its launch are bound by shape only (a revert inputs field
-// is carried to Tasks 6 and 9).
+// spawn, need them recorded. A revert's are Move.RecoveryInputs, which
+// its probe records before it renders (MoveRecordRevertInputs; M11,
+// Q11): its render, its release and its launch need them recorded and
+// are bound to them, never to Move.Inputs.
//
// The target's cleaning (ReplaceTimers(host, nil) at cleaning) and the
// source's revert close are not rows here: their callers use plain registry
@@ -465,6 +466,7 @@ func movesRecoveryPolicy(ctx context.Context, read MovesRecordReader, c *hostcla
return refuse("the record is not this revert", nil)
}
row, ok = revertingRights[c.Op.Journal[hostclaim.JournalRevPhase]]
+ recovery = rec.Move.RecoveryInputs
}
if !ok || row.claimPhase != c.Op.Phase || !row.actions[s.Action] {
return refuse("the phase does not permit the action", nil)
@@ -481,17 +483,14 @@ func movesRecoveryPolicy(ctx context.Context, read MovesRecordReader, c *hostcla
if !recoveryIdentityFits(s.Action, id) {
return refuse("the identity does not fit the action", nil)
}
- if rb == nil {
- // A revert records no recovery inputs: its render, its release and
- // its launch are bound by shape, never by Move.Inputs.
- return nil
- }
switch s.Action {
case movesActionHeldConfig, movesActionHeldRelease:
if recovery == nil {
// configured renders the inputs its closing CAS records, and a
- // clean return's render before its probe precedes the record.
- if rb.SourcePhase == provision.RbSourceConfigured || !rb.Dirty {
+ // clean return's render before its probe precedes the record. A
+ // revert's probe records before it renders (M11), so a revert
+ // has no render before the record.
+ if rb != nil && (rb.SourcePhase == provision.RbSourceConfigured || !rb.Dirty) {
return nil
}
return refuse("no recorded recovery inputs", nil)
diff --git a/internal/host/movespolicy_test.go b/internal/host/movespolicy_test.go
index 4cbcc731d..56d460c49 100644
--- a/internal/host/movespolicy_test.go
+++ b/internal/host/movespolicy_test.go
@@ -749,10 +749,11 @@ func rpRestoringRecord(phase string) *provision.Record {
}
// rpRevertingRecord is the revert's record: reverting, the region still the
-// source's.
+// source's, with the inputs its probe recorded (MoveRecordRevertInputs).
func rpRevertingRecord() *provision.Record {
rec := mpRecord()
rec.Region, rec.Move.State, rec.Move.LandingPhase = "ie", provision.MoveReverting, ""
+ rec.Move.RecoveryInputs = rpRecovery()
return rec
}
@@ -908,6 +909,43 @@ func TestMovesClaimPolicyRevertingRows(t *testing.T) {
})
}
+// M11 (Q11): a revert's held render, held release and spawn are bound to the
+// inputs its probe recorded before rendering them (Move.RecoveryInputs,
+// MoveRecordRevertInputs at probing), as a rollback's are bound to
+// Rollback.RecoveryInputs: never to the move's pins, never by shape alone,
+// and nothing before the record.
+func TestMovesClaimPolicyRevertBindsRecordedInputs(t *testing.T) {
+ recovered, pinned := rpRecovery(), mpPins()
+ c := rpRevertingClaim(hostclaim.PhaseProbing)
+ rec := rpRevertingRecord()
+ moves := MovesClaimPolicy((&mpReader{rec: rec}).read)
+ try := func(action string, id hostwrite.Identity) error {
+ return moves(t.Context(), c, rpSelection(c, action), id)
+ }
+ cfgOf := func(in *provision.MoveInputs) hostwrite.Identity {
+ return HeldConfigIdentity(in.BundleETags, in.OverlayETag)
+ }
+ for action, id := range map[string]hostwrite.Identity{"held-config": cfgOf(recovered), "held-release": {Release: recovered.Release}, "spawn": rpLaunch(recovered)} {
+ if err := try(action, id); err != nil {
+ t.Fatalf("%s with the recorded inputs: %v", action, err)
+ }
+ }
+ rpRefused(t, try("held-config", cfgOf(pinned)), "the config is not the recovery's")
+ rpRefused(t, try("held-release", hostwrite.Identity{Release: pinned.Release}), "the release is not the recovery's")
+ rpRefused(t, try("spawn", rpLaunch(pinned)), "the launch is not the recovery's")
+ rpRefused(t, try("spawn", hostwrite.Identity{Config: cfgOf(recovered).Config, Release: pinned.Release}), "the launch is not the recovery's")
+
+ // Nothing recorded: the probe records before it renders, so every row
+ // refuses; the move's pins never stand in.
+ rec.Move.RecoveryInputs = nil
+ for action, id := range map[string]hostwrite.Identity{"held-config": cfgOf(pinned), "held-release": {Release: pinned.Release}, "spawn": rpLaunch(pinned)} {
+ rpRefused(t, try(action, id), "no recorded recovery inputs")
+ }
+ rec.Move.RecoveryInputs = &provision.MoveInputs{BundleETags: recovered.BundleETags, OverlayETag: recovered.OverlayETag}
+ rpRefused(t, try("held-release", hostwrite.Identity{Release: recovered.Release}), "no recorded recovery release")
+ rpRefused(t, try("spawn", rpLaunch(recovered)), "no recorded recovery release")
+}
+
// Only the claim op's executor, the move's source, in the source's region,
// for this rollback's record: the target (the cleaning's ReplaceTimers
// included, which is a plain registry call and never a policy row), a third
@@ -1171,26 +1209,25 @@ func TestMovesClaimPolicyBindsRecoveryInputs(t *testing.T) {
}
rpRefused(t, try(provision.RbSourceProbing, "held-config", rec, cfgOf(pinned)), "the config is not the recovery's")
})
- t.Run("reverting: never the pins", func(t *testing.T) {
+ t.Run("reverting: the recorded inputs, never the pins (M11)", func(t *testing.T) {
moves := MovesClaimPolicy((&mpReader{rec: rpRevertingRecord()}).read)
c := rpRevertingClaim(hostclaim.PhaseProbing)
- for _, in := range []*provision.MoveInputs{recovered, pinned} {
- if err := moves(t.Context(), c, rpSelection(c, "held-config"), cfgOf(in)); err != nil {
- t.Fatal(err)
- }
- if err := moves(t.Context(), c, rpSelection(c, "spawn"), rpLaunch(in)); err != nil {
- t.Fatal(err)
- }
+ if err := moves(t.Context(), c, rpSelection(c, "held-config"), cfgOf(recovered)); err != nil {
+ t.Fatal(err)
+ }
+ if err := moves(t.Context(), c, rpSelection(c, "spawn"), rpLaunch(recovered)); err != nil {
+ t.Fatal(err)
}
+ rpRefused(t, moves(t.Context(), c, rpSelection(c, "held-config"), cfgOf(pinned)), "the config is not the recovery's")
+ rpRefused(t, moves(t.Context(), c, rpSelection(c, "spawn"), rpLaunch(pinned)), "the launch is not the recovery's")
for _, id := range []hostwrite.Identity{{}, {Config: cfgOf(recovered).Config}, {Release: "v1.3.0"}} {
rpRefused(t, moves(t.Context(), c, rpSelection(c, "spawn"), id), "the identity does not fit the action")
}
rpRefused(t, moves(t.Context(), c, rpSelection(c, "held-config"), rpLaunch(recovered)), "the identity does not fit the action")
- for _, in := range []*provision.MoveInputs{recovered, pinned} {
- if err := moves(t.Context(), c, rpSelection(c, "held-release"), hostwrite.Identity{Release: in.Release}); err != nil {
- t.Fatal(err)
- }
+ if err := moves(t.Context(), c, rpSelection(c, "held-release"), hostwrite.Identity{Release: recovered.Release}); err != nil {
+ t.Fatal(err)
}
+ rpRefused(t, moves(t.Context(), c, rpSelection(c, "held-release"), hostwrite.Identity{Release: pinned.Release}), "the release is not the recovery's")
rpRefused(t, moves(t.Context(), c, rpSelection(c, "held-release"), rpLaunch(recovered)), "the identity does not fit the action")
})
t.Run("a landing: the same difference is refused", func(t *testing.T) {
@@ -1259,7 +1296,7 @@ func TestMovesClaimPolicyRestoringThroughPolicyCheck(t *testing.T) {
// The release is materialized before every recovery probe (§7.7; Task 3b
// fix round 2), through hostbox.Policy.Check with each recovery's own hold:
// held-release at probing is granted to the executing source, bound to the
-// recorded recovery inputs on a return and by shape on a revert, and
+// recorded recovery inputs on a return and on a revert (M11), and
// refused at another phase, to another box, and for a release that is not
// the recorded one.
func TestMovesClaimPolicyRecoveryReleaseAtProbing(t *testing.T) {
@@ -1320,11 +1357,10 @@ func TestMovesClaimPolicyRecoveryReleaseAtProbing(t *testing.T) {
c := rpRevertingClaim(hostclaim.PhaseProbing)
s := rpSelection(c, "held-release")
rec := rpRevertingRecord()
- for _, v := range []string{recovered.Release, pinned.Release} {
- if err := through(mpOp, rec, c, s, hostwrite.Identity{Release: v}); err != nil {
- t.Fatalf("the executor at probing, release %s: %v", v, err)
- }
+ if err := through(mpOp, rec, c, s, hostwrite.Identity{Release: recovered.Release}); err != nil {
+ t.Fatalf("the executor at probing: %v", err)
}
+ rpRefused(t, through(mpOp, rec, c, s, hostwrite.Identity{Release: pinned.Release}), "the release is not the recovery's")
rpRefused(t, through(mpOp, rec, c, s, hostwrite.Identity{Release: recovered.Release, Config: "x"}), "the identity does not fit the action")
for _, rev := range []string{hostclaim.RevPhaseEvidence, hostclaim.RevPhaseVerify, hostclaim.PhaseProbed, hostclaim.PhaseOpened} {
c := rpRevertingClaim(rev)