carlos / platform Public

host: the root pass records healthy live builds before release.Pass, mints after (H4)

On the first pass after a requirement was switched on, release.Pass could adopt a proven head over a build that had run well for hours before proofRoot looked at it, so that build never entered the healthy history and a rollback to it would wait for a dev proof. proofRoot is split into recordLive (observe and record, before the pass) and mintAndLog (mint, marker, hourly line, after it); around runs them about the pass. The second half reuses the first half's observation when the live link and the unit's process are unchanged, and observes again otherwise.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 58dd5ba19c2c7ff81de2f50baf5f88306b48e7f7 parent e97a506
4 files changed, +256 −22
  • internal/host/agent.go +13 −9
  • internal/host/proofobserve.go +5 −1
  • internal/host/proofroot.go +103 −12
  • internal/host/proofroot_test.go +135 −0
diff --git a/internal/host/agent.go b/internal/host/agent.go
index be5a2eec4..d06d80ed4 100644
--- a/internal/host/agent.go
+++ b/internal/host/agent.go
@@ -3873,14 +3873,13 @@ func RunAdopt(args []string) {
if !*stageOnly {
passOpts = append(passOpts, release.NagExpiredCanaries(os.Stdout))
}
- if err := release.Pass(ctx, reg, store, pub, release.OptDir(), restart, passOpts...); err != nil {
- log.Fatalf("adopt: %v", err)
- }
-
- // D3 (proofroot.go): record live builds as healthy, mint on a proving
- // box, and on the hourly pass say when a requirement is off. After the
- // pass, so it looks at what this pass left live.
- proofRoot{
+ // D3 (proofroot.go) wraps the pass. Before it, record every named-unit
+ // route's live build that has run well as healthy, so the build live when
+ // a requirement is switched on is in the history before this pass can
+ // adopt a proven head over it (final review H4). After it, mint on a
+ // proving box and, on the hourly pass, say when a requirement is off;
+ // a route the pass changed is looked at again first.
+ pr := proofRoot{
obs: productionProofObserver(release.OptDir()),
reg: reg,
store: store,
@@ -3889,7 +3888,12 @@ func RunAdopt(args []string) {
readKey: readProvingKey,
hourly: !*stageOnly,
logf: log.Printf,
- }.run(ctx)
+ }
+ if err := pr.around(ctx, func() error {
+ return release.Pass(ctx, reg, store, pub, release.OptDir(), restart, passOpts...)
+ }); err != nil {
+ log.Fatalf("adopt: %v", err)
+ }
// Step 4: the advisories. The two capability advisories that fall out of
// READING manifests (declared-but-ungranted, and capabilities on an
diff --git a/internal/host/proofobserve.go b/internal/host/proofobserve.go
index b190f9f12..715233b2d 100644
--- a/internal/host/proofobserve.go
+++ b/internal/host/proofobserve.go
@@ -27,6 +27,9 @@ type liveBuild struct {
SHA256 string
Version string
ActiveSince time.Time
+ // The process observed, so a later look can tell it is the same one.
+ invocation string
+ pid int
}
type proofObserver struct {
@@ -85,7 +88,8 @@ func (o proofObserver) observe(rt registry.Route, after time.Duration) (liveBuil
return liveBuild{}, fmt.Errorf("%s restarted during the check", rt.Unit)
}
ver, _ := release.LiveVersion(o.optDir, rt.Host)
- return liveBuild{SHA256: exe, Version: ver, ActiveSince: before.ActiveEnterTimestamp}, nil
+ return liveBuild{SHA256: exe, Version: ver, ActiveSince: before.ActiveEnterTimestamp,
+ invocation: before.InvocationID, pid: before.MainPID}, nil
}
// recordSettled is checkedUnitRestart's settled hook on the root pass: hash
diff --git a/internal/host/proofroot.go b/internal/host/proofroot.go
index b0b72c1ab..5f2c1fb1e 100644
--- a/internal/host/proofroot.go
+++ b/internal/host/proofroot.go
@@ -1,13 +1,18 @@
// The root pass's D3 work (spec 2026-10-06-console-release-defences-design.md
-// §4.3, §4.5): after release.Pass, look at every named-unit route's live build.
+// §4.3, §4.5): look at every named-unit route's live build, once before
+// release.Pass and again, only where the pass changed it, after.
//
// - A build meeting observe's conditions (bound, active in one invocation
// for the policy's after, hashed from its running executable, ready)
// enters the app's healthy history, on every box, policy file or not.
// That is what puts the build already live when a requirement is switched
-// on into the history without a restart.
-// - On a proving box (a prove block lists the app) the same build is
-// minted a proof, unless a refusal record names it or the bucket already
+// on into the history without a restart. recordLive does it BEFORE
+// release.Pass (final review H4): on the first pass after a requirement
+// is switched on, the pass can adopt a proven head and replace the build
+// that has been running well for hours, and that build must already be in
+// the history, or a later rollback to it would wait for a dev proof.
+// - On a proving box (a prove block lists the app) the build the pass left
+// live is minted a proof (mintAndLog, after release.Pass), unless a refusal record names it or the bucket already
// has one. Minting needs the root-only key, which is why it is here.
// - A pass that reads a require entry leaves a proof-required/<app> marker
// in the bake directory. On the hourly pass, an app with that marker,
@@ -46,11 +51,98 @@ type proofRoot struct {
logf func(format string, a ...any)
}
+// liveObservation is one look at a route's live build: the live link's
+// target when it was taken, and observe's answer.
+type liveObservation struct {
+ target string
+ build liveBuild
+ err error
+}
+
+// proofObservations is recordLive's result, handed to mintAndLog so a route
+// the pass did not touch is observed once per pass, not twice.
+type proofObservations struct {
+ pol *release.ProofPolicy
+ perr error
+ byHost map[string]liveObservation
+}
+
+// run is both halves back to back, for a caller with no pass between them.
func (r proofRoot) run(ctx context.Context) {
+ r.mintAndLog(ctx, r.recordLive(ctx))
+}
+
+// around is RunAdopt's order: record the live builds, run the pass, then
+// mint on what the pass left live. A pass error is returned without minting.
+func (r proofRoot) around(ctx context.Context, pass func() error) error {
+ obs := r.recordLive(ctx)
+ if err := pass(); err != nil {
+ return err
+ }
+ r.mintAndLog(ctx, obs)
+ return nil
+}
+
+// recordLive reads the policy, observes every named-unit route's live build,
+// and records each one that qualifies as healthy.
+func (r proofRoot) recordLive(ctx context.Context) proofObservations {
pol, perr := release.LoadProofPolicy(r.policyPath)
if perr != nil {
r.logf("proof: %v; nothing is minted until it parses", perr)
}
+ obs := proofObservations{pol: pol, perr: perr, byHost: map[string]liveObservation{}}
+ routes, err := r.reg.List()
+ if err != nil {
+ r.logf("proof: list routes: %v", err)
+ return obs
+ }
+ for _, rt := range routes {
+ if !release.Adoptable(rt) || rt.Unit == "" {
+ continue
+ }
+ obs.byHost[rt.Host] = r.observeAndRecord(rt, pol.ProveAfter())
+ }
+ return obs
+}
+
+// observeAndRecord observes rt's live build and, when it qualifies, adds it
+// to the app's healthy history.
+func (r proofRoot) observeAndRecord(rt registry.Route, after time.Duration) liveObservation {
+ target, _ := os.Readlink(release.LiveLink(r.obs.optDir, rt.Host))
+ b, err := r.obs.observe(rt, after)
+ o := liveObservation{target: target, build: b, err: err}
+ if err != nil {
+ return o
+ }
+ if err := release.RecordHealthy(r.bakeDir, release.RouteAccount(rt), rt.App, release.HealthyBuild{
+ SHA256: b.SHA256, Version: b.Version, How: "live", At: r.obs.now().UTC()}); err != nil {
+ r.logf("proof: %s: recording the healthy build: %v", rt.Host, err)
+ }
+ return o
+}
+
+// stillLive reports whether o still describes rt's live build after the
+// pass: the live link names the same target and, for a build that passed,
+// the unit is the same process (a rollback can put the link back where it
+// was and restart the unit; that process has not run for after).
+func (r proofRoot) stillLive(rt registry.Route, o liveObservation) bool {
+ target, err := os.Readlink(release.LiveLink(r.obs.optDir, rt.Host))
+ if err != nil || target != o.target {
+ return false
+ }
+ if o.err != nil {
+ return true
+ }
+ p, err := r.obs.sys.Show(rt.Unit)
+ return err == nil && p.ActiveState == "active" && p.InvocationID == o.build.invocation && p.MainPID == o.build.pid
+}
+
+// mintAndLog is the second half: mint on a proving box, leave the
+// requirement marker, and on the hourly pass say when a requirement is off.
+// A route whose live build the pass changed is observed again (and recorded,
+// if it qualifies); every other route reuses recordLive's observation.
+func (r proofRoot) mintAndLog(ctx context.Context, obs proofObservations) {
+ pol, perr := obs.pol, obs.perr
routes, err := r.reg.List()
if err != nil {
r.logf("proof: list routes: %v", err)
@@ -65,19 +157,18 @@ func (r proofRoot) run(ctx context.Context) {
}
acct := release.RouteAccount(rt)
seen[acctApp{acct, rt.App}] = true
- b, err := r.obs.observe(rt, after)
- if err != nil {
+ o, ok := obs.byHost[rt.Host]
+ if !ok || !r.stillLive(rt, o) {
+ o = r.observeAndRecord(rt, after)
+ }
+ if o.err != nil {
if pol.Proves(rt.App) {
- r.logf("proof: %s: not minting yet: %v", rt.Host, err)
+ r.logf("proof: %s: not minting yet: %v", rt.Host, o.err)
}
continue
}
- if err := release.RecordHealthy(r.bakeDir, acct, rt.App, release.HealthyBuild{
- SHA256: b.SHA256, Version: b.Version, How: "live", At: r.obs.now().UTC()}); err != nil {
- r.logf("proof: %s: recording the healthy build: %v", rt.Host, err)
- }
if perr == nil && pol.Proves(rt.App) {
- r.mint(ctx, pol, rt, acct, b)
+ r.mint(ctx, pol, rt, acct, o.build)
}
}
if perr != nil {
diff --git a/internal/host/proofroot_test.go b/internal/host/proofroot_test.go
index 6557a29d5..03826398e 100644
--- a/internal/host/proofroot_test.go
+++ b/internal/host/proofroot_test.go
@@ -3,6 +3,8 @@ package host
import (
"context"
"crypto/ed25519"
+ "crypto/sha256"
+ "encoding/hex"
"fmt"
"os"
"path/filepath"
@@ -303,3 +305,136 @@ func TestProofRootRequireEntryLeavesTheMarker(t *testing.T) {
t.Fatalf("logs = %v, want the hourly requirement-off line once the file is gone", e.logs)
}
}
+
+// Final review H4: the first adoption after a requirement is switched on.
+// A has run well for two hours with an empty healthy history; B is promoted
+// with a valid dev proof. One RunAdopt-shaped run (proofRoot.around a real
+// release.Pass) adopts B, and A, the build B replaced, is in the history
+// too, so a later rollback to A does not wait for a dev proof. B enters it
+// through the settle hook.
+func TestProofRootRecordsTheReplacedBuildBeforeAdoption(t *testing.T) {
+ ctx := context.Background()
+ opt, bake, etc := t.TempDir(), t.TempDir(), t.TempDir()
+ s := release.NewDirStore(t.TempDir())
+ plat := release.AdoptPlatform()
+ pub, priv, err := ed25519.GenerateKey(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for ver, body := range map[string]string{"a": "build a", "b": "build b"} {
+ m := release.Manifest{App: "console", Version: ver, Kind: "binary", Artifacts: map[string]release.Artifact{plat: {}}}
+ if err := release.Ship(ctx, s, "bab", m, map[string][]byte{plat: []byte(body)}); err != nil {
+ t.Fatalf("Ship %s: %v", ver, err)
+ }
+ }
+ if err := release.StageArtifact(opt, "bab", "a", "console", []byte("build a")); err != nil {
+ t.Fatal(err)
+ }
+ if err := release.PointLive(opt, observedHost, "bab", "a", "console"); err != nil {
+ t.Fatal(err)
+ }
+ if err := release.Promote(ctx, s, priv, "bab", "console", "canary/console", "b"); err != nil {
+ t.Fatal(err)
+ }
+ reg := restartTestRegistry(t)
+ rt := registry.Route{Host: observedHost, Kind: "instance", App: "console",
+ Channel: "canary/console", Unit: "console.service", Status: "ready"}
+ if err := reg.Upsert(rt); err != nil {
+ t.Fatal(err)
+ }
+ shaA := sha256Hex("build a")
+ shaB := sha256Hex("build b")
+
+ seed, proverPub, err := release.GenerateKey()
+ if err != nil {
+ t.Fatal(err)
+ }
+ proverKey, err := release.ParseSigningKey(seed, "test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ now := time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC)
+ p, err := release.SignProof(proverKey, release.Proof{App: "console", Platform: plat, SHA256: shaB,
+ Version: "b", Prover: "dev", ActiveSince: now.Add(-time.Hour), ProvenAt: now})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := release.PutProof(ctx, s, "bab", p); err != nil {
+ t.Fatal(err)
+ }
+ policyPath := filepath.Join(etc, "proof-policy.json")
+ if err := os.WriteFile(policyPath, []byte(fmt.Sprintf(
+ `{"schema":1,"require":[{"app":"console","prover":"dev","key":%q}]}`, proverPub)), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ link := release.LiveLink(opt, observedHost)
+ units := &fakeUnits{shows: []unitProps{{ActiveState: "active", InvocationID: "a1", MainPID: 7,
+ ExecPath: link, ActiveEnterTimestamp: now.Add(-2 * time.Hour)}}}
+ procExe := func(int) string { return link } // the process runs whatever the link names
+ clock := func() time.Time { return now }
+ root := proofRoot{
+ obs: proofObserver{sys: units, procExe: procExe, optDir: opt, now: clock,
+ ready: func(registry.Route) func() error { return nil }},
+ reg: reg, store: s, bakeDir: bake, policyPath: policyPath, readKey: readProvingKey,
+ logf: func(f string, a ...any) { t.Logf(f, a...) },
+ }
+ // The restart starts B's process and settles, as checkedUnitRestart
+ // would, and its settled hook records what it runs.
+ restart := func(host string) error {
+ units.shows, units.n = []unitProps{{ActiveState: "active", InvocationID: "b1", MainPID: 8,
+ ExecPath: link, ActiveEnterTimestamp: now}}, 0
+ recordSettled(units, procExe, opt, bake, clock)(rt)
+ return nil
+ }
+ if err := root.around(ctx, func() error {
+ return release.Pass(ctx, reg, s, pub, opt, restart, release.BakeDir(bake),
+ release.WithProofGate(release.NewProofGate(policyPath, bake)))
+ }); err != nil {
+ t.Fatal(err)
+ }
+ if v, _ := release.LiveVersion(opt, observedHost); v != "b" {
+ t.Fatalf("live = %q, want b adopted on its proof", v)
+ }
+ got := map[string]string{}
+ for _, h := range release.ReadHealthy(bake, "bab", "console") {
+ got[h.SHA256] = h.How
+ }
+ if got[shaA] != "live" {
+ t.Fatalf("history = %+v, want a (sha256 %s) recorded live before the pass replaced it", got, shaA)
+ }
+ if got[shaB] != "settle" {
+ t.Fatalf("history = %+v, want b (sha256 %s) recorded by the settle hook", got, shaB)
+ }
+}
+
+// A route the pass did not touch is observed once: the second half reuses
+// the first half's look when the link and the process are unchanged, and
+// looks again when the process was restarted under the same link.
+func TestProofRootReusesAnUnchangedObservation(t *testing.T) {
+ e := newProofRootEnv(t, provePolicy)
+ units := e.root.obs.sys.(*fakeUnits)
+ obs := e.root.recordLive(context.Background())
+ if o := obs.byHost[observedHost]; o.err != nil {
+ t.Fatalf("first half: %v", o.err)
+ }
+ rt := registry.Route{Host: observedHost, App: "console", Unit: "console.service"}
+ if !e.root.stillLive(rt, obs.byHost[observedHost]) {
+ t.Fatal("an untouched route is not still live")
+ }
+ units.shows = []unitProps{{ActiveState: "active", InvocationID: "i2", MainPID: 9,
+ ExecPath: units.shows[0].ExecPath, ActiveEnterTimestamp: e.now}}
+ units.n = 0
+ if e.root.stillLive(rt, obs.byHost[observedHost]) {
+ t.Fatal("a restarted process under the same link counts as still live")
+ }
+ e.root.mintAndLog(context.Background(), obs)
+ if e.proof(t) != nil {
+ t.Fatal("minted from the first half's look at a process that has since restarted")
+ }
+}
+
+func sha256Hex(s string) string {
+ h := sha256.Sum256([]byte(s))
+ return hex.EncodeToString(h[:])
+}