carlos / platform Public

host: an unreadable install journal refuses rather than reads as no receipt; VerifyLocal reads P's manifest as the install does; one environment for the pass; the serial, floor-seam, op-id and attempt-count notes (final review minors 1, 3, 4, 5, 6, 7, 10, 11)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 9a9da0277bb15f8bc839ee5b6ac21fe3e3a9c4b4 parent 7f80f87
6 files changed, +83 −22
  • internal/host/claimparticipation.go +4 −1
  • internal/host/relocatepass.go +17 −9
  • internal/host/relocationflush.go +3 −1
  • internal/host/relocationinstall.go +17 −8
  • internal/host/relocationinstall_linux_test.go +35 −2
  • internal/host/relocationowners.go +7 −1
diff --git a/internal/host/claimparticipation.go b/internal/host/claimparticipation.go
index dc7272992..b1eec455c 100644
--- a/internal/host/claimparticipation.go
+++ b/internal/host/claimparticipation.go
@@ -60,7 +60,10 @@ func (p *productionClaimLocal) lockDir(name string) string {
}
// updateFloor is the controller's update-floor guard directory and
-// journal: production's under OptDir, none under the N12 test seam.
+// journal: production's under OptDir, none under the N12 test seam. No
+// production constructor sets lockRoot (OpenClaimOwners builds the owner
+// without it; only tests assign it), so the floor is never bypassed in
+// production.
func (p *productionClaimLocal) updateFloor() (string, string) {
if p.lockRoot != "" {
return "", ""
diff --git a/internal/host/relocatepass.go b/internal/host/relocatepass.go
index 708b7da15..3623fc26b 100644
--- a/internal/host/relocatepass.go
+++ b/internal/host/relocatepass.go
@@ -5,7 +5,6 @@ import (
"crypto/ed25519"
"errors"
"log"
- "os"
"path/filepath"
"strings"
"sync"
@@ -70,7 +69,10 @@ type RelocationRuntime struct {
// read that failed once is never taken as "no relocation on this box".
upgrade func(context.Context) (*RelocationRuntime, error)
// config is the assembly config the full pass was built with
- // (NewRelocationRuntime); zero for the completion-only runtime.
+ // (NewRelocationRuntime); zero for the completion-only runtime. No
+ // production code reads it: it is the seam Task 10's review M7 test
+ // asserts the held-stage evidence (ReleasePub, Ratchet) through
+ // (relocatepass_carries_test.go), kept for that reading alone.
config RelocationAssemblyConfig
}
@@ -387,8 +389,11 @@ func wakeDispatch(a *ClaimAssembly) (ClaimOwnerDispatch, bool) {
return d, ok
}
-// Run is one pass every 30 seconds and on a control serial change, each
-// bounded at two minutes.
+// Run is one pass every 30 seconds, each bounded at two minutes, and one
+// more whenever serial is signalled. The agent gives no serial channel
+// (launchRelocationPass): the control serial's one wake channel is the
+// regional loop's (agent.go, serialClaims), so the pass runs on its ticker
+// alone and a BumpSerial wakes it no sooner than the next tick.
func (r *RelocationRuntime) Run(ctx context.Context, store release.Store, serial <-chan struct{}) {
tick := time.NewTicker(30 * time.Second)
defer tick.Stop()
@@ -659,15 +664,18 @@ func (a *relocationAlerts) size() int {
}
// registerRelocationSource registers region HA's owner source with the
-// roster directory the claim's own owners read (claimproduction.go).
+// roster directory the claim's own owners read (claimproduction.go). The
+// release key and the deployment bucket are read from the claim entry's
+// environment, the one the flags and the claim store came from (final
+// review minor 5: one reading of the environment).
func registerRelocationSource(claimCtx context.Context, dataDir string) error {
- store := claimStore(claimCtx)
- pub, err := release.ParsePublicKey(os.Getenv("CARLOS_RELEASE_PUBKEY"), "CARLOS_RELEASE_PUBKEY")
+ store, getenv := claimStore(claimCtx), claimEntryEnv(claimCtx)
+ pub, err := release.ParsePublicKey(getenv("CARLOS_RELEASE_PUBKEY"), "CARLOS_RELEASE_PUBKEY")
if err != nil || store == nil {
return errors.Join(hostwrite.ErrUnavailable, err)
}
return RegisterProductionClaimRelocationSource(&RelocationSource{DataDir: dataDir,
- Directory: hostactors.Directory{Store: store, PublicKey: pub, Bucket: os.Getenv("CARLOS_DEPLOYMENT_BUCKET")},
+ Directory: hostactors.Directory{Store: store, PublicKey: pub, Bucket: getenv("CARLOS_DEPLOYMENT_BUCKET")},
FilesStore: func(ctx context.Context, bucket string) (release.Store, error) {
s, err := release.NewS3Store(ctx, bucket)
if err != nil {
@@ -704,7 +712,7 @@ func startRelocationPass(agentCtx, claimCtx context.Context, claims *ClaimWiring
ha := regionHAEnabled(getenv)
if ha {
full = func(ctx context.Context) (*RelocationRuntime, error) {
- in, err := productionRelocationRuntimeInputs(os.Getenv, release.OptDir())
+ in, err := productionRelocationRuntimeInputs(getenv, release.OptDir())
if err != nil {
return nil, err
}
diff --git a/internal/host/relocationflush.go b/internal/host/relocationflush.go
index e97e75740..dc27ee063 100644
--- a/internal/host/relocationflush.go
+++ b/internal/host/relocationflush.go
@@ -200,7 +200,9 @@ func (f ReleasePositionsFile) Recorded(_ context.Context, r StopRequest) (Releas
// validOpID is the one check that an op id can name a file or directory
// under the data root (region HA ruling 44): non-empty, not "." or "..",
// no separator or NUL. Release positions and every relocation journal use
-// it.
+// it. It is not moves.ValidOpID, which checks moves' own id shape
+// (mv_/rb_ prefixes) that relocation's hex ids (newClaimID) never have:
+// this is path safety, not identity.
func validOpID(op string) bool {
return op != "" && op != "." && op != ".." && !strings.ContainsAny(op, "/\\\x00")
}
diff --git a/internal/host/relocationinstall.go b/internal/host/relocationinstall.go
index dfbca99ac..ed2ecc185 100644
--- a/internal/host/relocationinstall.go
+++ b/internal/host/relocationinstall.go
@@ -637,13 +637,16 @@ func (a ClaimAssembly) relocationVerifyLocal(cfg RelocationAssemblyConfig) func(
return false, nil
}
p := o.Claim.Op.Journal["prepared"]
- raw, _, err := cfg.Data.Get(ctx, hostfiles.ManifestKey(o.Claim.Holder.Location+"/files/"+p+"/"))
+ prefix := o.Claim.Holder.Location + "/files/" + p + "/"
+ m, found, err := hostfiles.Load(ctx, cfg.Data, prefix)
if err != nil {
return false, err
}
- var m hostfiles.Manifest
- if err := json.Unmarshal(raw, &m); err != nil {
- return false, err
+ if !found {
+ // Not "the install is incomplete": the install itself refuses
+ // without it, so a repair could not mend this (absence is
+ // never evidence).
+ return false, fmt.Errorf("%w: P's files manifest %s is missing", hostrelocate.ErrRefused, prefix)
}
// No env file only by the pin's evidence, as the install judged it.
pin, err := relocationPin(ctx, a.Claims, o.Claim)
@@ -660,8 +663,11 @@ func (a ClaimAssembly) relocationVerifyLocal(cfg RelocationAssemblyConfig) func(
}
// relocationReceipt reads this box's durable install journal for the
-// claim's operation; false when there is none, it is damaged, or it is not
-// this operation's.
+// claim's operation; false, with no error, only when there is none. A
+// journal that does not decode, names another host or operation, or
+// records no materialized release is an error, never "no receipt"
+// (final review minor 1, as readRelocationProbeFence): it is never
+// reinstalled over, so an operator keeps the evidence.
func (a ClaimAssembly) relocationReceipt(cfg RelocationAssemblyConfig) func(context.Context, hostclaim.Observed) (hostclaim.LocalInstall, bool, error) {
return func(_ context.Context, o hostclaim.Observed) (hostclaim.LocalInstall, bool, error) {
if o.Claim == nil {
@@ -679,8 +685,11 @@ func (a ClaimAssembly) relocationReceipt(cfg RelocationAssemblyConfig) func(cont
return hostclaim.LocalInstall{}, false, err
}
var j relocationInstallJournal
- if err := json.Unmarshal(raw, &j); err != nil || j.V != 1 || j.Host != o.Host || j.Op != o.Claim.Op.ID || j.Steps["materialize"] == "" {
- return hostclaim.LocalInstall{}, false, nil
+ if err := json.Unmarshal(raw, &j); err != nil {
+ return hostclaim.LocalInstall{}, false, fmt.Errorf("%w: %s cannot be read: %v", hostrelocate.ErrRefused, path, err)
+ }
+ if j.V != 1 || j.Host != o.Host || j.Op != o.Claim.Op.ID || j.Steps["materialize"] == "" {
+ return hostclaim.LocalInstall{}, false, fmt.Errorf("%w: %s is not this operation's completed install journal", hostrelocate.ErrRefused, path)
}
seq, _ := strconv.ParseInt(j.Steps["files"], 10, 64)
sum := sha256.Sum256(raw)
diff --git a/internal/host/relocationinstall_linux_test.go b/internal/host/relocationinstall_linux_test.go
index 480a545a0..0bd92f496 100644
--- a/internal/host/relocationinstall_linux_test.go
+++ b/internal/host/relocationinstall_linux_test.go
@@ -817,6 +817,16 @@ func TestRelocationVerifyLocalReadsTheInstallBack(t *testing.T) {
if ok, err := verify(ctx, f.read(), l); err != nil || ok {
t.Fatalf("a missing file verified: %v %v", ok, err)
}
+ // Final review minor 10: P's manifest is read as the install reads it
+ // (hostfiles.Load); one that is gone is a refusal, not an install to
+ // repair (the install refuses without it too).
+ o := f.read()
+ if err := f.cfg.Data.Delete(ctx, hostfiles.ManifestKey(o.Claim.Holder.Location+"/files/"+o.Claim.Op.Journal["prepared"]+"/")); err != nil {
+ t.Fatal(err)
+ }
+ if ok, err := verify(ctx, o, l); ok || !errors.Is(err, hostrelocate.ErrRefused) {
+ t.Fatalf("a missing manifest: %v %v", ok, err)
+ }
}
func TestRelocationReceiptIsTheDurableJournal(t *testing.T) {
@@ -833,12 +843,35 @@ func TestRelocationReceiptIsTheDurableJournal(t *testing.T) {
if got, ok, err := receipt(ctx, f.read()); err != nil || !ok || got != l {
t.Fatalf("receipt %+v %v %v", got, ok, err)
}
+ // Final review minor 1: a journal that is present but cannot be read,
+ // or is not this operation's completed install, is an error, never
+ // "no receipt" that EnsureLocal would reinstall over.
+ journaled, _ := json.Marshal(l)
+ if _, err := hostclaim.Update(ctx, f.claims, f.read(), func(c *hostclaim.Claim) error { c.Op.Journal["local_install"] = string(journaled); return nil }); err != nil {
+ t.Fatal(err)
+ }
path, _ := relocationInstallPath(f.r.DataDir, "rel-1")
- if err := os.WriteFile(path, []byte(`{"v":1,"host":"`), 0o600); err != nil {
+ for name, body := range map[string]string{
+ "a damaged journal": `{"v":1,"host":"`,
+ "another operation's": `{"v":1,"host":"` + buildHost + `","op":"rel-2","steps":{"materialize":"v1"}}`,
+ "another host's": `{"v":1,"host":"other.example","op":"rel-1","steps":{"materialize":"v1"}}`,
+ "no materialized release": `{"v":1,"host":"` + buildHost + `","op":"rel-1","steps":{}}`,
+ } {
+ if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ if _, ok, err := receipt(ctx, f.read()); ok || !errors.Is(err, hostrelocate.ErrRefused) {
+ t.Fatalf("%s: %v %v", name, ok, err)
+ }
+ if err := f.effects().EnsureLocal(ctx, f.read()); !errors.Is(err, hostrelocate.ErrRefused) {
+ t.Fatalf("%s: EnsureLocal reinstalled over it: %v", name, err)
+ }
+ }
+ if err := os.Remove(path); err != nil {
t.Fatal(err)
}
if _, ok, err := receipt(ctx, f.read()); ok || err != nil {
- t.Fatalf("a damaged journal: %v %v", ok, err)
+ t.Fatalf("no journal: %v %v", ok, err)
}
}
diff --git a/internal/host/relocationowners.go b/internal/host/relocationowners.go
index 28a0229c9..5dad4dd25 100644
--- a/internal/host/relocationowners.go
+++ b/internal/host/relocationowners.go
@@ -220,7 +220,13 @@ func relocationProbeClaim(c *hostclaim.Claim) (relocationProbe, error) {
// probe deadline and, on P's prepared opening, P's install manifest. The
// claim it reads was checked by Preflight before the attempt was counted
// (relocationProbeClaim); it is read again here, on the counted
-// observation.
+// observation. The install manifest read and a ProbeWake error that is
+// not a probe result (an activator or launch failure) run after the count,
+// so an attempt spent on transient I/O is an attempt all the same (final
+// review minor 11; the same shape as moves' probe): until claim ask C11
+// one attempt is one launch, and its follow-up (plan "After this plan",
+// C11) is where three attempts become three probes by moving these reads
+// before the count.
func (a ClaimAssembly) relocationAdmit(cfg RelocationAssemblyConfig) func(context.Context, hostclaim.Observed, bool) (uint64, bool, error) {
return func(ctx context.Context, o hostclaim.Observed, certified bool) (uint64, bool, error) {
if cfg.Activator == nil || cfg.Versions == nil || o.Claim == nil || o.Claim.Holder == nil {