carlos / platform Public

WIP files seed checkpoint 6 (not for merge): root transport and lifetime wiring

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 0f33d460c21b85f08217b0b2734bb079ee16d867 parent d67fdb5
19 files changed, +1155 −48
  • internal/host/claimassembly.go +12 −0
  • internal/host/claimjournal_transport.go +4 −0
  • internal/host/claimretire_transport.go +1 −0
  • internal/host/claimservingartifact.go +3 −3
  • internal/host/claimservinglegacyroot_linux_test.go +46 −3
  • internal/host/claimservingpark_other.go +4 −0
  • internal/host/claimservingpark_records.go +3 −3
  • internal/host/claimservingpark_transport.go +187 −0
  • internal/host/claimservingpark_transport_linux_test.go +349 −0
  • internal/host/claimservingparkroot_linux.go +13 −4
  • internal/host/claimservingretire.go +4 −19
  • internal/host/claimservingretire_linux_test.go +1 −1
  • internal/host/rootexecution_runtime.go +4 −0
  • internal/hostbox/journalservice.go +20 −3
  • internal/hostbox/servingpark_service.go +108 −0
  • internal/hostbox/servingpark_service_test.go +203 −0
  • internal/hostbox/servingpark_transport.go +153 −0
  • internal/provision/plan9_b8_census_test.go +5 −4
  • internal/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