carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

Seeded Instances, Plan 3: The Seeds Floor Implementation Plan

> For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Make sure no box can read a seeded record as an ordinary new instance and mint it empty: a seeds protocol floor that boxes arm and never recover below, and a console check that writes no seed state until every box in the home region has armed it, runs nested and has no claim control.

Architecture: The seeds floor reuses the moves floor's mechanism (internal/host/movesfloor.go) beside it rather than inside it: its own object (control/seeds-protocol.json), its own two local files in the same .moves directory under the same floor.lock, its own verdict on carlos.prev (/opt/carlos/carlos.prev.seeds-allowed), a second clause in both system units, and a second check in the activation rule. The box publishes three new edges-file fields. The console's seedRegionReady reads the published floor, the region's edges files and the region's status heartbeats (the layout report and the claim flag), and answers a refusal or nil. seeds.ProtocolVersion starts at 0, so no box built from this plan can satisfy the console's >= 1; Plan 7 raises it.

Tech Stack: Go 1.25, release.Store, the host package's movesDir (Linux; movesfloor_other.go stubs it elsewhere), status.LoadBoxes, /bin/sh for the unit test.

Spec: docs/superpowers/specs/2026-10-08-seeded-instances-design.md rev 7: §3.3 (codex 16), §6.3 (layout and claims at the console), §10 steps 1 and 2. Index: 2026-10-08-seeded-instances-0-index.md. Copy deck rows SD-N1 to SD-N13, SD-N112 and SD-N113.

Global Constraints

  • Every user-facing string (CLI output, errors, console messages, docs prose) is copy-reviewed by Paul before it is written into code; implementation tasks use only strings from the copy deck. Copy voice: no em dashes; full stops and new sentences instead.
  • CI cross-compiles darwin and windows: linux-only code needs //go:build linux plus a !linux stub.
  • Local test commands: targeted tests (go test ./internal/<pkg>/ -run TestX), not the full suite in a loop. Inside the sandbox the default Go build cache may be read-only; if so use GOCACHE=$TMPDIR/gocache re-exported per command. Socket tests need a short TMPDIR (TMPDIR=/tmp/g). The local gate may need GOFLAGS=-buildvcs=false.
  • Heavy loops run under nice; no -race full suite until the end of a plan.
  • Production use needs nested boxes; the claim-armed path is refused here and left to #250 (spec §3.4).
  • Commits end with Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>.
  • No new file in this series imports internal/moves (the moves copy gate inventories every changed file that does, approved_moves_copy_test.go:984). internal/seeds keeps its own Floor and ReleaseAtOrAbove.
  • New sentences go in new files (internal/host/seedsfloor.go, internal/console/seedfloor.go, system_seeds.go), never in a file listed in movesScanFiles (approved_moves_copy_test.go:541): the moves gate's TestMovesLegacyExemptionsAreLegacy refuses a new sentence in its legacy-exempt files.
  • TestClaimAuthoritySourceCensus (internal/console/claim_authority_census_test.go) inventories every console call named in its selected set (Open, NewS3Store, WithAppMutation, and others); add each new call site this plan writes in internal/console or cmd/console to internal/console/testdata/claim-authority-sites.txt in the format the failing test prints.
  • When a task wires a deck row, append its id to seededWiredIDs in approved_seeded_copy_test.go in the same commit.
  • Write each deck sentence as exactly one string literal or one format string holding the row's text, with %s or %d at each <placeholder> and nothing else: no prefix, no trailing \n, no leading spaces. The gate's matcher (movesRowSpoken, approved_moves_copy_test.go:811) anchors both ends. Print with fmt.Fprintln(w, sentence) or a " %s\n" format around a separately built sentence.

Review Focus

  1. A box whose seeds-floor.min-release was written but whose seeds-floor.json was not (a crash between the two writes) must not let the units pick a carlos.prev the Go check would refuse. The write order is min-release first, JSON last, as for the moves floor. Test in Task 2 (TestSeedsFloorFilesWriteOrder).
  2. A deployment that never publishes the seeds floor must update and recover exactly as today: no file, no verdict, and the units' new clause is true. Test in Task 3 (TestSystemUnitsSelectPrevByTheSeedsFloor, case "no seeds floor").
  3. A box present in the region's edges files but missing from the status heartbeats (or stale there) must count as unconfirmed, never as nested. Test in Task 5 (TestSeedRegionReadyRefusesAnUnconfirmedBox).
  4. A box in another region that is below the floor, flat or claim-armed must not block a door in this region. Test in Task 5 (TestSeedRegionReadyIgnoresOtherRegions).
  5. Publishing a lower seeds floor must be refused, and publishing the same one again must succeed without change. Test in Task 1 (TestPublishFloorOnlyRises).

---

File structure

| path | responsibility | |---|---| | internal/seeds/floor.go | ProtocolVersion, RequiredProtocol, FloorKey, local file names, Floor, ReadFloor, PublishFloor, ReleaseAtOrAbove | | internal/host/seedsfloor.go | the box's local seeds floor, its verdict, the activation refusal, armSeedsFloor, the edges fields | | internal/host/movesfloor.go | movesFloorRefresh and rootJudgePrev also do the seeds floor | | internal/host/systemapply.go | armFloor, notePrev, floorArmed, activationRule, activationGuard, floorAllowsForward call the seeds half | | internal/host/units/carlos-system-recover.service, carlos-system-update.service | the second clause | | internal/host/edgesfile.go | three fields; the loop calls withSeedsFields | | internal/console/instancesplacement.go | the reader's three fields | | internal/console/seedfloor.go | seedRegionReady, handleAPISystemSeedsFloor | | internal/console/authcli.go | the route | | system.go, system_seeds.go | carlos system seeds-floor publish | | docs/site/cli.md | SD-N111 under ### carlos system |

---

Task 1: The floor object

Files:

  • Create: internal/seeds/floor.go
  • Test: internal/seeds/floor_test.go

Interfaces:

  • Produces: ProtocolVersion = 0 (this build's protocol; Plan 7 raises it to 1); RequiredProtocol = 1 (what the console requires of every box); FloorKey = "control/seeds-protocol.json"; FloorFile = ".moves/seeds-floor.json"; FloorMinReleaseFile = ".moves/seeds-floor.min-release"; PrevAllowedFile = "carlos.prev.seeds-allowed"; type Floor struct{ Min int; MinRelease string }; ReadFloor(ctx, s release.Store) (*Floor, error); ReleaseAtOrAbove(version, minRelease string) (bool, error); type FloorLowerError struct{ Current string }; PublishFloor(ctx, s release.Store, f Floor) error.
  • [ ] Step 1: Write the failing tests
package seeds

import (
	"context"
	"errors"
	"testing"

	"github.com/carlosframework/platform/internal/release"
)

func TestReadFloorAbsentIsNil(t *testing.T) {
	s := release.NewDirStore(t.TempDir())
	f, err := ReadFloor(context.Background(), s)
	if f != nil || err != nil {
		t.Fatalf("absent floor = %+v, %v; want nil, nil", f, err)
	}
	if _, err := s.Put(context.Background(), FloorKey, []byte("{not json"), ""); err != nil {
		t.Fatal(err)
	}
	if _, err := ReadFloor(context.Background(), s); err == nil {
		t.Fatal("garbage at the floor key read as no floor")
	}
}

func TestPublishFloorOnlyRises(t *testing.T) {
	ctx := context.Background()
	s := release.NewDirStore(t.TempDir())
	if err := PublishFloor(ctx, s, Floor{Min: RequiredProtocol, MinRelease: "v0.27.0"}); err != nil {
		t.Fatal(err)
	}
	if err := PublishFloor(ctx, s, Floor{Min: RequiredProtocol, MinRelease: "v0.27.0"}); err != nil {
		t.Fatalf("the same floor again: %v", err)
	}
	var low *FloorLowerError
	if err := PublishFloor(ctx, s, Floor{Min: RequiredProtocol, MinRelease: "v0.26.9"}); !errors.As(err, &low) || low.Current != "v0.27.0" {
		t.Fatalf("a lower floor: err = %v, want FloorLowerError naming v0.27.0", err)
	}
	if err := PublishFloor(ctx, s, Floor{Min: RequiredProtocol, MinRelease: "v0.28.0"}); err != nil {
		t.Fatalf("a higher floor: %v", err)
	}
	f, err := ReadFloor(ctx, s)
	if err != nil || f == nil || f.MinRelease != "v0.28.0" || f.Min != RequiredProtocol {
		t.Fatalf("floor = %+v, %v", f, err)
	}
	if err := PublishFloor(ctx, s, Floor{Min: RequiredProtocol, MinRelease: "v0.29.0-rc.1"}); err == nil {
		t.Fatal("a prerelease floor published")
	}
}

func TestReleaseAtOrAbove(t *testing.T) {
	for _, c := range []struct {
		v, min string
		want   bool
		err    bool
	}{
		{"v0.27.0", "v0.27.0", true, false},
		{"v0.27.1", "v0.27.0", true, false},
		{"v0.26.9", "v0.27.0", false, false},
		{"v0.28.0-rc.1", "v0.27.0", false, true},
		{"v0.27.0", "garbage", false, true},
	} {
		got, err := ReleaseAtOrAbove(c.v, c.min)
		if got != c.want || (err != nil) != c.err {
			t.Errorf("ReleaseAtOrAbove(%s, %s) = %v, %v", c.v, c.min, got, err)
		}
	}
}

func TestProtocolStartsBelowWhatTheConsoleRequires(t *testing.T) {
	// Plan 3 ships the floor dark: no box built from it can satisfy the
	// console's check. Plan 7 Task 7 raises ProtocolVersion and rewrites
	// this test.
	if ProtocolVersion >= RequiredProtocol {
		t.Fatalf("ProtocolVersion = %d, RequiredProtocol = %d", ProtocolVersion, RequiredProtocol)
	}
}
  • [ ] Step 2: Run the tests to verify they fail

Run: go test ./internal/seeds/ -run 'TestReadFloor|TestPublishFloor|TestReleaseAtOrAbove|TestProtocol' -count=1 Expected: FAIL to compile: undefined: ReadFloor.

  • [ ] Step 3: Write internal/seeds/floor.go
package seeds

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"strings"

	"github.com/carlosframework/platform/internal/release"
)

