carlos / platform Public

region ha: the relocation probe's launch names the build its executable was hashed as (Staged), so the activator's claimed-build check admits the probe start with BeforeExec set (L18 joint round 4, item 1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 3320b9ce420534df9d925e3fb3e2cf5205d3b4fd parent 26f6cdb
2 files changed, +98 −15
  • internal/host/relocationwake.go +1 −1
  • internal/host/relocationwake_test.go +97 −14
diff --git a/internal/host/relocationwake.go b/internal/host/relocationwake.go
index d3eb56776..540242350 100644
--- a/internal/host/relocationwake.go
+++ b/internal/host/relocationwake.go
@@ -144,7 +144,7 @@ func (r *RelocationWakeInputs) Select(ctx context.Context, rt registry.Route, o
}
prepared := c.Op.Journal["prepared"]
in := ClaimLaunchInputs{Roles: []string{"main"}, Identity: l.Identity, Executable: l.Executable, ExecutableSHA256: l.ExecutableSHA256, Args: append([]string(nil), l.Args...), Env: append([]string(nil), l.Env...),
- DBPath: rt.DBPath, Version: l.Version, Limit: l.Limit, ReadyPath: "/"}
+ DBPath: rt.DBPath, Version: l.Version, Staged: l.Staged, Limit: l.Limit, ReadyPath: "/"}
adm := hostclaim.Admission{ObservedOpID: c.Op.ID, ObservedPhase: hostclaim.PhaseRelocatingProbing, ObservedSession: h.Session, ObservedGeneration: h.Generation,
Box: r.Box, Mode: "cold", Region: r.Region, PredecessorRegion: r.Region}
if n := len(h.Admissions); n > 0 {
diff --git a/internal/host/relocationwake_test.go b/internal/host/relocationwake_test.go
index a18504b56..b8312514a 100644
--- a/internal/host/relocationwake_test.go
+++ b/internal/host/relocationwake_test.go
@@ -24,18 +24,34 @@ import (
"github.com/carlosframework/platform/internal/release"
)
-// Finding 6, ruling 47: P's first probe publishes through the shared
-// assembly. The claim's own runtime (the recovery fixture's controller)
-// admits P's opening through the relocation dispatch, binds region HA's
-// attempt journal to the probe start, and Materialize, under the admitted
-// spawn selection, runs moves' C8 Establish and writes P's marker: P as
-// the marker's op, the install manifest's pin, prepared_authority's
-// digest. Publish (the pre-replication call) finds that marker and
-// records the receipt; PublicationEvidence then passes. Publish without
-// Materialize's fence refuses.
-func TestRelocationFirstProbePublishesThroughTheAssembly(t *testing.T) {
- f := newRecoveryFixture(t)
+// relocationProbeRig is P's first probe on box-1 through the claim's own
+// runtime (the recovery fixture's controller), the relocation owners bound
+// as claimGroupDWiring binds them, the probe attempt counted and pending.
+type relocationProbeRig struct {
+ f *recoveryFixture
+ bucket *leasetest.VersionedBucket
+ files *release.DirStore
+ c8 *roFiles
+ inputs *RelocationWakeInputs
+ c *recoveryController
+ h *hostclaim.Holder
+ o hostclaim.Observed
+ im moves.InstallManifest
+ auth hostclaim.PreparedAuthority
+ route registry.Route
+}
+
+// roProbeLaunch is the serving launch source's result for the rig: the
+// live link hashed as build v1, staged under v1.
+func roProbeLaunch() ClaimServingLaunch {
+ return ClaimServingLaunch{Identity: hostwrite.Identity{Config: "sha256:config", Release: "sha256:release"}, Executable: "/usr/bin/true", ExecutableSHA256: strings.Repeat("a", 64), Version: "v1",
+ Staged: release.StagedIdentity{Version: "v1", StagedPath: "/opt/carlos/versions/v1/bab/jam", Digest: strings.Repeat("a", 64)}}
+}
+
+func newRelocationProbeRig(t *testing.T, launch ClaimServingLaunch) *relocationProbeRig {
+ t.Helper()
ctx := t.Context()
+ f := newRecoveryFixture(t)
bucket := leasetest.NewVersionedBucket()
bucket.AddBucket("data")
files := release.NewDirStore(t.TempDir())
@@ -72,9 +88,7 @@ func TestRelocationFirstProbePublishesThroughTheAssembly(t *testing.T) {
}
c8 := &roFiles{}
inputs := &RelocationWakeInputs{
- Launch: func(context.Context, registry.Route) (ClaimServingLaunch, error) {
- return ClaimServingLaunch{Identity: hostwrite.Identity{Config: "sha256:config", Release: "sha256:release"}, Executable: "/usr/bin/true", ExecutableSHA256: strings.Repeat("a", 64), Version: "v1"}, nil
- },
+ Launch: func(context.Context, registry.Route) (ClaimServingLaunch, error) { return launch, nil },
FilesManifest: c8, Markers: plan9D5Markers{bucket}, Claims: f.claims, FilesStore: files, Data: bucket, Bucket: "data", Region: "ie", Box: "box-1",
NewID: f.newID, Now: func() time.Time { return f.now }, Releases: &roReleases{}}
attempts := &relocationProbeAttempts{Claims: f.claims, DataDir: t.TempDir(), Box: "box-1", NewID: f.newID, Now: func() time.Time { return f.now }}
@@ -92,7 +106,22 @@ func TestRelocationFirstProbePublishesThroughTheAssembly(t *testing.T) {
c.wake.pending[recoveryHost] = &claimWakeAttempt{observed: o, held: recoveryHeld{}, ctx: live, cancel: cancel, done: done}
c.wake.mu.Unlock()
route := registry.Route{Host: recoveryHost, Kind: "instance", App: "jam", Account: "bab", Status: "relocating", HoldOp: roOp, Socket: filepath.Join(f.guard.DataDir, "app.sock"), DBPath: filepath.Join(f.guard.DataDir, "state", "data.db")}
+ return &relocationProbeRig{f: f, bucket: bucket, files: files, c8: c8, inputs: inputs, c: c, h: h, o: o, im: im, auth: auth, route: route}
+}
+// Finding 6, ruling 47: P's first probe publishes through the shared
+// assembly. The claim's own runtime (the recovery fixture's controller)
+// admits P's opening through the relocation dispatch, binds region HA's
+// attempt journal to the probe start, and Materialize, under the admitted
+// spawn selection, runs moves' C8 Establish and writes P's marker: P as
+// the marker's op, the install manifest's pin, prepared_authority's
+// digest. Publish (the pre-replication call) finds that marker and
+// records the receipt; PublicationEvidence then passes. Publish without
+// Materialize's fence refuses.
+func TestRelocationFirstProbePublishesThroughTheAssembly(t *testing.T) {
+ r := newRelocationProbeRig(t, roProbeLaunch())
+ ctx := t.Context()
+ f, bucket, files, c8, inputs, c, h, o, im, auth, route := r.f, r.bucket, r.files, r.c8, r.inputs, r.c, r.h, r.o, r.im, r.auth, r.route
admitted, err := c.runtime.Admit(ctx, route, o, roProbe)
if err != nil {
t.Fatal(err)
@@ -153,6 +182,60 @@ func TestRelocationFirstProbePublishesThroughTheAssembly(t *testing.T) {
}
}
+// L18 joint round, final 4, item 1 (ruling R-EXEC-CLAIMVERSION): the
+// relocation probe's start names the file its launch source hashed. With
+// the production BeforeExec hook set, the activator refuses any claimed
+// start whose Staged.Version is not its Version (Service.claimedBuild,
+// run on the start the runtime's Prepare returns), so a probe selection
+// that drops Staged never starts. The start Prepare hands the activator
+// for P's prepared opening carries the launch's Staged, and so does every
+// selection: a later cold attempt's and an empty P's.
+func TestTheRelocationProbeStartNamesTheBuildItHashed(t *testing.T) {
+ ctx := t.Context()
+ launch := roProbeLaunch()
+ r := newRelocationProbeRig(t, launch)
+ admitted, err := r.c.runtime.Admit(ctx, r.route, r.o, roProbe)
+ if err != nil {
+ t.Fatal(err)
+ }
+ s, err := r.c.runtime.Prepare(ctx, r.route, admitted, roProbe)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // claimedBuild's refusal, as the activator applies it with BeforeExec set.
+ if s.Staged != launch.Staged || s.Version != launch.Version || s.Staged.Version != s.Version {
+ t.Fatalf("the probe start names build %q but staged %+v; the launch hashed %+v", s.Version, s.Staged, launch.Staged)
+ }
+
+ for _, o := range []roOptions{{}, {empty: true}} {
+ f := newROFixture(t, o)
+ source := f.inputs.Launch
+ staged := release.StagedIdentity{Version: "v1", StagedPath: "/opt/carlos/versions/v1/acct/app", Digest: strings.Repeat("b", 64)}
+ f.inputs.Launch = func(ctx context.Context, rt registry.Route) (ClaimServingLaunch, error) {
+ l, err := source(ctx, rt)
+ l.Staged = staged
+ return l, err
+ }
+ first, err := f.inputs.Select(ctx, f.route, f.read(), roProbe)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if first.Staged != staged || first.Staged.Version != first.Version {
+ t.Fatalf("empty=%v: the first attempt names build %q but staged %+v", o.empty, first.Version, first.Staged)
+ }
+ if o.empty {
+ continue
+ }
+ again, err := f.inputs.Select(ctx, f.route, f.admit(f.read(), first), roProbe)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if again.Prepared || again.Staged != staged || again.Staged.Version != again.Version {
+ t.Fatalf("a later attempt (prepared %v) names build %q but staged %+v", again.Prepared, again.Version, again.Staged)
+ }
+ }
+}
+
// Ruling 47: a later attempt's fresh generation, and an empty P's opening,
// publish through main's fenced writer only after their first root.
func TestRelocationLaterAttemptsPublishFenced(t *testing.T) {