carlos / platform Public

moves-phase1-6 Task 3 fix round 3 (3): remove MoveRecoveryGrant.Log

Nothing calls it once the drainer delivers the console's entries: a grant
is spent before its own transition's entry exists. Its test, its B8 census
row and its authority census selection go with it; moveLogName moves to the
writer, whose checks are the ones platform-1e set for Log.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev afb2d7994bdc6bec8362bdb5c659057e7f2ca29c parent b00577b
6 files changed, +17 −174
  • internal/console/claim_authority_census_test.go +0 −3
  • internal/hostops/move_log_writer.go +16 −1
  • internal/hostops/move_log_writer_test.go +1 −1
  • internal/hostops/move_recovery_grant.go +0 −75
  • internal/hostops/move_recovery_grant_test.go +0 −90
  • internal/provision/plan9_b8_census_test.go +0 −4
diff --git a/internal/console/claim_authority_census_test.go b/internal/console/claim_authority_census_test.go
index 6c5a51572..5866ca06f 100644
--- a/internal/console/claim_authority_census_test.go
+++ b/internal/console/claim_authority_census_test.go
@@ -29,9 +29,6 @@ func claimAuthoritySites(t *testing.T) []string {
// The console's move pass (moves Plan 5 Task 7): every write of the
// move-recovery grant, and the lost target's stamp.
"ResolveStart": true, "Convert": true, "Claim": true, "Record": true, "StampLostMoveTargetParked": true, "stampLostMoveTarget": true,
- // The move log the pass writes through the grant (Plan 6 Task 3, fix
- // round 2 ruling 4): an entry of the grant's own move, create-only.
- "Log": true,
// The recovery pass's move log writer (Plan 6 Task 3, fix round 3
// ruling 1): minted inside RunRecovery for the console's outbox
// drainer; one create-only entry per call, under the entry's own
diff --git a/internal/hostops/move_log_writer.go b/internal/hostops/move_log_writer.go
index 7a31c0ce9..11f4753d9 100644
--- a/internal/hostops/move_log_writer.go
+++ b/internal/hostops/move_log_writer.go
@@ -55,7 +55,8 @@ func runMoveLogDrain(ctx context.Context, hook func(context.Context, *MoveLogWri
return hook(ctx, lw)
}
-// WriteMoveLog writes one entry, with the checks MoveRecoveryGrant.Log made:
+// WriteMoveLog writes one entry, with the checks platform-1e set for the
+// grant's Log, which this writer replaced (fix round 3, ruling 3):
//
// - The key is derived, never given: e.Name under the report prefix of
// e's account, app, host and move. Each is a valid name; the move id and
@@ -113,3 +114,17 @@ func (lw *MoveLogWriter) WriteMoveLog(ctx context.Context, e MoveLogEntry) (crea
}
return false, nil
}
+
+// moveLogName is one path segment of letters, digits, '_' and '-', at most
+// 128 bytes: every identity the move log names (moves.TransitionOf) fits.
+func moveLogName(name string) bool {
+ if name == "" || len(name) > 128 {
+ return false
+ }
+ for _, r := range name {
+ if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '_' || r == '-') {
+ return false
+ }
+ }
+ return true
+}
diff --git a/internal/hostops/move_log_writer_test.go b/internal/hostops/move_log_writer_test.go
index 26d1e49c0..a10c21058 100644
--- a/internal/hostops/move_log_writer_test.go
+++ b/internal/hostops/move_log_writer_test.go
@@ -14,7 +14,7 @@ import (
// Plan 6 Task 3 fix round 3, ruling 1: the recovery pass's move log writer
// (Recovery.MoveLogs) delivers the console's queued entries whatever their
-// moves now are, with Grant.Log's key derivation and checks: the key is
+// moves now are, with the grant Log's key derivation and checks: the key is
// derived from the entry's account, app, host and move under the report
// prefix; the name is one segment and the entry's identity; the entry is at
// most MoveLogEntryMax bytes and this move's and host's; the host's claim
diff --git a/internal/hostops/move_recovery_grant.go b/internal/hostops/move_recovery_grant.go
index f1e925ba1..690ed4eb5 100644
--- a/internal/hostops/move_recovery_grant.go
+++ b/internal/hostops/move_recovery_grant.go
@@ -1,7 +1,6 @@
package hostops
import (
- "bytes"
"context"
"crypto/rand"
"encoding/json"
@@ -126,80 +125,6 @@ func (g *MoveRecoveryGrant) current(ctx context.Context) (hostclaim.Observed, Re
return o, r, rec, err
}
-// Log writes one entry of this grant's move's log (Plan 6 Task 3; the
-// conditions are platform-1e's, fix round 2 ruling 4):
-//
-// - The key is the grant's alone: name under the report prefix of the
-// grant's move (its account, app, host and move id), whose record still
-// names that move at the grant's incarnation, live or closed as its
-// last. The caller gives only name, one segment, which must be the
-// entry's identity.
-// - body is at most MoveLogEntryMax bytes and decodes, strictly, as a
-// moves.LogEvent of this move and host.
-// - The write is create-only. A replay of the same bytes is a no-op
-// (created false); other bytes under the name are ErrMoveLogTaken.
-// - It answers only while the grant is valid, by the check every other
-// method makes (current): never after the grant's one mutation, after
-// its hook, or across a changed claim or record. A pass logs what it
-// owes before it acts; an entry its own write makes waits in the
-// caller's outbox for the next pass's grant.
-//
-// An entry records a transition and is never an input to one.
-func (g *MoveRecoveryGrant) Log(ctx context.Context, name string, body []byte) (created bool, err error) {
- if g == nil || g.state == nil {
- return false, ErrPending
- }
- g.state.mu.Lock()
- defer g.state.mu.Unlock()
- _, _, rec, err := g.current(ctx)
- if err != nil {
- return false, err
- }
- if !moveLogName(name) || len(body) > MoveLogEntryMax {
- return false, ErrIdentity
- }
- var ev moves.LogEvent
- if err := strictJSON(body, &ev); err != nil {
- return false, ErrIdentity
- }
- if ev.V != 1 || ev.Op != g.o.OpID || ev.Host != g.o.Host || ev.Identity != name {
- return false, ErrIdentity
- }
- live := rec.Move != nil && rec.Move.ID == g.o.OpID && rec.Move.Incarnation == g.o.Incarnation
- closed := rec.Move == nil && rec.LastMove != nil && rec.LastMove.ID == g.o.OpID
- if !live && !closed {
- return false, ErrIdentity
- }
- key := moves.LogKeyFor(moves.ReportPrefix(g.o.Account, g.o.App, g.o.Host, g.o.OpID), name)
- if _, err := g.w.raw.Put(ctx, key, body, ""); err == nil {
- return true, nil
- } else if !errors.Is(err, release.ErrCASConflict) {
- return false, err
- }
- have, _, err := g.w.raw.Get(ctx, key)
- if err != nil {
- return false, err
- }
- if !bytes.Equal(have, body) {
- return false, ErrMoveLogTaken
- }
- return false, nil
-}
-
-// moveLogName is one path segment of letters, digits, '_' and '-', at most
-// 128 bytes: every identity the move log names (moves.TransitionOf) fits.
-func moveLogName(name string) bool {
- if name == "" || len(name) > 128 {
- return false
- }
- for _, r := range name {
- if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '_' || r == '-') {
- return false
- }
- }
- return true
-}
-
// Transition is what the grant's record write did, named as the move log
// names it (moves.TransitionOf over the record before and after the write),
// once Record has written. It is a value, never authority: the caller's log
diff --git a/internal/hostops/move_recovery_grant_test.go b/internal/hostops/move_recovery_grant_test.go
index 827032803..470289fc4 100644
--- a/internal/hostops/move_recovery_grant_test.go
+++ b/internal/hostops/move_recovery_grant_test.go
@@ -2,15 +2,12 @@ package hostops
import (
"context"
- "encoding/json"
"errors"
"fmt"
- "strings"
"testing"
"time"
"github.com/carlosframework/platform/internal/hostclaim"
- "github.com/carlosframework/platform/internal/moves"
"github.com/carlosframework/platform/internal/provision"
"github.com/carlosframework/platform/internal/release"
)
@@ -185,93 +182,6 @@ func TestMoveRecoveryRecordCloseThenRelease(t *testing.T) {
}
}
-// Plan 6 Task 3 fix round 2, ruling 4 (platform-1e's conditions): the
-// grant's Log writes one entry of its own move's log. The key is derived
-// from the grant alone (its move's report prefix); the caller names only a
-// single-segment entry. The entry is at most MoveLogEntryMax bytes, is this
-// move's and this host's, and is named by its identity. The write is
-// create-only: a replay of the same bytes is a no-op, other bytes under the
-// name are ErrMoveLogTaken. Log answers only while the grant is valid, by
-// the check every other grant method uses: not after its one mutation, and
-// not after its hook.
-func TestMoveRecoveryGrantLogsItsOwnMove(t *testing.T) {
- w := newC7World(t, func(c *hostclaim.Claim, r *provision.Record, _ *AppAdmission) {
- c.Op.Kind, c.Op.Phase, c.Op.Exec = hostclaim.KindLanding, hostclaim.PhaseCommitted, "target"
- c.Obligations = nil
- r.Region = "uk"
- r.Move.State = provision.MoveCommitted
- now := r.CreatedAt
- r.Move.CommittedAt = &now
- r.Move.OpenedAt = &now
- r.Move.SourceParkedAt = &now
- })
- ctx := context.Background()
- entry := func(op, host, id string) []byte {
- b, err := json.Marshal(moves.LogEvent{V: 1, Op: op, Host: host, Event: "x", Identity: id, Actor: "console", At: w.now})
- if err != nil {
- t.Fatal(err)
- }
- return b
- }
- body := entry("move", w.host, "opened")
- var saved *MoveRecoveryGrant
- got := map[string]error{}
- var created, again bool
- err := w.control.RunRecovery(ctx, func(ctx context.Context) error {
- return runMoveRecovery(ctx, w.claim(t), func(ctx context.Context, g *MoveRecoveryGrant) error {
- saved = g
- var err error
- if created, err = g.Log(ctx, "opened", body); err != nil {
- return err
- }
- if again, err = g.Log(ctx, "opened", body); err != nil {
- return err
- }
- other, _ := json.Marshal(moves.LogEvent{V: 1, Op: "move", Host: w.host, Event: "x", Identity: "opened", Actor: "target", At: w.now})
- _, got["other bytes"] = g.Log(ctx, "opened", other)
- _, got["another move"] = g.Log(ctx, "other", entry("other", w.host, "other"))
- _, got["another host"] = g.Log(ctx, "elsewhere", entry("move", "elsewhere.example", "elsewhere"))
- _, got["a name that is not the identity"] = g.Log(ctx, "done", entry("move", w.host, "opened"))
- _, got["a path"] = g.Log(ctx, "../source", entry("move", w.host, "../source"))
- _, got["an empty name"] = g.Log(ctx, "", entry("move", w.host, ""))
- big := moves.LogEvent{V: 1, Op: "move", Host: w.host, Event: "x", Identity: "big", Fields: map[string]string{"pad": strings.Repeat("a", MoveLogEntryMax)}}
- bb, _ := json.Marshal(big)
- _, got["too big"] = g.Log(ctx, "big", bb)
- if err := g.Record(ctx, RecoveryMoveClose); err != nil {
- return err
- }
- _, got["after the mutation"] = g.Log(ctx, "done", entry("move", w.host, "done"))
- return nil
- })
- })
- if err != nil {
- t.Fatal(err)
- }
- if !created || again {
- t.Fatalf("created=%v, a replay of the same bytes created=%v", created, again)
- }
- for what, want := range map[string]error{
- "other bytes": ErrMoveLogTaken, "another move": ErrIdentity, "another host": ErrIdentity,
- "a name that is not the identity": ErrIdentity, "a path": ErrIdentity, "an empty name": ErrIdentity,
- "too big": ErrIdentity, "after the mutation": ErrPending,
- } {
- if !errors.Is(got[what], want) {
- t.Errorf("%s: %v, want %v", what, got[what], want)
- }
- }
- logged, err := moves.ReadLog(ctx, w.raw, moves.ReportPrefix("bab", "app", w.host, "move"))
- if err != nil || len(logged) != 1 || logged[0].Identity != "opened" {
- t.Fatalf("log %+v %v", logged, err)
- }
- raw, _, err := w.raw.Get(ctx, moves.LogKeyFor(moves.ReportPrefix("bab", "app", w.host, "move"), "opened"))
- if err != nil || string(raw) != string(body) {
- t.Fatalf("the object is not the caller's bytes: %s %v", raw, err)
- }
- if _, err := saved.Log(ctx, "late", entry("move", w.host, "late")); !errors.Is(err, ErrPending) {
- t.Fatalf("a grant kept past its hook logged: %v", err)
- }
-}
-
func TestMoveRecoveryStartRequiresDurableBaseAndHonorsAppliedJournal(t *testing.T) {
for _, marker := range []string{"missing-base", "applied-effect", "issued"} {
t.Run(marker, func(t *testing.T) {
diff --git a/internal/provision/plan9_b8_census_test.go b/internal/provision/plan9_b8_census_test.go
index 307ca32c2..b0b4e1bf0 100644
--- a/internal/provision/plan9_b8_census_test.go
+++ b/internal/provision/plan9_b8_census_test.go
@@ -477,10 +477,6 @@ var plan9B8CensusInventory = []censusEntry{
// log under its report prefix, for a host whose claim still names the
// app; never a record (Plan 6 Task 3 fix round 3, ruling 1).
{"internal/hostops/move_log_writer.go", "MoveLogWriter.WriteMoveLog", "nonrecord", "sel:Put", "9dffdb6f7f15f7dfffc104e0f09df4b6c6d7868a31ee1c85cfb811a04ce5c90c"},
- // Writes one entry of the grant's own move's log, create-only, under its
- // report prefix (moves/<acct>/<app>/<host>/<op>/log/); never a record
- // (Plan 6 Task 3 fix round 2, ruling 4).
- {"internal/hostops/move_recovery_grant.go", "MoveRecoveryGrant.Log", "nonrecord", "sel:Put", "9f90ff153382fc6278ed0c17dffa5e0bfda01287716af1fa3149fef9772213c2"},
{"internal/hostops/move_recovery_grant.go", "MoveRecoveryGrant.Record", "record-writer", "key:WriteRecord", "f7f04a9e2c579eaa7d254ce6310e2726d95855307d61f3f442ee9c1fa2a5d825"},
// Plan 10: reviewed typed console move and loss writers.
{"internal/hostops/console_move_grant.go", "ControlWriter.ConsoleMove", "record-writer", "key:WriteRecord", "21e7a6ddc4dbe5b3bf06e65b39a228e2eb588af1edc43beb09d91dd72570dd44"},