plan: claim Plan 8 rev 3, answers review round 2 (one P1, two P2)
Co-Authored-By: Codex <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 file changed,
+319
−28
docs/superpowers/plans/2026-10-05-host-lifecycle-claim-8-leases-floor.md+319 −28
CI passed — run details
diff --git a/docs/superpowers/plans/2026-10-05-host-lifecycle-claim-8-leases-floor.md b/docs/superpowers/plans/2026-10-05-host-lifecycle-claim-8-leases-floor.md| index c7d2ff41c..f0cab8138 100644 |
| --- a/docs/superpowers/plans/2026-10-05-host-lifecycle-claim-8-leases-floor.md |
| +++ b/docs/superpowers/plans/2026-10-05-host-lifecycle-claim-8-leases-floor.md |
| @@ -1,6 +1,6 @@ |
| # Host Lifecycle Claim, Plan 8: inert lease and lost-box contracts |
| -Rev 2 answers the independent review (one P1, two P2). |
| +Rev 3 answers review round 2 (one P1, two P2). |
| This plan follows claim spec **rev 6.20**, §7.3 D2–D6. These contracts remain **PROPOSED**. Paul has not ruled on P1–P3 in §10.1 and amadan discussion **#236**. Landing these libraries does not authorize activation. |
| @@ -2172,6 +2172,7 @@ import ( |
| "strings" |
| "github.com/aws/aws-sdk-go-v2/aws" |
| + awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" |
| awsconfig "github.com/aws/aws-sdk-go-v2/config" |
| "github.com/aws/aws-sdk-go-v2/service/ec2" |
| ) |
| @@ -2189,6 +2190,7 @@ type ExactInstance struct { |
| Region string `json:"region"` |
| InstanceID string `json:"instance_id"` |
| State string `json:"state"` |
| + RequestID string `json:"request_id"` |
| } |
| // ExactEC2API is the DescribeInstances slice of EC2API. The production EC2 |
| @@ -2237,6 +2239,10 @@ func VerifyExactStoppedInstance(ctx context.Context, api ExactEC2API, account, r |
| if out == nil || aws.ToString(out.NextToken) != "" || len(out.Reservations) != 1 { |
| return ExactInstance{}, fmt.Errorf("%w: DescribeInstances returned a partial or ambiguous result", ErrExactInstance) |
| } |
| + requestID, ok := awsmiddleware.GetRequestIDMetadata(out.ResultMetadata) |
| + if !ok || strings.TrimSpace(requestID) == "" { |
| + return ExactInstance{}, ErrExactInstance |
| + } |
| r := out.Reservations[0] |
| owner := aws.ToString(r.OwnerId) |
| if owner == "" || owner != wantAccount || len(r.Instances) != 1 { |
| @@ -2251,7 +2257,7 @@ func VerifyExactStoppedInstance(ctx context.Context, api ExactEC2API, account, r |
| if state != "stopped" && state != "terminated" { |
| return ExactInstance{}, fmt.Errorf("%w: instance %s is %q", ErrExactInstance, wantID, state) |
| } |
| - return ExactInstance{Account: owner, Region: wantRegion, InstanceID: gotID, State: state}, nil |
| + return ExactInstance{Account: owner, Region: wantRegion, InstanceID: gotID, State: state, RequestID: requestID}, nil |
| } |
| ``` |
| @@ -2267,8 +2273,10 @@ import ( |
| "testing" |
| "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/aws/smithy-go/middleware" |
| ) |
| type exactEC2 struct { |
| @@ -2289,10 +2297,12 @@ func (f *exactEC2) DescribeInstances(_ context.Context, in *ec2.DescribeInstance |
| } |
| func exactOutput(owner, id string, state types.InstanceStateName) *ec2.DescribeInstancesOutput { |
| - return &ec2.DescribeInstancesOutput{Reservations: []types.Reservation{{ |
| + out := &ec2.DescribeInstancesOutput{Reservations: []types.Reservation{{ |
| OwnerId: aws.String(owner), |
| Instances: []types.Instance{{InstanceId: aws.String(id), State: &types.InstanceState{Name: state}}}, |
| }}} |
| + awsmiddleware.SetRequestIDMetadata(&out.ResultMetadata, "aws-request-123") |
| + return out |
| } |
| func TestVerifyExactStoppedInstanceAcceptsOnlyStoppedOrTerminated(t *testing.T) { |
| @@ -2303,7 +2313,7 @@ func TestVerifyExactStoppedInstanceAcceptsOnlyStoppedOrTerminated(t *testing.T) |
| if err != nil { |
| t.Fatal(err) |
| } |
| - if got.Account != "123456789012" || got.Region != "eu-west-1" || got.InstanceID != "i-123" || got.State != string(state) { |
| + if got.Account != "123456789012" || got.Region != "eu-west-1" || got.InstanceID != "i-123" || got.State != string(state) || got.RequestID != "aws-request-123" { |
| t.Fatalf("observation = %+v", got) |
| } |
| if len(api.in.InstanceIds) != 1 || api.in.InstanceIds[0] != "i-123" { |
| @@ -2319,6 +2329,21 @@ func TestVerifyExactStoppedInstanceAcceptsOnlyStoppedOrTerminated(t *testing.T) |
| func TestVerifyExactStoppedInstanceRefusesEveryIncompleteOrLiveAnswer(t *testing.T) { |
| notFound := errors.New("InvalidInstanceID.NotFound") |
| cases := map[string]*exactEC2{ |
| + "missing-request-id": {out: func() *ec2.DescribeInstancesOutput { |
| + o := exactOutput("123456789012", "i-123", types.InstanceStateNameStopped) |
| + o.ResultMetadata = middleware.Metadata{} |
| + return o |
| + }()}, |
| + "empty-request-id": {out: func() *ec2.DescribeInstancesOutput { |
| + o := exactOutput("123456789012", "i-123", types.InstanceStateNameStopped) |
| + awsmiddleware.SetRequestIDMetadata(&o.ResultMetadata, "") |
| + return o |
| + }()}, |
| + "blank-request-id": {out: func() *ec2.DescribeInstancesOutput { |
| + o := exactOutput("123456789012", "i-123", types.InstanceStateNameStopped) |
| + awsmiddleware.SetRequestIDMetadata(&o.ResultMetadata, " ") |
| + return o |
| + }()}, |
| "not-found": {err: notFound}, |
| "nil": {}, |
| "no-reservation": {out: &ec2.DescribeInstancesOutput{}}, |
| @@ -2360,6 +2385,7 @@ import ( |
| "encoding/json" |
| "errors" |
| "fmt" |
| + "strings" |
| "time" |
| "github.com/carlosframework/platform/internal/awsinv" |
| @@ -2435,6 +2461,7 @@ type CloudObservation struct { |
| Region string `json:"region"` |
| InstanceID string `json:"instance_id"` |
| State string `json:"state"` |
| + RequestID string `json:"request_id"` |
| } |
| type OperatorAttestation struct { |
| @@ -2557,7 +2584,7 @@ func ObserveForChallenge(ctx context.Context, api awsinv.ExactEC2API, c Challeng |
| // observed_at is the conservative request start, not the response time: |
| // a DescribeInstances call blocked for a minute cannot make an old read |
| // appear fresh merely by returning now. |
| - o := CloudObservation{ObservedAt: started, Account: got.Account, Region: got.Region, InstanceID: got.InstanceID, State: got.State} |
| + o := CloudObservation{ObservedAt: started, Account: got.Account, Region: got.Region, InstanceID: got.InstanceID, State: got.State, RequestID: got.RequestID} |
| if err := ValidateObservation(c, o, consumedAt); err != nil { |
| return CloudObservation{}, err |
| } |
| @@ -2573,7 +2600,7 @@ func ValidateObservation(c Challenge, o CloudObservation, now time.Time) error { |
| return err |
| } |
| if o.Account != c.Cloud.Account || o.Region != c.Cloud.Region || o.InstanceID != c.Cloud.InstanceID || |
| - (o.State != "stopped" && o.State != "terminated") { |
| + (o.State != "stopped" && o.State != "terminated") || strings.TrimSpace(o.RequestID) == "" { |
| return fmt.Errorf("%w: cloud identity or state does not match the challenge", ErrObservation) |
| } |
| if !o.ObservedAt.After(c.IssuedAt) || o.ObservedAt.After(now.Add(ObservationMaxSkew)) || now.Sub(o.ObservedAt) > ObservationMaxAge { |
| @@ -2604,8 +2631,10 @@ import ( |
| "time" |
| "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/aws/smithy-go/middleware" |
| "github.com/carlosframework/platform/internal/release" |
| ) |
| @@ -2695,15 +2724,36 @@ func TestObserveForChallengeBindsExactStoppedInstance(t *testing.T) { |
| api := ec2Answer{out: &ec2.DescribeInstancesOutput{Reservations: []types.Reservation{{ |
| OwnerId: aws.String(c.Cloud.Account), Instances: []types.Instance{{InstanceId: aws.String(c.Cloud.InstanceID), State: &types.InstanceState{Name: types.InstanceStateNameStopped}}}, |
| }}}} |
| + awsmiddleware.SetRequestIDMetadata(&api.out.ResultMetadata, "aws-request-123") |
| times := []time.Time{issued.Add(time.Second), issued.Add(2 * time.Second)} |
| i := 0 |
| o, err := ObserveForChallenge(t.Context(), api, c, func() time.Time { v := times[i]; i++; return v }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| - if o.State != "stopped" || o.ObservedAt != times[0] { |
| + if o.State != "stopped" || o.ObservedAt != times[0] || o.RequestID != "aws-request-123" { |
| t.Fatalf("observation = %+v", o) |
| } |
| + |
| +} |
| + |
| +func TestObserveForChallengeRefusesMissingRequestID(t *testing.T) { |
| + issued := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC) |
| + c := challengeFixture(t, issued) |
| + for _, id := range []string{"missing", "", " "} { |
| + t.Run(id, func(t *testing.T) { |
| + api := ec2Answer{out: &ec2.DescribeInstancesOutput{Reservations: []types.Reservation{{ |
| + OwnerId: aws.String(c.Cloud.Account), Instances: []types.Instance{{InstanceId: aws.String(c.Cloud.InstanceID), State: &types.InstanceState{Name: types.InstanceStateNameStopped}}}, |
| + }}}} |
| + api.out.ResultMetadata = middleware.Metadata{} |
| + if id != "missing" { |
| + awsmiddleware.SetRequestIDMetadata(&api.out.ResultMetadata, id) |
| + } |
| + if _, err := ObserveForChallenge(t.Context(), api, c, func() time.Time { return issued.Add(time.Second) }); !errors.Is(err, ErrObservation) { |
| + t.Fatalf("missing request ID accepted: %v", err) |
| + } |
| + }) |
| + } |
| } |
| func TestObserveForChallengeRefusesAReadThatBlockedPastFreshness(t *testing.T) { |
| @@ -2712,6 +2762,7 @@ func TestObserveForChallengeRefusesAReadThatBlockedPastFreshness(t *testing.T) { |
| api := ec2Answer{out: &ec2.DescribeInstancesOutput{Reservations: []types.Reservation{{ |
| OwnerId: aws.String(c.Cloud.Account), Instances: []types.Instance{{InstanceId: aws.String(c.Cloud.InstanceID), State: &types.InstanceState{Name: types.InstanceStateNameStopped}}}, |
| }}}} |
| + awsmiddleware.SetRequestIDMetadata(&api.out.ResultMetadata, "aws-request-123") |
| times := []time.Time{issued.Add(time.Second), issued.Add(62 * time.Second)} |
| i := 0 |
| if _, err := ObserveForChallenge(t.Context(), api, c, func() time.Time { v := times[i]; i++; return v }); !errors.Is(err, ErrObservation) { |
| @@ -2722,11 +2773,13 @@ func TestObserveForChallengeRefusesAReadThatBlockedPastFreshness(t *testing.T) { |
| func TestValidateObservationFreshnessAndBinding(t *testing.T) { |
| issued := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC) |
| c := challengeFixture(t, issued) |
| - valid := CloudObservation{ObservedAt: issued.Add(10 * time.Second), Account: c.Cloud.Account, Region: c.Cloud.Region, InstanceID: c.Cloud.InstanceID, State: "terminated"} |
| + valid := CloudObservation{ObservedAt: issued.Add(10 * time.Second), Account: c.Cloud.Account, Region: c.Cloud.Region, InstanceID: c.Cloud.InstanceID, State: "terminated", RequestID: "aws-request-123"} |
| if err := ValidateObservation(c, valid, issued.Add(60*time.Second)); err != nil { |
| t.Fatal(err) |
| } |
| checks := []CloudObservation{ |
| + func() CloudObservation { o := valid; o.RequestID = ""; return o }(), |
| + func() CloudObservation { o := valid; o.RequestID = " "; return o }(), |
| func() CloudObservation { o := valid; o.ObservedAt = issued; return o }(), |
| func() CloudObservation { o := valid; o.ObservedAt = issued.Add(11 * time.Second); return o }(), |
| func() CloudObservation { o := valid; o.Account = "999999999999"; return o }(), |
| @@ -2734,7 +2787,10 @@ func TestValidateObservationFreshnessAndBinding(t *testing.T) { |
| } |
| for i, o := range checks { |
| now := issued.Add(70 * time.Second) |
| - if i == 1 { |
| + if i < 2 { |
| + now = issued.Add(60 * time.Second) |
| + } |
| + if i == 3 { |
| now = issued.Add(5 * time.Second) |
| } |
| if err := ValidateObservation(c, o, now); !errors.Is(err, ErrObservation) { |
| @@ -3135,9 +3191,10 @@ func Open(ctx context.Context, cfg Config) (*Controller, error) { |
| cctx, cancel := context.WithCancel(ctx) |
| c := &Controller{cfg: cfg, state: State{V: 1, Binding: cfg.Binding, State: "pending", Expires: wall.Add(TTL), SafeUntil: boot + int64(TTL-Margin)}, ctx: cctx, cancel: cancel, |
| ready: make(chan struct{}), lost: make(chan struct{}), done: make(chan struct{}), watchDone: make(chan struct{}), cleanupDone: make(chan struct{})} |
| - // A replacement may not hold the lifetime lock while its predecessor's |
| - // active bytes are visible. Hold the host guard across acquisition and the |
| - // pending write. Never wait for the lifetime lock while holding the guard. |
| + // Hold the host guard across lifetime-lock acquisition and the pending |
| + // write. Acquire the live lock only after pending state is durable, so |
| + // orphaned active bytes cannot regain admission. Never wait for the |
| + // lifetime lock while holding the guard. |
| for { |
| err = cfg.Guard.WithLocal(ctx, cfg.Binding.Host, func() error { |
| var lockErr error |
| @@ -3145,17 +3202,6 @@ func Open(ctx context.Context, cfg Config) (*Controller, error) { |
| if lockErr != nil { |
| return lockErr |
| } |
| - pending := false |
| - defer func() { |
| - if !pending { |
| - if c.unlive != nil { |
| - c.unlive() |
| - c.unlive = nil |
| - } |
| - c.unlock() |
| - c.unlock = nil |
| - } |
| - }() |
| cfg.CloseAdmission() |
| cfg.CancelWork() |
| d, openErr := openLocal(cfg.Guard.DataDir) |
| @@ -3167,10 +3213,9 @@ func Open(ctx context.Context, cfg Config) (*Controller, error) { |
| return writeErr |
| } |
| c.unlive, lockErr = tryAcquireLive(cfg.LockDir, cfg.Binding.Host) |
| - pending = lockErr == nil |
| return lockErr |
| }) |
| - if !errors.Is(err, ErrControllerBusy) { |
| + if c.unlock != nil || !errors.Is(err, ErrControllerBusy) { |
| break |
| } |
| select { |
| @@ -3181,20 +3226,39 @@ func Open(ctx context.Context, cfg Config) (*Controller, error) { |
| } |
| break |
| } |
| + // Once exclusion is ours, orphaned processes must be cleaned even when |
| + // the pending write fails. Cleanup must survive caller cancellation and |
| + // run independently of lost-state persistence, which can retry forever. |
| + var cleaned <-chan error |
| + if c.unlock != nil { |
| + result := make(chan error, 1) |
| + cleaned = result |
| + go func() { result <- cfg.Cleanup(context.Background()) }() |
| + } |
| if err == nil { |
| - err = cfg.Cleanup(ctx) |
| + err = <-cleaned |
| + cleaned = nil |
| } |
| if err == nil { |
| err = c.authority(ctx) |
| } |
| if err != nil { |
| c.state.State = "lost" |
| + hadLive := c.unlive != nil |
| if c.unlive != nil { |
| c.unlive() |
| c.unlive = nil |
| } |
| if c.unlock != nil { |
| - _ = c.save(c.state) |
| + // Before live-lock acquisition, no local state can authorize work. |
| + // A failed pending write (including an unsafe path) must not make |
| + // cleanup wait for another write to the same failed destination. |
| + if hadLive { |
| + err = errors.Join(err, c.save(c.state)) |
| + } |
| + if cleaned != nil { |
| + err = errors.Join(err, <-cleaned) |
| + } |
| c.unlock() |
| } |
| cancel() |
| @@ -4464,6 +4528,7 @@ import ( |
| "golang.org/x/sys/unix" |
| "os" |
| "os/exec" |
| + "sync" |
| "sync/atomic" |
| "testing" |
| "time" |
| @@ -4764,6 +4829,123 @@ func TestCheckLocalRefusesLostLiveLock(t *testing.T) { |
| t.Fatalf("old active record authorized an independent process: %v", err) |
| } |
| } |
| + |
| +func TestOpenOrphanControllerPendingWriteFailureCleansBeforeReleasingExclusion(t *testing.T) { |
| + cfg, _, clock, store, _ := fixture(t) |
| + wall, boot, _ := clock.Now() |
| + orphan := State{V: 1, Binding: cfg.Binding, State: "active", LeaseETag: "orphan-etag", Expires: wall.Add(TTL), SafeUntil: boot + int64(TTL-Margin)} |
| + orphan.ControllerID = "dead-controller" |
| + d, err := openLocal(cfg.Guard.DataDir) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer d.close() |
| + if err := d.write(orphan); err != nil { |
| + t.Fatal(err) |
| + } |
| + // The controller's locks have vanished, but its active bytes and a host |
| + // process survived. Admission refusal alone cannot stop that process. |
| + process := exec.Command("sleep", "60") |
| + if err := process.Start(); err != nil { |
| + t.Fatal(err) |
| + } |
| + defer process.Process.Kill() |
| + hostExited := make(chan struct{}) |
| + go func() { _ = process.Wait(); close(hostExited) }() |
| + allowJoin := make(chan struct{}) |
| + var joinOnce sync.Once |
| + defer joinOnce.Do(func() { close(allowJoin) }) |
| + cleaned := make(chan struct{}) |
| + cleanupResult := make(chan error, 1) |
| + cfg.Cleanup = func(ctx context.Context) error { |
| + cleanupResult <- ctx.Err() |
| + if err := process.Process.Kill(); err != nil { |
| + return err |
| + } |
| + <-hostExited |
| + close(cleaned) |
| + <-allowJoin |
| + return nil |
| + } |
| + ctx, cancel := context.WithCancel(t.Context()) |
| + defer cancel() |
| + var noSpace atomic.Bool |
| + var failures atomic.Int32 |
| + noSpace.Store(true) |
| + defer noSpace.Store(false) |
| + cfg.writeLocal = func(d *localDir, s State) error { |
| + if noSpace.Load() { |
| + if s.State == "pending" { |
| + cancel() // cleanup must not inherit the failed opener's lifetime |
| + } |
| + original := d.sync |
| + d.sync = func(int) error { return unix.ENOSPC } |
| + defer func() { d.sync = original }() |
| + err := d.write(s) |
| + if errors.Is(err, unix.ENOSPC) { |
| + failures.Add(1) |
| + } |
| + return err |
| + } |
| + return d.write(s) |
| + } |
| + done := make(chan error, 1) |
| + go func() { _, err := Open(ctx, cfg); done <- err }() |
| + select { |
| + case <-cleaned: |
| + case <-time.After(2 * time.Second): |
| + t.Fatal("failed pending write skipped orphan process cleanup") |
| + } |
| + if err := <-cleanupResult; err != nil { |
| + t.Fatalf("cleanup inherited caller cancellation: %v", err) |
| + } |
| + until(t, func() bool { return failures.Load() >= 1 }) |
| + if old, err := d.read(orphan.Host); err != nil || old != orphan { |
| + t.Fatalf("failed persistence did not retain orphan active bytes: %+v %v", old, err) |
| + } |
| + assertExcluded := func() { |
| + t.Helper() |
| + if unlock, err := tryAcquire(cfg.LockDir, orphan.Host); !errors.Is(err, ErrControllerBusy) { |
| + if unlock != nil { |
| + unlock() |
| + } |
| + t.Fatalf("exclusion released before cleanup joined: %v", err) |
| + } |
| + select { |
| + case err := <-done: |
| + t.Fatalf("Open returned before cleanup joined: %v", err) |
| + default: |
| + } |
| + } |
| + assertExcluded() |
| + if err := CheckLocal(cfg.Guard.DataDir, orphan.Binding, clock, cfg.LockDir); !errors.Is(err, ErrLost) { |
| + t.Fatalf("orphan bytes admitted work: %v", err) |
| + } |
| + // Persistence is still unavailable when cleanup joins. No live lock was |
| + // acquired, so orphan bytes cannot authorize work after exclusion ends. |
| + joinOnce.Do(func() { close(allowJoin) }) |
| + select { |
| + case err := <-done: |
| + if !errors.Is(err, unix.ENOSPC) { |
| + t.Fatalf("Open lost pending-write failure: %v", err) |
| + } |
| + case <-time.After(2 * time.Second): |
| + t.Fatal("Open did not join cleanup while persistence remained unavailable") |
| + } |
| + unlock, err := tryAcquire(cfg.LockDir, orphan.Host) |
| + if err != nil { |
| + t.Fatalf("exclusion retained after cleanup joined: %v", err) |
| + } |
| + unlock() |
| + if err := CheckLocal(cfg.Guard.DataDir, orphan.Binding, clock, cfg.LockDir); !errors.Is(err, ErrLost) { |
| + t.Fatalf("released exclusion revived orphan bytes: %v", err) |
| + } |
| + store.mu.Lock() |
| + defer store.mu.Unlock() |
| + if store.calls != 0 { |
| + t.Fatalf("failed startup attempted %d lease writes", store.calls) |
| + } |
| +} |
| ``` |
| - [ ] **Run the task tests.** |
| @@ -4806,6 +4988,96 @@ go test ./internal/lease -run ExecutionStore -count=1 |
| + s.cliRoute("POST /api/cli/ops/hosts/{host}/resolve/inspect", svcrole.Set{}, s.handleAPIHostResolveInspect) |
| + s.cliRoute("POST /api/cli/ops/hosts/{host}/resolve", svcrole.Set{}, s.handleAPIHostResolve) |
| + } |
| +--- a/internal/console/svcroutes_test.go |
| ++++ b/internal/console/svcroutes_test.go |
| +@@ -212,21 +212,4 @@ |
| +- s := newConsoleFixture(t).srv |
| +- |
| +- var empty []string |
| +- for pattern, set := range s.cliRoles { |
| +- if set.Empty() { |
| +- empty = append(empty, pattern) |
| +- } |
| +- } |
| +- sort.Strings(empty) |
| +- wantClosed := append([]string(nil), closedDoors...) |
| +- sort.Strings(wantClosed) |
| +- if !reflect.DeepEqual(empty, wantClosed) { |
| +- t.Errorf("doors registered with an empty role set:\n got %q\nwant %q\n"+ |
| +- "An unexpected empty Set is a door nobody can reach; a missing one is a door that should be shut and is not.", |
| +- empty, wantClosed) |
| +- } |
| +- |
| +- // scanCLISource fails on its own for a door registered outside |
| +- // cliRoute; this pairs what it DID find against what the running server |
| +- // recorded. The equality is what makes a walker that silently found |
| +- // nothing fail too — no magic minimum needed. |
| ++ hostClaimDoors := []string{ |
| ++ "POST /api/cli/ops/hosts/{host}/resolve/inspect", |
| ++ "POST /api/cli/ops/hosts/{host}/resolve", |
| ++ } |
| +@@ -234,8 +217,53 @@ |
| +- for _, pattern := range registered { |
| +- if _, ok := s.cliRoles[pattern]; !ok { |
| +- t.Errorf("%s: the source registers it, the server did not record it", pattern) |
| +- } |
| +- } |
| +- if len(s.cliRoles) != len(registered) { |
| +- t.Errorf("cliRoles holds %d doors, the source registers %d — the two must be the same set", |
| +- len(s.cliRoles), len(registered)) |
| ++ for _, tc := range []struct { |
| ++ name string |
| ++ enabled bool |
| ++ }{{"flag-off", false}, {"flag-on", true}} { |
| ++ t.Run(tc.name, func(t *testing.T) { |
| ++ st := openTestStore(t) |
| ++ s, err := NewServer(Config{Origin: "https://console.example", Keymail: "https://keymail.example", HostClaim: tc.enabled}, st) |
| ++ if err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ var empty []string |
| ++ for pattern, set := range s.cliRoles { |
| ++ if set.Empty() { |
| ++ empty = append(empty, pattern) |
| ++ } |
| ++ } |
| ++ wantClosed := append([]string(nil), closedDoors...) |
| ++ if tc.enabled { |
| ++ wantClosed = append(wantClosed, hostClaimDoors...) |
| ++ } |
| ++ sort.Strings(empty) |
| ++ sort.Strings(wantClosed) |
| ++ if !reflect.DeepEqual(empty, wantClosed) { |
| ++ t.Errorf("doors registered with an empty role set: got %q, want %q", empty, wantClosed) |
| ++ } |
| ++ |
| ++ // Only these named routes may be absent with the flag off. All |
| ++ // source registrations remain subject to the default-deny scan. |
| ++ wantRegistered := make(map[string]bool) |
| ++ for _, pattern := range registered { |
| ++ wantRegistered[pattern] = true |
| ++ } |
| ++ for _, pattern := range hostClaimDoors { |
| ++ if !wantRegistered[pattern] { |
| ++ t.Errorf("host claim route missing from source: %s", pattern) |
| ++ } |
| ++ if !tc.enabled { |
| ++ delete(wantRegistered, pattern) |
| ++ } |
| ++ set, present := s.cliRoles[pattern] |
| ++ if present != tc.enabled || (present && !set.Empty()) { |
| ++ t.Errorf("%s: present=%v empty=%v, want present=%v and no service roles", pattern, present, set.Empty(), tc.enabled) |
| ++ } |
| ++ } |
| ++ for pattern := range wantRegistered { |
| ++ if _, ok := s.cliRoles[pattern]; !ok { |
| ++ t.Errorf("%s: the source registers it, the server did not record it", pattern) |
| ++ } |
| ++ } |
| ++ if len(s.cliRoles) != len(wantRegistered) { |
| ++ t.Errorf("cliRoles holds %d doors, want %d for this flag state", len(s.cliRoles), len(wantRegistered)) |
| ++ } |
| ++ }) |
| ``` |
| - [ ] **Create `hostclaim_cli.go` with this complete content.** |
| @@ -5136,6 +5408,7 @@ func TestHostClaimSwitchFleetConvention(t *testing.T) { |
| - [ ] **Run the task tests.** |
| ```bash |
| +go test ./internal/console -run '^TestEveryCLIDoorNamesItsRoles$' -count=1 |
| go test ./internal/console -run 'HostClaim|HostResolve' -count=1 |
| go test . -run 'HostResolve|OpsHost|HostClaim' -count=1 |
| ``` |
| @@ -5148,6 +5421,7 @@ Run focused tests first, then the repository gate before landing the implementat |
| go test ./internal/hostclaim ./internal/hostexec ./internal/hostguard ./internal/hostquarantine ./internal/hostfence ./internal/awsinv -race -count=1 |
| go test ./internal/release -run RootEnvelope -count=1 |
| go test ./internal/lease -run ExecutionStore -count=1 |
| +go test ./internal/console -run '^TestEveryCLIDoorNamesItsRoles$' -count=1 |
| go test ./internal/console -run 'HostClaim|HostResolve' -count=1 |
| go test . -run 'HostResolve|OpsHost|HostClaim' -count=1 |
| go test ./cmd/console -run HostClaim -count=1 |
| @@ -5188,6 +5462,12 @@ The proposed answers live in **§7.3/§10.1** and **amadan #236**. They are not |
| ## Self-review |
| +Rev 3 answers review round 2: |
| + |
| +- **P1 — failed startup persistence:** after acquiring controller exclusion, `Open` starts cleanup with an independent context even if the pending write fails. Before live-lock acquisition, failed startup joins cleanup without requiring another write to the failed destination; no surviving record can authorize work without that lock. Failures after live-lock acquisition still persist denial independently of cleanup, and retain exclusion until both finish. `TestOpenOrphanControllerPendingWriteFailureCleansBeforeReleasingExclusion` leaves an orphaned active record and surviving host process, injects `ENOSPC`, and cancels the opener. It proves the process is terminated while persistence fails, exclusion is retained while cleanup has not joined, and the original error returns only after cleanup joins. Admission stays denied after exclusion ends even though the old active bytes remain. |
| +- **P2 — structural route coverage:** `TestEveryCLIDoorNamesItsRoles` checks both flag states against the source scanner. Only the two named host-claim routes may be absent with the flag off; both must register with empty service-role sets when enabled. Task 4 and the implementation gate run this test explicitly. |
| +- **P2 — AWS request evidence:** `VerifyExactStoppedInstance` reads `GetRequestIDMetadata(out.ResultMetadata)` and returns the exact ID; `ObserveForChallenge` preserves it in `CloudObservation.RequestID`. Both helpers refuse missing, empty or blank IDs. The exact-instance and challenge tests check propagation, while `ValidateObservation` rejects incomplete request evidence independently of freshness. Callers can use the returned ID for `Submission.RequestID` and `LossCloudObservation.RequestID`. |
| + |
| Rev 2 addresses each independent finding with code and regression tests: |
| - **P1 — denial after ambiguous renewal:** `loseLocked` revokes the process-held live lock before notifying callers. `save` holds the host guard across failed lost-state writes and retries until the file and directory are synced. The lifetime lock remains held until all work joins; cleanup runs independently. `TestAmbiguousRenewalENOSPCBeforeRenameDeniesImmediately` fails the temporary file’s sync with `ENOSPC` before rename, proves that the old active file and future deadline remain, and checks immediate local and separate-process denial, guard retention, independent cleanup, timed-out close and eventual durable denial. The live lock also closes the interval while persistence is waiting to acquire the guard. A crash drops both locks, so surviving active bytes still cannot authorize work. |
| @@ -5198,7 +5478,18 @@ The live-lock probe must run inside `Guard.With`, as does the lifetime-lock prob |
| ## Authoring verification |
| -Rev 2 was checked in a disposable on-disk source export outside this repository, reconstructed from the pinned merge baseline above plus this document’s **33 complete Go files and two exact patches**. All embedded Go files match the tested source byte for byte; the baseline patches are unchanged. Archives use `-buildvcs=false` because they are not Git checkouts. The default cache was attempted first; the existing shared on-disk `/home/paulca/.cache/tmp/gocache` was used after the sandbox reported the default cache read-only. |
| +Rev 3 was verified on 2026-10-06 in a disposable on-disk export of the same pinned merge baseline, with all 33 embedded Go files and both exact patches applied. The Task 4 patch now includes `internal/console/svcroutes_test.go`. Every embedded and patched file matched the verified source. The default Go cache was read-only in the sandbox, so these checks used the existing shared on-disk cache named below. |
| + |
| +- The complete `hostexec`, `awsinv` and `hostfence` suites passed with `-race -count=1`, including the orphan-process regression, request-ID propagation and missing-ID refusals. Existing unsafe-path tests also passed: startup cleanup does not wait for a second write to a rejected destination. |
| +- `go test -buildvcs=false ./internal/console -run '^TestEveryCLIDoorNamesItsRoles$' -count=1 -v` passed both `flag-off` and `flag-on` subtests. |
| +- The regressions were watched to fail in scratch when restoring the old `Open`, dropping the returned AWS request ID, or registering host-claim routes unconditionally. Each fixed file was restored afterward. |
| +- Whole-repository builds passed for Linux/amd64, Darwin/amd64, Windows/amd64 and Linux/arm64. `go test -buildvcs=false -exec=/bin/true ./...` compiled every test package; the full repository test suite was not executed. |
| +- Whole-repository `go vet -buildvcs=false ./...`, `gofmt -l .`, exact-patch application and the plan's `git diff --check` passed. |
| +- The Opus authority-review attempt timed out after 45 seconds without a verdict. That review remains outstanding before implementation lands. |
| + |
| +The following rev 2 and rev 1 results are historical: |
| + |
| +Rev 2 was checked in a disposable on-disk source export outside this repository, reconstructed from the pinned merge baseline above plus this document’s **33 complete Go files and two exact patches**. At rev 2, all embedded Go files matched the tested source byte for byte and the baseline patches were unchanged. Archives use `-buildvcs=false` because they are not Git checkouts. The default cache was attempted first; the existing shared on-disk `/home/paulca/.cache/tmp/gocache` was used after the sandbox reported the default cache read-only. |
| Checks on 2026-10-05 for rev 2: |