Skip to content

Commit 1e57ad7

Browse files
committed
Add D7 config-store-only hard cutoff; fix review-5 findings
- 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
1 parent bbe5d67 commit 1e57ad7

2 files changed

Lines changed: 32 additions & 28 deletions

File tree

docs/superpowers/plans/2026-07-02-edgezero-store-registry-migration.md

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,15 @@ Deliverable: a **decision record** appended to "Task 1 Output" that Tasks 2+ con
4141
Run:
4242
```bash
4343
cd /Users/ag/projects/iab/trusted-server/.claude/worktrees/edgezero-migration-spec
44-
# KV ids (logical ids referenced by Settings — NOT fastly.toml platform stores)
45-
rg -n 'ec_store|consent_store' crates/trusted-server-core/src/settings.rs crates/trusted-server-core/src/consent_config.rs trusted-server.example.toml
44+
# KV ids (logical ids referenced by Settings — NOT Fastly-only platform stores)
45+
rg -n 'ec_store|consent_store|creative_store' crates/trusted-server-core/src/settings.rs crates/trusted-server-core/src/consent_config.rs crates/trusted-server-core/src/auction_config_types.rs trusted-server.example.toml
4646
# config ids
4747
rg -n 'config_store_id|jwks|JWKS_CONFIG_STORE_NAME|"app_config"|config_store\s*=' crates/trusted-server-core trusted-server.example.toml
4848
# secret ids
4949
rg -n 'secret_store_id|secret_store\s*=|"secrets"|ts_secrets|signing_keys|SIGNING_SECRET_STORE_NAME' crates/trusted-server-core trusted-server.example.toml
5050
rg -n '\[stores\.' edgezero.toml
5151
```
52-
Expected (verified): **KV** ids = `ec.ec_store` (`ec_identity_store`, `settings.rs:452`) + `consent.consent_store` (`consent_config.rs:80`); **config** ids = `app_config` (`request_signing.config_store_id`) + the JWKS store (`JWKS_CONFIG_STORE_NAME`); **secret** ids = `secrets` (`request_signing.secret_store_id`), DataDome `ts_secrets`, the S3 secret store, `signing_keys` (`SIGNING_SECRET_STORE_NAME`) — versus `edgezero.toml` declaring only one id per kind. NOTE: `creative_store`/`counter_store`/`opid_store` appear only in `fastly.toml` as **platform** store declarations; they are not logical ids in `Settings` and are out of scope for D5 reconciliation. Confirm this during the run.
52+
Expected (verified): **KV** ids = `ec.ec_store` (`ec_identity_store`, `settings.rs:452`), `consent.consent_store` (`consent_config.rs:80`), and `auction.creative_store` (`auction_config_types.rs:28`, default `"creative_store"`, **deprecated** — creatives are delivered inline); **config** ids = `app_config` (`request_signing.config_store_id`) + the JWKS store (`JWKS_CONFIG_STORE_NAME`); **secret** ids = `secrets` (`request_signing.secret_store_id`), DataDome `ts_secrets`, the S3 secret store, `signing_keys` (`SIGNING_SECRET_STORE_NAME`) — versus `edgezero.toml` declaring only one id per kind. NOTE: `counter_store` (`RATE_COUNTER_NAME` in the Fastly `rate_limiter.rs`) and `opid_store` are **Fastly-only** platform stores, not `Settings` logical ids — out of scope for D5. `creative_store` **is** a `Settings` id: declare it in `[stores.kv]` (deprecated) so strict lookup can't fail, and flag it for removal in a later phase.
5353

5454
- [ ] **Step 2: Enumerate runtime WRITE sites**
5555

