carlos / platform Public

plan: claim Plan 15 rev 2 (batch origin proof in one traversal, O(N) with a counting witness at N=K=1000; bounded node reads; replay against moves' Task 8 integration with a real C8 provider)

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
codex agent, operated by paul@keymail.dev pushed by claude agent, operated by paul@keymail.dev c357902ee208fa455eae4385702ff2d4b78c3064 parent 176ab5a
1 file changed, +881 −142
  • docs/superpowers/plans/2026-10-12-host-lifecycle-claim-15-admitted-provenance.md +881 −142

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	155.438s
ok  	github.com/carlosframework/platform/cmd/console	2.222s
ok  	github.com/carlosframework/platform/internal/activator	55.668s
ok  	github.com/carlosframework/platform/internal/agentsock	0.016s
ok  	github.com/carlosframework/platform/internal/analytics	0.017s
ok  	github.com/carlosframework/platform/internal/appconfig	0.960s
ok  	github.com/carlosframework/platform/internal/apperr	0.071s
ok  	github.com/carlosframework/platform/internal/applog	0.117s
ok  	github.com/carlosframework/platform/internal/awsinv	0.003s
ok  	github.com/carlosframework/platform/internal/caps	0.011s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.038s
ok  	github.com/carlosframework/platform/internal/clamd	0.053s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	51.899s
?   	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	1.392s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.017s
ok  	github.com/carlosframework/platform/internal/dbverify	0.017s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.017s
ok  	github.com/carlosframework/platform/internal/docsite	0.017s
ok  	github.com/carlosframework/platform/internal/domains	1.054s
ok  	github.com/carlosframework/platform/internal/doorkey	0.017s
ok  	github.com/carlosframework/platform/internal/economics	0.017s
ok  	github.com/carlosframework/platform/internal/edge	23.037s
ok  	github.com/carlosframework/platform/internal/edgesfile	0.016s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.094s
ok  	github.com/carlosframework/platform/internal/gate	1.655s
ok  	github.com/carlosframework/platform/internal/host	6.824s
ok  	github.com/carlosframework/platform/internal/infraview	0.017s
ok  	github.com/carlosframework/platform/internal/instancectl	0.414s
ok  	github.com/carlosframework/platform/internal/instanceexec	0.025s
ok  	github.com/carlosframework/platform/internal/instancefs	0.017s
ok  	github.com/carlosframework/platform/internal/lease	5.090s
ok  	github.com/carlosframework/platform/internal/ledger	0.025s
ok  	github.com/carlosframework/platform/internal/mail	0.059s
ok  	github.com/carlosframework/platform/internal/maintenance	0.093s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.471s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.056s
ok  	github.com/carlosframework/platform/internal/metrics	0.019s
ok  	github.com/carlosframework/platform/internal/objstore	0.090s
ok  	github.com/carlosframework/platform/internal/ops	0.118s
ok  	github.com/carlosframework/platform/internal/privatefile	0.003s
ok  	github.com/carlosframework/platform/internal/provision	0.850s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.013s
ok  	github.com/carlosframework/platform/internal/registry	1.231s
ok  	github.com/carlosframework/platform/internal/relay	7.207s
ok  	github.com/carlosframework/platform/internal/release	3.576s
ok  	github.com/carlosframework/platform/internal/s3compat	0.005s
ok  	github.com/carlosframework/platform/internal/schedule	1.408s
ok  	github.com/carlosframework/platform/internal/status	0.026s
ok  	github.com/carlosframework/platform/internal/steering	0.007s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.006s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.002s
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.531s
=== 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
minio contract: PASS
diff --git a/docs/superpowers/plans/2026-10-12-host-lifecycle-claim-15-admitted-provenance.md b/docs/superpowers/plans/2026-10-12-host-lifecycle-claim-15-admitted-provenance.md
index 015a9f6c7..dd5588cc8 100644
--- a/docs/superpowers/plans/2026-10-12-host-lifecycle-claim-15-admitted-provenance.md
+++ b/docs/superpowers/plans/2026-10-12-host-lifecycle-claim-15-admitted-provenance.md
@@ -1,11 +1,13 @@
# Host lifecycle claim, Plan 15: durable admitted-generation provenance
-Implementation plan, 2026-10-12, rev 1. Based on `origin/main` at
-`8916558d34f68698f488ee7a0aa7f4a55808c380`. Apply Tasks A–E in order.
+Implementation plan, 2026-10-12, rev 2. Based on `origin/main` at
+`650e6152be0af5851b879980f67970382750becd`. Apply Tasks A–E in order.
Task M is a separately pinned moves integration replay. The spec amendment
is rev 6.35. This plan authorizes no commits, production changes or activation.
-Requirements: `/home/paulca/.cache/tmp/claude-1001/own/after236/plan15-brief.md`.
+Requirements: `/home/paulca/.cache/tmp/claude-1001/own/after236/plan15-brief.md`,
+as revised by `plan15-rev2.md` in the same directory. Both the Astra round 1
+and moves reviews named there are binding.
The originating review is
`/home/paulca/.cache/tmp/claude-1001/own/after236/review-moves-task6c.md`.
This plan and its write report are implementation instructions for the
@@ -71,12 +73,21 @@ reuse the head. Nonadjacent duplicates are harmless and count toward the cap.
Exhaustion refuses the next growing admission; it does not truncate history.
This bounds the added claim field, each object, the total committed record
-and decoder memory. A query streams one node at a time, with worst-case
-O(N) reads and O(1) traversal memory. Caller cancellation returns an error.
-Do not silently limit the walk to eight entries or treat a deadline as
-absence. Batch proof or an authenticated search index is a future performance
-improvement; it must retain this authority contract. Before activation,
-measure proof latency at the deployment's expected retained history size.
+and decoder memory. A batch authenticates one observation and traverses the
+chain once, reading every node at most once. It answers all requested tuples,
+including both verified home-name forms, with O(N) node GETs and O(K)
+membership memory. Canonicalizing the tuple set takes O(K log K) local work.
+There is no per-origin or per-alias restart. Cancellation and any invalid or
+unreadable node return an error with no partial answers. Before activation,
+measure latency at the deployment's expected retained history size; O(N)
+requests are still a deployment sizing constraint.
+
+Both reachable-node reads and candidate-conflict verification require
+`release.Opener`, read through `io.LimitReader` at 4097 bytes, close the body,
+and refuse anything above 4096 bytes. They never call `Store.Get` or
+`OpenObject`, whose fallback buffers the entire object. A store without a
+streaming opener refuses. The 4097th byte is the over-limit witness; a claimed
+Content-Length is not trusted.
Uncommitted candidates from failed CAS attempts are outside the committed
record. This plan adds no candidate garbage collector. Retries with the same
@@ -147,16 +158,25 @@ A malformed or missing referenced node refuses; it never falls back to hints.
```go
// internal/hostclaim. Claims is the authoritative deployment store.
+type OriginTuple struct {
+ Incarnation, Location, Region, Generation string
+}
+func AdmittedOrigins(ctx context.Context, claims release.Store,
+ observed Observed, set []OriginTuple) (map[OriginTuple]bool, error)
+func (*OriginProofCache) Proven(ctx context.Context, claims release.Store,
+ observed Observed, set []OriginTuple) (map[OriginTuple]bool, error)
+// AdmittedOrigin remains the singleton convenience form.
func AdmittedOrigin(ctx context.Context, claims release.Store,
observed Observed, incarnation, location, region, generation string) (bool, error)
-// internal/host. Implements movesOriginProof without depending on its branch.
+// internal/host. Task M changes movesOriginProof to the batch contract.
type ClaimOriginProof struct {
Claims release.Store
+ Cache *hostclaim.OriginProofCache
HomeRegion, Region string
}
func (ClaimOriginProof) Proven(context.Context, hostclaim.Observed,
- hostclaim.Admission, string) (bool, error)
+ hostclaim.Admission, []string) (map[string]bool, error)
```
The query compares the stored claim's ETag and bytes with the observation.
@@ -170,16 +190,27 @@ Moves refuses on either false or error; it does not substitute the manifest's
The library compares regions exactly. `ClaimOriginProof` queries the verified
home roster name and `@home` only when its configured region is that verified
home placement. A foreign region remains foreign. The adapter gets HomeRegion
-from the verified actor, never from a manifest. An error while proving one
-alias does not fall through to a permissive alias.
-
-Moves' `movesFilesAllowed` must prove every candidate, every entry/platform
-object's origin and every `Origins` hint through this adapter before forming
-its allowed set. Keep its location and generation syntax checks and the
-subsequent `ValidateManifest`. `Origins` remains a carry-forward hint. A
-prepared generation is proven only from the observation after admission.
-Cross-region prepared materialization writes destination-local objects;
-source-region history does not authorize them in the destination.
+from the verified actor, never from a manifest. All aliases enter the same
+exact tuple set before the walk. An error refuses the whole batch; a missing
+alias adds no second traversal.
+
+`OriginProofCache` holds one completed batch, bound to store identity, host,
+head digest and count, and the exact deduplicated tuple set. Its key also
+contains the observed ETag and complete claim bytes, binding retained legacy
+admissions. Every call reauthenticates the observation before looking up the
+cache. A mismatch, changed head, changed tuple set or error drops the cached
+entry. Noncomparable store identities disable reuse. Returned maps are copies;
+a caller cannot alter cached answers. The cache is bounded to one batch and
+is never used with a changed head. New calls still authenticate the claim even
+when no node reads are needed.
+
+Moves' `movesFilesAllowed` submits the admitted generation and every origin
+that a live entry or platform object references, once, through this adapter.
+Keep its location and generation syntax checks and subsequent
+`ValidateManifest`. `Origins` remains a hint: it cannot prove itself and an
+unused hint is not retained. A prepared generation is proven only from the
+observation after admission. Cross-region materialization writes
+destination-local objects; source-region history does not authorize them.
The proof API may establish historical membership for retired incarnations,
which is useful to an independently authorized reader. A moves start still
@@ -240,10 +271,13 @@ objects, additional Gets or changed generation names are introduced.
4. Old valid origins survive repeated pruning, holder removal and retirement
list loss. Wrong incarnation, location, host and region stay unproven.
5. Real files establishment uses the durable proof after pruning. An additional
- never-admitted `Origins` hint refuses even on a valid current manifest.
-6. The cap refuses growth without pruning, and corrupt/missing nodes refuse.
+ never-admitted live object origin refuses even on a current manifest.
+6. The N=1000, K=1000 wake reads exactly 1000 nodes and one claim; missing
+ aliases add no walk. Oversized reachable and conflicting objects consume
+ exactly 4097 bytes before refusal and close their streams.
+7. The cap refuses growth without pruning, and corrupt/missing nodes refuse.
No list-based fallback, blanket root-prefix permission or copied hint is added.
-7. Keep the independent writer and frozen backfill gates visible. Passing local
+8. Keep the independent writer and frozen backfill gates visible. Passing local
tests does not close either production gate.
## Replay without git writes
@@ -258,7 +292,7 @@ is a dependency of this replay. Verify that spec hash before Task E.
```bash
CLAIM15_REPO=/home/paulca/amadan.net/carlos/platform/.claude/worktrees/replica-ownership
CLAIM15_PLAN="$CLAIM15_REPO/docs/superpowers/plans/2026-10-12-host-lifecycle-claim-15-admitted-provenance.md"
-CLAIM15_BASE=8916558d34f68698f488ee7a0aa7f4a55808c380
+CLAIM15_BASE=650e6152be0af5851b879980f67970382750becd
CLAIM15_SCRATCH=$(mktemp -d "$TMPDIR/claim15.XXXXXX")
export CLAIM15_REPO CLAIM15_PLAN CLAIM15_BASE CLAIM15_SCRATCH
mkdir "$CLAIM15_SCRATCH/tree"
@@ -292,20 +326,26 @@ for body in blocks:
## Task A: Add the durable record, admission hook and reader
Files: `internal/hostclaim/{origins.go,claim.go,generation.go,store.go}` and
-`plan15_origins_test.go`. Preserve the public `AdmitWriter`, `Update` and
+`plan15_origins_test.go` and `plan15_batch_test.go`. Preserve the public `AdmitWriter`, `Update` and
loss-admission signatures. No caller can supply an `OriginHead` to admission.
The node Put is create-only in the same deployment store as the claim.
Focused command: `go test ./internal/hostclaim -run TestPlan15 -count=1`.
-Expected initial failure: `AdmittedOrigin`, `OriginHead` and the private node
-helpers are undefined. Then run the full hostclaim package, including existing
+Expected initial failure: the origin types, cache and query APIs are
+undefined. Against rev 1, the new batch tests also fail to compile. The
+oversize witness can run against rev 1 by selecting its standalone tests;
+the counting store must panic on the unbounded node Get. The mutation
+witnesses below independently check both read sites. Then run the full hostclaim package, including existing
warm/cold, loss, relocation, wake-retry and flag-off transition tests.
The tests cover legacy migration, actual admission pruning, retention across
holder removal, exact scope, unknown origins, warm reuse, caps, cancellation,
malformed counts/heads, raw candidate injection, observation forgery, direct
head edits, node failure, failed/conflicting claim writes, and a committed CAS
-whose reply is lost. They never claim to exercise real IAM permissions.
+whose reply is lost. The batch tests also cover duplicate-set reuse, exact tuple-set changes,
+foreign stores with identical claim bytes, forged and changed heads, stale
+observations and a corrupt tail after an earlier match. They never claim to
+exercise real IAM permissions.
<!-- apply: A tests -->
```diff
@@ -604,13 +644,157 @@ whose reply is lost. They never claim to exercise real IAM permissions.
+ }
+ }
+}
+--- /dev/null
++++ b/internal/hostclaim/plan15_batch_test.go
+@@ -0,0 +1,141 @@
++package hostclaim
++
++import (
++ "context"
++ "errors"
++ "io"
++ "strings"
++ "testing"
++
++ "github.com/carlosframework/platform/internal/release"
++)
++
++type plan15BoundedStore struct {
++ *release.DirStore
++ oversized, conflict bool
++ read, closed, opens, gets int
++}
++
++func (s *plan15BoundedStore) Get(ctx context.Context, key string) ([]byte, string, error) {
++ if strings.HasPrefix(key, "control/host-origins/") {
++ panic("unbounded node Get")
++ }
++ s.gets++
++ return s.DirStore.Get(ctx, key)
++}
++func (s *plan15BoundedStore) Put(ctx context.Context, key string, b []byte, tag string) (string, error) {
++ if s.conflict && strings.HasPrefix(key, "control/host-origins/") {
++ return "", release.ErrCASConflict
++ }
++ return s.DirStore.Put(ctx, key, b, tag)
++}
++
++type plan15Reader struct {
++ io.Reader
++ store *plan15BoundedStore
++}
++
++func (r plan15Reader) Read(b []byte) (int, error) {
++ n, e := r.Reader.Read(b)
++ r.store.read += n
++ return n, e
++}
++func (r plan15Reader) Close() error { r.store.closed++; return nil }
++func (s *plan15BoundedStore) Open(ctx context.Context, key string) (io.ReadCloser, string, error) {
++ s.opens++
++ if s.oversized {
++ return plan15Reader{strings.NewReader(strings.Repeat("x", 1<<20)), s}, "oversized", nil
++ }
++ return s.DirStore.Open(ctx, key)
++}
++func TestPlan15BoundedReachableAndConflicting(t *testing.T) {
++ for _, conflict := range []bool{false, true} {
++ t.Run(map[bool]string{false: "reachable", true: "candidate-conflict"}[conflict], func(t *testing.T) {
++ plan15Enable(t)
++ s := &plan15BoundedStore{DirStore: release.NewDirStore(t.TempDir())}
++ o := servingClaim(t, t.Context(), s)
++ if !conflict {
++ o = updateGenerationTest(t, t.Context(), s, o, plan15Admit(o, "real", "ie"))
++ }
++ s.oversized, s.conflict = true, conflict
++ s.opens, s.closed, s.read = 0, 0, 0
++ var err error
++ if conflict {
++ _, err = Update(t.Context(), s, o, plan15Admit(o, "candidate", "ie"))
++ } else {
++ h := o.Claim.Holder
++ _, err = AdmittedOrigin(t.Context(), s, o, h.Incarnation, h.Location, "ie", "real")
++ }
++ if !errors.Is(err, ErrMalformed) || s.read != 4097 || s.closed != 1 || s.opens != 1 {
++ t.Fatalf("err=%v bytes=%d closes=%d opens=%d", err, s.read, s.closed, s.opens)
++ }
++ })
++ }
++}
++func TestPlan15CacheBindings(t *testing.T) {
++ plan15Enable(t)
++ s := &plan15BoundedStore{DirStore: release.NewDirStore(t.TempDir())}
++ o := servingClaim(t, t.Context(), s)
++ o = updateGenerationTest(t, t.Context(), s, o, plan15Admit(o, "first", "ie"))
++ h := o.Claim.Holder
++ first := OriginTuple{h.Incarnation, h.Location, "ie", "first"}
++ cache := new(OriginProofCache)
++ prove := func(obs Observed, set []OriginTuple, want bool) {
++ t.Helper()
++ result, err := cache.Proven(t.Context(), s, obs, set)
++ if err != nil || result[set[0]] != want {
++ t.Fatalf("%v %v", result, err)
++ }
++ }
++ s.opens, s.gets = 0, 0
++ prove(o, []OriginTuple{first, first}, true)
++ prove(o, []OriginTuple{first}, true)
++ if s.opens != int(o.Claim.Origins.Count) || s.gets != 2 {
++ t.Fatalf("cache opens=%d reads=%d", s.opens, s.gets)
++ }
++ different := first
++ different.Region = "uk"
++ prove(o, []OriginTuple{different}, false)
++ if s.opens != 2*int(o.Claim.Origins.Count) {
++ t.Fatal("tuple set reused")
++ }
++ other := &plan15BoundedStore{DirStore: release.NewDirStore(t.TempDir())}
++ key, _ := Key(o.Host)
++ body, _, err := s.Get(t.Context(), key)
++ if err != nil {
++ t.Fatal(err)
++ }
++ if _, err := other.Put(t.Context(), key, body, ""); err != nil {
++ t.Fatal(err)
++ }
++ if got, err := cache.Proven(t.Context(), other, o, []OriginTuple{different}); got != nil || err == nil {
++ t.Fatal("foreign store reused")
++ }
++ prove(o, []OriginTuple{first}, true)
++ edited := o
++ c := *o.Claim
++ edited.Claim = &c
++ c.Origins = &OriginHead{Digest: strings.Repeat("f", 64), Count: o.Claim.Origins.Count}
++ if got, err := cache.Proven(t.Context(), s, edited, []OriginTuple{first}); got != nil || !errors.Is(err, ErrConflict) {
++ t.Fatal("edited head reused")
++ }
++ prove(o, []OriginTuple{first}, true)
++ newer := updateGenerationTest(t, t.Context(), s, o, plan15Admit(o, "second", "ie"))
++ if got, err := cache.Proven(t.Context(), s, o, []OriginTuple{first}); got != nil || !errors.Is(err, ErrConflict) {
++ t.Fatal("stale observation reused")
++ }
++ s.opens = 0
++ prove(newer, []OriginTuple{first}, true)
++ if s.opens != int(newer.Claim.Origins.Count) {
++ t.Fatal("changed head did not traverse")
++ }
++ // No partial positives escape a failed batch, including a corrupt tail.
++ if err := s.Delete(t.Context(), originKey(o.Host, o.Claim.Origins.Digest)); err != nil {
++ t.Fatal(err)
++ }
++ second := first
++ second.Generation = "second"
++ if got, err := cache.Proven(t.Context(), s, newer, []OriginTuple{first, second}); got != nil || err == nil {
++ t.Fatal("partial proof escaped")
++ }
++}
```
<!-- apply: A implementation -->
```diff
--- /dev/null
+++ b/internal/hostclaim/origins.go
-@@ -0,0 +1,284 @@
+@@ -0,0 +1,389 @@
+package hostclaim
+
+import (
@@ -622,7 +806,10 @@ whose reply is lost. They never claim to exercise real IAM permissions.
+ "errors"
+ "io"
+ "os"
++ "reflect"
++ "sort"
+ "strings"
++ "sync"
+
+ "github.com/carlosframework/platform/internal/release"
+)
@@ -636,13 +823,15 @@ whose reply is lost. They never claim to exercise real IAM permissions.
+ Count uint64 `json:"count"`
+}
+
-+type originTuple struct {
++type OriginTuple struct {
+ Incarnation string `json:"incarnation"`
+ Location string `json:"location"`
+ Region string `json:"region"`
+ Generation string `json:"generation"`
+}
+
++type originTuple = OriginTuple
++
+type originNode struct {
+ V int `json:"v"`
+ Host string `json:"host"`
@@ -713,7 +902,7 @@ whose reply is lost. They never claim to exercise real IAM permissions.
+ return n, err
+ }
+ key := originKey(host, h.Digest)
-+ b, _, err := s.Get(ctx, key)
++ b, err := readOriginBytes(ctx, s, key)
+ if err != nil {
+ return n, err
+ }
@@ -828,7 +1017,7 @@ whose reply is lost. They never claim to exercise real IAM permissions.
+ return err
+ }
+ if _, err = s.Put(ctx, key, body, ""); errors.Is(err, release.ErrCASConflict) {
-+ existing, _, readErr := s.Get(ctx, key)
++ existing, readErr := readOriginBytes(ctx, s, key)
+ if readErr != nil {
+ return readErr
+ }
@@ -852,48 +1041,148 @@ whose reply is lost. They never claim to exercise real IAM permissions.
+ return *a == *b
+}
+
-+// AdmittedOrigin proves one exact storage tuple. The caller uses a fresh claim
-+// observation from the authoritative deployment store, not the regional data
-+// bucket. No region alias is inferred here; pass the recorded engine name.
-+func AdmittedOrigin(ctx context.Context, s release.Store, obs Observed, incarnation, location, region, generation string) (bool, error) {
-+ want := originTuple{incarnation, location, region, generation}
-+ if s == nil || obs.Absent || obs.Claim == nil || obs.ETag == "" || obs.Host != obs.Claim.Host || !want.valid(obs.Host) {
-+ return false, ErrMalformed
++// OriginProofCache retains one exact batch, after authenticating its observation.
++// Stores with noncomparable identities are never cached. Zero value is usable.
++type OriginProofCache struct {
++ mu sync.Mutex
++ store release.Store
++ key string
++ result map[OriginTuple]bool
++}
++
++func readOriginBytes(ctx context.Context, s release.Store, key string) ([]byte, error) {
++ if err := ctx.Err(); err != nil {
++ return nil, err
++ }
++ // OpenObject's Get fallback would allocate an unbounded object.
++ opener, ok := s.(release.Opener)
++ if !ok {
++ return nil, ErrMalformed
++ }
++ reader, _, err := opener.Open(ctx, key)
++ if err != nil {
++ return nil, err
++ }
++ b, readErr := io.ReadAll(io.LimitReader(reader, originNodeLimit+1))
++ closeErr := reader.Close()
++ if readErr != nil {
++ return nil, readErr
++ }
++ if len(b) > originNodeLimit {
++ return nil, ErrMalformed
++ }
++ if closeErr != nil {
++ return nil, closeErr
++ }
++ if err := ctx.Err(); err != nil {
++ return nil, err
++ }
++ return b, nil
++}
++
++// AdmittedOrigins authenticates once and proves the complete requested set in
++// one walk. It publishes no partial results on a malformed or unreadable chain.
++func AdmittedOrigins(ctx context.Context, s release.Store, obs Observed, set []OriginTuple) (map[OriginTuple]bool, error) {
++ return new(OriginProofCache).Proven(ctx, s, obs, set)
++}
++
++func (cache *OriginProofCache) Proven(ctx context.Context, s release.Store, obs Observed, set []OriginTuple) (map[OriginTuple]bool, error) {
++ cache.mu.Lock()
++ defer cache.mu.Unlock()
++ invalidate := func() { cache.store, cache.key, cache.result = nil, "", nil }
++ if err := ctx.Err(); err != nil {
++ invalidate()
++ return nil, err
++ }
++ if s == nil || obs.Absent || obs.Claim == nil || obs.ETag == "" || obs.Host != obs.Claim.Host {
++ invalidate()
++ return nil, ErrMalformed
++ }
++ wanted := make(map[OriginTuple]bool, len(set))
++ for _, tuple := range set {
++ if !tuple.valid(obs.Host) {
++ invalidate()
++ return nil, ErrMalformed
++ }
++ wanted[tuple] = false
+ }
+ fresh, err := Read(ctx, s, obs.Host)
+ if err != nil {
-+ return false, err
++ invalidate()
++ return nil, err
+ }
+ before, marshalErr := json.Marshal(obs.Claim)
+ stored, storedErr := json.Marshal(fresh.Claim)
+ if fresh.Absent || fresh.ETag != obs.ETag || marshalErr != nil || storedErr != nil || !bytes.Equal(before, stored) {
-+ return false, ErrConflict
++ invalidate()
++ return nil, ErrConflict
+ }
-+ // The holder binding and head both come from the unchanged stored observation.
++ if err := ctx.Err(); err != nil {
++ invalidate()
++ return nil, err
++ }
++ // The store identity, host, head digest/count and exact canonical tuple set
++ // are all in the cache key. ETag and the full body also bind legacy admissions.
++ tuples := make([]string, 0, len(wanted))
++ for tuple := range wanted {
++ b, _ := json.Marshal(tuple)
++ tuples = append(tuples, string(b))
++ }
++ sort.Strings(tuples)
++ keyBytes, _ := json.Marshal(struct {
++ Host, ETag, Claim string
++ Set []string
++ }{obs.Host, obs.ETag, string(stored), tuples})
++ key := string(keyBytes)
++ comparable := reflect.TypeOf(s).Comparable()
++ if comparable && cache.store == s && cache.key == key {
++ for tuple := range wanted {
++ wanted[tuple] = cache.result[tuple]
++ }
++ return wanted, nil
++ }
++ invalidate()
+ c := fresh.Claim
+ if c.Origins == nil {
-+ h := c.Holder
-+ if h == nil || h.Incarnation != incarnation || h.Location != location {
-+ return false, nil
++ if h := c.Holder; h != nil {
++ for _, a := range h.Admissions {
++ tuple := OriginTuple{h.Incarnation, h.Location, a.Region, a.Generation}
++ if _, ok := wanted[tuple]; ok {
++ wanted[tuple] = true
++ }
++ }
+ }
-+ for _, a := range h.Admissions {
-+ if a.Generation == generation && a.Region == region {
-+ return true, nil
++ } else {
++ for head := *c.Origins; head.Count > 0; {
++ node, err := readOrigin(ctx, s, obs.Host, head)
++ if err != nil {
++ return nil, err
++ }
++ if _, ok := wanted[node.Origin]; ok {
++ wanted[node.Origin] = true
+ }
++ head = OriginHead{Digest: node.Previous, Count: head.Count - 1}
+ }
-+ return false, nil
+ }
-+ for head := *c.Origins; head.Count > 0; {
-+ node, err := readOrigin(ctx, s, obs.Host, head)
-+ if err != nil {
-+ return false, err
-+ }
-+ if node.Origin == want {
-+ return true, nil
++ if err := ctx.Err(); err != nil {
++ return nil, err
++ }
++ if comparable {
++ cache.store, cache.key = s, key
++ cache.result = make(map[OriginTuple]bool, len(wanted))
++ for tuple, proven := range wanted {
++ cache.result[tuple] = proven
+ }
-+ head = OriginHead{Digest: node.Previous, Count: head.Count - 1}
+ }
-+ return false, nil
++ return wanted, nil
++}
++
++// AdmittedOrigin is the singleton convenience form; multi-origin callers use
++// AdmittedOrigins or one OriginProofCache.Proven call for their entire set.
++func AdmittedOrigin(ctx context.Context, s release.Store, obs Observed, incarnation, location, region, generation string) (bool, error) {
++ tuple := OriginTuple{incarnation, location, region, generation}
++ proven, err := AdmittedOrigins(ctx, s, obs, []OriginTuple{tuple})
++ return proven[tuple], err
+}
--- a/internal/hostclaim/claim.go
+++ b/internal/hostclaim/claim.go
@@ -1081,8 +1370,9 @@ unchanged-head write so a blanket denial cannot satisfy them.
Add the adapter as a separate file so it compiles on the pinned main even
though `movesfiles.go` has not landed there. Its method set is compile-checked
-against the moves interface. The test creates and admits a real claim, checks
-exact and verified home-region reads, and refuses a free-standing next generation.
+against the batch interface that Task M installs. The test creates and admits
+a real claim, checks exact and verified home-region reads, and refuses a
+free-standing next generation.
Focused command: `go test ./internal/host -run TestPlan15 -count=1`.
Expected first failure: `ClaimOriginProof` is undefined. Task M below binds it
@@ -1106,7 +1396,7 @@ establishment. This main-based adapter alone is not a production wiring claim.
+)
+
+var _ interface {
-+ Proven(context.Context, hostclaim.Observed, hostclaim.Admission, string) (bool, error)
++ Proven(context.Context, hostclaim.Observed, hostclaim.Admission, []string) (map[string]bool, error)
+} = ClaimOriginProof{}
+
+func TestPlan15MovesProof(t *testing.T) {
@@ -1139,8 +1429,8 @@ establishment. This main-based adapter alone is not a production wiring claim.
+ {"ie", "uk", "admitted", false}, {"", "ie", "prepared", false},
+ } {
+ p := ClaimOriginProof{Claims: s, HomeRegion: tc.home, Region: tc.region}
-+ got, err := p.Proven(t.Context(), o, hostclaim.Admission{NewGeneration: tc.generation}, tc.generation)
-+ if got != tc.want || err != nil {
++ got, err := p.Proven(t.Context(), o, hostclaim.Admission{NewGeneration: tc.generation}, []string{tc.generation})
++ if got[tc.generation] != tc.want || err != nil {
+ t.Fatalf("%+v: %v %v", tc, got, err)
+ }
+ }
@@ -1151,7 +1441,7 @@ establishment. This main-based adapter alone is not a production wiring claim.
```diff
--- /dev/null
+++ b/internal/host/claimoriginproof.go
-@@ -0,0 +1,39 @@
+@@ -0,0 +1,52 @@
+package host
+
+import (
@@ -1167,12 +1457,13 @@ establishment. This main-based adapter alone is not a production wiring claim.
+// Admission.NewGeneration is deliberately not evidence of admission.
+type ClaimOriginProof struct {
+ Claims release.Store
++ Cache *hostclaim.OriginProofCache
+ HomeRegion, Region string
+}
+
-+func (p ClaimOriginProof) Proven(ctx context.Context, o hostclaim.Observed, _ hostclaim.Admission, generation string) (bool, error) {
++func (p ClaimOriginProof) Proven(ctx context.Context, o hostclaim.Observed, _ hostclaim.Admission, generations []string) (map[string]bool, error) {
+ if o.Absent || o.Claim == nil || o.Claim.Holder == nil || p.Region == "" {
-+ return false, hostclaim.ErrWrongState
++ return nil, hostclaim.ErrWrongState
+ }
+ h := o.Claim.Holder
+ regions := []string{p.Region}
@@ -1183,13 +1474,25 @@ establishment. This main-based adapter alone is not a production wiring claim.
+ regions = append(regions, regionname.HomeRegion)
+ }
+ }
-+ for _, region := range regions {
-+ ok, err := hostclaim.AdmittedOrigin(ctx, p.Claims, o, h.Incarnation, h.Location, region, generation)
-+ if err != nil || ok {
-+ return ok, err
++ set := make([]hostclaim.OriginTuple, 0, len(regions)*len(generations))
++ for _, generation := range generations {
++ for _, region := range regions {
++ set = append(set, hostclaim.OriginTuple{Incarnation: h.Incarnation, Location: h.Location, Region: region, Generation: generation})
+ }
+ }
-+ return false, nil
++ cache := p.Cache
++ if cache == nil {
++ cache = new(hostclaim.OriginProofCache)
++ }
++ proven, err := cache.Proven(ctx, p.Claims, o, set)
++ if err != nil {
++ return nil, err
++ }
++ result := make(map[string]bool, len(generations))
++ for _, tuple := range set {
++ result[tuple.Generation] = result[tuple.Generation] || proven[tuple]
++ }
++ return result, nil
+}
```
@@ -1233,7 +1536,7 @@ Command after the patch:
{"internal/hostclaim/lossrecord.go", "ReplaceLossRecord", "foreign-recordkey", "key:LossRecordKey,sel:Put", "576589457e4a18b64e6b56892603051982bd01770c355504305e72d53149bc52"},
- {"internal/hostclaim/store.go", "Update", "nonrecord", "sel:Put", "68f97a49f9a936228cb26e49d7bed3a4ab17c3a4c4255e32c2d2af1dfccd73a6"},
+ {"internal/hostclaim/origins.go", "originTuple.valid", "key-site", "lit:instances", "7f6f20f51e51694d665588f0b986def85bda4789f24623ee1993289265b01355"},
-+ {"internal/hostclaim/origins.go", "commitOrigins", "nonrecord", "sel:Put", "56a6eb6b4c9e67acccfb8f2b97ce5e338e21c56ab226359b12f3aacd75cd7f7f"},
++ {"internal/hostclaim/origins.go", "commitOrigins", "nonrecord", "sel:Put", "f5f506d8b1e5d4dced896c6be3a6e498354e5f28d81452131e220fec58e8bd78"},
+ {"internal/hostclaim/store.go", "Update", "nonrecord", "sel:Put", "ec5de8702797ce97a24147cbf16ad8f8e254e2da82d3a0859c0407501f0b575f"},
{"internal/hostevidence/signature.go", "immutable", "nonrecord", "sel:Put", "54fb2bdc230c2cb4193f3b808004b63bcfd092266846fc0c03e61617c3ad0ab5"},
{"internal/hostexec/controller.go", "Controller.run", "nonrecord", "sel:Put", "03df11b375761730700ce30c0db05c57f0828629eda0a5de126ea1a2f2f7501c"},
@@ -1263,7 +1566,7 @@ moves API, plus the credential-separation and backfill activation prerequisites.
@@ -1,5 +1,7 @@
# The host lifecycle claim
-+**Rev 6.35 (2026-10-12):** retains admitted-generation provenance beyond the admission pruning window. Claim Plan 15 defines the bounded digest-linked record, exact origin query, migration refusals and independent claim-writer activation prerequisite.
++**Rev 6.35 (2026-10-12):** retains admitted-generation provenance beyond the admission pruning window. Claim Plan 15 defines the bounded digest-linked record, batched origin queries, bounded streaming reads, migration refusals and independent claim-writer activation prerequisite.
+
**Rev 6.34 (2026-10-12):** separates root fence, execution and release authority from service-writable DataDir state; requires guarded migration and independent root provenance. Claim Plan 14 addresses amadan #280.
@@ -1277,7 +1580,7 @@ moves API, plus the credential-separation and backfill activation prerequisites.
6.5 added the R6-4 rule. Rev 6.6 added writer epochs (R7-1, amadan #225).
Rev 6.7 answered the moves session's notes on §6.4a. Rev 6.8 rewrote §6.4a
around writer sessions, for joint round 8. Rev 6.10 answers joint round 10
-@@ -2066,6 +2068,90 @@
+@@ -2066,6 +2068,102 @@
the operation that admitted the crashed writer has since been replaced
(R9-2).
@@ -1303,19 +1606,30 @@ moves API, plus the credential-separation and backfill activation prerequisites.
+new head. Candidate nodes are create-only before that CAS. Candidates
+left by a crash or conflict have no authority until a committed head
+reaches them. A conflicting candidate is reusable only after byte-exact
-+verification. Head removal, substitution and rollback by another claim
++verification. Both node and conflict reads require a streaming opener, read
++at most 4097 bytes, close the body, and refuse bodies above 4096 bytes;
++there is no fallback to an unbounded Get. Head removal, substitution and rollback by another claim
+transition refuse. A commit timeout is an unknown result: reread the
+claim; do not grant authority from a candidate upload acknowledgement.
+
-+`hostclaim.AdmittedOrigin(ctx, claims, observed, incarnation, location,
-+region, generation)` reads the authoritative claim at the observed ETag
-+and verifies hashes, host and descending counts on the path to the
-+requested origin. A match grants provenance only, not permission to
-+execute, write files, bypass stop evidence, or read another incarnation.
-+Unknown returns false; malformed, missing or unreadable reachable nodes
-+return an error. Both mean refuse. A changed ETag requires a fresh
-+observation. Region comparison is exact; the moves adapter may query the
-+verified home roster name and `@home`, and no other alias.
++`hostclaim.AdmittedOrigins(ctx, claims, observed, tuples)` authenticates one
++claim observation at its ETag, then traverses the committed chain once for
++all exact tuples. Every visited node is checked for its hash, host, shape
++and descending count; an invalid or unreadable chain returns no partial
++results. The singleton `AdmittedOrigin` remains available. A match grants
++provenance only, not permission to execute, write files, bypass stop evidence,
++or read another incarnation. Unknown returns false; errors and false both
++refuse. A changed ETag requires a fresh observation.
++
++The moves adapter's `Proven` takes the entire generation set, expanding
++verified home-name and `@home` counterparts before that single traversal.
++A failed alias adds no second walk. A one-entry cache is scoped by store
++identity, host, head digest/count and the exact tuple set, with ETag and the
++claim body also bound. Each call authenticates the observation before cache
++lookup; a changed observation or error invalidates it. With N nodes and K
++requested tuples, a wake performs N node reads and one claim read, with
++O(K) membership memory. K includes the admitted generation and live manifest
++references; unused hints do not add authority or proof work.
+
+Migration seeds only `AdmissionRecord.Generation` and its `Region` from
+retained admissions of the observed holder, bound to that holder's exact
@@ -1326,8 +1640,9 @@ moves API, plus the credential-separation and backfill activation prerequisites.
+retained admission entries. Empty legacy root generations have no
+`files/g-<generation>/` origin. Missing pre-window entries remain unproven.
+
-+Every origin carried in a files manifest, including `Origins` hints and
-+entries/platform objects, must be re-proven at establishment. The prepared
++Every origin referenced by a live manifest entry or platform object, and
++the generation being established, must be re-proven at establishment.
++`Origins` is a hint; unused hints are discarded and never confer authority. The prepared
+generation becomes an admitted origin only after the admission CAS.
+Cross-region imports re-copy verified bytes into the admitted destination
+prefix; a source-region grant never proves a destination-region origin.
@@ -1373,54 +1688,95 @@ moves API, plus the credential-separation and backfill activation prerequisites.
Verify the result contains `Design, 2026-10-12, **rev 6.35**`, the new
`Durable admitted origins (rev 6.35)` section, and Plan 14's P6-D9 section.
Expected result SHA-256:
-`d22efa116cd3385a9bdd42cfa0ede6f1246fcb80711585e7ee32912732e30174`.
-
-## Task M: Integrate with the separately pinned C8 branch
-
-This task has an explicit second code input: moves tip
-`6bb9d4171e5e4fcf7e2130a89ed403c88bf4c5bf`, read from
-`/home/paulca/amadan.net/carlos/platform-moves-p5f`. Export it to another
-scratch subdirectory. All existing production files changed by A and B are
-byte-identical at that tip and the required main base; verify this before
-applying A, B and C to the export. Do not overlay the whole moves tree onto
-main or modify the moves worktree. Apply M tests and implementation here,
-then N's branch-specific census patch. D's original line positions belong
-to the main replay; N carries the same five classifications at this tip.
-
-Inputs: `internal/host/movesfiles.go` SHA-256
-`1ce801cfaf50468f9ee0443af7530dfbcc86e90c89a076c2c43aa2c0cd9ad581`,
-and `internal/host/claimfiles.go` SHA-256
-`fba6daebee74d7237dc525dca727fbb74cdc43457fb9c5930c99cb3d78cd8795`.
-
-The existing `movesOriginProof.Proven` signature matches Task C. Keep
-`movesFilesAllowed`'s proof of every candidate, entry and `Origins` hint.
-Change `movesFilesSource.Open` to retain its existing deployment-store argument
-and set `Proof: ClaimOriginProof{Claims: claims, HomeRegion: actor.HomeRegion,
-Region: region}`. A nil claim store refuses construction. The manifest/data
-store remains the actor's regional bucket; never pass that store as Claims.
+`62bc39d05b8ef0989bc7d26a57754e1af35a4bdef96608b7a1886e0770b4344a`.
+
+## Task M: Integrate with current Task 8 and the real C8 provider
+
+The separate moves input is `moves-phase1-5f` at
+`7196746a6e6223fad952dd17bce12c41512404c8`, read with `git show` from
+`/home/paulca/amadan.net/carlos/platform-moves-p5f`. It descends from
+`7afef724` and includes Task 8 plus the live-reference reduction. Export that
+exact commit to another scratch subdirectory. All existing production files
+changed by A and B are byte-identical at this tip and the required main base;
+verify this before applying A, B and C. Never overlay the moves tree on main
+or modify the moves worktree. Apply M tests and implementation, then N's
+branch-specific census patch. D belongs to the main replay.
+
+Input SHA-256 values:
+
+- `internal/host/movesfiles.go`: `e0eda3482e86bcd8ebb28345e19a518d885823d98eccfef387da768f77b61dea`.
+- `internal/host/claimfiles.go`: `675ba6488b86280f3ff3bfab58427eae921f8f7837418cea8237827f81b57441`.
+- `internal/host/moveswakesource_linux_test.go`: `bd889b9cf8faf1ee7143c980b7a5c7aefbdfbee8003c0c4f9518b5e2fee493eb`.
+
+Moves will cut K by proving only origins that live manifest entries reference,
+plus the admitted generation. The pinned tip already implements that cut.
+**The batch read is required regardless:** K distinct live references must
+not trigger K walks. The M patch preserves that selection and changes
+`movesOriginProof.Proven` to take the complete generation set in one call.
+The retained-chain fallback and its test doubles adopt the batch signature;
+the production constructor always supplies `ClaimOriginProof` and a one-entry
+cache. Empty and never-published starts also use a single batch. There is no
+per-generation production fallback.
+
+`movesFilesSource.Open` binds the supplied authoritative deployment store to
+`Claims`, apart from the regional files bucket, and refuses a nil claim store.
+The verified actor supplies HomeRegion. The constructor witness checks these
+bindings. The 16-admission integration still exercises actual pruning to eight
+and an object retained from generation 1; an invented live object origin
+refuses. An unused `Origins` hint does not grant authority or add work.
+
+`TestPlan15ThousandOriginWake` makes 1000 real admission CAS operations,
+retaining eight records, and establishes a manifest with 999 distinct old live
+origins plus the newly admitted generation: N=1000, K=1000. A counting
+claim store panics on unbounded node Get, counts streaming node GETs and
+claim GETs, and requires exactly N node reads and one claim read, within
+N+2 total. Every `@home` lookup misses and its verified `ie` counterpart
+succeeds in the same traversal. A second batch with an invented origin
+fails with exactly one traversal too.
+
+`TestPlan15Task8PreparedRecovery` uses the registered Task 8 wake source,
+claim runtime admission and handover, the production C8 constructor, a real
+authoritative claim store, and the real prepared publisher/journal. The fixture builds and journals the prepared files manifest after admission,
+bound to that admitted stamp; it does not exercise installation scheduling or
+prove a pre-admission install receipt survives a stamp change. It calls
+the recovery owner's Materialize path with that C8 provider, verifies its
+provenance traversal, repeats the materialization without rewriting the
+unchanged manifest, and requires stale-observation refusal. It runs for exact
+region and verified home counterpart. This fixture's env already matches the
+prepared env, so the manifest correctly stays at sequence zero on both
+materializations. It does not assert a write that the publisher should skip.
+
+The existing `mwRegister` helper delegates to `mwRegisterSource`. Old tests
+retain their fake C8; the new test passes an actual `movesFilesSource` and
+asserts that both serving and recovery owners share its `*movesFiles`. No
+`f.c8` or injected permissive proof is used by this witness. Rendering and
+held-release effects retain the existing fixture seams; it does not test
+root release, live AWS, process launch or a real credential boundary.
Focused command:
-`go test ./internal/host -run 'TestPlan15|TestProductionFilesSource|TestEstablish' -count=1`.
-Before the implementation, `TestPlan15ProductionProofWiring` fails because the
-production provider still returns the retained-chain proof. The integration
-witness creates a real claim, admits and establishes 16 generations through
-the real publisher, marks coverage and observes actual pruning to eight.
-Its newest manifest still references an object from generation 1. Then it
-adds a never-admitted `Origins` hint and requires establishment to refuse.
-The constructor test proves the actual deployment store and verified home
-binding reach the provider; an injected permissive map cannot pass it.
+
+```bash
+go test ./internal/host -run 'TestPlan15|TestProductionFilesSource|TestEstablish|TestChainProof|TestRecoveryAdmissionAfterTargetSession|TestDirtyReturnThroughAfterRecoveryOnRealRows' -count=1
+```
+
+Before M implementation the new batch-shaped test doubles and production
+constructor witness fail. After M/N, run the command and the unchanged
+production/census selections, then `go build ./...` and `go vet ./...` in
+the moves export. M's test patch changes only test helpers and witnesses;
+M's implementation owns the interface and constructor changes.
<!-- apply: M tests -->
```diff
--- /dev/null
+++ b/internal/host/plan15_moves_integration_test.go
-@@ -0,0 +1,101 @@
+@@ -0,0 +1,184 @@
+package host
+
+import (
+ "context"
+ "errors"
+ "fmt"
++ "io"
+ "strings"
+ "testing"
+
@@ -1510,44 +1866,412 @@ binding reach the provider; an injected permissive map cannot pass it.
+ t.Fatalf("lost ancestor: %+v %v", m, err)
+ }
+ // A manifest cannot grant itself another origin, even with an otherwise valid chain.
-+ m.Origins = append(m.Origins, movesFilesPrefix(location, "never"))
++ m.Entries["a.txt"] = hostfiles.Entry{Object: movesFilesPrefix(location, "never") + "objects/0000000000000001-" + sha([]byte("A")), SHA256: sha([]byte("A")), Size: 1, Seq: 1}
+ x.bucket.Put("data", hostfiles.ManifestKey(movesFilesPrefix(location, "g16")), mustJSON(t, m))
+ if err := x.f.Establish(t.Context(), o, launchInputs("g16"), x); !errors.Is(err, hostfiles.ErrOrigin) {
+ t.Fatalf("manifest self proof: %v", err)
+ }
+}
++
++type plan15CountStore struct {
++ *release.DirStore
++ nodes, claims int
++}
++
++func (s *plan15CountStore) Get(ctx context.Context, key string) ([]byte, string, error) {
++ if strings.HasPrefix(key, "control/host-origins/") {
++ panic("node GET must stream")
++ }
++ s.claims++
++ return s.DirStore.Get(ctx, key)
++}
++func (s *plan15CountStore) Open(ctx context.Context, key string) (io.ReadCloser, string, error) {
++ s.nodes++
++ return s.DirStore.Open(ctx, key)
++}
++func TestPlan15ThousandOriginWake(t *testing.T) {
++ t.Setenv("CARLOS_HOST_CLAIM", "1")
++ t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", "1")
++ const n = 1000
++ ctx := t.Context()
++ x := newFilesFixture(t)
++ x.writeEnv("K=1\n")
++ claims := &plan15CountStore{DirStore: release.NewDirStore(t.TempDir())}
++ o, err := hostclaim.Read(ctx, claims, filesHost)
++ if err != nil {
++ t.Fatal(err)
++ }
++ nh := hostclaim.NewHolder{Family: "instance", Key: "apps/bab/jam/instances/" + filesHost + ".json", Account: "bab", App: "jam", Host: filesHost, Region: "ie"}
++ o, err = hostclaim.Update(ctx, claims, o, hostclaim.Adopt(nh, "uk-box", nil, nil, "adopt", func() string { return "id" }))
++ if err != nil {
++ t.Fatal(err)
++ }
++ m := emptyManifest()
++ for i := 1; i <= n; i++ {
++ h := o.Claim.Holder
++ gen := fmt.Sprintf("g%d", i)
++ o, err = hostclaim.Update(ctx, claims, o, hostclaim.AdmitWriter(hostclaim.Admission{ObservedSession: h.Session, ObservedGeneration: h.Generation, Box: "uk-box", Mode: "cold", Region: "ie", PredecessorRegion: "ie", NewGeneration: gen, PredecessorCertified: true}))
++ if err != nil {
++ t.Fatal(err)
++ }
++ obj := movesFilesPrefix(h.Location, gen) + "objects/0000000000000001-" + sha([]byte("A"))
++ x.bucket.Put("data", obj, []byte("A"))
++ m.Entries[fmt.Sprintf("file%d", i)] = hostfiles.Entry{Object: obj, SHA256: sha([]byte("A")), Size: 1, Seq: 1, Mode: 0600}
++ o, err = hostclaim.Update(ctx, claims, o, func(c *hostclaim.Claim) error {
++ for j := range c.Holder.Admissions {
++ c.Holder.Admissions[j].Covered = true
++ }
++ return nil
++ })
++ if err != nil {
++ t.Fatal(err)
++ }
++ }
++ delete(m.Entries, "file1000")
++ m.Seq = 1
++ location := o.Claim.Holder.Location
++ x.bucket.Put("data", hostfiles.ManifestKey(movesFilesPrefix(location, "g999")), mustJSON(t, m))
++ x.f.Region = "@home"
++ x.f.Proof = ClaimOriginProof{Claims: claims, HomeRegion: "ie", Region: "@home", Cache: new(hostclaim.OriginProofCache)}
++ in := launchInputs("g1000")
++ in.Admission.Region = "ie"
++ claims.nodes, claims.claims = 0, 0
++ if err := x.f.Establish(ctx, o, in, x); err != nil {
++ t.Fatal(err)
++ }
++ if claims.nodes != n || claims.nodes+claims.claims > n+2 || claims.claims != 1 {
++ t.Fatalf("N=%d K=%d node GETs=%d claim GETs=%d", n, n, claims.nodes, claims.claims)
++ }
++ t.Logf("N=%d K=%d node GETs=%d claim GETs=%d; @home misses, ie matches in same walk", n, n, claims.nodes, claims.claims)
++ // Neither region proves this origin; the failure still costs one walk.
++ x.f.Proof = ClaimOriginProof{Claims: claims, HomeRegion: "ie", Region: "@home"}
++ m.Entries["invented"] = hostfiles.Entry{Object: movesFilesPrefix(location, "never") + "objects/0000000000000001-" + sha([]byte("A")), SHA256: sha([]byte("A")), Size: 1, Seq: 1}
++ claims.nodes, claims.claims = 0, 0
++ if _, err := x.f.movesFilesAllowed(ctx, o, in.Admission, location, "g1000", m); !errors.Is(err, hostfiles.ErrOrigin) {
++ t.Fatal(err)
++ }
++ if claims.nodes != n || claims.claims != 1 {
++ t.Fatalf("alias miss repeated the walk: nodes=%d claims=%d", claims.nodes, claims.claims)
++ }
++}
+--- /dev/null
++++ b/internal/host/plan15_task8_linux_test.go
+@@ -0,0 +1,109 @@
++//go:build linux
++
++package host
++
++import (
++ "context"
++ "errors"
++ "path/filepath"
++ "testing"
++
++ "github.com/carlosframework/platform/internal/hostclaim"
++ "github.com/carlosframework/platform/internal/hostfiles"
++ "github.com/carlosframework/platform/internal/moves"
++ "github.com/carlosframework/platform/internal/release"
++)
++
++func TestPlan15Task8PreparedRecovery(t *testing.T) {
++ for _, alias := range []bool{false, true} {
++ t.Run(map[bool]string{false: "exact-region", true: "home-alias"}[alias], func(t *testing.T) {
++ t.Setenv("CARLOS_HOST_CLAIM", "1")
++ t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", "1")
++ ctx := t.Context()
++ f := newRCFixture(t, rcDirty)
++ counts := &plan15CountStore{DirStore: f.claims.(*release.DirStore)}
++ f.claims = counts
++ store := casBucketStore{b: f.bucket, bucket: "data"}
++ source := &movesFilesSource{DataDir: f.dataDir, newFiles: func(context.Context, string, string) (release.Store, error) { return store, nil }}
++ w := mwRegisterSource(t, f.poFixture, "ie-box", "ie", nil, source, "ie")
++ provider, ok := w.files.(*movesFiles)
++ if !ok {
++ t.Fatalf("C8 is %T", w.files)
++ }
++ proof, ok := provider.Proof.(ClaimOriginProof)
++ if !ok || proof.Claims != f.claims {
++ t.Fatalf("proof is %T", provider.Proof)
++ }
++ // Verified home alias on the C8 consumer; the admitted tuple uses ie.
++ if alias {
++ provider.Region = "@home"
++ proof.Region = "@home"
++ provider.Proof = proof
++ }
++ src := w.sourceInstallation()
++ w.build(nil, src.inventory, src.registry)
++ w.targetStopped()
++ o := f.read()
++ in, err := w.m.Select(ctx, f.route, o, f.policy())
++ if err != nil {
++ t.Fatal(err)
++ }
++ if !in.Prepared || in.Admission.NewGeneration != rpRB {
++ t.Fatalf("not prepared: %+v", in.Admission)
++ }
++ if err := w.wake.stopPredecessor(ctx, o); err != nil {
++ t.Fatal(err)
++ }
++ admitted, err := w.admit(o, f.policy())
++ if err != nil {
++ t.Fatal(err)
++ }
++ h := admitted.Claim.Holder
++ if admitted.Claim.Origins == nil {
++ t.Fatal("runtime admission omitted provenance")
++ }
++ // Use the real prepared publisher and journal binding for the recovered tree.
++ pm, err := provider.BuildPrepared(ctx, h.Location, rpRB, admitted.Claim.Op.ID, h.Stamp, f.files, filepath.Join(f.dataDir, poHost+".env"), true, hostfiles.WakeState{V: 1, Host: poHost, Timers: []hostfiles.WakeTimer{}})
++ if err != nil {
++ t.Fatal(err)
++ }
++ j, err := moves.ReadJournal(f.dataDir, poHost, rpRB)
++ if err != nil {
++ t.Fatal(err)
++ }
++ j.Mark(landStepFilesManifest, string(mustJSON(t, pm)))
++ if err := j.Commit(f.dataDir); err != nil {
++ t.Fatal(err)
++ }
++ // Hold configuration/release effects at the fixture's existing seams; the
++ // registered recovery owners, admission, C8 provider and proof are real.
++ w.m.Render, w.m.Release = f.owners.Render, f.owners.Release
++ work := poWork{action: movesActionSpawn, f: f.poFixture}
++ counts.nodes, counts.claims = 0, 0
++ if err := w.m.Materialize(ctx, admitted, in, work); err != nil {
++ t.Fatal(err)
++ }
++ if counts.nodes != int(admitted.Claim.Origins.Count) {
++ t.Fatalf("C8 did not traverse authoritative history: %d", counts.nodes)
++ }
++ m, found, err := hostfiles.Load(ctx, store, movesFilesPrefix(h.Location, rpRB))
++ if err != nil || !found || m.Seq != 0 {
++ t.Fatalf("prepared recovery: %+v %v", m, err)
++ }
++ version := f.bucket.LatestID("data", pm.Key)
++ if err := w.m.Materialize(ctx, admitted, in, work); err != nil {
++ t.Fatalf("recovery replay: %v", err)
++ }
++ if f.bucket.LatestID("data", pm.Key) != version {
++ t.Fatal("replay rewrote manifest")
++ }
++ // The same fully integrated path must fail when the claim observation moves.
++ if _, err := hostclaim.Update(ctx, f.claims, admitted, func(*hostclaim.Claim) error { return nil }); err != nil {
++ t.Fatal(err)
++ }
++ if err := provider.Establish(ctx, admitted, in, work); !errors.Is(err, hostclaim.ErrConflict) {
++ t.Fatalf("stale proof: %v", err)
++ }
++ })
++ }
++}
+--- a/internal/host/movesfiles_test.go
++++ b/internal/host/movesfiles_test.go
+@@ -946,2 +946,6 @@
+-func (r recordedOrigins) Proven(_ context.Context, _ hostclaim.Observed, _ hostclaim.Admission, generation string) (bool, error) {
+- return r[generation], nil
++func (r recordedOrigins) Proven(_ context.Context, _ hostclaim.Observed, _ hostclaim.Admission, generations []string) (map[string]bool, error) {
++ result := map[string]bool{}
++ for _, generation := range generations {
++ result[generation] = r[generation]
++ }
++ return result, nil
+@@ -958,5 +962,9 @@
+-func (a answeringProof) Proven(_ context.Context, _ hostclaim.Observed, _ hostclaim.Admission, generation string) (bool, error) {
+- if err := a.errs[generation]; err != nil {
+- return false, err
+- }
+- return a.proven[generation], nil
++func (a answeringProof) Proven(_ context.Context, _ hostclaim.Observed, _ hostclaim.Admission, generations []string) (map[string]bool, error) {
++ result := map[string]bool{}
++ for _, generation := range generations {
++ if err := a.errs[generation]; err != nil {
++ return nil, err
++ }
++ result[generation] = a.proven[generation]
++ }
++ return result, nil
+@@ -1039 +1047 @@
+- if ok, err := proof.Proven(ctx, before, in, "g2"); ok || err != nil {
++ if ok, err := proof.Proven(ctx, before, in, []string{"g2"}); ok["g2"] || err != nil {
+@@ -1042 +1050 @@
+- if ok, err := proof.Proven(ctx, hostclaim.Observed{Host: filesHost, Absent: true}, in, "g2"); ok || err != nil {
++ if ok, err := proof.Proven(ctx, hostclaim.Observed{Host: filesHost, Absent: true}, in, []string{"g2"}); ok["g2"] || err != nil {
+@@ -1045 +1053 @@
+- if ok, _ := proof.Proven(ctx, before, in, "g1"); !ok {
++ if ok, _ := proof.Proven(ctx, before, in, []string{"g1"}); !ok["g1"] {
+@@ -1049 +1057 @@
+- if ok, _ := proof.Proven(ctx, after, in, "g2"); !ok {
++ if ok, _ := proof.Proven(ctx, after, in, []string{"g2"}); !ok["g2"] {
+@@ -1077,3 +1085,7 @@
+-func (c *countingProof) Proven(_ context.Context, _ hostclaim.Observed, _ hostclaim.Admission, generation string) (bool, error) {
+- c.asked = append(c.asked, generation)
+- return c.proven[generation], nil
++func (c *countingProof) Proven(_ context.Context, _ hostclaim.Observed, _ hostclaim.Admission, generations []string) (map[string]bool, error) {
++ result := map[string]bool{}
++ for _, generation := range generations {
++ c.asked = append(c.asked, generation)
++ result[generation] = c.proven[generation]
++ }
++ return result, nil
+@@ -1088,3 +1100,2 @@
+-// refused when the proof says no; and the count of Proven calls is the
+-// distinct referenced origins plus the admitted generation, each asked
+-// once.
++// refused when the proof says no; and one Proven call contains the
++// distinct referenced origins plus the admitted generation, each named once.
+--- a/internal/host/moveswakesource_linux_test.go
++++ b/internal/host/moveswakesource_linux_test.go
+@@ -96,0 +97,4 @@
++ return mwRegisterSource(t, f, box, region, files, nil, "")
++}
++
++func mwRegisterSource(t *testing.T, f *poFixture, box, region string, files ClaimFilesManifest, source ClaimProductionFilesSource, home string) *mwWorld {
+@@ -142 +146 @@
+- actor := hostbox.Actor{Box: box, Region: region, Bucket: "data", Enrollment: "enrollment-1", Cloud: hostbox.CloudIdentity{Region: "eu-west-1"}}
++ actor := hostbox.Actor{Box: box, Region: region, HomeRegion: home, FleetHomeRegion: home, Bucket: "data", Enrollment: "enrollment-1", Cloud: hostbox.CloudIdentity{Region: "eu-west-1"}}
+@@ -152 +156,4 @@
+- if _, err := openClaimFilesManifest(ctx, env, &fakeFilesSource{files: files}, f.claims, f.bucket, actor, &out); err != nil {
++ if source == nil {
++ source = &fakeFilesSource{files: files}
++ }
++ if _, err := openClaimFilesManifest(ctx, env, source, f.claims, f.bucket, actor, &out); err != nil {
+@@ -157 +164 @@
+- if w.m.Files != files || w.files != files {
++ if w.m.Files != w.files || (files != nil && w.files != files) {
```
<!-- apply: M implementation -->
```diff
--- a/internal/host/claimfiles.go
+++ b/internal/host/claimfiles.go
-@@ -100,16 +100,16 @@
- }
- }
-
+@@ -12,0 +13 @@
++ "github.com/carlosframework/platform/internal/hostclaim"
+@@ -108 +109 @@
-func (s *movesFilesSource) Open(ctx context.Context, _ release.Store, data lease.VersionedStore, actor hostbox.Actor) (ClaimFilesManifest, error) {
+func (s *movesFilesSource) Open(ctx context.Context, claims release.Store, data lease.VersionedStore, actor hostbox.Actor) (ClaimFilesManifest, error) {
- region := park.PlacementRegion(actor.Region)
+@@ -110 +111 @@
- if s == nil || s.DataDir == "" || s.newFiles == nil || data == nil || actor.Bucket == "" || actor.Box == "" || region == "" || actor.Cloud.Region == "" {
+ if s == nil || s.DataDir == "" || s.newFiles == nil || claims == 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
- }
+@@ -117 +118 @@
- return &movesFiles{Files: files, Data: data, Bucket: actor.Bucket, HomeRegion: actor.HomeRegion, Region: region, Box: actor.Box, DataDir: s.DataDir, FilesDir: movesFilesDirFor(s.DataDir)}, nil
-+ return &movesFiles{Proof: ClaimOriginProof{Claims: claims, HomeRegion: actor.HomeRegion, Region: region}, Files: files, Data: data, Bucket: actor.Bucket, HomeRegion: actor.HomeRegion, Region: region, Box: actor.Box, DataDir: s.DataDir, FilesDir: movesFilesDirFor(s.DataDir)}, nil
- }
-
- func (s *movesFilesSource) Close(context.Context) error { return nil }
++ return &movesFiles{Proof: ClaimOriginProof{Claims: claims, Cache: new(hostclaim.OriginProofCache), HomeRegion: actor.HomeRegion, Region: region}, Files: files, Data: data, Bucket: actor.Bucket, HomeRegion: actor.HomeRegion, Region: region, Box: actor.Box, DataDir: s.DataDir, FilesDir: movesFilesDirFor(s.DataDir)}, nil
+--- a/internal/host/movesfiles.go
++++ b/internal/host/movesfiles.go
+@@ -414,5 +414,3 @@
+-// Today's implementation is chainOriginProof, the retained admission
+-// chain; claim Plan 15's durable per-incarnation record (platform-1e's,
+-// with a read API) slots in behind this interface. Anything unprovable
+-// refuses: a host past the pruning window refuses until Plan 15 lands,
+-// and that is accepted.
++// The production provider uses ClaimOriginProof's durable batch. The retained
++// chain implementation remains the default for callers without a bound proof.
++// One call contains all live references and the admitted generation.
+@@ -420 +418 @@
+- Proven(ctx context.Context, o hostclaim.Observed, in hostclaim.Admission, generation string) (bool, error)
++ Proven(ctx context.Context, o hostclaim.Observed, in hostclaim.Admission, generations []string) (map[string]bool, error)
+@@ -436,5 +434,10 @@
+-func (p chainOriginProof) Proven(_ context.Context, o hostclaim.Observed, _ hostclaim.Admission, generation string) (bool, error) {
+- if generation == "" || o.Absent || o.Claim == nil || o.Claim.Holder == nil {
+- return false, nil
+- }
+- return slices.Contains(movesFilesChain(o.Claim.Holder, p.home, p.region), generation), nil
++func (p chainOriginProof) Proven(_ context.Context, o hostclaim.Observed, _ hostclaim.Admission, generations []string) (map[string]bool, error) {
++ result := make(map[string]bool, len(generations))
++ if o.Absent || o.Claim == nil || o.Claim.Holder == nil {
++ return result, nil
++ }
++ chain := movesFilesChain(o.Claim.Holder, p.home, p.region)
++ for _, generation := range generations {
++ result[generation] = generation != "" && slices.Contains(chain, generation)
++ }
++ return result, nil
+@@ -450,37 +453,2 @@
+-// proveOrigin is the one route to origin proof (fix round 4): the
+-// generation's files prefix under location enters an allowed set only
+-// when movesOriginProof proves the generation. A proof that answers false
+-// is ErrOrigin; an error is the error. Nothing enters a set any other way,
+-// so a different proof (claim Plan 15's) has the final say over the
+-// chain's generations and the prepared generation as over every other.
+-func (f *movesFiles) proveOrigin(ctx context.Context, o hostclaim.Observed, in hostclaim.Admission, location, gen string) (string, error) {
+- if !movesFilesGeneration(gen) {
+- return "", fmt.Errorf("%w: %q is not a generation name", hostfiles.ErrOrigin, gen)
+- }
+- origin := movesFilesPrefix(location, gen)
+- proven, err := f.proof().Proven(ctx, o, in, gen)
+- if err != nil {
+- return "", fmt.Errorf("prove the origin %s: %w", origin, err)
+- }
+- if !proven {
+- return "", fmt.Errorf("%w: %s is not proven by the claim's recorded admissions", hostfiles.ErrOrigin, origin)
+- }
+- return origin, nil
+-}
+-
+-// movesFilesAllowed is the origin set an admission allows for m, the
+-// manifest it establishes from (Task 6b, the moves side of claim Plan 15's
+-// review): the origins m's entries actually reference, each proven against
+-// the claim's records through proveOrigin after the location rule
+-// (movesFilesOrigins), plus the one generation this admission opens
+-// (admitted: proven through the same route, round 4's binding, one call).
+-// Nothing else is proven and nothing else is carried: the retained chain's
+-// other generations are not candidates, and a carried hint
+-// (Manifest.Origins) that no entry references is neither proven nor
+-// carried forward, so the proof's cost follows the live references, not
+-// the history. A referenced origin the proof cannot prove is ErrOrigin.
+-// Then ValidateManifest over the proven set, which is the new manifest's
+-// carried set (SetOrigins). Nothing refused before is accepted now: every
+-// referenced origin and the admitted generation go through the proof as
+-// before; only unreferenced hints and unreferenced chain generations, which
+-// no entry depends on, are no longer asked about.
++// movesFilesAllowed proves live object origins and the admitted generation
++// together. Manifest hints that no object references are not carried forward.
+@@ -488,6 +456,5 @@
+- set := map[string]bool{}
+- own, err := f.proveOrigin(ctx, o, in, location, admitted)
+- if err != nil {
+- return nil, err
+- }
+- set[own] = true
++ if !movesFilesGeneration(admitted) {
++ return nil, hostfiles.ErrOrigin
++ }
++ generations := []string{admitted}
++ set := map[string]bool{movesFilesPrefix(location, admitted): true}
+@@ -508,3 +475 @@
+- if _, err := f.proveOrigin(ctx, o, in, location, gen); err != nil {
+- return nil, err
+- }
++ generations = append(generations, gen)
+@@ -511,0 +477,9 @@
++ }
++ proven, err := f.proof().Proven(ctx, o, in, generations)
++ if err != nil {
++ return nil, err
++ }
++ for _, gen := range generations {
++ if !proven[gen] {
++ return nil, fmt.Errorf("%w: %s is not proven by the claim's recorded admissions", hostfiles.ErrOrigin, movesFilesPrefix(location, gen))
++ }
+@@ -821 +795 @@
+- own, err := f.proveOrigin(ctx, o, in.Admission, h.Location, gen)
++ origins, err := f.movesFilesAllowed(ctx, o, in.Admission, h.Location, gen, emptyManifest())
+@@ -829 +803 @@
+- p.SetOrigins([]string{own})
++ p.SetOrigins(origins)
+@@ -847 +821 @@
+- own, err := f.proveOrigin(ctx, o, in.Admission, h.Location, gen)
++ origins, err = f.movesFilesAllowed(ctx, o, in.Admission, h.Location, gen, emptyManifest())
+@@ -851 +825 @@
+- base, origins = emptyManifest(), []string{own}
++ base = emptyManifest()
```
### Task N: Moves replay census
The classifications are exactly Task D's. This patch changes only those five
-rows in the moves tip's existing inventory. `movesFilesSource.Open` introduces
+rows in the moves tip's existing inventory. `movesFilesSource.Open` and the batch call sites introduce
no selected record write or console authority call. Run B8 and the unchanged
console authority inventory after applying it.
@@ -1568,7 +2292,7 @@ console authority inventory after applying it.
{"internal/hostclaim/lossrecord.go", "ReplaceLossRecord", "foreign-recordkey", "key:LossRecordKey,sel:Put", "576589457e4a18b64e6b56892603051982bd01770c355504305e72d53149bc52"},
- {"internal/hostclaim/store.go", "Update", "nonrecord", "sel:Put", "68f97a49f9a936228cb26e49d7bed3a4ab17c3a4c4255e32c2d2af1dfccd73a6"},
+ {"internal/hostclaim/origins.go", "originTuple.valid", "key-site", "lit:instances", "7f6f20f51e51694d665588f0b986def85bda4789f24623ee1993289265b01355"},
-+ {"internal/hostclaim/origins.go", "commitOrigins", "nonrecord", "sel:Put", "56a6eb6b4c9e67acccfb8f2b97ce5e338e21c56ab226359b12f3aacd75cd7f7f"},
++ {"internal/hostclaim/origins.go", "commitOrigins", "nonrecord", "sel:Put", "f5f506d8b1e5d4dced896c6be3a6e498354e5f28d81452131e220fec58e8bd78"},
+ {"internal/hostclaim/store.go", "Update", "nonrecord", "sel:Put", "ec5de8702797ce97a24147cbf16ad8f8e254e2da82d3a0859c0407501f0b575f"},
{"internal/hostevidence/signature.go", "immutable", "nonrecord", "sel:Put", "54fb2bdc230c2cb4193f3b808004b63bcfd092266846fc0c03e61617c3ad0ab5"},
{"internal/hostexec/controller.go", "Controller.run", "nonrecord", "sel:Put", "03df11b375761730700ce30c0db05c57f0828629eda0a5de126ea1a2f2f7501c"},
@@ -1603,7 +2327,11 @@ local stores and mocks; no live writes or IAM probes are authorized here.
As a mutation witness, temporarily remove the `pendingOrigin` call from
`AdmitWriter` only in scratch. `TestPlan15PruningMigrationAndScope` must fail
-when a retained seed ages out. Restore the exact function, then rerun it.
+when a retained seed ages out. Restore the exact function, then rerun it. Separately replace each
+`readOriginBytes` call with `Store.Get` in scratch. The reachable and
+candidate-conflict subtests of `TestPlan15BoundedReachableAndConflicting`
+must fail with `unbounded node Get`, respectively. Restore both sites and
+rerun those witnesses.
Delete the exact scratch directory created by `mktemp` after leaving it:
@@ -1614,6 +2342,17 @@ test -n "$CLAIM15_SCRATCH" && rm -r -- "$CLAIM15_SCRATCH"
Never delete `/home/paulca/.cache/tmp/claude-1001/gocache`. Record actual
red/green, build, vet, census, integration and cleanup outcomes in
-`/home/paulca/.cache/tmp/claude-1001/own/after236/plan15-write-report.md`.
+`/home/paulca/.cache/tmp/claude-1001/own/after236/plan15-rev2-report.md`.
The report must retain the unresolved credential-boundary and #250 backfill
conditions; neither a passing replay nor this plan authorizes activation.
+
+## Revision log
+
+- Rev 2, 2026-10-12: answer both binding reviews. Re-anchor on main
+ `650e6152be0af5851b879980f67970382750becd`; pin moves Task 8 and its
+ live-reference reduction at `7196746a6e6223fad952dd17bce12c41512404c8`.
+ Batch exact tuples and verified home counterparts in one authenticated
+ traversal; bind the cache to store/host/head/count/exact set and revalidate
+ before reuse. Stream both node and conflict reads to a 4097-byte refusal.
+ Add count, oversize, cache, and real-C8 prepared-recovery witnesses.
+ Refresh only the reviewed census digest and the rev 6.35 amendment.