package host import ( "context" "crypto/ed25519" "strconv" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/moves" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) // claimAdmissionHandover is the wake's AfterAdmission hook. A landing // target's first admission, whose predecessor writer is the move's source on // another box, goes to crossBox when the moves C1 integration bound one. Every // other admission keeps main's local cold handover, which needs this box's own // completed fence receipt for the predecessor session. func claimAdmissionHandover(box string, local, crossBox func(context.Context, hostclaim.Observed, hostclaim.Observed) error) func(context.Context, hostclaim.Observed, hostclaim.Observed) error { return func(ctx context.Context, prior, admitted hostclaim.Observed) error { if crossBox != nil && claimRemotePredecessor(prior, box) { return crossBox(ctx, prior, admitted) } return local(ctx, prior, admitted) } } // claimCrossBoxHandover authorizes a landing target's first cold admission // from the sealed cut and the source's stop evidence instead of a local fence // receipt (claim Plan 9 D2, moves ruling H2). The target never ran the // source's session, so it can never hold that session's fence receipt. // // Authority: op.exec moves only by a CAS citing stop evidence (claim §4, // §6.4a "AdmitWriter"; moves K3, K4). The flip handed op.exec to this box // citing stopped-source.json (moves §7.5); the cut journalled the stopped // session and the seal (moves §7.4, K13). The admission's coverage is sealed // exactly when the seal was built from the stopped database (claim §6.4a // "Admissions, coverage and retirement" item 2). The first probe keeps the // installation stamp (item 4), so the local record this box initialized for // the landing installation is the one the session runs under. type claimCrossBoxHandover struct { RecoveryRebind func(context.Context, recoveryRebindRequest) error Launches *hostbox.LaunchJournal afterRecoveryRows func() error PublicKey ed25519.PublicKey FenceAuthority hostclaim.FenceAuthority Directory hostactors.Directory Guard hostguard.Guard Evidence release.Store Inventory *hostbox.StopInventoryDB Registry *registry.Registry Identity hostbox.IdentityProvider Box string } func (h claimCrossBoxHandover) After(ctx context.Context, prior, admitted hostclaim.Observed) error { if claimRecoveryObservation(prior) { return h.afterRecovery(ctx, prior, admitted) } if h.Evidence == nil || h.Box == "" || prior.Claim == nil || prior.Claim.Holder == nil || admitted.Claim == nil || admitted.Claim.Holder == nil || prior.Host != admitted.Host || prior.Absent || admitted.Absent { return hostbox.ErrChanged } seal, err := h.sourceEvidence(ctx, prior) if err != nil { return err } op, old := prior.Claim.Op, prior.Claim.Holder source := op.Intent["source"] next, ac := admitted.Claim.Holder, admitted.Claim if ac.Op.ID != op.ID || ac.Op.Kind != op.Kind || ac.Op.Phase != op.Phase || ac.Op.Exec != h.Box || next.Incarnation != old.Incarnation || next.Stamp != old.Stamp || next.Session <= old.Session || next.WriterBox != h.Box || len(next.Admissions) == 0 { return hostbox.ErrChanged } predecessor := hostclaim.AdmissionRecord{Session: old.Session, Box: old.WriterBox, Generation: old.Generation} if len(old.Admissions) > 0 { predecessor = old.Admissions[len(old.Admissions)-1] } entry := next.Admissions[len(next.Admissions)-1] if predecessor.Session != old.Session || predecessor.Box != source || entry.Session != next.Session || entry.Box != h.Box || entry.Mode != "cold" || entry.Generation != next.Generation || entry.Predecessor != predecessor.Generation { return hostbox.ErrChanged } if (entry.Coverage != "sealed" && entry.Coverage != "") || (entry.Coverage == "sealed") != seal.FromDB { return hostbox.ErrChanged } // The kept stamp is this box's active landing installation, and the // admitted session is still the claim's under the guard. return h.Guard.With(ctx, admitted.Host, hostguard.Captured{Incarnation: next.Incarnation, Stamp: next.Stamp}, func(c *hostclaim.Claim) error { if c.Holder.Session != next.Session || c.Holder.Generation != next.Generation || c.Holder.WriterBox != h.Box || c.Op.ID != op.ID || c.Op.Kind != op.Kind || c.Op.Exec != h.Box { return hostbox.ErrChanged } return nil }, func() error { return nil }) } func (h claimCrossBoxHandover) sourceEvidence(ctx context.Context, prior hostclaim.Observed) (moves.Seal, error) { if h.Evidence == nil || h.Box == "" || prior.Absent || prior.Claim == nil || prior.Claim.Holder == nil { return moves.Seal{}, hostbox.ErrChanged } op, old := prior.Claim.Op, prior.Claim.Holder source := op.Intent["source"] if op.Kind != hostclaim.KindLanding || op.ID == "" || op.Phase != hostclaim.PhaseLandingProbing || op.Exec != h.Box || len(op.Participants) != 2 || op.Participants[0] != source || op.Participants[1] != h.Box || op.Intent["target"] != h.Box || source == "" || source == h.Box || old.WriterBox != source { return moves.Seal{}, hostbox.ErrChanged } cut, err := strconv.ParseUint(op.Journal[hostclaim.JournalCutSession], 10, 64) if err != nil || cut != old.Session { return moves.Seal{}, hostbox.ErrChanged } seal, err := moves.DecodeSeal(op.Journal[hostclaim.JournalSeal]) if err != nil || !seal.HasFiles() { return moves.Seal{}, hostbox.ErrChanged } key := op.Journal[hostclaim.JournalEvidenceSource] if key == "" { return moves.Seal{}, hostbox.ErrChanged } report, err := readStoppedReport(ctx, h.Evidence, key) if err != nil { return moves.Seal{}, err } if report.Role != "source" || report.Box != source || report.Host != prior.Host || report.OpID != op.ID || report.Incarnation != old.Incarnation || report.FinalTXID != seal.N { return moves.Seal{}, hostbox.ErrChanged } return seal, nil } func (h claimCrossBoxHandover) Before(ctx context.Context, prior hostclaim.Observed) error { if claimRecoveryObservation(prior) { return h.beforeRecovery(ctx, prior) } if _, err := h.sourceEvidence(ctx, prior); err != nil { return err } old := prior.Claim.Holder return h.Guard.With(ctx, prior.Host, hostguard.Captured{Incarnation: old.Incarnation, Stamp: old.Stamp}, func(c *hostclaim.Claim) error { if c.Op.ID != prior.Claim.Op.ID || c.Op.Kind != hostclaim.KindLanding || c.Op.Phase != hostclaim.PhaseLandingProbing || c.Op.Exec != h.Box || c.Holder.WriterBox != old.WriterBox || c.Holder.Session != old.Session || c.Holder.Generation != old.Generation { return hostbox.ErrChanged } return nil }, func() error { return nil }) } // Cross-box source evidence never substitutes this target's controller tuple // for a predecessor that ran on another box. All other shapes keep Stop. func claimPredecessorStop(w *ClaimWakeAssembly, box string, crossBox func(context.Context, hostclaim.Observed) error) func(context.Context, hostclaim.Observed) error { return func(ctx context.Context, prior hostclaim.Observed) error { if crossBox != nil && claimRemotePredecessor(prior, box) { return crossBox(ctx, prior) } return w.Stop(ctx, prior) } } // wireClaimAdmission routes the source proof to its operation owner before // lease acquisition and checks the target installation after admission. // With completed set, region HA's N6 handover goes in front of it // (wireCompletedHandover). func wireClaimAdmission(w *ClaimWakeAssembly, box string, local func(context.Context, hostclaim.Observed, hostclaim.Observed) error, landing claimCrossBoxHandover, relocation *ClaimRelocationOwners, completed *relocationCompletedHandover) { before := landing.Before after := landing.After if relocation != nil { before, after = relocationBefore(landing, relocation, box), relocationAfter(landing, relocation, box) } w.recoveryIntent = landing.prepareRecovery w.recoveryPrior = landing.pendingRecovery w.recoveryPrelaunched = landing.completeRecovery w.BeforeAdmission = claimPredecessorStop(w, box, before) w.AfterAdmission = claimAdmissionHandover(box, func(ctx context.Context, prior, admitted hostclaim.Observed) error { if claimInitialSourceRecovery(prior, box) { return landing.afterRecovery(ctx, prior, admitted) } return local(ctx, prior, admitted) }, after) if completed != nil { wireCompletedHandover(w, completed) } } // wireCompletedHandover is region HA N6 in front of whatever admission // hooks w already has: the first serving wake after a relocation completed // on this box hands over from the probe's existing receipt; every other // prior keeps those hooks unchanged (with no BeforeAdmission, main's // Wake.Stop). It is wired whenever claim mode is on, not only with region // HA on (ruling 82's reasoning, Task 9 review): it serves a relocation // that has already completed, and without a completion record it does // nothing. A serving prior on this box whose completion record cannot be // read is refused, never routed to main's stop (absence is never // evidence): that stop would replace the probe's fence. func wireCompletedHandover(w *ClaimWakeAssembly, completed *relocationCompletedHandover) { before, after := w.BeforeAdmission, w.AfterAdmission completedAfter := completed.After() w.BeforeAdmission = func(ctx context.Context, prior hostclaim.Observed) error { if _, ok, err := completed.completedPrior(prior); err != nil { return err } else if ok { return completed.Before(ctx, prior) } if before != nil { return before(ctx, prior) } return w.Stop(ctx, prior) } w.AfterAdmission = func(ctx context.Context, prior, admitted hostclaim.Observed) error { if _, ok, err := completed.completedPrior(prior); err != nil { return err } else if ok { return completedAfter(ctx, prior, admitted) } if after == nil { return hostbox.ErrChanged } return after(ctx, prior, admitted) } } // wireProductionAdmission is OpenProductionClaimAssembly's admission // wiring: with moves C1 on, wireClaimAdmission over the cross-box landing // handover and, with region HA's owners bound, the relocating routing; // with it off, main's hooks as they are. N6 goes in front in both, so a // relocation completed while region HA was on is handed over from its // probe's receipt at the first serving wake after region HA is turned off. func wireProductionAdmission(w *ClaimWakeAssembly, box string, movesC1 bool, landing claimCrossBoxHandover, relocation *ClaimRelocationOwners, completed *relocationCompletedHandover) { if movesC1 { wireClaimAdmission(w, box, w.AfterAdmission, landing, relocation, completed) return } wireCompletedHandover(w, completed) } func claimRemotePredecessor(prior hostclaim.Observed, box string) bool { if prior.Claim == nil || prior.Claim.Holder == nil || prior.Claim.Holder.WriterBox == box { return false } return (prior.Claim.Op.Kind == hostclaim.KindRestoring && prior.Claim.Holder.WriterBox != "" && prior.Claim.Holder.WriterBox == prior.Claim.Op.Intent["target"]) || prior.Claim.Op.Kind == hostclaim.KindRelocating || (prior.Claim.Op.Kind == hostclaim.KindLanding && prior.Claim.Holder.WriterBox != "") } // relocationBefore and relocationAfter are wireClaimAdmission's relocating // routing (Plan 11), unchanged, named so the completed-relocation branch // (N6) can fall through to them. func relocationBefore(landing claimCrossBoxHandover, relocation *ClaimRelocationOwners, box string) func(context.Context, hostclaim.Observed) error { return func(ctx context.Context, prior hostclaim.Observed) error { if prior.Claim.Op.Kind != hostclaim.KindRelocating { return landing.Before(ctx, prior) } if relocation.BeforeAdmission == nil || prior.Claim.Op.Exec != box || prior.Claim.Op.Phase != "probing" { return hostbox.ErrChanged } return relocation.BeforeAdmission(ctx, prior) } } func relocationAfter(landing claimCrossBoxHandover, relocation *ClaimRelocationOwners, box string) func(context.Context, hostclaim.Observed, hostclaim.Observed) error { return func(ctx context.Context, prior, admitted hostclaim.Observed) error { if prior.Claim.Op.Kind != hostclaim.KindRelocating { return landing.After(ctx, prior, admitted) } if relocation.AfterAdmission == nil || prior.Claim.Op.Exec != box || prior.Claim.Op.Phase != "probing" { return hostbox.ErrChanged } return relocation.AfterAdmission(ctx, prior, admitted) } }