WIP files seed checkpoint 18 (not for merge)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9 files changed,
+558
−25
internal/host/claimservingcollection.go+62 −0internal/host/claimservingcollection_linux_test.go+347 −0internal/host/claimservingcollection_rootrunner_linux_test.go+99 −0internal/host/claimservinglegacy.go+15 −0internal/host/claimservinglegacyroot_linux.go+3 −0internal/host/claimservingpark_records.go+18 −0internal/host/claimservingpark_recovery.go+4 −2internal/host/claimservingpark_recovery_linux.go+0 −15internal/provision/testdata/plan14-root-reads-seed.tsv+10 −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 347.673s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 2.946s
ok github.com/carlosframework/platform/cmd/moveprobe 93.229s
ok github.com/carlosframework/platform/cmd/zddprobe 0.705s
ok github.com/carlosframework/platform/internal/activator 233.243s
ok github.com/carlosframework/platform/internal/agentsock 0.017s
ok github.com/carlosframework/platform/internal/alerting 0.003s
ok github.com/carlosframework/platform/internal/analytics 0.021s
ok github.com/carlosframework/platform/internal/appconfig 2.336s
ok github.com/carlosframework/platform/internal/apperr 0.072s
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.815s
? 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.012s
ok github.com/carlosframework/platform/internal/certwarm 0.003s
ok github.com/carlosframework/platform/internal/cgroup 0.702s
ok github.com/carlosframework/platform/internal/changelog 0.299s
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 365.110s
? 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.501s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.009s
ok github.com/carlosframework/platform/internal/dbverify 0.005s
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.124s
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 26.245s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.007s
ok github.com/carlosframework/platform/internal/fleetapi 0.121s
ok github.com/carlosframework/platform/internal/gate 1.654s
ok github.com/carlosframework/platform/internal/genselect 0.016s
ok github.com/carlosframework/platform/internal/geo 0.017s
ok github.com/carlosframework/platform/internal/geoip 0.029s
ok github.com/carlosframework/platform/internal/host 149.345s
ok github.com/carlosframework/platform/internal/hostactors 0.471s
ok github.com/carlosframework/platform/internal/hostbox 2.299s
ok github.com/carlosframework/platform/internal/hostclaim 0.544s
ok github.com/carlosframework/platform/internal/hostevidence 0.014s
ok github.com/carlosframework/platform/internal/hostexec 4.836s
ok github.com/carlosframework/platform/internal/hostfence 0.038s
ok github.com/carlosframework/platform/internal/hostfiles 10.458s
ok github.com/carlosframework/platform/internal/hostfloor 0.014s
ok github.com/carlosframework/platform/internal/hostguard 0.976s
ok github.com/carlosframework/platform/internal/hostops 1.462s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.014s
ok github.com/carlosframework/platform/internal/hostquarantine 0.014s
ok github.com/carlosframework/platform/internal/hostrelocate 0.479s
ok github.com/carlosframework/platform/internal/hostresolve 0.146s
ok github.com/carlosframework/platform/internal/hostwrite 0.015s
ok github.com/carlosframework/platform/internal/infraview 0.014s
ok github.com/carlosframework/platform/internal/instancectl 1.829s
ok github.com/carlosframework/platform/internal/instanceexec 2.758s
ok github.com/carlosframework/platform/internal/instancefs 0.014s
ok github.com/carlosframework/platform/internal/instancekey 0.014s
ok github.com/carlosframework/platform/internal/instanceupdates 0.024s
ok github.com/carlosframework/platform/internal/layout 1.473s
ok github.com/carlosframework/platform/internal/lease 9.453s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.020s
ok github.com/carlosframework/platform/internal/ledger 0.011s
ok github.com/carlosframework/platform/internal/mail 0.167s
ok github.com/carlosframework/platform/internal/maintenance 0.086s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.533s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.079s
ok github.com/carlosframework/platform/internal/metrics 0.005s
ok github.com/carlosframework/platform/internal/moves 21.000s
ok github.com/carlosframework/platform/internal/moves/archive 0.776s
ok github.com/carlosframework/platform/internal/objstore 0.199s
ok github.com/carlosframework/platform/internal/ops 0.176s
ok github.com/carlosframework/platform/internal/park 0.208s
ok github.com/carlosframework/platform/internal/privatefile 0.003s
ok github.com/carlosframework/platform/internal/provision 12.824s
ok github.com/carlosframework/platform/internal/publicrelease 0.006s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 4.036s
ok github.com/carlosframework/platform/internal/relay 7.233s
ok github.com/carlosframework/platform/internal/release 19.503s
ok github.com/carlosframework/platform/internal/rollout 3.747s
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.005s
ok github.com/carlosframework/platform/internal/safefile 0.004s
ok github.com/carlosframework/platform/internal/schedule 2.291s
ok github.com/carlosframework/platform/internal/sim/hostlife 9.617s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 4.573s
ok github.com/carlosframework/platform/internal/status 0.041s
ok github.com/carlosframework/platform/internal/steering 0.007s
ok github.com/carlosframework/platform/internal/stopevidence 0.289s
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.005s
ok github.com/carlosframework/platform/internal/version 0.001s
ok github.com/carlosframework/platform/internal/vet 1.582s
=== 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.024s
=== RUN TestT4StorageProbeMinio
2026/10/14 20:42:50 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE801E74929A02, 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.01s)
PASS
ok github.com/carlosframework/platform/internal/host 0.022s
minio contract: PASS
Intent
Human
<task-notification> <task-id>bqyvrnft8</task-id> <tool-use-id>toolu_01G2yNrXhnA7RsPH2QLggAtM</tool-use-id> <output-file>/home/paulca/.cache/tmp/claude-1001/-home-paulca-amadan-net-carlos-platform/6387394d-0b76-4de7-bbb4-d725fe24b08c/tasks/bqyvrnft8.output</output-file> <status>completed</status> <summary>Background command "Wait for seed build run 18" completed (exit code 0)</summary> </task-notification>
diff --git a/internal/host/claimservingcollection.go b/internal/host/claimservingcollection.go| new file mode 100644 |
| index 000000000..a2fe3bf7c |
| --- /dev/null |
| +++ b/internal/host/claimservingcollection.go |
| @@ -0,0 +1,62 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "os" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| +) |
| + |
| +// Called after identity-checked event removal with namespace, slot and record |
| +// locks held. Latest slot references remain replayable; legacy consumption is |
| +// separate authority and is never collected here. |
| +func (l *servingParkLedger) collectClosedLocked(ctx context.Context, r *servingParkRecord, slot servingParkSlot) error { |
| + if err := ctx.Err(); err != nil { |
| + return err |
| + } |
| + if !r.Complete || !r.Closed || !r.EventRemoved { |
| + return nil |
| + } |
| + if r.CollectAfter.IsZero() { |
| + // Old records start their window when first observed fully complete. |
| + // Clock rollback must not move the deadline backwards on a later pass. |
| + now := l.now().UTC() |
| + if now.Before(r.ParkedAt) { |
| + return nil |
| + } |
| + r.CollectAfter = now.Add(hostbox.JournalReceiptRetention) |
| + return l.save(*r) |
| + } |
| + if l.now().Before(r.CollectAfter) || slot.ID == r.Authorization.ID || slot.Pending == r.Authorization.ID || slot.Last == r.Authorization.ID { |
| + return nil |
| + } |
| + if slot.Last == "" { |
| + return hostbox.ErrChanged |
| + } |
| + if r.Authorization.Legacy { |
| + consumed, err := l.legacyConsumption(r.Authorization.Successor) |
| + if err != nil || consumed == nil || !sameServingLegacyConsumption(*consumed, *r) { |
| + return hostbox.ErrChanged |
| + } |
| + // Distinguish collection from a crash after consumption was recorded |
| + // but before the first retirement intent was saved. |
| + if !consumed.Collected { |
| + consumed.Collected = true |
| + if err := l.write(servingLegacyKey("legacy", r.Authorization.Successor), consumed); err != nil { |
| + return err |
| + } |
| + } |
| + } |
| + // Every record-lock user first takes namespace.lock. No waiter can hold |
| + // this lock's old inode while another process creates its replacement. |
| + // Remove it first: a lost reply leaves the retirement discoverable for a |
| + // later pass, rather than leaving an orphan lock after record collection. |
| + if err := l.state.Remove("retirement-" + r.Authorization.ID + ".lock"); err != nil && !errors.Is(err, os.ErrNotExist) { |
| + return err |
| + } |
| + if err := ctx.Err(); err != nil { |
| + return err |
| + } |
| + return l.state.Remove("retirement-" + r.Authorization.ID + ".json") |
| +} |
diff --git a/internal/host/claimservingcollection_linux_test.go b/internal/host/claimservingcollection_linux_test.go| new file mode 100644 |
| index 000000000..875b0bfef |
| --- /dev/null |
| +++ b/internal/host/claimservingcollection_linux_test.go |
| @@ -0,0 +1,347 @@ |
| +//go:build linux |
| + |
| +package host |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "os" |
| + "path/filepath" |
| + "reflect" |
| + "strings" |
| + "testing" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/provision" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| + "golang.org/x/sys/unix" |
| +) |
| + |
| +func servingCollectionFixture(t *testing.T, legacy bool) (*servingParkDiskFixture, servingParkAuthorization, servingParkAuthorization) { |
| + t.Helper() |
| + f := newServingParkDiskFixture(t) |
| + f.put("", "old served bytes") |
| + if !legacy { |
| + f.put(".gen", "G10") |
| + } |
| + old, receipts := f.evidence(10, legacy, "started") |
| + if _, err := f.park(old, receipts); err != nil { |
| + t.Fatal(err) |
| + } |
| + f.complete(old) |
| + f.put("", "new served bytes") |
| + f.put(".gen", "G11") |
| + next, receipts := f.evidence(11, false, "started") |
| + if _, err := f.park(next, receipts); err != nil { |
| + t.Fatal(err) |
| + } |
| + f.complete(next) |
| + return f, old, next |
| +} |
| + |
| +func TestServingParkCollectionWindowAndReplay(t *testing.T) { |
| + f, old, next := servingCollectionFixture(t, false) |
| + if err := f.ledger.recoverCompleted(t.Context(), old.ID); err != nil { |
| + t.Fatal(err) |
| + } |
| + r, err := f.ledger.read(old.ID) |
| + if err != nil || !r.CollectAfter.Equal(f.now.Add(hostbox.JournalReceiptRetention)) { |
| + t.Fatalf("missing retention deadline: %+v %v", r, err) |
| + } |
| + before, _ := f.disk.Read("retirement-" + old.ID + ".json") |
| + for _, now := range []time.Time{f.now.Add(-time.Hour), r.CollectAfter.Add(-time.Nanosecond)} { |
| + f.now = now |
| + if err = f.ledger.recoverCompleted(t.Context(), old.ID); err != nil { |
| + t.Fatal(err) |
| + } |
| + after, err := f.disk.Read("retirement-" + old.ID + ".json") |
| + if err != nil || string(before) != string(after) { |
| + t.Fatal("deadline refreshed or record collected early", err) |
| + } |
| + } |
| + // A restarted ledger uses only the persisted deadline. |
| + restarted := *f.ledger |
| + f.now = r.CollectAfter |
| + if err = f.disk.Write("retirement-"+old.ID+".lock", nil); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err = restarted.recoverCompleted(t.Context(), old.ID); err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, suffix := range []string{".json", ".lock"} { |
| + if _, err = f.disk.Read("retirement-" + old.ID + suffix); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatal("collected file survived", suffix, err) |
| + } |
| + } |
| + f.retained(next, "new served bytes", r.ReapAfter) |
| + // Even a fresh transport nonce must not rebuild an old collected intent |
| + // from the current source. Root additionally checks the live claim. |
| + f.put("", "current database") |
| + f.put(".gen", "G10") |
| + if _, err = f.park(old, r.Receipts); !errors.Is(err, hostbox.ErrChanged) { |
| + t.Fatal("collected replay regained authority", err) |
| + } |
| + if _, err = f.ledger.read(old.ID); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatal("replay recreated retirement", err) |
| + } |
| + b, err := os.ReadFile(provision.NestedDBPath(f.data, f.base.Host)) |
| + if err != nil || string(b) != "current database" { |
| + t.Fatal("refused replay changed live source", err) |
| + } |
| +} |
| + |
| +func TestServingParkCollectionPreservesSlotReferences(t *testing.T) { |
| + for _, reference := range []string{"ID", "Pending", "Last"} { |
| + t.Run(reference, func(t *testing.T) { |
| + f, old, _ := servingCollectionFixture(t, false) |
| + if err := f.ledger.recoverCompleted(t.Context(), old.ID); err != nil { |
| + t.Fatal(err) |
| + } |
| + r, _ := f.ledger.read(old.ID) |
| + f.now = r.CollectAfter.Add(time.Hour) |
| + slot, err := f.ledger.slot(old) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + switch reference { |
| + case "ID": |
| + slot.ID = old.ID |
| + case "Pending": |
| + slot.Pending = old.ID |
| + case "Last": |
| + slot.Last = old.ID |
| + } |
| + if err = f.ledger.withNamespace(t.Context(), func() error { return f.ledger.collectClosedLocked(t.Context(), &r, slot) }); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err = f.ledger.read(old.ID); err != nil { |
| + t.Fatal("collected a referenced retirement", err) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestServingParkCollectionKeepsLegacyConsumption(t *testing.T) { |
| + f, old, next := servingCollectionFixture(t, true) |
| + consumed, err := f.ledger.legacyConsumption(old.Successor) |
| + if err != nil || consumed == nil { |
| + t.Fatal(err) |
| + } |
| + if err = f.ledger.recoverCompleted(t.Context(), old.ID); err != nil { |
| + t.Fatal(err) |
| + } |
| + r, _ := f.ledger.read(old.ID) |
| + f.now = r.CollectAfter |
| + if err = f.ledger.recoverCompleted(t.Context(), old.ID); err != nil { |
| + t.Fatal(err) |
| + } |
| + after, err := f.ledger.legacyConsumption(old.Successor) |
| + if err != nil || after == nil || !after.Collected { |
| + t.Fatal("collection lost the permanent refusal", err) |
| + } |
| + after.Collected = false |
| + if !reflect.DeepEqual(after, consumed) { |
| + t.Fatal("collection changed legacy evidence") |
| + } |
| + f.retained(next, "new served bytes", r.ReapAfter) |
| +} |
| + |
| +func TestServingParkCollectionLegacyRootRefusesRecapture(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) |
| + } |
| + c, err := f.p.ledger.legacyConsumption(f.current) |
| + if err != nil || c == nil { |
| + t.Fatal(err) |
| + } |
| + c.Collected = true |
| + if err = f.p.ledger.write(servingLegacyKey("legacy", f.current), c); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err = f.disk.Remove("retirement-" + a.ID + ".json"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err = f.p.parkLegacyServingDatabase(t.Context(), f.conn, f.current, servingLegacyPredecessor{}); !errors.Is(err, hostbox.ErrChanged) { |
| + t.Fatal("root accepted collected legacy source", err) |
| + } |
| + if _, err = f.p.ledger.read(a.ID); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatal("root recreated collected record", err) |
| + } |
| +} |
| + |
| +func TestServingParkCollectionLegacyConsumptionBeforeIntentStillResumes(t *testing.T) { |
| + f, _ := newServingLegacyRootFixture(t, false) |
| + store := &servingCollectionFaultStore{Store: f.disk} |
| + f.p.ledger.state = store |
| + store.write = func(name string, body []byte) error { |
| + if strings.HasPrefix(name, "retirement-") { |
| + return unix.EIO |
| + } |
| + return f.disk.Write(name, body) |
| + } |
| + if _, err := f.p.parkLegacyServingDatabase(t.Context(), f.conn, f.current, servingLegacyPredecessor{}); !errors.Is(err, unix.EIO) { |
| + t.Fatal("intent fault was not reached", err) |
| + } |
| + c, err := f.p.ledger.legacyConsumption(f.current) |
| + if err != nil || c == nil || c.Collected { |
| + t.Fatal("pending consumption unavailable", err) |
| + } |
| + if _, err := f.p.ledger.read(c.Authorization.ID); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatal("intent was saved", err) |
| + } |
| + f.p.ledger.state = f.disk |
| + if _, err := f.p.parkLegacyServingDatabase(t.Context(), f.conn, f.current, servingLegacyPredecessor{}); err != nil { |
| + t.Fatal("interrupted first capture did not resume", err) |
| + } |
| +} |
| + |
| +func TestServingParkCollectionRejectsInvalidDeadline(t *testing.T) { |
| + f, old, _ := servingCollectionFixture(t, false) |
| + r, err := f.ledger.read(old.ID) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + r.CollectAfter = f.now.Add(hostbox.JournalReceiptRetention) |
| + for _, change := range []string{"incomplete", "open", "event-present", "namespace-unknown", "early"} { |
| + t.Run(change, func(t *testing.T) { |
| + bad := r |
| + switch change { |
| + case "incomplete": |
| + bad.Complete = false |
| + case "open": |
| + bad.Closed = false |
| + case "event-present": |
| + bad.EventRemoved = false |
| + case "namespace-unknown": |
| + bad.Namespace = servingArtifactDirectory{} |
| + case "early": |
| + bad.CollectAfter = bad.ParkedAt.Add(hostbox.JournalReceiptRetention - time.Nanosecond) |
| + } |
| + if err := f.ledger.save(bad); !errors.Is(err, hostbox.ErrChanged) { |
| + t.Fatal("invalid collection authority accepted", err) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +type servingCollectionFaultStore struct { |
| + rootstate.Store |
| + remove func(string) error |
| + write func(string, []byte) error |
| +} |
| + |
| +func (s *servingCollectionFaultStore) Remove(name string) error { |
| + if s.remove != nil { |
| + return s.remove(name) |
| + } |
| + return s.Store.Remove(name) |
| +} |
| + |
| +func (s *servingCollectionFaultStore) Write(name string, body []byte) error { |
| + if s.write != nil { |
| + return s.write(name, body) |
| + } |
| + return s.Store.Write(name, body) |
| +} |
| + |
| +func TestServingParkCollectionInterruptedWrites(t *testing.T) { |
| + for _, boundary := range []string{"deadline-before", "deadline-after", "consumption-before", "consumption-after", "lock-before", "lock-after", "record-before", "record-after"} { |
| + t.Run(boundary, func(t *testing.T) { |
| + f, old, _ := servingCollectionFixture(t, true) |
| + deadline := strings.HasPrefix(boundary, "deadline") |
| + if !deadline { |
| + if err := f.ledger.recoverCompleted(t.Context(), old.ID); err != nil { |
| + t.Fatal(err) |
| + } |
| + r, _ := f.ledger.read(old.ID) |
| + f.now = r.CollectAfter |
| + } |
| + store := &servingCollectionFaultStore{Store: f.disk} |
| + f.ledger.state = store |
| + fired := false |
| + fault := func(name string, apply func() error) error { |
| + matches := deadline && strings.HasPrefix(name, "retirement-") && strings.HasSuffix(name, ".json") || strings.HasPrefix(boundary, "consumption") && strings.HasPrefix(name, "legacy-") || strings.HasPrefix(boundary, "lock") && strings.HasSuffix(name, ".lock") || strings.HasPrefix(boundary, "record") && strings.HasPrefix(name, "retirement-") && strings.HasSuffix(name, ".json") |
| + if !matches || fired { |
| + return apply() |
| + } |
| + fired = true |
| + if strings.HasSuffix(boundary, "after") { |
| + if err := apply(); err != nil && !errors.Is(err, os.ErrNotExist) { |
| + return err |
| + } |
| + } |
| + return unix.EIO |
| + } |
| + store.write = func(name string, body []byte) error { |
| + return fault(name, func() error { return f.disk.Write(name, body) }) |
| + } |
| + store.remove = func(name string) error { return fault(name, func() error { return f.disk.Remove(name) }) } |
| + if err := f.ledger.recoverCompleted(t.Context(), old.ID); !errors.Is(err, unix.EIO) || !fired { |
| + t.Fatalf("fault not reached: fired=%t err=%v", fired, err) |
| + } |
| + f.ledger.state = f.disk |
| + if boundary != "record-after" { |
| + if err := f.ledger.recoverCompleted(t.Context(), old.ID); err != nil { |
| + t.Fatal("restart did not resume", err) |
| + } |
| + } |
| + if deadline { |
| + r, err := f.ledger.read(old.ID) |
| + if err != nil || !r.CollectAfter.Equal(f.now.Add(hostbox.JournalReceiptRetention)) { |
| + t.Fatal("deadline lost after restart", err) |
| + } |
| + } else { |
| + if _, err := f.ledger.read(old.ID); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatal("collection incomplete", err) |
| + } |
| + consumed, err := f.ledger.legacyConsumption(old.Successor) |
| + if err != nil || consumed == nil || !consumed.Collected { |
| + t.Fatal("legacy evidence lost", err) |
| + } |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestServingParkCollectionRefusesChangedFilesystem(t *testing.T) { |
| + for _, change := range []string{"event", "namespace", "cancel"} { |
| + t.Run(change, func(t *testing.T) { |
| + f, old, _ := servingCollectionFixture(t, false) |
| + if err := f.ledger.recoverCompleted(t.Context(), old.ID); err != nil { |
| + t.Fatal(err) |
| + } |
| + r, _ := f.ledger.read(old.ID) |
| + f.now = r.CollectAfter |
| + ctx := t.Context() |
| + var err error |
| + switch change { |
| + case "event": |
| + err = os.Mkdir(filepath.Dir(f.path(old, "")), 0700) |
| + case "namespace": |
| + path := filepath.Join(f.data, "instances", ".serving-parked") |
| + err = os.Rename(path, path+"-moved") |
| + if err == nil { |
| + err = os.Mkdir(path, 0700) |
| + } |
| + case "cancel": |
| + var cancel context.CancelFunc |
| + ctx, cancel = context.WithCancel(ctx) |
| + cancel() |
| + } |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + before, _ := f.disk.Read("retirement-" + old.ID + ".json") |
| + if err = f.ledger.recoverCompleted(ctx, old.ID); err == nil { |
| + t.Fatal("collected through changed evidence") |
| + } |
| + after, err := f.disk.Read("retirement-" + old.ID + ".json") |
| + if err != nil || string(before) != string(after) { |
| + t.Fatal("refusal changed retirement", err) |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/host/claimservingcollection_rootrunner_linux_test.go b/internal/host/claimservingcollection_rootrunner_linux_test.go| new file mode 100644 |
| index 000000000..a4c26d434 |
| --- /dev/null |
| +++ b/internal/host/claimservingcollection_rootrunner_linux_test.go |
| @@ -0,0 +1,99 @@ |
| +//go:build linux && rootrunner |
| + |
| +package host |
| + |
| +import ( |
| + "errors" |
| + "os" |
| + "path/filepath" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/rootstate" |
| + "golang.org/x/sys/unix" |
| +) |
| + |
| +// Real rootstate locks, same-bind deletion and metadata collection; claim, |
| +// receipt, tenant-start and completion authorization remain explicit fixtures. |
| +func TestRootServingParkCollection(t *testing.T) { |
| + servingRootRunner(t) |
| + root, err := os.MkdirTemp("/var/lib", "carlos-rootrunner-collection-") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Cleanup(func() { |
| + if err := os.RemoveAll(root); err != nil { |
| + t.Error(err) |
| + } |
| + }) |
| + t.Setenv("TMPDIR", root) |
| + for _, lost := range []bool{false, true} { |
| + t.Run(map[bool]string{false: "ordinary", true: "lost-record-remove-reply"}[lost], func(t *testing.T) { |
| + f := newServingParkDiskFixture(t) |
| + servingRunnerMount(t, f.data) |
| + state, err := rootstate.Open(filepath.Join(t.TempDir(), "authority"), true, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Cleanup(func() { |
| + if err := state.Close(); err != nil { |
| + t.Error(err) |
| + } |
| + }) |
| + f.fs = newServingParkFS(f.data, state) |
| + f.ledger.fs, f.ledger.state = f.fs, state |
| + f.put("", "legacy bytes") |
| + old, receipts := f.evidence(10, true, "missing") |
| + if _, err = f.park(old, receipts); err != nil { |
| + t.Fatal(err) |
| + } |
| + f.complete(old) |
| + f.put("", "latest served bytes") |
| + f.put(".gen", "G11") |
| + next, receipts := f.evidence(11, false, "started") |
| + if _, err = f.park(next, receipts); err != nil { |
| + t.Fatal(err) |
| + } |
| + f.complete(next) |
| + if err = f.ledger.recoverCompleted(t.Context(), old.ID); err != nil { |
| + t.Fatal(err) |
| + } |
| + r, err := f.ledger.read(old.ID) |
| + if err != nil || r.CollectAfter.IsZero() { |
| + t.Fatal("no replay window", err) |
| + } |
| + f.now = r.CollectAfter |
| + if lost { |
| + fault := &servingCollectionFaultStore{Store: state} |
| + fault.remove = func(name string) error { |
| + if err := state.Remove(name); err != nil { |
| + return err |
| + } |
| + if name == "retirement-"+old.ID+".json" { |
| + return unix.EIO |
| + } |
| + return nil |
| + } |
| + f.ledger.state = fault |
| + } |
| + err = f.ledger.recoverCompleted(t.Context(), old.ID) |
| + if lost && !errors.Is(err, unix.EIO) || !lost && err != nil { |
| + t.Fatal("collection", err) |
| + } |
| + f.ledger.state = state |
| + for _, suffix := range []string{".json", ".lock"} { |
| + if _, err = state.Read("retirement-" + old.ID + suffix); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatal("collected file survived", suffix, err) |
| + } |
| + } |
| + consumed, err := f.ledger.legacyConsumption(old.Successor) |
| + if err != nil || consumed == nil || !consumed.Collected { |
| + t.Fatal("lost permanent legacy consumption", err) |
| + } |
| + ids, more, err := f.ledger.recoveryIDs(t.Context(), "", servingParkRecoveryLimit) |
| + if err != nil || more || len(ids) != 1 || ids[0] != next.ID { |
| + t.Fatal("restart rediscovered obsolete record", ids, more, err) |
| + } |
| + f.retained(next, "latest served bytes", r.ReapAfter) |
| + }) |
| + } |
| +} |
diff --git a/internal/host/claimservinglegacy.go b/internal/host/claimservinglegacy.go| index 9b318bbe4..be162307f 100644 |
| --- a/internal/host/claimservinglegacy.go |
| +++ b/internal/host/claimservinglegacy.go |
| @@ -127,3 +127,18 @@ func (l *servingParkLedger) legacyUnlaunched(ctx context.Context, a servingArtif |
| } |
| return nil |
| } |
| + |
| +func sameServingLegacyConsumption(c servingLegacyConsumption, r servingParkRecord) bool { |
| + if c.Authorization != r.Authorization || !sameServingRootStops(c.Receipts, r.Receipts) || !sameServingLegacyEvidence(c.LegacyEvidence, r.LegacyEvidence) || c.Source.Directory != r.Source.Directory || c.Source.Parent != r.Source.Parent { |
| + return false |
| + } |
| + for i, item := range c.Source.Files { |
| + other := r.Source.Files[i] |
| + item.Moved, item.DeleteIntent, item.Deleted = false, false, false |
| + other.Moved, other.DeleteIntent, other.Deleted = false, false, false |
| + if item != other { |
| + return false |
| + } |
| + } |
| + return true |
| +} |
diff --git a/internal/host/claimservinglegacyroot_linux.go b/internal/host/claimservinglegacyroot_linux.go| index eeb5fe062..5f743ab77 100644 |
| --- a/internal/host/claimservinglegacyroot_linux.go |
| +++ b/internal/host/claimservinglegacyroot_linux.go |
| @@ -144,6 +144,9 @@ func (p *servingParkRoot) parkLegacyServingDatabase(ctx context.Context, conn ne |
| var receipts []hostguard.FenceReceipt |
| var saved *servingParkRecord |
| if consumed != nil { |
| + if consumed.Collected { |
| + return hostbox.ErrChanged |
| + } |
| a, e, receipts = consumed.Authorization, consumed.LegacyEvidence, consumed.Receipts |
| if a.Directory != dir || e == nil { |
| return hostbox.ErrChanged |
diff --git a/internal/host/claimservingpark_records.go b/internal/host/claimservingpark_records.go| index 691e807df..e716abb29 100644 |
| --- a/internal/host/claimservingpark_records.go |
| +++ b/internal/host/claimservingpark_records.go |
| @@ -42,6 +42,7 @@ type servingParkSource struct { |
| } |
| type servingParkRecord struct { |
| + CollectAfter time.Time `json:",omitempty"` |
| Namespace servingArtifactDirectory `json:",omitempty"` |
| LegacyEvidence *servingLegacyEvidence `json:",omitempty"` |
| Continuations []servingParkContinuation `json:",omitempty"` |
| @@ -70,6 +71,7 @@ func (r servingParkRecord) activeAuthorization() servingParkAuthorization { |
| } |
| type servingLegacyConsumption struct { |
| + Collected bool `json:",omitempty"` |
| LegacyEvidence *servingLegacyEvidence `json:",omitempty"` |
| Receipts []hostguard.FenceReceipt `json:",omitempty"` |
| Authorization servingParkAuthorization |
| @@ -229,6 +231,9 @@ func validateServingParkRecord(r servingParkRecord) error { |
| if r.Closed && !r.Deleting { |
| return hostbox.ErrChanged |
| } |
| + if !r.CollectAfter.IsZero() && (!r.Complete || !r.Closed || !r.EventRemoved || r.Namespace == (servingArtifactDirectory{}) || r.CollectAfter.Before(r.ParkedAt.Add(hostbox.JournalReceiptRetention))) { |
| + return hostbox.ErrChanged |
| + } |
| if r.Closed { |
| for _, item := range r.Source.Files { |
| if item.Present && !item.Deleted { |
| @@ -390,6 +395,19 @@ func (l *servingParkLedger) parkWithEvidenceLocked(ctx context.Context, a servin |
| defer unlockRecord() |
| r, err := l.read(a.ID) |
| if errors.Is(err, os.ErrNotExist) { |
| + // Last survives collection. An old request cannot reconstruct a removed |
| + // intent from a restored source in the same incarnation. The outer root |
| + // guard independently requires the current incarnation and successor. |
| + if slot.Last != "" { |
| + last, e := l.read(slot.Last) |
| + if e != nil { |
| + return zero, e |
| + } |
| + prior := last.activeAuthorization().Successor |
| + if prior.Incarnation == a.Successor.Incarnation && a.Successor.Stamp <= prior.Stamp { |
| + return zero, hostbox.ErrChanged |
| + } |
| + } |
| captured, e := l.capture(ctx, a, receipts, legacy) |
| if e != nil { |
| return zero, e |
diff --git a/internal/host/claimservingpark_recovery.go b/internal/host/claimservingpark_recovery.go| index c3e0c0eb5..278e92a51 100644 |
| --- a/internal/host/claimservingpark_recovery.go |
| +++ b/internal/host/claimservingpark_recovery.go |
| @@ -103,9 +103,11 @@ func (l *servingParkLedger) recoverCompleted(ctx context.Context, id string) err |
| return err |
| } |
| slot.Pending = "" |
| - return l.write("slot-"+servingParkKey(slot.Box, slot.Host)+".json", slot) |
| + if err = l.write("slot-"+servingParkKey(slot.Box, slot.Host)+".json", slot); err != nil { |
| + return err |
| + } |
| } |
| - return nil |
| + return l.collectClosedLocked(ctx, &r, slot) |
| }) |
| } |
diff --git a/internal/host/claimservingpark_recovery_linux.go b/internal/host/claimservingpark_recovery_linux.go| index 80d0124dc..9e20f2b77 100644 |
| --- a/internal/host/claimservingpark_recovery_linux.go |
| +++ b/internal/host/claimservingpark_recovery_linux.go |
| @@ -62,21 +62,6 @@ func (p *servingParkRoot) recoverServingPark(ctx context.Context, saved servingP |
| }) |
| } |
| -func sameServingLegacyConsumption(c servingLegacyConsumption, r servingParkRecord) bool { |
| - if c.Authorization != r.Authorization || !sameServingRootStops(c.Receipts, r.Receipts) || !sameServingLegacyEvidence(c.LegacyEvidence, r.LegacyEvidence) || c.Source.Directory != r.Source.Directory || c.Source.Parent != r.Source.Parent { |
| - return false |
| - } |
| - for i, item := range c.Source.Files { |
| - other := r.Source.Files[i] |
| - item.Moved, item.DeleteIntent, item.Deleted = false, false, false |
| - other.Moved, other.DeleteIntent, other.Deleted = false, false, false |
| - if item != other { |
| - return false |
| - } |
| - } |
| - return true |
| -} |
| - |
| func (p *servingParkRoot) recoverServingParks(ctx context.Context, after string, limit int) (servingParkRecoveryPage, error) { |
| var out servingParkRecoveryPage |
| if p == nil || p.ledger == nil { |
diff --git a/internal/provision/testdata/plan14-root-reads-seed.tsv b/internal/provision/testdata/plan14-root-reads-seed.tsv| index 50794cdbd..1b990d8e8 100644 |
| --- a/internal/provision/testdata/plan14-root-reads-seed.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-seed.tsv |
| @@ -1,4 +1,4 @@ |
| -# Files seed checkpoint 17 overlay; pending Paul’s census ruling. |
| +# Files seed checkpoint 18 overlay; pending Paul’s census ruling. |
| # Database retirement activation remains refused; serving installation transfer uses the existing root transport. |
| # 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. |
| @@ -64,7 +64,7 @@ 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 through the typed journal service 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, receipts, source and artifact identities, progress, disposition and retention deadline; additive namespace must be prepared, distinct from event/parent and on the same mount e9223b996c4682efc30032c8b9306d1cf903b26f8749104a98ab07151801fac2 |
| +internal/host/claimservingpark_records.go validateServingParkRecord authority C,J4 rev3=PROPOSED-seed; private immutable authorization, source/event identities and durable progress; collection deadline requires complete closed removal and known namespace, no earlier than parked time plus transport receipt retention a199f98a4716aa6315e08b6a71f97b90ea21214b197ed7b219b7515a77ba1235 |
| 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 acquires these inputs under the host lock through the typed journal service 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; live edge peer reauthentication under namespace and host locks before census, inventory and final claim checks; root request entry refuses nil connections d3582f0760b4459721b3e6040cda738b105c1da19cc5d0746d04fea26e313b5c |
| @@ -75,7 +75,7 @@ internal/host/claimservingparkroot_linux.go validateServingRootHistory authority |
| 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 f07985c10ad6616354369c033f884b465971caff7a688823fe6bee61af6ed2b4 |
| +@coverage internal/host ea7df99b46e476a0a7261f0a8311827f92ce366cb1066342c6afbf32775128d2 |
| @coverage internal/hostbox eb1f4980ba609cc886ae4bd73aedbb6be3fb2eb104165bb1d8404da0814ed190 |
| @coverage internal/hostguard a721142f4f3ac5fa17b6b1e2ccfc66ca809cfdcbf2733fdc00fbd0efaa90aeb6 |
| 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 |
| @@ -87,9 +87,9 @@ internal/host/claimservinglegacy.go validateServingLegacyEvidence authority C,J4 |
| 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 no-process or stopped-admission evidence; exact predecessor fields with canonical recorded directory revision bounded by the current actor aa0dfe36c042ab65fd826e4b83eb81bfb9d03e6a54daafa828d5164492329d0d |
| -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; validated legacy authority records namespace parent before bounded quarantine recovery file:Read@3463 1330ef46dadfd17cc0e31100910a86f0bc94b02118429815e696696a61963eab |
| +internal/host/claimservinglegacyroot_linux.go servingParkRoot.parkLegacyServingDatabase authority C,J4 rev3=PROPOSED-seed; fresh serving claim, root installation, consumed legacy identity and exact stop/empty evidence; collected consumption refuses recapture, while consumption saved before the first intent still resumes file:Read@3525 c192eda0fc53bc5211b02123ed7745e8e2b1b2fc53ad5fb3b0d11fe95fd9672c |
| 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; shared read-only capture validates artifacts and disposition before any new authority write file:Prepare@2947,file:Read@1272 8dce52f721fceb7e14bbc3d3078fca00a508c987159bb100aa9e0262a4583de2 |
| +internal/host/claimservingpark_records.go servingParkLedger.parkWithEvidenceLocked authority C,J4 rev3=PROPOSED-seed; root-authenticated source, prior slot and immutable intent; a missing same-incarnation retirement must be newer than the last successor; preserve park ownership and replay refusal after collection file:Prepare@3449,file:Read@1774 099cf4772da0b33d2685e3d2b4de826923a256ba8e8dd576887df1d270c78abd |
| 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 |
| @@ -126,15 +126,15 @@ internal/host/claimretire_transport.go ClaimRootHTTP.RecoverPending authority C, |
| internal/host/claimservingparkroot_linux.go servingParkRoot.withRootInstallation authority C,J4 rev3=PROPOSED-seed; fresh claim, root installation and directory inventory, eligible enrollment, paired tags, census gate and post-input claim reread under namespace then host guard; startup grants no new identity file:Read@2113 8060d3a2628f86257c3600c986242e0d4957ee918214a89655af828dd3ddf93b |
| internal/host/claimservingpark_linux.go servingParkFS.removeEvent authority C,J4 rev3=PROPOSED-seed; recorded current or displaced namespace and empty event; durable removal intent, descriptor-relative rmdir and fsync; ambiguous old-root absence refuses file:Readdirnames@790 66aa36321bc2bf3245daec35032783bb164cb21fe9b47620fbec71fd4b56cb89 |
| internal/host/claimservingpark_linux.go servingParkFS.removedEvent authority C,J4 rev3=PROPOSED-seed; absence within the exact recorded current or displaced root; root replacement and ambiguous pre-upgrade absence refuse d4100944fa7e41b83578839a91df3e546fa069f2c2558b9e8216b53fd329306d |
| -internal/host/claimservingpark_records.go type servingParkRecord authority C,J4 rev3=PROPOSED-seed; private versioned retirement shape; additive namespace device/inode/mount alongside event deletion progress; old records remain readable e0e65b59eef4d478bdc0abab79fb4fe2db62047387582485e6c8bcecf5d2d31b |
| +internal/host/claimservingpark_records.go type servingParkRecord authority C,J4 rev3=PROPOSED-seed; private versioned retirement shape; additive collection deadline requires full completion and event removal; original authorization and retention deadline remain immutable ea0f918608527d73d259ca2c0947bea6a24485e1ac0777715282ae43b1fbb53d |
| internal/host/claimservingpark_recovery.go const servingParkRecoveryLimit authority C,J4 rev3=PROPOSED-seed; fixed root-side processing limit of 128 retirement records per recovery page 6b949581334f55a7bae1b4579e56ac74810fc3042fdf568fbfa8e6b3c5a2ef11 |
| internal/host/claimservingpark_recovery.go var errServingParkRetagPending authority C,J4 rev3=PROPOSED-seed; fixed pending-retag refusal; startup cannot write the independent claim writer tags 8fa87ebef4defae4fc1733528a76e0cc260711c2f5a664308e5368d69286d45b |
| internal/host/claimservingpark_recovery.go type servingParkRecoveryPage authority C,J4 rev3=PROPOSED-seed; private bounded recovery results, cursor and incomplete indicator; never a transport selector c2fb4445ed50b5237b72e8a5fff9b1b2f6c9dd1397473eeb2c78a6b8c2804b27 |
| internal/host/claimservingpark_recovery.go type servingParkRecoveryResult authority C,J4 rev3=PROPOSED-seed; root-derived host and record ID with local recovery outcome 4dd13c07e97470a6c5aa4fa98d326de8947422018f0d30d5c5af32d753cfcc39 |
| internal/host/claimservingpark_recovery.go servingParkLedger.recoveryIDs authority C,J4 rev3=PROPOSED-seed; private rootstate names; sorted bounded retirement page and continuation cursor; malformed record IDs retained for reporting 5b450cd0fb6ae6694bca2f9f75fe972c0d65681981a4caddde7e5c7269108782 |
| -internal/host/claimservingpark_recovery.go servingParkLedger.recoverCompleted authority C,J4 rev3=PROPOSED-seed; completed private deletion intent, original cause, slot and recorded filesystem identities under namespace/slot/record locks; preserves Last and ID replay references without live claim authority 2c7f9aed9d06a42f39ff1fd18bfcea919e5e1a17dcb46cd4274b3ecc68fce98b |
| +internal/host/claimservingpark_recovery.go servingParkLedger.recoverCompleted authority C,J4 rev3=PROPOSED-seed; private completed retirement, exact filesystem deletion and slot references; resume removal, clear completed pending reference and collect only obsolete tombstones after durable replay deadline a2a277c862ec65e8abaf8b93b7eba77a79e538a76d061fb080f37f6c0c6d48f4 |
| internal/host/claimservingpark_recovery.go servingParkRuntime.recoverPending authority C,J4 rev3=PROPOSED-seed; service lifetime cancellation and join; bounded startup results rendered only with indexed host/record/fixed-reason text 0aa3ef7edd0ac2c12b4221907ccd5927e7e3e03a4c2c0b77b669bc10cc49c241 |
| -internal/host/claimservingpark_recovery_linux.go sameServingLegacyConsumption authority C,J4 rev3=PROPOSED-seed; immutable legacy consumption and retained retirement source identities; excludes only per-artifact journal progress from comparison 1524b2aae315e05734a666f80596bc341a007098a84fedeaf1e32451663d4fd4 |
| +internal/host/claimservinglegacy.go sameServingLegacyConsumption authority C,J4 rev3=PROPOSED-seed; immutable legacy consumption and retained retirement source identities; compares original evidence while ignoring per-artifact progress and the separate collection flag 1524b2aae315e05734a666f80596bc341a007098a84fedeaf1e32451663d4fd4 |
| internal/host/claimservingpark_recovery_linux.go servingParkRoot.recoverServingPark authority C,J4 rev3=PROPOSED-seed; retained intent, exact current successor, independently rechecked installation, enrollment, receipt and local launch history; resumes original park and completes only existing paired tags; no retry settlement 0839c879792469648ceb62edce21319050351cf4db0a57c1a5220282afb7808f |
| internal/host/claimservingpark_recovery_linux.go servingParkRoot.recoverServingParks authority C,J4 rev3=PROPOSED-seed; private record page and per-record validation; continues past corrupt or refused hosts and reports incomplete enumeration; completed deletion separated from live host authority; first page recovers namespace under its lock using separate private parent evidence, without selecting a live host fb838aec596eaf1b26069f16006b5b2512284710c9bcebf8abfc65dbc9eb13cf |
| internal/host/claimservinghandover.go ClaimRootHTTP.servingHandover authority C,J4 rev3=PROPOSED-seed; root-only serving transfer constructor and existing private journal, inventory and registry dependencies 213a1885dd9aa5d0079cd52c4f88c6893ea5b43bfbfd83a67ec2c4697a83265a |
| @@ -174,3 +174,5 @@ internal/host/claimservingdisplaced_linux.go const servingParkRootSearchLimit au |
| internal/host/claimservingdisplaced_linux.go servingParkFS.eventRoot authority C,J4 rev3=PROPOSED-seed; current namespace and recorded retirement root; selects only current or independently recorded prior namespace; ambiguous old absent-event records refuse file:Read@562 7c47b53f88059eea930d483465570fc7205b3cec27a118472071ca4d6df7d5e7 |
| internal/host/claimservingdisplaced_linux.go servingParkFS.displacedRoot authority C,J4 rev3=PROPOSED-seed; completed private quarantine and prior namespace identity; no-follow bounded parent listing/statx and exact root-owned directory open; same device/inode/mount and safe parent recheck; no unknown child traversal file:Fstat@649,file:Read@144,file:Readdirnames@939,file:Statx@1259 762661ee74aaed30ca1147160a995786ad3a55200f6ffd25106125799ff0c74d |
| internal/host/claimservingdisplaced_linux.go servingParkFS.recordEventRoot authority C,J4 rev3=PROPOSED-seed; descriptor identity of an already verified event root; persist old-record namespace before first deletion; existing namespace must match exactly b4febb444cd09cd5ed45fc616d172966038d71026024c89dfce9e8c530435756 |
| +internal/host/claimservingcollection.go servingParkLedger.collectClosedLocked authority C,J4 rev3=PROPOSED-seed; private completed retirement, durable replay deadline, slot references and legacy-consumption evidence; preserve referenced records, mark permanent legacy consumption, then remove only obsolete private record and lock after 24 hours; no data deletion authority 97201d6a0b793715b5e8c0c0abb36d1c303e3b38f3d0e1a97aadd49063735984 |
| +internal/host/claimservingpark_records.go type servingLegacyConsumption authority C,J4 rev3=PROPOSED-seed; permanent legacy source and authorization evidence; additive Collected flag distinguishes garbage collection from interrupted first-intent creation 1f7d2cd2baaad00efda56e7c5637e8b386d80ad7e5f2b311487ebcf109c4861b |