| new file mode 100644 |
| index 000000000..c6f9fb31f |
| --- /dev/null |
| +++ b/internal/release/healthy.go |
| @@ -0,0 +1,118 @@ |
| +// The healthy history — builds this box has run well (spec |
| +// 2026-10-06-console-release-defences-design.md §4.5). A sha256 in an app's |
| +// history is adopted with no proof, which is what lets a requiring box roll |
| +// back to a build it ran before without waiting on the prover. |
| +// |
| +// Root writes it from two places in internal/host: when D1's settle check |
| +// passes for a restarted named unit (How "settle"), and when a root pass finds |
| +// a named-unit route's live build bound, active in one invocation for the |
| +// policy's after, hashed from its running executable and ready (How "live"). |
| +// Every sha256 here was hashed from bytes, never copied from a manifest. It |
| +// lives in the bake directory, which the sandboxed agent can read. |
| +package release |
| + |
| +import ( |
| + "encoding/json" |
| + "fmt" |
| + "log" |
| + "os" |
| + "path/filepath" |
| + "time" |
| +) |
| + |
| +const healthyMax = 50 |
| + |
| +type HealthyBuild struct { |
| + SHA256 string `json:"sha256"` |
| + Version string `json:"version,omitempty"` |
| + How string `json:"how"` |
| + At time.Time `json:"at"` |
| +} |
| + |
| +type healthyFile struct { |
| + Schema int `json:"schema"` |
| + Builds []HealthyBuild `json:"builds"` |
| +} |
| + |
| +func healthyPath(bakeDir, acct, app string) (string, error) { |
| + if bakeDir == "" || !ValidAccount(acct) || !ValidApp(app) { |
| + return "", fmt.Errorf("healthy history: invalid %q/%q", acct, app) |
| + } |
| + return filepath.Join(bakeDir, "healthy", acct, app+".json"), nil |
| +} |
| + |
| +func ReadHealthy(bakeDir, acct, app string) []HealthyBuild { |
| + p, err := healthyPath(bakeDir, acct, app) |
| + if err != nil { |
| + return nil |
| + } |
| + b, err := os.ReadFile(p) |
| + if err != nil { |
| + return nil |
| + } |
| + var f healthyFile |
| + if err := json.Unmarshal(b, &f); err != nil || f.Schema != 1 { |
| + log.Printf("healthy history: %s does not parse (%v), treating it as empty", p, err) |
| + return nil |
| + } |
| + return f.Builds |
| +} |
| + |
| +func IsHealthy(bakeDir, acct, app, sha string) bool { |
| + if sha == "" { |
| + return false |
| + } |
| + for _, b := range ReadHealthy(bakeDir, acct, app) { |
| + if b.SHA256 == sha { |
| + return true |
| + } |
| + } |
| + return false |
| +} |
| + |
| +// RecordHealthy adds b, newest first, keeping the first record of a sha256 |
| +// and the newest healthyMax builds. Written atomically and world-readable, |
| +// because the reader is the sandboxed agent. |
| +func RecordHealthy(bakeDir, acct, app string, b HealthyBuild) error { |
| + p, err := healthyPath(bakeDir, acct, app) |
| + if err != nil { |
| + return err |
| + } |
| + if !sha256Re.MatchString(b.SHA256) { |
| + return fmt.Errorf("healthy history: sha256 %q is not 64 lower-case hex digits", b.SHA256) |
| + } |
| + have := ReadHealthy(bakeDir, acct, app) |
| + for _, h := range have { |
| + if h.SHA256 == b.SHA256 { |
| + return nil |
| + } |
| + } |
| + builds := append([]HealthyBuild{b}, have...) |
| + if len(builds) > healthyMax { |
| + builds = builds[:healthyMax] |
| + } |
| + body, err := json.MarshalIndent(healthyFile{Schema: 1, Builds: builds}, "", " ") |
| + if err != nil { |
| + return err |
| + } |
| + if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { |
| + return err |
| + } |
| + tmp, err := os.CreateTemp(filepath.Dir(p), ".healthy-*") |
| + if err != nil { |
| + return err |
| + } |
| + defer os.Remove(tmp.Name()) |
| + if _, err := tmp.Write(append(body, '\n')); err != nil { |
| + tmp.Close() |
| + return err |
| + } |
| + if err := tmp.Chmod(0o644); err != nil { |
| + tmp.Close() |
| + return err |
| + } |
| + if err := tmp.Close(); err != nil { |
| + return err |
| + } |
| + return os.Rename(tmp.Name(), p) |
| +} |