carlos / platform Public

host: root refuses publication at a revert's probe and a clean return's probe (Task 7b fix round 2, P2-1)

Neither row journals a render under a rule root checks, and the journal is
agent-written, so a missing journal was the only refusal and a compromised
agent could plant one. publishVersion now refuses KindReverting outright
and a restoring probe when the return is not dirty, until those probes
journal their own render (RenderHeldUnderBefore at an equivalent point)
under a rule root checks. Regression with a planted journal for each row;
the round-1 test now expects root's refusal (the agent's adapter still
stops at not-yet).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev a8c99b22b0ff5fdee0e9cfe9a6e2347b69fed128 parent 2157985
3 files changed, +77 −16
  • internal/host/movesheldrelease_agent_test.go +8 −5
  • internal/host/movesheldrelease_root.go +15 −11
  • internal/host/movesheldrelease_test.go +54 −0
diff --git a/internal/host/movesheldrelease_agent_test.go b/internal/host/movesheldrelease_agent_test.go
index da744acb3..96795b7bf 100644
--- a/internal/host/movesheldrelease_agent_test.go
+++ b/internal/host/movesheldrelease_agent_test.go
@@ -325,8 +325,8 @@ func TestHeldReleaseAgentVerifiesRootsStage(t *testing.T) {
// run configured, so nothing journals a render hash under their hold op.
// With the recovery release missing from live/<host>, each fails closed: the
// probe's release adapter gets a typed not-yet and asks root nothing, root
-// asked directly answers not-yet before any selection, and nothing is
-// published. A known limitation: such a probe would need to journal its own
+// asked directly refuses the row outright (fix round 2, P2-1), and nothing
+// is published. A known limitation: such a probe would need to journal its own
// render hash through RenderHeldUnderBefore at an equivalent point.
func TestHeldReleaseRecoveryProbesFailClosed(t *testing.T) {
ctx := context.Background()
@@ -372,13 +372,16 @@ func TestHeldReleaseRecoveryProbesFailClosed(t *testing.T) {
if len(seen) != 0 {
t.Fatalf("root was asked %+v", seen)
}
- // Root asked directly, as a request carrying a render hash would.
+ // Root asked directly, as a request carrying a render hash would:
+ // since fix round 2 (P2-1) root refuses these rows outright,
+ // before any journal read (TestHeldReleaseRootRefusesUnjournalledRecoveryProbes
+ // covers a planted journal).
q := f.request(heldReleasePublish)
if q.HoldOp != tc.hold {
t.Fatalf("request hold op %q, want %q", q.HoldOp, tc.hold)
}
- if _, err := f.root.Serve(ctx, hrPeer, q); !errors.Is(err, errHeldReleaseNotYet) {
- t.Fatalf("root: %v, want errHeldReleaseNotYet", err)
+ if _, err := f.root.Serve(ctx, hrPeer, q); !errors.Is(err, release.ErrStaleHeldStage) {
+ t.Fatalf("root: %v, want a refusal", err)
}
f.untouched(t)
})
diff --git a/internal/host/movesheldrelease_root.go b/internal/host/movesheldrelease_root.go
index 508d22b6c..82932d408 100644
--- a/internal/host/movesheldrelease_root.go
+++ b/internal/host/movesheldrelease_root.go
@@ -316,10 +316,17 @@ func heldReturnRelease(m *provision.MoveState) string {
// kind and phase, and "" with the reason when the claim grants none: a
// landing's configured or probing step, on this box as the executor, with
// the record at that landing phase (the pinned release); a dirty return's
-// configured step (claimed) or a return's probe (its recovery release, the
-// pin at configured when none is recorded yet); a revert's probe (its
-// recovery release). Anything else, a moving op included, publishes
-// nothing: the policy is never widened.
+// configured step (claimed) or its probe (its recovery release, the pin at
+// configured when none is recorded yet). Anything else, a moving op
+// included, publishes nothing: the policy is never widened.
+//
+// A revert's probe and a clean return's probe are refused outright (fix
+// round 2, P2-1, platform-1e's authority review). Neither runs configured,
+// so no render is journalled under a rule root checks, and the journal is
+// agent-written: its absence cannot be what refuses them, since a
+// compromised agent could plant one. They stay refused until those probes
+// journal their own render (RenderHeldUnderBefore at an equivalent point)
+// under a rule root checks.
func publishVersion(c *hostclaim.Claim, m *provision.MoveState, box string) (string, error) {
if c.Op.Exec != box {
return "", staleHeldStage("this box is not the executor")
@@ -341,6 +348,9 @@ func publishVersion(c *hostclaim.Claim, m *provision.MoveState, box string) (str
switch {
case c.Op.Phase == hostclaim.PhaseClaimed && rb.SourcePhase == provision.RbSourceConfigured && rb.Dirty:
case c.Op.Phase == hostclaim.PhaseProbing && rb.SourcePhase == provision.RbSourceProbing:
+ if !rb.Dirty {
+ return "", staleHeldStage("a clean return's probe journals no render root checks, so root publishes nothing for it")
+ }
if rb.RecoveryInputs == nil || rb.RecoveryInputs.Release == "" {
return "", staleHeldStage("no recorded recovery release")
}
@@ -352,13 +362,7 @@ func publishVersion(c *hostclaim.Claim, m *provision.MoveState, box string) (str
}
return "", staleHeldStage("no recorded recovery release")
case hostclaim.KindReverting:
- if c.Op.Phase != hostclaim.PhaseProbing || c.Op.Journal[hostclaim.JournalRevPhase] != hostclaim.PhaseProbing || m.State != provision.MoveReverting {
- return "", staleHeldStage("a revert at %q publishes nothing", c.Op.Phase)
- }
- if m.RecoveryInputs == nil || m.RecoveryInputs.Release == "" {
- return "", staleHeldStage("no recorded recovery release")
- }
- return m.RecoveryInputs.Release, nil
+ return "", staleHeldStage("a revert's probe journals no render root checks, so root publishes nothing for it")
}
return "", staleHeldStage("a %s op publishes nothing", c.Op.Kind)
}
diff --git a/internal/host/movesheldrelease_test.go b/internal/host/movesheldrelease_test.go
index b74852277..49cad8df6 100644
--- a/internal/host/movesheldrelease_test.go
+++ b/internal/host/movesheldrelease_test.go
@@ -727,3 +727,57 @@ func TestHeldReleaseRouteIsMovesOnly(t *testing.T) {
t.Fatalf("moves off wired %#v", got)
}
}
+
+// TestHeldReleaseRootRefusesUnjournalledRecoveryProbes (fix round 2, P2-1,
+// platform-1e's authority review): a revert's probe and a clean return's
+// probe journal no render under any rule root checks, so their absent
+// journal cannot be what refuses them: the journal is agent-written. Root
+// refuses both rows outright, even with a planted journal and a matching
+// request, and publishes nothing. A dirty return's probe, whose configured
+// step journalled its render, still publishes.
+func TestHeldReleaseRootRefusesUnjournalledRecoveryProbes(t *testing.T) {
+ ctx := context.Background()
+ recovery := func(f *hrFixture) {
+ f.claim.Op.Exec, f.claim.Op.Participants = "uk-box", []string{"uk-box", "ie-box"}
+ f.claim.Op.Intent = map[string]string{"source": "uk-box", "target": "ie-box", "to": "ie"}
+ f.claim.Op.Phase = hostclaim.PhaseProbing
+ f.rec.Move.From, f.rec.Move.To, f.rec.Move.LandingPhase = "uk", "ie", ""
+ }
+ for _, tc := range []struct {
+ name string
+ set func(f *hrFixture)
+ hold string
+ }{
+ {"a revert's probe", func(f *hrFixture) {
+ recovery(f)
+ f.claim.Op.Kind = hostclaim.KindReverting
+ f.claim.Op.Journal = map[string]string{hostclaim.JournalRevPhase: hostclaim.PhaseProbing}
+ f.rec.Move.State = provision.MoveReverting
+ f.rec.Move.RecoveryInputs = &provision.MoveInputs{Release: "v7"}
+ }, p1ID},
+ {"a clean return's probe", func(f *hrFixture) {
+ recovery(f)
+ f.claim.Op.Kind = hostclaim.KindRestoring
+ f.claim.Op.Intent["rollback"] = hrRB
+ f.rec.Move.State = provision.MoveRollingBack
+ f.rec.Move.Rollback = &provision.MoveRollback{ID: hrRB, SourcePhase: provision.RbSourceProbing, RecoveryInputs: &provision.MoveInputs{Release: "v7"}}
+ f.row.HoldOp = hrRB
+ }, hrRB},
+ } {
+ t.Run(tc.name+" with a planted journal", func(t *testing.T) {
+ f := newHRFixture(t)
+ f.set(tc.set)
+ // A compromised agent plants the render it then sends.
+ f.journal(t, tc.hold, hrRendered)
+ q := f.request(heldReleasePublish)
+ if q.HoldOp != tc.hold {
+ t.Fatalf("request hold op %q, want %q", q.HoldOp, tc.hold)
+ }
+ _, err := f.root.Serve(ctx, hrPeer, q)
+ if !errors.Is(err, release.ErrStaleHeldStage) {
+ t.Fatalf("publish: %v, want a refusal", err)
+ }
+ f.untouched(t)
+ })
+ }
+}