carlos / platform Public

claim-untagged-pointer fix round 1: a missing identity object gets its own refusal; comments say what is true; the CLI's both-or-neither check is tested

platform-00's requests, as ruled by the controller:

1. When the bucket holds no identity object under the --identity file's
   tag, install-channel refuses with "no release identity is published
   for <tag>, so nothing was installed". "the --identity file is not the
   identity published for <tag>" stays for an object that exists but
   differs. Both cases are tested, and a mutation that merges them fails.
2. openChannelPolicy's comment names both files it reads.
3. The witness's and promoteAsSystemUpdate's comments now say what they
   do: an in-process store.Put (scaffolding) and release.PromoteSystem,
   tied to the console door only through
   TestSystemUpdateWritesAnUntaggedPointerBeforeTheFloor.
4. TestRunSystemInstallChannelTakesPolicyAndIdentityTogether runs the
   command in a child of the test binary: --policy alone and --identity
   alone exit 2 with the pair line, -h lists both flags, and with neither
   flag the root check is still the first refusal.

Census overlay claim-untagged-pointer-fix1 (PROPOSED): envelope only; no
unit digest moved. RHB-C19 and the cli.md paragraph are not applied.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev e990a47e5651f96bf9b1232c10808ce12fb81aaa parent 6892bdd
7 files changed, +124 −24
  • docs/superpowers/notes/census-proposal-claim-untagged-pointer.md +11 −0
  • internal/host/activation_console_rootrunner_linux_test.go +15 −13
  • internal/host/installchannel.go +6 −2
  • internal/host/installchannel_identity_test.go +76 −5
  • internal/host/installchannel_linux.go +5 −4
  • internal/provision/plan14_root_reads_test.go +8 −0
  • internal/provision/testdata/plan14-root-reads-claim-untagged-pointer-fix1.tsv +3 −0
