carlos / platform Public

hostclaim: create (record body in intent, host-bound), complete (stamp) and cancel

Co-Authored-By: Codex (gpt-6-astra) <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 ed1c2c96a40a87618366727b5d7636050018bb1a parent 8ef0099
2 files changed, +226 −0
  • internal/hostclaim/create.go +111 −0
  • internal/hostclaim/create_test.go +115 −0
diff --git a/internal/hostclaim/create.go b/internal/hostclaim/create.go
new file mode 100644
index 000000000..41c3663c5
--- /dev/null
+++ b/internal/hostclaim/create.go
@@ -0,0 +1,111 @@
+package hostclaim
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "time"
+)
+
+var ErrRetiredIncarnation = errors.New("hostclaim: incarnation was retired on this host")
+
+type NewHolder struct {
+ Family, Key, Account, App, Name, Host, Region string
+ CreatedAt time.Time
+}
+
+func keyMatches(nh NewHolder) bool {
+ 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, 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.Name == nh.Name &&
+ r.Region == nh.Region && r.CreatedAt.Equal(nh.CreatedAt)
+}
+
+func retire(c *Claim, inc string) {
+ c.RetiredIncarnations = append([]string{inc}, c.RetiredIncarnations...)
+ if len(c.RetiredIncarnations) > retiredCap {
+ c.RetiredIncarnations = c.RetiredIncarnations[:retiredCap]
+ }
+}
+
+func BeginCreate(nh NewHolder, recordBody []byte, opID, by string, now time.Time) func(*Claim) error {
+ return func(c *Claim) error {
+ if nh.Host != c.Host || !keyMatches(nh) {
+ return fmt.Errorf("%w: create inputs are not this host's", ErrWrongState)
+ }
+ if !c.Free() {
+ return fmt.Errorf("%w: host is held", ErrWrongState)
+ }
+ 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 {
+ return err
+ }
+ for _, r := range c.RetiredIncarnations {
+ if r == inc {
+ return ErrRetiredIncarnation
+ }
+ }
+ c.Holder = &Holder{Family: nh.Family, Key: nh.Key, Account: nh.Account, App: nh.App, Name: nh.Name,
+ Incarnation: inc, CreatedAt: nh.CreatedAt, Location: "instances/" + nh.Host + "/" + inc}
+ c.Op = Op{Kind: KindCreating, ID: opID, By: by, StartedAt: now.UTC(),
+ Intent: map[string]string{"key": nh.Key, "record": string(recordBody)}}
+ c.Touched = Touched{Complete: true, Regions: []string{nh.Region}}
+ return nil
+ }
+}
+
+func CompleteCreate(opID, execBox string) func(*Claim) error {
+ return func(c *Claim) error {
+ if err := RequireOp(opID)(c); err != nil {
+ return err
+ }
+ if c.Op.Kind != KindCreating || execBox == "" {
+ return fmt.Errorf("%w: kind %q", ErrWrongState, c.Op.Kind)
+ }
+ 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
+ }
+}
+
+func CancelCreate(opID string, now time.Time) func(*Claim) error {
+ return func(c *Claim) error {
+ if err := RequireOp(opID)(c); err != nil {
+ return err
+ }
+ if c.Op.Kind != KindCreating {
+ return fmt.Errorf("%w: kind %q", ErrWrongState, c.Op.Kind)
+ }
+ inc, key := c.Holder.Incarnation, c.Holder.Key
+ retire(c, inc)
+ c.CancelledCreates = append(c.CancelledCreates, CancelledCreate{Key: key, Incarnation: inc, Until: now.UTC().Add(time.Hour)})
+ c.Holder, c.Op, c.Touched = nil, Op{}, Touched{}
+ return nil
+ }
+}
diff --git a/internal/hostclaim/create_test.go b/internal/hostclaim/create_test.go
new file mode 100644
index 000000000..e404832ff
--- /dev/null
+++ b/internal/hostclaim/create_test.go
@@ -0,0 +1,115 @@
+package hostclaim
+
+import (
+ "context"
+ "errors"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/release"
+)
+
+func newHolder() NewHolder {
+ return NewHolder{Family: "instance", Key: "apps/bab/jam/instances/" + testHost + ".json",
+ Account: "bab", App: "jam", Host: testHost, Region: "ie",
+ 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","region":"ie","created_at":"2026-10-04T01:00:00Z"}`)
+
+func TestCreateLifecycle(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ now := time.Date(2026, 10, 4, 1, 0, 1, 0, time.UTC)
+ reg, _ := Read(ctx, s, testHost)
+ o, err := Update(ctx, s, reg, BeginCreate(newHolder(), record, "op_c1", "console", now))
+ if err != nil || o.Claim.Op.Kind != KindCreating || o.Claim.Op.Intent["record"] != string(record) {
+ t.Fatalf("begin = %+v, %v", o, err)
+ }
+ if o.Claim.Holder.Location != "instances/"+testHost+"/"+o.Claim.Holder.Incarnation {
+ t.Fatalf("location = %q", o.Claim.Holder.Location)
+ }
+ o, err = Update(ctx, s, o, CompleteCreate("op_c1", "adi-knitting"))
+ 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)
+ }
+ if _, err := Update(ctx, s, reg, BeginCreate(newHolder(), record, "op_c2", "console", now)); !errors.Is(err, ErrConflict) {
+ t.Fatalf("stale create = %v, want ErrConflict", err)
+ }
+}
+
+func TestBeginCreateRefusesAnotherHostOrKey(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ now := time.Date(2026, 10, 4, 1, 0, 1, 0, time.UTC)
+ reg, _ := Read(ctx, s, testHost)
+ other := newHolder()
+ other.Host = "other.bab.oncarlos.com"
+ if _, err := Update(ctx, s, reg, BeginCreate(other, record, "op_c1", "console", now)); !errors.Is(err, ErrWrongState) {
+ t.Fatalf("another host's create on this claim = %v", err)
+ }
+ badKey := newHolder()
+ badKey.Key = "apps/zzz/jam/instances/" + testHost + ".json"
+ 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","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}
+ 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 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())
+ now := time.Date(2026, 10, 4, 1, 0, 1, 0, time.UTC)
+ reg, _ := Read(ctx, s, testHost)
+ o, _ := Update(ctx, s, reg, BeginCreate(newHolder(), record, "op_c1", "console", now))
+ inc := o.Claim.Holder.Incarnation
+ o, err := Update(ctx, s, o, CancelCreate("op_c1", now))
+ if err != nil || !o.Claim.Free() || len(o.Claim.CancelledCreates) != 1 || o.Claim.RetiredIncarnations[0] != inc {
+ t.Fatalf("cancel = %+v, %v", o.Claim, err)
+ }
+ if _, err := Update(ctx, s, o, BeginCreate(newHolder(), record, "op_c3", "console", now)); !errors.Is(err, ErrRetiredIncarnation) {
+ t.Fatalf("reuse = %v, want ErrRetiredIncarnation", err)
+ }
+}