carlos / platform Public

host: register the C8 files manifest provider under the claim alone (Task 6, platform-1e ruling)

RegisterProductionClaimFiles is the claim assembly's one-shot seam for
the C8 provider, separate from the moves owners (platform-1e,
2026-10-12): Establish runs on every claim-on serving wake, so a
claim-only box must have it. OpenClaimOwners binds the registered
source's provider into Serving.Files with CARLOS_HOST_CLAIM on, moves on
or off; with the claim off, or nothing registered, nothing changes. A
provider the moves source already bound is refused as a wiring error,
never replaced. The source's close joins the bindings' close.

movesFilesSource is the production source: a movesFiles over the
verified actor's data bucket, read by exact version through the claim's
versioned store and written through an S3 store bound to the bucket's
cloud region, loaded through rootaws (#267). The agent registers it
before newClaimWiring with the claim on (one call in agent.go, beside
the moves parks registration; the isolated-exec branch also touches
agent.go, so the edit is kept to that call).

moveCfg.FilesDir and the source share movesFilesDirFor.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 43f8b6e8b9936f66ba247edbaa336db9647fb0a6 parent 5f3eaae
5 files changed, +339 −3
  • internal/host/agent.go +6 −0
  • internal/host/claimfiles.go +145 −0
  • internal/host/claimfiles_test.go +174 −0
  • internal/host/claimproduction.go +13 −0
  • internal/host/movespass.go +1 −3
diff --git a/internal/host/agent.go b/internal/host/agent.go
index 287985cbc..1625c0ee2 100644
--- a/internal/host/agent.go
+++ b/internal/host/agent.go
@@ -287,6 +287,12 @@ func runAgent(args []string, buildClaims ClaimFactory) {
if err := registerProductionMovesParks(claimCtx, claimEntryEnv(claimCtx), claimStore(claimCtx), *cfg, productionRosterRegion(claimStore(claimCtx), os.Getenv), movesRefusalLog); err != nil {
log.Printf("agent: moves: the park providers are not registered: %v", err)
}
+ // The C8 files manifest provider (moves Task 6) registers under the
+ // claim switch alone: every claim-on serving wake establishes its files
+ // manifest through it (claimfiles.go).
+ if err := registerProductionClaimFiles(claimEntryEnv(claimCtx), dd); err != nil {
+ log.Printf("agent: host claims: the files manifest provider is not registered: %v", err)
+ }
claims, err := newClaimWiring(claimCtx, claimEntryEnv(claimCtx), buildClaims)
if err != nil {
log.Fatalf("agent: host claims: %v", err)
diff --git a/internal/host/claimfiles.go b/internal/host/claimfiles.go
new file mode 100644
index 000000000..43f2d2851
--- /dev/null
+++ b/internal/host/claimfiles.go
@@ -0,0 +1,145 @@
+package host
+
+import (
+ "context"
+ "errors"
+ "path/filepath"
+ "sync"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ awsconfig "github.com/aws/aws-sdk-go-v2/config"
+ awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostwrite"
+ "github.com/carlosframework/platform/internal/lease"
+ "github.com/carlosframework/platform/internal/park"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/rootaws"
+ "github.com/carlosframework/platform/internal/s3compat"
+)
+
+// ClaimProductionFilesSource is the C8 files-manifest provider's production
+// source (platform-1e, 2026-10-12): registered under CARLOS_HOST_CLAIM
+// alone, apart from the moves owners, because Establish runs on every
+// claim-on serving wake and a claim-only box would otherwise refuse every
+// one (claimFilesRefusal). Open returns the provider for the verified box
+// actor. It grants no local process, roster or policy authority: the
+// provider sees only the observation, the launch inputs and the start's
+// captured work the serving owners hand it.
+type ClaimProductionFilesSource interface {
+ Open(context.Context, release.Store, lease.VersionedStore, hostbox.Actor) (ClaimFilesManifest, error)
+ Close(context.Context) error
+}
+
+var claimFilesSources struct {
+ sync.Mutex
+ Source ClaimProductionFilesSource
+}
+
+// RegisterProductionClaimFiles registers the C8 files source once at
+// process startup. A second registration, or a nil one, is refused: it can
+// neither replace a provider nor add a permissive fallback.
+func RegisterProductionClaimFiles(s ClaimProductionFilesSource) error {
+ claimFilesSources.Lock()
+ defer claimFilesSources.Unlock()
+ if s == nil || claimFilesSources.Source != nil {
+ return hostwrite.ErrUnavailable
+ }
+ claimFilesSources.Source = s
+ return nil
+}
+
+func productionClaimFilesSource() ClaimProductionFilesSource {
+ claimFilesSources.Lock()
+ defer claimFilesSources.Unlock()
+ return claimFilesSources.Source
+}
+
+// openClaimFilesManifest binds the registered source's provider into
+// out.Serving.Files with CARLOS_HOST_CLAIM on, moves on or off. With the
+// claim off, or nothing registered, out is unchanged and nothing is
+// opened. A provider the moves source already bound is a wiring error
+// (two providers), refused rather than replaced. The returned close is the
+// source's, or nil when nothing was opened.
+func openClaimFilesManifest(ctx context.Context, getenv func(string) string, source ClaimProductionFilesSource, store release.Store, data lease.VersionedStore, actor hostbox.Actor, out *ClaimOwnerBindings) (func(context.Context) error, error) {
+ if source == nil || getenv == nil || out == nil || !hostwrite.Enabled(getenv) {
+ return nil, nil
+ }
+ if out.Serving.Files != nil {
+ return nil, errors.Join(hostwrite.ErrUnavailable, errors.New("host claim: two C8 files manifest providers: the moves source bound one and a files source is registered"))
+ }
+ files, err := source.Open(ctx, store, data, actor)
+ if err != nil {
+ return nil, err
+ }
+ if files == nil {
+ return nil, errors.Join(hostwrite.ErrUnavailable, errors.New("host claim: the files source opened no provider"))
+ }
+ out.Serving.Files = files
+ return source.Close, nil
+}
+
+// movesFilesSource is the production ClaimProductionFilesSource: a
+// movesFiles over the verified actor's data bucket, read by exact version
+// through the claim's versioned store and written through a files store
+// bound to the bucket's cloud region. No pin reader is wired on this
+// branch (region HA's ReadPin is not on main), so Evidence is nil and a
+// host with no manifest anywhere in its chain refuses every serving wake
+// until it is.
+type movesFilesSource struct {
+ DataDir string
+ newFiles func(ctx context.Context, bucket, region string) (release.Store, error)
+}
+
+// movesFilesDirFor is the host's files directory and env file under
+// dataDir: <dataDir>/<host> and <dataDir>/<host>.env, the layout
+// moveCfg.FilesDir names.
+func movesFilesDirFor(dataDir string) func(host string) (string, string) {
+ return func(host string) (string, string) {
+ return filepath.Join(dataDir, host), filepath.Join(dataDir, host+".env")
+ }
+}
+
+func (s *movesFilesSource) Open(ctx context.Context, _ release.Store, data lease.VersionedStore, actor hostbox.Actor) (ClaimFilesManifest, error) {
+ region := park.PlacementRegion(actor.Region)
+ if s == nil || s.DataDir == "" || s.newFiles == nil || data == nil || actor.Bucket == "" || actor.Box == "" || region == "" || actor.Cloud.Region == "" {
+ return nil, hostwrite.ErrUnavailable
+ }
+ files, err := s.newFiles(ctx, actor.Bucket, actor.Cloud.Region)
+ if err != nil {
+ return nil, err
+ }
+ return &movesFiles{Files: files, Data: data, Bucket: actor.Bucket, HomeRegion: actor.HomeRegion, Region: region, Box: actor.Box, FilesDir: movesFilesDirFor(s.DataDir)}, nil
+}
+
+func (s *movesFilesSource) Close(context.Context) error { return nil }
+
+// newProductionFilesStore is the data bucket as a release.Store for the
+// files manifests: the S3 store (create-only and compare-and-swap Put)
+// over a client bound to the bucket's region, loaded through rootaws
+// (#267), with the options release.NewS3Store sets.
+func newProductionFilesStore(ctx context.Context, bucket, region string) (release.Store, error) {
+ cfg, err := rootaws.LoadDefaultConfig(ctx, awsconfig.WithRegion(region))
+ if err != nil {
+ return nil, err
+ }
+ client := awss3.NewFromConfig(cfg, s3compat.Options(), func(o *awss3.Options) {
+ o.RequestChecksumCalculation = aws.RequestChecksumCalculationWhenRequired
+ o.ResponseChecksumValidation = aws.ResponseChecksumValidationWhenRequired
+ })
+ return &release.S3Store{Client: client, Bucket: bucket}, nil
+}
+
+// registerProductionClaimFiles is the agent's registration, before
+// newClaimWiring: with CARLOS_HOST_CLAIM on, the production files source
+// over this box's data directory. Off, it registers nothing and the claim
+// keeps its named refusal (claimFilesRefusal).
+func registerProductionClaimFiles(getenv func(string) string, dataDir string) error {
+ if getenv == nil || !hostwrite.Enabled(getenv) {
+ return nil
+ }
+ if dataDir == "" {
+ return hostwrite.ErrUnavailable
+ }
+ return RegisterProductionClaimFiles(&movesFilesSource{DataDir: dataDir, newFiles: newProductionFilesStore})
+}
diff --git a/internal/host/claimfiles_test.go b/internal/host/claimfiles_test.go
new file mode 100644
index 000000000..7fc82a8a7
--- /dev/null
+++ b/internal/host/claimfiles_test.go
@@ -0,0 +1,174 @@
+package host
+
+import (
+ "context"
+ "errors"
+ "path/filepath"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostwrite"
+ "github.com/carlosframework/platform/internal/lease"
+ "github.com/carlosframework/platform/internal/lease/leasetest"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// fakeFilesSource is a C8 files source that records its opens and closes.
+type fakeFilesSource struct {
+ opens, closes int
+ actor hostbox.Actor
+ err error
+ files ClaimFilesManifest
+}
+
+func (s *fakeFilesSource) Open(_ context.Context, _ release.Store, _ lease.VersionedStore, actor hostbox.Actor) (ClaimFilesManifest, error) {
+ s.opens++
+ s.actor = actor
+ return s.files, s.err
+}
+
+func (s *fakeFilesSource) Close(context.Context) error { s.closes++; return nil }
+
+func withClaimFilesSource(t *testing.T, s ClaimProductionFilesSource) {
+ t.Helper()
+ claimFilesSources.Lock()
+ prev := claimFilesSources.Source
+ claimFilesSources.Source = s
+ claimFilesSources.Unlock()
+ t.Cleanup(func() {
+ claimFilesSources.Lock()
+ claimFilesSources.Source = prev
+ claimFilesSources.Unlock()
+ })
+}
+
+// The C8 provider registers once at process start, under
+// CARLOS_HOST_CLAIM alone (platform-1e, 2026-10-12): a nil source and a
+// second registration are refused, and neither can replace the first.
+func TestRegisterProductionClaimFilesIsOneShot(t *testing.T) {
+ withClaimFilesSource(t, nil)
+ if err := RegisterProductionClaimFiles(nil); !errors.Is(err, hostwrite.ErrUnavailable) {
+ t.Fatalf("nil source: %v", err)
+ }
+ first := &fakeFilesSource{files: &movesFiles{}}
+ if err := RegisterProductionClaimFiles(first); err != nil {
+ t.Fatal(err)
+ }
+ if err := RegisterProductionClaimFiles(&fakeFilesSource{}); !errors.Is(err, hostwrite.ErrUnavailable) {
+ t.Fatalf("second registration: %v", err)
+ }
+ if productionClaimFilesSource() != ClaimProductionFilesSource(first) {
+ t.Fatal("the registered source is not the first")
+ }
+}
+
+// openClaimFilesManifest binds the registered source's provider into the
+// serving sources with the claim on and moves off or on; with the claim
+// off, or nothing registered, the bindings are untouched and nothing is
+// opened. A provider the moves source already bound is a wiring error,
+// never silently replaced; an Open error or a nil provider refuses.
+func TestOpenClaimFilesManifestBindsUnderTheClaimAlone(t *testing.T) {
+ store := release.NewDirStore(t.TempDir())
+ data := leasetest.NewVersionedBucket()
+ actor := hostbox.Actor{Box: "box-1", Region: "ie", Bucket: "data", HomeRegion: "ie", Enrollment: "enr-1"}
+ claimOn := func(k string) string {
+ if k == "CARLOS_HOST_CLAIM" {
+ return "1"
+ }
+ return ""
+ }
+ bothOn := func(k string) string {
+ if k == "CARLOS_HOST_CLAIM" || k == "CARLOS_MOVES_PHASE1" {
+ return "1"
+ }
+ return ""
+ }
+ off := func(string) string { return "" }
+
+ provider := &movesFiles{}
+ for name, getenv := range map[string]func(string) string{"claim alone": claimOn, "both": bothOn} {
+ src := &fakeFilesSource{files: provider}
+ var out ClaimOwnerBindings
+ closeFn, err := openClaimFilesManifest(context.Background(), getenv, src, store, data, actor, &out)
+ if err != nil || closeFn == nil {
+ t.Fatalf("%s: %v %v", name, err, closeFn != nil)
+ }
+ if out.Serving.Files != ClaimFilesManifest(provider) || src.opens != 1 || src.actor != actor {
+ t.Fatalf("%s: bound %v, opens %d, actor %+v", name, out.Serving.Files, src.opens, src.actor)
+ }
+ if err := closeFn(context.Background()); err != nil || src.closes != 1 {
+ t.Fatalf("%s: close %v %d", name, err, src.closes)
+ }
+ }
+
+ // Claim off: untouched, not opened.
+ src := &fakeFilesSource{files: provider}
+ var out ClaimOwnerBindings
+ closeFn, err := openClaimFilesManifest(context.Background(), off, src, store, data, actor, &out)
+ if err != nil || closeFn != nil || out.Serving.Files != nil || src.opens != 0 {
+ t.Fatalf("claim off: %v %v %v %d", err, closeFn != nil, out.Serving.Files, src.opens)
+ }
+ // Nothing registered: untouched.
+ if closeFn, err := openClaimFilesManifest(context.Background(), claimOn, nil, store, data, actor, &out); err != nil || closeFn != nil || out.Serving.Files != nil {
+ t.Fatalf("nothing registered: %v %v %v", err, closeFn != nil, out.Serving.Files)
+ }
+ // Already bound by the moves source: refused, not replaced.
+ already := &movesFiles{Box: "other"}
+ out = ClaimOwnerBindings{Serving: ClaimServingSources{Files: already}}
+ src = &fakeFilesSource{files: provider}
+ if _, err := openClaimFilesManifest(context.Background(), claimOn, src, store, data, actor, &out); !errors.Is(err, hostwrite.ErrUnavailable) || out.Serving.Files != ClaimFilesManifest(already) || src.opens != 0 {
+ t.Fatalf("two providers: %v %v %d", err, out.Serving.Files, src.opens)
+ }
+ // An Open error refuses; a nil provider refuses.
+ out = ClaimOwnerBindings{}
+ src = &fakeFilesSource{err: errors.New("no bucket")}
+ if _, err := openClaimFilesManifest(context.Background(), claimOn, src, store, data, actor, &out); err == nil || out.Serving.Files != nil {
+ t.Fatalf("open error: %v %v", err, out.Serving.Files)
+ }
+ src = &fakeFilesSource{}
+ if _, err := openClaimFilesManifest(context.Background(), claimOn, src, store, data, actor, &out); !errors.Is(err, hostwrite.ErrUnavailable) || out.Serving.Files != nil {
+ t.Fatalf("nil provider: %v %v", err, out.Serving.Files)
+ }
+}
+
+// The production source opens a movesFiles over the verified actor: the
+// data bucket by exact version, the files store for that bucket in its
+// cloud region, this box's engine region and the home alias, and the
+// host's files directory and env file under the data directory. No pin
+// reader is wired on this branch, so Evidence is nil.
+func TestProductionFilesSourceOpensOverTheActor(t *testing.T) {
+ data := leasetest.NewVersionedBucket()
+ var gotBucket, gotRegion string
+ src := &movesFilesSource{DataDir: "/data", newFiles: func(_ context.Context, bucket, region string) (release.Store, error) {
+ gotBucket, gotRegion = bucket, region
+ return release.NewDirStore(t.TempDir()), nil
+ }}
+ actor := hostbox.Actor{Box: "box-1", Region: "", HomeRegion: "ie", Bucket: "data-ie", Cloud: hostbox.CloudIdentity{Region: "eu-west-1"}}
+ got, err := src.Open(context.Background(), release.NewDirStore(t.TempDir()), data, actor)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f, ok := got.(*movesFiles)
+ if !ok {
+ t.Fatalf("%T", got)
+ }
+ if gotBucket != "data-ie" || gotRegion != "eu-west-1" || f.Bucket != "data-ie" || f.Box != "box-1" || f.HomeRegion != "ie" || f.Region != "@home" || f.Data == nil || f.Files == nil || f.Evidence != nil {
+ t.Fatalf("provider: %+v (%s %s)", f, gotBucket, gotRegion)
+ }
+ dir, env := f.FilesDir("shop.example.com")
+ if dir != filepath.Join("/data", "shop.example.com") || env != filepath.Join("/data", "shop.example.com.env") {
+ t.Fatalf("files dir %s env %s", dir, env)
+ }
+ // An actor without a bucket, box or region cannot be opened.
+ if _, err := src.Open(context.Background(), release.NewDirStore(t.TempDir()), data, hostbox.Actor{Box: "box-1"}); !errors.Is(err, hostwrite.ErrUnavailable) {
+ t.Fatalf("bare actor: %v", err)
+ }
+ // Registration happens only with the claim on.
+ withClaimFilesSource(t, nil)
+ if err := registerProductionClaimFiles(func(string) string { return "" }, "/data"); err != nil || productionClaimFilesSource() != nil {
+ t.Fatalf("claim off registered: %v %v", err, productionClaimFilesSource())
+ }
+ if err := registerProductionClaimFiles(func(k string) string { return "1" }, "/data"); err != nil || productionClaimFilesSource() == nil {
+ t.Fatalf("claim on: %v", err)
+ }
+}
diff --git a/internal/host/claimproduction.go b/internal/host/claimproduction.go
index eb0bb6850..d00fc6416 100644
--- a/internal/host/claimproduction.go
+++ b/internal/host/claimproduction.go
@@ -195,9 +195,19 @@ func (ProductionClaimOwnerProvider) OpenClaimOwners(ctx context.Context, store r
return out, err
}
}
+ // The C8 files manifest provider (moves Task 6; platform-1e ruling of
+ // 2026-10-12): registered under CARLOS_HOST_CLAIM alone, apart from the
+ // moves owners, because Establish runs on every claim-on serving wake.
+ closeFiles, err := openClaimFilesManifest(ctx, os.Getenv, productionClaimFilesSource(), store, out.Data, actor, &out)
+ if err != nil {
+ return out, err
+ }
// Region HA's relocation owners (claim Plan 11), with CARLOS_REGION_HA on.
closeRelocation, err := openClaimRelocationOwners(ctx, os.Getenv, productionClaimRelocationSource(), store, out.Data, actor, &out)
if err != nil {
+ if closeFiles != nil {
+ err = errors.Join(err, closeFiles(ctx))
+ }
return out, err
}
bindClaimLegacyOwners(&out)
@@ -214,6 +224,9 @@ func (ProductionClaimOwnerProvider) OpenClaimOwners(ctx context.Context, store r
if closeRelocation != nil {
err = errors.Join(err, closeRelocation(ctx))
}
+ if closeFiles != nil {
+ err = errors.Join(err, closeFiles(ctx))
+ }
if external != nil {
return errors.Join(err, external.Close(ctx))
}
diff --git a/internal/host/movespass.go b/internal/host/movespass.go
index 8aabbb275..5da309685 100644
--- a/internal/host/movespass.go
+++ b/internal/host/movespass.go
@@ -440,9 +440,7 @@ func armPhase1(cfg *moveCfg, w *movesWorker, hib activator.Config, dataDir strin
cfg.SecretsKey, cfg.SecretsPriv, cfg.SecretsPub = raw, priv, priv.PublicKey()
}
}
- cfg.FilesDir = func(host string) (string, string) {
- return filepath.Join(dataDir, host), filepath.Join(dataDir, host+".env")
- }
+ cfg.FilesDir = movesFilesDirFor(dataDir)
// The cut (movescut.go).
cfg.FinalCheckpoint = finalCheckpoint(hib.Bucket, hib.Prefix, hib.Region)
cfg.LocalPosition = localLTXPosition