diff --git a/docs/superpowers/notes/census-proposal-claim-untagged-pointer.md b/docs/superpowers/notes/census-proposal-claim-untagged-pointer.md
index 84b619ee1..8ddb6d184 100644
--- a/docs/superpowers/notes/census-proposal-claim-untagged-pointer.md
+++ b/docs/superpowers/notes/census-proposal-claim-untagged-pointer.md
@@ -40,3 +40,14 @@ No 2b row is re-pinned. The two re-pinned claim rows keep their class exactly: `
## Test files
These are tests and are outside the census: the new `installchannel_identity_test.go`, the changes to `installchannel_policy_test.go`, `installchannel_policy_linux_test.go` and `installchannel_policy_rootrunner_linux_test.go`, the rewritten root-runner witness `activation_console_rootrunner_linux_test.go`, and the console's `apisystemupdate_test.go`.
+
+## Fix round 1 (PROPOSED)
+
+Status: PROPOSED. Overlay `internal/provision/testdata/plan14-root-reads-claim-untagged-pointer-fix1.tsv`, loaded after the ruled overlay. Envelope only.
+
+What changed, all on the `--policy` path or in comments:
+
+- `channelInstall.channelIdentity`: when the bucket holds no identity object under the `--identity` file's tag, the refusal is now "no release identity is published for <tag>, so nothing was installed". "the --identity file is not the identity published for <tag>, ..." stays for an object that exists but differs.
+- `openChannelPolicy`'s doc comment now names both files it reads. Its digest, which covers the declaration and not its doc comment, is unchanged.
+
+No root-read unit's digest or sites change: `channelInstall.run` and `openChannelPolicy` stand at census-ruling-claim-untagged-pointer's rows. The `internal/host` envelope is re-pinned to `846d5194…d741a5`. The seal stands, and no 9B8 entry is proposed: nothing new writes to a bucket.
diff --git a/internal/host/activation_console_rootrunner_linux_test.go b/internal/host/activation_console_rootrunner_linux_test.go
index 7d3534114..39a352476 100644
--- a/internal/host/activation_console_rootrunner_linux_test.go
+++ b/internal/host/activation_console_rootrunner_linux_test.go
@@ -102,14 +102,13 @@ type activationPhaseResult struct {
//
// - the box is installed at v0.9.0 (README steps 1 to 4: the binary,
// host.env, `carlos bootstrap`, the edge);
-// - the operator's system promote of v1.0.0 is `carlos system update
-// --version v1.0.0 --spread 0`'s code path before a floor is published:
-// the artifact mirrored create-only and release.PromoteSystem with the
-// release the console's door resolves, {Version, Source "release:<tag>",
-// Artifacts}, so the pointer carries no release tag and no moves
-// protocol, exactly as dev's v0.28.3 pointers do (the console's
-// TestSystemUpdateWritesAnUntaggedPointerBeforeTheFloor pins that
-// shape). The real agent and `carlos system apply` then update the box
+// - the operator's system promote of v1.0.0 is an in-process store.Put of
+// the artifact (scaffolding) and release.PromoteSystem with the untagged
+// release {Version, Source "release:<tag>", Artifacts}, so the pointer
+// carries no release tag and no moves protocol, as dev's v0.28.3
+// pointers do. It is tied to `carlos system update`'s door only through
+// the console's TestSystemUpdateWritesAnUntaggedPointerBeforeTheFloor,
+// which drives the real door with no floor and pins that shape. The real agent and `carlos system apply` then update the box
// to v1.0.0, leaving v0.9.0 in carlos.prev;
// - its AWS identity record is published by `carlos system box
// publish-record` as the agent user (step 5.0);
@@ -303,11 +302,14 @@ func TestRootActivationConsoleRuntimePayload(t *testing.T) {
write("/run/systemd/system/"+u+".d/10-rootrunner-network.conf", []byte("[Service]\nIPAddressDeny=any\nIPAddressAllow=127.0.0.1 169.254.169.254\n"), 0644)
}
ctl("daemon-reload")
- // The operator promotes v1.0.0 on the box's channel with `carlos system
- // update --version v1.0.0 --spread 0`, before any floor is published: the
- // console's door mirrors the artifact create-only (mirrorSystemArtifact)
- // and, in legacy mode (reserveHostUpdate returns before tagging),
- // promotes the release it resolved with nothing added.
+ // The operator's promote of v1.0.0, before any floor is published. This
+ // is not the console door or the carlos system update CLI: it is an
+ // in-process store.Put of the artifact (scaffolding, standing in for the
+ // door's mirror) and release.PromoteSystem with the untagged release
+ // {Version, Source "release:<tag>", Artifacts}. That shape is tied to the
+ // real door only through the console's
+ // TestSystemUpdateWritesAnUntaggedPointerBeforeTheFloor, which drives the
+ // door with no floor and pins exactly this pointer.
artifact := release.SystemArtifact{Key: release.SystemArtifactKey("v1.0.0", "linux-arm64"), SHA256: hostfloor.Digest(activationCarlosImage)}
_, err := store.Put(ctx, artifact.Key, activationCarlosImage, "")
check(err)
diff --git a/internal/host/installchannel.go b/internal/host/installchannel.go
index 2380cce31..63d9aa10c 100644
--- a/internal/host/installchannel.go
+++ b/internal/host/installchannel.go
@@ -487,7 +487,8 @@ func (in channelInstall) checkFloor(ctx context.Context, p *release.SystemPointe
// channelIdentity is --identity's check (untagged-pointer ruling R1), once
// the release key is known: the file verifies as a release identity under
// it, is byte for byte the object published at release.IdentityKey of its
-// own tag, and names what the verified pointer names: the same version and
+// own tag (none published under it is its own refusal), and names what the
+// verified pointer names: the same version and
// the same sha256 for this platform's artifact, and, when the pointer
// carries a release tag, that tag. It returns the identity's bytes and tag.
// Nothing is read through the pointer's tag.
@@ -505,7 +506,10 @@ func (in channelInstall) channelIdentity(ctx context.Context, s release.Store, p
if err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, "", fmt.Errorf("the release identity for %s could not be read, so nothing was installed: %w", tag, err)
}
- if err != nil || !bytes.Equal(published, in.identity) {
+ if err != nil {
+ return nil, "", fmt.Errorf("no release identity is published for %s, so nothing was installed", tag)
+ }
+ if !bytes.Equal(published, in.identity) {
return nil, "", fmt.Errorf("the --identity file is not the identity published for %s, so nothing was installed", tag)
}
router, ok := id.Current.Artifacts[in.a.platform]
diff --git a/internal/host/installchannel_identity_test.go b/internal/host/installchannel_identity_test.go
index c0c5e033e..0387f0d78 100644
--- a/internal/host/installchannel_identity_test.go
+++ b/internal/host/installchannel_identity_test.go
@@ -7,6 +7,7 @@ import (
"encoding/json"
"errors"
"os"
+ "os/exec"
"path/filepath"
"strings"
"testing"
@@ -27,10 +28,12 @@ import (
// TestSystemUpdateWritesAnUntaggedPointerBeforeTheFloor, pins the pointer's
// shape these fixtures promote.
-// promoteAsSystemUpdate moves stable to version as `carlos system update
-// --version <version> --spread 0` does before a floor is published: the
-// artifact mirrored create-only, then PromoteSystem with the release the door
-// resolved and nothing added.
+// promoteAsSystemUpdate moves stable to version with the pointer shape `carlos
+// system update` writes before a floor is published. It does not run the
+// door: it calls f.store.Put for the artifact (scaffolding) and
+// release.PromoteSystem in-process with the untagged release {Version,
+// Source "release:<version>", Artifacts}. The shape is tied to the console's
+// door only through TestSystemUpdateWritesAnUntaggedPointerBeforeTheFloor.
func (f *channelFixture) promoteAsSystemUpdate(t *testing.T, version string, body []byte) {
t.Helper()
ctx := context.Background()
@@ -268,7 +271,14 @@ func TestInstallChannelPolicyIdentityRefusals(t *testing.T) {
t.Fatal(err)
}
return identity
- }, notPublished},
+ }, "no release identity is published for v1.4.0, so nothing was installed"},
+ {"signed for a tag never published", func(t *testing.T, f *channelFixture, identity []byte) []byte {
+ wire, err := release.SignSystemIdentity(release.RootSigner(f.priv), identityRelease("v1.4.9", f.body))
+ if err != nil {
+ t.Fatal(err)
+ }
+ return wire
+ }, "no release identity is published for v1.4.9, so nothing was installed"},
{"another release's identity", func(t *testing.T, f *channelFixture, identity []byte) []byte {
return f.publishIdentity(t, identityRelease("v1.5.0", []byte("#!/bin/sh\necho carlos v1.5.0\n")))
}, "the --identity file names v1.5.0 (v1.5.0), not the release the stable channel names, so nothing was installed"},
@@ -415,3 +425,64 @@ func (f *channelFixture) identityOf(t *testing.T, tag string) []byte {
}
return raw
}
+
+// installChannelCLIEnv makes this test binary run RunSystemInstallChannel
+// with the arguments it holds (one per line), so its exits can be observed.
+const installChannelCLIEnv = "CARLOS_TEST_INSTALL_CHANNEL_ARGS"
+
+// TestInstallChannelCLIChild is the child runInstallChannelCLI starts; run
+// without installChannelCLIEnv it does nothing.
+func TestInstallChannelCLIChild(t *testing.T) {
+ raw, ok := os.LookupEnv(installChannelCLIEnv)
+ if !ok {
+ t.Skip("only as runInstallChannelCLI's child")
+ }
+ var args []string
+ if raw != "" {
+ args = strings.Split(raw, "\n")
+ }
+ RunSystemInstallChannel(args)
+ os.Exit(0)
+}
+
+// runInstallChannelCLI runs `carlos system install-channel args...` in a
+// child of this test binary, not as root, and returns its combined output
+// and exit code.
+func runInstallChannelCLI(t *testing.T, args ...string) (string, int) {
+ t.Helper()
+ cmd := exec.Command(os.Args[0], "-test.run=^TestInstallChannelCLIChild$")
+ cmd.Env = append(os.Environ(), installChannelCLIEnv+"="+strings.Join(args, "\n"), "CARLOS_SYSTEM_CHANNEL=")
+ cmd.Dir = t.TempDir()
+ out, err := cmd.CombinedOutput()
+ code := 0
+ if ee, ok := err.(*exec.ExitError); ok {
+ code = ee.ExitCode()
+ } else if err != nil {
+ t.Fatal(err)
+ }
+ return string(out), code
+}
+
+// The command's both-or-neither check (untagged-pointer R1, platform-00):
+// --policy alone and --identity alone are refused with exit 2 and the pair
+// line, before the root check and before any file is read; -h lists both
+// flags.
+func TestRunSystemInstallChannelTakesPolicyAndIdentityTogether(t *testing.T) {
+ missing := filepath.Join(t.TempDir(), "absent.json")
+ for _, args := range [][]string{{"--policy", missing}, {"--identity", missing}} {
+ out, code := runInstallChannelCLI(t, args...)
+ if code != 2 || strings.TrimSpace(out) != "--policy and --identity go together, so nothing was installed" {
+ t.Fatalf("%v: exit %d, %q", args, code, out)
+ }
+ }
+ out, code := runInstallChannelCLI(t, "-h")
+ if code != 0 || !strings.HasPrefix(out, installChannelUsage) || !strings.Contains(out, " --identity <string> "+installChannelIdentityFlag+"\n") || !strings.Contains(out, " --policy <string> "+installChannelPolicyFlag+"\n") {
+ t.Fatalf("-h: exit %d\n%s", code, out)
+ }
+ if os.Geteuid() != 0 {
+ // With neither flag, unchanged: the root check is the first refusal.
+ if out, code := runInstallChannelCLI(t); code != 1 || strings.TrimSpace(out) != installChannelNotRoot {
+ t.Fatalf("no flags: exit %d, %q", code, out)
+ }
+ }
+}
diff --git a/internal/host/installchannel_linux.go b/internal/host/installchannel_linux.go
index a4d468b6d..4912789df 100644
--- a/internal/host/installchannel_linux.go
+++ b/internal/host/installchannel_linux.go
@@ -38,10 +38,11 @@ func ownDir(path string, uid, gid int) error {
return unix.Fchown(fd, uid, gid)
}
-// openChannelPolicy reads --policy's file: opened with O_NOFOLLOW (a
-// symlink is refused, never followed) and O_NONBLOCK (a FIFO never blocks
-// the open), fstat'd to be a regular file of at most limit bytes, and only
-// then read, from the descriptor, up to limit.
+// openChannelPolicy reads each operator-staged file install-channel --policy
+// takes, the --policy file and the --identity file (readChannelInputs):
+// opened with O_NOFOLLOW (a symlink is refused, never followed) and
+// O_NONBLOCK (a FIFO never blocks the open), fstat'd to be a regular file of
+// at most limit bytes, and only then read, from the descriptor, up to limit.
func openChannelPolicy(path string, limit int64) ([]byte, error) {
fd, err := unix.Open(path, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_CLOEXEC|unix.O_NONBLOCK, 0)
if err != nil {
diff --git a/internal/provision/plan14_root_reads_test.go b/internal/provision/plan14_root_reads_test.go
index 42bca2440..9288448c9 100644
--- a/internal/provision/plan14_root_reads_test.go
+++ b/internal/provision/plan14_root_reads_test.go
@@ -896,6 +896,14 @@ func TestPlan14RootReadCensus(t *testing.T) {
if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-claim-untagged-pointer.tsv"), &inv, true); err != nil {
t.Fatal(err)
}
+ // Census overlay claim-untagged-pointer-fix1 (review fix round 1: a
+ // missing identity object gets its own refusal; openChannelPolicy's
+ // comment names both files), loaded last. Envelope only. PROPOSED
+ // (docs/superpowers/notes/census-proposal-claim-untagged-pointer.md,
+ // "Fix round 1"). The seal stands.
+ if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-claim-untagged-pointer-fix1.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-untagged-pointer-fix1.tsv b/internal/provision/testdata/plan14-root-reads-claim-untagged-pointer-fix1.tsv
new file mode 100644
index 000000000..deba92138
--- /dev/null
+++ b/internal/provision/testdata/plan14-root-reads-claim-untagged-pointer-fix1.tsv
@@ -0,0 +1,3 @@
+# Census overlay claim-untagged-pointer-fix1 (review fix round 1: platform-00's requests, as ruled by the controller), loaded LAST, after claim-untagged-pointer. PROPOSED: docs/superpowers/notes/census-proposal-claim-untagged-pointer.md, section "Fix round 1".
+# Envelope only. channelIdentity gives a missing identity object its own refusal ("no release identity is published for <tag>, so nothing was installed"); openChannelPolicy's doc comment names both files it reads. No root-read unit's digest or sites change: channelInstall.run and openChannelPolicy stand at census-ruling-claim-untagged-pointer's rows, refreshPrev at its ruled row. The seal stands; the 9B8 inventory is unchanged.
+@coverage internal/host 846d51942aff38963e0a87ce178024297208125a5eca39816d2c6d1d50d741a5