carlos / platform Public

console: return mode applies the free-disk and programs-file rules; a console without a secrets key does not refuse the target's

Fix round 1, Important 2 and minor 8.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev aa692a3532bc70053841f1798ffe6a55a2940e70 parent c7e0a8f
2 files changed, +45 −7
  • internal/console/offlinereturn.go +26 −4
  • internal/console/offlinereturn_test.go +19 −3
diff --git a/internal/console/offlinereturn.go b/internal/console/offlinereturn.go
index 6b700c26b..f3587befd 100644
--- a/internal/console/offlinereturn.go
+++ b/internal/console/offlinereturn.go
@@ -247,8 +247,6 @@ func (s *Server) readReturnReport(ctx context.Context, host, returnOf string, o
// - the nested directory and its mount view: absent, or present and
// empty, for a nested forward; absent for a flat one;
// - park directories: absent or the forward's own source/ and sealer/;
-// - the programs file: present or absent (it names the forward's
-// programs);
// - everything else: the ordinary rules.
func returnReadyDecision(host, label string, o *moves.OpenRecord, r *moves.TargetReadyReport, want targetWant) string {
if r == nil || r.Leftovers == nil || r.Leftovers.ForwardID != o.MoveID {
@@ -302,6 +300,8 @@ func returnReadyDecision(host, label string, o *moves.OpenRecord, r *moves.Targe
return ssStaleLiveLinkHere(host)
case !r.ProgramsFile.Known:
return couldNotDetermine(factProgramsFile, label)
+ case !r.ProgramsFile.OK:
+ return ssProgramsEntry(host)
}
for _, p := range targetDataPaths(host, r.DataPaths) {
f, ok := r.DataPaths[p]
@@ -319,7 +319,7 @@ func returnReadyDecision(host, label string, o *moves.OpenRecord, r *moves.Targe
switch {
case !r.ParkDir.Known:
return couldNotDetermine(factParkDir, label)
- case !r.DiskFree.Known:
+ case !r.DiskFree.Known || r.DiskFreeBytes < 2*returnReplicaBytes(r):
return ssDiskLow(host)
case !r.MovesPrefix.Known:
return couldNotDetermine(factMovesPrefix, label)
@@ -335,12 +335,34 @@ func returnReadyDecision(host, label string, o *moves.OpenRecord, r *moves.Targe
return ssLacksCapability(c)
}
}
- if want.secretsFP == "" || r.SecretsKeyFP != want.secretsFP {
+ switch {
+ case want.secretsFP == "":
+ // This console has no secrets key configured: the target's key
+ // cannot be compared, so it is not refused for it.
+ log.Printf("console: offline move %s: no secrets key fingerprint on this console; the return's target key is not compared", host)
+ case r.SecretsKeyFP != want.secretsFP:
return msgSelfServeKeyMismatch
}
return ""
}
+// returnReplicaBytes is the replica a return carries, as the report lists
+// the target bucket's instances prefix (the forward's leftover replica,
+// which the return's newer copy replaces): the free disk must be twice it.
+func returnReplicaBytes(r *moves.TargetReadyReport) int64 {
+ var n int64
+ for _, o := range r.InstancesPrefix.Objects {
+ n += o.Size
+ }
+ return n
+}
+
+// ssProgramsEntry is return mode's programs file with an entry for the host
+// (the ordinary rule: OK only when the host has none; pending copy review).
+func ssProgramsEntry(host string) string {
+ return fmt.Sprintf("the target box's programs file has an entry for %s", host)
+}
+
// ssStaleLiveLinkHere is return mode's live link that does not name the
// channel's build (pending copy review).
func ssStaleLiveLinkHere(host string) string {
diff --git a/internal/console/offlinereturn_test.go b/internal/console/offlinereturn_test.go
index 10441cab9..40528d0ed 100644
--- a/internal/console/offlinereturn_test.go
+++ b/internal/console/offlinereturn_test.go
@@ -147,9 +147,6 @@ func TestOperatorReturnHandshakeAtTheConsole(t *testing.T) {
"flat, the sealer's park directory only": func(w *returnWorld, r *moves.TargetReadyReport) {
r.Leftovers.ParkDirs = []string{returnFwd + "/sealer/"}
},
- "programs file present": func(w *returnWorld, r *moves.TargetReadyReport) {
- r.ProgramsFile = moves.Fact{Known: true, Detail: "./cmd/shop"}
- },
} {
t.Run(name, func(t *testing.T) {
w := newReturnWorld(t, nil)
@@ -215,6 +212,12 @@ func TestOperatorReturnHandshakeAtTheConsole(t *testing.T) {
"a stale live link": {func(w *returnWorld, r *moves.TargetReadyReport) {
r.LiveLink = moves.Fact{Known: true, Detail: "/opt/carlos/versions/2026-10-01.1/bab/shop"}
}, "the target box has a stale live link for " + offlineHost},
+ "a programs file entry": {func(w *returnWorld, r *moves.TargetReadyReport) {
+ r.ProgramsFile = moves.Fact{Known: true, Detail: "./cmd/shop"}
+ }, "the target box's programs file has an entry for " + offlineHost},
+ "free disk under twice the replica": {func(w *returnWorld, r *moves.TargetReadyReport) {
+ r.InstancesPrefix.Objects["0000/a.ltx"] = moves.PrefixObject{Size: 6 << 30, LastModified: w.open.OpenedAt.Add(-time.Minute)}
+ }, "the target box has too little free disk for " + offlineHost},
"timers left": {func(w *returnWorld, r *moves.TargetReadyReport) { r.TimersAbsent.OK = false },
"the target box has timers for " + offlineHost},
"leftovers of another move": {func(w *returnWorld, r *moves.TargetReadyReport) { r.Leftovers.ForwardID = "mv_other" },
@@ -297,6 +300,19 @@ func TestOperatorReturnHandshakeAtTheConsole(t *testing.T) {
t.Fatalf("after the start: %+v", rec.Move)
}
})
+ t.Run("a console without a secrets key does not refuse the target's", func(t *testing.T) {
+ w := newReturnWorld(t, nil)
+ r := w.ready()
+ r.SecretsKeyFP = "another"
+ want := targetWant{caps: []string{offlineMoveCapability, offlineStageCapability}}
+ if got := returnReadyDecision(offlineHost, "uk-box", &w.open, r, want); got != "" {
+ t.Fatalf("no fingerprint to compare: %q", got)
+ }
+ want.secretsFP = testKeyFP
+ if got := returnReadyDecision(offlineHost, "uk-box", &w.open, r, want); got != "the target box's secrets key does not match" {
+ t.Fatalf("another key: %q", got)
+ }
+ })
t.Run("a start or stage without return_of is unchanged", func(t *testing.T) {
w := newReturnWorld(t, nil)
if code, out := w.req(http.MethodPost, "/stage", `{"region":"uk"}`); code != http.StatusAccepted {