WIP claim Plan 14 track E checkpoint 16 (not for merge): E1 fresh-install evidence
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
18 files changed,
+925
−18
internal/host/claimlaunch_production.go+3 −0internal/host/claimlaunch_root.go+63 −0internal/host/claimretire_transport.go+1 −0internal/host/claimunit.go+1 −1internal/host/claimunit_test.go+1 −1internal/host/plan14_fresh_install_process_test.go+100 −0internal/host/plan14_fresh_install_test.go+271 −0internal/host/roothistory.go+1 −0internal/host/roothistory_creation.go+84 −0internal/host/roothistory_launch.go+55 −0internal/host/rootinventory.go+23 −4internal/provision/plan14_e_write_census_test.go+3 −0internal/provision/testdata/plan14-root-reads-e.tsv+24 −6internal/release/adopt.go+9 −2internal/release/plan14_fresh_install_test.go+215 −0internal/release/roothistory.go+9 −1internal/release/roothistory_creation.go+50 −0internal/release/roothistory_pass.go+12 −3
diff --git a/internal/host/claimlaunch_production.go b/internal/host/claimlaunch_production.go| index 041037098..f661ca022 100644 |
| --- a/internal/host/claimlaunch_production.go |
| +++ b/internal/host/claimlaunch_production.go |
| @@ -64,6 +64,7 @@ func WithProductionClaimLaunch(ctx context.Context, handler http.Handler) (http. |
| return nil, &ClaimDependencyError{Contracts: []string{"claim Plan 11 isolated root launch: production claim assembly"}} |
| } |
| a := entry.Assembly |
| + root.history = productionCreationHistory |
| wrapped, err := claimLaunchFor(root, a.Identity.Verify, a.ManagedRoots) |
| if err != nil { |
| return nil, err |
| @@ -72,6 +73,8 @@ func WithProductionClaimLaunch(ctx context.Context, handler http.Handler) (http. |
| return nil, ErrClaimLaunchUnavailable |
| } |
| wrapped.Launch.Local = a.Local |
| + wrapped.Launch.history = root.history |
| + wrapped.Launch.installedProgram = wrapped.Launch.rootInstalledProgram |
| return wrapped, nil |
| } |
diff --git a/internal/host/claimlaunch_root.go b/internal/host/claimlaunch_root.go| index 1d5b20e31..8870c832c 100644 |
| --- a/internal/host/claimlaunch_root.go |
| +++ b/internal/host/claimlaunch_root.go |
| @@ -18,6 +18,7 @@ import ( |
| execlease "github.com/carlosframework/platform/internal/hostexec" |
| "github.com/carlosframework/platform/internal/hostguard" |
| "github.com/carlosframework/platform/internal/instancectl" |
| + "github.com/carlosframework/platform/internal/release" |
| "github.com/carlosframework/platform/internal/rootstate" |
| ) |
| @@ -59,6 +60,8 @@ type ClaimLaunchObserved struct { |
| // roster identity, its local launch journal and its own view of the kernel. |
| // A request only names which reservation to run. |
| type ClaimLaunchRoot struct { |
| + history rootHistoryAccess |
| + installedProgram func(context.Context, hostbox.RoleLaunchRequest, instancectl.ClaimUnitStarted, ClaimLaunchObserved) (string, error) |
| executionState rootstate.Store |
| ValidateExecutable func(host, path, digest string) error |
| Local hostbox.LocalAuthority |
| @@ -159,6 +162,19 @@ func (h *ClaimLaunchRoot) launchRecord(ctx context.Context, peer StopPeer, t hos |
| } |
| func (h *ClaimLaunchRoot) start(ctx context.Context, peer StopPeer, q hostbox.RoleLaunchRequest) (hostbox.RoleLaunchGrant, error) { |
| + if h.history == nil { |
| + return h.startRecorded(ctx, peer, q, nil) |
| + } |
| + var out hostbox.RoleLaunchGrant |
| + err := h.history(ctx, func(history *release.RootHistory) error { |
| + var err error |
| + out, err = h.startRecorded(ctx, peer, q, history) |
| + return err |
| + }) |
| + return out, err |
| +} |
| + |
| +func (h *ClaimLaunchRoot) startRecorded(ctx context.Context, peer StopPeer, q hostbox.RoleLaunchRequest, history *release.RootHistory) (hostbox.RoleLaunchGrant, error) { |
| r := *q.Reservation |
| t := r.Target |
| if t.BootID != peer.BootID || t.ControllerID != peer.ControllerID { |
| @@ -178,13 +194,60 @@ func (h *ClaimLaunchRoot) start(ctx context.Context, peer StopPeer, q hostbox.Ro |
| return hostbox.RoleLaunchGrant{}, err |
| } |
| var grant hostbox.RoleLaunchGrant |
| + var binding release.HistoryBinding |
| + if history != nil { |
| + if h.Guard.Root == nil || h.installedProgram == nil { |
| + return grant, ErrClaimLaunchUnavailable |
| + } |
| + view, err := h.Guard.Root.View(ctx, t.Host, hostguard.Captured{Incarnation: t.Incarnation, Stamp: t.Stamp}) |
| + if err != nil { |
| + return grant, err |
| + } |
| + in := view.Installation |
| + if in.Box != t.Box || in.Enrollment != t.Enrollment || in.InventoryRevision == 0 { |
| + return grant, hostbox.ErrChanged |
| + } |
| + binding = release.HistoryBinding{Host: in.Host, Account: in.Account, App: in.App} |
| + } |
| check := func(c *hostclaim.Claim) error { return h.authorize(ctx, peer, q, c) } |
| // The fence, the claim check, the journal intention, the unit start and |
| // the started record all happen under the host guard a root fence also |
| // takes, so no fence or second launch interleaves with them. |
| err = h.Guard.With(ctx, t.Host, hostguard.Captured{Incarnation: t.Incarnation, Stamp: t.Stamp}, check, func() error { |
| + if history != nil { |
| + if err := history.ConsumeCreation(binding); err != nil { |
| + return err |
| + } |
| + } |
| var e error |
| grant, e = h.launchLocked(ctx, peer, q, r) |
| + if e == nil && history != nil { |
| + started := instancectl.ClaimUnitStarted{Unit: grant.Unit, InvocationID: grant.UnitInvocationID, MainPID: grant.PID, ControlGroup: grant.Group.RelativePath} |
| + observed, err := h.waitReady(ctx, started, r.Executable) |
| + if err != nil { |
| + return err |
| + } |
| + if observed.ProcessStart != grant.ProcessStart { |
| + return hostbox.ErrChanged |
| + } |
| + program, err := h.installedProgram(ctx, q, started, observed) |
| + if err != nil { |
| + return err |
| + } |
| + fresh, err := h.Guard.Claims.Read(ctx, t.Host) |
| + if err != nil { |
| + return err |
| + } |
| + if fresh.Absent { |
| + return hostbox.ErrChanged |
| + } |
| + if err := h.authorize(ctx, peer, q, fresh.Claim); err != nil { |
| + return err |
| + } |
| + if err := history.RecordInstalledProgram(binding, program); err != nil { |
| + return err |
| + } |
| + } |
| return e |
| }) |
| return grant, err |
diff --git a/internal/host/claimretire_transport.go b/internal/host/claimretire_transport.go| index 7cec4edf0..0061339f2 100644 |
| --- a/internal/host/claimretire_transport.go |
| +++ b/internal/host/claimretire_transport.go |
| @@ -22,6 +22,7 @@ import ( |
| ) |
| type ClaimRootHTTP struct { |
| + history rootHistoryAccess |
| executionRuntime *claimExecutionRuntime |
| Execution *execlease.RootBroker |
| InventoryActor func(context.Context) (hostbox.Actor, error) |
diff --git a/internal/host/claimunit.go b/internal/host/claimunit.go| index 85e9f9cb3..e9e48fb39 100644 |
| --- a/internal/host/claimunit.go |
| +++ b/internal/host/claimunit.go |
| @@ -96,7 +96,7 @@ func claimUnitContent(name string, content []byte, cfg claimUnitSettings) []byte |
| } |
| return strconv.Quote(strings.ReplaceAll(p, "%", "%%")) |
| } |
| - roots := []string{"/run/carlos-control", "/run/carlos", "/var/lib/carlos-claims", "/run/systemd/system", cfg.data, cfg.opt, "/etc/systemd/system", "/sys/fs/cgroup"} |
| + roots := []string{"/run/carlos-control", "/run/carlos", "/var/lib/carlos-claims", rootReleasePrivate, "/run/systemd/system", cfg.data, cfg.opt, "/etc/systemd/system", "/sys/fs/cgroup"} |
| for i, p := range roots { |
| roots[i] = quote(p) |
| } |
diff --git a/internal/host/claimunit_test.go b/internal/host/claimunit_test.go| index 55c5ae40d..4477e43f6 100644 |
| --- a/internal/host/claimunit_test.go |
| +++ b/internal/host/claimunit_test.go |
| @@ -79,7 +79,7 @@ func TestClaimControlUnitInstallers(t *testing.T) { |
| "EnvironmentFile": {"-/etc/carlos/host.env"}, |
| "StateDirectory": {"carlos-claims"}, |
| "StateDirectoryMode": {"0700"}, |
| - "ReadWritePaths": {"/run/carlos-control /run/carlos /var/lib/carlos-claims /run/systemd/system /data /opt/carlos /etc/systemd/system /sys/fs/cgroup"}, |
| + "ReadWritePaths": {"/run/carlos-control /run/carlos /var/lib/carlos-claims /var/lib/carlos-release/private /run/systemd/system /data /opt/carlos /etc/systemd/system /sys/fs/cgroup"}, |
| "RestrictAddressFamilies": {"AF_UNIX AF_INET AF_INET6"}, |
| "ProtectSystem": {"strict"}, |
| "ProtectHome": {"yes"}, |
diff --git a/internal/host/plan14_fresh_install_process_test.go b/internal/host/plan14_fresh_install_process_test.go| new file mode 100644 |
| index 000000000..3e445d694 |
| --- /dev/null |
| +++ b/internal/host/plan14_fresh_install_process_test.go |
| @@ -0,0 +1,100 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "os" |
| + "os/exec" |
| + "path/filepath" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/provision" |
| + "github.com/carlosframework/platform/internal/release" |
| +) |
| + |
| +func TestPlan14FreshInstallProcessCrash(t *testing.T) { |
| + if boundary := os.Getenv("PLAN14_CREATION_CRASH"); boundary != "" { |
| + dir := os.Getenv("PLAN14_CREATION_DIR") |
| + root, disk := plan14InventoryProcessRoot(t, dir) |
| + hs := &rollbackDisk{dir: filepath.Join(dir, "history")} |
| + h := &release.RootHistory{Store: hs} |
| + root.history = func(_ context.Context, fn func(*release.RootHistory) error) error { return fn(h) } |
| + count := 0 |
| + disk.afterWrite = func(string, []byte) { |
| + count++ |
| + if boundary == "intent" && count == 1 || boundary == "committed" && count == 2 { |
| + os.Exit(73) |
| + } |
| + } |
| + hs.afterWrite = func(string, []byte) { |
| + if boundary == "creation" { |
| + os.Exit(73) |
| + } |
| + } |
| + obs, err := hostclaim.Read(t.Context(), root.Stop.Coordinator.Evidence, recoveryHost) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + holder := obs.Claim.Holder |
| + _, etag, err := root.Stop.Coordinator.Evidence.Get(t.Context(), holder.Key) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + _, client := hostbox.RemoteJournals(root.commitRootInventory) |
| + _, err = client.WriteExpected(t.Context(), hostbox.StopInventory{Complete: true, Host: obs.Host, Incarnation: holder.Incarnation, InstallationStamp: holder.Stamp, Box: "box-1", Enrollment: "en", DirectoryRevision: "1"}, hostbox.InventoryExpectation{RecordKey: holder.Key, RecordETag: etag, Target: hostguard.FenceTarget{Host: obs.Host, Incarnation: holder.Incarnation, Stamp: holder.Stamp, Session: holder.Session, Box: "box-1", Enrollment: "en", ClaimETag: obs.ETag, Op: obs.Claim.Op.ID, Phase: obs.Claim.Op.Phase}}) |
| + t.Fatal("boundary not reached", boundary, err) |
| + } |
| + for _, boundary := range []string{"intent", "creation", "committed"} { |
| + t.Run(boundary, func(t *testing.T) { |
| + dir := t.TempDir() |
| + for _, name := range []string{"root", "data", "opt", "units", "locks", "child", "history"} { |
| + if err := os.Mkdir(filepath.Join(dir, name), 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + f := newRecoveryFixture(t) |
| + obs := f.read() |
| + holder := obs.Claim.Holder |
| + plan14PutRecord(t, f.claims, provision.Record{Schema: 1, Host: obs.Host, Account: holder.Account, App: holder.App, Channel: "stable", CreatedAt: f.now}) |
| + store := release.NewDirStore(filepath.Join(dir, "deployment")) |
| + keys, err := f.claims.List(t.Context(), "") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, key := range keys { |
| + raw, _, err := f.claims.Get(t.Context(), key) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err = store.Put(t.Context(), key, raw, ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + cmd := exec.Command(os.Args[0], "-test.run=^TestPlan14FreshInstallProcessCrash$") |
| + cmd.Env = append(os.Environ(), "PLAN14_CREATION_CRASH="+boundary, "PLAN14_CREATION_DIR="+dir, "TMPDIR="+filepath.Join(dir, "child")) |
| + output, err := cmd.CombinedOutput() |
| + var exit *exec.ExitError |
| + if !errors.As(err, &exit) || exit.ExitCode() != 73 { |
| + t.Fatalf("child: %v %s", err, output) |
| + } |
| + root, _ := plan14InventoryProcessRoot(t, dir) |
| + h := &release.RootHistory{Store: &rollbackDisk{dir: filepath.Join(dir, "history")}} |
| + root.history = func(_ context.Context, fn func(*release.RootHistory) error) error { return fn(h) } |
| + if err := root.recoverRootInventory(t.Context()); err != nil { |
| + t.Fatal(err) |
| + } |
| + view, err := root.Stop.Coordinator.Guard.Root.View(t.Context(), obs.Host, hostguard.Captured{Incarnation: holder.Incarnation, Stamp: holder.Stamp}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + b := release.HistoryBinding{Host: obs.Host, Account: holder.Account, App: holder.App} |
| + fresh, err := h.FreshInstallation(b, rootHistoryInstallation(view.Installation)) |
| + if err != nil || !fresh || view.Installation.InventoryRevision != 1 { |
| + t.Fatalf("recovered: %+v fresh=%v: %v", view, fresh, err) |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/host/plan14_fresh_install_test.go b/internal/host/plan14_fresh_install_test.go| new file mode 100644 |
| index 000000000..e8db1e5f5 |
| --- /dev/null |
| +++ b/internal/host/plan14_fresh_install_test.go |
| @@ -0,0 +1,271 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "crypto/sha256" |
| + "encoding/hex" |
| + "github.com/carlosframework/platform/internal/rootparticipation" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| + "io" |
| + "os" |
| + "strings" |
| + "testing" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/instancectl" |
| + "github.com/carlosframework/platform/internal/provision" |
| + "github.com/carlosframework/platform/internal/release" |
| +) |
| + |
| +func TestPlan14FreshInstallRegistration(t *testing.T) { |
| + for _, mode := range []string{"new", "writer", "relocation", "landing", "store-failure", "replay", "deleted-history"} { |
| + t.Run(mode, func(t *testing.T) { |
| + kind, phase := hostclaim.KindServing, "" |
| + if mode == "relocation" { |
| + kind, phase = hostclaim.KindRelocating, hostclaim.PhaseInstalling |
| + } |
| + if mode == "landing" { |
| + kind, phase = hostclaim.KindLanding, provision.LandChecking |
| + } |
| + f, root, q := plan14InstallRegistration(t, kind, phase) |
| + if mode == "writer" { |
| + o, err := hostclaim.Update(t.Context(), f.claims, f.read(), func(c *hostclaim.Claim) error { c.Holder.WriterBox = "box-1"; c.Holder.Session = 1; return nil }) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + q.Target.ClaimETag, q.Target.Session = o.ETag, 1 |
| + q = plan14PreparedInventory(t, root, q) |
| + } |
| + disk := &rollbackDisk{dir: t.TempDir()} |
| + history := &release.RootHistory{Store: disk} |
| + calls := 0 |
| + root.history = func(ctx context.Context, fn func(*release.RootHistory) error) error { |
| + calls++ |
| + if mode == "store-failure" { |
| + return os.ErrPermission |
| + } |
| + return fn(history) |
| + } |
| + out, err := root.commitRootInventory(t.Context(), q) |
| + if mode == "store-failure" { |
| + if err == nil { |
| + t.Fatal("registration ignored history failure") |
| + } |
| + if _, err = root.Stop.Coordinator.Guard.Capture(q.Target.Host); err == nil { |
| + t.Fatal("registered before release exclusion") |
| + } |
| + return |
| + } |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if out.Revision != 1 || calls != 1 { |
| + t.Fatalf("out=%+v calls=%d", out, calls) |
| + } |
| + in, err := root.Stop.Coordinator.Guard.Root.View(t.Context(), q.Target.Host, hostguard.Captured{Incarnation: q.Target.Incarnation, Stamp: q.Target.Stamp}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + b := release.HistoryBinding{Host: in.Installation.Host, Account: in.Installation.Account, App: in.Installation.App} |
| + id := rootHistoryInstallation(in.Installation) |
| + if mode == "replay" || mode == "deleted-history" { |
| + if mode == "deleted-history" { |
| + names, e := disk.Names() |
| + if e != nil { |
| + t.Fatal(e) |
| + } |
| + for _, name := range names { |
| + if e = disk.Remove(name); e != nil { |
| + t.Fatal(e) |
| + } |
| + } |
| + } |
| + if _, err = root.commitRootInventory(t.Context(), q); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + fresh, err := history.FreshInstallation(b, id) |
| + want := mode == "new" || mode == "replay" |
| + if want && (err != nil || !fresh) { |
| + t.Fatalf("new creation not recorded: %v %v", fresh, err) |
| + } |
| + if !want && err == nil && fresh { |
| + t.Fatal("old installation became fresh") |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestPlan14FreshInstallLaunchObservation(t *testing.T) { |
| + for _, mode := range []string{"ready", "not-ready", "history-failure", "record-failure"} { |
| + t.Run(mode, func(t *testing.T) { |
| + f := newLaunchFixture(t, servingOp()) |
| + f.claims.set(func(c *hostclaim.Claim) { |
| + c.Holder.Account = "bab" |
| + c.Holder.App = "app" |
| + c.Holder.Key = "record" |
| + c.Holder.Admissions = []hostclaim.AdmissionRecord{{Box: f.target.Box, Region: f.target.Region, Session: f.target.Session, Generation: c.Holder.Generation}} |
| + }) |
| + f.root.Guard.Root = hostguard.NewRootAuthority(&rollbackDisk{dir: t.TempDir()}) |
| + in := hostguard.RootInstallation{Host: f.target.Host, Account: "bab", App: "app", Record: "record", Incarnation: f.target.Incarnation, Stamp: f.target.Stamp, Box: f.target.Box, Enrollment: f.target.Enrollment, Migration: 1, InventoryRevision: 1} |
| + if err := f.root.Guard.EstablishRoot(t.Context(), in, func(*hostclaim.Claim) error { return nil }); err != nil { |
| + t.Fatal(err) |
| + } |
| + h := &release.RootHistory{Store: &rollbackDisk{dir: t.TempDir()}} |
| + b := release.HistoryBinding{Host: in.Host, Account: in.Account, App: in.App} |
| + if err := h.RecordCreation(b, rootHistoryInstallation(in)); err != nil { |
| + t.Fatal(err) |
| + } |
| + f.root.history = func(ctx context.Context, fn func(*release.RootHistory) error) error { |
| + if mode == "history-failure" { |
| + return os.ErrPermission |
| + } |
| + return fn(h) |
| + } |
| + observed := 0 |
| + f.root.installedProgram = func(context.Context, hostbox.RoleLaunchRequest, instancectl.ClaimUnitStarted, ClaimLaunchObserved) (string, error) { |
| + observed++ |
| + if mode == "record-failure" { |
| + return "", os.ErrPermission |
| + } |
| + return "actual/program", nil |
| + } |
| + if mode == "not-ready" { |
| + f.kernel.observeErr = os.ErrPermission |
| + } |
| + _, err := f.root.Serve(t.Context(), f.peer, f.req) |
| + if (err == nil) != (mode == "ready") { |
| + t.Fatalf("launch: %v", err) |
| + } |
| + programs, e := h.Programs(b) |
| + if e != nil { |
| + t.Fatal(e) |
| + } |
| + if mode == "ready" { |
| + if len(programs) != 1 || programs[0] != "actual/program" { |
| + t.Fatalf("programs: %v", programs) |
| + } |
| + } else if len(programs) != 0 { |
| + t.Fatal("unobserved program recorded", programs) |
| + } |
| + fresh, e := h.FreshInstallation(b, rootHistoryInstallation(in)) |
| + if e != nil || fresh != (mode == "history-failure") { |
| + t.Fatalf("fresh after attempt: %v %v", fresh, e) |
| + } |
| + if mode == "not-ready" && observed != 0 { |
| + t.Fatal("recorded unready launch") |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestPlan14InstalledLaunchProgramEvidence(t *testing.T) { |
| + path, err := os.Executable() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + file, err := os.Open(path) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + id, err := rootstate.Identity(file) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + raw, err := io.ReadAll(file) |
| + file.Close() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + digest := sha256.Sum256(raw) |
| + for _, mode := range []string{"observed", "staged-only", "digest", "inode", "process", "unit", "authorization"} { |
| + t.Run(mode, func(t *testing.T) { |
| + f := newLaunchFixture(t, servingOp()) |
| + f.req.Executable = path |
| + f.req.Spec.Digest = hex.EncodeToString(digest[:]) |
| + f.req.Reservation.Executable = hostbox.LaunchFile{Device: id.Device, Inode: id.Inode} |
| + seen := f.kernel.observed |
| + seen.Executable = f.req.Reservation.Executable |
| + seen.Running = seen.Executable |
| + f.kernel.observed = seen |
| + switch mode { |
| + case "staged-only": |
| + seen.TenantPID = 0 |
| + f.kernel.observed = seen |
| + case "digest": |
| + f.req.Spec.Digest = strings.Repeat("0", 64) |
| + case "inode": |
| + f.req.Reservation.Executable.Inode++ |
| + case "process": |
| + f.kernel.observed.TenantStart = "successor" |
| + case "unit": |
| + f.units.sameErr = os.ErrPermission |
| + case "authorization": |
| + f.root.ValidateExecutable = func(string, string, string) error { return os.ErrPermission } |
| + } |
| + program, err := f.root.rootInstalledProgram(t.Context(), f.req, f.units.started, seen) |
| + if mode == "observed" { |
| + if err != nil || program != release.FileProgram(path) { |
| + t.Fatalf("program=%q: %v", program, err) |
| + } |
| + } else if err == nil { |
| + t.Fatal("accepted", mode) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestPlan14FreshInstallCanonicalIdentity(t *testing.T) { |
| + for _, mode := range []string{"canonical", "account", "app", "host", "record", "census", "inventory", "service-files"} { |
| + t.Run(mode, func(t *testing.T) { |
| + f := newRestartConsumptionFixture(t) |
| + rt, err := f.e.root.reg.Get(f.claim.Host) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + id, err := f.p.source.creation(t.Context(), *rt) |
| + if err != nil || id == "" { |
| + t.Fatal(id, err) |
| + } |
| + switch mode { |
| + case "account": |
| + rt.Account = "bap" |
| + case "app": |
| + rt.App = "other" |
| + case "host": |
| + rt.Host = "other.example" |
| + case "record": |
| + rec, err := provision.GetRecord(t.Context(), f.e.store, rt.Account, rt.App, rt.Host) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + rec.CreatedAt = rec.CreatedAt.Add(time.Second) |
| + plan14PutRecord(t, f.e.store, *rec) |
| + case "census": |
| + f.p.source.census = func() (rootparticipation.Census, error) { return rootparticipation.Census{}, os.ErrPermission } |
| + case "inventory": |
| + if err := f.p.source.state.Remove(rt.Host + ".json"); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "service-files": |
| + if err := f.e.root.reg.Delete(rt.Host); err != nil { |
| + t.Fatal(err) |
| + } |
| + rt.Caps = "host-role" |
| + rt.GoodVersion = "invented" |
| + rt.Unit = "forged.service" |
| + } |
| + got, err := f.p.source.creation(t.Context(), *rt) |
| + if mode == "canonical" || mode == "service-files" { |
| + if err != nil || got != id { |
| + t.Fatalf("canonical identity changed: %q %v", got, err) |
| + } |
| + } else if err == nil { |
| + t.Fatal("accepted", mode) |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/host/roothistory.go b/internal/host/roothistory.go| index 420c29bcb..8e889a821 100644 |
| --- a/internal/host/roothistory.go |
| +++ b/internal/host/roothistory.go |
| @@ -22,6 +22,7 @@ func openRootHistory(store release.Store, optDir string) (*release.RootHistoryPa |
| } |
| source := productionProofBindingSource(store, systemdUnits{}, optDir) |
| h := boundRootHistory(&release.RootHistory{Store: s}, source.resolve, productionProofObserver(optDir)) |
| + h.Creation = rootHistoryCreation(store) |
| m := rootHistoryMaintenance{history: h.History, store: store, source: source, |
| checks: productionRootRestartChecks(systemdUnits{}, optDir), now: time.Now, |
| readback: func(ctx context.Context, b release.HistoryBinding, snapshot func(context.Context) (release.HistoryMigrationStatus, error)) error { |
diff --git a/internal/host/roothistory_creation.go b/internal/host/roothistory_creation.go| new file mode 100644 |
| index 000000000..4ead4a102 |
| --- /dev/null |
| +++ b/internal/host/roothistory_creation.go |
| @@ -0,0 +1,84 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "crypto/sha256" |
| + "encoding/hex" |
| + "encoding/json" |
| + "os" |
| + |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/provision" |
| + "github.com/carlosframework/platform/internal/registry" |
| + "github.com/carlosframework/platform/internal/release" |
| + "github.com/carlosframework/platform/internal/rootparticipation" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +type rootHistoryAccess func(context.Context, func(*release.RootHistory) error) error |
| + |
| +func productionCreationHistory(ctx context.Context, effect func(*release.RootHistory) error) error { |
| + legacy, err := legacyRootRestart() |
| + if err != nil { |
| + return err |
| + } |
| + if legacy { |
| + return os.ErrPermission |
| + } |
| + return withClaimRootRelease(ctx, true, func() (rootstate.Store, error) { |
| + return rootstate.Open(rootReleasePrivate, true, false) |
| + }, func() error { |
| + s, err := rootstate.Open(release.RootHistoryDir, true, false) |
| + if err != nil { |
| + return err |
| + } |
| + defer s.Close() |
| + return effect(&release.RootHistory{Store: s}) |
| + }) |
| +} |
| + |
| +// Inventory additions do not change the identity of the installation. |
| +func rootHistoryInstallation(in hostguard.RootInstallation) string { |
| + in.Empty = false |
| + in.InventoryRevision = 0 |
| + in.Registration = hostguard.InstallationRegistration{} |
| + raw, _ := json.Marshal(in) |
| + sum := sha256.Sum256(raw) |
| + return hex.EncodeToString(sum[:]) |
| +} |
| + |
| +func rootHistoryCreation(store release.Store) func(context.Context, registry.Route) (string, error) { |
| + return func(ctx context.Context, rt registry.Route) (string, error) { |
| + census, err := rootparticipation.Open() |
| + if err != nil { |
| + return "", err |
| + } |
| + defer census.State.Close() |
| + state, err := rootstate.Open("/var/lib/carlos-claims/authority/fences", false, false) |
| + if err != nil { |
| + return "", err |
| + } |
| + defer state.Close() |
| + return (rootRestartRequestSource{state: state, store: store, census: census.Census}).creation(ctx, rt) |
| + } |
| +} |
| + |
| +func (s rootRestartRequestSource) creation(ctx context.Context, rt registry.Route) (string, error) { |
| + in, err := s.installation(ctx, rt.Host) |
| + if err != nil { |
| + return "", err |
| + } |
| + if in.Host != rt.Host || in.Account != rt.Account || in.App != rt.App || in.Record != provision.RecordKey(in.Account, in.App, in.Host) { |
| + return "", os.ErrPermission |
| + } |
| + rec, _, err := provision.GetRecordETag(ctx, s.store, in.Account, in.App, in.Host) |
| + if err != nil { |
| + return "", err |
| + } |
| + inc, err := hostclaim.Incarnation(rec.Account, rec.App, rec.Host, rec.CreatedAt) |
| + if err != nil || inc != in.Incarnation { |
| + return "", os.ErrPermission |
| + } |
| + return rootHistoryInstallation(in), nil |
| +} |
diff --git a/internal/host/roothistory_launch.go b/internal/host/roothistory_launch.go| new file mode 100644 |
| index 000000000..a53d0892e |
| --- /dev/null |
| +++ b/internal/host/roothistory_launch.go |
| @@ -0,0 +1,55 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "crypto/sha256" |
| + "debug/buildinfo" |
| + "encoding/hex" |
| + "io" |
| + "os" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/instancectl" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| + "github.com/carlosframework/platform/internal/safefile" |
| +) |
| + |
| +func (h *ClaimLaunchRoot) rootInstalledProgram(ctx context.Context, q hostbox.RoleLaunchRequest, started instancectl.ClaimUnitStarted, observed ClaimLaunchObserved) (string, error) { |
| + if q.Reservation == nil || q.Spec == nil || observed.TenantPID <= 0 || observed.TenantStart == "" || observed.Running != q.Reservation.Executable || observed.Executable != observed.Running { |
| + return "", os.ErrPermission |
| + } |
| + if err := h.ValidateExecutable(q.Reservation.Target.Host, q.Executable, q.Spec.Digest); err != nil { |
| + return "", err |
| + } |
| + f, err := safefile.OpenFile(q.Executable, os.O_RDONLY, 0) |
| + if err != nil { |
| + return "", err |
| + } |
| + defer f.Close() |
| + id, err := rootstate.Identity(f) |
| + if err != nil { |
| + return "", err |
| + } |
| + if id.Device != observed.Running.Device || id.Inode != observed.Running.Inode { |
| + return "", os.ErrPermission |
| + } |
| + digest := sha256.New() |
| + if _, err := io.Copy(digest, f); err != nil { |
| + return "", err |
| + } |
| + if hex.EncodeToString(digest.Sum(nil)) != q.Spec.Digest { |
| + return "", os.ErrPermission |
| + } |
| + program := "" |
| + if info, err := buildinfo.Read(f); err == nil { |
| + program = info.Path |
| + } |
| + again, err := h.Kernel.Observe(started) |
| + if err != nil || again != observed { |
| + return "", os.ErrPermission |
| + } |
| + if err := h.Units.Same(ctx, started); err != nil { |
| + return "", err |
| + } |
| + return program, nil |
| +} |
diff --git a/internal/host/rootinventory.go b/internal/host/rootinventory.go| index dc06ae586..c9c06951b 100644 |
| --- a/internal/host/rootinventory.go |
| +++ b/internal/host/rootinventory.go |
| @@ -18,14 +18,28 @@ import ( |
| "github.com/carlosframework/platform/internal/hostclaim" |
| "github.com/carlosframework/platform/internal/hostguard" |
| "github.com/carlosframework/platform/internal/registry" |
| + "github.com/carlosframework/platform/internal/release" |
| "github.com/carlosframework/platform/internal/rootstate" |
| "github.com/carlosframework/platform/internal/safefile" |
| ) |
| func (h ClaimRootHTTP) commitRootInventory(ctx context.Context, q hostbox.JournalRequest) (hostbox.JournalResponse, error) { |
| - return h.commitRootInventoryPending(ctx, q, false) |
| + return h.withRootInventoryHistory(ctx, q, false) |
| } |
| -func (h ClaimRootHTTP) commitRootInventoryPending(ctx context.Context, q hostbox.JournalRequest, pending bool) (hostbox.JournalResponse, error) { |
| +func (h ClaimRootHTTP) withRootInventoryHistory(ctx context.Context, q hostbox.JournalRequest, pending bool) (hostbox.JournalResponse, error) { |
| + if h.history == nil { |
| + return h.commitRootInventoryPending(ctx, q, pending, nil) |
| + } |
| + var out hostbox.JournalResponse |
| + err := h.history(ctx, func(history *release.RootHistory) error { |
| + var err error |
| + out, err = h.commitRootInventoryPending(ctx, q, pending, history) |
| + return err |
| + }) |
| + return out, err |
| +} |
| + |
| +func (h ClaimRootHTTP) commitRootInventoryPending(ctx context.Context, q hostbox.JournalRequest, pending bool, history *release.RootHistory) (hostbox.JournalResponse, error) { |
| var out hostbox.JournalResponse |
| if !pending && (q.V == 2 || q.V == 3) { |
| now := time.Now() |
| @@ -252,7 +266,12 @@ func (h ClaimRootHTTP) commitRootInventoryPending(ctx context.Context, q hostbox |
| } |
| } |
| return nil |
| - }, func(hostguard.RootInstallation) error { |
| + }, func(prior hostguard.RootInstallation) error { |
| + if history != nil && q.Revision == 0 && prior.InventoryRevision == 0 && observed.Claim.Op.Kind == hostclaim.KindServing && holder.Session == 0 && holder.WriterBox == "" { |
| + if err := history.RecordCreation(release.HistoryBinding{Host: in.Host, Account: holder.Account, App: holder.App}, rootHistoryInstallation(installation)); err != nil { |
| + return err |
| + } |
| + } |
| if firstLanding && currentRow == nil { |
| if err := h.Reconcile.Registry.Upsert(canonical); err != nil { |
| return err |
| @@ -346,7 +365,7 @@ func (h ClaimRootHTTP) recoverRootInventory(ctx context.Context) error { |
| if err = json.Unmarshal(raw, &q); err != nil { |
| return err |
| } |
| - if _, err = h.commitRootInventoryPending(ctx, q, true); err != nil { |
| + if _, err = h.withRootInventoryHistory(ctx, q, true); err != nil { |
| if errors.Is(err, hostbox.ErrChanged) || errors.Is(err, hostbox.ErrJournalUncertain) { |
| err = errors.Join(err, h.Stop.Coordinator.Guard.AbandonRootInventory(ctx, q.Target.Host, raw, func(c *hostclaim.Claim) bool { |
| fresh, readErr := hostclaim.Read(ctx, h.Stop.Coordinator.Evidence, q.Target.Host) |
diff --git a/internal/provision/plan14_e_write_census_test.go b/internal/provision/plan14_e_write_census_test.go| index 53839474c..95b7c3819 100644 |
| --- a/internal/provision/plan14_e_write_census_test.go |
| +++ b/internal/provision/plan14_e_write_census_test.go |
| @@ -2,6 +2,9 @@ package provision |
| // Track E overlays the reviewed Part 1 write census without changing its baseline. |
| var plan14EWriteCensusOverlay = []censusEntry{ |
| + {"internal/host/rootinventory.go", "ClaimRootHTTP.commitRootInventoryPending", "key-site", "key:RecordKey", "cbc0c1fa8be968c6dd18a42ff200fe5765f25d35c07ed3ece3b18ad4d619d64c"}, |
| + {"internal/host/rootinventory.go", "ClaimRootHTTP.recoverRootInventory", "key-site", "key:RecordKey", "a828684ad31a409d3e1fc306216d907681f514cb813302eb965a040e5084feda"}, |
| + {"internal/host/roothistory_creation.go", "rootRestartRequestSource.creation", "key-site", "key:RecordKey", "af79a66b746b0f42388010b38ea8130dca6a8d29c4ba5623bfc9c3cc40dd9601"}, |
| {"internal/host/rootrestart_requests.go", "rootRestartRequests.consume", "key-site", "key:RecordKey", "0a9fbbe6f201077e276298b7e666e1ee2a345f1c420335290fec38a676b1ad84"}, |
| {"internal/host/rootrestart_requests_source.go", "rootRestartRequestSource.inventory", "key-site", "key:ParseSidecarKey,key:RecordKey", "1ae4bd07fdbd2b0fccd29cc032b0de4a6c53bc52da7fcbc186a11f8b65b394f9"}, |
| {"internal/host/rootrestart_requests_source.go", "rootRestartRequestSource.target", "key-site", "key:RecordKey", "301b62efbf0281f713cbed7fb15cd244b7d2c479882f4ad930c8d482b51177ff"}, |
diff --git a/internal/provision/testdata/plan14-root-reads-e.tsv b/internal/provision/testdata/plan14-root-reads-e.tsv| index bd9342e77..9261e1ab8 100644 |
| --- a/internal/provision/testdata/plan14-root-reads-e.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-e.tsv |
| @@ -11,7 +11,7 @@ internal/host/proofroot.go proofRoot.mint authority B3,J1,J4,E1 E1 production us |
| 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 |
| +internal/host/roothistory.go openRootHistory authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. file:Open@104,file:Open@978 a9cd1c2ff83ec5fe9eec63fd65ef4a9a6fe845e59a0ac98f98652b767837b3a3 |
| internal/host/roothistory.go recordRootProgram 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. whole:Route@151,whole:Route@297,whole:Route@398 fbbc9451c39309090580486d75da6120b02e1ba054157159846bea057ae0c80f |
| internal/host/roothistory_recovery.go historyRestartAttempt 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. registry:Host@187,registry:Host@361,whole:Route@189,whole:Route@363 1c2af7b90cb56053c1d58c10ccaa851c3aa25b0be9e595bf8e90a013ad0bae66 |
| internal/host/roothistory_recovery.go linkRootHistory 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. file:Prepare@121,registry:Account@374,registry:App@412,whole:Route@376,whole:Route@414 a60324796d2b25b3b130bb0439bbd9a415b679e6b51bc92a44f23f77e2d93e1e |
| @@ -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 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/adopt.go adoptHost authority B3,E1,E2,E3,E4,J1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. registry:App@10895,registry:App@11055,registry:App@12084,registry:App@13010,registry:App@14592,registry:App@19751,registry:App@25227,registry:App@3114,registry:App@32986,registry:App@6717,registry:App@8018,registry:App@8212,registry:Hibernate@33847,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@12297,registry:Host@12406,registry:Host@12608,registry:Host@13100,registry:Host@13886,registry:Host@14364,registry:Host@14578,registry:Host@14920,registry:Host@15510,registry:Host@19241,registry:Host@19349,registry:Host@19719,registry:Host@20539,registry:Host@20697,registry:Host@2075,registry:Host@21159,registry:Host@21211,registry:Host@21358,registry:Host@21445,registry:Host@22551,registry:Host@2264,registry:Host@22795,registry:Host@2345,registry:Host@23455,registry:Host@23840,registry:Host@23981,registry:Host@25195,registry:Host@25322,registry:Host@26664,registry:Host@27314,registry:Host@30547,registry:Host@31165,registry:Host@3127,registry:Host@31490,registry:Host@31564,registry:Host@31811,registry:Host@31962,registry:Host@33158,registry:Host@33325,registry:Host@33432,registry:Host@33660,registry:Host@33803,registry:Host@34548,registry:Host@34674,registry:Host@34762,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@31710,registry:Unit@577,whole:Route@10032,whole:Route@10890,whole:Route@11050,whole:Route@12074,whole:Route@13000,whole:Route@14560,whole:Route@15125,whole:Route@15486,whole:Route@16709,whole:Route@17012,whole:Route@17168,whole:Route@19498,whole:Route@19642,whole:Route@19741,whole:Route@2176,whole:Route@22280,whole:Route@22679,whole:Route@25217,whole:Route@30118,whole:Route@3109,whole:Route@33134,whole:Route@3554,whole:Route@4306,whole:Route@6712 5a5aba93df8af6fa4cda8fc5db5fc7ef9c0ef9f05fd400555eb68b5b06679dd7 |
| 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 |
| @@ -39,12 +39,12 @@ internal/release/roothistory_pass.go RootHistoryPass.CompleteAttempt authority B |
| internal/release/roothistory_pass.go RootHistoryPass.begin authority E1 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. file:Prepare@607,file:Prepare@638,file:Read@292,whole:Route@218 dde605d1d382309a8546c7fb4a78e276909bc7741a83b93c93d0f2ec9c783004 |
| internal/release/roothistory_pass.go RootHistoryPass.complete authority E1 E1 canonical binding selection and recheck; durable attempt precedes effects; only independent installed observation settles and retires the exact target refusal. whole:Route@205,whole:Route@278,whole:Route@350 10a2d72856ad28919399878857ee8d40e60877b86f76ba12d6f98d456001ccd2 |
| internal/release/roothistory_pass.go RootHistoryPass.recheck authority E1 E1 canonical binding selection and recheck; durable attempt precedes effects; only independent installed observation settles and retires the exact target refusal. whole:Route@133 b335ffd791175d39f69451d7f55b9fb59674cacd48ff6221ad3edf9719208038 |
| -internal/release/roothistory_pass.go RootHistoryPass.resolve authority E1 E1 canonical binding selection and recheck; durable attempt precedes effects; only independent installed observation settles and retires the exact target refusal. registry:App@394,registry:Host@341,whole:Route@240,whole:Route@378 7a0d3ca9f00fedcb219693d9ee98a68d03ae67e83b0efd42a943eccebadc7105 |
| +internal/release/roothistory_pass.go RootHistoryPass.resolve authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. registry:App@394,registry:Host@341,whole:Route@240,whole:Route@378,whole:Route@526 5fa39e1cad81603db8562b5f9a07f992085529b2a10ac13d6a76cc0a92e1766c |
| internal/release/roothistory_pass.go RootHistoryPass.selectRoute authority E1 E1 canonical binding selection and recheck; durable attempt precedes effects; only independent installed observation settles and retires the exact target refusal. whole:Route@134,whole:Route@370 83fabcbe7eea1c210666501b217ede898c0f64d22ff88db92b6c0a5790736dcd |
| internal/release/roothistory_pass.go passConfig.bindProof authority E1 E1 canonical binding selection and recheck; durable attempt precedes effects; only independent installed observation settles and retires the exact target refusal. whole:Route@214,whole:Route@277,whole:Route@353 395b72a300204af57355f0d6d037438b1210798f17c26efa9cd3dbef1f098806 |
| internal/release/roothistory_pass.go passConfig.recheckHistory authority E1 E1 canonical binding selection and recheck; durable attempt precedes effects; only independent installed observation settles and retires the exact target refusal. registry:Host@184,whole:Route@162 2a7c830d3889757864bf69ddc17a9ee1cff2151dd27c90e69c9b6e4f85854332 |
| internal/release/roothistory_pass.go passConfig.recordRefusal authority E1 E1 canonical binding selection and recheck; durable attempt precedes effects; only independent installed observation settles and retires the exact target refusal. registry:Host@151,whole:Route@241 298ca88f7dc02be7779aaa527990b4e4548c1fe5498fba0272a6e7041b236354 |
| -internal/release/roothistory_pass.go type RootHistoryPass authority B3,E1 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. file:Prepare@215 c90889be3f234547b634149feceb82515c03de58598912758152f39604f23274 |
| +internal/release/roothistory_pass.go type RootHistoryPass authority B3,E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. file:Prepare@282 6bc3106de363145e70f591b2938a4546d7b1afaf51b0e6ab2cca35ea6bb5caef |
| internal/release/roothistory_reconstruct.go RootHistory.Reconstruct authority E1 Signed replacement binds the exact pending digest and current sources/revisions; old intended refusals and hold bounds remain restrictive. Fixtures: TestPlan14ReconstructionReplacement, TestPlan14ReconstructionReplacementCrash. f5deeb978cef7024adf551c9ac6ef2fce4de88b2d5392affdf3313bc82df0d16 |
| internal/release/roothistory_reconstruct.go RootHistory.ReconstructionStatus 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@189 611a42b0f940b753e22a99266e6aae3384173233f717f3bdef0f38c80dcb00d5 |
| internal/release/roothistory_reconstruct.go RootHistory.reconstructionImages authority E1 E1 signed root-history reconstruction; exact namespace identity, revisions and canonical sources; journaled completion retains denies and stronger bounds. Tests: TestPlan14RootHistoryReconstruction, TestPlan14RootHistoryReconstructionCrash, TestPlan14RootHistoryReconstructionDeliveryPass. file:Read@1190,file:Read@226,file:Read@2946,file:Read@629 cb47b84dd9bbb94a253aca2fa2c95ceaa75ddfa6453c78ece1c1b432e977e79a |
| @@ -54,8 +54,8 @@ 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 b2bc360b179563924a103a527049fe2bfd013f3979f43d5585cf281059a0ce39 |
| -@coverage internal/release 41c455979764d29fd7801abfa4700e0d941341a954e37bbe1de00665ae081ec2 |
| +@coverage internal/host 602c4df9eb0b2143825502d87198495d19f4a4bfcc378a925b7816d474507560 |
| +@coverage internal/release a69c59eb1ff514d78b138292d8fe617887993c0be43bd9132394a95243ac8d3c |
| @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 |
| @@ -162,3 +162,21 @@ internal/host/rootrestart_requests_source.go rootRestartRequestSource.inventory |
| internal/host/rootrestart_requests_source.go rootRestartRequestSource.target authority E1 E15: census-only root unit-restart consumption uses retained installations, canonical records, exact requests, invocation evidence and admission after durable intent under release exclusion. No-census effects use the original consumer. Fixtures: TestPlan14RootRestartProductionConsumption, TestPlan14RootRestartProductionHeldRollover, TestPlan14RootRestartProductionAdmissionAfterIntent, TestPlan14RootRestartProductionUnknown, TestPlan14RootRestartProductionSyncFailure, TestPlan14RootRestartProductionCrash, TestPlan14RootRestartRequestCensusGate. registry:Unit@1211,whole:Route@891 301b62efbf0281f713cbed7fb15cd244b7d2c479882f4ad930c8d482b51177ff |
| internal/host/rootrestart_requests_source.go type rootRestartRequestSource authority E1 E15: census-only root unit-restart consumption uses retained installations, canonical records, exact requests, invocation evidence and admission after durable intent under release exclusion. No-census effects use the original consumer. Fixtures: TestPlan14RootRestartProductionConsumption, TestPlan14RootRestartProductionHeldRollover, TestPlan14RootRestartProductionAdmissionAfterIntent, TestPlan14RootRestartProductionUnknown, TestPlan14RootRestartProductionSyncFailure, TestPlan14RootRestartProductionCrash, TestPlan14RootRestartRequestCensusGate. db7ff5c46b6db3fe88657fddf8b4e17324b896fce9b21042d7f0f63140c1d776 |
| internal/release/rootrestart_history.go RootRestarts.Bindings authority E1 E15: census-only root unit-restart consumption uses retained installations, canonical records, exact requests, invocation evidence and admission after durable intent under release exclusion. No-census effects use the original consumer. Fixtures: TestPlan14RootRestartProductionConsumption, TestPlan14RootRestartProductionHeldRollover, TestPlan14RootRestartProductionAdmissionAfterIntent, TestPlan14RootRestartProductionUnknown, TestPlan14RootRestartProductionSyncFailure, TestPlan14RootRestartProductionCrash, TestPlan14RootRestartRequestCensusGate. file:Read@324 d22b7e6b5710ca0025639395101933bec5ce0bd38768ebcf44a0805f1f159b08 |
| +internal/host/claimlaunch_production.go WithProductionClaimLaunch authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. eddb5f39afb0a4a7e94f8063955159d105f38574e600f15fda8d39c6837183a0 |
| +internal/host/claimlaunch_root.go ClaimLaunchRoot.start authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. fe4bea8c5fff3656ca8e1c42eec41e0d2eedd3a4a3b2f00e1203740cb8ebc4f7 |
| +internal/host/claimlaunch_root.go ClaimLaunchRoot.startRecorded authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. file:Read@2599 1cdb9e6359ed4994f2a7ebd12390f84f3f85c944e2c003cc6911ceae93f201d9 |
| +internal/host/claimlaunch_root.go type ClaimLaunchRoot authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. b01a2842411ca0ef5d5b0d5b189dc6300e1ac8c62e9a5b907a1afe79ca2e66c7 |
| +internal/host/claimretire_transport.go type ClaimRootHTTP authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. e87bb168975b9a7c4334de1c1fc027c556d69597122a6134770e770c719a45be |
| +internal/host/claimunit.go claimUnitContent authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. 38dcfeb754659895bc28b024bc3186c6b12edeb17d087546507fa1299eb8e331 |
| +internal/host/roothistory_creation.go productionCreationHistory authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. file:Open@295,file:Open@374 b4f4f82422cf0b1d874be7a0d9ee4a2032a4704b022bf73b8264097c2a53705c |
| +internal/host/roothistory_creation.go rootHistoryCreation authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. file:Open@190,file:Open@300,whole:Route@535 49c28b2886b2ef03b818b2ad7d9a7f6fec65637f86e4d2f94e54394cac032bcc |
| +internal/host/roothistory_creation.go rootHistoryInstallation authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. 0b2eb471c2bb877739f75172ef05b4fd2f4f3ea0a11b84c0b72b35461a6d98fb |
| +internal/host/roothistory_creation.go rootRestartRequestSource.creation authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. registry:Account@219,registry:App@243,registry:Host@133,registry:Host@194 af79a66b746b0f42388010b38ea8130dca6a8d29c4ba5623bfc9c3cc40dd9601 |
| +internal/host/roothistory_creation.go type rootHistoryAccess authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. 9c97bcd7878d79022ed942d8e6c00e944a00e0fa27a1bc5c732b4f4c10f4e9dd |
| +internal/host/roothistory_launch.go ClaimLaunchRoot.rootInstalledProgram authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. file:OpenFile@541,file:Read@1043 e8d530dd1b8f613dfbd41d34a1b08b19aa3f74e0522c8f41db768ccd4e9e80d2 |
| +internal/host/rootinventory.go ClaimRootHTTP.commitRootInventoryPending authority C,E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. file:Read@1383,file:Read@3414,registry:Account@4778,registry:Account@4793,registry:App@4814,registry:App@4825,registry:ClaimIncarnation@4284,registry:ClaimOwner@4334,registry:ClaimOwner@5317,registry:ClaimOwnership@4876,registry:ClaimOwnership@4898,registry:ClaimOwnership@5631,registry:ClaimOwnership@7632,registry:ClaimRowID@4498,registry:ClaimRowID@4575,registry:ClaimStamp@4312,registry:DBPath@4842,registry:DBPath@4856,registry:Host@4748,registry:Host@4760,whole:ClaimOwnership@4880,whole:ClaimOwnership@4908,whole:ClaimOwnership@5641,whole:ClaimOwnership@7643,whole:Route@3606,whole:Route@3989,whole:Route@4005,whole:Route@4060,whole:Route@4711,whole:Route@4724,whole:Route@4733,whole:Route@4971,whole:Route@5203,whole:Route@5879,whole:Route@6015,whole:Route@6239,whole:Route@7476,whole:Route@8883,whole:Route@8944 cbc0c1fa8be968c6dd18a42ff200fe5765f25d35c07ed3ece3b18ad4d619d64c |
| +internal/release/roothistory_creation.go RootHistory.ConsumeCreation authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. 7ec65199b5afc894ae4aefded6a40f485086d0f8a4c86391eca193f473dca2a1 |
| +internal/release/roothistory_creation.go RootHistory.FreshInstallation authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. 71b326a36350139c9c592b0b5b4b91409831fcffdd0c61d68cf5ec2bf54bbd5b |
| +internal/release/roothistory_creation.go RootHistory.RecordCreation authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. 0b545ab63902eb8f2c8988991786c964a2af53d65eff46d8e53a0747da492b6d |
| +internal/host/rootinventory.go ClaimRootHTTP.recoverRootInventory authority C,E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. file:Read@1371,file:Read@707 a828684ad31a409d3e1fc306216d907681f514cb813302eb965a040e5084feda |
| +internal/host/rootinventory.go ClaimRootHTTP.withRootInventoryHistory authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. 81bc65e0a2018fcdaa8933231d44579429801c2265e6c87c274e9fd8e096c202 |
diff --git a/internal/release/adopt.go b/internal/release/adopt.go| index d2307d834..5f167e8f0 100644 |
| --- a/internal/release/adopt.go |
| +++ b/internal/release/adopt.go |
| @@ -1505,12 +1505,19 @@ func adoptHost(ctx context.Context, work hostwrite.Work, reg *registry.Registry, |
| newProg := FileProgram(VersionPath(optDir, RouteAccount(rt), ver, rt.App)) |
| if (cfg.bakeDir != "" || cfg.requireRootHistory) && newProg != "" { |
| var hist []string |
| + fresh := false |
| if cfg.requireRootHistory { |
| hist, err = cfg.rootHistory.History.Programs(cfg.historySelected[rt.Host].binding) |
| if err != nil { |
| cfg.historyError(err) |
| return |
| } |
| + sel := cfg.historySelected[rt.Host] |
| + fresh, err = cfg.rootHistory.History.FreshInstallation(sel.binding, sel.creation) |
| + if err != nil { |
| + cfg.historyError(err) |
| + return |
| + } |
| } else { |
| hist = ReadPrograms(cfg.bakeDir, rt.Host) |
| } |
| @@ -1524,7 +1531,7 @@ func adoptHost(ctx context.Context, work hostwrite.Work, reg *registry.Registry, |
| hist = append(slices.Clone(hist), live) |
| } |
| } |
| - if (len(hist) > 0 || cfg.requireRootHistory) && !slices.Contains(hist, newProg) && m == nil { |
| + if (len(hist) > 0 || cfg.requireRootHistory && !fresh) && !slices.Contains(hist, newProg) && m == nil { |
| // ensureStaged has the binary but could not obtain its manifest |
| // (no sibling, and the store did not answer), so whether the |
| // release carries --program-change is unknown. Unknown is not a |
| @@ -1540,7 +1547,7 @@ func adoptHost(ctx context.Context, work hostwrite.Work, reg *registry.Registry, |
| if cfg.requireRootHistory { |
| programChange = programChange && ptr.DigestOf(ver) != "" |
| } |
| - if (len(hist) > 0 || cfg.requireRootHistory) && !slices.Contains(hist, newProg) && !programChange { |
| + if (len(hist) > 0 || cfg.requireRootHistory && !fresh) && !slices.Contains(hist, newProg) && !programChange { |
| fmt.Printf(" %s: %s is %s, but this route has only run %s — not pointed live (ship a new version with carlos ship --program-change and promote it, if the change is intended)\n", |
| rt.Host, ver, newProg, strings.Join(hist, ", ")) |
| sha := m.Artifacts[adoptPlatform].SHA256 |
diff --git a/internal/release/plan14_fresh_install_test.go b/internal/release/plan14_fresh_install_test.go| new file mode 100644 |
| index 000000000..2dbe60af0 |
| --- /dev/null |
| +++ b/internal/release/plan14_fresh_install_test.go |
| @@ -0,0 +1,215 @@ |
| +package release |
| + |
| +import ( |
| + "context" |
| + "encoding/json" |
| + "github.com/carlosframework/platform/internal/registry" |
| + "os" |
| + "path/filepath" |
| + "testing" |
| + "time" |
| +) |
| + |
| +func TestPlan14FreshInstallEvidence(t *testing.T) { |
| + for _, state := range []string{"fresh", "empty", "observed", "refused", "attempted", "changed", "corrupt", "sync"} { |
| + t.Run(state, func(t *testing.T) { |
| + h, s, b := rootHistoryFixture(t) |
| + if err := os.Remove(filepath.Join(s.dir, historyKey(b))); err != nil { |
| + t.Fatal(err) |
| + } |
| + if state == "empty" { |
| + if err := h.RecordInstalledProgram(b, ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + } else { |
| + if err := h.RecordCreation(b, "installation"); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + switch state { |
| + case "observed": |
| + if err := h.RecordInstalledProgram(b, ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "refused": |
| + if err := h.AddRefusal(b, Refusal{Host: b.Host, App: b.App, Version: "v1", RefusedAt: time.Now()}); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "attempted": |
| + if err := h.Begin(b, HistoryAttempt{ID: "attempt", Version: "v1", Source: "record", Started: time.Now()}); 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 "sync": |
| + s.fail = 1 |
| + } |
| + identity := "installation" |
| + if state == "changed" { |
| + identity = "successor" |
| + } |
| + fresh, err := h.FreshInstallation(b, identity) |
| + if state == "fresh" { |
| + if err != nil || !fresh { |
| + t.Fatalf("fresh=%v: %v", fresh, err) |
| + } |
| + } else if err == nil && fresh { |
| + t.Fatal("invented fresh installation", state) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestPlan14CreationKeepsHistory(t *testing.T) { |
| + h, _, b := rootHistoryFixture(t) |
| + if err := h.RecordCreation(b, "installation"); err != nil { |
| + t.Fatal(err) |
| + } |
| + p, err := h.Programs(b) |
| + if err != nil || len(p) != 1 || p[0] != "old/program" { |
| + t.Fatalf("lost history: %v %v", p, err) |
| + } |
| + if fresh, err := h.FreshInstallation(b, "installation"); err != nil || fresh { |
| + t.Fatalf("old history became fresh: %v %v", fresh, err) |
| + } |
| +} |
| + |
| +func TestPlan14FreshInstallProgramGate(t *testing.T) { |
| + binary := tinyProgram(t, "first/program") |
| + for _, mode := range []string{"fresh", "unproven", "changed", "consumed", "missing-provider"} { |
| + t.Run(mode, func(t *testing.T) { |
| + h, s, b := rootHistoryFixture(t) |
| + if err := os.Remove(filepath.Join(s.dir, historyKey(b))); err != nil { |
| + t.Fatal(err) |
| + } |
| + if mode == "unproven" { |
| + if err := h.RecordInstalledProgram(b, ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + r, err := h.read(b) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + r.Refusals.Complete = true |
| + if err = h.write(r); err != nil { |
| + t.Fatal(err) |
| + } |
| + } else if err := h.RecordCreation(b, "installation"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if mode == "consumed" { |
| + if err := h.ConsumeCreation(b); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + seedRootBake(t, h, b, time.Now().UTC().Add(-time.Hour)) |
| + st := NewDirStore(t.TempDir()) |
| + priv, pub := testKeys(t) |
| + reg := testRegistry(t) |
| + opt := t.TempDir() |
| + addRoute(t, reg, b.Host, b.App, "canary/x", false) |
| + shipRelease(t, st, b.Account, b.App, "first", adoptPlatform, binary) |
| + if err := Promote(t.Context(), st, priv, b.Account, b.App, "canary/x", "first"); err != nil { |
| + t.Fatal(err) |
| + } |
| + calls, effects := 0, 0 |
| + deps := &RootHistoryPass{History: h, Resolve: func(context.Context, registry.Route) (HistoryBinding, string, error) { return b, "record", nil }, Installed: func(context.Context, registry.Route, string) (string, error) { return "first/program", nil }, Creation: func(context.Context, registry.Route) (string, error) { |
| + calls++ |
| + if mode == "changed" && calls >= 3 { |
| + return "successor", nil |
| + } |
| + return "installation", nil |
| + }} |
| + if mode == "missing-provider" { |
| + deps.Creation = nil |
| + } |
| + err := Pass(t.Context(), reg, st, pub, opt, func(string) error { effects++; return nil }, WithRootHistory(deps)) |
| + v, _ := LiveVersion(opt, b.Host) |
| + if mode == "fresh" { |
| + if err != nil || v != "first" || effects != 1 { |
| + t.Fatalf("fresh install: %s %d %v", v, effects, err) |
| + } |
| + } else if v != "" || effects != 0 { |
| + t.Fatalf("%s installed: %s %d %v", mode, v, effects, err) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestPlan14FreshInstallVisibleSync(t *testing.T) { |
| + h, s, b := rootHistoryFixture(t) |
| + if err := os.Remove(filepath.Join(s.dir, historyKey(b))); err != nil { |
| + t.Fatal(err) |
| + } |
| + s.fail = 2 |
| + if err := h.RecordCreation(b, "installation"); err == nil { |
| + t.Fatal("unsynced creation succeeded") |
| + } |
| + if fresh, err := h.FreshInstallation(b, "installation"); err == nil || fresh { |
| + t.Fatal("visible unsynced creation granted") |
| + } |
| + if fresh, err := h.FreshInstallation(b, "installation"); err != nil || !fresh { |
| + t.Fatalf("durability repair: %v %v", fresh, err) |
| + } |
| + s.fail = 3 |
| + if err := h.ConsumeCreation(b); err == nil { |
| + t.Fatal("unsynced read allowed attempt") |
| + } |
| + if err := h.ConsumeCreation(b); err == nil { |
| + t.Fatal("unsynced read allowed attempt") |
| + } |
| + if err := h.ConsumeCreation(b); err == nil { |
| + t.Fatal("unsynced read allowed attempt") |
| + } |
| + if err := h.ConsumeCreation(b); err != nil { |
| + t.Fatal(err) |
| + } |
| + if fresh, err := h.FreshInstallation(b, "installation"); err != nil || fresh { |
| + t.Fatalf("attempt rearmed: %v %v", fresh, err) |
| + } |
| +} |
| + |
| +type creationSyncFailure struct { |
| + *healthyRetryStore |
| + remaining int |
| +} |
| + |
| +func (s *creationSyncFailure) Write(name string, raw []byte) error { |
| + var r historyRecord |
| + if err := json.Unmarshal(raw, &r); err != nil { |
| + return err |
| + } |
| + if !r.Fresh && s.remaining > 0 { |
| + s.fail = 1 |
| + s.remaining-- |
| + } |
| + return s.healthyRetryStore.Write(name, raw) |
| +} |
| + |
| +func TestPlan14FreshInstallConsumptionSync(t *testing.T) { |
| + h, s, b := rootHistoryFixture(t) |
| + if err := os.Remove(filepath.Join(s.dir, historyKey(b))); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := h.RecordCreation(b, "installation"); err != nil { |
| + t.Fatal(err) |
| + } |
| + h.Store = &creationSyncFailure{healthyRetryStore: s, remaining: 2} |
| + if err := h.ConsumeCreation(b); err == nil { |
| + t.Fatal("visible unsynced attempt permitted launch") |
| + } |
| + if fresh, err := h.FreshInstallation(b, "installation"); err == nil || fresh { |
| + t.Fatal("unrepaired consumption read succeeded") |
| + } |
| + if fresh, err := h.FreshInstallation(b, "installation"); err != nil || fresh { |
| + t.Fatalf("consumption not recovered: %v %v", fresh, err) |
| + } |
| + if err := h.RecordCreation(b, "successor"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if fresh, err := h.FreshInstallation(b, "successor"); err != nil || fresh { |
| + t.Fatalf("replacement erased retained history: %v %v", fresh, err) |
| + } |
| +} |
diff --git a/internal/release/roothistory.go b/internal/release/roothistory.go| index ceef97f4e..e2dce29ef 100644 |
| --- a/internal/release/roothistory.go |
| +++ b/internal/release/roothistory.go |
| @@ -57,6 +57,8 @@ type historyRefusals struct { |
| } |
| type historyRecord struct { |
| + Creation string `json:"creation,omitempty"` |
| + Fresh bool `json:"fresh,omitempty"` |
| Schema int `json:"schema"` |
| Binding HistoryBinding `json:"binding"` |
| Revision uint64 `json:"revision"` |
| @@ -81,6 +83,9 @@ func (b HistoryBinding) valid() bool { |
| } |
| func validHistory(r historyRecord, b HistoryBinding) bool { |
| + if len(r.Creation) > 4096 || (r.Fresh && (r.Creation == "" || !r.Programs.Complete || !r.Refusals.Complete || len(r.Programs.Values) != 0 || len(r.Refusals.Values) != 0 || r.Attempt != nil)) { |
| + return false |
| + } |
| if !b.valid() || r.Schema != 1 || r.Binding != b || r.Revision == 0 || |
| r.Programs.Revision == 0 || r.Programs.Revision > r.Revision || r.Refusals.Revision == 0 || r.Refusals.Revision > r.Revision || |
| len(r.Programs.Values) > historyLimit || len(r.Refusals.Values) > historyLimit { |
| @@ -171,7 +176,7 @@ func (h *RootHistory) RecordInstalledProgram(b HistoryBinding, program string) e |
| } else if err != nil { |
| return err |
| } |
| - if r.Programs.Complete && (program == "" || slices.Contains(r.Programs.Values, program)) { |
| + if !r.Fresh && r.Programs.Complete && (program == "" || slices.Contains(r.Programs.Values, program)) { |
| return nil |
| } |
| if r.Revision == ^uint64(0) { |
| @@ -181,6 +186,7 @@ func (h *RootHistory) RecordInstalledProgram(b HistoryBinding, program string) e |
| r.Programs.Values = append(r.Programs.Values, program) |
| } |
| r.Revision++ |
| + r.Fresh = false |
| r.Programs.Complete, r.Programs.Revision = true, r.Revision |
| return h.write(r) |
| } |
| @@ -229,6 +235,7 @@ func (h *RootHistory) AddRefusal(b HistoryBinding, v Refusal) error { |
| return os.ErrPermission |
| } |
| r.Revision++ |
| + r.Fresh = false |
| r.Refusals.Revision = r.Revision |
| next := r |
| next.Refusals.Values = append(slices.Clone(r.Refusals.Values), v) |
| @@ -272,6 +279,7 @@ func (h *RootHistory) Begin(b HistoryBinding, a HistoryAttempt) error { |
| return os.ErrPermission |
| } |
| r.Revision++ |
| + r.Fresh = false |
| r.Attempt = &a |
| return h.write(r) |
| } |
diff --git a/internal/release/roothistory_creation.go b/internal/release/roothistory_creation.go| new file mode 100644 |
| index 000000000..09143109c |
| --- /dev/null |
| +++ b/internal/release/roothistory_creation.go |
| @@ -0,0 +1,50 @@ |
| +package release |
| + |
| +import ( |
| + "errors" |
| + "os" |
| +) |
| + |
| +// RecordCreation is called only inside root's first-registration transaction, |
| +// after root proves the installation is new. Existing history is preserved. |
| +func (h *RootHistory) RecordCreation(b HistoryBinding, installation string) error { |
| + if !b.valid() || installation == "" || len(installation) > 4096 { |
| + return os.ErrPermission |
| + } |
| + _, err := h.read(b) |
| + if err == nil { |
| + return nil |
| + } |
| + if !errors.Is(err, os.ErrNotExist) { |
| + return err |
| + } |
| + return h.write(historyRecord{Schema: 1, Binding: b, Revision: 1, Creation: installation, Fresh: true, |
| + Programs: historyPrograms{Revision: 1, Complete: true}, Refusals: historyRefusals{Revision: 1, Complete: true}}) |
| +} |
| + |
| +func (h *RootHistory) FreshInstallation(b HistoryBinding, installation string) (bool, error) { |
| + r, err := h.read(b) |
| + if err != nil { |
| + return false, err |
| + } |
| + return installation != "" && r.Creation == installation && r.Fresh, nil |
| +} |
| + |
| +func (h *RootHistory) ConsumeCreation(b HistoryBinding) error { |
| + r, err := h.read(b) |
| + if errors.Is(err, os.ErrNotExist) { |
| + return nil |
| + } |
| + if err != nil { |
| + return err |
| + } |
| + if !r.Fresh { |
| + return nil |
| + } |
| + if r.Revision == ^uint64(0) { |
| + return os.ErrPermission |
| + } |
| + r.Revision++ |
| + r.Fresh = false |
| + return h.write(r) |
| +} |
diff --git a/internal/release/roothistory_pass.go b/internal/release/roothistory_pass.go| index 4af39c775..2fb592030 100644 |
| --- a/internal/release/roothistory_pass.go |
| +++ b/internal/release/roothistory_pass.go |
| @@ -13,6 +13,7 @@ import ( |
| ) |
| type RootHistoryPass struct { |
| + Creation func(context.Context, registry.Route) (string, error) |
| History *RootHistory |
| Resolve func(context.Context, registry.Route) (HistoryBinding, string, error) |
| Installed func(context.Context, registry.Route, string) (string, error) |
| @@ -21,8 +22,9 @@ type RootHistoryPass struct { |
| } |
| type historySelection struct { |
| - binding HistoryBinding |
| - source string |
| + binding HistoryBinding |
| + source string |
| + creation string |
| } |
| func WithRecovery(fn func(context.Context, registry.Route) (bool, error), legacy ...func() (bool, error)) PassOption { |
| @@ -52,7 +54,14 @@ func (h *RootHistoryPass) resolve(ctx context.Context, rt registry.Route) (histo |
| if !b.valid() || source == "" || b.Host != rt.Host || b.Account != RouteAccount(rt) || b.App != rt.App { |
| return historySelection{}, os.ErrPermission |
| } |
| - return historySelection{b, source}, nil |
| + creation := "" |
| + if h.Creation != nil { |
| + creation, err = h.Creation(ctx, rt) |
| + if err != nil { |
| + return historySelection{}, err |
| + } |
| + } |
| + return historySelection{binding: b, source: source, creation: creation}, nil |
| } |
| func (h *RootHistoryPass) selectRoute(ctx context.Context, rt registry.Route) (historySelection, error) { |