EdgeZero Phase 1: store-registry migration (design + implementation) - #839
Draft
aram356 wants to merge 63 commits into
Draft
EdgeZero Phase 1: store-registry migration (design + implementation)#839aram356 wants to merge 63 commits into
aram356 wants to merge 63 commits into
Conversation
Defines the end-state, current-state gap analysis, and an ordered set of phases (stores, config injection, secret externalization, extractors, legacy-path removal) for moving Trusted Server completely onto EdgeZero. Phase 0 (State<T> extractor + nested #[secret]) is an upstream edgezero prerequisite tracked separately.
6 tasks
Clarify that platform/mod.rs and types.rs are edited (KV re-export and a shrinking RuntimeServices remain), not deleted, in Phase 1.
…tore id - P0-C: Fastly bypasses run_app (multi-value Set-Cookie, logger reinit, JA4/H2 capture) - add EdgeZero dispatch prereq or documented exception - P-BOOT: specify boot-time config/secret store access for Cloudflare/Spin (build_state runs before request context; registry is per-request) - D5: unify the split app-config store id (app_config vs trusted_server_config) - Promote the secret inventory to a spec artifact; array + optional secrets confirmed present, so edgezero #305 must ship ArrayEach + Option<String> - Phase 4 acceptance: per-adapter route parity (EC routes are Fastly-only) - Phase 5: expand deletion ledger (route_tests, viceroy config, fastly.toml, runbook) - Scope the include_str! ban to adapter/runtime app-config only
…iation (D5) - D6: EdgeZero stores are read-only, but KeyRotationManager writes/deletes config+secrets at runtime for /_ts/admin/keys/*. management_api.rs cannot be deleted in Phase 1 unconditionally; gate on a keep/move-to-ops/upstream decision (R10) - D5 expanded: reconcile ALL runtime store ids (app_config, secrets, JWKS, DataDome ts_secrets, S3, fixtures) with edgezero.toml or strict lookup fails - Fastly needs explicit registry injection into its custom oneshot path - Phase 1 plan starts with a store-capability inventory, not deletions
Task 1 is a decision gate (store-id inventory + D5/D6) per review, not deletions. Read-path migration and Fastly custom-dispatch registry injection are fully specified; management_api.rs deletion is gated on the D6 decision.
- Lock plan to D6-a; D6-b/c stop after Task 1 (separate plan) - Kind-aware store-id reconciliation (kv/config/secrets), incl. ec_store as KV - Composite read/write store bridge with a write-delegation test - New task: migrate Fastly/Axum BOOT config read to EdgeZero before deleting impls - Local Fastly registry builders (EdgeZero builders are pub(crate)); R11 - Concrete named tests, files, routes, fixtures (no <test_name> placeholders) - Sync spec D5 (kind-aware), Phase 1 boot-read, R11
…e tests - Task 3: composite holds the whole ConfigRegistry/SecretRegistry and resolves named(store_name) per read (multi-store); strict unknown-id error; test uses 2 ids - Task 1/spec: correct KV ids to ec_store + consent_store (creative/counter/opid are fastly.toml platform stores, not Settings logical ids) - Task 4: core-level loader test with InMemoryConfigStore + ConfigStoreHandle::new - Task 5: add non-default config-id (JWKS) + non-default secret-id (DataDome, S3) tests - Task 6: local EnvConfig runtime-dictionary reader (EdgeZero helper private); R12 - Task 8: tests build registries with >=2 ids and assert unknown id errors strictly - Spec D5/Phase 1: kind-aware incl [stores.kv]; R9 mentions ec_identity_store; R12
- D7: runtime app-config is config-store-only; NO runtime env vars. ts config push reads env at push time and bakes resolved values into the blob. Stores open by logical id (name == id); no runtime EDGEZERO__STORES__*__NAME read. - Task 6: drop local_env_config entirely (D7) - open stores by logical id; resolves the fastly::ConfigStore-has-no-iter and private-helper findings (R12) - creative_store IS a Settings KV id (deprecated); include it, exclude counter_store/opid_store (Fastly-adapter constants) - Task 2/spec: tighten kind-aware KV inventory (ec_store, consent_store, creative_store) - Task 4: run the actual core test name; Task 5: one filter per cargo test invocation
Switch the six edgezero git deps from branch main to worktree-state-nested-secrets-spec-review (stackpop/edgezero#306, stacked on #300) to pick up the Phase 0 State<T> extractor work. cargo check-axum passes.
- DataDome IP-CIDR config store (datadome-ip-bypass) added to store inventory - Explicit app-config decision: store id trusted_server_config, key app_config; rename DEFAULT_CONFIG_STORE_ID + repoint request_signing.config_store_id - Task 3: ConfigRegistry::named returns ConfigStoreBinding -> use binding.handle.get; map EdgeZero Ok(None)/Err to PlatformError - Task 6: exact builder signatures (KV Result<Option<..>,FastlyError>, config/secret Option<..>) + missing-store/open-failure policy - Task 5: DataDome secret read tested on a protected NON-integration route - StoreName reconciled to logical read id (D7); doc + call-site audit step - Task 4: Axum boot reads .edgezero/local-config-trusted_server_config.json, no env override
Blockers: - Hooks::stores() is not overridden on any adapter (empty StoresMetadata); add Task 2 Step 5 to wire stores() from edgezero.toml on all four adapters - Axum uses routes()+AxumDevServer, not run_app; Task 5 Step 0 switches Axum to edgezero_adapter_axum::run_app::<TrustedServerApp>() - request-signing reads use jwks_store/signing_keys but writes use config_store_id/secret_store_id; fix example/fixtures to jwks_store/signing_keys (NOT app_config/secrets); only the app-config store renames to trusted_server_config - PlatformConfigStore/SecretStore mix read+write; Task 3 Step 0 splits write-only PlatformConfigWriter/PlatformSecretWriter so Task 8 can drop reads and compile High/Medium: - D7 softened: EdgeZero builders read EnvConfig but fall back to logical id; we set none - Task 2 Step 6: declare stores in fastly.toml/wrangler.toml/spin.toml/Axum local files - Task 2 test parameterized over example + fixture + all-store-refs config - CI gate adds cargo check-cloudflare + check-spin (wasm surfaces) - Fix stale spec D5/R9 wording
- Axum: exact path = keep AxumDevServer::with_config + .with_{config,kv,secret}_registry
(dev_server::run_app drops custom PORT/axum.toml); add adapter-axum registry builders
- Task 5: extract whole registry via ctx.request().extensions().get::<ConfigRegistry>()
(RequestContext has no whole-registry getter; per-id accessors would wire only default)
- Task 2: enumerate exact file paths; replace brace-glob git add that would hit
non-existent adapter manifest paths
- Spec: state requirement that Fastly management id == runtime logical id for
jwks_store/signing_keys (or supply a mapping, out of Phase 1 scope)
- Task 3: composite writer test asserts (StoreId, key, value) forwarding (D6-a risk)
- Minor: expect("should ...") convention; Task 7 local verify adds check-cloudflare/spin
- Fastly build_per_request_services still read FastlyPlatformConfigStore directly, so injected registries were unused; add Task 6 Step 4b to build RuntimeServices from the composite via registries in extensions - App-config key == store id (trusted_server_config): default_config_key falls back to id and D7 forbids the __KEY env; set CONFIG_BLOB_KEY=trusted_server_config, retire the app_config key (no --key/env needed) - Task 5: rename heading to 'preserve AxumDevServer::with_config + add registries'; list adapter-axum/src/registries.rs (Step 0 already kept with_config) - Task 2 Step 6: exact per-adapter manifest mappings (CF config=KV binding, secrets=flat Worker secrets via wrangler secret put; Spin config/kv=KV labels) - Name S3 secret store id s3-auth explicitly in D5/Task 2 - Fix all-store-refs.toml include_str path (testdata/, not ../testdata/) - Writer traits Send+Sync; expect_err 'should ...' convention
…y file) - Add config_payload.rs (CONFIG_BLOB_KEY) to Task 2 files + git add (was missing) - Task 2 Step 6b: rename app_config in generate-viceroy-config.rs (+ its test), tests/common/config.rs, tests/environments/axum.rs env var (they run in adapter suites and break under the store/key rename) - Task 2 Step 6: Cloudflare/Spin secrets are FLAT (ignore store_name) -> provision the concrete secret KEYS the code reads (signing KIDs, DataDome key name, S3 access_key_id/secret_access_key/session token), not the store id - Task 6: add app.rs to files; test passes only after Step 4b (not 3-4) - Task 1 output table: drop EDGEZERO__STORES__*__NAME column (D7 -> platform resource per adapter, no env mapping)
The prior review's Critical 'APIs do not exist' findings were against a stale cargo-cache checkout (ce6bcf7, edgezero #253 'Add store support for Spin') that predates the registry refactor. Our actual pin is 6ebc29a5 (PR #306), which HAS StoreRegistry/StoresMetadata/Hooks::stores()/dispatch_with_registries/ with_*_registry/[stores.*] ids+default/CF-KV-namespace/Axum-local-file. Added a 'Pinned dependency (verified)' note recording this to prevent re-litigation. Valid fixes: - Cloudflare app.rs settings_from_cloudflare_config_json reads literal value.get("app_config") from the side-channel; change to CONFIG_BLOB_KEY so the key rename doesn't break Cloudflare boot (Phase 2 does the store migration) - Viceroy generator already emits a trusted_server_config store for rollout flags; the app_config->trusted_server_config rename must MERGE the blob into that table, not emit a duplicate table
User chose full convergence on the canonical app-demo wiring. Reshape the
umbrella spec:
- End-state: every adapter is one-line run_app::<App>; App from app! macro;
handlers #[action] + State<Arc<AppState>>; no TS-local registry/dispatch wiring
- Two NEW required upstream edgezero prerequisites (verified gaps in pinned 6ebc29a):
- P0-C: header-preserving Fastly run_app dispatch + pre-dispatch extension hook
(Set-Cookie/JA4/logger) - permanent-exception fallback removed
- P0-D: macro/run_app app-state injection (with_state is builder-only; app!
router + run_app never call it) - a Hooks state hook run_app inserts per request
- D1 retained (load-once Settings) but injected via P0-D state hook, not with_state
- Phase 4 rewritten as full convergence (app! macro, run_app, extractors, catch-all
fallback for integration/publisher dispatch)
- Sequencing note (R14): Phase 1 adapter-registry builders (Tasks 5-6) are throwaway
under run_app; recommend landing P0-C/P0-D early to skip them
- Risks R7 (P0-C required), R13 (P0-D), R14 (sequencing)
…ate) Implementation-ready spec to hand to the edgezero dev, grounded in pinned 6ebc29a: - P0-C: C1 append_header for multi-value Set-Cookie (response.rs:29 set_header bug), C2 Hooks::owns_logging() opt-out, C3 run_app_with_request_extensions pre-dispatch hook for JA4/H2/client_info from the raw fastly::Request - P0-D: Hooks::app_state() injected per request (symmetric with registries) + app! macro state= argument; note P0-D is avoidable via hand-built routes()+with_state
… gaps) - BLOCKER: Task 8 made Fastly read impls write-only, but legacy_main (live until Phase 5) reads through FastlyPlatformConfigStore via build_runtime_services. Fix: only Axum/CF/Spin go write-only in Phase 1; Fastly read impls stay until Phase 5 deletes legacy_main (spec Phase 1/5 boundary + ledger updated) - stores() drift: add Step 5b test asserting stores_metadata() and each adapter's Hooks::stores() equal edgezero.toml (registries build from stores(), not the toml that Step 1 validates) - KV multi-id: CF/Spin build RuntimeServices from kv_store_default() only, so non-default KV ids (consent_store/ec_store) don't resolve; Step 2c decides (registry-backed KV selector vs documented defer + narrowed acceptance) - cloudflare.toml still uses legacy [stores.*].name schema the pinned manifest rejects; Task 2 reconciles/deletes it - Add Cloudflare/Spin non-default config/secret tests (Step 2b)
- KV multi-id DECIDED: registry-backed KV now (not defer). All four adapters resolve named KV via KvRegistry from extensions; remove Fastly consent-store special-casing; per-adapter non-default-KV test. Spec acceptance updated. - Spec: fix stale Phase 1 'delete 4x read impls' bullet to match the corrected Fastly-stays-until-Phase-5 deletion note (no self-contradiction) - Spec: remove duplicate P-BOOT paragraph (kept the full-convergence note) - app_config rename: add tests/environments/cloudflare.rs literals + wrangler.toml placeholder JSON/comment to Task 2 Step 6b + git add
- KV named resolution is a CORE change, not adapter-only: RuntimeServices only has kv_store() single handle and consent (publisher.rs:626) drops the store id via .map(|_| services.kv_store()). Step 2c now adds RuntimeServices::kv_store_named + kv_registry field, updates consent call sites to pass consent_store, removes Fastly special-casing, and lists core files (types.rs, publisher.rs) - Step 2d: test-support helper to build registry-populated RequestContexts + migrate existing direct-context tests (strict registries break them otherwise) - ec_store scope narrowed: Phase 1 only validates it declares/resolves to a KvHandle; NOT EC identity-graph wiring on non-Fastly (separate larger effort)
- KvHandle/consent type path DECIDED: KvRegistry::named yields KvHandle (a wrapper, not a PlatformKvStore impl), and ConsentPipelineInput.kv_store is Option<&dyn PlatformKvStore>. Migrate the consent KV surface to KvHandle: RuntimeServices::kv_handle_named(id) -> Option<KvHandle> (mirrors existing kv_handle()); ConsentPipelineInput.kv_store -> Option<KvHandle>; consent persistence fns take &KvHandle. Files: consent/mod.rs, storage/kv_store.rs added. - Task 5 Step 3/4 now run all four adapters + wasm checks and commit core + Fastly changes (task touches types.rs/publisher.rs/Fastly), per the all-four-green rule
…ral test) - Fastly named-KV sequencing: Task 5 wired kv_registry + removed consent special-casing on Fastly, but Fastly injects registries only in Task 6. Move Fastly named-KV + build_per_request_services composite + runtime_services_for_consent_route removal into Task 6 Step 4b; Task 5 does core + Axum/CF/Spin and leaves Fastly compiling (kv_handle_named returns None) - Behavioral consent test: prove consent_store loads/saves/deletes through the NON-default store and leaves the default untouched (not just handle distinctness) - Convert existing core consent tests (consent/mod.rs kv_store: Some(&store) at ~1450/1481/1492) from &dyn PlatformKvStore to KvHandle - Fix understated KV summaries: spec gap table + plan goal now say default KV is EdgeZero but named/consent-store KV selection is not
… tests) - Interim Fastly regression: Task 5 flipped consent to kv_handle_named but Fastly has no KvRegistry until Task 6 -> consent would silently skip on Fastly. Fix: Task 5 migrates the consent TYPE but keeps the call site on kv_handle() (default, = Fastly's swapped consent store); the named-lookup FLIP + behavioral test are atomic in Task 6 Step 5b once all four inject a KvRegistry - Absent-registry policy: builders stay infallible (return RuntimeServices); build a composite over an EMPTY registry so reads error, not silently fall back - Task 6 Step 5c: Fastly named-KV/consent route test (guards the special-case removal) - Task 3 Step 1b: explicit CompositeSecretStore test (named id, unknown-id error, create/delete StoreId-preserving delegation) - flat CF/Spin secret namespaces mean route tests don't prove store-id binding - Task 5 hygiene: no longer edits/commits Fastly (moved to Task 6)
- Task 6 commit: add auction/endpoints.rs (task updates it for auction fail-closed) - Preflight: drop machine-specific 'present locally' claim; cargo fetch --locked + verify resolved rev is d8f71a4a before checking APIs against any checkout dir - Spin serve command lives in edgezero.toml + CLAUDE.md (not spin.toml); both added to files + commit - Axum KV path: executable rule - use public by-logical-id helper IF present, ELSE copy the exact .redb path algorithm + parity test + track upstream R11 - --locked: state that Run: snippets omit it for brevity; executor appends to every cargo invocation (or CARGO_NET_OFFLINE after preflight fetch)
…ins) Preflight green (cargo fetch/check --locked at d8f71a4a). D6-a confirmed: runtime key-rotation writes (only KeyRotationManager in rotation.rs) stay via composite writer + management_api.rs; reads move to EdgeZero. D5 kind-partitioned map recorded in the plan's Task 1 Output (9 store ids across kv/config/secrets).
Operator decision: resolve the app_config-vs-trusted_server_config inconsistency by declaring app_config in edgezero.toml, NOT by renaming across code/config/tests. Drops the CONFIG_BLOB_KEY/settings_data rename and the whole app_config->trusted_server_config cascade through the Viceroy generator, tests/common/config, test envs, and the Cloudflare side-channel. Only the request-signing config_store_id/secret_store_id 2-line fix remains in example.toml + fixture.
…-migration-spec # Conflicts: # Cargo.lock # Cargo.toml
…signing store ids Unify the config store id on trusted_server_config (matching every adapter manifest and edgezero.toml) while keeping the app-config blob key app_config. default_config_store_name() now yields trusted_server_config; default_config_key() derives from CONFIG_BLOB_KEY so already-pushed blobs stay readable. Point request_signing config/secret store ids at their real stores (jwks_store/signing_keys) in the example and integration fixture.
Expand the edgezero.toml [stores.*] declarations to the full logical id map (config: trusted_server_config/jwks_store/datadome-ip-bypass; kv: trusted_server_kv/ec_identity_store/consent_store/creative_store; secrets: trusted_server_secrets/signing_keys/ts_secrets/s3-auth) and expose it as a single-source-of-truth const STORES_METADATA in trusted-server-core. Every adapter's Hooks::stores() now returns that const, so the EdgeZero store registry is built identically on Fastly, Axum, Cloudflare, and Spin instead of the empty default that wired no registry. Provision the registry defaults and other declared ids so strict lookup and registry open cannot fail: add trusted_server_kv, trusted_server_secrets, s3-auth, and datadome-ip-bypass to fastly.toml and the Viceroy template, and label every kv and (KV-backed) config id in spin.toml. cloudflare.toml is left as-is (vestigial, not loaded; real bindings live in wrangler.toml). Add an anti-drift test that parses edgezero.toml and asserts each kind's ids/default match STORES_METADATA.
Record that main was merged (pin d8f71a4a->ff530286, legacy_main removed), the D5 reversal to trusted_server_config, and Task 2 completion. Fold in the two-agent review: Task 8 legacy re-scope, Task 6 non-fatal KV open, EC registry bypass correction, CF config Phase-2 boundary, cross-adapter consent coverage, clippy-cloudflare-wasm gate.
EdgeZero's manifest validator requires store ids to match [A-Za-z0-9_] so they are shell-exportable for EDGEZERO__STORES__ overrides; s3-auth and datadome-ip-bypass cannot be registry ids as-is and are not yet routed through the registry, so remove them from edgezero.toml and STORES_METADATA until their reads move onto the composite (with underscore logical ids). Also backtick EdgeZero/DataDome in composite/traits/stores docs to satisfy clippy doc_markdown.
Rename the S3 secret store s3-auth and the DataDome IP-CIDR config store datadome-ip-bypass to underscore ids (s3_auth, datadome_ip_bypass) so they are valid EdgeZero registry ids, and re-declare both in edgezero.toml and STORES_METADATA. Under D7 the logical id equals the physical store name, so rename the physical stores in fastly.toml, the viceroy template, and the Spin manifest to match; operators with differently named physical stores map them with EDGEZERO__STORES__<KIND>__<ID>__NAME. Update code defaults, tests, and the operator-facing guides accordingly.
9 tasks
Wire the EdgeZero store registries as the RuntimeServices reader on the three non-Fastly adapters, add the named-KV lookup surface to core, and migrate the consent KV surface onto KvHandle. Core: - RuntimeServices gains a kv_registry field, a builder setter, and kv_handle_named(id) to resolve a KV store by logical id. kv_handle() is unchanged and still returns the default request-path store. - ConsentPipelineInput.kv_store moves from Option<&dyn PlatformKvStore> to Option<KvHandle>, and the consent persistence functions take &KvHandle, so a store resolved from the registry can feed consent directly. - save_consent_to_kv clamps its TTL to KvHandle::MAX_TTL. EdgeZero's KvHandle rejects any TTL above one year, while the consent lifetime follows the IAB TCF norm of 13 months, so the configured TTL would otherwise fail validation and — since consent KV failures are non-fatal — silently never persist. The clamp is fail-safe (entries expire earlier, never later) and interim, pending an upstream change to raise or parameterize MAX_TTL. Adapters (Axum, Cloudflare, Spin): - Implement PlatformConfigWriter and PlatformSecretWriter for each adapter's own store types, completing the writer impls deferred from Task 3 for the orphan rule. - build_runtime_services now reads config and secrets through CompositeConfigStore and CompositeSecretStore, taking the whole registry from the request extensions so non-default logical ids resolve. An absent registry errors on first read rather than silently falling back to the default store. - Axum builds its registries from the shared STORES_METADATA using the public EdgeZero constructors and keeps AxumDevServer::with_config, preserving the PORT and axum.toml behavior. Fastly is untouched: kv_handle_named returns None there until its registry is injected, so consent keeps its current behavior.
… into core Fastly is the last adapter onto the EdgeZero store registries. It dispatches through its own oneshot, and EdgeZero's Fastly registry builders are private, so the three registries are built locally and inserted into the request extensions. Fastly: - New registries.rs builds the config, KV, and secret registries from the shared STORES_METADATA, opening every store by logical id (D7). A per-store open failure is non-fatal (warn and drop): eagerly opening every declared id and failing the request on any one of them would let a single unprovisioned or deprecated store break all traffic. Only an unopenable default id drops a registry, in which case the strict composite errors on first read rather than silently falling back. - The config registry binds the default app-config id to EdgeZero's chunk-aware FastlyConfigStore and every other id to a plain reader. EdgeZero's Fastly config store treats every value as a BlobEnvelope or a chunk pointer to one and rejects anything else as corrupt, but jwks_store and datadome_ip_bypass hold plain scalars, so reading them through it would break discovery, signature verification, key rotation, and the DataDome IP bypass. Axum's and Cloudflare's config stores are plain passthroughs; only Fastly's is envelope-only. - FastlyPlatformConfigStore and FastlyPlatformSecretStore implement the writer traits, completing the writer impls deferred from Task 3 for the orphan rule, and build_per_request_services now reads config and secrets through the registry-backed composites and populates RuntimeServices.kv_registry. Core: - consent::resolve_consent_kv resolves the configured consent store from the per-request KV registry and fails closed: configured but unresolved is an error (503 on consent-dependent routes), unconfigured is None (persistence is intentionally off). It is never a silent None, which would turn a wiring or provisioning fault into consent persistence quietly disabled. - handle_auction, handle_page_bids, and handle_publisher_request call it purely as a guard, replacing Fastly's runtime_services_for_consent_route wrapper (and its three call sites) with one core-level guard that fires on all four adapters. Consent KV persistence stays dormant and the EC identity graph is untouched. - The composite stores drive their store futures with a single poll before falling back to block_on. On Fastly the whole request already runs inside futures::executor::block_on, so a nested one aborted the instance with EnterError; the Fastly config and secret stores are synchronous under their async signature, so the first poll is always ready.
…try, writes via composite
The Axum dev server now reads app config from the EdgeZero config-store file (.edgezero/local-config-<id>.json, default store also honoring TRUSTED_SERVER_AXUM_CONFIG_PATH) and secrets from key-named environment variables via EnvSecretStore. Replace the retired TRUSTED_SERVER_CONFIG_/SECRET_ env conventions in the getting-started guide.
The consent pipeline already implemented read-fallback and write-on-change, but the only production caller passed no EC id and no KV store, so nothing persisted. Wire both: key persistence on the existing EC id, and resolve the configured consent store through the KV registry. Resolution is best-effort because this runs on every request that builds an EC context. Fail-closed enforcement for a missing consent store stays at the consent-dependent routes, so a misconfiguration cannot break unrelated ones.
…ock_on bridges
`PlatformConfigStore::get` and `PlatformSecretStore::{get_bytes,get_string}`
are now `async` under `#[async_trait(?Send)]`. Every caller on the request
path — the composite store, the consent KV helpers, `build_consent_context`,
`EcContext::read_from_request{,_with_geo}`, the request-signing endpoints,
rotation and signing paths, the proxy, publisher, and the DataDome protection
scope — propagates `.await` instead of bridging with
`futures::executor::block_on`.
Removing those inner bridges fixes a latent panic on Fastly-shaped runtimes:
a consent-persisting read driven from within an outer executor previously
aborted with `cannot execute LocalPool executor from within another executor:
EnterError`. A new regression test in `ec/mod.rs` drives a consent-persisting
`read_from_request` inside an outer `block_on(async { .. })` and asserts the
write lands; it fails with the original `EnterError` panic if any inner bridge
is reintroduced.
Store writes (`put`, `create`, `delete`) deliberately stay synchronous — they
use the platform management store id rather than the edge-visible store name,
and no request path drives them.
The remaining `block_on` calls are top-level only: the startup settings boot
in the Axum and Fastly adapters, the Fastly entry-point router driver and
response stream pump, and the post-send pull-sync dispatcher. Test call sites
across core and all four adapters wrap the now-async reads in `block_on`.
The app-config store and key disagreed across the push and boot paths: the Viceroy generator wrote the blob to an app_config store while the entry point opened trusted_server_config (which the generator left empty), and ts config push defaults the key to the logical store id, producing a third combination. Local Fastly and a normal config push therefore booted against a store that did not hold the blob. Collapse them onto one value. The store id, the blob key, and the registry binding's default_key are all trusted_server_config, which is what config push writes with no --key and what the boot read looks up. Two tests pin the contract: one asserts CONFIG_BLOB_KEY equals the declared config store default, and the generator test now asserts the emitted blob is readable at that key rather than asserting the store is empty.
Add PlatformError::NotFound so composite config/secret store reads report a genuinely-absent key separately from a store that could not be read. Key rotation and deletion now fail closed on an unreadable store instead of treating any read error as "key absent", which could mint colliding kids or delete the live signing key during a transient store outage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reconciles the EdgeZero store-registry migration with two months of main, principally PR #1036 (native secret-store config resolution) and PR #1016 (configuration-driven auction providers). Resolution highlights: - edgezero moves from the PR #306 branch pin to main's v0.0.8 tag, which contains the State<T> + nested #[secret] work the pin waited on. - Main's startup secret resolution is kept and made async end to end (settings_from_config_blob, resolve_secret_references, and get_settings_from_config_store now await PlatformSecretStore reads); the recursive path walker returns a LocalBoxFuture. - get_settings_from_config_store reads the blob through the branch's EdgeZero ConfigStoreHandle and resolves secret references through the supplied secret store; the duplicated Fastly chunk resolver stays deleted (EdgeZero's config store reassembles chunks). - Fastly boot resolves secrets through the registry-backed composite, so logical-to-physical mappings such as trusted_server_secrets -> ts_secrets apply at startup exactly as at request time. - Axum boot keeps the branch's file-backed config store and resolves secret references via the EdgeZero EnvSecretStore (env vars named exactly after the secret key); integration-test env vars renamed accordingly. - Cloudflare and Spin keep main's startup secret adapters, asyncified. - Request-time secret reads deleted by #1036 (Tinybird, DataDome server-side key, S3 credentials, per-route secret_store selectors) stay deleted; the branch's async conversions of those paths are dropped with them. - The Fastly-only consent wrapper stays replaced by the core fail-closed resolve_consent_kv guard, now also applied to page-bids after main's cross-site gate. - Hooks::stores() returns the shared STORES_METADATA on all adapters, superseding main's Fastly-only literal. - Rotation test double now reports absent keys as PlatformError::NotFound, matching the composite's not-found contract. Verified: fmt; test-fastly (2675 core + 198 fastly), test-axum, test-cloudflare, test-spin; parity 13/13; all six clippy targets; JS tests (901) and format; docs format. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…remainder Record that Phase 0 shipped in edgezero v0.0.8, Phase 1 landed in this PR, and mainline #1036 delivered Phase 3's secret externalization at startup (with the TrustedServerAppConfig wrapper and Redacted<T> retained as accepted deviations). Amend D7 to allow provisioning-time EDGEZERO__STORES__*__NAME mappings, mark D6 resolved as the write-only management path, note the legacy Fastly path is already deleted, and rescope the remaining work to full migration: the Cloudflare boot config-store read, the from_toml_and_env deletion, the EC identity graph through the registry, and the run_app/app!/extractor convergence gated on P0-C and P0-D. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four failures traced to two real causes plus two lint/docs issues: - The v0.0.8 CLI capability matrix marks the axum, cloudflare, and spin adapters Single-capable for secrets, and `ts config push` runs that check across every adapter the manifest declares — so the four-id [stores.secrets] declaration failed every push (surfaced by the template-cache ESI harness). [stores.secrets] now declares only the default id; the management-provisioned `signing_keys` store binds registry-locally through the new RUNTIME_ONLY_SECRET_IDS const in the Fastly and Axum registry builders. The retired `ts_secrets` and `s3_auth` logical ids are dropped: since #1036 those credentials resolve at startup from the default store. - The Fastly registry builders now bind each logical id to its runtime-env-mapped physical store name, so the boot-time secret resolution passes the logical default id and a trusted_server_secrets -> ts_secrets mapping resolves identically at boot and request time. - Cloudflare's v0.0.8 dispatcher treats every declared [stores.kv] id as a required binding and failed each request with a 500 when only TRUSTED_SERVER_KV existed, which the integration harness reported as "runtime not ready". wrangler.toml and wrangler.ci.toml now bind all four logical KV ids by their lowercase logical names, matching the registry lookup, and cloudflare.toml follows. Verified locally: the wordpress, nextjs, and cookie-boundary Cloudflare integration tests pass under CI=1 with a locally built worker bundle. - Clippy doc_markdown: backtick EdgeZero in the integration-test doc comment. VitePress: backtick two raw State<T> tokens that the Vue parser read as unclosed HTML elements; docs build is green locally. The ESI harness passes 21/21 locally; fmt, all six clippy targets, test-fastly/axum/cloudflare/spin, and parity are green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lease Review of both docs against the v0.0.8 tag found the P0 gating stale: P0-C (run_app_with_request_extensions pre-dispatch hook over the raw FastlyRequest, Hooks::owns_logging, multi-value Set-Cookie preservation) and P0-D (app! state = <expr> emitting RouterBuilder::with_state) both shipped in the #306 squash and are in v0.0.8, and FastlyService plus dispatch_with_registries are public (R11). Phase 4 is therefore no longer upstream-gated; the amendment now names the actual remaining trusted-server-side gaps: core response extensions dropped at the dispatch boundary, the plain-vs-envelope config split pending the still-open edgezero#324, hard-required KV ids in the upstream Fastly and Cloudflare builders, and the signing_keys runtime-only binding. Risk rows R7/R13/R14/R11 are marked resolved, edgezero#323 (MAX_TTL) is confirmed still open so the consent TTL clamp stays, and the D7 note now records that Fastly runtime-env keys are service-scoped. The Phase 1 plan gets a closing amendment: executed on PR #839, the branch-pin and lockfile-guard sections superseded by the v0.0.8 tag, Task 2's full secret id list superseded by the Single-capable check and RUNTIME_ONLY_SECRET_IDS, and Task 6's local builders narrowed to their three surviving reasons. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
EdgeZero Phase 1 — store-registry migration (design + implementation)
Migrates all runtime config / secret / KV access in Trusted Server onto EdgeZero's
store registry, across all four adapters (Fastly, Axum, Cloudflare, Spin). Config and
secret reads now resolve by logical store id through the EdgeZero registry; runtime
writes (key rotation) stay on the management path via a write-only composite (decision
D6-a). This PR carries both the design docs and the implementation.
Design
docs/superpowers/specs/2026-07-02-edgezero-full-migration-design.md— umbrella design.docs/superpowers/plans/2026-07-02-edgezero-store-registry-migration.md— the Phase-1 plan(with a post-merge amendment block reconciling it with
mainand two rounds of deep review).What changed (Tasks 1–8)
Hooks::stores()— all logical store ids declared inedgezero.toml; a sharedSTORES_METADATAconst feeds every adapter'sHooks::stores(), guarded by an anti-drift test.CompositeConfigStore/CompositeSecretStoreread via the registry (strict:absent registry and unknown id are hard errors, never a silent default), write via the
management path. Split write-only
PlatformConfigWriter/PlatformSecretWritertraits.ConfigStoreHandle;Axum/CF/Spin (5b) then Fastly (6) build
RuntimeServicesfrom the registry composite.RuntimeServices::kv_handle_named; consent migrated toKvHandle.runtime_services_for_consent_routewrapper is replaced by a core
resolve_consent_kvguard, so a configured-but-unresolved consentstore fails closed (503 on consent-dependent routes) on all four adapters. Both existing
Fastly 503 gates still pass.
s3-auth/datadome-ip-bypass→s3_auth/datadome_ip_bypass(EdgeZero store ids must be
[A-Za-z0-9_]); documentedEDGEZERO__STORES__…__NAMEmapping foroperators with differently-named physical stores.
retired the bespoke per-adapter read impls (now write-only), keeping the one Fastly management
api-keys read.
Verification
Full CI gate green:
fmt; all four test suites (test-fastly/axum/cloudflare/spin); parity 13/13;all six clippy targets (
-D warnings, native + wasm).--lockedthroughout (edgezero pinned to thePR #306 branch, which carries the Phase-0
State<T>+ nested#[secret]work).Notable fixes surfaced during the migration
KvHandle::MAX_TTL(365d) is below common retention windows; wouldhave silently stopped consent from persisting. Interim: clamp to
MAX_TTL+ warn (fail-safe);remove once upstream lands.
ConfigStore::getis envelope-only while Axum/CF are plainpassthroughs; would have 500'd plain-value config stores on Fastly only. Worked around by binding the
default (envelope) id to the chunk-aware store and non-default ids to a plain store.
block_on, because Fastly runs the whole requestinside
futures::executor::block_on(nested executor would abort withEnterError).Deferred follow-ups (deliberately out of scope for a behavior-preserving migration)
ec/mod.rspasseskv_store: None). The named-store plumbingnow exists; enabling it is a feature decision, not made here.
FastlyEcKvStoredirectly (bypasses the registry).Draft — for review.