carlos / platform Public

region HA 2b: carlos system install-channel

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev c547f40b944ef7fe5fcb573080d3de4d9f22481e parent 6bb2202
7 files changed, +599 −1
  • approved_region_ha_2b_copy_test.go +1 −1
  • internal/host/installchannel.go +269 −0
  • internal/host/installchannel_linux.go +29 −0
  • internal/host/installchannel_other.go +13 −0
  • internal/host/installchannel_test.go +283 −0
  • internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv +3 −0
  • region_ha_boundary_test.go +1 −0
diff --git a/approved_region_ha_2b_copy_test.go b/approved_region_ha_2b_copy_test.go
index 0c166df31..cbcc41beb 100644
--- a/approved_region_ha_2b_copy_test.go
+++ b/approved_region_ha_2b_copy_test.go
@@ -38,7 +38,7 @@ var regionHA2bCopyIDs = func() []string {
// regionHA2bCopyBuilt is every id whose text the code or docs carry. The
// task that writes a row's text adds its ids here in the same commit, once
// Paul has approved the rows.
-var regionHA2bCopyBuilt = []string{"RHB-C1", "RHB-C2", "RHB-C3", "RHB-C4", "RHB-C5", "RHB-C6", "RHB-C7", "RHB-C8", "RHB-C9", "RHB-C10", "RHB-C11", "RHB-C12", "RHB-C13", "RHB-C14", "RHB-C15", "RHB-C16", "RHB-C17", "RHB-C18", "RHB-C64", "RHB-C67", "RHB-C68"}
+var regionHA2bCopyBuilt = []string{"RHB-C1", "RHB-C2", "RHB-C3", "RHB-C4", "RHB-C5", "RHB-C6", "RHB-C7", "RHB-C8", "RHB-C9", "RHB-C10", "RHB-C11", "RHB-C12", "RHB-C13", "RHB-C14", "RHB-C15", "RHB-C16", "RHB-C17", "RHB-C18", "RHB-C64", "RHB-C67", "RHB-C68", "RHB-C19", "RHB-C20", "RHB-C21", "RHB-C22", "RHB-C23", "RHB-C24", "RHB-C25", "RHB-C26", "RHB-C27", "RHB-C28", "RHB-C29", "RHB-C30", "RHB-C31", "RHB-C32", "RHB-C33", "RHB-C34", "RHB-C35", "RHB-C36", "RHB-C37", "RHB-C38", "RHB-C39"}
func mustParseRegionHA2bRecord(t *testing.T) []copyRow {
t.Helper()
diff --git a/internal/host/installchannel.go b/internal/host/installchannel.go
new file mode 100644
index 000000000..cfd7e4ccd
--- /dev/null
+++ b/internal/host/installchannel.go
@@ -0,0 +1,269 @@
+package host
+
+import (
+ "context"
+ "crypto/ed25519"
+ "errors"
+ "flag"
+ "fmt"
+ "os"
+ "path/filepath"
+ "runtime"
+
+ "github.com/carlosframework/platform/internal/cliflag"
+ "github.com/carlosframework/platform/internal/hostfloor"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/version"
+)
+
+// carlos system install-channel (region HA spec 2 §5 step 3, A1-6, A2-2).
+//
+// A group box boots from a bootstrap object, which is whatever build the
+// deployment last uploaded there, not its neighbours' release. This puts
+// the release the box's signed channel pointer names in place instead,
+// through the same path `carlos system update` takes: the pointer verified
+// with CARLOS_RELEASE_PUBKEY (verifyPointer), the artifact fetched against
+// the signed sha256 into the agent's cache, then copied into root's
+// staging, checked again and asked its version (stageCandidate).
+//
+// Both protected slots, carlos and carlos.prev, become that artifact
+// (A2-2): guard-arm checks both against the release identity and refuses
+// a box with any other build in either. The release identity is kept
+// beside them (channelIdentityPath) for the join step's guard-arm, and the
+// release must pass the published host floor (hostfloor.CheckImage, the
+// check guard-arm makes) before anything is fetched.
+//
+// It is for a fresh box. A box whose guard is armed, or that already runs
+// another release this way or through an update, is refused: its releases
+// come through `carlos system update`. Run again on a box it has already
+// installed, it changes nothing.
+
+const installChannelUsage = "usage: carlos system install-channel\n\n" +
+ "Install the release this box's channel names, as root, on a box that has none yet.\n"
+
+const installChannelNotRoot = "carlos system install-channel runs as root: it installs the platform binary"
+
+// channelIdentityPath is where the installed release's signed identity is
+// kept for guard-arm (§5 step 5.3).
+func channelIdentityPath(optDir string) string {
+ return filepath.Join(systemRootDir(optDir), "channel-identity.json")
+}
+
+// channelInstall is one run. fetchTo is release.FetchToFile over the
+// pointer's store, and own hands a directory tree to the service user
+// (ownByService); both are seams for tests.
+type channelInstall struct {
+ a *systemApplier
+ bucket string
+ fetchTo func(ctx context.Context, s release.Store, key, dst, sha string) error
+ own func(path string) error
+}
+
+// channelInstalled is what a run did.
+type channelInstalled struct {
+ Version, Tag, Channel string
+ Already bool
+}
+
+// RunSystemInstallChannel is `carlos system install-channel`.
+func RunSystemInstallChannel(args []string) {
+ fs := flag.NewFlagSet("system install-channel", flag.ExitOnError)
+ fs.Usage = cliflag.Usage(fs, installChannelUsage)
+ _ = fs.Parse(args)
+ if fs.NArg() != 0 {
+ fs.Usage()
+ os.Exit(2)
+ }
+ if os.Geteuid() != 0 {
+ fmt.Fprintln(os.Stderr, installChannelNotRoot)
+ os.Exit(1)
+ }
+ channel, invalid := systemChannel(os.Getenv)
+ if invalid != "" {
+ fmt.Fprintln(os.Stderr, fmt.Sprintf("%s=%q is not a channel, so nothing was installed", systemChannelEnv, invalid))
+ os.Exit(1)
+ }
+ in := channelInstall{
+ a: &systemApplier{ops: productionSysOps(), openStore: openSystemStore, channel: channel, optDir: release.OptDir(),
+ dataDir: dataDir(), etcDir: "/etc", build: version.Build, platform: runtime.GOOS + "-" + runtime.GOARCH},
+ bucket: os.Getenv("CARLOS_DEPLOYMENT_BUCKET"),
+ fetchTo: func(ctx context.Context, s release.Store, key, dst, sha string) error {
+ return release.FetchToFile(ctx, s, key, dst, systemArtifactLimit, sha)
+ },
+ own: ownByService,
+ }
+ got, err := in.run(context.Background())
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ fmt.Println(installedLine(got))
+}
+
+func installedLine(got channelInstalled) string {
+ if got.Already {
+ return fmt.Sprintf("%s (%s) from the %s channel is already installed", got.Version, got.Tag, got.Channel)
+ }
+ return fmt.Sprintf("installed %s (%s) from the %s channel as carlos and carlos.prev", got.Version, got.Tag, got.Channel)
+}
+
+// run takes the apply lock, so no update runs beside it, and installs.
+func (in channelInstall) run(ctx context.Context) (channelInstalled, error) {
+ a := in.a
+ if err := os.MkdirAll(a.rootDir(), 0o755); err != nil {
+ return channelInstalled{}, err
+ }
+ lock, err := os.OpenFile(a.lockPath(), os.O_CREATE|os.O_RDWR, 0o600)
+ if err != nil {
+ return channelInstalled{}, err
+ }
+ defer lock.Close()
+ if err := flockExclusiveNonblock(lock); err != nil {
+ return channelInstalled{}, fmt.Errorf("an update is running on this box, so nothing was installed: %w", err)
+ }
+ return in.install(ctx)
+}
+
+func (in channelInstall) install(ctx context.Context) (channelInstalled, error) {
+ a := in.a
+ var pub ed25519.PublicKey
+ open := a.openStore
+ a.openStore = func(ctx context.Context) (release.Store, ed25519.PublicKey, error) {
+ s, k, err := open(ctx)
+ pub = k
+ return s, k, err
+ }
+ p, err := a.verifyPointer(ctx)
+ if err != nil {
+ return channelInstalled{}, fmt.Errorf("the %s channel could not be read, so nothing was installed: %w", a.channel, err)
+ }
+ got := channelInstalled{Version: p.Current.Version, Tag: p.Current.ReleaseTag, Channel: a.channel}
+ art, ok := p.Current.Artifacts[a.platform]
+ if !ok {
+ return got, fmt.Errorf("the %s channel names %s with no artifact for %s, so nothing was installed", a.channel, p.Current.Version, a.platform)
+ }
+ // A box whose guard is armed takes releases only through an update.
+ guard := hostfloor.Disk{Dir: systemGuardDir(a.optDir), Bucket: in.bucket, PublicKey: pub}
+ if policy, _, err := guard.Load(); err == nil && policy.Mode != "unpublished" {
+ return got, errors.New("this box's host lifecycle guard is armed, so carlos system update installs its releases")
+ } else if err != nil && !errors.Is(err, os.ErrNotExist) {
+ return got, fmt.Errorf("this box's host lifecycle guard could not be read, so nothing was installed: %w", err)
+ }
+ applied, err := readSystemApplied(a.optDir)
+ if err != nil {
+ return got, fmt.Errorf("this box's install record could not be read, so nothing was installed: %w", err)
+ }
+ if applied != nil && applied.Version != p.Current.Version {
+ return got, fmt.Errorf("this box already runs %s, so carlos system update installs its releases", applied.Version)
+ }
+ wire, err := in.checkFloor(ctx, p, art, pub)
+ if err != nil {
+ return got, err
+ }
+ if applied != nil && systemCacheHolds(systemBinaryPath(a.optDir), art.SHA256) && systemCacheHolds(systemPrevPath(a.optDir), art.SHA256) &&
+ systemCacheHolds(channelIdentityPath(a.optDir), hostfloor.Digest(wire)) {
+ got.Already = true
+ return got, nil
+ }
+ cache := systemCachePath(a.dataDir, p.Current.Version)
+ defer os.RemoveAll(filepath.Dir(cache))
+ defer os.RemoveAll(a.stagingDir())
+ if err := in.cacheDir(p.Current.Version); err != nil {
+ return got, err
+ }
+ if err := in.fetchTo(ctx, a.store, art.Key, cache, art.SHA256); err != nil {
+ return got, fmt.Errorf("%s could not be fetched, so nothing was installed: %w", p.Current.Version, err)
+ }
+ staged, err := a.stageCandidate(ctx, p, systemRequest{Version: p.Current.Version, PromotedAt: p.PromotedAt})
+ if err != nil {
+ return got, fmt.Errorf("%s did not verify, so nothing was installed: %w", p.Current.Version, err)
+ }
+ // carlos.prev first: a run cut off between the two copies leaves the
+ // new build in .prev and none in the current slot, which the next run
+ // replaces, never the other way round.
+ for _, slot := range []string{systemPrevPath(a.optDir), systemBinaryPath(a.optDir)} {
+ if err := copyFileDurable(staged, slot, 0o755); err != nil {
+ return got, err
+ }
+ if !systemCacheHolds(slot, art.SHA256) {
+ return got, fmt.Errorf("%s does not hold %s after the copy", slot, p.Current.Version)
+ }
+ }
+ if err := hostfloor.Durable(channelIdentityPath(a.optDir), wire, 0o644); err != nil {
+ return got, err
+ }
+ seen, err := readSystemSeen(a.optDir)
+ if err != nil {
+ return got, err
+ }
+ seen[a.channel] = p.PromotedAt
+ if err := writeJSONDurable(a.file("seen.json"), seen, 0o644); err != nil {
+ return got, err
+ }
+ if err := writeJSONDurable(a.file("applied.json"), systemApplied{Version: p.Current.Version, Channel: a.channel, At: a.ops.now().UTC()}, 0o644); err != nil {
+ return got, err
+ }
+ return got, nil
+}
+
+// checkFloor reads the published host floor and the release's signed
+// identity, and returns the identity once the release passes the floor
+// exactly as guard-arm will check it. No floor, or one that cannot be read,
+// refuses: a joining box could not arm its guard without one, and a read
+// that failed says nothing about where the floor is.
+func (in channelInstall) checkFloor(ctx context.Context, p *release.SystemPointer, art release.SystemArtifact, pub ed25519.PublicKey) ([]byte, error) {
+ s := in.a.store
+ raw, _, err := s.Get(ctx, hostfloor.Key)
+ if errors.Is(err, os.ErrNotExist) {
+ return nil, errors.New("no host floor is published, so this box could not arm its guard and nothing was installed")
+ }
+ if err != nil {
+ return nil, fmt.Errorf("the host floor could not be read, so nothing was installed: %w", err)
+ }
+ auth, _, err := s.Get(ctx, hostfloor.AuthKey)
+ if err != nil {
+ return nil, fmt.Errorf("the host floor could not be read, so nothing was installed: %w", err)
+ }
+ floor, err := hostfloor.Verify(pub, in.bucket, raw, auth)
+ if err != nil {
+ return nil, fmt.Errorf("the host floor does not verify, so nothing was installed: %w", err)
+ }
+ key, err := release.IdentityKey(p.Current.ReleaseTag)
+ if err != nil {
+ return nil, fmt.Errorf("the %s channel names %s with no release tag, so nothing was installed", in.a.channel, p.Current.Version)
+ }
+ wire, _, err := s.Get(ctx, key)
+ if err != nil {
+ return nil, fmt.Errorf("the release identity for %s could not be read, so nothing was installed: %w", p.Current.ReleaseTag, err)
+ }
+ if err := hostfloor.CheckImage(pub, floor, wire, p.Current.Version, in.a.platform, art.SHA256); err != nil {
+ return nil, fmt.Errorf("%s (%s) is below the host floor %s or is not the release its identity names, so nothing was installed", p.Current.Version, p.Current.ReleaseTag, floor.Floor.MinRelease)
+ }
+ return wire, nil
+}
+
+// cacheDir makes the agent's cache entry for version and reconciles the
+// ownership of every directory on its path below the data dir, on every
+// run: system, system/router and system/router/<version> are created as
+// bootstrap creates directories (bootstrapMakeDir: each component opened
+// with no symlink followed, so a symlinked parent is refused) and each is
+// handed to the service user, as bootstrap's chown of /data does
+// (privilegedPlan), whether this run created it or an earlier run that
+// failed before its handoff did. Nothing else is chowned. The agent
+// (User=carlos) later downloads releases beside the entry and writes its
+// update requests there. Only the entry itself is removed after the
+// install; its parents stay, owned by the agent.
+func (in channelInstall) cacheDir(version string) error {
+ parts := systemCacheParts(version)
+ dir := in.a.dataDir
+ for _, part := range parts[:len(parts)-1] {
+ dir = filepath.Join(dir, part)
+ if err := bootstrapMakeDir(dir, 0o755); err != nil {
+ return err
+ }
+ if err := in.own(dir); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/internal/host/installchannel_linux.go b/internal/host/installchannel_linux.go
new file mode 100644
index 000000000..c18b692c8
--- /dev/null
+++ b/internal/host/installchannel_linux.go
@@ -0,0 +1,29 @@
+//go:build linux
+
+package host
+
+import "golang.org/x/sys/unix"
+
+// ownByService hands one directory, and nothing beneath it, to the service
+// user and its group, as bootstrap's ownership step does
+// (bootstrapOwner). It opens the directory as bootstrap opens one
+// (bootstrapOpenDir: no symlink anywhere on the path, not a symlink
+// itself) and changes the open descriptor, so a path swapped for a
+// symlink is refused, never followed.
+func ownByService(path string) error {
+ uid, gid, err := bootstrapOwner(serviceUser, serviceUser)
+ if err != nil {
+ return err
+ }
+ return ownDir(path, uid, gid)
+}
+
+// ownDir is ownByService for a given owner.
+func ownDir(path string, uid, gid int) error {
+ fd, err := bootstrapOpenDir(path)
+ if err != nil {
+ return err
+ }
+ defer unix.Close(fd)
+ return unix.Fchown(fd, uid, gid)
+}
diff --git a/internal/host/installchannel_other.go b/internal/host/installchannel_other.go
new file mode 100644
index 000000000..21faa7f6b
--- /dev/null
+++ b/internal/host/installchannel_other.go
@@ -0,0 +1,13 @@
+//go:build !linux
+
+package host
+
+import "errors"
+
+// errOwnNotLinux is every ownership handoff off Linux: carlos system
+// install-channel runs only on a Linux box.
+var errOwnNotLinux = errors.New("install-channel's ownership handoff requires Linux")
+
+func ownByService(path string) error { return errOwnNotLinux }
+
+func ownDir(path string, uid, gid int) error { return errOwnNotLinux }
diff --git a/internal/host/installchannel_test.go b/internal/host/installchannel_test.go
new file mode 100644
index 000000000..2acdc66f5
--- /dev/null
+++ b/internal/host/installchannel_test.go
@@ -0,0 +1,283 @@
+package host
+
+import (
+ "bytes"
+ "context"
+ "crypto/ed25519"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "os"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/hostfloor"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// channelFixture is a fresh box (applyFixture with no binary on it) whose
+// stable channel names v1.4.0, whose identity is published, and whose
+// deployment has a host floor.
+type channelFixture struct {
+ *applyFixture
+ in channelInstall
+ body []byte
+ fetches int
+ owned []string
+}
+
+func newChannelFixture(t *testing.T, floor string) *channelFixture {
+ t.Helper()
+ f := &channelFixture{applyFixture: newApplyFixture(t, "stable", "bootstrap")}
+ if err := os.Remove(systemBinaryPath(f.a.optDir)); err != nil {
+ t.Fatal(err)
+ }
+ f.body = []byte("#!/bin/sh\necho carlos v1.4.0\n")
+ f.publish(t, "v1.4.0", f.body)
+ if floor != "" {
+ if err := hostfloor.Publish(context.Background(), f.store, release.RootSigner(f.priv), "bucket", hostfloor.Floor{Min: 1, MinRelease: floor}); err != nil {
+ t.Fatal(err)
+ }
+ }
+ f.in = channelInstall{a: f.a, bucket: "bucket", fetchTo: func(ctx context.Context, s release.Store, key, dst, sha string) error {
+ f.fetches++
+ return release.FetchToFile(ctx, s, key, dst, systemArtifactLimit, sha)
+ }, own: func(path string) error {
+ f.owned = append(f.owned, path)
+ return nil
+ }}
+ return f
+}
+
+// publish promotes version on stable with body as its linux-arm64
+// artifact and publishes its release identity.
+func (f *channelFixture) publish(t *testing.T, version string, body []byte) {
+ t.Helper()
+ ctx := context.Background()
+ sum := sha256.Sum256(body)
+ rel := release.SystemRelease{Version: version, ReleaseTag: version, MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{
+ "linux-arm64": {Key: release.SystemArtifactKey(version, "linux-arm64"), SHA256: hex.EncodeToString(sum[:])}}}
+ if _, err := f.store.Put(ctx, rel.Artifacts["linux-arm64"].Key, body, ""); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := release.PromoteSystem(ctx, f.store, release.RootSigner(f.priv), "router", "stable", rel, release.SystemPromoteOptions{}); err != nil {
+ t.Fatal(err)
+ }
+ wire, _, err := f.store.Get(ctx, release.SystemPointerKey("router", "stable"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := release.PublishSystemIdentity(ctx, f.store, f.pub, wire); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func (f *channelFixture) slots(t *testing.T) (cur, prev []byte) {
+ t.Helper()
+ cur, _ = os.ReadFile(systemBinaryPath(f.a.optDir))
+ prev, _ = os.ReadFile(systemPrevPath(f.a.optDir))
+ return cur, prev
+}
+
+func TestInstallChannelPutsTheChannelReleaseInBothSlots(t *testing.T) {
+ f := newChannelFixture(t, "v1.2.0")
+ got, err := f.in.run(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got.Version != "v1.4.0" || got.Tag != "v1.4.0" || got.Already {
+ t.Fatalf("installed %+v", got)
+ }
+ cur, prev := f.slots(t)
+ if !bytes.Equal(cur, f.body) || !bytes.Equal(prev, f.body) {
+ t.Fatalf("slots hold %q and %q, not the channel's artifact", cur, prev)
+ }
+ // The identity guard-arm will be given is the channel release's.
+ held, err := os.ReadFile(channelIdentityPath(f.a.optDir))
+ if err != nil {
+ t.Fatal(err)
+ }
+ p, err := release.VerifySystemIdentity(f.pub, held)
+ if err != nil || p.Current.Version != "v1.4.0" {
+ t.Fatalf("identity %v %v", p, err)
+ }
+ // An update later finds the binary on disk known good, and has seen
+ // this promote.
+ applied, err := readSystemApplied(f.a.optDir)
+ if err != nil || applied == nil || applied.Version != "v1.4.0" || applied.Channel != "stable" {
+ t.Fatalf("applied %+v %v", applied, err)
+ }
+ seen, err := readSystemSeen(f.a.optDir)
+ if err != nil || seen["stable"].IsZero() {
+ t.Fatalf("seen %v %v", seen, err)
+ }
+ // Nothing is left in staging or the agent's cache.
+ if _, err := os.Stat(f.a.stagingDir()); !os.IsNotExist(err) {
+ t.Fatalf("staging left: %v", err)
+ }
+ if _, err := os.Stat(filepath.Dir(systemCachePath(f.a.dataDir, "v1.4.0"))); !os.IsNotExist(err) {
+ t.Fatalf("cache left: %v", err)
+ }
+ // The candidate was asked its version from staging before it was put
+ // in place.
+ if !f.called("run carlos version") {
+ t.Fatalf("calls %v", f.calls)
+ }
+ // R2, S2: every directory on the cache's path is handed to the
+ // service user, and nothing else, so the agent's own update can
+ // download the next release beside them and ask for it.
+ if want := cachePathDirs(f.a.dataDir, "v1.4.0"); !reflect.DeepEqual(f.owned, want) {
+ t.Fatalf("handed to the service user: %v, want %v", f.owned, want)
+ }
+ // Run again: nothing to do, nothing fetched.
+ again, err := f.in.run(context.Background())
+ if err != nil || !again.Already || f.fetches != 1 {
+ t.Fatalf("second run %+v %v, %d fetches", again, err, f.fetches)
+ }
+ if line := installedLine(again); line != "v1.4.0 (v1.4.0) from the stable channel is already installed" {
+ t.Fatalf("line %q", line)
+ }
+ // The agent, its next release promoted, downloads it and asks for it
+ // in the directories install-channel left.
+ leg := &systemLeg{dataDir: f.a.dataDir, fetchTo: func(ctx context.Context, key, dst, sha string) error {
+ return release.FetchToFile(ctx, f.store, key, dst, systemArtifactLimit, sha)
+ }}
+ next := []byte("#!/bin/sh\necho carlos v1.5.0\n")
+ f.publish(t, "v1.5.0", next)
+ sum := sha256.Sum256(next)
+ pending := &systemPending{version: "v1.5.0", artifact: release.SystemArtifact{Key: release.SystemArtifactKey("v1.5.0", "linux-arm64"), SHA256: hex.EncodeToString(sum[:])}}
+ if err := leg.fetch(context.Background(), pending); err != nil {
+ t.Fatalf("the agent's next download: %v", err)
+ }
+ if err := leg.writeRequest(pending, f.now); err != nil {
+ t.Fatalf("the agent's next request: %v", err)
+ }
+}
+
+// cachePathDirs are the directories install-channel hands the service
+// user for version: system, system/router and the version's own.
+func cachePathDirs(dataDir, version string) []string {
+ return []string{filepath.Join(dataDir, "system"), filepath.Join(dataDir, "system", "router"), filepath.Join(dataDir, "system", "router", version)}
+}
+
+// S2: a run whose ownership handoff fails installs nothing; the next run
+// reconciles every directory on the path, including the ones the failed
+// run created, not only the ones it creates itself.
+func TestInstallChannelReconcilesOwnershipOnARetry(t *testing.T) {
+ f := newChannelFixture(t, "v1.2.0")
+ own := f.in.own
+ f.in.own = func(string) error { return errors.New("chown: operation not permitted") }
+ if _, err := f.in.run(context.Background()); err == nil {
+ t.Fatal("a failed handoff installed")
+ }
+ if cur, prev := f.slots(t); cur != nil || prev != nil || f.fetches != 0 {
+ t.Fatalf("a failed handoff wrote %q %q after %d fetches", cur, prev, f.fetches)
+ }
+ // The failed run created system, as root, and never handed it over.
+ if _, err := os.Stat(filepath.Join(f.a.dataDir, "system")); err != nil {
+ t.Fatalf("the failed run's parent: %v", err)
+ }
+ f.in.own = own
+ if _, err := f.in.run(context.Background()); err != nil {
+ t.Fatal(err)
+ }
+ if want := cachePathDirs(f.a.dataDir, "v1.4.0"); !reflect.DeepEqual(f.owned, want) {
+ t.Fatalf("the retry handed %v, want %v", f.owned, want)
+ }
+}
+
+// S2: a symlinked parent is refused before anything is fetched, and never
+// followed; the real handoff opens each directory without following one.
+func TestInstallChannelRefusesASymlinkedParent(t *testing.T) {
+ f := newChannelFixture(t, "v1.2.0")
+ elsewhere := t.TempDir()
+ if err := os.Symlink(elsewhere, filepath.Join(f.a.dataDir, "system")); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := f.in.run(context.Background()); err == nil || f.fetches != 0 || len(f.owned) != 0 {
+ t.Fatalf("symlinked parent: %v, %d fetches, owned %v", err, f.fetches, f.owned)
+ }
+ if ents, _ := os.ReadDir(elsewhere); len(ents) != 0 {
+ t.Fatalf("followed the symlink: %v", ents)
+ }
+ if err := ownDir(filepath.Join(f.a.dataDir, "system"), os.Getuid(), os.Getgid()); err == nil {
+ t.Fatal("the handoff followed a symlink")
+ }
+ if err := ownDir(elsewhere, os.Getuid(), os.Getgid()); err != nil {
+ t.Fatalf("the handoff of a real directory: %v", err)
+ }
+}
+
+func TestInstallChannelRefusesBelowTheFloorAndWithoutOne(t *testing.T) {
+ for _, c := range []struct{ name, floor, want string }{
+ {"below the floor", "v2.0.0", "below the host floor v2.0.0"},
+ {"no floor", "", "no host floor is published"},
+ } {
+ t.Run(c.name, func(t *testing.T) {
+ f := newChannelFixture(t, c.floor)
+ _, err := f.in.run(context.Background())
+ if err == nil || !strings.Contains(err.Error(), c.want) {
+ t.Fatalf("err %v", err)
+ }
+ cur, prev := f.slots(t)
+ if cur != nil || prev != nil || f.fetches != 0 {
+ t.Fatalf("refused install wrote %q %q after %d fetches", cur, prev, f.fetches)
+ }
+ })
+ }
+}
+
+func TestInstallChannelRefusesAnUnreadableStoreAndABadArtifact(t *testing.T) {
+ f := newChannelFixture(t, "v1.2.0")
+ f.a.openStore = func(context.Context) (release.Store, ed25519.PublicKey, error) {
+ return nil, nil, errors.New("no route to bucket")
+ }
+ if _, err := f.in.run(context.Background()); err == nil || !strings.Contains(err.Error(), "could not be read") {
+ t.Fatalf("unreachable store: %v", err)
+ }
+ g := newChannelFixture(t, "v1.2.0")
+ // The object no longer matches the signed sha256.
+ _, etag, _ := g.store.Get(context.Background(), release.SystemArtifactKey("v1.4.0", "linux-arm64"))
+ if _, err := g.store.Put(context.Background(), release.SystemArtifactKey("v1.4.0", "linux-arm64"), []byte("#!/bin/sh\necho carlos v0.0.1\n"), etag); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := g.in.run(context.Background()); err == nil || !strings.Contains(err.Error(), "could not be fetched") {
+ t.Fatalf("swapped artifact: %v", err)
+ }
+ if cur, prev := g.slots(t); cur != nil || prev != nil {
+ t.Fatalf("swapped artifact installed %q %q", cur, prev)
+ }
+}
+
+func TestInstallChannelLeavesAnInstalledBoxToUpdates(t *testing.T) {
+ // An armed guard.
+ f := newChannelFixture(t, "v1.2.0")
+ ctx := context.Background()
+ raw, _, _ := f.store.Get(ctx, hostfloor.Key)
+ auth, _, _ := f.store.Get(ctx, hostfloor.AuthKey)
+ policy, _ := json.Marshal(hostfloor.Policy{Schema: 1, DeploymentBucket: "bucket", Mode: "armed", Floor: raw, Authorization: auth})
+ if err := os.MkdirAll(systemGuardDir(f.a.optDir), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(systemGuardDir(f.a.optDir), "policy.json"), policy, 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := f.in.run(ctx); err == nil || !strings.Contains(err.Error(), "guard is armed") {
+ t.Fatalf("armed guard: %v", err)
+ }
+ // A box an earlier install or update put another release on.
+ g := newChannelFixture(t, "v1.2.0")
+ if _, err := g.in.run(ctx); err != nil {
+ t.Fatal(err)
+ }
+ g.publish(t, "v1.5.0", []byte("#!/bin/sh\necho carlos v1.5.0\n"))
+ if _, err := g.in.run(ctx); err == nil || !strings.Contains(err.Error(), "already runs v1.4.0") {
+ t.Fatalf("installed box: %v", err)
+ }
+ if cur, _ := g.slots(t); !bytes.Equal(cur, g.body) {
+ t.Fatalf("installed box changed to %q", cur)
+ }
+}
diff --git a/internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv b/internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv
index 7bc13951e..b5d5840cd 100644
--- a/internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv
+++ b/internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv
@@ -1,7 +1,10 @@
# Plan 14 census overlay region-ha-2b (region HA spec 2, plan 2b: docs/superpowers/plans/2026-10-17-region-ha-2b-joining.md), loaded last. RULED by the Plan 14 controller 2026-10-16 as census-ruling-region-ha-2b.
# Task 2: host.env from the deployment's parameters (internal/hostenv: hostenv.go, the layout, assembly and rendering, with an allowlist of names, shared values written create-only and the group-box marker added on write; cli.go, carlos system host-env write and seed; write_linux.go, the write through directories held by descriptor, and write_other.go, its refusal off Linux; awsssm.go, the SSM adapter) and the SSM SDK in go.mod. Three new root-read units. runHostEnv: seed's --from file. Ruled authority A,J4 on conditions (no box write to the parameters; allowlisted names, safe values, atomic write, checked parent; authority-selecting values cross-checked against the roster at the agent's use). writeHostEnv and checkedDir (Linux): write's fixed path, /etc/carlos/host.env, reached only through /etc and /etc/carlos opened O_NOFOLLOW|O_DIRECTORY and checked by fstat (root's, writable by no group or other), the file written 0600 in that directory, synced, renamed and the directory synced; ruled A,J4 under census-ruling-region-ha-2b, as the checked parent runHostEnv's condition asks for. The legacy seal stands.
+# Task 3: carlos system install-channel (internal/host installchannel.go, installchannel_linux.go): a fresh box installs its signed channel release into both protected slots through verifyPointer, hostfloor.CheckImage and stageCandidate, all unchanged, and on every run hands each directory on the agent's cache path (system, system/router and the version's) to the service user through bootstrap's own descriptor-anchored open, refusing a symlinked parent. One new root-read unit, channelInstall.run, whose one read site opens root's apply lock (systemApplier's lockPath). Ruled authority F,J4. stageCandidate, accepted-legacy, is called and not changed. The legacy seal stands.
@coverage . be7c96bb2ff00b5b018b436f976f0d33bbcc8765c17e357baa6cd0290f59f42d
@coverage internal/hostenv 45d9d4b90a5d441020f5ba4573821ca92701246c80b32d6f1823665fb0f65208
internal/hostenv/cli.go runHostEnv authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env: write runs as root at a box's start-up and writes /etc/carlos/host.env, 0600, whole (temporary file and rename), from allowlisted parameters with safe values only, refusing a missing required value; its one read site is seed's --from file, an operator's host.env whose values go only to the parameter store; write's path is fixed and reached only through writeHostEnv file:ReadFile@1500 df4470176b229b311927ff0ac4f268d354c3a2357cac4300ac0992d2ebb90899
internal/hostenv/write_linux.go checkedDir authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env write (root, Linux): opens one directory under one already held, O_NOFOLLOW|O_DIRECTORY, creating it 0755 first only when asked and missing, and refuses it unless fstat shows a directory owned by this process's user (root, as write requires) that no group or other can write file:Fstat@576,file:Openat@168,file:Openat@372 012257ac0866f3385044785d8abae9c31e61134928c394e22a2aeaef39ec9f3c
internal/hostenv/write_linux.go writeHostEnv authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env write (root, Linux): writes /etc/carlos/host.env only through /etc and /etc/carlos held by descriptor and checked by checkedDir; the new file is created 0600, O_EXCL|O_NOFOLLOW, in that directory, synced, renamed over host.env within it, and the directory synced; its read sites open the root and the temporary file file:Open@68,file:Openat@509 1245fc0f819ba73fb45545ffdcf92d5d4941802175e73bac209ab4d4238adff1
+@coverage internal/host 64ee4f561f2a28d584a6f7c82e068e57db4822f6cbb130116284bca583d2d390
+internal/host/installchannel.go channelInstall.run authority F,J4 rev3=census-ruling-region-ha-2b;basis=carlos system install-channel (root, at a fresh box's start-up): its one read site opens root's apply lock; the install goes through verifyPointer, hostfloor.CheckImage and stageCandidate into both protected slots, and refuses a box whose guard is armed or that already runs another release file:OpenFile@196 f0cdb6dd28e83da282c3b0d7bc2012dcd80db181a8cecbf1a2db6bcc41d1d5e3
diff --git a/region_ha_boundary_test.go b/region_ha_boundary_test.go
index 9ef8010a4..344d509c4 100644
--- a/region_ha_boundary_test.go
+++ b/region_ha_boundary_test.go
@@ -34,6 +34,7 @@ var regionHACoreFiles = []string{
"internal/hostenv/cli.go",
"internal/hostenv/write_linux.go",
"internal/hostenv/write_other.go",
+ "internal/host/installchannel.go",
}
// regionHAAdapterFiles are spec 2's AWS adapters. Each is named for AWS so