// ProtocolVersion is the seeds protocol this build speaks. 0 until the box
// can place, swap and roll back a seed (Plan 7 raises it to 1): a box that
// reports 0 can never satisfy the console's RequiredProtocol.
const ProtocolVersion = 0

// RequiredProtocol is what the console requires every box in the region to
// report before it writes any seed state (spec §3.3).
const RequiredProtocol = 1

// FloorKey is the operator-published seeds floor (spec §3.3).
const FloorKey = "control/seeds-protocol.json"

// The box's local copy of the floor and its verdict on carlos.prev, beside
// the moves floor's files under the same floor.lock
// (internal/host/movesfloor.go). The two system units read the second and
// third by fixed path.
const (
	FloorFile           = ".moves/seeds-floor.json"        // under the box's DataDir
	FloorMinReleaseFile = ".moves/seeds-floor.min-release" // under the box's DataDir
	PrevAllowedFile     = "carlos.prev.seeds-allowed"      // beside carlos.prev in the opt dir
)

// Floor is the seeds floor: the protocol every box must speak, and the
// first release that speaks it.
type Floor struct {
	Min        int    `json:"min"`
	MinRelease string `json:"min_release"`
}

// ReadFloor reads the published floor. Absent is (nil, nil); present but
// unreadable is an error, never a silent "no floor".
func ReadFloor(ctx context.Context, s release.Store) (*Floor, error) {
	f, _, err := readFloorETag(ctx, s)
	return f, err
}

func readFloorETag(ctx context.Context, s release.Store) (*Floor, string, error) {
	body, etag, err := s.Get(ctx, FloorKey)
	if errors.Is(err, os.ErrNotExist) {
		return nil, "", nil
	}
	if err != nil {
		return nil, "", fmt.Errorf("seeds: read floor: %w", err)
	}
	var f Floor
	if err := json.Unmarshal(body, &f); err != nil {
		return nil, "", fmt.Errorf("seeds: parse floor: %w", err)
	}
	return &f, etag, nil
}

// ReleaseAtOrAbove reports whether version is at or above minRelease, both
// vMAJOR.MINOR.PATCH. It fails closed: a prerelease or a malformed value is
// false with an error.
func ReleaseAtOrAbove(version, minRelease string) (bool, error) {
	min, ok := release.ParseSemVer(strings.TrimPrefix(minRelease, "v"))
	if !ok {
		return false, fmt.Errorf("seeds: malformed minimum release %q", minRelease)
	}
	v, ok := release.ParseSemVer(strings.TrimPrefix(version, "v"))
	if !ok {
		return false, fmt.Errorf("seeds: unparseable release %q", version)
	}
	return v.Compare(min) >= 0, nil
}

// FloorLowerError is PublishFloor refusing to lower the floor.
type FloorLowerError struct{ Current string }

func (e *FloorLowerError) Error() string {
	return "seeds: the published floor is already " + e.Current
}

// PublishFloor writes the floor by CAS. A floor only rises: a lower
// min_release is a *FloorLowerError; the same one is a no-op success.
func PublishFloor(ctx context.Context, s release.Store, f Floor) error {
	if ok, err := ReleaseAtOrAbove(f.MinRelease, f.MinRelease); !ok || err != nil {
		return fmt.Errorf("seeds: %q is not a release", f.MinRelease)
	}
	cur, etag, err := readFloorETag(ctx, s)
	if err != nil {
		return err
	}
	if cur != nil {
		if ok, _ := ReleaseAtOrAbove(f.MinRelease, cur.MinRelease); !ok {
			return &FloorLowerError{Current: cur.MinRelease}
		}
		if *cur == f {
			return nil
		}
	}
	body, err := json.Marshal(f)
	if err != nil {
		return err
	}
	_, err = s.Put(ctx, FloorKey, body, etag)
	return err
}

release.ParseSemVer and SemVer.Compare are the ones internal/moves/floor.go:88 uses.

  • [ ] Step 4: Run the tests to verify they pass

Run: go test ./internal/seeds/ -count=1 Expected: PASS.

  • [ ] Step 5: Commit
git add internal/seeds/floor.go internal/seeds/floor_test.go
git commit -m "seeds: the floor object, published by CAS and only rising (Plan 3 Task 1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"

Task 2: The box's local seeds floor and its verdict on carlos.prev

