package hostbox import ( "encoding/json" "errors" "os" "path/filepath" "strings" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" ) // ValidateAgentInventory confines agent-authored artifacts to this route's // state directory and registry rows. Opt and units need root-captured authority; // an inventory written by the agent never supplies that authority. func ValidateAgentInventory(in StopInventory, row *registry.Route, dataDir string) error { if !component(in.Host) { return ErrTypedInventory } if row != nil && (row.Host != in.Host || row.ClaimIncarnation != in.Incarnation || row.ClaimStamp != in.InstallationStamp || !row.ClaimOwnership.Valid()) { return ErrTypedInventory } seen := make(map[string]bool) for _, e := range in.Entries { if e.TimerRetired && e.Kind != "timer" { return ErrTypedInventory } key := e.ManagedRoot + "\x00" + e.RelativePath if seen[key] || !managedName(e.RelativePath) || (row != nil && e.Ownership != row.ClaimOwner) { return ErrTypedInventory } seen[key] = true switch e.Kind { case "route": if e.ManagedRoot != "registry" || e.RelativePath != in.Host || e.RowIdentity == "" || (row != nil && e.RowIdentity != row.ClaimRowID) || (row == nil && e.Materialization != "intended") { return ErrTypedInventory } case "timer": if row == nil || e.ManagedRoot != "registry" || !strings.HasPrefix(e.RelativePath, in.Host+"/") || !component(strings.TrimPrefix(e.RelativePath, in.Host+"/")) || e.RowIdentity == "" { return ErrTypedInventory } case "path": if row == nil || e.ManagedRoot != "data" || !filepath.IsAbs(dataDir) || !filepath.IsAbs(row.DBPath) { return ErrTypedInventory } if row.DBPath != provision.NestedDBPath(dataDir, in.Host) && row.DBPath != provision.DefaultDBPath(dataDir, in.Host) { return ErrTypedInventory } if filepath.Clean(e.RelativePath) != e.RelativePath || !filepath.IsLocal(e.RelativePath) { return ErrTypedInventory } parent, err := filepath.Rel(dataDir, filepath.Dir(row.DBPath)) if err != nil || (parent != "." && !managedName(parent)) { return ErrTypedInventory } if parent == "." { if row.DBPath == filepath.Join(dataDir, "registry.db") { return ErrTypedInventory } // Flat legacy layouts: the database and its exact SQLite // companion names. Enabled claim launches require the nested // layout, so an agent has no flat state-directory authority. db := filepath.Base(row.DBPath) isDB := e.RelativePath == db || e.RelativePath == db+"-wal" || e.RelativePath == db+"-shm" || e.RelativePath == db+"-journal" if !isDB { return ErrTypedInventory } } else if e.RelativePath != parent && !strings.HasPrefix(e.RelativePath, parent+string(filepath.Separator)) { // The host's own nested state container (root derives it from // NestedDBPath), or a path inside it. Parking the empty container // is what lets a cold handover find the namespace empty. return ErrTypedInventory } default: return ErrTypedInventory } } return nil } // retirementRoute recovers the row root archived before removing it. The // agent's inventory carries no database path and cannot choose this namespace. func (a ReconcileAdapter) retirementRoute(row registry.Route, target hostguard.FenceTarget) (*registry.Route, error) { current, err := a.Registry.Get(row.Host) if err != nil { return nil, err } if current != nil && current.ClaimOwnership == row.ClaimOwnership { return current, nil } raw, err := readTrustedJournal(filepath.Join(a.Retirement.JournalDir, target.FenceID+".json")) if errors.Is(err, os.ErrNotExist) { return nil, ErrTypedInventory } if err != nil { return nil, err } var journal RetirementJournal if decodeStrict(raw, &journal) != nil || journal.Target == nil || *journal.Target != target || journal.ID != target.FenceID { return nil, ErrTypedJournal } var archives []registry.ClaimRowArchive if json.Unmarshal(journal.Archives, &archives) != nil { return nil, ErrTypedJournal } var found *registry.Route for _, archive := range archives { if archive.Row.Kind != "route" { continue } var saved registry.Route if found != nil || archive.Row.Host != row.Host || archive.Row.Ownership != row.ClaimOwnership || json.Unmarshal(archive.Contents, &saved) != nil || saved.Host != row.Host || saved.ClaimOwnership != row.ClaimOwnership { return nil, ErrTypedJournal } found = &saved } if found == nil { return nil, ErrTypedInventory } return found, nil }