carlos / platform Public

plan: host lifecycle claim Plan 1 rev 3, after Astra's round 2

Exact sidecar host identity (shared holderKeyMatches); Validate checks key,
incarnation and free-has-no-chores; create checks the record body's
identity; ToParking refuses a holderless purge fence.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev aac132f03465118927785ece04734d01f18a990e parent 1acaeb3
1 file changed, +95 −20
  • docs/superpowers/plans/2026-10-04-host-lifecycle-claim-1-library.md +95 −20

CI passed — run details

diff --git a/docs/superpowers/plans/2026-10-04-host-lifecycle-claim-1-library.md b/docs/superpowers/plans/2026-10-04-host-lifecycle-claim-1-library.md
index c12b050bb..f29f80810 100644
--- a/docs/superpowers/plans/2026-10-04-host-lifecycle-claim-1-library.md
+++ b/docs/superpowers/plans/2026-10-04-host-lifecycle-claim-1-library.md
@@ -185,10 +185,11 @@ func TestIncarnationStableAndLegacyDistinct(t *testing.T) {
}
func TestValidate(t *testing.T) {
- inc := strings.Repeat("a", 64)
+ created := time.Date(2026, 10, 4, 1, 0, 0, 0, time.UTC)
+ inc, _ := Incarnation("bab", "jam", testHost, created)
ok := &Claim{V: 2, Host: testHost, Gen: 1,
Holder: &Holder{Family: "instance", Key: "apps/bab/jam/instances/" + testHost + ".json", Account: "bab", App: "jam",
- Incarnation: inc, Location: "instances/" + testHost + "/" + inc},
+ Incarnation: inc, CreatedAt: created, Location: "instances/" + testHost + "/" + inc},
Op: Op{Kind: KindServing}}
if err := ok.Validate(); err != nil {
t.Fatalf("valid claim: %v", err)
@@ -198,7 +199,13 @@ func TestValidate(t *testing.T) {
"unknown kind": func(c *Claim) { c.Op = Op{Kind: "teleporting", ID: "x"} },
"foreign location": func(c *Claim) { c.Holder.Location = "instances/other.example.com" },
"short incarnation": func(c *Claim) { c.Holder.Incarnation = "abc" },
- "old schema": func(c *Claim) { c.V = 1 },
+ "wrong incarnation": func(c *Claim) { c.Holder.Incarnation = strings.Repeat("a", 64) },
+ "foreign key": func(c *Claim) { c.Holder.Key = "apps/zzz/jam/instances/" + testHost + ".json" },
+ "free with chores": func(c *Claim) {
+ c.Holder, c.Op = nil, Op{}
+ c.Obligations = []Obligation{{ID: "o", Op: "x", Kind: "park-local", Box: "b"}}
+ },
+ "old schema": func(c *Claim) { c.V = 1 },
}
for name, mut := range bad {
raw, _ := json.Marshal(ok)
@@ -358,6 +365,26 @@ func Incarnation(account, app, host string, created time.Time) (string, error) {
return maintenanceguard.RecordIdentity(account, app, host, created)
}
+// sidecarSuffix restates provision.SidecarSuffix (no import: avoids a cycle).
+const sidecarSuffix = ".sidecar.internal"
+
+// holderKeyMatches is the one identity check for a holder's record key and
+// host, shared by Validate, BeginCreate and Adopt. An instance's key is
+// exactly apps/<acct>/<app>/instances/<host>.json. A sidecar's key is
+// apps/<acct>/<app>/sidecars/<name>.json, and its host is exactly
+// <name>.<app>.<acct>.sidecar.internal (provision.SidecarHost).
+func holderKeyMatches(family, key, account, app, name, host string) bool {
+ base := "apps/" + account + "/" + app + "/"
+ switch family {
+ case "instance":
+ return name == "" && key == base+"instances/"+host+".json"
+ case "sidecar":
+ return controlname.ValidLabel(name) && key == base+"sidecars/"+name+".json" &&
+ host == name+"."+app+"."+account+sidecarSuffix
+ }
+ return false
+}
+
func isHex64(s string) bool {
if len(s) != 64 {
return false
@@ -380,12 +407,21 @@ func (c *Claim) Validate() error {
if c.Op.Kind != "" && c.Op.Kind != KindPurging { // a purge fence has no holder (CC21)
return fmt.Errorf("%w: op %q without holder", ErrMalformed, c.Op.Kind)
}
+ if len(c.Obligations) > 0 {
+ return fmt.Errorf("%w: a free claim with outstanding obligations", ErrMalformed)
+ }
} else {
h := c.Holder
if (h.Family != "instance" && h.Family != "sidecar") || h.Key == "" || !isHex64(h.Incarnation) ||
!controlname.ValidAccount(h.Account) || !controlname.ValidApp(h.App) {
return fmt.Errorf("%w: holder identity", ErrMalformed)
}
+ if !holderKeyMatches(h.Family, h.Key, h.Account, h.App, h.Name, c.Host) {
+ return fmt.Errorf("%w: holder key %q is not this host's", ErrMalformed, h.Key)
+ }
+ if want, err := Incarnation(h.Account, h.App, c.Host, h.CreatedAt); err != nil || want != h.Incarnation {
+ return fmt.Errorf("%w: incarnation does not match the holder's creation fields", ErrMalformed)
+ }
legacy, scoped := "instances/"+c.Host, "instances/"+c.Host+"/"+h.Incarnation
if h.Location != legacy && h.Location != scoped {
return fmt.Errorf("%w: holder location %q", ErrMalformed, h.Location)
@@ -455,6 +491,8 @@ func TestReadRefusesMalformed(t *testing.T) {
"unknown field": `{"v":2,"host":"` + testHost + `","gen":1,"op":{},"touched":{"complete":false},"surprise":1}`,
"trailing data": good + ` {"v":2}`,
"wrong host": `{"v":2,"host":"other.bab.oncarlos.com","gen":1,"op":{},"touched":{"complete":false}}`,
+ "free with chores": `{"v":2,"host":"` + testHost + `","gen":1,"op":{},"touched":{"complete":false},` +
+ `"obligations":[{"id":"o","op":"x","kind":"park-local","box":"b","retirement":"r","owner":"box:b"}]}`,
} {
s2 := release.NewDirStore(t.TempDir())
if _, err := s2.Put(ctx, key, []byte(body), ""); err != nil {
@@ -806,7 +844,7 @@ func newHolder() NewHolder {
CreatedAt: time.Date(2026, 10, 4, 1, 0, 0, 0, time.UTC)}
}
-var record = []byte(`{"schema":1,"host":"shop.bab.oncarlos.com"}`)
+var record = []byte(`{"schema":1,"host":"shop.bab.oncarlos.com","account":"bab","app":"jam","created_at":"2026-10-04T01:00:00Z"}`)
func TestCreateLifecycle(t *testing.T) {
ctx := context.Background()
@@ -847,6 +885,19 @@ func TestBeginCreateRefusesAnotherHostOrKey(t *testing.T) {
if _, err := Update(ctx, s, reg, BeginCreate(badKey, record, "op_c1", "console", now)); !errors.Is(err, ErrWrongState) {
t.Fatalf("mismatched record key = %v", err)
}
+ other2 := []byte(`{"schema":1,"host":"shop.bab.oncarlos.com","account":"bab","app":"jam","created_at":"2026-10-04T09:00:00Z"}`)
+ if _, err := Update(ctx, s, reg, BeginCreate(newHolder(), other2, "op_c1", "console", now)); !errors.Is(err, ErrWrongState) {
+ t.Fatalf("record body with another CreatedAt = %v", err)
+ }
+ if _, err := Update(ctx, s, reg, BeginCreate(newHolder(), []byte("not json"), "op_c1", "console", now)); !errors.Is(err, ErrWrongState) {
+ t.Fatalf("record body not JSON = %v", err)
+ }
+ // A sidecar key cannot claim an instance-shaped host (P1, plan review round 2).
+ sc := NewHolder{Family: "sidecar", Key: "apps/bab/jam/sidecars/shop.json", Account: "bab", App: "jam", Name: "shop",
+ Host: testHost, Region: "ie", CreatedAt: newHolder().CreatedAt}
+ if _, err := Update(ctx, s, reg, BeginCreate(sc, record, "op_c1", "console", now)); !errors.Is(err, ErrWrongState) {
+ t.Fatalf("sidecar key on a non-sidecar host = %v", err)
+ }
}
func TestCancelRetiresAndRefusesReuse(t *testing.T) {
@@ -877,9 +928,9 @@ Expected: FAIL, with "undefined: BeginCreate".
package hostclaim
import (
+ "encoding/json"
"errors"
"fmt"
- "strings"
"time"
)
@@ -890,18 +941,24 @@ type NewHolder struct {
CreatedAt time.Time
}
-// keyMatches checks the record key belongs to this account, app and host:
-// apps/<acct>/<app>/instances/<host>.json for instances, and
-// apps/<acct>/<app>/sidecars/<name>.json for sidecars.
func keyMatches(nh NewHolder) bool {
- base := "apps/" + nh.Account + "/" + nh.App + "/"
- switch nh.Family {
- case "instance":
- return nh.Key == base+"instances/"+nh.Host+".json"
- case "sidecar":
- return nh.Name != "" && nh.Key == base+"sidecars/"+nh.Name+".json" && strings.HasPrefix(nh.Host, nh.Name+".")
+ return holderKeyMatches(nh.Family, nh.Key, nh.Account, nh.App, nh.Name, nh.Host)
+}
+
+// recordMatches checks the record body that create persists as its
+// reconstruction intent is this holder's record: valid JSON naming the
+// same host, account and app, and the same CreatedAt.
+func recordMatches(nh NewHolder, body []byte) bool {
+ var r struct {
+ Host string `json:"host"`
+ Account string `json:"account"`
+ App string `json:"app"`
+ CreatedAt time.Time `json:"created_at"`
+ }
+ if err := json.Unmarshal(body, &r); err != nil {
+ return false
}
- return false
+ return r.Host == nh.Host && r.Account == nh.Account && r.App == nh.App && r.CreatedAt.Equal(nh.CreatedAt)
}
func retire(c *Claim, inc string) {
@@ -919,8 +976,11 @@ func BeginCreate(nh NewHolder, recordBody []byte, opID, by string, now time.Time
if !c.Free() {
return fmt.Errorf("%w: host is held", ErrWrongState)
}
- if nh.CreatedAt.IsZero() || len(recordBody) == 0 || opID == "" {
- return fmt.Errorf("%w: create needs CreatedAt, a record body and an op id", ErrMalformed)
+ if nh.CreatedAt.IsZero() || opID == "" {
+ return fmt.Errorf("%w: create needs CreatedAt and an op id", ErrMalformed)
+ }
+ if !recordMatches(nh, recordBody) {
+ return fmt.Errorf("%w: the record body is not this holder's record", ErrWrongState)
}
inc, err := Incarnation(nh.Account, nh.App, nh.Host, nh.CreatedAt)
if err != nil {
@@ -971,7 +1031,8 @@ func CancelCreate(opID string, now time.Time) func(*Claim) error {
}
```
-The key shapes are confirmed against main:
+`holderKeyMatches` (Task 1) is the shared identity check. The key shapes
+are confirmed against main:
- `provision.RecordKey` gives `apps/<acct>/<app>/instances/<host>.json`;
- `provision.SidecarRecordKey` gives `apps/<acct>/<app>/sidecars/<name>.json`;
- a sidecar's host is `provision.SidecarHost(name, app, sqid)`, which is
@@ -1106,6 +1167,13 @@ func TestIncompleteTouchedParksEverywhere(t *testing.T) {
}
}
+func TestToParkingRefusesAPurgeFence(t *testing.T) {
+ fence := &Claim{V: 2, Host: testHost, Gen: 3, Op: Op{Kind: KindPurging, ID: "op_p"}}
+ if err := ToParking("op_p", nil, nil, counter())(fence); !errors.Is(err, ErrWrongState) {
+ t.Fatalf("purge fence = %v, want ErrWrongState", err)
+ }
+}
+
func TestRetiredCapKeepsNewest32(t *testing.T) {
c := &Claim{}
for i := 0; i < 33; i++ {
@@ -1119,7 +1187,7 @@ func TestRetiredCapKeepsNewest32(t *testing.T) {
- [ ] **Step 2: Run the test to verify it fails**
-Run: `go test ./internal/hostclaim/ -run 'TestDelete|TestIncomplete|TestRetiredCap' -count=1`
+Run: `go test ./internal/hostclaim/ -run 'TestDelete|TestIncomplete|TestToParking|TestRetiredCap' -count=1`
Expected: FAIL, with "undefined: BeginDelete".
- [ ] **Step 3: Write the minimal implementation**
@@ -1167,6 +1235,9 @@ func ToParking(opID string, allRegions, allBoxes []string, newID func() string)
if c.Op.Kind != KindDeleting && c.Op.Kind != KindPurging {
return fmt.Errorf("%w: kind %q", ErrWrongState, c.Op.Kind)
}
+ if c.Holder == nil { // a holderless purge fence (CC21) has nothing to park
+ return fmt.Errorf("%w: no holder to park", ErrWrongState)
+ }
regions, boxes := WriteSet(c, allRegions, allBoxes)
for _, r := range regions {
c.Obligations = append(c.Obligations, Obligation{ID: newID(), Op: opID, Kind: "park-replica", Region: r, Retirement: newID(), Owner: "region:" + r})
@@ -1223,7 +1294,7 @@ func FreeAfterPark(opID string) func(*Claim) error {
- [ ] **Step 4: Run the test to verify it passes**
-Run: `go test ./internal/hostclaim/ -run 'TestDelete|TestIncomplete|TestRetiredCap' -count=1`
+Run: `go test ./internal/hostclaim/ -run 'TestDelete|TestIncomplete|TestToParking|TestRetiredCap' -count=1`
Expected: PASS.
- [ ] **Step 5: Commit**
@@ -1488,6 +1559,10 @@ git commit -m "hostclaim: repository gate" || true
| Central gen (P2) | Task 2 |
| Roadmap dependencies and activation boundary (P2) | Roadmap |
| Repository gate (P3) | Task 8 |
+| Round 2: exact sidecar host check (P1) | Task 1 (`holderKeyMatches`), tested in Task 4 |
+| Round 2: semantic Validate (P2) | Task 1, plus malformed reads in Task 2 |
+| Round 2: record body identity (P2) | Task 4 |
+| Round 2: purge fence in ToParking (P2) | Task 5 |
**Types used across tasks:** `Observed{Host,…}`, `Update`, `RequireOp`,
`StartGuard`, `WriteSet`, `NewHolder`, `keyMatches`, `retire` and