carlos / platform Public

WIP claim Plan 14 track E checkpoint 13 (not for merge): Paul's scope rulings (unknown history allows signed targets, no-census minting paused, box-local E4 grants, E3 census-only); D3 health recorded independently of program history

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 6e16553f3e042c398a42248e304612998c00f974 parent d527923
16 files changed, +650 −48
  • internal/host/agent.go +9 −2
  • internal/host/namedrecovery.go +73 −0
  • internal/host/namedrecovery_active.go +4 −0
  • internal/host/plan14_active_recovery_test.go +13 −1
  • internal/host/plan14_incident_recipe_test.go +2 −2
  • internal/host/plan14_mint_census_linux_test.go +41 −0
  • internal/host/plan14_recovery_policy_test.go +114 −0
  • internal/host/plan14_root_history_test.go +36 −10
  • internal/host/proofroot.go +22 −7
  • internal/host/proofroot_test.go +1 −0
  • internal/provision/testdata/plan14-root-reads-e.tsv +15 −8
  • internal/release/adopt.go +10 −0
  • internal/release/plan14_recovery_policy_test.go +221 −0
  • internal/release/plan14_root_history_process_test.go +8 −2
  • internal/release/roothistory.go +5 −16
  • internal/release/roothistory_recovery_policy.go +76 −0
