WIP files seed checkpoint 6 (not for merge): root transport and lifetime wiring
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
19 files changed,
+1155
−48
internal/host/claimassembly.go+12 −0internal/host/claimjournal_transport.go+4 −0internal/host/claimretire_transport.go+1 −0internal/host/claimservingartifact.go+3 −3internal/host/claimservinglegacyroot_linux_test.go+46 −3internal/host/claimservingpark_other.go+4 −0internal/host/claimservingpark_records.go+3 −3internal/host/claimservingpark_transport.go+187 −0internal/host/claimservingpark_transport_linux_test.go+349 −0internal/host/claimservingparkroot_linux.go+13 −4internal/host/claimservingretire.go+4 −19internal/host/claimservingretire_linux_test.go+1 −1internal/host/rootexecution_runtime.go+4 −0internal/hostbox/journalservice.go+20 −3internal/hostbox/servingpark_service.go+108 −0internal/hostbox/servingpark_service_test.go+203 −0internal/hostbox/servingpark_transport.go+153 −0internal/provision/plan9_b8_census_test.go+5 −4internal/provision/testdata/plan14-root-reads-seed.tsv+35 −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 362.317s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 3.053s
ok github.com/carlosframework/platform/cmd/moveprobe 92.242s
ok github.com/carlosframework/platform/cmd/zddprobe 0.707s
ok github.com/carlosframework/platform/internal/activator 234.171s
ok github.com/carlosframework/platform/internal/agentsock 0.016s
ok github.com/carlosframework/platform/internal/alerting 0.003s
ok github.com/carlosframework/platform/internal/analytics 0.018s
ok github.com/carlosframework/platform/internal/appconfig 2.334s
ok github.com/carlosframework/platform/internal/apperr 0.071s
ok github.com/carlosframework/platform/internal/applog 0.124s
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.850s
? 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.011s
ok github.com/carlosframework/platform/internal/certwarm 0.004s
ok github.com/carlosframework/platform/internal/cgroup 0.703s
ok github.com/carlosframework/platform/internal/changelog 0.300s
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 368.265s
? 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.437s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.009s
ok github.com/carlosframework/platform/internal/dbverify 0.008s
ok github.com/carlosframework/platform/internal/dnsrec 0.005s
ok github.com/carlosframework/platform/internal/docsite 0.005s
ok github.com/carlosframework/platform/internal/domains 2.170s
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.191s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.008s
ok github.com/carlosframework/platform/internal/fleetapi 0.123s
ok github.com/carlosframework/platform/internal/gate 1.655s
ok github.com/carlosframework/platform/internal/genselect 0.015s
ok github.com/carlosframework/platform/internal/geo 0.018s
ok github.com/carlosframework/platform/internal/geoip 0.029s
ok github.com/carlosframework/platform/internal/host 130.151s
ok github.com/carlosframework/platform/internal/hostactors 0.396s
ok github.com/carlosframework/platform/internal/hostbox 2.216s
ok github.com/carlosframework/platform/internal/hostclaim 0.521s
ok github.com/carlosframework/platform/internal/hostevidence 0.014s
ok github.com/carlosframework/platform/internal/hostexec 4.831s
ok github.com/carlosframework/platform/internal/hostfence 0.032s
ok github.com/carlosframework/platform/internal/hostfiles 10.554s
ok github.com/carlosframework/platform/internal/hostfloor 0.014s
ok github.com/carlosframework/platform/internal/hostguard 1.013s
ok github.com/carlosframework/platform/internal/hostops 1.493s
? 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.013s
ok github.com/carlosframework/platform/internal/hostrelocate 0.468s
ok github.com/carlosframework/platform/internal/hostresolve 0.143s
ok github.com/carlosframework/platform/internal/hostwrite 0.014s
ok github.com/carlosframework/platform/internal/infraview 0.013s
ok github.com/carlosframework/platform/internal/instancectl 1.816s
ok github.com/carlosframework/platform/internal/instanceexec 2.737s
ok github.com/carlosframework/platform/internal/instancefs 0.018s
ok github.com/carlosframework/platform/internal/instancekey 0.015s
ok github.com/carlosframework/platform/internal/instanceupdates 0.022s
ok github.com/carlosframework/platform/internal/layout 1.468s
ok github.com/carlosframework/platform/internal/lease 9.451s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.005s
ok github.com/carlosframework/platform/internal/ledger 0.014s
ok github.com/carlosframework/platform/internal/mail 0.163s
ok github.com/carlosframework/platform/internal/maintenance 0.079s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.523s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.069s
ok github.com/carlosframework/platform/internal/metrics 0.005s
ok github.com/carlosframework/platform/internal/moves 21.070s
ok github.com/carlosframework/platform/internal/moves/archive 0.760s
ok github.com/carlosframework/platform/internal/objstore 0.180s
ok github.com/carlosframework/platform/internal/ops 0.171s
ok github.com/carlosframework/platform/internal/park 0.212s
ok github.com/carlosframework/platform/internal/privatefile 0.003s
ok github.com/carlosframework/platform/internal/provision 12.805s
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 4.056s
ok github.com/carlosframework/platform/internal/relay 7.221s
ok github.com/carlosframework/platform/internal/release 19.974s
ok github.com/carlosframework/platform/internal/rollout 3.724s
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.008s
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.288s
ok github.com/carlosframework/platform/internal/sim/hostlife 9.710s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 3.553s
ok github.com/carlosframework/platform/internal/status 0.039s
ok github.com/carlosframework/platform/internal/steering 0.005s
ok github.com/carlosframework/platform/internal/stopevidence 0.246s
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.006s
ok github.com/carlosframework/platform/internal/unixdial 0.003s
ok github.com/carlosframework/platform/internal/updatewords 0.005s
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.561s
=== 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.023s
=== RUN TestT4StorageProbeMinio
2026/10/14 08:39:18 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE58A2DD1B275B, 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.021s
minio contract: PASS
diff --git a/internal/host/claimassembly.go b/internal/host/claimassembly.go| index 3df628bb4..eb4f0169a 100644 |
| --- a/internal/host/claimassembly.go |
| +++ b/internal/host/claimassembly.go |
| @@ -359,11 +359,23 @@ func NewProductionRootClaimStop(ctx context.Context) (http.Handler, error) { |
| if err != nil { |
| return nil, err |
| } |
| + if assembly.Wake == nil || assembly.Wake.Inputs == nil { |
| + state.Close() |
| + return nil, hostbox.ErrUnsupported |
| + } |
| + root, err = withServingParkRoot(root, assembly.Wake.Inputs) |
| + if err != nil { |
| + state.Close() |
| + return nil, err |
| + } |
| runtime, err := newClaimExecutionRuntime(root, state) |
| if err != nil { |
| + root.servingParks.Close(ctx) |
| state.Close() |
| return nil, err |
| } |
| + runtime.parks = root.servingParks |
| + runtime.root = &root |
| root.Execution, root.executionRuntime = runtime.broker, runtime |
| return root, nil |
| } |
diff --git a/internal/host/claimjournal_transport.go b/internal/host/claimjournal_transport.go| index c380d7e71..8709aa809 100644 |
| --- a/internal/host/claimjournal_transport.go |
| +++ b/internal/host/claimjournal_transport.go |
| @@ -87,6 +87,10 @@ func (h ClaimRootHTTP) serveJournal(w http.ResponseWriter, r *http.Request, peer |
| w.WriteHeader(http.StatusBadRequest) |
| return |
| } |
| + if hostbox.ServingParkOperation(q.Op) || q.ServingPark != nil || q.V == 4 { |
| + h.serveServingPark(w, r, q) |
| + return |
| + } |
| if q.Op == "fence-view" { |
| h.serveFenceView(w, r, q) |
| return |
diff --git a/internal/host/claimretire_transport.go b/internal/host/claimretire_transport.go| index 7cec4edf0..4c174187a 100644 |
| --- a/internal/host/claimretire_transport.go |
| +++ b/internal/host/claimretire_transport.go |
| @@ -22,6 +22,7 @@ import ( |
| ) |
| type ClaimRootHTTP struct { |
| + servingParks *servingParkRuntime |
| executionRuntime *claimExecutionRuntime |
| Execution *execlease.RootBroker |
| InventoryActor func(context.Context) (hostbox.Actor, error) |
diff --git a/internal/host/claimservingartifact.go b/internal/host/claimservingartifact.go| index 008c0cda9..cf40c35bd 100644 |
| --- a/internal/host/claimservingartifact.go |
| +++ b/internal/host/claimservingartifact.go |
| @@ -1,5 +1,5 @@ |
| package host |
| -type servingArtifactDirectory struct { |
| - Device, Inode, Mount uint64 |
| -} |
| +import "github.com/carlosframework/platform/internal/hostbox" |
| + |
| +type servingArtifactDirectory = hostbox.ServingArtifactDirectory |
diff --git a/internal/host/claimservinglegacyroot_linux_test.go b/internal/host/claimservinglegacyroot_linux_test.go| index 3ec95cadc..12953f965 100644 |
| --- a/internal/host/claimservinglegacyroot_linux_test.go |
| +++ b/internal/host/claimservinglegacyroot_linux_test.go |
| @@ -19,6 +19,7 @@ import ( |
| "github.com/carlosframework/platform/internal/hostclaim" |
| "github.com/carlosframework/platform/internal/hostguard" |
| "github.com/carlosframework/platform/internal/lease" |
| + "github.com/carlosframework/platform/internal/lease/leasetest" |
| ) |
| func newServingLegacyRootFixture(t *testing.T, empty bool, beforeAdmission ...func(*servingRootFixture, *ClaimStopCoordinator, StopRequest)) (*servingRootFixture, ClaimLaunchInputs) { |
| @@ -328,7 +329,26 @@ func TestServingLegacyRootProductionRestoreSequence(t *testing.T) { |
| func runServingLegacyRestoreSequence(t *testing.T, failure string) { |
| f, in := newServingLegacyRootFixture(t, false) |
| - plan := plan12RestoreBaseline(t, f.f) |
| + fixture := plan12RestoreBaseline(t, f.f) |
| + raw, err := f.f.data.GetVersion(t.Context(), f.f.bucket, fixture.Files[0].Key, fixture.Files[0].VersionID) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + bucket := f.f.data.(*leasetest.VersionedBucket) |
| + rootKey := strings.Replace(fixture.Files[0].Key, "/g-src/", "/", 1) |
| + bucket.Put(f.f.bucket, rootKey, []byte("older invalid snapshot")) |
| + version := bucket.Put(f.f.bucket, rootKey, raw) |
| + client := hostbox.ServingParkClient{Call: servingParkSocket(t, servingTransportRoot(t, f))} |
| + var baseline *genselect.Plan |
| + sentinels := map[string]string{filepath.Join(filepath.Dir(f.db), "files", "payload"): "live file", filepath.Join(filepath.Dir(f.db), ".env"): "environment"} |
| + for path, body := range sentinels { |
| + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(path, []byte(body), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| var legacy servingParkRecord |
| for _, generation := range []string{"G", "G2"} { |
| if generation == "G2" { |
| @@ -352,7 +372,19 @@ func runServingLegacyRestoreSequence(t *testing.T, failure string) { |
| 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}} |
| + plan, err := genselect.Select(t.Context(), f.f.data, genselect.Request{Bucket: f.f.bucket, Region: f.actor.Region, Host: f.current.Host, Holder: *f.f.read().Claim.Holder}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if plan.Generation != hostclaim.RootGeneration || len(plan.Files) != 1 || plan.Files[0].Key != rootKey || plan.Files[0].VersionID != version { |
| + t.Fatalf("selected wrong baseline: %+v", plan) |
| + } |
| + if baseline == nil { |
| + baseline = plan |
| + } else if !reflect.DeepEqual(baseline, plan) { |
| + t.Fatal("successor changed selected remote baseline") |
| + } |
| + r := &claimServingArtifactRetirement{Claims: f.f.claims, Guard: f.f.guard, DataDir: f.f.guard.DataDir, Box: f.actor.Box, Region: f.actor.Region, Root: client} |
| 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 { |
| @@ -411,6 +443,17 @@ func runServingLegacyRestoreSequence(t *testing.T, failure string) { |
| t.Fatalf("legacy %q %v", b, err) |
| } |
| } |
| + for path, want := range sentinels { |
| + body, err := os.ReadFile(path) |
| + if err != nil || string(body) != want { |
| + t.Fatalf("unrelated artifact changed: %s %v", path, err) |
| + } |
| + } |
| + after, err := f.f.data.GetVersion(t.Context(), f.f.bucket, rootKey, version) |
| + if err != nil || string(after) != string(raw) { |
| + t.Fatal("remote baseline changed", err) |
| + } |
| + |
| } |
| func TestServingLegacyRootConsumedAfterExpiry(t *testing.T) { |
| @@ -542,7 +585,7 @@ func (r servingEmptyRaceReader) ListVersions(ctx context.Context, bucket, prefix |
| } |
| func TestServingLegacyRootEmptyProofClaimRace(t *testing.T) { |
| f, _ := newServingLegacyRootFixture(t, true) |
| - f.p.empty.Data = servingEmptyRaceReader{Reader: f.p.empty.Data, during: func() { |
| + f.p.empty.(*claimServingOwners).Data = servingEmptyRaceReader{Reader: f.p.empty.(*claimServingOwners).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) |
diff --git a/internal/host/claimservingpark_other.go b/internal/host/claimservingpark_other.go| index e6ed6f549..b11e9006d 100644 |
| --- a/internal/host/claimservingpark_other.go |
| +++ b/internal/host/claimservingpark_other.go |
| @@ -18,3 +18,7 @@ func (*servingParkFS) Move(*servingParkRecord, func() error) error { return h |
| func (*servingParkFS) Delete(*servingParkRecord, func() error) error { return hostbox.ErrUnsupported } |
| func (*servingParkFS) CheckParked(*servingParkRecord) error { return hostbox.ErrUnsupported } |
| + |
| +func newServingParkAuthority(ClaimRootHTTP, rootstate.Store, servingEmptyAuthority) (servingParkAuthority, error) { |
| + return nil, hostbox.ErrUnsupported |
| +} |
diff --git a/internal/host/claimservingpark_records.go b/internal/host/claimservingpark_records.go| index caf2ad260..0f36b8bca 100644 |
| --- a/internal/host/claimservingpark_records.go |
| +++ b/internal/host/claimservingpark_records.go |
| @@ -17,9 +17,9 @@ import ( |
| "github.com/carlosframework/platform/internal/rootstate" |
| ) |
| -// servingParkLedger has no production caller until root authorization and the |
| -// journal transport are bound. Its callbacks are private root implementation |
| -// seams; no request can supply a source, disposition, timestamp or completion. |
| +// servingParkLedger runs behind root authorization and the journal transport. |
| +// Its callbacks are private implementation seams; requests cannot supply a |
| +// source, disposition, timestamp or completion. |
| type servingParkLedger struct { |
| state rootstate.Store |
| fs servingParkFilesystem |
diff --git a/internal/host/claimservingpark_transport.go b/internal/host/claimservingpark_transport.go| new file mode 100644 |
| index 000000000..1c8d2d806 |
| --- /dev/null |
| +++ b/internal/host/claimservingpark_transport.go |
| @@ -0,0 +1,187 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "encoding/json" |
| + "errors" |
| + "net" |
| + "net/http" |
| + "sync" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/instancectl" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +var openClaimServingParkAuthority = func() (rootstate.Store, error) { |
| + return rootstate.Open("/var/lib/carlos-claims/authority/serving-parks", true, false) |
| +} |
| + |
| +type servingParkAuthority interface { |
| + parkServingDatabase(context.Context, net.Conn, servingArtifactIdentity, servingArtifactIdentity) (servingParkAuthorization, error) |
| + parkLegacyServingDatabase(context.Context, net.Conn, servingArtifactIdentity, servingLegacyPredecessor) (servingParkAuthorization, error) |
| + completeServingDatabasePark(context.Context, net.Conn, string, servingArtifactIdentity) error |
| + retainLegacyNoProcess(context.Context, hostclaim.Observed, StopResult) error |
| +} |
| + |
| +type servingEmptyAuthority interface { |
| + EmptyAuthority(context.Context, hostclaim.Observed, ClaimLaunchInputs) error |
| +} |
| + |
| +// Closing cancels and joins every request and stop-evidence retention before |
| +// releasing private state. A timed-out join leaves the store open for retry. |
| +type servingParkRuntime struct { |
| + authority servingParkAuthority |
| + state rootstate.Store |
| + ctx context.Context |
| + cancel context.CancelFunc |
| + mu sync.Mutex |
| + active int |
| + closing, closed bool |
| + idle chan struct{} |
| +} |
| + |
| +func withServingParkRoot(h ClaimRootHTTP, empty servingEmptyAuthority) (ClaimRootHTTP, error) { |
| + state, err := openClaimServingParkAuthority() |
| + if err != nil { |
| + return ClaimRootHTTP{}, err |
| + } |
| + runtime, err := newServingParkRuntime(h, state, empty) |
| + if err != nil { |
| + state.Close() |
| + return ClaimRootHTTP{}, err |
| + } |
| + h.servingParks = runtime |
| + h.Stop.Coordinator.RetainNoProcess = runtime.retainNoProcess |
| + return h, nil |
| +} |
| + |
| +func newServingParkRuntime(h ClaimRootHTTP, state rootstate.Store, empty servingEmptyAuthority) (*servingParkRuntime, error) { |
| + authority, err := newServingParkAuthority(h, state, empty) |
| + if err != nil { |
| + return nil, err |
| + } |
| + ctx, cancel := context.WithCancel(context.Background()) |
| + return &servingParkRuntime{authority: authority, state: state, ctx: ctx, cancel: cancel, idle: make(chan struct{})}, nil |
| +} |
| + |
| +func (p *servingParkRuntime) begin(ctx context.Context) (context.Context, func(), error) { |
| + if p == nil || ctx == nil { |
| + return nil, nil, hostbox.ErrUnsupported |
| + } |
| + p.mu.Lock() |
| + defer p.mu.Unlock() |
| + if p.closing { |
| + return nil, nil, hostbox.ErrUnsupported |
| + } |
| + p.active++ |
| + call, cancel := context.WithCancel(ctx) |
| + stop := context.AfterFunc(p.ctx, cancel) |
| + return call, func() { |
| + stop() |
| + cancel() |
| + p.mu.Lock() |
| + defer p.mu.Unlock() |
| + p.active-- |
| + if p.closing && p.active == 0 { |
| + close(p.idle) |
| + } |
| + }, nil |
| +} |
| + |
| +func (p *servingParkRuntime) Close(ctx context.Context) error { |
| + if p == nil { |
| + return nil |
| + } |
| + p.mu.Lock() |
| + if !p.closing { |
| + p.closing = true |
| + p.cancel() |
| + if p.active == 0 { |
| + close(p.idle) |
| + } |
| + } |
| + p.mu.Unlock() |
| + select { |
| + case <-p.idle: |
| + case <-ctx.Done(): |
| + select { |
| + case <-p.idle: |
| + default: |
| + return ctx.Err() |
| + } |
| + } |
| + p.mu.Lock() |
| + defer p.mu.Unlock() |
| + if p.closed { |
| + return nil |
| + } |
| + p.closed = true |
| + return p.state.Close() |
| +} |
| + |
| +func (p *servingParkRuntime) retainNoProcess(ctx context.Context, o hostclaim.Observed, result StopResult) error { |
| + call, done, err := p.begin(ctx) |
| + if err != nil { |
| + return err |
| + } |
| + defer done() |
| + return p.authority.retainLegacyNoProcess(call, o, result) |
| +} |
| + |
| +func (h ClaimRootHTTP) serveServingPark(w http.ResponseWriter, r *http.Request, q hostbox.JournalRequest) { |
| + out := hostbox.JournalResponse{V: 1, ID: q.ID} |
| + ctx, done, err := h.servingParks.begin(r.Context()) |
| + if err != nil { |
| + w.WriteHeader(http.StatusServiceUnavailable) |
| + return |
| + } |
| + defer done() |
| + conn := instancectl.RequestConn(ctx) |
| + if conn == nil || h.RequireNewWork == nil || h.InventoryActor == nil || h.Journals == nil || q.ServingPark == nil { |
| + w.WriteHeader(http.StatusForbidden) |
| + return |
| + } |
| + // This gate also covers receipt-only replay. The outer HTTP handler has |
| + // authenticated the live edge main process; effects authenticate it again |
| + // after acquiring root's namespace and host locks. |
| + if err = h.RequireNewWork(ctx); err != nil { |
| + w.WriteHeader(http.StatusForbidden) |
| + return |
| + } |
| + actor, err := h.InventoryActor(ctx) |
| + if err != nil || !actor.Eligible || actor.Quarantined || actor.Box == "" || actor.Enrollment == "" || actor.Revision == 0 || q.ServingPark.Successor.Box != actor.Box || q.ServingPark.Successor.Region != actor.Region { |
| + w.WriteHeader(http.StatusForbidden) |
| + return |
| + } |
| + out, err = h.Journals.ExecuteServingPark(ctx, actor.Box+"/"+actor.Enrollment, q, func(ctx context.Context) (hostbox.ServingParkResult, error) { |
| + p, a := *q.ServingPark, h.servingParks.authority |
| + switch q.Op { |
| + case "serving-park": |
| + return a.parkServingDatabase(ctx, conn, p.Successor, p.Predecessor) |
| + case "serving-park-legacy": |
| + return a.parkLegacyServingDatabase(ctx, conn, p.Successor, p.Legacy) |
| + case "serving-park-complete": |
| + err := a.completeServingDatabasePark(ctx, conn, p.Retirement, p.Successor) |
| + return hostbox.ServingParkResult{ID: p.Retirement, Successor: p.Successor}, err |
| + default: |
| + return hostbox.ServingParkResult{}, hostbox.ErrUnsupported |
| + } |
| + }) |
| + if err != nil { |
| + out.ServingPark = nil |
| + switch { |
| + case errors.Is(err, hostbox.ErrJournalUncertain): |
| + out.Error = "uncertain" |
| + case errors.Is(err, hostbox.ErrChanged), errors.Is(err, hostbox.ErrTypedJournal): |
| + out.Error = "refused" |
| + case errors.Is(err, hostbox.ErrUnsupported): |
| + out.Error = "unsupported" |
| + default: |
| + out.Error = "uncertain" |
| + } |
| + } |
| + w.Header().Set("Content-Type", "application/json") |
| + _ = json.NewEncoder(w).Encode(out) |
| +} |
diff --git a/internal/host/claimservingpark_transport_linux_test.go b/internal/host/claimservingpark_transport_linux_test.go| new file mode 100644 |
| index 000000000..a3edb257e |
| --- /dev/null |
| +++ b/internal/host/claimservingpark_transport_linux_test.go |
| @@ -0,0 +1,349 @@ |
| +//go:build linux |
| + |
| +package host |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "net" |
| + "net/http" |
| + "os" |
| + "path/filepath" |
| + "reflect" |
| + "sync/atomic" |
| + "testing" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/instancectl" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +func servingParkSocket(t *testing.T, handler http.Handler) hostbox.JournalCall { |
| + t.Helper() |
| + dir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "serving-park-") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Cleanup(func() { os.RemoveAll(dir) }) |
| + path := filepath.Join(dir, "root.sock") |
| + listener, err := net.Listen("unix", path) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + server := &instancectl.Server{ClaimStop: handler, PeerOK: func(net.Conn) error { return nil }, Logf: func(string, ...any) {}} |
| + ctx, cancel := context.WithCancel(context.Background()) |
| + done := make(chan error, 1) |
| + go func() { done <- server.Serve(ctx, listener) }() |
| + t.Cleanup(func() { |
| + cancel() |
| + if err := <-done; err != nil { |
| + t.Error(err) |
| + } |
| + }) |
| + return rootJournalClient(path) |
| +} |
| + |
| +func servingTransportRoot(t *testing.T, f *servingRootFixture) ClaimRootHTTP { |
| + t.Helper() |
| + ctx, cancel := context.WithCancel(context.Background()) |
| + p := &servingParkRuntime{authority: f.p, state: f.disk, ctx: ctx, cancel: cancel, idle: make(chan struct{})} |
| + t.Cleanup(func() { |
| + if err := p.Close(t.Context()); err != nil { |
| + t.Error(err) |
| + } |
| + }) |
| + h := f.p.root |
| + h.servingParks = p |
| + h.Stop.Coordinator.RetainNoProcess = p.retainNoProcess |
| + return h |
| +} |
| + |
| +func TestServingParkSocketRetiresAndCompletes(t *testing.T) { |
| + for _, legacy := range []bool{false, true} { |
| + t.Run(map[bool]string{false: "generation", true: "legacy"}[legacy], func(t *testing.T) { |
| + var f *servingRootFixture |
| + if legacy { |
| + f, _ = newServingLegacyRootFixture(t, false) |
| + } else { |
| + f = newServingRootFixture(t, true) |
| + } |
| + h := servingTransportRoot(t, f) |
| + call := servingParkSocket(t, h) |
| + var exact hostbox.JournalRequest |
| + client := hostbox.ServingParkClient{Call: func(ctx context.Context, q hostbox.JournalRequest) (hostbox.JournalResponse, error) { |
| + exact = q |
| + return call(ctx, q) |
| + }} |
| + var a servingParkAuthorization |
| + var err error |
| + if legacy { |
| + a, err = client.ParkLegacy(t.Context(), f.current, servingLegacyPredecessor{}) |
| + } else { |
| + a, err = client.Park(t.Context(), f.current, f.prior) |
| + } |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + parkRequest := exact |
| + r, err := f.p.ledger.read(a.ID) |
| + if err != nil || !r.Parked || r.Complete { |
| + t.Fatalf("park: %+v %v", r, err) |
| + } |
| + if _, err = os.Stat(f.db); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatal("source survived successful park", err) |
| + } |
| + f.tags(f.current.Stamp) |
| + if err = client.Complete(t.Context(), a.ID, f.current); err != nil { |
| + t.Fatal(err) |
| + } |
| + r, err = f.p.ledger.read(a.ID) |
| + if err != nil || !r.Complete { |
| + t.Fatalf("completion: %+v %v", r, err) |
| + } |
| + before, err := f.disk.Read("retirement-" + a.ID + ".json") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + // A replay returns the immutable reply even after restore writes the |
| + // successor. It must not inspect or park those new bytes. |
| + if err = os.WriteFile(f.db, []byte("successor"), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + out, err := call(t.Context(), parkRequest) |
| + if err != nil || out.Error != "" || out.ServingPark == nil || *out.ServingPark != a { |
| + t.Fatalf("replay: %+v %v", out, err) |
| + } |
| + after, err := f.disk.Read("retirement-" + a.ID + ".json") |
| + if err != nil || string(before) != string(after) { |
| + t.Fatal("replay changed retirement") |
| + } |
| + body, _ := os.ReadFile(f.db) |
| + if string(body) != "successor" { |
| + t.Fatal("replay touched successor") |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestServingParkSocketLostReplyReplaysExactRequest(t *testing.T) { |
| + f := newServingRootFixture(t, true) |
| + h := servingTransportRoot(t, f) |
| + var requests atomic.Int32 |
| + call := servingParkSocket(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| + if requests.Add(1) == 1 { |
| + // Execute against the real connection context, then drop the reply. |
| + h.ServeHTTP(discardServingReply{HeaderMap: make(http.Header)}, r) |
| + conn, _, err := w.(http.Hijacker).Hijack() |
| + if err == nil { |
| + conn.Close() |
| + } |
| + return |
| + } |
| + h.ServeHTTP(w, r) |
| + })) |
| + client := hostbox.ServingParkClient{Call: call} |
| + a, err := client.Park(t.Context(), f.current, f.prior) |
| + if err != nil || requests.Load() != 2 { |
| + t.Fatalf("lost reply: %+v %v requests=%d", a, err, requests.Load()) |
| + } |
| + r, err := f.p.ledger.read(a.ID) |
| + if err != nil || !r.Parked { |
| + t.Fatalf("park: %+v %v", r, err) |
| + } |
| +} |
| + |
| +type discardServingReply struct{ HeaderMap http.Header } |
| + |
| +func (w discardServingReply) Header() http.Header { return w.HeaderMap } |
| +func (discardServingReply) WriteHeader(int) {} |
| +func (discardServingReply) Write(b []byte) (int, error) { return len(b), nil } |
| + |
| +func TestServingParkSocketAuthorityGates(t *testing.T) { |
| + for _, gate := range []string{"peer", "census", "enrollment", "revision", "box", "quarantine"} { |
| + t.Run(gate, func(t *testing.T) { |
| + f := newServingRootFixture(t, true) |
| + h := servingTransportRoot(t, f) |
| + var refuse atomic.Bool |
| + auth := h.Stop.Authenticate |
| + h.Stop.Authenticate = func(ctx context.Context, c net.Conn) (StopPeer, error) { |
| + if refuse.Load() && gate == "peer" { |
| + return StopPeer{}, ErrClaimStopIdentity |
| + } |
| + return auth(ctx, c) |
| + } |
| + h.RequireNewWork = func(context.Context) error { |
| + if refuse.Load() && gate == "census" { |
| + return hostbox.ErrChanged |
| + } |
| + return nil |
| + } |
| + h.InventoryActor = func(context.Context) (hostbox.Actor, error) { |
| + a := f.actor |
| + if refuse.Load() { |
| + switch gate { |
| + case "enrollment": |
| + a.Enrollment = "other" |
| + case "revision": |
| + a.Revision = 0 |
| + case "box": |
| + a.Box = "other" |
| + case "quarantine": |
| + a.Quarantined = true |
| + } |
| + } |
| + return a, nil |
| + } |
| + call := servingParkSocket(t, h) |
| + var q hostbox.JournalRequest |
| + client := hostbox.ServingParkClient{Call: func(ctx context.Context, in hostbox.JournalRequest) (hostbox.JournalResponse, error) { |
| + q = in |
| + return call(ctx, in) |
| + }} |
| + a, err := client.Park(t.Context(), f.current, f.prior) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + before, _ := f.disk.Read("retirement-" + a.ID + ".json") |
| + refuse.Store(true) |
| + out, err := call(t.Context(), q) |
| + if err == nil && out.Error == "" { |
| + t.Fatal("replay bypassed authority", gate) |
| + } |
| + after, _ := f.disk.Read("retirement-" + a.ID + ".json") |
| + if !reflect.DeepEqual(before, after) { |
| + t.Fatal("refusal mutated record") |
| + } |
| + }) |
| + } |
| +} |
| + |
| +type servingCloseStore struct { |
| + rootstate.Store |
| + closed atomic.Bool |
| +} |
| + |
| +func (s *servingCloseStore) Close() error { s.closed.Store(true); return nil } |
| + |
| +func TestServingParkRuntimeJoinBeforePrivateStateClose(t *testing.T) { |
| + s := &servingCloseStore{} |
| + ctx, cancel := context.WithCancel(context.Background()) |
| + p := &servingParkRuntime{state: s, ctx: ctx, cancel: cancel, idle: make(chan struct{})} |
| + call, done, err := p.begin(t.Context()) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + deadline, stop := context.WithCancel(context.Background()) |
| + stop() |
| + if err = p.Close(deadline); !errors.Is(err, context.Canceled) || s.closed.Load() { |
| + t.Fatal("closed state before join", err) |
| + } |
| + select { |
| + case <-call.Done(): |
| + case <-time.After(time.Second): |
| + t.Fatal("active call not canceled") |
| + } |
| + if _, _, err = p.begin(t.Context()); err == nil { |
| + t.Fatal("new call admitted during close") |
| + } |
| + done() |
| + if err = p.Close(t.Context()); err != nil || !s.closed.Load() { |
| + t.Fatal("join did not close store", err) |
| + } |
| + if err = p.Close(t.Context()); err != nil { |
| + t.Fatal(err) |
| + } |
| +} |
| + |
| +func TestServingParkRuntimeRetainsNoProcess(t *testing.T) { |
| + // The production lifecycle wrapper must retain the same root observation |
| + // as the direct coordinator fixture, and refuse after shutdown. |
| + f, _ := newServingLegacyRootFixture(t, false) |
| + h := servingTransportRoot(t, f) |
| + saved, err := f.p.ledger.legacyStop(f.current) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + // Retention rechecks the original claim, so it refuses after admission. |
| + if err = h.Stop.Coordinator.RetainNoProcess(t.Context(), saved.Observed, saved.Result); err == nil { |
| + t.Fatal("retained stale preadmission observation") |
| + } |
| + if err = h.servingParks.Close(t.Context()); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err = h.Stop.Coordinator.RetainNoProcess(t.Context(), hostclaim.Observed{}, StopResult{}); err == nil { |
| + t.Fatal("closed runtime retained evidence") |
| + } |
| +} |
| + |
| +func TestServingParkProductionConstructorRequiresPrivateDependencies(t *testing.T) { |
| + f := newServingRootFixture(t, true) |
| + if _, err := newServingParkRuntime(f.p.root, nil, nil); err == nil { |
| + t.Fatal("missing private dependencies accepted") |
| + } |
| + // Production FS construction fixes root ownership and the nosymfollow |
| + // check. This test does not claim to open a privileged rootstate store. |
| + p, err := newServingParkRuntime(f.p.root, f.disk, plan9D5Fixture(t).owners) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer p.Close(t.Context()) |
| + fs := p.authority.(*servingParkRoot).ledger.fs.(*servingParkFS) |
| + if fs.owner != 0 || fs.mount == nil || p.authority.(*servingParkRoot).ledger.journal != f.p.root.Journals.Launches { |
| + t.Fatal("production constructor replaced root authority") |
| + } |
| + ctx, cancel := context.WithTimeout(t.Context(), time.Second) |
| + defer cancel() |
| + if err = p.Close(ctx); err != nil { |
| + t.Fatal(err) |
| + } |
| +} |
| + |
| +func TestServingParkProductionBindingRetainsSessionZero(t *testing.T) { |
| + old := openClaimServingParkAuthority |
| + t.Cleanup(func() { openClaimServingParkAuthority = old }) |
| + var closed *servingCloseStore |
| + f, _ := newServingLegacyRootFixture(t, false, func(f *servingRootFixture, coordinator *ClaimStopCoordinator, request StopRequest) { |
| + closed = &servingCloseStore{Store: f.disk} |
| + openClaimServingParkAuthority = func() (rootstate.Store, error) { return closed, nil } |
| + h, err := withServingParkRoot(f.p.root, f.p.empty) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Cleanup(func() { |
| + if err := h.servingParks.Close(context.Background()); err != nil { |
| + t.Error(err) |
| + } |
| + }) |
| + // This repeats the real preadmission coordinator path through production |
| + // binding. The original private no-process record must remain identical. |
| + key := servingLegacyKey("legacy-stop", servingArtifactIdentity{Host: request.Host, Box: request.Box, Incarnation: request.Incarnation}) |
| + before, err := f.disk.Read(key) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + coordinator.RetainNoProcess = h.Stop.Coordinator.RetainNoProcess |
| + if _, err = coordinator.FenceAndStop(t.Context(), request); err != nil { |
| + t.Fatal(err) |
| + } |
| + after, err := f.disk.Read(key) |
| + if err != nil || !reflect.DeepEqual(before, after) { |
| + t.Fatal("production retention changed original evidence", err) |
| + } |
| + if closed.closed.Load() { |
| + t.Fatal("private store closed during service") |
| + } |
| + if err = h.servingParks.Close(t.Context()); err != nil { |
| + t.Fatal(err) |
| + } |
| + }) |
| + if f == nil || closed == nil || !closed.closed.Load() { |
| + t.Fatal("production private store leaked") |
| + } |
| + bad := &servingCloseStore{Store: f.disk} |
| + openClaimServingParkAuthority = func() (rootstate.Store, error) { return bad, nil } |
| + if _, err := withServingParkRoot(ClaimRootHTTP{}, nil); err == nil || !bad.closed.Load() { |
| + t.Fatal("failed binding leaked private store", err) |
| + } |
| +} |
diff --git a/internal/host/claimservingparkroot_linux.go b/internal/host/claimservingparkroot_linux.go| index 87acf5c7c..2c10fcd26 100644 |
| --- a/internal/host/claimservingparkroot_linux.go |
| +++ b/internal/host/claimservingparkroot_linux.go |
| @@ -4,10 +4,12 @@ package host |
| import ( |
| "context" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| "net" |
| "path/filepath" |
| "sort" |
| "strconv" |
| + "time" |
| "github.com/carlosframework/platform/internal/hostbox" |
| "github.com/carlosframework/platform/internal/hostclaim" |
| @@ -16,13 +18,20 @@ import ( |
| ) |
| // servingParkRoot composes local root authority with the filesystem ledger. |
| -// 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. |
| +// Requests supply identities only; installation paths and receipts are read |
| +// under root's host lock. Serving activation still waits for recovery witnesses. |
| type servingParkRoot struct { |
| root ClaimRootHTTP |
| ledger *servingParkLedger |
| - empty *claimServingOwners |
| + empty servingEmptyAuthority |
| +} |
| + |
| +func newServingParkAuthority(h ClaimRootHTTP, state rootstate.Store, empty servingEmptyAuthority) (servingParkAuthority, error) { |
| + if state == nil || h.Journals == nil || h.Journals.Launches == nil || h.Journals.Inventory == nil || h.Journals.Inventory.Remote() || h.Stop.Coordinator.Guard.Root == nil || h.Stop.Authenticate == nil || h.RequireNewWork == nil || h.InventoryActor == nil || empty == nil || !filepath.IsAbs(h.Roots["data"]) || h.Roots["data"] != h.Stop.Coordinator.Guard.DataDir { |
| + return nil, hostbox.ErrUnsupported |
| + } |
| + ledger := &servingParkLedger{state: state, fs: newServingParkFS(h.Roots["data"], state), journal: h.Journals.Launches, now: time.Now} |
| + return &servingParkRoot{root: h, ledger: ledger, empty: empty}, nil |
| } |
| func (p *servingParkRoot) withAuthority(ctx context.Context, conn net.Conn, current servingArtifactIdentity, fn func(hostclaim.Observed, hostguard.FenceView, hostbox.Actor, servingArtifactDirectory, uint64) error) error { |
diff --git a/internal/host/claimservingretire.go b/internal/host/claimservingretire.go| index 540e63a85..ce954ab71 100644 |
| --- a/internal/host/claimservingretire.go |
| +++ b/internal/host/claimservingretire.go |
| @@ -20,26 +20,11 @@ import ( |
| // These selectors contain no local paths. Root must independently resolve the |
| // installation from its state inventory before returning an authorization. |
| -type servingArtifactIdentity struct { |
| - Host, Box, Region, Incarnation, Location, Generation string |
| - Stamp, Session uint64 |
| -} |
| +type servingArtifactIdentity = hostbox.ServingArtifactIdentity |
| -type servingLegacyPredecessor struct { |
| - Session uint64 |
| - Generation string |
| - Empty bool |
| -} |
| +type servingLegacyPredecessor = hostbox.ServingLegacyPredecessor |
| -type servingParkAuthorization struct { |
| - TagsFrom uint64 |
| - ID string |
| - Legacy bool |
| - Predecessor servingArtifactIdentity |
| - OriginalSuccessor servingArtifactIdentity |
| - Successor servingArtifactIdentity |
| - Directory servingArtifactDirectory |
| -} |
| +type servingParkAuthorization = hostbox.ServingParkResult |
| // A zero predecessor asks Park to resume an existing record for successor; |
| // it must never authorize a new retirement. Complete independently rereads |
| @@ -89,7 +74,7 @@ func servingWriterIdentity(o hostclaim.Observed, box, region string) (servingArt |
| if found != 1 { |
| return servingArtifactIdentity{}, hostbox.ErrChanged |
| } |
| - return servingArtifactIdentity{o.Host, box, region, h.Incarnation, h.Location, h.Generation, h.Stamp, h.Session}, nil |
| + return servingArtifactIdentity{Host: o.Host, Box: box, Region: region, Incarnation: h.Incarnation, Location: h.Location, Generation: h.Generation, Stamp: h.Stamp, Session: h.Session}, nil |
| } |
| func (r *claimServingArtifactRetirement) snapshot(ctx context.Context, admitted hostclaim.Observed, in ClaimLaunchInputs) (hostclaim.Observed, servingArtifactIdentity, string, uint64, error) { |
diff --git a/internal/host/claimservingretire_linux_test.go b/internal/host/claimservingretire_linux_test.go| index fa65d2e68..98ea1c3e7 100644 |
| --- a/internal/host/claimservingretire_linux_test.go |
| +++ b/internal/host/claimservingretire_linux_test.go |
| @@ -71,7 +71,7 @@ func (p *servingParkFixture) park(current, prior servingArtifactIdentity, legacy |
| if err := unix.Statx(unix.AT_FDCWD, filepath.Dir(p.db), unix.AT_SYMLINK_NOFOLLOW, unix.STATX_BASIC_STATS|unix.STATX_MNT_ID, &stat); err != nil { |
| return a, err |
| } |
| - a.Directory = servingArtifactDirectory{unix.Mkdev(stat.Dev_major, stat.Dev_minor), stat.Ino, stat.Mnt_id} |
| + a.Directory = servingArtifactDirectory{Device: unix.Mkdev(stat.Dev_major, stat.Dev_minor), Inode: stat.Ino, Mount: stat.Mnt_id} |
| dest := filepath.Join(p.f.guard.DataDir, "test-parks", a.ID) |
| if err := os.MkdirAll(dest, 0700); err != nil { |
| return a, err |
diff --git a/internal/host/rootexecution_runtime.go b/internal/host/rootexecution_runtime.go| index bc5958ebf..f3b32dfa4 100644 |
| --- a/internal/host/rootexecution_runtime.go |
| +++ b/internal/host/rootexecution_runtime.go |
| @@ -19,6 +19,7 @@ var openClaimExecutionAuthority = func() (rootstate.Store, error) { |
| } |
| type claimExecutionRuntime struct { |
| + parks *servingParkRuntime |
| broker *execlease.RootBroker |
| state rootstate.Store |
| mu sync.Mutex |
| @@ -93,6 +94,9 @@ func (r *claimExecutionRuntime) Close(ctx context.Context) error { |
| return ctx.Err() |
| } |
| } |
| + if err := r.parks.Close(ctx); err != nil { |
| + return err |
| + } |
| err := r.broker.Close(ctx) |
| if ctx.Err() != nil { |
| return errors.Join(err, ctx.Err()) |
diff --git a/internal/hostbox/journalservice.go b/internal/hostbox/journalservice.go| index 642251c50..be8598acc 100644 |
| --- a/internal/hostbox/journalservice.go |
| +++ b/internal/hostbox/journalservice.go |
| @@ -53,6 +53,9 @@ func journalPrepare(req JournalRequest, now time.Time) (JournalRequest, error) { |
| if req.RecordKey != "" || req.RecordETag != "" { |
| req.V = 3 |
| } |
| + if req.ServingPark != nil { |
| + req.V = 4 |
| + } |
| req.Nonce = hex.EncodeToString(nonce[:]) |
| req.IssuedAt = now.UnixNano() |
| req.ID = journalRequestID(req) |
| @@ -61,9 +64,11 @@ func journalPrepare(req JournalRequest, now time.Time) (JournalRequest, error) { |
| } |
| // JournalRequest carries values, never SQL or caller-chosen filesystem paths. |
| -// ID is retained by the transport across retries. Root commits the request and |
| -// its result in the same FULL-synchronous transaction as the journal change. |
| +// ID is retained by the transport across retries. Journal-only effects and |
| +// their replies share a FULL-synchronous transaction. Parking records a pending |
| +// receipt before filesystem work and a completed reply afterwards. |
| type JournalRequest struct { |
| + ServingPark *ServingParkRequest `json:"serving_park,omitempty"` |
| RecordKey string `json:"record_key,omitempty"` |
| RecordETag string `json:"record_etag,omitempty"` |
| Revision uint64 `json:"inventory_revision"` |
| @@ -83,6 +88,7 @@ type JournalRequest struct { |
| IssuedAt int64 `json:"issued_at,omitempty"` |
| } |
| type JournalResponse struct { |
| + ServingPark *ServingParkResult `json:"serving_park,omitempty"` |
| FenceView *hostguard.FenceView `json:"fence_view,omitempty"` |
| Revision uint64 `json:"inventory_revision"` |
| LastRequest string `json:"last_request"` |
| @@ -209,6 +215,10 @@ func journalRequestV2(q JournalRequest) []byte { |
| return raw |
| } |
| func journalRequestHash(req JournalRequest) [sha256.Size]byte { |
| + if req.V == 4 { |
| + raw, _ := json.Marshal([]any{"carlos-journal-request-v4", string(journalRequestV2(req)), journalServingParkV4(req.ServingPark)}) |
| + return sha256.Sum256(raw) |
| + } |
| if req.V == 3 { |
| raw, _ := json.Marshal([]any{"carlos-journal-request-v3", string(journalRequestV2(req)), req.RecordKey, req.RecordETag}) |
| return sha256.Sum256(raw) |
| @@ -227,6 +237,12 @@ func journalRequestDigest(req JournalRequest) string { |
| return hex.EncodeToString(sum[:]) |
| } |
| func validateJournalRequest(q JournalRequest) error { |
| + if q.V == 4 { |
| + return validateServingParkRequest(q) |
| + } |
| + if q.ServingPark != nil || ServingParkOperation(q.Op) { |
| + return ErrTypedJournal |
| + } |
| // The frozen request hash does not carry this root-only response field. |
| if q.Launch.WritersClosed || (q.V != 3 && (q.RecordKey != "" || q.RecordETag != "")) || (q.V == 3 && (q.Op != "inventory-write" || q.RecordKey == "" || q.RecordETag == "")) { |
| return ErrTypedJournal |
| @@ -304,6 +320,7 @@ func journalIdentity(path string, original os.FileInfo) error { |
| // batches. inventory-write needs no receipt to replay: its version row names |
| // the last committed request. |
| type JournalService struct { |
| + parkGate chan struct{} |
| Launches *LaunchJournal |
| Inventory *StopInventoryDB |
| Now func() time.Time |
| @@ -356,7 +373,7 @@ func NewJournalService(j *LaunchJournal) (*JournalService, error) { |
| if err != nil || closeErr != nil { |
| return nil, errors.Join(err, closeErr) |
| } |
| - return &JournalService{Launches: j, Inventory: &StopInventoryDB{db: j.db, shared: true}}, nil |
| + return &JournalService{parkGate: make(chan struct{}, 1), Launches: j, Inventory: &StopInventoryDB{db: j.db, shared: true}}, nil |
| } |
| // Replay returns only a byte-identical request by the same authenticated owner. |
diff --git a/internal/hostbox/servingpark_service.go b/internal/hostbox/servingpark_service.go| new file mode 100644 |
| index 000000000..02b1abbb1 |
| --- /dev/null |
| +++ b/internal/hostbox/servingpark_service.go |
| @@ -0,0 +1,108 @@ |
| +package hostbox |
| + |
| +import ( |
| + "context" |
| + "database/sql" |
| + "encoding/json" |
| + "errors" |
| + "time" |
| +) |
| + |
| +// ExecuteServingPark records uncertainty before invoking filesystem authority. |
| +// The callback runs without a SQLite transaction: it reads the same journal |
| +// under root's namespace and host locks. A pending replay revalidates authority |
| +// and resumes the deterministic retirement; it never rolls filesystem work back. |
| +func (s *JournalService) ExecuteServingPark(ctx context.Context, peer string, q JournalRequest, run func(context.Context) (ServingParkResult, error)) (JournalResponse, error) { |
| + out := JournalResponse{V: 1, ID: q.ID} |
| + if err := validateServingParkRequest(q); err != nil { |
| + return out, err |
| + } |
| + if s == nil || s.Launches == nil || s.Launches.db == nil || s.parkGate == nil || peer == "" || run == nil { |
| + return out, ErrUnsupported |
| + } |
| + select { |
| + case s.parkGate <- struct{}{}: |
| + defer func() { <-s.parkGate }() |
| + case <-ctx.Done(): |
| + return out, ctx.Err() |
| + } |
| + pending, replayed, err := s.prepareServingPark(ctx, peer, q) |
| + if err != nil || replayed { |
| + return pending, err |
| + } |
| + result, err := run(ctx) |
| + if err != nil { |
| + // Even an identity refusal may follow a prior rename or a lost save |
| + // reply. Preserve the pending receipt and the exact request for replay. |
| + return out, errors.Join(ErrJournalUncertain, err) |
| + } |
| + out.ServingPark = &result |
| + body, err := json.Marshal(out) |
| + if err != nil { |
| + return out, errors.Join(ErrJournalUncertain, err) |
| + } |
| + changed, err := s.Launches.db.ExecContext(ctx, "UPDATE journal_requests SET response=? WHERE id=? AND peer=? AND digest=?", body, q.ID, peer, journalRequestDigest(q)) |
| + if err != nil { |
| + return out, errors.Join(ErrJournalUncertain, err) |
| + } |
| + n, err := changed.RowsAffected() |
| + if err != nil || n != 1 { |
| + return out, ErrJournalUncertain |
| + } |
| + return out, nil |
| +} |
| + |
| +func (s *JournalService) prepareServingPark(ctx context.Context, peer string, q JournalRequest) (JournalResponse, bool, error) { |
| + out := JournalResponse{V: 1, ID: q.ID} |
| + tx, err := s.Launches.db.BeginTx(ctx, nil) |
| + if err != nil { |
| + return out, false, err |
| + } |
| + defer tx.Rollback() |
| + var oldPeer, digest string |
| + var body []byte |
| + err = tx.QueryRowContext(ctx, "SELECT peer,digest,response FROM journal_requests WHERE id=?", q.ID).Scan(&oldPeer, &digest, &body) |
| + found := err == nil |
| + if found { |
| + if oldPeer != peer || digest != journalRequestDigest(q) { |
| + return out, false, ErrChanged |
| + } |
| + if json.Unmarshal(body, &out) != nil || out.V != 1 || out.ID != q.ID { |
| + return out, false, ErrJournalUncertain |
| + } |
| + if out.Error == "" && out.ServingPark != nil { |
| + return out, true, nil |
| + } |
| + if out.Error != "uncertain" || out.ServingPark != nil { |
| + return out, false, ErrJournalUncertain |
| + } |
| + } else if !errors.Is(err, sql.ErrNoRows) { |
| + return out, false, err |
| + } |
| + now := time.Now() |
| + if s.Now != nil { |
| + now = s.Now() |
| + } |
| + var rejectBefore int64 |
| + if err = tx.QueryRowContext(ctx, "SELECT reject_before FROM journal_receipt_retention WHERE id=1").Scan(&rejectBefore); err != nil { |
| + return out, false, err |
| + } |
| + age := now.Sub(time.Unix(0, q.IssuedAt)) |
| + if q.IssuedAt <= rejectBefore || age > JournalRequestWindow || age < -JournalRequestWindow { |
| + return out, false, ErrJournalUncertain |
| + } |
| + if _, err = journalPrune(ctx, tx, rejectBefore, now); err != nil { |
| + return out, false, err |
| + } |
| + if !found { |
| + out.Error = "uncertain" |
| + body, err = json.Marshal(out) |
| + if err != nil { |
| + return out, false, err |
| + } |
| + if _, err = tx.ExecContext(ctx, "INSERT INTO journal_requests(id,peer,digest,response,created,issued) VALUES(?,?,?,?,?,?)", q.ID, peer, journalRequestDigest(q), body, now.UnixNano(), q.IssuedAt); err != nil { |
| + return out, false, err |
| + } |
| + } |
| + return out, false, tx.Commit() |
| +} |
diff --git a/internal/hostbox/servingpark_service_test.go b/internal/hostbox/servingpark_service_test.go| new file mode 100644 |
| index 000000000..091118ea1 |
| --- /dev/null |
| +++ b/internal/hostbox/servingpark_service_test.go |
| @@ -0,0 +1,203 @@ |
| +package hostbox |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "reflect" |
| + "strings" |
| + "testing" |
| + "time" |
| +) |
| + |
| +func parkRequest(t *testing.T) JournalRequest { |
| + t.Helper() |
| + p := ServingParkRequest{Successor: ServingArtifactIdentity{Host: "app.example", Box: "box", Region: "ie", Incarnation: "inc", Location: "instances/app.example", Generation: "G2", Stamp: 2, Session: 2}} |
| + p.Predecessor = p.Successor |
| + p.Predecessor.Stamp, p.Predecessor.Session, p.Predecessor.Generation = 1, 1, "G" |
| + return plan9Mutation(t, JournalRequest{Op: "serving-park", ServingPark: &p}) |
| +} |
| + |
| +func TestServingParkJournalPendingBeforeEffectAndReplay(t *testing.T) { |
| + s, path, _ := journalFixture(t) |
| + q := parkRequest(t) |
| + result := ServingParkResult{ID: strings.Repeat("a", 64), Successor: q.ServingPark.Successor} |
| + calls := 0 |
| + run := func(ctx context.Context) (ServingParkResult, error) { |
| + calls++ |
| + // This read would block if the callback held the write transaction. |
| + out, found, err := s.Replay(ctx, "peer", q) |
| + if err != nil || !found || out.Error != "uncertain" { |
| + t.Fatalf("effect without durable pending receipt: %+v %t %v", out, found, err) |
| + } |
| + if calls == 1 { |
| + return result, errors.New("lost retirement save reply") |
| + } |
| + return result, nil |
| + } |
| + if _, err := s.ExecuteServingPark(t.Context(), "peer", q, run); !errors.Is(err, ErrJournalUncertain) { |
| + t.Fatal(err) |
| + } |
| + if err := s.Launches.Close(); err != nil { |
| + t.Fatal(err) |
| + } |
| + j, err := OpenLaunchJournal(path) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Cleanup(func() { j.Close() }) |
| + s, err = NewJournalService(j) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + out, err := s.ExecuteServingPark(t.Context(), "peer", q, run) |
| + if err != nil || out.ServingPark == nil || *out.ServingPark != result || calls != 2 { |
| + t.Fatalf("pending replay: %+v %v calls=%d", out, err, calls) |
| + } |
| + s.Now = func() time.Time { return time.Now().Add(48 * time.Hour) } |
| + replayed, err := s.ExecuteServingPark(t.Context(), "peer", q, run) |
| + if err != nil || !reflect.DeepEqual(replayed, out) || calls != 2 { |
| + t.Fatalf("completed replay executed again: %+v %v calls=%d", replayed, err, calls) |
| + } |
| + if _, err = s.ExecuteServingPark(t.Context(), "other-enrollment", q, run); !errors.Is(err, ErrChanged) || calls != 2 { |
| + t.Fatalf("foreign owner replay: %v", err) |
| + } |
| +} |
| + |
| +func TestServingParkJournalAgeAndPrunedReplay(t *testing.T) { |
| + for _, pending := range []bool{false, true} { |
| + t.Run(map[bool]string{false: "new", true: "pending"}[pending], func(t *testing.T) { |
| + s, _, _ := journalFixture(t) |
| + q := parkRequest(t) |
| + calls := 0 |
| + run := func(context.Context) (ServingParkResult, error) { |
| + calls++ |
| + return ServingParkResult{}, errors.New("partial rename") |
| + } |
| + if pending { |
| + _, _ = s.ExecuteServingPark(t.Context(), "peer", q, run) |
| + } |
| + want := calls |
| + for _, shift := range []time.Duration{-16 * time.Minute, 16 * time.Minute} { |
| + s.Now = func() time.Time { return time.Unix(0, q.IssuedAt).Add(shift) } |
| + if _, err := s.ExecuteServingPark(t.Context(), "peer", q, run); !errors.Is(err, ErrJournalUncertain) || calls != want { |
| + t.Fatalf("expired request ran: %v calls=%d", err, calls) |
| + } |
| + } |
| + }) |
| + } |
| + s, _, _ := journalFixture(t) |
| + q := parkRequest(t) |
| + result := ServingParkResult{ID: strings.Repeat("a", 64), Successor: q.ServingPark.Successor} |
| + run := func(context.Context) (ServingParkResult, error) { return result, nil } |
| + if _, err := s.ExecuteServingPark(t.Context(), "peer", q, run); err != nil { |
| + t.Fatal(err) |
| + } |
| + // A new request prunes the old receipt. Roll the clock back afterwards: |
| + // the persisted rejection boundary still prevents executing the old ID. |
| + now := time.Unix(0, q.IssuedAt).Add(48 * time.Hour) |
| + s.Now = func() time.Time { return now } |
| + newQ, err := journalPrepare(JournalRequest{Op: q.Op, ServingPark: q.ServingPark}, now) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err = s.ExecuteServingPark(t.Context(), "peer", newQ, run); err != nil { |
| + t.Fatal(err) |
| + } |
| + s.Now = func() time.Time { return time.Unix(0, q.IssuedAt) } |
| + if _, err = s.ExecuteServingPark(t.Context(), "peer", q, func(context.Context) (ServingParkResult, error) { t.Fatal("pruned request reran"); return result, nil }); !errors.Is(err, ErrJournalUncertain) { |
| + t.Fatal(err) |
| + } |
| +} |
| + |
| +func TestServingParkJournalRejectsMixedAndChangedSelectors(t *testing.T) { |
| + s, _, _ := journalFixture(t) |
| + for name, change := range map[string]func(*JournalRequest){ |
| + "version": func(q *JournalRequest) { q.V = 2 }, |
| + "unknown": func(q *JournalRequest) { q.Op = "serving-park-delete" }, |
| + "path-field": func(q *JournalRequest) { q.RecordKey = "/victim" }, |
| + "launch": func(q *JournalRequest) { q.Launch.NoState = true }, |
| + "target": func(q *JournalRequest) { q.Target.Host = "victim" }, |
| + "successor": func(q *JournalRequest) { q.ServingPark.Successor.Generation = "forged" }, |
| + "predecessor": func(q *JournalRequest) { q.ServingPark.Predecessor.Generation = "forged" }, |
| + "legacy": func(q *JournalRequest) { q.ServingPark.Legacy.Empty = true }, |
| + "retirement": func(q *JournalRequest) { q.ServingPark.Retirement = strings.Repeat("f", 64) }, |
| + "pressure": func(q *JournalRequest) { q.Op = "serving-park-pressure" }, |
| + } { |
| + t.Run(name, func(t *testing.T) { |
| + q := parkRequest(t) |
| + change(&q) |
| + if _, err := s.ExecuteServingPark(t.Context(), "peer", q, func(context.Context) (ServingParkResult, error) { |
| + t.Fatal("invalid request reached effects") |
| + return ServingParkResult{}, nil |
| + }); err == nil { |
| + t.Fatal("accepted invalid request") |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestServingParkClientRetainsExactUncertainRequest(t *testing.T) { |
| + var sent JournalRequest |
| + client := ServingParkClient{Call: func(_ context.Context, q JournalRequest) (JournalResponse, error) { |
| + sent = q |
| + return JournalResponse{}, errors.New("lost reply") |
| + }} |
| + p := parkRequest(t).ServingPark |
| + _, err := client.Park(t.Context(), p.Successor, p.Predecessor) |
| + var uncertain *JournalUncertainError |
| + if !errors.As(err, &uncertain) || !reflect.DeepEqual(uncertain.Request, sent) || sent.V != 4 { |
| + t.Fatalf("lost request: %v", err) |
| + } |
| + client.Call = func(_ context.Context, q JournalRequest) (JournalResponse, error) { |
| + if !reflect.DeepEqual(q, sent) { |
| + t.Fatal("request changed on replay") |
| + } |
| + return JournalResponse{V: 1, ID: q.ID, Error: "refused"}, nil |
| + } |
| + if _, err = client.ReplayUncertain(t.Context(), uncertain); !errors.As(err, &uncertain) { |
| + t.Fatalf("late refusal became definite: %v", err) |
| + } |
| + client.Call = func(_ context.Context, q JournalRequest) (JournalResponse, error) { |
| + return JournalResponse{V: 1, ID: q.ID}, nil |
| + } |
| + if _, err = client.Park(t.Context(), p.Successor, p.Predecessor); !errors.As(err, &uncertain) { |
| + t.Fatalf("missing result lost request: %v", err) |
| + } |
| +} |
| + |
| +func TestServingParkJournalReceiptFailureKeepsUncertainty(t *testing.T) { |
| + for _, point := range []string{"before-effect", "after-effect"} { |
| + t.Run(point, func(t *testing.T) { |
| + s, _, _ := journalFixture(t) |
| + q := parkRequest(t) |
| + verb := "INSERT" |
| + if point == "after-effect" { |
| + verb = "UPDATE" |
| + } |
| + if _, err := s.Launches.db.Exec("CREATE TRIGGER fail_park_receipt BEFORE " + verb + " ON journal_requests BEGIN SELECT RAISE(ABORT, 'receipt unavailable'); END"); err != nil { |
| + t.Fatal(err) |
| + } |
| + calls := 0 |
| + run := func(context.Context) (ServingParkResult, error) { |
| + calls++ |
| + return ServingParkResult{ID: strings.Repeat("a", 64), Successor: q.ServingPark.Successor}, nil |
| + } |
| + _, err := s.ExecuteServingPark(t.Context(), "peer", q, run) |
| + if err == nil { |
| + t.Fatal("receipt failure succeeded") |
| + } |
| + if point == "before-effect" && calls != 0 { |
| + t.Fatal("effect without pending receipt") |
| + } |
| + if point == "after-effect" && (calls != 1 || !errors.Is(err, ErrJournalUncertain)) { |
| + t.Fatalf("lost outcome: %v calls=%d", err, calls) |
| + } |
| + if _, err = s.Launches.db.Exec("DROP TRIGGER fail_park_receipt"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err = s.ExecuteServingPark(t.Context(), "peer", q, run); err != nil { |
| + t.Fatal(err) |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/hostbox/servingpark_transport.go b/internal/hostbox/servingpark_transport.go| new file mode 100644 |
| index 000000000..63c0d2203 |
| --- /dev/null |
| +++ b/internal/hostbox/servingpark_transport.go |
| @@ -0,0 +1,153 @@ |
| +package hostbox |
| + |
| +import ( |
| + "context" |
| + "encoding/hex" |
| + "reflect" |
| + "time" |
| +) |
| + |
| +// Parking selectors carry identities only. Root derives all paths, receipts, |
| +// sizes and deadlines independently. |
| +type ServingArtifactIdentity struct { |
| + Host, Box, Region, Incarnation, Location, Generation string |
| + Stamp, Session uint64 |
| +} |
| + |
| +type ServingArtifactDirectory struct { |
| + Device, Inode, Mount uint64 |
| +} |
| + |
| +type ServingLegacyPredecessor struct { |
| + Session uint64 |
| + Generation string |
| + Empty bool |
| +} |
| + |
| +type ServingParkRequest struct { |
| + Successor ServingArtifactIdentity |
| + Predecessor ServingArtifactIdentity |
| + Legacy ServingLegacyPredecessor |
| + Retirement string |
| +} |
| + |
| +type ServingParkResult struct { |
| + TagsFrom uint64 |
| + ID string |
| + Legacy bool |
| + Predecessor ServingArtifactIdentity |
| + OriginalSuccessor ServingArtifactIdentity |
| + Successor ServingArtifactIdentity |
| + Directory ServingArtifactDirectory |
| +} |
| + |
| +// Keep this tuple frozen when the public wire structs grow. |
| +func journalServingParkV4(p *ServingParkRequest) any { |
| + if p == nil { |
| + return nil |
| + } |
| + identity := func(s ServingArtifactIdentity) any { |
| + return []any{s.Host, s.Box, s.Region, s.Incarnation, s.Location, s.Generation, s.Stamp, s.Session} |
| + } |
| + return []any{identity(p.Successor), identity(p.Predecessor), p.Legacy.Session, p.Legacy.Generation, p.Legacy.Empty, p.Retirement} |
| +} |
| + |
| +func ServingParkOperation(op string) bool { |
| + return op == "serving-park" || op == "serving-park-legacy" || op == "serving-park-complete" || op == "serving-park-pressure" |
| +} |
| + |
| +func validateServingParkRequest(q JournalRequest) error { |
| + if q.V != 4 || q.ServingPark == nil || !ServingParkOperation(q.Op) { |
| + return ErrTypedJournal |
| + } |
| + // Reject every field belonging to a different journal operation, including |
| + // fields omitted from v4's hash. Only these selectors are remotely writable. |
| + canonical := JournalRequest{V: q.V, ID: q.ID, Op: q.Op, Nonce: q.Nonce, IssuedAt: q.IssuedAt, ServingPark: q.ServingPark} |
| + if !reflect.DeepEqual(q, canonical) { |
| + return ErrTypedJournal |
| + } |
| + nonce, err := hex.DecodeString(q.Nonce) |
| + if err != nil || len(nonce) != 16 || q.IssuedAt <= 0 { |
| + return ErrTypedJournal |
| + } |
| + if q.ID != journalRequestID(q) { |
| + return ErrChanged |
| + } |
| + p := *q.ServingPark |
| + s := p.Successor |
| + if s.Host == "" || s.Box == "" || s.Region == "" || s.Incarnation == "" || s.Location == "" || s.Generation == "" || s.Stamp == 0 || s.Session != s.Stamp { |
| + return ErrTypedJournal |
| + } |
| + switch q.Op { |
| + case "serving-park": |
| + if p.Legacy != (ServingLegacyPredecessor{}) || p.Retirement != "" { |
| + return ErrTypedJournal |
| + } |
| + case "serving-park-legacy": |
| + if p.Predecessor != (ServingArtifactIdentity{}) || p.Retirement != "" || (p.Legacy.Session == 0) != (p.Legacy.Generation == "") || p.Legacy.Session >= s.Session || (p.Legacy.Empty && p.Legacy.Session != 0) { |
| + return ErrTypedJournal |
| + } |
| + case "serving-park-complete": |
| + id, err := hex.DecodeString(p.Retirement) |
| + if err != nil || len(id) != 32 || p.Predecessor != (ServingArtifactIdentity{}) || p.Legacy != (ServingLegacyPredecessor{}) { |
| + return ErrTypedJournal |
| + } |
| + case "serving-park-pressure": |
| + // Pressure also needs a preadmission observed-claim selector. It remains |
| + // unavailable until the independent sizing and relief mechanism exists. |
| + return ErrUnsupported |
| + } |
| + return nil |
| +} |
| + |
| +type ServingParkClient struct{ Call JournalCall } |
| + |
| +func (c ServingParkClient) request(ctx context.Context, op string, p ServingParkRequest) (ServingParkResult, error) { |
| + if c.Call == nil { |
| + return ServingParkResult{}, ErrUnsupported |
| + } |
| + q, err := journalPrepare(JournalRequest{Op: op, ServingPark: &p}, time.Now()) |
| + if err != nil { |
| + return ServingParkResult{}, err |
| + } |
| + out, err := journalSend(ctx, c.Call, q, false) |
| + if err != nil { |
| + return ServingParkResult{}, err |
| + } |
| + if err = servingParkResponse(q, out); err != nil { |
| + return ServingParkResult{}, err |
| + } |
| + return *out.ServingPark, nil |
| +} |
| + |
| +func (c ServingParkClient) Park(ctx context.Context, successor, predecessor ServingArtifactIdentity) (ServingParkResult, error) { |
| + return c.request(ctx, "serving-park", ServingParkRequest{Successor: successor, Predecessor: predecessor}) |
| +} |
| + |
| +func (c ServingParkClient) ParkLegacy(ctx context.Context, successor ServingArtifactIdentity, predecessor ServingLegacyPredecessor) (ServingParkResult, error) { |
| + return c.request(ctx, "serving-park-legacy", ServingParkRequest{Successor: successor, Legacy: predecessor}) |
| +} |
| + |
| +func (c ServingParkClient) Complete(ctx context.Context, id string, successor ServingArtifactIdentity) error { |
| + _, err := c.request(ctx, "serving-park-complete", ServingParkRequest{Successor: successor, Retirement: id}) |
| + return err |
| +} |
| + |
| +func (c ServingParkClient) ReplayUncertain(ctx context.Context, uncertain *JournalUncertainError) (JournalResponse, error) { |
| + if c.Call == nil || uncertain == nil || validateServingParkRequest(uncertain.Request) != nil { |
| + return JournalResponse{}, ErrUnsupported |
| + } |
| + out, err := journalSend(ctx, c.Call, uncertain.Request, true) |
| + if err == nil { |
| + err = servingParkResponse(uncertain.Request, out) |
| + } |
| + return out, err |
| +} |
| + |
| +func servingParkResponse(q JournalRequest, out JournalResponse) error { |
| + p := q.ServingPark |
| + if p == nil || out.ServingPark == nil || out.ServingPark.Successor != p.Successor || out.ServingPark.ID == "" || (p.Retirement != "" && out.ServingPark.ID != p.Retirement) { |
| + return &JournalUncertainError{Request: q, Cause: ErrJournalUncertain} |
| + } |
| + return nil |
| +} |
diff --git a/internal/provision/plan9_b8_census_test.go b/internal/provision/plan9_b8_census_test.go| index 6ce8bb5b3..f3e643558 100644 |
| --- a/internal/provision/plan9_b8_census_test.go |
| +++ b/internal/provision/plan9_b8_census_test.go |
| @@ -486,10 +486,11 @@ var plan9B8CensusInventory = []censusEntry{ |
| {"internal/hostbox/inventory_expected.go", "StopInventoryDB.WriteExpected", "key-site", "key:RecordKey", "8c2824998dcb510ed54c08a74846352c7918fe1ba2d7ffb3dd05596ca81b329b"}, |
| {"internal/hostbox/inventory_expected.go", "type InventoryExpectation", "key-site", "key:RecordKey", "c9d359aac6c5e352f31acfbd66548e09be10a5e621b5bf15da61b4ad3fd9feff"}, |
| {"internal/hostbox/inventory_registration.go", "ReconcileAdapter.CaptureInventory", "key-site", "key:RecordKey", "75b520c9ebd7816e4e0ce9fa0d767c35282f745055ff923cef54f35be109cac5"}, |
| - {"internal/hostbox/journalservice.go", "journalPrepare", "key-site", "key:RecordKey", "c41aa5bc3cdc1eaca61d8cd65ddaab5a93863378bc8553f2b4a02bcc8df55cf4"}, |
| - {"internal/hostbox/journalservice.go", "journalRequestHash", "key-site", "key:RecordKey", "8c07978658bb060c28ad88477dcbb10d8591ef194cac283cb54eb6a4e833ac76"}, |
| - {"internal/hostbox/journalservice.go", "type JournalRequest", "key-site", "key:RecordKey", "14baf4c57a342a8b2dadaa95c965de4650ee352183de3a8c56768f47642ff5ab"}, |
| - {"internal/hostbox/journalservice.go", "validateJournalRequest", "key-site", "key:RecordKey", "724be9634772f00bf0d9da2c39c8049763c9aad6f3075c10f31c97ab69c15103"}, |
| + // Files seed v4 rejects RecordKey; these remain key sites for older journal versions. |
| + {"internal/hostbox/journalservice.go", "journalPrepare", "key-site", "key:RecordKey", "c8d747d257f0ad48a7bfc645de0a9c50a05fec4c333a75df2a1360d9de9fb9f1"}, |
| + {"internal/hostbox/journalservice.go", "journalRequestHash", "key-site", "key:RecordKey", "aa4856b2927d21a68cd14a360b09a6d02057afadbe71fffbfee682aef3719ac4"}, |
| + {"internal/hostbox/journalservice.go", "type JournalRequest", "key-site", "key:RecordKey", "ef8b893667b6334a25b126a26cfa4712a3dd1d4a208187c10ecc3c2d1bb9c6b4"}, |
| + {"internal/hostbox/journalservice.go", "validateJournalRequest", "key-site", "key:RecordKey", "2d05c14a0afd5bbe4b5dd9e03e7eb885329caec62fb360e02256183d2894d351"}, |
| // Fix3 extends PID-zero binding states; RecordKey remains an identity read, with no record write. |
| {"internal/host/proofbinding.go", "proofBindingSource.binding", "key-site", "key:RecordKey", "8824e9a950f7d92ddbef2315938ec7702ec93814be24f067c6b8df1009ff4835"}, |
| {"internal/host/proofbinding.go", "type proofBinding", "key-site", "key:RecordKey", "418f61bc5a4192d99821b315baeb1a5b4619593303f8a02e9b13ef824003a613"}, |
diff --git a/internal/provision/testdata/plan14-root-reads-seed.tsv b/internal/provision/testdata/plan14-root-reads-seed.tsv| index ec2dd7d86..235f17ca5 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 5 overlay; pending Paul’s census ruling. |
| +# Files seed checkpoint 6 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. |
| @@ -29,9 +29,9 @@ internal/host/claimservingretire.go servingInventoryPair authority C,J4 rev3=PRO |
| internal/host/claimservingretire.go servingInventoryTransition authority C,J4 rev3=PROPOSED-seed; claim writer identity and complete local inventory; changes only database/marker pair and preserves other paths and boxes 2f4024d4d05b348e4d2b9e2fa4e25bbda69575fa8ac495c4f986444527425d48 |
| internal/host/claimservingretire.go servingPredecessor authority C,J4 rev3=PROPOSED-seed; retained local cold admission at the strictly older installation stamp; exact generation, box and region e5d5534da88e3bbbc051c8f3c19c9cfd14918d84bd6a0206faf87a2a612c3a57 |
| internal/host/claimservingretire.go servingRetirementID authority C,J4 rev3=PROPOSED-seed; immutable predecessor and original successor tuples; deterministic journal identity independent of claim operation IDs c984974d17e621d7fe9c276a03ae789b86789abbccbb82a09d06f55c3bf13819 |
| -internal/host/claimservingretire.go servingWriterIdentity authority C,J4 rev3=PROPOSED-seed; authoritative serving holder and unique cold admission; exact local executor, stamp, session, generation, incarnation, location and region file:Exec@403 ae2fa116ece3bbd596baa02971ef69fba597ce2ab4135abb6bf9caae6d326202 |
| +internal/host/claimservingretire.go servingWriterIdentity authority C,J4 rev3=PROPOSED-seed; authoritative serving holder and unique cold admission; exact local executor, stamp, session, generation, incarnation, location and region file:Exec@403 f7d8c868ce9856eab448aa72bdf20ee02cb70179ce0f435d45888f72e0c3b7bf |
| internal/hostbox/servingpark.go LaunchJournal.RootSessionLaunches authority C,J4 rev3=PROPOSED-seed; root-local persisted launch records for exact host/incarnation/stamp/session/box/enrollment; refuses agent journal transport cbbb9378bc53ac499972bed5fb654e05856a9afd7256d55323a717140ffcba18 |
| -internal/host/claimservingpark_linux.go newServingParkFS authority C,J4 rev3=PROPOSED-seed; root-only filesystem dependencies, fixed nosymfollow validation and private state store; no production caller 9a72f9bb7c62e85199e303411b42e01b0b016a2a5cf2d9c84e2588b5f36b52e2 |
| +internal/host/claimservingpark_linux.go newServingParkFS authority C,J4 rev3=PROPOSED-seed; root-only filesystem dependencies, fixed nosymfollow validation and private state store; bound through private root service construction 9a72f9bb7c62e85199e303411b42e01b0b016a2a5cf2d9c84e2588b5f36b52e2 |
| internal/host/claimservingpark_linux.go sameServingParkFile authority C,J4 rev3=PROPOSED-seed; recorded basename, presence, mount, device, inode, regular type, single link and allocated-byte identity d5d684356e793dcfd55fe1ec9c609f2320e0155e052caca42475348075e8fd68 |
| internal/host/claimservingpark_linux.go servingParkDirID authority C,J4 rev3=PROPOSED-seed; descriptor statx directory type, device, inode and mount identity file:Statx@102 3480dad6d65fedcc17e678df38cf13352ad3c4a8a9d0a09a395973d92b98094e |
| internal/host/claimservingpark_linux.go servingParkFS.Capture authority C,J4 rev3=PROPOSED-seed; canonical nested source directory, four fixed basenames, source mount and bounded generation-marker bytes through the captured descriptor file:Fstat@920,file:Openat@694,file:ReadAll@1138 c3a9348ddd9548f3fcfdcd45abc42b6a758a6399ed34e92481785e26c110763d |
| @@ -54,18 +54,18 @@ internal/host/claimservingpark_other.go servingParkFS.Move authority C,J4 rev3=P |
| internal/host/claimservingpark_other.go servingParkFS.Prepare authority C,J4 rev3=PROPOSED-seed; non-Linux refusal or construction of refusal-only filesystem; no filesystem authority file:Prepare@22 645770372357707f635351a377e943e78f1c160222385e41f42b80d57adedc29 |
| internal/host/claimservingpark_records.go servingParkDecode authority C,J4 rev3=PROPOSED-seed; strict private-record JSON with unknown-field and trailing-value refusal 189c7944ea9f5d269d39ed6a869b6bd739e3033195f06cc1f2c9f23a23180184 |
| 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.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 through typed journal completion 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; 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 |
| internal/host/claimservingpark_records.go servingParkLedger.slot authority C,J4 rev3=PROPOSED-seed; private box/host slot and pending retirement identities across incarnation changes; last-retirement identity supports discarded/artifact-free completion replay file:Read@179 239b806b2e2bfdd568f5d6b749d299f0bc7b118d675e872429432591a89b7b68 |
| -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.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, 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/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; 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 |
| @@ -75,8 +75,8 @@ 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 948bdc0ad5d699a917ce834c2e4f48c44486d72a5c689c5e9da9b748c88fe486 |
| -@coverage internal/hostbox 72c9b640d98c779a044579f70a103a68cd9819c794a0bb3e53f378d39ad11419 |
| +@coverage internal/host d9e2304b55fd9f7653139cc17d2254ef7ec730b2e8c812a9c8f343603f2f6aa6 |
| +@coverage internal/hostbox d48f1a63963edb2551230477c8162f7ac182761c80571c3d753fe19dd7acacba |
| @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 |
| @@ -93,3 +93,30 @@ internal/host/claimservingpark_records.go servingParkLedger.parkWithEvidenceLock |
| 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 |
| +internal/host/claimassembly.go NewProductionRootClaimStop authority C,J4 rev3=PROPOSED-seed; verified root assembly, actor and managed roots; binds private serving-park runtime and retained session-zero stop evidence; agent serving construction remains refused 7debdfe43ca6f17e2229c8aa8b566ee22112d86a595a66fb0cb0213253854aa8 |
| +internal/host/claimjournal_transport.go ClaimRootHTTP.serveJournal authority C,J4 rev3=PROPOSED-seed; typed journal envelope; routes v4 serving-park selectors before generic journal replay; existing operations retain their dispatch file:Read@3311,whole:Route@4465 51276984eeb10a9480b9ff1a92ed8f16e9a141134cb3913ec75ff18c03570868 |
| +internal/host/claimservingpark_other.go newServingParkAuthority authority C,J4 rev3=PROPOSED-seed; local root journal, guard, authentication, actor and empty-bootstrap dependencies; Linux fixes root ownership and nosymfollow validation, non-Linux refuses 5abda60daa2346c10d93bd92b4f10b470d792d2b3021b1a077734fd10c541efc |
| +internal/host/claimservingpark_transport.go ClaimRootHTTP.serveServingPark authority C,J4 rev3=PROPOSED-seed; authenticated edge connection and census actor; only identity selectors reach root authority; completed replay rechecks census and enrollment; pressure unavailable 6d5783a57cc3a7ee3c8303b9b205e5f5ccf0fd606f1e73e3bf4e69092f5085f4 |
| +internal/host/claimservingpark_transport.go newServingParkRuntime authority C,J4 rev3=PROPOSED-seed; private state and platform-specific root authority; constructs bounded service lifetime without starting workers c432ba5861dbb9b10bf102216cdeb5eb9b2cea21b547779a981a91a071d1dfc8 |
| +internal/host/claimservingpark_transport.go servingParkRuntime.Close authority C,J4 rev3=PROPOSED-seed; active-call count and cancellation state; cancels and joins requests before closing private state, preserving it on timeout 03776c15bc8d12062d84794817a681210d23e131a5c30706bc48682c382da0be |
| +internal/host/claimservingpark_transport.go servingParkRuntime.begin authority C,J4 rev3=PROPOSED-seed; in-process shutdown state; registers each request and cancels it with the service lifetime b5ec7229c514152b70acdb34840b66eefcd12d8d250de140aac1703a973a70e6 |
| +internal/host/claimservingpark_transport.go servingParkRuntime.retainNoProcess authority C,J4 rev3=PROPOSED-seed; root coordinator observation and kernel no-process result; lifetime-bound retention before receipt publication 5ef08963d95589e3653e48d85ea12081a761be1901263303134eeb6696ed760f |
| +internal/host/claimservingpark_transport.go withServingParkRoot authority C,J4 rev3=PROPOSED-seed; private rootstate constructor and empty-bootstrap authority; binds retention callback and closes state on construction failure 1ef9ae1b2325c6fe809fef8d1f82b04828cb00e9cc1dd76858c0731f01543265 |
| +internal/host/claimservingparkroot_linux.go newServingParkAuthority authority C,J4 rev3=PROPOSED-seed; local root journal, guard, authentication, actor and empty-bootstrap dependencies; Linux fixes root ownership and nosymfollow validation, non-Linux refuses 728ae9a258bfcd70dd29cb4c26cea705faee6bcd49558b5336a99acd1311dc83 |
| +internal/host/rootexecution_runtime.go claimExecutionRuntime.Close authority C,J4 rev3=PROPOSED-seed; existing execution monitor lifetime plus serving-park lifetime; both share caller deadline before authority stores close 08352d84ef3a8cd55924338abc3d33229f2216a608c0daef707aec8c472e2a8e |
| +internal/hostbox/journalservice.go NewJournalService authority C,J4 rev3=PROPOSED-seed; root-local SQLite launch journal; existing schema and durable receipts plus a cancellable single-request parking gate file:Exec@1218,file:Exec@1551,file:Exec@1692,file:Exec@823,file:Open@1834,file:QueryRow@1037,file:QueryRow@1373 1cdff9ce046394bd609b41f01bb0c86164e6513b44b8d2485f5efbea79b0235c |
| +internal/hostbox/journalservice.go journalPrepare authority C,J4 rev3=PROPOSED-seed; per-call nonce and issue time; selects v4 for parking identities without changing prior encoders file:Read@184,file:Read@333 c8d747d257f0ad48a7bfc645de0a9c50a05fec4c333a75df2a1360d9de9fb9f1 |
| +internal/hostbox/journalservice.go journalRequestHash authority C,J4 rev3=PROPOSED-seed; frozen versioned request tuples; v4 includes every parking selector while v1 through v3 remain unchanged aa4856b2927d21a68cd14a360b09a6d02057afadbe71fffbfee682aef3719ac4 |
| +internal/hostbox/journalservice.go validateJournalRequest authority C,J4 rev3=PROPOSED-seed; typed versioned envelope; rejects parking fields on older versions and delegates v4 to strict selector validation 2d05c14a0afd5bbe4b5dd9e03e7eb885329caec62fb360e02256183d2894d351 |
| +internal/hostbox/servingpark_service.go JournalService.ExecuteServingPark authority C,J4 rev3=PROPOSED-seed; exact journal request and authenticated enrollment; durable pending receipt before filesystem effects, completed reply after effects; uncertain outcomes retain request df93855480924b39cf49edbeedd136799a7e327860ee4dec7742796d3d428333 |
| +internal/hostbox/servingpark_service.go JournalService.prepareServingPark authority C,J4 rev3=PROPOSED-seed; SQLite peer, digest, reply and retention boundary; exact completed replay, bounded request age and clock-rollback rejection before callback; existing bounded receipt pruning file:QueryRowContext@1050,file:QueryRowContext@331 83f1bae59de484a9d1cae09ce8e359aa173634c091203ea3ce4f6e8639e20d5e |
| +internal/hostbox/servingpark_transport.go ServingParkClient.Complete authority C,J4 rev3=PROPOSED-seed; root retirement ID and successor tuple; no caller completion flag, filesystem path or deadline 355c7f3cc975b38456636038f2adead7c742fc08f423125e563d80ffae2f55aa |
| +internal/hostbox/servingpark_transport.go ServingParkClient.Park authority C,J4 rev3=PROPOSED-seed; successor and predecessor identities; requests generation retirement through the existing journal socket 00404df6666b8d57754598ae0ad6b7f6e3669144f538450482a873feed844840 |
| +internal/hostbox/servingpark_transport.go ServingParkClient.ParkLegacy authority C,J4 rev3=PROPOSED-seed; successor and legacy selector identities; requests legacy authority without caller source paths or stop receipts 47785478f59b6d495d877b202fd944efb93ace142fdf4aefd657e4eaee2cc1ec |
| +internal/hostbox/servingpark_transport.go ServingParkClient.ReplayUncertain authority C,J4 rev3=PROPOSED-seed; retained exact request and typed response; every refusal or malformed success stays uncertain 91a00ae50efce496d39459b40e0e7500cfbda3d327c0726593d5e415793f1601 |
| +internal/hostbox/servingpark_transport.go ServingParkClient.request authority C,J4 rev3=PROPOSED-seed; typed identity-only payload, per-call immutable nonce and issue time; validates reply identity and preserves exact uncertain request 280bb92c1572f0e716d7ecc4852555799ef07b240e1607e58e4e922d3b731458 |
| +internal/hostbox/servingpark_transport.go ServingParkOperation authority C,J4 rev3=PROPOSED-seed; fixed typed operation names; no generic rename or unlink operation 4b49ba85dab96b58f2992417ab539523aea8f5abb65981acf4e2d806f6d24f69 |
| +internal/hostbox/servingpark_transport.go journalServingParkV4 authority C,J4 rev3=PROPOSED-seed; explicit frozen identity and selector tuple for v4 request hashing d5c2405c66384a349002bdd5856d5d1af5c1fdd5c506b9389a8f9cf5f74504e9 |
| +internal/hostbox/servingpark_transport.go servingParkResponse authority C,J4 rev3=PROPOSED-seed; typed retirement reply and exact requested successor; missing or mismatched result stays uncertain fc8602dfe28b78ee04809509b1357778777b26c191b089a47d70defbca8ffd88 |
| +internal/hostbox/servingpark_transport.go validateServingParkRequest authority C,J4 rev3=PROPOSED-seed; v4 envelope, nonce, digest and identities; rejects mixed operation fields and unsupported pressure before any effect 2c87ae130afc670925c87f52b9688fdc24d940d0a5fcdbf09667df2b662f7f59 |
| +internal/host/claimservingpark_transport.go var openClaimServingParkAuthority authority C,J4 rev3=PROPOSED-seed; descriptor-relative private rootstate at /var/lib/carlos-claims/authority/serving-parks with creation enabled and public reads disabled file:Open@78 7a1db088a7240e6a65ba0bed96fc5f973a69f70c2273c4475c486a6e0359fb23 |