carlos / platform Public

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

CompleteCreate records the executor in touched.boxes; the record body must
agree on sidecar name and region; Validate checks obligation kind, ids,
location, owner and uniqueness.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev e0e3a82382e597b6725119f05f11ea2bcec669f5 parent aac132f
1 file changed, +76 −8
  • docs/superpowers/plans/2026-10-04-host-lifecycle-claim-1-library.md +76 −8

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 f29f80810..663e36152 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
@@ -205,6 +205,19 @@ func TestValidate(t *testing.T) {
c.Holder, c.Op = nil, Op{}
c.Obligations = []Obligation{{ID: "o", Op: "x", Kind: "park-local", Box: "b"}}
},
+ "unknown chore kind": func(c *Claim) {
+ c.Obligations = []Obligation{{ID: "o", Op: "x", Kind: "teleport", Region: "ie", Retirement: "r", Owner: "region:ie"}}
+ },
+ "locationless chore": func(c *Claim) {
+ c.Obligations = []Obligation{{ID: "o", Op: "x", Kind: "park-replica", Retirement: "r", Owner: "region:"}}
+ },
+ "owner mismatch": func(c *Claim) {
+ c.Obligations = []Obligation{{ID: "o", Op: "x", Kind: "park-local", Box: "b", Retirement: "r", Owner: "box:other"}}
+ },
+ "duplicate chore id": func(c *Claim) {
+ ob := Obligation{ID: "o", Op: "x", Kind: "park-local", Box: "b", Retirement: "r", Owner: "box:b"}
+ c.Obligations = []Obligation{ob, ob}
+ },
"old schema": func(c *Claim) { c.V = 1 },
}
for name, mut := range bad {
@@ -433,6 +446,25 @@ func (c *Claim) Validate() error {
if len(c.RetiredIncarnations) > retiredCap {
return fmt.Errorf("%w: retired_incarnations over cap", ErrMalformed)
}
+ seen := map[string]bool{}
+ for _, o := range c.Obligations {
+ if o.ID == "" || o.Op == "" || o.Retirement == "" || seen[o.ID] {
+ return fmt.Errorf("%w: obligation ids", ErrMalformed)
+ }
+ seen[o.ID] = true
+ switch o.Kind {
+ case "park-replica":
+ if o.Region == "" || o.Box != "" || o.Owner != "region:"+o.Region {
+ return fmt.Errorf("%w: park-replica %q location/owner", ErrMalformed, o.ID)
+ }
+ case "park-local":
+ if o.Box == "" || o.Region != "" || o.Owner != "box:"+o.Box {
+ return fmt.Errorf("%w: park-local %q location/owner", ErrMalformed, o.ID)
+ }
+ default:
+ return fmt.Errorf("%w: obligation kind %q", ErrMalformed, o.Kind)
+ }
+ }
return nil
}
```
@@ -493,6 +525,8 @@ func TestReadRefusesMalformed(t *testing.T) {
"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"}]}`,
+ "unknown chore kind": `{"v":2,"host":"` + testHost + `","gen":1,"op":{"kind":"purging","id":"p"},"touched":{"complete":false},` +
+ `"obligations":[{"id":"o","op":"x","kind":"teleport","region":"ie","retirement":"r","owner":"region:ie"}]}`,
} {
s2 := release.NewDirStore(t.TempDir())
if _, err := s2.Put(ctx, key, []byte(body), ""); err != nil {
@@ -844,7 +878,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","account":"bab","app":"jam","created_at":"2026-10-04T01:00:00Z"}`)
+var record = []byte(`{"schema":1,"host":"shop.bab.oncarlos.com","account":"bab","app":"jam","region":"ie","created_at":"2026-10-04T01:00:00Z"}`)
func TestCreateLifecycle(t *testing.T) {
ctx := context.Background()
@@ -862,6 +896,9 @@ func TestCreateLifecycle(t *testing.T) {
if err != nil || o.Claim.Op.Kind != KindServing || o.Claim.Op.Exec != "adi-knitting" || o.Claim.Holder.Stamp != o.Claim.Gen {
t.Fatalf("complete = %+v, %v", o.Claim, err)
}
+ if len(o.Claim.Touched.Boxes) != 1 || o.Claim.Touched.Boxes[0] != "adi-knitting" {
+ t.Fatalf("executor not recorded as touched: %+v", o.Claim.Touched)
+ }
if _, err := Update(ctx, s, o, CompleteCreate("op_c1", "adi-knitting")); !errors.Is(err, ErrStaleOp) {
t.Fatalf("replay = %v, want ErrStaleOp", err)
}
@@ -885,13 +922,17 @@ 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"}`)
+ other2 := []byte(`{"schema":1,"host":"shop.bab.oncarlos.com","account":"bab","app":"jam","region":"ie","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)
}
+ wrongRegion := []byte(`{"schema":1,"host":"shop.bab.oncarlos.com","account":"bab","app":"jam","region":"uk","created_at":"2026-10-04T01:00:00Z"}`)
+ if _, err := Update(ctx, s, reg, BeginCreate(newHolder(), wrongRegion, "op_c1", "console", now)); !errors.Is(err, ErrWrongState) {
+ t.Fatalf("record body in another region = %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}
@@ -900,6 +941,26 @@ func TestBeginCreateRefusesAnotherHostOrKey(t *testing.T) {
}
}
+func TestBeginCreateSidecar(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ now := time.Date(2026, 10, 4, 1, 0, 1, 0, time.UTC)
+ host := "bot.jam.bab.sidecar.internal"
+ sc := NewHolder{Family: "sidecar", Key: "apps/bab/jam/sidecars/bot.json", Account: "bab", App: "jam", Name: "bot",
+ Host: host, Region: "ie", CreatedAt: time.Date(2026, 10, 4, 1, 0, 0, 0, time.UTC)}
+ body := []byte(`{"name":"bot","host":"` + host + `","account":"bab","app":"jam","region":"ie","created_at":"2026-10-04T01:00:00Z"}`)
+ reg, _ := Read(ctx, s, host)
+ if _, err := Update(ctx, s, reg, BeginCreate(sc, body, "op_s1", "console", now)); err != nil {
+ t.Fatalf("sidecar create = %v", err)
+ }
+ s2 := release.NewDirStore(t.TempDir())
+ reg2, _ := Read(ctx, s2, host)
+ wrongName := []byte(`{"name":"other","host":"` + host + `","account":"bab","app":"jam","region":"ie","created_at":"2026-10-04T01:00:00Z"}`)
+ if _, err := Update(ctx, s2, reg2, BeginCreate(sc, wrongName, "op_s1", "console", now)); !errors.Is(err, ErrWrongState) {
+ t.Fatalf("sidecar body with another name = %v", err)
+ }
+}
+
func TestCancelRetiresAndRefusesReuse(t *testing.T) {
ctx := context.Background()
s := release.NewDirStore(t.TempDir())
@@ -947,18 +1008,22 @@ func keyMatches(nh NewHolder) bool {
// 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.
+// same host, account, app, sidecar name (empty for instances) and region,
+// and the same CreatedAt. The region seeds touched.regions, so it must agree.
func recordMatches(nh NewHolder, body []byte) bool {
var r struct {
+ Name string `json:"name"` // sidecar records only
Host string `json:"host"`
Account string `json:"account"`
App string `json:"app"`
+ Region string `json:"region"`
CreatedAt time.Time `json:"created_at"`
}
if err := json.Unmarshal(body, &r); err != nil {
return false
}
- return r.Host == nh.Host && r.Account == nh.Account && r.App == nh.App && r.CreatedAt.Equal(nh.CreatedAt)
+ return r.Host == nh.Host && r.Account == nh.Account && r.App == nh.App && r.Name == nh.Name &&
+ r.Region == nh.Region && r.CreatedAt.Equal(nh.CreatedAt)
}
func retire(c *Claim, inc string) {
@@ -1010,6 +1075,9 @@ func CompleteCreate(opID, execBox string) func(*Claim) error {
}
c.Op = Op{Kind: KindServing, Exec: execBox, Participants: []string{execBox}}
c.Holder.Stamp = c.Gen + 1 // the resulting gen: Update sets gen = observed+1
+ if !set(c.Touched.Boxes)[execBox] { // the executor will hold local state (§2 touched)
+ c.Touched.Boxes = append(c.Touched.Boxes, execBox)
+ }
return nil
}
}
@@ -1113,10 +1181,7 @@ func servingClaim(t *testing.T, ctx context.Context, s release.Store) Observed {
if o, err = Update(ctx, s, o, CompleteCreate("op_c1", "adi-knitting")); err != nil {
t.Fatal(err)
}
- if o, err = Update(ctx, s, o, func(c *Claim) error { c.Touched.Boxes = []string{"adi-knitting"}; return nil }); err != nil {
- t.Fatal(err)
- }
- return o
+ return o // no fixture repair: CompleteCreate itself records adi-knitting in touched.boxes
}
func TestDeleteParkFree(t *testing.T) {
@@ -1563,6 +1628,9 @@ git commit -m "hostclaim: repository gate" || true
| 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 |
+| Round 3: executor recorded as touched (P1) | Task 4 `CompleteCreate`; Task 5's fixture no longer repairs it |
+| Round 3: sidecar name and region in the record body (P2) | Task 4 `recordMatches` and `TestBeginCreateSidecar` |
+| Round 3: obligation validation (P2) | Task 1 `Validate`, with malformed reads in Task 2 |
**Types used across tasks:** `Observed{Host,…}`, `Update`, `RequireOp`,
`StartGuard`, `WriteSet`, `NewHolder`, `keyMatches`, `retire` and