carlos / platform Public

moves: report timestamps are bounded in the future too (Task 11 fix round 2)

A target or source report dated more than a minute ahead of the reader's
clock is refused, so it cannot stay fresh forever. The exact edges of the
measurement window are tested. The PutReplace comment states the real
reason its twins do not fit.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 2830956ff3d01b60acb611ed788215dae0f8bca3 parent 3fca22d
3 files changed, +86 −5
  • docs/superpowers/plans/2026-10-06-moves-phase1-copy.md +2 −0
  • internal/moves/readiness.go +16 −5
  • internal/moves/readiness_test.go +68 −0
diff --git a/docs/superpowers/plans/2026-10-06-moves-phase1-copy.md b/docs/superpowers/plans/2026-10-06-moves-phase1-copy.md
index 1d7b502fb..720f0a441 100644
--- a/docs/superpowers/plans/2026-10-06-moves-phase1-copy.md
+++ b/docs/superpowers/plans/2026-10-06-moves-phase1-copy.md
@@ -123,6 +123,8 @@ Columns: id, file, text, status.
| MV-N81 | internal/moves/archive/archive.go | archive: refused path: <path> is longer than 4096 bytes | pending |
| MV-N82 | internal/moves/readiness.go | T11: the target's report carries a rate that is not a usable number | pending |
| MV-N83 | internal/moves/readiness.go | T11: the target's report claims <n> probe attempts and its measurements admit <n> | pending |
+| MV-N84 | internal/moves/readiness.go | the target's readiness report is dated ahead of this clock | pending |
+| MV-N85 | internal/moves/readiness.go | the source's readiness report is dated ahead of this clock | pending |
## Plan 2
diff --git a/internal/moves/readiness.go b/internal/moves/readiness.go
index c99182e2c..d3077d6dd 100644
--- a/internal/moves/readiness.go
+++ b/internal/moves/readiness.go
@@ -36,12 +36,18 @@ var AllChecks = []string{CheckProtocol, CheckSecrets, CheckEnv, CheckStorage, Ch
// (§7.3); the cut re-checks it at `admitted` (§7.4).
const ReportFreshness = 10 * time.Minute
+// ReportSkew is how far ahead of the reader's clock a report may be dated
+// (clock skew between boxes). A report dated further ahead is refused, so a
+// wrong or stuck clock cannot keep a report fresh. It is the same bound as
+// MeasurementSkew.
+const ReportSkew = time.Minute
+
// MeasurementFreshness bounds how old a rate measurement may be and still
// price a cut (§7.3 T11). MeasurementSkew is how far ahead of the reader's
// clock a measurement made on another box may be dated.
const (
MeasurementFreshness = time.Hour
- MeasurementSkew = time.Minute
+ MeasurementSkew = ReportSkew
)
// TargetReport is what the target's box publishes at TargetReportKey, every
@@ -160,8 +166,10 @@ func Ready(roundDelta, bar int64, target *TargetReport, source *SourceReport, no
if target == nil {
return false, "the target has not published a readiness report yet"
}
- if now.Sub(target.At) > ReportFreshness {
+ if age := now.Sub(target.At); age > ReportFreshness {
return false, "the target's readiness report is stale"
+ } else if age < -ReportSkew {
+ return false, "the target's readiness report is dated ahead of this clock"
}
if ok, why := target.Passing(); !ok {
return false, why
@@ -179,8 +187,10 @@ func Ready(roundDelta, bar int64, target *TargetReport, source *SourceReport, no
if source == nil {
return false, "the source has not published a readiness report yet"
}
- if now.Sub(source.At) > ReportFreshness {
+ if age := now.Sub(source.At); age > ReportFreshness {
return false, "the source's readiness report is stale"
+ } else if age < -ReportSkew {
+ return false, "the source's readiness report is dated ahead of this clock"
}
if !source.S1.OK {
return false, "S1: " + reasonOr(source.S1, "the source did not answer")
@@ -346,8 +356,9 @@ func BudgetCheck(dbMiB float64, m Measurements, barBytes int64, windowSeconds fl
// It has twins: putCAS in internal/edge/certsync.go (three attempts) and
// status.WriteBox in internal/status/snapshot.go (one retry, a second
// conflict is returned loudly). Both are the same read-etag-then-If-Match
-// loop with different policies and error wording, and neither is importable
-// from here without a dependency cycle through the packages that own them.
+// loop with different policies and error wording. Neither fits here:
+// putCAS is unexported in internal/edge, and WriteBox writes a box's own
+// status snapshot to its fixed key, not an arbitrary key and body.
// This one retries ONCE on conflict, like WriteBox: a report has a single
// writer per pass, so two conflicts in a row mean something else is writing
// the key, and the caller sees ErrCASConflict rather than a silent third try.
diff --git a/internal/moves/readiness_test.go b/internal/moves/readiness_test.go
index 7c8a229b9..8736b2a7d 100644
--- a/internal/moves/readiness_test.go
+++ b/internal/moves/readiness_test.go
@@ -699,3 +699,71 @@ func TestPutReplaceWorksOnADirStore(t *testing.T) {
t.Fatalf("%q %v", got, err)
}
}
+
+// A report dated ahead of the reader's clock would stay fresh until the
+// clock caught up and then some. One minute ahead is clock skew between
+// boxes and is accepted; beyond that is refused.
+func TestReadyBoundsReportTimestampsInTheFuture(t *testing.T) {
+ now := t11Now
+ // A target report one minute ahead is fine (its own measurements are
+ // dated relative to it, so they sit in the past of now).
+ tr := passingTarget(now.Add(ReportSkew))
+ if ok, why := Ready(0, 16<<20, tr, goodSource(now), now); !ok {
+ t.Fatalf("a target report one minute ahead: %s", why)
+ }
+ tr = passingTarget(now.Add(ReportSkew + time.Nanosecond))
+ if ok, why := Ready(0, 16<<20, tr, goodSource(now), now); ok || !strings.Contains(why, "ahead") || !strings.Contains(why, "target") {
+ t.Fatalf("a target report beyond the skew: %v %q", ok, why)
+ }
+ // Far ahead, as a stuck or wrong clock would write.
+ if ok, _ := Ready(0, 16<<20, passingTarget(now.Add(24*time.Hour)), goodSource(now), now); ok {
+ t.Fatal("a target report a day ahead promoted")
+ }
+
+ sr := goodSource(now)
+ sr.At = now.Add(ReportSkew)
+ if ok, why := Ready(0, 16<<20, passingTarget(now), sr, now); !ok {
+ t.Fatalf("a source report one minute ahead: %s", why)
+ }
+ sr.At = now.Add(ReportSkew + time.Nanosecond)
+ if ok, why := Ready(0, 16<<20, passingTarget(now), sr, now); ok || !strings.Contains(why, "ahead") || !strings.Contains(why, "source") {
+ t.Fatalf("a source report beyond the skew: %v %q", ok, why)
+ }
+ sr.At = now.Add(24 * time.Hour)
+ if ok, _ := Ready(0, 16<<20, passingTarget(now), sr, now); ok {
+ t.Fatal("a source report a day ahead promoted")
+ }
+
+ // The old edge is unchanged: exactly ReportFreshness old is fresh.
+ if ok, why := Ready(0, 16<<20, passingTarget(now.Add(-ReportFreshness)), goodSource(now), now); !ok {
+ t.Fatal(why)
+ }
+ if ok, _ := Ready(0, 16<<20, passingTarget(now.Add(-ReportFreshness-time.Nanosecond)), goodSource(now), now); ok {
+ t.Fatal("a report a nanosecond past the freshness window promoted")
+ }
+}
+
+// The exact edges of a measurement's window: an hour old and a minute
+// ahead are accepted, a nanosecond beyond either is refused.
+func TestMeasurementWindowEdges(t *testing.T) {
+ at := func(d time.Duration) error {
+ m := goodMeasurements()
+ m.S3.At = t11Now.Add(d)
+ _, err := m.Rates(t11Now, 16<<20)
+ return err
+ }
+ for _, c := range []struct {
+ d time.Duration
+ ok bool
+ }{
+ {-MeasurementFreshness, true},
+ {-MeasurementFreshness - time.Nanosecond, false},
+ {MeasurementSkew, true},
+ {MeasurementSkew + time.Nanosecond, false},
+ {0, true},
+ } {
+ if err := at(c.d); (err == nil) != c.ok {
+ t.Errorf("age %v: err %v, want ok=%v", c.d, err, c.ok)
+ }
+ }
+}