carlos / platform Public

region ha: a replacement target activates its unopened installation at its new install stamp on the participant's path, so a target replaced at installed, or in probing before P's first admission, makes its local install instead of stalling on ErrNoRecord or ErrStampMismatch (Astra round 8, P2).

How it works:
- The local install (InstallLocal and EnsureLocal's repair, both e.Install) now activates before it captures the installation (relocationActivateInstalled).
- It uses the build's one mechanism, relocationActivateAt, the build's relocationActivate with its phases as a parameter:
  - main's guard.Activate, under the reader that holds every fresh read to relocationInstallAuthority;
  - the same checks: this box the executor, not stuck, not lost, P not opened, the holder's stamp the install stamp;
  - now also this box the named target_box;
  - main's Local check at that installation.
- The phases are widened only for the install's activation: exactly installed, or probing with P unopened. The build still activates at installing alone.
- The guard's own refusals stand: a retired or older stamp is never reactivated. The install maps those, and a stamp mismatch or a box the claim does not name, to hostrelocate.ErrRefused.
- For the original target, already activated by its build at the same stamp, the activation is the guard's no-op.

Tests:
- The replacement fixture no longer calls Guard.Activate. It drives dst2, a fresh box, through hostrelocate.Participant.Step, so production's activation runs.
- Both replacement cases establish P's files through the real provider. With the activation removed, they fail with "hostguard: no local record".
- New negatives:
  - a retired stamp: refused, wrapping ErrRetiredStamp, and not reactivated;
  - a box the claim does not name, and the replaced box: refused, no guard record made;
  - probing after P opened: activation refused.

The census covers neither function; no census or copy rows change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 6d4ba32f386be9db030228eeff38da2cd9f2174b parent ca9e54b
2 files changed, +169 −10
  • internal/host/relocationinstall.go +49 −3
  • internal/host/relocationprobefiles_linux_test.go +120 −7
diff --git a/internal/host/relocationinstall.go b/internal/host/relocationinstall.go
index 4b25b3503..d84abf804 100644
--- a/internal/host/relocationinstall.go
+++ b/internal/host/relocationinstall.go
@@ -108,15 +108,50 @@ func (r relocationActivationReader) Read(ctx context.Context, host string) (host
// Idempotent on the same stamp; a retired stamp is ErrRetiredStamp, never
// reactivated (ruling 52).
func (a ClaimAssembly) relocationActivate(cfg RelocationAssemblyConfig) func(context.Context, hostclaim.Observed) (hostguard.Captured, error) {
+ return a.relocationActivateAt(cfg, hostclaim.PhaseInstalling)
+}
+
+// relocationActivateInstalled is the same activation for a target that
+// resumes at installed without having built P (Astra round 8): a
+// replacement named at installed, or in probing before P's first
+// admission, which hostclaim.ReplaceTarget leaves at installed under a new
+// install stamp, with P and its prepared authority kept and no build to
+// run. The local install activates its unopened installation at that
+// stamp before it captures it, under the build's reader and checks with
+// the phase widened to exactly installed and probing, P not opened
+// (relocationInstallAuthority refuses an opened P), this box the executor
+// and the named current target, and the holder's stamp the install stamp.
+// For the original target, activated by its build at the same stamp, it
+// is the guard's idempotent no-op. A retired or older stamp is never
+// reactivated (ErrRetiredStamp, ErrOldStamp).
+func (a ClaimAssembly) relocationActivateInstalled(cfg RelocationAssemblyConfig) func(context.Context, hostclaim.Observed) (hostguard.Captured, error) {
+ return a.relocationActivateAt(cfg, hostclaim.PhaseInstalled, hostclaim.PhaseRelocatingProbing)
+}
+
+// relocationActivateAt is the one activation: this box's guard record at
+// install_stamp, every fresh read the guard makes held to this box's op at
+// one of phases (relocationInstallAuthority: the executor, not stuck, not
+// lost, P not opened, the holder's stamp the install stamp), this box the
+// named current target, and main's Local check at that installation.
+func (a ClaimAssembly) relocationActivateAt(cfg RelocationAssemblyConfig, phases ...string) func(context.Context, hostclaim.Observed) (hostguard.Captured, error) {
return func(ctx context.Context, o hostclaim.Observed) (hostguard.Captured, error) {
src := cfg.Source
if src.Local == nil || src.Box == "" || o.Claim == nil || o.Claim.Holder == nil {
return hostguard.Captured{}, ErrClaimStopUnavailable
}
op := o.Claim.Op.ID
+ authority := func(c *hostclaim.Claim) error {
+ if err := relocationInstallAuthority(c, op, src.Box, phases...); err != nil {
+ return err
+ }
+ if c.Op.Intent["target_box"] != src.Box {
+ return fmt.Errorf("%w: %s is not the named target", hostrelocate.ErrRefused, src.Box)
+ }
+ return nil
+ }
guard := a.Guard
guard.Claims = relocationActivationReader{claims: a.Claims, check: func(ctx context.Context, c *hostclaim.Claim) error {
- if err := relocationInstallAuthority(c, op, src.Box, hostclaim.PhaseInstalling); err != nil {
+ if err := authority(c); err != nil {
return err
}
return relocationInstallLocal(ctx, src, c, hostguard.Captured{Incarnation: c.Holder.Incarnation, Stamp: c.Holder.Stamp})
@@ -410,7 +445,9 @@ func relocationInstallSource(src hostbox.WorkSource, reg *registry.Registry) hos
// every write through the typed install authority (ruling 46):
//
// 1. the captured installation must be the active install stamp
-// (activated at installing; never reactivated, ruling 52);
+// (activated at installing by the build, or here for a replacement
+// that resumes at installed, relocationActivateInstalled; a retired
+// stamp is never reactivated, ruling 52);
// 2. the claim's typed inventory says nested (park.Local, once);
// 3. the build's scratch is removed (ruling 44);
// 4. in the install work: MintHeld first (the census initialises the
@@ -426,6 +463,7 @@ func relocationInstallSource(src hostbox.WorkSource, reg *registry.Registry) hos
//
// Idempotent: a pass that ends in ErrNotYet is repeated whole.
func (a ClaimAssembly) relocationInstall(cfg RelocationAssemblyConfig) func(context.Context, hostclaim.Observed) (hostclaim.LocalInstall, error) {
+ activate := a.relocationActivateInstalled(cfg)
render, materialize, verifyLive := cfg.render, cfg.materialize, cfg.verifyLive
if verifyLive == nil {
verifyLive = release.VerifyHeldLiveUnder
@@ -501,7 +539,15 @@ func (a ClaimAssembly) relocationInstall(cfg RelocationAssemblyConfig) func(cont
}
rt := rec.Route
paths := relocationPathsFor(cfg.DataDir, rt)
- captured, err := hostguard.Capture(a.Guard.DataDir, o.Host)
+ // The installation at the install stamp, activated here when this
+ // box resumes at installed without a build of its own (a
+ // replacement; the original target's build activated it already):
+ // a stamp the guard retired, or one older than its history, is
+ // never reactivated, and refuses.
+ captured, err := activate(ctx, o)
+ if errors.Is(err, hostguard.ErrRetiredStamp) || errors.Is(err, hostguard.ErrOldStamp) || errors.Is(err, hostguard.ErrStampMismatch) || errors.Is(err, hostguard.ErrNotParticipant) {
+ err = errors.Join(hostrelocate.ErrRefused, err)
+ }
if err != nil {
return hostclaim.LocalInstall{}, err
}
diff --git a/internal/host/relocationprobefiles_linux_test.go b/internal/host/relocationprobefiles_linux_test.go
index 8bd741de1..e853ead93 100644
--- a/internal/host/relocationprobefiles_linux_test.go
+++ b/internal/host/relocationprobefiles_linux_test.go
@@ -14,7 +14,10 @@ import (
"github.com/carlosframework/platform/internal/hostbox"
"github.com/carlosframework/platform/internal/hostclaim"
+ execlease "github.com/carlosframework/platform/internal/hostexec"
"github.com/carlosframework/platform/internal/hostfiles"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/hostrelocate"
"github.com/carlosframework/platform/internal/hostwrite"
"github.com/carlosframework/platform/internal/moves"
"github.com/carlosframework/platform/internal/registry"
@@ -78,9 +81,10 @@ func (f *probeFilesFixture) installAndProbe() {
// P, installed) names the replacement box as the executor, keeps P and its
// prepared authority, issues a new install stamp (the CAS's generation)
// and drops the receipt; ToInstalled does not run again. The replacement,
-// dst2, is a fresh box: its guard is activated at the new stamp, and it
-// makes its own local install, records it and enters probing. The replaced
-// target's receipt and journal bytes are kept for the negatives.
+// dst2, is a fresh box with no guard record: its participant's local
+// install activates the installation at the new stamp, makes the install,
+// records it and enters probing (participate). The replaced target's
+// receipt and journal bytes are kept for the negatives.
func newReplacedProbeFilesFixture(t *testing.T, opts buildOptions, phase string) *probeFilesFixture {
t.Helper()
f := newProbeFilesInstalled(t, opts)
@@ -121,14 +125,36 @@ func newReplacedProbeFilesFixture(t *testing.T, opts buildOptions, phase string)
t.Fatalf("the replacement keeps P's authority at the build stamp %d under a new stamp %d: %+v %v", build, c.Holder.Stamp, auth, err)
}
f.onBox(claimCensusBox(t), next)
- if err := f.a.Guard.Activate(t.Context(), buildHost, next.Box, nil); err != nil {
- t.Fatal(err)
- }
- f.installAndProbe()
+ f.participate()
f.bindProvider()
return f
}
+// participate drives this box's relocation participant from installed:
+// its first pass makes the local install (InstallLocal, which activates a
+// replacement's installation at its new stamp; nothing else does) and
+// journals the receipt, its second enters probing.
+func (f *probeFilesFixture) participate() {
+ f.t.Helper()
+ e := f.effects()
+ incarnation := f.read().Claim.Holder.Incarnation
+ e.Participation = func(context.Context, hostclaim.Observed) (execlease.State, error) {
+ return execlease.State{Binding: execlease.Binding{Host: buildHost, Incarnation: incarnation, Op: "rel-1"}}, nil
+ }
+ p := hostrelocate.Participant{Claims: f.claims, Box: f.actor.Box, Enrollment: f.actor.Enrollment, Effects: e, Now: time.Now}
+ o, err := p.Step(f.t.Context(), buildHost)
+ if err != nil || o.Claim.Op.Journal["local_install"] == "" {
+ f.t.Fatalf("the participant's install: %v", err)
+ }
+ if o, err = p.Step(f.t.Context(), buildHost); err != nil || o.Claim.Op.Phase != hostclaim.PhaseRelocatingProbing {
+ f.t.Fatalf("the participant's entry into probing: %v", err)
+ }
+ if err := json.Unmarshal([]byte(o.Claim.Op.Journal["local_install"]), &f.receipt); err != nil {
+ f.t.Fatal(err)
+ }
+ f.obs = o
+}
+
// bindProvider opens the real C8 provider the production source opens
// and binds the relocation's wake inputs to it.
func (f *probeFilesFixture) bindProvider() {
@@ -447,3 +473,90 @@ func TestRelocationReplacementRefusesTheWrongStamp(t *testing.T) {
})
}
}
+
+// Astra round 8: a replacement's activation is the install's, and no
+// broader. The box that runs it must be the claim's executor and named
+// target, P must be unopened, and a stamp the guard retired is never
+// reactivated; each refusal leaves the guard as it was.
+func TestRelocationReplacementActivationRefuses(t *testing.T) {
+ opts := buildOptions{DB: true, Files: map[string]string{"public/index.html": "<h1>shop</h1>"}, Platform: map[string]string{hostfiles.EnvItem: installEnv, hostfiles.WakeItem: installWake()}}
+ t.Run("a retired stamp", func(t *testing.T) {
+ f := newReplacedProbeFilesFixture(t, opts, hostclaim.PhaseInstalled)
+ if err := f.a.Guard.RetireInstallation(t.Context(), buildHost, func(*hostclaim.Claim) error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ f.update(func(c *hostclaim.Claim) error {
+ c.Op.Phase = hostclaim.PhaseInstalled
+ delete(c.Op.Journal, "local_install")
+ return nil
+ })
+ if _, err := f.effects().InstallLocal(t.Context(), f.read()); !errors.Is(err, hostrelocate.ErrRefused) || !errors.Is(err, hostguard.ErrRetiredStamp) {
+ t.Fatalf("a retired stamp was reactivated: %v", err)
+ }
+ if captured, err := hostguard.Capture(f.a.Guard.DataDir, buildHost); err == nil && captured.Stamp != 0 {
+ t.Fatalf("the retired installation is active again: %+v", captured)
+ }
+ })
+ // The claim keeps its executor and named target one box (Validate
+ // refuses them apart), so a box that is not named is one of two: the
+ // claim names another box, or the replaced box asks after the claim
+ // has named its replacement.
+ for name, tc := range map[string]struct {
+ change func(*hostclaim.Claim)
+ box string
+ }{
+ "a box the claim does not name": {change: func(c *hostclaim.Claim) { c.Op.Exec, c.Op.Intent["target_box"] = "dst3", "dst3" }},
+ "the replaced box": {change: func(*hostclaim.Claim) {}, box: "dst"},
+ } {
+ t.Run(name, func(t *testing.T) {
+ f := newReplacedNamedFixture(t, opts, tc.change)
+ if tc.box != "" {
+ f.cfg.Source.Box = tc.box
+ }
+ if _, err := f.a.relocationActivateInstalled(f.cfg)(t.Context(), f.read()); !errors.Is(err, hostrelocate.ErrRefused) {
+ t.Fatalf("activated: %v", err)
+ }
+ if _, err := f.effects().InstallLocal(t.Context(), f.read()); err == nil {
+ t.Fatal("installed")
+ }
+ if _, err := hostguard.Capture(f.a.Guard.DataDir, buildHost); !errors.Is(err, hostguard.ErrNoRecord) {
+ t.Fatalf("a guard record was made: %v", err)
+ }
+ })
+ }
+ t.Run("probing after P opened", func(t *testing.T) {
+ f := newReplacedProbeFilesFixture(t, opts, hostclaim.PhaseRelocatingProbing)
+ if _, err := f.probe(firstProbeEnv); err != nil {
+ t.Fatal(err)
+ }
+ if !hostclaim.PreparedOpened(f.read().Claim) {
+ t.Fatal("P is not opened")
+ }
+ if _, err := f.a.relocationActivateInstalled(f.cfg)(t.Context(), f.read()); !errors.Is(err, hostrelocate.ErrRefused) {
+ t.Fatalf("activated after P opened: %v", err)
+ }
+ })
+}
+
+// newReplacedNamedFixture is the replacement at installed of
+// newReplacedProbeFilesFixture on a fresh box dst2, stopped before dst2's
+// participant runs, with change applied to the claim.
+func newReplacedNamedFixture(t *testing.T, opts buildOptions, change func(*hostclaim.Claim)) *probeFilesFixture {
+ t.Helper()
+ f := newProbeFilesInstalled(t, opts)
+ next := hostbox.Actor{Box: "dst2", Enrollment: strings.Repeat("b", 32), Region: "ie", Revision: 4}
+ f.update(func(c *hostclaim.Claim) error {
+ stamp := c.Gen + 1
+ c.Op.Exec, c.Op.Phase = next.Box, hostclaim.PhaseInstalled
+ c.Op.Intent["target_box"], c.Op.Intent["target_enrollment"] = next.Box, next.Enrollment
+ c.Op.Participants = []string{"src", next.Box}
+ c.Op.Journal["exec_ack."+next.Box] = `{"op":"rel-1"}`
+ c.Holder.Stamp = stamp
+ c.Op.Journal["install_stamp"] = strconv.FormatUint(stamp, 10)
+ delete(c.Op.Journal, "local_install")
+ change(c)
+ return nil
+ })
+ f.onBox(claimCensusBox(t), next)
+ return f
+}