WIP files seed checkpoint 5 (not for merge): legacy and empty-bootstrap retirement authorization and recovery
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9 files changed,
+1142
−49
internal/host/claimservinglegacy.go+129 −0internal/host/claimservinglegacyroot_linux.go+264 −0internal/host/claimservinglegacyroot_linux_test.go+597 −0internal/host/claimservingpark_records.go+41 −7internal/host/claimservingparkroot_linux.go+28 −17internal/host/claimservingparkroot_linux_test.go+2 −2internal/host/claimstop.go+19 −13internal/host/claimstop_noprocess_test.go+40 −2internal/provision/testdata/plan14-root-reads-seed.tsv+22 −8
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 333.475s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 3.079s
ok github.com/carlosframework/platform/cmd/moveprobe 92.206s
ok github.com/carlosframework/platform/cmd/zddprobe 0.702s
ok github.com/carlosframework/platform/internal/activator 233.782s
ok github.com/carlosframework/platform/internal/agentsock 0.016s
ok github.com/carlosframework/platform/internal/alerting 0.005s
ok github.com/carlosframework/platform/internal/analytics 0.018s
ok github.com/carlosframework/platform/internal/appconfig 2.288s
ok github.com/carlosframework/platform/internal/apperr 0.082s
ok github.com/carlosframework/platform/internal/applog 0.132s
ok github.com/carlosframework/platform/internal/awsinv 0.004s
ok github.com/carlosframework/platform/internal/boxchoice 0.004s
ok github.com/carlosframework/platform/internal/boxid 0.834s
? 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.009s
ok github.com/carlosframework/platform/internal/caps 0.012s
ok github.com/carlosframework/platform/internal/certwarm 0.002s
ok github.com/carlosframework/platform/internal/cgroup 0.698s
ok github.com/carlosframework/platform/internal/changelog 0.291s
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 361.688s
? 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 5.673s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.010s
ok github.com/carlosframework/platform/internal/dbverify 0.005s
ok github.com/carlosframework/platform/internal/dnsrec 0.004s
ok github.com/carlosframework/platform/internal/docsite 0.005s
ok github.com/carlosframework/platform/internal/domains 2.107s
ok github.com/carlosframework/platform/internal/doorkey 0.006s
ok github.com/carlosframework/platform/internal/economics 0.005s
ok github.com/carlosframework/platform/internal/edge 26.079s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.009s
ok github.com/carlosframework/platform/internal/fleetapi 0.124s
ok github.com/carlosframework/platform/internal/gate 1.656s
ok github.com/carlosframework/platform/internal/genselect 0.017s
ok github.com/carlosframework/platform/internal/geo 0.011s
ok github.com/carlosframework/platform/internal/geoip 0.031s
ok github.com/carlosframework/platform/internal/host 128.964s
ok github.com/carlosframework/platform/internal/hostactors 0.340s
ok github.com/carlosframework/platform/internal/hostbox 2.282s
ok github.com/carlosframework/platform/internal/hostclaim 0.506s
ok github.com/carlosframework/platform/internal/hostevidence 0.006s
ok github.com/carlosframework/platform/internal/hostexec 4.748s
ok github.com/carlosframework/platform/internal/hostfence 0.033s
ok github.com/carlosframework/platform/internal/hostfiles 10.481s
ok github.com/carlosframework/platform/internal/hostfloor 0.012s
ok github.com/carlosframework/platform/internal/hostguard 1.012s
ok github.com/carlosframework/platform/internal/hostops 1.425s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.002s
ok github.com/carlosframework/platform/internal/hostquarantine 0.002s
ok github.com/carlosframework/platform/internal/hostrelocate 0.478s
ok github.com/carlosframework/platform/internal/hostresolve 0.145s
ok github.com/carlosframework/platform/internal/hostwrite 0.005s
ok github.com/carlosframework/platform/internal/infraview 0.002s
ok github.com/carlosframework/platform/internal/instancectl 1.820s
ok github.com/carlosframework/platform/internal/instanceexec 2.689s
ok github.com/carlosframework/platform/internal/instancefs 0.007s
ok github.com/carlosframework/platform/internal/instancekey 0.018s
ok github.com/carlosframework/platform/internal/instanceupdates 0.025s
ok github.com/carlosframework/platform/internal/layout 1.431s
ok github.com/carlosframework/platform/internal/lease 9.458s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.025s
ok github.com/carlosframework/platform/internal/ledger 0.014s
ok github.com/carlosframework/platform/internal/mail 0.161s
ok github.com/carlosframework/platform/internal/maintenance 0.078s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.520s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.088s
ok github.com/carlosframework/platform/internal/metrics 0.005s
ok github.com/carlosframework/platform/internal/moves 20.933s
ok github.com/carlosframework/platform/internal/moves/archive 0.743s
ok github.com/carlosframework/platform/internal/objstore 0.181s
ok github.com/carlosframework/platform/internal/ops 0.163s
ok github.com/carlosframework/platform/internal/park 0.199s
ok github.com/carlosframework/platform/internal/privatefile 0.007s
ok github.com/carlosframework/platform/internal/provision 10.957s
ok github.com/carlosframework/platform/internal/publicrelease 0.007s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 3.981s
ok github.com/carlosframework/platform/internal/relay 7.235s
ok github.com/carlosframework/platform/internal/release 19.240s
ok github.com/carlosframework/platform/internal/rollout 3.606s
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.010s
ok github.com/carlosframework/platform/internal/rootstate 0.026s
ok github.com/carlosframework/platform/internal/s3compat 0.005s
ok github.com/carlosframework/platform/internal/safefile 0.004s
ok github.com/carlosframework/platform/internal/schedule 2.278s
ok github.com/carlosframework/platform/internal/sim/hostlife 8.780s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 4.222s
ok github.com/carlosframework/platform/internal/status 0.041s
ok github.com/carlosframework/platform/internal/steering 0.006s
ok github.com/carlosframework/platform/internal/stopevidence 0.282s
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.003s
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.004s
ok github.com/carlosframework/platform/internal/version 0.002s
ok github.com/carlosframework/platform/internal/vet 1.545s
=== 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/14 08:04:08 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE56B7A50525B4, 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/host/claimservinglegacy.go b/internal/host/claimservinglegacy.go| new file mode 100644 |
| index 000000000..9b318bbe4 |
| --- /dev/null |
| +++ b/internal/host/claimservinglegacy.go |
| @@ -0,0 +1,129 @@ |
| +package host |
| + |
| +import ( |
| + "bytes" |
| + "context" |
| + "encoding/json" |
| + "errors" |
| + "os" |
| + "reflect" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| +) |
| + |
| +// A session-zero witness is retained separately from fence receipts. Admission |
| +// changes its ETag; keeping the original observation binds it to the first cold |
| +// admission without pretending that session zero held a serving fence. |
| +type servingLegacyStop struct { |
| + Observed hostclaim.Observed |
| + Result StopResult |
| +} |
| + |
| +type servingLegacyEvidence struct { |
| + Selector servingLegacyPredecessor |
| + NoProcess *servingLegacyStop `json:",omitempty"` |
| +} |
| + |
| +func servingLegacyKey(kind string, a servingArtifactIdentity) string { |
| + return kind + "-" + servingParkKey(a.Box, a.Host+"\x00"+a.Incarnation) + ".json" |
| +} |
| + |
| +func validateServingLegacyStop(s servingLegacyStop) error { |
| + o, result := s.Observed, s.Result |
| + if o.Absent || o.ETag == "" || o.Claim == nil || o.Claim.Holder == nil || o.Claim.Host != o.Host { |
| + return hostbox.ErrChanged |
| + } |
| + c, h, t := o.Claim, o.Claim.Holder, result.Target |
| + if c.Validate() != nil { |
| + return hostbox.ErrChanged |
| + } |
| + if c.Op.Kind != hostclaim.KindServing || c.Op.ID != "" || c.Op.Phase != "" || c.Op.Exec != t.Box || h.Session != 0 || h.Generation != "" || len(h.Admissions) != 0 || (h.WriterBox != "" && h.WriterBox != t.Box) || t.Host != o.Host || t.Incarnation != h.Incarnation || t.Stamp != h.Stamp || t.Stamp == 0 || t.Session != 0 || t.FenceID != "" || t.Op != "" || t.Phase != "" || t.ObligationID != "" || t.ClaimGen != c.Gen || t.ClaimETag != o.ETag { |
| + return hostbox.ErrChanged |
| + } |
| + var r StopReceipt |
| + if json.Unmarshal(result.Bytes, &r) != nil || !bytes.Equal(result.Bytes, mustMarshalStopReceipt(r)) || r.V != 1 || r.Kind != StopReceiptNoProcess || !stopComponent(r.ID) || r.ProducerDrainDigest != "" || !stopDigest(r.InventorySHA256) || !stopDigest(r.LocalJournalSHA256) || len(r.CgroupIdentities) == 0 || len(r.UnitInvocationIDs) != 0 || r.ClaimETag != o.ETag || r.ClaimGen != c.Gen || result.EvidenceAt.IsZero() || result.Key != stopEvidenceKey(o.Host, "", t.Box, r.ID) { |
| + return ErrClaimStopReceipt |
| + } |
| + req := StopRequest{Region: t.Region, Host: t.Host, Incarnation: t.Incarnation, InstallationStamp: t.Stamp, Box: t.Box, Enrollment: t.Enrollment, BootID: t.BootID, ControllerID: t.ControllerID, ExpectedClaimETag: o.ETag, ExpectedDirectoryRevision: t.DirectoryRevision} |
| + if t.Region == "" || t.Box == "" || t.Enrollment == "" || t.BootID == "" || t.ControllerID == "" || t.DirectoryRevision == "" { |
| + return ErrClaimStopReceipt |
| + } |
| + _, err := validateTransportStop(req, result) |
| + return err |
| +} |
| + |
| +func (l *servingParkLedger) legacyStop(a servingArtifactIdentity) (servingLegacyStop, error) { |
| + var s servingLegacyStop |
| + b, err := l.state.Read(servingLegacyKey("legacy-stop", a)) |
| + if err != nil { |
| + return s, err |
| + } |
| + if servingParkDecode(b, &s) != nil || validateServingLegacyStop(s) != nil { |
| + return s, hostbox.ErrChanged |
| + } |
| + t := s.Result.Target |
| + if t.Host != a.Host || t.Box != a.Box || t.Region != a.Region || t.Incarnation != a.Incarnation { |
| + return s, hostbox.ErrChanged |
| + } |
| + return s, nil |
| +} |
| + |
| +func (l *servingParkLedger) legacyConsumption(a servingArtifactIdentity) (*servingLegacyConsumption, error) { |
| + b, err := l.state.Read(servingLegacyKey("legacy", a)) |
| + if errors.Is(err, os.ErrNotExist) { |
| + return nil, nil |
| + } |
| + if err != nil { |
| + return nil, err |
| + } |
| + var c servingLegacyConsumption |
| + if servingParkDecode(b, &c) != nil || !c.Authorization.Legacy || c.Authorization.Successor.Host != a.Host || c.Authorization.Successor.Box != a.Box || c.Authorization.Successor.Incarnation != a.Incarnation { |
| + return nil, hostbox.ErrChanged |
| + } |
| + return &c, nil |
| +} |
| + |
| +func sameServingLegacyEvidence(a, b *servingLegacyEvidence) bool { return reflect.DeepEqual(a, b) } |
| + |
| +func validateServingLegacyEvidence(a servingParkAuthorization, evidence *servingLegacyEvidence) error { |
| + if !a.Legacy || evidence == nil { |
| + return hostbox.ErrChanged |
| + } |
| + p := evidence.Selector |
| + if (p.Session == 0) != (p.Generation == "") || p.Session >= a.OriginalSuccessor.Session || (p.Empty && p.Session != 0) { |
| + return hostbox.ErrChanged |
| + } |
| + if p.Session != 0 { |
| + if evidence.NoProcess != nil { |
| + return hostbox.ErrChanged |
| + } |
| + return nil |
| + } |
| + if evidence.NoProcess == nil || validateServingLegacyStop(*evidence.NoProcess) != nil { |
| + return hostbox.ErrChanged |
| + } |
| + s, current := *evidence.NoProcess, a.OriginalSuccessor |
| + h, t := s.Observed.Claim.Holder, s.Result.Target |
| + if t.Host != current.Host || t.Box != current.Box || t.Region != current.Region || t.Incarnation != current.Incarnation || h.Location != current.Location || h.Stamp >= current.Stamp || s.Observed.Claim.Gen >= current.Stamp || p.Empty != (h.WriterBox == "") { |
| + return hostbox.ErrChanged |
| + } |
| + return nil |
| +} |
| + |
| +// The current writer must still be only a preparation. Even an uncompleted |
| +// launch intention makes untagged bytes ambiguous and forbids legacy capture. |
| +func (l *servingParkLedger) legacyUnlaunched(ctx context.Context, a servingArtifactIdentity, enrollment string) error { |
| + records, err := l.journal.RootSessionLaunches(ctx, a.Host, a.Incarnation, a.Stamp, a.Session, a.Box, enrollment) |
| + if err != nil { |
| + return err |
| + } |
| + for _, r := range records { |
| + for _, reservation := range r.Reservations { |
| + if reservation.Role == "main" { |
| + return hostbox.ErrChanged |
| + } |
| + } |
| + } |
| + return nil |
| +} |
diff --git a/internal/host/claimservinglegacyroot_linux.go b/internal/host/claimservinglegacyroot_linux.go| new file mode 100644 |
| index 000000000..47d042597 |
| --- /dev/null |
| +++ b/internal/host/claimservinglegacyroot_linux.go |
| @@ -0,0 +1,264 @@ |
| +//go:build linux |
| + |
| +package host |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "net" |
| + "os" |
| + "reflect" |
| + "strconv" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/hostguard" |
| +) |
| + |
| +// Called only by the root stop coordinator after its kernel observation and |
| +// before publishing the receipt. There is no transport operation for inserting |
| +// this evidence. A claim change during the observation or lock wait refuses it. |
| +func (p *servingParkRoot) retainLegacyNoProcess(ctx context.Context, o hostclaim.Observed, result StopResult) error { |
| + if p == nil || p.ledger == nil || p.root.RequireNewWork == nil || p.root.InventoryActor == nil || p.root.Stop.Coordinator.Guard.Root == nil { |
| + return hostbox.ErrUnsupported |
| + } |
| + s := servingLegacyStop{Observed: o, Result: result} |
| + if validateServingLegacyStop(s) != nil { |
| + return ErrClaimStopReceipt |
| + } |
| + return p.ledger.withNamespace(ctx, func() error { |
| + return p.root.Stop.Coordinator.Guard.With(ctx, o.Host, hostguard.Captured{Incarnation: result.Target.Incarnation, Stamp: result.Target.Stamp}, func(c *hostclaim.Claim) error { |
| + if !reflect.DeepEqual(c, o.Claim) { |
| + return hostbox.ErrChanged |
| + } |
| + return nil |
| + }, func() error { |
| + if err := p.root.RequireNewWork(ctx); err != nil { |
| + return err |
| + } |
| + actor, err := p.root.InventoryActor(ctx) |
| + if err != nil { |
| + return err |
| + } |
| + t := result.Target |
| + if !actor.Eligible || actor.Quarantined || actor.Box != t.Box || actor.Region != t.Region || actor.Enrollment != t.Enrollment || actor.Revision == 0 || strconv.FormatUint(actor.Revision, 10) != t.DirectoryRevision { |
| + return hostbox.ErrChanged |
| + } |
| + fresh, err := p.root.Stop.Coordinator.Guard.Claims.Read(ctx, o.Host) |
| + if err != nil { |
| + return err |
| + } |
| + if fresh.ETag != o.ETag || fresh.Absent { |
| + return hostbox.ErrChanged |
| + } |
| + a := servingArtifactIdentity{Host: o.Host, Box: result.Target.Box, Region: result.Target.Region, Incarnation: result.Target.Incarnation} |
| + old, err := p.ledger.legacyStop(a) |
| + if err == nil { |
| + // Retry reservation and settlement change Gen, ETag and WakeRetry |
| + // without admitting a writer. Keep the first receipt across those |
| + // changes, but refuse a different holder or enrollment. |
| + priorClaim, nextClaim := *old.Observed.Claim, *o.Claim |
| + priorHolder, nextHolder := *priorClaim.Holder, *nextClaim.Holder |
| + priorHolder.WakeRetry, nextHolder.WakeRetry = nil, nil |
| + priorClaim.Holder, nextClaim.Holder = &priorHolder, &nextHolder |
| + priorClaim.Gen = nextClaim.Gen |
| + if !reflect.DeepEqual(priorClaim, nextClaim) || old.Result.Target.Enrollment != result.Target.Enrollment || old.Result.Target.DirectoryRevision != result.Target.DirectoryRevision { |
| + return hostbox.ErrChanged |
| + } |
| + return nil |
| + } |
| + if !errors.Is(err, os.ErrNotExist) { |
| + return err |
| + } |
| + return p.ledger.write(servingLegacyKey("legacy-stop", a), s) |
| + }) |
| + }) |
| +} |
| + |
| +func (p *servingParkRoot) validateLegacyRootEvidence(ctx context.Context, o hostclaim.Observed, view hostguard.FenceView, actor hostbox.Actor, a servingParkAuthorization, e *servingLegacyEvidence, receipts []hostguard.FenceReceipt) error { |
| + if validateServingLegacyEvidence(a, e) != nil { |
| + return hostbox.ErrChanged |
| + } |
| + current := a.OriginalSuccessor |
| + var entry *hostclaim.AdmissionRecord |
| + for i := range o.Claim.Holder.Admissions { |
| + r := &o.Claim.Holder.Admissions[i] |
| + if r.Session == current.Session { |
| + if entry != nil { |
| + return hostbox.ErrChanged |
| + } |
| + entry = r |
| + } |
| + } |
| + if entry == nil || entry.Mode != "cold" || entry.Box != current.Box || entry.Region != current.Region || entry.Generation != current.Generation || entry.Predecessor != e.Selector.Generation { |
| + return hostbox.ErrChanged |
| + } |
| + if e.NoProcess != nil { |
| + saved, err := p.ledger.legacyStop(current) |
| + if err != nil || !reflect.DeepEqual(saved, *e.NoProcess) || len(receipts) != 0 { |
| + return hostbox.ErrChanged |
| + } |
| + t, h := saved.Result.Target, saved.Observed.Claim.Holder |
| + if t.Enrollment != actor.Enrollment || t.DirectoryRevision != strconv.FormatUint(actor.Revision, 10) || h.Account != o.Claim.Holder.Account || h.App != o.Claim.Holder.App || h.Key != o.Claim.Holder.Key || !h.CreatedAt.Equal(o.Claim.Holder.CreatedAt) { |
| + return hostbox.ErrChanged |
| + } |
| + for _, r := range o.Claim.Holder.Admissions { |
| + if r.Session < current.Session { |
| + return hostbox.ErrChanged |
| + } |
| + } |
| + if e.Selector.Empty { |
| + if entry.PredecessorRegion != "" || entry.WritesLost != nil { |
| + return hostbox.ErrChanged |
| + } |
| + } else if entry.WritesLost == nil || entry.WritesLost.Box != current.Box || entry.WritesLost.Session != 0 || entry.WritesLost.Generation != "" || entry.PredecessorRegion != current.Region { |
| + return hostbox.ErrChanged |
| + } |
| + return nil |
| + } |
| + prior, err := servingPredecessor(o.Claim, a.Successor, e.Selector.Session, e.Selector.Generation) |
| + if err != nil || prior.Stamp >= current.Stamp { |
| + return hostbox.ErrChanged |
| + } |
| + stops, err := servingRootStops(ctx, view, prior, actor) |
| + if err != nil { |
| + return err |
| + } |
| + if !sameServingRootStops(stops, receipts) { |
| + return ErrClaimStopReceipt |
| + } |
| + return nil |
| +} |
| + |
| +func (p *servingParkRoot) parkLegacyServingDatabase(ctx context.Context, conn net.Conn, current servingArtifactIdentity, requested servingLegacyPredecessor) (servingParkAuthorization, error) { |
| + var out servingParkAuthorization |
| + err := p.withAuthority(ctx, conn, current, func(o hostclaim.Observed, view hostguard.FenceView, actor hostbox.Actor, dir servingArtifactDirectory, tags uint64) error { |
| + var err error |
| + consumed, err := p.ledger.legacyConsumption(current) |
| + if err != nil { |
| + return err |
| + } |
| + a := servingParkAuthorization{Legacy: true, OriginalSuccessor: current, Successor: current, Directory: dir} |
| + a.ID = servingRetirementID(a.Predecessor, current, true) |
| + e := &servingLegacyEvidence{Selector: requested} |
| + var receipts []hostguard.FenceReceipt |
| + var saved *servingParkRecord |
| + if consumed != nil { |
| + a, e, receipts = consumed.Authorization, consumed.LegacyEvidence, consumed.Receipts |
| + if a.Directory != dir || e == nil { |
| + return hostbox.ErrChanged |
| + } |
| + r, readErr := p.ledger.read(a.ID) |
| + if readErr == nil { |
| + saved = &r |
| + } else if !errors.Is(readErr, os.ErrNotExist) { |
| + return readErr |
| + } |
| + } else { |
| + if tags != 0 { |
| + return hostbox.ErrChanged |
| + } |
| + if requested.Session == 0 { |
| + stop, readErr := p.ledger.legacyStop(current) |
| + if readErr != nil { |
| + return readErr |
| + } |
| + e.NoProcess = &stop |
| + } else { |
| + prior, readErr := servingPredecessor(o.Claim, current, requested.Session, requested.Generation) |
| + if readErr != nil { |
| + return readErr |
| + } |
| + receipts, err = servingRootStops(ctx, view, prior, actor) |
| + if err != nil { |
| + return err |
| + } |
| + } |
| + } |
| + if err = p.validateLegacyRootEvidence(ctx, o, view, actor, a, e, receipts); err != nil { |
| + return err |
| + } |
| + active := a |
| + if saved != nil { |
| + if !sameServingLegacySource(saved.Source, consumed.Source) || saved.Authorization != a || !sameServingLegacyEvidence(saved.LegacyEvidence, e) || !sameServingRootStops(saved.Receipts, receipts) { |
| + return hostbox.ErrChanged |
| + } |
| + if err = validateServingRootHistory(ctx, o, view, actor, *saved); err != nil { |
| + return err |
| + } |
| + active = saved.activeAuthorization() |
| + } |
| + if tags != active.TagsFrom && tags != active.Successor.Stamp && tags != current.Stamp { |
| + return hostbox.ErrChanged |
| + } |
| + var next servingParkAuthorization |
| + var stops []hostguard.FenceReceipt |
| + if active.Successor != current { |
| + if saved != nil && saved.Complete || active.Successor.Stamp >= current.Stamp { |
| + return hostbox.ErrChanged |
| + } |
| + prior, e := servingPredecessor(o.Claim, current, active.Successor.Stamp, active.Successor.Generation) |
| + if e != nil || prior != active.Successor { |
| + return hostbox.ErrChanged |
| + } |
| + stops, err = servingRootStops(ctx, view, prior, actor) |
| + if err != nil { |
| + return err |
| + } |
| + next = active |
| + next.Successor, next.TagsFrom = current, tags |
| + } |
| + if err = p.ledger.legacyUnlaunched(ctx, current, actor.Enrollment); err != nil { |
| + return err |
| + } |
| + if consumed == nil && e.Selector.Empty { |
| + if p.empty == nil { |
| + return hostbox.ErrUnsupported |
| + } |
| + if a.Successor != current { |
| + return hostbox.ErrChanged |
| + } |
| + in := ClaimLaunchInputs{Empty: true, Admission: hostclaim.Admission{NewGeneration: current.Generation}} |
| + if err = p.empty.EmptyAuthority(ctx, o, in); err != nil { |
| + return err |
| + } |
| + } |
| + // Remote first-creator proof can wait on I/O; it cannot carry a stale claim |
| + // into the first source capture or any replay mutation. |
| + fresh, err := p.root.Stop.Coordinator.Guard.Claims.Read(ctx, current.Host) |
| + if err != nil { |
| + return err |
| + } |
| + if fresh.Absent || fresh.ETag != o.ETag { |
| + return hostbox.ErrChanged |
| + } |
| + r, err := p.ledger.parkWithEvidenceLocked(ctx, a, receipts, e) |
| + if err != nil { |
| + return err |
| + } |
| + if err = p.ledger.fs.CheckParked(&r); err != nil { |
| + return err |
| + } |
| + if next.ID != "" { |
| + r, err = p.ledger.continueLocked(ctx, a.ID, next, stops) |
| + if err != nil { |
| + return err |
| + } |
| + } |
| + out = r.activeAuthorization() |
| + return nil |
| + }) |
| + return out, err |
| +} |
| + |
| +func sameServingLegacySource(a, b servingParkSource) bool { |
| + if a.Directory != b.Directory || a.Parent != b.Parent { |
| + return false |
| + } |
| + for i := range a.Files { |
| + if !sameServingParkFile(a.Files[i], b.Files[i]) { |
| + return false |
| + } |
| + } |
| + return true |
| +} |
diff --git a/internal/host/claimservinglegacyroot_linux_test.go b/internal/host/claimservinglegacyroot_linux_test.go| new file mode 100644 |
| index 000000000..3ec95cadc |
| --- /dev/null |
| +++ b/internal/host/claimservinglegacyroot_linux_test.go |
| @@ -0,0 +1,597 @@ |
| +//go:build linux |
| + |
| +package host |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "fmt" |
| + "net" |
| + "os" |
| + "path/filepath" |
| + "reflect" |
| + "strings" |
| + "testing" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/genselect" |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/lease" |
| +) |
| + |
| +func newServingLegacyRootFixture(t *testing.T, empty bool, beforeAdmission ...func(*servingRootFixture, *ClaimStopCoordinator, StopRequest)) (*servingRootFixture, ClaimLaunchInputs) { |
| + t.Helper() |
| + f, _, _, in := servingRetirementFixture(t) |
| + if !empty { |
| + _, err := hostclaim.Update(t.Context(), f.claims, f.read(), func(c *hostclaim.Claim) error { |
| + c.Holder.WriterBox = "box-1" |
| + c.Holder.Location = "instances/" + c.Host |
| + return nil |
| + }) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + disk := &servingAuthorityDisk{rollbackDisk: &rollbackDisk{dir: t.TempDir()}} |
| + roots := &servingAuthorityDisk{rollbackDisk: &rollbackDisk{dir: t.TempDir()}} |
| + f.guard.Root = hostguard.NewRootAuthority(roots) |
| + journals, err := hostbox.NewJournalService(f.journal) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + fx := &servingRootFixture{t: t, f: f, disk: disk, rootDisk: roots, db: in.DBPath, actor: hostbox.Actor{Box: "box-1", Region: "ie", Enrollment: "enrollment", Revision: 7, Eligible: true}} |
| + fs := newServingParkFS(f.guard.DataDir, disk) |
| + fs.owner = uint32(os.Geteuid()) |
| + fs.mount = func(string) (bool, error) { return true, nil } |
| + fx.p = &servingParkRoot{ledger: &servingParkLedger{state: disk, fs: fs, journal: f.journal, now: func() time.Time { return f.now }}, empty: plan9D5Fixture(t).owners} |
| + f.peer = StopPeer{UID: 1001, PID: 77, ProcessStart: "edge-start", BootID: "boot", ControllerID: "controller"} |
| + coordinator := f.root(stopDrainFunc(func(context.Context, StopRequest, string) (DrainAcknowledgement, error) { |
| + t.Fatal("session zero requested drain") |
| + return DrainAcknowledgement{}, hostbox.ErrChanged |
| + })) |
| + coordinator.Kernel = &noProcessKernelFake{} |
| + coordinator.Inventory = stopInventoryFunc(func(ctx context.Context, host string) (StopInventory, error) { |
| + return journals.Inventory.ReadStopInventory(ctx, host) |
| + }) |
| + fx.p.root = ClaimRootHTTP{Roots: map[string]string{"data": f.guard.DataDir}, Journals: journals, Stop: ClaimStopHTTP{Coordinator: coordinator, Authenticate: func(context.Context, net.Conn) (StopPeer, error) { return f.peer, nil }}, InventoryActor: func(context.Context) (hostbox.Actor, error) { return fx.actor, nil }, RequireNewWork: func(context.Context) error { return nil }} |
| + fx.install(f.read()) |
| + coordinator.RetainNoProcess = fx.p.retainLegacyNoProcess |
| + o := f.read() |
| + h := o.Claim.Holder |
| + req := StopRequest{Region: "ie", Host: o.Host, Incarnation: h.Incarnation, InstallationStamp: h.Stamp, Box: "box-1", Enrollment: "enrollment", BootID: "boot", ControllerID: "controller", ExpectedClaimETag: o.ETag, ExpectedDirectoryRevision: "7"} |
| + if _, err := coordinator.FenceAndStop(t.Context(), req); err != nil { |
| + t.Fatalf("no-process: %v", err) |
| + } |
| + for _, fn := range beforeAdmission { |
| + fn(fx, &coordinator, req) |
| + } |
| + admission := hostclaim.Admission{ObservedSession: 0, Box: "box-1", Region: "ie", Mode: "cold", NewGeneration: "G"} |
| + if !empty { |
| + admission.PredecessorRegion = "ie" |
| + } |
| + admitted, err := hostclaim.Update(t.Context(), f.claims, f.read(), hostclaim.AdmitWriter(admission)) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + fx.admitted = admitted |
| + fx.current, err = servingWriterIdentity(admitted, "box-1", "ie") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + fx.install(admitted) |
| + in.Admission, in.Empty = admission, empty |
| + if !empty { |
| + for suffix, body := range map[string]string{"": "legacy bytes", "-wal": "legacy WAL", "-shm": "legacy SHM"} { |
| + if err := os.WriteFile(fx.db+suffix, []byte(body), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + } |
| + left, right := net.Pipe() |
| + fx.conn = left |
| + t.Cleanup(func() { left.Close(); right.Close() }) |
| + return fx, in |
| +} |
| + |
| +func TestServingLegacyRootAuthority(t *testing.T) { |
| + for _, empty := range []bool{false, true} { |
| + t.Run(fmt.Sprint(empty), func(t *testing.T) { |
| + f, in := newServingLegacyRootFixture(t, empty) |
| + r := &claimServingArtifactRetirement{Claims: f.f.claims, Guard: f.f.guard, DataDir: f.f.guard.DataDir, Box: f.actor.Box, Region: f.actor.Region, Root: servingRootClientFixture{f}} |
| + work := servingRootWork{f.f.guard, f.admitted} |
| + if err := r.BeforeLegacyInstall(t.Context(), f.admitted, in, work); err != nil { |
| + t.Fatal(err) |
| + } |
| + id := servingRetirementID(servingArtifactIdentity{}, f.current, true) |
| + record, err := f.p.ledger.read(id) |
| + if err != nil || !record.Complete { |
| + t.Fatalf("record: %+v %v", record, err) |
| + } |
| + if record.LegacyEvidence == nil || record.LegacyEvidence.NoProcess == nil || len(record.Receipts) != 0 { |
| + t.Fatal("invented fence for session zero") |
| + } |
| + if (record.Disposition == servingParkArtifactFree) != empty { |
| + t.Fatal(record.Disposition) |
| + } |
| + if !empty { |
| + b, err := os.ReadFile(filepath.Join(f.f.guard.DataDir, "instances", ".serving-parked", id, filepath.Base(f.db))) |
| + if err != nil || string(b) != "legacy bytes" { |
| + t.Fatalf("park %q %v", b, err) |
| + } |
| + } |
| + if err := r.Recover(t.Context(), f.admitted, in); err != nil { |
| + t.Fatalf("lost completion reply: %v", err) |
| + } |
| + if err := work.With(t.Context(), in.Identity, func() error { return r.EnrollLocked(t.Context(), f.admitted, in) }); err != nil { |
| + t.Fatal(err) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestServingLegacyRootRefusals(t *testing.T) { |
| + for _, attack := range []string{"stop-missing", "stop-bytes", "stop-region", "first-creator", "marker", "half-tags", "tags", "peer", "census", "enrollment", "revision", "source", "main-intention", "empty-database", "empty-wal", "empty-remote", "empty-unavailable", "selector"} { |
| + t.Run(attack, func(t *testing.T) { |
| + empty := strings.HasPrefix(attack, "empty-") |
| + f, _ := newServingLegacyRootFixture(t, empty) |
| + requested := servingLegacyPredecessor{Empty: empty} |
| + switch attack { |
| + case "stop-missing": |
| + if err := os.Remove(filepath.Join(f.disk.dir, servingLegacyKey("legacy-stop", f.current))); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "stop-bytes", "stop-region": |
| + s, err := f.p.ledger.legacyStop(f.current) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if attack == "stop-bytes" { |
| + s.Result.Bytes = []byte("forged") |
| + } else { |
| + s.Result.Target.Region = "foreign" |
| + } |
| + if err = f.p.ledger.write(servingLegacyKey("legacy-stop", f.current), s); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "first-creator": |
| + requested.Empty = true |
| + case "marker": |
| + if err := os.WriteFile(f.db+".gen", []byte("foreign"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "tags": |
| + f.tags(f.current.Stamp) |
| + case "half-tags": |
| + _, err := hostclaim.Update(t.Context(), f.f.claims, f.f.read(), func(c *hostclaim.Claim) error { |
| + rel, _ := filepath.Rel(f.f.guard.DataDir, f.db) |
| + c.Inventory = map[string]hostclaim.InventoryEntry{f.actor.Box: {Layout: "nested", Paths: map[string]uint64{rel: f.current.Stamp}}} |
| + return nil |
| + }) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + case "peer": |
| + f.p.root.Stop.Authenticate = func(context.Context, net.Conn) (StopPeer, error) { return StopPeer{}, ErrClaimStopIdentity } |
| + case "census": |
| + f.p.root.RequireNewWork = func(context.Context) error { return hostbox.ErrChanged } |
| + case "enrollment": |
| + f.actor.Enrollment = "changed" |
| + case "revision": |
| + f.actor.Revision++ |
| + case "source": |
| + if err := os.Rename(filepath.Dir(f.db), filepath.Dir(f.db)+".saved"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Mkdir(filepath.Dir(f.db), 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "main-intention": |
| + target := recoveryTuple(f.admitted, claimStopLocal{Actor: f.actor, BootID: "boot", ControllerID: "controller"}) |
| + if err := f.f.journal.Prelaunch(t.Context(), hostbox.LaunchInventory{Target: target, ControllerPID: 77, ProcessStart: "edge-start", NoState: true, Groups: []hostbox.LaunchGroup{{RelativePath: "carlos/parent", Device: 1, Inode: 2}}}); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := f.f.journal.IntendUnit(t.Context(), hostbox.LaunchReservation{ID: "attempt:main", Role: "main", InvocationID: "attempt", Inputs: "digest", Target: target, Group: hostbox.LaunchGroup{RelativePath: "system.slice/carlos-app@shop.service"}, Executable: hostbox.LaunchFile{Device: 1, Inode: 3}}); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "empty-database", "empty-wal": |
| + suffix := "" |
| + if attack == "empty-wal" { |
| + suffix = "-wal" |
| + } |
| + if err := os.WriteFile(f.db+suffix, []byte("unexpected"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "empty-remote": |
| + fixture := plan9D5Fixture(t) |
| + fixture.bucket.Put("data", f.admitted.Claim.Holder.Location+"/unexpected", []byte("x")) |
| + f.p.empty = fixture.owners |
| + case "empty-unavailable": |
| + f.p.empty = nil |
| + case "selector": |
| + requested.Session = 1 |
| + requested.Generation = "forged" |
| + } |
| + before, _ := f.disk.Names() |
| + if _, err := f.p.parkLegacyServingDatabase(t.Context(), f.conn, f.current, requested); err == nil { |
| + t.Fatal("unsafe legacy authority accepted") |
| + } |
| + after, _ := f.disk.Names() |
| + if !reflect.DeepEqual(before, after) { |
| + t.Fatalf("refusal effects: %v -> %v", before, after) |
| + } |
| + if !empty { |
| + db := f.db |
| + if attack == "source" { |
| + db = filepath.Join(filepath.Dir(db)+".saved", filepath.Base(db)) |
| + } |
| + b, err := os.ReadFile(db) |
| + if err != nil || string(b) != "legacy bytes" { |
| + t.Fatalf("source %q %v", b, err) |
| + } |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestServingLegacyRootContinuation(t *testing.T) { |
| + for _, point := range []string{"consumption", "intent", "rename", "parked", "retag", "lost-continuation"} { |
| + t.Run(point, func(t *testing.T) { |
| + f, _ := newServingLegacyRootFixture(t, false) |
| + original := f.current |
| + fs := f.p.ledger.fs.(*servingParkFS) |
| + rename := fs.rename |
| + if point == "rename" { |
| + once := false |
| + fs.rename = func(a int, b string, c int, d string, e uint) error { |
| + err := rename(a, b, c, d, e) |
| + if err == nil && !once { |
| + once = true |
| + return os.ErrPermission |
| + } |
| + return err |
| + } |
| + } |
| + if point == "consumption" || point == "intent" { |
| + n := 1 |
| + if point == "intent" { |
| + n = 2 |
| + } |
| + f.p.ledger.state = &servingParkFaultStore{Store: f.disk, failAt: n} |
| + } |
| + a, err := f.p.parkLegacyServingDatabase(t.Context(), f.conn, f.current, servingLegacyPredecessor{}) |
| + if point == "consumption" || point == "intent" || point == "rename" { |
| + if err == nil { |
| + t.Fatal("fault missed") |
| + } |
| + a.ID = servingRetirementID(servingArtifactIdentity{}, original, true) |
| + } else if err != nil { |
| + t.Fatal(err) |
| + } |
| + fs.rename = rename |
| + f.p.ledger.state = f.disk |
| + before, _ := f.p.ledger.read(a.ID) |
| + if point == "retag" || point == "lost-continuation" { |
| + f.tags(original.Stamp) |
| + } |
| + servingStop(t, f.f, f.f.read()) |
| + o := f.f.read() |
| + later, err := hostclaim.Update(t.Context(), f.f.claims, o, hostclaim.AdmitWriter(hostclaim.Admission{ObservedSession: o.Claim.Holder.Session, ObservedGeneration: o.Claim.Holder.Generation, Box: "box-1", Region: "ie", PredecessorRegion: "ie", Mode: "cold", NewGeneration: "G2"})) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + f.current, err = servingWriterIdentity(later, "box-1", "ie") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + f.admitted = later |
| + f.install(later) |
| + f.f.now = f.f.now.Add(time.Hour) |
| + if point == "lost-continuation" { |
| + f.p.ledger.state = &servingParkFaultStore{Store: f.disk, failAt: 1} |
| + if _, err = f.p.parkLegacyServingDatabase(t.Context(), f.conn, f.current, servingLegacyPredecessor{}); err == nil { |
| + t.Fatal("lost continuation fault missed") |
| + } |
| + f.p.ledger.state = f.disk |
| + } |
| + continued, err := f.p.parkLegacyServingDatabase(t.Context(), f.conn, f.current, servingLegacyPredecessor{}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if continued.ID != a.ID || continued.OriginalSuccessor != original || continued.Successor != f.current { |
| + t.Fatalf("relabelled: %+v", continued) |
| + } |
| + f.tags(f.current.Stamp) |
| + if err = f.p.completeServingDatabasePark(t.Context(), f.conn, a.ID, f.current); err != nil { |
| + t.Fatal(err) |
| + } |
| + after, err := f.p.ledger.read(a.ID) |
| + if err != nil || !after.Complete || len(after.Continuations) != 1 { |
| + t.Fatalf("record %+v %v", after, err) |
| + } |
| + if !before.ParkedAt.IsZero() && !before.ReapAfter.Equal(after.ReapAfter) { |
| + t.Fatal("deadline reset") |
| + } |
| + if err = f.p.completeServingDatabasePark(t.Context(), f.conn, a.ID, original); err == nil { |
| + t.Fatal("old successor completed") |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestServingLegacyRootProductionRestoreSequence(t *testing.T) { |
| + for _, failure := range []string{"", "enrolled-refusal", "missing-marker"} { |
| + t.Run("first-restore-"+failure, func(t *testing.T) { runServingLegacyRestoreSequence(t, failure) }) |
| + } |
| +} |
| + |
| +func runServingLegacyRestoreSequence(t *testing.T, failure string) { |
| + f, in := newServingLegacyRootFixture(t, false) |
| + plan := plan12RestoreBaseline(t, f.f) |
| + var legacy servingParkRecord |
| + for _, generation := range []string{"G", "G2"} { |
| + if generation == "G2" { |
| + prior := f.current |
| + target := recoveryTuple(f.f.read(), claimStopLocal{Actor: f.actor, BootID: "boot", ControllerID: "controller"}) |
| + if err := f.f.journal.Prelaunch(t.Context(), hostbox.LaunchInventory{Target: target, ControllerPID: 77, ProcessStart: "edge-start", NoState: true, Groups: []hostbox.LaunchGroup{{RelativePath: "carlos/parent", Device: 1, Inode: 2}}}); err != nil { |
| + t.Fatal(err) |
| + } |
| + servingStop(t, f.f, f.f.read()) |
| + o := f.f.read() |
| + admission := hostclaim.Admission{ObservedSession: prior.Session, ObservedGeneration: prior.Generation, Box: "box-1", Region: "ie", PredecessorRegion: "ie", Mode: "cold", NewGeneration: generation} |
| + var err error |
| + f.admitted, err = hostclaim.Update(t.Context(), f.f.claims, o, hostclaim.AdmitWriter(admission)) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + f.current, err = servingWriterIdentity(f.admitted, "box-1", "ie") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + in.Admission = admission |
| + f.install(f.admitted) |
| + } |
| + r := &claimServingArtifactRetirement{Claims: f.f.claims, Guard: f.f.guard, DataDir: f.f.guard.DataDir, Box: f.actor.Box, Region: f.actor.Region, Root: servingRootClientFixture{f}} |
| + work := servingRootWork{f.f.guard, f.admitted} |
| + restore := hostbox.ColdRestore{Reader: f.f.data, Bucket: f.f.bucket, Generation: generation, Work: work, BeforeLegacyInstall: func(ctx context.Context, _, _ string) error { return r.BeforeLegacyInstall(ctx, f.admitted, in, work) }, BeforeInstallLocked: func(ctx context.Context, _, _ string) error { return r.EnrollLocked(ctx, f.admitted, in) }, RetireArtifacts: func(ctx context.Context, g string) error { return r.Retire(ctx, f.admitted, in, g) }} |
| + if err := r.Recover(t.Context(), f.admitted, in); err != nil { |
| + t.Fatal(err) |
| + } |
| + if generation == "G" && failure == "enrolled-refusal" { |
| + enroll := restore.BeforeInstallLocked |
| + restore.BeforeInstallLocked = func(ctx context.Context, db, gen string) error { |
| + if err := enroll(ctx, db, gen); err != nil { |
| + return err |
| + } |
| + return os.ErrPermission |
| + } |
| + if err := restore.Install(t.Context(), f.db, plan); !errors.Is(err, os.ErrPermission) { |
| + t.Fatalf("enrollment fault: %v", err) |
| + } |
| + if _, err := os.Stat(f.db); !os.IsNotExist(err) { |
| + t.Fatalf("failed restore left database: %v", err) |
| + } |
| + } else { |
| + if err := restore.Install(t.Context(), f.db, plan); err != nil { |
| + t.Fatal(err) |
| + } |
| + if got := mwReadDB(t, f.db); got != "selected recovery baseline" { |
| + t.Fatal(got) |
| + } |
| + marker, err := hostbox.ReadGenerationMarker(f.db + ".gen") |
| + if err != nil || marker != generation { |
| + t.Fatalf("marker %q %v", marker, err) |
| + } |
| + // Reproduce the durable state after database rename but before |
| + // marker installation, with the actual restored SQLite database. |
| + if generation == "G" && failure == "missing-marker" { |
| + if err := os.Remove(f.db + ".gen"); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + } |
| + slot, err := f.p.ledger.slot(servingParkAuthorization{Successor: f.current}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + record, err := f.p.ledger.read(slot.ID) |
| + if err != nil || !record.Authorization.Legacy { |
| + t.Fatalf("legacy displaced %+v %v", record, err) |
| + } |
| + if generation == "G" { |
| + legacy = record |
| + } else if !reflect.DeepEqual(legacy, record) { |
| + t.Fatal("never-launched restore replaced legacy") |
| + } |
| + } |
| + for suffix, want := range map[string]string{"": "legacy bytes", "-wal": "legacy WAL", "-shm": "legacy SHM"} { |
| + b, err := os.ReadFile(filepath.Join(f.f.guard.DataDir, "instances", ".serving-parked", legacy.Authorization.ID, filepath.Base(f.db)+suffix)) |
| + if err != nil || string(b) != want { |
| + t.Fatalf("legacy %q %v", b, err) |
| + } |
| + } |
| +} |
| + |
| +func TestServingLegacyRootConsumedAfterExpiry(t *testing.T) { |
| + f, _ := newServingLegacyRootFixture(t, false) |
| + a, err := f.p.parkLegacyServingDatabase(t.Context(), f.conn, f.current, servingLegacyPredecessor{}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + f.tags(f.current.Stamp) |
| + if err = f.p.completeServingDatabasePark(t.Context(), f.conn, a.ID, f.current); err != nil { |
| + t.Fatal(err) |
| + } |
| + f.f.now = f.f.now.Add(7 * 24 * time.Hour) |
| + if err = f.p.ledger.withNamespace(t.Context(), func() error { return f.p.ledger.reapLocked(t.Context(), a.ID) }); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err = os.WriteFile(f.db, []byte("restored successor"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + _, err = hostclaim.Update(t.Context(), f.f.claims, f.f.read(), func(c *hostclaim.Claim) error { c.Inventory = nil; return nil }) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err = f.p.parkLegacyServingDatabase(t.Context(), f.conn, f.current, servingLegacyPredecessor{}); err == nil { |
| + t.Fatal("reopened legacy source") |
| + } |
| + b, err := os.ReadFile(f.db) |
| + if err != nil || string(b) != "restored successor" { |
| + t.Fatalf("source %q %v", b, err) |
| + } |
| +} |
| + |
| +func TestServingLegacyRootAdmittedPredecessor(t *testing.T) { |
| + f := newServingRootFixture(t, true) |
| + if err := os.Remove(f.db + ".gen"); err != nil { |
| + t.Fatal(err) |
| + } |
| + _, err := hostclaim.Update(t.Context(), f.f.claims, f.f.read(), func(c *hostclaim.Claim) error { c.Inventory = nil; return nil }) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + a, err := f.p.parkLegacyServingDatabase(t.Context(), f.conn, f.current, servingLegacyPredecessor{Session: f.prior.Session, Generation: f.prior.Generation}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + f.tags(f.current.Stamp) |
| + if err = f.p.completeServingDatabasePark(t.Context(), f.conn, a.ID, f.current); err != nil { |
| + t.Fatal(err) |
| + } |
| + r, err := f.p.ledger.read(a.ID) |
| + if err != nil || !r.Complete || r.LegacyEvidence.NoProcess != nil || len(r.Receipts) != 1 { |
| + t.Fatalf("record %+v %v", r, err) |
| + } |
| +} |
| + |
| +func TestServingLegacyRootContinuationNeedsStop(t *testing.T) { |
| + f, _ := newServingLegacyRootFixture(t, false) |
| + a, err := f.p.parkLegacyServingDatabase(t.Context(), f.conn, f.current, servingLegacyPredecessor{}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + original := f.current |
| + o := f.f.read() |
| + later, err := hostclaim.Update(t.Context(), f.f.claims, o, hostclaim.AdmitWriter(hostclaim.Admission{ObservedSession: o.Claim.Holder.Session, ObservedGeneration: o.Claim.Holder.Generation, Box: "box-1", Region: "ie", PredecessorRegion: "ie", Mode: "cold", NewGeneration: "G2"})) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + f.current, err = servingWriterIdentity(later, "box-1", "ie") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + f.install(later) |
| + if _, err = f.p.parkLegacyServingDatabase(t.Context(), f.conn, f.current, servingLegacyPredecessor{}); err == nil { |
| + t.Fatal("continued without stop") |
| + } |
| + r, err := f.p.ledger.read(a.ID) |
| + if err != nil || r.activeAuthorization().Successor != original || len(r.Continuations) != 0 { |
| + t.Fatalf("changed record %+v %v", r, err) |
| + } |
| +} |
| + |
| +func TestServingLegacyRootEmptyContinuationAfterConsumption(t *testing.T) { |
| + f, _ := newServingLegacyRootFixture(t, true) |
| + original := f.current |
| + f.p.ledger.state = &servingParkFaultStore{Store: f.disk, failAt: 1} |
| + if _, err := f.p.parkLegacyServingDatabase(t.Context(), f.conn, f.current, servingLegacyPredecessor{Empty: true}); err == nil { |
| + t.Fatal("consumption fault missed") |
| + } |
| + f.p.ledger.state = f.disk |
| + servingStop(t, f.f, f.f.read()) |
| + o := f.f.read() |
| + later, err := hostclaim.Update(t.Context(), f.f.claims, o, hostclaim.AdmitWriter(hostclaim.Admission{ObservedSession: o.Claim.Holder.Session, ObservedGeneration: o.Claim.Holder.Generation, Box: "box-1", Region: "ie", PredecessorRegion: "ie", Mode: "cold", NewGeneration: "G2"})) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + f.current, err = servingWriterIdentity(later, "box-1", "ie") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + f.install(later) |
| + a, err := f.p.parkLegacyServingDatabase(t.Context(), f.conn, f.current, servingLegacyPredecessor{}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if a.OriginalSuccessor != original { |
| + t.Fatal("lost first-creator identity") |
| + } |
| + f.tags(f.current.Stamp) |
| + if err = f.p.completeServingDatabasePark(t.Context(), f.conn, a.ID, f.current); err != nil { |
| + t.Fatal(err) |
| + } |
| + slot, err := f.p.ledger.slot(a) |
| + if err != nil || slot.ID != "" { |
| + t.Fatalf("empty bootstrap took retained slot: %+v %v", slot, err) |
| + } |
| +} |
| + |
| +type servingEmptyRaceReader struct { |
| + genselect.Reader |
| + during func() |
| +} |
| + |
| +func (r servingEmptyRaceReader) ListVersions(ctx context.Context, bucket, prefix string) ([]lease.ObjectVersion, error) { |
| + versions, err := r.Reader.ListVersions(ctx, bucket, prefix) |
| + if r.during != nil { |
| + r.during() |
| + } |
| + return versions, err |
| +} |
| +func TestServingLegacyRootEmptyProofClaimRace(t *testing.T) { |
| + f, _ := newServingLegacyRootFixture(t, true) |
| + f.p.empty.Data = servingEmptyRaceReader{Reader: f.p.empty.Data, during: func() { |
| + _, err := hostclaim.Update(t.Context(), f.f.claims, f.f.read(), func(c *hostclaim.Claim) error { return nil }) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + }} |
| + before, _ := f.disk.Names() |
| + if _, err := f.p.parkLegacyServingDatabase(t.Context(), f.conn, f.current, servingLegacyPredecessor{Empty: true}); err == nil { |
| + t.Fatal("stale first-creator proof authorized capture") |
| + } |
| + after, _ := f.disk.Names() |
| + if !reflect.DeepEqual(before, after) { |
| + t.Fatalf("stale proof wrote records: %v", after) |
| + } |
| +} |
| + |
| +func TestServingLegacyRootNoProcessRetryKeepsOriginalEvidence(t *testing.T) { |
| + for _, changedIdentity := range []bool{false, true} { |
| + t.Run(fmt.Sprint(changedIdentity), func(t *testing.T) { |
| + newServingLegacyRootFixture(t, false, func(f *servingRootFixture, c *ClaimStopCoordinator, req StopRequest) { |
| + o := f.f.read() |
| + a := servingArtifactIdentity{Host: o.Host, Box: req.Box, Region: req.Region, Incarnation: req.Incarnation} |
| + old, err := f.p.ledger.legacyStop(a) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + reserve := hostclaim.ReserveWakeRetryAttempt(hostclaim.WakeRetryReservation{HolderIncarnation: req.Incarnation, EpisodeID: "first-episode", Attempt: hostclaim.WakeRetryAttempt{ID: "first-attempt", Box: req.Box, Enrollment: req.Enrollment, BootID: req.BootID, ControllerID: req.ControllerID, InvocationID: "invocation", LaunchInputs: "digest"}, Now: f.f.now}) |
| + next, err := hostclaim.Update(t.Context(), f.f.claims, o, func(claim *hostclaim.Claim) error { |
| + if err := reserve(claim); err != nil { |
| + return err |
| + } |
| + if changedIdentity { |
| + claim.Holder.Location += "/" + claim.Holder.Incarnation |
| + } |
| + return nil |
| + }) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + f.f.peer.BootID, f.f.peer.ControllerID = "next-boot", "next-controller" |
| + req.ExpectedClaimETag, req.BootID, req.ControllerID = next.ETag, f.f.peer.BootID, f.f.peer.ControllerID |
| + _, err = c.FenceAndStop(t.Context(), req) |
| + if (err != nil) != changedIdentity { |
| + t.Fatalf("repeated no-process stop: %v (changed identity %t)", err, changedIdentity) |
| + } |
| + after, err := f.p.ledger.legacyStop(a) |
| + if err != nil || !reflect.DeepEqual(after, old) { |
| + t.Fatal("replaced original no-process evidence") |
| + } |
| + }) |
| + }) |
| + } |
| +} |
diff --git a/internal/host/claimservingpark_records.go b/internal/host/claimservingpark_records.go| index aa451e1c5..caf2ad260 100644 |
| --- a/internal/host/claimservingpark_records.go |
| +++ b/internal/host/claimservingpark_records.go |
| @@ -9,6 +9,7 @@ import ( |
| "errors" |
| "io" |
| "os" |
| + "reflect" |
| "time" |
| "github.com/carlosframework/platform/internal/hostbox" |
| @@ -41,6 +42,7 @@ type servingParkSource struct { |
| } |
| type servingParkRecord struct { |
| + LegacyEvidence *servingLegacyEvidence `json:",omitempty"` |
| Continuations []servingParkContinuation `json:",omitempty"` |
| Version int |
| Authorization servingParkAuthorization |
| @@ -66,8 +68,10 @@ func (r servingParkRecord) activeAuthorization() servingParkAuthorization { |
| } |
| type servingLegacyConsumption struct { |
| - Authorization servingParkAuthorization |
| - Source servingParkSource |
| + LegacyEvidence *servingLegacyEvidence `json:",omitempty"` |
| + Receipts []hostguard.FenceReceipt `json:",omitempty"` |
| + Authorization servingParkAuthorization |
| + Source servingParkSource |
| } |
| type servingParkSlot struct { |
| @@ -143,7 +147,11 @@ func validateServingParkRecord(r servingParkRecord) error { |
| return hostbox.ErrChanged |
| } |
| } |
| - if len(r.Receipts) == 0 { |
| + if r.LegacyEvidence != nil && validateServingLegacyEvidence(a, r.LegacyEvidence) != nil { |
| + return hostbox.ErrChanged |
| + } |
| + noProcess := a.Legacy && r.LegacyEvidence != nil && r.LegacyEvidence.NoProcess != nil |
| + if (len(r.Receipts) == 0) != noProcess { |
| return hostbox.ErrChanged |
| } |
| seen := make(map[string]bool) |
| @@ -152,6 +160,9 @@ func validateServingParkRecord(r servingParkRecord) error { |
| if t.Host != s.Host || t.Box != s.Box || t.Region != s.Region || t.Incarnation != s.Incarnation || t.Stamp == 0 || t.Stamp >= s.Stamp || t.Session >= s.Session || t.Op != "" || t.Phase != "" || t.ObligationID != "" || t.FenceID == "" || seen[t.FenceID] || verifyRetirementReceipt(context.Background(), receipt) != nil { |
| return hostbox.ErrChanged |
| } |
| + if a.Legacy && r.LegacyEvidence != nil && t.Session != r.LegacyEvidence.Selector.Session { |
| + return hostbox.ErrChanged |
| + } |
| if !a.Legacy && (t.Stamp != a.Predecessor.Stamp || t.Session != a.Predecessor.Session) { |
| return hostbox.ErrChanged |
| } |
| @@ -301,6 +312,10 @@ func (l *servingParkLedger) withNamespace(ctx context.Context, fn func() error) |
| // parkLocked runs inside independently verified root host authority. Fresh |
| // capture is never substituted for the immutable source on a replay. |
| func (l *servingParkLedger) parkLocked(ctx context.Context, a servingParkAuthorization, receipts []hostguard.FenceReceipt) (servingParkRecord, error) { |
| + return l.parkWithEvidenceLocked(ctx, a, receipts, nil) |
| +} |
| + |
| +func (l *servingParkLedger) parkWithEvidenceLocked(ctx context.Context, a servingParkAuthorization, receipts []hostguard.FenceReceipt, legacy *servingLegacyEvidence) (servingParkRecord, error) { |
| var zero servingParkRecord |
| if err := ctx.Err(); err != nil { |
| return zero, err |
| @@ -342,7 +357,14 @@ func (l *servingParkLedger) parkLocked(ctx context.Context, a servingParkAuthori |
| return zero, e |
| } |
| } |
| - r = servingParkRecord{Version: 1, Authorization: a, Source: source, Receipts: receipts, Disposition: disposition} |
| + r = servingParkRecord{Version: 1, Authorization: a, Source: source, Receipts: receipts, LegacyEvidence: legacy, Disposition: disposition} |
| + if legacy != nil && legacy.Selector.Empty { |
| + for _, f := range source.Files { |
| + if f.Present { |
| + return zero, hostbox.ErrChanged |
| + } |
| + } |
| + } |
| if e = validateServingParkRecord(r); e != nil { |
| return zero, e |
| } |
| @@ -350,10 +372,10 @@ func (l *servingParkLedger) parkLocked(ctx context.Context, a servingParkAuthori |
| if a.Legacy { |
| key := "legacy-" + servingParkKey(a.Successor.Box, a.Successor.Host+"\x00"+a.Successor.Incarnation) + ".json" |
| var consumed servingLegacyConsumption |
| - want := servingLegacyConsumption{Authorization: a, Source: source} |
| + want := servingLegacyConsumption{Authorization: a, Source: source, Receipts: receipts, LegacyEvidence: legacy} |
| body, e := l.state.Read(key) |
| if e == nil { |
| - if servingParkDecode(body, &consumed) != nil || consumed != want { |
| + if servingParkDecode(body, &consumed) != nil || !reflect.DeepEqual(consumed, want) { |
| return zero, hostbox.ErrChanged |
| } |
| } else if !errors.Is(e, os.ErrNotExist) { |
| @@ -369,7 +391,7 @@ func (l *servingParkLedger) parkLocked(ctx context.Context, a servingParkAuthori |
| } else if err != nil { |
| return zero, err |
| } |
| - if r.Authorization != a { |
| + if r.Authorization != a || !sameServingLegacyEvidence(r.LegacyEvidence, legacy) || !sameServingRootStops(r.Receipts, receipts) { |
| return zero, hostbox.ErrChanged |
| } |
| if r.Closed || r.Complete { |
| @@ -606,3 +628,15 @@ func (l *servingParkLedger) replaceLocked(ctx context.Context, id string) error |
| } |
| return l.deleteLocked(ctx, &old, cause) |
| } |
| + |
| +func sameServingRootStops(a, b []hostguard.FenceReceipt) bool { |
| + if len(a) != len(b) { |
| + return false |
| + } |
| + for i := range a { |
| + if a[i].Target != b[i].Target || !a[i].EvidenceAt.Equal(b[i].EvidenceAt) || !bytes.Equal(a[i].Bytes, b[i].Bytes) { |
| + return false |
| + } |
| + } |
| + return true |
| +} |
diff --git a/internal/host/claimservingparkroot_linux.go b/internal/host/claimservingparkroot_linux.go| index 35dd848a5..87acf5c7c 100644 |
| --- a/internal/host/claimservingparkroot_linux.go |
| +++ b/internal/host/claimservingparkroot_linux.go |
| @@ -3,7 +3,6 @@ |
| package host |
| import ( |
| - "bytes" |
| "context" |
| "net" |
| "path/filepath" |
| @@ -17,12 +16,13 @@ import ( |
| ) |
| // servingParkRoot composes local root authority with the filesystem ledger. |
| -// Production construction remains disabled until legacy authorization, journal |
| -// transport and startup recovery are implemented. Requests supply identities |
| +// Production construction remains disabled until journal transport and startup |
| +// recovery are implemented. Requests supply identities |
| // only; installation paths and receipts are read under root's host lock. |
| type servingParkRoot struct { |
| root ClaimRootHTTP |
| ledger *servingParkLedger |
| + empty *claimServingOwners |
| } |
| func (p *servingParkRoot) withAuthority(ctx context.Context, conn net.Conn, current servingArtifactIdentity, fn func(hostclaim.Observed, hostguard.FenceView, hostbox.Actor, servingArtifactDirectory, uint64) error) error { |
| @@ -120,20 +120,9 @@ func servingRootStops(ctx context.Context, view hostguard.FenceView, prior servi |
| return out, nil |
| } |
| -func sameServingRootStops(a, b []hostguard.FenceReceipt) bool { |
| - if len(a) != len(b) { |
| - return false |
| - } |
| - for i := range a { |
| - if a[i].Target != b[i].Target || !a[i].EvidenceAt.Equal(b[i].EvidenceAt) || !bytes.Equal(a[i].Bytes, b[i].Bytes) { |
| - return false |
| - } |
| - } |
| - return true |
| -} |
| - |
| func (p *servingParkRoot) parkServingDatabase(ctx context.Context, conn net.Conn, current, requestedPrior servingArtifactIdentity) (servingParkAuthorization, error) { |
| var out servingParkAuthorization |
| + resumeLegacy := false |
| err := p.withAuthority(ctx, conn, current, func(o hostclaim.Observed, view hostguard.FenceView, actor hostbox.Actor, dir servingArtifactDirectory, tags uint64) error { |
| var a servingParkAuthorization |
| var saved *servingParkRecord |
| @@ -153,7 +142,17 @@ func (p *servingParkRoot) parkServingDatabase(ctx context.Context, conn net.Conn |
| return err |
| } |
| a, saved = r.Authorization, &r |
| - if a.Legacy || a.Directory != dir { |
| + if a.Legacy { |
| + if requestedPrior != (servingArtifactIdentity{}) { |
| + prior, e := servingPredecessor(o.Claim, current, tags, requestedPrior.Generation) |
| + if e != nil || prior != requestedPrior { |
| + return hostbox.ErrChanged |
| + } |
| + } |
| + resumeLegacy = true |
| + return nil |
| + } |
| + if a.Directory != dir { |
| return hostbox.ErrChanged |
| } |
| active := r.activeAuthorization() |
| @@ -222,6 +221,9 @@ func (p *servingParkRoot) parkServingDatabase(ctx context.Context, conn net.Conn |
| out = r.activeAuthorization() |
| return nil |
| }) |
| + if err == nil && resumeLegacy { |
| + return p.parkLegacyServingDatabase(ctx, conn, current, servingLegacyPredecessor{}) |
| + } |
| return out, err |
| } |
| @@ -235,9 +237,18 @@ func (p *servingParkRoot) completeServingDatabasePark(ctx context.Context, conn |
| if err = validateServingRootHistory(ctx, o, view, actor, r); err != nil { |
| return err |
| } |
| - if a.Legacy || a.Successor != current || a.Directory != dir || tags != current.Stamp { |
| + if a.Successor != current || a.Directory != dir || tags != current.Stamp { |
| return hostbox.ErrChanged |
| } |
| + if a.Legacy { |
| + if err = p.validateLegacyRootEvidence(ctx, o, view, actor, r.Authorization, r.LegacyEvidence, r.Receipts); err != nil { |
| + return err |
| + } |
| + if err = p.ledger.legacyUnlaunched(ctx, current, actor.Enrollment); err != nil { |
| + return err |
| + } |
| + return p.ledger.completeLocked(ctx, id, current) |
| + } |
| prior, err := servingPredecessor(o.Claim, current, a.Predecessor.Stamp, a.Predecessor.Generation) |
| if err != nil || prior != a.Predecessor { |
| return hostbox.ErrChanged |
diff --git a/internal/host/claimservingparkroot_linux_test.go b/internal/host/claimservingparkroot_linux_test.go| index 1ac5b5b0c..479e845e8 100644 |
| --- a/internal/host/claimservingparkroot_linux_test.go |
| +++ b/internal/host/claimservingparkroot_linux_test.go |
| @@ -336,8 +336,8 @@ type servingRootClientFixture struct{ f *servingRootFixture } |
| func (c servingRootClientFixture) Park(ctx context.Context, current, prior servingArtifactIdentity) (servingParkAuthorization, error) { |
| return c.f.p.parkServingDatabase(ctx, c.f.conn, current, prior) |
| } |
| -func (c servingRootClientFixture) ParkLegacy(context.Context, servingArtifactIdentity, servingLegacyPredecessor) (servingParkAuthorization, error) { |
| - return servingParkAuthorization{}, hostbox.ErrUnsupported |
| +func (c servingRootClientFixture) ParkLegacy(ctx context.Context, current servingArtifactIdentity, prior servingLegacyPredecessor) (servingParkAuthorization, error) { |
| + return c.f.p.parkLegacyServingDatabase(ctx, c.f.conn, current, prior) |
| } |
| func (c servingRootClientFixture) Complete(ctx context.Context, id string, current servingArtifactIdentity) error { |
| return c.f.p.completeServingDatabasePark(ctx, c.f.conn, id, current) |
diff --git a/internal/host/claimstop.go b/internal/host/claimstop.go| index 3cb91f74d..d04ba5fe7 100644 |
| --- a/internal/host/claimstop.go |
| +++ b/internal/host/claimstop.go |
| @@ -128,18 +128,19 @@ type StopResult struct { |
| } |
| type ClaimStopCoordinator struct { |
| - EvidenceSigner ed25519.PrivateKey |
| - RequestLimit time.Duration |
| - Guard hostguard.Guard |
| - Claims StopClaimReader |
| - Peers StopPeerProvider |
| - Inventory StopInventoryReader |
| - Drainer StopDrainer |
| - Kernel StopKernelVerifier |
| - Evidence release.Store |
| - CarlosUID uint32 |
| - NewID func() string |
| - Now func() time.Time |
| + RetainNoProcess func(context.Context, hostclaim.Observed, StopResult) error |
| + EvidenceSigner ed25519.PrivateKey |
| + RequestLimit time.Duration |
| + Guard hostguard.Guard |
| + Claims StopClaimReader |
| + Peers StopPeerProvider |
| + Inventory StopInventoryReader |
| + Drainer StopDrainer |
| + Kernel StopKernelVerifier |
| + Evidence release.Store |
| + CarlosUID uint32 |
| + NewID func() string |
| + Now func() time.Time |
| } |
| func stopComponent(s string) bool { |
| @@ -383,7 +384,7 @@ func (c ClaimStopCoordinator) witnessNoProcess(ctx context.Context, req StopRequ |
| return StopResult{}, ErrClaimStopIdentity |
| } |
| claim, holder := observed.Claim, observed.Claim.Holder |
| - if holder.Incarnation != req.Incarnation || holder.Stamp != req.InstallationStamp || holder.Session != 0 || holder.WriterBox != "" || holder.Generation != "" || len(holder.Admissions) != 0 { |
| + if holder.Incarnation != req.Incarnation || holder.Stamp != req.InstallationStamp || holder.Session != 0 || (holder.WriterBox != "" && (c.RetainNoProcess == nil || holder.WriterBox != req.Box || claim.Op.Kind != hostclaim.KindServing)) || holder.Generation != "" || len(holder.Admissions) != 0 { |
| return StopResult{}, ErrClaimStopIdentity |
| } |
| target := hostguard.FenceTarget{Region: req.Region, ClaimGen: claim.Gen, ObligationID: req.ObligationID, Host: req.Host, Incarnation: req.Incarnation, Stamp: req.InstallationStamp, |
| @@ -431,6 +432,11 @@ func (c ClaimStopCoordinator) witnessNoProcess(ctx context.Context, req StopRequ |
| } |
| sum := sha256.Sum256(body) |
| result := StopResult{Target: target, Key: stopEvidenceKey(req.Host, req.OperationID, req.Box, receiptID), Digest: hex.EncodeToString(sum[:]), Bytes: body, EvidenceAt: evidenceAt} |
| + if c.RetainNoProcess != nil && claim.Op.Kind == hostclaim.KindServing { |
| + if err := c.RetainNoProcess(ctx, observed, result); err != nil { |
| + return StopResult{}, err |
| + } |
| + } |
| if err := c.Publish(ctx, result); err != nil { |
| return result, err |
| } |
diff --git a/internal/host/claimstop_noprocess_test.go b/internal/host/claimstop_noprocess_test.go| index 41364286a..18547ce04 100644 |
| --- a/internal/host/claimstop_noprocess_test.go |
| +++ b/internal/host/claimstop_noprocess_test.go |
| @@ -4,6 +4,7 @@ import ( |
| "context" |
| "encoding/json" |
| "errors" |
| + "fmt" |
| "strings" |
| "testing" |
| @@ -116,8 +117,11 @@ func TestNoProcessWitnessRefuses(t *testing.T) { |
| "a writer was admitted": {func(_ *ClaimStopCoordinator, _ *StopRequest, _ *noProcessKernelFake, cl *changingClaims) { |
| cl.obs.Claim.Holder.Admissions = []hostclaim.AdmissionRecord{{Box: "box-1", Session: 1}} |
| }, ErrClaimStopIdentity}, |
| - "a writer box is named": {func(_ *ClaimStopCoordinator, _ *StopRequest, _ *noProcessKernelFake, cl *changingClaims) { |
| - cl.obs.Claim.Holder.WriterBox = "box-1" |
| + "a legacy holder without retention": {func(_ *ClaimStopCoordinator, r *StopRequest, _ *noProcessKernelFake, cl *changingClaims) { |
| + cl.obs.Claim.Holder.WriterBox = r.Box |
| + }, ErrClaimStopIdentity}, |
| + "a foreign writer box is named": {func(_ *ClaimStopCoordinator, _ *StopRequest, _ *noProcessKernelFake, cl *changingClaims) { |
| + cl.obs.Claim.Holder.WriterBox = "box-2" |
| }, ErrClaimStopIdentity}, |
| "another executor": {func(_ *ClaimStopCoordinator, _ *StopRequest, _ *noProcessKernelFake, cl *changingClaims) { |
| cl.obs.Claim.Op.Exec = "box-2" |
| @@ -205,3 +209,37 @@ func TestFailedSessionZeroAdmissionStopsNothing(t *testing.T) { |
| t.Fatalf("an admitted session was not stopped: %v, stops %d", err, stops) |
| } |
| } |
| + |
| +func TestNoProcessWitnessRetainsBeforePublication(t *testing.T) { |
| + for _, reject := range []bool{false, true} { |
| + t.Run(fmt.Sprint(reject), func(t *testing.T) { |
| + c, req, _, claims := noProcessFixture(t) |
| + claims.obs.Claim.Holder.WriterBox = req.Box |
| + calls := 0 |
| + c.RetainNoProcess = func(ctx context.Context, o hostclaim.Observed, result StopResult) error { |
| + calls++ |
| + if o.ETag != req.ExpectedClaimETag || o.Claim.Holder.Session != 0 || result.Target.FenceID != "" { |
| + t.Fatal("wrong retained observation") |
| + } |
| + keys, err := c.Evidence.List(ctx, "control/") |
| + if err != nil || len(keys) != 0 { |
| + t.Fatal("published before retention") |
| + } |
| + if reject { |
| + return hostbox.ErrChanged |
| + } |
| + return nil |
| + } |
| + _, err := c.FenceAndStop(t.Context(), req) |
| + if calls != 1 || (err != nil) != reject { |
| + t.Fatalf("retention %d %v", calls, err) |
| + } |
| + if reject { |
| + keys, _ := c.Evidence.List(t.Context(), "control/") |
| + if len(keys) != 0 { |
| + t.Fatal("published despite failed retention") |
| + } |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/provision/testdata/plan14-root-reads-seed.tsv b/internal/provision/testdata/plan14-root-reads-seed.tsv| index 70e5eb676..ec2dd7d86 100644 |
| --- a/internal/provision/testdata/plan14-root-reads-seed.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-seed.tsv |
| @@ -1,5 +1,5 @@ |
| -# Files seed checkpoint 4 overlay; pending Paul’s census ruling. |
| -# Root generation authorization and continuation have test callers only; production binding is refused. |
| +# Files seed checkpoint 5 overlay; pending Paul’s census ruling. |
| +# Generation, legacy and empty-bootstrap root authority have test callers only; production binding remains refused. |
| # Load after zddr4 and its explicit seal check. No legacy fingerprints are added or removed. |
| # Every classification below is rev3=PROPOSED-seed, not an approved disposition. |
| internal/host/claimconstruction.go OpenProductionClaimAssembly authority C,G rev3=PROPOSED-seed; box process identity and provider bindings; serving dispatch independent of moves; serving enrollment required before a runtime can be built file:Read@4150,file:Read@9550,file:ReadFile@3694,registry:Host@9577,whole:Route@9665 088f34c7fe1b4f9bb7a3187d40d60bff0fd811ebf1e1888ce3d4781dc4de80d7 |
| @@ -56,7 +56,7 @@ internal/host/claimservingpark_records.go servingParkDecode authority C,J4 rev3= |
| internal/host/claimservingpark_records.go servingParkKey authority C,J4 rev3=PROPOSED-seed; box/host identity used for stable per-host namespace and slot keys 67518302550380dbf9b741a73758e5c94307eff70ff8a09cc56763a8aa794d86 |
| internal/host/claimservingpark_records.go servingParkLedger.completeLocked authority C,J4 rev3=PROPOSED-seed; root retirement and pending-slot records plus fresh filesystem absence/identity checks; completion requires outer root claim-tag authority which remains unbound 00f9c10b8130c134a41dea74848aa1651ae3331c8440a81e1b3f59f54f6a7218 |
| internal/host/claimservingpark_records.go servingParkLedger.deleteLocked authority C,J4 rev3=PROPOSED-seed; complete retained record or proved discard disposition and persisted cause/progress before filesystem deletion 1545950c564d51d9c29b0d76daca1ddec5a159481698b11accd4dd7411157b37 |
| -internal/host/claimservingpark_records.go servingParkLedger.parkLocked authority C,J4 rev3=PROPOSED-seed; immutable source capture, root-local launch journal, exact stop bytes, legacy consumption, pending work and incumbent slot; only started databases replace slots; outer root authorization remains unbound file:Prepare@3247,file:Read@1693 c46ddfb8aaf8339eb6d5dc414724ead938338552477825efe6d1e6f677897b32 |
| +internal/host/claimservingpark_records.go servingParkLedger.parkLocked authority C,J4 rev3=PROPOSED-seed; generation or test-fixture legacy entry delegates to immutable evidence-aware ledger; no independent source authority 1f16bea822e6c63df15c8259c8972a4084cdf364769f10396f396424e0fe848c |
| internal/host/claimservingpark_records.go servingParkLedger.read authority C,J4 rev3=PROPOSED-seed; private retirement record by validated deterministic ID and strict record invariants file:Read@249 868c8c3f5682dc36738d6960d57f2f1fcc6512374c8f8e0790187bf69cf3de64 |
| internal/host/claimservingpark_records.go servingParkLedger.reapLocked authority C,J4 rev3=PROPOSED-seed; root completion, retention disposition and fixed seven-day deadline; rollback or pending work refuses expiry aec4f50b45d1d955fac9814fe9e0d403f5f3f7f939c71f8f6b8ac15e9173b846 |
| internal/host/claimservingpark_records.go servingParkLedger.save authority C,J4 rev3=PROPOSED-seed; validated private retirement record before durable rootstate write b6f940b1e21cf1beb67638effea958d2b1119695494a562e22bbbe0469d8af49 |
| @@ -64,18 +64,32 @@ internal/host/claimservingpark_records.go servingParkLedger.slot authority C,J4 |
| internal/host/claimservingpark_records.go servingParkLedger.withNamespace authority C,J4 rev3=PROPOSED-seed; private rootstate namespace lock and cancellation before invoking root authority; composed with root host authority in generation tests; production remains unbound b7cfae44fdb7de9ef8130f5581710ae3a1a526bb059b4f616650172490f5c8ac |
| internal/host/claimservingpark_records.go servingParkLedger.write authority C,J4 rev3=PROPOSED-seed; serialized private record sent only to the supplied rootstate store 0635d2c27871e67079119a0f2e3034e817b765d0fe3a47e7d9bdc66fb2779571 |
| internal/host/claimservingpark_records.go type servingParkFilesystem authority C,J4 rev3=PROPOSED-seed; private filesystem effect interface over root-recorded retirement identities; no wire or public request fields file:Prepare@180 b93f0e2af16b66aee1ff71294e215eeba9e661fe696c917c982d9ebc2dc682c7 |
| -internal/host/claimservingpark_records.go validateServingParkRecord authority C,J4 rev3=PROPOSED-seed; version, immutable tuples, exact serving receipt bytes, source identities, per-file progress, disposition, completion and retention deadline invariants; bounded immutable continuation history and exact predecessor stops db48025925e4b5acaf3231fdbf4d19900f406017b402efdf5ba1cb2550d00bc6 |
| +internal/host/claimservingpark_records.go validateServingParkRecord authority C,J4 rev3=PROPOSED-seed; version, immutable tuples, exact serving receipt bytes, source identities, per-file progress, disposition, completion and retention deadline invariants; bounded immutable continuation history and exact predecessor stops 7164d44093a2bf9efd5d8798d8bf6674f5b399bca163d098b3f2c9b100e9ea87 |
| internal/host/claimservingparksource_linux.go servingParkStateDirectory authority C,J4 rev3=PROPOSED-seed; root installation/inventory snapshots and authoritative serving observation; exact account/app/record, revision/enrollment, canonical nested path, ownership and descriptor identity; root acquisition under host lock is exercised by generation tests; production remains unbound ec4a121330c5c8f27343f21af0bbe458cced885d4754e96c0d87b12d9b4a8f35 |
| internal/hostguard/servingretirement.go Guard.WithServingRetirement authority C,J4 rev3=PROPOSED-seed; fresh serving claim and root installation/fence snapshot under root and shared host locks; refuses pending, quarantined, foreign, retired or already stopped successors; permits an older completed predecessor 3d105c50be7e613777055f561321efadf77fe823d8dc3263a60f2c1211b71108 |
| internal/host/claimservingparkroot_linux.go servingParkRoot.withAuthority authority C,J4 rev3=PROPOSED-seed; independent active-edge peer authentication, census gate, roster, local root inventory and fresh claim recheck under namespace and root host authority; no registry path or caller receipt file:Read@2432 6d0c84a5a9c5403f166db4f200504a3ccd75ebcd7ddac1736e3ac046237a2882 |
| internal/host/claimservingparkroot_linux.go servingRootStops authority C,J4 rev3=PROPOSED-seed; root fence snapshot and known installation stamps; exact local serving receipt bytes, enrollment, revision and unique fence identities d16eb077adfb8f0f8c9e63d1609250dc3470ec424bdde63b57c47d09500b2bce |
| -internal/host/claimservingparkroot_linux.go sameServingRootStops authority C,J4 rev3=PROPOSED-seed; byte-exact comparison of persisted and freshly selected root receipt sets, including every target field and evidence time c9e0803635546eecc3a7d07760cb69fc915182f96b0d1581ed655de3382edd88 |
| -internal/host/claimservingparkroot_linux.go servingParkRoot.parkServingDatabase authority C,J4 rev3=PROPOSED-seed; fresh paired claim tags, root slot and retirement records, retained predecessor and successor admissions, root stop history and source identity before park or continuation 802f4b8f5a2e49c3d6d4b0530c8cc484d29abf0a25ae852d1e7f39509d0e986e |
| -internal/host/claimservingparkroot_linux.go servingParkRoot.completeServingDatabasePark authority C,J4 rev3=PROPOSED-seed; fresh paired enrollment, same current writer, root source identity and complete original/continuation stop history before durable completion f120d235529b61ce93167f4c364a962b7dc4197ba0611719547015182cba96e1 |
| +internal/host/claimservingparkroot_linux.go servingParkRoot.parkServingDatabase authority C,J4 rev3=PROPOSED-seed; fresh paired tags, root slot/record and retained admissions; recorded legacy work dispatches to separately locked fresh legacy authority 61c29d5b37168ab053499553eaf0052fcee134a601c86acb3195fad416d28247 |
| +internal/host/claimservingparkroot_linux.go servingParkRoot.completeServingDatabasePark authority C,J4 rev3=PROPOSED-seed; fresh paired enrollment, same current writer, root source and original/continuation stop history; legacy additionally authenticates persistent no-process or admitted predecessor evidence 8ca9bdd3c1209c6d5257bc07a7d12d0bab900e9c2911d0698077eb2116e5123c |
| internal/host/claimservingparkroot_linux.go validateServingRootHistory authority C,J4 rev3=PROPOSED-seed; retained local successor admissions and byte-exact root stops for every persisted continuation; rejects relabelled or missing history c85a1be96de5fa61acd48c4adf9393d8e837880f5133a2f965f1104037e02329 |
| internal/host/claimservingpark_records.go servingParkRecord.activeAuthorization authority C,J4 rev3=PROPOSED-seed; last persisted continuation authorization or immutable original authorization; no source or deadline changes fe168fe52317e45ce70c3e97a9bc2c52d2409328ab4844a8d2a34fc4babba36d |
| internal/host/claimservingpark_records.go servingParkLedger.continueLocked authority C,J4 rev3=PROPOSED-seed; namespace/host-protected slot and record locks; pending original retirement, absent source and retained parked identities before durable bounded continuation 87f297a924f7f153624304233218d8b5548a016fc7dbf0f7cb8dedb789324009 |
| internal/host/claimservingpark_records.go servingParkLedger.replaceLocked authority C,J4 rev3=PROPOSED-seed; completed incumbent record under its record lock; resumes its recorded deletion cause before slot replacement 3e67316e959c9a901c32a2a61fda4e6ea5e92fe51bdb7fdb5f5c53d9d5960aad |
| -@coverage internal/host 9900e8ad9fe9d49c4359c0d7d11ff5a804a5b7b54328a50bcf6c3dfde91f771e |
| +@coverage internal/host 948bdc0ad5d699a917ce834c2e4f48c44486d72a5c689c5e9da9b748c88fe486 |
| @coverage internal/hostbox 72c9b640d98c779a044579f70a103a68cd9819c794a0bb3e53f378d39ad11419 |
| @coverage internal/hostguard 94912485e1e5fad4bad56985dd48ede776cfcb5fe18e0f4a59953584ddab06c0 |
| +internal/host/claimservinglegacy.go servingLegacyKey authority C,J4 rev3=PROPOSED-seed; box, host and incarnation derive private stop and consumption keys; never a caller path 93c10e40202ab23986b54a366edd7abfab9f8d942736305b5732e8741e96f3ab |
| +internal/host/claimservinglegacy.go validateServingLegacyStop authority C,J4 rev3=PROPOSED-seed; original session-zero serving observation and exact root no-process bytes; claim ETag, receipt digest, kernel evidence and empty fence identity file:Exec@413 88bf4d578819b8d3f62b80e2068a4c574a1aaa8118a8461bbeee99569e035ed0 |
| +internal/host/claimservinglegacy.go servingParkLedger.legacyStop authority C,J4 rev3=PROPOSED-seed; strict private retained no-process record and exact host, box, region and incarnation; no bucket or caller evidence file:Read@131 3cc2122431a095406bcd11a1a6ace825a870f5f8d0b8053d2b27486084838c4f |
| +internal/host/claimservinglegacy.go servingParkLedger.legacyConsumption authority C,J4 rev3=PROPOSED-seed; private persistent consumption record for the box, host and incarnation; absence differs from malformed or unavailable state file:Read@121 f8acefe9ac0f00640b68e7ad51d02b6544fb06c4130863d9159f8e2800281ad8 |
| +internal/host/claimservinglegacy.go sameServingLegacyEvidence authority C,J4 rev3=PROPOSED-seed; exact immutable legacy selector and retained no-process observation comparison 806588dfe151a558273222fcd77c6f780a72e293f312531ab7ba9fcde7a9e46f |
| +internal/host/claimservinglegacy.go validateServingLegacyEvidence authority C,J4 rev3=PROPOSED-seed; original successor, legacy selector and original session-zero holder; first-creator status is not a request assertion 485ab1232cc0817ad52a638bb220fe532c5a16086379e689b1c27dea6948f6b5 |
| +internal/host/claimservinglegacy.go servingParkLedger.legacyUnlaunched authority C,J4 rev3=PROPOSED-seed; local root launch history for current successor; any main-role intention refuses legacy source capture and completion 1963414899b4ce2bb059950bc6cfdad4e8c5ea0a575d2e19f87c91a4cf82c8ad |
| +internal/host/claimservinglegacyroot_linux.go servingParkRoot.retainLegacyNoProcess authority C,J4 rev3=PROPOSED-seed; root coordinator result before publication; namespace and host guards, census, roster, fresh original claim and immutable retained stop; repeated preadmission witnesses tolerate only Gen, ETag and retry metadata changes file:Read@1210 5460530ae4367720515089e9fda3105aa1032b8282f4923bbf7f8af9600f4f63 |
| +internal/host/claimservinglegacyroot_linux.go servingParkRoot.validateLegacyRootEvidence authority C,J4 rev3=PROPOSED-seed; retained original observation, first cold admission and writes-lost identity or exact admitted-predecessor root stops; enrollment and revision are rechecked 8ebc1e7841b8b8cfabc8e4f3520ef13d40130e68aa7ded608b829063e3d3f89a |
| +internal/host/claimservinglegacyroot_linux.go servingParkRoot.parkLegacyServingDatabase authority C,J4 rev3=PROPOSED-seed; current root authority, persistent consumption, immutable original stop/source and continuation stops; production first-creator empty listing and final claim recheck precede source capture file:Read@3463 4a883d8cc15d58a9a9572d08790173773f35afd46b09caaf9ac99c9ec3a39d1c |
| +internal/host/claimservinglegacyroot_linux.go sameServingLegacySource authority C,J4 rev3=PROPOSED-seed; immutable directory and artifact identities from consumed source and retirement; mutable rename/delete progress is excluded 925e4f20aee042895a83ea3787f4482ae732b6c6bcd45d9d10226ff87bd37091 |
| +internal/host/claimservingpark_records.go servingParkLedger.parkWithEvidenceLocked authority C,J4 rev3=PROPOSED-seed; root source identities, exact stop bytes, launch disposition and optional session-zero evidence; persistent consumption precedes intent/rename, empty creators refuse all local artifacts file:Prepare@3632,file:Read@1957 257d2c2674dd3a781f6b1ec842d65128a8e9f3d03b5468cbf6f487c37b45e94f |
| +internal/host/claimservingpark_records.go sameServingRootStops authority C,J4 rev3=PROPOSED-seed; byte-identical root stop sets including targets, evidence timestamps and original receipt bytes c9e0803635546eecc3a7d07760cb69fc915182f96b0d1581ed655de3382edd88 |
| +internal/host/claimstop.go ClaimStopCoordinator.witnessNoProcess authority C,G,J4 rev3=PROPOSED-seed; existing peer, claim, root inventory and positive kernel no-process checks; local adopted holder requires new retained-evidence binding; original observation is retained before receipt publication file:Exec@1845,file:Read@2541,file:Read@668 4a0df51297d2f6ed2e9c284ee11c26b73747bf84e1da5214d00f190eece5eb38 |
| +internal/host/claimstop.go type ClaimStopCoordinator authority C,G,J4 rev3=PROPOSED-seed; root-only coordinator dependency for retaining its own session-zero observation; nil preserves existing refusal for adopted legacy holders b7114aa7340cdba82fef42d62b784eae16da7acc48407ae6edba6453a4889f9a |