diff --git a/internal/host/agent.go b/internal/host/agent.go
index 7548c9bbb..a8f9e87c3 100644
--- a/internal/host/agent.go
+++ b/internal/host/agent.go
@@ -4502,6 +4502,10 @@ func runAdoptUnderRootRelease(args []string, buildClaims ClaimFactory, ensureUni
// 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{
+ mintCensus: func() (bool, error) {
+ legacy, err := restartChecks.allowLegacy()
+ return !legacy && err == nil, err
+ },
obs: observer,
reg: reg,
store: store,
@@ -4514,9 +4518,12 @@ func runAdoptUnderRootRelease(args []string, buildClaims ClaimFactory, ensureUni
}
if history != nil {
pr.recordProgram = func(b liveBuild) error { return recordRootProgram(history, b) }
- pr.refusals = func(b release.HistoryBinding) ([]release.Refusal, error) {
- return restartChecks.mintRefusals(history.History, b)
+ }
+ pr.refusals = func(b release.HistoryBinding) ([]release.Refusal, error) {
+ if history == nil {
+ return nil, os.ErrPermission
}
+ return restartChecks.mintRefusals(history.History, b)
}
passErr := pr.around(ctx, func() error {
return release.Pass(ctx, reg, store, pub, release.OptDir(), restart, passOpts...)
diff --git a/internal/host/namedrecovery.go b/internal/host/namedrecovery.go
index 4dca745d4..015d1e3e0 100644
--- a/internal/host/namedrecovery.go
+++ b/internal/host/namedrecovery.go
@@ -7,9 +7,11 @@ import (
"encoding/hex"
"encoding/json"
"io"
+ "log"
"os"
"path/filepath"
"reflect"
+ "strings"
"github.com/carlosframework/platform/internal/hostwrite"
"github.com/carlosframework/platform/internal/release"
@@ -17,6 +19,8 @@ import (
)
type namedUnitRecovery struct {
+ attempts func() (rootstate.Store, error)
+ history func() (*release.RootHistoryPass, error)
source *proofBindingSource
pub ed25519.PublicKey
ratchet *release.DigestRatchet
@@ -93,6 +97,9 @@ func (n namedUnitRecovery) repair(ctx context.Context, host string) (bool, error
if !reflect.DeepEqual(fresh, pointer) {
return os.ErrPermission
}
+ if err := n.checkHistory(binding, pointer); err != nil {
+ return err
+ }
still, err := n.readLive(live)
if err != nil || still != before {
return os.ErrPermission
@@ -122,3 +129,69 @@ func (n namedUnitRecovery) repair(ctx context.Context, host string) (bool, error
})
return true, err
}
+
+func (n namedUnitRecovery) checkHistory(b proofBinding, p *release.ChannelPointer) error {
+ binding := release.HistoryBinding{Host: b.Route.Host, Account: b.Route.Account, App: b.Route.App}
+ var history *release.RootHistory
+ if n.history != nil {
+ h, err := n.history()
+ if err == nil && h != nil && h.History != nil && h.History.Store != nil {
+ history = h.History
+ defer history.Store.Close()
+ }
+ }
+ if n.originalRefusal(history, binding, p) {
+ return os.ErrPermission
+ }
+ blocked, err := history.Blocks(binding, p)
+ if err != nil {
+ return err
+ }
+ if blocked {
+ return os.ErrPermission
+ }
+ if history != nil {
+ if err := history.RetireD1(binding, p); err != nil {
+ log.Print(err)
+ }
+ }
+ return nil
+}
+
+func (n namedUnitRecovery) originalRefusal(h *release.RootHistory, b release.HistoryBinding, p *release.ChannelPointer) bool {
+ if n.attempts == nil {
+ return false
+ }
+ s, err := n.attempts()
+ if err != nil {
+ return false
+ }
+ defer s.Close()
+ names, _ := s.Names()
+ for _, name := range names {
+ if !strings.HasSuffix(name, ".recovery") && !strings.HasSuffix(name, ".attempt") {
+ continue
+ }
+ raw, err := s.Read(name)
+ var a rollbackAttempt
+ if err != nil || len(raw) > rootstate.MaxRecord || json.Unmarshal(raw, &a) != nil || (name != rollbackJournalKey(a.Host) && name != recoveryResultKey(a)) {
+ continue
+ }
+ r, err := recoveryHistoryOutcome(a)
+ if err != nil || r.Binding != b || (!r.Failed && !r.Recovering) {
+ continue
+ }
+ ref := release.Refusal{Version: r.Attempt.Version, PointerPromotedAt: r.Attempt.PromotedAt}
+ if !ref.Blocks(p) {
+ continue
+ }
+ if h != nil && r.Settled {
+ retired, err := h.RecoveryRetired(r)
+ if err == nil && retired {
+ continue
+ }
+ }
+ return true
+ }
+ return false
+}
diff --git a/internal/host/namedrecovery_active.go b/internal/host/namedrecovery_active.go
index 9b8259e61..9d98a9b87 100644
--- a/internal/host/namedrecovery_active.go
+++ b/internal/host/namedrecovery_active.go
@@ -22,6 +22,7 @@ func (n namedUnitRecovery) recover(ctx context.Context, host string, c rootResta
return false, nil
}
c.preserveOutcome = true
+ n.history, n.attempts = c.history, c.attempts
legacy, err := c.allowLegacy()
if err != nil {
return true, err
@@ -96,6 +97,9 @@ func (n namedUnitRecovery) recover(ctx context.Context, host string, c rootResta
if !reflect.DeepEqual(fresh, pointer) {
return os.ErrPermission
}
+ if err := n.checkHistory(b, pointer); err != nil {
+ return err
+ }
still, err := n.readLive(live)
if err != nil || still != before {
return os.ErrPermission
diff --git a/internal/host/plan14_active_recovery_test.go b/internal/host/plan14_active_recovery_test.go
index 3a811934f..224c3c066 100644
--- a/internal/host/plan14_active_recovery_test.go
+++ b/internal/host/plan14_active_recovery_test.go
@@ -53,7 +53,12 @@ func TestPlan14SignedActiveFixIgnoresHistory(t *testing.T) {
t.Fatal("fixture unexpectedly has bake completeness")
}
}
- c.history = func() (*release.RootHistoryPass, error) { return nil, os.ErrPermission }
+ c.history = func() (*release.RootHistoryPass, error) {
+ if deps == nil {
+ return nil, os.ErrPermission
+ }
+ return deps, nil
+ }
n := namedUnitRecovery{source: source, pub: pub, readLive: os.Readlink}
if fault == "claims on" {
n.writes = hostwrite.Config{Enabled: true, Selector: namedRecoveryWork{}}
@@ -62,6 +67,13 @@ func TestPlan14SignedActiveFixIgnoresHistory(t *testing.T) {
release.WithRootHistory(deps), release.WithRecovery(func(ctx context.Context, rt registry.Route) (bool, error) {
return n.recover(ctx, rt.Host, c, func(string) string { return "5s" }, e.root.reg)
}))
+ if fault == "refused" {
+ live, liveErr := release.LiveVersion(u.opt, rt.Host)
+ if err == nil || liveErr != nil || live != "v1" || len(u.effects) != 0 {
+ t.Fatalf("known refusal ignored: live=%s effects=%v error=%v", live, u.effects, err)
+ }
+ return
+ }
if err != nil || len(u.effects) != 1 || u.exe != release.VersionPath(u.opt, rt.Account, "v2", rt.App) {
t.Fatalf("history prevented active repair: effects=%v error=%v", u.effects, err)
}
diff --git a/internal/host/plan14_incident_recipe_test.go b/internal/host/plan14_incident_recipe_test.go
index e798f694c..bbf05acdf 100644
--- a/internal/host/plan14_incident_recipe_test.go
+++ b/internal/host/plan14_incident_recipe_test.go
@@ -21,7 +21,7 @@ func TestPlan14Incident20261006WithLedger(t *testing.T) {
e, rt, u, c, _ := plan14AdoptionFixture(t)
h := seedHostRootHistory(t, rt)
b := release.HistoryBinding{Host: rt.Host, Account: rt.Account, App: rt.App}
- if err := h.AddRefusal(b, release.Refusal{Host: rt.Host, App: rt.App, Version: "v1", Kind: release.RefusalWrongProgram, RefusedAt: time.Now()}); err != nil {
+ if err := h.AddRefusal(b, release.Refusal{Host: rt.Host, App: rt.App, Version: "v2", Kind: release.RefusalWrongProgram, RefusedAt: time.Now()}); err != nil {
t.Fatal(err)
}
mem := h.Store.(*rollbackMemory)
@@ -59,7 +59,7 @@ func TestPlan14Incident20261006WithLedger(t *testing.T) {
}
u.props.ActiveState, u.props.SubState, u.props.MainPID = "failed", "failed", 0
source := &proofBindingSource{store: e.store, sys: u, optDir: u.opt, openFile: os.Open, unitDirs: []string{filepath.Dir(u.props.FragmentPath)}}
- recovery := namedUnitRecovery{source: source, pub: pub, writes: hostwrite.Config{}, readLive: os.Readlink}
+ recovery := namedUnitRecovery{source: source, pub: pub, writes: hostwrite.Config{}, readLive: os.Readlink, history: func() (*release.RootHistoryPass, error) { return &release.RootHistoryPass{History: h}, nil }}
done, err := recovery.repair(t.Context(), rt.Host)
if err != nil || !done {
t.Fatalf("signed recovery: %v %v", done, err)
diff --git a/internal/host/plan14_mint_census_linux_test.go b/internal/host/plan14_mint_census_linux_test.go
new file mode 100644
index 000000000..22849e570
--- /dev/null
+++ b/internal/host/plan14_mint_census_linux_test.go
@@ -0,0 +1,41 @@
+package host
+
+import (
+ "context"
+ "github.com/carlosframework/platform/internal/release"
+ "testing"
+)
+
+func TestPlan14RootHistoryMintUsesRootInventory(t *testing.T) {
+ if !plan14RootFixture(t) {
+ return
+ }
+ if legacy, err := legacyRootRestart(); err != nil || !legacy {
+ t.Fatalf("expected a box without a census: legacy=%v error=%v", legacy, err)
+ }
+ e, _, _, _ := plan14ProofFixture(t)
+ h := &release.RootHistory{Store: &rollbackMemory{files: map[string][]byte{}}}
+ e.root.refusals = h.MintRefusals
+ e.root.mintCensus = func() (bool, error) {
+ legacy, err := legacyRootRestart()
+ return !legacy && err == nil, err
+ }
+ for pass := 1; pass <= 2; pass++ {
+ e.root.run(context.Background())
+ if e.proof(t) != nil {
+ t.Fatal("minted without a census")
+ }
+ pauses := 0
+ for _, line := range e.logs {
+ if line == "proof: minting paused until this box has a root census" {
+ pauses++
+ }
+ }
+ if pauses != pass {
+ t.Fatalf("pause logged %d times after %d passes: %v", pauses, pass, e.logs)
+ }
+ }
+ if len(release.ReadHealthy(e.root.historyDir(), "bab", "console")) != 1 {
+ t.Fatal("mint pause suppressed health")
+ }
+}
diff --git a/internal/host/plan14_recovery_policy_test.go b/internal/host/plan14_recovery_policy_test.go
new file mode 100644
index 000000000..d9fe5f406
--- /dev/null
+++ b/internal/host/plan14_recovery_policy_test.go
@@ -0,0 +1,114 @@
+package host
+
+import (
+ "crypto/ed25519"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/hostwrite"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+func TestPlan14RecoveryKnownRefusal(t *testing.T) {
+ for _, mode := range []string{"active", "failed", "inactive"} {
+ for _, kind := range []string{release.RefusalNotUp, release.RefusalWrongProgram, release.RefusalNotProven} {
+ for _, change := range []string{"same", "retry", "unsigned", "late-refusal"} {
+ t.Run(mode+"/"+kind+"/"+change, func(t *testing.T) {
+ e, u, c, source, pub := signedRecoveryFixture(t)
+ rt, err := e.root.reg.Get(observedHost)
+ if err != nil {
+ t.Fatal(err)
+ }
+ h := seedHostRootHistory(t, *rt)
+ b := release.HistoryBinding{Host: rt.Host, Account: rt.Account, App: rt.App}
+ p, err := release.Channel(t.Context(), e.store, pub, rt.Account, rt.App, rt.Channel)
+ if err != nil {
+ t.Fatal(err)
+ }
+ at := p.PromotedAt
+ if change == "retry" {
+ at = at.Add(-time.Second)
+ }
+ ref := release.Refusal{Host: rt.Host, App: rt.App, Version: p.Version, Kind: kind, PointerPromotedAt: at, RefusedAt: at}
+ add := func() {
+ if err := h.AddRefusal(b, ref); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if change != "late-refusal" {
+ add()
+ }
+ c.history = func() (*release.RootHistoryPass, error) { return &release.RootHistoryPass{History: h}, nil }
+ n := namedUnitRecovery{source: source, pub: pub, readLive: os.Readlink}
+ if change == "unsigned" {
+ n.pub = make(ed25519.PublicKey, ed25519.PublicKeySize)
+ }
+ if change == "late-refusal" {
+ n.writes = hostwrite.Config{Enabled: true, Selector: namedRecoveryWork{before: add}}
+ }
+ if mode != "active" {
+ u.props.ActiveState, u.props.SubState, u.props.MainPID = mode, mode, 0
+ }
+ handled, err := n.recover(t.Context(), rt.Host, c, func(string) string { return "5s" }, e.root.reg)
+ accepted := change == "retry" && kind == release.RefusalNotUp
+ live, liveErr := release.LiveVersion(u.opt, rt.Host)
+ want := "v1"
+ effects := 0
+ if accepted {
+ want = "v2"
+ if mode == "active" {
+ effects = 1
+ }
+ }
+ if !handled || (err == nil) != accepted || liveErr != nil || live != want || len(u.effects) != effects {
+ t.Fatalf("handled=%v error=%v live=%s effects=%v", handled, err, live, u.effects)
+ }
+ refs, err := h.Refusals(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if accepted && len(refs) != 0 {
+ t.Fatalf("verified retry retained D1: %+v", refs)
+ }
+ if !accepted && len(refs) != 1 {
+ t.Fatalf("refused target cleared evidence: %+v", refs)
+ }
+ })
+ }
+ }
+ }
+}
+
+func TestPlan14RecoveryOriginalRefusalWithUnknownHistory(t *testing.T) {
+ e, u, c, source, pub := signedRecoveryFixture(t)
+ old, err := c.host(t.Context(), observedHost)
+ if err != nil {
+ t.Fatal(err)
+ }
+ p, err := release.Channel(t.Context(), e.store, pub, old.Route.Account, old.Route.App, old.Route.Channel)
+ if err != nil {
+ t.Fatal(err)
+ }
+ disk, err := c.attempts()
+ if err != nil {
+ t.Fatal(err)
+ }
+ b := release.HistoryBinding{Host: old.Route.Host, Account: old.Route.Account, App: old.Route.App}
+ a, err := captureRollback(disk, old, c.now(), unitSettleTimeout(func(string) string { return "5s" }), release.VersionPath(u.opt, b.Account, p.Version, b.App), c.identity, historyRollbackLink{Binding: b, Attempt: release.HistoryAttempt{ID: "failed", Source: old.Source, Version: p.Version, SHA256: "digest", Platform: release.AdoptPlatform(), Started: c.now(), PromotedAt: p.PromotedAt}, Independent: true})
+ if err != nil {
+ t.Fatal(err)
+ }
+ a.Failed, a.Settled = true, true
+ if err := writeRollbackState(disk, recoveryResultKey(a), a); err != nil {
+ t.Fatal(err)
+ }
+ delete(disk.(*rollbackMemory).files, rollbackJournalKey(b.Host))
+ c.history = func() (*release.RootHistoryPass, error) { return nil, os.ErrPermission }
+ n := namedUnitRecovery{source: source, pub: pub, readLive: os.Readlink}
+ handled, err := n.recover(t.Context(), b.Host, c, func(string) string { return "5s" }, e.root.reg)
+ live, _ := release.LiveVersion(u.opt, b.Host)
+ if !handled || err == nil || live != "v1" || len(u.effects) != 0 {
+ t.Fatalf("retained failure ignored: handled=%v err=%v live=%s effects=%v", handled, err, live, u.effects)
+ }
+}
diff --git a/internal/host/plan14_root_history_test.go b/internal/host/plan14_root_history_test.go
index d781de27a..8f0b82182 100644
--- a/internal/host/plan14_root_history_test.go
+++ b/internal/host/plan14_root_history_test.go
@@ -7,6 +7,7 @@ import (
"encoding/json"
"fmt"
"os"
+ "strings"
"testing"
"time"
@@ -92,26 +93,51 @@ func TestPlan14RootHistoryProductionPass(t *testing.T) {
}
}
-func TestPlan14RootHistoryMintUsesRootInventory(t *testing.T) {
- for _, attack := range []string{"positive", "refused", "unknown"} {
- t.Run(attack, func(t *testing.T) {
+func TestPlan14RootHistoryMintCensus(t *testing.T) {
+ for _, state := range []string{"complete", "refused", "unknown", "census-error", "missing-probe", "no-census-complete"} {
+ t.Run(state, func(t *testing.T) {
e, rt, _, _ := plan14ProofFixture(t)
h := seedHostRootHistory(t, rt)
e.root.refusals = h.MintRefusals
- if attack == "refused" {
+ e.root.mintCensus = func() (bool, error) { return true, nil }
+ switch state {
+ case "refused":
if err := h.AddRefusal(release.HistoryBinding{Host: rt.Host, Account: rt.Account, App: rt.App}, release.Refusal{Host: rt.Host, App: rt.App, Version: "v1", SHA256: e.sha, RefusedAt: time.Now(), Recovered: true}); err != nil {
t.Fatal(err)
}
- }
- if attack == "unknown" {
+ case "unknown":
h.Store = &rollbackMemory{files: map[string][]byte{}}
+ case "census-error":
+ e.root.mintCensus = func() (bool, error) { return true, os.ErrPermission }
+ case "missing-probe":
+ e.root.mintCensus = nil
+ case "no-census-complete":
+ e.root.mintCensus = func() (bool, error) { return false, nil }
}
e.root.run(context.Background())
- if attack == "positive" && e.proof(t) == nil {
- t.Fatalf("root inventory blocked mint: %v", e.logs)
+ if (e.proof(t) != nil) != (state == "complete") {
+ t.Fatalf("mint %s: %v", state, e.logs)
+ }
+ })
+ }
+}
+
+func TestPlan14ProgramFailureStillRecordsHealth(t *testing.T) {
+ for _, census := range []bool{false, true} {
+ t.Run(fmt.Sprint(census), func(t *testing.T) {
+ e, _, _, _ := plan14ProofFixture(t)
+ e.root.mintCensus = func() (bool, error) { return census, nil }
+ e.root.recordProgram = func(liveBuild) error { return os.ErrPermission }
+ e.root.run(context.Background())
+ healthy := release.ReadHealthy(e.root.historyDir(), "bab", "console")
+ if len(healthy) != 1 || healthy[0].SHA256 != e.sha {
+ t.Fatalf("program error suppressed healthy record: %+v", healthy)
+ }
+ if e.proof(t) != nil {
+ t.Fatal("mint ignored program error")
}
- if attack != "positive" && e.proof(t) != nil {
- t.Fatal("root inventory bypassed")
+ if !strings.Contains(strings.Join(e.logs, "\n"), "recording the installed program") {
+ t.Fatalf("missing diagnostic: %v", e.logs)
}
})
}
diff --git a/internal/host/proofroot.go b/internal/host/proofroot.go
index ce6803bcc..2640fe418 100644
--- a/internal/host/proofroot.go
+++ b/internal/host/proofroot.go
@@ -43,6 +43,7 @@ import (
)
type proofRoot struct {
+ mintCensus func() (bool, error)
recordProgram func(liveBuild) error
refusals func(release.HistoryBinding) ([]release.Refusal, error)
obs proofObserver
@@ -124,17 +125,18 @@ func (r proofRoot) observeAndRecord(rt registry.Route, after time.Duration) live
if err != nil {
return o
}
- if r.recordProgram != nil {
- if err := r.recordProgram(b); err != nil {
- o.err = err
- return o
- }
- }
if err := release.RecordHealthy(r.historyDir(), b.binding.Route.Account, b.binding.Route.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)
o.err = err
}
+ if r.recordProgram != nil {
+ if err := r.recordProgram(b); err != nil {
+ r.logf("proof: %s: recording the installed program: %v", rt.Host, err)
+ o.err = errors.Join(o.err, err)
+ }
+ }
+
return o
}
@@ -172,6 +174,19 @@ func (r proofRoot) stillLive(rt registry.Route, o liveObservation) bool {
// 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
+ mintAllowed := false
+ if perr == nil && pol != nil && pol.Prove != nil {
+ var err error
+ if r.mintCensus != nil {
+ mintAllowed, err = r.mintCensus()
+ }
+ if err != nil {
+ mintAllowed = false
+ r.logf("proof: reading root census: %v; not minting", err)
+ } else if !mintAllowed {
+ r.logf("proof: minting paused until this box has a root census")
+ }
+ }
routes, err := r.reg.List()
if err != nil {
r.logf("proof: list routes: %v", err)
@@ -196,7 +211,7 @@ func (r proofRoot) mintAndLog(ctx context.Context, obs proofObservations) {
}
continue
}
- if perr == nil && pol.Proves(rt.App) {
+ if mintAllowed && perr == nil && pol.Proves(rt.App) {
r.mint(ctx, pol, o.build.binding, o.build)
}
}
diff --git a/internal/host/proofroot_test.go b/internal/host/proofroot_test.go
index 57c80c8b6..da239ae84 100644
--- a/internal/host/proofroot_test.go
+++ b/internal/host/proofroot_test.go
@@ -73,6 +73,7 @@ func newProofRootEnv(t *testing.T, policy func(keyFile string) string) *proofRoo
units := &fakeUnits{shows: []unitProps{{ActiveState: "active", InvocationID: "i1", MainPID: 7,
ExecPath: release.LiveLink(opt, observedHost), ActiveEnterTimestamp: e.now.Add(-20 * time.Minute)}}}
e.root = proofRoot{
+ mintCensus: func() (bool, error) { return true, nil },
obs: proofObserver{sys: units, procExe: func(int) string { return staged }, optDir: opt,
now: func() time.Time { return e.now }, resolve: proofFixtureResolver(rt), ready: func(registry.Route) func() error { return func() error { return nil } }},
reg: reg, store: e.store, bakeDir: bake, policyPath: policyPath, readKey: readProvingKey,
diff --git a/internal/provision/testdata/plan14-root-reads-e.tsv b/internal/provision/testdata/plan14-root-reads-e.tsv
index 20d1743e9..9b7e27b3d 100644
--- a/internal/provision/testdata/plan14-root-reads-e.tsv
+++ b/internal/provision/testdata/plan14-root-reads-e.tsv
@@ -1,14 +1,14 @@
# Track E integration after merge3. Earlier baselines, overlays and legacy fingerprints are immutable.
-internal/host/agent.go runAdoptUnderRootRelease authority B3,E1 Production recovery wrapper preserves main exit status for per-host failures; only remaining named routes can probe the census for history selection. Ordinary routes make no census or binding-store reads. Fixtures: TestPlan14ERecoveryOrdinaryPass, TestPlan14Fix4AdoptPerHostFailureExitsZero, TestPlan14Fix5MainEquivalence. whole:Route@10480,whole:Route@17661,whole:Route@17697,whole:Route@7901 c5c055aad44dd599334e4c37a958b836987c19ae69f2a386986b4839776266d0
+internal/host/agent.go runAdoptUnderRootRelease authority B3,E1 E13: signed recovery allows unknown history but preserves usable refusals, including retained originals; verified later promotion clears only D1. Health recording is independent of program recording; minting requires a census. Fixtures: TestPlan14RecoveryUnknownHistory, TestPlan14RecoveryKnownRefusal, TestPlan14RecoveryOriginalRefusalWithUnknownHistory, TestPlan14RootHistoryVerifiedRetryBeforeAttempt, TestPlan14ProgramFailureStillRecordsHealth, TestPlan14RootHistoryMintCensus. whole:Route@10480,whole:Route@17841,whole:Route@17877,whole:Route@7901 2cc358516c72149fd1cccf42f1e20765dba30ec8b98e6c83014fb002261b4d3a
internal/host/agent.go runAdoptWithEnsureSystemUnits authority B3,C,D,E1,E2,E3,E4,F,J1 E1 selects the checked private history, canonical named-unit provider and root mint inventory under release exclusion with claims off or on; reconstruction and restart consumption remain incomplete. file:Open@191 9cdd0a958f5924c9c8d8c5613fd665f2c96a194cd11322adb6f0050fb183b541
-internal/host/namedrecovery.go namedUnitRecovery.repair authority E1,J1 Signed channel target, canonical deployment record, checked named unit and root-owned live pointer authorize stopped-unit pointer repair. Rechecks source, claim selection, pointer and artifact at the effect; does not start a process or change grants. Fixtures: TestPlan14Incident20261006WithLedger and TestPlan14SignedNamedRecoveryAuthority. Uses account/app from the same canonical read as the unit binding. Fixture: TestPlan14SignedNamedRecoveryUsesOneCanonicalRecord. file:Open@2565,registry:Channel@1814,whole:Route@1060,whole:Route@1822 8a42a6a27537be49d30d32891f407789449f9b77d37674fea5cb288d908adf6a
-internal/host/namedrecovery_active.go namedUnitRecovery.recover authority B3,E1 E9 merge ruling 6: E effects require a census; no-census recovery delegates to main or records evidence only. Fixtures: TestPlan14ERecoveryNoCensus, TestPlan14ERecoveryNoCensusResume, TestR2ERecoveryFlagWithoutCensus, TestPlan14ERecoveryRestartOutcome. Census-backed recovery retains captured authority and outcome precedence. file:Open@5386,registry:Channel@2511,registry:Unit@216,registry:Unit@233,registry:Version@3500,registry:Version@3606,whole:Route@1431,whole:Route@202,whole:Route@2513,whole:Route@3426 22054f9fecd0e7c896087188ba5f9cf84ffdf3f5d8afca7574f29569b9311332
+internal/host/namedrecovery.go namedUnitRecovery.repair authority E1,J1 E13: signed recovery allows unknown history but preserves usable refusals, including retained originals; verified later promotion clears only D1. Health recording is independent of program recording; minting requires a census. Fixtures: TestPlan14RecoveryUnknownHistory, TestPlan14RecoveryKnownRefusal, TestPlan14RecoveryOriginalRefusalWithUnknownHistory, TestPlan14RootHistoryVerifiedRetryBeforeAttempt, TestPlan14ProgramFailureStillRecordsHealth, TestPlan14RootHistoryMintCensus. file:Open@2642,registry:Channel@1814,whole:Route@1060,whole:Route@1822 566bc61eafb118d9f6fba29228c372b18c8729549ed9a0bccf32c61b360eb9c3
+internal/host/namedrecovery_active.go namedUnitRecovery.recover authority B3,E1 E13: signed recovery allows unknown history but preserves usable refusals, including retained originals; verified later promotion clears only D1. Health recording is independent of program recording; minting requires a census. Fixtures: TestPlan14RecoveryUnknownHistory, TestPlan14RecoveryKnownRefusal, TestPlan14RecoveryOriginalRefusalWithUnknownHistory, TestPlan14RootHistoryVerifiedRetryBeforeAttempt, TestPlan14ProgramFailureStillRecordsHealth, TestPlan14RootHistoryMintCensus. file:Open@5504,registry:Channel@2558,registry:Unit@216,registry:Unit@233,registry:Version@3618,registry:Version@3724,whole:Route@1478,whole:Route@202,whole:Route@2560,whole:Route@3544 dabec20a8ea93c7d72b6d6c3a30a1ae7c99ffcee4de4c0416fba699734739b92
internal/host/proofbinding.go proofBinding.authorizedRecovery authority B3,E1 Independent signed recovery authority and retained attempt outcomes. Fixtures: TestPlan14SignedRestartWithoutHealthHistory, TestPlan14CapturedRollbackWithoutHealthHistory, TestPlan14IndependentRecoveryRetainsMintBarrier, TestPlan14ActiveRecoveryProcessCrash. a7a7cc9edf8437ed2a1fd4d4ed3d54e9ffab78b21ed3d2867a2254c7836e4f13
internal/host/proofbinding.go proofBindingSource.binding authority B3,J1 Signed named-unit recovery is independent of release and health history. Root rechecks canonical source, unit, process, signed target and trusted live pointer; captures attempt authority before publication; verifies readiness and resumes without repeated effects. Fixtures: TestPlan14SignedActiveFixIgnoresHistory, TestPlan14SignedActiveFixAuthority, TestPlan14SignedFixSupersedesOlderAttempt, TestPlan14ActiveRecoveryProcessCrash, TestPlan14SignedRestartWithoutHealthHistory, TestPlan14CapturedRollbackWithoutHealthHistory, TestPlan14SignedRestartRequiresTrustedLiveLink. file:EvalSymlinks@1176,file:EvalSymlinks@1485,file:EvalSymlinks@518,file:EvalSymlinks@757,registry:Unit@3161,whole:Route@265,whole:Route@3658,whole:Route@3665 56edaf01411de3813fe327973302b1df35e403d2870625fc341355b5bc5cfa0b
internal/host/proofbinding.go proofBindingSource.recovered authority B3,E1 Signed named-unit recovery is independent of release and health history. Root rechecks canonical source, unit, process, signed target and trusted live pointer; captures attempt authority before publication; verifies readiness and resumes without repeated effects. Fixtures: TestPlan14SignedActiveFixIgnoresHistory, TestPlan14SignedActiveFixAuthority, TestPlan14SignedFixSupersedesOlderAttempt, TestPlan14ActiveRecoveryProcessCrash, TestPlan14SignedRestartWithoutHealthHistory, TestPlan14CapturedRollbackWithoutHealthHistory, TestPlan14SignedRestartRequiresTrustedLiveLink. registry:Host@123,whole:Route@125 9ddd5ae1e5b981654e47a0f97cba43b93e32617feddf57918136c57c9943f8ae
internal/host/proofbinding.go proofBindingSource.unitBinding authority B3,J1,E1 Reads canonical deployment record, claim, unit file identities and systemd facts without assuming a running PID. Existing running/rollback binding uses the same verifier. Fixtures: TestPlan14SignedNamedRecoveryAuthority and TestPlan14ProofBindingProvenance. registry:Unit@508,registry:Unit@609,registry:Unit@800,whole:Route@2707,whole:Route@2714 fc6bbd54101f36b986ea48656973667139575d20b75780ad3892915904ef5151
internal/host/proofroot.go proofRoot.mint authority B3,J1,J4,E1 E1 production uses canonical observed programs and complete root refusal inventory; legacy diagnostics do not grant root mint authority. registry:Account@136,registry:App@1338,registry:App@1960,registry:App@2075,registry:App@2279,registry:App@316,registry:App@934,registry:Host@1135,registry:Host@1259,registry:Host@1599,registry:Host@2066,registry:Host@2189,registry:Host@287,registry:Host@469,registry:Host@622,whole:Route@129,whole:Route@144 8c4e2457e74ac917a5be303d74493247190e344585da622e989048fd0e00e61a
-internal/host/proofroot.go proofRoot.observeAndRecord authority B3,J1,J4,E1 E1 production uses canonical observed programs and complete root refusal inventory; legacy diagnostics do not grant root mint authority. file:Readlink@108,registry:Account@443,registry:App@468,registry:Host@151,registry:Host@657,whole:Route@186,whole:Route@453,whole:Route@478 88065f1cfe7ad31178ba65c2672d8dd1e25fab26820290368fc063907f7b3b98
+internal/host/proofroot.go proofRoot.observeAndRecord authority B3,J1,J4,E1 E13: signed recovery allows unknown history but preserves usable refusals, including retained originals; verified later promotion clears only D1. Health recording is independent of program recording; minting requires a census. Fixtures: TestPlan14RecoveryUnknownHistory, TestPlan14RecoveryKnownRefusal, TestPlan14RecoveryOriginalRefusalWithUnknownHistory, TestPlan14RootHistoryVerifiedRetryBeforeAttempt, TestPlan14ProgramFailureStillRecordsHealth, TestPlan14RootHistoryMintCensus. file:Readlink@108,registry:Account@335,registry:App@360,registry:Host@151,registry:Host@549,registry:Host@714,whole:Route@186,whole:Route@345,whole:Route@370 6a3a5e5bd6c0163567e9d2604eb090affeb1058f64922474a81aaa1ca589b578
internal/host/recoveryrefusals.go rootRestartChecks.mintRefusals authority B3,E1 Exact signed recovery retirement binds namespace, complete outcome digest and a later root-verified success; pending originals and changed evidence remain restrictive. Independent settlement publishes status without controlling effects. Fixtures: TestPlan14HistoryExactRecoveryRetirement, TestPlan14HistoryRecoveryRetirementAuthority, TestPlan14HistoryRetirementVisibleSync, TestPlan14HistoryRetirementProcessCrash, TestPlan14RecoveryRetirementProductionCallback, TestPlan14IndependentRecoveryPublishesStatus. 8087848fae0c92cc94908a7565019249aac203dbb3efbc313fafdf4b1a55b49d
internal/host/roothistory.go boundRootHistory authority E1 Exact signed recovery retirement binds namespace, complete outcome digest and a later root-verified success; pending originals and changed evidence remain restrictive. Independent settlement publishes status without controlling effects. Fixtures: TestPlan14HistoryExactRecoveryRetirement, TestPlan14HistoryRecoveryRetirementAuthority, TestPlan14HistoryRetirementVisibleSync, TestPlan14HistoryRetirementProcessCrash, TestPlan14RecoveryRetirementProductionCallback, TestPlan14IndependentRecoveryPublishesStatus. file:Open@1061,file:Read@1379,registry:Account@613,registry:App@635,registry:Channel@456,registry:Channel@470,registry:Host@590,whole:Route@1469,whole:Route@377,whole:Route@472,whole:Route@592,whole:Route@615,whole:Route@637,whole:Route@846 fe46269101116374044658bbb87c1bd44317525dcadf227d4515b47845238be2
internal/host/roothistory.go openRootHistory authority E1 Challenge-bound release-history readback authenticates the deployment request and active enrolled box key, rereads private history with original recovery restrictions, and rechecks roster/fence state. No status grants completeness or changes service effects. Fixtures: TestPlan14HistoryReadback, TestPlan14HistoryReadbackAuthority, TestPlan14HistoryReadbackReadRace, TestPlan14HistoryReadbackReenrollment, TestPlan14HistoryReadbackRequestRefusal, TestPlan14HistoryReadbackProduction, TestPlan14HistoryReadbackProcessCrash. file:Open@104,file:Open@937 92ba5735531ee5fc8b7f907030104a190462d31a7210a81508b01bfa2698fc98
@@ -28,7 +28,7 @@ internal/host/rootrollback.go settleRollbackAttempt authority B3,E1 Independent
internal/host/rootrollback_resume.go recoverRootRestarts authority B3 Recover every attempt independently, skip settled writes and remove leftover capabilities. file:Read@819 0eb3221e553183869f907660c74bae544d48da57f1e5592b92400377f5769a0e
internal/host/rootrollback_resume.go resumeRootRestart authority B3,J1,E1 Signed named-unit recovery is independent of release and health history. Root rechecks canonical source, unit, process, signed target and trusted live pointer; captures attempt authority before publication; verifies readiness and resumes without repeated effects. Fixtures: TestPlan14SignedActiveFixIgnoresHistory, TestPlan14SignedActiveFixAuthority, TestPlan14SignedFixSupersedesOlderAttempt, TestPlan14ActiveRecoveryProcessCrash, TestPlan14SignedRestartWithoutHealthHistory, TestPlan14CapturedRollbackWithoutHealthHistory, TestPlan14SignedRestartRequiresTrustedLiveLink. registry:Account@1419,registry:Account@2125,registry:App@1447,registry:App@2153,registry:Unit@1225,whole:Route@1227,whole:Route@1421,whole:Route@1449,whole:Route@2127,whole:Route@2155 f69e185f75b5758b439be86809c2a83dd300e2e1235879c4860b70cfb5cb39ba
internal/release/adopt.go PassFilter authority B3,E3,J1,E1 Signed named-unit recovery is independent of release and health history. Root rechecks canonical source, unit, process, signed target and trusted live pointer; captures attempt authority before publication; verifies readiness and resumes without repeated effects. Fixtures: TestPlan14SignedActiveFixIgnoresHistory, TestPlan14SignedActiveFixAuthority, TestPlan14SignedFixSupersedesOlderAttempt, TestPlan14ActiveRecoveryProcessCrash, TestPlan14SignedRestartWithoutHealthHistory, TestPlan14CapturedRollbackWithoutHealthHistory, TestPlan14SignedRestartRequiresTrustedLiveLink. E7 applies merge ruling 4: unbound no-census routes use the legacy pass and restart; ordinary candidates make no binding store call. Fixtures: TestR2UnguardedERecoveryUpdate, TestR2UnguardedERecoveryIncidentRecipe, TestPlan14ERecoveryBindingRefusalKeepsPromotedFix. registry:App@26180,registry:App@28637,registry:App@29129,registry:App@29366,registry:App@30773,registry:App@5975,registry:App@9000,registry:App@9141,registry:Hibernate@29012,registry:Host@1055,registry:Host@2746,registry:Host@2813,registry:Host@2909,registry:Host@29182,registry:Host@2944,registry:Host@30098,registry:Host@30826,registry:Host@690,registry:UpdateSettings@8878,whole:Route@1090,whole:Route@1251,whole:Route@2360,whole:Route@2384,whole:Route@2415,whole:Route@2472,whole:Route@2558,whole:Route@2580,whole:Route@26175,whole:Route@2680,whole:Route@28632,whole:Route@28890,whole:Route@28912,whole:Route@28959,whole:Route@28977,whole:Route@28987,whole:Route@29030,whole:Route@29051,whole:Route@29064,whole:Route@29124,whole:Route@29150,whole:Route@29224,whole:Route@29263,whole:Route@29279,whole:Route@29307,whole:Route@29361,whole:Route@29387,whole:Route@30140,whole:Route@30194,whole:Route@30210,whole:Route@30717,whole:Route@30768,whole:Route@30794,whole:Route@30868,whole:Route@30903,whole:Route@30919,whole:Route@3665,whole:Route@4312,whole:Route@579,whole:Route@5897,whole:Route@5919,whole:Route@5970,whole:Route@5996,whole:Route@617,whole:Route@675,whole:Route@793,whole:Route@817,whole:Route@8829,whole:Route@8943,whole:Route@8988,whole:Route@9136 49427f5f398cf7df2b0ac2f63bfacfee3d3c630a253b176493ad87e0f7a49f00
-internal/release/adopt.go adoptHost authority B3,E1,E2,E3,E4,J1 Root history intent is linked to captured canonical rollback authority before live effects; verified recovery settles only its exact intent. Fixtures: TestPlan14HistoryRollbackLink, TestPlan14HistoryRecoveryRefusesChanges, TestPlan14HistoryRecoveryProcessCrash, TestPlan14HistorySettlementRetriesVisibleSync. E1 migration and restart consumption remain incomplete. registry:App@10601,registry:App@10761,registry:App@11790,registry:App@12514,registry:App@14076,registry:App@19235,registry:App@24711,registry:App@3114,registry:App@32470,registry:App@6423,registry:App@7724,registry:App@7918,registry:Hibernate@33331,registry:Host@10481,registry:Host@10514,registry:Host@10614,registry:Host@10774,registry:Host@10876,registry:Host@10908,registry:Host@10973,registry:Host@11423,registry:Host@11556,registry:Host@11986,registry:Host@12112,registry:Host@12604,registry:Host@13380,registry:Host@13848,registry:Host@14062,registry:Host@14404,registry:Host@14994,registry:Host@18725,registry:Host@18833,registry:Host@19203,registry:Host@20023,registry:Host@20181,registry:Host@20643,registry:Host@20695,registry:Host@2075,registry:Host@20842,registry:Host@20929,registry:Host@22035,registry:Host@22279,registry:Host@2264,registry:Host@22939,registry:Host@23324,registry:Host@2345,registry:Host@23465,registry:Host@24679,registry:Host@24806,registry:Host@26148,registry:Host@26798,registry:Host@30031,registry:Host@30649,registry:Host@30974,registry:Host@31048,registry:Host@3127,registry:Host@31295,registry:Host@31446,registry:Host@32642,registry:Host@32809,registry:Host@32916,registry:Host@33144,registry:Host@33287,registry:Host@34032,registry:Host@34158,registry:Host@34246,registry:Host@4248,registry:Host@5211,registry:Host@5316,registry:Host@6535,registry:Host@6583,registry:Host@6700,registry:Host@6732,registry:Host@7610,registry:Host@7778,registry:Host@7904,registry:Host@8023,registry:Host@8501,registry:Host@8888,registry:Host@9114,registry:Host@9210,registry:Host@9301,registry:Kind@552,registry:Unit@31194,registry:Unit@577,whole:Route@10596,whole:Route@10756,whole:Route@11780,whole:Route@12504,whole:Route@14044,whole:Route@14609,whole:Route@14970,whole:Route@16193,whole:Route@16496,whole:Route@16652,whole:Route@18982,whole:Route@19126,whole:Route@19225,whole:Route@2176,whole:Route@21764,whole:Route@22163,whole:Route@24701,whole:Route@29602,whole:Route@3109,whole:Route@32618,whole:Route@3554,whole:Route@6418,whole:Route@9738 a2a16fd0445044393881dbc9b33807831cba5097c25c6fa1c0b36202fb63a5f0
+internal/release/adopt.go adoptHost authority B3,E1,E2,E3,E4,J1 E13: signed recovery allows unknown history but preserves usable refusals, including retained originals; verified later promotion clears only D1. Health recording is independent of program recording; minting requires a census. Fixtures: TestPlan14RecoveryUnknownHistory, TestPlan14RecoveryKnownRefusal, TestPlan14RecoveryOriginalRefusalWithUnknownHistory, TestPlan14RootHistoryVerifiedRetryBeforeAttempt, TestPlan14ProgramFailureStillRecordsHealth, TestPlan14RootHistoryMintCensus. registry:App@10895,registry:App@11055,registry:App@12084,registry:App@12808,registry:App@14370,registry:App@19529,registry:App@25005,registry:App@3114,registry:App@32764,registry:App@6717,registry:App@8018,registry:App@8212,registry:Hibernate@33625,registry:Host@10775,registry:Host@10808,registry:Host@10908,registry:Host@11068,registry:Host@11170,registry:Host@11202,registry:Host@11267,registry:Host@11717,registry:Host@11850,registry:Host@12280,registry:Host@12406,registry:Host@12898,registry:Host@13674,registry:Host@14142,registry:Host@14356,registry:Host@14698,registry:Host@15288,registry:Host@19019,registry:Host@19127,registry:Host@19497,registry:Host@20317,registry:Host@20475,registry:Host@2075,registry:Host@20937,registry:Host@20989,registry:Host@21136,registry:Host@21223,registry:Host@22329,registry:Host@22573,registry:Host@2264,registry:Host@23233,registry:Host@2345,registry:Host@23618,registry:Host@23759,registry:Host@24973,registry:Host@25100,registry:Host@26442,registry:Host@27092,registry:Host@30325,registry:Host@30943,registry:Host@31268,registry:Host@3127,registry:Host@31342,registry:Host@31589,registry:Host@31740,registry:Host@32936,registry:Host@33103,registry:Host@33210,registry:Host@33438,registry:Host@33581,registry:Host@34326,registry:Host@34452,registry:Host@34540,registry:Host@4428,registry:Host@4542,registry:Host@5505,registry:Host@5610,registry:Host@6829,registry:Host@6877,registry:Host@6994,registry:Host@7026,registry:Host@7904,registry:Host@8072,registry:Host@8198,registry:Host@8317,registry:Host@8795,registry:Host@9182,registry:Host@9408,registry:Host@9504,registry:Host@9595,registry:Kind@552,registry:Unit@31488,registry:Unit@577,whole:Route@10032,whole:Route@10890,whole:Route@11050,whole:Route@12074,whole:Route@12798,whole:Route@14338,whole:Route@14903,whole:Route@15264,whole:Route@16487,whole:Route@16790,whole:Route@16946,whole:Route@19276,whole:Route@19420,whole:Route@19519,whole:Route@2176,whole:Route@22058,whole:Route@22457,whole:Route@24995,whole:Route@29896,whole:Route@3109,whole:Route@32912,whole:Route@3554,whole:Route@4306,whole:Route@6712 c62d48c2f006b81d24e2778b83482475d5f2c1fda1c86d0985ebd9c3cf390a61
internal/release/rollback.go writeGroupRefusals authority E1 E1 group refusal writer persists root denial before its diagnostic projection; pending attempt remains restrictive. registry:App@801,whole:Route@783,whole:Route@882,whole:Route@888,whole:Route@952 301386c9d57f9f13db3c31a6cf3a26a44c7ca53ff8d98d910c10a7e8530b8795
internal/release/rootbake.go RootHistory.LoadBake authority E1 E1 strict root bake and app completeness revisions; backward clocks refuse and visible records are durably recommitted. file:Read@261 bd188ea6f2e76d277e1274043bf0f2a447391b1dc540aa400b36ad2573ccd84e
internal/release/roothistory.go RootHistory.read authority E1 Root per-binding refusal/program completeness and pending attempt state; strict decode and exact durable recommit before authority reads. E1 production integration is selected but incomplete. 4af99259d080d2b4c4ffee7cf2bc324e665cb07416031a495692421bd21878b4
@@ -54,15 +54,15 @@ internal/release/roothistory_replace.go replacementReconstruction authority E1 S
internal/release/signedhead.go StageSignedHead authority E1 Canonical route is supplied by the root unit verifier. Verifies signed channel, digest ratchet and manifest/artifact before staging; unavailable pin census never authorizes restaging differing bytes. Fixtures: TestPlan14Incident20261006WithLedger and TestPlan14SignedNamedRecoveryAuthority. registry:Account@202,registry:Account@302,registry:Account@403,registry:App@214,registry:App@415,registry:Channel@222,registry:Host@434 ca02f04e55556ba0233dfa0848b0c533a9d9aa06cad6abd33cfd2968b3bd838a
internal/rootstate/link_linux.go ReadTrustedLink authority A,E1 Checks the full root-owned parent chain before descriptor-based live-link reading. Fixture: TestPlan14TrustedLiveLink; privileged acceptance remains required. file:Open@156 90f8e8de4e2c71f07b5d749cbe4bb1c1dfa426576a82138823449f56fc9ec5ab
internal/rootstate/link_linux.go directory.readLink authority A,E1 Pins the symlink inode with O_PATH and checks owner/type/link count before readlinkat. Fixture: TestPlan14TrustedLiveLink. file:Fstat@356,file:Openat@183,file:Readlinkat@554 a8559fe63dce48830916004ecfe7fbbe5d419568c7daf7a3073cf1b5bb12c3d7
-@coverage internal/host 6cd891a68b42e2fe9c44ff3712ebd24b67b4d1602adb5a67605bdd121eda97d4
-@coverage internal/release 9417f6a8ddd23071820003d222a0c27915295d099496d87216db5a04a5f0bde7
+@coverage internal/host 4c080abdf7dcc48daacaacaf576edd01f1d2a2db7cb41dd3a2a9b21de1039dba
+@coverage internal/release 2536a8c84d34aa2b798cb8af551ed488ea9ce8023d4d2383db4db5c120f08a8b
@coverage internal/rootstate 6338f74a6b2059cf77cf478bd6111b37b441eb9d65a309464c378359d4bdac05
internal/release/roothistory_status.go RootHistory.MigrationStatus authority E1 Exact completion binds the accepted authorization digest from durable private history to enrolled-root signed readback. Pending originals, changed evidence and canonical sources cannot supply retirement completion. Fixtures: TestPlan14HistoryReconstructionCompletion, TestPlan14HistoryRetirementCompletion, TestPlan14ReadReconstructionCompletion, TestPlan14ReadRetirementCompletion, TestPlan14RecoveryRetirementProductionCallback, TestPlan14RootHistoryReconstructionCrash, TestPlan14HistoryRetirementProcessCrash. file:Read@1062,file:Read@3891 f0ab7ad2d2d70f8c22ebfd575f5a1c6931f7eba30ccdba80ad43d0efffc88154
internal/host/legacyrestart.go restartEffectFailed authority B3 E8 merge ruling 5: optional identity failures preserve the same restart outcome; no second restart; real not-up takes main rollback. Fixtures: TestPlan14ERecoveryRestartOutcome, TestPlan14ERecoveryBindingRefusalKeepsPromotedFix, TestPlan14ERecoveryNotUpPrecedesBinding. b68bb65884a14fe0cf78a65f56ded4ea41584bba99af9195827d1ea191b340a9
internal/host/legacyrestart.go legacyRootRestart authority B3,G E9 merge ruling 6: E effects require a census; no-census recovery delegates to main or records evidence only. Fixtures: TestPlan14ERecoveryNoCensus, TestPlan14ERecoveryNoCensusResume, TestR2ERecoveryFlagWithoutCensus, TestPlan14ERecoveryRestartOutcome. Census-backed recovery retains captured authority and outcome precedence. 5fd48683abd82664baa77c2ddc7528f92f557b04586225d64b4a6e77147df931
internal/host/recoveryrefusals.go recoveryHistoryOutcome authority E1 Exact signed recovery retirement binds namespace, complete outcome digest and a later root-verified success; pending originals and changed evidence remain restrictive. Independent settlement publishes status without controlling effects. Fixtures: TestPlan14HistoryExactRecoveryRetirement, TestPlan14HistoryRecoveryRetirementAuthority, TestPlan14HistoryRetirementVisibleSync, TestPlan14HistoryRetirementProcessCrash, TestPlan14RecoveryRetirementProductionCallback, TestPlan14IndependentRecoveryPublishesStatus. ab25e7fa0cca603c50cfee8862c669c674e714b5a8a263748257ce26e1495491
internal/host/recoveryrefusals.go rootRestartChecks.retainRecoveryHistory authority E1 Exact signed recovery retirement binds namespace, complete outcome digest and a later root-verified success; pending originals and changed evidence remain restrictive. Independent settlement publishes status without controlling effects. Fixtures: TestPlan14HistoryExactRecoveryRetirement, TestPlan14HistoryRecoveryRetirementAuthority, TestPlan14HistoryRetirementVisibleSync, TestPlan14HistoryRetirementProcessCrash, TestPlan14RecoveryRetirementProductionCallback, TestPlan14IndependentRecoveryPublishesStatus. 1e1b2235aef0c5b009f311b00ea3fbaae52b4f3bdfa4821a0ff8f6cf80f88c48
-internal/release/roothistory.go RootHistory.Blocks authority E1 E1 retains independently recorded outcomes in private history before minting/reconstruction. Failure bits never clear by replay, ambiguous attempts remain restrictive, and signed reconstruction cannot erase them. Fixtures: TestPlan14IndependentRecoveryRetainsMintBarrier, TestPlan14IndependentRecoveryRecordsHistory, TestPlan14HistoryRetainsRecoveryOutcomes, TestPlan14HistoryRecoveryCannotWeaken, TestPlan14HistoryReconstructionKeepsRecovery, TestPlan14HistoryRecoveryProcessCrash. 4ea9aa88a59c8bbfdea5f76fde1f27385b31ec5bcab4a362d9630f252d744b64
+internal/release/roothistory.go RootHistory.Blocks authority E1 E13: signed recovery allows unknown history but preserves usable refusals, including retained originals; verified later promotion clears only D1. Health recording is independent of program recording; minting requires a census. Fixtures: TestPlan14RecoveryUnknownHistory, TestPlan14RecoveryKnownRefusal, TestPlan14RecoveryOriginalRefusalWithUnknownHistory, TestPlan14RootHistoryVerifiedRetryBeforeAttempt, TestPlan14ProgramFailureStillRecordsHealth, TestPlan14RootHistoryMintCensus. fd918f8f24996cf7f72bb070c44597d417c4de499a4ab29a7f415208477206d1
internal/release/roothistory.go RootHistory.Refusals authority E1 E1 retains independently recorded outcomes in private history before minting/reconstruction. Failure bits never clear by replay, ambiguous attempts remain restrictive, and signed reconstruction cannot erase them. Fixtures: TestPlan14IndependentRecoveryRetainsMintBarrier, TestPlan14IndependentRecoveryRecordsHistory, TestPlan14HistoryRetainsRecoveryOutcomes, TestPlan14HistoryRecoveryCannotWeaken, TestPlan14HistoryReconstructionKeepsRecovery, TestPlan14HistoryRecoveryProcessCrash. 864a1b6119bb0c2985bb02700884daa1654e744a0549cc84371910795a474de4
internal/release/roothistory_outcomes.go RootHistory.RetainRecovery authority E1 Exact signed recovery retirement binds namespace, complete outcome digest and a later root-verified success; pending originals and changed evidence remain restrictive. Independent settlement publishes status without controlling effects. Fixtures: TestPlan14HistoryExactRecoveryRetirement, TestPlan14HistoryRecoveryRetirementAuthority, TestPlan14HistoryRetirementVisibleSync, TestPlan14HistoryRetirementProcessCrash, TestPlan14RecoveryRetirementProductionCallback, TestPlan14IndependentRecoveryPublishesStatus. file:Read@292 2b03f6d1976c9d37748a46ae2323be25163d1622aeb4f456ba4f257f8a89d714
internal/release/roothistory_outcomes.go RootHistory.recoveryOutcomes authority E1 Exact signed recovery retirement binds namespace, complete outcome digest and a later root-verified success; pending originals and changed evidence remain restrictive. Independent settlement publishes status without controlling effects. Fixtures: TestPlan14HistoryExactRecoveryRetirement, TestPlan14HistoryRecoveryRetirementAuthority, TestPlan14HistoryRetirementVisibleSync, TestPlan14HistoryRetirementProcessCrash, TestPlan14RecoveryRetirementProductionCallback, TestPlan14IndependentRecoveryPublishesStatus. file:Read@337 fb5dd3c1b5fe0d4124bb69cc295127f52026e19349c4e81f2d55e437747748db
@@ -121,3 +121,10 @@ internal/release/roothistory_status.go RootHistory.MigrationStatusWithSources au
internal/historyreadback/completion.go ReadReconstruction authority E1 Exact completion binds the accepted authorization digest from durable private history to enrolled-root signed readback. Pending originals, changed evidence and canonical sources cannot supply retirement completion. Fixtures: TestPlan14HistoryReconstructionCompletion, TestPlan14HistoryRetirementCompletion, TestPlan14ReadReconstructionCompletion, TestPlan14ReadRetirementCompletion, TestPlan14RecoveryRetirementProductionCallback, TestPlan14RootHistoryReconstructionCrash, TestPlan14HistoryRetirementProcessCrash. 9f64ca3108a374d26c0f690cd62e3984f25d237175b75a4529a945fda41db5ee
internal/historyreadback/completion.go ReadRetirement authority E1 Exact completion binds the accepted authorization digest from durable private history to enrolled-root signed readback. Pending originals, changed evidence and canonical sources cannot supply retirement completion. Fixtures: TestPlan14HistoryReconstructionCompletion, TestPlan14HistoryRetirementCompletion, TestPlan14ReadReconstructionCompletion, TestPlan14ReadRetirementCompletion, TestPlan14RecoveryRetirementProductionCallback, TestPlan14RootHistoryReconstructionCrash, TestPlan14HistoryRetirementProcessCrash. 051995729c46c84bc96346ca6098c68a4b1317c1d3a2f25655b4d730079534b8
internal/release/roothistory_retirement.go RootHistory.recoveryRetirementDigest authority E1 Exact completion binds the accepted authorization digest from durable private history to enrolled-root signed readback. Pending originals, changed evidence and canonical sources cannot supply retirement completion. Fixtures: TestPlan14HistoryReconstructionCompletion, TestPlan14HistoryRetirementCompletion, TestPlan14ReadReconstructionCompletion, TestPlan14ReadRetirementCompletion, TestPlan14RecoveryRetirementProductionCallback, TestPlan14RootHistoryReconstructionCrash, TestPlan14HistoryRetirementProcessCrash. file:Read@125 4bfcfe969b2230a094ecdf7f4d7653f8bf9f46c10f3e0419a054c0384c2971c8
+internal/host/namedrecovery.go namedUnitRecovery.checkHistory authority E1 E13: signed recovery allows unknown history but preserves usable refusals, including retained originals; verified later promotion clears only D1. Health recording is independent of program recording; minting requires a census. Fixtures: TestPlan14RecoveryUnknownHistory, TestPlan14RecoveryKnownRefusal, TestPlan14RecoveryOriginalRefusalWithUnknownHistory, TestPlan14RootHistoryVerifiedRetryBeforeAttempt, TestPlan14ProgramFailureStillRecordsHealth, TestPlan14RootHistoryMintCensus. registry:Account@155,registry:App@177,registry:Host@132,whole:Route@134,whole:Route@157,whole:Route@179 1ef9a1168e0c3f2982823cd9519c65c089a7cc8599a9baed2512af2f01b31207
+internal/host/namedrecovery.go namedUnitRecovery.originalRefusal authority E1 E13: signed recovery allows unknown history but preserves usable refusals, including retained originals; verified later promotion clears only D1. Health recording is independent of program recording; minting requires a census. Fixtures: TestPlan14RecoveryUnknownHistory, TestPlan14RecoveryKnownRefusal, TestPlan14RecoveryOriginalRefusalWithUnknownHistory, TestPlan14RootHistoryVerifiedRetryBeforeAttempt, TestPlan14ProgramFailureStillRecordsHealth, TestPlan14RootHistoryMintCensus. file:Read@413 69b56334f7b80c972c45c59363fceda6058ce281982b989c622cd4ef22ff096e
+internal/host/namedrecovery.go type namedUnitRecovery authority E1 E13: signed recovery allows unknown history but preserves usable refusals, including retained originals; verified later promotion clears only D1. Health recording is independent of program recording; minting requires a census. Fixtures: TestPlan14RecoveryUnknownHistory, TestPlan14RecoveryKnownRefusal, TestPlan14RecoveryOriginalRefusalWithUnknownHistory, TestPlan14RootHistoryVerifiedRetryBeforeAttempt, TestPlan14ProgramFailureStillRecordsHealth, TestPlan14RootHistoryMintCensus. a8be56e21473ab4cf3073ff0b997efc9a1e333c4a5ce72d59c2b515101999a35
+internal/host/proofroot.go proofRoot.mintAndLog authority E1 E13: signed recovery allows unknown history but preserves usable refusals, including retained originals; verified later promotion clears only D1. Health recording is independent of program recording; minting requires a census. Fixtures: TestPlan14RecoveryUnknownHistory, TestPlan14RecoveryKnownRefusal, TestPlan14RecoveryOriginalRefusalWithUnknownHistory, TestPlan14RootHistoryVerifiedRetryBeforeAttempt, TestPlan14ProgramFailureStillRecordsHealth, TestPlan14RootHistoryMintCensus. file:Stat@2011,registry:App@1014,registry:App@1152,registry:App@855,registry:Host@1069,registry:Host@772,registry:Host@893,registry:Unit@725,registry:Unit@742,whole:Route@685,whole:Route@718,whole:Route@830,whole:Route@927,whole:Route@962 f21b61edec1327ecc45f4920839fe43dd8e4ce152e1621acf053936f0ee16203
+internal/host/proofroot.go type proofRoot authority E1 E13: signed recovery allows unknown history but preserves usable refusals, including retained originals; verified later promotion clears only D1. Health recording is independent of program recording; minting requires a census. Fixtures: TestPlan14RecoveryUnknownHistory, TestPlan14RecoveryKnownRefusal, TestPlan14RecoveryOriginalRefusalWithUnknownHistory, TestPlan14RootHistoryVerifiedRetryBeforeAttempt, TestPlan14ProgramFailureStillRecordsHealth, TestPlan14RootHistoryMintCensus. e3a4fd0a9546176f2bcc278c3aa3152344eb352686558da8a5b06152501c0de6
+internal/release/roothistory_recovery_policy.go RootHistory.RetireD1 authority E1 E13: signed recovery allows unknown history but preserves usable refusals, including retained originals; verified later promotion clears only D1. Health recording is independent of program recording; minting requires a census. Fixtures: TestPlan14RecoveryUnknownHistory, TestPlan14RecoveryKnownRefusal, TestPlan14RecoveryOriginalRefusalWithUnknownHistory, TestPlan14RootHistoryVerifiedRetryBeforeAttempt, TestPlan14ProgramFailureStillRecordsHealth, TestPlan14RootHistoryMintCensus. 1dfff4e199ac8bede5a31d83a2519325f6610ddd35da9d5a1de61fe600b3f079
+internal/release/roothistory_recovery_policy.go RootHistory.recoveryDenials authority E1 E13: signed recovery allows unknown history but preserves usable refusals, including retained originals; verified later promotion clears only D1. Health recording is independent of program recording; minting requires a census. Fixtures: TestPlan14RecoveryUnknownHistory, TestPlan14RecoveryKnownRefusal, TestPlan14RecoveryOriginalRefusalWithUnknownHistory, TestPlan14RootHistoryVerifiedRetryBeforeAttempt, TestPlan14ProgramFailureStillRecordsHealth, TestPlan14RootHistoryMintCensus. file:Read@164,file:Read@847 e1ea25a10a50fbaf0d41be9a154ee946e4b6cd21aa41549dc9afebb154415845
diff --git a/internal/release/adopt.go b/internal/release/adopt.go
index 48bf2e3b3..d2307d834 100644
--- a/internal/release/adopt.go
+++ b/internal/release/adopt.go
@@ -1333,6 +1333,16 @@ func adoptHost(ctx context.Context, work hostwrite.Work, reg *registry.Registry,
if ptr == nil || ver != ptr.Version {
sel = &ChannelPointer{Channel: channel, Version: ver}
}
+ if cfg.requireRootHistory && sel == ptr && ptr != nil {
+ if err := cfg.recheckHistory(ctx, rt); err != nil {
+ cfg.historyError(err)
+ return
+ }
+ if err := cfg.rootHistory.History.RetireD1(cfg.historySelected[rt.Host].binding, ptr); err != nil {
+ cfg.historyError(err)
+ return
+ }
+ }
if cur == ver {
cfg.proof.clear(rt.Host) // live on its target: nothing left to wait for
// A refusal that no longer blocks the pointer is stale even though
diff --git a/internal/release/plan14_recovery_policy_test.go b/internal/release/plan14_recovery_policy_test.go
new file mode 100644
index 000000000..246adeb0d
--- /dev/null
+++ b/internal/release/plan14_recovery_policy_test.go
@@ -0,0 +1,221 @@
+package release
+
+import (
+ "context"
+ "encoding/json"
+ "github.com/carlosframework/platform/internal/hostwrite"
+ "github.com/carlosframework/platform/internal/registry"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+)
+
+func TestPlan14RecoveryUnknownHistory(t *testing.T) {
+ for _, state := range []string{"missing", "corrupt", "incomplete", "pending", "unknown-bake", "denied", "denied-incomplete", "denied-unsynced", "denied-corrupt-programs", "outcome-with-corrupt-host", "outcome-with-corrupt-peer"} {
+ t.Run(state, func(t *testing.T) {
+ h, s, b := rootHistoryFixture(t)
+ at := time.Date(2026, 10, 13, 0, 0, 0, 0, time.UTC)
+ p := &ChannelPointer{Version: "new", PromotedAt: at}
+ denied := state == "denied" || state == "denied-incomplete" || state == "denied-unsynced" || state == "denied-corrupt-programs" || state == "outcome-with-corrupt-host" || state == "outcome-with-corrupt-peer"
+ if state == "denied" || state == "denied-incomplete" || state == "denied-unsynced" || state == "denied-corrupt-programs" {
+ if err := h.AddRefusal(b, Refusal{Host: b.Host, App: b.App, Version: p.Version, PointerPromotedAt: at, RefusedAt: at}); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if state == "incomplete" || state == "denied-incomplete" {
+ r, err := h.read(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+ r.Refusals.Complete = false
+ if err := h.write(r); err != nil {
+ t.Fatal(err)
+ }
+ }
+ switch state {
+ case "missing":
+ if err := os.Remove(filepath.Join(s.dir, historyKey(b))); err != nil {
+ t.Fatal(err)
+ }
+ case "corrupt":
+ if err := os.WriteFile(filepath.Join(s.dir, historyKey(b)), []byte("{"), 0600); err != nil {
+ t.Fatal(err)
+ }
+ case "pending":
+ if err := h.Begin(b, HistoryAttempt{ID: "old-attempt", Version: "old", Source: "source", Started: at}); err != nil {
+ t.Fatal(err)
+ }
+ case "denied-corrupt-programs":
+ r, err := h.read(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+ r.Programs.Values = []string{""}
+ raw, err := json.Marshal(r)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(s.dir, historyKey(b)), raw, 0600); err != nil {
+ t.Fatal(err)
+ }
+ case "denied-unsynced":
+ h.Store = &historyFailNewStore{healthyRetryStore: s, remaining: 100}
+ case "outcome-with-corrupt-host", "outcome-with-corrupt-peer":
+ outcome := HistoryRecovery{Binding: b, Attempt: HistoryAttempt{ID: "failed", Version: p.Version, Source: "source", Started: at, PromotedAt: at, SHA256: "digest", Platform: AdoptPlatform()}, Failed: true}
+ if err := h.RetainRecovery(outcome); err != nil {
+ t.Fatal(err)
+ }
+ key := historyKey(b)
+ if state == "outcome-with-corrupt-peer" {
+ key = "corrupt.outcome"
+ }
+ if err := os.WriteFile(filepath.Join(s.dir, key), []byte("{"), 0600); err != nil {
+ t.Fatal(err)
+ }
+ }
+ blocked, err := h.Blocks(b, p)
+ if err != nil || blocked != denied {
+ t.Fatalf("%s: blocked=%v error=%v", state, blocked, err)
+ }
+ if state == "missing" || state == "corrupt" || state == "incomplete" || state == "pending" {
+ if _, err := h.Refusals(b); err == nil {
+ t.Fatal("recovery policy granted mint completeness")
+ }
+ }
+ })
+ }
+}
+
+func TestPlan14D1VerifiedRetryClear(t *testing.T) {
+ for _, state := range []string{"retry", "same-promotion", "older-promotion", "other-version", "visible-sync-failure"} {
+ t.Run(state, func(t *testing.T) {
+ h, s, b := rootHistoryFixture(t)
+ at := time.Date(2026, 10, 13, 0, 0, 0, 0, time.UTC)
+ for _, kind := range []string{RefusalNotUp, RefusalWrongProgram, RefusalNotProven} {
+ if err := h.AddRefusal(b, Refusal{Host: b.Host, App: b.App, Version: "new", Kind: kind, PointerPromotedAt: at, RefusedAt: at}); err != nil {
+ t.Fatal(err)
+ }
+ }
+ p := &ChannelPointer{Version: "new", PromotedAt: at.Add(time.Second)}
+ switch state {
+ case "same-promotion":
+ p.PromotedAt = at
+ case "older-promotion":
+ p.PromotedAt = at.Add(-time.Second)
+ case "other-version":
+ p.Version = "other"
+ case "visible-sync-failure":
+ h.Store = &historyFailNewStore{healthyRetryStore: s, remaining: 1}
+ }
+ err := h.RetireD1(b, p)
+ if state == "visible-sync-failure" {
+ if err == nil {
+ t.Fatal("unsynced clear accepted")
+ }
+ if err = h.RetireD1(b, p); err != nil {
+ t.Fatal(err)
+ }
+ } else if err != nil {
+ t.Fatal(err)
+ }
+ refs, err := h.Refusals(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+ count := 3
+ if state == "retry" || state == "visible-sync-failure" {
+ count = 2
+ }
+ if len(refs) != count {
+ t.Fatalf("refusals: %+v", refs)
+ }
+ for _, kind := range []string{RefusalWrongProgram, RefusalNotProven} {
+ found := false
+ for _, r := range refs {
+ found = found || r.Kind == kind
+ }
+ if !found {
+ t.Fatalf("retry cleared %s", kind)
+ }
+ }
+ })
+ }
+}
+
+func TestPlan14RootHistoryVerifiedRetryBeforeAttempt(t *testing.T) {
+ for _, state := range []string{"signed-retry", "unsigned", "same-promotion", "already-live", "record-change"} {
+ t.Run(state, func(t *testing.T) {
+ h, _, b := rootHistoryFixture(t)
+ seedRootBake(t, h, b, time.Now().UTC().Add(-time.Hour))
+ st := NewDirStore(t.TempDir())
+ priv, pub := testKeys(t)
+ opt := t.TempDir()
+ reg := testRegistry(t)
+ addRoute(t, reg, b.Host, b.App, "canary/x", false)
+ shipRelease(t, st, b.Account, b.App, "new", adoptPlatform, []byte("new"))
+ at := time.Now().UTC().Truncate(time.Second).Add(-time.Minute)
+ if err := h.AddRefusal(b, Refusal{Host: b.Host, App: b.App, Version: "new", PointerPromotedAt: at, RefusedAt: at}); err != nil {
+ t.Fatal(err)
+ }
+ promotion := at.Add(time.Second)
+ if state == "same-promotion" {
+ promotion = at
+ }
+ if state == "unsigned" {
+ priv, _ = testKeys(t)
+ }
+ writeBackdatedPointer(t, st, priv, b.Account, b.App, "canary/x", "new", promotion)
+ if state == "already-live" {
+ if err := StageArtifact(opt, b.Account, "new", b.App, []byte("new")); err != nil {
+ t.Fatal(err)
+ }
+ if err := PointLive(opt, b.Host, b.Account, "new", b.App); err != nil {
+ t.Fatal(err)
+ }
+ }
+ prepared, reads := 0, 0
+ deps := &RootHistoryPass{History: h,
+ Resolve: func(context.Context, registry.Route) (HistoryBinding, string, error) {
+ reads++
+ if state == "record-change" && reads > 1 {
+ return b, "changed", nil
+ }
+ return b, "record", nil
+ },
+ Installed: func(context.Context, registry.Route, string) (string, error) { return "", nil },
+ Prepare: func(context.Context, HistoryBinding, HistoryAttempt, hostwrite.Identity) error {
+ prepared++
+ r, err := h.read(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(r.Refusals.Values) != 0 {
+ t.Fatal("verified retry was not cleared before attempt preparation")
+ }
+ return os.ErrPermission
+ },
+ }
+ effects := 0
+ _ = Pass(t.Context(), reg, st, pub, opt, func(string) error { effects++; return nil }, WithRootHistory(deps))
+ want := 0
+ if state == "signed-retry" {
+ want = 1
+ }
+ if prepared != want || effects != 0 {
+ t.Fatalf("prepared=%d effects=%d", prepared, effects)
+ }
+ r, err := h.read(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+ retained := 1
+ if state == "signed-retry" || state == "already-live" {
+ retained = 0
+ }
+ if len(r.Refusals.Values) != retained {
+ t.Fatalf("wrong refusal clear: %+v", r)
+ }
+ })
+ }
+}
diff --git a/internal/release/plan14_root_history_process_test.go b/internal/release/plan14_root_history_process_test.go
index a03f0726d..50af1fd20 100644
--- a/internal/release/plan14_root_history_process_test.go
+++ b/internal/release/plan14_root_history_process_test.go
@@ -83,8 +83,14 @@ func TestPlan14RootHistoryProcessCrash(t *testing.T) {
t.Fatalf("lost pending attempt: %+v", r)
}
blocked, err := h.Blocks(b, &ChannelPointer{Version: "other"})
- if err == nil || !blocked {
- t.Fatal("crashed attempt allowed forward work")
+ if err != nil || blocked {
+ t.Fatal("unknown outcome blocked a different signed recovery target")
+ }
+ if err := h.Begin(b, HistoryAttempt{ID: "another", Version: "other", Source: a.Source, Started: a.Started}); err == nil {
+ t.Fatal("crashed intent allowed an unjournalled forward attempt")
+ }
+ if _, err := h.Refusals(b); err == nil {
+ t.Fatal("crashed intent granted mint completeness")
}
}
if _, err := os.Stat(filepath.Join(s.dir, historyKey(b))); err != nil {
diff --git a/internal/release/roothistory.go b/internal/release/roothistory.go
index e900654f7..ceef97f4e 100644
--- a/internal/release/roothistory.go
+++ b/internal/release/roothistory.go
@@ -243,25 +243,14 @@ func (h *RootHistory) AddRefusal(b HistoryBinding, v Refusal) error {
return h.write(next)
}
-// This pre-stage check blocks D1. D2 and D3 are reconsidered only after their
-// independent program and byte checks, so a new retry timestamp cannot clear them.
+// The caller has independently verified the signed target. Missing or corrupt
+// history cannot grant minting, but it does not prevent authorized recovery.
func (h *RootHistory) Blocks(b HistoryBinding, p *ChannelPointer) (bool, error) {
- if p == nil || p.Version == "" {
+ if !b.valid() || p == nil || p.Version == "" {
return true, os.ErrPermission
}
- r, err := h.read(b)
- if err != nil {
- return true, err
- }
- if !r.Refusals.Complete || r.Attempt != nil {
- return true, os.ErrPermission
- }
- refs, err := h.Refusals(b)
- if err != nil {
- return true, err
- }
- for _, v := range refs {
- if p != nil && v.Version == p.Version && (v.Kind != RefusalNotUp || v.Blocks(p)) {
+ for _, v := range h.recoveryDenials(b) {
+ if v.Version == p.Version && (v.Kind != RefusalNotUp || v.Blocks(p)) {
return true, nil
}
}
diff --git a/internal/release/roothistory_recovery_policy.go b/internal/release/roothistory_recovery_policy.go
new file mode 100644
index 000000000..805b3fe1d
--- /dev/null
+++ b/internal/release/roothistory_recovery_policy.go
@@ -0,0 +1,76 @@
+package release
+
+import (
+ "os"
+ "slices"
+ "strings"
+
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+// Recovery uses usable denials without asserting that the inventory is complete.
+// Minting continues to require Refusals and its durable completeness checks.
+func (h *RootHistory) recoveryDenials(b HistoryBinding) []Refusal {
+ if h == nil || h.Store == nil || !b.valid() {
+ return nil
+ }
+ var refs []Refusal
+ raw, err := h.Store.Read(historyKey(b))
+ var record historyRecord
+ if err == nil && len(raw) <= rootstate.MaxRecord && strictCanonicalJSON(raw, &record) == nil && record.Schema == 1 && record.Binding == b {
+ for _, ref := range record.Refusals.Values {
+ if ref.Schema == 1 && ref.Host == b.Host && ref.App == b.App && ref.Version != "" && !ref.RefusedAt.IsZero() && ref.MoveOp == "" &&
+ (ref.Kind == RefusalNotUp || ref.Kind == RefusalWrongProgram || ref.Kind == RefusalNotProven) {
+ refs = append(refs, ref)
+ }
+ }
+ }
+ names, _ := h.Store.Names()
+ var outcomes []HistoryRecovery
+ for _, name := range names {
+ if !strings.HasSuffix(name, ".outcome") {
+ continue
+ }
+ raw, err := h.Store.Read(name)
+ var r historyRecoveryRecord
+ if err != nil || len(raw) > rootstate.MaxRecord || strictCanonicalJSON(raw, &r) != nil || r.Schema != 1 || !validHistoryRecovery(r.HistoryRecovery) || historyRecoveryKey(r.HistoryRecovery) != name {
+ continue
+ }
+ outcomes = append(outcomes, r.HistoryRecovery)
+ }
+ for _, r := range outcomes {
+ if r.Binding != b || (!r.Failed && !r.Recovering) {
+ continue
+ }
+ retired, err := h.recoveryRetired(r, outcomes)
+ if err == nil && retired {
+ continue
+ }
+ refs = append(refs, Refusal{Schema: 1, Host: b.Host, App: b.App, Version: r.Attempt.Version, SHA256: r.Attempt.SHA256, Platform: r.Attempt.Platform, PointerPromotedAt: r.Attempt.PromotedAt, RefusedAt: r.Attempt.Started, Kind: RefusalNotUp})
+ }
+ return refs
+}
+
+// RetireD1 accepts only an independently verified later promotion of this
+// version. Program and proof refusals keep their separate authorization rules.
+func (h *RootHistory) RetireD1(b HistoryBinding, p *ChannelPointer) error {
+ if p == nil || p.Version == "" || p.PromotedAt.IsZero() {
+ return os.ErrPermission
+ }
+ r, err := h.read(b)
+ if err != nil {
+ return err
+ }
+ refs := slices.DeleteFunc(slices.Clone(r.Refusals.Values), func(v Refusal) bool {
+ return v.Kind == RefusalNotUp && v.Version == p.Version && p.PromotedAt.After(v.PointerPromotedAt)
+ })
+ if len(refs) == len(r.Refusals.Values) {
+ return nil
+ }
+ if r.Revision == ^uint64(0) {
+ return os.ErrPermission
+ }
+ r.Revision++
+ r.Refusals.Values, r.Refusals.Revision = refs, r.Revision
+ return h.write(r)
+}