Files:

  • Create: internal/host/seedsfloor.go
  • Modify: internal/host/movesfloor.go:447-475 (rootJudgePrev, movesFloorRefresh)
  • Test: internal/host/seedsfloor_test.go, internal/host/seedsfloor_linux_test.go (//go:build linux)

Interfaces:

  • Consumes: withFloorLock(dataDir string, create bool, fn func(d *movesDir) error) error, (*movesDir).WriteDurable(name string, data []byte, mode os.FileMode) error, openBeneath, readBeneathLine, readLine, writeFileDurable, removeDurable, errFloorWrite, prevVersionPath, systemPrevPath, withPrevLock, release.OptDir(), dataDir() (all in internal/host).
  • Produces: readLocalSeedsFloor(dataDir string) (*seeds.Floor, error); writeSeedsFloorFiles(d *movesDir, f seeds.Floor) error; syncLocalSeedsFloor(ctx, s release.Store, dataDir string) (*seeds.Floor, error); seedsPrevAllowedPath(optDir string) string; judgeSeedsPrev(optDir, dataDir string) error; seedsFloorArmed(dataDir string) bool.
  • [ ] Step 1: Write the failing tests

internal/host/seedsfloor_linux_test.go (the floor files are written through movesDir, which is Linux-only):

//go:build linux

package host

import (
	"context"
	"os"
	"path/filepath"
	"testing"

	"github.com/carlosframework/platform/internal/release"
	"github.com/carlosframework/platform/internal/seeds"
)

func publishSeedsFloor(t *testing.T, s release.Store, rel string) {
	t.Helper()
	if err := seeds.PublishFloor(context.Background(), s, seeds.Floor{Min: seeds.RequiredProtocol, MinRelease: rel}); err != nil {
		t.Fatal(err)
	}
}

func TestSyncLocalSeedsFloorArmsAndOnlyRises(t *testing.T) {
	ctx := context.Background()
	dd := t.TempDir()
	s := release.NewDirStore(t.TempDir())
	if f, err := syncLocalSeedsFloor(ctx, s, dd); f != nil || err != nil {
		t.Fatalf("nothing published: %+v, %v", f, err)
	}
	if seedsFloorArmed(dd) {
		t.Fatal("armed with nothing published")
	}
	publishSeedsFloor(t, s, "v0.27.0")
	f, err := syncLocalSeedsFloor(ctx, s, dd)
	if err != nil || f == nil || f.MinRelease != "v0.27.0" || !seedsFloorArmed(dd) {
		t.Fatalf("after publish: %+v, %v", f, err)
	}
	if line, ok := readBeneathLine(dd, seeds.FloorMinReleaseFile); !ok || line != "v0.27.0" {
		t.Fatalf("min-release file = %q, %v", line, ok)
	}
	// Once armed, armed: an object that disappears leaves the files.
	if err := s.Delete(ctx, seeds.FloorKey); err != nil {
		t.Fatal(err)
	}
	if f, _ := syncLocalSeedsFloor(ctx, s, dd); f == nil || f.MinRelease != "v0.27.0" {
		t.Fatalf("after the object went: %+v", f)
	}
}

func TestSeedsFloorFilesWriteOrder(t *testing.T) {
	dd := t.TempDir()
	// A crash after the min-release file and before the JSON: the Go
	// readers see no floor, and the units' line is AHEAD of every verdict.
	if err := os.MkdirAll(filepath.Join(dd, ".moves"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(dd, seeds.FloorMinReleaseFile), []byte("v0.27.0\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	if f, err := readLocalSeedsFloor(dd); f != nil || err != nil {
		t.Fatalf("readers trust only the JSON: %+v, %v", f, err)
	}
	// And the writer writes the line first, the JSON last.
	var order []string
	writeHook = func(name string) { order = append(order, name) }
	defer func() { writeHook = nil }()
	if err := withFloorLock(dd, true, func(d *movesDir) error {
		return writeSeedsFloorFiles(d, seeds.Floor{Min: 1, MinRelease: "v0.28.0"})
	}); err != nil {
		t.Fatal(err)
	}
	if len(order) != 2 || order[0] != filepath.Base(seeds.FloorMinReleaseFile) || order[1] != filepath.Base(seeds.FloorFile) {
		t.Fatalf("write order = %q", order)
	}
}

func TestJudgeSeedsPrev(t *testing.T) {
	opt, dd := t.TempDir(), t.TempDir()
	verdict := seedsPrevAllowedPath(opt)
	// No carlos.prev: no verdict.
	if err := judgeSeedsPrev(opt, dd); err != nil {
		t.Fatal(err)
	}
	if _, err := os.Stat(verdict); !os.IsNotExist(err) {
		t.Fatal("a verdict with no carlos.prev")
	}
	if err := os.WriteFile(systemPrevPath(opt), []byte("#!/bin/sh\n"), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := writePrevVersion(opt, "v0.27.2"); err != nil {
		t.Fatal(err)
	}
	// No seeds floor: no verdict (the units do not read one then).
	if err := judgeSeedsPrev(opt, dd); err != nil {
		t.Fatal(err)
	}
	if _, err := os.Stat(verdict); !os.IsNotExist(err) {
		t.Fatal("a verdict with no seeds floor")
	}
	s := release.NewDirStore(t.TempDir())
	publishSeedsFloor(t, s, "v0.27.0")
	if _, err := syncLocalSeedsFloor(context.Background(), s, dd); err != nil {
		t.Fatal(err)
	}
	if err := judgeSeedsPrev(opt, dd); err != nil {
		t.Fatal(err)
	}
	if line, ok := readLine(verdict); !ok || line != "v0.27.0" {
		t.Fatalf("verdict = %q, %v; want the floor's min_release", line, ok)
	}
	// carlos.prev below the floor: the verdict goes.
	if err := writePrevVersion(opt, "v0.26.9"); err != nil {
		t.Fatal(err)
	}
	if err := judgeSeedsPrev(opt, dd); err != nil {
		t.Fatal(err)
	}
	if _, err := os.Stat(verdict); !os.IsNotExist(err) {
		t.Fatal("a verdict for a carlos.prev below the floor")
	}
}

writeHook is a test seam this task adds to (*movesDir).WriteDurable in movesfloor_linux.go: a package variable var writeHook func(name string) called with the file name at the top of WriteDurable when non-nil. If movesfloor_linux_test.go already has an equivalent seam for TestFloorReadersTrustOnlyTheJSON, use that one instead and drop this addition.

  • [ ] Step 2: Run the tests to verify they fail

Run: go test ./internal/host/ -run 'TestSyncLocalSeedsFloor|TestSeedsFloorFilesWriteOrder|TestJudgeSeedsPrev' -count=1 Expected: FAIL to compile: undefined: syncLocalSeedsFloor.

  • [ ] Step 3: Write internal/host/seedsfloor.go
package host

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"

	"github.com/carlosframework/platform/internal/release"
	"github.com/carlosframework/platform/internal/seeds"
)

// The seeds protocol floor on a box (seeded-instances spec §3.3, codex 16):
// the moves floor's mechanism (movesfloor.go), beside it. Its two local
// files live in the same .moves directory and are written under the same
// floor.lock; its verdict on carlos.prev sits beside the moves verdict; the
// two system units carry a second clause for it; the activation rule
// checks it as well. A deployment that never publishes it has none of the
// files, and every path here then does what it did before.

func seedsPrevAllowedPath(optDir string) string { return filepath.Join(optDir, seeds.PrevAllowedFile) }

// readLocalSeedsFloor reads <dataDir>/.moves/seeds-floor.json without
// following a symlink below dataDir. (nil, nil): not armed. A file that is
// there but unusable is an error, which callers treat as armed and unknown.
func readLocalSeedsFloor(dataDir string) (*seeds.Floor, error) {
	f, err := openBeneath(dataDir, filepath.Dir(seeds.FloorFile), filepath.Base(seeds.FloorFile))
	if errors.Is(err, os.ErrNotExist) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	defer f.Close()
	st, err := f.Stat()
	if err != nil {
		return nil, err
	}
	if !st.Mode().IsRegular() || st.Size() > movesFloorMaxBytes {
		return nil, fmt.Errorf("%s is not a regular file under %d bytes", f.Name(), movesFloorMaxBytes)
	}
	var fl seeds.Floor
	if err := json.NewDecoder(io.LimitReader(f, movesFloorMaxBytes)).Decode(&fl); err != nil {
		return nil, fmt.Errorf("%s: %w", f.Name(), err)
	}
	if ok, err := seeds.ReleaseAtOrAbove(fl.MinRelease, fl.MinRelease); !ok || err != nil {
		return nil, fmt.Errorf("%s: min_release %q is not a release", f.Name(), fl.MinRelease)
	}
	return &fl, nil
}

// seedsFloorArmed: a local seeds floor that reads, or one that is there and
// cannot be read.
func seedsFloorArmed(dataDir string) bool {
	f, err := readLocalSeedsFloor(dataDir)
	return err != nil || f != nil
}

// writeSeedsFloorFiles writes the min-release line FIRST and the JSON LAST,
// for writeFloorFiles' reason: after a crash between them the line can only
// be ahead of the JSON, so the units never pick a carlos.prev the Go checks
// would refuse.
func writeSeedsFloorFiles(d *movesDir, f seeds.Floor) error {
	body, err := json.Marshal(f)
	if err != nil {
		return err
	}
	if err := d.WriteDurable(filepath.Base(seeds.FloorMinReleaseFile), []byte(f.MinRelease+"\n"), 0o644); err != nil {
		return err
	}
	return d.WriteDurable(filepath.Base(seeds.FloorFile), append(body, '\n'), 0o644)
}

// syncLocalSeedsFloor is syncLocalFloor's rule for the seeds floor: a
// published, usable floor that is not lower is written under the floor
// lock; absent leaves what is on disk; a lower one is refused; an
// unreadable store leaves what is on disk and returns the error.
func syncLocalSeedsFloor(ctx context.Context, s release.Store, dataDir string) (*seeds.Floor, error) {
	if s == nil {
		local, lerr := readLocalSeedsFloor(dataDir)
		return local, errors.Join(errors.New("no deployment store to read the seeds floor from"), lerr)
	}
	remote, rerr := seeds.ReadFloor(ctx, s)
	switch {
	case rerr != nil:
		local, lerr := readLocalSeedsFloor(dataDir)
		return local, errors.Join(rerr, lerr)
	case remote == nil:
		return readLocalSeedsFloor(dataDir)
	}
	if ok, err := seeds.ReleaseAtOrAbove(remote.MinRelease, remote.MinRelease); !ok || err != nil {
		local, lerr := readLocalSeedsFloor(dataDir)
		return local, errors.Join(fmt.Errorf("the published seeds min_release %q is not a release; the seeds floor on this box stays as it was", remote.MinRelease), lerr)
	}
	var out *seeds.Floor
	ran := false
	err := withFloorLock(dataDir, true, func(d *movesDir) error {
		ran = true
		local, lerr := readLocalSeedsFloor(dataDir)
		if lerr == nil && local != nil {
			if remote.Min < local.Min {
				out = local
				return fmt.Errorf("the published seeds floor's protocol minimum %d is below this box's %d", remote.Min, local.Min)
			}
			if ok, _ := seeds.ReleaseAtOrAbove(remote.MinRelease, local.MinRelease); !ok {
				out = local
				return fmt.Errorf("the published seeds min_release %q is below this box's %s", remote.MinRelease, local.MinRelease)
			}
			if *local == *remote {
				if line, ok := readBeneathLine(dataDir, seeds.FloorMinReleaseFile); ok && line == remote.MinRelease {
					out = local
					return nil
				}
			}
		}
		if err := writeSeedsFloorFiles(d, *remote); err != nil {
			out, _ = readLocalSeedsFloor(dataDir)
			return fmt.Errorf("%w: %v", errFloorWrite, err)
		}
		out = remote
		return nil
	})
	if !ran {
		out, _ = readLocalSeedsFloor(dataDir)
		return out, fmt.Errorf("%w: %v", errFloorWrite, err)
	}
	return out, err
}

// judgeSeedsPrev writes the seeds verdict when carlos.prev is at or above
// the armed seeds floor, and removes it otherwise (no carlos.prev, no
// floor, no version, below). Unlike the moves verdict it does not read the
// registry: a seed has no hold rows.
func judgeSeedsPrev(optDir, dataDir string) error {
	deny := func(cause error) error {
		if err := removeDurable(seedsPrevAllowedPath(optDir)); err != nil {
			return errors.Join(cause, fmt.Errorf("remove the seeds verdict on carlos.prev: %w", err))
		}
		return cause
	}
	if _, err := os.Stat(systemPrevPath(optDir)); err != nil {
		return deny(nil)
	}
	ran := false
	err := withFloorLock(dataDir, false, func(*movesDir) error {
		ran = true
		floor, err := readLocalSeedsFloor(dataDir)
		if err != nil {
			return deny(fmt.Errorf("seeds protocol floor: the floor on this box cannot be read: %v", err))
		}
		if floor == nil {
			return deny(nil)
		}
		version, ok := readLine(prevVersionPath(optDir))
		if !ok {
			return deny(nil)
		}
		if at, _ := seeds.ReleaseAtOrAbove(version, floor.MinRelease); !at {
			return deny(nil)
		}
		return writeFileDurable(seedsPrevAllowedPath(optDir), []byte(floor.MinRelease+"\n"), 0o644)
	})
	if !ran {
		return deny(fmt.Errorf("seeds protocol floor: the floor on this box cannot be read: %v", err))
	}
	return err
}

// rootJudgeSeedsPrev is the root adopt pass's call: nothing to do on a box
// that has neither a seeds floor nor a seeds verdict.
func rootJudgeSeedsPrev() error {
	optDir, dd := release.OptDir(), dataDir()
	if !seedsFloorArmed(dd) {
		if _, err := os.Stat(seedsPrevAllowedPath(optDir)); errors.Is(err, os.ErrNotExist) {
			return nil
		}
	}
	return withPrevLock(optDir, func() error { return judgeSeedsPrev(optDir, dd) })
}

// seedsFloorRefresh is the agent's hourly refresh of the seeds floor.
func seedsFloorRefresh(ctx context.Context, s release.Store, dataDir string, logf func(string, ...any)) {
	if _, err := syncLocalSeedsFloor(ctx, s, dataDir); err != nil {
		logf("agent: seeds protocol floor refresh: %v", err)
	}
}
  • [ ] Step 4: Call the seeds half from the moves floor's loop and the adopt pass

In internal/host/movesfloor.go, movesFloorRefresh (:469) gains one line after its own sync:

	seedsFloorRefresh(ctx, s, dataDir, logf)

and rootJudgePrev (:447) returns both judgements. Replace its body's two return paths so the seeds judgement always runs:

func rootJudgePrev(reg *registry.Registry) error {
	return errors.Join(rootJudgeMovesPrev(reg), rootJudgeSeedsPrev())
}

// rootJudgeMovesPrev is the moves half, rootJudgePrev's body before the
// seeds floor existed, unchanged.
func rootJudgeMovesPrev(reg *registry.Registry) error {
	optDir, dd := release.OptDir(), dataDir()
	// ... the existing body, verbatim
}
  • [ ] Step 5: Run the tests to verify they pass

Run: go test ./internal/host/ -run 'TestSyncLocalSeedsFloor|TestSeedsFloorFilesWriteOrder|TestJudgeSeedsPrev|TestJudgePrev|TestSyncLocalFloor|TestFloor' -count=1 Expected: PASS.

  • [ ] Step 6: Commit
git add internal/host/seedsfloor.go internal/host/seedsfloor_linux_test.go internal/host/movesfloor.go internal/host/movesfloor_linux.go
git commit -m "host: the box's local seeds floor and its verdict on carlos.prev (Plan 3 Task 2)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"

Task 3: The system units and the activation rule

Files:

  • Modify: internal/host/units/carlos-system-recover.service:29, internal/host/units/carlos-system-update.service:35 and both files' header comments
  • Modify: internal/host/systemapply.go (armFloor :833, notePrev :784, floorArmed :808, activationRule :1010, activationGuard :933, floorAllowsForward :1041)
  • Modify: internal/host/seedsfloor.go (add armSeedsFloor, seedsActivationRefusal)
  • Test: internal/host/seedsfloor_test.go, internal/host/host_test.go (TestSystemUnitsAreShapedLikeTheStageRelay's want lists)

Interfaces:

  • Consumes: Task 2's functions; refuse(format string, args ...any) error, errSystemDeferred, (a *systemApplier).ops.logf (all systemapply.go).
  • Produces: seedsActivationRefusal(version, dataDir string) error (an activationRefused, SD-N1); (a *systemApplier) armSeedsFloor(ctx) error.
  • [ ] Step 1: Write the failing tests

internal/host/seedsfloor_test.go (not Linux-only: it runs /bin/sh, which every CI unit runner has; skip on Windows):

package host

import (
	"errors"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

	"github.com/carlosframework/platform/internal/moves"
	"github.com/carlosframework/platform/internal/seeds"
)

func TestSystemUnitsSelectPrevByTheSeedsFloor(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("no /bin/sh")
	}
	for _, unit := range []string{"carlos-system-recover.service", "carlos-system-update.service"} {
		t.Run(unit, func(t *testing.T) {
			b, err := unitsFS.ReadFile("units/" + unit)
			if err != nil {
				t.Fatal(err)
			}
			var line string
			for _, l := range strings.Split(string(b), "\n") {
				if strings.HasPrefix(l, "ExecStart=") {
					line = l
				}
			}
			if strings.ContainsAny(line, "%$") {
				t.Fatalf("ExecStart carries a %% or a $: %s", line)
			}
			for _, path := range []string{"/data/" + seeds.FloorMinReleaseFile, "/opt/carlos/" + seeds.PrevAllowedFile} {
				if !strings.Contains(line, path) {
					t.Fatalf("ExecStart does not read %s: %s", path, line)
				}
			}
			body := strings.TrimSuffix(strings.TrimPrefix(line, "ExecStart=/bin/sh -c '"), "'")
			for _, tc := range []struct {
				name                 string
				movesFloor, movesOK  string
				seedsFloor, seedsOK  string
				want                 string
			}{
				{"no seeds floor", "", "", "", "", "carlos.prev"},
				{"seeds floor and a verdict naming it", "", "", "v0.27.0\n", "v0.27.0\n", "carlos.prev"},
				{"seeds floor and no verdict", "", "", "v0.27.0\n", "", "carlos"},
				{"a seeds verdict for an older floor", "", "", "v0.27.0\n", "v0.26.0\n", "carlos"},
				{"both floors, both verdicts", "v0.30.0\n", "v0.30.0\n", "v0.27.0\n", "v0.27.0\n", "carlos.prev"},
				{"both floors, only the moves verdict", "v0.30.0\n", "v0.30.0\n", "v0.27.0\n", "", "carlos"},
				{"both floors, only the seeds verdict", "v0.30.0\n", "", "v0.27.0\n", "v0.27.0\n", "carlos"},
			} {
				t.Run(tc.name, func(t *testing.T) {
					root := t.TempDir()
					opt, data, ran := filepath.Join(root, "opt"), filepath.Join(root, "data"), filepath.Join(root, "ran")
					os.MkdirAll(filepath.Join(opt, "system", "router"), 0o755)
					os.MkdirAll(filepath.Join(data, ".moves"), 0o755)
					for _, name := range []string{"carlos", "carlos.prev"} {
						os.WriteFile(filepath.Join(opt, name), []byte("#!/bin/sh\necho "+name+" > "+ran+"\n"), 0o755)
					}
					os.WriteFile(filepath.Join(opt, "system", "router", "journal.json"), []byte("{}"), 0o600)
					put := func(path, body string) {
						if body != "" {
							os.WriteFile(path, []byte(body), 0o644)
						}
					}
					put(filepath.Join(data, moves.FloorMinReleaseFile), tc.movesFloor)
					put(filepath.Join(opt, moves.PrevAllowedFile), tc.movesOK)
					put(filepath.Join(data, seeds.FloorMinReleaseFile), tc.seedsFloor)
					put(filepath.Join(opt, seeds.PrevAllowedFile), tc.seedsOK)
					script := strings.ReplaceAll(strings.ReplaceAll(body, "/data/", data+"/"), "/opt/carlos/", opt+"/")
					if out, err := exec.Command("/bin/sh", "-c", script).CombinedOutput(); err != nil {
						t.Fatalf("%v: %s", err, out)
					}
					got, _ := os.ReadFile(ran)
					if strings.TrimSpace(string(got)) != tc.want {
						t.Fatalf("ran %q, want %s", strings.TrimSpace(string(got)), tc.want)
					}
				})
			}
		})
	}
}

func TestSeedsActivationRefusal(t *testing.T) {
	dd := t.TempDir()
	if err := seedsActivationRefusal("v0.20.0", dd); err != nil {
		t.Fatalf("no seeds floor: %v", err)
	}
	os.MkdirAll(filepath.Join(dd, ".moves"), 0o755)
	os.WriteFile(filepath.Join(dd, seeds.FloorFile), []byte(`{"min":1,"min_release":"v0.27.0"}`), 0o644)
	if err := seedsActivationRefusal("v0.27.0", dd); err != nil {
		t.Fatalf("at the floor: %v", err)
	}
	err := seedsActivationRefusal("v0.26.9", dd)
	var refused activationRefused
	if !errors.As(err, &refused) || err.Error() != "seeds protocol floor: v0.26.9 is below v0.27.0" {
		t.Fatalf("below the floor: %v", err)
	}
	os.WriteFile(filepath.Join(dd, seeds.FloorFile), []byte(`{garbage`), 0o644)
	if err := seedsActivationRefusal("v0.30.0", dd); !errors.As(err, &refused) {
		t.Fatalf("an unreadable seeds floor: %v, want a refusal", err)
	}
}

Add "floor.min-release"'s seeds sibling to the want lists in TestSystemUnitsAreShapedLikeTheStageRelay (host_test.go:1300): append "seeds-floor.min-release", "carlos.prev.seeds-allowed" to both the update unit's and the recover unit's lists.

  • [ ] Step 2: Run the tests to verify they fail

Run: go test ./internal/host/ -run 'TestSystemUnitsSelectPrevByTheSeedsFloor|TestSeedsActivationRefusal|TestSystemUnitsAreShapedLikeTheStageRelay' -count=1 Expected: FAIL: the units lack the seeds paths; undefined: seedsActivationRefusal.

  • [ ] Step 3: Add the units' second clause

In both unit files, replace the ExecStart= line. Recover unit:

ExecStart=/bin/sh -c 'if [ -f /opt/carlos/system/router/journal.json ] && [ -x /opt/carlos/carlos.prev ] && { ! [ -s /data/.moves/floor.min-release ] || cmp -s /data/.moves/floor.min-release /opt/carlos/carlos.prev.allowed; } && { ! [ -s /data/.moves/seeds-floor.min-release ] || cmp -s /data/.moves/seeds-floor.min-release /opt/carlos/carlos.prev.seeds-allowed; }; then exec /opt/carlos/carlos.prev system apply --recover; fi; exec /opt/carlos/carlos system apply --recover'

Update unit: the same two { … } clauses joined with &&, keeping its own system apply arguments exactly as they are today. Extend each file's header comment by one paragraph:

# The seeds floor (seeded-instances spec §3.3) adds the same test for its
# own files: once /data/.moves/seeds-floor.min-release is there and not
# empty, carlos.prev runs only when /opt/carlos/carlos.prev.seeds-allowed
# names that same floor, byte for byte.
  • [ ] Step 4: Add the activation refusal and the apply's arming to seedsfloor.go
// seedsActivationRefusal is the seeds half of the activation rule: version
// below the armed seeds floor is refused (SD-N1). The floor is read from
// the local file alone (boot recovery runs before the network). Unarmed:
// nil.
func seedsActivationRefusal(version, dataDir string) error {
	floor, err := readLocalSeedsFloor(dataDir)
	if err != nil {
		return refuse("seeds protocol floor: the floor on this box cannot be read: %v", err)
	}
	if floor == nil {
		return nil
	}
	if ok, _ := seeds.ReleaseAtOrAbove(version, floor.MinRelease); !ok {
		return refuse("seeds protocol floor: %s is below %s", version, floor.MinRelease)
	}
	return nil
}

// armSeedsFloor is armFloor's seeds half, with the same rule: a published
// floor this box could not write down defers the apply; anything else is a
// log line.
func (a *systemApplier) armSeedsFloor(ctx context.Context) error {
	f, err := syncLocalSeedsFloor(ctx, a.store, a.dataDir)
	if err == nil {
		return nil
	}
	if errors.Is(err, errFloorWrite) {
		return fmt.Errorf("%w: seeds protocol floor: %v", errSystemDeferred, err)
	}
	onDisk := "none"
	if f != nil {
		onDisk = f.MinRelease
	}
	a.ops.logf("system apply: seeds protocol floor: %v (the seeds floor on this box stays %s)", err, onDisk)
	return nil
}

refuse("seeds protocol floor: %s is below %s", …) is SD-N1's approved text. The version == "" case (a carlos.prev with no recorded version) is below any floor: ReleaseAtOrAbove("", …) is false.

  • [ ] Step 5: Wire the seeds half into systemapply.go
  • armFloor (:833), first statement: if err := a.armSeedsFloor(ctx); err != nil { return err }.
  • notePrev (:784): in the replace branch, add removeDurable(seedsPrevAllowedPath(a.optDir)) to the errors.Join with the moves verdict and sidecar; replace the final return a.floorStep(judgePrevWith(…)) with return a.floorStep(errors.Join(judgePrevWith(a.optDir, a.dataDir, a.freezeHolds, func() string { return a.prevBuild(ctx) }), judgeSeedsPrev(a.optDir, a.dataDir))).
  • floorArmed (:808): return err != nil || f != nil || seedsFloorArmed(a.dataDir).
  • activationRule (:1010), first statement: if err := seedsActivationRefusal(version, a.dataDir); err != nil { return err }.
  • activationGuard (:933), inside the withFloorLock closure, before floor, err := readLocalFloor(a.dataDir): if err := seedsActivationRefusal(target, a.dataDir); err != nil { return err }.
  • floorAllowsForward (:1041), first statement: if err := seedsActivationRefusal(version, a.dataDir); err != nil { return err }.
  • [ ] Step 6: Run the tests to verify they pass

Run: go test ./internal/host/ -run 'TestSystemUnits|TestSeedsActivationRefusal|TestActivation|TestSystemApply|TestNotePrev|TestDecidePrev|TestFloor' -count=1 Expected: PASS. The moves floor's existing tests are unchanged: with no seeds files, every new call is a no-op.

  • [ ] Step 7: Wire SD-N1 and commit

Append "SD-N1" to seededWiredIDs in approved_seeded_copy_test.go. Run go test . -run TestSeededWiredCopyIsApplied -count=1 (PASS: the format string in seedsfloor.go matches the row).

git add internal/host/units/ internal/host/systemapply.go internal/host/seedsfloor.go internal/host/seedsfloor_test.go internal/host/host_test.go approved_seeded_copy_test.go
git commit -m "host: the seeds floor in the system units and the activation rule (Plan 3 Task 3)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"

Task 4: The edges file reports the seeds floor

Files:

  • Modify: internal/host/edgesfile.go:295-305 (struct), :817 (the loop)
  • Modify: internal/host/seedsfloor.go (add withSeedsFields)
  • Modify: internal/console/instancesplacement.go:83-86 (reader)
  • Test: internal/host/seedsfloor_linux_test.go

Interfaces:

  • Produces: edgesFile.SeedsProtocol int (json:"seeds_protocol", not omitempty), edgesFile.SeedsFloorMinRelease string (json:"seeds_floor_min_release,omitempty"), edgesFile.PrevSeedsAllowedFor string (json:"prev_seeds_allowed_for,omitempty"); the same three on edgesFilePlacementView; withSeedsFields(f edgesFile, dataDir, optDir string) edgesFile.
  • [ ] Step 1: Write the failing test

Append to internal/host/seedsfloor_linux_test.go:

func TestWithSeedsFields(t *testing.T) {
	dd, opt := t.TempDir(), t.TempDir()
	f := withSeedsFields(edgesFile{Box: "kass-1"}, dd, opt)
	if f.SeedsProtocol != seeds.ProtocolVersion || f.SeedsFloorMinRelease != "" || f.PrevSeedsAllowedFor != "" {
		t.Fatalf("unarmed = %+v", f)
	}
	s := release.NewDirStore(t.TempDir())
	publishSeedsFloor(t, s, "v0.27.0")
	if _, err := syncLocalSeedsFloor(context.Background(), s, dd); err != nil {
		t.Fatal(err)
	}
	os.WriteFile(seedsPrevAllowedPath(opt), []byte("v0.27.0\n"), 0o644)
	f = withSeedsFields(f, dd, opt)
	if f.SeedsFloorMinRelease != "v0.27.0" || f.PrevSeedsAllowedFor != "v0.27.0" {
		t.Fatalf("armed = %+v", f)
	}
	body, _ := json.Marshal(f)
	if !strings.Contains(string(body), `"seeds_protocol":0`) {
		t.Fatalf("seeds_protocol must be on the wire even at 0: %s", body)
	}
}

(add "encoding/json" and "strings" to that file's imports).

  • [ ] Step 2: Run it to verify it fails

Run: go test ./internal/host/ -run TestWithSeedsFields -count=1 Expected: FAIL to compile: undefined: withSeedsFields.

  • [ ] Step 3: Add the fields and the writer

In edgesFile, after PrevAllowedFor:

	// The seeds protocol (seeded-instances spec §3.3), read by the console's
	// seedRegionReady. SeedsProtocol is not omitempty: 0 is a real answer
	// (a build that cannot place a seed). SeedsFloorMinRelease is the
	// armed seeds floor's min_release (absent: not armed), and
	// PrevSeedsAllowedFor the floor carlos.prev was judged at or above.
	SeedsProtocol        int    `json:"seeds_protocol"`
	SeedsFloorMinRelease string `json:"seeds_floor_min_release,omitempty"`
	PrevSeedsAllowedFor  string `json:"prev_seeds_allowed_for,omitempty"`

In seedsfloor.go:

// withSeedsFields sets the edges file's seeds fields from this box's own
// files, read at each write, as withMovesFields does for the moves floor.
func withSeedsFields(f edgesFile, dataDir, optDir string) edgesFile {
	f.SeedsProtocol = seeds.ProtocolVersion
	f.SeedsFloorMinRelease = ""
	if fl, err := readLocalSeedsFloor(dataDir); err == nil && fl != nil {
		f.SeedsFloorMinRelease = fl.MinRelease
	}
	f.PrevSeedsAllowedFor, _ = readLine(seedsPrevAllowedPath(optDir))
	return f
}

In the loop (edgesfile.go:817), after ident = withMovesFields(ident, dataDir, optDir): ident = withSeedsFields(ident, dataDir, optDir).

In internal/console/instancesplacement.go, after PrevAllowedFor in edgesFilePlacementView:

	// The seeds floor (seeded-instances spec §3.3), read only by
	// seedRegionReady.
	SeedsProtocol        int    `json:"seeds_protocol"`
	SeedsFloorMinRelease string `json:"seeds_floor_min_release"`
	PrevSeedsAllowedFor  string `json:"prev_seeds_allowed_for"`
  • [ ] Step 4: Run the tests to verify they pass

Run: go test ./internal/host/ -run 'TestWithSeedsFields|TestEdgesFile|TestWithMovesFields' -count=1 && go test ./internal/console/ -run 'TestEdges|TestPlacement|TestSelectMoveBoxes' -count=1 Expected: PASS.

  • [ ] Step 5: Commit
git add internal/host/edgesfile.go internal/host/seedsfloor.go internal/host/seedsfloor_linux_test.go internal/console/instancesplacement.go
git commit -m "edges file: the seeds protocol, floor and verdict (Plan 3 Task 4)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"

Task 5: The console's region readiness check

Files:

  • Create: internal/console/seedfloor.go
  • Test: internal/console/seedfloor_test.go

Interfaces:

  • Consumes: s.liveEdges(ctx) (edgesSnapshot, error), edgesSnapshot.complete(), s.homeRegionIn(string) string, s.homeRegionOut(string) string, s.systemBoxes(ctx, strict bool) ([]status.Box, bool, error), status.BoxStale(b, now, status.SnapshotPeriod), doorRefusal{Status int; Msg string}, seeds.ReadFloor, seeds.RequiredProtocol.
  • Produces: (s *Server) seedRegionReady(ctx context.Context, region string, oneBox bool) *doorRefusal; nil means every box in region (the record vocabulary: "" is the home region) is ready. Plans 5 and 6 call it before writing any seed state; the import door passes oneBox true (see below).

The order of checks, each a refusal naming the boxes it found (sorted, comma-joined):

  1. s.cfg.HostClaim on this console: 409, SD-N8.
  2. The published floor: read error 503 SD-N4; absent 409 SD-N3.
  3. The edges snapshot: read error or !snap.complete() 503 SD-N5; no edges file for the region 503 SD-N5; with oneBox, more than one edges file for the region 409 SD-N113.
  4. Every edges file in the region: SeedsProtocol < seeds.RequiredProtocol, or SeedsFloorMinRelease != floor.MinRelease, or neither PrevRelease == "none" nor PrevSeedsAllowedFor == floor.MinRelease: 409 SD-N2.
  5. The heartbeats (systemBoxes, joined by label: an edges file's box is metricsBoxLabel, the same status.Label the heartbeat carries): a region box with no heartbeat, a stale one, or one with no Metrics.Layout: 503 SD-N5. Metrics.Layout.Mode != "nested": 409 SD-N6. Metrics.HostClaimEnabled: 409 SD-N7.

oneBox is for the import door. The swap, its journal and the rollback live on the one box that holds the legacy row (Plan 7), and §3.2's no-journal finish of an aborted seed would run on every other box of a multi-box region, where it could CAS an aborted import to rolled-back (and let the console delete the record, freeing the hostname) while the swapping box is still rolling back. The spec does not address multi-box regions; Tito's conversion runs on one box. Until a multi-box design exists, the import door refuses a region with more than one box.

  • [ ] Step 1: Write the failing tests
package console

import (
	"context"
	"net/http"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/carlosframework/platform/internal/seeds"
	"github.com/carlosframework/platform/internal/status"
)

func seedReadyBox(t *testing.T, fx *consoleFixture, label, placement string, protocol int, floor, prevAllowed, mode string, claim bool) {
	t.Helper()
	putEdges(t, fx, map[string]string{label: `{"v":1,"box":"` + label + `","eip":"1.2.3.4","placement":"` + placement +
		`","hibernation_configured":true,"hosts":{},"seeds_protocol":` + strconv.Itoa(protocol) +
		`,"seeds_floor_min_release":"` + floor + `","prev_release":"v0.27.1","prev_seeds_allowed_for":"` + prevAllowed + `"}`})
	var layout *status.LayoutReport
	if mode != "" {
		layout = &status.LayoutReport{Mode: mode}
	}
	b := status.CollectBox(nil, status.Probes{Layout: layout, HostClaimEnabled: claim}, time.Now())
	b.Label = label
	if err := status.WriteBox(context.Background(), fx.rel, b); err != nil {
		t.Fatal(err)
	}
}

func publishSeedsFloorFx(t *testing.T, fx *consoleFixture, rel string) {
	t.Helper()
	if err := seeds.PublishFloor(context.Background(), fx.rel, seeds.Floor{Min: seeds.RequiredProtocol, MinRelease: rel}); err != nil {
		t.Fatal(err)
	}
}

func TestSeedRegionReady(t *testing.T) {
	fx := newConsoleFixture(t)
	ctx := context.Background()
	if ref := fx.srv.seedRegionReady(ctx, "", false); ref == nil || ref.Status != http.StatusConflict || !strings.Contains(ref.Msg, "no seeds floor yet") {
		t.Fatalf("no floor: %+v", ref)
	}
	publishSeedsFloorFx(t, fx, "v0.27.0")
	seedReadyBox(t, fx, "kass-1", "", 1, "v0.27.0", "v0.27.0", "nested", false)
	if ref := fx.srv.seedRegionReady(ctx, "", false); ref != nil {
		t.Fatalf("a ready region: %+v", ref)
	}
	seedReadyBox(t, fx, "kass-2", "", 0, "v0.27.0", "v0.27.0", "nested", false)
	if ref := fx.srv.seedRegionReady(ctx, "", false); ref == nil || ref.Status != http.StatusConflict || !strings.Contains(ref.Msg, "kass-2") || !strings.Contains(ref.Msg, "below the seeds floor") {
		t.Fatalf("a box on protocol 0: %+v", ref)
	}
	seedReadyBox(t, fx, "kass-2", "", 1, "v0.27.0", "", "nested", false)
	if ref := fx.srv.seedRegionReady(ctx, "", false); ref == nil || !strings.Contains(ref.Msg, "below the seeds floor") {
		t.Fatalf("a carlos.prev without a seeds verdict: %+v", ref)
	}
	seedReadyBox(t, fx, "kass-2", "", 1, "v0.27.0", "v0.27.0", "flat", false)
	if ref := fx.srv.seedRegionReady(ctx, "", false); ref == nil || !strings.Contains(ref.Msg, "nested") {
		t.Fatalf("a flat box: %+v", ref)
	}
	seedReadyBox(t, fx, "kass-2", "", 1, "v0.27.0", "v0.27.0", "nested", true)
	if ref := fx.srv.seedRegionReady(ctx, "", false); ref == nil || !strings.Contains(ref.Msg, "claim control") {
		t.Fatalf("a claim-armed box: %+v", ref)
	}
	fx.srv.cfg.HostClaim = true
	if ref := fx.srv.seedRegionReady(ctx, "", false); ref == nil || !strings.Contains(ref.Msg, "This console runs with claim control on") {
		t.Fatalf("a claim console: %+v", ref)
	}
}

func TestSeedRegionReadyOneBoxForImports(t *testing.T) {
	fx := newConsoleFixture(t)
	publishSeedsFloorFx(t, fx, "v0.27.0")
	seedReadyBox(t, fx, "kass-1", "", 1, "v0.27.0", "v0.27.0", "nested", false)
	seedReadyBox(t, fx, "kass-2", "", 1, "v0.27.0", "v0.27.0", "nested", false)
	if ref := fx.srv.seedRegionReady(context.Background(), "", false); ref != nil {
		t.Fatalf("two ready boxes, --from: %+v", ref)
	}
	if ref := fx.srv.seedRegionReady(context.Background(), "", true); ref == nil || ref.Status != http.StatusConflict || !strings.Contains(ref.Msg, "more than one box") {
		t.Fatalf("two ready boxes, import: %+v", ref)
	}
}

func TestSeedRegionReadyRefusesAnUnconfirmedBox(t *testing.T) {
	fx := newConsoleFixture(t)
	publishSeedsFloorFx(t, fx, "v0.27.0")
	seedReadyBox(t, fx, "kass-1", "", 1, "v0.27.0", "v0.27.0", "", false) // no layout report
	if ref := fx.srv.seedRegionReady(context.Background(), "", false); ref == nil || ref.Status != http.StatusServiceUnavailable {
		t.Fatalf("a box with no layout report: %+v", ref)
	}
}

func TestSeedRegionReadyIgnoresOtherRegions(t *testing.T) {
	fx := newConsoleFixture(t)
	publishSeedsFloorFx(t, fx, "v0.27.0")
	seedReadyBox(t, fx, "kass-1", "", 1, "v0.27.0", "v0.27.0", "nested", false)
	seedReadyBox(t, fx, "uk-1", "uk", 0, "", "", "flat", true)
	if ref := fx.srv.seedRegionReady(context.Background(), "", false); ref != nil {
		t.Fatalf("a bad box in uk blocked the home region: %+v", ref)
	}
	if ref := fx.srv.seedRegionReady(context.Background(), "uk", false); ref == nil {
		t.Fatal("uk's own bad box did not block uk")
	}
}

The fixture's home region: newConsoleFixture leaves cfg.HomeRegion empty, so homeRegionIn("") is "" and the box with placement "" is in the home region. If the fixture sets a home region, give the home boxes that placement instead.

  • [ ] Step 2: Run the tests to verify they fail

Run: go test ./internal/console/ -run 'TestSeedRegionReady' -count=1 Expected: FAIL to compile: fx.srv.seedRegionReady undefined.

  • [ ] Step 3: Write internal/console/seedfloor.go
package console

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"sort"
	"strings"
	"time"

	"github.com/carlosframework/platform/internal/seeds"
	"github.com/carlosframework/platform/internal/status"
)

// seedRegionReady is the check both seed doors make before writing any seed
// state, even awaiting-upload (seeded-instances spec §3.3, §6.3): this
// console runs without claim control, the seeds floor is published, and
// every box in region has armed it, excluded or vouched for its
// carlos.prev, runs nested, and has no claim control. region is the record
// vocabulary ("" is the home region). nil means go ahead.
func (s *Server) seedRegionReady(ctx context.Context, region string, oneBox bool) *doorRefusal {
	if s.cfg.HostClaim {
		return &doorRefusal{http.StatusConflict, "This console runs with claim control on. Seeded instances are not supported here yet."}
	}
	floor, err := seeds.ReadFloor(ctx, s.cfg.Releases)
	if err != nil {
		log.Printf("console: seeds floor: %v", err)
		return &doorRefusal{http.StatusServiceUnavailable, "could not read the seeds floor. Try again."}
	}
	if floor == nil {
		return &doorRefusal{http.StatusConflict, "This deployment has no seeds floor yet. An operator publishes it with carlos system seeds-floor publish."}
	}
	name := s.homeRegionOut(region)
	if name == "" {
		name = "the home region"
	}
	unconfirmed := &doorRefusal{http.StatusServiceUnavailable, fmt.Sprintf("could not confirm every box in %s. Try again in a minute.", name)}
	snap, err := s.liveEdges(ctx)
	if err != nil || !snap.complete() {
		return unconfirmed
	}
	var inRegion, below []string
	for _, f := range snap.Files {
		if s.homeRegionIn(f.Placement) != region {
			continue
		}
		inRegion = append(inRegion, f.Box)
		prevOK := f.PrevRelease == "none" || f.PrevSeedsAllowedFor == floor.MinRelease
		if f.SeedsProtocol < seeds.RequiredProtocol || f.SeedsFloorMinRelease != floor.MinRelease || !prevOK {
			below = append(below, f.Box)
		}
	}
	if len(inRegion) == 0 {
		return unconfirmed
	}
	if oneBox && len(inRegion) > 1 {
		return &doorRefusal{http.StatusConflict, fmt.Sprintf("%s has more than one box. An import needs a region with one box for now.", name)}
	}
	if len(below) > 0 {
		return &doorRefusal{http.StatusConflict, fmt.Sprintf("%s in %s are below the seeds floor. Roll the platform there, then publish the seeds floor again.", boxList(below), name)}
	}
	boxes, _, err := s.systemBoxes(ctx, false)
	if err != nil {
		log.Printf("console: seeds readiness: boxes: %v", err)
		return unconfirmed
	}
	byLabel := map[string]status.Box{}
	for _, b := range boxes {
		byLabel[b.Label] = b
	}
	now := time.Now()
	var flat, claimed []string
	for _, label := range inRegion {
		b, ok := byLabel[label]
		if !ok || status.BoxStale(b, now, status.SnapshotPeriod) || b.Metrics == nil || b.Metrics.Layout == nil {
			return unconfirmed
		}
		if b.Metrics.Layout.Mode != "nested" {
			flat = append(flat, label)
		}
		if b.Metrics.HostClaimEnabled {
			claimed = append(claimed, label)
		}
	}
	if len(flat) > 0 {
		return &doorRefusal{http.StatusConflict, fmt.Sprintf("%s in %s do not run instances nested yet. A seeded instance needs every box there nested.", boxList(flat), name)}
	}
	if len(claimed) > 0 {
		return &doorRefusal{http.StatusConflict, fmt.Sprintf("%s in %s run with claim control on. Seeded instances are not supported there yet.", boxList(claimed), name)}
	}
	return nil
}

func boxList(boxes []string) string {
	sort.Strings(boxes)
	return strings.Join(boxes, ", ")
}

"the home region" matches dbRegionName's existing wording (apidb.go), which the deck reuses for <region>.

  • [ ] Step 4: Run the tests to verify they pass

Run: go test ./internal/console/ -run 'TestSeedRegionReady' -count=1 Expected: PASS.

  • [ ] Step 5: Wire SD-N2 to SD-N8 and commit

Append "SD-N2", "SD-N3", "SD-N4", "SD-N5", "SD-N6", "SD-N7", "SD-N8", "SD-N113" to seededWiredIDs; run go test . -run TestSeededWiredCopyIsApplied -count=1 (PASS).

git add internal/console/seedfloor.go internal/console/seedfloor_test.go approved_seeded_copy_test.go
git commit -m "console: seedRegionReady, the floor, layout and claim check before any seed state (Plan 3 Task 5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"

Task 6: carlos system seeds-floor publish

Files:

  • Modify: internal/console/seedfloor.go (add handleAPISystemSeedsFloor)
  • Modify: internal/console/authcli.go:932 (one route, beside the geoip doors)
  • Create: system_seeds.go, system_seeds_test.go
  • Modify: system.go:52 (systemVerb), :58 (runSystem), :107 (systemUsage)
  • Modify: docs/site/cli.md (under ### carlos system, :1863)
  • Test: internal/console/seedfloor_test.go

Interfaces:

  • Consumes: s.apiSystemRoller(w, r) (addr string, owner, ok bool), s.auditSystem(addr, action, detail string), readJSON, jsonError, writeJSON, seeds.PublishFloor, seeds.FloorLowerError; CLI consoleOption, requireDeployAPISession, printUsing, sess.postJSON, apiErrorFromBody.
  • Produces: POST /api/cli/system/seeds-floor {"min_release": "vX.Y.Z"} answering 200 {"min_release": "vX.Y.Z"}; runSystemSeedsFloorVerb(rest []string).
  • [ ] Step 1: Write the failing tests

Append to internal/console/seedfloor_test.go:

func TestSeedsFloorDoorIsForSystemOperators(t *testing.T) {
	fx := newConsoleFixture(t)
	if err := fx.st.AddMember(OpsAccountID, "member@keymail.dev", RoleMember, time.Now()); err != nil {
		t.Fatal(err)
	}
	member := cliToken(t, fx, "member@keymail.dev", "code-seeds-member")
	_, want := getRaw(t, fx.ts.URL+"/api/cli/system/no-such-door", member)
	if st, body := postRaw(t, fx.ts.URL+"/api/cli/system/seeds-floor", map[string]string{"min_release": "v0.27.0"}, member); st != http.StatusNotFound || body != want {
		t.Fatalf("a member = %d %q, want the bare 404", st, body)
	}
	owner := cliToken(t, fx, "paul@keymail.dev", "code-seeds-owner")
	if st, out := postJSON(t, fx.ts.URL+"/api/cli/system/seeds-floor", map[string]any{"min_release": "latest"}, owner); st != http.StatusBadRequest {
		t.Fatalf("a bad release = %d %v, want 400", st, out)
	}
	if st, out := postJSON(t, fx.ts.URL+"/api/cli/system/seeds-floor", map[string]any{"min_release": "v0.27.0"}, owner); st != http.StatusOK || out["min_release"] != "v0.27.0" {
		t.Fatalf("publish = %d %v", st, out)
	}
	st, out := postJSON(t, fx.ts.URL+"/api/cli/system/seeds-floor", map[string]any{"min_release": "v0.26.0"}, owner)
	if st != http.StatusConflict || !strings.Contains(str(t, out, "error"), "already v0.27.0") {
		t.Fatalf("a lower floor = %d %v, want 409 naming v0.27.0", st, out)
	}
	f, err := seeds.ReadFloor(context.Background(), fx.rel)
	if err != nil || f == nil || f.MinRelease != "v0.27.0" || f.Min != seeds.RequiredProtocol {
		t.Fatalf("floor = %+v, %v", f, err)
	}
}

system_seeds_test.go:

package main

import "testing"

func TestSystemVerbKnowsSeedsFloor(t *testing.T) {
	verb, rest, _ := systemVerb([]string{"seeds-floor", "publish", "--min-release", "v0.27.0"})
	if verb != "seeds-floor" || len(rest) != 3 || rest[0] != "publish" {
		t.Fatalf("systemVerb = %q %q", verb, rest)
	}
}
  • [ ] Step 2: Run the tests to verify they fail

Run: go test ./internal/console/ -run TestSeedsFloorDoorIsForSystemOperators -count=1; go test . -run TestSystemVerbKnowsSeedsFloor -count=1 Expected: FAIL: the route 404s for the owner too; systemVerb returns "".

  • [ ] Step 3: Write the door and register it

Append to internal/console/seedfloor.go (add "errors" to its imports):

// handleAPISystemSeedsFloor publishes the seeds floor (spec §3.3, §10 step
// 2). The update doors' authority: an ops account owner or an address on
// CARLOS_SYSTEM_OPERATORS (apiSystemRoller).
func (s *Server) handleAPISystemSeedsFloor(w http.ResponseWriter, r *http.Request) {
	addr, _, ok := s.apiSystemRoller(w, r)
	if !ok {
		return
	}
	var req struct {
		MinRelease string `json:"min_release"`
	}
	if !readJSON(w, r, &req) {
		return
	}
	if ok, err := seeds.ReleaseAtOrAbove(req.MinRelease, req.MinRelease); !ok || err != nil {
		jsonError(w, http.StatusBadRequest, "min_release must be a release such as v0.27.0")
		return
	}
	err := seeds.PublishFloor(r.Context(), s.cfg.Releases, seeds.Floor{Min: seeds.RequiredProtocol, MinRelease: req.MinRelease})
	var lower *seeds.FloorLowerError
	switch {
	case errors.As(err, &lower):
		jsonError(w, http.StatusConflict, fmt.Sprintf("the seeds floor is already %s. A floor only rises.", lower.Current))
		return
	case err != nil:
		log.Printf("console: publish seeds floor: %v", err)
		jsonError(w, http.StatusBadGateway, "could not publish the seeds floor. Try again.")
		return
	}
	s.auditSystem(addr, "seeds-floor-publish", fmt.Sprintf("published the seeds floor at %s", req.MinRelease))
	writeJSON(w, http.StatusOK, map[string]string{"min_release": req.MinRelease})
}

In authcli.go, after the three geoip routes:

	// The seeds floor (seeded-instances spec §3.3): apiSystemRoller.
	s.cliRoute("POST /api/cli/system/seeds-floor", svcrole.Set{}, s.handleAPISystemSeedsFloor)
  • [ ] Step 4: Write the CLI verb

system_seeds.go:

package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"net/http"
	"os"
)

// runSystemSeedsFloorVerb is `carlos system seeds-floor publish`
// (seeded-instances spec §3.3, §10 step 2).
func runSystemSeedsFloorVerb(rest []string) {
	if len(rest) == 0 || rest[0] != "publish" {
		systemUsage()
		os.Exit(2)
	}
	fs := flag.NewFlagSet("system seeds-floor publish", flag.ExitOnError)
	consoleFlag := consoleOption(fs, "console origin (default: the only logged-in one)")
	minRelease := fs.String("min-release", "", "the first release whose boxes can place a seed, such as v0.27.0")
	fs.Parse(rest[1:])
	if *minRelease == "" || fs.NArg() != 0 {
		fs.Usage()
		os.Exit(2)
	}
	sess := requireDeployAPISession(*consoleFlag)
	printUsing(stdout, sess)
	status, body, err := sess.postJSON(context.Background(), "/api/cli/system/seeds-floor", map[string]string{"min_release": *minRelease})
	if err != nil {
		log.Fatalf("carlos system seeds-floor publish: %v", err)
	}
	if status != http.StatusOK {
		log.Fatalf("carlos system seeds-floor publish: %v", apiErrorFromBody(status, body))
	}
	fmt.Fprintln(stdout, fmt.Sprintf("published the seeds floor at %s. Every box arms it within the hour.", *minRelease))
}

In system.go: add "seeds-floor" to systemVerb's case list (:52); add case "seeds-floor": runSystemSeedsFloorVerb(rest) to runSystem's switch beside case "geoip"; in systemUsage, add carlos system seeds-floor publish --min-release <release> to the usage lines at the top, and under "The platform's own binary (ops account owners and CARLOSSYSTEMOPERATORS):" add:

  seeds-floor publish --min-release <release>
            the first release whose boxes can place a seed, such as v0.27.0

In docs/site/cli.md, under ### carlos system, add a #### The seeds floor subsection whose body is SD-N111's approved text, followed by the example:

````markdown

carlos system seeds-floor publish --min-release v0.27.0

````

  • [ ] Step 5: Run the tests to verify they pass

Run: go test ./internal/console/ -run 'TestSeedsFloorDoor|TestSystemDoors' -count=1 && go test . -run 'TestSystemVerb|TestSeeded' -count=1 Expected: PASS.

  • [ ] Step 6: Wire SD-N9 to SD-N13, SD-N111, SD-N112 and commit

Append "SD-N9", "SD-N10", "SD-N11", "SD-N12", "SD-N13", "SD-N111", "SD-N112" to seededWiredIDs; run go test . -run TestSeededWiredCopyIsApplied -count=1 (PASS).

git add internal/console/seedfloor.go internal/console/seedfloor_test.go internal/console/authcli.go system.go system_seeds.go system_seeds_test.go docs/site/cli.md approved_seeded_copy_test.go
git commit -m "carlos system seeds-floor publish (Plan 3 Task 6)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"

Task 7: Plan gate

  • [ ] Step 1: Cross-compile and run the touched packages under -race

Run:

GOOS=darwin go build ./... && GOOS=windows go build ./... && GOOS=linux GOARCH=arm64 go build ./... && \
nice go test -race ./internal/seeds/ ./internal/host/ ./internal/console/ . -count=1

Expected: builds clean; tests PASS. The moves copy gate (go test . -run TestMoves) passes: no new file imports internal/moves, and the changed moves-scanned files (movesfloor.go, systemapply.go, edgesfile.go, instancesplacement.go, authcli.go) gained no sink.

---

Self-review

  • Spec coverage: §3.3 the floor object (Task 1), the box arming and the carlos.prev verdict (Task 2), recovery refusing below the floor in the units and in Go (Task 3), the console writing no seed state until every box reports armed and the verdict (Task 5, used by Plans 5 and 6); §6.3 console half, nested and not claim-armed (Task 5); §10 step 2's publish (Task 6).
  • Dark launch: ProtocolVersion 0 against RequiredProtocol 1 (Task 1's test pins it until Plan 7).
  • Review Focus: line 1 Task 2, line 2 Task 3, lines 3 and 4 Task 5, line 5 Task 1.
  • Deviation: oneBox (SD-N113) is not in the spec; it closes a multi-box gap the spec does not address (Task 5's note).