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>
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. |