carlos / platform Public

claim-prev-rehearsal: install-channel --policy arms an updater-installed box (ruling R1)

carlos system install-channel --policy FILE takes the armed policy
claims run sign-policy wrote as the host floor before one is published.
The policy is verified as guard-arm verifies it; a published floor, or
either half of one, must be its bytes. On a box carlos system update has
moved to the channel release (carlos already its signed artifact, guard
not armed), carlos.prev is refreshed from carlos by the updater's own
notePrev, so guard-arm's two-slot rule passes. Without --policy nothing
changes. Census overlay PROPOSED.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 5d1c3bec6160dd4a95acac232fec591d72a85d4c parent 7c1beb2
5 files changed, +640 −18
  • docs/superpowers/notes/census-proposal-claim-prev-rehearsal.md +33 −0
  • internal/host/installchannel.go +185 −18
  • internal/host/installchannel_policy_test.go +408 −0
  • internal/provision/plan14_root_reads_test.go +7 −0
  • internal/provision/testdata/plan14-root-reads-claim-prev-rehearsal.tsv +7 −0
diff --git a/docs/superpowers/notes/census-proposal-claim-prev-rehearsal.md b/docs/superpowers/notes/census-proposal-claim-prev-rehearsal.md
new file mode 100644
index 000000000..906a619e0
--- /dev/null
+++ b/docs/superpowers/notes/census-proposal-claim-prev-rehearsal.md
@@ -0,0 +1,33 @@
+# Census proposal: claim-prev-rehearsal
+
+Status: PROPOSED. Not ruled. Overlay `internal/provision/testdata/plan14-root-reads-claim-prev-rehearsal.tsv`, loaded last (after `claim-step6-artifacts-fix2`), every row `rev3=PROPOSED-claim-prev-rehearsal`.
+
+## What changed
+
+`carlos system install-channel` takes `--policy FILE` (controller ruling R1, with platform-00's binding additions). FILE is exactly what `carlos system claims run` sign-policy writes. With it:
+
+- the policy's floor, verified as guard-arm verifies it (the root-signed moves-protocol authorization, for this deployment's bucket, under `CARLOS_RELEASE_PUBKEY`), stands in for the published floor while none is published;
+- a published floor, or either half of one, must be the policy's bytes, or the run is refused;
+- one updated box is admitted: `applied.json` names the channel release, `carlos` hashes to its signed artifact, and the guard is not armed. Its `carlos.prev` is refreshed from `carlos` by the updater's own `notePrev`, and the identity is kept beside the slots.
+
+Without `--policy` nothing changes (ruling 6).
+
+## Units
+
+| Unit | Change | Class | Executing identity |
+|---|---|---|---|
+| `internal/host/installchannel.go` `readChannelPolicy` | new; read site `file:ReadFile` | authority F,J4 | uid 0 at the box's attended root shell, running `carlos system install-channel --policy`. Reads the operator-staged policy file once, before the apply lock or any store read. |
+| `internal/host/installchannel.go` `channelInstall.refreshPrev` | new; read site `file:Lstat` | authority F,J4 | the same uid 0 root shell, under root's apply lock. Lstats `carlos.prev` to refuse a symlink or a non-regular file. |
+| `internal/host/installchannel.go` `channelInstall.run` | unchanged digest | F,J4 (region-ha-2b) | unchanged |
+| `internal/host/installchannel_linux.go` `slotState`, `var openat2`, `removeBeneath` | unchanged digests | F,J4 (region-ha-2b) | unchanged; 2b's rows stand and are not restated |
+| `channelInstall.install`, `channelInstall.checkFloor`, `policyFloorPublished`, `channelPolicy`, `RunSystemInstallChannel` | changed or new, no read site of their own | envelope | the same root shell |
+
+The `internal/host` envelope is re-pinned. The seal stands: no accepted-legacy row. No box agent, update, recovery or edge path reaches either new unit.
+
+## 9B8
+
+No new bucket writer. install-channel writes only local files (`carlos.prev`, its version sidecar through `notePrev`, and `channel-identity.json`). No 9B8 entry is proposed.
+
+## Test files
+
+The rewritten root-runner witness (`activation_console_rootrunner_linux_test.go`) and the new `installchannel_policy_test.go` are tests and are outside the census.
diff --git a/internal/host/installchannel.go b/internal/host/installchannel.go
index 6e7dbf2e1..5d661b1f6 100644
--- a/internal/host/installchannel.go
+++ b/internal/host/installchannel.go
@@ -1,8 +1,10 @@
package host
import (
+ "bytes"
"context"
"crypto/ed25519"
+ "encoding/json"
"errors"
"flag"
"fmt"
@@ -37,10 +39,25 @@ import (
// 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.
+//
+// --policy FILE (claim trial, prev-rehearsal ruling R1) is for arming a box
+// before the host floor is published. FILE is exactly what `carlos system
+// claims run` sign-policy writes. Its floor is verified as guard-arm
+// verifies it (the root-signed moves-protocol authorization, for this
+// deployment's bucket, under CARLOS_RELEASE_PUBKEY) and stands in for the
+// published floor while there is none; a published floor, or either half
+// of one, must be the policy's own bytes. Every other refusal stands. It
+// also admits the one updated box A2-2 would otherwise leave unarmable:
+// one whose carlos already is the channel release's signed artifact. Its
+// carlos.prev, the release before, is replaced by that artifact
+// (refreshPrev), because guard-arm requires both slots to be the
+// identity's release. Without --policy nothing here changes.
-const installChannelUsage = "usage: carlos system install-channel\n\n" +
+const installChannelUsage = "usage: carlos system install-channel [--policy FILE]\n\n" +
"Install the release this box's channel names, as root, on a box that has none yet.\n"
+const installChannelPolicyFlag = "the armed policy carlos system claims run sign-policy wrote, used as the host floor until one is published"
+
const installChannelNotRoot = "carlos system install-channel runs as root: it installs the platform binary"
// channelIdentityPath is where the installed release's signed identity is
@@ -57,6 +74,8 @@ type channelInstall struct {
bucket string
fetchTo func(ctx context.Context, s release.Store, key, dst, sha string) error
own func(path string) error
+ // policy is --policy's file, byte for byte; nil without the flag.
+ policy []byte
}
// slotKind is what one protected slot holds (slotState).
@@ -72,11 +91,14 @@ const (
type channelInstalled struct {
Version, Tag, Channel string
Already bool
+ // Replaced: --policy replaced an updated box's carlos.prev (refreshPrev).
+ Replaced bool
}
// RunSystemInstallChannel is `carlos system install-channel`.
func RunSystemInstallChannel(args []string) {
fs := flag.NewFlagSet("system install-channel", flag.ExitOnError)
+ policyPath := fs.String("policy", "", installChannelPolicyFlag)
fs.Usage = cliflag.Usage(fs, installChannelUsage)
_ = fs.Parse(args)
if fs.NArg() != 0 {
@@ -101,6 +123,14 @@ func RunSystemInstallChannel(args []string) {
},
own: ownByService,
}
+ if *policyPath != "" {
+ raw, err := readChannelPolicy(*policyPath)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ in.policy = raw
+ }
got, err := in.run(context.Background())
if err != nil {
fmt.Fprintln(os.Stderr, err)
@@ -109,7 +139,40 @@ func RunSystemInstallChannel(args []string) {
fmt.Println(installedLine(got))
}
+// readChannelPolicy reads --policy's file and refuses, before anything else
+// is read, a file that is not an armed policy at all. Its signature is
+// checked once the release key is known (channelPolicy).
+func readChannelPolicy(path string) ([]byte, error) {
+ raw, err := os.ReadFile(path)
+ if err != nil {
+ return nil, fmt.Errorf("the policy %s could not be read, so nothing was installed: %w", path, err)
+ }
+ var p hostfloor.Policy
+ if json.Unmarshal(raw, &p) != nil || p.Mode != "armed" {
+ return nil, fmt.Errorf("%s is not an armed policy signed for this deployment, so nothing was installed", path)
+ }
+ return raw, nil
+}
+
+// channelPolicy is guard-arm's check of --policy (guardArmInputs, first
+// half): an armed policy whose floor carries the root-signed
+// moves-protocol authorization for this deployment's bucket.
+func channelPolicy(raw []byte, pub ed25519.PublicKey, bucket string) (hostfloor.Policy, hostfloor.Verified, error) {
+ var p hostfloor.Policy
+ if json.Unmarshal(raw, &p) != nil || p.Mode != "armed" {
+ return hostfloor.Policy{}, hostfloor.Verified{}, hostfloor.ErrFloor
+ }
+ f, err := p.Verify(pub, bucket)
+ if err != nil {
+ return hostfloor.Policy{}, hostfloor.Verified{}, err
+ }
+ return p, f, nil
+}
+
func installedLine(got channelInstalled) string {
+ if got.Replaced {
+ return fmt.Sprintf("replaced the rollback copy carlos.prev with %s (%s) from the %s channel, the release carlos already runs. the release carlos.prev held before is no longer on this box", got.Version, got.Tag, got.Channel)
+ }
if got.Already {
return fmt.Sprintf("%s (%s) from the %s channel is already installed", got.Version, got.Tag, got.Channel)
}
@@ -147,6 +210,15 @@ func (in channelInstall) install(ctx context.Context) (channelInstalled, error)
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}
+ var policy *hostfloor.Policy
+ var policyFloor hostfloor.Verified
+ if in.policy != nil {
+ pol, f, err := channelPolicy(in.policy, pub, in.bucket)
+ if err != nil {
+ return got, errors.New("the --policy file is not an armed policy signed for this deployment, so nothing was installed")
+ }
+ policy, policyFloor = &pol, f
+ }
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)
@@ -165,7 +237,7 @@ func (in channelInstall) install(ctx context.Context) (channelInstalled, error)
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)
+ wire, err := in.checkFloor(ctx, p, art, pub, policy, policyFloor)
if err != nil {
return got, err
}
@@ -179,6 +251,9 @@ func (in channelInstall) install(ctx context.Context) (channelInstalled, error)
got.Already = true
return got, nil
}
+ if policy != nil {
+ return in.refreshPrev(ctx, p, art, wire, got)
+ }
return got, fmt.Errorf("this box already runs %s, so carlos system update installs its releases", applied.Version)
}
// With no applied.json, a slot that holds anything but the signed
@@ -244,23 +319,33 @@ func (in channelInstall) install(ctx context.Context) (channelInstalled, error)
// 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) {
+// that failed says nothing about where the floor is. With --policy
+// (policy non-nil) the policy's verified floor is the floor instead
+// (policyFloorPublished).
+func (in channelInstall) checkFloor(ctx context.Context, p *release.SystemPointer, art release.SystemArtifact, pub ed25519.PublicKey, policy *hostfloor.Policy, policyFloor hostfloor.Verified) ([]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)
+ var floor hostfloor.Verified
+ if policy != nil {
+ if err := policyFloorPublished(ctx, s, *policy); err != nil {
+ return nil, err
+ }
+ floor = policyFloor
+ } else {
+ 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 {
@@ -276,6 +361,88 @@ func (in channelInstall) checkFloor(ctx context.Context, p *release.SystemPointe
return wire, nil
}
+// policyFloorPublished checks --policy against what the bucket holds: no
+// floor published, or a published pair (or the half of one that a torn
+// publication leaves; hostfloor.Publish writes the authorization first)
+// that is the policy's floor and authorization byte for byte. Anything
+// else, a read that fails included, refuses.
+func policyFloorPublished(ctx context.Context, s release.Store, policy hostfloor.Policy) error {
+ for _, half := range []struct {
+ key string
+ want []byte
+ }{{hostfloor.AuthKey, policy.Authorization}, {hostfloor.Key, policy.Floor}} {
+ got, _, err := s.Get(ctx, half.key)
+ if errors.Is(err, os.ErrNotExist) {
+ continue
+ }
+ if err != nil {
+ return fmt.Errorf("the host floor could not be read, so nothing was installed: %w", err)
+ }
+ if !bytes.Equal(got, half.want) {
+ return errors.New("the published host floor is not the floor in the --policy file, so nothing was installed")
+ }
+ }
+ return nil
+}
+
+// refreshPrev is --policy's one admission of an updated box (applied.json
+// names the channel release, and the slots and the identity are not yet
+// install-channel's): carlos must already hold the channel release's
+// signed artifact, hashed from a descriptor opened with O_NOFOLLOW
+// (slotState). Its carlos.prev, the release the update replaced, is then
+// refreshed from carlos the way an update refreshes it (notePrev: the
+// sidecar and verdicts of the old carlos.prev removed first, the copy, the
+// version sidecar, the floor's judgement), and the identity is kept beside
+// them. Nothing is fetched, and applied.json and seen.json are the
+// update's and stay as they are. The caller has already refused an armed
+// guard, another release and a release below the policy's floor.
+//
+// Why A2-2 allows it: A2-2 puts the identity's artifact in both slots so
+// guard-arm's two-slot rule can pass, and on a fresh box carlos.prev is
+// then a copy of carlos. Here carlos already is that artifact, so this
+// reaches A2-2's end state exactly. What is lost is the rollback to the
+// release before, which is below the floor whenever the floor is the
+// release being armed, and which the armed guard would refuse to run.
+func (in channelInstall) refreshPrev(ctx context.Context, p *release.SystemPointer, art release.SystemArtifact, wire []byte, got channelInstalled) (channelInstalled, error) {
+ a := in.a
+ current, prev := systemBinaryPath(a.optDir), systemPrevPath(a.optDir)
+ state, err := slotState(current, art.SHA256)
+ if err != nil {
+ return got, fmt.Errorf("this box's install record could not be read, so nothing was installed: %w", err)
+ }
+ if state != slotHolds {
+ return got, fmt.Errorf("%s holds a build this command did not install, so nothing was installed", current)
+ }
+ held, err := slotState(prev, art.SHA256)
+ if err != nil {
+ return got, fmt.Errorf("this box's install record could not be read, so nothing was installed: %w", err)
+ }
+ if held == slotForeign {
+ // The release before is replaced; a symlink or anything but a
+ // regular file is not what an update leaves, and is refused.
+ if fi, err := os.Lstat(prev); err != nil {
+ return got, fmt.Errorf("this box's install record could not be read, so nothing was installed: %w", err)
+ } else if !fi.Mode().IsRegular() {
+ return got, fmt.Errorf("%s holds a build this command did not install, so nothing was installed", prev)
+ }
+ }
+ // Always, even over a carlos.prev a cut-off run already copied, so the
+ // version sidecar is this release's.
+ if err := a.notePrev(ctx, true, p.Current.Version); err != nil {
+ return got, err
+ }
+ for _, slot := range []string{prev, current} {
+ if state, err := slotState(slot, art.SHA256); err != nil || state != slotHolds {
+ 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
+ }
+ got.Replaced = true
+ return got, 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
diff --git a/internal/host/installchannel_policy_test.go b/internal/host/installchannel_policy_test.go
new file mode 100644
index 000000000..6929f7f58
--- /dev/null
+++ b/internal/host/installchannel_policy_test.go
@@ -0,0 +1,408 @@
+package host
+
+import (
+ "bytes"
+ "context"
+ "crypto/ed25519"
+ "encoding/json"
+ "errors"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/hostfloor"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// install-channel --policy (claim trial prev-rehearsal, ruling R1). The box
+// these tests start from is made by the platform's own update path, never
+// by hand: v1.3.0 on disk, the channel promoted to v1.4.0, the agent's
+// request and cache written as systemLeg writes them, then the real apply
+// (systemApplier.Run) moving v1.3.0 to carlos.prev. That is the trial's
+// leo-climbing: applied.json names the channel release, carlos is its
+// artifact, carlos.prev the release before, and no channel identity.
+
+// updatedChannelBox returns that box. old and body are the two releases'
+// bytes.
+func updatedChannelBox(t *testing.T) (f *channelFixture, old []byte) {
+ t.Helper()
+ f = &channelFixture{applyFixture: newApplyFixture(t, "stable", "v1.3.0")}
+ old, err := os.ReadFile(systemBinaryPath(f.a.optDir))
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.body = []byte("#!/bin/sh\necho carlos v1.4.0\n")
+ f.publish(t, "v1.4.0", f.body)
+ ctx := context.Background()
+ p, _, err := release.GetSystemPointer(ctx, f.store, f.pub, release.SystemComponentRouter, "stable")
+ if err != nil {
+ t.Fatal(err)
+ }
+ // The agent's half of the update (systemLeg.fetch and writeRequest).
+ 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)
+ }}
+ pending := &systemPending{version: "v1.4.0", promotedAt: p.PromotedAt, artifact: p.Current.Artifacts["linux-arm64"]}
+ if err := leg.fetch(ctx, pending); err != nil {
+ t.Fatal(err)
+ }
+ if err := leg.writeRequest(pending, f.now); err != nil {
+ t.Fatal(err)
+ }
+ staged := filepath.Join(f.a.stagingDir(), "carlos")
+ f.answer["plan:"+staged] = f.answer["plan:"+systemBinaryPath(f.a.optDir)]
+ f.answer["units:"+staged] = f.answer["units:"+systemBinaryPath(f.a.optDir)]
+ prev := systemPrevPath(f.a.optDir)
+ f.answer["plan:"+prev] = f.answer["plan:"+systemBinaryPath(f.a.optDir)]
+ f.answer["units:"+prev] = f.answer["units:"+systemBinaryPath(f.a.optDir)]
+ // Every binary answers `version` from its own bytes.
+ delete(f.answer, "version:"+systemBinaryPath(f.a.optDir))
+ if err := f.a.Run(ctx, false); err != nil {
+ t.Fatal(err)
+ }
+ if r := f.result(); r == nil || r.Outcome != systemOutcomeApplied {
+ t.Fatalf("the update did not apply: %+v", r)
+ }
+ if cur, prev := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(prev, old) {
+ t.Fatalf("the update left %q and %q", cur, prev)
+ }
+ if _, err := os.Stat(channelIdentityPath(f.a.optDir)); !os.IsNotExist(err) {
+ t.Fatalf("an update wrote a channel identity: %v", 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, old
+}
+
+// signedPolicy is what claims run sign-policy writes for floor.
+func signedPolicy(t *testing.T, key ed25519.PrivateKey, bucket string, floor hostfloor.Floor) []byte {
+ t.Helper()
+ p, err := hostfloor.ArmedPolicy(release.RootSigner(key), bucket, floor)
+ if err != nil {
+ t.Fatal(err)
+ }
+ raw, err := json.Marshal(p)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return raw
+}
+
+// boxFiles is every file install-channel or the update keeps in the root
+// directory and the two slots, by name, so a refusal can be shown to have
+// changed nothing.
+func boxFiles(t *testing.T, f *channelFixture) map[string]string {
+ t.Helper()
+ got := map[string]string{}
+ for _, dir := range []string{f.a.optDir, systemRootDir(f.a.optDir)} {
+ ents, err := os.ReadDir(dir)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, e := range ents {
+ path := filepath.Join(dir, e.Name())
+ if fi, err := os.Lstat(path); err == nil && fi.Mode().IsRegular() {
+ b, _ := os.ReadFile(path)
+ got[path] = string(b)
+ } else if err == nil {
+ got[path] = fi.Mode().String()
+ }
+ }
+ }
+ return got
+}
+
+func TestInstallChannelPolicyArmsAnUpdatedBoxAtTheChannelHead(t *testing.T) {
+ f, old := updatedChannelBox(t)
+ ctx := context.Background()
+ // Without --policy the box is refused exactly as before, and not
+ // changed: with no floor published, for that (the trial's loop), and
+ // with one, as an updated box (the sibling
+ // TestInstallChannelRefusesAnUpdatedBoxAtTheChannelHead).
+ before := boxFiles(t, f)
+ if _, err := f.in.run(ctx); err == nil || err.Error() != "no host floor is published, so this box could not arm its guard and nothing was installed" {
+ t.Fatalf("without --policy: %v", err)
+ }
+ if after := boxFiles(t, f); !equalFiles(before, after) {
+ t.Fatalf("a refused run changed the box: %v", diffFiles(before, after))
+ }
+ applied, _ := os.ReadFile(f.a.file("applied.json"))
+ seen, _ := os.ReadFile(f.a.file("seen.json"))
+ f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
+ got, err := f.in.run(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !got.Replaced || got.Already || got.Version != "v1.4.0" || got.Tag != "v1.4.0" {
+ t.Fatalf("installed %+v", got)
+ }
+ if line := installedLine(got); line != "replaced the rollback copy carlos.prev with v1.4.0 (v1.4.0) from the stable channel, the release carlos already runs. the release carlos.prev held before is no longer on this box" {
+ t.Fatalf("line %q", line)
+ }
+ cur, prev := f.slots(t)
+ if !bytes.Equal(cur, f.body) || !bytes.Equal(prev, f.body) || bytes.Equal(prev, old) {
+ t.Fatalf("slots hold %q and %q", cur, prev)
+ }
+ // Nothing was fetched: carlos already is the artifact.
+ if f.fetches != 0 {
+ t.Fatalf("%d fetches", f.fetches)
+ }
+ // The update's records stand; the version sidecar names the release
+ // carlos.prev now holds.
+ if a, _ := os.ReadFile(f.a.file("applied.json")); !bytes.Equal(a, applied) {
+ t.Fatalf("applied.json changed: %s", a)
+ }
+ if s, _ := os.ReadFile(f.a.file("seen.json")); !bytes.Equal(s, seen) {
+ t.Fatalf("seen.json changed: %s", s)
+ }
+ if v, ok := readLine(prevVersionPath(f.a.optDir)); !ok || v != "v1.4.0" {
+ t.Fatalf("carlos.prev's version sidecar is %q %v", v, ok)
+ }
+ // The identity kept beside the slots is the published one, and
+ // guard-arm's two checks pass: its inputs, and both slots.
+ key, _ := release.IdentityKey("v1.4.0")
+ published, _, err := f.store.Get(ctx, key)
+ if err != nil {
+ t.Fatal(err)
+ }
+ held, err := os.ReadFile(channelIdentityPath(f.a.optDir))
+ if err != nil || !bytes.Equal(held, published) {
+ t.Fatalf("identity %v", err)
+ }
+ _, floor, err := guardArmInputs(f.in.policy, held, f.pub, "bucket", "v1.4.0", "linux-arm64", hostfloor.Digest(f.body))
+ if err != nil {
+ t.Fatalf("guard-arm's inputs: %v", err)
+ }
+ for _, slot := range []string{systemBinaryPath(f.a.optDir), systemPrevPath(f.a.optDir)} {
+ b, _ := os.ReadFile(slot)
+ if err := hostfloor.CheckImage(f.pub, floor, held, "v1.4.0", "linux-arm64", hostfloor.Digest(b)); err != nil {
+ t.Fatalf("guard-arm would refuse %s: %v", slot, err)
+ }
+ }
+ // Run again: nothing to do.
+ again, err := f.in.run(ctx)
+ if err != nil || !again.Already || again.Replaced || f.fetches != 0 {
+ t.Fatalf("second run %+v %v", again, err)
+ }
+ // The floor, once published from the same plan, is the policy's own
+ // bytes, so the run still passes; the same with half of it published,
+ // as a publication cut off after its first write leaves it.
+ var policy hostfloor.Policy
+ if err := json.Unmarshal(f.in.policy, &policy); err != nil {
+ t.Fatal(err)
+ }
+ if err := release.CreateExact(ctx, f.store, hostfloor.AuthKey, policy.Authorization); err != nil {
+ t.Fatal(err)
+ }
+ if got, err := f.in.run(ctx); err != nil || !got.Already {
+ t.Fatalf("half published: %+v %v", got, err)
+ }
+ if err := hostfloor.Publish(ctx, f.store, release.RootSigner(f.priv), "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}); err != nil {
+ t.Fatal(err)
+ }
+ if got, err := f.in.run(ctx); err != nil || !got.Already {
+ t.Fatalf("published: %+v %v", got, err)
+ }
+}
+
+// Each refusal of --policy on the updated box, and on a fresh one, leaves
+// the box as it was and fetches nothing.
+func TestInstallChannelPolicyRefusals(t *testing.T) {
+ _, otherKey, _ := ed25519.GenerateKey(nil)
+ for _, c := range []struct {
+ name string
+ setup func(t *testing.T, f *channelFixture)
+ want string
+ }{
+ {"signed by another key", func(t *testing.T, f *channelFixture) {
+ f.in.policy = signedPolicy(t, otherKey, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
+ }, "the --policy file is not an armed policy signed for this deployment, so nothing was installed"},
+ {"for another bucket", func(t *testing.T, f *channelFixture) {
+ f.in.policy = signedPolicy(t, f.priv, "another-bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
+ }, "the --policy file is not an armed policy signed for this deployment, so nothing was installed"},
+ {"re-indented", func(t *testing.T, f *channelFixture) {
+ var b bytes.Buffer
+ if err := json.Indent(&b, signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}), "", " "); err != nil {
+ t.Fatal(err)
+ }
+ f.in.policy = b.Bytes()
+ }, "the --policy file is not an armed policy signed for this deployment, so nothing was installed"},
+ {"unpublished mode", func(t *testing.T, f *channelFixture) {
+ f.in.policy = []byte(`{"schema":1,"deployment_bucket":"bucket","mode":"unpublished"}`)
+ }, "the --policy file is not an armed policy signed for this deployment, so nothing was installed"},
+ {"an armed guard", func(t *testing.T, f *channelFixture) {
+ f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
+ 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"), f.in.policy, 0o644); err != nil {
+ t.Fatal(err)
+ }
+ }, "this box's host lifecycle guard is armed, so carlos system update installs its releases"},
+ {"another release", func(t *testing.T, f *channelFixture) {
+ f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
+ f.publish(t, "v1.5.0", []byte("#!/bin/sh\necho carlos v1.5.0\n"))
+ }, "this box already runs v1.4.0, so carlos system update installs its releases"},
+ {"below the floor", func(t *testing.T, f *channelFixture) {
+ f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v2.0.0"})
+ }, "v1.4.0 (v1.4.0) is below the host floor v2.0.0 or is not the release its identity names, so nothing was installed"},
+ {"another floor published", func(t *testing.T, f *channelFixture) {
+ f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
+ if err := hostfloor.Publish(context.Background(), f.store, release.RootSigner(f.priv), "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.3.0"}); err != nil {
+ t.Fatal(err)
+ }
+ }, "the published host floor is not the floor in the --policy file, so nothing was installed"},
+ {"another floor half published", func(t *testing.T, f *channelFixture) {
+ f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
+ _, auth, err := hostfloor.Authorize(release.RootSigner(f.priv), "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.3.0"})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := release.CreateExact(context.Background(), f.store, hostfloor.AuthKey, auth); err != nil {
+ t.Fatal(err)
+ }
+ }, "the published host floor is not the floor in the --policy file, so nothing was installed"},
+ {"an unreadable floor", func(t *testing.T, f *channelFixture) {
+ f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
+ open := f.a.openStore
+ f.a.openStore = func(ctx context.Context) (release.Store, ed25519.PublicKey, error) {
+ s, k, err := open(ctx)
+ return floorUnreadable{s}, k, err
+ }
+ }, "the host floor could not be read, so nothing was installed: door restarting"},
+ {"a foreign carlos", func(t *testing.T, f *channelFixture) {
+ f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
+ if err := os.WriteFile(systemBinaryPath(f.a.optDir), []byte("#!/bin/sh\necho carlos v1.4.0 rebuilt\n"), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ }, "%opt%/carlos holds a build this command did not install, so nothing was installed"},
+ {"a symlinked carlos.prev", func(t *testing.T, f *channelFixture) {
+ f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
+ elsewhere := filepath.Join(t.TempDir(), "carlos.prev")
+ if err := os.Rename(systemPrevPath(f.a.optDir), elsewhere); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Symlink(elsewhere, systemPrevPath(f.a.optDir)); err != nil {
+ t.Fatal(err)
+ }
+ }, "%opt%/carlos.prev holds a build this command did not install, so nothing was installed"},
+ {"an unreadable install record", func(t *testing.T, f *channelFixture) {
+ f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
+ if err := os.WriteFile(f.a.file("applied.json"), []byte("{"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ }, "this box's install record could not be read, so nothing was installed: unexpected end of JSON input"},
+ } {
+ t.Run(c.name, func(t *testing.T) {
+ f, _ := updatedChannelBox(t)
+ c.setup(t, f)
+ before := boxFiles(t, f)
+ _, err := f.in.run(context.Background())
+ want := strings.ReplaceAll(c.want, "%opt%", f.a.optDir)
+ if err == nil || err.Error() != want {
+ t.Fatalf("err %v, want %q", err, want)
+ }
+ if after := boxFiles(t, f); !equalFiles(before, after) {
+ t.Fatalf("a refused run changed the box: %v", diffFiles(before, after))
+ }
+ if f.fetches != 0 {
+ t.Fatalf("%d fetches", f.fetches)
+ }
+ if _, err := os.Stat(channelIdentityPath(f.a.optDir)); !os.IsNotExist(err) {
+ t.Fatalf("a refused run kept an identity: %v", err)
+ }
+ })
+ }
+}
+
+// On a fresh box --policy stands in for the floor that is not published
+// yet, and the install is the one without it: both slots, .prev first.
+func TestInstallChannelPolicyInstallsAFreshBoxBeforeTheFloor(t *testing.T) {
+ f := newChannelFixture(t, "")
+ ctx := context.Background()
+ if _, err := f.in.run(ctx); err == nil || err.Error() != "no host floor is published, so this box could not arm its guard and nothing was installed" {
+ t.Fatalf("without --policy: %v", err)
+ }
+ f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
+ got, err := f.in.run(ctx)
+ if err != nil || got.Already || got.Replaced {
+ t.Fatalf("%+v %v", got, err)
+ }
+ if line := installedLine(got); line != "installed v1.4.0 (v1.4.0) from the stable channel as carlos and carlos.prev" {
+ t.Fatalf("line %q", line)
+ }
+ if cur, prev := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(prev, f.body) || f.fetches != 1 {
+ t.Fatalf("slots %q %q after %d fetches", cur, prev, f.fetches)
+ }
+ if got, err := f.in.run(ctx); err != nil || !got.Already {
+ t.Fatalf("again %+v %v", got, err)
+ }
+ // A hand-rolled slot is still refused with --policy.
+ g := newChannelFixture(t, "")
+ g.in.policy = signedPolicy(t, g.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
+ if err := os.WriteFile(systemPrevPath(g.a.optDir), []byte("#!/bin/sh\necho hand rolled\n"), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := g.in.run(ctx); err == nil || err.Error() != systemPrevPath(g.a.optDir)+" holds a build this command did not install, so nothing was installed" || g.fetches != 0 {
+ t.Fatalf("hand-rolled: %v", err)
+ }
+}
+
+// --policy's file is refused before anything else is read when it cannot
+// be read or is not an armed policy at all.
+func TestInstallChannelPolicyFileIsReadUpFront(t *testing.T) {
+ dir := t.TempDir()
+ missing := filepath.Join(dir, "policy.json")
+ if _, err := readChannelPolicy(missing); err == nil || !strings.HasPrefix(err.Error(), "the policy "+missing+" could not be read, so nothing was installed: ") {
+ t.Fatalf("missing: %v", err)
+ }
+ for _, body := range []string{"", "{", `{"mode":"unpublished"}`, `{"mode":"enforced"}`} {
+ if err := os.WriteFile(missing, []byte(body), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := readChannelPolicy(missing); err == nil || err.Error() != missing+" is not an armed policy signed for this deployment, so nothing was installed" {
+ t.Fatalf("%q: %v", body, err)
+ }
+ }
+ _, key, _ := ed25519.GenerateKey(nil)
+ raw := signedPolicy(t, key, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
+ if err := os.WriteFile(missing, raw, 0o600); err != nil {
+ t.Fatal(err)
+ }
+ if got, err := readChannelPolicy(missing); err != nil || !bytes.Equal(got, raw) {
+ t.Fatalf("a signed policy: %v", err)
+ }
+}
+
+// floorUnreadable fails every read of the host floor's two objects.
+type floorUnreadable struct{ release.Store }
+
+func (s floorUnreadable) Get(ctx context.Context, key string) ([]byte, string, error) {
+ if key == hostfloor.Key || key == hostfloor.AuthKey {
+ return nil, "", errors.New("door restarting")
+ }
+ return s.Store.Get(ctx, key)
+}
+
+func equalFiles(a, b map[string]string) bool { return len(diffFiles(a, b)) == 0 }
+
+func diffFiles(a, b map[string]string) []string {
+ var d []string
+ for k, v := range a {
+ if w, ok := b[k]; !ok || w != v {
+ d = append(d, k)
+ }
+ }
+ for k := range b {
+ if _, ok := a[k]; !ok {
+ d = append(d, k)
+ }
+ }
+ return d
+}
diff --git a/internal/provision/plan14_root_reads_test.go b/internal/provision/plan14_root_reads_test.go
index 889ac9320..fbdbb6273 100644
--- a/internal/provision/plan14_root_reads_test.go
+++ b/internal/provision/plan14_root_reads_test.go
@@ -858,6 +858,13 @@ func TestPlan14RootReadCensus(t *testing.T) {
if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-claim-step6-artifacts-fix2.tsv"), &inv, true); err != nil {
t.Fatal(err)
}
+ // Census overlay claim-prev-rehearsal (install-channel --policy: arm an
+ // updater-installed box, controller ruling R1), loaded last. PROPOSED
+ // (docs/superpowers/notes/census-proposal-claim-prev-rehearsal.md). It
+ // adds no accepted-legacy row, so the seal stands.
+ if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-claim-prev-rehearsal.tsv"), &inv, true); err != nil {
+ t.Fatal(err)
+ }
errs := append(compareRootReads(got, inv), checkRootReadLegacy(inv, seal)...)
errs = append(errs, checkRootReadDeclarations(got, inv)...)
if len(errs) > 0 {
diff --git a/internal/provision/testdata/plan14-root-reads-claim-prev-rehearsal.tsv b/internal/provision/testdata/plan14-root-reads-claim-prev-rehearsal.tsv
new file mode 100644
index 000000000..cf5c2ef0d
--- /dev/null
+++ b/internal/provision/testdata/plan14-root-reads-claim-prev-rehearsal.tsv
@@ -0,0 +1,7 @@
+# Census overlay claim-prev-rehearsal (claim trial: arm an updater-installed box; controller ruling R1, plus platform-00's binding additions), loaded LAST, after claim-step6-artifacts-fix2. PROPOSED, not ruled: docs/superpowers/notes/census-proposal-claim-prev-rehearsal.md.
+# carlos system install-channel --policy FILE (internal/host installchannel.go). Without --policy nothing changes: channelInstall.run keeps its digest and class (F,J4, census-ruling-region-ha-2b), and slotState, var openat2 and removeBeneath are unchanged and stay 2b's rows. install and checkFloor have no read site of their own (envelope only): with --policy the signed armed policy's verified floor stands in for the published one, and a published floor, or either half of one, must be the policy's bytes.
+# Two new root-read units, both reached only through install-channel --policy, run as uid 0 at the box's root shell: readChannelPolicy reads the operator-staged policy file and refuses it before anything else unless it is an armed policy (its signature is verified once the release key is known); channelInstall.refreshPrev, the one admission of an updated box at the channel head, Lstats carlos.prev to refuse a symlink or a non-regular file before the updater's own notePrev refreshes it from carlos.
+# The seal stands: no accepted-legacy row. No box agent, update, recovery or edge path reaches either new unit. The 9B8 inventory is unchanged: install-channel writes only local files.
+@coverage internal/host 422bed5ca44ec32c8eab923ad6b96c7a39c50829c4a3346f004fb2383b3dad9e
+internal/host/installchannel.go readChannelPolicy authority F,J4 rev3=PROPOSED-claim-prev-rehearsal;identity=uid 0 at the box's attended root shell (carlos system install-channel --policy);basis=reads the operator-staged --policy file once, before the apply lock or any store read; refuses an unreadable file or one that is not an armed policy; the bytes are used only after channelPolicy verifies the root-signed moves-protocol authorization for this deployment's bucket under CARLOS_RELEASE_PUBKEY, and a published floor must equal them byte for byte file:ReadFile@67 631724a2f53e641e7b5d4d42c927e8687472000f2ad506fa9ee64f0bb146e77f
+internal/host/installchannel.go channelInstall.refreshPrev authority F,J4 rev3=PROPOSED-claim-prev-rehearsal;identity=uid 0 at the box's attended root shell (carlos system install-channel --policy);basis=under the apply lock, after the armed-guard, other-release and floor refusals: carlos must hash, through slotState's O_NOFOLLOW descriptor, to the channel release's signed artifact sha256; its one read site Lstats carlos.prev to refuse a symlink or anything but a regular file; carlos.prev is then refreshed from carlos by the updater's notePrev and both slots are re-hashed before the identity is kept; nothing is fetched or executed and applied.json and seen.json are not written file:Lstat@907 d0848383f68009331536d067c1cade578186514edb351645a7a3ad2e250a7bf5