carlos / platform Public

plan: claim Plan 8b rev 1, the floor, the roster, the census and live lost-box wiring

Release identity and durable floor policy, the boxid-admitted signed
roster with census and reservations, root arming and pre-exec checks, the
regional termination-evidence producer, and operator resolution and
readmission wired to the real roster. Drafted against claim rev 6.27.
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
codex agent, operated by paul@keymail.dev pushed by claude agent, operated by paul@keymail.dev c3923c640437303dcc7baf6ba0a6b719d3d5f226 parent 73b1b9d
1 file changed, +5016 −0
  • docs/superpowers/plans/2026-10-06-host-lifecycle-claim-8b-floor-roster.md +5016 −0

CI passed — run details

diff --git a/docs/superpowers/plans/2026-10-06-host-lifecycle-claim-8b-floor-roster.md b/docs/superpowers/plans/2026-10-06-host-lifecycle-claim-8b-floor-roster.md
new file mode 100644
index 000000000..5e3f6ab3c
--- /dev/null
+++ b/docs/superpowers/plans/2026-10-06-host-lifecycle-claim-8b-floor-roster.md
@@ -0,0 +1,5016 @@
+# Host lifecycle claim, Plan 8b: the floor, the roster and live fencing
+
+Implementation plan, 2026-10-06. Branch to implement: `feat/hostclaim-floor`,
+from `feat/hostclaim-leases` at
+`ca5a79d8a3821172f5829231014969f106bc33e9`. This document is the deliverable.
+Do not implement it in the design worktree. Authoring this plan creates no
+implementation branch, stages nothing and commits nothing.
+
+The floor is the **first fully integrated tagged release**, as Paul ruled in
+claim rev 6.27 §1.1 P1. It includes the finished claim and moves integrations,
+box identity, this census, and guarded boot and recovery. This branch is not
+that release by itself. No release tag in a test is an activation choice.
+
+## Sources and boundaries
+
+- Claim rev 6.27, in this worktree:
+ `docs/superpowers/specs/2026-10-02-host-lifecycle-claim-design.md`.
+ §1.1 takes precedence over the older wording in §7.3 and §10.1. Their
+ surviving references to awaiting rulings are history, not a reason to keep
+ Plan 8's placeholder refusals.
+- The Plan 8 implementation is the pinned parent above, not the earlier code
+ printed in its plan. In particular, consume `hostexec.Open`,
+ `hostquarantine.ActorDirectory`, `hostclaim.DeclareLost`,
+ `hostclaim.Readmit`, signed loss records, and `hostfence`'s challenge and
+ exact observation validation as implemented there.
+- Region HA spec 1 rev 5.4 is
+ `origin/design-region-ha-rev5` at
+ `0c8ad11baa87c35bfca264c65fabfc67b9b178a4`:
+ `docs/superpowers/specs/2026-10-04-region-ha-box-placement-design.md`.
+ Read §5.1, §5.2, §5.9, §5.10 and the complete L1-L20 table in §9.
+- `internal/boxid` and its embedded regional certificates are already in the
+ parent. Keep `control/boxes/<reference>.json` as create-only identity
+ evidence. It grants no actor membership. The label remains the opaque box
+ reference; EC2 instance IDs stay in private roster/report data.
+- Plans 5 and 6 own lifecycle callers, returned-box retirement, the local
+ guard and the complete cold-admission integration. This plan supplies their
+ concrete directory, census/reservation APIs, and root update exclusion.
+ It does not invent an operation owner for moves or activate an unfinished
+ caller. `choose()` remains the ruled P5-D2 choice: prefer no instance of the
+ app, then fewest claimed hosts, then SHA-256 of `host + "\x00" + box`.
+ Store that selection once; neither loss handling nor retries choose again.
+
+Read the sources locally:
+
+```sh
+git show ca5a79d8a3821172f5829231014969f106bc33e9:internal/hostquarantine/quarantine.go
+git show ca5a79d8a3821172f5829231014969f106bc33e9:internal/hostexec/controller.go
+git show 0c8ad11baa87c35bfca264c65fabfc67b9b178a4:docs/superpowers/specs/2026-10-04-region-ha-box-placement-design.md
+```
+
+### What the unadopted region HA requests do not authorize
+
+| Requests | Work here | Boundary until adopted |
+| --- | --- | --- |
+| L1, L9-L13, L16-L18 | Preserve existing claim kinds, admission provenance and owner checks. | No `relocating` operation, `released` coverage, files publisher, new drain protocol or relaxation of park-generation barriers. |
+| L2, L3, L4 | P2 permits a region worker to persist exact terminated evidence without the console. Implement the worker and a separate destroyed-local predicate. | No invented quarantine revision, operator attestation or root signature on worker evidence. L4's zero-revision claim representation and L3's operation-specific discharge CAS still need adoption. Existing owner transitions must consume the evidence under their amended contract before recovery is enabled. |
+| L5, L20 | Ordinary D7 census is complete and fresh; independent consoles are members. | No exclusion solely on a worker fence, no inferred console-to-box `compute` binding, and no stale-console exception in ordinary starts. H18's exception is only for future box-started relocation after L20, never a general freshness relaxation. |
+| L6 | Build H14's read-only activation check and both sides of the durable pending/update-begun interlock. | A future box-started owner records `directory_revision` and uses this interlock. It never gets write authority to the roster. No new box-started operation is introduced here. |
+| L7, L19 | P3 evidence-based roster enrollment; H16/H17 membership-confirmation consumer and readiness invitation. | Spec 4 owns the ASG confirmer/producer and its automatic enrollment loop. Cross-region confirmation must positively account for every active or quarantined target-region enrollment. It is not a termination fence. |
+| L8, L14, L15 | Preserve the once-recorded executor and the existing operation-owner seam. | Do not substitute a target, weaken moves' commit boundary, or implement source-loss recovery in this plan. |
+
+H19 is operative for the evidence producer: a verified `terminated` read
+alone is sufficient. Disk evidence must never gate fencing, census readiness,
+admission or recovery. The volume-history predicate is used only by the
+owner of an exact `park-local` discharge. Unknown or incomplete history leaves
+that chore open. In particular, an empty terminated reply's mappings prove
+nothing about disks.
+
+## Invariants and implementation decisions
+
+1. `control/host-actors.json` is the only membership authority. It is signed
+ with the deployment root and replaced by ETag. A heartbeat, edges file,
+ invitation or membership confirmation grants no authority by itself.
+ Every directory lookup rereads and verifies that exact object. There is
+ no admission cache and no heartbeat-based retirement.
+2. Enrollment verifies the AWS-signed document against embedded certificates,
+ deployment account and configured AWS region. It scans existing claim and
+ edges references before accepting a new label binding. Existing references
+ require the operator-approved tofu instance ID; the automatic route cannot
+ bypass that check. Cloud identity and enrollment never change in place.
+3. A signed invitation supplies the random enrollment nonce before a new box
+ becomes a member. That box can arm its root guard and report readiness
+ while admission still refuses it. This breaks the post-floor enrollment
+ cycle without creating provisional execution authority.
+4. The roster has additive `regions`, `publication` and `floor_sha256` fields.
+ The initial publication CAS freezes membership at `preparing`; failed
+ readiness checks leave it frozen. Publication writes the authentication
+ companion first and the floor second, both create-only, then completes the
+ roster state. Starts require `published`. Later enrollment and reactivation
+ must pass the same authenticated floor/image checks before the roster CAS.
+5. A release build string is never ordered. Tags are canonical
+ `vMAJOR.MINOR.PATCH` and use `release.ParseSemVer`/`Compare`. Signed immutable
+ snapshots bind tag, actual build, protocol, platform and hash. Local
+ uploads can only reuse an approved identity whose bytes match.
+6. The console is built separately under `cmd/console`. Add optional signed
+ `SystemRelease.ConsoleArtifacts` beside router artifacts, using keys
+ `bootstrap/console/<version>/carlos-<platform>`. This is an additive encoding
+ of D1's actual-image requirement, not a second floor. A console report
+ names component `console` and must match that component's hash. A router
+ hash never certifies a console. The release approval workflow must approve
+ and retain both sets of artifacts in the immutable snapshot.
+7. Reports hash `/proc/self/exe`, not a pathname subsequently replaced on
+ disk. They include boot and enrollment identity. Every required private
+ report is at most 15 minutes old and at most five seconds in the future.
+ The local console process is checked directly too. Unsupported platforms
+ refuse protected image execution, but all packages must cross-compile.
+8. Lifecycle and executable reservations are root-signed roster entries with
+ exact enrollment references and authenticated hashes. Neither expires.
+ An uncertain start is cancelled by first invalidating its expected claim
+ ETag, including an absent claim's create-only position. The start CAS
+ writes `actors_hold`. A first-phase participant checks the hold before
+ its acknowledgement. No lease PUT or effect may substitute for that check.
+9. The root checker, policy, identities and unit drop-ins live outside
+ `system/router`'s restore set. `armed` already enforces the prospective
+ floor. Removing the lifecycle flag or losing the network cannot undo it.
+ The checker opens a protected file and executes that same descriptor.
+ Neither a shell fallback nor `version` inspection can run unverified bytes.
+10. Root writes update-begun before inspecting the complete controller
+ directory. Any entry, including `lost` or malformed state, refuses the
+ update. Controllers persist `pending` before checking root's marker and
+ journal. Only D6's completed cleanup clears controller exclusion.
+11. Operator loss resolution still requires a person and `Store.IsOperator`.
+ Fresh inspection binds exact claim, record, enrollment, operation and
+ consequences. Compute is observed by the operator CLI, not the console.
+ Global quarantine precedes claim mutation, partial failure keeps it, and
+ readmission clears it last by the original roster ETag. Unknown timers
+ remain unknown. A successful retry retains the original accepted fence.
+12. Only operation owners present on the parent are wired: delete/parking and
+ serving's exact destroyed-local-only case. Moving, landing, reverting,
+ pushing and other unsupported owners refuse. This is an operation support
+ refusal, not the removed P2/P3 placeholder. Moves' actual owner must keep
+ its source-loss refusal and record-commit CAS restrictions when integrated.
+
+## Before starting implementation
+
+The code below is pinned to the parent. Each task prints every added file
+and an exact zero-context patch for existing files. Save patches outside the
+repository and apply them with `git apply --unidiff-zero`. Do not substitute
+ellipses or prose for any body. Tests go in first, before their implementation.
+A new-package red result may be a missing symbol at compile time; after the
+implementation, the refusal cases must exercise their behavioral assertions.
+
+```sh
+git switch -c feat/hostclaim-floor ca5a79d8a3821172f5829231014969f106bc33e9
+```
+
+Use the default Go cache. Keep scratch under `$TMPDIR` or the shared disk
+cache root and remove only scratch created by this work. Do not change IAM,
+create queues, publish a floor, set the production flag or arm production
+units while implementing this branch.
+
+## Task 1: Authenticated release identity and durable floor policy
+
+Add the two requested SystemRelease fields, the separate console artifact mapping, immutable signed identity snapshots, the single floor and its authentication companion, actual-image reporting, durable policy storage and descriptor-based execution. Legacy system pointers still parse. The low-level floor publisher is only called through Task 2's complete census in the approved release workflow.
+
+**Step 1: add the failing tests.**
+
+### Create `internal/hostfloor/floor_test.go`
+
+```go
+package hostfloor
+
+import (
+ "context"
+ "crypto/ed25519"
+ "crypto/rand"
+ "encoding/json"
+ "github.com/carlosframework/platform/internal/release"
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+func TestFloorPairAndPermanentArming(t *testing.T) {
+ pub, key, _ := ed25519.GenerateKey(rand.Reader)
+ sg := release.RootSigner(key)
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ floor := Floor{1, "v1.2.3"}
+ raw, _ := json.Marshal(floor)
+ auth, _ := release.SignRootEnvelope(sg, Authorization{1, "moves-protocol", "bucket", Key, Digest(raw)})
+ if _, err := Verify(pub, "other", raw, auth); err == nil {
+ t.Fatal("wrong deployment")
+ }
+ if _, err := Verify(pub, "bucket", append(raw, ' '), auth); err == nil {
+ t.Fatal("changed bytes")
+ }
+ if _, err := s.Put(ctx, AuthKey, auth, ""); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := Read(ctx, s, pub, "bucket"); err == nil {
+ t.Fatal("incomplete pair accepted")
+ }
+ if err := Publish(ctx, s, sg, "bucket", floor); err != nil {
+ t.Fatal(err)
+ }
+ d := Disk{Dir: t.TempDir(), Bucket: "bucket", PublicKey: pub}
+ _ = os.Mkdir(filepath.Join(d.Dir, "identities"), 0700)
+ initial, _ := json.Marshal(Policy{Schema: 1, DeploymentBucket: "bucket", Mode: "unpublished"})
+ _ = os.WriteFile(filepath.Join(d.Dir, "policy.json"), initial, 0600)
+ armed := Policy{1, "bucket", "armed", raw, auth}
+ if err := d.Persist(armed, nil); err != nil {
+ t.Fatal(err)
+ }
+ if err := d.Persist(Policy{Schema: 1, DeploymentBucket: "bucket", Mode: "unpublished"}, nil); err == nil {
+ t.Fatal("floor lowered")
+ }
+ _ = os.Remove(filepath.Join(d.Dir, "policy.json"))
+ if _, _, err := d.Load(); err == nil {
+ t.Fatal("missing is unpublished")
+ }
+}
+func TestPendingAndLostControllersBlockUpdate(t *testing.T) {
+ for _, state := range []string{"pending", "active", "lost", "broken"} {
+ t.Run(state, func(t *testing.T) {
+ d := Disk{Dir: t.TempDir()}
+ data := t.TempDir()
+ _ = os.Mkdir(filepath.Join(data, ".hostexec"), 0700)
+ _ = os.WriteFile(filepath.Join(data, ".hostexec", "host.json"), []byte(state), 0600)
+ if err := d.Begin("update", data); err == nil {
+ t.Fatal("controller did not block")
+ }
+ if _, err := os.Stat(filepath.Join(d.Dir, "update-begun.json")); !os.IsNotExist(err) {
+ t.Fatal("marker survived refusal", err)
+ }
+ })
+ }
+ d := Disk{Dir: t.TempDir()}
+ data := t.TempDir()
+ _ = os.Mkdir(filepath.Join(data, ".hostexec"), 0700)
+ if err := d.Begin("update", data); err != nil {
+ t.Fatal(err)
+ }
+ if err := UpdateIdle(d.Dir, filepath.Join(data, "journal.json")); err == nil {
+ t.Fatal("participant ignored begun update")
+ }
+}
+func TestCanonicalReleaseTags(t *testing.T) {
+ for _, tag := range []string{"dev", "abc123", "1.2.3", "vv1.2.3", "v01.2.3", "v1.2.3-rc1"} {
+ if _, ok := release.ReleaseTag(tag); ok {
+ t.Fatal(tag)
+ }
+ }
+ a, _ := release.ReleaseTag("v1.10.0")
+ b, _ := release.ReleaseTag("v1.9.9")
+ if a.Compare(b) <= 0 {
+ t.Fatal("lexical ordering")
+ }
+}
+```
+
+Run this now and expect failure before the implementation is present:
+
+```sh
+go test ./internal/hostfloor -run 'TestFloorPair|TestPendingAndLost|TestCanonicalRelease' -count=1
+```
+
+**Step 2: add the implementation.**
+
+### Create `internal/release/systemidentity.go`
+
+```go
+package release
+
+import (
+ "bytes"
+ "context"
+ "crypto/ed25519"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "os"
+ "reflect"
+ "strings"
+)
+
+func ReleaseTag(tag string) (SemVer, bool) {
+ if !strings.HasPrefix(tag, "v") {
+ return SemVer{}, false
+ }
+ return ParseSemVer(strings.TrimPrefix(tag, "v"))
+}
+
+func VerifySystemIdentity(pub ed25519.PublicKey, wire []byte) (*SystemPointer, error) {
+ var wrapper signedWrapper
+ if json.Unmarshal(wire, &wrapper) != nil {
+ return nil, ErrBadSystemPointer
+ }
+ raw, err := base64.StdEncoding.DecodeString(wrapper.Payload)
+ if err != nil {
+ return nil, ErrBadSystemPointer
+ }
+ var claimed SystemPointer
+ if json.Unmarshal(raw, &claimed) != nil {
+ return nil, ErrBadSystemPointer
+ }
+ p, err := verifySystemPointer(pub, wire, SystemComponentRouter, claimed.Channel)
+ if err != nil {
+ return nil, err
+ }
+ if _, ok := ReleaseTag(p.Current.ReleaseTag); !ok || p.Current.MovesProtocol < 1 {
+ return nil, ErrBadSystemPointer
+ }
+ return p, nil
+}
+
+func IdentityKey(tag string) (string, error) {
+ if _, ok := ReleaseTag(tag); !ok {
+ return "", ErrBadSystemPointer
+ }
+ return "control/system/router/releases/" + tag + ".json", nil
+}
+
+// PublishSystemIdentity accepts a release-workflow signed pointer, never an
+// upload request's assertion of its own tag.
+func PublishSystemIdentity(ctx context.Context, s Store, pub ed25519.PublicKey, wire []byte) error {
+ p, err := VerifySystemIdentity(pub, wire)
+ if err != nil {
+ return err
+ }
+ key, err := IdentityKey(p.Current.ReleaseTag)
+ if err != nil {
+ return err
+ }
+ _, err = s.Put(ctx, key, wire, "")
+ if !errors.Is(err, ErrCASConflict) {
+ return err
+ }
+ old, _, err := s.Get(ctx, key)
+ if err != nil {
+ return err
+ }
+ prev, err := VerifySystemIdentity(pub, old)
+ if err != nil || !reflect.DeepEqual(prev.Current, p.Current) {
+ return ErrCASConflict
+ }
+ return nil
+}
+
+func GetSystemIdentity(ctx context.Context, s Store, pub ed25519.PublicKey, tag string) (*SystemPointer, error) {
+ key, err := IdentityKey(tag)
+ if err != nil {
+ return nil, err
+ }
+ raw, _, err := s.Get(ctx, key)
+ if err != nil {
+ return nil, err
+ }
+ p, err := VerifySystemIdentity(pub, raw)
+ if err != nil {
+ return nil, err
+ }
+ if p.Current.ReleaseTag != tag {
+ return nil, ErrBadSystemPointer
+ }
+ return p, nil
+}
+
+func CreateExact(ctx context.Context, s Store, key string, raw []byte) error {
+ _, err := s.Put(ctx, key, raw, "")
+ if err == nil {
+ return nil
+ }
+ // A timed-out PUT may have committed. Only identical bytes count as success.
+ got, _, readErr := s.Get(ctx, key)
+ if readErr == nil && bytes.Equal(raw, got) {
+ return nil
+ }
+ if readErr != nil && !errors.Is(readErr, os.ErrNotExist) {
+ return errors.Join(err, readErr)
+ }
+ return err
+}
+```
+
+### Create `internal/hostfloor/disk.go`
+
+```go
+package hostfloor
+
+import (
+ "crypto/ed25519"
+ "encoding/json"
+ "errors"
+ "github.com/carlosframework/platform/internal/release"
+ "io"
+ "os"
+ "path/filepath"
+)
+
+type Disk struct {
+ Dir, Bucket string
+ PublicKey ed25519.PublicKey
+}
+
+func (d Disk) Load() (Policy, Verified, error) {
+ raw, err := os.ReadFile(filepath.Join(d.Dir, "policy.json"))
+ if err != nil {
+ return Policy{}, Verified{}, err
+ }
+ var p Policy
+ if json.Unmarshal(raw, &p) != nil {
+ return Policy{}, Verified{}, ErrFloor
+ }
+ f, err := p.Verify(d.PublicKey, d.Bucket)
+ return p, f, err
+}
+func Durable(path string, raw []byte, mode os.FileMode) error {
+ dir := filepath.Dir(path)
+ f, err := os.CreateTemp(dir, ".guard-*")
+ if err != nil {
+ return err
+ }
+ name := f.Name()
+ defer os.Remove(name)
+ if err = f.Chmod(mode); err == nil {
+ _, err = f.Write(raw)
+ }
+ if err == nil {
+ err = f.Sync()
+ }
+ err = errors.Join(err, f.Close())
+ if err != nil {
+ return err
+ }
+ if err = os.Rename(name, path); err != nil {
+ return err
+ }
+ fd, err := os.Open(dir)
+ if err != nil {
+ return err
+ }
+ defer fd.Close()
+ return fd.Sync()
+}
+
+// Persist never creates unpublished state implicitly. Bootstrap creates that
+// explicit root-owned file before installing the guarded unit drop-ins.
+func (d Disk) Persist(next Policy, identities map[string][]byte) error {
+ _, nf, err := func() (Policy, Verified, error) { f, e := next.Verify(d.PublicKey, d.Bucket); return next, f, e }()
+ if err != nil {
+ return err
+ }
+ old, of, err := d.Load()
+ if err != nil {
+ return err
+ }
+ if old.Mode != "unpublished" {
+ if next.Mode == "unpublished" || nf.Digest != of.Digest {
+ return ErrFloor
+ }
+ if old.Mode == "enforced" && next.Mode != "enforced" {
+ return ErrFloor
+ }
+ }
+ for tag, raw := range identities {
+ p, err := release.VerifySystemIdentity(d.PublicKey, raw)
+ if err != nil || p.Current.ReleaseTag != tag {
+ return ErrFloor
+ }
+ if err = Durable(filepath.Join(d.Dir, "identities", tag+".json"), raw, 0644); err != nil {
+ return err
+ }
+ }
+ raw, err := json.Marshal(next)
+ if err != nil {
+ return err
+ }
+ return Durable(filepath.Join(d.Dir, "policy.json"), raw, 0644)
+}
+func (d Disk) VerifyFile(f *os.File, platform string, roles ...string) (string, error) {
+ component := "router"
+ if len(roles) == 1 {
+ component = roles[0]
+ } else if len(roles) > 1 {
+ return "", ErrFloor
+ }
+ p, floor, err := d.Load()
+ if err != nil {
+ return "", err
+ }
+ if p.Mode == "unpublished" {
+ return "", nil
+ }
+ if _, err = f.Seek(0, io.SeekStart); err != nil {
+ return "", err
+ }
+ raw, err := io.ReadAll(io.LimitReader(f, 512<<20))
+ if err != nil {
+ return "", err
+ }
+ hash := Digest(raw)
+ entries, err := os.ReadDir(filepath.Join(d.Dir, "identities"))
+ if err != nil {
+ return "", err
+ }
+ for _, entry := range entries {
+ if entry.IsDir() {
+ continue
+ }
+ wire, err := os.ReadFile(filepath.Join(d.Dir, "identities", entry.Name()))
+ if err != nil {
+ return "", err
+ }
+ identity, err := release.VerifySystemIdentity(d.PublicKey, wire)
+ if err != nil {
+ return "", err
+ }
+ if CheckComponent(d.PublicKey, floor, wire, identity.Current.Version, platform, hash, component) == nil {
+ return hash, nil
+ }
+ }
+ return "", ErrFloor
+}
+func (d Disk) Begin(id, dataDir string) error {
+ if id == "" {
+ return ErrFloor
+ }
+ marker := filepath.Join(d.Dir, "update-begun.json")
+ if _, err := os.Stat(marker); err == nil {
+ return ErrFloor
+ } else if !errors.Is(err, os.ErrNotExist) {
+ return err
+ }
+ raw, _ := json.Marshal(struct {
+ ID string `json:"id"`
+ }{id})
+ if err := Durable(marker, raw, 0644); err != nil {
+ return err
+ }
+ entries, err := os.ReadDir(filepath.Join(dataDir, ".hostexec"))
+ if err != nil || len(entries) != 0 {
+ removeErr := RemoveDurable(marker)
+ if err == nil {
+ err = ErrFloor
+ }
+ return errors.Join(err, removeErr)
+ }
+ return nil
+}
+func RemoveDurable(path string) error {
+ if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
+ return err
+ }
+ dir, err := os.Open(filepath.Dir(path))
+ if err != nil {
+ return err
+ }
+ defer dir.Close()
+ return dir.Sync()
+}
+func UpdateIdle(guardDir, journal string) error {
+ for _, p := range []string{filepath.Join(guardDir, "update-begun.json"), journal} {
+ if _, err := os.Lstat(p); err == nil {
+ return ErrFloor
+ } else if !errors.Is(err, os.ErrNotExist) {
+ return err
+ }
+ }
+ return nil
+}
+```
+
+### Create `internal/hostfloor/floor.go`
+
+```go
+package hostfloor
+
+import (
+ "context"
+ "crypto/ed25519"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+const Key = "control/moves-protocol.json"
+const AuthKey = "control/moves-protocol-auth.json"
+
+var ErrFloor = errors.New("hostfloor: authenticated floor required")
+
+type Floor struct {
+ Min int `json:"min"`
+ MinRelease string `json:"min_release"`
+}
+type Authorization struct {
+ Schema int `json:"schema"`
+ Purpose string `json:"purpose"`
+ DeploymentBucket string `json:"deployment_bucket"`
+ Key string `json:"key"`
+ SHA256 string `json:"sha256"`
+}
+type Policy struct {
+ Schema int `json:"schema"`
+ DeploymentBucket string `json:"deployment_bucket"`
+ Mode string `json:"mode"`
+ Floor json.RawMessage `json:"floor"`
+ Authorization json.RawMessage `json:"authorization"`
+}
+type Verified struct {
+ Floor Floor
+ Digest string
+}
+
+func Digest(b []byte) string { sum := sha256.Sum256(b); return hex.EncodeToString(sum[:]) }
+
+func Verify(pub ed25519.PublicKey, bucket string, raw, auth []byte) (Verified, error) {
+ var f Floor
+ var a Authorization
+ if bucket == "" || json.Unmarshal(raw, &f) != nil || f.Min < 1 {
+ return Verified{}, ErrFloor
+ }
+ if _, ok := release.ReleaseTag(f.MinRelease); !ok {
+ return Verified{}, ErrFloor
+ }
+ if release.VerifyRootEnvelope(pub, auth, &a) != nil || a.Schema != 1 || a.Purpose != "moves-protocol" || a.Key != Key || a.DeploymentBucket != bucket || a.SHA256 != Digest(raw) {
+ return Verified{}, ErrFloor
+ }
+ var fields map[string]json.RawMessage
+ if json.Unmarshal(raw, &fields) != nil || len(fields) != 2 {
+ return Verified{}, ErrFloor
+ }
+ return Verified{f, Digest(raw)}, nil
+}
+func Read(ctx context.Context, s release.Store, pub ed25519.PublicKey, bucket string) (Verified, error) {
+ raw, _, err := s.Get(ctx, Key)
+ if err != nil {
+ return Verified{}, err
+ }
+ auth, _, err := s.Get(ctx, AuthKey)
+ if err != nil {
+ return Verified{}, err
+ }
+ return Verify(pub, bucket, raw, auth)
+}
+func Publish(ctx context.Context, s release.Store, sg release.Signer, bucket string, f Floor) error {
+ raw, err := json.Marshal(f)
+ if err != nil {
+ return err
+ }
+ auth, err := release.SignRootEnvelope(sg, Authorization{1, "moves-protocol", bucket, Key, Digest(raw)})
+ if err != nil {
+ return err
+ }
+ if _, err = Verify(sg.RootPublicKey(), bucket, raw, auth); err != nil {
+ return err
+ }
+ if err = release.CreateExact(ctx, s, AuthKey, auth); err != nil {
+ return err
+ }
+ return release.CreateExact(ctx, s, Key, raw)
+}
+func (p Policy) Verify(pub ed25519.PublicKey, bucket string) (Verified, error) {
+ if p.Schema != 1 || p.DeploymentBucket != bucket {
+ return Verified{}, ErrFloor
+ }
+ switch p.Mode {
+ case "unpublished":
+ if (len(p.Floor) != 0 && string(p.Floor) != "null") || (len(p.Authorization) != 0 && string(p.Authorization) != "null") {
+ return Verified{}, ErrFloor
+ }
+ return Verified{}, nil
+ case "armed", "enforced":
+ return Verify(pub, bucket, p.Floor, p.Authorization)
+ default:
+ return Verified{}, ErrFloor
+ }
+}
+func CheckImage(pub ed25519.PublicKey, f Verified, identity []byte, build, platform, hash string) error {
+ return CheckComponent(pub, f, identity, build, platform, hash, "router")
+}
+func CheckComponent(pub ed25519.PublicKey, f Verified, identity []byte, build, platform, hash, component string) error {
+ p, err := release.VerifySystemIdentity(pub, identity)
+ if err != nil {
+ return err
+ }
+ have, ok := release.ReleaseTag(p.Current.ReleaseTag)
+ if !ok {
+ return ErrFloor
+ }
+ want, ok := release.ReleaseTag(f.Floor.MinRelease)
+ if !ok {
+ return ErrFloor
+ }
+ a, ok := p.Current.Artifacts[platform]
+ if component == "console" {
+ a, ok = p.Current.ConsoleArtifacts[platform]
+ } else if component != "router" {
+ return ErrFloor
+ }
+ if !ok || build != p.Current.Version || hash != a.SHA256 || have.Compare(want) < 0 || p.Current.MovesProtocol < f.Floor.Min {
+ return ErrFloor
+ }
+ return nil
+}
+```
+
+### Create `internal/hostfloor/image_linux.go`
+
+```go
+//go:build linux
+
+package hostfloor
+
+import (
+ "context"
+ "fmt"
+ "golang.org/x/sys/unix"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strconv"
+ "syscall"
+)
+
+func OpenProtected(path string) (*os.File, error) {
+ if !filepath.IsAbs(path) || filepath.Clean(path) != path {
+ return nil, ErrFloor
+ }
+ // Every parent is root-owned and cannot be changed by a platform worker.
+ for p := filepath.Dir(path); ; p = filepath.Dir(p) {
+ s, err := os.Lstat(p)
+ if err != nil {
+ return nil, err
+ }
+ stat, ok := s.Sys().(*syscall.Stat_t)
+ if !ok || stat.Uid != 0 || !s.IsDir() || s.Mode().Perm()&0022 != 0 {
+ return nil, ErrFloor
+ }
+ if p == "/" {
+ break
+ }
+ }
+ fd, err := unix.Open(path, unix.O_RDONLY|unix.O_CLOEXEC|unix.O_NOFOLLOW|unix.O_NONBLOCK, 0)
+ if err != nil {
+ return nil, err
+ }
+ f := os.NewFile(uintptr(fd), path)
+ s, err := f.Stat()
+ if err != nil {
+ f.Close()
+ return nil, err
+ }
+ stat, ok := s.Sys().(*syscall.Stat_t)
+ if !ok || stat.Uid != 0 || !s.Mode().IsRegular() || s.Mode().Perm()&0022 != 0 {
+ f.Close()
+ return nil, ErrFloor
+ }
+ return f, nil
+}
+func (d Disk) Exec(path, platform string, args, env []string, roles ...string) error {
+ f, err := OpenProtected(path)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ if _, err = d.VerifyFile(f, platform, roles...); err != nil {
+ return err
+ }
+ return syscall.Exec("/proc/self/fd/"+strconv.Itoa(int(f.Fd())), append([]string{path}, args...), env)
+}
+func (d Disk) Run(ctx context.Context, path, platform string, args ...string) (string, error) {
+ f, err := OpenProtected(path)
+ if err != nil {
+ return "", err
+ }
+ defer f.Close()
+ if _, err = d.VerifyFile(f, platform); err != nil {
+ return "", err
+ }
+ cmd := exec.CommandContext(ctx, "/proc/self/fd/3", args...)
+ cmd.ExtraFiles = []*os.File{f}
+ out, err := cmd.CombinedOutput()
+ return string(out), err
+}
+func ExecutingHash() (string, error) {
+ f, err := os.Open("/proc/self/exe")
+ if err != nil {
+ return "", err
+ }
+ defer f.Close()
+ raw, err := os.ReadFile(fmt.Sprintf("/proc/self/fd/%d", f.Fd()))
+ if err != nil {
+ return "", err
+ }
+ return Digest(raw), nil
+}
+```
+
+### Create `internal/hostfloor/image_other.go`
+
+```go
+//go:build !linux
+
+package hostfloor
+
+import (
+ "context"
+ "os"
+)
+
+func OpenProtected(string) (*os.File, error) { return nil, ErrFloor }
+func (d Disk) Exec(string, string, []string, []string, ...string) error { return ErrFloor }
+func (d Disk) Run(context.Context, string, string, ...string) (string, error) { return "", ErrFloor }
+func ExecutingHash() (string, error) { return "", ErrFloor }
+```
+
+### Create `internal/hostfloor/remote.go`
+
+```go
+package hostfloor
+
+import (
+ "context"
+ "crypto/ed25519"
+ "errors"
+ "github.com/aws/smithy-go"
+ "github.com/carlosframework/platform/internal/release"
+ "net"
+)
+
+func Offline(err error) bool {
+ if errors.Is(err, context.DeadlineExceeded) {
+ return true
+ }
+ var network net.Error
+ if errors.As(err, &network) {
+ return true
+ }
+ var api smithy.APIError
+ if errors.As(err, &api) {
+ switch api.ErrorCode() {
+ case "SlowDown", "ServiceUnavailable", "InternalError":
+ return true
+ }
+ }
+ return false
+}
+func CheckRemote(ctx context.Context, s release.Store, pub ed25519.PublicKey, bucket string, local Verified) error {
+ remote, err := Read(ctx, s, pub, bucket)
+ if err != nil {
+ if Offline(err) {
+ return nil
+ }
+ return err
+ }
+ if remote.Digest != local.Digest {
+ return ErrFloor
+ }
+ return nil
+}
+```
+
+### Create `internal/hostfloor/report.go`
+
+```go
+package hostfloor
+
+import (
+ "encoding/json"
+ "github.com/carlosframework/platform/internal/release"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+type Running struct {
+ ReleaseTag string
+ Hash string
+ Protocol int
+ BootID string
+ FloorSHA256 string
+ GuardReady bool
+}
+
+func (d Disk) Running(build, platform string, role ...string) (Running, error) {
+ component := "router"
+ if len(role) == 1 {
+ component = role[0]
+ } else if len(role) > 1 {
+ return Running{}, ErrFloor
+ }
+ _, f, err := d.Load()
+ if err != nil {
+ return Running{}, err
+ }
+ if component == "console" {
+ path, err := os.ReadFile(filepath.Join(d.Dir, "console-ready.json"))
+ if err != nil {
+ return Running{}, err
+ }
+ dropin, err := os.ReadFile("/etc/systemd/system/console.service.d/90-host-floor.conf")
+ if err != nil {
+ return Running{}, err
+ }
+ want := "[Service]\nExecStart=\nExecStart=/opt/carlos/system/guard/carlos system guard-exec console " + string(path) + "\n"
+ if string(dropin) != want {
+ return Running{}, ErrFloor
+ }
+ }
+ hash, err := ExecutingHash()
+ if err != nil {
+ return Running{}, err
+ }
+ boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
+ if err != nil {
+ return Running{}, err
+ }
+ entries, err := os.ReadDir(filepath.Join(d.Dir, "identities"))
+ if err != nil {
+ return Running{}, err
+ }
+ for _, entry := range entries {
+ if entry.IsDir() {
+ continue
+ }
+ raw, err := os.ReadFile(filepath.Join(d.Dir, "identities", entry.Name()))
+ if err != nil {
+ return Running{}, err
+ }
+ if CheckComponent(d.PublicKey, f, raw, build, platform, hash, component) != nil {
+ continue
+ }
+ tag := strings.TrimSuffix(entry.Name(), ".json")
+ if _, err := os.Stat(filepath.Join(d.Dir, "carlos")); err != nil {
+ return Running{}, err
+ }
+ identity, err := release.VerifySystemIdentity(d.PublicKey, raw)
+ if err != nil {
+ return Running{}, err
+ }
+ return Running{tag, hash, identity.Current.MovesProtocol, strings.TrimSpace(string(boot)), f.Digest, true}, nil
+ }
+ return Running{}, ErrFloor
+}
+
+// Enrollment is installed by root from the verified roster. It is reporting
+// configuration; readers always compare it to the fresh signed roster.
+type Enrollment struct {
+ Box string `json:"box"`
+ Enrollment string `json:"enrollment"`
+ InstanceID string `json:"instance_id"`
+}
+
+func ReadEnrollment(path string) (Enrollment, error) {
+ raw, err := os.ReadFile(path)
+ if err != nil {
+ return Enrollment{}, err
+ }
+ var e Enrollment
+ if json.Unmarshal(raw, &e) != nil || e.Box == "" || e.Enrollment == "" || e.InstanceID == "" {
+ return Enrollment{}, ErrFloor
+ }
+ return e, nil
+}
+```
+
+**Step 3: apply the exact existing-file patch.**
+
+```diff
+--- a/internal/release/systempointer.go
++++ b/internal/release/systempointer.go
+@@ -36,4 +36,7 @@
+- Version string `json:"version"`
+- Artifacts map[string]SystemArtifact `json:"artifacts"` // keyed by "<goos>-<goarch>"
+- Source string `json:"source,omitempty"`
+- ReleasedAt time.Time `json:"released_at"` // audit only; never compared
++ ConsoleArtifacts map[string]SystemArtifact `json:"console_artifacts,omitempty"`
++ ReleaseTag string `json:"release_tag,omitempty"`
++ MovesProtocol int `json:"moves_protocol,omitempty"`
++ Version string `json:"version"`
++ Artifacts map[string]SystemArtifact `json:"artifacts"` // keyed by "<goos>-<goarch>"
++ Source string `json:"source,omitempty"`
++ ReleasedAt time.Time `json:"released_at"` // audit only; never compared
+@@ -102,0 +106,5 @@
++ }
++ for platform, artifact := range r.ConsoleArtifacts {
++ if !systemPlatformRe.MatchString(platform) || artifact.Key != "bootstrap/console/"+r.Version+"/carlos-"+platform || !systemSHA256Re.MatchString(artifact.SHA256) {
++ return ErrBadSystemPointer
++ }
+@@ -287 +295 @@
+-func RollbackSystem(ctx context.Context, s Store, sg Signer, component, channel, actor string) (*SystemPointer, error) {
++func RollbackSystem(ctx context.Context, s Store, sg Signer, component, channel, actor string, expectedETag ...string) (*SystemPointer, error) {
+@@ -291,0 +300,3 @@
++ }
++ if len(expectedETag) > 1 || (len(expectedETag) == 1 && etag != expectedETag[0]) {
++ return nil, ErrCASConflict
+```
+
+**Step 4: verify the task.**
+
+```sh
+go test ./internal/hostfloor -run 'TestFloorPair|TestPendingAndLost|TestCanonicalRelease' -count=1
+```
+
+## Task 2: The signed roster, verified enrollment, census and reservations
+
+Build the exact-object ActorDirectory and enrollment protocol. Invite first, install readiness under that nonce, then admit. Production enrollment uses EnrollAWS and EmbeddedCerts; the Verifier argument on the lower-level method is a test seam. H16/H17 confirmation consumers take group/account/region configuration from the signed roster. Existing claims and edges are scanned completely for legacy references. Bootstrap Initialize creates no actors and cannot pass publication or a lifecycle census. Initial inventory reconciliation remains a required input to Paul's activation, not a heartbeat inference.
+
+**Step 1: add the failing tests.**
+
+### Create `internal/hostactors/census_test.go`
+
+```go
+package hostactors
+
+import (
+ "context"
+ "encoding/json"
+ "github.com/carlosframework/platform/internal/hostfloor"
+ "github.com/carlosframework/platform/internal/release"
+ "strings"
+ "testing"
+ "time"
+)
+
+type reportsFixture struct {
+ box, console Report
+ wire []byte
+}
+
+func (e reportsFixture) BoxReport(context.Context, string, Box) (Report, error) { return e.box, nil }
+func (e reportsFixture) ConsoleReport(context.Context, string, Console) (Report, error) {
+ return e.console, nil
+}
+func (e reportsFixture) LocalConsole(context.Context) (Report, error) { return e.console, nil }
+func (e reportsFixture) Identity(context.Context, string) ([]byte, error) { return e.wire, nil }
+func censusFixture(t *testing.T) (Directory, Observed, reportsFixture, hostfloor.Verified, Scope, time.Time) {
+ t.Helper()
+ ctx := context.Background()
+ d, sg, o := fixture(t)
+ now := time.Now().UTC()
+ tag := "v1.2.3"
+ hash := strings.Repeat("a", 64)
+ rel := release.SystemRelease{ConsoleArtifacts: map[string]release.SystemArtifact{"linux-amd64": {Key: "bootstrap/console/build123/carlos-linux-amd64", SHA256: hash}}, Version: "build123", ReleaseTag: tag, MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{"linux-amd64": {Key: release.SystemArtifactKey("build123", "linux-amd64"), SHA256: hash}}}
+ if _, err := release.PromoteSystem(ctx, d.Store, sg, "router", "stable", rel, release.SystemPromoteOptions{}); err != nil {
+ t.Fatal(err)
+ }
+ wire, _, err := d.Store.Get(ctx, release.SystemPointerKey("router", "stable"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ floor := hostfloor.Floor{Min: 1, MinRelease: tag}
+ raw, _ := json.Marshal(floor)
+ f := hostfloor.Verified{Floor: floor, Digest: hostfloor.Digest(raw)}
+ o.Roster.Publication = "published"
+ o.Roster.FloorSHA256 = f.Digest
+ c := Console{Enrollment: strings.Repeat("c", 32), InstanceID: "console-1", Namespaces: []string{"platform"}, DeploymentBucket: d.Bucket}
+ o.Roster.Consoles["console-1"] = c
+ box := Report{Component: "router", GeneratedAt: now, Actor: "box", Enrollment: o.Roster.Boxes["box"].Enrollment, InstanceID: "i-box", Release: "build123", ReleaseTag: tag, Platform: "linux-amd64", BinarySHA256: hash, MovesProtocol: 1, BootID: "boot", FloorSHA256: f.Digest, FloorGuardReady: true, HostClaimEnabled: true}
+ console := box
+ console.Component = "console"
+ console.Actor = "console-1"
+ console.Enrollment = c.Enrollment
+ console.InstanceID = "console-1"
+ console.ProcessID = "process-1"
+ return d, o, reportsFixture{box, console, wire}, f, Scope{Namespace: "platform", Regions: []string{"ie"}, Pools: []string{"default"}, ConsoleID: "console-1"}, now
+}
+func TestCensusRejectsMissingStaleAndWrongImages(t *testing.T) {
+ for _, field := range []string{"good", "old-box", "old-console", "future", "image", "floor", "flag", "enrollment", "scope", "unknown-participant"} {
+ t.Run(field, func(t *testing.T) {
+ d, o, e, f, scope, now := censusFixture(t)
+ switch field {
+ case "old-box":
+ e.box.GeneratedAt = now.Add(-16 * time.Minute)
+ case "old-console":
+ e.console.GeneratedAt = now.Add(-16 * time.Minute)
+ case "future":
+ e.box.GeneratedAt = now.Add(6 * time.Second)
+ case "image":
+ e.box.BinarySHA256 = strings.Repeat("b", 64)
+ case "floor":
+ e.console.FloorSHA256 = ""
+ case "flag":
+ e.box.HostClaimEnabled = false
+ case "enrollment":
+ e.box.Enrollment = "another"
+ case "scope":
+ scope.Regions = nil
+ case "unknown-participant":
+ scope.Participants = []string{"unknown"}
+ }
+ _, err := Census(context.Background(), o, e, d.PublicKey, f, scope, now)
+ if (err == nil) != (field == "good") {
+ t.Fatal(field, err)
+ }
+ })
+ }
+}
+```
+
+### Create `internal/hostactors/enroll_test.go`
+
+```go
+package hostactors
+
+import (
+ "context"
+ "crypto"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/sha256"
+ "crypto/x509"
+ "encoding/base64"
+ "encoding/json"
+ "github.com/carlosframework/platform/internal/boxid"
+ "testing"
+)
+
+func TestEnrollmentUsesSignedCloudAndRefusesRebinding(t *testing.T) {
+ d, sg, o := fixture(t)
+ key, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ t.Fatal(err)
+ }
+ cert := &x509.Certificate{PublicKey: &key.PublicKey, PublicKeyAlgorithm: x509.RSA}
+ document := []byte(`{"accountId":"123","region":"eu-west-1","instanceId":"i-new"}`)
+ sum := sha256.Sum256(document)
+ signature, err := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, sum[:])
+ if err != nil {
+ t.Fatal(err)
+ }
+ rec := boxid.Record{V: 1, Box: "new-label", IdentityDocument: base64.StdEncoding.EncodeToString(document), IdentitySignature: base64.StdEncoding.EncodeToString(signature)}
+ raw, _ := json.Marshal(rec)
+ v := boxid.Verifier{Account: "123", Certs: map[string]*x509.Certificate{"eu-west-1": cert}}
+ in := EnrollmentRequest{Reference: rec.Box, Namespace: "platform", Label: rec.Box, Region: "ie", Pool: "default", AWSRegion: "eu-west-1", Operator: "operator", ExistingReference: true, TofuInstanceID: "i-new"}
+ for _, wrong := range []string{"region", "tofu", "operator", "signature"} {
+ request := in
+ body := raw
+ switch wrong {
+ case "region":
+ request.AWSRegion = "us-east-1"
+ case "tofu":
+ request.TofuInstanceID = "i-other"
+ case "operator":
+ request.Operator = ""
+ case "signature":
+ bad := rec
+ bad.IdentitySignature = base64.StdEncoding.EncodeToString([]byte("wrong"))
+ body, _ = json.Marshal(bad)
+ }
+ if _, err = d.Enroll(context.Background(), sg, o, v, request, body); err == nil {
+ t.Fatal(wrong)
+ }
+ }
+ if _, err = d.Invite(context.Background(), sg, in.Reference, raw); err != nil {
+ t.Fatal(err)
+ }
+ next, err := d.Enroll(context.Background(), sg, o, v, in, raw)
+ if err != nil {
+ t.Fatal(err)
+ }
+ got := next.Roster.Boxes[rec.Box]
+ if got.Cloud.InstanceID != "i-new" || got.BoxID != rec.Box || got.Enrollment == "" {
+ t.Fatal(got)
+ }
+ in.Reference = "box"
+ rec.Box = "box"
+ raw, _ = json.Marshal(rec)
+ if _, err = d.Enroll(context.Background(), sg, next, v, in, raw); err == nil {
+ t.Fatal("reference rebound")
+ }
+}
+```
+
+### Create `internal/hostactors/roster_test.go`
+
+```go
+package hostactors
+
+import (
+ "context"
+ "crypto/ed25519"
+ "crypto/rand"
+ "github.com/carlosframework/platform/internal/release"
+ "strings"
+ "testing"
+)
+
+func fixture(t *testing.T) (Directory, release.Signer, Observed) {
+ t.Helper()
+ pub, key, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ t.Fatal(err)
+ }
+ s := release.NewDirStore(t.TempDir())
+ sg := release.RootSigner(key)
+ r := Roster{V: 1, DeploymentBucket: "bucket", Revision: 1, Boxes: map[string]Box{"box": {Enrollment: strings.Repeat("a", 32), Namespace: "platform", BoxID: "box", Label: "label", Region: "ie", Pool: "default", Cloud: Cloud{"aws", "123", "eu-west-1", "i-box"}, EvidenceSHA256: strings.Repeat("b", 64), State: "active", LossIDs: []string{}}}, Consoles: map[string]Console{}, Holds: map[string]Reservation{}, Activations: map[string]Reservation{}}
+ raw, err := release.SignRootEnvelope(sg, r)
+ if err != nil {
+ t.Fatal(err)
+ }
+ etag, err := s.Put(context.Background(), Key, raw, "")
+ if err != nil {
+ t.Fatal(err)
+ }
+ return Directory{s, pub, "bucket"}, sg, Observed{r, etag}
+}
+func TestDirectoryFreshQuarantineAndCAS(t *testing.T) {
+ ctx := context.Background()
+ d, sg, o := fixture(t)
+ first, err := d.Lookup(ctx, "box")
+ if err != nil || first.State != "active" {
+ t.Fatal(first, err)
+ }
+ changed, err := d.Quarantine(ctx, sg, o, "box", first.Enrollment, "loss-1")
+ if err != nil {
+ t.Fatal(err)
+ }
+ got, err := d.Lookup(ctx, "box")
+ if err != nil || got.State != "quarantined" || got.QuarantineRevision != 1 {
+ t.Fatal(got, err)
+ }
+ if _, err = d.Quarantine(ctx, sg, o, "box", first.Enrollment, "loss-2"); err == nil {
+ t.Fatal("stale CAS accepted")
+ }
+ next := Clone(changed.Roster)
+ b := next.Boxes["box"]
+ b.Cloud.InstanceID = "i-other"
+ next.Boxes["box"] = b
+ if _, err = d.Replace(ctx, sg, changed, next); err == nil {
+ t.Fatal("cloud binding changed")
+ }
+}
+func TestReservationsExcludeBothOrders(t *testing.T) {
+ ctx := context.Background()
+ for _, activationFirst := range []bool{true, false} {
+ d, sg, o := fixture(t)
+ en := o.Roster.Boxes["box"].Enrollment
+ h := Reservation{ID: "hold", Host: "app.example", OpID: "op", ExpectedClaimETag: "etag", FloorSHA256: strings.Repeat("f", 64), Actors: map[string]string{"box:box": en}, Releases: map[string]Report{"box:box": {Enrollment: en}}}
+ a := h
+ a.ID = "activation"
+ a.Hashes = map[string][]string{"box:box": {strings.Repeat("a", 64), strings.Repeat("b", 64), strings.Repeat("c", 64)}}
+ first, second := h, a
+ if activationFirst {
+ first, second = a, h
+ }
+ next, err := d.Reserve(ctx, sg, o, first, activationFirst)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err = d.Reserve(ctx, sg, next, second, !activationFirst); err == nil {
+ t.Fatal("overlap accepted")
+ }
+ }
+}
+```
+
+Run this now and expect failure before the implementation is present:
+
+```sh
+go test ./internal/hostactors -count=1
+```
+
+**Step 2: add the implementation.**
+
+### Create `internal/hostactors/activation_complete.go`
+
+```go
+package hostactors
+
+import (
+ "context"
+ "github.com/carlosframework/platform/internal/hostfloor"
+ "github.com/carlosframework/platform/internal/release"
+ "slices"
+ "strings"
+ "time"
+)
+
+func (d Directory) ReleaseActivation(ctx context.Context, sg release.Signer, id string, now time.Time) error {
+ o, err := d.Read(ctx)
+ if err != nil {
+ return err
+ }
+ h, ok := o.Roster.Activations[id]
+ if !ok {
+ return nil
+ }
+ floor, err := hostfloor.Read(ctx, d.Store, d.PublicKey, d.Bucket)
+ if err != nil {
+ return err
+ }
+ evidence := ExactEvidence{Store: d.Store, Boxes: d.PrivateBoxReport}
+ for actor, enrollment := range h.Actors {
+ ref, ok := strings.CutPrefix(actor, "box:")
+ if !ok {
+ return ErrRoster
+ }
+ box, ok := o.Roster.Boxes[ref]
+ if !ok || box.Enrollment != enrollment {
+ return ErrRoster
+ }
+ r, err := d.PrivateBoxReport(ctx, ref, box)
+ if err != nil {
+ return err
+ }
+ if err = VerifyReport(ctx, evidence, d.PublicKey, floor, r, enrollment, box.Cloud.InstanceID, now); err != nil {
+ return err
+ }
+ if r.ActivationID != id || !r.ActivationComplete || !slices.Contains(h.Hashes[actor], r.BinarySHA256) {
+ return ErrRoster
+ }
+ }
+ next := Clone(o.Roster)
+ delete(next.Activations, id)
+ _, err = d.Replace(ctx, sg, o, next)
+ return err
+}
+```
+
+### Create `internal/hostactors/census.go`
+
+```go
+package hostactors
+
+import (
+ "context"
+ "crypto/ed25519"
+ "encoding/json"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostfloor"
+ "github.com/carlosframework/platform/internal/release"
+ "slices"
+ "time"
+)
+
+type Report struct {
+ Component string `json:"component"`
+ SystemChannel string `json:"system_channel,omitempty"`
+ ActivationID string `json:"activation_id,omitempty"`
+ ActivationComplete bool `json:"activation_complete,omitempty"`
+ GeneratedAt time.Time `json:"generated_at"`
+ Actor string `json:"actor"`
+ Enrollment string `json:"enrollment"`
+ InstanceID string `json:"instance_id"`
+ ProcessID string `json:"process_id"`
+ Release string `json:"release"`
+ ReleaseTag string `json:"release_tag"`
+ Platform string `json:"platform"`
+ BinarySHA256 string `json:"binary_sha256"`
+ MovesProtocol int `json:"moves_protocol"`
+ BootID string `json:"boot_id"`
+ FloorSHA256 string `json:"floor_sha256"`
+ FloorGuardReady bool `json:"floor_guard_ready"`
+ HostClaimEnabled bool `json:"host_claim_enabled"`
+ RetirementReceiptSHA256 string `json:"retirement_receipt_sha256,omitempty"`
+}
+type Scope struct {
+ Namespace string
+ Regions []string
+ Pools []string
+ Participants []string
+ ConsoleID string
+}
+type Evidence interface {
+ BoxReport(context.Context, string, Box) (Report, error)
+ ConsoleReport(context.Context, string, Console) (Report, error)
+ LocalConsole(context.Context) (Report, error)
+ Identity(context.Context, string) ([]byte, error)
+}
+
+func VerifyReport(ctx context.Context, e Evidence, pub ed25519.PublicKey, f hostfloor.Verified, r Report, enrollment, instance string, now time.Time) error {
+ age := now.Sub(r.GeneratedAt)
+ if r.GeneratedAt.IsZero() || age > 15*time.Minute || age < -5*time.Second || r.Enrollment != enrollment || r.InstanceID != instance || r.BootID == "" || !r.FloorGuardReady || r.FloorSHA256 != f.Digest || r.MovesProtocol < f.Floor.Min {
+ return ErrRoster
+ }
+ wire, err := e.Identity(ctx, r.ReleaseTag)
+ if err != nil {
+ return err
+ }
+ p, err := release.VerifySystemIdentity(pub, wire)
+ if err != nil {
+ return err
+ }
+ if p.Current.ReleaseTag != r.ReleaseTag || p.Current.MovesProtocol != r.MovesProtocol {
+ return ErrRoster
+ }
+ return hostfloor.CheckComponent(pub, f, wire, r.Release, r.Platform, r.BinarySHA256, r.Component)
+}
+func Census(ctx context.Context, o Observed, e Evidence, pub ed25519.PublicKey, f hostfloor.Verified, scope Scope, now time.Time) (map[string]Report, error) {
+ if o.Roster.Publication != "published" || o.Roster.FloorSHA256 != f.Digest {
+ return nil, ErrRoster
+ }
+ if scope.Namespace == "" || len(scope.Regions) == 0 || len(scope.Pools) == 0 || scope.ConsoleID == "" || e == nil {
+ return nil, ErrRoster
+ }
+ reports := map[string]Report{}
+ participants := map[string]bool{}
+ for _, p := range scope.Participants {
+ participants[p] = true
+ }
+ for ref, b := range o.Roster.Boxes {
+ required := participants[ref] || (b.Namespace == scope.Namespace && slices.Contains(scope.Regions, b.Region) && slices.Contains(scope.Pools, b.Pool))
+ if !required {
+ continue
+ }
+ requiredParticipant := participants[ref]
+ delete(participants, ref)
+ if b.State != "active" {
+ if requiredParticipant {
+ return nil, ErrRoster
+ }
+ if len(b.LossIDs) == 0 || b.QuarantineRevision == 0 {
+ return nil, ErrRoster
+ }
+ exact, ok := e.(ExactEvidence)
+ if !ok {
+ return nil, ErrRoster
+ }
+ found := false
+ for _, id := range b.LossIDs {
+ loss, err := hostclaim.ReadLossRecord(ctx, exact.Store, pub, id)
+ if err != nil {
+ return nil, err
+ }
+ if loss.Record.Box != ref || loss.Record.Enrollment != b.Enrollment {
+ return nil, ErrRoster
+ }
+ if loss.Record.QuarantineRevision == b.QuarantineRevision {
+ found = true
+ }
+ }
+ if !found {
+ return nil, ErrRoster
+ }
+ continue
+ }
+ r, err := e.BoxReport(ctx, ref, b)
+ if err != nil {
+ return nil, err
+ }
+ if r.Component != "router" || r.Actor != ref || !r.HostClaimEnabled {
+ return nil, ErrRoster
+ }
+ if err = VerifyReport(ctx, e, pub, f, r, b.Enrollment, b.Cloud.InstanceID, now); err != nil {
+ return nil, err
+ }
+ reports["box:"+ref] = r
+ }
+ if len(participants) != 0 {
+ return nil, ErrRoster
+ }
+ for id, c := range o.Roster.Consoles {
+ if !slices.Contains(c.Namespaces, scope.Namespace) && id != scope.ConsoleID {
+ continue
+ }
+ r, err := e.ConsoleReport(ctx, id, c)
+ if err != nil {
+ return nil, err
+ }
+ if r.Component != "console" || r.Actor != id || r.ProcessID == "" || !r.HostClaimEnabled {
+ return nil, ErrRoster
+ }
+ if err = VerifyReport(ctx, e, pub, f, r, c.Enrollment, id, now); err != nil {
+ return nil, err
+ }
+ reports["console:"+id] = r
+ }
+ own, ok := o.Roster.Consoles[scope.ConsoleID]
+ if !ok {
+ return nil, ErrRoster
+ }
+ local, err := e.LocalConsole(ctx)
+ if err != nil {
+ return nil, err
+ }
+ published, ok := reports["console:"+scope.ConsoleID]
+ if !ok || local.Component != "console" || !local.HostClaimEnabled || local.Actor != scope.ConsoleID || local.ProcessID == "" || local.ProcessID != published.ProcessID {
+ return nil, ErrRoster
+ }
+ if err = VerifyReport(ctx, e, pub, f, local, own.Enrollment, scope.ConsoleID, now); err != nil {
+ return nil, err
+ }
+ return reports, nil
+}
+func (d Directory) Reserve(ctx context.Context, sg release.Signer, o Observed, h Reservation, activation bool) (Observed, error) {
+ r := Clone(o.Roster)
+ if h.ID == "" || len(h.Actors) == 0 || len(h.Releases) != len(h.Actors) {
+ return Observed{}, ErrRoster
+ }
+ if _, exists := r.Holds[h.ID]; exists {
+ return Observed{}, ErrRoster
+ }
+ if _, exists := r.Activations[h.ID]; exists {
+ return Observed{}, ErrRoster
+ }
+ if activation {
+ if len(h.Hashes) != len(h.Actors) {
+ return Observed{}, ErrRoster
+ }
+ for key := range h.Actors {
+ if len(h.Hashes[key]) != 3 {
+ return Observed{}, ErrRoster
+ }
+ }
+ for _, other := range r.Holds {
+ if overlaps(h, other) {
+ return Observed{}, ErrRoster
+ }
+ }
+ for _, other := range r.Activations {
+ if overlaps(h, other) {
+ return Observed{}, ErrRoster
+ }
+ }
+ r.Activations[h.ID] = h
+ } else {
+ if h.Host == "" || h.OpID == "" {
+ return Observed{}, ErrRoster
+ }
+ for _, other := range r.Activations {
+ if overlaps(h, other) {
+ return Observed{}, ErrRoster
+ }
+ }
+ r.Holds[h.ID] = h
+ }
+ return d.Replace(ctx, sg, o, r)
+}
+func (d Directory) CheckBoxStart(ctx context.Context, refs ...string) (uint64, error) {
+ o, err := d.Read(ctx)
+ if err != nil {
+ return 0, err
+ }
+ for _, ref := range refs {
+ b, ok := o.Roster.Boxes[ref]
+ if !ok || b.State != "active" {
+ return 0, ErrRoster
+ }
+ for _, a := range o.Roster.Activations {
+ for _, enrollment := range a.Actors {
+ if enrollment == b.Enrollment {
+ return 0, ErrRoster
+ }
+ }
+ }
+ }
+ return o.Roster.Revision, nil
+}
+
+// ExactEvidence adapts the existing private status object without changing
+// the public status shape. The supplied readers must use exact private keys.
+type ExactEvidence struct {
+ Store release.Store
+ Boxes func(context.Context, string, Box) (Report, error)
+ Self func(context.Context) (Report, error)
+}
+
+func (e ExactEvidence) BoxReport(ctx context.Context, id string, b Box) (Report, error) {
+ if e.Boxes == nil {
+ return Report{}, ErrRoster
+ }
+ return e.Boxes(ctx, id, b)
+}
+func (e ExactEvidence) LocalConsole(ctx context.Context) (Report, error) {
+ if e.Self == nil {
+ return Report{}, ErrRoster
+ }
+ return e.Self(ctx)
+}
+func (e ExactEvidence) ConsoleReport(ctx context.Context, id string, c Console) (Report, error) {
+ if !segment(id) {
+ return Report{}, ErrRoster
+ }
+ raw, _, err := e.Store.Get(ctx, "status/consoles/"+id+".json")
+ if err != nil {
+ return Report{}, err
+ }
+ var r Report
+ if err = json.Unmarshal(raw, &r); err != nil {
+ return Report{}, err
+ }
+ return r, nil
+}
+func (e ExactEvidence) Identity(ctx context.Context, tag string) ([]byte, error) {
+ key, err := release.IdentityKey(tag)
+ if err != nil {
+ return nil, err
+ }
+ raw, _, err := e.Store.Get(ctx, key)
+ return raw, err
+}
+```
+
+### Create `internal/hostactors/enroll.go`
+
+```go
+package hostactors
+
+import (
+ "context"
+ "encoding/base64"
+ "encoding/json"
+ "github.com/carlosframework/platform/internal/boxid"
+ "github.com/carlosframework/platform/internal/hostfloor"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+type EnrollmentRequest struct {
+ Reference string
+ Namespace string
+ Label string
+ Region string
+ Pool string
+ AWSRegion string
+ TofuInstanceID string
+ ExistingReference bool
+ Operator string
+}
+
+// Enroll uses operator-approved configuration, not values in the identity
+// request. H16/H17's worker confirmation producer belongs to region HA spec 4.
+func (d Directory) Enroll(ctx context.Context, sg release.Signer, o Observed, v boxid.Verifier, in EnrollmentRequest, raw []byte) (Observed, error) {
+ return d.enroll(ctx, sg, o, v, in, raw, false)
+}
+func (d Directory) enroll(ctx context.Context, sg release.Signer, o Observed, v boxid.Verifier, in EnrollmentRequest, raw []byte, automatic bool) (Observed, error) {
+ if (!automatic && in.Operator == "") || in.AWSRegion == "" || in.Reference == "" {
+ return Observed{}, ErrRoster
+ }
+ referenced, err := d.Referenced(ctx, in.Reference)
+ if err != nil {
+ return Observed{}, err
+ }
+ in.ExistingReference = in.ExistingReference || referenced
+ if automatic && referenced {
+ return Observed{}, ErrRoster
+ }
+ var rec boxid.Record
+ if json.Unmarshal(raw, &rec) != nil || rec.V != 1 || rec.Box != in.Reference {
+ return Observed{}, ErrRoster
+ }
+ // Verify the IMDS signature without asking the legacy bind object to grant
+ // enrollment. The label is bound by this root-signed roster write.
+ var doc struct {
+ InstanceID string `json:"instanceId"`
+ }
+ decoded, err := base64.StdEncoding.DecodeString(rec.IdentityDocument)
+ if err != nil {
+ return Observed{}, err
+ }
+ if json.Unmarshal(decoded, &doc) != nil || doc.InstanceID == "" {
+ return Observed{}, ErrRoster
+ }
+ proof := rec
+ proof.Box = doc.InstanceID
+ identity, err := v.Verify(proof, raw, nil)
+ if err != nil {
+ return Observed{}, err
+ }
+ if identity.Region != in.AWSRegion || (in.ExistingReference && in.TofuInstanceID != identity.InstanceID) {
+ return Observed{}, ErrRoster
+ }
+ cloud := Cloud{"aws", identity.AccountID, identity.Region, identity.InstanceID}
+ if old, exists := o.Roster.Boxes[in.Reference]; exists {
+ if old.Cloud == cloud && old.EvidenceSHA256 == hostfloor.Digest(raw) && old.Namespace == in.Namespace && old.Region == in.Region && old.Pool == in.Pool {
+ return o, nil
+ }
+ return Observed{}, ErrRoster
+ }
+ enrollment, err := d.enrollmentNonce(ctx, in, raw)
+ if err != nil {
+ return Observed{}, err
+ }
+ r := Clone(o.Roster)
+ r.Boxes[in.Reference] = Box{Enrollment: enrollment, Namespace: in.Namespace, BoxID: in.Reference, Label: in.Label, Region: in.Region, Pool: in.Pool, Cloud: cloud, EvidenceSHA256: hostfloor.Digest(raw), State: "active", LossIDs: []string{}}
+ return d.Replace(ctx, sg, o, r)
+}
+
+func (d Directory) EnrollAWS(ctx context.Context, sg release.Signer, o Observed, in EnrollmentRequest, raw []byte) (Observed, error) {
+ region, ok := o.Roster.Regions[in.Region]
+ if !ok || region.Account == "" || region.AWSRegion == "" {
+ return Observed{}, ErrRoster
+ }
+ certs, err := boxid.EmbeddedCerts()
+ if err != nil {
+ return Observed{}, err
+ }
+ in.AWSRegion = region.AWSRegion
+ return d.Enroll(ctx, sg, o, boxid.Verifier{Account: region.Account, Certs: certs}, in, raw)
+}
+```
+
+### Create `internal/hostactors/enrollment_confirmation.go`
+
+```go
+package hostactors
+
+import (
+ "context"
+ "encoding/json"
+ "github.com/carlosframework/platform/internal/boxid"
+ "github.com/carlosframework/platform/internal/release"
+ "time"
+)
+
+type Region struct {
+ Account string `json:"account"`
+ AWSRegion string `json:"aws_region"`
+ GroupName string `json:"group_name"`
+ GroupARN string `json:"group_arn"`
+}
+type TerminatedEnrollment struct {
+ Box string `json:"box"`
+ Enrollment string `json:"enrollment"`
+ InstanceID string `json:"instance_id"`
+ State string `json:"state"`
+ ObservedAt time.Time `json:"observed_at"`
+ RequestID string `json:"request_id"`
+}
+type MembershipConfirmation struct {
+ TargetPlacement string `json:"target_placement"`
+ AWSRegion string `json:"aws_region"`
+ GroupName string `json:"group_name"`
+ GroupARN string `json:"group_arn"`
+ InstanceID string `json:"instance_id"`
+ Account string `json:"account"`
+ AvailabilityZoneRegion string `json:"availability_zone_region"`
+ LifecycleState string `json:"lifecycle_state"`
+ InstanceState string `json:"instance_state"`
+ ObservedAt time.Time `json:"observed_at"`
+ RequestIDs []string `json:"request_ids"`
+ Confirmer struct {
+ Box string `json:"box"`
+ Enrollment string `json:"enrollment"`
+ Region string `json:"region"`
+ } `json:"confirmer"`
+ TerminatedEnrollments []TerminatedEnrollment `json:"terminated_enrollments"`
+}
+
+// Confirmers write this create-only evidence with read-only ASG/EC2 results.
+// This consumer gives H16/H17 their admission boundary; spec 4 owns producing it.
+func (d Directory) EnrollAutomatic(ctx context.Context, sg release.Signer, o Observed, v boxid.Verifier, in EnrollmentRequest, raw []byte, instance string, now time.Time) (Observed, error) {
+ region, ok := o.Roster.Regions[in.Region]
+ if !ok || region.Account != v.Account || region.AWSRegion != in.AWSRegion || region.GroupARN == "" || region.GroupName == "" || !segment(instance) || !segment(in.Reference) || in.ExistingReference {
+ return Observed{}, ErrRoster
+ }
+ body, _, err := d.Store.Get(ctx, "control/box-enrollment/"+in.Reference+"/"+instance+".json")
+ if err != nil {
+ return Observed{}, err
+ }
+ var c MembershipConfirmation
+ if json.Unmarshal(body, &c) != nil || c.TargetPlacement != in.Region || c.AWSRegion != region.AWSRegion || c.Account != region.Account || c.GroupARN != region.GroupARN || c.GroupName != region.GroupName || c.InstanceID != instance || c.AvailabilityZoneRegion != region.AWSRegion || (c.LifecycleState != "Pending" && c.LifecycleState != "InService") || (c.InstanceState != "pending" && c.InstanceState != "running") || !freshConfirmation(c.ObservedAt, now) || len(c.RequestIDs) < 2 {
+ return Observed{}, ErrRoster
+ }
+ for _, id := range c.RequestIDs {
+ if id == "" {
+ return Observed{}, ErrRoster
+ }
+ }
+ confirmer, ok := o.Roster.Boxes[c.Confirmer.Box]
+ if !ok || confirmer.State != "active" || confirmer.Enrollment != c.Confirmer.Enrollment || confirmer.Region != c.Confirmer.Region {
+ return Observed{}, ErrRoster
+ }
+ if confirmer.Region != in.Region {
+ seen := map[string]bool{}
+ for _, read := range c.TerminatedEnrollments {
+ b, ok := o.Roster.Boxes[read.Box]
+ if !ok || seen[read.Box] || b.Region != in.Region || b.Enrollment != read.Enrollment || b.Cloud.InstanceID != read.InstanceID || read.State != "terminated" || read.RequestID == "" || !freshConfirmation(read.ObservedAt, now) || read.ObservedAt.Before(c.ObservedAt.Add(-time.Minute)) {
+ return Observed{}, ErrRoster
+ }
+ seen[read.Box] = true
+ }
+ for ref, b := range o.Roster.Boxes {
+ if b.Region == in.Region && (b.State == "active" || b.State == "quarantined") && !seen[ref] {
+ return Observed{}, ErrRoster
+ }
+ }
+ }
+ // Pin the verified IMDS identity to the instance that membership proved.
+ in.TofuInstanceID = instance
+ in.ExistingReference = true
+ return d.enroll(ctx, sg, o, v, in, raw, true)
+}
+func freshConfirmation(at, now time.Time) bool {
+ return !at.IsZero() && now.Sub(at) <= time.Minute && !at.After(now.Add(5*time.Second))
+}
+```
+
+### Create `internal/hostactors/invitation.go`
+
+```go
+package hostactors
+
+import (
+ "context"
+ "crypto/rand"
+ "encoding/hex"
+ "errors"
+ "github.com/carlosframework/platform/internal/hostfloor"
+ "github.com/carlosframework/platform/internal/release"
+ "os"
+)
+
+type Invitation struct {
+ V int `json:"v"`
+ Bucket string `json:"bucket"`
+ Reference string `json:"reference"`
+ Enrollment string `json:"enrollment"`
+ EvidenceSHA256 string `json:"evidence_sha256"`
+}
+
+// Invite grants no membership. It gives an enrolled-identity candidate a
+// stable nonce with which to install its root guard and publish readiness.
+func (d Directory) Invite(ctx context.Context, sg release.Signer, reference string, evidence []byte) (Invitation, error) {
+ if !segment(reference) {
+ return Invitation{}, ErrRoster
+ }
+ key := "control/host-actor-invitations/" + reference + ".json"
+ raw, _, err := d.Store.Get(ctx, key)
+ if err == nil {
+ var invite Invitation
+ if release.VerifyRootEnvelope(d.PublicKey, raw, &invite) != nil || invite.V != 1 || invite.Bucket != d.Bucket || invite.Reference != reference || invite.EvidenceSHA256 != hostfloor.Digest(evidence) {
+ return Invitation{}, ErrRoster
+ }
+ return invite, nil
+ }
+ if !errors.Is(err, os.ErrNotExist) {
+ return Invitation{}, err
+ }
+ var id [16]byte
+ if _, err = rand.Read(id[:]); err != nil {
+ return Invitation{}, err
+ }
+ invite := Invitation{1, d.Bucket, reference, hex.EncodeToString(id[:]), hostfloor.Digest(evidence)}
+ raw, err = release.SignRootEnvelope(sg, invite)
+ if err != nil {
+ return Invitation{}, err
+ }
+ if _, err = d.Store.Put(ctx, key, raw, ""); err != nil {
+ return Invitation{}, err
+ }
+ return invite, nil
+}
+func (d Directory) enrollmentNonce(ctx context.Context, in EnrollmentRequest, evidence []byte) (string, error) {
+ key := "control/host-actor-invitations/" + in.Reference + ".json"
+ raw, _, err := d.Store.Get(ctx, key)
+ if err != nil {
+ return "", err
+ }
+ var invite Invitation
+ if release.VerifyRootEnvelope(d.PublicKey, raw, &invite) != nil || invite.V != 1 || invite.Bucket != d.Bucket || invite.Reference != in.Reference || !token.MatchString(invite.Enrollment) || invite.EvidenceSHA256 != hostfloor.Digest(evidence) {
+ return "", ErrRoster
+ }
+ return invite.Enrollment, nil
+}
+```
+
+### Create `internal/hostactors/lifecycle.go`
+
+```go
+package hostactors
+
+import (
+ "context"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostfloor"
+ "github.com/carlosframework/platform/internal/release"
+ "time"
+)
+
+func (d Directory) ReserveLifecycle(ctx context.Context, sg release.Signer, e Evidence, scope Scope, claim hostclaim.Observed, id, op string, now time.Time) (Observed, error) {
+ o, err := d.Read(ctx)
+ if err != nil {
+ return Observed{}, err
+ }
+ floor, err := hostfloor.Read(ctx, d.Store, d.PublicKey, d.Bucket)
+ if err != nil {
+ return Observed{}, err
+ }
+ reports, err := Census(ctx, o, e, d.PublicKey, floor, scope, now)
+ if err != nil {
+ return Observed{}, err
+ }
+ actors := map[string]string{}
+ for key, r := range reports {
+ actors[key] = r.Enrollment
+ }
+ return d.Reserve(ctx, sg, o, Reservation{ID: id, Host: claim.Host, OpID: op, ExpectedClaimETag: claim.ETag, FloorSHA256: floor.Digest, Actors: actors, Releases: reports}, false)
+}
+func (d Directory) ReleaseFinishedHold(ctx context.Context, sg release.Signer, id string) error {
+ o, err := d.Read(ctx)
+ if err != nil {
+ return err
+ }
+ h, ok := o.Roster.Holds[id]
+ if !ok {
+ return nil
+ }
+ claim, err := hostclaim.Read(ctx, d.Store, h.Host)
+ if err != nil {
+ return err
+ }
+ if claim.Absent || claim.ETag == h.ExpectedClaimETag {
+ return ErrRoster
+ }
+ if !claim.Claim.Free() && claim.Claim.Op.Kind != hostclaim.KindServing {
+ return ErrRoster
+ }
+ next := Clone(o.Roster)
+ delete(next.Holds, id)
+ _, err = d.Replace(ctx, sg, o, next)
+ return err
+}
+```
+
+### Create `internal/hostactors/membership.go`
+
+```go
+package hostactors
+
+import (
+ "context"
+ "github.com/carlosframework/platform/internal/hostfloor"
+ "reflect"
+ "time"
+)
+
+func (d Directory) checkMembership(ctx context.Context, old, next Roster) error {
+ if old.Publication == "" {
+ return nil
+ }
+ if next.FloorSHA256 != old.FloorSHA256 || next.Publication == "" {
+ return ErrRoster
+ }
+ changed := !reflect.DeepEqual(old.Boxes, next.Boxes) || !reflect.DeepEqual(old.Consoles, next.Consoles) || !reflect.DeepEqual(old.Regions, next.Regions)
+ if old.Publication == "preparing" {
+ if changed {
+ return ErrRoster
+ }
+ return nil
+ }
+ if old.Publication != "published" || next.Publication != "published" {
+ return ErrRoster
+ }
+ if !changed {
+ return nil
+ }
+ f, err := hostfloor.Read(ctx, d.Store, d.PublicKey, d.Bucket)
+ if err != nil || f.Digest != old.FloorSHA256 {
+ return ErrRoster
+ }
+ e := ExactEvidence{Store: d.Store, Boxes: d.PrivateBoxReport}
+ for ref, b := range next.Boxes {
+ previous, exists := old.Boxes[ref]
+ if exists && !(previous.State != "active" && b.State == "active") {
+ continue
+ }
+ report, err := d.PrivateBoxReport(ctx, ref, b)
+ if err != nil {
+ return err
+ }
+ if err = VerifyReport(ctx, e, d.PublicKey, f, report, b.Enrollment, b.Cloud.InstanceID, time.Now()); err != nil {
+ return err
+ }
+ }
+ for id, c := range next.Consoles {
+ if previous, exists := old.Consoles[id]; exists && reflect.DeepEqual(previous, c) {
+ continue
+ }
+ report, err := e.ConsoleReport(ctx, id, c)
+ if err != nil {
+ return err
+ }
+ if err = VerifyReport(ctx, e, d.PublicKey, f, report, c.Enrollment, id, time.Now()); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+```
+
+### Create `internal/hostactors/private.go`
+
+```go
+package hostactors
+
+import (
+ "context"
+ "encoding/json"
+ "github.com/carlosframework/platform/internal/status"
+ "strings"
+)
+
+func (d Directory) PrivateBoxReport(ctx context.Context, ref string, b Box) (Report, error) {
+ key := "status/boxes/" + ref + ".json"
+ if strings.HasPrefix(b.Namespace, "fleet:") {
+ fleet := strings.TrimPrefix(b.Namespace, "fleet:")
+ if !segment(fleet) || !segment(b.Label) {
+ return Report{}, ErrRoster
+ }
+ key = "fleets/" + fleet + "/status/boxes/" + b.Label + ".json"
+ }
+ raw, _, err := d.Store.Get(ctx, key)
+ if err != nil {
+ return Report{}, err
+ }
+ var box status.Box
+ if json.Unmarshal(raw, &box) != nil || box.Metrics == nil {
+ return Report{}, ErrRoster
+ }
+ m := box.Metrics
+ return Report{Component: "router", SystemChannel: m.SystemChannel, ActivationID: m.ActivationID, ActivationComplete: m.ActivationComplete, GeneratedAt: box.GeneratedAt, Actor: ref, Enrollment: m.Enrollment, InstanceID: m.InstanceID, Release: m.Release, ReleaseTag: m.ReleaseTag, Platform: m.Platform, BinarySHA256: m.BinarySHA256, MovesProtocol: m.MovesProtocol, BootID: m.BootID, FloorSHA256: m.FloorSHA256, FloorGuardReady: m.FloorGuardReady, HostClaimEnabled: m.HostClaimEnabled, RetirementReceiptSHA256: m.RetirementReceiptSHA256}, nil
+}
+```
+
+### Create `internal/hostactors/publication.go`
+
+```go
+package hostactors
+
+import (
+ "context"
+ "encoding/json"
+ "github.com/carlosframework/platform/internal/hostfloor"
+ "github.com/carlosframework/platform/internal/release"
+ "time"
+)
+
+// PublishFloor is called by the approved release workflow after the initial
+// deployment inventory has been reconciled and every guarded launch path
+// installed. It never discovers membership from reports.
+func (d Directory) PublishFloor(ctx context.Context, sg release.Signer, floor hostfloor.Floor, e Evidence, now time.Time) error {
+ o, err := d.Read(ctx)
+ if err != nil {
+ return err
+ }
+ if len(o.Roster.Boxes) == 0 || len(o.Roster.Consoles) == 0 || len(o.Roster.Holds) != 0 || len(o.Roster.Activations) != 0 {
+ return ErrRoster
+ }
+ raw, err := json.Marshal(floor)
+ if err != nil {
+ return err
+ }
+ prospective := hostfloor.Verified{Floor: floor, Digest: hostfloor.Digest(raw)}
+ if o.Roster.Publication == "" {
+ next := Clone(o.Roster)
+ next.Publication = "preparing"
+ next.FloorSHA256 = prospective.Digest
+ o, err = d.Replace(ctx, sg, o, next)
+ if err != nil {
+ return err
+ }
+ } else if o.Roster.FloorSHA256 != prospective.Digest {
+ return ErrRoster
+ }
+ for ref, b := range o.Roster.Boxes {
+ // Initial publication requires every enrollment. A retained quarantine
+ // is resolved by the activation runbook rather than skipped here.
+ if b.State != "active" {
+ return ErrRoster
+ }
+ r, err := e.BoxReport(ctx, ref, b)
+ if err != nil {
+ return err
+ }
+ if err = VerifyReport(ctx, e, d.PublicKey, prospective, r, b.Enrollment, b.Cloud.InstanceID, now); err != nil {
+ return err
+ }
+ }
+ for id, c := range o.Roster.Consoles {
+ r, err := e.ConsoleReport(ctx, id, c)
+ if err != nil {
+ return err
+ }
+ if err = VerifyReport(ctx, e, d.PublicKey, prospective, r, c.Enrollment, id, now); err != nil {
+ return err
+ }
+ }
+ // An inventory move during the scan is a failed publication attempt.
+ fresh, err := d.Read(ctx)
+ if err != nil {
+ return err
+ }
+ if fresh.ETag != o.ETag {
+ return ErrRoster
+ }
+ if err = hostfloor.Publish(ctx, d.Store, sg, d.Bucket, floor); err != nil {
+ return err
+ }
+ next := Clone(fresh.Roster)
+ next.Publication = "published"
+ _, err = d.Replace(ctx, sg, fresh, next)
+ return err
+}
+```
+
+### Create `internal/hostactors/references.go`
+
+```go
+package hostactors
+
+import (
+ "context"
+ "encoding/json"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "slices"
+ "strings"
+)
+
+func (d Directory) Referenced(ctx context.Context, reference string) (bool, error) {
+ keys, err := d.Store.List(ctx, "control/hosts/")
+ if err != nil {
+ return false, err
+ }
+ for _, key := range keys {
+ name := strings.TrimPrefix(key, "control/hosts/")
+ if strings.Contains(name, "/") {
+ continue
+ }
+ host, ok := strings.CutSuffix(name, ".json")
+ if !ok {
+ return false, ErrRoster
+ }
+ observed, err := hostclaim.Read(ctx, d.Store, host)
+ if err != nil {
+ return false, err
+ }
+ if observed.Absent {
+ return false, ErrRoster
+ }
+ c := observed.Claim
+ _, inventory := c.Inventory[reference]
+ if inventory || c.Op.Exec == reference || slices.Contains(c.Op.Participants, reference) || slices.Contains(c.Touched.Boxes, reference) || slices.Contains(c.Lost, reference) {
+ return true, nil
+ }
+ if c.Holder != nil {
+ if c.Holder.WriterBox == reference {
+ return true, nil
+ }
+ for _, a := range c.Holder.Admissions {
+ if a.Box == reference {
+ return true, nil
+ }
+ }
+ }
+ }
+ keys, err = d.Store.List(ctx, "control/edges/")
+ if err != nil {
+ return false, err
+ }
+ for _, key := range keys {
+ raw, _, err := d.Store.Get(ctx, key)
+ if err != nil {
+ return false, err
+ }
+ var edge struct {
+ BoxID string `json:"box_id"`
+ }
+ if json.Unmarshal(raw, &edge) != nil {
+ return false, ErrRoster
+ }
+ if edge.BoxID == reference || key == "control/edges/"+reference+".json" {
+ return true, nil
+ }
+ }
+ return false, nil
+}
+```
+
+### Create `internal/hostactors/reservations.go`
+
+```go
+package hostactors
+
+import (
+ "context"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostfloor"
+ "github.com/carlosframework/platform/internal/release"
+ "reflect"
+ "time"
+)
+
+// Start attaches the reservation in the same claim CAS as the start intent.
+func (d Directory) Start(ctx context.Context, o hostclaim.Observed, id string, tr func(*hostclaim.Claim) error) (hostclaim.Observed, error) {
+ roster, err := d.Read(ctx)
+ if err != nil {
+ return hostclaim.Observed{}, err
+ }
+ h, ok := roster.Roster.Holds[id]
+ if !ok || h.Host != o.Host || h.ExpectedClaimETag != o.ETag {
+ return hostclaim.Observed{}, ErrRoster
+ }
+ floor, err := hostfloor.Read(ctx, d.Store, d.PublicKey, d.Bucket)
+ if err != nil || floor.Digest != h.FloorSHA256 {
+ return hostclaim.Observed{}, ErrRoster
+ }
+ for _, report := range h.Releases {
+ if report.GeneratedAt.IsZero() || time.Since(report.GeneratedAt) > 15*time.Minute || report.GeneratedAt.After(time.Now().Add(5*time.Second)) {
+ return hostclaim.Observed{}, ErrRoster
+ }
+ }
+ return hostclaim.Update(ctx, d.Store, o, func(c *hostclaim.Claim) error {
+ if err := tr(c); err != nil {
+ return err
+ }
+ if c.Op.ID != h.OpID || c.Op.Intent == nil {
+ return ErrRoster
+ }
+ c.Op.Intent["actors_hold"] = id
+ return nil
+ })
+}
+
+// CancelHold invalidates the claim ETag before releasing a never-started hold.
+// If a delayed start won its CAS, the reread leaves its reservation held.
+func (d Directory) CancelHold(ctx context.Context, sg release.Signer, id string) error {
+ o, err := d.Read(ctx)
+ if err != nil {
+ return err
+ }
+ h, ok := o.Roster.Holds[id]
+ if !ok {
+ return nil
+ }
+ claim, err := hostclaim.Read(ctx, d.Store, h.Host)
+ if err != nil {
+ return err
+ }
+ if !claim.Absent && claim.Claim.Op.Intent["actors_hold"] == id {
+ return ErrRoster
+ }
+ if claim.ETag == h.ExpectedClaimETag {
+ _, err = hostclaim.Update(ctx, d.Store, claim, func(*hostclaim.Claim) error { return nil })
+ if err != nil {
+ return err
+ }
+ }
+ // Reread both records after the invalidating CAS. No timeout releases a hold.
+ fresh, err := d.Read(ctx)
+ if err != nil {
+ return err
+ }
+ if !reflect.DeepEqual(fresh.Roster.Holds[id], h) {
+ return ErrRoster
+ }
+ claim, err = hostclaim.Read(ctx, d.Store, h.Host)
+ if err != nil {
+ return err
+ }
+ if !claim.Absent && claim.Claim.Op.Intent["actors_hold"] == id {
+ return ErrRoster
+ }
+ r := Clone(fresh.Roster)
+ delete(r.Holds, id)
+ _, err = d.Replace(ctx, sg, fresh, r)
+ return err
+}
+func (d Directory) CheckHold(ctx context.Context, c *hostclaim.Claim, box, enrollment string) error {
+ if c == nil {
+ return ErrRoster
+ }
+ o, err := d.Read(ctx)
+ if err != nil {
+ return err
+ }
+ h, ok := o.Roster.Holds[c.Op.Intent["actors_hold"]]
+ if !ok || h.Host != c.Host || h.OpID != c.Op.ID || h.Actors["box:"+box] != enrollment {
+ return ErrRoster
+ }
+ return nil
+}
+```
+
+### Create `internal/hostactors/roster.go`
+
+```go
+package hostactors
+
+import (
+ "context"
+ "crypto/ed25519"
+ "encoding/json"
+ "errors"
+ "github.com/carlosframework/platform/internal/hostquarantine"
+ "github.com/carlosframework/platform/internal/release"
+ "math"
+ "reflect"
+ "regexp"
+ "slices"
+ "strings"
+)
+
+const Key = "control/host-actors.json"
+
+var ErrRoster = errors.New("hostactors: roster authority refused")
+var token = regexp.MustCompile(`^[0-9a-f]{32}$`)
+
+type Cloud struct {
+ Provider string `json:"provider"`
+ Account string `json:"account"`
+ Region string `json:"region"`
+ InstanceID string `json:"instance_id"`
+}
+type Box struct {
+ Enrollment string `json:"enrollment"`
+ Namespace string `json:"namespace"`
+ BoxID string `json:"box_id"`
+ Label string `json:"label"`
+ Region string `json:"region"`
+ Pool string `json:"pool"`
+ Cloud Cloud `json:"cloud"`
+ EvidenceSHA256 string `json:"evidence_sha256"`
+ State string `json:"state"`
+ QuarantineRevision uint64 `json:"quarantine_revision"`
+ LossIDs []string `json:"loss_ids"`
+}
+type Console struct {
+ Enrollment string `json:"enrollment"`
+ InstanceID string `json:"instance_id"`
+ Namespaces []string `json:"namespaces"`
+ DeploymentBucket string `json:"deployment_bucket"`
+}
+type Reservation struct {
+ ID string `json:"id"`
+ Host string `json:"host"`
+ OpID string `json:"op_id"`
+ ExpectedClaimETag string `json:"expected_claim_etag"`
+ FloorSHA256 string `json:"floor_sha256"`
+ Actors map[string]string `json:"actors"`
+ Releases map[string]Report `json:"releases"`
+ Hashes map[string][]string `json:"hashes,omitempty"`
+}
+type Roster struct {
+ Publication string `json:"publication,omitempty"`
+ FloorSHA256 string `json:"floor_sha256,omitempty"`
+ Regions map[string]Region `json:"regions,omitempty"`
+ V int `json:"v"`
+ DeploymentBucket string `json:"deployment_bucket"`
+ Revision uint64 `json:"revision"`
+ Boxes map[string]Box `json:"boxes"`
+ Consoles map[string]Console `json:"consoles"`
+ Holds map[string]Reservation `json:"holds"`
+ Activations map[string]Reservation `json:"activations"`
+}
+type Observed struct {
+ Roster Roster
+ ETag string
+}
+type Directory struct {
+ Store release.Store
+ PublicKey ed25519.PublicKey
+ Bucket string
+}
+
+func segment(s string) bool {
+ return s != "" && s != "." && s != ".." && !strings.ContainsAny(s, "/\\\x00")
+}
+func (r Roster) Validate(bucket string) error {
+ if r.V != 1 || bucket == "" || r.DeploymentBucket != bucket || r.Revision == 0 || r.Boxes == nil || r.Consoles == nil || r.Holds == nil || r.Activations == nil {
+ return ErrRoster
+ }
+ if r.Publication != "" && r.Publication != "preparing" && r.Publication != "published" {
+ return ErrRoster
+ }
+ if (r.Publication == "") != (r.FloorSHA256 == "") {
+ return ErrRoster
+ }
+ ids := map[string]bool{}
+ clouds := map[Cloud]bool{}
+ for key, b := range r.Boxes {
+ if !segment(key) || b.BoxID != key || !token.MatchString(b.Enrollment) || ids[b.Enrollment] || b.Namespace == "" || b.Label == "" || b.Region == "" || b.Pool == "" || b.Cloud.Provider != "aws" || b.Cloud.Account == "" || b.Cloud.Region == "" || b.Cloud.InstanceID == "" || len(b.EvidenceSHA256) != 64 || clouds[b.Cloud] {
+ return ErrRoster
+ }
+ if b.State != "active" && b.State != "quarantined" && b.State != "retired" {
+ return ErrRoster
+ }
+ if b.State == "quarantined" && (b.QuarantineRevision == 0 || len(b.LossIDs) == 0) {
+ return ErrRoster
+ }
+ ids[b.Enrollment] = true
+ clouds[b.Cloud] = true
+ }
+ for id, c := range r.Consoles {
+ if !segment(id) || !token.MatchString(c.Enrollment) || ids[c.Enrollment] || c.InstanceID != id || c.DeploymentBucket != bucket || len(c.Namespaces) == 0 {
+ return ErrRoster
+ }
+ ids[c.Enrollment] = true
+ }
+ for _, reservations := range []map[string]Reservation{r.Holds, r.Activations} {
+ for id, h := range reservations {
+ if !segment(id) || h.ID != id || len(h.FloorSHA256) != 64 || len(h.Actors) == 0 || len(h.Releases) != len(h.Actors) {
+ return ErrRoster
+ }
+ for key, enrollment := range h.Actors {
+ expected := ""
+ if ref, ok := strings.CutPrefix(key, "box:"); ok {
+ expected = r.Boxes[ref].Enrollment
+ }
+ if ref, ok := strings.CutPrefix(key, "console:"); ok {
+ expected = r.Consoles[ref].Enrollment
+ }
+ if expected != enrollment || !ids[enrollment] || h.Releases[key].Enrollment != enrollment {
+ return ErrRoster
+ }
+ }
+ }
+ }
+ for _, h := range r.Holds {
+ for _, a := range r.Activations {
+ if overlaps(h, a) {
+ return ErrRoster
+ }
+ }
+ }
+ return nil
+}
+func (d Directory) Read(ctx context.Context) (Observed, error) {
+ if d.Store == nil {
+ return Observed{}, ErrRoster
+ }
+ raw, etag, err := d.Store.Get(ctx, Key)
+ if err != nil {
+ return Observed{}, err
+ }
+ var r Roster
+ if etag == "" || release.VerifyRootEnvelope(d.PublicKey, raw, &r) != nil || r.Validate(d.Bucket) != nil {
+ return Observed{}, ErrRoster
+ }
+ return Observed{r, etag}, nil
+}
+func (d Directory) Lookup(ctx context.Context, box string) (hostquarantine.Actor, error) {
+ o, err := d.Read(ctx)
+ if err != nil {
+ return hostquarantine.Actor{}, err
+ }
+ b, ok := o.Roster.Boxes[box]
+ if !ok {
+ return hostquarantine.Actor{}, ErrRoster
+ }
+ return hostquarantine.Actor{Enrollment: b.Enrollment, State: b.State, QuarantineRevision: b.QuarantineRevision}, nil
+}
+func (d Directory) Replace(ctx context.Context, sg release.Signer, before Observed, next Roster) (Observed, error) {
+ if before.ETag == "" || before.Roster.Revision == math.MaxUint64 {
+ return Observed{}, ErrRoster
+ }
+ if err := d.checkMembership(ctx, before.Roster, next); err != nil {
+ return Observed{}, err
+ }
+ next.Revision = before.Roster.Revision + 1
+ for ref, old := range before.Roster.Boxes {
+ b, ok := next.Boxes[ref]
+ if !ok {
+ return Observed{}, ErrRoster
+ }
+ a, z := old, b
+ a.State = ""
+ z.State = ""
+ a.QuarantineRevision = 0
+ z.QuarantineRevision = 0
+ a.LossIDs = nil
+ z.LossIDs = nil
+ if !reflect.DeepEqual(a, z) || b.QuarantineRevision < old.QuarantineRevision {
+ return Observed{}, ErrRoster
+ }
+ if old.State == "retired" && b.State != "retired" {
+ return Observed{}, ErrRoster
+ }
+ }
+ if next.Validate(d.Bucket) != nil {
+ return Observed{}, ErrRoster
+ }
+ wire, err := release.SignRootEnvelope(sg, next)
+ if err != nil {
+ return Observed{}, err
+ }
+ etag, err := d.Store.Put(ctx, Key, wire, before.ETag)
+ if err != nil {
+ return Observed{}, err
+ }
+ return Observed{next, etag}, nil
+}
+func Clone(r Roster) Roster {
+ b, _ := json.Marshal(r)
+ var out Roster
+ _ = json.Unmarshal(b, &out)
+ return out
+}
+func (d Directory) Quarantine(ctx context.Context, sg release.Signer, o Observed, box, enrollment, loss string) (Observed, error) {
+ r := Clone(o.Roster)
+ b, ok := r.Boxes[box]
+ if !ok || b.Enrollment != enrollment || b.State == "retired" || loss == "" || b.QuarantineRevision == math.MaxUint64 {
+ return Observed{}, ErrRoster
+ }
+ if slices.Contains(b.LossIDs, loss) {
+ return o, nil
+ }
+ b.State = "quarantined"
+ b.QuarantineRevision++
+ b.LossIDs = append(b.LossIDs, loss)
+ r.Boxes[box] = b
+ return d.Replace(ctx, sg, o, r)
+}
+func (d Directory) Resolve(ctx context.Context, name string) (string, Box, error) {
+ o, err := d.Read(ctx)
+ if err != nil {
+ return "", Box{}, err
+ }
+ if b, ok := o.Roster.Boxes[name]; ok {
+ return name, b, nil
+ }
+ var ref string
+ var result Box
+ for key, b := range o.Roster.Boxes {
+ if b.Label == name && b.State != "retired" {
+ if ref != "" {
+ return "", Box{}, ErrRoster
+ }
+ ref = key
+ result = b
+ }
+ }
+ if ref == "" {
+ return "", Box{}, ErrRoster
+ }
+ return ref, result, nil
+}
+func overlaps(a, b Reservation) bool {
+ for _, x := range a.Actors {
+ for _, y := range b.Actors {
+ if x == y {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+func (d Directory) Initialize(ctx context.Context, sg release.Signer) error {
+ r := Roster{V: 1, DeploymentBucket: d.Bucket, Revision: 1, Boxes: map[string]Box{}, Consoles: map[string]Console{}, Holds: map[string]Reservation{}, Activations: map[string]Reservation{}}
+ if err := r.Validate(d.Bucket); err != nil {
+ return err
+ }
+ wire, err := release.SignRootEnvelope(sg, r)
+ if err != nil {
+ return err
+ }
+ return release.CreateExact(ctx, d.Store, Key, wire)
+}
+```
+
+**Step 3: apply the exact existing-file patch.**
+
+```diff
+--- a/internal/status/snapshot.go
++++ b/internal/status/snapshot.go
+@@ -41,0 +42,12 @@
++ ActivationID string
++ ActivationComplete bool
++ Enrollment string
++ MovesProtocol int
++ ReleaseTag string
++ BinarySHA256 string
++ BootID string
++ FloorSHA256 string
++ FloorGuardReady bool
++ HostClaimEnabled bool
++ RetirementReceiptSHA256 string
++
+@@ -213,0 +226,12 @@
++ ActivationID string `json:"activation_id,omitempty"`
++ ActivationComplete bool `json:"activation_complete,omitempty"`
++ Enrollment string `json:"enrollment,omitempty"`
++ MovesProtocol int `json:"moves_protocol,omitempty"`
++ ReleaseTag string `json:"release_tag,omitempty"`
++ BinarySHA256 string `json:"binary_sha256,omitempty"`
++ BootID string `json:"boot_id,omitempty"`
++ FloorSHA256 string `json:"floor_sha256,omitempty"`
++ FloorGuardReady bool `json:"floor_guard_ready,omitempty"`
++ HostClaimEnabled bool `json:"host_claim_enabled,omitempty"`
++ RetirementReceiptSHA256 string `json:"retirement_receipt_sha256,omitempty"`
++
+@@ -702,0 +727,11 @@
++ ActivationID: p.ActivationID, ActivationComplete: p.ActivationComplete,
++ Enrollment: p.Enrollment,
++ MovesProtocol: p.MovesProtocol,
++ ReleaseTag: p.ReleaseTag,
++ BinarySHA256: p.BinarySHA256,
++ BootID: p.BootID,
++ FloorSHA256: p.FloorSHA256,
++ FloorGuardReady: p.FloorGuardReady,
++ HostClaimEnabled: p.HostClaimEnabled,
++ RetirementReceiptSHA256: p.RetirementReceiptSHA256,
++
+```
+
+**Step 4: verify the task.**
+
+```sh
+go test ./internal/hostactors -count=1
+```
+
+## Task 3: Root arming, pre-exec checks and update/controller exclusion
+
+Install the persistent checker explicitly before activation. Ordinary bootstrap does not install a missing policy as unpublished. Guard-arm holds the existing update lock, validates current and recovery bytes and rejects an outstanding journal before arming. The drop-ins are separate from the unit files copied or restored by system apply. Console deployments pass --console with a root-owned current image and its .prev; a console cannot report guard readiness without the matching persistent drop-in. System apply persists its signed activation snapshot, checks every selected image before running it and keeps rejected recovery evidence. CheckLocal and the existing controller lifetime/live-lock rules stay in force.
+
+**Step 1: add the failing tests.**
+
+### Create `internal/host/guardtransaction_test.go`
+
+```go
+package host
+
+import (
+ "context"
+ "github.com/carlosframework/platform/internal/hostfloor"
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+func TestFloorRefusalKeepsRecoveryJournalAndExecutesNothing(t *testing.T) {
+ for _, phase := range []string{systemPhaseBackingUp, systemPhaseInstalled, systemPhaseGated} {
+ t.Run(phase, func(t *testing.T) {
+ f := newApplyFixture(t, "stable", "old-build")
+ f.a.floor = &hostfloor.Disk{Dir: t.TempDir(), Bucket: "bucket", PublicKey: f.pub}
+ _ = os.MkdirAll(f.a.rootDir(), 0700)
+ if err := f.a.journal(systemJournal{Phase: phase, Target: "new-build", PrevBuild: "old-build", StartedAt: f.now}); err != nil {
+ t.Fatal(err)
+ }
+ if err := f.a.Run(context.Background(), true); err == nil {
+ t.Fatal("missing policy accepted")
+ }
+ if _, err := os.Stat(filepath.Join(f.a.rootDir(), "journal.json")); err != nil {
+ t.Fatal("journal discarded", err)
+ }
+ if len(f.calls) != 0 {
+ t.Fatal("ran before guard", f.calls)
+ }
+ })
+ }
+}
+```
+
+Run this now and expect failure before the implementation is present:
+
+```sh
+go test ./internal/host -run 'TestFloorRefusal|TestApply|TestRecovery' -count=1
+go test ./internal/hostexec -count=1
+```
+
+**Step 2: add the implementation.**
+
+### Create `internal/host/guardroster.go`
+
+```go
+package host
+
+import (
+ "context"
+ "crypto/ed25519"
+ "github.com/carlosframework/platform/internal/hostactors"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+func readGuardRoster(ctx context.Context, s release.Store, pub ed25519.PublicKey, bucket string) (hostactors.Roster, error) {
+ o, err := (hostactors.Directory{Store: s, PublicKey: pub, Bucket: bucket}).Read(ctx)
+ return o.Roster, err
+}
+```
+
+### Create `internal/host/guardtransaction.go`
+
+```go
+package host
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "github.com/carlosframework/platform/internal/hostactors"
+ "github.com/carlosframework/platform/internal/hostfloor"
+ "github.com/carlosframework/platform/internal/release"
+ "os"
+ "path/filepath"
+ "slices"
+ "time"
+)
+
+type guardTransaction struct {
+ Roster json.RawMessage `json:"roster"`
+ ID string `json:"id"`
+ Actor string `json:"actor"`
+ Enrollment string `json:"enrollment"`
+}
+
+func (a *systemApplier) guarded() bool { return a.floor != nil }
+func (a *systemApplier) guardImage(path string) error {
+ if !a.guarded() {
+ return nil
+ }
+ f, err := hostfloor.OpenProtected(path)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ hash, err := a.floor.VerifyFile(f, a.platform)
+ if err != nil {
+ return err
+ }
+ p, _, err := a.floor.Load()
+ if err != nil || p.Mode == "unpublished" {
+ return err
+ }
+ tx, err := a.guardTransaction()
+ if err != nil {
+ if os.IsNotExist(err) {
+ if j, e := readSystemJournal(a.optDir); e == nil && j == nil && path == systemBinaryPath(a.optDir) {
+ return nil
+ }
+ }
+ return err
+ }
+ var roster hostactors.Roster
+ if release.VerifyRootEnvelope(a.floor.PublicKey, tx.Roster, &roster) != nil || roster.Validate(a.floor.Bucket) != nil {
+ return hostfloor.ErrFloor
+ }
+ reservation, ok := roster.Activations[tx.ID]
+ _, floor, err := a.floor.Load()
+ if err != nil {
+ return err
+ }
+ if !ok || reservation.FloorSHA256 != floor.Digest || reservation.Actors[tx.Actor] != tx.Enrollment || !slices.Contains(reservation.Hashes[tx.Actor], hash) {
+ return hostfloor.ErrFloor
+ }
+ return nil
+}
+func (a *systemApplier) guardTransaction() (guardTransaction, error) {
+ raw, err := os.ReadFile(filepath.Join(a.floor.Dir, "activation.json"))
+ if err != nil {
+ return guardTransaction{}, err
+ }
+ var tx guardTransaction
+ if json.Unmarshal(raw, &tx) != nil || tx.ID == "" || tx.Enrollment == "" || tx.Actor == "" {
+ return guardTransaction{}, hostfloor.ErrFloor
+ }
+ return tx, nil
+}
+func (a *systemApplier) beginGuard(ctx context.Context, target *release.SystemPointer) error {
+ if !a.guarded() {
+ return nil
+ }
+ p, _, err := a.floor.Load()
+ if err != nil {
+ return err
+ }
+ if p.Mode == "unpublished" {
+ return nil
+ }
+ if err := a.prepareGuard(ctx, target); err != nil {
+ return err
+ }
+ tx, err := a.guardTransaction()
+ if err != nil {
+ return err
+ }
+ // The signed reservation is persisted by the root approval step, never by
+ // the worker's request. A new transaction must still find it in the roster.
+ store, pub, err := a.openStore(ctx)
+ if err != nil {
+ return err
+ }
+ _, local, err := a.floor.Load()
+ if err != nil {
+ return err
+ }
+ remote, err := hostfloor.Read(ctx, store, pub, a.floor.Bucket)
+ if err != nil || remote.Digest != local.Digest {
+ return hostfloor.ErrFloor
+ }
+ d := hostactors.Directory{Store: store, PublicKey: pub, Bucket: a.floor.Bucket}
+ live, err := d.Read(ctx)
+ if err != nil {
+ return err
+ }
+ h, ok := live.Roster.Activations[tx.ID]
+ if !ok || h.Actors[tx.Actor] != tx.Enrollment {
+ return hostfloor.ErrFloor
+ }
+ var saved hostactors.Roster
+ if release.VerifyRootEnvelope(pub, tx.Roster, &saved) != nil {
+ return hostfloor.ErrFloor
+ }
+ x, _ := json.Marshal(h)
+ y, _ := json.Marshal(saved.Activations[tx.ID])
+ if string(x) != string(y) {
+ return hostfloor.ErrFloor
+ }
+ return a.floor.Begin(tx.ID, a.dataDir)
+}
+func (a *systemApplier) checkGuardRecovery() error {
+ if !a.guarded() {
+ return nil
+ }
+ p, _, err := a.floor.Load()
+ if err != nil {
+ return err
+ }
+ if p.Mode == "unpublished" {
+ return nil
+ }
+ j, err := readSystemJournal(a.optDir)
+ if err != nil {
+ return err
+ }
+ if j == nil {
+ return nil
+ }
+ if _, err = a.guardTransaction(); err != nil {
+ return err
+ }
+ if _, err = os.Stat(filepath.Join(a.floor.Dir, "update-begun.json")); err != nil {
+ return err
+ }
+ return a.guardImage(systemPrevPath(a.optDir))
+}
+func (a *systemApplier) endGuard() error {
+ if !a.guarded() {
+ return nil
+ }
+ tx, err := a.guardTransaction()
+ if err != nil {
+ return err
+ }
+ if err = hostfloor.RemoveDurable(filepath.Join(a.floor.Dir, "update-begun.json")); err != nil {
+ return err
+ }
+ return hostfloor.Durable(filepath.Join(a.floor.Dir, "completed.json"), []byte(tx.ID), 0644)
+}
+func (a *systemApplier) guardRun(ctx context.Context, wait time.Duration, name string, args ...string) (string, error) {
+ if !a.guarded() {
+ return a.ops.run(ctx, wait, name, args...)
+ }
+ if err := a.guardImage(name); err != nil {
+ return "", err
+ }
+ bounded, cancel := context.WithTimeout(ctx, wait)
+ defer cancel()
+ return a.floor.Run(bounded, name, a.platform, args...)
+}
+func installFloorHook(a *systemApplier) error {
+ dir := filepath.Join(a.optDir, "system", "guard")
+ if _, err := os.Stat(dir); errors.Is(err, os.ErrNotExist) {
+ return nil
+ } else if err != nil {
+ return err
+ }
+ d, err := floorDisk(a.optDir)
+ if err != nil {
+ return err
+ }
+ a.floor = &d
+ _, _, err = d.Load()
+ return err
+}
+
+func (a *systemApplier) prepareGuard(ctx context.Context, target *release.SystemPointer) error {
+ enrollment, err := hostfloor.ReadEnrollment(filepath.Join(a.etcDir, "carlos", "host-actor.json"))
+ if err != nil {
+ return err
+ }
+ store, pub, err := a.openStore(ctx)
+ if err != nil {
+ return err
+ }
+ raw, _, err := store.Get(ctx, hostactors.Key)
+ if err != nil {
+ return err
+ }
+ var roster hostactors.Roster
+ if release.VerifyRootEnvelope(pub, raw, &roster) != nil || roster.Validate(a.floor.Bucket) != nil {
+ return hostfloor.ErrFloor
+ }
+ b, ok := roster.Boxes[enrollment.Box]
+ if !ok || b.Enrollment != enrollment.Enrollment || b.Cloud.InstanceID != enrollment.InstanceID {
+ return hostfloor.ErrFloor
+ }
+ actor := "box:" + enrollment.Box
+ id := ""
+ for name, h := range roster.Activations {
+ hashes := h.Hashes[actor]
+ if h.Actors[actor] == enrollment.Enrollment && len(hashes) == 3 && hashes[1] == target.Current.Artifacts[a.platform].SHA256 {
+ if id != "" {
+ return hostfloor.ErrFloor
+ }
+ id = name
+ }
+ }
+ if id == "" {
+ return hostfloor.ErrFloor
+ }
+ identityKey, err := release.IdentityKey(target.Current.ReleaseTag)
+ if err != nil {
+ return err
+ }
+ identity, _, err := store.Get(ctx, identityKey)
+ if err != nil {
+ return err
+ }
+ _, floor, err := a.floor.Load()
+ if err != nil {
+ return err
+ }
+ if err = hostfloor.CheckImage(pub, floor, identity, target.Current.Version, a.platform, target.Current.Artifacts[a.platform].SHA256); err != nil {
+ return err
+ }
+ if err = hostfloor.Durable(filepath.Join(a.floor.Dir, "identities", target.Current.ReleaseTag+".json"), identity, 0644); err != nil {
+ return err
+ }
+ txRaw, _ := json.Marshal(guardTransaction{raw, id, actor, enrollment.Enrollment})
+ return hostfloor.Durable(filepath.Join(a.floor.Dir, "activation.json"), txRaw, 0644)
+}
+```
+
+### Create `internal/host/guardexec.go`
+
+```go
+package host
+
+import (
+ "context"
+ "errors"
+ "github.com/carlosframework/platform/internal/hostfloor"
+ "github.com/carlosframework/platform/internal/release"
+ "log"
+ "os"
+ "path/filepath"
+ "runtime"
+ "time"
+)
+
+func floorDisk(optDir string) (hostfloor.Disk, error) {
+ pub, err := release.ParsePublicKey(os.Getenv("CARLOS_RELEASE_PUBKEY"), "CARLOS_RELEASE_PUBKEY")
+ if err != nil {
+ return hostfloor.Disk{}, err
+ }
+ bucket := os.Getenv("CARLOS_DEPLOYMENT_BUCKET")
+ if bucket == "" {
+ return hostfloor.Disk{}, hostfloor.ErrFloor
+ }
+ return hostfloor.Disk{Dir: filepath.Join(optDir, "system", "guard"), Bucket: bucket, PublicKey: pub}, nil
+}
+func RunSystemGuardExec(args []string) {
+ if len(args) < 1 {
+ log.Fatal(hostfloor.ErrFloor)
+ }
+ d, err := floorDisk(release.OptDir())
+ if err != nil {
+ log.Fatal(err)
+ }
+ policy, local, err := d.Load()
+ if err != nil {
+ log.Fatal(err)
+ }
+ if policy.Mode != "unpublished" {
+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+ store, pub, e := openSystemStore(ctx)
+ if e == nil {
+ e = hostfloor.CheckRemote(ctx, store, pub, d.Bucket, local)
+ }
+ cancel()
+ if e != nil && !hostfloor.Offline(e) {
+ log.Fatal(e)
+ }
+ }
+ current := systemBinaryPath(release.OptDir())
+ candidate := current
+ component := "router"
+ switch args[0] {
+ case "apply", "recover":
+ _, err := os.Stat(filepath.Join(systemRootDir(release.OptDir()), "journal.json"))
+ if err == nil {
+ candidate = systemPrevPath(release.OptDir())
+ } else if !errors.Is(err, os.ErrNotExist) {
+ log.Fatal(err)
+ }
+ mode := args[0]
+ args = []string{"system", "apply"}
+ if mode == "recover" {
+ args = append(args, "--recover")
+ }
+ case "console":
+ if len(args) < 2 {
+ log.Fatal(hostfloor.ErrFloor)
+ }
+ component = "console"
+ candidate = args[1]
+ args = args[2:]
+ case "edge":
+ args = []string{"agent"}
+ default:
+ log.Fatal(hostfloor.ErrFloor)
+ }
+ if err = d.Exec(candidate, runtime.GOOS+"-"+runtime.GOARCH, args, os.Environ(), component); err != nil {
+ log.Fatal(err)
+ }
+}
+```
+
+### Create `internal/host/guardarm.go`
+
+```go
+package host
+
+import (
+ "context"
+ "encoding/json"
+ "flag"
+ "github.com/carlosframework/platform/internal/hostactors"
+ "github.com/carlosframework/platform/internal/hostfloor"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/version"
+ "log"
+ "os"
+ "path/filepath"
+ "runtime"
+)
+
+func RunSystemGuardArm(args []string) {
+ fs := flag.NewFlagSet("system guard-arm", flag.ExitOnError)
+ consolePath := fs.String("console", "", "")
+ policyPath := fs.String("policy", "", "")
+ identityPath := fs.String("identity", "", "")
+ enrollmentPath := fs.String("enrollment", "", "")
+ _ = fs.Parse(args)
+ if os.Geteuid() != 0 || *policyPath == "" || *identityPath == "" || *enrollmentPath == "" {
+ log.Fatal(hostfloor.ErrFloor)
+ }
+ lockDir := systemRootDir(release.OptDir())
+ if err := os.MkdirAll(lockDir, 0755); err != nil {
+ log.Fatal(err)
+ }
+ lock, err := os.OpenFile(filepath.Join(lockDir, systemLockFile), os.O_CREATE|os.O_RDWR, 0600)
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer lock.Close()
+ if err = flockExclusiveNonblock(lock); err != nil {
+ log.Fatal(err)
+ }
+ d, err := floorDisk(release.OptDir())
+ if err != nil {
+ log.Fatal(err)
+ }
+ raw, err := os.ReadFile(*policyPath)
+ if err != nil {
+ log.Fatal(err)
+ }
+ var p hostfloor.Policy
+ if json.Unmarshal(raw, &p) != nil || p.Mode != "armed" {
+ log.Fatal(hostfloor.ErrFloor)
+ }
+ f, err := p.Verify(d.PublicKey, d.Bucket)
+ if err != nil {
+ log.Fatal(err)
+ }
+ identity, err := os.ReadFile(*identityPath)
+ if err != nil {
+ log.Fatal(err)
+ }
+ hash, err := hostfloor.ExecutingHash()
+ if err != nil {
+ log.Fatal(err)
+ }
+ platform := runtime.GOOS + "-" + runtime.GOARCH
+ if err = hostfloor.CheckImage(d.PublicKey, f, identity, version.Build, platform, hash); err != nil {
+ log.Fatal(err)
+ }
+ if _, err = os.Stat(filepath.Join(systemRootDir(release.OptDir()), "journal.json")); !os.IsNotExist(err) {
+ log.Fatal(hostfloor.ErrFloor)
+ }
+ for _, path := range []string{systemBinaryPath(release.OptDir()), systemPrevPath(release.OptDir())} {
+ candidate, err := hostfloor.OpenProtected(path)
+ if err != nil {
+ log.Fatal(err)
+ }
+ b, err := os.ReadFile(candidate.Name())
+ candidate.Close()
+ if err != nil || hostfloor.CheckImage(d.PublicKey, f, identity, version.Build, platform, hostfloor.Digest(b)) != nil {
+ log.Fatal(hostfloor.ErrFloor)
+ }
+ }
+ // Enrollment input is reporting configuration, verified against the signed
+ // directory before root persists it. It cannot grant roster membership.
+ enrollment, err := hostfloor.ReadEnrollment(*enrollmentPath)
+ if err != nil {
+ log.Fatal(err)
+ }
+ store, pub, err := openSystemStore(context.Background())
+ if err != nil {
+ log.Fatal(err)
+ }
+ roster, err := readGuardRoster(context.Background(), store, pub, d.Bucket)
+ if err != nil {
+ log.Fatal(err)
+ }
+ b, ok := roster.Boxes[enrollment.Box]
+ if ok {
+ if b.Enrollment != enrollment.Enrollment || b.Cloud.InstanceID != enrollment.InstanceID {
+ log.Fatal(hostfloor.ErrFloor)
+ }
+ } else {
+ // A post-floor join must arm and report before it becomes an actor.
+ // The invitation grants a reporting nonce, never execution authority.
+ wire, _, e := store.Get(context.Background(), "control/host-actor-invitations/"+enrollment.Box+".json")
+ if e != nil {
+ log.Fatal(e)
+ }
+ var invitation hostactors.Invitation
+ if release.VerifyRootEnvelope(pub, wire, &invitation) != nil || invitation.Bucket != d.Bucket || invitation.Reference != enrollment.Box || invitation.Enrollment != enrollment.Enrollment {
+ log.Fatal(hostfloor.ErrFloor)
+ }
+ }
+ for _, dir := range []string{d.Dir, filepath.Join(d.Dir, "identities")} {
+ if err = os.MkdirAll(dir, 0755); err != nil {
+ log.Fatal(err)
+ }
+ }
+ path := filepath.Join(d.Dir, "policy.json")
+ if _, err = os.Stat(path); os.IsNotExist(err) {
+ initial, _ := json.Marshal(hostfloor.Policy{Schema: 1, DeploymentBucket: d.Bucket, Mode: "unpublished"})
+ if err = hostfloor.Durable(path, initial, 0644); err != nil {
+ log.Fatal(err)
+ }
+ }
+ // The checker is outside system/router and outside every restore set.
+ if err = copyFileDurable("/proc/self/exe", filepath.Join(d.Dir, "carlos"), 0755); err != nil {
+ log.Fatal(err)
+ }
+ snapshot, err := release.VerifySystemIdentity(d.PublicKey, identity)
+ if err != nil {
+ log.Fatal(err)
+ }
+ if err = hostfloor.Durable(filepath.Join(d.Dir, "identities", snapshot.Current.ReleaseTag+".json"), identity, 0644); err != nil {
+ log.Fatal(err)
+ }
+ enrollmentRaw, _ := json.Marshal(enrollment)
+ if err = hostfloor.Durable("/etc/carlos/host-actor.json", enrollmentRaw, 0644); err != nil {
+ log.Fatal(err)
+ }
+ dropins := map[string]string{
+ "carlos-system-update.service": "[Service]\nExecStart=\nExecStart=/opt/carlos/system/guard/carlos system guard-exec apply\n",
+ "carlos-system-recover.service": "[Service]\nExecStart=\nExecStart=/opt/carlos/system/guard/carlos system guard-exec recover\n",
+ "carlos-edge.service": "[Unit]\nRequires=carlos-system-recover.service\nAfter=carlos-system-recover.service\n[Service]\nExecStart=\nExecStart=/opt/carlos/system/guard/carlos system guard-exec edge\n",
+ }
+ if *consolePath != "" {
+ if !filepath.IsAbs(*consolePath) || filepath.Clean(*consolePath) != *consolePath {
+ log.Fatal(hostfloor.ErrFloor)
+ }
+ for _, path := range []string{*consolePath, *consolePath + ".prev"} {
+ file, e := hostfloor.OpenProtected(path)
+ if e != nil {
+ log.Fatal(e)
+ }
+ body, e := os.ReadFile(file.Name())
+ file.Close()
+ if e != nil || hostfloor.CheckComponent(d.PublicKey, f, identity, version.Build, platform, hostfloor.Digest(body), "console") != nil {
+ log.Fatal(hostfloor.ErrFloor)
+ }
+ }
+ dropins["console.service"] = "[Service]\nExecStart=\nExecStart=/opt/carlos/system/guard/carlos system guard-exec console " + *consolePath + "\n"
+ }
+ for unit, body := range dropins {
+ dir := filepath.Join("/etc/systemd/system", unit+".d")
+ if err = os.MkdirAll(dir, 0755); err != nil {
+ log.Fatal(err)
+ }
+ if err = hostfloor.Durable(filepath.Join(dir, "90-host-floor.conf"), []byte(body), 0644); err != nil {
+ log.Fatal(err)
+ }
+ }
+ if _, err = productionSysOps().systemctl("daemon-reload"); err != nil {
+ log.Fatal(err)
+ }
+ if *consolePath != "" {
+ if err = hostfloor.Durable(filepath.Join(d.Dir, "console-ready.json"), []byte(*consolePath), 0644); err != nil {
+ log.Fatal(err)
+ }
+ }
+ if err = d.Persist(p, map[string][]byte{snapshot.Current.ReleaseTag: identity}); err != nil {
+ log.Fatal(err)
+ }
+}
+```
+
+### Create `internal/host/claimreport.go`
+
+```go
+package host
+
+import (
+ "github.com/carlosframework/platform/internal/hostfloor"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/status"
+ "github.com/carlosframework/platform/internal/version"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+)
+
+func claimReport(p *status.Probes) {
+ d, err := floorDisk(release.OptDir())
+ if err != nil {
+ return
+ }
+ e, err := hostfloor.ReadEnrollment("/etc/carlos/host-actor.json")
+ if err != nil {
+ return
+ }
+ r, err := d.Running(version.Build, runtime.GOOS+"-"+runtime.GOARCH)
+ if err != nil {
+ return
+ }
+ if completed, err := os.ReadFile(filepath.Join(d.Dir, "completed.json")); err == nil && len(completed) == 32 {
+ p.ActivationID = string(completed)
+ p.ActivationComplete = true
+ }
+ p.Enrollment = e.Enrollment
+ p.ReleaseTag = r.ReleaseTag
+ p.BinarySHA256 = r.Hash
+ p.MovesProtocol = r.Protocol
+ p.BootID = r.BootID
+ p.FloorSHA256 = r.FloorSHA256
+ p.FloorGuardReady = r.GuardReady
+ p.HostClaimEnabled = strings.TrimSpace(os.Getenv("CARLOS_HOST_CLAIM")) != ""
+}
+```
+
+**Step 3: apply the exact existing-file patch.**
+
+```diff
+--- a/internal/host/systemapply.go
++++ b/internal/host/systemapply.go
+@@ -22,0 +23 @@
++ "github.com/carlosframework/platform/internal/hostfloor"
+@@ -98,0 +100,3 @@
++ }
++ if err := installFloorHook(a); err != nil {
++ log.Fatal(err)
+@@ -178 +182,2 @@
+- ops sysOps
++ floor *hostfloor.Disk
++ ops sysOps
+@@ -232 +237,9 @@
+- defer os.RemoveAll(a.stagingDir())
++ // Rejected floor recovery must retain staging and the journal.
++ if err := a.checkGuardRecovery(); err != nil {
++ return err
++ }
++ defer func() {
++ if j, e := readSystemJournal(a.optDir); e == nil && j == nil {
++ _ = os.RemoveAll(a.stagingDir())
++ }
++ }()
+@@ -286,0 +300,3 @@
++ }
++ if err := a.beginGuard(ctx, p); err != nil {
++ return err
+@@ -330,0 +347,3 @@
++ if err := a.guardImage(staged); err != nil {
++ return err
++ }
+@@ -420 +439 @@
+- return nil
++ return a.endGuard()
+@@ -450,0 +470,3 @@
++ if err := a.guardImage(systemBinaryPath(a.optDir)); err != nil {
++ return false, err
++ }
+@@ -458,0 +481,5 @@
++ if prevErr == nil {
++ if err := a.guardImage(prev); err != nil {
++ return false, err
++ }
++ }
+@@ -468 +495 @@
+- out, err := a.ops.run(ctx, systemVersionWait, systemPrevPath(a.optDir), "version")
++ out, err := a.guardRun(ctx, systemVersionWait, systemPrevPath(a.optDir), "version")
+@@ -484 +511 @@
+- out, err := a.ops.run(ctx, systemBootstrapWait, binary, "bootstrap", "-root", dir, "-plan-json")
++ out, err := a.guardRun(ctx, systemBootstrapWait, binary, "bootstrap", "-root", dir, "-plan-json")
+@@ -797,0 +825,3 @@
++ if err := a.endGuard(); err != nil {
++ return err
++ }
+@@ -808,0 +839,3 @@
++ if err := a.guardImage(prev); err != nil {
++ return bootstrapPlan{}, err
++ }
+@@ -967 +1000 @@
+- return true, nil
++ return true, a.endGuard()
+@@ -1048 +1081 @@
+- out, err := a.ops.run(ctx, systemVersionWait, systemBinaryPath(a.optDir), "version")
++ out, err := a.guardRun(ctx, systemVersionWait, systemBinaryPath(a.optDir), "version")
+@@ -1113 +1146 @@
+- out, err := a.ops.run(ctx, systemVersionWait, staged, "version")
++ out, err := a.guardRun(ctx, systemVersionWait, staged, "version")
+--- a/internal/hostexec/controller.go
++++ b/internal/hostexec/controller.go
+@@ -14,0 +15 @@
++ "github.com/carlosframework/platform/internal/hostfloor"
+@@ -17,0 +19 @@
++ "path/filepath"
+@@ -21,6 +23,8 @@
+- Binding Binding
+- Store release.Store
+- Guard hostguard.Guard
+- Directory hostquarantine.ActorDirectory
+- Clock Clock
+- LockDir string
++ Binding Binding
++ Store release.Store
++ Guard hostguard.Guard
++ Directory hostquarantine.ActorDirectory
++ Clock Clock
++ UpdateGuardDir string
++ UpdateJournal string
++ LockDir string
+@@ -64,0 +69,4 @@
++ if cfg.UpdateGuardDir == "" {
++ cfg.UpdateGuardDir = "/opt/carlos/system/guard"
++ cfg.UpdateJournal = filepath.Join("/opt/carlos/system/router", "journal.json")
++ }
+@@ -108,0 +117,5 @@
++ }
++ if cfg.UpdateGuardDir != "" {
++ if err := hostfloor.UpdateIdle(cfg.UpdateGuardDir, cfg.UpdateJournal); err != nil {
++ return err
++ }
+--- a/system.go
++++ b/system.go
+@@ -50 +50 @@
+- case "update", "status", "rollback", "apply", "antivirus", "help", "-h", "--help":
++ case "guard-arm", "guard-exec", "update", "status", "rollback", "apply", "antivirus", "help", "-h", "--help":
+@@ -58,0 +59,5 @@
++ case "guard-arm":
++ host.RunSystemGuardArm(rest)
++ case "guard-exec":
++ host.RunSystemGuardExec(rest)
++
+```
+
+**Step 4: verify the task.**
+
+```sh
+go test ./internal/host -run 'TestFloorRefusal|TestApply|TestRecovery' -count=1
+go test ./internal/hostexec -count=1
+```
+
+## Task 4: A live regional termination-evidence producer
+
+The box-side pass, behind CARLOS_HOST_CLAIM, uses read-only EC2 and the roster's cloud binding. It polls in-region and creates immutable worker evidence. It never calls the console for a signature and never stops or terminates compute. An EventBridge consumer can use Event only after validating the configured queue/rule provenance; region HA spec 3 owns that queue loop. Activity-history evidence stays unsupported. The complete running-time volume history is an input only to DestroyedLocal; the fence producer does not call it. Until L3/L4's claim representation is adopted, this task writes the verified fence but does not fabricate a claim loss record or discharge a chore.
+
+**Step 1: add the failing tests.**
+
+### Create `internal/hostfence/termination_test.go`
+
+```go
+package hostfence
+
+import (
+ "context"
+ "crypto/ed25519"
+ "crypto/rand"
+ "encoding/json"
+ "github.com/aws/aws-sdk-go-v2/aws"
+ awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
+ "github.com/aws/aws-sdk-go-v2/service/ec2"
+ "github.com/aws/aws-sdk-go-v2/service/ec2/types"
+ "github.com/carlosframework/platform/internal/awsinv"
+ "github.com/carlosframework/platform/internal/hostactors"
+ "github.com/carlosframework/platform/internal/release"
+ "strings"
+ "testing"
+ "time"
+)
+
+type terminatedEC2 struct {
+ state types.InstanceStateName
+ missing bool
+ owner string
+ after func()
+}
+
+func (f terminatedEC2) DescribeInstances(_ context.Context, in *ec2.DescribeInstancesInput, _ ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) {
+ out := &ec2.DescribeInstancesOutput{}
+ if !f.missing {
+ out.Reservations = []types.Reservation{{OwnerId: aws.String(f.owner), Instances: []types.Instance{{InstanceId: aws.String(in.InstanceIds[0]), State: &types.InstanceState{Name: f.state}}}}}
+ }
+ if f.after != nil {
+ f.after()
+ }
+ awsmiddleware.SetRequestIDMetadata(&out.ResultMetadata, "request")
+ return out, nil
+}
+func terminationFixture(t *testing.T) (TerminationWorker, release.Store) {
+ t.Helper()
+ ctx := context.Background()
+ pub, key, _ := ed25519.GenerateKey(rand.Reader)
+ s := release.NewDirStore(t.TempDir())
+ now := time.Now().UTC()
+ r := hostactors.Roster{V: 1, Revision: 1, DeploymentBucket: "bucket", Boxes: map[string]hostactors.Box{}, Consoles: map[string]hostactors.Console{}, Holds: map[string]hostactors.Reservation{}, Activations: map[string]hostactors.Reservation{}}
+ for i, ref := range []string{"worker", "dead"} {
+ en := strings.Repeat(string(rune('a'+i)), 32)
+ r.Boxes[ref] = hostactors.Box{Enrollment: en, Namespace: "platform", BoxID: ref, Label: ref, Region: "ie", Pool: "default", Cloud: hostactors.Cloud{Provider: "aws", Account: "123", Region: "eu-west-1", InstanceID: "i-" + ref}, EvidenceSHA256: strings.Repeat("e", 64), State: "active"}
+ }
+ raw, _ := release.SignRootEnvelope(release.RootSigner(key), r)
+ if _, err := s.Put(ctx, hostactors.Key, raw, ""); err != nil {
+ t.Fatal(err)
+ }
+ return TerminationWorker{Enabled: true, Directory: hostactors.Directory{Store: s, PublicKey: pub, Bucket: "bucket"}, Self: Worker{Box: "worker", Enrollment: r.Boxes["worker"].Enrollment}, Now: func() time.Time { return now }}, s
+}
+func TestTerminationAloneFencesAndAbsenceNeverDoes(t *testing.T) {
+ for _, test := range []struct {
+ name string
+ state types.InstanceStateName
+ missing bool
+ owner string
+ ok bool
+ }{
+ {"terminated-no-volumes", types.InstanceStateNameTerminated, false, "123", true},
+ {"stopped", types.InstanceStateNameStopped, false, "123", false},
+ {"shutting-down", types.InstanceStateNameShuttingDown, false, "123", false},
+ {"absent", types.InstanceStateNameTerminated, true, "123", false},
+ {"wrong-account", types.InstanceStateNameTerminated, false, "456", false},
+ } {
+ t.Run(test.name, func(t *testing.T) {
+ w, s := terminationFixture(t)
+ w.Reader = func(context.Context, string) (awsinv.ExactEC2API, error) {
+ return terminatedEC2{state: test.state, missing: test.missing, owner: test.owner}, nil
+ }
+ ref, err := w.Describe(context.Background(), "dead")
+ if (err == nil) != test.ok {
+ t.Fatal(ref, err)
+ }
+ if test.ok {
+ raw, _, err := s.Get(context.Background(), ref.Key)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var f Termination
+ _ = json.Unmarshal(raw, &f)
+ old := f.ObservedAt
+ w.Now = func() time.Time { return old.Add(time.Hour) }
+ again, err := w.Describe(context.Background(), "dead")
+ if err != nil || again != ref {
+ t.Fatal("retry moved evidence", again, err)
+ }
+ } else {
+ keys, _ := s.List(context.Background(), "control/box-fences/")
+ if len(keys) != 0 {
+ t.Fatal("refusal wrote a fence")
+ }
+ }
+ })
+ }
+}
+func TestDestroyedLocalIsIndependentAndNeedsHistory(t *testing.T) {
+ now := time.Now().UTC()
+ f := Termination{Box: "dead", Enrollment: "enrollment", ObservedAt: now}
+ if DestroyedLocal(f, nil, false, now.Add(-time.Hour), time.Hour) {
+ t.Fatal("empty terminated mappings proved destroyed storage")
+ }
+ r := VolumeRecord{V: 1, Box: f.Box, Enrollment: f.Enrollment, InstanceState: "running", ObservedAt: now.Add(-time.Minute), RequestID: "request", BlockDevices: []BlockDevice{{Device: "root", VolumeID: "volume", DeleteOnTermination: true, Status: "attached"}}}
+ if !DestroyedLocal(f, []VolumeRecord{r}, true, r.ObservedAt, time.Hour) {
+ t.Fatal("complete true history refused")
+ }
+ r.BlockDevices[0].DeleteOnTermination = false
+ if DestroyedLocal(f, []VolumeRecord{r}, true, r.ObservedAt, time.Hour) {
+ t.Fatal("surviving volume discharged")
+ }
+}
+
+func TestTerminationFenceTimeIsAfterTheVerifiedRead(t *testing.T) {
+ w, s := terminationFixture(t)
+ start := w.Now()
+ now := start
+ w.Now = func() time.Time { return now }
+ w.Reader = func(context.Context, string) (awsinv.ExactEC2API, error) {
+ return terminatedEC2{state: types.InstanceStateNameTerminated, owner: "123", after: func() { now = start.Add(time.Minute) }}, nil
+ }
+ ref, err := w.Describe(context.Background(), "dead")
+ if err != nil {
+ t.Fatal(err)
+ }
+ raw, _, err := s.Get(context.Background(), ref.Key)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var fence Termination
+ if err = json.Unmarshal(raw, &fence); err != nil {
+ t.Fatal(err)
+ }
+ if !fence.ObservedAt.Equal(now) {
+ t.Fatal("fence time predates completion of the terminal-state read", fence.ObservedAt, now)
+ }
+}
+```
+
+Run this now and expect failure before the implementation is present:
+
+```sh
+go test ./internal/hostfence -count=1
+```
+
+**Step 2: add the implementation.**
+
+### Create `internal/hostfence/termination.go`
+
+```go
+package hostfence
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "github.com/carlosframework/platform/internal/awsinv"
+ "github.com/carlosframework/platform/internal/hostactors"
+ "github.com/carlosframework/platform/internal/hostfloor"
+ "time"
+)
+
+type Worker struct {
+ Box string `json:"box"`
+ Enrollment string `json:"enrollment"`
+}
+type Termination struct {
+ V int `json:"v"`
+ Box string `json:"box"`
+ Enrollment string `json:"enrollment"`
+ Cloud hostactors.Cloud `json:"cloud"`
+ State string `json:"state"`
+ ObservedAt time.Time `json:"observed_at"`
+ Via string `json:"via"`
+ RequestID string `json:"request_id,omitempty"`
+ EventID string `json:"event_id,omitempty"`
+ WrittenBy Worker `json:"written_by"`
+}
+type EvidenceRef struct {
+ Key string `json:"key"`
+ SHA256 string `json:"sha256"`
+}
+type TerminationWorker struct {
+ Enabled bool
+ Directory hostactors.Directory
+ Self Worker
+ Reader func(context.Context, string) (awsinv.ExactEC2API, error)
+ Now func() time.Time
+}
+
+func FenceKey(enrollment string) string { return "control/box-fences/" + enrollment + ".json" }
+func (w TerminationWorker) binding(ctx context.Context, box string) (hostactors.Box, error) {
+ if !w.Enabled || w.Now == nil {
+ return hostactors.Box{}, ErrObservation
+ }
+ o, err := w.Directory.Read(ctx)
+ if err != nil {
+ return hostactors.Box{}, err
+ }
+ self, ok := o.Roster.Boxes[w.Self.Box]
+ if !ok || self.State != "active" || self.Enrollment != w.Self.Enrollment {
+ return hostactors.Box{}, ErrObservation
+ }
+ target, ok := o.Roster.Boxes[box]
+ if !ok || target.Region != self.Region || target.Cloud.Region != self.Cloud.Region || target.Cloud.Account != self.Cloud.Account {
+ return hostactors.Box{}, ErrObservation
+ }
+ return target, nil
+}
+func (w TerminationWorker) Describe(ctx context.Context, box string) (EvidenceRef, error) {
+ b, err := w.binding(ctx, box)
+ if err != nil {
+ return EvidenceRef{}, err
+ }
+ if w.Reader == nil {
+ return EvidenceRef{}, ErrObservation
+ }
+ api, err := w.Reader(ctx, b.Cloud.Region)
+ if err != nil {
+ return EvidenceRef{}, err
+ }
+ observation, err := awsinv.VerifyExactStoppedInstance(ctx, api, b.Cloud.Account, b.Cloud.Region, b.Cloud.InstanceID)
+ if err != nil {
+ return EvidenceRef{}, err
+ }
+ if observation.State != "terminated" {
+ return EvidenceRef{}, ErrObservation
+ }
+ return w.persist(ctx, Termination{V: 1, Box: box, Enrollment: b.Enrollment, Cloud: b.Cloud, State: "terminated", ObservedAt: w.Now().UTC(), Via: "describe", RequestID: observation.RequestID, WrittenBy: w.Self})
+}
+
+// Event is called only by the regional queue consumer, after the existing
+// queue URL and rule-ARN policy validation. It is never an HTTP request body.
+func (w TerminationWorker) Event(ctx context.Context, box string, raw []byte) (EvidenceRef, error) {
+ b, err := w.binding(ctx, box)
+ if err != nil {
+ return EvidenceRef{}, err
+ }
+ var event struct {
+ ID string `json:"id"`
+ Source string `json:"source"`
+ Type string `json:"detail-type"`
+ Account string `json:"account"`
+ Region string `json:"region"`
+ Time time.Time `json:"time"`
+ Detail struct {
+ ID string `json:"instance-id"`
+ State string `json:"state"`
+ } `json:"detail"`
+ }
+ if json.Unmarshal(raw, &event) != nil || event.ID == "" || event.Source != "aws.ec2" || event.Type != "EC2 Instance State-change Notification" || event.Account != b.Cloud.Account || event.Region != b.Cloud.Region || event.Detail.ID != b.Cloud.InstanceID || event.Detail.State != "terminated" || event.Time.IsZero() || event.Time.After(w.Now().Add(5*time.Second)) {
+ return EvidenceRef{}, ErrObservation
+ }
+ // Receipt time is conservative for the late completion horizon. Retries use
+ // the first accepted record, so they cannot move that horizon forward.
+ return w.persist(ctx, Termination{V: 1, Box: box, Enrollment: b.Enrollment, Cloud: b.Cloud, State: "terminated", ObservedAt: w.Now().UTC(), Via: "event", EventID: event.ID, WrittenBy: w.Self})
+}
+func (w TerminationWorker) persist(ctx context.Context, f Termination) (EvidenceRef, error) {
+ raw, err := json.Marshal(f)
+ if err != nil {
+ return EvidenceRef{}, err
+ }
+ key := FenceKey(f.Enrollment)
+ _, err = w.Directory.Store.Put(ctx, key, raw, "")
+ if err != nil {
+ saved, _, readErr := w.Directory.Store.Get(ctx, key)
+ if readErr != nil {
+ return EvidenceRef{}, errors.Join(err, readErr)
+ }
+ var old Termination
+ if json.Unmarshal(saved, &old) != nil || old.V != 1 || old.Box != f.Box || old.Enrollment != f.Enrollment || old.Cloud != f.Cloud || old.State != "terminated" || old.ObservedAt.IsZero() || (old.Via != "describe" && old.Via != "event") || (old.RequestID == "" && old.EventID == "") {
+ return EvidenceRef{}, ErrObservation
+ }
+ raw = saved
+ }
+ return EvidenceRef{key, hostfloor.Digest(raw)}, nil
+}
+
+type BlockDevice struct {
+ Device string `json:"device"`
+ VolumeID string `json:"volume_id"`
+ DeleteOnTermination bool `json:"delete_on_termination"`
+ Status string `json:"status"`
+}
+type VolumeRecord struct {
+ V int `json:"v"`
+ Box string `json:"box"`
+ Enrollment string `json:"enrollment"`
+ Cloud hostactors.Cloud `json:"cloud"`
+ InstanceState string `json:"instance_state"`
+ BlockDevices []BlockDevice `json:"block_devices"`
+ RemovedVolumes []string `json:"removed_volumes"`
+ ObservedAt time.Time `json:"observed_at"`
+ RequestID string `json:"request_id"`
+ WrittenBy Worker `json:"written_by"`
+}
+
+// DestroyedLocal never participates in execution-fence acceptance. The caller
+// supplies the complete paginated volume history, with enrollment-time first
+// evidence and a bounded final running observation. Unknown history is false.
+func DestroyedLocal(f Termination, records []VolumeRecord, complete bool, enrolledAt time.Time, maxAge time.Duration) bool {
+ if !complete || len(records) == 0 || enrolledAt.IsZero() || maxAge <= 0 {
+ return false
+ }
+ if records[0].ObservedAt.After(enrolledAt) {
+ return false
+ }
+ previous := map[string]bool{}
+ last := time.Time{}
+ for _, r := range records {
+ if r.V != 1 || r.Box != f.Box || r.Enrollment != f.Enrollment || r.Cloud != f.Cloud || r.RequestID == "" || r.ObservedAt.IsZero() || !r.ObservedAt.After(last) || r.ObservedAt.After(f.ObservedAt) || (r.InstanceState != "running" && r.InstanceState != "shutting-down") || len(r.RemovedVolumes) > 0 || len(r.BlockDevices) == 0 {
+ return false
+ }
+ current := map[string]bool{}
+ for _, v := range r.BlockDevices {
+ if v.VolumeID == "" || v.Device == "" || !v.DeleteOnTermination || v.Status != "attached" || current[v.VolumeID] {
+ return false
+ }
+ current[v.VolumeID] = true
+ }
+ for id := range previous {
+ if !current[id] {
+ return false
+ }
+ }
+ previous = current
+ last = r.ObservedAt
+ }
+ return f.ObservedAt.Sub(last) <= maxAge
+}
+```
+
+### Create `internal/host/hostfencepass.go`
+
+```go
+package host
+
+import (
+ "context"
+ "github.com/carlosframework/platform/internal/awsinv"
+ "github.com/carlosframework/platform/internal/hostactors"
+ "github.com/carlosframework/platform/internal/hostfence"
+ "github.com/carlosframework/platform/internal/hostfloor"
+ "github.com/carlosframework/platform/internal/release"
+ "log"
+ "os"
+ "strings"
+ "time"
+)
+
+func runHostFencePass(ctx context.Context) {
+ if strings.TrimSpace(os.Getenv("CARLOS_HOST_CLAIM")) == "" || os.Getenv("CARLOS_FLEET") != "" {
+ return
+ }
+ store, pub, err := openSystemStore(ctx)
+ if err != nil {
+ log.Printf("host fences: %v", err)
+ return
+ }
+ enrollment, err := hostfloor.ReadEnrollment("/etc/carlos/host-actor.json")
+ if err != nil {
+ log.Printf("host fences: %v", err)
+ return
+ }
+ d := hostactors.Directory{Store: store, PublicKey: pub, Bucket: os.Getenv("CARLOS_DEPLOYMENT_BUCKET")}
+ worker := hostfence.TerminationWorker{Enabled: true, Directory: d, Self: hostfence.Worker{Box: enrollment.Box, Enrollment: enrollment.Enrollment}, Reader: awsinv.NewEC2Reader, Now: time.Now}
+ ticker := time.NewTicker(10 * time.Minute)
+ defer ticker.Stop()
+ for {
+ pass, cancel := context.WithTimeout(ctx, 2*time.Minute)
+ floor, err := hostfloor.Read(pass, store, pub, d.Bucket)
+ if err == nil {
+ local, loadErr := floorDisk(release.OptDir())
+ if loadErr != nil {
+ err = loadErr
+ } else {
+ _, armed, e := local.Load()
+ if e != nil {
+ err = e
+ } else if armed.Digest != floor.Digest {
+ err = hostfloor.ErrFloor
+ }
+ }
+ }
+ if err == nil {
+ roster, e := d.Read(pass)
+ err = e
+ if e == nil {
+ self := roster.Roster.Boxes[enrollment.Box]
+ for ref, box := range roster.Roster.Boxes {
+ if ref == enrollment.Box || box.Region != self.Region || box.State == "retired" {
+ continue
+ }
+ if _, fenceErr := worker.Describe(pass, ref); fenceErr != nil {
+ log.Printf("host fence %s: awaiting verified termination: %v", ref, fenceErr)
+ }
+ }
+ }
+ }
+ if err != nil {
+ log.Printf("host fence pass: %v", err)
+ }
+ cancel()
+ select {
+ case <-ctx.Done():
+ return
+ case <-ticker.C:
+ }
+ }
+}
+```
+
+**Step 3: apply the exact existing-file patch.**
+
+```diff
+--- a/internal/host/agent.go
++++ b/internal/host/agent.go
+@@ -203,0 +204 @@
++ go runHostFencePass(context.Background())
+@@ -2938,0 +2940 @@
++ claimReport(&p)
+```
+
+**Step 4: verify the task.**
+
+```sh
+go test ./internal/hostfence -count=1
+```
+
+## Task 5: Operator resolution, readmission and console/update wiring
+
+Replace both placeholder HTTP handlers and the CLI's impossible-success branch. Inspection returns a signed challenge with readable consequences. The operator observes the enrolled instance after inspection, explicitly confirms the attestation and submits the same envelope. --yes never manufactures an attestation about compute or disk destruction. The console has no EC2 client. Accepted losses are durable before their claim CAS; a claim-write failure leaves global quarantine in place. Readmission validates the fresh private report and exact retirement receipt, all retained declarations and complete claim inventory before clearing the global deny. Report publishing runs at startup and five-minute intervals outside NewServer. System promote and rollback reserve exact affected actors before changing a pointer; rollback also binds the pointer ETag read before reservation.
+
+**Step 1: add the failing tests.**
+
+### Create `internal/hostresolve/service_test.go`
+
+```go
+package hostresolve
+
+import (
+ "context"
+ "crypto/ed25519"
+ "crypto/rand"
+ "errors"
+ "fmt"
+ "github.com/carlosframework/platform/internal/hostactors"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostfence"
+ "github.com/carlosframework/platform/internal/release"
+ "strings"
+ "testing"
+ "time"
+)
+
+type failClaimStore struct {
+ release.Store
+ fail bool
+}
+
+func (s *failClaimStore) Put(ctx context.Context, key string, raw []byte, etag string) (string, error) {
+ if s.fail && strings.HasPrefix(key, "control/hosts/") {
+ return "", errors.New("claim write unavailable")
+ }
+ return s.Store.Put(ctx, key, raw, etag)
+}
+func resolveFixture(t *testing.T) (Service, *failClaimStore, *time.Time, string) {
+ t.Helper()
+ ctx := context.Background()
+ pub, key, _ := ed25519.GenerateKey(rand.Reader)
+ store := &failClaimStore{Store: release.NewDirStore(t.TempDir())}
+ signer := release.RootSigner(key)
+ now := time.Now().UTC()
+ d := hostactors.Directory{Store: store, PublicKey: pub, Bucket: "bucket"}
+ if err := d.Initialize(ctx, signer); err != nil {
+ t.Fatal(err)
+ }
+ o, err := d.Read(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ r := hostactors.Clone(o.Roster)
+ r.Boxes["dead"] = hostactors.Box{Enrollment: strings.Repeat("a", 32), Namespace: "platform", BoxID: "dead", Label: "dead-label", Region: "ie", Pool: "default", Cloud: hostactors.Cloud{Provider: "aws", Account: "123", Region: "eu-west-1", InstanceID: "i-dead"}, EvidenceSHA256: strings.Repeat("b", 64), State: "active"}
+ if _, err = d.Replace(ctx, signer, o, r); err != nil {
+ t.Fatal(err)
+ }
+ host := "shop.bab.oncarlos.com"
+ observed, _ := hostclaim.Read(ctx, store, host)
+ n := 0
+ _, err = hostclaim.Update(ctx, store, observed, hostclaim.Adopt(hostclaim.NewHolder{Family: "instance", Key: "apps/bab/jam/instances/" + host + ".json", Account: "bab", App: "jam", Host: host, Region: "ie", RecordRegion: "ie"}, "live", nil, []string{"dead"}, "discovery", func() string { n++; return fmt.Sprintf("id-%d", n) }))
+ if err != nil {
+ t.Fatal(err)
+ }
+ return Service{Directory: d, Signer: signer, Now: func() time.Time { return now }}, store, &now, host
+}
+func submitted(t *testing.T, s Service, now *time.Time, host string) hostfence.Submission {
+ t.Helper()
+ in, err := s.Inspect(context.Background(), "operator", host, "dead-label")
+ if err != nil {
+ t.Fatal(err)
+ }
+ *now = now.Add(2 * time.Second)
+ return hostfence.Submission{Challenge: in.Challenge, Confirmation: in.Details.ID, Observation: hostfence.CloudObservation{ObservedAt: in.Details.IssuedAt.Add(time.Second), Account: "123", Region: "eu-west-1", InstanceID: "i-dead", State: "stopped", RequestID: "request"}, Attestation: hostfence.OperatorAttestation{MaintainComputeFence: true, ColdQuarantineStartOnly: true, Disks: in.Details.Consequences.DestroyedStorageAssertion}}
+}
+func TestLostWiringPreservesServingAuthorityAndRetryEvidence(t *testing.T) {
+ ctx := context.Background()
+ s, store, now, host := resolveFixture(t)
+ in := submitted(t, s, now, host)
+ before, _ := hostclaim.Read(ctx, store, host)
+ after, err := s.Declare(ctx, "operator", host, in)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if after.Claim.Op.Exec != before.Claim.Op.Exec || after.Claim.Holder.Stamp != before.Claim.Holder.Stamp || len(after.Claim.Obligations) != 0 {
+ t.Fatal(after.Claim)
+ }
+ loss, err := hostclaim.ReadLossRecord(ctx, store, s.Directory.PublicKey, in.Confirmation)
+ if err != nil || loss.Record.State != "committed" {
+ t.Fatal(loss, err)
+ }
+ first := loss.Record.FencedAt
+ *now = now.Add(time.Hour)
+ if _, err = s.Declare(ctx, "operator", host, in); err != nil {
+ t.Fatal("accepted retry expired", err)
+ }
+ loss, _ = hostclaim.ReadLossRecord(ctx, store, s.Directory.PublicKey, in.Confirmation)
+ if !loss.Record.FencedAt.Equal(first) {
+ t.Fatal("retry moved fence time")
+ }
+ if _, err = s.Declare(ctx, "other-person", host, in); err == nil {
+ t.Fatal("actor changed")
+ }
+}
+func TestFailedClaimWriteLeavesGlobalQuarantine(t *testing.T) {
+ s, store, now, host := resolveFixture(t)
+ in := submitted(t, s, now, host)
+ store.fail = true
+ if _, err := s.Declare(context.Background(), "operator", host, in); err == nil {
+ t.Fatal("expected write error")
+ }
+ actor, err := s.Directory.Lookup(context.Background(), "dead")
+ if err != nil || actor.State != "quarantined" {
+ t.Fatal(actor, err)
+ }
+ store.fail = false
+ if _, err := s.Declare(context.Background(), "operator", host, in); err != nil {
+ t.Fatal(err)
+ }
+}
+```
+
+Run this now and expect failure before the implementation is present:
+
+```sh
+go test ./internal/hostresolve -count=1
+go test ./internal/console -run '^TestHostClaimRoutesArePersonOperatorFlagGatedAndInert$' -count=1
+go test . -run '^TestHostClaimFlag|^TestParseHostResolve' -count=1
+```
+
+**Step 2: add the implementation.**
+
+### Create `internal/hostresolve/service.go`
+
+```go
+package hostresolve
+
+import (
+ "context"
+ "crypto/rand"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "github.com/carlosframework/platform/internal/hostactors"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostfence"
+ "github.com/carlosframework/platform/internal/release"
+ "os"
+ "reflect"
+ "slices"
+ "time"
+)
+
+var ErrResolve = errors.New("hostresolve: evidence or operation does not authorize resolution")
+
+type Service struct {
+ Directory hostactors.Directory
+ Signer release.Signer
+ Now func() time.Time
+}
+type Inspection struct {
+ Challenge json.RawMessage `json:"challenge"`
+ Details hostfence.Challenge `json:"details"`
+}
+
+func nonce() (string, error) {
+ var b [16]byte
+ _, err := rand.Read(b[:])
+ return hex.EncodeToString(b[:]), err
+}
+func (s Service) record(ctx context.Context, c *hostclaim.Claim) (hostfence.RecordObservation, error) {
+ _, etag, err := s.Directory.Store.Get(ctx, c.Holder.Key)
+ if errors.Is(err, os.ErrNotExist) {
+ return hostfence.RecordObservation{Key: c.Holder.Key, CommitState: "absent"}, nil
+ }
+ if err != nil {
+ return hostfence.RecordObservation{}, err
+ }
+ return hostfence.RecordObservation{Key: c.Holder.Key, ETag: etag, CommitState: "present"}, nil
+}
+
+// effects implements only the owners present on the Plan 8 baseline. Moves'
+// target-loss policy must come from the moves owner, never from an HTTP flag.
+func effects(c *hostclaim.Claim, box string) (hostfence.Consequences, hostclaim.LossConsequences, error) {
+ var shown hostfence.Consequences
+ var actual hostclaim.LossConsequences
+ if c == nil || c.Holder == nil {
+ return shown, actual, ErrResolve
+ }
+ actual.RecoveryIntent = json.RawMessage(`{"kind":"continue-parking"}`)
+ switch c.Op.Kind {
+ case hostclaim.KindServing:
+ shown.Transition = "destroyed-local-only"
+ actual.RecoveryIntent = json.RawMessage(`{"kind":"destroyed-local-only"}`)
+ for _, o := range c.Obligations {
+ if o.Kind == "park-local" && o.Box == box {
+ ref := hostclaim.ObligationRef{ID: o.ID, Op: o.Op, Retirement: o.Retirement}
+ shown.DischargeLocal = append(shown.DischargeLocal, ref)
+ actual.DischargeLocal = append(actual.DischargeLocal, ref)
+ key := o.Op + "/" + o.ID + "/" + o.Retirement
+ shown.DestroyedStorageAssertion = append(shown.DestroyedStorageAssertion, key)
+ actual.DestroyedStorage = append(actual.DestroyedStorage, key)
+ }
+ }
+ if len(actual.DischargeLocal) == 0 {
+ return shown, actual, ErrResolve
+ }
+ case hostclaim.KindDeleting, hostclaim.KindParking:
+ if c.Op.ID == "" || (!slices.Contains(c.Op.Participants, box) && c.Op.Exec != box) {
+ return shown, actual, ErrResolve
+ }
+ shown.Transition = "continue-parking"
+ shown.PossibleWriteLoss = true
+ shown.PossibleTimerLoss = true
+ actual.RevokeExecution = c.Op.Exec == box
+ actual.RemoveParticipation = slices.Contains(c.Op.Participants, box)
+ actual.WritesPotentiallyLost = true
+ actual.TimersPotentiallyLost = true
+ for _, o := range c.Obligations {
+ if (o.Kind == "park-replica" || o.Kind == "park-generation") && o.Owner == "box:"+box {
+ actual.ReassignRegional = append(actual.ReassignRegional, hostclaim.ObligationRef{ID: o.ID, Op: o.Op, Retirement: o.Retirement})
+ shown.Chores = append(shown.Chores, o.Op+"/"+o.ID+"/"+o.Retirement)
+ }
+ }
+ default:
+ return shown, actual, ErrResolve
+ }
+ return shown, actual, nil
+}
+func (s Service) Inspect(ctx context.Context, actor, host, name string) (Inspection, error) {
+ ref, b, err := s.Directory.Resolve(ctx, name)
+ if err != nil {
+ return Inspection{}, err
+ }
+ if b.State == "retired" {
+ return Inspection{}, ErrResolve
+ }
+ o, err := hostclaim.Read(ctx, s.Directory.Store, host)
+ if err != nil {
+ return Inspection{}, err
+ }
+ if o.Absent || o.Claim.Holder == nil {
+ return Inspection{}, ErrResolve
+ }
+ shown, _, err := effects(o.Claim, ref)
+ if err != nil {
+ return Inspection{}, err
+ }
+ record, err := s.record(ctx, o.Claim)
+ if err != nil {
+ return Inspection{}, err
+ }
+ id, err := nonce()
+ if err != nil {
+ return Inspection{}, err
+ }
+ c := o.Claim
+ challenge, err := hostfence.PrepareChallenge(s.Now(), hostfence.Challenge{ID: id, Actor: actor, Host: host, HolderIncarnation: c.Holder.Incarnation, HolderStamp: c.Holder.Stamp, HolderSession: c.Holder.Session, ClaimGen: c.Gen, ClaimETag: o.ETag, Op: hostfence.Operation{ID: c.Op.ID, Kind: c.Op.Kind, Phase: c.Op.Phase}, Record: record, Box: ref, Enrollment: b.Enrollment, Cloud: hostfence.CloudBinding{Provider: b.Cloud.Provider, Account: b.Cloud.Account, Region: b.Cloud.Region, InstanceID: b.Cloud.InstanceID}, Consequences: shown})
+ if err != nil {
+ return Inspection{}, err
+ }
+ wire, err := hostfence.SignChallenge(s.Signer, challenge)
+ return Inspection{wire, challenge}, err
+}
+
+type owner struct{}
+
+func (owner) ValidateDeclareLost(c hostclaim.Claim, d hostclaim.LostDeclaration) error {
+ _, want, err := effects(&c, d.Box)
+ if err != nil {
+ return err
+ }
+ if !reflect.DeepEqual(want, d.Consequences) {
+ return ErrResolve
+ }
+ return nil
+}
+func (s Service) Declare(ctx context.Context, actor, host string, in hostfence.Submission) (hostclaim.Observed, error) {
+ now := s.Now().UTC()
+ var c hostfence.Challenge
+ err := release.VerifyRootEnvelope(s.Directory.PublicKey, in.Challenge, &c)
+ if err != nil {
+ return hostclaim.Observed{}, err
+ }
+ if c.Actor != actor || c.Host != host || in.Confirmation != c.ID || !in.Attestation.MaintainComputeFence || !in.Attestation.ColdQuarantineStartOnly || !reflect.DeepEqual(in.Attestation.Disks, c.Consequences.DestroyedStorageAssertion) {
+ return hostclaim.Observed{}, ErrResolve
+ }
+ // A retry uses the durable accepted declaration even after its claim CAS.
+ // Inspect a new challenge only when no accepted declaration exists.
+ prior, readErr := hostclaim.ReadLossRecord(ctx, s.Directory.Store, s.Directory.PublicKey, c.ID)
+ if readErr == nil {
+ if prior.Record.Actor != actor || prior.Record.Host != host || prior.Record.Enrollment != c.Enrollment {
+ return hostclaim.Observed{}, ErrResolve
+ }
+ return s.finish(ctx, prior)
+ }
+ if !errors.Is(readErr, hostclaim.ErrLossRecordAbsent) {
+ return hostclaim.Observed{}, readErr
+ }
+ if _, err = hostfence.VerifyChallenge(s.Directory.PublicKey, in.Challenge, now); err != nil {
+ return hostclaim.Observed{}, err
+ }
+ if err = hostfence.ValidateObservation(c, in.Observation, now); err != nil {
+ return hostclaim.Observed{}, err
+ }
+ roster, err := s.Directory.Read(ctx)
+ if err != nil {
+ return hostclaim.Observed{}, err
+ }
+ b, ok := roster.Roster.Boxes[c.Box]
+ if !ok || b.Enrollment != c.Enrollment || b.State == "retired" || b.Cloud != (hostactors.Cloud{Provider: c.Cloud.Provider, Account: c.Cloud.Account, Region: c.Cloud.Region, InstanceID: c.Cloud.InstanceID}) {
+ return hostclaim.Observed{}, ErrResolve
+ }
+ observed, err := hostclaim.Read(ctx, s.Directory.Store, host)
+ if err != nil {
+ return hostclaim.Observed{}, err
+ }
+ if observed.Absent || observed.ETag != c.ClaimETag || observed.Claim.Gen != c.ClaimGen {
+ return hostclaim.Observed{}, ErrResolve
+ }
+ record, err := s.record(ctx, observed.Claim)
+ if err != nil || record != c.Record {
+ return hostclaim.Observed{}, ErrResolve
+ }
+ shown, actual, err := effects(observed.Claim, c.Box)
+ if err != nil || !reflect.DeepEqual(shown, c.Consequences) {
+ return hostclaim.Observed{}, ErrResolve
+ }
+ quarantined, err := s.Directory.Quarantine(ctx, s.Signer, roster, c.Box, c.Enrollment, c.ID)
+ if err != nil {
+ return hostclaim.Observed{}, err
+ }
+ // A partial declaration remains denied. Never undo this CAS on error.
+ q := quarantined.Roster.Boxes[c.Box]
+ loss := hostclaim.LossRecord{V: 1, ID: c.ID, State: "prepared", Actor: actor, AcceptedAt: now, Host: host, HolderIncarnation: c.HolderIncarnation, HolderStamp: c.HolderStamp, HolderSession: c.HolderSession, ExpectedClaimGen: c.ClaimGen, ExpectedClaimETag: c.ClaimETag, OpID: c.Op.ID, OpKind: c.Op.Kind, OpPhase: c.Op.Phase, Box: c.Box, Enrollment: c.Enrollment, QuarantineRevision: q.QuarantineRevision, FencedAt: now, ConfirmedConsequences: actual, Timers: hostclaim.TimerLoss{Knowledge: hostclaim.TimersUnknown}}
+ loss.CloudObservation = hostclaim.LossCloudObservation{ObservedAt: in.Observation.ObservedAt, Account: in.Observation.Account, Region: in.Observation.Region, InstanceID: in.Observation.InstanceID, State: in.Observation.State, RequestID: in.Observation.RequestID}
+ loss.OperatorAttestation = hostclaim.LossOperatorAttestation{MaintainComputeFence: true, ColdQuarantineStartOnly: true, DestroyedStorage: in.Attestation.Disks}
+ prepared, err := hostclaim.CreateLossRecord(ctx, s.Directory.Store, s.Signer, loss)
+ if err != nil {
+ return hostclaim.Observed{}, err
+ }
+ return s.finish(ctx, prepared)
+}
+func (s Service) finish(ctx context.Context, loss hostclaim.ObservedLossRecord) (hostclaim.Observed, error) {
+ observed, err := hostclaim.Read(ctx, s.Directory.Store, loss.Record.Host)
+ if err != nil {
+ return hostclaim.Observed{}, err
+ }
+ next, err := hostclaim.DeclareLost(ctx, s.Directory.Store, s.Directory.PublicKey, observed, loss.Record.ID, owner{})
+ if err != nil {
+ return hostclaim.Observed{}, err
+ }
+ if loss.Record.State == "committed" {
+ return next, nil
+ }
+ r := loss.Record
+ r.State = "committed"
+ r.ResultClaimGen = next.Claim.Gen
+ _, err = hostclaim.ReplaceLossRecord(ctx, s.Directory.Store, s.Signer, loss, r)
+ return next, err
+}
+```
+
+### Create `internal/hostresolve/readmit.go`
+
+```go
+package hostresolve
+
+import (
+ "context"
+ "encoding/json"
+ "github.com/carlosframework/platform/internal/hostactors"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostfloor"
+ "github.com/carlosframework/platform/internal/hostquarantine"
+ "reflect"
+ "slices"
+ "strings"
+ "time"
+)
+
+// Readmit keeps the global deny until every declaration, claim and receipt
+// check is complete. It grants no executor or writer session.
+func (s Service) Readmit(ctx context.Context, box, enrollment string, revision uint64, lossIDs []string) error {
+ before, err := s.Directory.Read(ctx)
+ if err != nil {
+ return err
+ }
+ b, ok := before.Roster.Boxes[box]
+ if !ok || b.Enrollment != enrollment || b.State != "quarantined" || b.QuarantineRevision != revision || !reflect.DeepEqual(b.LossIDs, lossIDs) {
+ return ErrResolve
+ }
+ report, err := s.Directory.PrivateBoxReport(ctx, box, b)
+ if err != nil {
+ return err
+ }
+ if report.Enrollment != enrollment || report.InstanceID != b.Cloud.InstanceID || report.BootID == "" || report.GeneratedAt.IsZero() || s.Now().Sub(report.GeneratedAt) > 15*time.Minute || report.GeneratedAt.After(s.Now().Add(5*time.Second)) {
+ return ErrResolve
+ }
+ floor, err := hostfloor.Read(ctx, s.Directory.Store, s.Directory.PublicKey, s.Directory.Bucket)
+ if err != nil {
+ return err
+ }
+ evidence := hostactors.ExactEvidence{Store: s.Directory.Store, Boxes: s.Directory.PrivateBoxReport}
+ if err = hostactors.VerifyReport(ctx, evidence, s.Directory.PublicKey, floor, report, enrollment, b.Cloud.InstanceID, s.Now()); err != nil {
+ return err
+ }
+ if !report.HostClaimEnabled {
+ return ErrResolve
+ }
+ key, err := hostquarantine.RetirementKey(box, enrollment, revision)
+ if err != nil {
+ return err
+ }
+ raw, _, err := s.Directory.Store.Get(ctx, key)
+ if err != nil {
+ return err
+ }
+ if report.RetirementReceiptSHA256 != hostfloor.Digest(raw) {
+ return ErrResolve
+ }
+ var receipt hostquarantine.RetirementCompletion
+ if json.Unmarshal(raw, &receipt) != nil || receipt.Validate() != nil || !receipt.Complete || receipt.Box != box || receipt.Enrollment != enrollment || receipt.InstanceID != b.Cloud.InstanceID || receipt.QuarantineRevision != revision || receipt.BootID != report.BootID || receipt.CompletedAt.After(report.GeneratedAt) {
+ return ErrResolve
+ }
+ for _, id := range lossIDs {
+ loss, err := hostclaim.ReadLossRecord(ctx, s.Directory.Store, s.Directory.PublicKey, id)
+ if err != nil {
+ return err
+ }
+ if loss.Record.State != "committed" || loss.Record.Box != box || loss.Record.Enrollment != enrollment {
+ return ErrResolve
+ }
+ proven := false
+ for _, entry := range receipt.Entries {
+ if entry.Host == loss.Record.Host && entry.Incarnation == loss.Record.HolderIncarnation && entry.Stamp == loss.Record.HolderStamp {
+ proven = true
+ }
+ }
+ if !proven && len(loss.Record.ConfirmedConsequences.DischargeLocal) == 0 {
+ return ErrResolve
+ }
+ }
+ // Store.List is a complete listing in both real implementations. Its S3
+ // implementation follows every continuation token; any error refuses.
+ keys, err := s.Directory.Store.List(ctx, "control/hosts/")
+ if err != nil {
+ return err
+ }
+ for _, key := range keys {
+ host, ok := claimHost(key)
+ if !ok {
+ continue
+ }
+ observed, err := hostclaim.Read(ctx, s.Directory.Store, host)
+ if err != nil {
+ return err
+ }
+ if observed.Absent {
+ continue
+ }
+ c := observed.Claim
+ for _, o := range c.Obligations {
+ if o.Kind == "park-local" && o.Box == box {
+ return ErrResolve
+ }
+ }
+ if slices.Contains(c.Lost, box) && c.LostRevisions[box] == revision {
+ if _, err = hostclaim.Update(ctx, s.Directory.Store, observed, hostclaim.Readmit(box, revision)); err != nil {
+ return err
+ }
+ }
+ }
+ // CAS against the original roster prevents a concurrent declaration,
+ // reservation or membership change being overwritten by this readmission.
+ next := hostactors.Clone(before.Roster)
+ b.State = "active"
+ next.Boxes[box] = b
+ _, err = s.Directory.Replace(ctx, s.Signer, before, next)
+ return err
+}
+
+func claimHost(key string) (string, bool) {
+ s, ok := strings.CutPrefix(key, "control/hosts/")
+ if !ok || strings.Contains(s, "/") {
+ return "", false
+ }
+ host, ok := strings.CutSuffix(s, ".json")
+ return host, ok && host != ""
+}
+```
+
+### Create `internal/console/hostreports.go`
+
+```go
+package console
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "github.com/carlosframework/platform/internal/hostactors"
+ "github.com/carlosframework/platform/internal/hostfloor"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/version"
+ "log"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "time"
+)
+
+func (s *Server) localHostReport(ctx context.Context) (hostactors.Report, error) {
+ id := s.cfg.HostConsoleID
+ if id == "" {
+ return hostactors.Report{}, hostactors.ErrRoster
+ }
+ d := hostactors.Directory{Store: s.cfg.Releases, PublicKey: s.cfg.ReleasePub, Bucket: s.cfg.Bucket}
+ roster, err := d.Read(ctx)
+ if err != nil {
+ return hostactors.Report{}, err
+ }
+ c, ok := roster.Roster.Consoles[id]
+ if !ok {
+ return hostactors.Report{}, hostactors.ErrRoster
+ }
+ disk := hostfloor.Disk{Dir: filepath.Join(release.OptDir(), "system", "guard"), Bucket: s.cfg.Bucket, PublicKey: s.cfg.ReleasePub}
+ running, err := disk.Running(version.Build, runtime.GOOS+"-"+runtime.GOARCH, "console")
+ if err != nil {
+ return hostactors.Report{}, err
+ }
+ return hostactors.Report{Component: "console", GeneratedAt: time.Now().UTC(), Actor: id, Enrollment: c.Enrollment, InstanceID: id, ProcessID: fmt.Sprintf("%s:%d", running.BootID, os.Getpid()), Release: version.Build, ReleaseTag: running.ReleaseTag, Platform: runtime.GOOS + "-" + runtime.GOARCH, BinarySHA256: running.Hash, MovesProtocol: running.Protocol, BootID: running.BootID, FloorSHA256: running.FloorSHA256, FloorGuardReady: running.GuardReady, HostClaimEnabled: s.cfg.HostClaim}, nil
+}
+func (s *Server) StartHostReports(ctx context.Context) {
+ // Reporting before the flag is enabled is harmless and needed for arming.
+ if s.cfg.HostConsoleID == "" || strings.ContainsAny(s.cfg.HostConsoleID, "/\\") || s.cfg.Releases == nil {
+ return
+ }
+ ticker := time.NewTicker(5 * time.Minute)
+ defer ticker.Stop()
+ for {
+ report, err := s.localHostReport(ctx)
+ if err == nil {
+ key := "status/consoles/" + s.cfg.HostConsoleID + ".json"
+ raw, _ := json.Marshal(report)
+ _, etag, readErr := s.cfg.Releases.Get(ctx, key)
+ if readErr != nil && !os.IsNotExist(readErr) {
+ err = readErr
+ } else {
+ _, err = s.cfg.Releases.Put(ctx, key, raw, etag)
+ }
+ }
+ if err == nil && len(s.cfg.Key) != 0 {
+ d := hostactors.Directory{Store: s.cfg.Releases, PublicKey: s.cfg.ReleasePub, Bucket: s.cfg.Bucket}
+ if o, e := d.Read(ctx); e == nil {
+ for id := range o.Roster.Activations {
+ _ = d.ReleaseActivation(ctx, release.RootSigner(s.cfg.Key), id, time.Now())
+ }
+ }
+ }
+ if err != nil {
+ log.Printf("console: host census report: %v", err)
+ }
+ select {
+ case <-ctx.Done():
+ return
+ case <-ticker.C:
+ }
+ }
+}
+```
+
+### Create `internal/console/hostactivation.go`
+
+```go
+package console
+
+import (
+ "context"
+ "crypto/rand"
+ "encoding/hex"
+ "errors"
+ "github.com/carlosframework/platform/internal/hostactors"
+ "github.com/carlosframework/platform/internal/hostfloor"
+ "github.com/carlosframework/platform/internal/release"
+ "os"
+ "strings"
+ "time"
+)
+
+func (s *Server) reserveHostUpdate(ctx context.Context, rel *release.SystemRelease, channel string) (string, error) {
+ pub := release.RootSigner(s.cfg.Key).RootPublicKey()
+ floor, err := hostfloor.Read(ctx, s.cfg.Releases, pub, s.cfg.Bucket)
+ if errors.Is(err, os.ErrNotExist) {
+ // An incomplete publication is not the old deployment mode.
+ if _, _, e := s.cfg.Releases.Get(ctx, hostfloor.AuthKey); !errors.Is(e, os.ErrNotExist) {
+ return "", hostfloor.ErrFloor
+ }
+ return "", nil
+ }
+ if err != nil {
+ return "", err
+ }
+ tag := strings.TrimPrefix(rel.Source, "release:")
+ if rel.ReleaseTag != "" {
+ tag = rel.ReleaseTag
+ }
+ if rel.Source == "file" {
+ keys, e := s.cfg.Releases.List(ctx, "control/system/router/releases/")
+ if e != nil {
+ return "", e
+ }
+ tag = ""
+ for _, key := range keys {
+ raw, _, e := s.cfg.Releases.Get(ctx, key)
+ if e != nil {
+ return "", e
+ }
+ identity, e := release.VerifySystemIdentity(pub, raw)
+ if e != nil {
+ return "", e
+ }
+ if identity.Current.Version != rel.Version {
+ continue
+ }
+ matches := true
+ for platform, artifact := range rel.Artifacts {
+ if identity.Current.Artifacts[platform] != artifact {
+ matches = false
+ }
+ }
+ if matches {
+ if tag != "" && tag != identity.Current.ReleaseTag {
+ return "", hostfloor.ErrFloor
+ }
+ tag = identity.Current.ReleaseTag
+ }
+ }
+ }
+ approved, err := release.GetSystemIdentity(ctx, s.cfg.Releases, pub, tag)
+ if err != nil {
+ return "", err
+ }
+ if approved.Current.Version != rel.Version {
+ return "", hostfloor.ErrFloor
+ }
+ for platform, artifact := range rel.Artifacts {
+ if approved.Current.Artifacts[platform] != artifact {
+ return "", hostfloor.ErrFloor
+ }
+ }
+ rel.ReleaseTag = approved.Current.ReleaseTag
+ rel.MovesProtocol = approved.Current.MovesProtocol
+ rel.ConsoleArtifacts = approved.Current.ConsoleArtifacts
+ d := hostactors.Directory{Store: s.cfg.Releases, PublicKey: pub, Bucket: s.cfg.Bucket}
+ o, err := d.Read(ctx)
+ if err != nil {
+ return "", err
+ }
+ var nonce [16]byte
+ if _, err = rand.Read(nonce[:]); err != nil {
+ return "", err
+ }
+ id := hex.EncodeToString(nonce[:])
+ h := hostactors.Reservation{ID: id, FloorSHA256: floor.Digest, Actors: map[string]string{}, Releases: map[string]hostactors.Report{}, Hashes: map[string][]string{}}
+ e := hostactors.ExactEvidence{Store: s.cfg.Releases, Boxes: d.PrivateBoxReport}
+ for ref, b := range o.Roster.Boxes {
+ if b.State != "active" {
+ continue
+ }
+ report, err := d.PrivateBoxReport(ctx, ref, b)
+ if err != nil {
+ return "", err
+ }
+ if err = hostactors.VerifyReport(ctx, e, pub, floor, report, b.Enrollment, b.Cloud.InstanceID, time.Now()); err != nil {
+ return "", err
+ }
+ if report.SystemChannel == "" {
+ return "", hostfloor.ErrFloor
+ }
+ if report.SystemChannel != channel {
+ continue
+ }
+ target, ok := rel.Artifacts[report.Platform]
+ if !ok {
+ return "", hostfloor.ErrFloor
+ }
+ identityKey, _ := release.IdentityKey(rel.ReleaseTag)
+ identity, _, err := s.cfg.Releases.Get(ctx, identityKey)
+ if err != nil {
+ return "", err
+ }
+ if err = hostfloor.CheckImage(pub, floor, identity, rel.Version, report.Platform, target.SHA256); err != nil {
+ return "", err
+ }
+ if target.SHA256 == report.BinarySHA256 {
+ continue
+ }
+ key := "box:" + ref
+ h.Actors[key] = b.Enrollment
+ h.Releases[key] = report
+ // This transaction refreshes recovery from the authenticated current
+ // image. A box unable to do that retains the reservation and refuses.
+ h.Hashes[key] = []string{report.BinarySHA256, target.SHA256, report.BinarySHA256}
+ }
+ if len(h.Actors) == 0 {
+ return "", nil
+ }
+ _, err = d.Reserve(ctx, release.RootSigner(s.cfg.Key), o, h, true)
+ return id, err
+}
+```
+
+**Step 3: apply the exact existing-file patch.**
+
+```diff
+--- a/internal/console/apihostclaim_test.go
++++ b/internal/console/apihostclaim_test.go
+@@ -49 +49 @@
+- if out["error"] != hostClaimPendingRuling {
++ if out["error"] != hostClaimUnavailable {
+--- a/internal/console/apihostclaim.go
++++ b/internal/console/apihostclaim.go
+@@ -4 +4,6 @@
+- "log"
++ "encoding/json"
++ "github.com/carlosframework/platform/internal/hostactors"
++ "github.com/carlosframework/platform/internal/hostfence"
++ "github.com/carlosframework/platform/internal/hostfloor"
++ "github.com/carlosframework/platform/internal/hostresolve"
++ "github.com/carlosframework/platform/internal/release"
+@@ -5,0 +11 @@
++ "time"
+@@ -8 +14 @@
+-const hostClaimPendingRuling = "awaiting Paul's ruling P2/P3 (amadan #236)"
++const hostClaimUnavailable = "host claim authority is unavailable"
+@@ -10,4 +16 @@
+-// hostClaimRefusal applies the D2 authority boundary before reporting that
+-// the follow-on integration's ActorDirectory is unavailable. It deliberately performs no claim,
+-// directory or cloud write.
+-func (s *Server) hostClaimRefusal(w http.ResponseWriter, r *http.Request) {
++func (s *Server) hostClaimOperator(w http.ResponseWriter, r *http.Request) (string, bool) {
+@@ -14,0 +18,24 @@
++ if !ok {
++ return "", false
++ }
++ yes, err := s.store.IsOperator(addr)
++ if err != nil || !yes || !s.cfg.HostClaim {
++ http.NotFound(w, r)
++ return "", false
++ }
++ if s.cfg.Bucket == "" || len(s.cfg.Key) == 0 || s.cfg.Releases == nil {
++ jsonError(w, http.StatusServiceUnavailable, hostClaimUnavailable)
++ return "", false
++ }
++ if _, err := hostfloor.Read(r.Context(), s.cfg.Releases, release.RootSigner(s.cfg.Key).RootPublicKey(), s.cfg.Bucket); err != nil {
++ jsonError(w, http.StatusServiceUnavailable, hostClaimUnavailable)
++ return "", false
++ }
++ return addr, true
++}
++func (s *Server) hostResolver() hostresolve.Service {
++ signer := release.RootSigner(s.cfg.Key)
++ return hostresolve.Service{Directory: hostactors.Directory{Store: s.cfg.Releases, PublicKey: signer.RootPublicKey(), Bucket: s.cfg.Bucket}, Signer: signer, Now: time.Now}
++}
++func (s *Server) handleAPIHostResolveInspect(w http.ResponseWriter, r *http.Request) {
++ actor, ok := s.hostClaimOperator(w, r)
+@@ -18,4 +45,6 @@
+- isOp, err := s.store.IsOperator(addr)
+- if err != nil {
+- log.Printf("console: host claim operator lookup: %v", err)
+- http.NotFound(w, r)
++ var in struct {
++ Action string `json:"action"`
++ Box string `json:"box"`
++ }
++ if json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&in) != nil {
++ jsonError(w, 400, "invalid resolution request")
+@@ -24,2 +53,8 @@
+- if !isOp || !s.cfg.HostClaim {
+- http.NotFound(w, r)
++ svc := s.hostResolver()
++ if in.Action == "readmit" {
++ ref, b, err := svc.Directory.Resolve(r.Context(), in.Box)
++ if err != nil {
++ jsonError(w, 409, err.Error())
++ return
++ }
++ json.NewEncoder(w).Encode(map[string]any{"box": ref, "enrollment": b.Enrollment, "revision": b.QuarantineRevision, "loss_ids": b.LossIDs})
+@@ -28 +63,10 @@
+- jsonError(w, http.StatusServiceUnavailable, hostClaimPendingRuling)
++ if in.Action != "declare-lost" {
++ jsonError(w, 400, "invalid resolution action")
++ return
++ }
++ inspected, err := svc.Inspect(r.Context(), actor, r.PathValue("host"), in.Box)
++ if err != nil {
++ jsonError(w, 409, err.Error())
++ return
++ }
++ json.NewEncoder(w).Encode(inspected)
+@@ -30,3 +74,33 @@
+-
+-func (s *Server) handleAPIHostResolveInspect(w http.ResponseWriter, r *http.Request) {
+- s.hostClaimRefusal(w, r)
++func (s *Server) handleAPIHostResolve(w http.ResponseWriter, r *http.Request) {
++ actor, ok := s.hostClaimOperator(w, r)
++ if !ok {
++ return
++ }
++ var in struct {
++ Action string `json:"action"`
++ Submission hostfence.Submission `json:"submission"`
++ Box string `json:"box"`
++ Enrollment string `json:"enrollment"`
++ Revision uint64 `json:"revision"`
++ LossIDs []string `json:"loss_ids"`
++ }
++ if json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&in) != nil {
++ jsonError(w, 400, "invalid resolution request")
++ return
++ }
++ svc := s.hostResolver()
++ if in.Action == "readmit" {
++ if err := svc.Readmit(r.Context(), in.Box, in.Enrollment, in.Revision, in.LossIDs); err != nil {
++ jsonError(w, 409, err.Error())
++ return
++ }
++ } else if in.Action == "declare-lost" {
++ if _, err := svc.Declare(r.Context(), actor, r.PathValue("host"), in.Submission); err != nil {
++ jsonError(w, 409, err.Error())
++ return
++ }
++ } else {
++ jsonError(w, 400, "invalid resolution action")
++ return
++ }
++ json.NewEncoder(w).Encode(map[string]bool{"ok": true})
+@@ -34,4 +107,0 @@
+-
+-func (s *Server) handleAPIHostResolve(w http.ResponseWriter, r *http.Request) {
+- s.hostClaimRefusal(w, r)
+-}
+--- a/internal/console/server.go
++++ b/internal/console/server.go
+@@ -458,0 +459,2 @@
++ HostConsoleID string
++
+--- a/internal/console/apisystemupdate.go
++++ b/internal/console/apisystemupdate.go
+@@ -461,0 +462,3 @@
++ if _, err := s.reserveHostUpdate(ctx, &rel, in.Channel); err != nil {
++ return err
++ }
+@@ -707 +710,9 @@
+- p, err := release.RollbackSystem(r.Context(), s.cfg.Releases, release.RootSigner(s.cfg.Key), in.Component, in.Channel, addr)
++ current, currentETag, readErr := release.GetSystemPointer(r.Context(), s.cfg.Releases, release.RootSigner(s.cfg.Key).RootPublicKey(), in.Component, in.Channel)
++ if readErr == nil && len(current.History) > 0 {
++ target := current.History[0]
++ if _, err := s.reserveHostUpdate(r.Context(), &target, in.Channel); err != nil {
++ jsonError(w, http.StatusConflict, err.Error())
++ return
++ }
++ }
++ p, err := release.RollbackSystem(r.Context(), s.cfg.Releases, release.RootSigner(s.cfg.Key), in.Component, in.Channel, addr, currentETag)
+--- a/cmd/console/main.go
++++ b/cmd/console/main.go
+@@ -298,0 +299 @@
++ HostConsoleID: os.Getenv("CARLOS_CONSOLE_ID"),
+@@ -368,0 +370 @@
++ go srv.StartHostReports(context.Background())
+--- a/hostclaim_cli.go
++++ b/hostclaim_cli.go
+@@ -3,0 +4 @@
++ "bufio"
+@@ -4,0 +6 @@
++ "encoding/json"
+@@ -6,0 +9,3 @@
++ "github.com/carlosframework/platform/internal/awsinv"
++ "github.com/carlosframework/platform/internal/hostfence"
++ "github.com/carlosframework/platform/internal/hostresolve"
+@@ -10,0 +16 @@
++ "time"
+@@ -14,2 +19,0 @@
+-
+-const hostClaimPendingRuling = "awaiting Paul's ruling P2/P3 (amadan #236)"
+@@ -93 +97 @@
+- if err := sess.inspectHostResolve(context.Background(), o); err != nil {
++ if err := sess.resolveHost(context.Background(), o); err != nil {
+@@ -96,2 +99,0 @@
+- // the follow-on integration replaces this impossible branch when ActorDirectory exists.
+- log.Fatal(hostClaimPendingRuling)
+@@ -111,0 +114,60 @@
++
++func (s *apiSession) resolveHost(ctx context.Context, o hostResolveOptions) error {
++ base := "/api/cli/ops/hosts/" + url.PathEscape(o.Host) + "/resolve"
++ status, body, err := s.postJSON(ctx, base+"/inspect", map[string]any{"action": o.Action, "box": o.Box})
++ if err != nil {
++ return err
++ }
++ if status != 200 {
++ return apiErrorFromBody(status, body)
++ }
++ if o.Action == "readmit" {
++ var request map[string]any
++ if err = json.Unmarshal(body, &request); err != nil {
++ return err
++ }
++ fmt.Fprintln(os.Stdout, string(body))
++ if !o.Yes {
++ fmt.Fprint(os.Stdout, "Readmit this exact enrollment and revision? Type readmit: ")
++ line, _ := bufio.NewReader(os.Stdin).ReadString('\n')
++ if strings.TrimSpace(line) != "readmit" {
++ return errors.New("readmission not confirmed")
++ }
++ }
++ request["action"] = "readmit"
++ status, body, err = s.postJSON(ctx, base, request)
++ } else {
++ var inspection hostresolve.Inspection
++ if err = json.Unmarshal(body, &inspection); err != nil {
++ return err
++ }
++ c := inspection.Details
++ // TLS-authenticated console response; the console verifies its unchanged
++ // signed envelope on submission. The client does not call this AWS proof.
++ fmt.Fprintln(os.Stdout, string(body))
++ fmt.Fprintln(os.Stdout, "Confirm that compute remains fenced, any restart will be cold through quarantine, and the listed storage was destroyed if asserted. Type the declaration ID:")
++ line, _ := bufio.NewReader(os.Stdin).ReadString('\n')
++ if strings.TrimSpace(line) != c.ID {
++ return errors.New("fencing attestation not confirmed")
++ }
++ // --yes does not silently attest facts about compute or disks.
++ api, err := awsinv.NewEC2Reader(ctx, c.Cloud.Region)
++ if err != nil {
++ return err
++ }
++ observation, err := hostfence.ObserveForChallenge(ctx, api, c, time.Now)
++ if err != nil {
++ return err
++ }
++ submission := hostfence.Submission{Challenge: inspection.Challenge, Observation: observation, RequestID: observation.RequestID, Confirmation: c.ID, Attestation: hostfence.OperatorAttestation{MaintainComputeFence: true, ColdQuarantineStartOnly: true, Disks: c.Consequences.DestroyedStorageAssertion}}
++ status, body, err = s.postJSON(ctx, base, map[string]any{"action": o.Action, "submission": submission})
++ }
++ if err != nil {
++ return err
++ }
++ if status != 200 {
++ return apiErrorFromBody(status, body)
++ }
++ fmt.Fprintln(os.Stdout, string(body))
++ return nil
++}
+```
+
+**Step 4: verify the task.**
+
+```sh
+go test ./internal/hostresolve -count=1
+go test ./internal/console -run '^TestHostClaimRoutesArePersonOperatorFlagGatedAndInert$' -count=1
+go test . -run '^TestHostClaimFlag|^TestParseHostResolve' -count=1
+```
+
+## Integration gate for the combined release
+
+The compiler cannot establish these cross-branch properties. Keep
+`CARLOS_HOST_CLAIM` off in deployment until the complete integrated branch
+passes them. Apply the five code tasks above to the pinned parent first;
+merge the reviewed Plan 5, Plan 6 and moves implementations before choosing
+the floor tag.
+
+- Every Plan 5 lifecycle start uses `Directory.ReserveLifecycle`, then
+ `Directory.Start` around the existing owning transition. It supplies the
+ complete placement scope, both move regions and explicit participants.
+ It uses `CancelHold` after an uncertain uncommitted start and
+ `ReleaseFinishedHold` only after the operation ends. It must never fall
+ through to legacy cleanup after beginning a claim operation.
+- The first-phase owner calls `Directory.CheckHold` before accepting the
+ execution acknowledgement. Box-started owners, once adopted, use
+ `CheckBoxStart` and persist `directory_revision`; they do not write a
+ console reservation or add themselves to the roster.
+- Every Plan 6 admission and guarded host-state write supplies this concrete
+ `Directory` to `hostquarantine.Check`. Every production `hostexec.Open`
+ uses the real root guard and journal paths. No test path override enters
+ production configuration. Returning-box startup keeps its all-host gate
+ closed until Plan 6's complete retirement receipt is durable and published.
+- Connect the receipt digest to the new private metrics field only after
+ Plan 6's root completion has survived fsync and its exact object upload.
+ Receipt publication failure leaves readmission refused. Do not derive a
+ successful receipt from an empty claim inventory or an empty local route
+ table. A fresh reboot must recheck and report that boot's receipt.
+- The release workflow approves both router and console artifact hashes,
+ publishes their immutable signed snapshot, and passes it to root before
+ any candidate inspection. Normal uploads never mint a new tag mapping.
+ Independent console/bootstrap replacement scripts use the same signed
+ activation reservation, current/target/recovery hashes and root lock before
+ changing their protected paths. They retain authorization through rollback.
+ A script that still activates an app-owned symlink or invokes an unchecked
+ candidate is not eligible for the first integrated release.
+- A region HA worker consumes the stored termination reference by key and
+ digest under the owning claim transition only after L3/L4 are adopted.
+ It preserves holder stamp/session, exact local chore identity and every
+ remote retirement scope. Do not turn `ObservedAt` into the cleanup time.
+ The complete volume recorder/history producer belongs beside that consumer;
+ absent enrollment-time evidence permanently prevents automatic local-chore
+ discharge for that enrollment. The execution fence remains usable.
+- Preserve the P7 cold-only rule. This plan provides no warm-admission escape
+ and issues no clean-close certificate for the unresolved Litestream case.
+
+Before implementation lands, add the new privileged `system guard-arm` and
+`system guard-exec` subcommands to the existing system section of
+`docs/site/cli.md`, with their root-only deployment purpose. The member command
+remains `carlos ops host resolve`. Route registration and the existing
+person/operator checks remain the authority for it. Have the actual UI/docs
+strings reviewed under the repository's copy workflow when implementing;
+this document is an implementation specification, not published user copy.
+
+## Verification and review gate
+
+Use the real repository gate on the implementation branch, once. The
+following are commands to execute, not claims that the authoring sandbox ran
+its whole suite:
+
+```sh
+gofmt -l .
+git diff --check
+go build ./...
+go vet ./...
+go test ./... -count=1
+go test -race ./internal/hostactors ./internal/hostfloor ./internal/hostfence ./internal/hostresolve ./internal/hostexec -count=1
+GOOS=darwin GOARCH=amd64 go build ./...
+GOOS=windows GOARCH=amd64 go build ./...
+GOOS=linux GOARCH=arm64 go build ./...
+```
+
+Rehearse failures as well as successes:
+
+1. Missing/changed floor bytes, missing companion, a scoped signature,
+ noncanonical/prerelease tags, a wrong component image, stale private
+ reports, an unenrolled actor and a new offline console all refuse starts.
+2. Race a lifecycle reservation with an executable reservation in both
+ orders. Race a late claim start with `CancelHold`. Exercise a create on
+ an absent claim. Its create-only position must be invalidated too.
+3. Race root's update-begun write with the participant's pending write in
+ both orders. Persist a `lost` controller whose cleanup fails and prove
+ every update path continues to refuse it. An unreadable directory is
+ not an empty controller set.
+4. Interrupt arming, each update journal phase, rollback and boot recovery.
+ Verify rejection occurs before any candidate's `version` or bootstrap
+ executes, preserves the journal, and prevents an unsafe edge start.
+ Unset the flag, remove network access and remove the remote floor;
+ none lowers the armed local policy.
+5. Terminated without volume evidence writes execution evidence. Stopped,
+ shutting-down, not-found, another account/region, partial/duplicate
+ describe replies and an untrusted event source write none. Preserve the
+ first evidence time on an ambiguous PUT retry. A false or removed volume,
+ stale history, a missing initial record or an incomplete history keeps
+ local cleanup open while fencing still succeeds.
+6. Exercise same-region ASG confirmation and H17 with one dead enrollment,
+ then with a second enrollment appearing before the console CAS. The latter
+ must refuse unless the confirmation positively accounts for it too.
+ A missing EC2 result never establishes an empty region.
+7. Kill operator resolution after quarantine, after prepared loss persistence,
+ after claim CAS and before committed bookkeeping. Retry with the same
+ declaration ID. A second loss must defeat an older readmission's final
+ roster CAS. No path clears the global deny on error.
+8. Compare public `fleet.json` and public status outputs before and after
+ private reporting. Enrollment, cloud identity, boot IDs and artifact hashes
+ must stay private. Independently running console replicas each need their
+ own configured `CARLOS_CONSOLE_ID` and roster enrollment.
+
+Have Opus review the authority changes and the combined branch before landing.
+Use amadan, never GitHub PRs: push the reviewed branch, describe the actual
+change and verification, squash to one change commit, wait for green CI on
+that exact tip, then use `amadan branch merge ... -expect <tip>`. Read main's
+CI after landing. This authoring task performs none of those actions.
+
+## Rollout note: Paul performs activation
+
+Pre-flag code is limited to additive wire fields, private readiness reports,
+identity snapshots and explicitly installed root enforcement. It neither
+creates actors from heartbeats nor starts lifecycle work. Installing and
+arming the root checker must happen before the flag can be enabled; once
+armed, its enforcement is intentionally independent of that flag.
+
+Paul's activation runbook must select the first fully integrated release and
+establish the deployment's inventory and permissions. Do not put a speculative
+tag, `dev`, a SHA or this implementation branch into `min_release`.
+
+The order is:
+
+1. Reconcile every platform box, fleet registration, pool, deployment route,
+ claim reference and independently running console against deployment
+ configuration. Include offline machines. Resolve missing registries,
+ ambiguous namespaces and unexplained actors before freezing the roster.
+ Validate the region map against deployment account, AWS region and, where
+ used, the exact ASG name/ARN. Enroll existing labels only with boxid evidence
+ and the approved tofu instance ID. Heartbeat expiry removes nobody.
+2. Ship the integrated release with the flag off. Produce the immutable signed
+ router/console snapshot through the approved release workflow. Provision
+ the already-specified host guard/controller directories. Establish separate
+ console IDs and root-owned executable/recovery paths. Finish or recover
+ outstanding updates before arming.
+3. Install the checker, identity snapshot and explicit unpublished policy, then
+ arm every box using `system guard-arm`. Console hosts include `--console`.
+ The input `--policy` is an armed policy containing the exact prospective
+ floor bytes and their root-signed authorization; `--identity` is the signed
+ snapshot, not a caller-supplied tag/hash pair; `--enrollment` is checked
+ against the roster or a signed invitation. These are root deployment
+ inputs. They are not member commands and contain no root signing key.
+4. Verify every current image, `.prev`, journal and persistent service command.
+ Test boot recovery and failed recovery in the designated rehearsal scope.
+ Private console/bootstrap deploy scripts must use the same reservation and
+ pre-exec boundary. No unguarded path may survive in the activation inventory.
+5. Wait for every required private readiness report with the same prospective
+ digest. Call `Directory.PublishFloor`: freeze membership, check all actors,
+ publish auth then floor create-only, and finish publication by roster CAS.
+ A partial attempt stays refused and is retried with identical bytes.
+6. Enable `CARLOS_HOST_CLAIM`, then wait for fresh participating-process reports
+ with the flag enabled before protected starts. Keep cold-only admission.
+ Test the operator path with mocks first. Live EC2/ASG/DNS/IAM experiments
+ require separately scoped production approval or the designated sandbox.
+7. Keep region HA relocation and the L5/L20 census exceptions disabled until
+ their claim amendments and owner integrations land. Observe automatic
+ termination evidence independently; do not equate a persisted fence with
+ completed host relocation or disk retirement.
+
+IAM changes are deployment work. The console keeps no EC2 permissions. Region
+workers receive only the scoped describes needed for their placement, plus
+H17's narrowly scoped cross-region describes for membership confirmation.
+Worker writes are limited to their specified evidence/report namespaces;
+`control/host-actors.json`, the floor authorization and release identities
+remain root-signed console/release-workflow authority. Existing IAM policies
+must be reviewed against each new object prefix before activation. No policy
+or cloud resource was changed while writing this plan.
+
+## Self-review
+
+- The parent already implements stricter loss semantics than the old plan
+ text. The operator coordinator uses its durable APIs and operation-owner
+ check. It does not reimplement lost-target recovery or change the native
+ additional-write-loss representation.
+- The serving destroyed-local branch has its exact required recovery intent
+ and chore identities. Its tests preserve the serving executor and stamp.
+ Unknown timer knowledge is retained as unknown.
+- Partial resolution leaves global quarantine. A committed retry can outlive
+ its inspection challenge because it uses the original accepted record;
+ a first acceptance still requires the challenge and observation deadlines.
+- A post-floor member cannot join merely because IMDS or ASG membership
+ verifies. The signed invitation permits pre-enrollment readiness, and the
+ membership CAS checks the current floor and actual image before admission.
+ Publication freezes membership across the census and the two-object floor
+ publication; it does not rely on a last read followed by an unguarded write.
+- System apply validates before executing candidate inspection, not only
+ before rename. Rejected recovery retains evidence. Drop-ins survive ordinary
+ unit restore, and the edge has a required recovery dependency plus a guarded
+ ExecStart. Root and controllers preserve each other's durable exclusion.
+- Console artifacts are separate signed bytes. Readiness never borrows the
+ router's hash. Unsupported custom console unit/replacement workflows must
+ remain outside activation until adapted to the same root boundary.
+- The live worker only reports cloud observations it made for roster-bound
+ instances in its region. It cannot author roster membership or operator
+ attestations. Volume evidence has no influence on its execution fence.
+- This branch deliberately does not claim that L1-L20 have been adopted.
+ There is no synthetic zero-revision loss, no stale-console admission, no
+ new relocation kind, no target substitution and no fake disk destruction.
+- Compilation is necessary but does not prove the complete release's
+ admission, bootstrap, retirement, deployment-script or IAM integration.
+ Those are explicit activation/review gates above, not hidden success flags.
+
+## Authoring verification
+
+Verification results are recorded below after reconstructing these exact
+files and patches in a disposable export of the pinned parent. No Go source,
+branch or implementation change is written into the design worktree.
+
+The author reconstructed all 37 complete Go files and all five patches
+covering 12 existing files in a fresh export of
+`ca5a79d8a3821172f5829231014969f106bc33e9`. Every reconstructed source byte
+matched the compiled working export. The following checks passed on that
+reconstruction:
+
+```sh
+go build -buildvcs=false ./...
+go test -buildvcs=false -exec=/bin/true ./...
+go vet -buildvcs=false ./...
+go test -buildvcs=false -race -count=1 ./internal/hostactors ./internal/hostfloor ./internal/hostfence ./internal/hostresolve ./internal/hostexec
+go test -buildvcs=false ./internal/host -run 'TestFloorRefusal|TestApply|TestRecovery' -count=1
+go test -buildvcs=false ./internal/console -run '^TestHostClaimRoutesArePersonOperatorFlagGatedAndInert$' -count=1
+go test -buildvcs=false . -run 'TestHostClaimFlag|TestParseHostResolve' -count=1
+GOOS=darwin GOARCH=amd64 go build -buildvcs=false ./...
+GOOS=windows GOARCH=amd64 go build -buildvcs=false ./...
+GOOS=linux GOARCH=arm64 go build -buildvcs=false ./...
+gofmt -l .
+```
+
+`-exec=/bin/true` compiles every test package; it does not execute that test
+suite. The runtime checks above are targeted. An earlier broader console
+selection reached the existing `TestCensusPassRefusesMissingRegistry`, which
+could not create its loopback test listener in this sandbox. The full runtime
+gate must still run on the implementation branch. No full-suite pass is
+claimed here.
+
+Four deliberate regressions were watched fail their behavioral assertions:
+accepting `stopped` as termination; ignoring pending, active, lost and malformed
+controller files during update exclusion; expiring an already accepted loss
+retry; and recording termination before the verified response completed.
+Each mutation was removed. The final source retains all four protections.
+
+The default Go cache was tried first and was read-only in this tool sandbox.
+Verification reused the existing shared on-disk
+`/home/paulca/.cache/tmp/gocache`; it did not create a per-task Go cache or use
+RAM-backed `/tmp`. The disposable source exports and authoring scratch were
+removed after verification. The shared cache was retained.
+
+An explicitly requested Opus review process timed out without a verdict.
+No independent authority review is claimed; the Opus review remains a gate
+before implementation lands. Cross-branch activation and deployment checks
+remain the explicit gates above. Only this Markdown plan was added to the
+design worktree. Nothing was staged or committed, and no production resource,
+floor, feature flag or service was changed.