package host import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "path/filepath" "sort" "strings" "sync" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/release" ) // These selectors contain no local paths. Root must independently resolve the // installation from its state inventory before returning an authorization. type servingArtifactIdentity = hostbox.ServingArtifactIdentity type servingLegacyPredecessor = hostbox.ServingLegacyPredecessor type servingParkAuthorization = hostbox.ServingParkResult // A zero predecessor asks Park to resume an existing record for successor; // it must never authorize a new retirement. Complete independently rereads // the claim's paired tags. Neither call may run while Work is held. type servingDatabaseParker interface { Park(context.Context, servingArtifactIdentity, servingArtifactIdentity) (servingParkAuthorization, error) ParkLegacy(context.Context, servingArtifactIdentity, servingLegacyPredecessor) (servingParkAuthorization, error) Complete(context.Context, string, servingArtifactIdentity) error } type claimServingArtifactRetirement struct { Claims release.Store // The assembly's authorized claim writer, never a new bucket client. Guard hostguard.Guard DataDir, Box, Region string Root servingDatabaseParker mu sync.Mutex completed map[string]servingParkAuthorization } func servingRetirementID(prior, original servingArtifactIdentity, legacy bool) string { body, _ := json.Marshal(struct { Prior, Successor servingArtifactIdentity Legacy bool }{prior, original, legacy}) sum := sha256.Sum256(body) return hex.EncodeToString(sum[:]) } func servingWriterIdentity(o hostclaim.Observed, box, region string) (servingArtifactIdentity, error) { if o.Absent || o.Host == "" || o.Claim == nil || o.Claim.Host != o.Host || o.Claim.Holder == nil || box == "" || region == "" { return servingArtifactIdentity{}, hostbox.ErrChanged } c, h := o.Claim, o.Claim.Holder if c.Op.Kind != hostclaim.KindServing || c.Op.ID != "" || c.Op.Phase != "" || c.Op.Exec != box || h.WriterBox != box || h.Incarnation == "" || h.Location == "" || h.Generation == "" || h.Session == 0 || h.Stamp != h.Session { return servingArtifactIdentity{}, hostbox.ErrChanged } found := 0 for _, a := range h.Admissions { if a.Session != h.Session { continue } if a.Box != box || a.Region != region || a.Generation != h.Generation || a.Mode != "cold" { return servingArtifactIdentity{}, hostbox.ErrChanged } found++ } if found != 1 { return servingArtifactIdentity{}, hostbox.ErrChanged } return servingArtifactIdentity{Host: o.Host, Box: box, Region: region, Incarnation: h.Incarnation, Location: h.Location, Generation: h.Generation, Stamp: h.Stamp, Session: h.Session}, nil } func (r *claimServingArtifactRetirement) snapshot(ctx context.Context, admitted hostclaim.Observed, in ClaimLaunchInputs) (hostclaim.Observed, servingArtifactIdentity, string, uint64, error) { fail := func(err error) (hostclaim.Observed, servingArtifactIdentity, string, uint64, error) { return hostclaim.Observed{}, servingArtifactIdentity{}, "", 0, err } if ctx == nil || r == nil || r.Claims == nil || r.Root == nil || !filepath.IsAbs(r.DataDir) || r.Guard.DataDir != r.DataDir { return fail(hostbox.ErrUnsupported) } expected, err := servingWriterIdentity(admitted, r.Box, r.Region) if err != nil { return fail(err) } if in.DBPath != provision.NestedDBPath(r.DataDir, admitted.Host) { return fail(hostbox.ErrChanged) } db, err := filepath.Rel(r.DataDir, in.DBPath) if err != nil || !filepath.IsLocal(db) { return fail(hostbox.ErrChanged) } db = filepath.ToSlash(db) fresh, err := hostclaim.Read(ctx, r.Claims, admitted.Host) if err != nil { return fail(err) } actual, err := servingWriterIdentity(fresh, r.Box, r.Region) if err != nil || actual != expected { return fail(hostbox.ErrChanged) } a, err := servingInventoryPair(fresh.Claim.Inventory[r.Box], db) if err != nil || a > actual.Stamp { return fail(hostbox.ErrChanged) } return fresh, actual, db, a, nil } func servingPredecessor(c *hostclaim.Claim, current servingArtifactIdentity, stamp uint64, generation string) (servingArtifactIdentity, error) { if stamp == 0 || stamp >= current.Stamp { return servingArtifactIdentity{}, hostbox.ErrChanged } var out servingArtifactIdentity count := 0 for _, a := range c.Holder.Admissions { if a.Session != stamp { continue } if a.Mode != "cold" || a.Box != current.Box || a.Region != current.Region || a.Generation == "" || a.Generation == current.Generation || (generation != "" && a.Generation != generation) { return out, hostbox.ErrChanged } out = current out.Session, out.Stamp, out.Generation = a.Session, stamp, a.Generation count++ } if count != 1 { return servingArtifactIdentity{}, hostbox.ErrChanged } return out, nil } func (r *claimServingArtifactRetirement) stopped(ctx context.Context, prior servingArtifactIdentity) error { receipts, err := r.Guard.CompletedServingStops(ctx, prior.Host, hostguard.Captured{Incarnation: prior.Incarnation, Stamp: prior.Stamp}, prior.Session, prior.Box) if err != nil { return err } if len(receipts) == 0 { return hostguard.ErrNoFenceReceipt } for _, receipt := range receipts { if receipt.Target.Region != prior.Region || verifyRetirementReceipt(ctx, receipt) != nil { return ErrClaimStopReceipt } } return nil } func (r *claimServingArtifactRetirement) Retire(ctx context.Context, admitted hostclaim.Observed, in ClaimLaunchInputs, generation string) error { if generation == "" { return hostbox.ErrChanged } return r.recover(ctx, admitted, in, generation) } func (r *claimServingArtifactRetirement) Recover(ctx context.Context, admitted hostclaim.Observed, in ClaimLaunchInputs) error { return r.recover(ctx, admitted, in, "") } func (r *claimServingArtifactRetirement) recover(ctx context.Context, admitted hostclaim.Observed, in ClaimLaunchInputs, generation string) error { r.forgetCompletion(admitted.Host) fresh, current, _, stamp, err := r.snapshot(ctx, admitted, in) if err != nil { return err } if stamp == 0 { // Legacy authority is available only after PrepareState. In particular, // missing tags never authorize deleting an existing generation marker. if generation != "" { return hostbox.ErrChanged } return nil } var prior servingArtifactIdentity if stamp != current.Stamp { prior, err = servingPredecessor(fresh.Claim, current, stamp, generation) if err != nil { return err } if err = r.stopped(ctx, prior); err != nil { return err } } local, err := servingAgentDirectory(in.DBPath) if err != nil { return err } authorization, err := r.Root.Park(ctx, current, prior) if err != nil { return err } if generation != "" && authorization.Predecessor.Generation != generation { return hostbox.ErrChanged } return r.finish(ctx, admitted, in, authorization, local, nil) } func (r *claimServingArtifactRetirement) BeforeLegacyInstall(ctx context.Context, admitted hostclaim.Observed, in ClaimLaunchInputs, work hostwrite.Work) error { r.forgetCompletion(admitted.Host) _, current, _, stamp, err := r.snapshot(ctx, admitted, in) if err != nil { return err } if work == nil { return hostbox.ErrUnsupported } if stamp != 0 { return r.Recover(ctx, admitted, in) } prior := servingLegacyPredecessor{Session: in.Admission.ObservedSession, Generation: in.Admission.ObservedGeneration, Empty: in.Empty && in.Admission.ObservedSession == 0} if (prior.Session == 0) != (prior.Generation == "") || prior.Session >= current.Session || (prior.Empty && prior.Session != 0) { return hostbox.ErrChanged } local, err := servingAgentDirectory(in.DBPath) if err != nil { return err } authorization, err := r.Root.ParkLegacy(ctx, current, prior) if err != nil { return err } if !authorization.Legacy { return hostbox.ErrChanged } return r.finish(ctx, admitted, in, authorization, local, work) } func (r *claimServingArtifactRetirement) validateAuthorization(ctx context.Context, fresh hostclaim.Observed, current servingArtifactIdentity, a servingParkAuthorization) error { original := a.OriginalSuccessor if a.TagsFrom > current.Stamp { return hostbox.ErrChanged } if a.TagsFrom != 0 && a.TagsFrom != current.Stamp && a.TagsFrom != a.Predecessor.Stamp { if a.TagsFrom < original.Stamp { return hostbox.ErrChanged } if _, err := servingPredecessor(fresh.Claim, current, a.TagsFrom, ""); err != nil { return err } } if !a.Legacy && a.TagsFrom == 0 { return hostbox.ErrChanged } if a.Successor != current || original.Host != current.Host || original.Box != current.Box || original.Region != current.Region || original.Incarnation != current.Incarnation || original.Location != current.Location || original.Stamp == 0 || original.Session != original.Stamp || original.Generation == "" || original.Session > current.Session || a.Directory.Device == 0 || a.Directory.Inode == 0 || a.Directory.Mount == 0 || a.ID != servingRetirementID(a.Predecessor, original, a.Legacy) { return hostbox.ErrChanged } if original.Session == current.Session && original != current { return hostbox.ErrChanged } // A continuation preserves the original record identity; it cannot rename // that record to the next admission. Root authenticates its own continuation. if original.Session < current.Session { if _, err := servingPredecessor(fresh.Claim, current, original.Stamp, original.Generation); err != nil { return err } } if !a.Legacy { prior, err := servingPredecessor(fresh.Claim, current, a.Predecessor.Stamp, a.Predecessor.Generation) if err != nil || prior != a.Predecessor || prior.Stamp >= original.Stamp { return hostbox.ErrChanged } return r.stopped(ctx, prior) } if a.Predecessor != (servingArtifactIdentity{}) { return hostbox.ErrChanged } return nil } func (r *claimServingArtifactRetirement) finish(ctx context.Context, admitted hostclaim.Observed, in ClaimLaunchInputs, a servingParkAuthorization, local servingArtifactDirectory, work hostwrite.Work) error { fresh, current, _, _, err := r.snapshot(ctx, admitted, in) if err != nil { return err } if err = r.validateAuthorization(ctx, fresh, current, a); err != nil { return err } // Mount IDs belong to a namespace. Root keeps its original mount identity; // the hardened agent pins its own mount before the RPC and checks it again // during enrollment. Both views must still name the same device and inode. if a.Directory.Device != local.Device || a.Directory.Inode != local.Inode || local.Mount == 0 { return hostbox.ErrChanged } a.Directory = local enroll := func() error { return r.enrollPairLocked(ctx, admitted, in, a) } if work != nil { err = work.With(ctx, in.Identity, enroll) } else { err = r.Guard.With(ctx, current.Host, hostguard.Captured{Incarnation: current.Incarnation, Stamp: current.Stamp}, func(c *hostclaim.Claim) error { identity, err := servingWriterIdentity(hostclaim.Observed{Host: current.Host, Claim: c}, r.Box, r.Region) if err != nil || identity != current { return hostbox.ErrChanged } return nil }, enroll) } if err != nil { return err } if err = r.Root.Complete(ctx, a.ID, current); err != nil { return err } r.mu.Lock() if r.completed == nil { r.completed = make(map[string]servingParkAuthorization) } r.completed[current.Host] = a r.mu.Unlock() return nil } // enrollPairLocked runs only under the selected Work or the matching Guard. // The root authorization proves the old sources have been consumed. The // descriptor check prevents a replacement inode from reaching purgeDB. func (r *claimServingArtifactRetirement) enrollPairLocked(ctx context.Context, admitted hostclaim.Observed, in ClaimLaunchInputs, a servingParkAuthorization) error { fresh, current, db, stamp, err := r.snapshot(ctx, admitted, in) if err != nil { return err } if a.Successor != current { return hostbox.ErrChanged } prior := a.TagsFrom if stamp != prior && stamp != current.Stamp { return hostbox.ErrChanged } if err = checkServingArtifactAbsence(in.DBPath, a.Directory); err != nil { return err } transition := servingInventoryTransition(current, db, prior) if stamp == current.Stamp { return transition(fresh.Claim) } written, updateErr := hostclaim.Update(ctx, r.Claims, fresh, transition) if updateErr == nil { if err := in.observeClaimWrite(fresh, written); err != nil { return err } } else if in.claimWrite != nil { return updateErr } // An uncertain CAS is resolved only by a fresh, exact pair and the same // writer. A refusal never permits a database or marker write. after, same, _, tagged, readErr := r.snapshot(ctx, admitted, in) if readErr != nil { return readErr } if same != current || tagged != current.Stamp { if updateErr != nil { return updateErr } return hostbox.ErrChanged } if err = transition(after.Claim); err != nil { return err } return checkServingArtifactAbsence(in.DBPath, a.Directory) } func servingInventoryTransition(current servingArtifactIdentity, db string, prior uint64) func(*hostclaim.Claim) error { return func(c *hostclaim.Claim) error { actual, err := servingWriterIdentity(hostclaim.Observed{Host: current.Host, Claim: c}, current.Box, current.Region) if err != nil || actual != current || !filepath.IsLocal(db) || filepath.ToSlash(filepath.Clean(db)) != db || strings.ContainsAny(db, "\\\x00") { return hostbox.ErrChanged } entry := c.Inventory[current.Box] stamp, err := servingInventoryPair(entry, db) if err != nil || (stamp != prior && stamp != current.Stamp) { return hostbox.ErrChanged } // Copy before editing: a rejected transition cannot mutate its observation. paths := make(map[string]uint64, len(entry.Paths)+2) for p, s := range entry.Paths { paths[p] = s } paths[db], paths[db+".gen"] = current.Stamp, current.Stamp if c.Inventory == nil { c.Inventory = make(map[string]hostclaim.InventoryEntry) } c.Inventory[current.Box] = hostclaim.InventoryEntry{Layout: "nested", Paths: paths} return nil } } func (r *claimServingArtifactRetirement) EnrollLocked(ctx context.Context, admitted hostclaim.Observed, in ClaimLaunchInputs) error { if r == nil { return hostbox.ErrUnsupported } r.mu.Lock() a, ok := r.completed[admitted.Host] r.mu.Unlock() if !ok { return hostbox.ErrChanged } // This final hook can only revalidate a pair completed outside Work. It // cannot finish an incomplete root record or enroll an unparked legacy DB. _, current, _, stamp, err := r.snapshot(ctx, admitted, in) if err != nil { return err } if a.Successor != current || stamp != current.Stamp { return hostbox.ErrChanged } return r.enrollPairLocked(ctx, admitted, in, a) } // servingInventoryPair preserves RecordInventory's normalized-path and // ancestor rules without taking its lock again inside Work. func servingInventoryPair(entry hostclaim.InventoryEntry, db string) (uint64, error) { if !filepath.IsLocal(db) || filepath.ToSlash(filepath.Clean(db)) != db || strings.ContainsAny(db, "\\\x00") { return 0, hostbox.ErrChanged } if len(entry.Paths) != 0 && entry.Layout != "nested" { return 0, hostbox.ErrChanged } left, leftOK := entry.Paths[db] right, rightOK := entry.Paths[db+".gen"] if leftOK != rightOK || left != right || (leftOK && left == 0) { return 0, hostbox.ErrChanged } all := make([]string, 0, len(entry.Paths)+2) for p, stamp := range entry.Paths { if !filepath.IsLocal(p) || filepath.ToSlash(filepath.Clean(p)) != p || strings.ContainsAny(p, "\\\x00") || stamp == 0 { return 0, hostbox.ErrChanged } all = append(all, p) } if !leftOK { all = append(all, db, db+".gen") } sort.Strings(all) for i, p := range all { for _, next := range all[i+1:] { if strings.HasPrefix(next, p+"/") { return 0, hostbox.ErrChanged } } } return left, nil } // bindServingArtifactRetirement composes the serving contract with whichever // operation owners the assembly already enabled. A missing root client leaves // the existing production refusal in place. func bindServingArtifactRetirement(w *ClaimWakeAssembly, r *claimServingArtifactRetirement) error { if w == nil || r == nil || r.Root == nil || r.Claims == nil { return hostbox.ErrUnsupported } operationRetire, operationRecover := w.RetireArtifacts, w.RecoverArtifacts w.RetireArtifacts = func(ctx context.Context, o hostclaim.Observed, in ClaimLaunchInputs, generation string) error { if o.Claim != nil && o.Claim.Op.Kind == hostclaim.KindServing { return r.Retire(ctx, o, in, generation) } if operationRetire == nil { return hostbox.ErrChanged } return operationRetire(ctx, o, in, generation) } w.RecoverArtifacts = func(ctx context.Context, o hostclaim.Observed, in ClaimLaunchInputs) error { if o.Claim != nil && o.Claim.Op.Kind == hostclaim.KindServing { return r.Recover(ctx, o, in) } if operationRecover == nil { return nil } return operationRecover(ctx, o, in) } w.BeforeLegacyInstall, w.BeforeInstallLocked = r.BeforeLegacyInstall, r.EnrollLocked w.RequireServingEnrollment = true return nil } func (r *claimServingArtifactRetirement) forgetCompletion(host string) { if r == nil { return } r.mu.Lock() delete(r.completed, host) r.mu.Unlock() }