// Package hostclaim is the host lifecycle claim (spec // 2026-10-02-host-lifecycle-claim-design.md): one object per host that // names the incarnation entitled to it, the operation running on it, where // its data lives and what is still owed. This package only reads, // validates and conditionally writes that object. It never deletes it // (C5a). package hostclaim import ( "crypto/sha256" "encoding/hex" "errors" "fmt" "strings" "time" "github.com/carlosframework/platform/internal/controlname" "github.com/carlosframework/platform/internal/maintenanceguard" ) const ( KindServing = "serving" KindCreating = "creating" KindDeleting = "deleting" KindParking = "parking" KindPurging = "purging" KindPlacing = "placing" KindMoving = "moving" // move-any-app KindLanding = "landing" // move-any-app KindReverting = "reverting" // move-any-app KindRestoring = "restoring" // move-any-app KindPushing = "pushing" // later push spec schemaVersion = 2 retiredCap = 32 ) var knownKinds = map[string]bool{KindServing: true, KindCreating: true, KindDeleting: true, KindParking: true, KindPurging: true, KindPlacing: true, KindMoving: true, KindLanding: true, KindReverting: true, KindRestoring: true, KindPushing: true, KindRelocating: true} var ( ErrMalformed = errors.New("hostclaim: malformed claim") ErrInvalidHost = errors.New("hostclaim: invalid host") ) type Holder struct { Acquisition string `json:"acq,omitempty"` Family string `json:"family"` // "instance" | "sidecar" Key string `json:"key"` // exact record key Account string `json:"account"` App string `json:"app"` Name string `json:"name,omitempty"` // sidecars only Incarnation string `json:"incarnation"` CreatedAt time.Time `json:"created_at"` Location string `json:"location"` Generation string `json:"generation,omitempty"` Stamp uint64 `json:"stamp,omitempty"` Session uint64 `json:"session,omitempty"` WriterBox string `json:"writer_box,omitempty"` Admissions []AdmissionRecord `json:"admissions,omitempty"` WakeRetry *WakeRetry `json:"wake_retry,omitempty"` } type AdmissionRecord struct { Region string `json:"region"` PredecessorRegion string `json:"predecessor_region,omitempty"` Session uint64 `json:"session"` Box string `json:"box"` Generation string `json:"generation"` Predecessor string `json:"predecessor,omitempty"` Mode string `json:"mode"` Coverage string `json:"coverage,omitempty"` Covered bool `json:"covered"` WritesLost *WritesLost `json:"writes_lost,omitempty"` AdditionalWritesLost []WritesLost `json:"additional_writes_lost,omitempty"` LossID string `json:"loss_id,omitempty"` LossArchivedClaimGen uint64 `json:"loss_archived_claim_gen,omitempty"` } type WritesLost struct { Box string `json:"box"` Generation string `json:"generation"` Session uint64 `json:"session"` RemoteMaxTXID uint64 `json:"remote_max_txid"` } type Op struct { Kind string `json:"kind,omitempty"` ID string `json:"id,omitempty"` Phase string `json:"phase,omitempty"` Intent map[string]string `json:"intent,omitempty"` Journal map[string]string `json:"journal,omitempty"` By string `json:"by,omitempty"` StartedAt time.Time `json:"started_at,omitempty"` Exec string `json:"exec,omitempty"` Participants []string `json:"participants,omitempty"` } type Touched struct { DiscoveryBound bool `json:"discovery_bound,omitempty"` DiscoveryBoxes []string `json:"discovery_boxes,omitempty"` Complete bool `json:"complete"` Regions []string `json:"regions,omitempty"` Boxes []string `json:"boxes,omitempty"` DiscoveryOp string `json:"discovery_op,omitempty"` } // InventoryEntry is one box's local paths for the host (§6.3), each tagged // with the installation stamp that produced it (§6.4). type InventoryEntry struct { Layout string `json:"layout"` // "flat" | "nested" Paths map[string]uint64 `json:"paths"` // path -> stamp } type ParkScope struct { Location string `json:"location"` Incarnation string `json:"incarnation"` EvidenceAt time.Time `json:"evidence_at"` RequestLimit time.Duration `json:"request_limit_ns"` } func (p *ParkScope) Validate(host string) error { if p == nil || !isHex64(p.Incarnation) || p.Incarnation != strings.ToLower(p.Incarnation) || p.EvidenceAt.IsZero() || p.RequestLimit <= 0 || p.RequestLimit > time.Duration(1<<63-1)-15*time.Minute { return fmt.Errorf("%w: park scope", ErrMalformed) } base, staging := "instances/"+host, "moves/"+host if p.Location == base || p.Location == base+"/"+p.Incarnation || p.Location == staging { return nil } if op, ok := strings.CutPrefix(p.Location, staging+"/"); ok && op != "" && op != "." && op != ".." && !strings.Contains(op, "/") { return nil } return fmt.Errorf("%w: park location", ErrMalformed) } type Obligation struct { EvidenceRefs map[string]RetirementEvidenceRef `json:"evidence_refs,omitempty"` ParkTarget string `json:"park_target,omitempty"` Park *ParkScope `json:"park,omitempty"` ID string `json:"id"` Op string `json:"op"` Kind string `json:"kind"` // "park-replica" | "park-local" | "park-generation" Region string `json:"region,omitempty"` Generation string `json:"generation,omitempty"` Box string `json:"box,omitempty"` Retirement string `json:"retirement"` Owner string `json:"owner"` // "region:" | "box:" } type CancelledCreate struct { Key string `json:"key"` Incarnation string `json:"incarnation"` Until time.Time `json:"until"` } type Claim struct { RetainedAdmissionLosses []RetainedAdmissionLoss `json:"retained_admission_losses,omitempty"` Origins *OriginHead `json:"admitted_origins,omitempty"` originAdmission *originTuple RetiredLocations map[string]string `json:"retired_locations,omitempty"` EvidenceRefs []AutomaticLoss `json:"evidence_refs,omitempty"` V int `json:"v"` Host string `json:"host"` Gen uint64 `json:"gen"` Holder *Holder `json:"holder,omitempty"` Op Op `json:"op"` Touched Touched `json:"touched"` Inventory map[string]InventoryEntry `json:"inventory,omitempty"` Obligations []Obligation `json:"obligations,omitempty"` Lost []string `json:"lost,omitempty"` LostRevisions map[string]uint64 `json:"lost_revisions,omitempty"` LossRefs []string `json:"loss_refs,omitempty"` RetiredIncarnations []string `json:"retired_incarnations,omitempty"` QuietAfter map[string]time.Time `json:"quiet_after,omitempty"` CancelledCreates []CancelledCreate `json:"cancelled_creates,omitempty"` } // Free reports whether the host has no holder and no operation. func (c *Claim) Free() bool { return c.Holder == nil && c.Op.Kind == "" } func Key(host string) (string, error) { if !controlname.ValidHostShape(host) { return "", ErrInvalidHost } return "control/hosts/" + host + ".json", nil } // Incarnation is §2's identity. A record with a zero CreatedAt (written // before CreatedAt existed) gets the domain-separated legacy identity. func Incarnation(account, app, host string, created time.Time) (string, error) { if created.IsZero() { if !controlname.ValidAccount(account) || !controlname.ValidApp(app) || !controlname.ValidHostShape(host) { return "", ErrMalformed } sum := sha256.Sum256([]byte("carlos-legacy-incarnation-v1\x00" + account + "\x00" + app + "\x00" + host)) return hex.EncodeToString(sum[:]), nil } return maintenanceguard.RecordIdentity(account, app, host, created) } // sidecarSuffix restates provision.SidecarSuffix (no import: avoids a cycle). const sidecarSuffix = ".sidecar.internal" // holderKeyMatches is the one identity check for a holder's record key and // host, shared by Validate, BeginCreate and Adopt. An instance's key is // exactly apps///instances/.json. A sidecar's key is // apps///sidecars/.json, and its host is exactly // ...sidecar.internal (provision.SidecarHost). func holderKeyMatches(family, key, account, app, name, host string) bool { base := "apps/" + account + "/" + app + "/" switch family { case "instance": return name == "" && key == base+"instances/"+host+".json" case "sidecar": return controlname.ValidLabel(name) && key == base+"sidecars/"+name+".json" && host == name+"."+app+"."+account+sidecarSuffix } return false } func isHex64(s string) bool { if len(s) != 64 { return false } _, err := hex.DecodeString(s) return err == nil } func (c *Claim) Validate() error { if c != nil && !originHeadValid(c.Origins) { return ErrMalformed } if c == nil || c.V != schemaVersion || !controlname.ValidHostShape(c.Host) || c.Gen == 0 { return fmt.Errorf("%w: header", ErrMalformed) } if err := validateRetainedAdmissionLosses(c); err != nil { return err } if err := validateRelocating(c); err != nil { return err } if c.Op.Kind != "" && !knownKinds[c.Op.Kind] { return fmt.Errorf("%w: unknown op kind %q", ErrMalformed, c.Op.Kind) } if c.Op.Kind != "" && c.Op.Kind != KindServing && c.Op.ID == "" { return fmt.Errorf("%w: op %q without id", ErrMalformed, c.Op.Kind) } if c.Holder == nil { if c.Op.Kind != "" && c.Op.Kind != KindPurging && !holderlessSweep(c) { // a purge fence has no holder (CC21) return fmt.Errorf("%w: op %q without holder", ErrMalformed, c.Op.Kind) } if len(c.Obligations) > 0 && !holderlessSweep(c) { return fmt.Errorf("%w: a free claim with outstanding obligations", ErrMalformed) } } else { h := c.Holder if (h.Family != "instance" && h.Family != "sidecar") || h.Key == "" || !isHex64(h.Incarnation) || !controlname.ValidAccount(h.Account) || !controlname.ValidApp(h.App) { return fmt.Errorf("%w: holder identity", ErrMalformed) } if !holderKeyMatches(h.Family, h.Key, h.Account, h.App, h.Name, c.Host) { return fmt.Errorf("%w: holder key %q is not this host's", ErrMalformed, h.Key) } if want, err := Incarnation(h.Account, h.App, c.Host, h.CreatedAt); err != nil || want != h.Incarnation { return fmt.Errorf("%w: incarnation does not match the holder's creation fields", ErrMalformed) } legacy, scoped := "instances/"+c.Host, "instances/"+c.Host+"/"+h.Incarnation if h.Location != legacy && h.Location != scoped { return fmt.Errorf("%w: holder location %q", ErrMalformed, h.Location) } if c.Op.Kind == "" { return fmt.Errorf("%w: holder without op", ErrMalformed) } for _, record := range h.Admissions { if record.Region == "" || !validWriteLosses(record.WritesLost, record.AdditionalWritesLost) { return fmt.Errorf("%w: invalid admission", ErrMalformed) } if record.LossID != "" && !safeLossID(record.LossID) { return fmt.Errorf("%w: invalid loss declaration id", ErrMalformed) } if record.LossArchivedClaimGen != 0 && (!admissionHasLoss(record) || record.LossArchivedClaimGen != record.Session) { return fmt.Errorf("%w: invalid archived admission loss", ErrMalformed) } } if err := h.WakeRetry.validate(); err != nil { return err } if h.WakeRetry != nil && h.WakeRetry.State == "attempting" && h.WakeRetry.Attempt.Session != 0 && (h.WakeRetry.Attempt.Session != h.Session || h.WakeRetry.Attempt.Box != h.WriterBox) { return fmt.Errorf("%w: wake retry attempt is not the live writer session", ErrMalformed) } } lost := set(c.Lost) for box, revision := range c.LostRevisions { if box == "" || revision == 0 || !lost[box] { return fmt.Errorf("%w: invalid lost revision", ErrMalformed) } } seenLossRefs := map[string]bool{} for _, id := range c.LossRefs { if !safeLossID(id) || seenLossRefs[id] { return fmt.Errorf("%w: invalid loss references", ErrMalformed) } seenLossRefs[id] = true } if err := validateAutomaticLosses(c, seenLossRefs); err != nil { return err } for inc, location := range c.RetiredLocations { if !isHex64(inc) || !stringPresent(c.RetiredIncarnations, inc) || (location != "instances/"+c.Host && location != "instances/"+c.Host+"/"+inc) { return ErrMalformed } } if len(c.RetiredIncarnations) > retiredCap { return fmt.Errorf("%w: retired_incarnations over cap", ErrMalformed) } seen := map[string]bool{} for _, o := range c.Obligations { if o.ID == "" || o.Op == "" || o.Retirement == "" || seen[o.ID] { return fmt.Errorf("%w: obligation ids", ErrMalformed) } if o.Park != nil { if o.Kind != "park-replica" && o.Kind != "park-generation" { return fmt.Errorf("%w: park scope", ErrMalformed) } if err := o.Park.Validate(c.Host); err != nil { return err } } seen[o.ID] = true switch o.Kind { case "relocation-release": if o.Region == "" || !validBoxReference(o.Box) || o.Box == "none" || o.Generation != "" || o.ParkTarget != "" || o.Owner != "region:"+o.Region { return fmt.Errorf("%w: relocation-release identity", ErrMalformed) } case "park-generation": if o.Region == "" || o.Generation == "" || o.Box != "" || !validRegionalOwner(o.Owner, o.Region) { return fmt.Errorf("%w: park-generation %q location/generation/owner", ErrMalformed, o.ID) } case "park-replica": if o.Region == "" || o.Box != "" || o.Generation != "" || !validRegionalOwner(o.Owner, o.Region) { return fmt.Errorf("%w: park-replica %q location/owner", ErrMalformed, o.ID) } case "park-local": if o.Box == "" || o.Region != "" || o.Generation != "" || o.Owner != "box:"+o.Box { return fmt.Errorf("%w: park-local %q location/owner", ErrMalformed, o.ID) } default: return fmt.Errorf("%w: obligation kind %q", ErrMalformed, o.Kind) } } return nil } func holderlessSweep(c *Claim) bool { if c.Holder != nil || c.Op.Kind != KindParking || c.Op.ID == "" || c.Op.Intent["park_sweep"] != "v1" || c.Op.Exec != "" || len(c.Obligations) == 0 { return false } for _, o := range c.Obligations { if o.Op != c.Op.ID || o.Park == nil || (o.Kind != "park-replica" && o.Kind != "park-generation") { return false } } return true }