@@ -120,7 +120,7 @@ Expected: FAIL — `ec_identity_store` (kv), `app_config`/JWKS (config), `secret
120120

121121
- [ ] **Step 3: Implement `referenced_store_ids_by_kind()` + manifest helper**
122122

123-
Add the `ReferencedStoreIds` struct + method returning KV ids (`ec.ec_store`, consent/creative/counter/opid), config ids (`request_signing.config_store_id`, `JWKS_CONFIG_STORE_NAME`, app-config), secret ids (`request_signing.secret_store_id`, DataDome, S3, `SIGNING_SECRET_STORE_NAME`). Add test-only `declared_store_ids_by_kind_from_manifest()` parsing `edgezero.toml`.
123+
Add the `ReferencedStoreIds` struct + method returning **KV** ids (`ec.ec_store`, `consent.consent_store`, `auction.creative_store`), **config** ids (`request_signing.config_store_id`, `JWKS_CONFIG_STORE_NAME`, app-config), **secret** ids (`request_signing.secret_store_id`, DataDome, S3, `SIGNING_SECRET_STORE_NAME`). Do **not** include `counter_store`/`opid_store` — those are Fastly-adapter constants, not `Settings` fields. Add test-only `declared_store_ids_by_kind_from_manifest()` parsing `edgezero.toml`.
124124

125125
- [ ] **Step 4: Update `edgezero.toml` + config fields/fixtures per the Task 1 map**
126126

@@ -244,9 +244,12 @@ Run: `cargo test-fastly get_settings_reads_blob_via_edgezero_handle` → Expecte
244244

245245
- [ ] **Step 2: Re-type `get_settings_from_config_store`** to `(&ConfigStoreHandle, key: &str)`; in Fastly `load_settings_from_config_store()` open the EdgeZero `FastlyConfigStore` at boot (`ConfigStore::open`/adapter constructor) and wrap in a `ConfigStoreHandle`; in Axum `build_state()` open the EdgeZero Axum config store. The adapter-level boot wiring is exercised by each adapter's existing `build_state` test path (no new Viceroy test needed — the core test above covers the parse logic).
246246

247-
- [ ] **Step 3: Run to verify pass** (Fastly + Axum)
247+
- [ ] **Step 3: Run to verify pass** (core test + adapter boot suites)
248248

249-
Run: `cargo test-fastly boot_config_loads_via_edgezero && cargo test-axum boot_config_loads_via_edgezero`
249+
Run: `cargo test-fastly get_settings_reads_blob_via_edgezero_handle`
250+
Expected: PASS.
251+
Then confirm the adapter boot paths still build/pass via their existing `build_state` coverage:
252+
Run: `cargo test-fastly && cargo test-axum`
250253
Expected: PASS.
251254

252255
- [ ] **Step 4: Commit**
@@ -275,7 +278,13 @@ These adapters use EdgeZero `dispatch_with_registries` (registries already inser
275278
- `datadome_reads_secret_from_nondefault_secret_store` — a request to the DataDome integration route: seed the `SecretRegistry` with two ids (default + `ts_secrets`) and the DataDome server-side key under `ts_secrets`; assert the handler reads it (proves non-default **secret** id resolution).
276279
- `first_party_proxy_reads_s3_secret``GET /first-party/proxy` for an S3-auth asset route: seed the S3 secret id; assert the SigV4 path obtains the secret (proves the S3 secret read).
277280

278-
Run: `cargo test-axum discovery_reads_jwks_from_nondefault_config_store datadome_reads_secret_from_nondefault_secret_store first_party_proxy_reads_s3_secret` → Expected: FAIL.
281+
Run each (one filter per `cargo test` invocation):
282+
```bash
283+
cargo test-axum discovery_reads_jwks_from_nondefault_config_store
284+
cargo test-axum datadome_reads_secret_from_nondefault_secret_store
285+
cargo test-axum first_party_proxy_reads_s3_secret
286+
```
287+
Expected: FAIL (all three).
279288

280289
- [ ] **Step 2: Build `RuntimeServices` via the composite** in each adapter's `build_runtime_services`, passing the whole request `ConfigRegistry`/`SecretRegistry` as the composite reader (Task 3) and keeping the existing writer.
281290

@@ -303,28 +312,16 @@ EdgeZero's Fastly `dispatch_with_registries` and its registry builders are `pub(
303312
- Test: `crates/trusted-server-adapter-fastly/src/registries.rs` (`#[cfg(test)]`) + a route test
304313

305314
**Interfaces:**
306-
- Consumes: `StoresMetadata` (from `Hooks::stores()`), `EnvConfig`, EdgeZero `FastlyConfigStore`/`FastlyKvStore`/`FastlySecretStore` open primitives, `StoreRegistry::from_parts`.
307-
- Produces: `local_env_config() -> EnvConfig` (Fastly runtime-dictionary reader, see Step 1); `build_config_registry(&StoresMetadata, &EnvConfig) -> ConfigRegistry` (+ `_secret_/_kv_` variants) matching EdgeZero's per-id name resolution (`EDGEZERO__STORES__<KIND>__<ID>__NAME`).
308-
309-
- [ ] **Step 1: Build a local `EnvConfig` reader (EdgeZero's is private).** Fastly Compute has no `std::env`; EdgeZero reads `EDGEZERO__*` from a Fastly Config Store (`env_config_from_runtime_dictionary`), which is **private** in the pinned dep (R12). Write `local_env_config()` in `registries.rs` that opens the `edgezero_runtime_env` Fastly Config Store, iterates its entries, and calls `EnvConfig::from_vars(...)`. If R11/R12 resolves by exposing a public EdgeZero helper, delete this and call that instead.
315+
- Consumes: `StoresMetadata` (from `Hooks::stores()`), EdgeZero `FastlyConfigStore`/`FastlyKvStore`/`FastlySecretStore` open primitives, `StoreRegistry::from_parts`.
316+
- Produces: `build_config_registry(&StoresMetadata) -> ConfigRegistry` (+ `_secret_/_kv_` variants) that opens each declared store **by its logical id** (per **D7** hard cutoff — no runtime env/dictionary read; platform store name == logical id).
310317

311-
```rust
312-
// registries.rs
313-
fn local_env_config() -> EnvConfig {
314-
// Mirror EdgeZero's runtime-dictionary reader: read the well-known
315-
// Fastly Config Store into (key,value) pairs, then EnvConfig::from_vars.
316-
match fastly::ConfigStore::try_open("edgezero_runtime_env") {
317-
Ok(store) => EnvConfig::from_vars(store.iter().map(|(k, v)| (k, v))),
318-
Err(_) => EnvConfig::default(),
319-
}
320-
}
321-
```
318+
- [ ] **Step 1: (D7) No runtime env reader.** Per D7 the runtime does **not** read `EDGEZERO__STORES__*__NAME` — stores are opened by **logical id**. This deletes the need for a Fastly runtime-dictionary `EnvConfig` reader (and sidesteps that `fastly::ConfigStore` has no `iter()` and EdgeZero's reader is private). If a deployment ever needs to remap a physical store name, that is handled at provisioning time, not here. No code in this step; it records the design constraint the builders follow.
322319

323-
- [ ] **Step 2: Write a failing builder test**`build_config_registry` yields a registry whose `default()` resolves and whose declared non-default id (e.g. `jwks_store`) resolves; unknown id `None`. Name: `build_config_registry_resolves_declared_ids`.
320+
- [ ] **Step 2: Write a failing builder test**`build_config_registry` opens each declared id by name and yields a registry whose `default()` resolves and whose declared non-default id (`jwks_store`) resolves; an id **not** in `StoresMetadata` is absent (`named("nope").is_none()`). Name: `build_config_registry_resolves_declared_ids`.
324321

325322
Run: `cargo test-fastly build_config_registry_resolves_declared_ids` → Expected: FAIL.
326323

327-
- [ ] **Step 3: Implement the three builders** in `registries.rs` (iterate `StoreMetadata.ids`, resolve platform name via `EnvConfig::store_name(kind, id)`, open the EdgeZero store, assemble `StoreRegistry::from_parts`), using `local_env_config()` from Step 1.
324+
- [ ] **Step 3: Implement the three builders** in `registries.rs`: iterate `StoreMetadata.ids`, open the EdgeZero Fastly store **by the logical id** (`FastlyConfigStore`/`FastlyKvStore`/`FastlySecretStore` open primitive), and assemble `StoreRegistry::from_parts(by_id, default_id)`. No `EnvConfig`, no runtime dictionary.
328325

329326
- [ ] **Step 4: Insert registries in the oneshot block** — replace the lone `core_req.extensions_mut().insert(config_store)` at `main.rs:477` with inserts of `ConfigRegistry`/`SecretRegistry`/`KvRegistry` (built via Step 3), preserving the existing `client_info`/`device_signals` inserts.
330327

docs/superpowers/specs/2026-07-02-edgezero-full-migration-design.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ Phase 1 (stores) ──> Phase 2 (config) ──> Phase 3 (secrets) Phase 4 (e
9595

9696
---
9797

98+
**D7 — Runtime app-config is config-store-only; no runtime environment variables (hard cutoff).** At runtime, every adapter builds `Settings` **solely** from the config-store blob: open store → verify envelope → deserialize (+ secret walk) → validate. There is **no runtime environment-variable overlay** for app config on any adapter. All env-var influence on config content happens at **push time**: `ts config push` (which runs on a host that has the env vars) reads them, applies the AppConfig env overlay, and bakes the resolved values into the signed blob. Consequences:
99+
- Delete `Settings::from_toml_and_env` and the `TRUSTED_SERVER__*` overlay entirely (Phase 2) — not merely unused, **forbidden** at runtime.
100+
- The AppConfig loader's `env_overlay` is a **push-time-only** option; runtime never applies it.
101+
- **Store-name binding follows the same spirit:** a logical store id resolves to the **platform store of the same name by default** (EdgeZero's `store_name` fallback), so the runtime does **not** need to read `EDGEZERO__STORES__*__NAME` env/dictionary to open stores. Adapters (incl. Fastly's custom path) open stores by **logical id**. If a deployment ever needs a different physical name, that is a **provisioning/manifest** concern resolved at deploy time, never a runtime env read. (This removes the need for a Fastly runtime-dictionary `EnvConfig` reader — see Phase 1 Task 6.)
102+
103+
---
104+
98105
## 4a. Prerequisites (must resolve before or during Phase 1/2)
99106

100107
These are not trusted-server refactors; they are EdgeZero-adapter capability gaps or up-front decisions that gate the phases.
@@ -131,7 +138,7 @@ Recommendation: **(a)** where the adapter env is available at boot (Cloudflare/S
131138
**Changes:**
132139
- **Reads (runtime + boot):** route `PlatformConfigStore`/`PlatformSecretStore` **reads** and the `RuntimeServices` config/secret fields through EdgeZero handles resolved from the per-request registries, matching KV — via a **composite store** (reads → EdgeZero, writes → existing management path; see D6-a). Also migrate the **boot-time** config load: Fastly `load_settings_from_config_store()` and Axum `build_state()` read `Settings` at boot through `&FastlyPlatformConfigStore` / `&AxumPlatformConfigStore` **before** request context exists, so those must move to a boot-time EdgeZero config read *before* the bespoke read impls are deleted. Migrate read consumers: `proxy.rs` (S3), `request_signing/{signing,rotation}.rs` (reads), `integrations/datadome/{protection,protection_scope}.rs`.
133140
- **Writes (D6):** `KeyRotationManager` writes+deletes **config and secrets at request time** (`store_private_key`/`store_public_jwk`/`delete_key` for `/_ts/admin/keys/rotate` + deactivate/delete). EdgeZero `ConfigStore`/`SecretStore` are **read-only by design**. So `management_api.rs` **cannot be deleted in Phase 1 as originally written**. Resolve per D6 before touching it.
134-
- **Store-id reconciliation (D5, expanded, kind-aware):** every runtime store id referenced by config must be declared in `edgezero.toml` under the **correct kind**`[stores.kv]`, `[stores.config]`, or `[stores.secrets]` — or strict registry lookup returns `None`. Reconcile at least: **KV**`ec.ec_store` (`ec_identity_store`), `consent.consent_store`; **config** — the app-config blob store, `request_signing.config_store_id` (`app_config` today), the JWKS/config-list store; **secrets**`request_signing.secret_store_id` (`secrets` today), DataDome secret store (`ts_secrets`), the S3 secret store — plus all `trusted-server.example.toml` + integration/test fixtures. (`creative_store`/`counter_store`/`opid_store` in `fastly.toml` are platform store declarations, **not** logical ids referenced by `Settings`.)
141+
- **Store-id reconciliation (D5, expanded, kind-aware):** every runtime store id referenced by config must be declared in `edgezero.toml` under the **correct kind**`[stores.kv]`, `[stores.config]`, or `[stores.secrets]` — or strict registry lookup returns `None`. Reconcile at least: **KV**`ec.ec_store` (`ec_identity_store`), `consent.consent_store`, `auction.creative_store` (deprecated — creatives are inline — but still a `Settings` field, so declare it to keep strict lookup safe); **config** — the app-config blob store, `request_signing.config_store_id` (`app_config` today), the JWKS/config-list store; **secrets**`request_signing.secret_store_id` (`secrets` today), DataDome secret store (`ts_secrets`), the S3 secret store — plus all `trusted-server.example.toml` + integration/test fixtures. (`counter_store`/`opid_store` are **Fastly-adapter** constants (rate limiter / opid), **not** `Settings` logical ids, and are out of scope.)
135142
- **Fastly registry injection (ties to P0-C):** Fastly's custom `oneshot` path (§1) currently inserts only a `ConfigStoreHandle`, not registries via `dispatch_with_registries`. EdgeZero's `dispatch_with_registries` and its registry builders are **`pub(crate)`** (verified in the pinned checkout), so trusted-server must build the registries **locally** (from `StoresMetadata` + `EnvConfig` + the EdgeZero Fastly store open primitives) and insert them into extensions before `oneshot` — or an EdgeZero public builder must be added upstream (**R11**).
136143
- Delete the 4× per-adapter `platform.rs` config/secret **read** impls; adapters build registries from `[stores.*]` metadata (via `dispatch_with_registries` on Axum/Cloudflare/Spin, via the Fastly-specific injection above).
137144
- Delete `settings_data.rs`'s `FastlyChunkPointer` resolver — EdgeZero's `FastlyConfigStore` resolves chunks transparently. `get_settings_from_config_store` collapses to `ConfigStore::get` + `settings_from_config_blob`.
@@ -149,7 +156,7 @@ Recommendation: **(a)** where the adapter env is available at boot (Cloudflare/S
149156
**Changes:**
150157
- Derive `AppConfig` on the config root (interim: still `TrustedServerAppConfig` until Phase 3 collapses it onto `Settings`) so all adapters use the same store-load path.
151158
- **Cloudflare** (`adapter-cloudflare/src/app.rs`) and **Spin** (`adapter-spin/src/app.rs`): replace startup config sourcing (Cloudflare's `TRUSTED_SERVER_CONFIG` env side-channel + the native `include_str!` fallback; Spin's baked example TOML) with a **boot-time config-store read** per **P-BOOT (§4a)**. `build_state()` obtains a config-store handle from the adapter env passed to `run_app` (option a) or defers to a lazy first-request cached load (option b). This is the load-bearing detail — settle the mechanism in the Phase 2 plan. Seed each platform's config store (`wrangler.toml` / `runtime-config.toml` / `fastly.toml` local blocks) with the pushed blob under the D5 store id/key.
152-
- Delete `Settings::from_toml_and_env`, `ENVIRONMENT_VARIABLE_PREFIX/SEPARATOR`, and the `config` **dev-dependency**. Any remaining env overlay uses EdgeZero's `EDGEZERO__*` / AppConfig `<APP>__…` layers.
159+
- Delete `Settings::from_toml_and_env`, `ENVIRONMENT_VARIABLE_PREFIX/SEPARATOR`, and the `config` **dev-dependency**. Per **D7 (hard cutoff)** there is **no runtime env overlay** — runtime reads the blob only. Env-var influence on config happens exclusively at **push time** via `ts config push` (AppConfig `env_overlay` applied there, then baked into the blob).
153160

154161
**Deletions:** both `include_str!` config paths, `from_toml_and_env`, `config` crate dep.
155162
**Acceptance:** Cloudflare + Spin serve with store-loaded config (no baked TOML); `ts config push` blob is the single source on all four adapters; tests green.
@@ -251,9 +258,9 @@ Two consequences: (1) edgezero #305 **must** ship `ArrayEach` + `Option<String>`
251258
| R7 | P0-C: upstream a header-preserving Fastly dispatch, or keep a permanent Fastly dispatch shim? | Decide with edgezero maintainer (§4a); gates the Fastly end-state and Phase 5. |
252259
| R8 | P-BOOT: boot-time store handle (a) vs lazy cached first-request load (b), per adapter? | Phase 2 plan (§4a). |
253260
| R9 | D5: reconcile **all** runtime store ids **by kind** — KV (`ec_identity_store`, `consent_store`), config (`app_config`, JWKS), secrets (`secrets`, DataDome `ts_secrets`, S3) + fixtures — with `edgezero.toml`; strict lookup fails otherwise. | Phase 1 plan task 1. |
254-
| R12 | Fastly `EnvConfig` reader (`env_config_from_runtime_dictionary`) is **private** upstream; Fastly has no `std::env`. Build a local runtime-dictionary reader, or make R11's public helper a prerequisite. | Phase 1 plan Task 6. |
261+
| R12 | Fastly `EnvConfig` reader is private / `fastly::ConfigStore` has no `iter()`. | **Resolved by D7**runtime opens stores by logical id; no store-name env/dictionary read; no local `EnvConfig` reader needed. |
255262
| R10 | D6: runtime write path for key rotation — keep write-capable admin abstraction (a), move to ops/CLI (b), or upstream an EdgeZero write API (c)? | **Blocks Phase 1 deletions.** Phase 1 plan locks to **(a)**; (b)/(c) → separate plan. |
256-
| R11 | Should EdgeZero expose a **public** Fastly registry-builder helper (so trusted-server need not maintain local builders)? | Decide with edgezero maintainer; Phase 1 plan uses local builders (Task 6) meanwhile. |
263+
| R11 | Should EdgeZero expose a **public** Fastly registry-builder helper? | Lower priority under D7 — local builders open by logical id and need only public store constructors. Decide with edgezero maintainer if convenient. |
257264
| R2 | `StoreName` vs `StoreId` split — still needed after `management_api.rs` deletion? | Phase 1; drop if only the CLI provision path used it. |
258265
| R3 | EC identity API + Fastly rate limiter are Fastly-only today | Out of scope here; note as a portability follow-up (not blocking). |
259266
| R4 | Cloudflare/Spin boot-time secret-store access for D3 | Confirm in Phase 3 scoping. |

0 commit comments

Comments
 (0)