carlos / platform Public

host: root's publish receipt binds the action, kind and phase (Task 7b fix round 2, P3-1)

A landing's configured receipt answered its probing request as a replay
within the same holder tuple. The receipt now carries Action, Kind and
Phase, so only exactly the same request is a replay; the probe's runs the
whole path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 50e4d94be87f327934e5ff0b5256f7c1734377d9 parent a8c99b2
2 files changed, +33 −4
  • internal/host/movesheldrelease_root.go +10 −4
  • internal/host/movesheldrelease_test.go +23 −0
diff --git a/internal/host/movesheldrelease_root.go b/internal/host/movesheldrelease_root.go
index 82932d408..6e59057a7 100644
--- a/internal/host/movesheldrelease_root.go
+++ b/internal/host/movesheldrelease_root.go
@@ -449,9 +449,14 @@ func (h *movesHeldReleaseRoot) publishRelease(ctx context.Context, actor hostbox
// heldReleaseReceipt is root's record of a publication it made, one per
// host (the latest), keyed by the session and the render hash with the
-// identity the publication was for.
+// identity the publication was for, and the action, kind and phase it was
+// made at (fix round 2, P3-1), so a landing's configured receipt never
+// answers its probing request.
type heldReleaseReceipt struct {
Schema string `json:"schema"`
+ Action string `json:"action"`
+ Kind string `json:"kind"`
+ Phase string `json:"phase"`
Host string `json:"host"`
Account string `json:"account"`
App string `json:"app"`
@@ -468,7 +473,7 @@ type heldReleaseReceipt struct {
}
func receiptFor(q heldReleaseRequest) heldReleaseReceipt {
- return heldReleaseReceipt{Schema: heldReleaseSchema, Host: q.Host, Account: q.Account, App: q.App, Version: q.Version, MoveID: q.MoveID, HoldOp: q.HoldOp,
+ return heldReleaseReceipt{Schema: heldReleaseSchema, Action: q.Action, Kind: q.Kind, Phase: q.Phase, Host: q.Host, Account: q.Account, App: q.App, Version: q.Version, MoveID: q.MoveID, HoldOp: q.HoldOp,
Incarnation: q.Incarnation, Stamp: q.Stamp, Session: q.Session, Generation: q.Generation, RenderHash: q.RenderHash, Outcome: heldReleaseOutcomePublished}
}
@@ -477,8 +482,9 @@ func (h *movesHeldReleaseRoot) receiptPath(host string) string {
}
// replayed reports whether root's receipt for q's host is for exactly q:
-// the same session and render hash, and the same identity. Anything else,
-// an unreadable receipt included, is not a replay and runs the whole path.
+// the same session and render hash, the same identity, action, kind and
+// phase. Anything else, an unreadable receipt included, is not a replay and
+// runs the whole path.
func (h *movesHeldReleaseRoot) replayed(q heldReleaseRequest) bool {
f, err := safefile.OpenFile(h.receiptPath(q.Host), os.O_RDONLY, 0)
if err != nil {
diff --git a/internal/host/movesheldrelease_test.go b/internal/host/movesheldrelease_test.go
index 49cad8df6..1975b4bd0 100644
--- a/internal/host/movesheldrelease_test.go
+++ b/internal/host/movesheldrelease_test.go
@@ -528,6 +528,29 @@ func TestHeldReleaseRootReplayAndCrashRecovery(t *testing.T) {
}
})
+ t.Run("a configured receipt never answers the probe", func(t *testing.T) {
+ // Fix round 2, P3-1: the receipt binds the action, the kind and the
+ // phase, so a landing's configured publication does not answer its
+ // probing request within the same holder tuple: the probe's request
+ // runs the whole path.
+ f := newHRFixture(t)
+ if _, err := f.root.Serve(ctx, hrPeer, f.request(heldReleasePublish)); err != nil {
+ t.Fatal(err)
+ }
+ f.set(func(f *hrFixture) {
+ f.claim.Op.Phase = provision.LandProbing
+ f.rec.Move.LandingPhase = provision.LandProbing
+ })
+ reads := f.claimReads
+ resp, err := f.root.Serve(ctx, hrPeer, f.request(heldReleasePublish))
+ if err != nil || resp.Replayed {
+ t.Fatalf("the probe's request: %+v %v, want the whole path", resp, err)
+ }
+ if f.sel.count() != 2 || f.claimReads == reads {
+ t.Fatalf("selections %d, claim reads +%d: the probe was answered from configured's receipt", f.sel.count(), f.claimReads-reads)
+ }
+ })
+
t.Run("a stale request is not answered from an old receipt", func(t *testing.T) {
f := newHRFixture(t)
if _, err := f.root.Serve(ctx, hrPeer, f.request(heldReleasePublish)); err != nil {