WIP claim Plan 14 track E checkpoint 12 (not for merge): E1 continued
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
10 files changed,
+484
−23
internal/historyreadback/completion.go+66 −0internal/historyreadback/plan14_completion_test.go+161 −0internal/host/plan14_history_retirement_test.go+57 −0internal/provision/testdata/plan14-root-reads-e.tsv+10 −7internal/release/plan14_history_completion_test.go+109 −0internal/release/plan14_history_retirement_process_test.go+24 −0internal/release/plan14_root_reconstruction_test.go+20 −0internal/release/roothistory_reconstruct.go+5 −4internal/release/roothistory_retirement.go+14 −9internal/release/roothistory_status.go+18 −3
CI passed — run details
CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok amadan.net/carlos/platform.git/launchctx 0.011s
? amadan.net/carlos/platform.git/launchctx/controlname [no test files]
? amadan.net/carlos/platform.git/launchctx/wireid [no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok github.com/carlosframework/platform 340.093s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 3.105s
ok github.com/carlosframework/platform/cmd/moveprobe 93.270s
ok github.com/carlosframework/platform/cmd/zddprobe 0.378s
ok github.com/carlosframework/platform/internal/activator 227.889s
ok github.com/carlosframework/platform/internal/agentsock 0.015s
ok github.com/carlosframework/platform/internal/alerting 0.003s
ok github.com/carlosframework/platform/internal/analytics 0.022s
ok github.com/carlosframework/platform/internal/appconfig 2.313s
ok github.com/carlosframework/platform/internal/apperr 0.075s
ok github.com/carlosframework/platform/internal/applog 0.133s
ok github.com/carlosframework/platform/internal/awsinv 0.003s
ok github.com/carlosframework/platform/internal/boxchoice 0.004s
ok github.com/carlosframework/platform/internal/boxid 0.936s
? github.com/carlosframework/platform/internal/boxid/boxidtest [no test files]
? github.com/carlosframework/platform/internal/boxid/internal/signtest [no test files]
ok github.com/carlosframework/platform/internal/boxvolumes 0.008s
ok github.com/carlosframework/platform/internal/caps 0.014s
ok github.com/carlosframework/platform/internal/certwarm 0.004s
ok github.com/carlosframework/platform/internal/cgroup 0.700s
ok github.com/carlosframework/platform/internal/changelog 0.294s
ok github.com/carlosframework/platform/internal/clamd 0.055s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 363.634s
? github.com/carlosframework/platform/internal/controlname [no test files]
? github.com/carlosframework/platform/internal/controlsock [no test files]
ok github.com/carlosframework/platform/internal/dbdoor 6.542s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.009s
ok github.com/carlosframework/platform/internal/dbverify 0.004s
ok github.com/carlosframework/platform/internal/dnsrec 0.002s
ok github.com/carlosframework/platform/internal/docsite 0.005s
ok github.com/carlosframework/platform/internal/domains 2.133s
ok github.com/carlosframework/platform/internal/doorkey 0.006s
ok github.com/carlosframework/platform/internal/economics 0.004s
ok github.com/carlosframework/platform/internal/edge 25.992s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.008s
ok github.com/carlosframework/platform/internal/fleetapi 0.122s
ok github.com/carlosframework/platform/internal/gate 1.656s
ok github.com/carlosframework/platform/internal/genselect 0.015s
ok github.com/carlosframework/platform/internal/geo 0.016s
ok github.com/carlosframework/platform/internal/geoip 0.029s
ok github.com/carlosframework/platform/internal/historyreadback 0.102s
ok github.com/carlosframework/platform/internal/host 100.070s
ok github.com/carlosframework/platform/internal/hostactors 0.290s
ok github.com/carlosframework/platform/internal/hostbox 2.113s
ok github.com/carlosframework/platform/internal/hostclaim 0.510s
ok github.com/carlosframework/platform/internal/hostevidence 0.007s
ok github.com/carlosframework/platform/internal/hostexec 4.880s
ok github.com/carlosframework/platform/internal/hostfence 0.036s
ok github.com/carlosframework/platform/internal/hostfiles 10.554s
ok github.com/carlosframework/platform/internal/hostfloor 0.016s
ok github.com/carlosframework/platform/internal/hostguard 0.977s
ok github.com/carlosframework/platform/internal/hostops 1.430s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.004s
ok github.com/carlosframework/platform/internal/hostquarantine 0.003s
ok github.com/carlosframework/platform/internal/hostrelocate 0.444s
ok github.com/carlosframework/platform/internal/hostresolve 0.136s
ok github.com/carlosframework/platform/internal/hostwrite 0.003s
ok github.com/carlosframework/platform/internal/infraview 0.003s
ok github.com/carlosframework/platform/internal/instancectl 1.812s
ok github.com/carlosframework/platform/internal/instanceexec 2.706s
ok github.com/carlosframework/platform/internal/instancefs 0.021s
ok github.com/carlosframework/platform/internal/instancekey 0.018s
ok github.com/carlosframework/platform/internal/instanceupdates 0.023s
ok github.com/carlosframework/platform/internal/layout 1.445s
ok github.com/carlosframework/platform/internal/lease 9.436s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.006s
ok github.com/carlosframework/platform/internal/ledger 0.017s
ok github.com/carlosframework/platform/internal/mail 0.158s
ok github.com/carlosframework/platform/internal/maintenance 0.078s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.546s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.077s
ok github.com/carlosframework/platform/internal/metrics 0.005s
ok github.com/carlosframework/platform/internal/moves 20.993s
ok github.com/carlosframework/platform/internal/moves/archive 0.759s
ok github.com/carlosframework/platform/internal/objstore 0.178s
ok github.com/carlosframework/platform/internal/ops 0.158s
ok github.com/carlosframework/platform/internal/park 0.202s
ok github.com/carlosframework/platform/internal/privatefile 0.007s
ok github.com/carlosframework/platform/internal/provision 13.408s
ok github.com/carlosframework/platform/internal/publicrelease 0.008s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 4.002s
ok github.com/carlosframework/platform/internal/relay 7.232s
ok github.com/carlosframework/platform/internal/release 21.103s
ok github.com/carlosframework/platform/internal/rollout 3.752s
ok github.com/carlosframework/platform/internal/rolloutwords 0.004s
ok github.com/carlosframework/platform/internal/rootaws 0.007s
ok github.com/carlosframework/platform/internal/rootparticipation 0.007s
ok github.com/carlosframework/platform/internal/rootstate 0.025s
ok github.com/carlosframework/platform/internal/s3compat 0.004s
ok github.com/carlosframework/platform/internal/safefile 0.004s
ok github.com/carlosframework/platform/internal/schedule 2.292s
ok github.com/carlosframework/platform/internal/sim/hostlife 9.636s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 3.801s
ok github.com/carlosframework/platform/internal/status 0.040s
ok github.com/carlosframework/platform/internal/steering 0.006s
ok github.com/carlosframework/platform/internal/stopevidence 0.167s
ok github.com/carlosframework/platform/internal/svcrole 0.002s
ok github.com/carlosframework/platform/internal/telemetry 0.004s
ok github.com/carlosframework/platform/internal/termfence 0.004s
ok github.com/carlosframework/platform/internal/tunnelclient 0.004s
ok github.com/carlosframework/platform/internal/unixdial 0.002s
ok github.com/carlosframework/platform/internal/updatewords 0.004s
ok github.com/carlosframework/platform/internal/usage 0.006s
ok github.com/carlosframework/platform/internal/version 0.002s
ok github.com/carlosframework/platform/internal/vet 1.562s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
minio: cached, digest verified
mc: cached, digest verified
minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
buckets and scoped user ready
=== RUN TestDeliveredContractMinio
=== RUN TestDeliveredContractMinio/put
=== RUN TestDeliveredContractMinio/get_round-trips_the_body
=== RUN TestDeliveredContractMinio/list_shows_the_key
=== RUN TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN TestDeliveredContractMinio/delete_removes_it
=== RUN TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
--- PASS: TestDeliveredContractMinio/put (0.00s)
--- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
--- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
--- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
--- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok github.com/carlosframework/platform/internal/objstore 0.022s
=== RUN TestT4StorageProbeMinio
2026/10/13 19:20:06 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE2D062A01D291, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.00s)
PASS
ok github.com/carlosframework/platform/internal/host 0.021s
minio contract: PASS
diff --git a/internal/historyreadback/completion.go b/internal/historyreadback/completion.go| new file mode 100644 |
| index 000000000..151270d8b |
| --- /dev/null |
| +++ b/internal/historyreadback/completion.go |
| @@ -0,0 +1,66 @@ |
| +package historyreadback |
| + |
| +import ( |
| + "context" |
| + "os" |
| + "slices" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/hostactors" |
| + "github.com/carlosframework/platform/internal/release" |
| +) |
| + |
| +// ReadReconstruction verifies the exact accepted authorization from fresh, |
| +// enrolled-root readback. It does not certify fresh installation or retire |
| +// independent recovery obligations carried in the returned status. |
| +func ReadReconstruction(ctx context.Context, d hostactors.Directory, q Request, g release.HistoryReconstruction, now time.Time) (Receipt, error) { |
| + r, err := ReadStatus(ctx, d, q, now) |
| + if err != nil { |
| + return Receipt{}, err |
| + } |
| + s := r.Status |
| + accepted := digest(g) |
| + if !validHex(accepted, 32) || g.Schema != 1 || g.Kind != "carlos-release-history" || g.Operation == "" || g.IssuedAt.IsZero() || g.Account != q.Scope.Binding.Account || g.App != q.Scope.Binding.App || g.Identity != s.Identity || g.Revision == 0 || g.Revision == ^uint64(0) || s.Revision != g.Revision+1 || s.Pending || s.LastReconstructionDigest != accepted || !s.AppComplete || s.AppRevision <= g.AppRevision || s.BakeComplete == 0 || s.BakeRevision <= g.BakeRevision || len(g.Hosts) == 0 || len(g.Hosts) > 256 { |
| + return Receipt{}, os.ErrPermission |
| + } |
| + seen := map[string]bool{} |
| + for _, expected := range g.Hosts { |
| + if seen[expected.Host] { |
| + return Receipt{}, os.ErrPermission |
| + } |
| + seen[expected.Host] = true |
| + i := slices.IndexFunc(s.Hosts, func(h release.HistoryHostStatus) bool { return h.Host == expected.Host }) |
| + if i < 0 || s.Hosts[i].Source != expected.Source || s.Hosts[i].Revision <= expected.Revision || !s.Hosts[i].RefusalStatus.Complete { |
| + return Receipt{}, os.ErrPermission |
| + } |
| + } |
| + if !seen[q.Scope.Binding.Host] { |
| + return Receipt{}, os.ErrPermission |
| + } |
| + return r, nil |
| +} |
| + |
| +// ReadRetirement verifies one effective retirement. Unrelated restrictions |
| +// remain in the receipt and must not be treated as cleared by this result. |
| +func ReadRetirement(ctx context.Context, d hostactors.Directory, q Request, g release.HistoryRecoveryRetirement, now time.Time) (Receipt, error) { |
| + r, err := ReadStatus(ctx, d, q, now) |
| + if err != nil { |
| + return Receipt{}, err |
| + } |
| + s := r.Status |
| + accepted := digest(g) |
| + if !validHex(accepted, 32) || g.Schema != 1 || g.Kind != "carlos-release-recovery-retirement" || g.Operation == "" || g.IssuedAt.IsZero() || g.Binding != q.Scope.Binding || g.Identity != s.Identity || s.Pending || s.RecoverySources == nil || !s.RecoverySources.Known || !validHex(g.OutcomeDigest, 32) || !validHex(g.VerifiedDigest, 32) || g.AttemptID == "" || g.VerifiedID == "" || g.AttemptID == g.VerifiedID { |
| + return Receipt{}, os.ErrPermission |
| + } |
| + for _, pending := range s.RecoverySources.Unresolved { |
| + if pending.Binding == g.Binding && (pending.Attempt.ID == g.AttemptID || pending.Attempt.ID == g.VerifiedID) { |
| + return Receipt{}, os.ErrPermission |
| + } |
| + } |
| + for _, h := range s.Hosts { |
| + if h.Host == g.Binding.Host && h.Source == g.Source && slices.Contains(h.RetiredRecoveries, g.AttemptID) && h.RetirementDigests[g.AttemptID] == accepted && h.RecoveryDigests[g.AttemptID] == g.OutcomeDigest && h.RecoveryDigests[g.VerifiedID] == g.VerifiedDigest { |
| + return r, nil |
| + } |
| + } |
| + return Receipt{}, os.ErrPermission |
| +} |
diff --git a/internal/historyreadback/plan14_completion_test.go b/internal/historyreadback/plan14_completion_test.go| new file mode 100644 |
| index 000000000..645fdc113 |
| --- /dev/null |
| +++ b/internal/historyreadback/plan14_completion_test.go |
| @@ -0,0 +1,161 @@ |
| +package historyreadback |
| + |
| +import ( |
| + "errors" |
| + "os" |
| + "strings" |
| + "testing" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/release" |
| +) |
| + |
| +func TestPlan14ReadReconstructionCompletion(t *testing.T) { |
| + for _, fault := range []string{"valid", "invalid serialization", "replacement", "delivery only", "different operation", "different replacement", "different bounds", "namespace", "revision", "pending", "source", "scope", "unknown app", "unknown host", "unknown bake", "missing digest", "signature", "old challenge"} { |
| + t.Run(fault, func(t *testing.T) { |
| + f := newFixture(t) |
| + g := release.HistoryReconstruction{Schema: 1, Kind: "carlos-release-history", Identity: f.status.Identity, Revision: 1, Operation: "reconstruct", IssuedAt: f.now, Account: f.scope.Binding.Account, App: f.scope.Binding.App, Hosts: []release.HistoryReconstructionHost{f.status.Hosts[0].HistoryReconstructionHost}} |
| + if fault == "replacement" { |
| + g.Replaces = strings.Repeat("d", 64) |
| + } |
| + f.status.Revision = 2 |
| + f.status.AppComplete, f.status.AppRevision, f.status.BakeComplete, f.status.BakeRevision = true, 1, 1, 1 |
| + f.status.Hosts[0].Revision = 1 |
| + f.status.Hosts[0].RefusalStatus.Complete = true |
| + f.status.LastReconstructionDigest = digest(g) |
| + switch fault { |
| + case "invalid serialization": |
| + g.IssuedAt = time.Date(10000, 1, 1, 0, 0, 0, 0, time.UTC) |
| + f.status.LastReconstructionDigest = "" |
| + f.status.Hosts[0].RetirementDigests = nil |
| + case "different operation": |
| + g.Operation = "other" |
| + case "different replacement": |
| + g.Replaces = strings.Repeat("e", 64) |
| + case "different bounds": |
| + g.Production = true |
| + case "namespace": |
| + f.status.Identity = strings.Repeat("f", 64) |
| + case "revision": |
| + f.status.Revision++ |
| + case "pending": |
| + f.status.Pending = true |
| + case "source": |
| + f.status.Hosts[0].Source = "changed" |
| + case "scope": |
| + g.Account = "xyz" |
| + f.status.LastReconstructionDigest = digest(g) |
| + case "unknown app": |
| + f.status.AppComplete = false |
| + case "unknown host": |
| + f.status.Hosts[0].RefusalStatus.Complete = false |
| + case "unknown bake": |
| + f.status.BakeComplete = 0 |
| + case "missing digest": |
| + f.status.LastReconstructionDigest = "" |
| + } |
| + q, err := RequestStatus(t.Context(), f.directory, f.signer, f.scope, f.now) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if fault != "delivery only" { |
| + if err := Respond(t.Context(), f.directory, f.scope, f.key, f.now, f.snapshot); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if fault == "signature" { |
| + r := Receipt{Schema: 1, Kind: "carlos-release-history-receipt", RequestDigest: digest(q), ObservedAt: f.now, Status: f.status} |
| + wire, err := release.SignRootEnvelope(f.signer, r) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := publish(t.Context(), f.directory.Store, ResponseKey(q), wire); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if fault == "old challenge" { |
| + q, err = RequestStatus(t.Context(), f.directory, f.signer, f.scope, f.now) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + _, err = ReadReconstruction(t.Context(), f.directory, q, g, f.now) |
| + if fault == "valid" || fault == "replacement" { |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + } else if err == nil { |
| + t.Fatal("unproven reconstruction reported complete") |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestPlan14ReadRetirementCompletion(t *testing.T) { |
| + for _, fault := range []string{"valid", "invalid serialization", "different operation", "different outcome", "different witness", "missing digest", "missing retired", "unknown originals", "pending original", "unrelated original", "pending reconstruction", "source", "namespace", "scope", "delivery only"} { |
| + t.Run(fault, func(t *testing.T) { |
| + f := newFixture(t) |
| + g := release.HistoryRecoveryRetirement{Schema: 1, Kind: "carlos-release-recovery-retirement", Identity: f.status.Identity, Operation: "retire", IssuedAt: f.now, Binding: f.scope.Binding, Source: "canonical", AttemptID: "failed", OutcomeDigest: strings.Repeat("d", 64), VerifiedID: "good", VerifiedDigest: strings.Repeat("e", 64)} |
| + f.status.RecoverySources = &release.HistoryRecoverySources{Known: true} |
| + f.status.Hosts[0].RetiredRecoveries = []string{g.AttemptID} |
| + f.status.Hosts[0].RetirementDigests = map[string]string{g.AttemptID: digest(g)} |
| + f.status.Hosts[0].RecoveryDigests = map[string]string{g.AttemptID: g.OutcomeDigest, g.VerifiedID: g.VerifiedDigest} |
| + switch fault { |
| + case "invalid serialization": |
| + g.IssuedAt = time.Date(10000, 1, 1, 0, 0, 0, 0, time.UTC) |
| + f.status.LastReconstructionDigest = "" |
| + f.status.Hosts[0].RetirementDigests = nil |
| + case "different operation": |
| + g.Operation = "other" |
| + case "different outcome": |
| + g.OutcomeDigest = strings.Repeat("f", 64) |
| + case "different witness": |
| + g.VerifiedDigest = strings.Repeat("f", 64) |
| + case "missing digest": |
| + f.status.Hosts[0].RetirementDigests = nil |
| + case "missing retired": |
| + f.status.Hosts[0].RetiredRecoveries = nil |
| + case "unknown originals": |
| + f.status.RecoverySources.Known = false |
| + case "pending original", "unrelated original": |
| + id := g.AttemptID |
| + if fault == "unrelated original" { |
| + id = "unrelated" |
| + } |
| + f.status.RecoverySources.Unresolved = []release.HistoryRecovery{{Binding: g.Binding, Attempt: release.HistoryAttempt{ID: id}}} |
| + case "pending reconstruction": |
| + f.status.Pending = true |
| + case "source": |
| + f.status.Hosts[0].Source = "changed" |
| + case "namespace": |
| + f.status.Identity = strings.Repeat("f", 64) |
| + case "scope": |
| + g.Binding.Host = "other.example" |
| + f.status.Hosts[0].RetirementDigests[g.AttemptID] = digest(g) |
| + } |
| + q, err := RequestStatus(t.Context(), f.directory, f.signer, f.scope, f.now) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if fault != "delivery only" { |
| + if err := Respond(t.Context(), f.directory, f.scope, f.key, f.now, f.snapshot); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + receipt, err := ReadRetirement(t.Context(), f.directory, q, g, f.now) |
| + if fault == "valid" || fault == "unrelated original" { |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if fault == "unrelated original" && len(receipt.Status.RecoverySources.Unresolved) != 1 { |
| + t.Fatal("lost unrelated obligation") |
| + } |
| + } else if err == nil { |
| + t.Fatal("unproven retirement reported complete") |
| + } |
| + if fault == "delivery only" && !errors.Is(err, os.ErrNotExist) { |
| + t.Fatalf("unexpected missing response: %v", err) |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/host/plan14_history_retirement_test.go b/internal/host/plan14_history_retirement_test.go| index ce9422ffc..b1ea6f3c8 100644 |
| --- a/internal/host/plan14_history_retirement_test.go |
| +++ b/internal/host/plan14_history_retirement_test.go |
| @@ -8,6 +8,9 @@ import ( |
| "testing" |
| "time" |
| + "github.com/carlosframework/platform/internal/historyreadback" |
| + "github.com/carlosframework/platform/internal/hostactors" |
| + "github.com/carlosframework/platform/internal/hostfloor" |
| "github.com/carlosframework/platform/internal/release" |
| "github.com/carlosframework/platform/internal/rootstate" |
| ) |
| @@ -54,6 +57,17 @@ func TestPlan14RecoveryRetirementProductionCallback(t *testing.T) { |
| projection := &rollbackMemory{files: map[string][]byte{}} |
| now := time.Now().UTC() |
| maintenance := rootHistoryMaintenance{history: h, store: e.store, source: source, checks: c, pub: func() (ed25519.PublicKey, error) { return signingPub, nil }, projection: func() (rootstate.Store, error) { return projection, nil }, now: func() time.Time { return now }} |
| + directory, responder, scope := completionReadbackFixture(t, e.store, signingPub, key, b, now) |
| + maintenance.readback = responder.publish |
| + challenge := func() historyreadback.Request { |
| + t.Helper() |
| + q, err := historyreadback.RequestStatus(t.Context(), directory, release.RootSigner(key), scope, now) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + return q |
| + } |
| + q := challenge() |
| deps := boundRootHistory(h, source.resolve, e.root.obs) |
| deps.Reconstruct = maintenance.run |
| if err := deps.Reconstruct(t.Context(), b); err == nil { |
| @@ -112,14 +126,33 @@ func TestPlan14RecoveryRetirementProductionCallback(t *testing.T) { |
| if err := deps.Reconstruct(t.Context(), b); err == nil { |
| t.Fatal("signed reconstruction erased ambiguity") |
| } |
| + receipt, err := historyreadback.ReadReconstruction(t.Context(), directory, q, grant, now) |
| + if err != nil { |
| + t.Fatal("completed reconstruction lacks exact readback", err) |
| + } |
| + if len(receipt.Status.RecoverySources.Unresolved) != 1 { |
| + t.Fatal("reconstruction cleared original ambiguity") |
| + } |
| + other := grant |
| + other.Operation = "not-accepted" |
| + if _, err := historyreadback.ReadReconstruction(t.Context(), directory, q, other, now); err == nil { |
| + t.Fatal("different authorization reported complete") |
| + } |
| retirement := release.HistoryRecoveryRetirement{Schema: 1, Kind: "carlos-release-recovery-retirement", Identity: view.Identity, Operation: "retire-exact-ambiguity", IssuedAt: now, Binding: b, Source: view.Hosts[0].Source, AttemptID: old.ID, OutcomeDigest: view.Hosts[0].RecoveryDigests[old.ID], VerifiedID: good.ID, VerifiedDigest: view.Hosts[0].RecoveryDigests[good.ID]} |
| if err := release.PublishHistoryRecoveryRetirement(t.Context(), e.store, release.RootSigner(key), retirement); err != nil { |
| t.Fatal(err) |
| } |
| + q = challenge() |
| + if _, err := historyreadback.ReadRetirement(t.Context(), directory, q, retirement, now); err == nil { |
| + t.Fatal("delivery counted as completion") |
| + } |
| before := len(u.effects) |
| if err := deps.Reconstruct(t.Context(), b); err != nil { |
| t.Fatal(err) |
| } |
| + if _, err := historyreadback.ReadRetirement(t.Context(), directory, q, retirement, now); err != nil { |
| + t.Fatal("retirement lacks exact readback", err) |
| + } |
| if refs, err := c.mintRefusals(h, b); err != nil || len(refs) != 0 { |
| t.Fatalf("exact authorized retirement blocked mint: %+v %v", refs, err) |
| } |
| @@ -145,9 +178,13 @@ func TestPlan14RecoveryRetirementProductionCallback(t *testing.T) { |
| if _, err := c.mintRefusals(h, b); err == nil { |
| t.Fatal("retirement consumed a pending original") |
| } |
| + q = challenge() |
| if err := deps.Reconstruct(t.Context(), b); err == nil { |
| t.Fatal("maintenance consumed a pending original") |
| } |
| + if _, err := historyreadback.ReadRetirement(t.Context(), directory, q, retirement, now); err == nil { |
| + t.Fatal("readback consumed pending original") |
| + } |
| raw, err = projection.Read(release.HistoryStatusKey(b.Account, b.App)) |
| if err != nil { |
| t.Fatal(err) |
| @@ -225,3 +262,23 @@ func (s *historyIntentFailure) Write(name string, raw []byte) error { |
| } |
| return nil |
| } |
| + |
| +func completionReadbackFixture(t *testing.T, store release.Store, pub ed25519.PublicKey, key ed25519.PrivateKey, b release.HistoryBinding, now time.Time) (hostactors.Directory, historyReadbackPublisher, historyreadback.Scope) { |
| + t.Helper() |
| + _, evidenceKey, err := ed25519.GenerateKey(nil) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + enrollment := hostfloor.Enrollment{Box: "box", Enrollment: strings.Repeat("e", 32), InstanceID: "i-box"} |
| + directory := hostactors.Directory{Store: store, PublicKey: pub, Bucket: "bucket"} |
| + roster := hostactors.Roster{V: 1, DeploymentBucket: "bucket", Revision: 1, Boxes: map[string]hostactors.Box{"box": {Enrollment: enrollment.Enrollment, Namespace: "platform", BoxID: "box", Label: "box", Region: "ie", Pool: "default", Cloud: hostactors.Cloud{Provider: "aws", Account: "123", Region: "eu-west-1", InstanceID: "i-box"}, EvidenceSHA256: strings.Repeat("b", 64), State: "active", CensusPublicKey: evidenceKey.Public().(ed25519.PublicKey)}}, Consoles: map[string]hostactors.Console{}, Holds: map[string]hostactors.Reservation{}, Activations: map[string]hostactors.Reservation{}} |
| + wire, err := release.SignRootEnvelope(release.RootSigner(key), roster) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := store.Put(t.Context(), hostactors.Key, wire, ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + responder := historyReadbackPublisher{directory: directory, enrollment: enrollment, key: func() (ed25519.PrivateKey, error) { return evidenceKey, nil }, now: func() time.Time { return now }} |
| + return directory, responder, historyreadback.Scope{Bucket: "bucket", Box: enrollment.Box, Enrollment: enrollment.Enrollment, Binding: b} |
| +} |
diff --git a/internal/provision/testdata/plan14-root-reads-e.tsv b/internal/provision/testdata/plan14-root-reads-e.tsv| index 14c9b9923..20d1743e9 100644 |
| --- a/internal/provision/testdata/plan14-root-reads-e.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-e.tsv |
| @@ -46,7 +46,7 @@ internal/release/roothistory_pass.go passConfig.recheckHistory authority E1 E1 c |
| internal/release/roothistory_pass.go passConfig.recordRefusal authority E1 E1 canonical binding selection and recheck; durable attempt precedes effects; only independent installed observation settles and retires the exact target refusal. registry:Host@151,whole:Route@241 298ca88f7dc02be7779aaa527990b4e4548c1fe5498fba0272a6e7041b236354 |
| internal/release/roothistory_pass.go type RootHistoryPass authority B3,E1 Root history intent is linked to captured canonical rollback authority before live effects; verified recovery settles only its exact intent. Fixtures: TestPlan14HistoryRollbackLink, TestPlan14HistoryRecoveryRefusesChanges, TestPlan14HistoryRecoveryProcessCrash, TestPlan14HistorySettlementRetriesVisibleSync. E1 migration and restart consumption remain incomplete. file:Prepare@215 c90889be3f234547b634149feceb82515c03de58598912758152f39604f23274 |
| internal/release/roothistory_reconstruct.go RootHistory.Reconstruct authority E1 Signed replacement binds the exact pending digest and current sources/revisions; old intended refusals and hold bounds remain restrictive. Fixtures: TestPlan14ReconstructionReplacement, TestPlan14ReconstructionReplacementCrash. f5deeb978cef7024adf551c9ac6ef2fce4de88b2d5392affdf3313bc82df0d16 |
| -internal/release/roothistory_reconstruct.go RootHistory.ReconstructionStatus authority E1 E1 signed root-history reconstruction; exact namespace identity, revisions and canonical sources; journaled completion retains denies and stronger bounds. Tests: TestPlan14RootHistoryReconstruction, TestPlan14RootHistoryReconstructionCrash, TestPlan14RootHistoryReconstructionDeliveryPass. file:Read@189 f2da2a6c97bc40bf972f1346b578c47334585053d85244c9ba6b63c45f0570a3 |
| +internal/release/roothistory_reconstruct.go RootHistory.ReconstructionStatus authority E1 Exact completion binds the accepted authorization digest from durable private history to enrolled-root signed readback. Pending originals, changed evidence and canonical sources cannot supply retirement completion. Fixtures: TestPlan14HistoryReconstructionCompletion, TestPlan14HistoryRetirementCompletion, TestPlan14ReadReconstructionCompletion, TestPlan14ReadRetirementCompletion, TestPlan14RecoveryRetirementProductionCallback, TestPlan14RootHistoryReconstructionCrash, TestPlan14HistoryRetirementProcessCrash. file:Read@189 611a42b0f940b753e22a99266e6aae3384173233f717f3bdef0f38c80dcb00d5 |
| internal/release/roothistory_reconstruct.go RootHistory.reconstructionImages authority E1 E1 signed root-history reconstruction; exact namespace identity, revisions and canonical sources; journaled completion retains denies and stronger bounds. Tests: TestPlan14RootHistoryReconstruction, TestPlan14RootHistoryReconstructionCrash, TestPlan14RootHistoryReconstructionDeliveryPass. file:Read@1190,file:Read@226,file:Read@2946,file:Read@629 cb47b84dd9bbb94a253aca2fa2c95ceaa75ddfa6453c78ece1c1b432e977e79a |
| internal/release/roothistory_reconstruct.go RootHistory.reconstructionState authority E1 E1 signed root-history reconstruction; exact namespace identity, revisions and canonical sources; journaled completion retains denies and stronger bounds. Tests: TestPlan14RootHistoryReconstruction, TestPlan14RootHistoryReconstructionCrash, TestPlan14RootHistoryReconstructionDeliveryPass. file:Read@178 7c53af838e0dc7ec7ad6ae5341da983bde442288126e9eea21f80d6a62fc478a |
| internal/release/roothistory_reconstruct.go RootHistory.resumeReconstruction authority E1 E1 signed root-history reconstruction; exact namespace identity, revisions and canonical sources; journaled completion retains denies and stronger bounds. Tests: TestPlan14RootHistoryReconstruction, TestPlan14RootHistoryReconstructionCrash, TestPlan14RootHistoryReconstructionDeliveryPass. file:Read@915 39d8949054462cf53823e4b6fc802a08349bbac79750f6beb5b90095880925b8 |
| @@ -55,9 +55,9 @@ internal/release/signedhead.go StageSignedHead authority E1 Canonical route is s |
| internal/rootstate/link_linux.go ReadTrustedLink authority A,E1 Checks the full root-owned parent chain before descriptor-based live-link reading. Fixture: TestPlan14TrustedLiveLink; privileged acceptance remains required. file:Open@156 90f8e8de4e2c71f07b5d749cbe4bb1c1dfa426576a82138823449f56fc9ec5ab |
| internal/rootstate/link_linux.go directory.readLink authority A,E1 Pins the symlink inode with O_PATH and checks owner/type/link count before readlinkat. Fixture: TestPlan14TrustedLiveLink. file:Fstat@356,file:Openat@183,file:Readlinkat@554 a8559fe63dce48830916004ecfe7fbbe5d419568c7daf7a3073cf1b5bb12c3d7 |
| @coverage internal/host 6cd891a68b42e2fe9c44ff3712ebd24b67b4d1602adb5a67605bdd121eda97d4 |
| -@coverage internal/release 2b58eb82843de5240966627f4982bbe26db3872a9126e7e9a145459663ccbd07 |
| +@coverage internal/release 9417f6a8ddd23071820003d222a0c27915295d099496d87216db5a04a5f0bde7 |
| @coverage internal/rootstate 6338f74a6b2059cf77cf478bd6111b37b441eb9d65a309464c378359d4bdac05 |
| -internal/release/roothistory_status.go RootHistory.MigrationStatus authority E1 Exact signed recovery retirement binds namespace, complete outcome digest and a later root-verified success; pending originals and changed evidence remain restrictive. Independent settlement publishes status without controlling effects. Fixtures: TestPlan14HistoryExactRecoveryRetirement, TestPlan14HistoryRecoveryRetirementAuthority, TestPlan14HistoryRetirementVisibleSync, TestPlan14HistoryRetirementProcessCrash, TestPlan14RecoveryRetirementProductionCallback, TestPlan14IndependentRecoveryPublishesStatus. file:Read@1062 fdd21d4a9823eab7937fb900655f8e7d342cf226f66e5da526ca8b6285845bf2 |
| +internal/release/roothistory_status.go RootHistory.MigrationStatus authority E1 Exact completion binds the accepted authorization digest from durable private history to enrolled-root signed readback. Pending originals, changed evidence and canonical sources cannot supply retirement completion. Fixtures: TestPlan14HistoryReconstructionCompletion, TestPlan14HistoryRetirementCompletion, TestPlan14ReadReconstructionCompletion, TestPlan14ReadRetirementCompletion, TestPlan14RecoveryRetirementProductionCallback, TestPlan14RootHistoryReconstructionCrash, TestPlan14HistoryRetirementProcessCrash. file:Read@1062,file:Read@3891 f0ab7ad2d2d70f8c22ebfd575f5a1c6931f7eba30ccdba80ad43d0efffc88154 |
| internal/host/legacyrestart.go restartEffectFailed authority B3 E8 merge ruling 5: optional identity failures preserve the same restart outcome; no second restart; real not-up takes main rollback. Fixtures: TestPlan14ERecoveryRestartOutcome, TestPlan14ERecoveryBindingRefusalKeepsPromotedFix, TestPlan14ERecoveryNotUpPrecedesBinding. b68bb65884a14fe0cf78a65f56ded4ea41584bba99af9195827d1ea191b340a9 |
| internal/host/legacyrestart.go legacyRootRestart authority B3,G E9 merge ruling 6: E effects require a census; no-census recovery delegates to main or records evidence only. Fixtures: TestPlan14ERecoveryNoCensus, TestPlan14ERecoveryNoCensusResume, TestR2ERecoveryFlagWithoutCensus, TestPlan14ERecoveryRestartOutcome. Census-backed recovery retains captured authority and outcome precedence. 5fd48683abd82664baa77c2ddc7528f92f557b04586225d64b4a6e77147df931 |
| internal/host/recoveryrefusals.go recoveryHistoryOutcome authority E1 Exact signed recovery retirement binds namespace, complete outcome digest and a later root-verified success; pending originals and changed evidence remain restrictive. Independent settlement publishes status without controlling effects. Fixtures: TestPlan14HistoryExactRecoveryRetirement, TestPlan14HistoryRecoveryRetirementAuthority, TestPlan14HistoryRetirementVisibleSync, TestPlan14HistoryRetirementProcessCrash, TestPlan14RecoveryRetirementProductionCallback, TestPlan14IndependentRecoveryPublishesStatus. ab25e7fa0cca603c50cfee8862c669c674e714b5a8a263748257ce26e1495491 |
| @@ -84,7 +84,7 @@ internal/release/roothistory_outcomes.go type HistoryRecovery authority E1 Exact |
| internal/release/roothistory_outcomes.go type historyRecoveryRecord authority E1 Exact signed recovery retirement binds namespace, complete outcome digest and a later root-verified success; pending originals and changed evidence remain restrictive. Independent settlement publishes status without controlling effects. Fixtures: TestPlan14HistoryExactRecoveryRetirement, TestPlan14HistoryRecoveryRetirementAuthority, TestPlan14HistoryRetirementVisibleSync, TestPlan14HistoryRetirementProcessCrash, TestPlan14RecoveryRetirementProductionCallback, TestPlan14IndependentRecoveryPublishesStatus. 49deb93476b73ca13e9133916babba5ded70b0e01dc793a1a252b9eee07d5a15 |
| internal/release/roothistory_retirement.go RootHistory.RecoveryRetired authority E1 Exact signed recovery retirement binds namespace, complete outcome digest and a later root-verified success; pending originals and changed evidence remain restrictive. Independent settlement publishes status without controlling effects. Fixtures: TestPlan14HistoryExactRecoveryRetirement, TestPlan14HistoryRecoveryRetirementAuthority, TestPlan14HistoryRetirementVisibleSync, TestPlan14HistoryRetirementProcessCrash, TestPlan14RecoveryRetirementProductionCallback, TestPlan14IndependentRecoveryPublishesStatus. 70b4fd04aa52687f9c4f140b8ddd345c26e9aca78e325e4f093a39a93d8dec67 |
| internal/release/roothistory_retirement.go RootHistory.RetireRecovery authority E1 Exact signed recovery retirement binds namespace, complete outcome digest and a later root-verified success; pending originals and changed evidence remain restrictive. Independent settlement publishes status without controlling effects. Fixtures: TestPlan14HistoryExactRecoveryRetirement, TestPlan14HistoryRecoveryRetirementAuthority, TestPlan14HistoryRetirementVisibleSync, TestPlan14HistoryRetirementProcessCrash, TestPlan14RecoveryRetirementProductionCallback, TestPlan14IndependentRecoveryPublishesStatus. 3bc3d947862f05d7407179d896646aa68ab61c9a3d861086709724869b32e3b7 |
| -internal/release/roothistory_retirement.go RootHistory.recoveryRetired authority E1 Exact signed recovery retirement binds namespace, complete outcome digest and a later root-verified success; pending originals and changed evidence remain restrictive. Independent settlement publishes status without controlling effects. Fixtures: TestPlan14HistoryExactRecoveryRetirement, TestPlan14HistoryRecoveryRetirementAuthority, TestPlan14HistoryRetirementVisibleSync, TestPlan14HistoryRetirementProcessCrash, TestPlan14RecoveryRetirementProductionCallback, TestPlan14IndependentRecoveryPublishesStatus. file:Read@114 e862a761df28742d9bb468740b7ab3b239085177d0404aacd5c50eb4920acd2a |
| +internal/release/roothistory_retirement.go RootHistory.recoveryRetired authority E1 Exact completion binds the accepted authorization digest from durable private history to enrolled-root signed readback. Pending originals, changed evidence and canonical sources cannot supply retirement completion. Fixtures: TestPlan14HistoryReconstructionCompletion, TestPlan14HistoryRetirementCompletion, TestPlan14ReadReconstructionCompletion, TestPlan14ReadRetirementCompletion, TestPlan14RecoveryRetirementProductionCallback, TestPlan14RootHistoryReconstructionCrash, TestPlan14HistoryRetirementProcessCrash. 8a25353fae605dcc105cb9d42513f004f2e7478f76b59483bb0288b285d6a212 |
| internal/release/roothistory_retirement.go historyRecoveryDigest authority E1 Exact signed recovery retirement binds namespace, complete outcome digest and a later root-verified success; pending originals and changed evidence remain restrictive. Independent settlement publishes status without controlling effects. Fixtures: TestPlan14HistoryExactRecoveryRetirement, TestPlan14HistoryRecoveryRetirementAuthority, TestPlan14HistoryRetirementVisibleSync, TestPlan14HistoryRetirementProcessCrash, TestPlan14RecoveryRetirementProductionCallback, TestPlan14IndependentRecoveryPublishesStatus. 92010ae7a24d9bcdfa237da83c2e832926c8244c6df9d2f697833ac69671f9e4 |
| internal/release/roothistory_retirement.go historyRetirementKey authority E1 Exact signed recovery retirement binds namespace, complete outcome digest and a later root-verified success; pending originals and changed evidence remain restrictive. Independent settlement publishes status without controlling effects. Fixtures: TestPlan14HistoryExactRecoveryRetirement, TestPlan14HistoryRecoveryRetirementAuthority, TestPlan14HistoryRetirementVisibleSync, TestPlan14HistoryRetirementProcessCrash, TestPlan14RecoveryRetirementProductionCallback, TestPlan14IndependentRecoveryPublishesStatus. 9e32f1cc354ae4921dd12e2a103208891dabba66594b8fa15d8782e391dd0540 |
| internal/release/roothistory_retirement.go retirementEvidence authority E1 Exact signed recovery retirement binds namespace, complete outcome digest and a later root-verified success; pending originals and changed evidence remain restrictive. Independent settlement publishes status without controlling effects. Fixtures: TestPlan14HistoryExactRecoveryRetirement, TestPlan14HistoryRecoveryRetirementAuthority, TestPlan14HistoryRetirementVisibleSync, TestPlan14HistoryRetirementProcessCrash, TestPlan14RecoveryRetirementProductionCallback, TestPlan14IndependentRecoveryPublishesStatus. 98a3c84eede1e1d039d42db42a61f167877b0bbf8a5f45cea384a2941281c81c |
| @@ -93,7 +93,7 @@ internal/release/roothistory_retirement.go validHistoryRetirement authority E1 E |
| internal/release/roothistory_status.go HistoryStatusKey authority E1 Exact signed recovery retirement binds namespace, complete outcome digest and a later root-verified success; pending originals and changed evidence remain restrictive. Independent settlement publishes status without controlling effects. Fixtures: TestPlan14HistoryExactRecoveryRetirement, TestPlan14HistoryRecoveryRetirementAuthority, TestPlan14HistoryRetirementVisibleSync, TestPlan14HistoryRetirementProcessCrash, TestPlan14RecoveryRetirementProductionCallback, TestPlan14IndependentRecoveryPublishesStatus. 302a4b007238395e23f3649bac42ad5c68c9b9727bbce54fc775e75da7e0af5e |
| internal/release/roothistory_status.go RootHistory.ProjectMigrationStatus authority E1 Challenge-bound release-history readback authenticates the deployment request and active enrolled box key, rereads private history with original recovery restrictions, and rechecks roster/fence state. No status grants completeness or changes service effects. Fixtures: TestPlan14HistoryReadback, TestPlan14HistoryReadbackAuthority, TestPlan14HistoryReadbackReadRace, TestPlan14HistoryReadbackReenrollment, TestPlan14HistoryReadbackRequestRefusal, TestPlan14HistoryReadbackProduction, TestPlan14HistoryReadbackProcessCrash. aa46b9876a485c18a790975804d9da01beae059f3f5cdf62b507c447c8914757 |
| internal/release/roothistory_status.go const RootHistoryStatusDir authority E1 Exact signed recovery retirement binds namespace, complete outcome digest and a later root-verified success; pending originals and changed evidence remain restrictive. Independent settlement publishes status without controlling effects. Fixtures: TestPlan14HistoryExactRecoveryRetirement, TestPlan14HistoryRecoveryRetirementAuthority, TestPlan14HistoryRetirementVisibleSync, TestPlan14HistoryRetirementProcessCrash, TestPlan14RecoveryRetirementProductionCallback, TestPlan14IndependentRecoveryPublishesStatus. c7fda64b13809b5d5b9c61e055aa2e5cc47fc018eaf0ee6a62a0b37408990142 |
| -internal/release/roothistory_status.go type HistoryHostStatus authority E1 Exact signed recovery retirement binds namespace, complete outcome digest and a later root-verified success; pending originals and changed evidence remain restrictive. Independent settlement publishes status without controlling effects. Fixtures: TestPlan14HistoryExactRecoveryRetirement, TestPlan14HistoryRecoveryRetirementAuthority, TestPlan14HistoryRetirementVisibleSync, TestPlan14HistoryRetirementProcessCrash, TestPlan14RecoveryRetirementProductionCallback, TestPlan14IndependentRecoveryPublishesStatus. 4cfaff43ce1651d564797ccfc41e0b17dfdefda033d03d68416d6cc6361e983d |
| +internal/release/roothistory_status.go type HistoryHostStatus authority E1 Exact signed recovery retirement binds namespace, complete outcome digest and a later root-verified success; pending originals and changed evidence remain restrictive. Independent settlement publishes status without controlling effects. Fixtures: TestPlan14HistoryExactRecoveryRetirement, TestPlan14HistoryRecoveryRetirementAuthority, TestPlan14HistoryRetirementVisibleSync, TestPlan14HistoryRetirementProcessCrash, TestPlan14RecoveryRetirementProductionCallback, TestPlan14IndependentRecoveryPublishesStatus. f0f202f04aa982d2867ef9e3d456c342d092abce81546cbdbd185049273f5899 |
| internal/release/roothistory_status.go type HistoryMigrationStatus authority E1 Exact signed recovery retirement binds namespace, complete outcome digest and a later root-verified success; pending originals and changed evidence remain restrictive. Independent settlement publishes status without controlling effects. Fixtures: TestPlan14HistoryExactRecoveryRetirement, TestPlan14HistoryRecoveryRetirementAuthority, TestPlan14HistoryRetirementVisibleSync, TestPlan14HistoryRetirementProcessCrash, TestPlan14RecoveryRetirementProductionCallback, TestPlan14IndependentRecoveryPublishesStatus. dcf1ce27171203061f7b7877a80e3fdcf8ac964309f0fcc47282a7f719f0a32d |
| internal/release/roothistory_status.go type HistoryRecoverySources diagnostic E1 Root journal source availability and unresolved attempts remain explicit in advisory status; no projection grants permission. Fixture: TestPlan14RecoveryRetirementProductionCallback. ff307b83963f45fdefe91b61699f090bd8e3f5e7ec539cec4b4bca51956ad07f |
| internal/host/namedrecovery_pass.go namedUnitRecovery.passOption authority B3,E1 Production recovery wrapper preserves main exit status for per-host failures; only remaining named routes can probe the census for history selection. Ordinary routes make no census or binding-store reads. Fixtures: TestPlan14ERecoveryOrdinaryPass, TestPlan14Fix4AdoptPerHostFailureExitsZero, TestPlan14Fix5MainEquivalence. registry:Host@282,registry:Host@506,registry:Unit@459,registry:Unit@476,whole:Route@419,whole:Route@451 36ef7a4bf960574b35ea896c72612ce9b4a8ad7d2c249eec16f796344b5dd37e |
| @@ -116,5 +116,8 @@ internal/historyreadback/readback.go validStatus authority E1 Challenge-bound re |
| internal/host/roothistory_readback.go historyReadbackPublisher.publish authority E1 Challenge-bound release-history readback authenticates the deployment request and active enrolled box key, rereads private history with original recovery restrictions, and rechecks roster/fence state. No status grants completeness or changes service effects. Fixtures: TestPlan14HistoryReadback, TestPlan14HistoryReadbackAuthority, TestPlan14HistoryReadbackReadRace, TestPlan14HistoryReadbackReenrollment, TestPlan14HistoryReadbackRequestRefusal, TestPlan14HistoryReadbackProduction, TestPlan14HistoryReadbackProcessCrash. file:Read@187 fbd23ac261c8affa9fbed38277d0fe88fa3e56d1dde1d3af5eb0839d2635d00f |
| internal/host/roothistory_readback.go publishRootHistoryReadback authority E1 Challenge-bound release-history readback authenticates the deployment request and active enrolled box key, rereads private history with original recovery restrictions, and rechecks roster/fence state. No status grants completeness or changes service effects. Fixtures: TestPlan14HistoryReadback, TestPlan14HistoryReadbackAuthority, TestPlan14HistoryReadbackReadRace, TestPlan14HistoryReadbackReenrollment, TestPlan14HistoryReadbackRequestRefusal, TestPlan14HistoryReadbackProduction, TestPlan14HistoryReadbackProcessCrash. da8fc3789d39285f28722603c91a5549e77034d1ebb17e839b16b860bd75a425 |
| internal/host/roothistory_readback.go type historyReadbackPublisher authority E1 Challenge-bound release-history readback authenticates the deployment request and active enrolled box key, rereads private history with original recovery restrictions, and rechecks roster/fence state. No status grants completeness or changes service effects. Fixtures: TestPlan14HistoryReadback, TestPlan14HistoryReadbackAuthority, TestPlan14HistoryReadbackReadRace, TestPlan14HistoryReadbackReenrollment, TestPlan14HistoryReadbackRequestRefusal, TestPlan14HistoryReadbackProduction, TestPlan14HistoryReadbackProcessCrash. 0112fc221bb6f41afe48ca3775063fb2138886da6872a141a0e5b1b589f2f43e |
| -internal/release/roothistory_status.go RootHistory.MigrationStatusWithSources authority E1 Challenge-bound release-history readback authenticates the deployment request and active enrolled box key, rereads private history with original recovery restrictions, and rechecks roster/fence state. No status grants completeness or changes service effects. Fixtures: TestPlan14HistoryReadback, TestPlan14HistoryReadbackAuthority, TestPlan14HistoryReadbackReadRace, TestPlan14HistoryReadbackReenrollment, TestPlan14HistoryReadbackRequestRefusal, TestPlan14HistoryReadbackProduction, TestPlan14HistoryReadbackProcessCrash. d458cd512378ad6a01f8cca796f06de78f47dd14976ef0df43fbd8d2fafa162a |
| -@coverage internal/historyreadback 56767f301f27780aec784b34abf9c4e44fd461b5f782c393c3825a4a419c7139 |
| +internal/release/roothistory_status.go RootHistory.MigrationStatusWithSources authority E1 Exact completion binds the accepted authorization digest from durable private history to enrolled-root signed readback. Pending originals, changed evidence and canonical sources cannot supply retirement completion. Fixtures: TestPlan14HistoryReconstructionCompletion, TestPlan14HistoryRetirementCompletion, TestPlan14ReadReconstructionCompletion, TestPlan14ReadRetirementCompletion, TestPlan14RecoveryRetirementProductionCallback, TestPlan14RootHistoryReconstructionCrash, TestPlan14HistoryRetirementProcessCrash. b21c4f2d6ce984632726f7bd1a2d8d876764794a5e937075424ae15fdd97452e |
| +@coverage internal/historyreadback 4ef0528aa6c49e6d32e53f84456680b279d52313a69fdf9aaaceead54af2a8e8 |
| +internal/historyreadback/completion.go ReadReconstruction authority E1 Exact completion binds the accepted authorization digest from durable private history to enrolled-root signed readback. Pending originals, changed evidence and canonical sources cannot supply retirement completion. Fixtures: TestPlan14HistoryReconstructionCompletion, TestPlan14HistoryRetirementCompletion, TestPlan14ReadReconstructionCompletion, TestPlan14ReadRetirementCompletion, TestPlan14RecoveryRetirementProductionCallback, TestPlan14RootHistoryReconstructionCrash, TestPlan14HistoryRetirementProcessCrash. 9f64ca3108a374d26c0f690cd62e3984f25d237175b75a4529a945fda41db5ee |
| +internal/historyreadback/completion.go ReadRetirement authority E1 Exact completion binds the accepted authorization digest from durable private history to enrolled-root signed readback. Pending originals, changed evidence and canonical sources cannot supply retirement completion. Fixtures: TestPlan14HistoryReconstructionCompletion, TestPlan14HistoryRetirementCompletion, TestPlan14ReadReconstructionCompletion, TestPlan14ReadRetirementCompletion, TestPlan14RecoveryRetirementProductionCallback, TestPlan14RootHistoryReconstructionCrash, TestPlan14HistoryRetirementProcessCrash. 051995729c46c84bc96346ca6098c68a4b1317c1d3a2f25655b4d730079534b8 |
| +internal/release/roothistory_retirement.go RootHistory.recoveryRetirementDigest authority E1 Exact completion binds the accepted authorization digest from durable private history to enrolled-root signed readback. Pending originals, changed evidence and canonical sources cannot supply retirement completion. Fixtures: TestPlan14HistoryReconstructionCompletion, TestPlan14HistoryRetirementCompletion, TestPlan14ReadReconstructionCompletion, TestPlan14ReadRetirementCompletion, TestPlan14RecoveryRetirementProductionCallback, TestPlan14RootHistoryReconstructionCrash, TestPlan14HistoryRetirementProcessCrash. file:Read@125 4bfcfe969b2230a094ecdf7f4d7653f8bf9f46c10f3e0419a054c0384c2971c8 |
diff --git a/internal/release/plan14_history_completion_test.go b/internal/release/plan14_history_completion_test.go| new file mode 100644 |
| index 000000000..2a5bcd727 |
| --- /dev/null |
| +++ b/internal/release/plan14_history_completion_test.go |
| @@ -0,0 +1,109 @@ |
| +package release |
| + |
| +import ( |
| + "encoding/json" |
| + "os" |
| + "testing" |
| +) |
| + |
| +func TestPlan14HistoryReconstructionCompletion(t *testing.T) { |
| + for _, boundary := range []string{"before", "intent", "complete", "unsynced"} { |
| + t.Run(boundary, func(t *testing.T) { |
| + h, s, b, pub, wire, now := reconstructionCrashFixture(t) |
| + var g HistoryReconstruction |
| + if err := VerifyRootEnvelope(pub, wire, &g); err != nil { |
| + t.Fatal(err) |
| + } |
| + raw, _ := json.Marshal(g) |
| + resolve := func(HistoryBinding) (string, error) { return "canonical", nil } |
| + if boundary != "before" { |
| + if boundary == "intent" { |
| + h.Store = &reconstructionCrashStore{healthyRetryStore: s, stage: "intent", fail: true} |
| + } |
| + err := h.Reconstruct(pub, wire, b.Account, b.App, now, resolve) |
| + if (boundary == "intent") != (err != nil) { |
| + t.Fatalf("reconstruct: %v", err) |
| + } |
| + } |
| + h = &RootHistory{Store: s} |
| + if boundary == "unsynced" { |
| + s.fail = 1 |
| + } |
| + view, err := h.MigrationStatus(b, resolve) |
| + if boundary == "unsynced" { |
| + if err == nil { |
| + t.Fatal("unsynced completion published") |
| + } |
| + view, err = h.MigrationStatus(b, resolve) |
| + } |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + want := "" |
| + if boundary == "complete" || boundary == "unsynced" { |
| + want = reconstructionDigest(raw) |
| + } |
| + if view.LastReconstructionDigest != want { |
| + t.Fatalf("completion=%q want %q", view.LastReconstructionDigest, want) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestPlan14HistoryRetirementCompletion(t *testing.T) { |
| + for _, fault := range []string{"valid", "missing", "unsynced", "changed outcome", "pending original", "changed source"} { |
| + t.Run(fault, func(t *testing.T) { |
| + h, s, old, _, g, pub, key := retirementFixture(t) |
| + if fault != "missing" { |
| + if err := retireForTest(h, g, pub, key); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + raw, _ := json.Marshal(g) |
| + if fault == "changed outcome" { |
| + old.Verified = true |
| + if err := h.RetainRecovery(old); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if fault == "unsynced" { |
| + h.Store = &retirementSyncStore{Store: s, fail: true} |
| + } |
| + source := "canonical" |
| + if fault == "changed source" { |
| + source = "changed" |
| + } |
| + sources := HistoryRecoverySources{Known: true} |
| + if fault == "pending original" { |
| + old.Settled = false |
| + sources.Unresolved = []HistoryRecovery{old} |
| + } |
| + view, err := h.MigrationStatusWithSources(old.Binding, func(HistoryBinding) (string, error) { return source, nil }, sources) |
| + if fault == "unsynced" { |
| + if err == nil { |
| + t.Fatal("unsynced retirement published") |
| + } |
| + h.Store = s |
| + view, err = h.MigrationStatusWithSources(old.Binding, func(HistoryBinding) (string, error) { return source, nil }, sources) |
| + } |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + want := "" |
| + if fault == "valid" || fault == "unsynced" { |
| + want = reconstructionDigest(raw) |
| + } |
| + if len(view.Hosts) != 1 || view.Hosts[0].RetirementDigests[old.Attempt.ID] != want { |
| + t.Fatalf("status=%+v want %q", view, want) |
| + } |
| + if fault == "pending original" { |
| + if len(view.RecoverySources.Unresolved) != 1 { |
| + t.Fatal("lost original") |
| + } |
| + } |
| + if _, err := s.Read(historyRecoveryKey(old)); os.IsNotExist(err) { |
| + t.Fatal("deleted retained outcome") |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/release/plan14_history_retirement_process_test.go b/internal/release/plan14_history_retirement_process_test.go| index 685397e33..525d317d3 100644 |
| --- a/internal/release/plan14_history_retirement_process_test.go |
| +++ b/internal/release/plan14_history_retirement_process_test.go |
| @@ -77,6 +77,21 @@ func TestPlan14HistoryRetirementProcessCrash(t *testing.T) { |
| } |
| fault := &retirementSyncStore{Store: s, fail: boundary == "visible"} |
| h := &RootHistory{Store: fault} |
| + view, statusErr := h.MigrationStatus(old.Binding, func(HistoryBinding) (string, error) { return "canonical", nil }) |
| + if boundary == "visible" { |
| + if statusErr == nil { |
| + t.Fatal("unsynced retirement completion after crash") |
| + } |
| + } else { |
| + expected := "" |
| + if boundary == "durable" { |
| + accepted, _ := json.Marshal(g) |
| + expected = reconstructionDigest(accepted) |
| + } |
| + if statusErr != nil || view.Hosts[0].RetirementDigests[old.Attempt.ID] != expected { |
| + t.Fatalf("crash completion: %+v %v", view, statusErr) |
| + } |
| + } |
| refs, err := h.Refusals(old.Binding) |
| if boundary == "visible" { |
| if err == nil { |
| @@ -85,6 +100,15 @@ func TestPlan14HistoryRetirementProcessCrash(t *testing.T) { |
| fault.fail = false |
| refs, err = h.Refusals(old.Binding) |
| } |
| + view, statusErr = h.MigrationStatus(old.Binding, func(HistoryBinding) (string, error) { return "canonical", nil }) |
| + expected := "" |
| + if boundary != "before" { |
| + accepted, _ := json.Marshal(g) |
| + expected = reconstructionDigest(accepted) |
| + } |
| + if statusErr != nil || view.Hosts[0].RetirementDigests[old.Attempt.ID] != expected { |
| + t.Fatalf("repaired completion: %+v %v", view, statusErr) |
| + } |
| want := 0 |
| if boundary == "before" { |
| want = 1 |
diff --git a/internal/release/plan14_root_reconstruction_test.go b/internal/release/plan14_root_reconstruction_test.go| index f6f1de88a..48bb041db 100644 |
| --- a/internal/release/plan14_root_reconstruction_test.go |
| +++ b/internal/release/plan14_root_reconstruction_test.go |
| @@ -175,6 +175,22 @@ func TestPlan14RootHistoryReconstructionCrash(t *testing.T) { |
| if !errors.As(err, &exit) || exit.ExitCode() != 83 { |
| t.Fatalf("child: %v %s", err, out) |
| } |
| + var accepted HistoryReconstruction |
| + if err := VerifyRootEnvelope(pub, wire, &accepted); err != nil { |
| + t.Fatal(err) |
| + } |
| + acceptedRaw, _ := json.Marshal(accepted) |
| + view, err := h.MigrationStatus(b, resolve) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + want := "" |
| + if stage == "complete" { |
| + want = reconstructionDigest(acceptedRaw) |
| + } |
| + if view.LastReconstructionDigest != want { |
| + t.Fatal("crash reported wrong completion") |
| + } |
| if stage != "complete" { |
| if _, err := h.Refusals(b); err == nil { |
| t.Fatal("partial reconstruction granted refusal completeness") |
| @@ -195,6 +211,10 @@ func TestPlan14RootHistoryReconstructionCrash(t *testing.T) { |
| if err := h.ResumeReconstruction(resolve); err != nil { |
| t.Fatal(err) |
| } |
| + view, err = h.MigrationStatus(b, resolve) |
| + if err != nil || view.LastReconstructionDigest != reconstructionDigest(acceptedRaw) || view.Pending { |
| + t.Fatalf("resume completion: %+v %v", view, err) |
| + } |
| if _, err := h.AppRefusals(b.Account, b.App); err != nil { |
| t.Fatal(err) |
| } |
diff --git a/internal/release/roothistory_reconstruct.go b/internal/release/roothistory_reconstruct.go| index f8cab93e6..c248b0d6b 100644 |
| --- a/internal/release/roothistory_reconstruct.go |
| +++ b/internal/release/roothistory_reconstruct.go |
| @@ -48,9 +48,10 @@ type HistoryReconstructionHold struct { |
| } |
| type HistoryReconstructionStatus struct { |
| - Identity string `json:"identity"` |
| - Revision uint64 `json:"revision"` |
| - Pending bool `json:"pending"` |
| + LastReconstructionDigest string `json:"last_reconstruction_digest,omitempty"` |
| + Identity string `json:"identity"` |
| + Revision uint64 `json:"revision"` |
| + Pending bool `json:"pending"` |
| } |
| type reconstructionImage struct { |
| @@ -123,7 +124,7 @@ func (h *RootHistory) ReconstructionStatus() (HistoryReconstructionStatus, error |
| if err != nil { |
| return HistoryReconstructionStatus{}, err |
| } |
| - return HistoryReconstructionStatus{Identity: s.Identity, Revision: s.Revision, Pending: s.Pending != nil}, nil |
| + return HistoryReconstructionStatus{Identity: s.Identity, Revision: s.Revision, Pending: s.Pending != nil, LastReconstructionDigest: s.Last}, nil |
| } |
| func (h *RootHistory) reconstructionReady() error { |
diff --git a/internal/release/roothistory_retirement.go b/internal/release/roothistory_retirement.go| index e97653770..a222e5849 100644 |
| --- a/internal/release/roothistory_retirement.go |
| +++ b/internal/release/roothistory_retirement.go |
| @@ -112,32 +112,37 @@ func (h *RootHistory) RetireRecovery(pub ed25519.PublicKey, wire []byte, b Histo |
| return h.Store.Write(historyRetirementKey(HistoryRecovery{Binding: b, Attempt: HistoryAttempt{ID: g.AttemptID}}), raw) |
| } |
| -func (h *RootHistory) recoveryRetired(r HistoryRecovery, outcomes []HistoryRecovery) (bool, error) { |
| +func (h *RootHistory) recoveryRetirementDigest(r HistoryRecovery, outcomes []HistoryRecovery) (string, error) { |
| raw, err := h.Store.Read(historyRetirementKey(r)) |
| if errors.Is(err, os.ErrNotExist) { |
| - return false, nil |
| + return "", nil |
| } |
| if err != nil { |
| - return false, err |
| + return "", err |
| } |
| var g HistoryRecoveryRetirement |
| if len(raw) > rootstate.MaxRecord || strictCanonicalJSON(raw, &g) != nil || !validHistoryRetirement(g) || g.Binding != r.Binding || g.AttemptID != r.Attempt.ID { |
| - return false, os.ErrPermission |
| + return "", os.ErrPermission |
| } |
| state, err := h.reconstructionState() |
| if err != nil { |
| - return false, err |
| + return "", err |
| } |
| if state.Pending != nil || state.Identity != g.Identity { |
| - return false, os.ErrPermission |
| + return "", os.ErrPermission |
| } |
| if historyRecoveryDigest(r) != g.OutcomeDigest || !retirementEvidence(g, outcomes) { |
| - return false, nil |
| + return "", nil |
| } |
| if err := h.Store.Write(historyRetirementKey(r), raw); err != nil { |
| - return false, err |
| + return "", err |
| } |
| - return true, nil |
| + return reconstructionDigest(raw), nil |
| +} |
| + |
| +func (h *RootHistory) recoveryRetired(r HistoryRecovery, outcomes []HistoryRecovery) (bool, error) { |
| + digest, err := h.recoveryRetirementDigest(r, outcomes) |
| + return digest != "", err |
| } |
| func (h *RootHistory) RecoveryRetired(r HistoryRecovery) (bool, error) { |
diff --git a/internal/release/roothistory_status.go b/internal/release/roothistory_status.go| index 7f4150415..a1eb1a840 100644 |
| --- a/internal/release/roothistory_status.go |
| +++ b/internal/release/roothistory_status.go |
| @@ -13,6 +13,7 @@ import ( |
| const RootHistoryStatusDir = "/var/lib/carlos-release/history-status" |
| type HistoryHostStatus struct { |
| + RetirementDigests map[string]string `json:"retirement_digests,omitempty"` |
| HistoryReconstructionHost |
| Programs historyPrograms `json:"programs"` |
| RefusalStatus historyRefusals `json:"refusal_status"` |
| @@ -153,22 +154,35 @@ func (h *RootHistory) MigrationStatus(b HistoryBinding, resolve func(HistoryBind |
| var recoveries []HistoryRecovery |
| digests := map[string]string{} |
| var retired []string |
| + retirementDigests := map[string]string{} |
| for _, r := range outcomes { |
| if r.Binding == binding { |
| recoveries = append(recoveries, r) |
| digests[r.Attempt.ID] = historyRecoveryDigest(r) |
| if state.Pending == nil { |
| - ok, err := h.recoveryRetired(r, outcomes) |
| + digest, err := h.recoveryRetirementDigest(r, outcomes) |
| if err != nil { |
| return out, err |
| } |
| - if ok { |
| + if digest != "" { |
| + retirementRaw, err := h.Store.Read(historyRetirementKey(r)) |
| + var grant HistoryRecoveryRetirement |
| + if err != nil { |
| + return out, err |
| + } |
| + if reconstructionDigest(retirementRaw) != digest || strictCanonicalJSON(retirementRaw, &grant) != nil { |
| + return out, os.ErrPermission |
| + } |
| + if grant.Source != source { |
| + continue |
| + } |
| + retirementDigests[r.Attempt.ID] = digest |
| retired = append(retired, r.Attempt.ID) |
| } |
| } |
| } |
| } |
| - out.Hosts = append(out.Hosts, HistoryHostStatus{Recoveries: recoveries, RecoveryDigests: digests, RetiredRecoveries: retired, HistoryReconstructionHost: HistoryReconstructionHost{Host: host, Source: source, Revision: record.Revision, Refusals: record.Refusals.Values}, Programs: record.Programs, RefusalStatus: record.Refusals, Attempt: record.Attempt}) |
| + out.Hosts = append(out.Hosts, HistoryHostStatus{RetirementDigests: retirementDigests, Recoveries: recoveries, RecoveryDigests: digests, RetiredRecoveries: retired, HistoryReconstructionHost: HistoryReconstructionHost{Host: host, Source: source, Revision: record.Revision, Refusals: record.Refusals.Values}, Programs: record.Programs, RefusalStatus: record.Refusals, Attempt: record.Attempt}) |
| } |
| for _, host := range out.Hosts { |
| source, err := resolve(HistoryBinding{Host: host.Host, Account: b.Account, App: b.App}) |
| @@ -211,6 +225,7 @@ func (h *RootHistory) MigrationStatusWithSources(b HistoryBinding, resolve func( |
| effective.Unresolved = append(effective.Unresolved, r) |
| for i := range view.Hosts { |
| if view.Hosts[i].Host == r.Binding.Host { |
| + delete(view.Hosts[i].RetirementDigests, r.Attempt.ID) |
| view.Hosts[i].RetiredRecoveries = slices.DeleteFunc(view.Hosts[i].RetiredRecoveries, func(id string) bool { return id == r.Attempt.ID }) |
| } |
| } |