package preflight import ( "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "io/fs" "os" "path/filepath" "slices" "strings" "github.com/aritmolab/thothii/tools/tht/internal/config" "github.com/aritmolab/thothii/tools/tht/internal/safeio" "github.com/aritmolab/thothii/tools/tht/internal/version" "gopkg.in/yaml.v3" ) type Plan struct { SchemaVersion int `json:"schema_version"` ValidatorProtocol int `json:"validator_protocol"` Validator version.Info `json:"validator"` Installation config.Installation `json:"installation"` Release Manifest `json:"release"` Platform string `json:"platform"` WorkspaceDirectory string `json:"workspace_directory"` WorkspaceRevision string `json:"workspace_revision"` Inputs []string `json:"inputs"` AbsentInputs []string `json:"absent_inputs"` InputSeal string `json:"input_seal"` Report Report `json:"report"` } func invalidPlan() error { return errors.New("plan inputs changed or protected plan files are unavailable; repeat validation and produce a new plan") } // CaptureInputs gives a value-free freshness guard around live probes and document validation. func CaptureInputs(paths []string, workspace string, absent ...string) (func() bool, error) { key := make([]byte, 32) if _, err := rand.Read(key); err != nil { return nil, invalidPlan() } probe := Plan{Inputs: paths, AbsentInputs: absent} before, err := seal(probe, key) if err != nil { return nil, err } files, err := treeFiles(workspace, true) if err != nil { return nil, err } return func() bool { after, err := seal(probe, key) current, treeErr := treeFiles(workspace, true) return err == nil && treeErr == nil && hmac.Equal([]byte(before), []byte(after)) && slices.Equal(files, current) }, nil } // A separate owner-only random key prevents public/offline guessing of low-entropy secrets. // Live prerequisites must still be rechecked immediately before any execution or resumption. func seal(plan Plan, key []byte) (string, error) { for _, path := range plan.AbsentInputs { if _, err := os.Lstat(path); !errors.Is(err, os.ErrNotExist) { return "", invalidPlan() } } mac := hmac.New(sha256.New, key) plan.InputSeal = "" data, err := json.Marshal(plan) if err != nil { return "", invalidPlan() } mac.Write(data) var total int for _, path := range plan.Inputs { contents, err := safeio.ReadCanonicalRegular(path, 32<<20) if err != nil { return "", invalidPlan() } total += len(contents) if total > 256<<20 { return "", invalidPlan() } length, _ := json.Marshal([]any{path, len(contents)}) mac.Write(length) mac.Write(contents) } return hex.EncodeToString(mac.Sum(nil)), nil } func AbsentOverrides(installation config.Installation) []string { paths := []string{} for _, path := range installation.Overrides { if _, err := os.Lstat(path); errors.Is(err, os.ErrNotExist) { paths = append(paths, path) } } return paths } func WritePlan(path string, plan *Plan, guards ...func() bool) error { if !plan.Report.OK || plan.ValidatorProtocol != Protocol || !filepath.IsAbs(path) { return invalidPlan() } if exists, err := safeio.PreflightPrivateDirectory(filepath.Dir(path)); err != nil || !exists { return invalidPlan() } for _, target := range []string{path, path + ".key"} { if _, err := os.Lstat(target); !errors.Is(err, os.ErrNotExist) { return invalidPlan() } } key := make([]byte, 32) if _, err := rand.Read(key); err != nil { return invalidPlan() } var err error plan.InputSeal, err = seal(*plan, key) if err != nil { return err } for _, guard := range guards { if guard == nil || !guard() { return invalidPlan() } } data, _ := json.MarshalIndent(plan, "", " ") if err := safeio.WriteCanonicalNewPrivateFile(path+".key", key, 0o600); err != nil { return invalidPlan() } if err := safeio.WriteCanonicalNewPrivateFile(path, append(data, '\n'), 0o600); err != nil { _ = safeio.RemoveCanonicalPrivateRegular(path + ".key") return invalidPlan() } return nil } func VerifyPlanInputs(path string) error { data, err := safeio.ReadCanonicalPrivateRegular(path, 4<<20) if err != nil { return invalidPlan() } key, err := safeio.ReadCanonicalPrivateRegular(path+".key", 32) if err != nil || len(key) != 32 { return invalidPlan() } var plan Plan if json.Unmarshal(data, &plan) != nil || plan.SchemaVersion != 1 || plan.ValidatorProtocol != Protocol || !plan.Report.OK || len(plan.Inputs) == 0 || len(plan.Inputs) > 10000 { return invalidPlan() } actual, err := seal(plan, key) if err != nil || !hmac.Equal([]byte(actual), []byte(plan.InputSeal)) { return invalidPlan() } // Detect added or removed workspace files as well as changes to known file bytes. if plan.WorkspaceDirectory != "" { files, err := treeFiles(plan.WorkspaceDirectory, true) if err != nil { return invalidPlan() } for _, file := range files { if !slices.Contains(plan.Inputs, file) { return invalidPlan() } } } return nil } func treeFiles(root string, skipGit bool) ([]string, error) { paths := []string{} count := 0 err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error { if err != nil { return invalidPlan() } count++ if count > 10000 { return invalidPlan() } if skipGit && entry.Name() == ".git" { if entry.IsDir() { return filepath.SkipDir } return nil } if entry.IsDir() { return nil } if !entry.Type().IsRegular() { return invalidPlan() } paths = append(paths, path) return nil }) return paths, err } // CollectInputs fingerprints exact prepared contents, with normalized configuration in Plan. // Referenced credentials are sealed, never copied. Git object stores are excluded. func CollectInputs(installation config.Installation, workspace, bootstrap, manifest string) ([]string, string, error) { paths := []string{installation.Path, installation.EnvFile, bootstrap, manifest} for _, root := range []string{workspace, installation.AuthenticationDirectory()} { files, err := treeFiles(root, root == workspace) if err != nil { return nil, "", err } paths = append(paths, files...) } secrets, err := installation.SecretFiles() if err != nil { return nil, "", invalidPlan() } for _, path := range secrets { paths = append(paths, path) } data, err := safeio.ReadCanonicalPrivateRegular(bootstrap, 1<<20) if err != nil { return nil, "", invalidPlan() } var bindings struct { Databases []struct { SecretFiles map[string]string `yaml:"secretFiles"` EvidenceSecretFiles map[string]string `yaml:"evidenceSecretFiles"` } `yaml:"databases"` } if yaml.Unmarshal(data, &bindings) != nil { return nil, "", invalidPlan() } for _, entry := range bindings.Databases { for _, values := range []map[string]string{entry.SecretFiles, entry.EvidenceSecretFiles} { for _, path := range values { paths = append(paths, path) } } } m, err := LoadManifest(manifest) if err != nil { return nil, "", err } for name := range m.Files { paths = append(paths, filepath.Join(filepath.Dir(manifest), filepath.FromSlash(name))) } for _, path := range installation.Overrides { if _, err := os.Lstat(path); err == nil { paths = append(paths, path) } } revision := "content-snapshot" head := filepath.Join(workspace, ".git", "HEAD") if data, err := safeio.ReadCanonicalRegular(head, 1024); err == nil { paths = append(paths, head) value := strings.TrimSpace(string(data)) if strings.HasPrefix(value, "ref: refs/") { ref := strings.TrimPrefix(value, "ref: ") if safeRelative(ref) { path := filepath.Join(workspace, ".git", filepath.FromSlash(ref)) if data, err := safeio.ReadCanonicalRegular(path, 1024); err == nil { paths = append(paths, path) value = strings.TrimSpace(string(data)) } } } if len(value) == 40 { if _, err := hex.DecodeString(value); err == nil { revision = value } } } slices.Sort(paths) paths = slices.Compact(paths) return paths, revision, nil } func AddRuntimeObligations(report *Report) { for _, check := range []Check{ {"container-network", "deferred-to-runtime", "bindings", "Repeat authenticated DWH and external connectivity checks from the core network."}, {"catalog-initialization", "deferred-to-runtime", "catalog", "Apply migrations and verify Catalog health plus prepared binding import."}, {"pi-operation", "deferred-to-runtime", "release.components.pi", "Verify the bundled Pi version and authenticated provider/model smoke operation inside core; do not install Pi on the host."}, {"local-embedding", "deferred-to-runtime", "modelCatalog.embedding", "Initialize the local embedding model and verify returned vector dimensions."}, {"workspace-preprocessing", "deferred-to-runtime", "workspaces", "Sync the exact verified workspace contents, materialize Evidence, preprocess and verify collections."}, {"workspace-readiness", "deferred-to-runtime", "workspaces", "Complete required administrative and human review gates before claiming final readiness."}, } { report.Checks = append(report.Checks, check) } }