carlos / platform Public

host: a moves source that does not open logs its cause once per agent start (Task 8 fix round 1, item 2)

platform-1e ruled the refusal stays fail closed: with the source unopened,
the claim refuses every wake on the box, serving included. The cause (for
example an unusable CARLOS_SECRETS_KEY) is now logged once per process, so
an operator can see why the box serves nothing.

Regression: TestMovesSourceOpenRefusalIsLoggedOnce (three refused opens,
the refusal kept each time, one line naming the cause).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 0a4e72bd44bb88fcf7b5f50fb12694318226c508 parent e462f93
2 files changed, +42 −0
  • internal/host/claimmoveparks.go +17 −0
  • internal/host/moveswakesource_test.go +25 −0
diff --git a/internal/host/claimmoveparks.go b/internal/host/claimmoveparks.go
index 26b056aed..e93c9c6dc 100644
--- a/internal/host/claimmoveparks.go
+++ b/internal/host/claimmoveparks.go
@@ -3,6 +3,9 @@ package host
import (
"context"
"errors"
+ "log"
+ "sync"
+
"github.com/carlosframework/platform/internal/activator"
"github.com/carlosframework/platform/internal/hostbox"
"github.com/carlosframework/platform/internal/hostclaim"
@@ -40,6 +43,14 @@ func RegisterProductionClaimMoveParks(view func(context.Context, hostclaim.Obser
return nil
}
+// claimMoveWakeLogf and claimMoveWakeLogOnce log a moves source that did
+// not open, once per process (per agent start); variables so the tests can
+// read and reset them.
+var (
+ claimMoveWakeLogf = log.Printf
+ claimMoveWakeLogOnce = new(sync.Once)
+)
+
// openProductionClaimMoveWake opens the registered moves source over the
// claim's data bucket as the serving owners see it (moves Task 8 fix round
// 1): through hostbox.RequestStore, so every bucket request the moves
@@ -59,6 +70,12 @@ func openProductionClaimMoveWake(ctx context.Context, store release.Store, actor
owners, files, err := external.Open(ctx, store, data, actor, out.moveRead)
if err != nil {
out.wakeUnavailable = errors.Join(&ClaimDependencyError{Contracts: []string{"moves wake owners and C8 origin-qualified files manifest provider"}}, err)
+ // Fail closed (platform-1e): every wake on this box is refused. Say
+ // why once, so an operator can see it (for example, an unusable
+ // CARLOS_SECRETS_KEY with CARLOS_MOVES_PHASE1 on).
+ claimMoveWakeLogOnce.Do(func() {
+ claimMoveWakeLogf("host claims: the moves wake source did not open, so every wake on this box is refused, serving included: %v", err)
+ })
return
}
out.Moves, out.Serving.Files = owners, files
diff --git a/internal/host/moveswakesource_test.go b/internal/host/moveswakesource_test.go
index fed015a73..948b9ca1f 100644
--- a/internal/host/moveswakesource_test.go
+++ b/internal/host/moveswakesource_test.go
@@ -4,8 +4,10 @@ import (
"context"
"encoding/base64"
"errors"
+ "fmt"
"reflect"
"strings"
+ "sync"
"testing"
"time"
@@ -454,3 +456,26 @@ func TestMovesOwnersReadTheBucketThroughTheRequestStore(t *testing.T) {
t.Fatal("the operation's cancellation did not reach the request")
}
}
+
+// TestMovesSourceOpenRefusalIsLoggedOnce (Task 8 fix round 1,
+// platform-1e): a moves source that does not open keeps every wake on the
+// box refused (fail closed), and the cause is logged once per agent start,
+// so an operator can see why the box serves nothing.
+func TestMovesSourceOpenRefusalIsLoggedOnce(t *testing.T) {
+ var lines []string
+ oldLogf, oldOnce := claimMoveWakeLogf, claimMoveWakeLogOnce
+ claimMoveWakeLogf = func(format string, args ...any) { lines = append(lines, fmt.Sprintf(format, args...)) }
+ claimMoveWakeLogOnce = new(sync.Once)
+ t.Cleanup(func() { claimMoveWakeLogf, claimMoveWakeLogOnce = oldLogf, oldOnce })
+ cause := errors.New("moves: secrets key: not a 32-byte key")
+ for i := 0; i < 3; i++ {
+ out := ClaimOwnerBindings{}
+ openProductionClaimMoveWake(t.Context(), nil, hostbox.Actor{}, &parkRegistrationWake{failure: cause}, &out, func(context.Context) error { return nil })
+ if !errors.Is(out.wakeUnavailable, cause) {
+ t.Fatalf("open %d: the refusal is not kept: %v", i, out.wakeUnavailable)
+ }
+ }
+ if len(lines) != 1 || !strings.Contains(lines[0], cause.Error()) || !strings.Contains(lines[0], "every wake") {
+ t.Fatalf("logged %q, want the cause once", lines)
+ }
+}