carlos / platform Public

host: C8 reads the claim's data bucket through the identity-bound RequestStore (whole-branch fixes, item 5)

OpenClaimOwners passed the raw versioned store to the files source, so
C8's version listings and exact-version reads lacked the per-request
deadline and identity check the serving and moves owners have; this
reaches every claim-on serving wake. openClaimFilesManifest now takes
the box's identity check and wraps the data bucket in
hostbox.RequestStore before opening the source, as the moves wake does;
OpenClaimOwners passes claimBoundIdentity. Without a store or a check
the provider is not opened. Task 8's wake-source tests pass a
pass-through check for their injected stores.

Regression: the provider's bucket is the request store; a listing
carries the request-limit deadline; a refused identity issues no bucket
call; an establishment under a refused identity refuses with ErrChanged
and writes nothing.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev ebeaf954585c39a14ad2f05c7200804c72d4fe5d parent 1f45953
5 files changed, +88 −12
  • internal/host/claimfiles.go +13 −2
  • internal/host/claimfiles_test.go +71 −6
  • internal/host/claimproduction.go +1 −1
  • internal/host/moveswakesource_linux_test.go +1 −1
  • internal/host/moveswakesource_test.go +2 −2
diff --git a/internal/host/claimfiles.go b/internal/host/claimfiles.go
index d214b86af..153ddc682 100644
--- a/internal/host/claimfiles.go
+++ b/internal/host/claimfiles.go
@@ -61,14 +61,25 @@ func productionClaimFilesSource() ClaimProductionFilesSource {
// 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) {
+//
+// The provider reads the claim's data bucket as the serving and moves
+// owners do (whole-branch fixes, item 5): through hostbox.RequestStore
+// over data, so every version listing and exact-version read is bounded
+// by the claim's request limit, runs under the wake's own context, and is
+// preceded by check, the box's bound identity (claimBoundIdentity); a
+// refused identity issues no bucket call. Without a check the provider is
+// not opened.
+func openClaimFilesManifest(ctx context.Context, getenv func(string) string, source ClaimProductionFilesSource, store release.Store, data lease.VersionedStore, actor hostbox.Actor, out *ClaimOwnerBindings, check func(context.Context) error) (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 data == nil || check == nil {
+ return nil, errors.Join(hostwrite.ErrUnavailable, errors.New("host claim: the C8 files manifest provider needs the claim's data bucket and the box's identity check"))
+ }
+ files, err := source.Open(ctx, store, hostbox.RequestStore{Store: data, Check: check}, actor)
if err != nil {
return nil, err
}
diff --git a/internal/host/claimfiles_test.go b/internal/host/claimfiles_test.go
index 7fc82a8a7..922ce3f3e 100644
--- a/internal/host/claimfiles_test.go
+++ b/internal/host/claimfiles_test.go
@@ -5,6 +5,7 @@ import (
"errors"
"path/filepath"
"testing"
+ "time"
"github.com/carlosframework/platform/internal/hostbox"
"github.com/carlosframework/platform/internal/hostwrite"
@@ -85,11 +86,12 @@ func TestOpenClaimFilesManifestBindsUnderTheClaimAlone(t *testing.T) {
}
off := func(string) string { return "" }
+ pass := func(context.Context) error { return nil }
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)
+ closeFn, err := openClaimFilesManifest(context.Background(), getenv, src, store, data, actor, &out, pass)
if err != nil || closeFn == nil {
t.Fatalf("%s: %v %v", name, err, closeFn != nil)
}
@@ -104,33 +106,96 @@ func TestOpenClaimFilesManifestBindsUnderTheClaimAlone(t *testing.T) {
// Claim off: untouched, not opened.
src := &fakeFilesSource{files: provider}
var out ClaimOwnerBindings
- closeFn, err := openClaimFilesManifest(context.Background(), off, src, store, data, actor, &out)
+ closeFn, err := openClaimFilesManifest(context.Background(), off, src, store, data, actor, &out, pass)
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 {
+ if closeFn, err := openClaimFilesManifest(context.Background(), claimOn, nil, store, data, actor, &out, pass); 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 {
+ if _, err := openClaimFilesManifest(context.Background(), claimOn, src, store, data, actor, &out, pass); !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 {
+ if _, err := openClaimFilesManifest(context.Background(), claimOn, src, store, data, actor, &out, pass); 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 {
+ if _, err := openClaimFilesManifest(context.Background(), claimOn, src, store, data, actor, &out, pass); !errors.Is(err, hostwrite.ErrUnavailable) || out.Serving.Files != nil {
t.Fatalf("nil provider: %v %v", err, out.Serving.Files)
}
}
+// C8 reads the claim's data bucket as the serving and moves owners do
+// (whole-branch fixes, item 5): through hostbox.RequestStore, so every
+// version listing and exact-version read it makes is bounded by the
+// claim's request limit and preceded by the box's identity check, a
+// refused identity issuing no bucket call. An establishment under a
+// refused identity refuses with hostbox.ErrChanged and writes nothing.
+func TestClaimFilesReadTheBucketThroughTheRequestStore(t *testing.T) {
+ inner := &mwBucket{VersionedBucket: leasetest.NewVersionedBucket()}
+ inner.AddBucket("data")
+ var refuse error
+ checks := 0
+ check := func(context.Context) error { checks++; return refuse }
+ src := &movesFilesSource{DataDir: t.TempDir(), newFiles: func(context.Context, string, string) (release.Store, error) {
+ return casBucketStore{b: inner.VersionedBucket, bucket: "data"}, nil
+ }}
+ actor := hostbox.Actor{Box: "uk-box", Region: "uk", Bucket: "data", HomeRegion: "ie", Cloud: hostbox.CloudIdentity{Region: "eu-west-2"}}
+ var out ClaimOwnerBindings
+ out.Data = inner
+ closeFn, err := openClaimFilesManifest(t.Context(), func(k string) string { return "1" }, src, release.NewDirStore(t.TempDir()), inner, actor, &out, check)
+ if err != nil || closeFn == nil {
+ t.Fatalf("open: %v", err)
+ }
+ f, ok := out.Serving.Files.(*movesFiles)
+ if !ok {
+ t.Fatalf("%T", out.Serving.Files)
+ }
+ if _, ok := f.Data.(hostbox.RequestStore); !ok {
+ t.Fatalf("C8's bucket is %T, not the claim's request store", f.Data)
+ }
+ before := time.Now()
+ if _, err := f.Data.ListVersions(t.Context(), "data", "instances/"); err != nil {
+ t.Fatal(err)
+ }
+ if inner.lists != 1 || checks != 1 || inner.deadlines[0].IsZero() || inner.deadlines[0].After(time.Now().Add(hostbox.RequestLimit)) || inner.deadlines[0].Before(before.Add(hostbox.RequestLimit-time.Minute)) {
+ t.Fatalf("the request's deadline %v (lists %d, checks %d), want the request limit", inner.deadlines, inner.lists, checks)
+ }
+ refuse = hostbox.ErrChanged
+ if _, err := f.Data.ListVersions(t.Context(), "data", "instances/"); !errors.Is(err, hostbox.ErrChanged) || inner.lists != 1 {
+ t.Fatalf("a refused identity: %v, %d bucket calls", err, inner.lists)
+ }
+ if _, err := f.Data.GetVersion(t.Context(), "data", "k", "v"); !errors.Is(err, hostbox.ErrChanged) {
+ t.Fatalf("a refused identity's read: %v", err)
+ }
+ // An establishment under a refused identity: the chain's first listing
+ // refuses, nothing is written.
+ x := &filesFixture{t: t, bucket: inner.VersionedBucket, files: casBucketStore{b: inner.VersionedBucket, bucket: "data"}, dir: src.DataDir}
+ x.f = f
+ f.FilesDir = func(host string) (string, string) {
+ return filepath.Join(x.dir, host), filepath.Join(x.dir, host+".env")
+ }
+ x.writeEnv("K=1\n")
+ o := servingObserved("g2", "g1:uk:@root:uk", "g2:uk:g1:uk")
+ if err := f.Establish(t.Context(), o, launchInputs("g2"), x); !errors.Is(err, hostbox.ErrChanged) {
+ t.Fatalf("establish under a refused identity: %v", err)
+ }
+ if _, found := x.manifest("g2"); found {
+ t.Fatal("a manifest was written under a refused identity")
+ }
+ if inner.lists != 1 {
+ t.Fatalf("%d bucket calls, want none from the refused establishment", inner.lists)
+ }
+}
+
// 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
diff --git a/internal/host/claimproduction.go b/internal/host/claimproduction.go
index 210da496c..aeca20c10 100644
--- a/internal/host/claimproduction.go
+++ b/internal/host/claimproduction.go
@@ -199,7 +199,7 @@ func (ProductionClaimOwnerProvider) OpenClaimOwners(ctx context.Context, store r
// 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)
+ closeFiles, err := openClaimFilesManifest(ctx, os.Getenv, productionClaimFilesSource(), store, out.Data, actor, &out, claimBoundIdentity(out.Identity.Verify, actor))
if err != nil {
return out, err
}
diff --git a/internal/host/moveswakesource_linux_test.go b/internal/host/moveswakesource_linux_test.go
index dbbb00e5d..15df1ebbe 100644
--- a/internal/host/moveswakesource_linux_test.go
+++ b/internal/host/moveswakesource_linux_test.go
@@ -149,7 +149,7 @@ func mwRegister(t *testing.T, f *poFixture, box, region string, files ClaimFiles
if err != nil || opened != nil {
t.Fatalf("the registered source's Open: %v (files %v)", err, opened)
}
- if _, err := openClaimFilesManifest(ctx, env, &fakeFilesSource{files: files}, f.claims, f.bucket, actor, &out); err != nil {
+ if _, err := openClaimFilesManifest(ctx, env, &fakeFilesSource{files: files}, f.claims, f.bucket, actor, &out, func(context.Context) error { return nil }); err != nil {
t.Fatal(err)
}
w.owners, w.files = out.Moves, out.Serving.Files
diff --git a/internal/host/moveswakesource_test.go b/internal/host/moveswakesource_test.go
index 9d4411ebe..ee55ae21b 100644
--- a/internal/host/moveswakesource_test.go
+++ b/internal/host/moveswakesource_test.go
@@ -335,7 +335,7 @@ func TestRegisteredSourceOpensTheMovesOwnersWithTheClaimsFiles(t *testing.T) {
// owners alike: one value.
out.Moves = owners
provider := &movesFiles{}
- closeFiles, err := openClaimFilesManifest(ctx, env, &fakeFilesSource{files: provider}, store, data, actor, &out)
+ closeFiles, err := openClaimFilesManifest(ctx, env, &fakeFilesSource{files: provider}, store, data, actor, &out, func(context.Context) error { return nil })
if err != nil || closeFiles == nil {
t.Fatalf("files: %v", err)
}
@@ -344,7 +344,7 @@ func TestRegisteredSourceOpensTheMovesOwnersWithTheClaimsFiles(t *testing.T) {
}
// Another provider for the same owners is refused, never swapped in.
again := ClaimOwnerBindings{Moves: owners}
- if _, err := openClaimFilesManifest(ctx, env, &fakeFilesSource{files: &movesFiles{}}, store, data, actor, &again); !errors.Is(err, hostwrite.ErrUnavailable) || m.Files != ClaimFilesManifest(provider) {
+ if _, err := openClaimFilesManifest(ctx, env, &fakeFilesSource{files: &movesFiles{}}, store, data, actor, &again, func(context.Context) error { return nil }); !errors.Is(err, hostwrite.ErrUnavailable) || m.Files != ClaimFilesManifest(provider) {
t.Fatalf("a second provider: %v, files %v", err, m.Files)
}
// Without the read access, or the secrets key, Open refuses: the wake